mirror of https://github.com/FreshRSS/FreshRSS.git
Compare commits
1062 Commits
Author | SHA1 | Date |
---|---|---|
dependabot[bot] | f6d3c3513a | |
dependabot[bot] | 7d4f85a58a | |
dependabot[bot] | 1510b81743 | |
dependabot[bot] | e727442f1d | |
dependabot[bot] | d99759b505 | |
maTh | 5b197b8130 | |
Alexandre Alapetite | be0e271fd5 | |
Alexandre Alapetite | aa2be914d7 | |
maTh | 848c8195dd | |
maTh | 9de4f23bb7 | |
Ramazan Sancar | 34fb69da02 | |
Ramazan Sancar | f63fdb3ddd | |
maTh | 8755dcf98d | |
Alexandre Alapetite | 7593e0815b | |
Alexandre Alapetite | b7cc814238 | |
Alexandre Alapetite | 3cd90a2b1f | |
Robin Métral | 3e7054bddd | |
maTh | 7f61a44201 | |
Alexandre Alapetite | 14b397afdf | |
Alexandre Alapetite | 6d7b5bf997 | |
Alexandre Alapetite | 5b1c36dcf1 | |
Amrul Izwan | 09d421c9a6 | |
Alexandre Alapetite | c0e87f6a5c | |
maTh | e5118d83e4 | |
Jordi Garcia | a69b44b94e | |
Karim | 08d6328975 | |
Karim | f193a910dc | |
Shane Redman | 8e3bfa1a47 | |
András Marczinkó | c54f4fe771 | |
Alexandre Alapetite | bb1a6cafb6 | |
Alexandre Alapetite | 2ed91026fc | |
Alexandre Alapetite | 2d17c020b6 | |
Alexandre Alapetite | f958eaef2f | |
Alexandre Alapetite | 4f57a46075 | |
Alexandre Alapetite | e0cc121c7a | |
maTh | 0940025980 | |
maTh | 3b953d759b | |
maTh | b4eca4ba03 | |
Alexandre Alapetite | fa731db286 | |
Alexandre Alapetite | ea2bcf3a5d | |
Alexandre Alapetite | fd7157e40c | |
Alexandre Alapetite | 44625eed25 | |
Alexandre Alapetite | 0e6f56bb4c | |
Alexandre Alapetite | 617f9a7fa6 | |
Alexandre Alapetite | cd8fc428cb | |
dependabot[bot] | c9307e4324 | |
dependabot[bot] | 5a8adf5f23 | |
dependabot[bot] | da9789d293 | |
dependabot[bot] | 47f941101c | |
dependabot[bot] | ed6e90c67e | |
dependabot[bot] | eddb10bba9 | |
dependabot[bot] | a580d4e4b2 | |
Alexandre Alapetite | ffe68dcb97 | |
maTh | cd66ca54ca | |
maTh | 40ac02544e | |
maTh | d4ac7ea26b | |
Alexandre Alapetite | 329fd4bcf6 | |
Alexandre Alapetite | 173555795a | |
Alexandre Alapetite | 5ca0b893b9 | |
maTh | d656896a95 | |
maTh | c47b785235 | |
maTh | aac3b21a8b | |
FromTheMoon | 7b92266855 | |
maTh | 154a36700c | |
Alexandre Alapetite | 5e66adcc51 | |
Alexandre Alapetite | 90fbb524ce | |
Tibor Repček | b37404cce7 | |
maTh | f0d9134478 | |
Tibor Repček | a51fb891b5 | |
Tibor Repček | c0fdefcfeb | |
Tibor Repček | 048b36a11b | |
Tibor Repček | bbb9834f92 | |
Tibor Repček | b1c3022a91 | |
-Shiken- | 339fcfda10 | |
Bartosz Taudul | b4d7649504 | |
May | 2f74ebafa8 | |
yzqzss | 5c33e5191a | |
maTh | 81f6bbf64e | |
Pedro Paulo | 68744f0106 | |
FabioL | 1308dd6b82 | |
Cilga Iscan Tercanli | f6f7764ba7 | |
Pedro Paulo | dcd3b80a90 | |
Frans de Jonge | a5f87e0671 | |
Sungjoon Moon | e1834f61a6 | |
zu | 7ccbdef935 | |
zu | bc7c680438 | |
maTh | 5a14ff3135 | |
maTh | 3261b7bafb | |
maTh | 0ffcf41f93 | |
maTh | 22172fd5bc | |
maTh | bf68205ae3 | |
Alexandre Alapetite | b22d9279bd | |
maTh | 6901ff1e70 | |
maTh | 2846fdba6f | |
Hkcomori | 18532eaa61 | |
Frans de Jonge | e19b8a4e0a | |
Alexandre Alapetite | 7aaed6092f | |
Jacopo Galati | 30f147410d | |
Alexandre Alapetite | 350edf398c | |
Alexandre Alapetite | 8280e3d88e | |
Alexandre Alapetite | 72933b301e | |
Alexandre Alapetite | 6e12781821 | |
Alexandre Alapetite | 283341e75e | |
Alexandre Alapetite | c052149e5a | |
Alexandre Alapetite | e3c86a164d | |
Alexandre Alapetite | 1c684a91d2 | |
dependabot[bot] | 3274f82307 | |
dependabot[bot] | 9742e99302 | |
dependabot[bot] | edd569c481 | |
dependabot[bot] | 5d7244e4e3 | |
dependabot[bot] | aa5c433093 | |
Frans de Jonge | d7f5ef3627 | |
maTh | c18987fae3 | |
maTh | 1ae21260bb | |
Alexandre Alapetite | 1fb0cdfd06 | |
Soniya Prasad | 9d48121e05 | |
Alexis Degrugillier | 7da0e70a72 | |
maTh | bb0fc2a54a | |
maTh | 75a17ff410 | |
Frans de Jonge | 5d1493ba98 | |
maTh | ff9325ed27 | |
maTh | 1c0b8a7dcd | |
Alexandre Alapetite | b5445e1e56 | |
maTh | 6bd6494ad4 | |
Alexandre Alapetite | d7bc70e3fb | |
maTh | 2feb97e9b9 | |
Alexandre Alapetite | cf29ca19c0 | |
maTh | c0db581f2b | |
Alexandre Alapetite | 354a5e8388 | |
maTh | 3841103fa0 | |
Alexandre Alapetite | d0072b9fb7 | |
dependabot[bot] | 01eaaed9bb | |
Alexandre Alapetite | f01269cd02 | |
dependabot[bot] | 836982538b | |
dependabot[bot] | 8295edd6f6 | |
dependabot[bot] | 83f7bba7c4 | |
dependabot[bot] | 80649ff23d | |
dependabot[bot] | dc01e2fe9d | |
dependabot[bot] | ad7cd73268 | |
dependabot[bot] | b8a2840473 | |
dependabot[bot] | c05ca141b2 | |
Alexandre Alapetite | 3a4fd0d506 | |
Alexandre Alapetite | 5aeab896e9 | |
Clemens Neubauer | 9711f1477d | |
maTh | da43fff437 | |
Alexandre Alapetite | 5e54d5bc58 | |
Alexandre Alapetite | 96484d22a1 | |
Kasimir Cash | 4b29e666b0 | |
maTh | 5de794ee0f | |
Alexandre Alapetite | 358e6e05ca | |
Alexandre Alapetite | bdf899164b | |
Alexandre Alapetite | bfd277065c | |
Alexandre Alapetite | 39cc1c11ec | |
Alexandre Alapetite | 25166c218b | |
Alexandre Alapetite | 84d88d4b5d | |
Alexandre Alapetite | 3eb3574b13 | |
Alexandre Alapetite | 7d6a64a522 | |
Alexandre Alapetite | 53d40ea3bb | |
hkcomori | f992a7007e | |
Alexandre Alapetite | 2989470e88 | |
Alexandre Alapetite | 06570b30f0 | |
maTh | 4d95ef7164 | |
Alexandre Alapetite | 6228f959f7 | |
Guilherme Gall | d36e11085d | |
Thomas Renes | 0c023a7b5c | |
Guilherme Gall | d61734a5e0 | |
FabioL | 86631d24b9 | |
Alexandre Alapetite | c0812032dd | |
FabioL | c66382cc91 | |
maTh | 415490dd7c | |
Alexandre Alapetite | 9672905711 | |
th0mcat | 79bf7d4967 | |
Alexandre Alapetite | 4a0e8a7058 | |
Kasimir Cash | 9b1f971333 | |
Kasimir Cash | 6d14813840 | |
Alexandre Alapetite | 314077a457 | |
Alexandre Alapetite | 52f6c8399b | |
Alexandre Alapetite | 74ed1e6c57 | |
eta-orionis | 9c97d8ca72 | |
Kasimir Cash | 9a80dde238 | |
Alexandre Alapetite | c89073d60e | |
Alexandre Alapetite | 4704c11d17 | |
Simone | 889746cb10 | |
Simone | 6075ee8158 | |
Alexandre Alapetite | 70e71b8364 | |
Alexandre Alapetite | 1e5f5078ed | |
Alexandre Alapetite | e240ee1caf | |
laxmanpradhan | 77108ea19e | |
Alexandre Alapetite | a3ebfe76ea | |
András Marczinkó | 0c178fe562 | |
Alexandre Alapetite | 9c104ee6bb | |
Alexandre Alapetite | 227233b4ef | |
maTh | 6a43d06566 | |
András Marczinkó | 9dec2af35d | |
Alexandre Alapetite | 386c982443 | |
Alexandre Alapetite | 39952f57fb | |
yzqzss | 2bd9f63030 | |
Alexandre Alapetite | 08345d0a95 | |
Alexandre Alapetite | 65c6c2d5cb | |
Alexandre Alapetite | 8b090ee4a3 | |
Alexandre Alapetite | d65f77c081 | |
Alexandre Alapetite | e968964538 | |
Alexandre Alapetite | bf1eda8c05 | |
Zhiyuan Zheng | ec7fd382e5 | |
Alexandre Alapetite | 941b370fe5 | |
Alexandre Alapetite | a67c11c52d | |
Alexandre Alapetite | ad990a21a1 | |
Alexandre Alapetite | ac5980231b | |
Alexandre Alapetite | 6d2e53178b | |
Alexandre Alapetite | c7a3281a73 | |
Alexandre Alapetite | bd9e33a25c | |
Alexandre Alapetite | 79604aa4b3 | |
Alexandre Alapetite | a80a5f48a1 | |
Alexandre Alapetite | 6bb45a8726 | |
Alexandre Alapetite | a3ed826913 | |
Alexandre Alapetite | 6a04503c7e | |
Kasimir Cash | ea6b8b7c6f | |
Alexandre Alapetite | f7c160b9af | |
Alexandre Alapetite | b46ea88c35 | |
Alexandre Alapetite | 803fa8fdb5 | |
Alexandre Alapetite | 8bff77c45e | |
Alexandre Alapetite | 133892a89e | |
Alexandre Alapetite | f0d4f2762d | |
András Marczinkó | eb2c2d9a01 | |
Alexandre Alapetite | 8c18dc16a9 | |
Alexandre Alapetite | 969758a73e | |
Alexandre Alapetite | 7e1f549eb4 | |
Alexandre Alapetite | 203132b015 | |
Alexandre Alapetite | 76cbfadcdf | |
Alexandre Alapetite | bc9ef0d188 | |
Alexandre Alapetite | b65ea97901 | |
Alexandre Alapetite | 445e49db15 | |
Alexandre Alapetite | e70e5542e4 | |
Luc SANCHEZ | 30c7a61a9b | |
martinrotter | ee99e7e2cc | |
maTh | 0504fc6766 | |
maTh | 96515d02be | |
maTh | b3c0b4e979 | |
Alexandre Alapetite | 618ce380e7 | |
Alexandre Alapetite | 8631d6f80d | |
Alexandre Alapetite | 9eba8726ac | |
FireFingers21 | b1d568697a | |
maTh | 2b8b80a5a9 | |
Joe Stump | 641b891972 | |
Dan Hersam | 0fb339f0f1 | |
Dan Hersam | 57f46922e8 | |
Alexandre Alapetite | e6b0f8c3e3 | |
Alexandre Alapetite | d918ab8bc8 | |
Benjamin Reich | 68aa9f335e | |
Ben Passmore | b9939bdaac | |
Alexandre Alapetite | 44a7c54a5a | |
Alexandre Alapetite | 348028a290 | |
Alexandre Alapetite | 7d26dcc847 | |
Alexandre Alapetite | 00ae423924 | |
François-Xavier Payet | e6c5054922 | |
Alexandre Alapetite | 85345559c7 | |
Alexandre Alapetite | 711e2153d1 | |
Alexandre Alapetite | d4f659f915 | |
Alexandre Alapetite | 487c740900 | |
Benjamin Reich | 8f07199777 | |
Justin Tracey | 3b2e66051b | |
Justin Tracey | df56d3b3bf | |
Frans de Jonge | ecf1585d74 | |
maTh | a4dc348c3d | |
maTh | d66dff4029 | |
Alexandre Alapetite | 8b2f7848eb | |
Alexandre Alapetite | 0795d47d82 | |
maTh | 3b408443be | |
Jan van den Berg | 61f01d9c35 | |
Alexandre Alapetite | 5a383c1054 | |
Alexandre Alapetite | 72aaea8636 | |
Alexandre Alapetite | a5748ad74f | |
Alexandre Alapetite | 0234f4e40b | |
Mark Monteiro | 6fd1195f95 | |
Alexandre Alapetite | ad8bae5aca | |
Alexandre Alapetite | 4207f2a5b2 | |
Alexandre Alapetite | d8ef50122d | |
Alexandre Alapetite | b897bd60b7 | |
Alexandre Alapetite | 5ca9101ce8 | |
Alexandre Alapetite | 9b3a867c35 | |
Alexandre Alapetite | 21a279179a | |
Alexandre Alapetite | f72a5a43b3 | |
Alexandre Alapetite | 0324df6f88 | |
Alexandre Alapetite | 06d0099504 | |
Luc SANCHEZ | 4a02352ccc | |
Alexandre Alapetite | d50bb386e7 | |
Alexandre Alapetite | cc2878aed8 | |
Mubarak Harran Alketbi | 3c80ee81f3 | |
Alexandre Alapetite | 58179a33a5 | |
Alexandre Alapetite | de51f6e7a0 | |
maTh | baab354ca2 | |
maTh | 1712d83c34 | |
Alexandre Alapetite | 1aa43e894e | |
maTh | 98aa0b474e | |
Alexandre Alapetite | 51a95afdbb | |
Alexandre Alapetite | b228342b2f | |
maTh | 58a46f0023 | |
maTh | 6836be6d33 | |
Frans de Jonge | ee04355b28 | |
Alexandre Alapetite | ce6ba583be | |
Alexandre Alapetite | 506fe3f44c | |
Alexandre Alapetite | 619d3f54a3 | |
XtremeOwnage | c479d9291c | |
XtremeOwnage | a9a7643e71 | |
maTh | 1db1035ec2 | |
Mossroy | 5ac21a8650 | |
Mossroy | 5374df384a | |
Mossroy | 280a1e1155 | |
maTh | e1ad4fc733 | |
Alexandre Alapetite | bfdf7b05ca | |
Alexandre Alapetite | 58cf2f058f | |
Alexandre Alapetite | 780088b16b | |
Alexandre Alapetite | 29f3a41d12 | |
Alexandre Alapetite | a2588fea22 | |
maTh | 41d7005c0e | |
Alexandre Alapetite | af854f07d7 | |
robertdahlem | 83cf7301f4 | |
Alexandre Alapetite | 430d467b5a | |
maTh | 3116fbdbc0 | |
NaeiKinDus | ed07055ace | |
maTh | 6f228453e4 | |
maTh | 573e8e7072 | |
Alexandre Alapetite | 13a1c412df | |
dependabot[bot] | 8d72465f28 | |
maTh | f5c3a9004d | |
VYSE V.E.O | b82c41f6b9 | |
Alexandre Alapetite | 89a3d36c3e | |
Alexandre Alapetite | 86d713478b | |
berumuron | 662c9fcc2f | |
Alexandre Alapetite | 2cb4f2e233 | |
Alexandre Alapetite | 0beabc333f | |
Alexandre Alapetite | db53d2655b | |
Alexandre Alapetite | 3617360883 | |
Alexandre Alapetite | bc5666cd27 | |
Sam Cohen | 52d87c3eaa | |
Alexandre Alapetite | 0bf33abac8 | |
Alexandre Alapetite | f050a94b48 | |
maTh | f5aba79d14 | |
Balazs Keresztury | f470724c6e | |
maTh | 61a2828820 | |
Alexandre Alapetite | 24be95756f | |
maTh | 9beba6337e | |
Alexandre Alapetite | 98559cebc3 | |
Alexandre Alapetite | 2e1d45a88d | |
Alexandre Alapetite | 1c7c1016f4 | |
maTh | da405ceee6 | |
Miguel Sánchez | dd91fe164d | |
Miguel Sánchez | 06b613a50d | |
András Marczinkó | 92e56ddc27 | |
András Marczinkó | 93fb55c9f8 | |
maTh | 3b4a865ce6 | |
FromTheMoon | 6749adc050 | |
Alexandre Alapetite | d165ed1fb6 | |
berumuron | 54592fa1fd | |
berumuron | 1e0e4f54a5 | |
berumuron | eb57f490db | |
AmirHossein | 6d8a5429cb | |
Steve Jones | 8f188b57ee | |
Alexandre Alapetite | 6ee73b5fca | |
Alexandre Alapetite | c49c29a561 | |
Alexandre Alapetite | 4039f6c9a4 | |
Alexandre Alapetite | e7689459f2 | |
Alexandre Alapetite | 0182d84142 | |
Alexandre Alapetite | 1f05c92376 | |
Zhaofeng Li | c35a9ee061 | |
dependabot[bot] | d12bcefb5a | |
otaconix | a066be93b0 | |
Alexandre Alapetite | db5d458cb2 | |
dependabot[bot] | 6f4c6f8993 | |
Luc SANCHEZ | f8f163d054 | |
Luc SANCHEZ | 7f9594b8c7 | |
Alexandre Alapetite | 1db606bc1b | |
maTh | ebf62a4296 | |
otaconix | fc579bd2bc | |
maTh | ee195354d9 | |
maTh | 2f48509678 | |
maTh | 666e951fa3 | |
David Lynch | 69994078d7 | |
maTh | fca8ae4207 | |
Alexandre Alapetite | de59076ae1 | |
Alexandre Alapetite | 8bf362838e | |
Alexandre Alapetite | 723f7577d0 | |
Alexandre Alapetite | 228d7adfdb | |
Alexandre Alapetite | 644427b9b1 | |
Alexandre Alapetite | dd5a021061 | |
Alexandre Alapetite | 3fe68a3285 | |
maTh | 3d9e0c47ec | |
maTh | 67130bca3a | |
Alexandre Alapetite | 148a2268ff | |
Alexandre Alapetite | ae8dfc1b1b | |
Aaron Schif | 58b254f9cb | |
yubiuser | 15d143989b | |
Alexandre Alapetite | 2f5ef39cf2 | |
Alwaysin | 7a5dd5cedd | |
maTh | bd97c5601d | |
Alexandre Alapetite | aab51be544 | |
Alexandre Alapetite | a495e995bc | |
Luc SANCHEZ | bab353ce61 | |
maTh | 0308c33d97 | |
LLeana | ca0c1edbe3 | |
Luc SANCHEZ | 1d2bb50f2e | |
Alexandre Alapetite | adb5db9d97 | |
Luc SANCHEZ | 8f0a121e6a | |
acbgbca | df865d7900 | |
LleanaRuv | 1c47483da7 | |
Alexandre Alapetite | 0292b2f1f3 | |
maTh | eeefbdf9c7 | |
vrachnis | df80913747 | |
Alexandre Alapetite | 445cc23abd | |
Alexandre Alapetite | d554d0f673 | |
Alexandre Alapetite | 3bdb897610 | |
Alexandre Alapetite | ea503975d5 | |
Alexandre Alapetite | 2038d50110 | |
Alexandre Alapetite | c8d2ead763 | |
maTh | 360400b723 | |
maTh | cd004cb978 | |
maTh | e65f399daa | |
Alexandre Alapetite | d8c535c25c | |
Alexis Degrugillier | 68766a9857 | |
maTh | b1d55b8de3 | |
Alexandre Alapetite | 4b2a94453f | |
maTh | deb306c33f | |
Alexandre Alapetite | 6e2f2f1c1e | |
Alexandre Alapetite | fe7d9bbcd6 | |
maTh | 2343f0ded1 | |
Alexandre Alapetite | 97226dc8a6 | |
maTh | b97545aff3 | |
Luc SANCHEZ | 078ceaa0bf | |
Alexandre Alapetite | 4c5f3bbd9b | |
Alexandre Alapetite | 26bc0e0ee9 | |
Alexandre Alapetite | afacebc3e2 | |
maTh | 54c8de86c7 | |
Luc SANCHEZ | 9172b65cdb | |
Luc SANCHEZ | f90cd8042c | |
Luc SANCHEZ | aa30635f97 | |
Alexandre Alapetite | fde4e79ed0 | |
Alexandre Alapetite | 30c69ef147 | |
Alexandre Alapetite | 675c56f579 | |
Alexandre Alapetite | 0a38aa7456 | |
Alexandre Alapetite | bd9fa803f1 | |
Alexandre Alapetite | 4de1d5efea | |
Alexandre Alapetite | 53808c6c05 | |
Alexandre Alapetite | ffacdaa57a | |
maTh | 9af1c1ca1b | |
Luc SANCHEZ | 49000ca587 | |
Luc SANCHEZ | 2199df8ad7 | |
Alexandre Alapetite | c72914bba2 | |
Alexandre Alapetite | 26e2a70312 | |
dependabot[bot] | c846b5a5a0 | |
Alexandre Alapetite | cc03cee746 | |
Alexandre Alapetite | 115724622f | |
Alexandre Alapetite | 8abe53d879 | |
Alexandre Alapetite | 2208974c00 | |
Alexandre Alapetite | 273b36c54c | |
Alexandre Alapetite | 5579dc88ab | |
Alexandre Alapetite | ecd956c736 | |
maTh | 2f53214c15 | |
Alexandre Alapetite | ef82e218ea | |
Alexandre Alapetite | 687d0b40a8 | |
Luc SANCHEZ | 8cc8127c3c | |
Alexandre Alapetite | 62496339b6 | |
Luc SANCHEZ | 5185bcef13 | |
Alexandre Alapetite | f3760f138d | |
maTh | 41fa4e746d | |
maTh | 52cde870e2 | |
maTh | 48954503d1 | |
Alexandre Alapetite | efcc8f387b | |
maTh | 789c44b502 | |
Luc SANCHEZ | a19b56064d | |
Luc SANCHEZ | b8662f8899 | |
Alexandre Alapetite | b3121709d6 | |
Alexandre Alapetite | 61550b1d2d | |
Luc SANCHEZ | 6e7fa07a39 | |
Alexandre Alapetite | a13a20de20 | |
maTh | 1f4f8d7553 | |
maTh | 5f33ca921a | |
maTh | 96dfecc875 | |
Luc SANCHEZ | 594d118bc4 | |
Luc SANCHEZ | 03129a2ee7 | |
maTh | 64a5608480 | |
maTh | 3aa54035f1 | |
Alexandre Alapetite | 90bf0ecd81 | |
Alexandre Alapetite | 74bf894db0 | |
maTh | 73057f6646 | |
Alexandre Alapetite | 6c07489466 | |
Alexandre Alapetite | 743ca371bb | |
Alexandre Alapetite | b2ee8a660f | |
Luc SANCHEZ | 2882f44179 | |
Alexandre Alapetite | b6ac505f8f | |
Alexandre Alapetite | dbbae15a84 | |
Luc SANCHEZ | d23d10bcde | |
Alexandre Alapetite | 6c01e4e7d6 | |
Alexandre Alapetite | 2118448133 | |
Luc SANCHEZ | 4f078958b5 | |
Alexandre Alapetite | 3f1695db03 | |
Alexandre Alapetite | 36aa0122e1 | |
Exerra | 2340f7a1ba | |
Exerra | 30b5a391f9 | |
Alexandre Alapetite | 4e2bbf820a | |
Alexandre Alapetite | 0f5e321c0b | |
Alexandre Alapetite | 288ed04ccc | |
Alexandre Alapetite | c9d5fe2da1 | |
Alexandre Alapetite | 6a5857ea5f | |
Alexandre Alapetite | ea87708010 | |
Alexandre Alapetite | bb6bc8fe2c | |
Alexandre Alapetite | ab49ee6c0c | |
Luc SANCHEZ | ac3dd96f48 | |
Luc SANCHEZ | 0317683155 | |
Alexandre Alapetite | f3af3f0f3d | |
maTh | 05e10f0e75 | |
Rufubi | 9604856482 | |
Alexandre Alapetite | 1ee2a3d72d | |
maTh | 59c1405c7d | |
Luc SANCHEZ | 5f898dcc5e | |
Alexandre Alapetite | df24fa2207 | |
Alexandre Alapetite | e750448f5b | |
Alexandre Alapetite | 1a0616562d | |
Luc SANCHEZ | 247215ffaa | |
maTh | e679d3df0e | |
maTh | 34f62896ac | |
Alexandre Alapetite | 9b424a8fd8 | |
maTh | 73b4fd74a4 | |
maTh | afccff4456 | |
Alexandre Alapetite | edee27ff0f | |
Alexandre Alapetite | b6223f2cfb | |
witchcraze | 425d790735 | |
maTh | ce5572d7dd | |
Rebecca Scott | b22ea9d3d7 | |
berumuron | 165a6b7347 | |
Alexandre Alapetite | 989bfc69bb | |
obrenckle | 538017c15a | |
obrenckle | fc7d3d4b5e | |
Luc SANCHEZ | 23447f1221 | |
Luc SANCHEZ | 6c01d05171 | |
Rebecca Scott | 1c502aaac2 | |
maTh | 76d547d830 | |
maTh | f0a545a47a | |
Konrad Gräfe | 974e5dd133 | |
Konrad Gräfe | 16472fd427 | |
maTh | 068d18b69b | |
Sadetdin EYILI | d3966befaf | |
maTh | 27c7367534 | |
maTh | 1c1e63c6ad | |
maTh | a7e1428485 | |
Alexandre Alapetite | b5969494f9 | |
Alexandre Alapetite | 88026e1e75 | |
Alexandre Alapetite | b3239256dc | |
Alexandre Alapetite | 0fe0ce894c | |
Alexandre Alapetite | de062c88ea | |
Alexandre Alapetite | d2cc5ad3f8 | |
Alexandre Alapetite | 32acd6c13e | |
Alexandre Alapetite | 44f72889a5 | |
Alexandre Alapetite | 4f957dfc4c | |
maTh | 21f342af2c | |
Alexandre Alapetite | c170c390b7 | |
Frans de Jonge | e56ecf79f6 | |
maTh | 859c48383a | |
mincerafter42 | 6764758658 | |
mincerafter42 | 60edc28528 | |
Alexandre Alapetite | e2a7e192a6 | |
yzqzss | f3216b61b9 | |
maTh | 2f9b14354c | |
maTh | f015029c8e | |
Luc SANCHEZ | 212ab5c9b3 | |
maTh | e908222589 | |
Alexandre Alapetite | 05ae1b0d26 | |
Luc SANCHEZ | b9a62a6aaa | |
Alexandre Alapetite | 64d68a691c | |
Alexandre Alapetite | e899e4edd9 | |
Alexandre Alapetite | de2077b563 | |
Luc SANCHEZ | 40aa8b9264 | |
Alexandre Alapetite | 9b5de54a9c | |
maTh | 9f221e9c51 | |
maTh | e53ba88bb9 | |
Alexandre Alapetite | 4ad66c24bf | |
maTh | 9e4b4c9072 | |
Alexandre Alapetite | 4f316b2ed3 | |
Alexandre Alapetite | 2303b29e68 | |
Alexandre Alapetite | e617830e96 | |
Alexandre Alapetite | d8744a9ec1 | |
Alexandre Alapetite | 07efaf71ea | |
Alexandre Alapetite | 2f02754522 | |
maTh | 1c4b328ae1 | |
maTh | 1aab0459fa | |
Alexandre Alapetite | 1c434971d4 | |
Luc SANCHEZ | 2216940f00 | |
maTh | 4755a12c96 | |
maTh | f31c310a6d | |
Luc SANCHEZ | 75a203fff2 | |
Alexandre Alapetite | c13f1de139 | |
Alexandre Alapetite | dbdb7869c4 | |
Alexandre Alapetite | d105761fec | |
berumuron | daaa391e33 | |
maTh | 216e39c3cc | |
maTh | 33468def4a | |
maTh | 0ab130eb9c | |
maTh | e67ca8c866 | |
Alexandre Alapetite | cfaaed7e0b | |
Alexandre Alapetite | 075cf4c800 | |
maTh | c75baefe40 | |
Alexandre Alapetite | 1d9d4e3e3c | |
Alexandre Alapetite | 3fb8ab8eb5 | |
maTh | b5a418ec16 | |
Sadetdin EYILI | bbe3eb8f41 | |
maTh | 7d12ecff01 | |
maTh | 5feefe416f | |
dependabot[bot] | e683d2f7ac | |
Alexandre Alapetite | 8f9c4143fc | |
maTh | af8480651d | |
maTh | 92d1b0cda7 | |
maTh | ba7cb98bdf | |
maTh | c0a89a99b4 | |
Sadetdin EYILI | 2850c0baea | |
Sadetdin EYILI | 914bc62aef | |
Sadetdin EYILI | de5f70c684 | |
maTh | 3c53125b01 | |
maTh | 9cb0bbd0c1 | |
Alexandre Alapetite | 7330cbab38 | |
maTh | b64650801f | |
maTh | 6131aa4479 | |
maTh | e072411baf | |
maTh | 695d6de362 | |
maTh | f8ba54a3df | |
Alexandre Alapetite | f5bf654b1e | |
Alexandre Alapetite | 27b71ffa99 | |
Alexandre Alapetite | c82bd3b173 | |
Alexandre Alapetite | 62afc060a8 | |
Alexandre Alapetite | b835c426d4 | |
Alexandre Alapetite | 45fb0d6803 | |
Alexandre Alapetite | ed578f224c | |
maTh | a501cc88e7 | |
maTh | 60ffdb0580 | |
Alexandre Alapetite | 543fa4e76c | |
maTh | f41a574a9f | |
maTh | be17cc6144 | |
Alexandre Alapetite | 6261dc9cf4 | |
Alexandre Alapetite | 60d626030d | |
maTh | b71806268c | |
maTh | c05e931d5e | |
Hippolyte Thomas | 999e88c662 | |
Alexandre Alapetite | 50f293c346 | |
maTh | 48c8ee574d | |
Alexandre Alapetite | aa07582419 | |
Alexandre Alapetite | be79c5a8e7 | |
maTh | ef5483490c | |
maTh | c9f5012f10 | |
maTh | 4869ae0fec | |
maTh | 1184c20467 | |
maTh | 18a4ade32f | |
Nicolas Frandeboeuf | 88ef6174c0 | |
Miika Launiainen | 10c5a9326e | |
Alexandre Alapetite | 4bf678f8e4 | |
maTh | 9b674e7e93 | |
Alexis Degrugillier | 02b906549e | |
Myuki | 77c214c83c | |
Alexandre Alapetite | be57be5ab2 | |
maTh | 278e07f7bd | |
maTh | e7aa062858 | |
maTh | 52d26d6da1 | |
maTh | 47883d3086 | |
maTh | 0ad8e6b418 | |
Alexandre Alapetite | e1b2f6ae13 | |
Alexandre Alapetite | 3bcceb1338 | |
Alexandre Alapetite | c5b741d98f | |
Axel Leroy | a0e3fac47e | |
Zhiyuan Zheng | ce5531b39a | |
Alexandre Alapetite | 42eeb402ad | |
maTh | 07c94061a9 | |
Alexandre Alapetite | 937ef98e4e | |
Luc SANCHEZ | d9e1c1c2c3 | |
Luc SANCHEZ | a44e2a0d0c | |
Alexandre Alapetite | 570503b7f1 | |
maTh | acf459429b | |
Alexandre Alapetite | 8864d514c8 | |
maTh | 1f86aae415 | |
Alexandre Alapetite | 37cc854d12 | |
Alexandre Alapetite | 5035dadfdd | |
Alexandre Alapetite | 4571b7d68d | |
maTh | 5c0653ad10 | |
maTh | ea808bb7c7 | |
Alexandre Alapetite | 330b1ab74a | |
Alexandre Alapetite | 44c0575e25 | |
maTh | 5daacd483b | |
Matt Sephton | dfaf94eb0a | |
maTh | 28fe689c06 | |
Alexandre Alapetite | 992f2230bf | |
maTh | 0b8b2d2263 | |
maTh | df962341c4 | |
Alexandre Alapetite | 2c0f3fad2d | |
Alexandre Alapetite | 5897487f2f | |
Alexandre Alapetite | f2fe9e2ff3 | |
maTh | ca6c9345b2 | |
maTh | b1f9463673 | |
maTh | 8781cd12d7 | |
Pyrox | 1e2683be7b | |
maTh | d8d30b7a1c | |
Pyrox | f16341aa26 | |
maTh | 400da71eeb | |
maTh | 746e37c4a6 | |
maTh | 65837de0e1 | |
maTh | 18251b836b | |
maTh | f47e13ac41 | |
maTh | 301d3c81ba | |
maTh | 024495fb1f | |
maTh | fc3ed3662a | |
maTh | 270828aa64 | |
maTh | a7719d9b90 | |
Thomas Guesnon | fc93776071 | |
maTh | a184712ef2 | |
Alexis Degrugillier | 4ec602e8e3 | |
Alexis Degrugillier | 9e27ab9200 | |
maTh | c5539009c9 | |
maTh | 45f6d84b69 | |
acbgbca | edbf0fe7db | |
Alwaysin | cb1c2da8e6 | |
maTh | c11976f587 | |
maTh | c191e0315e | |
Alexandre Alapetite | 5e71669589 | |
Alexandre Alapetite | e96b6266b5 | |
Alexis Degrugillier | 1f4e347cae | |
Thelonius Kort | d4181e098d | |
Alexandre Alapetite | 4d2175888b | |
maTh | 9621ab1747 | |
Alexis Degrugillier | 6fd063fa58 | |
Alexis Degrugillier | 0fd608420e | |
acbgbca | 3c4a3617a2 | |
Jan Lukas Gernert | 500d2e4902 | |
maTh | 36e7b694b3 | |
Alexis Degrugillier | ebce6a76c2 | |
Alexis Degrugillier | e10ff9faa4 | |
Edgardo Ramírez | 13e402cfda | |
Gulnur Baimukhambetova | b25a0684c3 | |
Konstantinos Megas | a4c2ce96a8 | |
Alexandre Alapetite | 894bd928d9 | |
acbgbca | eb773c5f65 | |
Amrul Izwan | ca1764f492 | |
Konstantinos Megas | 859aa84c2d | |
Gulnur Baimukhambetova | a62fa0e208 | |
Alexandre Alapetite | 8333dd972b | |
Alexandre Alapetite | ea283a16fc | |
Cilga Iscan Tercanli | 4692242889 | |
Cilga Iscan Tercanli | 4f830e0d71 | |
Alexandre Alapetite | 4881e126ee | |
Thomas Guesnon | 0ff5d0b8cb | |
Alexandre Alapetite | 648a876d77 | |
Alexandre Alapetite | 72265c1eca | |
Alexandre Alapetite | e626fd249e | |
Pyrox | 1e6bc7c21c | |
May | 2dbd11bd2a | |
maTh | 41061c837c | |
maTh | d78c796965 | |
Cyb10101 | 89202922b1 | |
Alexandre Alapetite | 82c4a54fea | |
maTh | 267b55154b | |
maTh | 8f31827e03 | |
Nicolas Pereira | 3f49827292 | |
maTh | 645224a303 | |
maTh | 46d0b4140e | |
maTh | 117b9c115d | |
Alexis Degrugillier | 60e723435e | |
maTh | 01a8c37b83 | |
Alexis Degrugillier | db4c2798ae | |
maTh | a9d4c78931 | |
Edgardo Ramírez | 3b54f8cd81 | |
Nicolas Pereira | c4a0fe364f | |
Edgardo Ramírez | 6405b6d169 | |
maTh | 79e3b99ae3 | |
312k | d84dd549a7 | |
maTh | fc6203a904 | |
maTh | fedbda4f6a | |
maTh | 87082767d8 | |
maTh | ad9329ff47 | |
maTh | 28461df5b8 | |
maTh | dec399f510 | |
maTh | b34bd30cc8 | |
maTh | 2c3900dbdb | |
maTh | 9505eb8445 | |
Alexandre Alapetite | ce5162042b | |
Alexandre Alapetite | 97fc0bc95e | |
Chris Francy | 6dc611db2b | |
Alexandre Alapetite | f56d1274c6 | |
maTh | f941533b3e | |
Alexandre Alapetite | 412b60ca83 | |
berumuron | 6813e16e95 | |
maTh | 6bed64f6f3 | |
maTh | 67ea2d16b7 | |
Alexis Degrugillier | 7de5b93da4 | |
Alexis Degrugillier | 37cf233907 | |
maTh | 3d5b9ac902 | |
maTh | 2734907000 | |
maTh | c1ecb7145c | |
Alexandre Alapetite | 945fde6192 | |
Alexandre Alapetite | 812e65447b | |
Alexandre Alapetite | eb850b7a01 | |
martinrotter | 4c911c67ef | |
martinrotter | f7adc43eb0 | |
Alexandre Alapetite | f1854cd0ae | |
Alexandre Alapetite | 0c472402f2 | |
Alexis Degrugillier | adf1c3bd9a | |
Frans de Jonge | fea12a0957 | |
Alexandre Alapetite | 31d0508b0c | |
Alexandre Alapetite | 3b81708c1a | |
Alexandre Alapetite | 442019a054 | |
Alexandre Alapetite | e2867cfe14 | |
Tealk | fd22ee4090 | |
maTh | 2e0fbf60e7 | |
-Shiken- | d921be63f6 | |
Alexandre Alapetite | 2cc8411cb0 | |
miles | dc3fd52eb6 | |
maTh | e4394ec6da | |
maTh | 8b85fa351a | |
Alexandre Alapetite | 59c2f9a71f | |
Moon Sungjoon | 213f115fe2 | |
maTh | b1a74a3476 | |
Alexandre Alapetite | a8353e4b4c | |
maTh | b5f1c3f29d | |
maTh | 2392a261d2 | |
maTh | de7ae9878c | |
berumuron | a38fe3a15e | |
maTh | 83437c0dd1 | |
Mejans | d5826a0d16 | |
maTh | c418c8a450 | |
Alwaysin | 5c5aaafb0f | |
maTh | 10f29a7362 | |
maTh | 5e3f2a4ad5 | |
Aidi Tan | ef84343576 | |
Alexandre Alapetite | 03f5a42640 | |
maTh | 2b1aa7eede | |
Bartosz Taudul | d815dd8dbe | |
maTh | 7ff6344854 | |
maTh | 34b07a729d | |
maTh | e866fead6c | |
Frans de Jonge | 3fe36e7c8c | |
Alexandre Alapetite | ef40e0659b | |
Matt Sephton | 23bf530639 | |
Tibor Repček | 1b813cfbe3 | |
maTh | bba3306500 | |
Tibor Repček | 51bd9be82f | |
Alexandre Alapetite | 866ee41d2f | |
Alexandre Alapetite | 20338b1f2e | |
Alexandre Alapetite | 1f6eed14da | |
maTh | 4214954ea1 | |
Luc SANCHEZ | 85991d1c5c | |
Alexandre Alapetite | 0ec3874620 | |
Roman D | 0ec65788f9 | |
maTh | 8f475523f5 | |
Aidi Tan | a3a0b2f97a | |
Alexandre Alapetite | 1603c10bba | |
Alexandre Alapetite | 4f111c5b30 | |
Alexandre Alapetite | 2acf3a4dd8 | |
maTh | ea0d924985 | |
Alexandre Alapetite | e27eb1ca91 | |
papaschloss | 8587efa621 | |
Alexandre Alapetite | 839fddaba9 | |
maTh | 36a615beeb | |
Alexandre Alapetite | 96e0efa6f0 | |
maTh | aaf15fba7d | |
Alexandre Alapetite | 09aeeeb325 | |
Alexandre Alapetite | e86c10e2f5 | |
Alexandre Alapetite | 82ac1d1e67 | |
Alexandre Alapetite | 240afa7d4d | |
maTh | 0cb9f59622 | |
maTh | a111f50c69 | |
harshad389 | 9ef849eb27 | |
Alexandre Alapetite | 3b6ff67515 | |
Alexandre Alapetite | 6af7854de3 | |
Alexandre Alapetite | 8bb11bd4f8 | |
maTh | c5ff838730 | |
maTh | 11d1f653d2 | |
maTh | 5177a17c9f | |
Alexandre Alapetite | 18b8e91e3f | |
maTh | 71ff543783 | |
maTh | a534fc79c4 | |
maTh | c596099890 | |
maTh | 4b4390d83a | |
Alexandre Alapetite | 0866fdaee8 | |
maTh | 2d807e06b1 | |
maTh | 6352a1dccb | |
maTh | b2e46d6215 | |
maTh | 954fcef9e2 | |
Duncan Bennie | 670239b94e | |
Tealk | 4f32939ec3 | |
Alexandre Alapetite | 509c8cae63 | |
Joel Garcia | 57d571230e | |
Joel Garcia | c05ab99e82 | |
Alexandre Alapetite | d785ddde2a | |
Alexandre Alapetite | 07a52137a9 | |
Alexandre Alapetite | e14a72681e | |
Alexandre Alapetite | 2a0b47a8a4 | |
Alexandre Alapetite | 47ab9d5e77 | |
Alexandre Alapetite | 5b31c8212f | |
Alexandre Alapetite | dcc77ee343 | |
Alexandre Alapetite | a90d93979f | |
Alexandre Alapetite | f365a9aeb4 | |
Alexandre Alapetite | 238e55b2fd | |
Alexandre Alapetite | 95af935a5f | |
Alexandre Alapetite | a6b4640a2f | |
Alexandre Alapetite | 880f500bb3 | |
ryoku-cha | 7ba3f0228d | |
Alexandre Alapetite | ec11da4e84 | |
Miika Launiainen | a398a135f5 | |
Alexandre Alapetite | f85c510ed4 | |
Alexandre Alapetite | f988b996ab | |
Alexandre Alapetite | 15de58a024 | |
maTh | 992b906062 | |
maTh | 98f9409155 | |
Alexandre Alapetite | 15cdb60ba0 | |
Alexandre Alapetite | b4ff1e03dd | |
Alexandre Alapetite | 516f0c090c | |
Alexandre Alapetite | f89819bd64 | |
Alexandre Alapetite | 8668ca7230 | |
ibiruai | 2ac92b2d7c | |
maTh | f1e9104c2d | |
maTh | c05b31bc90 | |
Alexandre Alapetite | e1f2ba85b1 | |
Alexandre Alapetite | cbf2bc085a | |
Alexandre Alapetite | f5aaf5f460 | |
bulewhale235 | c1eae57898 | |
Alexandre Alapetite | 0fc0060966 | |
Alexandre Alapetite | 20eba951f7 | |
Artur Weigandt | d6fd78b968 | |
Alexandre Alapetite | ffd1061850 | |
maTh | 807ea755e0 | |
Alexandre Alapetite | 0cde4e898f | |
bulewhale235 | 5736ec67c4 | |
Artur Weigandt | 5110d1db3e | |
Alexandre Alapetite | 7f0c378482 | |
Paweł Kalemba | 6e369e83bc | |
Alexandre Alapetite | da0a333b94 | |
Alexandre Alapetite | 4a87206f28 | |
Alexandre Alapetite | 9d1930d9ad | |
Alexandre Alapetite | 893d4d14c0 | |
Alexandre Alapetite | 05d263d67c | |
Alexandre Alapetite | 80a228e9a6 | |
Manuel Riel | d8ccb5800c | |
bluewhale235 | ae097f5bd8 | |
bluewhale235 | 0d29a97ca6 | |
bluewhale235 | 20ee2a8470 | |
Miika Launiainen | 347290aae6 | |
Alexandre Alapetite | 0b86e347ef | |
Clemens Neubauer | 046598e743 | |
Aaron | 55e6b7ea4c | |
maTh | 0bc9ff7300 | |
maTh | 54710c2046 | |
Alexandre Alapetite | 4e16dd1ae5 | |
Alexandre Alapetite | 20e0b848b1 | |
maTh | d350a54513 | |
Alexandre Alapetite | 93b1151484 | |
maTh | c9d0d20ef6 | |
maTh | 34f7558cfb | |
maTh | 578a05e0e3 | |
Alexandre Alapetite | 0698c8a0b2 | |
maTh | 4191f9859e | |
maTh | 5e28bf8b40 | |
drosoCode | 2aba861bc9 | |
Alexandre Alapetite | 191abf5ba5 | |
dependabot[bot] | e79dab6e01 | |
maTh | ab7d9c34ee | |
Alexandre Alapetite | efb57f965a | |
Alexandre Alapetite | 954cc0510d | |
Alexandre Alapetite | 92b0ffe05c | |
Alexandre Alapetite | e8af54a476 | |
Alexandre Alapetite | b0a63355b6 | |
maTh | 7d00ad8ed7 | |
maTh | eabe95e28c | |
maTh | c47bd12b77 | |
Alexandre Alapetite | 4363e13c34 | |
Alexandre Alapetite | 6f018cc674 | |
Alexandre Alapetite | 4d153eeaf8 | |
maTh | a89fce27cb | |
maTh | 9748ac48e4 | |
Alexandre Alapetite | ae54a590b9 | |
maTh | be5848fd4f | |
maTh | bdf7e4d29d | |
maTh | 6650d1d29e | |
maTh | 7b962e246b | |
maTh | d4db9c5a09 | |
maTh | c7790bc59a | |
Alexandre Alapetite | cc6deadf69 | |
Alexandre Alapetite | a9e23bd120 | |
Alexandre Alapetite | d27cf13248 | |
Alexandre Alapetite | dc849c3d87 | |
azlux | da2adaec8a | |
Alexandre Alapetite | 5eaefe0c9c | |
Alexandre Alapetite | 12000df805 | |
Nicolas Ferrari | a6a4e806e4 | |
Alexandre Alapetite | fd945ffb93 | |
Alexandre Alapetite | 467ca9d0de | |
Alexandre Alapetite | 7c74653cc2 | |
Alexandre Alapetite | 1fe66ad020 | |
maTh | fa23ae76ea | |
maTh | 639f8eea84 | |
maTh | c45e5ba85c | |
maTh | 926ce8ff5f | |
maTh | d9e3186471 | |
Alexandre Alapetite | 4b9d66faca | |
maTh | 133e0d61db | |
maTh | 2fd8ce6867 | |
Alexandre Alapetite | 946b0a0876 | |
Alexandre Alapetite | ede82f9819 | |
Alexandre Alapetite | 354f22b4fa | |
Alexandre Alapetite | 7c2da31418 | |
maTh | dfee46792f | |
Alexandre Alapetite | 1c5cf71859 | |
Alexandre Alapetite | fe880d1a98 | |
maTh | cb36fe25a7 | |
maTh | ba1259bb21 | |
Alexandre Alapetite | e28a2e320e | |
maTh | 9224668285 | |
Luc SANCHEZ | 87b181af21 | |
maTh | 4d5f3a20c0 | |
maTh | be9c06fd5c | |
maTh | dc27baa7d6 | |
maTh | 3938492b8a | |
maTh | f8cad8c959 | |
maTh | 827cec6d13 | |
maTh | b127777887 | |
maTh | 8698a0da16 | |
Alexandre Alapetite | 5a891dc0e4 | |
Alexandre Alapetite | 88b934da8b | |
Alexandre Alapetite | 60d96652dd | |
Alexandre Alapetite | 67aea86bae | |
Alexandre Alapetite | 05ca0517bb | |
maTh | 513f7aaa16 | |
maTh | 0ddbc103a1 | |
Alexandre Alapetite | 66912420a1 | |
Alexandre Alapetite | 47e242aa77 | |
Alexandre Alapetite | 24afafb74d | |
Alexandre Alapetite | 8808fb4545 | |
Alexandre Alapetite | 2e805f8c0b | |
Alexandre Alapetite | a568e11142 | |
maTh | cf433d4d79 | |
maTh | 9012db0155 | |
xnaas | 7ab4f89f54 | |
Adam Stephens | 60b856b2d1 | |
berumuron | bc5271b0eb | |
marapavelka | a6b6f31e9e | |
marapavelka | a8225f7244 | |
marapavelka | 525f3b4c83 | |
marapavelka | 56f4bdf8c6 | |
marapavelka | 189b18bdea | |
marapavelka | 48e92e2f53 | |
Frans de Jonge | a2fe354ccb | |
marapavelka | 12665cf27c | |
maTh | 14da55e21e | |
marapavelka | 0c5eaa9d16 | |
maTh | d2b6fe099a | |
maTh | f94ec779e6 | |
maTh | 9cd9e9a093 | |
Alexandre Alapetite | 3502e50cb2 | |
Alexandre Alapetite | 4e2dff4591 | |
Alexandre Alapetite | 9dbbe924c5 | |
Alexandre Alapetite | 1acd3ab09b | |
Thomas Renes | 916df412f5 | |
Alexis Degrugillier | 127b7f0a3a | |
Luc SANCHEZ | ed19445f74 | |
maTh | f8c5df28ab | |
Alexandre Alapetite | 8e398d24f1 | |
Frans de Jonge | d339b6dd45 | |
Alexandre Alapetite | a6ea90e58b | |
Alexandre Alapetite | 1335a0e3cf | |
Alexandre Alapetite | 0988b0c2be |
|
@ -0,0 +1,34 @@
|
|||
FROM alpine:3.18
|
||||
|
||||
ENV TZ UTC
|
||||
SHELL ["/bin/ash", "-eo", "pipefail", "-c"]
|
||||
|
||||
RUN apk add --no-cache \
|
||||
tzdata \
|
||||
apache2 php-apache2 \
|
||||
php php-curl php-gmp php-intl php-mbstring php-xml php-zip \
|
||||
php-ctype php-dom php-fileinfo php-iconv php-json php-opcache php-openssl php-phar php-session php-simplexml php-xmlreader php-xmlwriter php-xml php-tokenizer php-zlib \
|
||||
php-pdo_sqlite php-pdo_mysql php-pdo_pgsql \
|
||||
bash composer curl docker-cli-buildx git gpg make nodejs npm shellcheck shfmt sudo
|
||||
|
||||
RUN rm -f /etc/apache2/conf.d/languages.conf /etc/apache2/conf.d/info.conf \
|
||||
/etc/apache2/conf.d/status.conf /etc/apache2/conf.d/userdir.conf && \
|
||||
sed -r -i "/^\s*LoadModule .*mod_(alias|autoindex|negotiation|status).so$/s/^/#/" \
|
||||
/etc/apache2/httpd.conf && \
|
||||
sed -r -i "/^\s*#\s*LoadModule .*mod_(deflate|expires|headers|mime|remoteip|setenvif).so$/s/^\s*#//" \
|
||||
/etc/apache2/httpd.conf && \
|
||||
sed -r -i "/^\s*(CustomLog|ErrorLog|Listen) /s/^/#/" \
|
||||
/etc/apache2/httpd.conf
|
||||
|
||||
RUN adduser --ingroup www-data --disabled-password developer && \
|
||||
echo "developer ALL=(ALL) NOPASSWD: ALL" > /etc/sudoers.d/developer
|
||||
|
||||
ENV COPY_LOG_TO_SYSLOG On
|
||||
ENV COPY_SYSLOG_TO_STDERR On
|
||||
ENV CRON_MIN ''
|
||||
ENV DATA_PATH ''
|
||||
ENV FRESHRSS_ENV 'development'
|
||||
ENV LISTEN '0.0.0.0:8080'
|
||||
ENV TRUSTED_PROXY 0
|
||||
|
||||
EXPOSE 8080
|
|
@ -0,0 +1,24 @@
|
|||
# Dev Container for FreshRSS
|
||||
|
||||
This is a [Development Container](https://containers.dev) to provide a one-click full development environment
|
||||
with all the needed tools and configurations, to develop and test [FreshRSS](https://github.com/FreshRSS/FreshRSS/).
|
||||
|
||||
It can be used on your local machine (see for instance the [Dev Containers extension for Visual Studio Code](https://marketplace.visualstudio.com/items?itemName=ms-vscode-remote.remote-containers)),
|
||||
or as [GitHub Codespaces](https://github.com/features/codespaces) simply in a Web browser:
|
||||
|
||||
[![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://github.com/codespaces/new?hide_repo_select=true&ref=edge&repo=6322699)
|
||||
|
||||
## Test instance of FreshRSS
|
||||
|
||||
A test instance of FreshRSS is automatically started as visible from the *Ports* tab: check the *Local Address* column, and click on the *Open in browser* 🌐 icon.
|
||||
It runs the FreshRSS code that you are currently editing.
|
||||
|
||||
Apache logs can be seen in `/var/log/apache2/access.log` and `/var/log/apache2/error.log`.
|
||||
|
||||
## Software tests
|
||||
|
||||
Running the tests can be done directly from the built-in terminal, e.g.:
|
||||
|
||||
```sh
|
||||
make test-all
|
||||
```
|
|
@ -0,0 +1,44 @@
|
|||
// For format details, see https://aka.ms/devcontainer.json
|
||||
{
|
||||
"name": "FreshRSS-dev-Alpine",
|
||||
"build": {
|
||||
"dockerfile": "Dockerfile"
|
||||
},
|
||||
"containerEnv": {
|
||||
"DATA_PATH": "/home/developer/freshrss-data"
|
||||
},
|
||||
"customizations": {
|
||||
"codespaces": {
|
||||
"openFiles": [
|
||||
".devcontainer/README.md"
|
||||
]
|
||||
},
|
||||
"vscode": {
|
||||
"extensions": [
|
||||
"bmewburn.vscode-intelephense-client",
|
||||
"DavidAnson.vscode-markdownlint",
|
||||
"dbaeumer.vscode-eslint",
|
||||
"eamodio.gitlens",
|
||||
"EditorConfig.EditorConfig",
|
||||
"foxundermoon.shell-format",
|
||||
"GitHub.vscode-pull-request-github",
|
||||
"mrmlnc.vscode-apache",
|
||||
"ms-azuretools.vscode-docker",
|
||||
"redhat.vscode-yaml",
|
||||
"timonwong.shellcheck",
|
||||
"ValeryanM.vscode-phpsab"
|
||||
]
|
||||
}
|
||||
},
|
||||
"forwardPorts": [
|
||||
8080
|
||||
],
|
||||
"portsAttributes": {
|
||||
"8080": {
|
||||
"label": "FreshRSS Apache",
|
||||
"onAutoForward": "notify"
|
||||
}
|
||||
},
|
||||
"remoteUser": "developer",
|
||||
"postCreateCommand": "sudo .devcontainer/postCreateCommand.sh"
|
||||
}
|
|
@ -0,0 +1,12 @@
|
|||
#!/bin/sh
|
||||
|
||||
ln -s "$(pwd)" /var/www/FreshRSS
|
||||
|
||||
cp ./Docker/*.Apache.conf /etc/apache2/conf.d/
|
||||
|
||||
./Docker/entrypoint.sh
|
||||
|
||||
chown -R developer:www-data /home/developer/freshrss-data
|
||||
chmod -R g+rwX /home/developer/freshrss-data
|
||||
|
||||
httpd -c 'ErrorLog "/var/log/apache2/error.log"' -c 'CustomLog "/var/log/apache2/access.log" combined_proxy'
|
|
@ -1,6 +1,10 @@
|
|||
/.devcontainer/
|
||||
/.git/
|
||||
/.github/
|
||||
/bin/
|
||||
/data/
|
||||
/docs/
|
||||
/extensions/node_modules/
|
||||
/extensions/vendor/
|
||||
/node_modules/
|
||||
/vendor/
|
||||
|
|
|
@ -19,6 +19,12 @@ indent_style = tab
|
|||
indent_size = 4
|
||||
indent_style = tab
|
||||
|
||||
[*.sh]
|
||||
indent_style = tab
|
||||
|
||||
[*.svg]
|
||||
indent_style = tab
|
||||
|
||||
[*.xml]
|
||||
indent_style = tab
|
||||
|
||||
|
|
|
@ -1,4 +1,6 @@
|
|||
.git/
|
||||
*.min.js
|
||||
extensions/
|
||||
node_modules/
|
||||
p/scripts/vendor/
|
||||
vendor/
|
||||
|
|
|
@ -8,7 +8,10 @@
|
|||
],
|
||||
"rules": {
|
||||
"camelcase": "off",
|
||||
"comma-dangle": ["warn", "always-multiline"],
|
||||
"comma-dangle": ["warn", {
|
||||
"arrays": "always-multiline",
|
||||
"objects": "always-multiline"
|
||||
}],
|
||||
"eqeqeq": "off",
|
||||
"indent": ["warn", "tab", { "SwitchCase": 1 }],
|
||||
"linebreak-style": ["error", "unix"],
|
||||
|
|
|
@ -2,3 +2,5 @@
|
|||
*.png -text
|
||||
*.waff -text
|
||||
*.waff2 -text
|
||||
|
||||
/.github/ export-ignore
|
||||
|
|
|
@ -1,36 +0,0 @@
|
|||
---
|
||||
name: Bug report
|
||||
about: Create a report to help us improve
|
||||
title: "[BUG]"
|
||||
labels: ''
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**Describe the bug**
|
||||
A clear and concise description of what the bug is.
|
||||
|
||||
**To Reproduce**
|
||||
Steps to reproduce the behavior:
|
||||
1. Go to '...'
|
||||
2. Click on '....'
|
||||
3. Scroll down to '....'
|
||||
4. See error
|
||||
|
||||
**Expected behavior**
|
||||
A clear and concise description of what you expected to happen.
|
||||
|
||||
**Screenshots**
|
||||
If applicable, add screenshots to help explain your problem.
|
||||
|
||||
**Environment information (please complete the following information):**
|
||||
- Device: [e.g. iPhone6]
|
||||
- OS: [e.g. Ubuntu 20.04]
|
||||
- Browser: [e.g. Firefox 86]
|
||||
- FreshRSS version: [e.g. 1.17.1]
|
||||
- Database version: [e.g. Mysql 5.7]
|
||||
- PHP version: [e.g. PHP 7.4]
|
||||
- Installation type: [e.g. Yunohost]
|
||||
|
||||
**Additional context**
|
||||
Add any other context about the problem here.
|
|
@ -0,0 +1,55 @@
|
|||
name: Bug Report
|
||||
description: Create a report to help us improve
|
||||
title: "[Bug] "
|
||||
labels: ["Bug (unconfirmed)"]
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
Thanks for taking the time to fill out this bug report to help FreshRSS improve!
|
||||
- type: textarea
|
||||
id: description
|
||||
attributes:
|
||||
label: Describe the bug
|
||||
description: "A clear and concise description of what the bug is.\n\nIf applicable, add screenshots to help explain your problem."
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: reproduce
|
||||
attributes:
|
||||
label: To Reproduce
|
||||
description: Steps to reproduce the behavior. (Screenshots could help to explain the steps.)
|
||||
placeholder: "1. Go to '…'\n2. Click on '…'\n3. Scroll down to '…'\n4. See error"
|
||||
value: "1. Go to '…'\n2. Click on '…'\n3. Scroll down to '…'\n4. See error"
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: expected
|
||||
attributes:
|
||||
label: Expected behavior
|
||||
description: A clear and concise description of what you expected to happen.
|
||||
validations:
|
||||
required: false
|
||||
- type: input
|
||||
id: freshRSS
|
||||
attributes:
|
||||
label: FreshRSS version
|
||||
description: Which FreshRSS version is installed?
|
||||
placeholder: e.g. 1.23.1
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: environment
|
||||
attributes:
|
||||
label: Environment information
|
||||
description: Please complete the following information
|
||||
value: "- Database version: [e.g. Mysql 5.7, SQLite]\n- PHP version: [e.g. PHP 8.1]\n- Installation type: [e.g. Docker, Docker image source, git, Yunohost]\n-Web server type: [e.g. Apache, nginx]\n- Device: [e.g. iPhone13]\n- OS: [e.g. Ubuntu 22.04, Win10, MacOS14]\n- Browser: [e.g. Firefox 124]"
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: context
|
||||
attributes:
|
||||
label: Additional context
|
||||
description: Add any other context about the problem here.
|
||||
validations:
|
||||
required: false
|
|
@ -8,13 +8,13 @@ assignees: ''
|
|||
---
|
||||
|
||||
**Is your feature request related to a problem? Please describe.**
|
||||
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
|
||||
A clear and concise description of what the problem is. Ex. I’m always frustrated when […]
|
||||
|
||||
**Describe the solution you'd like**
|
||||
**Describe the solution you’d like**
|
||||
A clear and concise description of what you want to happen.
|
||||
|
||||
**Describe alternatives you've considered**
|
||||
A clear and concise description of any alternative solutions or features you've considered.
|
||||
**Describe alternatives you’ve considered**
|
||||
A clear and concise description of any alternative solutions or features you’ve considered.
|
||||
|
||||
**Additional context**
|
||||
Add any other context or screenshots about the feature request here.
|
||||
|
|
|
@ -0,0 +1,27 @@
|
|||
# https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates
|
||||
|
||||
version: 2
|
||||
updates:
|
||||
- package-ecosystem: "github-actions"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "monthly"
|
||||
- package-ecosystem: "npm"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "monthly"
|
||||
groups:
|
||||
eslint:
|
||||
patterns:
|
||||
- "eslint*"
|
||||
stylelint:
|
||||
patterns:
|
||||
- "stylelint*"
|
||||
- package-ecosystem: "composer"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "monthly"
|
||||
- package-ecosystem: "composer"
|
||||
directory: "/lib/"
|
||||
schedule:
|
||||
interval: "monthly"
|
|
@ -0,0 +1,93 @@
|
|||
name: Publish Docker images
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- edge
|
||||
release:
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
# packages: write
|
||||
|
||||
jobs:
|
||||
build-container-image:
|
||||
name: Build Docker image ${{ matrix.name }}
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
include:
|
||||
- name: Debian
|
||||
file: Docker/Dockerfile
|
||||
flavor: |
|
||||
latest=auto
|
||||
tags: |
|
||||
type=edge,onlatest=false
|
||||
type=semver,pattern={{version}}
|
||||
type=semver,pattern={{major}},enable=${{ !startsWith(github.ref, 'refs/tags/0.') }}
|
||||
# type=semver,pattern={{major}}.{{minor}}
|
||||
- name: Alpine
|
||||
file: Docker/Dockerfile-Alpine
|
||||
flavor: |
|
||||
latest=false
|
||||
tags: |
|
||||
type=raw,value=alpine,enable=${{ github.ref == 'refs/heads/latest' || startsWith(github.ref, 'refs/tags/') }}
|
||||
type=edge,suffix=-alpine,onlatest=false
|
||||
type=semver,pattern={{version}}-alpine
|
||||
type=semver,pattern={{major}}-alpine,enable=${{ !startsWith(github.ref, 'refs/tags/0.') }}
|
||||
# type=semver,pattern={{major}}.{{minor}}-alpine
|
||||
steps:
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Get FreshRSS version
|
||||
run: |
|
||||
FRESHRSS_VERSION=$(sed -n "s/^const FRESHRSS_VERSION = '\(.*\)'.*$/\1/p" constants.php)
|
||||
echo "$FRESHRSS_VERSION"
|
||||
echo "FRESHRSS_VERSION=$FRESHRSS_VERSION" >> $GITHUB_ENV
|
||||
|
||||
- name: Add metadata to Docker images
|
||||
id: meta
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
flavor: ${{ matrix.flavor }}
|
||||
images: |
|
||||
docker.io/freshrss/freshrss
|
||||
# ghcr.io/${{ github.repository }}
|
||||
tags: ${{ matrix.tags }}
|
||||
labels: |
|
||||
org.opencontainers.image.url=https://freshrss.org/
|
||||
org.opencontainers.image.version=${{ env.FRESHRSS_VERSION }}
|
||||
|
||||
- name: Login to Docker Hub
|
||||
if: github.repository_owner == 'FreshRSS'
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
# - name: Login to GitHub Container Registry
|
||||
# uses: docker/login-action@v3
|
||||
# with:
|
||||
# registry: ghcr.io
|
||||
# username: ${{ github.repository_owner }}
|
||||
# password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Build and push Docker images
|
||||
uses: docker/build-push-action@v5
|
||||
with:
|
||||
file: ${{ matrix.file }}
|
||||
platforms: linux/amd64,linux/arm/v7,linux/arm64
|
||||
build-args: |
|
||||
FRESHRSS_VERSION=${{ env.FRESHRSS_VERSION }}
|
||||
SOURCE_COMMIT=${{ github.sha }}
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
push: ${{ (github.ref == 'refs/heads/latest' || github.ref == 'refs/heads/edge' || startsWith(github.ref, 'refs/tags/')) && github.repository_owner == 'FreshRSS' }}
|
|
@ -0,0 +1,24 @@
|
|||
name: Update Docker Hub description
|
||||
|
||||
on:
|
||||
push:
|
||||
paths:
|
||||
- Docker/README.md
|
||||
branches:
|
||||
- edge
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
dockerhub-description:
|
||||
if: github.repository_owner == 'FreshRSS'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Update repo description
|
||||
uses: peter-evans/dockerhub-description@e98e4d1628a5f3be2be7c231e50981aee98723ae
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
repository: freshrss/freshrss
|
||||
readme-filepath: Docker/README.md
|
|
@ -0,0 +1,51 @@
|
|||
# Workflow for building and deploying a Jekyll site to GitHub Pages
|
||||
name: Deploy Jekyll with GitHub Pages dependencies preinstalled
|
||||
|
||||
on:
|
||||
# Runs on pushes targeting the default branch
|
||||
push:
|
||||
branches: ["edge"]
|
||||
|
||||
# Allows you to run this workflow manually from the Actions tab
|
||||
workflow_dispatch:
|
||||
|
||||
# Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages
|
||||
permissions:
|
||||
contents: read
|
||||
pages: write
|
||||
id-token: write
|
||||
|
||||
# Allow only one concurrent deployment, skipping runs queued between the run in-progress and latest queued.
|
||||
# However, do NOT cancel in-progress runs as we want to allow these production deployments to complete.
|
||||
concurrency:
|
||||
group: "pages"
|
||||
cancel-in-progress: false
|
||||
|
||||
jobs:
|
||||
# Build job
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
- name: Setup Pages
|
||||
uses: actions/configure-pages@v5
|
||||
- name: Build with Jekyll
|
||||
uses: actions/jekyll-build-pages@v1
|
||||
with:
|
||||
source: ./docs/
|
||||
destination: ./_site
|
||||
- name: Upload artifact
|
||||
uses: actions/upload-pages-artifact@v3
|
||||
|
||||
# Deployment job
|
||||
deploy:
|
||||
environment:
|
||||
name: github-pages
|
||||
url: ${{ steps.deployment.outputs.page_url }}
|
||||
runs-on: ubuntu-latest
|
||||
needs: build
|
||||
steps:
|
||||
- name: Deploy to GitHub Pages
|
||||
id: deployment
|
||||
uses: actions/deploy-pages@v4
|
|
@ -1,12 +0,0 @@
|
|||
name: Add latest tag to new release
|
||||
on:
|
||||
release:
|
||||
types: [published]
|
||||
|
||||
jobs:
|
||||
run:
|
||||
name: Run local action
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Run latest-tag
|
||||
uses: EndBug/latest-tag@6d22a6738f5c33059e3a8c6ca5dcf8eaf8a14599
|
|
@ -10,11 +10,11 @@ jobs:
|
|||
|
||||
tests:
|
||||
# https://github.com/actions/virtual-environments
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ubuntu-22.04
|
||||
|
||||
steps:
|
||||
- name: Git checkout source code
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v4
|
||||
|
||||
# Composer tests
|
||||
|
||||
|
@ -29,7 +29,7 @@ jobs:
|
|||
|
||||
- name: Use Composer cache
|
||||
id: composer-cache
|
||||
uses: actions/cache@v2
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: vendor
|
||||
key: ${{ runner.os }}-php-${{ hashFiles('**/composer.lock') }}
|
||||
|
@ -49,16 +49,19 @@ jobs:
|
|||
- name: PHPStan
|
||||
run: composer run-script phpstan
|
||||
|
||||
- name: PHPStan Next Level
|
||||
run: composer run-script phpstan-next
|
||||
|
||||
# NPM tests
|
||||
|
||||
- name: Uses Node.js
|
||||
uses: actions/setup-node@v2
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
# https://nodejs.org/en/about/releases/
|
||||
node-version: '14'
|
||||
# https://nodejs.org/en/about/previous-releases
|
||||
node-version: '20'
|
||||
cache: 'npm'
|
||||
|
||||
- run: npm install
|
||||
- run: npm ci
|
||||
|
||||
- name: Check JavaScript syntax
|
||||
run: npm run --silent eslint
|
||||
|
@ -66,43 +69,48 @@ jobs:
|
|||
- name: Check Markdown syntax
|
||||
run: npm run --silent markdownlint
|
||||
|
||||
- name: Check Right-to-left CSS
|
||||
run: npm run --silent rtlcss && git diff --exit-code
|
||||
|
||||
- name: Check CSS syntax
|
||||
run: npm run --silent stylelint
|
||||
|
||||
- name: Check Right-to-left CSS
|
||||
run: npm run --silent rtlcss && git diff --exit-code
|
||||
|
||||
# Shell tests
|
||||
|
||||
- name: Use shell cache
|
||||
id: shell-cache
|
||||
uses: actions/cache@v2
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: bin
|
||||
key: ${{ runner.os }}-bin-shfmt@v3.4.0c-hadolint@v2.7.0
|
||||
key: ${{ runner.os }}-bin-shfmt@v3.7.0-hadolint@v2.12.0-typos@v1.17.0
|
||||
|
||||
- name: Add ./bin/ to $PATH
|
||||
run: mkdir -p bin/ && echo "${PWD}/bin" >> $GITHUB_PATH
|
||||
|
||||
- name: Setup Go
|
||||
if: steps.shell-cache.outputs.cache-hit != 'true'
|
||||
# Multiple Go versions are pre-installed but the default 1.15 is too old
|
||||
# https://github.com/actions/setup-go
|
||||
uses: actions/setup-go@v2
|
||||
with:
|
||||
go-version: '1.17'
|
||||
|
||||
- name: Install shfmt
|
||||
if: steps.shell-cache.outputs.cache-hit != 'true'
|
||||
run: GOBIN=${PWD}/bin/ go install mvdan.cc/sh/v3/cmd/shfmt@v3.4.0
|
||||
run: GOBIN=${PWD}/bin/ go install mvdan.cc/sh/v3/cmd/shfmt@v3.7.0
|
||||
|
||||
- name: Check shell script syntax
|
||||
# shellcheck is pre-installed https://github.com/actions/virtual-environments/blob/main/images/linux/Ubuntu2004-README.md
|
||||
# shellcheck is pre-installed https://github.com/actions/virtual-environments/blob/main/images/linux/Ubuntu2204-Readme.md
|
||||
run: ./tests/shellchecks.sh
|
||||
|
||||
- name: Install hadolint
|
||||
if: steps.shell-cache.outputs.cache-hit != 'true'
|
||||
run: curl -sL -o ./bin/hadolint "https://github.com/hadolint/hadolint/releases/download/v2.7.0/hadolint-$(uname -s)-$(uname -m)" && chmod 700 ./bin/hadolint
|
||||
run: curl -sL -o ./bin/hadolint "https://github.com/hadolint/hadolint/releases/download/v2.12.0/hadolint-$(uname -s)-$(uname -m)" && chmod 700 ./bin/hadolint
|
||||
|
||||
- name: Check Dockerfile syntax
|
||||
run: find . -name 'Dockerfile*' -print0 | xargs -0 -n1 ./bin/hadolint --failure-threshold warning
|
||||
|
||||
- name: Install typos
|
||||
if: steps.shell-cache.outputs.cache-hit != 'true'
|
||||
run: |
|
||||
cd bin ;
|
||||
wget -q 'https://github.com/crate-ci/typos/releases/download/v1.17.0/typos-v1.17.0-x86_64-unknown-linux-musl.tar.gz' &&
|
||||
tar -xvf *.tar.gz './typos' &&
|
||||
chmod +x typos &&
|
||||
rm *.tar.gz ;
|
||||
cd ..
|
||||
|
||||
- name: Check spelling
|
||||
run: bin/typos
|
||||
|
|
|
@ -1,8 +1,13 @@
|
|||
/bin/
|
||||
/extensions/node_modules/
|
||||
/extensions/vendor/
|
||||
/node_modules/
|
||||
/vendor/
|
||||
/data.back/
|
||||
/constants.local.php
|
||||
|
||||
.vscode/
|
||||
|
||||
# Temp files
|
||||
*~
|
||||
*.bak
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
.git/
|
||||
extensions/
|
||||
node_modules/
|
||||
p/scripts/bcrypt.min.js
|
||||
p/scripts/vendor/
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
"line-length": false,
|
||||
"no-hard-tabs": false,
|
||||
"no-inline-html": {
|
||||
"allowed_elements": ["br", "kbd"]
|
||||
"allowed_elements": ["br", "img", "kbd"]
|
||||
},
|
||||
"no-multiple-blanks": {
|
||||
"maximum": 2
|
||||
|
|
|
@ -1,3 +1,8 @@
|
|||
.git/
|
||||
extensions/
|
||||
lib/marienfressinaud/
|
||||
lib/phpgt/
|
||||
lib/phpmailer/
|
||||
node_modules/
|
||||
p/scripts/vendor/
|
||||
vendor/
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
.git/
|
||||
extensions/
|
||||
node_modules/
|
||||
p/scripts/vendor/
|
||||
vendor/
|
||||
|
|
|
@ -2,7 +2,8 @@
|
|||
"extends": "stylelint-config-recommended-scss",
|
||||
"plugins": [
|
||||
"stylelint-order",
|
||||
"stylelint-scss"
|
||||
"stylelint-scss",
|
||||
"@stylistic/stylelint-plugin"
|
||||
],
|
||||
"rules": {
|
||||
"at-rule-empty-line-before": [
|
||||
|
@ -10,27 +11,27 @@
|
|||
"ignoreAtRules": [ "after-comment", "else" ]
|
||||
}
|
||||
],
|
||||
"at-rule-name-space-after": [
|
||||
"@stylistic/at-rule-name-space-after": [
|
||||
"always", {
|
||||
"ignoreAtRules": [ "after-comment" ]
|
||||
}
|
||||
],
|
||||
"block-closing-brace-newline-after": [
|
||||
"@stylistic/block-closing-brace-newline-after": [
|
||||
"always", {
|
||||
"ignoreAtRules": [ "if", "else" ]
|
||||
}
|
||||
],
|
||||
"block-closing-brace-newline-before": "always-multi-line",
|
||||
"block-opening-brace-newline-after": "always-multi-line",
|
||||
"block-opening-brace-space-before": "always",
|
||||
"color-hex-case": "lower",
|
||||
"@stylistic/block-closing-brace-newline-before": "always-multi-line",
|
||||
"@stylistic/block-opening-brace-newline-after": "always-multi-line",
|
||||
"@stylistic/block-opening-brace-space-before": "always",
|
||||
"@stylistic/color-hex-case": "lower",
|
||||
"color-hex-length": "short",
|
||||
"color-no-invalid-hex": true,
|
||||
"declaration-colon-space-after": "always",
|
||||
"declaration-colon-space-before": "never",
|
||||
"indentation": "tab",
|
||||
"@stylistic/declaration-colon-space-after": "always",
|
||||
"@stylistic/declaration-colon-space-before": "never",
|
||||
"@stylistic/indentation": "tab",
|
||||
"no-descending-specificity": null,
|
||||
"no-eol-whitespace": true,
|
||||
"@stylistic/no-eol-whitespace": true,
|
||||
"property-no-vendor-prefix": true,
|
||||
"rule-empty-line-before": [
|
||||
"always", {
|
||||
|
|
|
@ -0,0 +1,52 @@
|
|||
[default.extend-identifiers]
|
||||
ot = "ot"
|
||||
Ths2 = "Ths2"
|
||||
|
||||
[default.extend-words]
|
||||
referer = "referer"
|
||||
|
||||
[files]
|
||||
extend-exclude = [
|
||||
"*.fr.md",
|
||||
"*.map",
|
||||
"*.min.js",
|
||||
"*.po",
|
||||
"*.pot",
|
||||
"*.rtl.css",
|
||||
".git/",
|
||||
"app/i18n/cz/",
|
||||
"app/i18n/de/",
|
||||
"app/i18n/el/",
|
||||
"app/i18n/es/",
|
||||
"app/i18n/fa/",
|
||||
"app/i18n/fr/",
|
||||
"app/i18n/he/",
|
||||
"app/i18n/hu/",
|
||||
"app/i18n/id/",
|
||||
"app/i18n/it/",
|
||||
"app/i18n/ja/",
|
||||
"app/i18n/ko/",
|
||||
"app/i18n/lv/",
|
||||
"app/i18n/nl/",
|
||||
"app/i18n/oc/",
|
||||
"app/i18n/pl/",
|
||||
"app/i18n/pt-br/",
|
||||
"app/i18n/ru/",
|
||||
"app/i18n/sk/",
|
||||
"app/i18n/tr/",
|
||||
"app/i18n/zh-cn/",
|
||||
"app/i18n/zh-tw/",
|
||||
"bin/",
|
||||
"CHANGELOG-old.md",
|
||||
"composer.lock",
|
||||
"data/",
|
||||
"docs/fr/",
|
||||
"lib/marienfressinaud/",
|
||||
"lib/phpgt/",
|
||||
"lib/phpmailer/",
|
||||
"lib/SimplePie/",
|
||||
"node_modules/",
|
||||
"p/scripts/vendor/",
|
||||
"package-lock.json",
|
||||
"vendor/",
|
||||
]
|
|
@ -0,0 +1,242 @@
|
|||
# Journal des modifications de FreshRSS
|
||||
|
||||
[Voir les changements plus récents (en anglais)](./CHANGELOG.md)
|
||||
|
||||
## 2014-02-19 FreshRSS 0.7.1
|
||||
|
||||
* Mise à jour des flux plus rapide grâce à une meilleure utilisation du cache
|
||||
* Utilisation d’une signature MD5 du contenu intéressant pour les flux n’implémentant pas les requêtes conditionnelles
|
||||
* Modification des raccourcis
|
||||
* "s" partage directement si un seul moyen de partage
|
||||
* Moyens de partage accessibles par "1", "2", "3", etc.
|
||||
* Premier article : Home ; Dernier article : End
|
||||
* Ajout du déplacement au sein des catégories / flux (via modificateurs shift et alt)
|
||||
* UI
|
||||
* Séparation des descriptions des raccourcis par groupes
|
||||
* Revue rapide de la page de connexion
|
||||
* Amélioration de l’affichage des notifications sur mobile
|
||||
* Revue du système de rafraîchissement des flux
|
||||
* Meilleure gestion de la file de flux à rafraîchir en JSON
|
||||
* Rafraîchissement uniquement pour les flux non rafraîchis récemment
|
||||
* Possibilité donnée aux anonymes de rafraîchir les flux
|
||||
* SimplePie
|
||||
* Mise à jour de la lib
|
||||
* Corrige fuite de mémoire
|
||||
* Meilleure tolérance aux flux invalides
|
||||
* Corrections divers
|
||||
* Ne déplie plus l’article lors du clic sur l’icône lien externe
|
||||
* Ne boucle plus à la fin de la navigation dans les articles
|
||||
* Suppression du champ category.color inutile
|
||||
* Corrige bug redirection infinie (Persona)
|
||||
* Amélioration vérification de la requête POST
|
||||
* Ajout d’un verrou lorsqu’une action mark_read ou mark_favorite est en cours
|
||||
|
||||
|
||||
## 2014-01-29 FreshRSS 0.7
|
||||
|
||||
* Nouveau mode multi-utilisateur
|
||||
* L’utilisateur par défaut (administrateur) peut créer et supprimer d’autres utilisateurs
|
||||
* Nécessite un contrôle d’accès, soit :
|
||||
* par le nouveau mode de connexion par formulaire (nom d’utilisateur + mot de passe)
|
||||
* relativement sûr même sans HTTPS (le mot de passe n’est pas transmis en clair)
|
||||
* requiert JavaScript et PHP 5.3+
|
||||
* par HTTP (par exemple sous Apache en créant un fichier ./p/i/.htaccess et .htpasswd)
|
||||
* le nom d’utilisateur HTTP doit correspondre au nom d’utilisateur FreshRSS
|
||||
* par Mozilla Persona, en renseignant l’adresse courriel des utilisateurs
|
||||
* Installateur supportant les mises à jour :
|
||||
* Depuis une v0.6, placer application.ini et Configuration.array.php dans le nouveau répertoire “./data/”
|
||||
(voir réorganisation ci-dessous)
|
||||
* Pour les versions suivantes, juste garder le répertoire “./data/”
|
||||
* Rafraîchissement automatique du nombre d’articles non lus toutes les deux minutes (utilise le cache HTTP à bon escient)
|
||||
* Permet aussi de conserver la session valide, surtout dans le cas de Persona
|
||||
* Nouvelle page de statistiques (nombres d’articles par jour / catégorie)
|
||||
* Importation OPML instantanée et plus tolérante
|
||||
* Nouvelle gestion des favicons avec téléchargement en parallèle
|
||||
* Nouvelles options
|
||||
* Réorganisation des options
|
||||
* Gestion des utilisateurs
|
||||
* Améliorations partage vers Shaarli, Poche, Diaspora*, Facebook, Twitter, Google+, courriel
|
||||
* Raccourci ‘s’ par défaut
|
||||
* Permet la suppression de tous les articles d’un flux
|
||||
* Option pour marquer les articles comme lus dès la réception
|
||||
* Permet de configurer plus finement le nombre d’articles minimum à conserver par flux
|
||||
* Permet de modifier la description et l’adresse d’un flux RSS ainsi que le site Web associé
|
||||
* Nouveau raccourci pour ouvrir/fermer un article (‘c’ par défaut)
|
||||
* Boutons pour effacer les logs et pour purger les vieux articles
|
||||
* Nouveaux filtres d’affichage : seulement les articles favoris, et seulement les articles lus
|
||||
* SQL :
|
||||
* Nouveau moteur de recherche, aussi accessible depuis la vue mobile
|
||||
* Mots clefs de recherche “intitle:”, “inurl:”, “author:”
|
||||
* Les articles sont triés selon la date de leur ajout dans FreshRSS plutôt que la date déclarée (souvent erronée)
|
||||
* Permet de marquer tout comme lu sans affecter les nouveaux articles arrivés en cours de lecture
|
||||
* Permet une pagination efficace
|
||||
* Refactorisation
|
||||
* Les tables sont préfixées avec le nom d’utilisateur afin de permettre le mode multi-utilisateurs
|
||||
* Amélioration des performances
|
||||
* Tolère un beaucoup plus grand nombre d’articles
|
||||
* Compression des données côté MySQL plutôt que côté PHP
|
||||
* Incompatible avec la version 0.6 (nécessite une mise à jour grâce à l’installateur)
|
||||
* Affichage de la taille de la base de données dans FreshRSS
|
||||
* Correction problème de marquage de tous les favoris comme lus
|
||||
* HTML5 :
|
||||
* Support des balises HTML5 audio, video, et éléments associés
|
||||
* Utilisation de preload="none", et réécriture correcte des adresses, aussi en HTTPS
|
||||
* Protection HTML5 des iframe (sandbox="allow-scripts allow-same-origin")
|
||||
* Filtrage des object et embed
|
||||
* Chargement différé HTML5 (postpone="") pour iframe et video
|
||||
* Chargement différé JavaScript pour iframe
|
||||
* CSS :
|
||||
* Nouveau thème sombre
|
||||
* Chargement plus robuste des thèmes
|
||||
* Meilleur support des longs titres d’articles sur des écrans étroits
|
||||
* Meilleure accessibilité
|
||||
* FreshRSS fonctionne aussi en mode dégradé sans images (alternatives Unicode) et/ou sans CSS
|
||||
* Diverses améliorations
|
||||
* PHP :
|
||||
* Encore plus tolérant pour les flux comportant des erreurs
|
||||
* Mise à jour automatique de l’URL du flux (en base de données) lorsque SimplePie découvre qu’elle a changé
|
||||
* Meilleure gestion des caractères spéciaux dans différents cas
|
||||
* Compatibilité PHP 5.5+ avec OPcache
|
||||
* Amélioration des performances
|
||||
* Chargement automatique des classes
|
||||
* Alternative dans le cas d’absence de librairie JSON
|
||||
* Pour le développement, le cache HTTP peut être désactivé en créant un fichier “./data/no-cache.txt”
|
||||
* Réorganisation des fichiers et répertoires, en particulier :
|
||||
* Tous les fichiers utilisateur sont dans “./data/” (y compris “cache”, “favicons”, et “log”)
|
||||
* Déplacement de “./app/configuration/application.ini” vers “./data/config.php”
|
||||
* Meilleure sécurité et compatibilité
|
||||
* Déplacement de “./public/data/Configuration.array.php” vers “./data/*_user.php”
|
||||
* Déplacement de “./public/” vers “./p/”
|
||||
* Déplacement de “./public/index.php” vers “./p/i/index.php” (voir cookie ci-dessous)
|
||||
* Déplacement de “./actualize_script.php” vers “./app/actualize_script.php” (pour une meilleure sécurité)
|
||||
* Pensez à mettre à jour votre Cron !
|
||||
* Divers :
|
||||
* Nouvelle politique de cookie de session (témoin de connexion)
|
||||
* Utilise un nom poli “FreshRSS” (évite des problèmes avec certains filtres)
|
||||
* Se limite au répertoire “./FreshRSS/p/i/” pour de meilleures performances HTTP
|
||||
* Les images, CSS, scripts sont servis sans cookie
|
||||
* Utilise “HttpOnly” pour plus de sécurité
|
||||
* Nouvel “agent utilisateur” exposé lors du téléchargement des flux, par exemple :
|
||||
* `FreshRSS/0.7 (Linux; http://freshrss.org) SimplePie/1.3.1`
|
||||
* Script d’actualisation avec plus de messages
|
||||
* Sur la sortie standard, ainsi que dans le log système (syslog)
|
||||
* Affichage du numéro de version dans “À propos”
|
||||
|
||||
|
||||
## 2013-11-21 FreshRSS 0.6.1
|
||||
|
||||
* Corrige bug chargement du JavaScript
|
||||
* Affiche un message d’erreur plus explicite si fichier de configuration inaccessible
|
||||
|
||||
|
||||
## 2013-11-17 FreshRSS 0.6
|
||||
|
||||
* Nettoyage du code JavaScript + optimisations
|
||||
* Utilisation d’adresses relatives
|
||||
* Amélioration des performances coté client
|
||||
* Mise à jour automatique du nombre d’articles non lus
|
||||
* Corrections traductions
|
||||
* Mise en cache de FreshRSS
|
||||
* Amélioration des retours utilisateur lorsque la configuration n’est pas bonne
|
||||
* Actualisation des flux après une importation OPML
|
||||
* Meilleure prise en charge des flux RSS invalides
|
||||
* Amélioration de la vue globale
|
||||
* Possibilité de personnaliser les icônes de lecture
|
||||
* Suppression de champs lors de l’installation (base_url et sel)
|
||||
* Correction de bugs divers
|
||||
|
||||
|
||||
## 2013-10-15 FreshRSS 0.5.1
|
||||
|
||||
* Correction du bug des catégories disparues
|
||||
* Correction traduction i18n/fr et i18n/en
|
||||
* Suppression de certains appels à la feuille de style fallback.css
|
||||
|
||||
|
||||
## 2013-10-12 FreshRSS 0.5.0
|
||||
|
||||
* Possibilité d’interdire la lecture anonyme
|
||||
* Option pour garder l’historique d’un flux
|
||||
* Lors d’un clic sur “Marquer tous les articles comme lus”, FreshRSS peut désormais sauter à la prochaine catégorie / prochain flux avec des articles non lus.
|
||||
* Ajout d’un token pour accéder aux flux RSS générés par FreshRSS sans nécessiter de connexion
|
||||
* Possibilité de partager vers Facebook, Twitter et Google+
|
||||
* Possibilité de changer de thème
|
||||
* Le menu de navigation (article précédent / suivant / haut de page) a été ajouté à la vue non mobile
|
||||
* La police OpenSans est désormais appliquée
|
||||
* Amélioration de la page de configuration
|
||||
* Une meilleure sortie pour l’imprimante
|
||||
* Quelques retouches du design par défaut
|
||||
* Les vidéos ne dépassent plus du cadre de l’écran
|
||||
* Nouveau logo
|
||||
* Possibilité d’ajouter un préfixe aux tables lors de l’installation
|
||||
* Ajout d’un champ en base de données keep_history à la table feed
|
||||
* Si possible, création automatique de la base de données si elle n’existe pas lors de l’installation
|
||||
* L’utilisation d’UTF-8 est forcée
|
||||
* Le marquage automatique au défilement de la page a été amélioré
|
||||
* La vue globale a été énormément améliorée et est beaucoup plus utile
|
||||
* Amélioration des requêtes SQL
|
||||
* Amélioration du JavaScript
|
||||
* Correction bugs divers
|
||||
|
||||
|
||||
## 2013-07-02 FreshRSS 0.4.0
|
||||
|
||||
* Correction bug et ajout notification lors de la phase d’installation
|
||||
* Affichage d’erreur si fichier OPML invalide
|
||||
* Les tags sont maintenant cliquables pour filtrer dessus
|
||||
* Amélioration vue mobile (boutons plus gros et ajout d’une barre de navigation)
|
||||
* Possibilité d’ajouter directement un flux dans une catégorie dès son ajout
|
||||
* Affichage des flux en erreur (injoignable par exemple) en rouge pour les différencier
|
||||
* Possibilité de changer les noms des flux
|
||||
* Ajout d’une option (désactivable donc) pour charger les images en lazyload permettant de ne pas charger toutes les images d’un coup
|
||||
* Le framework Minz est maintenant directement inclus dans l’archive (plus besoin de passer par ./build.sh)
|
||||
* Amélioration des performances pour la récupération des flux tronqués
|
||||
* Possibilité d’importer des flux sans catégorie lors de l’import OPML
|
||||
* Suppression de “l’API” (qui était de toute façon très basique) et de la fonctionnalité de “notes”
|
||||
* Amélioration de la recherche (garde en mémoire si l’on a sélectionné une catégorie) par exemple
|
||||
* Modification apparence des balises hr et pre
|
||||
* Meilleure vérification des champs de formulaire
|
||||
* Remise en place du mode “endless” (permettant de simplement charger les articles qui suivent plutôt que de charger une nouvelle page)
|
||||
* Ajout d’une page de visualisation des logs
|
||||
* Ajout d’une option pour optimiser la BDD (diminue sa taille)
|
||||
* Ajout des vues lecture et globale (assez basique)
|
||||
* Les vidéos YouTube ne débordent plus du cadre sur les petits écrans
|
||||
* Ajout d’une option pour marquer les articles comme lus lors du défilement (et suppression de celle au chargement de la page)
|
||||
|
||||
|
||||
## 2013-05-05 FreshRSS 0.3.0
|
||||
|
||||
* Fallback pour les icônes SVG (utilisation de PNG à la place)
|
||||
* Fallback pour les propriétés CSS3 (utilisation de préfixes)
|
||||
* Affichage des tags associés aux articles
|
||||
* Internationalisation de l’application (gestion des langues anglaise et française)
|
||||
* Gestion des flux protégés par authentification HTTP
|
||||
* Mise en cache des favicons
|
||||
* Création d’un logo *temporaire*
|
||||
* Affichage des vidéos dans les articles
|
||||
* Gestion de la recherche et filtre par tags pleinement fonctionnels
|
||||
* Création d’un vrai script CRON permettant de mettre tous les flux à jour
|
||||
* Correction bugs divers
|
||||
|
||||
|
||||
## 2013-04-17 FreshRSS 0.2.0
|
||||
|
||||
* Création d’un installateur
|
||||
* Actualisation des flux en Ajax
|
||||
* Partage par mail et Shaarli ajouté
|
||||
* Export par flux RSS
|
||||
* Possibilité de vider une catégorie
|
||||
* Possibilité de sélectionner les catégories en vue mobile
|
||||
* Les flux peuvent être sortis du flux principal (système de priorité)
|
||||
* Amélioration ajout / import / export des flux
|
||||
* Amélioration actualisation (meilleure gestion des erreurs)
|
||||
* Améliorations du CSS
|
||||
* Changements dans la base de données
|
||||
* MàJ de la librairie SimplePie
|
||||
* Flux sans auteurs gérés normalement
|
||||
* Correction bugs divers
|
||||
|
||||
|
||||
## 2013-04-08 FreshRSS 0.1.0
|
||||
|
||||
* “Première” version
|
963
CHANGELOG.md
963
CHANGELOG.md
File diff suppressed because it is too large
Load Diff
|
@ -31,15 +31,15 @@ Did you want to fix a bug? To keep a great coordination between collaborators, y
|
|||
1. Be sure the bug is associated to a ticket and say you work on it.
|
||||
2. [Fork this project repository](https://help.github.com/articles/fork-a-repo/).
|
||||
3. [Create a new branch](https://help.github.com/articles/creating-and-deleting-branches-within-your-repository/). The name of the branch must be explicit and being prefixed by the related ticket id. For instance, `783-contributing-file` to fix [ticket #783](https://github.com/FreshRSS/FreshRSS/issues/783).
|
||||
4. Make your changes to your fork and [send a pull request](https://help.github.com/articles/using-pull-requests/) on the **edge branch**. Don't forget to add your name to CREDITS.md if you're contributing to FreshRSS for the very first time.
|
||||
4. Make your changes to your fork and [send a pull request](https://help.github.com/articles/using-pull-requests/) on the **edge branch**. Don’t forget to add your name to `CREDITS.md` if you’re contributing to FreshRSS for the very first time.
|
||||
|
||||
If you have to write code, please follow [our coding style recommendations](https://freshrss.github.io/FreshRSS/en/developers/01_First_steps.html).
|
||||
If you have to write code, please follow [our coding style recommendations](https://freshrss.github.io/FreshRSS/en/developers/02_First_steps.html).
|
||||
|
||||
**Tip:** if you are searching for bugs easy to fix, have a look at the « [Good first issue](https://github.com/FreshRSS/FreshRSS/issues?q=label%3A%22good+first+issue+%3Ababy%3A%22) » and/or « [Help wanted](https://github.com/FreshRSS/FreshRSS/issues?q=label%3A%22help+wanted+%3Aoctocat%3A%22) » ticket labels.
|
||||
|
||||
## Submit an idea
|
||||
|
||||
You have great ideas, yes! Don't be shy and open [a new ticket](https://github.com/FreshRSS/FreshRSS/issues/new) on our bug tracker to ask if we can implement it. The greatest ideas often come from the shyest suggestions!
|
||||
You have great ideas, yes! Don’t be shy and open [a new ticket](https://github.com/FreshRSS/FreshRSS/issues/new) on our bug tracker to ask if we can implement it. The greatest ideas often come from the shyest suggestions!
|
||||
|
||||
If your idea is nice, we’ll have a look at it.
|
||||
|
||||
|
|
84
CREDITS.md
84
CREDITS.md
|
@ -10,19 +10,33 @@ People are sorted by name so please keep this order.
|
|||
|
||||
* [312k](https://github.com/312k): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:312k)
|
||||
* [4xfu](https://github.com/4xfu): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:4xfu)
|
||||
* [Aaron Schif](https://github.com/aaronschif): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:aaronschif)
|
||||
* [Adam Stephens](https://github.com/adamcstephens): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:adamcstephens)
|
||||
* [Adrien Dorsaz](https://github.com/Trim): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:Trim), [Web](https://adorsaz.ch/)
|
||||
* [Aidi Stan](https://github.com/aidistan): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:aidistan), [Web](https://aidistan.site/)
|
||||
* [Alexander Steinhöfer](https://github.com/lx-s): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:lx-s), [Web](https://lx-s.de/)
|
||||
* [Alexandre Alapetite](https://github.com/Alkarex): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:Alkarex), [Web](https://alexandre.alapetite.fr/)
|
||||
* [Alexis Degrugillier](https://github.com/aledeg): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:aledeg)
|
||||
* [Alwaysin](https://github.com/Alwaysin): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:Alwaysin)
|
||||
* [Amaury Carrade](https://github.com/AmauryCarrade): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:AmauryCarrade), [Web](https://amaury.carrade.eu/)
|
||||
* [AmirHossein Marjani](https://github.com/Marjani): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:Marjani)
|
||||
* [Amrul Izwan](https://github.com/amrulizwan): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:amrulizwan)
|
||||
* [András Marczinkó](https://github.com/andris155): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:andris155)
|
||||
* [Andrew Barrow](https://github.com/acbgbca): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:acbgbca)
|
||||
* [Andrew Hunter](https://github.com/rexbron): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:rexbron)
|
||||
* [Andrew Rabert](https://github.com/nvllsvm): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:nvllsvm), [Web](https://nullsum.net)
|
||||
* [Anton Smirnov](https://github.com/arokettu): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:arokettu), [Web](https://sandfox.me/)
|
||||
* [ArthurHoaro](https://github.com/ArthurHoaro): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:ArthurHoaro)
|
||||
* [Artur Weigandt](https://github.com/Art4): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:Art4), [Web](https://ruhr.social/@Art4)
|
||||
* [ASMfreaK](https://github.com/ASMfreaK): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:ASMfreaK)
|
||||
* [Axel Leroy](https://github.com/axeleroy): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:axeleroy), [Web](https://axel.leroy.sh/)
|
||||
* [azlux](https://github.com/azlux): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:azlux), [Web](https://azlux.fr/)
|
||||
* [Balázs Keresztury](https://github.com/belidzs/): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:belidzs), [Web](https://keresztury.com/)
|
||||
* [Bartosz Taudul](https://github.com/wolfpld): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:wolfpld), [Web](https://wolf.nereid.pl/)
|
||||
* [Ben Passmore](https://github.com/passbe): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:passbe), [Web](https://passbe.com/)
|
||||
* [Benjamin Bouvier](https://github.com/bnjbvr): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:bnjbvr), [Web](https://benj.me/)
|
||||
* [Benjamin Reich](https://github.com/b-reich): [contributions](https://github.com/FreshRSS/FreshRSS/commits/edge?author=b-reich), [Web](https://benjaminreich.de/)
|
||||
* [bluewhale235](https://github.com/BuleWhale): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:BuleWhale)
|
||||
* [bpatath](https://github.com/bpatath): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:bpatath)
|
||||
* [Brewal Bouvet](https://github.com/Jucgshu): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:Jucgshu), [Web](https://dizolo.eu/)
|
||||
* [Brooke.](https://github.com/BrookeDot): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:BrookeDot), [Web](https://brooke.codes/)
|
||||
|
@ -30,75 +44,118 @@ People are sorted by name so please keep this order.
|
|||
* [Cem KOÇ](https://github.com/hckweb): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:hckweb)
|
||||
* [chemical1979](https://github.com/chemical1979): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:chemical1979)
|
||||
* [Chris Francy](https://github.com/zoredache): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:zoredache)
|
||||
* [Christian König](https://github.com/yubiuser): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:yubiuser)
|
||||
* [Çılga İşcan Tercanlı](https://github.com/CilgaIscan): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:CilgaIscan)
|
||||
* [Clemens Neubauer](https://github.com/cn-tools): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:cn-tools), [Web](http://cn-tools.eu/)
|
||||
* [Corentin Garcia](https://github.com/corenting): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:corenting), [Web](http://corenting.fr/)
|
||||
* [Craig Andrews](https://github.com/candrews): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:candrews), [Web](http://candrews.integralblue.com/)
|
||||
* [Creak](https://github.com/MightyCreak): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is%3Apr+author%3AMightyCreak)
|
||||
* [Crupuk](https://github.com/Crupuk): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:Crupuk)
|
||||
* [Cyb10101](https://github.com/Cyb10101): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:Cyb10101)
|
||||
* [Damien Leroy](https://github.com/ShiiFu): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:ShiiFu)
|
||||
* [Damstre](https://github.com/Damstre): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:Damstre)
|
||||
* [danc](https://github.com/danc): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:danc), [Web](http://tintouli.free.fr/)
|
||||
* [Daniel Lo Nigro](https://github.com/Daniel15): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:Daniel15), [Web](https://d.sb/)
|
||||
* [David Lynch](https://github.com/kemayo): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:kemayo), [Web](http://davidlynch.org/)
|
||||
* [David Souza](https://github.com/araujo0205): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:araujo0205), [Web](http://davidsouza.tech/)
|
||||
* [Dennis Cheng](https://github.com/den13501): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:den13501)
|
||||
* [Django Janny](https://github.com/keltroth): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:keltroth)
|
||||
* [drosoCode](https://github.com/drosoCode): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:drosoCode), [Web](https://thomasz.me/)
|
||||
* [dswd](https://github.com/dswd): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:dswd)
|
||||
* [DuncanBennie](https://github.com/DuncanBennie): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:DuncanBennie), [Web](https://duncanbennie.com)
|
||||
* [ealdraed](https://github.com/ealdraed): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:ealdraed)
|
||||
* [Ed Sandor](https://github.com/ewsandor): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:ewsandor), [Web](https://ewsandor.com)
|
||||
* [Edgardo Ramírez](https://github.com/SoldierCorp): [contributors](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:SoldierCorp)
|
||||
* [EdJoPaTo](https://github.com/EdJoPaTo): [contributions](https://github.com/FreshRSS/FreshRSS/commits/edge?author=EdJoPaTo)
|
||||
* [equinoxmatt](https://github.com/equinoxmatt): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:equinoxmatt)
|
||||
* [Exerra](https://github.com/Exerra): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:Exerra), [Web](https://exerra.xyz)
|
||||
* [fabianski7](https://github.com/fabianski7): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:fabianski7)
|
||||
* [Fabio Lovato](https://github.com/loviuz): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:loviuz)
|
||||
* [Fake4d](https://github.com/Fake4d): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:Fake4d)
|
||||
* [Felix2yu 石渠清心](https://github.com/Felix2yu): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:Felix2yu), [Web](https://yufei.im/)
|
||||
* [FireFingers21](https://github.com/firefingers21): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:firefingers21)
|
||||
* [flo0627](https://github.com/flo0627): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:flo0627)
|
||||
* [François-Xavier Payet](https://github.com/foux): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:foux)
|
||||
* [Frans de Jonge](https://github.com/Frenzie): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:Frenzie), [Web](http://fransdejonge.com/)
|
||||
* [FromTheMoon85](https://github.com/FromTheMoon85): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:FromTheMoon85)
|
||||
* [Gaurav Thakur](https://github.com/notfoss): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:notfoss), [Web](https://blog.notfoss.com/)
|
||||
* [Gianni Scolaro](https://github.com/giannidsp): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:giannidsp)
|
||||
* [Gregor Nathanael Meyer](https://github.com/spackmat): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:spackmat), [Web](https://der-meyer.de)
|
||||
* [gsongsong](https://github.com/gsongsong): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:gsongsong)
|
||||
* [Guilherme Gall](https://github.com/gmgall): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:gmgall), [Web](https://gmgall.net/)
|
||||
* [Guillaume Fillon](https://github.com/kokaz): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:kokaz), [Web](http://www.guillaume-fillon.com/)
|
||||
* [Guillaume Hayot](https://github.com/postblue): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:postblue), [Web](https://postblue.info/)
|
||||
* [Guillaume Pugnet](https://github.com/GuillaumePugnet): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:GuillaumePugnet)
|
||||
* [Gulyapulya](https://github.com/gulyapulya): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:gulyapulya), [Web](https://dev.to/gulyapulya)
|
||||
* [happymacarts](https://github.com/happymacarts): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:happymacarts)
|
||||
* [Harshad Hirapara](https://github.com/harshad389): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:harshad389)
|
||||
* [hesch](https://github.com/hesch): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:hesch)
|
||||
* [Hippolyte Thomas](https://github.com/hippothomas): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:hippothomas), [Web](https://hippolyte-thomas.fr/)
|
||||
* [hoilc](https://github.com/hoilc): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:hoilc)
|
||||
* [ibiruai](https://github.com/ibiruai): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:ibiruai)
|
||||
* [id-konstantin-stepanov](https://github.com/id-konstantin-stepanov): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:id-konstantin-stepanov)
|
||||
* [Ilias Vrachnis](https://github.com/vrachnis): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:vrachnis)
|
||||
* [jaden](https://github.com/jaden): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:jaden)
|
||||
* [Jake Mannens](https://github.com/jakem72360): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:jakem72360)
|
||||
* [Jamie Slome](https://github.com/JamieSlome): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:JamieSlome), [Web](https://418sec.com/)
|
||||
* [Jan Lukas Gernert](https://github.com/jangernert): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:jangernert)
|
||||
* [Jan van den Berg](https://github.com/jan-vandenberg): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:jan-vandenberg), [Web](https://j11g.com/)
|
||||
* [Jaussoin Timothée](https://github.com/edhelas): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:edhelas), [Web](http://edhelas.movim.eu/)
|
||||
* [Jeremy](https://github.com/Germs2004): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:Germs2004)
|
||||
* [jlefler](https://github.com/jlefler): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:jlefler)
|
||||
* [Joe Stump](https://github.com/joestump): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:joestump), [Web](http://stu.mp)
|
||||
* [Joel Garcia](https://github.com/joelchrono12): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:joelchrono12), [Web](https://joelchrono12.xyz)
|
||||
* [Jonas Östanbäck](https://github.com/cez81): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:cez81)
|
||||
* [Jordi Garcia](https://github.com/jgtorcal): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:jgtorcal)
|
||||
* [Joris Kinable](https://github.com/jkinable): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:jkinable)
|
||||
* [Jules Bertholet](https://github.com/Jules-Bertholet): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:Jules-Bertholet)
|
||||
* [Julien Reichardt](https://github.com/j8r): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:j8r), [Web](https://blog.jrei.ch/)
|
||||
* [Julien-Pierre Avérous](https://github.com/javerous): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:javerous), [Web](https://www.sourcemac.com/)
|
||||
* [Justin Tracey](https://github.com/jtracey): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:jtracey), [Web](https://unsuspicious.click)
|
||||
* [Kaibin Yang](https://github.com/SkyYkb): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:SkyYkb), [Web](https://kaibinyang.com/)
|
||||
* [Karim Sharafutdinov](https://github.com/krm-shrftdnv): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:krm-shrftdnv)
|
||||
* [Kasimir Cash](https://github.com/KasimirCash): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:KasimirCash)
|
||||
* [Kevin Papst](https://github.com/kevinpapst): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:kevinpapst), [Web](http://www.kevinpapst.de/)
|
||||
* [Kiblyn11](https://github.com/Kiblyn11): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:Kiblyn11)
|
||||
* [kinoushe](https://github.com/kinoushe): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:kinoushe)
|
||||
* [knasdk](https://github.com/knasdk): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:knasdk)
|
||||
* [Konrad Gräfe](https://github.com/kgraefe): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:kgraefe)
|
||||
* [Konstantinos Megas](https://github.com/nextdoorpanda): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:nextdoorpanda)
|
||||
* [Kristian Salonen](https://github.com/krisu5): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:krisu5)
|
||||
* [Leepic](https://github.com/Leepic): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:Leepic)
|
||||
* [LLeana](https://github.com/LleanaRuv): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:LleanaRuv)
|
||||
* [loft17](https://github.com/loft17): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:loft17)
|
||||
* [Luc Didry](https://github.com/ldidry): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:ldidry), [Web](https://www.fiat-tux.fr/)
|
||||
* [Luc Sanchez](https://github.com/ColonelMoutarde): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:ColonelMoutarde), [Web](https://www.luc-sanchez.fr/)
|
||||
* [LucasVerneyDGE](https://github.com/LucasVerneyDGE): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:LucasVerneyDGE)
|
||||
* [Lukas David Vacula](https://github.com/ldv8434): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:ldv8434), [Web](https://lvacula.com/)
|
||||
* [Manu](https://github.com/m3nu): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:m3nu)
|
||||
* [Marc Ole Bulling](https://github.com/Forceu): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:Forceu)
|
||||
* [Marco Hinniger](https://github.com/rom-1): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:rom-1), [Web](https://blog.rootdir.net/)
|
||||
* [marcohald](https://github.com/marcohald): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:marcohald)
|
||||
* [marcomrc](https://github.com/marcomrc): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:marcomrc)
|
||||
* [Marcus Rohrmoser](https://github.com/mro): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:mro), [Web](http://mro.name/~me)
|
||||
* [Marek Pavelka](https://github.com/marapavelka): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:marapavelka), [Web](https://marekpavelka.cz)
|
||||
* [Marien Fressinaud](https://github.com/marienfressinaud): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:marienfressinaud), [Web](https://marienfressinaud.fr/)
|
||||
* [Mark Monteiro](https://github.com/mark-monteiro): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:mark-monteiro), [Web](https://markmonteiro.info/)
|
||||
* [Martin](https://github.com/C0rn3j): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:C0rn3j), [Web](https://rys.pw/)
|
||||
* [math-GH](https://github.com/math-GH): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:math-GH)
|
||||
* [Matt Sephton](https://github.com/gingerbeardman): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:gingerbeardman)
|
||||
* [Maurice Schleußinger](https://github.com/maurice-schleussinger): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:maurice-schleussinger)
|
||||
* [May Meow](https://github.com/MayMeow): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:MayMeow), [Web](https://maymeow.com)
|
||||
* [Mejans](https://github.com/Mejans): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:Mejans)
|
||||
* [Melvyn Laïly](https://github.com/yaurthek): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:yaurthek), [Web](http://x2a.yt/)
|
||||
* [Miguel Sánchez](https://github.com/msdlr): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:msdlr)
|
||||
* [Miika Launiainen](https://gitlab.com/miicat): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:miicat), [Web](https://miicat.eu/)
|
||||
* [Mike Vanbuskirk](https://github.com/codevbus): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:codevbus) [Web](http://mikevanbuskirk.io/)
|
||||
* [miles](https://github.com/miles170): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:miles170)
|
||||
* [mincerafter42](https://github.com/mincerafter42): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:mincerafter42), [Web](https://mincerafter42.github.io)
|
||||
* [Mossroy](https://github.com/mossroy): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:mossroy)
|
||||
* [Mossroy](https://github.com/mossroy): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:mossroy), [Web](https://blog.mossroy.fr/)
|
||||
* [MSZ](https://github.com/mszkb): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:mszkb)
|
||||
* [Mubarak Harran Alketbi](https://github.com/MHketbi): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:MHketbi)
|
||||
* [Myuki](https://github.com/Myuki): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:Myuki)
|
||||
* [NaeiKinDus](https://github.com/NaeiKinDus): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:NaeiKinDus)
|
||||
* [Nainor](https://github.com/Nainor): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:Nainor)
|
||||
* [nanhualyq](https://github.com/nanhualyq): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:nanhualyq)
|
||||
* [Natalie Stroud](https://github.com/natastro): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:natastro)
|
||||
|
@ -106,17 +163,24 @@ People are sorted by name so please keep this order.
|
|||
* [Nico B](https://github.com/youknow0): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:youknow0)
|
||||
* [Nicola Spanti](https://github.com/RyDroid): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:RyDroid), [Web](http://www.nicola-spanti.info/)
|
||||
* [Nicolas Elie](https://github.com/nicolaselie): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:nicolaselie)
|
||||
* [Nicolas Ferrari](https://github.com/nferrari): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:nferrari), [Web](https://www.alwaysdata.com/)
|
||||
* [Nicolas Frandeboeuf](https://github.com/nicofrand): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:nicofrand), [Web](https://nicofrand.ey)
|
||||
* [Nicolas Lœuillet](https://github.com/nicosomb): [contributions](https://github.com/FreshRSS/documentation/pulls?q=is:pr+author:nicosomb), [Web](http://www.loeuillet.org/)
|
||||
* [Nicolas Pereira](https://github.com/NicolasPereira): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:NicolasPereira)
|
||||
* [No Name Pro](https://github.com/NoNamePro0): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:NoNamePro0)
|
||||
* [OctopusET](https://github.com/OctopusET): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:OctopusET)
|
||||
* [Offerel](https://github.com/Offerel): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:Offerel)
|
||||
* [Olivier Brencklé](https://github.com/obrenckle): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:obrenckle)
|
||||
* [Olivier Dossmann](https://github.com/blankoworld): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:blankoworld), [Web](https://olivier.dossmann.net)
|
||||
* [ORelio](https://github.com/ORelio): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:ORelio), [Web](https://microzoom.fr/)
|
||||
* [otaconix](https://github.com/otaconix): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:otaconix)
|
||||
* [Pablo Caro](https://github.com/pcaro90): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:pcaro90), [Web](https://pcaro.es/)
|
||||
* [PAHXO](https://github.com/PAHXO): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:PAHXO)
|
||||
* [papaschloss](https://github.com/papaschloss): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:papaschloss)
|
||||
* [Patrick Crandol](https://github.com/pattems): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:pattems)
|
||||
* [Paulius Šukys](https://github.com/psukys): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:psukys), [Web](http://sukys.eu)
|
||||
* [Paweł Kalemba](https://github.com/pkalemba): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:pkalemba)
|
||||
* [PedroPMS](https://github.com/PedroPMS): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:PedroPMS)
|
||||
* [perrinjerome](https://github.com/perrinjerome): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:perrinjerome)
|
||||
* [Peter Stoinov](https://github.com/stoinov): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:stoinov), [Web](https://stoinov.com)
|
||||
* [Petra Lamborn](https://github.com/petraoleum): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:petraoleum), [Web](https://petras.space)
|
||||
|
@ -129,29 +193,43 @@ People are sorted by name so please keep this order.
|
|||
* [printfuck](https://github.com/printfuck): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:printfuck), [Web](https://eris.cc)
|
||||
* [proletarius101](https://github.com/proletarius101): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:proletarius101)
|
||||
* [purexo](https://github.com/purexo): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:purexo), [Web](https://purexo.mom/)
|
||||
* [Pyr0x1](https://github.com/Pyr0x1): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:Pyr0x1)
|
||||
* [Quent-in](https://github.com/Quent-in): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:Quent-in)
|
||||
* [Quentin Dufour](https://github.com/superboum): [contributions](https://github.com/FreshRSS/documentation/pulls?q=is:pr+author:superboum), [Web](http://quentin.dufour.io/)
|
||||
* [Quentin Pagès](https://github.com/Quenty31): [contributions](https://github.com/FreshRSS/documentation/pulls?q=is:pr+author:Quenty31)
|
||||
* [Raineer](https://github.com/Raineer): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:Raineer)
|
||||
* [Ramazan Sancar](https://github.com/ramazansancar): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:ramazansancar), [Web](https://ramazansancar.com.tr/)
|
||||
* [Ramón Cutanda](https://github.com/rcutanda): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:rcutanda)
|
||||
* [rdmitr](https://github.com/rdmitr): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:rdmitr)
|
||||
* [Rebecca Scott](https://github.com/becdetat): [contirbutions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:becdetat), [Web](https://becdetat.com)
|
||||
* [Rezad](https://github.com/rezad1393): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:rezad1393)
|
||||
* [Robert Kaussow](https://github.com/xoxys): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:xoxys), [Web](https://geeklabor.de/)
|
||||
* [robertdahlem](https://github.com/robertdahlem): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:robertdahlem)
|
||||
* [rocka](https://github.com/rocka): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:rocka)
|
||||
* [romibi](https://github.com/romibi): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:romibi)
|
||||
* [Rosemary Le Faive](https://github.com/rosiel): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:rosiel)
|
||||
* [Rufubi](https://github.com/Rufubi): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:Rufubi)
|
||||
* [ryoku-cha](https://github.com/ryoku-cha): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:ryoku-cha)
|
||||
* [Sadetdin EYILI](https://github.com/sad270): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:sad270)
|
||||
* [Sam Cohen](https://github.com/samc1213): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:samc1213)
|
||||
* [Sandro Jäckel](https://github.com/SuperSandro2000): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:SuperSandro2000), [Web](https://supersandro.de/)
|
||||
* [Sebastian K](https://github.com/skrollme): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:skrollme)
|
||||
* [shn7798](https://github.com/shn7798): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:shn7798), [Web](http://www.code2talk.com/)
|
||||
* [Simone "roughnecks" Canaletti](https://github.com/roughnecks): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:roughnecks), [Web](https://woodpeckersnest.space/)
|
||||
* [sirideain](https://github.com/sirideain): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:sirideain)
|
||||
* [skrlet13](https://github.com/skrlet13): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:skrlet13), [Web](https://www.skrlet13.cl/)
|
||||
* [Sp3r4z](https://github.com/Sp3r4z): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:Sp3r4z)
|
||||
* [Steve Jones](https://github.com/squaregoldfish): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:squaregoldfish)
|
||||
* [Strubbl](https://github.com/Strubbl): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:Strubbl)
|
||||
* [Stunkymonkey](https://github.com/Stunkymonkey): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:Stunkymonkey)
|
||||
* [stysebae](https://github.com/stysebae): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:stysebae)
|
||||
* [subic](https://github.com/subic): [contributions](https://github.com/FreshRSS/documentation/pulls?q=is:pr+author:subic)
|
||||
* [Tealk](https://github.com/Tealk): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:Tealk), [Web](https://rollenspiel.monster/)
|
||||
* [Tets42](https://github.com/Tets42): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:Tets42)
|
||||
* [Thelonius Kort](https://github.com/tnt): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:tnt)
|
||||
* [Thomas Citharel](https://github.com/tcitworld): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:tomgue), [Web](https://www.tcit.fr/)
|
||||
* [Thomas Guesnon](https://github.com/patjennings): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:patjennings), [Web](http://www.thomasguesnon.fr/)
|
||||
* [Thomas Renes](https://github.com/thomasrenes): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:thomasrenes), [Web](https://thomas.renesweb.nl/)
|
||||
* [thomas-gt](https://github.com/thomas-gt): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:thomas-gt)
|
||||
* [ThomasSmallert](https://github.com/ThomasSmallert): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:ThomasSmallert)
|
||||
* [Ths2-9Y-LqJt6](https://github.com/Ths2-9Y-LqJt6): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:Ths2-9Y-LqJt6)
|
||||
|
@ -163,8 +241,14 @@ People are sorted by name so please keep this order.
|
|||
* [Uncovery](https://github.com/uncovery): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:uncovery)
|
||||
* [upskaling](https://github.com/upskaling): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:upskaling)
|
||||
* [Virgil Chen](https://github.com/VirgilChen97): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:VirgilChen97)
|
||||
* [VYSE V.E.O](https://github.com/V-E-O): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:V-E-O)
|
||||
* [Wanabo](https://github.com/Wanabo): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:Wanabo)
|
||||
* [witchcraze](https://github.com/witchcraze): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:witchcraze)
|
||||
* [wtoscer](https://github.com/wtoscer): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:wtoscer)
|
||||
* [xnaas](https://github.com/xnaas): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:xnaas), [Web](https://xnaas.info/)
|
||||
* [XtremeOwnage](https://github.com/XtremeOwnage): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:XtremeOwnageDotCom), [Web](https://static.xtremeownage.com/)
|
||||
* [Yamakuni](https://github.com/Yamakuni): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:Yamakuni), [Web](https://ofanch.me/)
|
||||
* [yzqzss|一座桥在水上](https://github.com/yzqzss): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:yzqzss), [Web](https://blog.othing.xyz/)
|
||||
* [Zhaofeng Li](https://github.com/zhaofengli): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:zhaofengli), [Web](https://zhaofeng.li/)
|
||||
* [Zhiyuan Zheng](https://github.com/zhzy0077): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:zhzy0077)
|
||||
* [zukizukizuki](https://github.com/zukizukizuki): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:zukizukizuki), [Web](https://zukkie.link/)
|
||||
|
|
25
Docker/.env
25
Docker/.env
|
@ -1,25 +0,0 @@
|
|||
# Environment file for docker-compose
|
||||
# In this file you need to define the different settings
|
||||
|
||||
|
||||
# ====================================
|
||||
# Database
|
||||
# ====================================
|
||||
|
||||
|
||||
# Database to use for freshrss
|
||||
POSTGRES_DB=freshrss
|
||||
|
||||
# User in the freshrss database
|
||||
POSTGRES_USER=freshrss
|
||||
|
||||
# Password for the defined user
|
||||
POSTGRES_PASSWORD=freshrss
|
||||
|
||||
|
||||
# ====================================
|
||||
# FreshRSS
|
||||
# ====================================
|
||||
|
||||
# Exposed port for the docker-container
|
||||
EXPOSED_PORT=8080
|
|
@ -1 +1,2 @@
|
|||
env.txt
|
||||
.env
|
||||
|
|
|
@ -1,13 +1,14 @@
|
|||
FROM debian:11-slim
|
||||
FROM debian:12-slim
|
||||
|
||||
ENV TZ UTC
|
||||
SHELL ["/bin/bash", "-o", "pipefail", "-c"]
|
||||
RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone
|
||||
|
||||
ARG DEBIAN_FRONTEND=noninteractive
|
||||
RUN apt-get update && \
|
||||
apt-get install --no-install-recommends -y \
|
||||
ca-certificates cron \
|
||||
apache2 libapache2-mod-php \
|
||||
libapache2-mod-auth-openidc \
|
||||
php-curl php-gmp php-intl php-mbstring php-xml php-zip \
|
||||
php-sqlite3 php-mysql php-pgsql && \
|
||||
rm -rf /var/lib/apt/lists/*
|
||||
|
@ -19,7 +20,6 @@ COPY . /var/www/FreshRSS
|
|||
COPY ./Docker/*.Apache.conf /etc/apache2/sites-available/
|
||||
|
||||
ARG FRESHRSS_VERSION
|
||||
ARG SOURCE_BRANCH
|
||||
ARG SOURCE_COMMIT
|
||||
|
||||
LABEL \
|
||||
|
@ -27,18 +27,20 @@ LABEL \
|
|||
org.opencontainers.image.description="A self-hosted RSS feed aggregator" \
|
||||
org.opencontainers.image.documentation="https://freshrss.github.io/FreshRSS/" \
|
||||
org.opencontainers.image.licenses="AGPL-3.0" \
|
||||
org.opencontainers.image.revision="${SOURCE_BRANCH}.${SOURCE_COMMIT}" \
|
||||
org.opencontainers.image.revision="${SOURCE_COMMIT}" \
|
||||
org.opencontainers.image.source="https://github.com/FreshRSS/FreshRSS" \
|
||||
org.opencontainers.image.title="FreshRSS" \
|
||||
org.opencontainers.image.url="https://freshrss.org/" \
|
||||
org.opencontainers.image.vendor="FreshRSS" \
|
||||
org.opencontainers.image.version="$FRESHRSS_VERSION"
|
||||
|
||||
RUN a2dismod -f alias autoindex negotiation status && \
|
||||
a2enmod deflate expires headers mime remoteip setenvif && \
|
||||
a2disconf '*' && \
|
||||
a2dissite '*' && \
|
||||
a2ensite 'FreshRSS*'
|
||||
RUN a2dismod -q -f alias autoindex negotiation status && \
|
||||
a2dismod -q auth_openidc && \
|
||||
phpdismod calendar exif ffi ftp gettext mysqli posix readline shmop sockets sysvmsg sysvsem sysvshm xsl && \
|
||||
a2enmod -q deflate expires headers mime remoteip setenvif && \
|
||||
a2disconf -q '*' && \
|
||||
a2dissite -q '*' && \
|
||||
a2ensite -q 'FreshRSS*'
|
||||
|
||||
RUN sed -r -i "/^\s*(CustomLog|ErrorLog|Listen) /s/^/#/" /etc/apache2/apache2.conf && \
|
||||
sed -r -i "/^\s*Listen /s/^/#/" /etc/apache2/ports.conf && \
|
||||
|
@ -52,8 +54,11 @@ RUN sed -r -i "/^\s*(CustomLog|ErrorLog|Listen) /s/^/#/" /etc/apache2/apache2.co
|
|||
ENV COPY_LOG_TO_SYSLOG On
|
||||
ENV COPY_SYSLOG_TO_STDERR On
|
||||
ENV CRON_MIN ''
|
||||
ENV DATA_PATH ''
|
||||
ENV FRESHRSS_ENV ''
|
||||
ENV LISTEN ''
|
||||
ENV OIDC_ENABLED ''
|
||||
ENV TRUSTED_PROXY ''
|
||||
|
||||
ENTRYPOINT ["./Docker/entrypoint.sh"]
|
||||
|
||||
|
@ -61,4 +66,4 @@ EXPOSE 80
|
|||
# hadolint ignore=DL3025
|
||||
CMD ([ -z "$CRON_MIN" ] || cron) && \
|
||||
. /etc/apache2/envvars && \
|
||||
exec apache2 -D FOREGROUND
|
||||
exec apache2 -D FOREGROUND $([ -n "$OIDC_ENABLED" ] && [ "$OIDC_ENABLED" -ne 0 ] && echo '-D OIDC_ENABLED')
|
||||
|
|
|
@ -1,12 +1,14 @@
|
|||
FROM alpine:3.15
|
||||
FROM alpine:3.19
|
||||
|
||||
ENV TZ UTC
|
||||
SHELL ["/bin/ash", "-eo", "pipefail", "-c"]
|
||||
|
||||
RUN apk add --no-cache \
|
||||
apache2 php8-apache2 \
|
||||
php8 php8-curl php8-gmp php8-intl php8-mbstring php8-xml php8-zip \
|
||||
php8-ctype php8-dom php8-fileinfo php8-iconv php8-json php8-opcache php8-phar php8-session php8-simplexml php8-xmlreader php8-xmlwriter php8-tokenizer php8-zlib \
|
||||
php8-pdo_sqlite php8-pdo_mysql php8-pdo_pgsql
|
||||
tzdata \
|
||||
apache2 php-apache2 \
|
||||
php php-curl php-gmp php-intl php-mbstring php-xml php-zip \
|
||||
php-ctype php-dom php-fileinfo php-iconv php-json php-opcache php-openssl php-phar php-session php-simplexml php-xmlreader php-xmlwriter php-xml php-tokenizer php-zlib \
|
||||
php-pdo_sqlite php-pdo_mysql php-pdo_pgsql
|
||||
|
||||
RUN mkdir -p /var/www/FreshRSS /run/apache2/
|
||||
WORKDIR /var/www/FreshRSS
|
||||
|
@ -15,7 +17,6 @@ COPY . /var/www/FreshRSS
|
|||
COPY ./Docker/*.Apache.conf /etc/apache2/conf.d/
|
||||
|
||||
ARG FRESHRSS_VERSION
|
||||
ARG SOURCE_BRANCH
|
||||
ARG SOURCE_COMMIT
|
||||
|
||||
LABEL \
|
||||
|
@ -23,7 +24,7 @@ LABEL \
|
|||
org.opencontainers.image.description="A self-hosted RSS feed aggregator" \
|
||||
org.opencontainers.image.documentation="https://freshrss.github.io/FreshRSS/" \
|
||||
org.opencontainers.image.licenses="AGPL-3.0" \
|
||||
org.opencontainers.image.revision="${SOURCE_BRANCH}.${SOURCE_COMMIT}" \
|
||||
org.opencontainers.image.revision="${SOURCE_COMMIT}" \
|
||||
org.opencontainers.image.source="https://github.com/FreshRSS/FreshRSS" \
|
||||
org.opencontainers.image.title="FreshRSS" \
|
||||
org.opencontainers.image.url="https://freshrss.org/" \
|
||||
|
@ -38,7 +39,6 @@ RUN rm -f /etc/apache2/conf.d/languages.conf /etc/apache2/conf.d/info.conf \
|
|||
/etc/apache2/httpd.conf && \
|
||||
sed -r -i "/^\s*(CustomLog|ErrorLog|Listen) /s/^/#/" \
|
||||
/etc/apache2/httpd.conf && \
|
||||
if [ ! -f /usr/bin/php ]; then ln -s /usr/bin/php8 /usr/bin/php; else true; fi && \
|
||||
# Disable built-in updates when using Docker, as the full image is supposed to be updated instead.
|
||||
sed -r -i "\\#disable_update#s#^.*#\t'disable_update' => true,#" ./config.default.php && \
|
||||
touch /var/www/FreshRSS/Docker/env.txt && \
|
||||
|
@ -49,12 +49,15 @@ RUN rm -f /etc/apache2/conf.d/languages.conf /etc/apache2/conf.d/info.conf \
|
|||
ENV COPY_LOG_TO_SYSLOG On
|
||||
ENV COPY_SYSLOG_TO_STDERR On
|
||||
ENV CRON_MIN ''
|
||||
ENV DATA_PATH ''
|
||||
ENV FRESHRSS_ENV ''
|
||||
ENV LISTEN ''
|
||||
ENV OIDC_ENABLED ''
|
||||
ENV TRUSTED_PROXY ''
|
||||
|
||||
ENTRYPOINT ["./Docker/entrypoint.sh"]
|
||||
|
||||
EXPOSE 80
|
||||
# hadolint ignore=DL3025
|
||||
CMD ([ -z "$CRON_MIN" ] || crond -d 6) && \
|
||||
exec httpd -D FOREGROUND
|
||||
exec httpd -D FOREGROUND $([ -n "$OIDC_ENABLED" ] && [ "$OIDC_ENABLED" -ne 0 ] && echo '-D OIDC_ENABLED')
|
||||
|
|
|
@ -2,11 +2,14 @@ FROM alpine:edge
|
|||
|
||||
ENV TZ UTC
|
||||
SHELL ["/bin/ash", "-eo", "pipefail", "-c"]
|
||||
RUN apk add --no-cache \
|
||||
apache2 php8-apache2 \
|
||||
php8 php8-curl php8-gmp php8-intl php8-mbstring php8-xml php8-zip \
|
||||
php8-ctype php8-dom php8-fileinfo php8-iconv php8-json php8-opcache php8-phar php8-session php8-simplexml php8-xmlreader php8-xmlwriter php8-tokenizer php8-zlib \
|
||||
php8-pdo_sqlite php8-pdo_mysql php8-pdo_pgsql
|
||||
RUN echo 'http://dl-cdn.alpinelinux.org/alpine/edge/testing' >> /etc/apk/repositories && \
|
||||
apk add --no-cache \
|
||||
tzdata \
|
||||
apache2 php83-apache2 \
|
||||
apache-mod-auth-openidc \
|
||||
php83 php83-curl php83-gmp php83-intl php83-mbstring php83-xml php83-zip \
|
||||
php83-ctype php83-dom php83-fileinfo php83-iconv php83-json php83-opcache php83-openssl php83-phar php83-session php83-simplexml php83-xmlreader php83-xmlwriter php83-xml php83-tokenizer php83-zlib \
|
||||
php83-pdo_sqlite php83-pdo_mysql php83-pdo_pgsql
|
||||
|
||||
RUN mkdir -p /var/www/FreshRSS /run/apache2/
|
||||
WORKDIR /var/www/FreshRSS
|
||||
|
@ -15,7 +18,6 @@ COPY . /var/www/FreshRSS
|
|||
COPY ./Docker/*.Apache.conf /etc/apache2/conf.d/
|
||||
|
||||
ARG FRESHRSS_VERSION
|
||||
ARG SOURCE_BRANCH
|
||||
ARG SOURCE_COMMIT
|
||||
|
||||
LABEL \
|
||||
|
@ -23,7 +25,7 @@ LABEL \
|
|||
org.opencontainers.image.description="A self-hosted RSS feed aggregator" \
|
||||
org.opencontainers.image.documentation="https://freshrss.github.io/FreshRSS/" \
|
||||
org.opencontainers.image.licenses="AGPL-3.0" \
|
||||
org.opencontainers.image.revision="${SOURCE_BRANCH}.${SOURCE_COMMIT}" \
|
||||
org.opencontainers.image.revision="${SOURCE_COMMIT}" \
|
||||
org.opencontainers.image.source="https://github.com/FreshRSS/FreshRSS" \
|
||||
org.opencontainers.image.title="FreshRSS" \
|
||||
org.opencontainers.image.url="https://freshrss.org/" \
|
||||
|
@ -38,7 +40,9 @@ RUN rm -f /etc/apache2/conf.d/languages.conf /etc/apache2/conf.d/info.conf \
|
|||
/etc/apache2/httpd.conf && \
|
||||
sed -r -i "/^\s*(CustomLog|ErrorLog|Listen) /s/^/#/" \
|
||||
/etc/apache2/httpd.conf && \
|
||||
if [ ! -f /usr/bin/php ]; then ln -s /usr/bin/php8 /usr/bin/php; else true; fi && \
|
||||
mv /etc/apache2/conf.d/mod-auth-openidc.conf /etc/apache2/conf.d/mod-auth-openidc.conf.bak && \
|
||||
if [ ! -f /usr/bin/php ]; then ln -s /usr/bin/php83 /usr/bin/php; else true; fi && \
|
||||
echo 'memory_limit = 256M' > /etc/php83/conf.d/10_memory.ini && \
|
||||
# Disable built-in updates when using Docker, as the full image is supposed to be updated instead.
|
||||
sed -r -i "\\#disable_update#s#^.*#\t'disable_update' => true,#" ./config.default.php && \
|
||||
touch /var/www/FreshRSS/Docker/env.txt && \
|
||||
|
@ -49,12 +53,15 @@ RUN rm -f /etc/apache2/conf.d/languages.conf /etc/apache2/conf.d/info.conf \
|
|||
ENV COPY_LOG_TO_SYSLOG On
|
||||
ENV COPY_SYSLOG_TO_STDERR On
|
||||
ENV CRON_MIN ''
|
||||
ENV DATA_PATH ''
|
||||
ENV FRESHRSS_ENV ''
|
||||
ENV LISTEN ''
|
||||
ENV OIDC_ENABLED ''
|
||||
ENV TRUSTED_PROXY ''
|
||||
|
||||
ENTRYPOINT ["./Docker/entrypoint.sh"]
|
||||
|
||||
EXPOSE 80
|
||||
# hadolint ignore=DL3025
|
||||
CMD ([ -z "$CRON_MIN" ] || crond -d 6) && \
|
||||
exec httpd -D FOREGROUND
|
||||
exec httpd -D FOREGROUND $([ -n "$OIDC_ENABLED" ] && [ "$OIDC_ENABLED" -ne 0 ] && echo '-D OIDC_ENABLED')
|
||||
|
|
|
@ -1,11 +1,13 @@
|
|||
FROM alpine:3.5
|
||||
FROM alpine:3.13
|
||||
|
||||
ENV TZ UTC
|
||||
SHELL ["/bin/ash", "-eo", "pipefail", "-c"]
|
||||
|
||||
RUN apk add --no-cache \
|
||||
tzdata \
|
||||
apache2 php7-apache2 \
|
||||
php7 php7-curl php7-gmp php7-intl php7-mbstring php7-xml php7-zip \
|
||||
php7-ctype php7-dom php7-iconv php7-json php7-opcache php7-openssl php7-phar php7-session php7-xmlreader php7-zlib \
|
||||
php7-ctype php7-dom php7-fileinfo php7-iconv php7-json php7-opcache php7-openssl php7-phar php7-session php7-simplexml php7-xmlreader php7-xmlwriter php7-xml php7-tokenizer php7-zlib \
|
||||
php7-pdo_sqlite php7-pdo_mysql php7-pdo_pgsql
|
||||
|
||||
RUN mkdir -p /var/www/FreshRSS /run/apache2/
|
||||
|
@ -15,7 +17,6 @@ COPY . /var/www/FreshRSS
|
|||
COPY ./Docker/*.Apache.conf /etc/apache2/conf.d/
|
||||
|
||||
ARG FRESHRSS_VERSION
|
||||
ARG SOURCE_BRANCH
|
||||
ARG SOURCE_COMMIT
|
||||
|
||||
LABEL \
|
||||
|
@ -23,7 +24,7 @@ LABEL \
|
|||
org.opencontainers.image.description="A self-hosted RSS feed aggregator" \
|
||||
org.opencontainers.image.documentation="https://freshrss.github.io/FreshRSS/" \
|
||||
org.opencontainers.image.licenses="AGPL-3.0" \
|
||||
org.opencontainers.image.revision="${SOURCE_BRANCH}.${SOURCE_COMMIT}" \
|
||||
org.opencontainers.image.revision="${SOURCE_COMMIT}" \
|
||||
org.opencontainers.image.source="https://github.com/FreshRSS/FreshRSS" \
|
||||
org.opencontainers.image.title="FreshRSS" \
|
||||
org.opencontainers.image.url="https://freshrss.org/" \
|
||||
|
@ -39,6 +40,7 @@ RUN rm -f /etc/apache2/conf.d/languages.conf /etc/apache2/conf.d/info.conf \
|
|||
sed -r -i "/^\s*(CustomLog|ErrorLog|Listen) /s/^/#/" \
|
||||
/etc/apache2/httpd.conf && \
|
||||
if [ ! -f /usr/bin/php ]; then ln -s /usr/bin/php7 /usr/bin/php; else true; fi && \
|
||||
echo 'memory_limit = 256M' > /etc/php7/conf.d/10_memory.ini && \
|
||||
# Disable built-in updates when using Docker, as the full image is supposed to be updated instead.
|
||||
sed -r -i "\\#disable_update#s#^.*#\t'disable_update' => true,#" ./config.default.php && \
|
||||
touch /var/www/FreshRSS/Docker/env.txt && \
|
||||
|
@ -49,12 +51,15 @@ RUN rm -f /etc/apache2/conf.d/languages.conf /etc/apache2/conf.d/info.conf \
|
|||
ENV COPY_LOG_TO_SYSLOG On
|
||||
ENV COPY_SYSLOG_TO_STDERR On
|
||||
ENV CRON_MIN ''
|
||||
ENV DATA_PATH ''
|
||||
ENV FRESHRSS_ENV ''
|
||||
ENV LISTEN ''
|
||||
ENV OIDC_ENABLED ''
|
||||
ENV TRUSTED_PROXY ''
|
||||
|
||||
ENTRYPOINT ["./Docker/entrypoint.sh"]
|
||||
|
||||
EXPOSE 80
|
||||
# hadolint ignore=DL3025
|
||||
CMD ([ -z "$CRON_MIN" ] || crond -d 6) && \
|
||||
exec httpd -D FOREGROUND
|
||||
exec httpd -D FOREGROUND $([ -n "$OIDC_ENABLED" ] && [ "$OIDC_ENABLED" -ne 0 ] && echo '-D OIDC_ENABLED')
|
||||
|
|
|
@ -1,76 +0,0 @@
|
|||
# Only relevant for Docker Hub or QEMU multi-architecture builds.
|
||||
# Prefer the normal `Dockerfile` if you are building manually on the targeted architecture.
|
||||
|
||||
FROM arm32v7/debian:11-slim
|
||||
|
||||
# Requires ./hooks/*
|
||||
COPY ./Docker/qemu-arm-* /usr/bin/
|
||||
|
||||
ENV TZ UTC
|
||||
SHELL ["/bin/bash", "-o", "pipefail", "-c"]
|
||||
RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone
|
||||
|
||||
RUN apt-get update && \
|
||||
apt-get install --no-install-recommends -y \
|
||||
ca-certificates cron \
|
||||
apache2 libapache2-mod-php \
|
||||
php-curl php-gmp php-intl php-mbstring php-xml php-zip \
|
||||
php-sqlite3 php-mysql php-pgsql && \
|
||||
rm -rf /var/lib/apt/lists/*
|
||||
|
||||
RUN mkdir -p /var/www/FreshRSS/ /run/apache2/
|
||||
WORKDIR /var/www/FreshRSS
|
||||
|
||||
COPY . /var/www/FreshRSS
|
||||
COPY ./Docker/*.Apache.conf /etc/apache2/sites-available/
|
||||
|
||||
ARG FRESHRSS_VERSION
|
||||
ARG SOURCE_BRANCH
|
||||
ARG SOURCE_COMMIT
|
||||
|
||||
LABEL \
|
||||
org.opencontainers.image.authors="Alkarex" \
|
||||
org.opencontainers.image.description="A self-hosted RSS feed aggregator" \
|
||||
org.opencontainers.image.documentation="https://freshrss.github.io/FreshRSS/" \
|
||||
org.opencontainers.image.licenses="AGPL-3.0" \
|
||||
org.opencontainers.image.revision="${SOURCE_BRANCH}.${SOURCE_COMMIT}" \
|
||||
org.opencontainers.image.source="https://github.com/FreshRSS/FreshRSS" \
|
||||
org.opencontainers.image.title="FreshRSS" \
|
||||
org.opencontainers.image.url="https://freshrss.org/" \
|
||||
org.opencontainers.image.vendor="FreshRSS" \
|
||||
org.opencontainers.image.version="$FRESHRSS_VERSION"
|
||||
|
||||
RUN a2dismod -f alias autoindex negotiation status && \
|
||||
a2enmod deflate expires headers mime remoteip setenvif && \
|
||||
a2disconf '*' && \
|
||||
a2dissite '*' && \
|
||||
a2ensite 'FreshRSS*'
|
||||
|
||||
RUN sed -r -i "/^\s*(CustomLog|ErrorLog|Listen) /s/^/#/" /etc/apache2/apache2.conf && \
|
||||
sed -r -i "/^\s*Listen /s/^/#/" /etc/apache2/ports.conf && \
|
||||
# Disable built-in updates when using Docker, as the full image is supposed to be updated instead.
|
||||
sed -r -i "\\#disable_update#s#^.*#\t'disable_update' => true,#" ./config.default.php && \
|
||||
touch /var/www/FreshRSS/Docker/env.txt && \
|
||||
echo "17,47 * * * * . /var/www/FreshRSS/Docker/env.txt; \
|
||||
su www-data -s /bin/sh -c 'php /var/www/FreshRSS/app/actualize_script.php' \
|
||||
2>> /proc/1/fd/2 > /tmp/FreshRSS.log" > /etc/crontab.freshrss.default
|
||||
|
||||
# Seems needed for arm32v7/ubuntu on Docker Hub
|
||||
RUN update-ca-certificates -f
|
||||
|
||||
# Useful with the `--squash` build option
|
||||
RUN rm /usr/bin/qemu-* /var/www/FreshRSS/Docker/qemu-*
|
||||
|
||||
ENV COPY_LOG_TO_SYSLOG On
|
||||
ENV COPY_SYSLOG_TO_STDERR On
|
||||
ENV CRON_MIN ''
|
||||
ENV FRESHRSS_ENV ''
|
||||
ENV LISTEN ''
|
||||
|
||||
ENTRYPOINT ["./Docker/entrypoint.sh"]
|
||||
|
||||
EXPOSE 80
|
||||
# hadolint ignore=DL3025
|
||||
CMD ([ -z "$CRON_MIN" ] || cron) && \
|
||||
. /etc/apache2/envvars && \
|
||||
exec apache2 -D FOREGROUND
|
|
@ -1,13 +1,60 @@
|
|||
ServerName freshrss.localhost
|
||||
Listen 0.0.0.0:80
|
||||
Listen 80
|
||||
DocumentRoot /var/www/FreshRSS/p/
|
||||
RemoteIPHeader X-Forwarded-For
|
||||
RemoteIPTrustedProxy 10.0.0.1/8 172.16.0.1/12 192.168.0.1/16
|
||||
LogFormat "%a %l %u %t \"%r\" %>s %b \"%{Referer}i\" \"%{User-Agent}i\"" combined_proxy
|
||||
CustomLog /dev/stdout combined_proxy
|
||||
ErrorLog /dev/stderr
|
||||
AllowEncodedSlashes On
|
||||
ServerTokens OS
|
||||
TraceEnable Off
|
||||
ErrorLog /dev/stderr
|
||||
|
||||
# For logging the original user-agent IP instead of proxy IPs:
|
||||
<IfModule mod_remoteip.c>
|
||||
# Can be disabled by setting the TRUSTED_PROXY environment variable to 0:
|
||||
RemoteIPHeader X-Forwarded-For
|
||||
# Can be overridden by the TRUSTED_PROXY environment variable:
|
||||
RemoteIPInternalProxy 10.0.0.1/8 172.16.0.1/12 192.168.0.1/16
|
||||
</IfModule>
|
||||
|
||||
LogFormat "%a %l %u %t \"%r\" %>s %b \"%{Referer}i\" \"%{User-Agent}i\"" combined_proxy
|
||||
CustomLog "|/var/www/FreshRSS/cli/sensitive-log.sh" combined_proxy
|
||||
|
||||
<IfDefine OIDC_ENABLED>
|
||||
<IfModule !auth_openidc_module>
|
||||
Error "The auth_openidc_module is not available. Install it or unset environment variable OIDC_ENABLED."
|
||||
</IfModule>
|
||||
|
||||
# Workaround to be able to check whether an environment variable is set
|
||||
# See: https://serverfault.com/questions/1022233/using-ifdefine-with-environment-variables/1022234#1022234
|
||||
Define VStart "${"
|
||||
Define VEnd "}"
|
||||
|
||||
OIDCProviderMetadataURL ${OIDC_PROVIDER_METADATA_URL}
|
||||
OIDCClientID ${OIDC_CLIENT_ID}
|
||||
OIDCClientSecret ${OIDC_CLIENT_SECRET}
|
||||
|
||||
OIDCRedirectURI /i/oidc/
|
||||
OIDCCryptoPassphrase ${OIDC_CLIENT_CRYPTO_KEY}
|
||||
|
||||
Define "Test_${OIDC_REMOTE_USER_CLAIM}"
|
||||
<IfDefine Test_${VStart}OIDC_REMOTE_USER_CLAIM${VEnd}>
|
||||
OIDCRemoteUserClaim preferred_username
|
||||
</IfDefine>
|
||||
<IfDefine !Test_${VStart}OIDC_REMOTE_USER_CLAIM${VEnd}>
|
||||
OIDCRemoteUserClaim "${OIDC_REMOTE_USER_CLAIM}"
|
||||
</IfDefine>
|
||||
Define "Test_${OIDC_SCOPES}"
|
||||
<IfDefine Test_${VStart}OIDC_SCOPES${VEnd}>
|
||||
OIDCScope openid
|
||||
</IfDefine>
|
||||
<IfDefine !Test_${VStart}OIDC_SCOPES${VEnd}>
|
||||
OIDCScope "${OIDC_SCOPES}"
|
||||
</IfDefine>
|
||||
Define "Test_${OIDC_X_FORWARDED_HEADERS}"
|
||||
<IfDefine !Test_${VStart}OIDC_X_FORWARDED_HEADERS${VEnd}>
|
||||
OIDCXForwardedHeaders ${OIDC_X_FORWARDED_HEADERS}
|
||||
</IfDefine>
|
||||
|
||||
OIDCRefreshAccessTokenBeforeExpiry 30
|
||||
</IfDefine>
|
||||
|
||||
<Directory />
|
||||
AllowOverride None
|
||||
|
@ -27,6 +74,12 @@ ServerTokens OS
|
|||
</Directory>
|
||||
|
||||
<Directory /var/www/FreshRSS/p/i>
|
||||
ExpiresActive Off
|
||||
|
||||
<IfDefine OIDC_ENABLED>
|
||||
AuthType openid-connect
|
||||
Require valid-user
|
||||
</IfDefine>
|
||||
IncludeOptional /var/www/FreshRSS/p/i/.htaccess
|
||||
</Directory>
|
||||
|
||||
|
|
785
Docker/README.md
785
Docker/README.md
|
@ -1,135 +1,110 @@
|
|||
![Docker Cloud Automated build](https://img.shields.io/docker/cloud/automated/freshrss/freshrss.svg)
|
||||
![Docker Cloud Build Status](https://img.shields.io/docker/cloud/build/freshrss/freshrss.svg)
|
||||
![MicroBadger Size](https://img.shields.io/microbadger/image-size/freshrss/freshrss.svg)
|
||||
![Docker Pulls](https://img.shields.io/docker/pulls/freshrss/freshrss.svg)
|
||||
[![Liberapay donations](https://img.shields.io/liberapay/receives/FreshRSS.svg?logo=liberapay)](https://liberapay.com/FreshRSS/donate)
|
||||
|
||||
# Deploy FreshRSS with Docker
|
||||
|
||||
* See also <https://hub.docker.com/r/freshrss/freshrss/>
|
||||
FreshRSS is a self-hosted RSS feed aggregator.
|
||||
|
||||
* Official website: [`freshrss.org`](https://freshrss.org/)
|
||||
* Official Docker images: [`hub.docker.com/r/freshrss/freshrss`](https://hub.docker.com/r/freshrss/freshrss/)
|
||||
* Repository: [`github.com/FreshRSS/FreshRSS`](https://github.com/FreshRSS/FreshRSS/)
|
||||
* Documentation: [`freshrss.github.io/FreshRSS`](https://freshrss.github.io/FreshRSS/)
|
||||
* License: [GNU AGPL 3](https://www.gnu.org/licenses/agpl-3.0.html)
|
||||
|
||||
![FreshRSS logo](https://github.com/FreshRSS/FreshRSS/raw/edge/docs/img/FreshRSS-logo.png)
|
||||
|
||||
## Install Docker
|
||||
|
||||
See <https://docs.docker.com/get-docker/>
|
||||
|
||||
Example for Linux Debian / Ubuntu:
|
||||
|
||||
```sh
|
||||
curl -fsSL https://get.docker.com/ -o get-docker.sh
|
||||
sh get-docker.sh
|
||||
# Install default Docker Compose and automatically the corresponding version of Docker
|
||||
apt install docker-compose-v2
|
||||
```
|
||||
|
||||
## Quick run
|
||||
|
||||
## Create an isolated network
|
||||
Example running FreshRSS (or scroll down to the [Docker Compose](#docker-compose) section instead):
|
||||
|
||||
```sh
|
||||
docker network create freshrss-network
|
||||
```
|
||||
|
||||
## Recommended: use [Træfik](https://traefik.io/) reverse proxy
|
||||
|
||||
It is a good idea to use a reverse proxy on your host server, providing HTTPS.
|
||||
Here is the recommended configuration using automatic [Let’s Encrypt](https://letsencrypt.org/) HTTPS certificates and with a redirection from HTTP to HTTPS. See further below for alternatives.
|
||||
|
||||
```sh
|
||||
docker volume create traefik-letsencrypt
|
||||
docker volume create traefik-tmp
|
||||
|
||||
# Just change your e-mail address in the command below:
|
||||
docker run -d --restart unless-stopped --log-opt max-size=10m \
|
||||
-v traefik-letsencrypt:/etc/traefik/acme \
|
||||
-v traefik-tmp:/tmp \
|
||||
-v /var/run/docker.sock:/var/run/docker.sock:ro \
|
||||
--net freshrss-network \
|
||||
-p 80:80 \
|
||||
-p 443:443 \
|
||||
--name traefik traefik:1.7 --docker \
|
||||
--loglevel=info \
|
||||
--entryPoints='Name:http Address::80 Compress:true Redirect.EntryPoint:https' \
|
||||
--entryPoints='Name:https Address::443 Compress:true TLS TLS.MinVersion:VersionTLS12 TLS.SniStrict:true TLS.CipherSuites:TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384,TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA' \
|
||||
--defaultentrypoints=http,https --keeptrailingslash=true \
|
||||
--acme=true --acme.entrypoint=https --acme.onhostrule=true --acme.tlsChallenge \
|
||||
--acme.storage=/etc/traefik/acme/acme.json --acme.email=you@example.net
|
||||
```
|
||||
|
||||
See [more information about Docker and Let’s Encrypt in Træfik](https://docs.traefik.io/user-guide/docker-and-lets-encrypt/).
|
||||
|
||||
|
||||
## Run FreshRSS
|
||||
|
||||
Example using the built-in refresh cron job (see further below for alternatives).
|
||||
You must first chose a domain (DNS) or sub-domain, e.g. `freshrss.example.net`.
|
||||
|
||||
> **N.B.:** Default images are for x64 (Intel, AMD) platforms. For ARM (e.g. Raspberry Pi), use the `*-arm` tags. For other platforms, see the section *Build Docker image* further below.
|
||||
|
||||
```sh
|
||||
docker volume create freshrss-data
|
||||
docker volume create freshrss-extensions
|
||||
|
||||
# Remember to replace freshrss.example.net by your server address in the command below:
|
||||
docker run -d --restart unless-stopped --log-opt max-size=10m \
|
||||
-v freshrss-data:/var/www/FreshRSS/data \
|
||||
-v freshrss-extensions:/var/www/FreshRSS/extensions \
|
||||
-e 'CRON_MIN=4,34' \
|
||||
-p 8080:80 \
|
||||
-e TZ=Europe/Paris \
|
||||
--net freshrss-network \
|
||||
--label traefik.port=80 \
|
||||
--label traefik.frontend.rule='Host:freshrss.example.net' \
|
||||
--label traefik.frontend.headers.forceSTSHeader=true \
|
||||
--label traefik.frontend.headers.STSSeconds=31536000 \
|
||||
--name freshrss freshrss/freshrss
|
||||
-e 'CRON_MIN=1,31' \
|
||||
-v freshrss_data:/var/www/FreshRSS/data \
|
||||
-v freshrss_extensions:/var/www/FreshRSS/extensions \
|
||||
--name freshrss \
|
||||
freshrss/freshrss
|
||||
```
|
||||
|
||||
* Replace `TZ=Europe/Paris` by your [server timezone](http://php.net/timezones), or remove the line to use `UTC`.
|
||||
* If you cannot have FreshRSS at the root of a dedicated domain, update the command above according to the following model:
|
||||
`--label traefik.frontend.rule='Host:freshrss.example.net;PathPrefixStrip:/FreshRSS/' \`
|
||||
* You may remove the `--label traefik.*` lines if you do not use Træfik.
|
||||
* Add `-p 8080:80 \` if you want to expose FreshRSS locally, e.g. on port `8080`.
|
||||
* Replace `freshrss/freshrss` by a more specific tag (see below) such as `freshrss/freshrss:edge` for the development version, or `freshrss/freshrss:arm` for a Raspberry Pi version.
|
||||
|
||||
This already works with a built-in **SQLite** database (easiest), but more powerful databases are supported:
|
||||
|
||||
### [MySQL](https://hub.docker.com/_/mysql/) or [MariaDB](https://hub.docker.com/_/mariadb)
|
||||
|
||||
```sh
|
||||
# If you already have a MySQL or MariaDB instance running, just attach it to the FreshRSS network:
|
||||
docker network connect freshrss-network mysql
|
||||
|
||||
# Otherwise, start a new MySQL instance, remembering to change the passwords:
|
||||
docker volume create mysql-data
|
||||
docker run -d --restart unless-stopped --log-opt max-size=10m \
|
||||
-v mysql-data:/var/lib/mysql \
|
||||
-e MYSQL_ROOT_PASSWORD=rootpass \
|
||||
-e MYSQL_DATABASE=freshrss \
|
||||
-e MYSQL_USER=freshrss \
|
||||
-e MYSQL_PASSWORD=pass \
|
||||
--net freshrss-network \
|
||||
--name mysql mysql
|
||||
```
|
||||
|
||||
### [PostgreSQL](https://hub.docker.com/_/postgres/)
|
||||
|
||||
```sh
|
||||
# If you already have a PostgreSQL instance running, just attach it to the FreshRSS network:
|
||||
docker network connect freshrss-network postgres
|
||||
|
||||
# Otherwise, start a new PostgreSQL instance, remembering to change the passwords:
|
||||
docker volume create pgsql-data
|
||||
docker run -d --restart unless-stopped --log-opt max-size=10m \
|
||||
-v pgsql-data:/var/lib/postgresql/data \
|
||||
-e POSTGRES_DB=freshrss \
|
||||
-e POSTGRES_USER=freshrss \
|
||||
-e POSTGRES_PASSWORD=pass \
|
||||
--net freshrss-network \
|
||||
--name postgres postgres
|
||||
```
|
||||
* Exposing on port 8080
|
||||
* With a [server timezone](http://php.net/timezones) (default is `UTC`)
|
||||
* With an automatic cron job to refresh feeds
|
||||
* Saving FreshRSS data in a Docker volume `freshrss_data` and optional extensions in `freshrss_extensions`
|
||||
* Using the default image, which is the latest stable release
|
||||
|
||||
### Complete installation
|
||||
|
||||
Browse to your server <https://freshrss.example.net/> to complete the installation via the FreshRSS Web interface,
|
||||
or use the command line described below.
|
||||
|
||||
## Command line
|
||||
|
||||
See the [CLI documentation](../cli/README.md) for all the commands, which can be applied like:
|
||||
|
||||
```sh
|
||||
docker exec --user www-data freshrss cli/list-users.php
|
||||
```
|
||||
|
||||
Example of installation via command line:
|
||||
|
||||
```sh
|
||||
docker exec --user www-data freshrss cli/do-install.php --default_user freshrss
|
||||
|
||||
docker exec --user www-data freshrss cli/create-user.php --user freshrss --password freshrss
|
||||
```
|
||||
|
||||
> ℹ️ You have to replace `--user www-data` by `--user apache` when using our images based on Linux Alpine.
|
||||
|
||||
## Our Docker image variants
|
||||
|
||||
The [tags](https://hub.docker.com/r/freshrss/freshrss/tags) correspond to FreshRSS branches and versions:
|
||||
|
||||
* `:latest` (default) is the [latest stable release](https://github.com/FreshRSS/FreshRSS/releases/latest)
|
||||
* `:edge` is the rolling release, same than our [git `edge` branch](https://github.com/FreshRSS/FreshRSS/tree/edge)
|
||||
* `:x.y.z` tags correspond to [specific FreshRSS releases](https://github.com/FreshRSS/FreshRSS/releases), allowing you to target a precise version for deployment
|
||||
* `:x` tags track the latest release within a major version series. For instance, `:1` will update to include any `1.x` releases, but will exclude versions beyond `2.x`
|
||||
* `*-alpine` use Linux Alpine as base-image instead of Debian
|
||||
* Our Docker images are designed with multi-architecture support, accommodating a variety of Linux platforms including `linux/arm/v7`, `linux/arm64`, and `linux/amd64`.
|
||||
* For other platforms, see the [custom build section](#build-custom-docker-image)
|
||||
|
||||
### Linux: Debian vs. Alpine
|
||||
|
||||
Our default image is based on [Debian](https://www.debian.org/). We offer an alternative based on [Alpine](https://alpinelinux.org/) (with the `*-alpine` tag suffix).
|
||||
In [our tests](https://github.com/FreshRSS/FreshRSS/pull/2205) (2019), Alpine was slower,
|
||||
while Alpine is smaller on disk (and much faster to build),
|
||||
and with newer packages in general (Apache, PHP).
|
||||
|
||||
> ℹ️ For some rare systems, one variant might work but not the other, for instance due to kernel incompatibilities.
|
||||
|
||||
## Environment variables
|
||||
|
||||
* `TZ`: (default is `UTC`) A [server timezone](http://php.net/timezones)
|
||||
* `CRON_MIN`: (default is disabled) Define minutes for the built-in cron job to automatically refresh feeds (see below for more advanced options)
|
||||
* `DATA_PATH`: (default is empty, defined by `./constants.local.php` or `./constants.php`) Defines the path for writeable data.
|
||||
* `FRESHRSS_ENV`: (default is `production`) Enables additional development information if set to `development` (increases the level of logging and ensures that errors are displayed) (see below for more development options)
|
||||
* `COPY_LOG_TO_SYSLOG`: (default is `On`) Copy all the logs to syslog
|
||||
* `COPY_SYSLOG_TO_STDERR`: (default is `On`) Copy syslog to Standard Error so that it is visible in docker logs
|
||||
* `LISTEN`: (default is `80`) Modifies the internal Apache listening address and port, e.g. `0.0.0.0:8080` (for advanced users; useful for [Docker host networking](https://docs.docker.com/network/host/))
|
||||
* `FRESHRSS_INSTALL`: automatically pass arguments to command line `cli/do-install.php` (for advanced users; see example in Docker Compose section). Only executed at the very first run (so far), so if you make any change, you need to delete your `freshrss` service, `freshrss_data` volume, before running again.
|
||||
* `FRESHRSS_USER`: automatically pass arguments to command line `cli/create-user.php` (for advanced users; see example in Docker Compose section). Only executed at the very first run (so far), so if you make any change, you need to delete your `freshrss` service, `freshrss_data` volume, before running again.
|
||||
|
||||
## How to update
|
||||
|
||||
```sh
|
||||
# Rebuild an image (see build section above) or get a new online version:
|
||||
# Rebuild an image (see build section below) or get a new online version:
|
||||
docker pull freshrss/freshrss
|
||||
# And then
|
||||
docker stop freshrss
|
||||
|
@ -140,140 +115,20 @@ docker run ... --name freshrss freshrss/freshrss
|
|||
docker rm freshrss_old
|
||||
```
|
||||
|
||||
## Build custom Docker image
|
||||
|
||||
## [Docker tags](https://hub.docker.com/r/freshrss/freshrss/tags)
|
||||
Building your own Docker image is especially relevant for platforms not available on our Docker Hub,
|
||||
which is currently limited to `x64` (Intel, AMD), `arm32v7`, `arm64`.
|
||||
|
||||
The tags correspond to FreshRSS branches and versions:
|
||||
> ℹ️ If you try to run an image for the wrong platform, you might get an error message like *exec format error*.
|
||||
|
||||
* `:latest` (default) is the latest stable release
|
||||
* `:edge` is the rolling release
|
||||
* `:x.y.z` are specific FreshRSS releases
|
||||
* `:arm` or `:*-arm` are the ARM versions (e.g. for Raspberry Pi)
|
||||
|
||||
### Linux: Debian vs. Alpine
|
||||
|
||||
Our default image is based on [Debian](https://www.debian.org/). We offer an alternative based on [Alpine](https://alpinelinux.org/) (with the `*-alpine` tag suffix).
|
||||
In [our tests](https://github.com/FreshRSS/FreshRSS/pull/2205), Alpine is slower,
|
||||
while Alpine is [smaller on disk](https://hub.docker.com/r/freshrss/freshrss/tags) (and much faster to build).
|
||||
|
||||
|
||||
## Optional: Build Docker image of FreshRSS
|
||||
|
||||
Building your own Docker image is optional because online images can be fetched automatically.
|
||||
Note that prebuilt images are less recent and only available for x64 (Intel, AMD) platforms.
|
||||
Pick `#latest` (stable release) or `#edge` (rolling release) or a specific release number such as `#1.21.0` like:
|
||||
|
||||
```sh
|
||||
# First time only
|
||||
git clone https://github.com/FreshRSS/FreshRSS.git
|
||||
|
||||
cd FreshRSS/
|
||||
git pull
|
||||
docker build --pull --tag freshrss/freshrss -f Docker/Dockerfile .
|
||||
docker build --pull --tag freshrss/freshrss:latest -f Docker/Dockerfile-Alpine https://github.com/FreshRSS/FreshRSS.git#latest
|
||||
```
|
||||
|
||||
|
||||
## Command line
|
||||
|
||||
```sh
|
||||
docker exec --user www-data -it freshrss php ./cli/list-users.php
|
||||
```
|
||||
|
||||
See the [CLI documentation](../cli/) for all the other commands.
|
||||
You might have to replace `--user www-data` by `--user apache` when using our images based on Linux Alpine.
|
||||
|
||||
|
||||
## Debugging
|
||||
|
||||
```sh
|
||||
# See FreshRSS data if you use Docker volume
|
||||
docker volume inspect freshrss-data
|
||||
sudo ls /var/lib/docker/volumes/freshrss-data/_data/
|
||||
|
||||
# See Web server logs
|
||||
docker logs -f freshrss
|
||||
|
||||
# Enter inside FreshRSS docker container
|
||||
docker exec -it freshrss sh
|
||||
## See FreshRSS root inside the container
|
||||
ls /var/www/FreshRSS/
|
||||
```
|
||||
|
||||
|
||||
## Cron job to automatically refresh feeds
|
||||
|
||||
We recommend a refresh rate of about twice per hour (see *WebSub* / *PubSubHubbub* for real-time updates).
|
||||
There are no less than 3 options. Pick a single one.
|
||||
|
||||
### Option 1) Cron inside the FreshRSS Docker image
|
||||
|
||||
Easiest, built-in solution, also used already in the examples above
|
||||
(but your Docker instance will have a second process in the background, without monitoring).
|
||||
Just pass the environment variable `CRON_MIN` to your `docker run` command,
|
||||
containing a valid cron minute definition such as `'13,43'` (recommended) or `'*/20'`.
|
||||
Not passing the `CRON_MIN` environment variable – or setting it to empty string – will disable the cron daemon.
|
||||
|
||||
```sh
|
||||
docker run ... \
|
||||
-e 'CRON_MIN=13,43' \
|
||||
--name freshrss freshrss/freshrss
|
||||
```
|
||||
|
||||
### Option 2) Cron on the host machine
|
||||
|
||||
Traditional solution.
|
||||
Set a cron job up on your host machine, calling the `actualize_script.php` inside the FreshRSS Docker instance.
|
||||
Remember not pass the `CRON_MIN` environment variable to your Docker run, to avoid running the built-in cron daemon of option 1.
|
||||
|
||||
Example on Debian / Ubuntu: Create `/etc/cron.d/FreshRSS` with:
|
||||
|
||||
```text
|
||||
7,37 * * * * root docker exec --user www-data freshrss php ./app/actualize_script.php > /tmp/FreshRSS.log 2>&1
|
||||
```
|
||||
|
||||
### Option 3) Cron as another instance of the same FreshRSS Docker image
|
||||
|
||||
For advanced users. Offers good logging and monitoring with auto-restart on failure.
|
||||
Watch out to use the same run parameters than in your main FreshRSS instance, for database, networking, and file system.
|
||||
See cron option 1 for customising the cron schedule.
|
||||
|
||||
#### For the Debian image (default)
|
||||
|
||||
```sh
|
||||
docker run -d --restart unless-stopped --log-opt max-size=10m \
|
||||
-v freshrss-data:/var/www/FreshRSS/data \
|
||||
-v freshrss-extensions:/var/www/FreshRSS/extensions \
|
||||
-e 'CRON_MIN=17,47' \
|
||||
--net freshrss-network \
|
||||
--name freshrss_cron freshrss/freshrss \
|
||||
cron -f
|
||||
```
|
||||
|
||||
#### For the Debian image (default) using a custom cron.d fragment
|
||||
|
||||
This method gives you the most flexibility most flexiblity to
|
||||
execute various freshrss cli commands.
|
||||
|
||||
```sh
|
||||
docker run -d --restart unless-stopped --log-opt max-size=10m \
|
||||
-v freshrss-data:/var/www/FreshRSS/data \
|
||||
-v freshrss-extensions:/var/www/FreshRSS/extensions \
|
||||
-v ./freshrss_crontab:/etc/cron.d/freshrss \
|
||||
--net freshrss-network \
|
||||
--name freshrss_cron freshrss/freshrss \
|
||||
cron -f
|
||||
```
|
||||
|
||||
#### For the Alpine image
|
||||
|
||||
```sh
|
||||
docker run -d --restart unless-stopped --log-opt max-size=10m \
|
||||
-v freshrss-data:/var/www/FreshRSS/data \
|
||||
-v freshrss-extensions:/var/www/FreshRSS/extensions \
|
||||
-e 'CRON_MIN=27,57' \
|
||||
--net freshrss-network \
|
||||
--name freshrss_cron freshrss/freshrss:alpine \
|
||||
crond -f -d 6
|
||||
```
|
||||
> ℹ️ See an automated way to do that in our [Docker Compose](#docker-compose) section, leveraging a [git build context](https://docs.docker.com/build/building/context/#git-repositories).
|
||||
|
||||
## Development mode
|
||||
|
||||
|
@ -281,22 +136,101 @@ To contribute to FreshRSS development, you can use one of the Docker images to r
|
|||
while reading the source code from your local (git) directory, like the following example:
|
||||
|
||||
```sh
|
||||
cd /path-to-local/FreshRSS/
|
||||
docker run --rm -p 8080:80 -e TZ=Europe/Paris -e FRESHRSS_ENV=development \
|
||||
cd ./FreshRSS/
|
||||
docker run --rm \
|
||||
-p 8080:80 \
|
||||
-e FRESHRSS_ENV=development \
|
||||
-e TZ=Europe/Paris \
|
||||
-e 'CRON_MIN=1,31' \
|
||||
-v $(pwd):/var/www/FreshRSS \
|
||||
-v freshrss_data:/var/www/FreshRSS/data \
|
||||
--name freshrss \
|
||||
freshrss/freshrss:edge
|
||||
```
|
||||
|
||||
This will start a server on port 8080, based on your local PHP code, which will show the logs directly in your terminal.
|
||||
Press <kbd>Control</kbd>+<kbd>c</kbd> to exit.
|
||||
Press <kbd>Control</kbd>+<kbd>C</kbd> to exit.
|
||||
|
||||
The `FRESHRSS_ENV=development` environment variable increases the level of logging and ensures that errors are displayed.
|
||||
### Special development images
|
||||
|
||||
> ℹ️ See the [custom build section](#build-custom-docker-image) for an introduction
|
||||
|
||||
Two special Dockerfile are provided to reproduce the oldest and newest supported platforms (based on Alpine Linux).
|
||||
They need to be compiled manually:
|
||||
|
||||
```sh
|
||||
cd ./FreshRSS/
|
||||
docker build --pull --tag freshrss/freshrss:oldest -f Docker/Dockerfile-Oldest .
|
||||
docker build --pull --tag freshrss/freshrss:newest -f Docker/Dockerfile-Newest .
|
||||
```
|
||||
|
||||
## Supported databases
|
||||
|
||||
FreshRSS has a built-in [**SQLite** database](https://sqlite.org/) (easiest and good performance), but more powerful databases are also supported:
|
||||
|
||||
### Create an isolated network
|
||||
|
||||
```sh
|
||||
docker network create freshrss-network
|
||||
# Run FreshRSS with a `--net freshrss-network` parameter or use the following command:
|
||||
docker network connect freshrss-network freshrss
|
||||
```
|
||||
|
||||
### [PostgreSQL](https://hub.docker.com/_/postgres/)
|
||||
|
||||
```sh
|
||||
# If you already have a PostgreSQL instance running, just attach it to the FreshRSS network:
|
||||
docker network connect freshrss-network postgres
|
||||
|
||||
# Otherwise, start a new PostgreSQL instance, remembering to change the passwords:
|
||||
docker run -d --restart unless-stopped --log-opt max-size=10m \
|
||||
-v pgsql_data:/var/lib/postgresql/data \
|
||||
-e POSTGRES_DB=freshrss \
|
||||
-e POSTGRES_USER=freshrss \
|
||||
-e POSTGRES_PASSWORD=freshrss \
|
||||
--net freshrss-network \
|
||||
--name freshrss-db postgres
|
||||
```
|
||||
|
||||
In the FreshRSS setup, you will then specify the name of the container (`freshrss-db`) as the host for the database.
|
||||
|
||||
See also the section [Docker Compose with PostgreSQL](#docker-compose-with-postgresql) below.
|
||||
|
||||
### [MySQL](https://hub.docker.com/_/mysql/) or [MariaDB](https://hub.docker.com/_/mariadb)
|
||||
|
||||
```sh
|
||||
# If you already have a MySQL or MariaDB instance running, just attach it to the FreshRSS network:
|
||||
docker network connect freshrss-network mysql
|
||||
|
||||
# Otherwise, start a new MySQL instance, remembering to change the passwords:
|
||||
docker run -d --restart unless-stopped --log-opt max-size=10m \
|
||||
-v mysql_data:/var/lib/mysql \
|
||||
-e MYSQL_ROOT_PASSWORD=rootpass \
|
||||
-e MYSQL_DATABASE=freshrss \
|
||||
-e MYSQL_USER=freshrss \
|
||||
-e MYSQL_PASSWORD=freshrss \
|
||||
--net freshrss-network \
|
||||
--name freshrss-db mysql \
|
||||
```
|
||||
|
||||
In the FreshRSS setup, you will then specify the name of the container (`freshrss-db`) as the host for the database.
|
||||
|
||||
## More deployment options
|
||||
|
||||
### Provide default global settings
|
||||
|
||||
An optional configuration file can be mounted to `/var/www/FreshRSS/data/config.custom.php` to provide custom settings before the FreshRSS setup,
|
||||
on the model of [`config.default.php`](../config.default.php).
|
||||
|
||||
### Provide default user settings
|
||||
|
||||
An optional configuration file can be mounted to `/var/www/FreshRSS/data/config-user.default.php` to provide custom user settings before a user is created,
|
||||
on the model of [`config-user.default.php`](../config-user.default.php).
|
||||
|
||||
### Custom Apache configuration (advanced users)
|
||||
|
||||
Changes in Apache `.htaccess` files are applied when restarting the container.
|
||||
The FreshRSS Docker image uses the [Web server Apache](https://httpd.apache.org/) internally.
|
||||
Changes in [Apache `.htaccess` files](https://httpd.apache.org/docs/trunk/howto/htaccess.html) are applied when restarting the container.
|
||||
In particular, if you want FreshRSS to use HTTP-based login (instead of the easier Web form login), you can mount your own `./FreshRSS/p/i/.htaccess`:
|
||||
|
||||
```sh
|
||||
|
@ -316,35 +250,244 @@ AuthType Basic
|
|||
Require valid-user
|
||||
```
|
||||
|
||||
### Example with [docker-compose](https://docs.docker.com/compose/)
|
||||
### Modify the configuration of a running FreshRSS instance
|
||||
|
||||
A [docker-compose.yml](docker-compose.yml) file is given as an example, using PostgreSQL. In order to use it, you have to adapt:
|
||||
|
||||
* In the `postgresql` service:
|
||||
* `container_name` directive. Whatever you set this to will be the value you put in the "Host" field during the "Database Configuration" step of installation;
|
||||
* the `volumes` section. Be careful to keep the path `/var/lib/postgresql/data` for the container. If the path is wrong, you will not get any error but your db will be gone at the next run;
|
||||
* the `POSTGRES_PASSWORD` in the `.env` file;
|
||||
* the `POSTGRES_DB` in the `.env` file;
|
||||
* the `POSTGRES_USER` in the `.env` file;
|
||||
* In the `freshrss` service:
|
||||
* the `volumes` section;
|
||||
* options under the `labels` section are specific to [Træfik](https://traefik.io/), a reverse proxy. If you are not using it, feel free to delete this section. If you are using it, adapt accordingly to your config, especially the `traefik.frontend.rule` option.
|
||||
* the `environment` section to adapt the strategy to update feeds.
|
||||
* the `EXPOSED_PORT` variable in the `.env` file;
|
||||
|
||||
If you don't want to use the `.env` file you can also directly edit the `docker-compose.yml` file. It's highly recommended to change the password. If you don't change it, it will use the default option.
|
||||
|
||||
You can then launch the stack (FreshRSS + PostgreSQL) with:
|
||||
Some FreshRSS configuration parameters are stored in [`./FreshRSS/data/config.php`](../config.default.php)
|
||||
(e.g. `base_url`, `'environment' => 'development'`, database parameters, cURL options, etc.)
|
||||
and the following procedure can be used to modify them:
|
||||
|
||||
```sh
|
||||
docker-compose up -d
|
||||
# Verify the name of your FreshRSS volume, typically `freshrss_data`
|
||||
docker volume ls
|
||||
# Verify the path of your FreshRSS volume, typically `/var/lib/docker/volumes/freshrss_data/`
|
||||
docker volume inspect freshrss_data
|
||||
# Then edit your configuration file
|
||||
sudo nano /var/lib/docker/volumes/freshrss_data/_data/config.php
|
||||
```
|
||||
|
||||
### Alternative reverse proxy using [nginx](https://docs.nginx.com/nginx/admin-guide/web-server/reverse-proxy/)
|
||||
## Docker Compose
|
||||
|
||||
First, put variables such as passwords in your `.env` file, which can live where your `docker-compose.yml` should be. See [`example.env`](./freshrss/example.env).
|
||||
|
||||
```ini
|
||||
ADMIN_EMAIL=admin@example.net
|
||||
ADMIN_PASSWORD=freshrss
|
||||
ADMIN_API_PASSWORD=freshrss
|
||||
# Published port if running locally
|
||||
PUBLISHED_PORT=8080
|
||||
# Database credentials (not relevant if using default SQLite database)
|
||||
DB_HOST=freshrss-db
|
||||
DB_BASE=freshrss
|
||||
DB_PASSWORD=freshrss
|
||||
DB_USER=freshrss
|
||||
```
|
||||
|
||||
See [`docker-compose.yml`](./freshrss/docker-compose.yml)
|
||||
|
||||
```sh
|
||||
cd ./FreshRSS/Docker/freshrss/
|
||||
# Update
|
||||
docker compose pull
|
||||
# Run
|
||||
docker compose -f docker-compose.yml -f docker-compose-local.yml up -d --remove-orphans
|
||||
# Logs
|
||||
docker compose logs -f --timestamps
|
||||
# Stop
|
||||
docker compose down --remove-orphans
|
||||
```
|
||||
|
||||
Detailed (partial) example of Docker Compose for FreshRSS:
|
||||
|
||||
```yaml
|
||||
version: "2.4"
|
||||
|
||||
volumes:
|
||||
data:
|
||||
extensions:
|
||||
|
||||
services:
|
||||
freshrss:
|
||||
image: freshrss/freshrss:edge
|
||||
# Optional build section if you want to build the image locally:
|
||||
build:
|
||||
# Pick #latest (stable release) or #edge (rolling release) or a specific release like #1.21.0
|
||||
context: https://github.com/FreshRSS/FreshRSS.git#edge
|
||||
dockerfile: Docker/Dockerfile-Alpine
|
||||
container_name: freshrss
|
||||
restart: unless-stopped
|
||||
logging:
|
||||
options:
|
||||
max-size: 10m
|
||||
volumes:
|
||||
# Recommended volume for FreshRSS persistent data such as configuration and SQLite databases
|
||||
- data:/var/www/FreshRSS/data
|
||||
# Optional volume for storing third-party extensions
|
||||
- extensions:/var/www/FreshRSS/extensions
|
||||
# Optional file providing custom global settings (used before a FreshRSS install)
|
||||
- ./config.custom.php:/var/www/FreshRSS/data/config.custom.php
|
||||
# Optional file providing custom user settings (used before a new user is created)
|
||||
- ./config-user.custom.php:/var/www/FreshRSS/data/config-user.custom.php
|
||||
ports:
|
||||
# If you want to open a port 8080 on the local machine:
|
||||
- "8080:80"
|
||||
environment:
|
||||
# A timezone http://php.net/timezones (default is UTC)
|
||||
TZ: Europe/Paris
|
||||
# Cron job to refresh feeds at specified minutes
|
||||
CRON_MIN: '2,32'
|
||||
# 'development' for additional logs; default is 'production'
|
||||
FRESHRSS_ENV: development
|
||||
# Optional advanced parameter controlling the internal Apache listening port
|
||||
LISTEN: 0.0.0.0:80
|
||||
# Optional parameter, remove for automatic settings, set to 0 to disable,
|
||||
# or (if you use a proxy) to a space-separated list of trusted IP ranges
|
||||
# compatible with https://httpd.apache.org/docs/current/mod/mod_remoteip.html#remoteipinternalproxy
|
||||
# This impacts which IP address is logged (X-Forwarded-For or REMOTE_ADDR).
|
||||
# This also impacts external authentication methods;
|
||||
# see https://freshrss.github.io/FreshRSS/en/admins/09_AccessControl.html
|
||||
TRUSTED_PROXY: 172.16.0.1/12 192.168.0.1/16
|
||||
# Optional parameter, set to 1 to enable OpenID Connect (only available in our Debian image)
|
||||
# Requires more environment variables. See https://freshrss.github.io/FreshRSS/en/admins/16_OpenID-Connect.html
|
||||
OIDC_ENABLED: 0
|
||||
# Optional auto-install parameters (the Web interface install is recommended instead):
|
||||
# ⚠️ Parameters below are only used at the very first run (so far).
|
||||
# So if changes are made (or in .env file), first delete the service and volumes.
|
||||
# ℹ️ All the --db-* parameters can be omitted if using built-in SQLite database.
|
||||
FRESHRSS_INSTALL: |-
|
||||
--api-enabled
|
||||
--base-url ${BASE_URL}
|
||||
--db-base ${DB_BASE}
|
||||
--db-host ${DB_HOST}
|
||||
--db-password ${DB_PASSWORD}
|
||||
--db-type pgsql
|
||||
--db-user ${DB_USER}
|
||||
--default_user admin
|
||||
--language en
|
||||
FRESHRSS_USER: |-
|
||||
--api-password ${ADMIN_API_PASSWORD}
|
||||
--email ${ADMIN_EMAIL}
|
||||
--language en
|
||||
--password ${ADMIN_PASSWORD}
|
||||
--user admin
|
||||
```
|
||||
|
||||
### Docker Compose with PostgreSQL
|
||||
|
||||
Example including a [PostgreSQL](https://www.postgresql.org/) database.
|
||||
|
||||
See [`docker-compose-db.yml`](./freshrss/docker-compose-db.yml)
|
||||
|
||||
```sh
|
||||
cd ./FreshRSS/Docker/freshrss/
|
||||
# Update
|
||||
docker compose -f docker-compose.yml -f docker-compose-db.yml pull
|
||||
# Run
|
||||
docker compose -f docker-compose.yml -f docker-compose-db.yml -f docker-compose-local.yml up -d --remove-orphans
|
||||
# Logs
|
||||
docker compose -f docker-compose.yml -f docker-compose-db.yml logs -f --timestamps
|
||||
```
|
||||
|
||||
See also the section [Migrate database](#migrate-database) below to upgrade to a major PostgreSQL version with Docker Compose.
|
||||
|
||||
### Docker Compose for development
|
||||
|
||||
Use the local (git) FreshRSS source code instead of the one inside the Docker container,
|
||||
to avoid having to rebuild/restart at each change in the source code.
|
||||
|
||||
See [`docker-compose-development.yml`](./freshrss/docker-compose-development.yml)
|
||||
|
||||
```sh
|
||||
cd ./FreshRSS/Docker/freshrss/
|
||||
# Update
|
||||
git pull --ff-only --prune
|
||||
docker compose pull
|
||||
# Run
|
||||
docker compose -f docker-compose-development.yml -f docker-compose.yml -f docker-compose-local.yml up --remove-orphans
|
||||
# Stop with [Control]+[C] and purge
|
||||
docker compose down --remove-orphans --volumes
|
||||
```
|
||||
|
||||
> ℹ️ You can combine it with `-f docker-compose-db.yml` to spin a PostgreSQL database.
|
||||
|
||||
## Run in production
|
||||
|
||||
For production, it is a good idea to use a reverse proxy on your host server, providing HTTPS.
|
||||
A dedicated solution such as [Træfik](https://traefik.io/traefik/) is recommended
|
||||
(or see [alternative options below](#alternative-reverse-proxy-configurations)).
|
||||
|
||||
You must first chose a domain (DNS) or sub-domain, e.g. `freshrss.example.net`, and set it in your `.env` file:
|
||||
|
||||
```ini
|
||||
SERVER_DNS=freshrss.example.net
|
||||
```
|
||||
|
||||
### Use [Træfik](https://traefik.io/traefik/) reverse proxy
|
||||
|
||||
#### Option 1: server FreshRSS as a sub-domain
|
||||
|
||||
Use [`Host()` rule](https://doc.traefik.io/traefik/routing/routers/#rule), like:
|
||||
|
||||
```yml
|
||||
- traefik.http.routers.freshrss.rule=Host(`freshrss.example.net`)
|
||||
```
|
||||
|
||||
#### Option 2: serve FreshRSS as a sub-path
|
||||
|
||||
Use [`PathPrefix()` rules](https://doc.traefik.io/traefik/routing/routers/#rule) and [`StripPrefix` middleware](https://doc.traefik.io/traefik/middlewares/http/stripprefix/#stripprefix), like:
|
||||
|
||||
```yml
|
||||
- traefik.http.middlewares.freshrssM3.stripprefix.prefixes=/freshrss
|
||||
- traefik.http.routers.freshrss.middlewares=freshrssM3
|
||||
- traefik.http.routers.freshrss.rule=PathPrefix(`/freshrss`)
|
||||
```
|
||||
|
||||
#### Full example
|
||||
|
||||
Here is the recommended configuration using automatic [Let’s Encrypt](https://letsencrypt.org/) HTTPS certificates and with a redirection from HTTP to HTTPS.
|
||||
|
||||
See [`docker-compose-proxy.yml`](./freshrss/docker-compose-proxy.yml)
|
||||
|
||||
```sh
|
||||
cd ./FreshRSS/Docker/freshrss/
|
||||
# Update
|
||||
docker compose -f docker-compose.yml -f docker-compose-proxy.yml pull
|
||||
# Run
|
||||
docker compose -f docker-compose.yml -f docker-compose-proxy.yml up -d --remove-orphans
|
||||
# Logs
|
||||
docker compose -f docker-compose.yml -f docker-compose-proxy.yml logs -f --timestamps
|
||||
# Stop
|
||||
docker compose -f docker-compose.yml -f docker-compose-proxy.yml down --remove-orphans
|
||||
```
|
||||
|
||||
> ℹ️ You can combine it with `-f docker-compose-db.yml` to spin a PostgreSQL database.
|
||||
|
||||
See [more information about Docker and Let’s Encrypt in Træfik](https://doc.traefik.io/traefik/https/acme/).
|
||||
|
||||
## Alternative reverse proxy configurations
|
||||
|
||||
### Alternative reverse proxy using Apache
|
||||
|
||||
Here is an example of a configuration file for running FreshRSS behind an [Apache 2.4 reverse proxy](https://httpd.apache.org/docs/2.4/howto/reverse_proxy.html) (as a subdirectory).
|
||||
You need a working SSL configuration and the Apache modules `proxy`, `proxy_http` and `headers` installed (depends on your distribution) and enabled (`a2enmod proxy proxy_http headers`).
|
||||
|
||||
```apache
|
||||
ProxyPreserveHost On
|
||||
|
||||
<Location /freshrss/>
|
||||
ProxyPass http://127.0.0.1:8080/
|
||||
ProxyPassReverse http://127.0.0.1:8080/
|
||||
RequestHeader set X-Forwarded-Prefix "/freshrss"
|
||||
RequestHeader set X-Forwarded-Proto "https"
|
||||
Require all granted
|
||||
Options none
|
||||
</Location>
|
||||
```
|
||||
|
||||
### Alternative reverse proxy using nginx
|
||||
|
||||
#### Hosted in a subdirectory
|
||||
|
||||
Here is an example of configuration to run FreshRSS behind an Nginx reverse proxy (as subdirectory).
|
||||
Here is an example of configuration to run FreshRSS behind an [nginx reverse proxy](https://docs.nginx.com/nginx/admin-guide/web-server/reverse-proxy/) (as subdirectory).
|
||||
|
||||
```nginx
|
||||
upstream freshrss {
|
||||
|
@ -372,7 +515,7 @@ server {
|
|||
}
|
||||
|
||||
location /freshrss/ {
|
||||
proxy_pass http://freshrss;
|
||||
proxy_pass http://freshrss/;
|
||||
add_header X-Frame-Options SAMEORIGIN;
|
||||
add_header X-XSS-Protection "1; mode=block";
|
||||
proxy_redirect off;
|
||||
|
@ -437,20 +580,120 @@ server {
|
|||
}
|
||||
```
|
||||
|
||||
### Alternative reverse proxy using [Apache 2.4](https://httpd.apache.org/docs/2.4/howto/reverse_proxy.html)
|
||||
## Cron job to automatically refresh feeds
|
||||
|
||||
Here is an example of a configuration file for running FreshRSS behind an Apache reverse proxy (as a subdirectory).
|
||||
You need a working SSL configuration and the Apache modules `proxy`, `proxy_http` and `headers` installed (depends on your distribution) and enabled (```a2enmod proxy proxy_http headers```).
|
||||
We recommend a refresh rate of about twice per hour (see *WebSub* / *PubSubHubbub* for real-time updates).
|
||||
There are no less than 3 options. Pick a single one.
|
||||
|
||||
```apache
|
||||
ProxyPreserveHost On
|
||||
### Option 1) Cron inside the FreshRSS Docker image
|
||||
|
||||
<Location /freshrss/>
|
||||
ProxyPass http://127.0.0.1:8080/
|
||||
ProxyPassReverse http://127.0.0.1:8080/
|
||||
RequestHeader set X-Forwarded-Prefix "/freshrss"
|
||||
RequestHeader set X-Forwarded-Proto "https"
|
||||
Require all granted
|
||||
Options none
|
||||
</Location>
|
||||
Easiest, built-in solution, also used already in the examples above
|
||||
(but your Docker instance will have a second process in the background, without monitoring).
|
||||
Just pass the environment variable `CRON_MIN` to your `docker run` command,
|
||||
containing a valid cron minute definition such as `'13,43'` (recommended) or `'*/20'`.
|
||||
Not passing the `CRON_MIN` environment variable – or setting it to empty string – will disable the cron daemon.
|
||||
|
||||
```sh
|
||||
docker run ... \
|
||||
-e 'CRON_MIN=13,43' \
|
||||
--name freshrss freshrss/freshrss
|
||||
```
|
||||
|
||||
### Option 2) Cron on the host machine
|
||||
|
||||
Traditional solution.
|
||||
Set a cron job up on your host machine, calling the `actualize_script.php` inside the FreshRSS Docker instance.
|
||||
Remember not pass the `CRON_MIN` environment variable to your Docker run, to avoid running the built-in cron daemon of option 1.
|
||||
|
||||
Example on Debian / Ubuntu: Create `/etc/cron.d/FreshRSS` with:
|
||||
|
||||
```text
|
||||
7,37 * * * * root docker exec --user www-data freshrss php ./app/actualize_script.php > /tmp/FreshRSS.log 2>&1
|
||||
```
|
||||
|
||||
### Option 3) Cron as another instance of the same FreshRSS Docker image
|
||||
|
||||
For advanced users. Offers good logging and monitoring with auto-restart on failure.
|
||||
Watch out to use the same run parameters than in your main FreshRSS instance, for database, networking, and file system.
|
||||
See cron option 1 for customising the cron schedule.
|
||||
|
||||
#### For the Debian image (default)
|
||||
|
||||
```sh
|
||||
docker run -d --restart unless-stopped --log-opt max-size=10m \
|
||||
-v freshrss_data:/var/www/FreshRSS/data \
|
||||
-v freshrss_extensions:/var/www/FreshRSS/extensions \
|
||||
-e 'CRON_MIN=17,47' \
|
||||
--net freshrss-network \
|
||||
--name freshrss_cron freshrss/freshrss \
|
||||
cron -f
|
||||
```
|
||||
|
||||
#### For the Debian image (default) using a custom cron.d fragment
|
||||
|
||||
This method gives most flexibility to execute various FreshRSS CLI commands.
|
||||
|
||||
```sh
|
||||
docker run -d --restart unless-stopped --log-opt max-size=10m \
|
||||
-v freshrss_data:/var/www/FreshRSS/data \
|
||||
-v freshrss_extensions:/var/www/FreshRSS/extensions \
|
||||
-v ./freshrss_crontab:/etc/cron.d/freshrss \
|
||||
--net freshrss-network \
|
||||
--name freshrss_cron freshrss/freshrss \
|
||||
cron -f
|
||||
```
|
||||
|
||||
#### For the Alpine image
|
||||
|
||||
```sh
|
||||
docker run -d --restart unless-stopped --log-opt max-size=10m \
|
||||
-v freshrss_data:/var/www/FreshRSS/data \
|
||||
-v freshrss_extensions:/var/www/FreshRSS/extensions \
|
||||
-e 'CRON_MIN=27,57' \
|
||||
--net freshrss-network \
|
||||
--name freshrss_cron freshrss/freshrss:alpine \
|
||||
crond -f -d 6
|
||||
```
|
||||
|
||||
## Migrate database
|
||||
|
||||
Our [CLI](../cli/README.md) offers commands to back-up and migrate user databases,
|
||||
with `cli/db-backup.php` and `cli/db-restore.php` in particular.
|
||||
|
||||
Here is an example (assuming our [Docker Compose example](#docker-compose-with-postgresql))
|
||||
intended for migrating to a newer major version of PostgreSQL,
|
||||
but which can also be used to migrate between other databases (e.g. MySQL to PostgreSQL).
|
||||
|
||||
```sh
|
||||
# Stop FreshRSS container (Web server + cron) during maintenance
|
||||
docker compose down freshrss
|
||||
|
||||
# Optional additional pre-upgrade back-up using PostgreSQL own mechanism
|
||||
docker compose -f docker-compose-db.yml \
|
||||
exec freshrss-db pg_dump -U freshrss freshrss | gzip -9 > freshrss-postgres-backup.sql.gz
|
||||
# ------↑ Name of your PostgreSQL Docker container
|
||||
# -----------------------------↑ Name of your PostgreSQL user for FreshRSS
|
||||
# --------------------------------------↑ Name of your PostgreSQL database for FreshRSS
|
||||
|
||||
# Back-up all users’ respective tables to SQLite files
|
||||
docker compose -f docker-compose.yml -f docker-compose-db.yml \
|
||||
run --rm freshrss cli/db-backup.php
|
||||
|
||||
# Remove old database (PostgreSQL) container and its data volume
|
||||
docker compose -f docker-compose-db.yml \
|
||||
down --volumes freshrss-db
|
||||
|
||||
# Edit your Compose file to use new database (e.g. newest postgres:xx)
|
||||
nano docker-compose-db.yml
|
||||
|
||||
# Start new database (PostgreSQL) container and its new empty data volume
|
||||
docker compose -f docker-compose.yml -f docker-compose-db.yml \
|
||||
up -d freshrss-db
|
||||
|
||||
# Restore all users’ respective tables from SQLite files
|
||||
docker compose -f docker-compose.yml -f docker-compose-db.yml \
|
||||
run --rm freshrss cli/db-restore.php --delete-backup
|
||||
|
||||
# Restart a new FreshRSS container after maintenance
|
||||
docker compose -f docker-compose.yml -f docker-compose-db.yml up -d freshrss
|
||||
```
|
||||
|
|
|
@ -1,35 +0,0 @@
|
|||
version: "3"
|
||||
|
||||
services:
|
||||
freshrss-db:
|
||||
image: postgres:12-alpine
|
||||
container_name: freshrss-db
|
||||
hostname: freshrss-db
|
||||
restart: unless-stopped
|
||||
volumes:
|
||||
- db:/var/lib/postgresql/data
|
||||
environment:
|
||||
POSTGRES_USER: ${POSTGRES_USER:-freshrss}
|
||||
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-freshrss}
|
||||
POSTGRES_DB: ${POSTGRES_DB:-freshrss}
|
||||
|
||||
freshrss-app:
|
||||
image: freshrss/freshrss:latest
|
||||
container_name: freshrss-app
|
||||
hostname: freshrss-app
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "${EXPOSED_PORT:-8080}:80"
|
||||
depends_on:
|
||||
- freshrss-db
|
||||
volumes:
|
||||
- data:/var/www/FreshRSS/data
|
||||
- extensions:/var/www/FreshRSS/extensions
|
||||
environment:
|
||||
CRON_MIN: '*/20'
|
||||
TZ: Europe/Paris
|
||||
|
||||
volumes:
|
||||
db:
|
||||
data:
|
||||
extensions:
|
|
@ -1,6 +1,7 @@
|
|||
#!/bin/sh
|
||||
|
||||
php -f ./cli/prepare.php >/dev/null
|
||||
ln -snf "/usr/share/zoneinfo/$TZ" /etc/localtime
|
||||
echo "$TZ" >/etc/timezone
|
||||
|
||||
find /etc/php*/ -type f -name php.ini -exec sed -r -i "\\#^;?date.timezone#s#^.*#date.timezone = $TZ#" {} \;
|
||||
find /etc/php*/ -type f -name php.ini -exec sed -r -i "\\#^;?post_max_size#s#^.*#post_max_size = 32M#" {} \;
|
||||
|
@ -10,47 +11,59 @@ if [ -n "$LISTEN" ]; then
|
|||
find /etc/apache2/ -type f -name FreshRSS.Apache.conf -exec sed -r -i "\\#^Listen#s#^.*#Listen $LISTEN#" {} \;
|
||||
fi
|
||||
|
||||
if [ -n "$TRUSTED_PROXY" ]; then
|
||||
if [ "$TRUSTED_PROXY" = "0" ]; then
|
||||
# Disable RemoteIPHeader and RemoteIPInternalProxy
|
||||
find /etc/apache2/ -type f -name FreshRSS.Apache.conf -exec sed -r -i "/^\s*RemoteIP.*$/s/^/#/" {} \;
|
||||
else
|
||||
# Custom list for RemoteIPInternalProxy
|
||||
find /etc/apache2/ -type f -name FreshRSS.Apache.conf -exec sed -r -i "\\#^\s*RemoteIPInternalProxy#s#^.*#\tRemoteIPInternalProxy $TRUSTED_PROXY#" {} \;
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ -n "$OIDC_ENABLED" ] && [ "$OIDC_ENABLED" -ne 0 ]; then
|
||||
# Debian
|
||||
(which a2enmod >/dev/null && a2enmod -q auth_openidc) ||
|
||||
# Alpine
|
||||
(mv /etc/apache2/conf.d/mod-auth-openidc.conf.bak /etc/apache2/conf.d/mod-auth-openidc.conf && echo 'Enabling module auth_openidc.')
|
||||
if [ -n "$OIDC_SCOPES" ]; then
|
||||
# Compatibility with : as separator instead of space
|
||||
OIDC_SCOPES=$(echo "$OIDC_SCOPES" | tr ':' ' ')
|
||||
export OIDC_SCOPES
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ -n "$CRON_MIN" ]; then
|
||||
(
|
||||
echo "export TZ=$TZ"
|
||||
echo "export COPY_LOG_TO_SYSLOG=$COPY_LOG_TO_SYSLOG"
|
||||
echo "export COPY_SYSLOG_TO_STDERR=$COPY_SYSLOG_TO_STDERR"
|
||||
echo "export FRESHRSS_ENV=$FRESHRSS_ENV"
|
||||
) >/var/www/FreshRSS/Docker/env.txt
|
||||
awk -v RS='\0' '!/^(FRESHRSS_INSTALL|FRESHRSS_USER|HOME|PATH|PWD|SHLVL|TERM|_)=/ {gsub("\047", "\047\\\047\047"); print "export \047" $0 "\047"}' /proc/self/environ >/var/www/FreshRSS/Docker/env.txt
|
||||
sed </etc/crontab.freshrss.default \
|
||||
-r "s#^[^ ]+ #$CRON_MIN #" | crontab -
|
||||
fi
|
||||
|
||||
./cli/access-permissions.sh
|
||||
|
||||
php -f ./cli/prepare.php >/dev/null
|
||||
|
||||
if [ -n "$FRESHRSS_INSTALL" ]; then
|
||||
# shellcheck disable=SC2046
|
||||
php -f ./cli/do-install.php -- \
|
||||
$(echo "$FRESHRSS_INSTALL" | sed -r 's/[\r\n]+/\n/g' | paste -s -) \
|
||||
1>/tmp/out.txt 2>/tmp/err.txt
|
||||
$(echo "$FRESHRSS_INSTALL" | sed -r 's/[\r\n]+/\n/g' | paste -s -)
|
||||
EXITCODE=$?
|
||||
grep -v 'Remember to' /tmp/out.txt
|
||||
grep -v 'Please use' /tmp/err.txt 1>&2
|
||||
|
||||
if [ $EXITCODE -eq 3 ]; then
|
||||
echo 'ℹ️ FreshRSS already installed; no change performed.'
|
||||
elif [ $EXITCODE -eq 0 ]; then
|
||||
echo '✅ FreshRSS successfully installed.'
|
||||
else
|
||||
rm -f /tmp/out.txt /tmp/err.txt
|
||||
echo '❌ FreshRSS error during installation!'
|
||||
exit $EXITCODE
|
||||
fi
|
||||
|
||||
rm -f /tmp/out.txt /tmp/err.txt
|
||||
fi
|
||||
|
||||
if [ -n "$FRESHRSS_USER" ]; then
|
||||
# shellcheck disable=SC2046
|
||||
php -f ./cli/create-user.php -- \
|
||||
$(echo "$FRESHRSS_USER" | sed -r 's/[\r\n]+/\n/g' | paste -s -) \
|
||||
1>/tmp/out.txt 2>/tmp/err.txt
|
||||
$(echo "$FRESHRSS_USER" | sed -r 's/[\r\n]+/\n/g' | paste -s -)
|
||||
EXITCODE=$?
|
||||
grep -v 'Remember to' /tmp/out.txt
|
||||
cat /tmp/err.txt 1>&2
|
||||
|
||||
if [ $EXITCODE -eq 3 ]; then
|
||||
echo 'ℹ️ FreshRSS user already exists; no change performed.'
|
||||
|
@ -58,15 +71,11 @@ if [ -n "$FRESHRSS_USER" ]; then
|
|||
echo '✅ FreshRSS user successfully created.'
|
||||
./cli/list-users.php | xargs -n1 ./cli/actualize-user.php --user
|
||||
else
|
||||
rm -f /tmp/out.txt /tmp/err.txt
|
||||
echo '❌ FreshRSS error during the creation of a user!'
|
||||
exit $EXITCODE
|
||||
fi
|
||||
|
||||
rm -f /tmp/out.txt /tmp/err.txt
|
||||
fi
|
||||
|
||||
chown -R :www-data .
|
||||
chmod -R g+r . && chmod -R g+w ./data/
|
||||
./cli/access-permissions.sh
|
||||
|
||||
exec "$@"
|
||||
|
|
|
@ -0,0 +1,29 @@
|
|||
version: "2.4"
|
||||
|
||||
volumes:
|
||||
db:
|
||||
|
||||
services:
|
||||
|
||||
freshrss-db:
|
||||
image: postgres:16
|
||||
container_name: freshrss-db
|
||||
hostname: freshrss-db
|
||||
restart: unless-stopped
|
||||
logging:
|
||||
options:
|
||||
max-size: 10m
|
||||
volumes:
|
||||
- db:/var/lib/postgresql/data
|
||||
environment:
|
||||
POSTGRES_DB: ${DB_BASE:-freshrss}
|
||||
POSTGRES_USER: ${DB_USER:-freshrss}
|
||||
POSTGRES_PASSWORD: ${DB_PASSWORD:-freshrss}
|
||||
command:
|
||||
# Examples of PostgreSQL tuning.
|
||||
# https://wiki.postgresql.org/wiki/Tuning_Your_PostgreSQL_Server
|
||||
# When in doubt, skip and stick to default PostgreSQL settings.
|
||||
- -c
|
||||
- shared_buffers=1GB
|
||||
- -c
|
||||
- work_mem=32MB
|
|
@ -0,0 +1,7 @@
|
|||
version: "2.4"
|
||||
|
||||
services:
|
||||
|
||||
freshrss:
|
||||
volumes:
|
||||
- ../..:/var/www/FreshRSS
|
|
@ -0,0 +1,7 @@
|
|||
version: "2.4"
|
||||
|
||||
services:
|
||||
|
||||
freshrss:
|
||||
ports:
|
||||
- "${PUBLISHED_PORT:-8080}:${LISTEN:-80}"
|
|
@ -0,0 +1,62 @@
|
|||
version: "2.4"
|
||||
|
||||
volumes:
|
||||
traefik-letsencrypt:
|
||||
traefik-tmp:
|
||||
|
||||
services:
|
||||
|
||||
traefik:
|
||||
image: traefik:3.0
|
||||
container_name: traefik
|
||||
restart: unless-stopped
|
||||
logging:
|
||||
options:
|
||||
max-size: 10m
|
||||
ports:
|
||||
- 80:80
|
||||
- 443:443
|
||||
volumes:
|
||||
- /var/run/docker.sock:/var/run/docker.sock:ro
|
||||
- traefik-tmp:/tmp
|
||||
- traefik-letsencrypt:/etc/traefik/acme
|
||||
- ./traefik/tls.yaml:/etc/traefik/tls.yaml:ro
|
||||
command:
|
||||
- --global.sendAnonymousUsage
|
||||
- --accesslog=true
|
||||
- --api=false
|
||||
- --providers.docker=true
|
||||
- --providers.docker.exposedByDefault=false
|
||||
- --log.level=INFO
|
||||
- --entryPoints.http.address=:80
|
||||
- --entryPoints.https.address=:443
|
||||
- --entryPoints.http.http.redirections.entryPoint.to=https
|
||||
- --entryPoints.http.http.redirections.entryPoint.scheme=https
|
||||
- --certificatesResolvers.letsEncrypt.acme.storage=/etc/traefik/acme/acme.json
|
||||
- --certificatesResolvers.letsEncrypt.acme.email=${ADMIN_EMAIL}
|
||||
- --certificatesResolvers.letsEncrypt.acme.tlsChallenge=true
|
||||
- --providers.file.filename=/etc/traefik/tls.yaml
|
||||
labels:
|
||||
- traefik.enable=false
|
||||
|
||||
freshrss:
|
||||
environment:
|
||||
TRUSTED_PROXY: 172.16.0.1/12
|
||||
labels:
|
||||
- traefik.enable=true
|
||||
- traefik.http.middlewares.freshrssM1.compress=true
|
||||
- traefik.http.middlewares.freshrssM2.headers.browserXssFilter=true
|
||||
- traefik.http.middlewares.freshrssM2.headers.forceSTSHeader=true
|
||||
- traefik.http.middlewares.freshrssM2.headers.frameDeny=true
|
||||
- traefik.http.middlewares.freshrssM2.headers.referrerPolicy=no-referrer-when-downgrade
|
||||
- traefik.http.middlewares.freshrssM2.headers.stsSeconds=31536000
|
||||
- traefik.http.routers.freshrss.entryPoints=https
|
||||
- traefik.http.routers.freshrss.tls.certResolver=letsEncrypt
|
||||
- traefik.http.routers.freshrss.tls=true
|
||||
## Option 1: server FreshRSS as sub-domain
|
||||
- traefik.http.routers.freshrss.middlewares=freshrssM1,freshrssM2
|
||||
- traefik.http.routers.freshrss.rule=Host(`${SERVER_DNS}`)
|
||||
## Option 2: serve FreshRSS as sub-path
|
||||
# - traefik.http.middlewares.freshrssM3.stripprefix.prefixes=/freshrss
|
||||
# - traefik.http.routers.freshrss.middlewares=freshrssM1,freshrssM2,freshrssM3
|
||||
# - traefik.http.routers.freshrss.rule=PathPrefix(`/freshrss`)
|
|
@ -0,0 +1,28 @@
|
|||
version: "2.4"
|
||||
|
||||
volumes:
|
||||
data:
|
||||
extensions:
|
||||
|
||||
services:
|
||||
|
||||
freshrss:
|
||||
image: freshrss/freshrss:latest
|
||||
# Optional build section if you want to build the image locally:
|
||||
build:
|
||||
# Pick #latest (stable release) or #edge (rolling release) or a specific release like #1.21.0
|
||||
context: https://github.com/FreshRSS/FreshRSS.git#latest
|
||||
dockerfile: Docker/Dockerfile-Alpine
|
||||
container_name: freshrss
|
||||
hostname: freshrss
|
||||
restart: unless-stopped
|
||||
logging:
|
||||
options:
|
||||
max-size: 10m
|
||||
volumes:
|
||||
- data:/var/www/FreshRSS/data
|
||||
- extensions:/var/www/FreshRSS/extensions
|
||||
environment:
|
||||
TZ: Europe/Paris
|
||||
CRON_MIN: '3,33'
|
||||
TRUSTED_PROXY: 172.16.0.1/12 192.168.0.1/16
|
|
@ -0,0 +1,38 @@
|
|||
# Example of environment file for docker-compose
|
||||
# Copy this file into your own `.env` file
|
||||
|
||||
# ================================
|
||||
# FreshRSS
|
||||
# ================================
|
||||
|
||||
ADMIN_EMAIL=admin@example.net
|
||||
|
||||
# Published port for development or local use (optional)
|
||||
PUBLISHED_PORT=8080
|
||||
|
||||
# =========================================
|
||||
# For automatic FreshRSS install (optional)
|
||||
# =========================================
|
||||
|
||||
ADMIN_PASSWORD=freshrss
|
||||
ADMIN_API_PASSWORD=freshrss
|
||||
|
||||
# Address at which the FreshRSS instance will be reachable:
|
||||
BASE_URL=https://freshrss.example.net
|
||||
|
||||
# Database server (not relevant if using default SQLite)
|
||||
# Use the name of the Docker container if running on the same machine
|
||||
DB_HOST=freshrss-db
|
||||
|
||||
# ===========================================================
|
||||
# Database credentials (not relevant if using default SQLite)
|
||||
# ===========================================================
|
||||
|
||||
# Database to use
|
||||
DB_BASE=freshrss
|
||||
|
||||
# User in the freshrss database
|
||||
DB_USER=freshrss
|
||||
|
||||
# Password for the defined user
|
||||
DB_PASSWORD=freshrss
|
|
@ -0,0 +1,15 @@
|
|||
tls:
|
||||
options:
|
||||
default:
|
||||
minVersion: VersionTLS12
|
||||
sniStrict: true
|
||||
cipherSuites:
|
||||
- TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384
|
||||
- TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384
|
||||
- TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256
|
||||
- TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256
|
||||
- TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305
|
||||
- TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305
|
||||
- TLS_AES_128_GCM_SHA256
|
||||
- TLS_AES_256_GCM_SHA384
|
||||
- TLS_CHACHA20_POLY1305_SHA256
|
|
@ -1,21 +0,0 @@
|
|||
#!/bin/bash
|
||||
|
||||
cd ..
|
||||
FRESHRSS_VERSION=$(grep "'FRESHRSS_VERSION'" constants.php | cut -d "'" -f4)
|
||||
echo "$FRESHRSS_VERSION"
|
||||
|
||||
if [[ $DOCKERFILE_PATH == *-ARM ]]; then
|
||||
#TODO: Add --squash --platform arm options when Docker Hub deamon supports them
|
||||
docker build \
|
||||
--build-arg FRESHRSS_VERSION="$FRESHRSS_VERSION" \
|
||||
--build-arg SOURCE_BRANCH="$SOURCE_BRANCH" \
|
||||
--build-arg SOURCE_COMMIT="$SOURCE_COMMIT" \
|
||||
-f "$DOCKERFILE_PATH" -t "$IMAGE_NAME" .
|
||||
else
|
||||
#TODO: Add --squash option when Docker Hub deamon supports it
|
||||
docker build \
|
||||
--build-arg FRESHRSS_VERSION="$FRESHRSS_VERSION" \
|
||||
--build-arg SOURCE_BRANCH="$SOURCE_BRANCH" \
|
||||
--build-arg SOURCE_COMMIT="$SOURCE_COMMIT" \
|
||||
-f "$DOCKERFILE_PATH" -t "$IMAGE_NAME" .
|
||||
fi
|
|
@ -1,4 +0,0 @@
|
|||
#!/bin/bash
|
||||
|
||||
mv ../README.md ../README.en.md
|
||||
mv README.md ../
|
|
@ -1,11 +0,0 @@
|
|||
#!/bin/bash
|
||||
|
||||
if [[ $DOCKERFILE_PATH == *-ARM ]]; then
|
||||
# https://github.com/balena-io/qemu
|
||||
# Download a local copy of QEMU on Docker Hub build machine
|
||||
curl -LSs 'https://github.com/balena-io/qemu/releases/download/v3.0.0%2Bresin/qemu-3.0.0+resin-arm.tar.gz' | tar -xzv --strip-components=1 --wildcards '*/qemu-*'
|
||||
|
||||
# https://github.com/multiarch/qemu-user-static
|
||||
# Register qemu-*-static for all supported processors except the current one, but also remove all registered binfmt_misc before
|
||||
docker run --rm --privileged multiarch/qemu-user-static:register --reset
|
||||
fi
|
81
Makefile
81
Makefile
|
@ -5,6 +5,7 @@ ifndef TAG
|
|||
endif
|
||||
|
||||
PORT ?= 8080
|
||||
NETWORK ?= freshrss-network
|
||||
|
||||
ifdef NO_DOCKER
|
||||
PHP = $(shell which php)
|
||||
|
@ -20,8 +21,6 @@ endif
|
|||
|
||||
ifeq ($(findstring alpine,$(TAG)),alpine)
|
||||
DOCKERFILE=Dockerfile-Alpine
|
||||
else ifeq ($(findstring arm,$(TAG)),arm)
|
||||
DOCKERFILE=Dockerfile-QEMU-ARM
|
||||
else
|
||||
DOCKERFILE=Dockerfile
|
||||
endif
|
||||
|
@ -38,49 +37,72 @@ build: ## Build a Docker image
|
|||
|
||||
.PHONY: start
|
||||
start: ## Start the development environment (use Docker)
|
||||
docker network create --driver bridge $(NETWORK) || true
|
||||
$(foreach extension,$(extensions),$(eval volumes=$(volumes) --volume $(extension):/var/www/FreshRSS/extensions/$(notdir $(extension)):z))
|
||||
docker run \
|
||||
-it \
|
||||
--rm \
|
||||
--volume $(shell pwd):/var/www/FreshRSS:z \
|
||||
$(volumes) \
|
||||
--publish $(PORT):80 \
|
||||
--env FRESHRSS_ENV=development \
|
||||
--name freshrss-dev \
|
||||
--network $(NETWORK) \
|
||||
freshrss/freshrss:$(TAG)
|
||||
|
||||
.PHONY: stop
|
||||
stop: ## Stop FreshRSS container if any
|
||||
docker stop freshrss-dev
|
||||
docker stop freshrss-dev || true
|
||||
docker network rm $(NETWORK) || true
|
||||
|
||||
######################
|
||||
## Tests and linter ##
|
||||
######################
|
||||
.PHONY: test
|
||||
test: bin/phpunit ## Run the test suite
|
||||
$(PHP) ./bin/phpunit --bootstrap ./tests/bootstrap.php ./tests
|
||||
test: vendor/bin/phpunit ## Run the test suite
|
||||
$(PHP) vendor/bin/phpunit --bootstrap ./tests/bootstrap.php ./tests
|
||||
|
||||
.PHONY: lint
|
||||
lint: bin/phpcs ## Run the linter on the PHP files
|
||||
$(PHP) ./bin/phpcs . -p -s
|
||||
lint: vendor/bin/phpcs ## Run the linter on the PHP files
|
||||
$(PHP) vendor/bin/phpcs . -p -s
|
||||
|
||||
.PHONY: lint-fix
|
||||
lint-fix: bin/phpcbf ## Fix the errors detected by the linter
|
||||
$(PHP) ./bin/phpcbf . -p -s
|
||||
lint-fix: vendor/bin/phpcbf ## Fix the errors detected by the linter
|
||||
$(PHP) vendor/bin/phpcbf . -p -s
|
||||
|
||||
bin/phpunit:
|
||||
bin/composer:
|
||||
mkdir -p bin/
|
||||
wget -O bin/phpunit https://phar.phpunit.de/phpunit-9.5.2.phar
|
||||
echo 'bcf913565bc60dfb5356cf67cbbccec1d8888dbd595b0fbb8343a5019342c67c bin/phpunit' | sha256sum -c - || rm bin/phpunit
|
||||
wget 'https://raw.githubusercontent.com/composer/getcomposer.org/8af47a6fd4910073ea7580378d6252c708f83a06/web/installer' -O - -q | php -- --quiet --install-dir='./bin/' --filename='composer'
|
||||
|
||||
bin/phpcs:
|
||||
mkdir -p bin/
|
||||
wget -O bin/phpcs https://github.com/squizlabs/PHP_CodeSniffer/releases/download/3.5.5/phpcs.phar
|
||||
echo '4a2f6aff1b1f760216bb00c0b3070431131e3ed91307436bb1bfb252281a804a bin/phpcs' | sha256sum -c - || rm bin/phpcs
|
||||
vendor/bin/phpunit: bin/composer
|
||||
bin/composer install --prefer-dist --no-progress
|
||||
ln -s ../vendor/bin/phpunit bin/phpunit
|
||||
|
||||
bin/phpcbf:
|
||||
vendor/bin/phpcs: bin/composer
|
||||
bin/composer install --prefer-dist --no-progress
|
||||
ln -s ../vendor/bin/phpcs bin/phpcs
|
||||
|
||||
vendor/bin/phpcbf: bin/composer
|
||||
bin/composer install --prefer-dist --no-progress
|
||||
ln -s ../vendor/bin/phpcbf bin/phpcbf
|
||||
|
||||
bin/typos:
|
||||
mkdir -p bin/
|
||||
wget -O bin/phpcbf https://github.com/squizlabs/PHP_CodeSniffer/releases/download/3.5.5/phpcbf.phar
|
||||
echo '6f64fe00dee53fa7b256f63656dc0154f5964666fc7e535fac86d0078e7dea41 bin/phpcbf' | sha256sum -c - || rm bin/phpcbf
|
||||
cd bin ; \
|
||||
wget -q 'https://github.com/crate-ci/typos/releases/download/v1.17.0/typos-v1.17.0-x86_64-unknown-linux-musl.tar.gz' && \
|
||||
tar -xvf *.tar.gz './typos' && \
|
||||
chmod +x typos && \
|
||||
rm *.tar.gz ; \
|
||||
cd ..
|
||||
|
||||
node_modules/.bin/eslint:
|
||||
npm install
|
||||
|
||||
node_modules/.bin/rtlcss:
|
||||
npm install
|
||||
|
||||
vendor/bin/phpstan: bin/composer
|
||||
bin/composer install --prefer-dist --no-progress
|
||||
|
||||
##########
|
||||
## I18N ##
|
||||
|
@ -158,8 +180,8 @@ endif
|
|||
## TOOLS ##
|
||||
###########
|
||||
.PHONY: rtl
|
||||
rtl: ## Generate RTL CSS files
|
||||
rtlcss -d p/themes/ && find p/themes/ -type f -name '*.rtl.rtl.css' -delete
|
||||
rtl: node_modules/.bin/rtlcss ## Generate RTL CSS files
|
||||
npm run-script rtlcss
|
||||
|
||||
.PHONY: pot
|
||||
pot: ## Generate POT templates for docs
|
||||
|
@ -176,25 +198,28 @@ refresh: ## Refresh feeds by fetching new messages
|
|||
|
||||
# TODO: Add composer install
|
||||
.PHONY: composer-test
|
||||
composer-test:
|
||||
composer run-script test
|
||||
composer-test: vendor/bin/phpstan
|
||||
bin/composer run-script test
|
||||
|
||||
.PHONY: composer-fix
|
||||
composer-fix:
|
||||
composer run-script fix
|
||||
bin/composer run-script fix
|
||||
|
||||
# TODO: Add npm install
|
||||
.PHONY: npm-test
|
||||
npm-test:
|
||||
npm-test: node_modules/.bin/eslint
|
||||
npm test
|
||||
|
||||
.PHONY: npm-fix
|
||||
npm-fix:
|
||||
npm-fix: node_modules/.bin/eslint
|
||||
npm run fix
|
||||
|
||||
.PHONY: typos-test
|
||||
typos-test: bin/typos
|
||||
bin/typos
|
||||
|
||||
# TODO: Add shellcheck, shfmt, hadolint
|
||||
.PHONY: test-all
|
||||
test-all: composer-test npm-test
|
||||
test-all: composer-test npm-test typos-test
|
||||
|
||||
.PHONY: fix-all
|
||||
fix-all: composer-fix npm-fix
|
||||
|
|
137
README.fr.md
137
README.fr.md
|
@ -5,33 +5,49 @@
|
|||
|
||||
# FreshRSS
|
||||
|
||||
FreshRSS est un agrégateur de flux RSS à auto-héberger à l’image de [Leed](https://github.com/LeedRSS/Leed) ou de [Kriss Feed](https://tontof.net/kriss/feed/).
|
||||
FreshRSS est un agrégateur de flux RSS à auto-héberger.
|
||||
|
||||
Il se veut léger et facile à prendre en main tout en étant un outil puissant et paramétrable.
|
||||
|
||||
Il permet de gérer plusieurs utilisateurs, dispose d’un mode de lecture anonyme, et supporte les étiquettes personnalisées.
|
||||
Il y a une API pour les clients (mobiles), ainsi qu’une [interface en ligne de commande](cli/README.md).
|
||||
|
||||
Grâce au standard [WebSub](https://www.w3.org/TR/websub/) (anciennement [PubSubHubbub](https://github.com/pubsubhubbub/PubSubHubbub)),
|
||||
FreshRSS est capable de recevoir des notifications push instantanées depuis les sources compatibles, telles [Mastodon](https://joinmastodon.org), [Friendica](https://friendi.ca), [WordPress](https://wordpress.org/plugins/pubsubhubbub/), Blogger, FeedBurner, etc.
|
||||
Grâce au standard [WebSub](https://freshrss.github.io/FreshRSS/fr/users/08_PubSubHubbub.html),
|
||||
FreshRSS est capable de recevoir des notifications push instantanées depuis les sources compatibles, [Friendica](https://friendi.ca), [WordPress](https://wordpress.org/plugins/pubsubhubbub/), Blogger, Medium, etc.
|
||||
|
||||
Enfin, il permet l’ajout d’[extensions](#extensions) pour encore plus de personnalisation.
|
||||
FreshRSS supporte nativement le [moissonnage du Web (Web Scraping)](https://freshrss.github.io/FreshRSS/en/users/11_website_scraping.html) basique,
|
||||
basé sur [XPath](https://www.w3.org/TR/xpath-10/), pour les sites Web sans flux RSS / Atom.
|
||||
Supporte aussi les documents JSON.
|
||||
|
||||
Les demandes de fonctionnalités, rapports de bugs, et autres contributions sont les bienvenues. Privilégiez pour cela des [demandes sur GitHub](https://github.com/FreshRSS/FreshRSS/issues).
|
||||
Nous sommes une communauté amicale.
|
||||
FreshRSS permet de [repartager des sélections d’articles par HTML, RSS, et OPML](https://freshrss.github.io/FreshRSS/en/users/user_queries.html).
|
||||
|
||||
Plusieurs [méthodes de connexion](https://freshrss.github.io/FreshRSS/en/admins/09_AccessControl.html) sont supportées : formulaire Web (avec un mode anonyme), Authentification HTTP (compatible avec proxy), OpenID Connect.
|
||||
|
||||
Enfin, FreshRSS permet l’ajout d’[extensions](#extensions) pour encore plus de personnalisation.
|
||||
|
||||
* Site officiel : <https://freshrss.org>
|
||||
* Démo : <http://demo.freshrss.org/>
|
||||
* Démo : <https://demo.freshrss.org>
|
||||
* Licence : [GNU AGPL 3](https://www.gnu.org/licenses/agpl-3.0.fr.html)
|
||||
|
||||
![Logo de FreshRSS](docs/img/FreshRSS-logo.png)
|
||||
|
||||
# Avertissements
|
||||
## Contributions
|
||||
|
||||
FreshRSS n’est fourni avec aucune garantie.
|
||||
Les demandes de fonctionnalités, rapports de bugs, et autres contributions sont les bienvenues. Privilégiez pour cela des [demandes sur GitHub](https://github.com/FreshRSS/FreshRSS/issues).
|
||||
Nous sommes une communauté amicale.
|
||||
|
||||
Pour faciliter les contributions, [l’option suivante](.devcontainer/README.md) est disponible :
|
||||
|
||||
[![Ouvrir dans GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://github.com/codespaces/new?hide_repo_select=true&ref=edge&repo=6322699)
|
||||
|
||||
## Capture d’écran
|
||||
|
||||
![Capture d’écran de FreshRSS](docs/img/FreshRSS-screenshot.png)
|
||||
|
||||
## Avertissements
|
||||
|
||||
FreshRSS n’est fourni avec aucune garantie.
|
||||
|
||||
# [Documentation](https://freshrss.github.io/FreshRSS/fr/)
|
||||
|
||||
* La [documentation utilisateurs](https://freshrss.github.io/FreshRSS/fr/users/02_First_steps.html) pour découvrir les fonctionnalités de FreshRSS.
|
||||
|
@ -39,33 +55,31 @@ FreshRSS n’est fourni avec aucune garantie.
|
|||
* La [documentation développeurs](https://freshrss.github.io/FreshRSS/fr/developers/01_First_steps.html) pour savoir comment contribuer et mieux comprendre le code source de FreshRSS.
|
||||
* Le [guide de contribution](https://freshrss.github.io/FreshRSS/fr/contributing.html) pour nous aider à développer FreshRSS.
|
||||
|
||||
# Prérequis
|
||||
## Prérequis
|
||||
|
||||
* Un navigateur Web récent tel que Firefox / IceCat, Edge, Chromium / Chrome, Opera, Safari.
|
||||
* Fonctionne aussi sur mobile (sauf certaines fonctionnalités)
|
||||
* Serveur modeste, par exemple sous Linux ou Windows
|
||||
* Fonctionne même sur un Raspberry Pi 1 avec des temps de réponse < 1s (testé sur 150 flux, 22k articles)
|
||||
* Serveur Web Apache2 (recommandé), ou nginx, lighttpd (non testé sur les autres)
|
||||
* PHP 7.0+
|
||||
* Requis : [cURL](https://www.php.net/curl), [DOM](https://www.php.net/dom), [JSON](https://www.php.net/json), [XML](https://www.php.net/xml), [session](https://www.php.net/session), [ctype](https://www.php.net/ctype), et [PDO_MySQL](https://www.php.net/pdo-mysql) ou [PDO_SQLite](https://www.php.net/pdo-sqlite) ou [PDO_PGSQL](https://www.php.net/pdo-pgsql)
|
||||
* Recommandés : [GMP](https://www.php.net/gmp) (pour accès API sur plateformes < 64 bits), [IDN](https://www.php.net/intl.idn) (pour les noms de domaines internationalisés), [mbstring](https://www.php.net/mbstring) (pour le texte Unicode), [iconv](https://www.php.net/iconv) (pour conversion d’encodages), [ZIP](https://www.php.net/zip) (pour import/export), [zlib](https://www.php.net/zlib) (pour les flux compressés)
|
||||
* MySQL 5.5.3+ ou équivalent MariaDB, ou SQLite 3.7.4+, ou PostgreSQL 9.5+
|
||||
* Serveur Web Apache2.4+ (recommandé), ou nginx, lighttpd (non testé sur les autres)
|
||||
* PHP 7.4+
|
||||
* Extensions requises : [cURL](https://www.php.net/curl), [DOM](https://www.php.net/dom), [JSON](https://www.php.net/json), [XML](https://www.php.net/xml), [session](https://www.php.net/session), [ctype](https://www.php.net/ctype)
|
||||
* Extensions recommandées : [PDO_SQLite](https://www.php.net/pdo-sqlite) (pour l’export/import), [GMP](https://www.php.net/gmp) (pour accès API sur plateformes < 64 bits), [IDN](https://www.php.net/intl.idn) (pour les noms de domaines internationalisés), [mbstring](https://www.php.net/mbstring) (pour le texte Unicode), [iconv](https://www.php.net/iconv) (pour conversion d’encodages), [ZIP](https://www.php.net/zip) (pour import/export), [zlib](https://www.php.net/zlib) (pour les flux compressés)
|
||||
* Extension pour base de données : [PDO_PGSQL](https://www.php.net/pdo-pgsql) ou [PDO_SQLite](https://www.php.net/pdo-sqlite) ou [PDO_MySQL](https://www.php.net/pdo-mysql)
|
||||
* PostgreSQL 9.5+ ou SQLite ou MySQL 5.5.3+ ou MariaDB 5.5+
|
||||
|
||||
|
||||
# Téléchargement
|
||||
# [Installation](https://freshrss.github.io/FreshRSS/fr/users/01_Installation.html)
|
||||
|
||||
Si vous préférez que votre FreshRSS soit stable, vous devriez télécharger la dernière version. De nouvelles versions sont publiées tous les 2 ou 3 mois. Voir la [liste des versions](https://github.com/FreshRSS/FreshRSS/releases).
|
||||
|
||||
Si vous voulez une publication continue (rolling release) avec les dernières nouveautés, ou bien aider à tester ou développer la future version stable, vous pouvez utiliser [la branche edge](https://github.com/FreshRSS/FreshRSS/tree/edge/).
|
||||
|
||||
|
||||
# [Installation](https://freshrss.github.io/FreshRSS/fr/users/01_Installation.html)
|
||||
|
||||
## Installation automatisée
|
||||
|
||||
* [![Docker](https://www.docker.com/sites/default/files/horizontal.png)](./Docker/)
|
||||
* [<img src="https://www.docker.com/wp-content/uploads/2022/03/horizontal-logo-monochromatic-white.png" width="200" alt="Docker" />](./Docker/)
|
||||
* [![YunoHost](https://install-app.yunohost.org/install-with-yunohost.png)](https://install-app.yunohost.org/?app=freshrss)
|
||||
* [![Cloudron](https://cloudron.io/img/button.svg)](https://cloudron.io/button.html?app=org.freshrss.cloudronapp)
|
||||
* [![PikaPods](https://www.pikapods.com/static/run-button-34.svg)](https://www.pikapods.com/pods?run=freshrss)
|
||||
|
||||
## Installation manuelle
|
||||
|
||||
|
@ -80,7 +94,7 @@ Si vous voulez une publication continue (rolling release) avec les dernières no
|
|||
|
||||
Plus d’informations sur l’installation et la configuration serveur peuvent être trouvées dans [notre documentation](https://freshrss.github.io/FreshRSS/fr/users/01_Installation.html).
|
||||
|
||||
### Exemple d’installation complète sur Linux Debian/Ubuntu
|
||||
## Exemple d’installation complète sur Linux Debian/Ubuntu
|
||||
|
||||
```sh
|
||||
# Si vous utilisez le serveur Web Apache (sinon il faut un autre serveur Web)
|
||||
|
@ -102,16 +116,17 @@ sudo apt-get install git
|
|||
sudo git clone https://github.com/FreshRSS/FreshRSS.git
|
||||
cd FreshRSS
|
||||
|
||||
# Si vous souhaitez utiliser la dernière version stable de FreshRSS
|
||||
sudo git checkout $(git describe --tags --abbrev=0)
|
||||
# La branche par défault “edge” est la celle de la publication continue,
|
||||
# mais vous pouvez changer de branche pour “latest” si vous préférez les versions stables de FreshRSS
|
||||
sudo git checkout latest
|
||||
|
||||
# Mettre les droits d’accès pour le serveur Web
|
||||
sudo chown -R :www-data . && sudo chmod -R g+r . && sudo chmod -R g+w ./data/
|
||||
# Si vous souhaitez permettre les mises à jour par l’interface Web
|
||||
sudo chmod -R g+w .
|
||||
sudo cli/access-permissions.sh
|
||||
# Si vous souhaitez permettre les mises à jour par l’interface Web (un peu moins sûr)
|
||||
sudo chown www-data:www-data -R .
|
||||
|
||||
# Publier FreshRSS dans votre répertoire HTML public
|
||||
sudo ln -s /usr/share/FreshRSS/p /var/www/html/FreshRSS
|
||||
[ ! -e "/var/www/html/FreshRSS" ] && sudo ln -s /usr/share/FreshRSS/p /var/www/html/FreshRSS || echo "/var/www/html/FreshRSS existe déjà"
|
||||
# Naviguez vers http://example.net/FreshRSS pour terminer l’installation
|
||||
# (Si vous le faite depuis localhost, vous pourrez avoir à ajuster le réglage de votre adresse publique)
|
||||
# ou utilisez l’interface en ligne de commande
|
||||
|
@ -119,7 +134,7 @@ sudo ln -s /usr/share/FreshRSS/p /var/www/html/FreshRSS
|
|||
# Mettre à jour FreshRSS vers une nouvelle version par git
|
||||
cd /usr/share/FreshRSS
|
||||
sudo git pull
|
||||
sudo chown -R :www-data . && sudo chmod -R g+r . && sudo chmod -R g+w ./data/
|
||||
sudo cli/access-permissions.sh
|
||||
```
|
||||
|
||||
Voir la [documentation de la ligne de commande](cli/README.md) pour plus de détails.
|
||||
|
@ -152,7 +167,7 @@ Créer `/etc/cron.d/FreshRSS` avec :
|
|||
7,37 * * * * www-data php -f /usr/share/FreshRSS/app/actualize_script.php > /tmp/FreshRSS.log 2>&1
|
||||
```
|
||||
|
||||
## Conseils
|
||||
# Conseils
|
||||
|
||||
* Pour une meilleure sécurité, faites en sorte que seul le répertoire `./p/` soit accessible depuis le Web, par exemple en faisant pointer un sous-domaine sur le répertoire `./p/`.
|
||||
* En particulier, les données personnelles se trouvent dans le répertoire `./data/`.
|
||||
|
@ -172,26 +187,14 @@ Créer `/etc/cron.d/FreshRSS` avec :
|
|||
* Il faut conserver vos fichiers `./data/config.php` ainsi que `./data/users/*/config.php`
|
||||
* Vous pouvez exporter votre liste de flux au format OPML soit depuis l’interface Web, soit [en ligne de commande](cli/README.md)
|
||||
|
||||
Pour sauvegarder les articles eux-mêmes :
|
||||
|
||||
## Dans le cas où vous utilisez MySQL
|
||||
|
||||
Vous pouvez utiliser [phpMyAdmin](https://www.phpmyadmin.net) ou les outils de MySQL :
|
||||
|
||||
```sh
|
||||
mysqldump --skip-comments --disable-keys --user=<db_user> --password --host <db_host> --result-file=freshrss.dump.sql --databases <freshrss_db>
|
||||
```
|
||||
|
||||
## Pour toutes les bases supportées
|
||||
|
||||
Vous pouvez utiliser la [ligne de commande](cli/README.md) pour exporter votre base de données vers une base de données au format SQLite :
|
||||
Pour sauvegarder les articles eux-mêmes, vous pouvez utiliser la [ligne de commande](cli/README.md) pour exporter votre base de données vers une base de données au format SQLite :
|
||||
|
||||
```sh
|
||||
./cli/export-sqlite-for-user.php --user <username> --filename </path/to/db.sqlite>
|
||||
```
|
||||
|
||||
> Il est impératif que le nom du fichier contenant la base de données ait une extension `sqlite`.
|
||||
Si ce n'est pas le cas, la commande ne fonctionnera pas correctement.
|
||||
Si ce n’est pas le cas, la commande ne fonctionnera pas correctement.
|
||||
|
||||
Vous pouvez encore utiliser la [ligne de commande](cli/README.md) pour importer la base de données au format SQLite dans votre base de données:
|
||||
|
||||
|
@ -199,11 +202,11 @@ Vous pouvez encore utiliser la [ligne de commande](cli/README.md) pour importer
|
|||
./cli/import-sqlite-for-user.php --user <username> --filename </path/to/db.sqlite>
|
||||
```
|
||||
|
||||
> Encore une fois, il est impératif que le nom du fichier contenant la base de données ait une extension `sqlite`. Si ce n'est pas le cas, la commande ne fonctionnera pas correctement.
|
||||
> Encore une fois, il est impératif que le nom du fichier contenant la base de données ait une extension `sqlite`. Si ce n’est pas le cas, la commande ne fonctionnera pas correctement.
|
||||
|
||||
Le processus d'import/export à l'aide d'une base de données SQLite est utile quand vous devez :
|
||||
Le processus d’import/export à l’aide d’une base de données SQLite est utile quand vous devez :
|
||||
|
||||
* exporter complètement les données d'un utilisateur,
|
||||
* exporter complètement les données d’un utilisateur,
|
||||
* sauvegarder votre service,
|
||||
* migrer votre service sur un autre serveur,
|
||||
* changer de type de base de données,
|
||||
|
@ -217,36 +220,39 @@ Voir le [dépôt dédié à ces extensions](https://github.com/FreshRSS/Extensio
|
|||
|
||||
# APIs et applications natives
|
||||
|
||||
FreshRSS supporte l’accès depuis des applications natives pour Linux, Android, iOS, et OS X, grâce à deux APIs distinctes :
|
||||
FreshRSS supporte l’accès depuis des applications natives pour Linux, Android, iOS, Windows et macOS, grâce à deux APIs distinctes :
|
||||
[l’API compatible Google Reader](https://freshrss.github.io/FreshRSS/fr/users/06_Mobile_access.html) (la meilleure),
|
||||
et [l’API Fever](https://freshrss.github.io/FreshRSS/fr/users/06_Fever_API.html) (moindres fonctionnalités et moins efficace).
|
||||
|
||||
| App | Plateforme | Logiciel libre | Maintenu & Dévelopé | API | Mode hors-ligne | Sync rapide | Récupère plus d’articles dans les vues individuelles | Récupère les articles lus | Favoris | Étiquettes | Podcasts | Gestion des flux |
|
||||
|:--------------------------------------------------------------------------------------|:-----------:|:-------------------------------------------------------------:|:----------------------:|:----------------:|:-------------:|:---------:|:------------------------------:|:-------------------:|:----------:|:------:|:--------:|:------------:|
|
||||
| [News+](https://play.google.com/store/apps/details?id=com.noinnion.android.newsplus) with [Google Reader extension](https://github.com/noinnion/newsplus/blob/master/apk/GoogleReaderCloneExtension_101.apk) | Android | [Partially](https://github.com/noinnion/newsplus/blob/master/extensions/GoogleReaderCloneExtension/src/com/noinnion/android/newsplus/extension/google_reader/) | 2015 | GReader | ✔️ | ⭐⭐⭐ | ✔️ | ✔️ | ✔️ | ✔️ | ✔️ | ✔️ |
|
||||
| [News+](https://github.com/noinnion/newsplus/blob/master/apk/NewsPlus_202.apk) with [Google Reader extension](https://github.com/noinnion/newsplus/blob/master/apk/GoogleReaderCloneExtension_101.apk) | Android | [Partially](https://github.com/noinnion/newsplus/blob/master/extensions/GoogleReaderCloneExtension/src/com/noinnion/android/newsplus/extension/google_reader/) | 2015 | GReader | ✔️ | ⭐⭐⭐ | ✔️ | ✔️ | ✔️ | ✔️ | ✔️ | ✔️ |
|
||||
| [FeedMe](https://play.google.com/store/apps/details?id=com.seazon.feedme) | Android | ➖ | ✔️✔️ | GReader | ✔️ | ⭐⭐ | ➖ | ➖ | ✔️ | ✓ | ✔️ | ✔️ |
|
||||
| [RSS Guard](https://github.com/martinrotter/rssguard) | Windows, GNU/Linux, MacOS, OS/2 | ✔️ | ✔️✔️ | GReader | ✔️ | ⭐ | ➖ | ✔️ | ✔️ | ✔️ | ✔️ | ➖ |
|
||||
| [EasyRSS](https://github.com/Alkarex/EasyRSS) | Android | [✔️](https://github.com/Alkarex/EasyRSS) | ✔️ | GReader | Bug | ⭐⭐ | ➖ | ➖ | ✔️ | ➖ | ➖ | ➖ |
|
||||
| [FocusReader](https://play.google.com/store/apps/details?id=allen.town.focus.reader) | Android | ➖ | ✔️✔️ | GReader | ✔️ | ⭐⭐⭐ | ➖ | ➖ | ✔️ | ➖ | ✓ | ✔️ |
|
||||
| [Readrops](https://github.com/readrops/Readrops) | Android | [✔️](https://github.com/readrops/Readrops) | ✔️✔️ | GReader | ✔️ | ⭐⭐⭐ | ➖ | ➖ | ➖ | ➖ | ➖ | ✔️ |
|
||||
| [ChristopheHenry](https://git.feneas.org/christophehenry/freshrss-android) | Android | [✔️](https://git.feneas.org/christophehenry/freshrss-android) | En développement | GReader | ✔️ | ⭐⭐ | ➖ | ✔️ | ✔️ | ➖ | ➖ | ➖ |
|
||||
| [Fluent Reader](https://hyliu.me/fluent-reader/) | Windows, Linux, MacOS| [✔️](https://github.com/yang991178/fluent-reader) | ✔️✔️ | Fever | ✔️ | ⭐ | ➖ | ✔️ | ✔️ | ➖ | ➖ | ➖ |
|
||||
| [FeedReader](https://jangernert.github.io/FeedReader/) | GNU/Linux | [✔️](https://jangernert.github.io/FeedReader/) | ✔️ | GReader | ✔️ | ⭐⭐ | ➖ | ✔️ | ✔️ | ➖ | ✔️ | ✔️ |
|
||||
| [NewsFlash](https://gitlab.com/news-flash/news_flash_gtk) | GNU/Linux | [✔️](https://gitlab.com/news-flash/news_flash_gtk) | ✔️✔️ | Fever | ➖ | ⭐⭐ | ✔️ | ✔️ | ✔️ | ➖ | ➖ | ➖ |
|
||||
| [NewsFlash](https://gitlab.com/news-flash/news_flash_gtk) | GNU/Linux | [✔️](https://gitlab.com/news-flash/news_flash_gtk) | En développement | GReader | ➖ | ❔ | ❔ | ❔ | ❔ | ❔ | ➖ | ❔ || [Newsboat 2.24+](https://newsboat.org/) | GNU/Linux, MacOS, FreeBSD | [✔️](https://github.com/newsboat/newsboat/) | ✔️✔️ | GReader | ➖ | ⭐ | ➖ | ✔️ | ✔️ | ➖ | ✔️ | ➖ |
|
||||
| [Vienna RSS](http://www.vienna-rss.com/) | MacOS | [✔️](https://github.com/ViennaRSS/vienna-rss) | ✔️✔️ | GReader | ❔ | ❔ | ❔ | ❔ | ❔ | ❔ | ❔ | ❔ |
|
||||
| [Reeder](https://www.reederapp.com/) | iOS, MacOS | ➖ | ✔️✔️ | GReader, Fever | ✔️ | ⭐⭐⭐ | ➖ | ✔️ | ✔️ | ➖ | ➖ | ✔️ |
|
||||
| [Fluent Reader Lite](https://hyliu.me/fluent-reader-lite/) | Android, iOS| [✔️](https://github.com/yang991178/fluent-reader-lite) | ✔️✔️ | GReader, Fever | ✔️ | ⭐⭐⭐ | ➖ | ➖ | ✓ | ➖ | ➖ | ➖ |
|
||||
| [Read You](https://github.com/Ashinch/ReadYou/) | Android | [✔️](https://github.com/Ashinch/ReadYou/) | [En développement](https://github.com/Ashinch/ReadYou/discussions/542) | GReader, Fever | ➖ | ⭐⭐ | ➖ | ✔️ | ✔️ | ➖ | ➖ | ✔️ |
|
||||
| [ChristopheHenry](https://gitlab.com/christophehenry/freshrss-android) | Android | [✔️](https://gitlab.com/christophehenry/freshrss-android) | En développement | GReader | ✔️ | ⭐⭐ | ➖ | ✔️ | ✔️ | ➖ | ➖ | ➖ |
|
||||
| [Fluent Reader](https://hyliu.me/fluent-reader/) | Windows, Linux, macOS| [✔️](https://github.com/yang991178/fluent-reader) | ✔️✔️ | Fever | ✔️ | ⭐ | ➖ | ✔️ | ✓ | ➖ | ➖ | ➖ |
|
||||
| [RSS Guard](https://github.com/martinrotter/rssguard) | Windows, GNU/Linux, macOS, OS/2 | [✔️](https://github.com/martinrotter/rssguard) | ✔️✔️ | GReader | ✔️ | ⭐⭐ | ➖ | ✔️ | ✔️ | ✔️ | ✔️ | ✔️ |
|
||||
| [NewsFlash](https://gitlab.com/news-flash/news_flash_gtk) | GNU/Linux | [✔️](https://gitlab.com/news-flash/news_flash_gtk) | ✔️✔️ | GReader, Fever | ➖ | ⭐⭐ | ➖ | ✔️ | ✔️ | ✔️ | ➖ | ➖ |
|
||||
| [Newsboat 2.24+](https://newsboat.org/) | GNU/Linux, macOS, FreeBSD | [✔️](https://github.com/newsboat/newsboat/) | ✔️✔️ | GReader | ➖ | ⭐ | ➖ | ✔️ | ✔️ | ➖ | ✔️ | ➖ |
|
||||
| [Vienna RSS](http://www.vienna-rss.com/) | macOS | [✔️](https://github.com/ViennaRSS/vienna-rss) | ✔️✔️ | GReader | ❔ | ❔ | ❔ | ❔ | ❔ | ❔ | ❔ | ❔ |
|
||||
| [Readkit](https://apps.apple.com/app/readkit-read-later-rss/id1615798039) | iOS, macOS | ➖ | ✔️✔️ | GReader | ✔️ | ⭐⭐⭐ | ➖ | ✔️ | ✔️ | ➖ | ✓ | 💲 |
|
||||
| [Reeder](https://www.reederapp.com/) | iOS, macOS | ➖ | ✔️✔️ | GReader, Fever | ✔️ | ⭐⭐⭐ | ➖ | ✔️ | ✔️ | ➖ | ➖ | ✔️ |
|
||||
| [lire](https://lireapp.com/) | iOS, macOS | ➖ | ✔️✔️ | GReader | ❔ | ❔ | ❔ | ❔ | ❔ | ❔ | ❔ | ❔ |
|
||||
| [Unread](https://apps.apple.com/app/unread-2/id1363637349) | iOS | ➖ | ✔️✔️ | Fever | ✔️ | ❔ | ❔ | ❔ | ✔️ | ➖ | ➖ | ➖ |
|
||||
| [Fiery Feeds](https://apps.apple.com/app/fiery-feeds-rss-reader/id1158763303) | iOS | ➖ | ✔️✔️ | Fever | ❔ | ❔ | ❔ | ❔ | ❔ | ➖ | ➖ | ➖ |
|
||||
| [Readkit](https://apps.apple.com/app/readkit/id588726889) | MacOS | ➖ | ✔️✔️ | Fever | ✔️ | ❔ | ❔ | ❔ | ❔ | ➖ | ➖ | ➖ |
|
||||
| [Netnewswire](https://ranchero.com/netnewswire/) | iOS, MacOS | [✔️](https://github.com/Ranchero-Software/NetNewsWire) | En développement | GReader | ✔️ | ❔ | ❔ | ❔ | ✔️ | ➖ | ❔ | ✔️ |
|
||||
| [Netnewswire](https://ranchero.com/netnewswire/) | iOS, macOS | [✔️](https://github.com/Ranchero-Software/NetNewsWire) | En développement | GReader | ✔️ | ❔ | ❔ | ❔ | ✔️ | ➖ | ❔ | ✔️ |
|
||||
|
||||
# Bibliothèques incluses
|
||||
|
||||
* [SimplePie](https://simplepie.org/)
|
||||
* [MINZ](https://github.com/marienfressinaud/MINZ)
|
||||
* [MINZ](https://framagit.org/marienfressinaud/MINZ)
|
||||
* [php-http-304](https://alexandre.alapetite.fr/doc-alex/php-http-304/)
|
||||
* [lib_opml](https://github.com/marienfressinaud/lib_opml)
|
||||
* [lib_opml](https://framagit.org/marienfressinaud/lib_opml)
|
||||
* [PhpGt/CssXPath](https://github.com/PhpGt/CssXPath)
|
||||
* [PHPMailer](https://github.com/PHPMailer/PHPMailer)
|
||||
* [Chart.js](https://www.chartjs.org)
|
||||
|
||||
|
@ -254,3 +260,12 @@ et [l’API Fever](https://freshrss.github.io/FreshRSS/fr/users/06_Fever_API.htm
|
|||
|
||||
* [bcrypt.js](https://github.com/dcodeIO/bcrypt.js)
|
||||
* [phpQuery](https://github.com/phpquery/phpquery)
|
||||
|
||||
# Alternatives
|
||||
|
||||
Si FreshRSS ne vous convient pas pour une raison ou pour une autre, voici d’autres solutions à considérer :
|
||||
|
||||
* [Kriss Feed](https://tontof.net/kriss/feed/)
|
||||
* [Leed](https://github.com/LeedRSS/Leed)
|
||||
* [Et plus…](https://framalibre.org/tags/lecteur-de-flux-rss)
|
||||
* [Et encore plus…](https://alternativeto.net/software/freshrss/) (mais si vous appréciez FreshRSS, mettez un “j’aime” !)
|
||||
|
|
110
README.md
110
README.md
|
@ -5,33 +5,49 @@
|
|||
|
||||
# FreshRSS
|
||||
|
||||
FreshRSS is a self-hosted RSS feed aggregator like [Leed](https://github.com/LeedRSS/Leed) or [Kriss Feed](https://tontof.net/kriss/feed/).
|
||||
FreshRSS is a self-hosted RSS feed aggregator.
|
||||
|
||||
It is lightweight, easy to work with, powerful, and customizable.
|
||||
|
||||
It is a multi-user application with an anonymous reading mode. It supports custom tags.
|
||||
There is an API for (mobile) clients, and a [Command-Line Interface](cli/README.md).
|
||||
|
||||
Thanks to the [WebSub](https://www.w3.org/TR/websub/) standard (formerly [PubSubHubbub](https://github.com/pubsubhubbub/PubSubHubbub)),
|
||||
FreshRSS is able to receive instant push notifications from compatible sources, such as [Mastodon](https://joinmastodon.org), [Friendica](https://friendi.ca), [WordPress](https://wordpress.org/plugins/pubsubhubbub/), Blogger, FeedBurner, etc.
|
||||
Thanks to the [WebSub](https://freshrss.github.io/FreshRSS/en/users/WebSub.html) standard,
|
||||
FreshRSS is able to receive instant push notifications from compatible sources, such as [Friendica](https://friendi.ca), [WordPress](https://wordpress.org/plugins/pubsubhubbub/), Blogger, Medium, etc.
|
||||
|
||||
Finally, it supports [extensions](#extensions) for further tuning.
|
||||
FreshRSS natively supports basic [Web scraping](https://freshrss.github.io/FreshRSS/en/users/11_website_scraping.html),
|
||||
based on [XPath](https://www.w3.org/TR/xpath-10/), for Web sites not providing any RSS / Atom feed.
|
||||
Also supports JSON documents.
|
||||
|
||||
Feature requests, bug reports, and other contributions are welcome. The best way to contribute is to [open an issue on GitHub](https://github.com/FreshRSS/FreshRSS/issues).
|
||||
We are a friendly community.
|
||||
FreshRSS offers the ability to [reshare selections of articles by HTML, RSS, and OPML](https://freshrss.github.io/FreshRSS/en/users/user_queries.html).
|
||||
|
||||
Different [login methods](https://freshrss.github.io/FreshRSS/en/admins/09_AccessControl.html) are supported: Web form (including an anonymous option), HTTP Authentication (compatible with proxy delegation), OpenID Connect.
|
||||
|
||||
Finally, FreshRSS supports [extensions](#extensions) for further tuning.
|
||||
|
||||
* Official website: <https://freshrss.org>
|
||||
* Demo: <https://demo.freshrss.org/>
|
||||
* Demo: <https://demo.freshrss.org>
|
||||
* License: [GNU AGPL 3](https://www.gnu.org/licenses/agpl-3.0.html)
|
||||
|
||||
![FreshRSS logo](docs/img/FreshRSS-logo.png)
|
||||
|
||||
# Disclaimer
|
||||
## Feedback and contributions
|
||||
|
||||
FreshRSS comes with absolutely no warranty.
|
||||
Feature requests, bug reports, and other contributions are welcome. The best way is to [open an issue on GitHub](https://github.com/FreshRSS/FreshRSS/issues).
|
||||
We are a friendly community.
|
||||
|
||||
To facilitate contributions, the [following option](.devcontainer/README.md) is available:
|
||||
|
||||
[![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://github.com/codespaces/new?hide_repo_select=true&ref=edge&repo=6322699)
|
||||
|
||||
## Screenshot
|
||||
|
||||
![FreshRSS screenshot](docs/img/FreshRSS-screenshot.png)
|
||||
|
||||
## Disclaimer
|
||||
|
||||
FreshRSS comes with absolutely no warranty.
|
||||
|
||||
# [Documentation](https://freshrss.github.io/FreshRSS/en/)
|
||||
|
||||
* [User documentation](https://freshrss.github.io/FreshRSS/en/users/02_First_steps.html), where you can discover all the possibilities offered by FreshRSS
|
||||
|
@ -45,27 +61,25 @@ FreshRSS comes with absolutely no warranty.
|
|||
* Works on mobile (except a few features)
|
||||
* Light server running Linux or Windows
|
||||
* It even works on Raspberry Pi 1 with response time under a second (tested with 150 feeds, 22k articles)
|
||||
* A web server: Apache2 (recommended), nginx, lighttpd (not tested on others)
|
||||
* PHP 7.0+
|
||||
* Required extensions: [cURL](https://www.php.net/curl), [DOM](https://www.php.net/dom), [JSON](https://www.php.net/json), [XML](https://www.php.net/xml), [session](https://www.php.net/session), [ctype](https://www.php.net/ctype), and [PDO_MySQL](https://www.php.net/pdo-mysql) or [PDO_SQLite](https://www.php.net/pdo-sqlite) or [PDO_PGSQL](https://www.php.net/pdo-pgsql)
|
||||
* Recommended extensions: [GMP](https://www.php.net/gmp) (for API access on 32-bit platforms), [IDN](https://www.php.net/intl.idn) (for Internationalized Domain Names), [mbstring](https://www.php.net/mbstring) (for Unicode strings), [iconv](https://www.php.net/iconv) (for charset conversion), [ZIP](https://www.php.net/zip) (for import/export), [zlib](https://www.php.net/zlib) (for compressed feeds)
|
||||
* MySQL 5.5.3+ or MariaDB equivalent, or SQLite 3.7.4+, or PostgreSQL 9.5+
|
||||
* A web server: Apache2.4+ (recommended), nginx, lighttpd (not tested on others)
|
||||
* PHP 7.4+
|
||||
* Required extensions: [cURL](https://www.php.net/curl), [DOM](https://www.php.net/dom), [JSON](https://www.php.net/json), [XML](https://www.php.net/xml), [session](https://www.php.net/session), [ctype](https://www.php.net/ctype)
|
||||
* Recommended extensions: [PDO_SQLite](https://www.php.net/pdo-sqlite) (for export/import), [GMP](https://www.php.net/gmp) (for API access on 32-bit platforms), [IDN](https://www.php.net/intl.idn) (for Internationalized Domain Names), [mbstring](https://www.php.net/mbstring) (for Unicode strings), [iconv](https://www.php.net/iconv) (for charset conversion), [ZIP](https://www.php.net/zip) (for import/export), [zlib](https://www.php.net/zlib) (for compressed feeds)
|
||||
* Extension for database: [PDO_PGSQL](https://www.php.net/pdo-pgsql) or [PDO_SQLite](https://www.php.net/pdo-sqlite) or [PDO_MySQL](https://www.php.net/pdo-mysql)
|
||||
* PostgreSQL 9.5+ or SQLite or MySQL 5.5.3+ or MariaDB 5.5+
|
||||
|
||||
|
||||
# Releases
|
||||
# [Installation](https://freshrss.github.io/FreshRSS/en/admins/03_Installation.html)
|
||||
|
||||
The latest stable release can be found [here](https://github.com/FreshRSS/FreshRSS/releases/latest). New versions are released every two to three months.
|
||||
|
||||
If you want a rolling release with the newest features, or want to help testing or developing the next stable version, you can use [the `edge` branch](https://github.com/FreshRSS/FreshRSS/tree/edge/).
|
||||
|
||||
|
||||
# [Installation](https://freshrss.github.io/FreshRSS/en/admins/03_Installation.html)
|
||||
|
||||
## Automated install
|
||||
|
||||
* [![Docker](https://www.docker.com/sites/default/files/horizontal.png)](./Docker/)
|
||||
* [<img src="https://www.docker.com/wp-content/uploads/2022/03/horizontal-logo-monochromatic-white.png" width="200" alt="Docker" />](./Docker/)
|
||||
* [![YunoHost](https://install-app.yunohost.org/install-with-yunohost.png)](https://install-app.yunohost.org/?app=freshrss)
|
||||
* [![Cloudron](https://cloudron.io/img/button.svg)](https://cloudron.io/button.html?app=org.freshrss.cloudronapp)
|
||||
* [![PikaPods](https://www.pikapods.com/static/run-button-34.svg)](https://www.pikapods.com/pods?run=freshrss)
|
||||
|
||||
## Manual install
|
||||
|
||||
|
@ -80,7 +94,7 @@ If you want a rolling release with the newest features, or want to help testing
|
|||
|
||||
More detailed information about installation and server configuration can be found in [our documentation](https://freshrss.github.io/FreshRSS/en/admins/03_Installation.html).
|
||||
|
||||
## Advice
|
||||
# Advice
|
||||
|
||||
* For better security, expose only the `./p/` folder to the Web.
|
||||
* Be aware that the `./data/` folder contains all personal data, so it is a bad idea to expose it.
|
||||
|
@ -103,39 +117,41 @@ See the [repository dedicated to those extensions](https://github.com/FreshRSS/E
|
|||
|
||||
# APIs & native apps
|
||||
|
||||
FreshRSS supports access from mobile / native apps for Linux, Android, iOS, and OS X, via two distinct APIs:
|
||||
[Google Reader API](https://freshrss.github.io/FreshRSS/en/users/06_Mobile_access.html) (best),
|
||||
and [Fever API](https://freshrss.github.io/FreshRSS/en/users/06_Fever_API.html) (limited features and less efficient).
|
||||
FreshRSS supports access from mobile / native apps for Linux, Android, iOS, Windows and macOS, via two distinct APIs:
|
||||
[Google Reader API](https://freshrss.github.io/FreshRSS/en/developers/06_GoogleReader_API.html) (best),
|
||||
and [Fever API](https://freshrss.github.io/FreshRSS/en/developers/06_Fever_API.html) (limited features and less efficient).
|
||||
|
||||
| App | Platform | Free Software | Maintained & Developed | API | Works offline | Fast sync | Fetch more in individual views | Fetch read articles | Favourites | Labels | Podcasts | Manage feeds |
|
||||
|:--------------------------------------------------------------------------------------|:-----------:|:-------------------------------------------------------------:|:----------------------:|:----------------:|:-------------:|:---------:|:------------------------------:|:-------------------:|:----------:|:------:|:--------:|:------------:|
|
||||
| [News+](https://play.google.com/store/apps/details?id=com.noinnion.android.newsplus) with [Google Reader extension](https://github.com/noinnion/newsplus/blob/master/apk/GoogleReaderCloneExtension_101.apk) | Android | [Partially](https://github.com/noinnion/newsplus/blob/master/extensions/GoogleReaderCloneExtension/src/com/noinnion/android/newsplus/extension/google_reader/) | 2015 | GReader | ✔️ | ⭐⭐⭐ | ✔️ | ✔️ | ✔️ | ✔️ | ✔️ | ✔️ |
|
||||
| [FeedMe](https://play.google.com/store/apps/details?id=com.seazon.feedme)* | Android | ➖ | ✔️✔️ | GReader | ✔️ | ⭐⭐ | ➖ | ➖ | ✔️ | ✓ | ✔️ | ✔️ |
|
||||
| [RSS Guard](https://github.com/martinrotter/rssguard) | Windows, GNU/Linux, MacOS, OS/2 | ✔️ | ✔️✔️ | GReader | ✔️ | ⭐ | ➖ | ✔️ | ✔️ | ✔️ | ✔️ | ➖ |
|
||||
| [EasyRSS](https://github.com/Alkarex/EasyRSS) | Android | [✔️](https://github.com/Alkarex/EasyRSS) | ✔️ | GReader | Bug | ⭐⭐ | ➖ | ➖ | ✔️ | ➖ | ➖ | ➖ |
|
||||
| [Readrops](https://github.com/readrops/Readrops) | Android | [✔️](https://github.com/readrops/Readrops) | ✔️✔️ | GReader | ✔️ | ⭐⭐⭐ | ➖ | ➖ | ➖ | ➖ | ➖ | ✔️ |
|
||||
| [FocusReader](https://play.google.com/store/apps/details?id=allen.town.focus.reader) | Android | ➖ | ✔️✔️ | GReader | ✔️ | ⭐⭐⭐ | ➖ | ➖ | ✔️ | ➖ | ✓ | ✔️ |
|
||||
| [ChristopheHenry](https://git.feneas.org/christophehenry/freshrss-android) | Android | [✔️](https://git.feneas.org/christophehenry/freshrss-android) | Work in progress | GReader | ✔️ | ⭐⭐ | ➖ | ✔️ | ✔️ | ➖ | ➖ | ➖ |
|
||||
| [Fluent Reader](https://hyliu.me/fluent-reader/) | Windows, Linux, MacOS| [✔️](https://github.com/yang991178/fluent-reader) | ✔️✔️ | Fever | ✔️ | ⭐ | ➖ | ✔️ | ✔️ | ➖ | ➖ | ➖ |
|
||||
| [FeedReader](https://jangernert.github.io/FeedReader/) | GNU/Linux | [✔️](https://jangernert.github.io/FeedReader/) | ✔️ | GReader | ✔️ | ⭐⭐ | ➖ | ✔️ | ✔️ | ➖ | ✔️ | ✔️ |
|
||||
| [NewsFlash](https://gitlab.com/news-flash/news_flash_gtk) | GNU/Linux | [✔️](https://gitlab.com/news-flash/news_flash_gtk) | ✔️✔️ | Fever | ➖ | ⭐⭐ | ✔️ | ✔️ | ✔️ | ➖ | ➖ | ➖ |
|
||||
| [NewsFlash](https://gitlab.com/news-flash/news_flash_gtk) | GNU/Linux | [✔️](https://gitlab.com/news-flash/news_flash_gtk) | Work in Progress | GReader | ➖ | ❔ | ❔ | ❔ | ❔ | ❔ | ➖ | ❔ |
|
||||
| [Newsboat 2.24+](https://newsboat.org/) | GNU/Linux, MacOS, FreeBSD | [✔️](https://github.com/newsboat/newsboat/) | ✔️✔️ | GReader | ➖ | ⭐ | ➖ | ✔️ | ✔️ | ➖ | ✔️ | ➖ |
|
||||
| [Vienna RSS](http://www.vienna-rss.com/) | MacOS | [✔️](https://github.com/ViennaRSS/vienna-rss) | ✔️✔️ | GReader | ❔ | ❔ | ❔ | ❔ | ❔ | ❔ | ❔ | ❔ |
|
||||
| [Reeder](https://www.reederapp.com/)* | iOS, MacOS | ➖ | ✔️✔️ | GReader, Fever | ✔️ | ⭐⭐⭐ | ➖ | ✔️ | ✔️ | ➖ | ➖ | ✔️ |
|
||||
| [News+](https://github.com/noinnion/newsplus/blob/master/apk/NewsPlus_202.apk) with [Google Reader extension](https://github.com/noinnion/newsplus/blob/master/apk/GoogleReaderCloneExtension_101.apk) | Android | [Partially](https://github.com/noinnion/newsplus/blob/master/extensions/GoogleReaderCloneExtension/src/com/noinnion/android/newsplus/extension/google_reader/) | 2015 | GReader | ✔️ | ⭐⭐⭐ | ✔️ | ✔️ | ✔️ | ✔️ | ✔️ | ✔️ |
|
||||
| [FeedMe](https://play.google.com/store/apps/details?id=com.seazon.feedme)* | Android | ➖ | ✔️✔️ | GReader | ✔️ | ⭐⭐ | ➖ | ➖ | ✔️ | ✓ | ✔️ | ✔️ |
|
||||
| [EasyRSS](https://github.com/Alkarex/EasyRSS) | Android | [✔️](https://github.com/Alkarex/EasyRSS) | ✔️ | GReader | Bug | ⭐⭐ | ➖ | ➖ | ✔️ | ➖ | ➖ | ➖ |
|
||||
| [Readrops](https://github.com/readrops/Readrops) | Android | [✔️](https://github.com/readrops/Readrops) | ✔️✔️ | GReader | ✔️ | ⭐⭐⭐ | ➖ | ➖ | ➖ | ➖ | ➖ | ✔️ |
|
||||
| [Fluent Reader Lite](https://hyliu.me/fluent-reader-lite/) | Android, iOS| [✔️](https://github.com/yang991178/fluent-reader-lite) | ✔️✔️ | GReader, Fever | ✔️ | ⭐⭐⭐ | ➖ | ➖ | ✓ | ➖ | ➖ | ➖ |
|
||||
| [FocusReader](https://play.google.com/store/apps/details?id=allen.town.focus.reader) | Android | ➖ | ✔️✔️ | GReader | ✔️ | ⭐⭐⭐ | ➖ | ➖ | ✔️ | ➖ | ✓ | ✔️ |
|
||||
| [Read You](https://github.com/Ashinch/ReadYou/) | Android | [✔️](https://github.com/Ashinch/ReadYou/) | [Work in progress](https://github.com/Ashinch/ReadYou/discussions/542) | GReader, Fever | ➖ | ⭐⭐ | ➖ | ✔️ | ✔️ | ➖ | ➖ | ✔️ |
|
||||
| [ChristopheHenry](https://gitlab.com/christophehenry/freshrss-android) | Android | [✔️](https://gitlab.com/christophehenry/freshrss-android) | Work in progress | GReader | ✔️ | ⭐⭐ | ➖ | ✔️ | ✔️ | ➖ | ➖ | ➖ |
|
||||
| [Fluent Reader](https://hyliu.me/fluent-reader/) | Windows, Linux, macOS| [✔️](https://github.com/yang991178/fluent-reader) | ✔️✔️ | GReader, Fever | ✔️ | ⭐ | ➖ | ✔️ | ✓ | ➖ | ➖ | ➖ |
|
||||
| [RSS Guard](https://github.com/martinrotter/rssguard) | Windows, GNU/Linux, macOS, OS/2 | [✔️](https://github.com/martinrotter/rssguard) | ✔️✔️ | GReader | ✔️ | ⭐⭐ | ➖ | ✔️ | ✔️ | ✔️ | ✔️ | ✔️ |
|
||||
| [NewsFlash](https://gitlab.com/news-flash/news_flash_gtk) | GNU/Linux | [✔️](https://gitlab.com/news-flash/news_flash_gtk) | ✔️✔️ | GReader, Fever | ➖ | ⭐⭐ | ➖ | ✔️ | ✔️ | ✔️ | ➖ | ➖ |
|
||||
| [Newsboat 2.24+](https://newsboat.org/) | GNU/Linux, macOS, FreeBSD | [✔️](https://github.com/newsboat/newsboat/) | ✔️✔️ | GReader | ➖ | ⭐ | ➖ | ✔️ | ✔️ | ➖ | ✔️ | ➖ |
|
||||
| [Vienna RSS](http://www.vienna-rss.com/) | macOS | [✔️](https://github.com/ViennaRSS/vienna-rss) | ✔️✔️ | GReader | ❔ | ❔ | ❔ | ❔ | ❔ | ❔ | ❔ | ❔ |
|
||||
| [Readkit](https://apps.apple.com/app/readkit-read-later-rss/id1615798039) | iOS, macOS | ➖ | ✔️✔️ | GReader | ✔️ | ⭐⭐⭐ | ➖ | ✔️ | ✔️ | ➖ | ✓ | 💲 |
|
||||
| [Reeder](https://www.reederapp.com/)* | iOS, macOS | ➖ | ✔️✔️ | GReader, Fever | ✔️ | ⭐⭐⭐ | ➖ | ✔️ | ✔️ | ➖ | ➖ | ✔️ |
|
||||
| [lire](https://lireapp.com/) | iOS, macOS | ➖ | ✔️✔️ | GReader | ❔ | ❔ | ❔ | ❔ | ❔ | ❔ | ❔ | ❔ |
|
||||
| [Unread](https://apps.apple.com/app/unread-2/id1363637349) | iOS | ➖ | ✔️✔️ | Fever | ✔️ | ❔ | ❔ | ❔ | ✔️ | ➖ | ➖ | ➖ |
|
||||
| [Fiery Feeds](https://apps.apple.com/app/fiery-feeds-rss-reader/id1158763303) | iOS | ➖ | ✔️✔️ | Fever | ❔ | ❔ | ❔ | ❔ | ❔ | ➖ | ➖ | ➖ |
|
||||
| [Readkit](https://apps.apple.com/app/readkit/id588726889) | MacOS | ➖ | ✔️✔️ | Fever | ✔️ | ❔ | ❔ | ❔ | ❔ | ➖ | ➖ | ➖ |
|
||||
| [Netnewswire](https://ranchero.com/netnewswire/) | iOS, MacOS | [✔️](https://github.com/Ranchero-Software/NetNewsWire) | Work in progress | GReader | ✔️ | ❔ | ❔ | ❔ | ✔️ | ➖ | ❔ | ✔️ |
|
||||
| [Netnewswire](https://ranchero.com/netnewswire/) | iOS, macOS | [✔️](https://github.com/Ranchero-Software/NetNewsWire) | Work in progress | GReader | ✔️ | ❔ | ❔ | ❔ | ✔️ | ➖ | ❔ | ✔️ |
|
||||
|
||||
\* Install and enable the [GReader Redate extension](https://github.com/javerous/freshrss-greader-redate) to have the correct publication date for feed articles if you are using Reeder or FeedMe.
|
||||
\* Install and enable the [GReader Redate extension](https://github.com/javerous/freshrss-greader-redate) to have the correct publication date for feed articles if you are using Reeder 4 or FeedMe. (No longer required for Reeder 5)
|
||||
|
||||
# Included libraries
|
||||
|
||||
* [SimplePie](https://simplepie.org/)
|
||||
* [MINZ](https://github.com/marienfressinaud/MINZ)
|
||||
* [MINZ](https://framagit.org/marienfressinaud/MINZ)
|
||||
* [php-http-304](https://alexandre.alapetite.fr/doc-alex/php-http-304/)
|
||||
* [lib_opml](https://github.com/marienfressinaud/lib_opml)
|
||||
* [lib_opml](https://framagit.org/marienfressinaud/lib_opml)
|
||||
* [PhpGt/CssXPath](https://github.com/PhpGt/CssXPath)
|
||||
* [PHPMailer](https://github.com/PHPMailer/PHPMailer)
|
||||
* [Chart.js](https://www.chartjs.org)
|
||||
|
||||
|
@ -143,3 +159,11 @@ and [Fever API](https://freshrss.github.io/FreshRSS/en/users/06_Fever_API.html)
|
|||
|
||||
* [bcrypt.js](https://github.com/dcodeIO/bcrypt.js)
|
||||
* [phpQuery](https://github.com/phpquery/phpquery)
|
||||
|
||||
# Alternatives
|
||||
|
||||
If FreshRSS does not suit you for one reason or another, here are alternative solutions to consider:
|
||||
|
||||
* [Kriss Feed](https://tontof.net/kriss/feed/)
|
||||
* [Leed](https://github.com/LeedRSS/Leed)
|
||||
* [And more…](https://alternativeto.net/software/freshrss/) (but if you like FreshRSS, give us a vote!)
|
||||
|
|
|
@ -2,4 +2,5 @@
|
|||
|
||||
## Reporting a Vulnerability
|
||||
|
||||
Please report security issues to alexandre@alapetite.fr
|
||||
Draft a [new security advisory](https://github.com/FreshRSS/FreshRSS/security/advisories) online,
|
||||
or report security issues to <alexandre@alapetite.fr> ([PGP public key if relevant](https://alexandre.alapetite.fr/cv/pgp.asc)).
|
||||
|
|
|
@ -1,28 +1,32 @@
|
|||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* This controller manage API-related features.
|
||||
*/
|
||||
class FreshRSS_api_Controller extends Minz_ActionController {
|
||||
class FreshRSS_api_Controller extends FreshRSS_ActionController {
|
||||
|
||||
/**
|
||||
* Update the user API password.
|
||||
* Return an error message, or `false` if no error.
|
||||
* @return false|string
|
||||
*/
|
||||
public static function updatePassword($apiPasswordPlain) {
|
||||
$username = Minz_Session::param('currentUser');
|
||||
$userConfig = FreshRSS_Context::$user_conf;
|
||||
public static function updatePassword(string $apiPasswordPlain) {
|
||||
$username = Minz_User::name();
|
||||
if ($username == null) {
|
||||
return _t('feedback.api.password.failed');
|
||||
}
|
||||
|
||||
$apiPasswordHash = FreshRSS_password_Util::hash($apiPasswordPlain);
|
||||
$userConfig->apiPasswordHash = $apiPasswordHash;
|
||||
FreshRSS_Context::userConf()->apiPasswordHash = $apiPasswordHash;
|
||||
|
||||
$feverKey = FreshRSS_fever_Util::updateKey($username, $apiPasswordPlain);
|
||||
if (!$feverKey) {
|
||||
return _t('feedback.api.password.failed');
|
||||
}
|
||||
|
||||
$userConfig->feverKey = $feverKey;
|
||||
if ($userConfig->save()) {
|
||||
FreshRSS_Context::userConf()->feverKey = $feverKey;
|
||||
if (FreshRSS_Context::userConf()->save()) {
|
||||
return false;
|
||||
} else {
|
||||
return _t('feedback.api.password.failed');
|
||||
|
@ -35,19 +39,18 @@ class FreshRSS_api_Controller extends Minz_ActionController {
|
|||
* Parameter is:
|
||||
* - apiPasswordPlain: the new user password
|
||||
*/
|
||||
public function updatePasswordAction() {
|
||||
public function updatePasswordAction(): void {
|
||||
if (!FreshRSS_Auth::hasAccess()) {
|
||||
Minz_Error::error(403);
|
||||
}
|
||||
|
||||
$return_url = array('c' => 'user', 'a' => 'profile');
|
||||
$return_url = ['c' => 'user', 'a' => 'profile'];
|
||||
|
||||
if (!Minz_Request::isPost()) {
|
||||
Minz_Request::forward($return_url, true);
|
||||
}
|
||||
|
||||
$apiPasswordPlain = Minz_Request::param('apiPasswordPlain', '', true);
|
||||
$apiPasswordPlain = trim($apiPasswordPlain);
|
||||
$apiPasswordPlain = Minz_Request::paramString('apiPasswordPlain', true);
|
||||
if ($apiPasswordPlain == '') {
|
||||
Minz_Request::forward($return_url, true);
|
||||
}
|
||||
|
|
|
@ -1,9 +1,10 @@
|
|||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* This controller handles action about authentication.
|
||||
*/
|
||||
class FreshRSS_auth_Controller extends Minz_ActionController {
|
||||
class FreshRSS_auth_Controller extends FreshRSS_ActionController {
|
||||
/**
|
||||
* This action handles authentication management page.
|
||||
*
|
||||
|
@ -14,10 +15,8 @@ class FreshRSS_auth_Controller extends Minz_ActionController {
|
|||
* - auth_type (default: none)
|
||||
* - unsafe_autologin (default: false)
|
||||
* - api_enabled (default: false)
|
||||
*
|
||||
* @todo move unsafe_autologin in an extension.
|
||||
*/
|
||||
public function indexAction() {
|
||||
public function indexAction(): void {
|
||||
if (!FreshRSS_Auth::hasAccess('admin')) {
|
||||
Minz_Error::error(403);
|
||||
}
|
||||
|
@ -27,27 +26,28 @@ class FreshRSS_auth_Controller extends Minz_ActionController {
|
|||
if (Minz_Request::isPost()) {
|
||||
$ok = true;
|
||||
|
||||
$anon = Minz_Request::param('anon_access', false);
|
||||
$anon = ((bool)$anon) && ($anon !== 'no');
|
||||
$anon_refresh = Minz_Request::param('anon_refresh', false);
|
||||
$anon_refresh = ((bool)$anon_refresh) && ($anon_refresh !== 'no');
|
||||
$auth_type = Minz_Request::param('auth_type', 'none');
|
||||
$unsafe_autologin = Minz_Request::param('unsafe_autologin', false);
|
||||
$api_enabled = Minz_Request::param('api_enabled', false);
|
||||
if ($anon != FreshRSS_Context::$system_conf->allow_anonymous ||
|
||||
$auth_type != FreshRSS_Context::$system_conf->auth_type ||
|
||||
$anon_refresh != FreshRSS_Context::$system_conf->allow_anonymous_refresh ||
|
||||
$unsafe_autologin != FreshRSS_Context::$system_conf->unsafe_autologin_enabled ||
|
||||
$api_enabled != FreshRSS_Context::$system_conf->api_enabled) {
|
||||
$anon = Minz_Request::paramBoolean('anon_access');
|
||||
$anon_refresh = Minz_Request::paramBoolean('anon_refresh');
|
||||
$auth_type = Minz_Request::paramString('auth_type') ?: 'form';
|
||||
$unsafe_autologin = Minz_Request::paramBoolean('unsafe_autologin');
|
||||
$api_enabled = Minz_Request::paramBoolean('api_enabled');
|
||||
if ($anon !== FreshRSS_Context::systemConf()->allow_anonymous ||
|
||||
$auth_type !== FreshRSS_Context::systemConf()->auth_type ||
|
||||
$anon_refresh !== FreshRSS_Context::systemConf()->allow_anonymous_refresh ||
|
||||
$unsafe_autologin !== FreshRSS_Context::systemConf()->unsafe_autologin_enabled ||
|
||||
$api_enabled !== FreshRSS_Context::systemConf()->api_enabled) {
|
||||
|
||||
// TODO: test values from form
|
||||
FreshRSS_Context::$system_conf->auth_type = $auth_type;
|
||||
FreshRSS_Context::$system_conf->allow_anonymous = $anon;
|
||||
FreshRSS_Context::$system_conf->allow_anonymous_refresh = $anon_refresh;
|
||||
FreshRSS_Context::$system_conf->unsafe_autologin_enabled = $unsafe_autologin;
|
||||
FreshRSS_Context::$system_conf->api_enabled = $api_enabled;
|
||||
if (in_array($auth_type, ['form', 'http_auth', 'none'], true)) {
|
||||
FreshRSS_Context::systemConf()->auth_type = $auth_type;
|
||||
} else {
|
||||
FreshRSS_Context::systemConf()->auth_type = 'form';
|
||||
}
|
||||
FreshRSS_Context::systemConf()->allow_anonymous = $anon;
|
||||
FreshRSS_Context::systemConf()->allow_anonymous_refresh = $anon_refresh;
|
||||
FreshRSS_Context::systemConf()->unsafe_autologin_enabled = $unsafe_autologin;
|
||||
FreshRSS_Context::systemConf()->api_enabled = $api_enabled;
|
||||
|
||||
$ok &= FreshRSS_Context::$system_conf->save();
|
||||
$ok &= FreshRSS_Context::systemConf()->save();
|
||||
}
|
||||
|
||||
invalidateHttpCache();
|
||||
|
@ -66,27 +66,32 @@ class FreshRSS_auth_Controller extends Minz_ActionController {
|
|||
* It forwards to the correct login page (form) or main page if
|
||||
* the user is already connected.
|
||||
*/
|
||||
public function loginAction() {
|
||||
if (FreshRSS_Auth::hasAccess() && Minz_Request::param('u', '') == '') {
|
||||
Minz_Request::forward(array('c' => 'index', 'a' => 'index'), true);
|
||||
public function loginAction(): void {
|
||||
if (FreshRSS_Auth::hasAccess() && Minz_Request::paramString('u') === '') {
|
||||
Minz_Request::forward(['c' => 'index', 'a' => 'index'], true);
|
||||
}
|
||||
|
||||
$auth_type = FreshRSS_Context::$system_conf->auth_type;
|
||||
$auth_type = FreshRSS_Context::systemConf()->auth_type;
|
||||
FreshRSS_Context::initUser(Minz_User::INTERNAL_USER, false);
|
||||
switch ($auth_type) {
|
||||
case 'form':
|
||||
Minz_Request::forward(array('c' => 'auth', 'a' => 'formLogin'));
|
||||
break;
|
||||
case 'http_auth':
|
||||
Minz_Error::error(403, array('error' => array(_t('feedback.access.denied'),
|
||||
' [HTTP Remote-User=' . htmlspecialchars(httpAuthUser(), ENT_NOQUOTES, 'UTF-8') . ']'
|
||||
)), false);
|
||||
break;
|
||||
case 'none':
|
||||
// It should not happen!
|
||||
Minz_Error::error(404);
|
||||
default:
|
||||
// TODO load plugin instead
|
||||
Minz_Error::error(404);
|
||||
case 'form':
|
||||
Minz_Request::forward(['c' => 'auth', 'a' => 'formLogin']);
|
||||
break;
|
||||
case 'http_auth':
|
||||
Minz_Error::error(403, [
|
||||
'error' => [
|
||||
_t('feedback.access.denied'),
|
||||
' [HTTP Remote-User=' . htmlspecialchars(httpAuthUser(false), ENT_NOQUOTES, 'UTF-8') .
|
||||
' ; Remote IP address=' . connectionRemoteAddress() . ']'
|
||||
]
|
||||
], false);
|
||||
break;
|
||||
case 'none':
|
||||
// It should not happen!
|
||||
Minz_Error::error(404);
|
||||
default:
|
||||
// TODO load plugin instead
|
||||
Minz_Error::error(404);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -103,75 +108,87 @@ class FreshRSS_auth_Controller extends Minz_ActionController {
|
|||
* - keep_logged_in (default: false)
|
||||
*
|
||||
* @todo move unsafe autologin in an extension.
|
||||
* @throws Exception
|
||||
*/
|
||||
public function formLoginAction() {
|
||||
public function formLoginAction(): void {
|
||||
invalidateHttpCache();
|
||||
|
||||
FreshRSS_View::prependTitle(_t('gen.auth.login') . ' · ');
|
||||
FreshRSS_View::appendScript(Minz_Url::display('/scripts/bcrypt.min.js?' . @filemtime(PUBLIC_PATH . '/scripts/bcrypt.min.js')));
|
||||
|
||||
$limits = FreshRSS_Context::$system_conf->limits;
|
||||
$this->view->cookie_days = round($limits['cookie_duration'] / 86400, 1);
|
||||
$limits = FreshRSS_Context::systemConf()->limits;
|
||||
$this->view->cookie_days = (int)round($limits['cookie_duration'] / 86400, 1);
|
||||
|
||||
$isPOST = Minz_Request::isPost() && !Minz_Session::param('POST_to_GET');
|
||||
$isPOST = Minz_Request::isPost() && !Minz_Session::paramBoolean('POST_to_GET');
|
||||
Minz_Session::_param('POST_to_GET');
|
||||
|
||||
if ($isPOST) {
|
||||
$nonce = Minz_Session::param('nonce');
|
||||
$username = Minz_Request::param('username', '');
|
||||
$challenge = Minz_Request::param('challenge', '');
|
||||
$nonce = Minz_Session::paramString('nonce');
|
||||
$username = Minz_Request::paramString('username');
|
||||
$challenge = Minz_Request::paramString('challenge');
|
||||
|
||||
usleep(rand(100, 10000)); //Primitive mitigation of timing attacks, in μs
|
||||
|
||||
FreshRSS_Context::initUser($username);
|
||||
if (FreshRSS_Context::$user_conf == null) {
|
||||
// Initialise the default user to be able to display the error page
|
||||
FreshRSS_Context::initUser(FreshRSS_Context::$system_conf->default_user);
|
||||
Minz_Error::error(403, array(_t('feedback.auth.login.invalid')), false);
|
||||
if ($nonce === '') {
|
||||
Minz_Log::warning("Invalid session during login for user={$username}, nonce={$nonce}");
|
||||
header('HTTP/1.1 403 Forbidden');
|
||||
Minz_Session::_param('POST_to_GET', true); //Prevent infinite internal redirect
|
||||
Minz_Request::setBadNotification(_t('install.session.nok'));
|
||||
Minz_Request::forward(['c' => 'auth', 'a' => 'login'], false);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!FreshRSS_Context::$user_conf->enabled || FreshRSS_Context::$user_conf->passwordHash == '') {
|
||||
usleep(rand(100, 5000)); //Primitive mitigation of timing attacks, in μs
|
||||
Minz_Error::error(403, array(_t('feedback.auth.login.invalid')), false);
|
||||
usleep(random_int(100, 10000)); //Primitive mitigation of timing attacks, in μs
|
||||
|
||||
FreshRSS_Context::initUser($username);
|
||||
if (!FreshRSS_Context::hasUserConf()) {
|
||||
// Initialise the default user to be able to display the error page
|
||||
FreshRSS_Context::initUser(FreshRSS_Context::systemConf()->default_user);
|
||||
Minz_Error::error(403, _t('feedback.auth.login.invalid'), false);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!FreshRSS_Context::userConf()->enabled || FreshRSS_Context::userConf()->passwordHash == '') {
|
||||
usleep(random_int(100, 5000)); //Primitive mitigation of timing attacks, in μs
|
||||
Minz_Error::error(403, _t('feedback.auth.login.invalid'), false);
|
||||
return;
|
||||
}
|
||||
|
||||
$ok = FreshRSS_FormAuth::checkCredentials(
|
||||
$username, FreshRSS_Context::$user_conf->passwordHash, $nonce, $challenge
|
||||
$username, FreshRSS_Context::userConf()->passwordHash, $nonce, $challenge
|
||||
);
|
||||
if ($ok) {
|
||||
// Set session parameter to give access to the user.
|
||||
Minz_Session::_params([
|
||||
'currentUser' => $username,
|
||||
'passwordHash' => FreshRSS_Context::$user_conf->passwordHash,
|
||||
Minz_User::CURRENT_USER => $username,
|
||||
'passwordHash' => FreshRSS_Context::userConf()->passwordHash,
|
||||
'csrf' => false,
|
||||
]);
|
||||
FreshRSS_Auth::giveAccess();
|
||||
|
||||
// Set cookie parameter if nedded.
|
||||
if (Minz_Request::param('keep_logged_in')) {
|
||||
FreshRSS_FormAuth::makeCookie($username, FreshRSS_Context::$user_conf->passwordHash);
|
||||
// Set cookie parameter if needed.
|
||||
if (Minz_Request::paramBoolean('keep_logged_in')) {
|
||||
FreshRSS_FormAuth::makeCookie($username, FreshRSS_Context::userConf()->passwordHash);
|
||||
} else {
|
||||
FreshRSS_FormAuth::deleteCookie();
|
||||
}
|
||||
|
||||
Minz_Translate::init(FreshRSS_Context::$user_conf->language);
|
||||
Minz_Translate::init(FreshRSS_Context::userConf()->language);
|
||||
|
||||
// All is good, go back to the index.
|
||||
Minz_Request::good(_t('feedback.auth.login.success'), [ 'c' => 'index', 'a' => 'index' ]);
|
||||
// All is good, go back to the original request or the index.
|
||||
$url = Minz_Url::unserialize(Minz_Request::paramString('original_request'));
|
||||
if (empty($url)) {
|
||||
$url = [ 'c' => 'index', 'a' => 'index' ];
|
||||
}
|
||||
Minz_Request::good(_t('feedback.auth.login.success'), $url);
|
||||
} else {
|
||||
Minz_Log::warning("Password mismatch for user={$username}, nonce={$nonce}, c={$challenge}");
|
||||
|
||||
header('HTTP/1.1 403 Forbidden');
|
||||
Minz_Session::_param('POST_to_GET', true); //Prevent infinite internal redirect
|
||||
Minz_Request::setBadNotification(_t('feedback.auth.login.invalid'));
|
||||
Minz_Request::forward(['c' => 'auth', 'a' => 'login'], false);
|
||||
return;
|
||||
}
|
||||
} elseif (FreshRSS_Context::$system_conf->unsafe_autologin_enabled) {
|
||||
$username = Minz_Request::param('u', '');
|
||||
$password = Minz_Request::param('p', '');
|
||||
} elseif (FreshRSS_Context::systemConf()->unsafe_autologin_enabled) {
|
||||
$username = Minz_Request::paramString('u');
|
||||
$password = Minz_Request::paramString('p');
|
||||
Minz_Request::_param('p');
|
||||
|
||||
if (!$username) {
|
||||
|
@ -181,29 +198,29 @@ class FreshRSS_auth_Controller extends Minz_ActionController {
|
|||
FreshRSS_FormAuth::deleteCookie();
|
||||
|
||||
FreshRSS_Context::initUser($username);
|
||||
if (FreshRSS_Context::$user_conf == null) {
|
||||
if (!FreshRSS_Context::hasUserConf()) {
|
||||
return;
|
||||
}
|
||||
|
||||
$s = FreshRSS_Context::$user_conf->passwordHash;
|
||||
$s = FreshRSS_Context::userConf()->passwordHash;
|
||||
$ok = password_verify($password, $s);
|
||||
unset($password);
|
||||
if ($ok) {
|
||||
Minz_Session::_params([
|
||||
'currentUser' => $username,
|
||||
Minz_User::CURRENT_USER => $username,
|
||||
'passwordHash' => $s,
|
||||
'csrf' => false,
|
||||
]);
|
||||
FreshRSS_Auth::giveAccess();
|
||||
|
||||
Minz_Translate::init(FreshRSS_Context::$user_conf->language);
|
||||
Minz_Translate::init(FreshRSS_Context::userConf()->language);
|
||||
|
||||
Minz_Request::good(_t('feedback.auth.login.success'), [ 'c' => 'index', 'a' => 'index' ]);
|
||||
Minz_Request::good(_t('feedback.auth.login.success'), ['c' => 'index', 'a' => 'index']);
|
||||
} else {
|
||||
Minz_Log::warning('Unsafe password mismatch for user ' . $username);
|
||||
Minz_Request::bad(
|
||||
_t('feedback.auth.login.invalid'),
|
||||
array('c' => 'auth', 'a' => 'login')
|
||||
['c' => 'auth', 'a' => 'login']
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -212,7 +229,7 @@ class FreshRSS_auth_Controller extends Minz_ActionController {
|
|||
/**
|
||||
* This action removes all accesses of the current user.
|
||||
*/
|
||||
public function logoutAction() {
|
||||
public function logoutAction(): void {
|
||||
invalidateHttpCache();
|
||||
FreshRSS_Auth::removeAccess();
|
||||
Minz_Request::good(_t('feedback.auth.logout.success'), [ 'c' => 'index', 'a' => 'index' ]);
|
||||
|
@ -221,22 +238,33 @@ class FreshRSS_auth_Controller extends Minz_ActionController {
|
|||
/**
|
||||
* This action gives possibility to a user to create an account.
|
||||
*
|
||||
* The user is redirected to the home if he's connected.
|
||||
* The user is redirected to the home when logged in.
|
||||
*
|
||||
* A 403 is sent if max number of registrations is reached.
|
||||
*/
|
||||
public function registerAction() {
|
||||
public function registerAction(): void {
|
||||
if (FreshRSS_Auth::hasAccess()) {
|
||||
Minz_Request::forward(array('c' => 'index', 'a' => 'index'), true);
|
||||
Minz_Request::forward(['c' => 'index', 'a' => 'index'], true);
|
||||
}
|
||||
|
||||
if (max_registrations_reached()) {
|
||||
Minz_Error::error(403);
|
||||
}
|
||||
|
||||
$this->view->show_tos_checkbox = file_exists(join_path(DATA_PATH, 'tos.html'));
|
||||
$this->view->show_email_field = FreshRSS_Context::$system_conf->force_email_validation;
|
||||
$this->view->preferred_language = Minz_Translate::getLanguage(null, Minz_Request::getPreferredLanguages(), FreshRSS_Context::$system_conf->language);
|
||||
$this->view->show_tos_checkbox = file_exists(TOS_FILENAME);
|
||||
$this->view->show_email_field = FreshRSS_Context::systemConf()->force_email_validation;
|
||||
$this->view->preferred_language = Minz_Translate::getLanguage(null, Minz_Request::getPreferredLanguages(), FreshRSS_Context::systemConf()->language);
|
||||
FreshRSS_View::prependTitle(_t('gen.auth.registration.title') . ' · ');
|
||||
}
|
||||
|
||||
public static function getLogoutUrl(): string {
|
||||
if (($_SERVER['AUTH_TYPE'] ?? '') === 'openid-connect') {
|
||||
$url_string = urlencode(Minz_Request::guessBaseUrl());
|
||||
return './oidc/?logout=' . $url_string . '/';
|
||||
# The trailing slash is necessary so that we don’t redirect to http://.
|
||||
# https://bz.apache.org/bugzilla/show_bug.cgi?id=61355#c13
|
||||
} else {
|
||||
return _url('auth', 'logout') ?: '';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,17 +1,19 @@
|
|||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* Controller to handle actions relative to categories.
|
||||
* User needs to be connected.
|
||||
*/
|
||||
class FreshRSS_category_Controller extends Minz_ActionController {
|
||||
class FreshRSS_category_Controller extends FreshRSS_ActionController {
|
||||
/**
|
||||
* This action is called before every other action in that class. It is
|
||||
* the common boiler plate for every action. It is triggered by the
|
||||
* underlying framework.
|
||||
*
|
||||
*/
|
||||
public function firstAction() {
|
||||
#[\Override]
|
||||
public function firstAction(): void {
|
||||
if (!FreshRSS_Auth::hasAccess()) {
|
||||
Minz_Error::error(403);
|
||||
}
|
||||
|
@ -26,12 +28,14 @@ class FreshRSS_category_Controller extends Minz_ActionController {
|
|||
* Request parameter is:
|
||||
* - new-category
|
||||
*/
|
||||
public function createAction() {
|
||||
public function createAction() :void {
|
||||
$catDAO = FreshRSS_Factory::createCategoryDao();
|
||||
$url_redirect = array('c' => 'subscription', 'a' => 'add');
|
||||
$tagDAO = FreshRSS_Factory::createTagDao();
|
||||
|
||||
$limits = FreshRSS_Context::$system_conf->limits;
|
||||
$this->view->categories = $catDAO->listCategories(false);
|
||||
$url_redirect = ['c' => 'subscription', 'a' => 'add'];
|
||||
|
||||
$limits = FreshRSS_Context::systemConf()->limits;
|
||||
$this->view->categories = $catDAO->listCategories(false) ?: [];
|
||||
|
||||
if (count($this->view->categories) >= $limits['max_categories']) {
|
||||
Minz_Request::bad(_t('feedback.sub.category.over_max', $limits['max_categories']), $url_redirect);
|
||||
|
@ -40,8 +44,8 @@ class FreshRSS_category_Controller extends Minz_ActionController {
|
|||
if (Minz_Request::isPost()) {
|
||||
invalidateHttpCache();
|
||||
|
||||
$cat_name = Minz_Request::param('new-category');
|
||||
if (!$cat_name) {
|
||||
$cat_name = Minz_Request::paramString('new-category');
|
||||
if ($cat_name === '') {
|
||||
Minz_Request::bad(_t('feedback.sub.category.no_name'), $url_redirect);
|
||||
}
|
||||
|
||||
|
@ -51,12 +55,20 @@ class FreshRSS_category_Controller extends Minz_ActionController {
|
|||
Minz_Request::bad(_t('feedback.sub.category.name_exists'), $url_redirect);
|
||||
}
|
||||
|
||||
$values = array(
|
||||
'id' => $cat->id(),
|
||||
'name' => $cat->name(),
|
||||
);
|
||||
if ($tagDAO->searchByName($cat->name()) != null) {
|
||||
Minz_Request::bad(_t('feedback.tag.name_exists', $cat->name()), $url_redirect);
|
||||
}
|
||||
|
||||
if ($catDAO->addCategory($values)) {
|
||||
$opml_url = checkUrl(Minz_Request::paramString('opml_url'));
|
||||
if ($opml_url != '') {
|
||||
$cat->_kind(FreshRSS_Category::KIND_DYNAMIC_OPML);
|
||||
$cat->_attribute('opml_url', $opml_url);
|
||||
} else {
|
||||
$cat->_kind(FreshRSS_Category::KIND_NORMAL);
|
||||
$cat->_attribute('opml_url', null);
|
||||
}
|
||||
|
||||
if ($catDAO->addCategoryObject($cat)) {
|
||||
$url_redirect['a'] = 'index';
|
||||
Minz_Request::good(_t('feedback.sub.category.created', $cat->name()), $url_redirect);
|
||||
} else {
|
||||
|
@ -69,41 +81,80 @@ class FreshRSS_category_Controller extends Minz_ActionController {
|
|||
|
||||
/**
|
||||
* This action updates the given category.
|
||||
*
|
||||
* Request parameters are:
|
||||
* - id
|
||||
* - name
|
||||
*/
|
||||
public function updateAction() {
|
||||
$catDAO = FreshRSS_Factory::createCategoryDao();
|
||||
$url_redirect = array('c' => 'subscription', 'a' => 'index');
|
||||
public function updateAction(): void {
|
||||
if (Minz_Request::paramBoolean('ajax')) {
|
||||
$this->view->_layout(null);
|
||||
}
|
||||
|
||||
$categoryDAO = FreshRSS_Factory::createCategoryDao();
|
||||
|
||||
$id = Minz_Request::paramInt('id');
|
||||
$category = $categoryDAO->searchById($id);
|
||||
if ($id === 0 || null === $category) {
|
||||
Minz_Error::error(404);
|
||||
return;
|
||||
}
|
||||
$this->view->category = $category;
|
||||
|
||||
FreshRSS_View::prependTitle($category->name() . ' · ' . _t('sub.title') . ' · ');
|
||||
|
||||
if (Minz_Request::isPost()) {
|
||||
$category->_filtersAction('read', Minz_Request::paramTextToArray('filteractions_read'));
|
||||
|
||||
if (Minz_Request::paramBoolean('use_default_purge_options')) {
|
||||
$category->_attribute('archiving', null);
|
||||
} else {
|
||||
if (!Minz_Request::paramBoolean('enable_keep_max')) {
|
||||
$keepMax = false;
|
||||
} elseif (($keepMax = Minz_Request::paramInt('keep_max')) !== 0) {
|
||||
$keepMax = FreshRSS_Feed::ARCHIVING_RETENTION_COUNT_LIMIT;
|
||||
}
|
||||
if (Minz_Request::paramBoolean('enable_keep_period')) {
|
||||
$keepPeriod = FreshRSS_Feed::ARCHIVING_RETENTION_PERIOD;
|
||||
if (is_numeric(Minz_Request::paramString('keep_period_count')) && preg_match('/^PT?1[YMWDH]$/', Minz_Request::paramString('keep_period_unit'))) {
|
||||
$keepPeriod = str_replace('1', Minz_Request::paramString('keep_period_count'), Minz_Request::paramString('keep_period_unit'));
|
||||
}
|
||||
} else {
|
||||
$keepPeriod = false;
|
||||
}
|
||||
$category->_attribute('archiving', [
|
||||
'keep_period' => $keepPeriod,
|
||||
'keep_max' => $keepMax,
|
||||
'keep_min' => Minz_Request::paramInt('keep_min'),
|
||||
'keep_favourites' => Minz_Request::paramBoolean('keep_favourites'),
|
||||
'keep_labels' => Minz_Request::paramBoolean('keep_labels'),
|
||||
'keep_unreads' => Minz_Request::paramBoolean('keep_unreads'),
|
||||
]);
|
||||
}
|
||||
|
||||
$position = Minz_Request::paramInt('position') ?: null;
|
||||
$category->_attribute('position', $position);
|
||||
|
||||
$opml_url = checkUrl(Minz_Request::paramString('opml_url'));
|
||||
if ($opml_url != '') {
|
||||
$category->_kind(FreshRSS_Category::KIND_DYNAMIC_OPML);
|
||||
$category->_attribute('opml_url', $opml_url);
|
||||
} else {
|
||||
$category->_kind(FreshRSS_Category::KIND_NORMAL);
|
||||
$category->_attribute('opml_url', null);
|
||||
}
|
||||
|
||||
$values = [
|
||||
'kind' => $category->kind(),
|
||||
'name' => Minz_Request::paramString('name'),
|
||||
'attributes' => $category->attributes(),
|
||||
];
|
||||
|
||||
invalidateHttpCache();
|
||||
|
||||
$id = Minz_Request::param('id');
|
||||
$name = Minz_Request::param('name', '');
|
||||
if (strlen($name) <= 0) {
|
||||
Minz_Request::bad(_t('feedback.sub.category.no_name'), $url_redirect);
|
||||
}
|
||||
|
||||
if ($catDAO->searchById($id) == null) {
|
||||
Minz_Request::bad(_t('feedback.sub.category.not_exist'), $url_redirect);
|
||||
}
|
||||
|
||||
$cat = new FreshRSS_Category($name);
|
||||
$values = array(
|
||||
'name' => $cat->name(),
|
||||
);
|
||||
|
||||
if ($catDAO->updateCategory($id, $values)) {
|
||||
$url_redirect = ['c' => 'subscription', 'params' => ['id' => $id, 'type' => 'category']];
|
||||
if (false !== $categoryDAO->updateCategory($id, $values)) {
|
||||
Minz_Request::good(_t('feedback.sub.category.updated'), $url_redirect);
|
||||
} else {
|
||||
Minz_Request::bad(_t('feedback.sub.category.error'), $url_redirect);
|
||||
}
|
||||
}
|
||||
|
||||
Minz_Request::forward($url_redirect, true);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -114,16 +165,16 @@ class FreshRSS_category_Controller extends Minz_ActionController {
|
|||
* Request parameter is:
|
||||
* - id (of a category)
|
||||
*/
|
||||
public function deleteAction() {
|
||||
public function deleteAction(): void {
|
||||
$feedDAO = FreshRSS_Factory::createFeedDao();
|
||||
$catDAO = FreshRSS_Factory::createCategoryDao();
|
||||
$url_redirect = array('c' => 'subscription', 'a' => 'index');
|
||||
$url_redirect = ['c' => 'subscription', 'a' => 'index'];
|
||||
|
||||
if (Minz_Request::isPost()) {
|
||||
invalidateHttpCache();
|
||||
|
||||
$id = Minz_Request::param('id');
|
||||
if (!$id) {
|
||||
$id = Minz_Request::paramInt('id');
|
||||
if ($id === 0) {
|
||||
Minz_Request::bad(_t('feedback.sub.category.no_id'), $url_redirect);
|
||||
}
|
||||
|
||||
|
@ -140,9 +191,10 @@ class FreshRSS_category_Controller extends Minz_ActionController {
|
|||
}
|
||||
|
||||
// Remove related queries.
|
||||
FreshRSS_Context::$user_conf->queries = remove_query_by_get(
|
||||
'c_' . $id, FreshRSS_Context::$user_conf->queries);
|
||||
FreshRSS_Context::$user_conf->save();
|
||||
/** @var array<array{'get'?:string,'name'?:string,'order'?:string,'search'?:string,'state'?:int,'url'?:string}> $queries */
|
||||
$queries = remove_query_by_get('c_' . $id, FreshRSS_Context::userConf()->queries);
|
||||
FreshRSS_Context::userConf()->queries = $queries;
|
||||
FreshRSS_Context::userConf()->save();
|
||||
|
||||
Minz_Request::good(_t('feedback.sub.category.deleted'), $url_redirect);
|
||||
}
|
||||
|
@ -156,31 +208,35 @@ class FreshRSS_category_Controller extends Minz_ActionController {
|
|||
*
|
||||
* Request parameter is:
|
||||
* - id (of a category)
|
||||
* - muted (truthy to remove only muted feeds, or falsy otherwise)
|
||||
*/
|
||||
public function emptyAction() {
|
||||
public function emptyAction(): void {
|
||||
$feedDAO = FreshRSS_Factory::createFeedDao();
|
||||
$url_redirect = array('c' => 'subscription', 'a' => 'index');
|
||||
$url_redirect = ['c' => 'subscription', 'a' => 'index'];
|
||||
|
||||
if (Minz_Request::isPost()) {
|
||||
invalidateHttpCache();
|
||||
|
||||
$id = Minz_Request::param('id');
|
||||
if (!$id) {
|
||||
$id = Minz_Request::paramInt('id');
|
||||
if ($id === 0) {
|
||||
Minz_Request::bad(_t('feedback.sub.category.no_id'), $url_redirect);
|
||||
}
|
||||
|
||||
// List feeds to remove then related user queries.
|
||||
$feeds = $feedDAO->listByCategory($id);
|
||||
$muted = Minz_Request::paramTernary('muted');
|
||||
|
||||
if ($feedDAO->deleteFeedByCategory($id)) {
|
||||
// List feeds to remove then related user queries.
|
||||
$feeds = $feedDAO->listByCategory($id, $muted);
|
||||
|
||||
if ($feedDAO->deleteFeedByCategory($id, $muted)) {
|
||||
// TODO: Delete old favicons
|
||||
|
||||
// Remove related queries
|
||||
foreach ($feeds as $feed) {
|
||||
FreshRSS_Context::$user_conf->queries = remove_query_by_get(
|
||||
'f_' . $feed->id(), FreshRSS_Context::$user_conf->queries);
|
||||
/** @var array<array{'get'?:string,'name'?:string,'order'?:string,'search'?:string,'state'?:int,'url'?:string}> */
|
||||
$queries = remove_query_by_get('f_' . $feed->id(), FreshRSS_Context::userConf()->queries);
|
||||
FreshRSS_Context::userConf()->queries = $queries;
|
||||
}
|
||||
FreshRSS_Context::$user_conf->save();
|
||||
FreshRSS_Context::userConf()->save();
|
||||
|
||||
Minz_Request::good(_t('feedback.sub.category.emptied'), $url_redirect);
|
||||
} else {
|
||||
|
@ -190,4 +246,64 @@ class FreshRSS_category_Controller extends Minz_ActionController {
|
|||
|
||||
Minz_Request::forward($url_redirect, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Request parameter is:
|
||||
* - id (of a category)
|
||||
*/
|
||||
public function refreshOpmlAction(): void {
|
||||
$catDAO = FreshRSS_Factory::createCategoryDao();
|
||||
$url_redirect = ['c' => 'subscription', 'a' => 'index'];
|
||||
|
||||
if (Minz_Request::isPost()) {
|
||||
invalidateHttpCache();
|
||||
|
||||
$id = Minz_Request::paramInt('id');
|
||||
if ($id === 0) {
|
||||
Minz_Request::bad(_t('feedback.sub.category.no_id'), $url_redirect);
|
||||
return;
|
||||
}
|
||||
|
||||
$category = $catDAO->searchById($id);
|
||||
if ($category === null) {
|
||||
Minz_Request::bad(_t('feedback.sub.category.not_exist'), $url_redirect);
|
||||
return;
|
||||
}
|
||||
|
||||
invalidateHttpCache();
|
||||
|
||||
$ok = $category->refreshDynamicOpml();
|
||||
|
||||
if (Minz_Request::paramBoolean('ajax')) {
|
||||
Minz_Request::setGoodNotification(_t('feedback.sub.category.updated'));
|
||||
$this->view->_layout(null);
|
||||
} else {
|
||||
if ($ok) {
|
||||
Minz_Request::good(_t('feedback.sub.category.updated'), $url_redirect);
|
||||
} else {
|
||||
Minz_Request::bad(_t('feedback.sub.category.error'), $url_redirect);
|
||||
}
|
||||
Minz_Request::forward($url_redirect, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** @return array<string,int> */
|
||||
public static function refreshDynamicOpmls(): array {
|
||||
$successes = 0;
|
||||
$errors = 0;
|
||||
$catDAO = FreshRSS_Factory::createCategoryDao();
|
||||
$categories = $catDAO->listCategoriesOrderUpdate(FreshRSS_Context::userConf()->dynamic_opml_ttl_default ?? 86400);
|
||||
foreach ($categories as $category) {
|
||||
if ($category->refreshDynamicOpml()) {
|
||||
$successes++;
|
||||
} else {
|
||||
$errors++;
|
||||
}
|
||||
}
|
||||
return [
|
||||
'successes' => $successes,
|
||||
'errors' => $errors,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,15 +1,17 @@
|
|||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* Controller to handle every configuration options.
|
||||
*/
|
||||
class FreshRSS_configure_Controller extends Minz_ActionController {
|
||||
class FreshRSS_configure_Controller extends FreshRSS_ActionController {
|
||||
/**
|
||||
* This action is called before every other action in that class. It is
|
||||
* the common boiler plate for every action. It is triggered by the
|
||||
* the common boilerplate for every action. It is triggered by the
|
||||
* underlying framework.
|
||||
*/
|
||||
public function firstAction() {
|
||||
#[\Override]
|
||||
public function firstAction(): void {
|
||||
if (!FreshRSS_Auth::hasAccess()) {
|
||||
Minz_Error::error(403);
|
||||
}
|
||||
|
@ -25,6 +27,7 @@ class FreshRSS_configure_Controller extends Minz_ActionController {
|
|||
* The options available on the page are:
|
||||
* - language (default: en)
|
||||
* - theme (default: Origin)
|
||||
* - darkMode (default: no)
|
||||
* - content width (default: thin)
|
||||
* - display of read action in header
|
||||
* - display of favorite action in header
|
||||
|
@ -33,36 +36,41 @@ class FreshRSS_configure_Controller extends Minz_ActionController {
|
|||
* - display of read action in footer
|
||||
* - display of favorite action in footer
|
||||
* - display of sharing action in footer
|
||||
* - display of tags in footer
|
||||
* - display of article tags in footer
|
||||
* - display of my Labels in footer
|
||||
* - display of date in footer
|
||||
* - display of open action in footer
|
||||
* - html5 notification timeout (default: 0)
|
||||
* Default values are false unless specified.
|
||||
*/
|
||||
public function displayAction() {
|
||||
public function displayAction(): void {
|
||||
if (Minz_Request::isPost()) {
|
||||
FreshRSS_Context::$user_conf->language = Minz_Request::param('language', 'en');
|
||||
FreshRSS_Context::$user_conf->theme = Minz_Request::param('theme', FreshRSS_Themes::$defaultTheme);
|
||||
FreshRSS_Context::$user_conf->content_width = Minz_Request::param('content_width', 'thin');
|
||||
FreshRSS_Context::$user_conf->topline_read = Minz_Request::param('topline_read', false);
|
||||
FreshRSS_Context::$user_conf->topline_favorite = Minz_Request::param('topline_favorite', false);
|
||||
FreshRSS_Context::$user_conf->topline_date = Minz_Request::param('topline_date', false);
|
||||
FreshRSS_Context::$user_conf->topline_link = Minz_Request::param('topline_link', false);
|
||||
FreshRSS_Context::$user_conf->topline_thumbnail = Minz_Request::param('topline_thumbnail', false);
|
||||
FreshRSS_Context::$user_conf->topline_summary = Minz_Request::param('topline_summary', false);
|
||||
FreshRSS_Context::$user_conf->topline_display_authors = Minz_Request::param('topline_display_authors', false);
|
||||
FreshRSS_Context::$user_conf->bottomline_read = Minz_Request::param('bottomline_read', false);
|
||||
FreshRSS_Context::$user_conf->bottomline_favorite = Minz_Request::param('bottomline_favorite', false);
|
||||
FreshRSS_Context::$user_conf->bottomline_sharing = Minz_Request::param('bottomline_sharing', false);
|
||||
FreshRSS_Context::$user_conf->bottomline_tags = Minz_Request::param('bottomline_tags', false);
|
||||
FreshRSS_Context::$user_conf->bottomline_date = Minz_Request::param('bottomline_date', false);
|
||||
FreshRSS_Context::$user_conf->bottomline_link = Minz_Request::param('bottomline_link', false);
|
||||
FreshRSS_Context::$user_conf->html5_notif_timeout = Minz_Request::param('html5_notif_timeout', 0);
|
||||
FreshRSS_Context::$user_conf->show_nav_buttons = Minz_Request::param('show_nav_buttons', false);
|
||||
FreshRSS_Context::$user_conf->save();
|
||||
FreshRSS_Context::userConf()->language = Minz_Request::paramString('language') ?: 'en';
|
||||
FreshRSS_Context::userConf()->timezone = Minz_Request::paramString('timezone');
|
||||
FreshRSS_Context::userConf()->theme = Minz_Request::paramString('theme') ?: FreshRSS_Themes::$defaultTheme;
|
||||
FreshRSS_Context::userConf()->darkMode = Minz_Request::paramString('darkMode') ?: 'no';
|
||||
FreshRSS_Context::userConf()->content_width = Minz_Request::paramString('content_width') ?: 'thin';
|
||||
FreshRSS_Context::userConf()->topline_read = Minz_Request::paramBoolean('topline_read');
|
||||
FreshRSS_Context::userConf()->topline_favorite = Minz_Request::paramBoolean('topline_favorite');
|
||||
FreshRSS_Context::userConf()->topline_date = Minz_Request::paramBoolean('topline_date');
|
||||
FreshRSS_Context::userConf()->topline_link = Minz_Request::paramBoolean('topline_link');
|
||||
FreshRSS_Context::userConf()->topline_website = Minz_Request::paramString('topline_website');
|
||||
FreshRSS_Context::userConf()->topline_thumbnail = Minz_Request::paramString('topline_thumbnail');
|
||||
FreshRSS_Context::userConf()->topline_summary = Minz_Request::paramBoolean('topline_summary');
|
||||
FreshRSS_Context::userConf()->topline_display_authors = Minz_Request::paramBoolean('topline_display_authors');
|
||||
FreshRSS_Context::userConf()->bottomline_read = Minz_Request::paramBoolean('bottomline_read');
|
||||
FreshRSS_Context::userConf()->bottomline_favorite = Minz_Request::paramBoolean('bottomline_favorite');
|
||||
FreshRSS_Context::userConf()->bottomline_sharing = Minz_Request::paramBoolean('bottomline_sharing');
|
||||
FreshRSS_Context::userConf()->bottomline_tags = Minz_Request::paramBoolean('bottomline_tags');
|
||||
FreshRSS_Context::userConf()->bottomline_myLabels = Minz_Request::paramBoolean('bottomline_myLabels');
|
||||
FreshRSS_Context::userConf()->bottomline_date = Minz_Request::paramBoolean('bottomline_date');
|
||||
FreshRSS_Context::userConf()->bottomline_link = Minz_Request::paramBoolean('bottomline_link');
|
||||
FreshRSS_Context::userConf()->show_nav_buttons = Minz_Request::paramBoolean('show_nav_buttons');
|
||||
FreshRSS_Context::userConf()->html5_notif_timeout = Minz_Request::paramInt('html5_notif_timeout');
|
||||
FreshRSS_Context::userConf()->save();
|
||||
|
||||
Minz_Session::_param('language', FreshRSS_Context::$user_conf->language);
|
||||
Minz_Translate::reset(FreshRSS_Context::$user_conf->language);
|
||||
Minz_Session::_param('language', FreshRSS_Context::userConf()->language);
|
||||
Minz_Translate::reset(FreshRSS_Context::userConf()->language);
|
||||
invalidateHttpCache();
|
||||
|
||||
Minz_Request::good(_t('feedback.conf.updated'), [ 'c' => 'configure', 'a' => 'display' ]);
|
||||
|
@ -99,36 +107,48 @@ class FreshRSS_configure_Controller extends Minz_ActionController {
|
|||
* - opened on site
|
||||
* - scrolled
|
||||
* - received
|
||||
* - focus
|
||||
* Default values are false unless specified.
|
||||
*/
|
||||
public function readingAction() {
|
||||
public function readingAction(): void {
|
||||
if (Minz_Request::isPost()) {
|
||||
FreshRSS_Context::$user_conf->posts_per_page = Minz_Request::param('posts_per_page', 10);
|
||||
FreshRSS_Context::$user_conf->view_mode = Minz_Request::param('view_mode', 'normal');
|
||||
FreshRSS_Context::$user_conf->default_view = Minz_Request::param('default_view', 'adaptive');
|
||||
FreshRSS_Context::$user_conf->show_fav_unread = Minz_Request::param('show_fav_unread', false);
|
||||
FreshRSS_Context::$user_conf->auto_load_more = Minz_Request::param('auto_load_more', false);
|
||||
FreshRSS_Context::$user_conf->display_posts = Minz_Request::param('display_posts', false);
|
||||
FreshRSS_Context::$user_conf->display_categories = Minz_Request::param('display_categories', 'active');
|
||||
FreshRSS_Context::$user_conf->hide_read_feeds = Minz_Request::param('hide_read_feeds', false);
|
||||
FreshRSS_Context::$user_conf->onread_jump_next = Minz_Request::param('onread_jump_next', false);
|
||||
FreshRSS_Context::$user_conf->lazyload = Minz_Request::param('lazyload', false);
|
||||
FreshRSS_Context::$user_conf->sides_close_article = Minz_Request::param('sides_close_article', false);
|
||||
FreshRSS_Context::$user_conf->sticky_post = Minz_Request::param('sticky_post', false);
|
||||
FreshRSS_Context::$user_conf->reading_confirm = Minz_Request::param('reading_confirm', false);
|
||||
FreshRSS_Context::$user_conf->auto_remove_article = Minz_Request::param('auto_remove_article', false);
|
||||
FreshRSS_Context::$user_conf->mark_updated_article_unread = Minz_Request::param('mark_updated_article_unread', false);
|
||||
FreshRSS_Context::$user_conf->sort_order = Minz_Request::param('sort_order', 'DESC');
|
||||
FreshRSS_Context::$user_conf->mark_when = array(
|
||||
'article' => Minz_Request::param('mark_open_article', false),
|
||||
'max_n_unread' => Minz_Request::paramBoolean('enable_keep_max_n_unread') ? Minz_Request::param('keep_max_n_unread', false) : false,
|
||||
'reception' => Minz_Request::param('mark_upon_reception', false),
|
||||
FreshRSS_Context::userConf()->posts_per_page = Minz_Request::paramInt('posts_per_page') ?: 10;
|
||||
FreshRSS_Context::userConf()->view_mode = Minz_Request::paramString('view_mode', true) ?: 'normal';
|
||||
FreshRSS_Context::userConf()->default_view = Minz_Request::paramString('default_view') ?: 'adaptive';
|
||||
FreshRSS_Context::userConf()->show_fav_unread = Minz_Request::paramBoolean('show_fav_unread');
|
||||
FreshRSS_Context::userConf()->auto_load_more = Minz_Request::paramBoolean('auto_load_more');
|
||||
FreshRSS_Context::userConf()->display_posts = Minz_Request::paramBoolean('display_posts');
|
||||
FreshRSS_Context::userConf()->display_categories = Minz_Request::paramString('display_categories') ?: 'active';
|
||||
FreshRSS_Context::userConf()->show_tags = Minz_Request::paramString('show_tags') ?: '0';
|
||||
FreshRSS_Context::userConf()->show_tags_max = Minz_Request::paramInt('show_tags_max');
|
||||
FreshRSS_Context::userConf()->show_author_date = Minz_Request::paramString('show_author_date') ?: '0';
|
||||
FreshRSS_Context::userConf()->show_feed_name = Minz_Request::paramString('show_feed_name') ?: 't';
|
||||
FreshRSS_Context::userConf()->hide_read_feeds = Minz_Request::paramBoolean('hide_read_feeds');
|
||||
FreshRSS_Context::userConf()->onread_jump_next = Minz_Request::paramBoolean('onread_jump_next');
|
||||
FreshRSS_Context::userConf()->lazyload = Minz_Request::paramBoolean('lazyload');
|
||||
FreshRSS_Context::userConf()->sides_close_article = Minz_Request::paramBoolean('sides_close_article');
|
||||
FreshRSS_Context::userConf()->sticky_post = Minz_Request::paramBoolean('sticky_post');
|
||||
FreshRSS_Context::userConf()->reading_confirm = Minz_Request::paramBoolean('reading_confirm');
|
||||
FreshRSS_Context::userConf()->auto_remove_article = Minz_Request::paramBoolean('auto_remove_article');
|
||||
FreshRSS_Context::userConf()->mark_updated_article_unread = Minz_Request::paramBoolean('mark_updated_article_unread');
|
||||
if (in_array(Minz_Request::paramString('sort_order'), ['ASC', 'DESC'], true)) {
|
||||
FreshRSS_Context::userConf()->sort_order = Minz_Request::paramString('sort_order');
|
||||
} else {
|
||||
FreshRSS_Context::userConf()->sort_order = 'DESC';
|
||||
}
|
||||
FreshRSS_Context::userConf()->mark_when = [
|
||||
'article' => Minz_Request::paramBoolean('mark_open_article'),
|
||||
'gone' => Minz_Request::paramBoolean('read_upon_gone'),
|
||||
'max_n_unread' => Minz_Request::paramBoolean('enable_keep_max_n_unread') ? Minz_Request::paramInt('keep_max_n_unread') : false,
|
||||
'reception' => Minz_Request::paramBoolean('mark_upon_reception'),
|
||||
'same_title_in_feed' => Minz_Request::paramBoolean('enable_read_when_same_title_in_feed') ?
|
||||
Minz_Request::param('read_when_same_title_in_feed', false) : false,
|
||||
'scroll' => Minz_Request::param('mark_scroll', false),
|
||||
'site' => Minz_Request::param('mark_open_site', false),
|
||||
);
|
||||
FreshRSS_Context::$user_conf->save();
|
||||
Minz_Request::paramInt('read_when_same_title_in_feed') : false,
|
||||
'scroll' => Minz_Request::paramBoolean('mark_scroll'),
|
||||
'site' => Minz_Request::paramBoolean('mark_open_site'),
|
||||
'focus' => Minz_Request::paramBoolean('mark_focus'),
|
||||
];
|
||||
FreshRSS_Context::userConf()->_filtersAction('read', Minz_Request::paramTextToArray('filteractions_read'));
|
||||
FreshRSS_Context::userConf()->save();
|
||||
invalidateHttpCache();
|
||||
|
||||
Minz_Request::good(_t('feedback.conf.updated'), [ 'c' => 'configure', 'a' => 'reading' ]);
|
||||
|
@ -147,14 +167,14 @@ class FreshRSS_configure_Controller extends Minz_ActionController {
|
|||
* Before v1.16, we used sharing instead of integration. This has
|
||||
* some unwanted behavior when the end-user was using an ad-blocker.
|
||||
*/
|
||||
public function integrationAction() {
|
||||
public function integrationAction(): void {
|
||||
FreshRSS_View::appendScript(Minz_Url::display('/scripts/integration.js?' . @filemtime(PUBLIC_PATH . '/scripts/integration.js')));
|
||||
FreshRSS_View::appendScript(Minz_Url::display('/scripts/draggable.js?' . @filemtime(PUBLIC_PATH . '/scripts/draggable.js')));
|
||||
|
||||
if (Minz_Request::isPost()) {
|
||||
$params = $_POST;
|
||||
FreshRSS_Context::$user_conf->sharing = $params['share'];
|
||||
FreshRSS_Context::$user_conf->save();
|
||||
FreshRSS_Context::userConf()->sharing = $params['share'];
|
||||
FreshRSS_Context::userConf()->save();
|
||||
invalidateHttpCache();
|
||||
|
||||
Minz_Request::good(_t('feedback.conf.updated'), [ 'c' => 'configure', 'a' => 'integration' ]);
|
||||
|
@ -175,20 +195,20 @@ class FreshRSS_configure_Controller extends Minz_ActionController {
|
|||
* escape, home, insert, left, page down, page up, return, right, space,
|
||||
* tab and up.
|
||||
*/
|
||||
public function shortcutAction() {
|
||||
public function shortcutAction(): void {
|
||||
$this->view->list_keys = SHORTCUT_KEYS;
|
||||
|
||||
if (Minz_Request::isPost()) {
|
||||
$shortcuts = Minz_Request::param('shortcuts');
|
||||
if (false !== Minz_Request::param('load_default_shortcuts')) {
|
||||
$shortcuts = Minz_Request::paramArray('shortcuts');
|
||||
if (Minz_Request::paramBoolean('load_default_shortcuts')) {
|
||||
$default = Minz_Configuration::load(FRESHRSS_PATH . '/config-user.default.php');
|
||||
$shortcuts = $default['shortcuts'];
|
||||
}
|
||||
FreshRSS_Context::$user_conf->shortcuts = array_map('trim', $shortcuts);
|
||||
FreshRSS_Context::$user_conf->save();
|
||||
FreshRSS_Context::userConf()->shortcuts = array_map('trim', $shortcuts);
|
||||
FreshRSS_Context::userConf()->save();
|
||||
invalidateHttpCache();
|
||||
|
||||
Minz_Request::good(_t('feedback.conf.shortcuts_updated'), array('c' => 'configure', 'a' => 'shortcut'));
|
||||
Minz_Request::good(_t('feedback.conf.shortcuts_updated'), ['c' => 'configure', 'a' => 'shortcut']);
|
||||
}
|
||||
|
||||
FreshRSS_View::prependTitle(_t('conf.shortcut.title') . ' · ');
|
||||
|
@ -206,34 +226,34 @@ class FreshRSS_configure_Controller extends Minz_ActionController {
|
|||
* - number of article to retain per feed (default: 0)
|
||||
* - refresh frequency (default: 0)
|
||||
*/
|
||||
public function archivingAction() {
|
||||
public function archivingAction(): void {
|
||||
if (Minz_Request::isPost()) {
|
||||
if (!Minz_Request::paramBoolean('enable_keep_max')) {
|
||||
if (Minz_Request::paramBoolean('enable_keep_max')) {
|
||||
$keepMax = Minz_Request::paramInt('keep_max') ?: FreshRSS_Feed::ARCHIVING_RETENTION_COUNT_LIMIT;
|
||||
} else {
|
||||
$keepMax = false;
|
||||
} elseif (!$keepMax = Minz_Request::param('keep_max')) {
|
||||
$keepMax = FreshRSS_Feed::ARCHIVING_RETENTION_COUNT_LIMIT;
|
||||
}
|
||||
if (Minz_Request::paramBoolean('enable_keep_period')) {
|
||||
$keepPeriod = FreshRSS_Feed::ARCHIVING_RETENTION_PERIOD;
|
||||
if (is_numeric(Minz_Request::param('keep_period_count')) && preg_match('/^PT?1[YMWDH]$/', Minz_Request::param('keep_period_unit'))) {
|
||||
$keepPeriod = str_replace('1', Minz_Request::param('keep_period_count'), Minz_Request::param('keep_period_unit'));
|
||||
if (is_numeric(Minz_Request::paramString('keep_period_count')) && preg_match('/^PT?1[YMWDH]$/', Minz_Request::paramString('keep_period_unit'))) {
|
||||
$keepPeriod = str_replace('1', Minz_Request::paramString('keep_period_count'), Minz_Request::paramString('keep_period_unit'));
|
||||
}
|
||||
} else {
|
||||
$keepPeriod = false;
|
||||
}
|
||||
|
||||
FreshRSS_Context::$user_conf->ttl_default = Minz_Request::param('ttl_default', FreshRSS_Feed::TTL_DEFAULT);
|
||||
FreshRSS_Context::$user_conf->archiving = [
|
||||
FreshRSS_Context::userConf()->ttl_default = Minz_Request::paramInt('ttl_default') ?: FreshRSS_Feed::TTL_DEFAULT;
|
||||
FreshRSS_Context::userConf()->archiving = [
|
||||
'keep_period' => $keepPeriod,
|
||||
'keep_max' => $keepMax,
|
||||
'keep_min' => Minz_Request::param('keep_min_default', 0),
|
||||
'keep_min' => Minz_Request::paramInt('keep_min_default'),
|
||||
'keep_favourites' => Minz_Request::paramBoolean('keep_favourites'),
|
||||
'keep_labels' => Minz_Request::paramBoolean('keep_labels'),
|
||||
'keep_unreads' => Minz_Request::paramBoolean('keep_unreads'),
|
||||
];
|
||||
FreshRSS_Context::$user_conf->keep_history_default = null; //Legacy < FreshRSS 1.15
|
||||
FreshRSS_Context::$user_conf->old_entries = null; //Legacy < FreshRSS 1.15
|
||||
FreshRSS_Context::$user_conf->save();
|
||||
FreshRSS_Context::userConf()->keep_history_default = null; //Legacy < FreshRSS 1.15
|
||||
FreshRSS_Context::userConf()->old_entries = null; //Legacy < FreshRSS 1.15
|
||||
FreshRSS_Context::userConf()->save();
|
||||
invalidateHttpCache();
|
||||
|
||||
Minz_Request::good(_t('feedback.conf.updated'), [ 'c' => 'configure', 'a' => 'archiving' ]);
|
||||
|
@ -244,15 +264,17 @@ class FreshRSS_configure_Controller extends Minz_ActionController {
|
|||
'keep_period_count' => '3',
|
||||
'keep_period_unit' => 'P1M',
|
||||
];
|
||||
$keepPeriod = FreshRSS_Context::$user_conf->archiving['keep_period'];
|
||||
if (preg_match('/^PT?(?P<count>\d+)[YMWDH]$/', $keepPeriod, $matches)) {
|
||||
$volatile = [
|
||||
'enable_keep_period' => true,
|
||||
'keep_period_count' => $matches['count'],
|
||||
'keep_period_unit' => str_replace($matches['count'], 1, $keepPeriod),
|
||||
];
|
||||
if (!empty(FreshRSS_Context::userConf()->archiving['keep_period'])) {
|
||||
$keepPeriod = FreshRSS_Context::userConf()->archiving['keep_period'];
|
||||
if (preg_match('/^PT?(?P<count>\d+)[YMWDH]$/', $keepPeriod, $matches)) {
|
||||
$volatile = [
|
||||
'enable_keep_period' => true,
|
||||
'keep_period_count' => $matches['count'],
|
||||
'keep_period_unit' => str_replace($matches['count'], '1', $keepPeriod),
|
||||
];
|
||||
}
|
||||
}
|
||||
FreshRSS_Context::$user_conf->volatile = $volatile;
|
||||
FreshRSS_Context::userConf()->volatile = $volatile;
|
||||
|
||||
$entryDAO = FreshRSS_Factory::createEntryDao();
|
||||
$this->view->nb_total = $entryDAO->count();
|
||||
|
@ -277,47 +299,46 @@ class FreshRSS_configure_Controller extends Minz_ActionController {
|
|||
* configuration page and verifies that every user query is runable by
|
||||
* checking if categories and feeds are still in use.
|
||||
*/
|
||||
public function queriesAction() {
|
||||
public function queriesAction(): void {
|
||||
FreshRSS_View::appendScript(Minz_Url::display('/scripts/draggable.js?' . @filemtime(PUBLIC_PATH . '/scripts/draggable.js')));
|
||||
|
||||
$category_dao = FreshRSS_Factory::createCategoryDao();
|
||||
$feed_dao = FreshRSS_Factory::createFeedDao();
|
||||
$tag_dao = FreshRSS_Factory::createTagDao();
|
||||
|
||||
if (Minz_Request::isPost()) {
|
||||
$params = Minz_Request::param('queries', array());
|
||||
/** @var array<int,array{'get'?:string,'name'?:string,'order'?:string,'search'?:string,'state'?:int,'url'?:string,'token'?:string}> $params */
|
||||
$params = Minz_Request::paramArray('queries');
|
||||
|
||||
$queries = [];
|
||||
foreach ($params as $key => $query) {
|
||||
if (!$query['name']) {
|
||||
$key = (int)$key;
|
||||
if (empty($query['name'])) {
|
||||
$query['name'] = _t('conf.query.number', $key + 1);
|
||||
}
|
||||
if ($query['search']) {
|
||||
if (!empty($query['search'])) {
|
||||
$query['search'] = urldecode($query['search']);
|
||||
}
|
||||
$queries[] = new FreshRSS_UserQuery($query, $feed_dao, $category_dao, $tag_dao);
|
||||
$queries[$key] = (new FreshRSS_UserQuery($query, FreshRSS_Context::categories(), FreshRSS_Context::labels()))->toArray();
|
||||
}
|
||||
FreshRSS_Context::$user_conf->queries = $queries;
|
||||
FreshRSS_Context::$user_conf->save();
|
||||
FreshRSS_Context::userConf()->queries = $queries;
|
||||
FreshRSS_Context::userConf()->save();
|
||||
|
||||
Minz_Request::good(_t('feedback.conf.updated'), [ 'c' => 'configure', 'a' => 'queries' ]);
|
||||
} else {
|
||||
$this->view->queries = array();
|
||||
foreach (FreshRSS_Context::$user_conf->queries as $key => $query) {
|
||||
$this->view->queries[$key] = new FreshRSS_UserQuery($query, $feed_dao, $category_dao, $tag_dao);
|
||||
$this->view->queries = [];
|
||||
foreach (FreshRSS_Context::userConf()->queries as $key => $query) {
|
||||
$this->view->queries[intval($key)] = new FreshRSS_UserQuery($query, FreshRSS_Context::categories(), FreshRSS_Context::labels());
|
||||
}
|
||||
}
|
||||
|
||||
$this->view->categories = $category_dao->listCategories(false);
|
||||
$this->view->feeds = $feed_dao->listFeeds();
|
||||
$this->view->tags = $tag_dao->listTags();
|
||||
$this->view->categories = FreshRSS_Context::categories();
|
||||
$this->view->feeds = FreshRSS_Context::feeds();
|
||||
$this->view->tags = FreshRSS_Context::labels();
|
||||
|
||||
$id = Minz_Request::param('id');
|
||||
$this->view->displaySlider = false;
|
||||
if (false !== $id) {
|
||||
$this->view->displaySlider = true;
|
||||
if (Minz_Request::paramTernary('id') !== null) {
|
||||
$id = Minz_Request::paramInt('id');
|
||||
$this->view->query = $this->view->queries[$id];
|
||||
$this->view->queryId = $id;
|
||||
$this->view->displaySlider = true;
|
||||
} else {
|
||||
$this->view->displaySlider = false;
|
||||
}
|
||||
|
||||
FreshRSS_View::prependTitle(_t('conf.query.title') . ' · ');
|
||||
|
@ -328,66 +349,82 @@ class FreshRSS_configure_Controller extends Minz_ActionController {
|
|||
* It displays the query configuration page and handles modifications
|
||||
* applied to the selected query.
|
||||
*/
|
||||
public function queryAction() {
|
||||
$this->view->_layout(false);
|
||||
public function queryAction(): void {
|
||||
if (Minz_Request::paramBoolean('ajax')) {
|
||||
$this->view->_layout(null);
|
||||
}
|
||||
|
||||
$id = Minz_Request::param('id');
|
||||
if (false === $id || !isset(FreshRSS_Context::$user_conf->queries[$id])) {
|
||||
$id = Minz_Request::paramInt('id');
|
||||
if (Minz_Request::paramTernary('id') === null || empty(FreshRSS_Context::userConf()->queries[$id])) {
|
||||
Minz_Error::error(404);
|
||||
return;
|
||||
}
|
||||
|
||||
$category_dao = FreshRSS_Factory::createCategoryDao();
|
||||
$feed_dao = FreshRSS_Factory::createFeedDao();
|
||||
$tag_dao = FreshRSS_Factory::createTagDao();
|
||||
|
||||
$query = new FreshRSS_UserQuery(FreshRSS_Context::$user_conf->queries[$id], $feed_dao, $category_dao, $tag_dao);
|
||||
$query = new FreshRSS_UserQuery(FreshRSS_Context::userConf()->queries[$id], FreshRSS_Context::categories(), FreshRSS_Context::labels());
|
||||
$this->view->query = $query;
|
||||
$this->view->queryId = $id;
|
||||
$this->view->categories = $category_dao->listCategories(false);
|
||||
$this->view->feeds = $feed_dao->listFeeds();
|
||||
$this->view->tags = $tag_dao->listTags();
|
||||
$this->view->categories = FreshRSS_Context::categories();
|
||||
$this->view->feeds = FreshRSS_Context::feeds();
|
||||
$this->view->tags = FreshRSS_Context::labels();
|
||||
|
||||
if (Minz_Request::isPost()) {
|
||||
$params = array_filter(Minz_Request::param('query', []));
|
||||
if (!empty($params['search'])) {
|
||||
$params['search'] = htmlspecialchars_decode($params['search'], ENT_QUOTES);
|
||||
}
|
||||
if (!empty($params['state'])) {
|
||||
$params['state'] = array_sum($params['state']);
|
||||
}
|
||||
$params['url'] = Minz_Url::display(['params' => $params]);
|
||||
$name = Minz_Request::param('name', _t('conf.query.number', $id + 1));
|
||||
$params = array_filter(Minz_Request::paramArray('query'));
|
||||
$queryParams = [];
|
||||
$name = Minz_Request::paramString('name') ?: _t('conf.query.number', $id + 1);
|
||||
if ('' === $name) {
|
||||
$name = _t('conf.query.number', $id + 1);
|
||||
}
|
||||
$params['name'] = $name;
|
||||
if (!empty($params['get']) && is_string($params['get'])) {
|
||||
$queryParams['get'] = htmlspecialchars_decode($params['get'], ENT_QUOTES);
|
||||
}
|
||||
if (!empty($params['order']) && is_string($params['order'])) {
|
||||
$queryParams['order'] = htmlspecialchars_decode($params['order'], ENT_QUOTES);
|
||||
}
|
||||
if (!empty($params['search']) && is_string($params['search'])) {
|
||||
$queryParams['search'] = htmlspecialchars_decode($params['search'], ENT_QUOTES);
|
||||
}
|
||||
if (!empty($params['state']) && is_array($params['state'])) {
|
||||
$queryParams['state'] = (int)(array_sum($params['state']));
|
||||
}
|
||||
if (empty($params['token']) || !is_string($params['token'])) {
|
||||
$queryParams['token'] = FreshRSS_UserQuery::generateToken($name);
|
||||
} else {
|
||||
$queryParams['token'] = $params['token'];
|
||||
}
|
||||
if (!empty($params['shareRss']) && ctype_digit($params['shareRss'])) {
|
||||
$queryParams['shareRss'] = (bool)$params['shareRss'];
|
||||
}
|
||||
if (!empty($params['shareOpml']) && ctype_digit($params['shareOpml'])) {
|
||||
$queryParams['shareOpml'] = (bool)$params['shareOpml'];
|
||||
}
|
||||
$queryParams['url'] = Minz_Url::display(['params' => $queryParams]);
|
||||
$queryParams['name'] = $name;
|
||||
|
||||
$queries = FreshRSS_Context::$user_conf->queries;
|
||||
$queries[$id] = new FreshRSS_UserQuery($params, $feed_dao, $category_dao, $tag_dao);
|
||||
FreshRSS_Context::$user_conf->queries = $queries;
|
||||
FreshRSS_Context::$user_conf->save();
|
||||
$queries = FreshRSS_Context::userConf()->queries;
|
||||
$queries[$id] = (new FreshRSS_UserQuery($queryParams, FreshRSS_Context::categories(), FreshRSS_Context::labels()))->toArray();
|
||||
FreshRSS_Context::userConf()->queries = $queries;
|
||||
FreshRSS_Context::userConf()->save();
|
||||
|
||||
Minz_Request::good(_t('feedback.conf.updated'), [ 'c' => 'configure', 'a' => 'queries', 'params' => ['id' => $id] ]);
|
||||
Minz_Request::good(_t('feedback.conf.updated'), [ 'c' => 'configure', 'a' => 'queries', 'params' => ['id' => (string)$id] ]);
|
||||
}
|
||||
|
||||
FreshRSS_View::prependTitle(_t('conf.query.title') . ' · ' . $query->getName() . ' · ');
|
||||
FreshRSS_View::prependTitle($query->getName() . ' · ' . _t('conf.query.title') . ' · ');
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles query deletion
|
||||
*/
|
||||
public function deleteQueryAction() {
|
||||
$id = Minz_Request::param('id');
|
||||
if (false === $id || !isset(FreshRSS_Context::$user_conf->queries[$id])) {
|
||||
public function deleteQueryAction(): void {
|
||||
$id = Minz_Request::paramInt('id');
|
||||
if (Minz_Request::paramTernary('id') === null || empty(FreshRSS_Context::userConf()->queries[$id])) {
|
||||
Minz_Error::error(404);
|
||||
return;
|
||||
}
|
||||
|
||||
$queries = FreshRSS_Context::$user_conf->queries;
|
||||
$queries = FreshRSS_Context::userConf()->queries;
|
||||
unset($queries[$id]);
|
||||
FreshRSS_Context::$user_conf->queries = $queries;
|
||||
FreshRSS_Context::$user_conf->save();
|
||||
FreshRSS_Context::userConf()->queries = $queries;
|
||||
FreshRSS_Context::userConf()->save();
|
||||
|
||||
Minz_Request::good(_t('feedback.conf.updated'), [ 'c' => 'configure', 'a' => 'queries' ]);
|
||||
}
|
||||
|
@ -399,22 +436,20 @@ class FreshRSS_configure_Controller extends Minz_ActionController {
|
|||
* storage. Before it is saved, the unwanted parameters are unset to keep
|
||||
* lean data.
|
||||
*/
|
||||
public function bookmarkQueryAction() {
|
||||
$category_dao = FreshRSS_Factory::createCategoryDao();
|
||||
$feed_dao = FreshRSS_Factory::createFeedDao();
|
||||
$tag_dao = FreshRSS_Factory::createTagDao();
|
||||
$queries = array();
|
||||
foreach (FreshRSS_Context::$user_conf->queries as $key => $query) {
|
||||
$queries[$key] = new FreshRSS_UserQuery($query, $feed_dao, $category_dao, $tag_dao);
|
||||
public function bookmarkQueryAction(): void {
|
||||
$queries = [];
|
||||
foreach (FreshRSS_Context::userConf()->queries as $key => $query) {
|
||||
$queries[$key] = (new FreshRSS_UserQuery($query, FreshRSS_Context::categories(), FreshRSS_Context::labels()))->toArray();
|
||||
}
|
||||
$params = $_GET;
|
||||
unset($params['name']);
|
||||
unset($params['rid']);
|
||||
$params['url'] = Minz_Url::display(array('params' => $params));
|
||||
$params['url'] = Minz_Url::display(['params' => $params]);
|
||||
$params['name'] = _t('conf.query.number', count($queries) + 1);
|
||||
$queries[] = new FreshRSS_UserQuery($params, $feed_dao, $category_dao, $tag_dao);
|
||||
$queries[] = (new FreshRSS_UserQuery($params, FreshRSS_Context::categories(), FreshRSS_Context::labels()))->toArray();
|
||||
|
||||
FreshRSS_Context::$user_conf->queries = $queries;
|
||||
FreshRSS_Context::$user_conf->save();
|
||||
FreshRSS_Context::userConf()->queries = $queries;
|
||||
FreshRSS_Context::userConf()->save();
|
||||
|
||||
Minz_Request::good(_t('feedback.conf.query_created', $params['name']), [ 'c' => 'configure', 'a' => 'queries' ]);
|
||||
}
|
||||
|
@ -437,22 +472,22 @@ class FreshRSS_configure_Controller extends Minz_ActionController {
|
|||
*
|
||||
* The `force-email-validation` is ignored with PHP < 5.5
|
||||
*/
|
||||
public function systemAction() {
|
||||
public function systemAction(): void {
|
||||
if (!FreshRSS_Auth::hasAccess('admin')) {
|
||||
Minz_Error::error(403);
|
||||
}
|
||||
|
||||
if (Minz_Request::isPost()) {
|
||||
$limits = FreshRSS_Context::$system_conf->limits;
|
||||
$limits['max_registrations'] = Minz_Request::param('max-registrations', 1);
|
||||
$limits['max_feeds'] = Minz_Request::param('max-feeds', 16384);
|
||||
$limits['max_categories'] = Minz_Request::param('max-categories', 16384);
|
||||
$limits['cookie_duration'] = Minz_Request::param('cookie-duration', FreshRSS_Auth::DEFAULT_COOKIE_DURATION);
|
||||
FreshRSS_Context::$system_conf->limits = $limits;
|
||||
FreshRSS_Context::$system_conf->title = Minz_Request::param('instance-name', 'FreshRSS');
|
||||
FreshRSS_Context::$system_conf->auto_update_url = Minz_Request::param('auto-update-url', false);
|
||||
FreshRSS_Context::$system_conf->force_email_validation = Minz_Request::param('force-email-validation', false);
|
||||
FreshRSS_Context::$system_conf->save();
|
||||
$limits = FreshRSS_Context::systemConf()->limits;
|
||||
$limits['max_registrations'] = Minz_Request::paramInt('max-registrations') ?: 1;
|
||||
$limits['max_feeds'] = Minz_Request::paramInt('max-feeds') ?: 16384;
|
||||
$limits['max_categories'] = Minz_Request::paramInt('max-categories') ?: 16384;
|
||||
$limits['cookie_duration'] = Minz_Request::paramInt('cookie-duration') ?: FreshRSS_Auth::DEFAULT_COOKIE_DURATION;
|
||||
FreshRSS_Context::systemConf()->limits = $limits;
|
||||
FreshRSS_Context::systemConf()->title = Minz_Request::paramString('instance-name') ?: 'FreshRSS';
|
||||
FreshRSS_Context::systemConf()->auto_update_url = Minz_Request::paramString('auto-update-url');
|
||||
FreshRSS_Context::systemConf()->force_email_validation = Minz_Request::paramBoolean('force-email-validation');
|
||||
FreshRSS_Context::systemConf()->save();
|
||||
|
||||
invalidateHttpCache();
|
||||
|
||||
|
|
|
@ -1,30 +1,31 @@
|
|||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* Controller to handle every entry actions.
|
||||
*/
|
||||
class FreshRSS_entry_Controller extends Minz_ActionController {
|
||||
class FreshRSS_entry_Controller extends FreshRSS_ActionController {
|
||||
|
||||
/**
|
||||
* JavaScript request or not.
|
||||
* @var bool
|
||||
*/
|
||||
private $ajax = false;
|
||||
private bool $ajax = false;
|
||||
|
||||
/**
|
||||
* This action is called before every other action in that class. It is
|
||||
* the common boiler plate for every action. It is triggered by the
|
||||
* the common boilerplate for every action. It is triggered by the
|
||||
* underlying framework.
|
||||
*/
|
||||
public function firstAction() {
|
||||
#[\Override]
|
||||
public function firstAction(): void {
|
||||
if (!FreshRSS_Auth::hasAccess()) {
|
||||
Minz_Error::error(403);
|
||||
}
|
||||
|
||||
// If ajax request, we do not print layout
|
||||
$this->ajax = Minz_Request::param('ajax');
|
||||
$this->ajax = Minz_Request::paramBoolean('ajax');
|
||||
if ($this->ajax) {
|
||||
$this->view->_layout(false);
|
||||
$this->view->_layout(null);
|
||||
Minz_Request::_param('ajax');
|
||||
}
|
||||
}
|
||||
|
@ -42,15 +43,15 @@ class FreshRSS_entry_Controller extends Minz_ActionController {
|
|||
* - idMax (default: 0)
|
||||
* - is_read (default: true)
|
||||
*/
|
||||
public function readAction() {
|
||||
public function readAction(): void {
|
||||
$id = Minz_Request::param('id');
|
||||
$get = Minz_Request::param('get');
|
||||
$next_get = Minz_Request::param('nextGet', $get);
|
||||
$id_max = Minz_Request::param('idMax', 0);
|
||||
$is_read = (bool)(Minz_Request::param('is_read', true));
|
||||
FreshRSS_Context::$search = new FreshRSS_BooleanSearch(Minz_Request::param('search', ''));
|
||||
$get = Minz_Request::paramString('get');
|
||||
$next_get = Minz_Request::paramString('nextGet') ?: $get;
|
||||
$id_max = Minz_Request::paramString('idMax') ?: '0';
|
||||
$is_read = Minz_Request::paramTernary('is_read') ?? true;
|
||||
FreshRSS_Context::$search = new FreshRSS_BooleanSearch(Minz_Request::paramString('search'));
|
||||
|
||||
FreshRSS_Context::$state = Minz_Request::param('state', 0);
|
||||
FreshRSS_Context::$state = Minz_Request::paramInt('state');
|
||||
if (FreshRSS_Context::isStateEnabled(FreshRSS_Entry::STATE_FAVORITE)) {
|
||||
FreshRSS_Context::$state = FreshRSS_Entry::STATE_FAVORITE;
|
||||
} elseif (FreshRSS_Context::isStateEnabled(FreshRSS_Entry::STATE_NOT_FAVORITE)) {
|
||||
|
@ -59,23 +60,23 @@ class FreshRSS_entry_Controller extends Minz_ActionController {
|
|||
FreshRSS_Context::$state = 0;
|
||||
}
|
||||
|
||||
$params = array();
|
||||
$this->view->tags = array();
|
||||
$params = [];
|
||||
$this->view->tagsForEntries = [];
|
||||
|
||||
$entryDAO = FreshRSS_Factory::createEntryDao();
|
||||
if ($id === false) {
|
||||
if ($id == false) {
|
||||
// id is false? It MUST be a POST request!
|
||||
if (!Minz_Request::isPost()) {
|
||||
Minz_Request::bad(_t('feedback.access.not_found'), array('c' => 'index', 'a' => 'index'));
|
||||
Minz_Request::bad(_t('feedback.access.not_found'), ['c' => 'index', 'a' => 'index']);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!$get) {
|
||||
// No get? Mark all entries as read (from $id_max)
|
||||
$entryDAO->markReadEntries($id_max, $is_read);
|
||||
$entryDAO->markReadEntries($id_max, false, FreshRSS_Feed::PRIORITY_MAIN_STREAM, FreshRSS_Feed::PRIORITY_IMPORTANT, null, 0, $is_read);
|
||||
} else {
|
||||
$type_get = $get[0];
|
||||
$get = substr($get, 2);
|
||||
$get = (int)substr($get, 2);
|
||||
switch($type_get) {
|
||||
case 'c':
|
||||
$entryDAO->markReadCat($get, $id_max, FreshRSS_Context::$search, FreshRSS_Context::$state, $is_read);
|
||||
|
@ -84,16 +85,22 @@ class FreshRSS_entry_Controller extends Minz_ActionController {
|
|||
$entryDAO->markReadFeed($get, $id_max, FreshRSS_Context::$search, FreshRSS_Context::$state, $is_read);
|
||||
break;
|
||||
case 's':
|
||||
$entryDAO->markReadEntries($id_max, true, 0, FreshRSS_Context::$search, FreshRSS_Context::$state, $is_read);
|
||||
$entryDAO->markReadEntries($id_max, true, null, FreshRSS_Feed::PRIORITY_IMPORTANT,
|
||||
FreshRSS_Context::$search, FreshRSS_Context::$state, $is_read);
|
||||
break;
|
||||
case 'a':
|
||||
$entryDAO->markReadEntries($id_max, false, 0, FreshRSS_Context::$search, FreshRSS_Context::$state, $is_read);
|
||||
$entryDAO->markReadEntries($id_max, false, FreshRSS_Feed::PRIORITY_MAIN_STREAM, FreshRSS_Feed::PRIORITY_IMPORTANT,
|
||||
FreshRSS_Context::$search, FreshRSS_Context::$state, $is_read);
|
||||
break;
|
||||
case 'i':
|
||||
$entryDAO->markReadEntries($id_max, false, FreshRSS_Feed::PRIORITY_IMPORTANT, null,
|
||||
FreshRSS_Context::$search, FreshRSS_Context::$state, $is_read);
|
||||
break;
|
||||
case 't':
|
||||
$entryDAO->markReadTag($get, $id_max, FreshRSS_Context::$search, FreshRSS_Context::$state, $is_read);
|
||||
break;
|
||||
case 'T':
|
||||
$entryDAO->markReadTag('', $id_max, FreshRSS_Context::$search, FreshRSS_Context::$state, $is_read);
|
||||
$entryDAO->markReadTag(0, $id_max, FreshRSS_Context::$search, FreshRSS_Context::$state, $is_read);
|
||||
break;
|
||||
}
|
||||
|
||||
|
@ -104,24 +111,26 @@ class FreshRSS_entry_Controller extends Minz_ActionController {
|
|||
}
|
||||
}
|
||||
} else {
|
||||
$ids = is_array($id) ? $id : array($id);
|
||||
$ids = is_array($id) ? $id : [$id];
|
||||
$entryDAO->markRead($ids, $is_read);
|
||||
$tagDAO = FreshRSS_Factory::createTagDao();
|
||||
$tagsForEntries = $tagDAO->getTagsForEntries($ids);
|
||||
$tags = array();
|
||||
$tagsForEntries = $tagDAO->getTagsForEntries($ids) ?: [];
|
||||
$tags = [];
|
||||
foreach ($tagsForEntries as $line) {
|
||||
$tags['t_' . $line['id_tag']][] = $line['id_entry'];
|
||||
}
|
||||
$this->view->tags = $tags;
|
||||
$this->view->tagsForEntries = $tags;
|
||||
}
|
||||
|
||||
if (!$this->ajax) {
|
||||
Minz_Request::good($is_read ? _t('feedback.sub.articles.marked_read') : _t('feedback.sub.articles.marked_unread'),
|
||||
array(
|
||||
'c' => 'index',
|
||||
'a' => 'index',
|
||||
'params' => $params,
|
||||
));
|
||||
Minz_Request::good(
|
||||
$is_read ? _t('feedback.sub.articles.marked_read') : _t('feedback.sub.articles.marked_unread'),
|
||||
[
|
||||
'c' => 'index',
|
||||
'a' => 'index',
|
||||
'params' => $params,
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -133,41 +142,43 @@ class FreshRSS_entry_Controller extends Minz_ActionController {
|
|||
* - is_favorite (default: true)
|
||||
* If id is false, nothing happened.
|
||||
*/
|
||||
public function bookmarkAction() {
|
||||
$id = Minz_Request::param('id');
|
||||
$is_favourite = (bool)Minz_Request::param('is_favorite', true);
|
||||
if ($id !== false) {
|
||||
public function bookmarkAction(): void {
|
||||
$id = Minz_Request::paramString('id');
|
||||
$is_favourite = Minz_Request::paramTernary('is_favorite') ?? true;
|
||||
if ($id != '') {
|
||||
$entryDAO = FreshRSS_Factory::createEntryDao();
|
||||
$entryDAO->markFavorite($id, $is_favourite);
|
||||
}
|
||||
|
||||
if (!$this->ajax) {
|
||||
Minz_Request::forward(array(
|
||||
Minz_Request::forward([
|
||||
'c' => 'index',
|
||||
'a' => 'index',
|
||||
), true);
|
||||
], true);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This action optimizes database to reduce its size.
|
||||
*
|
||||
* This action shouldbe reached by a POST request.
|
||||
* This action should be reached by a POST request.
|
||||
*
|
||||
* @todo move this action in configure controller.
|
||||
* @todo call this action through web-cron when available
|
||||
*/
|
||||
public function optimizeAction() {
|
||||
$url_redirect = array(
|
||||
public function optimizeAction(): void {
|
||||
$url_redirect = [
|
||||
'c' => 'configure',
|
||||
'a' => 'archiving',
|
||||
);
|
||||
];
|
||||
|
||||
if (!Minz_Request::isPost()) {
|
||||
Minz_Request::forward($url_redirect, true);
|
||||
}
|
||||
|
||||
@set_time_limit(300);
|
||||
if (function_exists('set_time_limit')) {
|
||||
@set_time_limit(300);
|
||||
}
|
||||
|
||||
$databaseDAO = FreshRSS_Factory::createDatabaseDAO();
|
||||
$databaseDAO->optimize();
|
||||
|
@ -185,8 +196,10 @@ class FreshRSS_entry_Controller extends Minz_ActionController {
|
|||
* @todo should be a POST request
|
||||
* @todo should be in feedController
|
||||
*/
|
||||
public function purgeAction() {
|
||||
@set_time_limit(300);
|
||||
public function purgeAction(): void {
|
||||
if (function_exists('set_time_limit')) {
|
||||
@set_time_limit(300);
|
||||
}
|
||||
|
||||
$feedDAO = FreshRSS_Factory::createFeedDao();
|
||||
$feeds = $feedDAO->listFeeds();
|
||||
|
@ -197,7 +210,7 @@ class FreshRSS_entry_Controller extends Minz_ActionController {
|
|||
$feedDAO->beginTransaction();
|
||||
|
||||
foreach ($feeds as $feed) {
|
||||
$nb_total += $feed->cleanOldEntries();
|
||||
$nb_total += ($feed->cleanOldEntries() ?: 0);
|
||||
}
|
||||
|
||||
$feedDAO->updateCachedValues();
|
||||
|
@ -207,9 +220,9 @@ class FreshRSS_entry_Controller extends Minz_ActionController {
|
|||
$databaseDAO->minorDbMaintenance();
|
||||
|
||||
invalidateHttpCache();
|
||||
Minz_Request::good(_t('feedback.sub.purge_completed', $nb_total), array(
|
||||
Minz_Request::good(_t('feedback.sub.purge_completed', $nb_total), [
|
||||
'c' => 'configure',
|
||||
'a' => 'archiving'
|
||||
));
|
||||
'a' => 'archiving',
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,9 +1,10 @@
|
|||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* Controller to handle error page.
|
||||
*/
|
||||
class FreshRSS_error_Controller extends Minz_ActionController {
|
||||
class FreshRSS_error_Controller extends FreshRSS_ActionController {
|
||||
/**
|
||||
* This action is the default one for the controller.
|
||||
*
|
||||
|
@ -13,9 +14,10 @@ class FreshRSS_error_Controller extends Minz_ActionController {
|
|||
* - error_code (default: 404)
|
||||
* - error_logs (default: array())
|
||||
*/
|
||||
public function indexAction() {
|
||||
$code_int = Minz_Session::param('error_code', 404);
|
||||
$error_logs = Minz_Session::param('error_logs', array());
|
||||
public function indexAction(): void {
|
||||
$code_int = Minz_Session::paramInt('error_code') ?: 404;
|
||||
/** @var array<string> */
|
||||
$error_logs = Minz_Session::paramArray('error_logs');
|
||||
Minz_Session::_params([
|
||||
'error_code' => false,
|
||||
'error_logs' => false,
|
||||
|
|
|
@ -1,15 +1,17 @@
|
|||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* The controller to manage extensions.
|
||||
*/
|
||||
class FreshRSS_extension_Controller extends Minz_ActionController {
|
||||
class FreshRSS_extension_Controller extends FreshRSS_ActionController {
|
||||
/**
|
||||
* This action is called before every other action in that class. It is
|
||||
* the common boiler plate for every action. It is triggered by the
|
||||
* underlying framework.
|
||||
*/
|
||||
public function firstAction() {
|
||||
#[\Override]
|
||||
public function firstAction(): void {
|
||||
if (!FreshRSS_Auth::hasAccess()) {
|
||||
Minz_Error::error(403);
|
||||
}
|
||||
|
@ -18,14 +20,14 @@ class FreshRSS_extension_Controller extends Minz_ActionController {
|
|||
/**
|
||||
* This action lists all the extensions available to the current user.
|
||||
*/
|
||||
public function indexAction() {
|
||||
public function indexAction(): void {
|
||||
FreshRSS_View::prependTitle(_t('admin.extensions.title') . ' · ');
|
||||
$this->view->extension_list = array(
|
||||
'system' => array(),
|
||||
'user' => array(),
|
||||
);
|
||||
$this->view->extension_list = [
|
||||
'system' => [],
|
||||
'user' => [],
|
||||
];
|
||||
|
||||
$this->view->extensions_installed = array();
|
||||
$this->view->extensions_installed = [];
|
||||
|
||||
$extensions = Minz_ExtensionManager::listExtensions();
|
||||
foreach ($extensions as $ext) {
|
||||
|
@ -33,38 +35,49 @@ class FreshRSS_extension_Controller extends Minz_ActionController {
|
|||
$this->view->extensions_installed[$ext->getEntrypoint()] = $ext->getVersion();
|
||||
}
|
||||
|
||||
$availableExtensions = $this->getAvailableExtensionList();
|
||||
$this->view->available_extensions = $availableExtensions;
|
||||
$this->view->available_extensions = $this->getAvailableExtensionList();
|
||||
}
|
||||
|
||||
/**
|
||||
* fetch extension list from GitHub
|
||||
* @return array<array{'name':string,'author':string,'description':string,'version':string,'entrypoint':string,'type':'system'|'user','url':string,'method':string,'directory':string}>
|
||||
*/
|
||||
protected function getAvailableExtensionList() {
|
||||
protected function getAvailableExtensionList(): array {
|
||||
$extensionListUrl = 'https://raw.githubusercontent.com/FreshRSS/Extensions/master/extensions.json';
|
||||
$json = file_get_contents($extensionListUrl);
|
||||
$json = @file_get_contents($extensionListUrl);
|
||||
|
||||
// we ran into problems, simply ignore them
|
||||
if ($json === false) {
|
||||
Minz_Log::error('Could not fetch available extension from GitHub');
|
||||
return array();
|
||||
return [];
|
||||
}
|
||||
|
||||
// fetch the list as an array
|
||||
/** @var array<string,mixed> $list*/
|
||||
$list = json_decode($json, true);
|
||||
if (empty($list)) {
|
||||
if (!is_array($list) || empty($list['extensions']) || !is_array($list['extensions'])) {
|
||||
Minz_Log::warning('Failed to convert extension file list');
|
||||
return array();
|
||||
return [];
|
||||
}
|
||||
|
||||
// we could use that for comparing and caching later
|
||||
$version = $list['version'];
|
||||
|
||||
// By now, all the needed data is kept in the main extension file.
|
||||
// In the future we could fetch detail information from the extensions metadata.json, but I tend to stick with
|
||||
// the current implementation for now, unless it becomes too much effort maintain the extension list manually
|
||||
$extensions = $list['extensions'];
|
||||
|
||||
$extensions = [];
|
||||
foreach ($list['extensions'] as $extension) {
|
||||
if (isset($extension['version']) && is_numeric($extension['version'])) {
|
||||
$extension['version'] = (string)$extension['version'];
|
||||
}
|
||||
foreach (['author', 'description', 'directory', 'entrypoint', 'method', 'name', 'type', 'url', 'version'] as $key) {
|
||||
if (empty($extension[$key]) || !is_string($extension[$key])) {
|
||||
continue 2;
|
||||
}
|
||||
}
|
||||
if (!in_array($extension['type'], ['system', 'user'], true)) {
|
||||
continue;
|
||||
}
|
||||
$extensions[] = $extension;
|
||||
}
|
||||
return $extensions;
|
||||
}
|
||||
|
||||
|
@ -78,24 +91,27 @@ class FreshRSS_extension_Controller extends Minz_ActionController {
|
|||
* - additional parameters which should be handle by the extension
|
||||
* handleConfigureAction() method (POST request).
|
||||
*/
|
||||
public function configureAction() {
|
||||
if (Minz_Request::param('ajax')) {
|
||||
$this->view->_layout(false);
|
||||
} else {
|
||||
public function configureAction(): void {
|
||||
if (Minz_Request::paramBoolean('ajax')) {
|
||||
$this->view->_layout(null);
|
||||
} elseif (Minz_Request::paramBoolean('slider')) {
|
||||
$this->indexAction();
|
||||
$this->view->_path('extension/index.phtml');
|
||||
}
|
||||
|
||||
$ext_name = urldecode(Minz_Request::param('e'));
|
||||
$ext_name = urldecode(Minz_Request::paramString('e'));
|
||||
$ext = Minz_ExtensionManager::findExtension($ext_name);
|
||||
|
||||
if (is_null($ext)) {
|
||||
if ($ext === null) {
|
||||
Minz_Error::error(404);
|
||||
return;
|
||||
}
|
||||
if ($ext->getType() === 'system' && !FreshRSS_Auth::hasAccess('admin')) {
|
||||
Minz_Error::error(403);
|
||||
return;
|
||||
}
|
||||
|
||||
FreshRSS_View::prependTitle($ext->getName() . ' · ' . _t('admin.extensions.title') . ' · ');
|
||||
$this->view->extension = $ext;
|
||||
$this->view->extension->handleConfigureAction();
|
||||
}
|
||||
|
@ -109,41 +125,52 @@ class FreshRSS_extension_Controller extends Minz_ActionController {
|
|||
* Parameter is:
|
||||
* - e: the extension name (urlencoded).
|
||||
*/
|
||||
public function enableAction() {
|
||||
$url_redirect = array('c' => 'extension', 'a' => 'index');
|
||||
public function enableAction(): void {
|
||||
$url_redirect = ['c' => 'extension', 'a' => 'index'];
|
||||
|
||||
if (Minz_Request::isPost()) {
|
||||
$ext_name = urldecode(Minz_Request::param('e'));
|
||||
$ext_name = urldecode(Minz_Request::paramString('e'));
|
||||
$ext = Minz_ExtensionManager::findExtension($ext_name);
|
||||
|
||||
if (is_null($ext)) {
|
||||
Minz_Request::bad(_t('feedback.extensions.not_found', $ext_name), $url_redirect);
|
||||
return;
|
||||
}
|
||||
|
||||
if ($ext->isEnabled()) {
|
||||
Minz_Request::bad(_t('feedback.extensions.already_enabled', $ext_name), $url_redirect);
|
||||
}
|
||||
|
||||
$conf = null;
|
||||
if ($ext->getType() === 'system' && FreshRSS_Auth::hasAccess('admin')) {
|
||||
$conf = FreshRSS_Context::$system_conf;
|
||||
} elseif ($ext->getType() === 'user') {
|
||||
$conf = FreshRSS_Context::$user_conf;
|
||||
} else {
|
||||
$type = $ext->getType();
|
||||
if ($type !== 'user' && !FreshRSS_Auth::hasAccess('admin')) {
|
||||
Minz_Request::bad(_t('feedback.extensions.no_access', $ext_name), $url_redirect);
|
||||
return;
|
||||
}
|
||||
|
||||
$conf = null;
|
||||
if ($type === 'system') {
|
||||
$conf = FreshRSS_Context::systemConf();
|
||||
} elseif ($type === 'user') {
|
||||
$conf = FreshRSS_Context::userConf();
|
||||
}
|
||||
|
||||
$res = $ext->install();
|
||||
|
||||
if ($res === true) {
|
||||
if ($conf !== null && $res === true) {
|
||||
$ext_list = $conf->extensions_enabled;
|
||||
$ext_list = array_filter($ext_list, static function(string $key) use($type) {
|
||||
// Remove from list the extensions that have disappeared or changed type
|
||||
$extension = Minz_ExtensionManager::findExtension($key);
|
||||
return $extension !== null && $extension->getType() === $type;
|
||||
}, ARRAY_FILTER_USE_KEY);
|
||||
|
||||
$ext_list[$ext_name] = true;
|
||||
$conf->extensions_enabled = $ext_list;
|
||||
$conf->save();
|
||||
|
||||
Minz_Request::good(_t('feedback.extensions.enable.ok', $ext_name), $url_redirect);
|
||||
} else {
|
||||
Minz_Log::warning('Can not enable extension ' . $ext_name . ': ' . $res);
|
||||
Minz_Log::warning('Cannot enable extension ' . $ext_name . ': ' . $res);
|
||||
Minz_Request::bad(_t('feedback.extensions.enable.ko', $ext_name, _url('index', 'logs')), $url_redirect);
|
||||
}
|
||||
}
|
||||
|
@ -160,45 +187,52 @@ class FreshRSS_extension_Controller extends Minz_ActionController {
|
|||
* Parameter is:
|
||||
* - e: the extension name (urlencoded).
|
||||
*/
|
||||
public function disableAction() {
|
||||
$url_redirect = array('c' => 'extension', 'a' => 'index');
|
||||
public function disableAction(): void {
|
||||
$url_redirect = ['c' => 'extension', 'a' => 'index'];
|
||||
|
||||
if (Minz_Request::isPost()) {
|
||||
$ext_name = urldecode(Minz_Request::param('e'));
|
||||
$ext_name = urldecode(Minz_Request::paramString('e'));
|
||||
$ext = Minz_ExtensionManager::findExtension($ext_name);
|
||||
|
||||
if (is_null($ext)) {
|
||||
Minz_Request::bad(_t('feedback.extensions.not_found', $ext_name), $url_redirect);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!$ext->isEnabled()) {
|
||||
Minz_Request::bad(_t('feedback.extensions.not_enabled', $ext_name), $url_redirect);
|
||||
}
|
||||
|
||||
$conf = null;
|
||||
if ($ext->getType() === 'system' && FreshRSS_Auth::hasAccess('admin')) {
|
||||
$conf = FreshRSS_Context::$system_conf;
|
||||
} elseif ($ext->getType() === 'user') {
|
||||
$conf = FreshRSS_Context::$user_conf;
|
||||
} else {
|
||||
$type = $ext->getType();
|
||||
if ($type !== 'user' && !FreshRSS_Auth::hasAccess('admin')) {
|
||||
Minz_Request::bad(_t('feedback.extensions.no_access', $ext_name), $url_redirect);
|
||||
return;
|
||||
}
|
||||
|
||||
$conf = null;
|
||||
if ($type === 'system') {
|
||||
$conf = FreshRSS_Context::systemConf();
|
||||
} elseif ($type === 'user') {
|
||||
$conf = FreshRSS_Context::userConf();
|
||||
}
|
||||
|
||||
$res = $ext->uninstall();
|
||||
|
||||
if ($res === true) {
|
||||
if ($conf !== null && $res === true) {
|
||||
$ext_list = $conf->extensions_enabled;
|
||||
$legacyKey = array_search($ext_name, $ext_list, true);
|
||||
if ($legacyKey !== false) { //Legacy format FreshRSS < 1.11.1
|
||||
unset($ext_list[$legacyKey]);
|
||||
}
|
||||
$ext_list = array_filter($ext_list, static function(string $key) use($type) {
|
||||
// Remove from list the extensions that have disappeared or changed type
|
||||
$extension = Minz_ExtensionManager::findExtension($key);
|
||||
return $extension !== null && $extension->getType() === $type;
|
||||
}, ARRAY_FILTER_USE_KEY);
|
||||
|
||||
$ext_list[$ext_name] = false;
|
||||
$conf->extensions_enabled = $ext_list;
|
||||
$conf->save();
|
||||
|
||||
Minz_Request::good(_t('feedback.extensions.disable.ok', $ext_name), $url_redirect);
|
||||
} else {
|
||||
Minz_Log::warning('Can not unable extension ' . $ext_name . ': ' . $res);
|
||||
Minz_Log::warning('Cannot disable extension ' . $ext_name . ': ' . $res);
|
||||
Minz_Request::bad(_t('feedback.extensions.disable.ko', $ext_name, _url('index', 'logs')), $url_redirect);
|
||||
}
|
||||
}
|
||||
|
@ -215,19 +249,20 @@ class FreshRSS_extension_Controller extends Minz_ActionController {
|
|||
* Parameter is:
|
||||
* -e: extension name (urlencoded)
|
||||
*/
|
||||
public function removeAction() {
|
||||
public function removeAction(): void {
|
||||
if (!FreshRSS_Auth::hasAccess('admin')) {
|
||||
Minz_Error::error(403);
|
||||
}
|
||||
|
||||
$url_redirect = array('c' => 'extension', 'a' => 'index');
|
||||
$url_redirect = ['c' => 'extension', 'a' => 'index'];
|
||||
|
||||
if (Minz_Request::isPost()) {
|
||||
$ext_name = urldecode(Minz_Request::param('e'));
|
||||
$ext_name = urldecode(Minz_Request::paramString('e'));
|
||||
$ext = Minz_ExtensionManager::findExtension($ext_name);
|
||||
|
||||
if (is_null($ext)) {
|
||||
Minz_Request::bad(_t('feedback.extensions.not_found', $ext_name), $url_redirect);
|
||||
return;
|
||||
}
|
||||
|
||||
$res = recursive_unlink($ext->getPath());
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -1,27 +1,26 @@
|
|||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* Controller to handle every import and export actions.
|
||||
*/
|
||||
class FreshRSS_importExport_Controller extends Minz_ActionController {
|
||||
class FreshRSS_importExport_Controller extends FreshRSS_ActionController {
|
||||
|
||||
private $catDAO;
|
||||
private $entryDAO;
|
||||
private $feedDAO;
|
||||
private FreshRSS_EntryDAO $entryDAO;
|
||||
|
||||
private FreshRSS_FeedDAO $feedDAO;
|
||||
|
||||
/**
|
||||
* This action is called before every other action in that class. It is
|
||||
* the common boiler plate for every action. It is triggered by the
|
||||
* the common boilerplate for every action. It is triggered by the
|
||||
* underlying framework.
|
||||
*/
|
||||
public function firstAction() {
|
||||
#[\Override]
|
||||
public function firstAction(): void {
|
||||
if (!FreshRSS_Auth::hasAccess()) {
|
||||
Minz_Error::error(403);
|
||||
}
|
||||
|
||||
require_once(LIB_PATH . '/lib_opml.php');
|
||||
|
||||
$this->catDAO = FreshRSS_Factory::createCategoryDao();
|
||||
$this->entryDAO = FreshRSS_Factory::createEntryDao();
|
||||
$this->feedDAO = FreshRSS_Factory::createFeedDao();
|
||||
}
|
||||
|
@ -29,12 +28,15 @@ class FreshRSS_importExport_Controller extends Minz_ActionController {
|
|||
/**
|
||||
* This action displays the main page for import / export system.
|
||||
*/
|
||||
public function indexAction() {
|
||||
public function indexAction(): void {
|
||||
$this->view->feeds = $this->feedDAO->listFeeds();
|
||||
FreshRSS_View::prependTitle(_t('sub.import_export.title') . ' · ');
|
||||
}
|
||||
|
||||
private static function megabytes($size_str) {
|
||||
/**
|
||||
* @return float|int|string
|
||||
*/
|
||||
private static function megabytes(string $size_str) {
|
||||
switch (substr($size_str, -1)) {
|
||||
case 'M': case 'm': return (int)$size_str;
|
||||
case 'K': case 'k': return (int)$size_str / 1024;
|
||||
|
@ -43,32 +45,40 @@ class FreshRSS_importExport_Controller extends Minz_ActionController {
|
|||
return $size_str;
|
||||
}
|
||||
|
||||
private static function minimumMemory($mb) {
|
||||
/**
|
||||
* @param string|int $mb
|
||||
*/
|
||||
private static function minimumMemory($mb): void {
|
||||
$mb = (int)$mb;
|
||||
$ini = self::megabytes(ini_get('memory_limit'));
|
||||
$ini = self::megabytes(ini_get('memory_limit') ?: '0');
|
||||
if ($ini < $mb) {
|
||||
ini_set('memory_limit', $mb . 'M');
|
||||
}
|
||||
}
|
||||
|
||||
public function importFile($name, $path, $username = null) {
|
||||
/**
|
||||
* @throws FreshRSS_Zip_Exception
|
||||
* @throws FreshRSS_ZipMissing_Exception
|
||||
* @throws Minz_ConfigurationNamespaceException
|
||||
* @throws Minz_PDOConnectionException
|
||||
*/
|
||||
public function importFile(string $name, string $path, ?string $username = null): bool {
|
||||
self::minimumMemory(256);
|
||||
|
||||
$this->catDAO = FreshRSS_Factory::createCategoryDao($username);
|
||||
$this->entryDAO = FreshRSS_Factory::createEntryDao($username);
|
||||
$this->feedDAO = FreshRSS_Factory::createFeedDao($username);
|
||||
|
||||
$type_file = self::guessFileType($name);
|
||||
|
||||
$list_files = array(
|
||||
'opml' => array(),
|
||||
'json_starred' => array(),
|
||||
'json_feed' => array(),
|
||||
'ttrss_starred' => array(),
|
||||
);
|
||||
$list_files = [
|
||||
'opml' => [],
|
||||
'json_starred' => [],
|
||||
'json_feed' => [],
|
||||
'ttrss_starred' => [],
|
||||
];
|
||||
|
||||
// We try to list all files according to their type
|
||||
$list = array();
|
||||
$list = [];
|
||||
if ('zip' === $type_file && extension_loaded('zip')) {
|
||||
$zip = new ZipArchive();
|
||||
$result = $zip->open($path);
|
||||
|
@ -77,6 +87,9 @@ class FreshRSS_importExport_Controller extends Minz_ActionController {
|
|||
throw new FreshRSS_Zip_Exception($result);
|
||||
}
|
||||
for ($i = 0; $i < $zip->numFiles; $i++) {
|
||||
if ($zip->getNameIndex($i) === false) {
|
||||
continue;
|
||||
}
|
||||
$type_zipfile = self::guessFileType($zip->getNameIndex($i));
|
||||
if ('unknown' !== $type_zipfile) {
|
||||
$list_files[$type_zipfile][] = $zip->getFromIndex($i);
|
||||
|
@ -99,7 +112,11 @@ class FreshRSS_importExport_Controller extends Minz_ActionController {
|
|||
$importService = new FreshRSS_Import_Service($username);
|
||||
|
||||
foreach ($list_files['opml'] as $opml_file) {
|
||||
if (!$importService->importOpml($opml_file)) {
|
||||
if ($opml_file === false) {
|
||||
continue;
|
||||
}
|
||||
$importService->importOpml($opml_file);
|
||||
if (!$importService->lastStatus()) {
|
||||
$ok = false;
|
||||
if (FreshRSS_Context::$isCli) {
|
||||
fwrite(STDERR, 'FreshRSS error during OPML import' . "\n");
|
||||
|
@ -109,7 +126,7 @@ class FreshRSS_importExport_Controller extends Minz_ActionController {
|
|||
}
|
||||
}
|
||||
foreach ($list_files['json_starred'] as $article_file) {
|
||||
if (!$this->importJson($article_file, true)) {
|
||||
if (!is_string($article_file) || !$this->importJson($article_file, true)) {
|
||||
$ok = false;
|
||||
if (FreshRSS_Context::$isCli) {
|
||||
fwrite(STDERR, 'FreshRSS error during JSON stars import' . "\n");
|
||||
|
@ -119,7 +136,7 @@ class FreshRSS_importExport_Controller extends Minz_ActionController {
|
|||
}
|
||||
}
|
||||
foreach ($list_files['json_feed'] as $article_file) {
|
||||
if (!$this->importJson($article_file)) {
|
||||
if (!is_string($article_file) || !$this->importJson($article_file)) {
|
||||
$ok = false;
|
||||
if (FreshRSS_Context::$isCli) {
|
||||
fwrite(STDERR, 'FreshRSS error during JSON feeds import' . "\n");
|
||||
|
@ -129,8 +146,8 @@ class FreshRSS_importExport_Controller extends Minz_ActionController {
|
|||
}
|
||||
}
|
||||
foreach ($list_files['ttrss_starred'] as $article_file) {
|
||||
$json = $this->ttrssXmlToJson($article_file);
|
||||
if (!$this->importJson($json, true)) {
|
||||
$json = is_string($article_file) ? $this->ttrssXmlToJson($article_file) : false;
|
||||
if ($json === false || !$this->importJson($json, true)) {
|
||||
$ok = false;
|
||||
if (FreshRSS_Context::$isCli) {
|
||||
fwrite(STDERR, 'FreshRSS error during TT-RSS articles import' . "\n");
|
||||
|
@ -152,9 +169,9 @@ class FreshRSS_importExport_Controller extends Minz_ActionController {
|
|||
* - file (default: nothing!)
|
||||
* Available file types are: zip, json or xml.
|
||||
*/
|
||||
public function importAction() {
|
||||
public function importAction(): void {
|
||||
if (!Minz_Request::isPost()) {
|
||||
Minz_Request::forward(array('c' => 'importExport', 'a' => 'index'), true);
|
||||
Minz_Request::forward(['c' => 'importExport', 'a' => 'index'], true);
|
||||
}
|
||||
|
||||
$file = $_FILES['file'];
|
||||
|
@ -165,22 +182,27 @@ class FreshRSS_importExport_Controller extends Minz_ActionController {
|
|||
Minz_Request::bad(_t('feedback.import_export.file_cannot_be_uploaded'), [ 'c' => 'importExport', 'a' => 'index' ]);
|
||||
}
|
||||
|
||||
@set_time_limit(300);
|
||||
if (function_exists('set_time_limit')) {
|
||||
@set_time_limit(300);
|
||||
}
|
||||
|
||||
$error = false;
|
||||
try {
|
||||
$error = !$this->importFile($file['name'], $file['tmp_name']);
|
||||
} catch (FreshRSS_ZipMissing_Exception $zme) {
|
||||
Minz_Request::bad(_t('feedback.import_export.no_zip_extension'),
|
||||
array('c' => 'importExport', 'a' => 'index'));
|
||||
Minz_Request::bad(
|
||||
_t('feedback.import_export.no_zip_extension'),
|
||||
['c' => 'importExport', 'a' => 'index']
|
||||
);
|
||||
} catch (FreshRSS_Zip_Exception $ze) {
|
||||
Minz_Log::warning('ZIP archive cannot be imported. Error code: ' . $ze->zipErrorCode());
|
||||
Minz_Request::bad(_t('feedback.import_export.zip_error'),
|
||||
array('c' => 'importExport', 'a' => 'index'));
|
||||
Minz_Request::bad(
|
||||
_t('feedback.import_export.zip_error'),
|
||||
['c' => 'importExport', 'a' => 'index']
|
||||
);
|
||||
}
|
||||
|
||||
// And finally, we get import status and redirect to the home page
|
||||
Minz_Session::_param('actualize_feeds', true);
|
||||
$content_notif = $error === true ? _t('feedback.import_export.feeds_imported_with_errors') : _t('feedback.import_export.feeds_imported');
|
||||
Minz_Request::good($content_notif);
|
||||
}
|
||||
|
@ -188,10 +210,10 @@ class FreshRSS_importExport_Controller extends Minz_ActionController {
|
|||
/**
|
||||
* This method tries to guess the file type based on its name.
|
||||
*
|
||||
* Itis a *very* basic guess file type function. Only based on filename.
|
||||
* That's could be improved but should be enough for what we have to do.
|
||||
* It is a *very* basic guess file type function. Only based on filename.
|
||||
* That could be improved but should be enough for what we have to do.
|
||||
*/
|
||||
private static function guessFileType($filename) {
|
||||
private static function guessFileType(string $filename): string {
|
||||
if (substr_compare($filename, '.zip', -4) === 0) {
|
||||
return 'zip';
|
||||
} elseif (stripos($filename, 'opml') !== false) {
|
||||
|
@ -212,20 +234,23 @@ class FreshRSS_importExport_Controller extends Minz_ActionController {
|
|||
return 'unknown';
|
||||
}
|
||||
|
||||
private function ttrssXmlToJson($xml) {
|
||||
/**
|
||||
* @return false|string
|
||||
*/
|
||||
private function ttrssXmlToJson(string $xml) {
|
||||
$table = (array)simplexml_load_string($xml, null, LIBXML_NOBLANKS | LIBXML_NOCDATA);
|
||||
$table['items'] = isset($table['article']) ? $table['article'] : array();
|
||||
$table['items'] = $table['article'] ?? [];
|
||||
unset($table['article']);
|
||||
for ($i = count($table['items']) - 1; $i >= 0; $i--) {
|
||||
$item = (array)($table['items'][$i]);
|
||||
$item = array_filter($item, function ($v) {
|
||||
$item = array_filter($item, static function ($v) {
|
||||
// Filter out empty properties, potentially reported as empty objects
|
||||
return (is_string($v) && trim($v) !== '') || !empty($v);
|
||||
});
|
||||
$item['updated'] = isset($item['updated']) ? strtotime($item['updated']) : '';
|
||||
$item['published'] = $item['updated'];
|
||||
$item['content'] = array('content' => isset($item['content']) ? $item['content'] : '');
|
||||
$item['categories'] = isset($item['tag_cache']) ? array($item['tag_cache']) : array();
|
||||
$item['content'] = ['content' => $item['content'] ?? ''];
|
||||
$item['categories'] = isset($item['tag_cache']) ? [$item['tag_cache']] : [];
|
||||
if (!empty($item['marked'])) {
|
||||
$item['categories'][] = 'user/-/state/com.google/starred';
|
||||
}
|
||||
|
@ -242,12 +267,13 @@ class FreshRSS_importExport_Controller extends Minz_ActionController {
|
|||
}
|
||||
}
|
||||
}
|
||||
$item['alternate'][0]['href'] = isset($item['link']) ? $item['link'] : '';
|
||||
$item['origin'] = array(
|
||||
'title' => isset($item['feed_title']) ? $item['feed_title'] : '',
|
||||
'feedUrl' => isset($item['feed_url']) ? $item['feed_url'] : '',
|
||||
);
|
||||
$item['id'] = isset($item['guid']) ? $item['guid'] : (isset($item['feed_url']) ? $item['feed_url'] : $item['published']);
|
||||
$item['alternate'][0]['href'] = $item['link'] ?? '';
|
||||
$item['origin'] = [
|
||||
'title' => $item['feed_title'] ?? '',
|
||||
'feedUrl' => $item['feed_url'] ?? '',
|
||||
];
|
||||
$item['id'] = $item['guid'] ?? ($item['feed_url'] ?? $item['published']);
|
||||
$item['guid'] = $item['id'];
|
||||
$table['items'][$i] = $item;
|
||||
}
|
||||
return json_encode($table);
|
||||
|
@ -256,13 +282,15 @@ class FreshRSS_importExport_Controller extends Minz_ActionController {
|
|||
/**
|
||||
* This method import a JSON-based file (Google Reader format).
|
||||
*
|
||||
* @param string $article_file the JSON file content.
|
||||
* @param boolean $starred true if articles from the file must be starred.
|
||||
* @return boolean false if an error occured, true otherwise.
|
||||
* $article_file the JSON file content.
|
||||
* true if articles from the file must be starred.
|
||||
* @return bool false if an error occurred, true otherwise.
|
||||
* @throws Minz_ConfigurationNamespaceException
|
||||
* @throws Minz_PDOConnectionException
|
||||
*/
|
||||
private function importJson($article_file, $starred = false) {
|
||||
private function importJson(string $article_file, bool $starred = false): bool {
|
||||
$article_object = json_decode($article_file, true);
|
||||
if ($article_object == null) {
|
||||
if (!is_array($article_object)) {
|
||||
if (FreshRSS_Context::$isCli) {
|
||||
fwrite(STDERR, 'FreshRSS error trying to import a non-JSON file' . "\n");
|
||||
} else {
|
||||
|
@ -270,20 +298,23 @@ class FreshRSS_importExport_Controller extends Minz_ActionController {
|
|||
}
|
||||
return false;
|
||||
}
|
||||
$items = isset($article_object['items']) ? $article_object['items'] : $article_object;
|
||||
$items = $article_object['items'] ?? $article_object;
|
||||
|
||||
$mark_as_read = FreshRSS_Context::$user_conf->mark_when['reception'] ? 1 : 0;
|
||||
$mark_as_read = FreshRSS_Context::userConf()->mark_when['reception'] ? 1 : 0;
|
||||
|
||||
$error = false;
|
||||
$article_to_feed = array();
|
||||
$article_to_feed = [];
|
||||
|
||||
$nb_feeds = count($this->feedDAO->listFeeds());
|
||||
$newFeedGuids = array();
|
||||
$limits = FreshRSS_Context::$system_conf->limits;
|
||||
$newFeedGuids = [];
|
||||
$limits = FreshRSS_Context::systemConf()->limits;
|
||||
|
||||
// First, we check feeds of articles are in DB (and add them if needed).
|
||||
foreach ($items as $item) {
|
||||
if (empty($item['id'])) {
|
||||
foreach ($items as &$item) {
|
||||
if (!isset($item['guid']) && isset($item['id'])) {
|
||||
$item['guid'] = $item['id'];
|
||||
}
|
||||
if (empty($item['guid'])) {
|
||||
continue;
|
||||
}
|
||||
if (empty($item['origin'])) {
|
||||
|
@ -307,7 +338,7 @@ class FreshRSS_importExport_Controller extends Minz_ActionController {
|
|||
$feed = new FreshRSS_Feed($feedUrl);
|
||||
$feed = $this->feedDAO->searchByUrl($feed->url());
|
||||
|
||||
if ($feed == null) {
|
||||
if ($feed === null) {
|
||||
// Feed does not exist in DB,we should to try to add it.
|
||||
if ((!FreshRSS_Context::$isCli) && ($nb_feeds >= $limits['max_feeds'])) {
|
||||
// Oops, no more place!
|
||||
|
@ -316,7 +347,7 @@ class FreshRSS_importExport_Controller extends Minz_ActionController {
|
|||
$feed = $this->addFeedJson($item['origin']);
|
||||
}
|
||||
|
||||
if ($feed == null) {
|
||||
if ($feed === null) {
|
||||
// Still null? It means something went wrong.
|
||||
$error = true;
|
||||
} else {
|
||||
|
@ -325,45 +356,45 @@ class FreshRSS_importExport_Controller extends Minz_ActionController {
|
|||
}
|
||||
|
||||
if ($feed != null) {
|
||||
$article_to_feed[$item['id']] = $feed->id();
|
||||
$article_to_feed[$item['guid']] = $feed->id();
|
||||
if (!isset($newFeedGuids['f_' . $feed->id()])) {
|
||||
$newFeedGuids['f_' . $feed->id()] = array();
|
||||
$newFeedGuids['f_' . $feed->id()] = [];
|
||||
}
|
||||
$newFeedGuids['f_' . $feed->id()][] = safe_ascii($item['id']);
|
||||
$newFeedGuids['f_' . $feed->id()][] = safe_ascii($item['guid']);
|
||||
}
|
||||
}
|
||||
|
||||
$tagDAO = FreshRSS_Factory::createTagDao();
|
||||
$labels = $tagDAO->listTags();
|
||||
$knownLabels = array();
|
||||
$labels = FreshRSS_Context::labels();
|
||||
$knownLabels = [];
|
||||
foreach ($labels as $label) {
|
||||
$knownLabels[$label->name()]['id'] = $label->id();
|
||||
$knownLabels[$label->name()]['articles'] = array();
|
||||
$knownLabels[$label->name()]['articles'] = [];
|
||||
}
|
||||
unset($labels);
|
||||
|
||||
// For each feed, check existing GUIDs already in database.
|
||||
$existingHashForGuids = array();
|
||||
$existingHashForGuids = [];
|
||||
foreach ($newFeedGuids as $feedId => $newGuids) {
|
||||
$existingHashForGuids[$feedId] = $this->entryDAO->listHashForFeedGuids(substr($feedId, 2), $newGuids);
|
||||
$existingHashForGuids[$feedId] = $this->entryDAO->listHashForFeedGuids((int)substr($feedId, 2), $newGuids);
|
||||
}
|
||||
unset($newFeedGuids);
|
||||
|
||||
// Then, articles are imported.
|
||||
$newGuids = array();
|
||||
$newGuids = [];
|
||||
$this->entryDAO->beginTransaction();
|
||||
foreach ($items as $item) {
|
||||
if (empty($item['id']) || empty($article_to_feed[$item['id']])) {
|
||||
foreach ($items as &$item) {
|
||||
if (empty($item['guid']) || empty($article_to_feed[$item['guid']])) {
|
||||
// Related feed does not exist for this entry, do nothing.
|
||||
continue;
|
||||
}
|
||||
|
||||
$feed_id = $article_to_feed[$item['id']];
|
||||
$author = isset($item['author']) ? $item['author'] : '';
|
||||
$is_starred = false;
|
||||
$feed_id = $article_to_feed[$item['guid']];
|
||||
$author = $item['author'] ?? '';
|
||||
$is_starred = null; // null is used to preserve the current state if that item exists and is already starred
|
||||
$is_read = null;
|
||||
$tags = empty($item['categories']) ? array() : $item['categories'];
|
||||
$labels = array();
|
||||
$tags = empty($item['categories']) ? [] : $item['categories'];
|
||||
$labels = [];
|
||||
for ($i = count($tags) - 1; $i >= 0; $i--) {
|
||||
$tag = trim($tags[$i]);
|
||||
if (strpos($tag, 'user/-/') !== false) {
|
||||
|
@ -397,14 +428,17 @@ class FreshRSS_importExport_Controller extends Minz_ActionController {
|
|||
} else {
|
||||
$url = '';
|
||||
}
|
||||
if (!is_string($url)) {
|
||||
$url = '';
|
||||
}
|
||||
|
||||
$title = empty($item['title']) ? $url : $item['title'];
|
||||
|
||||
if (!empty($item['content']['content'])) {
|
||||
if (isset($item['content']['content']) && is_string($item['content']['content'])) {
|
||||
$content = $item['content']['content'];
|
||||
} elseif (!empty($item['summary']['content'])) {
|
||||
} elseif (isset($item['summary']['content']) && is_string($item['summary']['content'])) {
|
||||
$content = $item['summary']['content'];
|
||||
} elseif (!empty($item['content'])) {
|
||||
} elseif (isset($item['content']) && is_string($item['content'])) {
|
||||
$content = $item['content']; //FeedBin
|
||||
} else {
|
||||
$content = '';
|
||||
|
@ -412,20 +446,23 @@ class FreshRSS_importExport_Controller extends Minz_ActionController {
|
|||
$content = sanitizeHTML($content, $url);
|
||||
|
||||
if (!empty($item['published'])) {
|
||||
$published = $item['published'];
|
||||
$published = '' . $item['published'];
|
||||
} elseif (!empty($item['timestampUsec'])) {
|
||||
$published = substr($item['timestampUsec'], 0, -6);
|
||||
$published = substr('' . $item['timestampUsec'], 0, -6);
|
||||
} elseif (!empty($item['updated'])) {
|
||||
$published = $item['updated'];
|
||||
$published = '' . $item['updated'];
|
||||
} else {
|
||||
$published = 0;
|
||||
$published = '0';
|
||||
}
|
||||
if (!ctype_digit('' . $published)) {
|
||||
$published = strtotime($published);
|
||||
if (!ctype_digit($published)) {
|
||||
$published = '' . strtotime($published);
|
||||
}
|
||||
if (strlen($published) > 10) { // Milliseconds, e.g. Feedly
|
||||
$published = substr($published, 0, -3);
|
||||
}
|
||||
|
||||
$entry = new FreshRSS_Entry(
|
||||
$feed_id, $item['id'], $title, $author,
|
||||
$feed_id, $item['guid'], $title, $author,
|
||||
$content, $url, $published, $is_read, $is_starred
|
||||
);
|
||||
$entry->_id(uTimeString());
|
||||
|
@ -437,30 +474,29 @@ class FreshRSS_importExport_Controller extends Minz_ActionController {
|
|||
$newGuids[$entry->guid()] = true;
|
||||
|
||||
$entry = Minz_ExtensionManager::callHook('entry_before_insert', $entry);
|
||||
if ($entry == null) {
|
||||
if (!($entry instanceof FreshRSS_Entry)) {
|
||||
// An extension has returned a null value, there is nothing to insert.
|
||||
continue;
|
||||
}
|
||||
|
||||
$values = $entry->toArray();
|
||||
$ok = false;
|
||||
if (isset($existingHashForGuids['f_' . $feed_id][$entry->guid()])) {
|
||||
$ok = $this->entryDAO->updateEntry($values);
|
||||
$ok = $this->entryDAO->updateEntry($entry->toArray());
|
||||
} else {
|
||||
$ok = $this->entryDAO->addEntry($values);
|
||||
$entry->_lastSeen(time());
|
||||
$ok = $this->entryDAO->addEntry($entry->toArray());
|
||||
}
|
||||
|
||||
foreach ($labels as $labelName) {
|
||||
if (empty($knownLabels[$labelName]['id'])) {
|
||||
$labelId = $tagDAO->addTag(array('name' => $labelName));
|
||||
$labelId = $tagDAO->addTag(['name' => $labelName]);
|
||||
$knownLabels[$labelName]['id'] = $labelId;
|
||||
$knownLabels[$labelName]['articles'] = array();
|
||||
$knownLabels[$labelName]['articles'] = [];
|
||||
}
|
||||
$knownLabels[$labelName]['articles'][] = array(
|
||||
//'id' => $entry->id(), //ID changes after commitNewEntries()
|
||||
'id_feed' => $entry->feed(),
|
||||
'guid' => $entry->guid(),
|
||||
);
|
||||
$knownLabels[$labelName]['articles'][] = [
|
||||
//'id' => $entry->id(), //ID changes after commitNewEntries()
|
||||
'id_feed' => $entry->feedId(),
|
||||
'guid' => $entry->guid(),
|
||||
];
|
||||
}
|
||||
|
||||
$error |= ($ok === false);
|
||||
|
@ -475,6 +511,9 @@ class FreshRSS_importExport_Controller extends Minz_ActionController {
|
|||
$this->entryDAO->beginTransaction();
|
||||
foreach ($knownLabels as $labelName => $knownLabel) {
|
||||
$labelId = $knownLabel['id'];
|
||||
if (!$labelId) {
|
||||
continue;
|
||||
}
|
||||
foreach ($knownLabel['articles'] as $article) {
|
||||
$entryId = $this->entryDAO->searchIdByGuid($article['id_feed'], $article['guid']);
|
||||
if ($entryId != null) {
|
||||
|
@ -492,11 +531,10 @@ class FreshRSS_importExport_Controller extends Minz_ActionController {
|
|||
/**
|
||||
* This method import a JSON-based feed (Google Reader format).
|
||||
*
|
||||
* @param array $origin represents a feed.
|
||||
* @return FreshRSS_Feed if feed is in database at the end of the process,
|
||||
* else null.
|
||||
* @param array<string,string> $origin represents a feed.
|
||||
* @return FreshRSS_Feed|null if feed is in database at the end of the process, else null.
|
||||
*/
|
||||
private function addFeedJson($origin) {
|
||||
private function addFeedJson(array $origin): ?FreshRSS_Feed {
|
||||
$return = null;
|
||||
if (!empty($origin['feedUrl'])) {
|
||||
$url = $origin['feedUrl'];
|
||||
|
@ -517,11 +555,11 @@ class FreshRSS_importExport_Controller extends Minz_ActionController {
|
|||
try {
|
||||
// Create a Feed object and add it in database.
|
||||
$feed = new FreshRSS_Feed($url);
|
||||
$feed->_category(FreshRSS_CategoryDAO::DEFAULTCATEGORYID);
|
||||
$feed->_categoryId(FreshRSS_CategoryDAO::DEFAULTCATEGORYID);
|
||||
$feed->_name($name);
|
||||
$feed->_website($website);
|
||||
if (!empty($origin['disable'])) {
|
||||
$feed->_ttl(-1 * FreshRSS_Context::$user_conf->ttl_default);
|
||||
$feed->_mute(true);
|
||||
}
|
||||
|
||||
// Call the extension hook
|
||||
|
@ -558,61 +596,58 @@ class FreshRSS_importExport_Controller extends Minz_ActionController {
|
|||
* - export_labelled (default: false)
|
||||
* - export_feeds (default: array()) a list of feed ids
|
||||
*/
|
||||
public function exportAction() {
|
||||
public function exportAction(): void {
|
||||
if (!Minz_Request::isPost()) {
|
||||
return Minz_Request::forward(
|
||||
array('c' => 'importExport', 'a' => 'index'),
|
||||
true
|
||||
);
|
||||
Minz_Request::forward(['c' => 'importExport', 'a' => 'index'], true);
|
||||
return;
|
||||
}
|
||||
|
||||
$username = Minz_Session::param('currentUser');
|
||||
$username = Minz_User::name() ?? '_';
|
||||
$export_service = new FreshRSS_Export_Service($username);
|
||||
|
||||
$export_opml = Minz_Request::param('export_opml', false);
|
||||
$export_starred = Minz_Request::param('export_starred', false);
|
||||
$export_labelled = Minz_Request::param('export_labelled', false);
|
||||
$export_feeds = Minz_Request::param('export_feeds', array());
|
||||
$export_opml = Minz_Request::paramBoolean('export_opml');
|
||||
$export_starred = Minz_Request::paramBoolean('export_starred');
|
||||
$export_labelled = Minz_Request::paramBoolean('export_labelled');
|
||||
/** @var array<numeric-string> */
|
||||
$export_feeds = Minz_Request::paramArray('export_feeds');
|
||||
$max_number_entries = 50;
|
||||
|
||||
$exported_files = [];
|
||||
|
||||
if ($export_opml) {
|
||||
list($filename, $content) = $export_service->generateOpml();
|
||||
[$filename, $content] = $export_service->generateOpml();
|
||||
$exported_files[$filename] = $content;
|
||||
}
|
||||
|
||||
// Starred and labelled entries are merged in the same `starred` file
|
||||
// to avoid duplication of content.
|
||||
if ($export_starred && $export_labelled) {
|
||||
list($filename, $content) = $export_service->generateStarredEntries('ST');
|
||||
[$filename, $content] = $export_service->generateStarredEntries('ST');
|
||||
$exported_files[$filename] = $content;
|
||||
} elseif ($export_starred) {
|
||||
list($filename, $content) = $export_service->generateStarredEntries('S');
|
||||
[$filename, $content] = $export_service->generateStarredEntries('S');
|
||||
$exported_files[$filename] = $content;
|
||||
} elseif ($export_labelled) {
|
||||
list($filename, $content) = $export_service->generateStarredEntries('T');
|
||||
[$filename, $content] = $export_service->generateStarredEntries('T');
|
||||
$exported_files[$filename] = $content;
|
||||
}
|
||||
|
||||
foreach ($export_feeds as $feed_id) {
|
||||
$result = $export_service->generateFeedEntries($feed_id, $max_number_entries);
|
||||
$result = $export_service->generateFeedEntries((int)$feed_id, $max_number_entries);
|
||||
if (!$result) {
|
||||
// It means the actual feed_id doesn't correspond to any existing feed
|
||||
// It means the actual feed_id doesn’t correspond to any existing feed
|
||||
continue;
|
||||
}
|
||||
|
||||
list($filename, $content) = $result;
|
||||
[$filename, $content] = $result;
|
||||
$exported_files[$filename] = $content;
|
||||
}
|
||||
|
||||
$nb_files = count($exported_files);
|
||||
if ($nb_files <= 0) {
|
||||
// There's nothing to do, there're no files to export
|
||||
return Minz_Request::forward(
|
||||
array('c' => 'importExport', 'a' => 'index'),
|
||||
true
|
||||
);
|
||||
// There’s nothing to do, there are no files to export
|
||||
Minz_Request::forward(['c' => 'importExport', 'a' => 'index'], true);
|
||||
return;
|
||||
}
|
||||
|
||||
if ($nb_files === 1) {
|
||||
|
@ -620,23 +655,29 @@ class FreshRSS_importExport_Controller extends Minz_ActionController {
|
|||
$filename = key($exported_files);
|
||||
$content = $exported_files[$filename];
|
||||
} else {
|
||||
// More files? Let's compress them in a Zip archive
|
||||
// More files? Let’s compress them in a Zip archive
|
||||
if (!extension_loaded('zip')) {
|
||||
// Oops, there is no ZIP extension!
|
||||
return Minz_Request::bad(
|
||||
Minz_Request::bad(
|
||||
_t('feedback.import_export.export_no_zip_extension'),
|
||||
array('c' => 'importExport', 'a' => 'index')
|
||||
['c' => 'importExport', 'a' => 'index']
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
list($filename, $content) = $export_service->zip($exported_files);
|
||||
[$filename, $content] = $export_service->zip($exported_files);
|
||||
}
|
||||
|
||||
if (!is_string($content)) {
|
||||
Minz_Request::bad(_t('feedback.import_export.zip_error'), ['c' => 'importExport', 'a' => 'index']);
|
||||
return;
|
||||
}
|
||||
|
||||
$content_type = self::filenameToContentType($filename);
|
||||
header('Content-Type: ' . $content_type);
|
||||
header('Content-disposition: attachment; filename="' . $filename . '"');
|
||||
|
||||
$this->view->_layout(false);
|
||||
$this->view->_layout(null);
|
||||
$this->view->content = $content;
|
||||
}
|
||||
|
||||
|
@ -645,12 +686,8 @@ class FreshRSS_importExport_Controller extends Minz_ActionController {
|
|||
*
|
||||
* If the type of the filename is not supported, it returns
|
||||
* `application/octet-stream` by default.
|
||||
*
|
||||
* @param string $filename
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
private static function filenameToContentType($filename) {
|
||||
private static function filenameToContentType(string $filename): string {
|
||||
$filetype = self::guessFileType($filename);
|
||||
switch ($filetype) {
|
||||
case 'zip':
|
||||
|
|
|
@ -1,33 +1,47 @@
|
|||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* This class handles main actions of FreshRSS.
|
||||
*/
|
||||
class FreshRSS_index_Controller extends Minz_ActionController {
|
||||
class FreshRSS_index_Controller extends FreshRSS_ActionController {
|
||||
|
||||
#[\Override]
|
||||
public function firstAction(): void {
|
||||
$this->view->html_url = Minz_Url::display(['c' => 'index', 'a' => 'index'], 'html', 'root');
|
||||
}
|
||||
|
||||
/**
|
||||
* This action only redirect on the default view mode (normal or global)
|
||||
*/
|
||||
public function indexAction() {
|
||||
$prefered_output = FreshRSS_Context::$user_conf->view_mode;
|
||||
Minz_Request::forward(array(
|
||||
public function indexAction(): void {
|
||||
$preferred_output = FreshRSS_Context::userConf()->view_mode;
|
||||
Minz_Request::forward([
|
||||
'c' => 'index',
|
||||
'a' => $prefered_output
|
||||
));
|
||||
'a' => $preferred_output,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* This action displays the normal view of FreshRSS.
|
||||
*/
|
||||
public function normalAction() {
|
||||
$allow_anonymous = FreshRSS_Context::$system_conf->allow_anonymous;
|
||||
public function normalAction(): void {
|
||||
$allow_anonymous = FreshRSS_Context::systemConf()->allow_anonymous;
|
||||
if (!FreshRSS_Auth::hasAccess() && !$allow_anonymous) {
|
||||
Minz_Request::forward(array('c' => 'auth', 'a' => 'login'));
|
||||
Minz_Request::forward(['c' => 'auth', 'a' => 'login']);
|
||||
return;
|
||||
}
|
||||
|
||||
$id = Minz_Request::paramInt('id');
|
||||
if ($id !== 0) {
|
||||
$view = Minz_Request::paramString('a');
|
||||
$url_redirect = ['c' => 'subscription', 'a' => 'feed', 'params' => ['id' => (string)$id, 'from' => $view]];
|
||||
Minz_Request::forward($url_redirect, true);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
$this->updateContext();
|
||||
FreshRSS_Context::updateUsingRequest(true);
|
||||
} catch (FreshRSS_Context_Exception $e) {
|
||||
Minz_Error::error(404);
|
||||
}
|
||||
|
@ -39,7 +53,7 @@ class FreshRSS_index_Controller extends Minz_ActionController {
|
|||
'media-src' => '*',
|
||||
]);
|
||||
|
||||
$this->view->categories = FreshRSS_Context::$categories;
|
||||
$this->view->categories = FreshRSS_Context::categories();
|
||||
|
||||
$this->view->rss_title = FreshRSS_Context::$name . ' | ' . FreshRSS_View::title();
|
||||
$title = FreshRSS_Context::$name;
|
||||
|
@ -50,24 +64,18 @@ class FreshRSS_index_Controller extends Minz_ActionController {
|
|||
|
||||
FreshRSS_Context::$id_max = time() . '000000';
|
||||
|
||||
$this->view->callbackBeforeFeeds = function ($view) {
|
||||
try {
|
||||
$tagDAO = FreshRSS_Factory::createTagDao();
|
||||
$view->tags = $tagDAO->listTags(true);
|
||||
$view->nbUnreadTags = 0;
|
||||
foreach ($view->tags as $tag) {
|
||||
$view->nbUnreadTags += $tag->nbUnread();
|
||||
}
|
||||
} catch (Exception $e) {
|
||||
Minz_Log::notice($e->getMessage());
|
||||
$this->view->callbackBeforeFeeds = static function (FreshRSS_View $view) {
|
||||
$view->tags = FreshRSS_Context::labels(true);
|
||||
$view->nbUnreadTags = 0;
|
||||
foreach ($view->tags as $tag) {
|
||||
$view->nbUnreadTags += $tag->nbUnread();
|
||||
}
|
||||
};
|
||||
|
||||
$this->view->callbackBeforeEntries = function ($view) {
|
||||
$this->view->callbackBeforeEntries = static function (FreshRSS_View $view) {
|
||||
try {
|
||||
FreshRSS_Context::$number++; //+1 for pagination
|
||||
$view->entries = FreshRSS_index_Controller::listEntriesByContext();
|
||||
FreshRSS_Context::$number--;
|
||||
// +1 to account for paging logic
|
||||
$view->entries = FreshRSS_index_Controller::listEntriesByContext(FreshRSS_Context::$number + 1);
|
||||
ob_start(); //Buffer "one entry at a time"
|
||||
} catch (FreshRSS_EntriesGetter_Exception $e) {
|
||||
Minz_Log::notice($e->getMessage());
|
||||
|
@ -75,9 +83,9 @@ class FreshRSS_index_Controller extends Minz_ActionController {
|
|||
}
|
||||
};
|
||||
|
||||
$this->view->callbackBeforePagination = function ($view, $nbEntries, $lastEntry) {
|
||||
$this->view->callbackBeforePagination = static function (?FreshRSS_View $view, int $nbEntries, FreshRSS_Entry $lastEntry) {
|
||||
if ($nbEntries >= FreshRSS_Context::$number) {
|
||||
//We have enough entries: we discard the last one to use it for the next pagination
|
||||
//We have enough entries: we discard the last one to use it for the next articles' page
|
||||
ob_clean();
|
||||
FreshRSS_Context::$next_id = $lastEntry->id();
|
||||
}
|
||||
|
@ -90,17 +98,17 @@ class FreshRSS_index_Controller extends Minz_ActionController {
|
|||
*
|
||||
* @todo: change this view into specific CSS rules?
|
||||
*/
|
||||
public function readerAction() {
|
||||
public function readerAction(): void {
|
||||
$this->normalAction();
|
||||
}
|
||||
|
||||
/**
|
||||
* This action displays the global view of FreshRSS.
|
||||
*/
|
||||
public function globalAction() {
|
||||
$allow_anonymous = FreshRSS_Context::$system_conf->allow_anonymous;
|
||||
public function globalAction(): void {
|
||||
$allow_anonymous = FreshRSS_Context::systemConf()->allow_anonymous;
|
||||
if (!FreshRSS_Auth::hasAccess() && !$allow_anonymous) {
|
||||
Minz_Request::forward(array('c' => 'auth', 'a' => 'login'));
|
||||
Minz_Request::forward(['c' => 'auth', 'a' => 'login']);
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -108,12 +116,12 @@ class FreshRSS_index_Controller extends Minz_ActionController {
|
|||
FreshRSS_View::appendScript(Minz_Url::display('/scripts/global_view.js?' . @filemtime(PUBLIC_PATH . '/scripts/global_view.js')));
|
||||
|
||||
try {
|
||||
$this->updateContext();
|
||||
FreshRSS_Context::updateUsingRequest(true);
|
||||
} catch (FreshRSS_Context_Exception $e) {
|
||||
Minz_Error::error(404);
|
||||
}
|
||||
|
||||
$this->view->categories = FreshRSS_Context::$categories;
|
||||
$this->view->categories = FreshRSS_Context::categories();
|
||||
|
||||
$this->view->rss_title = FreshRSS_Context::$name . ' | ' . FreshRSS_View::title();
|
||||
$title = _t('index.feed.title_global');
|
||||
|
@ -132,11 +140,12 @@ class FreshRSS_index_Controller extends Minz_ActionController {
|
|||
|
||||
/**
|
||||
* This action displays the RSS feed of FreshRSS.
|
||||
* @deprecated See user query RSS sharing instead
|
||||
*/
|
||||
public function rssAction() {
|
||||
$allow_anonymous = FreshRSS_Context::$system_conf->allow_anonymous;
|
||||
$token = FreshRSS_Context::$user_conf->token;
|
||||
$token_param = Minz_Request::param('token', '');
|
||||
public function rssAction(): void {
|
||||
$allow_anonymous = FreshRSS_Context::systemConf()->allow_anonymous;
|
||||
$token = FreshRSS_Context::userConf()->token;
|
||||
$token_param = Minz_Request::paramString('token');
|
||||
$token_is_ok = ($token != '' && $token === $token_param);
|
||||
|
||||
// Check if user has access.
|
||||
|
@ -147,7 +156,7 @@ class FreshRSS_index_Controller extends Minz_ActionController {
|
|||
}
|
||||
|
||||
try {
|
||||
$this->updateContext();
|
||||
FreshRSS_Context::updateUsingRequest(false);
|
||||
} catch (FreshRSS_Context_Exception $e) {
|
||||
Minz_Error::error(404);
|
||||
}
|
||||
|
@ -159,90 +168,106 @@ class FreshRSS_index_Controller extends Minz_ActionController {
|
|||
Minz_Error::error(404);
|
||||
}
|
||||
|
||||
// No layout for RSS output.
|
||||
$this->view->url = PUBLIC_TO_INDEX_PATH . '/' . (empty($_SERVER['QUERY_STRING']) ? '' : '?' . $_SERVER['QUERY_STRING']);
|
||||
$this->view->html_url = Minz_Url::display('', 'html', true);
|
||||
$this->view->rss_title = FreshRSS_Context::$name . ' | ' . FreshRSS_View::title();
|
||||
$this->view->_layout(false);
|
||||
$this->view->rss_url = htmlspecialchars(
|
||||
PUBLIC_TO_INDEX_PATH . '/' . (empty($_SERVER['QUERY_STRING']) ? '' : '?' . $_SERVER['QUERY_STRING']), ENT_COMPAT, 'UTF-8');
|
||||
|
||||
// No layout for RSS output.
|
||||
$this->view->_layout(null);
|
||||
header('Content-Type: application/rss+xml; charset=utf-8');
|
||||
}
|
||||
|
||||
/**
|
||||
* This action updates the Context object by using request parameters.
|
||||
*
|
||||
* Parameters are:
|
||||
* - state (default: conf->default_view)
|
||||
* - search (default: empty string)
|
||||
* - order (default: conf->sort_order)
|
||||
* - nb (default: conf->posts_per_page)
|
||||
* - next (default: empty string)
|
||||
* - hours (default: 0)
|
||||
* @deprecated See user query OPML sharing instead
|
||||
*/
|
||||
private function updateContext() {
|
||||
if (empty(FreshRSS_Context::$categories)) {
|
||||
$catDAO = FreshRSS_Factory::createCategoryDao();
|
||||
FreshRSS_Context::$categories = $catDAO->listSortedCategories();
|
||||
public function opmlAction(): void {
|
||||
$allow_anonymous = FreshRSS_Context::systemConf()->allow_anonymous;
|
||||
$token = FreshRSS_Context::userConf()->token;
|
||||
$token_param = Minz_Request::paramString('token');
|
||||
$token_is_ok = ($token != '' && $token === $token_param);
|
||||
|
||||
// Check if user has access.
|
||||
if (!FreshRSS_Auth::hasAccess() && !$allow_anonymous && !$token_is_ok) {
|
||||
Minz_Error::error(403);
|
||||
}
|
||||
|
||||
// Update number of read / unread variables.
|
||||
$entryDAO = FreshRSS_Factory::createEntryDao();
|
||||
FreshRSS_Context::$total_starred = $entryDAO->countUnreadReadFavorites();
|
||||
FreshRSS_Context::$total_unread = FreshRSS_CategoryDAO::CountUnreads(
|
||||
FreshRSS_Context::$categories, 1
|
||||
);
|
||||
|
||||
FreshRSS_Context::_get(Minz_Request::param('get', 'a'));
|
||||
|
||||
FreshRSS_Context::$state = Minz_Request::param(
|
||||
'state', FreshRSS_Context::$user_conf->default_state
|
||||
);
|
||||
$state_forced_by_user = Minz_Request::param('state', false) !== false;
|
||||
if (FreshRSS_Context::$user_conf->default_view === 'adaptive' &&
|
||||
FreshRSS_Context::$get_unread <= 0 &&
|
||||
!FreshRSS_Context::isStateEnabled(FreshRSS_Entry::STATE_READ) &&
|
||||
!$state_forced_by_user) {
|
||||
FreshRSS_Context::$state |= FreshRSS_Entry::STATE_READ;
|
||||
try {
|
||||
FreshRSS_Context::updateUsingRequest(false);
|
||||
} catch (FreshRSS_Context_Exception $e) {
|
||||
Minz_Error::error(404);
|
||||
}
|
||||
|
||||
FreshRSS_Context::$search = new FreshRSS_BooleanSearch(Minz_Request::param('search', ''));
|
||||
FreshRSS_Context::$order = Minz_Request::param(
|
||||
'order', FreshRSS_Context::$user_conf->sort_order
|
||||
);
|
||||
FreshRSS_Context::$number = intval(Minz_Request::param('nb', FreshRSS_Context::$user_conf->posts_per_page));
|
||||
if (FreshRSS_Context::$number > FreshRSS_Context::$user_conf->max_posts_per_rss) {
|
||||
FreshRSS_Context::$number = max(
|
||||
FreshRSS_Context::$user_conf->max_posts_per_rss,
|
||||
FreshRSS_Context::$user_conf->posts_per_page);
|
||||
$get = FreshRSS_Context::currentGet(true);
|
||||
$type = (string)$get[0];
|
||||
$id = (int)$get[1];
|
||||
|
||||
$this->view->excludeMutedFeeds = $type !== 'f'; // Exclude muted feeds except when we focus on a feed
|
||||
|
||||
switch ($type) {
|
||||
case 'a':
|
||||
$this->view->categories = FreshRSS_Context::categories();
|
||||
break;
|
||||
case 'c':
|
||||
$cat = FreshRSS_Context::categories()[$id] ?? null;
|
||||
if ($cat == null) {
|
||||
Minz_Error::error(404);
|
||||
return;
|
||||
}
|
||||
$this->view->categories = [ $cat->id() => $cat ];
|
||||
break;
|
||||
case 'f':
|
||||
// We most likely already have the feed object in cache
|
||||
$feed = FreshRSS_Category::findFeed(FreshRSS_Context::categories(), $id);
|
||||
if ($feed === null) {
|
||||
$feedDAO = FreshRSS_Factory::createFeedDao();
|
||||
$feed = $feedDAO->searchById($id);
|
||||
if ($feed == null) {
|
||||
Minz_Error::error(404);
|
||||
return;
|
||||
}
|
||||
}
|
||||
$this->view->feeds = [ $feed->id() => $feed ];
|
||||
break;
|
||||
case 's':
|
||||
case 't':
|
||||
case 'T':
|
||||
default:
|
||||
Minz_Error::error(404);
|
||||
return;
|
||||
}
|
||||
FreshRSS_Context::$first_id = Minz_Request::param('next', '');
|
||||
FreshRSS_Context::$sinceHours = intval(Minz_Request::param('hours', 0));
|
||||
|
||||
// No layout for OPML output.
|
||||
$this->view->_layout(null);
|
||||
header('Content-Type: application/xml; charset=utf-8');
|
||||
}
|
||||
|
||||
/**
|
||||
* This method returns a list of entries based on the Context object.
|
||||
* @param int $postsPerPage override `FreshRSS_Context::$number`
|
||||
* @return Traversable<FreshRSS_Entry>
|
||||
* @throws FreshRSS_EntriesGetter_Exception
|
||||
*/
|
||||
public static function listEntriesByContext() {
|
||||
public static function listEntriesByContext(?int $postsPerPage = null): Traversable {
|
||||
$entryDAO = FreshRSS_Factory::createEntryDao();
|
||||
|
||||
$get = FreshRSS_Context::currentGet(true);
|
||||
if (is_array($get)) {
|
||||
$type = $get[0];
|
||||
$id = $get[1];
|
||||
$id = (int)($get[1]);
|
||||
} else {
|
||||
$type = $get;
|
||||
$id = '';
|
||||
$id = 0;
|
||||
}
|
||||
|
||||
$limit = FreshRSS_Context::$number;
|
||||
|
||||
$date_min = 0;
|
||||
if (FreshRSS_Context::$sinceHours) {
|
||||
if (FreshRSS_Context::$sinceHours > 0) {
|
||||
$date_min = time() - (FreshRSS_Context::$sinceHours * 3600);
|
||||
$limit = FreshRSS_Context::$user_conf->max_posts_per_rss;
|
||||
}
|
||||
|
||||
foreach ($entryDAO->listWhere(
|
||||
$type, $id, FreshRSS_Context::$state, FreshRSS_Context::$order,
|
||||
$limit, FreshRSS_Context::$first_id,
|
||||
$postsPerPage ?? FreshRSS_Context::$number, FreshRSS_Context::$offset, FreshRSS_Context::$first_id,
|
||||
FreshRSS_Context::$search, $date_min)
|
||||
as $entry) {
|
||||
yield $entry;
|
||||
|
@ -252,20 +277,21 @@ class FreshRSS_index_Controller extends Minz_ActionController {
|
|||
/**
|
||||
* This action displays the about page of FreshRSS.
|
||||
*/
|
||||
public function aboutAction() {
|
||||
public function aboutAction(): void {
|
||||
FreshRSS_View::prependTitle(_t('index.about.title') . ' · ');
|
||||
}
|
||||
|
||||
/**
|
||||
* This action displays the EULA page of FreshRSS.
|
||||
* This action displays the EULA/TOS (Terms of Service) page of FreshRSS.
|
||||
* This page is enabled only if admin created a data/tos.html file.
|
||||
* The content of the page is the content of data/tos.html.
|
||||
* It returns 404 if there is no EULA.
|
||||
* It returns 404 if there is no EULA/TOS.
|
||||
*/
|
||||
public function tosAction() {
|
||||
$terms_of_service = file_get_contents(join_path(DATA_PATH, 'tos.html'));
|
||||
if (!$terms_of_service) {
|
||||
public function tosAction(): void {
|
||||
$terms_of_service = file_get_contents(TOS_FILENAME);
|
||||
if ($terms_of_service === false) {
|
||||
Minz_Error::error(404);
|
||||
return;
|
||||
}
|
||||
|
||||
$this->view->terms_of_service = $terms_of_service;
|
||||
|
@ -276,7 +302,7 @@ class FreshRSS_index_Controller extends Minz_ActionController {
|
|||
/**
|
||||
* This action displays logs of FreshRSS for the current user.
|
||||
*/
|
||||
public function logsAction() {
|
||||
public function logsAction(): void {
|
||||
if (!FreshRSS_Auth::hasAccess()) {
|
||||
Minz_Error::error(403);
|
||||
}
|
||||
|
@ -290,7 +316,7 @@ class FreshRSS_index_Controller extends Minz_ActionController {
|
|||
$logs = FreshRSS_LogDAO::lines(); //TODO: ask only the necessary lines
|
||||
|
||||
//gestion pagination
|
||||
$page = Minz_Request::param('page', 1);
|
||||
$page = Minz_Request::paramInt('page') ?: 1;
|
||||
$this->view->logsPaginator = new Minz_Paginator($logs);
|
||||
$this->view->logsPaginator->_nbItemsPerPage(50);
|
||||
$this->view->logsPaginator->_currentPage($page);
|
||||
|
|
|
@ -1,42 +1,67 @@
|
|||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
class FreshRSS_javascript_Controller extends Minz_ActionController {
|
||||
public function firstAction() {
|
||||
$this->view->_layout(false);
|
||||
class FreshRSS_javascript_Controller extends FreshRSS_ActionController {
|
||||
|
||||
/**
|
||||
* @var FreshRSS_ViewJavascript
|
||||
*/
|
||||
protected $view;
|
||||
|
||||
public function __construct() {
|
||||
parent::__construct(FreshRSS_ViewJavascript::class);
|
||||
}
|
||||
|
||||
public function actualizeAction() {
|
||||
#[\Override]
|
||||
public function firstAction(): void {
|
||||
$this->view->_layout(null);
|
||||
}
|
||||
|
||||
public function actualizeAction(): void {
|
||||
header('Content-Type: application/json; charset=UTF-8');
|
||||
Minz_Session::_param('actualize_feeds', false);
|
||||
|
||||
$databaseDAO = FreshRSS_Factory::createDatabaseDAO();
|
||||
$databaseDAO->minorDbMaintenance();
|
||||
Minz_ExtensionManager::callHookVoid('freshrss_user_maintenance');
|
||||
|
||||
$catDAO = FreshRSS_Factory::createCategoryDao();
|
||||
$this->view->categories = $catDAO->listCategoriesOrderUpdate(FreshRSS_Context::userConf()->dynamic_opml_ttl_default);
|
||||
|
||||
$feedDAO = FreshRSS_Factory::createFeedDao();
|
||||
$this->view->feeds = $feedDAO->listFeedsOrderUpdate(FreshRSS_Context::$user_conf->ttl_default);
|
||||
$this->view->feeds = $feedDAO->listFeedsOrderUpdate(FreshRSS_Context::userConf()->ttl_default);
|
||||
}
|
||||
|
||||
public function nbUnreadsPerFeedAction() {
|
||||
public function nbUnreadsPerFeedAction(): void {
|
||||
header('Content-Type: application/json; charset=UTF-8');
|
||||
$catDAO = FreshRSS_Factory::createCategoryDao();
|
||||
$this->view->categories = $catDAO->listCategories(true, false);
|
||||
$this->view->categories = $catDAO->listCategories(true, false) ?: [];
|
||||
$tagDAO = FreshRSS_Factory::createTagDao();
|
||||
$this->view->tags = $tagDAO->listTags(true);
|
||||
$this->view->tags = $tagDAO->listTags(true) ?: [];
|
||||
}
|
||||
|
||||
//For Web-form login
|
||||
public function nonceAction() {
|
||||
|
||||
/**
|
||||
* @throws Exception
|
||||
*/
|
||||
public function nonceAction(): void {
|
||||
header('Content-Type: application/json; charset=UTF-8');
|
||||
header('Last-Modified: ' . gmdate('D, d M Y H:i:s \G\M\T'));
|
||||
header('Expires: 0');
|
||||
header('Cache-Control: private, no-cache, no-store, must-revalidate');
|
||||
header('Pragma: no-cache');
|
||||
|
||||
$user = isset($_GET['user']) ? $_GET['user'] : '';
|
||||
if (FreshRSS_Context::initUser($user)) {
|
||||
$user = $_GET['user'] ?? '';
|
||||
FreshRSS_Context::initUser($user);
|
||||
if (FreshRSS_Context::hasUserConf()) {
|
||||
try {
|
||||
$salt = FreshRSS_Context::$system_conf->salt;
|
||||
$s = FreshRSS_Context::$user_conf->passwordHash;
|
||||
$salt = FreshRSS_Context::systemConf()->salt;
|
||||
$s = FreshRSS_Context::userConf()->passwordHash;
|
||||
if (strlen($s) >= 60) {
|
||||
//CRYPT_BLOWFISH Salt: "$2a$", a two digit cost parameter, "$", and 22 characters from the alphabet "./0-9A-Za-z".
|
||||
$this->view->salt1 = substr($s, 0, 29);
|
||||
$this->view->nonce = sha1($salt . uniqid(mt_rand(), true));
|
||||
$this->view->nonce = sha1($salt . uniqid('' . mt_rand(), true));
|
||||
Minz_Session::_param('nonce', $this->view->nonce);
|
||||
return; //Success
|
||||
}
|
||||
|
@ -44,14 +69,14 @@ class FreshRSS_javascript_Controller extends Minz_ActionController {
|
|||
Minz_Log::warning('Nonce failure: ' . $me->getMessage());
|
||||
}
|
||||
} else {
|
||||
Minz_Log::notice('Nonce failure due to invalid username!');
|
||||
Minz_Log::notice('Nonce failure due to invalid username! ' . $user);
|
||||
}
|
||||
//Failure: Return random data.
|
||||
$this->view->salt1 = sprintf('$2a$%02d$', FreshRSS_password_Util::BCRYPT_COST);
|
||||
$alphabet = './ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
|
||||
for ($i = 22; $i > 0; $i--) {
|
||||
$this->view->salt1 .= $alphabet[mt_rand(0, 63)];
|
||||
$this->view->salt1 .= $alphabet[random_int(0, 63)];
|
||||
}
|
||||
$this->view->nonce = sha1(mt_rand());
|
||||
$this->view->nonce = sha1('' . mt_rand());
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,49 +1,44 @@
|
|||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* Controller to handle application statistics.
|
||||
*/
|
||||
class FreshRSS_stats_Controller extends Minz_ActionController {
|
||||
class FreshRSS_stats_Controller extends FreshRSS_ActionController {
|
||||
|
||||
/**
|
||||
* @var FreshRSS_ViewStats
|
||||
*/
|
||||
protected $view;
|
||||
|
||||
public function __construct() {
|
||||
parent::__construct(FreshRSS_ViewStats::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* This action is called before every other action in that class. It is
|
||||
* the common boiler plate for every action. It is triggered by the
|
||||
* the common boilerplate for every action. It is triggered by the
|
||||
* underlying framework.
|
||||
*/
|
||||
public function firstAction() {
|
||||
#[\Override]
|
||||
public function firstAction(): void {
|
||||
if (!FreshRSS_Auth::hasAccess()) {
|
||||
Minz_Error::error(403);
|
||||
}
|
||||
|
||||
$this->_csp([
|
||||
'default-src' => "'self'",
|
||||
'img-src' => '* data:',
|
||||
'style-src' => "'self' 'unsafe-inline'",
|
||||
]);
|
||||
|
||||
$catDAO = FreshRSS_Factory::createCategoryDao();
|
||||
$catDAO->checkDefault();
|
||||
$this->view->categories = $catDAO->listSortedCategories(false) ?: [];
|
||||
|
||||
FreshRSS_View::prependTitle(_t('admin.stats.title') . ' · ');
|
||||
}
|
||||
|
||||
private function convertToSerie($data) {
|
||||
$serie = array();
|
||||
|
||||
foreach ($data as $key => $value) {
|
||||
$serie[] = array($key, $value);
|
||||
}
|
||||
|
||||
return $serie;
|
||||
}
|
||||
|
||||
private function convertToPieSerie($data) {
|
||||
$serie = array();
|
||||
|
||||
foreach ($data as $value) {
|
||||
$value['data'] = array(array(0, (int) $value['data']));
|
||||
$serie[] = $value;
|
||||
}
|
||||
|
||||
return $serie;
|
||||
}
|
||||
|
||||
/**
|
||||
* This action handles the statistic main page.
|
||||
*
|
||||
|
@ -55,29 +50,34 @@ class FreshRSS_stats_Controller extends Minz_ActionController {
|
|||
* - number of article by category (entryByCategory)
|
||||
* - list of most prolific feed (topFeed)
|
||||
*/
|
||||
public function indexAction() {
|
||||
public function indexAction(): void {
|
||||
$statsDAO = FreshRSS_Factory::createStatsDAO();
|
||||
FreshRSS_View::appendScript(Minz_Url::display('/scripts/vendor/chart.min.js?' . @filemtime(PUBLIC_PATH . '/scripts/vendor/chart.min.js')));
|
||||
|
||||
$this->view->repartition = $statsDAO->calculateEntryRepartition();
|
||||
$this->view->repartitions = $statsDAO->calculateEntryRepartition();
|
||||
|
||||
$entryCount = $statsDAO->calculateEntryCount();
|
||||
$this->view->entryCount = $entryCount;
|
||||
$this->view->average = round(array_sum(array_values($entryCount)) / count($entryCount), 2);
|
||||
if (count($entryCount) > 0) {
|
||||
$this->view->entryCount = $entryCount;
|
||||
$this->view->average = round(array_sum(array_values($entryCount)) / count($entryCount), 2);
|
||||
} else {
|
||||
$this->view->entryCount = [];
|
||||
$this->view->average = -1.0;
|
||||
}
|
||||
|
||||
$feedByCategory_calculated = $statsDAO->calculateFeedByCategory();
|
||||
$feedByCategory = [];
|
||||
$feedByCategory_calculated = $statsDAO->calculateFeedByCategory();
|
||||
for ($i = 0; $i < count($feedByCategory_calculated); $i++) {
|
||||
$feedByCategory['label'][$i] = $feedByCategory_calculated[$i]['label'];
|
||||
$feedByCategory['data'][$i] = $feedByCategory_calculated[$i]['data'];
|
||||
$feedByCategory['label'][$i] = $feedByCategory_calculated[$i]['label'];
|
||||
$feedByCategory['data'][$i] = $feedByCategory_calculated[$i]['data'];
|
||||
}
|
||||
$this->view->feedByCategory = $feedByCategory;
|
||||
|
||||
$entryByCategory_calculated = $statsDAO->calculateEntryByCategory();
|
||||
$entryByCategory = [];
|
||||
$entryByCategory_calculated = $statsDAO->calculateEntryByCategory();
|
||||
for ($i = 0; $i < count($entryByCategory_calculated); $i++) {
|
||||
$entryByCategory['label'][$i] = $entryByCategory_calculated[$i]['label'];
|
||||
$entryByCategory['data'][$i] = $entryByCategory_calculated[$i]['data'];
|
||||
$entryByCategory['label'][$i] = $entryByCategory_calculated[$i]['label'];
|
||||
$entryByCategory['data'][$i] = $entryByCategory_calculated[$i]['data'];
|
||||
}
|
||||
$this->view->entryByCategory = $entryByCategory;
|
||||
|
||||
|
@ -85,12 +85,29 @@ class FreshRSS_stats_Controller extends Minz_ActionController {
|
|||
|
||||
$last30DaysLabels = [];
|
||||
for ($i = 0; $i < 30; $i++) {
|
||||
$last30DaysLabels[$i] = date('d.m.Y', strtotime((-30 + $i) . ' days'));
|
||||
$last30DaysLabels[$i] = date('d.m.Y', strtotime((-30 + $i) . ' days') ?: null);
|
||||
}
|
||||
|
||||
$this->view->last30DaysLabels = $last30DaysLabels;
|
||||
}
|
||||
|
||||
/**
|
||||
* This action handles the feed action on the idle statistic page.
|
||||
* set the 'from' parameter to remember that it had a redirection coming from stats controller,
|
||||
* to use the subscription controller to save it,
|
||||
* but shows the stats idle page
|
||||
*/
|
||||
public function feedAction(): void {
|
||||
$id = Minz_Request::paramInt('id');
|
||||
$ajax = Minz_Request::paramBoolean('ajax');
|
||||
if ($ajax) {
|
||||
$url_redirect = ['c' => 'subscription', 'a' => 'feed', 'params' => ['id' => (string)$id, 'from' => 'stats', 'ajax' => (string)$ajax]];
|
||||
} else {
|
||||
$url_redirect = ['c' => 'subscription', 'a' => 'feed', 'params' => ['id' => (string)$id, 'from' => 'stats']];
|
||||
}
|
||||
Minz_Request::forward($url_redirect, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* This action handles the idle feed statistic page.
|
||||
*
|
||||
|
@ -105,19 +122,21 @@ class FreshRSS_stats_Controller extends Minz_ActionController {
|
|||
* - last month
|
||||
* - last week
|
||||
*/
|
||||
public function idleAction() {
|
||||
public function idleAction(): void {
|
||||
FreshRSS_View::appendScript(Minz_Url::display('/scripts/feed.js?' . @filemtime(PUBLIC_PATH . '/scripts/feed.js')));
|
||||
$feed_dao = FreshRSS_Factory::createFeedDao();
|
||||
$statsDAO = FreshRSS_Factory::createStatsDAO();
|
||||
$feeds = $statsDAO->calculateFeedLastDate();
|
||||
$idleFeeds = array(
|
||||
'last_5_year' => array(),
|
||||
'last_3_year' => array(),
|
||||
'last_2_year' => array(),
|
||||
'last_year' => array(),
|
||||
'last_6_month' => array(),
|
||||
'last_3_month' => array(),
|
||||
'last_month' => array(),
|
||||
'last_week' => array(),
|
||||
);
|
||||
$feeds = $statsDAO->calculateFeedLastDate() ?: [];
|
||||
$idleFeeds = [
|
||||
'last_5_year' => [],
|
||||
'last_3_year' => [],
|
||||
'last_2_year' => [],
|
||||
'last_year' => [],
|
||||
'last_6_month' => [],
|
||||
'last_3_month' => [],
|
||||
'last_month' => [],
|
||||
'last_week' => [],
|
||||
];
|
||||
$now = new \DateTime();
|
||||
$feedDate = clone $now;
|
||||
$lastWeek = clone $now;
|
||||
|
@ -138,6 +157,12 @@ class FreshRSS_stats_Controller extends Minz_ActionController {
|
|||
$last5Year->modify('-5 year');
|
||||
|
||||
foreach ($feeds as $feed) {
|
||||
$feedDAO = FreshRSS_Factory::createFeedDao();
|
||||
$feedObject = $feedDAO->searchById($feed['id']);
|
||||
if ($feedObject !== null) {
|
||||
$feed['favicon'] = $feedObject->favicon();
|
||||
}
|
||||
|
||||
$feedDate->setTimestamp($feed['last_date']);
|
||||
if ($feedDate >= $lastWeek) {
|
||||
continue;
|
||||
|
@ -162,6 +187,15 @@ class FreshRSS_stats_Controller extends Minz_ActionController {
|
|||
}
|
||||
|
||||
$this->view->idleFeeds = $idleFeeds;
|
||||
$this->view->feeds = $feed_dao->listFeeds();
|
||||
|
||||
$id = Minz_Request::paramInt('id');
|
||||
$this->view->displaySlider = false;
|
||||
if ($id !== 0) {
|
||||
$this->view->displaySlider = true;
|
||||
$feedDAO = FreshRSS_Factory::createFeedDao();
|
||||
$this->view->feed = $feedDAO->searchById($id) ?? FreshRSS_Feed::default();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -176,17 +210,20 @@ class FreshRSS_stats_Controller extends Minz_ActionController {
|
|||
* @todo verify that the metrics used here make some sense. Especially
|
||||
* for the average.
|
||||
*/
|
||||
public function repartitionAction() {
|
||||
public function repartitionAction(): void {
|
||||
$statsDAO = FreshRSS_Factory::createStatsDAO();
|
||||
$categoryDAO = FreshRSS_Factory::createCategoryDao();
|
||||
$feedDAO = FreshRSS_Factory::createFeedDao();
|
||||
|
||||
FreshRSS_View::appendScript(Minz_Url::display('/scripts/vendor/chart.min.js?' . @filemtime(PUBLIC_PATH . '/scripts/vendor/chart.min.js')));
|
||||
|
||||
$id = Minz_Request::param('id', null);
|
||||
$id = Minz_Request::paramInt('id');
|
||||
if ($id === 0) {
|
||||
$id = null;
|
||||
}
|
||||
|
||||
$this->view->categories = $categoryDAO->listCategories();
|
||||
$this->view->feed = $feedDAO->searchById($id);
|
||||
$this->view->categories = $categoryDAO->listCategories(true) ?: [];
|
||||
$this->view->feed = $id === null ? FreshRSS_Feed::default() : ($feedDAO->searchById($id) ?? FreshRSS_Feed::default());
|
||||
$this->view->days = $statsDAO->getDays();
|
||||
$this->view->months = $statsDAO->getMonths();
|
||||
|
||||
|
|
|
@ -1,26 +1,24 @@
|
|||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* Controller to handle subscription actions.
|
||||
*/
|
||||
class FreshRSS_subscription_Controller extends Minz_ActionController {
|
||||
class FreshRSS_subscription_Controller extends FreshRSS_ActionController {
|
||||
/**
|
||||
* This action is called before every other action in that class. It is
|
||||
* the common boiler plate for every action. It is triggered by the
|
||||
* the common boilerplate for every action. It is triggered by the
|
||||
* underlying framework.
|
||||
*/
|
||||
public function firstAction() {
|
||||
#[\Override]
|
||||
public function firstAction(): void {
|
||||
if (!FreshRSS_Auth::hasAccess()) {
|
||||
Minz_Error::error(403);
|
||||
}
|
||||
|
||||
$catDAO = FreshRSS_Factory::createCategoryDao();
|
||||
$feedDAO = FreshRSS_Factory::createFeedDao();
|
||||
|
||||
$catDAO->checkDefault();
|
||||
$feedDAO->updateTTL();
|
||||
$this->view->categories = $catDAO->listSortedCategories(false);
|
||||
$this->view->default_category = $catDAO->getDefault();
|
||||
$this->view->categories = $catDAO->listSortedCategories(false, true) ?: [];
|
||||
|
||||
$signalError = false;
|
||||
foreach ($this->view->categories as $cat) {
|
||||
|
@ -43,16 +41,17 @@ class FreshRSS_subscription_Controller extends Minz_ActionController {
|
|||
*
|
||||
* It displays categories and associated feeds.
|
||||
*/
|
||||
public function indexAction() {
|
||||
public function indexAction(): void {
|
||||
FreshRSS_View::appendScript(Minz_Url::display('/scripts/category.js?' . @filemtime(PUBLIC_PATH . '/scripts/category.js')));
|
||||
FreshRSS_View::appendScript(Minz_Url::display('/scripts/feed.js?' . @filemtime(PUBLIC_PATH . '/scripts/feed.js')));
|
||||
FreshRSS_View::prependTitle(_t('sub.title') . ' · ');
|
||||
|
||||
$this->view->onlyFeedsWithError = Minz_Request::paramTernary('error');
|
||||
$this->view->onlyFeedsWithError = Minz_Request::paramBoolean('error');
|
||||
|
||||
$id = Minz_Request::param('id');
|
||||
$id = Minz_Request::paramInt('id');
|
||||
$this->view->displaySlider = false;
|
||||
if (false !== $id) {
|
||||
$type = Minz_Request::param('type');
|
||||
if ($id !== 0) {
|
||||
$type = Minz_Request::paramString('type');
|
||||
$this->view->displaySlider = true;
|
||||
switch ($type) {
|
||||
case 'category':
|
||||
|
@ -61,7 +60,7 @@ class FreshRSS_subscription_Controller extends Minz_ActionController {
|
|||
break;
|
||||
default:
|
||||
$feedDAO = FreshRSS_Factory::createFeedDao();
|
||||
$this->view->feed = $feedDAO->searchById($id);
|
||||
$this->view->feed = $feedDAO->searchById($id) ?? FreshRSS_Feed::default();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
@ -72,7 +71,7 @@ class FreshRSS_subscription_Controller extends Minz_ActionController {
|
|||
*
|
||||
* It displays the feed configuration page.
|
||||
* If this action is reached through a POST request, it stores all new
|
||||
* configuraiton values then sends a notification to the user.
|
||||
* configuration values then sends a notification to the user.
|
||||
*
|
||||
* The options available on the page are:
|
||||
* - name
|
||||
|
@ -87,16 +86,18 @@ class FreshRSS_subscription_Controller extends Minz_ActionController {
|
|||
* - refresh frequency (default: 0)
|
||||
* Default values are empty strings unless specified.
|
||||
*/
|
||||
public function feedAction() {
|
||||
if (Minz_Request::param('ajax')) {
|
||||
$this->view->_layout(false);
|
||||
public function feedAction(): void {
|
||||
if (Minz_Request::paramBoolean('ajax')) {
|
||||
$this->view->_layout(null);
|
||||
} else {
|
||||
FreshRSS_View::appendScript(Minz_Url::display('/scripts/feed.js?' . @filemtime(PUBLIC_PATH . '/scripts/feed.js')));
|
||||
}
|
||||
|
||||
$feedDAO = FreshRSS_Factory::createFeedDao();
|
||||
$this->view->feeds = $feedDAO->listFeeds();
|
||||
|
||||
$id = Minz_Request::param('id');
|
||||
if ($id === false || !isset($this->view->feeds[$id])) {
|
||||
$id = Minz_Request::paramInt('id');
|
||||
if ($id === 0 || !isset($this->view->feeds[$id])) {
|
||||
Minz_Error::error(404);
|
||||
return;
|
||||
}
|
||||
|
@ -104,176 +105,228 @@ class FreshRSS_subscription_Controller extends Minz_ActionController {
|
|||
$feed = $this->view->feeds[$id];
|
||||
$this->view->feed = $feed;
|
||||
|
||||
FreshRSS_View::prependTitle(_t('sub.title.feed_management') . ' · ' . $feed->name() . ' · ');
|
||||
FreshRSS_View::prependTitle($feed->name() . ' · ' . _t('sub.title.feed_management') . ' · ');
|
||||
|
||||
if (Minz_Request::isPost()) {
|
||||
$user = trim(Minz_Request::param('http_user_feed' . $id, ''));
|
||||
$pass = trim(Minz_Request::param('http_pass_feed' . $id, ''));
|
||||
$user = Minz_Request::paramString('http_user_feed' . $id);
|
||||
$pass = Minz_Request::paramString('http_pass_feed' . $id);
|
||||
|
||||
$httpAuth = '';
|
||||
if ($user !== '' && $pass !== '') { //TODO: Sanitize
|
||||
$httpAuth = $user . ':' . $pass;
|
||||
}
|
||||
|
||||
$cat = intval(Minz_Request::param('category', 0));
|
||||
$feed->_ttl(Minz_Request::paramInt('ttl') ?: FreshRSS_Feed::TTL_DEFAULT);
|
||||
$feed->_mute(Minz_Request::paramBoolean('mute'));
|
||||
|
||||
$mute = Minz_Request::param('mute', false);
|
||||
$ttl = intval(Minz_Request::param('ttl', FreshRSS_Feed::TTL_DEFAULT));
|
||||
if ($mute && FreshRSS_Feed::TTL_DEFAULT === $ttl) {
|
||||
$ttl = FreshRSS_Context::$user_conf->ttl_default;
|
||||
}
|
||||
$feed->_attribute('read_upon_gone', Minz_Request::paramTernary('read_upon_gone'));
|
||||
$feed->_attribute('mark_updated_article_unread', Minz_Request::paramTernary('mark_updated_article_unread'));
|
||||
$feed->_attribute('read_upon_reception', Minz_Request::paramTernary('read_upon_reception'));
|
||||
$feed->_attribute('clear_cache', Minz_Request::paramTernary('clear_cache'));
|
||||
|
||||
$feed->_attributes('mark_updated_article_unread', Minz_Request::paramTernary('mark_updated_article_unread'));
|
||||
$feed->_attributes('read_upon_reception', Minz_Request::paramTernary('read_upon_reception'));
|
||||
$feed->_attributes('clear_cache', Minz_Request::paramTernary('clear_cache'));
|
||||
$keep_max_n_unread = Minz_Request::paramTernary('keep_max_n_unread') === true ? Minz_Request::paramInt('keep_max_n_unread') : null;
|
||||
$feed->_attribute('keep_max_n_unread', $keep_max_n_unread >= 0 ? $keep_max_n_unread : null);
|
||||
|
||||
$keep_max_n_unread = intval(Minz_Request::param('keep_max_n_unread', 0));
|
||||
$feed->_attributes('keep_max_n_unread', $keep_max_n_unread > 0 ? $keep_max_n_unread : null);
|
||||
|
||||
$read_when_same_title_in_feed = Minz_Request::param('read_when_same_title_in_feed', '');
|
||||
$read_when_same_title_in_feed = Minz_Request::paramString('read_when_same_title_in_feed');
|
||||
if ($read_when_same_title_in_feed === '') {
|
||||
$read_when_same_title_in_feed = null;
|
||||
} else {
|
||||
$read_when_same_title_in_feed = intval($read_when_same_title_in_feed);
|
||||
$read_when_same_title_in_feed = (int)$read_when_same_title_in_feed;
|
||||
if ($read_when_same_title_in_feed <= 0) {
|
||||
$read_when_same_title_in_feed = false;
|
||||
}
|
||||
}
|
||||
$feed->_attributes('read_when_same_title_in_feed', $read_when_same_title_in_feed);
|
||||
$feed->_attribute('read_when_same_title_in_feed', $read_when_same_title_in_feed);
|
||||
|
||||
$cookie = Minz_Request::param('curl_params_cookie', '');
|
||||
$useragent = Minz_Request::param('curl_params_useragent', '');
|
||||
$proxy_address = Minz_Request::param('curl_params', '');
|
||||
$proxy_type = Minz_Request::param('proxy_type', '');
|
||||
$cookie = Minz_Request::paramString('curl_params_cookie');
|
||||
$cookie_file = Minz_Request::paramBoolean('curl_params_cookiefile');
|
||||
$max_redirs = Minz_Request::paramInt('curl_params_redirects');
|
||||
$useragent = Minz_Request::paramString('curl_params_useragent');
|
||||
$proxy_address = Minz_Request::paramString('curl_params');
|
||||
$proxy_type = Minz_Request::paramString('proxy_type');
|
||||
$request_method = Minz_Request::paramString('curl_method');
|
||||
$request_fields = Minz_Request::paramString('curl_fields', true);
|
||||
$opts = [];
|
||||
if ($proxy_address !== '' && $proxy_type !== '' && in_array($proxy_type, [0, 2, 4, 5, 6, 7])) {
|
||||
if ($proxy_type !== '') {
|
||||
$opts[CURLOPT_PROXY] = $proxy_address;
|
||||
$opts[CURLOPT_PROXYTYPE] = intval($proxy_type);
|
||||
$opts[CURLOPT_PROXYTYPE] = (int)$proxy_type;
|
||||
}
|
||||
if ($cookie !== '') {
|
||||
$opts[CURLOPT_COOKIE] = $cookie;
|
||||
}
|
||||
if ($cookie_file) {
|
||||
// Pass empty cookie file name to enable the libcurl cookie engine
|
||||
// without reading any existing cookie data.
|
||||
$opts[CURLOPT_COOKIEFILE] = '';
|
||||
}
|
||||
if ($max_redirs != 0) {
|
||||
$opts[CURLOPT_MAXREDIRS] = $max_redirs;
|
||||
$opts[CURLOPT_FOLLOWLOCATION] = 1;
|
||||
}
|
||||
if ($useragent !== '') {
|
||||
$opts[CURLOPT_USERAGENT] = $useragent;
|
||||
}
|
||||
$feed->_attributes('curl_params', empty($opts) ? null : $opts);
|
||||
|
||||
$feed->_attributes('content_action', Minz_Request::param('content_action', 'replace'));
|
||||
if ($request_method === 'POST') {
|
||||
$opts[CURLOPT_POST] = true;
|
||||
if ($request_fields !== '') {
|
||||
$opts[CURLOPT_POSTFIELDS] = $request_fields;
|
||||
if (json_decode($request_fields, true) !== null) {
|
||||
$opts[CURLOPT_HTTPHEADER] = ['Content-Type: application/json'];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$feed->_attributes('ssl_verify', Minz_Request::paramTernary('ssl_verify'));
|
||||
$timeout = intval(Minz_Request::param('timeout', 0));
|
||||
$feed->_attributes('timeout', $timeout > 0 ? $timeout : null);
|
||||
$feed->_attribute('curl_params', empty($opts) ? null : $opts);
|
||||
|
||||
$feed->_attribute('content_action', Minz_Request::paramString('content_action', true) ?: 'replace');
|
||||
|
||||
$feed->_attribute('ssl_verify', Minz_Request::paramTernary('ssl_verify'));
|
||||
$timeout = Minz_Request::paramInt('timeout');
|
||||
$feed->_attribute('timeout', $timeout > 0 ? $timeout : null);
|
||||
|
||||
if (Minz_Request::paramBoolean('use_default_purge_options')) {
|
||||
$feed->_attributes('archiving', null);
|
||||
$feed->_attribute('archiving', null);
|
||||
} else {
|
||||
if (!Minz_Request::paramBoolean('enable_keep_max')) {
|
||||
if (Minz_Request::paramBoolean('enable_keep_max')) {
|
||||
$keepMax = Minz_Request::paramInt('keep_max') ?: FreshRSS_Feed::ARCHIVING_RETENTION_COUNT_LIMIT;
|
||||
} else {
|
||||
$keepMax = false;
|
||||
} elseif (!$keepMax = Minz_Request::param('keep_max')) {
|
||||
$keepMax = FreshRSS_Feed::ARCHIVING_RETENTION_COUNT_LIMIT;
|
||||
}
|
||||
if ($enableRetentionPeriod = Minz_Request::paramBoolean('enable_keep_period')) {
|
||||
if (Minz_Request::paramBoolean('enable_keep_period')) {
|
||||
$keepPeriod = FreshRSS_Feed::ARCHIVING_RETENTION_PERIOD;
|
||||
if (is_numeric(Minz_Request::param('keep_period_count')) && preg_match('/^PT?1[YMWDH]$/', Minz_Request::param('keep_period_unit'))) {
|
||||
$keepPeriod = str_replace(1, Minz_Request::param('keep_period_count'), Minz_Request::param('keep_period_unit'));
|
||||
if (is_numeric(Minz_Request::paramString('keep_period_count')) && preg_match('/^PT?1[YMWDH]$/', Minz_Request::paramString('keep_period_unit'))) {
|
||||
$keepPeriod = str_replace('1', Minz_Request::paramString('keep_period_count'), Minz_Request::paramString('keep_period_unit'));
|
||||
}
|
||||
} else {
|
||||
$keepPeriod = false;
|
||||
}
|
||||
$feed->_attributes('archiving', [
|
||||
$feed->_attribute('archiving', [
|
||||
'keep_period' => $keepPeriod,
|
||||
'keep_max' => $keepMax,
|
||||
'keep_min' => intval(Minz_Request::param('keep_min', 0)),
|
||||
'keep_min' => Minz_Request::paramInt('keep_min'),
|
||||
'keep_favourites' => Minz_Request::paramBoolean('keep_favourites'),
|
||||
'keep_labels' => Minz_Request::paramBoolean('keep_labels'),
|
||||
'keep_unreads' => Minz_Request::paramBoolean('keep_unreads'),
|
||||
]);
|
||||
}
|
||||
|
||||
$feed->_filtersAction('read', preg_split('/[\n\r]+/', Minz_Request::param('filteractions_read', '')));
|
||||
$feed->_filtersAction('read', Minz_Request::paramTextToArray('filteractions_read'));
|
||||
|
||||
$values = array(
|
||||
'name' => Minz_Request::param('name', ''),
|
||||
'description' => sanitizeHTML(Minz_Request::param('description', '', true)),
|
||||
'website' => checkUrl(Minz_Request::param('website', '')),
|
||||
'url' => checkUrl(Minz_Request::param('url', '')),
|
||||
'category' => $cat,
|
||||
'pathEntries' => Minz_Request::param('path_entries', ''),
|
||||
'priority' => intval(Minz_Request::param('priority', FreshRSS_Feed::PRIORITY_MAIN_STREAM)),
|
||||
'httpAuth' => $httpAuth,
|
||||
'ttl' => $ttl * ($mute ? -1 : 1),
|
||||
'attributes' => $feed->attributes(),
|
||||
);
|
||||
|
||||
invalidateHttpCache();
|
||||
|
||||
$url_redirect = array('c' => 'subscription', 'params' => array('id' => $id));
|
||||
if ($feedDAO->updateFeed($id, $values) !== false) {
|
||||
$feed->_category($cat);
|
||||
$feed->faviconPrepare();
|
||||
|
||||
Minz_Request::good(_t('feedback.sub.feed.updated'), $url_redirect);
|
||||
} else {
|
||||
Minz_Request::bad(_t('feedback.sub.feed.error'), $url_redirect);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function categoryAction() {
|
||||
$this->view->_layout(false);
|
||||
|
||||
$categoryDAO = FreshRSS_Factory::createCategoryDao();
|
||||
|
||||
$id = Minz_Request::param('id');
|
||||
$category = $categoryDAO->searchById($id);
|
||||
if ($id === false || null === $category) {
|
||||
Minz_Error::error(404);
|
||||
return;
|
||||
}
|
||||
$this->view->category = $category;
|
||||
|
||||
if (Minz_Request::isPost()) {
|
||||
if (Minz_Request::paramBoolean('use_default_purge_options')) {
|
||||
$category->_attributes('archiving', null);
|
||||
} else {
|
||||
if (!Minz_Request::paramBoolean('enable_keep_max')) {
|
||||
$keepMax = false;
|
||||
} elseif (!$keepMax = Minz_Request::param('keep_max')) {
|
||||
$keepMax = FreshRSS_Feed::ARCHIVING_RETENTION_COUNT_LIMIT;
|
||||
$feed->_kind(Minz_Request::paramInt('feed_kind') ?: FreshRSS_Feed::KIND_RSS);
|
||||
if ($feed->kind() === FreshRSS_Feed::KIND_HTML_XPATH || $feed->kind() === FreshRSS_Feed::KIND_XML_XPATH) {
|
||||
$xPathSettings = [];
|
||||
if (Minz_Request::paramString('xPathItem') != '')
|
||||
$xPathSettings['item'] = Minz_Request::paramString('xPathItem', true);
|
||||
if (Minz_Request::paramString('xPathItemTitle') != '')
|
||||
$xPathSettings['itemTitle'] = Minz_Request::paramString('xPathItemTitle', true);
|
||||
if (Minz_Request::paramString('xPathItemContent') != '')
|
||||
$xPathSettings['itemContent'] = Minz_Request::paramString('xPathItemContent', true);
|
||||
if (Minz_Request::paramString('xPathItemUri') != '')
|
||||
$xPathSettings['itemUri'] = Minz_Request::paramString('xPathItemUri', true);
|
||||
if (Minz_Request::paramString('xPathItemAuthor') != '')
|
||||
$xPathSettings['itemAuthor'] = Minz_Request::paramString('xPathItemAuthor', true);
|
||||
if (Minz_Request::paramString('xPathItemTimestamp') != '')
|
||||
$xPathSettings['itemTimestamp'] = Minz_Request::paramString('xPathItemTimestamp', true);
|
||||
if (Minz_Request::paramString('xPathItemTimeFormat') != '')
|
||||
$xPathSettings['itemTimeFormat'] = Minz_Request::paramString('xPathItemTimeFormat', true);
|
||||
if (Minz_Request::paramString('xPathItemThumbnail') != '')
|
||||
$xPathSettings['itemThumbnail'] = Minz_Request::paramString('xPathItemThumbnail', true);
|
||||
if (Minz_Request::paramString('xPathItemCategories') != '')
|
||||
$xPathSettings['itemCategories'] = Minz_Request::paramString('xPathItemCategories', true);
|
||||
if (Minz_Request::paramString('xPathItemUid') != '')
|
||||
$xPathSettings['itemUid'] = Minz_Request::paramString('xPathItemUid', true);
|
||||
if (!empty($xPathSettings))
|
||||
$feed->_attribute('xpath', $xPathSettings);
|
||||
} elseif ($feed->kind() === FreshRSS_Feed::KIND_JSON_DOTNOTATION) {
|
||||
$jsonSettings = [];
|
||||
if (Minz_Request::paramString('jsonFeedTitle') !== '') {
|
||||
$jsonSettings['feedTitle'] = Minz_Request::paramString('jsonFeedTitle', true);
|
||||
}
|
||||
if ($enableRetentionPeriod = Minz_Request::paramBoolean('enable_keep_period')) {
|
||||
$keepPeriod = FreshRSS_Feed::ARCHIVING_RETENTION_PERIOD;
|
||||
if (is_numeric(Minz_Request::param('keep_period_count')) && preg_match('/^PT?1[YMWDH]$/', Minz_Request::param('keep_period_unit'))) {
|
||||
$keepPeriod = str_replace(1, Minz_Request::param('keep_period_count'), Minz_Request::param('keep_period_unit'));
|
||||
}
|
||||
} else {
|
||||
$keepPeriod = false;
|
||||
if (Minz_Request::paramString('jsonItem') !== '') {
|
||||
$jsonSettings['item'] = Minz_Request::paramString('jsonItem', true);
|
||||
}
|
||||
if (Minz_Request::paramString('jsonItemTitle') !== '') {
|
||||
$jsonSettings['itemTitle'] = Minz_Request::paramString('jsonItemTitle', true);
|
||||
}
|
||||
if (Minz_Request::paramString('jsonItemContent') !== '') {
|
||||
$jsonSettings['itemContent'] = Minz_Request::paramString('jsonItemContent', true);
|
||||
}
|
||||
if (Minz_Request::paramString('jsonItemUri') !== '') {
|
||||
$jsonSettings['itemUri'] = Minz_Request::paramString('jsonItemUri', true);
|
||||
}
|
||||
if (Minz_Request::paramString('jsonItemAuthor') !== '') {
|
||||
$jsonSettings['itemAuthor'] = Minz_Request::paramString('jsonItemAuthor', true);
|
||||
}
|
||||
if (Minz_Request::paramString('jsonItemTimestamp') !== '') {
|
||||
$jsonSettings['itemTimestamp'] = Minz_Request::paramString('jsonItemTimestamp', true);
|
||||
}
|
||||
if (Minz_Request::paramString('jsonItemTimeFormat') !== '') {
|
||||
$jsonSettings['itemTimeFormat'] = Minz_Request::paramString('jsonItemTimeFormat', true);
|
||||
}
|
||||
if (Minz_Request::paramString('jsonItemThumbnail') !== '') {
|
||||
$jsonSettings['itemThumbnail'] = Minz_Request::paramString('jsonItemThumbnail', true);
|
||||
}
|
||||
if (Minz_Request::paramString('jsonItemCategories') !== '') {
|
||||
$jsonSettings['itemCategories'] = Minz_Request::paramString('jsonItemCategories', true);
|
||||
}
|
||||
if (Minz_Request::paramString('jsonItemUid') !== '') {
|
||||
$jsonSettings['itemUid'] = Minz_Request::paramString('jsonItemUid', true);
|
||||
}
|
||||
if (!empty($jsonSettings)) {
|
||||
$feed->_attribute('json_dotnotation', $jsonSettings);
|
||||
}
|
||||
$category->_attributes('archiving', [
|
||||
'keep_period' => $keepPeriod,
|
||||
'keep_max' => $keepMax,
|
||||
'keep_min' => intval(Minz_Request::param('keep_min', 0)),
|
||||
'keep_favourites' => Minz_Request::paramBoolean('keep_favourites'),
|
||||
'keep_labels' => Minz_Request::paramBoolean('keep_labels'),
|
||||
'keep_unreads' => Minz_Request::paramBoolean('keep_unreads'),
|
||||
]);
|
||||
}
|
||||
|
||||
$position = Minz_Request::param('position');
|
||||
$category->_attributes('position', '' === $position ? null : (int) $position);
|
||||
$feed->_attribute('path_entries_filter', Minz_Request::paramString('path_entries_filter', true));
|
||||
|
||||
$values = [
|
||||
'name' => Minz_Request::param('name', ''),
|
||||
'attributes' => $category->attributes(),
|
||||
'name' => Minz_Request::paramString('name'),
|
||||
'kind' => $feed->kind(),
|
||||
'description' => sanitizeHTML(Minz_Request::paramString('description', true)),
|
||||
'website' => checkUrl(Minz_Request::paramString('website')) ?: '',
|
||||
'url' => checkUrl(Minz_Request::paramString('url')) ?: '',
|
||||
'category' => Minz_Request::paramInt('category'),
|
||||
'pathEntries' => Minz_Request::paramString('path_entries'),
|
||||
'priority' => Minz_Request::paramTernary('priority') === null ? FreshRSS_Feed::PRIORITY_MAIN_STREAM : Minz_Request::paramInt('priority'),
|
||||
'httpAuth' => $httpAuth,
|
||||
'ttl' => $feed->ttl(true),
|
||||
'attributes' => $feed->attributes(),
|
||||
];
|
||||
|
||||
invalidateHttpCache();
|
||||
|
||||
$url_redirect = array('c' => 'subscription', 'params' => array('id' => $id, 'type' => 'category'));
|
||||
if (false !== $categoryDAO->updateCategory($id, $values)) {
|
||||
Minz_Request::good(_t('feedback.sub.category.updated'), $url_redirect);
|
||||
$from = Minz_Request::paramString('from');
|
||||
switch ($from) {
|
||||
case 'stats':
|
||||
$url_redirect = ['c' => 'stats', 'a' => 'idle', 'params' => ['id' => $id, 'from' => 'stats']];
|
||||
break;
|
||||
case 'normal':
|
||||
case 'reader':
|
||||
$get = Minz_Request::paramString('get');
|
||||
if ($get) {
|
||||
$url_redirect = ['c' => 'index', 'a' => $from, 'params' => ['get' => $get]];
|
||||
} else {
|
||||
$url_redirect = ['c' => 'index', 'a' => $from];
|
||||
}
|
||||
break;
|
||||
default:
|
||||
$url_redirect = ['c' => 'subscription', 'params' => ['id' => $id]];
|
||||
}
|
||||
|
||||
if ($values['url'] != '' && $feedDAO->updateFeed($id, $values) !== false) {
|
||||
$feed->_categoryId($values['category']);
|
||||
// update url and website values for faviconPrepare
|
||||
$feed->_url($values['url'], false);
|
||||
$feed->_website($values['website'], false);
|
||||
$feed->faviconPrepare();
|
||||
|
||||
Minz_Request::good(_t('feedback.sub.feed.updated'), $url_redirect);
|
||||
} else {
|
||||
Minz_Request::bad(_t('feedback.sub.category.error'), $url_redirect);
|
||||
if ($values['url'] == '') {
|
||||
Minz_Log::warning('Invalid feed URL!');
|
||||
}
|
||||
Minz_Request::bad(_t('feedback.sub.feed.error'), $url_redirect);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -281,14 +334,15 @@ class FreshRSS_subscription_Controller extends Minz_ActionController {
|
|||
/**
|
||||
* This action displays the bookmarklet page.
|
||||
*/
|
||||
public function bookmarkletAction() {
|
||||
public function bookmarkletAction(): void {
|
||||
FreshRSS_View::prependTitle(_t('sub.title.subscription_tools') . ' . ');
|
||||
}
|
||||
|
||||
/**
|
||||
* This action displays the page to add a new feed
|
||||
*/
|
||||
public function addAction() {
|
||||
public function addAction(): void {
|
||||
FreshRSS_View::appendScript(Minz_Url::display('/scripts/feed.js?' . @filemtime(PUBLIC_PATH . '/scripts/feed.js')));
|
||||
FreshRSS_View::prependTitle(_t('sub.title.add') . ' . ');
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,54 +1,54 @@
|
|||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* Controller to handle every tag actions.
|
||||
*/
|
||||
class FreshRSS_tag_Controller extends Minz_ActionController {
|
||||
class FreshRSS_tag_Controller extends FreshRSS_ActionController {
|
||||
|
||||
/**
|
||||
* JavaScript request or not.
|
||||
* @var bool
|
||||
*/
|
||||
private $ajax = false;
|
||||
private bool $ajax = false;
|
||||
|
||||
/**
|
||||
* This action is called before every other action in that class. It is
|
||||
* the common boiler plate for every action. It is triggered by the
|
||||
* the common boilerplate for every action. It is triggered by the
|
||||
* underlying framework.
|
||||
*/
|
||||
public function firstAction() {
|
||||
if (!FreshRSS_Auth::hasAccess()) {
|
||||
Minz_Error::error(403);
|
||||
}
|
||||
#[\Override]
|
||||
public function firstAction(): void {
|
||||
// If ajax request, we do not print layout
|
||||
$this->ajax = Minz_Request::param('ajax');
|
||||
$this->ajax = Minz_Request::paramBoolean('ajax');
|
||||
if ($this->ajax) {
|
||||
$this->view->_layout(false);
|
||||
Minz_Request::_param('ajax');
|
||||
$this->view->_layout(null);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This action adds (checked=true) or removes (checked=false) a tag to an entry.
|
||||
*/
|
||||
public function tagEntryAction() {
|
||||
public function tagEntryAction(): void {
|
||||
if (!FreshRSS_Auth::hasAccess()) {
|
||||
Minz_Error::error(403);
|
||||
}
|
||||
if (Minz_Request::isPost()) {
|
||||
$id_tag = Minz_Request::param('id_tag');
|
||||
$name_tag = trim(Minz_Request::param('name_tag'));
|
||||
$id_entry = Minz_Request::param('id_entry');
|
||||
$checked = Minz_Request::paramTernary('checked');
|
||||
if ($id_entry != false) {
|
||||
$id_tag = Minz_Request::paramInt('id_tag');
|
||||
$name_tag = Minz_Request::paramString('name_tag');
|
||||
$id_entry = Minz_Request::paramString('id_entry');
|
||||
$checked = Minz_Request::paramBoolean('checked');
|
||||
if ($id_entry != '') {
|
||||
$tagDAO = FreshRSS_Factory::createTagDao();
|
||||
if ($id_tag == 0 && $name_tag != '' && $checked) {
|
||||
if ($id_tag == 0 && $name_tag !== '' && $checked) {
|
||||
if ($existing_tag = $tagDAO->searchByName($name_tag)) {
|
||||
// Use existing tag
|
||||
$tagDAO->tagEntry($existing_tag->id(), $id_entry, $checked);
|
||||
} else {
|
||||
//Create new tag
|
||||
$id_tag = $tagDAO->addTag(array('name' => $name_tag));
|
||||
$id_tag = $tagDAO->addTag(['name' => $name_tag]);
|
||||
}
|
||||
}
|
||||
if ($id_tag != 0) {
|
||||
if ($id_tag != false) {
|
||||
$tagDAO->tagEntry($id_tag, $id_entry, $checked);
|
||||
}
|
||||
}
|
||||
|
@ -56,17 +56,20 @@ class FreshRSS_tag_Controller extends Minz_ActionController {
|
|||
Minz_Error::error(405);
|
||||
}
|
||||
if (!$this->ajax) {
|
||||
Minz_Request::forward(array(
|
||||
Minz_Request::forward([
|
||||
'c' => 'index',
|
||||
'a' => 'index',
|
||||
), true);
|
||||
], true);
|
||||
}
|
||||
}
|
||||
|
||||
public function deleteAction() {
|
||||
public function deleteAction(): void {
|
||||
if (!FreshRSS_Auth::hasAccess()) {
|
||||
Minz_Error::error(403);
|
||||
}
|
||||
if (Minz_Request::isPost()) {
|
||||
$id_tag = Minz_Request::param('id_tag');
|
||||
if ($id_tag != false) {
|
||||
$id_tag = Minz_Request::paramInt('id_tag');
|
||||
if ($id_tag !== 0) {
|
||||
$tagDAO = FreshRSS_Factory::createTagDao();
|
||||
$tagDAO->deleteTag($id_tag);
|
||||
}
|
||||
|
@ -74,28 +77,79 @@ class FreshRSS_tag_Controller extends Minz_ActionController {
|
|||
Minz_Error::error(405);
|
||||
}
|
||||
if (!$this->ajax) {
|
||||
Minz_Request::forward(array(
|
||||
Minz_Request::forward([
|
||||
'c' => 'tag',
|
||||
'a' => 'index',
|
||||
), true);
|
||||
], true);
|
||||
}
|
||||
}
|
||||
|
||||
public function getTagsForEntryAction() {
|
||||
$this->view->_layout(false);
|
||||
header('Content-Type: application/json; charset=UTF-8');
|
||||
header('Cache-Control: private, no-cache, no-store, must-revalidate');
|
||||
$id_entry = Minz_Request::param('id_entry', 0);
|
||||
|
||||
/**
|
||||
* This action updates the given tag.
|
||||
*/
|
||||
public function updateAction(): void {
|
||||
if (Minz_Request::paramBoolean('ajax')) {
|
||||
$this->view->_layout(null);
|
||||
}
|
||||
|
||||
$tagDAO = FreshRSS_Factory::createTagDao();
|
||||
$this->view->tags = $tagDAO->getTagsForEntry($id_entry);
|
||||
|
||||
$id = Minz_Request::paramInt('id');
|
||||
$tag = $tagDAO->searchById($id);
|
||||
if ($id === 0 || $tag === null) {
|
||||
Minz_Error::error(404);
|
||||
return;
|
||||
}
|
||||
$this->view->tag = $tag;
|
||||
|
||||
FreshRSS_View::prependTitle($tag->name() . ' · ' . _t('sub.title') . ' · ');
|
||||
|
||||
if (Minz_Request::isPost()) {
|
||||
invalidateHttpCache();
|
||||
$ok = true;
|
||||
|
||||
if ($tag->name() !== Minz_Request::paramString('name')) {
|
||||
$ok = $tagDAO->updateTagName($tag->id(), Minz_Request::paramString('name')) !== false;
|
||||
}
|
||||
|
||||
if ($ok) {
|
||||
$tag->_filtersAction('label', Minz_Request::paramTextToArray('filteractions_label'));
|
||||
$ok = $tagDAO->updateTagAttributes($tag->id(), $tag->attributes()) !== false;
|
||||
}
|
||||
|
||||
invalidateHttpCache();
|
||||
|
||||
$url_redirect = ['c' => 'tag', 'a' => 'update', 'params' => ['id' => $id]];
|
||||
if ($ok) {
|
||||
Minz_Request::good(_t('feedback.tag.updated'), $url_redirect);
|
||||
} else {
|
||||
Minz_Request::bad(_t('feedback.tag.error'), $url_redirect);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function addAction() {
|
||||
public function getTagsForEntryAction(): void {
|
||||
if (!FreshRSS_Auth::hasAccess() && !FreshRSS_Context::systemConf()->allow_anonymous) {
|
||||
Minz_Error::error(403);
|
||||
}
|
||||
$this->view->_layout(null);
|
||||
header('Content-Type: application/json; charset=UTF-8');
|
||||
header('Cache-Control: private, no-cache, no-store, must-revalidate');
|
||||
$id_entry = Minz_Request::paramString('id_entry');
|
||||
$tagDAO = FreshRSS_Factory::createTagDao();
|
||||
$this->view->tagsForEntry = $tagDAO->getTagsForEntry($id_entry) ?: [];
|
||||
}
|
||||
|
||||
public function addAction(): void {
|
||||
if (!FreshRSS_Auth::hasAccess()) {
|
||||
Minz_Error::error(403);
|
||||
}
|
||||
if (!Minz_Request::isPost()) {
|
||||
Minz_Error::error(405);
|
||||
}
|
||||
|
||||
$name = Minz_Request::param('name');
|
||||
$name = Minz_Request::paramString('name');
|
||||
$tagDAO = FreshRSS_Factory::createTagDao();
|
||||
if (strlen($name) > 0 && null === $tagDAO->searchByName($name)) {
|
||||
$tagDAO->addTag(['name' => $name]);
|
||||
|
@ -105,25 +159,33 @@ class FreshRSS_tag_Controller extends Minz_ActionController {
|
|||
Minz_Request::bad(_t('feedback.tag.name_exists', $name), ['c' => 'tag', 'a' => 'index']);
|
||||
}
|
||||
|
||||
public function renameAction() {
|
||||
/**
|
||||
* @throws Minz_ConfigurationNamespaceException
|
||||
* @throws Minz_PDOConnectionException
|
||||
*/
|
||||
public function renameAction(): void {
|
||||
if (!FreshRSS_Auth::hasAccess()) {
|
||||
Minz_Error::error(403);
|
||||
}
|
||||
if (!Minz_Request::isPost()) {
|
||||
Minz_Error::error(405);
|
||||
}
|
||||
|
||||
$targetName = Minz_Request::param('name');
|
||||
$sourceId = Minz_Request::param('id_tag');
|
||||
$targetName = Minz_Request::paramString('name');
|
||||
$sourceId = Minz_Request::paramInt('id_tag');
|
||||
|
||||
if ($targetName == '' || $sourceId == '') {
|
||||
return Minz_Error::error(400);
|
||||
if ($targetName == '' || $sourceId == 0) {
|
||||
Minz_Error::error(400);
|
||||
return;
|
||||
}
|
||||
|
||||
$tagDAO = FreshRSS_Factory::createTagDao();
|
||||
$sourceTag = $tagDAO->searchById($sourceId);
|
||||
$sourceName = $sourceTag == null ? null : $sourceTag->name();
|
||||
$sourceName = $sourceTag === null ? '' : $sourceTag->name();
|
||||
$targetTag = $tagDAO->searchByName($targetName);
|
||||
if ($targetTag == null) {
|
||||
if ($targetTag === null) {
|
||||
// There is no existing tag with the same target name
|
||||
$tagDAO->updateTag($sourceId, ['name' => $targetName]);
|
||||
$tagDAO->updateTagName($sourceId, $targetName);
|
||||
} else {
|
||||
// There is an existing tag with the same target name
|
||||
$tagDAO->updateEntryTag($sourceId, $targetTag->id());
|
||||
|
@ -133,8 +195,13 @@ class FreshRSS_tag_Controller extends Minz_ActionController {
|
|||
Minz_Request::good(_t('feedback.tag.renamed', $sourceName, $targetName), ['c' => 'tag', 'a' => 'index']);
|
||||
}
|
||||
|
||||
public function indexAction() {
|
||||
public function indexAction(): void {
|
||||
FreshRSS_View::prependTitle(_t('sub.menu.label_management') . ' · ');
|
||||
|
||||
if (!FreshRSS_Auth::hasAccess()) {
|
||||
Minz_Error::error(403);
|
||||
}
|
||||
$tagDAO = FreshRSS_Factory::createTagDao();
|
||||
$this->view->tags = $tagDAO->listTags();
|
||||
$this->view->tags = $tagDAO->listTags(true) ?: [];
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,27 +1,39 @@
|
|||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
class FreshRSS_update_Controller extends Minz_ActionController {
|
||||
class FreshRSS_update_Controller extends FreshRSS_ActionController {
|
||||
|
||||
public static function isGit() {
|
||||
private const LASTUPDATEFILE = 'last_update.txt';
|
||||
|
||||
public static function isGit(): bool {
|
||||
return is_dir(FRESHRSS_PATH . '/.git/');
|
||||
}
|
||||
|
||||
/**
|
||||
* Automatic change to the new name of edge branch since FreshRSS 1.18.0.
|
||||
* Automatic change to the new name of edge branch since FreshRSS 1.18.0,
|
||||
* and perform checks for several git errors.
|
||||
* @throws Minz_Exception
|
||||
*/
|
||||
public static function migrateToGitEdge() {
|
||||
$errorMessage = 'Error during git checkout to edge branch. Please change branch manually!';
|
||||
public static function migrateToGitEdge(): bool {
|
||||
if (!is_writable(FRESHRSS_PATH . '/.git/config')) {
|
||||
throw new Minz_Exception('Error during git checkout: .git directory does not seem writeable! ' .
|
||||
'Please git pull manually!');
|
||||
}
|
||||
|
||||
if (!is_writable(FRESHRSS_PATH . '/.git/')) {
|
||||
throw new Exception($errorMessage);
|
||||
exec('git --version', $output, $return);
|
||||
if ($return != 0) {
|
||||
throw new Minz_Exception("Error {$return} git not found: Please update manually!");
|
||||
}
|
||||
|
||||
//Note `git branch --show-current` requires git 2.22+
|
||||
exec('git symbolic-ref --short HEAD', $output, $return);
|
||||
exec('git symbolic-ref --short HEAD 2>&1', $output, $return);
|
||||
if ($return != 0) {
|
||||
throw new Exception($errorMessage);
|
||||
throw new Minz_Exception("Error {$return} during git symbolic-ref: " .
|
||||
'Reapply `chown www-data:www-data -R ' . FRESHRSS_PATH . '` ' .
|
||||
'or git pull manually! ' .
|
||||
json_encode($output, JSON_UNESCAPED_SLASHES));
|
||||
}
|
||||
$line = is_array($output) ? implode('', $output) : $output;
|
||||
$line = implode('', $output);
|
||||
if ($line !== 'master' && $line !== 'dev') {
|
||||
return true; // not on master or dev, nothing to do
|
||||
}
|
||||
|
@ -30,42 +42,64 @@ class FreshRSS_update_Controller extends Minz_ActionController {
|
|||
unset($output);
|
||||
exec('git checkout edge --guess -f', $output, $return);
|
||||
if ($return != 0) {
|
||||
throw new Exception($errorMessage);
|
||||
throw new Minz_Exception("Error {$return} during git checkout to edge branch! ' .
|
||||
'Please change branch manually!");
|
||||
}
|
||||
|
||||
unset($output);
|
||||
exec('git reset --hard FETCH_HEAD', $output, $return);
|
||||
if ($return != 0) {
|
||||
throw new Exception($errorMessage);
|
||||
throw new Minz_Exception("Error {$return} during git reset! Please git pull manually!");
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public static function hasGitUpdate() {
|
||||
public static function getCurrentGitBranch(): string {
|
||||
$output = [];
|
||||
exec('git branch --show-current', $output, $return);
|
||||
if ($return === 0) {
|
||||
return 'git branch: ' . $output[0];
|
||||
} else {
|
||||
return 'git';
|
||||
}
|
||||
}
|
||||
|
||||
public static function hasGitUpdate(): bool {
|
||||
$cwd = getcwd();
|
||||
if ($cwd === false) {
|
||||
Minz_Log::warning('getcwd() failed');
|
||||
return false;
|
||||
}
|
||||
chdir(FRESHRSS_PATH);
|
||||
$output = array();
|
||||
$output = [];
|
||||
try {
|
||||
/** @throws ValueError */
|
||||
exec('git fetch --prune', $output, $return);
|
||||
if ($return == 0) {
|
||||
$output = [];
|
||||
exec('git status -sb --porcelain remote', $output, $return);
|
||||
} else {
|
||||
$line = is_array($output) ? implode('; ', $output) : $output;
|
||||
$line = implode('; ', $output);
|
||||
Minz_Log::warning('git fetch warning: ' . $line);
|
||||
}
|
||||
} catch (Exception $e) {
|
||||
} catch (Throwable $e) {
|
||||
Minz_Log::warning('git fetch error: ' . $e->getMessage());
|
||||
}
|
||||
chdir($cwd);
|
||||
$line = is_array($output) ? implode('; ', $output) : $output;
|
||||
$line = implode('; ', $output);
|
||||
return $line == '' ||
|
||||
strpos($line, '[behind') !== false || strpos($line, '[ahead') !== false || strpos($line, '[gone') !== false;
|
||||
}
|
||||
|
||||
/** @return string|true */
|
||||
public static function gitPull() {
|
||||
Minz_Log::notice(_t('admin.update.viaGit'));
|
||||
$cwd = getcwd();
|
||||
if ($cwd === false) {
|
||||
Minz_Log::warning('getcwd() failed');
|
||||
return 'getcwd() failed';
|
||||
}
|
||||
chdir(FRESHRSS_PATH);
|
||||
$output = [];
|
||||
$return = 1;
|
||||
|
@ -78,11 +112,9 @@ class FreshRSS_update_Controller extends Minz_ActionController {
|
|||
|
||||
$output = [];
|
||||
self::migrateToGitEdge();
|
||||
} catch (Exception $e) {
|
||||
} catch (Throwable $e) {
|
||||
Minz_Log::warning('Git error: ' . $e->getMessage());
|
||||
if (empty($output)) {
|
||||
$output = $e->getMessage();
|
||||
}
|
||||
$output = $e->getMessage();
|
||||
$return = 1;
|
||||
}
|
||||
chdir($cwd);
|
||||
|
@ -90,7 +122,8 @@ class FreshRSS_update_Controller extends Minz_ActionController {
|
|||
return $return == 0 ? true : 'Git error: ' . $line;
|
||||
}
|
||||
|
||||
public function firstAction() {
|
||||
#[\Override]
|
||||
public function firstAction(): void {
|
||||
if (!FreshRSS_Auth::hasAccess('admin')) {
|
||||
Minz_Error::error(403);
|
||||
}
|
||||
|
@ -99,142 +132,163 @@ class FreshRSS_update_Controller extends Minz_ActionController {
|
|||
|
||||
invalidateHttpCache();
|
||||
|
||||
$this->view->is_release_channel_stable = $this->is_release_channel_stable(FRESHRSS_VERSION);
|
||||
|
||||
$this->view->update_to_apply = false;
|
||||
$this->view->last_update_time = 'unknown';
|
||||
$timestamp = @filemtime(join_path(DATA_PATH, 'last_update.txt'));
|
||||
$timestamp = @filemtime(join_path(DATA_PATH, self::LASTUPDATEFILE));
|
||||
if ($timestamp !== false) {
|
||||
$this->view->last_update_time = timestamptodate($timestamp);
|
||||
}
|
||||
}
|
||||
|
||||
public function indexAction() {
|
||||
public function indexAction(): void {
|
||||
FreshRSS_View::prependTitle(_t('admin.update.title') . ' · ');
|
||||
|
||||
if (file_exists(UPDATE_FILENAME)) {
|
||||
// There is an update file to apply!
|
||||
$version = @file_get_contents(join_path(DATA_PATH, 'last_update.txt'));
|
||||
$version = @file_get_contents(join_path(DATA_PATH, self::LASTUPDATEFILE));
|
||||
if ($version == '') {
|
||||
$version = 'unknown';
|
||||
}
|
||||
if (is_writable(FRESHRSS_PATH)) {
|
||||
if (@touch(FRESHRSS_PATH . '/index.html')) {
|
||||
$this->view->update_to_apply = true;
|
||||
$this->view->message = array(
|
||||
$this->view->message = [
|
||||
'status' => 'good',
|
||||
'title' => _t('gen.short.ok'),
|
||||
'body' => _t('feedback.update.can_apply', $version),
|
||||
);
|
||||
];
|
||||
} else {
|
||||
$this->view->message = array(
|
||||
$this->view->message = [
|
||||
'status' => 'bad',
|
||||
'title' => _t('gen.short.damn'),
|
||||
'body' => _t('feedback.update.file_is_nok', $version, FRESHRSS_PATH),
|
||||
);
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function checkAction() {
|
||||
private function is_release_channel_stable(string $currentVersion): bool {
|
||||
return strpos($currentVersion, 'dev') === false &&
|
||||
strpos($currentVersion, 'edge') === false;
|
||||
}
|
||||
|
||||
/* Check installation if there is a newer version.
|
||||
via Git, if available.
|
||||
Else via system configuration auto_update_url
|
||||
*/
|
||||
public function checkAction(): void {
|
||||
FreshRSS_View::prependTitle(_t('admin.update.title') . ' · ');
|
||||
$this->view->_path('update/index.phtml');
|
||||
|
||||
if (file_exists(UPDATE_FILENAME)) {
|
||||
// There is already an update file to apply: we don't need to check
|
||||
// There is already an update file to apply: we don’t need to check
|
||||
// the webserver!
|
||||
// Or if already check during the last hour, do nothing.
|
||||
Minz_Request::forward(array('c' => 'update'), true);
|
||||
Minz_Request::forward(['c' => 'update'], true);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$script = '';
|
||||
$version = '';
|
||||
|
||||
if (self::isGit()) {
|
||||
if (self::hasGitUpdate()) {
|
||||
$version = 'git';
|
||||
$version = self::getCurrentGitBranch();
|
||||
} else {
|
||||
$this->view->message = array(
|
||||
$this->view->message = [
|
||||
'status' => 'latest',
|
||||
'title' => _t('gen.short.damn'),
|
||||
'body' => _t('feedback.update.none')
|
||||
);
|
||||
@touch(join_path(DATA_PATH, 'last_update.txt'));
|
||||
'body' => _t('feedback.update.none'),
|
||||
];
|
||||
@touch(join_path(DATA_PATH, self::LASTUPDATEFILE));
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
$auto_update_url = FreshRSS_Context::$system_conf->auto_update_url . '?v=' . FRESHRSS_VERSION;
|
||||
$auto_update_url = FreshRSS_Context::systemConf()->auto_update_url . '/?v=' . FRESHRSS_VERSION;
|
||||
Minz_Log::debug('HTTP GET ' . $auto_update_url);
|
||||
$c = curl_init($auto_update_url);
|
||||
curl_setopt($c, CURLOPT_RETURNTRANSFER, true);
|
||||
curl_setopt($c, CURLOPT_SSL_VERIFYPEER, true);
|
||||
curl_setopt($c, CURLOPT_SSL_VERIFYHOST, 2);
|
||||
$result = curl_exec($c);
|
||||
$c_status = curl_getinfo($c, CURLINFO_HTTP_CODE);
|
||||
$c_error = curl_error($c);
|
||||
curl_close($c);
|
||||
$curlResource = curl_init($auto_update_url);
|
||||
|
||||
if ($c_status !== 200) {
|
||||
Minz_Log::warning(
|
||||
'Error during update (HTTP code ' . $c_status . '): ' . $c_error
|
||||
);
|
||||
|
||||
$this->view->message = array(
|
||||
if ($curlResource === false) {
|
||||
Minz_Log::warning('curl_init() failed');
|
||||
$this->view->message = [
|
||||
'status' => 'bad',
|
||||
'title' => _t('gen.short.damn'),
|
||||
'body' => _t('feedback.update.server_not_found', $auto_update_url)
|
||||
];
|
||||
return;
|
||||
}
|
||||
curl_setopt($curlResource, CURLOPT_RETURNTRANSFER, true);
|
||||
curl_setopt($curlResource, CURLOPT_SSL_VERIFYPEER, true);
|
||||
curl_setopt($curlResource, CURLOPT_SSL_VERIFYHOST, 2);
|
||||
$result = curl_exec($curlResource);
|
||||
$curlGetinfo = curl_getinfo($curlResource, CURLINFO_HTTP_CODE);
|
||||
$curlError = curl_error($curlResource);
|
||||
curl_close($curlResource);
|
||||
|
||||
if ($curlGetinfo !== 200) {
|
||||
Minz_Log::warning(
|
||||
'Error during update (HTTP code ' . $curlGetinfo . '): ' . $curlError
|
||||
);
|
||||
|
||||
$this->view->message = [
|
||||
'status' => 'bad',
|
||||
'body' => _t('feedback.update.server_not_found', $auto_update_url),
|
||||
];
|
||||
return;
|
||||
}
|
||||
|
||||
$res_array = explode("\n", $result, 2);
|
||||
$res_array = explode("\n", (string)$result, 2);
|
||||
$status = $res_array[0];
|
||||
if (strpos($status, 'UPDATE') !== 0) {
|
||||
$this->view->message = array(
|
||||
$this->view->message = [
|
||||
'status' => 'latest',
|
||||
'title' => _t('gen.short.damn'),
|
||||
'body' => _t('feedback.update.none')
|
||||
);
|
||||
@touch(join_path(DATA_PATH, 'last_update.txt'));
|
||||
'body' => _t('feedback.update.none'),
|
||||
];
|
||||
@touch(join_path(DATA_PATH, self::LASTUPDATEFILE));
|
||||
return;
|
||||
}
|
||||
|
||||
$script = $res_array[1];
|
||||
$version = explode(' ', $status, 2);
|
||||
$version = $version[1];
|
||||
|
||||
Minz_Log::notice(_t('admin.update.copiedFromURL', $auto_update_url));
|
||||
}
|
||||
|
||||
if (file_put_contents(UPDATE_FILENAME, $script) !== false) {
|
||||
@file_put_contents(join_path(DATA_PATH, 'last_update.txt'), $version);
|
||||
Minz_Request::forward(array('c' => 'update'), true);
|
||||
@file_put_contents(join_path(DATA_PATH, self::LASTUPDATEFILE), $version);
|
||||
Minz_Request::forward(['c' => 'update'], true);
|
||||
} else {
|
||||
$this->view->message = array(
|
||||
$this->view->message = [
|
||||
'status' => 'bad',
|
||||
'title' => _t('gen.short.damn'),
|
||||
'body' => _t('feedback.update.error', 'Cannot save the update script')
|
||||
);
|
||||
'body' => _t('feedback.update.error', 'Cannot save the update script'),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
public function applyAction() {
|
||||
if (!file_exists(UPDATE_FILENAME) || !is_writable(FRESHRSS_PATH) || Minz_Configuration::get('system')->disable_update) {
|
||||
Minz_Request::forward(array('c' => 'update'), true);
|
||||
public function applyAction(): void {
|
||||
if (FreshRSS_Context::systemConf()->disable_update || !file_exists(UPDATE_FILENAME) || !touch(FRESHRSS_PATH . '/index.html')) {
|
||||
Minz_Request::forward(['c' => 'update'], true);
|
||||
}
|
||||
|
||||
if (Minz_Request::param('post_conf', false)) {
|
||||
if (Minz_Request::paramBoolean('post_conf')) {
|
||||
if (self::isGit()) {
|
||||
$res = !self::hasGitUpdate();
|
||||
} else {
|
||||
require(UPDATE_FILENAME);
|
||||
// @phpstan-ignore-next-line
|
||||
// @phpstan-ignore function.notFound
|
||||
$res = do_post_update();
|
||||
}
|
||||
|
||||
Minz_ExtensionManager::callHook('post_update');
|
||||
Minz_ExtensionManager::callHookVoid('post_update');
|
||||
|
||||
if ($res === true) {
|
||||
@unlink(UPDATE_FILENAME);
|
||||
@file_put_contents(join_path(DATA_PATH, 'last_update.txt'), '');
|
||||
@file_put_contents(join_path(DATA_PATH, self::LASTUPDATEFILE), '');
|
||||
Minz_Log::notice(_t('feedback.update.finished'));
|
||||
Minz_Request::good(_t('feedback.update.finished'));
|
||||
} else {
|
||||
Minz_Log::error(_t('feedback.update.error', $res));
|
||||
Minz_Request::bad(_t('feedback.update.error', $res), [ 'c' => 'update', 'a' => 'index' ]);
|
||||
}
|
||||
} else {
|
||||
|
@ -245,25 +299,30 @@ class FreshRSS_update_Controller extends Minz_ActionController {
|
|||
} else {
|
||||
require(UPDATE_FILENAME);
|
||||
if (Minz_Request::isPost()) {
|
||||
// @phpstan-ignore-next-line
|
||||
// @phpstan-ignore function.notFound
|
||||
save_info_update();
|
||||
}
|
||||
// @phpstan-ignore-next-line
|
||||
// @phpstan-ignore function.notFound
|
||||
if (!need_info_update()) {
|
||||
// @phpstan-ignore-next-line
|
||||
// @phpstan-ignore function.notFound
|
||||
$res = apply_update();
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (function_exists('opcache_reset')) {
|
||||
opcache_reset();
|
||||
}
|
||||
|
||||
if ($res === true) {
|
||||
Minz_Request::forward(array(
|
||||
Minz_Request::forward([
|
||||
'c' => 'update',
|
||||
'a' => 'apply',
|
||||
'params' => array('post_conf' => true)
|
||||
), true);
|
||||
'params' => ['post_conf' => '1'],
|
||||
], true);
|
||||
} else {
|
||||
Minz_Log::error(_t('feedback.update.error', $res));
|
||||
Minz_Request::bad(_t('feedback.update.error', $res), [ 'c' => 'update', 'a' => 'index' ]);
|
||||
}
|
||||
}
|
||||
|
@ -272,7 +331,7 @@ class FreshRSS_update_Controller extends Minz_ActionController {
|
|||
/**
|
||||
* This action displays information about installation.
|
||||
*/
|
||||
public function checkInstallAction() {
|
||||
public function checkInstallAction(): void {
|
||||
FreshRSS_View::prependTitle(_t('admin.check_install.title') . ' · ');
|
||||
|
||||
$this->view->status_php = check_install_php();
|
||||
|
|
|
@ -1,24 +1,26 @@
|
|||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* Controller to handle user actions.
|
||||
*/
|
||||
class FreshRSS_user_Controller extends Minz_ActionController {
|
||||
class FreshRSS_user_Controller extends FreshRSS_ActionController {
|
||||
/**
|
||||
* The username is also used as folder name, file name, and part of SQL table name.
|
||||
* '_' is a reserved internal username.
|
||||
*/
|
||||
const USERNAME_PATTERN = '([0-9a-zA-Z_][0-9a-zA-Z_.@-]{1,38}|[0-9a-zA-Z])';
|
||||
public const USERNAME_PATTERN = '([0-9a-zA-Z_][0-9a-zA-Z_.@\-]{1,38}|[0-9a-zA-Z])';
|
||||
|
||||
public static function checkUsername($username) {
|
||||
public static function checkUsername(string $username): bool {
|
||||
return preg_match('/^' . self::USERNAME_PATTERN . '$/', $username) === 1;
|
||||
}
|
||||
|
||||
public static function userExists($username) {
|
||||
public static function userExists(string $username): bool {
|
||||
return @file_exists(USERS_PATH . '/' . $username . '/config.php');
|
||||
}
|
||||
|
||||
public static function updateUser($user, $email, $passwordPlain, $userConfigUpdated = array()) {
|
||||
/** @param array<string,mixed> $userConfigUpdated */
|
||||
public static function updateUser(string $user, ?string $email, string $passwordPlain, array $userConfigUpdated = []): bool {
|
||||
$userConfig = get_user_configuration($user);
|
||||
if ($userConfig === null) {
|
||||
return false;
|
||||
|
@ -27,9 +29,9 @@ class FreshRSS_user_Controller extends Minz_ActionController {
|
|||
if ($email !== null && $userConfig->mail_login !== $email) {
|
||||
$userConfig->mail_login = $email;
|
||||
|
||||
if (FreshRSS_Context::$system_conf->force_email_validation) {
|
||||
$salt = FreshRSS_Context::$system_conf->salt;
|
||||
$userConfig->email_validation_token = sha1($salt . uniqid(mt_rand(), true));
|
||||
if (FreshRSS_Context::systemConf()->force_email_validation) {
|
||||
$salt = FreshRSS_Context::systemConf()->salt;
|
||||
$userConfig->email_validation_token = sha1($salt . uniqid('' . mt_rand(), true));
|
||||
$mailer = new FreshRSS_User_Mailer();
|
||||
$mailer->send_email_need_validation($user, $userConfig);
|
||||
}
|
||||
|
@ -40,11 +42,9 @@ class FreshRSS_user_Controller extends Minz_ActionController {
|
|||
$userConfig->passwordHash = $passwordHash;
|
||||
}
|
||||
|
||||
if (is_array($userConfigUpdated)) {
|
||||
foreach ($userConfigUpdated as $configName => $configValue) {
|
||||
if ($configValue !== null) {
|
||||
$userConfig->_param($configName, $configValue);
|
||||
}
|
||||
foreach ($userConfigUpdated as $configName => $configValue) {
|
||||
if ($configValue !== null) {
|
||||
$userConfig->_param($configName, $configValue);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -52,30 +52,30 @@ class FreshRSS_user_Controller extends Minz_ActionController {
|
|||
return $ok;
|
||||
}
|
||||
|
||||
public function updateAction() {
|
||||
public function updateAction(): void {
|
||||
if (!FreshRSS_Auth::hasAccess('admin')) {
|
||||
Minz_Error::error(403);
|
||||
}
|
||||
|
||||
if (Minz_Request::isPost()) {
|
||||
$passwordPlain = Minz_Request::param('newPasswordPlain', '', true);
|
||||
$passwordPlain = Minz_Request::paramString('newPasswordPlain', true);
|
||||
Minz_Request::_param('newPasswordPlain'); //Discard plain-text password ASAP
|
||||
$_POST['newPasswordPlain'] = '';
|
||||
|
||||
$username = Minz_Request::param('username');
|
||||
$ok = self::updateUser($username, null, $passwordPlain, array(
|
||||
'token' => Minz_Request::param('token', null),
|
||||
));
|
||||
$username = Minz_Request::paramString('username');
|
||||
$ok = self::updateUser($username, null, $passwordPlain, [
|
||||
'token' => Minz_Request::paramString('token') ?: null,
|
||||
]);
|
||||
|
||||
if ($ok) {
|
||||
$isSelfUpdate = Minz_Session::param('currentUser', '_') === $username;
|
||||
$isSelfUpdate = Minz_User::name() === $username;
|
||||
if ($passwordPlain == '' || !$isSelfUpdate) {
|
||||
Minz_Request::good(_t('feedback.user.updated', $username), array('c' => 'user', 'a' => 'manage'));
|
||||
Minz_Request::good(_t('feedback.user.updated', $username), ['c' => 'user', 'a' => 'manage']);
|
||||
} else {
|
||||
Minz_Request::good(_t('feedback.profile.updated'), array('c' => 'index', 'a' => 'index'));
|
||||
Minz_Request::good(_t('feedback.profile.updated'), ['c' => 'index', 'a' => 'index']);
|
||||
}
|
||||
} else {
|
||||
Minz_Request::bad(_t('feedback.user.updated.error', $username), [ 'c' => 'user', 'a' => 'manage' ]);
|
||||
Minz_Request::bad(_t('feedback.user.updated.error', $username), ['c' => 'user', 'a' => 'manage']);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -83,12 +83,12 @@ class FreshRSS_user_Controller extends Minz_ActionController {
|
|||
/**
|
||||
* This action displays the user profile page.
|
||||
*/
|
||||
public function profileAction() {
|
||||
public function profileAction(): void {
|
||||
if (!FreshRSS_Auth::hasAccess()) {
|
||||
Minz_Error::error(403);
|
||||
}
|
||||
|
||||
$email_not_verified = FreshRSS_Context::$user_conf->email_validation_token != '';
|
||||
$email_not_verified = FreshRSS_Context::userConf()->email_validation_token != '';
|
||||
$this->view->disable_aside = false;
|
||||
if ($email_not_verified) {
|
||||
$this->view->_layout('simple');
|
||||
|
@ -99,62 +99,60 @@ class FreshRSS_user_Controller extends Minz_ActionController {
|
|||
|
||||
FreshRSS_View::appendScript(Minz_Url::display('/scripts/bcrypt.min.js?' . @filemtime(PUBLIC_PATH . '/scripts/bcrypt.min.js')));
|
||||
|
||||
if (Minz_Request::isPost()) {
|
||||
$system_conf = FreshRSS_Context::$system_conf;
|
||||
$user_config = FreshRSS_Context::$user_conf;
|
||||
$old_email = $user_config->mail_login;
|
||||
if (Minz_Request::isPost() && Minz_User::name() != null) {
|
||||
$old_email = FreshRSS_Context::userConf()->mail_login;
|
||||
|
||||
$email = trim(Minz_Request::param('email', ''));
|
||||
$passwordPlain = Minz_Request::param('newPasswordPlain', '', true);
|
||||
$email = Minz_Request::paramString('email');
|
||||
$passwordPlain = Minz_Request::paramString('newPasswordPlain', true);
|
||||
Minz_Request::_param('newPasswordPlain'); //Discard plain-text password ASAP
|
||||
$_POST['newPasswordPlain'] = '';
|
||||
|
||||
if ($system_conf->force_email_validation && empty($email)) {
|
||||
if (FreshRSS_Context::systemConf()->force_email_validation && empty($email)) {
|
||||
Minz_Request::bad(
|
||||
_t('user.email.feedback.required'),
|
||||
array('c' => 'user', 'a' => 'profile')
|
||||
['c' => 'user', 'a' => 'profile']
|
||||
);
|
||||
}
|
||||
|
||||
if (!empty($email) && !validateEmailAddress($email)) {
|
||||
Minz_Request::bad(
|
||||
_t('user.email.feedback.invalid'),
|
||||
array('c' => 'user', 'a' => 'profile')
|
||||
['c' => 'user', 'a' => 'profile']
|
||||
);
|
||||
}
|
||||
|
||||
$ok = self::updateUser(
|
||||
Minz_Session::param('currentUser'),
|
||||
Minz_User::name(),
|
||||
$email,
|
||||
$passwordPlain,
|
||||
array(
|
||||
'token' => Minz_Request::param('token', null),
|
||||
)
|
||||
[
|
||||
'token' => Minz_Request::paramString('token'),
|
||||
]
|
||||
);
|
||||
|
||||
Minz_Session::_param('passwordHash', FreshRSS_Context::$user_conf->passwordHash);
|
||||
Minz_Session::_param('passwordHash', FreshRSS_Context::userConf()->passwordHash);
|
||||
|
||||
if ($ok) {
|
||||
if ($system_conf->force_email_validation && $email !== $old_email) {
|
||||
Minz_Request::good(_t('feedback.profile.updated'), array('c' => 'user', 'a' => 'validateEmail'));
|
||||
if (FreshRSS_Context::systemConf()->force_email_validation && $email !== $old_email) {
|
||||
Minz_Request::good(_t('feedback.profile.updated'), ['c' => 'user', 'a' => 'validateEmail']);
|
||||
} elseif ($passwordPlain == '') {
|
||||
Minz_Request::good(_t('feedback.profile.updated'), array('c' => 'user', 'a' => 'profile'));
|
||||
Minz_Request::good(_t('feedback.profile.updated'), ['c' => 'user', 'a' => 'profile']);
|
||||
} else {
|
||||
Minz_Request::good(_t('feedback.profile.updated'), array('c' => 'index', 'a' => 'index'));
|
||||
Minz_Request::good(_t('feedback.profile.updated'), ['c' => 'index', 'a' => 'index']);
|
||||
}
|
||||
} else {
|
||||
Minz_Request::bad(_t('feedback.profile.error'), [ 'c' => 'user', 'a' => 'profile' ]);
|
||||
Minz_Request::bad(_t('feedback.profile.error'), ['c' => 'user', 'a' => 'profile']);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function purgeAction() {
|
||||
public function purgeAction(): void {
|
||||
if (!FreshRSS_Auth::hasAccess('admin')) {
|
||||
Minz_Error::error(403);
|
||||
}
|
||||
|
||||
if (Minz_Request::isPost()) {
|
||||
$username = Minz_Request::param('username');
|
||||
$username = Minz_Request::paramString('username');
|
||||
|
||||
if (!FreshRSS_UserDAO::exists($username)) {
|
||||
Minz_Error::error(404);
|
||||
|
@ -168,7 +166,7 @@ class FreshRSS_user_Controller extends Minz_ActionController {
|
|||
/**
|
||||
* This action displays the user management page.
|
||||
*/
|
||||
public function manageAction() {
|
||||
public function manageAction(): void {
|
||||
if (!FreshRSS_Auth::hasAccess('admin')) {
|
||||
Minz_Error::error(403);
|
||||
}
|
||||
|
@ -176,7 +174,7 @@ class FreshRSS_user_Controller extends Minz_ActionController {
|
|||
FreshRSS_View::prependTitle(_t('admin.user.title') . ' · ');
|
||||
|
||||
if (Minz_Request::isPost()) {
|
||||
$action = Minz_Request::param('action');
|
||||
$action = Minz_Request::paramString('action');
|
||||
switch ($action) {
|
||||
case 'delete':
|
||||
$this->deleteAction();
|
||||
|
@ -202,15 +200,21 @@ class FreshRSS_user_Controller extends Minz_ActionController {
|
|||
}
|
||||
}
|
||||
|
||||
$this->view->show_email_field = FreshRSS_Context::$system_conf->force_email_validation;
|
||||
$this->view->current_user = Minz_Request::param('u');
|
||||
$this->view->show_email_field = FreshRSS_Context::systemConf()->force_email_validation;
|
||||
$this->view->current_user = Minz_Request::paramString('u');
|
||||
|
||||
foreach (listUsers() as $user) {
|
||||
$this->view->users[$user] = $this->retrieveUserDetails($user);
|
||||
}
|
||||
}
|
||||
|
||||
public static function createUser($new_user_name, $email, $passwordPlain, $userConfigOverride = [], $insertDefaultFeeds = true) {
|
||||
/**
|
||||
* @param array<string,mixed> $userConfigOverride
|
||||
* @throws Minz_ConfigurationNamespaceException
|
||||
* @throws Minz_PDOConnectionException
|
||||
*/
|
||||
public static function createUser(string $new_user_name, ?string $email, string $passwordPlain,
|
||||
array $userConfigOverride = [], bool $insertDefaultFeeds = true): bool {
|
||||
$userConfig = [];
|
||||
|
||||
$customUserConfigPath = join_path(DATA_PATH, 'config-user.custom.php');
|
||||
|
@ -221,9 +225,7 @@ class FreshRSS_user_Controller extends Minz_ActionController {
|
|||
}
|
||||
}
|
||||
|
||||
if (is_array($userConfigOverride)) {
|
||||
$userConfig = array_merge($userConfig, $userConfigOverride);
|
||||
}
|
||||
$userConfig = array_merge($userConfig, $userConfigOverride);
|
||||
|
||||
$ok = self::checkUsername($new_user_name);
|
||||
$homeDir = join_path(DATA_PATH, 'users', $new_user_name);
|
||||
|
@ -231,18 +233,18 @@ class FreshRSS_user_Controller extends Minz_ActionController {
|
|||
|
||||
if ($ok) {
|
||||
$languages = Minz_Translate::availableLanguages();
|
||||
if (empty($userConfig['language']) || !in_array($userConfig['language'], $languages)) {
|
||||
if (empty($userConfig['language']) || !in_array($userConfig['language'], $languages, true)) {
|
||||
$userConfig['language'] = 'en';
|
||||
}
|
||||
|
||||
$ok &= !in_array(strtoupper($new_user_name), array_map('strtoupper', listUsers())); //Not an existing user, case-insensitive
|
||||
$ok &= !in_array(strtoupper($new_user_name), array_map('strtoupper', listUsers()), true); //Not an existing user, case-insensitive
|
||||
|
||||
$configPath = join_path($homeDir, 'config.php');
|
||||
$ok &= !file_exists($configPath);
|
||||
}
|
||||
if ($ok) {
|
||||
if (!is_dir($homeDir)) {
|
||||
mkdir($homeDir);
|
||||
mkdir($homeDir, 0770, true);
|
||||
}
|
||||
$ok &= (file_put_contents($configPath, "<?php\n return " . var_export($userConfig, true) . ';') !== false);
|
||||
}
|
||||
|
@ -265,7 +267,7 @@ class FreshRSS_user_Controller extends Minz_ActionController {
|
|||
|
||||
$ok &= self::updateUser($new_user_name, $email, $passwordPlain);
|
||||
}
|
||||
return $ok;
|
||||
return (bool)$ok;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -279,22 +281,19 @@ class FreshRSS_user_Controller extends Minz_ActionController {
|
|||
* - r (i.e. a redirection url, optional)
|
||||
*
|
||||
* @todo clean up this method. Idea: write a method to init a user with basic information.
|
||||
* @todo handle r redirection in Minz_Request::forward directly?
|
||||
*/
|
||||
public function createAction() {
|
||||
public function createAction(): void {
|
||||
if (!FreshRSS_Auth::hasAccess('admin') && max_registrations_reached()) {
|
||||
Minz_Error::error(403);
|
||||
}
|
||||
|
||||
if (Minz_Request::isPost()) {
|
||||
$system_conf = FreshRSS_Context::$system_conf;
|
||||
|
||||
$new_user_name = Minz_Request::param('new_user_name');
|
||||
$email = Minz_Request::param('new_user_email', '');
|
||||
$passwordPlain = Minz_Request::param('new_user_passwordPlain', '', true);
|
||||
$new_user_name = Minz_Request::paramString('new_user_name');
|
||||
$email = Minz_Request::paramString('new_user_email');
|
||||
$passwordPlain = Minz_Request::paramString('new_user_passwordPlain', true);
|
||||
$badRedirectUrl = [
|
||||
'c' => Minz_Request::param('originController', 'auth'),
|
||||
'a' => Minz_Request::param('originAction', 'register'),
|
||||
'c' => Minz_Request::paramString('originController') ?: 'auth',
|
||||
'a' => Minz_Request::paramString('originAction') ?: 'register',
|
||||
];
|
||||
|
||||
if (!self::checkUsername($new_user_name)) {
|
||||
|
@ -318,10 +317,16 @@ class FreshRSS_user_Controller extends Minz_ActionController {
|
|||
);
|
||||
}
|
||||
|
||||
$tos_enabled = file_exists(join_path(DATA_PATH, 'tos.html'));
|
||||
$accept_tos = Minz_Request::param('accept_tos', false);
|
||||
if (!FreshRSS_Auth::hasAccess('admin')) {
|
||||
// TODO: We may want to ask the user to accept TOS before first login
|
||||
$tos_enabled = file_exists(TOS_FILENAME);
|
||||
$accept_tos = Minz_Request::paramBoolean('accept_tos');
|
||||
if ($tos_enabled && !$accept_tos) {
|
||||
Minz_Request::bad(_t('user.tos.feedback.invalid'), $badRedirectUrl);
|
||||
}
|
||||
}
|
||||
|
||||
if ($system_conf->force_email_validation && empty($email)) {
|
||||
if (FreshRSS_Context::systemConf()->force_email_validation && empty($email)) {
|
||||
Minz_Request::bad(
|
||||
_t('user.email.feedback.required'),
|
||||
$badRedirectUrl
|
||||
|
@ -335,34 +340,32 @@ class FreshRSS_user_Controller extends Minz_ActionController {
|
|||
);
|
||||
}
|
||||
|
||||
if ($tos_enabled && !$accept_tos) {
|
||||
Minz_Request::bad(
|
||||
_t('user.tos.feedback.invalid'),
|
||||
$badRedirectUrl
|
||||
);
|
||||
}
|
||||
|
||||
$ok = self::createUser($new_user_name, $email, $passwordPlain, array(
|
||||
'language' => Minz_Request::param('new_user_language', FreshRSS_Context::$user_conf->language),
|
||||
$ok = self::createUser($new_user_name, $email, $passwordPlain, [
|
||||
'language' => Minz_Request::paramString('new_user_language') ?: FreshRSS_Context::userConf()->language,
|
||||
'timezone' => Minz_Request::paramString('new_user_timezone'),
|
||||
'is_admin' => Minz_Request::paramBoolean('new_user_is_admin'),
|
||||
'enabled' => true,
|
||||
));
|
||||
]);
|
||||
Minz_Request::_param('new_user_passwordPlain'); //Discard plain-text password ASAP
|
||||
$_POST['new_user_passwordPlain'] = '';
|
||||
invalidateHttpCache();
|
||||
|
||||
// If the user has admin access, it means he's already logged in
|
||||
// and we don't want to login with the new account. Otherwise, the
|
||||
// If the user has admin access, it means he’s already logged in
|
||||
// and we don’t want to login with the new account. Otherwise, the
|
||||
// user just created its account himself so he probably wants to
|
||||
// get started immediately.
|
||||
if ($ok && !FreshRSS_Auth::hasAccess('admin')) {
|
||||
$user_conf = get_user_configuration($new_user_name);
|
||||
Minz_Session::_params([
|
||||
'currentUser' => $new_user_name,
|
||||
'passwordHash' => $user_conf->passwordHash,
|
||||
'csrf' => false,
|
||||
]);
|
||||
FreshRSS_Auth::giveAccess();
|
||||
if ($user_conf !== null) {
|
||||
Minz_Session::_params([
|
||||
Minz_User::CURRENT_USER => $new_user_name,
|
||||
'passwordHash' => $user_conf->passwordHash,
|
||||
'csrf' => false,
|
||||
]);
|
||||
FreshRSS_Auth::giveAccess();
|
||||
} else {
|
||||
$ok = false;
|
||||
}
|
||||
}
|
||||
|
||||
if ($ok) {
|
||||
|
@ -372,29 +375,31 @@ class FreshRSS_user_Controller extends Minz_ActionController {
|
|||
}
|
||||
}
|
||||
|
||||
$redirect_url = urldecode(Minz_Request::param('r', false, true));
|
||||
if (!$redirect_url) {
|
||||
$redirect_url = array('c' => 'user', 'a' => 'manage');
|
||||
}
|
||||
$redirect_url = ['c' => 'user', 'a' => 'manage'];
|
||||
Minz_Request::forward($redirect_url, true);
|
||||
}
|
||||
|
||||
public static function deleteUser($username) {
|
||||
public static function deleteUser(string $username): bool {
|
||||
$ok = self::checkUsername($username);
|
||||
if ($ok) {
|
||||
$default_user = FreshRSS_Context::$system_conf->default_user;
|
||||
$default_user = FreshRSS_Context::systemConf()->default_user;
|
||||
$ok &= (strcasecmp($username, $default_user) !== 0); //It is forbidden to delete the default user
|
||||
}
|
||||
$user_data = join_path(DATA_PATH, 'users', $username);
|
||||
$ok &= is_dir($user_data);
|
||||
if ($ok) {
|
||||
FreshRSS_fever_Util::deleteKey($username);
|
||||
Minz_ModelPdo::$usesSharedPdo = false;
|
||||
$oldUserDAO = FreshRSS_Factory::createUserDao($username);
|
||||
$ok &= $oldUserDAO->deleteUser();
|
||||
Minz_ModelPdo::$usesSharedPdo = true;
|
||||
$ok &= recursive_unlink($user_data);
|
||||
array_map('unlink', glob(PSHB_PATH . '/feeds/*/' . $username . '.txt'));
|
||||
$filenames = glob(PSHB_PATH . '/feeds/*/' . $username . '.txt');
|
||||
if (!empty($filenames)) {
|
||||
array_map('unlink', $filenames);
|
||||
}
|
||||
}
|
||||
return $ok;
|
||||
return (bool)$ok;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -407,48 +412,50 @@ class FreshRSS_user_Controller extends Minz_ActionController {
|
|||
*
|
||||
* This route works with GET requests since the URL is provided by email.
|
||||
* The security risks (e.g. forged URL by an attacker) are not very high so
|
||||
* it's ok.
|
||||
* it’s ok.
|
||||
*
|
||||
* It returns 404 error if `force_email_validation` is disabled or if the
|
||||
* user doesn't exist.
|
||||
* user doesn’t exist.
|
||||
*
|
||||
* It returns 403 if user isn't logged in and `username` param isn't passed.
|
||||
* It returns 403 if user isn’t logged in and `username` param isn’t passed.
|
||||
*/
|
||||
public function validateEmailAction() {
|
||||
if (!FreshRSS_Context::$system_conf->force_email_validation) {
|
||||
public function validateEmailAction(): void {
|
||||
if (!FreshRSS_Context::systemConf()->force_email_validation) {
|
||||
Minz_Error::error(404);
|
||||
}
|
||||
|
||||
FreshRSS_View::prependTitle(_t('user.email.validation.title') . ' · ');
|
||||
$this->view->_layout('simple');
|
||||
|
||||
$username = Minz_Request::param('username');
|
||||
$token = Minz_Request::param('token');
|
||||
$username = Minz_Request::paramString('username');
|
||||
$token = Minz_Request::paramString('token');
|
||||
|
||||
if ($username) {
|
||||
if ($username !== '') {
|
||||
$user_config = get_user_configuration($username);
|
||||
} elseif (FreshRSS_Auth::hasAccess()) {
|
||||
$user_config = FreshRSS_Context::$user_conf;
|
||||
$user_config = FreshRSS_Context::userConf();
|
||||
} else {
|
||||
return Minz_Error::error(403);
|
||||
Minz_Error::error(403);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!FreshRSS_UserDAO::exists($username) || $user_config === null) {
|
||||
return Minz_Error::error(404);
|
||||
Minz_Error::error(404);
|
||||
return;
|
||||
}
|
||||
|
||||
if ($user_config->email_validation_token === '') {
|
||||
Minz_Request::good(
|
||||
_t('user.email.validation.feedback.unnecessary'),
|
||||
array('c' => 'index', 'a' => 'index')
|
||||
['c' => 'index', 'a' => 'index']
|
||||
);
|
||||
}
|
||||
|
||||
if ($token) {
|
||||
if ($token != '') {
|
||||
if ($user_config->email_validation_token !== $token) {
|
||||
Minz_Request::bad(
|
||||
_t('user.email.validation.feedback.wrong_token'),
|
||||
array('c' => 'user', 'a' => 'validateEmail')
|
||||
['c' => 'user', 'a' => 'validateEmail']
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -456,12 +463,12 @@ class FreshRSS_user_Controller extends Minz_ActionController {
|
|||
if ($user_config->save()) {
|
||||
Minz_Request::good(
|
||||
_t('user.email.validation.feedback.ok'),
|
||||
array('c' => 'index', 'a' => 'index')
|
||||
['c' => 'index', 'a' => 'index']
|
||||
);
|
||||
} else {
|
||||
Minz_Request::bad(
|
||||
_t('user.email.validation.feedback.error'),
|
||||
array('c' => 'user', 'a' => 'validateEmail')
|
||||
['c' => 'user', 'a' => 'validateEmail']
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -470,14 +477,14 @@ class FreshRSS_user_Controller extends Minz_ActionController {
|
|||
/**
|
||||
* This action resends a validation email to the current user.
|
||||
*
|
||||
* It only acts on POST requests but doesn't require any param (except the
|
||||
* It only acts on POST requests but doesn’t require any param (except the
|
||||
* CSRF token).
|
||||
*
|
||||
* It returns 403 error if the user is not logged in or 404 if request is
|
||||
* not POST. Else it redirects silently to the index if user has already
|
||||
* validated its email, or to the user#validateEmail route.
|
||||
*/
|
||||
public function sendValidationEmailAction() {
|
||||
public function sendValidationEmailAction(): void {
|
||||
if (!FreshRSS_Auth::hasAccess()) {
|
||||
Minz_Error::error(403);
|
||||
}
|
||||
|
@ -486,20 +493,19 @@ class FreshRSS_user_Controller extends Minz_ActionController {
|
|||
Minz_Error::error(404);
|
||||
}
|
||||
|
||||
$username = Minz_Session::param('currentUser', '_');
|
||||
$user_config = FreshRSS_Context::$user_conf;
|
||||
$username = Minz_User::name();
|
||||
|
||||
if ($user_config->email_validation_token === '') {
|
||||
Minz_Request::forward(array(
|
||||
if (FreshRSS_Context::userConf()->email_validation_token === '') {
|
||||
Minz_Request::forward([
|
||||
'c' => 'index',
|
||||
'a' => 'index',
|
||||
), true);
|
||||
], true);
|
||||
}
|
||||
|
||||
$mailer = new FreshRSS_User_Mailer();
|
||||
$ok = $mailer->send_email_need_validation($username, $user_config);
|
||||
$ok = $username != null && $mailer->send_email_need_validation($username, FreshRSS_Context::userConf());
|
||||
|
||||
$redirect_url = array('c' => 'user', 'a' => 'validateEmail');
|
||||
$redirect_url = ['c' => 'user', 'a' => 'validateEmail'];
|
||||
if ($ok) {
|
||||
Minz_Request::good(
|
||||
_t('user.email.validation.feedback.email_sent'),
|
||||
|
@ -521,28 +527,25 @@ class FreshRSS_user_Controller extends Minz_ActionController {
|
|||
*
|
||||
* @todo clean up this method. Idea: create a User->clean() method.
|
||||
*/
|
||||
public function deleteAction() {
|
||||
$username = Minz_Request::param('username');
|
||||
$self_deletion = Minz_Session::param('currentUser', '_') === $username;
|
||||
public function deleteAction(): void {
|
||||
$username = Minz_Request::paramString('username');
|
||||
$self_deletion = Minz_User::name() === $username;
|
||||
|
||||
if (!FreshRSS_Auth::hasAccess('admin') && !$self_deletion) {
|
||||
Minz_Error::error(403);
|
||||
}
|
||||
|
||||
$redirect_url = urldecode(Minz_Request::param('r', false, true));
|
||||
if (!$redirect_url) {
|
||||
$redirect_url = array('c' => 'user', 'a' => 'manage');
|
||||
}
|
||||
$redirect_url = ['c' => 'user', 'a' => 'manage'];
|
||||
|
||||
if (Minz_Request::isPost()) {
|
||||
$ok = true;
|
||||
if ($ok && $self_deletion) {
|
||||
// We check the password if it's a self-destruction
|
||||
$nonce = Minz_Session::param('nonce');
|
||||
$challenge = Minz_Request::param('challenge', '');
|
||||
if ($self_deletion) {
|
||||
// We check the password if it’s a self-destruction
|
||||
$nonce = Minz_Session::paramString('nonce');
|
||||
$challenge = Minz_Request::paramString('challenge');
|
||||
|
||||
$ok &= FreshRSS_FormAuth::checkCredentials(
|
||||
$username, FreshRSS_Context::$user_conf->passwordHash,
|
||||
$username, FreshRSS_Context::userConf()->passwordHash,
|
||||
$nonce, $challenge
|
||||
);
|
||||
}
|
||||
|
@ -551,7 +554,7 @@ class FreshRSS_user_Controller extends Minz_ActionController {
|
|||
}
|
||||
if ($ok && $self_deletion) {
|
||||
FreshRSS_Auth::removeAccess();
|
||||
$redirect_url = array('c' => 'index', 'a' => 'index');
|
||||
$redirect_url = ['c' => 'index', 'a' => 'index'];
|
||||
}
|
||||
invalidateHttpCache();
|
||||
|
||||
|
@ -565,23 +568,23 @@ class FreshRSS_user_Controller extends Minz_ActionController {
|
|||
Minz_Request::forward($redirect_url, true);
|
||||
}
|
||||
|
||||
public function promoteAction() {
|
||||
public function promoteAction(): void {
|
||||
$this->toggleAction('is_admin', true);
|
||||
}
|
||||
|
||||
public function demoteAction() {
|
||||
public function demoteAction(): void {
|
||||
$this->toggleAction('is_admin', false);
|
||||
}
|
||||
|
||||
public function enableAction() {
|
||||
public function enableAction(): void {
|
||||
$this->toggleAction('enabled', true);
|
||||
}
|
||||
|
||||
public function disableAction() {
|
||||
public function disableAction(): void {
|
||||
$this->toggleAction('enabled', false);
|
||||
}
|
||||
|
||||
private function toggleAction($field, $value) {
|
||||
private function toggleAction(string $field, bool $value): void {
|
||||
if (!FreshRSS_Auth::hasAccess('admin')) {
|
||||
Minz_Error::error(403);
|
||||
}
|
||||
|
@ -590,13 +593,14 @@ class FreshRSS_user_Controller extends Minz_ActionController {
|
|||
Minz_Error::error(403);
|
||||
}
|
||||
|
||||
$username = Minz_Request::param('username');
|
||||
$username = Minz_Request::paramString('username');
|
||||
if (!FreshRSS_UserDAO::exists($username)) {
|
||||
Minz_Error::error(404);
|
||||
}
|
||||
|
||||
if (null === $userConfig = get_user_configuration($username)) {
|
||||
Minz_Error::error(500);
|
||||
return;
|
||||
}
|
||||
|
||||
$userConfig->_param($field, $value);
|
||||
|
@ -605,35 +609,46 @@ class FreshRSS_user_Controller extends Minz_ActionController {
|
|||
FreshRSS_UserDAO::touch($username);
|
||||
|
||||
if ($ok) {
|
||||
Minz_Request::good(_t('feedback.user.updated', $username), array('c' => 'user', 'a' => 'manage'));
|
||||
Minz_Request::good(_t('feedback.user.updated', $username), ['c' => 'user', 'a' => 'manage']);
|
||||
} else {
|
||||
Minz_Request::bad(_t('feedback.user.updated.error', $username),
|
||||
array('c' => 'user', 'a' => 'manage'));
|
||||
Minz_Request::bad(
|
||||
_t('feedback.user.updated.error', $username),
|
||||
['c' => 'user', 'a' => 'manage']
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
public function detailsAction() {
|
||||
public function detailsAction(): void {
|
||||
if (!FreshRSS_Auth::hasAccess('admin')) {
|
||||
Minz_Error::error(403);
|
||||
}
|
||||
|
||||
$username = Minz_Request::param('username');
|
||||
$username = Minz_Request::paramString('username');
|
||||
if (!FreshRSS_UserDAO::exists($username)) {
|
||||
Minz_Error::error(404);
|
||||
}
|
||||
|
||||
if (Minz_Request::paramBoolean('ajax')) {
|
||||
$this->view->_layout(null);
|
||||
}
|
||||
|
||||
$this->view->username = $username;
|
||||
$this->view->details = $this->retrieveUserDetails($username);
|
||||
FreshRSS_View::prependTitle($username . ' · ' . _t('gen.menu.user_management') . ' · ');
|
||||
}
|
||||
|
||||
private function retrieveUserDetails($username) {
|
||||
/** @return array{'feed_count':int,'article_count':int,'database_size':int,'language':string,'mail_login':string,'enabled':bool,'is_admin':bool,'last_user_activity':string,'is_default':bool} */
|
||||
private function retrieveUserDetails(string $username): array {
|
||||
$feedDAO = FreshRSS_Factory::createFeedDao($username);
|
||||
$entryDAO = FreshRSS_Factory::createEntryDao($username);
|
||||
$databaseDAO = FreshRSS_Factory::createDatabaseDAO($username);
|
||||
|
||||
$userConfiguration = get_user_configuration($username);
|
||||
if ($userConfiguration === null) {
|
||||
throw new Exception('Error loading user configuration!');
|
||||
}
|
||||
|
||||
return array(
|
||||
return [
|
||||
'feed_count' => $feedDAO->count(),
|
||||
'article_count' => $entryDAO->count(),
|
||||
'database_size' => $databaseDAO->size(),
|
||||
|
@ -641,8 +656,8 @@ class FreshRSS_user_Controller extends Minz_ActionController {
|
|||
'mail_login' => $userConfiguration->mail_login,
|
||||
'enabled' => $userConfiguration->enabled,
|
||||
'is_admin' => $userConfiguration->is_admin,
|
||||
'last_user_activity' => date('c', FreshRSS_UserDAO::mtime($username)),
|
||||
'is_default' => FreshRSS_Context::$system_conf->default_user === $username,
|
||||
);
|
||||
'last_user_activity' => date('c', FreshRSS_UserDAO::mtime($username)) ?: '',
|
||||
'is_default' => FreshRSS_Context::systemConf()->default_user === $username,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,14 +1,16 @@
|
|||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
class FreshRSS_AlreadySubscribed_Exception extends Exception {
|
||||
private $feedName = '';
|
||||
class FreshRSS_AlreadySubscribed_Exception extends Minz_Exception {
|
||||
|
||||
public function __construct($url, $feedName) {
|
||||
private string $feedName = '';
|
||||
|
||||
public function __construct(string $url, string $feedName) {
|
||||
parent::__construct('Already subscribed! ' . $url, 2135);
|
||||
$this->feedName = $feedName;
|
||||
}
|
||||
|
||||
public function feedName() {
|
||||
public function feedName(): string {
|
||||
return $this->feedName;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,8 +1,9 @@
|
|||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
class FreshRSS_BadUrl_Exception extends FreshRSS_Feed_Exception {
|
||||
|
||||
public function __construct($url) {
|
||||
public function __construct(string $url) {
|
||||
parent::__construct('`' . $url . '` is not a valid URL');
|
||||
}
|
||||
|
||||
|
|
|
@ -1,8 +1,9 @@
|
|||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* An exception raised when a context is invalid
|
||||
*/
|
||||
class FreshRSS_Context_Exception extends Exception {
|
||||
class FreshRSS_Context_Exception extends Minz_Exception {
|
||||
|
||||
}
|
||||
|
|
|
@ -1,5 +0,0 @@
|
|||
<?php
|
||||
|
||||
class FreshRSS_DAO_Exception extends Exception {
|
||||
|
||||
}
|
|
@ -1,5 +1,6 @@
|
|||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
class FreshRSS_EntriesGetter_Exception extends Exception {
|
||||
class FreshRSS_EntriesGetter_Exception extends Minz_Exception {
|
||||
|
||||
}
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
class FreshRSS_Feed_Exception extends Exception {
|
||||
class FreshRSS_Feed_Exception extends Minz_Exception {
|
||||
|
||||
}
|
||||
|
|
|
@ -1,14 +1,16 @@
|
|||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
class FreshRSS_FeedNotAdded_Exception extends Exception {
|
||||
private $feedName = '';
|
||||
class FreshRSS_FeedNotAdded_Exception extends Minz_Exception {
|
||||
|
||||
public function __construct($url, $feedName) {
|
||||
private string $url = '';
|
||||
|
||||
public function __construct(string $url) {
|
||||
parent::__construct('Feed not added! ' . $url, 2147);
|
||||
$this->feedName = $feedName;
|
||||
$this->url = $url;
|
||||
}
|
||||
|
||||
public function feedName() {
|
||||
return $this->feedName;
|
||||
public function url(): string {
|
||||
return $this->url;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,14 +1,16 @@
|
|||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
class FreshRSS_Zip_Exception extends Exception {
|
||||
private $zipErrorCode = 0;
|
||||
class FreshRSS_Zip_Exception extends Minz_Exception {
|
||||
|
||||
public function __construct($zipErrorCode) {
|
||||
private int $zipErrorCode = 0;
|
||||
|
||||
public function __construct(int $zipErrorCode) {
|
||||
parent::__construct('ZIP error!', 2141);
|
||||
$this->zipErrorCode = $zipErrorCode;
|
||||
}
|
||||
|
||||
public function zipErrorCode() {
|
||||
public function zipErrorCode(): int {
|
||||
return $this->zipErrorCode;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
class FreshRSS_ZipMissing_Exception extends Exception {
|
||||
class FreshRSS_ZipMissing_Exception extends Minz_Exception {
|
||||
}
|
||||
|
|
|
@ -1,11 +1,11 @@
|
|||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
class FreshRSS extends Minz_FrontController {
|
||||
/**
|
||||
* Initialize the different FreshRSS / Minz components.
|
||||
*
|
||||
* PLEASE DON'T CHANGE THE ORDER OF INITIALIZATIONS UNLESS YOU KNOW WHAT
|
||||
* YOU DO!!
|
||||
* PLEASE DON’T CHANGE THE ORDER OF INITIALIZATIONS UNLESS YOU KNOW WHAT YOU DO!!
|
||||
*
|
||||
* Here is the list of components:
|
||||
* - Create a configuration setter and register it to system conf
|
||||
|
@ -16,91 +16,101 @@ class FreshRSS extends Minz_FrontController {
|
|||
* - Init i18n (need context)
|
||||
* - Init sharing system (need user conf and i18n)
|
||||
* - Init generic styles and scripts (need user conf)
|
||||
* - Init notifications
|
||||
* - Enable user extensions (need all the other initializations)
|
||||
*/
|
||||
public function init() {
|
||||
public function init(): void {
|
||||
if (!isset($_SESSION)) {
|
||||
Minz_Session::init('FreshRSS');
|
||||
}
|
||||
|
||||
Minz_ActionController::$viewType = 'FreshRSS_View';
|
||||
|
||||
FreshRSS_Context::initSystem();
|
||||
if (FreshRSS_Context::$system_conf == null) {
|
||||
if (!FreshRSS_Context::hasSystemConf()) {
|
||||
$message = 'Error during context system init!';
|
||||
Minz_Error::error(500, [$message], false);
|
||||
Minz_Error::error(500, $message, false);
|
||||
die($message);
|
||||
}
|
||||
|
||||
if (FreshRSS_Context::systemConf()->logo_html != '') {
|
||||
// Relax Content Security Policy to allow external images if a custom logo HTML is used
|
||||
Minz_ActionController::_defaultCsp([
|
||||
'default-src' => "'self'",
|
||||
'img-src' => '* data:',
|
||||
]);
|
||||
}
|
||||
|
||||
// Load list of extensions and enable the "system" ones.
|
||||
Minz_ExtensionManager::init();
|
||||
|
||||
// Auth has to be initialized before using currentUser session parameter
|
||||
// because it's this part which create this parameter.
|
||||
// because it’s this part which create this parameter.
|
||||
self::initAuth();
|
||||
if (FreshRSS_Context::$user_conf == null) {
|
||||
if (!FreshRSS_Context::hasUserConf()) {
|
||||
FreshRSS_Context::initUser();
|
||||
}
|
||||
if (FreshRSS_Context::$user_conf == null) {
|
||||
if (!FreshRSS_Context::hasUserConf()) {
|
||||
$message = 'Error during context user init!';
|
||||
Minz_Error::error(500, [$message], false);
|
||||
Minz_Error::error(500, $message, false);
|
||||
die($message);
|
||||
}
|
||||
|
||||
// Complete initialization of the other FreshRSS / Minz components.
|
||||
self::initI18n();
|
||||
self::loadNotifications();
|
||||
// Enable extensions for the current (logged) user.
|
||||
if (FreshRSS_Auth::hasAccess() || FreshRSS_Context::$system_conf->allow_anonymous) {
|
||||
$ext_list = FreshRSS_Context::$user_conf->extensions_enabled;
|
||||
Minz_ExtensionManager::enableByList($ext_list);
|
||||
if (FreshRSS_Auth::hasAccess() || FreshRSS_Context::systemConf()->allow_anonymous) {
|
||||
$ext_list = FreshRSS_Context::userConf()->extensions_enabled;
|
||||
Minz_ExtensionManager::enableByList($ext_list, 'user');
|
||||
}
|
||||
|
||||
if (FreshRSS_Context::$system_conf->force_email_validation && !FreshRSS_Auth::hasAccess('admin')) {
|
||||
if (FreshRSS_Context::systemConf()->force_email_validation && !FreshRSS_Auth::hasAccess('admin')) {
|
||||
self::checkEmailValidated();
|
||||
}
|
||||
|
||||
Minz_ExtensionManager::callHook('freshrss_init');
|
||||
Minz_ExtensionManager::callHookVoid('freshrss_init');
|
||||
}
|
||||
|
||||
private static function initAuth() {
|
||||
private static function initAuth(): void {
|
||||
FreshRSS_Auth::init();
|
||||
if (Minz_Request::isPost()) {
|
||||
if (!(FreshRSS_Auth::isCsrfOk() ||
|
||||
if (!FreshRSS_Context::hasSystemConf() || !(FreshRSS_Auth::isCsrfOk() ||
|
||||
(Minz_Request::controllerName() === 'auth' && Minz_Request::actionName() === 'login') ||
|
||||
(Minz_Request::controllerName() === 'user' && Minz_Request::actionName() === 'create' && !FreshRSS_Auth::hasAccess('admin')) ||
|
||||
(Minz_Request::controllerName() === 'feed' && Minz_Request::actionName() === 'actualize'
|
||||
&& FreshRSS_Context::$system_conf->allow_anonymous_refresh) ||
|
||||
&& FreshRSS_Context::systemConf()->allow_anonymous_refresh) ||
|
||||
(Minz_Request::controllerName() === 'javascript' && Minz_Request::actionName() === 'actualize'
|
||||
&& FreshRSS_Context::$system_conf->allow_anonymous)
|
||||
&& FreshRSS_Context::systemConf()->allow_anonymous)
|
||||
)) {
|
||||
// Token-based protection against XSRF attacks, except for the login or self-create user forms
|
||||
self::initI18n();
|
||||
Minz_Error::error(403, array('error' => array(
|
||||
_t('feedback.access.denied'),
|
||||
' [CSRF]'
|
||||
)));
|
||||
Minz_Error::error(403, ['error' => [_t('feedback.access.denied'), ' [CSRF]']]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static function initI18n() {
|
||||
$userLanguage = isset(FreshRSS_Context::$user_conf) ? FreshRSS_Context::$user_conf->language : null;
|
||||
$systemLanguage = isset(FreshRSS_Context::$system_conf) ? FreshRSS_Context::$system_conf->language : null;
|
||||
private static function initI18n(): void {
|
||||
$userLanguage = FreshRSS_Context::hasUserConf() ? FreshRSS_Context::userConf()->language : null;
|
||||
$systemLanguage = FreshRSS_Context::hasSystemConf() ? FreshRSS_Context::systemConf()->language : null;
|
||||
$language = Minz_Translate::getLanguage($userLanguage, Minz_Request::getPreferredLanguages(), $systemLanguage);
|
||||
|
||||
Minz_Session::_param('language', $language);
|
||||
Minz_Translate::init($language);
|
||||
|
||||
$timezone = FreshRSS_Context::hasUserConf() ? FreshRSS_Context::userConf()->timezone : '';
|
||||
if ($timezone == '') {
|
||||
$timezone = FreshRSS_Context::defaultTimeZone();
|
||||
}
|
||||
date_default_timezone_set($timezone);
|
||||
}
|
||||
|
||||
private static function getThemeFileUrl($theme_id, $filename) {
|
||||
private static function getThemeFileUrl(string $theme_id, string $filename): string {
|
||||
$filetime = @filemtime(PUBLIC_PATH . '/themes/' . $theme_id . '/' . $filename);
|
||||
return '/themes/' . $theme_id . '/' . $filename . '?' . $filetime;
|
||||
}
|
||||
|
||||
public static function loadStylesAndScripts() {
|
||||
$theme = FreshRSS_Themes::load(FreshRSS_Context::$user_conf->theme);
|
||||
public static function loadStylesAndScripts(): void {
|
||||
if (!FreshRSS_Context::hasUserConf()) {
|
||||
return;
|
||||
}
|
||||
$theme = FreshRSS_Themes::load(FreshRSS_Context::userConf()->theme);
|
||||
if ($theme) {
|
||||
foreach(array_reverse($theme['files']) as $file) {
|
||||
switch (substr($file, -3)) {
|
||||
|
@ -125,30 +135,28 @@ class FreshRSS extends Minz_FrontController {
|
|||
FreshRSS_View::prependStyle(Minz_Url::display(FreshRSS::getThemeFileUrl($theme_id, $filename)));
|
||||
}
|
||||
}
|
||||
|
||||
if (!empty($theme['theme-color'])) {
|
||||
FreshRSS_View::appendThemeColors($theme['theme-color']);
|
||||
}
|
||||
}
|
||||
//Use prepend to insert before extensions. Added in reverse order.
|
||||
if (Minz_Request::controllerName() !== 'index') {
|
||||
if (!in_array(Minz_Request::controllerName(), ['index', ''], true)) {
|
||||
FreshRSS_View::prependScript(Minz_Url::display('/scripts/extra.js?' . @filemtime(PUBLIC_PATH . '/scripts/extra.js')));
|
||||
}
|
||||
FreshRSS_View::prependScript(Minz_Url::display('/scripts/main.js?' . @filemtime(PUBLIC_PATH . '/scripts/main.js')));
|
||||
}
|
||||
|
||||
private static function loadNotifications() {
|
||||
$notif = Minz_Request::getNotification();
|
||||
if ($notif) {
|
||||
FreshRSS_View::_param('notification', $notif);
|
||||
}
|
||||
}
|
||||
|
||||
public static function preLayout() {
|
||||
public static function preLayout(): void {
|
||||
header("X-Content-Type-Options: nosniff");
|
||||
|
||||
FreshRSS_Share::load(join_path(APP_PATH, 'shares.php'));
|
||||
self::loadStylesAndScripts();
|
||||
}
|
||||
|
||||
private static function checkEmailValidated() {
|
||||
$email_not_verified = FreshRSS_Auth::hasAccess() && FreshRSS_Context::$user_conf->email_validation_token !== '';
|
||||
private static function checkEmailValidated(): void {
|
||||
$email_not_verified = FreshRSS_Auth::hasAccess() &&
|
||||
FreshRSS_Context::hasUserConf() && FreshRSS_Context::userConf()->email_validation_token !== '';
|
||||
$action_is_allowed = (
|
||||
Minz_Request::is('user', 'validateEmail') ||
|
||||
Minz_Request::is('user', 'sendValidationEmail') ||
|
||||
|
@ -159,10 +167,10 @@ class FreshRSS extends Minz_FrontController {
|
|||
Minz_Request::is('javascript', 'nonce')
|
||||
);
|
||||
if ($email_not_verified && !$action_is_allowed) {
|
||||
Minz_Request::forward(array(
|
||||
Minz_Request::forward([
|
||||
'c' => 'user',
|
||||
'a' => 'validateEmail',
|
||||
), true);
|
||||
], true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,30 +1,41 @@
|
|||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* Manage the emails sent to the users.
|
||||
*/
|
||||
class FreshRSS_User_Mailer extends Minz_Mailer {
|
||||
public function send_email_need_validation($username, $user_config) {
|
||||
|
||||
/**
|
||||
* @var FreshRSS_View
|
||||
*/
|
||||
protected $view;
|
||||
|
||||
public function __construct() {
|
||||
parent::__construct(FreshRSS_View::class);
|
||||
}
|
||||
|
||||
public function send_email_need_validation(string $username, FreshRSS_UserConfiguration $user_config): bool {
|
||||
Minz_Translate::reset($user_config->language);
|
||||
|
||||
$this->view->_path('user_mailer/email_need_validation.txt.php');
|
||||
|
||||
$this->view->username = $username;
|
||||
$this->view->site_title = FreshRSS_Context::$system_conf->title;
|
||||
$this->view->site_title = FreshRSS_Context::systemConf()->title;
|
||||
$this->view->validation_url = Minz_Url::display(
|
||||
array(
|
||||
[
|
||||
'c' => 'user',
|
||||
'a' => 'validateEmail',
|
||||
'params' => array(
|
||||
'params' => [
|
||||
'username' => $username,
|
||||
'token' => $user_config->email_validation_token
|
||||
)
|
||||
),
|
||||
'token' => $user_config->email_validation_token,
|
||||
],
|
||||
],
|
||||
'txt',
|
||||
true
|
||||
);
|
||||
|
||||
$subject_prefix = '[' . FreshRSS_Context::$system_conf->title . ']';
|
||||
$subject_prefix = '[' . FreshRSS_Context::systemConf()->title . ']';
|
||||
return $this->mail(
|
||||
$user_config->mail_login,
|
||||
$subject_prefix . ' ' ._t('user.mailer.email_need_validation.title')
|
||||
|
|
|
@ -0,0 +1,14 @@
|
|||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
abstract class FreshRSS_ActionController extends Minz_ActionController {
|
||||
|
||||
/**
|
||||
* @var FreshRSS_View
|
||||
*/
|
||||
protected $view;
|
||||
|
||||
public function __construct(string $viewType = '') {
|
||||
parent::__construct($viewType === '' ? FreshRSS_View::class : $viewType);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,66 @@
|
|||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* Logic to work with (JSON) attributes (for entries, feeds, categories, tags...).
|
||||
*/
|
||||
trait FreshRSS_AttributesTrait {
|
||||
/**
|
||||
* @var array<string,mixed>
|
||||
*/
|
||||
private array $attributes = [];
|
||||
|
||||
/** @return array<string,mixed> */
|
||||
public function attributes(): array {
|
||||
return $this->attributes;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param non-empty-string $key
|
||||
* @return array<int|string,mixed>|null
|
||||
*/
|
||||
public function attributeArray(string $key): ?array {
|
||||
$a = $this->attributes[$key] ?? null;
|
||||
return is_array($a) ? $a : null;
|
||||
}
|
||||
|
||||
/** @param non-empty-string $key */
|
||||
public function attributeBoolean(string $key): ?bool {
|
||||
$a = $this->attributes[$key] ?? null;
|
||||
return is_bool($a) ? $a : null;
|
||||
}
|
||||
|
||||
/** @param non-empty-string $key */
|
||||
public function attributeInt(string $key): ?int {
|
||||
$a = $this->attributes[$key] ?? null;
|
||||
return is_int($a) ? $a : null;
|
||||
}
|
||||
|
||||
/** @param non-empty-string $key */
|
||||
public function attributeString(string $key): ?string {
|
||||
$a = $this->attributes[$key] ?? null;
|
||||
return is_string($a) ? $a : null;
|
||||
}
|
||||
|
||||
/** @param string|array<string,mixed> $values Values, not HTML-encoded */
|
||||
public function _attributes($values): void {
|
||||
if (is_string($values)) {
|
||||
$values = json_decode($values, true);
|
||||
}
|
||||
if (is_array($values)) {
|
||||
$this->attributes = $values;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param non-empty-string $key
|
||||
* @param array<string,mixed>|mixed|null $value Value, not HTML-encoded
|
||||
*/
|
||||
public function _attribute(string $key, $value = null): void {
|
||||
if ($value === null) {
|
||||
unset($this->attributes[$key]);
|
||||
} else {
|
||||
$this->attributes[$key] = $value;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,4 +1,5 @@
|
|||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* This class handles all authentication process.
|
||||
|
@ -7,25 +8,25 @@ class FreshRSS_Auth {
|
|||
/**
|
||||
* Determines if user is connected.
|
||||
*/
|
||||
const DEFAULT_COOKIE_DURATION = 7776000;
|
||||
public const DEFAULT_COOKIE_DURATION = 7776000;
|
||||
|
||||
private static $login_ok = false;
|
||||
private static bool $login_ok = false;
|
||||
|
||||
/**
|
||||
* This method initializes authentication system.
|
||||
*/
|
||||
public static function init() {
|
||||
public static function init(): bool {
|
||||
if (isset($_SESSION['REMOTE_USER']) && $_SESSION['REMOTE_USER'] !== httpAuthUser()) {
|
||||
//HTTP REMOTE_USER has changed
|
||||
self::removeAccess();
|
||||
}
|
||||
|
||||
self::$login_ok = Minz_Session::param('loginOk', false);
|
||||
$current_user = Minz_Session::param('currentUser', '');
|
||||
if ($current_user == '') {
|
||||
$current_user = FreshRSS_Context::$system_conf->default_user;
|
||||
self::$login_ok = Minz_Session::paramBoolean('loginOk');
|
||||
$current_user = Minz_User::name();
|
||||
if ($current_user === null) {
|
||||
$current_user = FreshRSS_Context::systemConf()->default_user;
|
||||
Minz_Session::_params([
|
||||
'currentUser' => $current_user,
|
||||
Minz_User::CURRENT_USER => $current_user,
|
||||
'csrf' => false,
|
||||
]);
|
||||
}
|
||||
|
@ -47,10 +48,10 @@ class FreshRSS_Auth {
|
|||
* Required session parameters are also set in this method (such as
|
||||
* currentUser).
|
||||
*
|
||||
* @return boolean true if user can be connected, false else.
|
||||
* @return bool true if user can be connected, false otherwise.
|
||||
*/
|
||||
private static function accessControl() {
|
||||
$auth_type = FreshRSS_Context::$system_conf->auth_type;
|
||||
private static function accessControl(): bool {
|
||||
$auth_type = FreshRSS_Context::systemConf()->auth_type;
|
||||
switch ($auth_type) {
|
||||
case 'form':
|
||||
$credentials = FreshRSS_FormAuth::getCredentialsFromCookie();
|
||||
|
@ -58,7 +59,7 @@ class FreshRSS_Auth {
|
|||
if (isset($credentials[1])) {
|
||||
$current_user = trim($credentials[0]);
|
||||
Minz_Session::_params([
|
||||
'currentUser' => $current_user,
|
||||
Minz_User::CURRENT_USER => $current_user,
|
||||
'passwordHash' => trim($credentials[1]),
|
||||
'csrf' => false,
|
||||
]);
|
||||
|
@ -70,13 +71,13 @@ class FreshRSS_Auth {
|
|||
return false;
|
||||
}
|
||||
$login_ok = FreshRSS_UserDAO::exists($current_user);
|
||||
if (!$login_ok && FreshRSS_Context::$system_conf->http_auth_auto_register) {
|
||||
if (!$login_ok && FreshRSS_Context::systemConf()->http_auth_auto_register) {
|
||||
$email = null;
|
||||
if (FreshRSS_Context::$system_conf->http_auth_auto_register_email_field !== '' &&
|
||||
isset($_SERVER[FreshRSS_Context::$system_conf->http_auth_auto_register_email_field])) {
|
||||
$email = $_SERVER[FreshRSS_Context::$system_conf->http_auth_auto_register_email_field];
|
||||
if (FreshRSS_Context::systemConf()->http_auth_auto_register_email_field !== '' &&
|
||||
isset($_SERVER[FreshRSS_Context::systemConf()->http_auth_auto_register_email_field])) {
|
||||
$email = (string)$_SERVER[FreshRSS_Context::systemConf()->http_auth_auto_register_email_field];
|
||||
}
|
||||
$language = Minz_Translate::getLanguage(null, Minz_Request::getPreferredLanguages(), FreshRSS_Context::$system_conf->language);
|
||||
$language = Minz_Translate::getLanguage(null, Minz_Request::getPreferredLanguages(), FreshRSS_Context::systemConf()->language);
|
||||
Minz_Translate::init($language);
|
||||
$login_ok = FreshRSS_user_Controller::createUser($current_user, $email, '', [
|
||||
'language' => $language,
|
||||
|
@ -84,7 +85,7 @@ class FreshRSS_Auth {
|
|||
}
|
||||
if ($login_ok) {
|
||||
Minz_Session::_params([
|
||||
'currentUser' => $current_user,
|
||||
Minz_User::CURRENT_USER => $current_user,
|
||||
'csrf' => false,
|
||||
]);
|
||||
}
|
||||
|
@ -100,19 +101,19 @@ class FreshRSS_Auth {
|
|||
/**
|
||||
* Gives access to the current user.
|
||||
*/
|
||||
public static function giveAccess() {
|
||||
public static function giveAccess(): bool {
|
||||
FreshRSS_Context::initUser();
|
||||
if (FreshRSS_Context::$user_conf == null) {
|
||||
if (!FreshRSS_Context::hasUserConf()) {
|
||||
self::$login_ok = false;
|
||||
return false;
|
||||
}
|
||||
|
||||
switch (FreshRSS_Context::$system_conf->auth_type) {
|
||||
switch (FreshRSS_Context::systemConf()->auth_type) {
|
||||
case 'form':
|
||||
self::$login_ok = Minz_Session::param('passwordHash') === FreshRSS_Context::$user_conf->passwordHash;
|
||||
self::$login_ok = Minz_Session::paramString('passwordHash') === FreshRSS_Context::userConf()->passwordHash;
|
||||
break;
|
||||
case 'http_auth':
|
||||
$current_user = Minz_Session::param('currentUser');
|
||||
$current_user = Minz_User::name() ?? '';
|
||||
self::$login_ok = strcasecmp($current_user, httpAuthUser()) === 0;
|
||||
break;
|
||||
case 'none':
|
||||
|
@ -134,15 +135,15 @@ class FreshRSS_Auth {
|
|||
* Returns if current user has access to the given scope.
|
||||
*
|
||||
* @param string $scope general (default) or admin
|
||||
* @return boolean true if user has corresponding access, false else.
|
||||
* @return bool true if user has corresponding access, false else.
|
||||
*/
|
||||
public static function hasAccess($scope = 'general') {
|
||||
if (FreshRSS_Context::$user_conf == null) {
|
||||
public static function hasAccess(string $scope = 'general'): bool {
|
||||
if (!FreshRSS_Context::hasUserConf()) {
|
||||
return false;
|
||||
}
|
||||
$currentUser = Minz_Session::param('currentUser');
|
||||
$isAdmin = FreshRSS_Context::$user_conf->is_admin;
|
||||
$default_user = FreshRSS_Context::$system_conf->default_user;
|
||||
$currentUser = Minz_User::name();
|
||||
$isAdmin = FreshRSS_Context::userConf()->is_admin;
|
||||
$default_user = FreshRSS_Context::systemConf()->default_user;
|
||||
$ok = self::$login_ok;
|
||||
switch ($scope) {
|
||||
case 'general':
|
||||
|
@ -153,13 +154,13 @@ class FreshRSS_Auth {
|
|||
default:
|
||||
$ok = false;
|
||||
}
|
||||
return $ok;
|
||||
return (bool)$ok;
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes all accesses for the current user.
|
||||
*/
|
||||
public static function removeAccess() {
|
||||
public static function removeAccess(): void {
|
||||
self::$login_ok = false;
|
||||
Minz_Session::_params([
|
||||
'loginOk' => false,
|
||||
|
@ -168,9 +169,9 @@ class FreshRSS_Auth {
|
|||
]);
|
||||
|
||||
$username = '';
|
||||
$token_param = Minz_Request::param('token', '');
|
||||
$token_param = Minz_Request::paramString('token');
|
||||
if ($token_param != '') {
|
||||
$username = trim(Minz_Request::param('user', ''));
|
||||
$username = Minz_Request::paramString('user');
|
||||
if ($username != '') {
|
||||
$conf = get_user_configuration($username);
|
||||
if ($conf == null) {
|
||||
|
@ -179,18 +180,18 @@ class FreshRSS_Auth {
|
|||
}
|
||||
}
|
||||
if ($username == '') {
|
||||
$username = FreshRSS_Context::$system_conf->default_user;
|
||||
$username = FreshRSS_Context::systemConf()->default_user;
|
||||
}
|
||||
Minz_Session::_param('currentUser', $username);
|
||||
Minz_User::change($username);
|
||||
|
||||
switch (FreshRSS_Context::$system_conf->auth_type) {
|
||||
switch (FreshRSS_Context::systemConf()->auth_type) {
|
||||
case 'form':
|
||||
Minz_Session::_param('passwordHash');
|
||||
FreshRSS_FormAuth::deleteCookie();
|
||||
break;
|
||||
case 'http_auth':
|
||||
case 'none':
|
||||
// Nothing to do...
|
||||
// Nothing to do…
|
||||
break;
|
||||
default:
|
||||
// TODO: extensions
|
||||
|
@ -200,28 +201,29 @@ class FreshRSS_Auth {
|
|||
/**
|
||||
* Return if authentication is enabled on this instance of FRSS.
|
||||
*/
|
||||
public static function accessNeedsLogin() {
|
||||
return FreshRSS_Context::$system_conf->auth_type !== 'none';
|
||||
public static function accessNeedsLogin(): bool {
|
||||
return FreshRSS_Context::systemConf()->auth_type !== 'none';
|
||||
}
|
||||
|
||||
/**
|
||||
* Return if authentication requires a PHP action.
|
||||
*/
|
||||
public static function accessNeedsAction() {
|
||||
return FreshRSS_Context::$system_conf->auth_type === 'form';
|
||||
public static function accessNeedsAction(): bool {
|
||||
return FreshRSS_Context::systemConf()->auth_type === 'form';
|
||||
}
|
||||
|
||||
public static function csrfToken() {
|
||||
$csrf = Minz_Session::param('csrf');
|
||||
public static function csrfToken(): string {
|
||||
$csrf = Minz_Session::paramString('csrf');
|
||||
if ($csrf == '') {
|
||||
$salt = FreshRSS_Context::$system_conf->salt;
|
||||
$csrf = sha1($salt . uniqid(mt_rand(), true));
|
||||
$salt = FreshRSS_Context::systemConf()->salt;
|
||||
$csrf = sha1($salt . uniqid('' . random_int(0, mt_getrandmax()), true));
|
||||
Minz_Session::_param('csrf', $csrf);
|
||||
}
|
||||
return $csrf;
|
||||
}
|
||||
public static function isCsrfOk($token = null) {
|
||||
$csrf = Minz_Session::param('csrf');
|
||||
|
||||
public static function isCsrfOk(?string $token = null): bool {
|
||||
$csrf = Minz_Session::paramString('csrf');
|
||||
if ($token === null) {
|
||||
$token = $_POST['_csrf'] ?? '';
|
||||
}
|
||||
|
|
|
@ -1,22 +1,254 @@
|
|||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* Contains Boolean search from the search form.
|
||||
*/
|
||||
class FreshRSS_BooleanSearch {
|
||||
|
||||
private $raw_input = '';
|
||||
private $searches = array();
|
||||
private string $raw_input = '';
|
||||
/** @var array<FreshRSS_BooleanSearch|FreshRSS_Search> */
|
||||
private array $searches = [];
|
||||
|
||||
public function __construct($input) {
|
||||
/**
|
||||
* @phpstan-var 'AND'|'OR'|'AND NOT'
|
||||
*/
|
||||
private string $operator;
|
||||
|
||||
/** @param 'AND'|'OR'|'AND NOT' $operator */
|
||||
public function __construct(string $input, int $level = 0, string $operator = 'AND', bool $allowUserQueries = true) {
|
||||
$this->operator = $operator;
|
||||
$input = trim($input);
|
||||
if ($input == '') {
|
||||
if ($input === '') {
|
||||
return;
|
||||
}
|
||||
if ($level === 0) {
|
||||
$input = preg_replace('/:"(.*?)"/', ':"\1"', $input);
|
||||
if (!is_string($input)) {
|
||||
return;
|
||||
}
|
||||
$input = preg_replace('/(?<=[\s!-]|^)"(.*?)"/', '"\1"', $input);
|
||||
if (!is_string($input)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$input = $this->parseUserQueryNames($input, $allowUserQueries);
|
||||
$input = $this->parseUserQueryIds($input, $allowUserQueries);
|
||||
$input = trim($input);
|
||||
}
|
||||
$this->raw_input = $input;
|
||||
|
||||
$input = preg_replace('/:"(.*?)"/', ':"\1"', $input);
|
||||
$splits = preg_split('/\b(OR)\b/i', $input, -1, PREG_SPLIT_DELIM_CAPTURE);
|
||||
// Either parse everything as a series of BooleanSearch’s combined by implicit AND
|
||||
// or parse everything as a series of Search’s combined by explicit OR
|
||||
$this->parseParentheses($input, $level) || $this->parseOrSegments($input);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse the user queries (saved searches) by name and expand them in the input string.
|
||||
*/
|
||||
private function parseUserQueryNames(string $input, bool $allowUserQueries = true): string {
|
||||
$all_matches = [];
|
||||
if (preg_match_all('/\bsearch:(?P<delim>[\'"])(?P<search>.*)(?P=delim)/U', $input, $matchesFound)) {
|
||||
$all_matches[] = $matchesFound;
|
||||
}
|
||||
if (preg_match_all('/\bsearch:(?P<search>[^\s"]*)/', $input, $matchesFound)) {
|
||||
$all_matches[] = $matchesFound;
|
||||
}
|
||||
|
||||
if (!empty($all_matches)) {
|
||||
/** @var array<string,FreshRSS_UserQuery> */
|
||||
$queries = [];
|
||||
foreach (FreshRSS_Context::userConf()->queries as $raw_query) {
|
||||
$query = new FreshRSS_UserQuery($raw_query, FreshRSS_Context::categories(), FreshRSS_Context::labels());
|
||||
$queries[$query->getName()] = $query;
|
||||
}
|
||||
|
||||
$fromS = [];
|
||||
$toS = [];
|
||||
foreach ($all_matches as $matches) {
|
||||
if (empty($matches['search'])) {
|
||||
continue;
|
||||
}
|
||||
for ($i = count($matches['search']) - 1; $i >= 0; $i--) {
|
||||
$name = trim($matches['search'][$i]);
|
||||
if (!empty($queries[$name])) {
|
||||
$fromS[] = $matches[0][$i];
|
||||
if ($allowUserQueries) {
|
||||
$toS[] = '(' . trim($queries[$name]->getSearch()->getRawInput()) . ')';
|
||||
} else {
|
||||
$toS[] = '';
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$input = str_replace($fromS, $toS, $input);
|
||||
}
|
||||
return $input;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse the user queries (saved searches) by ID and expand them in the input string.
|
||||
*/
|
||||
private function parseUserQueryIds(string $input, bool $allowUserQueries = true): string {
|
||||
$all_matches = [];
|
||||
|
||||
if (preg_match_all('/\bS:(?P<search>\d+)/', $input, $matchesFound)) {
|
||||
$all_matches[] = $matchesFound;
|
||||
}
|
||||
|
||||
if (!empty($all_matches)) {
|
||||
/** @var array<string,FreshRSS_UserQuery> */
|
||||
$queries = [];
|
||||
foreach (FreshRSS_Context::userConf()->queries as $raw_query) {
|
||||
$query = new FreshRSS_UserQuery($raw_query, FreshRSS_Context::categories(), FreshRSS_Context::labels());
|
||||
$queries[] = $query;
|
||||
}
|
||||
|
||||
$fromS = [];
|
||||
$toS = [];
|
||||
foreach ($all_matches as $matches) {
|
||||
if (empty($matches['search'])) {
|
||||
continue;
|
||||
}
|
||||
for ($i = count($matches['search']) - 1; $i >= 0; $i--) {
|
||||
// Index starting from 1
|
||||
$id = (int)(trim($matches['search'][$i])) - 1;
|
||||
if (!empty($queries[$id])) {
|
||||
$fromS[] = $matches[0][$i];
|
||||
if ($allowUserQueries) {
|
||||
$toS[] = '(' . trim($queries[$id]->getSearch()->getRawInput()) . ')';
|
||||
} else {
|
||||
$toS[] = '';
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$input = str_replace($fromS, $toS, $input);
|
||||
}
|
||||
return $input;
|
||||
}
|
||||
|
||||
/** @return bool True if some parenthesis logic took over, false otherwise */
|
||||
private function parseParentheses(string $input, int $level): bool {
|
||||
$input = trim($input);
|
||||
$length = strlen($input);
|
||||
$i = 0;
|
||||
$before = '';
|
||||
$hasParenthesis = false;
|
||||
$nextOperator = 'AND';
|
||||
while ($i < $length) {
|
||||
$c = $input[$i];
|
||||
$backslashed = $i >= 1 ? $input[$i - 1] === '\\' : false;
|
||||
|
||||
if ($c === '(' && !$backslashed) {
|
||||
$hasParenthesis = true;
|
||||
|
||||
$before = trim($before);
|
||||
if (preg_match('/[!-]$/i', $before)) {
|
||||
// Trim trailing negation
|
||||
$before = substr($before, 0, -1);
|
||||
|
||||
// The text prior to the negation is a BooleanSearch
|
||||
$searchBefore = new FreshRSS_BooleanSearch($before, $level + 1, $nextOperator);
|
||||
if (count($searchBefore->searches()) > 0) {
|
||||
$this->searches[] = $searchBefore;
|
||||
}
|
||||
$before = '';
|
||||
|
||||
// The next BooleanSearch will have to be combined with AND NOT instead of default AND
|
||||
$nextOperator = 'AND NOT';
|
||||
} elseif (preg_match('/\bOR$/i', $before)) {
|
||||
// Trim trailing OR
|
||||
$before = substr($before, 0, -2);
|
||||
|
||||
// The text prior to the OR is a BooleanSearch
|
||||
$searchBefore = new FreshRSS_BooleanSearch($before, $level + 1, $nextOperator);
|
||||
if (count($searchBefore->searches()) > 0) {
|
||||
$this->searches[] = $searchBefore;
|
||||
}
|
||||
$before = '';
|
||||
|
||||
// The next BooleanSearch will have to be combined with OR instead of default AND
|
||||
$nextOperator = 'OR';
|
||||
} elseif ($before !== '') {
|
||||
// The text prior to the opening parenthesis is a BooleanSearch
|
||||
$searchBefore = new FreshRSS_BooleanSearch($before, $level + 1, $nextOperator);
|
||||
if (count($searchBefore->searches()) > 0) {
|
||||
$this->searches[] = $searchBefore;
|
||||
}
|
||||
$before = '';
|
||||
}
|
||||
|
||||
// Search the matching closing parenthesis
|
||||
$parentheses = 1;
|
||||
$sub = '';
|
||||
$i++;
|
||||
while ($i < $length) {
|
||||
$c = $input[$i];
|
||||
$backslashed = $input[$i - 1] === '\\';
|
||||
if ($c === '(' && !$backslashed) {
|
||||
// One nested level deeper
|
||||
$parentheses++;
|
||||
$sub .= $c;
|
||||
} elseif ($c === ')' && !$backslashed) {
|
||||
$parentheses--;
|
||||
if ($parentheses === 0) {
|
||||
// Found the matching closing parenthesis
|
||||
$searchSub = new FreshRSS_BooleanSearch($sub, $level + 1, $nextOperator);
|
||||
$nextOperator = 'AND';
|
||||
if (count($searchSub->searches()) > 0) {
|
||||
$this->searches[] = $searchSub;
|
||||
}
|
||||
$sub = '';
|
||||
break;
|
||||
} else {
|
||||
$sub .= $c;
|
||||
}
|
||||
} else {
|
||||
$sub .= $c;
|
||||
}
|
||||
$i++;
|
||||
}
|
||||
// $sub = trim($sub);
|
||||
// if ($sub != '') {
|
||||
// // TODO: Consider throwing an error or warning in case of non-matching parenthesis
|
||||
// }
|
||||
// } elseif ($c === ')') {
|
||||
// // TODO: Consider throwing an error or warning in case of non-matching parenthesis
|
||||
} else {
|
||||
$before .= $c;
|
||||
}
|
||||
$i++;
|
||||
}
|
||||
if ($hasParenthesis) {
|
||||
$before = trim($before);
|
||||
if (preg_match('/^OR\b/i', $before)) {
|
||||
// The next BooleanSearch will have to be combined with OR instead of default AND
|
||||
$nextOperator = 'OR';
|
||||
// Trim leading OR
|
||||
$before = substr($before, 2);
|
||||
}
|
||||
|
||||
// The remaining text after the last parenthesis is a BooleanSearch
|
||||
$searchBefore = new FreshRSS_BooleanSearch($before, $level + 1, $nextOperator);
|
||||
$nextOperator = 'AND';
|
||||
if (count($searchBefore->searches()) > 0) {
|
||||
$this->searches[] = $searchBefore;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
// There was no parenthesis logic to apply
|
||||
return false;
|
||||
}
|
||||
|
||||
private function parseOrSegments(string $input): void {
|
||||
$input = trim($input);
|
||||
if ($input === '') {
|
||||
return;
|
||||
}
|
||||
$splits = preg_split('/\b(OR)\b/i', $input, -1, PREG_SPLIT_DELIM_CAPTURE) ?: [];
|
||||
|
||||
$segment = '';
|
||||
$ns = count($splits);
|
||||
|
@ -28,9 +260,7 @@ class FreshRSS_BooleanSearch {
|
|||
$quotes = substr_count($segment, '"') + substr_count($segment, '"');
|
||||
if ($quotes % 2 === 0) {
|
||||
$segment = trim($segment);
|
||||
if ($segment != '') {
|
||||
$this->searches[] = new FreshRSS_Search($segment);
|
||||
}
|
||||
$this->searches[] = new FreshRSS_Search($segment);
|
||||
$segment = '';
|
||||
}
|
||||
}
|
||||
|
@ -41,23 +271,31 @@ class FreshRSS_BooleanSearch {
|
|||
}
|
||||
}
|
||||
|
||||
public function searches() {
|
||||
/**
|
||||
* Either a list of FreshRSS_BooleanSearch combined by implicit AND
|
||||
* or a series of FreshRSS_Search combined by explicit OR
|
||||
* @return array<FreshRSS_BooleanSearch|FreshRSS_Search>
|
||||
*/
|
||||
public function searches(): array {
|
||||
return $this->searches;
|
||||
}
|
||||
|
||||
public function add($search) {
|
||||
if ($search instanceof FreshRSS_Search) {
|
||||
$this->searches[] = $search;
|
||||
return $search;
|
||||
}
|
||||
return null;
|
||||
/** @return 'AND'|'OR'|'AND NOT' depending on how this BooleanSearch should be combined */
|
||||
public function operator(): string {
|
||||
return $this->operator;
|
||||
}
|
||||
|
||||
public function __toString() {
|
||||
/** @param FreshRSS_BooleanSearch|FreshRSS_Search $search */
|
||||
public function add($search): void {
|
||||
$this->searches[] = $search;
|
||||
}
|
||||
|
||||
#[\Override]
|
||||
public function __toString(): string {
|
||||
return $this->getRawInput();
|
||||
}
|
||||
|
||||
public function getRawInput() {
|
||||
public function getRawInput(): string {
|
||||
return $this->raw_input;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,47 +1,91 @@
|
|||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
class FreshRSS_Category extends Minz_Model {
|
||||
private $id = 0;
|
||||
private $name;
|
||||
private $nbFeed = -1;
|
||||
private $nbNotRead = -1;
|
||||
private $feeds = null;
|
||||
private $hasFeedsWithError = false;
|
||||
private $isDefault = false;
|
||||
private $attributes = [];
|
||||
use FreshRSS_AttributesTrait, FreshRSS_FilterActionsTrait;
|
||||
|
||||
public function __construct($name = '', $feeds = null) {
|
||||
/**
|
||||
* Normal
|
||||
*/
|
||||
public const KIND_NORMAL = 0;
|
||||
|
||||
/**
|
||||
* Category tracking a third-party Dynamic OPML
|
||||
*/
|
||||
public const KIND_DYNAMIC_OPML = 2;
|
||||
|
||||
private int $id = 0;
|
||||
private int $kind = 0;
|
||||
private string $name;
|
||||
private int $nbFeeds = -1;
|
||||
private int $nbNotRead = -1;
|
||||
/** @var array<FreshRSS_Feed>|null */
|
||||
private ?array $feeds = null;
|
||||
/** @var bool|int */
|
||||
private $hasFeedsWithError = false;
|
||||
private int $lastUpdate = 0;
|
||||
private bool $error = false;
|
||||
|
||||
/**
|
||||
* @param array<FreshRSS_Feed>|null $feeds
|
||||
*/
|
||||
public function __construct(string $name = '', int $id = 0, ?array $feeds = null) {
|
||||
$this->_id($id);
|
||||
$this->_name($name);
|
||||
if (isset($feeds)) {
|
||||
if ($feeds !== null) {
|
||||
$this->_feeds($feeds);
|
||||
$this->nbFeed = 0;
|
||||
$this->nbFeeds = 0;
|
||||
$this->nbNotRead = 0;
|
||||
foreach ($feeds as $feed) {
|
||||
$this->nbFeed++;
|
||||
$feed->_category($this);
|
||||
$this->nbFeeds++;
|
||||
$this->nbNotRead += $feed->nbNotRead();
|
||||
$this->hasFeedsWithError |= $feed->inError();
|
||||
$this->hasFeedsWithError |= ($feed->inError() && !$feed->mute());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function id() {
|
||||
public function id(): int {
|
||||
return $this->id;
|
||||
}
|
||||
public function name() {
|
||||
public function kind(): int {
|
||||
return $this->kind;
|
||||
}
|
||||
/** @return string HTML-encoded name of the category */
|
||||
public function name(): string {
|
||||
return $this->name;
|
||||
}
|
||||
public function isDefault() {
|
||||
return $this->isDefault;
|
||||
public function lastUpdate(): int {
|
||||
return $this->lastUpdate;
|
||||
}
|
||||
public function nbFeed() {
|
||||
if ($this->nbFeed < 0) {
|
||||
public function _lastUpdate(int $value): void {
|
||||
$this->lastUpdate = $value;
|
||||
}
|
||||
public function inError(): bool {
|
||||
return $this->error;
|
||||
}
|
||||
|
||||
/** @param bool|int $value */
|
||||
public function _error($value): void {
|
||||
$this->error = (bool)$value;
|
||||
}
|
||||
public function isDefault(): bool {
|
||||
return $this->id == FreshRSS_CategoryDAO::DEFAULTCATEGORYID;
|
||||
}
|
||||
public function nbFeeds(): int {
|
||||
if ($this->nbFeeds < 0) {
|
||||
$catDAO = FreshRSS_Factory::createCategoryDao();
|
||||
$this->nbFeed = $catDAO->countFeed($this->id());
|
||||
$this->nbFeeds = $catDAO->countFeed($this->id());
|
||||
}
|
||||
|
||||
return $this->nbFeed;
|
||||
return $this->nbFeeds;
|
||||
}
|
||||
public function nbNotRead() {
|
||||
|
||||
/**
|
||||
* @throws Minz_ConfigurationNamespaceException
|
||||
* @throws Minz_PDOConnectionException
|
||||
*/
|
||||
public function nbNotRead(): int {
|
||||
if ($this->nbNotRead < 0) {
|
||||
$catDAO = FreshRSS_Factory::createCategoryDao();
|
||||
$this->nbNotRead = $catDAO->countNotRead($this->id());
|
||||
|
@ -49,70 +93,204 @@ class FreshRSS_Category extends Minz_Model {
|
|||
|
||||
return $this->nbNotRead;
|
||||
}
|
||||
public function feeds() {
|
||||
|
||||
/** @return array<int,mixed> */
|
||||
public function curlOptions(): array {
|
||||
return []; // TODO (e.g., credentials for Dynamic OPML)
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<int,FreshRSS_Feed>
|
||||
* @throws Minz_ConfigurationNamespaceException
|
||||
* @throws Minz_PDOConnectionException
|
||||
*/
|
||||
public function feeds(): array {
|
||||
if ($this->feeds === null) {
|
||||
$feedDAO = FreshRSS_Factory::createFeedDao();
|
||||
$this->feeds = $feedDAO->listByCategory($this->id());
|
||||
$this->nbFeed = 0;
|
||||
$this->nbFeeds = 0;
|
||||
$this->nbNotRead = 0;
|
||||
foreach ($this->feeds as $feed) {
|
||||
$this->nbFeed++;
|
||||
$this->nbFeeds++;
|
||||
$this->nbNotRead += $feed->nbNotRead();
|
||||
$this->hasFeedsWithError |= $feed->inError();
|
||||
$this->hasFeedsWithError |= ($feed->inError() && !$feed->mute());
|
||||
}
|
||||
$this->sortFeeds();
|
||||
}
|
||||
return $this->feeds ?? [];
|
||||
}
|
||||
|
||||
public function hasFeedsWithError(): bool {
|
||||
return (bool)($this->hasFeedsWithError);
|
||||
}
|
||||
|
||||
public function _id(int $id): void {
|
||||
$this->id = $id;
|
||||
if ($id === FreshRSS_CategoryDAO::DEFAULTCATEGORYID) {
|
||||
$this->name = _t('gen.short.default_category');
|
||||
}
|
||||
}
|
||||
|
||||
public function _kind(int $kind): void {
|
||||
$this->kind = $kind;
|
||||
}
|
||||
|
||||
public function _name(string $value): void {
|
||||
if ($this->id !== FreshRSS_CategoryDAO::DEFAULTCATEGORYID) {
|
||||
$this->name = mb_strcut(trim($value), 0, FreshRSS_DatabaseDAO::LENGTH_INDEX_UNICODE, 'UTF-8');
|
||||
}
|
||||
}
|
||||
|
||||
/** @param array<FreshRSS_Feed>|FreshRSS_Feed $values */
|
||||
public function _feeds($values): void {
|
||||
if (!is_array($values)) {
|
||||
$values = [$values];
|
||||
}
|
||||
$this->feeds = $values;
|
||||
$this->sortFeeds();
|
||||
}
|
||||
|
||||
/**
|
||||
* To manually add feeds to this category (not committing to database).
|
||||
*/
|
||||
public function addFeed(FreshRSS_Feed $feed): void {
|
||||
if ($this->feeds === null) {
|
||||
$this->feeds = [];
|
||||
}
|
||||
$feed->_category($this);
|
||||
$this->feeds[] = $feed;
|
||||
$this->sortFeeds();
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws FreshRSS_Context_Exception
|
||||
*/
|
||||
public function cacheFilename(string $url): string {
|
||||
$simplePie = customSimplePie($this->attributes(), $this->curlOptions());
|
||||
$filename = $simplePie->get_cache_filename($url);
|
||||
return CACHE_PATH . '/' . $filename . '.opml.xml';
|
||||
}
|
||||
|
||||
public function refreshDynamicOpml(): bool {
|
||||
$url = $this->attributeString('opml_url');
|
||||
if ($url == null) {
|
||||
return false;
|
||||
}
|
||||
$ok = true;
|
||||
$cachePath = $this->cacheFilename($url);
|
||||
$opml = httpGet($url, $cachePath, 'opml', $this->attributes(), $this->curlOptions());
|
||||
if ($opml == '') {
|
||||
Minz_Log::warning('Error getting dynamic OPML for category ' . $this->id() . '! ' .
|
||||
SimplePie_Misc::url_remove_credentials($url));
|
||||
$ok = false;
|
||||
} else {
|
||||
$dryRunCategory = new FreshRSS_Category();
|
||||
$importService = new FreshRSS_Import_Service();
|
||||
$importService->importOpml($opml, $dryRunCategory, true);
|
||||
if ($importService->lastStatus()) {
|
||||
$feedDAO = FreshRSS_Factory::createFeedDao();
|
||||
|
||||
/** @var array<string,FreshRSS_Feed> */
|
||||
$dryRunFeeds = [];
|
||||
foreach ($dryRunCategory->feeds() as $dryRunFeed) {
|
||||
$dryRunFeeds[$dryRunFeed->url()] = $dryRunFeed;
|
||||
}
|
||||
|
||||
/** @var array<string,FreshRSS_Feed> */
|
||||
$existingFeeds = [];
|
||||
foreach ($this->feeds() as $existingFeed) {
|
||||
$existingFeeds[$existingFeed->url()] = $existingFeed;
|
||||
if (empty($dryRunFeeds[$existingFeed->url()])) {
|
||||
// The feed does not exist in the new dynamic OPML, so mute (disable) that feed
|
||||
$existingFeed->_mute(true);
|
||||
$ok &= ($feedDAO->updateFeed($existingFeed->id(), [
|
||||
'ttl' => $existingFeed->ttl(true),
|
||||
]) !== false);
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($dryRunCategory->feeds() as $dryRunFeed) {
|
||||
if (empty($existingFeeds[$dryRunFeed->url()])) {
|
||||
// The feed does not exist in the current category, so add that feed
|
||||
$dryRunFeed->_category($this);
|
||||
$ok &= ($feedDAO->addFeedObject($dryRunFeed) !== false);
|
||||
$existingFeeds[$dryRunFeed->url()] = $dryRunFeed;
|
||||
} else {
|
||||
$existingFeed = $existingFeeds[$dryRunFeed->url()];
|
||||
if ($existingFeed->mute()) {
|
||||
// The feed already exists in the current category but was muted (disabled), so unmute (enable) again
|
||||
$existingFeed->_mute(false);
|
||||
$ok &= ($feedDAO->updateFeed($existingFeed->id(), [
|
||||
'ttl' => $existingFeed->ttl(true),
|
||||
]) !== false);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
$ok = false;
|
||||
Minz_Log::warning('Error loading dynamic OPML for category ' . $this->id() . '! ' .
|
||||
SimplePie_Misc::url_remove_credentials($url));
|
||||
}
|
||||
}
|
||||
|
||||
usort($this->feeds, function ($a, $b) {
|
||||
$catDAO = FreshRSS_Factory::createCategoryDao();
|
||||
$catDAO->updateLastUpdate($this->id(), !$ok);
|
||||
|
||||
return (bool)$ok;
|
||||
}
|
||||
|
||||
private function sortFeeds(): void {
|
||||
if ($this->feeds === null) {
|
||||
return;
|
||||
}
|
||||
uasort($this->feeds, static function (FreshRSS_Feed $a, FreshRSS_Feed $b) {
|
||||
return strnatcasecmp($a->name(), $b->name());
|
||||
});
|
||||
|
||||
return $this->feeds;
|
||||
}
|
||||
|
||||
public function hasFeedsWithError() {
|
||||
return $this->hasFeedsWithError;
|
||||
}
|
||||
|
||||
public function attributes($key = '') {
|
||||
if ($key == '') {
|
||||
return $this->attributes;
|
||||
} else {
|
||||
return isset($this->attributes[$key]) ? $this->attributes[$key] : null;
|
||||
}
|
||||
}
|
||||
|
||||
public function _id($id) {
|
||||
$this->id = intval($id);
|
||||
if ($id == FreshRSS_CategoryDAO::DEFAULTCATEGORYID) {
|
||||
$this->_name(_t('gen.short.default_category'));
|
||||
}
|
||||
}
|
||||
public function _name($value) {
|
||||
$this->name = mb_strcut(trim($value), 0, 255, 'UTF-8');
|
||||
}
|
||||
public function _isDefault($value) {
|
||||
$this->isDefault = $value;
|
||||
}
|
||||
public function _feeds($values) {
|
||||
if (!is_array($values)) {
|
||||
$values = array($values);
|
||||
}
|
||||
|
||||
$this->feeds = $values;
|
||||
}
|
||||
|
||||
public function _attributes($key, $value) {
|
||||
if ('' == $key) {
|
||||
if (is_string($value)) {
|
||||
$value = json_decode($value, true);
|
||||
/**
|
||||
* Access cached feed
|
||||
* @param array<FreshRSS_Category> $categories
|
||||
*/
|
||||
public static function findFeed(array $categories, int $feed_id): ?FreshRSS_Feed {
|
||||
foreach ($categories as $category) {
|
||||
foreach ($category->feeds() as $feed) {
|
||||
if ($feed->id() === $feed_id) {
|
||||
$feed->_category($category); // Should already be done; just to be safe
|
||||
return $feed;
|
||||
}
|
||||
}
|
||||
if (is_array($value)) {
|
||||
$this->attributes = $value;
|
||||
}
|
||||
} elseif (null === $value) {
|
||||
unset($this->attributes[$key]);
|
||||
} else {
|
||||
$this->attributes[$key] = $value;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Access cached feeds
|
||||
* @param array<FreshRSS_Category> $categories
|
||||
* @return array<int,FreshRSS_Feed>
|
||||
*/
|
||||
public static function findFeeds(array $categories): array {
|
||||
$result = [];
|
||||
foreach ($categories as $category) {
|
||||
foreach ($category->feeds() as $feed) {
|
||||
$result[$feed->id()] = $feed;
|
||||
}
|
||||
}
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<FreshRSS_Category> $categories
|
||||
*/
|
||||
public static function countUnread(array $categories, int $minPriority = 0): int {
|
||||
$n = 0;
|
||||
foreach ($categories as $category) {
|
||||
foreach ($category->feeds() as $feed) {
|
||||
if ($feed->priority() >= $minPriority) {
|
||||
$n += $feed->nbNotRead();
|
||||
}
|
||||
}
|
||||
}
|
||||
return $n;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,29 +1,44 @@
|
|||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
class FreshRSS_CategoryDAO extends Minz_ModelPdo implements FreshRSS_Searchable {
|
||||
class FreshRSS_CategoryDAO extends Minz_ModelPdo {
|
||||
|
||||
const DEFAULTCATEGORYID = 1;
|
||||
public const DEFAULTCATEGORYID = 1;
|
||||
|
||||
public function resetDefaultCategoryName() {
|
||||
public function resetDefaultCategoryName(): bool {
|
||||
//FreshRSS 1.15.1
|
||||
$stm = $this->pdo->prepare('UPDATE `_category` SET name = :name WHERE id = :id');
|
||||
if ($stm) {
|
||||
if ($stm !== false) {
|
||||
$stm->bindValue(':id', self::DEFAULTCATEGORYID, PDO::PARAM_INT);
|
||||
$stm->bindValue(':name', 'Uncategorized');
|
||||
}
|
||||
return $stm && $stm->execute();
|
||||
}
|
||||
|
||||
protected function addColumn($name) {
|
||||
protected function addColumn(string $name): bool {
|
||||
if ($this->pdo->inTransaction()) {
|
||||
$this->pdo->commit();
|
||||
}
|
||||
Minz_Log::warning(__method__ . ': ' . $name);
|
||||
try {
|
||||
if ('attributes' === $name) { //v1.15.0
|
||||
if ($name === 'kind') { //v1.20.0
|
||||
return $this->pdo->exec('ALTER TABLE `_category` ADD COLUMN kind SMALLINT DEFAULT 0') !== false;
|
||||
} elseif ($name === 'lastUpdate') { //v1.20.0
|
||||
return $this->pdo->exec('ALTER TABLE `_category` ADD COLUMN `lastUpdate` BIGINT DEFAULT 0') !== false;
|
||||
} elseif ($name === 'error') { //v1.20.0
|
||||
return $this->pdo->exec('ALTER TABLE `_category` ADD COLUMN error SMALLINT DEFAULT 0') !== false;
|
||||
} elseif ('attributes' === $name) { //v1.15.0
|
||||
$ok = $this->pdo->exec('ALTER TABLE `_category` ADD COLUMN attributes TEXT') !== false;
|
||||
|
||||
$stm = $this->pdo->query('SELECT * FROM `_feed`');
|
||||
$feeds = $stm->fetchAll(PDO::FETCH_ASSOC);
|
||||
/** @var array<array{'id':int,'url':string,'kind':int,'category':int,'name':string,'website':string,'lastUpdate':int,
|
||||
* 'priority':int,'pathEntries':string,'httpAuth':string,'error':int,'keep_history':?int,'ttl':int,'attributes':string}> $feeds */
|
||||
$feeds = $this->fetchAssoc('SELECT * FROM `_feed`') ?? [];
|
||||
|
||||
$stm = $this->pdo->prepare('UPDATE `_feed` SET attributes = :attributes WHERE id = :id');
|
||||
if ($stm === false) {
|
||||
Minz_Log::error('SQL error ' . __METHOD__ . json_encode($this->pdo->errorInfo()));
|
||||
return false;
|
||||
}
|
||||
foreach ($feeds as $feed) {
|
||||
if (empty($feed['keep_history']) || empty($feed['id'])) {
|
||||
continue;
|
||||
|
@ -37,7 +52,7 @@ class FreshRSS_CategoryDAO extends Minz_ModelPdo implements FreshRSS_Searchable
|
|||
$attributes = [];
|
||||
}
|
||||
if ($keepHistory > 0) {
|
||||
$attributes['archiving']['keep_min'] = intval($keepHistory);
|
||||
$attributes['archiving']['keep_min'] = (int)$keepHistory;
|
||||
} elseif ($keepHistory == -1) { //Infinite
|
||||
$attributes['archiving']['keep_period'] = false;
|
||||
$attributes['archiving']['keep_max'] = false;
|
||||
|
@ -45,9 +60,11 @@ class FreshRSS_CategoryDAO extends Minz_ModelPdo implements FreshRSS_Searchable
|
|||
} else {
|
||||
continue;
|
||||
}
|
||||
$stm->bindValue(':id', $feed['id'], PDO::PARAM_INT);
|
||||
$stm->bindValue(':attributes', json_encode($attributes, JSON_UNESCAPED_SLASHES));
|
||||
$stm->execute();
|
||||
if (!($stm->bindValue(':id', $feed['id'], PDO::PARAM_INT) &&
|
||||
$stm->bindValue(':attributes', json_encode($attributes, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE)) &&
|
||||
$stm->execute())) {
|
||||
Minz_Log::error('SQL error ' . __METHOD__ . json_encode($stm->errorInfo()));
|
||||
}
|
||||
}
|
||||
|
||||
if ($this->pdo->dbType() !== 'sqlite') { //SQLite does not support DROP COLUMN
|
||||
|
@ -66,11 +83,13 @@ class FreshRSS_CategoryDAO extends Minz_ModelPdo implements FreshRSS_Searchable
|
|||
return false;
|
||||
}
|
||||
|
||||
protected function autoUpdateDb($errorInfo) {
|
||||
/** @param array<string|int> $errorInfo */
|
||||
protected function autoUpdateDb(array $errorInfo): bool {
|
||||
if (isset($errorInfo[0])) {
|
||||
if ($errorInfo[0] === FreshRSS_DatabaseDAO::ER_BAD_FIELD_ERROR || $errorInfo[0] === FreshRSS_DatabaseDAOPGSQL::UNDEFINED_COLUMN) {
|
||||
foreach (['attributes'] as $column) {
|
||||
if (stripos($errorInfo[2], $column) !== false) {
|
||||
$errorLines = explode("\n", (string)$errorInfo[2], 2); // The relevant column name is on the first line, other lines are noise
|
||||
foreach (['kind', 'lastUpdate', 'error', 'attributes'] as $column) {
|
||||
if (stripos($errorLines[0], $column) !== false) {
|
||||
return $this->addColumn($column);
|
||||
}
|
||||
}
|
||||
|
@ -79,12 +98,16 @@ class FreshRSS_CategoryDAO extends Minz_ModelPdo implements FreshRSS_Searchable
|
|||
return false;
|
||||
}
|
||||
|
||||
public function addCategory($valuesTmp) {
|
||||
/**
|
||||
* @param array{'name':string,'id'?:int,'kind'?:int,'lastUpdate'?:int,'error'?:int|bool,'attributes'?:string|array<string,mixed>} $valuesTmp
|
||||
* @return int|false
|
||||
*/
|
||||
public function addCategory(array $valuesTmp) {
|
||||
// TRIM() to provide a type hint as text
|
||||
// No tag of the same name
|
||||
$sql = <<<'SQL'
|
||||
INSERT INTO `_category`(name, attributes)
|
||||
SELECT * FROM (SELECT TRIM(?) AS name, TRIM(?) AS attributes) c2
|
||||
INSERT INTO `_category`(kind, name, attributes)
|
||||
SELECT * FROM (SELECT ABS(?) AS kind, TRIM(?) AS name, TRIM(?) AS attributes) c2
|
||||
WHERE NOT EXISTS (SELECT 1 FROM `_tag` WHERE name = TRIM(?))
|
||||
SQL;
|
||||
$stm = $this->pdo->prepare($sql);
|
||||
|
@ -93,144 +116,154 @@ SQL;
|
|||
if (!isset($valuesTmp['attributes'])) {
|
||||
$valuesTmp['attributes'] = [];
|
||||
}
|
||||
$values = array(
|
||||
$values = [
|
||||
$valuesTmp['kind'] ?? FreshRSS_Category::KIND_NORMAL,
|
||||
$valuesTmp['name'],
|
||||
is_string($valuesTmp['attributes']) ? $valuesTmp['attributes'] : json_encode($valuesTmp['attributes'], JSON_UNESCAPED_SLASHES),
|
||||
is_string($valuesTmp['attributes']) ? $valuesTmp['attributes'] : json_encode($valuesTmp['attributes'], JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE),
|
||||
$valuesTmp['name'],
|
||||
);
|
||||
];
|
||||
|
||||
if ($stm && $stm->execute($values)) {
|
||||
return $this->pdo->lastInsertId('`_category_id_seq`');
|
||||
if ($stm !== false && $stm->execute($values) && $stm->rowCount() > 0) {
|
||||
$catId = $this->pdo->lastInsertId('`_category_id_seq`');
|
||||
return $catId === false ? false : (int)$catId;
|
||||
} else {
|
||||
$info = $stm == null ? $this->pdo->errorInfo() : $stm->errorInfo();
|
||||
if ($this->autoUpdateDb($info)) {
|
||||
return $this->addCategory($valuesTmp);
|
||||
}
|
||||
Minz_Log::error('SQL error addCategory: ' . json_encode($info));
|
||||
Minz_Log::error('SQL error ' . __METHOD__ . json_encode($info));
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public function addCategoryObject($category) {
|
||||
/** @return int|false */
|
||||
public function addCategoryObject(FreshRSS_Category $category) {
|
||||
$cat = $this->searchByName($category->name());
|
||||
if (!$cat) {
|
||||
// Category does not exist yet in DB so we add it before continue
|
||||
$values = array(
|
||||
$values = [
|
||||
'kind' => $category->kind(),
|
||||
'name' => $category->name(),
|
||||
);
|
||||
'attributes' => $category->attributes(),
|
||||
];
|
||||
return $this->addCategory($values);
|
||||
}
|
||||
|
||||
return $cat->id();
|
||||
}
|
||||
|
||||
public function updateCategory($id, $valuesTmp) {
|
||||
/**
|
||||
* @param array{'name':string,'kind':int,'attributes'?:array<string,mixed>|mixed|null} $valuesTmp
|
||||
* @return int|false
|
||||
*/
|
||||
public function updateCategory(int $id, array $valuesTmp) {
|
||||
// No tag of the same name
|
||||
$sql = <<<'SQL'
|
||||
UPDATE `_category` SET name=?, attributes=? WHERE id=?
|
||||
UPDATE `_category` SET name=?, kind=?, attributes=? WHERE id=?
|
||||
AND NOT EXISTS (SELECT 1 FROM `_tag` WHERE name = ?)
|
||||
SQL;
|
||||
$stm = $this->pdo->prepare($sql);
|
||||
|
||||
$valuesTmp['name'] = mb_strcut(trim($valuesTmp['name']), 0, FreshRSS_DatabaseDAO::LENGTH_INDEX_UNICODE, 'UTF-8');
|
||||
if (!isset($valuesTmp['attributes'])) {
|
||||
if (empty($valuesTmp['attributes'])) {
|
||||
$valuesTmp['attributes'] = [];
|
||||
}
|
||||
$values = array(
|
||||
$values = [
|
||||
$valuesTmp['name'],
|
||||
is_string($valuesTmp['attributes']) ? $valuesTmp['attributes'] : json_encode($valuesTmp['attributes'], JSON_UNESCAPED_SLASHES),
|
||||
$valuesTmp['kind'] ?? FreshRSS_Category::KIND_NORMAL,
|
||||
is_string($valuesTmp['attributes']) ? $valuesTmp['attributes'] : json_encode($valuesTmp['attributes'], JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE),
|
||||
$id,
|
||||
$valuesTmp['name'],
|
||||
);
|
||||
];
|
||||
|
||||
if ($stm && $stm->execute($values)) {
|
||||
if ($stm !== false && $stm->execute($values)) {
|
||||
return $stm->rowCount();
|
||||
} else {
|
||||
$info = $stm == null ? $this->pdo->errorInfo() : $stm->errorInfo();
|
||||
if ($this->autoUpdateDb($info)) {
|
||||
return $this->updateCategory($id, $valuesTmp);
|
||||
}
|
||||
Minz_Log::error('SQL error updateCategory: ' . json_encode($info));
|
||||
Minz_Log::error('SQL error ' . __METHOD__ . json_encode($info));
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public function deleteCategory($id) {
|
||||
/** @return int|false */
|
||||
public function updateLastUpdate(int $id, bool $inError = false, int $mtime = 0) {
|
||||
$sql = 'UPDATE `_category` SET `lastUpdate`=?, error=? WHERE id=?';
|
||||
$values = [
|
||||
$mtime <= 0 ? time() : $mtime,
|
||||
$inError ? 1 : 0,
|
||||
$id,
|
||||
];
|
||||
$stm = $this->pdo->prepare($sql);
|
||||
|
||||
if ($stm !== false && $stm->execute($values)) {
|
||||
return $stm->rowCount();
|
||||
} else {
|
||||
$info = $stm == null ? $this->pdo->errorInfo() : $stm->errorInfo();
|
||||
Minz_Log::error('SQL error ' . __METHOD__ . json_encode($info));
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/** @return int|false */
|
||||
public function deleteCategory(int $id) {
|
||||
if ($id <= self::DEFAULTCATEGORYID) {
|
||||
return false;
|
||||
}
|
||||
$sql = 'DELETE FROM `_category` WHERE id=:id';
|
||||
$stm = $this->pdo->prepare($sql);
|
||||
$stm->bindParam(':id', $id, PDO::PARAM_INT);
|
||||
if ($stm && $stm->execute()) {
|
||||
if ($stm !== false && $stm->bindParam(':id', $id, PDO::PARAM_INT) && $stm->execute()) {
|
||||
return $stm->rowCount();
|
||||
} else {
|
||||
$info = $stm == null ? $this->pdo->errorInfo() : $stm->errorInfo();
|
||||
Minz_Log::error('SQL error deleteCategory: ' . json_encode($info));
|
||||
Minz_Log::error('SQL error ' . __METHOD__ . json_encode($info));
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public function selectAll() {
|
||||
$sql = 'SELECT id, name, attributes FROM `_category`';
|
||||
/** @return Traversable<array{'id':int,'name':string,'kind':int,'lastUpdate':int,'error':int,'attributes'?:array<string,mixed>}> */
|
||||
public function selectAll(): Traversable {
|
||||
$sql = 'SELECT id, name, kind, `lastUpdate`, error, attributes FROM `_category`';
|
||||
$stm = $this->pdo->query($sql);
|
||||
if ($stm != false) {
|
||||
if ($stm !== false) {
|
||||
while ($row = $stm->fetch(PDO::FETCH_ASSOC)) {
|
||||
/** @var array{'id':int,'name':string,'kind':int,'lastUpdate':int,'error':int,'attributes'?:array<string,mixed>} $row */
|
||||
yield $row;
|
||||
}
|
||||
} else {
|
||||
$info = $this->pdo->errorInfo();
|
||||
if ($this->autoUpdateDb($info)) {
|
||||
foreach ($this->selectAll() as $category) { // `yield from` requires PHP 7+
|
||||
yield $category;
|
||||
}
|
||||
yield from $this->selectAll();
|
||||
} else {
|
||||
Minz_Log::error(__method__ . ' error: ' . json_encode($info));
|
||||
}
|
||||
Minz_Log::error(__method__ . ' error: ' . json_encode($info));
|
||||
yield false;
|
||||
}
|
||||
}
|
||||
|
||||
public function searchById($id) {
|
||||
public function searchById(int $id): ?FreshRSS_Category {
|
||||
$sql = 'SELECT * FROM `_category` WHERE id=:id';
|
||||
$stm = $this->pdo->prepare($sql);
|
||||
$stm->bindParam(':id', $id, PDO::PARAM_INT);
|
||||
$stm->execute();
|
||||
$res = $stm->fetchAll(PDO::FETCH_ASSOC);
|
||||
$cat = self::daoToCategory($res);
|
||||
|
||||
if (isset($cat[0])) {
|
||||
return $cat[0];
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
$res = $this->fetchAssoc($sql, ['id' => $id]) ?? [];
|
||||
/** @var array<array{'name':string,'id':int,'kind':int,'lastUpdate':int,'error':int|bool,'attributes':string}> $res */
|
||||
$categories = self::daoToCategories($res);
|
||||
return reset($categories) ?: null;
|
||||
}
|
||||
public function searchByName($name) {
|
||||
|
||||
public function searchByName(string $name): ?FreshRSS_Category {
|
||||
$sql = 'SELECT * FROM `_category` WHERE name=:name';
|
||||
$stm = $this->pdo->prepare($sql);
|
||||
if ($stm == false) {
|
||||
return false;
|
||||
}
|
||||
$stm->bindParam(':name', $name);
|
||||
$stm->execute();
|
||||
$res = $stm->fetchAll(PDO::FETCH_ASSOC);
|
||||
$cat = self::daoToCategory($res);
|
||||
if (isset($cat[0])) {
|
||||
return $cat[0];
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
$res = $this->fetchAssoc($sql, ['name' => $name]) ?? [];
|
||||
/** @var array<array{'name':string,'id':int,'kind':int,'lastUpdate':int,'error':int|bool,'attributes':string}> $res */
|
||||
$categories = self::daoToCategories($res);
|
||||
return reset($categories) ?: null;
|
||||
}
|
||||
|
||||
public function listSortedCategories($prePopulateFeeds = true, $details = false) {
|
||||
/** @return array<int,FreshRSS_Category> */
|
||||
public function listSortedCategories(bool $prePopulateFeeds = true, bool $details = false): array {
|
||||
$categories = $this->listCategories($prePopulateFeeds, $details);
|
||||
|
||||
if (!is_array($categories)) {
|
||||
return $categories;
|
||||
}
|
||||
|
||||
uasort($categories, function ($a, $b) {
|
||||
$aPosition = $a->attributes('position');
|
||||
$bPosition = $b->attributes('position');
|
||||
uasort($categories, static function (FreshRSS_Category $a, FreshRSS_Category $b) {
|
||||
$aPosition = $a->attributeInt('position');
|
||||
$bPosition = $b->attributeInt('position');
|
||||
if ($aPosition === $bPosition) {
|
||||
return ($a->name() < $b->name()) ? -1 : 1;
|
||||
} elseif (null === $aPosition) {
|
||||
|
@ -244,44 +277,65 @@ SQL;
|
|||
return $categories;
|
||||
}
|
||||
|
||||
public function listCategories($prePopulateFeeds = true, $details = false) {
|
||||
/** @return array<int,FreshRSS_Category> */
|
||||
public function listCategories(bool $prePopulateFeeds = true, bool $details = false): array {
|
||||
if ($prePopulateFeeds) {
|
||||
$sql = 'SELECT c.id AS c_id, c.name AS c_name, c.attributes AS c_attributes, '
|
||||
. ($details ? 'f.* ' : 'f.id, f.name, f.url, f.website, f.priority, f.error, f.`cache_nbEntries`, f.`cache_nbUnreads`, f.ttl ')
|
||||
$sql = 'SELECT c.id AS c_id, c.name AS c_name, c.kind AS c_kind, c.`lastUpdate` AS c_last_update, c.error AS c_error, c.attributes AS c_attributes, '
|
||||
. ($details ? 'f.* ' : 'f.id, f.name, f.url, f.kind, f.website, f.priority, f.error, f.attributes, f.`cache_nbEntries`, f.`cache_nbUnreads`, f.ttl ')
|
||||
. 'FROM `_category` c '
|
||||
. 'LEFT OUTER JOIN `_feed` f ON f.category=c.id '
|
||||
. 'WHERE f.priority >= :priority_normal '
|
||||
. 'WHERE f.priority >= :priority '
|
||||
. 'GROUP BY f.id, c_id '
|
||||
. 'ORDER BY c.name, f.name';
|
||||
$stm = $this->pdo->prepare($sql);
|
||||
$values = [ ':priority_normal' => FreshRSS_Feed::PRIORITY_NORMAL ];
|
||||
if ($stm && $stm->execute($values)) {
|
||||
return self::daoToCategoryPrepopulated($stm->fetchAll(PDO::FETCH_ASSOC));
|
||||
$values = [ ':priority' => FreshRSS_Feed::PRIORITY_CATEGORY ];
|
||||
if ($stm !== false && $stm->execute($values)) {
|
||||
$res = $stm->fetchAll(PDO::FETCH_ASSOC) ?: [];
|
||||
/** @var array<array{'c_name':string,'c_id':int,'c_kind':int,'c_last_update':int,'c_error':int|bool,'c_attributes'?:string,
|
||||
* 'id'?:int,'name'?:string,'url'?:string,'kind'?:int,'category'?:int,'website'?:string,'priority'?:int,'error'?:int|bool,'attributes'?:string,'cache_nbEntries'?:int,'cache_nbUnreads'?:int,'ttl'?:int}> $res */
|
||||
return self::daoToCategoriesPrepopulated($res);
|
||||
} else {
|
||||
$info = $stm == null ? $this->pdo->errorInfo() : $stm->errorInfo();
|
||||
if ($this->autoUpdateDb($info)) {
|
||||
return $this->listCategories($prePopulateFeeds, $details);
|
||||
}
|
||||
Minz_Log::error('SQL error listCategories: ' . json_encode($info));
|
||||
return false;
|
||||
Minz_Log::error('SQL error ' . __METHOD__ . json_encode($info));
|
||||
return [];
|
||||
}
|
||||
} else {
|
||||
$sql = 'SELECT * FROM `_category` ORDER BY name';
|
||||
$stm = $this->pdo->query($sql);
|
||||
return self::daoToCategory($stm->fetchAll(PDO::FETCH_ASSOC));
|
||||
$res = $this->fetchAssoc('SELECT * FROM `_category` ORDER BY name');
|
||||
/** @var array<array{'name':string,'id':int,'kind':int,'lastUpdate'?:int,'error'?:int|bool,'attributes'?:string}> $res */
|
||||
return empty($res) ? [] : self::daoToCategories($res);
|
||||
}
|
||||
}
|
||||
|
||||
public function getDefault() {
|
||||
$sql = 'SELECT * FROM `_category` WHERE id=:id';
|
||||
/** @return array<int,FreshRSS_Category> */
|
||||
public function listCategoriesOrderUpdate(int $defaultCacheDuration = 86400, int $limit = 0): array {
|
||||
$sql = 'SELECT * FROM `_category` WHERE kind = :kind AND `lastUpdate` < :lu ORDER BY `lastUpdate`'
|
||||
. ($limit < 1 ? '' : ' LIMIT ' . $limit);
|
||||
$stm = $this->pdo->prepare($sql);
|
||||
$stm->bindValue(':id', self::DEFAULTCATEGORYID, PDO::PARAM_INT);
|
||||
$stm->execute();
|
||||
$res = $stm->fetchAll(PDO::FETCH_ASSOC);
|
||||
$cat = self::daoToCategory($res);
|
||||
if ($stm !== false &&
|
||||
$stm->bindValue(':kind', FreshRSS_Category::KIND_DYNAMIC_OPML, PDO::PARAM_INT) &&
|
||||
$stm->bindValue(':lu', time() - $defaultCacheDuration, PDO::PARAM_INT) &&
|
||||
$stm->execute()) {
|
||||
return self::daoToCategories($stm->fetchAll(PDO::FETCH_ASSOC));
|
||||
} else {
|
||||
$info = $stm ? $stm->errorInfo() : $this->pdo->errorInfo();
|
||||
if ($this->autoUpdateDb($info)) {
|
||||
return $this->listCategoriesOrderUpdate($defaultCacheDuration, $limit);
|
||||
}
|
||||
Minz_Log::warning(__METHOD__ . ' error: ' . $sql . ' : ' . json_encode($info));
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
if (isset($cat[0])) {
|
||||
return $cat[0];
|
||||
public function getDefault(): ?FreshRSS_Category {
|
||||
$sql = 'SELECT * FROM `_category` WHERE id=:id';
|
||||
$res = $this->fetchAssoc($sql, [':id' => self::DEFAULTCATEGORYID]) ?? [];
|
||||
/** @var array<array{'name':string,'id':int,'kind':int,'lastUpdate'?:int,'error'?:int|bool,'attributes'?:string}> $res */
|
||||
$categories = self::daoToCategories($res);
|
||||
if (isset($categories[self::DEFAULTCATEGORYID])) {
|
||||
return $categories[self::DEFAULTCATEGORYID];
|
||||
} else {
|
||||
if (FreshRSS_Context::$isCli) {
|
||||
fwrite(STDERR, 'FreshRSS database error: Default category not found!' . "\n");
|
||||
|
@ -290,12 +344,13 @@ SQL;
|
|||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/** @return int|bool */
|
||||
public function checkDefault() {
|
||||
$def_cat = $this->searchById(self::DEFAULTCATEGORYID);
|
||||
|
||||
if ($def_cat == null) {
|
||||
$cat = new FreshRSS_Category(_t('gen.short.default_category'));
|
||||
$cat->_id(self::DEFAULTCATEGORYID);
|
||||
$cat = new FreshRSS_Category(_t('gen.short.default_category'), self::DEFAULTCATEGORYID);
|
||||
|
||||
$sql = 'INSERT INTO `_category`(id, name) VALUES(?, ?)';
|
||||
if ($this->pdo->dbType() === 'pgsql') {
|
||||
|
@ -304,92 +359,67 @@ SQL;
|
|||
}
|
||||
$stm = $this->pdo->prepare($sql);
|
||||
|
||||
$values = array(
|
||||
$values = [
|
||||
$cat->id(),
|
||||
$cat->name(),
|
||||
);
|
||||
];
|
||||
|
||||
if ($stm && $stm->execute($values)) {
|
||||
return $this->pdo->lastInsertId('`_category_id_seq`');
|
||||
if ($stm !== false && $stm->execute($values)) {
|
||||
$catId = $this->pdo->lastInsertId('`_category_id_seq`');
|
||||
return $catId === false ? false : (int)$catId;
|
||||
} else {
|
||||
$info = $stm == null ? $this->pdo->errorInfo() : $stm->errorInfo();
|
||||
Minz_Log::error('SQL error check default category: ' . json_encode($info));
|
||||
Minz_Log::error('SQL error ' . __METHOD__ . json_encode($info));
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
public function count() {
|
||||
public function count(): int {
|
||||
$sql = 'SELECT COUNT(*) AS count FROM `_category`';
|
||||
$stm = $this->pdo->query($sql);
|
||||
$res = $stm->fetchAll(PDO::FETCH_ASSOC);
|
||||
return $res[0]['count'];
|
||||
$res = $this->fetchColumn($sql, 0);
|
||||
return isset($res[0]) ? (int)$res[0] : -1;
|
||||
}
|
||||
|
||||
public function countFeed($id) {
|
||||
public function countFeed(int $id): int {
|
||||
$sql = 'SELECT COUNT(*) AS count FROM `_feed` WHERE category=:id';
|
||||
$stm = $this->pdo->prepare($sql);
|
||||
$stm->bindParam(':id', $id, PDO::PARAM_INT);
|
||||
$stm->execute();
|
||||
$res = $stm->fetchAll(PDO::FETCH_ASSOC);
|
||||
return $res[0]['count'];
|
||||
$res = $this->fetchColumn($sql, 0, [':id' => $id]);
|
||||
return isset($res[0]) ? (int)$res[0] : -1;
|
||||
}
|
||||
|
||||
public function countNotRead($id) {
|
||||
public function countNotRead(int $id): int {
|
||||
$sql = 'SELECT COUNT(*) AS count FROM `_entry` e INNER JOIN `_feed` f ON e.id_feed=f.id WHERE category=:id AND e.is_read=0';
|
||||
$stm = $this->pdo->prepare($sql);
|
||||
$stm->bindParam(':id', $id, PDO::PARAM_INT);
|
||||
$stm->execute();
|
||||
$res = $stm->fetchAll(PDO::FETCH_ASSOC);
|
||||
return $res[0]['count'];
|
||||
$res = $this->fetchColumn($sql, 0, [':id' => $id]);
|
||||
return isset($res[0]) ? (int)$res[0] : -1;
|
||||
}
|
||||
|
||||
public static function findFeed($categories, $feed_id) {
|
||||
foreach ($categories as $category) {
|
||||
foreach ($category->feeds() as $feed) {
|
||||
if ($feed->id() === $feed_id) {
|
||||
return $feed;
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public static function CountUnreads($categories, $minPriority = 0) {
|
||||
$n = 0;
|
||||
foreach ($categories as $category) {
|
||||
foreach ($category->feeds() as $feed) {
|
||||
if ($feed->priority() >= $minPriority) {
|
||||
$n += $feed->nbNotRead();
|
||||
}
|
||||
}
|
||||
}
|
||||
return $n;
|
||||
}
|
||||
|
||||
public static function daoToCategoryPrepopulated($listDAO) {
|
||||
$list = array();
|
||||
|
||||
if (!is_array($listDAO)) {
|
||||
$listDAO = array($listDAO);
|
||||
}
|
||||
|
||||
$previousLine = null;
|
||||
$feedsDao = array();
|
||||
$feedDao = FreshRSS_Factory::createFeedDAO();
|
||||
/**
|
||||
* @param array<array{'c_name':string,'c_id':int,'c_kind':int,'c_last_update':int,'c_error':int|bool,'c_attributes'?:string,
|
||||
* 'id'?:int,'name'?:string,'url'?:string,'kind'?:int,'website'?:string,'priority'?:int,
|
||||
* 'error'?:int|bool,'attributes'?:string,'cache_nbEntries'?:int,'cache_nbUnreads'?:int,'ttl'?:int}> $listDAO
|
||||
* @return array<int,FreshRSS_Category>
|
||||
*/
|
||||
private static function daoToCategoriesPrepopulated(array $listDAO): array {
|
||||
$list = [];
|
||||
$previousLine = [];
|
||||
$feedsDao = [];
|
||||
$feedDao = FreshRSS_Factory::createFeedDao();
|
||||
foreach ($listDAO as $line) {
|
||||
FreshRSS_DatabaseDAO::pdoInt($line, ['c_id', 'c_kind', 'c_last_update', 'c_error',
|
||||
'id', 'kind', 'priority', 'error', 'cache_nbEntries', 'cache_nbUnreads', 'ttl']);
|
||||
if (!empty($previousLine['c_id']) && $line['c_id'] !== $previousLine['c_id']) {
|
||||
// End of the current category, we add it to the $list
|
||||
$cat = new FreshRSS_Category(
|
||||
$previousLine['c_name'],
|
||||
$feedDao->daoToFeed($feedsDao, $previousLine['c_id'])
|
||||
$previousLine['c_id'],
|
||||
$feedDao::daoToFeeds($feedsDao, $previousLine['c_id'])
|
||||
);
|
||||
$cat->_id($previousLine['c_id']);
|
||||
$cat->_attributes('', $previousLine['c_attributes']);
|
||||
$list[$previousLine['c_id']] = $cat;
|
||||
$cat->_kind($previousLine['c_kind']);
|
||||
$cat->_attributes($previousLine['c_attributes'] ?? '[]');
|
||||
$list[$cat->id()] = $cat;
|
||||
|
||||
$feedsDao = array(); //Prepare for next category
|
||||
$feedsDao = []; //Prepare for next category
|
||||
}
|
||||
|
||||
$previousLine = $line;
|
||||
|
@ -400,33 +430,37 @@ SQL;
|
|||
if ($previousLine != null) {
|
||||
$cat = new FreshRSS_Category(
|
||||
$previousLine['c_name'],
|
||||
$feedDao->daoToFeed($feedsDao, $previousLine['c_id'])
|
||||
$previousLine['c_id'],
|
||||
$feedDao::daoToFeeds($feedsDao, $previousLine['c_id'])
|
||||
);
|
||||
$cat->_id($previousLine['c_id']);
|
||||
$cat->_attributes('', $previousLine['c_attributes']);
|
||||
$list[$previousLine['c_id']] = $cat;
|
||||
$cat->_kind($previousLine['c_kind']);
|
||||
$cat->_lastUpdate($previousLine['c_last_update'] ?? 0);
|
||||
$cat->_error($previousLine['c_error'] ?? 0);
|
||||
$cat->_attributes($previousLine['c_attributes'] ?? []);
|
||||
$list[$cat->id()] = $cat;
|
||||
}
|
||||
|
||||
return $list;
|
||||
}
|
||||
|
||||
public static function daoToCategory($listDAO) {
|
||||
$list = array();
|
||||
|
||||
if (!is_array($listDAO)) {
|
||||
$listDAO = array($listDAO);
|
||||
}
|
||||
|
||||
foreach ($listDAO as $key => $dao) {
|
||||
/**
|
||||
* @param array<array{'name':string,'id':int,'kind':int,'lastUpdate'?:int,'error'?:int|bool,'attributes'?:string}> $listDAO
|
||||
* @return array<int,FreshRSS_Category>
|
||||
*/
|
||||
private static function daoToCategories(array $listDAO): array {
|
||||
$list = [];
|
||||
foreach ($listDAO as $dao) {
|
||||
FreshRSS_DatabaseDAO::pdoInt($dao, ['id', 'kind', 'lastUpdate', 'error']);
|
||||
$cat = new FreshRSS_Category(
|
||||
$dao['name']
|
||||
$dao['name'],
|
||||
$dao['id']
|
||||
);
|
||||
$cat->_id($dao['id']);
|
||||
$cat->_attributes('', isset($dao['attributes']) ? $dao['attributes'] : '');
|
||||
$cat->_isDefault(static::DEFAULTCATEGORYID === intval($dao['id']));
|
||||
$list[$key] = $cat;
|
||||
$cat->_kind($dao['kind']);
|
||||
$cat->_lastUpdate($dao['lastUpdate'] ?? 0);
|
||||
$cat->_error($dao['error'] ?? 0);
|
||||
$cat->_attributes($dao['attributes'] ?? '');
|
||||
$list[$cat->id()] = $cat;
|
||||
}
|
||||
|
||||
return $list;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,12 +1,15 @@
|
|||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
class FreshRSS_CategoryDAOSQLite extends FreshRSS_CategoryDAO {
|
||||
|
||||
protected function autoUpdateDb($errorInfo) {
|
||||
/** @param array<int|string> $errorInfo */
|
||||
#[\Override]
|
||||
protected function autoUpdateDb(array $errorInfo): bool {
|
||||
if ($tableInfo = $this->pdo->query("PRAGMA table_info('category')")) {
|
||||
$columns = $tableInfo->fetchAll(PDO::FETCH_COLUMN, 1);
|
||||
foreach (['attributes'] as $column) {
|
||||
if (!in_array($column, $columns)) {
|
||||
foreach (['kind', 'lastUpdate', 'error', 'attributes'] as $column) {
|
||||
if (!in_array($column, $columns, true)) {
|
||||
return $this->addColumn($column);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,397 +0,0 @@
|
|||
<?php
|
||||
|
||||
class FreshRSS_ConfigurationSetter {
|
||||
/**
|
||||
* Return if the given key is supported by this setter.
|
||||
* @param string $key the key to test.
|
||||
* @return boolean true if the key is supported, false otherwise.
|
||||
*/
|
||||
public function support($key) {
|
||||
$name_setter = '_' . $key;
|
||||
return is_callable(array($this, $name_setter));
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the given key in data with the current value.
|
||||
* @param array $data an array containing the list of all configuration data.
|
||||
* @param string $key the key to update.
|
||||
* @param mixed $value the value to set.
|
||||
*/
|
||||
public function handle(&$data, $key, $value) {
|
||||
$name_setter = '_' . $key;
|
||||
call_user_func_array(array($this, $name_setter), array(&$data, $value));
|
||||
}
|
||||
|
||||
/**
|
||||
* A helper to set boolean values.
|
||||
*
|
||||
* @param mixed $value the tested value.
|
||||
* @return boolean true if value is true and different from no, false else.
|
||||
*/
|
||||
private function handleBool($value) {
|
||||
return ((bool)$value) && $value !== 'no';
|
||||
}
|
||||
|
||||
/**
|
||||
* The (long) list of setters for user configuration.
|
||||
*/
|
||||
private function _apiPasswordHash(&$data, $value) {
|
||||
$data['apiPasswordHash'] = ctype_graph($value) && (strlen($value) >= 60) ? $value : '';
|
||||
}
|
||||
|
||||
private function _content_width(&$data, $value) {
|
||||
$value = strtolower($value);
|
||||
if (!in_array($value, array('thin', 'medium', 'large', 'no_limit'))) {
|
||||
$value = 'thin';
|
||||
}
|
||||
|
||||
$data['content_width'] = $value;
|
||||
}
|
||||
|
||||
private function _default_state(&$data, $value) {
|
||||
$data['default_state'] = (int)$value;
|
||||
}
|
||||
|
||||
private function _default_view(&$data, $value) {
|
||||
switch ($value) {
|
||||
case 'all':
|
||||
$data['default_view'] = $value;
|
||||
$data['default_state'] = (FreshRSS_Entry::STATE_READ + FreshRSS_Entry::STATE_NOT_READ);
|
||||
break;
|
||||
case 'adaptive':
|
||||
case 'unread':
|
||||
default:
|
||||
$data['default_view'] = $value;
|
||||
$data['default_state'] = FreshRSS_Entry::STATE_NOT_READ;
|
||||
}
|
||||
}
|
||||
|
||||
// It works for system config too!
|
||||
private function _extensions_enabled(&$data, $value) {
|
||||
if (!is_array($value)) {
|
||||
$value = array($value);
|
||||
}
|
||||
$data['extensions_enabled'] = $value;
|
||||
}
|
||||
|
||||
private function _html5_notif_timeout(&$data, $value) {
|
||||
$value = intval($value);
|
||||
$data['html5_notif_timeout'] = $value >= 0 ? $value : 0;
|
||||
}
|
||||
|
||||
// It works for system config too!
|
||||
private function _language(&$data, $value) {
|
||||
$value = strtolower($value);
|
||||
$languages = Minz_Translate::availableLanguages();
|
||||
if (!in_array($value, $languages)) {
|
||||
$value = 'en';
|
||||
}
|
||||
$data['language'] = $value;
|
||||
}
|
||||
|
||||
private function _passwordHash(&$data, $value) {
|
||||
$data['passwordHash'] = ctype_graph($value) && (strlen($value) >= 60) ? $value : '';
|
||||
}
|
||||
|
||||
private function _posts_per_page(&$data, $value) {
|
||||
$value = intval($value);
|
||||
$data['posts_per_page'] = $value > 0 ? $value : 10;
|
||||
}
|
||||
|
||||
private function _queries(&$data, $values) {
|
||||
$data['queries'] = array();
|
||||
foreach ($values as $value) {
|
||||
if ($value instanceof FreshRSS_UserQuery) {
|
||||
$data['queries'][] = $value->toArray();
|
||||
} elseif (is_array($value)) {
|
||||
$data['queries'][] = $value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private function _sharing(&$data, $values) {
|
||||
$data['sharing'] = array();
|
||||
foreach ($values as $value) {
|
||||
if (!is_array($value)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Verify URL and add default value when needed
|
||||
if (isset($value['url'])) {
|
||||
$is_url = checkUrl($value['url']);
|
||||
if (!$is_url) {
|
||||
continue;
|
||||
}
|
||||
} else {
|
||||
$value['url'] = null;
|
||||
}
|
||||
|
||||
$data['sharing'][] = $value;
|
||||
}
|
||||
}
|
||||
|
||||
private function _shortcuts(&$data, $values) {
|
||||
if (!is_array($values)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$data['shortcuts'] = $values;
|
||||
}
|
||||
|
||||
private function _sort_order(&$data, $value) {
|
||||
$data['sort_order'] = $value === 'ASC' ? 'ASC' : 'DESC';
|
||||
}
|
||||
|
||||
private function _ttl_default(&$data, $value) {
|
||||
$value = intval($value);
|
||||
$data['ttl_default'] = $value > FreshRSS_Feed::TTL_DEFAULT ? $value : 3600;
|
||||
}
|
||||
|
||||
private function _view_mode(&$data, $value) {
|
||||
$value = strtolower($value);
|
||||
if (!in_array($value, array('global', 'normal', 'reader'))) {
|
||||
$value = 'normal';
|
||||
}
|
||||
$data['view_mode'] = $value;
|
||||
}
|
||||
|
||||
/**
|
||||
* A list of boolean setters.
|
||||
*/
|
||||
private function _anon_access(&$data, $value) {
|
||||
$data['anon_access'] = $this->handleBool($value);
|
||||
}
|
||||
|
||||
private function _auto_load_more(&$data, $value) {
|
||||
$data['auto_load_more'] = $this->handleBool($value);
|
||||
}
|
||||
|
||||
private function _auto_remove_article(&$data, $value) {
|
||||
$data['auto_remove_article'] = $this->handleBool($value);
|
||||
}
|
||||
|
||||
private function _mark_updated_article_unread(&$data, $value) {
|
||||
$data['mark_updated_article_unread'] = $this->handleBool($value);
|
||||
}
|
||||
|
||||
private function _show_nav_buttons(&$data, $value) {
|
||||
$data['show_nav_buttons'] = $this->handleBool($value);
|
||||
}
|
||||
|
||||
private function _show_fav_unread(&$data, $value) {
|
||||
$data['show_fav_unread'] = $this->handleBool($value);
|
||||
}
|
||||
|
||||
private function _display_categories(&$data, $value) {
|
||||
if (!in_array($value, [ 'active', 'remember', 'all', 'none' ], true)) {
|
||||
$value = $value === true ? 'all' : 'active';
|
||||
}
|
||||
$data['display_categories'] = $value;
|
||||
}
|
||||
|
||||
private function _display_posts(&$data, $value) {
|
||||
$data['display_posts'] = $this->handleBool($value);
|
||||
}
|
||||
|
||||
private function _hide_read_feeds(&$data, $value) {
|
||||
$data['hide_read_feeds'] = $this->handleBool($value);
|
||||
}
|
||||
|
||||
private function _sides_close_article(&$data, $value) {
|
||||
$data['sides_close_article'] = $this->handleBool($value);
|
||||
}
|
||||
|
||||
private function _lazyload(&$data, $value) {
|
||||
$data['lazyload'] = $this->handleBool($value);
|
||||
}
|
||||
|
||||
private function _onread_jump_next(&$data, $value) {
|
||||
$data['onread_jump_next'] = $this->handleBool($value);
|
||||
}
|
||||
|
||||
private function _reading_confirm(&$data, $value) {
|
||||
$data['reading_confirm'] = $this->handleBool($value);
|
||||
}
|
||||
|
||||
private function _sticky_post(&$data, $value) {
|
||||
$data['sticky_post'] = $this->handleBool($value);
|
||||
}
|
||||
|
||||
private function _bottomline_date(&$data, $value) {
|
||||
$data['bottomline_date'] = $this->handleBool($value);
|
||||
}
|
||||
private function _bottomline_favorite(&$data, $value) {
|
||||
$data['bottomline_favorite'] = $this->handleBool($value);
|
||||
}
|
||||
private function _bottomline_link(&$data, $value) {
|
||||
$data['bottomline_link'] = $this->handleBool($value);
|
||||
}
|
||||
private function _bottomline_read(&$data, $value) {
|
||||
$data['bottomline_read'] = $this->handleBool($value);
|
||||
}
|
||||
private function _bottomline_sharing(&$data, $value) {
|
||||
$data['bottomline_sharing'] = $this->handleBool($value);
|
||||
}
|
||||
private function _bottomline_tags(&$data, $value) {
|
||||
$data['bottomline_tags'] = $this->handleBool($value);
|
||||
}
|
||||
|
||||
private function _topline_date(&$data, $value) {
|
||||
$data['topline_date'] = $this->handleBool($value);
|
||||
}
|
||||
private function _topline_favorite(&$data, $value) {
|
||||
$data['topline_favorite'] = $this->handleBool($value);
|
||||
}
|
||||
private function _topline_link(&$data, $value) {
|
||||
$data['topline_link'] = $this->handleBool($value);
|
||||
}
|
||||
private function _topline_read(&$data, $value) {
|
||||
$data['topline_read'] = $this->handleBool($value);
|
||||
}
|
||||
private function _topline_thumbnail(&$data, $value) {
|
||||
$value = strtolower($value);
|
||||
if (!in_array($value, array('none', 'portrait', 'square', 'landscape'))) {
|
||||
$value = 'none';
|
||||
}
|
||||
$data['topline_thumbnail'] = $value;
|
||||
}
|
||||
private function _topline_summary(&$data, $value) {
|
||||
$data['topline_summary'] = $this->handleBool($value);
|
||||
}
|
||||
private function _topline_display_authors(&$data, $value) {
|
||||
$data['topline_display_authors'] = $this->handleBool($value);
|
||||
}
|
||||
|
||||
/**
|
||||
* The (not so long) list of setters for system configuration.
|
||||
*/
|
||||
private function _allow_anonymous(&$data, $value) {
|
||||
$data['allow_anonymous'] = $this->handleBool($value) && FreshRSS_Auth::accessNeedsAction();
|
||||
}
|
||||
|
||||
private function _allow_anonymous_refresh(&$data, $value) {
|
||||
$data['allow_anonymous_refresh'] = $this->handleBool($value) && $data['allow_anonymous'];
|
||||
}
|
||||
|
||||
private function _api_enabled(&$data, $value) {
|
||||
$data['api_enabled'] = $this->handleBool($value);
|
||||
}
|
||||
|
||||
private function _auth_type(&$data, $value) {
|
||||
$value = strtolower($value);
|
||||
if (!in_array($value, array('form', 'http_auth', 'none'))) {
|
||||
$value = 'none';
|
||||
}
|
||||
$data['auth_type'] = $value;
|
||||
$this->_allow_anonymous($data, $data['allow_anonymous']);
|
||||
}
|
||||
|
||||
private function _db(&$data, $value) {
|
||||
if (!isset($value['type'])) {
|
||||
return;
|
||||
}
|
||||
|
||||
switch ($value['type']) {
|
||||
case 'mysql':
|
||||
case 'pgsql':
|
||||
if (empty($value['host']) ||
|
||||
empty($value['user']) ||
|
||||
empty($value['base']) ||
|
||||
!isset($value['password'])) {
|
||||
return;
|
||||
}
|
||||
|
||||
$data['db']['type'] = $value['type'];
|
||||
$data['db']['host'] = $value['host'];
|
||||
$data['db']['user'] = $value['user'];
|
||||
$data['db']['base'] = $value['base'];
|
||||
$data['db']['password'] = $value['password'];
|
||||
$data['db']['prefix'] = isset($value['prefix']) ? $value['prefix'] : '';
|
||||
break;
|
||||
case 'sqlite':
|
||||
$data['db']['type'] = $value['type'];
|
||||
$data['db']['host'] = '';
|
||||
$data['db']['user'] = '';
|
||||
$data['db']['base'] = '';
|
||||
$data['db']['password'] = '';
|
||||
$data['db']['prefix'] = '';
|
||||
break;
|
||||
default:
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
private function _default_user(&$data, $value) {
|
||||
$user_list = listUsers();
|
||||
if (in_array($value, $user_list)) {
|
||||
$data['default_user'] = $value;
|
||||
}
|
||||
}
|
||||
|
||||
private function _environment(&$data, $value) {
|
||||
$value = strtolower($value);
|
||||
if (!in_array($value, array('silent', 'development', 'production'))) {
|
||||
$value = 'production';
|
||||
}
|
||||
$data['environment'] = $value;
|
||||
}
|
||||
|
||||
private function _limits(&$data, $values) {
|
||||
$max_small_int = 16384;
|
||||
$limits_keys = array(
|
||||
'cookie_duration' => array(
|
||||
'min' => 0,
|
||||
),
|
||||
'cache_duration' => array(
|
||||
'min' => 0,
|
||||
),
|
||||
'timeout' => array(
|
||||
'min' => 0,
|
||||
),
|
||||
'max_inactivity' => array(
|
||||
'min' => 0,
|
||||
),
|
||||
'max_feeds' => array(
|
||||
'min' => 0,
|
||||
'max' => $max_small_int,
|
||||
),
|
||||
'max_categories' => array(
|
||||
'min' => 0,
|
||||
'max' => $max_small_int,
|
||||
),
|
||||
'max_registrations' => array(
|
||||
'min' => 0,
|
||||
),
|
||||
);
|
||||
|
||||
foreach ($values as $key => $value) {
|
||||
if (!isset($limits_keys[$key])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$value = intval($value);
|
||||
$limits = $limits_keys[$key];
|
||||
if ((!isset($limits['min']) || $value >= $limits['min']) &&
|
||||
(!isset($limits['max']) || $value <= $limits['max'])
|
||||
) {
|
||||
$data['limits'][$key] = $value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private function _unsafe_autologin_enabled(&$data, $value) {
|
||||
$data['unsafe_autologin_enabled'] = $this->handleBool($value);
|
||||
}
|
||||
|
||||
private function _auto_update_url(&$data, $value) {
|
||||
if (!$value) {
|
||||
return;
|
||||
}
|
||||
|
||||
$data['auto_update_url'] = $value;
|
||||
}
|
||||
|
||||
private function _force_email_validation(&$data, $value) {
|
||||
$data['force_email_validation'] = $this->handleBool($value);
|
||||
}
|
||||
}
|
|
@ -1,66 +1,99 @@
|
|||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* The context object handles the current configuration file and different
|
||||
* useful functions associated to the current view state.
|
||||
*/
|
||||
class FreshRSS_Context {
|
||||
public static $user_conf = null;
|
||||
public static $system_conf = null;
|
||||
public static $categories = array();
|
||||
public static $tags = array();
|
||||
final class FreshRSS_Context {
|
||||
|
||||
public static $name = '';
|
||||
public static $description = '';
|
||||
/**
|
||||
* @var array<int,FreshRSS_Category>
|
||||
*/
|
||||
private static array $categories = [];
|
||||
/**
|
||||
* @var array<int,FreshRSS_Tag>
|
||||
*/
|
||||
private static array $tags = [];
|
||||
public static string $name = '';
|
||||
public static string $description = '';
|
||||
public static int $total_unread = 0;
|
||||
public static int $total_important_unread = 0;
|
||||
|
||||
public static $total_unread = 0;
|
||||
public static $total_starred = array(
|
||||
/** @var array{'all':int,'read':int,'unread':int} */
|
||||
public static array $total_starred = [
|
||||
'all' => 0,
|
||||
'read' => 0,
|
||||
'unread' => 0,
|
||||
);
|
||||
];
|
||||
|
||||
public static $get_unread = 0;
|
||||
public static $current_get = array(
|
||||
public static int $get_unread = 0;
|
||||
|
||||
/** @var array{'all':bool,'starred':bool,'important':bool,'feed':int|false,'category':int|false,'tag':int|false,'tags':bool} */
|
||||
public static array $current_get = [
|
||||
'all' => false,
|
||||
'starred' => false,
|
||||
'important' => false,
|
||||
'feed' => false,
|
||||
'category' => false,
|
||||
'tag' => false,
|
||||
'tags' => false,
|
||||
);
|
||||
public static $next_get = 'a';
|
||||
];
|
||||
|
||||
public static $state = 0;
|
||||
public static $order = 'DESC';
|
||||
public static $number = 0;
|
||||
public static $search;
|
||||
public static $first_id = '';
|
||||
public static $next_id = '';
|
||||
public static $id_max = '';
|
||||
public static $sinceHours = 0;
|
||||
public static string $next_get = 'a';
|
||||
public static int $state = 0;
|
||||
/**
|
||||
* @phpstan-var 'ASC'|'DESC'
|
||||
*/
|
||||
public static string $order = 'DESC';
|
||||
public static int $number = 0;
|
||||
public static int $offset = 0;
|
||||
public static FreshRSS_BooleanSearch $search;
|
||||
public static string $first_id = '';
|
||||
public static string $next_id = '';
|
||||
public static string $id_max = '';
|
||||
public static int $sinceHours = 0;
|
||||
public static bool $isCli = false;
|
||||
|
||||
public static $isCli = false;
|
||||
/**
|
||||
* @deprecated Will be made `private`; use `FreshRSS_Context::systemConf()` instead.
|
||||
* @internal
|
||||
*/
|
||||
public static ?FreshRSS_SystemConfiguration $system_conf = null;
|
||||
/**
|
||||
* @deprecated Will be made `private`; use `FreshRSS_Context::userConf()` instead.
|
||||
* @internal
|
||||
*/
|
||||
public static ?FreshRSS_UserConfiguration $user_conf = null;
|
||||
|
||||
/**
|
||||
* Initialize the context for the global system.
|
||||
*/
|
||||
public static function initSystem($reload = false) {
|
||||
if ($reload || FreshRSS_Context::$system_conf == null) {
|
||||
public static function initSystem(bool $reload = false): void {
|
||||
if ($reload || FreshRSS_Context::$system_conf === null) {
|
||||
//TODO: Keep in session what we need instead of always reloading from disk
|
||||
Minz_Configuration::register('system', DATA_PATH . '/config.php', FRESHRSS_PATH . '/config.default.php');
|
||||
FreshRSS_Context::$system_conf = Minz_Configuration::get('system');
|
||||
// Register the configuration setter for the system configuration
|
||||
$configurationSetter = new FreshRSS_ConfigurationSetter();
|
||||
FreshRSS_Context::$system_conf->_configurationSetter($configurationSetter);
|
||||
FreshRSS_Context::$system_conf = FreshRSS_SystemConfiguration::init(DATA_PATH . '/config.php', FRESHRSS_PATH . '/config.default.php');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws FreshRSS_Context_Exception
|
||||
*/
|
||||
public static function &systemConf(): FreshRSS_SystemConfiguration {
|
||||
if (FreshRSS_Context::$system_conf === null) {
|
||||
throw new FreshRSS_Context_Exception('System configuration not initialised!');
|
||||
}
|
||||
return FreshRSS_Context::$system_conf;
|
||||
}
|
||||
|
||||
public static function hasSystemConf(): bool {
|
||||
return FreshRSS_Context::$system_conf !== null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize the context for the current user.
|
||||
*/
|
||||
public static function initUser($username = '', $userMustExist = true) {
|
||||
public static function initUser(string $username = '', bool $userMustExist = true): void {
|
||||
FreshRSS_Context::$user_conf = null;
|
||||
if (!isset($_SESSION)) {
|
||||
Minz_Session::init('FreshRSS');
|
||||
|
@ -68,38 +101,40 @@ class FreshRSS_Context {
|
|||
|
||||
Minz_Session::lock();
|
||||
if ($username == '') {
|
||||
$username = Minz_Session::param('currentUser', '');
|
||||
$username = Minz_User::name() ?? '';
|
||||
}
|
||||
if (($username === '_' || FreshRSS_user_Controller::checkUsername($username)) &&
|
||||
if (($username === Minz_User::INTERNAL_USER || FreshRSS_user_Controller::checkUsername($username)) &&
|
||||
(!$userMustExist || FreshRSS_user_Controller::userExists($username))) {
|
||||
try {
|
||||
//TODO: Keep in session what we need instead of always reloading from disk
|
||||
Minz_Configuration::register('user',
|
||||
FreshRSS_Context::$user_conf = FreshRSS_UserConfiguration::init(
|
||||
USERS_PATH . '/' . $username . '/config.php',
|
||||
FRESHRSS_PATH . '/config-user.default.php',
|
||||
FreshRSS_Context::$system_conf->configurationSetter());
|
||||
FRESHRSS_PATH . '/config-user.default.php');
|
||||
|
||||
Minz_Session::_param('currentUser', $username);
|
||||
FreshRSS_Context::$user_conf = Minz_Configuration::get('user');
|
||||
Minz_User::change($username);
|
||||
} catch (Exception $ex) {
|
||||
Minz_Log::warning($ex->getMessage(), USERS_PATH . '/_/log.txt');
|
||||
Minz_Log::warning($ex->getMessage(), USERS_PATH . '/_/' . LOG_FILENAME);
|
||||
}
|
||||
}
|
||||
if (FreshRSS_Context::$user_conf == null) {
|
||||
Minz_Session::_params([
|
||||
'loginOk' => false,
|
||||
'currentUser' => false,
|
||||
Minz_User::CURRENT_USER => false,
|
||||
]);
|
||||
}
|
||||
Minz_Session::unlock();
|
||||
|
||||
if (FreshRSS_Context::$user_conf == null) {
|
||||
return false;
|
||||
return;
|
||||
}
|
||||
|
||||
FreshRSS_Context::$search = new FreshRSS_BooleanSearch('');
|
||||
|
||||
//Legacy
|
||||
$oldEntries = (int)FreshRSS_Context::$user_conf->param('old_entries', 0);
|
||||
$keepMin = (int)FreshRSS_Context::$user_conf->param('keep_history_default', -5);
|
||||
$oldEntries = FreshRSS_Context::$user_conf->param('old_entries', 0);
|
||||
$oldEntries = is_numeric($oldEntries) ? (int)$oldEntries : 0;
|
||||
$keepMin = FreshRSS_Context::$user_conf->param('keep_history_default', -5);
|
||||
$keepMin = is_numeric($keepMin) ? (int)$keepMin : -5;
|
||||
if ($oldEntries > 0 || $keepMin > -5) { //Freshrss < 1.15
|
||||
$archiving = FreshRSS_Context::$user_conf->archiving;
|
||||
$archiving['keep_max'] = false;
|
||||
|
@ -119,81 +154,204 @@ class FreshRSS_Context {
|
|||
if (!in_array(FreshRSS_Context::$user_conf->display_categories, [ 'active', 'remember', 'all', 'none' ], true)) {
|
||||
FreshRSS_Context::$user_conf->display_categories = FreshRSS_Context::$user_conf->display_categories === true ? 'all' : 'active';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws FreshRSS_Context_Exception
|
||||
*/
|
||||
public static function &userConf(): FreshRSS_UserConfiguration {
|
||||
if (FreshRSS_Context::$user_conf === null) {
|
||||
throw new FreshRSS_Context_Exception('User configuration not initialised!');
|
||||
}
|
||||
return FreshRSS_Context::$user_conf;
|
||||
}
|
||||
|
||||
public static function hasUserConf(): bool {
|
||||
return FreshRSS_Context::$user_conf !== null;
|
||||
}
|
||||
|
||||
public static function clearUserConf(): void {
|
||||
FreshRSS_Context::$user_conf = null;
|
||||
}
|
||||
|
||||
/** @return array<int,FreshRSS_Category> */
|
||||
public static function categories(): array {
|
||||
if (empty(self::$categories)) {
|
||||
$catDAO = FreshRSS_Factory::createCategoryDao();
|
||||
self::$categories = $catDAO->listSortedCategories(true, false);
|
||||
}
|
||||
return self::$categories;
|
||||
}
|
||||
|
||||
/** @return array<int,FreshRSS_Feed> */
|
||||
public static function feeds(): array {
|
||||
return FreshRSS_Category::findFeeds(self::categories());
|
||||
}
|
||||
|
||||
/** @return array<int,FreshRSS_Tag> */
|
||||
public static function labels(bool $precounts = false): array {
|
||||
if (empty(self::$tags) || $precounts) {
|
||||
$tagDAO = FreshRSS_Factory::createTagDao();
|
||||
self::$tags = $tagDAO->listTags($precounts) ?: [];
|
||||
}
|
||||
return self::$tags;
|
||||
}
|
||||
|
||||
/**
|
||||
* This action updates the Context object by using request parameters.
|
||||
*
|
||||
* HTTP GET request parameters are:
|
||||
* - state (default: conf->default_view)
|
||||
* - search (default: empty string)
|
||||
* - order (default: conf->sort_order)
|
||||
* - nb (default: conf->posts_per_page)
|
||||
* - next (default: empty string)
|
||||
* - hours (default: 0)
|
||||
* @throws FreshRSS_Context_Exception
|
||||
* @throws Minz_ConfigurationNamespaceException
|
||||
* @throws Minz_PDOConnectionException
|
||||
*/
|
||||
public static function updateUsingRequest(bool $computeStatistics): void {
|
||||
if ($computeStatistics && self::$total_unread === 0) {
|
||||
// Update number of read / unread variables.
|
||||
$entryDAO = FreshRSS_Factory::createEntryDao();
|
||||
self::$total_starred = $entryDAO->countUnreadReadFavorites();
|
||||
self::$total_unread = FreshRSS_Category::countUnread(self::categories(), FreshRSS_Feed::PRIORITY_MAIN_STREAM);
|
||||
self::$total_important_unread = FreshRSS_Category::countUnread(self::categories(), FreshRSS_Feed::PRIORITY_IMPORTANT);
|
||||
}
|
||||
|
||||
self::_get(Minz_Request::paramString('get') ?: 'a');
|
||||
|
||||
self::$state = Minz_Request::paramInt('state') ?: FreshRSS_Context::userConf()->default_state;
|
||||
$state_forced_by_user = Minz_Request::paramString('state') !== '';
|
||||
if (!$state_forced_by_user && !self::isStateEnabled(FreshRSS_Entry::STATE_READ)) {
|
||||
if (FreshRSS_Context::userConf()->default_view === 'all') {
|
||||
self::$state |= FreshRSS_Entry::STATE_ALL;
|
||||
} elseif (FreshRSS_Context::userConf()->default_view === 'adaptive' && self::$get_unread <= 0) {
|
||||
self::$state |= FreshRSS_Entry::STATE_READ;
|
||||
}
|
||||
if (FreshRSS_Context::userConf()->show_fav_unread &&
|
||||
(self::isCurrentGet('s') || self::isCurrentGet('T') || self::isTag())) {
|
||||
self::$state |= FreshRSS_Entry::STATE_READ;
|
||||
}
|
||||
}
|
||||
|
||||
self::$search = new FreshRSS_BooleanSearch(Minz_Request::paramString('search'));
|
||||
$order = Minz_Request::paramString('order') ?: FreshRSS_Context::userConf()->sort_order;
|
||||
self::$order = in_array($order, ['ASC', 'DESC'], true) ? $order : 'DESC';
|
||||
self::$number = Minz_Request::paramInt('nb') ?: FreshRSS_Context::userConf()->posts_per_page;
|
||||
if (self::$number > FreshRSS_Context::userConf()->max_posts_per_rss) {
|
||||
self::$number = max(
|
||||
FreshRSS_Context::userConf()->max_posts_per_rss,
|
||||
FreshRSS_Context::userConf()->posts_per_page);
|
||||
}
|
||||
self::$offset = Minz_Request::paramInt('offset');
|
||||
self::$first_id = Minz_Request::paramString('next');
|
||||
self::$sinceHours = Minz_Request::paramInt('hours');
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns if the current state includes $state parameter.
|
||||
* @param int $state
|
||||
*/
|
||||
public static function isStateEnabled($state) {
|
||||
public static function isStateEnabled(int $state): int {
|
||||
return self::$state & $state;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the current state with or without $state parameter.
|
||||
* @param int $state
|
||||
*/
|
||||
public static function getRevertState($state) {
|
||||
public static function getRevertState(int $state): int {
|
||||
if (self::$state & $state) {
|
||||
return self::$state & ~$state;
|
||||
} else {
|
||||
return self::$state | $state;
|
||||
}
|
||||
return self::$state | $state;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the current get as a string or an array.
|
||||
*
|
||||
* If $array is true, the first item of the returned value is 'f' or 'c' and
|
||||
* the second is the id.
|
||||
* If $array is true, the first item of the returned value is 'f' or 'c' or 't' and the second is the id.
|
||||
* @phpstan-return ($asArray is true ? array{'a'|'c'|'f'|'i'|'s'|'t'|'T',bool|int} : string)
|
||||
* @return string|array{string,bool|int}
|
||||
*/
|
||||
public static function currentGet($array = false) {
|
||||
public static function currentGet(bool $asArray = false) {
|
||||
if (self::$current_get['all']) {
|
||||
return 'a';
|
||||
return $asArray ? ['a', true] : 'a';
|
||||
} elseif (self::$current_get['important']) {
|
||||
return $asArray ? ['i', true] : 'i';
|
||||
} elseif (self::$current_get['starred']) {
|
||||
return 's';
|
||||
return $asArray ? ['s', true] : 's';
|
||||
} elseif (self::$current_get['feed']) {
|
||||
if ($array) {
|
||||
return array('f', self::$current_get['feed']);
|
||||
if ($asArray) {
|
||||
return ['f', self::$current_get['feed']];
|
||||
} else {
|
||||
return 'f_' . self::$current_get['feed'];
|
||||
}
|
||||
} elseif (self::$current_get['category']) {
|
||||
if ($array) {
|
||||
return array('c', self::$current_get['category']);
|
||||
if ($asArray) {
|
||||
return ['c', self::$current_get['category']];
|
||||
} else {
|
||||
return 'c_' . self::$current_get['category'];
|
||||
}
|
||||
} elseif (self::$current_get['tag']) {
|
||||
if ($array) {
|
||||
return array('t', self::$current_get['tag']);
|
||||
if ($asArray) {
|
||||
return ['t', self::$current_get['tag']];
|
||||
} else {
|
||||
return 't_' . self::$current_get['tag'];
|
||||
}
|
||||
} elseif (self::$current_get['tags']) {
|
||||
return 'T';
|
||||
return $asArray ? ['T', true] : 'T';
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Return true if the current request targets a feed (and not a category or all articles), false otherwise.
|
||||
* @return bool true if the current request targets all feeds (main view), false otherwise.
|
||||
*/
|
||||
public static function isFeed() {
|
||||
public static function isAll(): bool {
|
||||
return self::$current_get['all'] != false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return bool true if the current request targets important feeds, false otherwise.
|
||||
*/
|
||||
public static function isImportant(): bool {
|
||||
return self::$current_get['important'] != false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return bool true if the current request targets a category, false otherwise.
|
||||
*/
|
||||
public static function isCategory(): bool {
|
||||
return self::$current_get['category'] != false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return bool true if the current request targets a feed (and not a category or all articles), false otherwise.
|
||||
*/
|
||||
public static function isFeed(): bool {
|
||||
return self::$current_get['feed'] != false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return true if $get parameter correspond to the $current_get attribute.
|
||||
* @return bool true if the current request targets a tag (though not all tags), false otherwise.
|
||||
*/
|
||||
public static function isCurrentGet($get) {
|
||||
$type = $get[0];
|
||||
public static function isTag(): bool {
|
||||
return self::$current_get['tag'] != false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return bool whether $get parameter corresponds to the $current_get attribute.
|
||||
*/
|
||||
public static function isCurrentGet(string $get): bool {
|
||||
$type = substr($get, 0, 1);
|
||||
$id = substr($get, 2);
|
||||
|
||||
switch($type) {
|
||||
case 'a':
|
||||
return self::$current_get['all'];
|
||||
case 'i':
|
||||
return self::$current_get['important'];
|
||||
case 's':
|
||||
return self::$current_get['starred'];
|
||||
case 'f':
|
||||
|
@ -221,28 +379,37 @@ class FreshRSS_Context {
|
|||
*
|
||||
* $name and $get_unread attributes are also updated as $next_get
|
||||
* Raise an exception if id or $get is invalid.
|
||||
* @throws FreshRSS_Context_Exception
|
||||
* @throws Minz_ConfigurationNamespaceException
|
||||
* @throws Minz_PDOConnectionException
|
||||
*/
|
||||
public static function _get($get) {
|
||||
public static function _get(string $get): void {
|
||||
$type = $get[0];
|
||||
$id = substr($get, 2);
|
||||
$nb_unread = 0;
|
||||
$id = (int)substr($get, 2);
|
||||
|
||||
if (empty(self::$categories)) {
|
||||
$catDAO = FreshRSS_Factory::createCategoryDao();
|
||||
self::$categories = $catDAO->listCategories();
|
||||
$details = $type === 'f'; // Load additional feed details in the case of feed view
|
||||
self::$categories = $catDAO->listCategories(true, $details);
|
||||
}
|
||||
|
||||
switch($type) {
|
||||
case 'a':
|
||||
self::$current_get['all'] = true;
|
||||
self::$name = _t('index.feed.title');
|
||||
self::$description = self::$system_conf->meta_description;
|
||||
self::$description = FreshRSS_Context::systemConf()->meta_description;
|
||||
self::$get_unread = self::$total_unread;
|
||||
break;
|
||||
case 'i':
|
||||
self::$current_get['important'] = true;
|
||||
self::$name = _t('index.menu.important');
|
||||
self::$description = FreshRSS_Context::systemConf()->meta_description;
|
||||
self::$get_unread = self::$total_unread;
|
||||
break;
|
||||
case 's':
|
||||
self::$current_get['starred'] = true;
|
||||
self::$name = _t('index.feed.title_fav');
|
||||
self::$description = self::$system_conf->meta_description;
|
||||
self::$description = FreshRSS_Context::systemConf()->meta_description;
|
||||
self::$get_unread = self::$total_starred['unread'];
|
||||
|
||||
// Update state if favorite is not yet enabled.
|
||||
|
@ -250,16 +417,16 @@ class FreshRSS_Context {
|
|||
break;
|
||||
case 'f':
|
||||
// We try to find the corresponding feed. When allowing robots, always retrieve the full feed including description
|
||||
$feed = FreshRSS_Context::$system_conf->allow_robots ? null : FreshRSS_CategoryDAO::findFeed(self::$categories, $id);
|
||||
$feed = FreshRSS_Context::systemConf()->allow_robots ? null : FreshRSS_Category::findFeed(self::$categories, $id);
|
||||
if ($feed === null) {
|
||||
$feedDAO = FreshRSS_Factory::createFeedDao();
|
||||
$feed = $feedDAO->searchById($id);
|
||||
if (!$feed) {
|
||||
if ($feed === null) {
|
||||
throw new FreshRSS_Context_Exception('Invalid feed: ' . $id);
|
||||
}
|
||||
}
|
||||
self::$current_get['feed'] = $id;
|
||||
self::$current_get['category'] = $feed->category();
|
||||
self::$current_get['category'] = $feed->categoryId();
|
||||
self::$name = $feed->name();
|
||||
self::$description = $feed->description();
|
||||
self::$get_unread = $feed->nbNotRead();
|
||||
|
@ -270,9 +437,10 @@ class FreshRSS_Context {
|
|||
if (!isset(self::$categories[$id])) {
|
||||
$catDAO = FreshRSS_Factory::createCategoryDao();
|
||||
$cat = $catDAO->searchById($id);
|
||||
if (!$cat) {
|
||||
if ($cat === null) {
|
||||
throw new FreshRSS_Context_Exception('Invalid category: ' . $id);
|
||||
}
|
||||
self::$categories[$id] = $cat;
|
||||
} else {
|
||||
$cat = self::$categories[$id];
|
||||
}
|
||||
|
@ -285,9 +453,10 @@ class FreshRSS_Context {
|
|||
if (!isset(self::$tags[$id])) {
|
||||
$tagDAO = FreshRSS_Factory::createTagDao();
|
||||
$tag = $tagDAO->searchById($id);
|
||||
if (!$tag) {
|
||||
if ($tag === null) {
|
||||
throw new FreshRSS_Context_Exception('Invalid tag: ' . $id);
|
||||
}
|
||||
self::$tags[$id] = $tag;
|
||||
} else {
|
||||
$tag = self::$tags[$id];
|
||||
}
|
||||
|
@ -310,17 +479,17 @@ class FreshRSS_Context {
|
|||
/**
|
||||
* Set the value of $next_get attribute.
|
||||
*/
|
||||
private static function _nextGet() {
|
||||
private static function _nextGet(): void {
|
||||
$get = self::currentGet();
|
||||
// By default, $next_get == $get
|
||||
self::$next_get = $get;
|
||||
|
||||
if (empty(self::$categories)) {
|
||||
$catDAO = FreshRSS_Factory::createCategoryDao();
|
||||
self::$categories = $catDAO->listCategories();
|
||||
self::$categories = $catDAO->listCategories(true);
|
||||
}
|
||||
|
||||
if (self::$user_conf->onread_jump_next && strlen($get) > 2) {
|
||||
if (FreshRSS_Context::userConf()->onread_jump_next && strlen($get) > 2) {
|
||||
$another_unread_id = '';
|
||||
$found_current_get = false;
|
||||
switch ($get[0]) {
|
||||
|
@ -382,11 +551,9 @@ class FreshRSS_Context {
|
|||
* - it is activated in the configuration
|
||||
* - the "read" state is not enable
|
||||
* - the "unread" state is enable
|
||||
*
|
||||
* @return boolean
|
||||
*/
|
||||
public static function isAutoRemoveAvailable() {
|
||||
if (!self::$user_conf->auto_remove_article) {
|
||||
public static function isAutoRemoveAvailable(): bool {
|
||||
if (!FreshRSS_Context::userConf()->auto_remove_article) {
|
||||
return false;
|
||||
}
|
||||
if (self::isStateEnabled(FreshRSS_Entry::STATE_READ)) {
|
||||
|
@ -403,11 +570,9 @@ class FreshRSS_Context {
|
|||
* by the user when it is selected in the configuration page or by the
|
||||
* application when the context allows to auto-remove articles when they
|
||||
* are read.
|
||||
*
|
||||
* @return boolean
|
||||
*/
|
||||
public static function isStickyPostEnabled() {
|
||||
if (self::$user_conf->sticky_post) {
|
||||
public static function isStickyPostEnabled(): bool {
|
||||
if (FreshRSS_Context::userConf()->sticky_post) {
|
||||
return true;
|
||||
}
|
||||
if (self::isAutoRemoveAvailable()) {
|
||||
|
@ -416,4 +581,8 @@ class FreshRSS_Context {
|
|||
return false;
|
||||
}
|
||||
|
||||
public static function defaultTimeZone(): string {
|
||||
$timezone = ini_get('date.timezone');
|
||||
return $timezone != false ? $timezone : 'UTC';
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* This class is used to test database is well-constructed.
|
||||
|
@ -6,22 +7,22 @@
|
|||
class FreshRSS_DatabaseDAO extends Minz_ModelPdo {
|
||||
|
||||
//MySQL error codes
|
||||
const ER_BAD_FIELD_ERROR = '42S22';
|
||||
const ER_BAD_TABLE_ERROR = '42S02';
|
||||
const ER_DATA_TOO_LONG = '1406';
|
||||
public const ER_BAD_FIELD_ERROR = '42S22';
|
||||
public const ER_BAD_TABLE_ERROR = '42S02';
|
||||
public const ER_DATA_TOO_LONG = '1406';
|
||||
|
||||
/**
|
||||
* Based on SQLite SQLITE_MAX_VARIABLE_NUMBER
|
||||
*/
|
||||
const MAX_VARIABLE_NUMBER = 998;
|
||||
public const MAX_VARIABLE_NUMBER = 998;
|
||||
|
||||
//MySQL InnoDB maximum index length for UTF8MB4
|
||||
//https://dev.mysql.com/doc/refman/8.0/en/innodb-restrictions.html
|
||||
const LENGTH_INDEX_UNICODE = 191;
|
||||
public const LENGTH_INDEX_UNICODE = 191;
|
||||
|
||||
public function create() {
|
||||
require(APP_PATH . '/SQL/install.sql.' . $this->pdo->dbType() . '.php');
|
||||
$db = FreshRSS_Context::$system_conf->db;
|
||||
public function create(): string {
|
||||
require_once(APP_PATH . '/SQL/install.sql.' . $this->pdo->dbType() . '.php');
|
||||
$db = FreshRSS_Context::systemConf()->db;
|
||||
|
||||
try {
|
||||
$sql = sprintf($GLOBALS['SQL_CREATE_DB'], empty($db['base']) ? '' : $db['base']);
|
||||
|
@ -32,105 +33,150 @@ class FreshRSS_DatabaseDAO extends Minz_ModelPdo {
|
|||
}
|
||||
}
|
||||
|
||||
public function testConnection() {
|
||||
public function testConnection(): string {
|
||||
try {
|
||||
$sql = 'SELECT 1';
|
||||
$stm = $this->pdo->query($sql);
|
||||
if ($stm === false) {
|
||||
return 'Error during SQL connection test!';
|
||||
}
|
||||
$res = $stm->fetchAll(PDO::FETCH_COLUMN, 0);
|
||||
return $res == false ? 'Error during SQL connection test!' : '';
|
||||
return $res == false ? 'Error during SQL connection fetch test!' : '';
|
||||
} catch (Exception $e) {
|
||||
syslog(LOG_DEBUG, __method__ . ' warning: ' . $e->getMessage());
|
||||
return $e->getMessage();
|
||||
}
|
||||
}
|
||||
|
||||
public function tablesAreCorrect() {
|
||||
$stm = $this->pdo->query('SHOW TABLES');
|
||||
$res = $stm->fetchAll(PDO::FETCH_ASSOC);
|
||||
public function exits(): bool {
|
||||
$sql = 'SELECT * FROM `_entry` LIMIT 1';
|
||||
$stm = $this->pdo->query($sql);
|
||||
if ($stm !== false) {
|
||||
$res = $stm->fetchAll(PDO::FETCH_COLUMN, 0);
|
||||
if ($res !== false) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
$tables = array(
|
||||
public function tablesAreCorrect(): bool {
|
||||
$res = $this->fetchAssoc('SHOW TABLES');
|
||||
if ($res == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$tables = [
|
||||
$this->pdo->prefix() . 'category' => false,
|
||||
$this->pdo->prefix() . 'feed' => false,
|
||||
$this->pdo->prefix() . 'entry' => false,
|
||||
$this->pdo->prefix() . 'entrytmp' => false,
|
||||
$this->pdo->prefix() . 'tag' => false,
|
||||
$this->pdo->prefix() . 'entrytag' => false,
|
||||
);
|
||||
];
|
||||
foreach ($res as $value) {
|
||||
$tables[array_pop($value)] = true;
|
||||
}
|
||||
|
||||
return count(array_keys($tables, true, true)) == count($tables);
|
||||
return count(array_keys($tables, true, true)) === count($tables);
|
||||
}
|
||||
|
||||
public function getSchema($table) {
|
||||
$sql = 'DESC `_' . $table . '`';
|
||||
$stm = $this->pdo->query($sql);
|
||||
return $this->listDaoToSchema($stm->fetchAll(PDO::FETCH_ASSOC));
|
||||
/** @return array<array<string,string|int|bool|null>> */
|
||||
public function getSchema(string $table): array {
|
||||
$res = $this->fetchAssoc('DESC `_' . $table . '`');
|
||||
return $res == null ? [] : $this->listDaoToSchema($res);
|
||||
}
|
||||
|
||||
public function checkTable($table, $schema) {
|
||||
/** @param array<string> $schema */
|
||||
public function checkTable(string $table, array $schema): bool {
|
||||
$columns = $this->getSchema($table);
|
||||
|
||||
$ok = (count($columns) == count($schema));
|
||||
foreach ($columns as $c) {
|
||||
$ok &= in_array($c['name'], $schema);
|
||||
if (count($columns) === 0 || count($schema) === 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return $ok;
|
||||
$ok = count($columns) === count($schema);
|
||||
foreach ($columns as $c) {
|
||||
$ok &= in_array($c['name'], $schema, true);
|
||||
}
|
||||
|
||||
return (bool)$ok;
|
||||
}
|
||||
|
||||
public function categoryIsCorrect() {
|
||||
return $this->checkTable('category', array(
|
||||
'id', 'name',
|
||||
));
|
||||
public function categoryIsCorrect(): bool {
|
||||
return $this->checkTable('category', ['id', 'name']);
|
||||
}
|
||||
|
||||
public function feedIsCorrect() {
|
||||
return $this->checkTable('feed', array(
|
||||
'id', 'url', 'category', 'name', 'website', 'description', 'lastUpdate',
|
||||
'priority', 'pathEntries', 'httpAuth', 'error', 'ttl', 'attributes',
|
||||
'cache_nbEntries', 'cache_nbUnreads',
|
||||
));
|
||||
public function feedIsCorrect(): bool {
|
||||
return $this->checkTable('feed', [
|
||||
'id',
|
||||
'url',
|
||||
'category',
|
||||
'name',
|
||||
'website',
|
||||
'description',
|
||||
'lastUpdate',
|
||||
'priority',
|
||||
'pathEntries',
|
||||
'httpAuth',
|
||||
'error',
|
||||
'ttl',
|
||||
'attributes',
|
||||
'cache_nbEntries',
|
||||
'cache_nbUnreads',
|
||||
]);
|
||||
}
|
||||
|
||||
public function entryIsCorrect() {
|
||||
return $this->checkTable('entry', array(
|
||||
'id', 'guid', 'title', 'author', 'content_bin', 'link', 'date', 'lastSeen', 'hash', 'is_read',
|
||||
'is_favorite', 'id_feed', 'tags',
|
||||
));
|
||||
public function entryIsCorrect(): bool {
|
||||
return $this->checkTable('entry', [
|
||||
'id',
|
||||
'guid',
|
||||
'title',
|
||||
'author',
|
||||
'content_bin',
|
||||
'link',
|
||||
'date',
|
||||
'lastSeen',
|
||||
'hash',
|
||||
'is_read',
|
||||
'is_favorite',
|
||||
'id_feed',
|
||||
'tags',
|
||||
]);
|
||||
}
|
||||
|
||||
public function entrytmpIsCorrect() {
|
||||
return $this->checkTable('entrytmp', array(
|
||||
'id', 'guid', 'title', 'author', 'content_bin', 'link', 'date', 'lastSeen', 'hash', 'is_read',
|
||||
'is_favorite', 'id_feed', 'tags',
|
||||
));
|
||||
public function entrytmpIsCorrect(): bool {
|
||||
return $this->checkTable('entrytmp', [
|
||||
'id', 'guid', 'title', 'author', 'content_bin', 'link', 'date', 'lastSeen', 'hash', 'is_read', 'is_favorite', 'id_feed', 'tags'
|
||||
]);
|
||||
}
|
||||
|
||||
public function tagIsCorrect() {
|
||||
return $this->checkTable('tag', array(
|
||||
'id', 'name', 'attributes',
|
||||
));
|
||||
public function tagIsCorrect(): bool {
|
||||
return $this->checkTable('tag', ['id', 'name', 'attributes']);
|
||||
}
|
||||
|
||||
public function entrytagIsCorrect() {
|
||||
return $this->checkTable('entrytag', array(
|
||||
'id_tag', 'id_entry',
|
||||
));
|
||||
public function entrytagIsCorrect(): bool {
|
||||
return $this->checkTable('entrytag', ['id_tag', 'id_entry']);
|
||||
}
|
||||
|
||||
public function daoToSchema($dao) {
|
||||
return array(
|
||||
'name' => $dao['Field'],
|
||||
'type' => strtolower($dao['Type']),
|
||||
/**
|
||||
* @param array<string,string|int|bool|null> $dao
|
||||
* @return array{'name':string,'type':string,'notnull':bool,'default':mixed}
|
||||
*/
|
||||
public function daoToSchema(array $dao): array {
|
||||
return [
|
||||
'name' => (string)($dao['Field']),
|
||||
'type' => strtolower((string)($dao['Type'])),
|
||||
'notnull' => (bool)$dao['Null'],
|
||||
'default' => $dao['Default'],
|
||||
);
|
||||
];
|
||||
}
|
||||
|
||||
public function listDaoToSchema($listDAO) {
|
||||
$list = array();
|
||||
/**
|
||||
* @param array<array<string,string|int|bool|null>> $listDAO
|
||||
* @return array<array<string,string|int|bool|null>>
|
||||
*/
|
||||
public function listDaoToSchema(array $listDAO): array {
|
||||
$list = [];
|
||||
|
||||
foreach ($listDAO as $dao) {
|
||||
$list[] = $this->daoToSchema($dao);
|
||||
|
@ -139,28 +185,40 @@ class FreshRSS_DatabaseDAO extends Minz_ModelPdo {
|
|||
return $list;
|
||||
}
|
||||
|
||||
public function size($all = false) {
|
||||
$db = FreshRSS_Context::$system_conf->db;
|
||||
$sql = 'SELECT SUM(data_length + index_length) FROM information_schema.TABLES WHERE table_schema=?'; //MySQL
|
||||
$values = array($db['base']);
|
||||
if (!$all) {
|
||||
$sql .= ' AND table_name LIKE ?';
|
||||
$values[] = $this->pdo->prefix() . '%';
|
||||
public function size(bool $all = false): int {
|
||||
$db = FreshRSS_Context::systemConf()->db;
|
||||
|
||||
// MariaDB does not refresh size information automatically
|
||||
$sql = <<<'SQL'
|
||||
ANALYZE TABLE `_category`, `_feed`, `_entry`, `_entrytmp`, `_tag`, `_entrytag`
|
||||
SQL;
|
||||
$stm = $this->pdo->query($sql);
|
||||
if ($stm !== false) {
|
||||
$stm->fetchAll();
|
||||
}
|
||||
$stm = $this->pdo->prepare($sql);
|
||||
$stm->execute($values);
|
||||
$res = $stm->fetchAll(PDO::FETCH_COLUMN, 0);
|
||||
return $res[0];
|
||||
|
||||
//MySQL:
|
||||
$sql = <<<'SQL'
|
||||
SELECT SUM(DATA_LENGTH + INDEX_LENGTH + DATA_FREE)
|
||||
FROM information_schema.TABLES WHERE TABLE_SCHEMA=:table_schema
|
||||
SQL;
|
||||
$values = [':table_schema' => $db['base']];
|
||||
if (!$all) {
|
||||
$sql .= ' AND table_name LIKE :table_name';
|
||||
$values[':table_name'] = $this->pdo->prefix() . '%';
|
||||
}
|
||||
$res = $this->fetchColumn($sql, 0, $values);
|
||||
return isset($res[0]) ? (int)($res[0]) : -1;
|
||||
}
|
||||
|
||||
public function optimize() {
|
||||
public function optimize(): bool {
|
||||
$ok = true;
|
||||
$tables = array('category', 'feed', 'entry', 'entrytmp', 'tag', 'entrytag');
|
||||
$tables = ['category', 'feed', 'entry', 'entrytmp', 'tag', 'entrytag'];
|
||||
|
||||
foreach ($tables as $table) {
|
||||
$sql = 'OPTIMIZE TABLE `_' . $table . '`'; //MySQL
|
||||
$stm = $this->pdo->query($sql);
|
||||
if ($stm == false || $stm->fetchAll(PDO::FETCH_ASSOC) === false) {
|
||||
if ($stm == false || $stm->fetchAll(PDO::FETCH_ASSOC) == false) {
|
||||
$ok = false;
|
||||
$info = $stm == null ? $this->pdo->errorInfo() : $stm->errorInfo();
|
||||
Minz_Log::warning(__METHOD__ . ' error: ' . $sql . ' : ' . json_encode($info));
|
||||
|
@ -169,30 +227,38 @@ class FreshRSS_DatabaseDAO extends Minz_ModelPdo {
|
|||
return $ok;
|
||||
}
|
||||
|
||||
public function ensureCaseInsensitiveGuids() {
|
||||
$ok = true;
|
||||
if ($this->pdo->dbType() === 'mysql') {
|
||||
include(APP_PATH . '/SQL/install.sql.mysql.php');
|
||||
|
||||
$ok = false;
|
||||
try {
|
||||
$ok = $this->pdo->exec($GLOBALS['SQL_UPDATE_GUID_LATIN1_BIN']) !== false; //FreshRSS 1.12
|
||||
} catch (Exception $e) {
|
||||
$ok = false;
|
||||
Minz_Log::error(__METHOD__ . ' error: ' . $e->getMessage());
|
||||
}
|
||||
}
|
||||
return $ok;
|
||||
}
|
||||
|
||||
public function minorDbMaintenance() {
|
||||
public function minorDbMaintenance(): void {
|
||||
$catDAO = FreshRSS_Factory::createCategoryDao();
|
||||
$catDAO->resetDefaultCategoryName();
|
||||
|
||||
$this->ensureCaseInsensitiveGuids();
|
||||
include_once(APP_PATH . '/SQL/install.sql.' . $this->pdo->dbType() . '.php');
|
||||
if (!empty($GLOBALS['SQL_UPDATE_MINOR'])) {
|
||||
$sql = $GLOBALS['SQL_UPDATE_MINOR'];
|
||||
$isMariaDB = false;
|
||||
|
||||
if ($this->pdo->dbType() === 'mysql') {
|
||||
$dbVersion = $this->fetchValue('SELECT version()') ?? '';
|
||||
$isMariaDB = stripos($dbVersion, 'MariaDB') !== false; // MariaDB includes its name in version, but not MySQL
|
||||
if (!$isMariaDB) {
|
||||
// MySQL does not support `DROP INDEX IF EXISTS` yet https://dev.mysql.com/doc/refman/8.3/en/drop-index.html
|
||||
// but MariaDB does https://mariadb.com/kb/en/drop-index/
|
||||
$sql = str_replace('DROP INDEX IF EXISTS', 'DROP INDEX', $sql);
|
||||
}
|
||||
}
|
||||
|
||||
if ($this->pdo->exec($sql) === false) {
|
||||
$info = $this->pdo->errorInfo();
|
||||
if ($this->pdo->dbType() === 'mysql' &&
|
||||
!$isMariaDB && !empty($info[2]) && (stripos($info[2], "Can't DROP ") !== false)) {
|
||||
// Too bad for MySQL, but ignore error
|
||||
return;
|
||||
}
|
||||
Minz_Log::error('SQL error ' . __METHOD__ . json_encode($this->pdo->errorInfo()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static function stdError($error) {
|
||||
private static function stdError(string $error): bool {
|
||||
if (defined('STDERR')) {
|
||||
fwrite(STDERR, $error . "\n");
|
||||
}
|
||||
|
@ -200,15 +266,16 @@ class FreshRSS_DatabaseDAO extends Minz_ModelPdo {
|
|||
return false;
|
||||
}
|
||||
|
||||
const SQLITE_EXPORT = 1;
|
||||
const SQLITE_IMPORT = 2;
|
||||
public const SQLITE_EXPORT = 1;
|
||||
public const SQLITE_IMPORT = 2;
|
||||
|
||||
public function dbCopy($filename, $mode, $clearFirst = false) {
|
||||
public function dbCopy(string $filename, int $mode, bool $clearFirst = false): bool {
|
||||
if (!extension_loaded('pdo_sqlite')) {
|
||||
return self::stdError('PHP extension pdo_sqlite is missing!');
|
||||
}
|
||||
$error = '';
|
||||
|
||||
$databaseDAO = FreshRSS_Factory::createDatabaseDAO();
|
||||
$userDAO = FreshRSS_Factory::createUserDao();
|
||||
$catDAO = FreshRSS_Factory::createCategoryDao();
|
||||
$feedDAO = FreshRSS_Factory::createFeedDao();
|
||||
|
@ -226,15 +293,18 @@ class FreshRSS_DatabaseDAO extends Minz_ModelPdo {
|
|||
$error = 'Error: SQLite import file is not readable: ' . $filename;
|
||||
} elseif ($clearFirst) {
|
||||
$userDAO->deleteUser();
|
||||
$userDAO = FreshRSS_Factory::createUserDao();
|
||||
if ($this->pdo->dbType() === 'sqlite') {
|
||||
//We cannot just delete the .sqlite file otherwise PDO gets buggy.
|
||||
//SQLite is the only one with database-level optimization, instead of at table level.
|
||||
$this->optimize();
|
||||
}
|
||||
} else {
|
||||
$nbEntries = $entryDAO->countUnreadRead();
|
||||
if (!empty($nbEntries['all'])) {
|
||||
$error = 'Error: Destination database already contains some entries!';
|
||||
if ($databaseDAO->exits()) {
|
||||
$nbEntries = $entryDAO->countUnreadRead();
|
||||
if (isset($nbEntries['all']) && $nbEntries['all'] > 0) {
|
||||
$error = 'Error: Destination database already contains some entries!';
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
@ -250,6 +320,7 @@ class FreshRSS_DatabaseDAO extends Minz_ModelPdo {
|
|||
|
||||
try {
|
||||
$sqlite = new Minz_PdoSqlite('sqlite:' . $filename);
|
||||
$sqlite->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_SILENT);
|
||||
} catch (Exception $e) {
|
||||
$error = 'Error while initialising SQLite copy: ' . $e->getMessage();
|
||||
return self::stdError($error);
|
||||
|
@ -278,7 +349,7 @@ class FreshRSS_DatabaseDAO extends Minz_ModelPdo {
|
|||
$tagFrom = $tagDAOSQLite; $tagTo = $tagDAO;
|
||||
break;
|
||||
default:
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
|
||||
$idMaps = [];
|
||||
|
@ -360,4 +431,31 @@ class FreshRSS_DatabaseDAO extends Minz_ModelPdo {
|
|||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure that some PDO columns are `int` and not `string`.
|
||||
* Compatibility with PHP 7.
|
||||
* @param array<string|int|null> $table
|
||||
* @param array<string> $columns
|
||||
*/
|
||||
public static function pdoInt(array &$table, array $columns): void {
|
||||
foreach ($columns as $column) {
|
||||
if (isset($table[$column]) && is_string($table[$column])) {
|
||||
$table[$column] = (int)$table[$column];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure that some PDO columns are `string` and not `bigint`.
|
||||
* @param array<string|int|null> $table
|
||||
* @param array<string> $columns
|
||||
*/
|
||||
public static function pdoString(array &$table, array $columns): void {
|
||||
foreach ($columns as $column) {
|
||||
if (isset($table[$column])) {
|
||||
$table[$column] = (string)$table[$column];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* This class is used to test database is well-constructed.
|
||||
|
@ -6,78 +7,82 @@
|
|||
class FreshRSS_DatabaseDAOPGSQL extends FreshRSS_DatabaseDAOSQLite {
|
||||
|
||||
//PostgreSQL error codes
|
||||
const UNDEFINED_COLUMN = '42703';
|
||||
const UNDEFINED_TABLE = '42P01';
|
||||
public const UNDEFINED_COLUMN = '42703';
|
||||
public const UNDEFINED_TABLE = '42P01';
|
||||
|
||||
public function tablesAreCorrect() {
|
||||
$db = FreshRSS_Context::$system_conf->db;
|
||||
$dbowner = $db['user'];
|
||||
$sql = 'SELECT * FROM pg_catalog.pg_tables where tableowner=?';
|
||||
$stm = $this->pdo->prepare($sql);
|
||||
$values = array($dbowner);
|
||||
$stm->execute($values);
|
||||
$res = $stm->fetchAll(PDO::FETCH_ASSOC);
|
||||
#[\Override]
|
||||
public function tablesAreCorrect(): bool {
|
||||
$db = FreshRSS_Context::systemConf()->db;
|
||||
$sql = 'SELECT * FROM pg_catalog.pg_tables where tableowner=:tableowner';
|
||||
$res = $this->fetchAssoc($sql, [':tableowner' => $db['user']]);
|
||||
if ($res == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$tables = array(
|
||||
$tables = [
|
||||
$this->pdo->prefix() . 'category' => false,
|
||||
$this->pdo->prefix() . 'feed' => false,
|
||||
$this->pdo->prefix() . 'entry' => false,
|
||||
$this->pdo->prefix() . 'entrytmp' => false,
|
||||
$this->pdo->prefix() . 'tag' => false,
|
||||
$this->pdo->prefix() . 'entrytag' => false,
|
||||
);
|
||||
];
|
||||
foreach ($res as $value) {
|
||||
$tables[array_pop($value)] = true;
|
||||
}
|
||||
|
||||
return count(array_keys($tables, true, true)) == count($tables);
|
||||
return count(array_keys($tables, true, true)) === count($tables);
|
||||
}
|
||||
|
||||
public function getSchema($table) {
|
||||
$sql = 'select column_name as field, data_type as type, column_default as default, is_nullable as null from INFORMATION_SCHEMA.COLUMNS where table_name = ?';
|
||||
$stm = $this->pdo->prepare($sql);
|
||||
$stm->execute(array($this->pdo->prefix() . $table));
|
||||
return $this->listDaoToSchema($stm->fetchAll(PDO::FETCH_ASSOC));
|
||||
/** @return array<array<string,string|int|bool|null>> */
|
||||
#[\Override]
|
||||
public function getSchema(string $table): array {
|
||||
$sql = <<<'SQL'
|
||||
SELECT column_name AS field, data_type AS type, column_default AS default, is_nullable AS null
|
||||
FROM INFORMATION_SCHEMA.COLUMNS WHERE table_name = :table_name
|
||||
SQL;
|
||||
$res = $this->fetchAssoc($sql, [':table_name' => $this->pdo->prefix() . $table]);
|
||||
return $res == null ? [] : $this->listDaoToSchema($res);
|
||||
}
|
||||
|
||||
public function daoToSchema($dao) {
|
||||
return array(
|
||||
'name' => $dao['field'],
|
||||
'type' => strtolower($dao['type']),
|
||||
/**
|
||||
* @param array<string,string|int|bool|null> $dao
|
||||
* @return array{'name':string,'type':string,'notnull':bool,'default':mixed}
|
||||
*/
|
||||
#[\Override]
|
||||
public function daoToSchema(array $dao): array {
|
||||
return [
|
||||
'name' => (string)($dao['field']),
|
||||
'type' => strtolower((string)($dao['type'])),
|
||||
'notnull' => (bool)$dao['null'],
|
||||
'default' => $dao['default'],
|
||||
);
|
||||
];
|
||||
}
|
||||
|
||||
public function size($all = false) {
|
||||
#[\Override]
|
||||
public function size(bool $all = false): int {
|
||||
if ($all) {
|
||||
$db = FreshRSS_Context::$system_conf->db;
|
||||
$sql = 'SELECT pg_database_size(:base)';
|
||||
$stm = $this->pdo->prepare($sql);
|
||||
$stm->bindParam(':base', $db['base']);
|
||||
$stm->execute();
|
||||
$db = FreshRSS_Context::systemConf()->db;
|
||||
$res = $this->fetchColumn('SELECT pg_database_size(:base)', 0, [':base' => $db['base']]);
|
||||
} else {
|
||||
$sql = <<<SQL
|
||||
SELECT
|
||||
pg_total_relation_size('`{$this->pdo->prefix()}category`')
|
||||
pg_total_relation_size('`{$this->pdo->prefix()}feed`')
|
||||
pg_total_relation_size('`{$this->pdo->prefix()}entry`')
|
||||
pg_total_relation_size('`{$this->pdo->prefix()}entrytmp`')
|
||||
pg_total_relation_size('`{$this->pdo->prefix()}tag`')
|
||||
pg_total_relation_size('`{$this->pdo->prefix()}category`') +
|
||||
pg_total_relation_size('`{$this->pdo->prefix()}feed`') +
|
||||
pg_total_relation_size('`{$this->pdo->prefix()}entry`') +
|
||||
pg_total_relation_size('`{$this->pdo->prefix()}entrytmp`') +
|
||||
pg_total_relation_size('`{$this->pdo->prefix()}tag`') +
|
||||
pg_total_relation_size('`{$this->pdo->prefix()}entrytag`')
|
||||
SQL;
|
||||
$stm = $this->pdo->query($sql);
|
||||
$res = $this->fetchColumn($sql, 0);
|
||||
}
|
||||
if ($stm == false) {
|
||||
return 0;
|
||||
}
|
||||
$res = $stm->fetchAll(PDO::FETCH_COLUMN, 0);
|
||||
return $res[0];
|
||||
return (int)($res[0] ?? -1);
|
||||
}
|
||||
|
||||
public function optimize() {
|
||||
#[\Override]
|
||||
public function optimize(): bool {
|
||||
$ok = true;
|
||||
$tables = array('category', 'feed', 'entry', 'entrytmp', 'tag', 'entrytag');
|
||||
$tables = ['category', 'feed', 'entry', 'entrytmp', 'tag', 'entrytag'];
|
||||
|
||||
foreach ($tables as $table) {
|
||||
$sql = 'VACUUM `_' . $table . '`';
|
||||
|
|
|
@ -1,22 +1,28 @@
|
|||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* This class is used to test database is well-constructed (SQLite).
|
||||
*/
|
||||
class FreshRSS_DatabaseDAOSQLite extends FreshRSS_DatabaseDAO {
|
||||
public function tablesAreCorrect() {
|
||||
|
||||
#[\Override]
|
||||
public function tablesAreCorrect(): bool {
|
||||
$sql = 'SELECT name FROM sqlite_master WHERE type="table"';
|
||||
$stm = $this->pdo->query($sql);
|
||||
$res = $stm->fetchAll(PDO::FETCH_ASSOC);
|
||||
$res = $stm ? $stm->fetchAll(PDO::FETCH_ASSOC) : false;
|
||||
if ($res === false) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$tables = array(
|
||||
$tables = [
|
||||
$this->pdo->prefix() . 'category' => false,
|
||||
$this->pdo->prefix() . 'feed' => false,
|
||||
$this->pdo->prefix() . 'entry' => false,
|
||||
$this->pdo->prefix() . 'entrytmp' => false,
|
||||
$this->pdo->prefix() . 'tag' => false,
|
||||
$this->pdo->prefix() . 'entrytag' => false,
|
||||
);
|
||||
];
|
||||
foreach ($res as $value) {
|
||||
$tables[$value['name']] = true;
|
||||
}
|
||||
|
@ -24,48 +30,57 @@ class FreshRSS_DatabaseDAOSQLite extends FreshRSS_DatabaseDAO {
|
|||
return count(array_keys($tables, true, true)) == count($tables);
|
||||
}
|
||||
|
||||
public function getSchema($table) {
|
||||
/** @return array<array<string,string|int|bool|null>> */
|
||||
#[\Override]
|
||||
public function getSchema(string $table): array {
|
||||
$sql = 'PRAGMA table_info(' . $table . ')';
|
||||
$stm = $this->pdo->query($sql);
|
||||
return $this->listDaoToSchema($stm->fetchAll(PDO::FETCH_ASSOC));
|
||||
return $stm ? $this->listDaoToSchema($stm->fetchAll(PDO::FETCH_ASSOC) ?: []) : [];
|
||||
}
|
||||
|
||||
public function entryIsCorrect() {
|
||||
return $this->checkTable('entry', array(
|
||||
'id', 'guid', 'title', 'author', 'content', 'link', 'date', 'lastSeen', 'hash', 'is_read',
|
||||
'is_favorite', 'id_feed', 'tags',
|
||||
));
|
||||
#[\Override]
|
||||
public function entryIsCorrect(): bool {
|
||||
return $this->checkTable('entry', [
|
||||
'id', 'guid', 'title', 'author', 'content', 'link', 'date', 'lastSeen', 'hash', 'is_read', 'is_favorite', 'id_feed', 'tags',
|
||||
]);
|
||||
}
|
||||
|
||||
public function entrytmpIsCorrect() {
|
||||
return $this->checkTable('entrytmp', array(
|
||||
'id', 'guid', 'title', 'author', 'content', 'link', 'date', 'lastSeen', 'hash', 'is_read',
|
||||
'is_favorite', 'id_feed', 'tags',
|
||||
));
|
||||
#[\Override]
|
||||
public function entrytmpIsCorrect(): bool {
|
||||
return $this->checkTable('entrytmp', [
|
||||
'id', 'guid', 'title', 'author', 'content', 'link', 'date', 'lastSeen', 'hash', 'is_read', 'is_favorite', 'id_feed', 'tags'
|
||||
]);
|
||||
}
|
||||
|
||||
public function daoToSchema($dao) {
|
||||
return array(
|
||||
'name' => $dao['name'],
|
||||
'type' => strtolower($dao['type']),
|
||||
'notnull' => $dao['notnull'] === '1' ? true : false,
|
||||
/**
|
||||
* @param array<string,string|int|bool|null> $dao
|
||||
* @return array{'name':string,'type':string,'notnull':bool,'default':mixed}
|
||||
*/
|
||||
#[\Override]
|
||||
public function daoToSchema(array $dao): array {
|
||||
return [
|
||||
'name' => (string)$dao['name'],
|
||||
'type' => strtolower((string)$dao['type']),
|
||||
'notnull' => $dao['notnull'] == '1' ? true : false,
|
||||
'default' => $dao['dflt_value'],
|
||||
);
|
||||
];
|
||||
}
|
||||
|
||||
public function size($all = false) {
|
||||
#[\Override]
|
||||
public function size(bool $all = false): int {
|
||||
$sum = 0;
|
||||
if ($all) {
|
||||
foreach (glob(DATA_PATH . '/users/*/db.sqlite') as $filename) {
|
||||
$sum += @filesize($filename);
|
||||
foreach (glob(DATA_PATH . '/users/*/db.sqlite') ?: [] as $filename) {
|
||||
$sum += (@filesize($filename) ?: 0);
|
||||
}
|
||||
} else {
|
||||
$sum = @filesize(DATA_PATH . '/users/' . $this->current_user . '/db.sqlite');
|
||||
$sum = (@filesize(DATA_PATH . '/users/' . $this->current_user . '/db.sqlite') ?: 0);
|
||||
}
|
||||
return $sum;
|
||||
}
|
||||
|
||||
public function optimize() {
|
||||
#[\Override]
|
||||
public function optimize(): bool {
|
||||
$ok = $this->pdo->exec('VACUUM') !== false;
|
||||
if (!$ok) {
|
||||
$info = $this->pdo->errorInfo();
|
||||
|
|
|
@ -1,7 +1,8 @@
|
|||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
class FreshRSS_Days {
|
||||
const TODAY = 0;
|
||||
const YESTERDAY = 1;
|
||||
const BEFORE_YESTERDAY = 2;
|
||||
public const TODAY = 0;
|
||||
public const YESTERDAY = 1;
|
||||
public const BEFORE_YESTERDAY = 2;
|
||||
}
|
||||
|
|
1034
app/Models/Entry.php
1034
app/Models/Entry.php
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
|
@ -1,42 +1,46 @@
|
|||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
class FreshRSS_EntryDAOPGSQL extends FreshRSS_EntryDAOSQLite {
|
||||
|
||||
public function hasNativeHex() {
|
||||
#[\Override]
|
||||
public static function hasNativeHex(): bool {
|
||||
return true;
|
||||
}
|
||||
|
||||
public function sqlHexDecode($x) {
|
||||
#[\Override]
|
||||
public static function sqlHexDecode(string $x): string {
|
||||
return 'decode(' . $x . ", 'hex')";
|
||||
}
|
||||
|
||||
public function sqlHexEncode($x) {
|
||||
#[\Override]
|
||||
public static function sqlHexEncode(string $x): string {
|
||||
return 'encode(' . $x . ", 'hex')";
|
||||
}
|
||||
|
||||
public function sqlIgnoreConflict($sql) {
|
||||
#[\Override]
|
||||
public static function sqlIgnoreConflict(string $sql): string {
|
||||
return rtrim($sql, ' ;') . ' ON CONFLICT DO NOTHING';
|
||||
}
|
||||
|
||||
protected function autoUpdateDb($errorInfo) {
|
||||
/** @param array<string|int> $errorInfo */
|
||||
#[\Override]
|
||||
protected function autoUpdateDb(array $errorInfo): bool {
|
||||
if (isset($errorInfo[0])) {
|
||||
if ($errorInfo[0] === FreshRSS_DatabaseDAOPGSQL::UNDEFINED_TABLE) {
|
||||
if (stripos($errorInfo[2], 'tag') !== false) {
|
||||
$tagDAO = FreshRSS_Factory::createTagDao();
|
||||
return $tagDAO->createTagTable(); //v1.12.0
|
||||
} elseif (stripos($errorInfo[2], 'entrytmp') !== false) {
|
||||
return $this->createEntryTempTable(); //v1.7.0
|
||||
if ($errorInfo[0] === FreshRSS_DatabaseDAO::ER_BAD_FIELD_ERROR || $errorInfo[0] === FreshRSS_DatabaseDAOPGSQL::UNDEFINED_COLUMN) {
|
||||
$errorLines = explode("\n", (string)$errorInfo[2], 2); // The relevant column name is on the first line, other lines are noise
|
||||
foreach (['attributes'] as $column) {
|
||||
if (stripos($errorLines[0], $column) !== false) {
|
||||
return $this->addColumn($column);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
protected function addColumn($name) {
|
||||
return false;
|
||||
}
|
||||
|
||||
public function commitNewEntries() {
|
||||
#[\Override]
|
||||
public function commitNewEntries(): bool {
|
||||
//TODO: Update to PostgreSQL 9.5+ syntax with ON CONFLICT DO NOTHING
|
||||
$sql = 'DO $$
|
||||
DECLARE
|
||||
|
@ -44,14 +48,14 @@ maxrank bigint := (SELECT MAX(id) FROM `_entrytmp`);
|
|||
rank bigint := (SELECT maxrank - COUNT(*) FROM `_entrytmp`);
|
||||
BEGIN
|
||||
INSERT INTO `_entry`
|
||||
(id, guid, title, author, content, link, date, `lastSeen`, hash, is_read, is_favorite, id_feed, tags)
|
||||
(SELECT rank + row_number() OVER(ORDER BY date) AS id, guid, title, author, content,
|
||||
link, date, `lastSeen`, hash, is_read, is_favorite, id_feed, tags
|
||||
(id, guid, title, author, content, link, date, `lastSeen`, hash, is_read, is_favorite, id_feed, tags, attributes)
|
||||
(SELECT rank + row_number() OVER(ORDER BY date, id) AS id, guid, title, author, content,
|
||||
link, date, `lastSeen`, hash, is_read, is_favorite, id_feed, tags, attributes
|
||||
FROM `_entrytmp` AS etmp
|
||||
WHERE NOT EXISTS (
|
||||
SELECT 1 FROM `_entry` AS ereal
|
||||
WHERE (etmp.id = ereal.id) OR (etmp.id_feed = ereal.id_feed AND etmp.guid = ereal.guid))
|
||||
ORDER BY date);
|
||||
ORDER BY date, id);
|
||||
DELETE FROM `_entrytmp` WHERE id <= maxrank;
|
||||
END $$;';
|
||||
$hadTransaction = $this->pdo->inTransaction();
|
||||
|
|
|
@ -1,63 +1,71 @@
|
|||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
class FreshRSS_EntryDAOSQLite extends FreshRSS_EntryDAO {
|
||||
|
||||
public function isCompressed() {
|
||||
#[\Override]
|
||||
public static function isCompressed(): bool {
|
||||
return false;
|
||||
}
|
||||
|
||||
public function hasNativeHex() {
|
||||
#[\Override]
|
||||
public static function hasNativeHex(): bool {
|
||||
return false;
|
||||
}
|
||||
|
||||
public function sqlHexDecode($x) {
|
||||
#[\Override]
|
||||
protected static function sqlConcat(string $s1, string $s2): string {
|
||||
return $s1 . '||' . $s2;
|
||||
}
|
||||
|
||||
#[\Override]
|
||||
public static function sqlHexDecode(string $x): string {
|
||||
return $x;
|
||||
}
|
||||
|
||||
public function sqlIgnoreConflict($sql) {
|
||||
#[\Override]
|
||||
public static function sqlIgnoreConflict(string $sql): string {
|
||||
return str_replace('INSERT INTO ', 'INSERT OR IGNORE INTO ', $sql);
|
||||
}
|
||||
|
||||
protected function autoUpdateDb($errorInfo) {
|
||||
if ($tableInfo = $this->pdo->query("SELECT sql FROM sqlite_master where name='tag'")) {
|
||||
$showCreate = $tableInfo->fetchColumn();
|
||||
if (stripos($showCreate, 'tag') === false) {
|
||||
$tagDAO = FreshRSS_Factory::createTagDao();
|
||||
return $tagDAO->createTagTable(); //v1.12.0
|
||||
}
|
||||
}
|
||||
if ($tableInfo = $this->pdo->query("SELECT sql FROM sqlite_master where name='entrytmp'")) {
|
||||
$showCreate = $tableInfo->fetchColumn();
|
||||
if (stripos($showCreate, 'entrytmp') === false) {
|
||||
return $this->createEntryTempTable(); //v1.7.0
|
||||
/** @param array<string|int> $errorInfo */
|
||||
#[\Override]
|
||||
protected function autoUpdateDb(array $errorInfo): bool {
|
||||
if ($tableInfo = $this->pdo->query("PRAGMA table_info('entry')")) {
|
||||
$columns = $tableInfo->fetchAll(PDO::FETCH_COLUMN, 1) ?: [];
|
||||
foreach (['attributes'] as $column) {
|
||||
if (!in_array($column, $columns, true)) {
|
||||
return $this->addColumn($column);
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public function commitNewEntries() {
|
||||
$sql = '
|
||||
#[\Override]
|
||||
public function commitNewEntries(): bool {
|
||||
$sql = <<<'SQL'
|
||||
DROP TABLE IF EXISTS `tmp`;
|
||||
CREATE TEMP TABLE `tmp` AS
|
||||
SELECT id, guid, title, author, content, link, date, `lastSeen`, hash, is_read, is_favorite, id_feed, tags
|
||||
SELECT id, guid, title, author, content, link, date, `lastSeen`, hash, is_read, is_favorite, id_feed, tags, attributes
|
||||
FROM `_entrytmp`
|
||||
ORDER BY date;
|
||||
ORDER BY date, id;
|
||||
INSERT OR IGNORE INTO `_entry`
|
||||
(id, guid, title, author, content, link, date, `lastSeen`, hash, is_read, is_favorite, id_feed, tags)
|
||||
(id, guid, title, author, content, link, date, `lastSeen`, hash, is_read, is_favorite, id_feed, tags, attributes)
|
||||
SELECT rowid + (SELECT MAX(id) - COUNT(*) FROM `tmp`) AS id,
|
||||
guid, title, author, content, link, date, `lastSeen`, hash, is_read, is_favorite, id_feed, tags
|
||||
guid, title, author, content, link, date, `lastSeen`, hash, is_read, is_favorite, id_feed, tags, attributes
|
||||
FROM `tmp`
|
||||
ORDER BY date;
|
||||
ORDER BY date, id;
|
||||
DELETE FROM `_entrytmp` WHERE id <= (SELECT MAX(id) FROM `tmp`);
|
||||
DROP TABLE IF EXISTS `tmp`;
|
||||
';
|
||||
SQL;
|
||||
$hadTransaction = $this->pdo->inTransaction();
|
||||
if (!$hadTransaction) {
|
||||
$this->pdo->beginTransaction();
|
||||
}
|
||||
$result = $this->pdo->exec($sql) !== false;
|
||||
if (!$result) {
|
||||
Minz_Log::error('SQL error commitNewEntries: ' . json_encode($this->pdo->errorInfo()));
|
||||
Minz_Log::error('SQL error ' . __METHOD__ . json_encode($this->pdo->errorInfo()));
|
||||
}
|
||||
if (!$hadTransaction) {
|
||||
$this->pdo->commit();
|
||||
|
@ -65,70 +73,33 @@ DROP TABLE IF EXISTS `tmp`;
|
|||
return $result;
|
||||
}
|
||||
|
||||
protected function sqlConcat($s1, $s2) {
|
||||
return $s1 . '||' . $s2;
|
||||
}
|
||||
|
||||
protected function updateCacheUnreads($catId = false, $feedId = false) {
|
||||
$sql = 'UPDATE `_feed` '
|
||||
. 'SET `cache_nbUnreads`=('
|
||||
. 'SELECT COUNT(*) AS nbUnreads FROM `_entry` e '
|
||||
. 'WHERE e.id_feed=`_feed`.id AND e.is_read=0)';
|
||||
$hasWhere = false;
|
||||
$values = array();
|
||||
if ($feedId !== false) {
|
||||
$sql .= $hasWhere ? ' AND' : ' WHERE';
|
||||
$hasWhere = true;
|
||||
$sql .= ' id=?';
|
||||
$values[] = $feedId;
|
||||
}
|
||||
if ($catId !== false) {
|
||||
$sql .= $hasWhere ? ' AND' : ' WHERE';
|
||||
$hasWhere = true;
|
||||
$sql .= ' category=?';
|
||||
$values[] = $catId;
|
||||
}
|
||||
$stm = $this->pdo->prepare($sql);
|
||||
if ($stm && $stm->execute($values)) {
|
||||
return true;
|
||||
} else {
|
||||
$info = $stm == null ? $this->pdo->errorInfo() : $stm->errorInfo();
|
||||
Minz_Log::error('SQL error updateCacheUnreads: ' . $info[2]);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggle the read marker on one or more article.
|
||||
* Then the cache is updated.
|
||||
*
|
||||
* @todo change the way the query is build because it seems there is
|
||||
* unnecessary code in here. For instance, the part with the str_repeat.
|
||||
* @todo remove code duplication. It seems the code is basically the
|
||||
* same if it is an array or not.
|
||||
*
|
||||
* @param integer|array $ids
|
||||
* @param boolean $is_read
|
||||
* @return integer affected rows
|
||||
* @param string|array<string> $ids
|
||||
* @param bool $is_read
|
||||
* @return int|false affected rows
|
||||
*/
|
||||
public function markRead($ids, $is_read = true) {
|
||||
#[\Override]
|
||||
public function markRead($ids, bool $is_read = true) {
|
||||
FreshRSS_UserDAO::touch();
|
||||
if (is_array($ids)) { //Many IDs at once (used by API)
|
||||
//if (true) { //Speed heuristics //TODO: Not implemented yet for SQLite (so always call IDs one by one)
|
||||
$affected = 0;
|
||||
foreach ($ids as $id) {
|
||||
$affected += $this->markRead($id, $is_read);
|
||||
$affected += ($this->markRead($id, $is_read) ?: 0);
|
||||
}
|
||||
return $affected;
|
||||
//}
|
||||
} else {
|
||||
$this->pdo->beginTransaction();
|
||||
$sql = 'UPDATE `_entry` SET is_read=? WHERE id=? AND is_read=?';
|
||||
$values = array($is_read ? 1 : 0, $ids, $is_read ? 0 : 1);
|
||||
$values = [$is_read ? 1 : 0, $ids, $is_read ? 0 : 1];
|
||||
$stm = $this->pdo->prepare($sql);
|
||||
if (!($stm && $stm->execute($values))) {
|
||||
$info = $stm == null ? $this->pdo->errorInfo() : $stm->errorInfo();
|
||||
Minz_Log::error('SQL error markRead 1: ' . $info[2]);
|
||||
Minz_Log::error('SQL error ' . __METHOD__ . ' A ' . json_encode($info));
|
||||
$this->pdo->rollBack();
|
||||
return false;
|
||||
}
|
||||
|
@ -136,11 +107,11 @@ DROP TABLE IF EXISTS `tmp`;
|
|||
if ($affected > 0) {
|
||||
$sql = 'UPDATE `_feed` SET `cache_nbUnreads`=`cache_nbUnreads`' . ($is_read ? '-' : '+') . '1 '
|
||||
. 'WHERE id=(SELECT e.id_feed FROM `_entry` e WHERE e.id=?)';
|
||||
$values = array($ids);
|
||||
$values = [$ids];
|
||||
$stm = $this->pdo->prepare($sql);
|
||||
if (!($stm && $stm->execute($values))) {
|
||||
$info = $stm == null ? $this->pdo->errorInfo() : $stm->errorInfo();
|
||||
Minz_Log::error('SQL error markRead 2: ' . $info[2]);
|
||||
Minz_Log::error('SQL error ' . __METHOD__ . ' B ' . json_encode($info));
|
||||
$this->pdo->rollBack();
|
||||
return false;
|
||||
}
|
||||
|
@ -150,103 +121,14 @@ DROP TABLE IF EXISTS `tmp`;
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark all entries as read depending on parameters.
|
||||
* If $onlyFavorites is true, it is used when the user mark as read in
|
||||
* the favorite pseudo-category.
|
||||
* If $priorityMin is greater than 0, it is used when the user mark as
|
||||
* read in the main feed pseudo-category.
|
||||
* Then the cache is updated.
|
||||
*
|
||||
* If $idMax equals 0, a deprecated debug message is logged
|
||||
*
|
||||
* @todo refactor this method along with markReadCat and markReadFeed
|
||||
* since they are all doing the same thing. I think we need to build a
|
||||
* tool to generate the query instead of having queries all over the
|
||||
* place. It will be reused also for the filtering making every thing
|
||||
* separated.
|
||||
*
|
||||
* @param integer $idMax fail safe article ID
|
||||
* @param boolean $onlyFavorites
|
||||
* @param integer $priorityMin
|
||||
* @return integer affected rows
|
||||
*/
|
||||
public function markReadEntries($idMax = 0, $onlyFavorites = false, $priorityMin = 0, $filters = null, $state = 0, $is_read = true) {
|
||||
FreshRSS_UserDAO::touch();
|
||||
if ($idMax == 0) {
|
||||
$idMax = time() . '000000';
|
||||
Minz_Log::debug('Calling markReadEntries(0) is deprecated!');
|
||||
}
|
||||
|
||||
$sql = 'UPDATE `_entry` SET is_read = ? WHERE is_read <> ? AND id <= ?';
|
||||
if ($onlyFavorites) {
|
||||
$sql .= ' AND is_favorite=1';
|
||||
} elseif ($priorityMin >= 0) {
|
||||
$sql .= ' AND id_feed IN (SELECT f.id FROM `_feed` f WHERE f.priority > ' . intval($priorityMin) . ')';
|
||||
}
|
||||
$values = array($is_read ? 1 : 0, $is_read ? 1 : 0, $idMax);
|
||||
|
||||
list($searchValues, $search) = $this->sqlListEntriesWhere('', $filters, $state);
|
||||
|
||||
$stm = $this->pdo->prepare($sql . $search);
|
||||
if (!($stm && $stm->execute(array_merge($values, $searchValues)))) {
|
||||
$info = $stm == null ? $this->pdo->errorInfo() : $stm->errorInfo();
|
||||
Minz_Log::error('SQL error markReadEntries: ' . $info[2]);
|
||||
return false;
|
||||
}
|
||||
$affected = $stm->rowCount();
|
||||
if (($affected > 0) && (!$this->updateCacheUnreads(false, false))) {
|
||||
return false;
|
||||
}
|
||||
return $affected;
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark all the articles in a category as read.
|
||||
* There is a fail safe to prevent to mark as read articles that are
|
||||
* loaded during the mark as read action. Then the cache is updated.
|
||||
*
|
||||
* If $idMax equals 0, a deprecated debug message is logged
|
||||
*
|
||||
* @param integer $id category ID
|
||||
* @param integer $idMax fail safe article ID
|
||||
* @return integer affected rows
|
||||
*/
|
||||
public function markReadCat($id, $idMax = 0, $filters = null, $state = 0, $is_read = true) {
|
||||
FreshRSS_UserDAO::touch();
|
||||
if ($idMax == 0) {
|
||||
$idMax = time() . '000000';
|
||||
Minz_Log::debug('Calling markReadCat(0) is deprecated!');
|
||||
}
|
||||
|
||||
$sql = 'UPDATE `_entry` '
|
||||
. 'SET is_read = ? '
|
||||
. 'WHERE is_read <> ? AND id <= ? AND '
|
||||
. 'id_feed IN (SELECT f.id FROM `_feed` f WHERE f.category=?)';
|
||||
$values = array($is_read ? 1 : 0, $is_read ? 1 : 0, $idMax, $id);
|
||||
|
||||
list($searchValues, $search) = $this->sqlListEntriesWhere('', $filters, $state);
|
||||
|
||||
$stm = $this->pdo->prepare($sql . $search);
|
||||
if (!($stm && $stm->execute(array_merge($values, $searchValues)))) {
|
||||
$info = $stm == null ? $this->pdo->errorInfo() : $stm->errorInfo();
|
||||
Minz_Log::error('SQL error markReadCat: ' . $info[2]);
|
||||
return false;
|
||||
}
|
||||
$affected = $stm->rowCount();
|
||||
if (($affected > 0) && (!$this->updateCacheUnreads($id, false))) {
|
||||
return false;
|
||||
}
|
||||
return $affected;
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark all the articles in a tag as read.
|
||||
* @param integer $id tag ID, or empty for targetting any tag
|
||||
* @param integer $idMax max article ID
|
||||
* @return integer affected rows
|
||||
* @param int $id tag ID, or empty for targeting any tag
|
||||
* @param string $idMax max article ID
|
||||
* @return int|false affected rows
|
||||
*/
|
||||
public function markReadTag($id = 0, $idMax = 0, $filters = null, $state = 0, $is_read = true) {
|
||||
#[\Override]
|
||||
public function markReadTag($id = 0, string $idMax = '0', ?FreshRSS_BooleanSearch $filters = null, int $state = 0, bool $is_read = true) {
|
||||
FreshRSS_UserDAO::touch();
|
||||
if ($idMax == 0) {
|
||||
$idMax = time() . '000000';
|
||||
|
@ -259,21 +141,21 @@ DROP TABLE IF EXISTS `tmp`;
|
|||
. 'id IN (SELECT et.id_entry FROM `_entrytag` et '
|
||||
. ($id == 0 ? '' : 'WHERE et.id_tag = ?')
|
||||
. ')';
|
||||
$values = array($is_read ? 1 : 0, $is_read ? 1 : 0, $idMax);
|
||||
$values = [$is_read ? 1 : 0, $is_read ? 1 : 0, $idMax];
|
||||
if ($id != 0) {
|
||||
$values[] = $id;
|
||||
}
|
||||
|
||||
list($searchValues, $search) = $this->sqlListEntriesWhere('e.', $filters, $state);
|
||||
[$searchValues, $search] = $this->sqlListEntriesWhere('e.', $filters, $state);
|
||||
|
||||
$stm = $this->pdo->prepare($sql . $search);
|
||||
if (!($stm && $stm->execute(array_merge($values, $searchValues)))) {
|
||||
$info = $stm == null ? $this->pdo->errorInfo() : $stm->errorInfo();
|
||||
Minz_Log::error('SQL error markReadTag: ' . $info[2]);
|
||||
Minz_Log::error('SQL error ' . __METHOD__ . json_encode($info));
|
||||
return false;
|
||||
}
|
||||
$affected = $stm->rowCount();
|
||||
if (($affected > 0) && (!$this->updateCacheUnreads(false, false))) {
|
||||
if (($affected > 0) && (!$this->updateCacheUnreads(null, null))) {
|
||||
return false;
|
||||
}
|
||||
return $affected;
|
||||
|
|
|
@ -1,13 +1,20 @@
|
|||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
class FreshRSS_Factory {
|
||||
|
||||
public static function createUserDao($username = null) {
|
||||
/**
|
||||
* @throws Minz_ConfigurationNamespaceException|Minz_PDOConnectionException
|
||||
*/
|
||||
public static function createUserDao(?string $username = null): FreshRSS_UserDAO {
|
||||
return new FreshRSS_UserDAO($username);
|
||||
}
|
||||
|
||||
public static function createCategoryDao($username = null) {
|
||||
switch (FreshRSS_Context::$system_conf->db['type']) {
|
||||
/**
|
||||
* @throws Minz_ConfigurationNamespaceException|Minz_PDOConnectionException
|
||||
*/
|
||||
public static function createCategoryDao(?string $username = null): FreshRSS_CategoryDAO {
|
||||
switch (FreshRSS_Context::systemConf()->db['type'] ?? '') {
|
||||
case 'sqlite':
|
||||
return new FreshRSS_CategoryDAOSQLite($username);
|
||||
default:
|
||||
|
@ -15,8 +22,11 @@ class FreshRSS_Factory {
|
|||
}
|
||||
}
|
||||
|
||||
public static function createFeedDao($username = null) {
|
||||
switch (FreshRSS_Context::$system_conf->db['type']) {
|
||||
/**
|
||||
* @throws Minz_ConfigurationNamespaceException|Minz_PDOConnectionException
|
||||
*/
|
||||
public static function createFeedDao(?string $username = null): FreshRSS_FeedDAO {
|
||||
switch (FreshRSS_Context::systemConf()->db['type'] ?? '') {
|
||||
case 'sqlite':
|
||||
return new FreshRSS_FeedDAOSQLite($username);
|
||||
default:
|
||||
|
@ -24,8 +34,11 @@ class FreshRSS_Factory {
|
|||
}
|
||||
}
|
||||
|
||||
public static function createEntryDao($username = null) {
|
||||
switch (FreshRSS_Context::$system_conf->db['type']) {
|
||||
/**
|
||||
* @throws Minz_ConfigurationNamespaceException|Minz_PDOConnectionException
|
||||
*/
|
||||
public static function createEntryDao(?string $username = null): FreshRSS_EntryDAO {
|
||||
switch (FreshRSS_Context::systemConf()->db['type'] ?? '') {
|
||||
case 'sqlite':
|
||||
return new FreshRSS_EntryDAOSQLite($username);
|
||||
case 'pgsql':
|
||||
|
@ -35,8 +48,11 @@ class FreshRSS_Factory {
|
|||
}
|
||||
}
|
||||
|
||||
public static function createTagDao($username = null) {
|
||||
switch (FreshRSS_Context::$system_conf->db['type']) {
|
||||
/**
|
||||
* @throws Minz_ConfigurationNamespaceException|Minz_PDOConnectionException
|
||||
*/
|
||||
public static function createTagDao(?string $username = null): FreshRSS_TagDAO {
|
||||
switch (FreshRSS_Context::systemConf()->db['type'] ?? '') {
|
||||
case 'sqlite':
|
||||
return new FreshRSS_TagDAOSQLite($username);
|
||||
case 'pgsql':
|
||||
|
@ -46,8 +62,11 @@ class FreshRSS_Factory {
|
|||
}
|
||||
}
|
||||
|
||||
public static function createStatsDAO($username = null) {
|
||||
switch (FreshRSS_Context::$system_conf->db['type']) {
|
||||
/**
|
||||
* @throws Minz_ConfigurationNamespaceException|Minz_PDOConnectionException
|
||||
*/
|
||||
public static function createStatsDAO(?string $username = null): FreshRSS_StatsDAO {
|
||||
switch (FreshRSS_Context::systemConf()->db['type'] ?? '') {
|
||||
case 'sqlite':
|
||||
return new FreshRSS_StatsDAOSQLite($username);
|
||||
case 'pgsql':
|
||||
|
@ -57,8 +76,11 @@ class FreshRSS_Factory {
|
|||
}
|
||||
}
|
||||
|
||||
public static function createDatabaseDAO($username = null) {
|
||||
switch (FreshRSS_Context::$system_conf->db['type']) {
|
||||
/**
|
||||
* @throws Minz_ConfigurationNamespaceException|Minz_PDOConnectionException
|
||||
*/
|
||||
public static function createDatabaseDAO(?string $username = null): FreshRSS_DatabaseDAO {
|
||||
switch (FreshRSS_Context::systemConf()->db['type'] ?? '') {
|
||||
case 'sqlite':
|
||||
return new FreshRSS_DatabaseDAOSQLite($username);
|
||||
case 'pgsql':
|
||||
|
|
1051
app/Models/Feed.php
1051
app/Models/Feed.php
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue