| 1 |
<?php |
| 2 |
|
| 3 |
namespace Templately\Modules\Settings\REST; |
| 4 |
|
| 5 |
use Templately\Admin\API\BaseAPI; |
| 6 |
use Templately\Modules\ElementorKitSettings\Utils\KitApplier; |
| 7 |
use Templately\Utils\Helper; |
| 8 |
use Templately\Utils\Response\FatalGuard; |
| 9 |
|
| 10 |
use WP_REST_Request; |
| 11 |
|
| 12 |
class Settings extends BaseAPI { |
| 13 |
private $endpoint = 'settings'; |
| 14 |
|
| 15 |
|
| 16 |
|
| 17 |
/** |
| 18 |
* @param $request WP_REST_Request for getting all route request in time. |
| 19 |
* |
| 20 |
* @return WP_Error|boolean |
| 21 |
*/ |
| 22 |
public function permission_check( WP_REST_Request $request ) { |
| 23 |
$this->request = $request; |
| 24 |
if(!current_user_can('manage_options')){ |
| 25 |
return false; |
| 26 |
} |
| 27 |
return true; |
| 28 |
} |
| 29 |
|
| 30 |
public function register_routes() { |
| 31 |
$this->post( $this->endpoint . '/update', [$this, 'update_settings'] ); |
| 32 |
} |
| 33 |
|
| 34 |
public function update_settings() { |
| 35 |
// A PHP fatal raised by a builder further down this method (see contain()) |
| 36 |
// otherwise reaches the browser as WP core's "critical error" page, which the |
| 37 |
// settings screen cannot parse. Armed so it comes back as the 043 envelope. |
| 38 |
FatalGuard::arm( 'settings/update' ); |
| 39 |
|
| 40 |
$_settings = $this->request->get_param('settings'); |
| 41 |
$platform = $_settings['platform'] ?? ''; |
| 42 |
$siteTitle = $_settings['siteTitle'] ?? ''; |
| 43 |
$siteTagline = $_settings['siteTagline'] ?? ''; |
| 44 |
$customCSS = $_settings['customCSS'] ?? ''; |
| 45 |
|
| 46 |
$failed = []; |
| 47 |
|
| 48 |
// Written one at a time, each contained. `blogname` / `blogdescription` are the |
| 49 |
// two options Elementor hooks (it syncs both into the active kit), so on a site |
| 50 |
// with a broken kit the FIRST write used to throw and take the whole save with |
| 51 |
// it — the tagline, the custom CSS and every colour/typography assignment below |
| 52 |
// were silently never written. See contain(). |
| 53 |
foreach ( [ |
| 54 |
'blogname' => $siteTitle, |
| 55 |
'blogdescription' => $siteTagline, |
| 56 |
'templately_custom_css' => $customCSS, |
| 57 |
] as $key => $value ) { |
| 58 |
if ( ! $this->contain( "option:{$key}", function () use ( $key, $value ) { |
| 59 |
update_option( $key, $value ); |
| 60 |
} ) ) { |
| 61 |
$failed[] = $key; |
| 62 |
} |
| 63 |
} |
| 64 |
|
| 65 |
if ( ! $this->contain( 'logo', [ $this, 'update_logo' ] ) ) { |
| 66 |
$failed[] = 'logo'; |
| 67 |
} |
| 68 |
|
| 69 |
if($platform === 'elementor'){ |
| 70 |
if(class_exists('Elementor\Plugin')){ |
| 71 |
if ( ! $this->contain( 'elementor-kit', function () use ( $_settings ) { |
| 72 |
KitApplier::apply($_settings); |
| 73 |
} ) ) { |
| 74 |
$failed[] = 'kit'; |
| 75 |
} |
| 76 |
} |
| 77 |
} |
| 78 |
else if($platform === 'gutenberg'){ |
| 79 |
if ( ! $this->contain( 'gutenberg-settings', function () use ( $_settings ) { |
| 80 |
$this->update_gutenberg_settings($_settings); |
| 81 |
} ) ) { |
| 82 |
$failed[] = 'global-styles'; |
| 83 |
} |
| 84 |
} |
| 85 |
|
| 86 |
$response = [ 'message' => 'Settings updated successfully' ]; |
| 87 |
|
| 88 |
// Named so the client can tell the user WHICH parts did not stick. An empty |
| 89 |
// list is omitted, so the shape is unchanged for the ordinary save. |
| 90 |
if ( ! empty( $failed ) ) { |
| 91 |
$response['failed'] = $failed; |
| 92 |
} |
| 93 |
|
| 94 |
return $this->success( $response ); |
| 95 |
} |
| 96 |
|
| 97 |
/** |
| 98 |
* Run one settings write, contained. |
| 99 |
* |
| 100 |
* Every write below reaches third-party code: `update_option()` fires that option's |
| 101 |
* hooks (Elementor listens on `update_option_blogname`/`update_option_blogdescription` |
| 102 |
* and writes the value into the ACTIVE KIT), and the kit/global-style writers call |
| 103 |
* into the builder directly. On a site whose Elementor kit post is gone — a bad |
| 104 |
* restore, a deleted kit, a half-finished migration — that path throws |
| 105 |
* `Exception( 'Invalid post.' )` from Elementor's page settings manager: `Kit` |
| 106 |
* documents for a missing post carry an empty `WP_Post`, so `save_settings()` asks |
| 107 |
* `ajax_before_save_settings()` to validate post ID `0`. |
| 108 |
* |
| 109 |
* Uncontained, an exception in a REST callback is an uncaught PHP fatal — WP does not |
| 110 |
* catch these during dispatch, and `RestEnvelope` runs on `rest_post_dispatch`, which |
| 111 |
* never sees it. The user got a raw 500 with nothing saved, from the very first write. |
| 112 |
* |
| 113 |
* Each write is therefore independent: a failure is logged with its section name, |
| 114 |
* reported in the response's `failed` list, and the remaining sections still run. |
| 115 |
* `\Throwable` rather than `Exception` because the same code raises PHP `\Error` too |
| 116 |
* (an add-on referencing a class Elementor has since removed). |
| 117 |
* |
| 118 |
* @param string $section Log/label for the write ("option:blogname", "logo", …). |
| 119 |
* @param callable $write The write itself. |
| 120 |
* |
| 121 |
* @return bool True when it completed without raising. |
| 122 |
*/ |
| 123 |
private function contain( $section, callable $write ): bool { |
| 124 |
try { |
| 125 |
$write(); |
| 126 |
return true; |
| 127 |
} catch ( \Throwable $e ) { |
| 128 |
Helper::log( |
| 129 |
sprintf( 'Settings update failed for "%s": %s', $section, $e->getMessage() ), |
| 130 |
'settings', |
| 131 |
'error' |
| 132 |
); |
| 133 |
|
| 134 |
return false; |
| 135 |
} |
| 136 |
} |
| 137 |
|
| 138 |
public function update_logo() { |
| 139 |
$_settings = $this->request->get_param('settings'); |
| 140 |
$logo = $_settings['logoImage'] ?? []; |
| 141 |
|
| 142 |
if(!empty($logo['id'])){ |
| 143 |
set_theme_mod( 'custom_logo', $logo['id'] ); |
| 144 |
update_option( 'site_logo', $logo['id'] ); |
| 145 |
|
| 146 |
// Update Elementor kit if Elementor is enabled |
| 147 |
if(class_exists('Elementor\Plugin')){ |
| 148 |
$kit = \Elementor\Plugin::$instance->kits_manager->get_active_kit(); |
| 149 |
// `get_active_kit()` NEVER returns falsy — a site with no valid kit gets |
| 150 |
// an empty Kit instance (post id 0) whose update_settings() throws |
| 151 |
// `Invalid post.`. get_id() is the check that actually means "real kit". |
| 152 |
if($kit && $kit->get_id()){ |
| 153 |
$settings = $kit->get_settings(); |
| 154 |
$settings['site_logo'] = $logo; |
| 155 |
$kit->update_settings($settings); |
| 156 |
} |
| 157 |
} |
| 158 |
} |
| 159 |
else{ |
| 160 |
remove_theme_mod( 'custom_logo' ); |
| 161 |
delete_option( 'site_logo' ); |
| 162 |
|
| 163 |
// Remove logo from Elementor kit if Elementor is enabled |
| 164 |
if(class_exists('Elementor\Plugin')){ |
| 165 |
$kit = \Elementor\Plugin::$instance->kits_manager->get_active_kit(); |
| 166 |
if($kit && $kit->get_id()){ // see the note on the branch above |
| 167 |
$settings = $kit->get_settings(); |
| 168 |
unset($settings['site_logo']); |
| 169 |
$kit->update_settings($settings); |
| 170 |
} |
| 171 |
} |
| 172 |
} |
| 173 |
|
| 174 |
return true; |
| 175 |
} |
| 176 |
|
| 177 |
private function update_gutenberg_settings($_settings){ |
| 178 |
$colors = $_settings['colors'] ?? []; |
| 179 |
$typography = $_settings['typography'] ?? []; |
| 180 |
$settings = get_option('eb_global_styles', []); |
| 181 |
$settings = is_array($settings) ? $settings: []; |
| 182 |
$settings = array_map(function($item) { return json_decode($item, true); }, $settings); |
| 183 |
|
| 184 |
|
| 185 |
if(!empty($colors)){ |
| 186 |
if (isset($settings['global_colors'])) { |
| 187 |
foreach ($settings['global_colors'] as $key => $color) { |
| 188 |
$settings['global_colors'][$key]['color'] = $colors[$color['var']] ?? $color['color']; |
| 189 |
} |
| 190 |
} |
| 191 |
|
| 192 |
if (isset($settings['custom_colors'])) { |
| 193 |
foreach ($settings['custom_colors'] as $key => $color) { |
| 194 |
$settings['custom_colors'][$key]['color'] = $colors[$color['var']] ?? $color['color']; |
| 195 |
} |
| 196 |
} |
| 197 |
} |
| 198 |
|
| 199 |
if(!empty($typography)){ |
| 200 |
$settings["global_typography"] = $typography; |
| 201 |
} |
| 202 |
|
| 203 |
$settings = array_map('json_encode', $settings); |
| 204 |
update_option('eb_global_styles', $settings); |
| 205 |
} |
| 206 |
} |
| 207 |
|