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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,11 @@
## 4.8.1

### Fix: republish is now fault-isolated (KYTE-#181) + partial content saves no longer blank `block_layout` (KYTE-#189)

**Republish resilience (KYTE-#181).** `ApplicationController`'s `republish_kyte_connect` hook re-stamps every `state=1` page across all of an app's sites. Previously it `throw`ew on the first page with missing `KytePageData`, **aborting the entire batch** — every later site/page kept the stale connect string, leaving an app half-migrated (e.g. a JWT dashboard with an HMAC login page). Now each page is re-stamped inside a `try/catch`: failures are collected (`page` id, `s3key`, `site`, `reason`) and logged, and the loop continues. The result is surfaced on the response as `republish_summary` (`succeeded` / `failed` / `failures[]`) so the caller can show a real outcome instead of trusting a silent all-or-nothing hook. Also fixed: CloudFront invalidation ran **once outside** the sites loop, so only the *last* site was invalidated — it now runs **per site**, so multi-site apps no longer leave other sites' caches stale.

**`block_layout` preservation (KYTE-#189).** The KytePage update content-save unconditionally wrote `block_layout`, blanking it to empty when the field wasn't in the payload. A partial save — notably the Shipyard IDE, which sends only `html`/`stylesheet`/`javascript` — would therefore **wipe an existing block-editor layout**. The update now only overwrites `block_layout` when it's actually provided, preserving the stored value otherwise. (The IDE "save silently doesn't persist" half of #189 was already resolved by the v4.8.0 guard relaxation; this closes the data-loss edge.)

## 4.8.0

### Change: drop JavaScript obfuscation columns (Phase 2 — schema; resolves KYTE-#191 Phase 2)
Expand Down
110 changes: 71 additions & 39 deletions src/Mvc/Controller/ApplicationController.php
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,11 @@ public function hook_response_data($method, $o, &$r = null, &$d = null) {
if (isset($d['republish_kyte_connect']) && $d['republish_kyte_connect'] == 1) {
$sites = new \Kyte\Core\Model(KyteSite);
$sites->retrieve('application', $o->id);

// Fault-isolated republish summary. A single bad page must NOT abort the
// whole batch and strand later pages on the stale connect string. See KYTE-#181.
$republishSummary = ['succeeded' => 0, 'failed' => 0, 'failures' => []];

foreach($sites->objects as $site) {
$credential = new \Kyte\Aws\Credentials($site->region, $o->aws_public_key, $o->aws_private_key);
$s3 = new \Kyte\Aws\S3($credential, $site->s3BucketName);
Expand All @@ -86,51 +91,78 @@ public function hook_response_data($method, $o, &$r = null, &$d = null) {
$pages->retrieve("state", 1, false, [['field' => 'site', 'value' => $site->id]]);

foreach($pages->objects as $page) {
$params = $this->getObject($page);
$pd = new \Kyte\Core\ModelObject(KytePageData);
if (!$pd->retrieve('page', $page->id)) {
throw new \Exception("CRITICAL ERROR: Unable to find page data.");
// Re-stamp each page independently; collect failures and continue
// instead of throwing and aborting the remaining pages/sites.
try {
$params = $this->getObject($page);
$pd = new \Kyte\Core\ModelObject(KytePageData);
if (!$pd->retrieve('page', $page->id)) {
throw new \Exception("Unable to find page data (KytePageData) for page {$page->id}.");
}

$params['html'] = bzdecompress($pd->html);
$params['stylesheet'] = bzdecompress($pd->stylesheet);
$params['javascript'] = bzdecompress($pd->javascript);
// footers and headers
if ($params['footer'] && isset($params['footer']['html'], $params['footer']['stylesheet'], $params['footer']['javascript'], $params['footer']['block_layout'])) {
$params['footer']['html'] = bzdecompress($params['footer']['html']);
$params['footer']['stylesheet'] = bzdecompress($params['footer']['stylesheet']);
$params['footer']['javascript'] = bzdecompress($params['footer']['javascript']);
$params['footer']['block_layout'] = bzdecompress($params['footer']['block_layout']);
}
if ($params['header'] && isset($params['header']['html'], $params['header']['stylesheet'], $params['header']['javascript'], $params['header']['block_layout'])) {
$params['header']['html'] = bzdecompress($params['header']['html']);
$params['header']['stylesheet'] = bzdecompress($params['header']['stylesheet']);
$params['header']['javascript'] = bzdecompress($params['header']['javascript']);
$params['header']['block_layout'] = bzdecompress($params['header']['block_layout']);
}
// compile html file
$data = \Kyte\Mvc\Controller\KytePageController::createHtml($params);
// write to file
$s3->write($page->s3key, $data);
$republishSummary['succeeded']++;
} catch (\Throwable $e) {
$republishSummary['failed']++;
$republishSummary['failures'][] = [
'page' => $page->id,
's3key' => $page->s3key,
'site' => $site->id,
'reason' => $e->getMessage(),
];
error_log("Republish: failed to re-stamp page {$page->id} (s3key {$page->s3key}, site {$site->id}): " . $e->getMessage());
continue;
}
}

$params['html'] = bzdecompress($pd->html);
$params['stylesheet'] = bzdecompress($pd->stylesheet);
$params['javascript'] = bzdecompress($pd->javascript);
// footers and headers
if ($params['footer'] && isset($params['footer']['html'], $params['footer']['stylesheet'], $params['footer']['javascript'], $params['footer']['block_layout'])) {
$params['footer']['html'] = bzdecompress($params['footer']['html']);
$params['footer']['stylesheet'] = bzdecompress($params['footer']['stylesheet']);
$params['footer']['javascript'] = bzdecompress($params['footer']['javascript']);
$params['footer']['block_layout'] = bzdecompress($params['footer']['block_layout']);
// Invalidate THIS site's CloudFront. Previously this ran once outside the
// sites loop, so only the last site was invalidated — multi-site apps left
// every other site's cache stale. See KYTE-#181.
try {
$invalidationPaths = ['/*'];
if (KYTE_USE_SNS) {
$snsCredential = new \Kyte\Aws\Credentials(SNS_REGION);
$sns = new \Kyte\Aws\Sns($snsCredential, SNS_QUEUE_SITE_MANAGEMENT);
$sns->publish([
'action' => 'cf_invalidate',
'site_id' => $site->id,
'cf_id' => $site->cfDistributionId,
'cf_invalidation_paths' => $invalidationPaths,
'caller_id' => time(),
]);
} else {
$cf = new \Kyte\Aws\CloudFront($credential);
$cf->createInvalidation($site->cfDistributionId, $invalidationPaths);
}
if ($params['header'] && isset($params['header']['html'], $params['header']['stylesheet'], $params['header']['javascript'], $params['header']['block_layout'])) {
$params['header']['html'] = bzdecompress($params['header']['html']);
$params['header']['stylesheet'] = bzdecompress($params['header']['stylesheet']);
$params['header']['javascript'] = bzdecompress($params['header']['javascript']);
$params['header']['block_layout'] = bzdecompress($params['header']['block_layout']);
}
// compile html file
$data = \Kyte\Mvc\Controller\KytePageController::createHtml($params);
// write to file
$s3->write($page->s3key, $data);
} catch (\Throwable $e) {
error_log("Republish: CloudFront invalidation failed for site {$site->id}: " . $e->getMessage());
}
}

// invalidate CF
$invalidationPaths = ['/*'];
if (KYTE_USE_SNS) {
$credential = new \Kyte\Aws\Credentials(SNS_REGION);
$sns = new \Kyte\Aws\Sns($credential, SNS_QUEUE_SITE_MANAGEMENT);
$sns->publish([
'action' => 'cf_invalidate',
'site_id' => $site->id,
'cf_id' => $site->cfDistributionId,
'cf_invalidation_paths' => $invalidationPaths,
'caller_id' => time(),
]);
} else {
// invalidate CF
$cf = new \Kyte\Aws\CloudFront($credential);
$cf->createInvalidation($site->cfDistributionId, $invalidationPaths);
// Surface the result so the caller (Shipyard) can show a real summary
// instead of trusting a silent all-or-nothing hook.
$r['republish_summary'] = $republishSummary;
if ($republishSummary['failed'] > 0) {
error_log("Republish completed with {$republishSummary['failed']} failure(s) of " . ($republishSummary['succeeded'] + $republishSummary['failed']) . " page(s): " . json_encode($republishSummary['failures']));
}
}
break;
Expand Down
31 changes: 17 additions & 14 deletions src/Mvc/Controller/KytePageController.php
Original file line number Diff line number Diff line change
Expand Up @@ -158,23 +158,26 @@ public function hook_response_data($method, $o, &$r = null, &$d = null) {
$params['javascript'] = $d['javascript'];

// Update KytePageData
$bz_html = isset($d['html']) ? bzcompress($d['html'], 9) : '';
$bz_stylesheet = isset($d['stylesheet']) ? bzcompress($d['stylesheet'], 9) : '';
$bz_javascript = isset($d['javascript']) ? bzcompress($d['javascript'], 9) : '';
$bz_block_layout = isset($d['block_layout']) ? bzcompress($d['block_layout'], 9) : '';
$pageDataUpdate = [
'html' => isset($d['html']) ? bzcompress($d['html'], 9) : '',
'stylesheet' => isset($d['stylesheet']) ? bzcompress($d['stylesheet'], 9) : '',
'javascript' => isset($d['javascript']) ? bzcompress($d['javascript'], 9) : '',
'modified_by' => (is_object($this->user) && isset($this->user->id))
? (int) $this->user->id
: ((is_numeric($this->user)) ? (int) $this->user : 0),
'date_modified' => time(),
];
// Only overwrite block_layout when it's actually provided. A partial
// content save (e.g. the Shipyard IDE, which sends only
// html/stylesheet/javascript) must NOT blank an existing block-editor
// layout. See KYTE-#189.
if (isset($d['block_layout'])) {
$pageDataUpdate['block_layout'] = bzcompress($d['block_layout'], 9);
}

$pd = new \Kyte\Core\ModelObject(KytePageData);
if($pd->retrieve('page', $o->id)) {
$pd->save([
'html' => $bz_html,
'stylesheet' => $bz_stylesheet,
'javascript' => $bz_javascript,
'block_layout' => $bz_block_layout,
'modified_by' => (is_object($this->user) && isset($this->user->id))
? (int) $this->user->id
: ((is_numeric($this->user)) ? (int) $this->user : 0),
'date_modified' => time(),
]);
$pd->save($pageDataUpdate);
} else {
throw new \Exception("CRITICAL ERROR: Unable to find page data.");
}
Expand Down
Loading