pingpp 1.0.2

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