\d+)'; $can = array( __CLASS__, 'can_edit_post' ); register_rest_route( self::NS, "/documents/$id", array( array( 'methods' => 'GET', 'callback' => array( __CLASS__, 'get_document' ), 'permission_callback' => $can, ), array( 'methods' => 'POST', 'callback' => array( __CLASS__, 'save_document' ), 'permission_callback' => $can, ), ) ); register_rest_route( self::NS, "/documents/$id/sync", array( 'methods' => 'POST', 'callback' => array( Sync::class, 'rest' ), 'permission_callback' => $can, ) ); register_rest_route( self::NS, "/documents/$id/lock", array( 'methods' => 'POST', 'callback' => array( __CLASS__, 'lock' ), 'permission_callback' => $can, ) ); register_rest_route( self::NS, "/documents/$id/revisions", array( 'methods' => 'GET', 'callback' => array( __CLASS__, 'revisions' ), 'permission_callback' => $can, ) ); register_rest_route( self::NS, "/documents/$id/revisions/(?P\d+)", array( 'methods' => 'GET', 'callback' => array( __CLASS__, 'revision' ), 'permission_callback' => $can, ) ); register_rest_route( self::NS, "/documents/$id/elementor", array( 'methods' => 'GET', 'callback' => array( __CLASS__, 'elementor_data' ), 'permission_callback' => $can, ) ); register_rest_route( self::NS, '/render', array( 'methods' => 'POST', 'callback' => array( __CLASS__, 'render' ), 'permission_callback' => array( __CLASS__, 'can_build' ), ) ); register_rest_route( self::NS, '/dynamic', array( 'methods' => 'POST', 'callback' => array( __CLASS__, 'dynamic' ), 'permission_callback' => array( __CLASS__, 'can_build' ), ) ); register_rest_route( self::NS, '/templates', array( array( 'methods' => 'GET', 'callback' => array( __CLASS__, 'list_templates' ), 'permission_callback' => array( __CLASS__, 'can_build' ), ), array( 'methods' => 'POST', 'callback' => array( __CLASS__, 'create_template' ), 'permission_callback' => array( __CLASS__, 'can_build' ), ), ) ); register_rest_route( self::NS, "/templates/$id", array( array( 'methods' => 'GET', 'callback' => array( __CLASS__, 'get_template' ), 'permission_callback' => array( __CLASS__, 'can_build' ), ), array( 'methods' => 'POST', 'callback' => array( __CLASS__, 'update_template' ), 'permission_callback' => $can, ), array( 'methods' => 'DELETE', 'callback' => array( __CLASS__, 'delete_template' ), 'permission_callback' => $can, ), ) ); register_rest_route( self::NS, "/templates/$id/duplicate", array( 'methods' => 'POST', 'callback' => array( __CLASS__, 'duplicate_template' ), 'permission_callback' => $can, ) ); register_rest_route( self::NS, '/pages', array( 'methods' => 'POST', 'callback' => array( __CLASS__, 'create_page' ), 'permission_callback' => array( __CLASS__, 'can_build' ), ) ); register_rest_route( self::NS, '/globals', array( array( 'methods' => 'GET', 'callback' => array( __CLASS__, 'get_globals' ), 'permission_callback' => array( __CLASS__, 'can_build' ), ), array( 'methods' => 'POST', 'callback' => array( __CLASS__, 'save_globals' ), 'permission_callback' => array( __CLASS__, 'can_design' ), ), ) ); register_rest_route( self::NS, '/settings', array( array( 'methods' => 'GET', 'callback' => array( __CLASS__, 'get_settings' ), 'permission_callback' => array( __CLASS__, 'can_manage' ), ), array( 'methods' => 'POST', 'callback' => array( __CLASS__, 'save_settings' ), 'permission_callback' => array( __CLASS__, 'can_manage' ), ), ) ); register_rest_route( self::NS, '/overview', array( 'methods' => 'GET', 'callback' => array( __CLASS__, 'overview' ), 'permission_callback' => array( __CLASS__, 'can_build' ), ) ); register_rest_route( self::NS, '/rebuild', array( array( 'methods' => 'GET', 'callback' => array( __CLASS__, 'rebuild_list' ), 'permission_callback' => array( __CLASS__, 'can_manage' ), ), array( 'methods' => 'POST', 'callback' => array( __CLASS__, 'rebuild_done' ), 'permission_callback' => array( __CLASS__, 'can_manage' ), ), ) ); register_rest_route( self::NS, '/posts', array( 'methods' => 'GET', 'callback' => array( __CLASS__, 'search_posts' ), 'permission_callback' => array( __CLASS__, 'can_build' ), ) ); register_rest_route( self::NS, '/terms', array( 'methods' => 'GET', 'callback' => array( __CLASS__, 'search_terms' ), 'permission_callback' => array( __CLASS__, 'can_build' ), ) ); register_rest_route( self::NS, '/submissions', array( 'methods' => 'GET', 'callback' => array( Forms::class, 'rest_list' ), 'permission_callback' => array( __CLASS__, 'can_submissions' ), ) ); register_rest_route( self::NS, "/submissions/$id", array( array( 'methods' => 'GET', 'callback' => array( Forms::class, 'rest_get' ), 'permission_callback' => array( __CLASS__, 'can_submissions' ), ), array( 'methods' => 'DELETE', 'callback' => array( Forms::class, 'rest_delete' ), 'permission_callback' => array( __CLASS__, 'can_submissions' ), ), array( 'methods' => 'POST', 'callback' => array( Forms::class, 'rest_update' ), 'permission_callback' => array( __CLASS__, 'can_submissions' ), ), ) ); register_rest_route( self::NS, '/submissions/export', array( 'methods' => 'GET', 'callback' => array( Forms::class, 'rest_export' ), 'permission_callback' => array( __CLASS__, 'can_submissions' ), ) ); register_rest_route( self::NS, '/kit/export', array( 'methods' => 'GET', 'callback' => array( __CLASS__, 'kit_export' ), 'permission_callback' => array( __CLASS__, 'can_manage' ), ) ); register_rest_route( self::NS, '/kit/import', array( 'methods' => 'POST', 'callback' => array( __CLASS__, 'kit_import' ), 'permission_callback' => array( __CLASS__, 'can_manage' ), ) ); register_rest_route( self::NS, '/elementor', array( 'methods' => 'GET', 'callback' => array( __CLASS__, 'elementor_list' ), 'permission_callback' => array( __CLASS__, 'can_manage' ), ) ); register_rest_route( self::NS, '/integrations/test', array( 'methods' => 'POST', 'callback' => array( Integrations::class, 'rest_test' ), 'permission_callback' => array( __CLASS__, 'can_manage' ), ) ); register_rest_route( self::NS, '/ai', array( 'methods' => 'POST', 'callback' => array( Ai::class, 'rest' ), 'permission_callback' => array( __CLASS__, 'can_build' ), ) ); // Public endpoints (no nonce: pages may be served from a full-page cache). register_rest_route( self::NS, '/forms/submit', array( 'methods' => 'POST', 'callback' => array( Forms::class, 'submit' ), 'permission_callback' => '__return_true', ) ); register_rest_route( self::NS, '/search', array( 'methods' => 'GET', 'callback' => array( __CLASS__, 'live_search' ), 'permission_callback' => '__return_true', ) ); register_rest_route( self::NS, '/loop', array( 'methods' => 'POST', 'callback' => array( Widgets::class, 'rest_loop' ), 'permission_callback' => '__return_true', ) ); } // ------------------------------------------------------------------ // Permissions // ------------------------------------------------------------------ public static function can_build() { return Plugin::can_build(); } public static function can_edit_post( \WP_REST_Request $r ) { $id = (int) $r['id']; return $id && get_post( $id ) && Plugin::can_build( $id ); } public static function can_manage() { return Plugin::can_manage(); } public static function can_design() { return Plugin::can_build() && ! Plugin::content_only() && current_user_can( 'edit_theme_options' ); } public static function can_submissions() { return current_user_can( 'edit_pages' ) && Plugin::can_build(); } // ------------------------------------------------------------------ // Documents // ------------------------------------------------------------------ public static function get_document( \WP_REST_Request $r ) { $id = (int) $r['id']; $post = get_post( $id ); $draft = Document::get_draft( $id ); return array( 'id' => $id, 'title' => $post->post_title, 'status' => $post->post_status, 'doc' => Document::get_doc( $id ), 'draft' => $draft ? array( 'doc' => $draft['doc'], 'time' => $draft['time'], ) : null, 'hash' => Document::hash( $id ), 'template' => get_page_template_slug( $id ), 'type' => 'akram_template' === $post->post_type ? Post_Types::template_type( $id ) : $post->post_type, 'conditions' => 'akram_template' === $post->post_type ? Theme_Builder::get_conditions( $id ) : null, 'settings' => 'akram_template' === $post->post_type ? Theme_Builder::get_settings( $id ) : null, ); } public static function save_document( \WP_REST_Request $r ) { $id = (int) $r['id']; $post = get_post( $id ); require_once ABSPATH . 'wp-admin/includes/post.php'; $p = $r->get_json_params(); $mode = isset( $p['mode'] ) && 'draft' === $p['mode'] ? 'draft' : 'publish'; if ( Plugin::content_only() && isset( $p['doc'] ) ) { $p['doc'] = self::merge_content_only( Document::get_doc( $id ), $p['doc'] ); } if ( ! empty( $p['base'] ) && empty( $p['force'] ) && $p['base'] !== Document::hash( $id ) ) { $lock = wp_check_post_lock( $id ); return new \WP_Error( 'akram_conflict', __( 'This page was changed in another tab or by another user.', 'akram' ), array( 'status' => 409, 'user' => $lock ? get_the_author_meta( 'display_name', $lock ) : '', ) ); } $res = Document::save( $id, (array) ( $p['doc'] ?? array() ), (array) ( $p['out'] ?? array() ), $mode ); if ( is_wp_error( $res ) ) { return $res; } $update = array( 'ID' => $id ); if ( isset( $p['title'] ) && '' !== trim( (string) $p['title'] ) ) { $update['post_title'] = sanitize_text_field( $p['title'] ); } if ( 'publish' === $mode && ! empty( $p['status'] ) && in_array( $p['status'], array( 'publish', 'draft', 'pending', 'private' ), true ) ) { $want = $p['status']; if ( 'publish' === $want && ! current_user_can( get_post_type_object( $post->post_type )->cap->publish_posts ) ) { $want = 'pending'; } $update['post_status'] = $want; } elseif ( 'auto-draft' === $post->post_status ) { $update['post_status'] = 'draft'; } if ( isset( $p['slug'] ) && '' !== $p['slug'] ) { $update['post_name'] = sanitize_title( $p['slug'] ); } if ( count( $update ) > 1 ) { wp_update_post( wp_slash( $update ) ); } if ( isset( $p['template'] ) && 'page' === $post->post_type || isset( $p['template'] ) && in_array( $post->post_type, Options::enabled_post_types(), true ) ) { $tpl = in_array( $p['template'], array( '', 'default', 'akram-full', 'akram-canvas' ), true ) ? $p['template'] : ''; update_post_meta( $id, '_wp_page_template', $tpl ? $tpl : 'default' ); } if ( 'akram_template' === $post->post_type ) { if ( isset( $p['conditions'] ) && current_user_can( 'edit_theme_options' ) ) { Theme_Builder::set_conditions( $id, (array) $p['conditions'] ); } if ( isset( $p['settings'] ) ) { Theme_Builder::set_settings( $id, (array) $p['settings'] ); } } if ( isset( $p['thumb'] ) ) { $p['thumb'] ? set_post_thumbnail( $id, (int) $p['thumb'] ) : delete_post_thumbnail( $id ); } wp_set_post_lock( $id ); $post = get_post( $id ); return array( 'ok' => true, 'mode' => $mode, 'hash' => Document::hash( $id ), 'status' => $post->post_status, 'link' => get_permalink( $id ), 'preview' => self::preview_link( $id ), 'title' => $post->post_title, 'slug' => $post->post_name, ); } /** Content-only editors: keep structure & styles from the stored doc, accept text/media changes. */ private static function merge_content_only( $stored, $incoming ) { if ( ! is_array( $stored ) ) { return $incoming; } foreach ( $stored['els'] as $id => $el ) { if ( isset( $incoming['els'][ $id ]['s'] ) && is_array( $incoming['els'][ $id ]['s'] ) ) { foreach ( $incoming['els'][ $id ]['s'] as $k => $v ) { if ( '_' === substr( (string) $k, 0, 1 ) ) { continue; } if ( is_string( $v ) || ( is_array( $v ) && ( isset( $v['url'] ) || isset( $v[0] ) ) ) ) { $stored['els'][ $id ]['s'][ $k ] = $v; } } } } return $stored; } public static function preview_link( $id ) { return add_query_arg( array( 'nx_preview' => 1, '_nxp' => wp_create_nonce( 'nx_preview_' . $id ), ), get_permalink( $id ) ); } public static function lock( \WP_REST_Request $r ) { $id = (int) $r['id']; $take = (bool) $r->get_param( 'take' ); require_once ABSPATH . 'wp-admin/includes/post.php'; $me = get_current_user_id(); $presence = self::presence( $id, $me, $r ); $locked = wp_check_post_lock( $id ); if ( $r->get_param( 'co' ) ) { return array( 'locked' => false, 'co' => true, 'hash' => Document::hash( $id ), 'others' => self::others( $presence, $me ), ); } // Hand-over: the lock holder passes editing to someone who asked for it. $to = (int) $r->get_param( 'handover' ); if ( $to && ! $locked && isset( $presence[ $to ] ) ) { update_post_meta( $id, '_edit_lock', time() . ':' . $to ); $presence[ $to ]['granted'] = time(); unset( $presence[ $to ]['requested'] ); set_transient( 'akram_presence_' . $id, $presence, MINUTE_IN_SECONDS ); return array( 'locked' => true, 'handedOver' => true, 'others' => self::others( $presence, $me ), ); } if ( $locked && ! $take ) { $u = get_userdata( $locked ); return array( 'locked' => true, 'user' => $u ? $u->display_name : '', 'avatar' => $u ? get_avatar_url( $u->ID ) : '', 'others' => self::others( $presence, $me ), ); } $granted = ! empty( $presence[ $me ]['granted'] ); if ( $granted ) { unset( $presence[ $me ]['granted'] ); set_transient( 'akram_presence_' . $id, $presence, MINUTE_IN_SECONDS ); } wp_set_post_lock( $id ); return array( 'locked' => false, 'hash' => Document::hash( $id ), 'granted' => $granted, 'others' => self::others( $presence, $me ), ); } /** Who has this document open (seen in the last 45 s), plus pending edit requests. */ private static function presence( $id, $me, \WP_REST_Request $r ) { $list = get_transient( 'akram_presence_' . $id ); $list = is_array( $list ) ? $list : array(); foreach ( $list as $uid => $p ) { if ( time() - (int) ( $p['seen'] ?? 0 ) > 45 ) { unset( $list[ $uid ] ); } } $u = wp_get_current_user(); $list[ $me ] = array_merge( $list[ $me ] ?? array(), array( 'name' => $u->display_name, 'avatar' => get_avatar_url( $me, array( 'size' => 64 ) ), 'seen' => time(), ) ); if ( $r->get_param( 'request' ) ) { $list[ $me ]['requested'] = time(); } if ( $r->get_param( 'co' ) ) { $list[ $me ]['co'] = time(); unset( $list[ $me ]['requested'] ); } if ( $r->get_param( 'leave' ) ) { unset( $list[ $me ] ); } set_transient( 'akram_presence_' . $id, $list, MINUTE_IN_SECONDS ); return $list; } private static function others( array $list, $me ) { $out = array(); foreach ( $list as $uid => $p ) { if ( (int) $uid !== (int) $me ) { $out[] = array( 'id' => (int) $uid, 'name' => (string) $p['name'], 'avatar' => (string) $p['avatar'], 'requested' => ! empty( $p['requested'] ), 'co' => ! empty( $p['co'] ), ); } } return $out; } public static function revisions( \WP_REST_Request $r ) { return Document::revisions( (int) $r['id'] ); } public static function revision( \WP_REST_Request $r ) { $doc = Document::revision_doc( (int) $r['rev'], (int) $r['id'] ); return $doc ? array( 'doc' => $doc ) : new \WP_Error( 'akram_missing', __( 'Revision not found.', 'akram' ), array( 'status' => 404 ) ); } public static function elementor_data( \WP_REST_Request $r ) { $raw = get_post_meta( (int) $r['id'], '_elementor_data', true ); $data = is_string( $raw ) ? json_decode( $raw, true ) : $raw; $kit = get_option( 'elementor_active_kit' ); return array( 'data' => is_array( $data ) ? $data : null, 'kit' => $kit ? get_post_meta( (int) $kit, '_elementor_page_settings', true ) : null, ); } public static function elementor_list() { $ids = get_posts( array( 'post_type' => 'any', 'meta_query' => array( // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query -- bounded query on a builder meta key. 'relation' => 'OR', array( 'key' => '_elementor_edit_mode', 'value' => 'builder', ), array( 'key' => '_akram_elementor_edit_mode', 'value' => 'builder', ), ), 'posts_per_page' => 300, 'fields' => 'ids', 'post_status' => array( 'publish', 'draft', 'private' ), ) ); $out = array(); foreach ( $ids as $id ) { $out[] = array( 'id' => $id, 'title' => get_the_title( $id ), 'type' => get_post_type( $id ), 'built' => Document::is_built( $id ), ); } return $out; } // ------------------------------------------------------------------ // Server widget previews & dynamic values // ------------------------------------------------------------------ public static function render( \WP_REST_Request $r ) { $p = $r->get_json_params(); $ctx = self::readable( $p['post'] ?? 0 ); $out = array(); Widgets::setup_context( $ctx, self::readable( $p['preview'] ?? 0 ) ?: null ); foreach ( (array) ( $p['items'] ?? array() ) as $item ) { $key = sanitize_key( $item['key'] ?? ( $item['id'] ?? '' ) ); if ( ! $key ) { continue; } $out[ $key ] = Widgets::render( sanitize_key( $item['t'] ?? '' ), (array) ( $item['s'] ?? array() ), sanitize_key( $item['id'] ?? $key ), $ctx, true ); } foreach ( (array) ( $p['templates'] ?? array() ) as $tid ) { $tid = (int) $tid; if ( $tid && current_user_can( 'read_post', $tid ) ) { $o = Document::get_out( $tid ); $out[ 'tpl_' . $tid ] = array( 'doc' => Document::get_doc( $tid ), 'html' => $o ? $o['html'] : '', 'css' => Document::css( $tid ), ); } } Widgets::reset_context(); return $out; } /** A context/preview post id the current user may read (else 0): previews never expose private posts or others' drafts. */ private static function readable( $id ) { $id = (int) $id; return $id && get_post( $id ) && current_user_can( 'read_post', $id ) ? $id : 0; } public static function dynamic( \WP_REST_Request $r ) { $p = $r->get_json_params(); $ctx = self::readable( $p['post'] ?? 0 ); Widgets::setup_context( $ctx, self::readable( $p['preview'] ?? 0 ) ?: null ); $out = array(); foreach ( (array) ( $p['tags'] ?? array() ) as $key => $payload ) { $out[ $key ] = Dynamic::resolve( (array) $payload ); } Widgets::reset_context(); return $out; } // ------------------------------------------------------------------ // Templates // ------------------------------------------------------------------ public static function list_templates( \WP_REST_Request $r ) { // Comma-separated list ("header,footer"): split first, then clean each type (sanitize_key would drop the commas). $types = array_values( array_filter( array_map( 'sanitize_key', explode( ',', (string) $r->get_param( 'type' ) ) ) ) ); $args = array( 'post_type' => 'akram_template', 'posts_per_page' => 300, 'post_status' => array( 'publish', 'draft', 'private' ), 'orderby' => 'modified', 'order' => 'DESC', ); if ( $types ) { $args['meta_query'] = array( // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query -- bounded query on a builder meta key. array( 'key' => '_akram_type', 'value' => $types, 'compare' => 'IN', ), ); } $out = array(); foreach ( get_posts( $args ) as $post ) { $out[] = self::template_row( $post ); } return $out; } public static function template_row( $post ) { $user = get_userdata( (int) $post->post_author ); return array( 'id' => $post->ID, 'title' => $post->post_title, 'type' => Post_Types::template_type( $post->ID ), 'status' => $post->post_status, 'modified' => ( get_post_modified_time( 'c', true, $post ) ?: get_post_modified_time( 'c', false, $post ) ), 'author' => $user ? $user->display_name : '', 'conditions' => Theme_Builder::get_conditions( $post->ID ), 'settings' => Theme_Builder::get_settings( $post->ID ), 'edit' => Editor::edit_url( $post->ID ), 'thumb' => get_the_post_thumbnail_url( $post->ID, 'medium' ), 'link' => get_permalink( $post->ID ), ); } public static function create_template( \WP_REST_Request $r ) { $p = $r->get_json_params(); $type = in_array( $p['type'] ?? '', Post_Types::TEMPLATE_TYPES, true ) ? $p['type'] : 'section'; if ( in_array( $type, Post_Types::THEME_TYPES, true ) && ! current_user_can( 'edit_theme_options' ) ) { return new \WP_Error( 'akram_forbidden', __( 'You cannot create theme templates.', 'akram' ), array( 'status' => 403 ) ); } $id = wp_insert_post( array( 'post_type' => 'akram_template', 'post_status' => 'publish', 'post_title' => sanitize_text_field( $p['title'] ?? __( 'Untitled template', 'akram' ) ), ), true ); if ( is_wp_error( $id ) ) { return $id; } update_post_meta( $id, '_akram_type', $type ); update_post_meta( $id, '_akram_mode', 'builder' ); if ( ! empty( $p['doc'] ) ) { Document::save( $id, (array) $p['doc'], (array) ( $p['out'] ?? array() ) ); } if ( isset( $p['conditions'] ) && current_user_can( 'edit_theme_options' ) ) { Theme_Builder::set_conditions( $id, (array) $p['conditions'] ); } if ( isset( $p['settings'] ) ) { Theme_Builder::set_settings( $id, (array) $p['settings'] ); } return self::template_row( get_post( $id ) ); } public static function get_template( \WP_REST_Request $r ) { $id = (int) $r['id']; $post = get_post( $id ); if ( ! $post || 'akram_template' !== $post->post_type || ! current_user_can( 'read_post', $id ) ) { return new \WP_Error( 'akram_missing', __( 'Template not found.', 'akram' ), array( 'status' => 404 ) ); } $row = self::template_row( $post ); $row['doc'] = Document::get_doc( $id ); return $row; } public static function update_template( \WP_REST_Request $r ) { $id = (int) $r['id']; $post = get_post( $id ); if ( ! $post || 'akram_template' !== $post->post_type ) { return new \WP_Error( 'akram_missing', __( 'Template not found.', 'akram' ), array( 'status' => 404 ) ); } $p = $r->get_json_params(); if ( isset( $p['title'] ) ) { wp_update_post( array( 'ID' => $id, 'post_title' => sanitize_text_field( $p['title'] ), ) ); } if ( isset( $p['status'] ) && in_array( $p['status'], array( 'publish', 'draft' ), true ) ) { wp_update_post( array( 'ID' => $id, 'post_status' => $p['status'], ) ); } if ( isset( $p['type'] ) && in_array( $p['type'], Post_Types::TEMPLATE_TYPES, true ) ) { update_post_meta( $id, '_akram_type', $p['type'] ); } if ( isset( $p['conditions'] ) && current_user_can( 'edit_theme_options' ) ) { Theme_Builder::set_conditions( $id, (array) $p['conditions'] ); } if ( isset( $p['settings'] ) ) { Theme_Builder::set_settings( $id, (array) $p['settings'] ); } Frontend::flush_caches(); return self::template_row( get_post( $id ) ); } public static function delete_template( \WP_REST_Request $r ) { $id = (int) $r['id']; if ( 'akram_template' !== get_post_type( $id ) ) { return new \WP_Error( 'akram_missing', __( 'Template not found.', 'akram' ), array( 'status' => 404 ) ); } wp_trash_post( $id ); Frontend::flush_caches(); return array( 'ok' => true ); } public static function duplicate_template( \WP_REST_Request $r ) { $id = (int) $r['id']; $post = get_post( $id ); $new = wp_insert_post( array( 'post_type' => $post->post_type, 'post_status' => 'draft', 'post_title' => $post->post_title . ' ' . __( '(copy)', 'akram' ), ) ); foreach ( array( '_akram_type', '_akram_mode', '_akram_data', '_akram_out', '_akram_css', '_akram_settings' ) as $key ) { $v = get_post_meta( $id, $key, true ); if ( '' !== $v ) { update_post_meta( $new, $key, wp_slash( $v ) ); } } Document::write_css( $new, Document::css( $new ) ); return 'akram_template' === $post->post_type ? self::template_row( get_post( $new ) ) : array( 'id' => $new, 'edit' => Editor::edit_url( $new ), ); } /** Create a page (from a kit/template or blank) and return its editor URL. */ public static function create_page( \WP_REST_Request $r ) { $p = $r->get_json_params(); $type = post_type_exists( $p['post_type'] ?? '' ) ? $p['post_type'] : 'page'; if ( ! current_user_can( get_post_type_object( $type )->cap->edit_posts ) ) { return new \WP_Error( 'akram_forbidden', __( 'Not allowed.', 'akram' ), array( 'status' => 403 ) ); } $id = wp_insert_post( array( 'post_type' => $type, 'post_status' => 'draft', 'post_title' => sanitize_text_field( $p['title'] ?? __( 'New page', 'akram' ) ), ), true ); if ( is_wp_error( $id ) ) { return $id; } update_post_meta( $id, '_akram_mode', 'builder' ); update_post_meta( $id, '_wp_page_template', Options::get( 'default_template', 'akram-full' ) ); if ( ! empty( $p['doc'] ) ) { Document::save( $id, (array) $p['doc'], (array) ( $p['out'] ?? array() ) ); if ( ! empty( $p['publish'] ) && current_user_can( 'publish_pages' ) ) { wp_update_post( array( 'ID' => $id, 'post_status' => 'publish', ) ); } } if ( ! empty( $p['front'] ) && current_user_can( 'manage_options' ) ) { update_option( 'show_on_front', 'page' ); update_option( 'page_on_front', $id ); } return array( 'id' => $id, 'edit' => Editor::edit_url( $id ), 'link' => get_permalink( $id ), ); } // ------------------------------------------------------------------ // Globals & settings // ------------------------------------------------------------------ public static function get_globals() { return array( 'globals' => Options::globals() ); } public static function save_globals( \WP_REST_Request $r ) { $p = $r->get_json_params(); $g = self::clean_globals( (array) ( $p['globals'] ?? array() ) ); Document::save_globals( $g ); return array( 'ok' => true, 'globals' => $g, ); } private static function clean_globals( array $g ) { $clean_str = static function ( $v ) { return is_scalar( $v ) ? sanitize_text_field( (string) $v ) : ''; }; $walk = static function ( $v ) use ( &$walk, $clean_str ) { if ( is_array( $v ) ) { $o = array(); foreach ( $v as $k => $x ) { $o[ is_int( $k ) ? $k : sanitize_text_field( $k ) ] = $walk( $x ); } return $o; } return is_bool( $v ) || is_int( $v ) || is_float( $v ) ? $v : $clean_str( $v ); }; unset( $g['css'] ); // no free-form global CSS: the design system is built from its visual settings return $walk( $g ); } public static function get_settings() { $s = Options::all(); foreach ( Integrations::SECRETS as $secret ) { $s[ $secret ] = $s[ $secret ] ? '••••••••' . substr( $s[ $secret ], -4 ) : ''; } return array( 'settings' => $s, 'post_types' => array_values( array_map( static function ( $pt ) { return array( 'id' => $pt->name, 'label' => $pt->labels->name, ); }, get_post_types( array( 'public' => true ), 'objects' ) ) ), 'roles' => array_map( static function ( $r ) { return translate_user_role( $r['name'] ); }, wp_roles()->roles ), ); } public static function save_settings( \WP_REST_Request $r ) { $p = (array) $r->get_json_params(); foreach ( Integrations::SECRETS as $secret ) { if ( isset( $p[ $secret ] ) && 0 === strpos( (string) $p[ $secret ], '••••' ) ) { unset( $p[ $secret ] ); } } $before = Options::all(); Options::update( $p ); $after = Options::all(); // Stylesheets depend on the breakpoints and on where CSS is stored: rebuild them from the saved data. if ( $before['bp_l'] !== $after['bp_l'] || $before['bp_t'] !== $after['bp_t'] || $before['bp_m'] !== $after['bp_m'] || $before['bp_laptop'] !== $after['bp_laptop'] || $before['css_method'] !== $after['css_method'] ) { Document::rebuild_all_css(); } Frontend::flush_caches(); return self::get_settings(); } public static function overview() { $counts = array(); foreach ( Post_Types::TEMPLATE_TYPES as $t ) { $counts[ $t ] = 0; } foreach ( get_posts( array( 'post_type' => 'akram_template', 'posts_per_page' => -1, 'fields' => 'ids', 'post_status' => array( 'publish', 'draft' ), ) ) as $id ) { $counts[ Post_Types::template_type( $id ) ]++; } $recent = array(); foreach ( get_posts( array( 'post_type' => Options::enabled_post_types(), 'meta_key' => '_akram_mode', // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key -- bounded query on a builder meta key. 'meta_value' => 'builder', // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_value -- bounded query on a builder meta key. 'posts_per_page' => 8, 'orderby' => 'modified', 'post_status' => array( 'publish', 'draft', 'private', 'pending' ), ) ) as $post ) { $recent[] = array( 'id' => $post->ID, 'title' => $post->post_title ?: __( '(no title)', 'akram' ), 'type' => $post->post_type, 'status' => $post->post_status, 'modified' => ( get_post_modified_time( 'c', true, $post ) ?: get_post_modified_time( 'c', false, $post ) ), 'edit' => Editor::edit_url( $post->ID ), 'link' => get_permalink( $post->ID ), ); } $subs = wp_count_posts( 'akram_submission' ); global $wp_version; $d = Document::upload_dir(); return array( 'templates' => $counts, 'recent' => $recent, 'pages' => count( Document::all_built( 2000 ) ), 'submissions' => array( 'total' => (int) ( $subs->publish ?? 0 ) + (int) ( $subs->private ?? 0 ), 'unread' => (int) ( $subs->private ?? 0 ), ), 'rebuild' => (bool) get_option( 'akram_needs_rebuild' ), 'elementor' => defined( 'ELEMENTOR_VERSION' ) || (bool) get_posts( array( 'post_type' => 'any', 'meta_key' => '_elementor_edit_mode', // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key -- bounded query on a builder meta key. 'posts_per_page' => 1, 'fields' => 'ids', ) ), 'system' => array( 'php' => PHP_VERSION, 'wp' => $wp_version, 'theme' => wp_get_theme()->get( 'Name' ), 'block' => function_exists( 'wp_is_block_theme' ) && wp_is_block_theme(), 'writable' => Document::ensure_upload_dir(), 'memory' => WP_MEMORY_LIMIT, 'woo' => Options::has_woo(), 'cssMethod' => Options::get( 'css_method' ), 'uploads' => $d['url'], ), ); } public static function rebuild_list() { $out = array(); foreach ( Document::all_built( 2000 ) as $id ) { $out[] = array( 'id' => $id, 'title' => get_the_title( $id ), 'doc' => Document::get_doc( $id ), ); } return array( 'items' => $out ); } public static function rebuild_done() { Document::save_globals( Options::globals() ); delete_option( 'akram_needs_rebuild' ); return array( 'ok' => true ); } // ------------------------------------------------------------------ // Pickers // ------------------------------------------------------------------ public static function search_posts( \WP_REST_Request $r ) { $q = sanitize_text_field( (string) $r->get_param( 'q' ) ); // Public post types plus templates; never form submissions or revisions. $allowed = array_values( array_diff( get_post_types( array( 'public' => true ) ), array( 'attachment' ) ) ); $types = $r->get_param( 'type' ) ? array_values( array_intersect( array_map( 'sanitize_key', explode( ',', (string) $r->get_param( 'type' ) ) ), $allowed ) ) : $allowed; $ids = $r->get_param( 'ids' ) ? array_map( 'absint', explode( ',', (string) $r->get_param( 'ids' ) ) ) : array(); $args = array( 'post_type' => $types ?: 'post', 'posts_per_page' => 20, 'post_status' => array( 'publish', 'draft', 'private', 'pending', 'future' ), 's' => $q, ); if ( $ids ) { $args = array( 'post_type' => $allowed, 'post_status' => 'any', 'post__in' => $ids, 'posts_per_page' => count( $ids ), 'orderby' => 'post__in', ); } $out = array(); foreach ( get_posts( $args ) as $post ) { // Only what this user may read: other authors' drafts and private posts stay hidden. if ( ! current_user_can( 'read_post', $post->ID ) ) { continue; } $out[] = array( 'id' => $post->ID, 'title' => $post->post_title ?: '#' . $post->ID, 'type' => $post->post_type, 'url' => get_permalink( $post->ID ), 'status' => $post->post_status, ); } return $out; } public static function search_terms( \WP_REST_Request $r ) { $q = sanitize_text_field( (string) $r->get_param( 'q' ) ); $ids = $r->get_param( 'ids' ) ? array_map( 'absint', explode( ',', (string) $r->get_param( 'ids' ) ) ) : array(); $args = array( 'hide_empty' => false, 'number' => 30, 'taxonomy' => get_taxonomies( array( 'public' => true ) ), ); if ( $q ) { $args['search'] = $q; } if ( $ids ) { $args['include'] = $ids; } $out = array(); foreach ( get_terms( $args ) as $t ) { $tax = get_taxonomy( $t->taxonomy ); $out[] = array( 'id' => $t->term_id, 'name' => $t->name, 'tax' => $t->taxonomy, 'taxLabel' => $tax ? $tax->labels->singular_name : $t->taxonomy, 'slug' => $t->slug, 'count' => $t->count, ); } return $out; } public static function live_search( \WP_REST_Request $r ) { $q = sanitize_text_field( (string) $r->get_param( 's' ) ?: (string) $r->get_param( 'q' ) ); if ( strlen( $q ) < 2 ) { return array(); } $types = array_filter( array_map( 'sanitize_key', explode( ',', (string) $r->get_param( 'types' ) ) ) ); $public = get_post_types( array( 'public' => true, 'exclude_from_search' => false, ) ); $types = $types ? array_values( array_intersect( $types, $public ) ) : array_values( $public ); $posts = get_posts( array( 's' => $q, 'post_type' => $types ?: 'post', 'posts_per_page' => min( 12, max( 1, (int) $r->get_param( 'limit' ) ?: 6 ) ), 'post_status' => 'publish', ) ); $out = array(); foreach ( $posts as $post ) { $pt = get_post_type_object( $post->post_type ); $row = array( 'title' => html_entity_decode( get_the_title( $post ), ENT_QUOTES ), 'url' => get_permalink( $post ), 'type' => $pt ? $pt->labels->singular_name : '', 'img' => get_the_post_thumbnail_url( $post, 'thumbnail' ) ?: '', ); if ( 'product' === $post->post_type && function_exists( 'wc_get_product' ) ) { $prod = wc_get_product( $post->ID ); if ( $prod ) { $row['price'] = wp_strip_all_tags( wc_price( $prod->get_price() ) ); } } $out[] = $row; } $res = rest_ensure_response( $out ); $res->header( 'Cache-Control', 'public, max-age=300' ); return $res; } // ------------------------------------------------------------------ // Site kit export / import // ------------------------------------------------------------------ public static function kit_export() { $templates = array(); foreach ( get_posts( array( 'post_type' => 'akram_template', 'posts_per_page' => -1, 'post_status' => array( 'publish', 'draft' ), ) ) as $post ) { $templates[] = array( 'title' => $post->post_title, 'type' => Post_Types::template_type( $post->ID ), 'status' => $post->post_status, 'doc' => Document::get_doc( $post->ID ), 'conditions' => Theme_Builder::get_conditions( $post->ID ), 'settings' => Theme_Builder::get_settings( $post->ID ), ); } $pages = array(); foreach ( Document::all_built( 300 ) as $id ) { if ( 'akram_template' === get_post_type( $id ) ) { continue; } $pages[] = array( 'title' => get_the_title( $id ), 'type' => get_post_type( $id ), 'slug' => get_post_field( 'post_name', $id ), 'doc' => Document::get_doc( $id ), 'template' => get_page_template_slug( $id ), ); } return array( 'akram' => AKRAM_VERSION, 'site' => get_bloginfo( 'name' ), 'date' => gmdate( 'c' ), 'globals' => Options::globals(), 'templates' => $templates, 'pages' => $pages, ); } /** * Import kit. Documents arrive already rendered by the admin app (render engine runs client-side). */ public static function kit_import( \WP_REST_Request $r ) { $p = $r->get_json_params(); $created = array(); if ( ! empty( $p['globals'] ) && is_array( $p['globals'] ) ) { Document::save_globals( self::clean_globals( $p['globals'] ) ); } foreach ( (array) ( $p['templates'] ?? array() ) as $t ) { $type = in_array( $t['type'] ?? '', Post_Types::TEMPLATE_TYPES, true ) ? $t['type'] : 'section'; $id = wp_insert_post( array( 'post_type' => 'akram_template', 'post_status' => 'draft' === ( $t['status'] ?? '' ) ? 'draft' : 'publish', 'post_title' => sanitize_text_field( $t['title'] ?? 'Template' ), ) ); update_post_meta( $id, '_akram_type', $type ); update_post_meta( $id, '_akram_mode', 'builder' ); Document::save( $id, (array) ( $t['doc'] ?? array() ), (array) ( $t['out'] ?? array() ) ); Theme_Builder::set_conditions( $id, (array) ( $t['conditions'] ?? array() ) ); Theme_Builder::set_settings( $id, (array) ( $t['settings'] ?? array() ) ); $created[] = array( 'id' => $id, 'title' => get_the_title( $id ), 'kind' => 'template', ); } foreach ( (array) ( $p['pages'] ?? array() ) as $pg ) { $type = post_type_exists( $pg['type'] ?? '' ) ? $pg['type'] : 'page'; $id = wp_insert_post( array( 'post_type' => $type, 'post_status' => 'publish', 'post_title' => sanitize_text_field( $pg['title'] ?? 'Page' ), 'post_name' => sanitize_title( $pg['slug'] ?? '' ), ) ); update_post_meta( $id, '_akram_mode', 'builder' ); update_post_meta( $id, '_wp_page_template', in_array( $pg['template'] ?? '', array( 'akram-full', 'akram-canvas' ), true ) ? $pg['template'] : 'akram-full' ); Document::save( $id, (array) ( $pg['doc'] ?? array() ), (array) ( $pg['out'] ?? array() ) ); if ( ! empty( $pg['front'] ) ) { update_option( 'show_on_front', 'page' ); update_option( 'page_on_front', $id ); } $created[] = array( 'id' => $id, 'title' => get_the_title( $id ), 'kind' => 'page', 'link' => get_permalink( $id ), 'edit' => Editor::edit_url( $id ), ); } Frontend::flush_caches(); return array( 'ok' => true, 'created' => $created, ); } }