rack-oauth2-server 1.2.2 → 1.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|
+
}
|