headdesk 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|