diff --git a/plugins/arMetadataExtractionPlugin/README.md b/plugins/arMetadataExtractionPlugin/README.md new file mode 100644 index 0000000000..77cd22171f --- /dev/null +++ b/plugins/arMetadataExtractionPlugin/README.md @@ -0,0 +1,273 @@ +# arMetadataExtractionPlugin for AtoM 2.9 + +A Symfony 1.4 plugin for AtoM (Access to Memory) that automatically extracts and applies metadata from uploaded digital objects (images, PDFs, etc.) to their associated information objects. + +## Features + +- **Automatic Metadata Extraction**: Extracts EXIF, IPTC, and XMP metadata from uploaded files +- **Smart Field Population**: Intelligently populates AtoM fields including: + - Title + - Description (Scope and Content) + - Creator (with automatic actor creation) + - Creation dates + - Subject access points (keywords) + - Rights statements + - GPS coordinates +- **Technical Metadata**: Adds camera settings and technical details to physical characteristics +- **Auto-generated Keywords**: Creates relevant subject terms based on camera type and settings +- **Configurable Settings**: Admin interface for controlling extraction behavior +- **Non-destructive Updates**: Option to only update empty fields or overwrite existing data + +## Requirements + +- AtoM 2.9.x +- PHP 8.3+ +- exiftool installed on the server +- arEmbeddedMetadataParser class (usually included with AtoM) + +## Installation + +### Step 1: Install the Plugin + +```bash +# Navigate to your AtoM plugins directory +cd /usr/share/nginx/atom/plugins/ + +# Copy the plugin folder +sudo cp -r /path/to/arMetadataExtractionPlugin ./ + +# Set proper permissions +sudo chown -R www-data:www-data arMetadataExtractionPlugin/ +sudo chmod -R 755 arMetadataExtractionPlugin/ +``` + +### Step 2: Replace the Digital Object Upload Action + +The plugin includes a modified version of the upload action. You have two options: + +#### Option A: Replace the Core File (Recommended for Testing) +```bash +# Backup the original file +sudo cp /usr/share/nginx/atom/apps/qubit/modules/object/actions/addDigitalObjectAction.class.php \ + /usr/share/nginx/atom/apps/qubit/modules/object/actions/addDigitalObjectAction.class.php.backup + +# Copy the plugin's version +sudo cp /usr/share/nginx/atom/plugins/arMetadataExtractionPlugin/modules/object/actions/addDigitalObjectAction.class.php \ + /usr/share/nginx/atom/apps/qubit/modules/object/actions/addDigitalObjectAction.class.php +``` + +#### Option B: Create a Local Override (Recommended for Production) +```bash +# Create local override directory if it doesn't exist +sudo mkdir -p /usr/share/nginx/atom/apps/qubit/modules/object/actions/ + +# Copy the plugin's action +sudo cp /usr/share/nginx/atom/plugins/arMetadataExtractionPlugin/modules/object/actions/addDigitalObjectAction.class.php \ + /usr/share/nginx/atom/apps/qubit/modules/object/actions/ +``` + +### Step 3: Clear Symfony Cache + +```bash +cd /usr/share/nginx/atom/ +sudo -u www-data php symfony cc +``` + +### Step 4: Update Database Settings (Optional) + +The plugin will automatically create its settings on first use. To manually initialize: + +```bash +1. Log in to AtoM as an administrator +2. Navigate to Admin → Settings +3. Look for "Metadata extraction settings" in the settings list +4. Configure the plugin according to your needs +``` + +### Step 5: Verify Installation + +1. Log in to AtoM as an administrator +2. Navigate to Admin → Settings +3. Look for "Metadata extraction settings" in the settings list +4. Configure the plugin according to your needs + +## Configuration + +Access the plugin settings at: **Admin → Settings → Metadata extraction settings** + +### Available Settings: + +#### Main Settings +- **Enable/Disable**: Turn metadata extraction on or off globally + +#### Metadata Types +- **Extract EXIF**: Camera settings, date taken, technical information +- **Extract IPTC**: Headlines, captions, keywords, creator information +- **Extract XMP**: Adobe metadata including descriptions and rights + +#### Field Update Behavior +- **Always overwrite title**: Replace existing titles (unchecked = only update if empty) +- **Always overwrite description**: Replace existing descriptions (unchecked = only update if empty) + +#### Additional Features +- **Auto-generate keywords**: Create subject terms based on camera/technical data +- **Extract GPS coordinates**: Store location data from geotagged images +- **Add technical metadata**: Include camera settings in physical characteristics + +## Usage + +Once installed and configured, the plugin works automatically: + +1. Upload a digital object to any information object +2. The plugin extracts available metadata from the file +3. Metadata is mapped to appropriate AtoM fields +4. Information object is updated with the extracted data + +### Metadata Mapping + +| Source Metadata | AtoM Field | Notes | +|-----------------|------------|-------| +| XMP Title / IPTC Headline | Title | Only updates if empty or configured to overwrite | +| XMP Description / IPTC Caption | Scope and Content | Only updates if empty or configured to overwrite | +| XMP Creator / IPTC By-line / EXIF Artist | Creator (Event) | Creates new actor if needed | +| EXIF DateTimeOriginal | Creation Date (Event) | Parsed to YYYY-MM-DD format | +| XMP Subject / IPTC Keywords | Subject Access Points | Added as taxonomy terms | +| EXIF GPS Data | Digital Object Properties | Stored as latitude/longitude | +| EXIF Camera/Lens Data | Physical Characteristics | Appended as technical metadata | +| XMP Rights / IPTC Copyright | Access Conditions | Only updates if empty | + +### Auto-generated Keywords Examples + +Based on camera and settings, the plugin may generate keywords like: +- "Canon Photography" (for Canon cameras) +- "Mobile Photography" (for phone cameras) +- "Wide Angle Photography" (focal length ≤ 35mm) +- "Telephoto Photography" (focal length ≥ 85mm) +- "High ISO Photography" (ISO ≥ 1600) +- "Digital Photography" (always added) + +## Troubleshooting + +### Metadata Not Extracting + +1. Check that exiftool is installed: + ```bash + which exiftool + ``` + +2. Verify file permissions: + ```bash + ls -la /usr/share/nginx/atom/plugins/arMetadataExtractionPlugin/ + ``` + +3. Check Symfony logs: + ```bash + tail -f /usr/share/nginx/atom/log/frontend_*.log + ``` + +### Settings Not Appearing + +1. Clear the cache: + ```bash + cd /usr/share/nginx/atom/ + sudo -u www-data php symfony cc + ``` + +2. Check plugin is enabled in ProjectConfiguration.class.php + +### GPS Coordinates Not Saving + +1. Ensure "Extract GPS coordinates" is enabled in settings +2. Verify the image contains GPS data using exiftool: + ```bash + exiftool -GPS* your-image.jpg + ``` + +## Uninstallation + +1. Restore the original addDigitalObjectAction.class.php: + ```bash + sudo mv /usr/share/nginx/atom/apps/qubit/modules/object/actions/addDigitalObjectAction.class.php.backup \ + /usr/share/nginx/atom/apps/qubit/modules/object/actions/addDigitalObjectAction.class.php + ``` + +2. Remove the plugin directory: + ```bash + sudo rm -rf /usr/share/nginx/atom/plugins/arMetadataExtractionPlugin/ + ``` + +3. Clear cache: + ```bash + cd /usr/share/nginx/atom/ + sudo -u www-data php symfony cc + ``` + +## Development + +### Plugin Structure + +``` +arMetadataExtractionPlugin/ +├── config/ +│ └── arMetadataExtractionPluginConfiguration.class.php # Plugin configuration +├── lib/ +│ └── arMetadataExtractor.class.php # Main extraction logic +├── modules/ +│ ├── object/ +│ │ └── actions/ +│ │ └── addDigitalObjectAction.class.php # Modified upload action +│ └── settings/ +│ ├── actions/ +│ │ └── metadataExtractionAction.class.php # Settings action +│ └── templates/ +│ └── metadataExtractionSuccess.php # Settings template +└── README.md +``` + +### Extending the Plugin + +To add support for additional metadata fields: + +1. Modify `normalizeMetadata()` in `arMetadataExtractor.class.php` +2. Add mapping logic in `applyMetadata()` +3. Update the settings interface if needed + +### Event System + +The plugin uses Symfony events: +- `digital_object.post_create`: Triggered after a digital object is saved +- Can be extended to listen for other events like `digital_object.post_update` + +## Contributing + +Contributions are welcome! Please: + +1. Fork the repository +2. Create a feature branch +3. Test thoroughly with AtoM 2.9 +4. Submit a pull request + +## License + +This plugin is released under the GNU Affero General Public License v3.0, the same license as AtoM. + +## Credits + +Developed by Johan Pieterse (johan@theahg.co.za) for The Archive and Heritage Group. + +Based on the original AtoM digital object handling code by Artefactual Systems. + +## Support + +For issues or questions: +- Email: johan@theahg.co.za +- AtoM Forum: https://groups.google.com/forum/#!forum/ica-atom-users + +## Version History + +### 1.0.0 (2024-01) +- Initial release +- Support for EXIF, IPTC, and XMP metadata +- Admin settings interface +- Auto-keyword generation +- GPS coordinate extraction diff --git a/plugins/arMetadataExtractionPlugin/config/arMetadataExtractionPluginConfiguration.class.php b/plugins/arMetadataExtractionPlugin/config/arMetadataExtractionPluginConfiguration.class.php new file mode 100644 index 0000000000..ddd898434f --- /dev/null +++ b/plugins/arMetadataExtractionPlugin/config/arMetadataExtractionPluginConfiguration.class.php @@ -0,0 +1,128 @@ +. + */ + +/** + * arMetadataExtractionPlugin configuration. + * + * @author Johan Pieterse The Archive and Heritage Group + */ +class arMetadataExtractionPluginConfiguration extends sfPluginConfiguration +{ + protected static $settingsInitialized = false; + + public function initialize() + { + // Register event listeners + $this->dispatcher->connect( + 'digital_object.post_create', + [$this, 'extractMetadata'] + ); + + // Defer settings initialization to when database is ready + $this->dispatcher->connect( + 'context.load_factories', + [$this, 'initializeSettings'] + ); + } + + public function initializeSettings(sfEvent $event) + { + if (self::$settingsInitialized) { + return; + } + + try { + if (class_exists('QubitSetting') && sfContext::hasInstance()) { + $this->addPluginSettings(); + self::$settingsInitialized = true; + } + } catch (Exception $e) { + // Silent; defaults in arMetadataExtractor will apply + } + } + + public function extractMetadata(sfEvent $event) + { + try { + $digitalObject = $event->getSubject(); + + if ($digitalObject instanceof QubitDigitalObject) { + // Determine target field for technical metadata + $targetField = 'physicalCharacteristics'; + + try { + if (class_exists('QubitSetting')) { + if ($s = QubitSetting::getByName('technical_metadata_target_field')) { + $v = $s->getValue(['sourceCulture' => true]); + if (is_string($v) && '' !== trim($v)) { + $targetField = trim($v); + } + } + } + } catch (Exception $e) { + // fall back to default + } + + // NOTE: here we **initiate the function with a variable for the field to save to** + $extractor = new arMetadataExtractor($targetField); + $extractor->processDigitalObject($digitalObject); + } + } catch (Exception $e) { + if (sfContext::hasInstance()) { + sfContext::getInstance() + ->getLogger() + ->err('Metadata extraction failed: '.$e->getMessage()); + } + } + } + + protected function addPluginSettings() + { + try { + $settings = [ + 'metadata_extraction_enabled' => true, + 'extract_exif' => true, + 'extract_iptc' => true, + 'extract_xmp' => true, + 'overwrite_title' => false, + 'overwrite_description' => false, + 'auto_generate_keywords' => true, + 'extract_gps_coordinates' => true, + 'add_technical_metadata' => true, + // NEW: field where technical metadata summary is stored + 'technical_metadata_target_field' => 'physicalCharacteristics', + ]; + + foreach ($settings as $name => $default) { + try { + if (null === QubitSetting::getByName($name)) { + $setting = new QubitSetting(); + $setting->name = $name; + $setting->value = $default; + $setting->save(); + } + } catch (Exception $e) { + continue; + } + } + } catch (Exception $e) { + // DB not ready, ignore + } + } +} diff --git a/plugins/arMetadataExtractionPlugin/install.sh b/plugins/arMetadataExtractionPlugin/install.sh new file mode 100644 index 0000000000..bb92822a67 --- /dev/null +++ b/plugins/arMetadataExtractionPlugin/install.sh @@ -0,0 +1,218 @@ +#!/bin/bash + +# arMetadataExtractionPlugin Installation Script for AtoM 2.9 +# Author: Johan Pieterse +# Version: 1.0.0 + +set -e + +# Colors for output +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +NC='\033[0m' # No Color + +# Configuration +ATOM_PATH="/usr/share/nginx/atom_psis" +PLUGIN_NAME="arMetadataExtractionPlugin" +WEB_USER="www-data" +WEB_GROUP="www-data" + +# Function to print colored messages +print_message() { + echo -e "${2}${1}${NC}" +} + +# Function to check if running as root +check_root() { + if [[ $EUID -ne 0 ]]; then + print_message "This script must be run as root (use sudo)" "$RED" + exit 1 + fi +} + +# Function to check if AtoM directory exists +check_atom_installation() { + if [ ! -d "$ATOM_PATH" ]; then + print_message "AtoM installation not found at $ATOM_PATH" "$RED" + read -p "Enter your AtoM installation path: " ATOM_PATH + if [ ! -d "$ATOM_PATH" ]; then + print_message "Invalid AtoM path. Exiting." "$RED" + exit 1 + fi + fi + print_message "Found AtoM installation at: $ATOM_PATH" "$GREEN" +} + +# Function to check prerequisites +check_prerequisites() { + print_message "\nChecking prerequisites..." "$YELLOW" + + # Check for exiftool + if ! command -v exiftool &> /dev/null; then + print_message "exiftool is not installed. Installing..." "$YELLOW" + apt-get update && apt-get install -y libimage-exiftool-perl + else + print_message "exiftool is installed" "$GREEN" + fi + + # Check for arEmbeddedMetadataParser + if [ ! -f "$ATOM_PATH/lib/helper/arEmbeddedMetadataParser.class.php" ]; then + print_message "Warning: arEmbeddedMetadataParser.class.php not found" "$YELLOW" + print_message "The plugin may not work without this file" "$YELLOW" + else + print_message "arEmbeddedMetadataParser found" "$GREEN" + fi +} + +# Function to backup original files +backup_files() { + print_message "\nBacking up original files..." "$YELLOW" + + ACTION_FILE="$ATOM_PATH/apps/qubit/modules/object/actions/addDigitalObjectAction.class.php" + + if [ -f "$ACTION_FILE" ]; then + if [ ! -f "${ACTION_FILE}.backup" ]; then + cp "$ACTION_FILE" "${ACTION_FILE}.backup" + print_message "Backed up addDigitalObjectAction.class.php" "$GREEN" + else + print_message "Backup already exists, skipping" "$YELLOW" + fi + fi +} + +# Function to install plugin +install_plugin() { + print_message "\nInstalling plugin..." "$YELLOW" + + # Get current directory (where the plugin is) + PLUGIN_SOURCE="$(pwd)" + + # Check if we're in the plugin directory + if [ ! -f "$PLUGIN_SOURCE/config/arMetadataExtractionPluginConfiguration.class.php" ]; then + print_message "Error: Not in plugin directory. Please run from arMetadataExtractionPlugin folder" "$RED" + exit 1 + fi + + # Copy plugin to AtoM plugins directory + PLUGIN_DEST="$ATOM_PATH/plugins/$PLUGIN_NAME" + + if [ -d "$PLUGIN_DEST" ]; then + print_message "Plugin directory already exists. Removing old version..." "$YELLOW" + rm -rf "$PLUGIN_DEST" + fi + + cp -r "$PLUGIN_SOURCE" "$PLUGIN_DEST" + print_message "Plugin files copied to $PLUGIN_DEST" "$GREEN" + + # Copy the modified action file + cp "$PLUGIN_DEST/modules/object/actions/addDigitalObjectAction.class.php" \ + "$ATOM_PATH/apps/qubit/modules/object/actions/addDigitalObjectAction.class.php" + print_message "Updated addDigitalObjectAction.class.php" "$GREEN" + + # Set permissions + chown -R ${WEB_USER}:${WEB_GROUP} "$PLUGIN_DEST" + chmod -R 755 "$PLUGIN_DEST" + print_message "Set correct permissions" "$GREEN" +} + +# Function to enable plugin in ProjectConfiguration +enable_plugin() { + print_message "\nEnabling plugin in ProjectConfiguration..." "$YELLOW" + + CONFIG_FILE="$ATOM_PATH/config/ProjectConfiguration.class.php" + + if grep -q "$PLUGIN_NAME" "$CONFIG_FILE"; then + print_message "Plugin already enabled in ProjectConfiguration" "$GREEN" + else + # Add plugin to enabled plugins list + # This is a simplified approach - may need manual editing for complex configurations + print_message "Please manually add '$PLUGIN_NAME' to the enabled plugins in:" "$YELLOW" + print_message "$CONFIG_FILE" "$YELLOW" + print_message "Add this line in the setup() method:" "$YELLOW" + print_message "\$this->enablePlugins('$PLUGIN_NAME');" "$YELLOW" + fi +} + +# Function to clear cache +clear_cache() { + print_message "\nClearing Symfony cache..." "$YELLOW" + + cd "$ATOM_PATH" + sudo -u $WEB_USER php symfony cc + + print_message "Cache cleared" "$GREEN" +} + +# Function to run post-installation tasks +post_installation() { + print_message "\n========================================" "$GREEN" + print_message "Installation completed successfully!" "$GREEN" + print_message "========================================" "$GREEN" + + print_message "\nNext steps:" "$YELLOW" + print_message "1. Log in to AtoM as an administrator" "$NC" + print_message "2. Navigate to Admin → Settings" "$NC" + print_message "3. Click on 'Metadata extraction settings'" "$NC" + print_message "4. Configure the plugin according to your needs" "$NC" + + print_message "\nTo test the plugin:" "$YELLOW" + print_message "1. Upload an image with EXIF/IPTC/XMP metadata" "$NC" + print_message "2. Check if metadata was extracted to the information object" "$NC" + + print_message "\nTo uninstall, run:" "$YELLOW" + print_message "sudo ./install.sh --uninstall" "$NC" +} + +# Function to uninstall plugin +uninstall_plugin() { + print_message "\nUninstalling plugin..." "$YELLOW" + + # Restore backup + ACTION_FILE="$ATOM_PATH/apps/qubit/modules/object/actions/addDigitalObjectAction.class.php" + if [ -f "${ACTION_FILE}.backup" ]; then + mv "${ACTION_FILE}.backup" "$ACTION_FILE" + print_message "Restored original addDigitalObjectAction.class.php" "$GREEN" + fi + + # Remove plugin directory + PLUGIN_DEST="$ATOM_PATH/plugins/$PLUGIN_NAME" + if [ -d "$PLUGIN_DEST" ]; then + rm -rf "$PLUGIN_DEST" + print_message "Removed plugin directory" "$GREEN" + fi + + # Clear cache + clear_cache + + print_message "\nPlugin uninstalled successfully" "$GREEN" + print_message "Remember to remove '$PLUGIN_NAME' from ProjectConfiguration.class.php manually" "$YELLOW" +} + +# Main execution +main() { + print_message "========================================" "$GREEN" + print_message "arMetadataExtractionPlugin Installer" "$GREEN" + print_message "========================================" "$NC" + + check_root + + # Check for uninstall flag + if [ "$1" == "--uninstall" ]; then + check_atom_installation + uninstall_plugin + exit 0 + fi + + # Normal installation + check_atom_installation + check_prerequisites + backup_files + install_plugin + enable_plugin + clear_cache + post_installation +} + +# Run the script +main "$@" diff --git a/plugins/arMetadataExtractionPlugin/lib/arMetadataExtractor.class.php b/plugins/arMetadataExtractionPlugin/lib/arMetadataExtractor.class.php new file mode 100644 index 0000000000..6547db9229 --- /dev/null +++ b/plugins/arMetadataExtractionPlugin/lib/arMetadataExtractor.class.php @@ -0,0 +1,399 @@ +. + */ + +/** + * Metadata extraction service for digital objects. + * + * @author Johan Pieterse The Archive and Heritage Group + */ +class arMetadataExtractor +{ + protected $settings = []; + protected $logger; + + public function __construct(?string $techMetadataTargetField = null) + { + $this->loadSettings(); + + // Allow override via constructor param + if (null !== $techMetadataTargetField) { + $this->settings['technical_metadata_target_field'] = $techMetadataTargetField; + } + + try { + if (sfContext::hasInstance()) { + $this->logger = sfContext::getInstance()->getLogger(); + } + } catch (Exception $e) { + $this->logger = null; + } + } + + public function processDigitalObject(QubitDigitalObject $digitalObject) + { + if (empty($this->settings['metadata_extraction_enabled'])) { + return false; + } + + $filePath = $digitalObject->getAbsolutePath(); + + if (!$filePath || !file_exists($filePath)) { + $this->log('Metadata extraction: File not found at '.$filePath, 'warning'); + + return false; + } + + $metadata = $this->extractMetadata($filePath); + + if (!$metadata) { + return false; + } + + $informationObject = $digitalObject->getInformationObject(); + + if (!$informationObject) { + $this->log('Metadata extraction: No information object linked to digital object', 'warning'); + + return false; + } + + $this->applyMetadata($informationObject, $digitalObject, $metadata); + + return true; + } + + /** + * Safe logging method that handles null logger. + * + * @param mixed $message + * @param mixed $level + */ + protected function log($message, $level = 'info') + { + if (!$this->logger) { + return; + } + + switch ($level) { + case 'error': + case 'err': + $this->logger->err($message); + + break; + + case 'warning': + case 'warn': + $this->logger->warning($message); + + break; + + case 'debug': + $this->logger->debug($message); + + break; + + default: + $this->logger->info($message); + } + } + + protected function loadSettings() + { + $defaults = [ + 'metadata_extraction_enabled' => true, + 'extract_exif' => true, + 'extract_iptc' => true, + 'extract_xmp' => true, + 'overwrite_title' => false, + 'overwrite_description' => false, + 'auto_generate_keywords' => true, + 'extract_gps_coordinates' => true, + 'add_technical_metadata' => true, + // NEW default for target field + 'technical_metadata_target_field' => 'physicalCharacteristics', + ]; + + $this->settings = $defaults; + + try { + if (class_exists('QubitSetting') && sfContext::hasInstance()) { + foreach ($defaults as $name => $defaultValue) { + try { + $setting = QubitSetting::getByName($name); + if ($setting) { + $this->settings[$name] = $setting->getValue([ + 'sourceCulture' => true, + ]); + } + } catch (Exception $e) { + $this->settings[$name] = $defaultValue; + } + } + } + } catch (Exception $e) { + // use defaults + } + } + + protected function extractMetadata($filePath) + { + if (!class_exists('arEmbeddedMetadataParser')) { + $this->log('arEmbeddedMetadataParser class not found', 'error'); + + return null; + } + + try { + $rawMetadata = arEmbeddedMetadataParser::extract($filePath); + + if (!$rawMetadata) { + return null; + } + + return $this->normalizeMetadata($rawMetadata); + } catch (Exception $e) { + $this->log('Metadata extraction failed: '.$e->getMessage(), 'error'); + + return null; + } + } + + protected function normalizeMetadata($rawMetadata) + { + $metadata = [ + 'title' => null, + 'description' => null, + 'creator' => null, + 'date' => null, + 'keywords' => [], + 'gps' => null, + 'technical' => [], + 'rights' => null, + // we keep raw payload if needed later + '_raw' => $rawMetadata, + ]; + + $norm = $rawMetadata['_norm'] ?? []; + + $metadata['title'] = $norm['title'] + ?? $rawMetadata['ObjectName'] + ?? $rawMetadata['ImageDescription'] + ?? null; + + $metadata['description'] = $norm['description'] + ?? $rawMetadata['Caption-Abstract'] + ?? null; + + $metadata['creator'] = $norm['creator'] + ?? $rawMetadata['By-line'] + ?? $rawMetadata['Artist'] + ?? null; + + if (isset($norm['createDate'])) { + $metadata['date'] = $this->parseDate($norm['createDate']); + } elseif (isset($rawMetadata['DateTimeOriginal'])) { + $metadata['date'] = $this->parseDate($rawMetadata['DateTimeOriginal']); + } + + if (isset($rawMetadata['Subject'])) { + $metadata['keywords'] = is_array($rawMetadata['Subject']) + ? $rawMetadata['Subject'] + : array_map('trim', explode(',', $rawMetadata['Subject'])); + } elseif (isset($rawMetadata['Keywords'])) { + $metadata['keywords'] = is_array($rawMetadata['Keywords']) + ? $rawMetadata['Keywords'] + : array_map('trim', explode(',', $rawMetadata['Keywords'])); + } + + if (isset($rawMetadata['GPSLatitude'], $rawMetadata['GPSLongitude'])) { + $metadata['gps'] = [ + 'latitude' => $rawMetadata['GPSLatitude'], + 'longitude' => $rawMetadata['GPSLongitude'], + ]; + } + + $technicalFields = [ + 'Make', 'Model', 'FocalLength', 'FNumber', + 'ExposureTime', 'ISO', 'ImageWidth', 'ImageHeight', + 'ColorSpace', 'WhiteBalance', 'Flash', 'LensModel', + ]; + + foreach ($technicalFields as $field) { + if (isset($rawMetadata[$field])) { + $metadata['technical'][$field] = $rawMetadata[$field]; + } + } + + $metadata['rights'] = $norm['rights'] ?? $rawMetadata['Copyright'] ?? null; + + return $metadata; + } + + // parseDate() unchanged... + + protected function applyMetadata($informationObject, $digitalObject, $metadata) + { + // Title + if ( + $metadata['title'] + && $this->shouldUpdateField($informationObject->getTitle(), 'title') + ) { + $informationObject->setTitle($metadata['title']); + $this->log('Updated title from metadata: '.$metadata['title']); + } + + // Description / Scope and content + if ( + $metadata['description'] + && $this->shouldUpdateField($informationObject->getScopeAndContent(), 'description') + ) { + $informationObject->setScopeAndContent($metadata['description']); + $this->log('Updated scope and content from metadata'); + } + + // Creator + if ($metadata['creator']) { + $this->addCreator($informationObject, $metadata['creator']); + } + + // Creation date + if ($metadata['date']) { + $this->addCreationDate($informationObject, $metadata['date']); + } + + // Keywords + if (!empty($metadata['keywords'])) { + $this->addSubjectAccessPoints($informationObject, $metadata['keywords']); + } + + // GPS + if ( + $metadata['gps'] + && !empty($this->settings['extract_gps_coordinates']) + ) { + $this->setGpsCoordinates($digitalObject, $metadata['gps']); + } + + // Technical metadata (summary) + if ( + !empty($metadata['technical']) + && !empty($this->settings['add_technical_metadata']) + ) { + $this->addTechnicalMetadata($informationObject, $metadata['technical']); + } + + // Rights + if ($metadata['rights']) { + $this->addRightsStatement($informationObject, $metadata['rights']); + } + + // Auto keywords + if (!empty($this->settings['auto_generate_keywords'])) { + $generatedKeywords = $this->generateKeywords($metadata['technical']); + if (!empty($generatedKeywords)) { + $this->addSubjectAccessPoints($informationObject, $generatedKeywords); + } + } + + $informationObject->save(); + $this->log('Metadata extraction completed for object ID: '.$informationObject->id); + } + + // shouldUpdateField(), addCreator(), addCreationDate(), + // addSubjectAccessPoints(), setGpsCoordinates(), addRightsStatement(), + // generateKeywords() remain as you had them. + + /** + * Add technical metadata summary to a configurable field on the IO. + * + * @param mixed $informationObject + * @param mixed $technical + */ + protected function addTechnicalMetadata($informationObject, $technical) + { + $sections = []; + + if (isset($technical['Make'], $technical['Model'])) { + $sections[] = 'Camera: '.$technical['Make'].' '.$technical['Model']; + } + + if (isset($technical['LensModel'])) { + $sections[] = 'Lens: '.$technical['LensModel']; + } + + $settings = []; + + if (isset($technical['FocalLength'])) { + $settings[] = 'Focal Length: '.$technical['FocalLength']; + } + if (isset($technical['FNumber'])) { + $settings[] = 'Aperture: f/'.$technical['FNumber']; + } + if (isset($technical['ExposureTime'])) { + $settings[] = 'Shutter Speed: '.$technical['ExposureTime']; + } + if (isset($technical['ISO'])) { + $settings[] = 'ISO: '.$technical['ISO']; + } + + if (!empty($settings)) { + $sections[] = 'Settings: '.implode(', ', $settings); + } + + if (isset($technical['ImageWidth'], $technical['ImageHeight'])) { + $sections[] = 'Dimensions: '.$technical['ImageWidth'].' × '.$technical['ImageHeight'].' pixels'; + } + + if (empty($sections)) { + return; + } + + $summary = "Technical Metadata:\n".implode("\n", $sections); + + // Determine target field + $targetField = $this->settings['technical_metadata_target_field'] ?? 'physicalCharacteristics'; + + // Resolve getter/setter dynamically, with fallback + $getter = 'get'.ucfirst($targetField); + $setter = 'set'.ucfirst($targetField); + if (!method_exists($informationObject, $getter) || !method_exists($informationObject, $setter)) { + $getter = 'getPhysicalCharacteristics'; + $setter = 'setPhysicalCharacteristics'; + } + + $current = (string) $informationObject->{$getter}(); + + if ($current) { + // Remove existing Technical Metadata section if present + $current = preg_replace('/\n?Technical Metadata:.*$/s', '', $current); + $current = rtrim($current); + $newValue = $current."\n\n".$summary; + } else { + $newValue = $summary; + } + + $informationObject->{$setter}($newValue); + + $this->log(sprintf( + 'Added technical metadata to %s for IO ID %d', + $targetField, + $informationObject->id + )); + } +} diff --git a/plugins/arMetadataExtractionPlugin/modules/object/actions/addDigitalObjectAction.class.php b/plugins/arMetadataExtractionPlugin/modules/object/actions/addDigitalObjectAction.class.php new file mode 100644 index 0000000000..516a30af67 --- /dev/null +++ b/plugins/arMetadataExtractionPlugin/modules/object/actions/addDigitalObjectAction.class.php @@ -0,0 +1,235 @@ +. + */ + +/** + * Digital Object edit component - Simplified version using metadata extraction plugin. + * + * @author david juhasz + * Modified by Johan Pieterse to use arMetadataExtractionPlugin + */ +class ObjectAddDigitalObjectAction extends sfAction +{ + public function execute($request) + { + $this->form = new sfForm(); + $this->form + ->getValidatorSchema() + ->setOption('allow_extra_fields', true); + + $this->resource = $this->getRoute()->resource; + + // Get repository to test upload limits + if ($this->resource instanceof QubitInformationObject) { + $this->repository = $this->resource->getRepository([ + 'inherit' => true, + ]); + } elseif ($this->resource instanceof QubitActor) { + $this->repository = $this->resource->getMaintainingRepository(); + } + + // Check that object exists and that it is not the root + if (!isset($this->resource) || !isset($this->resource->parent)) { + $this->forward404(); + } + + // Assemble resource description + sfContext::getInstance() + ->getConfiguration() + ->loadHelpers(['Qubit']); + + if ($this->resource instanceof QubitActor) { + $this->resourceDescription = render_title($this->resource); + } elseif ($this->resource instanceof QubitInformationObject) { + $this->resourceDescription = ''; + + if (isset($this->resource->identifier)) { + $this->resourceDescription .= + $this->resource->identifier.' - '; + } + + $this->resourceDescription .= render_title( + new sfIsadPlugin($this->resource) + ); + } + + // Check if already exists a digital object + if (null !== ($digitalObject = $this->resource->getDigitalObject())) { + // NARSSA/SITA JJP multiple object upload enabled + // $this->redirect([$digitalObject, 'module' => 'digitalobject', 'action' => 'edit']); + } + + // Check user authorization + if (!QubitAcl::check($this->resource, 'update')) { + QubitAcl::forwardUnauthorized(); + } + + // Check if uploads are allowed + if (!QubitDigitalObject::isUploadAllowed()) { + QubitAcl::forwardToSecureAction(); + } + + // Add form fields + $this->addFields($request); + + // Process form + if ($request->isMethod('post')) { + $this->form->bind( + $request->getPostParameters(), + $request->getFiles() + ); + if ($this->form->isValid()) { + $this->processForm(); + + $this->resource->save(); + + if ($this->resource instanceof QubitInformationObject) { + $this->resource->updateXmlExports(); + } + $this->redirect([$this->resource, 'module' => 'object']); + } + } + } + + /** + * Upload the asset selected by user and create a digital object with appropriate + * representations. + * + * @return DigitalObjectEditAction this action + */ + public function processForm() + { + $digitalObject = new QubitDigitalObject(); + + if (null !== $this->form->getValue('file')) { + $file = $this->form->getValue('file'); + $tempFilePath = $file->getTempName(); + $name = $file->getOriginalName(); + $content = file_get_contents($tempFilePath); + + // Set up digital object with assets + $digitalObject->assets[] = new QubitAsset($name, $content); + $digitalObject->usageId = QubitTerm::MASTER_ID; + + // Add digital object to resource + $this->resource->digitalObjectsRelatedByobjectId[] = $digitalObject; + + // Save the parent resource first (creates the relationship) + $this->resource->save(); + + // Set objectId BEFORE saving + $digitalObject->objectId = $this->resource->id; + + // Save the digital object so file exists on disk + $digitalObject->save(); + + // The plugin's event listener will automatically extract metadata + // when the digital object is saved (via the 'digital_object.post_create' event) + + // However, if you want to manually trigger extraction: + if (class_exists('arMetadataExtractor')) { + $extractor = new arMetadataExtractor(); + $extractor->processDigitalObject($digitalObject); + } + + // Also still run the embedded technical metadata extraction if available + $this->appendEmbeddedTechMetadata($digitalObject); + } elseif (null !== $this->form->getValue('url')) { + // Catch errors trying to download remote resource + try { + $digitalObject->importFromURI($this->form->getValue('url')); + $this->resource->digitalObjectsRelatedByobjectId[] = $digitalObject; + } catch (sfException $e) { + // Log download exception + $this->logMessage($e->getMessage(), 'err'); + } + } + } + + protected function addFields($request) + { + // Single upload + if (0 < count($request->getFiles())) { + $this->form->setValidator('file', new sfValidatorFile()); + } + + $this->form->setWidget('file', new sfWidgetFormInputFile()); + + // URL + if (isset($request->url) && 'http://' != $request->url) { + $this->form->setValidator('url', new QubitValidatorUrl()); + } + + $this->form->setDefault('url', 'http://'); + $this->form->setWidget('url', new sfWidgetFormInput()); + } + + /** + * Append embedded technical metadata using arEmbeddedMetadataParser + * This remains as a backup/additional method for technical metadata. + * + * @param mixed $digitalObject + */ + private function appendEmbeddedTechMetadata($digitalObject) + { + try { + if ( + class_exists('arEmbeddedMetadataParser', /* autoload */ true) + && isset($digitalObject) + && $digitalObject instanceof QubitDigitalObject + ) { + $absPath = method_exists($digitalObject, 'getAbsolutePath') + ? $digitalObject->getAbsolutePath() + : (string) $digitalObject->getPath(); + + if ($absPath && is_readable($absPath)) { + // Extract metadata using the helper + $meta = arEmbeddedMetadataParser::extract($absPath); + + if (is_array($meta)) { + // Format and save summary + $summary = arEmbeddedMetadataParser::formatSummary($meta); + + if ('' !== $summary) { + $io = $this->resource; + + if ($io instanceof QubitInformationObject) { + $existing = (string) $io->physicalCharacteristics; + + // Remove existing Technical Metadata section + if ($existing && false !== strpos($existing, 'Technical Metadata:')) { + $existing = preg_replace('/\n?Technical Metadata:.*\z/s', '', $existing); + $existing = rtrim($existing); + } + + $io->physicalCharacteristics = $existing + ? $existing."\n\n".$summary + : $summary; + $io->save(); + + error_log('Successfully saved technical metadata to physical characteristics'); + } + } + } + } + } + } catch (Throwable $e) { + error_log('Error in appendEmbeddedTechMetadata: '.$e->getMessage()); + } + } +} diff --git a/plugins/arMetadataExtractionPlugin/modules/settings/actions/metadataExtractionAction.class.php b/plugins/arMetadataExtractionPlugin/modules/settings/actions/metadataExtractionAction.class.php new file mode 100644 index 0000000000..d03add7a77 --- /dev/null +++ b/plugins/arMetadataExtractionPlugin/modules/settings/actions/metadataExtractionAction.class.php @@ -0,0 +1,172 @@ +. + */ + +/** + * Metadata extraction settings action. + * + * @author Johan Pieterse + */ +class MetadataExtractionSettingsAction extends DefaultEditAction +{ + public static function getTitle($context) + { + return __('Metadata extraction settings'); + } + + public static function getGroup() + { + return __('Admin'); + } + + public static function getDescription($context) + { + return __('Configure automatic metadata extraction from uploaded digital objects'); + } + + public function earlyExecute() + { + parent::earlyExecute(); + + $this->updateMessage = __('Settings saved successfully'); + } + + public function execute($request) + { + parent::execute($request); + + if ($request->isMethod('post')) { + $this->form->bind($request->getPostParameters()); + + if ($this->form->isValid()) { + $this->processForm(); + + $this->getUser()->setFlash('notice', $this->updateMessage); + $this->redirect(['module' => 'settings', 'action' => 'metadataExtraction']); + } + } + } + + protected function buildForm() + { + $form = new sfForm(); + + // Main enable/disable switch + $form->setWidget('metadata_extraction_enabled', new sfWidgetFormSelectRadio( + ['choices' => [ + 1 => __('Enabled'), + 0 => __('Disabled'), + ]], + ['class' => 'radio'] + )); + $form->setValidator('metadata_extraction_enabled', new sfValidatorChoice( + ['choices' => [0, 1]] + )); + $form->setDefault('metadata_extraction_enabled', + QubitSetting::getByName('metadata_extraction_enabled') ? + QubitSetting::getByName('metadata_extraction_enabled')->getValue(['sourceCulture' => true]) : 1 + ); + + // Metadata types to extract + $form->setWidget('extract_exif', new sfWidgetFormInputCheckbox()); + $form->setValidator('extract_exif', new sfValidatorBoolean()); + $form->setDefault('extract_exif', + QubitSetting::getByName('extract_exif') ? + QubitSetting::getByName('extract_exif')->getValue(['sourceCulture' => true]) : 1 + ); + + $form->setWidget('extract_iptc', new sfWidgetFormInputCheckbox()); + $form->setValidator('extract_iptc', new sfValidatorBoolean()); + $form->setDefault('extract_iptc', + QubitSetting::getByName('extract_iptc') ? + QubitSetting::getByName('extract_iptc')->getValue(['sourceCulture' => true]) : 1 + ); + + $form->setWidget('extract_xmp', new sfWidgetFormInputCheckbox()); + $form->setValidator('extract_xmp', new sfValidatorBoolean()); + $form->setDefault('extract_xmp', + QubitSetting::getByName('extract_xmp') ? + QubitSetting::getByName('extract_xmp')->getValue(['sourceCulture' => true]) : 1 + ); + + // Overwrite settings + $form->setWidget('overwrite_title', new sfWidgetFormInputCheckbox()); + $form->setValidator('overwrite_title', new sfValidatorBoolean()); + $form->setDefault('overwrite_title', + QubitSetting::getByName('overwrite_title') ? + QubitSetting::getByName('overwrite_title')->getValue(['sourceCulture' => true]) : 0 + ); + + $form->setWidget('overwrite_description', new sfWidgetFormInputCheckbox()); + $form->setValidator('overwrite_description', new sfValidatorBoolean()); + $form->setDefault('overwrite_description', + QubitSetting::getByName('overwrite_description') ? + QubitSetting::getByName('overwrite_description')->getValue(['sourceCulture' => true]) : 0 + ); + + // Additional features + $form->setWidget('auto_generate_keywords', new sfWidgetFormInputCheckbox()); + $form->setValidator('auto_generate_keywords', new sfValidatorBoolean()); + $form->setDefault('auto_generate_keywords', + QubitSetting::getByName('auto_generate_keywords') ? + QubitSetting::getByName('auto_generate_keywords')->getValue(['sourceCulture' => true]) : 1 + ); + + $form->setWidget('extract_gps_coordinates', new sfWidgetFormInputCheckbox()); + $form->setValidator('extract_gps_coordinates', new sfValidatorBoolean()); + $form->setDefault('extract_gps_coordinates', + QubitSetting::getByName('extract_gps_coordinates') ? + QubitSetting::getByName('extract_gps_coordinates')->getValue(['sourceCulture' => true]) : 1 + ); + + $form->setWidget('add_technical_metadata', new sfWidgetFormInputCheckbox()); + $form->setValidator('add_technical_metadata', new sfValidatorBoolean()); + $form->setDefault('add_technical_metadata', + QubitSetting::getByName('add_technical_metadata') ? + QubitSetting::getByName('add_technical_metadata')->getValue(['sourceCulture' => true]) : 1 + ); + + return $form; + } + + protected function processForm() + { + $settings = [ + 'metadata_extraction_enabled', + 'extract_exif', + 'extract_iptc', + 'extract_xmp', + 'overwrite_title', + 'overwrite_description', + 'auto_generate_keywords', + 'extract_gps_coordinates', + 'add_technical_metadata', + ]; + + foreach ($settings as $name) { + if (null !== $setting = QubitSetting::getByName($name)) { + $setting->setValue($this->form->getValue($name), ['sourceCulture' => true]); + } else { + $setting = new QubitSetting(); + $setting->name = $name; + $setting->value = $this->form->getValue($name); + } + $setting->save(); + } + } +} diff --git a/plugins/arMetadataExtractionPlugin/modules/settings/templates/metadataExtractionSuccess.php b/plugins/arMetadataExtractionPlugin/modules/settings/templates/metadataExtractionSuccess.php new file mode 100644 index 0000000000..7f743445d0 --- /dev/null +++ b/plugins/arMetadataExtractionPlugin/modules/settings/templates/metadataExtractionSuccess.php @@ -0,0 +1,135 @@ + + +

+ +renderGlobalErrors(); ?> + +
+ +
+ +
+ + +
+ renderLabel(); ?> + renderError(); ?> +
+ +
+
+ +
+
+
+ +
+ + +
+ renderError(); ?> +
+ + renderLabel(__('Extract EXIF metadata')); ?> +
+
+ +
+
+ +
+ renderError(); ?> +
+ + renderLabel(__('Extract IPTC metadata')); ?> +
+
+ +
+
+ +
+ renderError(); ?> +
+ + renderLabel(__('Extract XMP metadata')); ?> +
+
+ +
+
+
+ +
+ + +
+ renderError(); ?> +
+ + renderLabel(__('Always overwrite title')); ?> +
+
+ +
+
+ +
+ renderError(); ?> +
+ + renderLabel(__('Always overwrite description')); ?> +
+
+ +
+
+
+ +
+ + +
+ renderError(); ?> +
+ + renderLabel(__('Auto-generate keywords')); ?> +
+
+ +
+
+ +
+ renderError(); ?> +
+ + renderLabel(__('Extract GPS coordinates')); ?> +
+
+ +
+
+ +
+ renderError(); ?> +
+ + renderLabel(__('Add technical metadata')); ?> +
+
+ +
+
+
+ +
+ +
+
    +
  • +
  • 'settings', 'action' => 'list'], ['class' => 'c-btn']); ?>
  • +
+
+ +
diff --git a/plugins/arMetadataExtractionPlugin/package.sh b/plugins/arMetadataExtractionPlugin/package.sh new file mode 100644 index 0000000000..cd5e2ef324 --- /dev/null +++ b/plugins/arMetadataExtractionPlugin/package.sh @@ -0,0 +1,37 @@ +#!/bin/bash + +# Package script for arMetadataExtractionPlugin +# Creates a distributable archive + +PLUGIN_NAME="arMetadataExtractionPlugin" +VERSION="1.0.0" +ARCHIVE_NAME="${PLUGIN_NAME}-v${VERSION}.tar.gz" + +echo "Creating package: $ARCHIVE_NAME" + +# Create temporary directory +TEMP_DIR=$(mktemp -d) +PLUGIN_DIR="$TEMP_DIR/$PLUGIN_NAME" + +# Copy plugin files +mkdir -p "$PLUGIN_DIR" +cp -r config lib modules README.md install.sh "$PLUGIN_DIR/" + +# Create archive +cd "$TEMP_DIR" +tar czf "$ARCHIVE_NAME" "$PLUGIN_NAME" + +# Move archive to original directory +mv "$ARCHIVE_NAME" "$OLDPWD/" + +# Clean up +rm -rf "$TEMP_DIR" + +echo "Package created: $ARCHIVE_NAME" +echo "Size: $(du -h $OLDPWD/$ARCHIVE_NAME | cut -f1)" +echo "" +echo "To install on your AtoM server:" +echo "1. Upload $ARCHIVE_NAME to your server" +echo "2. Extract: tar xzf $ARCHIVE_NAME" +echo "3. cd $PLUGIN_NAME" +echo "4. sudo ./install.sh" diff --git a/plugins/arMetadataExtractionPlugin/test.sh b/plugins/arMetadataExtractionPlugin/test.sh new file mode 100644 index 0000000000..ba8c4e671a --- /dev/null +++ b/plugins/arMetadataExtractionPlugin/test.sh @@ -0,0 +1,148 @@ +#!/bin/bash + +# Test script for arMetadataExtractionPlugin +# Verifies installation and functionality + +set -e + +# Colors +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +NC='\033[0m' + +# Configuration +ATOM_PATH="/usr/share/nginx/atom" +PLUGIN_NAME="arMetadataExtractionPlugin" + +print_test() { + echo -e "${YELLOW}[TEST]${NC} $1" +} + +print_pass() { + echo -e "${GREEN}[PASS]${NC} $1" +} + +print_fail() { + echo -e "${RED}[FAIL]${NC} $1" +} + +echo "=========================================" +echo "arMetadataExtractionPlugin Test Suite" +echo "=========================================" +echo "" + +# Test 1: Check plugin directory exists +print_test "Checking plugin directory..." +if [ -d "$ATOM_PATH/plugins/$PLUGIN_NAME" ]; then + print_pass "Plugin directory exists" +else + print_fail "Plugin directory not found at $ATOM_PATH/plugins/$PLUGIN_NAME" + exit 1 +fi + +# Test 2: Check required files +print_test "Checking required plugin files..." +REQUIRED_FILES=( + "$ATOM_PATH/plugins/$PLUGIN_NAME/config/arMetadataExtractionPluginConfiguration.class.php" + "$ATOM_PATH/plugins/$PLUGIN_NAME/lib/arMetadataExtractor.class.php" + "$ATOM_PATH/plugins/$PLUGIN_NAME/README.md" +) + +for file in "${REQUIRED_FILES[@]}"; do + if [ -f "$file" ]; then + print_pass "Found: $(basename $file)" + else + print_fail "Missing: $file" + exit 1 + fi +done + +# Test 3: Check action file is updated +print_test "Checking digital object action file..." +ACTION_FILE="$ATOM_PATH/apps/qubit/modules/object/actions/addDigitalObjectAction.class.php" +if grep -q "arMetadataExtractor" "$ACTION_FILE"; then + print_pass "Action file has been updated with plugin code" +else + print_fail "Action file not updated. Plugin may not work." + exit 1 +fi + +# Test 4: Check exiftool installation +print_test "Checking exiftool installation..." +if command -v exiftool &> /dev/null; then + VERSION=$(exiftool -ver) + print_pass "exiftool installed (version $VERSION)" +else + print_fail "exiftool not installed. Run: apt-get install libimage-exiftool-perl" +fi + +# Test 5: Check arEmbeddedMetadataParser +print_test "Checking arEmbeddedMetadataParser..." +if [ -f "$ATOM_PATH/lib/helper/arEmbeddedMetadataParser.class.php" ]; then + print_pass "arEmbeddedMetadataParser found" +else + print_fail "arEmbeddedMetadataParser not found - plugin functionality limited" +fi + +# Test 6: Check file permissions +print_test "Checking file permissions..." +PLUGIN_DIR="$ATOM_PATH/plugins/$PLUGIN_NAME" +OWNER=$(stat -c '%U' "$PLUGIN_DIR") +if [ "$OWNER" = "www-data" ]; then + print_pass "Plugin owned by www-data" +else + print_fail "Plugin not owned by www-data (owned by $OWNER)" + echo " Fix with: sudo chown -R www-data:www-data $PLUGIN_DIR" +fi + +# Test 7: Check PHP syntax +print_test "Checking PHP syntax..." +ERROR_COUNT=0 +for phpfile in $(find "$PLUGIN_DIR" -name "*.php"); do + if php -l "$phpfile" > /dev/null 2>&1; then + echo -e " ${GREEN}✓${NC} $(basename $phpfile)" + else + echo -e " ${RED}✗${NC} $(basename $phpfile) - Syntax error" + ERROR_COUNT=$((ERROR_COUNT + 1)) + fi +done + +if [ $ERROR_COUNT -eq 0 ]; then + print_pass "All PHP files have valid syntax" +else + print_fail "$ERROR_COUNT PHP files have syntax errors" +fi + +# Test 8: Database settings check +print_test "Checking database settings..." +if [ -f "$ATOM_PATH/config/config.php" ]; then + print_pass "Database configuration found" + echo " Note: Cannot verify settings without database access" +else + print_fail "Database configuration not found" +fi + +echo "" +echo "=========================================" +echo "Test Summary" +echo "=========================================" +echo "" + +if [ $? -eq 0 ]; then + print_pass "All tests passed! Plugin appears to be installed correctly." + echo "" + echo "To complete setup:" + echo "1. Clear Symfony cache: cd $ATOM_PATH && sudo -u www-data php symfony cc" + echo "2. Log in to AtoM as administrator" + echo "3. Navigate to Admin → Settings → Metadata extraction settings" + echo "4. Configure and enable the plugin" + echo "5. Test by uploading an image with EXIF metadata" +else + print_fail "Some tests failed. Please fix the issues above." +fi + +echo "" +echo "For manual testing, create a test image with metadata:" +echo " exiftool -Artist='Test Artist' -Copyright='Test Copyright' test.jpg" +echo "Then upload it to AtoM and check if metadata is extracted."