headdesk 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.gitignore +10 -0
- data/.reek.yml +10 -0
- data/.ruby-gemset +1 -0
- data/.ruby-version +1 -0
- data/Gemfile +10 -0
- data/Gemfile.lock +86 -0
- data/LICENSE.txt +21 -0
- data/README.md +74 -0
- data/Rakefile +4 -0
- data/bin/console +15 -0
- data/bin/facebook_sdk_versions +24 -0
- data/bin/setup +8 -0
- data/exe/headdesk +5 -0
- data/ext/apktool_2.3.4.jar +0 -0
- data/headdesk.gemspec +44 -0
- data/lib/headdesk.rb +24 -0
- data/lib/headdesk/analize.rb +14 -0
- data/lib/headdesk/apk.rb +78 -0
- data/lib/headdesk/apk/class.rb +46 -0
- data/lib/headdesk/apk/field.rb +28 -0
- data/lib/headdesk/apk/method.rb +16 -0
- data/lib/headdesk/apk/resources.rb +95 -0
- data/lib/headdesk/apktool.rb +45 -0
- data/lib/headdesk/check.rb +184 -0
- data/lib/headdesk/checks/api26.rb +27 -0
- data/lib/headdesk/checks/facebook.rb +41 -0
- data/lib/headdesk/checks/receiver.rb +31 -0
- data/lib/headdesk/checks/teak.rb +52 -0
- data/lib/headdesk/checks/teak/api21_icon.rb +34 -0
- data/lib/headdesk/checks/teak/caching.rb +30 -0
- data/lib/headdesk/checks/teak/teak.rb +74 -0
- data/lib/headdesk/cli.rb +107 -0
- data/lib/headdesk/data/facebook_sdk_versions.yaml +281 -0
- data/lib/headdesk/descriptionator.rb +36 -0
- data/lib/headdesk/report.rb +91 -0
- data/lib/headdesk/version.rb +6 -0
- metadata +180 -0
@@ -0,0 +1,41 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Headdesk
|
4
|
+
module Checks
|
5
|
+
#
|
6
|
+
# Check the version of the Facebook SDK
|
7
|
+
#
|
8
|
+
# Facebook deprecates SDKs 2 years after they are released.
|
9
|
+
#
|
10
|
+
class FacebookSDK
|
11
|
+
include Check::APK
|
12
|
+
|
13
|
+
describe 'Facebook SDK version'
|
14
|
+
def call
|
15
|
+
skip_check unless: -> { apk.class?('com/facebook/FacebookSdk') }
|
16
|
+
facebook_sdk = apk.find_class('com/facebook/FacebookSdk')
|
17
|
+
|
18
|
+
# TODO: Parse https://developers.facebook.com/docs/android/change-log-4x
|
19
|
+
# and fail if > 2 years old, warn if < 3 months remaining
|
20
|
+
get_sdk_version = facebook_sdk.method('getSdkVersion').code
|
21
|
+
describe 'com.facebook.FacebookSdk contains getSdkVersion method'
|
22
|
+
fail_check if: -> { !get_sdk_version }
|
23
|
+
|
24
|
+
facebook_sdks = YAML.load_file(Headdesk::FACEBOOK_SDK_VERSIONS_YAML)
|
25
|
+
major, minor, patch = get_sdk_version.match(/const-string v0, "(\d+)\.(\d+)\.(\d+)"/).captures.map(&:to_i)
|
26
|
+
|
27
|
+
sdk_in_use = (facebook_sdks.select do |sdk|
|
28
|
+
sdk[:major] == major && sdk[:minor] == minor && sdk[:patch] == patch
|
29
|
+
end).first
|
30
|
+
|
31
|
+
describe "Found Facebook SDK version #{sdk_in_use[:version]}"
|
32
|
+
fail_check if: -> { !sdk_in_use }
|
33
|
+
|
34
|
+
export facebook_sdk: sdk_in_use
|
35
|
+
|
36
|
+
describe "Facebook SDK was released in the last 2 years (using #{sdk_in_use[:version]}, released #{sdk_in_use[:date]})"
|
37
|
+
fail_check if: -> { sdk_in_use[:date] < (Date.today - (365 * 2)) }
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Headdesk
|
4
|
+
module Checks
|
5
|
+
#
|
6
|
+
# Make sure all <reciever> blocks in AndroidManifest.xml point to a Java class
|
7
|
+
# that exists in the APK.
|
8
|
+
#
|
9
|
+
class Receiver
|
10
|
+
include Check::APK
|
11
|
+
|
12
|
+
describe 'All <receiver> blocks in AndroidManifest.xml point to valid Java classes'
|
13
|
+
def call
|
14
|
+
receivers = []
|
15
|
+
apk.android_manifest.xpath('//receiver').each do |receiver|
|
16
|
+
receiver_name = receiver.attributes['name'].to_s
|
17
|
+
fail_check unless: -> { apk.class?(receiver_name) }
|
18
|
+
klass = apk.find_class(receiver_name)
|
19
|
+
|
20
|
+
describe "#{receiver_name} has onReceive method"
|
21
|
+
fail_check unless: -> { klass.method?('onReceive') }
|
22
|
+
|
23
|
+
receivers << {
|
24
|
+
name: receiver_name
|
25
|
+
}
|
26
|
+
end
|
27
|
+
export receivers: receivers
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,52 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Headdesk
|
4
|
+
module Checks
|
5
|
+
#
|
6
|
+
# Module for Teak checks
|
7
|
+
#
|
8
|
+
module Teak
|
9
|
+
#
|
10
|
+
# Module for Teak APK checks
|
11
|
+
#
|
12
|
+
module APK
|
13
|
+
def self.included(klass)
|
14
|
+
klass.include(Check::APK)
|
15
|
+
klass.extend(Preconditions)
|
16
|
+
klass.include(Utility)
|
17
|
+
end
|
18
|
+
|
19
|
+
#
|
20
|
+
# Precondition tests for presence of Teak class SDK in APK
|
21
|
+
#
|
22
|
+
module Preconditions
|
23
|
+
def preconditions?
|
24
|
+
false unless apk.class?('io.teak.sdk.Teak')
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
#
|
29
|
+
# Utility methods for Teak based checks
|
30
|
+
#
|
31
|
+
module Utility
|
32
|
+
def teak_sdk
|
33
|
+
return @teak_sdk if @teak_sdk
|
34
|
+
|
35
|
+
major, minor, revision = apk.find_class('io.teak.sdk.Teak')
|
36
|
+
.field('SDKVersion')
|
37
|
+
.value
|
38
|
+
.to_version
|
39
|
+
@teak_sdk = OpenStruct.new(
|
40
|
+
version: "#{major}.#{minor}.#{revision}",
|
41
|
+
major: major, minor: minor, revision: revision
|
42
|
+
)
|
43
|
+
|
44
|
+
@teak_sdk
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
Dir[File.dirname(__FILE__) + '/teak/*.rb'].each { |file| require file }
|
@@ -0,0 +1,34 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Headdesk
|
4
|
+
module Checks
|
5
|
+
module Teak
|
6
|
+
#
|
7
|
+
# Check to make sure that an APK, which uses Teak, has an API 21+ icon.
|
8
|
+
#
|
9
|
+
class Api21Icon
|
10
|
+
include Checks::Teak::APK
|
11
|
+
|
12
|
+
describe 'Check for API 21+ Teak icons'
|
13
|
+
# :reek:UncommunicativeVariableName { accept: ['icon_v21'] }
|
14
|
+
def call
|
15
|
+
skip_check if: -> { apk.min_sdk 21 }
|
16
|
+
|
17
|
+
icon = apk.resources
|
18
|
+
.values(v: 20)
|
19
|
+
.drawable.io_teak_small_notification_icon
|
20
|
+
icon_v21 = apk.resources
|
21
|
+
.values(v: 21)
|
22
|
+
.drawable.io_teak_small_notification_icon
|
23
|
+
export icon: icon, icon_v21: icon_v21
|
24
|
+
|
25
|
+
describe "APK contains a drawable for 'io_teak_small_notification_icon'"
|
26
|
+
fail_check unless: -> { icon && icon_v21 }
|
27
|
+
|
28
|
+
describe "'io_teak_small_notification_icon' for v21 is different from < v21"
|
29
|
+
fail_check unless: -> { icon != icon_v21 }
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Headdesk
|
4
|
+
module Checks
|
5
|
+
module Teak
|
6
|
+
#
|
7
|
+
# Check to make sure that an APK, which uses Teak, has caching enabled.
|
8
|
+
#
|
9
|
+
class Caching
|
10
|
+
include Checks::Teak::APK
|
11
|
+
|
12
|
+
describe 'Check for io_teak_enable_caching'
|
13
|
+
def call
|
14
|
+
describe 'Teak SDK version is lower than 2.0.0'
|
15
|
+
major, = apk.find_class('io.teak.sdk.Teak')
|
16
|
+
.field('SDKVersion')
|
17
|
+
.value
|
18
|
+
.to_version
|
19
|
+
skip_check if: major.to_i >= 2
|
20
|
+
|
21
|
+
describe "APK enables caching of Teak notification content (via 'io_teak_enable_caching')"
|
22
|
+
fail_check unless: apk.resources
|
23
|
+
.values(v: 21)
|
24
|
+
.bool
|
25
|
+
.io_teak_enable_caching
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,74 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Headdesk
|
4
|
+
module Checks
|
5
|
+
module Teak
|
6
|
+
#
|
7
|
+
# Check to make sure that an APK, which uses Teak, has caching enabled.
|
8
|
+
#
|
9
|
+
class Configuration
|
10
|
+
include Checks::Teak::APK
|
11
|
+
|
12
|
+
describe 'Check Teak configuration'
|
13
|
+
# :reek:UncommunicativeVariableName { accept: ['gcm_defaultSenderId'] }
|
14
|
+
def call
|
15
|
+
# App Id
|
16
|
+
teak_app_id = apk.resources
|
17
|
+
.values
|
18
|
+
.string
|
19
|
+
.io_teak_app_id
|
20
|
+
# TODO: Manifest meta-data
|
21
|
+
describe 'Teak App Id configured'
|
22
|
+
fail_check if: !teak_app_id
|
23
|
+
export teak_app_id: teak_app_id
|
24
|
+
|
25
|
+
# Api Key
|
26
|
+
teak_api_key = apk.resources
|
27
|
+
.values
|
28
|
+
.string
|
29
|
+
.io_teak_api_key
|
30
|
+
# TODO: Manifest meta-data
|
31
|
+
describe 'Teak API Key configured'
|
32
|
+
fail_check if: !teak_api_key
|
33
|
+
export teak_api_key: teak_api_key
|
34
|
+
|
35
|
+
# GCM Sender Id
|
36
|
+
io_teak_gcm_sender_id = apk.resources
|
37
|
+
.values
|
38
|
+
.string
|
39
|
+
.io_teak_gcm_sender_id
|
40
|
+
# TODO: Manifest meta-data
|
41
|
+
gcm_defaultSenderId ||= apk.resources
|
42
|
+
.values
|
43
|
+
.string
|
44
|
+
.gcm_defaultSenderId
|
45
|
+
describe "'gcm_defaultSenderId' is specified, and different from 'io_teak_gcm_sender_id'"
|
46
|
+
fail_check if: gcm_defaultSenderId != io_teak_gcm_sender_id if gcm_defaultSenderId
|
47
|
+
|
48
|
+
gcm_sender_id = io_teak_gcm_sender_id || gcm_defaultSenderId
|
49
|
+
describe "Either 'io_teak_gcm_sender_id' or 'gcm_defaultSenderId' configured"
|
50
|
+
fail_check if: !gcm_sender_id
|
51
|
+
export gcm_sender_id: gcm_sender_id
|
52
|
+
|
53
|
+
# Firebase App Id
|
54
|
+
io_teak_firebase_app_id = apk.resources
|
55
|
+
.values
|
56
|
+
.string
|
57
|
+
.io_teak_firebase_app_id
|
58
|
+
# TODO: Manifest meta-data
|
59
|
+
google_app_id = apk.resources
|
60
|
+
.values
|
61
|
+
.string
|
62
|
+
.google_app_id
|
63
|
+
describe "'google_app_id' is specified, and different from 'io_teak_firebase_app_id'"
|
64
|
+
fail_check if: google_app_id != io_teak_firebase_app_id if google_app_id
|
65
|
+
|
66
|
+
firebase_app_id = google_app_id || io_teak_firebase_app_id
|
67
|
+
describe "Either 'io_teak_firebase_app_id' or 'google_app_id' configured"
|
68
|
+
fail_check if: !firebase_app_id, skip_if: teak_sdk.major < 2
|
69
|
+
export gcm_sender_id: gcm_sender_id
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
data/lib/headdesk/cli.rb
ADDED
@@ -0,0 +1,107 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'tmpdir'
|
4
|
+
require 'thor'
|
5
|
+
require 'headdesk'
|
6
|
+
require 'awesome_print'
|
7
|
+
|
8
|
+
module Headdesk
|
9
|
+
#
|
10
|
+
# headdesk CLI
|
11
|
+
#
|
12
|
+
# :reek:TooManyStatements
|
13
|
+
class CLI < Thor
|
14
|
+
desc 'unpack FILE [DESTINATION]', 'Unpack an APK or IPA to [DESTINATION] or to the current directory'
|
15
|
+
method_option :analize, type: :boolean, aliases: '-a'
|
16
|
+
def unpack(file, destination = nil)
|
17
|
+
# Make sure the input file exists
|
18
|
+
unless File.exist?(file)
|
19
|
+
STDERR.puts "Could not find: #{file}"
|
20
|
+
CLI.command_help(Thor::Base.shell.new, 'unpack')
|
21
|
+
return 1
|
22
|
+
end
|
23
|
+
|
24
|
+
# Make sure destination exists, if specified
|
25
|
+
unless !destination || Dir.exist?(destination)
|
26
|
+
STDERR.puts "Could not find destination path: #{destination}"
|
27
|
+
CLI.command_help(Thor::Base.shell.new, 'unpack')
|
28
|
+
return 1
|
29
|
+
end
|
30
|
+
|
31
|
+
begin
|
32
|
+
stdout = nil
|
33
|
+
output_path = destination
|
34
|
+
|
35
|
+
if !destination
|
36
|
+
# Output to tempdir, then copy to cwd if no destination specified
|
37
|
+
Dir.mktmpdir do |tmp_dir|
|
38
|
+
output_path = tmp_dir
|
39
|
+
stdout = Headdesk::ApkTool.unpack_to(file, tmp_dir)
|
40
|
+
FileUtils.cp_r("#{tmp_dir}/.", Dir.pwd)
|
41
|
+
end
|
42
|
+
else
|
43
|
+
stdout = Headdesk::ApkTool.unpack_to(file, destination)
|
44
|
+
end
|
45
|
+
|
46
|
+
# Analize if requested
|
47
|
+
Headdesk::Analize.at(output_path) if options[:analize]
|
48
|
+
rescue CliError => cli_err
|
49
|
+
STDERR.puts cli_err.message
|
50
|
+
CLI.command_help(Thor::Base.shell.new, 'unpack')
|
51
|
+
return 1
|
52
|
+
rescue StandardError => rb_err
|
53
|
+
STDERR.puts err.message.red
|
54
|
+
STDERR.puts err.backtrace.ai
|
55
|
+
return 1
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
desc 'analize [FILE]', 'Analize an APK or IPA'
|
60
|
+
method_option :path, type: :string
|
61
|
+
method_option :json, type: :boolean
|
62
|
+
def analize(file = nil)
|
63
|
+
# Make sure input file exsts, if specified
|
64
|
+
unless !file || File.exist?(file)
|
65
|
+
STDERR.puts "Could not find input file: #{file}"
|
66
|
+
CLI.command_help(Thor::Base.shell.new, 'analize')
|
67
|
+
return 1
|
68
|
+
end
|
69
|
+
|
70
|
+
# Unpack APK if needed
|
71
|
+
path = options[:path]
|
72
|
+
tmp_dir = nil
|
73
|
+
if file
|
74
|
+
path = tmp_dir = Dir.mktmpdir
|
75
|
+
Headdesk::ApkTool.unpack_to(file, tmp_dir)
|
76
|
+
end
|
77
|
+
|
78
|
+
# Make sure path exists
|
79
|
+
unless Dir.exist?(path)
|
80
|
+
STDERR.puts "Could not find path: #{path}"
|
81
|
+
CLI.command_help(Thor::Base.shell.new, 'analize')
|
82
|
+
return 1
|
83
|
+
end
|
84
|
+
|
85
|
+
# Analize
|
86
|
+
begin
|
87
|
+
report = Headdesk::Analize.at(path)
|
88
|
+
|
89
|
+
if options[:json]
|
90
|
+
STDOUT.puts report.to_json
|
91
|
+
else
|
92
|
+
STDOUT.puts report.to_s
|
93
|
+
end
|
94
|
+
rescue CliError => cli_err
|
95
|
+
STDERR.puts cli_err.message
|
96
|
+
CLI.command_help(Thor::Base.shell.new, 'analize')
|
97
|
+
return 1
|
98
|
+
rescue StandardError => err
|
99
|
+
STDERR.puts err.message.red
|
100
|
+
STDERR.puts err.backtrace.ai
|
101
|
+
return 1
|
102
|
+
end
|
103
|
+
ensure
|
104
|
+
FileUtils.remove_entry(tmp_dir) if tmp_dir
|
105
|
+
end
|
106
|
+
end
|
107
|
+
end
|
@@ -0,0 +1,281 @@
|
|
1
|
+
---
|
2
|
+
- :major: 4
|
3
|
+
:minor: 38
|
4
|
+
:patch: 1
|
5
|
+
:version: 4.38.1
|
6
|
+
:date: 2018-11-01
|
7
|
+
- :major: 4
|
8
|
+
:minor: 38
|
9
|
+
:patch: 0
|
10
|
+
:version: 4.38.0
|
11
|
+
:date: 2018-10-23
|
12
|
+
- :major: 4
|
13
|
+
:minor: 37
|
14
|
+
:patch: 0
|
15
|
+
:version: 4.37.0
|
16
|
+
:date: 2018-09-27
|
17
|
+
- :major: 4
|
18
|
+
:minor: 36
|
19
|
+
:patch: 1
|
20
|
+
:version: 4.36.1
|
21
|
+
:date: 2018-09-17
|
22
|
+
- :major: 4
|
23
|
+
:minor: 36
|
24
|
+
:patch: 0
|
25
|
+
:version: 4.36.0
|
26
|
+
:date: 2018-08-29
|
27
|
+
- :major: 4
|
28
|
+
:minor: 35
|
29
|
+
:patch: 0
|
30
|
+
:version: 4.35.0
|
31
|
+
:date: 2018-07-26
|
32
|
+
- :major: 4
|
33
|
+
:minor: 34
|
34
|
+
:patch: 0
|
35
|
+
:version: 4.34.0
|
36
|
+
:date: 2018-06-18
|
37
|
+
- :major: 4
|
38
|
+
:minor: 33
|
39
|
+
:patch: 0
|
40
|
+
:version: 4.33.0
|
41
|
+
:date: 2018-05-01
|
42
|
+
- :major: 4
|
43
|
+
:minor: 32
|
44
|
+
:patch: 0
|
45
|
+
:version: 4.32.0
|
46
|
+
:date: 2018-04-11
|
47
|
+
- :major: 4
|
48
|
+
:minor: 31
|
49
|
+
:patch: 0
|
50
|
+
:version: 4.31.0
|
51
|
+
:date: 2018-02-28
|
52
|
+
- :major: 4
|
53
|
+
:minor: 30
|
54
|
+
:patch: 0
|
55
|
+
:version: 4.30.0
|
56
|
+
:date: 2018-01-24
|
57
|
+
- :major: 4
|
58
|
+
:minor: 29
|
59
|
+
:patch: 0
|
60
|
+
:version: 4.29.0
|
61
|
+
:date: 2017-12-05
|
62
|
+
- :major: 4
|
63
|
+
:minor: 28
|
64
|
+
:patch: 0
|
65
|
+
:version: 4.28.0
|
66
|
+
:date: 2017-11-07
|
67
|
+
- :major: 4
|
68
|
+
:minor: 27
|
69
|
+
:patch: 0
|
70
|
+
:version: 4.27.0
|
71
|
+
:date: 2017-09-26
|
72
|
+
- :major: 4
|
73
|
+
:minor: 26
|
74
|
+
:patch: 0
|
75
|
+
:version: 4.26.0
|
76
|
+
:date: 2017-08-24
|
77
|
+
- :major: 4
|
78
|
+
:minor: 25
|
79
|
+
:patch: 0
|
80
|
+
:version: 4.25.0
|
81
|
+
:date: 2017-07-26
|
82
|
+
- :major: 4
|
83
|
+
:minor: 24
|
84
|
+
:patch: 0
|
85
|
+
:version: 4.24.0
|
86
|
+
:date: 2017-06-26
|
87
|
+
- :major: 4
|
88
|
+
:minor: 23
|
89
|
+
:patch: 0
|
90
|
+
:version: 4.23.0
|
91
|
+
:date: 2017-05-25
|
92
|
+
- :major: 4
|
93
|
+
:minor: 22
|
94
|
+
:patch: 1
|
95
|
+
:version: 4.22.1
|
96
|
+
:date: 2017-05-11
|
97
|
+
- :major: 4
|
98
|
+
:minor: 22
|
99
|
+
:patch: 0
|
100
|
+
:version: 4.22.0
|
101
|
+
:date: 2017-04-18
|
102
|
+
- :major: 4
|
103
|
+
:minor: 21
|
104
|
+
:patch: 1
|
105
|
+
:version: 4.21.1
|
106
|
+
:date: 2017-04-06
|
107
|
+
- :major: 4
|
108
|
+
:minor: 21
|
109
|
+
:patch: 0
|
110
|
+
:version: 4.21.0
|
111
|
+
:date: 2017-04-04
|
112
|
+
- :major: 4
|
113
|
+
:minor: 20
|
114
|
+
:patch: 0
|
115
|
+
:version: 4.20.0
|
116
|
+
:date: 2017-03-01
|
117
|
+
- :major: 4
|
118
|
+
:minor: 19
|
119
|
+
:patch: 0
|
120
|
+
:version: 4.19.0
|
121
|
+
:date: 2017-01-25
|
122
|
+
- :major: 4
|
123
|
+
:minor: 18
|
124
|
+
:patch: 0
|
125
|
+
:version: 4.18.0
|
126
|
+
:date: 2016-11-30
|
127
|
+
- :major: 4
|
128
|
+
:minor: 17
|
129
|
+
:patch: 0
|
130
|
+
:version: 4.17.0
|
131
|
+
:date: 2016-10-26
|
132
|
+
- :major: 4
|
133
|
+
:minor: 16
|
134
|
+
:patch: 1
|
135
|
+
:version: 4.16.1
|
136
|
+
:date: 2016-10-07
|
137
|
+
- :major: 4
|
138
|
+
:minor: 16
|
139
|
+
:patch: 0
|
140
|
+
:version: 4.16.0
|
141
|
+
:date: 2016-09-27
|
142
|
+
- :major: 4
|
143
|
+
:minor: 15
|
144
|
+
:patch: 0
|
145
|
+
:version: 4.15.0
|
146
|
+
:date: 2016-08-23
|
147
|
+
- :major: 4
|
148
|
+
:minor: 14
|
149
|
+
:patch: 1
|
150
|
+
:version: 4.14.1
|
151
|
+
:date: 2016-08-04
|
152
|
+
- :major: 4
|
153
|
+
:minor: 14
|
154
|
+
:patch: 0
|
155
|
+
:version: 4.14.0
|
156
|
+
:date: 2016-07-13
|
157
|
+
- :major: 4
|
158
|
+
:minor: 13
|
159
|
+
:patch: 2
|
160
|
+
:version: 4.13.2
|
161
|
+
:date: 2016-07-01
|
162
|
+
- :major: 4
|
163
|
+
:minor: 13
|
164
|
+
:patch: 1
|
165
|
+
:version: 4.13.1
|
166
|
+
:date: 2016-06-17
|
167
|
+
- :major: 4
|
168
|
+
:minor: 13
|
169
|
+
:patch: 0
|
170
|
+
:version: 4.13.0
|
171
|
+
:date: 2016-06-15
|
172
|
+
- :major: 4
|
173
|
+
:minor: 12
|
174
|
+
:patch: 1
|
175
|
+
:version: 4.12.1
|
176
|
+
:date: 2016-05-26
|
177
|
+
- :major: 4
|
178
|
+
:minor: 12
|
179
|
+
:patch: 0
|
180
|
+
:version: 4.12.0
|
181
|
+
:date: 2016-05-20
|
182
|
+
- :major: 4
|
183
|
+
:minor: 11
|
184
|
+
:patch: 0
|
185
|
+
:version: 4.11.0
|
186
|
+
:date: 2016-04-12
|
187
|
+
- :major: 4
|
188
|
+
:minor: 10
|
189
|
+
:patch: 1
|
190
|
+
:version: 4.10.1
|
191
|
+
:date: 2016-03-18
|
192
|
+
- :major: 4
|
193
|
+
:minor: 10
|
194
|
+
:patch: 0
|
195
|
+
:version: 4.10.0
|
196
|
+
:date: 2016-02-10
|
197
|
+
- :major: 4
|
198
|
+
:minor: 9
|
199
|
+
:patch: 0
|
200
|
+
:version: 4.9.0
|
201
|
+
:date: 2016-01-13
|
202
|
+
- :major: 4
|
203
|
+
:minor: 8
|
204
|
+
:patch: 2
|
205
|
+
:version: 4.8.2
|
206
|
+
:date: 2015-11-23
|
207
|
+
- :major: 4
|
208
|
+
:minor: 8
|
209
|
+
:patch: 1
|
210
|
+
:version: 4.8.1
|
211
|
+
:date: 2015-11-11
|
212
|
+
- :major: 4
|
213
|
+
:minor: 8
|
214
|
+
:patch: 0
|
215
|
+
:version: 4.8.0
|
216
|
+
:date: 2015-11-11
|
217
|
+
- :major: 4
|
218
|
+
:minor: 7
|
219
|
+
:patch: 0
|
220
|
+
:version: 4.7.0
|
221
|
+
:date: 2015-10-07
|
222
|
+
- :major: 4
|
223
|
+
:minor: 6
|
224
|
+
:patch: 0
|
225
|
+
:version: 4.6.0
|
226
|
+
:date: 2015-09-10
|
227
|
+
- :major: 4
|
228
|
+
:minor: 5
|
229
|
+
:patch: 1
|
230
|
+
:version: 4.5.1
|
231
|
+
:date: 2015-08-13
|
232
|
+
- :major: 4
|
233
|
+
:minor: 5
|
234
|
+
:patch: 0
|
235
|
+
:version: 4.5.0
|
236
|
+
:date: 2015-08-10
|
237
|
+
- :major: 4
|
238
|
+
:minor: 4
|
239
|
+
:patch: 1
|
240
|
+
:version: 4.4.1
|
241
|
+
:date: 2015-07-13
|
242
|
+
- :major: 4
|
243
|
+
:minor: 4
|
244
|
+
:patch: 0
|
245
|
+
:version: 4.4.0
|
246
|
+
:date: 2015-07-08
|
247
|
+
- :major: 4
|
248
|
+
:minor: 3
|
249
|
+
:patch: 0
|
250
|
+
:version: 4.3.0
|
251
|
+
:date: 2015-06-25
|
252
|
+
- :major: 4
|
253
|
+
:minor: 2
|
254
|
+
:patch: 0
|
255
|
+
:version: 4.2.0
|
256
|
+
:date: 2015-05-28
|
257
|
+
- :major: 4
|
258
|
+
:minor: 1
|
259
|
+
:patch: 2
|
260
|
+
:version: 4.1.2
|
261
|
+
:date: 2015-05-14
|
262
|
+
- :major: 4
|
263
|
+
:minor: 1
|
264
|
+
:patch: 1
|
265
|
+
:version: 4.1.1
|
266
|
+
:date: 2015-05-06
|
267
|
+
- :major: 4
|
268
|
+
:minor: 1
|
269
|
+
:patch: 0
|
270
|
+
:version: 4.1.0
|
271
|
+
:date: 2015-04-30
|
272
|
+
- :major: 4
|
273
|
+
:minor: 0
|
274
|
+
:patch: 1
|
275
|
+
:version: 4.0.1
|
276
|
+
:date: 2015-04-02
|
277
|
+
- :major: 4
|
278
|
+
:minor: 0
|
279
|
+
:patch: 0
|
280
|
+
:version: 4.0.0
|
281
|
+
:date: 2015-03-25
|