Compare commits

...

610 Commits

Author SHA1 Message Date
Frédéric Guillot dde2ca06c3 Update ChangeLog 2021-02-15 20:34:19 -08:00
Frédéric Guillot 091308787b Add header "Referrer-Policy: no-referrer"
https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Referrer-Policy
2021-02-14 11:33:31 -08:00
Frédéric Guillot 04f9c456d5 Handle entry title with double encoded entities in Atom feeds 2021-02-14 11:19:21 -08:00
Frédéric Guillot 6b7ffe0e26 Add Open Containers annotations to Docker image 2021-02-13 16:27:23 -08:00
Frédéric Guillot f29940d784 Rename storage certificate cache
This cache is used only for ACME certificates.

Being explicit is always better.
2021-02-13 14:16:30 -08:00
Frédéric Guillot 0413daf76b Remove iframe inner HTML contents
An iframe element never has fallback content, as it will always create a nested
browsing context, regardless of whether the specified initial contents are
successfully used.

https://www.w3.org/TR/2010/WD-html5-20101019/the-iframe-element.html#the-iframe-element
2021-02-13 14:00:21 -08:00
Frédéric Guillot 5043749b9f Add workaround for entry title with double encoded entities
Example: 'Text'
2021-02-13 13:33:59 -08:00
Nick Chitwood 793f475edd Update date parser to fix another time zone issue
The Washington Post has its feeds with EST, which is getting parsed by miniflux as UTC, and showing up as 8 hours off.

See http://feeds.washingtonpost.com/rss/politics for an example.

This fix applies a similar workaround for EST/EDT as was done for PST/PDT.
2021-02-10 22:45:02 -08:00
Benedikt Hopmann 03cfbfe4ac Update german translation for blocklist and keeplist 2021-02-08 16:32:07 -08:00
Frédéric Guillot e8d0360e64 Validate Keep list and Block list rules syntax 2021-02-07 18:53:56 -08:00
y0ast 05fd83bd6f add support for ipv6 with zone index 2021-02-07 15:57:40 -08:00
Frédéric Guillot 864dd9f219 Allow images with data URLs
Only URLs with a mime-type image/* are allowed
2021-02-06 14:46:01 -08:00
Frédéric Guillot 9a9a271b1f Limit full-text search indexation to first 500K characters
tsvector has a size limit of 1MB. See https://www.postgresql.org/docs/13/textsearch-limitations.html

Input text is now truncated to avoid this error:

"pq: string is too long for tsvector (1057834 bytes, max 1048575 bytes)"
2021-02-06 14:10:45 -08:00
Frédéric Guillot 89d17107af Change PWA display mode to standalone 2021-02-06 11:54:17 -08:00
Frédéric Guillot b6ddaae82a ETag value is not set correctly in HTTP client (regression)
Bug introduced after refactoring.

See commit 16b7b3bc3e.
2021-02-05 20:36:05 -08:00
Dave Marquard 0bece2df7d Database backed LetsEncrypt certificate cache (#993) 2021-01-29 18:44:40 -08:00
Ilya Mateyko 4464802947 Reformat some Go files
When working on #994 I noticed that some Go files are not formatted with
`gofmt`.

This PR fixes this.
2021-01-27 18:13:58 -08:00
Shizun Ge 7c44238bae Add global option POLLING_PARSING_ERROR_LIMIT 2021-01-25 21:41:36 -08:00
Ilya Mateyko b45c1cf327 Use systemctl edit for editing systemd config file
This is safer than directly editing the package-supplied version.

See https://wiki.archlinux.org/index.php/Systemd#Editing_provided_units.
2021-01-25 21:23:45 -08:00
Frédéric Guillot f1d7385294 Update Go version to 1.15 in go.mod 2021-01-24 11:50:37 -08:00
Kiskae 77f5b911b5 Don't discard the "Fetch via Proxy" option 2021-01-24 11:24:28 -08:00
Shizun Ge f8ca825524 Update go.mod 2021-01-24 11:06:52 -08:00
Frédéric Guillot 6377f3a8b3 Fix regression introduced by PR #973
The relation "entries_user_feed_idx" already exists
2021-01-24 11:00:05 -08:00
Shizun Ge 533d5dfc98 Update man page to show the default values 2021-01-21 23:16:05 -08:00
Patrick Marschik 1ed3816d6f PostgreSQL index optimizations 2021-01-21 23:02:01 -08:00
Shizun Ge 4ff52bd730 Add API endpoints to get feeds and entries of a category 2021-01-18 19:44:02 -08:00
Shizun Ge 02a4c9db53 Create feed query builder 2021-01-18 13:22:09 -08:00
dependabot[bot] 433de17562 Bump github.com/PuerkitoBio/goquery from 1.6.0 to 1.6.1
Bumps [github.com/PuerkitoBio/goquery](https://github.com/PuerkitoBio/goquery) from 1.6.0 to 1.6.1.
- [Release notes](https://github.com/PuerkitoBio/goquery/releases)
- [Commits](https://github.com/PuerkitoBio/goquery/compare/v1.6.0...v1.6.1)

Signed-off-by: dependabot[bot] <support@github.com>
2021-01-18 11:04:11 -08:00
Shizun Ge 23e4183bec Show global options in the about page
Only shows the options when current user is admin.
2021-01-18 11:02:40 -08:00
Shizun Ge 926ddf6d09 Update man page to mention -1 can be used for CLEANUP_ARCHIVE_* options 2021-01-16 18:00:29 -08:00
Frédéric Guillot a6017a8411 Update ChangeLog 2021-01-09 17:45:56 -08:00
Frédéric Guillot 608443b77f Add spellcheck="false" to input fields 2021-01-04 20:29:12 -08:00
Frédéric Guillot 11e110bc7d Refactor entry validation 2021-01-04 15:44:06 -08:00
Frédéric Guillot 806b9545a9 Refactor feed validator 2021-01-04 14:47:25 -08:00
Frédéric Guillot b35fece3d5 Remove unused function 2021-01-03 23:02:51 -08:00
Frédéric Guillot 4468ef1410 Refactor category validation 2021-01-03 22:50:24 -08:00
Frédéric Guillot e45cc2d2aa Refactor user validation
Validate each user field for creation/modification via API and web UI
2021-01-03 21:36:35 -08:00
Frédéric Guillot 291bf96d15 Do not strip tags for entry title
Some technical blogs have titles like "</some-title>" or "This is some <code>source code</code>".

Miniflux was removing these elements which prevent rendering the title correctly.
2021-01-03 11:44:07 -08:00
Frédéric Guillot f0610bdd9c Refactor feed creation to allow setting most fields via API
Allow API clients to create disabled feeds or define field like "ignore_http_cache".
2021-01-02 16:48:22 -08:00
Ilya Mateyko ab82c4b300 Subscribe to feeds with the Android Share menu 2021-01-02 11:48:07 -08:00
Jean Paul Galea 94eaf0e254 Modify systemd service file
Better support to run miniflux with Let's Encrypt, without a reverse proxy.
2020-12-29 21:08:40 -08:00
Bogdan Brinza 9a6c80f562 Updating read/star icons to SVGs 2020-12-29 20:47:18 -08:00
Frédéric Guillot 9a4b045bdc Add autocomplete="username" to HTML forms 2020-12-23 10:29:51 -08:00
Frédéric Guillot d3cfa6396d Improve user mass delete 2020-12-22 21:02:10 -08:00
Frédéric Guillot 60a7362327 Use SQL transaction when creating user sessions 2020-12-22 20:28:34 -08:00
Frédéric Guillot 7be9f5989e Use a SQL transaction when creating users 2020-12-22 15:47:35 -08:00
Frédéric Guillot 651ee02c11 Allow regular users to change settings via API 2020-12-22 15:20:36 -08:00
Frédéric Guillot fd9eaa3e83 Make user fields editable via API 2020-12-21 22:13:11 -08:00
Frédéric Guillot 83f3ccab0e Remove extra column from users table (HSTORE field)
Migrated key/value pairs to specific columns.
2020-12-21 21:35:03 -08:00
dependabot[bot] ae74f94655 Bump github.com/prometheus/client_golang from 1.8.0 to 1.9.0
Bumps [github.com/prometheus/client_golang](https://github.com/prometheus/client_golang) from 1.8.0 to 1.9.0.
- [Release notes](https://github.com/prometheus/client_golang/releases)
- [Changelog](https://github.com/prometheus/client_golang/blob/master/CHANGELOG.md)
- [Commits](https://github.com/prometheus/client_golang/compare/v1.8.0...v1.9.0)

Signed-off-by: dependabot[bot] <support@github.com>
2020-12-20 21:51:03 -08:00
Frédéric Guillot 989ca163a3 Add styles for <abbr> HTML tag 2020-12-20 20:24:01 -08:00
Frédéric Guillot 5b74083b5f Refactor SQL migrations
- Avoid embedding SQL files into binary
- Allow more flexible changes by using Go functions
2020-12-20 18:40:44 -08:00
Clément Joly f88e93a0b9 Add Server-Timing header to unread page 2020-12-18 15:08:17 -08:00
Frédéric Guillot 2cf9bde1af Show correct User Agent in input placeholders 2020-12-16 21:30:22 -08:00
Frédéric Guillot e589a35c67 Add autocomplete attribute to login form 2020-12-16 18:34:04 -08:00
Frédéric Guillot 4e5240c5ac Allow ignore_http_cache field to be changed via API 2020-12-13 20:42:41 -08:00
Frédéric Guillot 5922a7a051 Renaming non-existent category via API should returns a 404 2020-12-13 19:05:10 -08:00
dependabot[bot] 70effdc706 Bump github.com/lib/pq from 1.8.0 to 1.9.0
Bumps [github.com/lib/pq](https://github.com/lib/pq) from 1.8.0 to 1.9.0.
- [Release notes](https://github.com/lib/pq/releases)
- [Commits](https://github.com/lib/pq/compare/v1.8.0...v1.9.0)

Signed-off-by: dependabot[bot] <support@github.com>
2020-12-06 22:30:08 -08:00
Frédéric Guillot a72aca1d05 Add Grafana dashboard in contrib folder 2020-12-05 19:53:40 -08:00
Frédéric Guillot 351d157533 Update ChangeLog 2020-12-05 16:21:10 -08:00
Pedro Lucas Porcellis 9493fb111f Small update on reading time translation 2020-12-05 15:17:01 -08:00
Frédéric Guillot a026efce6e Use created_at instead of published_at for archiving entries 2020-12-02 21:08:55 -08:00
Frédéric Guillot 1908c84fbe Handle invalid French date 2020-12-02 20:59:14 -08:00
Frédéric Guillot f722fd1208 Handle invalid feeds with relative URLs 2020-12-02 20:58:18 -08:00
Ben Congdon 49feb1958c Add created_at field for entries 2020-11-29 17:04:18 -08:00
Benjamin Congdon e17d395ae7 Add API routes for "mark all as read" 2020-11-29 17:00:27 -08:00
Benjamin Congdon eeeea74bf1 Update Miniflux man page with UserAgent setting 2020-11-29 16:55:45 -08:00
Benjamin Congdon 52a626d7b9 Add support for setting a global default User-Agent 2020-11-29 16:55:45 -08:00
Pacman99 b8b6c74d86 Add rewrite rule replace for custom search and replace 2020-11-29 10:32:26 -08:00
Frédéric Guillot de7a613098 Calculate reading time during feed processing
The goal is to speed up the user interface.

Detecting the language based on the content is pretty slow.
2020-11-18 17:43:24 -08:00
Frédéric Guillot b1c9977711 Handle more invalid dates 2020-11-17 17:12:12 -08:00
Frédéric Guillot a108cb7808 Handle various invalid date 2020-11-16 21:37:33 -08:00
Pedro Lucas Porcellis 4f4f573955 Small update on the favorite label button translation 2020-11-11 22:28:07 -08:00
Frédéric Guillot eb77fcfb72 systemd: keep /run writeable
Folks using a unix socket could use /run/miniflux/miniflux.sock without permission issue
2020-11-09 21:13:03 -08:00
Frédéric Guillot 5e59fcbd4f debian package: add missing post-install script
The miniflux user is not created on new installations
2020-11-09 20:42:12 -08:00
Frédéric Guillot 246a48359c Do not follow redirects when trying known feed URLs
Some websites redirects unknown URLs to the home page.
As result, the list of known URLs is returned to the subscription list.
We don't want the user to choose between invalid feed URLs.
2020-11-06 17:46:54 -08:00
Frédéric Guillot 40e983664c Trim spaces around icon URLs 2020-11-06 17:18:58 -08:00
Mark Woodbridge 1eb239bf88 Reinstate EXPOSE instruction
This ensures that the relevant port is published when using `docker run -P`, Traefik's Docker integration etc
2020-11-04 13:38:45 -08:00
Benedikt Hopmann d5afdf7691 Update german translation 2020-11-03 16:21:10 -08:00
Frédéric Guillot b539eb8575 Fix auto-generated version (git describe) 2020-11-03 15:14:57 -08:00
Frédéric Guillot b8f7f44bb0 Update ChangeLog for version 2.0.25 2020-11-03 11:53:28 -08:00
Frédéric Guillot 2249de439e Rename original link to be more explicit 2020-10-31 18:41:13 -07:00
Frédéric Guillot 4f358aa0f3 Do not escape HTML for Atom 1.0 text content during parsing
Avoid encoding single quotes to HTML entities (&#39;).

Feed contents are sanitized after parsing.
2020-10-30 23:41:33 -07:00
Frédéric Guillot 2f3708d40c Do not use charset.NewReader if the body is a valid UTF-8 document 2020-10-30 23:00:05 -07:00
Lee Tang 46c13b5185 Restore the ability to use a proxy for all HTTP requests
The default transport was changed in commit 0d66f2c and the proxy feature was forgotten.

- https://golang.org/src/net/http/transport.go#L43
- https://golang.org/pkg/net/http/#ProxyFromEnvironment
2020-10-30 19:03:41 -07:00
Frédéric Guillot d70b4d39ab Show Git commit in about page 2020-10-28 21:22:23 -07:00
Frédéric Guillot a37a2dd043 Publish Docker images to GitHub Container Registry 2020-10-28 21:18:07 -07:00
Frédéric Guillot 519fbcf581 Add few Docker Compose examples 2020-10-27 21:27:15 -07:00
Quotengrote e74bf260c6 Added Ansible Role + Playbook for Miniflux 2020-10-27 14:37:29 -07:00
Frédéric Guillot 99f20868ad go mod tidy + go generate 2020-10-19 22:19:48 -07:00
Frédéric Guillot b30a045a4e Refactor entry filtering
Avoid looping multiple times across entries
2020-10-19 22:18:41 -07:00
Frédéric Guillot b50778d3eb Add rewrite rule to use noscript content for images rendered with Javascript 2020-10-19 21:31:10 -07:00
Frédéric Guillot 5ed1d7537a Docker workflow should checkout entire Git history to compute build version 2020-10-19 20:25:15 -07:00
Frédéric Guillot a2021b74ba Generate Docker tag from GitHub Action 2020-10-19 17:56:30 -07:00
dependabot[bot] 80b68fb126 Bump github.com/prometheus/client_golang from 1.7.1 to 1.8.0
Bumps [github.com/prometheus/client_golang](https://github.com/prometheus/client_golang) from 1.7.1 to 1.8.0.
- [Release notes](https://github.com/prometheus/client_golang/releases)
- [Changelog](https://github.com/prometheus/client_golang/blob/master/CHANGELOG.md)
- [Commits](https://github.com/prometheus/client_golang/compare/v1.7.1...v1.8.0)

Signed-off-by: dependabot[bot] <support@github.com>
2020-10-18 22:57:27 -07:00
Frédéric Guillot f87b05c90f Update contributor link and Godoc badge for client 2020-10-18 21:53:07 -07:00
Frédéric Guillot 73d73dc61e Move Debian package builder to main repo 2020-10-18 21:28:42 -07:00
Frédéric Guillot 578cd8b24b Move RPM build files to main repository 2020-10-18 16:20:00 -07:00
Frédéric Guillot bae68520a2 Add GitHub Action to generate Docker images 2020-10-18 15:30:49 -07:00
Frédéric Guillot 6e20d37dda Build multi-platform images with Docker Buildx 2020-10-18 15:09:47 -07:00
Richard Schneider cc8eb89ce0 Add keyboard shortcut to scroll current item to the top 2020-10-16 15:44:03 -07:00
Manuel Garrido 84b83fc3c8 Add feed filters (Keeplist and Blocklist) 2020-10-16 14:40:56 -07:00
Frédéric Guillot 3afdf25012 Do not proxy image data url 2020-10-14 22:26:54 -07:00
dependabot[bot] 5c3e78f605 Bump github.com/PuerkitoBio/goquery from 1.5.1 to 1.6.0
Bumps [github.com/PuerkitoBio/goquery](https://github.com/PuerkitoBio/goquery) from 1.5.1 to 1.6.0.
- [Release notes](https://github.com/PuerkitoBio/goquery/releases)
- [Commits](https://github.com/PuerkitoBio/goquery/compare/v1.5.1...v1.6.0)

Signed-off-by: dependabot[bot] <support@github.com>
2020-10-11 22:26:32 -07:00
Frédéric Guillot 6117a2f3cd Proxify articles crawled manually 2020-10-07 22:26:52 -07:00
Frédéric Guillot 997006cdd7 Proxify images defined in srcset attribute 2020-10-07 22:02:00 -07:00
Tony Wang 37bc451741 Remove plaintext Fever password from database 2020-10-06 21:22:24 -07:00
Richard Schneider 93f90dae68 Add keyboard shortcut to jump to an item's feed page 2020-10-06 18:06:12 -07:00
Philipp Rempe 7da72cf89a Add setting for swipe gesture on entries on mobile 2020-10-04 17:45:18 -07:00
Frédéric Guillot ded70cd250 Update ChangeLog 2020-10-03 18:16:09 -07:00
Frédéric Guillot 31435ef83e Add rewrite rule to fix Medium.com images 2020-09-29 22:27:32 -07:00
Frédéric Guillot d75ff0c5ab Add sanitizer support for responsive images
- Add support for picture HTML tag
- Add support for srcset, media, and sizes attributes to img and source tags
2020-09-28 23:22:08 -07:00
Frédéric Guillot c0eb66fe22 Enhance man page formatting 2020-09-28 20:45:57 -07:00
Frédéric Guillot c394a61a4e Add Prometheus exporter 2020-09-27 20:04:48 -07:00
Frédéric Guillot 16b7b3bc3e http client: remove dependency on global config options 2020-09-27 14:37:46 -07:00
Frédéric Guillot 065331c77f api: avoid database lookup if empty credentials are provided 2020-09-27 13:23:11 -07:00
Frédéric Guillot 2b74c6779a Add Feed ID in worker error logs 2020-09-27 13:22:50 -07:00
Frédéric Guillot 0d66f2c6d3 Tweak default HTTP client transport timeout values
Reducing these values avoid going over the max number of file descriptors when refreshing lot of feeds
2020-09-27 13:20:48 -07:00
Dave Z 6d25fb715b Various CSS tweaks and optimizations
Prevent sub and sup from affecting line-height.

Set touch-action on .items to prevent browser navigation.

Move font-family specific CSS to the appropriate file.

Update primary font-family for UI to be even more
compatible with various operating systems.

Make .entry-content font-weight variable depending
on font-family used.
2020-09-26 18:21:26 -07:00
junbaor 0ddfa5f7d9 Update zh_CN translation for page.api_keys.table.token 2020-09-26 13:44:01 -07:00
Frédéric Guillot b015887b02 Avoid Javascript minifier to break keyboard shortcuts 2020-09-23 21:01:58 -07:00
Frédéric Guillot 75b11d5744 Rename service worker script to avoid being blocked by uBlock 2020-09-23 20:40:06 -07:00
Dave Marquard eb026ae4ac handle Pacific Daylight Time in addition to Pacific Standard Time 2020-09-22 19:47:36 -07:00
Frédéric Guillot 38d3b28445 Create index to speed up bookmark page 2020-09-21 20:36:19 -07:00
Frédéric Guillot 0d0395b4e3 Do not try to update a duplicated feed after a refresh 2020-09-20 23:42:18 -07:00
Frédéric Guillot e6c6ee441a Use a transaction to refresh and create entries
Also includes few database improvements:

- Speed up entries clean up with an index and a goroutine
- Avoid the accumulation of enclosures for some feeds
2020-09-20 23:12:23 -07:00
Frédéric Guillot bfb96d536e Add workaround for parsing an invalid date 2020-09-14 21:23:26 -07:00
Frédéric Guillot ad66250734 Archive older entries first 2020-09-14 20:21:38 -07:00
Frédéric Guillot acd318b640 Update API client to support more filters 2020-09-14 20:12:41 -07:00
Frédéric Guillot 55fad7ea27 Avoid duplication between get feed entries and get entries API endpoints 2020-09-13 21:57:55 -07:00
Frédéric Guillot debbf5a5b0 Enable strict slash to avoid 404 when using trailing slash 2020-09-13 18:02:08 -07:00
Frédéric Guillot dd3078dc69 Add submit button to each section of integration page 2020-09-12 22:03:05 -07:00
Frédéric Guillot d9f52bb634 API: Add the possibility to filter entries by category ID 2020-09-12 21:46:26 -07:00
Frédéric Guillot 04c4890124 API: Add the possibility to filter entries by a list of statuses 2020-09-12 21:35:18 -07:00
yifei 1c103337fe Reload page after making page as read when showing unread entries 2020-09-12 20:35:01 -07:00
Frédéric Guillot 13c89c29c5 Add option to archive unread entries 2020-09-12 20:17:52 -07:00
Frédéric Guillot df7a6e18fd Add option to enable maintenance mode 2020-09-12 18:42:28 -07:00
Frédéric Guillot a8de067810 go mod tidy + go generate 2020-09-09 23:31:25 -07:00
Kebin Liu cf7712acea Add HTTP proxy option for subscriptions 2020-09-09 23:28:54 -07:00
alex 0f258fd55b Make add_invidious_video rule applicable for different invidious instances 2020-09-06 13:41:42 -07:00
Maxime Bailleul bf2f7d1e16 Fix reading time for jp, ko and zh languages 2020-09-06 13:15:11 -07:00
Shizun Ge e1b1618015 Update POLLING_SCHEDULER description in man page 2020-09-01 22:33:13 -07:00
dependabot[bot] e5d09491d4 Bump github.com/gorilla/mux from 1.7.4 to 1.8.0
Bumps [github.com/gorilla/mux](https://github.com/gorilla/mux) from 1.7.4 to 1.8.0.
- [Release notes](https://github.com/gorilla/mux/releases)
- [Commits](https://github.com/gorilla/mux/compare/v1.7.4...v1.8.0)

Signed-off-by: dependabot[bot] <support@github.com>
2020-08-24 19:57:25 -07:00
Pedro Lucas Porcellis e1c9e6ccb4 Add link to mark a feed as read 2020-08-19 22:17:33 -07:00
Frédéric Guillot 5947e2a664 Update ChangeLog 2020-08-15 15:40:16 -07:00
Frédéric Guillot e95c6ce8f4 Upgrade CI checks to Go 1.15 2020-08-11 21:24:43 -07:00
Frédéric Guillot c1e3783272 Revert "Set SameSite cookie attribute to Strict"
This reverts commit 5ac55518ab.

Google Authentication doesn't work when Cookies are using strict mode.
2020-08-10 18:51:40 -07:00
Frédéric Guillot eaf1fc896f Redirect to login page if CSRF token is expired
This will force the form to be populated with a valid token.
2020-08-09 13:16:57 -07:00
Frédéric Guillot e97ebfd8c2 Create index to speed up Fever API calls 2020-08-08 23:18:09 -07:00
Frédéric Guillot cc281f3533 Increase http server timeout values 2020-08-08 22:35:25 -07:00
Frédéric Guillot 4e07b549a2 Fever API: add client IP in middleware logs 2020-08-08 21:51:52 -07:00
Frédéric Guillot 019cf79704 Fever API: do not send article to external service when unsaving an item 2020-08-08 21:34:04 -07:00
Frédéric Guillot a102843789 Improve Fever API logging 2020-08-08 21:19:47 -07:00
Frédéric Guillot bf443a65e2 Fever API: fix max_id argument logic to follow the specs 2020-08-08 21:13:43 -07:00
Frédéric Guillot 5ac55518ab Set SameSite cookie attribute to Strict 2020-08-05 21:31:32 -07:00
dependabot[bot] 514f518d2a Bump github.com/lib/pq from 1.7.1 to 1.8.0
Bumps [github.com/lib/pq](https://github.com/lib/pq) from 1.7.1 to 1.8.0.
- [Release notes](https://github.com/lib/pq/releases)
- [Commits](https://github.com/lib/pq/compare/v1.7.1...v1.8.0)

Signed-off-by: dependabot[bot] <support@github.com>
2020-08-03 21:26:03 -07:00
Frédéric Guillot fc75b0cd8e Add workaround to get YouTube feed from video page 2020-08-02 12:24:46 -07:00
Frédéric Guillot 7380c64141 Add workaround to find YouTube channel feeds
YouTube doesn't expose RSS links anymore for new-style URLs.
2020-08-02 11:37:07 -07:00
Frédéric Guillot 943e7a7317 Align entry actions to the left
- Attempt to avoid awkward alignment on smartphone screens
- Keep the read/star actions aligned to the left
- Remove css flex to allow easier override with custom CSS
2020-07-28 22:26:32 -07:00
Frédéric Guillot 8fb71366f8 API: Delete users asynchronously
Deleting large users might lock the tables in the hosted offering
2020-07-28 20:39:10 -07:00
Frédéric Guillot 74c95ed34b API client: Do not return body for response with no content 2020-07-27 18:54:21 -07:00
dependabot[bot] 82b3ea31e6 Bump github.com/lib/pq from 1.7.0 to 1.7.1
Bumps [github.com/lib/pq](https://github.com/lib/pq) from 1.7.0 to 1.7.1.
- [Release notes](https://github.com/lib/pq/releases)
- [Commits](https://github.com/lib/pq/compare/v1.7.0...v1.7.1)

Signed-off-by: dependabot[bot] <support@github.com>
2020-07-27 18:21:18 -07:00
Benedikt Hopmann 1b5f217e9c Update german translation 2020-07-17 19:09:08 -07:00
Maxime Bailleul ee5a8a05c9 Add reading time for entries 2020-07-16 19:47:41 -07:00
Pedro Lucas Porcellis 499fb1f7df Add Brazillian Portuguese (pt_BR) translation
I've intentionally kept a differenciation between a Subscrition
(Inscrição) and a Feed (Fonte) as it makes more sense.

I also attempted to avoid using non-portuguese words like Consumer Key,
Consumer Secret etc. Some specific cases to avoid weird mistranslation I
have kept the original in parentheses.
2020-07-15 21:22:00 -07:00
logan 5f266319a3 Add option to change the number of entries per page (fixes #40) 2020-07-08 20:14:31 -07:00
Shizun Ge e32fa059e5 Do not archive shared items 2020-07-01 20:11:36 -07:00
L11R 3371b15631 Russian translation fixes 2020-06-30 20:51:30 -07:00
Sergei O. Udalov c0e407e156 Display recent entries first in search results 2020-06-30 20:50:34 -07:00
Frédéric Guillot d2f4ed93df Add support for secret keys exposed as a file
Secret keys are often exposed as a file in containerized environments.
2020-06-29 21:01:37 -07:00
Frédéric Guillot 1d6b0491a7 Ignore <media:title> in RSS 2.0 feeds
In the vast majority of cases, the default entry title is correct.

Ignoring <media:title> avoid overriding the default title if they are different.
2020-06-29 18:24:06 -07:00
Mohamad Safadieh c70bebb2aa Add support for RTL feed content 2020-06-24 21:46:37 -07:00
Kun Devanny f7273e5cdf Use getEntryIDs instead of getEntries in Fever API 2020-06-23 21:48:25 -07:00
Frédéric Guillot 8304666261 Use stdlib constants for HTTP methods instead of strings 2020-06-21 21:21:10 -07:00
Frédéric Guillot ec18abc97f Add DELETE method to CORS header 2020-06-21 21:04:34 -07:00
Andras Bali adbebd4de8 Added CORS support to API. 2020-06-21 20:57:51 -07:00
Gabriel Augendre e44b4b2540 Try known urls if no link alternate
I came across a few blogs that didn't have a link rel alternate
but offered a RSS/Atom feed.
This aims at solving this issue for "well known" feed urls, since
these urls are often the same.
2020-06-21 20:34:59 -07:00
Frédéric Guillot 248cb38390 Update ChangeLog 2020-06-19 11:43:05 -07:00
WasabiPizza 506c2ab1c9 Fix inverted entry sorting in the Italian translation 2020-06-18 20:59:18 -07:00
Frédéric Guillot 43bf6696d5 Revert "Always align action items to the right even when the window is small "
This reverts commit c9618458cb.
2020-06-17 21:48:07 -07:00
Frédéric Guillot f52f00abcf Fix regression: the entire hstore field is replaced when updating settings 2020-06-16 20:02:13 -07:00
dependabot[bot] da4ea18003 Bump github.com/lib/pq from 1.6.0 to 1.7.0
Bumps [github.com/lib/pq](https://github.com/lib/pq) from 1.6.0 to 1.7.0.
- [Release notes](https://github.com/lib/pq/releases)
- [Commits](https://github.com/lib/pq/compare/v1.6.0...v1.7.0)

Signed-off-by: dependabot[bot] <support@github.com>
2020-06-15 20:46:47 -07:00
Frédéric Guillot 5e5615a2d1 Add icons to feeds and categories list 2020-06-14 19:22:31 -07:00
Shizun Ge fe8347f32a Change feed title to a link to original website 2020-06-11 21:07:13 -07:00
Shizun Ge d2b360f7b3 Replace link to categories by a link to the list of entries in "Category > Feeds" page
Solves #659
2020-06-10 20:33:58 -07:00
Shizun Ge c9618458cb Always align action items to the right even when the window is small 2020-06-10 20:24:11 -07:00
Manuel Müller ca918bc7e3 Added scraper rule for dilbert.com and turnoff.us 2020-06-10 20:15:46 -07:00
schlich c820197667 Fix typo in German translation 2020-06-06 13:40:44 -07:00
Frédéric Guillot 6c6ca69141 Add feed option to ignore HTTP cache 2020-06-05 22:04:52 -07:00
Frédéric Guillot 57f62b4797 Update Docker image to Alpine 3.12 2020-06-02 20:55:34 -07:00
dependabot[bot] 1811522473 Bump github.com/lib/pq from 1.4.0 to 1.6.0
Bumps [github.com/lib/pq](https://github.com/lib/pq) from 1.4.0 to 1.6.0.
- [Release notes](https://github.com/lib/pq/releases)
- [Commits](https://github.com/lib/pq/compare/v1.4.0...v1.6.0)

Signed-off-by: dependabot[bot] <support@github.com>
2020-06-02 19:57:11 -07:00
Frédéric Guillot 8e500e8bef Move dependabot.yml to .github folder 2020-06-02 18:56:42 -07:00
Frédéric Guillot a2abd38ccf Add Dependabot 2020-06-01 22:02:41 -07:00
Vitor Pellegrino 7fb0bdc9a5 Adds keyboard shortcut for refreshing all feeds
- Binds the 'R' key to trigger a refresh in the background for all
feeds.
- Updates the locale, using the same description as the link in the
feeds page.

Co-authored-by: Vitor Pellegrino <pellegrino@linux.com>
2020-05-26 21:35:44 -07:00
Frédéric Guillot 7e5157f218 Rename alternative scheduler to entry_frequency 2020-05-25 15:12:47 -07:00
Shizun Ge cead85b165 Add alternative scheduler based on the number of entries 2020-05-25 14:06:56 -07:00
Corey McCaffrey 25d4b9fc0c Added scraper rule for financialsamurai.com
The default rule results in blank content.
2020-05-24 13:29:28 -07:00
Frédéric Guillot 4cdfd64db0 Speed up feed list page rendering 2020-05-22 18:02:49 -07:00
Frédéric Guillot 70c2568af8 Add index to speed up slow query 2020-05-20 18:19:35 -07:00
Corey McCaffrey 0683074b8b Added scraper rule for TheOatmeal.com
The default rule does not show the comic posted to the feed. The comic image is in a div with id "comic".
2020-05-13 21:28:00 -07:00
Corey McCaffrey 8f6c07afd6 Added scraper rule for RayWenderlich.com
RayWenderlich.com is a popular developer's community for iOS and Android developers. The default rule results in "GROUP GROUP GROUP GROUP…" instead of the content posted on the blog.
2020-05-13 21:28:00 -07:00
Frédéric Guillot 3b2f0a633c Update dependencies and remove vendor folder 2020-04-25 21:11:48 -07:00
Frédéric Guillot c920a8607c Add CSS styles for textarea 2020-04-25 20:35:29 -07:00
Frédéric Guillot eb8d8e84f2 Updating custom css field should also happen when updating the password
Bug introduced in PR #627
2020-04-25 20:26:02 -07:00
Frédéric Guillot 619aa58fb3 Handle more invalid dates
Fixes #617
2020-04-25 20:15:18 -07:00
Alex McGrath 7accdc4416 Add the ability to use custom css 2020-04-22 20:53:09 -07:00
Sebastian Wiesner ae8e5c4dbc Add /version endpoint
Closes #629
2020-04-17 18:39:53 -07:00
Savely Krasovsky 454eb590ce Remove child-src CSP policy (deprecated) 2020-03-31 19:50:25 -07:00
Frédéric Guillot 9698c5e40a Update ChangeLog 2020-03-28 14:51:26 -07:00
Frédéric Guillot 48680577a8 Update man page 2020-03-28 14:50:11 -07:00
Frédéric Guillot a170e6454f Restore original read/unread popups 2020-03-28 10:45:31 -07:00
Frédéric Guillot 25f90bc625 Restore original read/unread button in entry view 2020-03-25 20:22:40 -07:00
Benedikt Hopmann c3f3cc085a Update German translation 2020-03-23 18:20:40 -07:00
Frédéric Guillot 9871e4f5d0 Add page to list and remove shared entries 2020-03-22 19:53:44 -07:00
Frédéric Guillot 84229f1af9 Add some CSS tweaks for mobile devices 2020-03-22 16:33:35 -07:00
Frédéric Guillot 3c3241f93c Add SVG icons to entry actions 2020-03-22 15:37:13 -07:00
Frédéric Guillot 53dd2c1f1d Move schema version check after migration execution 2020-03-21 15:01:16 -07:00
Frédéric Guillot 592151bdb6 Add support for Invidious
- Embed Invidious player for invidio.us feeds
- Add new rewrite rule to use Invidious player for Youtube feeds
2020-03-20 20:56:59 -07:00
Frédéric Guillot e494d6e381 Check during startup if the database schema is up to date 2020-03-20 20:12:15 -07:00
Frédéric Guillot 05b88c8add Change share uri 2020-03-19 20:56:23 -07:00
Frédéric Guillot 7a397c3d13 Change default theme to "system_serif" 2020-03-19 20:53:53 -07:00
Frédéric Guillot bd5aed7b90 Fix template execution error (related to share feature) 2020-03-18 18:04:06 -07:00
Frédéric Guillot 59c8dac851 Display Feed URL on shared page instead of user feed entries 2020-03-17 20:28:53 -07:00
Lesterpig 41a2b7e58e Add "Share article" feature
A new "shareCode" field is generated for each entry, and allows
unlogged users to access the entry through the /shared endpoint.
This feature is particularly useful to share articles from miniflux
to third-party users without having them to visit the original source.

The image proxy is disabled and special cache headers are proposed in
the shared page to avoid denial of service.
2020-03-17 20:09:46 -07:00
Frédéric Guillot 1b86913c00 Fix SQL injection in full-text search rank ordering
Introduced in commit 267b706544
2020-03-10 21:12:28 -07:00
Patrick 3e1e0b604f Add generic OpenID Connect provider (OAuth2)
This adds the oauth2 provider `oidc`. It needs an additional argument, the OIDC discovery endpoint to figure out where the auth and token URLs are.

Configuration is similar to setting up the Google Authentication with these changes:

 * `OAUTH2_PROVIDER = oidc`
 * `OAUTH2_OIDC_DISCOVERY_ENDPOINT = https://auth.exampe.org/discovery`
2020-03-07 18:45:19 -08:00
Dave Marquard 54602b55bb Use secure TLS configuration for autocert server
This change lets Miniflux use the same secure TLS configuration options when using Let's Encrypt / Autocert as when using a manually specified certificate. It raises the server’s SSL Labs score from a B to an A+ with LetsEncrypt.
2020-03-02 21:30:48 -08:00
Frédéric Guillot 086b985383 Add API Client function to refresh all feeds 2020-03-01 19:45:10 -08:00
Frédéric Guillot 25cc0d2447 Add per-application API Keys 2020-03-01 18:22:45 -08:00
Frédéric Guillot d1afe13a1c Add Go 1.14 to GitHub Actions 2020-03-01 10:26:03 -08:00
Andrew Williams 9974e0f458 Addition of scraper rule for wdwnt.com
By default fetching original content for wdwnt.com results in a snippet of the comments section, this rule captures the article content.
2020-02-28 20:24:58 -08:00
Aled Griffiths 7b32f59281 Add refreshAllFeeds to the API
Adds refreshAllFeeds to the API, following the suggestion given in the
comments of #359.
2020-02-26 20:10:56 -08:00
Pavel Borzenkov 7389c79c52 Implement support for authentication via Auth Proxy
Auth Proxy allows to authenticate a user using an HTTP header provided
by an external authentication service. This provides a way to
authenticate users in miniflux using authentication schemes not
supported by miniflux itself (LDAP, non-Google OAuth2 providers, etc.)
and to implement SSO for multiple applications behind single
authentication service.

Auth Proxy header is checked for the '/' endpoint only, as the rest are
protected by the miniflux user/app sessions.

Closes #534

Signed-off-by: Pavel Borzenkov <pavel.borzenkov@gmail.com>
2020-02-24 19:43:45 -08:00
Vincent Bernat d5adf8b9f6 Use rel=prev/next on pagination link
While this is widely documented for SEO reason, it also helps for
accessibility. Notably, if you are using tools like Vimium to browse
using only the keyboard, these hints help to select the links. It's
all the more useful when the text is not in English.
2020-02-23 13:14:12 -08:00
Frédéric Guillot 47de317b65 Update list of changes 2020-02-15 18:00:00 -08:00
Hiroshi Okada 9e3252d1b0 Add Japanese translation 2020-02-14 19:52:16 -08:00
Peter De Wachter 2570c3410b History: show entries in the order in which they were read
Add a changed_at timestamp to the entries table. This field is updated
whenever the entry's metadata changes.
2020-02-10 20:20:03 -08:00
Frédéric Guillot dc4240e702 Add button to add to Home screen 2020-02-09 11:57:34 -08:00
Frédéric Guillot 997e9422eb Ignore enclosures without URL 2020-01-30 21:18:49 -08:00
Benjamin Barenblat cb494ac416 Correct spelling of “toggle” 2020-01-29 18:36:09 -08:00
somini 078f886f39 Align Info and Actionable items separately
Align the information to the left side, and the actionable buttons to
the right.

See https://github.com/miniflux/miniflux/pull/531#issuecomment-578547488
2020-01-28 20:25:10 -08:00
Vincent Bernat 15727f716a Redirect to /unread when getting a 404 for an unread expired entry
Hosted miniflux don't keep read entries for long. If I leave my PC at
home on an unread entry, read everything pending at work and get back
to home later, when I click "next", I get a bare 404 error. I have to
go back to /unread myself. I think it would be more user friendly (but
maybe a bit suprising) to go directly to /unread in this case.
2020-01-24 20:27:50 -08:00
Richard Schneider 476898b1d4 Do not advance to the next item when using the 'v' shortcut on the list of starred items 2020-01-14 11:54:21 -08:00
Richard Schneider 1a78e3a5bc Wrap around when navigating with keyboard shortcuts on a list view 2020-01-10 19:26:45 -08:00
Frédéric Guillot 86e71c48f5 Remove unused Feed.Entries from API client 2020-01-07 21:39:50 -08:00
Richard Schneider 6ebb29cd22 Add comments link keyboard shortcut 2020-01-06 22:02:02 -08:00
Frédéric Guillot e2315e6a54 Remove unused Entry.Category in API client 2020-01-05 22:52:38 -08:00
Frédéric Guillot 61f0c8aa66 Allow application/xhtml+xml links as comments URL in Atom replies 2020-01-04 16:07:06 -08:00
Frédéric Guillot bf632fad2e Allow only absolute URLs in comments URL
Some feeds are using invalid URLs (random text).
2020-01-04 15:54:16 -08:00
Kebin Liu 8cebd985a2 Use internal XML workarounds to detect feed format 2020-01-02 22:19:15 -08:00
Frédéric Guillot 6ff617a733 Make menu consistent across feed pages 2020-01-02 13:16:29 -08:00
Frédéric Guillot 4d9956cf65 Make sure external URLs are not encoded incorrectly by Go template engine 2020-01-02 11:20:10 -08:00
Frédéric Guillot ac3c936820 Make sure whitelisted URI schemes are handled properly by the sanitizer 2020-01-02 11:03:51 -08:00
Frédéric Guillot 08fc32b0e1 Use white background for favicon
Improve legibility when using a dark theme.
2019-12-30 15:51:52 -08:00
Frédéric Guillot d8ad49d14d Remove dependency on Sed to build Docker images
- Keep the Dockerfile standard and avoid external dependencies
- Use build args instead of Sed
- Bump Alpine Linux to version 3.11
- Always use the latest version of Golang
2019-12-28 11:38:07 -08:00
Frédéric Guillot 3debf75eb9 Normalize URL query string before executing HTTP requests
- Make sure query strings parameters are encoded
- As opposed to the standard library, do not append equal sign
for query parameters with empty value
- Strip URL fragments like Web browsers
2019-12-26 15:56:59 -08:00
Frédéric Guillot 200b1c304b Improve Dublin Core support for RDF feeds 2019-12-23 14:45:58 -08:00
Frédéric Guillot 1b33bb3d1c Improve Podcast support (iTunes and Google Play feeds)
- Add support for Google Play XML namespace
- Improve existing iTunes namespace implementation
2019-12-23 13:51:42 -08:00
Frédéric Guillot 33fdb2c489 Add support for Atom 0.3 2019-12-22 22:42:00 -08:00
Frédéric Guillot cfb6ddfcea Add support for Atom 'replies' link relation
Show comments URL for Atom feeds as per RFC 4685.
See https://tools.ietf.org/html/rfc4685#section-4

Note that only the first link with type "text/html" is taken into consideration.
2019-12-22 18:03:04 -08:00
cinput 8e1ed8bef3 Return outer HTML when scraping elements 2019-12-21 21:18:31 -08:00
somini 30f22fbd78 Update scraper rule for "Le Monde" 2019-12-19 18:35:29 -08:00
Jebbs a155ab6deb Filter valid XML characters for UTF-8 XML documents before decoding
This change should reduce "illegal character code" XML errors.
2019-12-19 18:31:52 -08:00
Frédéric Guillot a4ebb33cd5 Trim spaces for RDF entry links 2019-12-01 15:06:01 -08:00
Frédéric Guillot 64fa1aa8cf Update ChangeLog 2019-12-01 11:44:42 -08:00
Frédéric Guillot 120d6ec7d8 Do no rewrite Youtube description twice in "add_youtube_video" rule
This is already done before in <media:description>.
2019-11-30 22:56:06 -08:00
Frédéric Guillot 6291b59849 Use V to open original link in current tab
- To avoid a breaking change, keep v to open the original link in new tab.
- People who prefers to open the link in the current tab should use V.
2019-11-29 13:58:47 -08:00
Frédéric Guillot a96e966911 Do not expose yet feed counter via API 2019-11-29 11:56:00 -08:00
Frédéric Guillot 69aa650203 Add the possibility to add rules during feed creation 2019-11-29 11:27:58 -08:00
Frédéric Guillot 8028cc764f Wrap attachments into <details> disclosure element 2019-11-29 10:59:05 -08:00
Frédéric Guillot b3869a7833 Show attachment size on entry page 2019-11-29 10:36:35 -08:00
Frédéric Guillot 912a98788e Add support of media elements for Atom feeds 2019-11-28 23:55:40 -08:00
Frédéric Guillot f90e9dfab0 Add support of media elements for RSS 2 feeds 2019-11-28 21:33:32 -08:00
Frédéric Guillot c43c9458a9 Add rewrite functions: convert_text_link and nl2br 2019-11-28 21:33:12 -08:00
Neo Ng 90064a8cf0 Update scraper rule for openingsource.org 2019-11-28 19:40:26 -08:00
Frédéric Guillot bacab20f4a Add Makefile target to build only amd64 Docker image 2019-11-26 23:00:17 -08:00
Frédéric Guillot 7b995044b4 Make sure to remove integration settings when removing a user 2019-11-17 23:11:59 -08:00
Frédéric Guillot e878dca3d7 Add API parameter to filter entries by category 2019-11-17 23:05:22 -08:00
Frédéric Guillot fad9ad2be4 Display list of feeds per category 2019-11-17 20:10:44 -08:00
Frédéric Guillot 15fe9c20df Show the number of read and unread entries for each feed 2019-11-17 18:04:14 -08:00
Frédéric Guillot 789ad8257a Make sure settings menu is consistent 2019-11-17 17:40:24 -08:00
Frédéric Guillot 63a549f0e3 Remove fixed table-layout for entry content 2019-11-17 17:08:20 -08:00
Frédéric Guillot d422d4e067 Update autocert lib because ACME v1 is EOL 2019-11-15 20:34:55 -08:00
Vincent Bernat 881c0f406b Do not lighten blockquote font color
It is confusing to have blockquote content displayed with a lighter
font color. Usually, quotes are important to the text while reducing
the contrast may hint a user it is not. In my case, I usually skip the
content of the blockquote as it was some additional note in the text.

Without the color change, the blockquote is still indented
differently, with the vertical bar. It should be enough for a user to
identify it as a quote.
2019-11-15 19:52:40 -08:00
benne ee84731153 Update de_DE translation 2019-11-01 20:23:51 -07:00
Jebbs b62ecc6699 Send a response when changing status of removed entries in Fever API
When a client, like Reeder, tries to mark an item which is removed on Miniflux, the server doesn't response OK. This may terminate the sync process, the PR fixes it.
2019-10-31 18:59:04 -07:00
Maxim Baz 21f8b03483 Make "v" open link in the current tab on entry page 2019-10-30 20:19:48 -07:00
Frédéric Guillot 03406c68d0 Add meta tag to disable Google Translate 2019-10-29 23:06:25 -07:00
Frédéric Guillot d3883126bf Improve storage module 2019-10-29 22:59:00 -07:00
Maxim Baz e38333e272 Show unread counters on feeds page 2019-10-29 21:44:35 -07:00
Tony Wang 2eb2441f2b Improve XML decoder to remove illegal characters 2019-10-22 20:32:35 -07:00
Tony Wang 7409bba0d8 Compare Fever token case-insensitively 2019-10-21 20:00:16 -07:00
Frédéric Guillot 70940e768c Fix integration tests 2019-10-21 19:56:48 -07:00
Frédéric Guillot 37f93d5127 Make sure integration tests are marked as failed in Github Actions 2019-10-21 19:38:24 -07:00
Tony Wang 5517eebafe Add new formats to date parser 2019-10-20 09:52:18 -07:00
Jebbs bf357a0e2d Add toast message for some keyboard shortcuts
Add notification message when using keyboard shortcuts: f, s, and m.
2019-10-06 20:55:15 -07:00
Adam Hess 02dbe3ef2e call preventDefault() when the user is trying to enter a keyboard shortcut
Keyboard shortcuts might conflict with Firefox’s "Find as you type" feature.
2019-10-06 16:24:39 -07:00
Frédéric Guillot bf2ceded96 Add missing arm32v7 in loop 2019-09-30 23:20:22 -07:00
Frédéric Guillot 97d1c6f02b Update ChangeLog for v2.0.18 2019-09-25 20:23:57 -07:00
Frédéric Guillot e0b600b651 Add CSS tweak and fix typo in variable name 2019-09-25 19:55:55 -07:00
Frédéric Guillot 3379429329 Log image proxy URL in debug mode 2019-09-22 11:38:45 -07:00
Frédéric Guillot bdca3372f9 Add few CSS teaks 2019-09-22 10:50:10 -07:00
Frédéric Guillot ca9cc692e4 Add Docker image variant for arm32v7 2019-09-22 10:49:42 -07:00
Frédéric Guillot afe1faf214 Add theme variants
- Use CSS variables instead of inherence
- Rename default theme to "Light - Serif"
- Rename Black theme to "Dark - Serif"
- Rename "Sans-Serif" theme to "Light - Sans Serif"
- Add "System" theme that use system preferences: Dark or Light
- Add Serif and Sans-Serif variants for each color theme
2019-09-21 20:04:42 -07:00
Frédéric Guillot d610d091fe Avoid constraint error when having duplicate entries
During feed creation, duplicated entries will generate an SQL contraint error.

This change ignore the duplicated entry to avoid showing an error.
2019-09-18 22:53:47 -07:00
Frédéric Guillot 36d7732234 Disable strict XML parsing
This change should improve parsing of broken XML feeds.

See https://golang.org/pkg/encoding/xml/#Decoder
2019-09-18 22:45:56 -07:00
Frédéric Guillot ca48f7612a Ignore invalid content type 2019-09-18 22:32:29 -07:00
Frédéric Guillot 7d50cc40de Update man page 2019-09-15 12:01:28 -07:00
Frédéric Guillot 934385ff55 Replace Travis by GitHub Actions 2019-09-15 11:48:15 -07:00
Ty Cobb fb9a1a6129 Rename cleanup config variables
The config parser logs a warning when the user uses a deprecated variable. It also ignores the value from a deprecated variable if it has already been set using the corresponding non-deprecated variable (and logs another warning).

- CLEANUP_FREQUENCY_HOURS instead of CLEANUP_FREQUENCY
- CLEANUP_ARCHIVE_READ_DAYS instead of ARCHIVE_READ_DAYS
2019-09-15 11:47:39 -07:00
Ty Cobb 3a60abbac0 Make configurable the number of days to remove old sessions 2019-09-11 20:10:33 -07:00
Frédéric Guillot 8d8f78241d Add native lazy loading for images and iframes
This feature is available only in Chrome >= 76 for now.

See https://web.dev/native-lazy-loading
2019-09-10 21:22:19 -07:00
Peter De Wachter 937492f6f5 Do not buffer responses in the image proxy
The image proxy buffered the whole image before sending it to the
browser. If the image is large and/or hosted on a slow server, this
caused a long delay before the user's browser could display anything.
2019-09-10 20:43:44 -07:00
Frédéric Guillot b94160df72 Update dependencies 2019-09-05 21:53:12 -07:00
Frédéric Guillot 456ebaf423 Add Go 1.13 to test matrix 2019-09-05 21:28:10 -07:00
Frédéric Guillot 2d27f53941 Replace link border by outline to avoid slight content shift 2019-09-05 21:22:58 -07:00
Peter De Wachter b6f3160dbc add_mailto_subject: New rewrite function
Dinosaur Comics (qwantz.com) likes to hide jokes in mailto: links, but
miniflux's sanitizer strips those out.
2019-08-19 19:42:47 -07:00
Frédéric Guillot 77125f45cc Improve import form 2019-08-15 22:21:18 -07:00
Peter De Wachter d882bbca85 Import OPML from URL 2019-08-15 21:58:01 -07:00
Frédéric Guillot ac45307da6 Add test case for parsing HTML entities 2019-08-15 21:42:13 -07:00
Peter De Wachter ea2b6e3608 addImageTitle: Fix HTML injection
This rewrite rule would change this:

    <img title="<foo>">

to this:

    <figure><img><figcaption><foo></figcaption></figure>

The image title needs to be properly escaped.
2019-08-15 21:39:41 -07:00
Peter De Wachter 3a39d110f0 Accept HTML entities when parsing XML
Every once in a while, one of my feeds would throw an XML parse error
because it used `&nbsp;` or some other HTML entity. I feel Miniflux
should be lenient here, and Go already has a handy hook to make this
work.
2019-08-15 21:26:07 -07:00
Frédéric Guillot 6d39f2a4cb Update ChangeLog for version 2.0.17 2019-08-03 14:38:14 -07:00
Frédéric Guillot f82736641f Update Docker image to Alpine Linux 3.10.1 2019-08-03 14:35:15 -07:00
Frédéric Guillot 3d19313a7f Add option to disable feeds 2019-07-26 21:24:15 -07:00
Jebbs 17aae72583 Fix regressions introduced in previous JS refactoring 2019-07-25 19:02:39 -07:00
Frédéric Guillot a634839678 Ask for confirmation before clicking on mark all as read 2019-07-17 22:00:00 -07:00
Frédéric Guillot ac3693562b Ask for confirmation before flushing history 2019-07-17 21:40:03 -07:00
Frédéric Guillot 99149d9f2a Refactor confirmation handler 2019-07-17 21:15:30 -07:00
Frédéric Guillot 2b6e17c1ef Refactoring of Javascript code 2019-07-17 20:34:11 -07:00
Matías Lang 34421dcd49 Require confirmation for marking pages as read 2019-07-17 17:44:32 -07:00
Frode Aannevik 48abf57218 Add .search margin-right 2019-07-13 13:11:39 +02:00
Frédéric Guillot 867dcb0004 Fix syntax errors in man page 2019-07-10 17:27:17 -07:00
Ilya Glotov c840268678 Sort feed categories before serialization
A function is added for feeds and its categories normalization.
The test will ensure that the order is right.
2019-07-05 20:34:49 +03:00
Pierre Dubouilh f783b135c7 Pass auth header to manifest request 2019-07-04 23:13:25 -07:00
Frédéric Guillot a04fe1aa7e Update Docker image to Alpine Linux 3.10.0 2019-06-20 21:00:59 -07:00
Frédéric Guillot d01a2dd651 Update ChangeLog 2019-06-08 21:13:44 -07:00
Frédéric Guillot ec8fa56c95 Update vendor folder 2019-06-08 18:00:10 -07:00
Frédéric Guillot 91508c50b5 Add option to toggle date/time in log messages 2019-06-08 17:48:45 -07:00
Frédéric Guillot f7b7b63e3f Add optional config file parser in addition to environment variables 2019-06-02 18:28:29 -07:00
Frédéric Guillot bb720c87c1 Make HTTP Client timeout and max body size configurable 2019-06-02 07:29:56 -07:00
Frédéric Guillot 228862fefa Refactor config package
- Parse configuration only once during startup time
- Store configuration values in a global variable
2019-06-02 06:30:08 -07:00
Ben Cohen 04d85b3c63 Flip behavior of j and k keyboard shortcuts 2019-05-26 20:02:52 +02:00
Aaron Bieber 5f487e8c79 Bump pq to v1.1.1 to bring in SCRAM-SHA-256 authentication. 2019-05-16 22:08:37 -07:00
Frédéric Guillot 0dff432337 Remove debug timer from most storage functions 2019-04-28 20:21:31 -07:00
Frédéric Guillot 4295a86e55 Add option to enable/disable keyboard shortcuts 2019-04-28 18:35:58 -07:00
Frédéric Guillot 3c8cc0b2b6 Update links to the documentation 2019-04-22 20:38:07 -07:00
Frédéric Guillot 34df45da2f Add missing translation 2019-04-05 19:31:23 -07:00
Diego Agulló a6068aae1d Improve page reload when showing unread/all entries
- Show only unread entries = refresh current page
- Show all entries = go to next page
2019-04-04 18:59:04 -07:00
Frédéric Guillot 8ee1a09dc7 Always display feed entries even when there is a feed error 2019-04-02 21:33:11 -07:00
Frédéric Guillot f593dc8463 Use loading label instead of saving when submitting login form 2019-03-26 20:52:31 -07:00
Frédéric Guillot 129f1bf3da Add support for OPML v1 import 2019-03-26 20:09:31 -07:00
Jeremy Apthorp 304b43cb30 Add 'allow-popups' to iframe sandbox permissions 2019-03-26 18:26:56 -07:00
Frédéric Guillot 3a483be258 Update ChangeLog 2019-03-16 18:14:00 -07:00
Frédéric Guillot 220d9e3d56 Move Dockerfile to main repo 2019-03-14 20:54:05 -07:00
3ddysan fc473f1d11 Add double tap detection for next/previous page navigation 2019-03-11 20:23:19 -07:00
Jacob Wahlgren 22b68eefd8 Allow users to disable auto-remove
Adds ARCHIVE_READ_DAYS="-1"
2019-03-10 10:51:21 -07:00
Frédéric Guillot 6764a420b0 Make parser compatible with Go 1.12
See changes in strings.Map(): https://golang.org/doc/go1.12#strings
2019-02-28 21:23:33 -08:00
Frédéric Guillot 45df254fe7 Add Golang 1.12 to CI 2019-02-28 20:48:33 -08:00
Frédéric Guillot f3fc8b7072 Use feed ID instead of user ID to check entry URLs presence 2019-02-28 20:43:33 -08:00
Frédéric Guillot 1634e267b8 Fix typo in stylesheet 2019-02-19 17:57:40 -08:00
Jebbs 267b706544 Sort search results by relevance 2019-02-11 22:20:07 -08:00
Frédéric Guillot ed6ae7e0d2 Use preferably the published date for Atom feeds
YouTube feeds use the published date for the original creation date.
2019-01-29 20:01:36 -08:00
e.g. pavelka c71681b197 Add Spanish translation 2019-01-23 19:45:47 -08:00
Frédéric Guillot 04b6eb509c Rename session cookies 2019-01-21 20:26:46 -08:00
Frédéric Guillot 7897d8a8ad Handle the case when application session is expired and not user session 2019-01-21 20:21:05 -08:00
Frédéric Guillot 6378ad2734 Update ChangeLog 2019-01-13 20:35:43 -08:00
Peter De Wachter 6f5ef10553 Only attempt to change password if the confirmation field is filled in
Firefox autocompletes the password field (but not the password
confirmation field) for me. This makes it annoying to use the settings
page, because miniflux thinks I'm trying to change my password and
complains that the fields don't match.
2019-01-07 18:25:59 -08:00
Frédéric Guillot 28ba09e952 Remove URL from client user agent 2019-01-07 18:08:42 -08:00
Peter De Wachter a86a7dce57 Use the appropriate message if there are no unread entries 2019-01-07 17:56:39 -08:00
Peter De Wachter 27e79a4693 Make the feed list order case-insensitive 2019-01-07 17:55:57 -08:00
Peter De Wachter 0cdcec10ca More robust Atom text handling
Miniflux couldn't deal with XHTML Summary elements.

- Make Summary an 'atomContent' field
- Define an atomContentToString function rather than inling it three times
- Also properly escape special characters in plain text fields.
2019-01-07 17:55:02 -08:00
Peter De Wachter 15505ee4a2 Make UTF-8 the default encoding for XML feeds
Consider the feed http://planet.haskell.org/atom.xml
- This is a UTF-8 encoded XML file
- No encoding declaration in the XML header
- No Unicode byte order mark
- Served with HTTP Content-Type "text/xml" (no charset parameter)

Miniflux lets charset.NewReader handle this. The charset package
implements the HTML5 character encoding algorithm, which, in this
situation, defaults to windows-1252 encoding if there are no UTF-8
characters in the first 1000 bytes. So for this feed, we get the wrong
encoding.

I inserted an explicit "utf8.Valid()" check, which fixes this problem.
2019-01-02 21:05:05 -08:00
Clar Charr 31e2669c4d Add more targets to Makefile
I put a comment above them saying that they're unsupported as they won't be tested on CI, etc., but they're still nice to have.
2019-01-02 20:31:48 -08:00
Frédéric Guillot 362fc5c2ff Add -mod=vendor in Makefile 2018-12-28 17:21:57 -08:00
Frédéric Guillot 79e7d007a4 Update man page 2018-12-28 13:55:12 -08:00
Frédéric Guillot a9fad093e6 Move healthcheck endpoint from ui package to httpd service 2018-12-28 13:41:26 -08:00
Frédéric Guillot 56efd2eb3f Add workaround for non GMT dates (RFC822, RFC850, and RFC1123)
RFC822, RFC850, and RFC1123 are supposed to be always in GMT.

This is a workaround for the one defined in PST timezone.
2018-12-26 20:24:38 -08:00
Frédéric Guillot 078cd0050a Make sure that <strong> elements are bold 2018-12-19 22:30:05 -08:00
Frédéric Guillot e367f4ca21 Show scrollbars only when necessary for <pre> elements 2018-12-16 20:12:26 -08:00
Nicola Moretto 0b63e05fe2 Add Italian translation 2018-12-16 17:28:12 -08:00
Diego Agulló cf25efb4d9 Allow to switch between unread only and all entries on category/feed views 2018-12-16 16:58:48 -08:00
Frédéric Guillot 012138179c Add function storage.UpdateFeedError() 2018-12-15 13:04:38 -08:00
Tom Matthews 8b40778ee1 Add BBC News scraping rule 2018-12-13 20:25:30 -08:00
Frédéric Guillot 6ae935309a Ignore JSON feeds from EnsureUnicode() 2018-12-12 21:37:39 -08:00
Frédéric Guillot 43ccaf52af Preserve category selection when no feed is found 2018-12-12 21:31:50 -08:00
Frédéric Guillot 82e08d0f69 Update XML encoding regex to take single quotes into consideration 2018-12-12 21:13:06 -08:00
Bryan Roth 266d97ad63 Update en_US.json
Fixing translations.
2018-12-12 20:37:52 -08:00
Frederic Guillot 6869c6fe6f Send cli errors to stderr 2018-12-09 18:05:40 -08:00
Frederic Guillot 59d8e9b95e Update dependencies 2018-12-09 17:59:39 -08:00
Frederic Guillot 61bfb3cfa8 Make password prompt compatible with Windows 2018-12-09 17:44:33 -08:00
Jebbs 87648490fd Make configurable the number of days to archive read items 2018-12-05 20:35:30 -08:00
Frédéric Guillot 3e392dc3ae Change log level to debug when starting workers 2018-12-02 21:21:18 -08:00
Frédéric Guillot 35b890aadc Do not show $DATABASE_URL warning when showing application info 2018-12-02 21:19:09 -08:00
Frédéric Guillot 1bc8535dbb Move image proxy filter to template functions 2018-12-02 21:09:53 -08:00
Frédéric Guillot 6f5d93cbbe Update scraper rule for lemonde.fr 2018-12-02 20:53:22 -08:00
Frédéric Guillot 311a133ab8 Refactor manual entry scraper 2018-12-02 20:51:06 -08:00
djbshfbzehr 52de36b158 Apply rewriter rules on manual "Fetch Original Content" 2018-12-02 20:29:44 -08:00
Frédéric Guillot b51e7ea448 Add Makefile target for current OS and architecture 2018-12-02 13:35:13 -08:00
Frédéric Guillot 154b6a3bf7 Improve Makefile
- Use -mod=vendor
- Use same naming convention for all platforms
- Move go generate to its own target
2018-12-02 11:09:06 -08:00
Frédéric Guillot 04220ca1e7 Remove date and time from log messages
We assume that logs are sent to Syslog or similar.

Syslog or systemd already adds the timetamp.
2018-12-01 18:24:01 -08:00
Frédéric Guillot 8d9e6a2c62 Add Windows build target 2018-12-01 18:11:25 -08:00
mapl e47188eab2 Update scraper rule for heise.de 2018-12-01 11:49:30 -08:00
Frédéric Guillot 0be619355f Update ChangeLog 2018-11-25 19:49:05 -08:00
Frédéric Guillot af4c68feac Add support for Systemd Socket Activation 2018-11-25 17:41:23 -08:00
Frédéric Guillot 7838870a0c Change Unix socket permission to make it accessible from other services 2018-11-25 16:13:52 -08:00
Frédéric Guillot cb037910df Archive more read entries 2018-11-25 15:01:01 -08:00
Frédéric Guillot f3bff76aa1 Make sure slice is not out of range when reading XML prolog 2018-11-24 12:17:00 -08:00
ReVanTis 70be08eaf8 Improve overall Simplified Chinese translations.
- Unified few keywords.
- Unified all symbols to Chinese Symbols.
- Remove trailing period since it's not commonly used for Chinese on websites.
- Add one space between English words and Chinese words.
2018-11-21 21:07:22 -08:00
ariddell 3d04d92aa2 Improve time since post date displays
- 15 days now is "15 days" rather than "3 weeks" ago.
- 32 days is now "1 month" rather than "2 months" ago.
- 366 days is now "1 year" rather than "2 years" ago.

Closes #267
2018-11-17 12:09:02 -08:00
Frédéric Guillot 5cd7152ae7 Simplify application HTTP middlewares 2018-11-12 10:23:39 -08:00
Frédéric Guillot 9f85f67031 Make sure the remote address is populated even when using unix socket 2018-11-11 16:42:30 -08:00
Frédéric Guillot 1315282c7f Add the possiblity to listen on Unix socket 2018-11-11 16:21:57 -08:00
Frédéric Guillot becd086865 Add config options to disable HTTP and scheduler services 2018-11-11 15:54:19 -08:00
Frédéric Guillot 487852f07e Replace daemon and scheduler package with service package 2018-11-11 15:32:48 -08:00
Frédéric Guillot ca45765c46 Improve logging messages in ui package 2018-11-11 11:47:41 -08:00
Frédéric Guillot 5a69a61d48 Move UI middlewares and routes to ui package 2018-11-11 11:29:12 -08:00
Frédéric Guillot 0925899cee Move API middleware and routes to api package 2018-11-11 10:22:47 -08:00
Frédéric Guillot a9f98adb07 Move Fever middleware and routes to fever package 2018-11-11 09:54:32 -08:00
Piotr Kubisa 25c12053a6 Typo in README.md 2018-11-11 14:51:54 +01:00
Frédéric Guillot 36564ffbcd Add link to HTML man page 2018-11-10 11:18:40 -08:00
Frédéric Guillot 2f6e241417 Add man page 2018-11-09 22:01:58 -08:00
Frédéric Guillot 186f2b11c3 Change default database connection string 2018-11-09 21:58:09 -08:00
Frédéric Guillot 3b6e44c331 Allow the scraper to parse XHTML documents
Only "text/html" was authorized before.
2018-11-03 13:44:13 -07:00
Frédéric Guillot 1ff9950a55 Remove charset=utf-8 from JSON responses
See: https://www.iana.org/assignments/media-types/application/json
2018-11-03 12:03:06 -07:00
Diego Agulló a70e9d03ff Ignore hotkeys containing Control, Alt or Meta keys
Fixes #260
2018-10-31 13:59:02 +01:00
Frédéric Guillot ae1dc1a91e Handle more encoding conversion edge cases 2018-10-29 23:00:03 -07:00
Frédéric Guillot 5ff0630726 Disable go test caching 2018-10-29 22:46:08 -07:00
Frédéric Guillot 7d1b471d88 Add test case to check different feed encoding and HTTP headers 2018-10-29 19:04:36 -07:00
Frédéric Guillot 054fc8ef99 Avoid duplication of ldflags in Makefile 2018-10-28 22:22:48 -07:00
Frédéric Guillot 03d0114dcd Fix wrong translation key for category pages 2018-10-27 17:11:05 -07:00
Frédéric Guillot b17e01a288 Update ChangeLog 2018-10-26 20:07:56 -07:00
Frédéric Guillot f6028f3863 Improve Fever middleware and handle groupID=0 2018-10-26 19:49:49 -07:00
Ashe Connor 92c98bd986 Add OpenBSD build 2018-10-23 19:29:50 -07:00
Frédéric Guillot dc8668b904 Improve logging for OAuth2 callback 2018-10-22 20:49:10 -07:00
Frédéric Guillot f06a19814b Revert "Call preventDefault() when a keyboard shortcut is executed"
This reverts commit 9440bf47a5.
2018-10-22 20:26:17 -07:00
stratmaster 86dc2bf1c9 Unify german translation 2018-10-22 17:18:26 -07:00
Frédéric Guillot 9440bf47a5 Call preventDefault() when a keyboard shortcut is executed 2018-10-21 18:32:07 -07:00
Carl Helmertz 8c65c78904 NavHandler: Make "g f" go to feed, or list of feeds
When you're reading an entry, you may want to go to the feed first,
before quickly continuing to reading all of the feeds with another "g
f".
2018-10-21 11:53:17 -07:00
Frédéric Guillot 74c35ad000 Add more details in feed storage errors to facilitate debugging 2018-10-21 11:48:29 -07:00
Frédéric Guillot 85d48c8a71 Add entries storage error to feed errors count 2018-10-21 11:44:29 -07:00
Frédéric Guillot a614f98063 Set arbitrary maximum size for tsvector column
- The max size for tsvector is 1 MiB
- We index only the first million of characters,
  it should be enough for most feed entries.
2018-10-19 21:40:59 -07:00
Frédéric Guillot 715575001a Use different variable names for localized errors 2018-10-19 21:38:21 -07:00
Carl Helmertz 15a11c3da9 Unsubscribe from feed through link or "#"
After importing old OPML files, you may discover that many feeds are
obsolete or uninteresting. You list the feeds entries and determine that
you want to unsubscribe. This needs three clicks (edit feed, delete,
confirm) and requires moving the mouse to hit the different targets.

This quickly becomes tiring, if you are up to possibly deleting hundreds
of feeds. One mediation, introduced in this commit, is to add an
unsubscribe link to each feed's entry listing view, and also adding a
keyboard shortcut.

The keyboard shortcut "#" is:
* longer than one keystroke (requires shift)
* hard to type by accident
* used in Google products (thanks for the hint @fguillot)

In an effort to try to reduce the number of accidental feed
unsubscriptions.
2018-10-19 20:05:26 -07:00
Frédéric Guillot b8f874a37d Simplify feed entries filtering
- Rename processor package to filter
- Remove boilerplate code
2018-10-14 22:33:19 -07:00
Frédéric Guillot 234b3710d4 Fix golint import in travis.yml 2018-10-14 22:23:46 -07:00
Frédéric Guillot 778346b0b0 Simplify feed fetcher
- Add browser package to handle HTTP errors
- Reduce code duplication
2018-10-14 21:43:48 -07:00
Frédéric Guillot 5870f04260 Simplify feed parser and format detection
- Avoid doing multiple buffer copies
- Move parser and format detection logic to its own package
2018-10-14 11:46:41 -07:00
Frédéric Guillot d5ff4191b6 Improve unit tests in url package 2018-10-12 18:33:59 -07:00
Frédéric Guillot c1fe84521e Fix jshint error introduced in commit d4c1677 2018-10-08 21:09:25 -07:00
Frédéric Guillot 3daef197e9 Add short cli flags -i and -v 2018-10-08 21:00:00 -07:00
Frédéric Guillot 9606126196 Convert text links and line feeds to HTML in YouTube channels 2018-10-08 20:47:10 -07:00
Frédéric Guillot d4c1677e38 Change link state when marking all entries as read 2018-10-08 18:49:15 -07:00
Frédéric Guillot 9dc38a0803 Add missing package descriptions for GoDoc 2018-10-08 17:32:17 -07:00
Frédéric Guillot 11dfcdd3d6 Fix typo in license header 2018-10-08 15:50:15 -07:00
Frédéric Guillot 1f58b37a5e Refactor HTTP response builder 2018-10-08 15:31:58 -07:00
Frédéric Guillot ddfe969d6c Improve Fever API performances when marking a feed or group as read 2018-10-07 12:50:59 -07:00
Diego Agulló 98206059fc Set focus on article link when pressing prev/next hotkeys
This enhancement offer the possibility to open articles in the background by using the browser hotkey Ctrl+Return or Cmd+Return.
2018-10-01 20:52:51 -07:00
Frédéric Guillot 9d08139f43 Improve request package and add more unit tests 2018-09-23 21:02:26 -07:00
Frédéric Guillot 844680e573 Add more unit tests for config package 2018-09-23 15:23:46 -07:00
stratmaster 8601a67a7d Add another fix for translations 2018-09-23 12:50:38 -07:00
Frédéric Guillot b1e8f534ef Simplify locale package usage (refactoring) 2018-09-22 15:04:55 -07:00
Frédéric Guillot aae9b4eb83 Add missing translations and few fixes 2018-09-22 10:48:07 -07:00
Savely Krasovsky 0e23f37f57 Translate application in Russian 2018-09-22 10:22:58 -07:00
stratmaster 37ab6eec15 Update German translation 2018-09-22 09:58:23 -07:00
Frédéric Guillot 5d75089127 Use disclosure widget <details> for advanced feed options 2018-09-21 23:11:58 -07:00
Frédéric Guillot beb7a0cfcb Use unique translation IDs instead of English text as key 2018-09-21 22:23:23 -07:00
Frédéric Guillot f244df6293 Add more unit tests for template functions 2018-09-20 19:45:56 -07:00
Savely Krasovsky d79bab2997 Fix invalid output when truncating Unicode text in templates 2018-09-20 19:11:13 -07:00
Patrick 2538eea177 Add the possibility to override default user agent for each feed 2018-09-19 18:19:24 -07:00
Frédéric Guillot 1d335390c2 Split Makefile linux targets by architecture 2018-09-16 19:14:44 -07:00
Frédéric Guillot b9fee95cf2 Add compiler, Arch, and OS to info command 2018-09-16 19:13:16 -07:00
Diego Agulló 359aea8308 Avoid line break between emoji and (un)read/(un)star links 2018-09-15 11:28:44 -07:00
Frédéric Guillot e0bfd220f7 Update ChangeLog 2018-09-11 18:29:28 -07:00
Frédéric Guillot c9f9dd3262 Store client IP address in request context 2018-09-09 15:15:14 -07:00
Frédéric Guillot c1e1506720 Revert cookie flag from strict to lax mode otherwise oauth2 won't work 2018-09-09 14:25:56 -07:00
Frédéric Guillot 46932c91a6 Use predefined Ciphers when TLS is configured 2018-09-08 21:43:45 -07:00
Frédéric Guillot 2306a4b2f6 Avoid displaying an error when shutting down the daemon 2018-09-08 21:20:24 -07:00
Adam Vigneaux 07cd7d2135 Add "Mark this page as read" to the bottom
This better matches the most common use case for the "Mark this page as
read" link, which is to click it once you have reached the bottom of the
page. Especially on mobile, it can be annoying to scroll to the top of
the page to click the link.
2018-09-08 18:40:23 -07:00
Frédéric Guillot eee1f31903 Refactor HTTP context handling 2018-09-03 14:26:40 -07:00
Frédéric Guillot 88e81d4d80 Set cookie attribute SameSite to strict mode 2018-08-29 21:17:19 -07:00
Frédéric Guillot 6137b401ee Make user creation via environment variables idempotent 2018-08-29 21:00:23 -07:00
Frédéric Guillot 41ce0a3b80 Make latest changes compatible with Heroku 2018-08-29 20:50:36 -07:00
Frédéric Guillot 9ba9858bea Use regular text version of ✔︎ instead of emoji version on iOS
See this workaround: https://stackoverflow.com/questions/32639694/ios-9-removed-the-possibility-to-change-certain-symbol-colors-using-css/38362138#38362138
2018-08-28 20:46:15 -07:00
Dave Z 5341bbcbe2 Add toggle status button to entry page 2018-08-28 20:44:34 -07:00
Frédéric Guillot 4f62a704e2 Improve readme
- Add more links to the docs
- Add screenshots
2018-08-26 17:18:17 -07:00
Frédéric Guillot a3f3f51c60 Migrate to go modules (Go 1.11) 2018-08-26 16:43:53 -07:00
Michael a9e9c347f4 Switch Travis to Go 1.11 2018-08-26 16:36:33 -07:00
Dave Z 9169fbafb2 Show count of feeds with permanent errors in header menu
Only for feeds that reach `maxParsingError` are counted (so transient errors do not trigger counter).
2018-08-26 16:18:07 -07:00
dzaikos aae62aae08 Added remote client IP to API login failure error message.
Addresses #205

Changed error level reporting on API login failure to Error from Info to match the web login reporting.
2018-08-26 09:46:00 -07:00
stratmaster 62ec185154 Add german translation string 2018-08-26 09:43:57 -07:00
Frédéric Guillot 551b73acfb Add remove button in feed edit page 2018-08-25 12:21:54 -07:00
Frédéric Guillot febce4f2e3 Split integration tests into multiple files 2018-08-25 11:55:47 -07:00
Frédéric Guillot df2bebaf3d Update scraper rule for heise.de 2018-08-25 10:33:18 -07:00
Frédéric Guillot b17e4e88f6 Expose real error messages for internal server API errors 2018-08-25 10:04:58 -07:00
Frédéric Guillot c327833314 Simplify context keys 2018-08-25 09:50:43 -07:00
dzaikos 4148d8af80 Added remote client IP to login failure error message.
Fixes #205
2018-08-25 02:47:34 -04:00
Frédéric Guillot f43a055d63 Move Golang API client here 2018-08-24 22:23:03 -07:00
Frédéric Guillot dbcc5d8a97 Use canonical imports 2018-08-24 21:56:39 -07:00
tleb 7f2612d9a6 Add Procfile
Allows to git push to heroku/self-hosted alternatives easily
2018-08-07 12:54:38 -07:00
Frédéric Guillot cf03e0e338 Create database package (refactoring) 2018-08-01 20:28:45 -07:00
Frédéric Guillot 17054b396e Update user agent with new website URL 2018-07-28 10:32:48 -07:00
stratmaster 72bc113b88 Remove white spaces in German translation 2018-07-23 13:02:01 -07:00
Frédéric Guillot 33dcbae705 Update ChangeLog 2018-07-22 21:29:30 -07:00
Frédéric Guillot cfc1f3eb69 Avoid browser caching issues when assets changes 2018-07-22 12:24:46 -07:00
Frédéric Guillot 9f6533ece9 Compress JSON, CSS and Javascript responses 2018-07-19 19:27:05 -07:00
Frédéric Guillot a291d8a38b Improve themes handling
- Store user theme in session
- Logged out users will keep their theme
- Add theme background color to web manifest and meta tag
2018-07-18 22:30:05 -07:00
Frédéric Guillot c1ab27172c Update application icon with different sizes 2018-07-18 21:30:54 -07:00
Frédéric Guillot d10312fe15 Update URLs in readme 2018-07-18 17:28:57 -07:00
neepl 5365f31e90 Add support for published tag in Atom feeds 2018-07-17 21:52:05 -07:00
Frédéric Guillot 73a6e617bb Add few tweaks to the layout
- Increase truncate length to 35 chars for feed titles
- Add bottom margin for main menu in mobile view
- Add tooltip to feed domain in feeds list
2018-07-17 19:01:28 -07:00
Dave Z 3f31744911 Prevent vertical scrolling on swipe 2018-07-17 18:48:56 -07:00
Dave Z 3b018bc6bf Show feed title instead of domain in items list 2018-07-17 18:48:28 -07:00
Frédéric Guillot 6aa02680d8 Add service worker to cache feed icons 2018-07-15 21:51:09 -07:00
Dave Z c926498d3d Make image proxy configurable
Adds IMAGE_PROXY configuration setting to change image proxy filter behaviour:

- none = No proxy
- http-only = Proxy only non-HTTPS images (default)
- all = Proxy everything
2018-07-12 17:41:09 -07:00
Frédéric Guillot 6fd6f79daf Add alt attribute for feed icons 2018-07-10 23:20:32 -07:00
Frédéric Guillot aceea56681 Update CI jshint check 2018-07-10 20:59:39 -07:00
Frédéric Guillot a786e78aca Add embedly.com to iframe whitelist 2018-07-10 20:56:54 -07:00
Frédéric Guillot 3bdb9251da Use passive event listeners for touch events
Avoid this warning in Chrome console: https://www.chromestatus.com/feature/5745543795965952
2018-07-10 20:41:27 -07:00
dzaikos 6d25e02cb5 New add_dynamic_image rewriter for JavaScript-loaded images.
Searches tags for various `data-*` attributes and sets `img` tag `src` attribute appropriately. Falls back to searching `noscript` for `img` tags.

Includes unit tests.
2018-07-09 01:22:48 -04:00
Frédéric Guillot 8ee4280461 Change feed password field type to text to avoid autocomplete with Firefox
- Using autocomplete="off" or autocomplete="new-password" doesn't change anything
- Changing the input ID doesn't change anything
- Using a different input name doesn't change anything
- Only Chrome supports autocomplete="new-password"
2018-07-08 18:21:56 -07:00
Frédéric Guillot 55a1e97778 Add base URL validation 2018-07-07 14:01:02 -07:00
Frédéric Guillot cda0efb731 Update default stylesheet name in HTML layout 2018-07-07 12:07:34 -07:00
Frédéric Guillot 364198ba4a Add integration test for search query 2018-07-07 12:02:59 -07:00
Frédéric Guillot ca30800e6a Pre-generate themes stylesheets at build time 2018-07-07 11:00:39 -07:00
Frédéric Guillot 459bb4531f Update vendor dependencies 2018-07-06 21:18:14 -07:00
Frédéric Guillot 34a3fe426b Compress HTML responses to Gzip/Deflate if supported by browser 2018-07-06 20:39:28 -07:00
Frédéric Guillot 53deb0b8cd Refactor assets bundler and split Javascript files 2018-07-05 22:18:51 -07:00
dzaikos e1c56b2e53 Processor: Do rewriter before sanitizer for entry.Content.
Addresses #163.
2018-07-06 00:17:07 -04:00
Frédéric Guillot 845cf2adba Remove timestamp from generated files 2018-07-05 12:52:29 -07:00
stratmaster 7ed8401088 Add German translation for search form 2018-07-05 10:42:49 -07:00
Frédéric Guillot de1a4aad30 Add support for protocol relative YouTube URLs 2018-07-04 22:45:44 -07:00
Frédéric Guillot 6d0dc451e4 Add search form 2018-07-04 22:05:19 -07:00
Frédéric Guillot af15412954 Add full-text search for entries and add search parameter to the API 2018-07-04 17:40:03 -07:00
Daan Sprenkels 89e5dacca9 Improve updates to dutch locale 2018-07-03 12:57:47 -07:00
dzaikos 7d4a195519 Sandbox iframes when sanitizing.
Updated iframe unit tests.

Refactored sanitizer.getExtraAttributes() to use `switch` instead of multiple `if` statements.
2018-07-03 12:55:18 -07:00
dzaikos c3628813c6 Keep consistent text size on mobile orientation change
using vendor-specific CSS `*-text-size-adjust`.
2018-07-03 12:51:24 -07:00
Dave Z d3b48b7695 Update unit test for Sans-Serif theme 2018-07-02 21:37:07 -07:00
Frédéric Guillot 29131d4ee6 Update ChangeLog 2018-07-01 11:50:43 -07:00
Frédéric Guillot cc1e4f916f Avoid Chrome to autocomplete no-login password fields
Browsers always autocomplete saved passwords even with autocomplete="off".

https://developer.mozilla.org/en-US/docs/Web/Security/Securing_your_site/Turning_off_form_autocompletion
2018-06-30 18:05:51 -07:00
Frédéric Guillot 240f801755 Always mark the entry as read after fetching the pagination in unread mode 2018-06-30 17:35:06 -07:00
Unknown 4c7fce0233 Update German translation 2018-06-30 16:37:55 -07:00
Frédéric Guillot 5cf504745b Add cli flag to reset all feed errors 2018-06-30 14:22:45 -07:00
Frédéric Guillot 9c0f882ba0 Add specific 404 and 401 error messages 2018-06-30 12:42:12 -07:00
Frédéric Guillot a40f592aab Strip binaries to reduce size 2018-06-29 20:43:03 -07:00
Frédéric Guillot 77947282e4 Make sure we always get the pagination in unread mode 2018-06-29 20:29:04 -07:00
Dave Z 0b0f4751fa Fix incorrect data value when toggling bookmark flag on entry page 2018-06-28 18:56:04 -07:00
dzaikos 0227bb9828 Set opener to null when opening original URL with JavaScript. 2018-06-28 02:13:53 -04:00
Frédéric Guillot 9d8b5421ed Remove unnecessary style 2018-06-26 23:03:46 -07:00
Frédéric Guillot dd78fb4340 Do not ignore errored feeds when a user refresh feeds manually 2018-06-26 23:02:41 -07:00
dzaikos 45d7105ed1 Refactor AddImageTitle rewriter.
* Only processes images with `src` **and** `title` attributes (others are ignored).
* Processes **all** images in the document (not just the first one).
* Wraps the image and its title attribute in a `figure` tag with the title attribute's contents in a `figcaption` tag.

Updated xkcd rewriter unit test.

Added another xkcd rewriter unit test to check rendering of images without title tags.
2018-06-26 17:50:18 -04:00
dzaikos c9131b0e89 Improve sanitizer to remove style tag contents.
See #157.

Refactored how blacklisted tags are handled so they're easier manage in the future.
2018-06-24 19:53:23 -07:00
Dave Z d847b10e32 Improve sanitizer to remove script and noscript contents
These tags where removed but the content was rendered as escaped HTML.

See #157
2018-06-23 17:50:43 -07:00
Frédéric Guillot 7039df9af1 Improve feed and user API updates with optional values 2018-06-23 16:16:54 -07:00
stratmaster cd77ebd742 Corrected hard coded strings 2018-06-22 18:41:19 -07:00
stratmaster eb9d23c796 Update German translation 2018-06-20 14:27:36 -07:00
Frédéric Guillot bddca15b69 Add new fields for feed username/password 2018-06-19 22:58:29 -07:00
Frédéric Guillot 261695c14c Improve memory usage debug log 2018-06-19 20:55:29 -07:00
Frédéric Guillot b5c317f971 Disable keep-alive for HTTP client 2018-06-19 20:21:24 -07:00
Frédéric Guillot 7914261437 Close HTTP response body even for failed requests 2018-06-19 20:13:13 -07:00
Dave Z be95e87e0e Added Sans-Serif theme. 2018-06-19 00:57:30 -04:00
Frédéric Guillot c719cf7df0 Rewrite iframe Youtube URLs to https://www.youtube-nocookie.com 2018-06-12 18:45:09 -07:00
Frédéric Guillot 36dab8b518 Add more filters for API call /entries
New filters:

- before (unix timestamp)
- before_entry_id
- after
- after_entry_id
- starred (boolean)
2018-06-09 19:13:41 -07:00
Frédéric Guillot c5373ff2bf Rewrite individual entry pagination SQL queries 2018-06-09 13:40:20 -07:00
Frédéric Guillot bd70640794 Simplify entry query builder 2018-06-08 18:24:41 -07:00
Dave Z 2bbe21fb89 Prevent items from sticking on touchend. 2018-06-06 20:23:10 -07:00
Dave Z 084642eb0a Extended horizontal overflow to feed and category views. 2018-06-06 20:18:33 -07:00
stratmaster 11d880087d Update German translation 2018-06-05 18:36:03 -07:00
Frédéric Guillot 77cafe9c2c Update ChangeLog 2018-06-04 18:48:27 -07:00
Frédéric Guillot 3b39f0883c Rewrite RealIP() to avoid returning an empty string 2018-06-01 07:22:31 -07:00
Frédéric Guillot cf7a7e25fb Convert IP field from text to inet type 2018-05-21 19:58:36 -04:00
Frédéric Guillot fb49ad24d5 Improve error handling in integration clients 2018-05-21 19:58:36 -04:00
Frédéric Guillot b270159aae Offer the possibility to set Pocket Consumer Key as environment variable 2018-05-21 19:58:36 -04:00
Pascal Widdershoven 44decae557 Fix unread count in menu not triggering click
By moving unread count into anchor.

Previously, clicking the unread count would not cause the anchor to be
invoked. This was confusing. This change includes the unread count span
in the anchor.
2018-05-21 12:51:43 +02:00
Frédéric Guillot 7f2fd1fdd8 Add Pocket authorization flow in the user interface 2018-05-20 15:29:14 -07:00
Allan Reyes 0f3f5e442f Add Pocket integration 2018-05-20 13:31:56 -07:00
Frédéric Guillot f19ab21b7d Archive read entries automatically after 60 days 2018-05-19 16:45:27 -07:00
Frédéric Guillot ff8e0c6b3d Update readme and rebuild CSS 2018-05-19 15:47:46 -07:00
Allan Reyes b0667e1b78 Hide horizontal overflow on hiding articles; fixes #130 2018-05-19 11:48:54 -07:00
Frédéric Guillot 9554d0bd53 Add integration test for /me endpoint 2018-05-14 18:52:12 -07:00
Leviathan Jeanis e81e4f0ac3 Add API endpoint to get logged user 2018-05-14 18:41:41 -07:00
Abhinand 4674d559a4 Fever API: Return response with an empty list if there is no items 2018-05-08 22:08:01 -07:00
Frédéric Guillot 0c2e5ff0dc Handle feeds with dates formatted as Unix timestamp 2018-05-08 20:41:24 -07:00
Frédéric Guillot 39c4452142 Update ChangeLog 2018-05-07 20:14:54 -07:00
Frédéric Guillot 2f4cd59ad9 Make sure to close request body in HTTP client 2018-04-29 23:11:10 -07:00
Frédéric Guillot 5cacae6cf2 Add API endpoint to import OPML file 2018-04-29 18:56:40 -07:00
Frédéric Guillot 7a1653a2e9 Make sure integrations are configured before to make any HTTP requests 2018-04-29 17:58:09 -07:00
Frédéric Guillot 31da4db14f Do not show save link if no integration is configured 2018-04-29 17:43:40 -07:00
Frédéric Guillot b166ceaea7 Avoid people to unlink their OAuth2 account without having a local password 2018-04-29 17:04:43 -07:00
Frédéric Guillot f49b42f70f Use vanilla HTTP handlers (refactoring) 2018-04-29 16:35:04 -07:00
Frédéric Guillot 1eba1730d1 Move HTTP client to its own package 2018-04-28 10:51:07 -07:00
Frédéric Guillot 04adf5fdf5 Add middleware to read X-Forwarded-Proto header 2018-04-27 22:25:00 -07:00
Frédéric Guillot ddd3af4b85 Do not use shared variable to translate templates 2018-04-27 22:07:46 -07:00
Frédéric Guillot 6b360d08c1 Use Gorilla middleware (refactoring) 2018-04-27 20:38:46 -07:00
aniran 322b265d7a Scrape parent element for iframe
Current behavior: if you have an `iframe` scraper rule, `scrapContent`
tries to return the inner HTML of the `iframe`, which turns up blank.

New behavior: like `img` elements, if an `iframe` is matched by a scraper rule,
the parent element's inner HTML (i.e. the `iframe` is returned).
2018-04-27 17:57:22 -07:00
aniran 920dda79b7 Add soundcloud and bandcamp iframe sources 2018-04-27 17:55:58 -07:00
Frédéric Guillot 1ce522b98a Update ChangeLog 2018-04-20 21:56:38 -07:00
Frédéric Guillot 657e96e133 Improve graceful shutdown 2018-04-17 21:50:52 -07:00
Frédéric Guillot 0429bbb19d Simplify Heroku deployment 2018-04-15 21:07:59 -07:00
Frédéric Guillot 45dde0cf4a Display memory usage and some metrics in logs 2018-04-14 14:23:05 -07:00
Frédéric Guillot 4cdb2f820b Increase read/write timeout for HTTP server 2018-04-14 13:52:53 -07:00
Frédéric Guillot dcbb5047b1 Add support for Dublin Core date in RDF feeds 2018-04-10 18:13:05 -07:00
Frédéric Guillot 15202b8675 Do not return an error if the user session is not found 2018-04-09 21:52:24 -07:00
Frédéric Guillot 02ba735ba9 Handle some non-english date formats 2018-04-09 21:27:15 -07:00
Frédéric Guillot 20f874399d Add missing French translation 2018-04-09 20:39:56 -07:00
Frédéric Guillot e2d02bac5a Rename RSS parser getters 2018-04-09 20:38:12 -07:00
Frédéric Guillot f76093690c Get the right comments URL when having multiple namespaces 2018-04-09 20:30:55 -07:00
Frédéric Guillot 7640a8cbab Ignore caching headers for feeds that send "Expires: 0" 2018-04-09 20:18:54 -07:00
stratmaster 3d59cdba10 Add missing translation string 2018-04-09 18:24:24 -07:00
stratmaster d3855fef3f Update German translation 2018-04-09 10:33:40 -07:00
2926 changed files with 54188 additions and 1257314 deletions
+19
View File
@@ -0,0 +1,19 @@
version: 2
updates:
- package-ecosystem: "gomod"
directory: "/"
schedule:
interval: "weekly"
reviewers:
- "fguillot"
assignees:
- "fguillot"
- package-ecosystem: "docker"
directory: "/"
schedule:
interval: "weekly"
reviewers:
- "fguillot"
assignees:
- "fguillot"
+95
View File
@@ -0,0 +1,95 @@
name: CI Workflow
on:
pull_request:
branches:
- master
jobs:
linters:
name: Linter Check
runs-on: ubuntu-latest
steps:
- name: Set up Go
uses: actions/setup-go@v1
with:
go-version: 1.15
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Checkout
uses: actions/checkout@v1
with:
fetch-depth: 3
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Install linters
run: |
cd /tmp && go get -u golang.org/x/lint/golint
sudo npm install -g jshint
env:
GO111MODULE: off
- name: Run golint
run: |
export PATH=/home/runner/go/bin:$PATH
make lint
- name: Run jshint
run: jshint ui/static/js/*.js
unit-tests:
name: Unit Tests
runs-on: ${{ matrix.os }}
strategy:
max-parallel: 4
matrix:
os: [ubuntu-latest, windows-latest, macOS-latest]
go-version: [1.14, 1.15]
steps:
- name: Set up Go
uses: actions/setup-go@v1
with:
go-version: ${{ matrix.go-version }}
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Checkout
uses: actions/checkout@v1
with:
fetch-depth: 3
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Run unit tests
run: make test
integration-tests:
name: Integration Tests
runs-on: ubuntu-latest
services:
postgres:
image: postgres:9.5
env:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
POSTGRES_DB: postgres
ports:
- 5432:5432
options: --health-cmd pg_isready --health-interval 10s --health-timeout 5s --health-retries 5
steps:
- name: Set up Go
uses: actions/setup-go@v1
with:
go-version: 1.15
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Checkout
uses: actions/checkout@v1
with:
fetch-depth: 3
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Install Postgres client
run: sudo apt-get install -y postgresql-client
- name: Run integration tests
run: make integration-test
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
PGHOST: 127.0.0.1
PGPASSWORD: postgres
+57
View File
@@ -0,0 +1,57 @@
name: Docker
on:
schedule:
- cron: '0 1 * * *'
push:
tags:
- '*.*.*'
jobs:
docker-images:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v2
with:
fetch-depth: 0
- name: Generate Docker tag
id: docker_tag
run: |
DOCKER_IMAGE=miniflux/miniflux
DOCKER_VERSION=dev
if [ "${{ github.event_name }}" = "schedule" ]; then
DOCKER_VERSION=nightly
TAGS="${DOCKER_IMAGE}:${DOCKER_VERSION},ghcr.io/${DOCKER_IMAGE}:${DOCKER_VERSION}"
elif [[ $GITHUB_REF == refs/tags/* ]]; then
DOCKER_VERSION=${GITHUB_REF#refs/tags/}
TAGS="${DOCKER_IMAGE}:${DOCKER_VERSION},ghcr.io/${DOCKER_IMAGE}:${DOCKER_VERSION},${DOCKER_IMAGE}:latest,ghcr.io/${DOCKER_IMAGE}:latest"
fi
echo ::set-output name=tags::${TAGS}
- name: Set up QEMU
uses: docker/setup-qemu-action@v1
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v1
- name: Login to DockerHub
uses: docker/login-action@v1
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Login to GitHub Container Registry
uses: docker/login-action@v1
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ secrets.CR_PAT }}
- name: Build and push
uses: docker/build-push-action@v2
with:
context: .
file: ./packaging/docker/Dockerfile
platforms: linux/amd64,linux/arm/v6,linux/arm/v7,linux/arm64
push: true
tags: ${{ steps.docker_tag.outputs.tags }}
+3
View File
@@ -1 +1,4 @@
miniflux-*
miniflux
*.rpm
*.deb
-17
View File
@@ -1,17 +0,0 @@
notifications:
email: false
services:
- postgresql
addons:
postgresql: "9.4"
language: go
go:
- "1.10"
before_install:
- npm install -g jshint
- go get -u github.com/golang/lint/golint
script:
- jshint ui/static/js/app.js
- make lint
- make test
- make integration-test
+623
View File
@@ -1,3 +1,626 @@
Version 2.0.28 (Feb 15, 2021)
-----------------------------
* Add HTTP header "Referrer-Policy: no-referrer"
* Handle entry title with double encoded entities
* Add Open Containers annotations to Docker image
* Remove iframe inner HTML contents (iframe element never has fallback content)
* Update date parser to fix another time zone issue
* Update German translation for blocklist and keeplist
* Validate Keep list and Block list rules syntax
* Add support for IPv6 with zone index
* Allow images with data URLs
* Limit full-text search indexation to first 500K characters (tsvector has a size limit of 1MB)
* Change PWA display mode to standalone
* ETag value is not set correctly in HTTP client (regression)
* Add database backed Let's Encrypt certificate cache
* Add global option POLLING_PARSING_ERROR_LIMIT
* Update systemd service file comments to use `systemctl edit` for editing
* Update Go version to 1.15 in go.mod
* Don't discard the "Fetch via Proxy" option
* Update man page to show the default values
* Add PostgreSQL indices
* Add API endpoints to get feeds and entries of a category
* Create feed query builder
* Bump github.com/PuerkitoBio/goquery from 1.6.0 to 1.6.1
* Show global options in the about page
* Update man page to mention -1 can be used for CLEANUP_ARCHIVE_* options
Version 2.0.27 (Jan 9, 2021)
----------------------------
* Add spellcheck="false" to input fields
* Refactoring of entry, feed, category, and user validation
* Avoid stripping tags for entry title
* Add the possibility to subscribe to feeds with the Android Share menu
* API improvements:
- Change feed creation request to allow setting most fields via API
- Allow regular users to change settings via API
- Make user fields editable via API
- Renaming non-existent category via API should return a 404
* Update Systemd service file:
- Add capability CAP_NET_BIND_SERVICE (allow the process to listen on privileged ports)
- Enable a private /tmp for $CERT_CACHE (required when using Let's Encrypt)
* Update read/star icons to SVGs
* Add autocomplete="username" to HTML forms
* Improve user mass delete to use fewer Goroutines
* Use SQL transaction when creating user sessions and users
* Remove extra column (HSTORE field) from users table and migrate key/value pairs to specific columns
* Bump github.com/prometheus/client_golang from 1.8.0 to 1.9.0
* Bump github.com/lib/pq from 1.8.0 to 1.9.0
* Add styles for <abbr> HTML tag
* Refactor SQL migrations:
- Avoid embedding SQL files into binary
- Allow more flexible changes by using Go functions
* Add Server-Timing header to unread page
* Show correct User Agent in input placeholders
* Add autocomplete attribute to login form
* Add Grafana dashboard in contrib folder
Version 2.0.26 (Dec 5, 2020)
----------------------------
* Use created_at instead of published_at for archiving entries
* Add created_at field for entries
* Handle invalid feeds with relative URLs
* Add API routes for "mark all as read"
* Add support for setting a global default User-Agent
* Add rewrite rule "replace" for custom search and replace
* Calculate reading time during feed processing
* Handle various invalid dates
* systemd: keep /run writeable
* debian package: add missing post-install script
* Do not follow redirects when trying known feed URLs
* Trim spaces around icon URLs
* Reinstate EXPOSE instruction in Dockerfile
* Update German and Portuguese translations
Version 2.0.25 (Nov 3, 2020)
----------------------------
* Rename "original" link to be more explicit
* Do not escape HTML for Atom 1.0 text content during parsing (Avoid HTML entities issues)
* Do not use charset.NewReader if the body is a valid UTF-8 document
* Restore the ability to use a proxy for all HTTP requests (see https://golang.org/pkg/net/http/#ProxyFromEnvironment)
* Show Git commit in about page
* Publish Docker images to GitHub Container Registry
* Added few Docker Compose examples in contrib folder
* Added Ansible Role + Playbook for Miniflux in contrib folder
* Add rewrite rule to use noscript content for images rendered with Javascript
* Bump github.com/prometheus/client_golang from 1.7.1 to 1.8.0
* Update contributor link and Godoc badge for API client
* Move Debian package builder to main repository
* Move RPM build files to main repository
* Add GitHub Action to generate Docker images
* Build multi-platform images with Docker Buildx
* Add keyboard shortcut to scroll current item to the top
* Add feed filters (Keeplist and Blocklist)
* Do not proxy image with a data URL
* Bump github.com/PuerkitoBio/goquery from 1.5.1 to 1.6.0
* Proxify articles crawled manually
* Proxify images defined in srcset attribute
* Remove plaintext Fever password from database
* Add keyboard shortcut to jump to an item's feed page
* Add option for swipe gesture on entries on mobile
Version 2.0.24 (Oct 3, 2020)
----------------------------
* Add rewrite rule to fix Medium.com images
* Update sanitizer to support responsive images:
- Add support for picture HTML tag
- Add support for srcset, media, and sizes attributes to img and source tags
* Enhance man page formatting
* Add Prometheus exporter
* Remove dependency on global config options in HTTP client
* API:
- Avoid database lookup if empty credentials are provided
- Add the possibility to filter entries by category ID
- Add the possibility to filter entries by a list of statuses
* Add Feed ID in worker error logs
* Tweak default HTTP client transport timeout values to reduce the number of file descriptors
* CSS tweaks and optimizations:
- Prevent sub and sup from affecting line-height
- Set touch-action on .items to prevent browser navigation
- Move font-family specific CSS to the appropriate file
- Update primary font-family for UI to be even more compatible with various operating systems
- Make .entry-content font-weight variable depending on font-family used
* Avoid Javascript minifier to break keyboard shortcuts
* Rename service worker script to avoid being blocked by uBlock
* Update date parser to handle Pacific Daylight Time in addition to Pacific Standard Time
* Create index to speed up bookmark page
* Do not try to update a duplicated feed after a refresh
* Use a transaction to refresh and create entries
* Speed up entries clean up with an index and a goroutine
* Avoid the accumulation of enclosures by keeping only what is referenced in the feed
* Add workarounds for parsing an invalid date
* Archive older entries first
* Update API client to support more filters
* Avoid duplication between get feed entries and get entries API endpoints
* Enable strict slash to avoid a page not found (404) when using a trailing slash in the URLs
* Add a submit button to each section of the integration page
* Reload page after making page as read when showing unread entries
* Add option to archive unread entries
* Add option to enable maintenance mode
* Add HTTP proxy option for subscriptions
* Make add_invidious_video rule applicable for different invidious instances
* Fix reading time for jp, ko and zh languages
* Update POLLING_SCHEDULER description in man page
* Bump gorilla/mux from 1.7.4 to 1.8.0
* Add link to mark a feed as read
Version 2.0.23 (Aug 15, 2020)
-----------------------------
* Try known URLs when discovering subscriptions
* Add workarounds to find YouTube channel feeds (YouTube doesn't expose RSS links anymore for new-style URLs)
* Increase HTTP server timeout values
* Use stdlib constants for HTTP methods instead of strings
* Add support for RTL feed content
* Ignore <media:title> to avoid overriding the default title if they are different
* Add support for secret keys exposed as a file (useful for containerized environments)
* Display recent entries first in search results
* Do not archive shared items
* Add option to change the number of entries per page
* Add Brazilian Portuguese (pt_BR) translation
* Add reading time for entries
* Redirect to login page if CSRF token is expired
* Fever API:
- Use getEntryIDs instead of getEntries to reduce memory consumption
- Fix max_id argument logic to follow the specs
- Improve logging
- Do not send articles to external services when unsaving an item
- Create index to speed up API calls
- Log client IP in middleware
* API client: Do not return body for response with no content
* REST API:
- Delete users asynchronously (Deleting large users might lock the tables)
- Add CORS support
* Align entry actions to the left
- Attempt to avoid awkward alignment on smartphone screens
- Keep the read/star actions aligned to the left
- Remove CSS flex to allow easier override with custom CSS
* Upgrade Postgres client library
* Upgrade CI checks to Go 1.15
Version 2.0.22 (Jun 19, 2020)
-----------------------------
* Remove child-src CSP policy (deprecated)
* Add /version endpoint
* Add the ability to use custom css
* Handle more invalid dates
* Add CSS styles for textarea
* Add index to speed up slow query
* Speed up feed list page rendering
* Add alternative scheduler based on the number of entries
* Setup Dependabot on GitHub
* Update Docker image to Alpine 3.12
* Add feed option to ignore HTTP cache
* Fix some Italian and German translations
* Added scraper rule for RayWenderlich.com, TheOatmeal.com, financialsamurai.com, dilbert.com and turnoff.us
* Replace link to categories by a link to the list of entries in "Category > Feeds" page
* Change feed title to a link to the original website
* Add icons to feeds and categories list
* Update dependencies and remove vendor folder
Version 2.0.21 (Mar 28, 2020)
-----------------------------
* Add SVG icons to entry actions
* Add support for Invidious
- Embed Invidious player for invidio.us feeds
- Add new rewrite rule to use Invidious player for Youtube feeds
* Check during application startup if the database schema is up to date
* Change default theme for public pages to "System Serif"
* Add feature to share an article (create a public link of a feed entry)
* Fix SQL injection in full-text search rank ordering
* Add generic OpenID Connect provider (OAuth2)
* Use more secure TLS configuration for autocert server (increase SSL Labs score from B to A+)
* Add feature to create per-application API Keys
* Add Go 1.14 to GitHub Actions
* Add scraper rule for wdwnt.com
* Add API client function to refresh all feeds
* Add API endpoint to refresh all feeds
* Add Auth Proxy authentication
* Use rel=prev/next on pagination links
Version 2.0.20 (Feb 15, 2020)
-----------------------------
* Add Japanese translation
* History: show entries in the order in which they were read
* Add button to add to Home screen
* Ignore enclosures without URL
* Correct spelling of "toggle"
* List view: align information to the left side, and the actionable buttons to the right
* Redirect to /unread when getting a 404 for an unread expired entry
* Do not advance to the next item when using the 'v' shortcut on the list of starred items
* Wrap around when navigating with keyboard shortcuts on a list view
* Remove unused Feed.Entries and Entry.Category from API client
* Add comments link keyboard shortcut
* Allow application/xhtml+xml links as comments URL in Atom replies
* Allow only absolute URLs in comments URL
* Use internal XML workarounds to detect feed format
* Make menu consistent across feed pages
* Make sure external URLs are not encoded incorrectly by Go template engine
* Make sure whitelisted URI schemes are handled properly by the sanitizer
* Use white background for favicon (Improve legibility when using a dark theme)
* Remove dependency on Sed to build Docker images
* Normalize URL query string before executing HTTP requests
* Improve Dublin Core support for RDF feeds
* Improve Podcast support (iTunes and Google Play feeds)
* Add support for Atom 0.3
* Add support for Atom "replies" link relation
* Return outer HTML when scraping elements
* Update scraper rule for "Le Monde"
* Filter valid XML characters for UTF-8 XML documents before decoding
* Trim spaces for RDF entry links
Version 2.0.19 (Dec 1, 2019)
----------------------------
* Add shortcut "V" to open original link in current tab
* Add the possibility to add rules during feed creation
* Wrap attachments into <details> disclosure element
* Show attachment size on entry page
* Add support of RSS Media elements (group, description, peer link, and thumbnails)
* Add rewrite functions: convert_text_link and nl2br
* Add scraper rule for openingsource.org
* Add Makefile target to build only amd64 Docker image
* Make sure to remove integration settings when removing a user
* Add API parameter to filter entries by category
* Display list of feeds per category
* Show the number of read and unread entries for each feed
* Make sure settings menu is consistent
* Remove fixed table-layout for entry content
* Update autocert lib because ACME v1 is EOL
* Do not lighten blockquote font color
* Update de_DE translation
* Send a response when changing status of removed entries in Fever API
* Add meta tag to disable Google Translate
* Improve storage module
* Improve XML decoder to remove illegal characters
* Compare Fever token case-insensitively
* Make sure integration tests are marked as failed in Github Actions
* Add new formats to date parser
* Add notification message when using keyboard shortcuts: f, s, and m.
* Avoid keyboard shortcuts to conflict with Firefoxs "Find as you type" feature
Version 2.0.18 (Sep 25, 2019)
-----------------------------
* Add Docker image variant for arm32v7
* Add theme variants
- Use CSS variables instead of inherence
- Rename default theme to "Light - Serif"
- Rename Black theme to "Dark - Serif"
- Rename "Sans-Serif" theme to "Light - Sans Serif"
- Add "System" theme that use system preferences: Dark or Light
- Add Serif and Sans-Serif variants for each color theme
* Avoid constraint error when having duplicate entries during feed creation
* Disable strict XML parsing
* Ignore invalid content type
* Update man page
* Replace Travis by GitHub Actions
* Rename cleanup config variables and deprecate old ones
- CLEANUP_FREQUENCY_HOURS instead of CLEANUP_FREQUENCY
- CLEANUP_ARCHIVE_READ_DAYS instead of ARCHIVE_READ_DAYS
* Make configurable the number of days to remove old sessions
* Add native lazy loading for images and iframes
* Do not buffer responses in the image proxy
* Update dependencies
* Add Go 1.13 to test matrix
* Replace link border by outline to avoid slight content shift
* New rewrite function: add_mailto_subject
* Import OPML from URL
* Fix HTML injection in addImageTitle
* Accept HTML entities when parsing XML
Version 2.0.17 (Aug 3, 2019)
----------------------------
* Update Docker image to Alpine Linux 3.10.1
* Pass auth header to manifest request (crossorigin attribute)
* Sort feed categories before serialization
* Fix syntax errors in man page
* Add .search margin-right
* Ask for confirmation before flushing history, marking page as read, and mark all as read
* Add option to disable feeds
Version 2.0.16 (Jun 8, 2019)
----------------------------
* Add option to toggle date/time in log messages
* Add optional config file parser in addition to environment variables
* Make HTTP Client timeout and max body size configurable
* Refactor config package:
- Parse configuration only once during startup time
- Store configuration values in a global variable
* Flip behavior of j and k keyboard shortcuts
* Bump Postgresql client library to v1.1.1 to bring in SCRAM-SHA-256 authentication
* Add option to enable/disable keyboard shortcuts
* Add missing translation
* Improve page reload when showing unread/all entries:
- Show only unread entries = refresh current page
- Show all entries = go to next page
* Always display feed entries even when there is a feed error
* Use loading label instead of saving when submitting login form
* Add OPML v1 support during importation
* Add 'allow-popups' to iframe sandbox permissions
Version 2.0.15 (Mar 16, 2019)
-----------------------------
* Move Dockerfile to main repo
* Change location of the binary from /usr/local/bin to /usr/bin in Docker image
* Add double tap detection for next/previous page navigation
* Allow users to disable auto-remove
* Make parser compatible with Go 1.12
* Add Golang 1.12 to CI
* Use feed ID instead of user ID to check entry URLs presence
* Fix typo in stylesheet
* Sort search results by relevance
* Use preferably the published date for Atom feeds
* Add Spanish translation
* Rename session cookies
* Handle the case when application session is expired and not user session
Version 2.0.14 (Jan 13, 2019)
-----------------------------
* Only attempt to change password if the confirmation field is filled in (Firefox)
* Remove URL from client user agent
* Make the feed list order case-insensitive
* Handle XHTML Summary elements for Atom feeds
* Make UTF-8 the default encoding for XML feeds
* Add more targets to Makefile
* Add -mod=vendor in Makefile
* Move health check endpoint from ui package to httpd service
* Add workaround for non GMT dates (RFC822, RFC850, and RFC1123)
* Make sure `<strong>` elements are bold
* Show scrollbars only when necessary for <pre> elements
* Add Italian translation
* Allow to switch between unread only and all entries on category/feed views
* Add function storage.UpdateFeedError()
* Add BBC News scraping rule
* Ignore JSON feeds from EnsureUnicode()
* Preserve category selection when no feed is found
* Update XML encoding regex to take single quotes into consideration
* Send cli errors to stderr
* Update dependencies
* Make password prompt compatible with Windows
* Make configurable the number of days to archive read items
* Change log level to debug when starting workers
* Do not show $DATABASE_URL warning when showing application info
* Move image proxy filter to template functions
* Update scraper rule for lemonde.fr
* Refactor manual entry scraper
* Apply rewriter rules on manual "Fetch Original Content"
* Add Makefile target for current OS and architecture
* Improve Makefile
Version 2.0.13 (Nov 25, 2018)
-----------------------------
* Add man page
* Add support for Systemd Socket Activation (experimental)
* Add the possiblity to listen on Unix sockets
* Add config options to disable HTTP and scheduler services
* Archive more read entries in cleanup job
* Change default database connection string (backward compatible)
* Improve logging messages in ui package
* Improve overall Simplified Chinese translations
* Improve time since post date displays:
- "15 days" now is "15 days" rather than "3 weeks" ago
- "32 days" is now "1 month" rather than "2 months" ago
- "366 days" is now "1 year" rather than "2 years" ago
* Allow the scraper to parse XHTML documents
* Remove charset=utf-8 from JSON responses
* Ignore hotkeys containing Control, Alt or Meta keys
* Handle more encoding conversion edge cases
* Disable go test caching
* Avoid duplication of ldflags in Makefile
* Fix wrong translation key for category pages
* Code refactoring:
- Simplify application HTTP middlewares
- Replace daemon and scheduler package with service package
- Move UI middlewares and routes to ui package
- Move API middleware and routes to api package
- Move Fever middleware and routes to fever package
Version 2.0.12 (Oct 26, 2018)
-----------------------------
* Add OpenBSD build
* Improve logging for OAuth2 callback
* Make "g f" go to feed, or list of feeds
* Add more details in feed storage errors to facilitate debugging
* Add entries storage error to feed errors count
* Set arbitrary maximum size for tsvector column
* Unsubscribe from feed through link or "#"
* Simplify feed entries filtering
* Simplify feed fetcher
* Simplify feed parser and format detection
* Improve unit tests in url package
* Add short cli flags -i and -v
* Convert text links and line feeds to HTML in YouTube channels
* Change link state when marking all entries as read
* Add missing package descriptions for GoDoc
* Fix typo in license header
* Refactor HTTP response builder
* Improve Fever API performances when marking a feed or group as read
* Set focus on article link when pressing prev/next hotkeys
* Improve request package and add more unit tests
* Add more unit tests for config package
* Simplify locale package usage (refactoring)
* Translate application in Russian
* Use disclosure widget <details> for advanced feed options
* Use unique translation IDs instead of English text as key
* Add more unit tests for template functions
* Fix invalid output when truncating Unicode text in templates
* Add the possibility to override default user agent for each feed
* Split Makefile linux targets by architecture
* Add compiler, Arch, and OS to info command
* Avoid line break between emoji and (un)read/(un)star links
* Build Docker image for multiple architectures (amd64, arm32v6, arm64v8)
Version 2.0.11 (Sep 11, 2018)
-----------------------------
* Set cookie flag `SameSite` to Lax mode
* Use predefined ciphers when TLS is configured
* Avoid displaying an error when shutting down the daemon
* Add "Mark this page as read" to the bottom
* Store client IP address in request context
* Refactor HTTP context handling
* Make user creation via environment variables idempotent
* Use regular text version of ✔︎ instead of emoji version on iOS
* Add toggle status button to entry page
* Migrate to Go Modules and Go 1.11
* Show count of feeds with permanent errors in header menu
* Display remote client IP in logs when having a login failure (Fail2Ban)
* Add remove button in feed edit page
* Split integration tests into multiple files
* Update scraper rule for heise.de
* Expose real error messages for internal server API errors
* Move Golang API client in project source tree (the separate project is deprecated)
* Use canonical imports
* Add Procfile
* Create database package (refactoring)
* Update user agent with new website URL
* Update German translation
Version 2.0.10 (July 22, 2018)
------------------------------
* Avoid browser caching issues when assets changes
* Add Gzip/Deflate compression for HTML, JSON, CSS and Javascript responses
* Improve themes handling
* Store user theme in session
* Logged out users will keep their theme
* Add theme background color to web manifest and meta tag
* Update application icon with different sizes
* Add support for published tag in Atom feeds
* Add tooltip to feed domain in feeds list (title attribute)
* Prevent vertical scrolling on swipe
* Show feed title instead of domain in items list
* Add service worker to cache feed icons
* Make image proxy configurable via IMAGE_PROXY environment variable:
* none = No proxy
* http-only = Proxy only non-HTTPS images (default)
* all = Proxy everything
* Add alt attribute for feed icons
* Update CI jshint check
* Add embedly.com to iframe whitelist
* Use passive event listeners for touch events
* Add `add_dynamic_image` rewriter for JavaScript-loaded images
* Change feed password field type to text to avoid auto-completion with Firefox
* Using autocomplete="off" or autocomplete="new-password" doesn't change anything
* Changing the input ID doesn't change anything
* Using a different input name doesn't change anything
* Only Chrome supports autocomplete="new-password"
* Add base URL validation
* Update default stylesheet name in HTML layout
* Pre-generate themes stylesheets at build time
* Update vendor dependencies
* Refactor assets bundler and split Javascript files
* Run sanitizer after all processing and entry content rewrite
* Remove timestamp from generated files
* Add support for protocol relative YouTube URLs
* Add Postgres full-text search for entries
* Add search form in user interface
* Add search parameter to the API
* Improve Dutch locales
* Sandbox iframes when sanitizing
* Keep consistent text size on mobile orientation change
* Change permission of /etc/miniflux.conf to 600 instead of 644 in RPM package
* Add tzdata package to Docker image
* Update Docker image to Alpine Linux 3.8
Version 2.0.9 (July 1, 2018)
----------------------------
* Avoid Chrome to autocomplete no-login password fields
* Add cli flag to reset all feed errors
* Do not ignore errored feeds when a user refresh feeds manually
* Add specific 404 and 401 error messages
* Strip binaries to reduce size
* Make sure we always get the pagination in unread mode
* Fix incorrect data value when toggling bookmark flag on entry page
* Set opener to null when opening original URL with JavaScript
* Remove unnecessary style
* Refactor AddImageTitle rewriter
* Only processes images with `src` **and** `title` attributes (others are ignored)
* Processes **all** images in the document (not just the first one)
* Wraps the image and its title attribute in a `figure` tag with the title attribute's contents in a `figcaption` tag
* Improve sanitizer to remove `style`, `noscript` and `script` tag contents
* Improve feed and user API updates with optional values
* Add new fields for feed username/password
* Improve memory usage debug log
* Disable keep-alive for HTTP client
* Close HTTP response body even for failed requests
* Add Sans-Serif theme
* Rewrite iframe Youtube URLs to https://www.youtube-nocookie.com
* Add more filters for API call `/entries`:
* before (unix timestamp)
* before_entry_id (int64)
* after (unix timestamp)
* after_entry_id (int64)
* starred (boolean)
* Rewrite individual entry pagination SQL queries
* Simplify entry query builder
* Prevent items from sticking on touchend
* Extended horizontal overflow to feed and category views
* Translate missing strings
* Update German translation
Version 2.0.8 (June 4, 2018)
----------------------------
* Add Pocket integration
* Rewrite RealIP() to avoid returning an empty string
* Convert IP field from text to inet type
* Improve error handling in integration clients
* Make unread counter clickable
* Archive read entries automatically after 60 days
* Hide horizontal overflow when swiping articles on touch devices
* Add API endpoint to get logged user
* Fever API: Return response with an empty list if there is no item
* Handle feeds with dates formatted as Unix timestamp
Version 2.0.7 (May 7, 2018)
---------------------------
* Add API endpoint to import OPML file
* Make sure to close request body in HTTP client
* Do not show save link if no integration is configured
* Make sure integrations are configured before to make any HTTP requests
* Avoid people to unlink their OAuth2 account without having a local password
* Do not use shared variable to translate templates (avoid concurrency issue)
* Use vanilla HTTP handlers (refactoring)
* Move HTTP client to its own package (refactoring)
* Add middleware to read X-Forwarded-Proto header (refactoring)
* Use Gorilla middleware (refactoring)
* Scrape parent element for iframe
* Add SoundCloud and Bandcamp iframe sources
Version 2.0.6 (Apr 20, 2018)
----------------------------
* Improve graceful shutdown
* Simplify Heroku deployment
* Display memory usage and some metrics in logs
* Increase read/write timeout for HTTP server
* Add support for Dublin Core date in RDF feeds
* Do not return an error if the user session is not found
* Handle some non-english date formats
* Add missing French translation
* Rename RSS parser getters
* Get the right comments URL when having multiple namespaces
* Ignore caching headers for feeds that send "Expires: 0"
* Update translations
Version 2.0.5 (Apr 7, 2018)
---------------------------
Generated
-163
View File
@@ -1,163 +0,0 @@
# This file is autogenerated, do not edit; changes may be undone by the next 'dep ensure'.
[[projects]]
name = "github.com/PuerkitoBio/goquery"
packages = ["."]
revision = "e1271ee34c6a305e38566ecd27ae374944907ee9"
version = "v1.1.0"
[[projects]]
branch = "master"
name = "github.com/andybalholm/cascadia"
packages = ["."]
revision = "349dd0209470eabd9514242c688c403c0926d266"
[[projects]]
branch = "master"
name = "github.com/golang/protobuf"
packages = ["proto"]
revision = "1e59b77b52bf8e4b449a57e6f79f21226d571845"
[[projects]]
name = "github.com/gorilla/context"
packages = ["."]
revision = "1ea25387ff6f684839d82767c1733ff4d4d15d0a"
version = "v1.1"
[[projects]]
name = "github.com/gorilla/mux"
packages = ["."]
revision = "7f08801859139f86dfafd1c296e2cba9a80d292e"
version = "v1.6.0"
[[projects]]
branch = "master"
name = "github.com/lib/pq"
packages = [
".",
"hstore",
"oid"
]
revision = "83612a56d3dd153a94a629cd64925371c9adad78"
[[projects]]
branch = "master"
name = "github.com/miniflux/miniflux-go"
packages = ["."]
revision = "887ba3b062946784f0e64edb1734f435beb204f9"
[[projects]]
name = "github.com/tdewolff/minify"
packages = [
".",
"css",
"js"
]
revision = "222672169d634c440a73abc47685074e1a9daa60"
version = "v2.3.4"
[[projects]]
name = "github.com/tdewolff/parse"
packages = [
".",
"buffer",
"css",
"js",
"strconv"
]
revision = "639f6272aec6b52094db77b9ec488214b0b4b1a1"
version = "v2.3.2"
[[projects]]
branch = "master"
name = "github.com/tomasen/realip"
packages = ["."]
revision = "b5850897b7b539a1c9f22cdaa3b547d1bd453db8"
[[projects]]
branch = "master"
name = "golang.org/x/crypto"
packages = [
"acme",
"acme/autocert",
"bcrypt",
"blowfish",
"ssh/terminal"
]
revision = "1875d0a70c90e57f11972aefd42276df65e895b9"
[[projects]]
branch = "master"
name = "golang.org/x/net"
packages = [
"context",
"context/ctxhttp",
"html",
"html/atom",
"html/charset"
]
revision = "d866cfc389cec985d6fda2859936a575a55a3ab6"
[[projects]]
branch = "master"
name = "golang.org/x/oauth2"
packages = [
".",
"internal"
]
revision = "462316686f20eb6df426961c1c131bdaa5dfa68e"
[[projects]]
branch = "master"
name = "golang.org/x/sys"
packages = [
"unix",
"windows"
]
revision = "571f7bbbe08da2a8955aed9d4db316e78630e9a3"
[[projects]]
branch = "master"
name = "golang.org/x/text"
packages = [
"encoding",
"encoding/charmap",
"encoding/htmlindex",
"encoding/internal",
"encoding/internal/identifier",
"encoding/japanese",
"encoding/korean",
"encoding/simplifiedchinese",
"encoding/traditionalchinese",
"encoding/unicode",
"internal/gen",
"internal/tag",
"internal/utf8internal",
"language",
"runes",
"transform",
"unicode/cldr"
]
revision = "d5a9226ed7dd70cade6ccae9d37517fe14dd9fee"
[[projects]]
name = "google.golang.org/appengine"
packages = [
"internal",
"internal/base",
"internal/datastore",
"internal/log",
"internal/remote_api",
"internal/urlfetch",
"urlfetch"
]
revision = "150dc57a1b433e64154302bdc40b6bb8aefa313a"
version = "v1.0.0"
[solve-meta]
analyzer-name = "dep"
analyzer-version = 1
inputs-digest = "338222e5111416c46b2b8bde149443abc542b386dd02aff2a0dd6e13334bcf28"
solver-name = "gps-cdcl"
solver-version = 1
-58
View File
@@ -1,58 +0,0 @@
# Gopkg.toml example
#
# Refer to https://github.com/golang/dep/blob/master/docs/Gopkg.toml.md
# for detailed Gopkg.toml documentation.
#
# required = ["github.com/user/thing/cmd/thing"]
# ignored = ["github.com/user/project/pkgX", "bitbucket.org/user/project/pkgA/pkgY"]
#
# [[constraint]]
# name = "github.com/user/project"
# version = "1.0.0"
#
# [[constraint]]
# name = "github.com/user/project2"
# branch = "dev"
# source = "github.com/myfork/project2"
#
# [[override]]
# name = "github.com/x/y"
# version = "2.4.0"
[[constraint]]
name = "github.com/PuerkitoBio/goquery"
version = "1.1.0"
[[constraint]]
name = "github.com/gorilla/mux"
version = "1.6.0"
[[constraint]]
branch = "master"
name = "github.com/lib/pq"
[[constraint]]
branch = "master"
name = "github.com/miniflux/miniflux-go"
[[constraint]]
name = "github.com/tdewolff/minify"
version = "2.3.3"
[[constraint]]
branch = "master"
name = "github.com/tomasen/realip"
[[constraint]]
branch = "master"
name = "golang.org/x/crypto"
[[constraint]]
branch = "master"
name = "golang.org/x/net"
[[constraint]]
branch = "master"
name = "golang.org/x/oauth2"
+137 -32
View File
@@ -1,57 +1,162 @@
APP := miniflux
VERSION=$(shell git rev-parse --short HEAD)
BUILD_DATE=`date +%FT%T%z`
PKG_LIST := $(shell go list ./... | grep -v /vendor/)
DB_URL := postgres://postgres:postgres@localhost/miniflux_test?sslmode=disable
APP := miniflux
DOCKER_IMAGE := miniflux/miniflux
VERSION := $(shell git describe --tags --abbrev=0)
COMMIT := $(shell git rev-parse --short HEAD)
BUILD_DATE := `date +%FT%T%z`
LD_FLAGS := "-s -w -X 'miniflux.app/version.Version=$(VERSION)' -X 'miniflux.app/version.Commit=$(COMMIT)' -X 'miniflux.app/version.BuildDate=$(BUILD_DATE)'"
PKG_LIST := $(shell go list ./... | grep -v /vendor/)
DB_URL := postgres://postgres:postgres@localhost/miniflux_test?sslmode=disable
DEB_IMG_ARCH := amd64
.PHONY: linux linux-arm darwin freebsd build run clean test lint integration-test clean-integration-test
export PGPASSWORD := postgres
linux:
.PHONY: generate \
miniflux \
linux-amd64 \
linux-arm64 \
linux-armv7 \
linux-armv6 \
linux-armv5 \
linux-x86 \
darwin-amd64 \
freebsd-amd64 \
freebsd-x86 \
openbsd-amd64 \
openbsd-x86 \
netbsd-x86 \
netbsd-amd64 \
windows-amd64 \
windows-x86 \
build \
run \
clean \
test \
lint \
integration-test \
clean-integration-test \
docker-image \
docker-images \
rpm \
debian \
debian-packages
generate:
@ go generate
@ GOOS=linux GOARCH=amd64 go build -ldflags="-X 'github.com/miniflux/miniflux/version.Version=$(VERSION)' -X 'github.com/miniflux/miniflux/version.BuildDate=$(BUILD_DATE)'" -o $(APP)-linux-amd64 main.go
linux-arm:
@ go generate
@ GOOS=linux GOARCH=arm64 go build -ldflags="-X 'github.com/miniflux/miniflux/version.Version=$(VERSION)' -X 'github.com/miniflux/miniflux/version.BuildDate=$(BUILD_DATE)'" -o $(APP)-linux-armv8 main.go
@ GOOS=linux GOARCH=arm GOARM=7 go build -ldflags="-X 'github.com/miniflux/miniflux/version.Version=$(VERSION)' -X 'github.com/miniflux/miniflux/version.BuildDate=$(BUILD_DATE)'" -o $(APP)-linux-armv7 main.go
@ GOOS=linux GOARCH=arm GOARM=6 go build -ldflags="-X 'github.com/miniflux/miniflux/version.Version=$(VERSION)' -X 'github.com/miniflux/miniflux/version.BuildDate=$(BUILD_DATE)'" -o $(APP)-linux-armv6 main.go
@ GOOS=linux GOARCH=arm GOARM=5 go build -ldflags="-X 'github.com/miniflux/miniflux/version.Version=$(VERSION)' -X 'github.com/miniflux/miniflux/version.BuildDate=$(BUILD_DATE)'" -o $(APP)-linux-armv5 main.go
miniflux: generate
@ go build -ldflags=$(LD_FLAGS) -o $(APP) main.go
darwin:
@ go generate
@ GOOS=darwin GOARCH=amd64 go build -ldflags="-X 'github.com/miniflux/miniflux/version.Version=$(VERSION)' -X 'github.com/miniflux/miniflux/version.BuildDate=$(BUILD_DATE)'" -o $(APP)-darwin-amd64 main.go
linux-amd64: generate
@ GOOS=linux GOARCH=amd64 go build -ldflags=$(LD_FLAGS) -o $(APP)-linux-amd64 main.go
freebsd:
@ go generate
@ GOOS=freebsd GOARCH=amd64 go build -ldflags="-X 'githug.com/miniflux/miniflux/version.Version=$(VERSION)' -X 'github.com/miniflux/miniflux/version.BuildDate=$(BUILD_DATE)'" -o $(APP)-freebsd-amd64 main.go
linux-arm64: generate
@ GOOS=linux GOARCH=arm64 go build -ldflags=$(LD_FLAGS) -o $(APP)-linux-arm64 main.go
build: linux linux-arm darwin freebsd
linux-armv7: generate
@ GOOS=linux GOARCH=arm GOARM=7 go build -ldflags=$(LD_FLAGS) -o $(APP)-linux-armv7 main.go
run:
@ go generate
@ go run main.go -debug
linux-armv6: generate
@ GOOS=linux GOARCH=arm GOARM=6 go build -ldflags=$(LD_FLAGS) -o $(APP)-linux-armv6 main.go
linux-armv5: generate
@ GOOS=linux GOARCH=arm GOARM=5 go build -ldflags=$(LD_FLAGS) -o $(APP)-linux-armv5 main.go
darwin-amd64: generate
@ GOOS=darwin GOARCH=amd64 go build -ldflags=$(LD_FLAGS) -o $(APP)-darwin-amd64 main.go
freebsd-amd64: generate
@ GOOS=freebsd GOARCH=amd64 go build -ldflags=$(LD_FLAGS) -o $(APP)-freebsd-amd64 main.go
openbsd-amd64: generate
@ GOOS=openbsd GOARCH=amd64 go build -ldflags=$(LD_FLAGS) -o $(APP)-openbsd-amd64 main.go
windows-amd64: generate
@ GOOS=windows GOARCH=amd64 go build -ldflags=$(LD_FLAGS) -o $(APP)-windows-amd64 main.go
build: linux-amd64 linux-arm64 linux-armv7 linux-armv6 linux-armv5 darwin-amd64 freebsd-amd64 openbsd-amd64 windows-amd64
# NOTE: unsupported targets
netbsd-amd64: generate
@ GOOS=netbsd GOARCH=amd64 go build -ldflags=$(LD_FLAGS) -o $(APP)-netbsd-amd64 main.go
linux-x86: generate
@ GOOS=linux GOARCH=386 go build -ldflags=$(LD_FLAGS) -o $(APP)-linux-x86 main.go
freebsd-x86: generate
@ GOOS=freebsd GOARCH=386 go build -ldflags=$(LD_FLAGS) -o $(APP)-freebsd-x86 main.go
netbsd-x86: generate
@ GOOS=netbsd GOARCH=386 go build -ldflags=$(LD_FLAGS) -o $(APP)-netbsd-x86 main.go
openbsd-x86: generate
@ GOOS=openbsd GOARCH=386 go build -ldflags=$(LD_FLAGS) -o $(APP)-freebsd-x86 main.go
windows-x86: generate
@ GOOS=windows GOARCH=386 go build -ldflags=$(LD_FLAGS) -o $(APP)-windows-x86 main.go
run: generate
@ LOG_DATE_TIME=1 go run main.go -debug
clean:
@ rm -f $(APP)-*
@ rm -f $(APP)-* $(APP) $(APP)*.rpm $(APP)*.deb
test:
go test -cover -race ./...
go test -cover -race -count=1 ./...
lint:
@ golint -set_exit_status ${PKG_LIST}
golint -set_exit_status ${PKG_LIST}
integration-test:
psql -U postgres -c 'drop database if exists miniflux_test;'
psql -U postgres -c 'create database miniflux_test;'
DATABASE_URL=$(DB_URL) go run main.go -migrate
DATABASE_URL=$(DB_URL) ADMIN_USERNAME=admin ADMIN_PASSWORD=test123 go run main.go -create-admin
go build -o miniflux-test main.go
DATABASE_URL=$(DB_URL) ./miniflux-test -debug >/tmp/miniflux.log 2>&1 & echo "$$!" > "/tmp/miniflux.pid"
while ! echo exit | nc localhost 8080; do sleep 1; done >/dev/null
go test -v -tags=integration || cat /tmp/miniflux.log
DATABASE_URL=$(DB_URL) \
ADMIN_USERNAME=admin \
ADMIN_PASSWORD=test123 \
CREATE_ADMIN=1 \
RUN_MIGRATIONS=1 \
DEBUG=1 \
./miniflux-test >/tmp/miniflux.log 2>&1 & echo "$$!" > "/tmp/miniflux.pid"
while ! nc -z localhost 8080; do sleep 1; done
go test -v -tags=integration -count=1 miniflux.app/tests
clean-integration-test:
@ kill -9 `cat /tmp/miniflux.pid`
@ rm -f /tmp/miniflux.pid /tmp/miniflux.log
@ rm miniflux-test
@ psql -U postgres -c 'drop database if exists miniflux_test;'
docker-image:
docker build -t $(DOCKER_IMAGE):$(VERSION) -f packaging/docker/Dockerfile .
docker-images:
docker buildx build \
--platform linux/amd64,linux/arm64,linux/arm/v7,linux/arm/v6 \
--file packaging/docker/Dockerfile \
--tag $(DOCKER_IMAGE):$(VERSION) \
--push .
rpm: clean
@ docker build \
-t miniflux-rpm-builder \
-f packaging/rpm/Dockerfile \
.
@ docker run --rm \
-v ${PWD}:/root/rpmbuild/RPMS/x86_64 miniflux-rpm-builder \
rpmbuild -bb --define "_miniflux_version $(VERSION)" /root/rpmbuild/SPECS/miniflux.spec
debian:
@ docker build \
--build-arg BASE_IMAGE_ARCH=$(DEB_IMG_ARCH) \
-t $(DEB_IMG_ARCH)/miniflux-deb-builder \
-f packaging/debian/Dockerfile \
.
@ docker run --rm \
-v ${PWD}:/pkg $(DEB_IMG_ARCH)/miniflux-deb-builder
debian-packages: clean
$(MAKE) debian DEB_IMG_ARCH=amd64
$(MAKE) debian DEB_IMG_ARCH=arm64v8
$(MAKE) debian DEB_IMG_ARCH=arm32v7
+1
View File
@@ -0,0 +1 @@
web: miniflux.app
+30 -12
View File
@@ -1,8 +1,5 @@
Miniflux 2
==========
[![Build Status](https://travis-ci.org/miniflux/miniflux.svg?branch=master)](https://travis-ci.org/miniflux/miniflux)
[![GoDoc](https://godoc.org/github.com/miniflux/miniflux?status.svg)](https://godoc.org/github.com/miniflux/miniflux)
[![Documentation Status](https://readthedocs.org/projects/miniflux/badge/?version=latest)](https://docs.miniflux.net/)
Miniflux is a minimalist and opinionated feed reader:
@@ -11,26 +8,47 @@ Miniflux is a minimalist and opinionated feed reader:
- Doesn't use any ORM
- Doesn't use any complicated framework
- Use only modern vanilla Javascript (ES6 and Fetch API)
- Single binary compiled statically without dependency
- The number of features is voluntarily limited
It's simple, fast, lightweight and super easy to install.
Miniflux 2 is a rewrite of [Miniflux 1.x](https://github.com/miniflux/miniflux-legacy) in Golang.
Official website: <https://miniflux.app>
Documentation
-------------
The Miniflux documentation is available here: <https://docs.miniflux.net/>
The Miniflux documentation is available here: <https://miniflux.app/docs/> ([Man page](https://miniflux.app/miniflux.1.html))
- [Opinionated?](https://docs.miniflux.net/en/latest/opinionated.html)
- [Features](https://docs.miniflux.net/en/latest/features.html)
- [Requirements](https://docs.miniflux.net/en/latest/requirements.html)
- [Installation](https://docs.miniflux.net/en/latest/installation.html)
- [Upgrading to a new version](https://docs.miniflux.net/en/latest/upgrade.html)
- [Configuration](https://docs.miniflux.net/en/latest/configuration.html)
- [Opinionated?](https://miniflux.app/opinionated.html)
- [Features](https://miniflux.app/features.html)
- [Requirements](https://miniflux.app/docs/requirements.html)
- [Installation Instructions](https://miniflux.app/docs/installation.html)
- [Upgrading to a New Version](https://miniflux.app/docs/upgrade.html)
- [Configuration](https://miniflux.app/docs/configuration.html)
- [Command Line Usage](https://miniflux.app/docs/cli.html)
- [User Interface Usage](https://miniflux.app/docs/ui.html)
- [Keyboard Shortcuts](https://miniflux.app/docs/keyboard_shortcuts.html)
- [Integration with External Services](https://miniflux.app/docs/services.html)
- [Rewrite and Scraper Rules](https://miniflux.app/docs/rules.html)
- [API Reference](https://miniflux.app/docs/api.html)
- [Development](https://miniflux.app/docs/development.html)
- [Internationalization](https://miniflux.app/docs/i18n.html)
- [Frequently Asked Questions](https://miniflux.app/faq.html)
Screenshots
-----------
Default theme:
![Default theme](https://miniflux.app/images/overview.png)
Dark theme when using keyboard navigation:
![Dark theme](https://miniflux.app/images/item-selection-black-theme.png)
Credits
-------
- Author: Frédéric Guillot
- Authors: Frédéric Guillot - [List of contributors](https://github.com/miniflux/v2/graphs/contributors)
- Distributed under Apache 2.0 License
+65
View File
@@ -0,0 +1,65 @@
// Copyright 2018 Frédéric Guillot. All rights reserved.
// Use of this source code is governed by the Apache 2.0
// license that can be found in the LICENSE file.
package api // import "miniflux.app/api"
import (
"net/http"
"miniflux.app/storage"
"miniflux.app/worker"
"github.com/gorilla/mux"
)
type handler struct {
store *storage.Storage
pool *worker.Pool
}
// Serve declares API routes for the application.
func Serve(router *mux.Router, store *storage.Storage, pool *worker.Pool) {
handler := &handler{store, pool}
sr := router.PathPrefix("/v1").Subrouter()
middleware := newMiddleware(store)
sr.Use(middleware.handleCORS)
sr.Use(middleware.apiKeyAuth)
sr.Use(middleware.basicAuth)
sr.Methods(http.MethodOptions)
sr.HandleFunc("/users", handler.createUser).Methods(http.MethodPost)
sr.HandleFunc("/users", handler.users).Methods(http.MethodGet)
sr.HandleFunc("/users/{userID:[0-9]+}", handler.userByID).Methods(http.MethodGet)
sr.HandleFunc("/users/{userID:[0-9]+}", handler.updateUser).Methods(http.MethodPut)
sr.HandleFunc("/users/{userID:[0-9]+}", handler.removeUser).Methods(http.MethodDelete)
sr.HandleFunc("/users/{userID:[0-9]+}/mark-all-as-read", handler.markUserAsRead).Methods(http.MethodPut)
sr.HandleFunc("/users/{username}", handler.userByUsername).Methods(http.MethodGet)
sr.HandleFunc("/me", handler.currentUser).Methods(http.MethodGet)
sr.HandleFunc("/categories", handler.createCategory).Methods(http.MethodPost)
sr.HandleFunc("/categories", handler.getCategories).Methods(http.MethodGet)
sr.HandleFunc("/categories/{categoryID}", handler.updateCategory).Methods(http.MethodPut)
sr.HandleFunc("/categories/{categoryID}", handler.removeCategory).Methods(http.MethodDelete)
sr.HandleFunc("/categories/{categoryID}/mark-all-as-read", handler.markCategoryAsRead).Methods(http.MethodPut)
sr.HandleFunc("/categories/{categoryID}/feeds", handler.getCategoryFeeds).Methods(http.MethodGet)
sr.HandleFunc("/categories/{categoryID}/entries", handler.getCategoryEntries).Methods(http.MethodGet)
sr.HandleFunc("/categories/{categoryID}/entries/{entryID}", handler.getCategoryEntry).Methods(http.MethodGet)
sr.HandleFunc("/discover", handler.discoverSubscriptions).Methods(http.MethodPost)
sr.HandleFunc("/feeds", handler.createFeed).Methods(http.MethodPost)
sr.HandleFunc("/feeds", handler.getFeeds).Methods(http.MethodGet)
sr.HandleFunc("/feeds/refresh", handler.refreshAllFeeds).Methods(http.MethodPut)
sr.HandleFunc("/feeds/{feedID}/refresh", handler.refreshFeed).Methods(http.MethodPut)
sr.HandleFunc("/feeds/{feedID}", handler.getFeed).Methods(http.MethodGet)
sr.HandleFunc("/feeds/{feedID}", handler.updateFeed).Methods(http.MethodPut)
sr.HandleFunc("/feeds/{feedID}", handler.removeFeed).Methods(http.MethodDelete)
sr.HandleFunc("/feeds/{feedID}/icon", handler.feedIcon).Methods(http.MethodGet)
sr.HandleFunc("/feeds/{feedID}/mark-all-as-read", handler.markFeedAsRead).Methods(http.MethodPut)
sr.HandleFunc("/export", handler.exportFeeds).Methods(http.MethodGet)
sr.HandleFunc("/import", handler.importFeeds).Methods(http.MethodPost)
sr.HandleFunc("/feeds/{feedID}/entries", handler.getFeedEntries).Methods(http.MethodGet)
sr.HandleFunc("/feeds/{feedID}/entries/{entryID}", handler.getFeedEntry).Methods(http.MethodGet)
sr.HandleFunc("/entries", handler.getEntries).Methods(http.MethodGet)
sr.HandleFunc("/entries", handler.setEntryStatus).Methods(http.MethodPut)
sr.HandleFunc("/entries/{entryID}", handler.getEntry).Methods(http.MethodGet)
sr.HandleFunc("/entries/{entryID}/bookmark", handler.toggleBookmark).Methods(http.MethodPut)
}
+89 -67
View File
@@ -2,102 +2,124 @@
// Use of this source code is governed by the Apache 2.0
// license that can be found in the LICENSE file.
package api
package api // import "miniflux.app/api"
import (
"errors"
json_parser "encoding/json"
"net/http"
"time"
"github.com/miniflux/miniflux/http/handler"
"miniflux.app/http/request"
"miniflux.app/http/response/json"
"miniflux.app/model"
"miniflux.app/validator"
)
// CreateCategory is the API handler to create a new category.
func (c *Controller) CreateCategory(ctx *handler.Context, request *handler.Request, response *handler.Response) {
userID := ctx.UserID()
category, err := decodeCategoryPayload(request.Body())
func (h *handler) createCategory(w http.ResponseWriter, r *http.Request) {
userID := request.UserID(r)
var categoryRequest model.CategoryRequest
if err := json_parser.NewDecoder(r.Body).Decode(&categoryRequest); err != nil {
json.BadRequest(w, r, err)
return
}
if validationErr := validator.ValidateCategoryCreation(h.store, userID, &categoryRequest); validationErr != nil {
json.BadRequest(w, r, validationErr.Error())
return
}
category, err := h.store.CreateCategory(userID, &categoryRequest)
if err != nil {
response.JSON().BadRequest(err)
json.ServerError(w, r, err)
return
}
category.UserID = userID
if err := category.ValidateCategoryCreation(); err != nil {
response.JSON().BadRequest(err)
return
}
if c, err := c.store.CategoryByTitle(userID, category.Title); err != nil || c != nil {
response.JSON().BadRequest(errors.New("This category already exists"))
return
}
err = c.store.CreateCategory(category)
if err != nil {
response.JSON().ServerError(errors.New("Unable to create this category"))
return
}
response.JSON().Created(category)
json.Created(w, r, category)
}
// UpdateCategory is the API handler to update a category.
func (c *Controller) UpdateCategory(ctx *handler.Context, request *handler.Request, response *handler.Response) {
categoryID, err := request.IntegerParam("categoryID")
func (h *handler) updateCategory(w http.ResponseWriter, r *http.Request) {
userID := request.UserID(r)
categoryID := request.RouteInt64Param(r, "categoryID")
category, err := h.store.Category(userID, categoryID)
if err != nil {
response.JSON().BadRequest(err)
json.ServerError(w, r, err)
return
}
category, err := decodeCategoryPayload(request.Body())
if category == nil {
json.NotFound(w, r)
return
}
var categoryRequest model.CategoryRequest
if err := json_parser.NewDecoder(r.Body).Decode(&categoryRequest); err != nil {
json.BadRequest(w, r, err)
return
}
if validationErr := validator.ValidateCategoryModification(h.store, userID, category.ID, &categoryRequest); validationErr != nil {
json.BadRequest(w, r, validationErr.Error())
return
}
categoryRequest.Patch(category)
err = h.store.UpdateCategory(category)
if err != nil {
response.JSON().BadRequest(err)
json.ServerError(w, r, err)
return
}
category.UserID = ctx.UserID()
category.ID = categoryID
if err := category.ValidateCategoryModification(); err != nil {
response.JSON().BadRequest(err)
return
}
err = c.store.UpdateCategory(category)
if err != nil {
response.JSON().ServerError(errors.New("Unable to update this category"))
return
}
response.JSON().Created(category)
json.Created(w, r, category)
}
// GetCategories is the API handler to get a list of categories for a given user.
func (c *Controller) GetCategories(ctx *handler.Context, request *handler.Request, response *handler.Response) {
categories, err := c.store.Categories(ctx.UserID())
func (h *handler) markCategoryAsRead(w http.ResponseWriter, r *http.Request) {
userID := request.UserID(r)
categoryID := request.RouteInt64Param(r, "categoryID")
category, err := h.store.Category(userID, categoryID)
if err != nil {
response.JSON().ServerError(errors.New("Unable to fetch categories"))
json.ServerError(w, r, err)
return
}
response.JSON().Standard(categories)
if category == nil {
json.NotFound(w, r)
return
}
if err = h.store.MarkCategoryAsRead(userID, categoryID, time.Now()); err != nil {
json.ServerError(w, r, err)
return
}
json.NoContent(w, r)
}
// RemoveCategory is the API handler to remove a category.
func (c *Controller) RemoveCategory(ctx *handler.Context, request *handler.Request, response *handler.Response) {
userID := ctx.UserID()
categoryID, err := request.IntegerParam("categoryID")
func (h *handler) getCategories(w http.ResponseWriter, r *http.Request) {
categories, err := h.store.Categories(request.UserID(r))
if err != nil {
response.JSON().BadRequest(err)
json.ServerError(w, r, err)
return
}
if !c.store.CategoryExists(userID, categoryID) {
response.JSON().NotFound(errors.New("Category not found"))
return
}
if err := c.store.RemoveCategory(userID, categoryID); err != nil {
response.JSON().ServerError(errors.New("Unable to remove this category"))
return
}
response.JSON().NoContent()
json.OK(w, r, categories)
}
func (h *handler) removeCategory(w http.ResponseWriter, r *http.Request) {
userID := request.UserID(r)
categoryID := request.RouteInt64Param(r, "categoryID")
if !h.store.CategoryIDExists(userID, categoryID) {
json.NotFound(w, r)
return
}
if err := h.store.RemoveCategory(userID, categoryID); err != nil {
json.ServerError(w, r, err)
return
}
json.NoContent(w, r)
}
-21
View File
@@ -1,21 +0,0 @@
// Copyright 2017 Frédéric Guillot. All rights reserved.
// Use of this source code is governed by the Apache 2.0
// license that can be found in the LICENSE file.
package api
import (
"github.com/miniflux/miniflux/reader/feed"
"github.com/miniflux/miniflux/storage"
)
// Controller holds all handlers for the API.
type Controller struct {
store *storage.Storage
feedHandler *feed.Handler
}
// NewController creates a new controller.
func NewController(store *storage.Storage, feedHandler *feed.Handler) *Controller {
return &Controller{store: store, feedHandler: feedHandler}
}
-10
View File
@@ -1,10 +0,0 @@
// Copyright 2018 Frédéric Guillot. All rights reserved.
// Use of this source code is governed by the MIT license
// that can be found in the LICENSE file.
/*
Package api implements API endpoints for Miniflux application.
*/
package api
+129 -143
View File
@@ -2,222 +2,208 @@
// Use of this source code is governed by the Apache 2.0
// license that can be found in the LICENSE file.
package api
package api // import "miniflux.app/api"
import (
json_parser "encoding/json"
"errors"
"net/http"
"time"
"github.com/miniflux/miniflux/http/handler"
"github.com/miniflux/miniflux/model"
"miniflux.app/http/request"
"miniflux.app/http/response/json"
"miniflux.app/model"
"miniflux.app/storage"
"miniflux.app/validator"
)
// GetFeedEntry is the API handler to get a single feed entry.
func (c *Controller) GetFeedEntry(ctx *handler.Context, request *handler.Request, response *handler.Response) {
userID := ctx.UserID()
feedID, err := request.IntegerParam("feedID")
func getEntryFromBuilder(w http.ResponseWriter, r *http.Request, b *storage.EntryQueryBuilder) {
entry, err := b.GetEntry()
if err != nil {
response.JSON().BadRequest(err)
json.ServerError(w, r, err)
return
}
entryID, err := request.IntegerParam("entryID")
if err != nil {
response.JSON().BadRequest(err)
if entry == nil {
json.NotFound(w, r)
return
}
builder := c.store.NewEntryQueryBuilder(userID)
json.OK(w, r, entry)
}
func (h *handler) getFeedEntry(w http.ResponseWriter, r *http.Request) {
feedID := request.RouteInt64Param(r, "feedID")
entryID := request.RouteInt64Param(r, "entryID")
builder := h.store.NewEntryQueryBuilder(request.UserID(r))
builder.WithFeedID(feedID)
builder.WithEntryID(entryID)
entry, err := builder.GetEntry()
if err != nil {
response.JSON().ServerError(errors.New("Unable to fetch this entry from the database"))
return
}
if entry == nil {
response.JSON().NotFound(errors.New("Entry not found"))
return
}
response.JSON().Standard(entry)
getEntryFromBuilder(w, r, builder)
}
// GetEntry is the API handler to get a single entry.
func (c *Controller) GetEntry(ctx *handler.Context, request *handler.Request, response *handler.Response) {
userID := ctx.UserID()
entryID, err := request.IntegerParam("entryID")
if err != nil {
response.JSON().BadRequest(err)
return
}
func (h *handler) getCategoryEntry(w http.ResponseWriter, r *http.Request) {
categoryID := request.RouteInt64Param(r, "categoryID")
entryID := request.RouteInt64Param(r, "entryID")
builder := c.store.NewEntryQueryBuilder(userID)
builder := h.store.NewEntryQueryBuilder(request.UserID(r))
builder.WithCategoryID(categoryID)
builder.WithEntryID(entryID)
entry, err := builder.GetEntry()
if err != nil {
response.JSON().ServerError(errors.New("Unable to fetch this entry from the database"))
return
}
if entry == nil {
response.JSON().NotFound(errors.New("Entry not found"))
return
}
response.JSON().Standard(entry)
getEntryFromBuilder(w, r, builder)
}
// GetFeedEntries is the API handler to get all feed entries.
func (c *Controller) GetFeedEntries(ctx *handler.Context, request *handler.Request, response *handler.Response) {
userID := ctx.UserID()
feedID, err := request.IntegerParam("feedID")
if err != nil {
response.JSON().BadRequest(err)
return
}
func (h *handler) getEntry(w http.ResponseWriter, r *http.Request) {
entryID := request.RouteInt64Param(r, "entryID")
builder := h.store.NewEntryQueryBuilder(request.UserID(r))
builder.WithEntryID(entryID)
status := request.QueryStringParam("status", "")
if status != "" {
if err := model.ValidateEntryStatus(status); err != nil {
response.JSON().BadRequest(err)
getEntryFromBuilder(w, r, builder)
}
func (h *handler) getFeedEntries(w http.ResponseWriter, r *http.Request) {
feedID := request.RouteInt64Param(r, "feedID")
h.findEntries(w, r, feedID, 0)
}
func (h *handler) getCategoryEntries(w http.ResponseWriter, r *http.Request) {
categoryID := request.RouteInt64Param(r, "categoryID")
h.findEntries(w, r, 0, categoryID)
}
func (h *handler) getEntries(w http.ResponseWriter, r *http.Request) {
h.findEntries(w, r, 0, 0)
}
func (h *handler) findEntries(w http.ResponseWriter, r *http.Request, feedID int64, categoryID int64) {
statuses := request.QueryStringParamList(r, "status")
for _, status := range statuses {
if err := validator.ValidateEntryStatus(status); err != nil {
json.BadRequest(w, r, err)
return
}
}
order := request.QueryStringParam("order", model.DefaultSortingOrder)
if err := model.ValidateEntryOrder(order); err != nil {
response.JSON().BadRequest(err)
order := request.QueryStringParam(r, "order", model.DefaultSortingOrder)
if err := validator.ValidateEntryOrder(order); err != nil {
json.BadRequest(w, r, err)
return
}
direction := request.QueryStringParam("direction", model.DefaultSortingDirection)
if err := model.ValidateDirection(direction); err != nil {
response.JSON().BadRequest(err)
direction := request.QueryStringParam(r, "direction", model.DefaultSortingDirection)
if err := validator.ValidateDirection(direction); err != nil {
json.BadRequest(w, r, err)
return
}
limit := request.QueryIntegerParam("limit", 100)
offset := request.QueryIntegerParam("offset", 0)
if err := model.ValidateRange(offset, limit); err != nil {
response.JSON().BadRequest(err)
limit := request.QueryIntParam(r, "limit", 100)
offset := request.QueryIntParam(r, "offset", 0)
if err := validator.ValidateRange(offset, limit); err != nil {
json.BadRequest(w, r, err)
return
}
builder := c.store.NewEntryQueryBuilder(userID)
userID := request.UserID(r)
categoryID = request.QueryInt64Param(r, "category_id", categoryID)
if categoryID > 0 && !h.store.CategoryIDExists(userID, categoryID) {
json.BadRequest(w, r, errors.New("Invalid category ID"))
return
}
feedID = request.QueryInt64Param(r, "feed_id", feedID)
if feedID > 0 && !h.store.FeedExists(userID, feedID) {
json.BadRequest(w, r, errors.New("Invalid feed ID"))
return
}
builder := h.store.NewEntryQueryBuilder(userID)
builder.WithFeedID(feedID)
builder.WithStatus(status)
builder.WithCategoryID(categoryID)
builder.WithStatuses(statuses)
builder.WithOrder(order)
builder.WithDirection(direction)
builder.WithOffset(offset)
builder.WithLimit(limit)
configureFilters(builder, r)
entries, err := builder.GetEntries()
if err != nil {
response.JSON().ServerError(errors.New("Unable to fetch the list of entries"))
json.ServerError(w, r, err)
return
}
count, err := builder.CountEntries()
if err != nil {
response.JSON().ServerError(errors.New("Unable to count the number of entries"))
json.ServerError(w, r, err)
return
}
response.JSON().Standard(&entriesResponse{Total: count, Entries: entries})
json.OK(w, r, &entriesResponse{Total: count, Entries: entries})
}
// GetEntries is the API handler to fetch entries.
func (c *Controller) GetEntries(ctx *handler.Context, request *handler.Request, response *handler.Response) {
userID := ctx.UserID()
status := request.QueryStringParam("status", "")
if status != "" {
if err := model.ValidateEntryStatus(status); err != nil {
response.JSON().BadRequest(err)
return
}
}
order := request.QueryStringParam("order", model.DefaultSortingOrder)
if err := model.ValidateEntryOrder(order); err != nil {
response.JSON().BadRequest(err)
func (h *handler) setEntryStatus(w http.ResponseWriter, r *http.Request) {
var entriesStatusUpdateRequest model.EntriesStatusUpdateRequest
if err := json_parser.NewDecoder(r.Body).Decode(&entriesStatusUpdateRequest); err != nil {
json.BadRequest(w, r, err)
return
}
direction := request.QueryStringParam("direction", model.DefaultSortingDirection)
if err := model.ValidateDirection(direction); err != nil {
response.JSON().BadRequest(err)
if err := validator.ValidateEntriesStatusUpdateRequest(&entriesStatusUpdateRequest); err != nil {
json.BadRequest(w, r, err)
return
}
limit := request.QueryIntegerParam("limit", 100)
offset := request.QueryIntegerParam("offset", 0)
if err := model.ValidateRange(offset, limit); err != nil {
response.JSON().BadRequest(err)
if err := h.store.SetEntriesStatus(request.UserID(r), entriesStatusUpdateRequest.EntryIDs, entriesStatusUpdateRequest.Status); err != nil {
json.ServerError(w, r, err)
return
}
builder := c.store.NewEntryQueryBuilder(userID)
builder.WithStatus(status)
builder.WithOrder(order)
builder.WithDirection(direction)
builder.WithOffset(offset)
builder.WithLimit(limit)
entries, err := builder.GetEntries()
if err != nil {
response.JSON().ServerError(errors.New("Unable to fetch the list of entries"))
return
}
count, err := builder.CountEntries()
if err != nil {
response.JSON().ServerError(errors.New("Unable to count the number of entries"))
return
}
response.JSON().Standard(&entriesResponse{Total: count, Entries: entries})
json.NoContent(w, r)
}
// SetEntryStatus is the API handler to change the status of entries.
func (c *Controller) SetEntryStatus(ctx *handler.Context, request *handler.Request, response *handler.Response) {
userID := ctx.UserID()
entryIDs, status, err := decodeEntryStatusPayload(request.Body())
if err != nil {
response.JSON().BadRequest(errors.New("Invalid JSON payload"))
func (h *handler) toggleBookmark(w http.ResponseWriter, r *http.Request) {
entryID := request.RouteInt64Param(r, "entryID")
if err := h.store.ToggleBookmark(request.UserID(r), entryID); err != nil {
json.ServerError(w, r, err)
return
}
if err := model.ValidateEntryStatus(status); err != nil {
response.JSON().BadRequest(err)
return
}
if err := c.store.SetEntriesStatus(userID, entryIDs, status); err != nil {
response.JSON().ServerError(errors.New("Unable to change entries status"))
return
}
response.JSON().NoContent()
json.NoContent(w, r)
}
// ToggleBookmark is the API handler to toggle bookmark status.
func (c *Controller) ToggleBookmark(ctx *handler.Context, request *handler.Request, response *handler.Response) {
userID := ctx.UserID()
entryID, err := request.IntegerParam("entryID")
if err != nil {
response.JSON().BadRequest(err)
return
func configureFilters(builder *storage.EntryQueryBuilder, r *http.Request) {
beforeEntryID := request.QueryInt64Param(r, "before_entry_id", 0)
if beforeEntryID > 0 {
builder.BeforeEntryID(beforeEntryID)
}
if err := c.store.ToggleBookmark(userID, entryID); err != nil {
response.JSON().ServerError(errors.New("Unable to toggle bookmark value"))
return
afterEntryID := request.QueryInt64Param(r, "after_entry_id", 0)
if afterEntryID > 0 {
builder.AfterEntryID(afterEntryID)
}
response.JSON().NoContent()
beforeTimestamp := request.QueryInt64Param(r, "before", 0)
if beforeTimestamp > 0 {
builder.BeforeDate(time.Unix(beforeTimestamp, 0))
}
afterTimestamp := request.QueryInt64Param(r, "after", 0)
if afterTimestamp > 0 {
builder.AfterDate(time.Unix(afterTimestamp, 0))
}
categoryID := request.QueryInt64Param(r, "category_id", 0)
if categoryID > 0 {
builder.WithCategoryID(categoryID)
}
if request.HasQueryParam(r, "starred") {
builder.WithStarred()
}
searchQuery := request.QueryStringParam(r, "search", "")
if searchQuery != "" {
builder.WithSearchQuery(searchQuery)
}
}
+137 -125
View File
@@ -2,191 +2,203 @@
// Use of this source code is governed by the Apache 2.0
// license that can be found in the LICENSE file.
package api
package api // import "miniflux.app/api"
import (
"errors"
json_parser "encoding/json"
"net/http"
"time"
"github.com/miniflux/miniflux/reader/opml"
"github.com/miniflux/miniflux/http/handler"
"miniflux.app/http/request"
"miniflux.app/http/response/json"
"miniflux.app/model"
feedHandler "miniflux.app/reader/handler"
"miniflux.app/validator"
)
// CreateFeed is the API handler to create a new feed.
func (c *Controller) CreateFeed(ctx *handler.Context, request *handler.Request, response *handler.Response) {
userID := ctx.UserID()
feedURL, categoryID, err := decodeFeedCreationPayload(request.Body())
func (h *handler) createFeed(w http.ResponseWriter, r *http.Request) {
userID := request.UserID(r)
var feedCreationRequest model.FeedCreationRequest
if err := json_parser.NewDecoder(r.Body).Decode(&feedCreationRequest); err != nil {
json.BadRequest(w, r, err)
return
}
if validationErr := validator.ValidateFeedCreation(h.store, userID, &feedCreationRequest); validationErr != nil {
json.BadRequest(w, r, validationErr.Error())
return
}
feed, err := feedHandler.CreateFeed(h.store, userID, &feedCreationRequest)
if err != nil {
response.JSON().BadRequest(err)
json.ServerError(w, r, err)
return
}
if feedURL == "" {
response.JSON().BadRequest(errors.New("The feed_url is required"))
return
}
if categoryID <= 0 {
response.JSON().BadRequest(errors.New("The category_id is required"))
return
}
if c.store.FeedURLExists(userID, feedURL) {
response.JSON().BadRequest(errors.New("This feed_url already exists"))
return
}
if !c.store.CategoryExists(userID, categoryID) {
response.JSON().BadRequest(errors.New("This category_id doesn't exists or doesn't belongs to this user"))
return
}
feed, err := c.feedHandler.CreateFeed(userID, categoryID, feedURL, false)
if err != nil {
response.JSON().ServerError(errors.New("Unable to create this feed"))
return
}
type result struct {
FeedID int64 `json:"feed_id"`
}
response.JSON().Created(&result{FeedID: feed.ID})
json.Created(w, r, &feedCreationResponse{FeedID: feed.ID})
}
// RefreshFeed is the API handler to refresh a feed.
func (c *Controller) RefreshFeed(ctx *handler.Context, request *handler.Request, response *handler.Response) {
userID := ctx.UserID()
feedID, err := request.IntegerParam("feedID")
func (h *handler) refreshFeed(w http.ResponseWriter, r *http.Request) {
feedID := request.RouteInt64Param(r, "feedID")
userID := request.UserID(r)
if !h.store.FeedExists(userID, feedID) {
json.NotFound(w, r)
return
}
err := feedHandler.RefreshFeed(h.store, userID, feedID)
if err != nil {
response.JSON().BadRequest(err)
json.ServerError(w, r, err)
return
}
if !c.store.FeedExists(userID, feedID) {
response.JSON().NotFound(errors.New("Unable to find this feed"))
return
}
err = c.feedHandler.RefreshFeed(userID, feedID)
if err != nil {
response.JSON().ServerError(errors.New("Unable to refresh this feed"))
return
}
response.JSON().NoContent()
json.NoContent(w, r)
}
// UpdateFeed is the API handler that is used to update a feed.
func (c *Controller) UpdateFeed(ctx *handler.Context, request *handler.Request, response *handler.Response) {
userID := ctx.UserID()
feedID, err := request.IntegerParam("feedID")
func (h *handler) refreshAllFeeds(w http.ResponseWriter, r *http.Request) {
userID := request.UserID(r)
jobs, err := h.store.NewUserBatch(userID, h.store.CountFeeds(userID))
if err != nil {
response.JSON().BadRequest(err)
json.ServerError(w, r, err)
return
}
newFeed, err := decodeFeedModificationPayload(request.Body())
if err != nil {
response.JSON().BadRequest(err)
go func() {
h.pool.Push(jobs)
}()
json.NoContent(w, r)
}
func (h *handler) updateFeed(w http.ResponseWriter, r *http.Request) {
var feedModificationRequest model.FeedModificationRequest
if err := json_parser.NewDecoder(r.Body).Decode(&feedModificationRequest); err != nil {
json.BadRequest(w, r, err)
return
}
if newFeed.Category != nil && newFeed.Category.ID != 0 && !c.store.CategoryExists(userID, newFeed.Category.ID) {
response.JSON().BadRequest(errors.New("This category_id doesn't exists or doesn't belongs to this user"))
return
}
userID := request.UserID(r)
feedID := request.RouteInt64Param(r, "feedID")
originalFeed, err := c.store.FeedByID(userID, feedID)
originalFeed, err := h.store.FeedByID(userID, feedID)
if err != nil {
response.JSON().NotFound(errors.New("Unable to find this feed"))
json.NotFound(w, r)
return
}
if originalFeed == nil {
response.JSON().NotFound(errors.New("Feed not found"))
json.NotFound(w, r)
return
}
originalFeed.Merge(newFeed)
if err := c.store.UpdateFeed(originalFeed); err != nil {
response.JSON().ServerError(errors.New("Unable to update this feed"))
if validationErr := validator.ValidateFeedModification(h.store, userID, &feedModificationRequest); validationErr != nil {
json.BadRequest(w, r, validationErr.Error())
return
}
originalFeed, err = c.store.FeedByID(userID, feedID)
feedModificationRequest.Patch(originalFeed)
if err := h.store.UpdateFeed(originalFeed); err != nil {
json.ServerError(w, r, err)
return
}
originalFeed, err = h.store.FeedByID(userID, feedID)
if err != nil {
response.JSON().ServerError(errors.New("Unable to fetch this feed"))
json.ServerError(w, r, err)
return
}
response.JSON().Created(originalFeed)
json.Created(w, r, originalFeed)
}
// GetFeeds is the API handler that get all feeds that belongs to the given user.
func (c *Controller) GetFeeds(ctx *handler.Context, request *handler.Request, response *handler.Response) {
feeds, err := c.store.Feeds(ctx.UserID())
func (h *handler) markFeedAsRead(w http.ResponseWriter, r *http.Request) {
feedID := request.RouteInt64Param(r, "feedID")
userID := request.UserID(r)
feed, err := h.store.FeedByID(userID, feedID)
if err != nil {
response.JSON().ServerError(errors.New("Unable to fetch feeds from the database"))
return
}
response.JSON().Standard(feeds)
}
// Export is the API handler that incoves an OPML export.
func (c *Controller) Export(ctx *handler.Context, request *handler.Request, response *handler.Response) {
opmlHandler := opml.NewHandler(c.store)
opml, err := opmlHandler.Export(ctx.LoggedUser().ID)
if err != nil {
response.JSON().ServerError(errors.New("unable to export feeds to OPML"))
}
response.XML().Serve(opml)
}
// GetFeed is the API handler to get a feed.
func (c *Controller) GetFeed(ctx *handler.Context, request *handler.Request, response *handler.Response) {
userID := ctx.UserID()
feedID, err := request.IntegerParam("feedID")
if err != nil {
response.JSON().BadRequest(err)
return
}
feed, err := c.store.FeedByID(userID, feedID)
if err != nil {
response.JSON().ServerError(errors.New("Unable to fetch this feed"))
json.NotFound(w, r)
return
}
if feed == nil {
response.JSON().NotFound(errors.New("Feed not found"))
json.NotFound(w, r)
return
}
response.JSON().Standard(feed)
if err := h.store.MarkFeedAsRead(userID, feedID, time.Now()); err != nil {
json.ServerError(w, r, err)
return
}
json.NoContent(w, r)
}
// RemoveFeed is the API handler to remove a feed.
func (c *Controller) RemoveFeed(ctx *handler.Context, request *handler.Request, response *handler.Response) {
userID := ctx.UserID()
feedID, err := request.IntegerParam("feedID")
func (h *handler) getCategoryFeeds(w http.ResponseWriter, r *http.Request) {
userID := request.UserID(r)
categoryID := request.RouteInt64Param(r, "categoryID")
category, err := h.store.Category(userID, categoryID)
if err != nil {
response.JSON().BadRequest(err)
json.ServerError(w, r, err)
return
}
if !c.store.FeedExists(userID, feedID) {
response.JSON().NotFound(errors.New("Feed not found"))
if category == nil {
json.NotFound(w, r)
return
}
if err := c.store.RemoveFeed(userID, feedID); err != nil {
response.JSON().ServerError(errors.New("Unable to remove this feed"))
feeds, err := h.store.FeedsByCategoryWithCounters(userID, categoryID)
if err != nil {
json.ServerError(w, r, err)
return
}
response.JSON().NoContent()
json.OK(w, r, feeds)
}
func (h *handler) getFeeds(w http.ResponseWriter, r *http.Request) {
feeds, err := h.store.Feeds(request.UserID(r))
if err != nil {
json.ServerError(w, r, err)
return
}
json.OK(w, r, feeds)
}
func (h *handler) getFeed(w http.ResponseWriter, r *http.Request) {
feedID := request.RouteInt64Param(r, "feedID")
feed, err := h.store.FeedByID(request.UserID(r), feedID)
if err != nil {
json.ServerError(w, r, err)
return
}
if feed == nil {
json.NotFound(w, r)
return
}
json.OK(w, r, feed)
}
func (h *handler) removeFeed(w http.ResponseWriter, r *http.Request) {
feedID := request.RouteInt64Param(r, "feedID")
userID := request.UserID(r)
if !h.store.FeedExists(userID, feedID) {
json.NotFound(w, r)
return
}
if err := h.store.RemoveFeed(userID, feedID); err != nil {
json.ServerError(w, r, err)
return
}
json.NoContent(w, r)
}
+13 -18
View File
@@ -2,40 +2,35 @@
// Use of this source code is governed by the Apache 2.0
// license that can be found in the LICENSE file.
package api
package api // import "miniflux.app/api"
import (
"errors"
"net/http"
"github.com/miniflux/miniflux/http/handler"
"miniflux.app/http/request"
"miniflux.app/http/response/json"
)
// FeedIcon returns a feed icon.
func (c *Controller) FeedIcon(ctx *handler.Context, request *handler.Request, response *handler.Response) {
userID := ctx.UserID()
feedID, err := request.IntegerParam("feedID")
if err != nil {
response.JSON().BadRequest(err)
func (h *handler) feedIcon(w http.ResponseWriter, r *http.Request) {
feedID := request.RouteInt64Param(r, "feedID")
if !h.store.HasIcon(feedID) {
json.NotFound(w, r)
return
}
if !c.store.HasIcon(feedID) {
response.JSON().NotFound(errors.New("This feed doesn't have any icon"))
return
}
icon, err := c.store.IconByFeedID(userID, feedID)
icon, err := h.store.IconByFeedID(request.UserID(r), feedID)
if err != nil {
response.JSON().ServerError(errors.New("Unable to fetch feed icon"))
json.ServerError(w, r, err)
return
}
if icon == nil {
response.JSON().NotFound(errors.New("This feed doesn't have any icon"))
json.NotFound(w, r)
return
}
response.JSON().Standard(&feedIcon{
json.OK(w, r, &feedIconResponse{
ID: icon.ID,
MimeType: icon.MimeType,
Data: icon.DataURL(),
+128
View File
@@ -0,0 +1,128 @@
// Copyright 2018 Frédéric Guillot. All rights reserved.
// Use of this source code is governed by the Apache 2.0
// license that can be found in the LICENSE file.
package api // import "miniflux.app/api"
import (
"context"
"net/http"
"miniflux.app/http/request"
"miniflux.app/http/response/json"
"miniflux.app/logger"
"miniflux.app/storage"
)
type middleware struct {
store *storage.Storage
}
func newMiddleware(s *storage.Storage) *middleware {
return &middleware{s}
}
func (m *middleware) handleCORS(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Access-Control-Allow-Origin", "*")
w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS")
w.Header().Set("Access-Control-Allow-Headers", "X-Auth-Token")
if r.Method == http.MethodOptions {
w.WriteHeader(http.StatusOK)
return
}
next.ServeHTTP(w, r)
})
}
func (m *middleware) apiKeyAuth(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
clientIP := request.ClientIP(r)
token := r.Header.Get("X-Auth-Token")
if token == "" {
logger.Debug("[API][TokenAuth] [ClientIP=%s] No API Key provided, go to the next middleware", clientIP)
next.ServeHTTP(w, r)
return
}
user, err := m.store.UserByAPIKey(token)
if err != nil {
logger.Error("[API][TokenAuth] %v", err)
json.ServerError(w, r, err)
return
}
if user == nil {
logger.Error("[API][TokenAuth] [ClientIP=%s] No user found with the given API key", clientIP)
json.Unauthorized(w, r)
return
}
logger.Info("[API][TokenAuth] [ClientIP=%s] User authenticated: %s", clientIP, user.Username)
m.store.SetLastLogin(user.ID)
m.store.SetAPIKeyUsedTimestamp(user.ID, token)
ctx := r.Context()
ctx = context.WithValue(ctx, request.UserIDContextKey, user.ID)
ctx = context.WithValue(ctx, request.UserTimezoneContextKey, user.Timezone)
ctx = context.WithValue(ctx, request.IsAdminUserContextKey, user.IsAdmin)
ctx = context.WithValue(ctx, request.IsAuthenticatedContextKey, true)
next.ServeHTTP(w, r.WithContext(ctx))
})
}
func (m *middleware) basicAuth(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if request.IsAuthenticated(r) {
next.ServeHTTP(w, r)
return
}
w.Header().Set("WWW-Authenticate", `Basic realm="Restricted"`)
clientIP := request.ClientIP(r)
username, password, authOK := r.BasicAuth()
if !authOK {
logger.Debug("[API][BasicAuth] [ClientIP=%s] No authentication headers sent", clientIP)
json.Unauthorized(w, r)
return
}
if username == "" || password == "" {
logger.Error("[API][BasicAuth] [ClientIP=%s] Empty username or password", clientIP)
json.Unauthorized(w, r)
return
}
if err := m.store.CheckPassword(username, password); err != nil {
logger.Error("[API][BasicAuth] [ClientIP=%s] Invalid username or password: %s", clientIP, username)
json.Unauthorized(w, r)
return
}
user, err := m.store.UserByUsername(username)
if err != nil {
logger.Error("[API][BasicAuth] %v", err)
json.ServerError(w, r, err)
return
}
if user == nil {
logger.Error("[API][BasicAuth] [ClientIP=%s] User not found: %s", clientIP, username)
json.Unauthorized(w, r)
return
}
logger.Info("[API][BasicAuth] [ClientIP=%s] User authenticated: %s", clientIP, username)
m.store.SetLastLogin(user.ID)
ctx := r.Context()
ctx = context.WithValue(ctx, request.UserIDContextKey, user.ID)
ctx = context.WithValue(ctx, request.UserTimezoneContextKey, user.Timezone)
ctx = context.WithValue(ctx, request.IsAdminUserContextKey, user.IsAdmin)
ctx = context.WithValue(ctx, request.IsAuthenticatedContextKey, true)
next.ServeHTTP(w, r.WithContext(ctx))
})
}
+37
View File
@@ -0,0 +1,37 @@
// Copyright 2018 Frédéric Guillot. All rights reserved.
// Use of this source code is governed by the Apache 2.0
// license that can be found in the LICENSE file.
package api // import "miniflux.app/api"
import (
"net/http"
"miniflux.app/http/request"
"miniflux.app/http/response/json"
"miniflux.app/http/response/xml"
"miniflux.app/reader/opml"
)
func (h *handler) exportFeeds(w http.ResponseWriter, r *http.Request) {
opmlHandler := opml.NewHandler(h.store)
opml, err := opmlHandler.Export(request.UserID(r))
if err != nil {
json.ServerError(w, r, err)
return
}
xml.OK(w, r, opml)
}
func (h *handler) importFeeds(w http.ResponseWriter, r *http.Request) {
opmlHandler := opml.NewHandler(h.store)
err := opmlHandler.Import(request.UserID(r), r.Body)
defer r.Body.Close()
if err != nil {
json.ServerError(w, r, err)
return
}
json.Created(w, r, map[string]string{"message": "Feeds imported successfully"})
}
+5 -82
View File
@@ -2,17 +2,13 @@
// Use of this source code is governed by the Apache 2.0
// license that can be found in the LICENSE file.
package api
package api // import "miniflux.app/api"
import (
"encoding/json"
"fmt"
"io"
"github.com/miniflux/miniflux/model"
"miniflux.app/model"
)
type feedIcon struct {
type feedIconResponse struct {
ID int64 `json:"id"`
MimeType string `json:"mime_type"`
Data string `json:"data"`
@@ -23,79 +19,6 @@ type entriesResponse struct {
Entries model.Entries `json:"entries"`
}
func decodeUserPayload(data io.Reader) (*model.User, error) {
var user model.User
decoder := json.NewDecoder(data)
if err := decoder.Decode(&user); err != nil {
return nil, fmt.Errorf("Unable to decode user JSON object: %v", err)
}
return &user, nil
}
func decodeURLPayload(data io.Reader) (string, error) {
type payload struct {
URL string `json:"url"`
}
var p payload
decoder := json.NewDecoder(data)
if err := decoder.Decode(&p); err != nil {
return "", fmt.Errorf("invalid JSON payload: %v", err)
}
return p.URL, nil
}
func decodeEntryStatusPayload(data io.Reader) ([]int64, string, error) {
type payload struct {
EntryIDs []int64 `json:"entry_ids"`
Status string `json:"status"`
}
var p payload
decoder := json.NewDecoder(data)
if err := decoder.Decode(&p); err != nil {
return nil, "", fmt.Errorf("invalid JSON payload: %v", err)
}
return p.EntryIDs, p.Status, nil
}
func decodeFeedCreationPayload(data io.Reader) (string, int64, error) {
type payload struct {
FeedURL string `json:"feed_url"`
CategoryID int64 `json:"category_id"`
}
var p payload
decoder := json.NewDecoder(data)
if err := decoder.Decode(&p); err != nil {
return "", 0, fmt.Errorf("invalid JSON payload: %v", err)
}
return p.FeedURL, p.CategoryID, nil
}
func decodeFeedModificationPayload(data io.Reader) (*model.Feed, error) {
var feed model.Feed
decoder := json.NewDecoder(data)
if err := decoder.Decode(&feed); err != nil {
return nil, fmt.Errorf("Unable to decode feed JSON object: %v", err)
}
return &feed, nil
}
func decodeCategoryPayload(data io.Reader) (*model.Category, error) {
var category model.Category
decoder := json.NewDecoder(data)
if err := decoder.Decode(&category); err != nil {
return nil, fmt.Errorf("Unable to decode category JSON object: %v", err)
}
return &category, nil
type feedCreationResponse struct {
FeedID int64 `json:"feed_id"`
}
+27 -15
View File
@@ -2,34 +2,46 @@
// Use of this source code is governed by the Apache 2.0
// license that can be found in the LICENSE file.
package api
package api // import "miniflux.app/api"
import (
"errors"
"fmt"
json_parser "encoding/json"
"net/http"
"github.com/miniflux/miniflux/http/handler"
"github.com/miniflux/miniflux/reader/subscription"
"miniflux.app/http/response/json"
"miniflux.app/model"
"miniflux.app/reader/subscription"
"miniflux.app/validator"
)
// GetSubscriptions is the API handler to find subscriptions.
func (c *Controller) GetSubscriptions(ctx *handler.Context, request *handler.Request, response *handler.Response) {
websiteURL, err := decodeURLPayload(request.Body())
if err != nil {
response.JSON().BadRequest(err)
func (h *handler) discoverSubscriptions(w http.ResponseWriter, r *http.Request) {
var subscriptionDiscoveryRequest model.SubscriptionDiscoveryRequest
if err := json_parser.NewDecoder(r.Body).Decode(&subscriptionDiscoveryRequest); err != nil {
json.BadRequest(w, r, err)
return
}
subscriptions, err := subscription.FindSubscriptions(websiteURL)
if err != nil {
response.JSON().ServerError(errors.New("Unable to discover subscriptions"))
if validationErr := validator.ValidateSubscriptionDiscovery(&subscriptionDiscoveryRequest); validationErr != nil {
json.BadRequest(w, r, validationErr.Error())
return
}
subscriptions, finderErr := subscription.FindSubscriptions(
subscriptionDiscoveryRequest.URL,
subscriptionDiscoveryRequest.UserAgent,
subscriptionDiscoveryRequest.Username,
subscriptionDiscoveryRequest.Password,
subscriptionDiscoveryRequest.FetchViaProxy,
)
if finderErr != nil {
json.ServerError(w, r, finderErr)
return
}
if subscriptions == nil {
response.JSON().NotFound(fmt.Errorf("No subscription found"))
json.NotFound(w, r)
return
}
response.JSON().Standard(subscriptions)
json.OK(w, r, subscriptions)
}
+115 -98
View File
@@ -2,186 +2,203 @@
// Use of this source code is governed by the Apache 2.0
// license that can be found in the LICENSE file.
package api
package api // import "miniflux.app/api"
import (
json_parser "encoding/json"
"errors"
"net/http"
"github.com/miniflux/miniflux/http/handler"
"miniflux.app/http/request"
"miniflux.app/http/response/json"
"miniflux.app/model"
"miniflux.app/validator"
)
// CreateUser is the API handler to create a new user.
func (c *Controller) CreateUser(ctx *handler.Context, request *handler.Request, response *handler.Response) {
if !ctx.IsAdminUser() {
response.JSON().Forbidden()
return
}
user, err := decodeUserPayload(request.Body())
func (h *handler) currentUser(w http.ResponseWriter, r *http.Request) {
user, err := h.store.UserByID(request.UserID(r))
if err != nil {
response.JSON().BadRequest(err)
json.ServerError(w, r, err)
return
}
if err := user.ValidateUserCreation(); err != nil {
response.JSON().BadRequest(err)
return
}
if c.store.UserExists(user.Username) {
response.JSON().BadRequest(errors.New("This user already exists"))
return
}
err = c.store.CreateUser(user)
if err != nil {
response.JSON().ServerError(errors.New("Unable to create this user"))
return
}
user.Password = ""
response.JSON().Created(user)
json.OK(w, r, user)
}
// UpdateUser is the API handler to update the given user.
func (c *Controller) UpdateUser(ctx *handler.Context, request *handler.Request, response *handler.Response) {
if !ctx.IsAdminUser() {
response.JSON().Forbidden()
func (h *handler) createUser(w http.ResponseWriter, r *http.Request) {
if !request.IsAdminUser(r) {
json.Forbidden(w, r)
return
}
userID, err := request.IntegerParam("userID")
var userCreationRequest model.UserCreationRequest
if err := json_parser.NewDecoder(r.Body).Decode(&userCreationRequest); err != nil {
json.BadRequest(w, r, err)
return
}
if validationErr := validator.ValidateUserCreationWithPassword(h.store, &userCreationRequest); validationErr != nil {
json.BadRequest(w, r, validationErr.Error())
return
}
user, err := h.store.CreateUser(&userCreationRequest)
if err != nil {
response.JSON().BadRequest(err)
json.ServerError(w, r, err)
return
}
user, err := decodeUserPayload(request.Body())
json.Created(w, r, user)
}
func (h *handler) updateUser(w http.ResponseWriter, r *http.Request) {
userID := request.RouteInt64Param(r, "userID")
var userModificationRequest model.UserModificationRequest
if err := json_parser.NewDecoder(r.Body).Decode(&userModificationRequest); err != nil {
json.BadRequest(w, r, err)
return
}
originalUser, err := h.store.UserByID(userID)
if err != nil {
response.JSON().BadRequest(err)
return
}
if err := user.ValidateUserModification(); err != nil {
response.JSON().BadRequest(err)
return
}
originalUser, err := c.store.UserByID(userID)
if err != nil {
response.JSON().BadRequest(errors.New("Unable to fetch this user from the database"))
json.ServerError(w, r, err)
return
}
if originalUser == nil {
response.JSON().NotFound(errors.New("User not found"))
json.NotFound(w, r)
return
}
originalUser.Merge(user)
if err = c.store.UpdateUser(originalUser); err != nil {
response.JSON().ServerError(errors.New("Unable to update this user"))
if !request.IsAdminUser(r) {
if originalUser.ID != request.UserID(r) {
json.Forbidden(w, r)
return
}
if userModificationRequest.IsAdmin != nil && *userModificationRequest.IsAdmin {
json.BadRequest(w, r, errors.New("Only administrators can change permissions of standard users"))
return
}
}
if validationErr := validator.ValidateUserModification(h.store, originalUser.ID, &userModificationRequest); validationErr != nil {
json.BadRequest(w, r, validationErr.Error())
return
}
response.JSON().Created(originalUser)
userModificationRequest.Patch(originalUser)
if err = h.store.UpdateUser(originalUser); err != nil {
json.ServerError(w, r, err)
return
}
json.Created(w, r, originalUser)
}
// Users is the API handler to get the list of users.
func (c *Controller) Users(ctx *handler.Context, request *handler.Request, response *handler.Response) {
if !ctx.IsAdminUser() {
response.JSON().Forbidden()
func (h *handler) markUserAsRead(w http.ResponseWriter, r *http.Request) {
userID := request.RouteInt64Param(r, "userID")
if userID != request.UserID(r) {
json.Forbidden(w, r)
return
}
users, err := c.store.Users()
if err != nil {
response.JSON().ServerError(errors.New("Unable to fetch the list of users"))
if _, err := h.store.UserByID(userID); err != nil {
json.NotFound(w, r)
return
}
users.UseTimezone(ctx.UserTimezone())
response.JSON().Standard(users)
if err := h.store.MarkAllAsRead(userID); err != nil {
json.ServerError(w, r, err)
return
}
json.NoContent(w, r)
}
// UserByID is the API handler to fetch the given user by the ID.
func (c *Controller) UserByID(ctx *handler.Context, request *handler.Request, response *handler.Response) {
if !ctx.IsAdminUser() {
response.JSON().Forbidden()
func (h *handler) users(w http.ResponseWriter, r *http.Request) {
if !request.IsAdminUser(r) {
json.Forbidden(w, r)
return
}
userID, err := request.IntegerParam("userID")
users, err := h.store.Users()
if err != nil {
response.JSON().BadRequest(err)
json.ServerError(w, r, err)
return
}
user, err := c.store.UserByID(userID)
users.UseTimezone(request.UserTimezone(r))
json.OK(w, r, users)
}
func (h *handler) userByID(w http.ResponseWriter, r *http.Request) {
if !request.IsAdminUser(r) {
json.Forbidden(w, r)
return
}
userID := request.RouteInt64Param(r, "userID")
user, err := h.store.UserByID(userID)
if err != nil {
response.JSON().BadRequest(errors.New("Unable to fetch this user from the database"))
json.BadRequest(w, r, errors.New("Unable to fetch this user from the database"))
return
}
if user == nil {
response.JSON().NotFound(errors.New("User not found"))
json.NotFound(w, r)
return
}
user.UseTimezone(ctx.UserTimezone())
response.JSON().Standard(user)
user.UseTimezone(request.UserTimezone(r))
json.OK(w, r, user)
}
// UserByUsername is the API handler to fetch the given user by the username.
func (c *Controller) UserByUsername(ctx *handler.Context, request *handler.Request, response *handler.Response) {
if !ctx.IsAdminUser() {
response.JSON().Forbidden()
func (h *handler) userByUsername(w http.ResponseWriter, r *http.Request) {
if !request.IsAdminUser(r) {
json.Forbidden(w, r)
return
}
username := request.StringParam("username", "")
user, err := c.store.UserByUsername(username)
username := request.RouteStringParam(r, "username")
user, err := h.store.UserByUsername(username)
if err != nil {
response.JSON().BadRequest(errors.New("Unable to fetch this user from the database"))
json.BadRequest(w, r, errors.New("Unable to fetch this user from the database"))
return
}
if user == nil {
response.JSON().NotFound(errors.New("User not found"))
json.NotFound(w, r)
return
}
response.JSON().Standard(user)
json.OK(w, r, user)
}
// RemoveUser is the API handler to remove an existing user.
func (c *Controller) RemoveUser(ctx *handler.Context, request *handler.Request, response *handler.Response) {
if !ctx.IsAdminUser() {
response.JSON().Forbidden()
func (h *handler) removeUser(w http.ResponseWriter, r *http.Request) {
if !request.IsAdminUser(r) {
json.Forbidden(w, r)
return
}
userID, err := request.IntegerParam("userID")
userID := request.RouteInt64Param(r, "userID")
user, err := h.store.UserByID(userID)
if err != nil {
response.JSON().BadRequest(err)
return
}
user, err := c.store.UserByID(userID)
if err != nil {
response.JSON().ServerError(errors.New("Unable to fetch this user from the database"))
json.ServerError(w, r, err)
return
}
if user == nil {
response.JSON().NotFound(errors.New("User not found"))
json.NotFound(w, r)
return
}
if err := c.store.RemoveUser(user.ID); err != nil {
response.JSON().BadRequest(errors.New("Unable to remove this user from the database"))
if user.ID == request.UserID(r) {
json.BadRequest(w, r, errors.New("You cannot remove yourself"))
return
}
response.JSON().NoContent()
h.store.RemoveUserAsync(user.ID)
json.NoContent(w, r)
}
+13 -3
View File
@@ -2,7 +2,7 @@
// Use of this source code is governed by the Apache 2.0
// license that can be found in the LICENSE file.
package cli
package cli // import "miniflux.app/cli"
import (
"bufio"
@@ -14,13 +14,23 @@ import (
)
func askCredentials() (string, string) {
reader := bufio.NewReader(os.Stdin)
fd := int(os.Stdin.Fd())
if !terminal.IsTerminal(fd) {
fmt.Fprintf(os.Stderr, "This is not a terminal, exiting.\n")
os.Exit(1)
}
fmt.Print("Enter Username: ")
reader := bufio.NewReader(os.Stdin)
username, _ := reader.ReadString('\n')
fmt.Print("Enter Password: ")
bytePassword, _ := terminal.ReadPassword(0)
state, _ := terminal.GetState(fd)
defer terminal.Restore(fd, state)
bytePassword, _ := terminal.ReadPassword(fd)
fmt.Printf("\n")
return strings.TrimSpace(username), strings.TrimSpace(string(bytePassword))
+113 -28
View File
@@ -2,71 +2,156 @@
// Use of this source code is governed by the Apache 2.0
// license that can be found in the LICENSE file.
package cli
package cli // import "miniflux.app/cli"
import (
"flag"
"fmt"
"github.com/miniflux/miniflux/logger"
"miniflux.app/config"
"miniflux.app/database"
"miniflux.app/logger"
"miniflux.app/storage"
"miniflux.app/version"
)
"github.com/miniflux/miniflux/config"
"github.com/miniflux/miniflux/daemon"
"github.com/miniflux/miniflux/storage"
"github.com/miniflux/miniflux/version"
const (
flagInfoHelp = "Show application information"
flagVersionHelp = "Show application version"
flagMigrateHelp = "Run SQL migrations"
flagFlushSessionsHelp = "Flush all sessions (disconnect users)"
flagCreateAdminHelp = "Create admin user"
flagResetPasswordHelp = "Reset user password"
flagResetFeedErrorsHelp = "Clear all feed errors for all users"
flagDebugModeHelp = "Show debug logs"
flagConfigFileHelp = "Load configuration file"
flagConfigDumpHelp = "Print parsed configuration values"
)
// Parse parses command line arguments.
func Parse() {
flagInfo := flag.Bool("info", false, "Show application information")
flagVersion := flag.Bool("version", false, "Show application version")
flagMigrate := flag.Bool("migrate", false, "Migrate database schema")
flagFlushSessions := flag.Bool("flush-sessions", false, "Flush all sessions (disconnect users)")
flagCreateAdmin := flag.Bool("create-admin", false, "Create admin user")
flagResetPassword := flag.Bool("reset-password", false, "Reset user password")
flagDebugMode := flag.Bool("debug", false, "Enable debug mode (more verbose output)")
var (
err error
flagInfo bool
flagVersion bool
flagMigrate bool
flagFlushSessions bool
flagCreateAdmin bool
flagResetPassword bool
flagResetFeedErrors bool
flagDebugMode bool
flagConfigFile string
flagConfigDump bool
)
flag.BoolVar(&flagInfo, "info", false, flagInfoHelp)
flag.BoolVar(&flagInfo, "i", false, flagInfoHelp)
flag.BoolVar(&flagVersion, "version", false, flagVersionHelp)
flag.BoolVar(&flagVersion, "v", false, flagVersionHelp)
flag.BoolVar(&flagMigrate, "migrate", false, flagMigrateHelp)
flag.BoolVar(&flagFlushSessions, "flush-sessions", false, flagFlushSessionsHelp)
flag.BoolVar(&flagCreateAdmin, "create-admin", false, flagCreateAdminHelp)
flag.BoolVar(&flagResetPassword, "reset-password", false, flagResetPasswordHelp)
flag.BoolVar(&flagResetFeedErrors, "reset-feed-errors", false, flagResetFeedErrorsHelp)
flag.BoolVar(&flagDebugMode, "debug", false, flagDebugModeHelp)
flag.StringVar(&flagConfigFile, "config-file", "", flagConfigFileHelp)
flag.StringVar(&flagConfigFile, "c", "", flagConfigFileHelp)
flag.BoolVar(&flagConfigDump, "config-dump", false, flagConfigDumpHelp)
flag.Parse()
cfg := config.NewConfig()
cfg := config.NewParser()
if *flagDebugMode || cfg.HasDebugMode() {
if flagConfigFile != "" {
config.Opts, err = cfg.ParseFile(flagConfigFile)
if err != nil {
logger.Fatal("%v", err)
}
}
config.Opts, err = cfg.ParseEnvironmentVariables()
if err != nil {
logger.Fatal("%v", err)
}
if flagConfigDump {
fmt.Print(config.Opts)
return
}
if config.Opts.LogDateTime() {
logger.EnableDateTime()
}
if flagDebugMode || config.Opts.HasDebugMode() {
logger.EnableDebug()
}
store := storage.NewStorage(
cfg.DatabaseURL(),
cfg.DatabaseMaxConnections(),
)
if *flagInfo {
if flagInfo {
info()
return
}
if *flagVersion {
if flagVersion {
fmt.Println(version.Version)
return
}
if *flagMigrate {
store.Migrate()
if config.Opts.IsDefaultDatabaseURL() {
logger.Info("The default value for DATABASE_URL is used")
}
db, err := database.NewConnectionPool(
config.Opts.DatabaseURL(),
config.Opts.DatabaseMinConns(),
config.Opts.DatabaseMaxConns(),
)
if err != nil {
logger.Fatal("Unable to connect to the database: %v", err)
}
defer db.Close()
if flagMigrate {
database.Migrate(db)
return
}
if *flagFlushSessions {
store := storage.NewStorage(db)
if flagResetFeedErrors {
store.ResetFeedErrors()
return
}
if flagFlushSessions {
flushSessions(store)
return
}
if *flagCreateAdmin {
if flagCreateAdmin {
createAdmin(store)
return
}
if *flagResetPassword {
if flagResetPassword {
resetPassword(store)
return
}
daemon.Run(cfg, store)
// Run migrations and start the deamon.
if config.Opts.RunMigrations() {
if err := database.Migrate(db); err != nil {
logger.Fatal(`%v`, err)
}
}
if err := database.IsSchemaUpToDate(db); err != nil {
logger.Fatal(`You must run the SQL migrations, %v`, err)
}
// Create admin user and start the deamon.
if config.Opts.CreateAdmin() {
createAdmin(store)
}
startDaemon(store)
}
+23 -14
View File
@@ -2,33 +2,42 @@
// Use of this source code is governed by the Apache 2.0
// license that can be found in the LICENSE file.
package cli
package cli // import "miniflux.app/cli"
import (
"fmt"
"os"
"github.com/miniflux/miniflux/model"
"github.com/miniflux/miniflux/storage"
"miniflux.app/config"
"miniflux.app/logger"
"miniflux.app/model"
"miniflux.app/storage"
"miniflux.app/validator"
)
func createAdmin(store *storage.Storage) {
user := model.NewUser()
user.Username = os.Getenv("ADMIN_USERNAME")
user.Password = os.Getenv("ADMIN_PASSWORD")
user.IsAdmin = true
if user.Username == "" || user.Password == "" {
user.Username, user.Password = askCredentials()
userCreationRequest := &model.UserCreationRequest{
Username: config.Opts.AdminUsername(),
Password: config.Opts.AdminPassword(),
IsAdmin: true,
}
if err := user.ValidateUserCreation(); err != nil {
fmt.Println(err)
if userCreationRequest.Username == "" || userCreationRequest.Password == "" {
userCreationRequest.Username, userCreationRequest.Password = askCredentials()
}
if store.UserExists(userCreationRequest.Username) {
logger.Info(`User %q already exists, skipping creation`, userCreationRequest.Username)
return
}
if validationErr := validator.ValidateUserCreationWithPassword(store, userCreationRequest); validationErr != nil {
fmt.Fprintf(os.Stderr, "%s\n", validationErr)
os.Exit(1)
}
if err := store.CreateUser(user); err != nil {
fmt.Println(err)
if _, err := store.CreateUser(userCreationRequest); err != nil {
fmt.Fprintf(os.Stderr, "%v\n", err)
os.Exit(1)
}
}
+57
View File
@@ -0,0 +1,57 @@
// Copyright 2018 Frédéric Guillot. All rights reserved.
// Use of this source code is governed by the Apache 2.0
// license that can be found in the LICENSE file.
package cli // import "miniflux.app/cli"
import (
"context"
"net/http"
"os"
"os/signal"
"syscall"
"time"
"miniflux.app/config"
"miniflux.app/logger"
"miniflux.app/metric"
"miniflux.app/service/httpd"
"miniflux.app/service/scheduler"
"miniflux.app/storage"
"miniflux.app/worker"
)
func startDaemon(store *storage.Storage) {
logger.Info("Starting Miniflux...")
stop := make(chan os.Signal, 1)
signal.Notify(stop, os.Interrupt)
signal.Notify(stop, syscall.SIGTERM)
pool := worker.NewPool(store, config.Opts.WorkerPoolSize())
if config.Opts.HasSchedulerService() && !config.Opts.HasMaintenanceMode() {
scheduler.Serve(store, pool)
}
var httpServer *http.Server
if config.Opts.HasHTTPService() {
httpServer = httpd.Serve(store, pool)
}
if config.Opts.HasMetricsCollector() {
collector := metric.NewCollector(store, config.Opts.MetricsRefreshInterval())
go collector.GatherStorageMetrics()
}
<-stop
logger.Info("Shutting down the process...")
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
if httpServer != nil {
httpServer.Shutdown(ctx)
}
logger.Info("Process gracefully stopped")
}
+3 -3
View File
@@ -1,10 +1,10 @@
// Copyright 2018 Frédéric Guillot. All rights reserved.
// Use of this source code is governed by the MIT license
// that can be found in the LICENSE file.
// Use of this source code is governed by the Apache 2.0
// license that can be found in the LICENSE file.
/*
Package cli implements command line arguments for Miniflux application.
*/
package cli
package cli // import "miniflux.app/cli"
+3 -3
View File
@@ -2,19 +2,19 @@
// Use of this source code is governed by the Apache 2.0
// license that can be found in the LICENSE file.
package cli
package cli // import "miniflux.app/cli"
import (
"fmt"
"os"
"github.com/miniflux/miniflux/storage"
"miniflux.app/storage"
)
func flushSessions(store *storage.Storage) {
fmt.Println("Flushing all sessions (disconnect users)")
if err := store.FlushAllSessions(); err != nil {
fmt.Println(err)
fmt.Fprintf(os.Stderr, "%v\n", err)
os.Exit(1)
}
}
+6 -2
View File
@@ -2,17 +2,21 @@
// Use of this source code is governed by the Apache 2.0
// license that can be found in the LICENSE file.
package cli
package cli // import "miniflux.app/cli"
import (
"fmt"
"runtime"
"github.com/miniflux/miniflux/version"
"miniflux.app/version"
)
func info() {
fmt.Println("Version:", version.Version)
fmt.Println("Commit:", version.Commit)
fmt.Println("Build Date:", version.BuildDate)
fmt.Println("Go Version:", runtime.Version())
fmt.Println("Compiler:", runtime.Compiler)
fmt.Println("Arch:", runtime.GOARCH)
fmt.Println("OS:", runtime.GOOS)
}
+12 -8
View File
@@ -2,36 +2,40 @@
// Use of this source code is governed by the Apache 2.0
// license that can be found in the LICENSE file.
package cli
package cli // import "miniflux.app/cli"
import (
"fmt"
"os"
"github.com/miniflux/miniflux/storage"
"miniflux.app/model"
"miniflux.app/storage"
"miniflux.app/validator"
)
func resetPassword(store *storage.Storage) {
username, password := askCredentials()
user, err := store.UserByUsername(username)
if err != nil {
fmt.Println(err)
fmt.Fprintf(os.Stderr, "%v\n", err)
os.Exit(1)
}
if user == nil {
fmt.Println("User not found!")
fmt.Fprintf(os.Stderr, "User not found!\n")
os.Exit(1)
}
user.Password = password
if err := user.ValidatePassword(); err != nil {
fmt.Println(err)
userModificationRequest := &model.UserModificationRequest{
Password: &password,
}
if validationErr := validator.ValidateUserModification(store, user.ID, userModificationRequest); validationErr != nil {
fmt.Fprintf(os.Stderr, "%s\n", validationErr)
os.Exit(1)
}
if err := store.UpdateUser(user); err != nil {
fmt.Println(err)
fmt.Fprintf(os.Stderr, "%v\n", err)
os.Exit(1)
}
+56
View File
@@ -0,0 +1,56 @@
Miniflux API Client
===================
[![PkgGoDev](https://pkg.go.dev/badge/miniflux.app/client)](https://pkg.go.dev/miniflux.app/client)
Client library for Miniflux REST API.
Installation
------------
```bash
go get -u miniflux.app/client
```
Example
-------
```go
package main
import (
"fmt"
"io/ioutil"
miniflux "miniflux.app/client"
)
func main() {
// Authentication with username/password:
client := miniflux.New("https://api.example.org", "admin", "secret")
// Authentication with an API Key:
client := miniflux.New("https://api.example.org", "my-secret-token")
// Fetch all feeds.
feeds, err := client.Feeds()
if err != nil {
fmt.Println(err)
return
}
fmt.Println(feeds)
// Backup your feeds to an OPML file.
opml, err := client.Export()
if err != nil {
fmt.Println(err)
return
}
err = ioutil.WriteFile("opml.xml", opml, 0644)
if err != nil {
fmt.Println(err)
return
}
}
```
@@ -1,22 +1,48 @@
// Copyright 2017 Frédéric Guillot. All rights reserved.
// Use of this source code is governed by the MIT license
// that can be found in the LICENSE file.
// Copyright 2018 Frédéric Guillot. All rights reserved.
// Use of this source code is governed by the Apache 2.0
// license that can be found in the LICENSE file.
package miniflux
package client // import "miniflux.app/client"
import (
"encoding/json"
"fmt"
"io"
"io/ioutil"
"net/url"
"strconv"
)
// Client represents a Miniflux client.
// Client holds API procedure calls.
type Client struct {
request *request
}
// New returns a new Miniflux client.
func New(endpoint string, credentials ...string) *Client {
if len(credentials) == 2 {
return &Client{request: &request{endpoint: endpoint, username: credentials[0], password: credentials[1]}}
}
return &Client{request: &request{endpoint: endpoint, apiKey: credentials[0]}}
}
// Me returns the logged user information.
func (c *Client) Me() (*User, error) {
body, err := c.request.Get("/v1/me")
if err != nil {
return nil, err
}
defer body.Close()
var user *User
decoder := json.NewDecoder(body)
if err := decoder.Decode(&user); err != nil {
return nil, fmt.Errorf("miniflux: json error (%v)", err)
}
return user, nil
}
// Users returns all users.
func (c *Client) Users() (Users, error) {
body, err := c.request.Get("/v1/users")
@@ -70,7 +96,11 @@ func (c *Client) UserByUsername(username string) (*User, error) {
// CreateUser creates a new user in the system.
func (c *Client) CreateUser(username, password string, isAdmin bool) (*User, error) {
body, err := c.request.Post("/v1/users", &User{Username: username, Password: password, IsAdmin: isAdmin})
body, err := c.request.Post("/v1/users", &UserCreationRequest{
Username: username,
Password: password,
IsAdmin: isAdmin,
})
if err != nil {
return nil, err
}
@@ -86,8 +116,8 @@ func (c *Client) CreateUser(username, password string, isAdmin bool) (*User, err
}
// UpdateUser updates a user in the system.
func (c *Client) UpdateUser(user *User) (*User, error) {
body, err := c.request.Put(fmt.Sprintf("/v1/users/%d", user.ID), user)
func (c *Client) UpdateUser(userID int64, userChanges *UserModificationRequest) (*User, error) {
body, err := c.request.Put(fmt.Sprintf("/v1/users/%d", userID), userChanges)
if err != nil {
return nil, err
}
@@ -104,12 +134,13 @@ func (c *Client) UpdateUser(user *User) (*User, error) {
// DeleteUser removes a user from the system.
func (c *Client) DeleteUser(userID int64) error {
body, err := c.request.Delete(fmt.Sprintf("/v1/users/%d", userID))
if err != nil {
return err
}
body.Close()
return nil
return c.request.Delete(fmt.Sprintf("/v1/users/%d", userID))
}
// MarkAllAsRead marks all unread entries as read for a given user.
func (c *Client) MarkAllAsRead(userID int64) error {
_, err := c.request.Put(fmt.Sprintf("/v1/users/%d/mark-all-as-read", userID), nil)
return err
}
// Discover try to find subscriptions from a website.
@@ -186,15 +217,32 @@ func (c *Client) UpdateCategory(categoryID int64, title string) (*Category, erro
return category, nil
}
// DeleteCategory removes a category.
func (c *Client) DeleteCategory(categoryID int64) error {
body, err := c.request.Delete(fmt.Sprintf("/v1/categories/%d", categoryID))
// MarkCategoryAsRead marks all unread entries in a category as read.
func (c *Client) MarkCategoryAsRead(categoryID int64) error {
_, err := c.request.Put(fmt.Sprintf("/v1/categories/%d/mark-all-as-read", categoryID), nil)
return err
}
// CategoryFeeds gets feeds of a cateogry.
func (c *Client) CategoryFeeds(categoryID int64) (Feeds, error) {
body, err := c.request.Get(fmt.Sprintf("/v1/categories/%d/feeds", categoryID))
if err != nil {
return err
return nil, err
}
defer body.Close()
return nil
var feeds Feeds
decoder := json.NewDecoder(body)
if err := decoder.Decode(&feeds); err != nil {
return nil, fmt.Errorf("miniflux: response error (%v)", err)
}
return feeds, nil
}
// DeleteCategory removes a category.
func (c *Client) DeleteCategory(categoryID int64) error {
return c.request.Delete(fmt.Sprintf("/v1/categories/%d", categoryID))
}
// Feeds gets all feeds.
@@ -230,6 +278,12 @@ func (c *Client) Export() ([]byte, error) {
return opml, nil
}
// Import imports an OPML file.
func (c *Client) Import(f io.ReadCloser) error {
_, err := c.request.PostFile("/v1/import", f)
return err
}
// Feed gets a feed.
func (c *Client) Feed(feedID int64) (*Feed, error) {
body, err := c.request.Get(fmt.Sprintf("/v1/feeds/%d", feedID))
@@ -248,11 +302,8 @@ func (c *Client) Feed(feedID int64) (*Feed, error) {
}
// CreateFeed creates a new feed.
func (c *Client) CreateFeed(url string, categoryID int64) (int64, error) {
body, err := c.request.Post("/v1/feeds", map[string]interface{}{
"feed_url": url,
"category_id": categoryID,
})
func (c *Client) CreateFeed(feedCreationRequest *FeedCreationRequest) (int64, error) {
body, err := c.request.Post("/v1/feeds", feedCreationRequest)
if err != nil {
return 0, err
}
@@ -272,40 +323,42 @@ func (c *Client) CreateFeed(url string, categoryID int64) (int64, error) {
}
// UpdateFeed updates a feed.
func (c *Client) UpdateFeed(feed *Feed) (*Feed, error) {
body, err := c.request.Put(fmt.Sprintf("/v1/feeds/%d", feed.ID), feed)
func (c *Client) UpdateFeed(feedID int64, feedChanges *FeedModificationRequest) (*Feed, error) {
body, err := c.request.Put(fmt.Sprintf("/v1/feeds/%d", feedID), feedChanges)
if err != nil {
return nil, err
}
defer body.Close()
var f *Feed
decoder := json.NewDecoder(body)
if err := decoder.Decode(&f); err != nil {
if err := json.NewDecoder(body).Decode(&f); err != nil {
return nil, fmt.Errorf("miniflux: response error (%v)", err)
}
return f, nil
}
// RefreshFeed refresh a feed.
// MarkFeedAsRead marks all unread entries of the feed as read.
func (c *Client) MarkFeedAsRead(feedID int64) error {
_, err := c.request.Put(fmt.Sprintf("/v1/feeds/%d/mark-all-as-read", feedID), nil)
return err
}
// RefreshAllFeeds refreshes all feeds.
func (c *Client) RefreshAllFeeds() error {
_, err := c.request.Put(fmt.Sprintf("/v1/feeds/refresh"), nil)
return err
}
// RefreshFeed refreshes a feed.
func (c *Client) RefreshFeed(feedID int64) error {
body, err := c.request.Put(fmt.Sprintf("/v1/feeds/%d/refresh", feedID), nil)
if err != nil {
return err
}
body.Close()
return nil
_, err := c.request.Put(fmt.Sprintf("/v1/feeds/%d/refresh", feedID), nil)
return err
}
// DeleteFeed removes a feed.
func (c *Client) DeleteFeed(feedID int64) error {
body, err := c.request.Delete(fmt.Sprintf("/v1/feeds/%d", feedID))
if err != nil {
return err
}
body.Close()
return nil
return c.request.Delete(fmt.Sprintf("/v1/feeds/%d", feedID))
}
// FeedIcon gets a feed icon.
@@ -342,6 +395,23 @@ func (c *Client) FeedEntry(feedID, entryID int64) (*Entry, error) {
return entry, nil
}
// CategoryEntry gets a single category entry.
func (c *Client) CategoryEntry(categoryID, entryID int64) (*Entry, error) {
body, err := c.request.Get(fmt.Sprintf("/v1/categories/%d/entries/%d", categoryID, entryID))
if err != nil {
return nil, err
}
defer body.Close()
var entry *Entry
decoder := json.NewDecoder(body)
if err := decoder.Decode(&entry); err != nil {
return nil, fmt.Errorf("miniflux: response error (%v)", err)
}
return entry, nil
}
// Entry gets a single entry.
func (c *Client) Entry(entryID int64) (*Entry, error) {
body, err := c.request.Get(fmt.Sprintf("/v1/entries/%d", entryID))
@@ -397,6 +467,25 @@ func (c *Client) FeedEntries(feedID int64, filter *Filter) (*EntryResultSet, err
return &result, nil
}
// CategoryEntries fetch entries of a category.
func (c *Client) CategoryEntries(categoryID int64, filter *Filter) (*EntryResultSet, error) {
path := buildFilterQueryString(fmt.Sprintf("/v1/categories/%d/entries", categoryID), filter)
body, err := c.request.Get(path)
if err != nil {
return nil, err
}
defer body.Close()
var result EntryResultSet
decoder := json.NewDecoder(body)
if err := decoder.Decode(&result); err != nil {
return nil, fmt.Errorf("miniflux: response error (%v)", err)
}
return &result, nil
}
// UpdateEntries updates the status of a list of entries.
func (c *Client) UpdateEntries(entryIDs []int64, status string) error {
type payload struct {
@@ -404,29 +493,14 @@ func (c *Client) UpdateEntries(entryIDs []int64, status string) error {
Status string `json:"status"`
}
body, err := c.request.Put("/v1/entries", &payload{EntryIDs: entryIDs, Status: status})
if err != nil {
return err
}
body.Close()
return nil
_, err := c.request.Put("/v1/entries", &payload{EntryIDs: entryIDs, Status: status})
return err
}
// ToggleBookmark toggles entry bookmark value.
func (c *Client) ToggleBookmark(entryID int64) error {
body, err := c.request.Put(fmt.Sprintf("/v1/entries/%d/bookmark", entryID), nil)
if err != nil {
return err
}
body.Close()
return nil
}
// NewClient returns a new Client.
func NewClient(endpoint, username, password string) *Client {
return &Client{request: &request{endpoint: endpoint, username: username, password: password}}
_, err := c.request.Put(fmt.Sprintf("/v1/entries/%d/bookmark", entryID), nil)
return err
}
func buildFilterQueryString(path string, filter *Filter) string {
@@ -453,6 +527,42 @@ func buildFilterQueryString(path string, filter *Filter) string {
values.Set("offset", strconv.Itoa(filter.Offset))
}
if filter.After > 0 {
values.Set("after", strconv.FormatInt(filter.After, 10))
}
if filter.AfterEntryID > 0 {
values.Set("after_entry_id", strconv.FormatInt(filter.AfterEntryID, 10))
}
if filter.Before > 0 {
values.Set("before", strconv.FormatInt(filter.Before, 10))
}
if filter.BeforeEntryID > 0 {
values.Set("before_entry_id", strconv.FormatInt(filter.BeforeEntryID, 10))
}
if filter.Starred {
values.Set("starred", "1")
}
if filter.Search != "" {
values.Set("search", filter.Search)
}
if filter.CategoryID > 0 {
values.Set("category_id", strconv.FormatInt(filter.CategoryID, 10))
}
if filter.FeedID > 0 {
values.Set("feed_id", strconv.FormatInt(filter.FeedID, 10))
}
for _, status := range filter.Statuses {
values.Add("status", status)
}
path = fmt.Sprintf("%s?%s", path, values.Encode())
}
+231
View File
@@ -0,0 +1,231 @@
// Copyright 2018 Frédéric Guillot. All rights reserved.
// Use of this source code is governed by the Apache 2.0
// license that can be found in the LICENSE file.
package client // import "miniflux.app/client"
import (
"fmt"
"time"
)
// Entry statuses.
const (
EntryStatusUnread = "unread"
EntryStatusRead = "read"
EntryStatusRemoved = "removed"
)
// User represents a user in the system.
type User struct {
ID int64 `json:"id"`
Username string `json:"username"`
Password string `json:"password,omitempty"`
IsAdmin bool `json:"is_admin"`
Theme string `json:"theme"`
Language string `json:"language"`
Timezone string `json:"timezone"`
EntryDirection string `json:"entry_sorting_direction"`
Stylesheet string `json:"stylesheet"`
GoogleID string `json:"google_id"`
OpenIDConnectID string `json:"openid_connect_id"`
EntriesPerPage int `json:"entries_per_page"`
KeyboardShortcuts bool `json:"keyboard_shortcuts"`
ShowReadingTime bool `json:"show_reading_time"`
EntrySwipe bool `json:"entry_swipe"`
LastLoginAt *time.Time `json:"last_login_at"`
}
func (u User) String() string {
return fmt.Sprintf("#%d - %s (admin=%v)", u.ID, u.Username, u.IsAdmin)
}
// UserCreationRequest represents the request to create a user.
type UserCreationRequest struct {
Username string `json:"username"`
Password string `json:"password"`
IsAdmin bool `json:"is_admin"`
GoogleID string `json:"google_id"`
OpenIDConnectID string `json:"openid_connect_id"`
}
// UserModificationRequest represents the request to update a user.
type UserModificationRequest struct {
Username *string `json:"username"`
Password *string `json:"password"`
IsAdmin *bool `json:"is_admin"`
Theme *string `json:"theme"`
Language *string `json:"language"`
Timezone *string `json:"timezone"`
EntryDirection *string `json:"entry_sorting_direction"`
Stylesheet *string `json:"stylesheet"`
GoogleID *string `json:"google_id"`
OpenIDConnectID *string `json:"openid_connect_id"`
EntriesPerPage *int `json:"entries_per_page"`
KeyboardShortcuts *bool `json:"keyboard_shortcuts"`
ShowReadingTime *bool `json:"show_reading_time"`
EntrySwipe *bool `json:"entry_swipe"`
}
// Users represents a list of users.
type Users []User
// Category represents a feed category.
type Category struct {
ID int64 `json:"id,omitempty"`
Title string `json:"title,omitempty"`
UserID int64 `json:"user_id,omitempty"`
}
func (c Category) String() string {
return fmt.Sprintf("#%d %s", c.ID, c.Title)
}
// Categories represents a list of categories.
type Categories []*Category
// Subscription represents a feed subscription.
type Subscription struct {
Title string `json:"title"`
URL string `json:"url"`
Type string `json:"type"`
}
func (s Subscription) String() string {
return fmt.Sprintf(`Title="%s", URL="%s", Type="%s"`, s.Title, s.URL, s.Type)
}
// Subscriptions represents a list of subscriptions.
type Subscriptions []*Subscription
// Feed represents a Miniflux feed.
type Feed struct {
ID int64 `json:"id"`
UserID int64 `json:"user_id"`
FeedURL string `json:"feed_url"`
SiteURL string `json:"site_url"`
Title string `json:"title"`
CheckedAt time.Time `json:"checked_at,omitempty"`
EtagHeader string `json:"etag_header,omitempty"`
LastModifiedHeader string `json:"last_modified_header,omitempty"`
ParsingErrorMsg string `json:"parsing_error_message,omitempty"`
ParsingErrorCount int `json:"parsing_error_count,omitempty"`
Disabled bool `json:"disabled"`
IgnoreHTTPCache bool `json:"ignore_http_cache"`
FetchViaProxy bool `json:"fetch_via_proxy"`
ScraperRules string `json:"scraper_rules"`
RewriteRules string `json:"rewrite_rules"`
BlocklistRules string `json:"blocklist_rules"`
KeeplistRules string `json:"keeplist_rules"`
Crawler bool `json:"crawler"`
UserAgent string `json:"user_agent"`
Username string `json:"username"`
Password string `json:"password"`
Category *Category `json:"category,omitempty"`
}
// FeedCreationRequest represents the request to create a feed.
type FeedCreationRequest struct {
FeedURL string `json:"feed_url"`
CategoryID int64 `json:"category_id"`
UserAgent string `json:"user_agent"`
Username string `json:"username"`
Password string `json:"password"`
Crawler bool `json:"crawler"`
Disabled bool `json:"disabled"`
IgnoreHTTPCache bool `json:"ignore_http_cache"`
FetchViaProxy bool `json:"fetch_via_proxy"`
ScraperRules string `json:"scraper_rules"`
RewriteRules string `json:"rewrite_rules"`
BlocklistRules string `json:"blocklist_rules"`
KeeplistRules string `json:"keeplist_rules"`
}
// FeedModificationRequest represents the request to update a feed.
type FeedModificationRequest struct {
FeedURL *string `json:"feed_url"`
SiteURL *string `json:"site_url"`
Title *string `json:"title"`
ScraperRules *string `json:"scraper_rules"`
RewriteRules *string `json:"rewrite_rules"`
BlocklistRules *string `json:"blocklist_rules"`
KeeplistRules *string `json:"keeplist_rules"`
Crawler *bool `json:"crawler"`
UserAgent *string `json:"user_agent"`
Username *string `json:"username"`
Password *string `json:"password"`
CategoryID *int64 `json:"category_id"`
Disabled *bool `json:"disabled"`
IgnoreHTTPCache *bool `json:"ignore_http_cache"`
FetchViaProxy *bool `json:"fetch_via_proxy"`
}
// FeedIcon represents the feed icon.
type FeedIcon struct {
ID int64 `json:"id"`
MimeType string `json:"mime_type"`
Data string `json:"data"`
}
// Feeds represents a list of feeds.
type Feeds []*Feed
// Entry represents a subscription item in the system.
type Entry struct {
ID int64 `json:"id"`
UserID int64 `json:"user_id"`
FeedID int64 `json:"feed_id"`
Status string `json:"status"`
Hash string `json:"hash"`
Title string `json:"title"`
URL string `json:"url"`
Date time.Time `json:"published_at"`
CreatedAt time.Time `json:"created_at"`
Content string `json:"content"`
Author string `json:"author"`
ShareCode string `json:"share_code"`
Starred bool `json:"starred"`
ReadingTime int `json:"reading_time"`
Enclosures Enclosures `json:"enclosures,omitempty"`
Feed *Feed `json:"feed,omitempty"`
}
// Entries represents a list of entries.
type Entries []*Entry
// Enclosure represents an attachment.
type Enclosure struct {
ID int64 `json:"id"`
UserID int64 `json:"user_id"`
EntryID int64 `json:"entry_id"`
URL string `json:"url"`
MimeType string `json:"mime_type"`
Size int `json:"size"`
}
// Enclosures represents a list of attachments.
type Enclosures []*Enclosure
// Filter is used to filter entries.
type Filter struct {
Status string
Offset int
Limit int
Order string
Direction string
Starred bool
Before int64
After int64
BeforeEntryID int64
AfterEntryID int64
Search string
CategoryID int64
FeedID int64
Statuses []string
}
// EntryResultSet represents the response when fetching entries.
type EntryResultSet struct {
Total int `json:"total"`
Entries Entries `json:"entries"`
}
+35
View File
@@ -0,0 +1,35 @@
// Copyright 2018 Frédéric Guillot. All rights reserved.
// Use of this source code is governed by the Apache 2.0
// license that can be found in the LICENSE file.
/*
Package client implements a client library for the Miniflux REST API.
Examples
This code snippet fetch the list of users:
import (
miniflux "miniflux.app/client"
)
client := miniflux.New("https://api.example.org", "admin", "secret")
users, err := client.Users()
if err != nil {
fmt.Println(err)
return
}
fmt.Println(users, err)
This one discover subscriptions on a website:
subscriptions, err := client.Discover("https://example.org/")
if err != nil {
fmt.Println(err)
return
}
fmt.Println(subscriptions)
*/
package client // import "miniflux.app/client"
@@ -1,8 +1,8 @@
// Copyright 2017 Frédéric Guillot. All rights reserved.
// Use of this source code is governed by the MIT license
// that can be found in the LICENSE file.
// Copyright 2018 Frédéric Guillot. All rights reserved.
// Use of this source code is governed by the Apache 2.0
// license that can be found in the LICENSE file.
package miniflux
package client // import "miniflux.app/client"
import (
"bytes"
@@ -18,14 +18,16 @@ import (
)
const (
userAgent = "Miniflux Client Library <https://github.com/miniflux/miniflux-go>"
userAgent = "Miniflux Client Library"
defaultTimeout = 80
)
// List of exposed errors.
var (
errNotAuthorized = errors.New("miniflux: unauthorized (bad credentials)")
errForbidden = errors.New("miniflux: access forbidden")
errServerError = errors.New("miniflux: internal server error")
ErrNotAuthorized = errors.New("miniflux: unauthorized (bad credentials)")
ErrForbidden = errors.New("miniflux: access forbidden")
ErrServerError = errors.New("miniflux: internal server error")
ErrNotFound = errors.New("miniflux: resource not found")
)
type errorResponse struct {
@@ -36,6 +38,7 @@ type request struct {
endpoint string
username string
password string
apiKey string
}
func (r *request) Get(path string) (io.ReadCloser, error) {
@@ -46,12 +49,17 @@ func (r *request) Post(path string, data interface{}) (io.ReadCloser, error) {
return r.execute(http.MethodPost, path, data)
}
func (r *request) PostFile(path string, f io.ReadCloser) (io.ReadCloser, error) {
return r.execute(http.MethodPost, path, f)
}
func (r *request) Put(path string, data interface{}) (io.ReadCloser, error) {
return r.execute(http.MethodPut, path, data)
}
func (r *request) Delete(path string) (io.ReadCloser, error) {
return r.execute(http.MethodDelete, path, nil)
func (r *request) Delete(path string) error {
_, err := r.execute(http.MethodDelete, path, nil)
return err
}
func (r *request) execute(method, path string, data interface{}) (io.ReadCloser, error) {
@@ -69,10 +77,18 @@ func (r *request) execute(method, path string, data interface{}) (io.ReadCloser,
Method: method,
Header: r.buildHeaders(),
}
request.SetBasicAuth(r.username, r.password)
if r.username != "" && r.password != "" {
request.SetBasicAuth(r.username, r.password)
}
if data != nil {
request.Body = ioutil.NopCloser(bytes.NewBuffer(r.toJSON(data)))
switch data.(type) {
case io.ReadCloser:
request.Body = data.(io.ReadCloser)
default:
request.Body = ioutil.NopCloser(bytes.NewBuffer(r.toJSON(data)))
}
}
client := r.buildClient()
@@ -83,11 +99,20 @@ func (r *request) execute(method, path string, data interface{}) (io.ReadCloser,
switch response.StatusCode {
case http.StatusUnauthorized:
return nil, errNotAuthorized
response.Body.Close()
return nil, ErrNotAuthorized
case http.StatusForbidden:
return nil, errForbidden
response.Body.Close()
return nil, ErrForbidden
case http.StatusInternalServerError:
return nil, errServerError
response.Body.Close()
return nil, ErrServerError
case http.StatusNotFound:
response.Body.Close()
return nil, ErrNotFound
case http.StatusNoContent:
response.Body.Close()
return nil, nil
case http.StatusBadRequest:
defer response.Body.Close()
@@ -100,8 +125,9 @@ func (r *request) execute(method, path string, data interface{}) (io.ReadCloser,
return nil, fmt.Errorf("miniflux: bad request (%s)", resp.ErrorMessage)
}
if response.StatusCode >= 400 {
return nil, fmt.Errorf("miniflux: server error (statusCode=%d)", response.StatusCode)
if response.StatusCode > 400 {
response.Body.Close()
return nil, fmt.Errorf("miniflux: status code=%d", response.StatusCode)
}
return response.Body, nil
@@ -118,6 +144,9 @@ func (r *request) buildHeaders() http.Header {
headers.Add("User-Agent", userAgent)
headers.Add("Content-Type", "application/json")
headers.Add("Accept", "application/json")
if r.apiKey != "" {
headers.Add("X-Auth-Token", r.apiKey)
}
return headers
}
+4 -185
View File
@@ -1,189 +1,8 @@
// Copyright 2017 Frédéric Guillot. All rights reserved.
// Copyright 2018 Frédéric Guillot. All rights reserved.
// Use of this source code is governed by the Apache 2.0
// license that can be found in the LICENSE file.
package config
package config // import "miniflux.app/config"
import (
"net/url"
"os"
"strconv"
"github.com/miniflux/miniflux/logger"
)
const (
defaultBaseURL = "http://localhost"
defaultDatabaseURL = "postgres://postgres:postgres@localhost/miniflux2?sslmode=disable"
defaultWorkerPoolSize = 5
defaultPollingFrequency = 60
defaultBatchSize = 10
defaultDatabaseMaxConns = 20
defaultListenAddr = "127.0.0.1:8080"
defaultCertFile = ""
defaultKeyFile = ""
defaultCertDomain = ""
defaultCertCache = "/tmp/cert_cache"
defaultSessionCleanupFrequency = 24
)
// Config manages configuration parameters.
type Config struct {
IsHTTPS bool
baseURL string
rootURL string
basePath string
}
func (c *Config) get(key, fallback string) string {
value := os.Getenv(key)
if value == "" {
return fallback
}
return value
}
func (c *Config) getInt(key string, fallback int) int {
value := os.Getenv(key)
if value == "" {
return fallback
}
v, _ := strconv.Atoi(value)
return v
}
// HasDebugMode returns true if debug mode is enabled.
func (c *Config) HasDebugMode() bool {
return c.get("DEBUG", "") != ""
}
// BaseURL returns the application base URL with path.
func (c *Config) BaseURL() string {
if c.baseURL == "" {
c.baseURL = c.get("BASE_URL", defaultBaseURL)
if c.baseURL[len(c.baseURL)-1:] == "/" {
c.baseURL = c.baseURL[:len(c.baseURL)-1]
}
}
return c.baseURL
}
// RootURL returns the base URL without path.
func (c *Config) RootURL() string {
if c.rootURL == "" {
u, _ := url.Parse(c.BaseURL())
u.Path = ""
c.rootURL = u.String()
}
return c.rootURL
}
// BasePath returns the application base path according to the base URL.
func (c *Config) BasePath() string {
if c.basePath == "" {
u, _ := url.Parse(c.BaseURL())
c.basePath = u.Path
}
return c.basePath
}
// DatabaseURL returns the database URL.
func (c *Config) DatabaseURL() string {
value, exists := os.LookupEnv("DATABASE_URL")
if !exists {
logger.Info("The environment variable DATABASE_URL is not configured (the default value is used instead)")
}
if value == "" {
value = defaultDatabaseURL
}
return value
}
// DatabaseMaxConnections returns the number of maximum database connections.
func (c *Config) DatabaseMaxConnections() int {
return c.getInt("DATABASE_MAX_CONNS", defaultDatabaseMaxConns)
}
// ListenAddr returns the listen address for the HTTP server.
func (c *Config) ListenAddr() string {
return c.get("LISTEN_ADDR", defaultListenAddr)
}
// CertFile returns the SSL certificate filename if any.
func (c *Config) CertFile() string {
return c.get("CERT_FILE", defaultCertFile)
}
// KeyFile returns the private key filename for custom SSL certificate.
func (c *Config) KeyFile() string {
return c.get("KEY_FILE", defaultKeyFile)
}
// CertDomain returns the domain to use for Let's Encrypt certificate.
func (c *Config) CertDomain() string {
return c.get("CERT_DOMAIN", defaultCertDomain)
}
// CertCache returns the directory to use for Let's Encrypt session cache.
func (c *Config) CertCache() string {
return c.get("CERT_CACHE", defaultCertCache)
}
// SessionCleanupFrequency returns the interval for session cleanup.
func (c *Config) SessionCleanupFrequency() int {
return c.getInt("SESSION_CLEANUP_FREQUENCY", defaultSessionCleanupFrequency)
}
// WorkerPoolSize returns the number of background worker.
func (c *Config) WorkerPoolSize() int {
return c.getInt("WORKER_POOL_SIZE", defaultWorkerPoolSize)
}
// PollingFrequency returns the interval to refresh feeds in the background.
func (c *Config) PollingFrequency() int {
return c.getInt("POLLING_FREQUENCY", defaultPollingFrequency)
}
// BatchSize returns the number of feeds to send for background processing.
func (c *Config) BatchSize() int {
return c.getInt("BATCH_SIZE", defaultBatchSize)
}
// IsOAuth2UserCreationAllowed returns true if user creation is allowed for OAuth2 users.
func (c *Config) IsOAuth2UserCreationAllowed() bool {
return c.getInt("OAUTH2_USER_CREATION", 0) == 1
}
// OAuth2ClientID returns the OAuth2 Client ID.
func (c *Config) OAuth2ClientID() string {
return c.get("OAUTH2_CLIENT_ID", "")
}
// OAuth2ClientSecret returns the OAuth2 client secret.
func (c *Config) OAuth2ClientSecret() string {
return c.get("OAUTH2_CLIENT_SECRET", "")
}
// OAuth2RedirectURL returns the OAuth2 redirect URL.
func (c *Config) OAuth2RedirectURL() string {
return c.get("OAUTH2_REDIRECT_URL", "")
}
// OAuth2Provider returns the name of the OAuth2 provider configured.
func (c *Config) OAuth2Provider() string {
return c.get("OAUTH2_PROVIDER", "")
}
// HasHSTS returns true if HTTP Strict Transport Security is enabled.
func (c *Config) HasHSTS() bool {
return c.get("DISABLE_HSTS", "") == ""
}
// NewConfig returns a new Config.
func NewConfig() *Config {
return &Config{IsHTTPS: os.Getenv("HTTPS") != ""}
}
// Opts holds parsed configuration options.
var Opts *Options
+1352 -37
View File
File diff suppressed because it is too large Load Diff
+5 -5
View File
@@ -1,10 +1,10 @@
// Copyright 2018 Frédéric Guillot. All rights reserved.
// Use of this source code is governed by the MIT license
// that can be found in the LICENSE file.
// Copyright 2019 Frédéric Guillot. All rights reserved.
// Use of this source code is governed by the Apache 2.0
// license that can be found in the LICENSE file.
/*
Package config handles configuration values for Miniflux application.
Package config handles configuration management for the application.
*/
package config
package config // import "miniflux.app/config"
+528
View File
@@ -0,0 +1,528 @@
// Copyright 2019 Frédéric Guillot. All rights reserved.
// Use of this source code is governed by the Apache 2.0
// license that can be found in the LICENSE file.
package config // import "miniflux.app/config"
import (
"fmt"
"sort"
"strings"
"miniflux.app/version"
)
const (
defaultHTTPS = false
defaultLogDateTime = false
defaultHSTS = true
defaultHTTPService = true
defaultSchedulerService = true
defaultDebug = false
defaultTiming = false
defaultBaseURL = "http://localhost"
defaultRootURL = "http://localhost"
defaultBasePath = ""
defaultWorkerPoolSize = 5
defaultPollingFrequency = 60
defaultBatchSize = 10
defaultPollingScheduler = "round_robin"
defaultSchedulerEntryFrequencyMinInterval = 5
defaultSchedulerEntryFrequencyMaxInterval = 24 * 60
defaultPollingParsingErrorLimit = 3
defaultRunMigrations = false
defaultDatabaseURL = "user=postgres password=postgres dbname=miniflux2 sslmode=disable"
defaultDatabaseMaxConns = 20
defaultDatabaseMinConns = 1
defaultListenAddr = "127.0.0.1:8080"
defaultCertFile = ""
defaultKeyFile = ""
defaultCertDomain = ""
defaultCleanupFrequencyHours = 24
defaultCleanupArchiveReadDays = 60
defaultCleanupArchiveUnreadDays = 180
defaultCleanupRemoveSessionsDays = 30
defaultProxyImages = "http-only"
defaultCreateAdmin = false
defaultAdminUsername = ""
defaultAdminPassword = ""
defaultOAuth2UserCreation = false
defaultOAuth2ClientID = ""
defaultOAuth2ClientSecret = ""
defaultOAuth2RedirectURL = ""
defaultOAuth2OidcDiscoveryEndpoint = ""
defaultOAuth2Provider = ""
defaultPocketConsumerKey = ""
defaultHTTPClientTimeout = 20
defaultHTTPClientMaxBodySize = 15
defaultHTTPClientProxy = ""
defaultAuthProxyHeader = ""
defaultAuthProxyUserCreation = false
defaultMaintenanceMode = false
defaultMaintenanceMessage = "Miniflux is currently under maintenance"
defaultMetricsCollector = false
defaultMetricsRefreshInterval = 60
defaultMetricsAllowedNetworks = "127.0.0.1/8"
)
var defaultHTTPClientUserAgent = "Mozilla/5.0 (compatible; Miniflux/" + version.Version + "; +https://miniflux.app)"
// Option contains a key to value map of a single option. It may be used to output debug strings.
type Option struct {
Key string
Value interface{}
}
// Options contains configuration options.
type Options struct {
HTTPS bool
logDateTime bool
hsts bool
httpService bool
schedulerService bool
debug bool
serverTimingHeader bool
baseURL string
rootURL string
basePath string
databaseURL string
databaseMaxConns int
databaseMinConns int
runMigrations bool
listenAddr string
certFile string
certDomain string
certKeyFile string
cleanupFrequencyHours int
cleanupArchiveReadDays int
cleanupArchiveUnreadDays int
cleanupRemoveSessionsDays int
pollingFrequency int
batchSize int
pollingScheduler string
schedulerEntryFrequencyMinInterval int
schedulerEntryFrequencyMaxInterval int
pollingParsingErrorLimit int
workerPoolSize int
createAdmin bool
adminUsername string
adminPassword string
proxyImages string
oauth2UserCreationAllowed bool
oauth2ClientID string
oauth2ClientSecret string
oauth2RedirectURL string
oauth2OidcDiscoveryEndpoint string
oauth2Provider string
pocketConsumerKey string
httpClientTimeout int
httpClientMaxBodySize int64
httpClientProxy string
httpClientUserAgent string
authProxyHeader string
authProxyUserCreation bool
maintenanceMode bool
maintenanceMessage string
metricsCollector bool
metricsRefreshInterval int
metricsAllowedNetworks []string
}
// NewOptions returns Options with default values.
func NewOptions() *Options {
return &Options{
HTTPS: defaultHTTPS,
logDateTime: defaultLogDateTime,
hsts: defaultHSTS,
httpService: defaultHTTPService,
schedulerService: defaultSchedulerService,
debug: defaultDebug,
serverTimingHeader: defaultTiming,
baseURL: defaultBaseURL,
rootURL: defaultRootURL,
basePath: defaultBasePath,
databaseURL: defaultDatabaseURL,
databaseMaxConns: defaultDatabaseMaxConns,
databaseMinConns: defaultDatabaseMinConns,
runMigrations: defaultRunMigrations,
listenAddr: defaultListenAddr,
certFile: defaultCertFile,
certDomain: defaultCertDomain,
certKeyFile: defaultKeyFile,
cleanupFrequencyHours: defaultCleanupFrequencyHours,
cleanupArchiveReadDays: defaultCleanupArchiveReadDays,
cleanupArchiveUnreadDays: defaultCleanupArchiveUnreadDays,
cleanupRemoveSessionsDays: defaultCleanupRemoveSessionsDays,
pollingFrequency: defaultPollingFrequency,
batchSize: defaultBatchSize,
pollingScheduler: defaultPollingScheduler,
schedulerEntryFrequencyMinInterval: defaultSchedulerEntryFrequencyMinInterval,
schedulerEntryFrequencyMaxInterval: defaultSchedulerEntryFrequencyMaxInterval,
pollingParsingErrorLimit: defaultPollingParsingErrorLimit,
workerPoolSize: defaultWorkerPoolSize,
createAdmin: defaultCreateAdmin,
proxyImages: defaultProxyImages,
oauth2UserCreationAllowed: defaultOAuth2UserCreation,
oauth2ClientID: defaultOAuth2ClientID,
oauth2ClientSecret: defaultOAuth2ClientSecret,
oauth2RedirectURL: defaultOAuth2RedirectURL,
oauth2OidcDiscoveryEndpoint: defaultOAuth2OidcDiscoveryEndpoint,
oauth2Provider: defaultOAuth2Provider,
pocketConsumerKey: defaultPocketConsumerKey,
httpClientTimeout: defaultHTTPClientTimeout,
httpClientMaxBodySize: defaultHTTPClientMaxBodySize * 1024 * 1024,
httpClientProxy: defaultHTTPClientProxy,
httpClientUserAgent: defaultHTTPClientUserAgent,
authProxyHeader: defaultAuthProxyHeader,
authProxyUserCreation: defaultAuthProxyUserCreation,
maintenanceMode: defaultMaintenanceMode,
maintenanceMessage: defaultMaintenanceMessage,
metricsCollector: defaultMetricsCollector,
metricsRefreshInterval: defaultMetricsRefreshInterval,
metricsAllowedNetworks: []string{defaultMetricsAllowedNetworks},
}
}
// LogDateTime returns true if the date/time should be displayed in log messages.
func (o *Options) LogDateTime() bool {
return o.logDateTime
}
// HasMaintenanceMode returns true if maintenance mode is enabled.
func (o *Options) HasMaintenanceMode() bool {
return o.maintenanceMode
}
// MaintenanceMessage returns maintenance message.
func (o *Options) MaintenanceMessage() string {
return o.maintenanceMessage
}
// HasDebugMode returns true if debug mode is enabled.
func (o *Options) HasDebugMode() bool {
return o.debug
}
// HasServerTimingHeader returns true if server-timing headers enabled.
func (o *Options) HasServerTimingHeader() bool {
return o.serverTimingHeader
}
// BaseURL returns the application base URL with path.
func (o *Options) BaseURL() string {
return o.baseURL
}
// RootURL returns the base URL without path.
func (o *Options) RootURL() string {
return o.rootURL
}
// BasePath returns the application base path according to the base URL.
func (o *Options) BasePath() string {
return o.basePath
}
// IsDefaultDatabaseURL returns true if the default database URL is used.
func (o *Options) IsDefaultDatabaseURL() bool {
return o.databaseURL == defaultDatabaseURL
}
// DatabaseURL returns the database URL.
func (o *Options) DatabaseURL() string {
return o.databaseURL
}
// DatabaseMaxConns returns the maximum number of database connections.
func (o *Options) DatabaseMaxConns() int {
return o.databaseMaxConns
}
// DatabaseMinConns returns the minimum number of database connections.
func (o *Options) DatabaseMinConns() int {
return o.databaseMinConns
}
// ListenAddr returns the listen address for the HTTP server.
func (o *Options) ListenAddr() string {
return o.listenAddr
}
// CertFile returns the SSL certificate filename if any.
func (o *Options) CertFile() string {
return o.certFile
}
// CertKeyFile returns the private key filename for custom SSL certificate.
func (o *Options) CertKeyFile() string {
return o.certKeyFile
}
// CertDomain returns the domain to use for Let's Encrypt certificate.
func (o *Options) CertDomain() string {
return o.certDomain
}
// CleanupFrequencyHours returns the interval in hours for cleanup jobs.
func (o *Options) CleanupFrequencyHours() int {
return o.cleanupFrequencyHours
}
// CleanupArchiveReadDays returns the number of days after which marking read items as removed.
func (o *Options) CleanupArchiveReadDays() int {
return o.cleanupArchiveReadDays
}
// CleanupArchiveUnreadDays returns the number of days after which marking unread items as removed.
func (o *Options) CleanupArchiveUnreadDays() int {
return o.cleanupArchiveUnreadDays
}
// CleanupRemoveSessionsDays returns the number of days after which to remove sessions.
func (o *Options) CleanupRemoveSessionsDays() int {
return o.cleanupRemoveSessionsDays
}
// WorkerPoolSize returns the number of background worker.
func (o *Options) WorkerPoolSize() int {
return o.workerPoolSize
}
// PollingFrequency returns the interval to refresh feeds in the background.
func (o *Options) PollingFrequency() int {
return o.pollingFrequency
}
// BatchSize returns the number of feeds to send for background processing.
func (o *Options) BatchSize() int {
return o.batchSize
}
// PollingScheduler returns the scheduler used for polling feeds.
func (o *Options) PollingScheduler() string {
return o.pollingScheduler
}
// SchedulerEntryFrequencyMaxInterval returns the maximum interval in minutes for the entry frequency scheduler.
func (o *Options) SchedulerEntryFrequencyMaxInterval() int {
return o.schedulerEntryFrequencyMaxInterval
}
// SchedulerEntryFrequencyMinInterval returns the minimum interval in minutes for the entry frequency scheduler.
func (o *Options) SchedulerEntryFrequencyMinInterval() int {
return o.schedulerEntryFrequencyMinInterval
}
// PollingParsingErrorLimit returns the limit of errors when to stop polling.
func (o *Options) PollingParsingErrorLimit() int {
return o.pollingParsingErrorLimit
}
// IsOAuth2UserCreationAllowed returns true if user creation is allowed for OAuth2 users.
func (o *Options) IsOAuth2UserCreationAllowed() bool {
return o.oauth2UserCreationAllowed
}
// OAuth2ClientID returns the OAuth2 Client ID.
func (o *Options) OAuth2ClientID() string {
return o.oauth2ClientID
}
// OAuth2ClientSecret returns the OAuth2 client secret.
func (o *Options) OAuth2ClientSecret() string {
return o.oauth2ClientSecret
}
// OAuth2RedirectURL returns the OAuth2 redirect URL.
func (o *Options) OAuth2RedirectURL() string {
return o.oauth2RedirectURL
}
// OAuth2OidcDiscoveryEndpoint returns the OAuth2 OIDC discovery endpoint.
func (o *Options) OAuth2OidcDiscoveryEndpoint() string {
return o.oauth2OidcDiscoveryEndpoint
}
// OAuth2Provider returns the name of the OAuth2 provider configured.
func (o *Options) OAuth2Provider() string {
return o.oauth2Provider
}
// HasHSTS returns true if HTTP Strict Transport Security is enabled.
func (o *Options) HasHSTS() bool {
return o.hsts
}
// RunMigrations returns true if the environment variable RUN_MIGRATIONS is not empty.
func (o *Options) RunMigrations() bool {
return o.runMigrations
}
// CreateAdmin returns true if the environment variable CREATE_ADMIN is not empty.
func (o *Options) CreateAdmin() bool {
return o.createAdmin
}
// AdminUsername returns the admin username if defined.
func (o *Options) AdminUsername() string {
return o.adminUsername
}
// AdminPassword returns the admin password if defined.
func (o *Options) AdminPassword() string {
return o.adminPassword
}
// ProxyImages returns "none" to never proxy, "http-only" to proxy non-HTTPS, "all" to always proxy.
func (o *Options) ProxyImages() string {
return o.proxyImages
}
// HasHTTPService returns true if the HTTP service is enabled.
func (o *Options) HasHTTPService() bool {
return o.httpService
}
// HasSchedulerService returns true if the scheduler service is enabled.
func (o *Options) HasSchedulerService() bool {
return o.schedulerService
}
// PocketConsumerKey returns the Pocket Consumer Key if configured.
func (o *Options) PocketConsumerKey(defaultValue string) string {
if o.pocketConsumerKey != "" {
return o.pocketConsumerKey
}
return defaultValue
}
// HTTPClientTimeout returns the time limit in seconds before the HTTP client cancel the request.
func (o *Options) HTTPClientTimeout() int {
return o.httpClientTimeout
}
// HTTPClientMaxBodySize returns the number of bytes allowed for the HTTP client to transfer.
func (o *Options) HTTPClientMaxBodySize() int64 {
return o.httpClientMaxBodySize
}
// HTTPClientProxy returns the proxy URL for HTTP client.
func (o *Options) HTTPClientProxy() string {
return o.httpClientProxy
}
// HasHTTPClientProxyConfigured returns true if the HTTP proxy is configured.
func (o *Options) HasHTTPClientProxyConfigured() bool {
return o.httpClientProxy != ""
}
// AuthProxyHeader returns an HTTP header name that contains username for
// authentication using auth proxy.
func (o *Options) AuthProxyHeader() string {
return o.authProxyHeader
}
// IsAuthProxyUserCreationAllowed returns true if user creation is allowed for
// users authenticated using auth proxy.
func (o *Options) IsAuthProxyUserCreationAllowed() bool {
return o.authProxyUserCreation
}
// HasMetricsCollector returns true if metrics collection is enabled.
func (o *Options) HasMetricsCollector() bool {
return o.metricsCollector
}
// MetricsRefreshInterval returns the refresh interval in seconds.
func (o *Options) MetricsRefreshInterval() int {
return o.metricsRefreshInterval
}
// MetricsAllowedNetworks returns the list of networks allowed to connect to the metrics endpoint.
func (o *Options) MetricsAllowedNetworks() []string {
return o.metricsAllowedNetworks
}
// HTTPClientUserAgent returns the global User-Agent header for miniflux.
func (o *Options) HTTPClientUserAgent() string {
return o.httpClientUserAgent
}
// SortedOptions returns options as a list of key value pairs, sorted by keys.
func (o *Options) SortedOptions() []*Option {
var keyValues = map[string]interface{}{
"ADMIN_PASSWORD": o.adminPassword,
"ADMIN_USERNAME": o.adminUsername,
"AUTH_PROXY_HEADER": o.authProxyHeader,
"AUTH_PROXY_USER_CREATION": o.authProxyUserCreation,
"BASE_PATH": o.basePath,
"BASE_URL": o.baseURL,
"BATCH_SIZE": o.batchSize,
"CERT_DOMAIN": o.certDomain,
"CERT_FILE": o.certFile,
"CLEANUP_ARCHIVE_READ_DAYS": o.cleanupArchiveReadDays,
"CLEANUP_ARCHIVE_UNREAD_DAYS": o.cleanupArchiveUnreadDays,
"CLEANUP_FREQUENCY_HOURS": o.cleanupFrequencyHours,
"CLEANUP_REMOVE_SESSIONS_DAYS": o.cleanupRemoveSessionsDays,
"CREATE_ADMIN": o.createAdmin,
"DATABASE_MAX_CONNS": o.databaseMaxConns,
"DATABASE_MIN_CONNS": o.databaseMinConns,
"DATABASE_URL": o.databaseURL,
"DEBUG": o.debug,
"HSTS": o.hsts,
"HTTPS": o.HTTPS,
"HTTP_CLIENT_MAX_BODY_SIZE": o.httpClientMaxBodySize,
"HTTP_CLIENT_PROXY": o.httpClientProxy,
"HTTP_CLIENT_TIMEOUT": o.httpClientTimeout,
"HTTP_CLIENT_USER_AGENT": o.httpClientUserAgent,
"HTTP_SERVICE": o.httpService,
"KEY_FILE": o.certKeyFile,
"LISTEN_ADDR": o.listenAddr,
"LOG_DATE_TIME": o.logDateTime,
"MAINTENANCE_MESSAGE": o.maintenanceMessage,
"MAINTENANCE_MODE": o.maintenanceMode,
"METRICS_ALLOWED_NETWORKS": o.metricsAllowedNetworks,
"METRICS_COLLECTOR": o.metricsCollector,
"METRICS_REFRESH_INTERVAL": o.metricsRefreshInterval,
"OAUTH2_CLIENT_ID": o.oauth2ClientID,
"OAUTH2_CLIENT_SECRET": o.oauth2ClientSecret,
"OAUTH2_OIDC_DISCOVERY_ENDPOINT": o.oauth2OidcDiscoveryEndpoint,
"OAUTH2_PROVIDER": o.oauth2Provider,
"OAUTH2_REDIRECT_URL": o.oauth2RedirectURL,
"OAUTH2_USER_CREATION": o.oauth2UserCreationAllowed,
"POCKET_CONSUMER_KEY": o.pocketConsumerKey,
"POLLING_FREQUENCY": o.pollingFrequency,
"POLLING_PARSING_ERROR_LIMIT": o.pollingParsingErrorLimit,
"POLLING_SCHEDULER": o.pollingScheduler,
"PROXY_IMAGES": o.proxyImages,
"ROOT_URL": o.rootURL,
"RUN_MIGRATIONS": o.runMigrations,
"SCHEDULER_ENTRY_FREQUENCY_MAX_INTERVAL": o.schedulerEntryFrequencyMaxInterval,
"SCHEDULER_ENTRY_FREQUENCY_MIN_INTERVAL": o.schedulerEntryFrequencyMinInterval,
"SCHEDULER_SERVICE": o.schedulerService,
"SERVER_TIMING_HEADER": o.serverTimingHeader,
"WORKER_POOL_SIZE": o.workerPoolSize,
}
keys := make([]string, 0, len(keyValues))
for key := range keyValues {
keys = append(keys, key)
}
sort.Strings(keys)
var sortedOptions []*Option
for _, key := range keys {
sortedOptions = append(sortedOptions, &Option{Key: key, Value: keyValues[key]})
}
return sortedOptions
}
func (o *Options) String() string {
var builder strings.Builder
for _, option := range o.SortedOptions() {
builder.WriteString(fmt.Sprintf("%s: %v\n", option.Key, option.Value))
}
return builder.String()
}
+283
View File
@@ -0,0 +1,283 @@
// Copyright 2019 Frédéric Guillot. All rights reserved.
// Use of this source code is governed by the Apache 2.0
// license that can be found in the LICENSE file.
package config // import "miniflux.app/config"
import (
"bufio"
"bytes"
"errors"
"fmt"
"io"
"io/ioutil"
url_parser "net/url"
"os"
"strconv"
"strings"
)
// Parser handles configuration parsing.
type Parser struct {
opts *Options
}
// NewParser returns a new Parser.
func NewParser() *Parser {
return &Parser{
opts: NewOptions(),
}
}
// ParseEnvironmentVariables loads configuration values from environment variables.
func (p *Parser) ParseEnvironmentVariables() (*Options, error) {
err := p.parseLines(os.Environ())
if err != nil {
return nil, err
}
return p.opts, nil
}
// ParseFile loads configuration values from a local file.
func (p *Parser) ParseFile(filename string) (*Options, error) {
fp, err := os.Open(filename)
if err != nil {
return nil, err
}
defer fp.Close()
err = p.parseLines(p.parseFileContent(fp))
if err != nil {
return nil, err
}
return p.opts, nil
}
func (p *Parser) parseFileContent(r io.Reader) (lines []string) {
scanner := bufio.NewScanner(r)
for scanner.Scan() {
line := strings.TrimSpace(scanner.Text())
if len(line) > 0 && !strings.HasPrefix(line, "#") && strings.Index(line, "=") > 0 {
lines = append(lines, line)
}
}
return lines
}
func (p *Parser) parseLines(lines []string) (err error) {
var port string
for _, line := range lines {
fields := strings.SplitN(line, "=", 2)
key := strings.TrimSpace(fields[0])
value := strings.TrimSpace(fields[1])
switch key {
case "LOG_DATE_TIME":
p.opts.logDateTime = parseBool(value, defaultLogDateTime)
case "DEBUG":
p.opts.debug = parseBool(value, defaultDebug)
case "SERVER_TIMING_HEADER":
p.opts.serverTimingHeader = parseBool(value, defaultTiming)
case "BASE_URL":
p.opts.baseURL, p.opts.rootURL, p.opts.basePath, err = parseBaseURL(value)
if err != nil {
return err
}
case "PORT":
port = value
case "LISTEN_ADDR":
p.opts.listenAddr = parseString(value, defaultListenAddr)
case "DATABASE_URL":
p.opts.databaseURL = parseString(value, defaultDatabaseURL)
case "DATABASE_URL_FILE":
p.opts.databaseURL = readSecretFile(value, defaultDatabaseURL)
case "DATABASE_MAX_CONNS":
p.opts.databaseMaxConns = parseInt(value, defaultDatabaseMaxConns)
case "DATABASE_MIN_CONNS":
p.opts.databaseMinConns = parseInt(value, defaultDatabaseMinConns)
case "RUN_MIGRATIONS":
p.opts.runMigrations = parseBool(value, defaultRunMigrations)
case "DISABLE_HSTS":
p.opts.hsts = !parseBool(value, defaultHSTS)
case "HTTPS":
p.opts.HTTPS = parseBool(value, defaultHTTPS)
case "DISABLE_SCHEDULER_SERVICE":
p.opts.schedulerService = !parseBool(value, defaultSchedulerService)
case "DISABLE_HTTP_SERVICE":
p.opts.httpService = !parseBool(value, defaultHTTPService)
case "CERT_FILE":
p.opts.certFile = parseString(value, defaultCertFile)
case "KEY_FILE":
p.opts.certKeyFile = parseString(value, defaultKeyFile)
case "CERT_DOMAIN":
p.opts.certDomain = parseString(value, defaultCertDomain)
case "CLEANUP_FREQUENCY_HOURS":
p.opts.cleanupFrequencyHours = parseInt(value, defaultCleanupFrequencyHours)
case "CLEANUP_ARCHIVE_READ_DAYS":
p.opts.cleanupArchiveReadDays = parseInt(value, defaultCleanupArchiveReadDays)
case "CLEANUP_ARCHIVE_UNREAD_DAYS":
p.opts.cleanupArchiveUnreadDays = parseInt(value, defaultCleanupArchiveUnreadDays)
case "CLEANUP_REMOVE_SESSIONS_DAYS":
p.opts.cleanupRemoveSessionsDays = parseInt(value, defaultCleanupRemoveSessionsDays)
case "WORKER_POOL_SIZE":
p.opts.workerPoolSize = parseInt(value, defaultWorkerPoolSize)
case "POLLING_FREQUENCY":
p.opts.pollingFrequency = parseInt(value, defaultPollingFrequency)
case "BATCH_SIZE":
p.opts.batchSize = parseInt(value, defaultBatchSize)
case "POLLING_SCHEDULER":
p.opts.pollingScheduler = strings.ToLower(parseString(value, defaultPollingScheduler))
case "SCHEDULER_ENTRY_FREQUENCY_MAX_INTERVAL":
p.opts.schedulerEntryFrequencyMaxInterval = parseInt(value, defaultSchedulerEntryFrequencyMaxInterval)
case "SCHEDULER_ENTRY_FREQUENCY_MIN_INTERVAL":
p.opts.schedulerEntryFrequencyMinInterval = parseInt(value, defaultSchedulerEntryFrequencyMinInterval)
case "POLLING_PARSING_ERROR_LIMIT":
p.opts.pollingParsingErrorLimit = parseInt(value, defaultPollingParsingErrorLimit)
case "PROXY_IMAGES":
p.opts.proxyImages = parseString(value, defaultProxyImages)
case "CREATE_ADMIN":
p.opts.createAdmin = parseBool(value, defaultCreateAdmin)
case "ADMIN_USERNAME":
p.opts.adminUsername = parseString(value, defaultAdminUsername)
case "ADMIN_USERNAME_FILE":
p.opts.adminUsername = readSecretFile(value, defaultAdminUsername)
case "ADMIN_PASSWORD":
p.opts.adminPassword = parseString(value, defaultAdminPassword)
case "ADMIN_PASSWORD_FILE":
p.opts.adminPassword = readSecretFile(value, defaultAdminPassword)
case "POCKET_CONSUMER_KEY":
p.opts.pocketConsumerKey = parseString(value, defaultPocketConsumerKey)
case "POCKET_CONSUMER_KEY_FILE":
p.opts.pocketConsumerKey = readSecretFile(value, defaultPocketConsumerKey)
case "OAUTH2_USER_CREATION":
p.opts.oauth2UserCreationAllowed = parseBool(value, defaultOAuth2UserCreation)
case "OAUTH2_CLIENT_ID":
p.opts.oauth2ClientID = parseString(value, defaultOAuth2ClientID)
case "OAUTH2_CLIENT_ID_FILE":
p.opts.oauth2ClientID = readSecretFile(value, defaultOAuth2ClientID)
case "OAUTH2_CLIENT_SECRET":
p.opts.oauth2ClientSecret = parseString(value, defaultOAuth2ClientSecret)
case "OAUTH2_CLIENT_SECRET_FILE":
p.opts.oauth2ClientSecret = readSecretFile(value, defaultOAuth2ClientSecret)
case "OAUTH2_REDIRECT_URL":
p.opts.oauth2RedirectURL = parseString(value, defaultOAuth2RedirectURL)
case "OAUTH2_OIDC_DISCOVERY_ENDPOINT":
p.opts.oauth2OidcDiscoveryEndpoint = parseString(value, defaultOAuth2OidcDiscoveryEndpoint)
case "OAUTH2_PROVIDER":
p.opts.oauth2Provider = parseString(value, defaultOAuth2Provider)
case "HTTP_CLIENT_TIMEOUT":
p.opts.httpClientTimeout = parseInt(value, defaultHTTPClientTimeout)
case "HTTP_CLIENT_MAX_BODY_SIZE":
p.opts.httpClientMaxBodySize = int64(parseInt(value, defaultHTTPClientMaxBodySize) * 1024 * 1024)
case "HTTP_CLIENT_PROXY":
p.opts.httpClientProxy = parseString(value, defaultHTTPClientProxy)
case "HTTP_CLIENT_USER_AGENT":
p.opts.httpClientUserAgent = parseString(value, defaultHTTPClientUserAgent)
case "AUTH_PROXY_HEADER":
p.opts.authProxyHeader = parseString(value, defaultAuthProxyHeader)
case "AUTH_PROXY_USER_CREATION":
p.opts.authProxyUserCreation = parseBool(value, defaultAuthProxyUserCreation)
case "MAINTENANCE_MODE":
p.opts.maintenanceMode = parseBool(value, defaultMaintenanceMode)
case "MAINTENANCE_MESSAGE":
p.opts.maintenanceMessage = parseString(value, defaultMaintenanceMessage)
case "METRICS_COLLECTOR":
p.opts.metricsCollector = parseBool(value, defaultMetricsCollector)
case "METRICS_REFRESH_INTERVAL":
p.opts.metricsRefreshInterval = parseInt(value, defaultMetricsRefreshInterval)
case "METRICS_ALLOWED_NETWORKS":
p.opts.metricsAllowedNetworks = parseStringList(value, []string{defaultMetricsAllowedNetworks})
}
}
if port != "" {
p.opts.listenAddr = ":" + port
}
return nil
}
func parseBaseURL(value string) (string, string, string, error) {
if value == "" {
return defaultBaseURL, defaultRootURL, "", nil
}
if value[len(value)-1:] == "/" {
value = value[:len(value)-1]
}
url, err := url_parser.Parse(value)
if err != nil {
return "", "", "", fmt.Errorf("Invalid BASE_URL: %v", err)
}
scheme := strings.ToLower(url.Scheme)
if scheme != "https" && scheme != "http" {
return "", "", "", errors.New("Invalid BASE_URL: scheme must be http or https")
}
basePath := url.Path
url.Path = ""
return value, url.String(), basePath, nil
}
func parseBool(value string, fallback bool) bool {
if value == "" {
return fallback
}
value = strings.ToLower(value)
if value == "1" || value == "yes" || value == "true" || value == "on" {
return true
}
return false
}
func parseInt(value string, fallback int) int {
if value == "" {
return fallback
}
v, err := strconv.Atoi(value)
if err != nil {
return fallback
}
return v
}
func parseString(value string, fallback string) string {
if value == "" {
return fallback
}
return value
}
func parseStringList(value string, fallback []string) []string {
if value == "" {
return fallback
}
var strList []string
items := strings.Split(value, ",")
for _, item := range items {
strList = append(strList, strings.TrimSpace(item))
}
return strList
}
func readSecretFile(filename, fallback string) string {
data, err := ioutil.ReadFile(filename)
if err != nil {
return fallback
}
value := string(bytes.TrimSpace(data))
if value == "" {
return fallback
}
return value
}
+61
View File
@@ -0,0 +1,61 @@
// Copyright 2019 Frédéric Guillot. All rights reserved.
// Use of this source code is governed by the Apache 2.0
// license that can be found in the LICENSE file.
package config // import "miniflux.app/config"
import (
"testing"
)
func TestParseBoolValue(t *testing.T) {
scenarios := map[string]bool{
"": true,
"1": true,
"Yes": true,
"yes": true,
"True": true,
"true": true,
"on": true,
"false": false,
"off": false,
"invalid": false,
}
for input, expected := range scenarios {
result := parseBool(input, true)
if result != expected {
t.Errorf(`Unexpected result for %q, got %v instead of %v`, input, result, expected)
}
}
}
func TestParseStringValueWithUnsetVariable(t *testing.T) {
if parseString("", "defaultValue") != "defaultValue" {
t.Errorf(`Unset variables should returns the default value`)
}
}
func TestParseStringValue(t *testing.T) {
if parseString("test", "defaultValue") != "test" {
t.Errorf(`Defined variables should returns the specified value`)
}
}
func TestParseIntValueWithUnsetVariable(t *testing.T) {
if parseInt("", 42) != 42 {
t.Errorf(`Unset variables should returns the default value`)
}
}
func TestParseIntValueWithInvalidInput(t *testing.T) {
if parseInt("invalid integer", 42) != 42 {
t.Errorf(`Invalid integer should returns the default value`)
}
}
func TestParseIntValue(t *testing.T) {
if parseInt("2018", 42) != 2018 {
t.Errorf(`Defined variables should returns the specified value`)
}
}
+4
View File
@@ -0,0 +1,4 @@
The contrib directory contains various useful things contributed by the community.
Community contributions are not officially supported by the maintainers.
There is no guarantee whatsoever that anything in this folder works.
@@ -0,0 +1,8 @@
---
miniflux_linux_user: miniflux
miniflux_db_user_name: miniflux_db_user
miniflux_db_user_password: miniflux_db_user_password
miniflux_db: miniflux_db
miniflux_admin_name: admin
miniflux_admin_passwort: miniflux_admin_password
miniflux_port: 8080
+4
View File
@@ -0,0 +1,4 @@
---
- hosts: miniflux
roles:
- { role: mgrote.miniflux, tags: "miniflux" }
@@ -0,0 +1,23 @@
## mgrote.miniflux
### Details
Installs and configures Miniflux v2 with ansible
### Works on...
- [x] Ubuntu (>=18.04)
### Variables and Defaults
##### Linux User
miniflux_linux_user: miniflux
##### DB User
miniflux_db_user_name: miniflux_db_user
##### DB Password
miniflux_db_user_password: qqqqqqqqqqqqq
##### Database
miniflux_db: miniflux_db
##### Username Miniflux Admin
miniflux_admin_name: admin
##### Password Miniflux Admin
miniflux_admin_passwort: hallowelt
##### Port for Miniflux Frontend
miniflux_port: 8080
@@ -0,0 +1,11 @@
---
- name: start_miniflux.service
become: yes
systemd:
name: miniflux
state: restarted
enabled: yes
# wait 15 seconds(for systemd)
- name: miniflux_wait
wait_for:
timeout: 15
@@ -0,0 +1,43 @@
- name: add Apt-key for miniflux-repo
become: yes
apt_key:
url: https://apt.miniflux.app/KEY.gpg
state: present
- name: add miniflux-repo
become: yes
apt_repository:
repo: 'deb https://apt.miniflux.app/ /'
state: present
filename: miniflux_repo
update_cache: yes
- name: install miniflux
become: yes
apt:
name: miniflux
state: present
- name: add miniflux linux_user
become: yes
user:
name: "{{ miniflux_linux_user }}"
home: "/var/empty"
create_home: "no"
system: "yes"
shell: "/bin/false"
- name: create directory "/etc/miniflux.d"
become: yes
file:
path: /etc/miniflux.d
state: directory
- name: copy miniflux.conf
become: yes
template:
src: "miniflux.conf"
dest: "/etc/miniflux.conf"
notify:
- start_miniflux.service
- miniflux_wait
@@ -0,0 +1,18 @@
# See https://docs.miniflux.app/
LISTEN_ADDR=0.0.0.0:{{ miniflux_port }}
DATABASE_URL=user={{ miniflux_db_user_name }} password={{ miniflux_db_user_password }} dbname={{ miniflux_db }} sslmode=disable
POLLING_FREQUENCY=15
PROXY_IMAGES=http-only
# Run SQL migrations automatically:
RUN_MIGRATIONS=1
CREATE_ADMIN=1
ADMIN_USERNAME={{ miniflux_admin_name }}
ADMIN_PASSWORD={{ miniflux_admin_passwort }}
POLLING_FREQUENCY=10
# Options: https://miniflux.app/miniflux.1.html
+2
View File
@@ -0,0 +1,2 @@
miniflux.example.org
reverse_proxy miniflux:8080
+13
View File
@@ -0,0 +1,13 @@
Docker-Compose Examples
=======================
Here are few Docker Compose examples:
- `basic.yml`: Basic example
- `caddy.yml`: Use Caddy as reverse-proxy with automatic HTTPS
- `traefik.yml`: Use Traefik as reverse-proxy with automatic HTTPS
```bash
docker-compose -f basic.yml up -d db
docker-compose -f basic.yml up
```
+25
View File
@@ -0,0 +1,25 @@
version: '3.3'
services:
miniflux:
image: miniflux/miniflux:latest
container_name: miniflux
ports:
- "80:8080"
depends_on:
- db
environment:
- DATABASE_URL=postgres://miniflux:secret@db/miniflux?sslmode=disable
- RUN_MIGRATIONS=1
- CREATE_ADMIN=1
- ADMIN_USERNAME=admin
- ADMIN_PASSWORD=test123
db:
image: postgres:latest
container_name: postgres
environment:
- POSTGRES_USER=miniflux
- POSTGRES_PASSWORD=secret
volumes:
- miniflux-db:/var/lib/postgresql/data
volumes:
miniflux-db:
+38
View File
@@ -0,0 +1,38 @@
version: '3.3'
services:
caddy:
image: caddy:2
container_name: caddy
depends_on:
- miniflux
ports:
- "80:80"
- "443:443"
volumes:
- $PWD/Caddyfile:/etc/caddy/Caddyfile
- caddy_data:/data
- caddy_config:/config
miniflux:
image: miniflux/miniflux:latest
container_name: miniflux
depends_on:
- db
environment:
- DATABASE_URL=postgres://miniflux:secret@db/miniflux?sslmode=disable
- RUN_MIGRATIONS=1
- CREATE_ADMIN=1
- ADMIN_USERNAME=admin
- ADMIN_PASSWORD=test123
- BASE_URL=https://miniflux.example.org
db:
image: postgres:latest
container_name: postgres
environment:
- POSTGRES_USER=miniflux
- POSTGRES_PASSWORD=secret
volumes:
- miniflux-db:/var/lib/postgresql/data
volumes:
miniflux-db:
caddy_data:
caddy_config:
+48
View File
@@ -0,0 +1,48 @@
version: '3.3'
services:
traefik:
image: "traefik:v2.3"
container_name: traefik
command:
- "--providers.docker=true"
- "--providers.docker.exposedbydefault=false"
- "--entrypoints.websecure.address=:443"
- "--certificatesresolvers.myresolver.acme.tlschallenge=true"
- "--certificatesresolvers.myresolver.acme.email=postmaster@example.com"
- "--certificatesresolvers.myresolver.acme.storage=/letsencrypt/acme.json"
depends_on:
- miniflux
ports:
- "443:443"
volumes:
- "./letsencrypt:/letsencrypt"
- "/var/run/docker.sock:/var/run/docker.sock:ro"
miniflux:
image: miniflux/miniflux:latest
container_name: miniflux
depends_on:
- db
expose:
- "8080"
environment:
- DATABASE_URL=postgres://miniflux:secret@db/miniflux?sslmode=disable
- RUN_MIGRATIONS=1
- CREATE_ADMIN=1
- ADMIN_USERNAME=admin
- ADMIN_PASSWORD=test123
- BASE_URL=https://miniflux.example.org
labels:
- "traefik.enable=true"
- "traefik.http.routers.miniflux.rule=Host(`miniflux.example.org`)"
- "traefik.http.routers.miniflux.entrypoints=websecure"
- "traefik.http.routers.miniflux.tls.certresolver=myresolver"
db:
image: postgres:latest
container_name: postgres
environment:
- POSTGRES_USER=miniflux
- POSTGRES_PASSWORD=secret
volumes:
- miniflux-db:/var/lib/postgresql/data
volumes:
miniflux-db:
+1
View File
@@ -0,0 +1 @@
Grafana Dashboard for Miniflux
File diff suppressed because it is too large Load Diff
+7 -1
View File
@@ -2,12 +2,13 @@
// Use of this source code is governed by the Apache 2.0
// license that can be found in the LICENSE file.
package crypto
package crypto // import "miniflux.app/crypto"
import (
"crypto/rand"
"crypto/sha256"
"encoding/base64"
"encoding/hex"
"fmt"
)
@@ -36,3 +37,8 @@ func GenerateRandomBytes(size int) []byte {
func GenerateRandomString(size int) string {
return base64.URLEncoding.EncodeToString(GenerateRandomBytes(size))
}
// GenerateRandomStringHex returns a random hexadecimal string.
func GenerateRandomStringHex(size int) string {
return hex.EncodeToString(GenerateRandomBytes(size))
}
+3 -3
View File
@@ -1,10 +1,10 @@
// Copyright 2018 Frédéric Guillot. All rights reserved.
// Use of this source code is governed by the MIT license
// that can be found in the LICENSE file.
// Use of this source code is governed by the Apache 2.0
// license that can be found in the LICENSE file.
/*
Package crypto implements helpers related to cryptography.
*/
package crypto
package crypto // import "miniflux.app/crypto"
-50
View File
@@ -1,50 +0,0 @@
// Copyright 2018 Frédéric Guillot. All rights reserved.
// Use of this source code is governed by the Apache 2.0
// license that can be found in the LICENSE file.
package daemon
import (
"context"
"os"
"os/signal"
"syscall"
"time"
"github.com/miniflux/miniflux/config"
"github.com/miniflux/miniflux/locale"
"github.com/miniflux/miniflux/logger"
"github.com/miniflux/miniflux/reader/feed"
"github.com/miniflux/miniflux/scheduler"
"github.com/miniflux/miniflux/storage"
)
// Run starts the daemon.
func Run(cfg *config.Config, store *storage.Storage) {
logger.Info("Starting Miniflux...")
stop := make(chan os.Signal, 1)
signal.Notify(stop, os.Interrupt)
signal.Notify(stop, syscall.SIGTERM)
translator := locale.Load()
feedHandler := feed.NewFeedHandler(store, translator)
pool := scheduler.NewWorkerPool(feedHandler, cfg.WorkerPoolSize())
server := newServer(cfg, store, pool, feedHandler, translator)
scheduler.NewFeedScheduler(
store,
pool,
cfg.PollingFrequency(),
cfg.BatchSize(),
)
scheduler.NewSessionScheduler(store, cfg.SessionCleanupFrequency())
<-stop
logger.Info("Shutting down the server...")
ctx, _ := context.WithTimeout(context.Background(), 5*time.Second)
server.Shutdown(ctx)
store.Close()
logger.Info("Server gracefully stopped")
}
-10
View File
@@ -1,10 +0,0 @@
// Copyright 2018 Frédéric Guillot. All rights reserved.
// Use of this source code is governed by the MIT license
// that can be found in the LICENSE file.
/*
Package daemon handles the main application process.
*/
package daemon
-171
View File
@@ -1,171 +0,0 @@
// Copyright 2018 Frédéric Guillot. All rights reserved.
// Use of this source code is governed by the Apache 2.0
// license that can be found in the LICENSE file.
package daemon
import (
"net/http"
"github.com/miniflux/miniflux/api"
"github.com/miniflux/miniflux/config"
"github.com/miniflux/miniflux/fever"
"github.com/miniflux/miniflux/http/handler"
"github.com/miniflux/miniflux/http/middleware"
"github.com/miniflux/miniflux/locale"
"github.com/miniflux/miniflux/reader/feed"
"github.com/miniflux/miniflux/reader/opml"
"github.com/miniflux/miniflux/scheduler"
"github.com/miniflux/miniflux/storage"
"github.com/miniflux/miniflux/template"
"github.com/miniflux/miniflux/ui"
"github.com/gorilla/mux"
)
func routes(cfg *config.Config, store *storage.Storage, feedHandler *feed.Handler, pool *scheduler.WorkerPool, translator *locale.Translator) *mux.Router {
router := mux.NewRouter()
templateEngine := template.NewEngine(cfg, router, translator)
apiController := api.NewController(store, feedHandler)
feverController := fever.NewController(store)
uiController := ui.NewController(cfg, store, pool, feedHandler, opml.NewHandler(store))
apiHandler := handler.NewHandler(cfg, store, router, templateEngine, translator, middleware.NewChain(
middleware.NewBasicAuthMiddleware(store).Handler,
))
feverHandler := handler.NewHandler(cfg, store, router, templateEngine, translator, middleware.NewChain(
middleware.NewFeverMiddleware(store).Handler,
))
uiHandler := handler.NewHandler(cfg, store, router, templateEngine, translator, middleware.NewChain(
middleware.NewUserSessionMiddleware(store, router).Handler,
middleware.NewSessionMiddleware(cfg, store).Handler,
))
if cfg.BasePath() != "" {
router = router.PathPrefix(cfg.BasePath()).Subrouter()
}
router.Handle("/fever/", feverHandler.Use(feverController.Handler)).Name("feverEndpoint")
router.Handle("/v1/users", apiHandler.Use(apiController.CreateUser)).Methods("POST")
router.Handle("/v1/users", apiHandler.Use(apiController.Users)).Methods("GET")
router.Handle("/v1/users/{userID:[0-9]+}", apiHandler.Use(apiController.UserByID)).Methods("GET")
router.Handle("/v1/users/{userID:[0-9]+}", apiHandler.Use(apiController.UpdateUser)).Methods("PUT")
router.Handle("/v1/users/{userID:[0-9]+}", apiHandler.Use(apiController.RemoveUser)).Methods("DELETE")
router.Handle("/v1/users/{username}", apiHandler.Use(apiController.UserByUsername)).Methods("GET")
router.Handle("/v1/categories", apiHandler.Use(apiController.CreateCategory)).Methods("POST")
router.Handle("/v1/categories", apiHandler.Use(apiController.GetCategories)).Methods("GET")
router.Handle("/v1/categories/{categoryID}", apiHandler.Use(apiController.UpdateCategory)).Methods("PUT")
router.Handle("/v1/categories/{categoryID}", apiHandler.Use(apiController.RemoveCategory)).Methods("DELETE")
router.Handle("/v1/discover", apiHandler.Use(apiController.GetSubscriptions)).Methods("POST")
router.Handle("/v1/feeds", apiHandler.Use(apiController.CreateFeed)).Methods("POST")
router.Handle("/v1/feeds", apiHandler.Use(apiController.GetFeeds)).Methods("Get")
router.Handle("/v1/feeds/{feedID}/refresh", apiHandler.Use(apiController.RefreshFeed)).Methods("PUT")
router.Handle("/v1/feeds/{feedID}", apiHandler.Use(apiController.GetFeed)).Methods("GET")
router.Handle("/v1/feeds/{feedID}", apiHandler.Use(apiController.UpdateFeed)).Methods("PUT")
router.Handle("/v1/feeds/{feedID}", apiHandler.Use(apiController.RemoveFeed)).Methods("DELETE")
router.Handle("/v1/feeds/{feedID}/icon", apiHandler.Use(apiController.FeedIcon)).Methods("GET")
router.Handle("/v1/export", apiHandler.Use(apiController.Export)).Methods("GET")
router.Handle("/v1/feeds/{feedID}/entries", apiHandler.Use(apiController.GetFeedEntries)).Methods("GET")
router.Handle("/v1/feeds/{feedID}/entries/{entryID}", apiHandler.Use(apiController.GetFeedEntry)).Methods("GET")
router.Handle("/v1/entries", apiHandler.Use(apiController.GetEntries)).Methods("GET")
router.Handle("/v1/entries", apiHandler.Use(apiController.SetEntryStatus)).Methods("PUT")
router.Handle("/v1/entries/{entryID}", apiHandler.Use(apiController.GetEntry)).Methods("GET")
router.Handle("/v1/entries/{entryID}/bookmark", apiHandler.Use(apiController.ToggleBookmark)).Methods("PUT")
router.Handle("/stylesheets/{name}.css", uiHandler.Use(uiController.Stylesheet)).Name("stylesheet").Methods("GET")
router.Handle("/js", uiHandler.Use(uiController.Javascript)).Name("javascript").Methods("GET")
router.Handle("/favicon.ico", uiHandler.Use(uiController.Favicon)).Name("favicon").Methods("GET")
router.Handle("/icon/{filename}", uiHandler.Use(uiController.AppIcon)).Name("appIcon").Methods("GET")
router.Handle("/manifest.json", uiHandler.Use(uiController.WebManifest)).Name("webManifest").Methods("GET")
router.Handle("/subscribe", uiHandler.Use(uiController.AddSubscription)).Name("addSubscription").Methods("GET")
router.Handle("/subscribe", uiHandler.Use(uiController.SubmitSubscription)).Name("submitSubscription").Methods("POST")
router.Handle("/subscriptions", uiHandler.Use(uiController.ChooseSubscription)).Name("chooseSubscription").Methods("POST")
router.Handle("/mark-all-as-read", uiHandler.Use(uiController.MarkAllAsRead)).Name("markAllAsRead").Methods("GET")
router.Handle("/unread", uiHandler.Use(uiController.ShowUnreadPage)).Name("unread").Methods("GET")
router.Handle("/history", uiHandler.Use(uiController.ShowHistoryPage)).Name("history").Methods("GET")
router.Handle("/starred", uiHandler.Use(uiController.ShowStarredPage)).Name("starred").Methods("GET")
router.Handle("/feed/{feedID}/refresh", uiHandler.Use(uiController.RefreshFeed)).Name("refreshFeed").Methods("GET")
router.Handle("/feed/{feedID}/edit", uiHandler.Use(uiController.EditFeed)).Name("editFeed").Methods("GET")
router.Handle("/feed/{feedID}/remove", uiHandler.Use(uiController.RemoveFeed)).Name("removeFeed").Methods("POST")
router.Handle("/feed/{feedID}/update", uiHandler.Use(uiController.UpdateFeed)).Name("updateFeed").Methods("POST")
router.Handle("/feed/{feedID}/entries", uiHandler.Use(uiController.ShowFeedEntries)).Name("feedEntries").Methods("GET")
router.Handle("/feeds", uiHandler.Use(uiController.ShowFeedsPage)).Name("feeds").Methods("GET")
router.Handle("/feeds/refresh", uiHandler.Use(uiController.RefreshAllFeeds)).Name("refreshAllFeeds").Methods("GET")
router.Handle("/unread/entry/{entryID}", uiHandler.Use(uiController.ShowUnreadEntry)).Name("unreadEntry").Methods("GET")
router.Handle("/history/entry/{entryID}", uiHandler.Use(uiController.ShowReadEntry)).Name("readEntry").Methods("GET")
router.Handle("/history/flush", uiHandler.Use(uiController.FlushHistory)).Name("flushHistory").Methods("GET")
router.Handle("/feed/{feedID}/entry/{entryID}", uiHandler.Use(uiController.ShowFeedEntry)).Name("feedEntry").Methods("GET")
router.Handle("/category/{categoryID}/entry/{entryID}", uiHandler.Use(uiController.ShowCategoryEntry)).Name("categoryEntry").Methods("GET")
router.Handle("/starred/entry/{entryID}", uiHandler.Use(uiController.ShowStarredEntry)).Name("starredEntry").Methods("GET")
router.Handle("/entry/status", uiHandler.Use(uiController.UpdateEntriesStatus)).Name("updateEntriesStatus").Methods("POST")
router.Handle("/entry/save/{entryID}", uiHandler.Use(uiController.SaveEntry)).Name("saveEntry").Methods("POST")
router.Handle("/entry/download/{entryID}", uiHandler.Use(uiController.FetchContent)).Name("fetchContent").Methods("POST")
router.Handle("/entry/bookmark/{entryID}", uiHandler.Use(uiController.ToggleBookmark)).Name("toggleBookmark").Methods("POST")
router.Handle("/categories", uiHandler.Use(uiController.ShowCategories)).Name("categories").Methods("GET")
router.Handle("/category/create", uiHandler.Use(uiController.CreateCategory)).Name("createCategory").Methods("GET")
router.Handle("/category/save", uiHandler.Use(uiController.SaveCategory)).Name("saveCategory").Methods("POST")
router.Handle("/category/{categoryID}/entries", uiHandler.Use(uiController.ShowCategoryEntries)).Name("categoryEntries").Methods("GET")
router.Handle("/category/{categoryID}/edit", uiHandler.Use(uiController.EditCategory)).Name("editCategory").Methods("GET")
router.Handle("/category/{categoryID}/update", uiHandler.Use(uiController.UpdateCategory)).Name("updateCategory").Methods("POST")
router.Handle("/category/{categoryID}/remove", uiHandler.Use(uiController.RemoveCategory)).Name("removeCategory").Methods("POST")
router.Handle("/feed/icon/{iconID}", uiHandler.Use(uiController.ShowIcon)).Name("icon").Methods("GET")
router.Handle("/proxy/{encodedURL}", uiHandler.Use(uiController.ImageProxy)).Name("proxy").Methods("GET")
router.Handle("/users", uiHandler.Use(uiController.ShowUsers)).Name("users").Methods("GET")
router.Handle("/user/create", uiHandler.Use(uiController.CreateUser)).Name("createUser").Methods("GET")
router.Handle("/user/save", uiHandler.Use(uiController.SaveUser)).Name("saveUser").Methods("POST")
router.Handle("/users/{userID}/edit", uiHandler.Use(uiController.EditUser)).Name("editUser").Methods("GET")
router.Handle("/users/{userID}/update", uiHandler.Use(uiController.UpdateUser)).Name("updateUser").Methods("POST")
router.Handle("/users/{userID}/remove", uiHandler.Use(uiController.RemoveUser)).Name("removeUser").Methods("POST")
router.Handle("/about", uiHandler.Use(uiController.AboutPage)).Name("about").Methods("GET")
router.Handle("/settings", uiHandler.Use(uiController.ShowSettings)).Name("settings").Methods("GET")
router.Handle("/settings", uiHandler.Use(uiController.UpdateSettings)).Name("updateSettings").Methods("POST")
router.Handle("/bookmarklet", uiHandler.Use(uiController.Bookmarklet)).Name("bookmarklet").Methods("GET")
router.Handle("/integrations", uiHandler.Use(uiController.ShowIntegrations)).Name("integrations").Methods("GET")
router.Handle("/integration", uiHandler.Use(uiController.UpdateIntegration)).Name("updateIntegration").Methods("POST")
router.Handle("/sessions", uiHandler.Use(uiController.ShowSessions)).Name("sessions").Methods("GET")
router.Handle("/sessions/{sessionID}/remove", uiHandler.Use(uiController.RemoveSession)).Name("removeSession").Methods("POST")
router.Handle("/export", uiHandler.Use(uiController.Export)).Name("export").Methods("GET")
router.Handle("/import", uiHandler.Use(uiController.Import)).Name("import").Methods("GET")
router.Handle("/upload", uiHandler.Use(uiController.UploadOPML)).Name("uploadOPML").Methods("POST")
router.Handle("/oauth2/{provider}/unlink", uiHandler.Use(uiController.OAuth2Unlink)).Name("oauth2Unlink").Methods("GET")
router.Handle("/oauth2/{provider}/redirect", uiHandler.Use(uiController.OAuth2Redirect)).Name("oauth2Redirect").Methods("GET")
router.Handle("/oauth2/{provider}/callback", uiHandler.Use(uiController.OAuth2Callback)).Name("oauth2Callback").Methods("GET")
router.Handle("/login", uiHandler.Use(uiController.CheckLogin)).Name("checkLogin").Methods("POST")
router.Handle("/logout", uiHandler.Use(uiController.Logout)).Name("logout").Methods("GET")
router.Handle("/", uiHandler.Use(uiController.ShowLoginPage)).Name("login").Methods("GET")
router.HandleFunc("/healthcheck", func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("OK"))
})
router.HandleFunc("/robots.txt", func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "text/plain")
w.Write([]byte("User-agent: *\nDisallow: /"))
})
return router
}
-71
View File
@@ -1,71 +0,0 @@
// Copyright 2018 Frédéric Guillot. All rights reserved.
// Use of this source code is governed by the Apache 2.0
// license that can be found in the LICENSE file.
package daemon
import (
"crypto/tls"
"net/http"
"time"
"github.com/miniflux/miniflux/config"
"github.com/miniflux/miniflux/locale"
"github.com/miniflux/miniflux/logger"
"github.com/miniflux/miniflux/reader/feed"
"github.com/miniflux/miniflux/scheduler"
"github.com/miniflux/miniflux/storage"
"golang.org/x/crypto/acme/autocert"
)
func newServer(cfg *config.Config, store *storage.Storage, pool *scheduler.WorkerPool, feedHandler *feed.Handler, translator *locale.Translator) *http.Server {
certFile := cfg.CertFile()
keyFile := cfg.KeyFile()
certDomain := cfg.CertDomain()
certCache := cfg.CertCache()
server := &http.Server{
ReadTimeout: 5 * time.Second,
WriteTimeout: 10 * time.Second,
IdleTimeout: 60 * time.Second,
Addr: cfg.ListenAddr(),
Handler: routes(cfg, store, feedHandler, pool, translator),
}
if certDomain != "" && certCache != "" {
cfg.IsHTTPS = true
server.Addr = ":https"
certManager := autocert.Manager{
Cache: autocert.DirCache(certCache),
Prompt: autocert.AcceptTOS,
HostPolicy: autocert.HostWhitelist(certDomain),
}
// Handle http-01 challenge.
s := &http.Server{
Handler: certManager.HTTPHandler(nil),
Addr: ":http",
}
go s.ListenAndServe()
go func() {
logger.Info(`Listening on "%s" by using auto-configured certificate for "%s"`, server.Addr, certDomain)
logger.Fatal(server.Serve(certManager.Listener()).Error())
}()
} else if certFile != "" && keyFile != "" {
server.TLSConfig = &tls.Config{MinVersion: tls.VersionTLS12}
cfg.IsHTTPS = true
go func() {
logger.Info(`Listening on "%s" by using certificate "%s" and key "%s"`, server.Addr, certFile, keyFile)
logger.Fatal(server.ListenAndServeTLS(certFile, keyFile).Error())
}()
} else {
go func() {
logger.Info(`Listening on "%s" without TLS`, server.Addr)
logger.Fatal(server.ListenAndServe().Error())
}()
}
return server
}
+76
View File
@@ -0,0 +1,76 @@
// Copyright 2018 Frédéric Guillot. All rights reserved.
// Use of this source code is governed by the Apache 2.0
// license that can be found in the LICENSE file.
package database // import "miniflux.app/database"
import (
"database/sql"
"fmt"
// Postgresql driver import
_ "github.com/lib/pq"
)
// NewConnectionPool configures the database connection pool.
func NewConnectionPool(dsn string, minConnections, maxConnections int) (*sql.DB, error) {
db, err := sql.Open("postgres", dsn)
if err != nil {
return nil, err
}
db.SetMaxOpenConns(maxConnections)
db.SetMaxIdleConns(minConnections)
return db, nil
}
// Migrate executes database migrations.
func Migrate(db *sql.DB) error {
var currentVersion int
db.QueryRow(`SELECT version FROM schema_version`).Scan(&currentVersion)
fmt.Println("-> Current schema version:", currentVersion)
fmt.Println("-> Latest schema version:", schemaVersion)
for version := currentVersion; version < schemaVersion; version++ {
newVersion := version + 1
fmt.Println("* Migrating to version:", newVersion)
tx, err := db.Begin()
if err != nil {
return fmt.Errorf("[Migration v%d] %v", newVersion, err)
}
if err := migrations[version](tx); err != nil {
tx.Rollback()
return fmt.Errorf("[Migration v%d] %v", newVersion, err)
}
if _, err := tx.Exec(`DELETE FROM schema_version`); err != nil {
tx.Rollback()
return fmt.Errorf("[Migration v%d] %v", newVersion, err)
}
if _, err := tx.Exec(`INSERT INTO schema_version (version) VALUES ($1)`, newVersion); err != nil {
tx.Rollback()
return fmt.Errorf("[Migration v%d] %v", newVersion, err)
}
if err := tx.Commit(); err != nil {
return fmt.Errorf("[Migration v%d] %v", newVersion, err)
}
}
return nil
}
// IsSchemaUpToDate checks if the database schema is up to date.
func IsSchemaUpToDate(db *sql.DB) error {
var currentVersion int
db.QueryRow(`SELECT version FROM schema_version`).Scan(&currentVersion)
if currentVersion < schemaVersion {
return fmt.Errorf(`the database schema is not up to date: current=v%d expected=v%d`, currentVersion, schemaVersion)
}
return nil
}
+517
View File
@@ -0,0 +1,517 @@
// Copyright 2020 Frédéric Guillot. All rights reserved.
// Use of this source code is governed by the Apache 2.0
// license that can be found in the LICENSE file.
package database // import "miniflux.app/database"
import (
"database/sql"
)
var schemaVersion = len(migrations)
var migrations = []func(tx *sql.Tx) error{
func(tx *sql.Tx) (err error) {
sql := `
CREATE TABLE schema_version (
version text not null
);
CREATE TABLE users (
id serial not null,
username text not null unique,
password text,
is_admin bool default 'f',
language text default 'en_US',
timezone text default 'UTC',
theme text default 'default',
last_login_at timestamp with time zone,
primary key (id)
);
CREATE TABLE sessions (
id serial not null,
user_id int not null,
token text not null unique,
created_at timestamp with time zone default now(),
user_agent text,
ip text,
primary key (id),
unique (user_id, token),
foreign key (user_id) references users(id) on delete cascade
);
CREATE TABLE categories (
id serial not null,
user_id int not null,
title text not null,
primary key (id),
unique (user_id, title),
foreign key (user_id) references users(id) on delete cascade
);
CREATE TABLE feeds (
id bigserial not null,
user_id int not null,
category_id int not null,
title text not null,
feed_url text not null,
site_url text not null,
checked_at timestamp with time zone default now(),
etag_header text default '',
last_modified_header text default '',
parsing_error_msg text default '',
parsing_error_count int default 0,
primary key (id),
unique (user_id, feed_url),
foreign key (user_id) references users(id) on delete cascade,
foreign key (category_id) references categories(id) on delete cascade
);
CREATE TYPE entry_status as enum('unread', 'read', 'removed');
CREATE TABLE entries (
id bigserial not null,
user_id int not null,
feed_id bigint not null,
hash text not null,
published_at timestamp with time zone not null,
title text not null,
url text not null,
author text,
content text,
status entry_status default 'unread',
primary key (id),
unique (feed_id, hash),
foreign key (user_id) references users(id) on delete cascade,
foreign key (feed_id) references feeds(id) on delete cascade
);
CREATE INDEX entries_feed_idx on entries using btree(feed_id);
CREATE TABLE enclosures (
id bigserial not null,
user_id int not null,
entry_id bigint not null,
url text not null,
size int default 0,
mime_type text default '',
primary key (id),
foreign key (user_id) references users(id) on delete cascade,
foreign key (entry_id) references entries(id) on delete cascade
);
CREATE TABLE icons (
id bigserial not null,
hash text not null unique,
mime_type text not null,
content bytea not null,
primary key (id)
);
CREATE TABLE feed_icons (
feed_id bigint not null,
icon_id bigint not null,
primary key(feed_id, icon_id),
foreign key (feed_id) references feeds(id) on delete cascade,
foreign key (icon_id) references icons(id) on delete cascade
);
`
_, err = tx.Exec(sql)
return err
},
func(tx *sql.Tx) (err error) {
sql := `
CREATE EXTENSION IF NOT EXISTS hstore;
ALTER TABLE users ADD COLUMN extra hstore;
CREATE INDEX users_extra_idx ON users using gin(extra);
`
_, err = tx.Exec(sql)
return err
},
func(tx *sql.Tx) (err error) {
sql := `
CREATE TABLE tokens (
id text not null,
value text not null,
created_at timestamp with time zone not null default now(),
primary key(id, value)
);
`
_, err = tx.Exec(sql)
return err
},
func(tx *sql.Tx) (err error) {
sql := `
CREATE TYPE entry_sorting_direction AS enum('asc', 'desc');
ALTER TABLE users ADD COLUMN entry_direction entry_sorting_direction default 'asc';
`
_, err = tx.Exec(sql)
return err
},
func(tx *sql.Tx) (err error) {
sql := `
CREATE TABLE integrations (
user_id int not null,
pinboard_enabled bool default 'f',
pinboard_token text default '',
pinboard_tags text default 'miniflux',
pinboard_mark_as_unread bool default 'f',
instapaper_enabled bool default 'f',
instapaper_username text default '',
instapaper_password text default '',
fever_enabled bool default 'f',
fever_username text default '',
fever_password text default '',
fever_token text default '',
primary key(user_id)
);
`
_, err = tx.Exec(sql)
return err
},
func(tx *sql.Tx) (err error) {
sql := `ALTER TABLE feeds ADD COLUMN scraper_rules text default ''`
_, err = tx.Exec(sql)
return err
},
func(tx *sql.Tx) (err error) {
sql := `ALTER TABLE feeds ADD COLUMN rewrite_rules text default ''`
_, err = tx.Exec(sql)
return err
},
func(tx *sql.Tx) (err error) {
sql := `ALTER TABLE feeds ADD COLUMN crawler boolean default 'f'`
_, err = tx.Exec(sql)
return err
},
func(tx *sql.Tx) (err error) {
sql := `ALTER TABLE sessions rename to user_sessions`
_, err = tx.Exec(sql)
return err
},
func(tx *sql.Tx) (err error) {
sql := `
DROP TABLE tokens;
CREATE TABLE sessions (
id text not null,
data jsonb not null,
created_at timestamp with time zone not null default now(),
primary key(id)
);
`
_, err = tx.Exec(sql)
return err
},
func(tx *sql.Tx) (err error) {
sql := `
ALTER TABLE integrations ADD COLUMN wallabag_enabled bool default 'f';
ALTER TABLE integrations ADD COLUMN wallabag_url text default '';
ALTER TABLE integrations ADD COLUMN wallabag_client_id text default '';
ALTER TABLE integrations ADD COLUMN wallabag_client_secret text default '';
ALTER TABLE integrations ADD COLUMN wallabag_username text default '';
ALTER TABLE integrations ADD COLUMN wallabag_password text default '';
`
_, err = tx.Exec(sql)
return err
},
func(tx *sql.Tx) (err error) {
sql := `ALTER TABLE entries ADD COLUMN starred bool default 'f'`
_, err = tx.Exec(sql)
return err
},
func(tx *sql.Tx) (err error) {
sql := `
CREATE INDEX entries_user_status_idx ON entries(user_id, status);
CREATE INDEX feeds_user_category_idx ON feeds(user_id, category_id);
`
_, err = tx.Exec(sql)
return err
},
func(tx *sql.Tx) (err error) {
sql := `
ALTER TABLE integrations ADD COLUMN nunux_keeper_enabled bool default 'f';
ALTER TABLE integrations ADD COLUMN nunux_keeper_url text default '';
ALTER TABLE integrations ADD COLUMN nunux_keeper_api_key text default '';
`
_, err = tx.Exec(sql)
return err
},
func(tx *sql.Tx) (err error) {
sql := `ALTER TABLE enclosures ALTER COLUMN size SET DATA TYPE bigint`
_, err = tx.Exec(sql)
return err
},
func(tx *sql.Tx) (err error) {
sql := `ALTER TABLE entries ADD COLUMN comments_url text default ''`
_, err = tx.Exec(sql)
return err
},
func(tx *sql.Tx) (err error) {
sql := `
ALTER TABLE integrations ADD COLUMN pocket_enabled bool default 'f';
ALTER TABLE integrations ADD COLUMN pocket_access_token text default '';
ALTER TABLE integrations ADD COLUMN pocket_consumer_key text default '';
`
_, err = tx.Exec(sql)
return err
},
func(tx *sql.Tx) (err error) {
sql := `
ALTER TABLE user_sessions ALTER COLUMN ip SET DATA TYPE inet using ip::inet;
`
_, err = tx.Exec(sql)
return err
},
func(tx *sql.Tx) (err error) {
sql := `
ALTER TABLE feeds ADD COLUMN username text default '';
ALTER TABLE feeds ADD COLUMN password text default '';
`
_, err = tx.Exec(sql)
return err
},
func(tx *sql.Tx) (err error) {
sql := `
ALTER TABLE entries ADD COLUMN document_vectors tsvector;
UPDATE entries SET document_vectors = to_tsvector(substring(title || ' ' || coalesce(content, '') for 1000000));
CREATE INDEX document_vectors_idx ON entries USING gin(document_vectors);
`
_, err = tx.Exec(sql)
return err
},
func(tx *sql.Tx) (err error) {
sql := `ALTER TABLE feeds ADD COLUMN user_agent text default ''`
_, err = tx.Exec(sql)
return err
},
func(tx *sql.Tx) (err error) {
sql := `
UPDATE
entries
SET
document_vectors = setweight(to_tsvector(substring(coalesce(title, '') for 1000000)), 'A') || setweight(to_tsvector(substring(coalesce(content, '') for 1000000)), 'B')
`
_, err = tx.Exec(sql)
return err
},
func(tx *sql.Tx) (err error) {
sql := `ALTER TABLE users ADD COLUMN keyboard_shortcuts boolean default 't'`
_, err = tx.Exec(sql)
return err
},
func(tx *sql.Tx) (err error) {
sql := `ALTER TABLE feeds ADD COLUMN disabled boolean default 'f';`
_, err = tx.Exec(sql)
return err
},
func(tx *sql.Tx) (err error) {
sql := `
ALTER TABLE users ALTER COLUMN theme SET DEFAULT 'light_serif';
UPDATE users SET theme='light_serif' WHERE theme='default';
UPDATE users SET theme='light_sans_serif' WHERE theme='sansserif';
UPDATE users SET theme='dark_serif' WHERE theme='black';
`
_, err = tx.Exec(sql)
return err
},
func(tx *sql.Tx) (err error) {
sql := `
ALTER TABLE entries ADD COLUMN changed_at timestamp with time zone;
UPDATE entries SET changed_at = published_at;
ALTER TABLE entries ALTER COLUMN changed_at SET not null;
`
_, err = tx.Exec(sql)
return err
},
func(tx *sql.Tx) (err error) {
sql := `
CREATE TABLE api_keys (
id serial not null,
user_id int not null references users(id) on delete cascade,
token text not null unique,
description text not null,
last_used_at timestamp with time zone,
created_at timestamp with time zone default now(),
primary key(id),
unique (user_id, description)
);
`
_, err = tx.Exec(sql)
return err
},
func(tx *sql.Tx) (err error) {
sql := `
ALTER TABLE entries ADD COLUMN share_code text not null default '';
CREATE UNIQUE INDEX entries_share_code_idx ON entries USING btree(share_code) WHERE share_code <> '';
`
_, err = tx.Exec(sql)
return err
},
func(tx *sql.Tx) (err error) {
sql := `CREATE INDEX enclosures_user_entry_url_idx ON enclosures(user_id, entry_id, md5(url))`
_, err = tx.Exec(sql)
return err
},
func(tx *sql.Tx) (err error) {
sql := `
ALTER TABLE feeds ADD COLUMN next_check_at timestamp with time zone default now();
CREATE INDEX entries_user_feed_idx ON entries (user_id, feed_id);
`
_, err = tx.Exec(sql)
return err
},
func(tx *sql.Tx) (err error) {
sql := `ALTER TABLE feeds ADD COLUMN ignore_http_cache bool default false`
_, err = tx.Exec(sql)
return err
},
func(tx *sql.Tx) (err error) {
sql := `ALTER TABLE users ADD COLUMN entries_per_page int default 100`
_, err = tx.Exec(sql)
return err
},
func(tx *sql.Tx) (err error) {
sql := `ALTER TABLE users ADD COLUMN show_reading_time boolean default 't'`
_, err = tx.Exec(sql)
return err
},
func(tx *sql.Tx) (err error) {
sql := `CREATE INDEX entries_id_user_status_idx ON entries USING btree (id, user_id, status)`
_, err = tx.Exec(sql)
return err
},
func(tx *sql.Tx) (err error) {
sql := `ALTER TABLE feeds ADD COLUMN fetch_via_proxy bool default false`
_, err = tx.Exec(sql)
return err
},
func(tx *sql.Tx) (err error) {
sql := `CREATE INDEX entries_feed_id_status_hash_idx ON entries USING btree (feed_id, status, hash)`
_, err = tx.Exec(sql)
return err
},
func(tx *sql.Tx) (err error) {
sql := `CREATE INDEX entries_user_id_status_starred_idx ON entries (user_id, status, starred)`
_, err = tx.Exec(sql)
return err
},
func(tx *sql.Tx) (err error) {
sql := `ALTER TABLE users ADD COLUMN entry_swipe boolean default 't'`
_, err = tx.Exec(sql)
return err
},
func(tx *sql.Tx) (err error) {
sql := `ALTER TABLE integrations DROP COLUMN fever_password`
_, err = tx.Exec(sql)
return err
},
func(tx *sql.Tx) (err error) {
sql := `
ALTER TABLE feeds
ADD COLUMN blocklist_rules text not null default '',
ADD COLUMN keeplist_rules text not null default ''
`
_, err = tx.Exec(sql)
return err
},
func(tx *sql.Tx) (err error) {
sql := `ALTER TABLE entries ADD COLUMN reading_time int not null default 0`
_, err = tx.Exec(sql)
return err
},
func(tx *sql.Tx) (err error) {
sql := `
ALTER TABLE entries ADD COLUMN created_at timestamp with time zone not null default now();
UPDATE entries SET created_at = published_at;
`
_, err = tx.Exec(sql)
return err
},
func(tx *sql.Tx) (err error) {
_, err = tx.Exec(`
ALTER TABLE users
ADD column stylesheet text not null default '',
ADD column google_id text not null default '',
ADD column openid_connect_id text not null default ''
`)
if err != nil {
return err
}
_, err = tx.Exec(`
DECLARE my_cursor CURSOR FOR
SELECT
id,
COALESCE(extra->'custom_css', '') as custom_css,
COALESCE(extra->'google_id', '') as google_id,
COALESCE(extra->'oidc_id', '') as oidc_id
FROM users
FOR UPDATE
`)
if err != nil {
return err
}
defer tx.Exec("CLOSE my_cursor")
for {
var (
userID int64
customStylesheet string
googleID string
oidcID string
)
if err := tx.QueryRow(`FETCH NEXT FROM my_cursor`).Scan(&userID, &customStylesheet, &googleID, &oidcID); err != nil {
if err == sql.ErrNoRows {
break
}
return err
}
_, err := tx.Exec(
`UPDATE
users
SET
stylesheet=$2,
google_id=$3,
openid_connect_id=$4
WHERE
id=$1
`,
userID, customStylesheet, googleID, oidcID)
if err != nil {
return err
}
}
return err
},
func(tx *sql.Tx) (err error) {
_, err = tx.Exec(`
ALTER TABLE users DROP COLUMN extra;
CREATE UNIQUE INDEX users_google_id_idx ON users(google_id) WHERE google_id <> '';
CREATE UNIQUE INDEX users_openid_connect_id_idx ON users(openid_connect_id) WHERE openid_connect_id <> '';
`)
return err
},
func(tx *sql.Tx) (err error) {
_, err = tx.Exec(`
CREATE INDEX entries_feed_url_idx ON entries(feed_id, url);
CREATE INDEX entries_user_status_feed_idx ON entries(user_id, status, feed_id);
CREATE INDEX entries_user_status_changed_idx ON entries(user_id, status, changed_at);
`)
return err
},
func(tx *sql.Tx) (err error) {
_, err = tx.Exec(`
CREATE TABLE acme_cache (
key varchar(400) not null primary key,
data bytea not null,
updated_at timestamptz not null
);
`)
return err
},
}
+4 -4
View File
@@ -1,10 +1,10 @@
// Copyright 2018 Frédéric Guillot. All rights reserved.
// Use of this source code is governed by the MIT license
// that can be found in the LICENSE file.
// Use of this source code is governed by the Apache 2.0
// license that can be found in the LICENSE file.
/*
Package main implements a feed reader application.
Miniflux is a feed reader application.
*/
package main
package main // import "miniflux.app"
+3 -3
View File
@@ -1,10 +1,10 @@
// Copyright 2018 Frédéric Guillot. All rights reserved.
// Use of this source code is governed by the MIT license
// that can be found in the LICENSE file.
// Use of this source code is governed by the Apache 2.0
// license that can be found in the LICENSE file.
/*
Package errors handles localized errors.
*/
package errors
package errors // import "miniflux.app/errors"
+4 -4
View File
@@ -2,12 +2,12 @@
// Use of this source code is governed by the Apache 2.0
// license that can be found in the LICENSE file.
package errors
package errors // import "miniflux.app/errors"
import (
"fmt"
"github.com/miniflux/miniflux/locale"
"miniflux.app/locale"
)
// LocalizedError represents an error than could be translated to another language.
@@ -22,8 +22,8 @@ func (l LocalizedError) Error() string {
}
// Localize returns the translated error message.
func (l LocalizedError) Localize(translation *locale.Language) string {
return translation.Get(l.message, l.args...)
func (l LocalizedError) Localize(printer *locale.Printer) string {
return printer.Printf(l.message, l.args...)
}
// NewLocalizedError returns a new LocalizedError.
+3 -3
View File
@@ -1,10 +1,10 @@
// Copyright 2018 Frédéric Guillot. All rights reserved.
// Use of this source code is governed by the MIT license
// that can be found in the LICENSE file.
// Use of this source code is governed by the Apache 2.0
// license that can be found in the LICENSE file.
/*
Package fever implements Fever API endpoints.
*/
package fever
package fever // import "miniflux.app/fever"
-631
View File
@@ -1,631 +0,0 @@
// Copyright 2017 Frédéric Guillot. All rights reserved.
// Use of this source code is governed by the Apache 2.0
// license that can be found in the LICENSE file.
package fever
import (
"strconv"
"strings"
"time"
"github.com/miniflux/miniflux/http/handler"
"github.com/miniflux/miniflux/integration"
"github.com/miniflux/miniflux/logger"
"github.com/miniflux/miniflux/model"
"github.com/miniflux/miniflux/storage"
)
type baseResponse struct {
Version int `json:"api_version"`
Authenticated int `json:"auth"`
LastRefresh int64 `json:"last_refreshed_on_time"`
}
func (b *baseResponse) SetCommonValues() {
b.Version = 3
b.Authenticated = 1
b.LastRefresh = time.Now().Unix()
}
/*
The default response is a JSON object containing two members:
api_version contains the version of the API responding (positive integer)
auth whether the request was successfully authenticated (boolean integer)
The API can also return XML by passing xml as the optional value of the api argument like so:
http://yourdomain.com/fever/?api=xml
The top level XML element is named response.
The response to each successfully authenticated request will have auth set to 1 and include
at least one additional member:
last_refreshed_on_time contains the time of the most recently refreshed (not updated)
feed (Unix timestamp/integer)
*/
func newBaseResponse() baseResponse {
r := baseResponse{}
r.SetCommonValues()
return r
}
type groupsResponse struct {
baseResponse
Groups []group `json:"groups"`
FeedsGroups []feedsGroups `json:"feeds_groups"`
}
type feedsResponse struct {
baseResponse
Feeds []feed `json:"feeds"`
FeedsGroups []feedsGroups `json:"feeds_groups"`
}
type faviconsResponse struct {
baseResponse
Favicons []favicon `json:"favicons"`
}
type itemsResponse struct {
baseResponse
Items []item `json:"items"`
Total int `json:"total_items"`
}
type unreadResponse struct {
baseResponse
ItemIDs string `json:"unread_item_ids"`
}
type savedResponse struct {
baseResponse
ItemIDs string `json:"saved_item_ids"`
}
type group struct {
ID int64 `json:"id"`
Title string `json:"title"`
}
type feedsGroups struct {
GroupID int64 `json:"group_id"`
FeedIDs string `json:"feed_ids"`
}
type feed struct {
ID int64 `json:"id"`
FaviconID int64 `json:"favicon_id"`
Title string `json:"title"`
URL string `json:"url"`
SiteURL string `json:"site_url"`
IsSpark int `json:"is_spark"`
LastUpdated int64 `json:"last_updated_on_time"`
}
type item struct {
ID int64 `json:"id"`
FeedID int64 `json:"feed_id"`
Title string `json:"title"`
Author string `json:"author"`
HTML string `json:"html"`
URL string `json:"url"`
IsSaved int `json:"is_saved"`
IsRead int `json:"is_read"`
CreatedAt int64 `json:"created_on_time"`
}
type favicon struct {
ID int64 `json:"id"`
Data string `json:"data"`
}
// Controller implements the Fever API.
type Controller struct {
store *storage.Storage
}
// Handler handles Fever API calls
func (c *Controller) Handler(ctx *handler.Context, request *handler.Request, response *handler.Response) {
switch {
case request.HasQueryParam("groups"):
c.handleGroups(ctx, request, response)
case request.HasQueryParam("feeds"):
c.handleFeeds(ctx, request, response)
case request.HasQueryParam("favicons"):
c.handleFavicons(ctx, request, response)
case request.HasQueryParam("unread_item_ids"):
c.handleUnreadItems(ctx, request, response)
case request.HasQueryParam("saved_item_ids"):
c.handleSavedItems(ctx, request, response)
case request.HasQueryParam("items"):
c.handleItems(ctx, request, response)
case request.FormValue("mark") == "item":
c.handleWriteItems(ctx, request, response)
case request.FormValue("mark") == "feed":
c.handleWriteFeeds(ctx, request, response)
case request.FormValue("mark") == "group":
c.handleWriteGroups(ctx, request, response)
default:
response.JSON().Standard(newBaseResponse())
}
}
/*
A request with the groups argument will return two additional members:
groups contains an array of group objects
feeds_groups contains an array of feeds_group objects
A group object has the following members:
id (positive integer)
title (utf-8 string)
The feeds_group object is documented under “Feeds/Groups Relationships.”
The “Kindling” super group is not included in this response and is composed of all feeds with
an is_spark equal to 0.
The “Sparks” super group is not included in this response and is composed of all feeds with an
is_spark equal to 1.
*/
func (c *Controller) handleGroups(ctx *handler.Context, request *handler.Request, response *handler.Response) {
userID := ctx.UserID()
logger.Debug("[Fever] Fetching groups for userID=%d", userID)
categories, err := c.store.Categories(userID)
if err != nil {
response.JSON().ServerError(err)
return
}
feeds, err := c.store.Feeds(userID)
if err != nil {
response.JSON().ServerError(err)
return
}
var result groupsResponse
for _, category := range categories {
result.Groups = append(result.Groups, group{ID: category.ID, Title: category.Title})
}
result.FeedsGroups = c.buildFeedGroups(feeds)
result.SetCommonValues()
response.JSON().Standard(result)
}
/*
A request with the feeds argument will return two additional members:
feeds contains an array of group objects
feeds_groups contains an array of feeds_group objects
A feed object has the following members:
id (positive integer)
favicon_id (positive integer)
title (utf-8 string)
url (utf-8 string)
site_url (utf-8 string)
is_spark (boolean integer)
last_updated_on_time (Unix timestamp/integer)
The feeds_group object is documented under “Feeds/Groups Relationships.”
The “All Items” super feed is not included in this response and is composed of all items from all feeds
that belong to a given group. For the “Kindling” super group and all user created groups the items
should be limited to feeds with an is_spark equal to 0.
For the “Sparks” super group the items should be limited to feeds with an is_spark equal to 1.
*/
func (c *Controller) handleFeeds(ctx *handler.Context, request *handler.Request, response *handler.Response) {
userID := ctx.UserID()
logger.Debug("[Fever] Fetching feeds for userID=%d", userID)
feeds, err := c.store.Feeds(userID)
if err != nil {
response.JSON().ServerError(err)
return
}
var result feedsResponse
result.Feeds = make([]feed, 0)
for _, f := range feeds {
subscripion := feed{
ID: f.ID,
Title: f.Title,
URL: f.FeedURL,
SiteURL: f.SiteURL,
IsSpark: 0,
LastUpdated: f.CheckedAt.Unix(),
}
if f.Icon != nil {
subscripion.FaviconID = f.Icon.IconID
}
result.Feeds = append(result.Feeds, subscripion)
}
result.FeedsGroups = c.buildFeedGroups(feeds)
result.SetCommonValues()
response.JSON().Standard(result)
}
/*
A request with the favicons argument will return one additional member:
favicons contains an array of favicon objects
A favicon object has the following members:
id (positive integer)
data (base64 encoded image data; prefixed by image type)
An example data value:
image/gif;base64,R0lGODlhAQABAIAAAObm5gAAACH5BAEAAAAALAAAAAABAAEAAAICRAEAOw==
The data member of a favicon object can be used with the data: protocol to embed an image in CSS or HTML.
A PHP/HTML example:
echo '<img src="data:'.$favicon['data'].'">';
*/
func (c *Controller) handleFavicons(ctx *handler.Context, request *handler.Request, response *handler.Response) {
userID := ctx.UserID()
logger.Debug("[Fever] Fetching favicons for userID=%d", userID)
icons, err := c.store.Icons(userID)
if err != nil {
response.JSON().ServerError(err)
return
}
var result faviconsResponse
for _, i := range icons {
result.Favicons = append(result.Favicons, favicon{
ID: i.ID,
Data: i.DataURL(),
})
}
result.SetCommonValues()
response.JSON().Standard(result)
}
/*
A request with the items argument will return two additional members:
items contains an array of item objects
total_items contains the total number of items stored in the database (added in API version 2)
An item object has the following members:
id (positive integer)
feed_id (positive integer)
title (utf-8 string)
author (utf-8 string)
html (utf-8 string)
url (utf-8 string)
is_saved (boolean integer)
is_read (boolean integer)
created_on_time (Unix timestamp/integer)
Most servers wont have enough memory allocated to PHP to dump all items at once.
Three optional arguments control determine the items included in the response.
Use the since_id argument with the highest id of locally cached items to request 50 additional items.
Repeat until the items array in the response is empty.
Use the max_id argument with the lowest id of locally cached items (or 0 initially) to request 50 previous items.
Repeat until the items array in the response is empty. (added in API version 2)
Use the with_ids argument with a comma-separated list of item ids to request (a maximum of 50) specific items.
(added in API version 2)
*/
func (c *Controller) handleItems(ctx *handler.Context, request *handler.Request, response *handler.Response) {
var result itemsResponse
userID := ctx.UserID()
logger.Debug("[Fever] Fetching items for userID=%d", userID)
builder := c.store.NewEntryQueryBuilder(userID)
builder.WithoutStatus(model.EntryStatusRemoved)
builder.WithLimit(50)
builder.WithOrder("id")
builder.WithDirection(model.DefaultSortingDirection)
sinceID := request.QueryIntegerParam("since_id", 0)
if sinceID > 0 {
builder.WithGreaterThanEntryID(int64(sinceID))
}
maxID := request.QueryIntegerParam("max_id", 0)
if maxID > 0 {
builder.WithOffset(maxID)
}
csvItemIDs := request.QueryStringParam("with_ids", "")
if csvItemIDs != "" {
var itemIDs []int64
for _, strItemID := range strings.Split(csvItemIDs, ",") {
strItemID = strings.TrimSpace(strItemID)
itemID, _ := strconv.Atoi(strItemID)
itemIDs = append(itemIDs, int64(itemID))
}
builder.WithEntryIDs(itemIDs)
}
entries, err := builder.GetEntries()
if err != nil {
response.JSON().ServerError(err)
return
}
builder = c.store.NewEntryQueryBuilder(userID)
builder.WithoutStatus(model.EntryStatusRemoved)
result.Total, err = builder.CountEntries()
if err != nil {
response.JSON().ServerError(err)
return
}
for _, entry := range entries {
isRead := 0
if entry.Status == model.EntryStatusRead {
isRead = 1
}
isSaved := 0
if entry.Starred {
isSaved = 1
}
result.Items = append(result.Items, item{
ID: entry.ID,
FeedID: entry.FeedID,
Title: entry.Title,
Author: entry.Author,
HTML: entry.Content,
URL: entry.URL,
IsSaved: isSaved,
IsRead: isRead,
CreatedAt: entry.Date.Unix(),
})
}
result.SetCommonValues()
response.JSON().Standard(result)
}
/*
The unread_item_ids and saved_item_ids arguments can be used to keep your local cache synced
with the remote Fever installation.
A request with the unread_item_ids argument will return one additional member:
unread_item_ids (string/comma-separated list of positive integers)
*/
func (c *Controller) handleUnreadItems(ctx *handler.Context, request *handler.Request, response *handler.Response) {
userID := ctx.UserID()
logger.Debug("[Fever] Fetching unread items for userID=%d", userID)
builder := c.store.NewEntryQueryBuilder(userID)
builder.WithStatus(model.EntryStatusUnread)
entries, err := builder.GetEntries()
if err != nil {
response.JSON().ServerError(err)
return
}
var itemIDs []string
for _, entry := range entries {
itemIDs = append(itemIDs, strconv.FormatInt(entry.ID, 10))
}
var result unreadResponse
result.ItemIDs = strings.Join(itemIDs, ",")
result.SetCommonValues()
response.JSON().Standard(result)
}
/*
The unread_item_ids and saved_item_ids arguments can be used to keep your local cache synced
with the remote Fever installation.
A request with the saved_item_ids argument will return one additional member:
saved_item_ids (string/comma-separated list of positive integers)
*/
func (c *Controller) handleSavedItems(ctx *handler.Context, request *handler.Request, response *handler.Response) {
userID := ctx.UserID()
logger.Debug("[Fever] Fetching saved items for userID=%d", userID)
builder := c.store.NewEntryQueryBuilder(userID)
builder.WithStarred()
entryIDs, err := builder.GetEntryIDs()
if err != nil {
response.JSON().ServerError(err)
return
}
var itemsIDs []string
for _, entryID := range entryIDs {
itemsIDs = append(itemsIDs, strconv.FormatInt(entryID, 10))
}
result := &savedResponse{ItemIDs: strings.Join(itemsIDs, ",")}
result.SetCommonValues()
response.JSON().Standard(result)
}
/*
mark=item
as=? where ? is replaced with read, saved or unsaved
id=? where ? is replaced with the id of the item to modify
*/
func (c *Controller) handleWriteItems(ctx *handler.Context, request *handler.Request, response *handler.Response) {
userID := ctx.UserID()
logger.Debug("[Fever] Receiving mark=item call for userID=%d", userID)
entryID := request.FormIntegerValue("id")
if entryID <= 0 {
return
}
builder := c.store.NewEntryQueryBuilder(userID)
builder.WithEntryID(entryID)
builder.WithoutStatus(model.EntryStatusRemoved)
entry, err := builder.GetEntry()
if err != nil {
response.JSON().ServerError(err)
return
}
if entry == nil {
return
}
switch request.FormValue("as") {
case "read":
c.store.SetEntriesStatus(userID, []int64{entryID}, model.EntryStatusRead)
case "unread":
c.store.SetEntriesStatus(userID, []int64{entryID}, model.EntryStatusUnread)
case "saved", "unsaved":
if err := c.store.ToggleBookmark(userID, entryID); err != nil {
response.JSON().ServerError(err)
return
}
settings, err := c.store.Integration(userID)
if err != nil {
response.JSON().ServerError(err)
return
}
go func() {
integration.SendEntry(entry, settings)
}()
}
response.JSON().Standard(newBaseResponse())
}
/*
mark=? where ? is replaced with feed or group
as=read
id=? where ? is replaced with the id of the feed or group to modify
before=? where ? is replaced with the Unix timestamp of the the local clients most recent items API request
*/
func (c *Controller) handleWriteFeeds(ctx *handler.Context, request *handler.Request, response *handler.Response) {
userID := ctx.UserID()
logger.Debug("[Fever] Receiving mark=feed call for userID=%d", userID)
feedID := request.FormIntegerValue("id")
if feedID <= 0 {
return
}
builder := c.store.NewEntryQueryBuilder(userID)
builder.WithStatus(model.EntryStatusUnread)
builder.WithFeedID(feedID)
before := request.FormIntegerValue("before")
if before > 0 {
t := time.Unix(before, 0)
builder.Before(&t)
}
entryIDs, err := builder.GetEntryIDs()
if err != nil {
response.JSON().ServerError(err)
return
}
err = c.store.SetEntriesStatus(userID, entryIDs, model.EntryStatusRead)
if err != nil {
response.JSON().ServerError(err)
return
}
response.JSON().Standard(newBaseResponse())
}
/*
mark=? where ? is replaced with feed or group
as=read
id=? where ? is replaced with the id of the feed or group to modify
before=? where ? is replaced with the Unix timestamp of the the local clients most recent items API request
*/
func (c *Controller) handleWriteGroups(ctx *handler.Context, request *handler.Request, response *handler.Response) {
userID := ctx.UserID()
logger.Debug("[Fever] Receiving mark=group call for userID=%d", userID)
groupID := request.FormIntegerValue("id")
if groupID < 0 {
return
}
builder := c.store.NewEntryQueryBuilder(userID)
builder.WithStatus(model.EntryStatusUnread)
builder.WithCategoryID(groupID)
before := request.FormIntegerValue("before")
if before > 0 {
t := time.Unix(before, 0)
builder.Before(&t)
}
entryIDs, err := builder.GetEntryIDs()
if err != nil {
response.JSON().ServerError(err)
return
}
err = c.store.SetEntriesStatus(userID, entryIDs, model.EntryStatusRead)
if err != nil {
response.JSON().ServerError(err)
return
}
response.JSON().Standard(newBaseResponse())
}
/*
A feeds_group object has the following members:
group_id (positive integer)
feed_ids (string/comma-separated list of positive integers)
*/
func (c *Controller) buildFeedGroups(feeds model.Feeds) []feedsGroups {
feedsGroupedByCategory := make(map[int64][]string)
for _, feed := range feeds {
feedsGroupedByCategory[feed.Category.ID] = append(feedsGroupedByCategory[feed.Category.ID], strconv.FormatInt(feed.ID, 10))
}
result := make([]feedsGroups, 0)
for categoryID, feedIDs := range feedsGroupedByCategory {
result = append(result, feedsGroups{
GroupID: categoryID,
FeedIDs: strings.Join(feedIDs, ","),
})
}
return result
}
// NewController returns a new Fever API.
func NewController(store *storage.Storage) *Controller {
return &Controller{store: store}
}
+532
View File
@@ -0,0 +1,532 @@
// Copyright 2018 Frédéric Guillot. All rights reserved.
// Use of this source code is governed by the Apache 2.0
// license that can be found in the LICENSE file.
package fever // import "miniflux.app/fever"
import (
"net/http"
"strconv"
"strings"
"time"
"miniflux.app/http/request"
"miniflux.app/http/response/json"
"miniflux.app/integration"
"miniflux.app/logger"
"miniflux.app/model"
"miniflux.app/storage"
"github.com/gorilla/mux"
)
// Serve handles Fever API calls.
func Serve(router *mux.Router, store *storage.Storage) {
handler := &handler{store}
sr := router.PathPrefix("/fever").Subrouter()
sr.Use(newMiddleware(store).serve)
sr.HandleFunc("/", handler.serve).Name("feverEndpoint")
}
type handler struct {
store *storage.Storage
}
func (h *handler) serve(w http.ResponseWriter, r *http.Request) {
switch {
case request.HasQueryParam(r, "groups"):
h.handleGroups(w, r)
case request.HasQueryParam(r, "feeds"):
h.handleFeeds(w, r)
case request.HasQueryParam(r, "favicons"):
h.handleFavicons(w, r)
case request.HasQueryParam(r, "unread_item_ids"):
h.handleUnreadItems(w, r)
case request.HasQueryParam(r, "saved_item_ids"):
h.handleSavedItems(w, r)
case request.HasQueryParam(r, "items"):
h.handleItems(w, r)
case r.FormValue("mark") == "item":
h.handleWriteItems(w, r)
case r.FormValue("mark") == "feed":
h.handleWriteFeeds(w, r)
case r.FormValue("mark") == "group":
h.handleWriteGroups(w, r)
default:
json.OK(w, r, newBaseResponse())
}
}
/*
A request with the groups argument will return two additional members:
groups contains an array of group objects
feeds_groups contains an array of feeds_group objects
A group object has the following members:
id (positive integer)
title (utf-8 string)
The feeds_group object is documented under “Feeds/Groups Relationships.”
The “Kindling” super group is not included in this response and is composed of all feeds with
an is_spark equal to 0.
The “Sparks” super group is not included in this response and is composed of all feeds with an
is_spark equal to 1.
*/
func (h *handler) handleGroups(w http.ResponseWriter, r *http.Request) {
userID := request.UserID(r)
logger.Debug("[Fever] Fetching groups for user #%d", userID)
categories, err := h.store.Categories(userID)
if err != nil {
json.ServerError(w, r, err)
return
}
feeds, err := h.store.Feeds(userID)
if err != nil {
json.ServerError(w, r, err)
return
}
var result groupsResponse
for _, category := range categories {
result.Groups = append(result.Groups, group{ID: category.ID, Title: category.Title})
}
result.FeedsGroups = h.buildFeedGroups(feeds)
result.SetCommonValues()
json.OK(w, r, result)
}
/*
A request with the feeds argument will return two additional members:
feeds contains an array of group objects
feeds_groups contains an array of feeds_group objects
A feed object has the following members:
id (positive integer)
favicon_id (positive integer)
title (utf-8 string)
url (utf-8 string)
site_url (utf-8 string)
is_spark (boolean integer)
last_updated_on_time (Unix timestamp/integer)
The feeds_group object is documented under “Feeds/Groups Relationships.”
The “All Items” super feed is not included in this response and is composed of all items from all feeds
that belong to a given group. For the “Kindling” super group and all user created groups the items
should be limited to feeds with an is_spark equal to 0.
For the “Sparks” super group the items should be limited to feeds with an is_spark equal to 1.
*/
func (h *handler) handleFeeds(w http.ResponseWriter, r *http.Request) {
userID := request.UserID(r)
logger.Debug("[Fever] Fetching feeds for userID=%d", userID)
feeds, err := h.store.Feeds(userID)
if err != nil {
json.ServerError(w, r, err)
return
}
var result feedsResponse
result.Feeds = make([]feed, 0)
for _, f := range feeds {
subscripion := feed{
ID: f.ID,
Title: f.Title,
URL: f.FeedURL,
SiteURL: f.SiteURL,
IsSpark: 0,
LastUpdated: f.CheckedAt.Unix(),
}
if f.Icon != nil {
subscripion.FaviconID = f.Icon.IconID
}
result.Feeds = append(result.Feeds, subscripion)
}
result.FeedsGroups = h.buildFeedGroups(feeds)
result.SetCommonValues()
json.OK(w, r, result)
}
/*
A request with the favicons argument will return one additional member:
favicons contains an array of favicon objects
A favicon object has the following members:
id (positive integer)
data (base64 encoded image data; prefixed by image type)
An example data value:
image/gif;base64,R0lGODlhAQABAIAAAObm5gAAACH5BAEAAAAALAAAAAABAAEAAAICRAEAOw==
The data member of a favicon object can be used with the data: protocol to embed an image in CSS or HTML.
A PHP/HTML example:
echo '<img src="data:'.$favicon['data'].'">';
*/
func (h *handler) handleFavicons(w http.ResponseWriter, r *http.Request) {
userID := request.UserID(r)
logger.Debug("[Fever] Fetching favicons for user #%d", userID)
icons, err := h.store.Icons(userID)
if err != nil {
json.ServerError(w, r, err)
return
}
var result faviconsResponse
for _, i := range icons {
result.Favicons = append(result.Favicons, favicon{
ID: i.ID,
Data: i.DataURL(),
})
}
result.SetCommonValues()
json.OK(w, r, result)
}
/*
A request with the items argument will return two additional members:
items contains an array of item objects
total_items contains the total number of items stored in the database (added in API version 2)
An item object has the following members:
id (positive integer)
feed_id (positive integer)
title (utf-8 string)
author (utf-8 string)
html (utf-8 string)
url (utf-8 string)
is_saved (boolean integer)
is_read (boolean integer)
created_on_time (Unix timestamp/integer)
Most servers wont have enough memory allocated to PHP to dump all items at once.
Three optional arguments control determine the items included in the response.
Use the since_id argument with the highest id of locally cached items to request 50 additional items.
Repeat until the items array in the response is empty.
Use the max_id argument with the lowest id of locally cached items (or 0 initially) to request 50 previous items.
Repeat until the items array in the response is empty. (added in API version 2)
Use the with_ids argument with a comma-separated list of item ids to request (a maximum of 50) specific items.
(added in API version 2)
*/
func (h *handler) handleItems(w http.ResponseWriter, r *http.Request) {
var result itemsResponse
userID := request.UserID(r)
builder := h.store.NewEntryQueryBuilder(userID)
builder.WithoutStatus(model.EntryStatusRemoved)
builder.WithLimit(50)
builder.WithOrder("id")
builder.WithDirection(model.DefaultSortingDirection)
switch {
case request.HasQueryParam(r, "since_id"):
sinceID := request.QueryInt64Param(r, "since_id", 0)
if sinceID > 0 {
logger.Debug("[Fever] Fetching items since #%d for user #%d", sinceID, userID)
builder.AfterEntryID(sinceID)
}
case request.HasQueryParam(r, "max_id"):
maxID := request.QueryInt64Param(r, "max_id", 0)
if maxID == 0 {
logger.Debug("[Fever] Fetching most recent items for user #%d", userID)
builder.WithDirection("desc")
} else if maxID > 0 {
logger.Debug("[Fever] Fetching items before #%d for user #%d", maxID, userID)
builder.BeforeEntryID(maxID)
builder.WithDirection("desc")
}
case request.HasQueryParam(r, "with_ids"):
csvItemIDs := request.QueryStringParam(r, "with_ids", "")
if csvItemIDs != "" {
var itemIDs []int64
for _, strItemID := range strings.Split(csvItemIDs, ",") {
strItemID = strings.TrimSpace(strItemID)
itemID, _ := strconv.ParseInt(strItemID, 10, 64)
itemIDs = append(itemIDs, itemID)
}
builder.WithEntryIDs(itemIDs)
}
default:
logger.Debug("[Fever] Fetching oldest items for user #%d", userID)
}
entries, err := builder.GetEntries()
if err != nil {
json.ServerError(w, r, err)
return
}
builder = h.store.NewEntryQueryBuilder(userID)
builder.WithoutStatus(model.EntryStatusRemoved)
result.Total, err = builder.CountEntries()
if err != nil {
json.ServerError(w, r, err)
return
}
result.Items = make([]item, 0)
for _, entry := range entries {
isRead := 0
if entry.Status == model.EntryStatusRead {
isRead = 1
}
isSaved := 0
if entry.Starred {
isSaved = 1
}
result.Items = append(result.Items, item{
ID: entry.ID,
FeedID: entry.FeedID,
Title: entry.Title,
Author: entry.Author,
HTML: entry.Content,
URL: entry.URL,
IsSaved: isSaved,
IsRead: isRead,
CreatedAt: entry.Date.Unix(),
})
}
result.SetCommonValues()
json.OK(w, r, result)
}
/*
The unread_item_ids and saved_item_ids arguments can be used to keep your local cache synced
with the remote Fever installation.
A request with the unread_item_ids argument will return one additional member:
unread_item_ids (string/comma-separated list of positive integers)
*/
func (h *handler) handleUnreadItems(w http.ResponseWriter, r *http.Request) {
userID := request.UserID(r)
logger.Debug("[Fever] Fetching unread items for user #%d", userID)
builder := h.store.NewEntryQueryBuilder(userID)
builder.WithStatus(model.EntryStatusUnread)
rawEntryIDs, err := builder.GetEntryIDs()
if err != nil {
json.ServerError(w, r, err)
return
}
var itemIDs []string
for _, entryID := range rawEntryIDs {
itemIDs = append(itemIDs, strconv.FormatInt(entryID, 10))
}
var result unreadResponse
result.ItemIDs = strings.Join(itemIDs, ",")
result.SetCommonValues()
json.OK(w, r, result)
}
/*
The unread_item_ids and saved_item_ids arguments can be used to keep your local cache synced
with the remote Fever installation.
A request with the saved_item_ids argument will return one additional member:
saved_item_ids (string/comma-separated list of positive integers)
*/
func (h *handler) handleSavedItems(w http.ResponseWriter, r *http.Request) {
userID := request.UserID(r)
logger.Debug("[Fever] Fetching saved items for user #%d", userID)
builder := h.store.NewEntryQueryBuilder(userID)
builder.WithStarred()
entryIDs, err := builder.GetEntryIDs()
if err != nil {
json.ServerError(w, r, err)
return
}
var itemsIDs []string
for _, entryID := range entryIDs {
itemsIDs = append(itemsIDs, strconv.FormatInt(entryID, 10))
}
result := &savedResponse{ItemIDs: strings.Join(itemsIDs, ",")}
result.SetCommonValues()
json.OK(w, r, result)
}
/*
mark=item
as=? where ? is replaced with read, saved or unsaved
id=? where ? is replaced with the id of the item to modify
*/
func (h *handler) handleWriteItems(w http.ResponseWriter, r *http.Request) {
userID := request.UserID(r)
logger.Debug("[Fever] Receiving mark=item call for user #%d", userID)
entryID := request.FormInt64Value(r, "id")
if entryID <= 0 {
return
}
builder := h.store.NewEntryQueryBuilder(userID)
builder.WithEntryID(entryID)
builder.WithoutStatus(model.EntryStatusRemoved)
entry, err := builder.GetEntry()
if err != nil {
json.ServerError(w, r, err)
return
}
if entry == nil {
logger.Debug("[Fever] Marking entry #%d but not found, ignored", entryID)
json.OK(w, r, newBaseResponse())
return
}
switch r.FormValue("as") {
case "read":
logger.Debug("[Fever] Mark entry #%d as read for user #%d", entryID, userID)
h.store.SetEntriesStatus(userID, []int64{entryID}, model.EntryStatusRead)
case "unread":
logger.Debug("[Fever] Mark entry #%d as unread for user #%d", entryID, userID)
h.store.SetEntriesStatus(userID, []int64{entryID}, model.EntryStatusUnread)
case "saved":
logger.Debug("[Fever] Mark entry #%d as saved for user #%d", entryID, userID)
if err := h.store.ToggleBookmark(userID, entryID); err != nil {
json.ServerError(w, r, err)
return
}
settings, err := h.store.Integration(userID)
if err != nil {
json.ServerError(w, r, err)
return
}
go func() {
integration.SendEntry(entry, settings)
}()
case "unsaved":
logger.Debug("[Fever] Mark entry #%d as unsaved for user #%d", entryID, userID)
if err := h.store.ToggleBookmark(userID, entryID); err != nil {
json.ServerError(w, r, err)
return
}
}
json.OK(w, r, newBaseResponse())
}
/*
mark=feed
as=read
id=? where ? is replaced with the id of the feed or group to modify
before=? where ? is replaced with the Unix timestamp of the the local clients most recent items API request
*/
func (h *handler) handleWriteFeeds(w http.ResponseWriter, r *http.Request) {
userID := request.UserID(r)
feedID := request.FormInt64Value(r, "id")
before := time.Unix(request.FormInt64Value(r, "before"), 0)
logger.Debug("[Fever] Mark feed #%d as read for user #%d before %v", feedID, userID, before)
if feedID <= 0 {
return
}
go func() {
if err := h.store.MarkFeedAsRead(userID, feedID, before); err != nil {
logger.Error("[Fever] MarkFeedAsRead failed: %v", err)
}
}()
json.OK(w, r, newBaseResponse())
}
/*
mark=group
as=read
id=? where ? is replaced with the id of the feed or group to modify
before=? where ? is replaced with the Unix timestamp of the the local clients most recent items API request
*/
func (h *handler) handleWriteGroups(w http.ResponseWriter, r *http.Request) {
userID := request.UserID(r)
groupID := request.FormInt64Value(r, "id")
before := time.Unix(request.FormInt64Value(r, "before"), 0)
logger.Debug("[Fever] Mark group #%d as read for user #%d before %v", groupID, userID, before)
if groupID < 0 {
return
}
go func() {
var err error
if groupID == 0 {
err = h.store.MarkAllAsRead(userID)
} else {
err = h.store.MarkCategoryAsRead(userID, groupID, before)
}
if err != nil {
logger.Error("[Fever] MarkCategoryAsRead failed: %v", err)
}
}()
json.OK(w, r, newBaseResponse())
}
/*
A feeds_group object has the following members:
group_id (positive integer)
feed_ids (string/comma-separated list of positive integers)
*/
func (h *handler) buildFeedGroups(feeds model.Feeds) []feedsGroups {
feedsGroupedByCategory := make(map[int64][]string)
for _, feed := range feeds {
feedsGroupedByCategory[feed.Category.ID] = append(feedsGroupedByCategory[feed.Category.ID], strconv.FormatInt(feed.ID, 10))
}
result := make([]feedsGroups, 0)
for categoryID, feedIDs := range feedsGroupedByCategory {
result = append(result, feedsGroups{
GroupID: categoryID,
FeedIDs: strings.Join(feedIDs, ","),
})
}
return result
}
+59
View File
@@ -0,0 +1,59 @@
// Copyright 2018 Frédéric Guillot. All rights reserved.
// Use of this source code is governed by the Apache 2.0
// license that can be found in the LICENSE file.
package fever // import "miniflux.app/fever"
import (
"context"
"net/http"
"miniflux.app/http/request"
"miniflux.app/http/response/json"
"miniflux.app/logger"
"miniflux.app/storage"
)
type middleware struct {
store *storage.Storage
}
func newMiddleware(s *storage.Storage) *middleware {
return &middleware{s}
}
func (m *middleware) serve(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
clientIP := request.ClientIP(r)
apiKey := r.FormValue("api_key")
if apiKey == "" {
logger.Info("[Fever] [ClientIP=%s] No API key provided", clientIP)
json.OK(w, r, newAuthFailureResponse())
return
}
user, err := m.store.UserByFeverToken(apiKey)
if err != nil {
logger.Error("[Fever] %v", err)
json.OK(w, r, newAuthFailureResponse())
return
}
if user == nil {
logger.Info("[Fever] [ClientIP=%s] No user found with this API key", clientIP)
json.OK(w, r, newAuthFailureResponse())
return
}
logger.Info("[Fever] [ClientIP=%s] User #%d is authenticated with user agent %q", clientIP, user.ID, r.UserAgent())
m.store.SetLastLogin(user.ID)
ctx := r.Context()
ctx = context.WithValue(ctx, request.UserIDContextKey, user.ID)
ctx = context.WithValue(ctx, request.UserTimezoneContextKey, user.Timezone)
ctx = context.WithValue(ctx, request.IsAdminUserContextKey, user.IsAdmin)
ctx = context.WithValue(ctx, request.IsAuthenticatedContextKey, true)
next.ServeHTTP(w, r.WithContext(ctx))
})
}
+120
View File
@@ -0,0 +1,120 @@
// Copyright 2018 Frédéric Guillot. All rights reserved.
// Use of this source code is governed by the Apache 2.0
// license that can be found in the LICENSE file.
package fever // import "miniflux.app/fever"
import (
"time"
)
type baseResponse struct {
Version int `json:"api_version"`
Authenticated int `json:"auth"`
LastRefresh int64 `json:"last_refreshed_on_time"`
}
func (b *baseResponse) SetCommonValues() {
b.Version = 3
b.Authenticated = 1
b.LastRefresh = time.Now().Unix()
}
/*
The default response is a JSON object containing two members:
api_version contains the version of the API responding (positive integer)
auth whether the request was successfully authenticated (boolean integer)
The API can also return XML by passing xml as the optional value of the api argument like so:
http://yourdomain.com/fever/?api=xml
The top level XML element is named response.
The response to each successfully authenticated request will have auth set to 1 and include
at least one additional member:
last_refreshed_on_time contains the time of the most recently refreshed (not updated)
feed (Unix timestamp/integer)
*/
func newBaseResponse() baseResponse {
r := baseResponse{}
r.SetCommonValues()
return r
}
func newAuthFailureResponse() baseResponse {
return baseResponse{Version: 3, Authenticated: 0}
}
type groupsResponse struct {
baseResponse
Groups []group `json:"groups"`
FeedsGroups []feedsGroups `json:"feeds_groups"`
}
type feedsResponse struct {
baseResponse
Feeds []feed `json:"feeds"`
FeedsGroups []feedsGroups `json:"feeds_groups"`
}
type faviconsResponse struct {
baseResponse
Favicons []favicon `json:"favicons"`
}
type itemsResponse struct {
baseResponse
Items []item `json:"items"`
Total int `json:"total_items"`
}
type unreadResponse struct {
baseResponse
ItemIDs string `json:"unread_item_ids"`
}
type savedResponse struct {
baseResponse
ItemIDs string `json:"saved_item_ids"`
}
type group struct {
ID int64 `json:"id"`
Title string `json:"title"`
}
type feedsGroups struct {
GroupID int64 `json:"group_id"`
FeedIDs string `json:"feed_ids"`
}
type feed struct {
ID int64 `json:"id"`
FaviconID int64 `json:"favicon_id"`
Title string `json:"title"`
URL string `json:"url"`
SiteURL string `json:"site_url"`
IsSpark int `json:"is_spark"`
LastUpdated int64 `json:"last_updated_on_time"`
}
type item struct {
ID int64 `json:"id"`
FeedID int64 `json:"feed_id"`
Title string `json:"title"`
Author string `json:"author"`
HTML string `json:"html"`
URL string `json:"url"`
IsSaved int `json:"is_saved"`
IsRead int `json:"is_read"`
CreatedAt int64 `json:"created_on_time"`
}
type favicon struct {
ID int64 `json:"id"`
Data string `json:"data"`
}
-10
View File
@@ -1,10 +0,0 @@
// Copyright 2018 Frédéric Guillot. All rights reserved.
// Use of this source code is governed by the MIT license
// that can be found in the LICENSE file.
/*
Package filter implements a content filter to rewrite image links.
*/
package filter
-41
View File
@@ -1,41 +0,0 @@
// Copyright 2017 Frédéric Guillot. All rights reserved.
// Use of this source code is governed by the Apache 2.0
// license that can be found in the LICENSE file.
package filter
import (
"encoding/base64"
"strings"
"github.com/miniflux/miniflux/http/route"
"github.com/miniflux/miniflux/url"
"github.com/PuerkitoBio/goquery"
"github.com/gorilla/mux"
)
// ImageProxyFilter rewrites image tag URLs without HTTPS to local proxy URL
func ImageProxyFilter(router *mux.Router, data string) string {
doc, err := goquery.NewDocumentFromReader(strings.NewReader(data))
if err != nil {
return data
}
doc.Find("img").Each(func(i int, img *goquery.Selection) {
if srcAttr, ok := img.Attr("src"); ok {
if !url.IsHTTPS(srcAttr) {
img.SetAttr("src", Proxify(router, srcAttr))
}
}
})
output, _ := doc.Find("body").First().Html()
return output
}
// Proxify returns a proxified link.
func Proxify(router *mux.Router, link string) string {
// We use base64 url encoding to avoid slash in the URL.
return route.Path(router, "proxy", "encodedURL", base64.URLEncoding.EncodeToString([]byte(link)))
}
-38
View File
@@ -1,38 +0,0 @@
// Copyright 2017 Frédéric Guillot. All rights reserved.
// Use of this source code is governed by the Apache 2.0
// license that can be found in the LICENSE file.
package filter
import (
"net/http"
"testing"
"github.com/gorilla/mux"
)
func TestProxyFilterWithHttp(t *testing.T) {
r := mux.NewRouter()
r.HandleFunc("/proxy/{encodedURL}", func(w http.ResponseWriter, r *http.Request) {}).Name("proxy")
input := `<p><img src="http://website/folder/image.png" alt="Test"/></p>`
output := ImageProxyFilter(r, input)
expected := `<p><img src="/proxy/aHR0cDovL3dlYnNpdGUvZm9sZGVyL2ltYWdlLnBuZw==" alt="Test"/></p>`
if expected != output {
t.Errorf(`Not expected output: got "%s" instead of "%s"`, output, expected)
}
}
func TestProxyFilterWithHttps(t *testing.T) {
r := mux.NewRouter()
r.HandleFunc("/proxy/{encodedURL}", func(w http.ResponseWriter, r *http.Request) {}).Name("proxy")
input := `<p><img src="https://website/folder/image.png" alt="Test"/></p>`
output := ImageProxyFilter(r, input)
expected := `<p><img src="https://website/folder/image.png" alt="Test"/></p>`
if expected != output {
t.Errorf(`Not expected output: got "%s" instead of "%s"`, output, expected)
}
}
+176 -75
View File
@@ -16,17 +16,15 @@ import (
"path/filepath"
"strings"
"text/template"
"time"
"github.com/tdewolff/minify"
"github.com/tdewolff/minify/css"
"github.com/tdewolff/minify/js"
"github.com/tdewolff/minify/v2"
"github.com/tdewolff/minify/v2/css"
"github.com/tdewolff/minify/v2/js"
)
const tpl = `// Code generated by go generate; DO NOT EDIT.
// {{ .Timestamp }}
package {{ .Package }}
package {{ .Package }} // import "miniflux.app/{{ .ImportPath }}"
var {{ .Map }} = map[string]string{
{{ range $constant, $content := .Files }}` + "\t" + `"{{ $constant }}": ` + "`{{ $content }}`" + `,
@@ -37,84 +35,187 @@ var {{ .Map }}Checksums = map[string]string{
{{ end }}}
`
var generatedTpl = template.Must(template.New("").Parse(tpl))
var bundleTpl = template.Must(template.New("").Parse(tpl))
type GeneratedFile struct {
Package, Map string
Timestamp time.Time
Files map[string]string
Checksums map[string]string
type Bundle struct {
Package string
Map string
ImportPath string
Files map[string]string
Checksums map[string]string
}
func normalizeBasename(filename string) string {
filename = strings.TrimSuffix(filename, filepath.Ext(filename))
return strings.Replace(filename, " ", "_", -1)
}
func generateFile(serializer, pkg, mapName, pattern, output string) {
generatedFile := &GeneratedFile{
Package: pkg,
Map: mapName,
Timestamp: time.Now(),
Files: make(map[string]string),
Checksums: make(map[string]string),
}
files, _ := filepath.Glob(pattern)
for _, file := range files {
basename := path.Base(file)
content, err := ioutil.ReadFile(file)
if err != nil {
panic(err)
}
switch serializer {
case "css":
m := minify.New()
m.AddFunc("text/css", css.Minify)
content, err = m.Bytes("text/css", content)
if err != nil {
panic(err)
}
basename = normalizeBasename(basename)
generatedFile.Files[basename] = string(content)
case "js":
m := minify.New()
m.AddFunc("text/javascript", js.Minify)
content, err = m.Bytes("text/javascript", content)
if err != nil {
panic(err)
}
basename = normalizeBasename(basename)
generatedFile.Files[basename] = string(content)
case "base64":
encodedContent := base64.StdEncoding.EncodeToString(content)
generatedFile.Files[basename] = encodedContent
default:
basename = normalizeBasename(basename)
generatedFile.Files[basename] = string(content)
}
generatedFile.Checksums[basename] = fmt.Sprintf("%x", sha256.Sum256(content))
}
f, err := os.Create(output)
func (b *Bundle) Write(filename string) {
f, err := os.Create(filename)
if err != nil {
panic(err)
}
defer f.Close()
generatedTpl.Execute(f, generatedFile)
bundleTpl.Execute(f, b)
}
func NewBundle(pkg, mapName, importPath string) *Bundle {
return &Bundle{
Package: pkg,
Map: mapName,
ImportPath: importPath,
Files: make(map[string]string),
Checksums: make(map[string]string),
}
}
func readFile(filename string) []byte {
data, err := ioutil.ReadFile(filename)
if err != nil {
panic(err)
}
return data
}
func checksum(data []byte) string {
return fmt.Sprintf("%x", sha256.Sum256(data))
}
func basename(filename string) string {
return path.Base(filename)
}
func stripExtension(filename string) string {
filename = strings.TrimSuffix(filename, path.Ext(filename))
return strings.Replace(filename, " ", "_", -1)
}
func glob(pattern string) []string {
// There is no Glob function in path package, so we have to use filepath and replace in case of Windows
files, _ := filepath.Glob(pattern)
for i := range files {
if strings.Contains(files[i], "\\") {
files[i] = strings.Replace(files[i], "\\", "/", -1)
}
}
return files
}
func concat(files []string) string {
var b strings.Builder
for _, file := range files {
b.Write(readFile(file))
}
return b.String()
}
func generateJSBundle(bundleFile string, bundleFiles map[string][]string, prefixes, suffixes map[string]string) {
bundle := NewBundle("static", "Javascripts", "ui/static")
m := minify.New()
m.AddFunc("text/javascript", js.Minify)
for name, srcFiles := range bundleFiles {
var b strings.Builder
if prefix, found := prefixes[name]; found {
b.WriteString(prefix)
}
b.WriteString(concat(srcFiles))
if suffix, found := suffixes[name]; found {
b.WriteString(suffix)
}
minifiedData, err := m.String("text/javascript", b.String())
if err != nil {
panic(err)
}
bundle.Files[name] = minifiedData
bundle.Checksums[name] = checksum([]byte(minifiedData))
}
bundle.Write(bundleFile)
}
func generateCSSBundle(bundleFile string, themes map[string][]string) {
bundle := NewBundle("static", "Stylesheets", "ui/static")
m := minify.New()
m.AddFunc("text/css", css.Minify)
for theme, srcFiles := range themes {
data := concat(srcFiles)
minifiedData, err := m.String("text/css", data)
if err != nil {
panic(err)
}
bundle.Files[theme] = minifiedData
bundle.Checksums[theme] = checksum([]byte(minifiedData))
}
bundle.Write(bundleFile)
}
func generateBinaryBundle(bundleFile string, srcFiles []string) {
bundle := NewBundle("static", "Binaries", "ui/static")
for _, srcFile := range srcFiles {
data := readFile(srcFile)
filename := basename(srcFile)
encodedData := base64.StdEncoding.EncodeToString(data)
bundle.Files[filename] = string(encodedData)
bundle.Checksums[filename] = checksum(data)
}
bundle.Write(bundleFile)
}
func generateBundle(bundleFile, pkg, mapName string, srcFiles []string) {
bundle := NewBundle(pkg, mapName, pkg)
for _, srcFile := range srcFiles {
data := readFile(srcFile)
filename := stripExtension(basename(srcFile))
bundle.Files[filename] = string(data)
bundle.Checksums[filename] = checksum(data)
}
bundle.Write(bundleFile)
}
func main() {
generateFile("none", "sql", "SqlMap", "sql/*.sql", "sql/sql.go")
generateFile("base64", "static", "Binaries", "ui/static/bin/*", "ui/static/bin.go")
generateFile("css", "static", "Stylesheets", "ui/static/css/*.css", "ui/static/css.go")
generateFile("js", "static", "Javascript", "ui/static/js/*.js", "ui/static/js.go")
generateFile("none", "template", "templateViewsMap", "template/html/*.html", "template/views.go")
generateFile("none", "template", "templateCommonMap", "template/html/common/*.html", "template/common.go")
generateFile("none", "locale", "translations", "locale/translations/*.json", "locale/translations.go")
generateJSBundle("ui/static/js.go", map[string][]string{
"app": []string{
"ui/static/js/dom_helper.js",
"ui/static/js/touch_handler.js",
"ui/static/js/keyboard_handler.js",
"ui/static/js/request_builder.js",
"ui/static/js/modal_handler.js",
"ui/static/js/app.js",
"ui/static/js/bootstrap.js",
},
"service-worker": []string{
"ui/static/js/service_worker.js",
},
}, map[string]string{
"app": "(function(){'use strict';",
"sw": "'use strict';",
}, map[string]string{
"app": "})();",
})
generateCSSBundle("ui/static/css.go", map[string][]string{
"light_serif": []string{"ui/static/css/light.css", "ui/static/css/serif.css", "ui/static/css/common.css"},
"light_sans_serif": []string{"ui/static/css/light.css", "ui/static/css/sans_serif.css", "ui/static/css/common.css"},
"dark_serif": []string{"ui/static/css/dark.css", "ui/static/css/serif.css", "ui/static/css/common.css"},
"dark_sans_serif": []string{"ui/static/css/dark.css", "ui/static/css/sans_serif.css", "ui/static/css/common.css"},
"system_serif": []string{"ui/static/css/system.css", "ui/static/css/serif.css", "ui/static/css/common.css"},
"system_sans_serif": []string{"ui/static/css/system.css", "ui/static/css/sans_serif.css", "ui/static/css/common.css"},
})
generateBinaryBundle("ui/static/bin.go", glob("ui/static/bin/*"))
generateBundle("template/views.go", "template", "templateViewsMap", glob("template/html/*.html"))
generateBundle("template/common.go", "template", "templateCommonMap", glob("template/html/common/*.html"))
generateBundle("locale/translations.go", "locale", "translations", glob("locale/translations/*.json"))
}
+27
View File
@@ -0,0 +1,27 @@
module miniflux.app
// +heroku goVersion go1.15
require (
github.com/PuerkitoBio/goquery v1.6.1
github.com/coreos/go-oidc v2.2.1+incompatible
github.com/felixge/httpsnoop v1.0.1 // indirect
github.com/golang/gddo v0.0.0-20200831202555-721e228c7686 // indirect
github.com/gorilla/mux v1.8.0
github.com/lib/pq v1.9.0
github.com/mitchellh/go-server-timing v1.0.2-0.20201108055052-feb680ab92c2
github.com/pquerna/cachecontrol v0.0.0-20180517163645-1555304b9b35 // indirect
github.com/prometheus/client_golang v1.9.0
github.com/rylans/getlang v0.0.0-20200505200108-4c3188ff8a2d
github.com/stretchr/testify v1.6.1 // indirect
github.com/tdewolff/minify/v2 v2.9.11 // indirect
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9
golang.org/x/net v0.0.0-20201029221708-28c70e62bb1d
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d
google.golang.org/appengine v1.6.6 // indirect
google.golang.org/protobuf v1.25.0 // indirect
gopkg.in/square/go-jose.v2 v2.5.0 // indirect
mvdan.cc/xurls/v2 v2.2.0
)
go 1.15
+584
View File
@@ -0,0 +1,584 @@
cloud.google.com/go v0.16.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/Knetic/govaluate v3.0.1-0.20171022003610-9aa49832a739+incompatible/go.mod h1:r7JcOSlj0wfOMncg0iLm8Leh48TZaKVeNIfJntJ2wa0=
github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
github.com/PuerkitoBio/goquery v1.6.1 h1:FgjbQZKl5HTmcn4sKBgvx8vv63nhyhIpv7lJpFGCWpk=
github.com/PuerkitoBio/goquery v1.6.1/go.mod h1:GsLWisAFVj4WgDibEWF4pvYnkVQBpKBKeU+7zCJoLcc=
github.com/Shopify/sarama v1.19.0/go.mod h1:FVkBWblsNy7DGZRfXLU0O9RCGt5g3g3yEuWXgklEdEo=
github.com/Shopify/toxiproxy v2.1.4+incompatible/go.mod h1:OXgGpZ6Cli1/URJOF1DMxUHB2q5Ap20/P/eIdh4G0pI=
github.com/VividCortex/gohistogram v1.0.0/go.mod h1:Pf5mBqqDxYaXu3hDrrU+w6nw50o/4+TcAqDqk/vUH7g=
github.com/afex/hystrix-go v0.0.0-20180502004556-fa1af6a1f4f5/go.mod h1:SkGFH1ia65gfNATL8TAiHDNxPzPdmEL5uirI2Uyuz6c=
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho=
github.com/andybalholm/cascadia v1.1.0 h1:BuuO6sSfQNFRu1LppgbD25Hr2vLYW25JvxHs5zzsLTo=
github.com/andybalholm/cascadia v1.1.0/go.mod h1:GsXiBklL0woXo1j/WYWtSYYC4ouU9PqHO0sqidkEA4Y=
github.com/apache/thrift v0.12.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ=
github.com/apache/thrift v0.13.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ=
github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o=
github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8=
github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY=
github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8=
github.com/aryann/difflib v0.0.0-20170710044230-e206f873d14a/go.mod h1:DAHtR1m6lCRdSC2Tm3DSWRPvIPr6xNKyeHdqDQSQT+A=
github.com/aws/aws-lambda-go v1.13.3/go.mod h1:4UKl9IzQMoD+QF79YdCuzCwp8VbmG4VAQwij/eHl5CU=
github.com/aws/aws-sdk-go v1.27.0/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo=
github.com/aws/aws-sdk-go-v2 v0.18.0/go.mod h1:JWVYvqSMppoMJC0x5wdwiImzgXTI9FuZwxzkQq9wy+g=
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs=
github.com/bradfitz/gomemcache v0.0.0-20170208213004-1952afaa557d/go.mod h1:PmM6Mmwb0LSuEubjR8N7PtNe1KxZLtOUHtbeikc5h60=
github.com/casbin/casbin/v2 v2.1.2/go.mod h1:YcPU1XXisHhLzuxH9coDNf2FbKpjGlbCg3n9yuLkIJQ=
github.com/cenkalti/backoff v2.2.1+incompatible/go.mod h1:90ReRw6GdpyfrHakVjL/QHaoyV4aDUVVkXQJJJ3NXXM=
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
github.com/cespare/xxhash v1.1.0 h1:a6HrQnmkObjyL+Gs60czilIUGqrzKutQD6XZog3p+ko=
github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc=
github.com/cespare/xxhash/v2 v2.1.1 h1:6MnRN8NT7+YBpUIWxHtefFZOKTAPgGjpQSxqLNn0+qY=
github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/cheekybits/is v0.0.0-20150225183255-68e9c0620927/go.mod h1:h/aW8ynjgkuj+NQRlZcDbAbM1ORAbXjXX77sX7T289U=
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
github.com/clbanning/x2j v0.0.0-20191024224557-825249438eec/go.mod h1:jMjuTZXRI4dUb/I5gc9Hdhagfvm9+RyrPryS/auMzxE=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/cockroachdb/datadriven v0.0.0-20190809214429-80d97fb3cbaa/go.mod h1:zn76sxSg3SzpJ0PPJaLDCu+Bu0Lg3sKTORVIj19EIF8=
github.com/codahale/hdrhistogram v0.0.0-20161010025455-3a0bb77429bd/go.mod h1:sE/e/2PUdi/liOCUjSTXgM1o87ZssimdTWN964YiIeI=
github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk=
github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=
github.com/coreos/go-oidc v2.2.1+incompatible h1:mh48q/BqXqgjVHpy2ZY7WnWAbenxRjsz9N1i1YxjHAk=
github.com/coreos/go-oidc v2.2.1+incompatible/go.mod h1:CgnwVTmzoESiwO9qyAFEMiHoZ1nMCKZlZ9V6mm3/LKc=
github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
github.com/coreos/go-systemd v0.0.0-20180511133405-39ca1b05acc7/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
github.com/coreos/pkg v0.0.0-20160727233714-3ac0863d7acf/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA=
github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA=
github.com/cosiner/argv v0.1.0/go.mod h1:EusR6TucWKX+zFgtdUsKT2Cvg45K5rtpCcWz4hK06d8=
github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE=
github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no=
github.com/dustin/go-humanize v0.0.0-20171111073723-bb3d318650d4/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
github.com/eapache/go-resiliency v1.1.0/go.mod h1:kFI+JgMyC7bLPUVY133qvEBtVayf5mFgVsvEsIPBvNs=
github.com/eapache/go-xerial-snappy v0.0.0-20180814174437-776d5712da21/go.mod h1:+020luEh2TKB4/GOp8oxxtq0Daoen/Cii55CzbTV6DU=
github.com/eapache/queue v1.1.0/go.mod h1:6eCeP0CKFpHLu8blIFXhExK/dRa7WDZfr6jVFPTqq+I=
github.com/edsrzf/mmap-go v1.0.0/go.mod h1:YO35OhQPt3KJa3ryjFM5Bs14WD66h8eGKpfaBNrHW5M=
github.com/envoyproxy/go-control-plane v0.6.9/go.mod h1:SBwIajubJHhxtWwsL9s8ss4safvEdbitLhGGK48rN6g=
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
github.com/felixge/httpsnoop v1.0.0 h1:gh8fMGz0rlOv/1WmRZm7OgncIOTsAj21iNJot48omJQ=
github.com/felixge/httpsnoop v1.0.0/go.mod h1:3+D9sFq0ahK/JeJPhCBUV1xlf4/eIYrUQaxulT0VzX8=
github.com/felixge/httpsnoop v1.0.1 h1:lvB5Jl89CsZtGIWuTcDM1E/vkVs49/Ml7JJe07l8SPQ=
github.com/felixge/httpsnoop v1.0.1/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
github.com/franela/goblin v0.0.0-20200105215937-c9ffbefa60db/go.mod h1:7dvUGVsVBjqR7JHJk0brhHOZYGmfBYOrK0ZhYMEtBr4=
github.com/franela/goreq v0.0.0-20171204163338-bcd34c9993f8/go.mod h1:ZhphrRTfi2rbfLwlschooIH4+wKKDR4Pdxhh+TRoA20=
github.com/fsnotify/fsnotify v1.4.3-0.20170329110642-4da3e2cfbabc/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
github.com/garyburd/redigo v1.1.1-0.20170914051019-70e1b1943d4f/go.mod h1:NR3MbYisc3/PwhQ00EMzDiPmrwpPxAn5GI05/YaO1SY=
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
github.com/go-delve/delve v1.5.0/go.mod h1:c6b3a1Gry6x8a4LGCe/CWzrocrfaHvkUxCj3k4bvSUQ=
github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
github.com/go-kit/kit v0.10.0/go.mod h1:xUsJbQ/Fp4kEt7AFgCuvyX4a71u8h9jB8tj/ORgOZ7o=
github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A=
github.com/go-sql-driver/mysql v1.4.0/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w=
github.com/go-stack/stack v1.6.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
github.com/gogo/googleapis v1.1.0/go.mod h1:gf4bu3Q80BeJ6H1S1vYPm8/ELATdvryBaNFGgqEef3s=
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
github.com/gogo/protobuf v1.2.0/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4=
github.com/golang/gddo v0.0.0-20180823221919-9d8ff1c67be5 h1:yrv1uUvgXH/tEat+wdvJMRJ4g51GlIydtDpU9pFjaaI=
github.com/golang/gddo v0.0.0-20180823221919-9d8ff1c67be5/go.mod h1:xEhNfoBDX1hzLm2Nf80qUvZ2sVwoMZ8d6IE2SrsQfh4=
github.com/golang/gddo v0.0.0-20200831202555-721e228c7686 h1:5vu7C+63KTbsSNnLhrgB98Sqy8MNVSW8FdhkcWA/3Rk=
github.com/golang/gddo v0.0.0-20200831202555-721e228c7686/go.mod h1:sam69Hju0uq+5uvLJUMDlsKlQ21Vrs1Kd/1YFPNYdOU=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
github.com/golang/groupcache v0.0.0-20160516000752-02826c3e7903/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/lint v0.0.0-20170918230701-e5d664eb928e/go.mod h1:tluoj9z5200jBnyusfRPU2LqT6J+DAorxEvtC7LHB+E=
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/protobuf v1.2.0 h1:P3YflyNX/ehuJFLhxviNdFxQPkGK5cDcApsge1SqnvM=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
github.com/golang/protobuf v1.4.0 h1:oOuy+ugB+P/kBdUnG5QaMXSIyJ1q38wWSojYCb3z5VQ=
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=
github.com/golang/protobuf v1.4.2 h1:+Z5KGCizgyZCbGh1KZqA0fcLLkwbsjIzS4aV2v7wJX0=
github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
github.com/golang/protobuf v1.4.3 h1:JjCZWpVbqXDqFVmTfYWEVTMIYrL/NPdPSCHPJ0T/raM=
github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
github.com/golang/snappy v0.0.0-20170215233205-553a64147049/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/go-cmp v0.1.1-0.20171103154506-982329095285/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.4.0 h1:xsAVV57WRhGj6kEIi8ReJzQlHHqcBYCElAvkovg3B/4=
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.0 h1:/QaMHBdZ26BB3SSst0Iwl10Epc+xhTquomWX0oZEB6w=
github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-dap v0.2.0/go.mod h1:5q8aYQFnHOAZEMP+6vmq25HKYAEwE+LF5yh7JKrrhSQ=
github.com/google/go-dap v0.3.0/go.mod h1:5q8aYQFnHOAZEMP+6vmq25HKYAEwE+LF5yh7JKrrhSQ=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
github.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/googleapis/gax-go v2.0.0+incompatible/go.mod h1:SFVmujtThgffbyetf+mdk2eWhX2bMyUtNHzFKcPA9HY=
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg=
github.com/gorilla/mux v1.6.2/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=
github.com/gorilla/mux v1.7.3/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=
github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI=
github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So=
github.com/gorilla/websocket v0.0.0-20170926233335-4201258b820c/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ=
github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ=
github.com/gregjones/httpcache v0.0.0-20170920190843-316c5e0ff04e/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA=
github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs=
github.com/grpc-ecosystem/go-grpc-middleware v1.0.1-0.20190118093823-f849b5445de4/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs=
github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk=
github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY=
github.com/grpc-ecosystem/grpc-gateway v1.9.5/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY=
github.com/hashicorp/consul/api v1.3.0/go.mod h1:MmDNSzIMUjNpY/mQ398R4bk2FnqQLoPndWW5VkKPlCE=
github.com/hashicorp/consul/sdk v0.3.0/go.mod h1:VKf9jXwCTEY1QZP2MOLRhb5i/I/ssyNV1vwHyQBF0x8=
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80=
github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60=
github.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM=
github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk=
github.com/hashicorp/go-rootcerts v1.0.0/go.mod h1:K6zTfqpRlCUIjkwsN4Z+hiSfzSTQa6eBIzfwKfwNnHU=
github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU=
github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4=
github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
github.com/hashicorp/go-version v1.2.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA=
github.com/hashicorp/go.net v0.0.1/go.mod h1:hjKkEWcCURg++eb33jQU7oqQcI9XDCnUzHA0oac0k90=
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hashicorp/golang-lru v0.5.4/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4=
github.com/hashicorp/hcl v0.0.0-20170914154624-68e816d1c783/go.mod h1:oZtUIOe8dh44I2q6ScRibXws4Ajl+d+nod3AaR9vL5w=
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64=
github.com/hashicorp/mdns v1.0.0/go.mod h1:tL+uN++7HEJ6SQLQ2/p+z2pH24WQKWjBPkE0mNTz8vQ=
github.com/hashicorp/memberlist v0.1.3/go.mod h1:ajVTdAv/9Im8oMAAj5G31PhhMCZJV2pPBoIllUwCN7I=
github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/JwenrHc=
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
github.com/hudl/fargo v1.3.0/go.mod h1:y3CKSmjA+wD2gak7sUSXTAoopbhU08POFhmITJgmKTg=
github.com/inconshreveable/log15 v0.0.0-20170622235902-74a0988b5f80/go.mod h1:cOaXtrgN4ScfRrD9Bre7U1thNq5RtJ8ZoP4iXVGRj6o=
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
github.com/influxdata/influxdb1-client v0.0.0-20191209144304-8bf82d3c094d/go.mod h1:qj24IKcXYK6Iy9ceXlo3Tc+vtHo9lIhSX5JddghvEPo=
github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k=
github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo=
github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4=
github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
github.com/json-iterator/go v1.1.8/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM=
github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pretty v0.2.0 h1:s5hAObm+yFO5uHYt5dYjxi2rXrsnmRpJx4OYvIWUaQs=
github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/lib/pq v1.9.0 h1:L8nSXQQzAYByakOFMTwpjRoHsMJklur4Gi59b6VivR8=
github.com/lib/pq v1.9.0/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
github.com/lightstep/lightstep-tracer-common/golang/gogo v0.0.0-20190605223551-bc2310a04743/go.mod h1:qklhhLq1aX+mtWk9cPHPzaBjWImj5ULL6C7HFJtXQMM=
github.com/lightstep/lightstep-tracer-go v0.18.1/go.mod h1:jlF1pusYV4pidLvZ+XD0UBX0ZE6WURAspgAczcDHrL4=
github.com/lyft/protoc-gen-validate v0.0.13/go.mod h1:XbGvPuh87YZc5TdIa2/I4pLk0QoUACkjt2znoq26NVQ=
github.com/magiconair/properties v1.7.4-0.20170902060319-8d7837e64d3c/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
github.com/matryer/try v0.0.0-20161228173917-9ac251b645a2/go.mod h1:0KeJpeMD6o+O4hW7qJOT7vyQPKrWmj26uf5wMc/IiIs=
github.com/mattn/go-colorable v0.0.0-20170327083344-ded68f7a9561/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
github.com/mattn/go-colorable v0.0.10-0.20170816031813-ad5389df28cd/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
github.com/mattn/go-colorable v0.1.8/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
github.com/mattn/go-isatty v0.0.2/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
github.com/mattn/go-runewidth v0.0.2/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU=
github.com/mattn/go-runewidth v0.0.3/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU=
github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU=
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg=
github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc=
github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
github.com/mitchellh/go-server-timing v1.0.2-0.20201108055052-feb680ab92c2 h1:9kYRAm87/M5VL+HWegDGIorBWDiErrZrksLKTJBF2IQ=
github.com/mitchellh/go-server-timing v1.0.2-0.20201108055052-feb680ab92c2/go.mod h1:8mKJVJkyMI20ifXXO0zlV3sh3FrtjvltAeBqz38clBE=
github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI=
github.com/mitchellh/gox v0.4.0/go.mod h1:Sd9lOJ0+aimLBi73mGofS1ycjY8lL3uZM3JPS42BGNg=
github.com/mitchellh/iochan v1.0.0/go.mod h1:JwYml1nuB7xOzsp52dPpHFffvOCDupsG0QubkSMEySY=
github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
github.com/mitchellh/mapstructure v0.0.0-20170523030023-d0303fe80992/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
github.com/nats-io/jwt v0.3.0/go.mod h1:fRYCDE99xlTsqUzISS1Bi75UBJ6ljOJQOAAu5VglpSg=
github.com/nats-io/jwt v0.3.2/go.mod h1:/euKqTS1ZD+zzjYrY7pseZrTtWQSjujC7xjPc8wL6eU=
github.com/nats-io/nats-server/v2 v2.1.2/go.mod h1:Afk+wRZqkMQs/p45uXdrVLuab3gwv3Z8C4HTBu8GD/k=
github.com/nats-io/nats.go v1.9.1/go.mod h1:ZjDU1L/7fJ09jvUSRVBR2e7+RnLiiIQyqyzEE/Zbp4w=
github.com/nats-io/nkeys v0.1.0/go.mod h1:xpnFELMwJABBLVhffcfd1MZx6VsNRFpEugbxziKVo7w=
github.com/nats-io/nkeys v0.1.3/go.mod h1:xpnFELMwJABBLVhffcfd1MZx6VsNRFpEugbxziKVo7w=
github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c=
github.com/oklog/oklog v0.3.2/go.mod h1:FCV+B7mhrz4o+ueLpx+KqkyXRGMWOYEvfiXtdGtbWGs=
github.com/oklog/run v1.0.0/go.mod h1:dlhp/R75TPv97u0XWUtDeV/lRKWPKSdTuV0TZvrmrQA=
github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U=
github.com/olekukonko/tablewriter v0.0.0-20170122224234-a0225b3f23b5/go.mod h1:vsDQFd/mU46D+Z4whnwzcISnGGzXWMclvtLoiIKAKIo=
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
github.com/op/go-logging v0.0.0-20160315200505-970db520ece7/go.mod h1:HzydrMdWErDVzsI23lYNej1Htcns9BCg93Dk0bBINWk=
github.com/opentracing-contrib/go-observer v0.0.0-20170622124052-a52f23424492/go.mod h1:Ngi6UdF0k5OKD5t5wlmGhe/EDKPoUM3BXZSSfIuJbis=
github.com/opentracing/basictracer-go v1.0.0/go.mod h1:QfBfYuafItcjQuMwinw9GhYKwFXS9KnPs5lxoYwgW74=
github.com/opentracing/opentracing-go v1.0.2/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o=
github.com/opentracing/opentracing-go v1.1.0/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o=
github.com/openzipkin-contrib/zipkin-go-opentracing v0.4.5/go.mod h1:/wsWhb9smxSfWAKL3wpBW7V8scJMt8N8gnaMCS9E/cA=
github.com/openzipkin/zipkin-go v0.1.6/go.mod h1:QgAqvLzwWbR/WpD4A3cGpPtJrZXNIiJc5AZX7/PBEpw=
github.com/openzipkin/zipkin-go v0.2.1/go.mod h1:NaW6tEwdmWMaCDZzg8sh+IBNOxHMPnhQw8ySjnjRyN4=
github.com/openzipkin/zipkin-go v0.2.2/go.mod h1:NaW6tEwdmWMaCDZzg8sh+IBNOxHMPnhQw8ySjnjRyN4=
github.com/pact-foundation/pact-go v1.0.4/go.mod h1:uExwJY4kCzNPcHRj+hCR/HBbOOIwwtUjcrb0b5/5kLM=
github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
github.com/pborman/uuid v1.2.0/go.mod h1:X/NO0urCmaxf9VXbdlT7C2Yzkj2IKimNn4k+gtPdI/k=
github.com/pelletier/go-toml v1.0.1-0.20170904195809-1d6b12b7cb29/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
github.com/performancecopilot/speed v3.0.0+incompatible/go.mod h1:/CLtqpZ5gBg1M9iaPbIdPPGyKcA8hKdoy6hAWba7Yac=
github.com/peterh/liner v0.0.0-20170317030525-88609521dc4b/go.mod h1:xIteQHvHuaLYG9IFj6mSxM0fCKrs34IrEQUhOYuGPHc=
github.com/peterh/liner v1.2.0/go.mod h1:CRroGNssyjTd/qIG2FyxByd2S8JEAZXBl4qUrZf8GS0=
github.com/pierrec/lz4 v1.0.2-0.20190131084431-473cd7ce01a1/go.mod h1:3/3N9NVKO0jef7pBehbT1qWhCMrIgbYNnFAZCqQ5LRc=
github.com/pierrec/lz4 v2.0.5+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY=
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/profile v1.2.1/go.mod h1:hJw3o1OdXxsrSjjVksARp5W95eeEaEfptyVZyv6JUPA=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI=
github.com/pquerna/cachecontrol v0.0.0-20180517163645-1555304b9b35 h1:J9b7z+QKAmPf4YLrFg6oQUotqHQeUNWwkvo7jZp1GLU=
github.com/pquerna/cachecontrol v0.0.0-20180517163645-1555304b9b35/go.mod h1:prYjPmNq4d1NPVmpShWobRqXY3q7Vp+80DqgxxUrUIA=
github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
github.com/prometheus/client_golang v0.9.3-0.20190127221311-3c4408c8b829/go.mod h1:p2iRAGwDERtqlqzRXnrOVns+ignqQo//hLXqYxZYVNs=
github.com/prometheus/client_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDft0ttaMvbicHlPoso=
github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo=
github.com/prometheus/client_golang v1.3.0/go.mod h1:hJaj2vgQTGQmVCsAACORcieXFeDPbaTKGT+JTgUa3og=
github.com/prometheus/client_golang v1.7.1 h1:NTGy1Ja9pByO+xAeH/qiWnLrKtr3hJPNjaVUwnjpdpA=
github.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M=
github.com/prometheus/client_golang v1.9.0 h1:Rrch9mh17XcxvEu9D9DEpb4isxjGBtcevQjKvxPRQIU=
github.com/prometheus/client_golang v1.9.0/go.mod h1:FqZLKOZnGdFAhOK4nqGHa7D66IdsO+O441Eve7ptJDU=
github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
github.com/prometheus/client_model v0.0.0-20190115171406-56726106282f/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/prometheus/client_model v0.1.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/prometheus/client_model v0.2.0 h1:uq5h0d+GuxiXLJLNABMgp2qUWDPiLvgCzz2dUR+/W/M=
github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/prometheus/common v0.0.0-20181113130724-41aa239b4cce/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro=
github.com/prometheus/common v0.2.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
github.com/prometheus/common v0.4.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
github.com/prometheus/common v0.7.0/go.mod h1:DjGbpBbp5NYNiECxcL/VnbXCCaQpKd3tt26CguLLsqA=
github.com/prometheus/common v0.10.0 h1:RyRA7RzGXQZiW+tGMr7sxa85G1z0yOpM1qq5c8lNawc=
github.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo=
github.com/prometheus/common v0.15.0 h1:4fgOnadei3EZvgRwxJ7RMpG1k1pOZth5Pc13tyspaKM=
github.com/prometheus/common v0.15.0/go.mod h1:U+gB1OBLb1lF3O42bTCL+FK18tX9Oar16Clt/msog/s=
github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
github.com/prometheus/procfs v0.0.0-20190117184657-bf6a532e95b1/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
github.com/prometheus/procfs v0.0.8/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+GxbHq6oeK9A=
github.com/prometheus/procfs v0.1.3 h1:F0+tqvhOksq22sc6iCHF5WGlWjdwj92p0udFh1VFBS8=
github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU=
github.com/prometheus/procfs v0.2.0 h1:wH4vA7pcjKuZzjF7lM8awk4fnuJO6idemZXoKnULUx4=
github.com/prometheus/procfs v0.2.0/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU=
github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU=
github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4=
github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg=
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
github.com/rogpeppe/go-internal v1.5.2/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g=
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts=
github.com/rylans/getlang v0.0.0-20200505200108-4c3188ff8a2d h1:4Jn2kzSKcpxxwpJdyWxfNWKCYe4Uki2VibkExYmBLiA=
github.com/rylans/getlang v0.0.0-20200505200108-4c3188ff8a2d/go.mod h1:3vfmZI6aJd5Rb9W2TQ0Nmupl+qem21R05+hmCscI0Bk=
github.com/samuel/go-zookeeper v0.0.0-20190923202752-2cc03de413da/go.mod h1:gi+0XIa01GRL2eRQVjQkKGqKF3SF9vZR/HnPullcV2E=
github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc=
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88=
github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM=
github.com/sony/gobreaker v0.4.1/go.mod h1:ZKptC7FHNvhBz7dN2LGjPVBz2sZJmc0/PkyDJOjmxWY=
github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
github.com/spf13/afero v0.0.0-20170901052352-ee1bd8ee15a1/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ=
github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ=
github.com/spf13/cast v1.1.0/go.mod h1:r2rcYCSwa1IExKTDiTfzaxqT2FNHs8hODu4LnUfgKEg=
github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
github.com/spf13/cobra v0.0.0-20170417170307-b6cb39589372/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ=
github.com/spf13/cobra v0.0.3/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ=
github.com/spf13/cobra v1.0.0/go.mod h1:/6GTrnGXV9HjY+aR4k0oJ5tcvakLuG6EuKReYlHNrgE=
github.com/spf13/jwalterweatherman v0.0.0-20170901151539-12bd96e66386/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo=
github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo=
github.com/spf13/pflag v0.0.0-20170417173400-9e4c21054fa1/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
github.com/spf13/pflag v1.0.1-0.20170901120850-7aff26db30c1/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
github.com/spf13/pflag v1.0.1/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/spf13/viper v1.0.0/go.mod h1:A8kyI5cUJhb8N+3pkfONlcEcZbueH6nhAm0Fq7SrnBM=
github.com/spf13/viper v1.4.0/go.mod h1:PTJ7Z/lr49W6bUbkmS1V3by4uWynFiR9p7+dSq/yZzE=
github.com/streadway/amqp v0.0.0-20190404075320-75d898a42a94/go.mod h1:AZpEONHx3DKn8O/DFsRAY58/XVQiIPMTMB1SddzLXVw=
github.com/streadway/amqp v0.0.0-20190827072141-edfb9018d271/go.mod h1:AZpEONHx3DKn8O/DFsRAY58/XVQiIPMTMB1SddzLXVw=
github.com/streadway/handy v0.0.0-20190108123426-d5acb3125c2a/go.mod h1:qNTQ5P5JnDBl6z3cMAg/SywNDC5ABu5ApDIw6lUbRmI=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/tdewolff/minify v1.1.0 h1:nxHQi1ML+g3ZbZHffiZ6eC7vMqNvSRfX3KB5Y5y/kfw=
github.com/tdewolff/minify v2.3.6+incompatible h1:2hw5/9ZvxhWLvBUnHE06gElGYz+Jv9R4Eys0XUzItYo=
github.com/tdewolff/minify/v2 v2.9.11 h1:8o6hclGwxm6MNwTPHabvdND5SghhHs0bn+3/+uAf0yQ=
github.com/tdewolff/minify/v2 v2.9.11/go.mod h1:YZk0lGOc6CvQrqvm5f7V3ihaq3QUd9acS4HESdVDOaM=
github.com/tdewolff/parse/v2 v2.5.8 h1:vutkOO9Xi3DehIzCLHqvMM2hFXo54S0iDvIG/hYznnE=
github.com/tdewolff/parse/v2 v2.5.8/go.mod h1:WzaJpRSbwq++EIQHYIRTpbYKNA3gn9it1Ik++q4zyho=
github.com/tdewolff/test v1.0.6/go.mod h1:6DAvZliBAAnD7rhVgwaM7DE5/d9NMOAJ09SqYqeK4QE=
github.com/tmc/grpc-websocket-proxy v0.0.0-20170815181823-89b8d40f7ca8/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=
github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=
github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc=
github.com/urfave/cli v1.20.0/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA=
github.com/urfave/cli v1.22.1/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0=
github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU=
github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q=
go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU=
go.etcd.io/bbolt v1.3.3/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU=
go.etcd.io/etcd v0.0.0-20191023171146-3cf2f69b5738/go.mod h1:dnLIgRNXwCJa5e+c6mIZCrds/GIG4ncV9HhK5PX7jPg=
go.opencensus.io v0.20.1/go.mod h1:6WKK9ahsWS3RSO+PY9ZHZUfv2irvY6gN279GOPZjmmk=
go.opencensus.io v0.20.2/go.mod h1:6WKK9ahsWS3RSO+PY9ZHZUfv2irvY6gN279GOPZjmmk=
go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go.starlark.net v0.0.0-20190702223751-32f345186213/go.mod h1:c1/X6cHgvdXj6pUlmWKMkuqRnW4K8x2vwt6JAaaircg=
go.starlark.net v0.0.0-20201006213952-227f4aabceb5/go.mod h1:f0znQkUKRrkk36XxWbGjMqQM8wGv/xHBVE2qc3B5oFU=
go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
go.uber.org/atomic v1.5.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ=
go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0=
go.uber.org/multierr v1.3.0/go.mod h1:VgVr7evmIr6uPjLBxg28wmKNXyqE9akIJ5XnfpiKl+4=
go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9Ejo0C68/HhF8uaILCdgjnY+goOA=
go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
go.uber.org/zap v1.13.0/go.mod h1:zwrFLgMcdUuIBviXEYEH1YKNaOBnKXsx2IPda5bBwHM=
golang.org/x/arch v0.0.0-20190927153633-4e8777c89be4/go.mod h1:flIaEI6LNU6xOCD5PaJvn9wGP0agmIOqjrtsKGRguv4=
golang.org/x/arch v0.0.0-20201008161808-52c3e6f60cff/go.mod h1:flIaEI6LNU6xOCD5PaJvn9wGP0agmIOqjrtsKGRguv4=
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9 h1:psW17arqaxU48Z5kZ0CQnkZWQJsqcURM6tKiBApRjXI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
golang.org/x/net v0.0.0-20180218175443-cbe0f9307d01/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181023162649-9b4f9f5ad519/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181201002055-351d144fa1fc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190125091013-d26f9f9a57f3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190522155817-f3200d17e092/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200625001655-4c5254603344 h1:vGXIOMxbNfDTk/aXCmfdLgkrSV+Z2tcbze+pEc3v5W4=
golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/net v0.0.0-20201029221708-28c70e62bb1d h1:dOiJ2n2cMwGLce/74I/QHMbnpk5GfY7InR8rczoMqRM=
golang.org/x/net v0.0.0-20201029221708-28c70e62bb1d/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/oauth2 v0.0.0-20170912212905-13449ad91cb2/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d h1:TzXSXBo42m9gQenoE3b9BGiEpg5IG2JkU5FkPIawgtw=
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/sync v0.0.0-20170517211232-f52d1811a629/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181026203630-95b1ffbd15a5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181122145206-62eef0e2fa9b/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190626221950-04f50cda93cb/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190826190057-c7b8b68b1456/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191220142924-d4481acd189f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200106162015-b016eb3dc98e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200724161237-0e2f3a69832c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201009025420-dfb3f7c4e634/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201214210602-f9fddec55a1e h1:AyodaIpKjppX+cBfTASF2E1US3H2JFBj920Ot3rtDjs=
golang.org/x/sys v0.0.0-20201214210602-f9fddec55a1e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.3 h1:cokOdA+Jmi5PJGXLlLllQSgYigAEfHXJAERHVMaCc2k=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/time v0.0.0-20170424234030-8be79e1e0910/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191127201027-ecd32218bd7f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20200103221440-774c71fcf114/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/api v0.0.0-20170921000349-586095a6e407/go.mod h1:4mhQ8q/RsB7i+udVvVy5NUi08OU8ZlA0gRVgrF7VFY0=
google.golang.org/api v0.3.1/go.mod h1:6wY9I6uQWHQ8EM57III9mq/AjF+i8G65rmVagqKMtkk=
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
google.golang.org/appengine v1.2.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
google.golang.org/appengine v1.6.6 h1:lMO5rYAqUxkmaj76jAkRUvt5JZgFymx/+Q5Mzfivuhc=
google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
google.golang.org/genproto v0.0.0-20170918111702-1e559d0a00ee/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190530194941-fb225487d101/go.mod h1:z3L6/3dTEVtUr6QSP8miRzeRqwQOioJ9I66odjN4I7s=
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
google.golang.org/grpc v1.2.1-0.20170921194603-d4b75ebd4f9f/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw=
google.golang.org/grpc v1.17.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
google.golang.org/grpc v1.20.0/go.mod h1:chYK+tFQF0nDUGJgXMSgLCQk3phJEuONr2DCgLDdAQM=
google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
google.golang.org/grpc v1.21.0/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
google.golang.org/grpc v1.22.1/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
google.golang.org/grpc v1.23.1/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.25.0 h1:Ejskq+SyPohKW+1uil0JJMtmHCgJPJ/qWTxr8qp+R4c=
google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/cheggaaa/pb.v1 v1.0.25/go.mod h1:V/YB90LKu/1FcN3WVnfiiE5oMCibMjukxqG/qStrOgw=
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
gopkg.in/gcfg.v1 v1.2.3/go.mod h1:yesOnuUOFQAhST5vPY4nbZsb/huCgGGXlipJsBn0b3o=
gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo=
gopkg.in/square/go-jose.v2 v2.5.0 h1:OZ4sdq+Y+SHfYB7vfthi1Ei8b0vkP8ZPQgUfUwdUSqo=
gopkg.in/square/go-jose.v2 v2.5.0/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI=
gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74=
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
mvdan.cc/xurls/v2 v2.2.0 h1:NSZPykBXJFCetGZykLAxaL6SIpvbVy/UFEniIfHAa8A=
mvdan.cc/xurls/v2 v2.2.0/go.mod h1:EV1RMtya9D6G5DMYPGD8zTQzaHet6Jh8gFlRgGRJeO8=
rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=
sigs.k8s.io/yaml v1.1.0/go.mod h1:UJmg0vDUVViEyp3mgSv9WPwZCDxu4rQW1olrI1uml+o=
sourcegraph.com/sourcegraph/appdash v0.0.0-20190731080439-ebfcffb1b5c0/go.mod h1:hI742Nqp5OhwiqlzhgfbWU4mW4yO10fP+LoT9WOswdU=
-210
View File
@@ -1,210 +0,0 @@
// Copyright 2017 Frédéric Guillot. All rights reserved.
// Use of this source code is governed by the Apache 2.0
// license that can be found in the LICENSE file.
package http
import (
"bytes"
"crypto/tls"
"crypto/x509"
"encoding/json"
"fmt"
"io"
"net"
"net/http"
"net/url"
"strings"
"time"
"github.com/miniflux/miniflux/errors"
"github.com/miniflux/miniflux/logger"
"github.com/miniflux/miniflux/timer"
"github.com/miniflux/miniflux/version"
)
const (
// 20 seconds max.
requestTimeout = 20
// 15MB max.
maxBodySize = 1024 * 1024 * 15
)
var (
errInvalidCertificate = "Invalid SSL certificate (original error: %q)"
errTemporaryNetworkOperation = "This website is temporarily unreachable (original error: %q)"
errPermanentNetworkOperation = "This website is permanently unreachable (original error: %q)"
errRequestTimeout = "Website unreachable, the request timed out after %d seconds"
)
// Client is a HTTP Client :)
type Client struct {
url string
etagHeader string
lastModifiedHeader string
authorizationHeader string
username string
password string
Insecure bool
}
// Get execute a GET HTTP request.
func (c *Client) Get() (*Response, error) {
request, err := c.buildRequest(http.MethodGet, nil)
if err != nil {
return nil, err
}
return c.executeRequest(request)
}
// PostForm execute a POST HTTP request with form values.
func (c *Client) PostForm(values url.Values) (*Response, error) {
request, err := c.buildRequest(http.MethodPost, strings.NewReader(values.Encode()))
if err != nil {
return nil, err
}
request.Header.Add("Content-Type", "application/x-www-form-urlencoded")
return c.executeRequest(request)
}
// PostJSON execute a POST HTTP request with JSON payload.
func (c *Client) PostJSON(data interface{}) (*Response, error) {
b, err := json.Marshal(data)
if err != nil {
return nil, err
}
request, err := c.buildRequest(http.MethodPost, bytes.NewReader(b))
if err != nil {
return nil, err
}
request.Header.Add("Content-Type", "application/json")
return c.executeRequest(request)
}
func (c *Client) executeRequest(request *http.Request) (*Response, error) {
defer timer.ExecutionTime(time.Now(), fmt.Sprintf("[HttpClient] url=%s", c.url))
client := c.buildClient()
resp, err := client.Do(request)
if err != nil {
if uerr, ok := err.(*url.Error); ok {
switch uerr.Err.(type) {
case x509.CertificateInvalidError, x509.HostnameError:
err = errors.NewLocalizedError(errInvalidCertificate, uerr.Err)
case *net.OpError:
if uerr.Err.(*net.OpError).Temporary() {
err = errors.NewLocalizedError(errTemporaryNetworkOperation, uerr.Err)
} else {
err = errors.NewLocalizedError(errPermanentNetworkOperation, uerr.Err)
}
case net.Error:
nerr := uerr.Err.(net.Error)
if nerr.Timeout() {
err = errors.NewLocalizedError(errRequestTimeout, requestTimeout)
} else if nerr.Temporary() {
err = errors.NewLocalizedError(errTemporaryNetworkOperation, nerr)
}
}
}
return nil, err
}
if resp.ContentLength > maxBodySize {
return nil, fmt.Errorf("client: response too large (%d bytes)", resp.ContentLength)
}
response := &Response{
Body: resp.Body,
StatusCode: resp.StatusCode,
EffectiveURL: resp.Request.URL.String(),
LastModified: resp.Header.Get("Last-Modified"),
ETag: resp.Header.Get("ETag"),
ContentType: resp.Header.Get("Content-Type"),
ContentLength: resp.ContentLength,
}
logger.Debug("[HttpClient:%s] OriginalURL=%s, StatusCode=%d, ContentLength=%d, ContentType=%s, ETag=%s, LastModified=%s, EffectiveURL=%s",
request.Method,
c.url,
response.StatusCode,
resp.ContentLength,
response.ContentType,
response.ETag,
response.LastModified,
response.EffectiveURL,
)
return response, err
}
func (c *Client) buildRequest(method string, body io.Reader) (*http.Request, error) {
request, err := http.NewRequest(method, c.url, body)
if err != nil {
return nil, err
}
request.Header = c.buildHeaders()
if c.username != "" && c.password != "" {
request.SetBasicAuth(c.username, c.password)
}
return request, nil
}
func (c *Client) buildClient() http.Client {
client := http.Client{Timeout: time.Duration(requestTimeout * time.Second)}
if c.Insecure {
client.Transport = &http.Transport{
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
}
}
return client
}
func (c *Client) buildHeaders() http.Header {
headers := make(http.Header)
headers.Add("User-Agent", "Mozilla/5.0 (compatible; Miniflux/"+version.Version+"; +https://miniflux.net)")
headers.Add("Accept", "*/*")
if c.etagHeader != "" {
headers.Add("If-None-Match", c.etagHeader)
}
if c.lastModifiedHeader != "" {
headers.Add("If-Modified-Since", c.lastModifiedHeader)
}
if c.authorizationHeader != "" {
headers.Add("Authorization", c.authorizationHeader)
}
return headers
}
// NewClient returns a new HTTP client.
func NewClient(url string) *Client {
return &Client{url: url, Insecure: false}
}
// NewClientWithCredentials returns a new HTTP client that requires authentication.
func NewClientWithCredentials(url, username, password string) *Client {
return &Client{url: url, Insecure: false, username: username, password: password}
}
// NewClientWithAuthorization returns a new client with a custom authorization header.
func NewClientWithAuthorization(url, authorization string) *Client {
return &Client{url: url, Insecure: false, authorizationHeader: authorization}
}
// NewClientWithCacheHeaders returns a new HTTP client that send cache headers.
func NewClientWithCacheHeaders(url, etagHeader, lastModifiedHeader string) *Client {
return &Client{url: url, etagHeader: etagHeader, lastModifiedHeader: lastModifiedHeader, Insecure: false}
}
+335
View File
@@ -0,0 +1,335 @@
// Copyright 2018 Frédéric Guillot. All rights reserved.
// Use of this source code is governed by the Apache 2.0
// license that can be found in the LICENSE file.
package client // import "miniflux.app/http/client"
import (
"bytes"
"crypto/x509"
"encoding/json"
"fmt"
"io"
"io/ioutil"
"net"
"net/http"
"net/url"
"strings"
"time"
"miniflux.app/config"
"miniflux.app/errors"
"miniflux.app/logger"
"miniflux.app/timer"
url_helper "miniflux.app/url"
)
const (
defaultHTTPClientTimeout = 20
defaultHTTPClientMaxBodySize = 15 * 1024 * 1024
)
var (
errInvalidCertificate = "Invalid SSL certificate (original error: %q)"
errTemporaryNetworkOperation = "This website is temporarily unreachable (original error: %q)"
errPermanentNetworkOperation = "This website is permanently unreachable (original error: %q)"
errRequestTimeout = "Website unreachable, the request timed out after %d seconds"
)
// Client builds and executes HTTP requests.
type Client struct {
inputURL string
requestURL string
requestEtagHeader string
requestLastModifiedHeader string
requestAuthorizationHeader string
requestUsername string
requestPassword string
requestUserAgent string
useProxy bool
doNotFollowRedirects bool
ClientTimeout int
ClientMaxBodySize int64
ClientProxyURL string
}
// New initializes a new HTTP client.
func New(url string) *Client {
return &Client{
inputURL: url,
ClientTimeout: defaultHTTPClientTimeout,
ClientMaxBodySize: defaultHTTPClientMaxBodySize,
}
}
// NewClientWithConfig initializes a new HTTP client with application config options.
func NewClientWithConfig(url string, opts *config.Options) *Client {
return &Client{
inputURL: url,
requestUserAgent: opts.HTTPClientUserAgent(),
ClientTimeout: opts.HTTPClientTimeout(),
ClientMaxBodySize: opts.HTTPClientMaxBodySize(),
ClientProxyURL: opts.HTTPClientProxy(),
}
}
func (c *Client) String() string {
etagHeader := c.requestEtagHeader
if c.requestEtagHeader == "" {
etagHeader = "None"
}
lastModifiedHeader := c.requestLastModifiedHeader
if c.requestLastModifiedHeader == "" {
lastModifiedHeader = "None"
}
return fmt.Sprintf(
`InputURL=%q RequestURL=%q ETag=%s LastModified=%s Auth=%v UserAgent=%q`,
c.inputURL,
c.requestURL,
etagHeader,
lastModifiedHeader,
c.requestAuthorizationHeader != "" || (c.requestUsername != "" && c.requestPassword != ""),
c.requestUserAgent,
)
}
// WithCredentials defines the username/password for HTTP Basic authentication.
func (c *Client) WithCredentials(username, password string) *Client {
if username != "" && password != "" {
c.requestUsername = username
c.requestPassword = password
}
return c
}
// WithAuthorization defines the authorization HTTP header value.
func (c *Client) WithAuthorization(authorization string) *Client {
c.requestAuthorizationHeader = authorization
return c
}
// WithCacheHeaders defines caching headers.
func (c *Client) WithCacheHeaders(etagHeader, lastModifiedHeader string) *Client {
c.requestEtagHeader = etagHeader
c.requestLastModifiedHeader = lastModifiedHeader
return c
}
// WithProxy enables proxy for the current HTTP request.
func (c *Client) WithProxy() *Client {
c.useProxy = true
return c
}
// WithoutRedirects disables HTTP redirects.
func (c *Client) WithoutRedirects() *Client {
c.doNotFollowRedirects = true
return c
}
// WithUserAgent defines the User-Agent header to use for HTTP requests.
func (c *Client) WithUserAgent(userAgent string) *Client {
if userAgent != "" {
c.requestUserAgent = userAgent
}
return c
}
// Get performs a GET HTTP request.
func (c *Client) Get() (*Response, error) {
request, err := c.buildRequest(http.MethodGet, nil)
if err != nil {
return nil, err
}
return c.executeRequest(request)
}
// PostForm performs a POST HTTP request with form encoded values.
func (c *Client) PostForm(values url.Values) (*Response, error) {
request, err := c.buildRequest(http.MethodPost, strings.NewReader(values.Encode()))
if err != nil {
return nil, err
}
request.Header.Add("Content-Type", "application/x-www-form-urlencoded")
return c.executeRequest(request)
}
// PostJSON performs a POST HTTP request with a JSON payload.
func (c *Client) PostJSON(data interface{}) (*Response, error) {
b, err := json.Marshal(data)
if err != nil {
return nil, err
}
request, err := c.buildRequest(http.MethodPost, bytes.NewReader(b))
if err != nil {
return nil, err
}
request.Header.Add("Content-Type", "application/json")
return c.executeRequest(request)
}
func (c *Client) executeRequest(request *http.Request) (*Response, error) {
defer timer.ExecutionTime(time.Now(), fmt.Sprintf("[HttpClient] inputURL=%s", c.inputURL))
logger.Debug("[HttpClient:Before] Method=%s %s",
request.Method,
c.String(),
)
client := c.buildClient()
resp, err := client.Do(request)
if resp != nil {
defer resp.Body.Close()
}
if err != nil {
if uerr, ok := err.(*url.Error); ok {
switch uerr.Err.(type) {
case x509.CertificateInvalidError, x509.HostnameError:
err = errors.NewLocalizedError(errInvalidCertificate, uerr.Err)
case *net.OpError:
if uerr.Err.(*net.OpError).Temporary() {
err = errors.NewLocalizedError(errTemporaryNetworkOperation, uerr.Err)
} else {
err = errors.NewLocalizedError(errPermanentNetworkOperation, uerr.Err)
}
case net.Error:
nerr := uerr.Err.(net.Error)
if nerr.Timeout() {
err = errors.NewLocalizedError(errRequestTimeout, c.ClientTimeout)
} else if nerr.Temporary() {
err = errors.NewLocalizedError(errTemporaryNetworkOperation, nerr)
}
}
}
return nil, err
}
if resp.ContentLength > c.ClientMaxBodySize {
return nil, fmt.Errorf("client: response too large (%d bytes)", resp.ContentLength)
}
buf, err := ioutil.ReadAll(resp.Body)
if err != nil {
return nil, fmt.Errorf("client: error while reading body %v", err)
}
response := &Response{
Body: bytes.NewReader(buf),
StatusCode: resp.StatusCode,
EffectiveURL: resp.Request.URL.String(),
LastModified: resp.Header.Get("Last-Modified"),
ETag: resp.Header.Get("ETag"),
Expires: resp.Header.Get("Expires"),
ContentType: resp.Header.Get("Content-Type"),
ContentLength: resp.ContentLength,
}
logger.Debug("[HttpClient:After] Method=%s %s; Response => %s",
request.Method,
c.String(),
response,
)
// Ignore caching headers for feeds that do not want any cache.
if resp.Header.Get("Expires") == "0" {
logger.Debug("[HttpClient] Ignore caching headers for %q", response.EffectiveURL)
response.ETag = ""
response.LastModified = ""
}
return response, err
}
func (c *Client) buildRequest(method string, body io.Reader) (*http.Request, error) {
c.requestURL = url_helper.RequestURI(c.inputURL)
request, err := http.NewRequest(method, c.requestURL, body)
if err != nil {
return nil, err
}
request.Header = c.buildHeaders()
if c.requestUsername != "" && c.requestPassword != "" {
request.SetBasicAuth(c.requestUsername, c.requestPassword)
}
return request, nil
}
func (c *Client) buildClient() http.Client {
client := http.Client{
Timeout: time.Duration(c.ClientTimeout) * time.Second,
}
transport := &http.Transport{
Proxy: http.ProxyFromEnvironment,
DialContext: (&net.Dialer{
// Default is 30s.
Timeout: 10 * time.Second,
// Default is 30s.
KeepAlive: 15 * time.Second,
}).DialContext,
// Default is 100.
MaxIdleConns: 50,
// Default is 90s.
IdleConnTimeout: 10 * time.Second,
}
if c.doNotFollowRedirects {
client.CheckRedirect = func(req *http.Request, via []*http.Request) error {
return http.ErrUseLastResponse
}
}
if c.useProxy && c.ClientProxyURL != "" {
proxyURL, err := url.Parse(c.ClientProxyURL)
if err != nil {
logger.Error("[HttpClient] Proxy URL error: %v", err)
} else {
logger.Debug("[HttpClient] Use proxy: %s", proxyURL)
transport.Proxy = http.ProxyURL(proxyURL)
}
}
client.Transport = transport
return client
}
func (c *Client) buildHeaders() http.Header {
headers := make(http.Header)
headers.Add("Accept", "*/*")
if c.requestUserAgent != "" {
headers.Add("User-Agent", c.requestUserAgent)
}
if c.requestEtagHeader != "" {
headers.Add("If-None-Match", c.requestEtagHeader)
}
if c.requestLastModifiedHeader != "" {
headers.Add("If-Modified-Since", c.requestLastModifiedHeader)
}
if c.requestAuthorizationHeader != "" {
headers.Add("Authorization", c.requestAuthorizationHeader)
}
headers.Add("Connection", "close")
return headers
}
+53
View File
@@ -0,0 +1,53 @@
// Copyright 2020 Frédéric Guillot. All rights reserved.
// Use of this source code is governed by the Apache 2.0
// license that can be found in the LICENSE file.
package client // import "miniflux.app/http/client"
import (
"testing"
)
func TestClientWithDelay(t *testing.T) {
clt := New("http://httpbin.org/delay/5")
clt.ClientTimeout = 1
_, err := clt.Get()
if err == nil {
t.Fatal(`The client should stops after 1 second`)
}
}
func TestClientWithError(t *testing.T) {
clt := New("http://httpbin.org/status/502")
clt.ClientTimeout = 1
response, err := clt.Get()
if err != nil {
t.Fatal(err)
}
if response.StatusCode != 502 {
t.Fatalf(`Unexpected response status code: %d`, response.StatusCode)
}
if !response.HasServerFailure() {
t.Fatal(`A 500 error is considered as server failure`)
}
}
func TestClientWithResponseTooLarge(t *testing.T) {
clt := New("http://httpbin.org/bytes/100")
clt.ClientMaxBodySize = 10
_, err := clt.Get()
if err == nil {
t.Fatal(`The client should fails when reading a response too large`)
}
}
func TestClientWithBasicAuth(t *testing.T) {
clt := New("http://httpbin.org/basic-auth/testuser/testpassword")
clt.WithCredentials("testuser", "testpassword")
_, err := clt.Get()
if err != nil {
t.Fatalf(`The client should be authenticated successfully: %v`, err)
}
}
+10
View File
@@ -0,0 +1,10 @@
// Copyright 2018 Frédéric Guillot. All rights reserved.
// Use of this source code is governed by the Apache 2.0
// license that can be found in the LICENSE file.
/*
Package client provides an HTTP client builder.
*/
package client // import "miniflux.app/http/client"
+121
View File
@@ -0,0 +1,121 @@
// Copyright 2017 Frédéric Guillot. All rights reserved.
// Use of this source code is governed by the Apache 2.0
// license that can be found in the LICENSE file.
package client // import "miniflux.app/http/client"
import (
"bytes"
"fmt"
"io"
"io/ioutil"
"regexp"
"strings"
"unicode/utf8"
"golang.org/x/net/html/charset"
)
var xmlEncodingRegex = regexp.MustCompile(`<\?xml(.*)encoding=["'](.+)["'](.*)\?>`)
// Response wraps a server response.
type Response struct {
Body io.Reader
StatusCode int
EffectiveURL string
LastModified string
ETag string
Expires string
ContentType string
ContentLength int64
}
func (r *Response) String() string {
return fmt.Sprintf(
`StatusCode=%d EffectiveURL=%q LastModified=%q ETag=%s Expires=%s ContentType=%q ContentLength=%d`,
r.StatusCode,
r.EffectiveURL,
r.LastModified,
r.ETag,
r.Expires,
r.ContentType,
r.ContentLength,
)
}
// IsNotFound returns true if the resource doesn't exists anymore.
func (r *Response) IsNotFound() bool {
return r.StatusCode == 404 || r.StatusCode == 410
}
// IsNotAuthorized returns true if the resource require authentication.
func (r *Response) IsNotAuthorized() bool {
return r.StatusCode == 401
}
// HasServerFailure returns true if the status code represents a failure.
func (r *Response) HasServerFailure() bool {
return r.StatusCode >= 400
}
// IsModified returns true if the resource has been modified.
func (r *Response) IsModified(etag, lastModified string) bool {
if r.StatusCode == 304 {
return false
}
if r.ETag != "" && r.ETag == etag {
return false
}
if r.LastModified != "" && r.LastModified == lastModified {
return false
}
return true
}
// EnsureUnicodeBody makes sure the body is encoded in UTF-8.
//
// If a charset other than UTF-8 is detected, we convert the document to UTF-8.
// This is used by the scraper and feed readers.
//
// Do not forget edge cases:
//
// - Feeds with encoding specified only in Content-Type header and not in XML document
// - Feeds with encoding specified in both places
// - Feeds with encoding specified only in XML document and not in HTTP header
// - Feeds with wrong encoding defined and already in UTF-8
func (r *Response) EnsureUnicodeBody() (err error) {
buffer, err := ioutil.ReadAll(r.Body)
if err != nil {
return err
}
r.Body = bytes.NewReader(buffer)
if utf8.Valid(buffer) {
return nil
}
if strings.Contains(r.ContentType, "xml") {
// We ignore documents with encoding specified in XML prolog.
// This is going to be handled by the XML parser.
length := 1024
if len(buffer) < 1024 {
length = len(buffer)
}
if xmlEncodingRegex.Match(buffer[0:length]) {
return nil
}
}
r.Body, err = charset.NewReader(r.Body, r.ContentType)
return err
}
// BodyAsString returns the response body as string.
func (r *Response) BodyAsString() string {
bytes, _ := ioutil.ReadAll(r.Body)
return string(bytes)
}
+149
View File
@@ -0,0 +1,149 @@
// Copyright 2017 Frédéric Guillot. All rights reserved.
// Use of this source code is governed by the Apache 2.0
// license that can be found in the LICENSE file.
package client // import "miniflux.app/http/client"
import (
"bytes"
"io/ioutil"
"strings"
"testing"
"unicode/utf8"
)
func TestIsNotFound(t *testing.T) {
scenarios := map[int]bool{
200: false,
404: true,
410: true,
}
for input, expected := range scenarios {
r := &Response{StatusCode: input}
actual := r.IsNotFound()
if actual != expected {
t.Errorf(`Unexpected result, got %v instead of %v for status code %d`, actual, expected, input)
}
}
}
func TestIsNotAuthorized(t *testing.T) {
scenarios := map[int]bool{
200: false,
401: true,
403: false,
}
for input, expected := range scenarios {
r := &Response{StatusCode: input}
actual := r.IsNotAuthorized()
if actual != expected {
t.Errorf(`Unexpected result, got %v instead of %v for status code %d`, actual, expected, input)
}
}
}
func TestHasServerFailure(t *testing.T) {
scenarios := map[int]bool{
200: false,
404: true,
500: true,
}
for input, expected := range scenarios {
r := &Response{StatusCode: input}
actual := r.HasServerFailure()
if actual != expected {
t.Errorf(`Unexpected result, got %v instead of %v for status code %d`, actual, expected, input)
}
}
}
func TestIsModifiedWith304Status(t *testing.T) {
r := &Response{StatusCode: 304}
if r.IsModified("etag", "lastModified") {
t.Error("The resource should not be considered modified")
}
}
func TestIsModifiedWithIdenticalEtag(t *testing.T) {
r := &Response{StatusCode: 200, ETag: "etag"}
if r.IsModified("etag", "lastModified") {
t.Error("The resource should not be considered modified")
}
}
func TestIsModifiedWithIdenticalLastModified(t *testing.T) {
r := &Response{StatusCode: 200, LastModified: "lastModified"}
if r.IsModified("etag", "lastModified") {
t.Error("The resource should not be considered modified")
}
}
func TestIsModifiedWithDifferentHeaders(t *testing.T) {
r := &Response{StatusCode: 200, ETag: "some etag", LastModified: "some date"}
if !r.IsModified("etag", "lastModified") {
t.Error("The resource should be considered modified")
}
}
func TestToString(t *testing.T) {
input := `test`
r := &Response{Body: strings.NewReader(input)}
if r.BodyAsString() != input {
t.Error(`Unexpected ouput`)
}
}
func TestEnsureUnicodeWithHTMLDocuments(t *testing.T) {
var unicodeTestCases = []struct {
filename, contentType string
convertedToUnicode bool
}{
{"HTTP-charset.html", "text/html; charset=iso-8859-15", true},
{"UTF-16LE-BOM.html", "", true},
{"UTF-16BE-BOM.html", "", true},
{"meta-content-attribute.html", "text/html", true},
{"meta-charset-attribute.html", "text/html", true},
{"No-encoding-declaration.html", "text/html", true},
{"HTTP-vs-UTF-8-BOM.html", "text/html; charset=iso-8859-15", true},
{"HTTP-vs-meta-content.html", "text/html; charset=iso-8859-15", true},
{"HTTP-vs-meta-charset.html", "text/html; charset=iso-8859-15", true},
{"UTF-8-BOM-vs-meta-content.html", "text/html", true},
{"UTF-8-BOM-vs-meta-charset.html", "text/html", true},
{"windows_1251.html", "text/html; charset=windows-1251", true},
{"gb2312.html", "text/html", true},
{"urdu.xml", "text/xml; charset=utf-8", true},
{"content-type-only-win-8859-1.xml", "application/xml; charset=ISO-8859-1", true},
{"rdf_utf8.xml", "application/rss+xml; charset=utf-8", true},
{"rdf_utf8.xml", "application/rss+xml; charset: utf-8", true}, // Invalid Content-Type
{"charset-content-type-xml-iso88591.xml", "application/rss+xml; charset=ISO-8859-1", false},
{"windows_1251.xml", "text/xml", false},
{"smallfile.xml", "text/xml; charset=utf-8", true},
{"single_quote_xml_encoding.xml", "text/xml; charset=utf-8", true},
}
for _, tc := range unicodeTestCases {
content, err := ioutil.ReadFile("testdata/" + tc.filename)
if err != nil {
t.Fatalf(`Unable to read file %q: %v`, tc.filename, err)
}
r := &Response{Body: bytes.NewReader(content), ContentType: tc.contentType}
parseErr := r.EnsureUnicodeBody()
if parseErr != nil {
t.Fatalf(`Unicode conversion error for %q - %q: %v`, tc.filename, tc.contentType, parseErr)
}
isUnicode := utf8.ValidString(r.BodyAsString())
if isUnicode != tc.convertedToUnicode {
t.Errorf(`Unicode conversion %q - %q, got: %v, expected: %v`,
tc.filename, tc.contentType, isUnicode, tc.convertedToUnicode)
}
}
}
@@ -0,0 +1,422 @@
<?xml version="1.0" encoding="ISO-8859-1"?>
<!-- generator="FeedCreator 1.6" -->
<rss xmlns:content="http://purl.org/rss/1.0/modules/content/"
xmlns:slash="http://purl.org/rss/1.0/modules/slash/"
xmlns:atom="http://www.w3.org/2005/Atom"
version="2.0">
<channel>
<title>Golem.de</title>
<description>IT-News fuer Profis</description>
<link>https://www.golem.de/</link>
<atom:link rel="self" href="https://rss.golem.de/rss.php?feed=RSS2.0" />
<lastBuildDate>Sun, 28 Oct 2018 13:49:01 +0100</lastBuildDate>
<generator>FeedCreator 1.6</generator>
<image>
<url>https://www.golem.de/staticrl/images/golem-rss.png</url>
<title>Golem.de</title>
<link>https://www.golem.de/</link>
<description>Golem.de News Feed</description>
</image>
<language>de</language>
<atom:link rel="hub" href="http://golem.superfeedr.com/" />
<item>
<title>Red Dead Redemption 2: Hinweise auf PC-Umsetzung in App von Rockstar Games</title>
<link>https://www.golem.de/news/red-dead-redemption-2-hinweise-auf-pc-umsetzung-in-app-von-rockstar-games-1810-137358-rss.html</link>
<description>Viele Spieler wünschen sich eine PC-Version von Red Dead Redemption 2, aber Entwickler Rockstar Games schweigt zu dem Thema. Anders die offizielle Companion App: In einigen ihrer Daten gibt es Hinweise auf die Umsetzung. (&lt;a href=&quot;https://www.golem.de/specials/red-dead-redemption-2/&quot;&gt;Red Dead Redemption 2&lt;/a&gt;, &lt;a href=&quot;https://www.golem.de/specials/red-dead-redemption/&quot;&gt;Red Dead Redemption&lt;/a&gt;) &lt;img src=&quot;https://cpx.golem.de/cpx.php?class=17&amp;amp;aid=137358&amp;amp;page=1&amp;amp;ts=1540730880&quot; alt=&quot;&quot; width=&quot;1&quot; height=&quot;1&quot; /&gt;</description>
<pubDate>Sun, 28 Oct 2018 13:48:00 +0100</pubDate>
<guid>https://www.golem.de/1810/137358-rss.html</guid>
<content:encoded><![CDATA[<img src="https://www.golem.de/1810/137358-177541-177538_rc.jpg" width="140" height="140" vspace="3" hspace="8" align="left">Viele Spieler wünschen sich eine PC-Version von Red Dead Redemption 2, aber Entwickler Rockstar Games schweigt zu dem Thema. Anders die offizielle Companion App: In einigen ihrer Daten gibt es Hinweise auf die Umsetzung. (<a href="https://www.golem.de/specials/red-dead-redemption-2/">Red Dead Redemption 2</a>, <a href="https://www.golem.de/specials/red-dead-redemption/">Red Dead Redemption</a>) <img src="https://cpx.golem.de/cpx.php?class=17&amp;aid=137358&amp;page=1&amp;ts=1540730880" alt="" width="1" height="1" />]]></content:encoded>
<slash:comments />
</item>
<item>
<title>Let's Play: Twitch will Streamer zusammen spielen und singen lassen</title>
<link>https://www.golem.de/news/let-s-play-twitch-will-streamer-zusammen-spielen-und-singen-lassen-1810-137357-rss.html</link>
<description>Der Streamingdienst Twitch hat auf seiner Hausmesse neue Funktionen für Kanalbetreiber und Zuschauer vorgestellt. Unter anderem soll es künftig Übertragungen mit bis zu vier Spielern geben - und Singwettbewerbe. (&lt;a href=&quot;https://www.golem.de/specials/twitch/&quot;&gt;Twitch&lt;/a&gt;, &lt;a href=&quot;https://www.golem.de/specials/amazon/&quot;&gt;Amazon&lt;/a&gt;) &lt;img src=&quot;https://cpx.golem.de/cpx.php?class=17&amp;amp;aid=137357&amp;amp;page=1&amp;amp;ts=1540728000&quot; alt=&quot;&quot; width=&quot;1&quot; height=&quot;1&quot; /&gt;</description>
<comments>https://forum.golem.de/kommentare/games/let-s-play-twitch-will-streamer-zusammen-spielen-und-singen-lassen/121579,list.html</comments>
<pubDate>Sun, 28 Oct 2018 13:00:00 +0100</pubDate>
<guid>https://www.golem.de/1810/137357-rss.html</guid>
<content:encoded><![CDATA[<img src="https://www.golem.de/1810/137357-177536-177533_rc.jpg" width="140" height="140" vspace="3" hspace="8" align="left">Der Streamingdienst Twitch hat auf seiner Hausmesse neue Funktionen für Kanalbetreiber und Zuschauer vorgestellt. Unter anderem soll es künftig Übertragungen mit bis zu vier Spielern geben - und Singwettbewerbe. (<a href="https://www.golem.de/specials/twitch/">Twitch</a>, <a href="https://www.golem.de/specials/amazon/">Amazon</a>) <img src="https://cpx.golem.de/cpx.php?class=17&amp;aid=137357&amp;page=1&amp;ts=1540728000" alt="" width="1" height="1" />]]></content:encoded>
<slash:comments />
</item>
<item>
<title>Zhuque-1: Erste private chinesische Satellitenmission fehlgeschlagen</title>
<link>https://www.golem.de/news/zhuque-1-erste-private-chinesische-satellitenmission-fehlgeschlagen-1810-137356-rss.html</link>
<description>Die Zhuque-1 hat es nicht in den Orbit geschafft: Beim Zünden der dritten Raketenstufe kam es zu Problemen. Bei einem Erfolg wäre der Hersteller Landspace das erste von rund 60 kommerziellen chinesischen Unternehmen gewesen, das einen Satelliten ins All gebracht hätte. (&lt;a href=&quot;https://www.golem.de/specials/raumfahrt/&quot;&gt;Raumfahrt&lt;/a&gt;, &lt;a href=&quot;https://www.golem.de/specials/internet/&quot;&gt;Internet&lt;/a&gt;) &lt;img src=&quot;https://cpx.golem.de/cpx.php?class=17&amp;amp;aid=137356&amp;amp;page=1&amp;amp;ts=1540722420&quot; alt=&quot;&quot; width=&quot;1&quot; height=&quot;1&quot; /&gt;</description>
<comments>https://forum.golem.de/kommentare/internet/zhuque-1-erste-private-chinesische-satellitenmission-fehlgeschlagen/121578,list.html</comments>
<pubDate>Sun, 28 Oct 2018 11:27:00 +0100</pubDate>
<guid>https://www.golem.de/1810/137356-rss.html</guid>
<content:encoded><![CDATA[<img src="https://www.golem.de/1810/137356-177532-177529_rc.jpg" width="140" height="140" vspace="3" hspace="8" align="left">Die Zhuque-1 hat es nicht in den Orbit geschafft: Beim Zünden der dritten Raketenstufe kam es zu Problemen. Bei einem Erfolg wäre der Hersteller Landspace das erste von rund 60 kommerziellen chinesischen Unternehmen gewesen, das einen Satelliten ins All gebracht hätte. (<a href="https://www.golem.de/specials/raumfahrt/">Raumfahrt</a>, <a href="https://www.golem.de/specials/internet/">Internet</a>) <img src="https://cpx.golem.de/cpx.php?class=17&amp;aid=137356&amp;page=1&amp;ts=1540722420" alt="" width="1" height="1" />]]></content:encoded>
<slash:comments>1</slash:comments>
</item>
<item>
<title>City Transformer: Startup entwickelt faltbares Elektroauto gegen Parkplatznot</title>
<link>https://www.golem.de/news/city-transformer-startup-entwickelt-faltbares-elektroauto-gegen-parkplatznot-1810-137355-rss.html</link>
<description>Es passt fast in jede Parklücke: Ein Faltauto des Startups City Transformer soll Städtern künftig das Leben erleichtern. Das innovative Fahrzeug wird zusammen mit Yamaha entwickelt. Vorbestellungen sollen voraussichtlich ab 2020 möglich sein, mehrere Versionen sind geplant. (&lt;a href=&quot;https://www.golem.de/specials/elektromobilitaet/&quot;&gt;Elektromobilität&lt;/a&gt;, &lt;a href=&quot;https://www.golem.de/specials/elektroauto/&quot;&gt;Elektroauto&lt;/a&gt;) &lt;img src=&quot;https://cpx.golem.de/cpx.php?class=17&amp;amp;aid=137355&amp;amp;page=1&amp;amp;ts=1540721400&quot; alt=&quot;&quot; width=&quot;1&quot; height=&quot;1&quot; /&gt;</description>
<comments>https://forum.golem.de/kommentare/automobil/city-transformer-startup-entwickelt-faltbares-elektroauto-gegen-parkplatznot/121577,list.html</comments>
<pubDate>Sun, 28 Oct 2018 11:10:00 +0100</pubDate>
<guid>https://www.golem.de/1810/137355-rss.html</guid>
<content:encoded><![CDATA[<img src="https://www.golem.de/1810/137355-177527-177524_rc.jpg" width="140" height="140" vspace="3" hspace="8" align="left">Es passt fast in jede Parklücke: Ein Faltauto des Startups City Transformer soll Städtern künftig das Leben erleichtern. Das innovative Fahrzeug wird zusammen mit Yamaha entwickelt. Vorbestellungen sollen voraussichtlich ab 2020 möglich sein, mehrere Versionen sind geplant. (<a href="https://www.golem.de/specials/elektromobilitaet/">Elektromobilität</a>, <a href="https://www.golem.de/specials/elektroauto/">Elektroauto</a>) <img src="https://cpx.golem.de/cpx.php?class=17&amp;aid=137355&amp;page=1&amp;ts=1540721400" alt="" width="1" height="1" />]]></content:encoded>
<slash:comments>37</slash:comments>
</item>
<item>
<title>Machine Learning: Von KI erstelltes Porträt für 432.500 US.Dollar versteigert</title>
<link>https://www.golem.de/news/machine-learning-von-ki-erstelltes-portraet-fuer-432-500-us-dollar-versteigert-1810-137353-rss.html</link>
<description>Kann Software Kunst erstellen? Eine erfolgreiche Auktion beweist, dass es zumindest Abnehmer dafür gibt. Allerdings hat sich das Entwicklerteam Obvious wohl stark bei anderen KI-Systemen bedient. (&lt;a href=&quot;https://www.golem.de/specials/neuronalesnetzwerk/&quot;&gt;Neuronales Netzwerk&lt;/a&gt;, &lt;a href=&quot;https://www.golem.de/specials/ki/&quot;&gt;KI&lt;/a&gt;) &lt;img src=&quot;https://cpx.golem.de/cpx.php?class=17&amp;amp;aid=137353&amp;amp;page=1&amp;amp;ts=1540643580&quot; alt=&quot;&quot; width=&quot;1&quot; height=&quot;1&quot; /&gt;</description>
<comments>https://forum.golem.de/kommentare/applikationen/machine-learning-von-ki-erstelltes-portraet-fuer-432.500-us.dollar-versteigert/121575,list.html</comments>
<pubDate>Sat, 27 Oct 2018 13:33:00 +0100</pubDate>
<guid>https://www.golem.de/1810/137353-rss.html</guid>
<content:encoded><![CDATA[<img src="https://www.golem.de/1810/137353-177523-177520_rc.jpg" width="140" height="140" vspace="3" hspace="8" align="left">Kann Software Kunst erstellen? Eine erfolgreiche Auktion beweist, dass es zumindest Abnehmer dafür gibt. Allerdings hat sich das Entwicklerteam Obvious wohl stark bei anderen KI-Systemen bedient. (<a href="https://www.golem.de/specials/neuronalesnetzwerk/">Neuronales Netzwerk</a>, <a href="https://www.golem.de/specials/ki/">KI</a>) <img src="https://cpx.golem.de/cpx.php?class=17&amp;aid=137353&amp;page=1&amp;ts=1540643580" alt="" width="1" height="1" />]]></content:encoded>
<slash:comments>31</slash:comments>
</item>
<item>
<title>Projekt Jedi: Microsoft will weiter mit US-Militär zusammenarbeiten</title>
<link>https://www.golem.de/news/project-jedi-microsoft-will-weiter-mit-us-militaer-zusammenarbeiten-1810-137352-rss.html</link>
<description>In einem Blogbeitrag hat sich Microsoft-Präsident Brad Smith zur Zusammenarbeit mit dem US-Verteidigungsministerium bekannt. Mitarbeiter, die nicht an derartigen Projekten arbeiten wollen, sollen in andere Bereiche des Unternehmens wechseln können. (&lt;a href=&quot;https://www.golem.de/specials/microsoft/&quot;&gt;Microsoft&lt;/a&gt;, &lt;a href=&quot;https://www.golem.de/specials/google/&quot;&gt;Google&lt;/a&gt;) &lt;img src=&quot;https://cpx.golem.de/cpx.php?class=17&amp;amp;aid=137352&amp;amp;page=1&amp;amp;ts=1540641780&quot; alt=&quot;&quot; width=&quot;1&quot; height=&quot;1&quot; /&gt;</description>
<comments>https://forum.golem.de/kommentare/internet/projekt-jedi-microsoft-will-weiter-mit-us-militaer-zusammenarbeiten/121574,list.html</comments>
<pubDate>Sat, 27 Oct 2018 13:03:00 +0100</pubDate>
<guid>https://www.golem.de/1810/137352-rss.html</guid>
<content:encoded><![CDATA[<img src="https://www.golem.de/1810/137055-176130-176127_rc.jpg" width="140" height="140" vspace="3" hspace="8" align="left">In einem Blogbeitrag hat sich Microsoft-Präsident Brad Smith zur Zusammenarbeit mit dem US-Verteidigungsministerium bekannt. Mitarbeiter, die nicht an derartigen Projekten arbeiten wollen, sollen in andere Bereiche des Unternehmens wechseln können. (<a href="https://www.golem.de/specials/microsoft/">Microsoft</a>, <a href="https://www.golem.de/specials/google/">Google</a>) <img src="https://cpx.golem.de/cpx.php?class=17&amp;aid=137352&amp;page=1&amp;ts=1540641780" alt="" width="1" height="1" />]]></content:encoded>
<slash:comments>21</slash:comments>
</item>
<item>
<title>Star Wars: Boba-Fett-Film ist &quot;zu 100 Prozent tot&quot;</title>
<link>https://www.golem.de/news/star-wars-boba-fett-film-ist-zu-100-prozent-tot-1810-137351-rss.html</link>
<description>Es wird wohl doch keinen dritten Star-Wars-Ableger geben, der sich um den kultigen Kopfgeldjäger Boba Fett dreht. Das wird laut einem Medienbericht teils auf den geringen Erfolg des Han-Solo-Films zurückgeführt. Stattdessen soll ein bisher unbekannter Charakter in einer Serie die mandalorianische Rüstung anziehen. (&lt;a href=&quot;https://www.golem.de/specials/star-wars/&quot;&gt;Star Wars&lt;/a&gt;, &lt;a href=&quot;https://www.golem.de/specials/film/&quot;&gt;Film&lt;/a&gt;) &lt;img src=&quot;https://cpx.golem.de/cpx.php?class=17&amp;amp;aid=137351&amp;amp;page=1&amp;amp;ts=1540639620&quot; alt=&quot;&quot; width=&quot;1&quot; height=&quot;1&quot; /&gt;</description>
<comments>https://forum.golem.de/kommentare/audio-video/star-wars-boba-fett-film-ist-zu-100-prozent-tot/121573,list.html</comments>
<pubDate>Sat, 27 Oct 2018 12:27:00 +0100</pubDate>
<guid>https://www.golem.de/1810/137351-rss.html</guid>
<content:encoded><![CDATA[<img src="https://www.golem.de/1810/137351-177519-177514_rc.jpg" width="140" height="140" vspace="3" hspace="8" align="left">Es wird wohl doch keinen dritten Star-Wars-Ableger geben, der sich um den kultigen Kopfgeldjäger Boba Fett dreht. Das wird laut einem Medienbericht teils auf den geringen Erfolg des Han-Solo-Films zurückgeführt. Stattdessen soll ein bisher unbekannter Charakter in einer Serie die mandalorianische Rüstung anziehen. (<a href="https://www.golem.de/specials/star-wars/">Star Wars</a>, <a href="https://www.golem.de/specials/film/">Film</a>) <img src="https://cpx.golem.de/cpx.php?class=17&amp;aid=137351&amp;page=1&amp;ts=1540639620" alt="" width="1" height="1" />]]></content:encoded>
<slash:comments>148</slash:comments>
</item>
<item>
<title>Lenovo: Fehlerhafte Bios-Einstellung macht Thinkpads unbrauchbar</title>
<link>https://www.golem.de/news/lenovo-fehlerhafte-bios-einstellung-macht-thinkpads-unbrauchbar-1810-137350-rss.html</link>
<description>Die Bios-Unterstützung für Thunderbolt bei Thinkpads zu aktivieren, ist derzeit keine gute Idee: Mehrere Nutzer berichten von nicht mehr startenden Notebooks, nachdem sie diese Funktion aktiviert haben. Das konnte auf diversen Linux-Distributionen, aber auch mit Windows 10 repliziert werden. (&lt;a href=&quot;https://www.golem.de/specials/lenovo/&quot;&gt;Lenovo&lt;/a&gt;, &lt;a href=&quot;https://www.golem.de/specials/business-notebooks/&quot;&gt;Business-Notebooks&lt;/a&gt;) &lt;img src=&quot;https://cpx.golem.de/cpx.php?class=17&amp;amp;aid=137350&amp;amp;page=1&amp;amp;ts=1540633200&quot; alt=&quot;&quot; width=&quot;1&quot; height=&quot;1&quot; /&gt;</description>
<comments>https://forum.golem.de/kommentare/applikationen/lenovo-fehlerhafte-bios-einstellung-macht-thinkpads-unbrauchbar/121572,list.html</comments>
<pubDate>Sat, 27 Oct 2018 10:40:00 +0100</pubDate>
<guid>https://www.golem.de/1810/137350-rss.html</guid>
<content:encoded><![CDATA[<img src="https://www.golem.de/1810/137350-177513-177510_rc.jpg" width="140" height="140" vspace="3" hspace="8" align="left">Die Bios-Unterstützung für Thunderbolt bei Thinkpads zu aktivieren, ist derzeit keine gute Idee: Mehrere Nutzer berichten von nicht mehr startenden Notebooks, nachdem sie diese Funktion aktiviert haben. Das konnte auf diversen Linux-Distributionen, aber auch mit Windows 10 repliziert werden. (<a href="https://www.golem.de/specials/lenovo/">Lenovo</a>, <a href="https://www.golem.de/specials/business-notebooks/">Business-Notebooks</a>) <img src="https://cpx.golem.de/cpx.php?class=17&amp;aid=137350&amp;page=1&amp;ts=1540633200" alt="" width="1" height="1" />]]></content:encoded>
<slash:comments>16</slash:comments>
</item>
<item>
<title>Wochenrückblick: Wilder Westen, buntes Handy, nutzloses Siegel</title>
<link>https://www.golem.de/news/wochenrueckblick-wilder-westen-buntes-handy-nutzloses-siegel-1810-137318-rss.html</link>
<description> Wir testen das iPhone Xr, sind ein Revolverheld und entdecken wieder Sicherheitslücken. Sieben Tage und viele Meldungen im Überblick. (&lt;a href=&quot;https://www.golem.de/specials/golemwochenrueckblick/&quot;&gt;Golem-Wochenrückblick&lt;/a&gt;, &lt;a href=&quot;https://www.golem.de/specials/business-notebooks/&quot;&gt;Business-Notebooks&lt;/a&gt;) &lt;img src=&quot;https://cpx.golem.de/cpx.php?class=17&amp;amp;aid=137318&amp;amp;page=1&amp;amp;ts=1540623720&quot; alt=&quot;&quot; width=&quot;1&quot; height=&quot;1&quot; /&gt;</description>
<comments>https://forum.golem.de/kommentare/politik-recht/wochenrueckblick-wilder-westen-buntes-handy-nutzloses-siegel/121571,list.html</comments>
<pubDate>Sat, 27 Oct 2018 08:02:00 +0100</pubDate>
<guid>https://www.golem.de/1810/137318-rss.html</guid>
<content:encoded><![CDATA[<img src="https://www.golem.de/1810/137318-177504-177501_rc.jpg" width="140" height="140" vspace="3" hspace="8" align="left"> Wir testen das iPhone Xr, sind ein Revolverheld und entdecken wieder Sicherheitslücken. Sieben Tage und viele Meldungen im Überblick. (<a href="https://www.golem.de/specials/golemwochenrueckblick/">Golem-Wochenrückblick</a>, <a href="https://www.golem.de/specials/business-notebooks/">Business-Notebooks</a>) <img src="https://cpx.golem.de/cpx.php?class=17&amp;aid=137318&amp;page=1&amp;ts=1540623720" alt="" width="1" height="1" />]]></content:encoded>
<slash:comments />
</item>
<item>
<title>Fernsehen: 5G-Netz wird so wichtig wie Strom und Wasser</title>
<link>https://www.golem.de/news/fernsehen-5g-netz-wird-so-wichtig-wie-strom-und-wasser-1810-137349-rss.html</link>
<description>Ein 5G-FeMBMS-Sendernetz für die Fernsehverbreitung sorgt für Aufsehen, noch bevor man weiß, ob es funktioniert. Wie Rundfunkübertragung und Mobilfunk zusammenkommen können, wurde auf den Medientagen München besprochen. (&lt;a href=&quot;https://www.golem.de/specials/fernsehen/&quot;&gt;Fernsehen&lt;/a&gt;, &lt;a href=&quot;https://www.golem.de/specials/technologie/&quot;&gt;Technologie&lt;/a&gt;) &lt;img src=&quot;https://cpx.golem.de/cpx.php?class=17&amp;amp;aid=137349&amp;amp;page=1&amp;amp;ts=1540572960&quot; alt=&quot;&quot; width=&quot;1&quot; height=&quot;1&quot; /&gt;</description>
<comments>https://forum.golem.de/kommentare/handy/fernsehen-5g-netz-wird-so-wichtig-wie-strom-und-wasser/121570,list.html</comments>
<pubDate>Fri, 26 Oct 2018 17:56:00 +0100</pubDate>
<guid>https://www.golem.de/1810/137349-rss.html</guid>
<content:encoded><![CDATA[<img src="https://www.golem.de/1810/137349-177509-177506_rc.jpg" width="140" height="140" vspace="3" hspace="8" align="left">Ein 5G-FeMBMS-Sendernetz für die Fernsehverbreitung sorgt für Aufsehen, noch bevor man weiß, ob es funktioniert. Wie Rundfunkübertragung und Mobilfunk zusammenkommen können, wurde auf den Medientagen München besprochen. (<a href="https://www.golem.de/specials/fernsehen/">Fernsehen</a>, <a href="https://www.golem.de/specials/technologie/">Technologie</a>) <img src="https://cpx.golem.de/cpx.php?class=17&amp;aid=137349&amp;page=1&amp;ts=1540572960" alt="" width="1" height="1" />]]></content:encoded>
<slash:comments>25</slash:comments>
</item>
<item>
<title>Linux und BSD: Sicherheitslücke in X.org ermöglicht Root-Rechte</title>
<link>https://www.golem.de/news/linux-und-bsd-sicherheitsluecke-in-x-org-ermoeglicht-root-rechte-1810-137347-rss.html</link>
<description>Eine Sicherheitslücke im Displayserver X.org erlaubt unter bestimmten Umständen das Überschreiben von Dateien und das Ausweiten der Benutzerrechte. Der passende Exploit passt in einen Tweet. (&lt;a href=&quot;https://www.golem.de/specials/sicherheitsluecke/&quot;&gt;Sicherheitslücke&lt;/a&gt;, &lt;a href=&quot;https://www.golem.de/specials/openbsd/&quot;&gt;OpenBSD&lt;/a&gt;) &lt;img src=&quot;https://cpx.golem.de/cpx.php?class=17&amp;amp;aid=137347&amp;amp;page=1&amp;amp;ts=1540564620&quot; alt=&quot;&quot; width=&quot;1&quot; height=&quot;1&quot; /&gt;</description>
<comments>https://forum.golem.de/kommentare/security/linux-und-bsd-sicherheitsluecke-in-x.org-ermoeglicht-root-rechte/121569,list.html</comments>
<pubDate>Fri, 26 Oct 2018 15:37:00 +0100</pubDate>
<guid>https://www.golem.de/1810/137347-rss.html</guid>
<content:encoded><![CDATA[<img src="https://www.golem.de/1810/137347-177500-177497_rc.jpg" width="140" height="140" vspace="3" hspace="8" align="left">Eine Sicherheitslücke im Displayserver X.org erlaubt unter bestimmten Umständen das Überschreiben von Dateien und das Ausweiten der Benutzerrechte. Der passende Exploit passt in einen Tweet. (<a href="https://www.golem.de/specials/sicherheitsluecke/">Sicherheitslücke</a>, <a href="https://www.golem.de/specials/openbsd/">OpenBSD</a>) <img src="https://cpx.golem.de/cpx.php?class=17&amp;aid=137347&amp;page=1&amp;ts=1540564620" alt="" width="1" height="1" />]]></content:encoded>
<slash:comments>47</slash:comments>
</item>
<item>
<title>Augsburg: Fujitsu Deutschland macht alles dicht</title>
<link>https://www.golem.de/news/augsburg-fujitsu-deutschland-macht-alles-dicht-1810-137348-rss.html</link>
<description>Fujitsu will seine gesamte Fertigung außerhalb Japans schließen. In Deutschland ist der Standort in Augsburg komplett betroffen. (&lt;a href=&quot;https://www.golem.de/specials/fujitsu/&quot;&gt;Fujitsu&lt;/a&gt;, &lt;a href=&quot;https://www.golem.de/specials/sap/&quot;&gt;SAP&lt;/a&gt;) &lt;img src=&quot;https://cpx.golem.de/cpx.php?class=17&amp;amp;aid=137348&amp;amp;page=1&amp;amp;ts=1540562340&quot; alt=&quot;&quot; width=&quot;1&quot; height=&quot;1&quot; /&gt;</description>
<comments>https://forum.golem.de/kommentare/wirtschaft/augsburg-fujitsu-deutschland-macht-alles-dicht/121568,list.html</comments>
<pubDate>Fri, 26 Oct 2018 14:59:00 +0100</pubDate>
<guid>https://www.golem.de/1810/137348-rss.html</guid>
<content:encoded><![CDATA[<img src="https://www.golem.de/1810/137348-177485-177484_rc.jpg" width="140" height="140" vspace="3" hspace="8" align="left">Fujitsu will seine gesamte Fertigung außerhalb Japans schließen. In Deutschland ist der Standort in Augsburg komplett betroffen. (<a href="https://www.golem.de/specials/fujitsu/">Fujitsu</a>, <a href="https://www.golem.de/specials/sap/">SAP</a>) <img src="https://cpx.golem.de/cpx.php?class=17&amp;aid=137348&amp;page=1&amp;ts=1540562340" alt="" width="1" height="1" />]]></content:encoded>
<slash:comments>56</slash:comments>
</item>
<item>
<title>Bundesnetzagentur: Seehofer fordert Verschiebung von 5G-Auktion</title>
<link>https://www.golem.de/news/bundesnetzagentur-seehofer-fordert-verschiebung-von-5g-auktion-1810-137346-rss.html</link>
<description>Bundesinnenminister Horst Seehofer will die 5G-Auktion verschieben, bis die ländlichen Regionen besser berücksichtigt werden. Er wird von einer Gruppe um den CDU-Abgeordneten Stefan Rouenhoff unterstützt. (&lt;a href=&quot;https://www.golem.de/specials/5g/&quot;&gt;5G&lt;/a&gt;, &lt;a href=&quot;https://www.golem.de/specials/bundesnetzagentur/&quot;&gt;Bundesnetzagentur&lt;/a&gt;) &lt;img src=&quot;https://cpx.golem.de/cpx.php?class=17&amp;amp;aid=137346&amp;amp;page=1&amp;amp;ts=1540557900&quot; alt=&quot;&quot; width=&quot;1&quot; height=&quot;1&quot; /&gt;</description>
<comments>https://forum.golem.de/kommentare/handy/bundesnetzagentur-seehofer-fordert-verschiebung-von-5g-auktion/121567,list.html</comments>
<pubDate>Fri, 26 Oct 2018 13:45:00 +0100</pubDate>
<guid>https://www.golem.de/1810/137346-rss.html</guid>
<content:encoded><![CDATA[<img src="https://www.golem.de/1810/137346-177483-177480_rc.jpg" width="140" height="140" vspace="3" hspace="8" align="left">Bundesinnenminister Horst Seehofer will die 5G-Auktion verschieben, bis die ländlichen Regionen besser berücksichtigt werden. Er wird von einer Gruppe um den CDU-Abgeordneten Stefan Rouenhoff unterstützt. (<a href="https://www.golem.de/specials/5g/">5G</a>, <a href="https://www.golem.de/specials/bundesnetzagentur/">Bundesnetzagentur</a>) <img src="https://cpx.golem.de/cpx.php?class=17&amp;aid=137346&amp;page=1&amp;ts=1540557900" alt="" width="1" height="1" />]]></content:encoded>
<slash:comments>14</slash:comments>
</item>
<item>
<title>Linux und Patente: Open Source bei Microsoft ist &quot;Kultur statt Strategie&quot;</title>
<link>https://www.golem.de/news/linux-und-patente-open-source-bei-microsoft-ist-kultur-statt-strategie-1810-137345-rss.html</link>
<description>Der Microsoft-Angestellte Stephen Walli beschreibt den Wandel bei Microsoft hin zu Open Source Software und Linux als kulturell getrieben. Mit Blick auf den Beitritt zu dem Patentpool des Open Invention Network zeigt sich jedoch auch, dass das Unternehmen noch sehr viel Arbeit vor sich hat. Ein Bericht von Sebastian Grüner (&lt;a href=&quot;https://www.golem.de/specials/microsoft/&quot;&gt;Microsoft&lt;/a&gt;, &lt;a href=&quot;https://www.golem.de/specials/softwarepatente/&quot;&gt;Softwarepatent&lt;/a&gt;) &lt;img src=&quot;https://cpx.golem.de/cpx.php?class=17&amp;amp;aid=137345&amp;amp;page=1&amp;amp;ts=1540556820&quot; alt=&quot;&quot; width=&quot;1&quot; height=&quot;1&quot; /&gt;</description>
<comments>https://forum.golem.de/kommentare/politik-recht/linux-und-patente-open-source-bei-microsoft-ist-kultur-statt-strategie/121566,list.html</comments>
<pubDate>Fri, 26 Oct 2018 13:27:00 +0100</pubDate>
<guid>https://www.golem.de/1810/137345-rss.html</guid>
<content:encoded><![CDATA[<img src="https://www.golem.de/1810/137345-177479-177475_rc.jpg" width="140" height="140" vspace="3" hspace="8" align="left">Der Microsoft-Angestellte Stephen Walli beschreibt den Wandel bei Microsoft hin zu Open Source Software und Linux als kulturell getrieben. Mit Blick auf den Beitritt zu dem Patentpool des Open Invention Network zeigt sich jedoch auch, dass das Unternehmen noch sehr viel Arbeit vor sich hat. Ein Bericht von Sebastian Grüner (<a href="https://www.golem.de/specials/microsoft/">Microsoft</a>, <a href="https://www.golem.de/specials/softwarepatente/">Softwarepatent</a>) <img src="https://cpx.golem.de/cpx.php?class=17&amp;aid=137345&amp;page=1&amp;ts=1540556820" alt="" width="1" height="1" />]]></content:encoded>
<slash:comments>20</slash:comments>
</item>
<item>
<title>Sicherheitslücke: Daten von 185.000 weiteren British-Airways-Kunden betroffen</title>
<link>https://www.golem.de/news/sicherheitsluecke-daten-von-185-000-weiteren-british-airways-kunden-betroffen-1810-137344-rss.html</link>
<description>Von dem Datenleck im Buchungssystem von British Airways waren deutlich mehr Kunden betroffen als bisher bekannt. Die Fluggesellschaft rät betroffenen Kunden, ihre Bank zu kontaktieren. Kreditkarten werden in diesem Fall häufig komplett ausgetauscht. (&lt;a href=&quot;https://www.golem.de/specials/sicherheitsluecke/&quot;&gt;Sicherheitslücke&lt;/a&gt;, &lt;a href=&quot;https://www.golem.de/specials/datenschutz/&quot;&gt;Datenschutz&lt;/a&gt;) &lt;img src=&quot;https://cpx.golem.de/cpx.php?class=17&amp;amp;aid=137344&amp;amp;page=1&amp;amp;ts=1540553820&quot; alt=&quot;&quot; width=&quot;1&quot; height=&quot;1&quot; /&gt;</description>
<comments>https://forum.golem.de/kommentare/security/sicherheitsluecke-daten-von-185.000-weiteren-british-airways-kunden-betroffen/121565,list.html</comments>
<pubDate>Fri, 26 Oct 2018 12:37:00 +0100</pubDate>
<guid>https://www.golem.de/1810/137344-rss.html</guid>
<content:encoded><![CDATA[<img src="https://www.golem.de/1810/137344-177474-177471_rc.jpg" width="140" height="140" vspace="3" hspace="8" align="left">Von dem Datenleck im Buchungssystem von British Airways waren deutlich mehr Kunden betroffen als bisher bekannt. Die Fluggesellschaft rät betroffenen Kunden, ihre Bank zu kontaktieren. Kreditkarten werden in diesem Fall häufig komplett ausgetauscht. (<a href="https://www.golem.de/specials/sicherheitsluecke/">Sicherheitslücke</a>, <a href="https://www.golem.de/specials/datenschutz/">Datenschutz</a>) <img src="https://cpx.golem.de/cpx.php?class=17&amp;aid=137344&amp;page=1&amp;ts=1540553820" alt="" width="1" height="1" />]]></content:encoded>
<slash:comments />
</item>
<item>
<title>iPhone Xr im Test: Apples günstigeres iPhone ist nicht günstig</title>
<link>https://www.golem.de/news/iphone-xr-im-test-apples-guenstiges-iphone-ist-nicht-guenstig-1810-137327-rss.html</link>
<description>Apple versucht es 2018 wieder einmal mit einem relativ preisgünstigen iPhone - weniger teuer als die Xs-Modelle, aber mit 850 Euro auch nicht gerade preiswert. Käufer bekommen dafür allerdings auch ein Smartphone mit sehr guter Ausstattung, in einigen Punkten wurde jedoch auf Hardware der teuren Modelle verzichtet. Ein Test von Tobias Költzsch (&lt;a href=&quot;https://www.golem.de/specials/iphone/&quot;&gt;iPhone&lt;/a&gt;, &lt;a href=&quot;https://www.golem.de/specials/smartphone/&quot;&gt;Smartphone&lt;/a&gt;) &lt;img src=&quot;https://cpx.golem.de/cpx.php?class=17&amp;amp;aid=137327&amp;amp;page=1&amp;amp;ts=1540548180&quot; alt=&quot;&quot; width=&quot;1&quot; height=&quot;1&quot; /&gt;</description>
<comments>https://forum.golem.de/kommentare/handy/iphone-xr-im-test-apples-guenstigeres-iphone-ist-nicht-guenstig/121563,list.html</comments>
<pubDate>Fri, 26 Oct 2018 11:03:00 +0100</pubDate>
<guid>https://www.golem.de/1810/137327-rss.html</guid>
<content:encoded><![CDATA[<img src="https://www.golem.de/1810/137327-177418-177414_rc.jpg" width="140" height="140" vspace="3" hspace="8" align="left">Apple versucht es 2018 wieder einmal mit einem relativ preisgünstigen iPhone - weniger teuer als die Xs-Modelle, aber mit 850 Euro auch nicht gerade preiswert. Käufer bekommen dafür allerdings auch ein Smartphone mit sehr guter Ausstattung, in einigen Punkten wurde jedoch auf Hardware der teuren Modelle verzichtet. Ein Test von Tobias Költzsch (<a href="https://www.golem.de/specials/iphone/">iPhone</a>, <a href="https://www.golem.de/specials/smartphone/">Smartphone</a>) <img src="https://cpx.golem.de/cpx.php?class=17&amp;aid=137327&amp;page=1&amp;ts=1540548180" alt="" width="1" height="1" />]]></content:encoded>
<slash:comments>169</slash:comments>
</item>
<item>
<title>Microsoft: PC-Spieleangebot des Xbox Game Pass wird erweitert</title>
<link>https://www.golem.de/news/microsoft-pc-spieleangebot-des-xbox-game-pass-wird-erweitert-1810-137343-rss.html</link>
<description>Der Xbox Game Pass soll künftig um mehr Angebote für Windows-PC erweitert werden, sagt Microsoft-Chef Satya Nadella. Derzeit gibt es für 10 Euro nur wenige plattformübergreifend verfügbare Spiele. (&lt;a href=&quot;https://www.golem.de/specials/xbox-one/&quot;&gt;Xbox One&lt;/a&gt;, &lt;a href=&quot;https://www.golem.de/specials/microsoft/&quot;&gt;Microsoft&lt;/a&gt;) &lt;img src=&quot;https://cpx.golem.de/cpx.php?class=17&amp;amp;aid=137343&amp;amp;page=1&amp;amp;ts=1540546800&quot; alt=&quot;&quot; width=&quot;1&quot; height=&quot;1&quot; /&gt;</description>
<comments>https://forum.golem.de/kommentare/wirtschaft/microsoft-pc-spieleangebot-des-xbox-game-pass-wird-erweitert/121562,list.html</comments>
<pubDate>Fri, 26 Oct 2018 10:40:00 +0100</pubDate>
<guid>https://www.golem.de/1810/137343-rss.html</guid>
<content:encoded><![CDATA[<img src="https://www.golem.de/1810/137343-177453-177450_rc.jpg" width="140" height="140" vspace="3" hspace="8" align="left">Der Xbox Game Pass soll künftig um mehr Angebote für Windows-PC erweitert werden, sagt Microsoft-Chef Satya Nadella. Derzeit gibt es für 10 Euro nur wenige plattformübergreifend verfügbare Spiele. (<a href="https://www.golem.de/specials/xbox-one/">Xbox One</a>, <a href="https://www.golem.de/specials/microsoft/">Microsoft</a>) <img src="https://cpx.golem.de/cpx.php?class=17&amp;aid=137343&amp;page=1&amp;ts=1540546800" alt="" width="1" height="1" />]]></content:encoded>
<slash:comments>19</slash:comments>
</item>
<item>
<title>Breitbandgesellschaft: Grüne wollen Netzbetreiber zum Ausbau zwingen</title>
<link>https://www.golem.de/news/breitbandgesellschaft-gruene-wollen-netzbetreiber-zum-ausbau-zwingen-1810-137342-rss.html</link>
<description>Die Grünen haben die Netzversorgung in Deutschland analysiert. Die Partei, die zurzeit in Wählerumfragen stark zugewinnt, fordert den Breitbandausbau auf Kosten der Konzerne und will Glasfaser staatlich durchsetzen. (&lt;a href=&quot;https://www.golem.de/specials/breitband/&quot;&gt;Breitband&lt;/a&gt;, &lt;a href=&quot;https://www.golem.de/specials/handy/&quot;&gt;Handy&lt;/a&gt;) &lt;img src=&quot;https://cpx.golem.de/cpx.php?class=17&amp;amp;aid=137342&amp;amp;page=1&amp;amp;ts=1540545840&quot; alt=&quot;&quot; width=&quot;1&quot; height=&quot;1&quot; /&gt;</description>
<comments>https://forum.golem.de/kommentare/politik-recht/breitbandgesellschaft-gruene-wollen-netzbetreiber-zum-ausbau-zwingen/121561,list.html</comments>
<pubDate>Fri, 26 Oct 2018 10:24:00 +0100</pubDate>
<guid>https://www.golem.de/1810/137342-rss.html</guid>
<content:encoded><![CDATA[<img src="https://www.golem.de/1807/135605-169300-169297_rc.jpg" width="140" height="140" vspace="3" hspace="8" align="left">Die Grünen haben die Netzversorgung in Deutschland analysiert. Die Partei, die zurzeit in Wählerumfragen stark zugewinnt, fordert den Breitbandausbau auf Kosten der Konzerne und will Glasfaser staatlich durchsetzen. (<a href="https://www.golem.de/specials/breitband/">Breitband</a>, <a href="https://www.golem.de/specials/handy/">Handy</a>) <img src="https://cpx.golem.de/cpx.php?class=17&amp;aid=137342&amp;page=1&amp;ts=1540545840" alt="" width="1" height="1" />]]></content:encoded>
<slash:comments>22</slash:comments>
</item>
<item>
<title>Studie: Silicon Valley dient als rechte Hand des großen Bruders</title>
<link>https://www.golem.de/news/studie-silicon-valley-dient-als-rechte-hand-des-grossen-bruders-1810-137316-rss.html</link>
<description>Die US-Hightech-Branche verdingt sich zunehmend als technischer Dienstleister für staatliche Big-Brother-Projekte wie die Überwachung und Abschiebung von Immigranten, heißt es in einem Bericht von Bürgerrechtlern. Amazon und Palantir verdienten damit am meisten. Von Stefan Krempl (&lt;a href=&quot;https://www.golem.de/specials/datenschutz/&quot;&gt;Datenschutz&lt;/a&gt;, &lt;a href=&quot;https://www.golem.de/specials/ibm/&quot;&gt;IBM&lt;/a&gt;) &lt;img src=&quot;https://cpx.golem.de/cpx.php?class=17&amp;amp;aid=137316&amp;amp;page=1&amp;amp;ts=1540543080&quot; alt=&quot;&quot; width=&quot;1&quot; height=&quot;1&quot; /&gt;</description>
<comments>https://forum.golem.de/kommentare/security/studie-silicon-valley-dient-als-rechte-hand-des-grossen-bruders/121560,list.html</comments>
<pubDate>Fri, 26 Oct 2018 09:38:00 +0100</pubDate>
<guid>https://www.golem.de/1810/137316-rss.html</guid>
<content:encoded><![CDATA[<img src="https://www.golem.de/1810/137316-177360-177357_rc.jpg" width="140" height="140" vspace="3" hspace="8" align="left">Die US-Hightech-Branche verdingt sich zunehmend als technischer Dienstleister für staatliche Big-Brother-Projekte wie die Überwachung und Abschiebung von Immigranten, heißt es in einem Bericht von Bürgerrechtlern. Amazon und Palantir verdienten damit am meisten. Von Stefan Krempl (<a href="https://www.golem.de/specials/datenschutz/">Datenschutz</a>, <a href="https://www.golem.de/specials/ibm/">IBM</a>) <img src="https://cpx.golem.de/cpx.php?class=17&amp;aid=137316&amp;page=1&amp;ts=1540543080" alt="" width="1" height="1" />]]></content:encoded>
<slash:comments>20</slash:comments>
</item>
<item>
<title>Bethesda: Postnukleare PC-Systemanforderungen für Fallout 76</title>
<link>https://www.golem.de/news/bethesda-postnukleare-pc-systemanforderungen-fuer-fallout-76-1810-137340-rss.html</link>
<description>Kurz vor dem Start der Betaversion für PC-Spieler hat Bethesda die Systemanforderung von Fallout 76 veröffentlicht. Hoffnungen auf lange Abenteuer in der Vorabversion gibt es aber zumindest nach den Erfahrungen des Xbox-Zugangs eher nicht. (&lt;a href=&quot;https://www.golem.de/specials/fallout-76/&quot;&gt;Fallout 76&lt;/a&gt;, &lt;a href=&quot;https://www.golem.de/specials/rollenspiel/&quot;&gt;Rollenspiel&lt;/a&gt;) &lt;img src=&quot;https://cpx.golem.de/cpx.php?class=17&amp;amp;aid=137340&amp;amp;page=1&amp;amp;ts=1540542180&quot; alt=&quot;&quot; width=&quot;1&quot; height=&quot;1&quot; /&gt;</description>
<comments>https://forum.golem.de/kommentare/games/bethesda-postnukleare-pc-systemanforderungen-fuer-fallout-76/121559,list.html</comments>
<pubDate>Fri, 26 Oct 2018 09:23:00 +0100</pubDate>
<guid>https://www.golem.de/1810/137340-rss.html</guid>
<content:encoded><![CDATA[<img src="https://www.golem.de/1810/137340-177448-177445_rc.jpg" width="140" height="140" vspace="3" hspace="8" align="left">Kurz vor dem Start der Betaversion für PC-Spieler hat Bethesda die Systemanforderung von Fallout 76 veröffentlicht. Hoffnungen auf lange Abenteuer in der Vorabversion gibt es aber zumindest nach den Erfahrungen des Xbox-Zugangs eher nicht. (<a href="https://www.golem.de/specials/fallout-76/">Fallout 76</a>, <a href="https://www.golem.de/specials/rollenspiel/">Rollenspiel</a>) <img src="https://cpx.golem.de/cpx.php?class=17&amp;aid=137340&amp;page=1&amp;ts=1540542180" alt="" width="1" height="1" />]]></content:encoded>
<slash:comments>101</slash:comments>
</item>
<item>
<title>Quartalszahlen: Intel legt 19-Milliarden-USD-Rekord vor</title>
<link>https://www.golem.de/news/quartalszahlen-intel-legt-19-milliarden-usd-rekord-vor-1810-137339-rss.html</link>
<description>Ungeachtet der 14-nm-Knappheit und diverser Sicherheitslücken konnte Intel im dritten Quartal 2018 mehr Umsatz erwirtschaften und mehr Gewinn erzielen als jemals zuvor. Vor allem das florierende Server-Geschäft wird bei Intel immer wichtiger. (&lt;a href=&quot;https://www.golem.de/specials/intel/&quot;&gt;Intel&lt;/a&gt;, &lt;a href=&quot;https://www.golem.de/specials/cpu/&quot;&gt;Prozessor&lt;/a&gt;) &lt;img src=&quot;https://cpx.golem.de/cpx.php?class=17&amp;amp;aid=137339&amp;amp;page=1&amp;amp;ts=1540540260&quot; alt=&quot;&quot; width=&quot;1&quot; height=&quot;1&quot; /&gt;</description>
<comments>https://forum.golem.de/kommentare/wirtschaft/quartalszahlen-intel-legt-19-milliarden-usd-rekord-vor/121558,list.html</comments>
<pubDate>Fri, 26 Oct 2018 08:51:00 +0100</pubDate>
<guid>https://www.golem.de/1810/137339-rss.html</guid>
<content:encoded><![CDATA[<img src="https://www.golem.de/1810/137339-177444-177441_rc.jpg" width="140" height="140" vspace="3" hspace="8" align="left">Ungeachtet der 14-nm-Knappheit und diverser Sicherheitslücken konnte Intel im dritten Quartal 2018 mehr Umsatz erwirtschaften und mehr Gewinn erzielen als jemals zuvor. Vor allem das florierende Server-Geschäft wird bei Intel immer wichtiger. (<a href="https://www.golem.de/specials/intel/">Intel</a>, <a href="https://www.golem.de/specials/cpu/">Prozessor</a>) <img src="https://cpx.golem.de/cpx.php?class=17&amp;aid=137339&amp;page=1&amp;ts=1540540260" alt="" width="1" height="1" />]]></content:encoded>
<slash:comments>10</slash:comments>
</item>
<item>
<title>Physik: Weg mit der Schönheit!</title>
<link>https://www.golem.de/news/physik-weg-mit-der-schoenheit-1810-137161-rss.html</link>
<description>Ist eine Theorie richtig, nur weil sie schön ist? Nein, sagt Sabine Hossenfelder. In ihrem Buch &quot;Das hässliche Universum&quot; zeigt die theoretische Physikerin, wie das Schönheitsdenken die Wissenschaft lähmt und erklärt dabei recht unterhaltsam die unterschiedlichen Theorien und Modelle der Teilchenphysik. Eine Rezension von Friedemann Zweynert (&lt;a href=&quot;https://www.golem.de/specials/physik/&quot;&gt;Physik&lt;/a&gt;, &lt;a href=&quot;https://www.golem.de/specials/internet/&quot;&gt;Internet&lt;/a&gt;) &lt;img src=&quot;https://cpx.golem.de/cpx.php?class=17&amp;amp;aid=137161&amp;amp;page=1&amp;amp;ts=1540537380&quot; alt=&quot;&quot; width=&quot;1&quot; height=&quot;1&quot; /&gt;</description>
<comments>https://forum.golem.de/kommentare/internet/physik-weg-mit-der-schoenheit/121557,list.html</comments>
<pubDate>Fri, 26 Oct 2018 08:03:00 +0100</pubDate>
<guid>https://www.golem.de/1810/137161-rss.html</guid>
<content:encoded><![CDATA[<img src="https://www.golem.de/1810/137161-176716-176713_rc.jpg" width="140" height="140" vspace="3" hspace="8" align="left">Ist eine Theorie richtig, nur weil sie schön ist? Nein, sagt Sabine Hossenfelder. In ihrem Buch "Das hässliche Universum" zeigt die theoretische Physikerin, wie das Schönheitsdenken die Wissenschaft lähmt und erklärt dabei recht unterhaltsam die unterschiedlichen Theorien und Modelle der Teilchenphysik. Eine Rezension von Friedemann Zweynert (<a href="https://www.golem.de/specials/physik/">Physik</a>, <a href="https://www.golem.de/specials/internet/">Internet</a>) <img src="https://cpx.golem.de/cpx.php?class=17&amp;aid=137161&amp;page=1&amp;ts=1540537380" alt="" width="1" height="1" />]]></content:encoded>
<slash:comments>86</slash:comments>
</item>
<item>
<title>Elon Musk: Teslas Model 3 für 35.000 US-Dollar derzeit unmöglich</title>
<link>https://www.golem.de/news/elon-musk-teslas-model-3-fuer-35-000-us-dollar-derzeit-unmoeglich-1810-137335-rss.html</link>
<description>Tesla-Chef Elon Musk hat eingeräumt, das bei 35.000 US-Dollar startende Basismodell des Elektroautos Model 3 immer noch nicht liefern zu können. (&lt;a href=&quot;https://www.golem.de/specials/tesla-model-3/&quot;&gt;Tesla Model 3&lt;/a&gt;, &lt;a href=&quot;https://www.golem.de/specials/technologie/&quot;&gt;Technologie&lt;/a&gt;) &lt;img src=&quot;https://cpx.golem.de/cpx.php?class=17&amp;amp;aid=137335&amp;amp;page=1&amp;amp;ts=1540533540&quot; alt=&quot;&quot; width=&quot;1&quot; height=&quot;1&quot; /&gt;</description>
<comments>https://forum.golem.de/kommentare/automobil/elon-musk-teslas-model-3-fuer-35.000-us-dollar-derzeit-unmoeglich/121556,list.html</comments>
<pubDate>Fri, 26 Oct 2018 06:59:00 +0100</pubDate>
<guid>https://www.golem.de/1810/137335-rss.html</guid>
<content:encoded><![CDATA[<img src="https://www.golem.de/1810/137335-177428-177424_rc.jpg" width="140" height="140" vspace="3" hspace="8" align="left">Tesla-Chef Elon Musk hat eingeräumt, das bei 35.000 US-Dollar startende Basismodell des Elektroautos Model 3 immer noch nicht liefern zu können. (<a href="https://www.golem.de/specials/tesla-model-3/">Tesla Model 3</a>, <a href="https://www.golem.de/specials/technologie/">Technologie</a>) <img src="https://cpx.golem.de/cpx.php?class=17&amp;aid=137335&amp;page=1&amp;ts=1540533540" alt="" width="1" height="1" />]]></content:encoded>
<slash:comments>235</slash:comments>
</item>
<item>
<title>Solarzellen als Dach: Tesla-Solarschindeln verzögern sich bis 2019</title>
<link>https://www.golem.de/news/solarzellen-als-dach-tesla-solarschindeln-verzoegern-sich-auf-2019-1810-137334-rss.html</link>
<description>Tesla wird die Serienproduktion seiner Solardachziegel nicht mehr wie geplant in diesem Jahr starten. Nach Angaben von Firmenchef Elon Musk verschiebt sich das Vorhaben auf 2019. (&lt;a href=&quot;https://www.golem.de/specials/solarenergie/&quot;&gt;Solarenergie&lt;/a&gt;, &lt;a href=&quot;https://www.golem.de/specials/technologie/&quot;&gt;Technologie&lt;/a&gt;) &lt;img src=&quot;https://cpx.golem.de/cpx.php?class=17&amp;amp;aid=137334&amp;amp;page=1&amp;amp;ts=1540532460&quot; alt=&quot;&quot; width=&quot;1&quot; height=&quot;1&quot; /&gt;</description>
<comments>https://forum.golem.de/kommentare/wissenschaft/solarzellen-als-dach-tesla-solarschindeln-verzoegern-sich-bis-2019/121555,list.html</comments>
<pubDate>Fri, 26 Oct 2018 06:41:00 +0100</pubDate>
<guid>https://www.golem.de/1810/137334-rss.html</guid>
<content:encoded><![CDATA[<img src="https://www.golem.de/1705/127761-139613-i_rc.jpg" width="140" height="140" vspace="3" hspace="8" align="left">Tesla wird die Serienproduktion seiner Solardachziegel nicht mehr wie geplant in diesem Jahr starten. Nach Angaben von Firmenchef Elon Musk verschiebt sich das Vorhaben auf 2019. (<a href="https://www.golem.de/specials/solarenergie/">Solarenergie</a>, <a href="https://www.golem.de/specials/technologie/">Technologie</a>) <img src="https://cpx.golem.de/cpx.php?class=17&amp;aid=137334&amp;page=1&amp;ts=1540532460" alt="" width="1" height="1" />]]></content:encoded>
<slash:comments>47</slash:comments>
</item>
<item>
<title>Uniti One: Elektroauto für 15.000 Euro wird in Großbritannien gebaut</title>
<link>https://www.golem.de/news/uniti-one-elektroauto-fuer-15-000-euro-wird-in-grossbritannien-gebaut-1810-137333-rss.html</link>
<description>Das schwedische Unternehmen Uniti will sein Elektroauto One in Großbritannien bauen. Einen fahrenden Prototyp des Uniti One gibt es schon. Das Auto soll je nach Modell für 15.000 bis 20.000 Euro auf den Markt kommen. (&lt;a href=&quot;https://www.golem.de/specials/elektroauto/&quot;&gt;Elektroauto&lt;/a&gt;, &lt;a href=&quot;https://www.golem.de/specials/technologie/&quot;&gt;Technologie&lt;/a&gt;) &lt;img src=&quot;https://cpx.golem.de/cpx.php?class=17&amp;amp;aid=137333&amp;amp;page=1&amp;amp;ts=1540531080&quot; alt=&quot;&quot; width=&quot;1&quot; height=&quot;1&quot; /&gt;</description>
<comments>https://forum.golem.de/kommentare/automobil/uniti-one-elektroauto-fuer-15.000-euro-wird-in-grossbritannien-gebaut/121554,list.html</comments>
<pubDate>Fri, 26 Oct 2018 06:18:00 +0100</pubDate>
<guid>https://www.golem.de/1810/137333-rss.html</guid>
<content:encoded><![CDATA[<img src="https://www.golem.de/1805/134432-163131-163128_rc.jpg" width="140" height="140" vspace="3" hspace="8" align="left">Das schwedische Unternehmen Uniti will sein Elektroauto One in Großbritannien bauen. Einen fahrenden Prototyp des Uniti One gibt es schon. Das Auto soll je nach Modell für 15.000 bis 20.000 Euro auf den Markt kommen. (<a href="https://www.golem.de/specials/elektroauto/">Elektroauto</a>, <a href="https://www.golem.de/specials/technologie/">Technologie</a>) <img src="https://cpx.golem.de/cpx.php?class=17&amp;aid=137333&amp;page=1&amp;ts=1540531080" alt="" width="1" height="1" />]]></content:encoded>
<slash:comments>28</slash:comments>
</item>
<item>
<title>Quartalsbericht: Alphabet macht in drei Monaten 9,2 Milliarden Dollar Gewinn</title>
<link>https://www.golem.de/news/quartalsbericht-alphabet-macht-in-drei-monaten-9-2-milliarden-dollar-gewinn-1810-137337-rss.html</link>
<description>Alphabet erwirtschaftet weiter extrem hohe Gewinne, der Umsatz wächst nicht ganz so stark. Google muss zugleich auf eine Enthüllung in der US-Presse zu sexueller Belästigung um Android-Begründer Andy Rubin reagieren. (&lt;a href=&quot;https://www.golem.de/specials/google/&quot;&gt;Google&lt;/a&gt;, &lt;a href=&quot;https://www.golem.de/specials/boerse/&quot;&gt;Börse&lt;/a&gt;) &lt;img src=&quot;https://cpx.golem.de/cpx.php?class=17&amp;amp;aid=137337&amp;amp;page=1&amp;amp;ts=1540504320&quot; alt=&quot;&quot; width=&quot;1&quot; height=&quot;1&quot; /&gt;</description>
<comments>https://forum.golem.de/kommentare/wirtschaft/quartalsbericht-alphabet-macht-in-drei-monaten-9-2-milliarden-dollar-gewinn/121553,list.html</comments>
<pubDate>Thu, 25 Oct 2018 22:52:00 +0100</pubDate>
<guid>https://www.golem.de/1810/137337-rss.html</guid>
<content:encoded><![CDATA[<img src="https://www.golem.de/1810/137053-176114-176111_rc.jpg" width="140" height="140" vspace="3" hspace="8" align="left">Alphabet erwirtschaftet weiter extrem hohe Gewinne, der Umsatz wächst nicht ganz so stark. Google muss zugleich auf eine Enthüllung in der US-Presse zu sexueller Belästigung um Android-Begründer Andy Rubin reagieren. (<a href="https://www.golem.de/specials/google/">Google</a>, <a href="https://www.golem.de/specials/boerse/">Börse</a>) <img src="https://cpx.golem.de/cpx.php?class=17&amp;aid=137337&amp;page=1&amp;ts=1540504320" alt="" width="1" height="1" />]]></content:encoded>
<slash:comments>12</slash:comments>
</item>
<item>
<title>Quartalsbericht: Amazon verfehlt die Umsatzprognosen</title>
<link>https://www.golem.de/news/quartalsbericht-amazon-verfehlt-die-umsatzprognosen-1810-137336-rss.html</link>
<description>Amazon weist erneut einen hohen Gewinn aus. Doch der Konzern lag beim Umsatz unter den Prognosen der Analysten. (&lt;a href=&quot;https://www.golem.de/specials/amazon/&quot;&gt;Amazon&lt;/a&gt;, &lt;a href=&quot;https://www.golem.de/specials/onlineshop/&quot;&gt;Onlineshop&lt;/a&gt;) &lt;img src=&quot;https://cpx.golem.de/cpx.php?class=17&amp;amp;aid=137336&amp;amp;page=1&amp;amp;ts=1540500780&quot; alt=&quot;&quot; width=&quot;1&quot; height=&quot;1&quot; /&gt;</description>
<comments>https://forum.golem.de/kommentare/wirtschaft/quartalsbericht-amazon-verfehlt-die-umsatzprognosen/121552,list.html</comments>
<pubDate>Thu, 25 Oct 2018 21:53:00 +0100</pubDate>
<guid>https://www.golem.de/1810/137336-rss.html</guid>
<content:encoded><![CDATA[<img src="https://www.golem.de/1810/137336-177432-177429_rc.jpg" width="140" height="140" vspace="3" hspace="8" align="left">Amazon weist erneut einen hohen Gewinn aus. Doch der Konzern lag beim Umsatz unter den Prognosen der Analysten. (<a href="https://www.golem.de/specials/amazon/">Amazon</a>, <a href="https://www.golem.de/specials/onlineshop/">Onlineshop</a>) <img src="https://cpx.golem.de/cpx.php?class=17&amp;aid=137336&amp;page=1&amp;ts=1540500780" alt="" width="1" height="1" />]]></content:encoded>
<slash:comments>24</slash:comments>
</item>
<item>
<title>Datenskandal: Britische Datenschutzbehörde verurteilt Facebook</title>
<link>https://www.golem.de/news/datenskandal-britische-datenschutzbehoerde-verurteilt-facebook-1810-137332-rss.html</link>
<description>Im Skandal um Cambridge Analytica hat die britische Datenschutzbehörde die Höchststrafe von 500.000 Pfund verhängt. Facebook habe einen schweren Verstoß gegen geltendes Recht zugelassen. (&lt;a href=&quot;https://www.golem.de/specials/facebook/&quot;&gt;Facebook&lt;/a&gt;, &lt;a href=&quot;https://www.golem.de/specials/socialnetwork/&quot;&gt;Soziales Netz&lt;/a&gt;) &lt;img src=&quot;https://cpx.golem.de/cpx.php?class=17&amp;amp;aid=137332&amp;amp;page=1&amp;amp;ts=1540483080&quot; alt=&quot;&quot; width=&quot;1&quot; height=&quot;1&quot; /&gt;</description>
<comments>https://forum.golem.de/kommentare/security/datenskandal-britische-datenschutzbehoerde-verurteilt-facebook/121550,list.html</comments>
<pubDate>Thu, 25 Oct 2018 16:58:00 +0100</pubDate>
<guid>https://www.golem.de/1810/137332-rss.html</guid>
<content:encoded><![CDATA[<img src="https://www.golem.de/1810/137332-177420-177419_rc.jpg" width="140" height="140" vspace="3" hspace="8" align="left">Im Skandal um Cambridge Analytica hat die britische Datenschutzbehörde die Höchststrafe von 500.000 Pfund verhängt. Facebook habe einen schweren Verstoß gegen geltendes Recht zugelassen. (<a href="https://www.golem.de/specials/facebook/">Facebook</a>, <a href="https://www.golem.de/specials/socialnetwork/">Soziales Netz</a>) <img src="https://cpx.golem.de/cpx.php?class=17&amp;aid=137332&amp;page=1&amp;ts=1540483080" alt="" width="1" height="1" />]]></content:encoded>
<slash:comments>35</slash:comments>
</item>
<item>
<title>Corsair: Neue K70 MK.2 kommt mit Cherrys Low-Profile-Switches</title>
<link>https://www.golem.de/news/corsair-neue-k70-mk-2-kommt-mit-cherrys-low-profile-switches-1810-137331-rss.html</link>
<description>Corsair erweitert sein Tastaturportefeuille um zwei Gaming-Tastaturen mit Cherrys flachen Low-Profile-Switches. Ein Modell hat Schalter mit einem besonders kurzem Auslöseweg von 1 mm - die Schalter darf Corsair exklusiv verwenden. (&lt;a href=&quot;https://www.golem.de/specials/corsair/&quot;&gt;Corsair&lt;/a&gt;, &lt;a href=&quot;https://www.golem.de/specials/eingabegeraet/&quot;&gt;Eingabegerät&lt;/a&gt;) &lt;img src=&quot;https://cpx.golem.de/cpx.php?class=17&amp;amp;aid=137331&amp;amp;page=1&amp;amp;ts=1540480320&quot; alt=&quot;&quot; width=&quot;1&quot; height=&quot;1&quot; /&gt;</description>
<comments>https://forum.golem.de/kommentare/sonstiges/corsair-neue-k70-mk.2-kommt-mit-cherrys-low-profile-switches/121549,list.html</comments>
<pubDate>Thu, 25 Oct 2018 16:12:00 +0100</pubDate>
<guid>https://www.golem.de/1810/137331-rss.html</guid>
<content:encoded><![CDATA[<img src="https://www.golem.de/1810/137331-177413-177410_rc.jpg" width="140" height="140" vspace="3" hspace="8" align="left">Corsair erweitert sein Tastaturportefeuille um zwei Gaming-Tastaturen mit Cherrys flachen Low-Profile-Switches. Ein Modell hat Schalter mit einem besonders kurzem Auslöseweg von 1 mm - die Schalter darf Corsair exklusiv verwenden. (<a href="https://www.golem.de/specials/corsair/">Corsair</a>, <a href="https://www.golem.de/specials/eingabegeraet/">Eingabegerät</a>) <img src="https://cpx.golem.de/cpx.php?class=17&amp;aid=137331&amp;page=1&amp;ts=1540480320" alt="" width="1" height="1" />]]></content:encoded>
<slash:comments>19</slash:comments>
</item>
<item>
<title>Cambridge-Analytica-Skandal: EU-Parlament fordert schärfere Kontrolle von Facebook</title>
<link>https://www.golem.de/news/cambridge-analytica-skandal-eu-parlament-fordert-schaerfere-kontrolle-von-facebook-1810-137329-rss.html</link>
<description>Die EU-Abgeordneten haben als Reaktion auf die Datenschutzverstöße von Facebook und Cambridge Analytica Behörden angehalten, ihre Aktivitäten auf dem Netzwerk zu überdenken. Profiling zu politischen Zwecken wollen sie verbieten. Von Stefan Krempl (&lt;a href=&quot;https://www.golem.de/specials/facebook/&quot;&gt;Facebook&lt;/a&gt;, &lt;a href=&quot;https://www.golem.de/specials/socialnetwork/&quot;&gt;Soziales Netz&lt;/a&gt;) &lt;img src=&quot;https://cpx.golem.de/cpx.php?class=17&amp;amp;aid=137329&amp;amp;page=1&amp;amp;ts=1540479120&quot; alt=&quot;&quot; width=&quot;1&quot; height=&quot;1&quot; /&gt;</description>
<comments>https://forum.golem.de/kommentare/security/cambridge-analytica-skandal-eu-parlament-fordert-schaerfere-kontrolle-von-facebook/121548,list.html</comments>
<pubDate>Thu, 25 Oct 2018 15:52:00 +0100</pubDate>
<guid>https://www.golem.de/1810/137329-rss.html</guid>
<content:encoded><![CDATA[<img src="https://www.golem.de/1810/137329-177404-177403_rc.jpg" width="140" height="140" vspace="3" hspace="8" align="left">Die EU-Abgeordneten haben als Reaktion auf die Datenschutzverstöße von Facebook und Cambridge Analytica Behörden angehalten, ihre Aktivitäten auf dem Netzwerk zu überdenken. Profiling zu politischen Zwecken wollen sie verbieten. Von Stefan Krempl (<a href="https://www.golem.de/specials/facebook/">Facebook</a>, <a href="https://www.golem.de/specials/socialnetwork/">Soziales Netz</a>) <img src="https://cpx.golem.de/cpx.php?class=17&amp;aid=137329&amp;page=1&amp;ts=1540479120" alt="" width="1" height="1" />]]></content:encoded>
<slash:comments>3</slash:comments>
</item>
<item>
<title>Kuriosum: Das Hellgate London öffnet sich mal wieder</title>
<link>https://www.golem.de/news/kuriosum-das-hellgate-london-oeffnet-sich-mal-wieder-1810-137328-rss.html</link>
<description>Einer der großen Trash-Klassiker der Spielegeschichte wagt einen neuen Anlauf: Mitte November 2018 soll ein neue, für Einzelspieler ausgelegte Windows-Fassung von Hellgate London erscheinen. Das Ursprungskonzept hatten sich ehemalige Blizzard-Chefentwickler ausgedacht. (&lt;a href=&quot;https://www.golem.de/specials/rollenspiel/&quot;&gt;Rollenspiel&lt;/a&gt;, &lt;a href=&quot;https://www.golem.de/specials/steam/&quot;&gt;Steam&lt;/a&gt;) &lt;img src=&quot;https://cpx.golem.de/cpx.php?class=17&amp;amp;aid=137328&amp;amp;page=1&amp;amp;ts=1540476600&quot; alt=&quot;&quot; width=&quot;1&quot; height=&quot;1&quot; /&gt;</description>
<comments>https://forum.golem.de/kommentare/games/kuriosum-das-hellgate-london-oeffnet-sich-mal-wieder/121547,list.html</comments>
<pubDate>Thu, 25 Oct 2018 15:10:00 +0100</pubDate>
<guid>https://www.golem.de/1810/137328-rss.html</guid>
<content:encoded><![CDATA[<img src="https://www.golem.de/1810/137328-177396-177392_rc.jpg" width="140" height="140" vspace="3" hspace="8" align="left">Einer der großen Trash-Klassiker der Spielegeschichte wagt einen neuen Anlauf: Mitte November 2018 soll ein neue, für Einzelspieler ausgelegte Windows-Fassung von Hellgate London erscheinen. Das Ursprungskonzept hatten sich ehemalige Blizzard-Chefentwickler ausgedacht. (<a href="https://www.golem.de/specials/rollenspiel/">Rollenspiel</a>, <a href="https://www.golem.de/specials/steam/">Steam</a>) <img src="https://cpx.golem.de/cpx.php?class=17&amp;aid=137328&amp;page=1&amp;ts=1540476600" alt="" width="1" height="1" />]]></content:encoded>
<slash:comments>61</slash:comments>
</item>
<item>
<title>Tweether: 10 GBit/s über einen Quadratkilometer verteilt</title>
<link>https://www.golem.de/news/tweether-10-gbit-s-ueber-einen-quadratkilometer-verteilt-1810-137326-rss.html</link>
<description>Eine neue Technologie verteilt 10 GBit/s über eine große Fläche. Der erste Feldversuch ist erfolgreich verlaufen. Zum ersten Mal wurde ein stabiles drahtloses Netzwerk bei diesen Frequenzen und mit diesen Datenraten betrieben. (&lt;a href=&quot;https://www.golem.de/specials/wissenschaft/&quot;&gt;Wissenschaft&lt;/a&gt;, &lt;a href=&quot;https://www.golem.de/specials/mobil/&quot;&gt;Mobil&lt;/a&gt;) &lt;img src=&quot;https://cpx.golem.de/cpx.php?class=17&amp;amp;aid=137326&amp;amp;page=1&amp;amp;ts=1540475400&quot; alt=&quot;&quot; width=&quot;1&quot; height=&quot;1&quot; /&gt;</description>
<comments>https://forum.golem.de/kommentare/wissenschaft/tweether-10-gbit-s-ueber-einen-quadratkilometer-verteilt/121546,list.html</comments>
<pubDate>Thu, 25 Oct 2018 14:50:00 +0100</pubDate>
<guid>https://www.golem.de/1810/137326-rss.html</guid>
<content:encoded><![CDATA[<img src="https://www.golem.de/1810/137326-177391-177388_rc.jpg" width="140" height="140" vspace="3" hspace="8" align="left">Eine neue Technologie verteilt 10 GBit/s über eine große Fläche. Der erste Feldversuch ist erfolgreich verlaufen. Zum ersten Mal wurde ein stabiles drahtloses Netzwerk bei diesen Frequenzen und mit diesen Datenraten betrieben. (<a href="https://www.golem.de/specials/wissenschaft/">Wissenschaft</a>, <a href="https://www.golem.de/specials/mobil/">Mobil</a>) <img src="https://cpx.golem.de/cpx.php?class=17&amp;aid=137326&amp;page=1&amp;ts=1540475400" alt="" width="1" height="1" />]]></content:encoded>
<slash:comments>20</slash:comments>
</item>
<item>
<title>Red Dead Redemption 2: Saloon-Prügelei (der Worte) im Livestream</title>
<link>https://www.golem.de/news/red-dead-redemption-2-saloon-pruegelei-der-worte-im-livestream-1810-137312-rss.html</link>
<description> Die Golem.de-Redakteure Peter Steinlechner und Michael Wieczorek diskutieren gemeinsam mit unserer Community über den Test zu Red Dead Redemption 2 live ab 18 Uhr. (&lt;a href=&quot;https://www.golem.de/specials/red-dead-redemption-2/&quot;&gt;Red Dead Redemption 2&lt;/a&gt;, &lt;a href=&quot;https://www.golem.de/specials/spieletest/&quot;&gt;Spieletest&lt;/a&gt;) &lt;img src=&quot;https://cpx.golem.de/cpx.php?class=17&amp;amp;aid=137312&amp;amp;page=1&amp;amp;ts=1540474200&quot; alt=&quot;&quot; width=&quot;1&quot; height=&quot;1&quot; /&gt;</description>
<comments>https://forum.golem.de/kommentare/games/red-dead-redemption-2-saloon-pruegelei-der-worte-im-livestream/121545,list.html</comments>
<pubDate>Thu, 25 Oct 2018 14:30:00 +0100</pubDate>
<guid>https://www.golem.de/1810/137312-rss.html</guid>
<content:encoded><![CDATA[<img src="https://www.golem.de/1810/137312-177341-177338_rc.jpg" width="140" height="140" vspace="3" hspace="8" align="left"> Die Golem.de-Redakteure Peter Steinlechner und Michael Wieczorek diskutieren gemeinsam mit unserer Community über den Test zu Red Dead Redemption 2 live ab 18 Uhr. (<a href="https://www.golem.de/specials/red-dead-redemption-2/">Red Dead Redemption 2</a>, <a href="https://www.golem.de/specials/spieletest/">Spieletest</a>) <img src="https://cpx.golem.de/cpx.php?class=17&amp;aid=137312&amp;page=1&amp;ts=1540474200" alt="" width="1" height="1" />]]></content:encoded>
<slash:comments />
</item>
<item>
<title>Wolf Intelligence: Trojanerfirma aus Deutschland lässt interne Daten im Netz</title>
<link>https://www.golem.de/news/wolf-intelligence-trojanerfirma-aus-deutschland-laesst-interne-daten-im-netz-1810-137323-rss.html</link>
<description>Wolf Intelligence verkauft Schadsoftware an Staaten. Eine Sicherheitsfirma hat sensible Daten des Unternehmens öffentlich zugänglich im Internet gefunden. In einer Präsentation wurden die Funde gezeigt. (&lt;a href=&quot;https://www.golem.de/specials/trojaner/&quot;&gt;Trojaner&lt;/a&gt;, &lt;a href=&quot;https://www.golem.de/specials/virus/&quot;&gt;Virus&lt;/a&gt;) &lt;img src=&quot;https://cpx.golem.de/cpx.php?class=17&amp;amp;aid=137323&amp;amp;page=1&amp;amp;ts=1540472400&quot; alt=&quot;&quot; width=&quot;1&quot; height=&quot;1&quot; /&gt;</description>
<comments>https://forum.golem.de/kommentare/security/wolf-intelligence-trojanerfirma-aus-deutschland-laesst-interne-daten-im-netz/121543,list.html</comments>
<pubDate>Thu, 25 Oct 2018 14:00:00 +0100</pubDate>
<guid>https://www.golem.de/1810/137323-rss.html</guid>
<content:encoded><![CDATA[<img src="https://www.golem.de/1810/137323-177383-177380_rc.jpg" width="140" height="140" vspace="3" hspace="8" align="left">Wolf Intelligence verkauft Schadsoftware an Staaten. Eine Sicherheitsfirma hat sensible Daten des Unternehmens öffentlich zugänglich im Internet gefunden. In einer Präsentation wurden die Funde gezeigt. (<a href="https://www.golem.de/specials/trojaner/">Trojaner</a>, <a href="https://www.golem.de/specials/virus/">Virus</a>) <img src="https://cpx.golem.de/cpx.php?class=17&amp;aid=137323&amp;page=1&amp;ts=1540472400" alt="" width="1" height="1" />]]></content:encoded>
<slash:comments>16</slash:comments>
</item>
<item>
<title>NBN: Der Top-Nutzer verwendet 24 TByte im Monat</title>
<link>https://www.golem.de/news/nbn-der-top-nutzer-verwendet-24-tbyte-im-monat-1810-137324-rss.html</link>
<description> Ein staatliches FTTH-Netzwerk für fast alle in Australien bis 2017 war einst das Ziel. Doch davon ist beim (NBN) National Broadband Network nicht mehr viel übrig geblieben. In Berlin wurde eine Zwischenbilanz gezogen. (&lt;a href=&quot;https://www.golem.de/specials/festnetz/&quot;&gt;Festnetz&lt;/a&gt;, &lt;a href=&quot;https://www.golem.de/specials/dsl/&quot;&gt;DSL&lt;/a&gt;) &lt;img src=&quot;https://cpx.golem.de/cpx.php?class=17&amp;amp;aid=137324&amp;amp;page=1&amp;amp;ts=1540471380&quot; alt=&quot;&quot; width=&quot;1&quot; height=&quot;1&quot; /&gt;</description>
<comments>https://forum.golem.de/kommentare/internet/nbn-der-top-nutzer-verwendet-24-tbyte-im-monat/121542,list.html</comments>
<pubDate>Thu, 25 Oct 2018 13:43:00 +0100</pubDate>
<guid>https://www.golem.de/1810/137324-rss.html</guid>
<content:encoded><![CDATA[<img src="https://www.golem.de/1810/137324-177387-177384_rc.jpg" width="140" height="140" vspace="3" hspace="8" align="left"> Ein staatliches FTTH-Netzwerk für fast alle in Australien bis 2017 war einst das Ziel. Doch davon ist beim (NBN) National Broadband Network nicht mehr viel übrig geblieben. In Berlin wurde eine Zwischenbilanz gezogen. (<a href="https://www.golem.de/specials/festnetz/">Festnetz</a>, <a href="https://www.golem.de/specials/dsl/">DSL</a>) <img src="https://cpx.golem.de/cpx.php?class=17&amp;aid=137324&amp;page=1&amp;ts=1540471380" alt="" width="1" height="1" />]]></content:encoded>
<slash:comments>51</slash:comments>
</item>
<item>
<title>Linux-Kernel: Mit Machine Learning auf der Suche nach Bug-Fixes</title>
<link>https://www.golem.de/news/linux-kernel-mit-machine-learning-auf-der-suche-nach-bug-fixes-1810-137321-rss.html</link>
<description>Wichtige Patches, die in stabilen Kernel-Versionen landen sollten, werden von der Linux-Community oft vergessen oder übersehen. Abhilfe schaffen soll offenbar Machine Learning, wie die Entwickler Sasha Levin und Julia Lawall erklären. (&lt;a href=&quot;https://www.golem.de/specials/linux-kernel/&quot;&gt;Linux-Kernel&lt;/a&gt;, &lt;a href=&quot;https://www.golem.de/specials/linux/&quot;&gt;Linux&lt;/a&gt;) &lt;img src=&quot;https://cpx.golem.de/cpx.php?class=17&amp;amp;aid=137321&amp;amp;page=1&amp;amp;ts=1540468800&quot; alt=&quot;&quot; width=&quot;1&quot; height=&quot;1&quot; /&gt;</description>
<comments>https://forum.golem.de/kommentare/security/linux-kernel-mit-machine-learning-auf-der-suche-nach-bug-fixes/121541,list.html</comments>
<pubDate>Thu, 25 Oct 2018 13:00:00 +0100</pubDate>
<guid>https://www.golem.de/1810/137321-rss.html</guid>
<content:encoded><![CDATA[<img src="https://www.golem.de/1810/137321-177375-177372_rc.jpg" width="140" height="140" vspace="3" hspace="8" align="left">Wichtige Patches, die in stabilen Kernel-Versionen landen sollten, werden von der Linux-Community oft vergessen oder übersehen. Abhilfe schaffen soll offenbar Machine Learning, wie die Entwickler Sasha Levin und Julia Lawall erklären. (<a href="https://www.golem.de/specials/linux-kernel/">Linux-Kernel</a>, <a href="https://www.golem.de/specials/linux/">Linux</a>) <img src="https://cpx.golem.de/cpx.php?class=17&amp;aid=137321&amp;page=1&amp;ts=1540468800" alt="" width="1" height="1" />]]></content:encoded>
<slash:comments>4</slash:comments>
</item>
<item>
<title>Quartalszahlen: AMDs Aktie gibt wegen mäßiger Aussichten nach</title>
<link>https://www.golem.de/news/quartalszahlen-amds-aktie-gibt-wegen-maessiger-aussichten-nach-1810-137320-rss.html</link>
<description>Im dritten Quartal 2018 konnte AMD zwar Umsatz und Gewinn steigern, aber nicht so stark wie erwartet. Die Aktie brach dennoch von über 25 US-Dollar auf 17 US-Dollar ein, da das vierte Quartal schlechter laufen wird als von den Anlegern gedacht - hier wurde zu viel erwartet. (&lt;a href=&quot;https://www.golem.de/specials/amd/&quot;&gt;AMD&lt;/a&gt;, &lt;a href=&quot;https://www.golem.de/specials/cpu/&quot;&gt;Prozessor&lt;/a&gt;) &lt;img src=&quot;https://cpx.golem.de/cpx.php?class=17&amp;amp;aid=137320&amp;amp;page=1&amp;amp;ts=1540467600&quot; alt=&quot;&quot; width=&quot;1&quot; height=&quot;1&quot; /&gt;</description>
<comments>https://forum.golem.de/kommentare/wirtschaft/quartalszahlen-amds-aktie-gibt-wegen-maessiger-aussichten-nach/121540,list.html</comments>
<pubDate>Thu, 25 Oct 2018 12:40:00 +0100</pubDate>
<guid>https://www.golem.de/1810/137320-rss.html</guid>
<content:encoded><![CDATA[<img src="https://www.golem.de/1810/137320-177379-177376_rc.jpg" width="140" height="140" vspace="3" hspace="8" align="left">Im dritten Quartal 2018 konnte AMD zwar Umsatz und Gewinn steigern, aber nicht so stark wie erwartet. Die Aktie brach dennoch von über 25 US-Dollar auf 17 US-Dollar ein, da das vierte Quartal schlechter laufen wird als von den Anlegern gedacht - hier wurde zu viel erwartet. (<a href="https://www.golem.de/specials/amd/">AMD</a>, <a href="https://www.golem.de/specials/cpu/">Prozessor</a>) <img src="https://cpx.golem.de/cpx.php?class=17&amp;aid=137320&amp;page=1&amp;ts=1540467600" alt="" width="1" height="1" />]]></content:encoded>
<slash:comments>17</slash:comments>
</item>
<item>
<title>Projekt Broadband: Bahn will 3,5 Milliarden Euro vom Bund für Glasfasernetz</title>
<link>https://www.golem.de/news/projekt-broadband-bahn-will-3-5-milliarden-euro-vom-bund-fuer-glasfasernetz-1810-137322-rss.html</link>
<description>Die Pläne für das eigene Glasfasernetz der Deutschen Bahn werden konkret. Über die Finanzierung redet man jetzt mit der Regierung. Das Netz soll schnell gebaut werden. (&lt;a href=&quot;https://www.golem.de/specials/deutsche-bahn/&quot;&gt;Deutsche Bahn&lt;/a&gt;, &lt;a href=&quot;https://www.golem.de/specials/umts/&quot;&gt;UMTS&lt;/a&gt;) &lt;img src=&quot;https://cpx.golem.de/cpx.php?class=17&amp;amp;aid=137322&amp;amp;page=1&amp;amp;ts=1540466580&quot; alt=&quot;&quot; width=&quot;1&quot; height=&quot;1&quot; /&gt;</description>
<comments>https://forum.golem.de/kommentare/internet/projekt-broadband-bahn-will-3-5-milliarden-euro-vom-bund-fuer-glasfasernetz/121539,list.html</comments>
<pubDate>Thu, 25 Oct 2018 12:23:00 +0100</pubDate>
<guid>https://www.golem.de/1810/137322-rss.html</guid>
<content:encoded><![CDATA[<img src="https://www.golem.de/1808/136062-171340-171337_rc.jpg" width="140" height="140" vspace="3" hspace="8" align="left">Die Pläne für das eigene Glasfasernetz der Deutschen Bahn werden konkret. Über die Finanzierung redet man jetzt mit der Regierung. Das Netz soll schnell gebaut werden. (<a href="https://www.golem.de/specials/deutsche-bahn/">Deutsche Bahn</a>, <a href="https://www.golem.de/specials/umts/">UMTS</a>) <img src="https://cpx.golem.de/cpx.php?class=17&amp;aid=137322&amp;page=1&amp;ts=1540466580" alt="" width="1" height="1" />]]></content:encoded>
<slash:comments>31</slash:comments>
</item>
<item>
<title>Red Dead Redemption 2 im Test: Der Revolverhelden-Simulator</title>
<link>https://www.golem.de/news/red-dead-redemption-2-im-test-der-revolverhelden-simulator-1810-137304-rss.html</link>
<description>Reiten, prügeln, kochen, jagen, schießen, böse sein oder (relativ) brav: In Red Dead Redemption 2 gibt es enorme Möglichkeiten, sich als Revolverheld in einer wunderschönen Westernwelt auszuleben. Das Actionspiel von Rockstar Games ist ein großer Spaß - aber nicht ganz so gut wie GTA 5. Von Peter Steinlechner (&lt;a href=&quot;https://www.golem.de/specials/red-dead-redemption-2/&quot;&gt;Red Dead Redemption 2&lt;/a&gt;, &lt;a href=&quot;https://www.golem.de/specials/spieletest/&quot;&gt;Spieletest&lt;/a&gt;) &lt;img src=&quot;https://cpx.golem.de/cpx.php?class=17&amp;amp;aid=137304&amp;amp;page=1&amp;amp;ts=1540465260&quot; alt=&quot;&quot; width=&quot;1&quot; height=&quot;1&quot; /&gt;</description>
<comments>https://forum.golem.de/kommentare/games/red-dead-redemption-2-im-test-der-revolverhelden-simulator/121537,list.html</comments>
<pubDate>Thu, 25 Oct 2018 12:01:00 +0100</pubDate>
<guid>https://www.golem.de/1810/137304-rss.html</guid>
<content:encoded><![CDATA[<img src="https://www.golem.de/1810/137304-177303-177300_rc.jpg" width="140" height="140" vspace="3" hspace="8" align="left">Reiten, prügeln, kochen, jagen, schießen, böse sein oder (relativ) brav: In Red Dead Redemption 2 gibt es enorme Möglichkeiten, sich als Revolverheld in einer wunderschönen Westernwelt auszuleben. Das Actionspiel von Rockstar Games ist ein großer Spaß - aber nicht ganz so gut wie GTA 5. Von Peter Steinlechner (<a href="https://www.golem.de/specials/red-dead-redemption-2/">Red Dead Redemption 2</a>, <a href="https://www.golem.de/specials/spieletest/">Spieletest</a>) <img src="https://cpx.golem.de/cpx.php?class=17&amp;aid=137304&amp;page=1&amp;ts=1540465260" alt="" width="1" height="1" />]]></content:encoded>
<slash:comments>107</slash:comments>
</item>
<item>
<title>Xiaomi: Das Mi Mix 3 hat keine Notch und eine versteckte Frontkamera</title>
<link>https://www.golem.de/news/xiaomi-das-mi-mix-3-hat-keine-notch-und-eine-versteckte-frontkamera-1810-137319-rss.html</link>
<description>Das Display von Xiaomis angekündigtem Smartphone Mi Mix 3 ist nahezu randlos. Die Frontkamera versteckt das Gerät hinter der aufschiebbaren Schale. Neu ist zudem, dass Xiaomi ein OLED-Panel verbaut und wieder den Qi-Ladestandard nutzt. (&lt;a href=&quot;https://www.golem.de/specials/xiaomi/&quot;&gt;Xiaomi&lt;/a&gt;, &lt;a href=&quot;https://www.golem.de/specials/smartphone/&quot;&gt;Smartphone&lt;/a&gt;) &lt;img src=&quot;https://cpx.golem.de/cpx.php?class=17&amp;amp;aid=137319&amp;amp;page=1&amp;amp;ts=1540464540&quot; alt=&quot;&quot; width=&quot;1&quot; height=&quot;1&quot; /&gt;</description>
<comments>https://forum.golem.de/kommentare/handy/xiaomi-das-mi-mix-3-hat-keine-notch-und-eine-versteckte-frontkamera/121536,list.html</comments>
<pubDate>Thu, 25 Oct 2018 11:49:00 +0100</pubDate>
<guid>https://www.golem.de/1810/137319-rss.html</guid>
<content:encoded><![CDATA[<img src="https://www.golem.de/1810/137319-177371-177370_rc.jpg" width="140" height="140" vspace="3" hspace="8" align="left">Das Display von Xiaomis angekündigtem Smartphone Mi Mix 3 ist nahezu randlos. Die Frontkamera versteckt das Gerät hinter der aufschiebbaren Schale. Neu ist zudem, dass Xiaomi ein OLED-Panel verbaut und wieder den Qi-Ladestandard nutzt. (<a href="https://www.golem.de/specials/xiaomi/">Xiaomi</a>, <a href="https://www.golem.de/specials/smartphone/">Smartphone</a>) <img src="https://cpx.golem.de/cpx.php?class=17&amp;aid=137319&amp;page=1&amp;ts=1540464540" alt="" width="1" height="1" />]]></content:encoded>
<slash:comments>125</slash:comments>
</item>
</channel>
</rss>
+79
View File
@@ -0,0 +1,79 @@
<rss version="2.0" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#">
<channel>
<title>Flux RSS du magazine de psychologie Le Cercle Psy</title>
<link>https://le-cercle-psy.scienceshumaines.com/rss</link>
<description>Flux RSS du magazine de psychologie Le Cercle Psy, le magazine de toutes les psychologies.</description>
<copyright>Le Cercle Psy</copyright>
<item>
<title>Perturbateurs endocriniens : quels effets sur le cerveau ?</title>
<link>https://le-cercle-psy.scienceshumaines.com/perturbateurs-endocriniens-quels-effets-sur-le-cerveau_sh_39995</link>
<pubDate>Wed, 17 Oct 2018 10:30:00 GMT</pubDate>
<description>Si leur impact semble discret au premier abord, nombre d'études montrent que les perturbateurs endocriniens pourraient être à l'origine de troubles neuro-développementaux chez l'enfant.</description>
</item>
<item>
<title>Masters en Psycho : une simplificationet#8230; très complexe </title>
<link>https://le-cercle-psy.scienceshumaines.com/masters-en-psycho-une-simplification-tres-complexe_sh_40065</link>
<pubDate>Wed, 17 Oct 2018 10:30:00 GMT</pubDate>
<description>Une nouvelle nomenclature adoptée en 2014 a voulu simplifier les options proposées aux étudiants en Master de Psychologie. Mais on en revient à des choix aussi illisibles qu'auparavant !</description>
</item>
<item>
<title>La criminalité liée surtout à... l'ennui ?</title>
<link>https://le-cercle-psy.scienceshumaines.com/la-criminalite-liee-surtout-a-l-ennui_sh_39986</link>
<pubDate>Wed, 17 Oct 2018 10:30:00 GMT</pubDate>
<description>« L'oisiveté est mère de tous les vices », dit le proverbe... Certains chercheurs américains paraissent proches de cette position !</description>
</item>
<item>
<title></title>
<link></link>
<pubDate>Wed, 17 Oct 2018 10:30:00 GMT</pubDate>
<description></description>
</item>
<item>
<title>Caroline Eliacheff : « Dolto reste authentiquement subversive »</title>
<link>https://le-cercle-psy.scienceshumaines.com/caroline-eliacheff-dolto-reste-authentiquement-subversive_sh_39992</link>
<pubDate>Wed, 17 Oct 2018 10:30:00 GMT</pubDate>
<description>Françoise Dolto est morte il y a trente ans. D'abord adulée par des générations de parents et de collègues, on lui a ensuite reproché d'avoir favorisé l'émergence d'enfants-rois tyranniques. Et s'il existait une autre voie ?</description>
</item>
<item>
<title>L'enfant doué : quand trop comprendre... empêche parfois de comprendre</title>
<link>https://le-cercle-psy.scienceshumaines.com/l-enfant-doue-quand-trop-comprendre-empeche-parfois-de-comprendre_sh_40004</link>
<pubDate>Wed, 17 Oct 2018 10:30:00 GMT</pubDate>
<description>On ne le répètera jamais assez : réaliser le portrait-robot d'un enfant « doué », « surdoué », « à haut potentiel », peu importe la qualification choisie, est vain. Cet ouvrage nous rappelle que chaque facilité, talent, compétence ou même don, peut s'accompagner d'un versant potentiellement plus problématique. Mais insistons : potentiellement.</description>
</item>
<item>
<title>Travail, organisations, emploi : les modèles européens</title>
<link>https://le-cercle-psy.scienceshumaines.com/travail-organisations-emploi-les-modeles-europeens_sh_33090</link>
<pubDate>Wed, 17 Oct 2018 10:30:00 GMT</pubDate>
<description>Les pays européens diffèrent en matière de performance, de niveau de chômage et de qualité de vie au travail. Certains pays réussissent mieux que d'autres et sont pris comme «modèles». Comment font-ils?</description>
</item>
<item>
<title>Migrants : l'urgence thérapeutique</title>
<link>https://le-cercle-psy.scienceshumaines.com/migrants-l-urgence-therapeutique_sh_39180</link>
<pubDate>Wed, 17 Oct 2018 10:30:00 GMT</pubDate>
<description>Poussés à l'exil par les conflits, la pauvreté et l'espoir d'une vie meilleure, les migrants arrivent après un long parcours. Beaucoup sont blessés, brisés, désespérés parfois. Quelle réponse pour les aider ?</description>
</item>
<item>
<title>Psy en prison, une mission impossible ?</title>
<link>https://le-cercle-psy.scienceshumaines.com/psy-en-prison-une-mission-impossible_sh_38718</link>
<pubDate>Wed, 17 Oct 2018 10:30:00 GMT</pubDate>
<description>Détenus proches de la psychose, manque cruel de moyens, hiérarchie intrusive... Les praticiens intervenant en prison n'ont pas un quotidien facile. Retour sur un sacerdoce des temps modernes.</description>
</item>
<item>
<title>Psychologue à domicile : de la clinique à l'état brut</title>
<link>https://le-cercle-psy.scienceshumaines.com/psychologue-a-domicile-de-la-clinique-a-l-etat-brut_sh_35540</link>
<pubDate>Wed, 17 Oct 2018 10:30:00 GMT</pubDate>
<description>Si tu ne peux pas venir au psychologue, le psychologue viendra à toi ! L'intervention à domicile demeure une pratique encore peu répandue chez les psys. En quoi diffère-t-elle d'une consultation ordinaire ?</description>
</item>
</channel>
</rss>
+862
View File
@@ -0,0 +1,862 @@
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=gb2312" />
<title>警惕7大最常见的网络钓鱼主题行,看看你中招没? - 51CTO.COM</title>
<meta name="description" content="如今,我们的电子邮件中正潜伏着各种网络钓鱼骗局。即便是你现在没有发现,那么也许明天或者是后天……总之总有一天会发现。关键问题是,在面对网络钓鱼骗局时,你会中招吗?"/>
<meta name="keywords" content="网络钓鱼,攻击,网络安全"/>
<base target="_blank" />
<link rel="stylesheet" type="text/css" href="https://static4.51cto.com/51cto/cms/2016/css/article_head.css?v=0.1"/>
<link rel="stylesheet" type="text/css" href="https://static1.51cto.com/51cto/cms/2016/css/article_layout.css?v=2.17"/>
<link rel="stylesheet" type="text/css" href="https://static2.51cto.com/51cto/cms/2016/css/article_right.css"/>
<script type="text/javascript" src="https://static1.51cto.com/libs/jquery/1.8.3/jquery.min.js"></script>
</head>
<body >
<!-- 导航 开始-->
<!-- top start -->
<style>
.home-top .subweb .sub_widthAuto{width:auto;}
.top-nav .nav li .topnavA_zxf{width:194px;left:-60px;}
.top-nav .topnavA_zxf a{padding: 0 10px;}
.top-nav .nav li:hover .topnavA_zxf a{width:auto;text-indent:inherit;}
.zxf_menubot{background:#F4F4F4;padding:15px 20px 0;border-top:4px solid #be0000;font-size:14px;}
.zxf_menubot a{padding:0;}
.zxf_menubot dl{margin-right:48px;_margin-right:40px;*margin-right:40px;}
.zxf_menubot .nmr{margin-right:0;}
.zxf_menubot dt,.zxf_menubot dd{float:left;}
.zxf_menulist li{float:left;text-align:left;margin-right:20px;}
.zxf_menulist a{display:block;margin-bottom:10px;}
.zxf_menulist li.nmr{margin-right:0;}
.zxf_menu_ico{width:28px;margin-right:20px;text-align:center;color:#666;}
.zxf_menu_ico img{display:block;margin:10px auto 10px;}
.redNav{color:#be0000;}
.subweb-list .navcodebox{ width:410px; left:-225px; padding:20px 15px;}
.subweb-list .navcodebox span{ width:136px;display:inline-block; float:left; text-align:center; color:#747474; font-size:13px; }
.subweb-list .navcodebox span p{ padding-top: 10px;color:#333;}
.rmzw_xx li a {
float: left;
font-size: 16px;
height: 23px;
line-height: 23px;
overflow: hidden;
width: 95%;
}
.rmzw_xx li span.ico {
background: rgba(0, 0, 0, 0) url("https://s2.51cto.com/wyfs02/M02/98/D8/wKiom1lBBebwbP2OAAAAuOBywqs269.png") no-repeat scroll left center;
float: left;
height: 22px;
line-height: 22px;
width: 15px;
}
.rmzw_xx li {
border-bottom: 1px dotted #ccc;
float: left;
margin-top: 6px;
padding: 5px 0;
width: 100%;
}
.ctoll1 dd a {
color: #343434;
float: left;
font-family: "Microsoft YaHei";
font-size: 14px;
padding: 15px 19px 0;
}
</style>
<div class="home-top" id="topx">
<div class="w1001 cent">
<div class="pdr10 fl"><a href="http://www.51cto.com/">51CTO首页</a></div>
<div class="pdr10 fl">|</div>
<div class="pdr10 fl">
<div class="subweb"><span class="trans">技术频道</span><i></i>
<div class="subweb-list" style="width: 90px; top: 37px;">
<a href="http://ai.51cto.com/" target="_blank">人工智能</a>
<a href="http://cloud.51cto.com/" target="_blank">云计算</a>
<a href="http://bigdata.51cto.com/" target="_blank">大数据</a>
<a href="http://network.51cto.com/" target="_blank">网络</a>
<a href="http://developer.51cto.com/" target="_blank">开发</a>
<a href="http://netsecurity.51cto.com/" target="_blank">安全</a>
<a href="http://os.51cto.com/" target="_blank">系统</a>
<a href="http://mobile.51cto.com/" target="_blank">移动</a>
<a href="http://server.51cto.com/" target="_blank">服务器</a>
</div>
</div>
</div>
<div class="pdr10 fl">|</div>
<div class="pdr10 fl">
<div class="subweb"><span class="trans">51CTO旗下网站</span><i></i>
<!--旗下网站-->
<div class="subweb-list" style="top: 37px;">
<a href="http://www.51cto.com" target="_blank">51CTO.COM</a>
<a href="http://www.cioage.com" target="_blank">CIOAge.COM</a>
<a href="http://www.hc3i.cn" target="_blank">HC3i.CN</a>
<!--<a href="http://zhijiapro.com/" target="_blank">zhijiapro.com</a>-->
</div>
<!--旗下网站-->
</div>
</div>
<div class="pdr10 fl">|</div>
<div class="pdr10 fl"><a href="http://www.51cto.com/about/map.htm" target="_blank">地图</a></div>
<div class="pdr10 fl">|</div>
<div class="pdr10 fl">
<div class="subweb"><span class="trans">移动端</span><i></i>
<!--旗下网站-->
<div class="subweb-list sub_widthAuto" style="top: 37px;">
<div class="navcodebox clearfix" style="width: 546px; display: block;top: 0; border-top: 0; left:-1px;">
<span>
<img src="https://s3.51cto.com/wyfs02/M02/97/8A/wKiom1kvkpKBgQFIAACDBen2-dg668.jpg" width="105" height="107" />
<p>51CTO技术栈</p>
</span>
<span>
<img src="http://s5.51cto.com/wyfs02/M02/8E/33/wKioL1i40HrAObC6AAAcvPVCCV0975.jpg" width="105" height="107" />
<p>51CTO微站服务号</p>
</span>
<span>
<img src="http://s3.51cto.com/wyfs02/M00/8E/33/wKioL1i40HrQZy_TAAAcDOkrRAE327.jpg" width="107" height="107" />
<p>51CTO学院客户端</p>
</span>
<span>
<img src="https://s3.51cto.com/oss/201712/27/48594addc4b5eebf52a15bd51262ab23.jpg" width="107" height="107" />
<p>CIO进化论</p>
</span>
</div>
</div>
<!--旗下网站-->
</div>
</div>
<div class="top-r">
<div id="login_status" style="text-align:right;" class="login"> </div>
</div>
</div>
</div>
<!-- top end -->
<!-- 导航 结束-->
<!-- 频道导航 -->
<div class="top_bg">
<div class="wrap">
<div id="tonglanad" class="left"></div>
<div id="list4" class="right" style="position:relative;">
<ul>
<li id="wordlinkad1"></li>
<li id="wordlinkad2"></li>
<li id="wordlinkad3"></li>
<li id="wordlinkad4"></li>
</ul>
<div style="right: 0px; width: 24px; height: 14px; z-index: 12; position: absolute; background: transparent url('http://s5.51cto.com/wyfs02/M00/86/BB/wKiom1fI4nWStYqXAAAEoZQn6vs942.png') repeat scroll 0% 0%; bottom: 2px;"></div>
</div>
</div>
<div class="nav">
<a href="http://www.51cto.com" class="logo"><img src="http://static4.51cto.com/51cto/cms/2016/images/nr_logo.png?v=0.1" alt=""></a>
<ul>
<li><a href="http://netsecurity.51cto.com" class="active">安全频道</a></li>
<li><a href="http://netsecurity.51cto.com">首页</a></li>
<li><a href="http://netsecurity.51cto.com/col/1073/">资讯</a></li>
<li><a href="http://netsecurity.51cto.com/col/516/">应用安全</a></li>
<li><a href="http://netsecurity.51cto.com/col/1068/">数据安全</a></li>
<li><a href="http://netsecurity.51cto.com/col/1537/">移动安全</a></li>
<li><a href="http://netsecurity.51cto.com/col/1591/">云安全</a></li>
<li><a href="http://netsecurity.51cto.com/col/518/">黑客</a></li>
<li><a href="http://netsecurity.51cto.com/col/1593/">工具</a></li>
<li><a href="http://netsecurity.51cto.com/speclist/1156/">专题</a></li>
</ul>
<div class="nav-rsear">
<form method="post" action="http://www.51cto.com/php/search.php" name="searchform" target="_blank">
<input name="keyword" id="q" type="text" placeholder="输入您要搜索的内容" class="sear-1">
<input name="" type="submit" value="" class="sear-2">
</form>
</div>
</div>
</div>
<!-- 频道导航结束 -->
<div class="main">
<!-- 左侧内容 -->
<div class="main_left">
<div class="wznr">
<h2>警惕7大最常见的网络钓鱼主题行,看看你中招没?</h2>
<p>如今,我们的电子邮件中正潜伏着各种网络钓鱼骗局。即便是你现在没有发现,那么也许明天或者是后天……总之总有一天会发现。关键问题是,在面对网络钓鱼骗局时,你会中招吗?</p>
<dl>
<dt><span>作者:小二郎</span><span>来源:<a href='http://www.4hou.com/info/news/14195.html' target='_blank'>4hou</a></span>|<em>2018-10-29 10:35</em></dt>
<dd>
<div class="left" style="padding-right: 10px">
<a href="javascript:favorBox('open');" title="一键收藏,随时查看,分享好友!" target="_self" class="bds_more1">&nbsp;收藏</a>
</div>
<div class="bdsharebuttonbox left" data-tag="share_2">
<a href="javascript:;" class="bds_more" data-cmd="more">&nbsp;&nbsp;分享</a>
</div>
</dd>
</dl>
</div>
<div class="zwnr">
<!-- <h2><a href="http://mdsa.51cto.com/act/Tech/Tech24" target="_blank" style="text-decoration:none;">【51CTO技术沙龙】10月27日,让我们共同探索AI场景化应用实现之道</a></h2> -->
<p>如今,我们的电子邮件中正潜伏着各种网络钓鱼骗局。即便是你现在没有发现,那么也许明天或者是后天&hellip;&hellip;总之总有一天会发现。关键问题是,在面对网络钓鱼骗局时,你会中招吗?</p>
<p>有研究人员发表的报告声明:电子邮件中的网络钓鱼骗局已经变得越来越难以阻止,攻击者正在创新和完善他们的诱饵,使网络钓鱼更逼真从而对用户更具吸引力和说服力。Webroot公司首席信息安全官Gary Hayslip表示:虽然用户越来越警惕各种钓鱼诈骗的攻击,各种防网络钓鱼软件也不少,但还是不能阻挡各种各样的钓鱼诈骗攻击。正所谓&ldquo;道高一尺魔高一丈&rdquo;,网络钓鱼攻击依旧通过各种方式,来骗取个人用户和企业的信息。</p>
<p>Hayslip表示:</p>
<table style="border-right: #cccccc 1px dotted; table-layout: fixed; border-top: #cccccc 1px dotted; border-left: #cccccc 1px dotted; border-bottom: #cccccc 1px dotted" width="95%" cellspacing="0" cellpadding="6" border="0" align="center">
<tbody>
<tr>
<td style="word-wrap: break-word" bgcolor="#fdfddf">&ldquo;我觉得电子邮件中的网络钓鱼骗局已经到了普遍存在的地步。用户现在也习惯下意识地点击查看网络钓鱼电子邮件,即便他们明知不应该这么做。这就是攻击者利用人性来实施攻击活动的可怕之处!&rdquo;</td>
</tr>
</tbody>
</table>
<p>人们总是充满好奇心和同情心,渴望通过自己的力量来帮助有需要的人,而这两种品质正是他们易受网络钓鱼攻击的根源所在。当他们中招了以后,他们又习惯用&ldquo;我当时忙糊涂了&rdquo;&ldquo;我忘了&rdquo;&ldquo;我本应该知道这是钓鱼邮件的&rdquo;等诸多借口,来推脱自己打来恶意电子邮件的错误行为。</p>
<p>Hayslip补充道:</p>
<table style="border-right: #cccccc 1px dotted; table-layout: fixed; border-top: #cccccc 1px dotted; border-left: #cccccc 1px dotted; border-bottom: #cccccc 1px dotted" width="95%" cellspacing="0" cellpadding="6" border="0" align="center">
<tbody>
<tr>
<td style="word-wrap: break-word" bgcolor="#fdfddf">&ldquo;无论你采用多少技术手段和工具来阻止它们,它们总有办法成功欺骗用户&rdquo;</td>
</tr>
</tbody>
</table>
<p>近日,Webroot公司扫描了过去18个月内的成千上万封网络钓鱼电子邮件,以了解有关针对特定目标的常见主题行的发展趋势。Hayslip向全美约100名首席信息安全官展示了此次调查结果,并了解到:&ldquo;很多人都会收到类似的邮件&rdquo;。在网络钓鱼电子邮件中经常可以看到与财务有关的消息和紧急通知,尽管是在不同的主题之下。</p>
<p>Cofense(前身为PhishMe)的网络安全战略家John&ldquo;Lex&rdquo;Robinson回应了Hayslip的观点,并表示攻击者对于他们发送的电子邮件的背景以及他们的攻击目标,已经有了越来越深地了解。他说:</p>
<table style="border-right: #cccccc 1px dotted; table-layout: fixed; border-top: #cccccc 1px dotted; border-left: #cccccc 1px dotted; border-bottom: #cccccc 1px dotted" width="95%" cellspacing="0" cellpadding="6" border="0" align="center">
<tbody>
<tr>
<td style="word-wrap: break-word" bgcolor="#fdfddf">&ldquo;如果把我们今天的沟通方式(包含许多流行语)与15年前、20年前甚至30年前相比,可能就显得不那么通俗了。但是网络钓鱼邮件要求的从来都不是通俗,而是要与商业行话保持一致。&rdquo;</td>
</tr>
</tbody>
</table>
<p>以下是一些最常见的网络钓鱼主题行,并向大家展示它们所包含的信息,以及它们所揭示的攻击者的目标和策略等内容。</p>
<p><strong>1. 紧急求助</strong></p>
<p style="text-align: center;"><a href="http://s1.51cto.com/oss/201810/29/5057306a0af6920eab7313b53a996a3c.jpg-wh_651x-s_3729779577.jpg" target="_blank"><img src="http://s1.51cto.com/oss/201810/29/5057306a0af6920eab7313b53a996a3c.jpg-wh_651x-s_3729779577.jpg" alt="紧急求助" title="紧急求助" width="auto" height="auto" border="0" /></a></p>
<p>当攻击者不希望目标犹豫不决时,他们就会在主题行中传达一种紧迫感,因为他们希望你能够快速做出决定。</p>
<p>也许该钓鱼邮件不会直接说&ldquo;紧急求助&rdquo;,而是选择言语之间存在类似的暗示。Hayslip表示,作为一名首席信息安全官,他就经常会看到人们因为想要提供帮助而中招,而且重要的是,很多受害者的心理活动是,并不想因为没有采取某些可能很重要的行动而受到惩罚。对于这种情况,Hayslip特意告诉员工:</p>
<table style="border-right: #cccccc 1px dotted; table-layout: fixed; border-top: #cccccc 1px dotted; border-left: #cccccc 1px dotted; border-bottom: #cccccc 1px dotted" width="95%" cellspacing="0" cellpadding="6" border="0" align="center">
<tbody>
<tr>
<td style="word-wrap: break-word" bgcolor="#fdfddf">&ldquo;我宁愿你去寻求专业帮助,也不愿你因为做错事而陷入困境。我建议你可以忽略任何在主题行中言及&lsquo;紧急&rsquo;字眼的未知邮件。因为真的紧急的事情可以通过很多其他更高效的途径来解决,例如打电话。&rdquo;</td>
</tr>
</tbody>
</table>
<p><strong>2. 发票</strong></p>
<p style="text-align: center;"><a href="http://s3.51cto.com/oss/201810/29/06ac27ce87a64de6a45010f77cda9460.jpg" target="_blank"><img src="http://s3.51cto.com/oss/201810/29/06ac27ce87a64de6a45010f77cda9460.jpg" alt="" title="" width="auto" height="auto" border="0" /></a></p>
<p style="text-align: left;">在Cofense检测到的&ldquo;Top10网络钓鱼邮件主题行&rdquo;中,&ldquo;发票&rdquo;一词就独占6大主题行(只是表达方式有所差异),这也说明在考虑网络钓鱼主题时,财务动机仍然处于主导地位。</p>
<p style="text-align: left;">在谈及Cofense追踪到的最常见的网络钓鱼骗局时,Robinson表示:</p>
<table style="border-right: #cccccc 1px dotted; table-layout: fixed; border-top: #cccccc 1px dotted; border-left: #cccccc 1px dotted; border-bottom: #cccccc 1px dotted" width="95%" cellspacing="0" cellpadding="6" border="0" align="center">
<tbody>
<tr>
<td style="word-wrap: break-word" bgcolor="#fdfddf">&ldquo;对我来说,最有意思的是绝大多数网络钓鱼骗局都涉及金钱主题。很显然,金钱对每个人来说都是一个极具刺激性的话题&hellip;&hellip;当你有了这样一个极具刺激性的话题时,就能立即引起人们的兴趣。&rdquo;</td>
</tr>
</tbody>
</table>
<p style="text-align: left;">虽然前6个骗局的具体消息内容有所不同,但所有人都试图用&ldquo;发票&rdquo;一词作为主题来吸引他们的目标。金钱是一个强大的动力,攻击者很清楚地知道这一点,并且正在利用它来击垮人们的心理防线。根据数据显示,在Cofense扫描的网络钓鱼电子邮件中,大约有100,000个电子邮件是利用&ldquo;发票&rdquo;一词作为主题的。</p>
<p style="text-align: left;">Cofense研究人员还发现,&ldquo;付款汇款&rdquo;是另一个受欢迎的标题,其使用量也超过了40,000封电子邮件。同时,&ldquo;声明&rdquo;&ldquo;付款&rdquo;也是非常受欢迎的主题。</p>
<p style="text-align: left;">WebRoot则发现,&ldquo;电汇&rdquo;也是一种常见的选择,当然还有一些电子邮件会更具体:&ldquo;您最近的Chase付款通知&rdquo;是另一个受欢迎的财务主题。</p>
<p style="text-align: left;">需要注意的是,不要以为网络钓鱼欺诈只会发生在没有上网经验的人身上,即使是互联网公司也会遭到电汇诈骗,而且诈骗数额不小,已经达到数百万美元。谷歌和Facebook的会计部门电脑上由于被安装了恶意软件,通过查阅它们的转账记录瞄准了一个开发商,开了数百万美元的发票。</p>
<p style="text-align: left;">虽然事后经过执法机关的努力,追回了损失;但其他公司就不会这么幸运了,2016年价值30亿美元被电汇诈骗,绝大部分都无法追回。</p>
<p style="text-align: left;"><strong>3. 银行通知</strong></p>
<p style="text-align: center;"><a href="http://s1.51cto.com/oss/201810/29/9cf0f8daf78f23189e7e68468690abb6.jpg" target="_blank"><img src="http://s1.51cto.com/oss/201810/29/9cf0f8daf78f23189e7e68468690abb6.jpg" alt="" title="" width="auto" height="auto" border="0" /></a></p>
<p style="text-align: left;">Hayslip表示,针对公司高管的经济动机网络钓鱼攻击,往往需要攻击者付出更多的努力和研究,以及更为严谨的措辞和语法。他解释称:</p>
<table style="border-right: #cccccc 1px dotted; table-layout: fixed; border-top: #cccccc 1px dotted; border-left: #cccccc 1px dotted; border-bottom: #cccccc 1px dotted" width="95%" cellspacing="0" cellpadding="6" border="0" align="center">
<tbody>
<tr>
<td style="word-wrap: break-word" bgcolor="#fdfddf">&ldquo;我认为,这就是&lsquo;捕鲸攻击&rsquo;(whaling attacks)和普通的网络钓鱼攻击之间的区别。所谓&ldquo;捕鲸攻击&rdquo;,实际上是一种欺诈类型,钓鱼者找到某个公司高层或高管团队的姓名和电子邮件地址(此类信息通常会在网页上免费提供),并撰写与这些人员及其公司职位相称的电子邮件。这些电子邮件会试图诱使高管们单击某个链接并访问某个网站,在此恶意软件会下载到其计算机中,并复制按键记录或搜出敏感信息或公司机密。&rdquo;</td>
</tr>
</tbody>
</table>
<p style="text-align: left;">针对高级员工实施网络钓鱼活动的攻击者,会希望其钓鱼邮件信息尽可能真实。他们可能会向行政助理发送包含特定银行名称或&ldquo;紧急协助&rdquo;信号的欺诈性电子邮件。他们会先对目标机构所选择的银行进行研究,并尝试模仿银行发布的通知信息。此外,攻击者还可能会联系助理,称他们的CEO或CFO正在旅行并遇到了一些财务问题,需要经济上的帮助,并利用诸如此类的借口来证明资产转移的合理性和可信性。</p>
<p style="text-align: left;"><strong>4. 账户验证</strong></p>
<p style="text-align: center;"><a href="http://s4.51cto.com/oss/201810/29/cdde8bad58338f3e1515aa08fa9578ad.jpg" target="_blank"><img src="http://s4.51cto.com/oss/201810/29/cdde8bad58338f3e1515aa08fa9578ad.jpg" alt="" title="" width="auto" height="auto" border="0" /></a></p>
<p style="text-align: left;">这一主题行与直接的经济收益关系不大,但与知识产权盗窃存在很大关联。这种类型的攻击通常会进行凭证钓鱼(credential phishing),为了在目标网络内获取立足点。许多线上服务都需要有凭证作为确认服务真实性的基础,但是凭证钓鱼就会企图获取线上服务的凭证,因此,凭证一旦遭窃,攻击者就可以直接获取所需的各类信息,包括使用者的账号、密码等等。</p>
<p style="text-align: left;">在进行凭证钓鱼时,就需要通过&ldquo;账户验证&rdquo;之类的请求来引诱你进入登录页面以验证你的凭证。因为在一系列后续活动中,攻击者需要获取到你的用户名和密码等数据,而想要获取这些信息,就可能涉及冒充您经常使用的品牌发送&ldquo;账户验证&rdquo;主题的钓鱼邮件。</p>
<p style="text-align: left;"><strong>5. 拷贝或文档拷贝</strong></p>
<p style="text-align: center;"><a href="http://s3.51cto.com/oss/201810/29/fb75b867b80e55846070b4dd1a0bdf71.jpg" target="_blank"><img src="http://s3.51cto.com/oss/201810/29/fb75b867b80e55846070b4dd1a0bdf71.jpg" alt="" title="" width="auto" height="auto" border="0" /></a></p>
<p style="text-align: left;">虽然恶意链接在钓鱼电子邮件中日益普遍,但Robinson认为,附件也仍然很受欢迎,且十分有效,尤其是与发票、付款通知和声明相关的电子邮件,或者是在线订购和结算相关的警报中。</p>
<p style="text-align: left;">这符合攻击者提高他们对业务环境的理解的趋势。因为如果他们知道员工经常发送文件的事实,就会知道恶意电子表格或Word文件形式的附件是合理的。越来越多的附件,以及将宏视为主要交付方式的事实,均表明攻击者越来越善于理解业务环境,以至于他们清楚地知道在网络钓鱼电子邮件中放入什么内容才是正常的。</p>
<p style="text-align: left;">此外,许多网络钓鱼邮件的主题都非常短,甚至只有一两个字。这也表明攻击者理解现代商业沟通的方式是相对非正式的。因为商业环境中的人奔波忙碌,不必设置非常正式、具体的主题。</p>
<p style="text-align: left;">就该主题而言,创建标准的沟通流程和政策可以帮助组织有效地防御这种形式的网络钓鱼攻击。组织可以向员工展示这些电子邮件应该来自哪里,以及它们应该采取何种格式,以便他们能够及时发现欺诈性邮件。</p>
<p style="text-align: left;"><strong>6. 行动请求,如&ldquo;支付卖家尾款&rdquo;</strong></p>
<p style="text-align: center;"><a href="http://s1.51cto.com/oss/201810/29/3b6b6bdb4e35d68ccdac7e0ff881fc9e.jpg" target="_blank"><img src="http://s1.51cto.com/oss/201810/29/3b6b6bdb4e35d68ccdac7e0ff881fc9e.jpg" alt="" title="" width="auto" height="auto" border="0" /></a></p>
<p style="text-align: left;">Hayslip指出,目标往往会轻信此类钓鱼邮件主题。在电子邮件中采用&ldquo;我们需要您这样做&rdquo;相关的主题行,往往能够成功地诱使目标完成攻击者需要他们完成的任何事情。调查数据也指出,在面对与&ldquo;行动请求&rdquo;相关的主题时,人们实际上确实是这样做的&mdash;&mdash;在与&ldquo;行动请求&rdquo;相关的钓鱼邮件中,恶意链接的点击率高达约40%,攻击者会将受害者重定向到虚假的人力资源网站,以窃取其登录凭证。受害者在被骗后会悔悟称,&ldquo;我知道我当时不应该那样做。&rdquo;</p>
<p style="text-align: left;">在过去一年中,攻击者已经从使用恶意附件转化为嵌入恶意链接。如今,大多数网络钓鱼电子邮件中都包含恶意链接,并且从表面看来,受害者越来越难以判断这些链接是否可以安全点击。因为在过去,他们可以将鼠标悬停在一个链接上来查看它是否可疑;但是随着攻击者的技能升级,这种方式如今已经起不了作用了。</p>
<p style="text-align: left;"><strong>7. 亚马逊/某宝:您的订单#812-4623可能已到达</strong></p>
<p style="text-align: center;"><a href="http://s2.51cto.com/oss/201810/29/561de6b00a7bb4093f4eb699bfc54110.jpg" target="_blank"><img src="http://s2.51cto.com/oss/201810/29/561de6b00a7bb4093f4eb699bfc54110.jpg" alt="" title="" width="auto" height="auto" border="0" /></a></p>
<p style="text-align: left;">Hayslip指出,这种类型的网络钓鱼电子邮件通常会在假日期间出现(比如马上就要到来的双11盛典)。他解释称,每逢重大节假日,各类商家都会向目标客户发送大量打折促销、快递动态等邮件,这时候,钓鱼电子邮件也就伺机而动了。此外,一些特定类型的攻击还会在一年中的不同时间段出现:例如,金融和财务相关的骗局会在税收季到来时泛滥;而在圣诞节期间就会出现&ldquo;支付&rdquo;相关的欺诈性消息。</p>
<p style="text-align: left;">Hayslip补充道,该消息也可能并不会特别提及包裹是否已经到达,它可能会以您最近购买东西的收据为诱饵,并附带恶意附件。</p>
<p style="text-align: left;">经常在亚马逊/某宝上购物的人,很大机会会毫不犹豫地点击这些电子邮件,以查看其中的内容确定自己买的什么东西,何时能够到达等等。当然,他们也就会毫不犹豫地点击其中的恶意链接来查看他们多订购的商品,而当他们发现自己的设备已经感染恶意软件时,早已悔之晚矣。</p>
<p style="text-align: left;"><strong>用户应该如何警惕钓鱼邮件?</strong></p>
<ul>
<li>避免开启来路不明的电子邮件及文件,安装杀毒软件并及时升级病毒知识库和操作系统补丁,将敏感信息输入隐私保护,打开个人防火墙;</li>
<li>对要求重新输入账号信息,否则将停掉信用卡账号之类的邮件不予理睬;</li>
<li>尤为重要的是不要回复或者点击邮件的链接,如果你想核实电子邮件的信息,使用电话,而非鼠标;若想访问某个公司的网站,使用浏览器直接访问,而非点击邮件中的链接;</li>
<li>留意网址&ndash;多数合法网站的网址相对较短,通常以.com或者.gov结尾,仿冒网站的地址通常较长,只是在其中包括合法的企业名字(甚至根本不包含);</li>
<li>不同账号使用不同口令,不要使用同样的口令;</li>
<li>不要使用很简单的口令,(如000000、生日等);</li>
</ul>
<p style="text-align: left;">最后提醒一句,不幸中招者最好尽快更换相关密码和取消信用卡。</p>
<p>【编辑推荐】</p>
<div>
<ol>
<li><a href="http://netsecurity.51cto.com/art/201810/585733.htm" target="_blank">AI攻击怕不怕? 多数安全专家表示怕怕</a></li>
<li><a href="http://zhuanlan.51cto.com/art/201810/585770.htm" target="_blank">企业风险管理(ERM):如何将网络安全威胁融入业务上下文</a></li>
<li><a href="http://netsecurity.51cto.com/art/201810/585796.htm" target="_blank">聚焦金融网络犯罪团伙Cobalt Gang 的Commodity Builder 及其infrastructure攻击</a></li>
<li><a href="http://netsecurity.51cto.com/art/201810/585809.htm" target="_blank">黑客将Python作为攻击编码语言的首选</a></li>
<li><a href="http://netsecurity.51cto.com/art/201810/585824.htm" target="_blank">网络攻击者可以使用你的特权用户凭证的3种隐藏方式</a></li>
</ol>
</div>
<div align="right">【责任编辑:<a class="ln" href="mailto:sunsj@51cto.com">赵宁宁</a> TEL:(01068476606】</div><br>
<a href="###" class="dzdz zhan abc" target="_self">点赞 <span>0</span></a>
</div>
<div class="share5">
<ul>
<li><a href='http://www.51cto.com/php/search.php?keyword=%CD%F8%C2%E7%B5%F6%D3%E3' target='_blank' class='underline'>网络钓鱼</a>&nbsp;&nbsp;<a href='http://www.51cto.com/php/search.php?keyword=%B9%A5%BB%F7' target='_blank' class='underline'>攻击</a>&nbsp;&nbsp;<a href='http://www.51cto.com/php/search.php?keyword=%CD%F8%C2%E7%B0%B2%C8%AB' target='_blank' class='underline'>网络安全</a></li>
</ul>
<dl>
<dt><em>分享:</em>
<div class="bdsharebuttonbox" data-tag="share_1">
<a class="wb" data-cmd="tsina"></a>
<a class="wx" data-cmd="weixin"></a>
<a class="more" data-cmd="more"></a>
</div>
</dt>
<script type="text/javascript">
window._bd_share_config = {
common : {
bdText : document.title
},
share : [{
"bdSize" : 16,
}]
}
with(document)0[(getElementsByTagName('head')[0]||body).appendChild(createElement('script')).src='http://bdimg.share.baidu.com/static/api/js/share.js?cdnversion='+~(-new Date()/36e5)];
</script>
<!-- Baidu Button END -->
</dl>
</div>
<div class="nrdp comment">
<script charset="utf-8" id="ParadigmSDKv3" src="https://nbrecsys.4paradigm.com/sdk/js/ParadigmSDK_v3.js"></script>
<!--<div id='c21148bdd1e7483e87966a00982902d4'></div>-->
<script>
var host = window.location.host;
var domain_prefix = host.replace(/\.51cto\.com/, '');
function appendRecommend(){
$(".share5").prepend("<div id='c21148bdd1e7483e87966a00982902d4'></div>");
}
function clock(){
$(".dzk").hide();
var isShow = $(".dzk").css("display");
if(isShow == 'none'){
clearInterval(timer);
}
}
if(domain_prefix == 'developer') {
appendRecommend();
ParadigmSDKv3.init("dcde33d0dc6845609e9cb47f754d1094");
ParadigmSDKv3.renderArticle('c21148bdd1e7483e87966a00982902d4',346,605);
var timer = setInterval("clock()",50);
}
</script>
<div class='comment center'>
<div class='inner center' id="cmscmt_iframe"></div>
</div>
<script type="text/javascript" id="UYScript" src="http://comment.51cto.com/static/js/api_js/iframe_cmt.js" async=""></script>
</div>
<div class="dzk">
<dl><dt class="show">大家都在看</dt><dt>猜你喜欢</dt></dl>
<div>
<ul>
<li class="show">
<div class="djdzk" id="djdzk">
</div>
</li>
<li>
<div class="djdzk" id="cnxh"></div>
</li>
</ul>
</div>
</div>
</div>
<!-- 文章左侧结束 -->
<!-- 文章右侧 -->
<div class="wrap_right">
<div><!-- intel广告位 -->
<span class="mtgg" id="right1_ad" style="height:auto; width:300px;margin-bottom: 10px;">
<script type="text/javascript">
var s=window.location.toString();
var s1=s.substr(7,s.length);
var s2=s1.indexOf(".");
s=s.substr(7,s2);
if ( s == 'stor') {
var m3_u = (location.protocol=='https:'?'https://gg.51cto.com/www/delivery/ajs.php':'http://gg1.51cto.com/www/delivery/ajs.php');
var m3_r = Math.floor(Math.random()*99999999999);
if (!document.MAX_used) document.MAX_used = ',';
document.write ("<scr"+"ipt type='text/javascript' src='"+m3_u);
document.write ("?zoneid=987");
document.write ('&amp;cb=' + m3_r);
if (document.MAX_used != ',') document.write ("&amp;exclude=" + document.MAX_used);
document.write (document.charset ? '&amp;charset='+document.charset : (document.characterSet ? '&amp;charset='+document.characterSet : ''));
document.write ("&amp;loc=" + escape(window.location));
if (document.referrer) document.write ("&amp;referer=" + escape(document.referrer));
if (document.context) document.write ("&context=" + escape(document.context));
if (document.mmm_fo) document.write ("&amp;mmm_fo=1");
document.write ("'><\/scr"+"ipt>");
}else if (s == 'server'){
var m3_u = (location.protocol=='https:'?'https://gg.51cto.com/www/delivery/ajs.php':'http://gg1.51cto.com/www/delivery/ajs.php');
var m3_r = Math.floor(Math.random()*99999999999);
if (!document.MAX_used) document.MAX_used = ',';
document.write ("<scr"+"ipt type='text/javascript' src='"+m3_u);
document.write ("?zoneid=1061");
document.write ('&amp;cb=' + m3_r);
if (document.MAX_used != ',') document.write ("&amp;exclude=" + document.MAX_used);
document.write (document.charset ? '&amp;charset='+document.charset : (document.characterSet ? '&amp;charset='+document.characterSet : ''));
document.write ("&amp;loc=" + escape(window.location));
if (document.referrer) document.write ("&amp;referer=" + escape(document.referrer));
if (document.context) document.write ("&context=" + escape(document.context));
if (document.mmm_fo) document.write ("&amp;mmm_fo=1");
document.write ("'><\/scr"+"ipt>");
}else if(s == 'network'){
var m3_u = (location.protocol=='https:'?'https://gg.51cto.com/www/delivery/ajs.php':'http://gg1.51cto.com/www/delivery/ajs.php');
var m3_r = Math.floor(Math.random()*99999999999);
if (!document.MAX_used) document.MAX_used = ',';
document.write ("<scr"+"ipt type='text/javascript' src='"+m3_u);
document.write ("?zoneid=985");
document.write ('&amp;cb=' + m3_r);
if (document.MAX_used != ',') document.write ("&amp;exclude=" + document.MAX_used);
document.write (document.charset ? '&amp;charset='+document.charset : (document.characterSet ? '&amp;charset='+document.characterSet : ''));
document.write ("&amp;loc=" + escape(window.location));
if (document.referrer) document.write ("&amp;referer=" + escape(document.referrer));
if (document.context) document.write ("&context=" + escape(document.context));
if (document.mmm_fo) document.write ("&amp;mmm_fo=1");
document.write ("'><\/scr"+"ipt>");
// var str = '<div class="m30" style="position:relative;">';
// str += '<iframe src="http://network.51cto.com/act/cisco/art201709" scrolling="no" frameborder="0" sytle="" width="300" align="middle" height="360" target ="_parent"></iframe>';
// str +='<div style="left: 0px; width: 24px; height: 14px; z-index: 12; position: absolute; background: transparent url(http://s5.51cto.com/wyfs02/M00/86/BB/wKiom1fI4nWStYqXAAAEoZQn6vs942.png)repeat scroll 0% 0%; bottom: 2px;"></div>';
// str += '</div>';
// $('#right1_ad').html(str);
}
// var timestamp = Date.parse(new Date());
//timestamp = timestamp / 1000;
if (s == 'cloud') {
// var str = "<ins class='dcmads' style='display:inline-block;width:300px;height:360px' data-dcm-placement='N5751.51CTO/B11527656.153459357' data-dcm-rendering-mode='script' data-dcm-http-only data-dcm-app-id=''></ins>";
//
// $('#right1_ad').html(str);
//1496592000
var m3_u = (location.protocol=='https:'?'https://gg.51cto.com/www/delivery/ajs.php':'http://gg3.51cto.com/www/delivery/ajs.php');
var m3_r = Math.floor(Math.random()*99999999999);
if (!document.MAX_used) document.MAX_used = ',';
document.write ("<scr"+"ipt type='text/javascript' src='"+m3_u);
document.write ("?zoneid=969");
document.write ('&amp;cb=' + m3_r);
if (document.MAX_used != ',') document.write ("&amp;exclude=" + document.MAX_used);
document.write (document.charset ? '&amp;charset='+document.charset : (document.characterSet ? '&amp;charset='+document.characterSet : ''));
document.write ("&amp;loc=" + escape(window.location));
if (document.referrer) document.write ("&amp;referer=" + escape(document.referrer));
if (document.context) document.write ("&context=" + escape(document.context));
if (document.mmm_fo) document.write ("&amp;mmm_fo=1");
document.write ("'><\/scr"+"ipt>");
}
if (s == 'bigdata') {
var m3_u = (location.protocol=='https:'?'https://gg.51cto.com/www/delivery/ajs.php':'http://gg2.51cto.com/www/delivery/ajs.php');
var m3_r = Math.floor(Math.random()*99999999999);
if (!document.MAX_used) document.MAX_used = ',';
document.write ("<scr"+"ipt type='text/javascript' src='"+m3_u);
document.write ("?zoneid=970");
document.write ('&amp;cb=' + m3_r);
if (document.MAX_used != ',') document.write ("&amp;exclude=" + document.MAX_used);
document.write (document.charset ? '&amp;charset='+document.charset : (document.characterSet ? '&amp;charset='+document.characterSet : ''));
document.write ("&amp;loc=" + escape(window.location));
if (document.referrer) document.write ("&amp;referer=" + escape(document.referrer));
if (document.context) document.write ("&context=" + escape(document.context));
if (document.mmm_fo) document.write ("&amp;mmm_fo=1");
document.write ("'><\/scr"+"ipt>");
}
//var timestamp = Date.parse(new Date());
//timestamp = timestamp / 1000;
// if(s == 'netsecurity'&& (timestamp >= 1506182400)){
// var str = '<div class="m30" style="position:relative;">';
// str += '<iframe src="http://netsecurity.51cto.com/act/cisco/art201709" scrolling="no" frameborder="0" sytle="" width="300" align="middle" height="360" target ="_parent"></iframe>';
// str +='<div style="left: 0px; width: 24px; height: 14px; z-index: 12; position: absolute; background: transparent url(http://s5.51cto.com/wyfs02/M00/86/BB/wKiom1fI4nWStYqXAAAEoZQn6vs942.png)repeat scroll 0% 0%; bottom: 2px;"></div>';
// str += '</div>';
// $('#right1_ad').html(str);
// }
</script>
<script src='http://www.googletagservices.com/dcm/dcmads.js'></script>
</span></div>
<div class="mtgg m30"><script type="text/javascript" src="http://image.51cto.com/ad/art/hzh/ad.js"></script></div>
<div><!--推荐系统start-->
<script charset="utf-8" id="ParadigmSDKv3" src="https://nbrecsys.4paradigm.com/sdk/js/ParadigmSDK_v3.js"></script>
<style>
.article-box{margin-bottom: 20px;}
</style>
<div id='279ee0c8d3f94395a91a27085384c3b9'></div>
<script>
var host = window.location.host;
var domain_prefix = host.replace(/\.51cto\.com/, '');
ParadigmSDKv3.init("dcde33d0dc6845609e9cb47f754d1094");
if(domain_prefix == 'developer'){
ParadigmSDKv3.renderArticle('279ee0c8d3f94395a91a27085384c3b9',346,682);
}else{
ParadigmSDKv3.renderArticle('279ee0c8d3f94395a91a27085384c3b9',269,460);
}
var timer = setInterval("clock()",50);
var clock = function(){
$('.paradigm-recomm-logo').css({'width':'100px'});
var w = $('.paradigm-recomm-logo').width();
if(w == 100){
clearInterval(timer);
}
}
</script>
<!--推荐系统end-->
</div>
<div class="bjtj m30">
<h2><span>编辑推荐</span></h2>
<dl><dt>原创</dt><dd><a href="http://netsecurity.51cto.com/art/201810/585722.htm" title="雅虎黑客案和解,可企业数据泄露事件并未停止">雅虎黑客案和解,可企业数据泄露事件并未停止</a></dd></dl><dl><dt>原创</dt><dd><a href="http://netsecurity.51cto.com/art/201810/585257.htm" title="走进几维安全,听CTO刘柏江讲述他们想做的事儿">走进几维安全,听CTO刘柏江讲述他们想做的事儿</a></dd></dl><dl><dt>热点</dt><dd><a href="http://netsecurity.51cto.com/art/201810/585371.htm" title="浅谈公共云安全三大认知误区">浅谈公共云安全三大认知误区</a></dd></dl><dl><dt>聚焦</dt><dd><a href="http://netsecurity.51cto.com/art/201810/585354.htm" title="黑客基础,Metasploit模块简介,渗透攻击模块、攻击载荷模块">黑客基础,Metasploit模块简介,渗透攻击模块、攻击载荷模块</a></dd></dl><dl><dt>头条</dt><dd><a href="http://netsecurity.51cto.com/art/201810/585235.htm" title="企业被“勒索”遭殃,企业数据安全路在何方">企业被“勒索”遭殃,企业数据安全路在何方</a></dd></dl>
</div>
<div></div>
<div class="news m30">
<dl><dt class="show">24H热文</dt><dt>一周话题</dt><dt>本月最赞</dt></dl>
<ul><li class="show"><a href=http://netsecurity.51cto.com/art/201808/581172.htm title=2018年十大局域网监控工具,网络管理员值得一试>2018年十大局域网监控工具,网络管理员值得一试</a><a href=http://netsecurity.51cto.com/art/200909/149683.htm title=上网行为管理的发展和现状>上网行为管理的发展和现状</a><a href=http://netsecurity.51cto.com/art/201004/193632.htm title=浅析上网行为管理存在价值和理解误区>浅析上网行为管理存在价值和理解误区</a><a href=http://netsecurity.51cto.com/art/201702/531606.htm title=安卓系统里最好用的VPN工具汇总>安卓系统里最好用的VPN工具汇总</a><a href=http://netsecurity.51cto.com/art/201107/276208.htm title=上网行为管理路由器你选对了吗?>上网行为管理路由器你选对了吗?</a><a href=http://netsecurity.51cto.com/art/201112/307746.htm title=数据防泄密(DLP)产品类型浅析>数据防泄密(DLP)产品类型浅析</a><a href=http://netsecurity.51cto.com/art/201703/534641.htm title=最值得推荐的5个VPN服务>最值得推荐的5个VPN服务</a><a href=http://netsecurity.51cto.com/art/200709/57185.htm title=ARP欺骗攻击原理也可以这样理解>ARP欺骗攻击原理也可以这样理解</a></li><li><a href=http://netsecurity.51cto.com/art/201808/581172.htm title=2018年十大局域网监控工具,网络管理员值得一试>2018年十大局域网监控工具,网络管理员值得一试</a><a href=http://netsecurity.51cto.com/art/200909/149683.htm title=上网行为管理的发展和现状>上网行为管理的发展和现状</a><a href=http://netsecurity.51cto.com/art/201004/193632.htm title=浅析上网行为管理存在价值和理解误区>浅析上网行为管理存在价值和理解误区</a><a href=http://netsecurity.51cto.com/art/201107/276208.htm title=上网行为管理路由器你选对了吗?>上网行为管理路由器你选对了吗?</a><a href=http://netsecurity.51cto.com/art/201702/531606.htm title=安卓系统里最好用的VPN工具汇总>安卓系统里最好用的VPN工具汇总</a><a href=http://netsecurity.51cto.com/art/201703/534641.htm title=最值得推荐的5个VPN服务>最值得推荐的5个VPN服务</a><a href=http://netsecurity.51cto.com/art/201112/307746.htm title=数据防泄密(DLP)产品类型浅析>数据防泄密(DLP)产品类型浅析</a><a href=http://netsecurity.51cto.com/art/201802/565745.htm title=最值得推荐的五个付费VPN服务>最值得推荐的五个付费VPN服务</a></li><li><a href=http://netsecurity.51cto.com/art/201808/581172.htm title=2018年十大局域网监控工具,网络管理员值得一试>2018年十大局域网监控工具,网络管理员值得一试</a><a href=http://netsecurity.51cto.com/art/200909/149683.htm title=上网行为管理的发展和现状>上网行为管理的发展和现状</a><a href=http://netsecurity.51cto.com/art/201004/193632.htm title=浅析上网行为管理存在价值和理解误区>浅析上网行为管理存在价值和理解误区</a><a href=http://netsecurity.51cto.com/art/201702/531606.htm title=安卓系统里最好用的VPN工具汇总>安卓系统里最好用的VPN工具汇总</a><a href=http://netsecurity.51cto.com/art/201703/534641.htm title=最值得推荐的5个VPN服务>最值得推荐的5个VPN服务</a><a href=http://netsecurity.51cto.com/art/201107/276208.htm title=上网行为管理路由器你选对了吗?>上网行为管理路由器你选对了吗?</a><a href=http://netsecurity.51cto.com/art/201802/565745.htm title=最值得推荐的五个付费VPN服务>最值得推荐的五个付费VPN服务</a><a href=http://netsecurity.51cto.com/art/201112/307746.htm title=数据防泄密(DLP)产品类型浅析>数据防泄密(DLP)产品类型浅析</a></li></ul>
</div>
<div><div class="areaAd mt5" id="ad_1"></div>
<div style="display:none">
<span id="ad1">
<script type='text/javascript'><!--//<![CDATA[
var m3_u = (location.protocol=='https:'?'https://gg.51cto.com/www/delivery/ajs.php':'http://gg3.51cto.com/www/delivery/ajs.php');
var m3_r = Math.floor(Math.random()*99999999999);
if (!document.MAX_used) document.MAX_used = ',';
document.write ("<scr"+"ipt type='text/javascript' src='"+m3_u);
document.write ("?zoneid=693");
document.write ('&amp;cb=' + m3_r);
if (document.MAX_used != ',') document.write ("&amp;exclude=" + document.MAX_used);
document.write (document.charset ? '&amp;charset='+document.charset : (document.characterSet ? '&amp;charset='+document.characterSet : ''));
document.write ("&amp;loc=" + escape(window.location));
if (document.referrer) document.write ("&amp;referer=" + escape(document.referrer));
if (document.context) document.write ("&context=" + escape(document.context));
if (document.mmm_fo) document.write ("&amp;mmm_fo=1");
document.write ("'><\/scr"+"ipt>");
//]]>--></script><noscript><a href='//gg2.51cto.com/www/delivery/ck.php?n=ae95ac07&amp;cb=INSERT_RANDOM_NUMBER_HERE' target='_blank'><img src='//gg.51cto.com/www/delivery/avw.php?zoneid=693&amp;cb=INSERT_RANDOM_NUMBER_HERE&amp;n=ae95ac07' border='0' alt='' /></a></noscript>
</span>
</div>
<script>
document.getElementById('ad_1').innerHTML = document.getElementById('ad1').innerHTML;
</script></div>
<div class="spkc m30">
<h2><span>视频课程</span><a href="http://edu.51cto.com/">+更多</a></h2>
<dl>
<dt><a href="http://edu.51cto.com/course/course_id-13963.html"><img src="https://s1.51cto.com/images/201806/16/cb25f91dd59d07ad1a62030e24c802e4.png?x-oss-process=image/resize,m_fixed,h_94,w_124" title="诸神之眼 - Nmap扫描工具 基础-主机发现视频教程(全套Nmap专题更优惠)" alt="诸神之眼 - Nmap扫描工具 基础-主机发现视频教程(全套Nmap专题更优惠)" width="100px" height="80px"></a><span></span></dt>
<dd>
<h3><a href="http://edu.51cto.com/course/course_id-13963.html" target="_blank" title="诸神之眼 - Nmap扫描工具 基础-主机发现视频教程(全套Nmap专题更优惠)">诸神之眼 - Nmap扫描工具 基础-主机发现视频</a></h3>
<h4><span class="fl">讲师:<em><a href="http://edu.51cto.com/lecturer/user_id-12102806.html" target="_blank">刘晓阳</a></em></span><span class="fr"><em>949</em>人学习过</span></h4>
</dd>
</dl>
<dl>
<dt><a href="http://edu.51cto.com/course/course_id-12185.html"><img src="https://s1.51cto.com/images/201706/22/667e5ec121dd0c02e7baf0c933b7b52d.png?x-oss-process=image/resize,m_fixed,h_94,w_124" title="网络安全之最新华为USG防火墙的理论及实战视频课程" alt="网络安全之最新华为USG防火墙的理论及实战视频课程" width="100px" height="80px"></a><span></span></dt>
<dd>
<h3><a href="http://edu.51cto.com/course/course_id-12185.html" target="_blank" title="网络安全之最新华为USG防火墙的理论及实战视频课程">网络安全之最新华为USG防火墙的理论及实战视</a></h3>
<h4><span class="fl">讲师:<em><a href="http://edu.51cto.com/lecturer/user_id-9130833.html" target="_blank">李熠芳</a></em></span><span class="fr"><em>3047</em>人学习过</span></h4>
</dd>
</dl>
<dl>
<dt><a href="http://edu.51cto.com/course/course_id-14219.html"><img src="https://s1.51cto.com/images/201807/09/7959a2e0fba12d5e3043e9507275b271.png?x-oss-process=image/resize,m_fixed,h_94,w_124" title="诸神之眼 - Nmap扫描工具 大型网络扫描视频教程" alt="诸神之眼 - Nmap扫描工具 大型网络扫描视频教程" width="100px" height="80px"></a><span></span></dt>
<dd>
<h3><a href="http://edu.51cto.com/course/course_id-14219.html" target="_blank" title="诸神之眼 - Nmap扫描工具 大型网络扫描视频教程">诸神之眼 - Nmap扫描工具 大型网络扫描视频教</a></h3>
<h4><span class="fl">讲师:<em><a href="http://edu.51cto.com/lecturer/user_id-12102806.html" target="_blank">刘晓阳</a></em></span><span class="fr"><em>208</em>人学习过</span></h4>
</dd>
</dl>
</div>
<div id="zxf0309" class="fl m30">
<div class="Central_bank mb20">
<div class="titall">
<a href="http://club.51cto.com/act/cto/caff?51CTOQ" class="zlmore" target="_blank">+ 更多</a>
<div class="mintitall">
<span class="cur">CTO品牌</span>
</div>
</div>
<div class="jp-list" >
<div class="rmzw_xx">
<ul>
<li><span class="ico"></span><a href="http://siliconvalley2018.mikecrm.com/qMTa1RQ" target="_blank">CTO训练营第六季,抢先报名申请表</a></li>
<li><span class="ico"></span><a href="http://x.51cto.com/act/cto/camp/page/course_list/cid/98" target="_blank">金融科技时代,CTO的破局之道</a></li>
<li><span class="ico"></span><A href="http://siliconvalley2018.mikecrm.com/RGszRhY" target="_blank">线上社群,仅限技术管理者免费申请</A></li>
</ul>
</div>
<dl class="ctoll clearfix">
<dt><a href="http://x.51cto.com?51ctoQ" target="_blank"><span style="color:#BE0000">CTO训练营</span></a></dt>
<dd>
<A href="http://x.51cto.com/act/cto/camp/page/enroll" target="_blank">申请入营&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;</A>
<A href="http://x.51cto.com/act/cto/camp/page/course_list/cid/83?51ctoQ" target="_blank">互联网班&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;</A>
<A href="http://x.51cto.com/act/cto/camp/page/course_list/cid/98?51ctoQ" target="_blank">金融班</A>
</dd>
</dl>
<dl class="ctoll clearfix">
<dt><a href="http://club.51cto.com?51ctoQ" target="_blank"><span style="color:#BE0000">CTO俱乐部</span></a></dt>
<dd>
<A href="http://club.51cto.com/act/cto/caff/page/member-notice?51ctoQ" target="_blank">申请加入&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;</A>
<A href="http://club.51cto.com/act/cto/caff/page/activity-list?51ctoQ" target="_blank">最新活动&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;</A>
<A href="http://club.51cto.com/act/cto/caff/page/year-list?51ctoQ" target="_blank">全部课程</A>
</dd>
</dl>
</div>
</div>
</div>
<div></div>
<div></div>
<div class="zxzt m30">
<h2><span>最新专题</span><a href="http://netsecurity.51cto.com/speclist/1074">+更多</a></h2>
<dl>
<dt><a href="http://netsecurity.51cto.com/art/201810/585738.htm" title="关注网络安全 预防网络诈骗"><img src="https://s3.51cto.com/oss/201810/26/9d077d891ece918a8ed9985d2d55320d.jpg-wh_100x70-s_2209487519.jpg" alt="关注网络安全 预防网络诈骗" title="关注网络安全 预防网络诈骗"></a></dt>
<dd>
<a href="http://netsecurity.51cto.com/art/201810/585738.htm" title="关注网络安全 预防网络诈骗">关注网络安全 预防网络诈骗</a>
<h3><a href='http://www.51cto.com/php/search.php?keyword=%CD%F8%C2%E7%D5%A9%C6%AD'>网络诈骗</a></h3>
</dd>
</dl><dl>
<dt><a href="http://netsecurity.51cto.com/art/201808/581030.htm" title="2018美国黑帽大会专题报道"><img src="https://s4.51cto.com/oss/201808/10/347f7f094ab76f6edf0003b9d3744c94.jpg-wh_100x70-s_3573024760.jpg" alt="2018美国黑帽大会专题报道" title="2018美国黑帽大会专题报道"></a></dt>
<dd>
<a href="http://netsecurity.51cto.com/art/201808/581030.htm" title="2018美国黑帽大会专题报道">2018美国黑帽大会专题报道</a>
<h3><a href='http://www.51cto.com/php/search.php?keyword=%BA%DA%C3%B1%B4%F3%BB%E1'>黑帽大会</a></h3>
</dd>
</dl><dl>
<dt><a href="http://netsecurity.51cto.com/art/201804/571743.htm" title="2018第五届首都网络安全日专题报道"><img src="https://s3.51cto.com/oss/201804/27/9bc39ff8302de958e56ac7acfb254a19.jpg-wh_100x70-s_3558624953.jpg" alt="2018第五届首都网络安全日专题报道" title="2018第五届首都网络安全日专题报道"></a></dt>
<dd>
<a href="http://netsecurity.51cto.com/art/201804/571743.htm" title="2018第五届首都网络安全日专题报道">2018第五届首都网络安全日专题报道</a>
<h3><a href='http://www.51cto.com/php/search.php?keyword=%CA%D7%B6%BC%CD%F8%C2%E7%B0%B2%C8%AB%C8%D5'>首都网络安全日</a></h3>
</dd>
</dl><dl>
<dt><a href="http://netsecurity.51cto.com/art/201804/570631.htm" title="2018美国RSA信息安全大会专题报道"><img src="https://s2.51cto.com/oss/201804/16/744f4ecc960102e6d9dc4fe351d31ea9.png-wh_100x70-s_246488505.png" alt="2018美国RSA信息安全大会专题报道" title="2018美国RSA信息安全大会专题报道"></a></dt>
<dd>
<a href="http://netsecurity.51cto.com/art/201804/570631.htm" title="2018美国RSA信息安全大会专题报道">2018美国RSA信息安全大会专题报道</a>
<h3><a href='http://www.51cto.com/php/search.php?keyword=RSA'>RSA</a></h3>
</dd>
</dl>
</div>
<div></div>
<div id="jcpl"></div>
<div></div>
<div class="news m30">
<dl><dt class="show">精选博文</dt><dt>论坛热帖</dt><dt>下载排行</dt></dl>
<div>
<ul>
<li class="show">
<a href="http://sery.blog.51cto.com/10037/2309999/" target="_blank" title="proxmox超融合集群用户授权">proxmox超融合集群用户授权</a><a href="http://13706760.blog.51cto.com/13696760/2309915/" target="_blank" title="在LNMP架构中搭建zabbix监控服务!!!">在LNMP架构中搭建zabbix监控服务!!</a><a href="http://13981273.blog.51cto.com/13971273/2309905/" target="_blank" title="30分钟入门比特币与区块链">30分钟入门比特币与区块链</a><a href="http://13746824.blog.51cto.com/13736824/2309868/" target="_blank" title="自动化运维-Ansible (第三部:Playbook 介绍)">自动化运维-Ansible (第三部:Playb</a><a href="http://xjsunjie.blog.51cto.com/999372/2309332/" target="_blank" title="数据中心:一个支点,撬动全世界">数据中心:一个支点,撬动全世界</a>
</li>
<li>
<a href="http://bbs.51cto.com/thread-1506478-1.html" target="_blank" title="【其他】看看你能拿到PHP的什么学位">【其他】看看你能拿到PHP的什么学位</a><a href="http://bbs.51cto.com/thread-1515497-1.html" target="_blank" title="【小白必备】Python入门学习必备的资料(未完待续...)">【小白必备】Python入门学习必备的资</a><a href="http://bbs.51cto.com/thread-1515732-1.html" target="_blank" title="【教程经验】悬赏:函数调用的性能研究讨论">【教程经验】悬赏:函数调用的性能研</a><a href="http://bbs.51cto.com/thread-1515871-1.html" target="_blank" title="Excel 十大学习体系-表哥、表姐、表神之路必会(含47门实战课程推荐)">Excel 十大学习体系-表哥、表姐、表</a><a href="http://bbs.51cto.com/thread-1515895-1.html" target="_blank" title="IT运维10年的茫然">IT运维10年的茫然</a>
</li>
<li>
<a href="http://down.51cto.com/data/2220121/" target="_blank" title="javaservlet学习笔记">javaservlet学习笔记</a><a href="http://down.51cto.com/data/2220120/" target="_blank" title="confluence5.1-crack">confluence5.1-crack</a><a href="http://down.51cto.com/data/2220119/" target="_blank" title="20150729-电流电压互感器检验标准">20150729-电流电压互感器检验标准</a><a href="http://down.51cto.com/data/2220118/" target="_blank" title="JSP+ext+人力资源管理系统">JSP+ext+人力资源管理系统</a><a href="http://down.51cto.com/data/2220117/" target="_blank" title="JNI资料大全">JNI资料大全</a>
</li>
</ul>
</div>
</div>
<div></div>
<div class="ds m30">
<h2><span>读 书 </span><a href="http://book.51cto.com/">+更多</a></h2>
<dl>
<dt><a href="http://book.51cto.com/art/200707/51132.htm" title="SOA概念、技术与设计"><img src="http://images.51cto.com/files/uploadimg/20070712/160915523.gif" width="98px" height="144px"/></a></dt>
<dd><h3><a href="http://book.51cto.com/art/200707/51132.htm" title="SOA概念、技术与设计">SOA概念、技术与设计</a></h3>
在本书中,Thomas ERL呈现了第一部端对端的教程,提供了从基层开始的面向服务的建模与设计的逐步指导。通过逐步的、清晰生动的、良好的SOA...
</dd>
</dl>
</div>
<div></div>
<div class="dydy">
<dl>
<dt><img src="https://static1.51cto.com/51cto/cms/2016/images/dydy.jpg" alt=""></dt>
<dd>
<h3>订阅51CTO邮刊</h3>
<h4><a href="http://news.51cto.com/col/1323/">点击这里查看样刊</a></h4>
<a href="http://home.51cto.com/index.php?s=/Subscribe"><img src="https://static4.51cto.com/51cto/cms/2016/images/ljdy.jpg" alt="订阅51CTO邮刊"></a>
</dd>
</dl>
</div>
<div></div>
</div>
<!-- 文章右侧结束 -->
</div>
<div class="footer_nav">
<div class="wrap">
<h2>51CTO旗下网站:</h2>
<a href="http://www.51cto.com">领先的IT技术网站 51CTO</a>|<a href="http://cio.51cto.com">中国专业CIO网站 CIOage </a>|
<a href="http://www.hc3i.cn">中国数字医疗领军网站 HC3i</a>
<!--|<a href="http://zhijiapro.com/">区块链聚合媒体 zhijiapro</a>-->
</div>
</div>
<div id="ft"><div id="foot" align="center"><script src="http://www.51cto.com/images/copy_right/copy_right.js?v=0.5"></script></div></div>
<div class="clk"><a class="ewm" href="###" target="_self"><img src="https://s5.51cto.com/wyfs02/M01/9B/36/wKiom1lfV4XgxZ3zAADAK8-_E6k046.jpg" style="display: none;"></a><a class="yjk" href="#comment" target="_self"></a><a class="topx" href="#topx" target="_self"></a></div>
<script>
$(function(){
var host = window.location.host;
document.getElementById('cnxh').style.display = '';
});
var $$ = function(func){
if (document.addEventListener) {
window.addEventListener("load", func, false);
}
else if (document.attachEvent) {
window.attachEvent("onload", func);
}
};
$$(function(){
show();
});
var show = function(){
var aa = $('#cmscmt_iframe dl').html();
setTimeout(function(){
$("[data-track]").live("click", function() {
if (aa.indexOf('uid') > -1) {
var label = $(this).data("track") + '-login';
} else {
var label = $(this).data("track") + '-not-login';
}console.log(label);
window._hmt && window._hmt.push(['_trackEvent', label, 'click']);
});
}, 3000);
}
</script>
<script src="http://logs.51cto.com/rizhi/count/count.js"></script>
<!-- 学院广告位 -->
<!--<div id="edu_adver">-->
<!--<div class="aderbox">-->
<!--<span class="educlose">×</span>-->
<!--<a href="http://edu.51cto.com/activity/6.html?qd=CMSzuoce"><img src="https://s5.51cto.com/oss/201712/05/94601626f864124ca789425588a5a1f5.jpg" width="100" height="300" alt="51CTO学院双十二活动" title="51CTO学院双十二活动"></a>-->
<!--</div>-->
<!--</div>-->
<style type="text/css">
#edu_adver{position:fixed; top:240px; left:50%; margin-left:-623px; width:100px; height:300px; border:1px solid #c4c4c6;}
#edu_adver .aderbox{position:relative;}
#edu_adver .educlose{position:absolute; right:6px; top:6px; background:#655c4d; color:#fff; font-size:12px; display:inline-block; width:13px; height:13px; text-align:center; line-height:13px; cursor:pointer;}
</style>
<script>
$('#edu_adver .educlose').click(function () {
$("#edu_adver").hide();
});
var s=window.location.toString();
var s1=s.substr(7,s.length);
var s2=s1.indexOf(".");
s=s.substr(7,s2);
if(s=="stor"||s=="server"||s=="network"||s=="virtual"){
$("#edu_adver").hide();
}
if (s=="stor"||s=="server"||s=="network"||s=="virtual") {
$('.aderbox').find('a')[0].href = '';
$('.aderbox').find('img')[0].src = 'https://s2.51cto.com/oss/201711/01/507eecf589ca025c76a3aff2dfc1fcc4.jpg';
}
</script>
<!-- <script src="http://home.51cto.com/iframe/iframe-member-adv?www"></script> -->
<!--数据统计-->
<script>
//ParadigmSDKv3.init("dcde33d0dc6845609e9cb47f754d1094");
if(domain_prefix == 'developer') {
ParadigmSDKv3.trackDetailPageShow(346);
}else{
ParadigmSDKv3.trackDetailPageShow(269);
}
</script>
<script>
var host = window.location.host;
var domain_prefix = host.replace(/\.51cto\.com/, '');
var nowTime = new Date().getTime();
//get start time and end
var startTime1 = 0;
var endTime1 = 0;
var startTime2 = 0;
var endTime2 = 0;
var startTime3 = 0;
var endTime3 = 0;
var startTime4 = 0;
var endTime4 = 0;
var startTime5 = 0;
var endTime5 = 0;
var adContent = '';
if(domain_prefix == 'cloud'){
startTime1 = new Date('2018/08/31 00:00:00').getTime();
endTime1 = new Date('2018/08/31 23:59:59').getTime();
startTime2 = new Date('2018/09/05 00:00:00').getTime();
endTime2 = new Date('2018/09/07 23:59:59').getTime();
startTime3 = new Date('2018/09/12 00:00:00').getTime();
endTime3 = new Date('2018/09/13 23:59:59').getTime();
startTime4 = new Date('2018/09/19 00:00:00').getTime();
endTime4 = new Date('2018/09/20 23:59:59').getTime();
startTime5 = new Date('2018/09/25 00:00:00').getTime();
endTime5 = new Date('2018/09/26 23:59:59').getTime();
if((nowTime > startTime1 && nowTime < endTime1) || (nowTime > startTime2 && nowTime < endTime2) || (nowTime > startTime3 && nowTime < endTime3) || (nowTime > startTime4 && nowTime < endTime4) || (nowTime > startTime5 && nowTime < endTime5)){
adContent = "<iframe src=\"http://cloud.51cto.com/act/IBM/201808ad\" frameborder=\"0\" width=\"300px\" height=\"250px\" scrolling=\"no\"></iframe>";
changeAd();
}
}else if(domain_prefix == 'bigdata'){
startTime1 = new Date('2018/08/31 00:00:00').getTime();
endTime1 = new Date('2018/08/31 23:59:59').getTime();
startTime2 = new Date('2018/09/04 00:00:00').getTime();
endTime2 = new Date('2018/09/04 23:59:59').getTime();
startTime3 = new Date('2018/09/10 00:00:00').getTime();
endTime3 = new Date('2018/09/10 23:59:59').getTime();
startTime4 = new Date('2018/09/17 00:00:00').getTime();
endTime4 = new Date('2018/09/17 23:59:59').getTime();
startTime5 = new Date('2018/09/27 00:00:00').getTime();
endTime5 = new Date('2018/09/27 23:59:59').getTime();
if((nowTime > startTime1 && nowTime < endTime1) || (nowTime > startTime2 && nowTime < endTime2) || (nowTime > startTime3 && nowTime < endTime3) || (nowTime > startTime4 && nowTime < endTime4) || (nowTime > startTime5 && nowTime < endTime5)){
adContent = "<iframe src=\"http://cloud.51cto.com/act/IBM/201808ad2\" frameborder=\"0\" width=\"300px\" height=\"250px\" scrolling=\"no\"></iframe>";
changeAd();
}
}
function changeAd(){
//add new
$("div.mtgg").after(adContent);
//move some block to left
var adBox = $("div.mtgg");
$(".zwnr p:first").before(adBox);
$("div.mtgg").css('margin-top', '7px');
$("div.mtgg").css('margin-right', '10px');
}
</script>
<script type="text/javascript">var artid = 585852</script>
<script src='http://home.51cto.com/index.php?s=/Index/getLoginStatus2015/reback/http%253A%252F%252Fnetsecurity.51cto.com%252Fart%252F201810%252F585852.htm' charset="utf-8"></script>
<script type="text/javascript" src="https://static4.51cto.com/51cto/cms/2016/js/article.js?v=1.0"></script>
<script type="text/javascript" src="https://static5.51cto.com/51cto/cms/2016/js/article_ajax.js?v=2.1"></script>
<script src="http://other.51cto.com/php/count.php?view=yes&artID=585852" type="text/javascript"></script>
<script type="text/javascript" src="http://home.51cto.com/apps/favorite/Tpl/default/Public/js/favorbox.js"></script>
<!-- 一大波JS来袭 -->
<div id="MyMoveAd" style="display:none">
<span id="pinglun"><script type="text/javascript" src="http://other.51cto.com/php/getArtCount.php?artid=585852&type=all"></script></span>
<span id="tonglan"><script type="text/javascript" src="http://image.51cto.com/ad/art/tonglan/ad.js"></script></span>
<span id="wordlink_1"><script src="http://image.51cto.com/ad/art/wordlink/wordlink1.js"></script></span>
<span id="wordlink_2"><script src="http://image.51cto.com/ad/art/wordlink/wordlink2.js"></script></span>
<span id="wordlink_3"><script src="http://image.51cto.com/ad/art/wordlink/wordlink3.js"></script></span>
<span id="wordlink_4"><script src="http://image.51cto.com/ad/art/wordlink/wordlink4.js"></script></span>
<span id="wordlink"><script src="http://image.51cto.com/ad/art/wordlink/ad.js"></script></span>
</div>
<script type="text/javascript">
var thistid=585852;
//收藏按钮
var favor_url = 'http://netsecurity.51cto.com/art/201810/585852.htm';
var favor_title = '警惕7大最常见的网络钓鱼主题行,看看你中招没?';
document.getElementById('tonglanad').innerHTML=document.getElementById('tonglan').innerHTML;
</script>
<!-- 结束 -->
</body>
</html>

Some files were not shown because too many files have changed in this diff Show More