'en_US', 'pl' => 'pl_PL', 'fr' => 'fr_FR', 'de' => 'de_DE', 'es' => 'es_ES']; // English has no catalogue: WordPress uses the source strings. return WP_LANG_DIR . '/plugins/inceptus-process-' . $locales[$language] . '.mo'; } add_filter('load_textdomain_mofile', 'inceptus_process_translation_file', 10, 2); /** * Let WordPress discover an external base-language pack for a regional locale. * Exact-locale packs keep priority; no bundled translation path is registered. */ function inceptus_process_translation_directory($path, $domain, $locale) { if ($domain !== 'inceptus-process' || $path) { return $path; } $locales = ['pl' => 'pl_PL', 'fr' => 'fr_FR', 'de' => 'de_DE', 'es' => 'es_ES']; $language = inceptus_process_normalize_language($locale); if (!isset($locales[$language])) { return $path; } $base = WP_LANG_DIR . '/plugins/inceptus-process-' . $locales[$language]; return is_readable($base . '.mo') || is_readable($base . '.l10n.php') ? WP_LANG_DIR . '/plugins/' : $path; } add_filter('lang_dir_for_domain', 'inceptus_process_translation_directory', 10, 3); /** * Detect the language of the currently displayed page. * * The front-end language is intentionally independent from the WordPress * administrator/user locale. Common multilingual plugins are checked first; * WordPress locale is used as the fallback. */ function inceptus_process_page_language() { // Polylang. if (function_exists('pll_current_language')) { $slug = pll_current_language('slug'); $language = inceptus_process_normalize_language($slug, ''); if ($language !== '') { return $language; } $locale = pll_current_language('locale'); $language = inceptus_process_normalize_language($locale, ''); if ($language !== '') { return $language; } if ((is_string($slug) && trim($slug) !== '') || (is_string($locale) && trim($locale) !== '')) { return 'en'; } } // WPML. $wpml_language = apply_filters('wpml_current_language', null); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- WPML owns this documented hook name. if (is_string($wpml_language) && trim($wpml_language) !== '') { return inceptus_process_normalize_language($wpml_language); } // TranslatePress exposes the currently selected front-end language here. global $TRP_LANGUAGE; if (is_string($TRP_LANGUAGE) && trim($TRP_LANGUAGE) !== '') { return inceptus_process_normalize_language($TRP_LANGUAGE); } return inceptus_process_interface_language(); } /** * Fixed public-view labels are selected from the current page language. * Content coming from process.json is never translated by the plugin. */ function inceptus_process_frontend_labels($language = null) { $language = $language === null ? inceptus_process_page_language() : inceptus_process_normalize_language($language); $labels = [ 'en' => [ 'invalid_initial_view' => 'Invalid initial view. Use width, height or custom.', 'invalid_mode' => 'Invalid presentation mode. Use simulation or animation.', 'invalid_shortcode' => 'Use either a process code or a process group.', 'empty_group' => 'This group contains no available processes.', 'invalid_code' => 'Invalid or missing process code.', 'display_error' => 'The process could not be displayed.', 'start' => 'Start', 'select_start' => 'Select a start event', 'zoom_out' => 'Zoom out', 'zoom_in' => 'Zoom in', 'loading' => 'Loading process…', 'process_description' => 'Process description', 'process_elements' => 'Process elements', ], 'pl' => [ 'invalid_initial_view' => 'Nieprawidłowy widok początkowy. Użyj width, height lub custom.', 'invalid_mode' => 'Nieprawidłowy tryb prezentacji. Użyj simulation lub animation.', 'invalid_shortcode' => 'Podaj kod procesu albo kod grupy procesów.', 'empty_group' => 'Ta grupa nie zawiera dostępnych procesów.', 'invalid_code' => 'Nieprawidłowy lub brakujący kod procesu.', 'display_error' => 'Proces nie może zostać wyświetlony.', 'start' => 'Start', 'select_start' => 'Wybierz zdarzenie startowe', 'zoom_out' => 'Pomniejsz', 'zoom_in' => 'Powiększ', 'loading' => 'Ładowanie procesu…', 'process_description' => 'Opis procesu', 'process_elements' => 'Elementy procesu', ], 'fr' => [ 'invalid_initial_view' => 'Vue initiale non valide. Utilisez width, height ou custom.', 'invalid_mode' => 'Mode de présentation non valide. Utilisez simulation ou animation.', 'invalid_shortcode' => 'Indiquez soit un code de processus, soit un code de groupe de processus.', 'empty_group' => 'Ce groupe ne contient aucun processus disponible.', 'invalid_code' => 'Code de processus invalide ou manquant.', 'display_error' => 'Le processus n’a pas pu être affiché.', 'start' => 'Démarrer', 'select_start' => 'Sélectionnez un événement de début', 'zoom_out' => 'Zoom arrière', 'zoom_in' => 'Zoom avant', 'loading' => 'Chargement du processus…', 'process_description' => 'Description du processus', 'process_elements' => 'Éléments du processus', ], 'de' => [ 'invalid_initial_view' => 'Ungültige Anfangsansicht. Verwenden Sie width, height oder custom.', 'invalid_mode' => 'Ungültiger Präsentationsmodus. Verwenden Sie simulation oder animation.', 'invalid_shortcode' => 'Geben Sie entweder einen Prozesscode oder einen Prozessgruppencode an.', 'empty_group' => 'Diese Gruppe enthält keine verfügbaren Prozesse.', 'invalid_code' => 'Ungültiger oder fehlender Prozesscode.', 'display_error' => 'Der Prozess konnte nicht angezeigt werden.', 'start' => 'Start', 'select_start' => 'Startereignis auswählen', 'zoom_out' => 'Verkleinern', 'zoom_in' => 'Vergrößern', 'loading' => 'Prozess wird geladen…', 'process_description' => 'Prozessbeschreibung', 'process_elements' => 'Prozesselemente', ], 'es' => [ 'invalid_initial_view' => 'Vista inicial no válida. Use width, height o custom.', 'invalid_mode' => 'Modo de presentación no válido. Use simulation o animation.', 'invalid_shortcode' => 'Indique un código de proceso o un código de grupo de procesos.', 'empty_group' => 'Este grupo no contiene procesos disponibles.', 'invalid_code' => 'Código de proceso no válido o ausente.', 'display_error' => 'No se ha podido mostrar el proceso.', 'start' => 'Iniciar', 'select_start' => 'Seleccione un evento de inicio', 'zoom_out' => 'Alejar', 'zoom_in' => 'Acercar', 'loading' => 'Cargando proceso…', 'process_description' => 'Descripción del proceso', 'process_elements' => 'Elementos del proceso', ], ]; $selected = $labels[$language] ?? $labels['en']; /** * Allow wording to be customised without changing the plugin source. * The filter receives the selected labels and supported language code. */ return apply_filters('inceptus_process_frontend_labels', $selected, $language); } /** * Public storage for published process models. * * /wp-content/uploads/inceptus/processes/{code}/process.json */ function inceptus_process_storage() { $uploads = wp_upload_dir(); return [ 'dir' => trailingslashit($uploads['basedir']) . 'inceptus/processes', 'url' => trailingslashit($uploads['baseurl']) . 'inceptus/processes', ]; } function inceptus_process_ensure_storage() { $storage = inceptus_process_storage(); if (!is_dir($storage['dir'])) { wp_mkdir_p($storage['dir']); } return $storage; } function inceptus_process_activate() { inceptus_process_ensure_storage(); } register_activation_hook(__FILE__, 'inceptus_process_activate'); function inceptus_process_register_assets() { wp_register_style( 'inceptus-process', INCEPTUS_PROCESS_URL . 'assets/css/inceptus-process.css', [], INCEPTUS_PROCESS_VERSION ); wp_register_script( 'inceptus-published-viewer', INCEPTUS_PROCESS_URL . 'assets/js/inceptus.publish.js', [], INCEPTUS_PROCESS_VERSION, true ); wp_register_script( 'inceptus-process-animation', INCEPTUS_PROCESS_URL . 'assets/js/inceptus-process-animation.js', ['inceptus-published-viewer'], INCEPTUS_PROCESS_VERSION, true ); wp_register_script( 'inceptus-process-loader', INCEPTUS_PROCESS_URL . 'assets/js/inceptus-process-loader.js', ['inceptus-published-viewer', 'inceptus-process-animation'], INCEPTUS_PROCESS_VERSION, true ); wp_localize_script('inceptus-published-viewer', 'inceptusProcessLabels', inceptus_process_frontend_labels()); } add_action('wp_enqueue_scripts', 'inceptus_process_register_assets'); /** Load assets before Elementor inserts or refreshes a Shortcode widget. */ function inceptus_process_elementor_preview_assets() { inceptus_process_register_assets(); wp_enqueue_style('inceptus-process'); wp_enqueue_script('inceptus-process-loader'); } add_action('elementor/preview/enqueue_scripts', 'inceptus_process_elementor_preview_assets'); function inceptus_process_code_pattern() { return '/\A[a-z0-9][a-z0-9-]{0,79}\z/'; } function inceptus_process_valid_code($code) { return is_string($code) && (bool) preg_match(inceptus_process_code_pattern(), $code); } /** * process.json is the only supported publication format. */ function inceptus_process_decode_model($raw) { if (!is_string($raw) || trim($raw) === '') { return new WP_Error('empty_model', __('The process.json file is empty.', 'inceptus-process')); } $data = json_decode($raw, true); if (!is_array($data) || json_last_error() !== JSON_ERROR_NONE) { return new WP_Error('invalid_json', __('The file is not valid JSON.', 'inceptus-process')); } if (($data['format'] ?? null) !== 'Inceptus.PublishedProcess') { return new WP_Error('invalid_format', __('Unsupported process format. Expected Inceptus.PublishedProcess.', 'inceptus-process')); } if ((int) ($data['formatVersion'] ?? 0) !== 1) { return new WP_Error('invalid_version', __('Unsupported process format version.', 'inceptus-process')); } $code = $data['publication']['code'] ?? null; if (!inceptus_process_valid_code($code)) { return new WP_Error( 'invalid_publication_code', __('process.json must contain a valid publication.code: lowercase letters a-z, digits and hyphens.', 'inceptus-process') ); } if ( empty($data['presentation']) || !is_array($data['presentation']['nodes'] ?? null) || !is_array($data['presentation']['connectors'] ?? null) || !is_array($data['presentation']['items'] ?? null) || !is_array($data['presentation']['contentBounds'] ?? null) || empty($data['tokenGraph']) || !is_array($data['tokenGraph']['nodes'] ?? null) || empty($data['runtime']) ) { return new WP_Error('invalid_structure', __('The model does not contain the required published process structure.', 'inceptus-process')); } return $data; } function inceptus_process_model_path($code) { if (!inceptus_process_valid_code($code)) { return null; } $storage = inceptus_process_storage(); return trailingslashit($storage['dir']) . $code . '/process.json'; } function inceptus_process_model_url($code) { if (!inceptus_process_valid_code($code)) { return null; } $storage = inceptus_process_storage(); return trailingslashit($storage['url']) . rawurlencode($code) . '/process.json'; } function inceptus_process_load_model($code) { $path = inceptus_process_model_path($code); if (!$path || !is_readable($path)) { // translators: %s: Publication code. return new WP_Error('not_found', sprintf(__('Process "%s" was not found.', 'inceptus-process'), $code)); } $model = inceptus_process_decode_model(file_get_contents($path)); if (is_wp_error($model)) { return $model; } if (($model['publication']['code'] ?? '') !== $code) { return new WP_Error( 'code_mismatch', // translators: %s: Publication directory code. sprintf(__('The code in process.json does not match the process directory "%s".', 'inceptus-process'), $code) ); } return $model; } function inceptus_process_semantic_elements($model) { $elements = []; $seen = []; // presentation.nodes is the canonical publication source for element // labels and descriptions. publication.elements remains supported as a // backwards-compatible fallback for older publication files. if (!empty($model['presentation']['nodes']) && is_array($model['presentation']['nodes'])) { foreach ($model['presentation']['nodes'] as $node) { if (!is_array($node)) { continue; } $label = trim((string) ($node['label'] ?? '')); if ($label === '') { continue; } $id = trim((string) ($node['id'] ?? '')); $key = $id !== '' ? $id : $label; if (isset($seen[$key])) { continue; } $seen[$key] = true; $elements[] = [ 'id' => $id, 'label' => $label, 'description' => trim((string) ($node['description'] ?? '')), ]; } } elseif (!empty($model['publication']['elements']) && is_array($model['publication']['elements'])) { foreach ($model['publication']['elements'] as $element) { if (is_array($element)) { $label = trim((string) ($element['label'] ?? $element['name'] ?? '')); $description = trim((string) ($element['description'] ?? '')); $id = trim((string) ($element['id'] ?? '')); } else { $label = trim((string) $element); $description = ''; $id = ''; } if ($label === '') { continue; } $key = $id !== '' ? $id : $label; if (isset($seen[$key])) { continue; } $seen[$key] = true; $elements[] = [ 'id' => $id, 'label' => $label, 'description' => $description, ]; } } return $elements; } /** * Groups belong to WordPress, never to process.json. Reads tolerate missing or * malformed option data without writing it back or changing publications. */ function inceptus_process_groups() { $stored = get_option(INCEPTUS_PROCESS_GROUPS_OPTION, []); $groups = []; $assigned = []; if (!is_array($stored)) { return $groups; } foreach ($stored as $code => $group) { $code = (string) $code; if (!inceptus_process_valid_code($code) || !is_array($group) || !is_string($group['title'] ?? null)) { continue; } $title = sanitize_text_field($group['title']); if ($title === '') { continue; } $processes = []; foreach (is_array($group['processes'] ?? null) ? $group['processes'] : [] as $process_code) { if (inceptus_process_valid_code($process_code) && !isset($assigned[$process_code])) { $processes[] = $process_code; $assigned[$process_code] = true; } } $groups[$code] = ['title' => $title, 'processes' => $processes]; } return $groups; } function inceptus_process_group_code($value) { if (!is_string($value)) { return ''; } $value = strtolower(trim($value)); $code = sanitize_key($value); // Reject punctuation instead of silently turning it into another group code. return $code === $value && inceptus_process_valid_code($code) ? $code : ''; } function inceptus_process_save_groups($groups) { if (get_option(INCEPTUS_PROCESS_GROUPS_OPTION, []) === $groups) { return true; } if (!update_option(INCEPTUS_PROCESS_GROUPS_OPTION, $groups, false)) { return new WP_Error('groups_save_failed', __('Process groups could not be saved.', 'inceptus-process')); } return true; } function inceptus_process_save_group($code, $title, $create = false) { $title = is_string($title) ? sanitize_text_field($title) : ''; return inceptus_process_store_group_record($code, $title, $create); } /** Internal persistence boundary; callers sanitize the title exactly once per write. */ function inceptus_process_store_group_record($code, $title, $create = false) { $code = inceptus_process_group_code($code); if ($code === '') { return new WP_Error('invalid_group_code', __('Invalid group code. Use 1-80 lowercase letters, digits or hyphens, starting with a letter or digit.', 'inceptus-process')); } if (!is_string($title) || $title === '') { return new WP_Error('invalid_group_title', __('Enter a group title.', 'inceptus-process')); } $groups = inceptus_process_groups(); if ($create && isset($groups[$code])) { return new WP_Error('duplicate_group', __('This group code already exists.', 'inceptus-process')); } if (!$create && !isset($groups[$code])) { return new WP_Error('missing_group', __('Process group not found.', 'inceptus-process')); } $groups[$code] = ['title' => $title, 'processes' => $groups[$code]['processes'] ?? []]; return inceptus_process_save_groups($groups); } function inceptus_process_delete_group($code) { $code = inceptus_process_group_code($code); $groups = inceptus_process_groups(); if ($code === '' || !isset($groups[$code])) { return new WP_Error('missing_group', __('Process group not found.', 'inceptus-process')); } unset($groups[$code]); return inceptus_process_save_groups($groups); } function inceptus_process_groups_without_process($groups, $process_code) { foreach ($groups as &$group) { $group['processes'] = array_values(array_filter( $group['processes'], static fn($code) => $code !== $process_code )); } unset($group); return $groups; } function inceptus_process_assign_group($process_code, $group_code) { if (!inceptus_process_valid_code($process_code)) { return new WP_Error('invalid_code', __('Invalid process code.', 'inceptus-process')); } $model = inceptus_process_load_model($process_code); if (is_wp_error($model)) { return $model; } $groups = inceptus_process_groups(); $normalized_group = inceptus_process_group_code($group_code); if ($group_code !== '' && ($normalized_group === '' || !isset($groups[$normalized_group]))) { return new WP_Error('missing_group', __('Process group not found.', 'inceptus-process')); } $groups = inceptus_process_groups_without_process($groups, $process_code); if ($normalized_group !== '') { $groups[$normalized_group]['processes'][] = $process_code; } return inceptus_process_save_groups($groups); } function inceptus_process_group_members($group_code) { $group_code = inceptus_process_group_code($group_code); $groups = inceptus_process_groups(); $codes = []; foreach ($groups[$group_code]['processes'] ?? [] as $code) { // Loading also verifies JSON structure and publication.code, so a stale, // corrupt or mismatched file cannot participate in the random selection. if (!is_wp_error(inceptus_process_load_model($code))) { $codes[] = $code; } } return $codes; } function inceptus_process_shortcode($atts) { $atts = shortcode_atts( [ 'code' => '', 'group' => '', 'height' => 'auto', 'mode' => 'simulation', 'initial_view' => 'custom', ], $atts, 'inceptus_process' ); $page_language = inceptus_process_page_language(); $labels = inceptus_process_frontend_labels($page_language); if (!is_string($atts['initial_view']) && $atts['initial_view'] !== null) { return '

' . esc_html($labels['invalid_initial_view']) . '

'; } $initial_view = strtolower(trim((string) $atts['initial_view'])); $initial_view = $initial_view === '' ? 'custom' : $initial_view; if (!in_array($initial_view, ['width', 'height', 'custom'], true)) { return '

' . esc_html($labels['invalid_initial_view']) . '

'; } if (!is_string($atts['mode']) && $atts['mode'] !== null) { return '

' . esc_html($labels['invalid_mode']) . '

'; } $mode = strtolower(trim((string) $atts['mode'])); $mode = $mode === '' ? 'simulation' : $mode; if (!in_array($mode, ['simulation', 'animation'], true)) { return '

' . esc_html($labels['invalid_mode']) . '

'; } if ((!is_scalar($atts['code']) && $atts['code'] !== null) || (!is_scalar($atts['group']) && $atts['group'] !== null)) { return '

' . esc_html($labels['invalid_shortcode']) . '

'; } $code = strtolower(trim((string) $atts['code'])); $group = trim((string) $atts['group']); if ($code !== '' && $group !== '') { return '

' . esc_html($labels['invalid_shortcode']) . '

'; } if ($group !== '') { $codes = inceptus_process_group_members($group); if (!$codes) { return '

' . esc_html($labels['empty_group']) . '

'; } $code = $codes[count($codes) === 1 ? 0 : wp_rand(0, count($codes) - 1)]; } if (!inceptus_process_valid_code($code)) { return '

' . esc_html($labels['invalid_code']) . '

'; } $model = inceptus_process_load_model($code); if (is_wp_error($model)) { return '

' . esc_html($labels['display_error']) . '

'; } $height_value = strtolower(trim((string) $atts['height'])); $height_mode = 'auto'; $height = null; if ($height_value !== '' && $height_value !== 'auto' && is_numeric($height_value)) { $height = max(320, min(1400, (int) $height_value)); $height_mode = 'fixed'; } $content_bounds = $initial_view === 'custom' ? ($model['initialView']['visibleRegion'] ?? $model['presentation']['contentBounds'] ?? []) : ($model['presentation']['contentBounds'] ?? []); $content_width = isset($content_bounds['width']) && is_numeric($content_bounds['width']) ? max(1.0, (float) $content_bounds['width']) : 16.0; $content_height = isset($content_bounds['height']) && is_numeric($content_bounds['height']) ? max(1.0, (float) $content_bounds['height']) : 6.0; $aspect_ratio = $content_width . ' / ' . $content_height; $style_parts = [ '--inceptus-process-aspect-ratio: ' . $aspect_ratio, ]; if ($height_mode === 'fixed' && $height !== null) { $style_parts[] = '--inceptus-process-height: ' . $height . 'px'; } $style_attribute = implode('; ', $style_parts) . ';'; $title = trim((string) ($model['publication']['title'] ?? '')); if ($title === '') { $title = trim((string) ($model['source']['documentId'] ?? $code)); } $description = $mode === 'simulation' ? trim((string) ($model['publication']['description'] ?? '')) : ''; $elements = $mode === 'simulation' ? inceptus_process_semantic_elements($model) : []; $model_url = inceptus_process_model_url($code); $path = inceptus_process_model_path($code); $version = ($path && file_exists($path)) ? (string) filemtime($path) : INCEPTUS_PROCESS_VERSION; $model_url = add_query_arg('v', rawurlencode($version), $model_url); wp_enqueue_style('inceptus-process'); wp_enqueue_script('inceptus-published-viewer'); wp_enqueue_script('inceptus-process-loader'); ob_start(); ?>

__('Copied', 'inceptus-process'), 'delete_confirm' => __('Delete process?', 'inceptus-process'), ]); $preview = inceptus_process_preview_request(); if (is_string($preview)) { inceptus_process_register_assets(); wp_enqueue_style('inceptus-process'); wp_enqueue_script('inceptus-published-viewer'); wp_enqueue_script('inceptus-process-loader'); } } add_action('admin_enqueue_scripts', 'inceptus_process_admin_assets'); /** Return a verified preview code, an error, or null for an ordinary admin page. */ function inceptus_process_preview_request() { if (!current_user_can('manage_options')) { return null; } $action = is_string($_GET['action'] ?? null) ? sanitize_key(wp_unslash($_GET['action'])) : ''; if ($action !== 'preview') { return null; } $code = is_string($_GET['code'] ?? null) ? strtolower(sanitize_text_field(wp_unslash($_GET['code']))) : ''; $nonce = is_string($_GET['_inceptus_preview_nonce'] ?? null) ? sanitize_text_field(wp_unslash($_GET['_inceptus_preview_nonce'])) : ''; if (!inceptus_process_valid_code($code) || !wp_verify_nonce($nonce, 'inceptus_process_preview_' . $code)) { return new WP_Error('invalid_preview', __('The process could not be displayed.', 'inceptus-process')); } return $code; } /** * Validate and store a Modeler publication without modifying its contents. * publication.code determines the destination directory. */ function inceptus_process_store_uploaded_model($file) { // PHP's single-file structure must be validated before reading any field. if (!is_array($file) || array_diff(['name', 'type', 'tmp_name', 'error', 'size'], array_keys($file)) || array_diff(array_keys($file), ['name', 'type', 'tmp_name', 'error', 'size', 'full_path']) || !is_string($file['name']) || !is_string($file['type']) || !is_string($file['tmp_name']) || !is_int($file['error']) || !is_int($file['size']) || (array_key_exists('full_path', $file) && !is_string($file['full_path'])) || $file['error'] !== UPLOAD_ERR_OK || $file['size'] <= 0 ) { return new WP_Error('invalid_upload', __('Select a valid process.json file.', 'inceptus-process')); } if (pathinfo($file['name'], PATHINFO_EXTENSION) !== 'json') { return new WP_Error('invalid_extension', __('Only the process.json file is supported.', 'inceptus-process')); } if ($file['name'] !== 'process.json' || (isset($file['full_path']) && $file['full_path'] !== 'process.json') ) { return new WP_Error('invalid_filename', __('The file must be named process.json.', 'inceptus-process')); } $tmp_name = $file['tmp_name']; // Do not sanitize a filesystem path: require PHP's HTTP-upload provenance. if ($tmp_name === '' || str_contains($tmp_name, "\0") || !is_uploaded_file($tmp_name) || !is_file($tmp_name) || !is_readable($tmp_name) ) { return new WP_Error('upload_missing', __('The uploaded file could not be read.', 'inceptus-process')); } clearstatcache(true, $tmp_name); $size = filesize($tmp_name); $limit = wp_max_upload_size(); if (!is_int($size) || $size !== $file['size'] || !is_int($limit) || $limit <= 0 || $size > $limit) { return new WP_Error('invalid_upload_size', __('Select a valid process.json file.', 'inceptus-process')); } // Bound the read by the verified size, then reject incomplete or changed files. $raw = file_get_contents($tmp_name, false, null, 0, $size); clearstatcache(true, $tmp_name); if (!is_string($raw) || strlen($raw) !== $size || filesize($tmp_name) !== $size) { return new WP_Error('upload_read_failed', __('The uploaded file could not be read.', 'inceptus-process')); } $model = inceptus_process_decode_model($raw); if (is_wp_error($model)) { return $model; } $code = $model['publication']['code']; $storage = inceptus_process_ensure_storage(); $process_dir = trailingslashit($storage['dir']) . $code; if (!is_dir($process_dir) && !wp_mkdir_p($process_dir)) { return new WP_Error('mkdir_failed', __('The process directory could not be created.', 'inceptus-process')); } $result = inceptus_process_write_publication($raw, $process_dir); if (is_wp_error($result)) { return $result; } return ['code' => $code, 'path' => trailingslashit($process_dir) . 'process.json']; } /** Store already validated publication bytes. Filesystem move is not atomic and has no rollback. */ function inceptus_process_write_publication($raw, $process_dir) { $target = trailingslashit($process_dir) . 'process.json'; $error = new WP_Error('write_failed', __('process.json could not be saved in the uploads directory.', 'inceptus-process')); // move(..., true) deletes its destination recursively: only a regular file is eligible. clearstatcache(true, $target); if (is_link($target) || (file_exists($target) && !is_file($target))) { return $error; } if (!function_exists('wp_tempnam')) { require_once ABSPATH . 'wp-admin/includes/file.php'; } if (!class_exists('WP_Filesystem_Direct')) { require_once ABSPATH . 'wp-admin/includes/class-wp-filesystem-base.php'; require_once ABSPATH . 'wp-admin/includes/class-wp-filesystem-direct.php'; } $filesystem = new WP_Filesystem_Direct(null); $pending = wp_tempnam('process.json', trailingslashit($process_dir)); $owned_pending = is_string($pending) && !is_link($pending) && realpath(dirname($pending)) === realpath($process_dir); try { if (!$owned_pending || !is_file($pending) || file_put_contents($pending, $raw, LOCK_EX) !== strlen($raw)) { return $error; } clearstatcache(true, $target); if (is_link($target) || (file_exists($target) && !is_file($target)) || !$filesystem->move($pending, $target, true)) { return $error; } clearstatcache(true, $target); if (!is_file($target) || is_link($target) || filesize($target) !== strlen($raw) || hash_file('sha256', $target) !== hash('sha256', $raw) ) { return $error; } } finally { if ($owned_pending && is_file($pending) && !is_link($pending)) { wp_delete_file($pending); } } return true; } function inceptus_process_delete_model($code) { if (!inceptus_process_valid_code($code)) { return new WP_Error('invalid_code', __('Invalid process code.', 'inceptus-process')); } $path = inceptus_process_model_path($code); if (!$path || !is_file($path)) { // translators: %s: Publication code. return new WP_Error('not_found', sprintf(__('Process "%s" was not found.', 'inceptus-process'), $code)); } // Publications live in the local uploads directory. Use the direct WordPress // adapter to retain exact-path deletion without FTP credentials or recursion. if (!class_exists('WP_Filesystem_Direct')) { require_once ABSPATH . 'wp-admin/includes/class-wp-filesystem-base.php'; require_once ABSPATH . 'wp-admin/includes/class-wp-filesystem-direct.php'; } $filesystem = new WP_Filesystem_Direct(null); if (!$filesystem->delete($path, false, 'f')) { return new WP_Error('delete_failed', __('The process.json file could not be deleted.', 'inceptus-process')); } $groups = inceptus_process_groups(); $group_result = inceptus_process_save_groups(inceptus_process_groups_without_process($groups, $code)); $directory = dirname($path); if (is_dir($directory)) { $remaining = array_diff(scandir($directory) ?: [], ['.', '..']); if (empty($remaining)) { $filesystem->delete($directory, false, 'd'); } } return $group_result; } function inceptus_process_existing_models() { $storage = inceptus_process_ensure_storage(); $files = glob(trailingslashit($storage['dir']) . '*/process.json') ?: []; $models = []; foreach ($files as $file) { $code = basename(dirname($file)); if (!inceptus_process_valid_code($code)) { continue; } $model = inceptus_process_decode_model(file_get_contents($file)); if (is_wp_error($model) || ($model['publication']['code'] ?? '') !== $code) { continue; } $models[] = [ 'code' => $code, 'title' => (string) ($model['publication']['title'] ?? ''), 'revision' => (string) ($model['source']['revision'] ?? ''), 'modified' => filemtime($file), ]; } usort($models, static fn($a, $b) => strcmp($a['code'], $b['code'])); return $models; } function inceptus_process_admin_page_url($args = []) { return add_query_arg( array_merge(['page' => 'inceptus-processes'], $args), admin_url('tools.php') ); } function inceptus_process_admin_page() { if (!current_user_can('manage_options')) { return; } $notice = null; $notice_class = 'notice-success'; if (isset($_POST['inceptus_process_group_action'])) { $group_action = is_string($_POST['inceptus_process_group_action']) ? filter_var(wp_unslash($_POST['inceptus_process_group_action']), FILTER_VALIDATE_REGEXP, ['options' => ['regexp' => '/\A(?:create|update|delete|assign)\z/']]) : false; $group_notices = [ 'create' => __('Process group created.', 'inceptus-process'), 'update' => __('Process group updated.', 'inceptus-process'), 'delete' => __('Process group deleted.', 'inceptus-process'), 'assign' => __('Process group assignment updated.', 'inceptus-process'), ]; if ($group_action === false || !isset($group_notices[$group_action])) { $notice = __('Invalid process group action.', 'inceptus-process'); $notice_class = 'notice-error'; } else { check_admin_referer('inceptus_process_group_' . $group_action); // Missing/empty means unassign; malformed nonempty input must never become empty. $group_code = ''; if (isset($_POST['group_code']) && $_POST['group_code'] !== '') { $group_code = is_string($_POST['group_code']) ? filter_var(wp_unslash($_POST['group_code']), FILTER_VALIDATE_REGEXP, // Accept exactly the existing trim/case normalization; helpers apply it. ['options' => ['regexp' => '/\A[ \t\n\r\0\x0B]*[a-z0-9][a-z0-9-]{0,79}[ \t\n\r\0\x0B]*\z/i']]) : false; } if ($group_code === false) { $result = new WP_Error('invalid_group_code', __('Invalid group code. Use 1-80 lowercase letters, digits or hyphens, starting with a letter or digit.', 'inceptus-process')); } elseif ($group_action === 'assign') { $process_code = is_string($_POST['process_code'] ?? null) ? filter_var(wp_unslash($_POST['process_code']), FILTER_VALIDATE_REGEXP, ['options' => ['regexp' => inceptus_process_code_pattern()]]) : false; $result = inceptus_process_assign_group($process_code, $group_code); } elseif ($group_action === 'delete') { $result = inceptus_process_delete_group($group_code); } else { $title = is_string($_POST['group_title'] ?? null) ? sanitize_text_field(wp_unslash($_POST['group_title'])) : ''; $result = inceptus_process_store_group_record($group_code, $title, $group_action === 'create'); } if (is_wp_error($result)) { $notice = $result->get_error_message(); $notice_class = 'notice-error'; } else { $notice = $group_notices[$group_action]; } } } elseif (isset($_POST['inceptus_process_delete'])) { $code = strtolower(sanitize_text_field(wp_unslash($_POST['process_code'] ?? ''))); if (!inceptus_process_valid_code($code)) { $notice = __('Invalid process code.', 'inceptus-process'); $notice_class = 'notice-error'; } else { check_admin_referer('inceptus_process_delete_' . $code); $result = inceptus_process_delete_model($code); if (is_wp_error($result)) { $notice = $result->get_error_message(); $notice_class = 'notice-error'; } else { // translators: %s: Deleted publication code. $notice = sprintf(__('Process "%s" was deleted.', 'inceptus-process'), $code); } } } elseif (isset($_POST['inceptus_process_upload'])) { check_admin_referer('inceptus_process_upload_model'); $result = inceptus_process_store_uploaded_model($_FILES['process_file'] ?? null); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- This boundary validates the complete upload structure, exact name, PHP provenance, actual size and JSON before storage. if (is_wp_error($result)) { $notice = $result->get_error_message(); $notice_class = 'notice-error'; } else { $notice = sprintf( /* translators: 1: Saved publication code, 2: The same code in its shortcode. */ __('Process "%1$s" was saved. Shortcode: [inceptus_process code="%2$s"]', 'inceptus-process'), $result['code'], $result['code'] ); } } $models = inceptus_process_existing_models(); $groups = inceptus_process_groups(); $model_codes = array_column($models, 'code'); $assignments = []; foreach ($groups as $group_code => $group) { foreach ($group['processes'] as $process_code) { $assignments[$process_code] = (string) $group_code; } } $preview = inceptus_process_preview_request(); $preview_code = is_string($preview) ? $preview : ''; $preview_model = is_string($preview) ? inceptus_process_load_model($preview) : $preview; // Use the admin/user locale, independently of the public page language. $editor_url = add_query_arg('lang', inceptus_process_interface_language(), 'https://bpmn-editor.inceptus.online/'); ?>

process.json file.', 'inceptus-process')); ?>

/wp-content/uploads/inceptus/processes/{code}/process.json.', 'inceptus-process')); ?>

publication.code. The title, description and revision also come from process.json and are not edited in WordPress.', 'inceptus-process')); ?>

· ·

get_error_message()); ?>

$group) : ?>

'preview', 'code' => $item['code'], '_inceptus_preview_nonce' => wp_create_nonce('inceptus_process_preview_' . $item['code']), ]); ?>

Elementor

Shortcode widget and use the publication code, e.g. [inceptus_process code="kompletacja-zamowienia"].', 'inceptus-process')); ?>

initial_view="width", initial_view="height" or initial_view="custom" (default, saved diagram parameters).', 'inceptus-process')); ?>

[inceptus_process code="kompletacja-zamowienia" height="800"].', 'inceptus-process')); ?>