rack-oauth2-server 1.2.2 → 1.3.0
Sign up to get free protection for your applications and to get access to all the features.
- data/CHANGELOG +14 -0
- data/Gemfile +3 -0
- data/README.rdoc +26 -7
- data/Rakefile +1 -1
- data/VERSION +1 -0
- data/lib/rack/oauth2/admin/css/screen.css +233 -0
- data/lib/rack/oauth2/admin/images/loading.gif +0 -0
- data/lib/rack/oauth2/admin/js/application.js +154 -0
- data/lib/rack/oauth2/admin/js/jquery.js +166 -0
- data/lib/rack/oauth2/admin/js/jquery.tmpl.js +414 -0
- data/lib/rack/oauth2/admin/js/sammy.js +5 -0
- data/lib/rack/oauth2/admin/js/sammy.json.js +5 -0
- data/lib/rack/oauth2/admin/js/sammy.storage.js +5 -0
- data/lib/rack/oauth2/admin/js/sammy.title.js +5 -0
- data/lib/rack/oauth2/admin/js/sammy.tmpl.js +5 -0
- data/lib/rack/oauth2/admin/js/underscore.js +722 -0
- data/lib/rack/oauth2/admin/views/client.tmpl +48 -0
- data/lib/rack/oauth2/admin/views/clients.tmpl +36 -0
- data/lib/rack/oauth2/admin/views/edit.tmpl +57 -0
- data/lib/rack/oauth2/admin/views/index.html +26 -0
- data/lib/rack/oauth2/models/access_grant.rb +6 -4
- data/lib/rack/oauth2/models/access_token.rb +36 -4
- data/lib/rack/oauth2/models/auth_request.rb +4 -3
- data/lib/rack/oauth2/models/client.rb +15 -2
- data/lib/rack/oauth2/server.rb +71 -58
- data/lib/rack/oauth2/server/admin.rb +216 -0
- data/lib/rack/oauth2/server/helper.rb +4 -4
- data/lib/rack/oauth2/sinatra.rb +2 -2
- data/rack-oauth2-server.gemspec +2 -3
- data/test/admin/api_test.rb +196 -0
- data/test/admin_test_.rb +49 -0
- data/test/{access_grant_test.rb → oauth/access_grant_test.rb} +1 -1
- data/test/{access_token_test.rb → oauth/access_token_test.rb} +83 -12
- data/test/{authorization_test.rb → oauth/authorization_test.rb} +1 -1
- data/test/rails/config/environment.rb +2 -0
- data/test/rails/log/test.log +72938 -0
- data/test/setup.rb +17 -1
- data/test/sinatra/my_app.rb +1 -1
- metadata +27 -9
- data/lib/rack/oauth2/server/version.rb +0 -9
data/CHANGELOG
CHANGED
@@ -1,3 +1,17 @@
|
|
1
|
+
2010-11-07 version 1.3.0
|
2
|
+
|
3
|
+
Added OAuth authorization console.
|
4
|
+
|
5
|
+
Added param_authentication option: turn this on if you need to support
|
6
|
+
oauth_token query parameter or form field. Disabled by default.
|
7
|
+
|
8
|
+
Added host option: only check requests sent to that host (e.g. only check
|
9
|
+
requests to api.example.com).
|
10
|
+
|
11
|
+
Added path option: only check requests under this path (e.g. only check
|
12
|
+
requests for /api/...).
|
13
|
+
|
14
|
+
|
1
15
|
2010-11-03 version 1.2.2
|
2
16
|
|
3
17
|
Store ObjectId references in database.
|
data/Gemfile
CHANGED
@@ -2,6 +2,8 @@ source :rubygems
|
|
2
2
|
gemspec
|
3
3
|
|
4
4
|
group :development do
|
5
|
+
gem "sinatra"
|
6
|
+
gem "thin"
|
5
7
|
gem "yard"
|
6
8
|
end
|
7
9
|
|
@@ -13,5 +15,6 @@ group :test do
|
|
13
15
|
gem "rails", "~>2.3"
|
14
16
|
gem "shoulda"
|
15
17
|
gem "sinatra"
|
18
|
+
gem "therubyracer", "~>0.8.0.pre"
|
16
19
|
gem "timecop"
|
17
20
|
end
|
data/README.rdoc
CHANGED
@@ -72,6 +72,10 @@ The configuration options are:
|
|
72
72
|
- +:authorize_path+ -- Path for requesting end-user authorization. By
|
73
73
|
convention defaults to +/oauth/authorize+.
|
74
74
|
- +:database+ -- +Mongo::DB+ instance.
|
75
|
+
- +:host+ -- Only check requests sent to this host.
|
76
|
+
- +:path+ -- Only check requests for resources under this path.
|
77
|
+
- +:param_authentication+ -- If true, supports authentication using query/form
|
78
|
+
parameters.
|
75
79
|
- +:realm+ -- Authorization realm that will show up in 401 responses. Defaults
|
76
80
|
to use the request host name.
|
77
81
|
- +:scopes+ -- Array listing all supported scopes, e.g. ["read", "write"].
|
@@ -113,6 +117,11 @@ In Rails, the entire flow would look something like this:
|
|
113
117
|
|
114
118
|
class OauthController < ApplicationController
|
115
119
|
def authorize
|
120
|
+
if current_user
|
121
|
+
render :action=>"authorize"
|
122
|
+
else
|
123
|
+
redirect_to :action=>"login", :authorization=>oauth.authorization
|
124
|
+
end
|
116
125
|
end
|
117
126
|
|
118
127
|
def grant
|
@@ -132,7 +141,11 @@ method.
|
|
132
141
|
In Sinatra/Padrino, it would look something like this:
|
133
142
|
|
134
143
|
get "/oauth/authorize" do
|
135
|
-
|
144
|
+
if current_user
|
145
|
+
render "oauth/authorize"
|
146
|
+
else
|
147
|
+
redirect "/oauth/login?authorization=#{oauth.authorization}"
|
148
|
+
end
|
136
149
|
end
|
137
150
|
|
138
151
|
post "/oauth/grant" do
|
@@ -159,13 +172,19 @@ The view would look something like this:
|
|
159
172
|
|
160
173
|
== Step 4: Protect Your Path
|
161
174
|
|
162
|
-
Rack::OAuth2::Server intercepts all incoming requests and looks for
|
163
|
-
|
164
|
-
|
165
|
-
|
175
|
+
Rack::OAuth2::Server intercepts all incoming requests and looks for an
|
176
|
+
Authorization header that uses OAuth authentication scheme, like so:
|
177
|
+
|
178
|
+
Authorization: OAuth e57807eb99f8c29f60a27a75a80fec6e
|
179
|
+
|
180
|
+
It can also support the +oauth_token+ query parameter or form field, if you set
|
181
|
+
+param_authentication+ to true. This option is off by default to prevent
|
182
|
+
conflict with OAuth 1.0 callback.
|
166
183
|
|
167
|
-
|
168
|
-
|
184
|
+
If Rack::OAuth2::Server finder a valid access token in the request, it sets the
|
185
|
+
request header +oauth.identity+ to the value you supplied during authorization
|
186
|
+
(step 3). You can use +oauth.identity+ to resolve the access token back to
|
187
|
+
user, account or whatever you put there.
|
169
188
|
|
170
189
|
If the access token is invalid or revoked, it returns 401 (Unauthorized) to the
|
171
190
|
client. However, if there's no access token, the request goes through. You
|
data/Rakefile
CHANGED
data/VERSION
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
1.3.0
|
@@ -0,0 +1,233 @@
|
|
1
|
+
body {
|
2
|
+
margin: 0;
|
3
|
+
width: 100%;
|
4
|
+
font: 12pt "Helvetica", "Lucida Sans", "Verdana";
|
5
|
+
}
|
6
|
+
|
7
|
+
a { text-decoration: none; color: #00c; }
|
8
|
+
a:hover, a:focus { text-decoration: underline; color: #00c; }
|
9
|
+
h1, h2 {
|
10
|
+
text-shadow: rgba(255,255,255,.2) 0 1px 1px;
|
11
|
+
color: rgb(76, 86, 108);
|
12
|
+
}
|
13
|
+
h1 { font-size: 18pt; margin: 0.6em 0 }
|
14
|
+
h2 { font-size: 16pt; margin: 0.3em 0 }
|
15
|
+
|
16
|
+
label {
|
17
|
+
display: block;
|
18
|
+
color: #000;
|
19
|
+
font-weight: 600;
|
20
|
+
font-size: 0.9em;
|
21
|
+
margin: 0.9em 0;
|
22
|
+
}
|
23
|
+
label input, label textarea, label select { display: block }
|
24
|
+
label input, label textarea {
|
25
|
+
font-size: 12pt;
|
26
|
+
line-height: 1.3em;
|
27
|
+
}
|
28
|
+
label .hint {
|
29
|
+
font-weight: normal;
|
30
|
+
color: #666;
|
31
|
+
margin: 0;
|
32
|
+
}
|
33
|
+
button {
|
34
|
+
font-size: 11pt;
|
35
|
+
text-shadow: 0 -1px 1px rgba(0,0,0,0.25);
|
36
|
+
border: 1px solid #dddddd;
|
37
|
+
background: #f6f6f6 50% 50% repeat-x;
|
38
|
+
font-weight: bold;
|
39
|
+
color: #0073ea;
|
40
|
+
outline: none;
|
41
|
+
line-height: 1.3em;
|
42
|
+
vertical-align: bottom;
|
43
|
+
padding: 2px 8px;
|
44
|
+
background: -webkit-gradient(linear, 0% 0%, 0% 100%, from(rgba(226,226,226,0.0)), to(rgba(226,226,226,1.0)));
|
45
|
+
-webkit-border-radius: 4px; -moz-border-radius: 4px;
|
46
|
+
-moz-box-shadow: 0 0 4px rgba(0,0,0,0.0);
|
47
|
+
-webkit-box-shadow: 0 0 4px rgba(0,0,0,0.0);
|
48
|
+
}
|
49
|
+
button:hover, button:focus {
|
50
|
+
text-shadow: 0 -1px 1px rgba(255,255,255,0.25);
|
51
|
+
border: 1px solid #0073ea;
|
52
|
+
background: #0073ea 50% 50% repeat-x;
|
53
|
+
background: -webkit-gradient(linear, 0% 0%, 0% 100%, from(rgba(0, 115, 234, 0.5)), to(rgba(0,115,234, 1.0)));
|
54
|
+
color: #fff;
|
55
|
+
text-decoration: none;
|
56
|
+
cursor: pointer;
|
57
|
+
-moz-box-shadow: 0 2px 4px rgba(0,0,0,0.5);
|
58
|
+
-webkit-box-shadow: 0 1px 3px rgba(0,0,0,0.5);
|
59
|
+
}
|
60
|
+
button:active { background: -webkit-gradient(linear, 0% 0%, 0% 100%, from(rgba(0, 115, 234, 1.0)), to(rgba(0,115,234, 0.5))); position: relative; top: 1px }
|
61
|
+
|
62
|
+
|
63
|
+
/* Message dropping down from the top */
|
64
|
+
#notice {
|
65
|
+
position: absolute;
|
66
|
+
top: 0;
|
67
|
+
left: 0;
|
68
|
+
right: 0;
|
69
|
+
line-height: 1.6em;
|
70
|
+
background: #ffd;
|
71
|
+
color: #000;
|
72
|
+
border-bottom: 1px solid #ddd;
|
73
|
+
text-align: center;
|
74
|
+
z-index: 99;
|
75
|
+
}
|
76
|
+
|
77
|
+
#header {
|
78
|
+
margin: 0 2em;
|
79
|
+
}
|
80
|
+
#header .title {
|
81
|
+
font-size: 1.4em;
|
82
|
+
font-weight: bold;
|
83
|
+
color: #000;
|
84
|
+
text-decoration: none;
|
85
|
+
margin: 0.6em 0;
|
86
|
+
display: block;
|
87
|
+
text-align: right;
|
88
|
+
}
|
89
|
+
|
90
|
+
#main {
|
91
|
+
margin: 0 2em;
|
92
|
+
}
|
93
|
+
|
94
|
+
table {
|
95
|
+
width: 100%;
|
96
|
+
table-layout: auto;
|
97
|
+
empty-cells: show;
|
98
|
+
border-collapse: separate;
|
99
|
+
border-spacing: 0px;
|
100
|
+
}
|
101
|
+
table th {
|
102
|
+
text-align: left;
|
103
|
+
border-bottom: 1px solid #ccc;
|
104
|
+
margin-right: 48px;
|
105
|
+
}
|
106
|
+
table td {
|
107
|
+
text-align: left;
|
108
|
+
vertical-align: top;
|
109
|
+
border-bottom: 1px solid #ddf;
|
110
|
+
line-height: 32px;
|
111
|
+
margin: 0;
|
112
|
+
padding: 0;
|
113
|
+
}
|
114
|
+
table tr:hover td {
|
115
|
+
background: #ddf;
|
116
|
+
}
|
117
|
+
table td.created, table td.revoke {
|
118
|
+
width: 6em;
|
119
|
+
}
|
120
|
+
table tr.revoked td, table tr.revoked a {
|
121
|
+
color: #888;
|
122
|
+
}
|
123
|
+
table button {
|
124
|
+
margin-top: -2px;
|
125
|
+
font-size: 10pt;
|
126
|
+
}
|
127
|
+
|
128
|
+
table.clients td.name {
|
129
|
+
padding-left: 32px;
|
130
|
+
}
|
131
|
+
table.clients td.name img {
|
132
|
+
width: 24px;
|
133
|
+
height: 24px;
|
134
|
+
border: none;
|
135
|
+
margin: 4px 4px -4px -32px;
|
136
|
+
}
|
137
|
+
table.clients td.secrets {
|
138
|
+
width: 28em;
|
139
|
+
}
|
140
|
+
table.clients td.secrets dl {
|
141
|
+
display: none;
|
142
|
+
width: 40em;
|
143
|
+
margin: 0 -12em 0.6em 0;
|
144
|
+
line-height: 1.3em;
|
145
|
+
}
|
146
|
+
table.clients td.secrets dt {
|
147
|
+
width: 4em;
|
148
|
+
float: left;
|
149
|
+
color: #888;
|
150
|
+
margin-bottom: 0.3em;
|
151
|
+
}
|
152
|
+
table.clients td.secrets dd:after {
|
153
|
+
content: ".";
|
154
|
+
display: block;
|
155
|
+
clear: both;
|
156
|
+
visibility: hidden;
|
157
|
+
line-height: 0;
|
158
|
+
height: 0;
|
159
|
+
}
|
160
|
+
|
161
|
+
table.tokens td.token {
|
162
|
+
width: 32em;
|
163
|
+
}
|
164
|
+
table.tokens td.scope {
|
165
|
+
}
|
166
|
+
|
167
|
+
.pagination {
|
168
|
+
width: 100%;
|
169
|
+
margin-top: 2em;
|
170
|
+
}
|
171
|
+
.pagination a[rel=next] {
|
172
|
+
float: right;
|
173
|
+
}
|
174
|
+
.pagination a[rel=previous] {
|
175
|
+
float: left;
|
176
|
+
}
|
177
|
+
|
178
|
+
.badges {
|
179
|
+
list-style: none;
|
180
|
+
margin: 1.1em 0;
|
181
|
+
padding: 0;
|
182
|
+
text-align: right;
|
183
|
+
width: 100%;
|
184
|
+
}
|
185
|
+
.badges li {
|
186
|
+
display: inline-block;
|
187
|
+
margin-left: 8px;
|
188
|
+
min-width: 8em;
|
189
|
+
}
|
190
|
+
.badges big {
|
191
|
+
font-size: 22pt;
|
192
|
+
display: block;
|
193
|
+
text-align: center;
|
194
|
+
}
|
195
|
+
.badges small {
|
196
|
+
font-size: 11pt;
|
197
|
+
display: block;
|
198
|
+
text-align: center;
|
199
|
+
}
|
200
|
+
|
201
|
+
.client .details {
|
202
|
+
float: left;
|
203
|
+
}
|
204
|
+
.client .details .name {
|
205
|
+
font-size: 15pt;
|
206
|
+
font-weight: bold;
|
207
|
+
}
|
208
|
+
.client .details img {
|
209
|
+
border: none;
|
210
|
+
width: 24px;
|
211
|
+
height: 24px;
|
212
|
+
vertical-align: bottom;
|
213
|
+
}
|
214
|
+
.client .details a[rel=edit] {
|
215
|
+
margin: 0 0.3em 0 1em;
|
216
|
+
}
|
217
|
+
.client .details .meta {
|
218
|
+
color: #888;
|
219
|
+
font-size: 10pt;
|
220
|
+
}
|
221
|
+
.client.new>#image, .client.edit>#image {
|
222
|
+
float: left;
|
223
|
+
margin: 0 12px 0 0;
|
224
|
+
width: 48px;
|
225
|
+
height: 48px;
|
226
|
+
}
|
227
|
+
.client.new>*, .client.edit>* {
|
228
|
+
margin-left: 60px;
|
229
|
+
}
|
230
|
+
|
231
|
+
.loading {
|
232
|
+
background: url("../images/loading.gif") no-repeat 50% 50%;
|
233
|
+
}
|
Binary file
|
@@ -0,0 +1,154 @@
|
|
1
|
+
Sammy("#main", function(app) {
|
2
|
+
this.use(Sammy.Tmpl);
|
3
|
+
this.use(Sammy.Session);
|
4
|
+
this.use(Sammy.Title);
|
5
|
+
this.setTitle("OAuth Console - ");
|
6
|
+
|
7
|
+
// Use OAuth access token in all API requests.
|
8
|
+
$(document).ajaxSend(function(e, xhr) {
|
9
|
+
xhr.setRequestHeader("Authorization", "OAuth " + app.session("oauth.token"));
|
10
|
+
});
|
11
|
+
// For all request (except callback), if we don't have an OAuth access token,
|
12
|
+
// ask for one by requesting authorization.
|
13
|
+
this.before({ except: { path: /^#(access_token=|[^\\].*&access_token=)/ } }, function(context) {
|
14
|
+
if (!app.session("oauth.token"))
|
15
|
+
context.redirect(document.location.pathname + "/authorize?state=" + escape(context.path));
|
16
|
+
})
|
17
|
+
// We recognize the OAuth authorization callback based on one of its
|
18
|
+
// parameters. Crude but works here.
|
19
|
+
this.get(/^#(access_token=|[^\\].*&access_token=)/, function(context) {
|
20
|
+
// Instead of a hash we get query parameters, so turn those into an object.
|
21
|
+
var params = context.path.substring(1).split("&"), args = {};
|
22
|
+
for (var i in params) {
|
23
|
+
var splat = params[i].split("=");
|
24
|
+
args[splat[0]] = splat[1];
|
25
|
+
}
|
26
|
+
app.session("oauth.token", args.access_token);
|
27
|
+
// When the filter redirected the original request, it passed the original
|
28
|
+
// request's URL in the state parameter, which we get back after
|
29
|
+
// authorization.
|
30
|
+
context.redirect(args.state.length == 0 ? "#/" : unescape(args.state));
|
31
|
+
});
|
32
|
+
|
33
|
+
|
34
|
+
var api = document.location.pathname + "/api";
|
35
|
+
// View all clients
|
36
|
+
this.get("#/", function(context) {
|
37
|
+
context.title("All Clients");
|
38
|
+
$.getJSON(api + "/clients", function(json) {
|
39
|
+
context.partial("admin/views/clients.tmpl", { clients: json.list, tokens: json.tokens });
|
40
|
+
});
|
41
|
+
});
|
42
|
+
// Edit client
|
43
|
+
this.get("#/client/:id/edit", function(context) {
|
44
|
+
$.getJSON(api + "/client/" + context.params.id, function(client) {
|
45
|
+
context.title(client.displayName);
|
46
|
+
context.partial("admin/views/edit.tmpl", client)
|
47
|
+
})
|
48
|
+
});
|
49
|
+
this.put("#/client/:id", function(context) {
|
50
|
+
$.ajax({ type: "put", url: api + "/client/" + context.params.id,
|
51
|
+
data: {
|
52
|
+
displayName: context.params.displayName,
|
53
|
+
link: context.params.link,
|
54
|
+
redirectUri: context.params.redirectUri,
|
55
|
+
imageUrl: context.params.imageUrl
|
56
|
+
},
|
57
|
+
success: function(client) {
|
58
|
+
context.redirect("#/client/" + context.params.id);
|
59
|
+
app.trigger("notice", "Saved your changes");
|
60
|
+
},
|
61
|
+
error: function(xhr) {
|
62
|
+
context.partial("admin/views/edit.tmpl", context.params);
|
63
|
+
app.trigger("notice", xhr.responseText);
|
64
|
+
}
|
65
|
+
})
|
66
|
+
});
|
67
|
+
// View single client
|
68
|
+
this.get("#/client/:id", function(context) {
|
69
|
+
$.getJSON(api + "/client/" + context.params.id, function(client) {
|
70
|
+
context.title(client.displayName);
|
71
|
+
context.partial("admin/views/client.tmpl", client)
|
72
|
+
});
|
73
|
+
});
|
74
|
+
this.get("#/client/:id/:page", function(context) {
|
75
|
+
$.getJSON(api + "/client/" + context.params.id + "?page=" + context.params.page, function(client) {
|
76
|
+
context.title(client.displayName);
|
77
|
+
context.partial("admin/views/client.tmpl", client)
|
78
|
+
});
|
79
|
+
});
|
80
|
+
// Create new client
|
81
|
+
this.get("#/new", function(context) {
|
82
|
+
context.title("Add New Client");
|
83
|
+
context.partial("admin/views/edit.tmpl", context.params);
|
84
|
+
});
|
85
|
+
this.post("#/clients", function(context) {
|
86
|
+
context.title("Add New Client");
|
87
|
+
$.ajax({ type: "post", url: api + "/clients",
|
88
|
+
data: {
|
89
|
+
displayName: context.params.displayName,
|
90
|
+
link: context.params.link,
|
91
|
+
redirectUri: context.params.redirectUri,
|
92
|
+
imageUrl: context.params.imageUrl
|
93
|
+
},
|
94
|
+
success: function(client) {
|
95
|
+
app.trigger("notice", "Added new client application " + client.displayName);
|
96
|
+
context.redirect("#/");
|
97
|
+
},
|
98
|
+
error: function(xhr) {
|
99
|
+
app.trigger("notice", xhr.responseText);
|
100
|
+
context.partial("admin/views/edit.tmpl", context.params);
|
101
|
+
}
|
102
|
+
});
|
103
|
+
});
|
104
|
+
|
105
|
+
// Client/token revoke buttons do this.
|
106
|
+
$("a[data-method=post]").live("click", function(evt) {
|
107
|
+
evt.preventDefault();
|
108
|
+
var link = $(this);
|
109
|
+
if (link.attr("data-confirm") && !confirm(link.attr("data-confirm")))
|
110
|
+
return;
|
111
|
+
$.post(link.attr("href"), function(success) {
|
112
|
+
app.trigger("notice", "Revoked!");
|
113
|
+
app.refresh();
|
114
|
+
});
|
115
|
+
});
|
116
|
+
// Link to reveal/hide client ID/secret
|
117
|
+
$("td.secrets a[rel=toggle]").live("click", function(evt) {
|
118
|
+
evt.preventDefault();
|
119
|
+
var dl = $(this).next("dl");
|
120
|
+
if (dl.is(":visible")) {
|
121
|
+
$(this).html("Reveal");
|
122
|
+
dl.hide();
|
123
|
+
} else {
|
124
|
+
$(this).html("Hide");
|
125
|
+
dl.show();
|
126
|
+
}
|
127
|
+
});
|
128
|
+
// Error/notice at top of screen
|
129
|
+
var noticeTimeout;
|
130
|
+
app.bind("notice", function(evt, message) {
|
131
|
+
$("#notice").text(message).fadeIn("fast");
|
132
|
+
if (noticeTimeout) {
|
133
|
+
cancelTimeout(noticeTimeout);
|
134
|
+
noticeTimeout = null;
|
135
|
+
}
|
136
|
+
noticeTimeout = setTimeout(function() {
|
137
|
+
noticeTimeout = null;
|
138
|
+
$("#notice").fadeOut("slow");
|
139
|
+
}, 5000);
|
140
|
+
});
|
141
|
+
$("#notice").live("click", function() { $(this).fadeOut("slow") });
|
142
|
+
});
|
143
|
+
|
144
|
+
// Adds thousands separator to integer or float (can also pass formatted string
|
145
|
+
// if you care about precision).
|
146
|
+
$.thousands = function(integer) {
|
147
|
+
return integer.toString().replace(/^(\d+?)((\d{3})+)$/g, function(x,a,b) { return a + b.replace(/(\d{3})/g, ",$1") })
|
148
|
+
.replace(/\.((\d{3})+)(\d+)$/g, function(x,a,b,c) { return "." + a.replace(/(\d{3})/g, "$1,") + c })
|
149
|
+
}
|
150
|
+
|
151
|
+
$.shortdate = function(integer) {
|
152
|
+
var date = new Date(integer * 1000);
|
153
|
+
return "<span title='" + date.toLocaleString() + "'>" + date.toISOString().substring(0,10) + "</span>";
|
154
|
+
}
|