├── README.md
├── SAML.xml
├── package.json
└── sharepoint.js
/README.md:
--------------------------------------------------------------------------------
1 | # SharePoint client for Node.js
2 | This Node module provides a SharePoint client for Node.js applications. This allows you to access SharePoint 2010 lists and items, using ListData.svc, an OData based REST API for SharePoint 2010.
3 |
4 | The current version is restricted to SharePoint Online, using claims based authentication.
5 |
6 | ## Installation
7 |
8 | Use npm to install the module:
9 |
10 | ````
11 | > npm install sharepoint
12 | ````
13 |
14 | ## API
15 |
16 | Due to the asynchrounous nature of Node.js, the SharePoint client requires the use callbacks in requests. See documentation below.
17 |
18 | All callbacks have 2 arguments: err and data:
19 |
20 | ````
21 | function callback (err, data) {
22 | // err contains an error, if any
23 | // data contains the resulting data
24 | }
25 | ````
26 |
27 |
28 | ### SP.RestService(site)
29 | An object of this class represents a REST Service client for the specified SharePoint site.
30 |
31 | Example:
32 |
33 | ````
34 | var client = new SP.RestService('http://oxida.sharepoint.com/teamsite')
35 | ````
36 |
37 | ### client.signin (username, password, callback)
38 | The signin method performs a claims-based authentication:
39 |
40 | - build a SAML request (using SAML.xml template included in module)
41 | - submit a SAML token request to Microsoft Online Security Token Service
42 | - receive a signed security token
43 | - POST the token to SharePoint Online
44 | - receive FedAuth and rtFa authentication cookies
45 | - store the cookies in client for use in subsequent requests
46 |
47 | Callback is called when authentication is completed. You can wrap all your service requests inside this callback
48 |
49 | Example:
50 |
51 | ````
52 | client.signin('myname', 'mypassword', function(err,data) {
53 |
54 | // check for errors during login, e.g. invalid credentials
55 | if (err) {
56 | console.log("Error found: ", err);
57 | return;
58 | }
59 |
60 | // start to do authenticated requests here....
61 |
62 | })
63 | ````
64 |
65 |
66 | ### client.metadata(callback)
67 | Return the metadata document for the service ($metadata)
68 |
69 | ````
70 | var contacts = client.metadata(function(err, data) {
71 | console.log(data);
72 | });
73 | ````
74 |
75 |
76 | ### client.list(name)
77 | Return a new List object, which provides get, update and del(ete) operations
78 |
79 | ````
80 | var contacts = client.list('Contacts');
81 | ````
82 |
83 | ### list.get(callback)
84 | Fetch all items from list.
85 |
86 | ````
87 | contacts.get(function (err, data) {
88 | // data.results contains an array of all items in Contacts list
89 | // data.__count contains the total number items in Contacts list
90 | // Use query {$inlinecount:'allpages'} to request the __count property (see below).
91 | })
92 | ````
93 |
94 | ### list.get(id, callback)
95 | Get a single item with id from the list.
96 |
97 | ````
98 | contacts.get(12, function (err, data) {
99 | // data contains item with Id 12 from Contacts list
100 | })
101 | ````
102 |
103 | ### list.get(query, callback)
104 | Query the list using OData query options.
105 |
106 | ````
107 | contacts.get({$orderby: 'FirstName'}, function (err, data) {
108 | // data contains items from Contacts list, sorted on FirstName.
109 | })
110 | ````
111 | Use $inlinecount to request the total count of items in list:
112 |
113 | ````
114 | // get the first 3 items and return total number of items in Contacts list
115 | contacts.get({$top: 3, $inlinecount:'allpages'}, function (err, data) {
116 | // data contains items from Contacts list, sorted on FirstName.
117 | })
118 | ````
119 |
120 | ### list.add(attributes, callback)
121 | Add a new item to the list
122 |
123 | ````
124 | contacts.add({LastName: 'Picolet', FirstName: 'Emma'}, function (err, data) {
125 | // data contains the new item returned from server.
126 | // data.Id will be the server assigned Id.
127 | })
128 | ````
129 |
130 | ### list.update(id, attributes, callback)
131 | Update the attributes for the list item specified by Id. The client performs a partial update: only the attributes specified in the hash are changed. Partial updates require the use of etags, so you need to get the item first before you change it.
132 |
133 | ````
134 | contacts.get(411, function (err, data) {
135 |
136 | var changes = {
137 | // include the changes that need to be made
138 | LastName: 'Tell',
139 | FirstName: 'William',
140 |
141 | // pass the metadata from the fetched item
142 | // this includes the require etag
143 | __metadata: data.__metadata
144 | }
145 |
146 | contacts.update(411, changes, function () {
147 | // at this point, the change is completed
148 | })
149 | })
150 |
151 | ````
152 |
153 | ### list.del(id, callback)
154 | Delete list item specified by Id from the list.
155 |
156 | ````
157 | contacts.del(411, function (err, data) {
158 | // at this point deletion is completed.
159 | })
160 |
161 | ````
162 |
163 |
164 | ## To Do
165 |
166 | This first version of SharePoint client library allows you to read and write to SharePoint Online lists.
167 |
168 | There's still a lot to do:
169 |
170 | - Implement and improve error handling
171 | - Support for on-premise SharePoint
172 | - Support for other authentication mechanisms (form based, NTLM, ..)
173 | - Support for other SharePoint APIs (SOAP, CSOM).
174 |
175 |
--------------------------------------------------------------------------------
/SAML.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | http://schemas.xmlsoap.org/ws/2005/02/trust/RST/Issue
4 |
5 | http://www.w3.org/2005/08/addressing/anonymous
6 |
7 | https://login.microsoftonline.com/extSTS.srf
8 |
9 |
10 | [username]
11 | [password]
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 | [endpoint]
20 |
21 |
22 | http://schemas.xmlsoap.org/ws/2005/05/identity/NoProofKey
23 | http://schemas.xmlsoap.org/ws/2005/02/trust/Issue
24 | urn:oasis:names:tc:SAML:1.0:assertion
25 |
26 |
27 |
28 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "author": "Luc Stakenborg (http://www.oxida.com)",
3 | "name": "sharepoint",
4 | "description": "SharePoint client for Node.js",
5 | "version": "0.0.5",
6 | "homepage": "https://github.com/lstak/node-sharepoint",
7 | "repository": {
8 | "type": "git",
9 | "url": "git@github.com:lstak/node-sharepoint.git"
10 | },
11 | "main": "sharepoint.js",
12 | "engines": {
13 | "node": ">=0.6"
14 | },
15 |
16 | "dependencies": {
17 | "xml2js": ">= 0.0.1"
18 | },
19 | "devDependencies": {}
20 | }
21 |
--------------------------------------------------------------------------------
/sharepoint.js:
--------------------------------------------------------------------------------
1 | var fs = require('fs'),
2 | qs = require('querystring'),
3 | xml2js = require('xml2js'),
4 | http = require('http'),
5 | https = require('https'),
6 | urlparse = require('url').parse,
7 | samlRequestTemplate = fs.readFileSync(__dirname+'/SAML.xml', 'utf8');
8 |
9 |
10 | var buildSamlRequest = function (params) {
11 | var key,
12 | saml = samlRequestTemplate;
13 |
14 | for (key in params) {
15 | saml = saml.replace('[' + key + ']', params[key])
16 | }
17 |
18 | return saml;
19 | }
20 |
21 | var parseXml = function (xml, callback) {
22 | var parser = new xml2js.Parser({
23 | emptyTag: '' // use empty string as value when tag empty
24 | });
25 |
26 | parser.on('end', function (js) {
27 | callback && callback(js)
28 | });
29 |
30 | parser.parseString(xml);
31 | };
32 |
33 | var parseCookie = function (txt) {
34 | var properties = txt.split('; '),
35 | cookie = {};
36 |
37 | properties.forEach(function (property, index) {
38 | var idx = property.indexOf('='),
39 | name = (idx > 0 ? property.substring(0, idx) : property),
40 | value = (idx > 0 ? property.substring(idx + 1) : undefined);
41 |
42 | if (index == 0) {
43 | cookie.name = name,
44 | cookie.value = value
45 | } else {
46 | cookie[name] = value
47 | }
48 |
49 | })
50 |
51 | return cookie;
52 | };
53 |
54 | var parseCookies = function (txts) {
55 | var cookies = []
56 |
57 | if (txts) {
58 | txts.forEach(function (txt) {
59 | var cookie = parseCookie(txt);
60 | cookies.push(cookie)
61 | })
62 | };
63 |
64 | return cookies;
65 | }
66 |
67 |
68 | var getCookie = function (cookies, name) {
69 | var cookie,
70 | i = 0,
71 | len = cookies.length;
72 |
73 | for (; i < len; i++) {
74 | cookie = cookies[i]
75 | if (cookie.name == name) {
76 | return cookie
77 | }
78 | }
79 |
80 | return undefined;
81 |
82 | }
83 |
84 | function requestToken(params, callback) {
85 | var samlRequest = buildSamlRequest({
86 | username: params.username,
87 | password: params.password,
88 | endpoint: params.endpoint
89 | });
90 |
91 | var options = {
92 | method: 'POST',
93 | host: params.sts.host,
94 | path: params.sts.path,
95 | headers: {
96 | 'Content-Length': samlRequest.length
97 | }
98 | };
99 |
100 |
101 | var req = https.request(options, function (res) {
102 | var xml = '';
103 |
104 | res.setEncoding('utf8');
105 | res.on('data', function (chunk) {
106 | xml += chunk;
107 | })
108 |
109 | res.on('end', function () {
110 |
111 | parseXml(xml, function (js) {
112 |
113 | // check for errors
114 | if (js['S:Body']['S:Fault']) {
115 | var error = js['S:Body']['S:Fault']['S:Detail']['psf:error']['psf:internalerror']['psf:text'];
116 | callback(error);
117 | return;
118 | }
119 |
120 | // extract token
121 | var token = js['S:Body']['wst:RequestSecurityTokenResponse']['wst:RequestedSecurityToken']['wsse:BinarySecurityToken']['#'];
122 |
123 | // Now we have the token, we need to submit it to SPO
124 | submitToken({
125 | token: token,
126 | endpoint: params.endpoint
127 | }, callback)
128 | })
129 | })
130 | });
131 |
132 | req.end(samlRequest);
133 | }
134 |
135 | function submitToken(params, callback) {
136 | var token = params.token,
137 | url = urlparse(params.endpoint),
138 | ssl = (url.protocol == 'https:');
139 |
140 | var options = {
141 | method: 'POST',
142 | host: url.hostname,
143 | path: url.path,
144 | headers: {
145 | // E accounts require a user agent string
146 | 'User-Agent': 'Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.1; Win64; x64; Trident/5.0)'
147 | }
148 | };
149 |
150 | var protocol = (ssl ? https : http);
151 |
152 |
153 |
154 | var req = protocol.request(options, function (res) {
155 |
156 | var xml = '';
157 | res.setEncoding('utf8');
158 | res.on('data', function (chunk) {
159 | xml += chunk;
160 | })
161 |
162 | res.on('end', function () {
163 |
164 | var cookies = parseCookies(res.headers['set-cookie'])
165 |
166 | callback(null, {
167 | FedAuth: getCookie(cookies, 'FedAuth').value,
168 | rtFa: getCookie(cookies, 'rtFa').value
169 | })
170 | })
171 | });
172 |
173 | req.end(token);
174 | }
175 |
176 |
177 | function signin(username, password, callback) {
178 | var self = this;
179 |
180 | var options = {
181 | username: username,
182 | password: password,
183 | sts: self.sts,
184 | endpoint: self.url.protocol + '//' + self.url.hostname + self.login
185 | }
186 |
187 | requestToken(options, function (err, data) {
188 |
189 | if (err) {
190 | callback(err);
191 | return;
192 | }
193 |
194 | self.FedAuth = data.FedAuth;
195 | self.rtFa = data.rtFa;
196 |
197 | callback(null, data);
198 | })
199 | }
200 |
201 |
202 |
203 | function request(options, next) {
204 |
205 | var req_data = options.data || '',
206 | list = options.list,
207 | id = options.id,
208 | query = options.query,
209 | ssl = (this.protocol == 'https:'),
210 | path = this.path + this.odatasvc + list +
211 | (id ? '(' + id + ')' : '') +
212 | (query ? '?' + qs.stringify(query) : '');
213 |
214 | var req_options = {
215 | method: options.method,
216 | host: this.host,
217 | path: path,
218 | headers: {
219 | 'Accept': options.accept || 'application/json',
220 | 'Content-type': 'application/json',
221 | 'Cookie': 'FedAuth=' + this.FedAuth + '; rtFa=' + this.rtFa,
222 | 'Content-length': req_data.length
223 | }
224 | };
225 |
226 | // Include If-Match header if etag is specified
227 | if (options.etag) {
228 | req_options.headers['If-Match'] = options.etag;
229 | };
230 |
231 | //console.log('OPTIONS:', req_options);
232 | //console.log('DATA:', req_data);
233 |
234 | // support for using https
235 | var protocol = (ssl ? https : http);
236 |
237 | var req = protocol.request(req_options, function (res) {
238 | //console.log('STATUS:', res.statusCode);
239 | //console.log('HEADERS:', res.headers);
240 |
241 |
242 | var res_data = '';
243 | res.setEncoding('utf8');
244 | res.on('data', function (chunk) {
245 | //console.log('CHUNK:', chunk);
246 | res_data += chunk;
247 | });
248 | res.on('end', function () {
249 | // if no callback is defined, we're done.
250 | if (!next) return;
251 |
252 | // if data of content-type application/json is return, parse into JS:
253 | if (res_data && (res.headers['content-type'].indexOf('json') > 0)) {
254 | res_data = JSON.parse(res_data).d
255 | }
256 |
257 | if (res_data) {
258 | next(null, res_data)
259 | }
260 | else {
261 | next()
262 | }
263 | });
264 | })
265 |
266 | req.end(req_data);
267 | }
268 |
269 |
270 | // the following call to get are allowed:
271 | // get([callback]) - fecth all items from list
272 | // get(id [,callback]) - fetch an item from the list
273 | // get(query [,callback]) - query list
274 |
275 | function get(arg1, arg2) {
276 | var id, query, callback;
277 |
278 | if ('object' == typeof arg1) {
279 | query = arg1,
280 | callback = arg2
281 | }
282 |
283 | if ('string' == typeof arg1 || 'number' == typeof arg1) {
284 | id = arg1,
285 | callback = arg2
286 | }
287 |
288 | if (!arg2) {
289 | callback = arg1
290 | }
291 |
292 | var options = {
293 | list: this.name,
294 | id: id,
295 | query: query,
296 | method: 'GET'
297 | };
298 |
299 | this.service.request(options, callback);
300 | }
301 |
302 |
303 |
304 |
305 | function add(attributes, next) {
306 | var options = {
307 | list: this.name,
308 | method: 'POST',
309 | data: JSON.stringify(attributes)
310 | };
311 |
312 | this.service.request(options, next);
313 | }
314 |
315 | function del(id, next) {
316 | var options = {
317 | list: this.name,
318 | id: id,
319 | method: 'DELETE'
320 | };
321 |
322 | this.service.request(options, next);
323 | }
324 |
325 |
326 | function update(id, attributes, next) {
327 | var options = {
328 | list: this.name,
329 | id: id,
330 | method: 'MERGE',
331 | etag: attributes.__metadata.etag,
332 | data: JSON.stringify(attributes)
333 | };
334 |
335 | this.service.request(options, next);
336 | }
337 |
338 |
339 |
340 | // convenience method to create a List provided by RestService.
341 | function list(name) {
342 | var list = new SP.List(this, name)
343 | return list;
344 | };
345 |
346 | // method to fetch a RestService metadata document
347 | function metadata(next) {
348 | var options = {
349 | list: '$metadata',
350 | accept: 'application/xml',
351 | method: 'GET'
352 | };
353 |
354 | this.request(options, next);
355 | }
356 |
357 |
358 | var SP = {};
359 |
360 | // constructor for REST service
361 | SP.RestService = function (url) {
362 | this.url = urlparse(url);
363 | this.host = this.url.host;
364 | this.path = this.url.path;
365 | this.protocol = this.url.protocol;
366 |
367 |
368 | // External Security Token Service for SPO
369 | this.sts = {
370 | host: 'login.microsoftonline.com',
371 | path: '/extSTS.srf'
372 | };
373 |
374 | // Form to submit SAML token
375 | this.login = '/_forms/default.aspx?wa=wsignin1.0';
376 |
377 |
378 | // SharePoint Odata (REST) service
379 | this.odatasvc = '/_vti_bin/ListData.svc/';
380 |
381 | };
382 |
383 | SP.RestService.prototype = {
384 | signin: signin,
385 | list: list,
386 | request: request,
387 | metadata: metadata
388 | };
389 |
390 |
391 | // Constructor for SP List
392 | SP.List = function (service, name) {
393 | this.service = service
394 | this.name = name
395 | }
396 |
397 | SP.List.prototype = {
398 | get: get,
399 | add: add,
400 | update: update,
401 | del: del
402 | };
403 |
404 | module.exports = SP;
--------------------------------------------------------------------------------