dfg_3dviewer_drupal_module/dfg_3dviewer.module
2026-06-25 09:09:16 +02:00

1438 lines
42 KiB
Text
Executable file

<?php
use Drupal\Core\Routing\RouteMatchInterface;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Template\Attribute;
use Drupal\Core\Url;
use Drupal\image\Entity\ImageStyle;
use Drupal\Core\Archiver\Zip;
use Drupal\Core\Archiver\ArchiverException;
use Symfony\Component\Yaml\Yaml;
use Symfony\Component\Process\Process;
use Drupal\dfg_3dviewer\ModelFormatManager;
function dfg_3dviewer_config(): array {
static $cfg;
if ($cfg === NULL) {
$config = \Drupal::config('dfg_3dviewer.settings');
$main_url = trim((string) (
$config->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 '<p>' . t('This is the WissKI module for the integration ' .
'of IIP (https://iipimage.sourceforge.io/).') . '</p>';
}
}
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';
}