├── .rspec
├── Gemfile
├── .gitignore
├── .rubocop.yml
├── _includes
├── search-autocomplete-default-result-template.html
├── search-full-default-result-template.html
├── package-list-item.html
└── browse-paginator-navigation.html
├── _layouts
├── category-packages.html
├── sidebar-latest-packages.html
└── sidebar-lastupdated-packages.html
├── lib
├── fdroid
│ ├── Permission.rb
│ ├── Repo.rb
│ ├── Version.rb
│ ├── IndexV1.rb
│ └── Package.rb
├── jekyll-fdroid.rb
└── jekyll
│ ├── FDroidBrowsingPage.rb
│ ├── FDroidLatestPackagesTag.rb
│ ├── FDroidLastUpdatedPackagesTag.rb
│ ├── FDroidRepoInfoTag.rb
│ ├── FDroidPackageDetailPage.rb
│ ├── ReadYamlPage.rb
│ ├── FDroidCategoryDetailPage.rb
│ └── FDroidPackageDetailGenerator.rb
├── spec
├── lib
│ └── fdroid
│ │ ├── FDroidRepoInfoTag_spec.rb
│ │ └── FDroidIndex_spec.rb
├── spec_helper.rb
└── assets
│ └── localized.json
├── _pages
└── categories-overview.html
├── jekyll-fdroid.gemspec
├── README.md
├── .gitlab-ci.yml
├── _sass
└── jekyll-fdroid.scss
└── LICENSE
/.rspec:
--------------------------------------------------------------------------------
1 | --color
2 | --require spec_helper
3 |
--------------------------------------------------------------------------------
/Gemfile:
--------------------------------------------------------------------------------
1 | source 'https://rubygems.org'
2 |
3 | gemspec
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .idea
2 | *.iml
3 | *.gem
4 | .vscode
5 | vendor
6 | .bundle
7 |
--------------------------------------------------------------------------------
/.rubocop.yml:
--------------------------------------------------------------------------------
1 | AllCops:
2 | DisabledByDefault: true
3 | TargetRubyVersion: 2.5
4 |
5 | Layout:
6 | Enabled: true
7 |
8 | Layout/LineLength:
9 | Max: 600
--------------------------------------------------------------------------------
/_includes/search-autocomplete-default-result-template.html:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/_includes/search-full-default-result-template.html:
--------------------------------------------------------------------------------
1 |
14 |
--------------------------------------------------------------------------------
/_layouts/category-packages.html:
--------------------------------------------------------------------------------
1 | ---
2 | layout: page
3 | title: " "
4 | permalink: filled_by_script
5 | pagination:
6 | enabled: true
7 | collection: filled_by_script
8 | per_page: 30
9 | trail:
10 | before: 4
11 | after: 4
12 | permalink: '/:num/'
13 | title: ':title'
14 | sort_field: 'last_updated'
15 | sort_reverse: true
16 | ---
17 |
18 | {{ site.data.strings.app_categories[page.app_category] }}
19 |
20 | {% for package in paginator.posts %}
21 | {% include package-list-item.html package=package %}
22 | {% endfor %}
23 |
24 | {% include_cached browse-paginator-navigation.html paginator=paginator permalink=page.dir %}
25 |
26 |
--------------------------------------------------------------------------------
/_includes/package-list-item.html:
--------------------------------------------------------------------------------
1 |
18 |
--------------------------------------------------------------------------------
/_layouts/sidebar-latest-packages.html:
--------------------------------------------------------------------------------
1 | {% assign packages = site.packages | sort: "last_updated" | reverse | sort: "added" | reverse %}
2 | {% assign remaining = 3 %}
3 | {% for package in packages %}
4 | {% if package.is_localized and package.icon and package.summary and package.whats_new and package.anti_features == null %}
5 | {% include package-list-item.html package=package %}
6 | {% assign remaining = remaining | minus: 1 %}
7 | {% endif %}
8 | {% if remaining <= 0 %}{% break %}{% endif %}
9 | {% endfor %}
10 |
11 | {% for package in packages %}
12 | {% if remaining <= 0 %}{% break %}{% endif %}
13 | {% if package.is_localized and package.icon and package.summary and package.anti_features == null %}
14 | {% include package-list-item.html package=package %}
15 | {% assign remaining = remaining | minus: 1 %}
16 | {% endif %}
17 | {% endfor %}
18 |
19 |
20 | {% for package in packages limit:remaining %}
21 | {% include package-list-item.html package=package %}
22 | {% endfor %}
23 |
--------------------------------------------------------------------------------
/_layouts/sidebar-lastupdated-packages.html:
--------------------------------------------------------------------------------
1 | {% assign packages = site.packages | sort: "added" | reverse | sort: "last_updated" | reverse %}
2 | {% assign remaining = 3 %}
3 | {% for package in packages %}
4 | {% if package.is_localized and package.icon and package.summary and package.whats_new and package.anti_features == null %}
5 | {% include package-list-item.html package=package %}
6 | {% assign remaining = remaining | minus: 1 %}
7 | {% endif %}
8 | {% if remaining <= 0 %}{% break %}{% endif %}
9 | {% endfor %}
10 |
11 | {% for package in packages %}
12 | {% if remaining <= 0 %}{% break %}{% endif %}
13 | {% if package.is_localized and package.icon and package.summary and package.anti_features == null %}
14 | {% include package-list-item.html package=package %}
15 | {% assign remaining = remaining | minus: 1 %}
16 | {% endif %}
17 | {% endfor %}
18 |
19 |
20 | {% for package in packages limit:remaining %}
21 | {% include package-list-item.html package=package %}
22 | {% endfor %}
23 |
--------------------------------------------------------------------------------
/lib/fdroid/Permission.rb:
--------------------------------------------------------------------------------
1 | # F-Droid's Jekyll Plugin
2 | #
3 | # Copyright (C) 2017 Nico Alt
4 | #
5 | # This program is free software: you can redistribute it and/or modify
6 | # it under the terms of the GNU Affero General Public License as
7 | # published by the Free Software Foundation, either version 3 of the
8 | # License, or (at your option) any later version.
9 | #
10 | # This program is distributed in the hope that it will be useful,
11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 | # GNU Affero General Public License for more details.
14 | #
15 | # You should have received a copy of the GNU Affero General Public License
16 | # along with this program. If not, see .
17 |
18 | module FDroid
19 | class Permission
20 | def initialize(permission)
21 | @permission = permission[0]
22 | @min_sdk = permission[1]
23 | end
24 |
25 | def to_data
26 | {
27 | 'permission' => @permission,
28 | 'min_sdk' => @min_sdk,
29 | }
30 | end
31 | end
32 | end
33 |
--------------------------------------------------------------------------------
/lib/jekyll-fdroid.rb:
--------------------------------------------------------------------------------
1 | # F-Droid's Jekyll Plugin
2 | #
3 | # Copyright (C) 2017 Nico Alt
4 | #
5 | # This program is free software: you can redistribute it and/or modify
6 | # it under the terms of the GNU Affero General Public License as
7 | # published by the Free Software Foundation, either version 3 of the
8 | # License, or (at your option) any later version.
9 | #
10 | # This program is distributed in the hope that it will be useful,
11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 | # GNU Affero General Public License for more details.
14 | #
15 | # You should have received a copy of the GNU Affero General Public License
16 | # along with this program. If not, see .
17 |
18 | require "fdroid/IndexV1"
19 | require "jekyll/ReadYamlPage"
20 | require "jekyll/FDroidBrowsingPage"
21 | require "jekyll/FDroidLastUpdatedPackagesTag"
22 | require "jekyll/FDroidLatestPackagesTag"
23 | require "jekyll/FDroidPackageDetailGenerator"
24 | require "jekyll/FDroidPackageDetailPage"
25 | require "jekyll/FDroidCategoryDetailPage"
26 | require "jekyll/FDroidRepoInfoTag"
27 |
--------------------------------------------------------------------------------
/spec/lib/fdroid/FDroidRepoInfoTag_spec.rb:
--------------------------------------------------------------------------------
1 | require 'rspec'
2 | require 'liquid'
3 | require 'jekyll'
4 | require_relative '../../../lib/jekyll/FDroidRepoInfoTag'
5 |
6 | module Jekyll
7 | RSpec.describe FDroidRepoInfoTag do
8 | def make_context
9 | Liquid::Context.new(
10 | {},
11 | {},
12 | {
13 | :site => Jekyll::Site.new(
14 | {
15 | 'source' => '/tmp',
16 | 'destination' => '/tmp/build',
17 | 'permalink' => '',
18 | 'liquid' => {
19 | 'error_mode' => ''
20 | },
21 | 'limit_posts' => 0,
22 | 'plugins' => [],
23 | 'kramdown' => {},
24 | 'fdroid-repo' => 'https://guardianproject.info/fdroid/repo'
25 | }
26 | )
27 | }
28 | )
29 | end
30 |
31 | it 'renders fdroid_repo_info correctly', { :network => true, :tag => true } do
32 | template = Liquid::Template.parse('Repo: {% fdroid_repo_info %}').render make_context
33 | expect(template).to match(/Repo: Guardian Project Official Releases \d\d\d\d-\d\d-\d\d/)
34 | end
35 | end
36 | end
37 |
--------------------------------------------------------------------------------
/lib/jekyll/FDroidBrowsingPage.rb:
--------------------------------------------------------------------------------
1 | # F-Droid's Jekyll Plugin
2 | #
3 | # Copyright (C) 2017 Nico Alt
4 | #
5 | # This program is free software: you can redistribute it and/or modify
6 | # it under the terms of the GNU Affero General Public License as
7 | # published by the Free Software Foundation, either version 3 of the
8 | # License, or (at your option) any later version.
9 | #
10 | # This program is distributed in the hope that it will be useful,
11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 | # GNU Affero General Public License for more details.
14 | #
15 | # You should have received a copy of the GNU Affero General Public License
16 | # along with this program. If not, see .
17 |
18 | module Jekyll
19 | class FDroidBrowsingPage < ReadYamlPage
20 | def initialize(site, base)
21 | @site = site
22 | @base = base
23 | @dir = "packages"
24 | @name = "index.html"
25 |
26 | self.process(@name)
27 | self.read_yaml((File.expand_path "../../_pages", File.dirname(__FILE__)), 'categories-overview.html')
28 | end
29 | end
30 | end
31 |
--------------------------------------------------------------------------------
/lib/jekyll/FDroidLatestPackagesTag.rb:
--------------------------------------------------------------------------------
1 | # F-Droid's Jekyll Plugin
2 | #
3 | # Copyright (C) 2017 Nico Alt
4 | #
5 | # This program is free software: you can redistribute it and/or modify
6 | # it under the terms of the GNU Affero General Public License as
7 | # published by the Free Software Foundation, either version 3 of the
8 | # License, or (at your option) any later version.
9 | #
10 | # This program is distributed in the hope that it will be useful,
11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 | # GNU Affero General Public License for more details.
14 | #
15 | # You should have received a copy of the GNU Affero General Public License
16 | # along with this program. If not, see .
17 |
18 | module Jekyll
19 | class FDroidLatestPackagesTag < Liquid::Tag
20 | def initialize(tag_name, text, tokens)
21 | super
22 | end
23 |
24 | def render(context)
25 | template = Liquid::Template.parse(IO.read((File.expand_path "../../_layouts/sidebar-latest-packages.html", File.dirname(__FILE__))))
26 | template.render(context)
27 | end
28 | end
29 | end
30 |
31 | Liquid::Template.register_tag('fdroid_show_latest_packages', Jekyll::FDroidLatestPackagesTag)
32 |
--------------------------------------------------------------------------------
/lib/jekyll/FDroidLastUpdatedPackagesTag.rb:
--------------------------------------------------------------------------------
1 | # F-Droid's Jekyll Plugin
2 | #
3 | # Copyright (C) 2017 Nico Alt
4 | #
5 | # This program is free software: you can redistribute it and/or modify
6 | # it under the terms of the GNU Affero General Public License as
7 | # published by the Free Software Foundation, either version 3 of the
8 | # License, or (at your option) any later version.
9 | #
10 | # This program is distributed in the hope that it will be useful,
11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 | # GNU Affero General Public License for more details.
14 | #
15 | # You should have received a copy of the GNU Affero General Public License
16 | # along with this program. If not, see .
17 |
18 | module Jekyll
19 | class FDroidLastUpdatedPackagesTag < Liquid::Tag
20 | def initialize(tag_name, text, tokens)
21 | super
22 | end
23 |
24 | def render(context)
25 | template = Liquid::Template.parse(IO.read((File.expand_path "../../_layouts/sidebar-lastupdated-packages.html", File.dirname(__FILE__))))
26 | template.render(context)
27 | end
28 | end
29 | end
30 |
31 | Liquid::Template.register_tag('fdroid_show_last_updated_packages', Jekyll::FDroidLastUpdatedPackagesTag)
32 |
--------------------------------------------------------------------------------
/lib/jekyll/FDroidRepoInfoTag.rb:
--------------------------------------------------------------------------------
1 | # F-Droid's Jekyll Plugin
2 | #
3 | # Copyright (C) 2017 Peter Serwylo
4 | #
5 | # This program is free software: you can redistribute it and/or modify
6 | # it under the terms of the GNU Affero General Public License as
7 | # published by the Free Software Foundation, either version 3 of the
8 | # License, or (at your option) any later version.
9 | #
10 | # This program is distributed in the hope that it will be useful,
11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 | # GNU Affero General Public License for more details.
14 | #
15 | # You should have received a copy of the GNU Affero General Public License
16 | # along with this program. If not, see .
17 |
18 | require_relative '../fdroid/IndexV1'
19 |
20 | module Jekyll
21 | # Used to output the repo name/timestamp used to generate this F-Droid site.
22 | class FDroidRepoInfoTag < Liquid::Tag
23 | @@repotag = ''
24 |
25 | def initialize(tag_name, text, tokens)
26 | super
27 | end
28 |
29 | def render(context)
30 | if @@repotag == ''
31 | site = context.registers[:site]
32 | url = site.config['fdroid-repo']
33 | index = FDroid::IndexV1.download(url, 'en')
34 | @@repotag = "#{index.repo.name} #{index.repo.date}"
35 | end
36 | return @@repotag
37 | end
38 | end
39 | end
40 |
41 | Liquid::Template.register_tag('fdroid_repo_info', Jekyll::FDroidRepoInfoTag)
42 |
--------------------------------------------------------------------------------
/_pages/categories-overview.html:
--------------------------------------------------------------------------------
1 | ---
2 | layout: page
3 | title: " "
4 | permalink: /packages/
5 | ---
6 | {% for app_category in site.app_categories %}
7 | {% assign app_category_escaped = app_category | escape %}
8 | {% assign filtered_packages = site.packages | where_exp: "item", "item.categories contains app_category_escaped" | sort: "last_updated" | reverse %}
9 | {% if filtered_packages.size > 0 %}
10 | {{ site.data.strings.app_categories[app_category] }}
11 |
12 | {% assign remaining = 3 %}
13 | {% for package in filtered_packages %}
14 | {% if package.is_localized and package.icon and package.summary and package.whats_new %}
15 | {% include package-list-item.html package=package %}
16 | {% assign remaining = remaining | minus: 1 %}
17 | {% endif %}
18 | {% if remaining <= 0 %}{% break %}{% endif %}
19 | {% endfor %}
20 | {% for package in filtered_packages %}
21 | {% if package.is_localized and package.icon and package.summary %}
22 | {% include package-list-item.html package=package %}
23 | {% assign remaining = remaining | minus: 1 %}
24 | {% endif %}
25 | {% if remaining <= 0 %}{% break %}{% endif %}
26 | {% endfor %}
27 | {% for package in filtered_packages limit: remaining %}
28 | {% include package-list-item.html package=package %}
29 | {% endfor %}
30 |
31 | {{ site.data.strings.app_categories.show_all_packages | replace: '%d', filtered_packages.size }}
32 | {% endif %}
33 | {% endfor %}
34 |
--------------------------------------------------------------------------------
/lib/jekyll/FDroidPackageDetailPage.rb:
--------------------------------------------------------------------------------
1 | # F-Droid's Jekyll Plugin
2 | #
3 | # Copyright (C) 2017 Nico Alt
4 | #
5 | # This program is free software: you can redistribute it and/or modify
6 | # it under the terms of the GNU Affero General Public License as
7 | # published by the Free Software Foundation, either version 3 of the
8 | # License, or (at your option) any later version.
9 | #
10 | # This program is distributed in the hope that it will be useful,
11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 | # GNU Affero General Public License for more details.
14 | #
15 | # You should have received a copy of the GNU Affero General Public License
16 | # along with this program. If not, see .
17 |
18 | module Jekyll
19 | class FDroidPackageDetailPage < ReadYamlPage
20 | # @param [Jekyll::Site] site
21 | # @param [string] base
22 | # @param [FDroid::Package] package
23 | def initialize(site, base, package)
24 | @site = site
25 | @base = base
26 | @dir = 'packages'
27 | @name = "#{package.package_name}/index.html"
28 |
29 | self.process(@name)
30 | self.read_yaml(get_layout_dir, 'package.html')
31 | self.data.update(package.to_data)
32 | end
33 |
34 | def get_layout_dir()
35 | layout_dir_override = File.join(site.source, '_layouts')
36 | if File.exists? File.join(layout_dir_override, 'package.html')
37 | layout_dir_override
38 | else
39 | File.expand_path '../../_layouts', File.dirname(__FILE__)
40 | end
41 | end
42 | end
43 | end
44 |
--------------------------------------------------------------------------------
/jekyll-fdroid.gemspec:
--------------------------------------------------------------------------------
1 | # F-Droid's Jekyll Plugin
2 | #
3 | # Copyright (C) 2017 Nico Alt
4 | #
5 | # This program is free software: you can redistribute it and/or modify
6 | # it under the terms of the GNU Affero General Public License as
7 | # published by the Free Software Foundation, either version 3 of the
8 | # License, or (at your option) any later version.
9 | #
10 | # This program is distributed in the hope that it will be useful,
11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 | # GNU Affero General Public License for more details.
14 | #
15 | # You should have received a copy of the GNU Affero General Public License
16 | # along with this program. If not, see .
17 |
18 | Gem::Specification.new do |s|
19 | s.name = 'jekyll-fdroid'
20 | s.version = '1.2.1'
21 | s.add_runtime_dependency 'jekyll', '< 5.0'
22 | s.add_runtime_dependency "jekyll-include-cache"
23 | s.add_runtime_dependency "jekyll-paginate-v2"
24 | s.add_runtime_dependency 'rubyzip'
25 | s.add_runtime_dependency 'json', '>= 1.8.5'
26 | s.add_runtime_dependency 'loofah'
27 | s.add_development_dependency 'rspec'
28 | s.add_development_dependency 'rubocop'
29 | s.date = '2020-03-13'
30 | s.summary = "F-Droid - Free and Open Source Android App Repository"
31 | s.description = "Browse packages of a F-Droid repository."
32 | s.authors = ["F-Droid"]
33 | s.email = 'team@f-droid.org'
34 | s.files = Dir['lib/**/*.rb']
35 | s.homepage =
36 | 'https://gitlab.com/fdroid/jekyll-fdroid'
37 | s.license = 'AGPL-3.0'
38 | end
39 |
--------------------------------------------------------------------------------
/lib/jekyll/ReadYamlPage.rb:
--------------------------------------------------------------------------------
1 | # F-Droid's Jekyll Plugin
2 | #
3 | # Copyright (C) 2017 Nico Alt
4 | #
5 | # This program is free software: you can redistribute it and/or modify
6 | # it under the terms of the GNU Affero General Public License as
7 | # published by the Free Software Foundation, either version 3 of the
8 | # License, or (at your option) any later version.
9 | #
10 | # This program is distributed in the hope that it will be useful,
11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 | # GNU Affero General Public License for more details.
14 | #
15 | # You should have received a copy of the GNU Affero General Public License
16 | # along with this program. If not, see .
17 | #
18 | # Found at https://github.com/ggreer/jekyll-gallery-generator/blob/master/lib/jekyll-gallery-generator.rb#L69
19 |
20 | module Jekyll
21 | class ReadYamlPage < Page
22 | # We need do define it ourself because the templates are in the plugin's directory
23 | def read_yaml(base, name, opts = {})
24 | begin
25 | self.content = File.read(File.join(base.to_s, name.to_s), **(site ? site.file_read_opts : {}).merge(opts))
26 | if content =~ /\A(---\s*\n.*?\n?)^((---|\.\.\.)\s*$\n?)/m
27 | self.content = $POSTMATCH
28 | self.data = SafeYAML.load($1)
29 | end
30 | rescue SyntaxError => e
31 | Jekyll.logger.warn "YAML Exception reading #{File.join(base.to_s, name.to_s)}: #{e.message}"
32 | rescue Exception => e
33 | Jekyll.logger.warn "Error reading file #{File.join(base.to_s, name.to_s)}: #{e.message}"
34 | end
35 |
36 | self.data ||= {}
37 | end
38 | end
39 | end
40 |
--------------------------------------------------------------------------------
/lib/fdroid/Repo.rb:
--------------------------------------------------------------------------------
1 | # F-Droid's Jekyll Plugin
2 | #
3 | # Copyright (C) 2017 Peter Serwylo
4 | #
5 | # This program is free software: you can redistribute it and/or modify
6 | # it under the terms of the GNU Affero General Public License as
7 | # published by the Free Software Foundation, either version 3 of the
8 | # License, or (at your option) any later version.
9 | #
10 | # This program is distributed in the hope that it will be useful,
11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 | # GNU Affero General Public License for more details.
14 | #
15 | # You should have received a copy of the GNU Affero General Public License
16 | # along with this program. If not, see .
17 |
18 | require 'uri'
19 |
20 | module FDroid
21 | class Repo
22 | def initialize(repo)
23 | @repo = repo
24 | end
25 |
26 | def name
27 | escape_html @repo['name']
28 | end
29 |
30 | def address
31 | url = @repo['address']
32 | url =~ /\A#{URI::regexp}\z/ ? escape_html(url) : nil
33 | end
34 |
35 | def icon_url
36 | url = "#{self.address}/icons/#{@repo['icon']}"
37 | url =~ /\A#{URI::regexp}\z/ ? escape_html(url) : nil
38 | end
39 |
40 | def description
41 | escape_html @repo['description']
42 | end
43 |
44 | def timestamp
45 | Integer(@repo['timestamp']) rescue nil
46 | end
47 |
48 | def date
49 | Date.strptime("#{@repo['timestamp'] / 1000}", '%s')
50 | end
51 |
52 | private
53 |
54 | def escape_html(value)
55 | value.gsub(/[<>"'&]/, ESCAPES)
56 | end
57 |
58 | ESCAPES = {
59 | '<' => '<', '>' => '>', '"' => '"', "'" => ''', '&' => '&'
60 | }
61 | end
62 | end
63 |
--------------------------------------------------------------------------------
/lib/jekyll/FDroidCategoryDetailPage.rb:
--------------------------------------------------------------------------------
1 | # F-Droid's Jekyll Plugin
2 | #
3 | # Copyright (C) 2017 Nico Alt
4 | #
5 | # This program is free software: you can redistribute it and/or modify
6 | # it under the terms of the GNU Affero General Public License as
7 | # published by the Free Software Foundation, either version 3 of the
8 | # License, or (at your option) any later version.
9 | #
10 | # This program is distributed in the hope that it will be useful,
11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 | # GNU Affero General Public License for more details.
14 | #
15 | # You should have received a copy of the GNU Affero General Public License
16 | # along with this program. If not, see .
17 |
18 | module Jekyll
19 | class FDroidCategoryDetailPage < ReadYamlPage
20 | # @param [Jekyll::Site] site
21 | # @param [string] base
22 | # @param [string] app_category
23 | # @param [string] app_category_id
24 | def initialize(site, base, app_category, app_category_id)
25 | @site = site
26 | @base = base
27 | @dir = 'categories'
28 |
29 | # Avoid special characters in URL, otherwise language support doesn't work
30 | @name = "#{app_category_id}/index.html"
31 |
32 | self.process(@name)
33 | self.read_yaml(get_layout_dir, 'category-packages.html')
34 | self.data['app_category'] = app_category
35 | self.data['permalink'] = "/categories/#{app_category_id}/"
36 | self.data['pagination']['collection'] = app_category_id
37 | end
38 |
39 | def get_layout_dir()
40 | layout_dir_override = File.join(site.source, '_layouts')
41 | if File.exists? File.join(layout_dir_override, 'category-packages.html')
42 | layout_dir_override
43 | else
44 | File.expand_path '../../_layouts', File.dirname(__FILE__)
45 | end
46 | end
47 | end
48 | end
49 |
--------------------------------------------------------------------------------
/_includes/browse-paginator-navigation.html:
--------------------------------------------------------------------------------
1 | {% if include.paginator.total_pages > 1 %}
2 |
3 |
4 |
5 | {% if include.paginator.previous_page %}
6 |
7 | <
8 |
9 | {% else %}
10 | <
11 | {% endif %}
12 |
13 | {% if include.paginator.page_trail[0].num > 1 %}
14 |
15 | 1
16 |
17 | …
18 | {% endif %}
19 |
20 | {% for trail in include.paginator.page_trail %}
21 | {% if include.paginator.page == trail.num %}
22 | {{ trail.num }}
23 | {% else %}
24 |
25 | {{ trail.num }}
26 |
27 | {% endif %}
28 | {% endfor %}
29 |
30 | {% if include.paginator.page_trail[-1].num < include.paginator.total_pages %}
31 | …
32 |
33 | {% comment %} Workaround for https://github.com/sverrirs/jekyll-paginate-v2/issues/28 {% endcomment %}
34 | {% if include.paginator.page == 1 %}
35 | {% assign back = '' %}
36 | {% else %}
37 | {% assign back = '../' %}
38 | {% endif %}
39 | {{include.paginator.total_pages}}
40 |
41 | {% endif %}
42 |
43 | {% if include.paginator.next_page %}
44 |
45 | >
46 |
47 | {% else %}
48 | >
49 | {% endif %}
50 |
51 |
52 |
53 | {% endif %}
54 |
--------------------------------------------------------------------------------
/lib/fdroid/Version.rb:
--------------------------------------------------------------------------------
1 | # F-Droid's Jekyll Plugin
2 | #
3 | # Copyright (C) 2017 Nico Alt
4 | #
5 | # This program is free software: you can redistribute it and/or modify
6 | # it under the terms of the GNU Affero General Public License as
7 | # published by the Free Software Foundation, either version 3 of the
8 | # License, or (at your option) any later version.
9 | #
10 | # This program is distributed in the hope that it will be useful,
11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 | # GNU Affero General Public License for more details.
14 | #
15 | # You should have received a copy of the GNU Affero General Public License
16 | # along with this program. If not, see .
17 |
18 | require_relative './Permission'
19 |
20 | module FDroid
21 | class Version
22 | def initialize(version)
23 | @version = version
24 | end
25 |
26 | def <=>(other)
27 | self.version_code <=> other.version_code
28 | end
29 |
30 | def version_code
31 | @version['versionCode']
32 | end
33 |
34 | def version_name
35 | @version['versionName']
36 | end
37 |
38 | def to_data
39 | added = nil
40 | if @version['added'] != nil then
41 | added = Date.strptime("#{@version['added'] / 1000}", '%s')
42 | end
43 |
44 | {
45 | 'added' => added,
46 | 'anti_features' => @version['antiFeatures'],
47 | 'apk_name' => @version['apkName'],
48 | 'file_extension' => File.extname(@version['apkName'].to_s).strip.upcase[1..-1],
49 | 'hash' => @version['hash'],
50 | 'hash_type' => @version['hashType'],
51 | 'max_sdk_version' => @version['maxSdkVersion'],
52 | 'min_sdk_version' => @version['minSdkVersion'],
53 | 'nativecode' => @version['nativecode'],
54 | 'srcname' => @version['srcname'],
55 | 'sig' => @version['sig'],
56 | 'signer' => @version['signer'],
57 | 'size' => @version['size'],
58 | 'target_sdk_version' => @version['targetSdkVersion'],
59 | 'uses_permission' => permission,
60 | 'version_name' => version_name,
61 | 'version_code' => version_code,
62 | }
63 | end
64 |
65 | def permission
66 | if @version['uses-permission'] == nil then
67 | []
68 | else
69 | @version['uses-permission'].map { |perm| Permission.new(perm).to_data }
70 | end
71 | end
72 | end
73 | end
74 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # F-Droid's Jekyll Plugin
2 |
3 | [](https://rubygems.org/gems/jekyll-fdroid)
4 |
5 | With this gem you can browse packages of a F-Droid repository in a Jekyll site.
6 | Add the following configurations to your `_config.yml`:
7 | ```
8 | gems:
9 | - jekyll-fdroid
10 | - jekyll-include-cache
11 | - jekyll-paginate-v2
12 | fdroid-repo: https://guardianproject.info/fdroid/repo
13 | ```
14 |
15 | `jekyll-include-cache` and `jekyll-paginate-v2` are needed to be added to the configuration manually
16 | because we [weren't able to add the configuration programmatically](https://gitlab.com/fdroid/jekyll-fdroid/issues/29).
17 |
18 | For default styling of the browsing and packages' pages
19 | you need to import the plugin's stylesheet in your SASS file like this:
20 | ```
21 | @import "jekyll-fdroid";
22 | ```
23 |
24 | To show a list of latest or last updated packages,
25 | use the following tags in your page:
26 | ```
27 | {% fdroid_show_latest_packages %}
28 | {% fdroid_show_last_updated_packages %}
29 | ```
30 |
31 | ## Running Tests
32 |
33 | To run the test suite, you must first have installed the releveant dependencies:
34 |
35 | ```
36 | bundle install --path vendor
37 | ```
38 |
39 | The tests are then run via RSpec:
40 |
41 | ```
42 | bundle exec rspec
43 | ```
44 |
45 | If you want to exclude tests which hit the network to download F-Droid metadata, run:
46 |
47 | ```
48 | bundle exec rspec --tag "~network"
49 | ```
50 |
51 | ## Can I use this plugin with the old index?
52 |
53 | Starting at version 0.2.0 this plugin only supports the new JSON index
54 | of F-Droid.
55 | If you want to use this plugin with the old XML index,
56 | you can use the [release 0.1.1](https://rubygems.org/gems/jekyll-fdroid/versions/0.1.1)
57 | which is the last one supporting the old index.
58 |
59 | ## Publishing a new version
60 |
61 | Jekyll-FDroid is distributed via [RubyGems.org](https://rubygems.org/gems/jekyll-fdroid).
62 | To quickly sum up [their extensive guides](https://guides.rubygems.org/):
63 |
64 | ```bash
65 | # Build gem package
66 | gem build jekyll-fdroid.gemspec
67 | # Push to RubyGems
68 | gem push jekyll-fdroid-1.0.0.gem
69 | ```
70 |
71 | ## License
72 |
73 | This program is Free Software:
74 | You can use, study share and improve it at your will.
75 | Specifically you can redistribute and/or modify it under the terms of the
76 | [GNU Affero General Public License](https://www.gnu.org/licenses/agpl.html)
77 | as published by the Free Software Foundation,
78 | either version 3 of the License,
79 | or (at your option) any later version.
80 |
--------------------------------------------------------------------------------
/.gitlab-ci.yml:
--------------------------------------------------------------------------------
1 |
2 | variables:
3 | LC_ALL: C.UTF-8
4 | DEBIAN_FRONTEND: noninteractive
5 |
6 | # this job aims to setup a common Ruby dev environment
7 | ruby_latest:
8 | image: ruby
9 | script:
10 | - ruby -v
11 | - for f in `find * -name \*.rb`; do printf "$f\t"; ruby -c $f; done
12 | - apt-get update
13 | - apt-get install -y bundler
14 | - bundle config set path 'vendor'
15 | - bundle install
16 | - bundle exec rubocop lib spec
17 | - bundle exec rspec
18 |
19 | # this job aims to reproduce the production setup
20 | fdroid-website:
21 | image: debian:buster
22 | variables:
23 | DEBIAN_FRONTEND: noninteractive
24 | LANG: C.UTF-8
25 | artifacts:
26 | paths:
27 | - public
28 | script:
29 | - apt-get -q update
30 | - apt-get -qy install bundler git ruby --no-install-recommends
31 | - git -C .. clone --depth=1 https://gitlab.com/fdroid/fdroid-website.git
32 | - cd ../fdroid-website
33 | # use the 'gitlab ci' subset of languages
34 | - sed -i
35 | -e 's,^languages:,ignored_languages:,'
36 | -e 's,^gitlab_ci_languages:,languages:,'
37 | _config.yml
38 | # force the use of the local jekyll-fdroid
39 | - perl -i -pe "BEGIN{undef $/;} s@\n *gem 'jekyll-fdroid'.*?\n\s*:ref *=> *'[^']+'\n@\n@smg" Gemfile
40 | - sed -i "s@^end@ gem 'jekyll-fdroid', path\x3a '$CI_PROJECT_DIR'\nend@" Gemfile
41 | # run fdroid-website setup
42 | - ruby -e "require 'yaml'; d=YAML.load_file('.gitlab-ci.yml');
43 | puts d['.apt-template'].gsub(/\\\\\\n/, ' ');
44 | puts d['.setup_for_jekyll'].gsub(/\\\\\\n/, ' ')"
45 | | bash -e
46 |
47 | - cd $CI_PROJECT_DIR
48 | - rubocop lib spec
49 | - rspec
50 |
51 | - cd ../fdroid-website
52 | # This is where GitLab pages will deploy to by default (e.g. "https://fdroid.gitlab.io/jekyll-fdroid")
53 | # so we need to make sure that the Jekyll configuration understands this.
54 | - sed -Ei
55 | -e "s,^(url\x3a).*,\1 'https://$CI_PROJECT_NAMESPACE.gitlab.io',"
56 | -e "s,^(baseurl\x3a).*,\1 '/$CI_PROJECT_NAME',"
57 | _config.yml
58 | - echo "Jekyll config used for CI:" && cat _config.yml
59 | - jekyll build -d $CI_PROJECT_DIR/public --trace
60 | - ./tools/prepare-multi-lang.sh $CI_PROJECT_DIR/public --no-type-maps
61 |
62 | # TODO kludge to get css until someone figures out why sass is not compiling the css
63 | - apt-get install curl
64 | - mkdir -p $CI_PROJECT_DIR/public/css
65 | - curl https://fdroid.gitlab.io/fdroid-website/css/main.css > $CI_PROJECT_DIR/public/css/main.css
66 |
67 |
68 | pages:
69 | stage: deploy
70 | artifacts:
71 | paths:
72 | - public
73 | expire_in: 1w
74 | when: always
75 | script:
76 | - ls -lR public
77 |
78 |
--------------------------------------------------------------------------------
/lib/fdroid/IndexV1.rb:
--------------------------------------------------------------------------------
1 | # F-Droid's Jekyll Plugin
2 | #
3 | # Copyright (C) 2017 Nico Alt
4 | #
5 | # This program is free software: you can redistribute it and/or modify
6 | # it under the terms of the GNU Affero General Public License as
7 | # published by the Free Software Foundation, either version 3 of the
8 | # License, or (at your option) any later version.
9 | #
10 | # This program is distributed in the hope that it will be useful,
11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 | # GNU Affero General Public License for more details.
14 | #
15 | # You should have received a copy of the GNU Affero General Public License
16 | # along with this program. If not, see .
17 |
18 | require 'tmpdir'
19 | require 'open-uri'
20 | require 'net/http'
21 | require 'json'
22 | require 'zip'
23 | require_relative './Package'
24 | require_relative './Repo'
25 |
26 | module FDroid
27 | class IndexV1
28 | attr_reader :packages, :repo
29 |
30 | @@downloaded_repos = {}
31 |
32 | # Download and parse an index, returning a new instance of IndexV1.
33 | # @param [string] repo
34 | # @param [string] locale
35 | # @return [FDroid::IndexV1]
36 | def self.download(repo, locale)
37 | repo = URI.parse "#{repo}/index-v1.jar"
38 | index = download_index repo
39 | IndexV1.new(JSON.parse(index), locale)
40 | end
41 |
42 | # Make a network request, download the index-v1.jar file from the repo, unzip and get the contents
43 | # of the index-v1.json file.
44 | # @param [string] repo
45 | # @return [Hash]
46 | def self.download_index(repo)
47 | if @@downloaded_repos.has_key? repo
48 | return @@downloaded_repos[repo]
49 | end
50 |
51 | Dir.mktmpdir do |dir|
52 | jar = File.join dir, 'index-v1.jar'
53 | open(jar, 'wb') do |file|
54 | begin
55 | file.write(Net::HTTP.get(repo))
56 | rescue Net::OpenTimeout, Net::ReadTimeout => e
57 | puts "Timeout (#{e}), retrying in 1 second..."
58 | sleep(1)
59 | retry
60 | end
61 | end
62 |
63 | Zip::File.open(jar) do |zip_file|
64 | entry = zip_file.glob('index-v1.json').first
65 | @@downloaded_repos[repo] = entry.get_input_stream.read
66 | next @@downloaded_repos[repo]
67 | end
68 | end
69 | end
70 |
71 | # What IndexV2 calls "packages", IndexV1 calls "apps"
72 | # What IndexV2 calls "versions", IndexV1 calls "packages"
73 | def initialize(index, locale)
74 | @packages = index['apps'].map do |app_json|
75 | packages_json = index['packages'][app_json['packageName']]
76 | Package.new(app_json, packages_json, locale)
77 | end
78 |
79 | @repo = Repo.new(index['repo'])
80 | end
81 | end
82 | end
83 |
--------------------------------------------------------------------------------
/lib/jekyll/FDroidPackageDetailGenerator.rb:
--------------------------------------------------------------------------------
1 | # F-Droid's Jekyll Plugin
2 | #
3 | # Copyright (C) 2017 Nico Alt
4 | #
5 | # This program is free software: you can redistribute it and/or modify
6 | # it under the terms of the GNU Affero General Public License as
7 | # published by the Free Software Foundation, either version 3 of the
8 | # License, or (at your option) any later version.
9 | #
10 | # This program is distributed in the hope that it will be useful,
11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 | # GNU Affero General Public License for more details.
14 | #
15 | # You should have received a copy of the GNU Affero General Public License
16 | # along with this program. If not, see .
17 |
18 | module Jekyll
19 | class FDroidPackagesGenerator < Generator
20 | attr_accessor :alreadyBuilt
21 |
22 | safe true
23 | priority :highest
24 |
25 | def generate(site)
26 | # generator will only run on first build, not because of auto-regeneration
27 | if @alreadyBuilt != true
28 | @alreadyBuilt = true
29 |
30 | # Add plugin's SASS directory so site's list of SASS directories
31 | if site.config["sass"].nil? || site.config["sass"].empty?
32 | site.config["sass"] = Hash.new
33 | end
34 | if site.config["sass"]["load_paths"].nil? || site.config["sass"]["load_paths"].empty?
35 | site.config["sass"]["load_paths"] = ["_sass", (File.expand_path "../../_sass", File.dirname(__FILE__))]
36 | else
37 | site.config["sass"]["load_paths"] << (File.expand_path "../../_sass", File.dirname(__FILE__))
38 | end
39 |
40 | # Enable pagination
41 | if site.config["pagination"].nil? || site.config["pagination"].empty?
42 | site.config["pagination"] = Hash.new
43 | end
44 | site.config["pagination"]["enabled"] = true
45 |
46 | index = FDroid::IndexV1.download(site.config["fdroid-repo"], site.active_lang || 'en_US')
47 |
48 | # Generate collection and detail page for every category
49 | site.config["app_categories"].each do |app_category|
50 | app_category_id = Utils.slugify(app_category)
51 | site.collections[app_category_id] = Collection.new(site, app_category_id)
52 | site.pages << FDroidCategoryDetailPage.new(site, site.source, app_category, app_category_id)
53 | end
54 |
55 | # Generate detail page for every package
56 | site.collections["packages"] = Collection.new(site, "packages")
57 | index.packages.each do |package|
58 | # This page needs to be created twice, once for site.pages, and once for site.collections.
59 | # If not, then the i18n code in jekyll-polyglot will end up processing the page twice, as
60 | # it iterates over all pages and all packages. The end result is a double prefix for "/en/en"
61 | # for any links in the page.
62 | # https://gitlab.com/fdroid/jekyll-fdroid/issues/38
63 | site.pages << FDroidPackageDetailPage.new(site, site.source, package)
64 | site.collections["packages"].docs << FDroidPackageDetailPage.new(site, site.source, package)
65 |
66 | package.categories.each do |app_category|
67 | app_category_id = Utils.slugify(app_category)
68 | if site.collections[app_category_id].nil?
69 | puts("Warning: Package '#{package.package_name}' has unknown category '#{app_category}', will be ignored")
70 | else
71 | site.collections[app_category_id].docs << FDroidPackageDetailPage.new(site, site.source, package)
72 | end
73 | end
74 | end
75 |
76 | # Generate browsing pages
77 | site.includes_load_paths << (File.expand_path "../../_includes", File.dirname(__FILE__))
78 | site.pages << FDroidBrowsingPage.new(site, site.source)
79 | end
80 | end
81 | end
82 | end
83 |
--------------------------------------------------------------------------------
/_sass/jekyll-fdroid.scss:
--------------------------------------------------------------------------------
1 | /**
2 | * F-Droid's Jekyll Plugin
3 | *
4 | * Copyright (C) 2017 Nico Alt
5 | * Includes tweak by Dario Centrella in 2020
6 | *
7 | * This program is free software: you can redistribute it and/or modify
8 | * it under the terms of the GNU Affero General Public License as
9 | * published by the Free Software Foundation, either version 3 of the
10 | * License, or (at your option) any later version.
11 | *
12 | * This program is distributed in the hope that it will be useful,
13 | * but WITHOUT ANY WARRANTY; without even the implied warranty of
14 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 | * GNU Affero General Public License for more details.
16 | *
17 | * You should have received a copy of the GNU Affero General Public License
18 | * along with this program. If not, see .
19 | */
20 |
21 | @if global-variable-exists(icon-size) != true {
22 | $icon-size: 48px !global;
23 | }
24 | @if global-variable-exists(base-font-size) != true {
25 | $base-font-size: 16px !global;
26 | }
27 | @if global-variable-exists(spacing-unit) != true {
28 | $spacing-unit: 32px !global;
29 | }
30 | @if global-variable-exists(text-color) != true {
31 | $text-color: lighten(#000, 13%) !global;
32 | }
33 | @if global-variable-exists(text-color-light) != true {
34 | $text-color-light: lighten(#000, 46%) !global;
35 | }
36 |
37 | /**
38 | * Browsing
39 | */
40 |
41 | .package-header, .package-header:visited {
42 | display: flex;
43 | align-items: top;
44 | color: $text-color;
45 | margin-bottom: $spacing-unit / 3;
46 |
47 | &:hover {
48 | text-decoration: none;
49 | }
50 |
51 | .package-icon {
52 | width: $icon-size;
53 | height: $icon-size;
54 | margin-top: $spacing-unit / 4;
55 | margin-inline-end: $spacing-unit / 2;
56 | flex-shrink: 0;
57 | }
58 |
59 | .package-name {
60 | margin: 0;
61 | }
62 |
63 | .package-license {
64 | color: $text-color-light;
65 |
66 | &:before { content: " (" }
67 | &:after { content: ")" }
68 | }
69 | }
70 |
71 | /**
72 | * List of packages for sidebar. Show a more condensed view than the full list of packages.
73 | */
74 | .sidebar-widget .package-header {
75 | .package-icon {
76 | width: $icon-size * 5 / 6;
77 | height: $icon-size * 5 / 6;
78 | }
79 |
80 | .package-name {
81 | font-size: $base-font-size * 1.1;
82 | }
83 |
84 | .package-summary, .package-license {
85 | color: $text-color-light;
86 | font-size: $base-font-size * 0.9;
87 | }
88 | }
89 |
90 | .browse-navigation {
91 | @include reset-ul;
92 |
93 | display: inline-block;
94 | margin: 1em;
95 |
96 | .nav {
97 | $border-radius: 0.2em;
98 | display: inline;
99 | float: left;
100 | color: $text-color;
101 |
102 | .label {
103 | border: solid 1px #aaa;
104 | margin: 0 0 0 -1px;
105 | text-align: center;
106 | padding: 0.25em 0.5em;
107 | }
108 |
109 | &.disabled {
110 | color: #bbb;
111 | }
112 |
113 | &.active {
114 | .label {
115 | background-color: lighten($primary-color, 50%);
116 | }
117 | }
118 |
119 | &:first-child .label {
120 | border-top-left-radius: $border-radius;
121 | border-bottom-left-radius: $border-radius;
122 | }
123 |
124 | &:last-child .label {
125 | border-top-right-radius: $border-radius;
126 | border-bottom-right-radius: $border-radius;
127 | }
128 |
129 | a {
130 | color: $text-color;
131 | }
132 |
133 | a:hover {
134 | text-decoration: none;
135 | background: lighten($primary-color, 15%);
136 | }
137 |
138 | }
139 |
140 | // Only display previous, current, and next on smaller screens.
141 | @include media-query($tablet) {
142 | .nav .label {
143 | display: none;
144 | }
145 |
146 | .nav.page.active .label,
147 | .nav.previous .label,
148 | .nav.next .label {
149 | display: inline;
150 | }
151 | }
152 | }
153 |
154 | /**
155 | * Packages view
156 | */
157 | .package-versions-list {
158 | list-style:none;
159 | padding-inline-start: 0;
160 | }
161 |
162 | .package-version {
163 | margin-bottom: 5px;
164 | }
165 |
--------------------------------------------------------------------------------
/spec/spec_helper.rb:
--------------------------------------------------------------------------------
1 | # This file was generated by the `rspec --init` command. Conventionally, all
2 | # specs live under a `spec` directory, which RSpec adds to the `$LOAD_PATH`.
3 | # The generated `.rspec` file contains `--require spec_helper` which will cause
4 | # this file to always be loaded, without a need to explicitly require it in any
5 | # files.
6 | #
7 | # Given that it is always loaded, you are encouraged to keep this file as
8 | # light-weight as possible. Requiring heavyweight dependencies from this file
9 | # will add to the boot time of your test suite on EVERY test run, even for an
10 | # individual file that may not need all of that loaded. Instead, consider making
11 | # a separate helper file that requires the additional dependencies and performs
12 | # the additional setup, and require it from the spec files that actually need
13 | # it.
14 | #
15 | # The `.rspec` file also contains a few flags that are not defaults but that
16 | # users commonly want.
17 | #
18 | # See http://rubydoc.info/gems/rspec-core/RSpec/Core/Configuration
19 | RSpec.configure do |config|
20 | # rspec-expectations config goes here. You can use an alternate
21 | # assertion/expectation library such as wrong or the stdlib/minitest
22 | # assertions if you prefer.
23 | config.expect_with :rspec do |expectations|
24 | # This option will default to `true` in RSpec 4. It makes the `description`
25 | # and `failure_message` of custom matchers include text for helper methods
26 | # defined using `chain`, e.g.:
27 | # be_bigger_than(2).and_smaller_than(4).description
28 | # # => "be bigger than 2 and smaller than 4"
29 | # ...rather than:
30 | # # => "be bigger than 2"
31 | expectations.include_chain_clauses_in_custom_matcher_descriptions = true
32 | end
33 |
34 | # rspec-mocks config goes here. You can use an alternate test double
35 | # library (such as bogus or mocha) by changing the `mock_with` option here.
36 | config.mock_with :rspec do |mocks|
37 | # Prevents you from mocking or stubbing a method that does not exist on
38 | # a real object. This is generally recommended, and will default to
39 | # `true` in RSpec 4.
40 | mocks.verify_partial_doubles = true
41 | end
42 |
43 | # This option will default to `:apply_to_host_groups` in RSpec 4 (and will
44 | # have no way to turn it off -- the option exists only for backwards
45 | # compatibility in RSpec 3). It causes shared context metadata to be
46 | # inherited by the metadata hash of host groups and examples, rather than
47 | # triggering implicit auto-inclusion in groups with matching metadata.
48 | config.shared_context_metadata_behavior = :apply_to_host_groups
49 |
50 | # The settings below are suggested to provide a good initial experience
51 | # with RSpec, but feel free to customize to your heart's content.
52 | =begin
53 | # This allows you to limit a spec run to individual examples or groups
54 | # you care about by tagging them with `:focus` metadata. When nothing
55 | # is tagged with `:focus`, all examples get run. RSpec also provides
56 | # aliases for `it`, `describe`, and `context` that include `:focus`
57 | # metadata: `fit`, `fdescribe` and `fcontext`, respectively.
58 | config.filter_run_when_matching :focus
59 |
60 | # Allows RSpec to persist some state between runs in order to support
61 | # the `--only-failures` and `--next-failure` CLI options. We recommend
62 | # you configure your source control system to ignore this file.
63 | config.example_status_persistence_file_path = "spec/examples.txt"
64 |
65 | # Limits the available syntax to the non-monkey patched syntax that is
66 | # recommended. For more details, see:
67 | # - http://rspec.info/blog/2012/06/rspecs-new-expectation-syntax/
68 | # - http://www.teaisaweso.me/blog/2013/05/27/rspecs-new-message-expectation-syntax/
69 | # - http://rspec.info/blog/2014/05/notable-changes-in-rspec-3/#zero-monkey-patching-mode
70 | config.disable_monkey_patching!
71 |
72 | # This setting enables warnings. It's recommended, but in some cases may
73 | # be too noisy due to issues in dependencies.
74 | config.warnings = true
75 |
76 | # Many RSpec users commonly either run the entire suite or an individual
77 | # file, and it's useful to allow more verbose output when running an
78 | # individual spec file.
79 | if config.files_to_run.one?
80 | # Use the documentation formatter for detailed output,
81 | # unless a formatter has already been configured
82 | # (e.g. via a command-line flag).
83 | config.default_formatter = 'doc'
84 | end
85 |
86 | # Print the 10 slowest examples and example groups at the
87 | # end of the spec run, to help surface which specs are running
88 | # particularly slow.
89 | config.profile_examples = 10
90 |
91 | # Run specs in random order to surface order dependencies. If you find an
92 | # order dependency and want to debug it, you can fix the order by providing
93 | # the seed, which is printed after each run.
94 | # --seed 1234
95 | config.order = :random
96 |
97 | # Seed global randomization in this process using the `--seed` CLI option.
98 | # Setting this allows you to use `--seed` to deterministically reproduce
99 | # test failures related to randomization by passing the same `--seed` value
100 | # as the one that triggered the failure.
101 | Kernel.srand config.seed
102 | =end
103 | end
104 |
--------------------------------------------------------------------------------
/spec/assets/localized.json:
--------------------------------------------------------------------------------
1 | {
2 | "fr-CA": {
3 | "phoneScreenshots": [
4 | "Phone 1 [fr-CA].jpg",
5 | "Phone 2 [fr-CA].jpg"
6 | ],
7 | "wearScreenshots": [
8 | "Wear 1 [fr-CA].jpg",
9 | "Wear 2 [fr-CA].jpg"
10 | ],
11 | "sevenInchScreenshots": [
12 | "7\" 1 [fr-CA].jpg",
13 | "7\" 2 [fr-CA].jpg"
14 | ],
15 | "tenInchScreenshots": [
16 | "10\" 1 [fr-CA].jpg",
17 | "10\" 2 [fr-CA].jpg"
18 | ],
19 | "tvScreenshots": [
20 | "TV 1 [fr-CA].jpg",
21 | "TV 2 [fr-CA].jpg"
22 | ],
23 | "whatsNew": "Whats New [fr-CA].png",
24 | "video": "Video [fr-CA].png",
25 | "icon": "icon [fr-CA].png",
26 | "name": "App [fr-CA]",
27 | "summary": "Summary [fr-CA]",
28 | "featureGraphic": "Feature Graphic [fr-CA].png",
29 | "promoGraphic": "Promo Graphic [fr-CA].png",
30 | "tvBanner": "TV Banner [fr-CA].png",
31 | "description": "Description [fr-CA]"
32 | },
33 | "de": {
34 | "phoneScreenshots": [
35 | "Phone 1 [de].jpg",
36 | "Phone 2 [de].jpg"
37 | ],
38 | "wearScreenshots": [
39 | "Wear 1 [de].jpg",
40 | "Wear 2 [de].jpg"
41 | ],
42 | "sevenInchScreenshots": [
43 | "7\" 1 [de].jpg",
44 | "7\" 2 [de].jpg"
45 | ],
46 | "tenInchScreenshots": [
47 | "10\" 1 [de].jpg",
48 | "10\" 2 [de].jpg"
49 | ],
50 | "tvScreenshots": [
51 | "TV 1 [de].jpg",
52 | "TV 2 [de].jpg"
53 | ],
54 | "whatsNew": "Whats New [de].png",
55 | "video": "Video [de].png",
56 | "icon": "icon [de].png",
57 | "name": "App [de]",
58 | "summary": "Summary [de]",
59 | "featureGraphic": "Feature Graphic [de].png",
60 | "promoGraphic": "Promo Graphic [de].png",
61 | "tvBanner": "TV Banner [de].png",
62 | "description": "Description [de]"
63 | },
64 | "de-DE": {
65 | "phoneScreenshots": [
66 | "Phone 1 [de-DE].jpg",
67 | "Phone 2 [de-DE].jpg"
68 | ],
69 | "wearScreenshots": [
70 | "Wear 1 [de-DE].jpg",
71 | "Wear 2 [de-DE].jpg"
72 | ],
73 | "sevenInchScreenshots": [
74 | "7\" 1 [de-DE].jpg",
75 | "7\" 2 [de-DE].jpg"
76 | ],
77 | "tenInchScreenshots": [
78 | "10\" 1 [de-DE].jpg",
79 | "10\" 2 [de-DE].jpg"
80 | ],
81 | "tvScreenshots": [
82 | "TV 1 [de-DE].jpg",
83 | "TV 2 [de-DE].jpg"
84 | ],
85 | "whatsNew": "Whats New [de-DE].png",
86 | "video": "Video [de-DE].png",
87 | "icon": "icon [de-DE].png",
88 | "name": "App [de-DE]",
89 | "summary": "Summary [de-DE]",
90 | "featureGraphic": "Feature Graphic [de-DE].png",
91 | "promoGraphic": "Promo Graphic [de-DE].png",
92 | "tvBanner": "TV Banner [de-DE].png",
93 | "description": "Description [de-DE]"
94 | },
95 | "de-AT": {
96 | "phoneScreenshots": [
97 | "Phone 1 [de-AT].jpg",
98 | "Phone 2 [de-AT].jpg"
99 | ],
100 | "wearScreenshots": [
101 | "Wear 1 [de-AT].jpg",
102 | "Wear 2 [de-AT].jpg"
103 | ],
104 | "sevenInchScreenshots": [
105 | "7\" 1 [de-AT].jpg",
106 | "7\" 2 [de-AT].jpg"
107 | ],
108 | "tenInchScreenshots": [
109 | "10\" 1 [de-AT].jpg",
110 | "10\" 2 [de-AT].jpg"
111 | ],
112 | "tvScreenshots": [
113 | "TV 1 [de-AT].jpg",
114 | "TV 2 [de-AT].jpg"
115 | ],
116 | "whatsNew": "Whats New [de-AT].png",
117 | "video": "Video [de-AT].png",
118 | "icon": "icon [de-AT].png",
119 | "name": "App [de-AT]",
120 | "summary": "Summary [de-AT]",
121 | "featureGraphic": "Feature Graphic [de-AT].png",
122 | "promoGraphic": "Promo Graphic [de-AT].png",
123 | "tvBanner": "TV Banner [de-AT].png",
124 | "description": "Description [de-AT]"
125 | },
126 | "en-US": {
127 | "phoneScreenshots": [
128 | "Phone 1 [en-US].jpg",
129 | "Phone 2 [en-US].jpg"
130 | ],
131 | "wearScreenshots": [
132 | "Wear 1 [en-US].jpg",
133 | "Wear 2 [en-US].jpg"
134 | ],
135 | "sevenInchScreenshots": [
136 | "7\" 1 [en-US].jpg",
137 | "7\" 2 [en-US].jpg"
138 | ],
139 | "tenInchScreenshots": [
140 | "10\" 1 [en-US].jpg",
141 | "10\" 2 [en-US].jpg"
142 | ],
143 | "tvScreenshots": [
144 | "TV 1 [en-US].jpg",
145 | "TV 2 [en-US].jpg"
146 | ],
147 | "whatsNew": "Whats New [en-US].png",
148 | "video": "Video [en-US].png",
149 | "icon": "icon [en-US].png",
150 | "name": "App [en-US]",
151 | "summary": "Summary [en-US]",
152 | "featureGraphic": "Feature Graphic [en-US].png",
153 | "promoGraphic": "Promo Graphic [en-US].png",
154 | "tvBanner": "TV Banner [en-US].png",
155 | "description": "Description [en-US]"
156 | },
157 | "en-AU": {
158 | "phoneScreenshots": [
159 | "Phone 1 [en-AU].jpg",
160 | "Phone 2 [en-AU].jpg"
161 | ],
162 | "wearScreenshots": [
163 | "Wear 1 [en-AU].jpg",
164 | "Wear 2 [en-AU].jpg"
165 | ],
166 | "sevenInchScreenshots": [
167 | "7\" 1 [en-AU].jpg",
168 | "7\" 2 [en-AU].jpg"
169 | ],
170 | "tenInchScreenshots": [
171 | "10\" 1 [en-AU].jpg",
172 | "10\" 2 [en-AU].jpg"
173 | ],
174 | "tvScreenshots": [
175 | "TV 1 [en-AU].jpg",
176 | "TV 2 [en-AU].jpg"
177 | ],
178 | "whatsNew": "Whats New [en-AU].png",
179 | "video": "Video [en-AU].png",
180 | "icon": "icon [en-AU].png",
181 | "name": "App [en-AU]",
182 | "summary": "Summary [en-AU]",
183 | "featureGraphic": "Feature Graphic [en-AU].png",
184 | "promoGraphic": "Promo Graphic [en-AU].png",
185 | "tvBanner": "TV Banner [en-AU].png",
186 | "description": "Description [en-AU]"
187 | },
188 | "en": {
189 | "phoneScreenshots": [
190 | "Phone 1 [en].jpg",
191 | "Phone 2 [en].jpg"
192 | ],
193 | "wearScreenshots": [
194 | "Wear 1 [en].jpg",
195 | "Wear 2 [en].jpg"
196 | ],
197 | "sevenInchScreenshots": [
198 | "7\" 1 [en].jpg",
199 | "7\" 2 [en].jpg"
200 | ],
201 | "tenInchScreenshots": [
202 | "10\" 1 [en].jpg",
203 | "10\" 2 [en].jpg"
204 | ],
205 | "tvScreenshots": [
206 | "TV 1 [en].jpg",
207 | "TV 2 [en].jpg"
208 | ],
209 | "whatsNew": "Whats New [en].png",
210 | "video": "Video [en].png",
211 | "icon": "icon [en].png",
212 | "name": "App [en]",
213 | "summary": "Summary [en]",
214 | "featureGraphic": "Feature Graphic [en].png",
215 | "promoGraphic": "Promo Graphic [en].png",
216 | "tvBanner": "TV Banner [en].png",
217 | "description": "Description [en]"
218 | }
219 | }
--------------------------------------------------------------------------------
/lib/fdroid/Package.rb:
--------------------------------------------------------------------------------
1 | # F-Droid's Jekyll Plugin
2 | #
3 | # Copyright (C) 2017 Nico Alt
4 | # Copyright (C) 2022 FC Stegerman
5 | #
6 | # This program is free software: you can redistribute it and/or modify
7 | # it under the terms of the GNU Affero General Public License as
8 | # published by the Free Software Foundation, either version 3 of the
9 | # License, or (at your option) any later version.
10 | #
11 | # This program is distributed in the hope that it will be useful,
12 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 | # GNU Affero General Public License for more details.
15 | #
16 | # You should have received a copy of the GNU Affero General Public License
17 | # along with this program. If not, see .
18 |
19 | require 'loofah'
20 | require 'uri'
21 | require_relative './Version'
22 |
23 | # override the HTML elements loofah allows; be more restrictive
24 | module Loofah::HTML5::Scrub
25 | OVERRIDDEN_SAFE_ELEMENTS = Set.new(
26 | ["a", "b", "big", "blockquote", "br", "cite", "em", "i", "small",
27 | "strike", "strong", "sub", "sup", "tt", "u"] + ["li", "ol", "ul"]
28 | )
29 |
30 | def self.allowed_element?(element_name)
31 | OVERRIDDEN_SAFE_ELEMENTS.include?(element_name)
32 | end
33 | end
34 |
35 | module Loofah::Scrubbers
36 | class FDroid < Loofah::Scrubber
37 | def initialize
38 | @direction = :top_down
39 | end
40 |
41 | def scrub(node)
42 | return CONTINUE unless (node.type == Nokogiri::XML::Node::ELEMENT_NODE) && (node.name == 'a')
43 |
44 | node.keys.each do |attribute|
45 | if attribute != 'href'
46 | node.delete attribute
47 | end
48 | end
49 |
50 | begin
51 | url = URI.parse(node.attributes['href'].to_s)
52 | return STOP if url.host == nil || url.host.empty? || url.host == 'f-droid.org'
53 | rescue URI::Error
54 | # treat this URL as external
55 | end
56 |
57 | append_attribute(node, 'rel', 'external')
58 | append_attribute(node, 'rel', 'nofollow')
59 | append_attribute(node, 'rel', 'noopener')
60 | append_attribute(node, 'target', '_blank')
61 | return STOP
62 | end
63 | end
64 | end
65 |
66 | Loofah::Scrubbers::MAP[:fdroid] = Loofah::Scrubbers::FDroid
67 |
68 | module FDroid
69 | class Package
70 | def initialize(package, versions, locale)
71 | # Sort versions in reverse-chronological order
72 | @versions = versions.map { |p| Version.new(p) }
73 | @package = package
74 | @locale = locale
75 | @available_locales = package.key?('localized') ? Package.available_locales(locale, package['localized']) : nil
76 | @is_localized = Package.is_localized(locale, @available_locales)
77 | end
78 |
79 | # NB: safe (has strict checks on it and was the subject of a previous audit)
80 | def package_name
81 | @package['packageName']
82 | end
83 |
84 | def to_s
85 | package_name
86 | end
87 |
88 | # NB: safe (can contain '&' but must be in site.config["app_categories"])
89 | def categories
90 | @package['categories']
91 | end
92 |
93 | # Generates a hash of dumb strings to be used in templates.
94 | # If a specific value is not present, then it will have a nil value.
95 | # If a value can be localized, then it will choose the most appropriate
96 | # translation based on @available_locales and @locale.
97 | # The 'versions' key is an array of Version.to_data hashes.
98 | # @return [Hash]
99 | def to_data
100 | liberapay = @package['liberapay']
101 | if liberapay == nil
102 | liberapayID = @package['liberapayID']
103 | if liberapayID != nil
104 | liberapay = "~#{liberapayID}"
105 | end
106 | end
107 | data = {
108 | # These fields are taken as is from the metadata. If not present, they are
109 | 'package_name' => package_name,
110 | 'author_email' => @package['authorEmail'],
111 | 'author_name' => @package['authorName'],
112 | 'author_website' => @package['authorWebSite'],
113 | 'translation' => @package['translation'],
114 | 'bitcoin' => @package['bitcoin'],
115 | 'litecoin' => @package['litecoin'],
116 | 'donate' => @package['donate'],
117 | 'flattrID' => @package['flattrID'],
118 | 'liberapay' => liberapay,
119 | 'liberapayID' => @package['liberapayID'],
120 | 'openCollective' => @package['openCollective'],
121 | 'categories' => @package['categories'],
122 | 'anti_features' => @package['antiFeatures'],
123 | 'suggested_version_code' => suggested_version_code,
124 | 'suggested_version_name' => @versions.detect { |p| p.version_code == suggested_version_code }&.version_name,
125 | 'issue_tracker' => @package['issueTracker'],
126 | 'changelog' => @package['changelog'],
127 | 'license' => @package['license'],
128 | 'source_code' => @package['sourceCode'],
129 | 'website' => @package['webSite'],
130 | 'added' => @package['added'],
131 | 'last_updated' => @package['lastUpdated'],
132 | 'is_localized' => @is_localized,
133 | 'whats_new' => Package.process_package_description(Package.localized(@available_locales, @package['localized'], 'whatsNew')),
134 | 'icon' => icon,
135 | 'title' => name,
136 | 'summary' => summary,
137 | 'description' => Package.process_package_description(description),
138 | 'feature_graphic' => Package.localized_graphic_path(@available_locales, @package['localized'], 'featureGraphic'),
139 | 'phone_screenshots' => Package.localized_graphic_list_paths(@available_locales, @package['localized'], 'phoneScreenshots'),
140 | 'seven_inch_screenshots' => Package.localized_graphic_list_paths(@available_locales, @package['localized'], 'sevenInchScreenshots'),
141 | 'ten_inch_screenshots' => Package.localized_graphic_list_paths(@available_locales, @package['localized'], 'tenInchScreenshots'),
142 | 'tv_screenshots' => Package.localized_graphic_list_paths(@available_locales, @package['localized'], 'tvScreenshots'),
143 | 'wear_screenshots' => Package.localized_graphic_list_paths(@available_locales, @package['localized'], 'wearScreenshots'),
144 | 'versions' => @versions.sort.reverse.map { |p| p.to_data },
145 | 'beautiful_url' => "/packages/#{package_name}"
146 | }
147 |
148 | # recursively sanitise data before returning, except for description and
149 | # whats_new, which have already passed through process_package_description
150 | # (and were thus scrubbed by loofah via format_description_to_html)
151 | return Package.sanitise(data, skip = ['description', 'whats_new'])
152 | end
153 |
154 | # Any transformations which are required to turn the "description" into something which is
155 | # displayable via HTML is done here (e.g. replacing "fdroid.app:" schemes, formatting new lines,
156 | # etc.
157 | def self.process_package_description(string)
158 | return nil if string == nil
159 |
160 | format_description_to_html(replace_fdroid_app_links(string))
161 | end
162 |
163 | # Finds all https://f-droid.org links that end with an Application ID, and
164 | # replaces them with an HTML link.
165 | # @param [string] string
166 | # @return [string]
167 | def self.replace_fdroid_app_links(string)
168 | string.gsub(/fdroid\.app:([a-zA-Z0-9._]+)/,
169 | '\1 ')
170 | .gsub(/([^"])(https:\/\/f-droid\.org\/[^\s?#]+\/)((?:[a-zA-Z_]+(?:\d*[a-zA-Z_]*)*)(?:\.[a-zA-Z_]+(?:\d*[a-zA-Z_]*)*)*)\/?/,
171 | '\1\3 ')
172 | end
173 |
174 | # Ensure newlines in descriptions are preserved (converted to " " tags)
175 | # Handles UNIX, Windows and MacOS newlines, with a one-to-one replacement
176 | def self.format_description_to_html(string)
177 | Loofah.fragment(string)
178 | .scrub!(:strip)
179 | .scrub!(:fdroid)
180 | .to_html(:save_with => 0)
181 | .gsub(/(?:\n\r?|\r\n?)/, ' ')
182 | end
183 |
184 | # @param [string] available_locales
185 | # @param [string] localized
186 | # @param [string] field
187 | # @return [string]
188 | def self.localized(available_locales, localized, field)
189 | return nil unless available_locales != nil
190 |
191 | available_locales.each do |l|
192 | if localized[l].key?(field)
193 | return localized[l][field]
194 | end
195 | end
196 |
197 | return nil
198 | end
199 |
200 | # Prefixes the result with "chosen_locale/" before returning.
201 | # @see localized
202 | def self.localized_graphic_path(available_locales, localized, field)
203 | return nil unless available_locales != nil
204 |
205 | available_locales.each do |l|
206 | if localized[l].key?(field)
207 | return "#{l}/#{localized[l][field]}"
208 | end
209 | end
210 | return nil
211 | end
212 |
213 | # Similar to localized_graphic_path, but prefixes each item in the resulting array
214 | # with "chosen_locale/field/".
215 | # @see localized
216 | # @see localized_graphic_path
217 | def self.localized_graphic_list_paths(available_locales, localized, field)
218 | return nil unless available_locales != nil
219 |
220 | available_locales.each do |l|
221 | if localized[l].key?(field)
222 | return localized[l][field].map { |val| "#{l}/#{field}/#{val}" }
223 | end
224 | end
225 | return nil
226 | end
227 |
228 | # simple test for whether this app contains localized metadata for this app
229 | def self.is_localized(locale, available_locales)
230 | return nil unless locale != nil && available_locales != nil
231 | return locale if locale == '_'
232 |
233 | available_locales.each do |l|
234 | if l == locale
235 | return l
236 | end
237 | end
238 | lang = locale.split(/[_-]/)[0]
239 | available_locales.each do |l|
240 | if l == lang
241 | return l
242 | end
243 | end
244 | available_locales.each do |l|
245 | if l.start_with?(lang)
246 | return l
247 | end
248 | end
249 | return nil
250 | end
251 |
252 | # Given the desired_locale, searches through the list of localized_data entries
253 | # and finds those with keys which match either:
254 | # * The desired locale exactly
255 | # * The same language as the desired locale (but different region)
256 | # * Any English language (so if the desired language is not there it will suffice)
257 | #
258 | # These will be sorted in order of preference:
259 | # * Exact matches (language and region)
260 | # * Language portion matches and region matches an "alias" (e.g. zh_Hant and zh-TW).
261 | # * Language portion matches but region is absent/doesn't match.
262 | # * en-US
263 | # * en
264 | # * en-*
265 | #
266 | # It is intentionally liberal in searching for either "_" or "-" to separate language
267 | # and region, because they both mean (in different context) to split langugae on the
268 | # left, and region on the right, and it is cheap to do so.
269 | #
270 | # @param [string] desired_locale
271 | # @param [Hash] localized_data
272 | # @return [Array]
273 | def self.available_locales(desired_locale, localized_data)
274 | # website uses zh_Hant/zh_Hans, but zh-TW/zh-CN are common in localized data
275 | aliases = { 'zh' => { 'Hant' => ['TW'], 'Hans' => ['CN'] } }
276 |
277 | parts = desired_locale.split(/[_-]/)
278 | desired_lang = parts[0]
279 | desired_region = parts.length > 1 ? parts[1] : nil
280 |
281 | locales = localized_data.keys.select do |available_locale|
282 | parts = available_locale.split(/[_-]/)
283 | available_lang = parts[0]
284 | available_lang == desired_lang || available_lang == 'en'
285 | end
286 |
287 | measure_locale_goodness = lambda do |locale|
288 | parts = locale.split(/[_-]/)
289 | lang = parts[0]
290 | region = parts.length > 1 ? parts[1] : nil
291 | if locale == desired_locale
292 | return 1
293 | elsif lang == desired_lang
294 | if aliases.fetch(lang, {}).fetch(desired_region, []).include?(region)
295 | return 2
296 | else
297 | return 3
298 | end
299 | elsif locale == 'en-US'
300 | return 4
301 | elsif lang == 'en' && region.nil?
302 | return 5
303 | elsif lang == 'en'
304 | return 6
305 | end
306 | end
307 |
308 | locales.sort do |a, b|
309 | measure_locale_goodness.call(a) <=> measure_locale_goodness.call(b)
310 | end
311 | end
312 |
313 | # used to recursively sanitise the hash returned by to_data, except for any
314 | # data already passed through process_package_description (and thus scrubbed
315 | # by loofah)
316 | def self.sanitise(value, skip = [])
317 | case value
318 | when String
319 | value.gsub(/[<>"'&]/, ESCAPES)
320 | when Hash
321 | value.map { |k, v| skip.include?(k) ? [k, v] : [k, sanitise(v)] }.to_h
322 | when Array
323 | value.map { |x| sanitise(x) }
324 | when Date, Float, Integer, nil
325 | value
326 | else
327 | raise "cannot sanitise #{value.inspect}"
328 | end
329 | end
330 |
331 | ESCAPES = {
332 | '<' => '<', '>' => '>', '"' => '"', "'" => ''', '&' => '&'
333 | }
334 |
335 | private
336 |
337 | def icon
338 | localized = Package.localized_graphic_path(@available_locales, @package['localized'], 'icon')
339 | if localized
340 | "#{package_name}/#{localized}"
341 | elsif @package['icon']
342 | "icons-640/#{@package['icon']}"
343 | end
344 | end
345 |
346 | # this must exist since all entries are sorted by name,
347 | # it uses tildes since they sort last
348 | def name
349 | @package['name'] || Package.localized(@available_locales, @package['localized'], 'name') || '~missing name~'
350 | end
351 |
352 | def summary
353 | @package['summary'] || Package.localized(@available_locales, @package['localized'], 'summary')
354 | end
355 |
356 | def description
357 | @package['description'] || Package.localized(@available_locales, @package['localized'], 'description')
358 | end
359 |
360 | def suggested_version_code
361 | Integer(@package['suggestedVersionCode']) rescue nil
362 | end
363 | end
364 | end
365 |
--------------------------------------------------------------------------------
/spec/lib/fdroid/FDroidIndex_spec.rb:
--------------------------------------------------------------------------------
1 | # coding: utf-8
2 |
3 | require 'rspec'
4 | require 'pp'
5 | require 'json'
6 | require_relative '../../../lib/fdroid/IndexV1'
7 | require_relative '../../../lib/fdroid/Package'
8 |
9 | module FDroid
10 | RSpec.describe Package do
11 | localized_path = File.expand_path '../../assets/localized.json', File.dirname(__FILE__)
12 | localized = JSON.parse(File.read(localized_path))
13 |
14 | gp_path = File.expand_path '../../assets/index-v1.gp.json', File.dirname(__FILE__)
15 | gp_json = JSON.parse(File.read(gp_path))
16 |
17 | it 'Decides which locales to use' do
18 | de_locales = Package.available_locales('de-DE', localized)
19 | expect(de_locales).to eq(['de-DE', 'de', 'de-AT', 'en-US', 'en', 'en-AU'])
20 | expect(Package.is_localized('de-CH', de_locales)).to eq('de')
21 | expect(Package.is_localized('de', de_locales)).to eq('de')
22 | expect(Package.is_localized('de-DE', de_locales)).to eq('de-DE')
23 | expect(Package.is_localized('de-AT', de_locales)).to eq('de-AT')
24 | expect(Package.is_localized('en-AU', de_locales)).to eq('en-AU')
25 | expect(Package.is_localized('en-US', de_locales)).to eq('en-US')
26 | expect(Package.is_localized('en-GB', de_locales)).to eq('en')
27 | expect(Package.is_localized('zh-CN', de_locales)).to eq(nil)
28 |
29 | fr_locales = Package.available_locales('fr-FR', localized)
30 | expect(fr_locales).to eq(['fr-CA', 'en-US', 'en', 'en-AU'])
31 | expect(Package.is_localized('fr-CA', fr_locales)).to eq('fr-CA')
32 | expect(Package.is_localized('fr-FR', fr_locales)).to eq('fr-CA')
33 | expect(Package.is_localized('zh-CN', fr_locales)).to eq(nil)
34 |
35 | en_locales = Package.available_locales('en', localized)
36 | expect(en_locales).to eq(['en', 'en-US', 'en-AU'])
37 | expect(Package.is_localized('en-AU', en_locales)).to eq('en-AU')
38 | expect(Package.is_localized('en-ZA', en_locales)).to eq('en')
39 | expect(Package.is_localized('fr-FR', en_locales)).to eq(nil)
40 |
41 | zh_locales = Package.available_locales('zh', localized)
42 | expect(zh_locales).to eq(['en-US', 'en', 'en-AU'])
43 | expect(Package.is_localized('fr-FR', zh_locales)).to eq(nil)
44 | expect(Package.is_localized('zh-CN', zh_locales)).to eq(nil)
45 | end
46 |
47 | it 'Handles locale aliases' do
48 | localized_zh = { 'en-US' => {}, 'zh-CN' => {}, 'zh-TW' => {} }
49 | zh_Hant = Package.available_locales('zh_Hant', localized_zh)
50 | expect(zh_Hant).to eq(['zh-TW', 'zh-CN', 'en-US'])
51 | zh_Hans = Package.available_locales('zh_Hans', localized_zh)
52 | expect(zh_Hans).to eq(['zh-CN', 'zh-TW', 'en-US'])
53 | end
54 |
55 | it 'Calculates localized metadata correctly' do
56 | de_locales = Package.available_locales('de-DE', localized)
57 |
58 | name = Package.localized(de_locales, localized, 'name')
59 | expect(name).to eql('App [de-DE]')
60 |
61 | feature_graphic = Package.localized_graphic_path(de_locales, localized, 'featureGraphic')
62 | expect(feature_graphic).to eql('de-DE/Feature Graphic [de-DE].png')
63 |
64 | phone_screenshots = Package.localized_graphic_list_paths(de_locales, localized, 'phoneScreenshots')
65 | expect(phone_screenshots).to eql(
66 | [
67 | 'de-DE/phoneScreenshots/Phone 1 [de-DE].jpg',
68 | 'de-DE/phoneScreenshots/Phone 2 [de-DE].jpg',
69 | ]
70 | )
71 | end
72 |
73 | it 'Formats package descriptions correctly' do
74 | text = "This
75 | is
76 | a
77 |
78 | multi-line
79 |
80 | string
81 | here"
82 | multi_line = Package.format_description_to_html(text)
83 | expect(multi_line).to eql("This is a multi-line string here")
84 | end
85 |
86 | it 'Formats f-droid.org links in descriptions' do
87 | text = "fdroid.app:com.linuxcounter.lico_update_003:"
88 | multi_line = Package.process_package_description(text)
89 | expect(multi_line).to eql('com.linuxcounter.lico_update_003 :')
90 |
91 | text = "pointing to https://f-droid.org/packages/com.banasiak.coinflip/:
92 | This"
93 | multi_line = Package.process_package_description(text)
94 | expect(multi_line).to eql('pointing to com.banasiak.coinflip : This')
95 |
96 | text = "Starting with https://f-droid.org/packages/SpeedoMeterPackage.main.
97 | (this"
98 | multi_line = Package.process_package_description(text)
99 | expect(multi_line).to eql('Starting with SpeedoMeterPackage.main . (this')
100 |
101 | text = "works https://f-droid.org/packages/org.fitchfamily.android.wifi_backend_v2)
102 | Do"
103 | multi_line = Package.process_package_description(text)
104 | expect(multi_line).to eql('works org.fitchfamily.android.wifi_backend_v2 ) Do')
105 |
106 | text = "forget https://f-droid.org/packages/org.microg.nlp"
107 | multi_line = Package.process_package_description(text)
108 | expect(multi_line).to eql('forget org.microg.nlp ')
109 |
110 | text = ' * North America, Canada '
111 | multi_line = Package.process_package_description(text)
112 | expect(multi_line).to eql(text)
113 |
114 | text = ' Kwik DMAP is a stand-alone digital'
115 | multi_line = Package.process_package_description(text)
116 | expect(multi_line).to eql(text)
117 | end
118 |
119 | it 'Scrubs , , ,
125 | END
126 | output = <<~'END'.gsub("\n", ' ')
127 | bold text
128 |
129 |
130 | alert("oops")
131 | END
132 | scrubbed = Package.process_package_description(input)
133 | expect(scrubbed).to eql(output)
134 | end
135 |
136 | it 'Scrubs attributes from ' do
137 | text = ' CLICK ME! '
138 | multi_line = Package.process_package_description(text)
139 | expect(multi_line).to eql('CLICK ME! ')
140 |
141 | text = 'CLICK ME! '
142 | multi_line = Package.process_package_description(text)
143 | expect(multi_line).to eql('CLICK ME! ')
144 | end
145 |
146 | it 'strips attributes and adds "rel" stuff to tags' do
147 | input = <<~'END'
148 | bold text
149 | link
150 | END
151 | output = <<~'END'.gsub("\n", ' ')
152 | bold text
153 | link
154 | END
155 | scrubbed = Package.process_package_description(input)
156 | expect(scrubbed).to eql(output)
157 | end
158 |
159 | it 'sanitises' do
160 | input = {
161 | 'description' => 'bold/b>',
162 | 'summary' => 'bold ',
163 | 'foo' => [''],
164 | }
165 | output = {
166 | 'description' => 'bold/b>',
167 | 'summary' => '<b>bold</b>',
168 | 'foo' => ['<oops>'],
169 | }
170 | expect(Package.sanitise(input, skip = ['description'])).to eql(output)
171 | end
172 | end
173 |
174 | RSpec.describe Permission do
175 | it 'Serializes in a sane manner' do
176 | permission = Permission.new(["my-permission", nil]).to_data
177 | expect(permission).to eql({ "permission" => "my-permission", "min_sdk" => nil })
178 |
179 | version = Version.new(
180 | {
181 | "uses-permission" =>
182 | [
183 | ["perm1", nil],
184 | ["perm2", 24]
185 | ]
186 | }
187 | ).to_data
188 |
189 | expect(version['uses_permission']).to eql(
190 | [
191 | { "permission" => "perm1", "min_sdk" => nil },
192 | { "permission" => "perm2", "min_sdk" => 24 },
193 | ]
194 | )
195 | end
196 | end
197 |
198 | RSpec.describe IndexV1 do
199 | it 'Downloads and extracts jar files', :network => true do
200 | repo = 'https://guardianproject.info/fdroid/repo'
201 | index = FDroid::IndexV1.download(repo, 'en_US')
202 | expect(index.packages.count).to be >= 10
203 | end
204 |
205 | def parse_checkey_from_gp(locale)
206 | path = File.expand_path '../../assets/index-v1.gp.json', File.dirname(__FILE__)
207 | index_json = JSON.parse(File.read(path))
208 | index = FDroid::IndexV1.new(index_json, locale)
209 |
210 | expect(index.repo.name).to eql('Guardian Project Official Releases')
211 | expect(index.repo.address).to eql('https://guardianproject.info/fdroid/repo')
212 | expect(index.repo.date).to eql(Date.new(2017, 07, 19))
213 | expect(index.repo.description).to eql(
214 | 'The official app repository of The Guardian Project. Applications in ' +
215 | 'this repository are official binaries build by the original ' +
216 | 'application developers and signed by the same key as the APKs that ' +
217 | 'are released in the Google Play store. '
218 | )
219 |
220 | expect(index.packages.count).to eql(11)
221 |
222 | # Force each package to parse itself and make sure it doesn't crash.
223 | index.packages.each { |package| package.to_data }
224 |
225 | # Then return Checkey when we know that each package is able to be parsed.
226 | index.packages.detect { |package| package.package_name == 'info.guardianproject.checkey' }
227 | end
228 |
229 | def parse_camerav_from_gp(locale)
230 | path = File.expand_path '../../assets/index-v1.gp.json', File.dirname(__FILE__)
231 | index_json = JSON.parse(File.read(path))
232 | index = FDroid::IndexV1.new(index_json, locale)
233 | index.packages.detect { |package| package.package_name == 'org.witness.informacam.app' }
234 | end
235 |
236 | def parse_loofah_test_from_gp()
237 | path = File.expand_path '../../assets/index-v1.gp.json', File.dirname(__FILE__)
238 | index_json = JSON.parse(File.read(path))
239 | index = FDroid::IndexV1.new(index_json, 'en_US')
240 | index.packages.detect { |package| package.package_name == 'loofah.test' }
241 | end
242 |
243 | it 'Loofah runs on all text fields that can be rendered with HTML' do
244 | loofah_test = parse_loofah_test_from_gp().to_data
245 | expect(loofah_test['description']).to eq("This is just a test that alert('pwned!') loofah is stripping.")
246 | expect(loofah_test['summary']).to eq("트리거 불안에 때 개인 정보를 보호하거나 상황을 패닉 앱<script>alert('pwned!')</script>")
247 | expect(loofah_test['title']).to eq("<script>alert('PWN!')</script>")
248 | expect(loofah_test['whats_new']).to eq("Feature: * Add support for packs (@Rudloff) alert('pwned!') Minor: * Change name to Launcher ")
249 | end
250 |
251 | it 'Parses the Guardian Project repo metadata correctly' do
252 | checkey_en_US = parse_checkey_from_gp('en_US').to_data
253 | checkey_en_AU = parse_checkey_from_gp('en_AU').to_data
254 | checkey_en = parse_checkey_from_gp('en').to_data
255 | checkey_unknown = parse_checkey_from_gp('unknown locale').to_data
256 | checkey_unknown['is_localized'] = 'en-US' # fake this to keep the test simple
257 |
258 | expect(checkey_en_US).to eql(checkey_en_AU)
259 | expect(checkey_en_US).to eql(checkey_en)
260 | expect(checkey_en_US).to eql(checkey_unknown)
261 |
262 | checkey_fi = parse_checkey_from_gp('fi').to_data
263 |
264 | expect(checkey_en_US).not_to eql(checkey_fi)
265 | expect(checkey_en_US['title']).not_to eq(checkey_fi['title'])
266 | expect(checkey_en_US['summary']).not_to eq(checkey_fi['summary'])
267 | expect(checkey_en_US['description']).not_to eq(checkey_fi['description'])
268 |
269 | expect(checkey_en_US['phone_screenshots'].length).to eq(5)
270 |
271 | expect(checkey_en_US['liberapay']).to eql('GuardianProject')
272 | expect(checkey_en_US['liberapayID']).to eql('33617')
273 | end
274 |
275 | it 'Follows proper override rules for name/summary/description' do
276 | camerav_en_US = parse_camerav_from_gp('en_US').to_data
277 | camerav_th = parse_camerav_from_gp('th').to_data
278 |
279 | expect(camerav_en_US['title']).to eq(camerav_th['title'])
280 | expect(camerav_en_US['summary']).to eq(camerav_th['summary'])
281 | expect(camerav_en_US['description']).to eq(camerav_th['description'])
282 |
283 | expect(camerav_en_US['liberapay']).to eql('GuardianProject')
284 | expect(camerav_en_US['liberapayID']).to eql(nil)
285 | end
286 |
287 | it 'Processes the F-Droid repo metadata correctly' do
288 | path = File.expand_path '../../assets/index-v1.json', File.dirname(__FILE__)
289 | index_json = JSON.parse(File.read(path))
290 | index = FDroid::IndexV1.new(index_json, 'en_US')
291 |
292 | expect(index.repo.name).to eql('F-Droid')
293 | expect(index.repo.address).to eql('https://f-droid.org/repo')
294 | expect(index.repo.icon_url).to eql('https://f-droid.org/repo/icons/fdroid-icon.png')
295 | expect(index.repo.date).to eql(Date.new(2018, 12, 27))
296 | expect(index.repo.description).to eql(
297 | 'The official FDroid repository. Applications in this repository are ' +
298 | 'built directly from the source code. (One, Firefox, is the official ' +
299 | 'binary built by the Mozilla. This will ultimately be replaced by a ' +
300 | 'source-built version. '
301 | )
302 |
303 | expect(index.packages.count).to eql(1717)
304 |
305 | # Force each package to parse itself and make sure it doesn't crash.
306 | index.packages.each { |package| package.to_data }
307 |
308 | fdroid = index.packages.detect { |package| package.package_name == 'org.fdroid.fdroid' }.to_data
309 |
310 | # Assert that versions are ordered in reverse-chronological order
311 | expect(fdroid['versions'].map { |p| p['version_code'] }).to eql([1005050, 1005002, 1005001, 1005000, 1004050, 1004001, 1004000, 1003051, 1003050, 1003005, 1003004, 1003003])
312 |
313 | fdroid_version = fdroid['versions'][0]
314 | fdroid['versions'] = nil # remove versions for later app test
315 |
316 | expected_version = {
317 | "added" => Date.new(2018, 12, 27),
318 | "anti_features" => nil,
319 | "apk_name" => "org.fdroid.fdroid_1005050.apk",
320 | "file_extension" => "APK",
321 | "hash" => "edbabb5d76bbb509151f4cdbbc1f340a095e169b41838f4619ecb0b10ea702c8",
322 | "hash_type" => "sha256",
323 | "min_sdk_version" => "14",
324 | "max_sdk_version" => nil,
325 | "target_sdk_version" => "25",
326 | "nativecode" => nil,
327 | "srcname" => "org.fdroid.fdroid_1005050_src.tar.gz",
328 | "sig" => "9063aaadfff9cfd811a9c72fb5012f28",
329 | "signer" => "43238d512c1e5eb2d6569f4a3afbf5523418b82e0a3ed1552770abb9a9c9ccab",
330 | "size" => 7675508,
331 | "uses_permission" =>
332 | [
333 | { "min_sdk" => nil, "permission" => "android.permission.INTERNET" },
334 | { "min_sdk" => nil, "permission" => "android.permission.ACCESS_NETWORK_STATE" },
335 | { "min_sdk" => nil, "permission" => "android.permission.ACCESS_WIFI_STATE" },
336 | { "min_sdk" => nil, "permission" => "android.permission.CHANGE_WIFI_MULTICAST_STATE" },
337 | { "min_sdk" => nil, "permission" => "android.permission.CHANGE_NETWORK_STATE" },
338 | { "min_sdk" => nil, "permission" => "android.permission.CHANGE_WIFI_STATE" },
339 | { "min_sdk" => nil, "permission" => "android.permission.BLUETOOTH" },
340 | { "min_sdk" => nil, "permission" => "android.permission.BLUETOOTH_ADMIN" },
341 | { "min_sdk" => nil, "permission" => "android.permission.RECEIVE_BOOT_COMPLETED" },
342 | { "min_sdk" => nil, "permission" => "android.permission.READ_EXTERNAL_STORAGE" },
343 | { "min_sdk" => nil, "permission" => "android.permission.WRITE_EXTERNAL_STORAGE" },
344 | { "min_sdk" => nil, "permission" => "android.permission.WRITE_SETTINGS" },
345 | { "min_sdk" => nil, "permission" => "android.permission.NFC" },
346 | { "min_sdk" => nil, "permission" => "android.permission.WAKE_LOCK" }
347 | ],
348 | "version_name" => "1.5",
349 | "version_code" => 1005050,
350 | }
351 |
352 | expected_package = {
353 | "package_name" => "org.fdroid.fdroid",
354 | "author_email" => nil,
355 | "author_name" => nil,
356 | "author_website" => nil,
357 | "bitcoin" => "15u8aAPK4jJ5N8wpWJ5gutAyyeHtKX5i18",
358 | "litecoin" => nil,
359 | "openCollective" => "f-droid-testing",
360 | "donate" => "https://f-droid.org/about",
361 | "flattrID" => "343053",
362 | "categories" => ["System"],
363 | "anti_features" => nil,
364 | "suggested_version_code" => 1004050,
365 | "suggested_version_name" => "1.4",
366 | "issue_tracker" => "https://gitlab.com/fdroid/fdroidclient/issues",
367 | "translation" => "https://hosted.weblate.org/projects/f-droid/f-droid",
368 | "changelog" => "https://gitlab.com/fdroid/fdroidclient/raw/HEAD/CHANGELOG.md",
369 | "license" => "GPL-3.0-or-later",
370 | "source_code" => "https://gitlab.com/fdroid/fdroidclient",
371 | "website" => "https://f-droid.org",
372 | "added" => 1295222400000,
373 | "last_updated" => 1545900545000,
374 | "liberapay" => "~27859",
375 | "liberapayID" => "27859",
376 | "icon" => "icons-640/org.fdroid.fdroid.1005050.png",
377 | "is_localized" => "en-US",
378 | "title" => "F-Droid",
379 | "whats_new" => "* huge overhaul of the \"Versions\" list in the App Details screen, and many other UI improvements, thanks to new contributor @wsdfhjxc * fix keyboard/d-pad navigation in many places, thanks to new contributor @doeffinger * show \"Open\" button when media is installed and viewable * add Share button to \"Installed Apps\" to export CSV list * add clickable list of APKs to the swap HTML index page * retry index downloads from mirrors * fix \"Send F-Droid via Bluetooth\" on recent Android versions ",
380 | "summary" => "The app store that respects freedom and privacy",
381 | "description" =>
382 | "F-Droid is an installable catalogue of FOSS (Free and Open Source " +
383 | "Software) applications for the Android platform. The client makes it " +
384 | "easy to browse, install, and keep track of updates on your device. " +
385 | "It connects to any F-Droid compatible repositories. The default repo " +
386 | "is hosted at f-droid.org, which contains only bona fide Free and Open " +
387 | "Source Software. " +
388 | "Android itself is open in the sense that you are free to install apks " +
389 | "from anywhere you wish, but there are many good reasons for using " +
390 | "F-Droid as your free software app manager: " +
391 | "* Be notified when updates are available " +
392 | "* optionally download and install updates automatically " +
393 | "* Keep track of older and beta versions " +
394 | "* Filter apps that aren't compatible with the device " +
395 | "* Find apps via categories and searchable descriptions " +
396 | "* Access associated urls for donations, source code etc. " +
397 | "* Stay safe by checking repo index signatures and apk hashes ",
398 | "feature_graphic" => "en-US/featureGraphic.jpg",
399 | "phone_screenshots" => [
400 | "en-US/phoneScreenshots/screenshot-app-details.png",
401 | "en-US/phoneScreenshots/screenshot-dark-details.png",
402 | "en-US/phoneScreenshots/screenshot-dark-home.png",
403 | "en-US/phoneScreenshots/screenshot-dark-knownvuln.png",
404 | "en-US/phoneScreenshots/screenshot-knownvuln.png",
405 | "en-US/phoneScreenshots/screenshot-search.png",
406 | "en-US/phoneScreenshots/screenshot-updates.png"
407 | ],
408 | "seven_inch_screenshots" => nil,
409 | "ten_inch_screenshots" => nil,
410 | "tv_screenshots" => nil,
411 | "wear_screenshots" => nil,
412 | "versions" => nil,
413 | "beautiful_url" => "/packages/org.fdroid.fdroid"
414 | }
415 |
416 | expect(fdroid).to eql(expected_package)
417 | expect(fdroid_version).to eql(expected_version)
418 |
419 | anysoftkeyboard = index.packages.detect { |package| package.package_name == 'com.menny.android.anysoftkeyboard' }.to_data
420 | expect(anysoftkeyboard['whats_new']).to eql("* Power-Saving mode improvements - you can pick which features to include in Power-Saving. * Also, we allow switching to dark, simple theme in Power-Saving mode. But this is optional. * New Workman layout, Terminal generic-top-row and long-press fixes. Done by Alex Griffin. * Updated localization: AR, BE, EU, FR, HU, IT, KA, KN, KU, LT, NB, NL, PT, RO, RU, SC, UK. More here: https://github.com/AnySoftKeyboard/AnySoftKeyboard/milestone/87")
421 |
422 | subreddit = index.packages.detect { |package| package.package_name == 'subreddit.android.appstore' }.to_data
423 | expected_package_anti_features = [
424 | "NonFreeAdd",
425 | "NonFreeNet",
426 | ]
427 | expect(subreddit['anti_features']).to eql(expected_package_anti_features)
428 |
429 | droidnotify = index.packages.detect { |package| package.package_name == 'apps.droidnotify' }.to_data
430 | droidnotify_version = droidnotify['versions'][0]
431 | expected_version_anti_features = [
432 | "NoSourceSince"
433 | ]
434 | expect(droidnotify_version['anti_features']).to eql(expected_version_anti_features)
435 |
436 | perms_minsdk = index.packages.detect { |package| package.package_name == "protect.gift_card_guard" }.to_data
437 | perms_minsdk_version = perms_minsdk['versions'][0]
438 |
439 | expected_uses_permissions = [
440 | { "permission" => "android.permission.WRITE_EXTERNAL_STORAGE", "min_sdk" => 18 },
441 | { "permission" => "android.permission.READ_EXTERNAL_STORAGE", "min_sdk" => 18 },
442 | ]
443 | expect(perms_minsdk_version['uses_permission']).to eql(expected_uses_permissions)
444 | end
445 | end
446 | end
447 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | GNU AFFERO GENERAL PUBLIC LICENSE
2 | Version 3, 19 November 2007
3 |
4 | Copyright (C) 2007 Free Software Foundation, Inc.
5 | Everyone is permitted to copy and distribute verbatim copies
6 | of this license document, but changing it is not allowed.
7 |
8 | Preamble
9 |
10 | The GNU Affero General Public License is a free, copyleft license for
11 | software and other kinds of works, specifically designed to ensure
12 | cooperation with the community in the case of network server software.
13 |
14 | The licenses for most software and other practical works are designed
15 | to take away your freedom to share and change the works. By contrast,
16 | our General Public Licenses are intended to guarantee your freedom to
17 | share and change all versions of a program--to make sure it remains free
18 | software for all its users.
19 |
20 | When we speak of free software, we are referring to freedom, not
21 | price. Our General Public Licenses are designed to make sure that you
22 | have the freedom to distribute copies of free software (and charge for
23 | them if you wish), that you receive source code or can get it if you
24 | want it, that you can change the software or use pieces of it in new
25 | free programs, and that you know you can do these things.
26 |
27 | Developers that use our General Public Licenses protect your rights
28 | with two steps: (1) assert copyright on the software, and (2) offer
29 | you this License which gives you legal permission to copy, distribute
30 | and/or modify the software.
31 |
32 | A secondary benefit of defending all users' freedom is that
33 | improvements made in alternate versions of the program, if they
34 | receive widespread use, become available for other developers to
35 | incorporate. Many developers of free software are heartened and
36 | encouraged by the resulting cooperation. However, in the case of
37 | software used on network servers, this result may fail to come about.
38 | The GNU General Public License permits making a modified version and
39 | letting the public access it on a server without ever releasing its
40 | source code to the public.
41 |
42 | The GNU Affero General Public License is designed specifically to
43 | ensure that, in such cases, the modified source code becomes available
44 | to the community. It requires the operator of a network server to
45 | provide the source code of the modified version running there to the
46 | users of that server. Therefore, public use of a modified version, on
47 | a publicly accessible server, gives the public access to the source
48 | code of the modified version.
49 |
50 | An older license, called the Affero General Public License and
51 | published by Affero, was designed to accomplish similar goals. This is
52 | a different license, not a version of the Affero GPL, but Affero has
53 | released a new version of the Affero GPL which permits relicensing under
54 | this license.
55 |
56 | The precise terms and conditions for copying, distribution and
57 | modification follow.
58 |
59 | TERMS AND CONDITIONS
60 |
61 | 0. Definitions.
62 |
63 | "This License" refers to version 3 of the GNU Affero General Public License.
64 |
65 | "Copyright" also means copyright-like laws that apply to other kinds of
66 | works, such as semiconductor masks.
67 |
68 | "The Program" refers to any copyrightable work licensed under this
69 | License. Each licensee is addressed as "you". "Licensees" and
70 | "recipients" may be individuals or organizations.
71 |
72 | To "modify" a work means to copy from or adapt all or part of the work
73 | in a fashion requiring copyright permission, other than the making of an
74 | exact copy. The resulting work is called a "modified version" of the
75 | earlier work or a work "based on" the earlier work.
76 |
77 | A "covered work" means either the unmodified Program or a work based
78 | on the Program.
79 |
80 | To "propagate" a work means to do anything with it that, without
81 | permission, would make you directly or secondarily liable for
82 | infringement under applicable copyright law, except executing it on a
83 | computer or modifying a private copy. Propagation includes copying,
84 | distribution (with or without modification), making available to the
85 | public, and in some countries other activities as well.
86 |
87 | To "convey" a work means any kind of propagation that enables other
88 | parties to make or receive copies. Mere interaction with a user through
89 | a computer network, with no transfer of a copy, is not conveying.
90 |
91 | An interactive user interface displays "Appropriate Legal Notices"
92 | to the extent that it includes a convenient and prominently visible
93 | feature that (1) displays an appropriate copyright notice, and (2)
94 | tells the user that there is no warranty for the work (except to the
95 | extent that warranties are provided), that licensees may convey the
96 | work under this License, and how to view a copy of this License. If
97 | the interface presents a list of user commands or options, such as a
98 | menu, a prominent item in the list meets this criterion.
99 |
100 | 1. Source Code.
101 |
102 | The "source code" for a work means the preferred form of the work
103 | for making modifications to it. "Object code" means any non-source
104 | form of a work.
105 |
106 | A "Standard Interface" means an interface that either is an official
107 | standard defined by a recognized standards body, or, in the case of
108 | interfaces specified for a particular programming language, one that
109 | is widely used among developers working in that language.
110 |
111 | The "System Libraries" of an executable work include anything, other
112 | than the work as a whole, that (a) is included in the normal form of
113 | packaging a Major Component, but which is not part of that Major
114 | Component, and (b) serves only to enable use of the work with that
115 | Major Component, or to implement a Standard Interface for which an
116 | implementation is available to the public in source code form. A
117 | "Major Component", in this context, means a major essential component
118 | (kernel, window system, and so on) of the specific operating system
119 | (if any) on which the executable work runs, or a compiler used to
120 | produce the work, or an object code interpreter used to run it.
121 |
122 | The "Corresponding Source" for a work in object code form means all
123 | the source code needed to generate, install, and (for an executable
124 | work) run the object code and to modify the work, including scripts to
125 | control those activities. However, it does not include the work's
126 | System Libraries, or general-purpose tools or generally available free
127 | programs which are used unmodified in performing those activities but
128 | which are not part of the work. For example, Corresponding Source
129 | includes interface definition files associated with source files for
130 | the work, and the source code for shared libraries and dynamically
131 | linked subprograms that the work is specifically designed to require,
132 | such as by intimate data communication or control flow between those
133 | subprograms and other parts of the work.
134 |
135 | The Corresponding Source need not include anything that users
136 | can regenerate automatically from other parts of the Corresponding
137 | Source.
138 |
139 | The Corresponding Source for a work in source code form is that
140 | same work.
141 |
142 | 2. Basic Permissions.
143 |
144 | All rights granted under this License are granted for the term of
145 | copyright on the Program, and are irrevocable provided the stated
146 | conditions are met. This License explicitly affirms your unlimited
147 | permission to run the unmodified Program. The output from running a
148 | covered work is covered by this License only if the output, given its
149 | content, constitutes a covered work. This License acknowledges your
150 | rights of fair use or other equivalent, as provided by copyright law.
151 |
152 | You may make, run and propagate covered works that you do not
153 | convey, without conditions so long as your license otherwise remains
154 | in force. You may convey covered works to others for the sole purpose
155 | of having them make modifications exclusively for you, or provide you
156 | with facilities for running those works, provided that you comply with
157 | the terms of this License in conveying all material for which you do
158 | not control copyright. Those thus making or running the covered works
159 | for you must do so exclusively on your behalf, under your direction
160 | and control, on terms that prohibit them from making any copies of
161 | your copyrighted material outside their relationship with you.
162 |
163 | Conveying under any other circumstances is permitted solely under
164 | the conditions stated below. Sublicensing is not allowed; section 10
165 | makes it unnecessary.
166 |
167 | 3. Protecting Users' Legal Rights From Anti-Circumvention Law.
168 |
169 | No covered work shall be deemed part of an effective technological
170 | measure under any applicable law fulfilling obligations under article
171 | 11 of the WIPO copyright treaty adopted on 20 December 1996, or
172 | similar laws prohibiting or restricting circumvention of such
173 | measures.
174 |
175 | When you convey a covered work, you waive any legal power to forbid
176 | circumvention of technological measures to the extent such circumvention
177 | is effected by exercising rights under this License with respect to
178 | the covered work, and you disclaim any intention to limit operation or
179 | modification of the work as a means of enforcing, against the work's
180 | users, your or third parties' legal rights to forbid circumvention of
181 | technological measures.
182 |
183 | 4. Conveying Verbatim Copies.
184 |
185 | You may convey verbatim copies of the Program's source code as you
186 | receive it, in any medium, provided that you conspicuously and
187 | appropriately publish on each copy an appropriate copyright notice;
188 | keep intact all notices stating that this License and any
189 | non-permissive terms added in accord with section 7 apply to the code;
190 | keep intact all notices of the absence of any warranty; and give all
191 | recipients a copy of this License along with the Program.
192 |
193 | You may charge any price or no price for each copy that you convey,
194 | and you may offer support or warranty protection for a fee.
195 |
196 | 5. Conveying Modified Source Versions.
197 |
198 | You may convey a work based on the Program, or the modifications to
199 | produce it from the Program, in the form of source code under the
200 | terms of section 4, provided that you also meet all of these conditions:
201 |
202 | a) The work must carry prominent notices stating that you modified
203 | it, and giving a relevant date.
204 |
205 | b) The work must carry prominent notices stating that it is
206 | released under this License and any conditions added under section
207 | 7. This requirement modifies the requirement in section 4 to
208 | "keep intact all notices".
209 |
210 | c) You must license the entire work, as a whole, under this
211 | License to anyone who comes into possession of a copy. This
212 | License will therefore apply, along with any applicable section 7
213 | additional terms, to the whole of the work, and all its parts,
214 | regardless of how they are packaged. This License gives no
215 | permission to license the work in any other way, but it does not
216 | invalidate such permission if you have separately received it.
217 |
218 | d) If the work has interactive user interfaces, each must display
219 | Appropriate Legal Notices; however, if the Program has interactive
220 | interfaces that do not display Appropriate Legal Notices, your
221 | work need not make them do so.
222 |
223 | A compilation of a covered work with other separate and independent
224 | works, which are not by their nature extensions of the covered work,
225 | and which are not combined with it such as to form a larger program,
226 | in or on a volume of a storage or distribution medium, is called an
227 | "aggregate" if the compilation and its resulting copyright are not
228 | used to limit the access or legal rights of the compilation's users
229 | beyond what the individual works permit. Inclusion of a covered work
230 | in an aggregate does not cause this License to apply to the other
231 | parts of the aggregate.
232 |
233 | 6. Conveying Non-Source Forms.
234 |
235 | You may convey a covered work in object code form under the terms
236 | of sections 4 and 5, provided that you also convey the
237 | machine-readable Corresponding Source under the terms of this License,
238 | in one of these ways:
239 |
240 | a) Convey the object code in, or embodied in, a physical product
241 | (including a physical distribution medium), accompanied by the
242 | Corresponding Source fixed on a durable physical medium
243 | customarily used for software interchange.
244 |
245 | b) Convey the object code in, or embodied in, a physical product
246 | (including a physical distribution medium), accompanied by a
247 | written offer, valid for at least three years and valid for as
248 | long as you offer spare parts or customer support for that product
249 | model, to give anyone who possesses the object code either (1) a
250 | copy of the Corresponding Source for all the software in the
251 | product that is covered by this License, on a durable physical
252 | medium customarily used for software interchange, for a price no
253 | more than your reasonable cost of physically performing this
254 | conveying of source, or (2) access to copy the
255 | Corresponding Source from a network server at no charge.
256 |
257 | c) Convey individual copies of the object code with a copy of the
258 | written offer to provide the Corresponding Source. This
259 | alternative is allowed only occasionally and noncommercially, and
260 | only if you received the object code with such an offer, in accord
261 | with subsection 6b.
262 |
263 | d) Convey the object code by offering access from a designated
264 | place (gratis or for a charge), and offer equivalent access to the
265 | Corresponding Source in the same way through the same place at no
266 | further charge. You need not require recipients to copy the
267 | Corresponding Source along with the object code. If the place to
268 | copy the object code is a network server, the Corresponding Source
269 | may be on a different server (operated by you or a third party)
270 | that supports equivalent copying facilities, provided you maintain
271 | clear directions next to the object code saying where to find the
272 | Corresponding Source. Regardless of what server hosts the
273 | Corresponding Source, you remain obligated to ensure that it is
274 | available for as long as needed to satisfy these requirements.
275 |
276 | e) Convey the object code using peer-to-peer transmission, provided
277 | you inform other peers where the object code and Corresponding
278 | Source of the work are being offered to the general public at no
279 | charge under subsection 6d.
280 |
281 | A separable portion of the object code, whose source code is excluded
282 | from the Corresponding Source as a System Library, need not be
283 | included in conveying the object code work.
284 |
285 | A "User Product" is either (1) a "consumer product", which means any
286 | tangible personal property which is normally used for personal, family,
287 | or household purposes, or (2) anything designed or sold for incorporation
288 | into a dwelling. In determining whether a product is a consumer product,
289 | doubtful cases shall be resolved in favor of coverage. For a particular
290 | product received by a particular user, "normally used" refers to a
291 | typical or common use of that class of product, regardless of the status
292 | of the particular user or of the way in which the particular user
293 | actually uses, or expects or is expected to use, the product. A product
294 | is a consumer product regardless of whether the product has substantial
295 | commercial, industrial or non-consumer uses, unless such uses represent
296 | the only significant mode of use of the product.
297 |
298 | "Installation Information" for a User Product means any methods,
299 | procedures, authorization keys, or other information required to install
300 | and execute modified versions of a covered work in that User Product from
301 | a modified version of its Corresponding Source. The information must
302 | suffice to ensure that the continued functioning of the modified object
303 | code is in no case prevented or interfered with solely because
304 | modification has been made.
305 |
306 | If you convey an object code work under this section in, or with, or
307 | specifically for use in, a User Product, and the conveying occurs as
308 | part of a transaction in which the right of possession and use of the
309 | User Product is transferred to the recipient in perpetuity or for a
310 | fixed term (regardless of how the transaction is characterized), the
311 | Corresponding Source conveyed under this section must be accompanied
312 | by the Installation Information. But this requirement does not apply
313 | if neither you nor any third party retains the ability to install
314 | modified object code on the User Product (for example, the work has
315 | been installed in ROM).
316 |
317 | The requirement to provide Installation Information does not include a
318 | requirement to continue to provide support service, warranty, or updates
319 | for a work that has been modified or installed by the recipient, or for
320 | the User Product in which it has been modified or installed. Access to a
321 | network may be denied when the modification itself materially and
322 | adversely affects the operation of the network or violates the rules and
323 | protocols for communication across the network.
324 |
325 | Corresponding Source conveyed, and Installation Information provided,
326 | in accord with this section must be in a format that is publicly
327 | documented (and with an implementation available to the public in
328 | source code form), and must require no special password or key for
329 | unpacking, reading or copying.
330 |
331 | 7. Additional Terms.
332 |
333 | "Additional permissions" are terms that supplement the terms of this
334 | License by making exceptions from one or more of its conditions.
335 | Additional permissions that are applicable to the entire Program shall
336 | be treated as though they were included in this License, to the extent
337 | that they are valid under applicable law. If additional permissions
338 | apply only to part of the Program, that part may be used separately
339 | under those permissions, but the entire Program remains governed by
340 | this License without regard to the additional permissions.
341 |
342 | When you convey a copy of a covered work, you may at your option
343 | remove any additional permissions from that copy, or from any part of
344 | it. (Additional permissions may be written to require their own
345 | removal in certain cases when you modify the work.) You may place
346 | additional permissions on material, added by you to a covered work,
347 | for which you have or can give appropriate copyright permission.
348 |
349 | Notwithstanding any other provision of this License, for material you
350 | add to a covered work, you may (if authorized by the copyright holders of
351 | that material) supplement the terms of this License with terms:
352 |
353 | a) Disclaiming warranty or limiting liability differently from the
354 | terms of sections 15 and 16 of this License; or
355 |
356 | b) Requiring preservation of specified reasonable legal notices or
357 | author attributions in that material or in the Appropriate Legal
358 | Notices displayed by works containing it; or
359 |
360 | c) Prohibiting misrepresentation of the origin of that material, or
361 | requiring that modified versions of such material be marked in
362 | reasonable ways as different from the original version; or
363 |
364 | d) Limiting the use for publicity purposes of names of licensors or
365 | authors of the material; or
366 |
367 | e) Declining to grant rights under trademark law for use of some
368 | trade names, trademarks, or service marks; or
369 |
370 | f) Requiring indemnification of licensors and authors of that
371 | material by anyone who conveys the material (or modified versions of
372 | it) with contractual assumptions of liability to the recipient, for
373 | any liability that these contractual assumptions directly impose on
374 | those licensors and authors.
375 |
376 | All other non-permissive additional terms are considered "further
377 | restrictions" within the meaning of section 10. If the Program as you
378 | received it, or any part of it, contains a notice stating that it is
379 | governed by this License along with a term that is a further
380 | restriction, you may remove that term. If a license document contains
381 | a further restriction but permits relicensing or conveying under this
382 | License, you may add to a covered work material governed by the terms
383 | of that license document, provided that the further restriction does
384 | not survive such relicensing or conveying.
385 |
386 | If you add terms to a covered work in accord with this section, you
387 | must place, in the relevant source files, a statement of the
388 | additional terms that apply to those files, or a notice indicating
389 | where to find the applicable terms.
390 |
391 | Additional terms, permissive or non-permissive, may be stated in the
392 | form of a separately written license, or stated as exceptions;
393 | the above requirements apply either way.
394 |
395 | 8. Termination.
396 |
397 | You may not propagate or modify a covered work except as expressly
398 | provided under this License. Any attempt otherwise to propagate or
399 | modify it is void, and will automatically terminate your rights under
400 | this License (including any patent licenses granted under the third
401 | paragraph of section 11).
402 |
403 | However, if you cease all violation of this License, then your
404 | license from a particular copyright holder is reinstated (a)
405 | provisionally, unless and until the copyright holder explicitly and
406 | finally terminates your license, and (b) permanently, if the copyright
407 | holder fails to notify you of the violation by some reasonable means
408 | prior to 60 days after the cessation.
409 |
410 | Moreover, your license from a particular copyright holder is
411 | reinstated permanently if the copyright holder notifies you of the
412 | violation by some reasonable means, this is the first time you have
413 | received notice of violation of this License (for any work) from that
414 | copyright holder, and you cure the violation prior to 30 days after
415 | your receipt of the notice.
416 |
417 | Termination of your rights under this section does not terminate the
418 | licenses of parties who have received copies or rights from you under
419 | this License. If your rights have been terminated and not permanently
420 | reinstated, you do not qualify to receive new licenses for the same
421 | material under section 10.
422 |
423 | 9. Acceptance Not Required for Having Copies.
424 |
425 | You are not required to accept this License in order to receive or
426 | run a copy of the Program. Ancillary propagation of a covered work
427 | occurring solely as a consequence of using peer-to-peer transmission
428 | to receive a copy likewise does not require acceptance. However,
429 | nothing other than this License grants you permission to propagate or
430 | modify any covered work. These actions infringe copyright if you do
431 | not accept this License. Therefore, by modifying or propagating a
432 | covered work, you indicate your acceptance of this License to do so.
433 |
434 | 10. Automatic Licensing of Downstream Recipients.
435 |
436 | Each time you convey a covered work, the recipient automatically
437 | receives a license from the original licensors, to run, modify and
438 | propagate that work, subject to this License. You are not responsible
439 | for enforcing compliance by third parties with this License.
440 |
441 | An "entity transaction" is a transaction transferring control of an
442 | organization, or substantially all assets of one, or subdividing an
443 | organization, or merging organizations. If propagation of a covered
444 | work results from an entity transaction, each party to that
445 | transaction who receives a copy of the work also receives whatever
446 | licenses to the work the party's predecessor in interest had or could
447 | give under the previous paragraph, plus a right to possession of the
448 | Corresponding Source of the work from the predecessor in interest, if
449 | the predecessor has it or can get it with reasonable efforts.
450 |
451 | You may not impose any further restrictions on the exercise of the
452 | rights granted or affirmed under this License. For example, you may
453 | not impose a license fee, royalty, or other charge for exercise of
454 | rights granted under this License, and you may not initiate litigation
455 | (including a cross-claim or counterclaim in a lawsuit) alleging that
456 | any patent claim is infringed by making, using, selling, offering for
457 | sale, or importing the Program or any portion of it.
458 |
459 | 11. Patents.
460 |
461 | A "contributor" is a copyright holder who authorizes use under this
462 | License of the Program or a work on which the Program is based. The
463 | work thus licensed is called the contributor's "contributor version".
464 |
465 | A contributor's "essential patent claims" are all patent claims
466 | owned or controlled by the contributor, whether already acquired or
467 | hereafter acquired, that would be infringed by some manner, permitted
468 | by this License, of making, using, or selling its contributor version,
469 | but do not include claims that would be infringed only as a
470 | consequence of further modification of the contributor version. For
471 | purposes of this definition, "control" includes the right to grant
472 | patent sublicenses in a manner consistent with the requirements of
473 | this License.
474 |
475 | Each contributor grants you a non-exclusive, worldwide, royalty-free
476 | patent license under the contributor's essential patent claims, to
477 | make, use, sell, offer for sale, import and otherwise run, modify and
478 | propagate the contents of its contributor version.
479 |
480 | In the following three paragraphs, a "patent license" is any express
481 | agreement or commitment, however denominated, not to enforce a patent
482 | (such as an express permission to practice a patent or covenant not to
483 | sue for patent infringement). To "grant" such a patent license to a
484 | party means to make such an agreement or commitment not to enforce a
485 | patent against the party.
486 |
487 | If you convey a covered work, knowingly relying on a patent license,
488 | and the Corresponding Source of the work is not available for anyone
489 | to copy, free of charge and under the terms of this License, through a
490 | publicly available network server or other readily accessible means,
491 | then you must either (1) cause the Corresponding Source to be so
492 | available, or (2) arrange to deprive yourself of the benefit of the
493 | patent license for this particular work, or (3) arrange, in a manner
494 | consistent with the requirements of this License, to extend the patent
495 | license to downstream recipients. "Knowingly relying" means you have
496 | actual knowledge that, but for the patent license, your conveying the
497 | covered work in a country, or your recipient's use of the covered work
498 | in a country, would infringe one or more identifiable patents in that
499 | country that you have reason to believe are valid.
500 |
501 | If, pursuant to or in connection with a single transaction or
502 | arrangement, you convey, or propagate by procuring conveyance of, a
503 | covered work, and grant a patent license to some of the parties
504 | receiving the covered work authorizing them to use, propagate, modify
505 | or convey a specific copy of the covered work, then the patent license
506 | you grant is automatically extended to all recipients of the covered
507 | work and works based on it.
508 |
509 | A patent license is "discriminatory" if it does not include within
510 | the scope of its coverage, prohibits the exercise of, or is
511 | conditioned on the non-exercise of one or more of the rights that are
512 | specifically granted under this License. You may not convey a covered
513 | work if you are a party to an arrangement with a third party that is
514 | in the business of distributing software, under which you make payment
515 | to the third party based on the extent of your activity of conveying
516 | the work, and under which the third party grants, to any of the
517 | parties who would receive the covered work from you, a discriminatory
518 | patent license (a) in connection with copies of the covered work
519 | conveyed by you (or copies made from those copies), or (b) primarily
520 | for and in connection with specific products or compilations that
521 | contain the covered work, unless you entered into that arrangement,
522 | or that patent license was granted, prior to 28 March 2007.
523 |
524 | Nothing in this License shall be construed as excluding or limiting
525 | any implied license or other defenses to infringement that may
526 | otherwise be available to you under applicable patent law.
527 |
528 | 12. No Surrender of Others' Freedom.
529 |
530 | If conditions are imposed on you (whether by court order, agreement or
531 | otherwise) that contradict the conditions of this License, they do not
532 | excuse you from the conditions of this License. If you cannot convey a
533 | covered work so as to satisfy simultaneously your obligations under this
534 | License and any other pertinent obligations, then as a consequence you may
535 | not convey it at all. For example, if you agree to terms that obligate you
536 | to collect a royalty for further conveying from those to whom you convey
537 | the Program, the only way you could satisfy both those terms and this
538 | License would be to refrain entirely from conveying the Program.
539 |
540 | 13. Remote Network Interaction; Use with the GNU General Public License.
541 |
542 | Notwithstanding any other provision of this License, if you modify the
543 | Program, your modified version must prominently offer all users
544 | interacting with it remotely through a computer network (if your version
545 | supports such interaction) an opportunity to receive the Corresponding
546 | Source of your version by providing access to the Corresponding Source
547 | from a network server at no charge, through some standard or customary
548 | means of facilitating copying of software. This Corresponding Source
549 | shall include the Corresponding Source for any work covered by version 3
550 | of the GNU General Public License that is incorporated pursuant to the
551 | following paragraph.
552 |
553 | Notwithstanding any other provision of this License, you have
554 | permission to link or combine any covered work with a work licensed
555 | under version 3 of the GNU General Public License into a single
556 | combined work, and to convey the resulting work. The terms of this
557 | License will continue to apply to the part which is the covered work,
558 | but the work with which it is combined will remain governed by version
559 | 3 of the GNU General Public License.
560 |
561 | 14. Revised Versions of this License.
562 |
563 | The Free Software Foundation may publish revised and/or new versions of
564 | the GNU Affero General Public License from time to time. Such new versions
565 | will be similar in spirit to the present version, but may differ in detail to
566 | address new problems or concerns.
567 |
568 | Each version is given a distinguishing version number. If the
569 | Program specifies that a certain numbered version of the GNU Affero General
570 | Public License "or any later version" applies to it, you have the
571 | option of following the terms and conditions either of that numbered
572 | version or of any later version published by the Free Software
573 | Foundation. If the Program does not specify a version number of the
574 | GNU Affero General Public License, you may choose any version ever published
575 | by the Free Software Foundation.
576 |
577 | If the Program specifies that a proxy can decide which future
578 | versions of the GNU Affero General Public License can be used, that proxy's
579 | public statement of acceptance of a version permanently authorizes you
580 | to choose that version for the Program.
581 |
582 | Later license versions may give you additional or different
583 | permissions. However, no additional obligations are imposed on any
584 | author or copyright holder as a result of your choosing to follow a
585 | later version.
586 |
587 | 15. Disclaimer of Warranty.
588 |
589 | THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
590 | APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
591 | HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
592 | OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
593 | THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
594 | PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
595 | IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
596 | ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
597 |
598 | 16. Limitation of Liability.
599 |
600 | IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
601 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
602 | THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
603 | GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
604 | USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
605 | DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
606 | PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
607 | EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
608 | SUCH DAMAGES.
609 |
610 | 17. Interpretation of Sections 15 and 16.
611 |
612 | If the disclaimer of warranty and limitation of liability provided
613 | above cannot be given local legal effect according to their terms,
614 | reviewing courts shall apply local law that most closely approximates
615 | an absolute waiver of all civil liability in connection with the
616 | Program, unless a warranty or assumption of liability accompanies a
617 | copy of the Program in return for a fee.
618 |
619 | END OF TERMS AND CONDITIONS
620 |
621 | How to Apply These Terms to Your New Programs
622 |
623 | If you develop a new program, and you want it to be of the greatest
624 | possible use to the public, the best way to achieve this is to make it
625 | free software which everyone can redistribute and change under these terms.
626 |
627 | To do so, attach the following notices to the program. It is safest
628 | to attach them to the start of each source file to most effectively
629 | state the exclusion of warranty; and each file should have at least
630 | the "copyright" line and a pointer to where the full notice is found.
631 |
632 |
633 | Copyright (C)
634 |
635 | This program is free software: you can redistribute it and/or modify
636 | it under the terms of the GNU Affero General Public License as published by
637 | the Free Software Foundation, either version 3 of the License, or
638 | (at your option) any later version.
639 |
640 | This program is distributed in the hope that it will be useful,
641 | but WITHOUT ANY WARRANTY; without even the implied warranty of
642 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
643 | GNU Affero General Public License for more details.
644 |
645 | You should have received a copy of the GNU Affero General Public License
646 | along with this program. If not, see .
647 |
648 | Also add information on how to contact you by electronic and paper mail.
649 |
650 | If your software can interact with users remotely through a computer
651 | network, you should also make sure that it provides a way for users to
652 | get its source. For example, if your program is a web application, its
653 | interface could display a "Source" link that leads users to an archive
654 | of the code. There are many ways you could offer source, and different
655 | solutions will be better for different programs; see section 13 for the
656 | specific requirements.
657 |
658 | You should also get your employer (if you work as a programmer) or school,
659 | if any, to sign a "copyright disclaimer" for the program, if necessary.
660 | For more information on this, and how to apply and follow the GNU AGPL, see
661 | .
662 |
--------------------------------------------------------------------------------