gist 5.1.0 → 6.0.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: b81d6d7f5378a97ea2110a82b03f703ef3e288ac038c233428e0413149b5bb34
4
- data.tar.gz: 2f01fcdd837e72af4a7ed7c53120b1605ad73fd15d15a4e8d4dd709d43775f7e
3
+ metadata.gz: 78e3d009d0e3e3a535d20e8d7e0582705c6ea61308958c3f59a4d4a2e365fa3c
4
+ data.tar.gz: 23df778298090151ce7e2b8f3dbb8f97786bc7d576f218be1aeb454123090e91
5
5
  SHA512:
6
- metadata.gz: b5c0ae73ff4b52e917a485ad0e95b4a5520ba8212b39557a82ccc3cc8bf309a34ddfceedc67fe24222069ffd485fa857776238b05a57fc0c52ec7a215c6ca297
7
- data.tar.gz: 9ded7356c613f6130bbb9b3c8859723c12a0c897073c6c6149c37038d3d3dd285a98dd2b773ba7dc036ba520e4b8440eec76c9468a22ff8a0213e70e0d8ddc52
6
+ metadata.gz: 70506cc45562b725085e2f10d5627a3ac4c700fa6e599e39a6625629fbd2c63903b2d42a4a95d9642f0ac01893bd8de27d72acb7e4622eaa44ab4ea96b08840c
7
+ data.tar.gz: b3476f46cb844603a169e3a67fae3bb37e5399494ddec6c78178f4bb664a90cda642c5be780ea007d12e96cc96312f695b86827872bc4947603c092a4e3d6774
data/README.md CHANGED
@@ -84,9 +84,26 @@ To read a gist and print it to STDOUT
84
84
 
85
85
  ## Login
86
86
 
87
- If you want to associate your gists with your GitHub account, you need to login
88
- with gist. It doesn't store your username and password, it just uses them to get
89
- an OAuth2 token (with the "gist" permission).
87
+ Before you use `gist` for the first time you will need to log in. There are two supported login flows:
88
+
89
+ 1. The Github device-code Oauth flow. This is the default for authenticating to github.com, and can be enabled for Github Enterprise by creating an Oauth app, and exporting the environment variable `GIST_CLIENT_ID` with the client id of the Oauth app.
90
+ 2. The (deprecated) username and password token exchange flow. This is the default for GitHub Enterprise, and can be used to log into github.com by exporting the environment variable `GIST_USE_USERNAME_AND_PASSWORD`.
91
+
92
+ ### The device-code flow
93
+
94
+ This flow allows you to obtain a token by logging into GitHub in the browser and typing a verification code. This is the preferred mechanism.
95
+
96
+ gist --login
97
+ Requesting login parameters...
98
+ Please sign in at https://github.com/login/device
99
+ and enter code: XXXX-XXXX
100
+ Success! https://github.com/settings/connections/applications/4f7ec0d4eab38e74384e
101
+
102
+ The returned access_token is stored in `~/.gist` and used for all future gisting. If you need to you can revoke access from https://github.com/settings/connections/applications/4f7ec0d4eab38e74384e.
103
+
104
+ ### The username-password flow
105
+
106
+ This flow asks for your GitHub username and password (and 2FA code), and exchanges them for a token with the "gist" permission (your username and password are not stored). This mechanism is deprecated by GitHub, but may still work with GitHub Enterprise.
90
107
 
91
108
  gist --login
92
109
  Obtaining OAuth2 access_token from GitHub.
@@ -102,8 +119,9 @@ file.
102
119
  #### Password-less login
103
120
 
104
121
  If you have a complicated authorization requirement you can manually create a
105
- token file by pasting a GitHub token with only the `gist` permission into a
106
- file called `~/.gist`. You can create one from https://github.com/settings/tokens
122
+ token file by pasting a GitHub token with `gist` scope (and maybe the `user:email`
123
+ for GitHub Enterprise) into a file called `~/.gist`. You can create one from
124
+ https://github.com/settings/tokens
107
125
 
108
126
  This file should contain only the token (~40 hex characters), and to make it
109
127
  easier to edit, can optionally have a final newline (`\n` or `\r\n`).
@@ -156,7 +174,7 @@ If you need more advanced features you can also pass:
156
174
  * `:copy` to copy the resulting URL to the clipboard (default is false).
157
175
  * `:open` to open the resulting URL in a browser (default is false).
158
176
 
159
- NOTE: The access_token must have the "gist" scope.
177
+ NOTE: The access_token must have the `gist` scope and may also require the `user:email` scope.
160
178
 
161
179
  ‌If you want to upload multiple files in the same gist, you can:
162
180
 
@@ -9,7 +9,7 @@ Gem::Specification.new do |s|
9
9
  s.email = ['conrad.irwin@gmail.com', 'rkingist@sharpsaw.org']
10
10
  s.authors = ['Conrad Irwin', '☈king']
11
11
  s.license = 'MIT'
12
- s.files = `git ls-files`.split("\n")
12
+ s.files = `git ls-files`.split("\n") - Dir.glob("build/*") - [".gitignore"]
13
13
  s.require_paths = ["lib"]
14
14
 
15
15
  s.executables << 'gist'
@@ -12,7 +12,7 @@ end
12
12
  module Gist
13
13
  extend self
14
14
 
15
- VERSION = '5.1.0'
15
+ VERSION = '6.0.0'
16
16
 
17
17
  # A list of clipboard commands with copy and paste support.
18
18
  CLIPBOARD_COMMANDS = {
@@ -23,12 +23,16 @@ module Gist
23
23
  }
24
24
 
25
25
  GITHUB_API_URL = URI("https://api.github.com/")
26
+ GITHUB_URL = URI("https://github.com/")
26
27
  GIT_IO_URL = URI("https://git.io")
27
28
 
28
29
  GITHUB_BASE_PATH = ""
29
30
  GHE_BASE_PATH = "/api/v3"
30
31
 
32
+ GITHUB_CLIENT_ID = '4f7ec0d4eab38e74384e'
33
+
31
34
  URL_ENV_NAME = "GITHUB_URL"
35
+ CLIENT_ID_ENV_NAME = "GIST_CLIENT_ID"
32
36
 
33
37
  USER_AGENT = "gist/#{VERSION} (Net::HTTP, #{RUBY_DESCRIPTION})"
34
38
 
@@ -44,7 +48,7 @@ module Gist
44
48
  module AuthTokenFile
45
49
  def self.filename
46
50
  if ENV.key?(URL_ENV_NAME)
47
- File.expand_path "~/.gist.#{ENV[URL_ENV_NAME].gsub(/:/, '.').gsub(/[^a-z0-9.]/, '')}"
51
+ File.expand_path "~/.gist.#{ENV[URL_ENV_NAME].gsub(/:/, '.').gsub(/[^a-z0-9.-]/, '')}"
48
52
  else
49
53
  File.expand_path "~/.gist"
50
54
  end
@@ -200,19 +204,12 @@ module Gist
200
204
  url = "#{base_path}"
201
205
 
202
206
  if user == ""
203
- access_token = auth_token()
204
- if access_token.to_s != ''
205
- url << "/gists?per_page=100"
206
- get_gist_pages(url, access_token)
207
- else
208
- raise Error, "Not authenticated. Use 'gist --login' to login or 'gist -l username' to view public gists."
209
- end
210
-
207
+ url << "/gists?per_page=100"
211
208
  else
212
209
  url << "/users/#{user}/gists?per_page=100"
213
- get_gist_pages(url)
214
210
  end
215
211
 
212
+ get_gist_pages(url, auth_token())
216
213
  end
217
214
 
218
215
  def read_gist(id, file_name=nil)
@@ -336,15 +333,71 @@ module Gist
336
333
 
337
334
  # Log the user into gist.
338
335
  #
336
+ def login!(credentials={})
337
+ if (login_url == GITHUB_URL || ENV.key?(CLIENT_ID_ENV_NAME)) && credentials.empty? && !ENV.key?('GIST_USE_USERNAME_AND_PASSWORD')
338
+ device_flow_login!
339
+ else
340
+ access_token_login!(credentials)
341
+ end
342
+ end
343
+
344
+ def device_flow_login!
345
+ puts "Requesting login parameters..."
346
+ request = Net::HTTP::Post.new("/login/device/code")
347
+ request.body = JSON.dump({
348
+ :scope => 'gist',
349
+ :client_id => client_id,
350
+ })
351
+ request.content_type = 'application/json'
352
+ request['accept'] = "application/json"
353
+ response = http(login_url, request)
354
+
355
+ if response.code != '200'
356
+ raise Error, "HTTP #{response.code}: #{response.body}"
357
+ end
358
+
359
+ body = JSON.parse(response.body)
360
+
361
+ puts "Please sign in at #{body['verification_uri']}"
362
+ puts " and enter code: #{body['user_code']}"
363
+ device_code = body['device_code']
364
+ interval = body['interval']
365
+
366
+ loop do
367
+ sleep(interval.to_i)
368
+ request = Net::HTTP::Post.new("/login/oauth/access_token")
369
+ request.body = JSON.dump({
370
+ :client_id => client_id,
371
+ :grant_type => 'urn:ietf:params:oauth:grant-type:device_code',
372
+ :device_code => device_code
373
+ })
374
+ request.content_type = 'application/json'
375
+ request['Accept'] = 'application/json'
376
+ response = http(login_url, request)
377
+ if response.code != '200'
378
+ raise Error, "HTTP #{response.code}: #{response.body}"
379
+ end
380
+ body = JSON.parse(response.body)
381
+ break unless body['error'] == 'authorization_pending'
382
+ end
383
+
384
+ if body['error']
385
+ raise Error, body['error_description']
386
+ end
387
+
388
+ AuthTokenFile.write JSON.parse(response.body)['access_token']
389
+
390
+ puts "Success! #{ENV[URL_ENV_NAME] || "https://github.com/"}settings/connections/applications/#{client_id}"
391
+ end
392
+
393
+ # Logs the user into gist.
394
+ #
339
395
  # This method asks the user for a username and password, and tries to obtain
340
396
  # and OAuth2 access token, which is then stored in ~/.gist
341
397
  #
342
398
  # @raise [Gist::Error] if something went wrong
343
- # @param [Hash] credentials login details
344
- # @option credentials [String] :username
345
- # @option credentials [String] :password
346
399
  # @see http://developer.github.com/v3/oauth/
347
- def login!(credentials={})
400
+ def access_token_login!(credentials={})
348
401
  puts "Obtaining OAuth2 access_token from GitHub."
349
402
  loop do
350
403
  print "GitHub username: "
@@ -401,7 +454,11 @@ module Gist
401
454
  env = ENV['http_proxy'] || ENV['HTTP_PROXY']
402
455
  connection = if env
403
456
  proxy = URI(env)
404
- Net::HTTP::Proxy(proxy.host, proxy.port).new(uri.host, uri.port)
457
+ if proxy.user
458
+ Net::HTTP::Proxy(proxy.host, proxy.port, proxy.user, proxy.password).new(uri.host, uri.port)
459
+ else
460
+ Net::HTTP::Proxy(proxy.host, proxy.port).new(uri.host, uri.port)
461
+ end
405
462
  else
406
463
  Net::HTTP.new(uri.host, uri.port)
407
464
  end
@@ -555,11 +612,19 @@ Could not find copy command, tried:
555
612
  ENV.key?(URL_ENV_NAME) ? GHE_BASE_PATH : GITHUB_BASE_PATH
556
613
  end
557
614
 
615
+ def login_url
616
+ ENV.key?(URL_ENV_NAME) ? URI(ENV[URL_ENV_NAME]) : GITHUB_URL
617
+ end
618
+
558
619
  # Get the API URL
559
620
  def api_url
560
621
  ENV.key?(URL_ENV_NAME) ? URI(ENV[URL_ENV_NAME]) : GITHUB_API_URL
561
622
  end
562
623
 
624
+ def client_id
625
+ ENV.key?(CLIENT_ID_ENV_NAME) ? URI(ENV[CLIENT_ID_ENV_NAME]) : GITHUB_CLIENT_ID
626
+ end
627
+
563
628
  def legacy_private_gister?
564
629
  return unless which('git')
565
630
  `git config --global gist.private` =~ /\Ayes|1|true|on\z/i
@@ -5,10 +5,10 @@ describe '...' do
5
5
  MOCK_USER = 'foo'
6
6
  MOCK_PASSWORD = 'bar'
7
7
 
8
- MOCK_AUTHZ_GHE_URL = "#{MOCK_GHE_PROTOCOL}://#{MOCK_USER}:#{MOCK_PASSWORD}@#{MOCK_GHE_HOST}/api/v3/"
8
+ MOCK_AUTHZ_GHE_URL = "#{MOCK_GHE_PROTOCOL}://#{MOCK_GHE_HOST}/api/v3/"
9
9
  MOCK_GHE_URL = "#{MOCK_GHE_PROTOCOL}://#{MOCK_GHE_HOST}/api/v3/"
10
- MOCK_AUTHZ_GITHUB_URL = "https://#{MOCK_USER}:#{MOCK_PASSWORD}@api.github.com/"
11
10
  MOCK_GITHUB_URL = "https://api.github.com/"
11
+ MOCK_LOGIN_URL = "https://github.com/"
12
12
 
13
13
  before do
14
14
  @saved_env = ENV[Gist::URL_ENV_NAME]
@@ -20,8 +20,15 @@ describe '...' do
20
20
  # stub requests for /authorizations
21
21
  stub_request(:post, /#{MOCK_AUTHZ_GHE_URL}authorizations/).
22
22
  to_return(:status => 201, :body => '{"token": "asdf"}')
23
- stub_request(:post, /#{MOCK_AUTHZ_GITHUB_URL}authorizations/).
23
+ stub_request(:post, /#{MOCK_GITHUB_URL}authorizations/).
24
+ with(headers: {'Authorization'=>'Basic Zm9vOmJhcg=='}).
24
25
  to_return(:status => 201, :body => '{"token": "asdf"}')
26
+
27
+ stub_request(:post, /#{MOCK_LOGIN_URL}login\/device\/code/).
28
+ to_return(:status => 200, :body => '{"interval": "0.1", "user_code":"XXXX-XXXX", "device_code": "xxxx", "verification_uri": "https://github.com/login/device"}')
29
+
30
+ stub_request(:post, /#{MOCK_LOGIN_URL}login\/oauth\/access_token/).
31
+ to_return(:status => 200, :body => '{"access_token":"zzzz"}')
25
32
  end
26
33
 
27
34
  after do
@@ -48,7 +55,7 @@ describe '...' do
48
55
 
49
56
  Gist.login!
50
57
 
51
- assert_requested(:post, /#{MOCK_AUTHZ_GITHUB_URL}authorizations/)
58
+ assert_requested(:post, /#{MOCK_LOGIN_URL}login\/oauth\/access_token/)
52
59
  end
53
60
 
54
61
  it "should access to #{MOCK_GHE_HOST} when $#{Gist::URL_ENV_NAME} was set" do
@@ -65,7 +72,7 @@ describe '...' do
65
72
  $stdin = StringIO.new "#{MOCK_USER}_wrong\n#{MOCK_PASSWORD}_wrong\n"
66
73
  Gist.login! :username => MOCK_USER, :password => MOCK_PASSWORD
67
74
 
68
- assert_requested(:post, /#{MOCK_AUTHZ_GITHUB_URL}authorizations/)
75
+ assert_requested(:post, /#{MOCK_GITHUB_URL}authorizations/)
69
76
  end
70
77
 
71
78
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: gist
3
3
  version: !ruby/object:Gem::Version
4
- version: 5.1.0
4
+ version: 6.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Conrad Irwin
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2020-02-07 00:00:00.000000000 Z
12
+ date: 2020-08-27 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: rake
@@ -76,15 +76,12 @@ executables:
76
76
  extensions: []
77
77
  extra_rdoc_files: []
78
78
  files:
79
- - ".gitignore"
80
79
  - ".rspec"
81
80
  - Gemfile
82
81
  - LICENSE.MIT
83
82
  - README.md
84
83
  - Rakefile
85
84
  - bin/gist
86
- - build/gist
87
- - build/gist.1
88
85
  - gist.gemspec
89
86
  - lib/gist.rb
90
87
  - spec/auth_token_file_spec.rb
data/.gitignore DELETED
@@ -1,8 +0,0 @@
1
- .rvmrc
2
- Gemfile.lock
3
-
4
- # OS X
5
- .DS_Store
6
- .yardoc
7
- doc
8
- *.gem
data/build/gist DELETED
@@ -1,2091 +0,0 @@
1
- #!/usr/bin/env ruby
2
- # This is generated from https://github.com/defunkt/gist using 'rake standalone'
3
- # any changes will be overwritten.
4
- require 'net/https'
5
- require 'cgi'
6
- require 'uri'
7
-
8
- begin
9
- require 'strscan'
10
-
11
- module JSON
12
- module Pure
13
- # This class implements the JSON parser that is used to parse a JSON string
14
- # into a Ruby data structure.
15
- class Parser < StringScanner
16
- STRING = /" ((?:[^\x0-\x1f"\\] |
17
- # escaped special characters:
18
- \\["\\\/bfnrt] |
19
- \\u[0-9a-fA-F]{4} |
20
- # match all but escaped special characters:
21
- \\[\x20-\x21\x23-\x2e\x30-\x5b\x5d-\x61\x63-\x65\x67-\x6d\x6f-\x71\x73\x75-\xff])*)
22
- "/nx
23
- INTEGER = /(-?0|-?[1-9]\d*)/
24
- FLOAT = /(-?
25
- (?:0|[1-9]\d*)
26
- (?:
27
- \.\d+(?i:e[+-]?\d+) |
28
- \.\d+ |
29
- (?i:e[+-]?\d+)
30
- )
31
- )/x
32
- NAN = /NaN/
33
- INFINITY = /Infinity/
34
- MINUS_INFINITY = /-Infinity/
35
- OBJECT_OPEN = /\{/
36
- OBJECT_CLOSE = /\}/
37
- ARRAY_OPEN = /\[/
38
- ARRAY_CLOSE = /\]/
39
- PAIR_DELIMITER = /:/
40
- COLLECTION_DELIMITER = /,/
41
- TRUE = /true/
42
- FALSE = /false/
43
- NULL = /null/
44
- IGNORE = %r(
45
- (?:
46
- //[^\n\r]*[\n\r]| # line comments
47
- /\* # c-style comments
48
- (?:
49
- [^*/]| # normal chars
50
- /[^*]| # slashes that do not start a nested comment
51
- \*[^/]| # asterisks that do not end this comment
52
- /(?=\*/) # single slash before this comment's end
53
- )*
54
- \*/ # the End of this comment
55
- |[ \t\r\n]+ # whitespaces: space, horicontal tab, lf, cr
56
- )+
57
- )mx
58
-
59
- UNPARSED = Object.new
60
-
61
- # Creates a new JSON::Pure::Parser instance for the string _source_.
62
- #
63
- # It will be configured by the _opts_ hash. _opts_ can have the following
64
- # keys:
65
- # * *max_nesting*: The maximum depth of nesting allowed in the parsed data
66
- # structures. Disable depth checking with :max_nesting => false|nil|0,
67
- # it defaults to 19.
68
- # * *allow_nan*: If set to true, allow NaN, Infinity and -Infinity in
69
- # defiance of RFC 4627 to be parsed by the Parser. This option defaults
70
- # to false.
71
- # * *symbolize_names*: If set to true, returns symbols for the names
72
- # (keys) in a JSON object. Otherwise strings are returned, which is also
73
- # the default.
74
- # * *create_additions*: If set to false, the Parser doesn't create
75
- # additions even if a matchin class and create_id was found. This option
76
- # defaults to true.
77
- # * *object_class*: Defaults to Hash
78
- # * *array_class*: Defaults to Array
79
- # * *quirks_mode*: Enables quirks_mode for parser, that is for example
80
- # parsing single JSON values instead of documents is possible.
81
- def initialize(source, opts = {})
82
- opts ||= {}
83
- unless @quirks_mode = opts[:quirks_mode]
84
- source = convert_encoding source
85
- end
86
- super source
87
- if !opts.key?(:max_nesting) # defaults to 19
88
- @max_nesting = 19
89
- elsif opts[:max_nesting]
90
- @max_nesting = opts[:max_nesting]
91
- else
92
- @max_nesting = 0
93
- end
94
- @allow_nan = !!opts[:allow_nan]
95
- @symbolize_names = !!opts[:symbolize_names]
96
- if opts.key?(:create_additions)
97
- @create_additions = !!opts[:create_additions]
98
- else
99
- @create_additions = true
100
- end
101
- @create_id = @create_additions ? JSON.create_id : nil
102
- @object_class = opts[:object_class] || Hash
103
- @array_class = opts[:array_class] || Array
104
- @match_string = opts[:match_string]
105
- end
106
-
107
- alias source string
108
-
109
- def quirks_mode?
110
- !!@quirks_mode
111
- end
112
-
113
- def reset
114
- super
115
- @current_nesting = 0
116
- end
117
-
118
- # Parses the current JSON string _source_ and returns the complete data
119
- # structure as a result.
120
- def parse
121
- reset
122
- obj = nil
123
- if @quirks_mode
124
- while !eos? && skip(IGNORE)
125
- end
126
- if eos?
127
- raise ParserError, "source did not contain any JSON!"
128
- else
129
- obj = parse_value
130
- obj == UNPARSED and raise ParserError, "source did not contain any JSON!"
131
- end
132
- else
133
- until eos?
134
- case
135
- when scan(OBJECT_OPEN)
136
- obj and raise ParserError, "source '#{peek(20)}' not in JSON!"
137
- @current_nesting = 1
138
- obj = parse_object
139
- when scan(ARRAY_OPEN)
140
- obj and raise ParserError, "source '#{peek(20)}' not in JSON!"
141
- @current_nesting = 1
142
- obj = parse_array
143
- when skip(IGNORE)
144
- ;
145
- else
146
- raise ParserError, "source '#{peek(20)}' not in JSON!"
147
- end
148
- end
149
- obj or raise ParserError, "source did not contain any JSON!"
150
- end
151
- obj
152
- end
153
-
154
- private
155
-
156
- def convert_encoding(source)
157
- if source.respond_to?(:to_str)
158
- source = source.to_str
159
- else
160
- raise TypeError, "#{source.inspect} is not like a string"
161
- end
162
- if defined?(::Encoding)
163
- if source.encoding == ::Encoding::ASCII_8BIT
164
- b = source[0, 4].bytes.to_a
165
- source =
166
- case
167
- when b.size >= 4 && b[0] == 0 && b[1] == 0 && b[2] == 0
168
- source.dup.force_encoding(::Encoding::UTF_32BE).encode!(::Encoding::UTF_8)
169
- when b.size >= 4 && b[0] == 0 && b[2] == 0
170
- source.dup.force_encoding(::Encoding::UTF_16BE).encode!(::Encoding::UTF_8)
171
- when b.size >= 4 && b[1] == 0 && b[2] == 0 && b[3] == 0
172
- source.dup.force_encoding(::Encoding::UTF_32LE).encode!(::Encoding::UTF_8)
173
- when b.size >= 4 && b[1] == 0 && b[3] == 0
174
- source.dup.force_encoding(::Encoding::UTF_16LE).encode!(::Encoding::UTF_8)
175
- else
176
- source.dup
177
- end
178
- else
179
- source = source.encode(::Encoding::UTF_8)
180
- end
181
- source.force_encoding(::Encoding::ASCII_8BIT)
182
- else
183
- b = source
184
- source =
185
- case
186
- when b.size >= 4 && b[0] == 0 && b[1] == 0 && b[2] == 0
187
- JSON.iconv('utf-8', 'utf-32be', b)
188
- when b.size >= 4 && b[0] == 0 && b[2] == 0
189
- JSON.iconv('utf-8', 'utf-16be', b)
190
- when b.size >= 4 && b[1] == 0 && b[2] == 0 && b[3] == 0
191
- JSON.iconv('utf-8', 'utf-32le', b)
192
- when b.size >= 4 && b[1] == 0 && b[3] == 0
193
- JSON.iconv('utf-8', 'utf-16le', b)
194
- else
195
- b
196
- end
197
- end
198
- source
199
- end
200
-
201
- # Unescape characters in strings.
202
- UNESCAPE_MAP = Hash.new { |h, k| h[k] = k.chr }
203
- UNESCAPE_MAP.update({
204
- ?" => '"',
205
- ?\\ => '\\',
206
- ?/ => '/',
207
- ?b => "\b",
208
- ?f => "\f",
209
- ?n => "\n",
210
- ?r => "\r",
211
- ?t => "\t",
212
- ?u => nil,
213
- })
214
-
215
- EMPTY_8BIT_STRING = ''
216
- if ::String.method_defined?(:encode)
217
- EMPTY_8BIT_STRING.force_encoding Encoding::ASCII_8BIT
218
- end
219
-
220
- def parse_string
221
- if scan(STRING)
222
- return '' if self[1].empty?
223
- string = self[1].gsub(%r((?:\\[\\bfnrt"/]|(?:\\u(?:[A-Fa-f\d]{4}))+|\\[\x20-\xff]))n) do |c|
224
- if u = UNESCAPE_MAP[$&[1]]
225
- u
226
- else # \uXXXX
227
- bytes = EMPTY_8BIT_STRING.dup
228
- i = 0
229
- while c[6 * i] == ?\\ && c[6 * i + 1] == ?u
230
- bytes << c[6 * i + 2, 2].to_i(16) << c[6 * i + 4, 2].to_i(16)
231
- i += 1
232
- end
233
- JSON.iconv('utf-8', 'utf-16be', bytes)
234
- end
235
- end
236
- if string.respond_to?(:force_encoding)
237
- string.force_encoding(::Encoding::UTF_8)
238
- end
239
- if @create_additions and @match_string
240
- for (regexp, klass) in @match_string
241
- klass.json_creatable? or next
242
- string =~ regexp and return klass.json_create(string)
243
- end
244
- end
245
- string
246
- else
247
- UNPARSED
248
- end
249
- rescue => e
250
- raise ParserError, "Caught #{e.class} at '#{peek(20)}': #{e}"
251
- end
252
-
253
- def parse_value
254
- case
255
- when scan(FLOAT)
256
- Float(self[1])
257
- when scan(INTEGER)
258
- Integer(self[1])
259
- when scan(TRUE)
260
- true
261
- when scan(FALSE)
262
- false
263
- when scan(NULL)
264
- nil
265
- when (string = parse_string) != UNPARSED
266
- string
267
- when scan(ARRAY_OPEN)
268
- @current_nesting += 1
269
- ary = parse_array
270
- @current_nesting -= 1
271
- ary
272
- when scan(OBJECT_OPEN)
273
- @current_nesting += 1
274
- obj = parse_object
275
- @current_nesting -= 1
276
- obj
277
- when @allow_nan && scan(NAN)
278
- NaN
279
- when @allow_nan && scan(INFINITY)
280
- Infinity
281
- when @allow_nan && scan(MINUS_INFINITY)
282
- MinusInfinity
283
- else
284
- UNPARSED
285
- end
286
- end
287
-
288
- def parse_array
289
- raise NestingError, "nesting of #@current_nesting is too deep" if
290
- @max_nesting.nonzero? && @current_nesting > @max_nesting
291
- result = @array_class.new
292
- delim = false
293
- until eos?
294
- case
295
- when (value = parse_value) != UNPARSED
296
- delim = false
297
- result << value
298
- skip(IGNORE)
299
- if scan(COLLECTION_DELIMITER)
300
- delim = true
301
- elsif match?(ARRAY_CLOSE)
302
- ;
303
- else
304
- raise ParserError, "expected ',' or ']' in array at '#{peek(20)}'!"
305
- end
306
- when scan(ARRAY_CLOSE)
307
- if delim
308
- raise ParserError, "expected next element in array at '#{peek(20)}'!"
309
- end
310
- break
311
- when skip(IGNORE)
312
- ;
313
- else
314
- raise ParserError, "unexpected token in array at '#{peek(20)}'!"
315
- end
316
- end
317
- result
318
- end
319
-
320
- def parse_object
321
- raise NestingError, "nesting of #@current_nesting is too deep" if
322
- @max_nesting.nonzero? && @current_nesting > @max_nesting
323
- result = @object_class.new
324
- delim = false
325
- until eos?
326
- case
327
- when (string = parse_string) != UNPARSED
328
- skip(IGNORE)
329
- unless scan(PAIR_DELIMITER)
330
- raise ParserError, "expected ':' in object at '#{peek(20)}'!"
331
- end
332
- skip(IGNORE)
333
- unless (value = parse_value).equal? UNPARSED
334
- result[@symbolize_names ? string.to_sym : string] = value
335
- delim = false
336
- skip(IGNORE)
337
- if scan(COLLECTION_DELIMITER)
338
- delim = true
339
- elsif match?(OBJECT_CLOSE)
340
- ;
341
- else
342
- raise ParserError, "expected ',' or '}' in object at '#{peek(20)}'!"
343
- end
344
- else
345
- raise ParserError, "expected value in object at '#{peek(20)}'!"
346
- end
347
- when scan(OBJECT_CLOSE)
348
- if delim
349
- raise ParserError, "expected next name, value pair in object at '#{peek(20)}'!"
350
- end
351
- if @create_additions and klassname = result[@create_id]
352
- klass = JSON.deep_const_get klassname
353
- break unless klass and klass.json_creatable?
354
- result = klass.json_create(result)
355
- end
356
- break
357
- when skip(IGNORE)
358
- ;
359
- else
360
- raise ParserError, "unexpected token in object at '#{peek(20)}'!"
361
- end
362
- end
363
- result
364
- end
365
- end
366
- end
367
- end
368
-
369
- module JSON
370
- MAP = {
371
- "\x0" => '\u0000',
372
- "\x1" => '\u0001',
373
- "\x2" => '\u0002',
374
- "\x3" => '\u0003',
375
- "\x4" => '\u0004',
376
- "\x5" => '\u0005',
377
- "\x6" => '\u0006',
378
- "\x7" => '\u0007',
379
- "\b" => '\b',
380
- "\t" => '\t',
381
- "\n" => '\n',
382
- "\xb" => '\u000b',
383
- "\f" => '\f',
384
- "\r" => '\r',
385
- "\xe" => '\u000e',
386
- "\xf" => '\u000f',
387
- "\x10" => '\u0010',
388
- "\x11" => '\u0011',
389
- "\x12" => '\u0012',
390
- "\x13" => '\u0013',
391
- "\x14" => '\u0014',
392
- "\x15" => '\u0015',
393
- "\x16" => '\u0016',
394
- "\x17" => '\u0017',
395
- "\x18" => '\u0018',
396
- "\x19" => '\u0019',
397
- "\x1a" => '\u001a',
398
- "\x1b" => '\u001b',
399
- "\x1c" => '\u001c',
400
- "\x1d" => '\u001d',
401
- "\x1e" => '\u001e',
402
- "\x1f" => '\u001f',
403
- '"' => '\"',
404
- '\\' => '\\\\',
405
- } # :nodoc:
406
-
407
- # Convert a UTF8 encoded Ruby string _string_ to a JSON string, encoded with
408
- # UTF16 big endian characters as \u????, and return it.
409
- if defined?(::Encoding)
410
- def utf8_to_json(string) # :nodoc:
411
- string = string.dup
412
- string << '' # XXX workaround: avoid buffer sharing
413
- string.force_encoding(::Encoding::ASCII_8BIT)
414
- string.gsub!(/["\\\x0-\x1f]/) { MAP[$&] }
415
- string.force_encoding(::Encoding::UTF_8)
416
- string
417
- end
418
-
419
- def utf8_to_json_ascii(string) # :nodoc:
420
- string = string.dup
421
- string << '' # XXX workaround: avoid buffer sharing
422
- string.force_encoding(::Encoding::ASCII_8BIT)
423
- string.gsub!(/["\\\x0-\x1f]/) { MAP[$&] }
424
- string.gsub!(/(
425
- (?:
426
- [\xc2-\xdf][\x80-\xbf] |
427
- [\xe0-\xef][\x80-\xbf]{2} |
428
- [\xf0-\xf4][\x80-\xbf]{3}
429
- )+ |
430
- [\x80-\xc1\xf5-\xff] # invalid
431
- )/nx) { |c|
432
- c.size == 1 and raise GeneratorError, "invalid utf8 byte: '#{c}'"
433
- s = JSON.iconv('utf-16be', 'utf-8', c).unpack('H*')[0]
434
- s.gsub!(/.{4}/n, '\\\\u\&')
435
- }
436
- string.force_encoding(::Encoding::UTF_8)
437
- string
438
- rescue => e
439
- raise GeneratorError, "Caught #{e.class}: #{e}"
440
- end
441
- else
442
- def utf8_to_json(string) # :nodoc:
443
- string.gsub(/["\\\x0-\x1f]/) { MAP[$&] }
444
- end
445
-
446
- def utf8_to_json_ascii(string) # :nodoc:
447
- string = string.gsub(/["\\\x0-\x1f]/) { MAP[$&] }
448
- string.gsub!(/(
449
- (?:
450
- [\xc2-\xdf][\x80-\xbf] |
451
- [\xe0-\xef][\x80-\xbf]{2} |
452
- [\xf0-\xf4][\x80-\xbf]{3}
453
- )+ |
454
- [\x80-\xc1\xf5-\xff] # invalid
455
- )/nx) { |c|
456
- c.size == 1 and raise GeneratorError, "invalid utf8 byte: '#{c}'"
457
- s = JSON.iconv('utf-16be', 'utf-8', c).unpack('H*')[0]
458
- s.gsub!(/.{4}/n, '\\\\u\&')
459
- }
460
- string
461
- rescue => e
462
- raise GeneratorError, "Caught #{e.class}: #{e}"
463
- end
464
- end
465
- module_function :utf8_to_json, :utf8_to_json_ascii
466
-
467
- module Pure
468
- module Generator
469
- # This class is used to create State instances, that are use to hold data
470
- # while generating a JSON text from a Ruby data structure.
471
- class State
472
- # Creates a State object from _opts_, which ought to be Hash to create
473
- # a new State instance configured by _opts_, something else to create
474
- # an unconfigured instance. If _opts_ is a State object, it is just
475
- # returned.
476
- def self.from_state(opts)
477
- case
478
- when self === opts
479
- opts
480
- when opts.respond_to?(:to_hash)
481
- new(opts.to_hash)
482
- when opts.respond_to?(:to_h)
483
- new(opts.to_h)
484
- else
485
- SAFE_STATE_PROTOTYPE.dup
486
- end
487
- end
488
-
489
- # Instantiates a new State object, configured by _opts_.
490
- #
491
- # _opts_ can have the following keys:
492
- #
493
- # * *indent*: a string used to indent levels (default: ''),
494
- # * *space*: a string that is put after, a : or , delimiter (default: ''),
495
- # * *space_before*: a string that is put before a : pair delimiter (default: ''),
496
- # * *object_nl*: a string that is put at the end of a JSON object (default: ''),
497
- # * *array_nl*: a string that is put at the end of a JSON array (default: ''),
498
- # * *check_circular*: is deprecated now, use the :max_nesting option instead,
499
- # * *max_nesting*: sets the maximum level of data structure nesting in
500
- # the generated JSON, max_nesting = 0 if no maximum should be checked.
501
- # * *allow_nan*: true if NaN, Infinity, and -Infinity should be
502
- # generated, otherwise an exception is thrown, if these values are
503
- # encountered. This options defaults to false.
504
- # * *quirks_mode*: Enables quirks_mode for parser, that is for example
505
- # generating single JSON values instead of documents is possible.
506
- def initialize(opts = {})
507
- @indent = ''
508
- @space = ''
509
- @space_before = ''
510
- @object_nl = ''
511
- @array_nl = ''
512
- @allow_nan = false
513
- @ascii_only = false
514
- @quirks_mode = false
515
- @buffer_initial_length = 1024
516
- configure opts
517
- end
518
-
519
- # This string is used to indent levels in the JSON text.
520
- attr_accessor :indent
521
-
522
- # This string is used to insert a space between the tokens in a JSON
523
- # string.
524
- attr_accessor :space
525
-
526
- # This string is used to insert a space before the ':' in JSON objects.
527
- attr_accessor :space_before
528
-
529
- # This string is put at the end of a line that holds a JSON object (or
530
- # Hash).
531
- attr_accessor :object_nl
532
-
533
- # This string is put at the end of a line that holds a JSON array.
534
- attr_accessor :array_nl
535
-
536
- # This integer returns the maximum level of data structure nesting in
537
- # the generated JSON, max_nesting = 0 if no maximum is checked.
538
- attr_accessor :max_nesting
539
-
540
- # If this attribute is set to true, quirks mode is enabled, otherwise
541
- # it's disabled.
542
- attr_accessor :quirks_mode
543
-
544
- # :stopdoc:
545
- attr_reader :buffer_initial_length
546
-
547
- def buffer_initial_length=(length)
548
- if length > 0
549
- @buffer_initial_length = length
550
- end
551
- end
552
- # :startdoc:
553
-
554
- # This integer returns the current depth data structure nesting in the
555
- # generated JSON.
556
- attr_accessor :depth
557
-
558
- def check_max_nesting # :nodoc:
559
- return if @max_nesting.zero?
560
- current_nesting = depth + 1
561
- current_nesting > @max_nesting and
562
- raise NestingError, "nesting of #{current_nesting} is too deep"
563
- end
564
-
565
- # Returns true, if circular data structures are checked,
566
- # otherwise returns false.
567
- def check_circular?
568
- !@max_nesting.zero?
569
- end
570
-
571
- # Returns true if NaN, Infinity, and -Infinity should be considered as
572
- # valid JSON and output.
573
- def allow_nan?
574
- @allow_nan
575
- end
576
-
577
- # Returns true, if only ASCII characters should be generated. Otherwise
578
- # returns false.
579
- def ascii_only?
580
- @ascii_only
581
- end
582
-
583
- # Returns true, if quirks mode is enabled. Otherwise returns false.
584
- def quirks_mode?
585
- @quirks_mode
586
- end
587
-
588
- # Configure this State instance with the Hash _opts_, and return
589
- # itself.
590
- def configure(opts)
591
- @indent = opts[:indent] if opts.key?(:indent)
592
- @space = opts[:space] if opts.key?(:space)
593
- @space_before = opts[:space_before] if opts.key?(:space_before)
594
- @object_nl = opts[:object_nl] if opts.key?(:object_nl)
595
- @array_nl = opts[:array_nl] if opts.key?(:array_nl)
596
- @allow_nan = !!opts[:allow_nan] if opts.key?(:allow_nan)
597
- @ascii_only = opts[:ascii_only] if opts.key?(:ascii_only)
598
- @depth = opts[:depth] || 0
599
- @quirks_mode = opts[:quirks_mode] if opts.key?(:quirks_mode)
600
- if !opts.key?(:max_nesting) # defaults to 19
601
- @max_nesting = 19
602
- elsif opts[:max_nesting]
603
- @max_nesting = opts[:max_nesting]
604
- else
605
- @max_nesting = 0
606
- end
607
- self
608
- end
609
- alias merge configure
610
-
611
- # Returns the configuration instance variables as a hash, that can be
612
- # passed to the configure method.
613
- def to_h
614
- result = {}
615
- for iv in %w[indent space space_before object_nl array_nl allow_nan max_nesting ascii_only quirks_mode buffer_initial_length depth]
616
- result[iv.intern] = instance_variable_get("@#{iv}")
617
- end
618
- result
619
- end
620
-
621
- # Generates a valid JSON document from object +obj+ and returns the
622
- # result. If no valid JSON document can be created this method raises a
623
- # GeneratorError exception.
624
- def generate(obj)
625
- result = obj.to_json(self)
626
- unless @quirks_mode
627
- unless result =~ /\A\s*\[/ && result =~ /\]\s*\Z/ ||
628
- result =~ /\A\s*\{/ && result =~ /\}\s*\Z/
629
- then
630
- raise GeneratorError, "only generation of JSON objects or arrays allowed"
631
- end
632
- end
633
- result
634
- end
635
-
636
- # Return the value returned by method +name+.
637
- def [](name)
638
- __send__ name
639
- end
640
- end
641
-
642
- module GeneratorMethods
643
- module Object
644
- # Converts this object to a string (calling #to_s), converts
645
- # it to a JSON string, and returns the result. This is a fallback, if no
646
- # special method #to_json was defined for some object.
647
- def to_json(*) to_s.to_json end
648
- end
649
-
650
- module Hash
651
- # Returns a JSON string containing a JSON object, that is unparsed from
652
- # this Hash instance.
653
- # _state_ is a JSON::State object, that can also be used to configure the
654
- # produced JSON string output further.
655
- # _depth_ is used to find out nesting depth, to indent accordingly.
656
- def to_json(state = nil, *)
657
- state = State.from_state(state)
658
- state.check_max_nesting
659
- json_transform(state)
660
- end
661
-
662
- private
663
-
664
- def json_shift(state)
665
- state.object_nl.empty? or return ''
666
- state.indent * state.depth
667
- end
668
-
669
- def json_transform(state)
670
- delim = ','
671
- delim << state.object_nl
672
- result = '{'
673
- result << state.object_nl
674
- depth = state.depth += 1
675
- first = true
676
- indent = !state.object_nl.empty?
677
- each { |key,value|
678
- result << delim unless first
679
- result << state.indent * depth if indent
680
- result << key.to_s.to_json(state)
681
- result << state.space_before
682
- result << ':'
683
- result << state.space
684
- result << value.to_json(state)
685
- first = false
686
- }
687
- depth = state.depth -= 1
688
- result << state.object_nl
689
- result << state.indent * depth if indent if indent
690
- result << '}'
691
- result
692
- end
693
- end
694
-
695
- module Array
696
- # Returns a JSON string containing a JSON array, that is unparsed from
697
- # this Array instance.
698
- # _state_ is a JSON::State object, that can also be used to configure the
699
- # produced JSON string output further.
700
- def to_json(state = nil, *)
701
- state = State.from_state(state)
702
- state.check_max_nesting
703
- json_transform(state)
704
- end
705
-
706
- private
707
-
708
- def json_transform(state)
709
- delim = ','
710
- delim << state.array_nl
711
- result = '['
712
- result << state.array_nl
713
- depth = state.depth += 1
714
- first = true
715
- indent = !state.array_nl.empty?
716
- each { |value|
717
- result << delim unless first
718
- result << state.indent * depth if indent
719
- result << value.to_json(state)
720
- first = false
721
- }
722
- depth = state.depth -= 1
723
- result << state.array_nl
724
- result << state.indent * depth if indent
725
- result << ']'
726
- end
727
- end
728
-
729
- module Integer
730
- # Returns a JSON string representation for this Integer number.
731
- def to_json(*) to_s end
732
- end
733
-
734
- module Float
735
- # Returns a JSON string representation for this Float number.
736
- def to_json(state = nil, *)
737
- state = State.from_state(state)
738
- case
739
- when infinite?
740
- if state.allow_nan?
741
- to_s
742
- else
743
- raise GeneratorError, "#{self} not allowed in JSON"
744
- end
745
- when nan?
746
- if state.allow_nan?
747
- to_s
748
- else
749
- raise GeneratorError, "#{self} not allowed in JSON"
750
- end
751
- else
752
- to_s
753
- end
754
- end
755
- end
756
-
757
- module String
758
- if defined?(::Encoding)
759
- # This string should be encoded with UTF-8 A call to this method
760
- # returns a JSON string encoded with UTF16 big endian characters as
761
- # \u????.
762
- def to_json(state = nil, *args)
763
- state = State.from_state(state)
764
- if encoding == ::Encoding::UTF_8
765
- string = self
766
- else
767
- string = encode(::Encoding::UTF_8)
768
- end
769
- if state.ascii_only?
770
- '"' << JSON.utf8_to_json_ascii(string) << '"'
771
- else
772
- '"' << JSON.utf8_to_json(string) << '"'
773
- end
774
- end
775
- else
776
- # This string should be encoded with UTF-8 A call to this method
777
- # returns a JSON string encoded with UTF16 big endian characters as
778
- # \u????.
779
- def to_json(state = nil, *args)
780
- state = State.from_state(state)
781
- if state.ascii_only?
782
- '"' << JSON.utf8_to_json_ascii(self) << '"'
783
- else
784
- '"' << JSON.utf8_to_json(self) << '"'
785
- end
786
- end
787
- end
788
-
789
- # Module that holds the extinding methods if, the String module is
790
- # included.
791
- module Extend
792
- # Raw Strings are JSON Objects (the raw bytes are stored in an
793
- # array for the key "raw"). The Ruby String can be created by this
794
- # module method.
795
- def json_create(o)
796
- o['raw'].pack('C*')
797
- end
798
- end
799
-
800
- # Extends _modul_ with the String::Extend module.
801
- def self.included(modul)
802
- modul.extend Extend
803
- end
804
-
805
- # This method creates a raw object hash, that can be nested into
806
- # other data structures and will be unparsed as a raw string. This
807
- # method should be used, if you want to convert raw strings to JSON
808
- # instead of UTF-8 strings, e. g. binary data.
809
- def to_json_raw_object
810
- {
811
- JSON.create_id => self.class.name,
812
- 'raw' => self.unpack('C*'),
813
- }
814
- end
815
-
816
- # This method creates a JSON text from the result of
817
- # a call to to_json_raw_object of this String.
818
- def to_json_raw(*args)
819
- to_json_raw_object.to_json(*args)
820
- end
821
- end
822
-
823
- module TrueClass
824
- # Returns a JSON string for true: 'true'.
825
- def to_json(*) 'true' end
826
- end
827
-
828
- module FalseClass
829
- # Returns a JSON string for false: 'false'.
830
- def to_json(*) 'false' end
831
- end
832
-
833
- module NilClass
834
- # Returns a JSON string for nil: 'null'.
835
- def to_json(*) 'null' end
836
- end
837
- end
838
- end
839
- end
840
- end
841
-
842
- module JSON
843
- class << self
844
- # If _object_ is string-like, parse the string and return the parsed result
845
- # as a Ruby data structure. Otherwise generate a JSON text from the Ruby
846
- # data structure object and return it.
847
- #
848
- # The _opts_ argument is passed through to generate/parse respectively. See
849
- # generate and parse for their documentation.
850
- def [](object, opts = {})
851
- if object.respond_to? :to_str
852
- JSON.parse(object.to_str, opts)
853
- else
854
- JSON.generate(object, opts)
855
- end
856
- end
857
-
858
- # Returns the JSON parser class that is used by JSON. This is either
859
- # JSON::Ext::Parser or JSON::Pure::Parser.
860
- attr_reader :parser
861
-
862
- # Set the JSON parser class _parser_ to be used by JSON.
863
- def parser=(parser) # :nodoc:
864
- @parser = parser
865
- remove_const :Parser if JSON.const_defined_in?(self, :Parser)
866
- const_set :Parser, parser
867
- end
868
-
869
- # Return the constant located at _path_. The format of _path_ has to be
870
- # either ::A::B::C or A::B::C. In any case, A has to be located at the top
871
- # level (absolute namespace path?). If there doesn't exist a constant at
872
- # the given path, an ArgumentError is raised.
873
- def deep_const_get(path) # :nodoc:
874
- path.to_s.split(/::/).inject(Object) do |p, c|
875
- case
876
- when c.empty? then p
877
- when JSON.const_defined_in?(p, c) then p.const_get(c)
878
- else
879
- begin
880
- p.const_missing(c)
881
- rescue NameError => e
882
- raise ArgumentError, "can't get const #{path}: #{e}"
883
- end
884
- end
885
- end
886
- end
887
-
888
- # Set the module _generator_ to be used by JSON.
889
- def generator=(generator) # :nodoc:
890
- old, $VERBOSE = $VERBOSE, nil
891
- @generator = generator
892
- generator_methods = generator::GeneratorMethods
893
- for const in generator_methods.constants
894
- klass = deep_const_get(const)
895
- modul = generator_methods.const_get(const)
896
- klass.class_eval do
897
- instance_methods(false).each do |m|
898
- m.to_s == 'to_json' and remove_method m
899
- end
900
- include modul
901
- end
902
- end
903
- self.state = generator::State
904
- const_set :State, self.state
905
- const_set :SAFE_STATE_PROTOTYPE, State.new
906
- const_set :FAST_STATE_PROTOTYPE, State.new(
907
- :indent => '',
908
- :space => '',
909
- :object_nl => "",
910
- :array_nl => "",
911
- :max_nesting => false
912
- )
913
- const_set :PRETTY_STATE_PROTOTYPE, State.new(
914
- :indent => ' ',
915
- :space => ' ',
916
- :object_nl => "\n",
917
- :array_nl => "\n"
918
- )
919
- ensure
920
- $VERBOSE = old
921
- end
922
-
923
- # Returns the JSON generator module that is used by JSON. This is
924
- # either JSON::Ext::Generator or JSON::Pure::Generator.
925
- attr_reader :generator
926
-
927
- # Returns the JSON generator state class that is used by JSON. This is
928
- # either JSON::Ext::Generator::State or JSON::Pure::Generator::State.
929
- attr_accessor :state
930
-
931
- # This is create identifier, which is used to decide if the _json_create_
932
- # hook of a class should be called. It defaults to 'json_class'.
933
- attr_accessor :create_id
934
- end
935
- self.create_id = 'json_class'
936
-
937
- NaN = 0.0/0
938
-
939
- Infinity = 1.0/0
940
-
941
- MinusInfinity = -Infinity
942
-
943
- # The base exception for JSON errors.
944
- class JSONError < StandardError; end
945
-
946
- # This exception is raised if a parser error occurs.
947
- class ParserError < JSONError; end
948
-
949
- # This exception is raised if the nesting of parsed data structures is too
950
- # deep.
951
- class NestingError < ParserError; end
952
-
953
- # :stopdoc:
954
- class CircularDatastructure < NestingError; end
955
- # :startdoc:
956
-
957
- # This exception is raised if a generator or unparser error occurs.
958
- class GeneratorError < JSONError; end
959
- # For backwards compatibility
960
- UnparserError = GeneratorError
961
-
962
- # This exception is raised if the required unicode support is missing on the
963
- # system. Usually this means that the iconv library is not installed.
964
- class MissingUnicodeSupport < JSONError; end
965
-
966
- module_function
967
-
968
- # Parse the JSON document _source_ into a Ruby data structure and return it.
969
- #
970
- # _opts_ can have the following
971
- # keys:
972
- # * *max_nesting*: The maximum depth of nesting allowed in the parsed data
973
- # structures. Disable depth checking with :max_nesting => false. It defaults
974
- # to 19.
975
- # * *allow_nan*: If set to true, allow NaN, Infinity and -Infinity in
976
- # defiance of RFC 4627 to be parsed by the Parser. This option defaults
977
- # to false.
978
- # * *symbolize_names*: If set to true, returns symbols for the names
979
- # (keys) in a JSON object. Otherwise strings are returned. Strings are
980
- # the default.
981
- # * *create_additions*: If set to false, the Parser doesn't create
982
- # additions even if a matching class and create_id was found. This option
983
- # defaults to true.
984
- # * *object_class*: Defaults to Hash
985
- # * *array_class*: Defaults to Array
986
- def parse(source, opts = {})
987
- Parser.new(source, opts).parse
988
- end
989
-
990
- # Parse the JSON document _source_ into a Ruby data structure and return it.
991
- # The bang version of the parse method defaults to the more dangerous values
992
- # for the _opts_ hash, so be sure only to parse trusted _source_ documents.
993
- #
994
- # _opts_ can have the following keys:
995
- # * *max_nesting*: The maximum depth of nesting allowed in the parsed data
996
- # structures. Enable depth checking with :max_nesting => anInteger. The parse!
997
- # methods defaults to not doing max depth checking: This can be dangerous
998
- # if someone wants to fill up your stack.
999
- # * *allow_nan*: If set to true, allow NaN, Infinity, and -Infinity in
1000
- # defiance of RFC 4627 to be parsed by the Parser. This option defaults
1001
- # to true.
1002
- # * *create_additions*: If set to false, the Parser doesn't create
1003
- # additions even if a matching class and create_id was found. This option
1004
- # defaults to true.
1005
- def parse!(source, opts = {})
1006
- opts = {
1007
- :max_nesting => false,
1008
- :allow_nan => true
1009
- }.update(opts)
1010
- Parser.new(source, opts).parse
1011
- end
1012
-
1013
- # Generate a JSON document from the Ruby data structure _obj_ and return
1014
- # it. _state_ is * a JSON::State object,
1015
- # * or a Hash like object (responding to to_hash),
1016
- # * an object convertible into a hash by a to_h method,
1017
- # that is used as or to configure a State object.
1018
- #
1019
- # It defaults to a state object, that creates the shortest possible JSON text
1020
- # in one line, checks for circular data structures and doesn't allow NaN,
1021
- # Infinity, and -Infinity.
1022
- #
1023
- # A _state_ hash can have the following keys:
1024
- # * *indent*: a string used to indent levels (default: ''),
1025
- # * *space*: a string that is put after, a : or , delimiter (default: ''),
1026
- # * *space_before*: a string that is put before a : pair delimiter (default: ''),
1027
- # * *object_nl*: a string that is put at the end of a JSON object (default: ''),
1028
- # * *array_nl*: a string that is put at the end of a JSON array (default: ''),
1029
- # * *allow_nan*: true if NaN, Infinity, and -Infinity should be
1030
- # generated, otherwise an exception is thrown if these values are
1031
- # encountered. This options defaults to false.
1032
- # * *max_nesting*: The maximum depth of nesting allowed in the data
1033
- # structures from which JSON is to be generated. Disable depth checking
1034
- # with :max_nesting => false, it defaults to 19.
1035
- #
1036
- # See also the fast_generate for the fastest creation method with the least
1037
- # amount of sanity checks, and the pretty_generate method for some
1038
- # defaults for pretty output.
1039
- def generate(obj, opts = nil)
1040
- if State === opts
1041
- state, opts = opts, nil
1042
- else
1043
- state = SAFE_STATE_PROTOTYPE.dup
1044
- end
1045
- if opts
1046
- if opts.respond_to? :to_hash
1047
- opts = opts.to_hash
1048
- elsif opts.respond_to? :to_h
1049
- opts = opts.to_h
1050
- else
1051
- raise TypeError, "can't convert #{opts.class} into Hash"
1052
- end
1053
- state = state.configure(opts)
1054
- end
1055
- state.generate(obj)
1056
- end
1057
-
1058
- # :stopdoc:
1059
- # I want to deprecate these later, so I'll first be silent about them, and
1060
- # later delete them.
1061
- alias unparse generate
1062
- module_function :unparse
1063
- # :startdoc:
1064
-
1065
- # Generate a JSON document from the Ruby data structure _obj_ and return it.
1066
- # This method disables the checks for circles in Ruby objects.
1067
- #
1068
- # *WARNING*: Be careful not to pass any Ruby data structures with circles as
1069
- # _obj_ argument because this will cause JSON to go into an infinite loop.
1070
- def fast_generate(obj, opts = nil)
1071
- if State === opts
1072
- state, opts = opts, nil
1073
- else
1074
- state = FAST_STATE_PROTOTYPE.dup
1075
- end
1076
- if opts
1077
- if opts.respond_to? :to_hash
1078
- opts = opts.to_hash
1079
- elsif opts.respond_to? :to_h
1080
- opts = opts.to_h
1081
- else
1082
- raise TypeError, "can't convert #{opts.class} into Hash"
1083
- end
1084
- state.configure(opts)
1085
- end
1086
- state.generate(obj)
1087
- end
1088
-
1089
- # :stopdoc:
1090
- # I want to deprecate these later, so I'll first be silent about them, and later delete them.
1091
- alias fast_unparse fast_generate
1092
- module_function :fast_unparse
1093
- # :startdoc:
1094
-
1095
- # Generate a JSON document from the Ruby data structure _obj_ and return it.
1096
- # The returned document is a prettier form of the document returned by
1097
- # #unparse.
1098
- #
1099
- # The _opts_ argument can be used to configure the generator. See the
1100
- # generate method for a more detailed explanation.
1101
- def pretty_generate(obj, opts = nil)
1102
- if State === opts
1103
- state, opts = opts, nil
1104
- else
1105
- state = PRETTY_STATE_PROTOTYPE.dup
1106
- end
1107
- if opts
1108
- if opts.respond_to? :to_hash
1109
- opts = opts.to_hash
1110
- elsif opts.respond_to? :to_h
1111
- opts = opts.to_h
1112
- else
1113
- raise TypeError, "can't convert #{opts.class} into Hash"
1114
- end
1115
- state.configure(opts)
1116
- end
1117
- state.generate(obj)
1118
- end
1119
-
1120
- # :stopdoc:
1121
- # I want to deprecate these later, so I'll first be silent about them, and later delete them.
1122
- alias pretty_unparse pretty_generate
1123
- module_function :pretty_unparse
1124
- # :startdoc:
1125
-
1126
- class << self
1127
- # The global default options for the JSON.load method:
1128
- # :max_nesting: false
1129
- # :allow_nan: true
1130
- # :quirks_mode: true
1131
- attr_accessor :load_default_options
1132
- end
1133
- self.load_default_options = {
1134
- :max_nesting => false,
1135
- :allow_nan => true,
1136
- :quirks_mode => true,
1137
- }
1138
-
1139
- # Load a ruby data structure from a JSON _source_ and return it. A source can
1140
- # either be a string-like object, an IO-like object, or an object responding
1141
- # to the read method. If _proc_ was given, it will be called with any nested
1142
- # Ruby object as an argument recursively in depth first order. The default
1143
- # options for the parser can be changed via the load_default_options method.
1144
- #
1145
- # This method is part of the implementation of the load/dump interface of
1146
- # Marshal and YAML.
1147
- def load(source, proc = nil)
1148
- opts = load_default_options
1149
- if source.respond_to? :to_str
1150
- source = source.to_str
1151
- elsif source.respond_to? :to_io
1152
- source = source.to_io.read
1153
- elsif source.respond_to?(:read)
1154
- source = source.read
1155
- end
1156
- if opts[:quirks_mode] && (source.nil? || source.empty?)
1157
- source = 'null'
1158
- end
1159
- result = parse(source, opts)
1160
- recurse_proc(result, &proc) if proc
1161
- result
1162
- end
1163
-
1164
- # Recursively calls passed _Proc_ if the parsed data structure is an _Array_ or _Hash_
1165
- def recurse_proc(result, &proc)
1166
- case result
1167
- when Array
1168
- result.each { |x| recurse_proc x, &proc }
1169
- proc.call result
1170
- when Hash
1171
- result.each { |x, y| recurse_proc x, &proc; recurse_proc y, &proc }
1172
- proc.call result
1173
- else
1174
- proc.call result
1175
- end
1176
- end
1177
-
1178
- alias restore load
1179
- module_function :restore
1180
-
1181
- class << self
1182
- # The global default options for the JSON.dump method:
1183
- # :max_nesting: false
1184
- # :allow_nan: true
1185
- # :quirks_mode: true
1186
- attr_accessor :dump_default_options
1187
- end
1188
- self.dump_default_options = {
1189
- :max_nesting => false,
1190
- :allow_nan => true,
1191
- :quirks_mode => true,
1192
- }
1193
-
1194
- # Dumps _obj_ as a JSON string, i.e. calls generate on the object and returns
1195
- # the result.
1196
- #
1197
- # If anIO (an IO-like object or an object that responds to the write method)
1198
- # was given, the resulting JSON is written to it.
1199
- #
1200
- # If the number of nested arrays or objects exceeds _limit_, an ArgumentError
1201
- # exception is raised. This argument is similar (but not exactly the
1202
- # same!) to the _limit_ argument in Marshal.dump.
1203
- #
1204
- # The default options for the generator can be changed via the
1205
- # dump_default_options method.
1206
- #
1207
- # This method is part of the implementation of the load/dump interface of
1208
- # Marshal and YAML.
1209
- def dump(obj, anIO = nil, limit = nil)
1210
- if anIO and limit.nil?
1211
- anIO = anIO.to_io if anIO.respond_to?(:to_io)
1212
- unless anIO.respond_to?(:write)
1213
- limit = anIO
1214
- anIO = nil
1215
- end
1216
- end
1217
- opts = JSON.dump_default_options
1218
- limit and opts.update(:max_nesting => limit)
1219
- result = generate(obj, opts)
1220
- if anIO
1221
- anIO.write result
1222
- anIO
1223
- else
1224
- result
1225
- end
1226
- rescue JSON::NestingError
1227
- raise ArgumentError, "exceed depth limit"
1228
- end
1229
-
1230
- # Swap consecutive bytes of _string_ in place.
1231
- def self.swap!(string) # :nodoc:
1232
- 0.upto(string.size / 2) do |i|
1233
- break unless string[2 * i + 1]
1234
- string[2 * i], string[2 * i + 1] = string[2 * i + 1], string[2 * i]
1235
- end
1236
- string
1237
- end
1238
-
1239
- # Shortuct for iconv.
1240
- if ::String.method_defined?(:encode)
1241
- # Encodes string using Ruby's _String.encode_
1242
- def self.iconv(to, from, string)
1243
- string.encode(to, from)
1244
- end
1245
- else
1246
- require 'iconv'
1247
- # Encodes string using _iconv_ library
1248
- def self.iconv(to, from, string)
1249
- Iconv.conv(to, from, string)
1250
- end
1251
- end
1252
-
1253
- if ::Object.method(:const_defined?).arity == 1
1254
- def self.const_defined_in?(modul, constant)
1255
- modul.const_defined?(constant)
1256
- end
1257
- else
1258
- def self.const_defined_in?(modul, constant)
1259
- modul.const_defined?(constant, false)
1260
- end
1261
- end
1262
- end
1263
-
1264
- module ::Kernel
1265
- private
1266
-
1267
- # Outputs _objs_ to STDOUT as JSON strings in the shortest form, that is in
1268
- # one line.
1269
- def j(*objs)
1270
- objs.each do |obj|
1271
- puts JSON::generate(obj, :allow_nan => true, :max_nesting => false)
1272
- end
1273
- nil
1274
- end
1275
-
1276
- # Ouputs _objs_ to STDOUT as JSON strings in a pretty format, with
1277
- # indentation and over many lines.
1278
- def jj(*objs)
1279
- objs.each do |obj|
1280
- puts JSON::pretty_generate(obj, :allow_nan => true, :max_nesting => false)
1281
- end
1282
- nil
1283
- end
1284
-
1285
- # If _object_ is string-like, parse the string and return the parsed result as
1286
- # a Ruby data structure. Otherwise, generate a JSON text from the Ruby data
1287
- # structure object and return it.
1288
- #
1289
- # The _opts_ argument is passed through to generate/parse respectively. See
1290
- # generate and parse for their documentation.
1291
- def JSON(object, *args)
1292
- if object.respond_to? :to_str
1293
- JSON.parse(object.to_str, args.first)
1294
- else
1295
- JSON.generate(object, args.first)
1296
- end
1297
- end
1298
- end
1299
-
1300
- # Extends any Class to include _json_creatable?_ method.
1301
- class ::Class
1302
- # Returns true if this class can be used to create an instance
1303
- # from a serialised JSON string. The class has to implement a class
1304
- # method _json_create_ that expects a hash as first parameter. The hash
1305
- # should include the required data.
1306
- def json_creatable?
1307
- respond_to?(:json_create)
1308
- end
1309
- end
1310
-
1311
- JSON.generator = JSON::Pure::Generator
1312
- JSON.parser = JSON::Pure::Parser
1313
- rescue LoadError
1314
- require File.join File.dirname(File.dirname(__FILE__)), 'vendor', 'json.rb'
1315
- end
1316
-
1317
- # It just gists.
1318
- module Gist
1319
- extend self
1320
-
1321
- VERSION = '5.1.0'
1322
-
1323
- # A list of clipboard commands with copy and paste support.
1324
- CLIPBOARD_COMMANDS = {
1325
- 'pbcopy' => 'pbpaste',
1326
- 'xclip' => 'xclip -o',
1327
- 'xsel -i' => 'xsel -o',
1328
- 'putclip' => 'getclip',
1329
- }
1330
-
1331
- GITHUB_API_URL = URI("https://api.github.com/")
1332
- GIT_IO_URL = URI("https://git.io")
1333
-
1334
- GITHUB_BASE_PATH = ""
1335
- GHE_BASE_PATH = "/api/v3"
1336
-
1337
- URL_ENV_NAME = "GITHUB_URL"
1338
-
1339
- USER_AGENT = "gist/#{VERSION} (Net::HTTP, #{RUBY_DESCRIPTION})"
1340
-
1341
- # Exception tag for errors raised while gisting.
1342
- module Error;
1343
- def self.exception(*args)
1344
- RuntimeError.new(*args).extend(self)
1345
- end
1346
- end
1347
- class ClipboardError < RuntimeError; include Error end
1348
-
1349
- # helper module for authentication token actions
1350
- module AuthTokenFile
1351
- def self.filename
1352
- if ENV.key?(URL_ENV_NAME)
1353
- File.expand_path "~/.gist.#{ENV[URL_ENV_NAME].gsub(/:/, '.').gsub(/[^a-z0-9.]/, '')}"
1354
- else
1355
- File.expand_path "~/.gist"
1356
- end
1357
- end
1358
-
1359
- def self.read
1360
- File.read(filename).chomp
1361
- end
1362
-
1363
- def self.write(token)
1364
- File.open(filename, 'w', 0600) do |f|
1365
- f.write token
1366
- end
1367
- end
1368
- end
1369
-
1370
- # auth token for authentication
1371
- #
1372
- # @return [String] string value of access token or `nil`, if not found
1373
- def auth_token
1374
- @token ||= AuthTokenFile.read rescue nil
1375
- end
1376
-
1377
- # Upload a gist to https://gist.github.com
1378
- #
1379
- # @param [String] content the code you'd like to gist
1380
- # @param [Hash] options more detailed options, see
1381
- # the documentation for {multi_gist}
1382
- #
1383
- # @see http://developer.github.com/v3/gists/
1384
- def gist(content, options = {})
1385
- filename = options[:filename] || default_filename
1386
- multi_gist({filename => content}, options)
1387
- end
1388
-
1389
- def default_filename
1390
- "gistfile1.txt"
1391
- end
1392
-
1393
- # Upload a gist to https://gist.github.com
1394
- #
1395
- # @param [Hash] files the code you'd like to gist: filename => content
1396
- # @param [Hash] options more detailed options
1397
- #
1398
- # @option options [String] :description the description
1399
- # @option options [Boolean] :public (false) is this gist public
1400
- # @option options [Boolean] :anonymous (false) is this gist anonymous
1401
- # @option options [String] :access_token (`File.read("~/.gist")`) The OAuth2 access token.
1402
- # @option options [String] :update the URL or id of a gist to update
1403
- # @option options [Boolean] :copy (false) Copy resulting URL to clipboard, if successful.
1404
- # @option options [Boolean] :open (false) Open the resulting URL in a browser.
1405
- # @option options [Boolean] :skip_empty (false) Skip gisting empty files.
1406
- # @option options [Symbol] :output (:all) The type of return value you'd like:
1407
- # :html_url gives a String containing the url to the gist in a browser
1408
- # :short_url gives a String contianing a git.io url that redirects to html_url
1409
- # :javascript gives a String containing a script tag suitable for embedding the gist
1410
- # :all gives a Hash containing the parsed json response from the server
1411
- #
1412
- # @return [String, Hash] the return value as configured by options[:output]
1413
- # @raise [Gist::Error] if something went wrong
1414
- #
1415
- # @see http://developer.github.com/v3/gists/
1416
- def multi_gist(files, options={})
1417
- if options[:anonymous]
1418
- raise 'Anonymous gists are no longer supported. Please log in with `gist --login`. ' \
1419
- '(GitHub now requires credentials to gist https://bit.ly/2GBBxKw)'
1420
- else
1421
- access_token = (options[:access_token] || auth_token())
1422
- end
1423
-
1424
- json = {}
1425
-
1426
- json[:description] = options[:description] if options[:description]
1427
- json[:public] = !!options[:public]
1428
- json[:files] = {}
1429
-
1430
- files.each_pair do |(name, content)|
1431
- if content.to_s.strip == ""
1432
- raise "Cannot gist empty files" unless options[:skip_empty]
1433
- else
1434
- name = name == "-" ? default_filename : File.basename(name)
1435
- json[:files][name] = {:content => content}
1436
- end
1437
- end
1438
-
1439
- return if json[:files].empty? && options[:skip_empty]
1440
-
1441
- existing_gist = options[:update].to_s.split("/").last
1442
-
1443
- url = "#{base_path}/gists"
1444
- url << "/" << CGI.escape(existing_gist) if existing_gist.to_s != ''
1445
-
1446
- request = Net::HTTP::Post.new(url)
1447
- request['Authorization'] = "token #{access_token}" if access_token.to_s != ''
1448
- request.body = JSON.dump(json)
1449
- request.content_type = 'application/json'
1450
-
1451
- retried = false
1452
-
1453
- begin
1454
- response = http(api_url, request)
1455
- if Net::HTTPSuccess === response
1456
- on_success(response.body, options)
1457
- else
1458
- raise "Got #{response.class} from gist: #{response.body}"
1459
- end
1460
- rescue => e
1461
- raise if retried
1462
- retried = true
1463
- retry
1464
- end
1465
-
1466
- rescue => e
1467
- raise e.extend Error
1468
- end
1469
-
1470
- # List all gists(private also) for authenticated user
1471
- # otherwise list public gists for given username (optional argument)
1472
- #
1473
- # @param [String] user
1474
- # @deprecated
1475
- #
1476
- # see https://developer.github.com/v3/gists/#list-gists
1477
- def list_gists(user = "")
1478
- url = "#{base_path}"
1479
-
1480
- if user == ""
1481
- access_token = auth_token()
1482
- if access_token.to_s != ''
1483
- url << "/gists"
1484
-
1485
- request = Net::HTTP::Get.new(url)
1486
- request['Authorization'] = "token #{access_token}"
1487
- response = http(api_url, request)
1488
-
1489
- pretty_gist(response)
1490
-
1491
- else
1492
- raise Error, "Not authenticated. Use 'gist --login' to login or 'gist -l username' to view public gists."
1493
- end
1494
-
1495
- else
1496
- url << "/users/#{user}/gists"
1497
-
1498
- request = Net::HTTP::Get.new(url)
1499
- response = http(api_url, request)
1500
-
1501
- pretty_gist(response)
1502
- end
1503
- end
1504
-
1505
- def list_all_gists(user = "")
1506
- url = "#{base_path}"
1507
-
1508
- if user == ""
1509
- access_token = auth_token()
1510
- if access_token.to_s != ''
1511
- url << "/gists?per_page=100"
1512
- get_gist_pages(url, access_token)
1513
- else
1514
- raise Error, "Not authenticated. Use 'gist --login' to login or 'gist -l username' to view public gists."
1515
- end
1516
-
1517
- else
1518
- url << "/users/#{user}/gists?per_page=100"
1519
- get_gist_pages(url)
1520
- end
1521
-
1522
- end
1523
-
1524
- def read_gist(id, file_name=nil)
1525
- url = "#{base_path}/gists/#{id}"
1526
-
1527
- access_token = auth_token()
1528
-
1529
- request = Net::HTTP::Get.new(url)
1530
- request['Authorization'] = "token #{access_token}" if access_token.to_s != ''
1531
- response = http(api_url, request)
1532
-
1533
- if response.code == '200'
1534
- body = JSON.parse(response.body)
1535
- files = body["files"]
1536
-
1537
- if file_name
1538
- file = files[file_name]
1539
- raise Error, "Gist with id of #{id} and file #{file_name} does not exist." unless file
1540
- else
1541
- file = files.values.first
1542
- end
1543
-
1544
- puts file["content"]
1545
- else
1546
- raise Error, "Gist with id of #{id} does not exist."
1547
- end
1548
- end
1549
-
1550
- def delete_gist(id)
1551
- id = id.split("/").last
1552
- url = "#{base_path}/gists/#{id}"
1553
-
1554
- access_token = auth_token()
1555
- if access_token.to_s != ''
1556
- request = Net::HTTP::Delete.new(url)
1557
- request["Authorization"] = "token #{access_token}"
1558
- response = http(api_url, request)
1559
- else
1560
- raise Error, "Not authenticated. Use 'gist --login' to login."
1561
- end
1562
-
1563
- if response.code == '204'
1564
- puts "Deleted!"
1565
- else
1566
- raise Error, "Gist with id of #{id} does not exist."
1567
- end
1568
- end
1569
-
1570
- def get_gist_pages(url, access_token = "")
1571
-
1572
- request = Net::HTTP::Get.new(url)
1573
- request['Authorization'] = "token #{access_token}" if access_token.to_s != ''
1574
- response = http(api_url, request)
1575
- pretty_gist(response)
1576
-
1577
- link_header = response.header['link']
1578
-
1579
- if link_header
1580
- links = Hash[ link_header.gsub(/(<|>|")/, "").split(',').map { |link| link.split('; rel=') } ].invert
1581
- get_gist_pages(links['next'], access_token) if links['next']
1582
- end
1583
-
1584
- end
1585
-
1586
- # return prettified string result of response body for all gists
1587
- #
1588
- # @params [Net::HTTPResponse] response
1589
- # @return [String] prettified result of listing all gists
1590
- #
1591
- # see https://developer.github.com/v3/gists/#response
1592
- def pretty_gist(response)
1593
- body = JSON.parse(response.body)
1594
- if response.code == '200'
1595
- body.each do |gist|
1596
- description = "#{gist['description'] || gist['files'].keys.join(" ")} #{gist['public'] ? '' : '(secret)'}"
1597
- puts "#{gist['html_url']} #{description.tr("\n", " ")}\n"
1598
- $stdout.flush
1599
- end
1600
-
1601
- else
1602
- raise Error, body['message']
1603
- end
1604
- end
1605
-
1606
- # Convert long github urls into short git.io ones
1607
- #
1608
- # @param [String] url
1609
- # @return [String] shortened url, or long url if shortening fails
1610
- def shorten(url)
1611
- request = Net::HTTP::Post.new("/create")
1612
- request.set_form_data(:url => url)
1613
- response = http(GIT_IO_URL, request)
1614
- case response.code
1615
- when "200"
1616
- URI.join(GIT_IO_URL, response.body).to_s
1617
- when "201"
1618
- response['Location']
1619
- else
1620
- url
1621
- end
1622
- end
1623
-
1624
- # Convert github url into raw file url
1625
- #
1626
- # Unfortunately the url returns from github's api is legacy,
1627
- # we have to taking a HTTPRedirection before appending it
1628
- # with '/raw'. Let's looking forward for github's api fix :)
1629
- #
1630
- # @param [String] url
1631
- # @return [String] the raw file url
1632
- def rawify(url)
1633
- uri = URI(url)
1634
- request = Net::HTTP::Get.new(uri.path)
1635
- response = http(uri, request)
1636
- if Net::HTTPSuccess === response
1637
- url + '/raw'
1638
- elsif Net::HTTPRedirection === response
1639
- rawify(response.header['location'])
1640
- end
1641
- end
1642
-
1643
- # Log the user into gist.
1644
- #
1645
- # This method asks the user for a username and password, and tries to obtain
1646
- # and OAuth2 access token, which is then stored in ~/.gist
1647
- #
1648
- # @raise [Gist::Error] if something went wrong
1649
- # @param [Hash] credentials login details
1650
- # @option credentials [String] :username
1651
- # @option credentials [String] :password
1652
- # @see http://developer.github.com/v3/oauth/
1653
- def login!(credentials={})
1654
- puts "Obtaining OAuth2 access_token from GitHub."
1655
- loop do
1656
- print "GitHub username: "
1657
- username = credentials[:username] || $stdin.gets.strip
1658
- print "GitHub password: "
1659
- password = credentials[:password] || begin
1660
- `stty -echo` rescue nil
1661
- $stdin.gets.strip
1662
- ensure
1663
- `stty echo` rescue nil
1664
- end
1665
- puts ""
1666
-
1667
- request = Net::HTTP::Post.new("#{base_path}/authorizations")
1668
- request.body = JSON.dump({
1669
- :scopes => [:gist],
1670
- :note => "The gist gem (#{Time.now})",
1671
- :note_url => "https://github.com/ConradIrwin/gist"
1672
- })
1673
- request.content_type = 'application/json'
1674
- request.basic_auth(username, password)
1675
-
1676
- response = http(api_url, request)
1677
-
1678
- if Net::HTTPUnauthorized === response && response['X-GitHub-OTP']
1679
- print "2-factor auth code: "
1680
- twofa_code = $stdin.gets.strip
1681
- puts ""
1682
-
1683
- request['X-GitHub-OTP'] = twofa_code
1684
- response = http(api_url, request)
1685
- end
1686
-
1687
- if Net::HTTPCreated === response
1688
- AuthTokenFile.write JSON.parse(response.body)['token']
1689
- puts "Success! #{ENV[URL_ENV_NAME] || "https://github.com/"}settings/tokens"
1690
- return
1691
- elsif Net::HTTPUnauthorized === response
1692
- puts "Error: #{JSON.parse(response.body)['message']}"
1693
- next
1694
- else
1695
- raise "Got #{response.class} from gist: #{response.body}"
1696
- end
1697
- end
1698
- rescue => e
1699
- raise e.extend Error
1700
- end
1701
-
1702
- # Return HTTP connection
1703
- #
1704
- # @param [URI::HTTP] The URI to which to connect
1705
- # @return [Net::HTTP]
1706
- def http_connection(uri)
1707
- env = ENV['http_proxy'] || ENV['HTTP_PROXY']
1708
- connection = if env
1709
- proxy = URI(env)
1710
- Net::HTTP::Proxy(proxy.host, proxy.port).new(uri.host, uri.port)
1711
- else
1712
- Net::HTTP.new(uri.host, uri.port)
1713
- end
1714
- if uri.scheme == "https"
1715
- connection.use_ssl = true
1716
- connection.verify_mode = OpenSSL::SSL::VERIFY_NONE
1717
- end
1718
- connection.open_timeout = 10
1719
- connection.read_timeout = 10
1720
- connection
1721
- end
1722
-
1723
- # Run an HTTP operation
1724
- #
1725
- # @param [URI::HTTP] The URI to which to connect
1726
- # @param [Net::HTTPRequest] The request to make
1727
- # @return [Net::HTTPResponse]
1728
- def http(url, request)
1729
- request['User-Agent'] = USER_AGENT
1730
-
1731
- http_connection(url).start do |http|
1732
- http.request request
1733
- end
1734
- rescue Timeout::Error
1735
- raise "Could not connect to #{api_url}"
1736
- end
1737
-
1738
- # Called after an HTTP response to gist to perform post-processing.
1739
- #
1740
- # @param [String] body the text body from the github api
1741
- # @param [Hash] options more detailed options, see
1742
- # the documentation for {multi_gist}
1743
- def on_success(body, options={})
1744
- json = JSON.parse(body)
1745
-
1746
- output = case options[:output]
1747
- when :javascript
1748
- %Q{<script src="#{json['html_url']}.js"></script>}
1749
- when :html_url
1750
- json['html_url']
1751
- when :raw_url
1752
- rawify(json['html_url'])
1753
- when :short_url
1754
- shorten(json['html_url'])
1755
- when :short_raw_url
1756
- shorten(rawify(json['html_url']))
1757
- else
1758
- json
1759
- end
1760
-
1761
- Gist.copy(output.to_s) if options[:copy]
1762
- Gist.open(json['html_url']) if options[:open]
1763
-
1764
- output
1765
- end
1766
-
1767
- # Copy a string to the clipboard.
1768
- #
1769
- # @param [String] content
1770
- # @raise [Gist::Error] if no clipboard integration could be found
1771
- #
1772
- def copy(content)
1773
- IO.popen(clipboard_command(:copy), 'r+') { |clip| clip.print content }
1774
-
1775
- unless paste == content
1776
- message = 'Copying to clipboard failed.'
1777
-
1778
- if ENV["TMUX"] && clipboard_command(:copy) == 'pbcopy'
1779
- message << "\nIf you're running tmux on a mac, try http://robots.thoughtbot.com/post/19398560514/how-to-copy-and-paste-with-tmux-on-mac-os-x"
1780
- end
1781
-
1782
- raise Error, message
1783
- end
1784
- rescue Error => e
1785
- raise ClipboardError, e.message + "\nAttempted to copy: #{content}"
1786
- end
1787
-
1788
- # Get a string from the clipboard.
1789
- #
1790
- # @param [String] content
1791
- # @raise [Gist::Error] if no clipboard integration could be found
1792
- def paste
1793
- `#{clipboard_command(:paste)}`
1794
- end
1795
-
1796
- # Find command from PATH environment.
1797
- #
1798
- # @param [String] cmd command name to find
1799
- # @param [String] options PATH environment variable
1800
- # @return [String] the command found
1801
- def which(cmd, path=ENV['PATH'])
1802
- if RUBY_PLATFORM.downcase =~ /mswin(?!ce)|mingw|bccwin|cygwin/
1803
- path.split(File::PATH_SEPARATOR).each {|dir|
1804
- f = File.join(dir, cmd+".exe")
1805
- return f if File.executable?(f) && !File.directory?(f)
1806
- }
1807
- nil
1808
- else
1809
- return system("which #{cmd} > /dev/null 2>&1")
1810
- end
1811
- end
1812
-
1813
- # Get the command to use for the clipboard action.
1814
- #
1815
- # @param [Symbol] action either :copy or :paste
1816
- # @return [String] the command to run
1817
- # @raise [Gist::ClipboardError] if no clipboard integration could be found
1818
- def clipboard_command(action)
1819
- command = CLIPBOARD_COMMANDS.keys.detect do |cmd|
1820
- which cmd
1821
- end
1822
- raise ClipboardError, <<-EOT unless command
1823
- Could not find copy command, tried:
1824
- #{CLIPBOARD_COMMANDS.values.join(' || ')}
1825
- EOT
1826
- action == :copy ? command : CLIPBOARD_COMMANDS[command]
1827
- end
1828
-
1829
- # Open a URL in a browser.
1830
- #
1831
- # @param [String] url
1832
- # @raise [RuntimeError] if no browser integration could be found
1833
- #
1834
- # This method was heavily inspired by defunkt's Gist#open,
1835
- # @see https://github.com/defunkt/gist/blob/bca9b29/lib/gist.rb#L157
1836
- def open(url)
1837
- command = if ENV['BROWSER']
1838
- ENV['BROWSER']
1839
- elsif RUBY_PLATFORM =~ /darwin/
1840
- 'open'
1841
- elsif RUBY_PLATFORM =~ /linux/
1842
- %w(
1843
- sensible-browser
1844
- xdg-open
1845
- firefox
1846
- firefox-bin
1847
- ).detect do |cmd|
1848
- which cmd
1849
- end
1850
- elsif ENV['OS'] == 'Windows_NT' || RUBY_PLATFORM =~ /djgpp|(cyg|ms|bcc)win|mingw|wince/i
1851
- 'start ""'
1852
- else
1853
- raise "Could not work out how to use a browser."
1854
- end
1855
-
1856
- `#{command} #{url}`
1857
- end
1858
-
1859
- # Get the API base path
1860
- def base_path
1861
- ENV.key?(URL_ENV_NAME) ? GHE_BASE_PATH : GITHUB_BASE_PATH
1862
- end
1863
-
1864
- # Get the API URL
1865
- def api_url
1866
- ENV.key?(URL_ENV_NAME) ? URI(ENV[URL_ENV_NAME]) : GITHUB_API_URL
1867
- end
1868
-
1869
- def legacy_private_gister?
1870
- return unless which('git')
1871
- `git config --global gist.private` =~ /\Ayes|1|true|on\z/i
1872
- end
1873
-
1874
- def should_be_public?(options={})
1875
- if options.key? :private
1876
- !options[:private]
1877
- else
1878
- !Gist.legacy_private_gister?
1879
- end
1880
- end
1881
- end
1882
- #!/usr/bin/env ruby
1883
-
1884
- # Silence Ctrl-C's
1885
- trap('INT'){ exit 1 }
1886
-
1887
- if Signal.list.include? 'PIPE'
1888
- trap('PIPE', 'EXIT')
1889
- end
1890
-
1891
- require 'optparse'
1892
-
1893
- # For the holdings of options.
1894
- options = {}
1895
- filenames = []
1896
-
1897
- OptionParser.new do |opts|
1898
- executable_name = File.split($0)[1]
1899
- opts.banner = <<-EOS
1900
- Gist (v#{Gist::VERSION}) lets you upload to https://gist.github.com/
1901
-
1902
- The content to be uploaded can be passed as a list of files, if none are
1903
- specified STDIN will be read. The default filename for STDIN is "a.rb", and all
1904
- filenames can be overridden by repeating the "-f" flag. The most useful reason
1905
- to do this is to change the syntax highlighting.
1906
-
1907
- All gists must to be associated with a GitHub account, so you will need to login with
1908
- `gist --login` to obtain an OAuth2 access token. This is stored and used by gist in the future.
1909
-
1910
- Private gists do not have guessable URLs and can be created with "-p", you can
1911
- also set the description at the top of the gist by passing "-d".
1912
-
1913
- If you would like to shorten the resulting gist URL, use the -s flag. This will
1914
- use GitHub's URL shortener, git.io. You can also use -R to get the link to the
1915
- raw gist.
1916
-
1917
- To copy the resulting URL to your clipboard you can use the -c option, or to
1918
- just open it directly in your browser, use -o. Using the -e option will copy the
1919
- embeddable URL to the clipboard. You can add `alias gist='gist -c'` to your
1920
- shell's rc file to configure this behaviour by default.
1921
-
1922
- Instead of creating a new gist, you can update an existing one by passing its ID
1923
- or URL with "-u". For this to work, you must be logged in, and have created the
1924
- original gist with the same GitHub account.
1925
-
1926
- If you want to skip empty files, use the --skip-empty flag. If all files are
1927
- empty no gist will be created.
1928
-
1929
- Usage: #{executable_name} [-o|-c|-e] [-p] [-s] [-R] [-d DESC] [-u URL]
1930
- [--skip-empty] [-P] [-f NAME|-t EXT]* FILE*
1931
- #{executable_name} --login
1932
- #{executable_name} [-l|-r]
1933
-
1934
- EOS
1935
-
1936
- opts.on("--login", "Authenticate gist on this computer.") do
1937
- Gist.login!
1938
- exit
1939
- end
1940
-
1941
- opts.on("-f", "--filename [NAME.EXTENSION]", "Sets the filename and syntax type.") do |filename|
1942
- filenames << filename
1943
- options[:filename] = filename
1944
- end
1945
-
1946
- opts.on("-t", "--type [EXTENSION]", "Sets the file extension and syntax type.") do |extension|
1947
- filenames << "foo.#{extension}"
1948
- options[:filename] = "foo.#{extension}"
1949
- end
1950
-
1951
- opts.on("-p", "--private", "Makes your gist private.") do
1952
- options[:private] = true
1953
- end
1954
-
1955
- opts.on("--no-private") do
1956
- options[:private] = false
1957
- end
1958
-
1959
- opts.on("-d", "--description DESCRIPTION", "Adds a description to your gist.") do |description|
1960
- options[:description] = description
1961
- end
1962
-
1963
- opts.on("-s", "--shorten", "Shorten the gist URL using git.io.") do |shorten|
1964
- options[:shorten] = shorten
1965
- end
1966
-
1967
- opts.on("-u", "--update [ URL | ID ]", "Update an existing gist.") do |update|
1968
- options[:update] = update
1969
- end
1970
-
1971
- opts.on("-c", "--copy", "Copy the resulting URL to the clipboard") do
1972
- options[:copy] = true
1973
- end
1974
-
1975
- opts.on("-e", "--embed", "Copy the embed code for the gist to the clipboard") do
1976
- options[:embed] = true
1977
- options[:copy] = true
1978
- end
1979
-
1980
- opts.on("-o", "--open", "Open the resulting URL in a browser") do
1981
- options[:open] = true
1982
- end
1983
-
1984
- opts.on("--no-open")
1985
-
1986
- opts.on("--skip-empty", "Skip gisting empty files") do
1987
- options[:skip_empty] = true
1988
- end
1989
-
1990
- opts.on("-P", "--paste", "Paste from the clipboard to gist") do
1991
- options[:paste] = true
1992
- end
1993
-
1994
- opts.on("-R", "--raw", "Display raw URL of the new gist") do
1995
- options[:raw] = true
1996
- end
1997
-
1998
- opts.on("-l", "--list [USER]", "List all gists for user") do |user|
1999
- options[:list] = user
2000
- end
2001
-
2002
- opts.on("-r", "--read ID [FILENAME]", "Read a gist and print out the contents") do |id|
2003
- options[:read] = id
2004
- end
2005
-
2006
- opts.on("--delete [ URL | ID ]", "Delete a gist") do |id|
2007
- options[:delete] = id
2008
- end
2009
-
2010
- opts.on_tail("-h","--help", "Show this message.") do
2011
- puts opts
2012
- exit
2013
- end
2014
-
2015
- opts.on_tail("-v", "--version", "Print the version.") do
2016
- puts "gist v#{Gist::VERSION}"
2017
- exit
2018
- end
2019
-
2020
- end.parse!
2021
-
2022
- begin
2023
- if Gist.auth_token.nil?
2024
- puts 'Please log in with `gist --login`. ' \
2025
- '(GitHub now requires credentials to gist https://bit.ly/2GBBxKw)'
2026
- exit(1)
2027
- end
2028
-
2029
- options[:output] = if options[:embed] && options[:shorten]
2030
- raise Gist::Error, "--embed does not make sense with --shorten"
2031
- elsif options[:embed]
2032
- :javascript
2033
- elsif options[:shorten] and options[:raw]
2034
- :short_raw_url
2035
- elsif options[:shorten]
2036
- :short_url
2037
- elsif options[:raw]
2038
- :raw_url
2039
- else
2040
- :html_url
2041
- end
2042
-
2043
- options[:public] = Gist.should_be_public?(options)
2044
-
2045
- if options.key? :list
2046
- if options[:list]
2047
- Gist.list_all_gists(options[:list])
2048
- else
2049
- Gist.list_all_gists
2050
- end
2051
- exit
2052
- end
2053
-
2054
- if options.key? :read
2055
- file_name = ARGV.first
2056
- Gist.read_gist(options[:read], file_name)
2057
- exit
2058
- end
2059
-
2060
- if options.key? :delete
2061
- Gist.delete_gist(options[:delete])
2062
- exit
2063
- end
2064
-
2065
- if options[:paste]
2066
- puts Gist.gist(Gist.paste, options)
2067
- else
2068
- to_read = ARGV.empty? ? ['-'] : ARGV
2069
- files = {}
2070
- to_read.zip(filenames).each do |(file, name)|
2071
- files[name || file] =
2072
- begin
2073
- if file == '-'
2074
- $stderr.puts "(type a gist. <ctrl-c> to cancel, <ctrl-d> when done)" if $stdin.tty?
2075
- STDIN.read
2076
- else
2077
- File.read(File.expand_path(file))
2078
- end
2079
- rescue => e
2080
- raise e.extend(Gist::Error)
2081
- end
2082
- end
2083
-
2084
- output = Gist.multi_gist(files, options)
2085
- puts output if output
2086
- end
2087
-
2088
- rescue Gist::Error => e
2089
- puts "Error: #{e.message}"
2090
- exit 1
2091
- end