tools.go 8.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206
  1. //
  2. // Copyright (C) 2017-2018 Marcus Rohrmoser, http://purl.mro.name/ShaarliGo
  3. //
  4. // This program is free software: you can redistribute it and/or modify
  5. // it under the terms of the GNU General Public License as published by
  6. // the Free Software Foundation, either version 3 of the License, or
  7. // (at your option) any later version.
  8. //
  9. // This program is distributed in the hope that it will be useful,
  10. // but WITHOUT ANY WARRANTY; without even the implied warranty of
  11. // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  12. // GNU General Public License for more details.
  13. //
  14. // You should have received a copy of the GNU General Public License
  15. // along with this program. If not, see <http://www.gnu.org/licenses/>.
  16. //
  17. package main
  18. import (
  19. "html/template"
  20. "io"
  21. "log"
  22. "net/http"
  23. "net/url"
  24. "os"
  25. "strings"
  26. "time"
  27. )
  28. const timeoutShaarliImportFetch = time.Minute
  29. func (app *App) handleTools(w http.ResponseWriter, r *http.Request) {
  30. now := time.Now()
  31. if !app.IsLoggedIn(now) {
  32. http.Redirect(w, r, cgiName+"?do=login&returnurl="+url.QueryEscape(r.URL.String()), http.StatusUnauthorized)
  33. return
  34. }
  35. if !app.cfg.IsConfigured() {
  36. http.Redirect(w, r, cgiName+"/config", http.StatusPreconditionFailed)
  37. return
  38. }
  39. xmlBase := xmlBaseFromRequestURL(r.URL, os.Getenv("SCRIPT_NAME"))
  40. switch r.Method {
  41. case http.MethodGet:
  42. app.KeepAlive(w, r, now)
  43. if tmpl, err := template.New("tools").Parse(`<html xmlns="http://www.w3.org/1999/xhtml">
  44. <head><title>{{.title}}</title></head>
  45. <body>
  46. <ol>
  47. <li id="disclosure">
  48. <b>Responsible Disclosure:</b> In case you are reluctant to <a
  49. href="http://purl.mro.name/ShaarliGo/">file a public issue</a>, feel free to
  50. email <a href="mailto:ShaarliGo@mro.name?subject=">ShaarliGo@mro.name</a>.
  51. </li>
  52. <li id="update">
  53. <b>Update:</b> Just replace the file <code>shaarligo.cgi</code>. To update the assets, delete them and
  54. <code>app/delete_me_to_restore</code>, then clear your browser cache and visit the CGI, e.g.
  55. the <a href="../search/?q=foo">search</a>.
  56. <br class="br"/>
  57. <code>$ ssh <kbd>myserver.example.com</kbd><br class="br"/>
  58. $ cd <kbd>filesystem/path/to/</kbd><br class="br"/>
  59. $ curl -R -L -o shaarligo.cgi.gz <a href="http://purl.mro.name/shaarligo_cgi.gz">http://purl.mro.name/shaarligo_cgi.gz</a> &amp;&amp; gunzip shaarligo.cgi.gz<br class="br"/>
  60. $ chmod a+x shaarligo.cgi<br class="br"/>
  61. $ ls -l shaarligo?cgi*<br class="br"/>
  62. $ rm -rf .htaccess assets app/delete_me_to_restore</code>
  63. </li>
  64. <li id="config"><a href="../config/">Config</a></li>
  65. <li>
  66. <form class="form-inline" name="tag_rename">
  67. <div class="form-group">
  68. <label for="tag_rename_old">Rename Tag:</label>
  69. <input type="text" class="form-control" id="tag_rename_old" placeholder="#before" value="{{ .tag_rename_old }}"/>
  70. </div>
  71. <div class="form-group">
  72. <label for="tag_rename_new" class="sr-only">To:</label>
  73. <input type="text" class="form-control" id="tag_rename_new" placeholder="#after" value="{{ .tag_rename_new }}"/>
  74. </div>
  75. <button type="submit" class="btn btn-primary">Rename</button>
  76. </form>
  77. </li>
  78. <li>
  79. <form class="form-inline" name="shaarli_import" method="post">
  80. <div class="form-group">
  81. <label for="shaarli_import_url">Import Other Shaarli:</label>
  82. <input type="url" class="form-control" name="shaarli_import_url" placeholder="https://demo.shaarli.org/?" value="{{ .other_shaarli_url }}"/>
  83. </div>
  84. <div class="form-group">
  85. <label for="shaarli_import_tag" class="sr-only">#MarkerForThisImport</label>
  86. <input type="text" class="form-control" name="shaarli_import_tag" placeholder="#MarkerTagForThisImport" value="#{{ .other_shaarli_tag }}"/>
  87. </div>
  88. <button name="shaarli_import_submit" type="submit" value="shaarli_import_submit" class="btn btn-primary">Import</button>
  89. </form>
  90. </li>
  91. <li id="bookmarklet">
  92. <b>Bookmarklet:</b> <a
  93. onclick="alert('Drag this link to your bookmarks toolbar, or right-click it and choose Bookmark This Link...');return false;"
  94. href="javascript:javascript:(function(){var%20url%20=%20location.href;var%20title%20=%20document.title%20||%20url;window.open('{{.xml_base}}?post='%20+%20encodeURIComponent(url)+'&amp;title='%20+%20encodeURIComponent(title)+'&amp;description='%20+%20encodeURIComponent(document.getSelection())+'&amp;source=bookmarklet','_blank','menubar=no,height=450,width=600,toolbar=no,scrollbars=no,status=no,dialog=1');})();"
  95. >✚ShaarliGo 🌺</a>
  96. <span>⇐ Drag this link to your bookmarks toolbar (or right-click it and choose Bookmark This Link…).
  97. Then click "✚ShaarliGo 🌺" button in any page you want to share.</span>
  98. </li>
  99. <li id="version">
  100. <b>Version:</b> <span id="number">v{{.version}}</span>+<span id="gitsha1">{{.gitsha1}}</span>
  101. </li>
  102. </ol>
  103. </body>
  104. </html>
  105. `); err == nil {
  106. w.Header().Set("Content-Type", "text/xml; charset=utf-8")
  107. io.WriteString(w, `<?xml version="1.0" encoding="UTF-8"?>
  108. <?xml-stylesheet type='text/xsl' href='../../assets/`+app.cfg.Skin+`/tools.xslt'?>
  109. `)
  110. data := map[string]string{
  111. "title": app.cfg.Title,
  112. "xml_base": xmlBase.String() + cgiName,
  113. "tag_rename_old": "",
  114. "tag_rename_new": "",
  115. "other_shaarli_url": "",
  116. "other_shaarli_tag": time.Now().Format(time.RFC3339[:16]),
  117. "version": version,
  118. "gitsha1": GitSHA1,
  119. }
  120. if err := tmpl.Execute(w, data); err != nil {
  121. http.Error(w, "Coudln't render tools: "+err.Error(), http.StatusInternalServerError)
  122. }
  123. }
  124. case http.MethodPost:
  125. app.KeepAlive(w, r, now)
  126. if "" != r.FormValue("shaarli_import_submit") {
  127. if url, err := url.Parse(strings.TrimSpace(r.FormValue("shaarli_import_url")) + "?do=atom&nb=all"); err != nil {
  128. http.Error(w, "Coudln't parse shaarli_import_url "+err.Error(), http.StatusBadRequest)
  129. } else {
  130. if rq, err := HttpGetBody(url, timeoutShaarliImportFetch); err != nil {
  131. http.Error(w, "Coudln't fetch shaarli_import_url "+err.Error(), http.StatusBadRequest)
  132. } else {
  133. if importedFeed, err := FeedFromReader(rq); err != nil {
  134. http.Error(w, "Coudln't parse feed from shaarli_import_url "+err.Error(), http.StatusBadRequest)
  135. } else {
  136. log.Printf("Import %d entries from %v\n", len(importedFeed.Entries), url)
  137. cat := Category{Term: strings.TrimSpace(strings.TrimPrefix(r.FormValue("shaarli_import_tag"), "#"))}
  138. feed, _ := app.LoadFeed()
  139. feed.XmlBase = xmlBase.String()
  140. // feed.Id = feed.XmlBase
  141. impEnt := make([]*Entry, 0, len(importedFeed.Entries))
  142. for _, entry := range importedFeed.Entries {
  143. if et, err := entry.NormaliseAfterImport(); err != nil {
  144. log.Printf("Error with %v: %v\n", entry.Id, err.Error())
  145. } else {
  146. // log.Printf("done entry: %s\n", et.Id)
  147. if "" != cat.Term {
  148. et.Categories = append(et.Categories, cat)
  149. }
  150. if _, err := feed.Append(&et); err == nil {
  151. impEnt = append(impEnt, &et)
  152. } else {
  153. log.Printf("couldn't add entry: %s\n", err.Error())
  154. }
  155. }
  156. }
  157. if err := app.SaveFeed(feed); err != nil {
  158. http.Error(w, "couldn't store feed data: "+err.Error(), http.StatusInternalServerError)
  159. return
  160. }
  161. feed.XmlBase = xmlBase.String()
  162. if err := app.PublishFeedsForModifiedEntries(feed, feed.Entries); err != nil {
  163. log.Println("couldn't write feeds: ", err.Error())
  164. http.Error(w, "couldn't write feeds: "+err.Error(), http.StatusInternalServerError)
  165. return
  166. }
  167. }
  168. }
  169. }
  170. }
  171. http.Redirect(w, r, xmlBase.String(), http.StatusFound)
  172. }
  173. }
  174. func (entry Entry) NormaliseAfterImport() (Entry, error) {
  175. // log.Printf("process entry: %s\n", entry.Id)
  176. // normalise Id
  177. if idx := strings.Index(entry.Id, "?"); idx >= 0 {
  178. entry.Id = entry.Id[idx+1:]
  179. }
  180. if entry.Published.IsZero() {
  181. entry.Published = entry.Updated
  182. }
  183. // normalise Links
  184. if nil != entry.Content {
  185. entry.Content = &HumanText{Body: cleanLegacyContent(entry.Content.Body)}
  186. }
  187. err := entry.Validate()
  188. return entry, err
  189. }