php bin/magento swissup:module:list>');
@@ -145,15 +149,12 @@ protected function execute(InputInterface $input, OutputInterface $output): int
}
}
- $moduleModel = $this->moduleFactory->create();
- $moduleModel->load($moduleCode);
-
- $identityKey = $moduleModel->getData('identity_key');
- if (!empty($identityKey)) {
- $rows[] = ["Identity Key", $identityKey];
+ try {
+ // array_filter to remove empty items caused by non-magento modules requirements
+ $depends = array_filter($this->packageInfo->getRequire($moduleCode));
+ } catch (\Exception $e) {
+ $depends = [];
}
-
- $depends = $moduleModel->getData('depends');
if (!empty($depends)) {
$rows[] = ["Depends", implode(' ', $depends)];
}
diff --git a/Console/Command/ModuleListCommand.php b/Console/Command/ModuleListCommand.php
index c6c82ba..d0d3363 100644
--- a/Console/Command/ModuleListCommand.php
+++ b/Console/Command/ModuleListCommand.php
@@ -108,7 +108,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int
$this->loader->refresh();
}
- $items = $this->loader->getItems();
+ $items = $this->loader->getModuleItems();
$output->writeln('List of swissup modules : ' . count($items));
$rows = [];
diff --git a/Installer/Command/CategoryUpdate.php b/Installer/Command/CategoryUpdate.php
new file mode 100644
index 0000000..0c043a3
--- /dev/null
+++ b/Installer/Command/CategoryUpdate.php
@@ -0,0 +1,51 @@
+logger->info('Category Update: Prepare category data');
+
+ foreach ($request->getParams() as $data) {
+ $collection = $this->collectionHelper->getCollection(
+ [],
+ \Magento\Catalog\Model\ResourceModel\Category\Collection::class,
+ $data['filters'] ?? []
+ );
+
+ $storeIds = $data['store_id'] ?? [Store::DEFAULT_STORE_ID];
+ if (!is_array($storeIds)) {
+ $storeIds = [$storeIds];
+ }
+
+ foreach ($collection as $category) {
+ foreach ($data['data'] as $key => $value) {
+ $category
+ ->setData($key, $value)
+ ->setCustomAttribute($key, $value);
+ }
+
+ foreach ($storeIds as $storeId) {
+ try {
+ $category->setStoreId($storeId)->save();
+ } catch (\Exception $e) {
+ $this->logger->warning($e->getMessage());
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/Installer/Command/CmsBlock.php b/Installer/Command/CmsBlock.php
new file mode 100644
index 0000000..e0d6374
--- /dev/null
+++ b/Installer/Command/CmsBlock.php
@@ -0,0 +1,82 @@
+logger->info('Cms Blocks: Backup existing and create new blocks');
+
+ $idsToInstall = array_flip($request->getExtraOptions());
+ $isSingleStoreMode = $this->storeManager->isSingleStoreMode();
+
+ foreach ($request->getParams() as $data) {
+ if ($idsToInstall && !isset($idsToInstall[$data['identifier']])) {
+ continue;
+ }
+
+ $collection = $this->collectionFactory->create()
+ ->addStoreFilter($request->getStoreIds())
+ ->addFieldToFilter('identifier', $data['identifier']);
+
+ foreach ($collection as $block) {
+ $block->load($block->getId()); // load stores
+
+ $storesToLeave = array_diff($block->getStoreId(), $request->getStoreIds());
+
+ if (count($storesToLeave) && !$isSingleStoreMode) {
+ $block->setStores($storesToLeave);
+ } else {
+ $block->setIsActive(0)
+ ->setIdentifier($this->getBackupIdentifier($block->getIdentifier()));
+ }
+
+ try {
+ $block->save();
+ } catch (\Exception $e) {
+ $this->logger->warning($e->getMessage());
+ }
+ }
+
+ $data = array_merge([
+ 'is_active' => 1,
+ ], $data);
+
+ try {
+ $this->blockFactory->create()
+ ->setData($data)
+ ->setStores($request->getStoreIds()) // see Magento\Cms\Model\ResourceModel\Block::_afterSave
+ ->save();
+ } catch (\Exception $e) {
+ $this->logger->warning($e->getMessage());
+ }
+ }
+ }
+
+ /**
+ * @param string $identifier
+ * @return string
+ */
+ private function getBackupIdentifier($identifier)
+ {
+ return $identifier
+ . '_backup_'
+ . rand(10, 99)
+ . '_'
+ . $this->localeDate->date()->format('Y-m-d-H-i-s');
+ }
+}
diff --git a/Installer/Command/CmsPage.php b/Installer/Command/CmsPage.php
new file mode 100644
index 0000000..9a4aeda
--- /dev/null
+++ b/Installer/Command/CmsPage.php
@@ -0,0 +1,149 @@
+getExtraOptions());
+ $identifiers = array_map(
+ function ($item) {
+ return $item['identifier'];
+ },
+ $request->getParams()
+ );
+
+ $this->cleanupUrlRewrites($identifiers, $request->getStoreIds());
+
+ $isSingleStoreMode = $this->storeManager->isSingleStoreMode();
+ $collection = $this->collectionFactory->create()
+ ->addStoreFilter($request->getStoreIds())
+ ->addFieldToFilter('is_active', 1)
+ ->addFieldToFilter('identifier', ['in' => $identifiers]);
+
+ $this->logger->info('Cms Pages: Backup existing pages');
+ foreach ($collection as $page) {
+ if ($idsToInstall && !isset($idsToInstall[$page->getIdentifier()])) {
+ continue;
+ }
+
+ $page->load($page->getId()); // load stores
+
+ $storesToLeave = array_diff($page->getStoreId(), $request->getStoreIds());
+
+ if (count($storesToLeave) && !$isSingleStoreMode) {
+ $page->setStores($storesToLeave);
+ } else {
+ // duplicate page, because original page will be used for new content
+ $page = $this->pageFactory->create()
+ ->addData($page->getData())
+ ->unsPageId()
+ ->setIsActive(0)
+ ->setIdentifier($this->getBackupIdentifier($page->getIdentifier()));
+ }
+
+ try {
+ $page->save();
+ } catch (\Exception $e) {
+ $this->logger->warning(
+ sprintf('%s "%s"', $e->getMessage(), $page->getIdentifier())
+ );
+ }
+ }
+
+ $this->logger->info('CMS PAGES: Create new pages');
+ foreach ($request->getParams() as $data) {
+ if ($idsToInstall && !isset($idsToInstall[$data['identifier']])) {
+ continue;
+ }
+
+ $canUseExistingPage = false;
+ $pages = $collection->getItemsByColumnValue(
+ 'identifier',
+ $data['identifier']
+ );
+
+ // If page is linked to destination stores only - use it. Otherwise, create new.
+ foreach ($pages as $page) {
+ $diff = array_diff($page->getStoreId(), $request->getStoreIds());
+ if (!count($diff)) {
+ $canUseExistingPage = true;
+ break;
+ }
+ }
+
+ if (!$canUseExistingPage) {
+ $page = $this->pageFactory->create();
+ }
+
+ $data = array_merge([
+ 'is_active' => 1,
+ 'page_layout' => '1column',
+ 'content_heading' => '',
+ 'layout_update_xml' => '',
+ 'custom_theme' => null,
+ 'custom_root_template' => null,
+ 'custom_layout_update_xml' => null,
+ ], $data);
+
+ try {
+ $page->addData($data)
+ ->setStores($request->getStoreIds()) // see Magento\Cms\Model\ResourceModel\Page::_afterSave
+ ->save();
+ } catch (\Exception $e) {
+ $this->logger->warning(
+ sprintf('%s "%s"', $e->getMessage(), $data['identifier'])
+ );
+ }
+ }
+ }
+
+ /**
+ * @param string $identifier
+ * @return string
+ */
+ private function getBackupIdentifier($identifier)
+ {
+ return $identifier
+ . '_backup_'
+ . rand(10, 99)
+ . '_'
+ . $this->localeDate->date()->format('Y-m-d-H-i-s');
+ }
+
+ /**
+ * @param array $identifiers
+ * @return void
+ */
+ private function cleanupUrlRewrites($identifiers, $storeIds)
+ {
+ $urls = $this->urlRewriteCollectionFactory->create()
+ ->addFieldToFilter('entity_type', 'cms-page')
+ ->addFieldToFilter('request_path', ['in' => $identifiers]);
+
+ if (in_array(0, $storeIds)) {
+ $storeIds = array_keys($this->storeManager->getStores(true));
+ }
+
+ $urls->addFieldToFilter('store_id', ['in' => $storeIds]);
+
+ foreach ($urls as $url) {
+ $url->delete();
+ }
+ }
+}
diff --git a/Installer/Command/Config.php b/Installer/Command/Config.php
new file mode 100644
index 0000000..0c77557
--- /dev/null
+++ b/Installer/Command/Config.php
@@ -0,0 +1,89 @@
+logger->info('Config: Update store parameters');
+
+ foreach ($request->getStoreIds() as $storeId) {
+ foreach ($request->getParams() as $path => $value) {
+ if (is_array($value)) {
+ $this->processArray($path, $value, $storeId);
+ } else {
+ $this->processScalar($path, $value, $storeId);
+ }
+ }
+ }
+ }
+
+ /**
+ * @param string $path
+ * @param string $value
+ * @param int $storeId
+ * @return void
+ */
+ private function processScalar($path, $value, $storeId)
+ {
+ if (!$storeId) {
+ $scope = ScopeConfigInterface::SCOPE_TYPE_DEFAULT;
+ } else {
+ $scope = ScopeInterface::SCOPE_STORES;
+ }
+
+ $this->configWriter->save($path, $value, $scope, $storeId);
+ }
+
+ /**
+ * @param array $path
+ * @param array $data
+ * @param int $storeId
+ * @return void
+ */
+ private function processArray($path, $data, $storeId)
+ {
+ if (!$storeId) {
+ $scope = ScopeConfigInterface::SCOPE_TYPE_DEFAULT;
+ } else {
+ $scope = ScopeInterface::SCOPE_STORE;
+ }
+
+ $remote = $data['path'] ?? $path;
+ $search = $data['search'] ?? $data['remove'];
+ $replace = $data['replace'] ?? '';
+ $value = $this->scopeConfig->getValue($remote, $scope, $storeId);
+
+ if (!$value) {
+ return;
+ }
+
+ if (!is_array($search)) {
+ $search = [$search];
+ }
+
+ foreach ($search as $i => $string) {
+ $value = str_replace(
+ $string,
+ is_array($replace) ? ($replace[$i] ?? '') : $replace,
+ $value
+ );
+ }
+
+ $this->processScalar($path, $value, $storeId);
+ }
+}
diff --git a/Installer/Command/CopyMediaDir.php b/Installer/Command/CopyMediaDir.php
new file mode 100644
index 0000000..c425b19
--- /dev/null
+++ b/Installer/Command/CopyMediaDir.php
@@ -0,0 +1,49 @@
+logger->info('Resources: Copy media files');
+
+ $media = $this->filesystem->getDirectoryWrite(DirectoryList::MEDIA);
+ $driver = $media->getDriver();
+ $mediaPath = $media->getAbsolutePath();
+
+ foreach ($request->getParams() as $dir) {
+ $paths = $driver->readDirectoryRecursively($dir);
+ $paths = array_reverse($paths); // put deepest in the end
+
+ foreach ($paths as $path) {
+ $relative = str_replace($dir . '/', '', $path);
+ $destination = $mediaPath . $relative;
+
+ try {
+ if ($driver->isExists($destination)) {
+ continue;
+ }
+
+ if ($driver->isDirectory($path)) {
+ $driver->createDirectory($destination);
+ } else {
+ $driver->copy($path, $destination);
+ }
+ } catch (\Exception $e) {
+ $this->logger->warning($e->getMessage());
+ }
+ }
+ }
+ }
+}
diff --git a/Installer/Command/Product.php b/Installer/Command/Product.php
new file mode 100644
index 0000000..f538d87
--- /dev/null
+++ b/Installer/Command/Product.php
@@ -0,0 +1,14 @@
+logger->warning('Product Command is deprecated. Please use ProductCollection instead');
+ parent::execute($request);
+ }
+}
diff --git a/Installer/Command/ProductAttribute.php b/Installer/Command/ProductAttribute.php
new file mode 100644
index 0000000..9d8d132
--- /dev/null
+++ b/Installer/Command/ProductAttribute.php
@@ -0,0 +1,78 @@
+logger->info('Product Attributes: Update attributes');
+
+ $entityTypeId = $this->eavEntityFactory->create()
+ ->setType(\Magento\Catalog\Model\Product::ENTITY)
+ ->getTypeId();
+ $attributeSets = $this->attributeSetCollectionFactory->create()
+ ->setEntityTypeFilter($entityTypeId);
+
+ foreach ($request->getParams() as $data) {
+ /* @var $model \Magento\Catalog\Model\ResourceModel\Eav\Attribute */
+ $model = $this->attributeFactory->create()
+ ->load($data['attribute_code'], 'attribute_code');
+ if ($model->getId()) {
+ continue;
+ }
+
+ $data = array_merge([
+ 'is_global'=> 0,
+ 'frontend_input'=> 'boolean',
+ 'is_configurable'=> 0,
+ 'is_filterable'=> 0,
+ 'is_filterable_in_search' => 0,
+ 'sort_order' => 1,
+ ], $data);
+
+ $data['source_model'] = $this->productHelper->getAttributeSourceModelByInputType(
+ $data['frontend_input']
+ );
+ $data['backend_model'] = $this->productHelper->getAttributeBackendModelByInputType(
+ $data['frontend_input']
+ );
+ $data['backend_type'] = $model->getBackendTypeByInput($data['frontend_input']);
+
+ $model->addData($data);
+ $model->setEntityTypeId($entityTypeId);
+ $model->setIsUserDefined(1);
+
+ foreach ($attributeSets as $set) {
+ $model->setAttributeSetId($set->getId());
+ $model->setAttributeGroupId($set->getDefaultGroupId());
+ try {
+ $model->save();
+ } catch (\Exception $e) {
+ $this->logger->warning($e->getMessage());
+ }
+ }
+
+ if (!$attributeSets->count()) {
+ try {
+ $model->save();
+ } catch (\Exception $e) {
+ $this->logger->warning($e->getMessage());
+ }
+ }
+ }
+ }
+}
diff --git a/Installer/Command/ProductCollection.php b/Installer/Command/ProductCollection.php
new file mode 100644
index 0000000..079b145
--- /dev/null
+++ b/Installer/Command/ProductCollection.php
@@ -0,0 +1,98 @@
+logger->info('Product Collection: Prepare collections');
+
+ $data = $request->getParams();
+ $visibility = $this->catalogProductVisibility->getVisibleInCatalogIds();
+ $isAllStores = in_array(0, $request->getStoreIds());
+ $attributes = $this->attributeCollectionFactory->create()
+ ->addFieldToFilter('attribute_code', ['in' => array_keys($data)]);
+
+ foreach ($attributes as $attribute) {
+ $collection = $this->productCollectionFactory->create()
+ ->setPageSize(1)
+ ->setCurPage(1);
+
+ switch ($attribute->getFrontendInput()) {
+ case 'boolean':
+ $value = 1;
+ $unsetValue = 0;
+ $collection->addAttributeToFilter($attribute, 1);
+ break;
+ case 'date':
+ $value = $this->localeDate->date()->format('Y-m-d H:i:s');
+ // far future date never matches the 'to now' filter
+ $unsetValue = '2222-12-31 00:00:00';
+ $collection->addAttributeToFilter(
+ $attribute,
+ [
+ [
+ 'date' => true,
+ 'to' => $value
+ ]
+ ]
+ );
+ break;
+ default:
+ continue 2;
+ }
+
+ if ($collection->getSize()) {
+ // store has products with specified attribute
+ continue;
+ }
+
+ foreach ($request->getStoreIds() as $storeId) {
+ $collectionStoreId = $storeId;
+
+ if ($storeId == \Magento\Store\Model\Store::DEFAULT_STORE_ID) {
+ // compatibility with M2.2.5 when install on 'All Store Views'
+ $collectionStoreId = $this->storeManager->getDefaultStoreView()->getId();
+ }
+
+ $visibleProducts = $this->productCollectionFactory->create()
+ ->setStoreId($collectionStoreId)
+ ->setVisibility($visibility)
+ ->addStoreFilter($storeId)
+ ->setPageSize($data[$attribute->getAttributeCode()])
+ ->setCurPage(1);
+
+ if (!$visibleProducts->getSize()) {
+ continue;
+ }
+
+ foreach ($visibleProducts as $product) {
+ $product->addAttributeUpdate(
+ $attribute->getAttributeCode(),
+ $isAllStores ? $value : $unsetValue,
+ \Magento\Store\Model\Store::DEFAULT_STORE_ID
+ );
+
+ $product->setStoreId($storeId)
+ ->setData($attribute->getAttributeCode(), $value)
+ ->save();
+ }
+ }
+ }
+ }
+}
diff --git a/Installer/Command/Unpack.php b/Installer/Command/Unpack.php
new file mode 100644
index 0000000..8973012
--- /dev/null
+++ b/Installer/Command/Unpack.php
@@ -0,0 +1,29 @@
+logger->info('Unpack');
+ $params = $request->getParams();
+ $destination = $params['destination'];
+ $this->ioFile->checkAndCreateFolder($destination);
+ $archive = $params['archive'];
+ $this->archiver->unpack($archive, $destination);
+ }
+}
diff --git a/Installer/Command/Widget.php b/Installer/Command/Widget.php
new file mode 100644
index 0000000..1d99d53
--- /dev/null
+++ b/Installer/Command/Widget.php
@@ -0,0 +1,140 @@
+logger->info('Widgets: Backup existing and create new widgets');
+
+ $isSingleStoreMode = $this->storeManager->isSingleStoreMode();
+
+ foreach ($request->getParams() as $raw) {
+ $collection = $this->collectionFactory->create()
+ ->addStoreFilter($request->getStoreIds())
+ ->addFieldToFilter('title', $raw['title'])
+ ->addFieldToFilter('instance_type', $raw['type'])
+ ->addFieldToFilter('theme_id', $raw['theme_id']);
+
+ foreach ($collection as $widget) {
+ $storesToLeave = array_diff($widget->getStoreIds(), $request->getStoreIds());
+
+ if (count($storesToLeave) && !$isSingleStoreMode) {
+ // unset stores. new widget will be added for them.
+ $widget->setStoreIds($storesToLeave);
+ } else {
+ // re-create widget with new params
+ $widget->delete();
+ continue;
+ }
+
+ try {
+ $widget->save();
+ } catch (\Exception $e) {
+ $this->logger->warning($e->getMessage());
+ }
+ }
+
+ $data = [
+ 'title' => $raw['title'],
+ 'instance_type' => $raw['type'],
+ 'theme_id' => $raw['theme_id'],
+ 'store_ids' => $request->getStoreIds(),
+ 'widget_parameters' => $raw['params'],
+ 'sort_order' => $raw['sort_order'] ?? 0,
+ ];
+
+ $pageGroups = [];
+ foreach ($raw['pages'] as $page) {
+ $pageGroup = $this->getPageGroupData($page, [
+ 'page_id' => 0,
+ 'for' => 'all',
+ 'block' => $page['reference'] ?? 'content.top',
+ 'template' => $raw['template'] ?? '',
+ ]);
+
+ if ($pageGroup) {
+ $pageGroups[] = $pageGroup;
+ }
+ }
+
+ $data['page_groups'] = $pageGroups;
+
+ try {
+ $this->widgetFactory->create()->addData($data)->save();
+ } catch (\Exception $e) {
+ $this->logger->warning($e->getMessage());
+ }
+ }
+ }
+
+ private function getPageGroupData($data, $defaultData)
+ {
+ $groupName = $groupData = null;
+
+ if (isset($data['handle'])) {
+ $groupName = $this->getGroupName($data['handle']);
+ $groupData = [
+ 'layout_handle' => $data['handle'],
+ ];
+ } elseif (isset($data['page_layout'])) {
+ $groupName = 'page_layouts';
+ $groupData = [
+ 'layout_handle' => $data['page_layout'],
+ ];
+ } elseif (isset($data['category_ids'])) {
+ $groupName = 'anchor_categories';
+ $groupData = [
+ 'for' => Instance::SPECIFIC_ENTITIES,
+ 'entities' => $data['category_ids'],
+ ];
+ } elseif (isset($data['product_ids'])) {
+ $groupName = 'all_products';
+ $groupData = [
+ 'for' => Instance::SPECIFIC_ENTITIES,
+ 'entities' => $data['product_ids'],
+ ];
+ }
+
+ if ($groupName && $groupData) {
+ return [
+ 'page_group' => $groupName,
+ $groupName => array_merge($defaultData, $groupData),
+ ];
+ }
+
+ return false;
+ }
+
+ private function getGroupName($handle)
+ {
+ $mapping = [
+ 'default' => 'all_pages',
+ 'catalog_product_view' => 'all_products',
+ 'catalog_product_view_type_simple' => 'simple_products',
+ 'catalog_product_view_type_virtual' => 'virtual_products',
+ 'catalog_product_view_type_bundle' => 'bundle_products',
+ 'catalog_product_view_type_configurable' => 'configurable_products',
+ 'catalog_product_view_type_downloadable' => 'downloadable_products',
+ 'catalog_product_view_type_grouped' => 'grouped_products',
+ 'catalog_category_view_type_layered' => 'anchor_categories',
+ 'catalog_category_view_type_default' => 'notanchor_categories',
+ ];
+
+ return $mapping[$handle] ?? 'pages';
+ }
+}
diff --git a/Installer/ConfigReader.php b/Installer/ConfigReader.php
new file mode 100644
index 0000000..e834f15
--- /dev/null
+++ b/Installer/ConfigReader.php
@@ -0,0 +1,412 @@
+readFiles() as $path => $content) {
+ $xml = $this->configFactory->create(['sourceData' => $content]);
+ $nodePackages = (array) $xml->getNode('packages/package');
+
+ if (array_intersect($packages, $nodePackages)) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * @return array
+ * @throws \Magento\Framework\Exception\LocalizedException
+ */
+ public function read()
+ {
+ $output = [
+ 'rules' => [], // package/name => [installer_keys]
+ 'fields' => [], // installer_key => form data
+ 'conditions' => [], // installer_key => conditions to check before commands execution
+ 'commands' => [], // installer_key => commands to run
+ ];
+
+ foreach ($this->readFiles() as $path => $content) {
+ $this->currentPath = $path;
+
+ $xml = $this->configFactory->create(['sourceData' => $content]);
+
+ $packages = (array) $xml->getNode('packages/package');
+ foreach ($packages as $packageName) {
+ $output['rules'][$packageName][] = $path;
+ }
+
+ $output['fields'][$path] = $this->parseFields($xml);
+ $output['conditions'][$path] = $this->parseConditions($xml);
+ $output['commands'][$path] = $this->parseCommands($xml);
+ }
+
+ return $output;
+ }
+
+ /**
+ * @return array
+ */
+ protected function readFiles()
+ {
+ if ($this->files !== null) {
+ return $this->files;
+ }
+
+ $this->files = [];
+ $paths = $this->componentRegistrar->getPaths(ComponentRegistrar::MODULE);
+ $paths += $this->componentRegistrar->getPaths(ComponentRegistrar::THEME);
+
+ foreach ($paths as $path) {
+ $dir = $this->readDirFactory->create($path);
+ $filepath = self::DIR . '/' . self::FILE;
+
+ if (!$dir->isReadable($filepath)) {
+ continue;
+ }
+
+ $this->files[$path] = $dir->readFile($filepath);
+ }
+
+ return $this->files;
+ }
+
+ /**
+ * @param Config $xml
+ * @return array
+ */
+ protected function parseFields(Config $xml)
+ {
+ $node = $xml->getNode('fields');
+ if (!$node) {
+ return [];
+ }
+
+ $result = [];
+ foreach ($node->children() as $field) {
+ $name = $field->getAttribute('name');
+
+ if (!isset($result[$name])) {
+ $result[$name] = [];
+ }
+
+ $result[$name]['title'] = $field->getAttribute('title');
+
+ if (!$field->hasChildren()) {
+ continue;
+ }
+
+ $options = false;
+ $items = $field->descend('option');
+ $model = $field->descend('source_model');
+
+ if ($items) {
+ $options = [];
+ foreach ($items as $item) {
+ $value = (string) $item[0];
+ $options[$value] = [
+ 'value' => $value,
+ 'label' => $item->getAttribute('title'),
+ ];
+ }
+ } elseif ($model) {
+ $model = (string) $model[0];
+ $options = $this->objectManager->get($model)->toOptionArray();
+ }
+
+ if ($options !== false) {
+ $result[$name]['options'] = $options;
+ }
+ }
+
+ return $result;
+ }
+
+ /**
+ * @param Config $xml
+ * @return array
+ */
+ protected function parseConditions(Config $xml)
+ {
+ $node = $xml->getNode('commands');
+ if (!$node || !$node->descend('conditions')) {
+ return [];
+ }
+
+ $result = [];
+ foreach ($node->descend('conditions') as $condition) {
+ $result = $this->parseArguments($condition);
+ }
+
+ return $result;
+ }
+
+ /**
+ * @param Config $xml
+ * @return array
+ */
+ protected function parseCommands(Config $xml)
+ {
+ $node = $xml->getNode('commands');
+ if (!$node) {
+ return [];
+ }
+
+ $commands = [];
+ foreach ($node->children() as $child) {
+ $tagName = $child->getName();
+
+ if (!in_array($tagName, ['command', 'include'])) {
+ continue;
+ }
+
+ if ($tagName === 'command') {
+ $commands[] = $child;
+ continue;
+ }
+
+ // read commands from separate file
+ $path = $child->getAttribute('path');
+ $dir = $this->readDirFactory->create($this->currentPath . '/' . self::DIR);
+
+ if (!$dir->isReadable($path)) {
+ continue;
+ }
+
+ $xml = $this->configFactory->create([
+ 'sourceData' => $dir->readFile($path)
+ ]);
+
+ foreach ($xml->getNode('command') as $command) {
+ $commands[] = $command;
+ }
+ }
+
+ $result = [];
+ foreach ($commands as $i => $command) {
+ $class = $this->resolveClass($command->getAttribute('class'));
+ $module = $command->getAttribute('module');
+
+ if (!$this->isModuleEnabled($module) || !class_exists($class)) {
+ continue;
+ }
+
+ $result[$i]['class'] = $class;
+
+ if (defined($class . '::ALIAS')) {
+ $alias = constant($class . '::ALIAS');
+ } else {
+ // convert to kebabcase https://stackoverflow.com/a/75687338/2754377
+ $classparts = explode('\\', $class);
+ $alias = end($classparts);
+ $alias = strtolower(preg_replace('/(?:\d++|[A-Za-z]?[a-z]++)\K(?!$)/', '-', $alias));
+ }
+
+ $result[$i]['alias'] = $alias;
+
+ if (!$command->hasChildren() || !$command->descend('data')) {
+ continue;
+ }
+
+ $result[$i]['data'] = $this->parseArguments(
+ $command->descend('data')->children()
+ );
+ }
+
+ return $result;
+ }
+
+ /**
+ * Installer files of the released packages reference the classes of
+ * swissup/module-marketplace, which is not required anymore.
+ *
+ * @param string $class Class name, optionally with ::method suffix
+ * @return string
+ */
+ protected function resolveClass($class)
+ {
+ list($name, $method) = array_pad(explode('::', $class, 2), 2, null);
+
+ $ported = str_replace(
+ 'Swissup\\Marketplace\\Installer\\',
+ 'Swissup\\Core\\Installer\\',
+ $name
+ );
+
+ if (class_exists($ported)) {
+ $name = $ported;
+ }
+
+ return $method === null ? $name : $name . '::' . $method;
+ }
+
+ /**
+ * @param string $module
+ * @return boolean
+ */
+ protected function isModuleEnabled($module)
+ {
+ if (!$module) {
+ return true;
+ }
+ return $this->moduleManager->isEnabled($module);
+ }
+
+ /**
+ * @param \Magento\Framework\Simplexml\Element $node
+ * @return array
+ * @throws \Exception
+ */
+ protected function parseArguments(\Magento\Framework\Simplexml\Element $node)
+ {
+ $i = 0;
+ $result = [];
+
+ foreach ($node as $item) {
+ $key = $item->getAttribute('name') ?: $i++;
+ $helper = $item->getAttribute('helper');
+
+ if ($helper) {
+ $helper = $this->resolveClass($helper);
+ }
+
+ if (!$item->hasChildren() && !$helper) {
+ $value = (string) $item[0];
+ $type = (string) $item->getAttribute('type');
+
+ if ($type) {
+ $method = 'prepare' . ucfirst($type);
+ if (method_exists($this, $method)) {
+ $value = $this->{$method}($value);
+ }
+ }
+
+ $result[$key] = $value;
+ continue;
+ }
+
+ $arguments = $this->parseArguments($item->children());
+
+ if ($helper) {
+ $result[$key] = [
+ 'helper' => $helper,
+ 'arguments' => $arguments,
+ ];
+ } else {
+ $result[$key] = $arguments;
+ }
+ }
+
+ return $result;
+ }
+
+ /**
+ * @param string $value
+ * @return string
+ * @throws SecurityViolationException
+ */
+ private function preparePath($value)
+ {
+ $subdir = $this->currentPath . '/' . self::DIR . '/';
+ $subdir = str_replace('/', DIRECTORY_SEPARATOR, $subdir);
+ $result = $subdir . $value;
+ $result = realpath($result);
+
+ if ($result === false || strpos($result, $subdir) !== 0) {
+ throw new SecurityViolationException(
+ __(
+ 'Error during "%1" processing. Relative paths are forbidden: "%2"',
+ $this->currentPath,
+ $value
+ )
+ );
+ }
+
+ return $result;
+ }
+
+ /**
+ * @param string $value
+ * @return mixed
+ * @throws RuntimeException
+ */
+ private function prepareConst($value)
+ {
+ if (!defined($value)) {
+ throw new RuntimeException(
+ __('Requested constant is not defined: %1', $value)
+ );
+ }
+ return constant($value);
+ }
+
+ /**
+ * @param string $value
+ * @return int
+ */
+ private function prepareInt($value)
+ {
+ return (int) $value;
+ }
+
+ /**
+ * @param string $value
+ * @return boolean
+ */
+ private function prepareBoolean($value)
+ {
+ if ($value === 'false') {
+ $value = false;
+ } else {
+ $value = (bool) $value;
+ }
+ return $value;
+ }
+
+ /**
+ * @param string $value
+ * @return boolean
+ */
+ private function prepareNull($value)
+ {
+ return null;
+ }
+}
diff --git a/Installer/Helper/Arr.php b/Installer/Helper/Arr.php
new file mode 100644
index 0000000..9529674
--- /dev/null
+++ b/Installer/Helper/Arr.php
@@ -0,0 +1,11 @@
+objectManager->create($class);
+
+ foreach ($filters as $filter) {
+ if (isset($filter['field'], $filter['value'])) {
+ $collection->addFieldToFilter($filter['field'], $filter['value']);
+ } elseif (isset($filter['method'], $filter['params'])) {
+ call_user_func_array([$collection, $filter['method']], $filter['params']);
+ }
+ }
+
+ return $collection;
+ }
+
+ public function getCollection(array $request, $class, array $filters = [])
+ {
+ return $this->prepareCollection($class, $filters);
+ }
+
+ /**
+ * @param array $request
+ * @param string $class
+ * @param array $filters
+ * @return int|string
+ */
+ public function getId(array $request, $class, array $filters = [])
+ {
+ return $this->prepareCollection($class, $filters)->setPageSize(1)->getFirstItem()->getId();
+ }
+
+ /**
+ * @param array $request
+ * @param string $class
+ * @param array $filters
+ * @return array
+ */
+ public function getIds(array $request, $class, array $filters = [])
+ {
+ return $this->prepareCollection($class, $filters)->getAllIds();
+ }
+}
diff --git a/Installer/Helper/Renderer.php b/Installer/Helper/Renderer.php
new file mode 100644
index 0000000..f389837
--- /dev/null
+++ b/Installer/Helper/Renderer.php
@@ -0,0 +1,25 @@
+jsonSerializer->serialize($value);
+ }
+}
diff --git a/Installer/Helper/Text.php b/Installer/Helper/Text.php
new file mode 100644
index 0000000..26cfabf
--- /dev/null
+++ b/Installer/Helper/Text.php
@@ -0,0 +1,15 @@
+memo[$path])) {
+ $this->memo[$path] = $this->collectionFactory->create()
+ ->getThemeByFullPath($path)
+ ->getThemeId();
+ }
+ return $this->memo[$path];
+ }
+}
diff --git a/Installer/Installer.php b/Installer/Installer.php
new file mode 100644
index 0000000..36967de
--- /dev/null
+++ b/Installer/Installer.php
@@ -0,0 +1,235 @@
+runOnlyIfRequired = $flag;
+ return $this;
+ }
+
+ /**
+ * Run packages installation with optional request data.
+ *
+ * @param array $packages
+ * @param array $requestData
+ * @return void
+ */
+ public function run(array $packages, array $requestData = [])
+ {
+ $request = $this->requestFactory->create(['data' => $requestData]);
+
+ try {
+ $this->appState->getAreaCode();
+ } catch (\Exception $e) {
+ $this->appState->setAreaCode(\Magento\Framework\App\Area::AREA_ADMINHTML);
+ }
+
+ foreach ($this->getInstallers($packages) as $installer) {
+ $requirements = $this->data['conditions'][$installer] ?? [];
+ foreach ($requirements as $param => $value) {
+ if ($request->getData($param) != $value) {
+ // customer didn't select this package in the form.
+ // Another argento theme, for example.
+ continue 2;
+ }
+ }
+
+ $info = array_slice(explode('/', $installer), -2, 2);
+ $this->getLogger()->notice(sprintf('Processing %s', implode('/', $info)));
+
+ $commands = $this->data['commands'][$installer] ?? [];
+
+ foreach ($commands as $config) {
+ if (isset($requestData['skip-' . $config['alias']]) ||
+ $this->runOnlyIfRequired && !isset($requestData[$config['alias']])
+ ) {
+ continue;
+ }
+
+ $request->setExtraOptions([]);
+ if (!empty($requestData[$config['alias']])) {
+ $request->setExtraOptions(explode(',', $requestData[$config['alias']]));
+ }
+
+ $params = [];
+ $data = isset($config['data']) ? $config['data'] : [];
+ foreach ($data as $key => $param) {
+ $params[$key] = $this->processArguments($param, $requestData);
+ }
+
+ $request->setParams($params);
+
+ $command = $this->objectManager->get($config['class']);
+
+ if (method_exists($command, 'setLogger')) {
+ $command->setLogger($this->getLogger());
+ }
+
+ $command->execute($request);
+ }
+ }
+
+ $this->cache->clean(array_intersect([
+ 'block_html',
+ 'config',
+ 'full_page',
+ 'layout',
+ 'translate',
+ 'compiled_config',
+ ], $this->cache->getAvailableTypes()));
+ }
+
+ /**
+ * @param mixed $packages
+ * @return boolean
+ */
+ public function hasInstaller($packages)
+ {
+ if (!is_array($packages)) {
+ $packages = [$packages];
+ }
+
+ return $this->configReader->hasConfig($packages);
+ }
+
+ /**
+ * @param array $packages
+ * @return array
+ */
+ public function getFormConfig($packages)
+ {
+ $result = [];
+
+ foreach ($this->getInstallers($packages) as $installer) {
+ $fields = $this->data['fields'][$installer] ?? [];
+ $result = array_replace_recursive($result, $fields);
+ }
+
+ return $result;
+ }
+
+ public function getCommandAliases($packages)
+ {
+ $result = [];
+
+ foreach ($this->getInstallers($packages) as $installer) {
+ foreach ($this->data['commands'][$installer] as $command) {
+ if (isset($command['alias'])) {
+ $result[] = $command['alias'];
+ }
+ }
+ }
+
+ return $result;
+ }
+
+ /**
+ * @param string $package
+ * @return array
+ */
+ private function getLogger(): LoggerInterface
+ {
+ return $this->logger ??= new NullLogger();
+ }
+
+ /**
+ * @param string $package
+ * @return array
+ */
+ private function getInstaller($package)
+ {
+ $this->load();
+
+ return $this->data['rules'][$package] ?? [];
+ }
+
+ /**
+ * @param array $packages
+ * @return array
+ */
+ private function getInstallers($packages)
+ {
+ $result = [];
+
+ foreach ($packages as $package) {
+ $installer = $this->getInstaller($package);
+
+ if (!$installer) {
+ continue;
+ }
+
+ $result = array_merge($result, $installer);
+ }
+
+ return array_unique($result);
+ }
+
+ /**
+ * @param array $arguments
+ * @param array $requestData
+ * @return array
+ */
+ private function processArguments($arguments, $requestData)
+ {
+ if (!is_array($arguments)) {
+ return $arguments;
+ }
+
+ // start from the deepest nested helper
+ foreach ($arguments as $key => $value) {
+ if (is_array($value)) {
+ $arguments[$key] = $this->processArguments($value, $requestData);
+ }
+ }
+
+ if (isset($arguments['helper'], $arguments['arguments']) &&
+ strpos($arguments['helper'], '::') !== false
+ ) {
+ list($class, $method) = explode('::', $arguments['helper']);
+
+ return call_user_func_array(
+ [$this->objectManager->get($class), $method],
+ array_merge([$requestData], array_values($arguments['arguments']))
+ );
+ }
+
+ return $arguments;
+ }
+
+ /**
+ * @return void
+ */
+ private function load()
+ {
+ if ($this->data !== null) {
+ return;
+ }
+
+ $this->data = $this->configReader->read();
+ }
+}
diff --git a/Installer/Request.php b/Installer/Request.php
new file mode 100644
index 0000000..0eb4012
--- /dev/null
+++ b/Installer/Request.php
@@ -0,0 +1,13 @@
+_data['store_id'] ?? [Store::DEFAULT_STORE_ID];
+ }
+}
diff --git a/Model/ComponentList/Loader.php b/Model/ComponentList/Loader.php
index 1866971..2e47d8f 100644
--- a/Model/ComponentList/Loader.php
+++ b/Model/ComponentList/Loader.php
@@ -101,6 +101,13 @@ public function getItems()
return $this->load();
}
+ public function getModuleItems()
+ {
+ return array_filter($this->getItems(), function ($item) {
+ return ($item['type'] ?? '') !== 'metapackage';
+ });
+ }
+
public function getInstalledItems()
{
return array_filter($this->getItems(), function ($item) {
diff --git a/Model/ComponentList/Loader/AbstractLoader.php b/Model/ComponentList/Loader/AbstractLoader.php
index d023683..e4544be 100644
--- a/Model/ComponentList/Loader/AbstractLoader.php
+++ b/Model/ComponentList/Loader/AbstractLoader.php
@@ -57,13 +57,12 @@ public function load()
}
foreach ($components as $name => $config) {
- $code = $this->componentHelper->convertPackageNameToModuleName(
- $config['name']
- );
+ $id = $config['name'];
+ $this->items[$id]['code'] = $this->componentHelper
+ ->convertPackageNameToModuleName($id);
- $this->items[$code]['code'] = $code;
foreach ($this->getMapping() as $source => $destination) {
- if (!empty($this->items[$code][$destination])) {
+ if (!empty($this->items[$id][$destination])) {
continue;
}
@@ -78,7 +77,7 @@ public function load()
if (is_array($value)) {
$value = implode(',', $value);
}
- $this->items[$code][$destination] = $value;
+ $this->items[$id][$destination] = $value;
}
}
return $this->items;
diff --git a/Model/ComponentList/Loader/Remote.php b/Model/ComponentList/Loader/Remote.php
index fcb42de..623288a 100644
--- a/Model/ComponentList/Loader/Remote.php
+++ b/Model/ComponentList/Loader/Remote.php
@@ -60,8 +60,6 @@ public function getMapping()
'version' => 'latest_version',
'type' => 'type',
'time' => 'release_date',
- 'extra.marketplace.links.docs' => 'docs_link',
- 'extra.marketplace.links.changelog' => 'changelog_link',
'extra.swissup.links.store' => 'link',
'extra.swissup.links.docs' => 'docs_link',
'extra.swissup.links.download' => 'download_link',
@@ -69,6 +67,8 @@ public function getMapping()
'extra.swissup.links.marketplace' => 'marketplace_link',
'extra.swissup.links.identity_key' => 'identity_key_link',
'extra.swissup.purchase_code' => 'purchase_code',
+ 'extra.marketplace.links.docs' => 'docs_link',
+ 'extra.marketplace.links.changelog' => 'changelog_link',
];
}
@@ -92,12 +92,6 @@ public function getComponentsInfo()
return $carry;
}, $versions[0] ?? 0);
- if (!empty($info[$latestVersion]['type']) &&
- $info[$latestVersion]['type'] === 'metapackage'
- ) {
- continue;
- }
-
$modules[$packageName] = $info[$latestVersion];
if (isset($info['dev-master']['extra']['swissup'])) {
@@ -107,22 +101,6 @@ public function getComponentsInfo()
}
}
- $modules['swissup/subscription'] = [
- 'name' => 'swissup/subscription',
- 'type' => 'subscription-plan',
- 'description' => 'SwissUpLabs Modules Subscription',
- 'version' => '',
- 'extra' => [
- 'swissup' => [
- 'links' => [
- 'store' => 'https://swissuplabs.com',
- 'download' => 'https://swissuplabs.com/subscription/customer/products/',
- 'identity_key' => 'https://swissuplabs.com/license/customer/identity/'
- ]
- ]
- ]
- ];
-
return $modules;
}
@@ -323,8 +301,8 @@ protected function fetch($url)
/**
* Get packages url from satis repository.
*
- * To do that we send a request to http://docs.swissuplabs.com/packages/packages.json,
- * which returns actual packages list url: http://docs.swissuplabs.com/packages/include/all${sha1}.json
+ * To do that we send a request to https://swissup.github.io/packages-latest/packages.json,
+ * which returns actual packages list url: https://swissup.github.io/packages-latest/include/all${sha1}.json
*
* @return mixed
*/
@@ -362,7 +340,7 @@ protected function getPackagesUrlPrefix()
\Magento\Store\Model\ScopeInterface::SCOPE_STORE
);
- // docs.swissuplabs.com/packages
+ // swissup.github.io/packages-latest
return ($useHttps ? 'https://' : 'http://') . $url;
}
}
diff --git a/Model/Installer/Composer.php b/Model/Installer/Composer.php
new file mode 100644
index 0000000..dc983aa
--- /dev/null
+++ b/Model/Installer/Composer.php
@@ -0,0 +1,162 @@
+getRootDir() . '/vendor/composer/installed.json');
+
+ if (!$file->exists()) {
+ return true;
+ }
+
+ return $file->read()['dev'] ?? true;
+ }
+
+ /**
+ * Root requirements from composer.json
+ *
+ * @return array Package name => version constraint
+ */
+ public function getRequirements()
+ {
+ $file = new JsonFile($this->getRootDir() . '/composer.json');
+ $json = $file->exists() ? $file->read() : [];
+
+ return array_merge($json['require'] ?? [], $json['require-dev'] ?? []);
+ }
+
+ /**
+ * @return array Path => content (null if file does not exist)
+ */
+ public function backupFiles()
+ {
+ $backup = [];
+
+ foreach (['composer.json', 'composer.lock'] as $filename) {
+ $path = $this->getRootDir() . '/' . $filename;
+ $backup[$path] = is_file($path) ? file_get_contents($path) : null;
+ }
+
+ return $backup;
+ }
+
+ /**
+ * @param array $backup Result of backupFiles()
+ * @return void
+ * @throws \RuntimeException
+ */
+ public function restoreFiles(array $backup)
+ {
+ foreach ($backup as $path => $content) {
+ if ($content === null) {
+ if (is_file($path) && !unlink($path)) {
+ throw new \RuntimeException(sprintf('Unable to delete %s file', $path));
+ }
+ } elseif (file_put_contents($path, $content) === false) {
+ throw new \RuntimeException(sprintf('Unable to restore %s file', $path));
+ }
+ }
+ }
+
+ /**
+ * @param array $args
+ * @param OutputInterface $output
+ * @param boolean $interactive
+ * @return int Exit code
+ * @throws \RuntimeException
+ */
+ public function run(array $args, OutputInterface $output, $interactive = true)
+ {
+ if (!$this->process->canRun()) {
+ throw new \RuntimeException(sprintf(
+ 'proc_open is disabled. Run the command manually: composer %s',
+ implode(' ', $args)
+ ));
+ }
+
+ return $this->process->run($this->getCommand($args), $output, $interactive, $this->getEnv());
+ }
+
+ /**
+ * Run non-interactive composer command and return its stdout
+ *
+ * @param array $args
+ * @return string
+ * @throws \RuntimeException
+ */
+ public function capture(array $args)
+ {
+ return $this->process->capture($this->getCommand($args), $this->getEnv());
+ }
+
+ /**
+ * @return boolean
+ */
+ public function hasAuthJson()
+ {
+ return is_file($this->getRootDir() . '/auth.json');
+ }
+
+ /**
+ * @param array $args
+ * @return array
+ */
+ private function getCommand(array $args)
+ {
+ $args[] = '--working-dir=' . $this->getRootDir();
+
+ return array_merge([$this->getComposerBin()], $args);
+ }
+
+ /**
+ * @return array
+ */
+ private function getEnv()
+ {
+ return ['COMPOSER_HOME' => false]; // unset Magento's var/composer_home
+ }
+
+ /**
+ * @return string
+ */
+ private function getComposerBin()
+ {
+ // vendor/composer/composer/src/Composer/Composer.php
+ $file = (new \ReflectionClass(\Composer\Composer::class))->getFileName();
+
+ return dirname($file, 3) . '/bin/composer';
+ }
+
+ /**
+ * @return string
+ */
+ private function getRootDir()
+ {
+ return dirname($this->composerJsonFinder->findComposerJson());
+ }
+}
diff --git a/Model/Installer/ComposerRepository.php b/Model/Installer/ComposerRepository.php
new file mode 100644
index 0000000..3d2f33a
--- /dev/null
+++ b/Model/Installer/ComposerRepository.php
@@ -0,0 +1,418 @@
+getKey() !== null;
+ }
+
+ /**
+ * Add repository on top of the others (composer uses the first match)
+ * keeping composer.json formatting.
+ * Composer's JsonConfigSource is not used because since 2.9 it
+ * converts "repositories" object into the list.
+ *
+ * @return void
+ * @throws \RuntimeException
+ */
+ public function enable()
+ {
+ if ($this->isEnabled()) {
+ return;
+ }
+
+ $path = $this->getComposerJsonPath();
+ $json = (new JsonFile($path))->read();
+ $manipulator = new JsonManipulator(file_get_contents($path));
+ $config = [
+ 'type' => self::TYPE,
+ 'url' => self::URL,
+ ];
+
+ $repositories = $json['repositories'] ?? null;
+ if ($repositories && array_keys($repositories) === range(0, count($repositories) - 1)) {
+ // addListItem is not available in older composer versions
+ $result = method_exists($manipulator, 'addListItem')
+ && $manipulator->addListItem('repositories', $config, false);
+ } else {
+ $result = $manipulator->addSubNode('repositories', self::ID, $config, false);
+ }
+
+ if (!$result) {
+ throw new \RuntimeException(sprintf('Unable to update %s file', $path));
+ }
+
+ file_put_contents($path, $manipulator->getContents());
+ }
+
+ /**
+ * Remove repository keeping composer.json formatting
+ *
+ * @return void
+ * @throws \RuntimeException
+ */
+ public function disable()
+ {
+ $key = $this->getKey();
+ if ($key === null) {
+ return;
+ }
+
+ $path = $this->getComposerJsonPath();
+ $manipulator = new JsonManipulator(file_get_contents($path));
+
+ if (is_int($key)) {
+ // removeListItem is not available in older composer versions
+ $result = method_exists($manipulator, 'removeListItem')
+ && $manipulator->removeListItem('repositories', $key);
+ } else {
+ $result = $manipulator->removeSubNode('repositories', $key);
+ }
+
+ if (!$result) {
+ throw new \RuntimeException(sprintf('Unable to update %s file', $path));
+ }
+
+ file_put_contents($path, $manipulator->getContents());
+ }
+
+ /**
+ * Store domain is used as username for swissuplabs repository
+ *
+ * @return string
+ */
+ public function getDomain()
+ {
+ return (string) parse_url(
+ (string) $this->scopeConfig->getValue('web/unsecure/base_url'),
+ PHP_URL_HOST
+ );
+ }
+
+ /**
+ * Get credentials that composer uses: global auth.json,
+ * project's auth.json and COMPOSER_AUTH merged together.
+ *
+ * "http-basic" is read as a whole because older composer versions
+ * fail to parse "http-basic.ci.swissuplabs.com" key.
+ *
+ * @return array ['username' => string, 'password' => string]
+ * @throws \RuntimeException
+ */
+ public function getCredentials()
+ {
+ if ($this->credentials === null) {
+ $data = json_decode($this->composer->capture(['config', 'http-basic']), true);
+ if (!is_array($data)) {
+ throw new \RuntimeException('Unable to read http-basic credentials from composer config');
+ }
+
+ $this->credentials = [
+ 'username' => $data[self::HOSTNAME]['username'] ?? '',
+ 'password' => $data[self::HOSTNAME]['password'] ?? '',
+ ];
+ }
+
+ return $this->credentials;
+ }
+
+ /**
+ * Saved username or store domain for the new credentials
+ *
+ * @return string
+ */
+ public function getUsername()
+ {
+ return $this->getCredentials()['username'] ?: $this->getDomain();
+ }
+
+ /**
+ * Access keys are stored as space separated password
+ *
+ * @return string[]
+ */
+ public function getKeys()
+ {
+ $password = trim($this->getCredentials()['password']);
+
+ return $password === '' ? [] : preg_split('/\s+/', $password);
+ }
+
+ /**
+ * Get the site where the key was issued.
+ * Key format: base64(domain):secret:
+ *
+ * @param string $key
+ * @return string|null
+ */
+ public function getKeyDomain($key)
+ {
+ if (strpos($key, ':') === false) {
+ return null;
+ }
+
+ $domain = base64_decode(strtok($key, ':'), true);
+ if ($domain === false || !preg_match('/^[a-z0-9.-]+\.[a-z]{2,}$/i', $domain)) {
+ return null;
+ }
+
+ return $domain;
+ }
+
+ /**
+ * Check the key alone (to not save invalid key along with valid ones)
+ * and save it with all currently used keys.
+ *
+ * @param string $key
+ * @return array Packages available with this key
+ * @throws AuthenticationException
+ * @throws \RuntimeException
+ */
+ public function addKey($key)
+ {
+ $key = trim($key);
+ if ($key === '' || preg_match('/\s/', $key)) {
+ throw new \RuntimeException('Access key cannot be empty or contain spaces');
+ }
+
+ $username = $this->getUsername();
+ $packages = $this->getPackages($username, $key);
+
+ $keys = $this->getKeys();
+ if (!in_array($key, $keys, true)) {
+ $keys[] = $key;
+ $this->saveCredentials($username, implode(' ', $keys));
+ }
+
+ return $packages;
+ }
+
+ /**
+ * Remove the key from the project's auth.json file
+ *
+ * @param string $key
+ * @return boolean False if the key is still used from global auth.json
+ * @throws \RuntimeException
+ */
+ public function removeKey($key)
+ {
+ $keys = $this->getKeys();
+ if (!in_array($key, $keys, true)) {
+ throw new \RuntimeException('Access key is not found');
+ }
+
+ $keys = array_values(array_diff($keys, [$key]));
+ if ($keys) {
+ // project's entry overrides global one, so the key is not used anymore
+ $this->saveCredentials($this->getUsername(), implode(' ', $keys));
+ return true;
+ }
+
+ if ($this->composer->hasAuthJson()) {
+ $this->composer->capture(['config', '--unset', 'http-basic.' . self::HOSTNAME]);
+ $this->credentials = null;
+ }
+
+ return !in_array($key, $this->getKeys(), true);
+ }
+
+ /**
+ * Save credentials to the project's auth.json file.
+ * Password must contain all keys, including the ones from global auth.json,
+ * because project's entry overrides the global one.
+ *
+ * @param string $username
+ * @param string $password
+ * @return void
+ * @throws \RuntimeException
+ */
+ public function saveCredentials($username, $password)
+ {
+ $this->composer->capture(['config', 'http-basic.' . self::HOSTNAME, $username, $password]);
+ $this->credentials = null;
+ }
+
+ /**
+ * Fetch packages available for the given credentials
+ *
+ * @param string $username
+ * @param string $password
+ * @return array
+ * @throws AuthenticationException
+ * @throws \RuntimeException
+ */
+ public function getPackages($username, $password)
+ {
+ $packages = $this->getPackagesBatch($username, [$password])[$password];
+
+ if ($packages instanceof \Exception) {
+ throw $packages;
+ }
+
+ return $packages;
+ }
+
+ /**
+ * @param string $username
+ * @param string[] $passwords
+ * @return array [password => array|\Exception]
+ */
+ public function getPackagesBatch($username, array $passwords)
+ {
+ $requests = [];
+ foreach ($passwords as $password) {
+ $requests[$password] = ['url' => self::URL, 'password' => $password];
+ }
+
+ $responses = $this->fetchAll($username, $requests);
+ $packages = [];
+ $includeRequests = [];
+
+ foreach ($responses as $password => $response) {
+ if ($response instanceof \Exception) {
+ $packages[$password] = $response;
+ continue;
+ }
+
+ $packages[$password] = $response['packages'] ?? [];
+ foreach (array_keys($response['includes'] ?? []) as $include) {
+ $includeRequests[] = [
+ 'url' => dirname(self::URL) . '/' . ltrim($include, '/'),
+ 'password' => $password,
+ ];
+ }
+ }
+
+ foreach ($this->fetchAll($username, $includeRequests) as $i => $response) {
+ $password = $includeRequests[$i]['password'];
+ $packages[$password] = $response instanceof \Exception
+ ? $response
+ : array_merge($packages[$password], $response['packages'] ?? []);
+ }
+
+ return $packages;
+ }
+
+ /**
+ * Send all the requests before reading any response, so that they are
+ * performed in parallel.
+ *
+ * @param string $username
+ * @param array $requests [id => ['url' => string, 'password' => string]]
+ * @return array [id => array|\Exception]
+ */
+ private function fetchAll($username, array $requests)
+ {
+ $deferred = [];
+ foreach ($requests as $id => $request) {
+ $deferred[$id] = $this->asyncClient->request(new Request(
+ $request['url'],
+ Request::METHOD_GET,
+ ['Authorization' => 'Basic ' . base64_encode($username . ':' . $request['password'])],
+ null
+ ));
+ }
+
+ $responses = [];
+ foreach ($deferred as $id => $response) {
+ try {
+ $responses[$id] = $this->parse($username, $requests[$id]['url'], $response->get());
+ } catch (\Exception $e) {
+ $responses[$id] = $e;
+ }
+ }
+
+ return $responses;
+ }
+
+ /**
+ * @param string $username
+ * @param string $url
+ * @param Response $response
+ * @return array
+ * @throws AuthenticationException
+ * @throws \RuntimeException
+ */
+ private function parse($username, $url, Response $response)
+ {
+ $status = $response->getStatusCode();
+ if ($status === 401 || $status === 403) {
+ throw new AuthenticationException(__(
+ 'Access denied for "%1". Make sure the domain is activated and the key is correct.',
+ $username
+ ));
+ }
+
+ if ($status !== 200) {
+ throw new \RuntimeException(sprintf('%s returned %s response code', $url, $status));
+ }
+
+ $data = json_decode($response->getBody(), true);
+ if (!is_array($data)) {
+ throw new \RuntimeException(sprintf('%s returned malformed response', $url));
+ }
+
+ return $data;
+ }
+
+ /**
+ * Find repository in composer.json by its hostname.
+ * The name may differ from self::ID if repo was added manually.
+ *
+ * @return string|int|null Object key or list index
+ */
+ private function getKey()
+ {
+ $json = (new JsonFile($this->getComposerJsonPath()))->read();
+
+ foreach ($json['repositories'] ?? [] as $key => $repo) {
+ if (!is_array($repo) || empty($repo['url'])) {
+ continue;
+ }
+
+ if (parse_url($repo['url'], PHP_URL_HOST) === self::HOSTNAME) {
+ return $key;
+ }
+ }
+
+ return null;
+ }
+
+ /**
+ * @return string
+ */
+ private function getComposerJsonPath()
+ {
+ return $this->composerJsonFinder->findComposerJson();
+ }
+}
diff --git a/Model/Installer/Process.php b/Model/Installer/Process.php
new file mode 100644
index 0000000..7f5d6bd
--- /dev/null
+++ b/Model/Installer/Process.php
@@ -0,0 +1,99 @@
+canRun()) {
+ throw new \RuntimeException('proc_open is disabled');
+ }
+
+ $command[] = $output->isDecorated() ? '--ansi' : '--no-ansi';
+
+ if (!$interactive) {
+ $command[] = '--no-interaction';
+ }
+
+ if ($output->isQuiet()) {
+ $command[] = '--quiet';
+ } elseif ($output->isDebug()) {
+ $command[] = '-vvv';
+ } elseif ($output->isVeryVerbose()) {
+ $command[] = '-vv';
+ } elseif ($output->isVerbose()) {
+ $command[] = '-v';
+ }
+
+ array_unshift($command, PHP_BINARY);
+
+ $process = new SymfonyProcess($command, BP, $env);
+ $process->setTimeout(null);
+
+ if ($interactive && SymfonyProcess::isTtySupported()) {
+ $process->setTty(true);
+ return $process->run();
+ }
+
+ $errorOutput = $output instanceof ConsoleOutputInterface ? $output->getErrorOutput() : $output;
+
+ return $process->run(function ($type, $buffer) use ($output, $errorOutput) {
+ $stream = $type === SymfonyProcess::ERR ? $errorOutput : $output;
+ $stream->write($buffer, false, OutputInterface::OUTPUT_RAW);
+ });
+ }
+
+ /**
+ * Run non-interactive command and return its stdout
+ *
+ * @param array $command Script path and its arguments
+ * @param array $env
+ * @return string
+ * @throws \RuntimeException
+ */
+ public function capture(array $command, array $env = [])
+ {
+ if (!$this->canRun()) {
+ throw new \RuntimeException('proc_open is disabled');
+ }
+
+ array_unshift($command, PHP_BINARY);
+ $command[] = '--no-ansi';
+ $command[] = '--no-interaction';
+
+ $process = new SymfonyProcess($command, BP, $env);
+ $process->setTimeout(null);
+ $process->run();
+
+ if (!$process->isSuccessful()) {
+ $error = preg_replace('/\s+/', ' ', trim($process->getErrorOutput()));
+ throw new \RuntimeException($error ?: sprintf('Command failed with exit code %s', $process->getExitCode()));
+ }
+
+ return $process->getOutput();
+ }
+}
diff --git a/Model/Module.php b/Model/Module.php
deleted file mode 100644
index 3f49e50..0000000
--- a/Model/Module.php
+++ /dev/null
@@ -1,307 +0,0 @@
-licenseValidatorFactory = $licenseValidatorFactory;
- $this->installerFactory = $installerFactory;
- $this->packageInfo = $packageInfo;
- $this->remoteComponents = $remoteComponents;
- $this->localComponents = $localComponents;
- parent::__construct($context, $registry, $resource, $resourceCollection, $data);
- }
-
- /**
- * @return void
- */
- protected function _construct()
- {
- $this->_init('Swissup\Core\Model\ResourceModel\Module');
- }
-
- public function load($modelId, $field = null)
- {
- parent::load($modelId, $field);
-
- $this->setId($modelId);
-
- try {
- // array_filter to remove empty items caused by non-magento modules requirements
- $depends = array_filter($this->packageInfo->getRequire($this->getCode()));
- $version = $this->packageInfo->getVersion($this->getCode());
- $packageName = $this->packageInfo->getPackageName($this->getCode());
- } catch (\Exception $e) {
- $depends = [];
- $version = $e->getMessage() . ' (Third-party composer.json)';
- $packageName = $this->getCode();
- }
- $this->setDepends($depends);
- $this->setVersion($version);
- $this->setPackageName($packageName);
-
- return $this;
- }
-
- public function up()
- {
- $this->getInstaller()->up();
- }
-
- public function getInstaller()
- {
- if (null === $this->installer) {
- $this->installer = $this->installerFactory->create(['module' => $this]);
- }
- return $this->installer;
- }
-
- /**
- * Check is module already installed at any store
- *
- * @return boolean
- */
- public function isInstalled()
- {
- return $this->getDataVersion() && $this->getOldStores();
- }
-
- public function validateLicense()
- {
- return $this->licenseValidatorFactory->create(['module' => $this])->validate();
- }
-
- public function getRemote()
- {
- $remoteData = $this->remoteComponents->getItemById($this->getId());
- if (!$remoteData) {
- return false;
- }
- return new \Magento\Framework\DataObject($remoteData);
- }
-
- public function getLocal()
- {
- $localData = $this->localComponents->getItemById($this->getId());
- if (!$localData) {
- return false;
- }
- return new \Magento\Framework\DataObject($localData);
- }
-
- /**
- * Prepare store ids
- *
- * @return \Magento\Framework\Model\AbstractModel
- */
- public function beforeSave()
- {
- $oldStores = $this->getOldStores();
- $newStores = $this->getNewStoreIds();
- if (is_array($newStores)) {
- $stores = array_merge($oldStores, $newStores);
- $this->setStoreIds(implode(',', array_unique($stores)));
- }
- return parent::beforeSave();
- }
-
- /**
- * Retieve store ids, where the module is already installed
- *
- * @return array
- */
- public function getOldStores()
- {
- $ids = $this->getStoreIds();
- if (null === $ids || '' === $ids) {
- return array();
- }
- if (!is_array($ids)) {
- $ids = explode(',', $ids);
- }
- return $ids;
- }
-
- /**
- * Get the stores, where the module should be installed or reinstalled
- *
- * @return array
- */
- public function getNewStores()
- {
- $storeIds = $this->getNewStoreIds();
- if (!$storeIds) {
- return [];
- }
- return $storeIds;
- }
-
- /**
- * Set the stores, where the module should be installed or reinstalled
- *
- * @param array $ids
- * @return ModuleInterface
- */
- public function setNewStores(array $ids)
- {
- $this->setData('new_store_ids', array_unique($ids));
- return $this;
- }
-
- /**
- * Retrieve module code
- *
- * @return int
- */
- public function getId()
- {
- return $this->getData(self::CODE);
- }
-
- /**
- * Retrieve module code
- *
- * @return string
- */
- public function getCode()
- {
- return $this->getData(self::CODE);
- }
-
- /**
- * Retrieve module data version
- *
- * @return string
- */
- public function getDataVersion()
- {
- return $this->getData(self::DATA_VERSION);
- }
-
- /**
- * Retrieve module identity key
- *
- * @return string
- */
- public function getIdentityKey()
- {
- return $this->getData(self::IDENTITY_KEY);
- }
-
- /**
- * Retrieve module store ids
- *
- * @return string
- */
- public function getStoreIds()
- {
- return $this->getData(self::STORE_IDS);
- }
-
- /**
- * Set code
- *
- * @param string $id
- * @return ModuleInterface
- */
- public function setId($id)
- {
- return $this->setData(self::CODE, $id);
- }
-
- /**
- * Set code
- *
- * @param string $code
- * @return ModuleInterface
- */
- public function setCode($code)
- {
- return $this->setData(self::CODE, $code);
- }
-
- /**
- * Set data version
- *
- * @param string $dataVersion
- * @return ModuleInterface
- */
- public function setDataVersion($dataVersion)
- {
- return $this->setData(self::DATA_VERSION, $dataVersion);
- }
-
- /**
- * Set identity key
- *
- * @param string $identityKey
- * @return ModuleInterface
- */
- public function setIdentityKey($identityKey)
- {
- return $this->setData(self::IDENTITY_KEY, $identityKey);
- }
-
- /**
- * Set store ids
- *
- * @param string $storeIds Comma separated store ids
- * @return ModuleInterface
- */
- public function setStoreIds($storeIds)
- {
- return $this->setData(self::STORE_IDS, $storeIds);
- }
-}
diff --git a/Model/Module/Installer.php b/Model/Module/Installer.php
deleted file mode 100644
index 2ebed93..0000000
--- a/Model/Module/Installer.php
+++ /dev/null
@@ -1,93 +0,0 @@
-module = $module;
- $this->moduleFactory = $moduleFactory;
- $this->messageLogger = $messageLogger;
- }
-
- /**
- * 1. Run dependent modules upgrades
- * 2. Run module upgrades on installed stores
- * 3. Run module upgrades on new stores
- *
- * @return void
- */
- public function up()
- {
- $oldStores = $this->module->getOldStores();
- $newStores = $this->module->getNewStores();
- if (!count($oldStores) && !count($newStores)) {
- return;
- }
-
- foreach ($this->module->getDepends() as $moduleCode) {
- if (0 !== strpos($moduleCode, 'Swissup')) {
- continue;
- }
- $this->getModuleObject($moduleCode)->up();
- }
-
- $this->module->save();
- }
-
- /**
- * Retrieve singleton instance of error logger, used in upgrade file
- * to write errors and module controller to read them.
- *
- * @return \Swissup\Core\Model\Module\MessageLogger
- */
- public function getMessageLogger()
- {
- return $this->messageLogger;
- }
-
- /**
- * Returns loded module object with copied new_store_ids and skip_upgrade
- * instructions into it
- *
- * @return Swissup\Core\Model\Module
- */
- protected function getModuleObject($code)
- {
- $module = $this->moduleFactory->create()
- ->load($code)
- ->setNewStores($this->module->getNewStores());
-
- if (!$module->getIdentityKey()) {
- $module->setIdentityKey($this->module->getIdentityKey());
- }
-
- return $module;
- }
-}
diff --git a/Model/Module/LicenseValidator.php b/Model/Module/LicenseValidator.php
deleted file mode 100644
index 73c0ce7..0000000
--- a/Model/Module/LicenseValidator.php
+++ /dev/null
@@ -1,221 +0,0 @@
-request = $request;
- $this->module = $module;
- $this->scopeConfig = $scopeConfig;
- $this->jsonHelper = $jsonHelper;
- $this->curlFactory = $curlFactory;
- }
-
- /**
- * Checks if module should be validated
- *
- * @return boolean
- */
- protected function canValidate()
- {
- return $this->module->getRemote() &&
- $this->module->getRemote()->getIdentityKeyLink();
- }
-
- /**
- * Validate module using curl request
- *
- * @return boolean|array
- */
- public function validate()
- {
- if (!$this->canValidate()) {
- return true;
- }
-
- $key = trim($this->module->getIdentityKey());
- if (empty($key)) {
- return ['error' => ['Identity key is required']];
- }
-
- // key format is: encoded_site:secret_key:optional_suffix
- $parts = explode(':', $key);
- if (count($parts) < 3) {
- return ['error' => ['Identity key is not valid']];
- }
- list($site, $secret, $suffix) = explode(':', $key);
-
- try {
- $client = $this->curlFactory->create();
- $client->setConfig(['maxredirects' => 5, 'timeout' =>30]);
- $client->write(
- \Laminas\Http\Request::METHOD_GET,
- $this->getUrl($site, [
- 'key' => $secret,
- 'suffix' => $suffix,
- ])
- );
- $responseString = $client->read();
- $responseParts = preg_split('|(?:\r\n){2}|m', $responseString, 2);
- $responseBody = trim($responseParts[1] ?? '');
-
- $client->close();
- } catch (\Exception $e) {
- return [
- 'error' => [
- 'Response error: %1', $e->getMessage()
- ],
- 'response' => $e->getTraceAsString()
- ];
- }
-
- return $this->parseResponse($responseBody);
- }
-
- /**
- * Parse server response
- *
- * @param string $response
- *
- * "{success: true}" or "{error: error_message}"
- *
- */
- protected function parseResponse($response)
- {
- try {
- $result = $this->jsonHelper->jsonDecode($response);
- if (!is_array($result)) {
- throw new \Exception('Decoding failed');
- }
- if (is_array($result) && isset($result['error'])) {
- $result['error'][0] = $this->convertMagento1xTranslation($result['error'][0]);
- }
- } catch (\Exception $e) {
- $result = [
- 'error' => [
- 'Sorry, try again in five minutes. Validation response parsing error: %1',
- $e->getMessage()
- ],
- 'response' => $response
- ];
- }
- return $result;
- }
-
- /**
- * Convert Magento 1.x translation phrase into 2.x standard:
- *
- * %s replaced with %1...%n
- *
- * @param string $text
- * @return string
- */
- protected function convertMagento1xTranslation($text)
- {
- $parts = explode('%s', $text);
- $result = $parts[0];
- unset($parts[0]);
- foreach ($parts as $i => $part) {
- $result .= '%' . $i . $part;
- }
- return $result;
- }
-
- /**
- * Retrieve validation url according to the encoded $site
- *
- * @param string $site Base64 encoded site url
- * @param array $queryParams
- */
- protected function getUrl($site, array $queryParams = [])
- {
- $useHttps = $this->scopeConfig->getValue(
- self::XML_USE_HTTPS_PATH,
- \Magento\Store\Model\ScopeInterface::SCOPE_STORE
- );
- $url = $this->scopeConfig->getValue(
- self::XML_VALIDATE_URL_PATH,
- \Magento\Store\Model\ScopeInterface::SCOPE_STORE
- );
-
- $site = base64_decode($site);
- $url = ($useHttps ? 'https://' : 'http://') . rtrim($site, '/ ') . $url;
- $url .= '?' . http_build_query(array_merge($this->getQueryParams(), $queryParams));
-
- return $url;
- }
-
- /**
- * Prepare query parameters for the request
- *
- * @return array
- */
- protected function getQueryParams()
- {
- $purchaseCode = $this->module->getRemote()->getPurchaseCode();
- if (!$purchaseCode) {
- $purchaseCode = $this->module->getCode();
- }
-
- $domain = $this->module->getDomain();
- if (empty($domain)) {
- $domain = $this->request->getHttpHost();
- }
-
- $params = [
- 'module' => $purchaseCode,
- 'module_code' => $this->module->getCode(),
- 'domain' => $domain,
- ];
-
- if ($this->module->getConfigSection()) {
- $params['config_section'] = $this->module->getConfigSection();
- }
-
- return $params;
- }
-}
diff --git a/Model/Module/MessageLogger.php b/Model/Module/MessageLogger.php
deleted file mode 100644
index 379a355..0000000
--- a/Model/Module/MessageLogger.php
+++ /dev/null
@@ -1,58 +0,0 @@
- array(),
- 'notices' => array(),
- 'success' => array()
- );
-
- /**
- * @param string $group
- * @param mixed $error array or string with error message
- *
- * message required
- * trace optional
- *
- */
- public function addError($group, $error)
- {
- $this->messages['errors'][$group][] = $error;
- }
-
- public function getErrors()
- {
- return $this->messages['errors'];
- }
-
- /**
- * @param string $group
- * @param string $notice
- */
- public function addNotice($group, $notice)
- {
- $this->messages['notices'][$group][] = $notice;
- }
-
- public function getNotices()
- {
- return $this->messages['notices'];
- }
-
- /**
- * @param string $group
- * @param string $success
- */
- public function addSuccess($group, $success)
- {
- $this->messages['success'][$group][] = $success;
- }
-
- public function getSuccess()
- {
- return $this->messages['success'];
- }
-}
diff --git a/Model/ResourceModel/Module.php b/Model/ResourceModel/Module.php
deleted file mode 100644
index 0e490c3..0000000
--- a/Model/ResourceModel/Module.php
+++ /dev/null
@@ -1,23 +0,0 @@
-_init('swissup_core_module', 'code');
- }
-}
diff --git a/Model/ResourceModel/Module/Collection.php b/Model/ResourceModel/Module/Collection.php
deleted file mode 100644
index ffffaa1..0000000
--- a/Model/ResourceModel/Module/Collection.php
+++ /dev/null
@@ -1,25 +0,0 @@
-_init('Swissup\Core\Model\Module', 'Swissup\Core\Model\ResourceModel\Module');
- }
-}
diff --git a/README.md b/README.md
index 6c885b0..64d9741 100644
--- a/README.md
+++ b/README.md
@@ -1,7 +1,7 @@
-# Core
+# Swissup Core
-Swissup_Core module adds menu and config entries to Magento backend. It also
-utilize some common tasks used by other modules.
+This module ships Swissup Installer and adds Swissup menu and config entries
+to Magento backend. It also provides a set of common tasks used by other modules.
## Installation
@@ -10,6 +10,25 @@ composer require swissup/module-core
bin/magento setup:upgrade
```
+## Swissup Installer
+
+Available commands
+
+Command | Description
+------------------------------------------------|---------------------------------------
+`bin/magento swissup:channel:enable` | Add SwissupLabs repository to composer.json file
+`bin/magento swissup:channel:disable` | Remove SwissupLabs repository from composer.json file
+**Authorization** |
+`bin/magento swissup:auth:add {key}` | Add SwissupLabs access key
+`bin/magento swissup:auth:check` | Display SwissupLabs access keys currently in use with count of available packages per key
+`bin/magento swissup:auth:remove {key}` | Remove SwissupLabs access key
+`bin/magento swissup:auth:show` | Display SwissupLabs access username and password currently in use
+**Packages** |
+`bin/magento swissup:package:require {package}` | Download SwissupLabs package(s) using composer and run setup:upgrade
+`bin/magento swissup:package:install {package}` | Run installer for downloaded package
+`bin/magento swissup:package:update` | Update SwissupLabs package(s) using composer and run setup:upgrade
+`bin/magento swissup:package:remove {package}` | Remove SwissupLabs package(s) using composer and run setup:upgrade
+
## Popup Message Manager
Popup message manager allows to show regular Magento messages with additional
diff --git a/Ui/DataProvider/ModuleListingDataProvider.php b/Ui/DataProvider/ModuleListingDataProvider.php
index 6928126..2b1dcd0 100644
--- a/Ui/DataProvider/ModuleListingDataProvider.php
+++ b/Ui/DataProvider/ModuleListingDataProvider.php
@@ -10,9 +10,6 @@
use Magento\Framework\View\Element\UiComponent\DataProvider\DataProvider;
use Swissup\Core\Model\ComponentList\Loader;
-/**
- * Swissup modules grid data, taken from the component list instead of the database
- */
class ModuleListingDataProvider extends DataProvider
{
const SEARCH_FIELDS = ['code', 'name'];
@@ -62,7 +59,7 @@ public function __construct(
public function getData()
{
$criteria = $this->getSearchCriteria();
- $items = $this->loader->getItems();
+ $items = $this->loader->getModuleItems();
foreach ($criteria->getFilterGroups() as $group) {
foreach ($group->getFilters() as $filter) {
@@ -86,7 +83,7 @@ public function getData()
break;
}
- $field = $sortOrder->getField();
+ $field = (string) $sortOrder->getField();
$result = $this->normalize($field, $a[$field] ?? null)
<=> $this->normalize($field, $b[$field] ?? null);
diff --git a/composer.json b/composer.json
index f6a117c..b4b1681 100644
--- a/composer.json
+++ b/composer.json
@@ -1,9 +1,16 @@
{
"name": "swissup/module-core",
- "description": "Swissup core module. It's purpose is to add Swissup menu and config entries",
+ "description": "Swissup core module. Your starting point to use SwissupLabs packages.",
"type": "magento2-module",
"version": "1.13.1",
"license": "OSL-3.0",
+ "require": {
+ "php": "^8.0",
+ "composer/composer": "^2.0",
+ "psr/log": "^1.0 || ^2.0 || ^3.0",
+ "symfony/console": "^4.4 || ^5.0 || ^6.0 || ^7.0",
+ "symfony/process": "^4.4 || ^5.0 || ^6.0 || ^7.0"
+ },
"autoload": {
"files": [ "registration.php" ],
"psr-4": {
diff --git a/etc/db_schema.xml b/etc/db_schema.xml
deleted file mode 100644
index 5884aa2..0000000
--- a/etc/db_schema.xml
+++ /dev/null
@@ -1,28 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/etc/db_schema_whitelist.json b/etc/db_schema_whitelist.json
deleted file mode 100644
index fa3f925..0000000
--- a/etc/db_schema_whitelist.json
+++ /dev/null
@@ -1,26 +0,0 @@
-{
- "swissup_core_module": {
- "column": {
- "code": true,
- "data_version": true,
- "identity_key": true,
- "store_ids": true,
- "name": true,
- "description": true,
- "keywords": true,
- "type": true,
- "version": true,
- "release_date": true,
- "link": true,
- "download_link": true,
- "identity_key_link": true,
- "latest_version": true
- },
- "index": {
- "SWISSUP_CORE_MODULE_CODE_NAME_DESCRIPTION_KEYWORDS": true
- },
- "constraint": {
- "PRIMARY": true
- }
- }
-}
\ No newline at end of file
diff --git a/etc/di.xml b/etc/di.xml
index 97297fd..bc1ab1d 100644
--- a/etc/di.xml
+++ b/etc/di.xml
@@ -7,6 +7,16 @@
- Swissup\Core\Console\Command\ModuleListCommand
- Swissup\Core\Console\Command\ThemeCreateCommand
- Swissup\Core\Console\Command\StoreCreateCommand
+ - Swissup\Core\Console\Command\Installer\ChannelEnableCommand
+ - Swissup\Core\Console\Command\Installer\ChannelDisableCommand
+ - Swissup\Core\Console\Command\Installer\PackageRequireCommand
+ - Swissup\Core\Console\Command\Installer\PackageUpdateCommand
+ - Swissup\Core\Console\Command\Installer\PackageInstallCommand
+ - Swissup\Core\Console\Command\Installer\PackageRemoveCommand
+ - Swissup\Core\Console\Command\Installer\AuthShowCommand
+ - Swissup\Core\Console\Command\Installer\AuthCheckCommand
+ - Swissup\Core\Console\Command\Installer\AuthAddCommand
+ - Swissup\Core\Console\Command\Installer\AuthRemoveCommand
diff --git a/etc/module.xml b/etc/module.xml
index 1d8b430..7209af9 100644
--- a/etc/module.xml
+++ b/etc/module.xml
@@ -1,5 +1,5 @@
-
+
diff --git a/view/adminhtml/ui_component/swissup_module_manager.xml b/view/adminhtml/ui_component/swissup_module_manager.xml
index fdeb965..3f23e31 100644
--- a/view/adminhtml/ui_component/swissup_module_manager.xml
+++ b/view/adminhtml/ui_component/swissup_module_manager.xml
@@ -103,11 +103,11 @@
-
-
-
- Read Documentation
+ - Documentation
- docs_link
-
-
- Read Changelog
+ - Changelog
- changelog_link