Checks username and password against the wiki.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

496 lines
10 KiB

  1. // Copyright 2013 James McGuire
  2. // This code is covered under the MIT License
  3. // Please refer to the LICENSE file in the root of this
  4. // repository for any information.
  5. // go-mediawiki provides a wrapper for interacting with the Mediawiki API
  6. //
  7. // Please see http://www.mediawiki.org/wiki/API:Main_page
  8. // for any API specific information or refer to any of the
  9. // functions defined for the MWApi struct for information
  10. // regarding this specific implementation.
  11. //
  12. // The client subdirectory contains an example application
  13. // that uses this API.
  14. package mediawiki
  15. import (
  16. "bytes"
  17. "encoding/json"
  18. "errors"
  19. "io"
  20. "io/ioutil"
  21. "mime/multipart"
  22. "net/http"
  23. "net/http/cookiejar"
  24. "net/url"
  25. "strings"
  26. )
  27. // The main mediawiki API struct, this is generated via mwapi.New()
  28. type MWApi struct {
  29. Username string
  30. Password string
  31. Domain string
  32. userAgent string
  33. debug bool
  34. url *url.URL
  35. client *http.Client
  36. format string
  37. edittoken string
  38. }
  39. // This is used for passing data to the mediawiki API via key=value in a POST
  40. type Values map[string]string
  41. // Unmarshal login data...
  42. type outerLogin struct {
  43. Login loginResponse
  44. }
  45. type loginResponse struct {
  46. Result string
  47. Token string
  48. }
  49. // Unmarshall response from page edits...
  50. type outerEdit struct {
  51. Edit edit
  52. }
  53. type edit struct {
  54. Result string
  55. PageId int
  56. Title string
  57. OldRevId int
  58. NewRevId int
  59. }
  60. // General query response from mediawiki
  61. type mwQuery struct {
  62. Query query
  63. }
  64. type query struct {
  65. Pages map[string]page
  66. }
  67. type page struct {
  68. Pageid int
  69. Ns float64
  70. Title string
  71. Touched string
  72. Lastrevid float64
  73. // This will appear as both a string and a number... and the JSON unmarshaler
  74. // will crap out if this isn't set to a string.
  75. //Counter string
  76. Length float64
  77. Edittoken string
  78. Revisions []revision
  79. Imageinfo []image
  80. }
  81. type revision struct {
  82. Body string `json:"*"`
  83. User string
  84. Timestamp string
  85. comment string
  86. }
  87. type image struct {
  88. Url string
  89. Descriptionurl string
  90. }
  91. type mwError struct {
  92. Error errorType
  93. }
  94. type errorType struct {
  95. Code string
  96. Info string
  97. }
  98. type uploadResponse struct {
  99. Upload uploadResult
  100. }
  101. type uploadResult struct {
  102. Result string
  103. }
  104. // Helper function for translating mediawiki errors in to golang errors.
  105. func CheckError(response []byte) error {
  106. var mwerror mwError
  107. err := json.Unmarshal(response, &mwerror)
  108. if err != nil {
  109. return nil
  110. } else if mwerror.Error.Code != "" {
  111. return errors.New(mwerror.Error.Code + ": " + mwerror.Error.Info)
  112. } else {
  113. return nil
  114. }
  115. }
  116. // Generate a new mediawiki API struct
  117. //
  118. // Example: mwapi.New("http://en.wikipedia.org/w/api.php", "My Mediawiki Bot")
  119. // Returns errors if the URL is invalid
  120. func New(wikiUrl, userAgent string) (*MWApi, error) {
  121. cookiejar, err := cookiejar.New(nil)
  122. if err != nil {
  123. return nil, err
  124. }
  125. client := http.Client{
  126. Transport: nil,
  127. CheckRedirect: nil,
  128. Jar: cookiejar,
  129. }
  130. clientUrl, err := url.Parse(wikiUrl)
  131. if err != nil {
  132. return nil, err
  133. }
  134. return &MWApi{
  135. url: clientUrl,
  136. client: &client,
  137. format: "json",
  138. userAgent: "go-mediawiki https://github.com/sadbox/go-mediawiki " + userAgent,
  139. debug: false,
  140. }, nil
  141. }
  142. // This will automatically add the user agent and encode the http request properly
  143. func (m *MWApi) postForm(query url.Values) ([]byte, error) {
  144. request, err := http.NewRequest("POST", m.url.String(), strings.NewReader(query.Encode()))
  145. if err != nil {
  146. return nil, err
  147. }
  148. request.Header.Set("Content-Type", "application/x-www-form-urlencoded")
  149. request.Header.Set("user-agent", m.userAgent)
  150. resp, err := m.client.Do(request)
  151. if err != nil {
  152. return nil, err
  153. }
  154. defer resp.Body.Close()
  155. body, err := ioutil.ReadAll(resp.Body)
  156. if err != nil {
  157. return nil, err
  158. }
  159. if err = CheckError(body); err != nil {
  160. return nil, err
  161. }
  162. return body, nil
  163. }
  164. // Download a file.
  165. //
  166. // Returns a readcloser that must be closed manually. Refer to the
  167. // example app for additional usage.
  168. func (m *MWApi) Download(filename string) (io.ReadCloser, error) {
  169. // First get the direct url of the file
  170. query := Values{
  171. "action": "query",
  172. "prop": "imageinfo",
  173. "iiprop": "url",
  174. "titles": filename,
  175. }
  176. body, _, err := m.API(query)
  177. if err != nil {
  178. return nil, err
  179. }
  180. var response mwQuery
  181. err = json.Unmarshal(body, &response)
  182. if err != nil {
  183. return nil, err
  184. }
  185. var fileurl string
  186. for _, page := range response.Query.Pages {
  187. if len(page.Imageinfo) < 1 {
  188. return nil, errors.New("No file found")
  189. }
  190. fileurl = page.Imageinfo[0].Url
  191. break
  192. }
  193. // Then return the body of the response
  194. request, err := http.NewRequest("GET", fileurl, nil)
  195. if err != nil {
  196. return nil, err
  197. }
  198. request.Header.Set("user-agent", m.userAgent)
  199. resp, err := m.client.Do(request)
  200. if err != nil {
  201. return nil, err
  202. }
  203. return resp.Body, nil
  204. }
  205. // Upload a file
  206. //
  207. // This does a simple, but more error-prone upload. Mediawiki
  208. // has a chunked upload version but it is only available in newer
  209. // versions of the API.
  210. //
  211. // Automatically retrieves an edit token if necessary.
  212. func (m *MWApi) Upload(dstFilename string, file io.Reader) error {
  213. if m.edittoken == "" {
  214. err := m.GetEditToken()
  215. if err != nil {
  216. return err
  217. }
  218. }
  219. query := Values{
  220. "action": "upload",
  221. "filename": dstFilename,
  222. "token": m.edittoken,
  223. "format": m.format,
  224. }
  225. buffer := &bytes.Buffer{}
  226. writer := multipart.NewWriter(buffer)
  227. for key, value := range query {
  228. err := writer.WriteField(key, value)
  229. if err != nil {
  230. return err
  231. }
  232. }
  233. part, err := writer.CreateFormFile("file", dstFilename)
  234. _, err = io.Copy(part, file)
  235. if err != nil {
  236. return err
  237. }
  238. err = writer.Close()
  239. if err != nil {
  240. return err
  241. }
  242. request, err := http.NewRequest("POST", m.url.String(), buffer)
  243. if err != nil {
  244. return err
  245. }
  246. request.Header.Set("Content-Type", "multipart/form-data; boundary="+writer.Boundary())
  247. request.Header.Set("user-agent", m.userAgent)
  248. resp, err := m.client.Do(request)
  249. if err != nil {
  250. return err
  251. }
  252. defer resp.Body.Close()
  253. body, err := ioutil.ReadAll(resp.Body)
  254. if err != nil {
  255. return err
  256. }
  257. if err = CheckError(body); err != nil {
  258. return err
  259. }
  260. var response uploadResponse
  261. err = json.Unmarshal(body, &response)
  262. if err != nil {
  263. return err
  264. }
  265. if response.Upload.Result == "Success" || response.Upload.Result == "Warning" {
  266. return nil
  267. } else {
  268. return errors.New(response.Upload.Result)
  269. }
  270. }
  271. // Login to the Mediawiki Website
  272. //
  273. // This will throw an error if you didn't define a username
  274. // or password.
  275. func (m *MWApi) Login() error {
  276. if m.Username == "" || m.Password == "" {
  277. return errors.New("Username or password not set.")
  278. }
  279. query := Values{
  280. "action": "login",
  281. "lgname": m.Username,
  282. "lgpassword": m.Password,
  283. }
  284. if m.Domain != "" {
  285. query["lgdomain"] = m.Domain
  286. }
  287. body, _, err := m.API(query)
  288. if err != nil {
  289. return err
  290. }
  291. var response outerLogin
  292. err = json.Unmarshal(body, &response)
  293. if err != nil {
  294. return err
  295. }
  296. if response.Login.Result == "Success" {
  297. return nil
  298. } else if response.Login.Result != "NeedToken" {
  299. return errors.New("Error logging in: " + response.Login.Result)
  300. }
  301. // Need to use the login token
  302. query["lgtoken"] = response.Login.Token
  303. body, _, err = m.API(query)
  304. if err != nil {
  305. return err
  306. }
  307. err = json.Unmarshal(body, &response)
  308. if err != nil {
  309. return err
  310. }
  311. if response.Login.Result == "Success" {
  312. return nil
  313. } else {
  314. return errors.New("Error logging in: " + response.Login.Result)
  315. }
  316. }
  317. // Get an edit token
  318. //
  319. // This is necessary for editing any page.
  320. //
  321. // The Edit() function will call this automatically
  322. // but it is available if you want to make direct
  323. // calls to API().
  324. func (m *MWApi) GetEditToken() error {
  325. query := Values{
  326. "action": "query",
  327. "prop": "info|revisions",
  328. "intoken": "edit",
  329. "titles": "Main Page",
  330. }
  331. body, _, err := m.API(query)
  332. if err != nil {
  333. return err
  334. }
  335. var response mwQuery
  336. err = json.Unmarshal(body, &response)
  337. if err != nil {
  338. return err
  339. }
  340. for _, value := range response.Query.Pages {
  341. m.edittoken = value.Edittoken
  342. break
  343. }
  344. return nil
  345. }
  346. // Log out of the mediawiki website
  347. func (m *MWApi) Logout() {
  348. m.API(Values{"action": "logout"})
  349. }
  350. // Edit a page
  351. //
  352. // This function will automatically grab an Edit Token if there
  353. // is not one currently stored.
  354. //
  355. // Example:
  356. //
  357. // editConfig := mediawiki.Values{
  358. // "title": "SOME PAGE",
  359. // "summary": "THIS IS WHAT SHOWS UP IN THE LOG",
  360. // "text": "THE ENTIRE TEXT OF THE PAGE",
  361. // }
  362. // err = client.Edit(editConfig)
  363. func (m *MWApi) Edit(values Values) error {
  364. if m.edittoken == "" {
  365. err := m.GetEditToken()
  366. if err != nil {
  367. return err
  368. }
  369. }
  370. query := Values{
  371. "action": "edit",
  372. "token": m.edittoken,
  373. }
  374. body, _, err := m.API(query, values)
  375. if err != nil {
  376. return err
  377. }
  378. var response outerEdit
  379. err = json.Unmarshal(body, &response)
  380. if err != nil {
  381. return err
  382. }
  383. if response.Edit.Result == "Success" {
  384. return nil
  385. } else {
  386. return errors.New(response.Edit.Result)
  387. }
  388. }
  389. // Request a wiki page and it's metadata.
  390. func (m *MWApi) Read(pageName string) (*revision, error) {
  391. query := Values{
  392. "action": "query",
  393. "prop": "revisions",
  394. "titles": pageName,
  395. "rvlimit": "1",
  396. "rvprop": "content|timestamp|user|comment",
  397. }
  398. body, _, err := m.API(query)
  399. var response mwQuery
  400. err = json.Unmarshal(body, &response)
  401. if err != nil {
  402. return nil, err
  403. }
  404. for _, page := range response.Query.Pages {
  405. return &page.Revisions[0], nil
  406. }
  407. return nil, errors.New("No revisions found")
  408. }
  409. // A generic interface to the Mediawiki API
  410. // Refer to the mediawiki API reference for any information regarding
  411. // what to pass to this function
  412. //
  413. // This is used by all internal functions to interact with the API
  414. //
  415. // The second return is simply the json data decoded in to an empty interface
  416. // that can be used by something like https://github.com/jmoiron/jsonq
  417. func (m *MWApi) API(values ...Values) ([]byte, interface{}, error) {
  418. query := m.url.Query()
  419. for _, valuemap := range values {
  420. for key, value := range valuemap {
  421. query.Set(key, value)
  422. }
  423. }
  424. query.Set("format", m.format)
  425. body, err := m.postForm(query)
  426. if err != nil {
  427. return nil, nil, err
  428. }
  429. var unmarshalto interface{}
  430. err = json.Unmarshal(body, &unmarshalto)
  431. if err != nil {
  432. return nil, nil, err
  433. }
  434. return body, unmarshalto, nil
  435. }