{"id":360855,"date":"2026-09-05T20:28:48","date_gmt":"2026-09-05T20:28:48","guid":{"rendered":"https:\/\/wordpress.org\/plugins\/itdatex-sync-gate\/"},"modified":"2026-09-05T20:28:30","modified_gmt":"2026-09-05T20:28:30","slug":"itdatex-sync-gate","status":"publish","type":"plugin","link":"https:\/\/cs.wordpress.org\/plugins\/itdatex-sync-gate\/","author":23515351,"comment_status":"closed","ping_status":"closed","template":"","meta":{"version":"0.11.2","stable_tag":"0.11.2","tested":"7.1","requires":"6.4","requires_php":"8.1","requires_plugins":null,"header_name":"itdatex Sync Gate","header_author":"itdatex","header_description":"Granular staging sync for WordPress. Copy individual database tables between two paired sites (staging <-> live), with pre-backup, serialization-safe URL rewrite and sensible default exclusions. No cloud broker, HMAC-signed REST endpoints.","assets_banners_color":"363b45","last_updated":"2026-09-05 20:28:30","external_support_url":"","external_repository_url":"","donate_link":"","header_plugin_uri":"","header_author_uri":"https:\/\/wp.itdatex.support\/","rating":0,"author_block_rating":0,"active_installs":0,"downloads":42,"num_ratings":0,"support_threads":0,"support_threads_resolved":0,"author_block_count":0,"sections":["description","installation","faq","changelog"],"tags":{"0.11.2":{"tag":"0.11.2","author":"itdatex","date":"2026-09-05 20:28:30","revision":3682798}},"upgrade_notice":{"0.11.2":"<p>wp.org review pass. Real REST permission_callback, enqueued admin JS, no functional change. Safe to update.<\/p>","0.11.1":"<p>Compliance and language pass, no behavior change. Safe to update.<\/p>","0.11.0":"<p>Contains two fixes for bugs that surfaced in the first real cross-site test: server-side exclusion was client-overridable (security), ChunkStore meta.json was not race-safe under parallel push (consistency). If you rely on server-side user\/order exclusions, upgrade to v0.11.0. No DB schema change, no migration.<\/p>","0.10.0":"<p>Adds pull auto-resume. On first activation a new wp-cron hook <code>itdatex_sg_pull_cleanup<\/code> is registered and the folder <code>wp-content\/uploads\/itdatex-sync-gate\/pulls\/<\/code> is created. No DB schema change, no server endpoint change. Peak disk usage during a running pull equals the target file size.<\/p>","0.9.0":"<p>Adds parallel pull. Peak RAM during pull rises to chunk_concurrency * chunk_size * ~1.33 (default 3 * 5 MB ~= 20 MB). No DB schema change, no migration, no server endpoint change.<\/p>","0.8.0":"<p>Chunk threshold follows chunk_size setting. If you set chunk_size &lt; 5 MB, all files above that threshold switch to chunked. No DB schema change, no migration.<\/p>","0.7.0":"<p>Two new settings, no migration needed. Defaults match v0.6 behavior exactly (5 MB chunk_size, 24h session_ttl).<\/p>","0.6.0":"<p>New option <code>itdatex_sg_settings<\/code>, no migration needed. Existing behavior does not change (default remains concurrency 3).<\/p>","0.5.0":"<p>Parallel upload + resume. No DB schema change. Aborted v0.4 sessions must be discarded and restarted (session layout incompatible).<\/p>","0.4.0":"<p>Chunking up to 500 MB. No DB schema change, but a new wp-cron job is registered on activation.<\/p>","0.3.0":"<p>File sync added. No migration overhead, no DB schema change.<\/p>","0.2.0":"<p>Push direction added.<\/p>","0.1.0":"<p>Initial release.<\/p>"},"ratings":[],"assets_icons":{"icon-128x128.png":{"filename":"icon-128x128.png","revision":3682797,"resolution":"128x128","location":"assets","locale":"","width":128,"height":128},"icon-256x256.png":{"filename":"icon-256x256.png","revision":3682797,"resolution":"256x256","location":"assets","locale":"","width":256,"height":256}},"assets_banners":{"banner-1544x500.png":{"filename":"banner-1544x500.png","revision":3682797,"resolution":"1544x500","location":"assets","locale":"","width":1544,"height":500},"banner-772x250.png":{"filename":"banner-772x250.png","revision":3682797,"resolution":"772x250","location":"assets","locale":"","width":772,"height":250}},"assets_blueprints":{},"all_blocks":[],"tagged_versions":["0.11.2"],"block_files":[],"assets_screenshots":[],"screenshots":[]},"plugin_section":[],"plugin_tags":[153,2275,4155,19979,1558],"plugin_category":[59],"plugin_contributors":[272517],"plugin_business_model":[],"class_list":["post-360855","plugin","type-plugin","status-publish","hentry","plugin_tags-database","plugin_tags-developer","plugin_tags-migration","plugin_tags-staging","plugin_tags-sync","plugin_category-utilities-and-tools","plugin_contributors-itdatex","plugin_committers-itdatex"],"banners":{"banner":"https:\/\/ps.w.org\/itdatex-sync-gate\/assets\/banner-772x250.png?rev=3682797","banner_2x":"https:\/\/ps.w.org\/itdatex-sync-gate\/assets\/banner-1544x500.png?rev=3682797","banner_rtl":false,"banner_2x_rtl":false},"icons":{"svg":false,"icon":"https:\/\/ps.w.org\/itdatex-sync-gate\/assets\/icon-128x128.png?rev=3682797","icon_2x":"https:\/\/ps.w.org\/itdatex-sync-gate\/assets\/icon-256x256.png?rev=3682797","generated":false},"screenshots":[],"raw_content":"<!--section=description-->\n<p>Sync Gate connects two WordPress installs through an HMAC-signed pair handshake (no cloud broker in between). After pairing, individual database tables can be pulled from or pushed to the other site, with:<\/p>\n\n<ul>\n<li>Automatic pre-backup (SQL dump inside the uploads\/ directory, HTTP access blocked)<\/li>\n<li>Serialization-safe URL rewrite (unserialize -&gt; replace -&gt; serialize, so no broken serialized options escape)<\/li>\n<li>Sensible default exclusions (users, orders, sessions stay local)<\/li>\n<li>Full audit log (which table, when, how many rows, which backup)<\/li>\n<\/ul>\n\n<p><strong>Audience:<\/strong> Agencies and developers who need to selectively synchronize WooCommerce shops or content sites between staging and live.<\/p>\n\n<h3>Features (v0.11.2)<\/h3>\n\n<ul>\n<li>HMAC-signed REST endpoints (no cloud middleman)<\/li>\n<li>Pairing via pending secret + handshake (15 minute TTL)<\/li>\n<li><strong>Table sync<\/strong> - pull and push individual database tables between paired sites<\/li>\n<li><strong>File sync<\/strong> - pull and push wp-content\/uploads\/ with diff (size + mtime), backup, dry-run and optional --delete. <strong>Chunked up to 500 MB per file<\/strong> (5 MB per HTTP chunk, SHA256 integrity check, wp-cron cleanup of expired sessions). <strong>Parallel chunks via curl_multi in both directions<\/strong> (1..10 configurable, default 3) and <strong>auto-resume on push AND pull<\/strong> (push session as WP option, pull session as folder-backed store in <code>uploads\/itdatex-sync-gate\/pulls\/<\/code>)<\/li>\n<li><strong>Central settings<\/strong> under Tools -&gt; Sync Gate -&gt; Settings and via WP-CLI <code>wp sync-gate config<\/code>. Configurable: chunk_concurrency (1..10), chunk_size (256 KB..20 MB), session_ttl_hours (1..168)<\/li>\n<li>Web UI under Tools -&gt; Sync Gate with tabs \"Pairs\", \"Tables\", \"Files\", \"Log\" and direction radios<\/li>\n<li>WP-CLI: <code>list-tables<\/code>, <code>pair-init<\/code>, <code>pair-connect<\/code>, <code>pair-list<\/code>, <code>pull<\/code>, <code>push<\/code>, <code>files pull|push<\/code>, <code>files-resume-list<\/code>, <code>pull-resume-list<\/code>, <code>pull-resume-forget<\/code><\/li>\n<li>Default exclusions for WooCommerce orders, users, sync-gate's own tables (enforced server-side and client-side)<\/li>\n<li>Serialized values are handled correctly (including nested)<\/li>\n<li>Nonce-based replay protection (10 minute TTL) and time window +\/- 60s<\/li>\n<li>Path traversal protection, .php\/.phar exclusion, symlink ignore<\/li>\n<\/ul>\n\n<h3>Security model<\/h3>\n\n<p>Every REST call between paired sites carries four headers:<\/p>\n\n<ul>\n<li><code>X-Itdatex-SG-Signature<\/code> = HMAC-SHA256(secret, method || path || sha256(body) || nonce || timestamp)<\/li>\n<li><code>X-Itdatex-SG-Nonce<\/code> (16 byte hex, valid once for 10 minutes)<\/li>\n<li><code>X-Itdatex-SG-Timestamp<\/code> (unix seconds, +\/- 60s window)<\/li>\n<li><code>X-Itdatex-SG-Remote-URL<\/code> (sender URL for pair lookup)<\/li>\n<\/ul>\n\n<p>The secret itself is transmitted over HTTPS and stored in plain text in the pair table. Anyone with database access on either side effectively has access to the other; encryption at rest would not add real security here because the decryption key would live in the same WordPress install.<\/p>\n\n<!--section=installation-->\n<ol>\n<li>Extract the plugin into <code>\/wp-content\/plugins\/itdatex-sync-gate\/<\/code><\/li>\n<li>Activate it in WP Admin under Plugins<\/li>\n<li>Activate it on both sites that should be paired<\/li>\n<li>Open Tools -&gt; Sync Gate and run the pairing<\/li>\n<\/ol>\n\n<!--section=faq-->\n<dl>\n<dt id=\"what%20happens%20to%20my%20local%20table%20before%20a%20pull%3F\"><h3>What happens to my local table before a pull?<\/h3><\/dt>\n<dd><p>It is written as an SQL dump to <code>wp-content\/uploads\/itdatex-sync-gate\/backups\/&lt;table&gt;-&lt;timestamp&gt;.sql<\/code>. Then DROP + CREATE + INSERT with the remote data.<\/p><\/dd>\n<dt id=\"what%20about%20the%20users%20%2F%20orders%20tables%3F\"><h3>What about the users \/ orders tables?<\/h3><\/dt>\n<dd><p>They are on the default exclusion list. They are not synced unless you pass an explicit <code>--force<\/code> flag.<\/p><\/dd>\n<dt id=\"can%20i%20push%20%28local%20-%3E%20remote%29%20as%20well%3F\"><h3>Can I push (local -&gt; remote) as well?<\/h3><\/dt>\n<dd><p>Yes, since v0.2.0. <code>wp sync-gate push posts --pair-id=1<\/code> or via the web UI (choose the \"Push\" radio).<\/p><\/dd>\n<dt id=\"are%20uploads%20%2F%20media%20synchronized%3F\"><h3>Are uploads \/ media synchronized?<\/h3><\/dt>\n<dd><p>Yes, since v0.3.0. <code>wp sync-gate files pull --pair-id=1 --dry-run<\/code> shows the diff; without --dry-run the files are transferred. Themes and plugins are excluded (deploy them via Git\/Composer). .php, .phar and .htaccess files are never synced for security reasons.<\/p><\/dd>\n<dt id=\"what%20happens%20if%20the%20connection%20drops%20during%20a%20pull%3F\"><h3>What happens if the connection drops during a pull?<\/h3><\/dt>\n<dd><p>The backup has already been written. The sync log carries an \"error\" status. Manual recovery via <code>wp db import &lt;backup-file&gt;<\/code>.<\/p><\/dd>\n\n<\/dl>\n\n<!--section=changelog-->\n<h4>0.11.2<\/h4>\n\n<ul>\n<li>wp.org review pass: REST endpoints now expose a real permission_callback that performs the HMAC\/pair verification (previously __return_true + in-handler check); admin tab-tables JS moved out of an inline <code>&lt;script&gt;<\/code> block and enqueued via wp_enqueue_script; load_plugin_textdomain() removed (wp.org auto-loads since WP 4.6); readme short description shortened and comparative wording dropped. No behavior change.<\/li>\n<\/ul>\n\n<h4>0.11.1<\/h4>\n\n<ul>\n<li>wp.org compliance pass: settings admin labels are English, plugin header language corrected, defensive wp_unslash+sanitize on all admin $_POST reads, REST permission_callback rationale documented inline. No functional change.<\/li>\n<\/ul>\n\n<h4>0.11.0<\/h4>\n\n<ul>\n<li>SECURITY: server-side table exclusion could be bypassed by client-side --force. From v0.11.0 the server exclusion is absolute, both for pull and push. A compromised pair partner can no longer override the exclusion via \"force\": true in the body.<\/li>\n<li>FIX: ChunkStore::append_chunk had a race in load_meta at chunk_concurrency &gt; 1 - parallel chunk writers could read a truncated meta.json and throw \"meta.json corrupt\". Read-modify-write now runs entirely under flock.<\/li>\n<li>NEW: tools\/e2e\/ with a complete cross-site test suite (setup.sh + 6 tests + teardown.sh).<\/li>\n<\/ul>\n\n<h4>0.10.0<\/h4>\n\n<ul>\n<li>Pull auto-resume: <code>PullSessionStore<\/code> persists chunk progress across sync runs (assembly.bin preallocated with ftruncate, received_offsets in meta.json). Aborted pulls resume at the incomplete offsets on the next sync. New wp-cron <code>itdatex_sg_pull_cleanup<\/code> and new WP-CLI commands <code>pull-resume-list<\/code> and <code>pull-resume-forget<\/code>. No server change, no migration.<\/li>\n<\/ul>\n\n<h4>0.9.0<\/h4>\n\n<ul>\n<li>Parallel pull: <code>FileSyncer::pull_one_chunked<\/code> fetches chunks the same way the push already did (<code>chunk_concurrency<\/code> batches over <code>ParallelHttp<\/code>, out-of-order via <code>fseek<\/code>+<code>fwrite<\/code> into a temp file preallocated with <code>ftruncate<\/code>). No server change, no migration.<\/li>\n<\/ul>\n\n<h4>0.8.0<\/h4>\n\n<ul>\n<li>Chunk threshold now follows the chunk_size setting. In v0.7 the threshold was hardcoded to 5 MB, so a 3 MB file was transferred single-shot even when chunk_size=1 MB. Now: file &gt; chunk_size -&gt; chunked. Fallback 5 MB only when the settings service is unreachable.<\/li>\n<\/ul>\n\n<h4>0.7.0<\/h4>\n\n<ul>\n<li>chunk_size (256 KB..20 MB) and session_ttl_hours (1..168) are settings now. chunk_size is pinned per session in meta.json, running uploads are protected against setting changes.<\/li>\n<\/ul>\n\n<h4>0.6.0<\/h4>\n\n<ul>\n<li>Configurable parallelism for chunked file push. New admin tab \"Settings\" and WP-CLI <code>wp sync-gate config list|get|set<\/code>. Currently only chunk_concurrency (1..10); more settings to follow.<\/li>\n<\/ul>\n\n<h4>0.5.0<\/h4>\n\n<ul>\n<li>Parallel upload (3 in-flight chunks via curl_multi) and auto-resume after connection drop. ChunkStore switched to out-of-order layout. New REST endpoint \/files\/put-status. WP-CLI files-resume-list + files-resume-forget.<\/li>\n<\/ul>\n\n<h4>0.4.0<\/h4>\n\n<ul>\n<li>Chunking for large files: file sync now supports up to 500 MB per file via 5 MB chunks with SHA256 verification. REST endpoints \/files\/{put,fetch}-{init,chunk,commit}. Daily wp-cron cleanup for aborted sessions.<\/li>\n<\/ul>\n\n<h4>0.3.0<\/h4>\n\n<ul>\n<li>File sync for wp-content\/uploads\/: REST \/files\/manifest + \/files\/fetch + \/files\/put, WP-CLI <code>files pull|push<\/code>, admin tab \"Files\" with dry-run + --delete.<\/li>\n<\/ul>\n\n<h4>0.2.0<\/h4>\n\n<ul>\n<li>Push direction: <code>wp sync-gate push<\/code>, REST \/push-target, admin UI pull\/push radio, log <code>push<\/code> + <code>push-in<\/code>.<\/li>\n<\/ul>\n\n<h4>0.1.0<\/h4>\n\n<ul>\n<li>Initial release. Pull direction (remote -&gt; local), pairing, backup, URL rewrite, exclusions.<\/li>\n<\/ul>","raw_excerpt":"Granular per-table sync between two paired WordPress sites. Copy individual tables (posts, options, terms) between staging and live.","jetpack_sharing_enabled":true,"_links":{"self":[{"href":"https:\/\/cs.wordpress.org\/plugins\/wp-json\/wp\/v2\/plugin\/360855","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/cs.wordpress.org\/plugins\/wp-json\/wp\/v2\/plugin"}],"about":[{"href":"https:\/\/cs.wordpress.org\/plugins\/wp-json\/wp\/v2\/types\/plugin"}],"replies":[{"embeddable":true,"href":"https:\/\/cs.wordpress.org\/plugins\/wp-json\/wp\/v2\/comments?post=360855"}],"author":[{"embeddable":true,"href":"https:\/\/cs.wordpress.org\/plugins\/wp-json\/wporg\/v1\/users\/itdatex"}],"wp:attachment":[{"href":"https:\/\/cs.wordpress.org\/plugins\/wp-json\/wp\/v2\/media?parent=360855"}],"wp:term":[{"taxonomy":"plugin_section","embeddable":true,"href":"https:\/\/cs.wordpress.org\/plugins\/wp-json\/wp\/v2\/plugin_section?post=360855"},{"taxonomy":"plugin_tags","embeddable":true,"href":"https:\/\/cs.wordpress.org\/plugins\/wp-json\/wp\/v2\/plugin_tags?post=360855"},{"taxonomy":"plugin_category","embeddable":true,"href":"https:\/\/cs.wordpress.org\/plugins\/wp-json\/wp\/v2\/plugin_category?post=360855"},{"taxonomy":"plugin_contributors","embeddable":true,"href":"https:\/\/cs.wordpress.org\/plugins\/wp-json\/wp\/v2\/plugin_contributors?post=360855"},{"taxonomy":"plugin_business_model","embeddable":true,"href":"https:\/\/cs.wordpress.org\/plugins\/wp-json\/wp\/v2\/plugin_business_model?post=360855"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}