From 783e2b453be549a870de011e6eac655c7cfb51ce Mon Sep 17 00:00:00 2001 From: Simon Lecoq <22963968+lowlighter@users.noreply.github.com> Date: Thu, 11 Feb 2021 21:39:40 +0100 Subject: [PATCH] Add licenses plugin (#118) --- Dockerfile | 5 +- .../api/github/graphql/licenses.default.mjs | 278 ++++++++++++++++++ .../github/graphql/licenses.repository.mjs | 13 + source/plugins/licenses/README.md | 37 +++ source/plugins/licenses/index.mjs | 147 +++++++++ source/plugins/licenses/metadata.yml | 32 ++ .../plugins/licenses/queries/licenses.graphql | 20 ++ .../licenses/queries/repository.graphql | 14 + source/plugins/licenses/tests.yml | 13 + source/templates/classic/style.css | 51 ++++ source/templates/repository/partials/_.json | 3 +- .../repository/partials/licenses.ejs | 120 ++++++++ 12 files changed, 730 insertions(+), 3 deletions(-) create mode 100644 source/app/mocks/api/github/graphql/licenses.default.mjs create mode 100644 source/app/mocks/api/github/graphql/licenses.repository.mjs create mode 100644 source/plugins/licenses/README.md create mode 100644 source/plugins/licenses/index.mjs create mode 100644 source/plugins/licenses/metadata.yml create mode 100644 source/plugins/licenses/queries/licenses.graphql create mode 100644 source/plugins/licenses/queries/repository.graphql create mode 100644 source/plugins/licenses/tests.yml create mode 100644 source/templates/repository/partials/licenses.ejs diff --git a/Dockerfile b/Dockerfile index 2fe63407..0f99975d 100644 --- a/Dockerfile +++ b/Dockerfile @@ -16,12 +16,13 @@ RUN chmod +x /metrics/source/app/action/index.mjs \ && apt-get install -y google-chrome-stable fonts-ipafont-gothic fonts-wqy-zenhei fonts-thai-tlwg fonts-kacst fonts-freefont-ttf libxss1 \ --no-install-recommends \ && rm -rf /var/lib/apt/lists/* \ - # Install ruby to support linguist - # Based on https://github.com/github/linguist + # Install ruby to support github gems + # Based on https://github.com/github/linguist and https://github.com/github/licensed && apt-get update \ && apt-get install -y ruby-full \ && apt-get install -y git g++ cmake pkg-config libicu-dev zlib1g-dev libcurl4-openssl-dev libssl-dev ruby-dev \ && gem install github-linguist \ + && gem install licensed \ # Install python for node-gyp && apt-get update \ && apt-get install -y python3 \ diff --git a/source/app/mocks/api/github/graphql/licenses.default.mjs b/source/app/mocks/api/github/graphql/licenses.default.mjs new file mode 100644 index 00000000..5da8a885 --- /dev/null +++ b/source/app/mocks/api/github/graphql/licenses.default.mjs @@ -0,0 +1,278 @@ +/**Mocked data */ + export default function({faker, query, login = faker.internet.userName()}) { + console.debug("metrics/compute/mocks > mocking graphql api result > licenses/default") + return ({ + licenses:[ + { + spdxId:"AGPL-3.0", + name:"GNU Affero General Public License v3.0", + nickname:"GNU AGPLv3", + key:"agpl-3.0", + limitations:[ + {key:"liability", label:"Liability"}, + {key:"warranty", label:"Warranty"}, + ], + conditions:[ + {key:"include-copyright", label:"License and copyright notice"}, + {key:"document-changes", label:"State changes"}, + {key:"disclose-source", label:"Disclose source"}, + {key:"network-use-disclose", label:"Network use is distribution"}, + {key:"same-license", label:"Same license"}, + ], + permissions:[ + {key:"commercial-use", label:"Commercial use"}, + {key:"modifications", label:"Modification"}, + {key:"distribution", label:"Distribution"}, + {key:"patent-use", label:"Patent use"}, + {key:"private-use", label:"Private use"}, + ], + }, + { + spdxId:"Apache-2.0", + name:"Apache License 2.0", + nickname:null, + key:"apache-2.0", + limitations:[ + {key:"trademark-use", label:"Trademark use"}, + {key:"liability", label:"Liability"}, + {key:"warranty", label:"Warranty"}, + ], + conditions:[ + {key:"include-copyright", label:"License and copyright notice"}, + {key:"document-changes", label:"State changes"}, + ], + permissions:[ + {key:"commercial-use", label:"Commercial use"}, + {key:"modifications", label:"Modification"}, + {key:"distribution", label:"Distribution"}, + {key:"patent-use", label:"Patent use"}, + {key:"private-use", label:"Private use"}, + ], + }, + { + spdxId:"BSD-2-Clause", + name:'BSD 2-Clause "Simplified" License', + nickname:null, + key:"bsd-2-clause", + limitations:[ + {key:"liability", label:"Liability"}, + {key:"warranty", label:"Warranty"}, + ], + conditions:[ + {key:"include-copyright", label:"License and copyright notice"}, + ], + permissions:[ + {key:"commercial-use", label:"Commercial use"}, + {key:"modifications", label:"Modification"}, + {key:"distribution", label:"Distribution"}, + {key:"private-use", label:"Private use"}, + ], + }, + { + spdxId:"BSD-3-Clause", + name:'BSD 3-Clause "New" or "Revised" License', + nickname:null, + key:"bsd-3-clause", + limitations:[ + {key:"liability", label:"Liability"}, + {key:"warranty", label:"Warranty"}, + ], + conditions:[ + {key:"include-copyright", label:"License and copyright notice"}, + ], + permissions:[ + {key:"commercial-use", label:"Commercial use"}, + {key:"modifications", label:"Modification"}, + {key:"distribution", label:"Distribution"}, + {key:"private-use", label:"Private use"}, + ], + }, + { + spdxId:"BSL-1.0", + name:"Boost Software License 1.0", + nickname:null, + key:"bsl-1.0", + limitations:[ + {key:"liability", label:"Liability"}, + {key:"warranty", label:"Warranty"}, + ], + conditions:[ + {key:"include-copyright--source", label:"License and copyright notice for source"}, + ], + permissions:[ + {key:"commercial-use", label:"Commercial use"}, + {key:"modifications", label:"Modification"}, + {key:"distribution", label:"Distribution"}, + {key:"private-use", label:"Private use"}, + ], + }, + { + spdxId:"CC0-1.0", + name:"Creative Commons Zero v1.0 Universal", + nickname:null, + key:"cc0-1.0", + limitations:[ + {key:"liability", label:"Liability"}, + {key:"trademark-use", label:"Trademark use"}, + {key:"patent-use", label:"Patent use"}, + {key:"warranty", label:"Warranty"}, + ], + conditions:[], + permissions:[ + {key:"commercial-use", label:"Commercial use"}, + {key:"modifications", label:"Modification"}, + {key:"distribution", label:"Distribution"}, + {key:"private-use", label:"Private use"}, + ], + }, + { + spdxId:"EPL-2.0", + name:"Eclipse Public License 2.0", + nickname:null, + key:"epl-2.0", + limitations:[ + {key:"liability", label:"Liability"}, + {key:"warranty", label:"Warranty"}, + ], + conditions:[ + {key:"disclose-source", label:"Disclose source"}, + {key:"include-copyright", label:"License and copyright notice"}, + {key:"same-license", label:"Same license"}, + ], + permissions:[ + {key:"commercial-use", label:"Commercial use"}, + {key:"distribution", label:"Distribution"}, + {key:"modifications", label:"Modification"}, + {key:"patent-use", label:"Patent use"}, + {key:"private-use", label:"Private use"}, + ], + }, + { + spdxId:"GPL-2.0", + name:"GNU General Public License v2.0", + nickname:"GNU GPLv2", + key:"gpl-2.0", + limitations:[ + {key:"liability", label:"Liability"}, + {key:"warranty", label:"Warranty"}, + ], + conditions:[ + {key:"include-copyright", label:"License and copyright notice"}, + {key:"document-changes", label:"State changes"}, + {key:"disclose-source", label:"Disclose source"}, + {key:"same-license", label:"Same license"}, + ], + permissions:[ + {key:"commercial-use", label:"Commercial use"}, + {key:"modifications", label:"Modification"}, + {key:"distribution", label:"Distribution"}, + {key:"private-use", label:"Private use"}, + ], + }, + { + spdxId:"GPL-3.0", + name:"GNU General Public License v3.0", + nickname:"GNU GPLv3", + key:"gpl-3.0", + limitations:[ + {key:"liability", label:"Liability"}, + {key:"warranty", label:"Warranty"}, + ], + conditions:[ + {key:"include-copyright", label:"License and copyright notice"}, + {key:"document-changes", label:"State changes"}, + {key:"disclose-source", label:"Disclose source"}, + {key:"same-license", label:"Same license"}, + ], + permissions:[ + {key:"commercial-use", label:"Commercial use"}, + {key:"modifications", label:"Modification"}, + {key:"distribution", label:"Distribution"}, + {key:"patent-use", label:"Patent use"}, + {key:"private-use", label:"Private use"}, + ], + }, + { + spdxId:"LGPL-2.1", + name:"GNU Lesser General Public License v2.1", + nickname:"GNU LGPLv2.1", + key:"lgpl-2.1", + limitations:[ + {key:"liability", label:"Liability"}, + {key:"warranty", label:"Warranty"}, + ], + conditions:[ + {key:"include-copyright", label:"License and copyright notice"}, + {key:"disclose-source", label:"Disclose source"}, + {key:"document-changes", label:"State changes"}, + {key:"same-license--library", label:"Same license (library)"}, + ], + permissions:[ + {key:"commercial-use", label:"Commercial use"}, + {key:"modifications", label:"Modification"}, + {key:"distribution", label:"Distribution"}, + {key:"private-use", label:"Private use"}, + ], + }, + { + spdxId:"MIT", + name:"MIT License", + nickname:null, + key:"mit", + limitations:[ + {key:"liability", label:"Liability"}, + {key:"warranty", label:"Warranty"}, + ], + conditions:[ + {key:"include-copyright", label:"License and copyright notice"}, + ], + permissions:[ + {key:"commercial-use", label:"Commercial use"}, + {key:"modifications", label:"Modification"}, + {key:"distribution", label:"Distribution"}, + {key:"private-use", label:"Private use"}, + ], + }, + { + spdxId:"MPL-2.0", + name:"Mozilla Public License 2.0", + nickname:null, + key:"mpl-2.0", + limitations:[ + {key:"liability", label:"Liability"}, + {key:"trademark-use", label:"Trademark use"}, + {key:"warranty", label:"Warranty"}, + ], + conditions:[ + {key:"disclose-source", label:"Disclose source"}, + {key:"include-copyright", label:"License and copyright notice"}, + {key:"same-license--file", label:"Same license (file)"}, + ], + permissions:[ + {key:"commercial-use", label:"Commercial use"}, + {key:"modifications", label:"Modification"}, + {key:"distribution", label:"Distribution"}, + {key:"patent-use", label:"Patent use"}, + {key:"private-use", label:"Private use"}, + ], + }, + { + spdxId:"Unlicense", + name:"The Unlicense", + nickname:null, + key:"unlicense", + limitations:[ + {key:"liability", label:"Liability"}, + {key:"warranty", label:"Warranty"}, + ], + conditions:[], + permissions:[ + {key:"private-use", label:"Private use"}, + {key:"commercial-use", label:"Commercial use"}, + {key:"modifications", label:"Modification"}, + {key:"distribution", label:"Distribution"}, + ], + }, + ], + }) + } diff --git a/source/app/mocks/api/github/graphql/licenses.repository.mjs b/source/app/mocks/api/github/graphql/licenses.repository.mjs new file mode 100644 index 00000000..2eb533c2 --- /dev/null +++ b/source/app/mocks/api/github/graphql/licenses.repository.mjs @@ -0,0 +1,13 @@ +/**Mocked data */ + export default function({faker, query, login = faker.internet.userName()}) { + console.debug("metrics/compute/mocks > mocking graphql api result > licenses/repository") + return ({ + user:{ + repository:{ + licenseInfo:{spdxId:"MIT", name:"MIT License", nickname:null, key:"mit"}, + url:"https://github.com/lowlighter/metrics", + databaseId:293860197, + }, + }, + }) + } diff --git a/source/plugins/licenses/README.md b/source/plugins/licenses/README.md new file mode 100644 index 00000000..ac7b6c6d --- /dev/null +++ b/source/plugins/licenses/README.md @@ -0,0 +1,37 @@ +### 📜 Licenses + + âš ī¸ This is NOT legal advice, use at your own risk + đŸ’Ŗ Do NOT enable this plugin on public web instances (plugin allows raw commands injection) + +The *licenses* plugin lets you display license informations like permissions, limitations and conditions along with additional metrics about dependencies. + + + +
+ +
With licenses ratio + +
+ +
+ +Project must be setup with dependencies using `plugin_licenses_setup` option (for example, `npm ci` for a NodeJS project). + +Dependencies will be analyzed with [github/licensed](https://github.com/github/licensed) and compared against GitHub known licenses. + +#### â„šī¸ Examples workflows + +[âžĄī¸ Available options for this plugin](metadata.yml) + +```yaml +- uses: lowlighter/metrics@latest + with: + # ... other options + template: repository + user: repository-owner + query: '{"repo":"repository-name"}' + plugin_licenses: yes + plugin_licenses_setup: npm ci # Command to setup target repository + plugin_licenses_ratio: yes # Display used licenses ratio + plugin_licenses_legal: yes # Display permissions, limitations and conditions +``` \ No newline at end of file diff --git a/source/plugins/licenses/index.mjs b/source/plugins/licenses/index.mjs new file mode 100644 index 00000000..0f71db27 --- /dev/null +++ b/source/plugins/licenses/index.mjs @@ -0,0 +1,147 @@ +//Setup + export default async function({login, q, imports, data, graphql, queries, account}, {enabled = false} = {}) { + //Plugin execution + try { + //Check if plugin is enabled and requirements are met + if ((!enabled)||(!q.licenses)) + return null + + //Load inputs + let {setup, ratio, legal} = imports.metadata.plugins.licenses.inputs({data, account, q}) + + //Initialization + const {user:{repository}} = await graphql(queries.licenses.repository({owner:data.repo.owner.login, name:data.repo.name, account})) + const result = {ratio, legal, default:repository.licenseInfo, licensed:{available:false}, text:{}, list:[], used:{}, dependencies:[], known:0, unknown:0} + const {used, text} = result + + //Register existing licenses properties + const licenses = Object.fromEntries((await graphql(queries.licenses())).licenses.map(license => [license.key, license])) + for (const license of Object.values(licenses)) + [...license.limitations, ...license.conditions, ...license.permissions].flat().map(({key, label}) => text[key] = label) + colors(licenses) + + //Check if licensed exists + if (await imports.which("licensed")) { + //Setup for licensed + console.debug(`metrics/compute/${login}/plugins > licenses > searching dependencies licenses using licensed`) + const path = imports.paths.join(imports.os.tmpdir(), `${repository.databaseId}`) + //Create temporary directory + console.debug(`metrics/compute/${login}/plugins > licenses > creating temp dir ${path}`) + await imports.fs.rmdir(path, {recursive:true}) + await imports.fs.mkdir(path, {recursive:true}) + //Clone repository + console.debug(`metrics/compute/${login}/plugins > licenses > cloning temp git repository ${repository.url} to ${path}`) + const git = imports.git(path) + await git.clone(repository.url, path) + //Run setup + if (setup) { + console.debug(`metrics/compute/${login}/plugins > licenses > running setup [${setup}]`) + await imports.run(setup, {cwd:path}, {prefixed:false}) + } + //Create configuration file if needed + if (!(await imports.fs.stat(imports.paths.join(path, ".licensed.yml")).then(() => 1).catch(() => 0))) { + console.debug(`metrics/compute/${login}/plugins > licenses > building .licensed.yml configuration file`) + await imports.fs.writeFile(imports.paths.join(path, ".licensed.yml"), [ + "cache_path: .licensed", + ].join("\n")) + } + else + console.debug(`metrics/compute/${login}/plugins > licenses > a .licensed.yml configuration file already exists`) + //Spawn licensed process + console.debug(`metrics/compute/${login}/plugins > licenses > running licensed`) + JSON.parse(await imports.run("licensed list --format=json --licenses", {cwd:path})).apps + .map(({sources}) => sources?.flatMap(source => source.dependencies.map(({dependency, license}) => { + used[license] = (used[license] ?? 0) + 1 + result.dependencies.push(dependency) + result.known += (license in licenses) + result.unknown += !(license in licenses) + }))) + //Cleaning + console.debug(`metrics/compute/${login}/plugins > licensed > cleaning temp dir ${path}`) + await imports.fs.rmdir(path, {recursive:true}) + } + else + console.debug(`metrics/compute/${login}/plugins > licenses > licensed not available`) + + //List licenses properties + console.debug(`metrics/compute/${login}/plugins > licenses > compute licenses properties`) + const base = {permissions:new Set(), limitations:new Set(), conditions:new Set()} + const combined = {permissions:new Set(), limitations:new Set(), conditions:new Set()} + const detected = Object.entries(used).map(([key, _value]) => ({key})) + for (const properties of Object.keys(base)) { + //Base license + if (repository.licenseInfo) + licenses[repository.licenseInfo.key]?.[properties]?.map(({key}) => base[properties].add(key)) + //Combined licenses + for (const {key} of detected) + licenses[key]?.[properties]?.map(({key}) => combined[properties].add(key)) + } + + //Merge limitations and conditions + for (const properties of ["limitations", "conditions"]) + result[properties] = [[...base[properties]].map(key => ({key, text:text[key], inherited:false})), [...combined[properties]].filter(key => !base[properties].has(key)).map(key => ({key, text:text[key], inherited:true}))].flat() + //Remove base permissions conflicting with inherited limitations + result.permissions = [...base.permissions].filter(key => !combined.limitations.has(key)).map(key => ({key, text:text[key]})) + + //Count used licenses + console.debug(`metrics/compute/${login}/plugins > licenses > computing ratio`) + const total = Object.values(used).reduce((a, b) => a + b, 0) + //Format used licenses and compute positions + const list = Object.entries(used).map(([key, count]) => ({name:licenses[key]?.spdxId ?? `${key.charAt(0).toLocaleUpperCase()}${key.substring(1)}`, key, count, value:count/total, x:0, color:licenses[key]?.color ?? "#6e7681", order:licenses[key]?.order ?? -1})).sort((a, b) => a.order === b.order ? b.count - a.count : b.order - a.order) + for (let i = 0; i < list.length; i++) + list[i].x = (list[i-1]?.x ?? 0) + (list[i-1]?.value ?? 0) + //Save ratios + result.list = list + + //Results + return result + } + //Handle errors + catch (error) { + throw {error:{message:"An error occured", instance:error}} + } + } + +/**Licenses colorizer (based on categorie) */ + function colors(licenses) { + for (const [license, value] of Object.entries(licenses)) { + const [permissions, conditions] = [value.permissions, value.conditions].map(properties => properties.map(({key}) => key)) + switch (true) { + //Other licenses + case (license === "other"):{ + value.color = "#8b949e" + value.order = 0 + break + } + //Strongly protective licenses and network protective + case ((conditions.includes("disclose-source"))&&(conditions.includes("same-license"))&&(conditions.includes("network-use-disclose"))):{ + value.color = "#388bfd" + value.order = 1 + break + } + //Strongly protective licenses + case ((conditions.includes("disclose-source"))&&(conditions.includes("same-license"))):{ + value.color = "#79c0ff" + value.order = 2 + break + } + //Weakly protective licenses + case ((conditions.includes("disclose-source"))&&(conditions.includes("same-license--library"))):{ + value.color = "#7ee787" + value.order = 3 + break + } + //Permissive license + case ((permissions.includes("private-use"))&&(permissions.includes("commercial-use"))&&(permissions.includes("modifications"))&&(permissions.includes("distribution"))):{ + value.color = "#56d364" + value.order = 4 + break + } + //Unknown + default:{ + value.color = "#6e7681" + value.order = -1 + } + } + } + } \ No newline at end of file diff --git a/source/plugins/licenses/metadata.yml b/source/plugins/licenses/metadata.yml new file mode 100644 index 00000000..77d43e4a --- /dev/null +++ b/source/plugins/licenses/metadata.yml @@ -0,0 +1,32 @@ +name: "📜 Licenses" +cost: N/A +supports: + - repository +inputs: + + # Enable or disable plugin + plugin_licenses: + description: Display licenses informations + type: boolean + default: no + + # Command to use to setup target repository + # It is required to install all dependencies that will be analyzed with github/licensed + plugin_licenses_setup: + description: Command to setup target repository + type: string + default: "" + example: npm ci + + # Display used licenses from both repository license and dependencies licenses ratio + plugin_licenses_ratio: + description: Display used licenses ratio + type: boolean + default: no + + # Display permissions, limitations and conditions from both repository license and dependencies licenses + # Note that this is NOT legal advice, use at your own risk + plugin_licenses_legal: + description: Display legal informations about used licenses + type: boolean + default: yes \ No newline at end of file diff --git a/source/plugins/licenses/queries/licenses.graphql b/source/plugins/licenses/queries/licenses.graphql new file mode 100644 index 00000000..4a124508 --- /dev/null +++ b/source/plugins/licenses/queries/licenses.graphql @@ -0,0 +1,20 @@ +query LicensesDefault { + licenses { + spdxId + name + nickname + key + limitations { + key + label + } + conditions { + key + label + } + permissions { + key + label + } + } +} diff --git a/source/plugins/licenses/queries/repository.graphql b/source/plugins/licenses/queries/repository.graphql new file mode 100644 index 00000000..7c5530a6 --- /dev/null +++ b/source/plugins/licenses/queries/repository.graphql @@ -0,0 +1,14 @@ +query LicensesRepository { + $account(login: "$owner") { + repository(name: "$name") { + licenseInfo { + spdxId + name + nickname + key + } + url + databaseId + } + } +} \ No newline at end of file diff --git a/source/plugins/licenses/tests.yml b/source/plugins/licenses/tests.yml new file mode 100644 index 00000000..180461ba --- /dev/null +++ b/source/plugins/licenses/tests.yml @@ -0,0 +1,13 @@ +- name: Licenses plugin (complete) + uses: lowlighter/metrics@latest + with: + token: MOCKED_TOKEN + template: repository + query: '{"repo":"metrics"}' + plugin_licenses: yes + plugin_licenses_setup: npm ci + plugin_licenses_ratio: yes + plugin_licenses_legal: yes + timeout: 1200000 + modes: + - action \ No newline at end of file diff --git a/source/templates/classic/style.css b/source/templates/classic/style.css index 62c9f9d9..b44d6924 100644 --- a/source/templates/classic/style.css +++ b/source/templates/classic/style.css @@ -635,6 +635,57 @@ border-radius: 7px; } +/* Licenses */ + .licenses { + display: flex; + } + .licenses .column { + align-items: flex-start; + font-size: 12px; + color: #666666; + flex-shrink: 0; + } + .licenses-details { + margin-top: 8px; + } + .field.license.details { + display: flex; + justify-content: space-between; + } + .field.license.details small { + display: flex; + justify-content: space-between; + color: #666666; + text-align: right; + } + .licenses .column:nth-child(1) { + margin-left: 13px; + width: 25%; + } + .licenses .column:nth-child(2) { + width: 25%; + } + .licenses .column:nth-child(3) { + width: 50%; + } + .licenses .column svg { + height: 12px; + width: 12px; + } + .licenses .column .title { + font-weight: 600; + margin-left: 15px; + } + .licenses .column .permission svg { + fill: #56d364; + } + .licenses .column .limitation svg { + fill: #f85149; + } + .licenses .column .condition svg { + fill: #58a6ff; + } + /* Fade animation */ .af { opacity: 0; diff --git a/source/templates/repository/partials/_.json b/source/templates/repository/partials/_.json index d5dc9d61..f35e07f6 100644 --- a/source/templates/repository/partials/_.json +++ b/source/templates/repository/partials/_.json @@ -6,5 +6,6 @@ "pagespeed", "stargazers", "people", - "activity" + "activity", + "licenses" ] \ No newline at end of file diff --git a/source/templates/repository/partials/licenses.ejs b/source/templates/repository/partials/licenses.ejs new file mode 100644 index 00000000..ada1da96 --- /dev/null +++ b/source/templates/repository/partials/licenses.ejs @@ -0,0 +1,120 @@ +<% if (plugins.licenses) { %> +
+

+ + Licenses +

+ <% if (plugins.licenses.error) { %> +
+
+ + <%= plugins.licenses.error.message %> +
+
+ <% } else { %> +
+
+
+ + <%= plugins.licenses.default?.spdxId ?? "No license provided" %> +
+
+ + <%= plugins.licenses.dependencies.length %> dependenc<%= s(plugins.licenses.dependencies.length, "y") %> +
+
+
+
+ + <%= plugins.licenses.known %> known license<%= s(plugins.licenses.known) %> used +
+
+ + <%= plugins.licenses.unknown %> unknown license<%= s(plugins.licenses.unknown) %> used +
+
+
+ <% if (plugins.licenses.ratio) { %> +
+
+ + + + + + <% for (const {name, value, color, x} of plugins.licenses.list) { %> + + <% } %> + +
+
+ <% for (const row of [0, 1]) { %> +
+ <% for (const {name, value, color, count} of plugins.licenses.list.filter((_, i) => i%2 === row)) { %> +
+
+ + <%= f.ellipsis(name) %> +
+
<%= count %>
+
+ <% } %> +
+ <% } %> +
+
+ <% } %> + <% if (plugins.licenses.legal) { %> +
+
+ <% if (plugins.licenses.permissions?.length) { %> +
+
Permissions
+ <% for (const {text, disabled} of plugins.licenses.permissions) { %> +
"> + <% if (disabled) { %> + + <% } else { %> + + <% } %> + <%= text %> +
+ <% } %> +
+ <% } %> + <% if (plugins.licenses.limitations?.length) { %> +
+
Limitations
+ <% for (const {text, inherited} of plugins.licenses.limitations) { %> +
+ <% if (inherited) { %> + + <% } else { %> + + <% } %> + <%= text %> +
+ <% } %> +
+ <% } %> + <% if (plugins.licenses.conditions?.length) { %> +
+
Conditions
+ <% for (const {text, inherited} of plugins.licenses.conditions) { %> +
+ <% if (inherited) { %> + + <% } else { %> + + <% } %> + <%= text %> +
+ <% } %> +
+ <% } %> +
+
+ <% } %> + <% } %> +
+<% } %>