mxrb 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/LICENSE +21 -0
- data/README.de-DE.md +108 -0
- data/README.md +172 -0
- data/README.pt-BR.md +109 -0
- data/bin/mxrb +1613 -0
- data/docs/README.md +10 -0
- data/docs/de-DE/README.md +24 -0
- data/docs/de-DE/architectural-patterns.md +26 -0
- data/docs/de-DE/architectural-standard.md +48 -0
- data/docs/de-DE/architecture.md +76 -0
- data/docs/de-DE/compiler.md +162 -0
- data/docs/de-DE/conventions.md +19 -0
- data/docs/de-DE/design-system.md +52 -0
- data/docs/de-DE/entity-dsl.md +53 -0
- data/docs/de-DE/native-runtime-quality-report.md +74 -0
- data/docs/de-DE/oql-sql.md +211 -0
- data/docs/de-DE/platform-api-integration.md +36 -0
- data/docs/de-DE/platform-operations.md +243 -0
- data/docs/de-DE/project-structure.md +32 -0
- data/docs/de-DE/ruby-first-roadmap.md +116 -0
- data/docs/de-DE/scaffolds.md +59 -0
- data/docs/de-DE/semantic-refactoring.md +56 -0
- data/docs/de-DE/team-server.md +33 -0
- data/docs/de-DE/validation-matrix.md +64 -0
- data/docs/de-DE/vetclinic-acceptance.md +24 -0
- data/docs/de-DE/writing.md +138 -0
- data/docs/en-US/README.md +24 -0
- data/docs/en-US/architectural-patterns.md +151 -0
- data/docs/en-US/architectural-standard.md +89 -0
- data/docs/en-US/architecture.md +170 -0
- data/docs/en-US/compiler.md +157 -0
- data/docs/en-US/conventions.md +82 -0
- data/docs/en-US/design-system.md +163 -0
- data/docs/en-US/entity-dsl.md +53 -0
- data/docs/en-US/native-runtime-quality-report.md +73 -0
- data/docs/en-US/oql-sql.md +201 -0
- data/docs/en-US/platform-api-integration.md +36 -0
- data/docs/en-US/platform-operations.md +244 -0
- data/docs/en-US/project-structure.md +49 -0
- data/docs/en-US/ruby-first-roadmap.md +186 -0
- data/docs/en-US/scaffolds.md +57 -0
- data/docs/en-US/semantic-refactoring.md +198 -0
- data/docs/en-US/team-server.md +32 -0
- data/docs/en-US/validation-matrix.md +257 -0
- data/docs/en-US/vetclinic-acceptance.md +23 -0
- data/docs/en-US/writing.md +655 -0
- data/docs/pt-BR/README.md +24 -0
- data/docs/pt-BR/architectural-patterns.md +25 -0
- data/docs/pt-BR/architectural-standard.md +533 -0
- data/docs/pt-BR/architecture.md +300 -0
- data/docs/pt-BR/compiler.md +162 -0
- data/docs/pt-BR/conventions.md +19 -0
- data/docs/pt-BR/design-system.md +65 -0
- data/docs/pt-BR/entity-dsl.md +116 -0
- data/docs/pt-BR/native-runtime-quality-report.md +73 -0
- data/docs/pt-BR/oql-sql.md +208 -0
- data/docs/pt-BR/platform-api-integration.md +36 -0
- data/docs/pt-BR/platform-operations.md +260 -0
- data/docs/pt-BR/project-structure.md +32 -0
- data/docs/pt-BR/ruby-first-roadmap.md +239 -0
- data/docs/pt-BR/scaffolds.md +104 -0
- data/docs/pt-BR/semantic-refactoring.md +55 -0
- data/docs/pt-BR/team-server.md +59 -0
- data/docs/pt-BR/validation-matrix.md +86 -0
- data/docs/pt-BR/vetclinic-acceptance.md +50 -0
- data/docs/pt-BR/writing.md +177 -0
- data/examples/sudoku_evaluation.rb +14 -0
- data/examples/sudoku_functional_test.rb +28 -0
- data/lib/mxrb/architecture/graph.rb +124 -0
- data/lib/mxrb/architecture/validator.rb +98 -0
- data/lib/mxrb/benchmark.rb +33 -0
- data/lib/mxrb/compare.rb +390 -0
- data/lib/mxrb/compiler/adapter.rb +96 -0
- data/lib/mxrb/compiler/artifact_document_compiler.rb +102 -0
- data/lib/mxrb/compiler/artifact_materializer.rb +53 -0
- data/lib/mxrb/compiler/client_model_materializer.rb +42 -0
- data/lib/mxrb/compiler/code_action_document_compiler.rb +64 -0
- data/lib/mxrb/compiler/code_action_materializer.rb +35 -0
- data/lib/mxrb/compiler/code_action_type_compiler.rb +32 -0
- data/lib/mxrb/compiler/combo_box_bundle_compiler.rb +314 -0
- data/lib/mxrb/compiler/compatibility_analyzer.rb +169 -0
- data/lib/mxrb/compiler/constants_materializer.rb +111 -0
- data/lib/mxrb/compiler/data_grid_bundle_compiler.rb +290 -0
- data/lib/mxrb/compiler/database_connector_action_compiler.rb +280 -0
- data/lib/mxrb/compiler/deployment_asset_copier.rb +60 -0
- data/lib/mxrb/compiler/deployment_bootstrapper.rb +208 -0
- data/lib/mxrb/compiler/deployment_materializer.rb +43 -0
- data/lib/mxrb/compiler/deployment_metadata.rb +64 -0
- data/lib/mxrb/compiler/domain_document_compiler.rb +153 -0
- data/lib/mxrb/compiler/domain_model_materializer.rb +35 -0
- data/lib/mxrb/compiler/domain_security_compiler.rb +82 -0
- data/lib/mxrb/compiler/gallery_bundle_compiler.rb +190 -0
- data/lib/mxrb/compiler/generic_widget_bundle_compiler.rb +281 -0
- data/lib/mxrb/compiler/image_bundle_compiler.rb +216 -0
- data/lib/mxrb/compiler/java_proxy_generator.rb +509 -0
- data/lib/mxrb/compiler/legacy_data_grid_compiler.rb +267 -0
- data/lib/mxrb/compiler/legacy_page_builder.rb +287 -0
- data/lib/mxrb/compiler/mda.rb +99 -0
- data/lib/mxrb/compiler/microflow_document_compiler.rb +85 -0
- data/lib/mxrb/compiler/microflow_materializer.rb +48 -0
- data/lib/mxrb/compiler/microflow_node_compiler.rb +250 -0
- data/lib/mxrb/compiler/model_package.rb +116 -0
- data/lib/mxrb/compiler/model_values.rb +67 -0
- data/lib/mxrb/compiler/nanoflow_program_compiler.rb +483 -0
- data/lib/mxrb/compiler/navigation_document_compiler.rb +93 -0
- data/lib/mxrb/compiler/packager.rb +100 -0
- data/lib/mxrb/compiler/page_bundle_builder.rb +56 -0
- data/lib/mxrb/compiler/page_bundle_compiler.rb +2004 -0
- data/lib/mxrb/compiler/page_document_compiler.rb +77 -0
- data/lib/mxrb/compiler/portable_packager.rb +356 -0
- data/lib/mxrb/compiler/project_jar_archive.rb +84 -0
- data/lib/mxrb/compiler/project_jar_builder.rb +121 -0
- data/lib/mxrb/compiler/project_materializer.rb +139 -0
- data/lib/mxrb/compiler/project_model_orderer.rb +52 -0
- data/lib/mxrb/compiler/runtime_data_types.rb +31 -0
- data/lib/mxrb/compiler/runtime_model_schema.rb +90 -0
- data/lib/mxrb/compiler/schemas/runtime-10.24.0.73019.json +1052 -0
- data/lib/mxrb/compiler/schemas/runtime-11.json +1215 -0
- data/lib/mxrb/compiler/schemas/runtime-6.10.8.json +1148 -0
- data/lib/mxrb/compiler/schemas/runtime-7.17.0.json +1144 -0
- data/lib/mxrb/compiler/schemas/runtime-7.5.0.json +1215 -0
- data/lib/mxrb/compiler/schemas/runtime-9.6.1.29396.json +868 -0
- data/lib/mxrb/compiler/schemas/system-model-10.24.0.73019.b64 +1135 -0
- data/lib/mxrb/compiler/schemas/system-model-11.12.1.b64 +815 -0
- data/lib/mxrb/compiler/schemas/system-model-6.10.8.b64 +497 -0
- data/lib/mxrb/compiler/schemas/system-model-7.17.0.b64 +519 -0
- data/lib/mxrb/compiler/schemas/system-model-7.5.0.b64 +504 -0
- data/lib/mxrb/compiler/schemas/system-model-9.6.1.29396.b64 +816 -0
- data/lib/mxrb/compiler/security_materializer.rb +104 -0
- data/lib/mxrb/compiler/settings_document_compiler.rb +45 -0
- data/lib/mxrb/compiler/settings_materializer.rb +25 -0
- data/lib/mxrb/compiler/source_model.rb +128 -0
- data/lib/mxrb/compiler/system_model_seed.rb +88 -0
- data/lib/mxrb/compiler/system_queue_materializer.rb +54 -0
- data/lib/mxrb/compiler/system_text_materializer.rb +49 -0
- data/lib/mxrb/compiler/translation_materializer.rb +95 -0
- data/lib/mxrb/compiler/web_bundle_builder.rb +155 -0
- data/lib/mxrb/compiler/web_list_data_source.rb +139 -0
- data/lib/mxrb/compiler/web_operation_compiler.rb +614 -0
- data/lib/mxrb/compiler/web_shell_materializer.rb +194 -0
- data/lib/mxrb/compiler/widget_package_extractor.rb +63 -0
- data/lib/mxrb/doctor.rb +116 -0
- data/lib/mxrb/dsl/builder.rb +1636 -0
- data/lib/mxrb/errors.rb +18 -0
- data/lib/mxrb/evaluation.rb +131 -0
- data/lib/mxrb/exporter.rb +1716 -0
- data/lib/mxrb/frontend/migrator.rb +775 -0
- data/lib/mxrb/functional.rb +307 -0
- data/lib/mxrb/github/annotator.rb +161 -0
- data/lib/mxrb/initializer.rb +257 -0
- data/lib/mxrb/integrity/validator.rb +136 -0
- data/lib/mxrb/io/bson_codec.rb +145 -0
- data/lib/mxrb/io/mpr_file.rb +558 -0
- data/lib/mxrb/io/mxunit_codec.rb +42 -0
- data/lib/mxrb/marketplace.rb +380 -0
- data/lib/mxrb/model/association.rb +98 -0
- data/lib/mxrb/model/attribute.rb +100 -0
- data/lib/mxrb/model/connector.rb +17 -0
- data/lib/mxrb/model/design_materializer.rb +133 -0
- data/lib/mxrb/model/design_migration.rb +126 -0
- data/lib/mxrb/model/design_system.rb +111 -0
- data/lib/mxrb/model/domain_model.rb +51 -0
- data/lib/mxrb/model/entity.rb +180 -0
- data/lib/mxrb/model/menu.rb +38 -0
- data/lib/mxrb/model/microflow.rb +100 -0
- data/lib/mxrb/model/module.rb +142 -0
- data/lib/mxrb/model/navigation.rb +127 -0
- data/lib/mxrb/model/page.rb +410 -0
- data/lib/mxrb/model/project.rb +187 -0
- data/lib/mxrb/model/unit.rb +48 -0
- data/lib/mxrb/module_initializer.rb +75 -0
- data/lib/mxrb/official_marketplace/content_api.rb +292 -0
- data/lib/mxrb/official_marketplace/dependency_resolver.rb +329 -0
- data/lib/mxrb/official_marketplace/lifecycle.rb +367 -0
- data/lib/mxrb/official_marketplace/module_package_importer.rb +318 -0
- data/lib/mxrb/official_marketplace/widget_package_installer.rb +414 -0
- data/lib/mxrb/official_marketplace.rb +720 -0
- data/lib/mxrb/oql/analyzer.rb +155 -0
- data/lib/mxrb/oql/index_advisor.rb +85 -0
- data/lib/mxrb/oql/plan_analyzer.rb +185 -0
- data/lib/mxrb/oql/server.rb +132 -0
- data/lib/mxrb/oql/sql_server_plan_analyzer.rb +190 -0
- data/lib/mxrb/oql/sql_server_workload_analyzer.rb +80 -0
- data/lib/mxrb/oql/workload_analyzer.rb +175 -0
- data/lib/mxrb/oql/workload_baseline.rb +60 -0
- data/lib/mxrb/oql.rb +424 -0
- data/lib/mxrb/project_lifecycle.rb +76 -0
- data/lib/mxrb/protocols/plan.rb +86 -0
- data/lib/mxrb/protocols.rb +123 -0
- data/lib/mxrb/runtime/database_workspace.rb +677 -0
- data/lib/mxrb/runtime/docker_executor.rb +41 -0
- data/lib/mxrb/runtime/docker_workspace.rb +29 -0
- data/lib/mxrb/runtime/executor.rb +169 -0
- data/lib/mxrb/runtime/java_locator.rb +26 -0
- data/lib/mxrb/runtime/native.rb +474 -0
- data/lib/mxrb/runtime/sql_server_database.rb +146 -0
- data/lib/mxrb/runtime/toolchain.rb +82 -0
- data/lib/mxrb/scaffold/cli.rb +113 -0
- data/lib/mxrb/scaffold/generator.rb +246 -0
- data/lib/mxrb/scaffold/help.rb +80 -0
- data/lib/mxrb/scaffold/page_templates.rb +54 -0
- data/lib/mxrb/scaffold/recipes.rb +198 -0
- data/lib/mxrb/scaffold/registry.rb +82 -0
- data/lib/mxrb/scaffold/templates.rb +723 -0
- data/lib/mxrb/scaffold/transaction.rb +84 -0
- data/lib/mxrb/schema/tables.rb +55 -0
- data/lib/mxrb/semantic/analyzer.rb +473 -0
- data/lib/mxrb/semantic/batch_plan.rb +68 -0
- data/lib/mxrb/semantic/domain_mutator.rb +419 -0
- data/lib/mxrb/semantic/embedder.rb +51 -0
- data/lib/mxrb/semantic/extractor.rb +290 -0
- data/lib/mxrb/semantic/index.rb +623 -0
- data/lib/mxrb/semantic/inliner.rb +216 -0
- data/lib/mxrb/semantic/mover.rb +153 -0
- data/lib/mxrb/semantic/onnx_embedder.rb +34 -0
- data/lib/mxrb/semantic/remover.rb +70 -0
- data/lib/mxrb/semantic/renamer.rb +211 -0
- data/lib/mxrb/semantic/tfidf_embedder.rb +46 -0
- data/lib/mxrb/semantic/vec_store.rb +75 -0
- data/lib/mxrb/team_server.rb +390 -0
- data/lib/mxrb/templates/project/10.24.0.73019.json +32 -0
- data/lib/mxrb/templates/project/11.12.1.json +32 -0
- data/lib/mxrb/templates/project/6.10.8.json +50 -0
- data/lib/mxrb/templates/project/7.17.0.json +50 -0
- data/lib/mxrb/templates/project/7.5.0.json +50 -0
- data/lib/mxrb/templates/project/9.6.1.29396.json +50 -0
- data/lib/mxrb/version.rb +5 -0
- data/lib/mxrb/widget_package.rb +338 -0
- data/lib/mxrb/widget_synchronizer.rb +31 -0
- data/lib/mxrb/writer.rb +3775 -0
- data/lib/mxrb.rb +188 -0
- data/marketplace/catalog.json +10 -0
- data/marketplace/modules/shared-kernel/domain/README.md +4 -0
- data/marketplace/modules/shared-kernel/module.rb +5 -0
- data/marketplace/modules/shared-kernel/mxrb-module.json +9 -0
- metadata +447 -0
|
@@ -0,0 +1,414 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'digest'
|
|
4
|
+
require 'fileutils'
|
|
5
|
+
require 'json'
|
|
6
|
+
require 'rexml/document'
|
|
7
|
+
require 'zip'
|
|
8
|
+
|
|
9
|
+
module Mxrb
|
|
10
|
+
module OfficialMarketplace
|
|
11
|
+
WidgetInstallation = Data.define(
|
|
12
|
+
:package, :destination, :sha256, :widget_name, :widget_ids, :archive
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
# Validates the two official Marketplace package envelopes MXRB accepts.
|
|
16
|
+
# rubocop:disable Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
|
|
17
|
+
class PackageEnvelope
|
|
18
|
+
def self.kind(path)
|
|
19
|
+
Zip::File.open(path) do |archive|
|
|
20
|
+
package = archive.find_entry('package.xml')
|
|
21
|
+
raise MarketplaceError, 'package.xml is missing' unless package
|
|
22
|
+
|
|
23
|
+
document = REXML::Document.new(package.get_input_stream.read)
|
|
24
|
+
names = document.root&.elements&.map(&:name) || []
|
|
25
|
+
return :module if names.include?('modelerProject')
|
|
26
|
+
return :widget if names.include?('clientModule')
|
|
27
|
+
|
|
28
|
+
raise MarketplaceError, 'package.xml does not declare a module or widget'
|
|
29
|
+
end
|
|
30
|
+
rescue Zip::Error, REXML::ParseException => e
|
|
31
|
+
raise MarketplaceError, "invalid Mendix package: #{e.message}"
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
# rubocop:enable Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
|
|
35
|
+
|
|
36
|
+
# Read-only, fail-closed inventory for a clientModule MPK.
|
|
37
|
+
# rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength
|
|
38
|
+
class WidgetPackageInventory
|
|
39
|
+
attr_reader :name, :version, :widget_ids, :project_filename
|
|
40
|
+
|
|
41
|
+
def initialize(name:, version:, widget_ids:, project_filename:)
|
|
42
|
+
@name = name
|
|
43
|
+
@version = version
|
|
44
|
+
@widget_ids = widget_ids.freeze
|
|
45
|
+
@project_filename = project_filename
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def self.read(path)
|
|
49
|
+
Zip::File.open(path) do |archive|
|
|
50
|
+
validate_entries!(archive)
|
|
51
|
+
document = package_document(archive)
|
|
52
|
+
clients = elements(document.root, 'clientModule')
|
|
53
|
+
raise MarketplaceError, 'package.xml must declare exactly one clientModule' unless clients.one?
|
|
54
|
+
|
|
55
|
+
client = clients.first
|
|
56
|
+
|
|
57
|
+
name = valid_name(client.attributes['name'])
|
|
58
|
+
version = valid_version(client.attributes['version'])
|
|
59
|
+
widget_paths = elements(client, 'widgetFile').map do |element|
|
|
60
|
+
safe_relative_path(element.attributes['path'])
|
|
61
|
+
end.uniq
|
|
62
|
+
raise MarketplaceError, 'widget package does not declare widgetFiles' if widget_paths.empty?
|
|
63
|
+
|
|
64
|
+
validate_declared_files!(archive, client)
|
|
65
|
+
widget_ids = widget_paths.map { widget_id(archive, _1) }
|
|
66
|
+
filename = project_filename(name, widget_ids)
|
|
67
|
+
new(name:, version:, widget_ids:, project_filename: filename)
|
|
68
|
+
end
|
|
69
|
+
rescue Zip::Error, REXML::ParseException => e
|
|
70
|
+
raise MarketplaceError, "invalid Mendix widget package: #{e.message}"
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
class << self
|
|
74
|
+
private
|
|
75
|
+
|
|
76
|
+
def package_document(archive)
|
|
77
|
+
package = archive.find_entry('package.xml')
|
|
78
|
+
raise MarketplaceError, 'package.xml is missing' unless package
|
|
79
|
+
|
|
80
|
+
REXML::Document.new(package.get_input_stream.read)
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def validate_entries!(archive)
|
|
84
|
+
normalized = {}
|
|
85
|
+
archive.each do |entry|
|
|
86
|
+
path = safe_relative_path(entry.name, directory: true)
|
|
87
|
+
key = path.downcase
|
|
88
|
+
raise MarketplaceError, "duplicate widget package path #{entry.name.inspect}" if normalized[key]
|
|
89
|
+
raise MarketplaceError, "symbolic link in widget package: #{entry.name}" if entry.symlink?
|
|
90
|
+
|
|
91
|
+
normalized[key] = true
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def validate_declared_files!(archive, client)
|
|
96
|
+
elements(client, 'file').each do |element|
|
|
97
|
+
relative = safe_relative_path(element.attributes['path'], directory: true)
|
|
98
|
+
present = archive.any? do |entry|
|
|
99
|
+
normalized = entry.name.to_s.tr('\\', '/').delete_suffix('/')
|
|
100
|
+
normalized == relative || normalized.start_with?("#{relative}/")
|
|
101
|
+
end
|
|
102
|
+
raise MarketplaceError, "declared widget file is missing: #{relative}" unless present
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def widget_id(archive, relative)
|
|
107
|
+
entry = archive.find_entry(relative) || archive.find_entry(relative.tr('/', '\\'))
|
|
108
|
+
raise MarketplaceError, "declared widget definition is missing: #{relative}" unless entry&.file?
|
|
109
|
+
|
|
110
|
+
root = REXML::Document.new(entry.get_input_stream.read).root
|
|
111
|
+
value = root&.name == 'widget' ? root.attributes['id'].to_s : ''
|
|
112
|
+
return value if value.match?(/\A[A-Za-z][A-Za-z0-9_]*(?:\.[A-Za-z][A-Za-z0-9_]*)+\z/)
|
|
113
|
+
|
|
114
|
+
raise MarketplaceError, "invalid widget ID #{value.inspect} in #{relative}"
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def project_filename(name, widget_ids)
|
|
118
|
+
namespaces = widget_ids.map { _1.split('.')[0...-2] }.uniq
|
|
119
|
+
raise MarketplaceError, 'widget definitions do not share one package namespace' unless namespaces.one?
|
|
120
|
+
|
|
121
|
+
"#{(namespaces.first + [name]).join('.')}.mpk"
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
def valid_name(value)
|
|
125
|
+
name = value.to_s
|
|
126
|
+
return name if name.match?(/\A[A-Za-z][A-Za-z0-9_]*\z/)
|
|
127
|
+
|
|
128
|
+
raise MarketplaceError, "invalid clientModule name #{name.inspect}"
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
def valid_version(value)
|
|
132
|
+
version = value.to_s
|
|
133
|
+
return version if version.match?(/\A[0-9A-Za-z][0-9A-Za-z.+_-]*\z/)
|
|
134
|
+
|
|
135
|
+
raise MarketplaceError, "invalid clientModule version #{version.inspect}"
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
def safe_relative_path(value, directory: false)
|
|
139
|
+
path = value.to_s.tr('\\', '/').delete_suffix('/')
|
|
140
|
+
parts = path.split('/')
|
|
141
|
+
unsafe = path.empty? || path.start_with?('/') || parts.include?('..') || parts.include?('') ||
|
|
142
|
+
path.include?("\0")
|
|
143
|
+
raise MarketplaceError, "unsafe widget package path #{value.inspect}" if unsafe
|
|
144
|
+
if !directory && File.extname(path).downcase != '.xml'
|
|
145
|
+
raise MarketplaceError, "widget definition is not XML: #{value.inspect}"
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
path
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
def elements(root, name)
|
|
152
|
+
found = []
|
|
153
|
+
root&.each_recursive { found << _1 if _1.is_a?(REXML::Element) && _1.name == name }
|
|
154
|
+
found
|
|
155
|
+
end
|
|
156
|
+
end
|
|
157
|
+
end
|
|
158
|
+
# rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength
|
|
159
|
+
|
|
160
|
+
# Enumerates widget identities delivered either directly or as declared
|
|
161
|
+
# assets of a modelerProject package such as Data Widgets.
|
|
162
|
+
# rubocop:disable Metrics/AbcSize, Metrics/MethodLength
|
|
163
|
+
class WidgetBundleInventory
|
|
164
|
+
attr_reader :kind, :widget_ids
|
|
165
|
+
|
|
166
|
+
def initialize(kind:, widget_ids:)
|
|
167
|
+
@kind = kind
|
|
168
|
+
@widget_ids = widget_ids.freeze
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
def self.read(path)
|
|
172
|
+
kind = PackageEnvelope.kind(path)
|
|
173
|
+
return new(kind:, widget_ids: WidgetPackageInventory.read(path).widget_ids) if kind == :widget
|
|
174
|
+
|
|
175
|
+
ModulePackageInventory.read(path)
|
|
176
|
+
ids = Dir.mktmpdir('mxrb-widget-bundle-') do |temporary|
|
|
177
|
+
Zip::File.open(path) do |archive|
|
|
178
|
+
reader = ModulePackageReader.new(archive)
|
|
179
|
+
descriptor = reader.descriptor
|
|
180
|
+
descriptor.files.grep(%r{\Awidgets/[^/]+\.mpk\z}i).flat_map do |relative|
|
|
181
|
+
entry = archive.find_entry(relative) || archive.find_entry(relative.tr('/', '\\'))
|
|
182
|
+
destination = File.join(temporary, File.basename(relative))
|
|
183
|
+
File.open(destination, 'wb') { ::IO.copy_stream(entry.get_input_stream, _1) }
|
|
184
|
+
WidgetPackageInventory.read(destination).widget_ids
|
|
185
|
+
end
|
|
186
|
+
end
|
|
187
|
+
end
|
|
188
|
+
new(kind:, widget_ids: ids.uniq.sort)
|
|
189
|
+
rescue Zip::Error, REXML::ParseException => e
|
|
190
|
+
raise MarketplaceError, "invalid Mendix widget bundle: #{e.message}"
|
|
191
|
+
end
|
|
192
|
+
end
|
|
193
|
+
# rubocop:enable Metrics/AbcSize, Metrics/MethodLength
|
|
194
|
+
|
|
195
|
+
# Installs an already authenticated official widget archive as one project-owned MPK.
|
|
196
|
+
# rubocop:disable Metrics
|
|
197
|
+
class WidgetPackageInstaller
|
|
198
|
+
def initialize(target:)
|
|
199
|
+
@target = File.expand_path(target)
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
def install(archive, package)
|
|
203
|
+
validate_target!
|
|
204
|
+
inventory = WidgetPackageInventory.read(archive)
|
|
205
|
+
validate_identity!(inventory, package)
|
|
206
|
+
digest = Digest::SHA256.file(archive).hexdigest
|
|
207
|
+
destination = safe_path(File.join('widgets', inventory.project_filename))
|
|
208
|
+
cache = safe_path(cache_relative(inventory, package))
|
|
209
|
+
lock_path = safe_path(File.join('.mxrb', 'marketplace.lock.json'))
|
|
210
|
+
current = validate_boundaries!(package, destination, cache, lock_path)
|
|
211
|
+
install_transaction(
|
|
212
|
+
archive, package, inventory, digest, destination, cache, lock_path, current
|
|
213
|
+
)
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
private
|
|
217
|
+
|
|
218
|
+
def validate_target!
|
|
219
|
+
raise MarketplaceError, "Marketplace target not found: #{@target}" unless File.directory?(@target)
|
|
220
|
+
raise MarketplaceError, "Marketplace target is a symbolic link: #{@target}" if File.symlink?(@target)
|
|
221
|
+
end
|
|
222
|
+
|
|
223
|
+
def validate_identity!(inventory, package)
|
|
224
|
+
return if inventory.version == package.version.to_s
|
|
225
|
+
|
|
226
|
+
raise MarketplaceError,
|
|
227
|
+
"widget package version #{inventory.version} does not match Marketplace #{package.version}"
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
def validate_boundaries!(package, destination, cache, lock_path)
|
|
231
|
+
ensure_no_symlink_components(destination)
|
|
232
|
+
ensure_no_symlink_components(cache)
|
|
233
|
+
ensure_no_symlink_components(lock_path)
|
|
234
|
+
raise MarketplaceError, "widget destination is a symbolic link: #{destination}" if File.symlink?(destination)
|
|
235
|
+
raise MarketplaceError, "widget destination is a directory: #{destination}" if File.directory?(destination)
|
|
236
|
+
|
|
237
|
+
lock = OfficialMarketplace.lock(@target)
|
|
238
|
+
current = lock.fetch('packages').find do |name, entry|
|
|
239
|
+
name.casecmp(package.name.to_s).zero? ||
|
|
240
|
+
entry['content_id'].to_s == package.content_id.to_s
|
|
241
|
+
end
|
|
242
|
+
validate_current!(current, package, destination)
|
|
243
|
+
validate_asset_owners!(lock, destination) unless current
|
|
244
|
+
unless !File.exist?(cache) || current&.last&.[]('archive') == relative(cache)
|
|
245
|
+
raise MarketplaceError, "widget cache destination already exists: #{cache}"
|
|
246
|
+
end
|
|
247
|
+
|
|
248
|
+
current
|
|
249
|
+
end
|
|
250
|
+
|
|
251
|
+
def validate_current!(current, package, destination)
|
|
252
|
+
return unless current
|
|
253
|
+
|
|
254
|
+
name, entry = current
|
|
255
|
+
raise MarketplaceError, "Marketplace package #{name.inspect} is not a widget" unless entry['kind'] == 'widget'
|
|
256
|
+
unless entry['destination'] == relative(destination)
|
|
257
|
+
raise MarketplaceError, "Marketplace widget #{name.inspect} changed its destination"
|
|
258
|
+
end
|
|
259
|
+
unless OfficialMarketplace.verify_widget(@target, entry)[:valid]
|
|
260
|
+
raise MarketplaceError, "installed Marketplace widget #{name.inspect} failed verification"
|
|
261
|
+
end
|
|
262
|
+
return unless entry['version'].to_s == package.version.to_s
|
|
263
|
+
|
|
264
|
+
raise MarketplaceError, "widget version #{package.version} is already installed"
|
|
265
|
+
end
|
|
266
|
+
|
|
267
|
+
def validate_asset_owners!(lock, destination)
|
|
268
|
+
relative_path = relative(destination)
|
|
269
|
+
owners = lock.fetch('packages').select do |_name, entry|
|
|
270
|
+
Array(entry['files']).include?(relative_path)
|
|
271
|
+
end
|
|
272
|
+
return if owners.empty? || !File.file?(destination)
|
|
273
|
+
|
|
274
|
+
expected = owners.filter_map { |_name, entry| locked_asset_digest(entry, relative_path) }.uniq
|
|
275
|
+
actual = Digest::SHA256.file(destination).hexdigest
|
|
276
|
+
return if expected.include?(actual)
|
|
277
|
+
|
|
278
|
+
raise MarketplaceError, "owned widget asset changed: #{relative_path}"
|
|
279
|
+
end
|
|
280
|
+
|
|
281
|
+
def locked_asset_digest(entry, relative_path)
|
|
282
|
+
return entry['sha256'] if entry['kind'] == 'widget' && entry['destination'] == relative_path
|
|
283
|
+
return unless entry['kind'] == 'module'
|
|
284
|
+
|
|
285
|
+
archive = safe_path(entry.fetch('archive'))
|
|
286
|
+
raise MarketplaceError, "cached Marketplace package is missing: #{archive}" unless File.file?(archive)
|
|
287
|
+
|
|
288
|
+
ModulePackageInventory.read(archive).files[relative_path]
|
|
289
|
+
end
|
|
290
|
+
|
|
291
|
+
def install_transaction(archive, package, inventory, digest, destination, cache, lock_path, current)
|
|
292
|
+
original = current&.last&.[]('asset_original')
|
|
293
|
+
backup = original ? safe_path(original) : backup_path(package, destination)
|
|
294
|
+
old_cache = safe_path(current.last.fetch('archive')) if current
|
|
295
|
+
paths = [destination, cache, lock_path, backup, old_cache].compact.uniq
|
|
296
|
+
with_rollback(paths) do
|
|
297
|
+
preserve_original(destination, backup) unless original
|
|
298
|
+
atomic_copy(archive, destination)
|
|
299
|
+
atomic_copy(archive, cache)
|
|
300
|
+
verify_copy!(destination, digest)
|
|
301
|
+
verify_copy!(cache, digest)
|
|
302
|
+
write_lock(
|
|
303
|
+
lock_path, package, inventory, digest, destination, cache,
|
|
304
|
+
asset_original: File.file?(backup) ? relative(backup) : nil,
|
|
305
|
+
replaced_name: current&.first
|
|
306
|
+
)
|
|
307
|
+
FileUtils.rm_f(old_cache) if old_cache && old_cache != cache
|
|
308
|
+
end
|
|
309
|
+
WidgetInstallation.new(package, destination, digest, inventory.name, inventory.widget_ids, cache)
|
|
310
|
+
end
|
|
311
|
+
|
|
312
|
+
def preserve_original(destination, backup)
|
|
313
|
+
return unless File.file?(destination)
|
|
314
|
+
|
|
315
|
+
FileUtils.mkdir_p(File.dirname(backup))
|
|
316
|
+
FileUtils.cp(destination, backup)
|
|
317
|
+
end
|
|
318
|
+
|
|
319
|
+
def backup_path(package, destination)
|
|
320
|
+
owner = "Widget-#{package.content_id || package.name.to_s.gsub(/[^A-Za-z0-9_.-]/, '_')}"
|
|
321
|
+
safe_path(File.join('.mxrb', 'marketplace-originals', owner, relative(destination)))
|
|
322
|
+
end
|
|
323
|
+
|
|
324
|
+
def atomic_copy(source, destination)
|
|
325
|
+
temporary = "#{destination}.tmp-#{Process.pid}"
|
|
326
|
+
FileUtils.mkdir_p(File.dirname(destination))
|
|
327
|
+
FileUtils.cp(source, temporary)
|
|
328
|
+
File.rename(temporary, destination)
|
|
329
|
+
ensure
|
|
330
|
+
FileUtils.rm_f(temporary)
|
|
331
|
+
end
|
|
332
|
+
|
|
333
|
+
def verify_copy!(path, digest)
|
|
334
|
+
return if Digest::SHA256.file(path).hexdigest == digest
|
|
335
|
+
|
|
336
|
+
raise MarketplaceError, "widget package checksum mismatch: #{path}"
|
|
337
|
+
end
|
|
338
|
+
|
|
339
|
+
def write_lock(path, package, inventory, digest, destination, cache,
|
|
340
|
+
asset_original:, replaced_name: nil)
|
|
341
|
+
lock = OfficialMarketplace.lock(@target)
|
|
342
|
+
lock.fetch('packages').delete(replaced_name) if replaced_name && replaced_name != package.name
|
|
343
|
+
lock.fetch('packages')[package.name] = {
|
|
344
|
+
'kind' => 'widget', 'version' => package.version, 'source' => package.source,
|
|
345
|
+
'repository' => package.repository, 'sha256' => digest,
|
|
346
|
+
'destination' => relative(destination), 'archive' => relative(cache),
|
|
347
|
+
'files' => [relative(destination)], 'asset_original' => asset_original,
|
|
348
|
+
'widget_name' => inventory.name, 'widget_ids' => inventory.widget_ids,
|
|
349
|
+
'content_id' => package.content_id, 'version_id' => package.version_id,
|
|
350
|
+
'content_type' => package.content_type, 'version_type' => package.version_type,
|
|
351
|
+
'security_issues' => package.security_issues, 'private' => package.private,
|
|
352
|
+
'company_approved' => package.company_approved
|
|
353
|
+
}.compact
|
|
354
|
+
temporary = "#{path}.tmp-#{Process.pid}"
|
|
355
|
+
FileUtils.mkdir_p(File.dirname(path))
|
|
356
|
+
File.write(temporary, JSON.pretty_generate(lock) << "\n")
|
|
357
|
+
File.rename(temporary, path)
|
|
358
|
+
ensure
|
|
359
|
+
FileUtils.rm_f(temporary)
|
|
360
|
+
end
|
|
361
|
+
|
|
362
|
+
def with_rollback(paths)
|
|
363
|
+
Dir.mktmpdir('mxrb-widget-rollback-') do |temporary|
|
|
364
|
+
snapshots = paths.each_with_index.map { |path, index| snapshot(path, temporary, index) }
|
|
365
|
+
yield
|
|
366
|
+
rescue StandardError
|
|
367
|
+
snapshots.reverse_each { restore_snapshot(_1) }
|
|
368
|
+
raise
|
|
369
|
+
end
|
|
370
|
+
end
|
|
371
|
+
|
|
372
|
+
def snapshot(path, temporary, index)
|
|
373
|
+
backup = File.join(temporary, index.to_s)
|
|
374
|
+
type = File.file?(path) ? :file : :missing
|
|
375
|
+
FileUtils.cp(path, backup) if type == :file
|
|
376
|
+
[path, backup, type]
|
|
377
|
+
end
|
|
378
|
+
|
|
379
|
+
def restore_snapshot(snapshot)
|
|
380
|
+
path, backup, type = snapshot
|
|
381
|
+
FileUtils.rm_f(path)
|
|
382
|
+
return if type == :missing
|
|
383
|
+
|
|
384
|
+
FileUtils.mkdir_p(File.dirname(path))
|
|
385
|
+
FileUtils.cp(backup, path)
|
|
386
|
+
end
|
|
387
|
+
|
|
388
|
+
def cache_relative(inventory, package)
|
|
389
|
+
version = package.version.to_s.gsub(/[^A-Za-z0-9_.-]/, '_')
|
|
390
|
+
stem = File.basename(inventory.project_filename, '.mpk')
|
|
391
|
+
File.join('.mxrb', 'marketplace', "#{stem}-#{version}.mpk")
|
|
392
|
+
end
|
|
393
|
+
|
|
394
|
+
def safe_path(relative)
|
|
395
|
+
path = File.expand_path(File.join(@target, relative))
|
|
396
|
+
prefix = "#{@target}#{File::SEPARATOR}"
|
|
397
|
+
raise MarketplaceError, "path is outside marketplace target: #{path}" unless path.start_with?(prefix)
|
|
398
|
+
|
|
399
|
+
path
|
|
400
|
+
end
|
|
401
|
+
|
|
402
|
+
def relative(path) = path.delete_prefix("#{@target}#{File::SEPARATOR}")
|
|
403
|
+
|
|
404
|
+
def ensure_no_symlink_components(path)
|
|
405
|
+
current = @target
|
|
406
|
+
relative(path).split(File::SEPARATOR)[0...-1].each do |part|
|
|
407
|
+
current = File.join(current, part)
|
|
408
|
+
raise MarketplaceError, "widget destination traverses a symbolic link: #{current}" if File.symlink?(current)
|
|
409
|
+
end
|
|
410
|
+
end
|
|
411
|
+
end
|
|
412
|
+
# rubocop:enable Metrics
|
|
413
|
+
end
|
|
414
|
+
end
|