httparty 0.16.3 → 0.16.4

Sign up to get free protection for your applications and to get access to all the features.

Potentially problematic release.


This version of httparty might be problematic. Click here for more details.

checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: d2efd166a0534991fe03c8f0ae6a72424c9e8c3d
4
- data.tar.gz: 41ec81b8fbd9ae18ae51f8b93ce0a36e6b1174c8
3
+ metadata.gz: d8ea06db7242bc4bea719ac28217cb7a03643277
4
+ data.tar.gz: 3341968702712ab91fc1348f000337afe1bf4047
5
5
  SHA512:
6
- metadata.gz: 53473b5f6b517e590cf7cd5937dee78c6493226ad6508865e9e57efe844b62218473e7137ea83a902de113221f53e0626f163eea39ac5d1094a65a7253e6a7ff
7
- data.tar.gz: 4490aaf02f3bbf8eee7568bf62e37ea0c1d23caef64338b1dd09688efa8fe1b3b9047c3891d6e822f88b61b16ad9aa27618437d307b722d8b52e90f9016db8a9
6
+ metadata.gz: bfd0717ef9adb117bc7568a35648b0bc295d0f92210696a423c268be80b7ef12d06d84d2d94ed33a24d31367fd03c3fae3cabc6afbc5cc5b0446f15f01feff79
7
+ data.tar.gz: 853d4aa140b4717e36ae0baeeb6c1ba113567242398abd56a944bcf29543f74e3a0a3430c0f8823b9fe9c00d8a4bdb2c3a3ff53c59cf9263e080ac7002013f4e
@@ -2,9 +2,10 @@ language: ruby
2
2
  rvm:
3
3
  - 2.0.0
4
4
  - 2.1.10
5
- - 2.2.9
6
- - 2.3.6
7
- - 2.4.3
8
- - 2.5.0
5
+ - 2.2.10
6
+ - 2.3.8
7
+ - 2.4.5
8
+ - 2.5.3
9
+ - 2.6.0
9
10
  bundler_args: --without development
10
- before_install: gem install bundler
11
+ before_install: gem install bundler -v '< 2'
@@ -1,4 +1,10 @@
1
1
 
2
+ ## 0.16.4
3
+ * [Add support for Ruby 2.6](https://github.com/jnunemaker/httparty/pull/636)
4
+ * [Fix a few multipart issues](https://github.com/jnunemaker/httparty/pull/626)
5
+ * [Improve a memory usage for https requests](https://github.com/jnunemaker/httparty/pull/625)
6
+ * [Add response code to streamed body](https://github.com/jnunemaker/httparty/pull/588)
7
+
2
8
  ## 0.16.3
3
9
  * [Add Logstash-compatible formatter](https://github.com/jnunemaker/httparty/pull/612)
4
10
  * [Add support for headers specified with symbols](https://github.com/jnunemaker/httparty/pull/622)
@@ -9,8 +9,14 @@ url = "https://cdn.kernel.org/pub/linux/kernel/v4.x/#{filename}"
9
9
 
10
10
  File.open(filename, "w") do |file|
11
11
  response = HTTParty.get(url, stream_body: true) do |fragment|
12
- print "."
13
- file.write(fragment)
12
+ if [301, 302].include?(fragment.code)
13
+ print "skip writing for redirect"
14
+ elsif fragment.code == 200
15
+ print "."
16
+ file.write(fragment)
17
+ else
18
+ raise StandardError, "Non-success status code while streaming #{fragment.code}"
19
+ end
14
20
  end
15
21
  end
16
22
  puts
@@ -574,8 +574,11 @@ module HTTParty
574
574
  end
575
575
 
576
576
  def process_headers(options)
577
- if options[:headers] && headers.any?
578
- options[:headers] = headers.merge(options[:headers])
577
+ if options[:headers]
578
+ if headers.any?
579
+ options[:headers] = headers.merge(options[:headers])
580
+ end
581
+
579
582
  options[:headers] = Utils.stringify_keys(process_dynamic_headers(options[:headers]))
580
583
  end
581
584
  end
@@ -77,6 +77,12 @@ module HTTParty
77
77
  new(uri, options).connection
78
78
  end
79
79
 
80
+ def self.default_cert_store
81
+ @default_cert_store ||= OpenSSL::X509::Store.new.tap do |cert_store|
82
+ cert_store.set_default_paths
83
+ end
84
+ end
85
+
80
86
  attr_reader :uri, :options
81
87
 
82
88
  def initialize(uri, options = {})
@@ -176,8 +182,7 @@ module HTTParty
176
182
  http.cert_store = options[:cert_store]
177
183
  else
178
184
  # Use the default cert store by default, i.e. system ca certs
179
- http.cert_store = OpenSSL::X509::Store.new
180
- http.cert_store.set_default_paths
185
+ http.cert_store = self.class.default_cert_store
181
186
  end
182
187
  else
183
188
  http.verify_mode = OpenSSL::SSL::VERIFY_NONE
@@ -0,0 +1,20 @@
1
+ require 'delegate'
2
+
3
+ module HTTParty
4
+ # Allow access to http_response and code by delegation on fragment
5
+ class FragmentWithResponse < SimpleDelegator
6
+ extend Forwardable
7
+
8
+ attr_reader :http_response
9
+
10
+ def code
11
+ @http_response.code.to_i
12
+ end
13
+
14
+ def initialize(fragment, http_response)
15
+ @fragment = fragment
16
+ @http_response = http_response
17
+ super fragment
18
+ end
19
+ end
20
+ end
@@ -1,5 +1,6 @@
1
1
  require 'erb'
2
2
  require 'httparty/request/body'
3
+ require 'httparty/fragment_with_response'
3
4
 
4
5
  module HTTParty
5
6
  class Request #:nodoc:
@@ -148,7 +149,7 @@ module HTTParty
148
149
 
149
150
  http_response.read_body do |fragment|
150
151
  chunks << fragment unless options[:stream_body]
151
- block.call(fragment)
152
+ block.call FragmentWithResponse.new(fragment, http_response)
152
153
  end
153
154
 
154
155
  chunked_body = chunks.join
@@ -22,7 +22,7 @@ module HTTParty
22
22
  end
23
23
 
24
24
  def multipart?
25
- params.respond_to?(:to_hash) && (force_multipart || has_file?(params.to_hash))
25
+ params.respond_to?(:to_hash) && (force_multipart || has_file?(params))
26
26
  end
27
27
 
28
28
  private
@@ -46,24 +46,18 @@ module HTTParty
46
46
  multipart += "--#{boundary}--\r\n"
47
47
  end
48
48
 
49
- def has_file?(hash)
50
- hash.detect do |key, value|
51
- if value.respond_to?(:to_hash) || includes_hash?(value)
52
- has_file?(value)
53
- elsif value.respond_to?(:to_ary)
54
- value.any? { |e| file?(e) }
55
- else
56
- file?(value)
57
- end
49
+ def has_file?(value)
50
+ if value.respond_to?(:to_hash)
51
+ value.to_hash.any? { |_, v| has_file?(v) }
52
+ elsif value.respond_to?(:to_ary)
53
+ value.to_ary.any? { |v| has_file?(v) }
54
+ else
55
+ file?(value)
58
56
  end
59
57
  end
60
58
 
61
59
  def file?(object)
62
- object.respond_to?(:path) && object.respond_to?(:read) # add memoization
63
- end
64
-
65
- def includes_hash?(object)
66
- object.respond_to?(:to_ary) && object.any? { |e| e.respond_to?(:hash) }
60
+ object.respond_to?(:path) && object.respond_to?(:read)
67
61
  end
68
62
 
69
63
  def normalize_query(query)
@@ -65,6 +65,15 @@ module HTTParty
65
65
  alias_method :multiple_choice?, :multiple_choices?
66
66
  end
67
67
 
68
+ # Support old status codes method from pre 2.6.0 era.
69
+ if ::RUBY_VERSION >= "2.6.0" && ::RUBY_PLATFORM != "java"
70
+ alias_method :gateway_time_out?, :gateway_timeout?
71
+ alias_method :request_entity_too_large?, :payload_too_large?
72
+ alias_method :request_time_out?, :request_timeout?
73
+ alias_method :request_uri_too_long?, :uri_too_long?
74
+ alias_method :requested_range_not_satisfiable?, :range_not_satisfiable?
75
+ end
76
+
68
77
  def nil?
69
78
  response.nil? || response.body.nil? || response.body.empty?
70
79
  end
@@ -1,3 +1,3 @@
1
1
  module HTTParty
2
- VERSION = "0.16.3"
2
+ VERSION = "0.16.4"
3
3
  end
@@ -0,0 +1,10 @@
1
+ <!doctype html>
2
+ <html>
3
+ <head>
4
+ <title>Example HTML</title>
5
+ </head>
6
+
7
+ <body>
8
+ <h1>Example</h1>
9
+ </body>
10
+ </html>
@@ -12,7 +12,7 @@ mkdir generated
12
12
  openssl req -batch -subj '/CN=INSECURE Test Certificate Authority' -newkey rsa:1024 -new -x509 -days 999999 -keyout generated/ca.key -nodes -out generated/ca.crt
13
13
 
14
14
  # Create symlinks for ssl_ca_path
15
- c_rehash generated
15
+ openssl generated
16
16
 
17
17
  # Generate the server private key and self-signed certificate
18
18
  openssl req -batch -subj '/CN=localhost' -newkey rsa:1024 -new -x509 -days 999999 -keyout generated/server.key -nodes -out generated/selfsigned.crt
@@ -0,0 +1,14 @@
1
+ require File.expand_path(File.join(File.dirname(__FILE__), '../spec_helper'))
2
+
3
+ RSpec.describe HTTParty::FragmentWithResponse do
4
+ it "access to fragment" do
5
+ fragment = HTTParty::FragmentWithResponse.new("chunk", nil)
6
+ expect(fragment).to eq("chunk")
7
+ end
8
+ it "has access to delegators" do
9
+ response = double(code: '200')
10
+ fragment = HTTParty::FragmentWithResponse.new("chunk", response)
11
+ expect(fragment.code).to eq(200)
12
+ expect(fragment.http_response).to eq response
13
+ end
14
+ end
@@ -110,7 +110,7 @@ RSpec.describe HTTParty::Logger::CurlFormatter do
110
110
 
111
111
  subject = described_class.new(logger_double, :info)
112
112
 
113
- stub_http_response_with("google.html")
113
+ stub_http_response_with("example.html")
114
114
 
115
115
  response = HTTParty::Request.new.perform
116
116
  subject.format(response.request, response)
@@ -106,4 +106,60 @@ RSpec.describe HTTParty::Request::Body do
106
106
  end
107
107
  end
108
108
  end
109
+
110
+ describe '#multipart?' do
111
+ let(:force_multipart) { false }
112
+ let(:file) { File.open('spec/fixtures/tiny.gif') }
113
+
114
+ subject { described_class.new(params, force_multipart: force_multipart).multipart? }
115
+
116
+ context 'when params does not respond to to_hash' do
117
+ let(:params) { 'name=Bob%20Jones' }
118
+
119
+ it { is_expected.to be false }
120
+ end
121
+
122
+ context 'when params responds to to_hash' do
123
+ class HashLike
124
+ def initialize(hash)
125
+ @hash = hash
126
+ end
127
+
128
+ def to_hash
129
+ @hash
130
+ end
131
+ end
132
+
133
+ class ArrayLike
134
+ def initialize(ary)
135
+ @ary = ary
136
+ end
137
+
138
+ def to_ary
139
+ @ary
140
+ end
141
+ end
142
+
143
+ context 'when force_multipart is true' do
144
+ let(:params) { { name: 'Bob Jones' } }
145
+ let(:force_multipart) { true }
146
+
147
+ it { is_expected.to be true }
148
+ end
149
+
150
+ context 'when it does not contain a file' do
151
+ let(:hash_like_param) { HashLike.new(first: 'Bob', last: ArrayLike.new(['Jones'])) }
152
+ let(:params) { { name: ArrayLike.new([hash_like_param]) } }
153
+
154
+ it { is_expected.to eq false }
155
+ end
156
+
157
+ context 'when it contains file' do
158
+ let(:hash_like_param) { HashLike.new(first: 'Bob', last: 'Jones', file: ArrayLike.new([file])) }
159
+ let(:params) { { name: ArrayLike.new([hash_like_param]) } }
160
+
161
+ it { is_expected.to be true }
162
+ end
163
+ end
164
+ end
109
165
  end
@@ -282,6 +282,15 @@ RSpec.describe HTTParty::Response do
282
282
  SPECIFIC_CODES[:multiple_choices?] = Net::HTTPMultipleChoices
283
283
  end
284
284
 
285
+ # Ruby 2.6, those status codes have been updated.
286
+ if RUBY_VERSION >= "2.6.0" && ::RUBY_PLATFORM != "java"
287
+ SPECIFIC_CODES[:gateway_timeout?] = Net::HTTPGatewayTimeout
288
+ SPECIFIC_CODES[:payload_too_large?] = Net::HTTPPayloadTooLarge
289
+ SPECIFIC_CODES[:request_timeout?] = Net::HTTPRequestTimeout
290
+ SPECIFIC_CODES[:uri_too_long?] = Net::HTTPURITooLong
291
+ SPECIFIC_CODES[:range_not_satisfiable?] = Net::HTTPRangeNotSatisfiable
292
+ end
293
+
285
294
  SPECIFIC_CODES.each do |method, klass|
286
295
  it "responds to #{method}" do
287
296
  net_response = response_mock(klass)
@@ -1,3 +1,5 @@
1
+ require_relative 'spec_helper'
2
+
1
3
  RSpec.describe HTTParty do
2
4
  before(:each) do
3
5
  @klass = Class.new
@@ -192,7 +194,7 @@ RSpec.describe HTTParty do
192
194
  end
193
195
 
194
196
  it 'adds optional cookies to the optional headers' do
195
- expect_headers(baz: 'spax', 'cookie' => 'type=snickerdoodle')
197
+ expect_headers('baz' => 'spax', 'cookie' => 'type=snickerdoodle')
196
198
  @klass.get('', cookies: {type: 'snickerdoodle'}, headers: {baz: 'spax'})
197
199
  end
198
200
  end
@@ -215,12 +217,14 @@ RSpec.describe HTTParty do
215
217
  end
216
218
 
217
219
  context 'when headers passed as symbols' do
218
- let(:headers) { { 'foo' => 'application/json', 'bar' => 'example' } }
219
-
220
220
  it 'converts them to string' do
221
- expect(HTTParty::Request).to receive(:new)
222
- .with(anything, anything, hash_including({ headers: headers }))
223
- .and_return(double("mock response", perform: nil))
221
+ expect_headers('foo' => 'application/json', 'bar' => 'example')
222
+ headers = { foo: 'application/json', bar: 'example' }
223
+ @klass.post('http://example.com', headers: headers)
224
+ end
225
+
226
+ it 'converts default headers to string' do
227
+ expect_headers('foo' => 'application/json', 'bar' => 'example')
224
228
 
225
229
  @klass.headers(foo: 'application/json')
226
230
  @klass.post('http://example.com', headers: { bar: 'example' })
@@ -804,8 +808,8 @@ RSpec.describe HTTParty do
804
808
 
805
809
  describe "#get" do
806
810
  it "should be able to get html" do
807
- stub_http_response_with('google.html')
808
- expect(HTTParty.get('http://www.google.com').parsed_response).to eq(file_fixture('google.html'))
811
+ stub_http_response_with('example.html')
812
+ expect(HTTParty.get('http://www.example.com').parsed_response).to eq(file_fixture('example.html'))
809
813
  end
810
814
 
811
815
  it "should be able to get chunked html" do
@@ -827,6 +831,8 @@ RSpec.describe HTTParty do
827
831
  expect(
828
832
  HTTParty.get('http://www.google.com', options) do |fragment|
829
833
  expect(chunks).to include(fragment)
834
+ expect(fragment.code).to eql 200
835
+ expect(fragment.http_response).to be
830
836
  end.parsed_response
831
837
  ).to eq(nil)
832
838
  end
@@ -886,16 +892,16 @@ RSpec.describe HTTParty do
886
892
  end
887
893
 
888
894
  it "should accept http URIs" do
889
- stub_http_response_with('google.html')
895
+ stub_http_response_with('example.html')
890
896
  expect do
891
- HTTParty.get('http://google.com')
897
+ HTTParty.get('http://example.com')
892
898
  end.not_to raise_error
893
899
  end
894
900
 
895
901
  it "should accept https URIs" do
896
- stub_http_response_with('google.html')
902
+ stub_http_response_with('example.html')
897
903
  expect do
898
- HTTParty.get('https://google.com')
904
+ HTTParty.get('https://example.com')
899
905
  end.not_to raise_error
900
906
  end
901
907
 
@@ -37,6 +37,11 @@ RSpec.configure do |config|
37
37
 
38
38
  config.order = :random
39
39
 
40
+ config.before(:each) do
41
+ # Reset default_cert_store cache
42
+ HTTParty::ConnectionAdapter.instance_variable_set(:@default_cert_store, nil)
43
+ end
44
+
40
45
  Kernel.srand config.seed
41
46
  end
42
47
 
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: httparty
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.16.3
4
+ version: 0.16.4
5
5
  platform: ruby
6
6
  authors:
7
7
  - John Nunemaker
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2018-11-12 00:00:00.000000000 Z
12
+ date: 2019-02-05 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: multi_xml
@@ -102,6 +102,7 @@ files:
102
102
  - lib/httparty/connection_adapter.rb
103
103
  - lib/httparty/cookie_hash.rb
104
104
  - lib/httparty/exceptions.rb
105
+ - lib/httparty/fragment_with_response.rb
105
106
  - lib/httparty/hash_conversions.rb
106
107
  - lib/httparty/logger/apache_formatter.rb
107
108
  - lib/httparty/logger/curl_formatter.rb
@@ -120,7 +121,7 @@ files:
120
121
  - script/release
121
122
  - spec/fixtures/delicious.xml
122
123
  - spec/fixtures/empty.xml
123
- - spec/fixtures/google.html
124
+ - spec/fixtures/example.html
124
125
  - spec/fixtures/ssl/generate.sh
125
126
  - spec/fixtures/ssl/generated/bogushost.crt
126
127
  - spec/fixtures/ssl/generated/ca.crt
@@ -137,6 +138,7 @@ files:
137
138
  - spec/httparty/connection_adapter_spec.rb
138
139
  - spec/httparty/cookie_hash_spec.rb
139
140
  - spec/httparty/exception_spec.rb
141
+ - spec/httparty/fragment_with_response_spec.rb
140
142
  - spec/httparty/hash_conversions_spec.rb
141
143
  - spec/httparty/logger/apache_formatter_spec.rb
142
144
  - spec/httparty/logger/curl_formatter_spec.rb
@@ -196,7 +198,7 @@ test_files:
196
198
  - features/supports_timeout_option.feature
197
199
  - spec/fixtures/delicious.xml
198
200
  - spec/fixtures/empty.xml
199
- - spec/fixtures/google.html
201
+ - spec/fixtures/example.html
200
202
  - spec/fixtures/ssl/generate.sh
201
203
  - spec/fixtures/ssl/generated/bogushost.crt
202
204
  - spec/fixtures/ssl/generated/ca.crt
@@ -213,6 +215,7 @@ test_files:
213
215
  - spec/httparty/connection_adapter_spec.rb
214
216
  - spec/httparty/cookie_hash_spec.rb
215
217
  - spec/httparty/exception_spec.rb
218
+ - spec/httparty/fragment_with_response_spec.rb
216
219
  - spec/httparty/hash_conversions_spec.rb
217
220
  - spec/httparty/logger/apache_formatter_spec.rb
218
221
  - spec/httparty/logger/curl_formatter_spec.rb
@@ -1,3 +0,0 @@
1
- <html><head><meta http-equiv="content-type" content="text/html; charset=ISO-8859-1"><title>Google</title><style>body,td,a,p,.h{font-family:arial,sans-serif}.h{color:#36c;font-size:20px}.q{color:#00c}.ts td{padding:0}.ts{border-collapse:collapse}#gbar{height:22px;padding-left:2px}.gbh,.gbd{border-top:1px solid #c9d7f1;font-size:1px}.gbh{height:0;position:absolute;top:24px;width:100%}#gbi,#gbs{background:#fff;left:0;position:absolute;top:24px;visibility:hidden;z-index:1000}#gbi{border:1px solid;border-color:#c9d7f1 #36c #36c #a2bae7;z-index:1001}#guser{padding-bottom:7px !important}#gbar,#guser{font-size:13px;padding-top:1px !important}@media all{.gb1,.gb3{height:22px;margin-right:.73em;vertical-align:top}#gbar{float:left}}.gb2{display:block;padding:.2em .5em}a.gb1,a.gb2,a.gb3{color:#00c !important}.gb2,.gb3{text-decoration:none}a.gb2:hover{background:#36c;color:#fff !important}</style><script>window.google={kEI:"Zuk6ScOkLKHCMrrttckF",kEXPI:"17259,19124,19314",kHL:"en"};
2
- google.y={};google.x=function(e,g){google.y[e.id]=[e,g];return false};function sf(){document.f.q.focus()}
3
- window.gbar={};(function(){var b=window.gbar,f,h;b.qs=function(a){var c=window.encodeURIComponent&&(document.forms[0].q||"").value;if(c)a.href=a.href.replace(/([?&])q=[^&]*|$/,function(i,g){return(g||"&")+"q="+encodeURIComponent(c)})};function j(a,c){a.visibility=h?"hidden":"visible";a.left=c+"px"}b.tg=function(a){a=a||window.event;var c=0,i,g=window.navExtra,d=document.getElementById("gbi"),e=a.target||a.srcElement;a.cancelBubble=true;if(!f){f=document.createElement(Array.every||window.createPopup?"iframe":"div");f.frameBorder="0";f.src="#";d.parentNode.appendChild(f).id="gbs";if(g)for(i in g)d.insertBefore(g[i],d.firstChild).className="gb2";document.onclick=b.close}if(e.className!="gb3")e=e.parentNode;do c+=e.offsetLeft;while(e=e.offsetParent);j(d.style,c);f.style.width=d.offsetWidth+"px";f.style.height=d.offsetHeight+"px";j(f.style,c);h=!h};b.close=function(a){h&&b.tg(a)}})();</script></head><body bgcolor=#ffffff text=#000000 link=#0000cc vlink=#551a8b alink=#ff0000 onload="sf();if(document.images)new Image().src='/images/nav_logo3.png'" topmargin=3 marginheight=3><div id=gbar><nobr><b class=gb1>Web</b> <a href="http://images.google.com/imghp?hl=en&tab=wi" onclick=gbar.qs(this) class=gb1>Images</a> <a href="http://maps.google.com/maps?hl=en&tab=wl" onclick=gbar.qs(this) class=gb1>Maps</a> <a href="http://news.google.com/nwshp?hl=en&tab=wn" onclick=gbar.qs(this) class=gb1>News</a> <a href="http://www.google.com/prdhp?hl=en&tab=wf" onclick=gbar.qs(this) class=gb1>Shopping</a> <a href="http://mail.google.com/mail/?hl=en&tab=wm" class=gb1>Gmail</a> <a href="http://www.google.com/intl/en/options/" onclick="this.blur();gbar.tg(event);return !1" class=gb3><u>more</u> <small>&#9660;</small></a><div id=gbi> <a href="http://video.google.com/?hl=en&tab=wv" onclick=gbar.qs(this) class=gb2>Video</a> <a href="http://groups.google.com/grphp?hl=en&tab=wg" onclick=gbar.qs(this) class=gb2>Groups</a> <a href="http://books.google.com/bkshp?hl=en&tab=wp" onclick=gbar.qs(this) class=gb2>Books</a> <a href="http://scholar.google.com/schhp?hl=en&tab=ws" onclick=gbar.qs(this) class=gb2>Scholar</a> <a href="http://finance.google.com/finance?hl=en&tab=we" onclick=gbar.qs(this) class=gb2>Finance</a> <a href="http://blogsearch.google.com/?hl=en&tab=wb" onclick=gbar.qs(this) class=gb2>Blogs</a> <div class=gb2><div class=gbd></div></div> <a href="http://www.youtube.com/?hl=en&tab=w1" onclick=gbar.qs(this) class=gb2>YouTube</a> <a href="http://www.google.com/calendar/render?hl=en&tab=wc" class=gb2>Calendar</a> <a href="http://picasaweb.google.com/home?hl=en&tab=wq" onclick=gbar.qs(this) class=gb2>Photos</a> <a href="http://docs.google.com/?hl=en&tab=wo" class=gb2>Documents</a> <a href="http://www.google.com/reader/view/?hl=en&tab=wy" class=gb2>Reader</a> <a href="http://sites.google.com/?hl=en&tab=w3" class=gb2>Sites</a> <div class=gb2><div class=gbd></div></div> <a href="http://www.google.com/intl/en/options/" class=gb2>even more &raquo;</a></div> </nobr></div><div class=gbh style=left:0></div><div class=gbh style=right:0></div><div align=right id=guser style="font-size:84%;padding:0 0 4px" width=100%><nobr><a href="/url?sa=p&pref=ig&pval=3&q=http://www.google.com/ig%3Fhl%3Den%26source%3Diglk&usg=AFQjCNFA18XPfgb7dKnXfKz7x7g1GDH1tg">iGoogle</a> | <a href="https://www.google.com/accounts/Login?continue=http://www.google.com/&hl=en">Sign in</a></nobr></div><center><br clear=all id=lgpd><img alt="Google" height=110 src="/intl/en_ALL/images/logo.gif" width=276><br><br><form action="/search" name=f><table cellpadding=0 cellspacing=0><tr valign=top><td width=25%>&nbsp;</td><td align=center nowrap><input name=hl type=hidden value=en><input type=hidden name=ie value="ISO-8859-1"><input autocomplete="off" maxlength=2048 name=q size=55 title="Google Search" value=""><br><input name=btnG type=submit value="Google Search"><input name=btnI type=submit value="I'm Feeling Lucky"></td><td nowrap width=25%><font size=-2>&nbsp;&nbsp;<a href=/advanced_search?hl=en>Advanced Search</a><br>&nbsp;&nbsp;<a href=/preferences?hl=en>Preferences</a><br>&nbsp;&nbsp;<a href=/language_tools?hl=en>Language Tools</a></font></td></tr></table></form><br><br><font size=-1><a href="/intl/en/ads/">Advertising&nbsp;Programs</a> - <a href="/services/">Business Solutions</a> - <a href="/intl/en/about.html">About Google</a></font><p><font size=-2>&copy;2008 - <a href="/intl/en/privacy.html">Privacy</a></font></p></center></body><script>if(google.y)google.y.first=[];window.setTimeout(function(){var xjs=document.createElement('script');xjs.src='/extern_js/f/CgJlbhICdXMgACswCjgMLCswDjgCLCswGDgDLA/8MIofMT_4o8.js';document.getElementsByTagName('head')[0].appendChild(xjs)},0);google.y.first.push(function(){google.ac.i(document.f,document.f.q,'','')})</script></html>