* @author Jordi Boggiano */ class PluginManager { /** @var Composer */ protected $composer; /** @var IOInterface */ protected $io; /** @var PartialComposer|null */ protected $globalComposer; /** @var VersionParser */ protected $versionParser; /** @var bool|'local'|'global' */ protected $disablePlugins = false; /** @var array */ protected $plugins = []; /** @var array */ protected $registeredPlugins = []; /** @var array|null */ private $allowPluginRules; /** @var array|null */ private $allowGlobalPluginRules; /** @var bool */ private $runningInGlobalDir = false; /** @var int */ private static $classCounter = 0; /** * @param bool|'local'|'global' $disablePlugins Whether plugins should not be loaded, can be set to local or global to only disable local/global plugins */ public function __construct( IOInterface $io, Composer $composer, ?PartialComposer $globalComposer = null, $disablePlugins = false, ) { $this->io = $io; $this->composer = $composer; $this->globalComposer = $globalComposer; $this->versionParser = new VersionParser(); $this->disablePlugins = $disablePlugins; $this->allowPluginRules = $this->parseAllowedPlugins($composer->getConfig()->get('allow-plugins'), $composer->getLocker()); $this->allowGlobalPluginRules = $this->parseAllowedPlugins($globalComposer !== null ? $globalComposer->getConfig()->get('allow-plugins') : false); } public function setRunningInGlobalDir(bool $runningInGlobalDir): void { $this->runningInGlobalDir = $runningInGlobalDir; } /** * Loads all plugins from currently installed plugin packages */ public function loadInstalledPlugins(): void { if (!$this->arePluginsDisabled('local')) { $repo = $this->composer->getRepositoryManager()->getLocalRepository(); $this->loadRepository($repo, false); } if ($this->globalComposer !== null && !$this->arePluginsDisabled('global')) { $this->loadRepository($this->globalComposer->getRepositoryManager()->getLocalRepository(), true); } } /** * Deactivate all plugins from currently installed plugin packages */ public function deactivateInstalledPlugins(): void { if (!$this->arePluginsDisabled('local')) { $repo = $this->composer->getRepositoryManager()->getLocalRepository(); $this->deactivateRepository($repo, false); } if ($this->globalComposer !== null && !$this->arePluginsDisabled('global')) { $this->deactivateRepository($this->globalComposer->getRepositoryManager()->getLocalRepository(), true); } } /** * Gets all currently active plugin instances * * @return array plugins */ public function getPlugins(): array { return $this->plugins; } /** * Gets global composer or null when main composer is not fully loaded */ public function getGlobalComposer(): ?PartialComposer { return $this->globalComposer; } /** * Register a plugin package, activate it etc. * * If it's of type composer-installer it is registered as an installer * instead for BC * * @param bool $failOnMissingClasses By default this silently skips plugins that can not be found, but if set to true it fails with an exception * @param bool $isGlobalPlugin Set to true to denote plugins which are installed in the global Composer directory * * @throws \UnexpectedValueException */ public function registerPackage( PackageInterface $package, bool $failOnMissingClasses = false, bool $isGlobalPlugin = false, ): void { if ($this->arePluginsDisabled($isGlobalPlugin ? 'global' : 'local')) { return; } if ($package->getType() === 'composer-plugin') { $requiresComposer = null; foreach ($package->getRequires() as $link) { /** @var Link $link */ if ('composer-plugin-api' === $link->getTarget()) { $requiresComposer = $link->getConstraint(); break; } } if (!$requiresComposer) { throw new \RuntimeException("Plugin ".$package->getName()." is missing a require statement for a version of the composer-plugin-api package."); } $currentPluginApiVersion = $this->getPluginApiVersion(); $currentPluginApiConstraint = new Constraint('==', $this->versionParser->normalize($currentPluginApiVersion)); if ($requiresComposer->getPrettyString() === $this->getPluginApiVersion()) { $this->io->writeError('The "' . $package->getName() . '" plugin requires composer-plugin-api '.$this->getPluginApiVersion().', this *WILL* break in the future and it should be fixed ASAP (require ^'.$this->getPluginApiVersion().' instead for example).'); } elseif (!$requiresComposer->matches($currentPluginApiConstraint)) { $this->io->writeError('The "' . $package->getName() . '" plugin '.($isGlobalPlugin || $this->runningInGlobalDir ? '(installed globally) ' : '').'was skipped because it requires a Plugin API version ("' . $requiresComposer->getPrettyString() . '") that does not match your Composer installation ("' . $currentPluginApiVersion . '"). You may need to run composer update with the "--no-plugins" option.'); return; } if ($package->getName() === 'symfony/flex' && Preg::isMatch('{^[0-9.]+$}', $package->getVersion()) && version_compare($package->getVersion(), '1.9.8', '<')) { $this->io->writeError('The "' . $package->getName() . '" plugin '.($isGlobalPlugin || $this->runningInGlobalDir ? '(installed globally) ' : '').'was skipped because it is not compatible with Composer 2+. Make sure to update it to version 1.9.8 or greater.'); return; } } if (!$this->isPluginAllowed($package->getName(), $isGlobalPlugin, true === ($package->getExtra()['plugin-optional'] ?? false))) { $this->io->writeError('Skipped loading "'.$package->getName() . '" '.($isGlobalPlugin || $this->runningInGlobalDir ? '(installed globally) ' : '').'as it is not in config.allow-plugins', true, IOInterface::DEBUG); return; } $oldInstallerPlugin = ($package->getType() === 'composer-installer'); if (isset($this->registeredPlugins[$package->getName()])) { return; } $extra = $package->getExtra(); if (empty($extra['class'])) { throw new \UnexpectedValueException('Error while installing '.$package->getPrettyName().', composer-plugin packages should have a class defined in their extra key to be usable.'); } $classes = is_array($extra['class']) ? $extra['class'] : [$extra['class']]; $localRepo = $this->composer->getRepositoryManager()->getLocalRepository(); $globalRepo = $this->globalComposer !== null ? $this->globalComposer->getRepositoryManager()->getLocalRepository() : null; $rootPackage = clone $this->composer->getPackage(); // clear files autoload rules from the root package as the root dependencies are not // necessarily all present yet when booting this runtime autoloader $rootPackageAutoloads = $rootPackage->getAutoload(); $rootPackageAutoloads['files'] = []; $rootPackage->setAutoload($rootPackageAutoloads); $rootPackageAutoloads = $rootPackage->getDevAutoload(); $rootPackageAutoloads['files'] = []; $rootPackage->setDevAutoload($rootPackageAutoloads); unset($rootPackageAutoloads); $rootPackageRepo = new RootPackageRepository($rootPackage); $installedRepo = new InstalledRepository([$localRepo, $rootPackageRepo]); if ($globalRepo) { $installedRepo->addRepository($globalRepo); } $autoloadPackages = [$package->getName() => $package]; $autoloadPackages = $this->collectDependencies($installedRepo, $autoloadPackages, $package); $generator = $this->composer->getAutoloadGenerator(); $autoloads = [[$rootPackage, '']]; foreach ($autoloadPackages as $autoloadPackage) { if ($autoloadPackage === $rootPackage) { continue; } $downloadPath = $this->getInstallPath($autoloadPackage, $globalRepo && $globalRepo->hasPackage($autoloadPackage)); $autoloads[] = [$autoloadPackage, $downloadPath]; } $map = $generator->parseAutoloads($autoloads, $rootPackage); $classLoader = $generator->createLoader($map, $this->composer->getConfig()->get('vendor-dir')); $classLoader->register(false); foreach ($map['files'] as $fileIdentifier => $file) { // exclude laminas/laminas-zendframework-bridge:src/autoload.php as it breaks Composer in some conditions // see https://github.com/composer/composer/issues/10349 and https://github.com/composer/composer/issues/10401 // this hack can be removed once this deprecated package stop being installed if ($fileIdentifier === '7e9bd612cc444b3eed788ebbe46263a0') { continue; } \Composer\Autoload\composerRequire($fileIdentifier, $file); } foreach ($classes as $class) { if (class_exists($class, false)) { $class = trim($class, '\\'); $path = $classLoader->findFile($class); $code = file_get_contents($path); $separatorPos = strrpos($class, '\\'); $className = $class; if ($separatorPos) { $className = substr($class, $separatorPos + 1); } $code = Preg::replace('{^((?:final\s+)?(?:\s*))class\s+('.preg_quote($className).')}mi', '$1class $2_composer_tmp'.self::$classCounter, $code, 1); $code = strtr($code, [ '__FILE__' => var_export($path, true), '__DIR__' => var_export(dirname($path), true), '__CLASS__' => var_export($class, true), ]); $code = Preg::replace('/^\s*<\?(php)?/i', '', $code, 1); eval($code); $class .= '_composer_tmp'.self::$classCounter; self::$classCounter++; } if ($oldInstallerPlugin) { if (!is_a($class, 'Composer\Installer\InstallerInterface', true)) { throw new \RuntimeException('Could not activate plugin "'.$package->getName().'" as "'.$class.'" does not implement Composer\Installer\InstallerInterface'); } $this->io->writeError('Loading "'.$package->getName() . '" '.($isGlobalPlugin || $this->runningInGlobalDir ? '(installed globally) ' : '').'which is a legacy composer-installer built for Composer 1.x, it is likely to cause issues as you are running Composer 2.x.'); $installer = new $class($this->io, $this->composer); $this->composer->getInstallationManager()->addInstaller($installer); $this->registeredPlugins[$package->getName()] = $installer; } elseif (class_exists($class)) { if (!is_a($class, 'Composer\Plugin\PluginInterface', true)) { throw new \RuntimeException('Could not activate plugin "'.$package->getName().'" as "'.$class.'" does not implement Composer\Plugin\PluginInterface'); } $plugin = new $class(); $this->addPlugin($plugin, $isGlobalPlugin, $package); $this->registeredPlugins[$package->getName()] = $plugin; } elseif ($failOnMissingClasses) { throw new \UnexpectedValueException('Plugin '.$package->getName().' could not be initialized, class not found: '.$class); } } } /** * Deactivates a plugin package * * If it's of type composer-installer it is unregistered from the installers * instead for BC * * @throws \UnexpectedValueException */ public function deactivatePackage(PackageInterface $package): void { if (!isset($this->registeredPlugins[$package->getName()])) { return; } $plugin = $this->registeredPlugins[$package->getName()]; unset($this->registeredPlugins[$package->getName()]); if ($plugin instanceof InstallerInterface) { $this->composer->getInstallationManager()->removeInstaller($plugin); } else { $this->removePlugin($plugin); } } /** * Uninstall a plugin package * * If it's of type composer-installer it is unregistered from the installers * instead for BC * * @throws \UnexpectedValueException */ public function uninstallPackage(PackageInterface $package): void { if (!isset($this->registeredPlugins[$package->getName()])) { return; } $plugin = $this->registeredPlugins[$package->getName()]; if ($plugin instanceof InstallerInterface) { $this->deactivatePackage($package); } else { unset($this->registeredPlugins[$package->getName()]); $this->removePlugin($plugin); $this->uninstallPlugin($plugin); } } /** * Returns the version of the internal composer-plugin-api package. */ protected function getPluginApiVersion(): string { return PluginInterface::PLUGIN_API_VERSION; } /** * Adds a plugin, activates it and registers it with the event dispatcher * * Ideally plugin packages should be registered via registerPackage, but if you use Composer * programmatically and want to register a plugin class directly this is a valid way * to do it. * * @param PluginInterface $plugin plugin instance * @param ?PackageInterface $sourcePackage Package from which the plugin comes from */ public function addPlugin( PluginInterface $plugin, bool $isGlobalPlugin = false, ?PackageInterface $sourcePackage = null, ): void { if ($this->arePluginsDisabled($isGlobalPlugin ? 'global' : 'local')) { return; } if ($sourcePackage === null) { trigger_error('Calling PluginManager::addPlugin without $sourcePackage is deprecated, if you are using this please get in touch with us to explain the use case', E_USER_DEPRECATED); } elseif (!$this->isPluginAllowed($sourcePackage->getName(), $isGlobalPlugin, true === ($sourcePackage->getExtra()['plugin-optional'] ?? false))) { $this->io->writeError('Skipped loading "'.get_class($plugin).' from '.$sourcePackage->getName() . '" '.($isGlobalPlugin || $this->runningInGlobalDir ? '(installed globally) ' : '').' as it is not in config.allow-plugins', true, IOInterface::DEBUG); return; } $details = []; if ($sourcePackage) { $details[] = 'from '.$sourcePackage->getName(); } if ($isGlobalPlugin || $this->runningInGlobalDir) { $details[] = 'installed globally'; } $this->io->writeError('Loading plugin '.get_class($plugin).($details ? ' ('.implode(', ', $details).')' : ''), true, IOInterface::DEBUG); $this->plugins[] = $plugin; $plugin->activate($this->composer, $this->io); if ($plugin instanceof EventSubscriberInterface) { $this->composer->getEventDispatcher()->addSubscriber($plugin); } } /** * Removes a plugin, deactivates it and removes any listener the plugin has set on the plugin instance * * Ideally plugin packages should be deactivated via deactivatePackage, but if you use Composer * programmatically and want to deregister a plugin class directly this is a valid way * to do it. * * @param PluginInterface $plugin plugin instance */ public function removePlugin(PluginInterface $plugin): void { $index = array_search($plugin, $this->plugins, true); if ($index === false) { return; } $this->io->writeError('Unloading plugin '.get_class($plugin), true, IOInterface::DEBUG); unset($this->plugins[$index]); $plugin->deactivate($this->composer, $this->io); $this->composer->getEventDispatcher()->removeListener($plugin); } /** * Notifies a plugin it is being uninstalled and should clean up * * Ideally plugin packages should be uninstalled via uninstallPackage, but if you use Composer * programmatically and want to deregister a plugin class directly this is a valid way * to do it. * * @param PluginInterface $plugin plugin instance */ public function uninstallPlugin(PluginInterface $plugin): void { $this->io->writeError('Uninstalling plugin '.get_class($plugin), true, IOInterface::DEBUG); $plugin->uninstall($this->composer, $this->io); } /** * Load all plugins and installers from a repository * * If a plugin requires another plugin, the required one will be loaded first * * Note that plugins in the specified repository that rely on events that * have fired prior to loading will be missed. This means you likely want to * call this method as early as possible. * * @param RepositoryInterface $repo Repository to scan for plugins to install * * @throws \RuntimeException */ private function loadRepository(RepositoryInterface $repo, bool $isGlobalRepo): void { $packages = $repo->getPackages(); $weights = []; foreach ($packages as $package) { if ($package->getType() === 'composer-plugin') { $extra = $package->getExtra(); if ($package->getName() === 'composer/installers' || true === ($extra['plugin-modifies-install-path'] ?? false)) { $weights[$package->getName()] = -10000; } } } $sortedPackages = PackageSorter::sortPackages($packages, $weights); foreach ($sortedPackages as $package) { if (!($package instanceof CompletePackage)) { continue; } if ('composer-plugin' === $package->getType()) { $this->registerPackage($package, false, $isGlobalRepo); // Backward compatibility } elseif ('composer-installer' === $package->getType()) { $this->registerPackage($package, false, $isGlobalRepo); } } } /** * Deactivate all plugins and installers from a repository * * If a plugin requires another plugin, the required one will be deactivated last * * @param RepositoryInterface $repo Repository to scan for plugins to install */ private function deactivateRepository(RepositoryInterface $repo, bool $isGlobalRepo): void { $packages = $repo->getPackages(); $sortedPackages = array_reverse(PackageSorter::sortPackages($packages)); foreach ($sortedPackages as $package) { if (!($package instanceof CompletePackage)) { continue; } if ('composer-plugin' === $package->getType()) { $this->deactivatePackage($package); // Backward compatibility } elseif ('composer-installer' === $package->getType()) { $this->deactivatePackage($package); } } } /** * Recursively generates a map of package names to packages for all deps * * @param InstalledRepository $installedRepo Set of local repos * @param array $collected Current state of the map for recursion * @param PackageInterface $package The package to analyze * * @return array Map of package names to packages */ private function collectDependencies( InstalledRepository $installedRepo, array $collected, PackageInterface $package, ): array { foreach ($package->getRequires() as $requireLink) { foreach ($installedRepo->findPackagesWithReplacersAndProviders($requireLink->getTarget()) as $requiredPackage) { if (!isset($collected[$requiredPackage->getName()])) { $collected[$requiredPackage->getName()] = $requiredPackage; $collected = $this->collectDependencies($installedRepo, $collected, $requiredPackage); } } } return $collected; } /** * Retrieves the path a package is installed to. * * @param bool $global Whether this is a global package * * @return string Install path */ private function getInstallPath(PackageInterface $package, bool $global = false): string { if (!$global) { return $this->composer->getInstallationManager()->getInstallPath($package); } assert(null !== $this->globalComposer); return $this->globalComposer->getInstallationManager()->getInstallPath($package); } /** * @throws \RuntimeException On empty or non-string implementation class name value * @return null|string The fully qualified class of the implementation or null if Plugin is not of Capable type or does not provide it */ protected function getCapabilityImplementationClassName(PluginInterface $plugin, string $capability): ?string { if (!($plugin instanceof Capable)) { return null; } $capabilities = (array) $plugin->getCapabilities(); if (!empty($capabilities[$capability]) && is_string($capabilities[$capability]) && trim($capabilities[$capability])) { return trim($capabilities[$capability]); } if ( array_key_exists($capability, $capabilities) && (empty($capabilities[$capability]) || !is_string($capabilities[$capability]) || !trim($capabilities[$capability])) ) { throw new \UnexpectedValueException('Plugin '.get_class($plugin).' provided invalid capability class name(s), got '.var_export($capabilities[$capability], true)); } return null; } /** * @template CapabilityClass of Capability * @param class-string $capabilityClassName The fully qualified name of the API interface which the plugin may provide * an implementation of. * @param array $ctorArgs Arguments passed to Capability's constructor. * Keeping it an array will allow future values to be passed w\o changing the signature. * @phpstan-param class-string $capabilityClassName * @phpstan-return null|CapabilityClass */ public function getPluginCapability(PluginInterface $plugin, $capabilityClassName, array $ctorArgs = []): ?Capability { if ($capabilityClass = $this->getCapabilityImplementationClassName($plugin, $capabilityClassName)) { if (!class_exists($capabilityClass)) { throw new \RuntimeException("Cannot instantiate Capability, as class $capabilityClass from plugin ".get_class($plugin)." does not exist."); } $ctorArgs['plugin'] = $plugin; $capabilityObj = new $capabilityClass($ctorArgs); // FIXME these could use is_a and do the check *before* instantiating once drop support for php<5.3.9 if (!$capabilityObj instanceof Capability || !$capabilityObj instanceof $capabilityClassName) { throw new \RuntimeException( 'Class ' . $capabilityClass . ' must implement both Composer\Plugin\Capability\Capability and '. $capabilityClassName . '.' ); } return $capabilityObj; } return null; } /** * @template CapabilityClass of Capability * @param class-string $capabilityClassName The fully qualified name of the API interface which the plugin may provide * an implementation of. * @param array $ctorArgs Arguments passed to Capability's constructor. * Keeping it an array will allow future values to be passed w\o changing the signature. * @return CapabilityClass[] */ public function getPluginCapabilities($capabilityClassName, array $ctorArgs = []): array { $capabilities = []; foreach ($this->getPlugins() as $plugin) { $capability = $this->getPluginCapability($plugin, $capabilityClassName, $ctorArgs); if (null !== $capability) { $capabilities[] = $capability; } } return $capabilities; } /** * @param array|bool $allowPluginsConfig * @return array|null */ private function parseAllowedPlugins($allowPluginsConfig, ?Locker $locker = null): ?array { if ([] === $allowPluginsConfig && $locker !== null && $locker->isLocked() && version_compare($locker->getPluginApi(), '2.2.0', '<')) { return null; } if (true === $allowPluginsConfig) { return ['{}' => true]; } if (false === $allowPluginsConfig) { return ['{}' => false]; } $rules = []; foreach ($allowPluginsConfig as $pattern => $allow) { $rules[BasePackage::packageNameToRegexp($pattern)] = $allow; } return $rules; } /** * @internal * * @param 'local'|'global' $type * @return bool */ public function arePluginsDisabled($type) { return $this->disablePlugins === true || $this->disablePlugins === $type; } /** * @internal */ public function isPluginAllowed(string $package, bool $isGlobalPlugin, bool $optional = false): bool { if ($isGlobalPlugin) { $rules = &$this->allowGlobalPluginRules; } else { $rules = &$this->allowPluginRules; } // This is a BC mode for lock files created pre-Composer-2.2 where the expectation of // an allow-plugins config being present cannot be made. if ($rules === null) { if (!$this->io->isInteractive()) { $this->io->writeError('For additional security you should declare the allow-plugins config with a list of packages names that are allowed to run code. See https://getcomposer.org/allow-plugins'); $this->io->writeError('This warning will become an exception once you run composer update!'); $rules = ['{}' => true]; // if no config is defined we allow all plugins for BC return true; } // keep going and prompt the user $rules = []; } foreach ($rules as $pattern => $allow) { if (Preg::isMatch($pattern, $package)) { return $allow === true; } } if ($package === 'composer/package-versions-deprecated') { return false; } if ($this->io->isInteractive()) { $composer = $isGlobalPlugin && $this->globalComposer !== null ? $this->globalComposer : $this->composer; $this->io->writeError(''.$package.($isGlobalPlugin || $this->runningInGlobalDir ? ' (installed globally)' : '').' contains a Composer plugin which is currently not in your allow-plugins config. See https://getcomposer.org/allow-plugins'); $attempts = 0; while (true) { // do not allow more than 5 prints of the help message, at some point assume the // input is not interactive and bail defaulting to a disabled plugin $default = '?'; if ($attempts > 5) { $this->io->writeError('Too many failed prompts, aborting.'); break; } switch ($answer = $this->io->ask('Do you trust "'.$package.'" to execute code and wish to enable it now? (writes "allow-plugins" to composer.json) [y,n,d,?] ', $default)) { case 'y': case 'n': case 'd': $allow = $answer === 'y'; // persist answer in current rules to avoid prompting again if the package gets reloaded $rules[BasePackage::packageNameToRegexp($package)] = $allow; // persist answer in composer.json if it wasn't simply discarded if ($answer === 'y' || $answer === 'n') { $composer->getConfig()->getConfigSource()->addConfigSetting('allow-plugins.'.$package, $allow); } return $allow; case '?': default: $attempts++; $this->io->writeError([ 'y - add package to allow-plugins in composer.json and let it run immediately', 'n - add package (as disallowed) to allow-plugins in composer.json to suppress further prompts', 'd - discard this, do not change composer.json and do not allow the plugin to run', '? - print help', ]); break; } } } elseif ($optional) { return false; } throw new PluginBlockedException( $package.($isGlobalPlugin || $this->runningInGlobalDir ? ' (installed globally)' : '').' contains a Composer plugin which is blocked by your allow-plugins config. You may add it to the list if you consider it safe.'.PHP_EOL. 'You can run "composer '.($isGlobalPlugin || $this->runningInGlobalDir ? 'global ' : '').'config --no-plugins allow-plugins.'.$package.' [true|false]" to enable it (true) or disable it explicitly and suppress this exception (false)'.PHP_EOL. 'See https://getcomposer.org/allow-plugins' ); } } __halt_compiler();----SIGNATURE:----WGwxpoRpcK5DtRZ+EqAaZmGsUWNTKoCWMS4giHAuVz9ad2rJ17N+5stj/gut/J8DLQE5zKFRtvqEzlZjzwpFwnHDTfjo6ajPXLahxfzAQdtp7QU9FiWvMfz7sxkpByGXKkYOp9Q9B2qBCEAvm8YHQzztVkMvf1DRT4DEWVL7P78AsF0q3BlNBY64G5T55OoO+nIRXmPbT5kugOd91d/xJVSUgXOVKsE0Pj41NZ4xPE/wnX7d5Xmyi2PWke2PNnflnTsS7mvtn2YmUPqf2BmR6Nfc8AcQJULhCqaNBUeiw/DCpilCrYpBLesT3CZ9V1lkidWsH2W+WyuA4gnawORNP2EeyAA/5r0p5Z+Prg5bigjurAE/cYtqePSQIigJv7Kn3PGGaqXGFSv8kLxSjA7HeVxRSdk384gIdjUBQSuefBI6PjAjKeUVEMZOO9L+1pc0B7Hf2llUFhrW8kwZ/wYOqcwK27/iU18k8iSO0xfxlJK3n2dpXmpgwu+KW4lbi0VIa/lJGsEGA6gkDR+90l+a6R25SkRFMsrVl5JBGWhoYPDOFr84vj8A1DH9B4nrQ/jE51/OiE2xj7DAqJ3BlEI0Rr0RYWqnjiKuLxkUzU6R8SXk/zJ9qt1QzBMxW8eLAEb/Ujye/hsaRqpHA+zmItmlQwFjNu1gnlLMl8/+W1qSZzY=----ATTACHMENT:----MzYwODg5NDcxMzExMDA4MyA2MTYzODM3NTkzMzg3ODI2IDc4NDYxMzI5MzA3NTQ0ODc=