pingpp 1.0.2

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.
@@ -0,0 +1,16 @@
1
+ module Pingpp
2
+ module APIOperations
3
+ module Create
4
+ module ClassMethods
5
+ def create(params={}, api_key=nil)
6
+ response, api_key = Pingpp.request(:post, self.url, api_key, params)
7
+ Util.convert_to_pingpp_object(response, api_key)
8
+ end
9
+ end
10
+
11
+ def self.included(base)
12
+ base.extend(ClassMethods)
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,11 @@
1
+ module Pingpp
2
+ module APIOperations
3
+ module Delete
4
+ def delete(params = {})
5
+ response, api_key = Pingpp.request(:delete, url, @api_key, params)
6
+ refresh_from(response, api_key)
7
+ self
8
+ end
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,16 @@
1
+ module Pingpp
2
+ module APIOperations
3
+ module List
4
+ module ClassMethods
5
+ def all(filters={}, api_key=nil)
6
+ response, api_key = Pingpp.request(:get, url, api_key, filters)
7
+ Util.convert_to_pingpp_object(response, api_key)
8
+ end
9
+ end
10
+
11
+ def self.included(base)
12
+ base.extend(ClassMethods)
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,57 @@
1
+ module Pingpp
2
+ module APIOperations
3
+ module Update
4
+ def save(opts={})
5
+ values = serialize_params(self).merge(opts)
6
+
7
+ if @values[:metadata]
8
+ values[:metadata] = serialize_metadata
9
+ end
10
+
11
+ if values.length > 0
12
+ values.delete(:id)
13
+
14
+ response, api_key = Pingpp.request(:post, url, @api_key, values)
15
+ refresh_from(response, api_key)
16
+ end
17
+ self
18
+ end
19
+
20
+ def serialize_metadata
21
+ if @unsaved_values.include?(:metadata)
22
+ # the metadata object has been reassigned
23
+ # i.e. as object.metadata = {key => val}
24
+ metadata_update = @values[:metadata] # new hash
25
+ new_keys = metadata_update.keys.map(&:to_sym)
26
+ # remove keys at the server, but not known locally
27
+ keys_to_unset = @previous_metadata.keys - new_keys
28
+ keys_to_unset.each {|key| metadata_update[key] = ''}
29
+
30
+ metadata_update
31
+ else
32
+ # metadata is a PingppObject, and can be serialized normally
33
+ serialize_params(@values[:metadata])
34
+ end
35
+ end
36
+
37
+ def serialize_params(obj)
38
+ case obj
39
+ when nil
40
+ ''
41
+ when PingppObject
42
+ unsaved_keys = obj.instance_variable_get(:@unsaved_values)
43
+ obj_values = obj.instance_variable_get(:@values)
44
+ update_hash = {}
45
+
46
+ unsaved_keys.each do |k|
47
+ update_hash[k] = serialize_params(obj_values[k])
48
+ end
49
+
50
+ update_hash
51
+ else
52
+ obj
53
+ end
54
+ end
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,33 @@
1
+ module Pingpp
2
+ class APIResource < PingppObject
3
+ def self.class_name
4
+ self.name.split('::')[-1]
5
+ end
6
+
7
+ def self.url()
8
+ if self == APIResource
9
+ raise NotImplementedError.new('APIResource is an abstract class. You should perform actions on its subclasses (Charge, etc.)')
10
+ end
11
+ "/v1/#{CGI.escape(class_name.downcase)}s"
12
+ end
13
+
14
+ def url
15
+ unless id = self['id']
16
+ raise InvalidRequestError.new("Could not determine which URL to request: #{self.class} instance has invalid ID: #{id.inspect}", 'id')
17
+ end
18
+ "#{self.class.url}/#{CGI.escape(id)}"
19
+ end
20
+
21
+ def refresh
22
+ response, api_key = Pingpp.request(:get, url, @api_key, @retrieve_options)
23
+ refresh_from(response, api_key)
24
+ self
25
+ end
26
+
27
+ def self.retrieve(id, api_key=nil)
28
+ instance = self.new(id, api_key)
29
+ instance.refresh
30
+ instance
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,53 @@
1
+ require 'uri'
2
+ require 'digest/sha1'
3
+
4
+ module Pingpp
5
+ module CertificateBlacklist
6
+
7
+ BLACKLIST = {
8
+ "api.pingplusplus.com" => [
9
+ '05c0b3643694470a888c6e7feb5c9e24e823dc53',
10
+ '5b7dc7fbc98d78bf76d4d4fa6f597a0c901fad5c',
11
+ ]
12
+ }
13
+
14
+ # Preflight the SSL certificate presented by the backend. This isn't 100%
15
+ # bulletproof, in that we're not actually validating the transport used to
16
+ # communicate with Pingpp, merely that the first attempt to does not use a
17
+ # revoked certificate.
18
+
19
+ # Unfortunately the interface to OpenSSL doesn't make it easy to check the
20
+ # certificate before sending potentially sensitive data on the wire. This
21
+ # approach raises the bar for an attacker significantly.
22
+
23
+ def self.check_ssl_cert(uri, ca_file)
24
+ uri = URI.parse(uri)
25
+
26
+ sock = TCPSocket.new(uri.host, uri.port)
27
+ ctx = OpenSSL::SSL::SSLContext.new
28
+ ctx.set_params(:verify_mode => OpenSSL::SSL::VERIFY_PEER,
29
+ :ca_file => ca_file)
30
+
31
+ socket = OpenSSL::SSL::SSLSocket.new(sock, ctx)
32
+ socket.connect
33
+
34
+ certificate = socket.peer_cert.to_der
35
+ fingerprint = Digest::SHA1.hexdigest(certificate)
36
+
37
+ if blacklisted_certs = BLACKLIST[uri.host]
38
+ if blacklisted_certs.include?(fingerprint)
39
+ raise APIConnectionError.new(
40
+ "Invalid server certificate. You tried to connect to a server that" +
41
+ "has a revoked SSL certificate, which means we cannot securely send" +
42
+ "data to that server. Please email support@pingplusplus.com if you need" +
43
+ "help connecting to the correct API server."
44
+ )
45
+ end
46
+ end
47
+
48
+ socket.close
49
+
50
+ return true
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,9 @@
1
+ module Pingpp
2
+ class Channel
3
+ ALIPAY = "alipay"
4
+ WECHAT = "wx"
5
+ UPMP = "upmp"
6
+ ALIPAY_WAP = "alipay_wap"
7
+ UPMP_WAP = "upmp_wap"
8
+ end
9
+ end
@@ -0,0 +1,19 @@
1
+ module Pingpp
2
+ class Charge < APIResource
3
+ include Pingpp::APIOperations::List
4
+ include Pingpp::APIOperations::Create
5
+ include Pingpp::APIOperations::Update
6
+
7
+ def refund(params={})
8
+ response, api_key = Pingpp.request(:post, refund_url, @api_key, params)
9
+ refresh_from(response, api_key)
10
+ self
11
+ end
12
+
13
+ private
14
+
15
+ def refund_url
16
+ url + '/refunds'
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,4 @@
1
+ module Pingpp
2
+ class APIConnectionError < PingppError
3
+ end
4
+ end
@@ -0,0 +1,4 @@
1
+ module Pingpp
2
+ class APIError < PingppError
3
+ end
4
+ end
@@ -0,0 +1,4 @@
1
+ module Pingpp
2
+ class AuthenticationError < PingppError
3
+ end
4
+ end
@@ -0,0 +1,10 @@
1
+ module Pingpp
2
+ class InvalidRequestError < PingppError
3
+ attr_accessor :param
4
+
5
+ def initialize(message, param, http_status=nil, http_body=nil, json_body=nil)
6
+ super(message, http_status, http_body, json_body)
7
+ @param = param
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,20 @@
1
+ module Pingpp
2
+ class PingppError < StandardError
3
+ attr_reader :message
4
+ attr_reader :http_status
5
+ attr_reader :http_body
6
+ attr_reader :json_body
7
+
8
+ def initialize(message=nil, http_status=nil, http_body=nil, json_body=nil)
9
+ @message = message
10
+ @http_status = http_status
11
+ @http_body = http_body
12
+ @json_body = json_body
13
+ end
14
+
15
+ def to_s
16
+ status_string = @http_status.nil? ? "" : "(Status #{@http_status}) "
17
+ "#{status_string}#{@message}"
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,35 @@
1
+ module Pingpp
2
+ class ListObject < PingppObject
3
+
4
+ def [](k)
5
+ case k
6
+ when String, Symbol
7
+ super
8
+ else
9
+ raise ArgumentError.new("You tried to access the #{k.inspect} index, but ListObject types only support String keys. (HINT: List calls return an object with a 'data' (which is the data array). You likely want to call #data[#{k.inspect}])")
10
+ end
11
+ end
12
+
13
+ def each(&blk)
14
+ self.data.each(&blk)
15
+ end
16
+
17
+ def retrieve(id, api_key=nil)
18
+ api_key ||= @api_key
19
+ response, api_key = Pingpp.request(:get,"#{url}/#{CGI.escape(id)}", api_key)
20
+ Util.convert_to_pingpp_object(response, api_key)
21
+ end
22
+
23
+ def create(params={}, api_key=nil)
24
+ api_key ||= @api_key
25
+ response, api_key = Pingpp.request(:post, url, api_key, params)
26
+ Util.convert_to_pingpp_object(response, api_key)
27
+ end
28
+
29
+ def all(params={}, api_key=nil)
30
+ api_key ||= @api_key
31
+ response, api_key = Pingpp.request(:get, url, api_key, params)
32
+ Util.convert_to_pingpp_object(response, api_key)
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,187 @@
1
+ module Pingpp
2
+ class PingppObject
3
+ include Enumerable
4
+
5
+ attr_accessor :api_key
6
+ @@permanent_attributes = Set.new([:api_key, :id])
7
+
8
+ # The default :id method is deprecated and isn't useful to us
9
+ if method_defined?(:id)
10
+ undef :id
11
+ end
12
+
13
+ def initialize(id=nil, api_key=nil)
14
+ # parameter overloading!
15
+ if id.kind_of?(Hash)
16
+ @retrieve_options = id.dup
17
+ @retrieve_options.delete(:id)
18
+ id = id[:id]
19
+ else
20
+ @retrieve_options = {}
21
+ end
22
+
23
+ @api_key = api_key
24
+ @values = {}
25
+ # This really belongs in APIResource, but not putting it there allows us
26
+ # to have a unified inspect method
27
+ @unsaved_values = Set.new
28
+ @transient_values = Set.new
29
+ @values[:id] = id if id
30
+ end
31
+
32
+ def self.construct_from(values, api_key=nil)
33
+ obj = self.new(values[:id], api_key)
34
+ obj.refresh_from(values, api_key)
35
+ obj
36
+ end
37
+
38
+ def to_s(*args)
39
+ JSON.pretty_generate(@values)
40
+ end
41
+
42
+ def inspect()
43
+ id_string = (self.respond_to?(:id) && !self.id.nil?) ? " id=#{self.id}" : ""
44
+ "#<#{self.class}:0x#{self.object_id.to_s(16)}#{id_string}> JSON: " + JSON.pretty_generate(@values)
45
+ end
46
+
47
+ def refresh_from(values, api_key, partial=false)
48
+ @api_key = api_key
49
+
50
+ @previous_metadata = values[:metadata]
51
+ removed = partial ? Set.new : Set.new(@values.keys - values.keys)
52
+ added = Set.new(values.keys - @values.keys)
53
+
54
+ instance_eval do
55
+ remove_accessors(removed)
56
+ add_accessors(added)
57
+ end
58
+ removed.each do |k|
59
+ @values.delete(k)
60
+ @transient_values.add(k)
61
+ @unsaved_values.delete(k)
62
+ end
63
+ values.each do |k, v|
64
+ @values[k] = Util.convert_to_pingpp_object(v, api_key)
65
+ @transient_values.delete(k)
66
+ @unsaved_values.delete(k)
67
+ end
68
+ end
69
+
70
+ def [](k)
71
+ @values[k.to_sym]
72
+ end
73
+
74
+ def []=(k, v)
75
+ send(:"#{k}=", v)
76
+ end
77
+
78
+ def keys
79
+ @values.keys
80
+ end
81
+
82
+ def values
83
+ @values.values
84
+ end
85
+
86
+ def to_json(*a)
87
+ JSON.generate(@values)
88
+ end
89
+
90
+ def as_json(*a)
91
+ @values.as_json(*a)
92
+ end
93
+
94
+ def to_hash
95
+ @values.inject({}) do |acc, (key, value)|
96
+ acc[key] = value.respond_to?(:to_hash) ? value.to_hash : value
97
+ acc
98
+ end
99
+ end
100
+
101
+ def each(&blk)
102
+ @values.each(&blk)
103
+ end
104
+
105
+ def _dump(level)
106
+ Marshal.dump([@values, @api_key])
107
+ end
108
+
109
+ def self._load(args)
110
+ values, api_key = Marshal.load(args)
111
+ construct_from(values, api_key)
112
+ end
113
+
114
+ if RUBY_VERSION < '1.9.2'
115
+ def respond_to?(symbol)
116
+ @values.has_key?(symbol) || super
117
+ end
118
+ end
119
+
120
+ protected
121
+
122
+ def metaclass
123
+ class << self; self; end
124
+ end
125
+
126
+ def remove_accessors(keys)
127
+ metaclass.instance_eval do
128
+ keys.each do |k|
129
+ next if @@permanent_attributes.include?(k)
130
+ k_eq = :"#{k}="
131
+ remove_method(k) if method_defined?(k)
132
+ remove_method(k_eq) if method_defined?(k_eq)
133
+ end
134
+ end
135
+ end
136
+
137
+ def add_accessors(keys)
138
+ metaclass.instance_eval do
139
+ keys.each do |k|
140
+ next if @@permanent_attributes.include?(k)
141
+ k_eq = :"#{k}="
142
+ define_method(k) { @values[k] }
143
+ define_method(k_eq) do |v|
144
+ if v == ""
145
+ raise ArgumentError.new(
146
+ "You cannot set #{k} to an empty string." +
147
+ "We interpret empty strings as nil in requests." +
148
+ "You may set #{self}.#{k} = nil to delete the property.")
149
+ end
150
+ @values[k] = v
151
+ @unsaved_values.add(k)
152
+ end
153
+ end
154
+ end
155
+ end
156
+
157
+ def method_missing(name, *args)
158
+ # TODO: only allow setting in updateable classes.
159
+ if name.to_s.end_with?('=')
160
+ attr = name.to_s[0...-1].to_sym
161
+ add_accessors([attr])
162
+ begin
163
+ mth = method(name)
164
+ rescue NameError
165
+ raise NoMethodError.new("Cannot set #{attr} on this object. HINT: you can't set: #{@@permanent_attributes.to_a.join(', ')}")
166
+ end
167
+ return mth.call(args[0])
168
+ else
169
+ return @values[name] if @values.has_key?(name)
170
+ end
171
+
172
+ begin
173
+ super
174
+ rescue NoMethodError => e
175
+ if @transient_values.include?(name)
176
+ raise NoMethodError.new(e.message + ". HINT: The '#{name}' attribute was set in the past, however. It was then wiped when refreshing the object with the result returned by Pingpp's API, probably as a result of a save(). The attributes currently available on this object are: #{@values.keys.join(', ')}")
177
+ else
178
+ raise
179
+ end
180
+ end
181
+ end
182
+
183
+ def respond_to_missing?(symbol, include_private = false)
184
+ @values && @values.has_key?(symbol) || super
185
+ end
186
+ end
187
+ end
@@ -0,0 +1,14 @@
1
+ module Pingpp
2
+ class Refund < APIResource
3
+ include Pingpp::APIOperations::Update
4
+ include Pingpp::APIOperations::List
5
+
6
+ def url
7
+ "#{Charge.url}/#{CGI.escape(charge)}/refunds/#{CGI.escape(id)}"
8
+ end
9
+
10
+ def self.retrieve(id, api_key=nil)
11
+ raise NotImplementedError.new("Refunds cannot be retrieved without a charge ID. Retrieve a refund using charge.refunds.retrieve('refund_id')")
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,20 @@
1
+ module Pingpp
2
+ class SingletonAPIResource < APIResource
3
+ def self.url()
4
+ if self == SingletonAPIResource
5
+ raise NotImplementedError.new('SingletonAPIResource is an abstract class. You should perform actions on its subclasses (Account, etc.)')
6
+ end
7
+ "/v1/#{CGI.escape(class_name.downcase)}"
8
+ end
9
+
10
+ def url
11
+ self.class.url
12
+ end
13
+
14
+ def self.retrieve(api_key=nil)
15
+ instance = self.new(nil, api_key)
16
+ instance.refresh
17
+ instance
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,99 @@
1
+ module Pingpp
2
+ module Util
3
+ def self.objects_to_ids(h)
4
+ case h
5
+ when APIResource
6
+ h.id
7
+ when Hash
8
+ res = {}
9
+ h.each { |k, v| res[k] = objects_to_ids(v) unless v.nil? }
10
+ res
11
+ when Array
12
+ h.map { |v| objects_to_ids(v) }
13
+ else
14
+ h
15
+ end
16
+ end
17
+
18
+ def self.object_classes
19
+ @object_classes ||= {
20
+ 'charge' => Charge,
21
+ 'list' => ListObject,
22
+ }
23
+ end
24
+
25
+ def self.convert_to_pingpp_object(resp, api_key)
26
+ case resp
27
+ when Array
28
+ resp.map { |i| convert_to_pingpp_object(i, api_key) }
29
+ when Hash
30
+ # Try converting to a known object class. If none available, fall back to generic PingppObject
31
+ object_classes.fetch(resp[:object], PingppObject).construct_from(resp, api_key)
32
+ else
33
+ resp
34
+ end
35
+ end
36
+
37
+ def self.file_readable(file)
38
+ # This is nominally equivalent to File.readable?, but that can
39
+ # report incorrect results on some more oddball filesystems
40
+ # (such as AFS)
41
+ begin
42
+ File.open(file) { |f| }
43
+ rescue
44
+ false
45
+ else
46
+ true
47
+ end
48
+ end
49
+
50
+ def self.symbolize_names(object)
51
+ case object
52
+ when Hash
53
+ new = {}
54
+ object.each do |key, value|
55
+ key = (key.to_sym rescue key) || key
56
+ new[key] = symbolize_names(value)
57
+ end
58
+ new
59
+ when Array
60
+ object.map { |value| symbolize_names(value) }
61
+ else
62
+ object
63
+ end
64
+ end
65
+
66
+ def self.url_encode(key)
67
+ URI.escape(key.to_s, Regexp.new("[^#{URI::PATTERN::UNRESERVED}]"))
68
+ end
69
+
70
+ def self.flatten_params(params, parent_key=nil)
71
+ result = []
72
+ params.each do |key, value|
73
+ calculated_key = parent_key ? "#{parent_key}[#{url_encode(key)}]" : url_encode(key)
74
+ if value.is_a?(Hash)
75
+ result += flatten_params(value, calculated_key)
76
+ elsif value.is_a?(Array)
77
+ result += flatten_params_array(value, calculated_key)
78
+ else
79
+ result << [calculated_key, value]
80
+ end
81
+ end
82
+ result
83
+ end
84
+
85
+ def self.flatten_params_array(value, calculated_key)
86
+ result = []
87
+ value.each do |elem|
88
+ if elem.is_a?(Hash)
89
+ result += flatten_params(elem, calculated_key)
90
+ elsif elem.is_a?(Array)
91
+ result += flatten_params_array(elem, calculated_key)
92
+ else
93
+ result << ["#{calculated_key}[]", elem]
94
+ end
95
+ end
96
+ result
97
+ end
98
+ end
99
+ end
@@ -0,0 +1,3 @@
1
+ module Pingpp
2
+ VERSION = '1.0.2'
3
+ end