--- a/lpagery/lpagery.php
+++ b/lpagery/lpagery.php
@@ -4,7 +4,7 @@
Plugin Name: LPagery
Plugin URI: https://lpagery.io/
Description: Create hundreds or even thousands of landingpages for local businesses, services etc.
-Version: 2.4.9
+Version: 2.4.10
Author: LPagery
License: GPLv2 or later
*/
@@ -14,6 +14,7 @@
use LPageryfactoriesGoogleSheetSyncProcessHandlerFactory;
use LPageryioMapper;
use LPagerymodelProcessSheetSyncParams;
+use LPageryserviceimage_lookupAttachmentBasenameService;
use LPageryserviceInstallationDateHandler;
use LPageryservicesettingsSettingsController;
use LPageryservicesheet_syncGoogleSheetQueueWorkerFactory;
@@ -589,6 +590,127 @@
10,
2
);
+ // Keep basename lookup table in sync for fast attachment searches
+ // Note: add_attachment fires BEFORE _wp_attached_file metadata is set, so we use hooks that fire after
+ // Hook into attachment metadata generation (fires after _wp_attached_file is set for new uploads)
+ add_filter(
+ 'wp_generate_attachment_metadata',
+ 'lpagery_on_attachment_metadata_generated',
+ 10,
+ 2
+ );
+ function lpagery_on_attachment_metadata_generated( $metadata, $attachment_id ) {
+ $file = get_post_meta( $attachment_id, '_wp_attached_file', true );
+ if ( $file ) {
+ AttachmentBasenameService::get_instance()->insert( $attachment_id, $file );
+ }
+ return $metadata;
+ }
+
+ // Hook into attachment metadata updates
+ add_filter(
+ 'wp_update_attachment_metadata',
+ 'lpagery_on_attachment_metadata_updated',
+ 10,
+ 2
+ );
+ function lpagery_on_attachment_metadata_updated( $metadata, $attachment_id ) {
+ $file = get_post_meta( $attachment_id, '_wp_attached_file', true );
+ if ( $file ) {
+ AttachmentBasenameService::get_instance()->insert( $attachment_id, $file );
+ }
+ return $metadata;
+ }
+
+ // Hook into post meta addition (catches _wp_attached_file being set)
+ add_action(
+ 'added_post_meta',
+ 'lpagery_on_attachment_file_meta_added',
+ 10,
+ 4
+ );
+ function lpagery_on_attachment_file_meta_added(
+ $meta_id,
+ $post_id,
+ $meta_key,
+ $meta_value
+ ) {
+ if ( $meta_key === '_wp_attached_file' && $meta_value ) {
+ AttachmentBasenameService::get_instance()->insert( $post_id, $meta_value );
+ }
+ }
+
+ // Hook into post meta updates (catches _wp_attached_file being updated)
+ add_action(
+ 'updated_post_meta',
+ 'lpagery_on_attachment_file_meta_updated',
+ 10,
+ 4
+ );
+ function lpagery_on_attachment_file_meta_updated(
+ $meta_id,
+ $post_id,
+ $meta_key,
+ $meta_value
+ ) {
+ if ( $meta_key === '_wp_attached_file' && $meta_value ) {
+ AttachmentBasenameService::get_instance()->insert( $post_id, $meta_value );
+ }
+ }
+
+ // Hook into attachment edits
+ add_action( 'edit_attachment', 'lpagery_on_edit_attachment' );
+ function lpagery_on_edit_attachment( $attachment_id ) {
+ $file = get_post_meta( $attachment_id, '_wp_attached_file', true );
+ if ( $file ) {
+ AttachmentBasenameService::get_instance()->insert( $attachment_id, $file );
+ }
+ }
+
+ // Hook into attachment updates (fires when attachment post is updated)
+ add_action(
+ 'attachment_updated',
+ 'lpagery_on_attachment_updated',
+ 10,
+ 3
+ );
+ function lpagery_on_attachment_updated( $post_id, $post_after, $post_before ) {
+ $file = get_post_meta( $post_id, '_wp_attached_file', true );
+ if ( $file ) {
+ AttachmentBasenameService::get_instance()->insert( $post_id, $file );
+ }
+ }
+
+ // Hook into REST API attachment creation/updates
+ add_action(
+ 'rest_after_insert_attachment',
+ 'lpagery_on_rest_attachment_insert',
+ 10,
+ 3
+ );
+ function lpagery_on_rest_attachment_insert( $attachment, $request, $creating ) {
+ $file = get_post_meta( $attachment->ID, '_wp_attached_file', true );
+ if ( $file ) {
+ AttachmentBasenameService::get_instance()->insert( $attachment->ID, $file );
+ }
+ }
+
+ // Clean up basename lookup table when attachment is deleted
+ add_action( 'delete_attachment', 'lpagery_delete_attachment_basename' );
+ function lpagery_delete_attachment_basename( $attachment_id ) {
+ AttachmentBasenameService::get_instance()->delete( $attachment_id );
+ }
+
+ // Daily cron job to backfill the attachment basename lookup table
+ add_action( 'lpagery_backfill_attachment_basename', 'lpagery_backfill_attachment_basename' );
+ function lpagery_backfill_attachment_basename() {
+ AttachmentBasenameService::get_instance()->backfill();
+ }
+
+ // Schedule the daily backfill cron if not already scheduled
+ if ( !wp_next_scheduled( 'lpagery_backfill_attachment_basename' ) ) {
+ wp_schedule_event( time(), 'daily', 'lpagery_backfill_attachment_basename' );
+ }
function lpagery_save_replace_filename_field( $post, $attachment ) {
if ( isset( $attachment['lpagery_replace_filename'] ) ) {
// Update or add the custom field value
--- a/lpagery/src/data/DbDeltaExecutor.php
+++ b/lpagery/src/data/DbDeltaExecutor.php
@@ -97,6 +97,15 @@
hashed_payload varchar(255) null,
KEY sync_queue_process_id (process_id),
PRIMARY KEY (id)
+ ) $charset_collate;",
+
+ "CREATE TABLE {$prefix}lpagery_attachment_basename (
+ attachment_id BIGINT UNSIGNED NOT NULL,
+ basename VARCHAR(191) NOT NULL,
+ basename_no_ext VARCHAR(191) NOT NULL,
+ KEY idx_basename (basename),
+ KEY idx_basename_no_ext (basename_no_ext),
+ PRIMARY KEY (attachment_id)
) $charset_collate;"
];
--- a/lpagery/src/data/LPageryDatabaseMigrator.php
+++ b/lpagery/src/data/LPageryDatabaseMigrator.php
@@ -4,8 +4,8 @@
use LPageryfactoriesInputParamProviderFactory;
use LPageryfactoriesSubstitutionHandlerFactory;
+use LPageryserviceimage_lookupAttachmentBasenameService;
use LPageryutilsUtils;
-use Throwable;
class LPageryDatabaseMigrator
{
@@ -304,5 +304,25 @@
$wpdb->query("create index process_post_hashed_payload_process_id on $table_name_process_post (hashed_payload, lpagery_process_id)");
update_option("lpagery_database_version", 14);
}
+
+ if ($db_version < 15 && $this->lpagery_table_exists_migrate($table_name_process_post)) {
+ $table_name_attachment_basename = $wpdb->prefix . 'lpagery_attachment_basename';
+
+ $sql_attachment_basename = "CREATE TABLE {$table_name_attachment_basename} (
+ attachment_id BIGINT UNSIGNED NOT NULL,
+ basename VARCHAR(191) NOT NULL,
+ basename_no_ext VARCHAR(191) NOT NULL,
+ KEY idx_basename (basename),
+ KEY idx_basename_no_ext (basename_no_ext),
+ PRIMARY KEY (attachment_id)
+ ) $charset_collate";
+
+ $wpdb->query($sql_attachment_basename);
+
+ // Backfill from existing attachments
+ AttachmentBasenameService::get_instance()->backfill();
+
+ update_option("lpagery_database_version", 15);
+ }
}
}
No newline at end of file
--- a/lpagery/src/io/AjaxActions.php
+++ b/lpagery/src/io/AjaxActions.php
@@ -15,9 +15,22 @@
use LPageryioCreatePageDebugger;
use LPageryiosuiteSuiteClient;
use LPagerymodelProcessSheetSyncParams;
+use LPageryserviceimage_lookupAttachmentBasenameService;
use LPageryservicesettingsSettingsController;
use LPageryutilsUtils;
+function lpagery_require_admin() {
+ if (!current_user_can('manage_options')) {
+ wp_send_json(array("success" => false, "exception" => 'You do not have permission to perform this action.'));
+ }
+}
+
+function lpagery_require_editor() {
+ if (!current_user_can('edit_pages')) {
+ wp_send_json(array("success" => false, "exception" => 'You do not have permission to perform this action.'));
+ }
+}
+
add_action('wp_ajax_lpagery_sanitize_slug', 'LPagerylpagery_sanitize_slug');
function lpagery_sanitize_slug()
@@ -53,6 +66,7 @@
global $wpdb;
$nonce_validity = check_ajax_referer('lpagery_ajax');
+ lpagery_require_editor();
// Check if debug mode is enabled
$debug_mode = isset($_POST['debug_mode']) && rest_sanitize_boolean($_POST['debug_mode']);
@@ -262,6 +276,7 @@
function lpagery_upsert_process()
{
check_ajax_referer('lpagery_ajax');
+ lpagery_require_editor();
try {
$processController = ProcessController::get_instance();
$upsertParams = LPagerymodelUpsertProcessParams::fromArray($_POST, "plugin");
@@ -367,6 +382,7 @@
function lpagery_create_onboarding_template_page()
{
check_ajax_referer('lpagery_ajax');
+ lpagery_require_editor();
$utilityController = UtilityController::get_instance();
$result = $utilityController->createOnboardingTemplatePage();
@@ -378,6 +394,7 @@
function lpagery_assign_page_set_to_me()
{
check_ajax_referer('lpagery_ajax');
+ lpagery_require_editor();
try {
$process_id = isset($_POST['process_id']) ? (int)$_POST['process_id'] : null;
if (!$process_id) {
@@ -398,6 +415,7 @@
function lpagery_reset_data()
{
check_ajax_referer('lpagery_ajax');
+ lpagery_require_admin();
// Get delete_pages parameter
$delete_pages = isset($_POST['delete_pages']) ? rest_sanitize_boolean($_POST['delete_pages']) : false;
@@ -412,6 +430,7 @@
function lpagery_update_process_managing_system_ajax()
{
check_ajax_referer('lpagery_ajax');
+ lpagery_require_editor();
$id = isset($_POST['id']) ? (int)$_POST['id'] : 0;
$LPageryDao = LPageryDao::get_instance();
@@ -498,6 +517,7 @@
function lpagery_trigger_look_sync_ajax()
{
check_ajax_referer('lpagery_ajax');
+ lpagery_require_editor();
try {
$page_set_id = isset($_POST['page_set_id']) ? intval($_POST['page_set_id']) : 0;
@@ -538,6 +558,7 @@
function lpagery_repair_database_schema_ajax()
{
check_ajax_referer('lpagery_ajax');
+ lpagery_require_admin();
$dbDeltaExecutor = new DbDeltaExecutor();
$error = $dbDeltaExecutor->run();
@@ -545,8 +566,10 @@
wp_send_json(array("success" => false,
"exception" => $error));
} else {
+ $rows_inserted = AttachmentBasenameService::get_instance()->backfill();
+
wp_send_json(array("success" => true,
- "message" => 'Database schema repaired successfully.'));
+ "message" => "Database schema repaired successfully. Attachment basename index updated ({$rows_inserted} entries)."));
}
}
--- a/lpagery/src/service/image_lookup/AttachmentBasenameService.php
+++ b/lpagery/src/service/image_lookup/AttachmentBasenameService.php
@@ -0,0 +1,268 @@
+<?php
+
+namespace LPageryserviceimage_lookup;
+
+class AttachmentBasenameService
+{
+ private static ?AttachmentBasenameService $instance = null;
+
+ public static function get_instance(): AttachmentBasenameService
+ {
+ if (self::$instance === null) {
+ self::$instance = new AttachmentBasenameService();
+ }
+ return self::$instance;
+ }
+
+ /**
+ * Get the basename lookup table name
+ */
+ public function get_table_name(): string
+ {
+ global $wpdb;
+ return $wpdb->prefix . 'lpagery_attachment_basename';
+ }
+
+ /**
+ * Insert or update an attachment in the basename lookup table
+ */
+ public function insert(int $attachment_id, string $filepath): void
+ {
+ global $wpdb;
+ $basename = basename($filepath);
+ $basename_no_ext = pathinfo($basename, PATHINFO_FILENAME);
+
+ $wpdb->replace(
+ $this->get_table_name(),
+ [
+ 'attachment_id' => $attachment_id,
+ 'basename' => $basename,
+ 'basename_no_ext' => $basename_no_ext
+ ],
+ ['%d', '%s', '%s']
+ );
+ }
+
+ /**
+ * Delete an attachment from the basename lookup table
+ */
+ public function delete(int $attachment_id): void
+ {
+ global $wpdb;
+ $wpdb->delete(
+ $this->get_table_name(),
+ ['attachment_id' => $attachment_id],
+ ['%d']
+ );
+ }
+
+ /**
+ * Search for attachments by exact basename (with extension)
+ *
+ * @return array Array of objects with ID property
+ */
+ public function search_by_basename(string $basename): array
+ {
+ global $wpdb;
+ $table = $this->get_table_name();
+
+ $query = $wpdb->prepare(
+ "SELECT ab.attachment_id as ID
+ FROM {$table} ab
+ INNER JOIN {$wpdb->posts} p ON p.ID = ab.attachment_id
+ WHERE p.post_type = 'attachment'
+ AND p.post_status IN ('inherit','private')
+ AND ab.basename = %s
+ LIMIT 20",
+ $basename
+ );
+
+ return $wpdb->get_results($query) ?: [];
+ }
+
+ /**
+ * Search for attachments by basename without extension
+ *
+ * @return array Array of objects with ID property
+ */
+ public function search_by_basename_no_ext(string $basename_no_ext): array
+ {
+ global $wpdb;
+ $table = $this->get_table_name();
+
+ $query = $wpdb->prepare(
+ "SELECT ab.attachment_id as ID
+ FROM {$table} ab
+ INNER JOIN {$wpdb->posts} p ON p.ID = ab.attachment_id
+ WHERE p.post_type = 'attachment'
+ AND p.post_status IN ('inherit','private')
+ AND ab.basename_no_ext = %s
+ LIMIT 20",
+ $basename_no_ext
+ );
+
+ return $wpdb->get_results($query) ?: [];
+ }
+
+ /**
+ * Search for attachments - automatically chooses the right column based on whether
+ * the search term has an extension
+ *
+ * @return array Array of objects with ID property
+ */
+ public function search(string $name): array
+ {
+ $search_basename = basename(urldecode($name));
+ $has_extension = pathinfo($search_basename, PATHINFO_EXTENSION) !== '';
+
+ if ($has_extension) {
+ return $this->search_by_basename($search_basename);
+ } else {
+ return $this->search_by_basename_no_ext($search_basename);
+ }
+ }
+
+ /**
+ * Search for attachments with multiple filename variations.
+ * Tries: exact basename, basename_no_ext, jpg/jpeg equivalents, and -scaled removal.
+ *
+ * @return array Array of objects with ID property
+ */
+ public function search_with_variations(string $name): array
+ {
+ $variations = $this->get_search_variations($name);
+
+ foreach ($variations as $variation) {
+ // Try exact basename match first
+ $results = $this->search_by_basename($variation['basename']);
+ if (!empty($results)) {
+ return $results;
+ }
+
+ // Try basename without extension
+ $results = $this->search_by_basename_no_ext($variation['basename_no_ext']);
+ if (!empty($results)) {
+ return $results;
+ }
+ }
+
+ return [];
+ }
+
+ /**
+ * Generate all search variations for a filename.
+ * Includes: original, jpg/jpeg swapped, -scaled removed, and combinations.
+ *
+ * @return array Array of variations, each with 'basename' and 'basename_no_ext' keys
+ */
+ private function get_search_variations(string $name): array
+ {
+ $search_basename = basename(urldecode($name));
+ $variations = [];
+ $seen = [];
+
+ // Helper to add a variation if not already seen
+ $addVariation = function(string $basename) use (&$variations, &$seen) {
+ $basename_no_ext = pathinfo($basename, PATHINFO_FILENAME);
+ $key = strtolower($basename . '|' . $basename_no_ext);
+ if (!isset($seen[$key])) {
+ $seen[$key] = true;
+ $variations[] = [
+ 'basename' => $basename,
+ 'basename_no_ext' => $basename_no_ext
+ ];
+ }
+ };
+
+ // Get all base variations (original, jpg/jpeg swapped, -scaled removed, combined)
+ $base_variations = $this->get_base_variations($search_basename);
+
+ foreach ($base_variations as $basename) {
+ $addVariation($basename);
+ }
+
+ return $variations;
+ }
+
+ /**
+ * Generate base filename variations (jpg/jpeg swap and -scaled removal).
+ *
+ * @return array Array of filename strings
+ */
+ private function get_base_variations(string $basename): array
+ {
+ $variations = [$basename];
+
+ $extension = strtolower(pathinfo($basename, PATHINFO_EXTENSION));
+ $filename = pathinfo($basename, PATHINFO_FILENAME);
+
+ // Handle jpg <-> jpeg equivalence
+ $jpeg_variant = null;
+ if ($extension === 'jpg') {
+ $jpeg_variant = $filename . '.jpeg';
+ $variations[] = $jpeg_variant;
+ } elseif ($extension === 'jpeg') {
+ $jpeg_variant = $filename . '.jpg';
+ $variations[] = $jpeg_variant;
+ }
+
+ // Handle -scaled removal
+ if (str_ends_with(strtolower($filename), '-scaled')) {
+ $filename_no_scaled = substr($filename, 0, -7); // Remove '-scaled'
+
+ // Add without -scaled (with original extension)
+ if ($extension) {
+ $no_scaled_variant = $filename_no_scaled . '.' . $extension;
+ $variations[] = $no_scaled_variant;
+
+ // Also add jpg/jpeg variant without -scaled
+ if ($extension === 'jpg') {
+ $variations[] = $filename_no_scaled . '.jpeg';
+ } elseif ($extension === 'jpeg') {
+ $variations[] = $filename_no_scaled . '.jpg';
+ }
+ } else {
+ $variations[] = $filename_no_scaled;
+ }
+ }
+
+ return array_unique($variations);
+ }
+
+ /**
+ * Backfill the basename lookup table from all existing attachments.
+ * Uses INSERT IGNORE to avoid duplicates.
+ *
+ * @return int Number of rows inserted
+ */
+ public function backfill(): int
+ {
+ global $wpdb;
+ $table = $this->get_table_name();
+
+ // Use a subquery to extract basename first, then compute basename_no_ext
+ // by extracting text before the LAST dot (matching PHP's pathinfo behavior).
+ // For files like "image.backup.jpg", this returns "image.backup" not "image".
+ $wpdb->query("
+ INSERT IGNORE INTO {$table} (attachment_id, basename, basename_no_ext)
+ SELECT
+ attachment_id,
+ basename,
+ IF(
+ LOCATE('.', basename) = 0,
+ basename,
+ SUBSTRING_INDEX(basename, '.', CHAR_LENGTH(basename) - CHAR_LENGTH(REPLACE(basename, '.', '')))
+ ) as basename_no_ext
+ FROM (
+ SELECT
+ pm.post_id as attachment_id,
+ SUBSTRING_INDEX(pm.meta_value, '/', -1) as basename
+ FROM {$wpdb->postmeta} pm
+ WHERE pm.meta_key = '_wp_attached_file'
+ ) as derived
+ ");
+
+ return $wpdb->rows_affected;
+ }
+}
+
--- a/lpagery/src/service/settings/Settings.php
+++ b/lpagery/src/service/settings/Settings.php
@@ -4,6 +4,7 @@
{
public bool $spintax;
public bool $image_processing;
+ public bool $image_partial_match;
public array $custom_post_types;
public int $author_id;
public string $hierarchical_taxonomy_handling;
--- a/lpagery/src/service/settings/SettingsController.php
+++ b/lpagery/src/service/settings/SettingsController.php
@@ -79,6 +79,7 @@
$userSettings = [
'spintax' => $settings->spintax,
'image_processing' => $settings->image_processing,
+ 'image_partial_match' => $settings->image_partial_match,
'custom_post_types' => $settings->custom_post_types,
'hierarchical_taxonomy_handling' => $settings->hierarchical_taxonomy_handling,
'author_id' => $settings->author_id,
@@ -141,6 +142,7 @@
$userOptions = [
'spintax' => false,
'image_processing' => lpagery_fs()->is_plan_or_trial("extended"),
+ 'image_partial_match' => true,
'custom_post_types' => [],
'author_id' => get_current_user_id(),
];
@@ -152,6 +154,7 @@
$settings = new Settings();
$settings->spintax = false;
$settings->image_processing = false;
+ $settings->image_partial_match = true;
$settings->custom_post_types = [];
$settings->author_id = get_current_user_id();
$settings->google_sheet_sync_interval = "hourly";
@@ -187,6 +190,7 @@
$settings = new Settings();
$settings->spintax = filter_var($userOptions['spintax'] ?? false, FILTER_VALIDATE_BOOLEAN);
$settings->image_processing = filter_var($userOptions['image_processing'] ?? lpagery_fs()->is_plan_or_trial("extended"), FILTER_VALIDATE_BOOLEAN);
+ $settings->image_partial_match = filter_var($userOptions['image_partial_match'] ?? true, FILTER_VALIDATE_BOOLEAN);
$settings->custom_post_types = $custom_post_types;
$settings->hierarchical_taxonomy_handling = $userOptions['hierarchical_taxonomy_handling'] ?? 'last';
$settings->author_id = $userOptions['author_id'] ?? get_current_user_id();
@@ -248,6 +252,17 @@
}
/**
+ * Checks if image partial match (LIKE fallback) is enabled
+ */
+ public function isImagePartialMatchEnabled($processId = null): bool
+ {
+ $userId = $this->getUserId($processId);
+ $userSettings = $this->getUserSettings($userId);
+
+ return filter_var($userSettings['image_partial_match'] ?? true, FILTER_VALIDATE_BOOLEAN);
+ }
+
+ /**
* Checks if spintax is enabled
*/
public function isSpintaxEnabled($processId = null): bool
@@ -337,6 +352,7 @@
$userOptions = [
'spintax' => false,
'image_processing' => lpagery_fs()->is_plan_or_trial("extended"),
+ 'image_partial_match' => true,
'custom_post_types' => [],
'hierarchical_taxonomy_handling' => 'last',
'author_id' => get_current_user_id(),
--- a/lpagery/vendor/composer/autoload_classmap.php
+++ b/lpagery/vendor/composer/autoload_classmap.php
@@ -55,6 +55,7 @@
'LPagery\service\duplicates\DuplicateSlugProvider' => $baseDir . '/src/service/duplicates/DuplicateSlugProvider.php',
'LPagery\service\duplicates\DuplicateSlugResult' => $baseDir . '/src/service/duplicates/DuplicateSlugResult.php',
'LPagery\service\duplicates\ExistingSlugResult' => $baseDir . '/src/service/duplicates/ExistingSlugResult.php',
+ 'LPagery\service\image_lookup\AttachmentBasenameService' => $baseDir . '/src/service/image_lookup/AttachmentBasenameService.php',
'LPagery\service\media\AttachmentHelper' => $baseDir . '/src/service/media/AttachmentHelper.php',
'LPagery\service\media\AttachmentMetadataUpdater' => $baseDir . '/src/service/media/AttachmentMetadataUpdater.php',
'LPagery\service\media\AttachmentReplacementProvider' => $baseDir . '/src/service/media/AttachmentReplacementProvider.php',
--- a/lpagery/vendor/composer/autoload_static.php
+++ b/lpagery/vendor/composer/autoload_static.php
@@ -83,6 +83,7 @@
'LPagery\service\duplicates\DuplicateSlugProvider' => __DIR__ . '/../..' . '/src/service/duplicates/DuplicateSlugProvider.php',
'LPagery\service\duplicates\DuplicateSlugResult' => __DIR__ . '/../..' . '/src/service/duplicates/DuplicateSlugResult.php',
'LPagery\service\duplicates\ExistingSlugResult' => __DIR__ . '/../..' . '/src/service/duplicates/ExistingSlugResult.php',
+ 'LPagery\service\image_lookup\AttachmentBasenameService' => __DIR__ . '/../..' . '/src/service/image_lookup/AttachmentBasenameService.php',
'LPagery\service\media\AttachmentHelper' => __DIR__ . '/../..' . '/src/service/media/AttachmentHelper.php',
'LPagery\service\media\AttachmentMetadataUpdater' => __DIR__ . '/../..' . '/src/service/media/AttachmentMetadataUpdater.php',
'LPagery\service\media\AttachmentReplacementProvider' => __DIR__ . '/../..' . '/src/service/media/AttachmentReplacementProvider.php',