get('dfg_3dviewer_main_url') ?? $config->get('main_url') )) ?: 'https://3d-repository.hs-mainz.de'; $json_export_base_url = trim((string) ( $config->get('dfg_3dviewer_json_export_base_url') ?? $config->get('json_export_base_url') )) ?: $main_url; $entity_bundle = trim((string) ( $config->get('dfg_3dviewer_entitybundle') ?? $config->get('entitybundle') )) ?: 'bd3d7baa74856d141bcff7b4193fa128'; $viewer_file_upload = trim((string) ( $config->get('dfg_3dviewer_viewer_file_upload') ?? $config->get('viewer_file_upload') )) ?: 'fad29437cb2a561b91b26aca5dbb7c42'; $viewer_file_name = trim((string) ( $config->get('dfg_3dviewer_viewer_file_name') ?? $config->get('viewer_file_name') )) ?: 'fb76901eb219495fee0512b5cdfdaa18'; $api_3d_file_field = trim((string) ( $config->get('dfg_3dviewer_api_3d_file_field') ?? $config->get('api_3d_file_field') )); $image_generation = trim((string) ( $config->get('dfg_3dviewer_image_generation') ?? $config->get('image_generation') )) ?: 'f605dc6b727a1099b9e52b3ccbdf5673'; $field_df = trim((string) ( $config->get('dfg_3dviewer_field_df') ?? $config->get('field_df') )) ?: 'field_df'; $export_viewer = trim((string) ( $config->get('dfg_3dviewer_export_viewer') ?? $config->get('export_viewer') )) ?: 'export_viewer'; $export_viewer_url = trim((string) ( $config->get('dfg_3dviewer_export_viewer_url') ?? $config->get('export_viewer_url') )) ?: ''; $lightweight = filter_var( $config->get('dfg_3dviewer_lightweight') ?? $config->get('lightweight') ?? false, FILTER_VALIDATE_BOOLEAN ); $gallery_image_class = trim((string) ( $config->get('dfg_3dviewer_gallery_image_class') ?? $config->get('gallery_image_class') )) ?: 'field--name-fd6a974b7120d422c7b21b5f1f2315d9'; $cfg = [ 'main_url' => $main_url, 'json_export_base_url' => $json_export_base_url, 'entity_bundle' => $entity_bundle, 'viewer_file_upload' => $viewer_file_upload, 'viewer_file_name' => $viewer_file_name, 'api_3d_file_field' => $api_3d_file_field, 'image_generation' => $image_generation, 'field_df' => $field_df, 'lightweight' => $lightweight, 'gallery_image_class' => $gallery_image_class, 'export_viewer' => $export_viewer, 'export_viewer_url' => $export_viewer_url, ]; } return $cfg; } function preload() { $cfg = dfg_3dviewer_config(); if (isset($cfg)) { foreach ($cfg as $key => $value) { if (!defined($key)) { define($key, $value); } } } else { \Drupal::messenger()->addMessage("Cant load settings file", 'warning'); } } function dfg_3dviewer_init_constants(): void { static $done = false; if ($done) { return; } $cfg = dfg_3dviewer_config(); foreach ($cfg as $key => $value) { $const = 'DFG_3DVIEWER_' . strtoupper($key); if (!defined($const)) { define($const, $value); } } $done = true; } /** * Returns the queue runner log path. */ function dfg_3dviewer_queue_runner_log_path(): string { return '/opt/drupal/dfg3dworker.log'; } /** * Returns the persistent worker PID file path. */ function dfg_3dviewer_queue_runner_pid_path(): string { return '/opt/drupal/dfg3dworker.pid'; } /** * Checks whether a persistent queue worker loop is already running. */ function dfg_3dviewer_has_persistent_convert_worker(): bool { $pid_file = dfg_3dviewer_queue_runner_pid_path(); if (!is_file($pid_file) || !is_readable($pid_file)) { return FALSE; } $pid = trim((string) @file_get_contents($pid_file)); if ($pid === '' || !ctype_digit($pid)) { @unlink($pid_file); return FALSE; } $pid_int = (int) $pid; if ($pid_int <= 0) { @unlink($pid_file); return FALSE; } $is_running = FALSE; if (function_exists('posix_kill')) { $is_running = @posix_kill($pid_int, 0); } else { $is_running = is_dir('/proc/' . $pid); } if (!$is_running) { @unlink($pid_file); return FALSE; } return TRUE; } /** * Builds a detached drush queue runner command with line timestamps. */ function dfg_3dviewer_build_timestamped_queue_runner_command(array $arguments, string $runner_log): string { $escaped = array_map('escapeshellarg', $arguments); $drush_command = implode(' ', $escaped); $awk_script = "awk '{ print strftime(\"%Y-%m-%d %H:%M:%S\"), \$0; fflush(); }'"; return sprintf( '{ %s 2>&1 | %s >> %s; } &', $drush_command, $awk_script, escapeshellarg($runner_log) ); } /** * Starts a background queue runner in a non-blocking way. */ function dfg_3dviewer_kick_convert_worker(): void { \Drupal::logger('dfg_3dviewer')->notice('dfg_3dviewer_kick_convert_worker() invoked.'); if (!empty($GLOBALS['dfg_3dviewer_worker_running'])) { return; } $lock = \Drupal::lock(); $lock_name = 'dfg_3dviewer_convert_runner_kick'; if (!$lock->acquire($lock_name, 30.0)) { \Drupal::logger('dfg_3dviewer')->notice('Skip queue runner kick: lock "@lock" is already held.', ['@lock' => $lock_name]); return; } try { $queue = \Drupal::service('queue')->get('dfg_3dviewer_convert'); $queue_size = method_exists($queue, 'numberOfItems') ? (int) $queue->numberOfItems() : -1; \Drupal::logger('dfg_3dviewer')->notice( 'Queue runner kick requested. Queue size before start: @size', ['@size' => (string) $queue_size] ); if (dfg_3dviewer_has_persistent_convert_worker()) { \Drupal::logger('dfg_3dviewer')->notice( 'Skip queue runner kick: persistent worker loop is already active (pid file: @pid_file).', ['@pid_file' => dfg_3dviewer_queue_runner_pid_path()] ); return; } $drush = dfg_3dviewer_find_drush_binary(); if ($drush === '') { \Drupal::logger('dfg_3dviewer')->warning('Cannot kick queue runner: drush binary not found.'); return; } $arguments = [$drush]; $site_uri = ''; $site_uri_candidates = []; try { $request = \Drupal::service('request_stack')->getCurrentRequest(); if ($request) { $site_uri_candidates[] = $request->getSchemeAndHttpHost(); } } catch (\Throwable $e) { // Fall back to the configured canonical URL below. } $site_uri_candidates[] = trim((string) (dfg_3dviewer_config()['main_url'] ?? '')); foreach ($site_uri_candidates as $candidate) { $candidate = trim((string) $candidate); $candidate_parts = parse_url($candidate); $candidate_host = is_array($candidate_parts) ? (string) ($candidate_parts['host'] ?? '') : ''; if (is_array($candidate_parts) && !empty($candidate_parts['scheme']) && $candidate_host !== '' && strpos($candidate_host, '_') === FALSE && strtolower($candidate_host) !== 'default') { $site_uri = rtrim($candidate, '/'); break; } } if ($site_uri !== '') { $arguments[] = '--uri=' . $site_uri; } else { \Drupal::logger('dfg_3dviewer')->warning( 'Queue runner starts without --uri because neither the current request nor configured main URL is a safe absolute URL.' ); } $arguments = array_merge($arguments, [ 'queue:run', 'dfg_3dviewer_convert', '--time-limit=30', ]); // Using Process::start() here is unreliable because the Process object // is destroyed at the end of this function and may terminate the child. // Spawn a detached shell command instead. $runner_log = dfg_3dviewer_queue_runner_log_path(); $command = dfg_3dviewer_build_timestamped_queue_runner_command($arguments, $runner_log); // Drush may fail in web/PHP contexts when HOME is missing. $home = (string) getenv('HOME'); if ($home === '') { $home = '/tmp'; } $composer_home = (string) getenv('COMPOSER_HOME'); if ($composer_home === '') { $composer_home = rtrim($home, '/') . '/.composer'; } $xdg_config_home = (string) getenv('XDG_CONFIG_HOME'); if ($xdg_config_home === '') { $xdg_config_home = rtrim($home, '/') . '/.config'; } $env = [ 'HOME' => $home, 'COMPOSER_HOME' => $composer_home, 'XDG_CONFIG_HOME' => $xdg_config_home, ]; $process = Process::fromShellCommandline($command, DRUPAL_ROOT, $env); $process->disableOutput(); $process->run(); \Drupal::logger('dfg_3dviewer')->notice( 'Triggered background queue runner for dfg_3dviewer_convert (log: @log).', ['@log' => $runner_log] ); } catch (\Throwable $e) { \Drupal::logger('dfg_3dviewer')->warning( 'Failed to trigger background queue runner: @msg', ['@msg' => $e->getMessage()] ); } finally { $lock->release($lock_name); } } /** * Finds a usable drush executable path. */ function dfg_3dviewer_find_drush_binary(): string { $candidates = [ DRUPAL_ROOT . '/vendor/bin/drush', DRUPAL_ROOT . '/vendor/bin/drush.bat', DRUPAL_ROOT . '/../vendor/bin/drush', DRUPAL_ROOT . '/../vendor/bin/drush.bat', ]; foreach ($candidates as $candidate) { if (is_file($candidate)) { return $candidate; } } return ''; } /** * Tries to resolve file ID from entity field values. */ function dfg_3dviewer_extract_file_id(array $file_info): ?int { $first = $file_info[0] ?? NULL; if (!is_array($first)) { return NULL; } foreach (['target_id', 'fid', 'id', 'value', 'original_target_id', 'uri'] as $key) { if (!array_key_exists($key, $first)) { continue; } $candidate = (string) $first[$key]; if ($candidate !== '' && ctype_digit($candidate)) { return (int) $candidate; } if ($candidate !== '') { $mapped = dfg_3dviewer_file_id_from_location($candidate); if ($mapped !== NULL) { return $mapped; } } } return NULL; } /** * Maps URL/public path to a file entity ID when possible. */ function dfg_3dviewer_file_id_from_location(string $location): ?int { $uri = dfg_3dviewer_location_to_public_uri($location); if ($uri === NULL) { return NULL; } try { $storage = \Drupal::entityTypeManager()->getStorage('file'); $existing = $storage->loadByProperties(['uri' => $uri]); if (!empty($existing)) { $file = reset($existing); return $file ? (int) $file->id() : NULL; } } catch (\Throwable $e) { return NULL; } return NULL; } /** * Converts full URL or public path into public:// URI. */ function dfg_3dviewer_location_to_public_uri(string $location): ?string { $location = trim($location); if ($location === '') { return NULL; } if (str_starts_with($location, 'public://')) { return $location; } if (preg_match('#^[a-z][a-z0-9+.-]*://#i', $location)) { $path = parse_url($location, PHP_URL_PATH); if (!is_string($path) || $path === '') { return NULL; } $location = $path; } $location = ltrim($location, '/'); if (str_starts_with($location, 'sites/default/files/')) { return 'public://' . substr($location, strlen('sites/default/files/')); } return NULL; } /** * Tries to detect a non-empty file reference field on entity. * * @return array{field_name:string,file_id:int}|null */ function dfg_3dviewer_detect_file_field_fallback(Drupal\Core\Entity\EntityInterface $entity): ?array { if (!method_exists($entity, 'getFields')) { return NULL; } foreach ($entity->getFields() as $field_name => $field_item_list) { try { if ($field_item_list->isEmpty()) { continue; } $definition = $field_item_list->getFieldDefinition(); $storage_definition = $definition ? $definition->getFieldStorageDefinition() : NULL; if (!$storage_definition) { continue; } $field_type = (string) $storage_definition->getType(); if (!in_array($field_type, ['entity_reference', 'file'], TRUE)) { continue; } $settings = $definition->getSettings(); $target_type = (string) ($settings['target_type'] ?? ''); if ($target_type !== '' && $target_type !== 'file') { continue; } $file_info = $field_item_list->getValue(); $file_id = dfg_3dviewer_extract_file_id($file_info); if ($file_id !== NULL) { return [ 'field_name' => (string) $field_name, 'file_id' => $file_id, ]; } } catch (\Throwable $e) { continue; } } return NULL; } /** * Tries to resolve file ID from a field on base/untranslated/translated entity. */ function dfg_3dviewer_extract_file_id_from_entity_field( Drupal\Core\Entity\EntityInterface $entity, string $field_name ): ?int { if ($field_name === '' || !method_exists($entity, 'hasField') || !$entity->hasField($field_name)) { return NULL; } $file_info = $entity->{$field_name}->getValue(); $file_id = dfg_3dviewer_extract_file_id($file_info); if ($file_id !== NULL) { return $file_id; } if (method_exists($entity, 'getUntranslated')) { try { $untranslated = $entity->getUntranslated(); if ($untranslated && method_exists($untranslated, 'hasField') && $untranslated->hasField($field_name)) { $file_info = $untranslated->{$field_name}->getValue(); $file_id = dfg_3dviewer_extract_file_id($file_info); if ($file_id !== NULL) { return $file_id; } } } catch (\Throwable $e) { // Ignore and continue. } } if (method_exists($entity, 'getTranslationLanguages') && method_exists($entity, 'hasTranslation') && method_exists($entity, 'getTranslation')) { try { foreach ($entity->getTranslationLanguages() as $langcode => $language) { if (!method_exists($entity, 'hasTranslation') || !$entity->hasTranslation($langcode)) { continue; } $translation = $entity->getTranslation($langcode); if (!method_exists($translation, 'hasField') || !$translation->hasField($field_name)) { continue; } $file_info = $translation->{$field_name}->getValue(); $file_id = dfg_3dviewer_extract_file_id($file_info); if ($file_id !== NULL) { return $file_id; } } } catch (\Throwable $e) { // Ignore and return NULL below. } } if (!empty($entity->original) && method_exists($entity->original, 'hasField') && $entity->original->hasField($field_name)) { try { $file_info = $entity->original->{$field_name}->getValue(); $file_id = dfg_3dviewer_extract_file_id($file_info); if ($file_id !== NULL) { return $file_id; } } catch (\Throwable $e) { // Ignore and return NULL below. } } return NULL; } /** * Resolves current file reference for entity from configured field or fallback. * * @return array{field_name:string,file_id:int,configured_field:string,used_fallback:bool,configured_raw:array} */ function dfg_3dviewer_resolve_current_file_reference( Drupal\Core\Entity\EntityInterface $entity, string $configured_field ): ?array { $configured_raw = []; if ($configured_field !== '' && method_exists($entity, 'hasField') && $entity->hasField($configured_field)) { $file_info = $entity->{$configured_field}->getValue(); $configured_raw = is_array($file_info[0] ?? NULL) ? $file_info[0] : []; $file_id = dfg_3dviewer_extract_file_id_from_entity_field($entity, $configured_field); if ($file_id !== NULL) { return [ 'field_name' => $configured_field, 'file_id' => $file_id, 'configured_field' => $configured_field, 'used_fallback' => FALSE, 'configured_raw' => $configured_raw, ]; } } $fallback = dfg_3dviewer_detect_file_field_fallback($entity); if ($fallback !== NULL) { return [ 'field_name' => (string) $fallback['field_name'], 'file_id' => (int) $fallback['file_id'], 'configured_field' => $configured_field, 'used_fallback' => TRUE, 'configured_raw' => $configured_raw, ]; } return NULL; } /** * Resolves previous file ID from original entity by selected field. */ function dfg_3dviewer_resolve_previous_file_id( Drupal\Core\Entity\EntityInterface $entity, string $field_name ): ?int { if (empty($entity->original) || !method_exists($entity->original, 'hasField') || !$entity->original->hasField($field_name)) { return NULL; } $old = $entity->original->{$field_name}->getValue(); return dfg_3dviewer_extract_file_id($old); } /** * Stores last resolved file ID per entity for fallback re-queueing. */ function dfg_3dviewer_store_last_file_id(string $entity_type, string $entity_id, int $file_id): void { if ($entity_type === '' || $entity_id === '' || $file_id <= 0) { return; } $key = 'dfg_3dviewer.last_file_id.' . $entity_type . '.' . $entity_id; \Drupal::state()->set($key, $file_id); } /** * Loads last resolved file ID per entity. */ function dfg_3dviewer_load_last_file_id(string $entity_type, string $entity_id): ?int { if ($entity_type === '' || $entity_id === '') { return NULL; } $key = 'dfg_3dviewer.last_file_id.' . $entity_type . '.' . $entity_id; $value = \Drupal::state()->get($key); if ($value === NULL) { return NULL; } $candidate = (string) $value; return ($candidate !== '' && ctype_digit($candidate)) ? (int) $candidate : NULL; } /** * Implements hook_help(). */ function dfg_3dviewer_help($route_name, RouteMatchInterface $route_match) { switch ($route_name) { case 'help.page.wisski_iip_image': return '

' . t('This is the WissKI module for the integration ' . 'of IIP (https://iipimage.sourceforge.io/).') . '

'; } } function in_arrayi($needle, $haystack) { return in_array(strtolower($needle), array_map('strtolower', $haystack)); } function array_searchi($needle, $haystack) { return array_search(strtolower($needle), array_map('strtolower', $haystack)); } function url_exists(string $url): bool { $ch = curl_init($url); curl_setopt_array($ch, [ CURLOPT_NOBODY => true, CURLOPT_FOLLOWLOCATION => true, CURLOPT_TIMEOUT => 5, CURLOPT_SSL_VERIFYPEER => true, CURLOPT_RETURNTRANSFER => true, ]); curl_exec($ch); $code = curl_getinfo($ch, CURLINFO_HTTP_CODE); curl_close($ch); return $code >= 200 && $code < 400; } function automatic_helper (&$autoPath, $prefix, $filename, $value) { $autoPath = $prefix . "/" . $filename . "." . $value; if(!file_exists($autoPath)) { $autoPath = ''; } else { return; } } /** * Implements hook_file_validate(). */ function dfg_3dviewer_hook_file_validate(Drupal\file\FileInterface $file) { $errors = []; if (!is_array($filename)) { $filename = $file->getFilename(); if (!$filename) { $errors[] = t("The file's name is empty. Give a name to the file."); } if (strlen($filename) > 255) { $errors[] = t("The file's name exceeds the 255 characters limit. Rename the file and try again."); } if (preg_match('/\s/', $filename)) { $errors[] = t("Whitespaces are not allowed in filenames"); } } return $errors; } /** * Implements hook_entity_presave(). */ function dfg_3dviewer_entity_presave(Drupal\Core\Entity\EntityInterface $entity) { if (!empty($GLOBALS['dfg_3dviewer_worker_running'])) { return; } $cfg = dfg_3dviewer_config(); if (empty($cfg) || $entity->bundle() !== $cfg['entity_bundle']) { return; } \Drupal::logger('dfg_3dviewer')->notice( 'entity_presave matched target entity: type=@type bundle=@bundle id=@id', [ '@type' => $entity->getEntityTypeId(), '@bundle' => (string) $entity->bundle(), '@id' => (string) ($entity->id() ?? ''), ] ); $configured_raw_first = []; if (method_exists($entity, 'hasField') && $entity->hasField((string) $cfg['viewer_file_upload'])) { $configured_values = $entity->{(string) $cfg['viewer_file_upload']}->getValue(); $configured_raw_first = is_array($configured_values[0] ?? NULL) ? $configured_values[0] : []; } $resolved = dfg_3dviewer_resolve_current_file_reference($entity, (string) $cfg['viewer_file_upload']); if ($resolved === NULL) { \Drupal::logger('dfg_3dviewer')->notice( 'entity_presave skip: upload field "@field" has no file ID in first item on entity @entity_id. Raw value: @raw', [ '@field' => $cfg['viewer_file_upload'], '@entity_id' => (string) ($entity->id() ?? ''), '@raw' => json_encode($configured_raw_first, JSON_UNESCAPED_SLASHES), ] ); return; } $file_id = (int) $resolved['file_id']; $source_field = (string) $resolved['field_name']; if (!empty($resolved['used_fallback'])) { \Drupal::logger('dfg_3dviewer')->notice( 'entity_presave fallback: configured upload field "@field" is empty; using "@fallback_field" with file_id=@file_id for entity @entity_id. Configured raw: @raw', [ '@field' => (string) $resolved['configured_field'], '@fallback_field' => $source_field, '@file_id' => (string) $file_id, '@entity_id' => (string) ($entity->id() ?? ''), '@raw' => json_encode($resolved['configured_raw'], JSON_UNESCAPED_SLASHES), ] ); } $entity_id = $entity->id(); $uploaded_file = \Drupal\file\Entity\File::load($file_id); if ($uploaded_file) { \Drupal::logger('dfg_3dviewer')->notice( 'Upload source field "@field" on entity @entity_id resolved to file_id=@file_id, filename="@filename", uri="@uri".', [ '@field' => $source_field, '@entity_id' => (string) ($entity_id ?? ''), '@file_id' => $file_id, '@filename' => $uploaded_file->getFilename(), '@uri' => $uploaded_file->getFileUri(), ] ); } else { \Drupal::logger('dfg_3dviewer')->warning( 'Upload source field "@field" on entity @entity_id points to missing file_id=@file_id.', [ '@field' => $source_field, '@entity_id' => (string) ($entity_id ?? ''), '@file_id' => $file_id, ] ); } if (empty($entity_id) && $entity->hasField('eid')) { $entity_id = $entity->get('eid')->value; } \Drupal::logger('dfg_3dviewer')->notice( 'entity_presave force requeue on save: entity @entity_id file @file_id.', [ '@entity_id' => (string) ($entity_id ?? $entity->id() ?? ''), '@file_id' => (string) $file_id, ] ); if ($entity->hasField('field_processing_progress')) { $entity->set('field_processing_progress', 0); } if ($entity->hasField('field_processing_status')) { $entity->set('field_processing_status', 'queued'); } if ($entity->hasField('field_processing_message')) { $entity->set('field_processing_message', 'Queued for conversion'); } $source_filename = $uploaded_file ? $uploaded_file->getFilename() : ''; $source_uri = $uploaded_file ? $uploaded_file->getFileUri() : ''; if (empty($entity_id)) { \Drupal::logger('dfg_3dviewer')->notice( 'Entity ID is not available in presave; enqueue deferred to entity_insert for file @file_id.', [ '@file_id' => $file_id, ] ); return; } try { \Drupal::service('queue')->get('dfg_3dviewer_convert')->createItem([ 'entity_type' => $entity->getEntityTypeId(), 'entity_id' => $entity_id, 'file_id' => $file_id, 'source_filename' => $source_filename, 'source_uri' => $source_uri, ]); dfg_3dviewer_store_last_file_id((string) $entity->getEntityTypeId(), (string) $entity_id, (int) $file_id); \Drupal::logger('dfg_3dviewer')->notice( 'entity_presave enqueue ok: entity @entity_id file @file_id.', [ '@entity_id' => (string) $entity_id, '@file_id' => (string) $file_id, ] ); dfg_3dviewer_kick_convert_worker(); \Drupal::logger('dfg_3dviewer')->notice( 'Queued conversion for entity @entity_id and file @file_id.', [ '@entity_id' => $entity_id, '@file_id' => $file_id, ] ); } catch (\Throwable $e) { \Drupal::logger('dfg_3dviewer')->error( 'Failed to enqueue conversion in presave for entity @entity_id and file @file_id: @msg', [ '@entity_id' => $entity_id, '@file_id' => $file_id, '@msg' => $e->getMessage(), ] ); } $key = $entity->getEntityTypeId() . ':' . $entity_id . ':' . $file_id; $GLOBALS['dfg_3dviewer_enqueued_in_presave'][$key] = TRUE; } /** * Implements hook_entity_insert(). */ function dfg_3dviewer_entity_insert(Drupal\Core\Entity\EntityInterface $entity) { if (!empty($GLOBALS['dfg_3dviewer_worker_running'])) { return; } $cfg = dfg_3dviewer_config(); if (empty($cfg) || $entity->bundle() !== $cfg['entity_bundle']) { return; } \Drupal::logger('dfg_3dviewer')->notice( 'entity_insert matched target entity: type=@type bundle=@bundle id=@id', [ '@type' => $entity->getEntityTypeId(), '@bundle' => (string) $entity->bundle(), '@id' => (string) ($entity->id() ?? ''), ] ); $status = $entity->hasField('field_processing_status') ? (string) $entity->get('field_processing_status')->value : ''; if ($status !== 'queued') { \Drupal::logger('dfg_3dviewer')->notice( 'entity_insert skip: processing status is "@status" (expected "queued") for entity @entity_id.', [ '@status' => $status, '@entity_id' => (string) ($entity->id() ?? ''), ] ); return; } $configured_raw_first = []; if (method_exists($entity, 'hasField') && $entity->hasField((string) $cfg['viewer_file_upload'])) { $configured_values = $entity->{(string) $cfg['viewer_file_upload']}->getValue(); $configured_raw_first = is_array($configured_values[0] ?? NULL) ? $configured_values[0] : []; } $resolved = dfg_3dviewer_resolve_current_file_reference($entity, (string) $cfg['viewer_file_upload']); if ($resolved === NULL) { \Drupal::logger('dfg_3dviewer')->notice( 'entity_insert skip: upload field "@field" has no file ID in first item on entity @entity_id. Raw value: @raw', [ '@field' => $cfg['viewer_file_upload'], '@entity_id' => (string) ($entity->id() ?? ''), '@raw' => json_encode($configured_raw_first, JSON_UNESCAPED_SLASHES), ] ); return; } $file_id = (int) $resolved['file_id']; if (!empty($resolved['used_fallback'])) { \Drupal::logger('dfg_3dviewer')->notice( 'entity_insert fallback: configured upload field "@field" is empty; using "@fallback_field" with file_id=@file_id for entity @entity_id. Configured raw: @raw', [ '@field' => (string) $resolved['configured_field'], '@fallback_field' => (string) $resolved['field_name'], '@file_id' => (string) $file_id, '@entity_id' => (string) ($entity->id() ?? ''), '@raw' => json_encode($resolved['configured_raw'], JSON_UNESCAPED_SLASHES), ] ); } $entity_id = $entity->id(); if (empty($entity_id) && $entity->hasField('eid')) { $entity_id = $entity->get('eid')->value; } if (empty($entity_id)) { \Drupal::logger('dfg_3dviewer')->warning('Cannot enqueue conversion on insert: missing entity ID.'); return; } $key = $entity->getEntityTypeId() . ':' . $entity_id . ':' . $file_id; if (!empty($GLOBALS['dfg_3dviewer_enqueued_in_presave'][$key])) { return; } $uploaded_file = \Drupal\file\Entity\File::load($file_id); $source_filename = $uploaded_file ? $uploaded_file->getFilename() : ''; $source_uri = $uploaded_file ? $uploaded_file->getFileUri() : ''; try { \Drupal::service('queue')->get('dfg_3dviewer_convert')->createItem([ 'entity_type' => $entity->getEntityTypeId(), 'entity_id' => $entity_id, 'file_id' => $file_id, 'source_filename' => $source_filename, 'source_uri' => $source_uri, ]); dfg_3dviewer_store_last_file_id((string) $entity->getEntityTypeId(), (string) $entity_id, (int) $file_id); dfg_3dviewer_kick_convert_worker(); \Drupal::logger('dfg_3dviewer')->notice( 'Queued conversion on insert for entity @entity_id and file @file_id.', [ '@entity_id' => $entity_id, '@file_id' => $file_id, ] ); } catch (\Throwable $e) { \Drupal::logger('dfg_3dviewer')->error( 'Failed to enqueue conversion on insert for entity @entity_id and file @file_id: @msg', [ '@entity_id' => $entity_id, '@file_id' => $file_id, '@msg' => $e->getMessage(), ] ); } } /** * Implements hook_entity_update(). */ function dfg_3dviewer_entity_update(Drupal\Core\Entity\EntityInterface $entity) { if (!empty($GLOBALS['dfg_3dviewer_worker_running'])) { return; } $cfg = dfg_3dviewer_config(); if (empty($cfg) || $entity->bundle() !== $cfg['entity_bundle']) { return; } \Drupal::logger('dfg_3dviewer')->notice( 'entity_update matched target entity: type=@type bundle=@bundle id=@id', [ '@type' => $entity->getEntityTypeId(), '@bundle' => (string) $entity->bundle(), '@id' => (string) ($entity->id() ?? ''), ] ); $configured_raw_first = []; if (method_exists($entity, 'hasField') && $entity->hasField((string) $cfg['viewer_file_upload'])) { $configured_values = $entity->{(string) $cfg['viewer_file_upload']}->getValue(); $configured_raw_first = is_array($configured_values[0] ?? NULL) ? $configured_values[0] : []; } $resolved = dfg_3dviewer_resolve_current_file_reference($entity, (string) $cfg['viewer_file_upload']); if ($resolved === NULL) { \Drupal::logger('dfg_3dviewer')->notice( 'entity_update skip: upload field "@field" has no file ID in first item on entity @entity_id. Raw value: @raw', [ '@field' => $cfg['viewer_file_upload'], '@entity_id' => (string) ($entity->id() ?? ''), '@raw' => json_encode($configured_raw_first, JSON_UNESCAPED_SLASHES), ] ); $entity_id_fallback = $entity->id(); $entity_type_fallback = $entity->getEntityTypeId(); if (!empty($entity_id_fallback)) { try { $storage = \Drupal::entityTypeManager()->getStorage($entity_type_fallback); $candidates = []; if (!empty($entity->original)) { $candidates[] = $entity->original; } $loaded = $storage->load($entity_id_fallback); if ($loaded) { $candidates[] = $loaded; } if (method_exists($storage, 'loadUnchanged')) { $loaded_unchanged = $storage->loadUnchanged($entity_id_fallback); if ($loaded_unchanged) { $candidates[] = $loaded_unchanged; } } foreach ($candidates as $candidate_entity) { $stored_resolved = dfg_3dviewer_resolve_current_file_reference($candidate_entity, (string) $cfg['viewer_file_upload']); if ($stored_resolved === NULL) { continue; } $stored_file_id = (int) $stored_resolved['file_id']; $stored_file = \Drupal\file\Entity\File::load($stored_file_id); $stored_source_filename = $stored_file ? $stored_file->getFilename() : ''; $stored_source_uri = $stored_file ? $stored_file->getFileUri() : ''; \Drupal::service('queue')->get('dfg_3dviewer_convert')->createItem([ 'entity_type' => $entity_type_fallback, 'entity_id' => $entity_id_fallback, 'file_id' => $stored_file_id, 'source_filename' => $stored_source_filename, 'source_uri' => $stored_source_uri, ]); dfg_3dviewer_store_last_file_id((string) $entity_type_fallback, (string) $entity_id_fallback, (int) $stored_file_id); \Drupal::logger('dfg_3dviewer')->notice( 'entity_update fallback enqueue from candidate entity: entity @entity_id file @file_id.', [ '@entity_id' => (string) $entity_id_fallback, '@file_id' => (string) $stored_file_id, ] ); dfg_3dviewer_kick_convert_worker(); return; } } catch (\Throwable $e) { \Drupal::logger('dfg_3dviewer')->warning( 'entity_update stored fallback enqueue failed for entity @entity_id: @msg', [ '@entity_id' => (string) $entity_id_fallback, '@msg' => $e->getMessage(), ] ); } } if (!empty($entity_id_fallback)) { $last_file_id = dfg_3dviewer_load_last_file_id((string) $entity_type_fallback, (string) $entity_id_fallback); if ($last_file_id !== NULL) { try { $last_file = \Drupal\file\Entity\File::load($last_file_id); $last_source_filename = $last_file ? $last_file->getFilename() : ''; $last_source_uri = $last_file ? $last_file->getFileUri() : ''; \Drupal::service('queue')->get('dfg_3dviewer_convert')->createItem([ 'entity_type' => $entity_type_fallback, 'entity_id' => $entity_id_fallback, 'file_id' => $last_file_id, 'source_filename' => $last_source_filename, 'source_uri' => $last_source_uri, ]); \Drupal::logger('dfg_3dviewer')->notice( 'entity_update state fallback enqueue: entity @entity_id file @file_id.', [ '@entity_id' => (string) $entity_id_fallback, '@file_id' => (string) $last_file_id, ] ); dfg_3dviewer_kick_convert_worker(); return; } catch (\Throwable $e) { \Drupal::logger('dfg_3dviewer')->warning( 'entity_update state fallback enqueue failed for entity @entity_id: @msg', [ '@entity_id' => (string) $entity_id_fallback, '@msg' => $e->getMessage(), ] ); } } } \Drupal::logger('dfg_3dviewer')->notice( 'entity_update fallback behavior: kicking queue runner even without resolved file_id for entity @entity_id.', ['@entity_id' => (string) ($entity->id() ?? '')] ); try { $entity_id_fallback = (string) ($entity->id() ?? ''); if ($entity_id_fallback !== '') { \Drupal::service('queue')->get('dfg_3dviewer_convert')->createItem([ 'entity_type' => (string) $entity->getEntityTypeId(), 'entity_id' => $entity_id_fallback, 'file_id' => 0, 'source_filename' => '', 'source_uri' => '', ]); \Drupal::logger('dfg_3dviewer')->notice( 'entity_update enqueued entity-only fallback job for entity @entity_id.', ['@entity_id' => $entity_id_fallback] ); } } catch (\Throwable $e) { \Drupal::logger('dfg_3dviewer')->warning( 'entity_update failed to enqueue entity-only fallback job for entity @entity_id: @msg', [ '@entity_id' => (string) ($entity->id() ?? ''), '@msg' => $e->getMessage(), ] ); } dfg_3dviewer_kick_convert_worker(); return; } $file_id = (int) $resolved['file_id']; if (!empty($resolved['used_fallback'])) { \Drupal::logger('dfg_3dviewer')->notice( 'entity_update fallback: configured upload field "@field" is empty; using "@fallback_field" with file_id=@file_id for entity @entity_id. Configured raw: @raw', [ '@field' => (string) $resolved['configured_field'], '@fallback_field' => (string) $resolved['field_name'], '@file_id' => (string) $file_id, '@entity_id' => (string) ($entity->id() ?? ''), '@raw' => json_encode($resolved['configured_raw'], JSON_UNESCAPED_SLASHES), ] ); } $entity_id = $entity->id(); if (empty($entity_id) && $entity->hasField('eid')) { $entity_id = $entity->get('eid')->value; } if (empty($entity_id)) { \Drupal::logger('dfg_3dviewer')->warning('entity_update cannot enqueue: missing entity ID.'); dfg_3dviewer_kick_convert_worker(); return; } $uploaded_file = \Drupal\file\Entity\File::load($file_id); $source_filename = $uploaded_file ? $uploaded_file->getFilename() : ''; $source_uri = $uploaded_file ? $uploaded_file->getFileUri() : ''; $key = $entity->getEntityTypeId() . ':' . $entity_id . ':' . $file_id; if (!empty($GLOBALS['dfg_3dviewer_enqueued_in_presave'][$key])) { \Drupal::logger('dfg_3dviewer')->notice( 'entity_update skip: conversion was already queued in presave for entity @entity_id file @file_id.', [ '@entity_id' => (string) $entity_id, '@file_id' => (string) $file_id, ] ); return; } try { \Drupal::service('queue')->get('dfg_3dviewer_convert')->createItem([ 'entity_type' => $entity->getEntityTypeId(), 'entity_id' => $entity_id, 'file_id' => $file_id, 'source_filename' => $source_filename, 'source_uri' => $source_uri, ]); dfg_3dviewer_store_last_file_id((string) $entity->getEntityTypeId(), (string) $entity_id, (int) $file_id); \Drupal::logger('dfg_3dviewer')->notice( 'entity_update enqueue ok: entity @entity_id file @file_id.', [ '@entity_id' => (string) $entity_id, '@file_id' => (string) $file_id, ] ); } catch (\Throwable $e) { \Drupal::logger('dfg_3dviewer')->error( 'entity_update enqueue failed for entity @entity_id file @file_id: @msg', [ '@entity_id' => (string) $entity_id, '@file_id' => (string) $file_id, '@msg' => $e->getMessage(), ] ); } \Drupal::logger('dfg_3dviewer')->notice( 'entity_update force kick on save: entity @entity_id file @file_id.', [ '@entity_id' => (string) $entity_id, '@file_id' => (string) $file_id, ] ); dfg_3dviewer_kick_convert_worker(); } /** * Normalizes the viewer asset base path to a path-first value. */ function dfg_3dviewer_normalize_base_module_path(string $value): string { $value = trim($value); if ($value === '') { return $value; } if (preg_match('@^https?://@i', $value)) { $parts = parse_url($value); if (!empty($parts['path'])) { $value = $parts['path']; } } elseif (preg_match('@^/[^/]+\.[^/]+/.+@', $value)) { $segments = array_values(array_filter(explode('/', $value), 'strlen')); array_shift($segments); $value = '/' . implode('/', $segments); } elseif (preg_match('@^[^/]+\.[^/]+/.+@', $value)) { $segments = explode('/', $value); array_shift($segments); $value = '/' . implode('/', $segments); } $value = preg_replace('@/{2,}@', '/', $value); if ($value !== '' && $value[0] !== '/') { $value = '/' . $value; } return rtrim($value, '/'); } /** * Builds the viewer runtime settings object for drupalSettings / JS CONFIG. */ function dfg_3dviewer_build_js_settings(): array { $config = \Drupal::config('dfg_3dviewer.settings'); $main_url = trim((string) ( $config->get('dfg_3dviewer_main_url') ?? $config->get('main_url') ?? '' )); $metadata_url = trim((string) ( $config->get('dfg_3dviewer_metadata_url') ?? $config->get('metadata_url') ?? '' )); $json_export_base_url = trim((string) ( $config->get('dfg_3dviewer_json_export_base_url') ?? $config->get('json_export_base_url') ?? '' )); $base_namespace = trim((string) ( $config->get('dfg_3dviewer_basenamespace') ?? $config->get('base_namespace') ?? '' )); $base_module_path = dfg_3dviewer_normalize_base_module_path(trim((string) ( $config->get('dfg_3dviewer_base_module_path') ?? $config->get('base_path') ?? '' ))); if ($base_module_path === '') { $base_module_path = '/libraries/dfg-3dviewer/assets'; } $lightweight = filter_var( $config->get('dfg_3dviewer_lightweight') ?? $config->get('lightweight') ?? FALSE, FILTER_VALIDATE_BOOLEAN ); return [ 'baseNamespace' => $base_namespace, 'mainUrl' => $main_url, 'metadataUrl' => $metadata_url, 'jsonExportBaseUrl' => $json_export_base_url, 'baseModulePath' => $base_module_path, 'entity' => [ 'bundle' => trim((string) ( $config->get('dfg_3dviewer_entitybundle') ?? $config->get('entity_bundle') ?? '' )), 'fieldDf' => trim((string) ( $config->get('dfg_3dviewer_field_df') ?? $config->get('field_df') ?? 'field_df' )), 'idUri' => trim((string) ( $config->get('dfg_3dviewer_entity_id_uri') ?? $config->get('entity_id_uri') ?? '/wisski/navigate/(.*)/view' )), 'viewEntityPath' => trim((string) ( $config->get('dfg_3dviewer_view_entity_path') ?? $config->get('view_entity_path') ?? '/wisski/navigate/' )), 'attributeId' => trim((string) ( $config->get('dfg_3dviewer_attribute_id') ?? $config->get('attribute_id') ?? 'wisski_id' )), 'exportViewer' => trim((string) ( $config->get('dfg_3dviewer_export_viewer') ?? $config->get('export_viewer') ?? 'field_df' )), 'exportViewerUrl' => trim((string) ( $config->get('dfg_3dviewer_export_viewer_url') ?? $config->get('export_viewer_url') ?? '' )), 'metadata' => [ 'source' => 'Drupal', 'sourceType' => '', 'url' => '', ], ], 'viewer' => [ 'container' => trim((string) ( $config->get('dfg_3dviewer_container') ?? $config->get('container') ?? 'DFG_3DViewer' )), 'fileUpload' => trim((string) ( $config->get('dfg_3dviewer_viewer_file_upload') ?? $config->get('viewer_file_upload') ?? '' )), 'fileName' => trim((string) ( $config->get('dfg_3dviewer_viewer_file_name') ?? $config->get('viewer_file_name') ?? '' )), 'api3dFileField' => trim((string) ( $config->get('dfg_3dviewer_api_3d_file_field') ?? $config->get('api_3d_file_field') ?? '' )), 'imageGeneration' => trim((string) ( $config->get('dfg_3dviewer_image_generation') ?? $config->get('image_generation') ?? '' )), 'lightweight' => $lightweight, 'editor' => !$lightweight, 'scaleContainer' => [ 'x' => (string) ( $config->get('dfg_3dviewer_scale_container_x') ?? $config->get('scale_container_x') ?? '1' ), 'y' => (string) ( $config->get('dfg_3dviewer_scale_container_y') ?? $config->get('scale_container_y') ?? '1' ), ], 'gallery' => [ 'container' => trim((string) ( $config->get('dfg_3dviewer_gallery_container') ?? $config->get('gallery_container') ?? '' )), 'imageClass' => trim((string) ( $config->get('dfg_3dviewer_gallery_image_class') ?? $config->get('gallery_image_class') ?? '' )), 'imageId' => trim((string) ( $config->get('dfg_3dviewer_gallery_image_id') ?? $config->get('gallery_image_id') ?? '' )), 'build' => TRUE, ], ], ]; } /** * Attaches viewer settings and CSRF token to a render element. */ function dfg_3dviewer_attach_settings(array &$element): void { $settings = dfg_3dviewer_build_js_settings(); $token = \Drupal::csrfToken()->get('rest'); $settings['csrfToken'] = $token; $element['#attached']['drupalSettings']['dfg_3dviewer'] = $settings; $element['#attached']['html_head'][] = [ [ '#tag' => 'script', '#value' => 'window.CSRF_TOKEN = ' . json_encode($token, JSON_THROW_ON_ERROR) . ';', ], 'dfg_3dviewer_csrf_token', ]; } function dfg_3dviewer_get_library(): string { return 'dfg_3dviewer/dfg_3dviewer.viewer'; }