Repository: gatsbyjs/wp-gatsby
Branch: master
Commit: 1004609a2295
Files: 87
Total size: 397.1 KB
Directory structure:
gitextract_1lfhn0r3/
├── .dockerignore
├── .env.dist
├── .github/
│ └── workflows/
│ ├── main.yml
│ └── tests.yml
├── .gitignore
├── .prettierrc.js
├── CHANGELOG.md
├── README.md
├── access-functions.php
├── bin/
│ ├── install-test-env.sh
│ └── run-docker.sh
├── codeception.dist.yml
├── composer.json
├── docker/
│ ├── app.Dockerfile
│ ├── app.entrypoint.sh
│ ├── testing.Dockerfile
│ └── testing.entrypoint.sh
├── docker-compose.yml
├── docs/
│ ├── action-monitor.md
│ └── running-tests.md
├── lib/
│ └── wp-settings-api.php
├── license.txt
├── readme.txt
├── src/
│ ├── ActionMonitor/
│ │ ├── ActionMonitor.php
│ │ └── Monitors/
│ │ ├── AcfMonitor.php
│ │ ├── MediaMonitor.php
│ │ ├── Monitor.php
│ │ ├── NavMenuMonitor.php
│ │ ├── PostMonitor.php
│ │ ├── PostTypeMonitor.php
│ │ ├── PreviewMonitor.php
│ │ ├── SettingsMonitor.php
│ │ ├── TaxonomyMonitor.php
│ │ ├── TermMonitor.php
│ │ └── UserMonitor.php
│ ├── Admin/
│ │ ├── Preview.php
│ │ ├── Settings.php
│ │ └── includes/
│ │ ├── no-preview-url-set.php
│ │ ├── post-type-not-shown-in-graphql.php
│ │ ├── preview-template.php
│ │ └── style.css
│ ├── GraphQL/
│ │ ├── Auth.php
│ │ └── ParseAuthToken.php
│ ├── Schema/
│ │ ├── Schema.php
│ │ ├── SiteMeta.php
│ │ └── WPGatsbyWPGraphQLSchemaChanges.php
│ ├── ThemeSupport/
│ │ └── ThemeSupport.php
│ └── Utils/
│ └── Utils.php
├── tests/
│ ├── _data/
│ │ ├── .gitignore
│ │ ├── .gitkeep
│ │ └── config.php
│ ├── _output/
│ │ ├── .gitignore
│ │ └── .gitkeep
│ ├── _support/
│ │ ├── AcceptanceTester.php
│ │ ├── FunctionalTester.php
│ │ ├── Helper/
│ │ │ ├── Acceptance.php
│ │ │ ├── Functional.php
│ │ │ ├── Unit.php
│ │ │ └── Wpunit.php
│ │ ├── UnitTester.php
│ │ ├── WpunitTester.php
│ │ └── _generated/
│ │ └── .gitignore
│ ├── acceptance.suite.dist.yml
│ ├── functional.suite.dist.yml
│ ├── wpunit/
│ │ └── ActionMonitorTest.php
│ └── wpunit.suite.dist.yml
├── vendor/
│ ├── autoload.php
│ ├── composer/
│ │ ├── ClassLoader.php
│ │ ├── LICENSE
│ │ ├── autoload_classmap.php
│ │ ├── autoload_namespaces.php
│ │ ├── autoload_psr4.php
│ │ ├── autoload_real.php
│ │ ├── autoload_static.php
│ │ └── semver/
│ │ ├── CHANGELOG.md
│ │ ├── LICENSE
│ │ ├── README.md
│ │ └── composer.json
│ └── firebase/
│ └── php-jwt/
│ ├── LICENSE
│ ├── README.md
│ ├── composer.json
│ └── src/
│ ├── BeforeValidException.php
│ ├── ExpiredException.php
│ ├── JWK.php
│ ├── JWT.php
│ └── SignatureInvalidException.php
└── wp-gatsby.php
================================================
FILE CONTENTS
================================================
================================================
FILE: .dockerignore
================================================
.env
.github_changelog_generator
.travis.yml
codeception.yml
================================================
FILE: .env.dist
================================================
DB_NAME=wordpress
DB_HOST=app_db
DB_USER=wordpress
DB_PASSWORD=wordpress
WP_TABLE_PREFIX=wp_
WP_URL=http://localhost
WP_DOMAIN=localhost
ADMIN_EMAIL=admin@example.com
ADMIN_USERNAME=admin
ADMIN_PASSWORD=password
ADMIN_PATH=/wp-admin
TEST_DB_NAME=wpgatsby_tests
TEST_DB_HOST=127.0.0.1
TEST_DB_USER=wordpress
TEST_DB_PASSWORD=wordpress
TEST_WP_TABLE_PREFIX=wp_
SKIP_DB_CREATE=false
TEST_WP_ROOT_FOLDER=/tmp/wordpress
TEST_ADMIN_EMAIL=admin@wp.test
TESTS_DIR=tests
TESTS_OUTPUT=tests/_output
TESTS_DATA=tests/_data
TESTS_SUPPORT=tests/_support
TESTS_ENVS=tests/_envs
WPGRAPHQL_VERSION=v1.1.5
SKIP_TESTS_CLEANUP=1
SUITES=wpunit
================================================
FILE: .github/workflows/main.yml
================================================
name: Deploy to WordPress.org
on:
push:
tags:
- "*"
jobs:
tag:
name: New tag
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@master
- name: WordPress Plugin Deploy
uses: 10up/action-wordpress-plugin-deploy@master
env:
SVN_PASSWORD: ${{ secrets.SVN_PASSWORD }}
SVN_USERNAME: ${{ secrets.SVN_USERNAME }}
SLUG: wp-gatsby
================================================
FILE: .github/workflows/tests.yml
================================================
name: Automated-Testing
on:
push:
branches:
- master
pull_request:
types: [opened, synchronize]
jobs:
continuous_integration:
runs-on: ubuntu-latest
strategy:
matrix:
php: ["7.3", "7.4"]
wordpress: ["5.6", "5.5.3", "5.4.2"]
include:
- php: "7.4"
wordpress: "5.6"
- php: "7.4"
wordpress: "5.5.3"
- php: "7.4"
wordpress: "5.4.2"
- php: "7.3"
wordpress: "5.6"
- php: "7.3"
wordpress: "5.5.3"
- php: "7.3"
wordpress: "5.4.2"
fail-fast: false
name: WordPress ${{ matrix.wordpress }} on PHP ${{ matrix.php }}
steps:
- name: Checkout
uses: actions/checkout@v2
- name: Install PHP
if: matrix.coverage == 1
uses: shivammathur/setup-php@v2
with:
php-version: ${{ matrix.php }}
extensions: json, mbstring
- name: Get Composer Cache Directory
id: composercache
run: echo "::set-output name=dir::$(composer config cache-files-dir)"
- name: Cache dependencies
uses: actions/cache@v2
with:
path: ${{ steps.composercache.outputs.dir }}
key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.lock') }}
restore-keys: ${{ runner.os }}-composer-
- name: Install dependencies
run: composer install --no-dev
- name: Build "testing" Docker Image
env:
PHP_VERSION: ${{ matrix.php }}
WP_VERSION: ${{ matrix.wordpress }}
USE_XDEBUG: ${{ matrix.use_xdebug }}
run: composer build-test
- name: Run Tests w/ Docker.
env:
COVERAGE: ${{ matrix.coverage }}
DEBUG: ${{ matrix.debug }}
SKIP_TESTS_CLEANUP: ${{ matrix.coverage }}
LOWEST: ${{ matrix.lowest }}
run: composer run-test
================================================
FILE: .gitignore
================================================
.DS_Store
node_modules
phpcs.xml
phpunit.xml
Thumbs.db
wp-cli.local.yml
node_modules/
*.sql
*.tar.gz
*.zip
.env
.env.*
!.env.dist
.idea
.vscode
.github_changelog_generator
!vendor/
vendor/*
!vendor/autoload.php
!vendor/composer
!vendor/firebase
!vendor/ircmaxell
vendor/composer/installed.json
vendor/composer/*/
!vendor/composer/semver/*
!tests
tests/*.suite.yml
build/
coverage/*
schema.graphql
phpunit.xml
docker-output
composer.lock
c3.php
.log/
php-coveralls.phar
codeception.yml
================================================
FILE: .prettierrc.js
================================================
module.exports = {
arrowParens: "avoid",
semi: false,
}
================================================
FILE: CHANGELOG.md
================================================
# Change Log
## 2.3.3
Fixes an issue where publicly_queryable post types that were not public weren't being tracked in action monitor even though those post types were available in Gatsby. Thanks @nickcernis! (via PR #217)
## 2.3.2
Previously Author and Contributor roles couldn't properly use Gatsby Cloud Preview. This release introduces new custom role capabilities which allow all authenticated users that can use WP preview to use Gatsby Preview.
## 2.3.1
Fixes bug in last version where not having the right ACF version installed would throw an error about `Call to undefined function "acf_get_options_pages"`
## 2.3.0
Added action monitor tracking for ACF options pages via PR #206. Thanks @henrikwirth!
## 2.2.1
Bumped the "tested to" version to latest WP version.
## 2.2.0
Added support for Gatsby Cloud Preview's new feature "eager redirects" which reduces the amount of time spent watching the preview loading screen by redirecting to the preview frontend as soon as possible and then letting the user wait for the preview to finish building there.
## 2.1.1
Changing away from the default UTC+0 timezone in WP could cause problems with local development and syncing changed data from WP. This release fixes that via PR #204.
## 2.1.0
Updated how the `gatsby_action_monitors` filter works. Previously this filter didn't properly expose the ActionMonitor class making it impossible or very difficult to add your own action monitors. Thanks @justlevine! via PR #203.
## 2.0.2
WPGraphQL made a breaking change in a minor release v1.6.7 which caused delta updates to stop working. Fixed via https://github.com/gatsbyjs/wp-gatsby/pull/201. Breaking change notice here https://github.com/wp-graphql/wp-graphql/blob/develop/readme.txt#L80-L109
## 2.0.1
- gatsby-source-wordpress v5.14.2 and v6.1.0 both support WPGatsby v2.0.0+. This release re-published v2 as the latest WPGatsby version.
## 1.1.4
- Rolling out a release to overwrite v2.0.0. gatsby-source-wordpress didn't yet have a new release allowing WPGatsby v2.0.0+ support.
## 2.0.0
We finalized support for Gatsby Cloud Content Sync Previews in this release. Content Sync is a Gatsby Cloud preview loader. Previously preview loading was handled within this plugin but we removed support for that legacy preview loader as the support burden for keeping the old and new preview logic around would be too much. Gatsby Cloud Content Sync is far more reliable than WPGatsby's preview loader as it has more context on the Gatsby build process.
For Content Sync to work you will need to upgrade to the latest version of `gatsby-source-wordpress` and either the latest `3.0.0` or `4.0.0` version of Gatsby core.
In addition this release comes with some bug fixes:
- Fixed double instantiation of ActionMonitor classes which caused double webhooks and potentially double saving of data in some cases.
- Makes preview routing more reliable by simplifying our logic and adding a `gatsby_preview=true` param to all preview links. For some users every second preview would fail to correctly route to the preview template. This is now fixed.
## 1.1.3
- The uri field was being overwritten during GraphQL requests, resulting in post uri's that included the content sync URL.
- Some logic attempting to choose the correct manifest ID instead of regenerating it was causing manifest id's to be outdated during previews.
## 1.1.2
- Fixed redirection to Gatsby Cloud Content Sync preview loader in Gutenberg
## 1.1.0
- Added support for the new Gatsby Cloud Content Sync API. This new API moves the WPGatsby Preview loader logic to the Gatsby Cloud side as Cloud has more context on the Gatsby process making it more reliable than the existing WPGatsby preview loader with fewer restrictions and caveats.
## 1.0.12
Preview webhooks added the remote url as a property on the webhook body. When publishing updates we also send a preview webhook to update the preview Gatsby site. These two webhook bodies previously differed in that the latter didn't include a remoteUrl property. As of gatsby-source-wordpress@5.10.0 this causes problems because the source plugin assumes this property always exists. Related to https://github.com/gatsbyjs/gatsby/issues/32732. Fixed in https://github.com/gatsbyjs/wp-gatsby/pull/184
## 1.0.11
- Fixed a warning state for Preview to let users know when the preview Gatsby site set in the preview webhook setting is pointing at a Gatsby site which isn't sourcing data from the current WP site. Preview requires a 1:1 connection between WP and Gatsby where settings point at a Gatsby site that sources data from the WP instance previews are originating from.
## 1.0.10
- Fixed preview loader logic for subdirectory WP installs so that we request the GraphQL endpoint from the right URL.
## 1.0.9
- Fixed a bug where draft posts weren't previewable.
## 1.0.8
- Our internal preview logic had a bug where a request was being made with double forward slashes in the url in some cases. This broke incremental builds previews but worked on regular `gatsby develop` previews. This is fixed in this release.
## 1.0.7
- Before using WPGraphQL::debug() we weren't making sure that the debug method exists on that class. This could throw errors for older versions of WPGraphQL - we now check that the method exists before using it.
- Documents using multiple webhooks support in Build and Preview webhook input field labels.
- Fixes trailing comma in MediaActionMonitor log array.
## 1.0.6
- Bump stable version tag
## 1.0.5
- Fixed our build/publish process which was failing due to using the develop branch of WPGraphQL in tests.
## 1.0.4
- In some cases the homepage was not previewable in Gatsby Preview - this is now fixed.
## 1.0.3
- Fixed `wp_save_image_file` and `wp_save_image_editor_file` callback argument count.
## 1.0.2
- An erroneous change in our composer autoload broke our first stable release 😅 bit of a rocky start but lets try this again 🤝 😁 You can bet we'll be adding a test for this 😂
## 1.0.1
- Fixed a broken link in the readme.
## 1.0.0
This plugin has come a long way over the past few months! This release introduces no changes outside of a few pages of docs. We're choosing this point to call this plugin stable as the plugin is well tested via our test suites and members of the community using it in the wild. Thanks everyone for your help and support in getting this plugin to this point!
## 0.9.2
### Bug Fixes
- The preview template loader was fixed for cases where the global $post is not set, which previously lead to PHP errors.
## 0.9.1
- Removed a new internal taxonomy from the GraphQL schema which was unintentionally added in the last release.
## 0.9.0
### Breaking Changes
- This release massively increases the performance of Gatsby Previews when more than one person is previewing or editing content at the same time. Previously when multiple users previewed simultaneously, only one of those users would see their preview or it would take a very long time for the others to see their previews. Now many users can preview concurrently. This was tested with a headless chrome puppeteer script. We found that 10 users making 100 previews over the course of a few minutes now have a 100% success rate. Previously 3 users making 30 previews would have a less than 30% success rate. This is a breaking change because `gatsby-source-wordpress-experimental` has some changes which are required to make this work.
- Previously, saving a Media item would call the build and preview webhooks. This wasn't desireable because if you upload an image to your post, that will start a build to just source that media item, then when you press publish or preview you'd have to wait for the image build to complete before being able to see your build. Now a webhook is not sent out when images are uploaded/edited and other content updates which do send a webhook will catch these image changes and apply them alongside the other changes.
## 0.8.0
### Breaking Changes
This is a breaking change release as a lot of internals for the Action Monitor class have been modified and moved around. For most users nothing will change but for those who are using our internal plugin functions/classes in their own custom code, things might break.
- Refactors Action Monitor to have separate classes for tracking activity for Acf, Media, Menus, Posts, Post Types, Settings, Taxonomies, Terms, and Users.
### Fixes and improvements
- TESTS! Lots of tests for the Action Monitors.
- JWT Secret is now set once when WPGatsby is first loaded, instead of every time the settings page is visited.
### Issues closed by this release
- [#70](https://github.com/gatsbyjs/wp-gatsby/issues/70): When field groups are saved using ACF Field Group GUI, a "Diff Schema" action is triggered
- [#58](https://github.com/gatsbyjs/wp-gatsby/issues/58): A "Refetch All" action is available and is used when Permalinks are changed
- [#57](https://github.com/gatsbyjs/wp-gatsby/issues/57): Term meta is now properly tracked when changed
- [#56](https://github.com/gatsbyjs/wp-gatsby/issues/56): Custom post types (all post types that are public and show_in_graphql) are now tracked when they are moved from publish to trash and vis-versa
- [#41](https://github.com/gatsbyjs/wp-gatsby/issues/41): Codeception tests are now in place
- [#38](https://github.com/gatsbyjs/wp-gatsby/issues/38): Many core WordPress options have been added to an allow-list and trigger a general NON_NODE_ROOT_FIELDS action. A few specific actions trigger specific actions for specific nodes. For example, changing the home_page triggers an update for the new page and the old page being changed as the URI is now different.
- [#26](https://github.com/gatsbyjs/wp-gatsby/issues/26): Posts that transition from future->publish now trigger an action (ensuring WordPress cron is triggered for WordPress sites using Gatsby front-ends might need more thought still though. . .)
- [#17](https://github.com/gatsbyjs/wp-gatsby/issues/17): Meta is now tracked for Posts, Terms and Users (comments are not currently tracked at the moment)
- [#15](https://github.com/gatsbyjs/wp-gatsby/issues/15): Saving permalinks triggers a REFETCH_ALL Action
- [#7](https://github.com/gatsbyjs/wp-gatsby/issues/7): Gatsby JWT Secret is now generated once and saved immediately and not generated again
- [#6](https://github.com/gatsbyjs/wp-gatsby/issues/6): Gatsby now tracks only post_types (and taxonomies) that are set to be both public and show_in_graphql and there are filters to override as needed.
## 0.7.3
- Small internal changes to Previews to facilitate e2e tests.
## 0.7.2
- Version 0.7.0 introduced a change which resulted in Previews for some WP instances being overwritten by published posts on each preview.
## 0.7.1
- The last version added some internal taxonomies to the GraphQL schema unintentionally. This release removes them.
## 0.7.0
### Breaking Changes
- Previously we were storing a brand new post internally for every content-related action that happened in your site. As of this release we only make a single action post for each post you take actions against and update it each time instead of creating a new one.
## 0.6.8
- The `NO_PAGE_CREATED_FOR_PREVIEWED_NODE` preview status was no longer making it through to the preview template because we were checking if the preview had deployed before checking if a page had been created in Gatsby for the preview. this release fixes that.
- The preview-template.php check for wether or not the preview frontend is online could occasionally come back with a false negative. It is now more resilient and will recheck for 10 seconds before showing an error.
- The above check used to throw harmless CORS errors in the console, this check is now done server-side so that CORS isn't an issue.
## 0.6.7
- Gatsby Preview process errors were not coming through for new post drafts. They do now :)
- I was checking if the Gatsby webhook hit by WPGatsby returned any errors and displaying an error in the preview client if it did. It turns out this is problematic because the webhook can return errors in WPGatsby and yet Gatsby can still have successfully received it. So the logic is now more optimistic and tries to load the preview regardless of wether or not we received an error when posting to the webhook.
## 0.6.6
- Fixed a timing issue between Previews and WPGatsby. WPGatsby now reads the page-data.json of the page being previewed in order to determine wether or not it's been deployed.
- Added publish webhooks for Preview so that polling is not needed in Gatsby Preview on the source plugin side.
## 0.6.5
- Improved garbage collection of old action monitor posts. Garbage collection previously took over 20 seconds to clean up 6,204 action_monitor actions, after this change it takes approximately 1/10 of a second.
## 0.6.4
- Extended WPGatsby JWT expiry to 1 hour. It was previously 30 seconds which can be problematic for slower servers and Gatsby setups.
## 0.6.3
- graphql_single_name's that start with a capital letter were causing issues because WPGatsby was not making the first character lowercase but WPGraphQL does do this when adding the field to schema.
## 0.6.2
- More PHP 7.1 syntax fixes. We will soon have CI tests which will prevent these issues.
## 0.6.1
- Fixed an unexpected token syntax error.
## 0.6.0
- This release adds a major re-work to the Gatsby Preview experience! It adds remote Gatsby state management and error handling in WordPress so that WP and Gatsby don't get out of sync during the Preview process.
## 0.5.4
- Force enable WPGraphQL Introspection when WPGatsby is enabled. [WPGraphQL v0.14.0](https://github.com/wp-graphql/wp-graphql/releases/tag/v0.14.0) has Introspection disabled by default with an option to enable it, and Gatsby requires it to be enabled, so WPGatsby force-enables it.
## 0.5.3
- Meta delta syncing was using the same code for posts and users. In many cases this was causing errors when updating usermeta. This code is now scoped to posts only and we will add usermeta delta syncing separately.
- Our composer setup was previously double autoloading
## 0.5.2
- Added a backwards compatibility fix for a regression introduced in v0.4.18 where WPGraphQL::debug() was called. This method is only available in later versions of WPGraphQL, but this plugin currently supports earlier versions
## 0.5.1
- Fixed a typo in the new footer locations 🤦♂️ gatbsy should be gatsby
## 0.5.0
### Bug Fixes
- Added support for delta syncing menu locations. This appeared as a bug where updating your menu locations didn't update in Gatsby, but this was actually a missing feature.
## 0.4.18
### Bug Fixes
- The action_monitor post type was registered incorrectly so that it was showing in the rest api, in search, and other places it didn't need to be. This release fixes that. Thanks @jasonbahl!
## 0.4.17
### New Features
- Added `WPGatsby.arePrettyPermalinksEnabled` to the schema in order to add more helpful error messages to the Gatsby build process.
- Added a filter `gatsby_trigger_dispatch_args` to filter the arguments passed to `wp_safe_remote_post` when triggering webhooks.
## 0.4.16
### Bug Fixes
It turns out the new feature in the last release could potentially cause many more issues than it presently solves, so it has been disabled as a bug fix. This will be re-enabled within the next couple weeks as we do more testing and thinking on how best to approach sending WP options events to Gatsby.
## 0.4.15
### New Features
- Non-node root fields (options and settings) are now recorded as an action so Gatsby can inc build when the site title changes for example.
## 0.4.14
### Bug Fixes
- Making a post into a draft was not previously saving an action monitor post which means posts that became drafts would never be deleted.
## 0.4.13
### Bug Fixes
- the ContentType.archivePath field was returning an empty string instead of `/` for a homepage archive.
## 0.4.12
### New Features
- Added temporary `ContentType.archivePath` and `Taxonomy.archivePath` fields to the schema until WPGraphQL supports these fields.
## 0.4.11
### Bug Fixes
- get_home_url() was being used where get_site_url() should've been used, causing the gql endpoint to not be referenced correctly in some situations. For example when using Bedrock.
## 0.4.10
### Bug Fixes
- The Preview fix in the last release introduced a new bug where saving a draft at any time would send a webhook to the Preview instance.
## 0.4.9
### Bug Fixes
- Preview wasn't working properly for new posts that hadn't yet been published or for drafts.
## 0.4.8
Pushing release to WordPress.org
## 0.4.7
### New Features
- Added a link to the GatsbyJS settings page on how to configure this plugin.
### Bug Fixes
- Activating this plugin before WPGraphQL was causing PHP errors.
## 0.4.6
Add Wapuu Icons for display in the WordPress.org repo
## 0.4.5
Re-publish with proper package name
## 0.4.4
Testing Github Actions
## 0.4.3
New release to trigger publishing to WordPress.org!
## 0.4.2
### Bug Fixes
- Previously when a post transitioned from published to draft, it wouldn't be deleted in Gatsby
## 0.4.1
Version bump to add /vendor directory to Git so that Github releases work as WP plugins without running `composer install`. In the future there will be a better release process, but for now this works.
## 0.4.0
### Breaking Changes
- WPGraphQL was using nav_menu for it's menu relay id's instead of term. WPGQL 0.9.1 changes this from nav_menu to term. This is a breaking change because cache invalidation wont work properly if the id is incorrect. So we move to v0.4.0 so gatsby-source-wordpress-experimental can set 0.4.0 as it's min version and cache invalidation will keep working.
## 0.3.0
### Breaking Changes
- Updated Relay ids to be compatible with WPGraphQL 0.9.0. See https://github.com/wp-graphql/wp-graphql/releases/tag/v0.9.0 for more info.
- Bumped min PHP and WP versions
## 0.2.6
### Bug fixes
Fixed an issue where we were trying to access post object properties when we didn't yet have the post.
## 0.2.5
### Bug Fixes
Earlier versions of WPGatsby were recording up to 4 duplicate content saves per content change in WordPress. This release stops that from happening. WPGatsby does garbage collection, so any duplicate actions will be automatically removed from your DB.
================================================
FILE: README.md
================================================
# WPGatsby
WPGatsby is a free open-source WordPress plugin that optimizes your WordPress site to work as a data source for [Gatsby](https://www.gatsbyjs.com/docs/how-to/sourcing-data/sourcing-from-wordpress).
This plugin must be used in combination with the npm package [`gatsby-source-wordpress@>=4.0.0`](https://www.npmjs.com/package/gatsby-source-wordpress).
## Install and Activation
WPGatsby is available on the WordPress.org repository and can be installed from your WordPress dashboard, or by using any other plugin installation method you prefer, such as installing with Composer from wpackagist.org.
## Plugin Overview
This plugin has 2 primary responsibilities:
- [Monitor Activity in WordPress to keep Gatsby in sync with WP](https://github.com/gatsbyjs/wp-gatsby/blob/master/docs/action-monitor.md)
- [Configure WordPress Previews to work with Gatsby](https://github.com/gatsbyjs/gatsby/blob/master/packages/gatsby-source-wordpress/docs/tutorials/configuring-wp-gatsby.md#setting-up-preview)
Additionally, WPGatsby has a settings page to connect your WordPress site with your Gatsby site:
- [WPGatsby Settings](https://github.com/gatsbyjs/gatsby/blob/master/packages/gatsby-source-wordpress/docs/tutorials/configuring-wp-gatsby.md)
================================================
FILE: access-functions.php
================================================
"$2";
elif [ `which wget` ]; then
wget -nv -O "$2" "$1"
fi
}
if [[ $WP_VERSION =~ ^[0-9]+\.[0-9]+\-(beta|RC)[0-9]+$ ]]; then
WP_BRANCH=${WP_VERSION%\-*}
WP_TESTS_TAG="branches/$WP_BRANCH"
elif [[ $WP_VERSION =~ ^[0-9]+\.[0-9]+$ ]]; then
WP_TESTS_TAG="branches/$WP_VERSION"
elif [[ $WP_VERSION =~ [0-9]+\.[0-9]+\.[0-9]+ ]]; then
if [[ $WP_VERSION =~ [0-9]+\.[0-9]+\.[0] ]]; then
# version x.x.0 means the first release of the major version, so strip off the .0 and download version x.x
WP_TESTS_TAG="tags/${WP_VERSION%??}"
else
WP_TESTS_TAG="tags/$WP_VERSION"
fi
elif [[ $WP_VERSION == 'nightly' || $WP_VERSION == 'trunk' ]]; then
WP_TESTS_TAG="trunk"
else
# http serves a single offer, whereas https serves multiple. we only want one
download http://api.wordpress.org/core/version-check/1.7/ /tmp/wp-latest.json
grep '[0-9]+\.[0-9]+(\.[0-9]+)?' /tmp/wp-latest.json
LATEST_VERSION=$(grep -o '"version":"[^"]*' /tmp/wp-latest.json | sed 's/"version":"//')
if [[ -z "$LATEST_VERSION" ]]; then
echo "Latest WordPress version could not be found"
exit 1
fi
WP_TESTS_TAG="tags/$LATEST_VERSION"
fi
set -ex
install_wp() {
if [ -d $WP_CORE_DIR ]; then
return;
fi
mkdir -p $WP_CORE_DIR
if [[ $WP_VERSION == 'nightly' || $WP_VERSION == 'trunk' ]]; then
mkdir -p $TMPDIR/wordpress-nightly
download https://wordpress.org/nightly-builds/wordpress-latest.zip $TMPDIR/wordpress-nightly/wordpress-nightly.zip
unzip -q $TMPDIR/wordpress-nightly/wordpress-nightly.zip -d $TMPDIR/wordpress-nightly/
mv $TMPDIR/wordpress-nightly/wordpress/* $WP_CORE_DIR
else
if [ $WP_VERSION == 'latest' ]; then
local ARCHIVE_NAME='latest'
elif [[ $WP_VERSION =~ [0-9]+\.[0-9]+ ]]; then
# https serves multiple offers, whereas http serves single.
download https://api.wordpress.org/core/version-check/1.7/ $TMPDIR/wp-latest.json
if [[ $WP_VERSION =~ [0-9]+\.[0-9]+\.[0] ]]; then
# version x.x.0 means the first release of the major version, so strip off the .0 and download version x.x
LATEST_VERSION=${WP_VERSION%??}
else
# otherwise, scan the releases and get the most up to date minor version of the major release
local VERSION_ESCAPED=`echo $WP_VERSION | sed 's/\./\\\\./g'`
LATEST_VERSION=$(grep -o '"version":"'$VERSION_ESCAPED'[^"]*' $TMPDIR/wp-latest.json | sed 's/"version":"//' | head -1)
fi
if [[ -z "$LATEST_VERSION" ]]; then
local ARCHIVE_NAME="wordpress-$WP_VERSION"
else
local ARCHIVE_NAME="wordpress-$LATEST_VERSION"
fi
else
local ARCHIVE_NAME="wordpress-$WP_VERSION"
fi
download https://wordpress.org/${ARCHIVE_NAME}.tar.gz $TMPDIR/wordpress.tar.gz
tar --strip-components=1 -zxmf $TMPDIR/wordpress.tar.gz -C $WP_CORE_DIR
fi
download https://raw.github.com/markoheijnen/wp-mysqli/master/db.php $WP_CORE_DIR/wp-content/db.php
}
install_db() {
if [ ${SKIP_DB_CREATE} = "true" ]; then
return 0
fi
# parse DB_HOST for port or socket references
local PARTS=(${DB_HOST//\:/ })
local DB_HOSTNAME=${PARTS[0]};
local DB_SOCK_OR_PORT=${PARTS[1]};
local EXTRA=""
if ! [ -z $DB_HOSTNAME ] ; then
if [ $(echo $DB_SOCK_OR_PORT | grep -e '^[0-9]\{1,\}$') ]; then
EXTRA=" --host=$DB_HOSTNAME --port=$DB_SOCK_OR_PORT --protocol=tcp"
elif ! [ -z $DB_SOCK_OR_PORT ] ; then
EXTRA=" --socket=$DB_SOCK_OR_PORT"
elif ! [ -z $DB_HOSTNAME ] ; then
EXTRA=" --host=$DB_HOSTNAME --protocol=tcp"
fi
fi
# create database
RESULT=`mysql -u $DB_USER --password="$DB_PASS" --skip-column-names -e "SHOW DATABASES LIKE '$DB_NAME'"$EXTRA`
if [ "$RESULT" != $DB_NAME ]; then
mysqladmin create $DB_NAME --user="$DB_USER" --password="$DB_PASS"$EXTRA
fi
}
configure_wordpress() {
cd $WP_CORE_DIR
wp config create --dbname="$DB_NAME" --dbuser="$DB_USER" --dbpass="$DB_PASS" --dbhost="$DB_HOST" --skip-check --force=true
wp core install --url=wp.test --title="WPGatsby Tests" --admin_user=admin --admin_password=password --admin_email=admin@wp.test
wp rewrite structure '/%year%/%monthnum%/%postname%/'
}
setup_wpgraphql() {
if [ ! -d $WP_CORE_DIR/wp-content/plugins/wp-graphql ]; then
echo "Cloning WPGraphQL"
wp plugin install https://github.com/wp-graphql/wp-graphql/releases/download/${WPGRAPHQL_VERSION}/wp-graphql.zip
fi
echo "Activating WPGraphQL"
wp plugin activate wp-graphql
}
setup_plugin() {
# Add this repo as a plugin to the repo
if [ ! -d $WP_CORE_DIR/wp-content/plugins/wp-gatsby ]; then
ln -s $PLUGIN_DIR $WP_CORE_DIR/wp-content/plugins/wp-gatsby
cd $WP_CORE_DIR/wp-content/plugins
pwd
ls
fi
cd $WP_CORE_DIR
wp plugin list
# activate the plugin
wp plugin activate wp-gatsby
# Flush the permalinks
wp rewrite flush
# Export the db for codeception to use
wp db export $PLUGIN_DIR/tests/_data/dump.sql
}
install_wp
install_db
configure_wordpress
setup_wpgraphql
setup_plugin
================================================
FILE: bin/run-docker.sh
================================================
#!/usr/bin/env bash
set -eu
##
# Use this script through Composer scripts in the package.json.
# To quickly build and run the docker-compose scripts for an app or automated testing
# run the command below after run `composer install --no-dev` with the respectively
# flag for what you need.
##
print_usage_instructions() {
echo "Usage: composer build-and-run -- [-a|-t]";
echo " -a Spin up a WordPress installation.";
echo " -t Run the automated tests.";
exit 1
}
if [ -z "$1" ]; then
print_usage_instructions
fi
env_file=".env.dist";
subcommand=$1; shift
case "$subcommand" in
"build" )
while getopts ":at" opt; do
case ${opt} in
a )
docker build -f docker/app.Dockerfile \
-t wpgatsby-app:latest \
--build-arg WP_VERSION=${WP_VERSION-5.4} \
--build-arg PHP_VERSION=${PHP_VERSION-7.4} \
.
;;
t )
docker build -f docker/app.Dockerfile \
-t wpgatsby-app:latest \
--build-arg WP_VERSION=${WP_VERSION-5.4} \
--build-arg PHP_VERSION=${PHP_VERSION-7.4} \
.
docker build -f docker/testing.Dockerfile \
-t wpgatsby-testing:latest \
--build-arg USE_XDEBUG=${USE_XDEBUG-} \
.
;;
\? ) print_usage_instructions;;
* ) print_usage_instructions;;
esac
done
shift $((OPTIND -1))
;;
"run" )
while getopts "e:at" opt; do
case ${opt} in
e )
env_file=${OPTARG};
if [ ! -f $env_file ]; then
echo "No file found at $env_file"
fi
;;
a ) docker-compose up --scale testing=0;;
t )
source ${env_file}
docker-compose run --rm \
-e SUITES=${SUITES-wpunit} \
-e COVERAGE=${COVERAGE-} \
-e DEBUG=${DEBUG-} \
-e SKIP_TESTS_CLEANUP=${SKIP_TESTS_CLEANUP-} \
-e LOWEST=${LOWEST-} \
testing --scale app=0
;;
\? ) print_usage_instructions;;
* ) print_usage_instructions;;
esac
done
shift $((OPTIND -1))
;;
\? ) print_usage_instructions;;
* ) print_usage_instructions;;
esac
================================================
FILE: codeception.dist.yml
================================================
paths:
tests: '%TESTS_DIR%'
output: '%TESTS_OUTPUT%'
data: '%TESTS_DATA%'
support: '%TESTS_SUPPORT%'
envs: '%TESTS_ENVS%'
params:
- env
- .env.dist
actor_suffix: Tester
settings:
colors: true
memory_limit: 1024M
coverage:
enabled: true
remote: false
c3_url: '%WP_URL%/wp-content/plugins/wp-gatsby/wp-gatsby.php'
include:
- includes/*
exclude:
- wp-gatsby.php
- vendor/*
show_only_summary: false
extensions:
enabled:
- Codeception\Extension\RunFailed
commands:
- Codeception\Command\GenerateWPUnit
- Codeception\Command\GenerateWPRestApi
- Codeception\Command\GenerateWPRestController
- Codeception\Command\GenerateWPRestPostTypeController
- Codeception\Command\GenerateWPAjax
- Codeception\Command\GenerateWPCanonical
- Codeception\Command\GenerateWPXMLRPC
modules:
config:
WPDb:
dsn: 'mysql:host=%DB_HOST%;dbname=%DB_NAME%'
user: '%DB_USER%'
password: '%DB_PASSWORD%'
populator: 'mysql -u $user -p$password -h $host $dbname < $dump'
dump: 'tests/_data/dump.sql'
populate: false
cleanup: true
waitlock: 0
url: '%WP_URL%'
urlReplacement: true
tablePrefix: '%WP_TABLE_PREFIX%'
WPBrowser:
url: '%WP_URL%'
wpRootFolder: '%WP_ROOT_FOLDER%'
adminUsername: '%ADMIN_USERNAME%'
adminPassword: '%ADMIN_PASSWORD%'
adminPath: '/wp-admin'
cookies: false
REST:
depends: WPBrowser
url: '%WP_URL%'
WPFilesystem:
wpRootFolder: '%WP_ROOT_FOLDER%'
plugins: '/wp-content/plugins'
mu-plugins: '/wp-content/mu-plugins'
themes: '/wp-content/themes'
uploads: '/wp-content/uploads'
WPLoader:
wpRootFolder: '%WP_ROOT_FOLDER%'
dbName: '%DB_NAME%'
dbHost: '%DB_HOST%'
dbUser: '%DB_USER%'
dbPassword: '%DB_PASSWORD%'
tablePrefix: '%WP_TABLE_PREFIX%'
domain: '%WP_DOMAIN%'
adminEmail: '%ADMIN_EMAIL%'
title: 'Test'
plugins:
- wp-graphql/wp-graphql.php
- wp-gatsby/wp-gatsby.php
activatePlugins:
- wp-graphql/wp-graphql.php
- wp-gatsby/wp-gatsby.php
configFile: 'tests/_data/config.php'
================================================
FILE: composer.json
================================================
{
"name": "gatsbyjs/wp-gatsby",
"description": "Optimize your WordPress site as a source for Gatsby site(s)",
"type": "wordpress-plugin",
"license": "GPL-3.0-or-later",
"authors": [
{
"name": "GatsbyJS"
},
{
"name": "Jason Bahl"
},
{
"name": "Tyler Barnes"
}
],
"autoload": {
"psr-4": {
"WPGatsby\\": "src/"
}
},
"autoload-dev": {
"files": [
"tests/_data/config.php"
]
},
"config": {
"optimize-autoloader": true,
"process-timeout": 0
},
"require": {
"php": "^7.3||^8.0",
"firebase/php-jwt": "^5.2",
"ircmaxell/random-lib": "^1.2",
"composer/semver": "^1.5"
},
"require-dev": {
"lucatume/wp-browser": "^2.4",
"codeception/module-asserts": "^1.0",
"codeception/module-phpbrowser": "^1.0",
"codeception/module-webdriver": "^1.0",
"codeception/module-db": "^1.0",
"codeception/module-filesystem": "^1.0",
"codeception/module-cli": "^1.0",
"codeception/util-universalframework": "^1.0",
"dealerdirect/phpcodesniffer-composer-installer": "^0.7.1",
"wp-coding-standards/wpcs": "2.1.1",
"phpcompatibility/phpcompatibility-wp": "2.1.0",
"squizlabs/php_codesniffer": "3.5.4",
"codeception/module-rest": "^1.2",
"wp-graphql/wp-graphql-testcase": "^1.0",
"phpunit/phpunit": "9.4.1"
},
"scripts": {
"install-test-env": "bash bin/install-test-env.sh",
"docker-build": "bash bin/run-docker.sh build",
"docker-run": "bash bin/run-docker.sh run",
"docker-destroy": "docker-compose down",
"build-and-run": [
"@docker-build",
"@docker-run"
],
"build-app": "@docker-build -a",
"build-test": "@docker-build -t",
"run-app": "@docker-run -a",
"run-test": "@docker-run -t",
"lint": "vendor/bin/phpcs",
"phpcs-i": [
"php ./vendor/bin/phpcs -i"
],
"check-cs": [
"php ./vendor/bin/phpcs src"
],
"fix-cs": [
"php ./vendor/bin/phpcbf src"
]
},
"support": {
"issues": "https://github.com/gatsbyjs/wp-gatsby/issues",
"source": "https://github.com/gatsbyjs/wp-gatsby"
}
}
================================================
FILE: docker/app.Dockerfile
================================================
###############################################################################
# Pre-configured WordPress Installation w/ WPGraphQL, WPGatsby #
# For testing only, use in production not recommended. #
###############################################################################
ARG WP_VERSION
ARG PHP_VERSION
FROM wordpress:${WP_VERSION}-php${PHP_VERSION}-apache
ENV WP_VERSION=${WP_VERSION}
ENV PHP_VERSION=${PHP_VERSION}
LABEL author=jasonbahl
LABEL author_uri=https://github.com/jasonbahl
SHELL [ "/bin/bash", "-c" ]
# Install system packages
RUN apt-get update && \
apt-get -y install \
# CircleCI depedencies
git \
ssh \
tar \
gzip \
wget \
mariadb-client
# Install Dockerize
ENV DOCKERIZE_VERSION v0.6.1
RUN wget https://github.com/jwilder/dockerize/releases/download/$DOCKERIZE_VERSION/dockerize-linux-amd64-$DOCKERIZE_VERSION.tar.gz \
&& tar -C /usr/local/bin -xzvf dockerize-linux-amd64-$DOCKERIZE_VERSION.tar.gz \
&& rm dockerize-linux-amd64-$DOCKERIZE_VERSION.tar.gz
# Install WP-CLI
RUN curl -O https://raw.githubusercontent.com/wp-cli/builds/gh-pages/phar/wp-cli.phar \
&& chmod +x wp-cli.phar \
&& mv wp-cli.phar /usr/local/bin/wp
# Set project environmental variables
ENV WP_ROOT_FOLDER="/var/www/html"
ENV WORDPRESS_DB_HOST=${DB_HOST}
ENV WORDPRESS_DB_USER=${DB_USER}
ENV WORDPRESS_DB_PASSWORD=${DB_PASSWORD}
ENV WORDPRESS_DB_NAME=${DB_NAME}
ENV PLUGINS_DIR="${WP_ROOT_FOLDER}/wp-content/plugins"
ENV PROJECT_DIR="${PLUGINS_DIR}/wp-gatsby"
# Remove exec statement from base entrypoint script.
RUN sed -i '$d' /usr/local/bin/docker-entrypoint.sh
# Set up Apache
RUN echo 'ServerName localhost' >> /etc/apache2/apache2.conf
# Set up entrypoint
WORKDIR /var/www/html
COPY docker/app.entrypoint.sh /usr/local/bin/app-entrypoint.sh
RUN chmod 755 /usr/local/bin/app-entrypoint.sh
ENTRYPOINT ["app-entrypoint.sh"]
CMD ["apache2-foreground"]
================================================
FILE: docker/app.entrypoint.sh
================================================
#!/bin/bash
# Run WordPress docker entrypoint.
. docker-entrypoint.sh 'apache2'
set +u
# Ensure mysql is loaded
dockerize -wait tcp://${DB_HOST}:${DB_HOST_PORT:-3306} -timeout 1m
# Config WordPress
if [ ! -f "${WP_ROOT_FOLDER}/wp-config.php" ]; then
wp config create \
--path="${WP_ROOT_FOLDER}" \
--dbname="${DB_NAME}" \
--dbuser="${DB_USER}" \
--dbpass="${DB_PASSWORD}" \
--dbhost="${DB_HOST}" \
--dbprefix="${WP_TABLE_PREFIX}" \
--skip-check \
--quiet \
--allow-root
fi
# Install WP if not yet installed
if ! $( wp core is-installed --allow-root ); then
wp core install \
--path="${WP_ROOT_FOLDER}" \
--url="${WP_URL}" \
--title='Test' \
--admin_user="${ADMIN_USERNAME}" \
--admin_password="${ADMIN_PASSWORD}" \
--admin_email="${ADMIN_EMAIL}" \
--allow-root
fi
# Install and activate WPGraphQL
if [ ! -f "${PLUGINS_DIR}/wp-graphql/wp-graphql.php" ]; then
wp plugin install \
https://github.com/wp-graphql/wp-graphql/archive/${WPGRAPHQL_VERSION}.zip \
--activate --allow-root
else
wp plugin activate wp-graphql --allow-root
fi
# Install and activate WPGatsby
wp plugin activate wp-gatsby --allow-root
# Set pretty permalinks.
wp rewrite structure '/%year%/%monthnum%/%postname%/' --allow-root
wp db export "${PROJECT_DIR}/tests/_data/dump.sql" --allow-root
exec "$@"
================================================
FILE: docker/testing.Dockerfile
================================================
############################################################################
# Container for running Codeception tests on a WooGraphQL Docker instance. #
############################################################################
# Using the 'DESIRED_' prefix to avoid confusion with environment variables of the same name.
FROM wpgatsby-app:latest
LABEL author=jasonbahl
LABEL author_uri=https://github.com/jasonbahl
SHELL [ "/bin/bash", "-c" ]
# Redeclare ARGs and set as environmental variables for reuse.
ARG USE_XDEBUG
ENV USING_XDEBUG=${USE_XDEBUG}
# Install php extensions
RUN docker-php-ext-install pdo_mysql
# Install composer
ENV COMPOSER_ALLOW_SUPERUSER=1
RUN curl -sS https://getcomposer.org/installer | php -- \
--filename=composer \
--install-dir=/usr/local/bin
# Add composer global binaries to PATH
ENV PATH "$PATH:~/.composer/vendor/bin"
# Configure php
RUN echo "date.timezone = UTC" >> /usr/local/etc/php/php.ini
# Remove exec statement from base entrypoint script.
RUN sed -i '$d' /usr/local/bin/app-entrypoint.sh
# Set up entrypoint
WORKDIR /var/www/html/wp-content/plugins/wp-gatsby
COPY docker/testing.entrypoint.sh /usr/local/bin/testing-entrypoint.sh
RUN chmod 755 /usr/local/bin/testing-entrypoint.sh
ENTRYPOINT ["testing-entrypoint.sh"]
================================================
FILE: docker/testing.entrypoint.sh
================================================
#!/bin/bash
# Processes parameters and runs Codeception.
run_tests() {
echo "Running Tests"
if [[ -n "$COVERAGE" ]]; then
local coverage="--coverage --coverage-xml"
fi
if [[ -n "$DEBUG" ]]; then
local debug="--debug"
fi
local suites=${1:-" ;"}
IFS=';' read -ra target_suites <<< "$suites"
for suite in "${target_suites[@]}"; do
vendor/bin/codecept run -c codeception.dist.yml ${suite} ${coverage:-} ${debug:-} --no-exit
done
}
# Exits with a status of 0 (true) if provided version number is higher than proceeding numbers.
version_gt() {
test "$(printf '%s\n' "$@" | sort -V | head -n 1)" != "$1";
}
write_htaccess() {
echo "
RewriteEngine On
RewriteBase /
SetEnvIf Authorization \"(.*)\" HTTP_AUTHORIZATION=\$1
RewriteRule ^index\.php$ - [L]
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule . /index.php [L]
" >> ${WP_ROOT_FOLDER}/.htaccess
}
# Move to WordPress root folder
workdir="$PWD"
echo "Moving to WordPress root directory."
cd ${WP_ROOT_FOLDER}
# Run app entrypoint script.
. app-entrypoint.sh
write_htaccess
# Return to PWD.
echo "Moving back to project working directory."
cd ${workdir}
# Ensure Apache is running
service apache2 start
# Ensure everything is loaded
dockerize \
-wait tcp://${DB_HOST}:${DB_HOST_PORT:-3306} \
-wait ${WP_URL} \
-timeout 1m
# Download c3 for testing.
if [ ! -f "$PROJECT_DIR/c3.php" ]; then
echo "Downloading Codeception's c3.php"
curl -L 'https://raw.github.com/Codeception/c3/2.0/c3.php' > "$PROJECT_DIR/c3.php"
fi
if [[ -n "$LOWEST" ]]; then
PREFER_LOWEST="--prefer-source"
fi
# Install dependencies
COMPOSER_MEMORY_LIMIT=-1 composer update --prefer-source ${PREFER_LOWEST}
COMPOSER_MEMORY_LIMIT=-1 composer install --prefer-source --no-interaction
# Install pcov/clobber if PHP7.1+
if version_gt $PHP_VERSION 7.0 && [[ -n "$COVERAGE" ]] && [[ -z "$USING_XDEBUG" ]]; then
echo "Installing pcov/clobber"
COMPOSER_MEMORY_LIMIT=-1 composer require --dev pcov/clobber
vendor/bin/pcov clobber
elif [[ -n "$COVERAGE" ]]; then
echo "Using XDebug for codecoverage"
fi
# Set output permission
echo "Setting Codeception output directory permissions"
chmod 777 ${TESTS_OUTPUT}
# Run tests
run_tests ${SUITES}
# Remove c3.php
if [ -f "$PROJECT_DIR/c3.php" ] && [ "$SKIP_TESTS_CLEANUP" != "1" ]; then
echo "Removing Codeception's c3.php"
rm -rf "$PROJECT_DIR/c3.php"
fi
# Clean coverage.xml and clean up PCOV configurations.
if [ -f "${TESTS_OUTPUT}/coverage.xml" ] && [[ -n "$COVERAGE" ]]; then
echo 'Cleaning coverage.xml for deployment'.
pattern="$PROJECT_DIR/"
sed -i "s~$pattern~~g" "$TESTS_OUTPUT"/coverage.xml
# Remove pcov/clobber
if version_gt $PHP_VERSION 7.0 && [[ -z "$SKIP_TESTS_CLEANUP" ]] && [[ -z "$USING_XDEBUG" ]]; then
echo 'Removing pcov/clobber.'
vendor/bin/pcov unclobber
COMPOSER_MEMORY_LIMIT=-1 composer remove --dev pcov/clobber
fi
fi
if [[ -z "$SKIP_TESTS_CLEANUP" ]]; then
echo 'Changing composer configuration in container.'
composer config --global discard-changes true
echo 'Removing devDependencies.'
composer install --no-dev -n
echo 'Removing composer.lock'
rm composer.lock
fi
# Set public test result files permissions.
if [ -n "$(ls "$TESTS_OUTPUT")" ]; then
echo 'Setting result files permissions'.
chmod 777 -R "$TESTS_OUTPUT"/*
fi
# Check results and exit accordingly.
if [ -f "${TESTS_OUTPUT}/failed" ]; then
echo "Uh oh, some went wrong."
exit 1
else
echo "Woohoo! It's working!"
exit 0
fi
================================================
FILE: docker-compose.yml
================================================
version: '3.3'
services:
app:
depends_on:
- app_db
image: wpgatsby-app:latest
volumes:
- '.:/var/www/html/wp-content/plugins/wp-gatsby'
- './.log/app:/var/log/apache2'
environment:
WP_URL: 'http://localhost:8091'
WP_DOMAIN: 'localhost:8091'
DB_HOST: app_db
DB_NAME: wordpress
DB_USER: wordpress
DB_PASSWORD: wordpress
WP_DOMAIN: localhost
ADMIN_EMAIL: admin@example.com
ADMIN_USERNAME: admin
ADMIN_PASSWORD: password
INCLUDE_WPGRAPHIQL: 1
IMPORT_WC_PRODUCTS: 1
STRIPE_GATEWAY: 1
ports:
- '8091:80'
networks:
local:
app_db:
image: mysql:5.7
environment:
MYSQL_ROOT_PASSWORD: root
MYSQL_DATABASE: wordpress
MYSQL_USER: wordpress
MYSQL_PASSWORD: wordpress
ports:
- '3306'
networks:
testing:
local:
testing:
depends_on:
- app_db
image: wpgatsby-testing:latest
volumes:
- '.:/var/www/html/wp-content/plugins/wp-gatsby'
- './.log/testing:/var/log/apache2'
- './codeception.dist.yml:/var/www/html/wp-content/plugins/wp-gatsby/codeception.yml'
env_file: .env.dist
environment:
XDEBUG_CONFIG: remote_host=host.docker.internal remote_port=9000 remote_enable=1
DB_HOST: app_db
WP_URL: 'http://localhost'
WP_DOMAIN: 'localhost'
STRIPE_GATEWAY: 1
networks:
testing:
networks:
local:
testing:
================================================
FILE: docs/action-monitor.md
================================================
# Activity Monitor
WPGatsby monitors activity that occurs in your WordPress site and notifies your Gatsby site
of the changes, allowing your Gatsby site to stay in sync with your WordPress site.
This document covers how WPGatsby tracks activity, what activity is tracked and how to customize the
activity tracking.
## How does WPGatsby track activity?
WPGatsby listens for CRUD (create, read, update and delete) actions that occur in WordPress, such as
publishing or deleting a post, updating menus, and more.
WPGatsby uses common WordPress actions to capture the objects that are changing, and stores records
of the actions in a custom post type named "action_monitor".
Whenever tracked activity is detected and an "action_monitor" action is created, a webhook is sent
to the Gatsby site that is configured in the [GatsbyJS settings page](./settings.md), allowing Gatsby to rebuild
pages that are affected by the changed data.
## What activity does WPGatsby track?
WPGatsby tracks when public data is changed in WordPress. Private data, such as draft posts, or users
with no published content, is not tracked (except for during previews but that data is private and deleted as soon as the preview completes).
Below you can read more details about all the data that WPGatsby tracks.
Additionally, you can check out [the tests](https://github.com/gatsbyjs/wp-gatsby/blob/master/tests/wpunit/ActionMonitorTest.php)
to see all the data that is tracked and the expected outcomes of different actions in WordPress.
### Advanced Custom Fields
Whenever ACF Field Groups are updated or deleted (using the ACF User Interface), WPGatsby logs an
action monitor action to notify Gatsby that the WPGraphQL Schema _may_ have changed.
#### Activity Tracked for ACF
- Update Field Group
- Delete Field Group
### Media
Whenever Media Items are uploaded, edited or deleted in the WordPress Media Library, WPGatsby logs
an action monitor action to notify Gatsby of the change.
#### Activity Tracked for Media
- Add Attachment
- Edit Attachment
- Delete Attachment
- Save Image Editor File
- Save Image File
### Menus
By default, Menus are considered private entities in WordPress. Once they are assinged to a Menu
Location, they become public. WPGatsby tracks activity related to public Menus and Menu Items.
Menus that are not assigned to Nav Menu Locations are not tracked, other than when they transition
from being not assigned a location to assigned a location, or the inverse.
#### Activity Tracked for Menus
- Update Nav Menu Locations
- Update Nav Menu
- Delete Nav Menu
- Update Nav Menu Item
- Add Nav Menu Item
### Posts (of any public post type, set to show in GraphQL)
Posts (and Pages and Custom Post Types) are typically pretty central to any WordPress site, and it's
important for Gatsby to know when they change.
WPGatsby tracks when posts are published (made public), and when published posts are edited or
deleted. Non-published posts are not tracked by WPGatsby. So changes can be made to draft posts, for
example and WPGatsby won't track that activity.
#### Activity Tracked for Posts
- Post Updated
- Transition Post Status
- Deleted Post
- Post Meta Added
- Post Meta Updated
- Post Meta Deleted
### Post Types (registered post types, not content of a post type)
WPGatsby caches the list of registered post types, and whenever it detects changes to the Post Type
registry, it logs an "action_monitor" action and notifies Gatsby of the change.
This allows Gatsby to update it's GraphQL Schema to reflect the changes in WPGraphQL.
#### Activity Tracked for Post Types
- Post Type registry changes
### Settings
The way WordPress stores settings is a bit of a blackbox. Many different things are stored in the
options table, so tracking changes to _all_ settings could be problematic. For example, tracking all
changes to all settings would cause WPGatsby to track transients. A transient changing would cause
Gatsby to fetch data from WordPress again, which could trigger further transient changes, and thus
could lead to an infinite loop.
So, instead of tracking updates to _all_ settings, WPGatsby only tracks specific settings that have
been configured to be "allowed" to be tracked.
WPGatsby provides an initial list of settings to track, and this list can be filtered (using the
`gatsby_action_monitor_should_track_option` filter) to disallow tracked settings, or allow tracking
of additional settings.
#### Activity Tracked for Settings
- Updated settings (based on filterable allow-list of settings to track)
### Taxonomies (registered taxonomies, not terms of a taxonomy)
WPGatsby caches the list of registered taxonomies, and whenever it detects changes to the Taxonomy
registry, it logs an "action_monitor" action and notifies Gatsby of the change.
This allows Gatsby to update it's GraphQL Schema to reflect the changes in WPGraphQL.
#### Activity Tracked for Taxonomies
- Custom Taxonomy registry changes
### Terms (of any public taxonomy, set to show in GraphQL)
Terms are tracked when they are created, updated or deleted.
#### Activity Tracked for Terms
- Term Created
- Term Updated
- Term Deleted
- Term Meta Added
- Term Meta Updated
- Term Meta Deleted
### Users (must be a published author of public content)
In WordPress, users are considered private by default. But once a user publishes content of a public
post type, that user becomes a public entity, as it then has an author archive page, REST API
endpoint, etc. WPGatsby tracks activity of these public users. Users with no published content are
not tracked.
#### Activity Tracked for Users
- Profile Update
- Delete User
- Update User Meta
- Add User Meta
- Delete User Meta
- Publish Post by author
## How to customize WPGatsby Activity Monitoring
You may find that you want to ignore certain actions from being tracked, or more likely you may want
to track additional actions that are not tracked by default.
Below you can learn more about both of these cases:
### Skip tracking of an action
If you'd like to prevent an action from being logged, you can use the `gatsby_pre_log_action_monitor_action` filter.
This filter will get passed the array of data to be logged.
If the filter returns `false`, the action will not be logged. If it returns anything else, the action
will proceed to be logged.
Example:
The example below would ignore logging actions for Post with ID `15`.
```php
add_filter( 'gatsby_pre_log_action_monitor_action', function( $null, $log_data ) {
if ( 'Post' === $log_data['graphql_single_name'] && 15 === $log_data['node_id'] ) {
return false;
}
return $null;
}, 10, 2 );
```
### Tracking a custom action
If you have a plugin that stores data in non-traditional ways, such as in a Custom Database Table,
you may need to track custom actions to tell Gatsby that something has changed.
You can do this by extending the `Monitor` class, and registering it with the `gatsby_action_monitors` filter.
**Note**: All of the fields passed to the `log_action` method below are required.
```php
/**
* Class - MyCustomActionMonitor
*/
class MyCustomActionMonitor extends \WPGatsby\ActionMonitor\Monitors\Monitor {
/**
* Initialize the custom tracker.
*/
public function init() {
// Hook into the custom action you want to log.
add_action( 'my_custom_action', [ $this, 'custom_action_callback' ] );
}
/**
* Callback for custom action.
*/
public function custom_action_callback( $your_custom_object ) {
/**
* Log an action to Action Monitor.
*
* This will create an entry in the `action_monitor` post type
* and will notify Gatsby Source WordPress about the activity.
*/
$this->monitor->log_action( [
'action_type' => 'CREATE',
'title' => $your_custom_object->title,
'graphql_single_name' => 'MyCustomType',
'graphql_plural_name' => 'MyCustomTypes',
'status' => 'publish',
'relay_id' => base64_encode( 'MyCustomType:' . $your_custom_object->ID ),
'node_id' => $your_custom_object->ID,
] );
}
}
add_filter( 'gatsby_action_monitors', function( array $monitors, \WPGatsby\ActionMonitor\ActionMonitor $action_monitor) {
$monitors['MyCustomActionMonitor'] = new MyCustomActionMonitor( $action_monitor );
return $monitors;
}, 10, 2 );
```
================================================
FILE: docs/running-tests.md
================================================
# Running Tests
This document provides information on running tests locally.
There are 2 options:
- [Running tests with your own local environment](#local-tests) (faster, more room for environmental inconsistencies)
- [Running tests with Docker](#docker-tests) (slower, more consistent)
## Running Tests with your own local environment
Below are instructions for running tests with your own local environment. Running tests this way is more flexible and faster than running tests with Docker. But, there's a chance your local environment (PHP versions, MySQL versions, etc) could change (as you work on other projects, for example), and that could cause problems with the test suite.
If you follow the instructions below, you should be able to run tests locally with your own local environment.
### Prerequisties
You must have the following available locally:
- **PHP**
- **MySQL** (or equivalent such as MariaDB)
- **[Composer](https://getcomposer.org/doc/00-intro.md)**
- **[WP-CLI](https://wp-cli.org/)** installed as well as terminal/shell/command-line access.
### Codeception & the wp-browser module
**WPGraphQL** and **WPGatsby** both use the **[Codeception](https://codeception.com/)** testing framework alongside the **[wp-browser](https://wpbrowser.wptestkit.dev/)** module created by [Luca Tumedei](https://www.theaveragedev.com/) for running the automated test suite. We'll be using Codeception scaffolding to generate all the tedious test code, but this will not be an in-depth guide on either of these libraries. It's not required to process with this tutorial, but it's highly recommended that after finishing this tutorial you take a look at the documentation for both.
- **[Codeception](https://codeception.com/docs/01-Introduction)**
- **[wp-browser](https://wpbrowser.wptestkit.dev/)**
1. Start by cloning **[WPGatsby](https://github.com/wp-gatsby/wp-gatsby)**.
2. Open your terminal.
3. Copy the `.env.dist` to `.env` by execute the following in your terminal in the **WPGatsby** root directory.
```
cp .env.dist .env
```
4. Open the .env and update the highlighted environmental variables to match your machine setup.

5. Last thing to do is run the WordPress testing environment install script in the terminal.
```
composer install-test-env
```
This will create and configure a WordPress installation in a temporary directory for the purpose of testing.
### Setting up Codeception
Now that we have setup our testing environment, let's run the tests. To do this we will need to install the **Codeception** and the rest of our **devDependencies**
1. First run `composer install` in the terminal.
2. Next copy the `codeception.dist.yml` to `codeception.yml`
```
cp codeception.dist.yml codeception.yml
```
3. Open `codeception.yml` and make the following changes.


Now you are ready to run the tests.
### Running the tests
Now your're ready to rum the tests. There is a small issue you may have with our testing environment. The WordPress installation we created doesn't support **end-to-end (*e2e*)** testing, however this won't be a problem. **WPGraphQL** is an API and most of the time you can get away with just ensuring that your query works, and **WPGraphQL** provides a few functions that will allow us to do just that.
Let's get started by running all the unit tests. Back in your terminal run the following:
```
vendor/bin/codecept run wpunit
```
If everything is how it should be you should get all passing tests.

You can also run individual test suites by specifying the path:
```
vendor/bin/codecept run tests/wpunit/ActionMonitorTest.php
```
Or even run a single test by specifying the specific test:
```
vendor/bin/codecept run tests/wpunit/ActionMonitorTest.php:testActionMonitorQueryIsValid
```
## Running Tests with Docker
The automated tests that run in the Github workflows use Docker. This ensures the environment is always what we expect. No risk of inconsistencies with PHP versions, MySQL versions, etc.
The trade off (at least as of right now) is that you must run the entire test suite instead of having the ability to run individual tests. And you must wait for the Docker environment to boot up. Overall, this is slower and less flexible than running tests with your own local environment, but it's more consistent.
To run tests with Docker, run the following commands:
- `composer install`: This will install the composer dependencies needed for testing
- `composer build-test`: This will build a Docker environment. This can take some time, especially the first time you run it.
- `composer run-test`: This will run the tests in the Docker environment.
================================================
FILE: lib/wp-settings-api.php
================================================
* @link https://tareq.co Tareq Hasan
* @link https://github.com/tareq1988/wordpress-settings-api-class
* @example example/oop-example.php How to use the class
*/
if ( ! class_exists( 'WPGraphQL_Settings_API' ) ) :
class WPGraphQL_Settings_API {
/**
* settings sections array
*
* @var array
*/
protected $settings_sections = array();
/**
* Settings fields array
*
* @var array
*/
protected $settings_fields = array();
public function __construct() {
add_action( 'admin_enqueue_scripts', array( $this, 'admin_enqueue_scripts' ) );
}
/**
* Enqueue scripts and styles
*/
function admin_enqueue_scripts() {
wp_enqueue_style( 'wp-color-picker' );
wp_enqueue_media();
wp_enqueue_script( 'wp-color-picker' );
wp_enqueue_script( 'jquery' );
}
/**
* Set settings sections
*
* @param array $sections setting sections array
*/
function set_sections( $sections ) {
$this->settings_sections = $sections;
return $this;
}
/**
* Add a single section
*
* @param array $section
*/
function add_section( $section ) {
$this->settings_sections[] = $section;
return $this;
}
/**
* Set settings fields
*
* @param array $fields settings fields array
*/
function set_fields( $fields ) {
$this->settings_fields = $fields;
return $this;
}
function add_field( $section, $field ) {
$defaults = array(
'name' => '',
'label' => '',
'desc' => '',
'type' => 'text'
);
$arg = wp_parse_args( $field, $defaults );
$this->settings_fields[ $section ][] = $arg;
return $this;
}
/**
* Initialize and registers the settings sections and fileds to WordPress
*
* Usually this should be called at `admin_init` hook.
*
* This function gets the initiated settings sections and fields. Then
* registers them to WordPress and ready for use.
*/
function admin_init() {
//register settings sections
foreach ( $this->settings_sections as $section ) {
if ( false == get_option( $section['id'] ) ) {
add_option( $section['id'] );
}
if ( isset( $section['desc'] ) && ! empty( $section['desc'] ) ) {
$section['desc'] = '
WPGatsby is a free open-source WordPress plugin that optimizes your WordPress site to work as a data source for [Gatsby](https://www.gatsbyjs.com/docs/how-to/sourcing-data/sourcing-from-wordpress).
This plugin must be used in combination with the npm package [`gatsby-source-wordpress@^4.0.0`](https://www.npmjs.com/package/gatsby-source-wordpress).
## Install and Activation
WPGatsby is available on the WordPress.org repository and can be installed from your WordPress dashboard, or by using any other plugin installation method you prefer, such as installing with Composer from wpackagist.org.
## Plugin Overview
This plugin has 2 primary responsibilities:
- [Monitor Activity in WordPress to keep Gatsby in sync with WP](https://github.com/gatsbyjs/wp-gatsby/blob/master/docs/action-monitor.md)
- [Configure WordPress Previews to work with Gatsby](https://github.com/gatsbyjs/gatsby/blob/master/packages/gatsby-source-wordpress/docs/tutorials/configuring-wp-gatsby.md#setting-up-preview)
Additionally, WPGatsby has a settings page to connect your WordPress site with your Gatsby site:
- [WPGatsby Settings](https://github.com/gatsbyjs/gatsby/blob/master/packages/gatsby-source-wordpress/docs/tutorials/configuring-wp-gatsby.md)
================================================
FILE: src/ActionMonitor/ActionMonitor.php
================================================
wpgraphql_debug_mode =
class_exists( 'WPGraphQL' ) && method_exists( 'WPGraphQL', 'debug' )
? \WPGraphQL::debug()
: false;
// Initialize action monitors
add_action( 'wp_loaded', [ $this, 'init_action_monitors' ], 11 );
// Register the GraphQL Fields Gatsby Source WordPress needs to interact with the Action Monitor
add_action( 'graphql_register_types', [ $this, 'register_graphql_fields' ] );
// Register post type and taxonomies to track CRUD events in WordPress
add_action( 'init', [ $this, 'init_post_type_and_taxonomies' ] );
add_filter( 'manage_action_monitor_posts_columns', [ $this, 'add_modified_column' ], 10 );
add_action(
'manage_action_monitor_posts_custom_column',
[
$this,
'render_modified_column',
],
10,
2
);
// Trigger webhook dispatch
add_action( 'shutdown', [ $this, 'trigger_dispatch' ] );
// allow any role to use Gatsby Preview
add_action( 'admin_init', [ $this, 'action_monitor_add_role_caps' ], 999 );
}
/**
* For Action Monitor, all of these roles need to be able to view and edit private action monitor posts so that Preview works for all roles.
*/
public function action_monitor_add_role_caps() {
$doing_graphql_request
= defined( 'GRAPHQL_REQUEST' ) && true === GRAPHQL_REQUEST;
if ( $doing_graphql_request ) {
// we only need to add roles one time. checking capabilities repeatedly isn't needed, just when the user is in the admin area is fine.
return;
}
$roles = apply_filters(
'gatsby_private_action_monitor_roles',
[
'editor',
'administrator',
'contributor',
'author'
]
);
foreach( $roles as $the_role ) {
$role = get_role($the_role);
if ( ! $role->has_cap( 'read_private_action_monitor_posts' ) ) {
$role->add_cap( 'read_private_action_monitor_posts' );
}
if ( ! $role->has_cap( 'edit_others_action_monitor_posts' ) ) {
$role->add_cap( 'edit_others_action_monitor_posts' );
}
}
}
/**
* Get the post types that are tracked by WPGatsby.
*
* @return array|mixed|void
*/
public function get_tracked_post_types() {
$public_post_types = get_post_types(
[
'show_in_graphql' => true,
'public' => true,
]
);
$publicly_queryable_post_types = get_post_types(
[
'show_in_graphql' => true,
'public' => false,
'publicly_queryable' => true,
]
);
$excludes = [
'action_monitor' => 'action_monitor',
];
$tracked_post_types = array_diff(
array_merge( $public_post_types, $publicly_queryable_post_types ),
$excludes
);
$tracked_post_types = apply_filters(
'gatsby_action_monitor_tracked_post_types',
$tracked_post_types
);
return ! empty( $tracked_post_types ) && is_array( $tracked_post_types ) ? $tracked_post_types : [];
}
/**
* Get the taxonomies that are tracked by WPGatsby
*
* @return array|mixed|void
*/
public function get_tracked_taxonomies() {
$tracked_taxonomies = apply_filters(
'gatsby_action_monitor_tracked_taxonomies',
get_taxonomies(
[
'show_in_graphql' => true,
'public' => true,
]
)
);
return ! empty( $tracked_taxonomies ) && is_array( $tracked_taxonomies ) ? $tracked_taxonomies : [];
}
/**
* Register Action monitor post type and associated taxonomies.
*
* The post type is used to store records of CRUD actions that have occurred in WordPress so
* that Gatsby can keep in Sync with changes in WordPress.
*
* The taxonomies are registered to store data related to the actions, but make it more
* efficient to filter actions by the values as Tax Queries are much more efficient than Meta
* Queries.
*/
public function init_post_type_and_taxonomies() {
/**
* Post Type: Action Monitor.
*/
$post_type_labels = [
'name' => __( 'Action Monitor', 'WPGatsby' ),
'singular_name' => __( 'Action Monitor', 'WPGatsby' ),
];
// Registers the post_type that logs actions for Gatsby
register_post_type(
'action_monitor',
[
'label' => __( 'Action Monitor', 'WPGatsby' ),
'labels' => $post_type_labels,
'description' => 'Used to keep a log of actions in WordPress for cache invalidation in gatsby-source-wordpress.',
'public' => false,
'publicly_queryable' => true,
'show_ui' => $this->wpgraphql_debug_mode,
'delete_with_user' => false,
'show_in_rest' => false,
'rest_base' => '',
'rest_controller_class' => 'WP_REST_Posts_Controller',
'has_archive' => false,
'show_in_menu' => $this->wpgraphql_debug_mode,
'show_in_nav_menus' => false,
'exclude_from_search' => true,
'capabilities' => [
// these custom capabilities allow any role to use Preview
'read_private_posts' => 'read_private_action_monitor_posts',
'edit_others_posts' => 'edit_others_action_monitor_posts',
// these are regular role capabilities for a CPT
'create_post' => 'create_post',
'edit_post' => 'edit_post',
'read_post' => 'read_post',
'delete_post' => 'delete_post',
'edit_posts' => 'edit_posts',
'publish_posts' => 'publish_posts',
'create_posts' => 'create_posts'
],
'map_meta_cap' => false,
'hierarchical' => false,
'rewrite' => [
'slug' => 'action_monitor',
'with_front' => true,
],
'query_var' => true,
'supports' => [ 'title', 'editor' ],
'show_in_graphql' => true,
'graphql_single_name' => 'ActionMonitorAction',
'graphql_plural_name' => 'ActionMonitorActions',
]
);
// Registers the taxonomy that connects the node type to the action_monitor post
register_taxonomy(
'gatsby_action_ref_node_type',
'action_monitor',
[
'label' => __( 'Referenced Node Type', 'WPGatsby' ),
'public' => false,
'show_ui' => $this->wpgraphql_debug_mode,
'show_in_graphql' => false,
'graphql_single_name' => 'ReferencedNodeType',
'graphql_plural_name' => 'ReferencedNodeTypes',
'hierarchical' => false,
'show_in_nav_menus' => false,
'show_tagcloud' => false,
'show_admin_column' => true,
]
);
// Registers the taxonomy that connects the node databaseId to the action_monitor post
register_taxonomy(
'gatsby_action_ref_node_dbid',
'action_monitor',
[
'label' => __( 'Referenced Node Database ID', 'WPGatsby' ),
'public' => false,
'show_ui' => $this->wpgraphql_debug_mode,
'show_in_graphql' => false,
'graphql_single_name' => 'ReferencedNodeDatabaseId',
'graphql_plural_name' => 'ReferencedNodeDatabaseIds',
'hierarchical' => false,
'show_in_nav_menus' => false,
'show_tagcloud' => false,
'show_admin_column' => true,
]
);
// Registers the taxonomy that connects the node global ID to the action_monitor post
register_taxonomy(
'gatsby_action_ref_node_id',
'action_monitor',
[
'label' => __( 'Referenced Node Global ID', 'WPGatsby' ),
'public' => false,
'show_ui' => $this->wpgraphql_debug_mode,
'show_in_graphql' => false,
'graphql_single_name' => 'ReferencedNodeId',
'graphql_plural_name' => 'ReferencedNodeIds',
'hierarchical' => false,
'show_in_nav_menus' => false,
'show_tagcloud' => false,
'show_admin_column' => true,
]
);
// Registers the taxonomy that connects the action type (CREATE, UPDATE, DELETE) to the action_monitor post
register_taxonomy(
'gatsby_action_type',
'action_monitor',
[
'label' => __( 'Action Type', 'WPGatsby' ),
'public' => false,
'show_ui' => $this->wpgraphql_debug_mode,
'show_in_graphql' => false,
'hierarchical' => false,
'show_in_nav_menus' => false,
'show_tagcloud' => false,
'show_admin_column' => true,
]
);
register_taxonomy( 'gatsby_action_stream_type', 'action_monitor', [
'label' => __( 'Stream Type', 'WPGatsby' ),
'public' => false,
'show_ui' => $this->wpgraphql_debug_mode,
'show_in_graphql' => false,
'hierarchical' => false,
'show_in_nav_menus' => false,
'show_tagcloud' => false,
'show_admin_column' => true,
] );
}
/**
* Adds a column to the action monitor Post Type to show the last modified time
*
* @param array $columns The column names included in the post table
*
* @return array
*/
public function add_modified_column( array $columns ) {
$columns['gatsby_last_modified'] = __( 'Last Modified', 'WPGatsby' );
return $columns;
}
/**
* Renders the last modified time in the action_monitor post type "modified" column
*
* @param string $column_name The name of the column
* @param int $post_id The ID of the post in the table
*/
public function render_modified_column( string $column_name, int $post_id ) {
if ( 'gatsby_last_modified' === $column_name ) {
$m_orig = get_post_field( 'post_modified', $post_id, 'raw' );
$m_stamp = strtotime( $m_orig );
$modified = date( 'n/j/y @ g:i a', $m_stamp );
echo '
';
echo '' . esc_html( $modified ) . ' ';
echo '
';
}
}
/**
* Sets should_dispatch to true
*/
public function schedule_dispatch() {
$this->should_dispatch = true;
}
/**
* Deletes all posts of the action_monitor post_type that are 7 days old, as well as any
* associated post meta and term relationships.
*
* @return bool|int
*/
public function garbage_collect_actions() {
global $wpdb;
$post_type = 'action_monitor';
$sql = wp_strip_all_tags(
'DELETE posts, pm, pt
FROM ' . $wpdb->prefix . 'posts AS posts
LEFT JOIN ' . $wpdb->prefix . 'term_relationships AS pt ON pt.object_id = posts.ID
LEFT JOIN ' . $wpdb->prefix . 'postmeta AS pm ON pm.post_id = posts.ID
WHERE posts.post_type = \'%1$s\'
AND posts.post_modified < \'%2$s\'',
true
);
$query = $wpdb->prepare( $sql, $post_type, date( 'Y-m-d H:i:s', strtotime( '-7 days' ) ) );
return $wpdb->query( $query );
}
/**
* Given the name of an Action Monitor, this returns it
*
* @param string $name The name of the Action Monitor to get
*
* @return mixed|null
*/
public function get_action_monitor( string $name ) {
return $this->action_monitors[ $name ] ?? null;
}
/**
* Use WP Action hooks to create action monitor posts
*/
function init_action_monitors() {
$class_names = [
'AcfMonitor',
'MediaMonitor',
'NavMenuMonitor',
'PostMonitor',
'PostTypeMonitor',
'SettingsMonitor',
'TaxonomyMonitor',
'TermMonitor',
'UserMonitor',
'PreviewMonitor',
];
$action_monitors = [];
foreach ( $class_names as $class_name ) {
$class = 'WPGatsby\ActionMonitor\Monitors\\' . $class_name;
if ( class_exists( $class ) ) {
$monitor = new $class( $this );
$action_monitors[ $class_name ] = $monitor;
}
}
/**
* Filter the action monitors. This can allow for other monitors
* to be registered, or can allow for monitors to be overridden.
*
* Overriding monitors is not advised, but there are cases where it might
* be necessary. Override with caution.
*
* @param array $action_monitors
* @param \WPGatsby\ActionMonitor\ActionMonitor $monitor The class instance, used to initialize the monitor.
*/
$this->action_monitors = apply_filters( 'gatsby_action_monitors', $action_monitors, $this );
do_action( 'gatsby_init_action_monitors', $this->action_monitors );
}
function register_post_graphql_fields() {
register_graphql_field(
'ActionMonitorAction',
'actionType',
[
'type' => 'String',
'description' => __(
'The type of action (CREATE, UPDATE, DELETE)',
'WPGatsby'
),
'resolve' => function( $post ) {
$terms = get_the_terms( $post->databaseId, 'gatsby_action_type' );
if ( ! is_wp_error( $terms ) && ! empty( $terms ) ) {
$action_type = (string) $terms[0]->name;
} else {
$action_type
= get_post_meta( $post->ID, 'action_type', true );
}
return $action_type ? $action_type : null;
},
]
);
register_graphql_field(
'ActionMonitorAction',
'referencedNodeStatus',
[
'type' => 'String',
'description' => __(
'The post status of the post that triggered this action',
'WPGatsby'
),
'resolve' => function( $post ) {
$referenced_node_status = get_post_meta(
$post->ID,
'referenced_node_status',
true
);
return $referenced_node_status ?? null;
},
]
);
register_graphql_field(
'ActionMonitorAction',
'previewData',
[
'type' => 'GatsbyPreviewData',
'description' => __(
'The preview data of the post that triggered this action.',
'WPGatsby'
),
'resolve' => function( $post ) {
$referenced_node_preview_data = get_post_meta(
$post->ID,
'_gatsby_preview_data',
true
);
return $referenced_node_preview_data
&& $referenced_node_preview_data !== ""
? json_decode( $referenced_node_preview_data )
: null;
}
]
);
register_graphql_object_type(
'GatsbyPreviewData',
[
'description' => __( 'Gatsby Preview webhook data.', 'WPGatsby' ),
'fields' => [
'previewDatabaseId' => [
'type' => 'Int',
'description' => __( 'The WordPress database ID of the preview. Could be a revision or draft ID.', 'WPGatsby' ),
],
'userDatabaseId' => [
'type' => 'Int',
'description' => __( 'The database ID of the user who made the original preview.', 'WPGatsby' ),
],
'id' => [
'type' => 'ID',
'description' => __( 'The Relay id of the previewed node.', 'WPGatsby' ),
],
'singleName' => [
'type' => 'String',
'description' => __( 'The GraphQL single field name for the type of the preview.', 'WPGatsby' ),
],
'isDraft' => [
'type' => 'Boolean',
'description' => __( 'Wether or not the preview is a draft.', 'WPGatsby' ),
],
'remoteUrl' => [
'type' => 'String',
'description' => __( 'The WP url at the time of the preview.', 'WPGatsby' ),
],
'modified' => [
'type' => 'String',
'description' => __( 'The modified time of the previewed node.', 'WPGatsby' ),
],
'parentDatabaseId' => [
'type' => 'Int',
'description' => __( 'The WordPress database ID of the preview. If this is a draft it will potentially return 0, if it\'s a revision of a post, it will return the ID of the original post that this is a revision of.', 'WPGatsby' ),
],
'manifestIds' => [
'type' => [ 'list_of' => 'String' ],
'description' => __( 'A list of manifest ID\'s a preview action has seen during it\'s lifetime.', 'WPGatsby' ),
]
]
]
);
register_graphql_field(
'ActionMonitorAction',
'referencedNodeID',
[
'type' => 'String',
'description' => __(
'The post ID of the post that triggered this action',
'WPGatsby'
),
'resolve' => function( $post ) {
$terms = get_the_terms( $post->databaseId, 'gatsby_action_ref_node_dbid' );
if ( ! is_wp_error( $terms ) && ! empty( $terms ) ) {
$referenced_node_id = (string) $terms[0]->name;
} else {
$referenced_node_id = get_post_meta(
$post->ID,
'referenced_node_id',
true
);
}
return $referenced_node_id ?? null;
},
]
);
register_graphql_field(
'ActionMonitorAction',
'referencedNodeGlobalRelayID',
[
'type' => 'String',
'description' => __(
'The global relay ID of the post that triggered this action',
'WPGatsby'
),
'resolve' => function( $post ) {
$terms = get_the_terms( $post->databaseId, 'gatsby_action_ref_node_id' );
if ( ! is_wp_error( $terms ) && ! empty( $terms ) ) {
$referenced_node_relay_id = (string) $terms[0]->name;
} else {
$referenced_node_relay_id = get_post_meta(
$post->ID,
'referenced_node_relay_id',
true
);
}
return $referenced_node_relay_id ?? null;
},
]
);
register_graphql_field(
'ActionMonitorAction',
'referencedNodeSingularName',
[
'type' => 'String',
'description' => __(
'The WPGraphQL single name of the referenced post',
'WPGatsby'
),
'resolve' => function( $post ) {
$referenced_node_single_name = get_post_meta(
$post->ID,
'referenced_node_single_name',
true
);
return $referenced_node_single_name ?? null;
},
]
);
register_graphql_field(
'ActionMonitorAction',
'referencedNodePluralName',
[
'type' => 'String',
'description' => __(
'The WPGraphQL plural name of the referenced post',
'WPGatsby'
),
'resolve' => function( $post ) {
$referenced_node_plural_name = get_post_meta(
$post->ID,
'referenced_node_plural_name',
true
);
return $referenced_node_plural_name ?? null;
},
]
);
register_graphql_field(
'RootQueryToActionMonitorActionConnectionWhereArgs',
'sinceTimestamp',
[
'type' => 'Number',
'description' => 'List Actions performed since a timestamp.',
]
);
// @todo write a test for this previewStream input arg
register_graphql_field(
'RootQueryToActionMonitorActionConnectionWhereArgs',
'previewStream',
[
'type' => 'boolean',
'description' => 'List Actions of the PREVIEW stream type.',
]
);
add_filter(
'graphql_post_object_connection_query_args',
function( $args ) {
$sinceTimestamp = $args['sinceTimestamp'] ?? null;
if ( $sinceTimestamp ) {
$args['date_query'] = [
[
'after' => gmdate(
'Y-m-d H:i:s',
$sinceTimestamp / 1000
),
'column' => 'post_modified_gmt',
],
];
}
return $args;
}
);
add_filter(
'graphql_post_object_connection_query_args',
function( $args ) {
$previewStream = $args['previewStream'] ?? false;
if ( $previewStream ) {
$args['tax_query'] = [
[
'taxonomy' => 'gatsby_action_stream_type',
'field' => 'slug',
'terms' => 'preview',
],
];
}
return $args;
}
);
}
/**
* Add post meta to schema
*/
function register_graphql_fields() {
$this->register_post_graphql_fields();
}
/**
* Triggers the dispatch to the remote endpoint(s)
*/
public function trigger_dispatch() {
$build_webhook_field = Settings::prefix_get_option( 'builds_api_webhook', 'wpgatsby_settings', false );
$preview_webhook_field = Settings::prefix_get_option( 'preview_api_webhook', 'wpgatsby_settings', false );
$should_call_build_webhooks =
$build_webhook_field &&
$this->should_dispatch;
$we_should_call_preview_webhooks =
$preview_webhook_field &&
$this->should_dispatch;
if ( $should_call_build_webhooks ) {
$webhooks = explode( ',', $build_webhook_field );
$truthy_webhooks = array_filter( $webhooks );
$unique_webhooks = array_unique( $truthy_webhooks );
foreach ( $unique_webhooks as $webhook ) {
$args = apply_filters( 'gatsby_trigger_dispatch_args', [], $webhook );
wp_safe_remote_post( $webhook, $args );
}
}
if ( $we_should_call_preview_webhooks ) {
$webhooks = explode( ',', $preview_webhook_field );
$truthy_webhooks = array_filter( $webhooks );
$unique_webhooks = array_unique( $truthy_webhooks );
foreach ( $unique_webhooks as $webhook ) {
$token = \WPGatsby\GraphQL\Auth::get_token();
// For preview webhooks we send the token
// because this is a build but
// we want it to source any pending previews
// in case someone pressed preview right after
// we got to this point from someone else pressing
// publish/update.
$graphql_endpoint = apply_filters( 'graphql_endpoint', 'graphql' );
$graphql_url = get_site_url() . '/' . ltrim( $graphql_endpoint, '/' );
$post_body = apply_filters(
'gatsby_trigger_preview_build_dispatch_post_body',
[
'token' => $token,
'userDatabaseId' => get_current_user_id(),
'remoteUrl' => $graphql_url
]
);
$args = apply_filters(
'gatsby_trigger_preview_build_dispatch_args',
[
'body' => wp_json_encode( $post_body ),
'headers' => [
'Content-Type' => 'application/json; charset=utf-8',
],
'method' => 'POST',
'data_format' => 'body',
],
$webhook
);
wp_safe_remote_post( $webhook, $args );
}
}
}
}
================================================
FILE: src/ActionMonitor/Monitors/AcfMonitor.php
================================================
trigger_schema_diff(
[
'title' => __( 'Update ACF Field Group', 'WPGatsby' ),
]
);
}
);
add_action(
'acf/delete_field_group',
function() {
$this->trigger_schema_diff(
[
'title' => __( 'Delete ACF Field Group', 'WPGatsby' ),
]
);
}
);
add_action('acf/save_post', [$this, 'after_acf_save_post'], 20);
}
/**
* Handles content updates of ACF option pages.
*/
public function after_acf_save_post() {
if ( ! function_exists( 'acf_get_options_pages' ) ) {
return;
}
$option_pages = acf_get_options_pages();
if ( ! is_array( $option_pages ) ) {
return;
}
$option_pages_slugs = array_keys( $option_pages );
/**
* Filters the $option_pages_slugs array.
*
* @since 2.1.2
*
* @param array $option_pages_slugs Array with slugs of all registered ACF option pages.
*/
$option_pages_slugs = apply_filters(
'gatsby_action_monitor_tracked_acf_options_pages',
$option_pages_slugs
);
$screen = get_current_screen();
if(
! empty( $option_pages_slugs )
&& is_array( $option_pages_slugs )
&& Utils::str_in_substr_array( $screen->id, $option_pages_slugs )
) {
$this->trigger_non_node_root_field_update();
}
}
}
================================================
FILE: src/ActionMonitor/Monitors/MediaMonitor.php
================================================
log_action(
[
'action_type' => 'CREATE',
'title' => $attachment->post_title ?? "Attachment #$attachment_id",
// there is no concept of inheriting post status in Gatsby, so images will always be considered published.
'status' => 'publish',
'node_id' => $attachment_id,
'relay_id' => $global_relay_id,
'graphql_single_name' => 'mediaItem',
'graphql_plural_name' => 'mediaItems',
'skip_webhook' => true,
],
);
}
/**
* Logs an action when Media Items are edited
*
* @param int $attachment_id
*/
public function callback_edit_attachment( int $attachment_id ) {
$attachment = get_post( $attachment_id );
if ( ! $attachment ) {
return;
}
$global_relay_id = Relay::toGlobalId(
'post',
$attachment_id
);
$this->log_action(
[
'action_type' => 'UPDATE',
'title' => $attachment->post_title ?? "Attachment #$attachment_id",
// there is no concept of inheriting post status in Gatsby, so images will always be considered published.
'status' => 'publish',
'node_id' => $attachment_id,
'relay_id' => $global_relay_id,
'graphql_single_name' => 'mediaItem',
'graphql_plural_name' => 'mediaItems',
'skip_webhook' => true,
],
);
}
/**
* Logs an action when media items are deleted from the Media Library
*
* @param int $attachment_id The ID of the media item being deleted
*/
public function callback_delete_attachment( int $attachment_id ) {
$attachment = get_post( $attachment_id );
if ( ! $attachment ) {
return;
}
$global_relay_id = Relay::toGlobalId(
'post',
$attachment_id
);
$this->log_action(
[
'action_type' => 'DELETE',
'title' => $attachment->post_title ?? "Attachment #$attachment_id",
// there is no concept of inheriting post status in Gatsby, so images will always be considered published.
'status' => 'trash',
'node_id' => $attachment_id,
'relay_id' => $global_relay_id,
'graphql_single_name' => 'mediaItem',
'graphql_plural_name' => 'mediaItems',
'skip_webhook' => true,
],
);
}
/**
* Logs an action when image files are saved from the image editor
*
* @param string $dummy Unused.
* @param string $filename Filename.
* @param string $image Unused.
* @param string $mime_type Unused.
* @param int $post_id Post ID.
*/
public function callback_wp_save_image_editor_file( $dummy, $filename, $image, $mime_type, $post_id ) {
$this->callback_edit_attachment( $post_id );
}
/**
* Logs an action when image files are saved from the image editor
*
* @param string $dummy Unused.
* @param string $filename Filename.
* @param string $image Unused.
* @param string $mime_type Unused.
* @param int $post_id Post ID.
*/
public function callback_wp_save_image_file( $dummy, $filename, $image, $mime_type, $post_id ) {
return $this->callback_wp_save_image_editor_file( $dummy, $filename, $image, $mime_type, $post_id );
}
}
================================================
FILE: src/ActionMonitor/Monitors/Monitor.php
================================================
action_monitor = $action_monitor;
$this->init();
}
/**
* Allows IDs to be set that will be ignored by the logger
*
* @param int[] $ids Array of database IDs to ignore logging for
*/
public function set_ignored_ids( array $ids ) {
if ( ! empty( $ids ) && is_array( $ids ) ) {
$this->ignored_ids = array_merge( $this->ignored_ids, $ids );
}
}
/**
* Given an array of IDs, this removes them from the list of ignored IDs
*
* @param array $ids
*/
public function unset_ignored_ids( array $ids ) {
if ( ! empty( $ids ) && is_array( $ids ) ) {
foreach ( $ids as $id ) {
if ( isset( $this->ignored_ids[ $id ] ) ) {
unset( $this->ignored_ids[ $id ] );
}
}
}
}
/**
* Resets the ignored IDs to an empty array
*/
public function reset_ignored_ids() {
$this->ignored_ids = [];
}
/**
* Trigger action for non node root field updates
*
* @param array $args Optional args to pass to the action
*/
public function trigger_non_node_root_field_update( array $args = [] ) {
$default = [
'action_type' => 'NON_NODE_ROOT_FIELDS',
'title' => 'Non node root field changed',
'node_id' => 'update_non_node_root_field',
'relay_id' => 'update_non_node_root_field',
'graphql_single_name' => 'update_non_node_root_field',
'graphql_plural_name' => 'update_non_node_root_field',
'status' => 'update_non_node_root_field',
];
$this->log_action( array_merge( $default, $args ) );
}
/**
* Trigger action to refetch everything
*
* @param array $args Optional args to pass to the action
*/
public function trigger_refetch_all( $args = [] ) {
$default = [
'action_type' => 'REFETCH_ALL',
'title' => 'Something changed (such as permalink structure) that requires everything to be refetched',
'node_id' => 'refetch_all',
'relay_id' => 'refetch_all',
'graphql_single_name' => 'refetch_all',
'graphql_plural_name' => 'refetch_all',
'status' => 'refetch_all',
];
$this->log_action( array_merge( $default, $args ) );
}
/**
* Determines whether the meta should be tracked or not
*
* @param string $meta_key Metadata key.
* @param mixed $meta_value Metadata value. Serialized if non-scalar.
* @param object $object The object the metadata is for.
*
* @return bool
*/
protected function should_track_meta( string $meta_key, $meta_value, $object ) {
/**
* This filter allows plugins to opt-in or out of tracking for meta.
*
* @param bool $should_track Whether the meta key should be tracked.
* @param string $meta_key Metadata key.
* @param int $meta_id ID of updated metadata entry.
* @param mixed $meta_value Metadata value. Serialized if non-scalar.
* @param mixed $object The object the meta is being updated for.
*
* @param bool $tracked whether the meta key is tracked by Gatsby Action Monitor
*/
$should_track = apply_filters( 'gatsby_action_monitor_should_track_meta', null, $meta_key, $meta_value, $object );
// If the filter has been applied return it
if ( null !== $should_track ) {
return (bool) $should_track;
}
// If the meta key starts with an underscore, don't track it
if ( '_' === substr( $meta_key, 0, 1 ) ) {
return false;
}
return true;
}
/**
* Inserts an action that triggers Gatsby Source WordPress to diff the Schemas.
*
* This can be used for plugins such as Custom Post Type UI, Advanced Custom Fields, etc that
* alter the Schema in some way.
*
* @param array $args Optional args to add to the action
*/
public function trigger_schema_diff( $args = [] ) {
$default = [
'title' => __( 'Diff schemas', 'WPGatsby' ),
'node_id' => 'none',
'relay_id' => 'none',
'graphql_single_name' => 'none',
'graphql_plural_name' => 'none',
'status' => 'none',
];
$args = array_merge( $default, $args );
$args['action_type'] = 'DIFF_SCHEMAS';
$this->log_action( $args );
}
/**
* Insert new action
*
* $args = [$action_type, $title, $status, $node_id, $relay_id, $graphql_single_name,
* $graphql_plural_name]
*
* @param array $args Array of arguments to configure the action to be inserted
*
*/
public function log_action( array $args ) {
if (
! isset( $args['action_type'] ) ||
! isset( $args['title'] ) ||
! isset( $args['node_id'] ) ||
! isset( $args['relay_id'] ) ||
! isset( $args['graphql_single_name'] ) ||
! isset( $args['graphql_plural_name'] ) ||
! isset( $args['status'] )
) {
// @todo log that this action isn't working??
return;
}
/**
* Filter to allow skipping a logged action. If set to false, the action will not be logged.
*
* @param null|bool $enable Whether the action should be logged
* @param array $arguments The args to log
* @param Monitor $monitor Instance of the Monitor
*/
$pre_log_action = apply_filters( 'gatsby_pre_log_action_monitor_action', null, $args, $this );
if ( null !== $pre_log_action ) {
if ( false === $pre_log_action ) {
return;
}
}
// If the node_id is set to be ignored, don't create a log
if ( in_array( $args['node_id'], $this->ignored_ids, true ) ) {
return;
}
$should_dispatch =
! isset( $args['skip_webhook'] ) || ! $args['skip_webhook'];
$time = time();
$node_type = 'unknown';
if ( isset( $args['graphql_single_name'] ) ) {
$node_type = $args['graphql_single_name'];
} elseif ( isset( $args['relay_id'] ) ) {
$id_parts = Relay::fromGlobalId( $args['relay_id'] );
if ( ! isset( $id_parts['type'] ) ) {
$node_type = $id_parts['type'];
}
}
$stream_type = ( $args['stream_type'] ?? null ) === 'PREVIEW'
? 'PREVIEW'
: 'CONTENT';
$is_preview_stream = $stream_type === 'PREVIEW';
// Check to see if an action already exists for this node type/database id
$existing = new \WP_Query( [
'post_type' => 'action_monitor',
'post_status' => 'any',
'posts_per_page' => 1,
'no_found_rows' => true,
'fields' => 'ids',
'tax_query' => [
'relation' => 'AND',
[
'taxonomy' => 'gatsby_action_ref_node_dbid',
'field' => 'name',
'terms' => sanitize_text_field( $args['node_id'] ),
],
[
'taxonomy' => 'gatsby_action_ref_node_type',
'field' => 'name',
'terms' => $node_type,
],
[
'taxonomy' => 'gatsby_action_stream_type',
'field' => 'name',
'terms' => $stream_type,
]
],
] );
// If there's already an action logged for this node, update the record
if ( isset( $existing->posts ) && ! empty( $existing->posts ) ) {
$existing_id = $existing->posts[0];
$action_monitor_post_id = wp_update_post( [
'ID' => absint( $existing_id ),
'post_title' => $args['title'],
'post_content' => wp_json_encode( $args ),
] );
} else {
$action_monitor_post_id = \wp_insert_post(
[
'post_title' => $args['title'],
'post_type' => 'action_monitor',
'post_status' => 'private',
'author' => -1,
'post_name' => sanitize_title( "{$args['title']}-{$time}" ),
'post_content' => wp_json_encode( $args ),
]
);
wp_set_object_terms( $action_monitor_post_id, sanitize_text_field( $args['node_id'] ), 'gatsby_action_ref_node_dbid' );
wp_set_object_terms( $action_monitor_post_id, sanitize_text_field( $node_type ), 'gatsby_action_ref_node_type' );
}
wp_set_object_terms( $action_monitor_post_id, sanitize_text_field( $args['relay_id'] ), 'gatsby_action_ref_node_id' );
wp_set_object_terms( $action_monitor_post_id, $args['action_type'], 'gatsby_action_type' );
wp_set_object_terms( $action_monitor_post_id, $stream_type, 'gatsby_action_stream_type' );
if ( $action_monitor_post_id !== 0 ) {
if ( isset( $args['preview_data'] ) ) {
$existing_preview_data = \get_post_meta(
$action_monitor_post_id,
'_gatsby_preview_data',
true
);
$manifest_id = Preview::get_preview_manifest_id_for_post(
get_post( $args['node_id'] )
);
$manifest_ids = [$manifest_id];
// if we have existing data, we want to merge our manifest id
// into any existing manifest ids
if ( $existing_preview_data && $existing_preview_data !== "" ) {
$existing_preview_data = json_decode( $existing_preview_data );
if ( $existing_preview_data->manifestIds ?? false ) {
$manifest_ids = array_unique(
array_merge(
$existing_preview_data->manifestIds,
$manifest_ids
)
);
}
}
// add manifest ids
$preview_data = json_decode( $args['preview_data'] );
$preview_data->manifestIds = $manifest_ids;
$preview_data = json_encode( $preview_data );
\update_post_meta(
$action_monitor_post_id,
'_gatsby_preview_data',
$preview_data
);
}
\update_post_meta(
$action_monitor_post_id,
'referenced_node_status',
$args['status'] // menus don't have post status. This is for Gatsby
);
\update_post_meta(
$action_monitor_post_id,
'referenced_node_single_name',
graphql_format_field_name( $args['graphql_single_name'] )
);
\update_post_meta(
$action_monitor_post_id,
'referenced_node_plural_name',
graphql_format_field_name( $args['graphql_plural_name'] )
);
// preview actions should remain private
if ( !$is_preview_stream ) {
\wp_update_post( [
'ID' => $action_monitor_post_id,
'post_status' => 'publish'
] );
}
}
// If $should_dispatch is not set to false, schedule a dispatch. Actions being logged that
// set $should_dispatch to false will be logged, but not trigger a webhook immediately.
// if this is a preview we should always not dispatch
if ( $should_dispatch && ! $is_preview_stream ) {
// we've saved at least 1 action, so we should update
// but only if this isn't a preview
// previews will dispatch on their own
$this->action_monitor->schedule_dispatch();
}
// Delete old actions
$this->action_monitor->garbage_collect_actions();
}
/**
* Initialize the Monitor
*
* @return mixed
*/
abstract public function init();
}
================================================
FILE: src/ActionMonitor/Monitors/NavMenuMonitor.php
================================================
log_diffed_menus( $old_locations, $new_locations );
// Return the value passed to the filter, without making any changes
return $value;
}
/**
* Determines whether a menu is considered public and should be tracked
* by the activity monitor
*
* @param int $menu_id ID of the menu
*
* @return bool
*/
public function is_menu_public( int $menu_id ) {
$locations = get_theme_mod( 'nav_menu_locations' );
$assigned_menu_ids = ! empty( $locations ) ? array_values( $locations ) : [];
if ( empty( $assigned_menu_ids ) ) {
return false;
}
if ( in_array( $menu_id, $assigned_menu_ids, true ) ) {
return true;
}
return false;
}
/**
* Log action when a nav menu is updated
*
* @param int $menu_id The ID of the menu being updated
* @param array $menu_data The data associated with the menu
*/
public function callback_update_nav_menu( int $menu_id, array $menu_data = [] ) {
if ( ! $this->is_menu_public( $menu_id ) ) {
return;
}
$menu = get_term_by( 'id', absint( $menu_id ), 'nav_menu' );
$this->log_action(
[
'action_type' => 'UPDATE',
'title' => $menu->name,
// menus don't have post status. This is for Gatsby
'status' => 'publish',
'node_id' => (int) $menu->term_id,
'relay_id' => Relay::toGlobalId( 'term', (int) $menu->term_id ),
'graphql_single_name' => 'menu',
'graphql_plural_name' => 'menus',
]
);
}
/**
* Given an array of old menu locations and new menu locations, this
* diffs them and logs an action for the menu assigned to the added/removed location
*
* @param array $old_locations Old locations with a menu assigned
* @param array $new_locations New locations with a menu assigned
*/
public function log_diffed_menus( array $old_locations, array $new_locations ) {
// If old locations are same as new locations, do nothing
if ( $old_locations === $new_locations ) {
return;
}
// Trigger an action for each added location
$added = array_diff( $new_locations, $old_locations );
if ( ! empty( $added ) && is_array( $added ) ) {
foreach ( $added as $location => $added_menu_id ) {
if ( ! empty( $menu = get_term_by( 'id', (int) $added_menu_id, 'nav_menu' ) ) && $menu instanceof WP_Term ) {
$this->log_action(
[
'action_type' => 'CREATE',
'title' => $menu->name,
// menus don't have post status. This is for Gatsby
'status' => 'publish',
'node_id' => (int) $added_menu_id,
'relay_id' => Relay::toGlobalId( 'term', (int) $added_menu_id ),
'graphql_single_name' => 'menu',
'graphql_plural_name' => 'menus',
]
);
}
}
}
// Trigger an action for each location deleted
$removed = array_diff( $old_locations, $new_locations );
if ( ! empty( $removed ) ) {
foreach ( $removed as $location => $removed_menu_id ) {
$this->log_action(
[
'action_type' => 'DELETE',
'title' => $removed_menu_id,
// menus don't have post status. This is for Gatsby
'status' => 'trash',
'node_id' => (int) $removed_menu_id,
'relay_id' => Relay::toGlobalId( 'term', $removed_menu_id ),
'graphql_single_name' => 'menu',
'graphql_plural_name' => 'menus',
]
);
}
}
}
/**
* Log an action when a menu is deleted
*
* @param int $term_id ID of the deleted menu.
*/
public function callback_delete_nav_menu( $term_id ) {
$this->log_action(
[
'action_type' => 'DELETE',
'title' => '#' . $term_id,
// menus don't have post status. This is for Gatsby
'status' => 'trash',
'node_id' => (int) $term_id,
'relay_id' => Relay::toGlobalId( 'term', $term_id ),
'graphql_single_name' => 'menu',
'graphql_plural_name' => 'menus',
]
);
}
/**
* @param int $menu_id ID of the updated menu.
* @param int $menu_item_db_id ID of the updated menu item.
* @param array $args An array of arguments used to update a menu item.
*/
public function callback_add_nav_menu_item( int $menu_id, int $menu_item_db_id, array $args ) {
if ( ! $this->is_menu_public( $menu_id ) ) {
return;
}
$menu = get_term_by( 'id', $menu_id, 'nav_menu' );
$menu_item = get_post( $menu_item_db_id );
// Log action for the updated menu
$this->log_action(
[
'action_type' => 'UPDATE',
'title' => $menu->name,
// menus don't have post status. This is for Gatsby
'status' => 'publish',
'node_id' => (int) $menu->term_id,
'relay_id' => Relay::toGlobalId( 'term', (int) $menu->term_id ),
'graphql_single_name' => 'menu',
'graphql_plural_name' => 'menus',
]
);
// Log action for the added menu item
$this->log_action(
[
'action_type' => 'CREATE',
'title' => $menu_item->post_title,
// menus don't have post status. This is for Gatsby
'status' => 'publish',
'node_id' => (int) $menu_item->ID,
'relay_id' => Relay::toGlobalId( 'post', (int) $menu_item->ID ),
'graphql_single_name' => 'menuItem',
'graphql_plural_name' => 'menuItems',
]
);
}
/**
* @param int $menu_id ID of the updated menu.
* @param int $menu_item_db_id ID of the updated menu item.
* @param array $args An array of arguments used to update a menu item.
*/
public function callback_update_nav_menu_item( int $menu_id, int $menu_item_db_id, array $args ) {
if ( ! $this->is_menu_public( $menu_id ) ) {
return;
}
$menu = get_term_by( 'id', $menu_id, 'nav_menu' );
$menu_item = get_post( $menu_item_db_id );
// Log action for the updated menu
$this->log_action(
[
'action_type' => 'UPDATE',
'title' => $menu->name,
// menus don't have post status. This is for Gatsby
'status' => 'publish',
'node_id' => (int) $menu->term_id,
'relay_id' => Relay::toGlobalId( 'term', (int) $menu->term_id ),
'graphql_single_name' => 'menu',
'graphql_plural_name' => 'menus',
]
);
// Log action for the added menu item
$this->log_action(
[
'action_type' => 'UPDATE',
'title' => $menu_item->post_title,
// menus don't have post status. This is for Gatsby
'status' => 'publish',
'node_id' => (int) $menu_item->ID,
'relay_id' => Relay::toGlobalId( 'post', (int) $menu_item->ID ),
'graphql_single_name' => 'menuItem',
'graphql_plural_name' => 'menuItems',
]
);
}
}
================================================
FILE: src/ActionMonitor/Monitors/PostMonitor.php
================================================
post_author ) && (int) $post_after->post_author !== (int) $post_before->post_author ) {
/**
* Log user update action
*/
$this->log_user_update( $post_after );
$this->log_user_update( $post_before );
}
}
/**
* Log all post status changes ( creating / updating / trashing )
*
* @action transition_post_status
*
* @param mixed $new_status New status.
* @param mixed $old_status Old status.
* @param WP_Post $post Post object.
*/
public function callback_transition_post_status( $new_status, $old_status, WP_Post $post ) {
if ( defined( 'DOING_AUTOSAVE' ) && DOING_AUTOSAVE ) {
return;
}
// If the object is not a valid post, ignore it
if ( ! is_a( $post, 'WP_Post' ) ) {
return;
}
// If the post type is not intentionally tracked, ignore it
if ( ! $this->is_post_type_tracked( $post->post_type ) ) {
return;
}
$initial_post_statuses = [ 'auto-draft', 'inherit', 'new' ];
// If the post is a fresh post that hasn't been made public, don't track the action
if ( in_array( $new_status, $initial_post_statuses, true ) ) {
return;
}
// Saving drafts should not log actions
if ( 'draft' === $new_status && 'draft' === $old_status ) {
return;
}
// If the post isn't coming from a publish state or going to a publish state
// we can ignore the action.
if ( 'publish' !== $old_status && 'publish' !== $new_status ) {
return;
}
$action_type = 'UPDATE';
// If a post is moved from 'publish' to any other status, set the action_type to delete
// to let Gatsby know it should no longer cache the post
if ( 'publish' !== $new_status && 'publish' === $old_status ) {
$action_type = 'DELETE';
// If a post that was not published becomes published, set the action_type to create
// to let Gatsby know it should fetch and cache the post
} elseif ( 'publish' === $new_status && 'publish' !== $old_status ) {
$action_type = 'CREATE';
// If a published post is saved, it's an update.
} elseif ( 'publish' === $new_status && 'publish' === $old_status ) {
$action_type = 'UPDATE';
}
// We don't need to log a user update if the post is simply being updated.
// The exception would be when the post author is changed, but that's
// handled in a different action
if ( 'UPDATE' !== $action_type ) {
$this->log_user_update( $post );
}
$post_type_object = get_post_type_object( $post->post_type );
$action = [
'action_type' => $action_type,
'title' => $post->post_title,
'node_id' => $post->ID,
'relay_id' => Relay::toGlobalId( 'post', $post->ID ),
'graphql_single_name' => $post_type_object->graphql_single_name,
'graphql_plural_name' => $post_type_object->graphql_plural_name,
'status' => $new_status,
];
/**
* Log an action
*/
$this->log_action( $action );
}
/**
* Logs actions when posts are deleted
*
* @param int $post_id The ID of the deleted post
*/
public function callback_deleted_post( int $post_id ) {
$post = get_post( $post_id );
// If there is no post object, do nothing
if ( ! is_a( $post, 'WP_Post' ) ) {
return;
}
// If the deleted post is of a post type that isn't being tracked, do nothing
if ( ! $this->is_post_type_tracked( $post->post_type ) ) {
return;
}
// Ignore posts that were deleted that weren't published
if ( 'publish' !== $post->post_status ) {
return;
}
$post_type_object = get_post_type_object( $post->post_type );
$action = [
'action_type' => 'DELETE',
'title' => $post->post_title,
'node_id' => $post->ID,
'relay_id' => Relay::toGlobalId( 'post', $post->ID ),
'graphql_single_name' => $post_type_object->graphql_single_name,
'graphql_plural_name' => $post_type_object->graphql_plural_name,
'status' => 'trash',
];
// Log the action
$this->log_action( $action );
// Log user update
$this->log_user_update( $post );
}
/**
* Whether the post type is tracked
*
* @param string $post_type The name of the post type to check
*
* @return bool
*/
public function is_post_type_tracked( string $post_type ) {
return in_array( $post_type, $this->action_monitor->get_tracked_post_types(), true );
}
/**
* Logs activity when meta is updated on posts
*
* @param int $meta_id ID of updated metadata entry.
* @param int $object_id ID of the object metadata is for.
* @param string $meta_key Metadata key.
* @param mixed $meta_value Metadata value. Serialized if non-scalar.
*/
public function callback_updated_post_meta( int $meta_id, int $object_id, string $meta_key, $meta_value ) {
$post = get_post( $object_id );
if ( empty( $post ) || ! is_a( $post, 'WP_Post' ) ) {
return;
}
// If the deleted post is of a post type that isn't being tracked, do nothing
if ( ! $this->is_post_type_tracked( $post->post_type ) ) {
return;
}
if ( 'publish' !== $post->post_status ) {
return;
}
if ( false === $this->should_track_meta( $meta_key, $meta_value, $post ) ) {
return;
}
$post_type_object = get_post_type_object( $post->post_type );
$action = [
'action_type' => 'UPDATE',
'title' => $post->post_title,
'node_id' => $post->ID,
'relay_id' => Relay::toGlobalId( 'post', $post->ID ),
'graphql_single_name' => $post_type_object->graphql_single_name,
'graphql_plural_name' => $post_type_object->graphql_plural_name,
'status' => $post->post_status,
];
// Log the action
$this->log_action( $action );
}
/**
* Logs activity when meta is updated on posts
*
* @param string[] $meta_ids An array of metadata entry IDs to delete.
* @param int $object_id ID of the object metadata is for.
* @param string $meta_key Metadata key.
* @param mixed $meta_value Metadata value. Serialized if non-scalar.
*/
public function callback_deleted_post_meta( array $meta_ids, int $object_id, string $meta_key, $meta_value ) {
$post = get_post( $object_id );
if ( empty( $post ) || ! is_a( $post, 'WP_Post' ) ) {
return;
}
// If the deleted post is of a post type that isn't being tracked, do nothing
if ( ! $this->is_post_type_tracked( $post->post_type ) ) {
return;
}
if ( 'publish' !== $post->post_status ) {
return;
}
if ( false === $this->should_track_meta( $meta_key, $meta_value, $post ) ) {
return;
}
$post_type_object = get_post_type_object( $post->post_type );
$action = [
'action_type' => 'UPDATE',
'title' => $post->post_title,
'node_id' => $post->ID,
'relay_id' => Relay::toGlobalId( 'post', $post->ID ),
'graphql_single_name' => $post_type_object->graphql_single_name,
'graphql_plural_name' => $post_type_object->graphql_plural_name,
'status' => $post->post_status,
];
// Log the action
$this->log_action( $action );
}
/**
* Log a user update when a post is created or deleted, telling Gatsby to
* invalidate user caches
*
* @param WP_Post $post The post data of the Post being updated
*
* @todo:
* This should be able to be removed at some point as Gatsby
* _should_ be able to handle bi-directional relationships implicitly. When a Post is
* created, Gatsby queries the full post fields, including the Author.node.id, and should
* be able to handle the relationship between the new post and the author. When a post is
* deleted, Gatsby should remove the post node and any queries (such as author archive pages)
* that include references to the deleted post node should automatically be updated by Gatsby.
*/
public function log_user_update( WP_Post $post ) {
if ( empty( $post->post_author ) || ! absint( $post->post_author ) ) {
return;
}
$user = get_user_by( 'id', absint( $post->post_author ) );
if ( ! $user || 0 === $user->ID ) {
return;
}
$user_monitor = $this->action_monitor->get_action_monitor( 'UserMonitor' );
if ( empty( $user_monitor ) || ! $user_monitor instanceof UserMonitor ) {
return;
}
if ( ! $user_monitor->is_published_author( $user->ID ) ) {
$action_type = 'DELETE';
$status = 'trash';
} else {
$action_type = 'UPDATE';
$status = 'publish';
}
$this->log_action(
[
'action_type' => $action_type,
'title' => $user->display_name,
'node_id' => $user->ID,
'relay_id' => Relay::toGlobalId( 'user', $user->ID ),
'graphql_single_name' => 'user',
'graphql_plural_name' => 'users',
'status' => $status,
]
);
}
}
================================================
FILE: src/ActionMonitor/Monitors/PostTypeMonitor.php
================================================
option_name = '_gatsby_tracked_post_types';
// Check to see if the post types are different
add_action( 'gatsby_init_action_monitors', [ $this, 'check_post_types' ], 999 );
}
/**
* Check post types and trigger a Schema diff if detected
*/
public function check_post_types() {
$this->current_post_types = array_keys( $this->action_monitor->get_tracked_post_types() );
$this->prev_post_types = get_option( $this->option_name, [] );
if ( empty( $this->prev_post_types ) ) {
update_option( $this->option_name, $this->current_post_types );
return;
}
/**
* If the current_post_types and prev_post_types do not match,
* update the option and cache the tracked post types
*/
if ( $this->current_post_types === $this->prev_post_types ) {
return;
}
update_option( $this->option_name, $this->current_post_types );
// Check for added post types
$added = array_diff( $this->current_post_types, $this->prev_post_types );
// Check for removed post types
$removed = array_diff( $this->prev_post_types, $this->current_post_types );
// if there are
if ( ! empty( $added ) ) {
$this->trigger_schema_diff(
[
'title' => __( 'Post Type added', 'WPGatsby' ),
]
);
}
if ( ! empty( $removed ) ) {
$this->trigger_schema_diff(
[
'title' => __( 'Post type removed', 'WPGatsby' ),
]
);
}
}
}
================================================
FILE: src/ActionMonitor/Monitors/PreviewMonitor.php
================================================
post_type ? get_post_type_object( $post->post_type ) : null;
if ( $post_type_object && ! $post_type_object->show_in_graphql ?? true ) {
return plugin_dir_path( __FILE__ ) . '../../Admin/includes/post-type-not-shown-in-graphql.php';
}
// WP doesn't call post_save for every second preview with no content changes.
// Since we're using post_save to trigger the webhook to Gatsby, we need to get WP to call post_save for this post.
do_action( 'save_post', $post->ID, $post, true );
$this->post_to_preview_instance( $post->ID, $post );
return trailingslashit( dirname( __FILE__ ) ) . '../../Admin/includes/preview-template.php';
}
return $template;
}
/**
* Send a Preview to Gatsby
*/
public function post_to_preview_instance( $post_ID, $post ) {
$revisions_are_disabled =
! wp_revisions_enabled( $post );
if (
defined( 'DOING_AUTOSAVE' )
&& DOING_AUTOSAVE
// if revisions are disabled, our autosave is our preview
&& ! $revisions_are_disabled
) {
return;
}
if ( $post->post_type === 'action_monitor' ) {
return;
}
if ( $post->post_status === 'auto-draft' ) {
return;
}
$is_draft = $post->post_status === 'draft';
$is_new_post_draft =
(
$post->post_status === 'auto-draft'
|| $post->post_status === 'draft'
) &&
$post->post_date_gmt === '0000-00-00 00:00:00';
$is_revision = $post->post_type === 'revision';
$is_draft = $post->post_status === 'draft';
$is_gatsby_content_sync_preview = self::is_gatsby_content_sync_preview();
if (
! $is_draft
&& ! $is_revision
&& ! $is_new_post_draft
&& ! $is_gatsby_content_sync_preview
) {
return;
}
$token = \WPGatsby\GraphQL\Auth::get_token();
if ( ! $token ) {
error_log(
'Please set a JWT token in WPGatsby to enable Preview support.'
);
return;
}
$preview_webhook = $this::get_gatsby_preview_webhook();
$original_post = get_post( $post->post_parent );
$this_is_a_publish_not_a_preview =
$original_post
&& $original_post->post_modified === $post->post_modified
&& ! $is_gatsby_content_sync_preview;
if ( $this_is_a_publish_not_a_preview ) {
// we will handle this in ActionMonitor.php, not here.
return;
}
$post_type_object = $original_post
? \get_post_type_object( $original_post->post_type )
: \get_post_type_object( $post->post_type );
if ( ! $post_type_object->show_in_graphql ?? true ) {
// if the post type doesn't have show_in_graphql set,
// we don't want to send a preview webhook for this post type.
return;
}
$parent_post_id = $original_post->ID ?? $post_ID;
$global_relay_id = Relay::toGlobalId(
'post',
// sometimes this is a draft instead of a revision
// so we can't expect original post to exist.
absint( $original_post->ID ?? $post_ID )
);
$referenced_node_single_name
= $post_type_object->graphql_single_name ?? null;
$referenced_node_single_name_normalized = lcfirst(
$referenced_node_single_name
);
$referenced_node_plural_name
= $post_type_object->graphql_plural_name ?? null;
$referenced_node_plural_name_normalized = lcfirst(
$referenced_node_plural_name
);
$graphql_endpoint = apply_filters( 'graphql_endpoint', 'graphql' );
$graphql_url = get_site_url() . '/' . ltrim( $graphql_endpoint, '/' );
$preview_data = [
'previewDatabaseId' => $post_ID,
'id' => $global_relay_id,
'singleName' => $referenced_node_single_name_normalized,
'isDraft' => $is_draft,
'remoteUrl' => $graphql_url,
'modified' => $post->post_modified,
'parentDatabaseId' => $post->post_parent,
'userDatabaseId' => get_current_user_id(),
];
$post_body = array_merge(
$preview_data,
[
'token' => $token
]
);
$this->log_action( [
'action_type' => 'UPDATE',
'title' => $post->post_title,
'node_id' => $parent_post_id,
'relay_id' => $global_relay_id,
'graphql_single_name' => $referenced_node_single_name_normalized,
'graphql_plural_name' => $referenced_node_plural_name_normalized,
// everything that should show in Gatsby is publish
// as far as Gatsby is concerned.
'status' => 'publish',
'stream_type' => 'PREVIEW',
'preview_data' => wp_json_encode( $preview_data ),
] );
// @todo move this to shutdown hook to prevent race conditions
$response = wp_remote_post(
$preview_webhook,
[
'body' => wp_json_encode( $post_body ),
'headers' => [
'Content-Type' => 'application/json; charset=utf-8',
],
'method' => 'POST',
'data_format' => 'body',
]
);
if ( \is_wp_error( $response ) ) {
error_log( "WPGatsby couldn\'t POST to the Preview webhook set in plugin options.\nWebhook returned error: {$response->get_error_message()}" );
}
}
/**
* Get the Gatsby Preview instance refresh webhook
*/
static function get_gatsby_preview_webhook() {
$preview_webhook = Settings::get_setting( 'preview_api_webhook' );
if ( ! $preview_webhook || ! filter_var( $preview_webhook, FILTER_VALIDATE_URL ) ) {
return false;
}
if ( substr( $preview_webhook, -1 ) !== '/' ) {
$preview_webhook .= '/';
}
return $preview_webhook;
}
}
================================================
FILE: src/ActionMonitor/Monitors/SettingsMonitor.php
================================================
should_track_option( $option_name, $old_value, $value ) ) {
return;
}
$this->trigger_non_node_root_field_update(
[
'title' => __( 'Update Setting: ', 'WPGatsby' ) . ' ' . $option_name,
]
);
}
/**
* Log action when permalink_structure is changed
*
* @param mixed $old_value The old option value.
* @param mixed $new_value The new option value.
* @param string $option_name Name of the option to update.
*/
public function callback_update_permalink_structure( $old_value, $new_value, string $option_name ) {
if ( $old_value === $new_value ) {
return;
}
$this->trigger_refetch_all(
[
'title' => __( 'Permalink structure updated', 'WPGatsby' ),
]
);
}
/**
* Log action when page_on_front is changed
*
* @param mixed $old_value The old option value.
* @param mixed $new_value The new option value.
* @param string $option_name Name of the option to update.
*/
public function callback_update_page_on_front( $old_value, $new_value, string $option_name ) {
if ( (int) $old_value === (int) $new_value ) {
return;
}
$old_page = get_post( absint( $old_value ) );
$new_page = get_post( absint( $new_value ) );
if ( $old_page instanceof \WP_Post ) {
$this->log_action(
[
'action_type' => 'UPDATE',
'title' => $old_page->post_title,
'node_id' => $old_page->ID,
'relay_id' => Relay::toGlobalId( 'post', $old_page->ID ),
'graphql_single_name' => get_post_type_object( $old_page->post_type )->graphql_single_name,
'graphql_plural_name' => get_post_type_object( $old_page->post_type )->graphql_plural_name,
'status' => $old_page->post_status,
]
);
}
if ( $new_page instanceof \WP_Post ) {
$this->log_action(
[
'action_type' => 'UPDATE',
'title' => $new_page->post_title,
'node_id' => $new_page->ID,
'relay_id' => Relay::toGlobalId( 'post', $new_page->ID ),
'graphql_single_name' => get_post_type_object( $new_page->post_type )->graphql_single_name,
'graphql_plural_name' => get_post_type_object( $new_page->post_type )->graphql_plural_name,
'status' => $new_page->post_status,
]
);
}
}
/**
* Log action when page_for_posts is changed
*
* @param mixed $old_value The old option value.
* @param mixed $new_value The new option value.
* @param string $option_name Name of the option to update.
*/
public function callback_update_page_for_posts( $old_value, $new_value, string $option_name ) {
if ( (int) $old_value === (int) $new_value ) {
return;
}
$old_page = get_post( absint( $old_value ) );
$new_page = get_post( absint( $new_value ) );
if ( $old_page instanceof \WP_Post ) {
$this->log_action(
[
'action_type' => 'UPDATE',
'title' => $old_page->post_title,
'node_id' => $old_page->ID,
'relay_id' => Relay::toGlobalId( 'post', $old_page->ID ),
'graphql_single_name' => get_post_type_object( $old_page->post_type )->graphql_single_name,
'graphql_plural_name' => get_post_type_object( $old_page->post_type )->graphql_plural_name,
'status' => $old_page->post_status,
]
);
} else {
$this->log_action(
[
'action_type' => 'UPDATE',
'title' => 'Change page on front away from posts',
'node_id' => 'post',
'relay_id' => Relay::toGlobalId( 'post_type', 'post' ),
'graphql_single_name' => 'contentType',
'graphql_plural_name' => 'contentTypes',
'status' => 'publish',
]
);
}
if ( $new_page instanceof \WP_Post ) {
$this->log_action(
[
'action_type' => 'UPDATE',
'title' => $new_page->post_title,
'node_id' => $new_page->ID,
'relay_id' => Relay::toGlobalId( 'post', $new_page->ID ),
'graphql_single_name' => get_post_type_object( $new_page->post_type )->graphql_single_name,
'graphql_plural_name' => get_post_type_object( $new_page->post_type )->graphql_plural_name,
'status' => $new_page->post_status,
]
);
} else {
$this->log_action(
[
'action_type' => 'UPDATE',
'title' => 'Set page on front to posts',
'node_id' => 'post',
'relay_id' => Relay::toGlobalId( 'post_type', 'post' ),
'graphql_single_name' => 'contentType',
'graphql_plural_name' => 'contentTypes',
'status' => 'publish',
]
);
}
}
}
================================================
FILE: src/ActionMonitor/Monitors/TaxonomyMonitor.php
================================================
option_name = '_gatsby_tracked_taxonomies';
// Check to see if the taxonomies are different
add_action( 'gatsby_init_action_monitors', [ $this, 'check_taxonomies' ], 999 );
}
/**
* Check taxonomies and trigger a Schema diff if detected
*/
public function check_taxonomies() {
$this->current_taxonomies = array_keys( $this->action_monitor->get_tracked_taxonomies() );
$this->prev_taxonomies = get_option( $this->option_name, [] );
if ( empty( $this->prev_taxonomies ) ) {
update_option( $this->option_name, $this->current_taxonomies );
return;
}
/**
* If the current_taxonomies and prev_taxonomies do not match,
* update the option and cache the tracked taxonomies
*/
if ( $this->current_taxonomies === $this->prev_taxonomies ) {
return;
}
update_option( $this->option_name, $this->current_taxonomies );
// Check for added taxonomies
$added = array_diff( $this->current_taxonomies, $this->prev_taxonomies );
// Check for removed taxonomies
$removed = array_diff( $this->prev_taxonomies, $this->current_taxonomies );
// if there are
if ( ! empty( $added ) ) {
$this->trigger_schema_diff(
[
'title' => __( 'Taxonomy added', 'WPGatsby' ),
]
);
}
if ( ! empty( $removed ) ) {
$this->trigger_schema_diff(
[
'title' => __( 'Taxonomy removed', 'WPGatsby' ),
]
);
}
}
}
================================================
FILE: src/ActionMonitor/Monitors/TermMonitor.php
================================================
action_monitor->get_tracked_taxonomies(), true );
}
/**
* Tracks creation of terms
*
* @param int $term_id Term ID.
* @param int $tt_id Taxonomy term ID.
* @param string $taxonomy Taxonomy name.
*/
public function callback_created_term( int $term_id, int $tt_id, string $taxonomy ) {
$tax_object = get_taxonomy( $taxonomy );
// If the term is in a taxonomy that's not being tracked, ignore it
if ( false === $tax_object || ! $this->is_taxonomy_tracked( $taxonomy ) ) {
return;
}
$term = get_term( $term_id, $taxonomy );
if ( ! is_a( $term, 'WP_Term' ) ) {
return;
}
$this->log_action(
[
'action_type' => 'CREATE',
'title' => $term->name,
'node_id' => $term->term_id,
'relay_id' => Relay::toGlobalId( 'term', $term->term_id ),
'graphql_single_name' => $tax_object->graphql_single_name,
'graphql_plural_name' => $tax_object->graphql_plural_name,
'status' => 'publish',
]
);
if ( true === $tax_object->hierarchical ) {
$this->update_hierarchical_relatives( $term, $tax_object );
}
}
/**
* @param int $term_id The ID of the term object being deleted
* @param string $taxonomy The name of the taxonomy of the term being deleted
*/
public function callback_pre_delete_term( int $term_id, string $taxonomy ) {
$term = get_term_by( 'id', $term_id, $taxonomy );
if ( ! $term instanceof WP_Term ) {
return;
}
$before_delete = [
'term' => $term,
];
if ( true === get_taxonomy( $taxonomy )->hierarchical ) {
$term_children = get_term_children( $term->term_id, $taxonomy );
if ( ! empty( $term_children ) ) {
$before_delete['children'] = $term_children;
}
}
$this->terms_before_delete[ $term->term_id ] = $before_delete;
}
/**
* Tracks deletion of taxonomy terms
*
* @param int $term_id Term ID.
* @param int $tt_id Taxonomy term ID.
* @param string $taxonomy Taxonomy name.
* @param mixed $deleted_term Deleted term object.
*/
public function callback_delete_term( int $term_id, int $tt_id, string $taxonomy, $deleted_term ) {
$tax_object = get_taxonomy( $taxonomy );
if ( false === $tax_object || ! $this->is_taxonomy_tracked( $taxonomy ) ) {
return;
}
$this->log_action(
[
'action_type' => 'DELETE',
'title' => $deleted_term->name,
'node_id' => $deleted_term->term_id,
'relay_id' => Relay::toGlobalId( 'term', $deleted_term->term_id ),
'graphql_single_name' => $tax_object->graphql_single_name,
'graphql_plural_name' => $tax_object->graphql_plural_name,
'status' => 'trash',
]
);
if ( true === $tax_object->hierarchical ) {
$this->update_hierarchical_relatives( $deleted_term, $tax_object );
}
}
/**
* Tracks updated of taxonomy terms
*
* @param int $term_id Term ID.
* @param int $tt_id Taxonomy term ID.
* @param string $taxonomy Taxonomy name.
*/
public function callback_edited_term( int $term_id, int $tt_id, string $taxonomy ) {
$tax_object = get_taxonomy( $taxonomy );
if ( false === $tax_object || ! $this->is_taxonomy_tracked( $taxonomy ) ) {
return;
}
$term = get_term( $term_id, $taxonomy );
$this->log_action(
[
'action_type' => 'UPDATE',
'title' => $term->name,
'node_id' => $term->term_id,
'relay_id' => Relay::toGlobalId( 'term', $term->term_id ),
'graphql_single_name' => $tax_object->graphql_single_name,
'graphql_plural_name' => $tax_object->graphql_plural_name,
'status' => 'publish',
]
);
if ( true === $tax_object->hierarchical ) {
$this->update_hierarchical_relatives( $term, $tax_object );
}
}
public function update_hierarchical_relatives( WP_Term $term, WP_Taxonomy $tax_object ) {
$taxonomy = $tax_object->name;
if ( true === $tax_object->hierarchical ) {
if ( ! empty( $term->parent ) ) {
$parent = get_term_by( 'id', absint( $term->parent ), $taxonomy );
if ( is_a( $parent, 'WP_Term' ) ) {
$this->log_action(
[
'action_type' => 'UPDATE',
'title' => $parent->name . ' Parent',
'node_id' => $parent->term_id,
'relay_id' => Relay::toGlobalId( 'term', $parent->term_id ),
'graphql_single_name' => $tax_object->graphql_single_name,
'graphql_plural_name' => $tax_object->graphql_plural_name,
'status' => 'publish',
]
);
}
}
if ( isset( $this->terms_before_delete[ $term->term_id ]['children'] ) ) {
$child_ids = $this->terms_before_delete[ $term->term_id ]['children'];
} else {
$child_ids = get_term_children( $term->term_id, $taxonomy );
}
if ( ! empty( $child_ids ) && is_array( $child_ids ) ) {
foreach ( $child_ids as $child_term_id ) {
$child_term = get_term_by( 'id', $child_term_id, $taxonomy );
if ( ! empty( $child_term ) ) {
$this->log_action(
[
'action_type' => 'UPDATE',
'title' => $child_term->name . ' Parent',
'node_id' => $child_term->term_id,
'relay_id' => Relay::toGlobalId( 'term', $child_term->term_id ),
'graphql_single_name' => $tax_object->graphql_single_name,
'graphql_plural_name' => $tax_object->graphql_plural_name,
'status' => 'publish',
]
);
}
}
}
}
}
/**
* Logs activity when meta is updated on terms
*
* @param int $meta_id ID of updated metadata entry.
* @param int $object_id ID of the object metadata is for.
* @param string $meta_key Metadata key.
* @param mixed $meta_value Metadata value. Serialized if non-scalar.
*/
public function callback_updated_term_meta( int $meta_id, int $object_id, string $meta_key, $meta_value ) {
if ( empty( $term = get_term( $object_id ) ) || ! is_a( $term, 'WP_Term' ) ) {
return;
}
$tax_object = get_taxonomy( $term->taxonomy );
// If the updated term is of a post type that isn't being tracked, do nothing
if ( false === $tax_object || ! $this->is_taxonomy_tracked( $term->taxonomy ) ) {
return;
}
if ( false === $this->should_track_meta( $meta_key, $meta_value, $term ) ) {
return;
}
$action = [
'action_type' => 'UPDATE',
'title' => $term->name,
'node_id' => $term->term_id,
'relay_id' => Relay::toGlobalId( 'term', $term->term_id ),
'graphql_single_name' => $tax_object->graphql_single_name,
'graphql_plural_name' => $tax_object->graphql_plural_name,
'status' => 'publish',
];
// Log the action
$this->log_action( $action );
}
/**
* Logs activity when meta is updated on terms
*
* @param string[] $meta_ids An array of metadata entry IDs to delete.
* @param int $object_id ID of the object metadata is for.
* @param string $meta_key Metadata key.
* @param mixed $meta_value Metadata value. Serialized if non-scalar.
*/
public function callback_deleted_term_meta( array $meta_ids, int $object_id, string $meta_key, $meta_value ) {
if ( empty( $term = get_term( $object_id ) ) || ! is_a( $term, 'WP_Term' ) ) {
return;
}
$tax_object = get_taxonomy( $term->taxonomy );
// If the updated term is of a post type that isn't being tracked, do nothing
if ( false === $tax_object || ! $this->is_taxonomy_tracked( $term->taxonomy ) ) {
return;
}
if ( false === $this->should_track_meta( $meta_key, $meta_value, $term ) ) {
return;
}
$action = [
'action_type' => 'UPDATE',
'title' => $term->name,
'node_id' => $term->term_id,
'relay_id' => Relay::toGlobalId( 'term', $term->term_id ),
'graphql_single_name' => $tax_object->graphql_single_name,
'graphql_plural_name' => $tax_object->graphql_plural_name,
'status' => 'publish',
];
// Log the action
$this->log_action( $action );
}
}
================================================
FILE: src/ActionMonitor/Monitors/UserMonitor.php
================================================
*/
protected $users_before_delete;
/**
* IDs of posts to reassign
*
* @var array
*/
protected $post_ids_to_reassign;
/**
* Initialize UserMonitor Actions
*
* @return void
*/
public function init() {
$this->post_ids_to_reassign = [];
add_action( 'profile_update', [ $this, 'callback_profile_update' ], 10, 1 );
add_action( 'delete_user', [ $this, 'callback_delete_user' ], 10, 2 );
add_action( 'deleted_user', [ $this, 'callback_deleted_user' ], 10, 1 );
add_action( 'updated_user_meta', [ $this, 'callback_updated_user_meta' ], 10, 4 );
add_action( 'added_user_meta', [ $this, 'callback_updated_user_meta' ], 10, 4 );
add_action( 'deleted_user_meta', [ $this, 'callback_deleted_user_meta' ], 10, 4 );
}
/**
* This method accepts a user ID, and checks if the user has published posts
* of any of the tracked post types
*
* @param int $user_id The ID of the user to check
*
* @return bool
*/
public function is_published_author( int $user_id ) {
$post_types = $this->action_monitor->get_tracked_post_types();
$published_posts_count = count_user_posts( $user_id, $post_types );
if ( empty( $published_posts_count ) ) {
return false;
}
return true;
}
/**
* Determines whether the meta should be tracked or not.
*
* User meta is all untracked other than a few specific keys. Plugins and themes that
* expose user meta intended for public display will need to filter this to
* have updates to those meta fields trigger updates with Gatsby.
*
* @param string $meta_key Metadata key.
* @param mixed $meta_value Metadata value. Serialized if non-scalar.
* @param object $object The object the metadata is for.
*
* @return bool
*/
public function should_track_meta( string $meta_key, $meta_value, $object ) {
$tracked_meta_keys = [
'description',
'nickname',
'firstName',
'lastName',
];
$tracked_meta_keys = apply_filters( 'gatsby_action_monitor_tracked_user_meta_keys', $tracked_meta_keys, $meta_key, $meta_value, $object );
if ( in_array( $meta_key, $tracked_meta_keys, true ) ) {
return true;
}
return false;
}
/**
* Log action when a user is updated.
*
* @param int $user_id
*/
public function callback_profile_update( int $user_id ) {
if ( empty( $user_id ) ) {
return;
}
$user = get_user_by( 'id', $user_id );
if ( ! $user instanceof \WP_User || $user_id !== $user->ID ) {
return;
}
if ( ! $this->is_published_author( $user_id ) ) {
return;
}
$this->log_action(
[
'action_type' => 'UPDATE',
'title' => $user->display_name,
'node_id' => (int) $user->ID,
'relay_id' => Relay::toGlobalId( 'user', (int) $user->ID ),
'graphql_single_name' => 'user',
'graphql_plural_name' => 'users',
'status' => 'publish',
]
);
}
/**
* There's no logging in this callback's action, the reason
* behind this hook is so that we can store user objects before
* being deleted.
*
* During `deleted_user` hook, our callback
* receives $user_id param but it's useless as the user record
* was already removed from DB.
*
* @param mixed|int|null $user_id User ID that may be deleted
* @param mixed|int|null $reassign_id User ID that posts should be reassigned to
*/
public function callback_delete_user( $user_id, $reassign_id ) {
if ( empty( $user_id ) ) {
return;
}
if ( ! $this->is_published_author( $user_id ) ) {
return;
}
// Get the user the posts should be re-assigned to
$reassign_user = ! empty( $reassign_id ) ? get_user_by( 'id', $reassign_id ) : null;
if ( ! empty( $reassign_user ) ) {
// @todo: We should get rid of this as it can get expensive to log these actions.
// Gatsby Source WordPress should have support for bulk-actions so we can log a single action
// such as "DELETE_AUTHOR_AND_REASSIGN_POSTS" and pass the old author ID and new author ID and
// Gatsby could do it without an action per modified post.
global $wpdb;
$post_types = $this->action_monitor->get_tracked_post_types();
$post_types = implode( "', '", $post_types );
$post_ids = $wpdb->get_col( $wpdb->prepare( "SELECT ID FROM $wpdb->posts WHERE post_author = %d AND post_status = 'publish' AND post_type IN ('$post_types')", $user_id ) );
if ( ! empty( $post_ids ) && is_array( $post_ids ) ) {
$this->post_ids_to_reassign = array_merge( $this->post_ids_to_reassign, $post_ids );
}
}
$this->users_before_delete[ (int) $user_id ] = [
'user' => get_user_by( 'id', (int) $user_id ),
'reassign' => ! empty( $reassign_user ) && $reassign_user instanceof \WP_User ? $reassign_user : null,
];
}
/**
* Log deleted user.
*
* @param int $user_id Deleted user ID
*/
public function callback_deleted_user( int $user_id ) {
$before_delete = isset( $this->users_before_delete[ (int) $user_id ] ) ? $this->users_before_delete[ (int) $user_id ] : null;
if ( empty( $before_delete ) || ! isset( $before_delete['user']->data->display_name ) ) {
return;
}
$this->log_action(
[
'action_type' => 'DELETE',
'title' => $before_delete['user']->data->display_name,
'node_id' => (int) $before_delete['user']->ID,
'relay_id' => Relay::toGlobalId( 'user', (int) $before_delete['user']->ID ),
'graphql_single_name' => 'user',
'graphql_plural_name' => 'users',
'status' => 'trash',
]
);
if ( isset( $before_delete['reassign']->display_name ) ) {
$this->log_action(
[
'action_type' => 'UPDATE',
'title' => $before_delete['reassign']->display_name,
'node_id' => (int) $before_delete['reassign']->ID,
'relay_id' => Relay::toGlobalId( 'user', (int) $before_delete['reassign']->ID ),
'graphql_single_name' => 'user',
'graphql_plural_name' => 'users',
'status' => 'publish',
]
);
if ( ! empty( $this->post_ids_to_reassign ) && is_array( $this->post_ids_to_reassign ) ) {
foreach ( $this->post_ids_to_reassign as $post_id ) {
// If there's a post for the Post ID
if ( ! empty( $post = get_post( absint( $post_id ) ) ) ) {
// If the post status is not published, don't track an action for it
if ( 'publish' !== $post->post_status ) {
return;
}
// Get the post type object
$post_type_object = get_post_type_object( $post->post_type );
// Log an action for the post being re-assigned
$this->log_action(
[
'action_type' => 'UPDATE',
'title' => $post->post_title,
'node_id' => (int) $post_id,
'relay_id' => Relay::toGlobalId( 'post', (int) $post_id ),
'graphql_single_name' => $post_type_object->graphql_single_name,
'graphql_plural_name' => $post_type_object->graphql_plural_name,
'status' => 'publish',
]
);
}
}
}
}
}
/**
* Logs activity when meta is updated for a user
*
* @param int $meta_id ID of updated metadata entry.
* @param int $object_id ID of the object metadata is for.
* @param string $meta_key Metadata key.
* @param mixed $meta_value Metadata value. Serialized if non-scalar.
*/
public function callback_updated_user_meta( int $meta_id, int $object_id, string $meta_key, $meta_value ) {
if ( empty( $user = get_user_by( 'id', $object_id ) ) || ! is_a( $user, 'WP_User' ) ) {
return;
}
if ( ! $this->is_published_author( $object_id ) ) {
return;
}
if ( false === $this->should_track_meta( $meta_key, $meta_value, $user ) ) {
return;
}
$action = [
'action_type' => 'UPDATE',
'title' => $user->display_name,
'node_id' => (int) $user->ID,
'relay_id' => Relay::toGlobalId( 'user', (int) $user->ID ),
'graphql_single_name' => 'user',
'graphql_plural_name' => 'users',
'status' => 'publish',
];
// Log the action
$this->log_action( $action );
}
/**
* Logs activity when meta is updated on terms
*
* @param string[] $meta_ids An array of metadata entry IDs to delete.
* @param int $object_id ID of the object metadata is for.
* @param string $meta_key Metadata key.
* @param mixed $meta_value Metadata value. Serialized if non-scalar.
*/
public function callback_deleted_user_meta( array $meta_ids, int $object_id, string $meta_key, $meta_value ) {
if ( empty( $user = get_user_by( 'id', $object_id ) ) || ! is_a( $user, 'WP_User' ) ) {
return;
}
if ( ! $this->is_published_author( $object_id ) ) {
return;
}
if ( ! $this->should_track_meta( $meta_key, $meta_value, $user ) ) {
return;
}
$action = [
'action_type' => 'UPDATE',
'title' => $user->display_name,
'node_id' => (int) $user->ID,
'relay_id' => Relay::toGlobalId( 'user', (int) $user->ID ),
'graphql_single_name' => 'user',
'graphql_plural_name' => 'users',
'status' => 'publish',
];
// Log the action
$this->log_action( $action );
}
}
================================================
FILE: src/Admin/Preview.php
================================================
register_preview_status_fields_and_mutations();
}
);
}
public static function get_gatsby_content_sync_url_for_post( $post ) {
// get the Gatsby Cloud loader url w/ site id
$gatsby_content_sync_url = Settings::get_setting( 'gatsby_content_sync_url' );
// Create the dynamic path Content Sync will need
$manifest_id = self::get_preview_manifest_id_for_post( $post );
$content_id = $post->ID;
$path = "/gatsby-source-wordpress/$manifest_id/$content_id";
$url = preg_replace(
// remove any double forward slashes from the path
'/([^:])(\/{2,})/', '$1/',
"$gatsby_content_sync_url$path"
);
return $url;
}
public static function get_previewable_post_object_by_post_id( $post_id ) {
$revision = array_values(
wp_get_post_revisions( $post_id )
)[0]
// or if revisions are disabled, get the autosave
?? wp_get_post_autosave( $post_id, get_current_user_id() )
// otherwise we can't preview anything
?? null;
if ( $revision ) {
return $revision;
}
return get_post( $post_id );
}
public static function get_preview_manifest_id_for_post( $post ) {
$revision = self::get_previewable_post_object_by_post_id( $post->ID );
$revision_modified = $revision->post_modified ?? null;
$modified =
$post->post_status === "draft"
? $post->post_modified
: $revision_modified;
if ( ! $modified || $modified === "" ) {
return null;
}
$manifest_id = $post->ID . $modified;
return $manifest_id;
}
/**
* This is used to print out the client CSS file directly to the
* Preview template html when Content Sync isn't set up correctly.
*/
public static function print_file_contents( $fileName ) {
$pluginDirectory = plugin_dir_path( __FILE__ );
$filePath = $pluginDirectory . $fileName;
echo file_get_contents( $filePath );
}
function register_preview_status_fields_and_mutations() {
register_graphql_enum_type(
'WPGatsbyRemotePreviewStatusEnum',
[
'description' => __( 'The different statuses a Gatsby Preview can be in for a single node.', 'wp-gatsby' ),
'values' => [
'PREVIEW_SUCCESS' => [
'value' => 'PREVIEW_SUCCESS',
],
'NO_PAGE_CREATED_FOR_PREVIEWED_NODE' => [
'value' => 'NO_PAGE_CREATED_FOR_PREVIEWED_NODE',
],
'GATSBY_PREVIEW_PROCESS_ERROR' => [
'value' => 'GATSBY_PREVIEW_PROCESS_ERROR',
],
'RECEIVED_PREVIEW_DATA_FROM_WRONG_URL' => [
'value' => 'RECEIVED_PREVIEW_DATA_FROM_WRONG_URL',
],
],
]
);
register_graphql_mutation(
'wpGatsbyRemotePreviewStatus',
[
'inputFields' => [
// parentDatabaseId is the only input arg we need now.
// the rest are left for backwards compatibility so errors aren't thrown.
'parentDatabaseId' => [
'type' => 'Number',
'description' => __( 'The previewed revisions post parent id', 'wp-gatsby' ),
],
'pagePath' => [
'type' => 'String',
'description' => __( 'The Gatsby page path for this preview.', 'wp-gatsby' ),
],
'modified' => [
'type' => 'String',
'description' => __( 'The modified date of the latest revision for this preview.', 'wp-gatsby' ),
],
'status' => [
'type' => [ 'non_null' => 'WPGatsbyRemotePreviewStatusEnum' ],
'description' => __( 'The remote status of the previewed node', 'wp-gatsby' ),
],
'statusContext' => [
'type' => 'String',
'description' => __( 'Additional context about the preview status', 'wp-gatsby' ),
],
],
'outputFields' => [
'success' => [
'type' => 'Boolean',
'description' => __( 'Wether or not the revision mutation was successful', 'wp-gatsby' ),
'resolve' => function( $payload, $args, $context, $info ) {
$success = $payload['success'] ?? null;
return [
'success' => $success,
];
},
],
],
'mutateAndGetPayload' => function( $input, $context, $info ) {
$parent_id = $input['parentDatabaseId'] ?? null;
$post = get_post( $parent_id );
$post_type_object = $post
? get_post_type_object( $post->post_type )
: null;
$user_can_edit_this_post = $post
? current_user_can(
$post_type_object->cap->edit_posts,
$parent_id
)
: null;
if ( ! $post || ! $user_can_edit_this_post ) {
$message = sprintf(
__(
'Sorry, you are not allowed to update post %1$s',
'wp-gatsby'
),
$parent_id
);
throw new UserError( $message );
}
// delete action monitor preview action.
// once we've saved this preview status as succes
// we don't need the preview action anymore.
$existing = new \WP_Query( [
'post_type' => 'action_monitor',
'post_status' => 'any',
'posts_per_page' => 1,
'no_found_rows' => true,
'fields' => 'ids',
'tax_query' => [
'relation' => 'AND',
[
'taxonomy' => 'gatsby_action_ref_node_dbid',
'field' => 'name',
'terms' => sanitize_text_field( $parent_id ),
],
[
'taxonomy' => 'gatsby_action_stream_type',
'field' => 'name',
'terms' => 'PREVIEW',
]
],
] );
if ( isset( $existing->posts ) && ! empty( $existing->posts ) ) {
wp_delete_post( $existing->posts[0], true );
}
return [
'success' => true,
];
},
]
);
register_graphql_object_type(
'WPGatsbyPageNode',
[
'description' => __( 'A previewed Gatsby page node.' ),
'fields' => [
'path' => [
'type' => 'String',
],
],
]
);
register_graphql_enum_type(
'WPGatsbyWPPreviewedNodeStatus',
[
'description' => __( 'The different statuses a Gatsby Preview can be in for a single node.', 'wp-gatsby' ),
'values' => [
'NO_NODE_FOUND' => [
'value' => 'NO_NODE_FOUND',
],
'PREVIEW_READY' => [
'value' => 'PREVIEW_READY',
],
'REMOTE_NODE_NOT_YET_UPDATED' => [
'value' => 'REMOTE_NODE_NOT_YET_UPDATED',
],
'NO_PREVIEW_PATH_FOUND' => [
'value' => 'NO_PREVIEW_PATH_FOUND',
],
'RECEIVED_PREVIEW_DATA_FROM_WRONG_URL' => [
'value' => 'RECEIVED_PREVIEW_DATA_FROM_WRONG_URL',
],
'PREVIEW_PAGE_UPDATED_BUT_NOT_YET_DEPLOYED' => [
'value' => 'PREVIEW_PAGE_UPDATED_BUT_NOT_YET_DEPLOYED',
],
],
]
);
register_graphql_object_type(
'WPGatsbyPreviewStatus',
[
'description' => __( 'Check compatibility with a given version of gatsby-source-wordpress and the WordPress source site.' ),
'fields' => [
'pageNode' => [
'type' => 'WPGatsbyPageNode',
],
'statusType' => [
'type' => 'WPGatsbyWPPreviewedNodeStatus',
],
'remoteStatus' => [
'type' => 'WPGatsbyRemotePreviewStatusEnum',
],
'modifiedLocal' => [
'type' => 'String',
],
'modifiedRemote' => [
'type' => 'String',
],
'statusContext' => [
'type' => 'String',
],
],
]
);
register_graphql_field(
'WPGatsby',
'gatsbyPreviewStatus',
[
'description' => __( 'The current status of a Gatsby Preview.', 'wp-gatsby' ),
'type' => 'WPGatsbyPreviewStatus',
'args' => [
'nodeId' => [
'type' => [ 'non_null' => 'Number' ],
'description' => __( 'The post id for the previewed node.', 'wp-gatsby' ),
],
],
'resolve' => function( $root, $args, $context, $info ) {
$post_id = $args['nodeId'] ?? null;
// make sure post_id is a valid post
$post = get_post( $post_id );
$post_type_object = $post
? get_post_type_object( $post->post_type )
: null;
$user_can_edit_this_post = $post
? current_user_can(
$post_type_object->cap->edit_posts,
$post_id
)
: null;
if ( ! $post || ! $user_can_edit_this_post ) {
throw new UserError(
sprintf(
__(
'Sorry, you are not allowed to access the Preview status of post %1$s',
'wp-gatsby'
),
$post_id
)
);
}
if ( ! $post ) {
return [
'statusType' => 'NO_NODE_FOUND',
];
}
$found_preview_path_post_meta = get_post_meta(
$post_id,
'_wpgatsby_page_path',
true
);
$revision = Preview::getPreviewablePostObjectByPostId( $post_id );
$revision_modified = $revision->post_modified ?? null;
$modified = $revision_modified ?? $post->post_modified;
$gatsby_node_modified = get_post_meta(
$post_id,
'_wpgatsby_node_modified',
true
);
$remote_status = get_post_meta(
$post_id,
'_wpgatsby_node_remote_preview_status',
true
);
$node_modified_was_updated =
strtotime( $gatsby_node_modified ) >= strtotime( $modified );
if (
$node_modified_was_updated
&& (
'NO_PAGE_CREATED_FOR_PREVIEWED_NODE' === $remote_status
|| 'RECEIVED_PREVIEW_DATA_FROM_WRONG_URL' === $remote_status
)
) {
return [
'statusType' => null,
'statusContext' => null,
'remoteStatus' => $remote_status,
];
}
$node_was_updated = false;
if ( $node_modified_was_updated && $found_preview_path_post_meta ) {
$server_side = true;
$gatbsy_preview_frontend_url =
self::get_gatsby_preview_instance_url(
$server_side
);
$page_data_path = $found_preview_path_post_meta === "/"
? "/index/"
: $found_preview_path_post_meta;
$page_data_path_trimmed = trim( $page_data_path, "/" );
$modified_deployed_url =
$gatbsy_preview_frontend_url .
"page-data/$page_data_path_trimmed/page-data.json";
// check if node page was deployed
$request = wp_remote_get( $modified_deployed_url );
$response = wp_remote_retrieve_body( $request );
$page_data = json_decode( $response );
$modified_response =
$page_data->result->pageContext->__wpGatsbyNodeModified
?? null;
error_log(print_r('$modified_response', true));
error_log(print_r($modified_response, true));
error_log(print_r('$modified', true));
error_log(print_r($modified, true));
$preview_was_deployed =
$modified_response &&
strtotime( $modified_response ) >= strtotime( $modified );
error_log(print_r('$preview_was_deployed', true));
error_log(print_r($preview_was_deployed, true));
if ( ! $preview_was_deployed ) {
// if preview was not yet deployed, send back PREVIEW_PAGE_UPDATED_BUT_NOT_YET_DEPLOYED.
return [
'statusType' =>
'PREVIEW_PAGE_UPDATED_BUT_NOT_YET_DEPLOYED',
'statusContext' => null,
'remoteStatus' => null,
];
} else {
// if it is deployed, send back PREVIEW_READY below.
$node_was_updated = true;
}
}
// if the node wasn't updated, then any status we have is stale.
$remote_status_type = $remote_status && $node_was_updated
? $remote_status
: null;
/**
* We need the above check for wether the node was updated so we
* don't show stale statuses on existing nodes, but in the case that
* it's a brand new draft, $node_was_updated will always be false
* because at this point we're potentially getting an error on a
* node that was never created. So GATSBY_PREVIEW_PROCESS_ERROR is a
* special case where we always need to show the status regardless
* of wether the node was updated.
*/
if ( 'GATSBY_PREVIEW_PROCESS_ERROR' === $remote_status ) {
$remote_status_type = $remote_status;
}
$status_type = 'PREVIEW_READY';
if ( ! $node_was_updated ) {
$status_type = 'REMOTE_NODE_NOT_YET_UPDATED';
}
if ( ! $found_preview_path_post_meta ) {
$status_type = 'NO_PREVIEW_PATH_FOUND';
}
$status_context = get_post_meta(
$post_id,
'_wpgatsby_node_remote_preview_status_context',
true
);
if ( $status_context === '' ) {
$status_context = null;
}
$normalized_preview_page_path =
$found_preview_path_post_meta !== ''
? $found_preview_path_post_meta
: null;
return [
'statusType' => $status_type,
'statusContext' => $status_context,
'remoteStatus' => $remote_status_type,
'pageNode' => [
'path' => $normalized_preview_page_path,
],
'modifiedLocal' => $modified,
'modifiedRemote' => $gatsby_node_modified,
];
},
]
);
register_graphql_field(
'WPGatsby',
'isPreviewFrontendOnline',
[
'description' => __( 'Wether or not the Preview frontend URL is online.', 'wp-gatsby' ),
'type' => 'Boolean',
'resolve' => function( $root, $args, $context, $info ) {
if ( ! is_user_logged_in() ) {
return false;
}
$preview_url = self::get_gatsby_preview_instance_url();
$request = wp_remote_get( $preview_url );
$request_was_successful =
$this->was_request_successful( $request );
return $request_was_successful;
},
]
);
}
}
================================================
FILE: src/Admin/Settings.php
================================================
settings_api = new \WPGraphQL_Settings_API;
add_action( 'init', [ $this, 'set_default_jwt_key' ] );
add_action( 'admin_init', [ $this, 'admin_init' ] );
add_action( 'admin_menu', [ $this, 'register_settings_page' ] );
// Filter the GraphQL Settings for introspection to force enable Introspection when WPGatsby is active
add_filter( 'graphql_setting_field_config', [ $this, 'filter_graphql_introspection_setting_field' ], 10, 3 );
add_filter( 'graphql_get_setting_section_field_value', [ $this, 'filter_graphql_introspection_setting_value' ], 10, 5 );
}
/**
* If the settings haven't been saved yet, save the JWT once to prevent it from re-generating.
*/
public function set_default_jwt_key() {
// Get the JWT Secret
$default_secret = self::get_setting( 'preview_jwt_secret' );
if ( empty( $default_secret ) ) {
// Get the WPGatsby Settings from the options
$options = get_option( 'wpgatsby_settings', [] );
// If settings haven't been saved before, instantiate them as a new array
if ( empty( $options ) || ! is_array( $options ) ) {
$options = [];
}
// Se the preview secret
$options['preview_jwt_secret'] = self::generate_secret();
// Save the settings to prevent the JWT Secret from generating again
update_option( 'wpgatsby_settings', $options );
}
}
/**
* Overrides the "public_introspection_enabled" setting field in the GraphQL Settings to be
* checked and disabled so users can't uncheck it.
*
* @param array $field_config The field config for the setting
* @param string $field_name The name of the field (unfilterable in the config)
* @param string $section The slug of the section the field is registered to
*
* @return mixed
*/
public function filter_graphql_introspection_setting_field( $field_config, $field_name, $section ) {
if ( 'graphql_general_settings' === $section && 'public_introspection_enabled' === $field_name ) {
$field_config['value'] = 'on';
$field_config['disabled'] = true;
$field_config['desc'] = $field_config['desc'] . ' (' . __( 'Force enabled by WPGatsby. Gatsby requires WPGraphQL introspection to communicate with WordPress.', 'WPGatsby' ) . ')';
}
return $field_config;
}
/**
* Filters the value of the "public_introspection_enabled" setting to always be "on" when
* WPGatsby is enabled
*
* @param mixed $value The value of the field
* @param mixed $default The default value if there is no value set
* @param string $field_name The name of the option
* @param array $section_fields The setting values within the section
* @param string $section_name The name of the section the setting belongs to
*
* @return string
*/
public function filter_graphql_introspection_setting_value( $value, $default, $field_name, $section_fields, $section_name ) {
if ( 'graphql_general_settings' === $section_name && 'public_introspection_enabled' === $field_name ) {
return 'on';
}
return $value;
}
function admin_init() {
//set the settings
$this->settings_api->set_sections( $this->get_settings_sections() );
$this->settings_api->set_fields( $this->get_settings_fields() );
//initialize settings
$this->settings_api->admin_init();
}
function admin_menu() {
add_options_page(
'Settings API',
'Settings API',
'delete_posts',
'settings_api_test',
[
$this,
'plugin_page',
]
);
}
function get_settings_sections() {
$sections = [
[
'id' => 'wpgatsby_settings',
'title' => __( 'Settings', 'wpgatsby_settings' ),
],
];
return $sections;
}
public function register_settings_page() {
add_options_page(
'Gatsby',
'GatsbyJS',
'manage_options',
'gatsbyjs',
[
$this,
'plugin_page',
]
);
}
function plugin_page() {
echo '
Please add your Gatsby Cloud Content Sync URL to the WPGatsby plugin settings page.
Troubleshooting
Please ensure your URL is correct and your Preview instance is up and running.
If you've set the correct URL, your Preview instance is currently running, and you're still having trouble, please refer to the docs for
troubleshooting steps, ask your developer, or contact
support if that doesn't solve your issue.