confluence/
lib.rs

1/*!
2Access and modify [Atlassian Confluence](https://www.atlassian.com/software/confluence/) pages from Rust.
3
4## Working with this library
5
6To start, create a new `Session` by calling a `login` on it
7with your credentials.
8
9Internally, the `Session` struct stores the auth `token`
10and uses it when calling remote methods.
11
12The token will be destroyed (automatic logout) when `Session` goes out of scope.
13*/
14
15#[macro_use]
16extern crate log;
17extern crate chrono;
18extern crate reqwest;
19extern crate xml;
20extern crate xmltree;
21
22pub mod http;
23pub mod rpser;
24pub mod wsdl;
25
26mod page;
27mod space;
28mod transforms;
29
30pub use page::{Page, PageSummary, PageUpdateOptions, UpdatePage};
31pub use space::Space;
32pub use transforms::FromElement;
33
34use std::io::Error as IoError;
35use std::result;
36
37use self::http::HttpError;
38use self::rpser::xml::BuildElement;
39use self::rpser::{Method, RpcError};
40use xmltree::Element;
41
42const V2_API_RPC_PATH: &str = "/rpc/soap-axis/confluenceservice-v2?wsdl";
43
44/// Client's session.
45pub struct Session {
46    wsdl: wsdl::Wsdl,
47    token: String,
48}
49
50impl Drop for Session {
51    fn drop(&mut self) {
52        self.logout().unwrap();
53    }
54}
55
56impl Session {
57    /**
58    Create new confluence session.
59
60    ## Example
61
62    ```no_run
63    let session = confluence::Session::login(
64        "https://confluence",
65        "user",
66        "pass"
67    ).unwrap();
68    ```
69    */
70    pub fn login(url: &str, user: &str, pass: &str) -> Result<Session> {
71        debug!("logging in at url {:?} with user {:?}", url, user);
72
73        let url = if url.ends_with('/') {
74            &url[..url.len() - 1]
75        } else {
76            url
77        };
78        let wsdl_url = [url, V2_API_RPC_PATH].concat();
79
80        debug!("getting wsdl from url {:?}", wsdl_url);
81
82        let wsdl = try!(wsdl::fetch(&wsdl_url));
83        let mut session = Session {
84            wsdl,
85            token: String::new(),
86        };
87
88        let response = try!(session.call(
89            Method::new("login")
90                .with(Element::node("username").with_text(user))
91                .with(Element::node("password").with_text(pass))
92        ));
93
94        let token = match try!(response.body.descend(&["loginReturn"])).text {
95            Some(token) => token,
96            _ => return Err(Error::ReceivedNoLoginToken),
97        };
98
99        session.token = token;
100
101        Ok(session)
102    }
103
104    /// Explicitly log out out of confluence.
105    ///
106    /// This is done automatically at the end of Session's lifetime.
107    pub fn logout(&self) -> Result<bool> {
108        let response = try!(self.call(
109            Method::new("logout").with(Element::node("token").with_text(self.token.clone()))
110        ));
111
112        Ok(match try!(response.body.descend(&["logoutReturn"])).text {
113            Some(ref v) if v == "true" => {
114                debug!("logged out successfully");
115                true
116            }
117            _ => {
118                debug!("log out failed (maybe expired token, maybe not loged in)");
119                false
120            }
121        })
122    }
123
124    /**
125    Returns a single Space.
126
127    If the spaceKey does not exist: earlier versions of Confluence will throw an Exception. Later versions (3.0+) will return a null object.
128
129    In this client the difference will be in error type.
130
131    ## Example
132
133    ```no_run
134    # let session = confluence::Session::login("https://confluence", "user", "pass").unwrap();
135    println!("Space: {:#?}",
136        session.get_space(
137            "SomeSpaceKey"
138        )
139    );
140    ```
141    */
142    pub fn get_space(&self, space_key: &str) -> Result<Space> {
143        let response = try!(self.call(
144            Method::new("getSpace")
145                .with(Element::node("token").with_text(self.token.clone()))
146                .with(Element::node("spaceKey").with_text(space_key))
147        ));
148
149        let element = try!(response.body.descend(&["getSpaceReturn"]));
150
151        Ok(try!(Space::from_element(element)))
152    }
153
154    /**
155    Returns a single Page by space and title.
156
157    ## Example
158
159    ```no_run
160    # let session = confluence::Session::login("https://confluence", "user", "pass").unwrap();
161    println!("Page: {:#?}",
162        session.get_page_by_title(
163            "SomeSpaceKey", "Page Title"
164        )
165    );
166    ```
167    */
168    pub fn get_page_by_title(&self, space_key: &str, page_title: &str) -> Result<Page> {
169        let response = try!(self.call(
170            Method::new("getPage")
171                .with(Element::node("token").with_text(self.token.clone()))
172                .with(Element::node("spaceKey").with_text(space_key))
173                .with(Element::node("pageTitle").with_text(page_title))
174        ));
175
176        let element = try!(response.body.descend(&["getPageReturn"]));
177
178        Ok(try!(Page::from_element(element)))
179    }
180
181    /**
182    Returns a single Page by id.
183
184    ## Example
185
186    ```no_run
187    # let session = confluence::Session::login("https://confluence", "user", "pass").unwrap();
188    println!("Page: {:#?}",
189        session.get_page_by_id(
190            123456
191        )
192    );
193    ```
194    */
195    pub fn get_page_by_id(&self, page_id: i64) -> Result<Page> {
196        let response = try!(self.call(
197            Method::new("getPage")
198                .with(Element::node("token").with_text(self.token.clone()))
199                .with(Element::node("pageId").with_text(page_id.to_string()))
200        ));
201
202        let element = try!(response.body.descend(&["getPageReturn"]));
203
204        Ok(try!(Page::from_element(element)))
205    }
206
207    /**
208    Adds or updates a page.
209
210    # For adding
211
212    The Page given as an argument should have:
213
214    - (optional) parent_id
215    - space
216    - title
217    - content
218
219    fields at a minimum.
220
221    Use helper `UpdatePage::with_create_fields` to create such page.
222
223    ## Example
224
225    ```no_run
226    use confluence::UpdatePage;
227
228    # let session = confluence::Session::login("https://confluence", "user", "pass").unwrap();
229    session.store_page(
230        UpdatePage::with_create_fields(
231            None,
232            "SpaceKey",
233            "Page Title",
234            "<b>Works</b>"
235        )
236    );
237    ```
238
239    # For updating
240
241    The Page given should have:
242
243    - (optional) parent_id
244    - id
245    - space
246    - title
247    - content
248    - version
249
250    fields at a minimum.
251
252    Use method `into` on `Page` to convert it to `UpdatePage`.
253
254    ## Example
255
256    ```no_run
257    use confluence::UpdatePage;
258
259    # let session = confluence::Session::login("https://confluence", "user", "pass").unwrap();
260    let mut page = session.get_page_by_title(
261        "SomeSpaceKey", "Page Title"
262    ).unwrap();
263
264    page.title = "New Page Title".into();
265
266    session.store_page(page.into());
267    ```
268    */
269    pub fn store_page(&self, page: UpdatePage) -> Result<Page> {
270        let mut element_items = vec![
271            Element::node("space").with_text(page.space),
272            Element::node("title").with_text(page.title),
273            Element::node("content").with_text(page.content),
274        ];
275
276        if let Some(id) = page.id {
277            element_items.push(Element::node("id").with_text(id.to_string()));
278        }
279
280        if let Some(version) = page.version {
281            element_items.push(Element::node("version").with_text(version.to_string()));
282        }
283
284        if let Some(parent_id) = page.parent_id {
285            element_items.push(Element::node("parentId").with_text(parent_id.to_string()));
286        }
287
288        let response = try!(self.call(
289            Method::new("storePage")
290                .with(Element::node("token").with_text(self.token.clone()))
291                .with(Element::node("page").with_children(element_items))
292        ));
293
294        let element = try!(response.body.descend(&["storePageReturn"]));
295
296        Ok(try!(Page::from_element(element)))
297    }
298
299    /**
300    Updates the page.
301
302    Same as `store_page`, but with additional update options parameter.
303    */
304    pub fn update_page(&self, page: UpdatePage, options: PageUpdateOptions) -> Result<Page> {
305        let mut element_items = vec![
306            Element::node("space").with_text(page.space),
307            Element::node("title").with_text(page.title),
308            Element::node("content").with_text(page.content),
309        ];
310
311        if let Some(id) = page.id {
312            element_items.push(Element::node("id").with_text(id.to_string()));
313        }
314
315        if let Some(version) = page.version {
316            element_items.push(Element::node("version").with_text(version.to_string()));
317        }
318
319        if let Some(parent_id) = page.parent_id {
320            element_items.push(Element::node("parentId").with_text(parent_id.to_string()));
321        }
322
323        let mut update_options = vec![];
324
325        if let Some(comment) = options.version_comment {
326            update_options.push(Element::node("versionComment").with_text(comment));
327        }
328
329        update_options.push(Element::node("minorEdit").with_text(if options.minor_edit {
330            "true"
331        } else {
332            "false"
333        }));
334
335        let response = try!(self.call(
336            Method::new("updatePage")
337                .with(Element::node("token").with_text(self.token.clone()))
338                .with(Element::node("page").with_children(element_items))
339                .with(Element::node("pageUpdateOptions").with_children(update_options))
340        ));
341
342        let element = try!(response.body.descend(&["updatePageReturn"]));
343
344        Ok(try!(Page::from_element(element)))
345    }
346
347    /**
348    Returns all the direct children of this page.
349
350    ## Example
351
352    ```no_run
353    # let session = confluence::Session::login("https://confluence", "user", "pass").unwrap();
354    println!("Page Summaries: {:#?}",
355        session.get_children(
356            123456
357        )
358    );
359    ```
360    */
361    pub fn get_children(&self, page_id: i64) -> Result<Vec<PageSummary>> {
362        let response = try!(self.call(
363            Method::new("getChildren")
364                .with(Element::node("token").with_text(self.token.clone()))
365                .with(Element::node("pageId").with_text(page_id.to_string()))
366        ));
367
368        let element = try!(response.body.descend(&["getChildrenReturn"]));
369
370        let mut summaries = vec![];
371
372        for element in element.children {
373            summaries.push(try!(PageSummary::from_element(element)));
374        }
375
376        Ok(summaries)
377    }
378
379    /// Call a custom method on this session.
380    ///
381    /// ## Usage
382    ///
383    /// The elements in `Method` struct here will be converted directly
384    /// into SOAP envelope's Body.
385    ///
386    /// The returned `Response`.`body` will contain the parsed Body element.
387    ///
388    /// ## Discussion
389    ///
390    /// So far only few methods have convenience wrappers here, so if you need to call [something
391    /// else](https://developer.atlassian.com/confdev/confluence-rest-api/confluence-xml-rpc-and-soap-apis/remote-confluence-methods),
392    /// it's not so convenient, but possible.
393    ///
394    /// If you need an example, look at how these convenience methods are implemented.
395    ///
396    /// Pull requests are welcome!
397    pub fn call(&self, method: rpser::Method) -> Result<rpser::Response> {
398        let url = match self.wsdl.operations.get(&method.name) {
399            None => return Err(Error::MethodNotFoundInWsdl(method.name)),
400            Some(ref op) => &op.url,
401        };
402
403        // do now show password in logs
404        if method.name == "login" {
405            debug!("[call] login ******");
406        } else {
407            debug!("[call] {}", method);
408        }
409
410        let envelope = method.as_xml(url);
411
412        // do now show password in logs
413        if method.name != "login" {
414            trace!("[method xml] {}", envelope);
415        }
416
417        let http_response = try!(http::soap_action(url, &method.name, &envelope));
418
419        trace!("[response xml] {}", http_response.body);
420
421        Ok(try!(rpser::Response::from_xml(&http_response.body)))
422    }
423}
424
425/// Confluence library error.
426#[derive(Debug)]
427pub enum Error {
428    MethodNotFoundInWsdl(String),
429    ReceivedNoLoginToken,
430    Io(IoError),
431    Http(HttpError),
432    Rpc(Box<RpcError>),
433}
434
435impl From<HttpError> for Error {
436    fn from(other: HttpError) -> Error {
437        Error::Http(other)
438    }
439}
440
441impl From<RpcError> for Error {
442    fn from(other: RpcError) -> Error {
443        Error::Rpc(Box::new(other))
444    }
445}
446
447impl From<rpser::xml::Error> for Error {
448    fn from(other: rpser::xml::Error) -> Error {
449        RpcError::from(other).into()
450    }
451}
452
453impl From<IoError> for Error {
454    fn from(other: IoError) -> Error {
455        Error::Io(other)
456    }
457}
458
459pub type Result<T> = result::Result<T, Error>;