| @@ -132,8 +132,330 @@ | ||
| 132 | 132 | |
| 133 | 133 | wp_send_json_success(array('message' => __('Plugin activated successfully.', 'pagelayer'))); |
| 134 | 134 | } |
| 135 | 135 | |
| 136 | +// AI Agents Connection AJAX handlers | |
| 137 | +// NOTE: These must be registered BEFORE the pagelayer_nonce gate below, | |
| 138 | +// because they use their own nonce (pagelayer_mcp_nonce) or basic auth. | |
| 139 | +add_action('wp_ajax_pagelayer_mcp_generate_app_password', 'pagelayer_mcp_generate_app_password'); | |
| 140 | +add_action('wp_ajax_pagelayer_mcp_adapter_action', 'pagelayer_mcp_adapter_action'); | |
| 141 | +add_action('wp_ajax_pagelayer_mcp_test_connection', 'pagelayer_mcp_test_connection'); | |
| 142 | +add_action('wp_ajax_pagelayer_mcp_save_test_status', 'pagelayer_mcp_save_test_status'); | |
| 143 | +add_action('wp_ajax_pagelayer_mcp_save_image_api_key', 'pagelayer_mcp_save_image_api_key'); | |
| 144 | + | |
| 145 | +// Verifies, without any HTTP round trip, the two things a loopback request would | |
| 146 | +// have proved: that the supplied Application Password authenticates this user, | |
| 147 | +// and that the Pagelayer abilities are registered. Used when the loopback itself | |
| 148 | +// fails (cURL timeout etc.) so the panel can still tell the user where they stand. | |
| 149 | +function pagelayer_mcp_verify_locally($username, $password) { | |
| 150 | + $result = array('password_ok' => false, 'abilities' => 0, 'message' => ''); | |
| 151 | + | |
| 152 | + if(!class_exists('WP_Application_Passwords')){ | |
| 153 | + $result['message'] = __('Application Passwords are not supported on this WordPress version.', 'pagelayer'); | |
| 154 | + return $result; | |
| 155 | + } | |
| 156 | + | |
| 157 | + $user = get_user_by('login', $username); | |
| 158 | + if(!$user && is_email($username)){ | |
| 159 | + $user = get_user_by('email', $username); | |
| 160 | + } | |
| 161 | + | |
| 162 | + if(!$user){ | |
| 163 | + $result['message'] = sprintf(__('No user named "%s" exists on this site.', 'pagelayer'), $username); | |
| 164 | + return $result; | |
| 165 | + } | |
| 166 | + | |
| 167 | + // Core strips non-alphanumerics before comparing, so the password works with | |
| 168 | + // or without the readability spaces (see wp_authenticate_application_password). | |
| 169 | + $stripped = preg_replace('/[^a-z\d]/i', '', $password); | |
| 170 | + | |
| 171 | + foreach(\WP_Application_Passwords::get_user_application_passwords($user->ID) as $item){ | |
| 172 | + if(\WP_Application_Passwords::check_password($stripped, $item['password'])){ | |
| 173 | + $result['password_ok'] = true; | |
| 174 | + break; | |
| 175 | + } | |
| 176 | + } | |
| 177 | + | |
| 178 | + if(function_exists('wp_get_abilities')){ | |
| 179 | + foreach(wp_get_abilities() as $name => $ability){ | |
| 180 | + $name = is_string($name) ? $name : ''; | |
| 181 | + if(strpos($name, 'pagelayer-') === 0){ | |
| 182 | + $result['abilities']++; | |
| 183 | + } | |
| 184 | + } | |
| 185 | + } | |
| 186 | + | |
| 187 | + if($result['password_ok']){ | |
| 188 | + $result['message'] = sprintf( | |
| 189 | + __('Checked locally instead: your Application Password IS valid for "%1$s", and %2$d Pagelayer abilities are registered.', 'pagelayer'), | |
| 190 | + $username, | |
| 191 | + $result['abilities'] | |
| 192 | + ); | |
| 193 | + }else{ | |
| 194 | + $result['message'] = sprintf( | |
| 195 | + __('Checked locally instead: the supplied Application Password does NOT match any password stored for "%s" — generate a new one below.', 'pagelayer'), | |
| 196 | + $username | |
| 197 | + ); | |
| 198 | + } | |
| 199 | + | |
| 200 | + return $result; | |
| 201 | +} | |
| 202 | + | |
| 203 | +function pagelayer_mcp_test_connection() { | |
| 204 | + include_once(PAGELAYER_DIR.'/main/abilities.php'); | |
| 205 | + check_ajax_referer('pagelayer_mcp_nonce', 'nonce'); | |
| 206 | + | |
| 207 | + if(!current_user_can('manage_options')){ | |
| 208 | + wp_send_json_error(__('You do not have permission to test the connection.', 'pagelayer')); | |
| 209 | + } | |
| 210 | + | |
| 211 | + $start = microtime(true); | |
| 212 | + $url = trailingslashit(home_url()) . ltrim(Pagelayer_Abilities::$ABILITIES_ENDPOINT, '/'); | |
| 213 | + | |
| 214 | + $username = isset($_POST['username']) ? sanitize_text_field(wp_unslash($_POST['username'])) : ''; | |
| 215 | + $password = isset($_POST['password']) ? sanitize_text_field(wp_unslash($_POST['password'])) : ''; | |
| 216 | + | |
| 217 | + // Application Passwords are silently inert unless the site is on HTTPS or the | |
| 218 | + // environment type is "local" (wp_is_application_passwords_supported()). WP | |
| 219 | + // still hands out a password string in that state, so the only symptom is a | |
| 220 | + // 401 on every request — check it up front instead of blaming the password. | |
| 221 | + if(function_exists('wp_is_application_passwords_available') && !wp_is_application_passwords_available()){ | |
| 222 | + $message = sprintf( | |
| 223 | + __('Application Passwords are disabled on this site, so every request is treated as anonymous (HTTP 401). WordPress only enables them over HTTPS or when the environment type is "local". This site reports %1$s and environment type "%2$s". Either serve the site over HTTPS, or add define(\'WP_ENVIRONMENT_TYPE\', \'local\'); to wp-config.php for a development site.', 'pagelayer'), | |
| 224 | + is_ssl() ? 'HTTPS' : 'HTTP', | |
| 225 | + function_exists('wp_get_environment_type') ? wp_get_environment_type() : 'unknown' | |
| 226 | + ); | |
| 227 | + Pagelayer_Abilities::save_test_connection_status(false, $message); | |
| 228 | + wp_send_json_error(['message' => $message]); | |
| 229 | + } | |
| 230 | + | |
| 231 | + $response = wp_remote_get($url, [ | |
| 232 | + // wp_remote_get() defaults to a 5s timeout, which a local dev stack will | |
| 233 | + // blow through on a loopback request (TLS handshake plus a full second | |
| 234 | + // WordPress bootstrap, often with WP_DEBUG on). | |
| 235 | + 'timeout' => 20, | |
| 236 | + 'headers' => [ | |
| 237 | + 'Accept' => 'application/json', | |
| 238 | + 'Authorization' => 'Basic ' . base64_encode($username . ':' . $password), | |
| 239 | + ], | |
| 240 | + 'sslverify' => false, | |
| 241 | + ]); | |
| 242 | + | |
| 243 | + $elapsed = round((microtime(true) - $start) * 1000); | |
| 244 | + | |
| 245 | + if(is_wp_error($response)){ | |
| 246 | + // The loopback never completed, so it told us nothing about the HTTP path. | |
| 247 | + // Verify in-process what we actually can — that the Application Password | |
| 248 | + // is valid and the abilities are registered — and say plainly which part | |
| 249 | + // is still unverified, instead of reporting a flat failure. | |
| 250 | + $local = pagelayer_mcp_verify_locally($username, $password); | |
| 251 | + | |
| 252 | + $message = sprintf( | |
| 253 | + __('Could not complete the loopback request to %1$s (%2$s). %3$s %4$s', 'pagelayer'), | |
| 254 | + $url, | |
| 255 | + $response->get_error_message(), | |
| 256 | + $local['message'], | |
| 257 | + __('A loopback failure is usually the local server itself, not your setup: many dev stacks run too few PHP workers to serve a second request while this one is still open, so the site cannot call itself. Your AI client connects from outside and is not affected. Confirm from a terminal with: curl -ik -u "USERNAME:APP PASSWORD" ', 'pagelayer') . $url | |
| 258 | + ); | |
| 259 | + | |
| 260 | + Pagelayer_Abilities::save_test_connection_status(false, $message); | |
| 261 | + wp_send_json_error([ | |
| 262 | + 'message' => $message, | |
| 263 | + 'local_check' => $local, | |
| 264 | + 'loopback_failed' => true, | |
| 265 | + ]); | |
| 266 | + } | |
| 267 | + | |
| 268 | + $code = wp_remote_retrieve_response_code($response); | |
| 269 | + $body = json_decode(wp_remote_retrieve_body($response), true); | |
| 270 | + | |
| 271 | + if($code >= 200 && $code < 300 && is_array($body)){ | |
| 272 | + $pagelayer_abilities = 0; | |
| 273 | + foreach($body as $ability){ | |
| 274 | + $name = isset($ability['name']) ? $ability['name'] : (isset($ability['id']) ? $ability['id'] : ''); | |
| 275 | + if(is_string($name) && strpos($name, 'pagelayer-') === 0){ | |
| 276 | + $pagelayer_abilities++; | |
| 277 | + } | |
| 278 | + } | |
| 279 | + | |
| 280 | + $message = sprintf(__('Authenticated with your Application Password and discovered %1$d Pagelayer abilities in %2$dms. Your site is ready to connect.', 'pagelayer'), $pagelayer_abilities, $elapsed); | |
| 281 | + | |
| 282 | + Pagelayer_Abilities::save_test_connection_status(true, $message); | |
| 283 | + | |
| 284 | + wp_send_json_success([ | |
| 285 | + 'message' => $message, | |
| 286 | + 'abilities' => $pagelayer_abilities, | |
| 287 | + 'elapsed_ms' => $elapsed, | |
| 288 | + ]); | |
| 289 | + } | |
| 290 | + | |
| 291 | + // A 401 means the request arrived with no authenticated user at all; a 403 | |
| 292 | + // means it authenticated but lacked the capability. Those need different fixes. | |
| 293 | + if($code === 401){ | |
| 294 | + $auth_header_seen = (bool) (function_exists('wp_get_authorization_header') ? wp_get_authorization_header() : (!empty($_SERVER['HTTP_AUTHORIZATION']) || !empty($_SERVER['REDIRECT_HTTP_AUTHORIZATION']))); | |
| 295 | + | |
| 296 | + $message = __('The abilities endpoint treated the request as anonymous (HTTP 401), so the Application Password never authenticated. The two usual causes are: (1) the password was revoked or mistyped — generate a new one below; or (2) your web server is stripping the Authorization header before PHP sees it. For Apache, ensure the "RewriteRule .* - [E=HTTP_AUTHORIZATION:%{HTTP:Authorization}]" line is present in .htaccess and that AllowOverride permits it. For nginx/FastCGI, ensure the Authorization header is forwarded to PHP-FPM.', 'pagelayer'); | |
| 297 | + | |
| 298 | + if(!$auth_header_seen){ | |
| 299 | + $message .= ' ' . __('Note: this admin request itself arrived without an Authorization header, which is normal here but means the stripping check is inconclusive — test with curl -u user:app-password against the endpoint to confirm.', 'pagelayer'); | |
| 300 | + } | |
| 301 | + | |
| 302 | + Pagelayer_Abilities::save_test_connection_status(false, $message); | |
| 303 | + wp_send_json_error([ | |
| 304 | + 'message' => $message, | |
| 305 | + ]); | |
| 306 | + } | |
| 307 | + | |
| 308 | + if($code === 403){ | |
| 309 | + $message = __('The Application Password authenticated, but the account was refused (HTTP 403). Check that this user still has the required capabilities, and that no security plugin is blocking REST API requests.', 'pagelayer'); | |
| 310 | + Pagelayer_Abilities::save_test_connection_status(false, $message); | |
| 311 | + wp_send_json_error([ | |
| 312 | + 'message' => $message, | |
| 313 | + ]); | |
| 314 | + } | |
| 315 | + | |
| 316 | + $message = sprintf(__('The abilities endpoint responded with status %1$d. Check the MCP Adapter is active and try again.', 'pagelayer'), $code); | |
| 317 | + Pagelayer_Abilities::save_test_connection_status(false, $message); | |
| 318 | + wp_send_json_error([ | |
| 319 | + 'message' => $message, | |
| 320 | + ]); | |
| 321 | +} | |
| 322 | + | |
| 323 | +function pagelayer_mcp_save_test_status() { | |
| 324 | + include_once(PAGELAYER_DIR.'/main/abilities.php'); | |
| 325 | + check_ajax_referer('pagelayer_mcp_nonce', 'nonce'); | |
| 326 | + | |
| 327 | + if(!current_user_can('manage_options')){ | |
| 328 | + wp_send_json_error(__('You do not have permission to do that.', 'pagelayer')); | |
| 329 | + } | |
| 330 | + | |
| 331 | + $ok = !empty($_POST['ok']); | |
| 332 | + $message = !empty($_POST['message']) ? sanitize_text_field(wp_unslash($_POST['message'])) : ''; | |
| 333 | + | |
| 334 | + Pagelayer_Abilities::save_test_connection_status($ok, $message); | |
| 335 | + | |
| 336 | + wp_send_json_success(); | |
| 337 | +} | |
| 338 | + | |
| 339 | +function pagelayer_mcp_save_image_api_key() { | |
| 340 | + check_ajax_referer('pagelayer_mcp_nonce', 'nonce'); | |
| 341 | + | |
| 342 | + if(!current_user_can('manage_options')){ | |
| 343 | + wp_send_json_error(array('message' => __('You do not have permission to do that.', 'pagelayer'))); | |
| 344 | + } | |
| 345 | + | |
| 346 | + $provider = !empty($_POST['provider']) ? sanitize_key(wp_unslash($_POST['provider'])) : ''; | |
| 347 | + $api_key = !empty($_POST['api_key']) ? sanitize_text_field(wp_unslash($_POST['api_key'])) : ''; | |
| 348 | + | |
| 349 | + if(empty($api_key)){ | |
| 350 | + wp_send_json_error(array('message' => __('An API key is required.', 'pagelayer'))); | |
| 351 | + } | |
| 352 | + | |
| 353 | + if($provider !== 'pexels'){ | |
| 354 | + wp_send_json_error(array('message' => __('Unsupported image provider.', 'pagelayer'))); | |
| 355 | + } | |
| 356 | + | |
| 357 | + update_option('pagelayer_pexels_api_key', $api_key, false); | |
| 358 | + | |
| 359 | + wp_send_json_success(array('message' => __('Image search API key saved.', 'pagelayer'))); | |
| 360 | +} | |
| 361 | + | |
| 362 | +function pagelayer_mcp_generate_app_password() { | |
| 363 | + include_once(PAGELAYER_DIR.'/main/abilities.php'); | |
| 364 | + check_ajax_referer('pagelayer_mcp_nonce', 'nonce'); | |
| 365 | + | |
| 366 | + if(!current_user_can('manage_options')){ | |
| 367 | + wp_send_json_error('Unauthorized'); | |
| 368 | + } | |
| 369 | + | |
| 370 | + $user_id = get_current_user_id(); | |
| 371 | + if(!class_exists('WP_Application_Passwords')){ | |
| 372 | + wp_send_json_error('Application Passwords not supported in this WP version.'); | |
| 373 | + } | |
| 374 | + | |
| 375 | + // WP_Application_Passwords::create_new_application_password() does NOT check | |
| 376 | + // availability, so without this the UI happily issues a password that | |
| 377 | + // wp_authenticate_application_password() will always reject — a "Generated" | |
| 378 | + // checkmark followed by a permanent 401. | |
| 379 | + if(function_exists('wp_is_application_passwords_available') && !wp_is_application_passwords_available()){ | |
| 380 | + wp_send_json_error(sprintf( | |
| 381 | + __('Application Passwords are disabled on this site, so a generated password could never authenticate. WordPress enables them only over HTTPS or when the environment type is "local". This site reports %1$s and environment type "%2$s". Serve the site over HTTPS, or add define(\'WP_ENVIRONMENT_TYPE\', \'local\'); to wp-config.php for a development site.', 'pagelayer'), | |
| 382 | + is_ssl() ? 'HTTPS' : 'HTTP', | |
| 383 | + function_exists('wp_get_environment_type') ? wp_get_environment_type() : 'unknown' | |
| 384 | + )); | |
| 385 | + } | |
| 386 | + | |
| 387 | + $passwords = \WP_Application_Passwords::get_user_application_passwords($user_id); | |
| 388 | + foreach($passwords as $app){ | |
| 389 | + if(!empty($app['app_id']) && $app['app_id'] === Pagelayer_Abilities::$APP_PASSWORD_APP_ID){ | |
| 390 | + \WP_Application_Passwords::delete_application_password($user_id, $app['uuid']); | |
| 391 | + } | |
| 392 | + } | |
| 393 | + | |
| 394 | + $new_password = \WP_Application_Passwords::create_new_application_password($user_id, array( | |
| 395 | + 'name' => Pagelayer_Abilities::$APP_PASSWORD_NAME, | |
| 396 | + 'app_id' => Pagelayer_Abilities::$APP_PASSWORD_APP_ID, | |
| 397 | + )); | |
| 398 | + | |
| 399 | + if(is_wp_error($new_password)){ | |
| 400 | + wp_send_json_error($new_password->get_error_message()); | |
| 401 | + } | |
| 402 | + | |
| 403 | + wp_send_json_success(array('password' => $new_password[0])); | |
| 404 | +} | |
| 405 | + | |
| 406 | +function pagelayer_mcp_adapter_action() { | |
| 407 | + include_once(PAGELAYER_DIR.'/main/abilities.php'); | |
| 408 | + check_ajax_referer('pagelayer_mcp_nonce', 'nonce'); | |
| 409 | + | |
| 410 | + if(!current_user_can('manage_options')){ | |
| 411 | + wp_send_json_error('Unauthorized'); | |
| 412 | + } | |
| 413 | + | |
| 414 | + $type = isset($_POST['type']) ? sanitize_text_field($_POST['type']) : ''; | |
| 415 | + | |
| 416 | + if(!function_exists('get_plugins')){ | |
| 417 | + require_once ABSPATH . 'wp-admin/includes/plugin.php'; | |
| 418 | + } | |
| 419 | + | |
| 420 | + if ($type === 'install') { | |
| 421 | + include_once ABSPATH . 'wp-admin/includes/class-wp-upgrader.php'; | |
| 422 | + include_once ABSPATH . 'wp-admin/includes/file.php'; | |
| 423 | + | |
| 424 | + // The MCP Adapter is hosted on GitHub, not WordPress.org, so fetch the download URL from the GitHub API | |
| 425 | + $download_url = Pagelayer_Abilities::get_mcp_adapter_download_url(); | |
| 426 | + | |
| 427 | + $upgrader = new \Plugin_Upgrader(new \Automatic_Upgrader_Skin()); | |
| 428 | + $installed = $upgrader->install($download_url); | |
| 429 | + | |
| 430 | + if(is_wp_error($installed)){ | |
| 431 | + wp_send_json_error($installed->get_error_message()); | |
| 432 | + }elseif(!$installed){ | |
| 433 | + wp_send_json_error('Installation failed. The MCP Adapter plugin is downloaded from GitHub. Please check your server can reach github.com.'); | |
| 434 | + } | |
| 435 | + | |
| 436 | + $plugin_file = Pagelayer_Abilities::get_installed_mcp_adapter_file(); | |
| 437 | + if($plugin_file){ | |
| 438 | + activate_plugin($plugin_file); | |
| 439 | + wp_send_json_success('Installed and activated'); | |
| 440 | + } | |
| 441 | + wp_send_json_success('Installed but not activated'); | |
| 442 | + | |
| 443 | + } elseif ($type === 'activate') { | |
| 444 | + $plugin_file = Pagelayer_Abilities::get_installed_mcp_adapter_file(); | |
| 445 | + if($plugin_file){ | |
| 446 | + $result = activate_plugin($plugin_file); | |
| 447 | + if(is_wp_error($result)){ | |
| 448 | + wp_send_json_error($result->get_error_message()); | |
| 449 | + } | |
| 450 | + wp_send_json_success('Activated'); | |
| 451 | + } | |
| 452 | + wp_send_json_error('Plugin not found'); | |
| 453 | + } | |
| 454 | + | |
| 455 | + wp_send_json_error('Invalid action'); | |
| 456 | +} | |
| 457 | + | |
| 136 | 458 | // Is the nonce there ? |
| 137 | 459 | if(empty($_REQUEST['pagelayer_nonce'])){ |
| 138 | 460 | return; |
| 139 | 461 | } |
| @@ -259,8 +581,90 @@ | ||
| 259 | 581 | } |
| 260 | 582 | |
| 261 | 583 | pagelayer_json_output($ret); |
| 262 | 584 | |
| 585 | +} | |
| 586 | + | |
| 587 | +// Build with AI — wrappers; generation/settings logic lives in Pagelayer_AI_Controller | |
| 588 | +add_action('wp_ajax_pagelayer_ai_generate', 'pagelayer_ai_generate'); | |
| 589 | +function pagelayer_ai_generate(){ | |
| 590 | + | |
| 591 | + check_ajax_referer('pagelayer_ajax', 'pagelayer_nonce'); | |
| 592 | + | |
| 593 | + $done = []; | |
| 594 | + | |
| 595 | + if(!current_user_can('edit_posts')){ | |
| 596 | + $done['error'][] = __pl('no_permission'); | |
| 597 | + pagelayer_json_output($done); | |
| 598 | + } | |
| 599 | + | |
| 600 | + $ctrl = Pagelayer_AI_Controller::get_instance(); | |
| 601 | + $res = $ctrl->process_generate(); | |
| 602 | + | |
| 603 | + if(is_wp_error($res)){ | |
| 604 | + $done['error'] = $res->get_error_message(); | |
| 605 | + pagelayer_json_output($done); | |
| 606 | + } | |
| 607 | + | |
| 608 | + $done = is_array($res) ? $res : []; | |
| 609 | + if(!isset($done['success'])){ | |
| 610 | + $done['success'] = true; | |
| 611 | + } | |
| 612 | + pagelayer_json_output($done); | |
| 613 | +} | |
| 614 | + | |
| 615 | +add_action('wp_ajax_pagelayer_ai_settings', 'pagelayer_ai_settings'); | |
| 616 | +function pagelayer_ai_settings(){ | |
| 617 | + | |
| 618 | + check_ajax_referer('pagelayer_ajax', 'pagelayer_nonce'); | |
| 619 | + | |
| 620 | + $done = []; | |
| 621 | + | |
| 622 | + if(!current_user_can('edit_posts')){ | |
| 623 | + $done['error'][] = __pl('no_permission'); | |
| 624 | + pagelayer_json_output($done); | |
| 625 | + } | |
| 626 | + | |
| 627 | + $ctrl = Pagelayer_AI_Controller::get_instance(); | |
| 628 | + $res = $ctrl->process_get_settings(); | |
| 629 | + | |
| 630 | + if(is_wp_error($res)){ | |
| 631 | + $done['error'] = $res->get_error_message(); | |
| 632 | + pagelayer_json_output($done); | |
| 633 | + } | |
| 634 | + | |
| 635 | + $done = is_array($res) ? $res : []; | |
| 636 | + if(!isset($done['success'])){ | |
| 637 | + $done['success'] = true; | |
| 638 | + } | |
| 639 | + pagelayer_json_output($done); | |
| 640 | +} | |
| 641 | + | |
| 642 | +add_action('wp_ajax_pagelayer_ai_save_settings', 'pagelayer_ai_save_settings'); | |
| 643 | +function pagelayer_ai_save_settings(){ | |
| 644 | + | |
| 645 | + check_ajax_referer('pagelayer_ajax', 'pagelayer_nonce'); | |
| 646 | + | |
| 647 | + $done = []; | |
| 648 | + | |
| 649 | + if(!current_user_can('edit_posts')){ | |
| 650 | + $done['error'][] = __pl('no_permission'); | |
| 651 | + pagelayer_json_output($done); | |
| 652 | + } | |
| 653 | + | |
| 654 | + $ctrl = Pagelayer_AI_Controller::get_instance(); | |
| 655 | + $res = $ctrl->process_save_settings(); | |
| 656 | + | |
| 657 | + if(is_wp_error($res)){ | |
| 658 | + $done['error'] = $res->get_error_message(); | |
| 659 | + pagelayer_json_output($done); | |
| 660 | + } | |
| 661 | + | |
| 662 | + $done = is_array($res) ? $res : []; | |
| 663 | + if(!isset($done['success'])){ | |
| 664 | + $done['success'] = true; | |
| 665 | + } | |
| 666 | + pagelayer_json_output($done); | |
| 263 | 667 | } |
| 264 | 668 | |
| 265 | 669 | // Update Post content |
| 266 | 670 | add_action('wp_ajax_pagelayer_save_content', 'pagelayer_save_content'); |