Sanitize::html( (string) ( $out['html'] ?? '' ) ), 'modules' => array_values( array_filter( array_map( 'sanitize_key', (array) ( $out['modules'] ?? array() ) ) ) ), 'types' => array_values( array_filter( array_map( 'sanitize_key', (array) ( $out['types'] ?? array() ) ) ) ), 'dyn' => is_array( $out['dyn'] ?? null ) ? $out['dyn'] : array(), 'cond' => is_array( $out['cond'] ?? null ) ? $out['cond'] : array(), 'forms' => is_array( $out['forms'] ?? null ) ? $out['forms'] : array(), 'lock' => self::clean_locks( (array) ( $out['lock'] ?? array() ) ), 'flags' => array_map( 'sanitize_key', array_filter( (array) ( $out['flags'] ?? array() ), 'is_scalar' ) ), 'tpl' => array_values( array_filter( array_map( 'absint', (array) ( $out['tpl'] ?? array() ) ) ) ), 'fonts' => self::clean_fonts( (array) ( $out['fonts'] ?? array() ), $css_fonts ), 'anim' => ! empty( $out['anim'] ), 'engine' => AKRAM_VERSION, 'time' => time(), ); return $clean; } /** Font families to load (family => weights), merged from several lists and reduced to safe names. */ public static function clean_fonts( array ...$lists ) { $clean = array(); foreach ( $lists as $list ) { foreach ( $list as $family => $weights ) { $family = trim( preg_replace( '/[^A-Za-z0-9 \-]/', '', (string) $family ) ); if ( '' === $family ) { continue; } foreach ( (array) $weights as $w ) { $w = preg_replace( '/[^0-9]/', '', (string) $w ); if ( '' !== $w ) { $clean[ $family ][ $w ] = $w; } } $clean[ $family ] = $clean[ $family ] ?? array(); } } foreach ( $clean as $family => $weights ) { sort( $weights, SORT_STRING ); $clean[ $family ] = array_values( $weights ); } return $clean; } /** Protected-content rules: passwords are stored as a keyed hash, never in clear text. */ private static function clean_locks( array $locks ) { $clean = array(); foreach ( $locks as $id => $l ) { $id = sanitize_key( (string) $id ); if ( ! $id || ! is_array( $l ) ) { continue; } $mode = in_array( $l['mode'] ?? '', array( 'password', 'login', 'role' ), true ) ? $l['mode'] : 'password'; $pass = (string) ( $l['pass'] ?? '' ); $clean[ $id ] = array( 'mode' => $mode, 'pass' => '' === $pass ? '' : ( 0 === strpos( $pass, 'nxh$' ) ? $pass : Runtime::pass_hash( $pass ) ), 'roles' => sanitize_text_field( (string) ( $l['roles'] ?? '' ) ), ); } return $clean; } /** * Save a document. $mode: 'publish' writes the live version, 'draft' only stores a draft. * The stylesheet is built here from the document's design settings (Css::document); no CSS is accepted. */ public static function save( $post_id, array $doc, array $out, $mode = 'publish' ) { $doc = Sanitize::document( $doc ); if ( null === $doc ) { return new \WP_Error( 'akram_invalid', __( 'The document structure is invalid.', 'akram' ), array( 'status' => 400 ) ); } $compiled = Css::document( $doc ); $css = $compiled['css']; $out = self::clean_out( $out, $compiled['fonts'] ); // Documents rendered before the post existed (page kits, imports) carry form ID 0. $out['html'] = str_replace( 'data-nx-form="0:', 'data-nx-form="' . (int) $post_id . ':', $out['html'] ); Conditions::note_rules( $out['cond'] ); if ( 'draft' === $mode ) { update_post_meta( $post_id, '_akram_draft', wp_slash( wp_json_encode( array( 'doc' => $doc, 'out' => $out, 'css' => $css, 'time' => time(), 'user' => get_current_user_id(), ) ) ) ); update_post_meta( $post_id, '_akram_mode', 'builder' ); return true; } self::store_revision( $post_id ); update_post_meta( $post_id, '_akram_mode', 'builder' ); // A page migrated from Elementor: stop Elementor from rendering it and loading its assets. // The Elementor data stays untouched; the flag is kept so the switch can be undone. if ( 'builder' === get_post_meta( $post_id, '_elementor_edit_mode', true ) ) { update_post_meta( $post_id, '_akram_elementor_edit_mode', 'builder' ); delete_post_meta( $post_id, '_elementor_edit_mode' ); if ( 'elementor_header_footer' === get_post_meta( $post_id, '_wp_page_template', true ) || 'elementor_canvas' === get_post_meta( $post_id, '_wp_page_template', true ) ) { update_post_meta( $post_id, '_wp_page_template', 'elementor_canvas' === get_post_meta( $post_id, '_wp_page_template', true ) ? 'akram-canvas' : 'akram-full' ); } } update_post_meta( $post_id, '_akram_data', wp_slash( wp_json_encode( $doc ) ) ); update_post_meta( $post_id, '_akram_out', wp_slash( wp_json_encode( $out ) ) ); update_post_meta( $post_id, '_akram_css', wp_slash( $css ) ); delete_post_meta( $post_id, '_akram_draft' ); self::write_css( $post_id, $css ); self::write_fallback( $post_id, $out ); Frontend::flush_caches( $post_id ); do_action( 'akram_document_saved', $post_id, $doc, $out ); return true; } /** * Plain-HTML copy in post_content: survives plugin deactivation, readable by SEO plugins & search. * It holds markup only; styles are always loaded with wp_enqueue_style() / wp_add_inline_style(). */ private static function write_fallback( $post_id, array $out ) { $post = get_post( $post_id ); if ( ! $post || 'akram_template' === $post->post_type ) { return; } $html = preg_replace( '/[\s\S]*?/i', '', $out['html'] ); $html = preg_replace( '//', '', $html ); $html = preg_replace( '/\{\{nx:[A-Za-z0-9+\/=]+\}\}/', '', $html ); $content = '' . $html; remove_action( 'post_updated', 'wp_save_post_revision' ); $GLOBALS['akram_saving_fallback'] = true; wp_update_post( array( 'ID' => $post_id, 'post_content' => $content, ) ); $GLOBALS['akram_saving_fallback'] = false; add_action( 'post_updated', 'wp_save_post_revision' ); } private static function store_revision( $post_id ) { $current = get_post_meta( $post_id, '_akram_data', true ); if ( ! $current ) { return; } wp_insert_post( array( 'post_type' => 'akram_revision', 'post_parent' => $post_id, 'post_status' => 'publish', 'post_title' => get_the_title( $post_id ), 'post_content' => wp_slash( $current ), 'post_author' => get_current_user_id(), ) ); $old = get_posts( array( 'post_type' => 'akram_revision', 'post_parent' => $post_id, 'posts_per_page' => -1, 'fields' => 'ids', 'orderby' => 'ID', 'order' => 'DESC', 'offset' => self::MAX_REVISIONS, ) ); foreach ( $old as $id ) { wp_delete_post( $id, true ); } } public static function revisions( $post_id ) { $out = array(); foreach ( get_posts( array( 'post_type' => 'akram_revision', 'post_parent' => $post_id, 'posts_per_page' => self::MAX_REVISIONS, 'orderby' => 'ID', 'order' => 'DESC', ) ) as $rev ) { $user = get_userdata( (int) $rev->post_author ); $out[] = array( 'id' => $rev->ID, 'date' => get_post_time( 'c', true, $rev ), 'author' => $user ? $user->display_name : '', 'avatar' => $user ? get_avatar_url( $user->ID, array( 'size' => 48 ) ) : '', 'size' => strlen( $rev->post_content ), ); } return $out; } public static function revision_doc( $rev_id, $post_id ) { $rev = get_post( $rev_id ); if ( ! $rev || 'akram_revision' !== $rev->post_type || (int) $rev->post_parent !== (int) $post_id ) { return null; } $doc = json_decode( $rev->post_content, true ); return is_array( $doc ) ? $doc : null; } // --------------------------------------------------------------------- // CSS files // --------------------------------------------------------------------- public static function upload_dir() { $u = wp_upload_dir( null, false ); return array( 'dir' => trailingslashit( $u['basedir'] ) . 'akram/', 'url' => set_url_scheme( trailingslashit( $u['baseurl'] ) . 'akram/' ), ); } public static function ensure_upload_dir() { $d = self::upload_dir(); foreach ( array( '', 'css', 'fonts' ) as $sub ) { if ( ! is_dir( $d['dir'] . $sub ) ) { wp_mkdir_p( $d['dir'] . $sub ); } } if ( ! file_exists( $d['dir'] . 'index.php' ) ) { @file_put_contents( $d['dir'] . 'index.php', " print inline). */ public static function css_url( $post_id ) { $v = get_post_meta( $post_id, '_akram_css_file', true ); if ( ! $v || 'file' !== Options::get( 'css_method' ) ) { return null; } $d = self::upload_dir(); if ( ! file_exists( $d['dir'] . 'css/post-' . (int) $post_id . '.css' ) ) { return null; } return $d['url'] . 'css/post-' . (int) $post_id . '.css?v=' . $v; } public static function css( $post_id ) { return (string) get_post_meta( $post_id, '_akram_css', true ); } // --------------------------------------------------------------------- // Global design system CSS // --------------------------------------------------------------------- public static function global_css() { $css = get_option( 'akram_global_css', '' ); if ( '' === $css ) { $file = AKRAM_DIR . 'assets/build/' . Plugin::build_file( 'global-default.css' ); $css = file_exists( $file ) ? (string) file_get_contents( $file ) : ''; } return $css; } /** Save the design system. Its stylesheet is built here from the settings (Css::globals). */ public static function save_globals( array $globals ) { $compiled = Css::globals( $globals ); $css = $compiled['css']; update_option( 'akram_globals', $globals, true ); update_option( 'akram_global_css', $css, true ); update_option( 'akram_global_fonts', self::clean_fonts( $compiled['fonts'] ), true ); update_option( 'akram_global_ver', substr( md5( $css . microtime() ), 0, 10 ), true ); if ( 'file' === Options::get( 'css_method' ) && self::ensure_upload_dir() ) { $d = self::upload_dir(); @file_put_contents( $d['dir'] . 'css/global.css', $css ); } Frontend::flush_caches(); } /** Rebuild a saved document's stylesheet from its stored design data. */ public static function rebuild_css( $post_id ) { $doc = self::get_doc( $post_id ); if ( ! $doc ) { return false; } $css = Css::document( $doc )['css']; update_post_meta( $post_id, '_akram_css', wp_slash( $css ) ); self::write_css( $post_id, $css ); return true; } /** Rebuild every stylesheet (after the breakpoints or the CSS storage method change). Returns the count. */ public static function rebuild_all_css( $limit = 5000 ) { $n = 0; foreach ( self::all_built( absint( $limit ) ? absint( $limit ) : 5000 ) as $id ) { $n += self::rebuild_css( $id ) ? 1 : 0; } self::save_globals( Options::globals() ); return $n; } public static function global_css_url() { $d = self::upload_dir(); $ver = get_option( 'akram_global_ver', '' ); if ( 'file' === Options::get( 'css_method' ) && $ver && file_exists( $d['dir'] . 'css/global.css' ) ) { return $d['url'] . 'css/global.css?v=' . $ver; } if ( ! get_option( 'akram_global_css' ) && file_exists( AKRAM_DIR . 'assets/build/global-default.css' ) ) { return Plugin::build_url( 'global-default.css' ) . '?v=' . AKRAM_VERSION; } return null; } /** All posts built with Akram (for rebuild tools). */ public static function all_built( $limit = 500 ) { return get_posts( array( 'post_type' => array_values( array_unique( array_merge( array_values( get_post_types( array( 'public' => true ) ) ), array( 'akram_template' ) ) ) ), 'post_status' => array( 'publish', 'draft', 'private', 'pending', 'future' ), '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' => $limit, 'fields' => 'ids', ) ); } }