mirror of
https://github.com/neocities/neocities.git
synced 2025-04-28 11:12:30 +02:00
Compare commits
2434 commits
Author | SHA1 | Date | |
---|---|---|---|
|
369f1655a9 | ||
|
380bd5f484 | ||
|
b7bef2bf55 | ||
|
e21e20b32e | ||
|
9479ca05d0 | ||
|
51ef1c3b2c | ||
|
416e82ee65 | ||
|
86c0938b50 | ||
|
818d520faf | ||
|
6662774be1 | ||
|
0c2132fe5e | ||
|
739a797a2e | ||
|
3e39b6f5c7 | ||
|
118d68ba6a | ||
|
5c142cdee9 | ||
|
bb03385a28 | ||
|
3ccb4ace37 | ||
|
f4fcf94b1a | ||
|
2f0f5c2c01 | ||
|
e35af8c058 | ||
|
e50f8e42a5 | ||
|
da984bf29b | ||
|
b08847b723 | ||
|
fabd2e6a69 | ||
|
833030cff8 | ||
|
c17153ef33 | ||
|
4001c5236a | ||
|
321f8c6e29 | ||
|
2a76e75c6f | ||
|
3419337628 | ||
|
fcc9f66076 | ||
|
e00f991c4c | ||
|
ca428a22da | ||
|
180c6adbd0 | ||
|
9cf07eec75 | ||
|
1584bb0865 | ||
|
03b052a3a7 | ||
|
39aa74964a | ||
|
b766432974 | ||
|
e7cda8f410 | ||
|
124671d2e3 | ||
|
31a738b5d6 | ||
|
bd9024f6a9 | ||
|
b21d15e285 | ||
|
6e49ac23cd | ||
|
91055a2feb | ||
|
a81a708be5 | ||
|
2dd1f55119 | ||
|
2cb5fb2399 | ||
|
f8f914879c | ||
|
98acf7ce89 | ||
|
f7c367b939 | ||
|
4fcabe5a35 | ||
|
cf712b7175 | ||
|
514836fd4f | ||
|
f3a2116b6e | ||
|
92902696ab | ||
|
1bddcd4cfb | ||
|
bb30252000 | ||
|
2cfc848ab2 | ||
|
db35971217 | ||
|
d63467c4ca | ||
|
a854244c33 | ||
|
16fb3444fa | ||
|
44644cc7fa | ||
|
1463074f35 | ||
|
afef407744 | ||
|
40bef7d125 | ||
|
aae94f2078 | ||
|
3767725759 | ||
|
4876708d99 | ||
|
eec3adfd23 | ||
|
84f916c6bd | ||
|
b495761db4 | ||
|
dccd03a941 | ||
|
9f24cce038 | ||
|
8aa48f1ac2 | ||
|
0d78a105d4 | ||
|
dded5d130a | ||
|
abc5a4cf56 | ||
|
920f25b098 | ||
|
5fb1523396 | ||
|
0e10225f19 | ||
|
553be38cda | ||
|
d54a359b78 | ||
|
7363215ea7 | ||
|
d2f977635c | ||
|
c57f636e13 | ||
|
2c63328750 | ||
|
4cac0c839c | ||
|
29a3006f1c | ||
|
790d31f578 | ||
|
c994f9f4c1 | ||
|
d07e201e4d | ||
|
8666d1b773 | ||
|
864aacd45a | ||
|
e9fe9b90ba | ||
|
8263bb9788 | ||
|
131d978180 | ||
|
67a5294e5a | ||
|
238844b723 | ||
|
9975daca5c | ||
|
e93e2f31a0 | ||
|
8ad26817f1 | ||
|
36237484f6 | ||
|
ebc81be9ca | ||
|
811b956a4b | ||
|
d11e951fe6 | ||
|
372b22aa48 | ||
|
5f0c590ca6 | ||
|
4bd30a97c8 | ||
|
3b97b3f89c | ||
|
214465a369 | ||
|
264d715447 | ||
|
f29244bf20 | ||
|
0daaf69e25 | ||
|
a40defb927 | ||
|
8c7655ac6f | ||
|
2aec58b704 | ||
|
504ea78593 | ||
|
ff27c37cf5 | ||
|
037a9893db | ||
|
22864c927a | ||
|
4a7483509e | ||
|
75e966d958 | ||
|
c2d6b2a361 | ||
|
a0ac6938cf | ||
|
6f030797fa | ||
|
165e17f844 | ||
|
47edba24dc | ||
|
4347642ac6 | ||
|
4a8c55f6be | ||
|
3a41029313 | ||
|
0e827319cd | ||
|
48cef121a1 | ||
|
208950df55 | ||
|
dee9128cb3 | ||
|
1794a4bcba | ||
|
605fa2cc74 | ||
|
4fe0443a10 | ||
|
8331cb85b5 | ||
|
1e59bab1eb | ||
|
81ae359ec0 | ||
|
ccb6c85c41 | ||
|
1f1ee0b64e | ||
|
581756e110 | ||
|
137f8af60b | ||
|
123d05d685 | ||
|
8a96e65f9f | ||
|
724ee54658 | ||
|
91110edc52 | ||
|
aeb2e34243 | ||
|
68e1e2fed2 | ||
|
6ced50a88d | ||
|
2625e1328a | ||
|
a67588a1c0 | ||
|
6802210509 | ||
|
a00d460ce0 | ||
|
9ad4b27a50 | ||
|
a148f87701 | ||
|
05c682b0e5 | ||
|
e1113287b4 | ||
|
d10713455c | ||
|
b50d93f030 | ||
|
c4487af180 | ||
|
ea45da0dbd | ||
|
53c258d58f | ||
|
601f58e025 | ||
|
72d148b237 | ||
|
6ee8d6ba18 | ||
|
4f8ef9d709 | ||
|
3d58817927 | ||
|
498f29bfa2 | ||
|
88f419ce67 | ||
|
6c0ecf752c | ||
|
314227136e | ||
|
4eeec12b67 | ||
|
4fc5698d0e | ||
|
18102eb76b | ||
|
c12db50b27 | ||
|
5f7d29617c | ||
|
2ef0d2101c | ||
|
eded1340ca | ||
|
4f9b75442e | ||
|
af7f28f32c | ||
|
141bba6d62 | ||
|
96ffe92ce3 | ||
|
cc7731fe9c | ||
|
fb0b2e2e58 | ||
|
bd3909c7d7 | ||
|
dfd30d489c | ||
|
0531a1d85f | ||
|
c0dca503c9 | ||
|
a220efb4ac | ||
|
79370bcba4 | ||
|
6296c4dfbf | ||
|
d3908a1c3e | ||
|
50b1ea65e3 | ||
|
bca7ecb95f | ||
|
262fc34bb9 | ||
|
305dc01820 | ||
|
cd05cdd44f | ||
|
11ad75d932 | ||
|
215e72d07c | ||
|
b5ca5f0bc9 | ||
|
5adef9adb9 | ||
|
1a698b5765 | ||
|
ceefdcf7ca | ||
|
5de38ec406 | ||
|
af9a9410a2 | ||
|
3d9cd560c5 | ||
|
781ad6512f | ||
|
87dd81581c | ||
|
871f13ceb1 | ||
|
11ca42e6f0 | ||
|
905c2fbe26 | ||
|
f1bfe198f6 | ||
|
5a936a599c | ||
|
ca288fcd8b | ||
|
766bc7a4a8 | ||
|
9bd9d7cc3a | ||
|
ca71db54ba | ||
|
be6c4252ff | ||
|
160dc9e2de | ||
|
6ab3656040 | ||
|
5eac211d4d | ||
|
10acf8e7bb | ||
|
bb430d455f | ||
|
c16c5272e2 | ||
|
5fa023e9f5 | ||
|
d1d6e3727e | ||
|
86f40a431d | ||
|
d57e33d951 | ||
|
07d578ab57 | ||
|
3db080b7f7 | ||
|
fcd300a3fd | ||
|
bf4d43067d | ||
|
2b2954b0fd | ||
|
d850559f36 | ||
|
862ae1fc93 | ||
|
efd61fb885 | ||
|
e732c3ad8b | ||
|
1892072128 | ||
|
8fdc30a88c | ||
|
c090392b41 | ||
|
f731466317 | ||
|
2831d2fd77 | ||
|
0704521d4a | ||
|
935af9d4d1 | ||
|
4e05121dfc | ||
|
a975c9c2be | ||
|
26b6f6f6c8 | ||
|
25ca2a879b | ||
|
4bd2390c1d | ||
|
8cf32a9195 | ||
|
8ba6005c67 | ||
|
8f6a85d81d | ||
|
d0ab40f750 | ||
|
39f37b2cf5 | ||
|
bb83caf8fe | ||
|
4367479522 | ||
|
47bc9788ef | ||
|
34ca68f1ae | ||
|
077551556d | ||
|
b0b6c0c0f6 | ||
|
283086136b | ||
|
bc4ad96b76 | ||
|
103afdc07e | ||
|
88ad085ac8 | ||
|
9dbec11353 | ||
|
9bc93a8d15 | ||
|
d506f6cb3e | ||
|
1cb99d7706 | ||
|
a91041ae61 | ||
|
a0707e9d54 | ||
|
f6af2cbbcf | ||
|
943271b509 | ||
|
577cd0a82a | ||
|
baff1d23e6 | ||
|
e7e5121700 | ||
|
3087e39b57 | ||
|
49f83ca5f7 | ||
|
7bb13b2280 | ||
|
71633e268c | ||
|
90eb054e5e | ||
|
b886f6021c | ||
|
1cf9dfd33e | ||
|
c928b02eea | ||
|
3751801796 | ||
|
2ff8365651 | ||
|
5e793c4fbb | ||
|
01aab733bb | ||
|
b0bb40fe23 | ||
|
53141a03d7 | ||
|
f7341f5a51 | ||
|
79a6fd0541 | ||
|
29619d51ee | ||
|
b4caf44af4 | ||
|
c686dd026f | ||
|
2aa30eaa28 | ||
|
3fcbb116fe | ||
|
68dcda74d0 | ||
|
7576b52ab6 | ||
|
0c1812e457 | ||
|
9800a4ad4b | ||
|
6444fe7e94 | ||
|
f7e65ec61b | ||
|
1d87b87e62 | ||
|
e551aeb342 | ||
|
4ffeddaf5a | ||
|
1beedb79b1 | ||
|
59dbc1b2e1 | ||
|
b89ffc2bad | ||
|
04bcd555c3 | ||
|
6378e6ce5a | ||
|
777207f5a3 | ||
|
763003db7a | ||
|
88e6c3967d | ||
|
786f72ad2f | ||
|
8e77810fcf | ||
|
ce0e9d422a | ||
|
51d65653e8 | ||
|
682f450561 | ||
|
623dfd05fc | ||
|
2448b51d61 | ||
|
b122d54f1b | ||
|
7d43dbae19 | ||
|
3e6422bb24 | ||
|
c19a7400a1 | ||
|
959a721345 | ||
|
2f2025aa31 | ||
|
4f3ca20f55 | ||
|
89be4c0a87 | ||
|
3a4956b549 | ||
|
0fe2bcb649 | ||
|
2e2ddec790 | ||
|
c4ce344459 | ||
|
96c2c4059b | ||
|
f110dc6fb4 | ||
|
5dafa17e36 | ||
|
8a398c3028 | ||
|
afd5efbebc | ||
|
c70b6cef0f | ||
|
5916501e94 | ||
|
29dfe755fa | ||
|
b25c8d1353 | ||
|
632deabfe7 | ||
|
4dadb018ba | ||
|
fd15fd6856 | ||
|
28928b2591 | ||
|
7788437c33 | ||
|
37cac9d1bd | ||
|
13969fef50 | ||
|
a1cb3c6a11 | ||
|
1870286f2a | ||
|
0888c9e2df | ||
|
19dadfd6d2 | ||
|
9382505a29 | ||
|
d7001cefdf | ||
|
9aed1a9f29 | ||
|
a1d32b67a3 | ||
|
f231ed9d4d | ||
|
f261db16b0 | ||
|
2f6ba312dc | ||
|
0ffc5c5232 | ||
|
4b8c7c1933 | ||
|
0c7c2d44b1 | ||
|
8091e3d8c0 | ||
|
47a478a3a7 | ||
|
cb7e8661e6 | ||
|
b8baad628b | ||
|
f9852d04fd | ||
|
837b0ea2d0 | ||
|
afb1d756e0 | ||
|
8c5a8b6f22 | ||
|
0ef9bdefce | ||
|
cee8da725f | ||
|
f387059c66 | ||
|
350a7039c7 | ||
|
bc81646302 | ||
|
02b54afea1 | ||
|
c7764d3ed5 | ||
|
e96bce69bd | ||
|
47a5508919 | ||
|
3fa6f34e54 | ||
|
b4000c104a | ||
|
ed02178289 | ||
|
10ff8a7cc7 | ||
|
363453a78a | ||
|
dde5225752 | ||
|
d394b2601e | ||
|
36765afb5c | ||
|
9b954f086e | ||
|
cac2cbd986 | ||
|
8dc323d803 | ||
|
9b67926cc2 | ||
|
8dfb60fe55 | ||
|
2f3e658f5e | ||
|
8a7fcd5c85 | ||
|
c0dd7ed51f | ||
|
9b00bd0169 | ||
|
2bf24375d4 | ||
|
c3789564f0 | ||
|
3c62b7be22 | ||
|
aa4d277bab | ||
|
1aada9db53 | ||
|
a3832f25e1 | ||
|
36e35c913e | ||
|
08ac0510a6 | ||
|
075332d9da | ||
|
c1ecc63aa2 | ||
|
c83295b06f | ||
|
b875fcbd2c | ||
|
7e49bd96a4 | ||
|
6d237600fd | ||
|
6008e2be4e | ||
|
6ddef6aa59 | ||
|
7c04f53af1 | ||
|
84a23e35ba | ||
|
0d8684704f | ||
|
7b9107393b | ||
|
4c3daea4a8 | ||
|
0f860cfdcc | ||
|
c31b45575f | ||
|
9fa4cc0e13 | ||
|
7f354bf8f6 | ||
|
ffaca51d36 | ||
|
2e1a8af30d | ||
|
aad6546631 | ||
|
3ee578e696 | ||
|
7f05c2c9dc | ||
|
40e848e2c0 | ||
|
143704215f | ||
|
5dfbc7a91a | ||
|
f78775fb3f | ||
|
7b7b3c05e1 | ||
|
0c8696009f | ||
|
c217d31dee | ||
|
caecfa3a32 | ||
|
1ecc148226 | ||
|
95e61a3f45 | ||
|
9f96150b2d | ||
|
1ad20de96d | ||
|
19050fea82 | ||
|
72b9d19e8a | ||
|
6c4a80123c | ||
|
d16bc0bace | ||
|
161dfb70f3 | ||
|
a157cda92a | ||
|
22b8af52c3 | ||
|
bf6f667741 | ||
|
8867765c20 | ||
|
d98934641d | ||
|
83e6d482cd | ||
|
a54aa726e8 | ||
|
4e499d7b41 | ||
|
2a3b8593df | ||
|
4382eec54b | ||
|
4d7f9da286 | ||
|
9fee42f99d | ||
|
6b6fcba12e | ||
|
3dff836eb3 | ||
|
c1f8427e77 | ||
|
5b449dc8e1 | ||
|
dc33d70809 | ||
|
2bd0d8d35b | ||
|
10a6e1634f | ||
|
8b8791212c | ||
|
d1f60254a6 | ||
|
e908bab348 | ||
|
8bcd123206 | ||
|
10f40c8f5c | ||
|
6a045685d0 | ||
|
e901f599c2 | ||
|
243eda9321 | ||
|
beafd2daf8 | ||
|
7887553666 | ||
|
6925c21d2b | ||
|
454e4162ce | ||
|
5a77fcb519 | ||
|
acf54a0eb5 | ||
|
6f108c43ee | ||
|
11301b3c21 | ||
|
28ed2718ba | ||
|
a0b3216ec8 | ||
|
bb4aaba71b | ||
|
1e253c9f50 | ||
|
8082944d68 | ||
|
7213678739 | ||
|
a471166673 | ||
|
7c2c972b3f | ||
|
38a339f396 | ||
|
2bac0970d5 | ||
|
c8333350de | ||
|
c0b4b34c01 | ||
|
3774d2cb4f | ||
|
852c97a0b1 | ||
|
f109b416a0 | ||
|
ea927551ef | ||
|
75cb539e56 | ||
|
86b1adeb9b | ||
|
70a33ce4a5 | ||
|
86c8f35bba | ||
|
520413140d | ||
|
3eb858bb6f | ||
|
5fb1af2a8c | ||
|
d9d129dcc5 | ||
|
0993c58215 | ||
|
ce2f032ce8 | ||
|
9bb34424ff | ||
|
52ff1d1df1 | ||
|
5a329836df | ||
|
1d448f8cac | ||
|
4239fe3843 | ||
|
0b0a575394 | ||
|
2be4fbbde8 | ||
|
1651c66da9 | ||
|
a2098a300e | ||
|
74df720aea | ||
|
b8578059da | ||
|
73579a8a94 | ||
|
0d49ac2213 | ||
|
e1416fd40f | ||
|
e03923b1f1 | ||
|
fa7eceea52 | ||
|
d9e117bca5 | ||
|
ca9b897d5f | ||
|
831752fdab | ||
|
2e6281c07a | ||
|
6ea07df6a8 | ||
|
b54b2a4818 | ||
|
e9afe63695 | ||
|
7557c5b827 | ||
|
9c6ed2d7c5 | ||
|
8052844640 | ||
|
2f18163579 | ||
|
e7eb6d8a2d | ||
|
3006486b22 | ||
|
60b7180f97 | ||
|
66bbbbcf05 | ||
|
959c13b735 | ||
|
57c0424d3c | ||
|
05fd84e9f5 | ||
|
0da213c747 | ||
|
0ca1473d22 | ||
|
d56891e3ad | ||
|
fcdbf2f55c | ||
|
0cf4ae1700 | ||
|
5670ef1ad8 | ||
|
7b27184131 | ||
|
586739d4b6 | ||
|
158d2453eb | ||
|
04de657035 | ||
|
75f225fd88 | ||
|
4cf91506c6 | ||
|
0b7a05487d | ||
|
671b29bc9f | ||
|
322201332f | ||
|
08a29c8768 | ||
|
558edb442e | ||
|
f468b261dd | ||
|
e61de5b0d6 | ||
|
985a21d347 | ||
|
a9dd102383 | ||
|
d43fa67643 | ||
|
e049ccc1a2 | ||
|
bc0addb27b | ||
|
6d87c80df1 | ||
|
3741d39fa2 | ||
|
0aff063af1 | ||
|
cb7611cd4b | ||
|
96ed1b62b5 | ||
|
f5613d2776 | ||
|
fea2ec55f9 | ||
|
5570fc1300 | ||
|
a5af32d143 | ||
|
6cfbf54560 | ||
|
ca1802758f | ||
|
16205f9b82 | ||
|
2f2e79e9ef | ||
|
4b69ab232f | ||
|
5cdd13c519 | ||
|
e9ff2ea633 | ||
|
6be83e4b17 | ||
|
f33f661f08 | ||
|
4115ebffa7 | ||
|
c1705f0c52 | ||
|
1260a856d5 | ||
|
234df04300 | ||
|
2821f513f1 | ||
|
79cdd06b19 | ||
|
a2a46d406e | ||
|
e4c580564b | ||
|
a55a00f5c3 | ||
|
1d76a0c45d | ||
|
0ec3a9a1b2 | ||
|
25d0110ad9 | ||
|
61534c249e | ||
|
510cc9fcd9 | ||
|
0ec1c20f56 | ||
|
1a04ea2961 | ||
|
7e7f6fa072 | ||
|
d91c196d69 | ||
|
0eba98bffd | ||
|
f32ef0a292 | ||
|
2d33f99d31 | ||
|
4533214d71 | ||
|
a4dd827584 | ||
|
3b9e9c0e19 | ||
|
7b0df670fd | ||
|
c6a83f8426 | ||
|
f2e0b1dba7 | ||
|
d3dfdc850c | ||
|
2656303458 | ||
|
a546c88bad | ||
|
478e133b05 | ||
|
f225c7c336 | ||
|
9e5dbfa2c6 | ||
|
8b762bb786 | ||
|
3057eda4b0 | ||
|
10018c5124 | ||
|
4dfacac83a | ||
|
5a3184f6fe | ||
|
bf00086074 | ||
|
dffba2a532 | ||
|
a7e458ffcc | ||
|
1dcc11a1b5 | ||
|
e9b688e388 | ||
|
f4d12b625a | ||
|
50da1d37f5 | ||
|
8efb440285 | ||
|
ee91f73039 | ||
|
1073ed8c2a | ||
|
38f55a8ec0 | ||
|
440935fb76 | ||
|
a9e49d7cdd | ||
|
225bf84946 | ||
|
c8b748069c | ||
|
a385ae7d1f | ||
|
406e475ecb | ||
|
b2b38b90b9 | ||
|
2055ddc78b | ||
|
ff696d3231 | ||
|
780c093dd6 | ||
|
9f7bbe16e7 | ||
|
00f79c8019 | ||
|
368d000551 | ||
|
eb519d0bbf | ||
|
027dfb52ef | ||
|
0afe4f6df3 | ||
|
82462b2461 | ||
|
675a182b89 | ||
|
767ee1591e | ||
|
a228994de3 | ||
|
dbf91a1af0 | ||
|
f84e5a67c9 | ||
|
42561ac41b | ||
|
32e1d474a0 | ||
|
d827d34f04 | ||
|
606215b799 | ||
|
c1afa24574 | ||
|
525065e2f0 | ||
|
c4995ce262 | ||
|
e26e23b439 | ||
|
72ac64802e | ||
|
16673f0111 | ||
|
ae3df1d0f9 | ||
|
da391d059a | ||
|
9c2fb8d7b2 | ||
|
5cfb849554 | ||
|
b949154df0 | ||
|
8070b8dd61 | ||
|
66868a352b | ||
|
40f068f7e0 | ||
|
54ba54c936 | ||
|
7e7548f4d9 | ||
|
d6d0d983a1 | ||
|
6957305fc0 | ||
|
f84290f8f4 | ||
|
86df307d5f | ||
|
8875d75a9f | ||
|
6113eb2d48 | ||
|
8c24b9831c | ||
|
6d74434e9a | ||
|
116954a592 | ||
|
4d27f625f1 | ||
|
18041d4d28 | ||
|
00cd44878a | ||
|
0444461b2e | ||
|
895d059942 | ||
|
3ef953be62 | ||
|
013086d3bc | ||
|
10155986d8 | ||
|
904e773d16 | ||
|
a191da9f84 | ||
|
ea187e8675 | ||
|
69c32d17ed | ||
|
e223b687b2 | ||
|
6a5a7c8e3b | ||
|
b966321282 | ||
|
58a09f0bd9 | ||
|
28690fbe8a | ||
|
f496613c41 | ||
|
9fd31c744d | ||
|
4840e1d8d1 | ||
|
24c2d7d3d3 | ||
|
c5bdb00c13 | ||
|
1e1a7f25cc | ||
|
69ba17d081 | ||
|
71534eda41 | ||
|
e183ac32f6 | ||
|
7fd75170e2 | ||
|
19ae045c7d | ||
|
aa1ea29a89 | ||
|
69a0b90930 | ||
|
99956d762a | ||
|
44ef76dcb6 | ||
|
92047adcdb | ||
|
2864514933 | ||
|
ed5f646cdd | ||
|
e1c4440612 | ||
|
cba3e827db | ||
|
a37e908b5e | ||
|
0b14ff9d68 | ||
|
0bb6774473 | ||
|
c5d0194946 | ||
|
54a27a23ed | ||
|
880f70477b | ||
|
ee94e4e540 | ||
|
f647daa87e | ||
|
bd23c94b65 | ||
|
a5b71c4396 | ||
|
7a50bed7d3 | ||
|
9e5d8d044f | ||
|
fc096f155d | ||
|
d681687a38 | ||
|
daff8628f9 | ||
|
10b9b0c9a0 | ||
|
16f7e90938 | ||
|
449e16e099 | ||
|
ef1f448a40 | ||
|
2c5ef5195b | ||
|
da3036c03d | ||
|
26aa62fcae | ||
|
f8697e50a6 | ||
|
e278a1cefe | ||
|
0231cca30f | ||
|
6f7a6098a4 | ||
|
249ea7a2d0 | ||
|
687a134207 | ||
|
a2677bb930 | ||
|
8d0e339f72 | ||
|
556bc9d2be | ||
|
75172f054d | ||
|
54ab148236 | ||
|
1fa8aa3a74 | ||
|
2eab561a28 | ||
|
830d5cb713 | ||
|
9581f5ad12 | ||
|
9606c951b6 | ||
|
8e7a6bff33 | ||
|
5598d0a852 | ||
|
0ba71bc1b6 | ||
|
8b0d396565 | ||
|
9b8d7747b8 | ||
|
590d2bdd49 | ||
|
e81d3fed40 | ||
|
b2e1c8bb61 | ||
|
0b5b07aad4 | ||
|
8576d954b0 | ||
|
706c9a19ec | ||
|
a733a3b742 | ||
|
ee3516829f | ||
|
e760f87bfb | ||
|
9399bebee7 | ||
|
2feeb933cb | ||
|
c6140531ee | ||
|
81d8c09c18 | ||
|
4cf7aee08d | ||
|
b250b19bbb | ||
|
63d3c89cd3 | ||
|
af0c65e94f | ||
|
3370ef59d1 | ||
|
7eee6a084c | ||
|
a310b91ab3 | ||
|
6f46060c9b | ||
|
0d4ebf42ac | ||
|
62df9c47e9 | ||
|
52fb279fed | ||
|
73607a5dfc | ||
|
966e5106e8 | ||
|
a8d64e0af5 | ||
|
951e66861e | ||
|
1afb5da0bb | ||
|
24e458d368 | ||
|
11b99598b4 | ||
|
a5d331082c | ||
|
e235090108 | ||
|
83acf308e1 | ||
|
2c4972d89c | ||
|
dae2466487 | ||
|
7b0c26c1c2 | ||
|
5526cef426 | ||
|
0a94093fe6 | ||
|
693c07914e | ||
|
e89f5d7092 | ||
|
3aea4e8333 | ||
|
86e188f29f | ||
|
a99e3cc330 | ||
|
d467e9be96 | ||
|
33054a8298 | ||
|
15230c5622 | ||
|
532830697a | ||
|
209f032d0b | ||
|
e476d649ea | ||
|
9d3b86302d | ||
|
203d6ecb47 | ||
|
2f7f5a5467 | ||
|
d0936bce67 | ||
|
115580a18d | ||
|
9d33464ef0 | ||
|
c24d1d73bd | ||
|
6c9267d791 | ||
|
68352e3d8f | ||
|
ea760fc849 | ||
|
8405cb5067 | ||
|
0c75d5cda5 | ||
|
99ca2591c7 | ||
|
b3a5b2d860 | ||
|
f8db008a0d | ||
|
d96aaeba5a | ||
|
35d93f5755 | ||
|
0f0ce3d047 | ||
|
0c54347d4d | ||
|
1a563e8aee | ||
|
99cd5d1ab8 | ||
|
fcb2da9166 | ||
|
b3b30d1b58 | ||
|
49d8732459 | ||
|
b1ec65ac14 | ||
|
dbc47f0586 | ||
|
bdee6500ee | ||
|
a8a2462e05 | ||
|
bb3354b09d | ||
|
d2d1f730d6 | ||
|
1002aa01f7 | ||
|
f82cbad73e | ||
|
bd45dfdab6 | ||
|
de669d86cb | ||
|
4822a9d14b | ||
|
f293122004 | ||
|
2b56391f8f | ||
|
90c89318bc | ||
|
b3838f2976 | ||
|
29d65b4d16 | ||
|
ce21e9a519 | ||
|
3f57e452ca | ||
|
45834176c1 | ||
|
3da57195c6 | ||
|
742e762baf | ||
|
5b81b5fc97 | ||
|
7b068b0872 | ||
|
3ff6f420a4 | ||
|
d559dc8b99 | ||
|
34ac611d9b | ||
|
0bd8557620 | ||
|
bf2fec7cf9 | ||
|
50b32257df | ||
|
0adc6d65fd | ||
|
8d5a1c0561 | ||
|
6798d31e0f | ||
|
e7d03ce613 | ||
|
291cec71a3 | ||
|
49ca4c4614 | ||
|
4a33f3f9d3 | ||
|
ffb67cd3a8 | ||
|
5c2f56e0b6 | ||
|
b80c4a98a2 | ||
|
5728d6ff7f | ||
|
9c693acc00 | ||
|
77870927ac | ||
|
56bd469bcd | ||
|
43e6098b19 | ||
|
9989e00f6d | ||
|
92ed88cd9b | ||
|
b66ecd5811 | ||
|
a9c6ec8edb | ||
|
83756af770 | ||
|
31c7b7637a | ||
|
547e174aa4 | ||
|
023f03af15 | ||
|
86cc58af2d | ||
|
a086749881 | ||
|
3b5d4b50cb | ||
|
c9ba1cf0f4 | ||
|
371f8ec465 | ||
|
c4e307fcf1 | ||
|
d39beaa2f8 | ||
|
25c2242ee6 | ||
|
b121261cc1 | ||
|
19b325b278 | ||
|
e6deaa05b9 | ||
|
f73b6e5a75 | ||
|
4b7bd4cdc5 | ||
|
0212167b3e | ||
|
4d397df015 | ||
|
8a4fcd3d44 | ||
|
bf089379aa | ||
|
61bf9012d6 | ||
|
4a2926508e | ||
|
7d17202f39 | ||
|
fa8737d809 | ||
|
cfef4bc9c9 | ||
|
6e873a0778 | ||
|
9f8afbd18e | ||
|
1274e9fa63 | ||
|
73ec613283 | ||
|
05711b6c7f | ||
|
31b864227a | ||
|
282ae9e45d | ||
|
6038517c24 | ||
|
0e3b6ee3a0 | ||
|
2e5cc9b243 | ||
|
d2fc7a3a16 | ||
|
14d8b5e986 | ||
|
d0a59f5de1 | ||
|
7d352be907 | ||
|
443552f3d5 | ||
|
ebc3ffe7e9 | ||
|
b7d629a87e | ||
|
e7be10fb8e | ||
|
f5da0d4ef0 | ||
|
b0c5c95b5e | ||
|
f1f91763fb | ||
|
0d2e2afb35 | ||
|
f9c885c23b | ||
|
2b69e9cb8a | ||
|
07be2dcecc | ||
|
9d9ada3acc | ||
|
baf9b6100d | ||
|
04af230f8d | ||
|
a542796ce3 | ||
|
c8f980e152 | ||
|
3fdd165f41 | ||
|
d6f51a1cbb | ||
|
b7685009bd | ||
|
1300ff1ab9 | ||
|
73523236f7 | ||
|
cc476e5b15 | ||
|
746f0b48e0 | ||
|
7a1b9989b2 | ||
|
11f6367e55 | ||
|
ff5e424c4b | ||
|
26a53995db | ||
|
29a6e95a2a | ||
|
43dbbcd8c6 | ||
|
024fad8226 | ||
|
d622a7065d | ||
|
ae6b0dce49 | ||
|
dc373ae021 | ||
|
2efe578ab0 | ||
|
458b6a410a | ||
|
e37d25894b | ||
|
b47aef9205 | ||
|
e555f6e6b2 | ||
|
0e9f2ca533 | ||
|
6080a2c591 | ||
|
e608aff5a2 | ||
|
7adb8b0c9b | ||
|
54422802e5 | ||
|
d597208e38 | ||
|
68729d1d63 | ||
|
9f73ec61bd | ||
|
d03df1c490 | ||
|
d57809608f | ||
|
73f2ba1b3d | ||
|
ed9083070c | ||
|
709509fd15 | ||
|
4a7fa0384c | ||
|
471127488c | ||
|
ca8f8146c7 | ||
|
7b5c34b29b | ||
|
18e6b9f9ff | ||
|
5aeb1b0c8b | ||
|
49ffeb77c7 | ||
|
3c1e155a76 | ||
|
cdcc65e951 | ||
|
788f99831d | ||
|
f3ab1a6f24 | ||
|
8d5a1e95f1 | ||
|
ae1f3a81e3 | ||
|
3e485fcf30 | ||
|
57e13ffcae | ||
|
019ceff01c | ||
|
460dea3f5e | ||
|
8c787e0352 | ||
|
395ae6b4f9 | ||
|
dd40671576 | ||
|
2c6ef58e6f | ||
|
229537d975 | ||
|
b74ab7dd15 | ||
|
c383229489 | ||
|
732e4d3adc | ||
|
03b74dcd1f | ||
|
4b120c19d3 | ||
|
3dc38bfc5a | ||
|
42b3f25b11 | ||
|
55e78edcd6 | ||
|
58559ffaea | ||
|
e50199bd57 | ||
|
0562c549b3 | ||
|
3bb3312831 | ||
|
ba628805bd | ||
|
6c939eaa83 | ||
|
13e67fc264 | ||
|
48884994e1 | ||
|
5d8f9df7e0 | ||
|
036a45630e | ||
|
dc38851163 | ||
|
f6550555b0 | ||
|
b556795f78 | ||
|
28a71d275a | ||
|
bd1ed0d732 | ||
|
cc07e90b44 | ||
|
621e45b6a2 | ||
|
e52aa29290 | ||
|
28000fc73c | ||
|
80b31c29cb | ||
|
0d9c366555 | ||
|
ad22cc519f | ||
|
e43e7d973a | ||
|
0428a1246e | ||
|
46b402a633 | ||
|
be2c2598c7 | ||
|
4f09d954e5 | ||
|
89f123d125 | ||
|
6dc1abe99b | ||
|
79887f87ba | ||
|
e95dd99c29 | ||
|
eed42b7558 | ||
|
f0ca037237 | ||
|
9d718f09b7 | ||
|
65008f4ac4 | ||
|
5178ea4e78 | ||
|
d16e768072 | ||
|
60a4ad470d | ||
|
154abfc167 | ||
|
7198e788d1 | ||
|
6e37fd7d25 | ||
|
ea467c1eef | ||
|
bedc08cb58 | ||
|
7be37ce595 | ||
|
57e31c66e3 | ||
|
d12dc53631 | ||
|
663c69cd83 | ||
|
0c61010981 | ||
|
79fb01e6a7 | ||
|
1dca65a4c2 | ||
|
522fe16f67 | ||
|
f796fdf9a8 | ||
|
f92d6c6139 | ||
|
749f2fd5e0 | ||
|
616875b1d0 | ||
|
e821410d84 | ||
|
841779a36c | ||
|
fa53929e66 | ||
|
276a8a0eb7 | ||
|
571e445dc3 | ||
|
119381c222 | ||
|
34c075bbc6 | ||
|
07d7ad237c | ||
|
9981a08175 | ||
|
20cbd4ebac | ||
|
9b493483b7 | ||
|
2b526d85ed | ||
|
b2377051dc | ||
|
b55584997f | ||
|
55729d8737 | ||
|
fc1afb558a | ||
|
57b430c895 | ||
|
817012042d | ||
|
e92fba18be | ||
|
67ddf7d419 | ||
|
e554666fc5 | ||
|
4dc613e27a | ||
|
4fca52e03d | ||
|
a8a83c434e | ||
|
3272b16f47 | ||
|
c77299cd05 | ||
|
aa20d39141 | ||
|
98387741e8 | ||
|
a2e854d3b3 | ||
|
620e53bf63 | ||
|
ea8a49ddfc | ||
|
411bf26347 | ||
|
882d2fb4b2 | ||
|
d5493d4d23 | ||
|
80b3f6b0be | ||
|
c3ea1fc9e4 | ||
|
a2828bed16 | ||
|
55a245c290 | ||
|
1ffcfb8487 | ||
|
2c88c62cbc | ||
|
dadeb778c9 | ||
|
dec243f14d | ||
|
383bb2e672 | ||
|
dfd1a525a3 | ||
|
b9fa24e479 | ||
|
d6b33ddad8 | ||
|
1ff9c0a845 | ||
|
40866f12f2 | ||
|
1ac30ea529 | ||
|
776bbeaa2c | ||
|
83c1d90776 | ||
|
13b60163af | ||
|
d220d73b0c | ||
|
92abcd6a49 | ||
|
93b333dbf2 | ||
|
9ba0006232 | ||
|
83e307674d | ||
|
1c98e591af | ||
|
6132875b6b | ||
|
bfa13e5325 | ||
|
ce905200da | ||
|
c5fbf1f0e5 | ||
|
bdbc4b44cd | ||
|
b3300f4ad0 | ||
|
7c4ffcb413 | ||
|
a75a02d653 | ||
|
444f8a5bd0 | ||
|
b9a9e42d17 | ||
|
3885fe9afc | ||
|
3cb7be6f35 | ||
|
d73017ec56 | ||
|
4e74d048e6 | ||
|
431b3d5fd9 | ||
|
217333cdff | ||
|
b36a062eb4 | ||
|
e6d2e4d29b | ||
|
7bbaa9826e | ||
|
a74f79aeae | ||
|
0fd68aafc0 | ||
|
eea530cecb | ||
|
7f64a62926 | ||
|
54c14eb4cf | ||
|
2d9e6c520e | ||
|
0cbbc938e9 | ||
|
23b5ba2883 | ||
|
4ca2e3f443 | ||
|
dc0bf88392 | ||
|
32ddc97a95 | ||
|
21299e3abc | ||
|
83ee577a14 | ||
|
b1492802ca | ||
|
69186f1814 | ||
|
5e2f5c1ae5 | ||
|
9c17b9cddb | ||
|
69996cb5e8 | ||
|
c33a8a86b1 | ||
|
80d7536fe1 | ||
|
b48fa044cc | ||
|
8d71c04f88 | ||
|
68e4424725 | ||
|
01e721adca | ||
|
ed44cc5a17 | ||
|
54ff80ecd2 | ||
|
713d057395 | ||
|
8a109f7b4b | ||
|
568a7e9900 | ||
|
aa6cd46eeb | ||
|
3d73172246 | ||
|
5a5845e890 | ||
|
060376acfe | ||
|
68399bd76c | ||
|
2598474596 | ||
|
96642dc27a | ||
|
1fc3120363 | ||
|
06b74f3e8e | ||
|
695de69ab4 | ||
|
3b193ed58e | ||
|
9ef69854ca | ||
|
e1c69dbc13 | ||
|
7c3bcf7d2c | ||
|
9341271bda | ||
|
8473b99d56 | ||
|
6a8aaacd20 | ||
|
35d03e95e2 | ||
|
2ef7668595 | ||
|
d5dd58b4a2 | ||
|
233b6f758a | ||
|
7c6418a6fb | ||
|
927c34ed01 | ||
|
7ef47038a8 | ||
|
3a6b0479ba | ||
|
4dcc7255e8 | ||
|
71be5eeed7 | ||
|
4c0ddce4bf | ||
|
52c488a3d7 | ||
|
df78018547 | ||
|
ffbc4f91f4 | ||
|
892eca6c6a | ||
|
cb3a918aed | ||
|
4b6001b6ef | ||
|
76cb669659 | ||
|
6b88c8339d | ||
|
f97ebd9781 | ||
|
ce21f8386f | ||
|
3d317da545 | ||
|
b2146eb011 | ||
|
a2a6a40438 | ||
|
5014083277 | ||
|
7f055574b5 | ||
|
d525bf6dee | ||
|
d3c4c5f340 | ||
|
cef1611003 | ||
|
3cdba5f578 | ||
|
d2d63ccf6d | ||
|
bba781c5be | ||
|
1d08832948 | ||
|
23992e34ba | ||
|
a4defc69f7 | ||
|
1a31d6db3b | ||
|
ad167fa4df | ||
|
0baacf1d10 | ||
|
8556f7f551 | ||
|
71894452d0 | ||
|
6f4117ac31 | ||
|
d779c012db | ||
|
f368486e7a | ||
|
c09c12ced4 | ||
|
cca04adb80 | ||
|
f0a7568d71 | ||
|
33c7eee1f4 | ||
|
103f45eb27 | ||
|
79a62d445b | ||
|
b53d87e51e | ||
|
2f09415d88 | ||
|
be6196d67c | ||
|
bafe75463a | ||
|
0f4c0562f2 | ||
|
837efa7094 | ||
|
07bffc7d25 | ||
|
890dc900be | ||
|
4ac2f2c4e1 | ||
|
c133252cb6 | ||
|
1f6ef958dc | ||
|
8c5b9a803d | ||
|
322a87b1d0 | ||
|
347cb1b923 | ||
|
041c3a08e4 | ||
|
0fe8b47b61 | ||
|
10789f46f4 | ||
|
ca6dfce8e6 | ||
|
0a0a367541 | ||
|
10b66b6594 | ||
|
578b11a2d5 | ||
|
6de648c0be | ||
|
4f0eb2b82b | ||
|
006c5aba8d | ||
|
d6aab961c0 | ||
|
939cfb44bc | ||
|
1067ce9bce | ||
|
a512d9735f | ||
|
110a8f508a | ||
|
fafca061b7 | ||
|
edde56c60c | ||
|
5d4e5ec109 | ||
|
8eb2c4e926 | ||
|
f58aa79283 | ||
|
5ab7f97486 | ||
|
c4ec5fe00e | ||
|
05e5b3998b | ||
|
35ec826a71 | ||
|
fd3a7ccabc | ||
|
a03056863e | ||
|
02ecd19f58 | ||
|
4d223e356e | ||
|
b15f1aab1d | ||
|
6f6d7bd717 | ||
|
709516cc07 | ||
|
2a867b0786 | ||
|
06b22bf971 | ||
|
0dcffb37d4 | ||
|
055cbf7ab5 | ||
|
7178fb34e6 | ||
|
f07f804138 | ||
|
8cd765dc0c | ||
|
6ee4305795 | ||
|
7b7fc91eb4 | ||
|
c41c0433b2 | ||
|
7084cde42f | ||
|
47c0e52d93 | ||
|
fda95b7f07 | ||
|
19638ff712 | ||
|
93e81e9d00 | ||
|
ac0afbd2e9 | ||
|
8aa0061ab4 | ||
|
7cce210f90 | ||
|
bbe57750da | ||
|
ed9c516e12 | ||
|
6ba8efaa11 | ||
|
f026d9d5fe | ||
|
a868312bb4 | ||
|
07216f5099 | ||
|
221287d236 | ||
|
d74810d04f | ||
|
a0544672c8 | ||
|
4657a8563b | ||
|
fcb86b185e | ||
|
e09e925dfa | ||
|
aa6132079a | ||
|
95d5155028 | ||
|
b51bb9d917 | ||
|
939d902b71 | ||
|
098ec87d39 | ||
|
aa5d5ccd45 | ||
|
ab82ed898d | ||
|
aeb5fe4e34 | ||
|
608b3e00a7 | ||
|
9170f9f99c | ||
|
b967093ba7 | ||
|
be9fe7ca12 | ||
|
7db121e09f | ||
|
393fc4a63d | ||
|
ad89bf4187 | ||
|
b878417ee0 | ||
|
a37586bad4 | ||
|
3ef075b24b | ||
|
415d853764 | ||
|
f3aab94de7 | ||
|
32b4fe0d49 | ||
|
7714dc7c53 | ||
|
1ca6c9238a | ||
|
c42196e16d | ||
|
d9f8d1b688 | ||
|
0487e0c95b | ||
|
0c81f212b6 | ||
|
c6077724be | ||
|
390414e1ee | ||
|
d86f614248 | ||
|
71fabef58f | ||
|
242cc6e92d | ||
|
9cc85a48bb | ||
|
aa59a4cae7 | ||
|
2296ece1c2 | ||
|
7786320a94 | ||
|
2bac42078c | ||
|
7d8ad81fd0 | ||
|
8174db65e1 | ||
|
b2bdcfd1f5 | ||
|
dd51efb1da | ||
|
688dd6ea62 | ||
|
a2dcd57200 | ||
|
518a847b9e | ||
|
607d1fa1fd | ||
|
4918f2b5a6 | ||
|
65bc388ee9 | ||
|
3b9491b719 | ||
|
d422795c04 | ||
|
66a9feef54 | ||
|
4bab84cf9d | ||
|
e974d59905 | ||
|
5bff6b8c7c | ||
|
3b5b9ce4dc | ||
|
2a451e1eec | ||
|
11bb1644f4 | ||
|
d31655d73f | ||
|
053a82d3eb | ||
|
b3e2d286f1 | ||
|
966721043f | ||
|
2cc969198f | ||
|
b671a19326 | ||
|
7549855071 | ||
|
df917f3791 | ||
|
06778925e0 | ||
|
4195b903d7 | ||
|
7ea3935571 | ||
|
74c65cf680 | ||
|
6ef52c3020 | ||
|
b6a759d036 | ||
|
67c37e57c5 | ||
|
6559f9b129 | ||
|
9ee34e5a5b | ||
|
96478b25bb | ||
|
1e46191684 | ||
|
b98d982e5b | ||
|
d018e9df2d | ||
|
251f4fcad1 | ||
|
6fcc43297b | ||
|
ae94473a3c | ||
|
4502771702 | ||
|
eb8873c381 | ||
|
11ab120f77 | ||
|
f753b66ffd | ||
|
3dbdb584f7 | ||
|
5c0cc42a01 | ||
|
4f807e4d34 | ||
|
955e3e86bc | ||
|
2dcbbcf529 | ||
|
fb5aa4f236 | ||
|
704a992f11 | ||
|
faa738d6c1 | ||
|
575a38fbeb | ||
|
5ea0f8a7db | ||
|
2fe1d47db2 | ||
|
09dba515b5 | ||
|
e17f566833 | ||
|
00b211f38c | ||
|
69b636d477 | ||
|
f00214758c | ||
|
a7e370764b | ||
|
4af150f736 | ||
|
7c1a895c89 | ||
|
843fd9c40d | ||
|
19d243e31e | ||
|
e271c2c802 | ||
|
310dac87ab | ||
|
84a1983994 | ||
|
8e9d3aef1e | ||
|
bbc6536971 | ||
|
92de3ce7d7 | ||
|
a9380d4554 | ||
|
1b6e12a831 | ||
|
ab6b174ac5 | ||
|
84b4d98e35 | ||
|
caa702c964 | ||
|
ee9ad3f759 | ||
|
8524bc9d17 | ||
|
48845f3435 | ||
|
ff953f4c09 | ||
|
4447d24ed3 | ||
|
97f0428745 | ||
|
064e919490 | ||
|
b75116d13c | ||
|
a732cfda88 | ||
|
b246eefe1d | ||
|
55dce7e80e | ||
|
985a2f8b60 | ||
|
12a543d2aa | ||
|
341880db74 | ||
|
5a7522e656 | ||
|
93b0dce694 | ||
|
40e74d8f6f | ||
|
3035175c4f | ||
|
0c8bba8114 | ||
|
06ba067be8 | ||
|
49df4a4d21 | ||
|
d722b0855b | ||
|
b5920d8ba8 | ||
|
1efe0d486b | ||
|
ec306f9059 | ||
|
b6686531c7 | ||
|
6e0b21bfd2 | ||
|
ac9ca39bfe | ||
|
351816c154 | ||
|
b726abe94f | ||
|
8d1ccdca8c | ||
|
e4c88688e7 | ||
|
a1023948f8 | ||
|
b71e04cd9d | ||
|
7e2ee59186 | ||
|
afd9821c77 | ||
|
7bd2ad6b51 | ||
|
0f0baa78ff | ||
|
0ddcd3b35a | ||
|
0a27c0a9da | ||
|
efd9627837 | ||
|
766ca009ca | ||
|
5df014a221 | ||
|
91f7cda905 | ||
|
07a1d569b7 | ||
|
0c6ad24110 | ||
|
f2496126f8 | ||
|
fd9b82d148 | ||
|
82fe01fb4d | ||
|
aca48d3b0d | ||
|
4a24983bb5 | ||
|
91640d87b6 | ||
|
233265f8ab | ||
|
205036004b | ||
|
64fb2cf4c4 | ||
|
04d1ba7ae4 | ||
|
b85903c4f6 | ||
|
08ce614451 | ||
|
f4904b622f | ||
|
7408960e5d | ||
|
d264672b51 | ||
|
3166ea9a13 | ||
|
8814680678 | ||
|
55062092de | ||
|
33cfc8dacf | ||
|
34de6cffba | ||
|
a409016496 | ||
|
834a79b4ef | ||
|
b30e26513f | ||
|
b3979514cd | ||
|
eef333431e | ||
|
2fb2a43e6a | ||
|
0bcefd9e2d | ||
|
afd14f012b | ||
|
78dcd83191 | ||
|
93427dca14 | ||
|
2fd5aaf850 | ||
|
3c08f91d8e | ||
|
b38472260c | ||
|
375de16acc | ||
|
ac9d314b2b | ||
|
6773034487 | ||
|
aa56561dff | ||
|
af0a31d6a2 | ||
|
a7ee94b0c7 | ||
|
bb44965c8e | ||
|
a5fdbfe4a7 | ||
|
4b87c818e7 | ||
|
3a6ca6c12b | ||
|
4bfaa32431 | ||
|
6116e90e62 | ||
|
cff1c3a20b | ||
|
da4c51e1ef | ||
|
1ae265d184 | ||
|
b986c57577 | ||
|
fb50667c26 | ||
|
51f42e3ae5 | ||
|
b164acbff4 | ||
|
91e9acb49d | ||
|
0524180724 | ||
|
d4303c7e46 | ||
|
2bb32e0091 | ||
|
ba1b00cdb5 | ||
|
e50239ee39 | ||
|
35c8a98ec0 | ||
|
f38998b46a | ||
|
098552e303 | ||
|
17889dff9a | ||
|
a9e142dee0 | ||
|
c9b4393dce | ||
|
964b4493f8 | ||
|
fb31f2de57 | ||
|
334a9c019a | ||
|
8a4bc57524 | ||
|
c7edd8a712 | ||
|
b11350b04f | ||
|
d3eda71447 | ||
|
bf61a5384e | ||
|
5b13244712 | ||
|
784ba44785 | ||
|
397f34a014 | ||
|
092eb4536f | ||
|
11927f027d | ||
|
864903aa1f | ||
|
3023c8badd | ||
|
6452442f42 | ||
|
453e762f8c | ||
|
64ebf70b31 | ||
|
781ea9c170 | ||
|
987d9789be | ||
|
b5b529dca9 | ||
|
179289b8ac | ||
|
9458903bb3 | ||
|
e5eeb58201 | ||
|
fdd98d5fba | ||
|
06521b2b01 | ||
|
a48d5db958 | ||
|
d58e678dd0 | ||
|
aea574da72 | ||
|
97226ced22 | ||
|
c4e181032c | ||
|
59feb09928 | ||
|
8424cc02e8 | ||
|
e138fc1dfe | ||
|
e4f2495438 | ||
|
9bf33e192b | ||
|
0cf25653e1 | ||
|
7c017388f0 | ||
|
239c1e2eb7 | ||
|
235460abf0 | ||
|
28a908e1f8 | ||
|
0f22f2949a | ||
|
097f17358e | ||
|
b7f96cb72c | ||
|
674798a01b | ||
|
b0667a73a7 | ||
|
5878692c81 | ||
|
a374a7a9c6 | ||
|
120fc5296a | ||
|
bd0b23e612 | ||
|
f92023a5d0 | ||
|
51140856c5 | ||
|
dc01d795f3 | ||
|
16eec7d10d | ||
|
f687b7becd | ||
|
496385438a | ||
|
cc4cc6b5a6 | ||
|
e91208c2f2 | ||
|
2b7a552b03 | ||
|
e8e903bf09 | ||
|
ecc0085f33 | ||
|
021589d7dc | ||
|
9adf6ba4fb | ||
|
5d89cb305d | ||
|
5a07d9d0f1 | ||
|
977ba19610 | ||
|
c5d62b19a3 | ||
|
9affb83a2c | ||
|
439710b9d5 | ||
|
cc519c00d6 | ||
|
93b5b94f14 | ||
|
85fc6d6e39 | ||
|
2d7de7e246 | ||
|
ae87b48bef | ||
|
f8f8fe1a22 | ||
|
65bce59126 | ||
|
49defcd0c7 | ||
|
4f2b9ff3cb | ||
|
1a67da05e9 | ||
|
830bc28aed | ||
|
4805dbcdf3 | ||
|
9e29572510 | ||
|
0834651363 | ||
|
f00e5d0757 | ||
|
258ef281e0 | ||
|
8d24b0fe09 | ||
|
0b6111e197 | ||
|
b90ebd19c1 | ||
|
1fa734f679 | ||
|
e08a1362e1 | ||
|
704011a1c0 | ||
|
918231ea0f | ||
|
227b123fc9 | ||
|
0ee7727461 | ||
|
d9babf5cd6 | ||
|
e5f3969ed8 | ||
|
c2705157c4 | ||
|
9c80d5eecd | ||
|
6a8fa080eb | ||
|
3bca5e8839 | ||
|
94ee46d9e8 | ||
|
84dd32f61e | ||
|
434deee366 | ||
|
44e3d276f4 | ||
|
ddf7dd7ca2 | ||
|
10ce4c058f | ||
|
33892c515e | ||
|
1ed9fbeaf9 | ||
|
1145bc6bff | ||
|
c7649a9292 | ||
|
bf59b98631 | ||
|
391f20c9d5 | ||
|
bded4ad064 | ||
|
0552eb8833 | ||
|
ec4a3b4440 | ||
|
eba8a307bd | ||
|
8b32946b19 | ||
|
0acbcbb235 | ||
|
876a701be1 | ||
|
43959d5185 | ||
|
526f4c9543 | ||
|
c4376bc580 | ||
|
5346b7345e | ||
|
fa66b2bdbe | ||
|
d637f2d05c | ||
|
8b3c07b9ab | ||
|
a945904cfc | ||
|
949380ed09 | ||
|
baf5884b7b | ||
|
602fc1a82d | ||
|
0da379fb71 | ||
|
1867e12c36 | ||
|
93397a4176 | ||
|
0ccf82f4ad | ||
|
976a9728b3 | ||
|
95659e1662 | ||
|
0e4adcdeb4 | ||
|
e44e4ffcb7 | ||
|
02cea425e9 | ||
|
cd94d2ce36 | ||
|
7c4b332acf | ||
|
f92a0d7b43 | ||
|
343e3ee502 | ||
|
724fe393bc | ||
|
23f811e3ed | ||
|
bf212abb6f | ||
|
577682277d | ||
|
245ce0c6b0 | ||
|
b7e091b440 | ||
|
15d00c6d7a | ||
|
ceddec5b5c | ||
|
55f1c8c066 | ||
|
1ccdc7a996 | ||
|
fe00760cd6 | ||
|
3e8514fde4 | ||
|
42576bcac0 | ||
|
293ce32b16 | ||
|
b6e2d8c47a | ||
|
32882da161 | ||
|
54ea98be1c | ||
|
ebc3254724 | ||
|
e6301e52b4 | ||
|
24fe3775d0 | ||
|
28615bb1f7 | ||
|
f739c4bcd4 | ||
|
307e6299b2 | ||
|
da2907e959 | ||
|
1400a090b6 | ||
|
dfa52604ff | ||
|
b72c152ef9 | ||
|
f5c0dad309 | ||
|
4a150f2564 | ||
|
44350c9d84 | ||
|
0c13dee38f | ||
|
8935aaf43a | ||
|
6195c784ad | ||
|
f23b5c4871 | ||
|
eb316031a2 | ||
|
2e76bc9a0b | ||
|
acc87b328c | ||
|
169a741213 | ||
|
75f6c5d31b | ||
|
0efa3cada0 | ||
|
2c3441d791 | ||
|
3a157b697f | ||
|
08faf639b0 | ||
|
a2ac84dc42 | ||
|
edadd279ed | ||
|
0f7c5bd024 | ||
|
5640f02ee8 | ||
|
a3fb4bd2c5 | ||
|
dd4318505f | ||
|
28fad8d3f2 | ||
|
55f74f3a01 | ||
|
6af47e3bd6 | ||
|
e7b87929c4 | ||
|
759e5500ed | ||
|
43ea7a7ed7 | ||
|
7ead36fffb | ||
|
5e283cf62d | ||
|
070a8f0a66 | ||
|
326650b4e3 | ||
|
ca5d21d56b | ||
|
78a33c8b62 | ||
|
235a828214 | ||
|
8a7e9606fe | ||
|
742092cd8b | ||
|
d1eb303b3f | ||
|
dce7d8e7d4 | ||
|
3f7f90c9e7 | ||
|
e3108cb515 | ||
|
82c473351f | ||
|
d22be6ce2d | ||
|
3d458873d2 | ||
|
73eb52d52f | ||
|
ad650a3800 | ||
|
41f2096a7d | ||
|
5af99daa0d | ||
|
4248d27544 | ||
|
a20a2be99e | ||
|
9ec23e6d3f | ||
|
03610c5aa6 | ||
|
5f609f2683 | ||
|
17ef25b572 | ||
|
91ff69f86d | ||
|
25ee91a759 | ||
|
79e8885ae5 | ||
|
a397df8e1f | ||
|
98a4e450b0 | ||
|
1721502cda | ||
|
3f67a37a09 | ||
|
535570e1f5 | ||
|
2a9752b503 | ||
|
d085a99ddc | ||
|
4e0ff8a7ca | ||
|
0f958023d0 | ||
|
5450ab4e83 | ||
|
6da092aee7 | ||
|
25698f1b9c | ||
|
7848a1436e | ||
|
b71a549d05 | ||
|
ae14987a2f | ||
|
dcf7fdca32 | ||
|
4319bfde05 | ||
|
69f8bf9232 | ||
|
7b83db26e3 | ||
|
97fff828d7 | ||
|
834aff78ed | ||
|
410f9c5683 | ||
|
91a9d1c5f2 | ||
|
8d9582bab8 | ||
|
6ccabdc5f4 | ||
|
73adf1f984 | ||
|
251ecc8aca | ||
|
41c4a5a1ef | ||
|
c8bd1b3d59 | ||
|
fbee506f88 | ||
|
a0f02cf2c5 | ||
|
e32bd5d85a | ||
|
0ef25a55b7 | ||
|
7f8fdb159d | ||
|
314e67adcd | ||
|
e1ef255f84 | ||
|
c2978f03f0 | ||
|
05860c2193 | ||
|
b822d4345b | ||
|
122c9eeb89 | ||
|
c28a72df5c | ||
|
781e85f908 | ||
|
fe9b051c87 | ||
|
ca2a0c057b | ||
|
79b2d2322e | ||
|
d0d6cec34e | ||
|
67f8573d31 | ||
|
d2daf69167 | ||
|
671f5af493 | ||
|
804067d9c3 | ||
|
fbf66be7b0 | ||
|
14ebe8128a | ||
|
762b3c7ba6 | ||
|
3c40fb6503 | ||
|
9aa0f245ea | ||
|
2209c54fc3 | ||
|
474f5be8e7 | ||
|
ac44a0419d | ||
|
029eb57b85 | ||
|
7390dbbd4c | ||
|
d500721788 | ||
|
016de5f983 | ||
|
478156ea88 | ||
|
c01d130e4d | ||
|
23c8fc9ed7 | ||
|
4849a0cefb | ||
|
d56edd80ec | ||
|
b782632b69 | ||
|
cb1bb24d86 | ||
|
ef404e4d20 | ||
|
c3b58beb68 | ||
|
bf48e03b9a | ||
|
1f01baf664 | ||
|
78a5791e5a | ||
|
12b7a2830e | ||
|
60f9b5b589 | ||
|
41a7cb763d | ||
|
d53b6584d5 | ||
|
8fba5e7a9b | ||
|
a8edcc8ab6 | ||
|
fbe00f6f9c | ||
|
e6aaba74fb | ||
|
af2b7a3f86 | ||
|
0552b4e9b7 | ||
|
0d73682ba1 | ||
|
e9bbb20c48 | ||
|
84f4c3b3aa | ||
|
91aad73da5 | ||
|
c66a935cb1 | ||
|
6a59cb65cf | ||
|
8b1f7f1e8c | ||
|
a5307448bb | ||
|
cf4edffbe0 | ||
|
5fb16d967f | ||
|
2c9029c998 | ||
|
0d72dea0d7 | ||
|
42820c848b | ||
|
963bb29d9d | ||
|
4da6fe9da4 | ||
|
847f28c6af | ||
|
981118b9fd | ||
|
9038141e28 | ||
|
46883b2bdd | ||
|
042e3e11e6 | ||
|
5296e486e6 | ||
|
f5faa8877c | ||
|
d3c5e0f765 | ||
|
1553229b95 | ||
|
ded6275c71 | ||
|
63a7d1edb5 | ||
|
54a5a8b30f | ||
|
73f29bf9cc | ||
|
b9530b3d43 | ||
|
9a31ecb1a1 | ||
|
779b44c121 | ||
|
81c9a4c94c | ||
|
af2c4eea26 | ||
|
5085e3ccbf | ||
|
be34383627 | ||
|
d42c4a3c7e | ||
|
8e31f06818 | ||
|
4271a5e223 | ||
|
bd43c38753 | ||
|
70f2057854 | ||
|
8ed3183c5f | ||
|
f88c81b54a | ||
|
126ad0c494 | ||
|
f296b86752 | ||
|
1d9c71b9f8 | ||
|
d112290b3b | ||
|
671aa53aae | ||
|
50bedaa442 | ||
|
b7fe2ab8ee | ||
|
96b8453ae8 | ||
|
be0267956b | ||
|
39ce179d16 | ||
|
8a0e121f29 | ||
|
be8ad1a888 | ||
|
d611a5a290 | ||
|
85fbfed910 | ||
|
e7fc76a938 | ||
|
3cf0714a29 | ||
|
839edbe922 | ||
|
92c23cdc38 | ||
|
2368ec2b89 | ||
|
88503191bd | ||
|
fe5b47ce53 | ||
|
0de126d6b1 | ||
|
6c330666f2 | ||
|
4e847e2295 | ||
|
9874ce3c5f | ||
|
aefbc1ad30 | ||
|
1b3d7f7933 | ||
|
20f3e34d9c | ||
|
d2e2dd3482 | ||
|
7dc4179bf8 | ||
|
b49590a8f8 | ||
|
badfc671d9 | ||
|
f4c812d0df | ||
|
b3e1fa4110 | ||
|
4e9ab96678 | ||
|
f5ae443ad5 | ||
|
38a46ed552 | ||
|
661279bc1e | ||
|
fa6158ba02 | ||
|
f9781ac16e | ||
|
6f8d153aad | ||
|
75a8975941 | ||
|
178334ec9b | ||
|
0a8b33f67a | ||
|
3354df5bbe | ||
|
f6ba160db6 | ||
|
e09609131d | ||
|
f4541bbeb8 | ||
|
715de65234 | ||
|
8dab34bc39 | ||
|
64f625cf41 | ||
|
1cc6d62611 | ||
|
d704a1b7eb | ||
|
e2ee2f2281 | ||
|
128e90398e | ||
|
343d6b7c47 | ||
|
ad2e60ad1c | ||
|
81fa54cb53 | ||
|
941f809987 | ||
|
a2c01e91dd | ||
|
62d0fa36d7 | ||
|
0cd510da33 | ||
|
f7b81c33e2 | ||
|
f69312e42e | ||
|
010a1dd994 | ||
|
8630e5d431 | ||
|
d2f472e2c8 | ||
|
e0cffc4c35 | ||
|
ddf6578398 | ||
|
d4ffeb061e | ||
|
c2eedc3952 | ||
|
8fe2bf2296 | ||
|
c5910cde4b | ||
|
60e6697529 | ||
|
4cbcc3a5e9 | ||
|
ab60744ac5 | ||
|
ce110f4101 | ||
|
e1a4c4b5c9 | ||
|
2f432281a3 | ||
|
2a36bca4c0 | ||
|
8bdf4e7663 | ||
|
56f712bb89 | ||
|
587d5c8559 | ||
|
17610787ee | ||
|
acb6449e77 | ||
|
6dddb5e6c3 | ||
|
14d29b5bb5 | ||
|
76eb1384cc | ||
|
b6c76e6d73 | ||
|
8d5bf652ad | ||
|
3a1b907f09 | ||
|
76a56598bd | ||
|
d2ec02a0e6 | ||
|
72fa948d93 | ||
|
e08d52d09e | ||
|
96fdf9e846 | ||
|
e0be1603b5 | ||
|
34dc2d2db5 | ||
|
a0421dd34e | ||
|
e54c1a750b | ||
|
3ece3a59bc | ||
|
d20a448e25 | ||
|
2b513f9f7e | ||
|
a673d851c1 | ||
|
d73afc4760 | ||
|
5b5cfd84f1 | ||
|
3066b91af2 | ||
|
cfbd7fb673 | ||
|
64433ac957 | ||
|
97dc10207c | ||
|
08ae90d601 | ||
|
b33f9f57ad | ||
|
5b33ebca6b | ||
|
6ae74c9caf | ||
|
f4c7467ea0 | ||
|
d0bca81cef | ||
|
66fdbab21e | ||
|
134790dcbc | ||
|
214770ac80 | ||
|
578246668e | ||
|
78b11c75dd | ||
|
7e74f632b9 | ||
|
b85e8ceb9e | ||
|
b636eedbcc | ||
|
e7955b77dc | ||
|
1c03e2bdfe | ||
|
c2b661ae0c | ||
|
a829c064e7 | ||
|
c65a584591 | ||
|
be77ae26e6 | ||
|
d7ec37b13b | ||
|
5dcac62bf2 | ||
|
f3ee315336 | ||
|
0b22717fc1 | ||
|
b7cc13b9c6 | ||
|
576561956e | ||
|
4feb316c04 | ||
|
9d6e2566dd | ||
|
45fc351565 | ||
|
a64c123a8c | ||
|
4af4347492 | ||
|
6593c9c2ed | ||
|
ce3b111678 | ||
|
26136a7901 | ||
|
094b9112ad | ||
|
ac9e0ed195 | ||
|
4983a9b24e | ||
|
1b7269c201 | ||
|
53d25cba1d | ||
|
d362dd34b0 | ||
|
8e25e751c2 | ||
|
c406989baf | ||
|
59a18bac52 | ||
|
8e5a25ea7f | ||
|
2f7f9d8989 | ||
|
a4d1f30f2c | ||
|
2e6c5ed312 | ||
|
187fa7753e | ||
|
82bd6ea35c | ||
|
fecfc62265 | ||
|
42e21bce9a | ||
|
83e47d0be8 | ||
|
3694548489 | ||
|
20bdd2ac55 | ||
|
b446160d6b | ||
|
0f8adcace4 | ||
|
209a2589c4 | ||
|
ca8dcaf269 | ||
|
172593fbde | ||
|
831141a7a9 | ||
|
aedc5e363c | ||
|
60a8eeaa8c | ||
|
5c76e96506 | ||
|
d821ec9dca | ||
|
20a0e6096e | ||
|
1c55dbf4d4 | ||
|
d2f349acd6 | ||
|
cd78c91abb | ||
|
3c18f2c082 | ||
|
9b54cf6fff | ||
|
9a07df3157 | ||
|
da4ec035de | ||
|
d4b33c669a | ||
|
91817cb98b | ||
|
52fdd1065a | ||
|
e04c5f6f9e | ||
|
03b0909a4e | ||
|
002c0ca17c | ||
|
dde3ce4abc | ||
|
6122a188b6 | ||
|
5eeee34017 | ||
|
e35d3dbc6f | ||
|
84beecac20 | ||
|
6d3a6aa9e0 | ||
|
8e268d2ec3 | ||
|
0cb39bcf55 | ||
|
26b11aa9f7 | ||
|
f2d6bf0eac | ||
|
5abfcabe8b | ||
|
f673c8b231 | ||
|
0fed9320f8 | ||
|
ecd4312d72 | ||
|
fdcaaf0177 | ||
|
eec822f633 | ||
|
0ee44a9924 | ||
|
a9af3d7707 | ||
|
8960df487d | ||
|
8613505cd6 | ||
|
05807e2f39 | ||
|
df5d6a81e7 | ||
|
87e97b9cef | ||
|
460ffb6698 | ||
|
f50b3a23d9 | ||
|
c36ca15055 | ||
|
ef405a753f | ||
|
518ef4579d | ||
|
cfdc753ff5 | ||
|
a06a7a74a4 | ||
|
d47d52b6f8 | ||
|
b112db7e62 | ||
|
7565a300e1 | ||
|
3c029cf43b | ||
|
cbd4b1bf0b | ||
|
d9f6e80930 | ||
|
609cee1c17 | ||
|
6ff82a699e | ||
|
51bc2b3fae | ||
|
230a6b4b0d | ||
|
e82fa9ad12 | ||
|
2ae81b6624 | ||
|
5235286d8a | ||
|
e82e1b4631 | ||
|
ecd838c891 | ||
|
d2f46fdf66 | ||
|
a2f9c65a3f | ||
|
e575d18019 | ||
|
fa9d7c39ac | ||
|
f2180b1385 | ||
|
3613991fc2 | ||
|
58131f4964 | ||
|
9c5337edae | ||
|
31c43c3170 | ||
|
06205e7bde | ||
|
f26ee30b74 | ||
|
7e25397845 | ||
|
0b013b55de | ||
|
9584cf9a67 | ||
|
9ca5caab9d | ||
|
7d43fe2ab8 | ||
|
6e4ad797d5 | ||
|
5c96a4d4ce | ||
|
d56beab370 | ||
|
7163fec474 | ||
|
bf4381e1ca | ||
|
4e987286ee | ||
|
26a1af2bcc | ||
|
5e83b7f4b5 | ||
|
f580cf2856 | ||
|
df30d5879e | ||
|
d83fbf81d3 | ||
|
15c60cb6a8 | ||
|
9140f3b929 | ||
|
42e2935916 | ||
|
3016ef8d79 | ||
|
1fd6dd9101 | ||
|
9cc9b0e5d7 | ||
|
f1ffd458e4 | ||
|
bb5bb45687 | ||
|
a1ba1a0974 | ||
|
f30b77a3e0 | ||
|
f8a27aec6c | ||
|
1aac62923d | ||
|
6c4cacbd3e | ||
|
fb437bf2cf | ||
|
39a7b93cc1 | ||
|
8d592bb982 | ||
|
8a0d556fc7 | ||
|
8649973d71 | ||
|
1a55ca93d8 | ||
|
f6a6df6c86 | ||
|
1cc411f199 | ||
|
d0ae9badca | ||
|
12ea73c89c | ||
|
2ce7620b1d | ||
|
9d9c1c4495 | ||
|
11cbc74526 | ||
|
ee2e2d2c57 | ||
|
bbfb2ac21a | ||
|
525f47dcf1 | ||
|
2f0380cd4e | ||
|
f13a484e76 | ||
|
988a72002b | ||
|
f09d0f12d3 | ||
|
289e17134c | ||
|
086863075d | ||
|
52aece64d6 | ||
|
36c7186637 | ||
|
5f7821accf | ||
|
5dc0111581 | ||
|
745bfaeb27 | ||
|
24e6f87d24 | ||
|
2a73fac5d3 | ||
|
6448cc2363 | ||
|
e917023fe9 | ||
|
edb2da5f50 | ||
|
8345b7aa04 | ||
|
1c2433c670 | ||
|
570db1211b | ||
|
0937bc0306 | ||
|
6bb76f1894 | ||
|
05ea963464 | ||
|
275314513e | ||
|
f5bb352032 | ||
|
ae2bd68858 | ||
|
c1939b5d72 | ||
|
dbdf55841f | ||
|
bc39a77dc0 | ||
|
f2bf9da535 | ||
|
8cb368e584 | ||
|
600eefeecf | ||
|
f2e57193dd | ||
|
5011d83436 | ||
|
8914219a75 | ||
|
3750e25a7a | ||
|
8665e70be1 | ||
|
5db702cf14 | ||
|
26c27cef49 | ||
|
a6b01bc004 | ||
|
ed86705411 | ||
|
56f2f5bfb4 | ||
|
19bb544b78 | ||
|
4bfdd9bbc3 | ||
|
4713c99041 | ||
|
ffc3d49af7 | ||
|
7f5c2a1cfe | ||
|
24d2368f6e | ||
|
54376428ef | ||
|
86a1d2dcac | ||
|
1b75c950bd | ||
|
47dde508d3 | ||
|
7f5f028cc5 | ||
|
4bdf43097e | ||
|
1d14f973ce | ||
|
a362c89e42 | ||
|
ed660595df | ||
|
b4f04cdab8 | ||
|
d0a4991079 | ||
|
7f7b1fc8d5 | ||
|
6995409c50 | ||
|
52fde7a0d5 | ||
|
f44ce014d3 | ||
|
50debf9a81 | ||
|
f0ce08f85d | ||
|
caf8621c78 | ||
|
5997abdcb2 | ||
|
a7706983a9 | ||
|
8231afe983 | ||
|
1334a03875 | ||
|
3f607501d6 | ||
|
7fe2209944 | ||
|
4681523c4c | ||
|
931a1f8554 | ||
|
204b1fbd0d | ||
|
90939f3488 | ||
|
8abd3298af | ||
|
21b0848030 | ||
|
d0970cdf9e | ||
|
7e36dc83f8 | ||
|
584c7cfbd6 | ||
|
d3c1546d45 | ||
|
1c6f7c1e49 | ||
|
c99d9f0fdf | ||
|
b105d55316 | ||
|
df1b3f86a5 | ||
|
8a36f1c6f5 | ||
|
5bb1b4f5b6 | ||
|
ec1a19e7e8 | ||
|
a524ef7de5 | ||
|
2dd3a34961 | ||
|
5403d61ac2 | ||
|
fbdce831de | ||
|
1d200a886e | ||
|
9628238d91 | ||
|
dbb7d873cf | ||
|
45168341de | ||
|
3f8167dc17 | ||
|
c93d0bbc7f | ||
|
3fa67447bb | ||
|
ae867847a5 | ||
|
4a4553f676 | ||
|
698c602475 | ||
|
e92f13728d | ||
|
27525c2762 | ||
|
624d77fcfa | ||
|
f5814226a6 | ||
|
5cb988b3e7 | ||
|
df4cf8a3d6 | ||
|
3e54d15cea | ||
|
0d4d31d0f8 | ||
|
50c1f67bdf | ||
|
86990f3535 | ||
|
555e64ac97 | ||
|
7020edc5e7 | ||
|
8a4e1765f0 | ||
|
4441c12d09 | ||
|
96e277c331 | ||
|
0ab91a224a | ||
|
305bb71aa3 | ||
|
4168854d76 | ||
|
8cd61a06f2 | ||
|
3d32e616da | ||
|
af605f352e | ||
|
46744df98d | ||
|
24c7f71c89 | ||
|
16c4a93305 | ||
|
7caba79217 | ||
|
95278eacb4 | ||
|
225bdc03cf | ||
|
9da6f128ec | ||
|
aa35f2f158 | ||
|
92fcee0aea | ||
|
8274ac3e80 | ||
|
ab0e6e7ab4 | ||
|
6b442bd318 | ||
|
4b94fc65bf | ||
|
74f5ef31d9 | ||
|
71d4e99b1c | ||
|
47344e8bdc | ||
|
ab6abc637e | ||
|
6f33be1e4f | ||
|
c53e1bdb22 | ||
|
7520bc70b8 | ||
|
48451f2d12 | ||
|
b2906808d1 | ||
|
0f9b9bad56 | ||
|
9aca6cd0a0 | ||
|
fbf78f04d6 | ||
|
0aca6b3328 | ||
|
3e4e85ceee | ||
|
7afc07ecff | ||
|
445ec92226 | ||
|
e200eb47c6 | ||
|
2743e02cc1 | ||
|
2f73732daa | ||
|
521063c6df | ||
|
605fdce9be | ||
|
d261e5bf95 | ||
|
c0bea57468 | ||
|
adaf581731 | ||
|
c085fd068d | ||
|
576b0037ff | ||
|
545652722e | ||
|
c989819328 | ||
|
afbbcaa33e | ||
|
19ad676df6 | ||
|
08026ac235 | ||
|
6fe7db054b | ||
|
de48a1ff35 | ||
|
89998e2f18 | ||
|
4fe339f51f | ||
|
63d9348012 | ||
|
be128608b9 | ||
|
8e161c0099 | ||
|
7445f8006b | ||
|
3095185121 | ||
|
30d4d72b7a | ||
|
3e02b65cfb | ||
|
d4ed6a30c2 | ||
|
cc64a57172 | ||
|
e756724fe7 | ||
|
94b3fd6903 | ||
|
b786df44d1 | ||
|
aab39212ef | ||
|
3233494281 | ||
|
d3fc35989d | ||
|
933fb9fcfd | ||
|
7a751e811e | ||
|
49055145f1 | ||
|
363147b96d | ||
|
35298346d6 | ||
|
201e63865a | ||
|
afa3a312c9 | ||
|
351b2d7178 | ||
|
17119d369c | ||
|
be32867860 | ||
|
1ac9cdf606 | ||
|
581398f826 | ||
|
64914a7ebe | ||
|
36f7603142 | ||
|
7de447cf6f | ||
|
7499c29f6c | ||
|
ba840423a6 | ||
|
9cedbd086d | ||
|
b2a2281ff7 | ||
|
6a26c3115d | ||
|
7c05c16925 | ||
|
c62e8fe12c | ||
|
50d0e9d4db | ||
|
c512b579ce | ||
|
25e8b7104e | ||
|
a420132632 | ||
|
ab95fa01af | ||
|
fa35da3c12 | ||
|
70a58176fa | ||
|
c3da5cc262 | ||
|
447f654435 | ||
|
e546b22797 | ||
|
5edeba9495 | ||
|
b47997a030 | ||
|
769a122b03 | ||
|
575189656b | ||
|
51ccd367c4 | ||
|
f6ebb4ee41 | ||
|
f3c2a81e0c | ||
|
e0efe996b0 | ||
|
e9b7826621 | ||
|
909c0c9a63 | ||
|
e1eb377a92 | ||
|
13988ad74a | ||
|
4155211051 | ||
|
084a09c688 | ||
|
7eee0f062d | ||
|
da9d1e4eb3 | ||
|
9275b92d9f | ||
|
d9b33238e9 | ||
|
264e4becea | ||
|
ad9ac1e68d | ||
|
070d8df1db | ||
|
2097350a1c | ||
|
32da2d15b6 | ||
|
0a29a496d7 | ||
|
51a85a0b03 | ||
|
8210670122 | ||
|
a4a2b5165b | ||
|
6277a94a5b | ||
|
f681e6c704 | ||
|
b96730b83f | ||
|
eba4a6e3f5 | ||
|
cf6bf7f031 | ||
|
d9ee4f8c65 | ||
|
177e4d3592 | ||
|
5bc66e2524 | ||
|
44311e4008 | ||
|
d23ffc3b17 | ||
|
725539b867 |
1727 changed files with 766678 additions and 6970 deletions
46
.github/workflows/ci.yml
vendored
Normal file
46
.github/workflows/ci.yml
vendored
Normal file
|
@ -0,0 +1,46 @@
|
|||
name: CI
|
||||
on: [push, pull_request]
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-22.04
|
||||
|
||||
services:
|
||||
postgres:
|
||||
image: postgres
|
||||
env:
|
||||
POSTGRES_DB: ci_test
|
||||
POSTGRES_PASSWORD: citestpassword
|
||||
# Set health checks to wait until postgres has started
|
||||
options: >-
|
||||
--health-cmd pg_isready
|
||||
--health-interval 10s
|
||||
--health-timeout 5s
|
||||
--health-retries 5
|
||||
ports:
|
||||
# Maps tcp port 5432 on service container to the host
|
||||
- 5432:5432
|
||||
redis:
|
||||
image: redis
|
||||
# Set health checks to wait until redis has started
|
||||
options: >-
|
||||
--health-cmd "redis-cli ping"
|
||||
--health-interval 10s
|
||||
--health-timeout 5s
|
||||
--health-retries 5
|
||||
ports:
|
||||
# Maps port 6379 on service container to the host
|
||||
- 6379:6379
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- run: sudo apt-get update && sudo apt-get -y install libimlib2-dev chromium-browser
|
||||
- uses: ruby/setup-ruby@v1
|
||||
with:
|
||||
ruby-version: '3.3'
|
||||
bundler-cache: true
|
||||
- name: Install dependencies
|
||||
run: bundle install
|
||||
- name: Run tests with Coveralls
|
||||
env:
|
||||
COVERALLS_REPO_TOKEN: ${{ secrets.COVERALLS_REPO_TOKEN }}
|
||||
run: bundle exec rake
|
41
.gitignore
vendored
41
.gitignore
vendored
|
@ -1,24 +1,31 @@
|
|||
*.gem
|
||||
*.rbc
|
||||
.bundle
|
||||
.config
|
||||
coverage
|
||||
InstalledFiles
|
||||
lib/bundler/man
|
||||
pkg
|
||||
rdoc
|
||||
spec/reports
|
||||
test/tmp
|
||||
test/version_tmp
|
||||
tmp
|
||||
# YARD artifacts
|
||||
.yardoc
|
||||
_yardoc
|
||||
doc/
|
||||
tests/coverage
|
||||
config.yml
|
||||
.DS_Store
|
||||
public/assets/css/.sass-cache/
|
||||
public/css/neo.css
|
||||
public/css/neo.css.map
|
||||
public/site_thumbnails
|
||||
public/sites
|
||||
public/site_screenshots
|
||||
public/site_screenshots_test
|
||||
public/site_thumbnails_test
|
||||
*.swp
|
||||
files/map.txt
|
||||
files/supporter-map.txt
|
||||
files/maps
|
||||
.sass-cache
|
||||
.sass-cache/*
|
||||
files/sslsites.zip
|
||||
.tm_properties
|
||||
.vagrant
|
||||
public/banned_sites
|
||||
public/deleted_sites
|
||||
files/disposable_email_whitelist.conf
|
||||
files/disposable_email_blacklist.conf
|
||||
files/banned_email_blacklist.conf
|
||||
files/letsencrypt.key
|
||||
files/tor.txt
|
||||
.bundle
|
||||
ext/black_box.rb
|
||||
files/trumpplan.txt
|
||||
public/sitemap
|
||||
|
|
|
@ -1,7 +0,0 @@
|
|||
language: ruby
|
||||
rvm:
|
||||
- "2.1.0"
|
||||
addons:
|
||||
postgresql: "9.3"
|
||||
before_script:
|
||||
- psql -c 'create database travis_ci_test;' -U postgres
|
118
Gemfile
118
Gemfile
|
@ -3,66 +3,90 @@ source 'https://rubygems.org'
|
|||
gem 'sinatra'
|
||||
gem 'redis'
|
||||
gem 'sequel'
|
||||
gem 'slim'
|
||||
gem 'redis-namespace'
|
||||
gem 'bcrypt'
|
||||
gem 'sinatra-flash', require: 'sinatra/flash'
|
||||
gem 'sinatra-xsendfile', require: 'sinatra/xsendfile'
|
||||
gem 'puma', require: nil
|
||||
gem 'rubyzip', require: 'zip'
|
||||
gem 'rack-recaptcha', require: 'rack/recaptcha'
|
||||
gem 'rmagick', require: nil
|
||||
gem 'sidekiq'
|
||||
gem 'ago'
|
||||
gem 'puma', '< 7', require: nil
|
||||
gem 'sidekiq', '~> 7'
|
||||
gem 'mail'
|
||||
gem 'google-api-client', require: 'google/api_client'
|
||||
gem 'tilt'
|
||||
gem 'erubis'
|
||||
gem 'stripe', :git => 'https://github.com/stripe/stripe-ruby'
|
||||
gem 'screencap'
|
||||
gem 'erubi'
|
||||
gem 'stripe' #, source: 'https://code.stripe.com/'
|
||||
gem 'terrapin'
|
||||
gem 'sass', require: nil
|
||||
gem 'dav4rack', git: 'https://github.com/neocities/dav4rack.git', ref: '1bf1975c613d4f14d00f1e70ce7e0bb9e2e6cd9b'
|
||||
gem 'filesize'
|
||||
gem 'thread'
|
||||
gem 'rest-client', require: 'rest_client'
|
||||
gem 'addressable', '>= 2.8.0', require: 'addressable/uri'
|
||||
gem 'paypal-recurring', require: 'paypal/recurring'
|
||||
gem 'geoip'
|
||||
gem 'io-extra', require: 'io/extra'
|
||||
#gem 'rye'
|
||||
gem 'coveralls_reborn', require: false
|
||||
gem 'sanitize'
|
||||
gem 'will_paginate'
|
||||
gem 'simpleidn'
|
||||
gem 'gandi'
|
||||
gem 'hoe', require: nil
|
||||
gem 'msgpack'
|
||||
gem 'acme-client', '~> 2.0.0'
|
||||
gem 'http'
|
||||
gem 'htmlentities'
|
||||
gem 'rinku'
|
||||
gem 'image_optim'
|
||||
gem 'image_optim_pack'
|
||||
gem 'ipaddress'
|
||||
gem 'feedjira', '2.1.4'
|
||||
gem 'monetize'
|
||||
gem 'xmlrpc'
|
||||
gem 'magic'
|
||||
gem 'pg'
|
||||
gem 'sequel_pg', require: nil
|
||||
gem 'hiredis'
|
||||
gem 'activesupport'
|
||||
gem 'facter', require: nil
|
||||
gem 'maxmind-db'
|
||||
gem 'json', '>= 2.3.0'
|
||||
gem 'nokogiri'
|
||||
gem 'webp-ffi'
|
||||
gem 'rszr'
|
||||
gem 'zip_tricks'
|
||||
gem 'adequate_crypto_address'
|
||||
gem 'twilio-ruby'
|
||||
gem 'phonelib'
|
||||
gem 'dnsbl-client'
|
||||
gem 'minfraud'
|
||||
gem 'image_optimizer' # apt install optipng jpegoptim pngquant
|
||||
gem 'rubyzip', require: 'zip'
|
||||
gem 'airbrake'
|
||||
gem 'csv'
|
||||
|
||||
platform :mri do
|
||||
gem 'magic' # sudo apt-get install file, For OSX: brew install libmagic
|
||||
gem 'pg'
|
||||
gem 'sequel_pg', require: nil
|
||||
gem 'hiredis'
|
||||
gem 'rainbows', require: nil
|
||||
|
||||
group :development, :test do
|
||||
gem 'pry'
|
||||
gem 'pry-debugger'
|
||||
end
|
||||
end
|
||||
|
||||
platform :jruby do
|
||||
gem 'jruby-openssl'
|
||||
gem 'json'
|
||||
gem 'jdbc-postgres'
|
||||
|
||||
group :development do
|
||||
gem 'ruby-debug', require: nil
|
||||
gem 'sass', require: nil
|
||||
end
|
||||
group :development, :test do
|
||||
gem 'pry'
|
||||
end
|
||||
|
||||
group :development do
|
||||
gem 'shotgun', require: nil
|
||||
gem 'certified'
|
||||
end
|
||||
|
||||
group :test do
|
||||
gem 'faker'
|
||||
gem 'fabrication', require: 'fabrication'
|
||||
gem 'fabrication', require: 'fabrication'
|
||||
gem 'minitest'
|
||||
gem 'minitest-reporters', require: 'minitest/reporters'
|
||||
gem 'rack-test', require: 'rack/test'
|
||||
gem 'webmock'
|
||||
gem 'mocha', require: nil
|
||||
gem 'rake', require: nil
|
||||
gem 'poltergeist'
|
||||
gem 'phantomjs', require: 'phantomjs/poltergeist'
|
||||
gem 'capybara'
|
||||
gem 'capybara_minitest_spec'
|
||||
|
||||
platform :mri do
|
||||
gem 'simplecov', require: nil
|
||||
end
|
||||
gem 'minitest-reporters', require: 'minitest/reporters'
|
||||
gem 'rack-test', require: 'rack/test'
|
||||
gem 'mocha', require: nil
|
||||
gem 'rake', '>= 12.3.3', require: nil
|
||||
gem 'capybara', require: nil #, '2.10.1', require: nil
|
||||
gem 'selenium-webdriver'
|
||||
gem 'rack_session_access', require: nil
|
||||
gem 'webmock', require: nil
|
||||
gem 'stripe-ruby-mock', '~> 3.1.0.rc3', require: 'stripe_mock'
|
||||
gem 'timecop'
|
||||
gem 'mock_redis'
|
||||
gem 'simplecov', require: nil
|
||||
gem 'm'
|
||||
end
|
||||
|
|
637
Gemfile.lock
637
Gemfile.lock
|
@ -1,239 +1,488 @@
|
|||
GIT
|
||||
remote: https://github.com/stripe/stripe-ruby
|
||||
revision: 48f76057f425ab5c3bb147f3d71c3d36d951159f
|
||||
remote: https://github.com/neocities/dav4rack.git
|
||||
revision: 1bf1975c613d4f14d00f1e70ce7e0bb9e2e6cd9b
|
||||
ref: 1bf1975c613d4f14d00f1e70ce7e0bb9e2e6cd9b
|
||||
specs:
|
||||
stripe (1.11.0)
|
||||
json (~> 1.8.1)
|
||||
mime-types (~> 1.25)
|
||||
rest-client (~> 1.4)
|
||||
dav4rack (0.3.0)
|
||||
nokogiri (>= 1.4.2)
|
||||
rack (~> 3.0)
|
||||
uuidtools (~> 2.1.1)
|
||||
webrick
|
||||
|
||||
GEM
|
||||
remote: https://rubygems.org/
|
||||
specs:
|
||||
addressable (2.3.6)
|
||||
ago (0.1.5)
|
||||
ansi (1.4.3)
|
||||
autoparse (0.3.3)
|
||||
addressable (>= 2.3.1)
|
||||
extlib (>= 0.9.15)
|
||||
multi_json (>= 1.0.0)
|
||||
bcrypt (3.1.7)
|
||||
builder (3.2.2)
|
||||
capybara (2.2.1)
|
||||
mime-types (>= 1.16)
|
||||
nokogiri (>= 1.3.3)
|
||||
rack (>= 1.0.0)
|
||||
rack-test (>= 0.5.4)
|
||||
xpath (~> 2.0)
|
||||
capybara_minitest_spec (1.0.1)
|
||||
capybara (>= 2)
|
||||
minitest (>= 2)
|
||||
celluloid (0.15.2)
|
||||
timers (~> 1.1.0)
|
||||
cliver (0.3.2)
|
||||
coderay (1.1.0)
|
||||
columnize (0.3.6)
|
||||
connection_pool (2.0.0)
|
||||
crack (0.4.2)
|
||||
safe_yaml (~> 1.0.0)
|
||||
debugger (1.6.6)
|
||||
columnize (>= 0.3.1)
|
||||
debugger-linecache (~> 1.2.0)
|
||||
debugger-ruby_core_source (~> 1.3.2)
|
||||
debugger-linecache (1.2.0)
|
||||
debugger-ruby_core_source (1.3.2)
|
||||
docile (1.1.3)
|
||||
erubis (2.7.0)
|
||||
extlib (0.9.16)
|
||||
fabrication (2.11.0)
|
||||
faker (1.3.0)
|
||||
i18n (~> 0.5)
|
||||
faraday (0.9.0)
|
||||
multipart-post (>= 1.2, < 3)
|
||||
ffi (1.9.3)
|
||||
google-api-client (0.7.1)
|
||||
addressable (>= 2.3.2)
|
||||
autoparse (>= 0.3.3)
|
||||
extlib (>= 0.9.15)
|
||||
faraday (>= 0.9.0)
|
||||
jwt (>= 0.1.5)
|
||||
launchy (>= 2.1.1)
|
||||
multi_json (>= 1.0.0)
|
||||
retriable (>= 1.4)
|
||||
signet (>= 0.5.0)
|
||||
uuidtools (>= 2.1.0)
|
||||
hashie (2.0.5)
|
||||
hiredis (0.5.0)
|
||||
i18n (0.6.9)
|
||||
json (1.8.1)
|
||||
jwt (0.1.11)
|
||||
multi_json (>= 1.5)
|
||||
kgio (2.9.2)
|
||||
launchy (2.4.2)
|
||||
addressable (~> 2.3)
|
||||
magic (0.2.6)
|
||||
acme-client (2.0.19)
|
||||
base64 (~> 0.2.0)
|
||||
faraday (>= 1.0, < 3.0.0)
|
||||
faraday-retry (>= 1.0, < 3.0.0)
|
||||
activesupport (8.0.1)
|
||||
base64
|
||||
benchmark (>= 0.3)
|
||||
bigdecimal
|
||||
concurrent-ruby (~> 1.0, >= 1.3.1)
|
||||
connection_pool (>= 2.2.5)
|
||||
drb
|
||||
i18n (>= 1.6, < 2)
|
||||
logger (>= 1.4.2)
|
||||
minitest (>= 5.1)
|
||||
securerandom (>= 0.3)
|
||||
tzinfo (~> 2.0, >= 2.0.5)
|
||||
uri (>= 0.13.1)
|
||||
addressable (2.8.7)
|
||||
public_suffix (>= 2.0.2, < 7.0)
|
||||
adequate_crypto_address (0.1.9)
|
||||
base58 (~> 0.2)
|
||||
keccak (~> 1.3)
|
||||
airbrake (13.0.5)
|
||||
airbrake-ruby (~> 6.0)
|
||||
airbrake-ruby (6.2.2)
|
||||
rbtree3 (~> 0.6)
|
||||
ansi (1.5.0)
|
||||
base58 (0.2.3)
|
||||
base64 (0.2.0)
|
||||
bcrypt (3.1.20)
|
||||
benchmark (0.4.0)
|
||||
bigdecimal (3.1.9)
|
||||
builder (3.3.0)
|
||||
capybara (3.40.0)
|
||||
addressable
|
||||
matrix
|
||||
mini_mime (>= 0.1.3)
|
||||
nokogiri (~> 1.11)
|
||||
rack (>= 1.6.0)
|
||||
rack-test (>= 0.6.3)
|
||||
regexp_parser (>= 1.5, < 3.0)
|
||||
xpath (~> 3.2)
|
||||
certified (1.0.0)
|
||||
climate_control (1.2.0)
|
||||
coderay (1.1.3)
|
||||
concurrent-ruby (1.3.5)
|
||||
connection_pool (2.5.0)
|
||||
coveralls_reborn (0.28.0)
|
||||
simplecov (~> 0.22.0)
|
||||
term-ansicolor (~> 1.7)
|
||||
thor (~> 1.2)
|
||||
tins (~> 1.32)
|
||||
crack (1.0.0)
|
||||
bigdecimal
|
||||
rexml
|
||||
crass (1.0.6)
|
||||
csv (3.3.2)
|
||||
dante (0.2.0)
|
||||
date (3.4.1)
|
||||
dnsbl-client (1.1.1)
|
||||
docile (1.4.1)
|
||||
domain_name (0.6.20240107)
|
||||
drb (2.2.1)
|
||||
erubi (1.13.1)
|
||||
exifr (1.4.1)
|
||||
fabrication (2.31.0)
|
||||
facter (4.10.0)
|
||||
hocon (~> 1.3)
|
||||
thor (>= 1.0.1, < 1.3)
|
||||
faker (3.5.1)
|
||||
i18n (>= 1.8.11, < 2)
|
||||
faraday (1.10.4)
|
||||
faraday-em_http (~> 1.0)
|
||||
faraday-em_synchrony (~> 1.0)
|
||||
faraday-excon (~> 1.1)
|
||||
faraday-httpclient (~> 1.0)
|
||||
faraday-multipart (~> 1.0)
|
||||
faraday-net_http (~> 1.0)
|
||||
faraday-net_http_persistent (~> 1.0)
|
||||
faraday-patron (~> 1.0)
|
||||
faraday-rack (~> 1.0)
|
||||
faraday-retry (~> 1.0)
|
||||
ruby2_keywords (>= 0.0.4)
|
||||
faraday-em_http (1.0.0)
|
||||
faraday-em_synchrony (1.0.0)
|
||||
faraday-excon (1.1.0)
|
||||
faraday-httpclient (1.0.1)
|
||||
faraday-multipart (1.1.0)
|
||||
multipart-post (~> 2.0)
|
||||
faraday-net_http (1.0.2)
|
||||
faraday-net_http_persistent (1.2.0)
|
||||
faraday-patron (1.0.0)
|
||||
faraday-rack (1.0.0)
|
||||
faraday-retry (1.0.3)
|
||||
faraday_middleware (1.2.1)
|
||||
faraday (~> 1.0)
|
||||
feedjira (2.1.4)
|
||||
faraday (>= 0.9)
|
||||
faraday_middleware (>= 0.9)
|
||||
loofah (>= 2.0)
|
||||
sax-machine (>= 1.0)
|
||||
ffi (1.17.1-aarch64-linux-gnu)
|
||||
ffi (1.17.1-aarch64-linux-musl)
|
||||
ffi (1.17.1-arm-linux-gnu)
|
||||
ffi (1.17.1-arm-linux-musl)
|
||||
ffi (1.17.1-arm64-darwin)
|
||||
ffi (1.17.1-x86_64-darwin)
|
||||
ffi (1.17.1-x86_64-linux-gnu)
|
||||
ffi (1.17.1-x86_64-linux-musl)
|
||||
ffi-compiler (1.3.2)
|
||||
ffi (>= 1.15.5)
|
||||
rake
|
||||
filesize (0.2.0)
|
||||
fspath (3.1.2)
|
||||
gandi (3.3.28)
|
||||
hashie
|
||||
xmlrpc
|
||||
geoip (1.6.4)
|
||||
hashdiff (1.1.2)
|
||||
hashie (5.0.0)
|
||||
hiredis (0.6.3)
|
||||
hocon (1.4.0)
|
||||
hoe (4.2.2)
|
||||
rake (>= 0.8, < 15.0)
|
||||
htmlentities (4.3.4)
|
||||
http (5.2.0)
|
||||
addressable (~> 2.8)
|
||||
base64 (~> 0.1)
|
||||
http-cookie (~> 1.0)
|
||||
http-form_data (~> 2.2)
|
||||
llhttp-ffi (~> 0.5.0)
|
||||
http-accept (1.7.0)
|
||||
http-cookie (1.0.8)
|
||||
domain_name (~> 0.5)
|
||||
http-form_data (2.3.0)
|
||||
i18n (1.14.7)
|
||||
concurrent-ruby (~> 1.0)
|
||||
image_optim (0.31.4)
|
||||
exifr (~> 1.2, >= 1.2.2)
|
||||
fspath (~> 3.0)
|
||||
image_size (>= 1.5, < 4)
|
||||
in_threads (~> 1.3)
|
||||
progress (~> 3.0, >= 3.0.1)
|
||||
image_optim_pack (0.11.2)
|
||||
fspath (>= 2.1, < 4)
|
||||
image_optim (~> 0.19)
|
||||
image_optim_pack (0.11.2-x86_64-darwin)
|
||||
fspath (>= 2.1, < 4)
|
||||
image_optim (~> 0.19)
|
||||
image_optim_pack (0.11.2-x86_64-linux)
|
||||
fspath (>= 2.1, < 4)
|
||||
image_optim (~> 0.19)
|
||||
image_optimizer (1.9.0)
|
||||
image_size (3.4.0)
|
||||
in_threads (1.6.0)
|
||||
io-extra (1.4.0)
|
||||
ipaddress (0.8.3)
|
||||
json (2.9.1)
|
||||
jwt (2.10.1)
|
||||
base64
|
||||
keccak (1.3.2)
|
||||
llhttp-ffi (0.5.0)
|
||||
ffi-compiler (~> 1.0)
|
||||
rake (~> 13.0)
|
||||
logger (1.6.5)
|
||||
loofah (2.24.0)
|
||||
crass (~> 1.0.2)
|
||||
nokogiri (>= 1.12.0)
|
||||
m (1.6.2)
|
||||
method_source (>= 0.6.7)
|
||||
rake (>= 0.9.2.2)
|
||||
magic (0.2.9)
|
||||
ffi (>= 0.6.3)
|
||||
mail (2.5.4)
|
||||
mime-types (~> 1.16)
|
||||
treetop (~> 1.4.8)
|
||||
metaclass (0.0.4)
|
||||
method_source (0.8.2)
|
||||
mime-types (1.25.1)
|
||||
mini_portile (0.5.3)
|
||||
minitest (5.3.1)
|
||||
minitest-reporters (1.0.2)
|
||||
mail (2.8.1)
|
||||
mini_mime (>= 0.1.1)
|
||||
net-imap
|
||||
net-pop
|
||||
net-smtp
|
||||
matrix (0.4.2)
|
||||
maxmind-db (1.2.0)
|
||||
maxmind-geoip2 (1.2.0)
|
||||
connection_pool (~> 2.2)
|
||||
http (>= 4.3, < 6.0)
|
||||
maxmind-db (~> 1.2)
|
||||
method_source (1.1.0)
|
||||
mime-types (3.6.0)
|
||||
logger
|
||||
mime-types-data (~> 3.2015)
|
||||
mime-types-data (3.2025.0107)
|
||||
minfraud (2.6.0)
|
||||
connection_pool (~> 2.2)
|
||||
http (>= 4.3, < 6.0)
|
||||
maxmind-geoip2 (~> 1.2)
|
||||
simpleidn (~> 0.1, >= 0.1.1)
|
||||
mini_mime (1.1.5)
|
||||
minitest (5.25.4)
|
||||
minitest-reporters (1.7.1)
|
||||
ansi
|
||||
builder
|
||||
minitest (>= 5.0)
|
||||
powerbar
|
||||
mocha (1.0.0)
|
||||
metaclass (~> 0.0.1)
|
||||
multi_json (1.9.2)
|
||||
multipart-post (2.0.0)
|
||||
nokogiri (1.6.1)
|
||||
mini_portile (~> 0.5.0)
|
||||
pg (0.17.1)
|
||||
phantomjs (1.9.7.0)
|
||||
poltergeist (1.5.0)
|
||||
capybara (~> 2.1)
|
||||
cliver (~> 0.3.1)
|
||||
multi_json (~> 1.0)
|
||||
websocket-driver (>= 0.2.0)
|
||||
polyglot (0.3.4)
|
||||
powerbar (1.0.11)
|
||||
ansi (~> 1.4.0)
|
||||
hashie (>= 1.1.0)
|
||||
pry (0.9.12.6)
|
||||
coderay (~> 1.0)
|
||||
method_source (~> 0.8)
|
||||
slop (~> 3.4)
|
||||
pry-debugger (0.2.2)
|
||||
debugger (~> 1.3)
|
||||
pry (~> 0.9.10)
|
||||
puma (2.8.1)
|
||||
rack (>= 1.1, < 2.0)
|
||||
rack (1.5.2)
|
||||
rack-protection (1.5.2)
|
||||
rack
|
||||
rack-recaptcha (0.6.6)
|
||||
json
|
||||
rack-test (0.6.2)
|
||||
ruby-progressbar
|
||||
mocha (2.7.1)
|
||||
ruby2_keywords (>= 0.0.5)
|
||||
mock_redis (0.49.0)
|
||||
redis (~> 5)
|
||||
monetize (1.13.0)
|
||||
money (~> 6.12)
|
||||
money (6.19.0)
|
||||
i18n (>= 0.6.4, <= 2)
|
||||
msgpack (1.7.5)
|
||||
multi_json (1.15.0)
|
||||
multipart-post (2.4.1)
|
||||
mustermann (3.0.3)
|
||||
ruby2_keywords (~> 0.0.1)
|
||||
net-imap (0.5.6)
|
||||
date
|
||||
net-protocol
|
||||
net-pop (0.1.2)
|
||||
net-protocol
|
||||
net-protocol (0.2.2)
|
||||
timeout
|
||||
net-smtp (0.5.1)
|
||||
net-protocol
|
||||
netrc (0.11.0)
|
||||
nio4r (2.7.4)
|
||||
nokogiri (1.18.8-aarch64-linux-gnu)
|
||||
racc (~> 1.4)
|
||||
nokogiri (1.18.8-aarch64-linux-musl)
|
||||
racc (~> 1.4)
|
||||
nokogiri (1.18.8-arm-linux-gnu)
|
||||
racc (~> 1.4)
|
||||
nokogiri (1.18.8-arm-linux-musl)
|
||||
racc (~> 1.4)
|
||||
nokogiri (1.18.8-arm64-darwin)
|
||||
racc (~> 1.4)
|
||||
nokogiri (1.18.8-x86_64-darwin)
|
||||
racc (~> 1.4)
|
||||
nokogiri (1.18.8-x86_64-linux-gnu)
|
||||
racc (~> 1.4)
|
||||
nokogiri (1.18.8-x86_64-linux-musl)
|
||||
racc (~> 1.4)
|
||||
ostruct (0.6.1)
|
||||
paypal-recurring (1.1.0)
|
||||
pg (1.5.9)
|
||||
phonelib (0.10.3)
|
||||
progress (3.6.0)
|
||||
pry (0.15.2)
|
||||
coderay (~> 1.1)
|
||||
method_source (~> 1.0)
|
||||
public_suffix (6.0.1)
|
||||
puma (6.6.0)
|
||||
nio4r (~> 2.0)
|
||||
racc (1.8.1)
|
||||
rack (3.1.12)
|
||||
rack-protection (4.1.1)
|
||||
base64 (>= 0.1.0)
|
||||
logger (>= 1.6.0)
|
||||
rack (>= 3.0.0, < 4)
|
||||
rack-session (2.1.0)
|
||||
base64 (>= 0.1.0)
|
||||
rack (>= 3.0.0)
|
||||
rack-test (2.2.0)
|
||||
rack (>= 1.3)
|
||||
rack_session_access (0.2.0)
|
||||
builder (>= 2.0.0)
|
||||
rack (>= 1.0.0)
|
||||
rake (13.2.1)
|
||||
rb-fsevent (0.11.2)
|
||||
rb-inotify (0.11.1)
|
||||
ffi (~> 1.0)
|
||||
rbtree3 (0.7.1)
|
||||
redis (5.3.0)
|
||||
redis-client (>= 0.22.0)
|
||||
redis-client (0.23.2)
|
||||
connection_pool
|
||||
redis-namespace (1.11.0)
|
||||
redis (>= 4)
|
||||
regexp_parser (2.10.0)
|
||||
rest-client (2.1.0)
|
||||
http-accept (>= 1.7.0, < 2.0)
|
||||
http-cookie (>= 1.0.2, < 2.0)
|
||||
mime-types (>= 1.16, < 4.0)
|
||||
netrc (~> 0.8)
|
||||
rexml (3.4.1)
|
||||
rinku (2.0.6)
|
||||
rszr (1.5.0)
|
||||
ruby-progressbar (1.13.0)
|
||||
ruby2_keywords (0.0.5)
|
||||
rubyzip (2.4.1)
|
||||
sanitize (7.0.0)
|
||||
crass (~> 1.0.2)
|
||||
nokogiri (>= 1.16.8)
|
||||
sass (3.7.4)
|
||||
sass-listen (~> 4.0.0)
|
||||
sass-listen (4.0.0)
|
||||
rb-fsevent (~> 0.9, >= 0.9.4)
|
||||
rb-inotify (~> 0.9, >= 0.9.7)
|
||||
sax-machine (1.3.2)
|
||||
securerandom (0.4.1)
|
||||
selenium-webdriver (4.28.0)
|
||||
base64 (~> 0.2)
|
||||
logger (~> 1.4)
|
||||
rexml (~> 3.2, >= 3.2.5)
|
||||
rubyzip (>= 1.2.2, < 3.0)
|
||||
websocket (~> 1.0)
|
||||
sequel (5.89.0)
|
||||
bigdecimal
|
||||
sequel_pg (1.17.1)
|
||||
pg (>= 0.18.0, != 1.2.0)
|
||||
sequel (>= 4.38.0)
|
||||
shotgun (0.9.2)
|
||||
rack (>= 1.0)
|
||||
rainbows (4.6.1)
|
||||
kgio (~> 2.5)
|
||||
rack (~> 1.1)
|
||||
unicorn (~> 4.8)
|
||||
raindrops (0.13.0)
|
||||
rake (10.2.1)
|
||||
redis (3.0.7)
|
||||
redis-namespace (1.4.1)
|
||||
redis (~> 3.0.4)
|
||||
rest-client (1.6.7)
|
||||
mime-types (>= 1.16)
|
||||
retriable (1.4.1)
|
||||
rmagick (2.13.2)
|
||||
rubyzip (1.1.2)
|
||||
safe_yaml (1.0.1)
|
||||
screencap (0.1.1)
|
||||
phantomjs
|
||||
sequel (4.8.0)
|
||||
sequel_pg (1.6.9)
|
||||
pg (>= 0.8.0)
|
||||
sequel (>= 3.39.0)
|
||||
shotgun (0.9)
|
||||
rack (>= 1.0)
|
||||
sidekiq (3.0.0)
|
||||
celluloid (>= 0.15.2)
|
||||
connection_pool (>= 2.0.0)
|
||||
json
|
||||
redis (>= 3.0.6)
|
||||
redis-namespace (>= 1.3.1)
|
||||
signet (0.5.0)
|
||||
addressable (>= 2.2.3)
|
||||
faraday (>= 0.9.0.rc5)
|
||||
jwt (>= 0.1.5)
|
||||
multi_json (>= 1.0.0)
|
||||
simplecov (0.8.2)
|
||||
docile (~> 1.1.0)
|
||||
multi_json
|
||||
simplecov-html (~> 0.8.0)
|
||||
simplecov-html (0.8.0)
|
||||
sinatra (1.4.4)
|
||||
rack (~> 1.4)
|
||||
rack-protection (~> 1.4)
|
||||
tilt (~> 1.3, >= 1.3.4)
|
||||
sidekiq (7.3.8)
|
||||
base64
|
||||
connection_pool (>= 2.3.0)
|
||||
logger
|
||||
rack (>= 2.2.4)
|
||||
redis-client (>= 0.22.2)
|
||||
simplecov (0.22.0)
|
||||
docile (~> 1.1)
|
||||
simplecov-html (~> 0.11)
|
||||
simplecov_json_formatter (~> 0.1)
|
||||
simplecov-html (0.13.1)
|
||||
simplecov_json_formatter (0.1.4)
|
||||
simpleidn (0.2.3)
|
||||
sinatra (4.1.1)
|
||||
logger (>= 1.6.0)
|
||||
mustermann (~> 3.0)
|
||||
rack (>= 3.0.0, < 4)
|
||||
rack-protection (= 4.1.1)
|
||||
rack-session (>= 2.0.0, < 3)
|
||||
tilt (~> 2.0)
|
||||
sinatra-flash (0.3.0)
|
||||
sinatra (>= 1.0.0)
|
||||
sinatra-xsendfile (0.4.2)
|
||||
sinatra (>= 0.9.1)
|
||||
slim (2.0.2)
|
||||
temple (~> 0.6.6)
|
||||
tilt (>= 1.3.3, < 2.1)
|
||||
slop (3.5.0)
|
||||
temple (0.6.7)
|
||||
tilt (1.4.1)
|
||||
timers (1.1.0)
|
||||
treetop (1.4.15)
|
||||
polyglot
|
||||
polyglot (>= 0.3.1)
|
||||
unicorn (4.8.2)
|
||||
kgio (~> 2.6)
|
||||
rack
|
||||
raindrops (~> 0.7)
|
||||
uuidtools (2.1.4)
|
||||
webmock (1.17.4)
|
||||
addressable (>= 2.2.7)
|
||||
stripe (5.55.0)
|
||||
stripe-ruby-mock (3.1.0)
|
||||
dante (>= 0.2.0)
|
||||
multi_json (~> 1.0)
|
||||
stripe (> 5, < 6)
|
||||
sync (0.5.0)
|
||||
term-ansicolor (1.11.2)
|
||||
tins (~> 1.0)
|
||||
terrapin (1.0.1)
|
||||
climate_control
|
||||
thor (1.2.2)
|
||||
thread (0.2.2)
|
||||
tilt (2.6.0)
|
||||
timecop (0.9.10)
|
||||
timeout (0.4.3)
|
||||
tins (1.38.0)
|
||||
bigdecimal
|
||||
sync
|
||||
twilio-ruby (7.4.3)
|
||||
benchmark
|
||||
faraday (>= 0.9, < 3.0)
|
||||
jwt (>= 1.5, < 3.0)
|
||||
nokogiri (>= 1.6, < 2.0)
|
||||
ostruct
|
||||
tzinfo (2.0.6)
|
||||
concurrent-ruby (~> 1.0)
|
||||
uri (1.0.3)
|
||||
uuidtools (2.1.5)
|
||||
webmock (3.25.0)
|
||||
addressable (>= 2.8.0)
|
||||
crack (>= 0.3.2)
|
||||
websocket-driver (0.3.2)
|
||||
xpath (2.0.0)
|
||||
nokogiri (~> 1.3)
|
||||
hashdiff (>= 0.4.0, < 2.0.0)
|
||||
webp-ffi (0.4.0)
|
||||
ffi (>= 1.9.0)
|
||||
ffi-compiler (>= 0.1.2)
|
||||
webrick (1.9.1)
|
||||
websocket (1.2.11)
|
||||
will_paginate (4.0.1)
|
||||
xmlrpc (0.3.3)
|
||||
webrick
|
||||
xpath (3.2.0)
|
||||
nokogiri (~> 1.8)
|
||||
zip_tricks (5.6.0)
|
||||
|
||||
PLATFORMS
|
||||
ruby
|
||||
aarch64-linux-gnu
|
||||
aarch64-linux-musl
|
||||
arm-linux-gnu
|
||||
arm-linux-musl
|
||||
arm64-darwin
|
||||
x86_64-darwin
|
||||
x86_64-linux-gnu
|
||||
x86_64-linux-musl
|
||||
|
||||
DEPENDENCIES
|
||||
ago
|
||||
acme-client (~> 2.0.0)
|
||||
activesupport
|
||||
addressable (>= 2.8.0)
|
||||
adequate_crypto_address
|
||||
airbrake
|
||||
bcrypt
|
||||
capybara
|
||||
capybara_minitest_spec
|
||||
erubis
|
||||
certified
|
||||
coveralls_reborn
|
||||
csv
|
||||
dav4rack!
|
||||
dnsbl-client
|
||||
erubi
|
||||
fabrication
|
||||
facter
|
||||
faker
|
||||
google-api-client
|
||||
feedjira (= 2.1.4)
|
||||
filesize
|
||||
gandi
|
||||
geoip
|
||||
hiredis
|
||||
jdbc-postgres
|
||||
jruby-openssl
|
||||
json
|
||||
hoe
|
||||
htmlentities
|
||||
http
|
||||
image_optim
|
||||
image_optim_pack
|
||||
image_optimizer
|
||||
io-extra
|
||||
ipaddress
|
||||
json (>= 2.3.0)
|
||||
m
|
||||
magic
|
||||
mail
|
||||
maxmind-db
|
||||
minfraud
|
||||
minitest
|
||||
minitest-reporters
|
||||
mocha
|
||||
mock_redis
|
||||
monetize
|
||||
msgpack
|
||||
nokogiri
|
||||
paypal-recurring
|
||||
pg
|
||||
phantomjs
|
||||
poltergeist
|
||||
phonelib
|
||||
pry
|
||||
pry-debugger
|
||||
puma
|
||||
rack-recaptcha
|
||||
puma (< 7)
|
||||
rack-test
|
||||
rainbows
|
||||
rake
|
||||
rack_session_access
|
||||
rake (>= 12.3.3)
|
||||
redis
|
||||
rmagick
|
||||
ruby-debug
|
||||
redis-namespace
|
||||
rest-client
|
||||
rinku
|
||||
rszr
|
||||
rubyzip
|
||||
sanitize
|
||||
sass
|
||||
screencap
|
||||
selenium-webdriver
|
||||
sequel
|
||||
sequel_pg
|
||||
shotgun
|
||||
sidekiq
|
||||
sidekiq (~> 7)
|
||||
simplecov
|
||||
simpleidn
|
||||
sinatra
|
||||
sinatra-flash
|
||||
sinatra-xsendfile
|
||||
slim
|
||||
stripe!
|
||||
stripe
|
||||
stripe-ruby-mock (~> 3.1.0.rc3)
|
||||
terrapin
|
||||
thread
|
||||
tilt
|
||||
timecop
|
||||
twilio-ruby
|
||||
webmock
|
||||
webp-ffi
|
||||
will_paginate
|
||||
xmlrpc
|
||||
zip_tricks
|
||||
|
||||
BUNDLED WITH
|
||||
2.6.3
|
||||
|
|
57
README.md
57
README.md
|
@ -1,63 +1,36 @@
|
|||
# NeoCities.org
|
||||
### NOTE: THIS IS NOT FOR NEOCITIES SUPPORT! Any issues filed not related to the source code itself will be closed. For support please contact: https://neocities.org/contact
|
||||
|
||||
[](https://travis-ci.org/neocities/neocities)
|
||||
# Neocities.org
|
||||
|
||||
The web site for NeoCities! It's open source. Want a feature on the site? Send a pull request!
|
||||
[](https://github.com/neocities/neocities/actions?query=workflow%3ACI)
|
||||
[](https://coveralls.io/github/neocities/neocities?branch=master)
|
||||
|
||||
## Installation (OSX)
|
||||
The web site for Neocities! It's open source. Want a feature on the site? Send a pull request!
|
||||
|
||||
Install homebrew:
|
||||
```
|
||||
ruby -e "$(curl -fsSL https://raw.github.com/Homebrew/homebrew/go/install)"
|
||||
```
|
||||
## Getting Started
|
||||
|
||||
Install deps:
|
||||
```
|
||||
$ brew install redis postgresql phantomjs libmagic imagemagick
|
||||
```
|
||||
|
||||
Fork the repository on Github.
|
||||
Clone the forked repo to your local machine: git clone git@github.com:YOURUSERNAME/neocities.git
|
||||
Install deps:
|
||||
Neocities can be quickly launched in development mode with [Vagrant](https://www.vagrantup.com). Vagrant builds a virtual machine that automatically installs everything you need to run Neocities as a developer. Install Vagrant, then from the command line:
|
||||
|
||||
```
|
||||
$ cd neocities
|
||||
$ gem install bundler
|
||||
$ bundle install
|
||||
vagrant up --provision
|
||||
```
|
||||
|
||||
Create postgres databases:
|
||||

|
||||
|
||||
Make a copy of `config.yml.template` in the root directory, and rename it to `config.yml`. Then:
|
||||
|
||||
```
|
||||
createdb neocities_test
|
||||
createdb neocities_dev
|
||||
vagrant ssh
|
||||
bundle exec rackup -o 0.0.0.0
|
||||
```
|
||||
|
||||
Copy config.yml.template to config.yml and edit to something like this:
|
||||
```
|
||||
development:
|
||||
database: 'postgres://neocities@127.0.0.1/neocities_dev'
|
||||
database_pool: 1
|
||||
session_secret: SECRET1234
|
||||
recaptcha_public_key: ENTER RECAPTCHA PUBLIC KEY HERE
|
||||
recaptcha_private_key: ENTER RECAPTCHA PRIVATE KEY HERE
|
||||
sidekiq_user: sidekiq
|
||||
sidekiq_pass: sidekiq
|
||||
phantomjs_url:
|
||||
- http://localhost:8910
|
||||
```
|
||||
|
||||
Run the tests to see if they work:
|
||||
|
||||
```
|
||||
bundle exec rake test
|
||||
```
|
||||
Now you can access the running site from your browser: http://127.0.0.1:9292
|
||||
|
||||
## Want to contribute?
|
||||
|
||||
If you'd like to fix a bug, or make an improvement, or add a new feature, it's easy! Just send us a Pull Request.
|
||||
|
||||
1. Fork it ( http://github.com/YOURUSERNAME/neocities/fork )
|
||||
1. Fork it (https://github.com/neocities/neocities/fork)
|
||||
2. Create your feature branch (`git checkout -b my-new-feature`)
|
||||
3. Commit your changes (`git commit -am 'Add some feature'`)
|
||||
4. Push to the branch (`git push origin my-new-feature`)
|
||||
|
|
255
Rakefile
255
Rakefile
|
@ -1,4 +1,4 @@
|
|||
require "rake/testtask"
|
||||
require 'rake/testtask'
|
||||
|
||||
task :environment do
|
||||
require './environment.rb'
|
||||
|
@ -7,36 +7,253 @@ end
|
|||
desc "Run all tests"
|
||||
Rake::TestTask.new do |t|
|
||||
t.libs << "spec"
|
||||
t.test_files = FileList['tests/*_tests.rb']
|
||||
t.verbose = true
|
||||
t.test_files = FileList['tests/**/*_tests.rb']
|
||||
t.verbose = false
|
||||
t.warning = false
|
||||
end
|
||||
|
||||
task :default => :test
|
||||
|
||||
desc "prune logs"
|
||||
task :prune_logs => [:environment] do
|
||||
Stat.prune!
|
||||
StatLocation.prune!
|
||||
StatReferrer.prune!
|
||||
StatPath.prune!
|
||||
end
|
||||
|
||||
desc "parse logs"
|
||||
task :parse_logs => [:environment] do
|
||||
hits = {}
|
||||
logfile = File.open '/var/log/nginx/neocities-sites.log.1', 'r'
|
||||
while hit = logfile.gets
|
||||
hit = hit.split ' '
|
||||
Stat.parse_logfiles $config['logs_path']
|
||||
end
|
||||
|
||||
# It says hits, but really we're tracking visits to index"
|
||||
if hit[3] == '/'
|
||||
hits[hit[1]] ||= 0
|
||||
hits[hit[1]] += 1
|
||||
desc 'Update disposable email blacklist'
|
||||
task :update_disposable_email_blacklist => [:environment] do
|
||||
# Formerly: https://raw.githubusercontent.com/martenson/disposable-email-domains/master/disposable_email_blocklist.conf
|
||||
uri = URI.parse('https://raw.githubusercontent.com/disposable/disposable-email-domains/master/domains.txt')
|
||||
File.write(Site::DISPOSABLE_EMAIL_BLACKLIST_PATH, HTTP.get(uri))
|
||||
end
|
||||
|
||||
desc 'Update banned IPs list'
|
||||
task :update_blocked_ips => [:environment] do
|
||||
|
||||
filename = 'listed_ip_365_ipv46'
|
||||
zip_path = "/tmp/#{filename}.zip"
|
||||
|
||||
File.open(zip_path, 'wb') do |file|
|
||||
response = HTTP.get "https://www.stopforumspam.com/downloads/#{filename}.zip"
|
||||
response.body.each do |chunk|
|
||||
file.write chunk
|
||||
end
|
||||
end
|
||||
logfile.close
|
||||
|
||||
hits.each do |username,hitcount|
|
||||
DB['update sites set hits=hits+? where username=?', hitcount, username].first
|
||||
Zip::File.open(zip_path) do |zip_file|
|
||||
zip_file.each do |entry|
|
||||
if entry.name == "#{filename}.txt"
|
||||
ips = entry.get_input_stream.read
|
||||
insert_hashes = []
|
||||
ips.each_line { |ip| insert_hashes << { ip: ip.strip, created_at: Time.now } }
|
||||
ips = nil
|
||||
|
||||
# Database transaction
|
||||
DB.transaction do
|
||||
DB[:blocked_ips].delete
|
||||
DB[:blocked_ips].multi_insert insert_hashes
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
FileUtils.rm zip_path
|
||||
end
|
||||
|
||||
desc 'Update screenshots'
|
||||
task :update_screenshots => [:environment] do
|
||||
Site.select(:username).filter(is_banned: false).filter(~{updated_at: nil}).order(:updated_at.desc).all.collect {|s|
|
||||
ScreenshotWorker.perform_async s.username
|
||||
}
|
||||
desc 'rebuild_thumbnails'
|
||||
task :rebuild_thumbnails => [:environment] do
|
||||
dirs = Dir[Site::SITE_FILES_ROOT+'/**/*'].collect {|s| s.sub(Site::SITE_FILES_ROOT, '')}.collect {|s| s.sub('/', '')}
|
||||
dirs.each do |d|
|
||||
next if File.directory?(d)
|
||||
|
||||
full_path = d.split('/')
|
||||
|
||||
username = full_path.first
|
||||
path = '/'+full_path[1..full_path.length].join('/')
|
||||
|
||||
if Pathname(path).extname.gsub('.', '').match Site::IMAGE_REGEX
|
||||
begin
|
||||
ThumbnailWorker.new.perform username, path
|
||||
rescue Magick::ImageMagickError
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
desc 'compute_scores'
|
||||
task :compute_scores => [:environment] do
|
||||
Site.compute_scores
|
||||
end
|
||||
|
||||
desc 'renew_ssl_certs'
|
||||
task :renew_ssl_certs => [:environment] do
|
||||
delay = 0
|
||||
DB[%{select id from sites where (domain is not null or domain != '') and is_banned != 't' and is_deleted != 't' and (cert_updated_at is null or cert_updated_at < ?)}, 60.days.ago].all.each do |site|
|
||||
LetsEncryptWorker.perform_in delay.seconds, site[:id]
|
||||
delay += 10
|
||||
end
|
||||
end
|
||||
|
||||
desc 'purge_tmp_turds'
|
||||
task :purge_tmp_turds => [:environment] do
|
||||
['neocities_screenshot*', 'RackMultipart*', 'neocities_saving_file*', 'newinstall-*', '*.dmp', 'davfile*', 'magick*', '*.scan', '*.jpg'].each do |target|
|
||||
Dir.glob("/tmp/#{target}").select {|filename| File::Stat.new(filename).ctime < (Time.now - 3600)}.each {|filename| FileUtils.rm(filename)}
|
||||
end
|
||||
end
|
||||
|
||||
desc 'compute_follow_count_scores'
|
||||
task :compute_follow_count_scores => [:environment] do
|
||||
|
||||
Site.select(:id,:username,:follow_count).all.each do |site|
|
||||
count = site.scorable_follow_count
|
||||
|
||||
if count != 0
|
||||
puts "#{site.username} #{site.follow_count} => #{count}"
|
||||
end
|
||||
DB['update sites set follow_count=? where id=?', count, site.id].first
|
||||
end
|
||||
end
|
||||
|
||||
desc 'ml_screenshots_list_dump'
|
||||
task :ml_screenshots_list_dump => [:environment] do
|
||||
['phishing', 'spam', 'ham', nil].each do |classifier|
|
||||
File.open("./files/screenshot-urls#{classifier.nil? ? '' : '-'+classifier.to_s}.txt", 'w') do |fp|
|
||||
SiteFile.where(classifier: classifier).where(path: 'index.html').each do |site_file|
|
||||
begin
|
||||
fp.write "#{site_file.site.screenshot_url('index.html', Site::SCREENSHOT_RESOLUTIONS.first)}\n"
|
||||
rescue NoMethodError
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
desc 'generate_sitemap'
|
||||
task :generate_sitemap => [:environment] do
|
||||
sorted_sites = {}
|
||||
|
||||
# We pop off array, so highest scores go last.
|
||||
sites = Site.
|
||||
select(:id, :username, :updated_at, :profile_enabled).
|
||||
where(site_changed: true).
|
||||
exclude(updated_at: nil).
|
||||
exclude(is_deleted: true).
|
||||
order(:score).
|
||||
all
|
||||
|
||||
site_files = []
|
||||
|
||||
sites.each do |site|
|
||||
site.site_files_dataset.exclude(path: 'not_found.html').where(path: /\.html?$/).all.each do |site_file|
|
||||
|
||||
if site.uri(site_file.path) == site.uri
|
||||
priority = 0.5
|
||||
else
|
||||
priority = 0.4
|
||||
end
|
||||
|
||||
site_files << [site.uri(site_file.path), site_file.updated_at.utc.iso8601, priority]
|
||||
end
|
||||
end
|
||||
|
||||
sites = nil
|
||||
GC.start
|
||||
|
||||
sitemap_root = File.join Site::PUBLIC_ROOT, 'sitemap'
|
||||
FileUtils.mkdir_p sitemap_root
|
||||
|
||||
index = 0
|
||||
until site_files.empty?
|
||||
sfs = site_files.pop 50000
|
||||
|
||||
file = File.open File.join(sitemap_root, "sites-#{index}.xml.gz"), 'w'
|
||||
|
||||
Zlib::GzipWriter.open File.join(sitemap_root, "sites-#{index}.xml.gz") do |gz|
|
||||
gz.write %{<?xml version="1.0" encoding="UTF-8"?>\n}
|
||||
gz.write %{<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">\n}
|
||||
|
||||
sfs.each do |sf|
|
||||
gz.write %{<url><loc>#{sf[0].encode(xml: :text)}</loc><lastmod>#{sf[1].encode(xml: :text)}</lastmod><priority>#{sf[2].to_s.encode(xml: :text)}</priority></url>\n}
|
||||
end
|
||||
|
||||
gz.write %{</urlset>}
|
||||
end
|
||||
|
||||
index += 1
|
||||
end
|
||||
|
||||
|
||||
# Set basic neocities.org root paths
|
||||
builder = Nokogiri::XML::Builder.new { |xml|
|
||||
xml.urlset(xmlns: 'http://www.sitemaps.org/schemas/sitemap/0.9') {
|
||||
File.read(File.join(DIR_ROOT, 'files', 'root_site_uris.txt')).each_line { |uri|
|
||||
priority, changefreq, uri = uri.strip.split(',')
|
||||
xml.url {
|
||||
xml.loc uri
|
||||
xml.changefreq changefreq
|
||||
xml.priority priority
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Zlib::GzipWriter.open File.join(sitemap_root, 'root.xml.gz') do |gz|
|
||||
gz.write builder.to_xml(encoding: 'UTF-8')
|
||||
end
|
||||
|
||||
|
||||
# Tagged sites sitemap
|
||||
builder = Nokogiri::XML::Builder.new { |xml|
|
||||
xml.urlset(xmlns: 'http://www.sitemaps.org/schemas/sitemap/0.9') {
|
||||
Tag.popular_names(Site.count).each { |tag|
|
||||
xml.url {
|
||||
xml.loc "https://neocities.org/browse?sort_by=views&tag=#{tag[:name]}"
|
||||
xml.changefreq 'daily'
|
||||
xml.lastmod Time.now.utc.iso8601
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Zlib::GzipWriter.open File.join(sitemap_root, 'tags.xml.gz') do |gz|
|
||||
gz.write builder.to_xml(encoding: 'UTF-8')
|
||||
end
|
||||
|
||||
|
||||
# Final index.xml.gz entrypoint
|
||||
Zlib::GzipWriter.open File.join(sitemap_root, 'index.xml.gz') do |gz|
|
||||
gz.write %{<?xml version="1.0" encoding="UTF-8"?>\n}
|
||||
gz.write %{<sitemapindex xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">\n}
|
||||
gz.write %{<sitemap><loc>https://neocities.org/sitemap/root.xml.gz</loc><lastmod>#{Time.now.utc.iso8601}</lastmod></sitemap>\n}
|
||||
gz.write %{<sitemap><loc>https://neocities.org/sitemap/tags.xml.gz</loc><lastmod>#{Time.now.utc.iso8601}</lastmod></sitemap>\n}
|
||||
0.upto(index-1).each do |i|
|
||||
gz.write %{<sitemap><loc>https://neocities.org/sitemap/sites-#{i}.xml.gz</loc><lastmod>#{Time.now.utc.iso8601}</lastmod></sitemap>\n}
|
||||
end
|
||||
gz.write %{</sitemapindex>}
|
||||
end
|
||||
end
|
||||
|
||||
desc 'dedupe tags'
|
||||
task :dedupetags => [:environment] do
|
||||
Tag.all.each do |tag|
|
||||
begin
|
||||
tag.reload
|
||||
rescue Sequel::Error => e
|
||||
next if e.message =~ /Record not found/
|
||||
end
|
||||
|
||||
matching_tags = Tag.exclude(id: tag.id).where(name: tag.name).all
|
||||
|
||||
matching_tags.each do |matching_tag|
|
||||
DB[:sites_tags].where(tag_id: matching_tag.id).update(tag_id: tag.id)
|
||||
matching_tag.delete
|
||||
end
|
||||
end
|
||||
end
|
12
Vagrantfile
vendored
Normal file
12
Vagrantfile
vendored
Normal file
|
@ -0,0 +1,12 @@
|
|||
VAGRANTFILE_API_VERSION = '2'
|
||||
|
||||
Vagrant.configure(VAGRANTFILE_API_VERSION) do |config|
|
||||
config.vm.box = 'ubuntu/jammy64'
|
||||
config.vm.provision :shell, path: './vagrant/development.sh'
|
||||
config.vm.network :forwarded_port, guest: 9292, host: 9292
|
||||
|
||||
config.vm.provider :virtualbox do |vb|
|
||||
vb.customize ['modifyvm', :id, '--memory', '8192']
|
||||
vb.name = 'neocities'
|
||||
end
|
||||
end
|
52
app/activity.rb
Normal file
52
app/activity.rb
Normal file
|
@ -0,0 +1,52 @@
|
|||
get '/activity' do
|
||||
@page = params[:page] || 1
|
||||
|
||||
if params[:tag]
|
||||
query1 = Event
|
||||
.join(:sites, id: :site_id)
|
||||
.join(:sites_tags, site_id: :id)
|
||||
.join(:tags, id: :tag_id)
|
||||
.where(tags__name: params[:tag])
|
||||
.where(events__is_deleted: false, sites__is_deleted: false)
|
||||
.where{sites__score > Event::ACTIVITY_TAG_SCORE_LIMIT}
|
||||
.where(sites__is_nsfw: false)
|
||||
.where(follow_id: nil)
|
||||
.select_all(:events)
|
||||
|
||||
query2 = Event
|
||||
.join(:sites, id: :actioning_site_id)
|
||||
.join(:sites_tags, site_id: :id)
|
||||
.join(:tags, id: :tag_id)
|
||||
.where(tags__name: params[:tag])
|
||||
.where(events__is_deleted: false, sites__is_deleted: false)
|
||||
.where{sites__score > Event::ACTIVITY_TAG_SCORE_LIMIT}
|
||||
.where(sites__is_nsfw: false)
|
||||
.where(follow_id: nil)
|
||||
.select_all(:events)
|
||||
|
||||
if current_site
|
||||
blocking_site_ids = current_site.blocking_site_ids
|
||||
query1 = query1.where(Sequel.|({events__site_id: nil}, ~{events__site_id: blocking_site_ids})).where(Sequel.|({events__actioning_site_id: nil}, ~{events__actioning_site_id: blocking_site_ids}))
|
||||
query2 = query2.where(Sequel.|({events__site_id: nil}, ~{events__site_id: blocking_site_ids})).where(Sequel.|({events__actioning_site_id: nil}, ~{events__actioning_site_id: blocking_site_ids}))
|
||||
end
|
||||
|
||||
ds = query1.union(query2, all: false).order(Sequel.desc(:created_at))
|
||||
else
|
||||
ds = Event.news_feed_default_dataset.exclude(sites__is_nsfw: true)
|
||||
|
||||
if current_site
|
||||
blocking_site_ids = current_site.blocking_site_ids
|
||||
ds = ds.where(Sequel.|({events__site_id: nil}, ~{events__site_id: blocking_site_ids})).where(Sequel.|({events__actioning_site_id: nil}, ~{events__actioning_site_id: blocking_site_ids}))
|
||||
end
|
||||
|
||||
ds = ds.where(
|
||||
Sequel.expr(Sequel[:sites][:score] > Event::GLOBAL_SCORE_LIMIT) |
|
||||
Sequel.expr(Sequel[:actioning_sites][:score] > Event::GLOBAL_SCORE_LIMIT)
|
||||
)
|
||||
end
|
||||
|
||||
@pagination_dataset = ds.paginate @page.to_i, Event::GLOBAL_PAGINATION_LENGTH
|
||||
@events = @pagination_dataset.all
|
||||
|
||||
erb :'activity'
|
||||
end
|
308
app/admin.rb
Normal file
308
app/admin.rb
Normal file
|
@ -0,0 +1,308 @@
|
|||
get '/admin' do
|
||||
require_admin
|
||||
@banned_sites = Site.select(:username).filter(is_banned: true).order(:username).all
|
||||
@nsfw_sites = Site.select(:username).filter(is_nsfw: true).order(:username).all
|
||||
erb :'admin'
|
||||
end
|
||||
|
||||
get '/admin/reports' do
|
||||
require_admin
|
||||
@reports = Report.order(:created_at.desc).all
|
||||
erb :'admin/reports'
|
||||
end
|
||||
|
||||
get '/admin/site/:username' do |username|
|
||||
require_admin
|
||||
@site = Site[username: username]
|
||||
not_found if @site.nil?
|
||||
@title = "Site Inspector - #{@site.username}"
|
||||
erb :'admin/site'
|
||||
end
|
||||
|
||||
post '/admin/reports' do
|
||||
|
||||
end
|
||||
|
||||
post '/admin/site_files/train' do
|
||||
require_admin
|
||||
site = Site[params[:site_id]]
|
||||
site_file = site.site_files_dataset.where(path: params[:path]).first
|
||||
not_found if site_file.nil?
|
||||
site.untrain site_file.path
|
||||
site.train site_file.path, params[:classifier]
|
||||
'ok'
|
||||
end
|
||||
|
||||
get '/admin/usage' do
|
||||
require_admin
|
||||
today = Date.today
|
||||
current_month = Date.new today.year, today.month, 1
|
||||
|
||||
@monthly_stats = []
|
||||
|
||||
month = current_month
|
||||
|
||||
until month.year == 2016 && month.month == 2 do
|
||||
|
||||
stats = DB["select sum(views) as views, sum(hits) as hits,sum(bandwidth) as bandwidth from daily_site_stats where created_at::text LIKE '#{month.year}-#{month.strftime('%m')}-%'"].first
|
||||
|
||||
stats.keys.each do |key|
|
||||
stats[key] ||= 0
|
||||
end
|
||||
|
||||
stats.collect {|s| s == 0}.uniq
|
||||
|
||||
if stats[:views] != 0 && stats[:hits] != 0 && stats[:bandwidth] != 0
|
||||
popular_sites = DB[
|
||||
'select sum(bandwidth) as bandwidth,username from stats left join sites on sites.id=stats.site_id where stats.created_at >= ? and stats.created_at < ? group by username order by bandwidth desc limit 50',
|
||||
month,
|
||||
month.next_month
|
||||
].all
|
||||
|
||||
@monthly_stats.push stats.merge(date: month).merge(popular_sites: popular_sites)
|
||||
end
|
||||
|
||||
month = month.prev_month
|
||||
end
|
||||
|
||||
erb :'admin/usage'
|
||||
end
|
||||
|
||||
get '/admin/email' do
|
||||
require_admin
|
||||
erb :'admin/email'
|
||||
end
|
||||
|
||||
get '/admin/stats' do
|
||||
require_admin
|
||||
|
||||
@stats = {
|
||||
total_hosted_site_hits: DB['SELECT SUM(hits) FROM sites'].first[:sum],
|
||||
total_hosted_site_views: DB['SELECT SUM(views) FROM sites'].first[:sum],
|
||||
total_site_changes: DB['select max(changed_count) from sites'].first[:max],
|
||||
total_sites: Site.count
|
||||
}
|
||||
|
||||
# Start with the date of the first created site
|
||||
|
||||
start = Site.select(:created_at).
|
||||
exclude(created_at: nil).
|
||||
order(:created_at).
|
||||
first[:created_at].to_date
|
||||
|
||||
runner = start
|
||||
|
||||
monthly_stats = []
|
||||
|
||||
now = Date.today
|
||||
|
||||
while Date.new(runner.year, runner.month, 1) <= Date.new(now.year, now.month, 1)
|
||||
monthly_stats.push(
|
||||
date: runner,
|
||||
sites_created: Site.where(created_at: runner..runner.next_month).count,
|
||||
total_from_start: Site.where(created_at: start..runner.next_month).count,
|
||||
supporters: Site.where(created_at: start..runner.next_month).exclude(stripe_customer_id: nil).count,
|
||||
)
|
||||
|
||||
runner = runner.next_month
|
||||
end
|
||||
|
||||
@stats[:monthly_stats] = monthly_stats
|
||||
|
||||
if $stripe_cache && Time.now < $stripe_cache[:time] + 14400
|
||||
customers = $stripe_cache[:customers]
|
||||
else
|
||||
customers = Stripe::Customer.all limit: 100000
|
||||
$stripe_cache = {
|
||||
customers: customers,
|
||||
time: Time.now
|
||||
}
|
||||
end
|
||||
|
||||
@stats[:monthly_revenue] = 0.0
|
||||
|
||||
subscriptions = []
|
||||
@stats[:cancelled_subscriptions] = 0
|
||||
|
||||
customers.each do |customer|
|
||||
sub = {created_at: Time.at(customer.created)}
|
||||
|
||||
if customer[:subscriptions][:data].empty?
|
||||
@stats[:cancelled_subscriptions] += 1
|
||||
next
|
||||
end
|
||||
|
||||
next if customer[:subscriptions][:data].first[:plan][:amount] == 0
|
||||
|
||||
sub[:status] = 'active'
|
||||
plan = customer[:subscriptions][:data].first[:plan]
|
||||
|
||||
sub[:amount_without_fees] = (plan[:amount] / 100.0).round(2)
|
||||
sub[:percentage_fee] = (sub[:amount_without_fees]/(100/2.9)).ceil_to(2)
|
||||
sub[:fixed_fee] = 0.30
|
||||
sub[:amount] = sub[:amount_without_fees] - sub[:percentage_fee] - sub[:fixed_fee]
|
||||
|
||||
if(plan[:interval] == 'year')
|
||||
sub[:amount] = (sub[:amount] / 12).round(2)
|
||||
end
|
||||
|
||||
@stats[:monthly_revenue] += sub[:amount]
|
||||
|
||||
subscriptions.push sub
|
||||
end
|
||||
|
||||
@stats[:subscriptions] = subscriptions
|
||||
|
||||
# Hotwired for now
|
||||
@stats[:expenses] = 300.0 #/mo
|
||||
@stats[:percent_until_profit] = (
|
||||
(@stats[:monthly_revenue].to_f / @stats[:expenses]) * 100
|
||||
)
|
||||
|
||||
@stats[:poverty_threshold] = 11_945
|
||||
@stats[:poverty_threshold_percent] = (@stats[:monthly_revenue].to_f / ((@stats[:poverty_threshold]/12) + @stats[:expenses])) * 100
|
||||
|
||||
# http://en.wikipedia.org/wiki/Poverty_threshold
|
||||
|
||||
@stats[:average_developer_salary] = 93_280.00 # google "average developer salary"
|
||||
@stats[:percent_until_developer_salary] = (@stats[:monthly_revenue].to_f / ((@stats[:average_developer_salary]/12) + @stats[:expenses])) * 100
|
||||
|
||||
erb :'admin/stats'
|
||||
end
|
||||
|
||||
post '/admin/email' do
|
||||
require_admin
|
||||
|
||||
%i{subject body}.each do |k|
|
||||
if params[k].nil? || params[k].empty?
|
||||
flash[:error] = "#{k.capitalize} is missing."
|
||||
redirect '/admin/email'
|
||||
end
|
||||
end
|
||||
|
||||
sites = Site.newsletter_sites
|
||||
|
||||
day = 0
|
||||
|
||||
until sites.empty?
|
||||
seconds = 0.0
|
||||
queued_sites = []
|
||||
Site::EMAIL_BLAST_MAXIMUM_PER_DAY.times {
|
||||
break if sites.empty?
|
||||
queued_sites << sites.pop
|
||||
}
|
||||
|
||||
queued_sites.each do |site|
|
||||
EmailWorker.perform_at((day.days.from_now + seconds), {
|
||||
from: 'Kyle from Neocities <kyle@neocities.org>',
|
||||
to: site.email,
|
||||
subject: params[:subject],
|
||||
body: params[:body]
|
||||
})
|
||||
seconds += 0.5
|
||||
end
|
||||
|
||||
day += 1
|
||||
end
|
||||
|
||||
flash[:success] = "#{sites.length} emails have been queued, #{Site::EMAIL_BLAST_MAXIMUM_PER_DAY} per day."
|
||||
redirect '/'
|
||||
end
|
||||
|
||||
post '/admin/banhammer' do
|
||||
require_admin
|
||||
|
||||
if params[:usernames].empty?
|
||||
flash[:error] = 'no usernames provided'
|
||||
redirect '/admin'
|
||||
end
|
||||
|
||||
usernames = params[:usernames].split("\n").collect {|u| u.strip}
|
||||
|
||||
deleted_count = 0
|
||||
ip_deleted_count = 0
|
||||
|
||||
usernames.each do |username|
|
||||
next if username == ''
|
||||
site = Site[username: username]
|
||||
next if site.nil? || site.is_banned
|
||||
|
||||
if !params[:classifier].empty?
|
||||
site.untrain 'index.html'
|
||||
site.train 'index.html', params[:classifier]
|
||||
end
|
||||
|
||||
site.ban!
|
||||
deleted_count += 1
|
||||
|
||||
if !params[:ban_using_ips].empty? && IPAddress.valid?(site.ip)
|
||||
sites = Site.filter(ip: site.ip, is_banned: false).all
|
||||
sites.each do |s|
|
||||
next if usernames.include?(s.username)
|
||||
s.ban!
|
||||
end
|
||||
ip_deleted_count += 1
|
||||
end
|
||||
|
||||
if params[:classifier] == 'spam' || params[:classifier] == 'phishing'
|
||||
next unless IPAddress.valid?(site.ip)
|
||||
StopForumSpamWorker.perform_async(
|
||||
username: site.username,
|
||||
email: site.email,
|
||||
ip: site.ip,
|
||||
classifier: params[:classifier]
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
flash[:success] = "#{ip_deleted_count + deleted_count} sites have been banned, including #{ip_deleted_count} matching IPs."
|
||||
redirect '/admin'
|
||||
end
|
||||
|
||||
post '/admin/mark_nsfw' do
|
||||
require_admin
|
||||
site = Site[username: params[:username]]
|
||||
|
||||
if site.nil?
|
||||
flash[:error] = 'User not found'
|
||||
redirect '/admin'
|
||||
end
|
||||
|
||||
site.is_nsfw = true
|
||||
site.admin_nsfw = true
|
||||
site.save_changes validate: false
|
||||
|
||||
flash[:success] = 'MISSION ACCOMPLISHED'
|
||||
redirect '/admin'
|
||||
end
|
||||
|
||||
post '/admin/feature' do
|
||||
require_admin
|
||||
site = Site[username: params[:username]]
|
||||
|
||||
if site.nil?
|
||||
flash[:error] = 'User not found'
|
||||
redirect '/admin'
|
||||
end
|
||||
|
||||
site.featured_at = Time.now
|
||||
site.save_changes(validate: false)
|
||||
flash[:success] = 'Site has been featured.'
|
||||
redirect '/admin'
|
||||
end
|
||||
|
||||
get '/admin/masquerade/:username' do
|
||||
require_admin
|
||||
site = Site[username: params[:username]]
|
||||
not_found if site.nil?
|
||||
session[:id] = site.id
|
||||
redirect '/'
|
||||
end
|
||||
|
||||
def require_admin
|
||||
redirect '/' unless is_admin?
|
||||
end
|
||||
|
||||
def is_admin?
|
||||
signed_in? && current_site.is_admin
|
||||
end
|
307
app/api.rb
Normal file
307
app/api.rb
Normal file
|
@ -0,0 +1,307 @@
|
|||
require 'base64'
|
||||
|
||||
get '/api' do
|
||||
@title = 'Developers API'
|
||||
erb :'api'
|
||||
end
|
||||
|
||||
post '/api/upload_hash' do
|
||||
require_api_credentials
|
||||
res = {}
|
||||
files = []
|
||||
params.each do |k,v|
|
||||
res[k] = current_site.sha1_hash_match? k, v
|
||||
end
|
||||
api_success files: res
|
||||
end
|
||||
|
||||
get '/api/list' do
|
||||
require_api_credentials
|
||||
|
||||
files = []
|
||||
|
||||
if params[:path].nil? || params[:path].empty?
|
||||
file_list = current_site.site_files
|
||||
else
|
||||
file_list = current_site.file_list params[:path]
|
||||
end
|
||||
|
||||
file_list.each do |file|
|
||||
new_file = {}
|
||||
new_file[:path] = file[:path]
|
||||
new_file[:is_directory] = file[:is_directory]
|
||||
new_file[:size] = file[:size] unless file[:is_directory]
|
||||
new_file[:created_at] = file[:created_at].rfc2822
|
||||
new_file[:updated_at] = file[:updated_at].rfc2822
|
||||
new_file[:sha1_hash] = file[:sha1_hash] unless file[:is_directory]
|
||||
files << new_file
|
||||
end
|
||||
|
||||
files.each {|f| f[:path].sub!(/^\//, '')}
|
||||
|
||||
api_success files: files
|
||||
end
|
||||
|
||||
def extract_files(params, files = [])
|
||||
# Check if the entire input is directly an array of files
|
||||
if params.is_a?(Array)
|
||||
params.each do |item|
|
||||
# Call extract_files on each item if it's an Array or Hash to handle nested structures
|
||||
if item.is_a?(Array) || item.is_a?(Hash)
|
||||
extract_files(item, files)
|
||||
end
|
||||
end
|
||||
elsif params.is_a?(Hash)
|
||||
params.each do |key, value|
|
||||
# If the value is a Hash and contains a :tempfile key, it's considered an uploaded file.
|
||||
if value.is_a?(Hash) && value.has_key?(:tempfile) && !value[:tempfile].nil?
|
||||
files << {filename: value[:name], tempfile: value[:tempfile]}
|
||||
elsif value.is_a?(Array)
|
||||
value.each do |val|
|
||||
if val.is_a?(Hash) && val.has_key?(:tempfile) && !val[:tempfile].nil?
|
||||
# Directly add the file info if it's an uploaded file within an array
|
||||
files << {filename: val[:name], tempfile: val[:tempfile]}
|
||||
elsif val.is_a?(Hash) || val.is_a?(Array)
|
||||
# Recursively search for more files if the element is a Hash or Array
|
||||
extract_files(val, files)
|
||||
end
|
||||
end
|
||||
elsif value.is_a?(Hash)
|
||||
# Recursively search for more files if the value is a Hash
|
||||
extract_files(value, files)
|
||||
end
|
||||
end
|
||||
end
|
||||
files
|
||||
end
|
||||
|
||||
post '/api/upload' do
|
||||
require_api_credentials
|
||||
files = extract_files params
|
||||
|
||||
if !params[:username].blank?
|
||||
site = Site[username: params[:username]]
|
||||
|
||||
if site.nil? || site.is_deleted
|
||||
api_error 400, 'site_not_found', "could not find site"
|
||||
end
|
||||
|
||||
if site.owned_by?(current_site)
|
||||
@_site = site
|
||||
else
|
||||
api_error 400, 'site_not_allowed', "not allowed to change this site with your current logged in site"
|
||||
end
|
||||
end
|
||||
|
||||
api_error 400, 'missing_files', 'you must provide files to upload' if files.empty?
|
||||
|
||||
uploaded_size = files.collect {|f| f[:tempfile].size}.inject{|sum,x| sum + x }
|
||||
|
||||
if current_site.file_size_too_large? uploaded_size
|
||||
api_error 400, 'too_large', 'files are too large to fit in your space, try uploading smaller (or less) files'
|
||||
end
|
||||
|
||||
if current_site.too_many_files?(files.length)
|
||||
api_error 400, 'too_many_files', "cannot exceed the maximum site files limit (#{current_site.plan_feature(:maximum_site_files)})"
|
||||
end
|
||||
|
||||
files.each do |file|
|
||||
file[:filename] = Rack::Utils.unescape file[:filename]
|
||||
if !current_site.okay_to_upload?(file)
|
||||
api_error 400, 'invalid_file_type', "#{file[:filename]} is not an allowed file type for free sites, supporter required"
|
||||
end
|
||||
|
||||
if File.directory? file[:filename]
|
||||
api_error 400, 'directory_exists', "#{file[:filename]} being used by a directory"
|
||||
end
|
||||
|
||||
if current_site.file_size_too_large? file[:tempfile].size
|
||||
api_error 400, 'file_too_large' "#{file[:filename]} is too large"
|
||||
end
|
||||
|
||||
if SiteFile.path_too_long? file[:filename]
|
||||
api_error 400, 'file_path_too_long', "#{file[:filename]} path is too long"
|
||||
end
|
||||
|
||||
if SiteFile.name_too_long? file[:filename]
|
||||
api_error 400, 'file_name_too_long', "#{file[:filename]} filename is too long (exceeds #{SiteFile::FILE_NAME_CHARACTER_LIMIT} characters)"
|
||||
end
|
||||
end
|
||||
|
||||
results = current_site.store_files files
|
||||
api_success 'your file(s) have been successfully uploaded'
|
||||
end
|
||||
|
||||
post '/api/rename' do
|
||||
require_api_credentials
|
||||
|
||||
api_error 400, 'missing_arguments', 'you must provide path and new_path' if params[:path].blank? || params[:new_path].blank?
|
||||
|
||||
path = current_site.scrubbed_path params[:path]
|
||||
new_path = current_site.scrubbed_path params[:new_path]
|
||||
|
||||
unless path.is_a?(String)
|
||||
api_error 400, 'bad_path', "#{path} is not a valid path, cancelled renaming"
|
||||
end
|
||||
|
||||
unless new_path.is_a?(String)
|
||||
api_error 400, 'bad_new_path', "#{new_path} is not a valid new_path, cancelled renaming"
|
||||
end
|
||||
|
||||
site_file = current_site.site_files.select {|sf| sf.path == path}.first
|
||||
|
||||
if site_file.nil?
|
||||
api_error 400, 'missing_file', "could not find #{path}"
|
||||
end
|
||||
|
||||
res = site_file.rename new_path
|
||||
|
||||
if res.first == true
|
||||
api_success "#{path} has been renamed to #{new_path}"
|
||||
else
|
||||
api_error 400, 'rename_error', res.last
|
||||
end
|
||||
end
|
||||
|
||||
post '/api/delete' do
|
||||
require_api_credentials
|
||||
|
||||
api_error 400, 'missing_filenames', 'you must provide files to delete' if params[:filenames].nil? || params[:filenames].empty?
|
||||
|
||||
paths = []
|
||||
params[:filenames].each do |path|
|
||||
unless path.is_a?(String)
|
||||
api_error 400, 'bad_filename', "#{path} is not a valid filename, canceled deleting"
|
||||
end
|
||||
|
||||
if current_site.files_path(path) == current_site.files_path
|
||||
api_error 400, 'cannot_delete_site_directory', 'cannot delete the root directory of the site'
|
||||
end
|
||||
|
||||
if !current_site.file_exists?(path)
|
||||
api_error 400, 'missing_files', "#{path} was not found on your site, canceled deleting"
|
||||
end
|
||||
|
||||
if path == 'index.html' || path == '/index.html'
|
||||
api_error 400, 'cannot_delete_index', 'you cannot delete your index.html file, canceled deleting'
|
||||
end
|
||||
|
||||
paths << path
|
||||
end
|
||||
|
||||
paths.each do |path|
|
||||
current_site.delete_file(path)
|
||||
end
|
||||
|
||||
api_success 'file(s) have been deleted'
|
||||
end
|
||||
|
||||
get '/api/info' do
|
||||
if params[:sitename]
|
||||
site = Site[username: params[:sitename]]
|
||||
api_error 400, 'site_not_found', "could not find site #{params[:sitename]}" if site.nil? || site.is_banned
|
||||
api_success api_info_for(site)
|
||||
else
|
||||
init_api_credentials
|
||||
api_success api_info_for(current_site)
|
||||
end
|
||||
end
|
||||
|
||||
get '/api/key' do
|
||||
require_api_credentials
|
||||
current_site.generate_api_key! if current_site.api_key.blank?
|
||||
api_success api_key: current_site.api_key
|
||||
end
|
||||
|
||||
def api_info_for(site)
|
||||
{
|
||||
info: {
|
||||
sitename: site.username,
|
||||
views: site.views,
|
||||
hits: site.hits,
|
||||
created_at: site.created_at.rfc2822,
|
||||
last_updated: site.site_updated_at ? site.site_updated_at.rfc2822 : nil,
|
||||
domain: site.domain,
|
||||
tags: site.tags.collect {|t| t.name}
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
# Catch-all for missing api calls
|
||||
|
||||
get '/api/:name' do
|
||||
api_not_found
|
||||
end
|
||||
|
||||
post '/api/:name' do
|
||||
api_not_found
|
||||
end
|
||||
|
||||
def require_api_credentials
|
||||
return true if current_site && csrf_safe?
|
||||
|
||||
if !request.env['HTTP_AUTHORIZATION'].nil?
|
||||
init_api_credentials
|
||||
else
|
||||
api_error_invalid_auth
|
||||
end
|
||||
end
|
||||
|
||||
def init_api_credentials
|
||||
auth = request.env['HTTP_AUTHORIZATION']
|
||||
|
||||
begin
|
||||
if bearer_match = auth.match(/^Bearer (.+)/)
|
||||
api_key = bearer_match.captures.first
|
||||
api_error_invalid_auth if api_key.nil? || api_key.empty?
|
||||
else
|
||||
user, pass = Base64.decode64(auth.match(/Basic (.+)/)[1]).split(':')
|
||||
end
|
||||
rescue
|
||||
api_error_invalid_auth
|
||||
end
|
||||
|
||||
if defined?(api_key) && !api_key.blank?
|
||||
site = Site[api_key: api_key]
|
||||
elsif defined?(user) && defined?(pass)
|
||||
site = Site.get_site_from_login user, pass
|
||||
else
|
||||
api_error_invalid_auth
|
||||
end
|
||||
|
||||
if site.nil? || site.is_banned || site.is_deleted || !(site.required_validations_met?)
|
||||
api_error_invalid_auth
|
||||
end
|
||||
|
||||
DB['update sites set api_calls=api_calls+1 where id=?', site.id].first
|
||||
|
||||
session[:id] = site.id
|
||||
end
|
||||
|
||||
def api_success(message_or_obj)
|
||||
output = {result: 'success'}
|
||||
|
||||
if message_or_obj.is_a?(String)
|
||||
output[:message] = message_or_obj
|
||||
else
|
||||
output.merge! message_or_obj
|
||||
end
|
||||
|
||||
api_response(200, output)
|
||||
end
|
||||
|
||||
def api_response(status, output)
|
||||
halt status, JSON.pretty_generate(output)+"\n"
|
||||
end
|
||||
|
||||
def api_error(status, error_type, message)
|
||||
api_response(status, result: 'error', error_type: error_type, message: message)
|
||||
end
|
||||
|
||||
def api_error_invalid_auth
|
||||
api_error 403, 'invalid_auth', 'invalid credentials - please check your username and password (or your api key)'
|
||||
end
|
||||
|
||||
def api_not_found
|
||||
api_error 404, 'not_found', 'the requested api call does not exist'
|
||||
end
|
7
app/blog.rb
Normal file
7
app/blog.rb
Normal file
|
@ -0,0 +1,7 @@
|
|||
get '/blog/?' do
|
||||
redirect 'https://blog.neocities.org', 301
|
||||
end
|
||||
|
||||
get '/blog/:article' do |article|
|
||||
redirect "https://blog.neocities.org/#{article}.html", 301
|
||||
end
|
189
app/browse.rb
Normal file
189
app/browse.rb
Normal file
|
@ -0,0 +1,189 @@
|
|||
get '/browse/?' do
|
||||
@page = params[:page]
|
||||
@page = 1 if @page.not_an_integer?
|
||||
|
||||
if params[:tag]
|
||||
params[:tag] = params[:tag].gsub(Tag::INVALID_TAG_REGEX, '').gsub(/\s+/, '').slice(0, Tag::NAME_LENGTH_MAX)
|
||||
@title = "Sites tagged #{params[:tag]}"
|
||||
end
|
||||
|
||||
if is_education?
|
||||
ds = education_sites_dataset
|
||||
else
|
||||
ds = browse_sites_dataset
|
||||
end
|
||||
|
||||
ds = ds.paginate @page.to_i, Site::BROWSE_PAGINATION_LENGTH
|
||||
@pagination_dataset = ds
|
||||
@sites = ds.all
|
||||
|
||||
site_ids = @sites.collect {|s| s[:id]}
|
||||
tags = DB['select site_id,name from tags join sites_tags on tags.id=sites_tags.tag_id where site_id IN ?', site_ids].all
|
||||
|
||||
@site_tags = {}
|
||||
site_ids.each do |site_id|
|
||||
@site_tags[site_id] = tags.select {|t| t[:site_id] == site_id}.collect {|t| t[:name]}
|
||||
end
|
||||
|
||||
erb :browse
|
||||
end
|
||||
|
||||
def education_sites_dataset
|
||||
ds = Site.filter is_deleted: false
|
||||
ds = ds.association_join(:tags).select_all(:sites)
|
||||
params[:tag] = current_site.tags.first.name
|
||||
ds = ds.where ['tags.name = ?', params[:tag]]
|
||||
end
|
||||
|
||||
def browse_sites_dataset
|
||||
ds = Site.browse_dataset
|
||||
ds = ds.where is_education: false
|
||||
|
||||
if current_site
|
||||
ds = ds.or sites__id: current_site.id
|
||||
|
||||
if !current_site.blocking_site_ids.empty?
|
||||
ds = ds.where(Sequel.~(Sequel.qualify(:sites, :id) => current_site.blocking_site_ids))
|
||||
end
|
||||
|
||||
if current_site.blocks_dataset.count
|
||||
ds = ds.where(
|
||||
Sequel.~(Sequel.qualify(:sites, :id) => current_site.blocks_dataset.select(:actioning_site_id).all.collect {|s| s.actioning_site_id})
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
if current_site && current_site.is_admin && params[:sites]
|
||||
ds = ds.where sites__username: params[:sites].split(',')
|
||||
return ds
|
||||
end
|
||||
|
||||
params[:sort_by] ||= 'special_sauce'
|
||||
|
||||
case params[:sort_by]
|
||||
when 'special_sauce'
|
||||
ds = ds.where{score > 1} unless params[:tag]
|
||||
ds = ds.order :score.desc, :follow_count.desc, :views.desc, :site_updated_at.desc
|
||||
when 'random'
|
||||
ds = ds.where{score > 3} unless params[:tag]
|
||||
ds = ds.order(Sequel.lit('RANDOM()'))
|
||||
when 'most_followed'
|
||||
ds = ds.where{views > Site::BROWSE_MINIMUM_FOLLOWER_VIEWS}
|
||||
ds = ds.where{follow_count > Site::BROWSE_FOLLOWER_MINIMUM_FOLLOWS}
|
||||
ds = ds.where{updated_at > Site::BROWSE_FOLLOWER_UPDATED_AT_CUTOFF.ago} unless params[:tag]
|
||||
ds = ds.order :follow_count.desc, :score.desc, :updated_at.desc
|
||||
when 'last_updated'
|
||||
ds = ds.where{score > 3} unless params[:tag]
|
||||
ds = ds.exclude site_updated_at: nil
|
||||
ds = ds.order :site_updated_at.desc
|
||||
when 'newest'
|
||||
ds = ds.where{views > Site::BROWSE_MINIMUM_VIEWS} unless is_admin?
|
||||
ds = ds.exclude site_updated_at: nil
|
||||
ds = ds.order :created_at.desc, :views.desc
|
||||
when 'oldest'
|
||||
ds = ds.where{score > 0.4} unless params[:tag]
|
||||
ds = ds.exclude site_updated_at: nil
|
||||
ds = ds.order(:created_at, :views.desc)
|
||||
when 'hits'
|
||||
ds = ds.where{score > 1}
|
||||
ds = ds.order(:hits.desc, :site_updated_at.desc)
|
||||
when 'views'
|
||||
ds = ds.where{score > 3}
|
||||
ds = ds.order(:views.desc, :site_updated_at.desc)
|
||||
when 'featured'
|
||||
ds = ds.exclude featured_at: nil
|
||||
ds = ds.order :featured_at.desc
|
||||
when 'tipping_enabled'
|
||||
ds = ds.where tipping_enabled: true
|
||||
ds = ds.where("(tipping_paypal is not null and tipping_paypal != '') or (tipping_bitcoin is not null and tipping_bitcoin != '')")
|
||||
ds = ds.where{score > 1} unless params[:tag]
|
||||
ds = ds.group :sites__id
|
||||
ds = ds.order :follow_count.desc, :views.desc, :updated_at.desc
|
||||
when 'blocks'
|
||||
require_admin
|
||||
ds = ds.select{[sites.*, Sequel[count(site_id)].as(:total)]}
|
||||
ds = ds.inner_join :blocks, :site_id => :id
|
||||
ds = ds.group :sites__id
|
||||
ds = ds.order :total.desc
|
||||
end
|
||||
|
||||
ds = ds.where ['sites.is_nsfw = ?', (params[:is_nsfw] == 'true' ? true : false)]
|
||||
|
||||
if params[:tag]
|
||||
ds = ds.select_all :sites
|
||||
ds = ds.inner_join :sites_tags, :site_id => :id
|
||||
ds = ds.inner_join :tags, :id => :sites_tags__tag_id
|
||||
ds = ds.where ['tags.name = ?', params[:tag]]
|
||||
ds = ds.where ['tags.is_nsfw = ?', (params[:is_nsfw] == 'true' ? true : false)]
|
||||
end
|
||||
|
||||
ds
|
||||
end
|
||||
|
||||
def daily_search_max?
|
||||
query_count = $redis_cache.get('search_query_count').to_i
|
||||
query_count >= $config['google_custom_search_query_limit']
|
||||
end
|
||||
|
||||
get '/browse/search' do
|
||||
@title = 'Site Search'
|
||||
|
||||
@daily_search_max_reached = daily_search_max?
|
||||
|
||||
if @daily_search_max_reached
|
||||
params[:q] = nil
|
||||
end
|
||||
|
||||
if !params[:q].blank?
|
||||
created = $redis_cache.set('search_query_count', 1, nx: true, ex: 86400)
|
||||
$redis_cache.incr('search_query_count') unless created
|
||||
|
||||
@start = params[:start].to_i
|
||||
@start = 0 if @start < 0
|
||||
|
||||
@resp = JSON.parse HTTP.get('https://www.googleapis.com/customsearch/v1', params: {
|
||||
key: $config['google_custom_search_key'],
|
||||
cx: $config['google_custom_search_cx'],
|
||||
safe: 'active',
|
||||
start: @start,
|
||||
q: Rack::Utils.escape(params[:q]) + ' -filetype:pdf -filetype:txt site:*.neocities.org'
|
||||
})
|
||||
|
||||
@items = []
|
||||
|
||||
if @total_results != 0 && @resp['error'].nil? && @resp['searchInformation']['totalResults'] != "0"
|
||||
@total_results = @resp['searchInformation']['totalResults'].to_i
|
||||
@resp['items'].each do |item|
|
||||
link = Addressable::URI.parse(item['link'])
|
||||
unencoded_path = Rack::Utils.unescape(Rack::Utils.unescape(link.path)) # Yes, it needs to be decoded twice
|
||||
item['unencoded_link'] = unencoded_path == '/' ? link.host : link.host+unencoded_path
|
||||
item['link'] = link
|
||||
|
||||
next if link.host == 'neocities.org'
|
||||
|
||||
username = link.host.split('.').first
|
||||
site = Site[username: username]
|
||||
next if site.nil? || site.is_deleted || site.is_nsfw
|
||||
|
||||
screenshot_path = unencoded_path
|
||||
|
||||
screenshot_path << 'index' if screenshot_path[-1] == '/'
|
||||
|
||||
['.html', '.htm'].each do |ext|
|
||||
if site.screenshot_exists?(screenshot_path + ext, '540x405')
|
||||
screenshot_path += ext
|
||||
break
|
||||
end
|
||||
end
|
||||
|
||||
item['screenshot_url'] = site.screenshot_url(screenshot_path, '540x405')
|
||||
@items << item
|
||||
end
|
||||
end
|
||||
else
|
||||
@items = nil
|
||||
@total_results = 0
|
||||
end
|
||||
|
||||
erb :'search'
|
||||
end
|
21
app/comment.rb
Normal file
21
app/comment.rb
Normal file
|
@ -0,0 +1,21 @@
|
|||
post '/comment/:comment_id/toggle_like' do |comment_id|
|
||||
require_login
|
||||
content_type :json
|
||||
comment = Comment[id: comment_id]
|
||||
return 403 if comment.event.site.is_blocking?(current_site) || current_site.is_blocking?(comment.event.site)
|
||||
liked_response = comment.toggle_site_like(current_site) ? 'liked' : 'unliked'
|
||||
{result: liked_response, comment_like_count: comment.comment_likes_dataset.count, liking_site_names: comment.liking_site_usernames}.to_json
|
||||
end
|
||||
|
||||
post '/comment/:comment_id/delete' do |comment_id|
|
||||
require_login
|
||||
content_type :json
|
||||
comment = Comment[id: comment_id]
|
||||
|
||||
if comment.event.site == current_site || comment.actioning_site == current_site
|
||||
comment.delete
|
||||
return {result: 'success'}.to_json
|
||||
end
|
||||
|
||||
return {result: 'error'}.to_json
|
||||
end
|
50
app/contact.rb
Normal file
50
app/contact.rb
Normal file
|
@ -0,0 +1,50 @@
|
|||
get '/contact' do
|
||||
erb :'contact'
|
||||
end
|
||||
|
||||
post '/contact' do
|
||||
@errors = []
|
||||
|
||||
if params[:email].empty? || params[:subject].empty? || params[:body].empty?
|
||||
@errors << 'Please fill out all fields'
|
||||
end
|
||||
|
||||
if params[:faq_check] == 'no'
|
||||
@errors << 'Please check Frequently Asked Questions before sending a contact message'
|
||||
end
|
||||
|
||||
unless hcaptcha_valid?
|
||||
@errors << 'Captcha was not filled out (or was filled out incorrectly)'
|
||||
end
|
||||
|
||||
if !@errors.empty?
|
||||
erb :'contact'
|
||||
else
|
||||
body = params[:body]
|
||||
|
||||
if current_site
|
||||
body = "current username: #{current_site.username}\n\n" + body
|
||||
if parent_site != current_site
|
||||
body = "parent username: #{parent_site.username}\n\n" + body
|
||||
end
|
||||
end
|
||||
|
||||
if current_site && current_site.supporter?
|
||||
subject = "[Neocities Supporter Contact]: #{params[:subject]}"
|
||||
else
|
||||
subject = "[Neocities Contact]: #{params[:subject]}"
|
||||
end
|
||||
|
||||
EmailWorker.perform_async({
|
||||
from: 'web@neocities.org',
|
||||
reply_to: params[:email],
|
||||
to: 'contact@neocities.org',
|
||||
subject: subject,
|
||||
body: body,
|
||||
no_footer: true
|
||||
})
|
||||
|
||||
flash[:success] = 'Your contact message has been sent.'
|
||||
redirect '/'
|
||||
end
|
||||
end
|
154
app/create.rb
Normal file
154
app/create.rb
Normal file
|
@ -0,0 +1,154 @@
|
|||
CREATE_MATCH_REGEX = /^username$|^password$|^email$|^new_tags_string$|^is_education$/
|
||||
|
||||
def education_whitelist_required?
|
||||
return true if params[:is_education] == 'true' && $config['education_tag_whitelist']
|
||||
false
|
||||
end
|
||||
|
||||
def education_whitelisted?
|
||||
return true if education_whitelist_required? && !$config['education_tag_whitelist'].select {|t| params[:new_tags_string].match(t)}.empty?
|
||||
false
|
||||
end
|
||||
|
||||
post '/create_validate_all' do
|
||||
content_type :json
|
||||
fields = params.select {|p| p.match CREATE_MATCH_REGEX}
|
||||
|
||||
begin
|
||||
site = Site.new fields
|
||||
rescue ArgumentError => e
|
||||
if e.message == 'input string invalid'
|
||||
return {error: 'invalid input'}.to_json
|
||||
else
|
||||
raise e
|
||||
end
|
||||
end
|
||||
|
||||
if site.valid?
|
||||
return [].to_json if education_whitelisted?
|
||||
end
|
||||
|
||||
resp = site.errors.collect {|e| [e.first, e.last.first]}
|
||||
resp << ['captcha', 'Please complete the captcha.'] if params[:'h-captcha-response'].empty? && !self.class.test?
|
||||
resp.to_json
|
||||
end
|
||||
|
||||
post '/create_validate' do
|
||||
content_type :json
|
||||
|
||||
if !params[:field].match CREATE_MATCH_REGEX
|
||||
return {error: 'not a valid field'}.to_json
|
||||
end
|
||||
|
||||
begin
|
||||
site = Site.new(params[:field] => params[:value])
|
||||
rescue ArgumentError => e
|
||||
if e.message == 'input string invalid'
|
||||
return {error: 'invalid input'}.to_json
|
||||
else
|
||||
raise e
|
||||
end
|
||||
end
|
||||
|
||||
site.is_education = params[:is_education]
|
||||
site.valid?
|
||||
|
||||
field_sym = params[:field].to_sym
|
||||
|
||||
if site.errors[field_sym]
|
||||
return {error: site.errors[field_sym].first}.to_json
|
||||
end
|
||||
|
||||
{result: 'ok'}.to_json
|
||||
end
|
||||
|
||||
post '/create' do
|
||||
content_type :json
|
||||
dashboard_if_signed_in
|
||||
|
||||
@site = Site.new(
|
||||
username: params[:username],
|
||||
password: params[:password],
|
||||
email: params[:email],
|
||||
new_tags_string: params[:new_tags_string],
|
||||
is_education: params[:is_education] == 'true' ? true : false,
|
||||
ip: request.ip,
|
||||
ga_adgroupid: session[:ga_adgroupid]
|
||||
)
|
||||
|
||||
if education_whitelist_required?
|
||||
if education_whitelisted?
|
||||
@site.email_confirmed = true
|
||||
else
|
||||
flash[:error] = 'The class tag is invalid.'
|
||||
return {result: 'error'}.to_json
|
||||
end
|
||||
else
|
||||
if !hcaptcha_valid?
|
||||
flash[:error] = 'The captcha was not valid, please try again.'
|
||||
return {result: 'error'}.to_json
|
||||
end
|
||||
|
||||
if Site.ip_create_limit?(request.ip)
|
||||
flash[:error] = 'Your IP address has created too many sites, please try again later or contact support.'
|
||||
return {result: 'error'}.to_json
|
||||
end
|
||||
|
||||
if Site.disposable_mx_record?(@site.email)
|
||||
flash[:error] = 'Cannot use a disposable email address.'
|
||||
return {result: 'error'}.to_json
|
||||
end
|
||||
|
||||
if defined?(BlackBox.create_disabled?) && BlackBox.create_disabled?(@site, request)
|
||||
flash[:error] = 'Site creation is not currently available from your location, please try again later.'
|
||||
return {result: 'error'}.to_json
|
||||
end
|
||||
|
||||
if defined?(BlackBox.tutorial_required?) && BlackBox.tutorial_required?(@site, request)
|
||||
@site.tutorial_required = true
|
||||
end
|
||||
|
||||
if !@site.valid?
|
||||
flash[:error] = @site.errors.first.last.first
|
||||
return {result: 'error'}.to_json
|
||||
end
|
||||
end
|
||||
|
||||
@site.email_confirmed = true if self.class.development?
|
||||
@site.phone_verified = true if self.class.development?
|
||||
|
||||
begin
|
||||
@site.phone_verification_required = true if self.class.production? && BlackBox.phone_verification_required?(@site)
|
||||
rescue => e
|
||||
EmailWorker.perform_async({
|
||||
from: 'web@neocities.org',
|
||||
to: 'errors@neocities.org',
|
||||
subject: "[Neocities Error] Phone verification exception",
|
||||
body: "#{e.inspect}\n#{e.backtrace}",
|
||||
no_footer: true
|
||||
})
|
||||
end
|
||||
|
||||
begin
|
||||
@site.save
|
||||
rescue Sequel::UniqueConstraintViolation => e
|
||||
if e.message =~ /username.+already exists/
|
||||
flash[:error] = 'Username already exists.'
|
||||
return {result: 'error'}.to_json
|
||||
end
|
||||
|
||||
raise e
|
||||
end
|
||||
|
||||
unless education_whitelisted?
|
||||
send_confirmation_email @site
|
||||
|
||||
@site.send_email(
|
||||
subject: "[Neocities] Welcome to Neocities!",
|
||||
body: Tilt.new('./views/templates/email/welcome.erb', pretty: true).render(self)
|
||||
)
|
||||
end
|
||||
|
||||
session[:id] = @site.id
|
||||
{result: 'ok'}.to_json
|
||||
end
|
40
app/dashboard.rb
Normal file
40
app/dashboard.rb
Normal file
|
@ -0,0 +1,40 @@
|
|||
get '/dashboard' do
|
||||
require_login
|
||||
dashboard_init
|
||||
dont_browser_cache
|
||||
|
||||
unless current_site.dashboard_accessed
|
||||
current_site.dashboard_accessed = true
|
||||
current_site.save_changes validate: false
|
||||
end
|
||||
|
||||
erb :'dashboard/index'
|
||||
end
|
||||
|
||||
def dashboard_init
|
||||
if params[:dir] && params[:dir][0] != '/'
|
||||
params[:dir] = '/'+params[:dir]
|
||||
end
|
||||
|
||||
if !File.directory?(current_site.files_path(params[:dir]))
|
||||
if !File.directory?(current_site.files_path)
|
||||
flash[:error] = 'Could not find your web site, please contact support.'
|
||||
signout
|
||||
redirect '/'
|
||||
else
|
||||
flash[:error] = 'Could not find the requested directory.'
|
||||
redirect '/dashboard'
|
||||
end
|
||||
end
|
||||
|
||||
@dir = params[:dir]
|
||||
@file_list = current_site.file_list @dir
|
||||
end
|
||||
|
||||
get '/dashboard/files' do
|
||||
require_login
|
||||
dashboard_init
|
||||
dont_browser_cache
|
||||
|
||||
erb :'dashboard/files', layout: false
|
||||
end
|
38
app/dmca.rb
Normal file
38
app/dmca.rb
Normal file
|
@ -0,0 +1,38 @@
|
|||
get '/dmca' do
|
||||
@title = 'DMCA'
|
||||
erb :'dmca'
|
||||
end
|
||||
|
||||
get '/dmca/contact_info' do
|
||||
content_type :json
|
||||
{data: erb(:'dmca/contact_info', layout: false)}.to_json
|
||||
end
|
||||
|
||||
post '/dmca/contact' do
|
||||
@title = 'DMCA'
|
||||
@errors = []
|
||||
|
||||
if params[:email].empty? || params[:subject].empty? || params[:urls].empty? || params[:body].empty?
|
||||
@errors << 'Please fill out all fields'
|
||||
end
|
||||
|
||||
if !hcaptcha_valid?
|
||||
@errors << 'Captcha was not filled out (or was filled out incorrectly)'
|
||||
end
|
||||
|
||||
if !@errors.empty?
|
||||
erb :'dmca'
|
||||
else
|
||||
EmailWorker.perform_async({
|
||||
from: 'web@neocities.org',
|
||||
reply_to: params[:email],
|
||||
to: 'dmca@neocities.org',
|
||||
subject: "[Neocities DMCA Notice]: #{params[:subject]}",
|
||||
body: "#{params[:urls].to_s}\n#{params[:body].to_s}",
|
||||
no_footer: true
|
||||
})
|
||||
|
||||
flash[:success] = 'Your DMCA notification has been sent.'
|
||||
redirect '/'
|
||||
end
|
||||
end
|
29
app/domain.rb
Normal file
29
app/domain.rb
Normal file
|
@ -0,0 +1,29 @@
|
|||
get '/domain/new' do
|
||||
require_login
|
||||
@title = 'Register a Domain'
|
||||
|
||||
erb :'domain/new'
|
||||
end
|
||||
|
||||
post '/domain/check_availability.json' do
|
||||
require_login
|
||||
content_type :json
|
||||
|
||||
timer = Time.now.to_i
|
||||
|
||||
while true
|
||||
if (Time.now.to_i - timer) > 60
|
||||
api_error 200, :contact_fail, 'Error contacting domain server, please try again.'
|
||||
end
|
||||
|
||||
begin
|
||||
res = $gandi.domain.available([params[:domain]])[params[:domain]]
|
||||
rescue => Gandi::DataError
|
||||
api_error 200, :invalid_domain, 'Domain name was invalid, please try another.'
|
||||
end
|
||||
|
||||
api_success res unless res == 'pending'
|
||||
sleep 0.2
|
||||
end
|
||||
|
||||
end
|
56
app/event.rb
Normal file
56
app/event.rb
Normal file
|
@ -0,0 +1,56 @@
|
|||
post '/event/:event_id/toggle_like' do |event_id|
|
||||
require_login
|
||||
content_type :json
|
||||
event = Event[id: event_id]
|
||||
return 403 if event.site && event.site.is_blocking?(current_site)
|
||||
return 403 if event.actioning_site && event.actioning_site.is_blocking?(current_site)
|
||||
liked_response = event.toggle_site_like(current_site) ? 'liked' : 'unliked'
|
||||
{result: liked_response, event_like_count: event.likes_dataset.count, liking_site_names: event.liking_site_usernames}.to_json
|
||||
end
|
||||
|
||||
post '/event/:event_id/comment' do |event_id|
|
||||
require_login
|
||||
content_type :json
|
||||
event = Event[id: event_id]
|
||||
|
||||
return 403 if event.site && event.site.is_blocking?(current_site)
|
||||
return 403 if event.actioning_site && event.actioning_site.is_blocking?(current_site)
|
||||
|
||||
site = event.site
|
||||
|
||||
if(site.is_blocking?(current_site) ||
|
||||
site.profile_comments_enabled == false ||
|
||||
current_site.commenting_allowed? == false ||
|
||||
(current_site.is_a_jerk? && event.site_id != current_site.id && !site.is_following?(current_site)) ||
|
||||
params[:message].length > Site::MAX_COMMENT_SIZE)
|
||||
return {result: 'error'}.to_json
|
||||
end
|
||||
|
||||
event.add_site_comment current_site, params[:message]
|
||||
{result: 'success'}.to_json
|
||||
end
|
||||
|
||||
post '/event/:event_id/update_profile_comment' do |event_id|
|
||||
require_login
|
||||
content_type :json
|
||||
event = Event[id: event_id]
|
||||
return {result: 'error'}.to_json unless (current_site.id == event.profile_comment.actioning_site_id &&
|
||||
params[:message].length <= Site::MAX_COMMENT_SIZE)
|
||||
|
||||
event.profile_comment.update message: params[:message]
|
||||
return {result: 'success'}.to_json
|
||||
end
|
||||
|
||||
post '/event/:event_id/delete' do |event_id|
|
||||
require_login
|
||||
content_type :json
|
||||
|
||||
event = Event[id: event_id]
|
||||
|
||||
if event.site_id == current_site.id || event.actioning_site_id == current_site.id
|
||||
event.delete
|
||||
return {result: 'success'}.to_json
|
||||
end
|
||||
|
||||
return {result: 'error'}.to_json
|
||||
end
|
182
app/index.rb
Normal file
182
app/index.rb
Normal file
|
@ -0,0 +1,182 @@
|
|||
get '/?' do
|
||||
if current_site
|
||||
require_login
|
||||
|
||||
redirect '/dashboard' if current_site.is_education
|
||||
|
||||
@page = params[:page]
|
||||
@page = 1 if @page.not_an_integer?
|
||||
|
||||
if params[:activity] == 'mine'
|
||||
events_dataset = current_site.latest_events(@page)
|
||||
elsif params[:event_id]
|
||||
event = Event.select(:id).where(id: params[:event_id]).first
|
||||
not_found if event.nil?
|
||||
not_found if event.is_deleted
|
||||
events_dataset = Event.where(id: params[:event_id]).paginate(1, 1)
|
||||
else
|
||||
events_dataset = current_site.news_feed(@page)
|
||||
end
|
||||
|
||||
@pagination_dataset = events_dataset
|
||||
@events = events_dataset.all
|
||||
|
||||
current_site.events_dataset.update notification_seen: true
|
||||
|
||||
halt erb :'home', locals: {site: current_site}
|
||||
end
|
||||
|
||||
if SimpleCache.expired?(:sites_count)
|
||||
@sites_count = SimpleCache.store :sites_count, Site.count.roundup(100), 4.hours
|
||||
else
|
||||
@sites_count = SimpleCache.get :sites_count
|
||||
end
|
||||
|
||||
if SimpleCache.expired?(:total_hits_count)
|
||||
@total_hits_count = SimpleCache.store :total_hits_count, DB['SELECT SUM(hits) AS hits FROM SITES'].first[:hits], 4.hours
|
||||
else
|
||||
@total_hits_count = SimpleCache.get :total_hits_count
|
||||
end
|
||||
|
||||
@total_hits_count ||= 0
|
||||
|
||||
if SimpleCache.expired?(:total_views_count)
|
||||
@total_views_count = SimpleCache.store :total_views_count, DB['SELECT SUM(views) AS views FROM SITES'].first[:views], 4.hours
|
||||
else
|
||||
@total_views_count = SimpleCache.get :total_views_count
|
||||
end
|
||||
|
||||
@total_views_count ||= 0
|
||||
|
||||
if SimpleCache.expired?(:changed_count)
|
||||
@changed_count = SimpleCache.store :changed_count, DB['SELECT SUM(changed_count) AS changed_count FROM SITES'].first[:changed_count], 4.hours
|
||||
else
|
||||
@changed_count = SimpleCache.get :changed_count
|
||||
end
|
||||
|
||||
@changed_count ||= 0
|
||||
|
||||
=begin
|
||||
if SimpleCache.expired?(:blog_feed_html)
|
||||
@blog_feed_html = ''
|
||||
|
||||
begin
|
||||
xml = HTTP.timeout(global: 2).get('https://blog.neocities.org/feed.xml').to_s
|
||||
feed = Feedjira::Feed.parse xml
|
||||
feed.entries[0..2].each do |entry|
|
||||
@blog_feed_html += %{<a href="#{entry.url}">#{entry.title.split('.').first}</a> <span style="float: right">#{entry.published.strftime('%b %-d, %Y')}</span><br>}
|
||||
end
|
||||
rescue
|
||||
@blog_feed_html = 'The latest news on Neocities can be found on our blog.'
|
||||
end
|
||||
|
||||
@blog_feed_html = SimpleCache.store :blog_feed_html, @blog_feed_html, 8.hours
|
||||
else
|
||||
@blog_feed_html = SimpleCache.get :blog_feed_html
|
||||
end
|
||||
=end
|
||||
|
||||
@blog_feed_html = 'The latest news on Neocities can be found on our blog.'
|
||||
|
||||
if SimpleCache.expired?(:featured_sites)
|
||||
@featured_sites = Site.order(:score.desc).exclude(is_nsfw: true).exclude(is_deleted: true).limit(1000).all.sample(12).collect {|s| {screenshot_url: s.screenshot_url('index.html', '540x405'), uri: s.uri, title: s.title}}
|
||||
SimpleCache.store :featured_sites, @featured_sites, 1.hour
|
||||
else
|
||||
@featured_sites = SimpleCache.get :featured_sites
|
||||
end
|
||||
|
||||
@create_disabled = false
|
||||
|
||||
erb :index, layout: :index_layout
|
||||
end
|
||||
|
||||
get '/welcome' do
|
||||
require_login
|
||||
redirect '/' if current_site.supporter?
|
||||
@title = 'Welcome!'
|
||||
erb :'welcome', locals: {site: current_site}
|
||||
end
|
||||
|
||||
get '/education' do
|
||||
redirect '/' if signed_in?
|
||||
erb :education, layout: :index_layout
|
||||
end
|
||||
|
||||
get '/donate' do
|
||||
erb :'donate'
|
||||
end
|
||||
|
||||
get '/about' do
|
||||
erb :'about'
|
||||
end
|
||||
|
||||
get '/terms' do
|
||||
erb :'terms'
|
||||
end
|
||||
|
||||
get '/privacy' do
|
||||
erb :'privacy'
|
||||
end
|
||||
|
||||
get '/press' do
|
||||
erb :'press'
|
||||
end
|
||||
|
||||
get '/legal/?' do
|
||||
@title = 'Legal Guide to Neocities'
|
||||
erb :'legal'
|
||||
end
|
||||
|
||||
get '/thankyou' do
|
||||
@title = 'Thank you!'
|
||||
erb :'thankyou'
|
||||
end
|
||||
|
||||
get '/cli' do
|
||||
@title = 'Command Line Interface'
|
||||
erb :'cli'
|
||||
end
|
||||
|
||||
get '/forgot_username' do
|
||||
@title = 'Forgot Username'
|
||||
erb :'forgot_username'
|
||||
end
|
||||
|
||||
post '/forgot_username' do
|
||||
if params[:email].blank?
|
||||
flash[:error] = 'Cannot use an empty email address!'
|
||||
redirect '/forgot_username'
|
||||
end
|
||||
|
||||
begin
|
||||
sites = Site.get_recovery_sites_with_email params[:email]
|
||||
rescue ArgumentError
|
||||
redirect '/forgot_username'
|
||||
end
|
||||
|
||||
sites.each do |site|
|
||||
body = <<-EOT
|
||||
Hello! This is the Neocities cat, and I have received a username lookup request using this email address.
|
||||
|
||||
Your username is #{site.username}
|
||||
|
||||
If you didn't request this, you can ignore it. Or hide under a bed. Or take a nap. Your call.
|
||||
|
||||
Meow,
|
||||
the Neocities Cat
|
||||
EOT
|
||||
|
||||
body.strip!
|
||||
|
||||
EmailWorker.perform_async({
|
||||
from: 'web@neocities.org',
|
||||
to: params[:email],
|
||||
subject: '[Neocities] Username lookup',
|
||||
body: body
|
||||
})
|
||||
|
||||
end
|
||||
|
||||
flash[:success] = 'If your email was valid, the Neocities Cat will send an e-mail with your username in it.'
|
||||
redirect '/'
|
||||
end
|
37
app/mockup.rb
Normal file
37
app/mockup.rb
Normal file
|
@ -0,0 +1,37 @@
|
|||
=begin
|
||||
get '/mockup/home' do
|
||||
erb :'mockup/home'
|
||||
end
|
||||
|
||||
get '/mockup/edit' do
|
||||
erb :'mockup/edit'
|
||||
end
|
||||
|
||||
get '/mockup/profile' do
|
||||
require_login
|
||||
erb :'mockup/profile', locals: {site: current_site}
|
||||
end
|
||||
|
||||
get '/mockup/browse' do
|
||||
erb :'mockup/browse'
|
||||
end
|
||||
|
||||
get '/mockup/tips' do
|
||||
erb :'mockup/tips'
|
||||
end
|
||||
|
||||
get '/mockup/welcome' do
|
||||
require_login
|
||||
erb :'mockup/welcome', locals: {site: current_site}
|
||||
end
|
||||
|
||||
get '/mockup/stats' do
|
||||
require_login
|
||||
erb :'mockup/stats', locals: {site: current_site}
|
||||
end
|
||||
|
||||
get '/mockup/tutorial-c1p2' do
|
||||
require_login
|
||||
erb :'mockup/tutorial-c1p2', locals: {site: current_site}
|
||||
end
|
||||
=end
|
86
app/password_reset.rb
Normal file
86
app/password_reset.rb
Normal file
|
@ -0,0 +1,86 @@
|
|||
get '/password_reset' do
|
||||
@title = 'Password Reset'
|
||||
redirect '/' if signed_in?
|
||||
erb :'password_reset'
|
||||
end
|
||||
|
||||
post '/send_password_reset' do
|
||||
if params[:email].blank?
|
||||
flash[:error] = 'You must enter a valid email address.'
|
||||
redirect '/password_reset'
|
||||
end
|
||||
|
||||
sites = Site.get_recovery_sites_with_email params[:email]
|
||||
|
||||
if sites.length > 0
|
||||
token = SecureRandom.uuid.gsub('-', '')+'-'+Time.now.to_i.to_s
|
||||
sites.each do |site|
|
||||
next unless site.parent?
|
||||
site.password_reset_token = token
|
||||
site.save_changes validate: false
|
||||
|
||||
body = <<-EOT
|
||||
Hello! This is the Penelope the Neocities cat, and I have received a password reset request for your e-mail address.
|
||||
|
||||
Go to this URL to reset your password: https://neocities.org/password_reset_confirm?username=#{Rack::Utils.escape(site.username)}&token=#{Rack::Utils.escape(token)}
|
||||
|
||||
This link will expire in 24 hours.
|
||||
|
||||
If you didn't request this password reset, you can ignore it. Or hide under a bed. Or take a nap. Your call.
|
||||
|
||||
Meow,
|
||||
Penelope
|
||||
EOT
|
||||
|
||||
body.strip!
|
||||
|
||||
EmailWorker.perform_async({
|
||||
from: 'web@neocities.org',
|
||||
to: params[:email],
|
||||
subject: '[Neocities] Password Reset',
|
||||
body: body
|
||||
})
|
||||
|
||||
end
|
||||
end
|
||||
|
||||
flash[:success] = "We sent an e-mail with password reset instructions. Check your spam folder if you don't see it in your inbox."
|
||||
redirect '/'
|
||||
end
|
||||
|
||||
get '/password_reset_confirm' do
|
||||
@title = 'Password Reset Confirm'
|
||||
|
||||
if params[:token].nil? || params[:token].strip.empty?
|
||||
flash[:error] = 'Token cannot be empty.'
|
||||
redirect '/'
|
||||
end
|
||||
|
||||
reset_site = Site.where(username: params[:username], password_reset_token: params[:token]).first
|
||||
|
||||
if reset_site.nil?
|
||||
flash[:error] = 'Could not find a site with this username and token.'
|
||||
redirect '/'
|
||||
end
|
||||
|
||||
timestamp = Time.at(reset_site.password_reset_token.split('-').last.to_i)
|
||||
|
||||
if Time.now.to_i - timestamp.to_i > Site::PASSWORD_RESET_EXPIRATION_TIME
|
||||
flash[:error] = 'Token has expired.'
|
||||
redirect '/'
|
||||
end
|
||||
|
||||
if reset_site.is_deleted
|
||||
unless reset_site.undelete!
|
||||
flash[:error] = "Sorry, we cannot restore this account."
|
||||
redirect '/'
|
||||
end
|
||||
end
|
||||
|
||||
reset_site.password_reset_confirmed = true
|
||||
reset_site.save_changes
|
||||
|
||||
session[:id] = reset_site.id
|
||||
|
||||
redirect '/settings#password'
|
||||
end
|
3
app/plan.rb
Normal file
3
app/plan.rb
Normal file
|
@ -0,0 +1,3 @@
|
|||
get '/plan/?' do
|
||||
redirect '/supporter'
|
||||
end
|
393
app/settings.rb
Normal file
393
app/settings.rb
Normal file
|
@ -0,0 +1,393 @@
|
|||
require 'socket'
|
||||
require 'ipaddr'
|
||||
|
||||
get '/settings/?' do
|
||||
require_login
|
||||
@site = parent_site
|
||||
erb :'settings/account'
|
||||
end
|
||||
|
||||
def require_ownership_for_settings
|
||||
@site = Site[username: params[:username]]
|
||||
|
||||
not_found if @site.nil?
|
||||
|
||||
unless @site.owned_by? parent_site
|
||||
flash[:error] = 'Cannot edit this site, you do not have permission.'
|
||||
redirect request.referrer
|
||||
end
|
||||
end
|
||||
|
||||
get '/settings/invoices/?' do
|
||||
require_login
|
||||
@title = 'Invoices'
|
||||
@invoices = parent_site.stripe_customer_id ? Stripe::Invoice.list(customer: parent_site.stripe_customer_id) : []
|
||||
erb :'settings/invoices'
|
||||
end
|
||||
|
||||
get '/settings/:username/?' do |username|
|
||||
# This is for the email_unsubscribe below
|
||||
pass if Site.select(:id).where(username: username).first.nil?
|
||||
require_login
|
||||
require_ownership_for_settings
|
||||
|
||||
@title = "Site settings for #{username}"
|
||||
erb :'settings/site'
|
||||
end
|
||||
|
||||
post '/settings/:username/delete' do
|
||||
require_login
|
||||
require_ownership_for_settings
|
||||
|
||||
if params[:confirm_username] != @site.username
|
||||
flash[:error] = 'Site user name and entered user name did not match.'
|
||||
redirect "/settings/#{@site.username}#delete"
|
||||
end
|
||||
|
||||
@site.deleted_reason = params[:deleted_reason]
|
||||
@site.save validate: false
|
||||
@site.destroy
|
||||
|
||||
flash[:success] = 'Site deleted.'
|
||||
|
||||
if @site.username == current_site.username
|
||||
signout
|
||||
redirect '/'
|
||||
end
|
||||
|
||||
redirect '/settings#sites'
|
||||
end
|
||||
|
||||
post '/settings/:username/profile' do
|
||||
require_login
|
||||
require_ownership_for_settings
|
||||
|
||||
@site.update(
|
||||
profile_comments_enabled: params[:site][:profile_comments_enabled],
|
||||
profile_enabled: params[:site][:profile_enabled]
|
||||
)
|
||||
flash[:success] = 'Profile settings changed.'
|
||||
redirect "/settings/#{@site.username}#profile"
|
||||
end
|
||||
|
||||
post '/settings/:username/change_name' do
|
||||
require_login
|
||||
require_ownership_for_settings
|
||||
|
||||
old_site = Site[username: @site.username]
|
||||
old_username = @site.username
|
||||
|
||||
if params[:name] == nil || params[:name] == ''
|
||||
flash[:error] = 'Name cannot be blank.'
|
||||
redirect "/settings/#{@site.username}#username"
|
||||
end
|
||||
|
||||
if old_username.downcase == params[:name].downcase
|
||||
flash[:error] = 'You already have this name.'
|
||||
redirect "/settings/#{@site.username}#username"
|
||||
end
|
||||
|
||||
old_host = @site.host
|
||||
old_site_file_paths = @site.site_files.collect {|site_file| site_file.path}
|
||||
|
||||
@site.username = params[:name]
|
||||
|
||||
if @site.valid?
|
||||
DB.transaction {
|
||||
@site.save_changes
|
||||
@site.move_files_from old_username
|
||||
}
|
||||
|
||||
old_site.delete_all_thumbnails_and_screenshots
|
||||
old_site.purge_all_cache
|
||||
@site.purge_all_cache
|
||||
@site.regenerate_thumbnails_and_screenshots
|
||||
|
||||
flash[:success] = "Site/user name has been changed. You will need to use this name to login, <b>don't forget it!</b>"
|
||||
redirect "/settings/#{@site.username}#username"
|
||||
else
|
||||
flash[:error] = @site.errors.first.last.first
|
||||
redirect "/settings/#{old_username}#username"
|
||||
end
|
||||
end
|
||||
|
||||
post '/settings/:username/tipping' do
|
||||
require_login
|
||||
require_ownership_for_settings
|
||||
|
||||
current_site.tipping_enabled = params[:site][:tipping_enabled]
|
||||
current_site.tipping_paypal = params[:site][:tipping_paypal]
|
||||
current_site.tipping_bitcoin = params[:site][:tipping_bitcoin]
|
||||
|
||||
if current_site.valid?
|
||||
current_site.save_changes
|
||||
flash[:success] = "Tip settings have been updated."
|
||||
else
|
||||
flash[:error] = current_site.errors.first.last.first
|
||||
end
|
||||
|
||||
redirect "/settings/#{current_site.username}#tipping"
|
||||
end
|
||||
|
||||
post '/settings/:username/change_nsfw' do
|
||||
require_login
|
||||
require_ownership_for_settings
|
||||
|
||||
redirect "/settings/#{@site.username}" if @site.admin_nsfw == true
|
||||
|
||||
@site.is_nsfw = params[:is_nsfw]
|
||||
@site.save_changes validate: false
|
||||
flash[:success] = @site.is_nsfw ? 'Marked 18+' : 'Unmarked 18+'
|
||||
redirect "/settings/#{@site.username}#nsfw"
|
||||
end
|
||||
|
||||
post '/settings/:username/custom_domain' do
|
||||
require_login
|
||||
require_ownership_for_settings
|
||||
|
||||
original_domain = @site.domain
|
||||
@site.domain = params[:domain]
|
||||
|
||||
if params[:domain] =~ /^www\..+$/i
|
||||
flash[:error] = 'Cannot begin with www - please only enter the domain name.'
|
||||
redirect "/settings/#{@site.username}/#custom_domain"
|
||||
end
|
||||
|
||||
begin
|
||||
addr = IPAddr.new @site.values[:domain]
|
||||
if addr.ipv4? || addr.ipv6?
|
||||
flash[:error] = 'IP addresses are not allowed. Please enter a valid domain name.'
|
||||
redirect "/settings/#{@site.username}#custom_domain"
|
||||
end
|
||||
rescue IPAddr::InvalidAddressError
|
||||
end
|
||||
|
||||
begin
|
||||
Socket.gethostbyname @site.values[:domain]
|
||||
rescue SocketError, ResolutionError => e
|
||||
flash[:error] = "The domain isn't setup to use Neocities yet, cannot add. Please make the A and CNAME record changes where you registered your domain."
|
||||
redirect "/settings/#{@site.username}#custom_domain"
|
||||
end
|
||||
|
||||
if @site.valid?
|
||||
@site.save_changes
|
||||
|
||||
if @site.domain != original_domain
|
||||
LetsEncryptWorker.perform_async @site.id
|
||||
# Sometimes the www record isn't ready for some reason, so try a delay to fix that.
|
||||
LetsEncryptWorker.perform_in 40.minutes, @site.id
|
||||
end
|
||||
|
||||
flash[:success] = 'The domain has been successfully updated! Make sure your configuration with the domain registrar is correct. It could take a while for the changes to take effect (15-40 minutes), please be patient.'
|
||||
redirect "/settings/#{@site.username}#custom_domain"
|
||||
else
|
||||
flash[:error] = @site.errors.first.last.first
|
||||
redirect "/settings/#{@site.username}#custom_domain"
|
||||
end
|
||||
end
|
||||
|
||||
post '/settings/:username/bluesky_set_did' do
|
||||
require_login
|
||||
require_ownership_for_settings
|
||||
|
||||
# todo standards based validation
|
||||
if params[:did].length > 50
|
||||
flash[:error] = 'DID provided was too long'
|
||||
elsif !params[:did].match(/^did:plc:([a-z|0-9)]+)$/)
|
||||
flash[:error] = 'DID was invalid'
|
||||
else
|
||||
tmpfile = Tempfile.new 'atproto-did'
|
||||
tmpfile.write params[:did]
|
||||
tmpfile.close
|
||||
|
||||
@site.store_files [{filename: '.well-known/atproto-did', tempfile: tmpfile}]
|
||||
$redis_proxy.hdel "dns-_atproto.#{@site.username}.neocities.org", 'TXT'
|
||||
flash[:success] = 'DID set! You can now verify the handle on the Bluesky app.'
|
||||
end
|
||||
|
||||
redirect "/settings/#{@site.username}#bluesky"
|
||||
end
|
||||
|
||||
post '/settings/:username/generate_api_key' do
|
||||
require_login
|
||||
require_ownership_for_settings
|
||||
is_new = @site.api_key.nil?
|
||||
@site.generate_api_key!
|
||||
|
||||
msg = is_new ? "New API key has been generated." : "API key has been regenerated."
|
||||
flash[:success] = msg
|
||||
redirect "/settings/#{@site.username}#api_key"
|
||||
end
|
||||
|
||||
post '/settings/change_password' do
|
||||
require_login
|
||||
|
||||
if !current_site.password_reset_confirmed && !Site.valid_login?(parent_site.username, params[:current_password])
|
||||
flash[:error] = 'Your provided password does not match the current one.'
|
||||
redirect "/settings#password"
|
||||
end
|
||||
|
||||
parent_site.password = params[:new_password]
|
||||
parent_site.valid?
|
||||
|
||||
if params[:new_password] != params[:new_password_confirm]
|
||||
parent_site.errors.add :password, 'New passwords do not match.'
|
||||
end
|
||||
|
||||
parent_site.password_reset_token = nil
|
||||
parent_site.password_reset_confirmed = false
|
||||
|
||||
if parent_site.errors.empty?
|
||||
parent_site.save_changes
|
||||
|
||||
parent_site.send_email(
|
||||
subject: "[Neocities] Your password has been changed",
|
||||
body: Tilt.new('./views/templates/email/password_changed.erb', pretty: true).render(self)
|
||||
)
|
||||
|
||||
flash[:success] = 'Successfully changed password.'
|
||||
redirect "/settings#password"
|
||||
else
|
||||
flash[:error] = current_site.errors.first.last.first
|
||||
redirect '/settings#password'
|
||||
end
|
||||
end
|
||||
|
||||
post '/settings/change_email' do
|
||||
require_login
|
||||
|
||||
if params[:from_confirm]
|
||||
redirect_url = "/site/#{parent_site.username}/confirm_email"
|
||||
else
|
||||
redirect_url = '/settings#email'
|
||||
end
|
||||
|
||||
if params[:email] == parent_site.email
|
||||
flash[:error] = 'You are already using this email address for this account.'
|
||||
redirect redirect_url
|
||||
end
|
||||
|
||||
previous_email = parent_site.email
|
||||
parent_site.email = params[:email]
|
||||
parent_site.email_confirmation_token = SecureRandom.hex 3
|
||||
parent_site.email_confirmed = false
|
||||
parent_site.password_reset_token = nil
|
||||
|
||||
if parent_site.valid?
|
||||
parent_site.save_changes
|
||||
send_confirmation_email
|
||||
|
||||
parent_site.send_email(
|
||||
subject: "[Neocities] Your email address has been changed",
|
||||
body: Tilt.new('./views/templates/email/email_changed.erb', pretty: true).render(self, site: parent_site, previous_email: previous_email)
|
||||
)
|
||||
|
||||
if !parent_site.supporter?
|
||||
session[:fromsettings] = true
|
||||
redirect "/site/#{parent_site.email}/confirm_email"
|
||||
else
|
||||
flash[:success] = 'Email address changed.'
|
||||
redirect '/settings#email'
|
||||
end
|
||||
end
|
||||
|
||||
flash[:error] = parent_site.errors.first.last.first
|
||||
redirect redirect_url
|
||||
end
|
||||
|
||||
post '/settings/change_email_notification' do
|
||||
require_login
|
||||
|
||||
owner = current_site.owner
|
||||
|
||||
owner.send_emails = params[:send_emails]
|
||||
owner.send_comment_emails = params[:send_comment_emails]
|
||||
owner.send_follow_emails = params[:send_follow_emails]
|
||||
owner.email_invoice = params[:email_invoice]
|
||||
owner.save_changes validate: false
|
||||
flash[:success] = 'Email notification settings have been updated.'
|
||||
redirect '/settings#email'
|
||||
end
|
||||
|
||||
post '/settings/change_editor_settings' do
|
||||
require_login
|
||||
|
||||
owner = current_site.owner
|
||||
|
||||
owner.editor_autocomplete_enabled = params[:editor_autocomplete_enabled]
|
||||
owner.editor_font_size = params[:editor_font_size]
|
||||
owner.editor_keyboard_mode = params[:editor_keyboard_mode]
|
||||
owner.editor_tab_width = params[:editor_tab_width]
|
||||
owner.editor_help_tooltips = params[:editor_help_tooltips]
|
||||
owner.save_changes validate: false
|
||||
|
||||
@filename = params[:path]
|
||||
redirect '/site_files/text_editor?filename=' + Rack::Utils.escape(@filename)
|
||||
end
|
||||
|
||||
post '/settings/create_child' do
|
||||
require_login
|
||||
|
||||
if !current_site.plan_feature(:unlimited_site_creation)
|
||||
flash[:error] = 'Cannot create a new site with your current plan, please become a supporter.'
|
||||
redirect '/settings#sites'
|
||||
end
|
||||
|
||||
site = Site.new
|
||||
|
||||
site.parent_site_id = parent_site.id
|
||||
site.username = params[:username]
|
||||
|
||||
if site.valid?
|
||||
site.save
|
||||
flash[:success] = 'Your new site has been created! To manage it, click your username in the top right and go to "Switch Site".'
|
||||
redirect '/settings#sites'
|
||||
else
|
||||
flash[:error] = site.errors.first.last.first
|
||||
redirect '/settings#sites'
|
||||
end
|
||||
end
|
||||
|
||||
get '/settings/unsubscribe_email/?' do
|
||||
redirect "/settings/#email" if signed_in?
|
||||
|
||||
if params[:email] && params[:token] && params[:email] != '' && Site.valid_email_unsubscribe_token?(params[:email], params[:token])
|
||||
Site.where(email: params[:email]).all.each do |site|
|
||||
site.send_emails = false
|
||||
site.save_changes validate: false
|
||||
end
|
||||
|
||||
@message = "You have been successfully unsubscribed from future emails to #{params[:email]}. Our apologies for the inconvenience."
|
||||
else
|
||||
@message = 'There was an error unsubscribing your email address. Please contact support.'
|
||||
end
|
||||
erb :'settings/account/unsubscribe'
|
||||
end
|
||||
|
||||
post '/settings/update_card' do
|
||||
require_login
|
||||
|
||||
customer = Stripe::Customer.retrieve parent_site.stripe_customer_id
|
||||
|
||||
old_card_ids = customer.sources.collect {|s| s.id}
|
||||
|
||||
begin
|
||||
customer.sources.create source: params[:stripe_token]
|
||||
rescue Stripe::InvalidRequestError, Stripe::CardError => e
|
||||
if e.message.match /cannot use a.+token more than once/
|
||||
flash[:error] = 'Card is already being used.'
|
||||
redirect '/settings#billing'
|
||||
elsif e.message.match /Your card was declined/
|
||||
flash[:error] = 'The card was declined. Please contact your bank.'
|
||||
else
|
||||
raise e
|
||||
end
|
||||
end
|
||||
|
||||
old_card_ids.each do |card_id|
|
||||
customer.sources.retrieve(card_id).delete
|
||||
end
|
||||
|
||||
flash[:success] = 'Card information updated.'
|
||||
redirect '/settings#billing'
|
||||
end
|
80
app/signin.rb
Normal file
80
app/signin.rb
Normal file
|
@ -0,0 +1,80 @@
|
|||
get '/signin/?' do
|
||||
dashboard_if_signed_in
|
||||
@title = 'Sign In'
|
||||
erb :'signin/index'
|
||||
end
|
||||
|
||||
post '/signin' do
|
||||
dashboard_if_signed_in
|
||||
|
||||
if Site.valid_login? params[:username], params[:password]
|
||||
site = Site.get_with_identifier params[:username]
|
||||
|
||||
if site.is_banned
|
||||
flash[:error] = 'Invalid login.'
|
||||
flash[:username] = params[:username]
|
||||
redirect '/signin'
|
||||
end
|
||||
|
||||
if site.is_deleted
|
||||
session[:deleted_site_id] = site.id
|
||||
redirect '/signin/restore'
|
||||
end
|
||||
|
||||
session[:id] = site.id
|
||||
redirect '/'
|
||||
else
|
||||
flash[:error] = 'Invalid login.'
|
||||
flash[:username] = params[:username]
|
||||
redirect '/signin'
|
||||
end
|
||||
end
|
||||
|
||||
get '/signin/restore' do
|
||||
redirect '/' unless session[:deleted_site_id]
|
||||
@site = Site[session[:deleted_site_id]]
|
||||
redirect '/' if @site.nil?
|
||||
@title = 'Restore Deleted Site'
|
||||
erb :'signin/restore'
|
||||
end
|
||||
|
||||
get '/signin/cancel_restore' do
|
||||
session[:deleted_site_id] = nil
|
||||
flash[:success] = 'Site restore was cancelled.'
|
||||
redirect '/'
|
||||
end
|
||||
|
||||
post '/signin/restore' do
|
||||
redirect '/' unless session[:deleted_site_id]
|
||||
@site = Site[session[:deleted_site_id]]
|
||||
session[:deleted_site_id] = nil
|
||||
|
||||
if @site.undelete!
|
||||
session[:id] = @site.id
|
||||
else
|
||||
flash[:error] = "Sorry, we cannot restore this account."
|
||||
end
|
||||
|
||||
redirect '/'
|
||||
end
|
||||
|
||||
get '/signin/:username' do
|
||||
require_login
|
||||
@site = Site[username: params[:username]]
|
||||
|
||||
not_found if @site.nil?
|
||||
|
||||
if @site.owned_by? current_site
|
||||
session[:id] = @site.id
|
||||
redirect request.referrer
|
||||
end
|
||||
|
||||
flash[:error] = 'You do not have permission to switch to this site.'
|
||||
redirect request.referrer
|
||||
end
|
||||
|
||||
post '/signout' do
|
||||
require_login
|
||||
signout
|
||||
redirect '/'
|
||||
end
|
388
app/site.rb
Normal file
388
app/site.rb
Normal file
|
@ -0,0 +1,388 @@
|
|||
get '/site/:username.rss' do |username|
|
||||
site = Site[username: username]
|
||||
halt 404 if site.nil? || (current_site && site.is_blocking?(current_site))
|
||||
content_type :xml
|
||||
site.to_rss
|
||||
end
|
||||
|
||||
get '/site/:username/?' do |username|
|
||||
site = Site[username: username]
|
||||
# TODO: There should probably be a "this site was deleted" page.
|
||||
not_found if site.nil? || site.is_banned || site.is_deleted || (current_site && site.is_blocking?(current_site))
|
||||
|
||||
redirect '/' if site.is_education
|
||||
|
||||
redirect site.uri unless site.profile_enabled
|
||||
|
||||
@title = site.title
|
||||
|
||||
@page = params[:page]
|
||||
@page = 1 if @page.not_an_integer?
|
||||
|
||||
if params[:event_id]
|
||||
not_found if params[:event_id].not_an_integer?
|
||||
not_found if params[:event_id].to_i > 2_147_483_647 # max integer
|
||||
event = Event.where(id: params[:event_id]).exclude(is_deleted: true).first
|
||||
not_found if event.nil?
|
||||
event_site = event.site
|
||||
event_actioning_site = event.actioning_site
|
||||
not_found if current_site && event_site && event_site.is_blocking?(current_site)
|
||||
not_found if current_site && event_actioning_site && event_actioning_site.is_blocking?(current_site)
|
||||
events_dataset = Event.where(id: params[:event_id]).paginate(1, 1)
|
||||
else
|
||||
events_dataset = site.latest_events(@page, current_site)
|
||||
end
|
||||
|
||||
@page_count = events_dataset.page_count || 1
|
||||
@pagination_dataset = events_dataset
|
||||
@latest_events = events_dataset.all
|
||||
|
||||
meta_robots 'noindex, follow'
|
||||
|
||||
erb :'site', locals: {site: site, is_current_site: site == current_site}
|
||||
end
|
||||
|
||||
MAX_STAT_POINTS = 30
|
||||
get '/site/:username/stats' do
|
||||
@default_stat_points = 7
|
||||
@site = Site[username: params[:username]]
|
||||
not_found if @site.nil? || @site.is_banned || @site.is_deleted || (current_site && @site.is_blocking?(current_site))
|
||||
|
||||
@title = "Site stats for #{@site.host}"
|
||||
|
||||
@stats = {}
|
||||
|
||||
%i{referrers locations paths}.each do |stat|
|
||||
@stats[stat] = @site.send("stat_#{stat}_dataset".to_sym).order(:views.desc).limit(100).all
|
||||
end
|
||||
|
||||
@stats[:locations].collect! do |location|
|
||||
location_name = ''
|
||||
|
||||
location_name += location.city_name if location.city_name
|
||||
|
||||
if location.region_name
|
||||
# Some of the region names are numbers for some reason.
|
||||
begin
|
||||
Integer(location.region_name)
|
||||
rescue
|
||||
location_name += ', ' unless location_name == ''
|
||||
location_name += location.region_name
|
||||
end
|
||||
end
|
||||
|
||||
if location.country_code2 && !$country_codes[location.country_code2].nil?
|
||||
location_name += ', ' unless location_name == ''
|
||||
location_name += $country_codes[location.country_code2]
|
||||
end
|
||||
|
||||
location_hash = {name: location_name, views: location.views}
|
||||
if location.latitude && location.longitude
|
||||
location_hash.merge! latitude: location.latitude, longitude: location.longitude
|
||||
end
|
||||
location_hash
|
||||
end
|
||||
|
||||
stats_dataset = @site.stats_dataset.order(:created_at.desc).exclude(created_at: Date.today)
|
||||
|
||||
if @site.supporter?
|
||||
unless params[:days].to_s == 'sincethebigbang'
|
||||
unless params[:days].not_an_integer?
|
||||
stats_dataset = stats_dataset.limit params[:days]
|
||||
else
|
||||
params[:days] = @default_stat_points
|
||||
stats_dataset = stats_dataset.limit @default_stat_points
|
||||
end
|
||||
end
|
||||
else
|
||||
stats_dataset = stats_dataset.limit @default_stat_points
|
||||
end
|
||||
|
||||
stats = stats_dataset.all.reverse
|
||||
|
||||
if current_site && @site.owned_by?(current_site) && params[:format] == 'csv'
|
||||
content_type 'application/csv'
|
||||
attachment "#{current_site.username}-stats.csv"
|
||||
|
||||
return CSV.generate do |csv|
|
||||
csv << ['day', 'hits', 'views', 'bandwidth']
|
||||
stats.each do |s|
|
||||
csv << [s[:created_at].to_s, s[:hits], s[:views], s[:bandwidth]]
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
if stats.length > MAX_STAT_POINTS
|
||||
stats = stats.select.with_index {|a, i| (i % (stats.length / MAX_STAT_POINTS.to_f).round) == 0}
|
||||
end
|
||||
|
||||
@stats[:stat_days] = stats
|
||||
@multi_tooltip_template = "<%= datasetLabel %> - <%= value %>"
|
||||
|
||||
erb :'site/stats', locals: {site: @site}
|
||||
end
|
||||
|
||||
post '/site/:username/set_editor_theme' do
|
||||
require_login
|
||||
current_site.editor_theme = params[:editor_theme]
|
||||
current_site.save_changes validate: false
|
||||
'ok'
|
||||
end
|
||||
|
||||
get '/site/:username/follows' do |username|
|
||||
@title = "Sites #{username} follows"
|
||||
@site = Site[username: username]
|
||||
not_found if @site.nil? || @site.is_deleted || (current_site && (@site.is_blocking?(current_site) || current_site.is_blocking?(@site)))
|
||||
|
||||
params[:page] ||= "1"
|
||||
|
||||
@pagination_dataset = @site.followings_dataset.paginate(params[:page].to_i, Site::FOLLOW_PAGINATION_LIMIT)
|
||||
erb :'site/follows'
|
||||
end
|
||||
|
||||
get '/site/:username/followers' do |username|
|
||||
@title = "Sites that follow #{username}"
|
||||
@site = Site[username: username]
|
||||
not_found if @site.nil? || @site.is_deleted || (current_site && (@site.is_blocking?(current_site) || current_site.is_blocking?(@site)))
|
||||
|
||||
params[:page] ||= "1"
|
||||
|
||||
@pagination_dataset = @site.follows_dataset.paginate(params[:page].to_i, Site::FOLLOW_PAGINATION_LIMIT)
|
||||
erb :'site/followers'
|
||||
end
|
||||
|
||||
post '/site/:username/comment' do |username|
|
||||
require_login
|
||||
|
||||
site = Site[username: username]
|
||||
|
||||
redirect request.referer if current_site && (site.is_blocking?(current_site) || current_site.is_blocking?(site))
|
||||
|
||||
last_comment = site.profile_comments_dataset.order(:created_at.desc).first
|
||||
|
||||
if last_comment && last_comment.message == params[:message] && last_comment.created_at > 2.hours.ago
|
||||
redirect request.referer
|
||||
end
|
||||
|
||||
if site.profile_comments_enabled == false ||
|
||||
params[:message].empty? ||
|
||||
params[:message].length > Site::MAX_COMMENT_SIZE ||
|
||||
site.is_blocking?(current_site) ||
|
||||
current_site.is_blocking?(site) ||
|
||||
current_site.commenting_allowed? == false ||
|
||||
(current_site.is_a_jerk? && site.id != current_site.id && !site.is_following?(current_site))
|
||||
redirect request.referrer
|
||||
end
|
||||
|
||||
site.add_profile_comment(
|
||||
actioning_site_id: current_site.id,
|
||||
message: params[:message]
|
||||
)
|
||||
|
||||
redirect request.referrer
|
||||
end
|
||||
|
||||
post '/site/:site_id/toggle_follow' do |site_id|
|
||||
require_login
|
||||
content_type :json
|
||||
site = Site[id: site_id]
|
||||
return 403 if site.is_blocking?(current_site)
|
||||
{result: (current_site.toggle_follow(site) ? 'followed' : 'unfollowed')}.to_json
|
||||
end
|
||||
|
||||
post '/site/create_directory' do
|
||||
require_login
|
||||
|
||||
path = "#{params[:dir] || ''}/#{params[:name]}"
|
||||
result = current_site.create_directory path
|
||||
|
||||
if result != true
|
||||
flash[:error] = result
|
||||
end
|
||||
|
||||
redirect "/dashboard?dir=#{Rack::Utils.escape params[:dir]}"
|
||||
end
|
||||
|
||||
get '/site/:username/confirm_email/:token' do
|
||||
@title = 'Confirm email'
|
||||
|
||||
if current_site && current_site.email_confirmed
|
||||
return erb(:'site_email_confirmed')
|
||||
end
|
||||
|
||||
site = Site[username: params[:username]]
|
||||
|
||||
if site.nil?
|
||||
return erb(:'site_email_not_confirmed')
|
||||
end
|
||||
|
||||
if site.email_confirmed
|
||||
return erb(:'site_email_confirmed')
|
||||
end
|
||||
|
||||
if site.email_confirmation_token == params[:token]
|
||||
site.email_confirmation_token = nil
|
||||
site.email_confirmation_count = 0
|
||||
site.email_confirmed = true
|
||||
site.save_changes
|
||||
|
||||
erb :'site_email_confirmed'
|
||||
else
|
||||
erb :'site_email_not_confirmed'
|
||||
end
|
||||
end
|
||||
|
||||
get '/site/:username/confirm_email' do
|
||||
require_login
|
||||
@title = 'Confirm your Email Address'
|
||||
@fromsettings = session[:fromsettings]
|
||||
redirect '/' if current_site.username != params[:username] || !current_site.parent? || current_site.email_confirmed
|
||||
erb :'site/confirm_email'
|
||||
end
|
||||
|
||||
post '/site/:username/confirm_email' do
|
||||
require_login
|
||||
|
||||
redirect '/' if current_site.username != params[:username] || !current_site.parent? || current_site.email_confirmed
|
||||
|
||||
# Update email, resend token
|
||||
if params[:email]
|
||||
send_confirmation_email @site
|
||||
end
|
||||
|
||||
if params[:token].blank?
|
||||
flash[:error] = 'You must enter a valid token.'
|
||||
redirect "/site/#{current_site.username}/confirm_email"
|
||||
end
|
||||
|
||||
if current_site.email_confirmation_token == params[:token]
|
||||
current_site.email_confirmation_token = nil
|
||||
current_site.email_confirmation_count = 0
|
||||
current_site.email_confirmed = true
|
||||
current_site.save_changes
|
||||
|
||||
if session[:fromsettings]
|
||||
session[:fromsettings] = nil
|
||||
flash[:success] = 'Email address changed.'
|
||||
redirect '/settings#email'
|
||||
end
|
||||
|
||||
redirect '/tutorial'
|
||||
else
|
||||
flash[:error] = 'You must enter a valid token.'
|
||||
redirect "/site/#{current_site.username}/confirm_email"
|
||||
end
|
||||
end
|
||||
|
||||
post '/site/:username/block' do |username|
|
||||
require_login
|
||||
site = Site[username: username]
|
||||
redirect request.referer if current_site.id == site.id
|
||||
|
||||
current_site.block! site
|
||||
|
||||
if request.referer.match /\/site\/#{username}/i
|
||||
redirect '/'
|
||||
else
|
||||
redirect request.referer
|
||||
end
|
||||
end
|
||||
|
||||
get '/site/:username/unblock' do |username|
|
||||
require_login
|
||||
site = Site[username: username]
|
||||
|
||||
if site.nil? || current_site.id == site.id
|
||||
redirect request.referer
|
||||
end
|
||||
|
||||
current_site.unblock! site
|
||||
redirect request.referer
|
||||
end
|
||||
|
||||
get '/site/:username/confirm_phone' do
|
||||
require_login
|
||||
redirect '/' unless current_site.phone_verification_needed?
|
||||
@title = 'Verify your Phone Number'
|
||||
erb :'site/confirm_phone'
|
||||
end
|
||||
|
||||
def restart_phone_verification
|
||||
current_site.phone_verification_sent_at = nil
|
||||
current_site.phone_verification_sid = nil
|
||||
current_site.save_changes validate: false
|
||||
redirect "/site/#{current_site.username}/confirm_phone"
|
||||
end
|
||||
|
||||
post '/site/:username/confirm_phone' do
|
||||
require_login
|
||||
redirect '/' unless current_site.phone_verification_needed?
|
||||
|
||||
if params[:phone_intl]
|
||||
phone = Phonelib.parse params[:phone_intl]
|
||||
|
||||
if !phone.valid?
|
||||
flash[:error] = "Invalid phone number, please try again."
|
||||
redirect "/site/#{current_site.username}/confirm_phone"
|
||||
end
|
||||
|
||||
if phone.types.include?(:premium_rate) || phone.types.include?(:shared_cost)
|
||||
flash[:error] = 'Neocities does not support this type of number, please use another number.'
|
||||
redirect "/site/#{current_site.username}/confirm_phone"
|
||||
end
|
||||
|
||||
current_site.phone_verification_sent_at = Time.now
|
||||
current_site.phone_verification_attempts += 1
|
||||
|
||||
if current_site.phone_verification_attempts > Site::PHONE_VERIFICATION_LOCKOUT_ATTEMPTS
|
||||
flash[:error] = 'You have exceeded the number of phone verification attempts allowed.'
|
||||
redirect "/site/#{current_site.username}/confirm_phone"
|
||||
end
|
||||
|
||||
current_site.save_changes validate: false
|
||||
|
||||
verification = $twilio.verify
|
||||
.v2
|
||||
.services($config['twilio_service_sid'])
|
||||
.verifications
|
||||
.create(to: phone.e164, channel: 'sms')
|
||||
|
||||
current_site.phone_verification_sid = verification.sid
|
||||
current_site.save_changes validate: false
|
||||
|
||||
flash[:success] = 'Validation message sent! Check your phone and enter the code below.'
|
||||
else
|
||||
|
||||
restart_phone_verification if current_site.phone_verification_sent_at < Time.now - Site::PHONE_VERIFICATION_EXPIRATION_TIME
|
||||
minutes_remaining = ((current_site.phone_verification_sent_at - (Time.now - Site::PHONE_VERIFICATION_EXPIRATION_TIME))/60).round
|
||||
|
||||
begin
|
||||
# Check code
|
||||
vc = $twilio.verify
|
||||
.v2
|
||||
.services($config['twilio_service_sid'])
|
||||
.verification_checks
|
||||
.create(verification_sid: current_site.phone_verification_sid, code: params[:code])
|
||||
|
||||
# puts vc.status (pending if failed, approved if it passed)
|
||||
if vc.status == 'approved'
|
||||
current_site.phone_verified = true
|
||||
current_site.save_changes validate: false
|
||||
else
|
||||
flash[:error] = "Code was not correct, please try again. If the phone number you entered was incorrect, you can re-enter the number after #{minutes_remaining} more minutes have passed."
|
||||
end
|
||||
|
||||
rescue Twilio::REST::RestError => e
|
||||
if e.message =~ /60202/
|
||||
flash[:error] = "You have exhausted your check attempts. Please try again in #{minutes_remaining} minutes."
|
||||
elsif e.message =~ /20404/ # Unable to create record
|
||||
restart_phone_verification
|
||||
else
|
||||
raise e
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# Will redirect to / automagically if phone was verified
|
||||
redirect "/site/#{current_site.username}/confirm_phone"
|
||||
end
|
270
app/site_files.rb
Normal file
270
app/site_files.rb
Normal file
|
@ -0,0 +1,270 @@
|
|||
get '/site_files/new_page' do
|
||||
require_login
|
||||
@title = 'New Page'
|
||||
erb :'site_files/new_page'
|
||||
end
|
||||
|
||||
# Redirect from original path
|
||||
get '/site_files/new' do
|
||||
require_login
|
||||
redirect '/site_files/new_page'
|
||||
end
|
||||
|
||||
post '/site_files/create' do
|
||||
require_login
|
||||
@errors = []
|
||||
|
||||
filename = params[:filename]
|
||||
|
||||
filename.gsub!(/[^a-zA-Z0-9_\-.]/, '')
|
||||
|
||||
redirect_uri = '/dashboard'
|
||||
redirect_uri += "?dir=#{Rack::Utils.escape params[:dir]}" if params[:dir]
|
||||
|
||||
if filename.nil? || filename.strip.empty?
|
||||
flash[:error] = 'You must provide a file name.'
|
||||
redirect redirect_uri
|
||||
end
|
||||
|
||||
name = "#{filename}"
|
||||
|
||||
name = "#{params[:dir]}/#{name}" if params[:dir]
|
||||
|
||||
name = current_site.scrubbed_path name
|
||||
|
||||
if current_site.file_exists?(name)
|
||||
flash[:error] = %{Web page "#{Rack::Utils.escape_html name}" already exists! Choose another name.}
|
||||
redirect redirect_uri
|
||||
end
|
||||
|
||||
extname = File.extname name
|
||||
|
||||
unless extname.empty? || extname.match(/^\.#{Site::EDITABLE_FILE_EXT}/i)
|
||||
flash[:error] = "Must be an editable text file type (#{Site::VALID_EDITABLE_EXTENSIONS.join(', ')})."
|
||||
redirect redirect_uri
|
||||
end
|
||||
|
||||
site_file = current_site.site_files_dataset.where(path: name).first
|
||||
|
||||
if site_file
|
||||
flash[:error] = 'File already exists, cannot create.'
|
||||
redirect redirect_uri
|
||||
end
|
||||
|
||||
if extname.match(/^\.html|^\.htm/i)
|
||||
begin
|
||||
current_site.install_new_html_file name
|
||||
rescue Sequel::UniqueConstraintViolation
|
||||
end
|
||||
else
|
||||
file_path = current_site.files_path(name)
|
||||
FileUtils.touch file_path
|
||||
File.chmod 0640, file_path
|
||||
|
||||
site_file ||= SiteFile.new site_id: current_site.id, path: name
|
||||
|
||||
site_file.size = 0
|
||||
site_file.set size: 0
|
||||
site_file.set sha1_hash: Digest::SHA1.hexdigest('')
|
||||
site_file.set updated_at: Time.now
|
||||
site_file.save
|
||||
end
|
||||
|
||||
escaped_name = Rack::Utils.escape_html name
|
||||
|
||||
flash[:success] = %{#{escaped_name} was created! <a style="color: #FFFFFF; text-decoration: underline" href="/site_files/text_editor/#{escaped_name}">Click here to edit it</a>.}
|
||||
|
||||
redirect redirect_uri
|
||||
end
|
||||
|
||||
def file_upload_response(error=nil)
|
||||
if error
|
||||
flash[:error] = error
|
||||
end
|
||||
|
||||
if params[:from_button]
|
||||
query_string = params[:dir] ? "?"+Rack::Utils.build_query(dir: params[:dir]) : ''
|
||||
redirect "/dashboard#{query_string}"
|
||||
else
|
||||
halt 406, error if error
|
||||
halt 200, 'File(s) successfully uploaded.'
|
||||
end
|
||||
end
|
||||
|
||||
def require_login_file_upload_ajax
|
||||
file_upload_response 'You are not signed in!' unless signed_in?
|
||||
end
|
||||
|
||||
post '/site_files/delete' do
|
||||
require_login
|
||||
path = HTMLEntities.new.decode params[:filename]
|
||||
begin
|
||||
current_site.delete_file path
|
||||
rescue Sequel::NoExistingObject
|
||||
# the deed was presumably already done
|
||||
end
|
||||
flash[:success] = "Deleted #{Rack::Utils.escape_html params[:filename]}."
|
||||
|
||||
dirname = Pathname(path).dirname
|
||||
dir_query = dirname.nil? || dirname.to_s == '.' ? '' : "?dir=#{Rack::Utils.escape dirname}"
|
||||
|
||||
redirect "/dashboard#{dir_query}"
|
||||
end
|
||||
|
||||
post '/site_files/rename' do
|
||||
require_login
|
||||
path = HTMLEntities.new.decode params[:path]
|
||||
new_path = HTMLEntities.new.decode params[:new_path]
|
||||
site_file = current_site.site_files.select {|s| s.path == path}.first
|
||||
|
||||
escaped_path = Rack::Utils.escape_html path
|
||||
escaped_new_path = Rack::Utils.escape_html new_path
|
||||
|
||||
if site_file.nil?
|
||||
flash[:error] = "File #{escaped_path} does not exist."
|
||||
else
|
||||
res = site_file.rename new_path
|
||||
|
||||
if res.first == true
|
||||
flash[:success] = "Renamed #{escaped_path} to #{escaped_new_path}"
|
||||
else
|
||||
flash[:error] = "Failed to rename #{escaped_path} to #{escaped_new_path}: #{Rack::Utils.escape_html res.last}"
|
||||
end
|
||||
end
|
||||
|
||||
dirname = Pathname(path).dirname
|
||||
dir_query = dirname.nil? || dirname.to_s == '.' ? '' : "?dir=#{Rack::Utils.escape dirname}"
|
||||
|
||||
redirect "/dashboard#{dir_query}"
|
||||
end
|
||||
|
||||
get '/site_files/download' do
|
||||
require_login
|
||||
|
||||
if !current_site.dl_queued_at.nil? && current_site.dl_queued_at > 1.hour.ago
|
||||
flash[:error] = 'Site downloads are currently limited to once per hour, please try again later.'
|
||||
redirect request.referer
|
||||
end
|
||||
|
||||
content_type 'application/zip'
|
||||
attachment "neocities-#{current_site.username}.zip"
|
||||
|
||||
current_site.dl_queued_at = Time.now
|
||||
current_site.save_changes validate: false
|
||||
|
||||
directory_path = current_site.files_path
|
||||
|
||||
stream do |out|
|
||||
ZipTricks::Streamer.open(out) do |zip|
|
||||
Dir["#{directory_path}/**/*"].each do |file|
|
||||
next if File.directory?(file)
|
||||
|
||||
zip_path = file.sub("#{directory_path}/", '')
|
||||
zip.write_stored_file(zip_path) do |file_writer|
|
||||
File.open(file, 'rb') do |file|
|
||||
IO.copy_stream(file, file_writer)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
get %r{\/site_files\/download\/(.+)} do
|
||||
require_login
|
||||
dont_browser_cache
|
||||
not_found if params[:captures].nil? || params[:captures].length != 1
|
||||
filename = params[:captures].first
|
||||
attachment filename
|
||||
send_file current_site.current_files_path(filename)
|
||||
end
|
||||
|
||||
get %r{\/site_files\/text_editor\/(.+)} do
|
||||
require_login
|
||||
dont_browser_cache
|
||||
|
||||
@filename = params[:captures].first
|
||||
redirect '/site_files/text_editor?filename=' + Rack::Utils.escape(@filename)
|
||||
end
|
||||
|
||||
get '/site_files/text_editor' do
|
||||
require_login
|
||||
dont_browser_cache
|
||||
|
||||
@filename = params[:filename]
|
||||
extname = File.extname @filename
|
||||
|
||||
@ace_mode = case extname
|
||||
when /htm|html/ then 'html'
|
||||
when /js/ then 'javascript'
|
||||
when /md/ then 'markdown'
|
||||
when /css/ then 'css'
|
||||
else
|
||||
nil
|
||||
end
|
||||
|
||||
file_path = current_site.current_files_path @filename
|
||||
|
||||
if File.directory? file_path
|
||||
flash[:error] = 'Cannot edit a directory.'
|
||||
redirect '/dashboard'
|
||||
end
|
||||
|
||||
if !File.exist?(file_path)
|
||||
flash[:error] = 'We could not find the requested file.'
|
||||
redirect '/dashboard'
|
||||
end
|
||||
|
||||
@title = "Editing #{@filename}"
|
||||
|
||||
erb :'site_files/text_editor'
|
||||
end
|
||||
|
||||
get '/site_files/allowed_types' do
|
||||
@title = 'Allowed File Types'
|
||||
erb :'site_files/allowed_types'
|
||||
end
|
||||
|
||||
get '/site_files/hotlinking' do
|
||||
@title = 'Hotlinking Information'
|
||||
erb :'site_files/hotlinking'
|
||||
end
|
||||
|
||||
get '/site_files/mount_info' do
|
||||
@title = 'Site Mount Information'
|
||||
erb :'site_files/mount_info'
|
||||
end
|
||||
|
||||
post '/site_files/chat' do
|
||||
require_login
|
||||
dont_browser_cache
|
||||
headers 'X-Accel-Buffering' => 'no'
|
||||
halt(403) unless parent_site.supporter?
|
||||
|
||||
# Ensure the request is treated as a stream
|
||||
stream do |out|
|
||||
url = 'https://api.anthropic.com/v1/messages'
|
||||
|
||||
headers = {
|
||||
"anthropic-version" => "2023-06-01",
|
||||
"anthropic-beta" => "messages-2023-12-15",
|
||||
"content-type" => "application/json",
|
||||
"x-api-key" => $config['anthropic_api_key']
|
||||
}
|
||||
|
||||
body = {
|
||||
model: "claude-3-haiku-20240307",
|
||||
system: params[:system],
|
||||
messages: JSON.parse(params[:messages]),
|
||||
max_tokens: 4096,
|
||||
temperature: 0.5,
|
||||
stream: true
|
||||
}.to_json
|
||||
|
||||
res = HTTP.headers(headers).post(url, body: body)
|
||||
|
||||
while(buffer = res.body.readpartial)
|
||||
out << buffer
|
||||
end
|
||||
end
|
||||
end
|
199
app/supporter.rb
Normal file
199
app/supporter.rb
Normal file
|
@ -0,0 +1,199 @@
|
|||
get '/supporter/?' do
|
||||
@title = 'Become a Supporter'
|
||||
erb :'welcome'
|
||||
end
|
||||
|
||||
post '/supporter/end' do
|
||||
require_login
|
||||
redirect '/' unless parent_site.paying_supporter?
|
||||
parent_site.end_supporter_membership!
|
||||
|
||||
flash[:success] = "Your supporter membership has been cancelled. We're sorry to see you go, but thanks again for your support! Remember, you can always become a supporter again in the future."
|
||||
redirect '/supporter'
|
||||
end
|
||||
|
||||
post '/supporter/update' do
|
||||
require_login
|
||||
plan_type = 'supporter'
|
||||
|
||||
if is_special_upgrade
|
||||
require_admin
|
||||
site = Site[username: params[:username]]
|
||||
|
||||
plan_type = 'special'
|
||||
|
||||
if site.nil?
|
||||
flash[:error] = 'Cannot find the requested user.'
|
||||
redirect '/admin'
|
||||
end
|
||||
end
|
||||
|
||||
site ||= parent_site
|
||||
|
||||
DB.transaction do
|
||||
if site.stripe_customer_id
|
||||
customer = Stripe::Customer.retrieve site.stripe_customer_id
|
||||
customer.cards.each {|card| card.delete}
|
||||
|
||||
if !params[:stripe_token].blank?
|
||||
customer.sources.create source: params[:stripe_token]
|
||||
end
|
||||
|
||||
begin
|
||||
subscription = customer.subscriptions.create plan: plan_type
|
||||
rescue Stripe::CardError => e
|
||||
flash[:error] = "Error: #{Rack::Utils.escape_html e.message}"
|
||||
redirect '/supporter'
|
||||
end
|
||||
|
||||
site.plan_ended = false
|
||||
site.plan_type = plan_type
|
||||
site.stripe_subscription_id = subscription.id
|
||||
site.save_changes validate: false
|
||||
else
|
||||
begin
|
||||
customer = Stripe::Customer.create(
|
||||
source: params[:stripe_token],
|
||||
description: "#{site.username} - #{site.id}",
|
||||
email: site.email,
|
||||
plan: plan_type
|
||||
)
|
||||
rescue Stripe::CardError => e
|
||||
flash[:error] = "Error: #{Rack::Utils.escape_html e.message} This is likely caused by incorrect information, or an issue with your credit card. Please try again, or contact your bank."
|
||||
redirect '/supporter'
|
||||
end
|
||||
|
||||
site.stripe_customer_id = customer.id
|
||||
site.stripe_subscription_id = customer.subscriptions.first.id
|
||||
site.plan_ended = false
|
||||
site.plan_type = plan_type
|
||||
site.save_changes validate: false
|
||||
end
|
||||
end
|
||||
|
||||
if site.email
|
||||
if is_special_upgrade
|
||||
site.send_email(
|
||||
subject: "[Neocities] Your site has been upgraded to supporter!",
|
||||
body: Tilt.new('./views/templates/email/supporter_upgrade.erb', pretty: true).render(self)
|
||||
)
|
||||
|
||||
redirect '/admin'
|
||||
end
|
||||
|
||||
site.send_email(
|
||||
subject: "[Neocities] You've become a supporter!",
|
||||
body: Tilt.new('./views/templates/email/subscription.erb', pretty: true).render(
|
||||
self, {
|
||||
username: site.username,
|
||||
plan_name: Site::PLAN_FEATURES[params[:plan_type].to_sym][:name],
|
||||
plan_space: Site::PLAN_FEATURES[params[:plan_type].to_sym][:space].pretty,
|
||||
plan_bw: Site::PLAN_FEATURES[params[:plan_type].to_sym][:bandwidth].pretty
|
||||
})
|
||||
)
|
||||
end
|
||||
|
||||
if is_special_upgrade
|
||||
flash[:success] = "#{site.username} has been upgraded to supporter."
|
||||
redirect '/admin'
|
||||
end
|
||||
|
||||
redirect '/supporter/thanks'
|
||||
end
|
||||
|
||||
get '/supporter/thanks' do
|
||||
@title = 'Supporter Confirmation'
|
||||
require_login
|
||||
erb :'supporter/thanks'
|
||||
end
|
||||
|
||||
get '/supporter/bitcoin/?' do
|
||||
@title = 'Bitcoin Supporter'
|
||||
erb :'supporter/bitcoin'
|
||||
end
|
||||
|
||||
get '/supporter/paypal' do
|
||||
require_login
|
||||
redirect '/supporter' if parent_site.supporter?
|
||||
|
||||
hash = paypal_recurring_authorization_hash
|
||||
|
||||
if parent_site.paypal_token
|
||||
hash.merge! token: parent_site.paypal_token
|
||||
end
|
||||
|
||||
ppr = PayPal::Recurring.new hash
|
||||
|
||||
paypal_response = ppr.checkout
|
||||
|
||||
if !paypal_response.valid?
|
||||
flash[:error] = 'There was an issue connecting to Paypal, please contact support.'
|
||||
redirect '/supporter'
|
||||
end
|
||||
|
||||
redirect paypal_response.checkout_url
|
||||
end
|
||||
|
||||
get '/supporter/paypal/return' do
|
||||
require_login
|
||||
|
||||
if params[:token].nil? || params[:PayerID].nil?
|
||||
flash[:error] = 'Unknown error, could not complete the request. Please contact Neocities support.'
|
||||
end
|
||||
|
||||
ppr = PayPal::Recurring.new(paypal_recurring_hash.merge(
|
||||
token: params[:token],
|
||||
payer_id: params[:PayerID]
|
||||
))
|
||||
|
||||
paypal_response = ppr.request_payment
|
||||
unless paypal_response.approved? && paypal_response.completed?
|
||||
flash[:error] = 'Unknown error, could not complete the request. Please contact Neocities support.'
|
||||
redirect '/supporter'
|
||||
end
|
||||
|
||||
site = current_site.parent || current_site
|
||||
|
||||
ppr = PayPal::Recurring.new(paypal_recurring_authorization_hash.merge(
|
||||
frequency: 1,
|
||||
token: params[:token],
|
||||
period: :monthly,
|
||||
reference: site.id.to_s,
|
||||
payer_id: params[:PayerID],
|
||||
start_at: 1.month.from_now,
|
||||
failed: 3,
|
||||
outstanding: :next_billing
|
||||
))
|
||||
|
||||
paypal_response = ppr.create_recurring_profile
|
||||
|
||||
site.paypal_token = params[:token]
|
||||
site.paypal_profile_id = paypal_response.profile_id
|
||||
site.paypal_active = true
|
||||
site.plan_type = 'supporter'
|
||||
site.plan_ended = false
|
||||
site.save_changes validate: false
|
||||
|
||||
redirect '/supporter/thanks'
|
||||
end
|
||||
|
||||
def paypal_recurring_hash
|
||||
{
|
||||
ipn_url: "https://neocities.org/webhooks/paypal",
|
||||
description: 'Neocities Supporter - Monthly',
|
||||
amount: Site::PLAN_FEATURES[:supporter][:price].to_s,
|
||||
currency: 'USD'
|
||||
}
|
||||
end
|
||||
|
||||
def paypal_recurring_authorization_hash
|
||||
paypal_recurring_hash.merge(
|
||||
return_url: "https://neocities.org/supporter/paypal/return",
|
||||
cancel_url: "https://neocities.org/supporter",
|
||||
ipn_url: "https://neocities.org/webhooks/paypal"
|
||||
)
|
||||
end
|
||||
|
||||
def is_special_upgrade
|
||||
params[:username] && params[:plan_type] == 'special'
|
||||
end
|
85
app/sysops.rb
Normal file
85
app/sysops.rb
Normal file
|
@ -0,0 +1,85 @@
|
|||
require 'base64'
|
||||
require 'zlib'
|
||||
require 'rubygems/package'
|
||||
|
||||
get '/sysops/proxy/map.txt' do
|
||||
require_proxy_auth
|
||||
domains = ''
|
||||
Site.exclude(domain: nil).
|
||||
exclude(domain: '').
|
||||
select(:username,:domain).
|
||||
all.
|
||||
collect do |s|
|
||||
domains << "#{s.domain} #{s.username};\n"
|
||||
end
|
||||
content_type :text
|
||||
domains
|
||||
end
|
||||
|
||||
get '/sysops/proxy/sslcerts.tar.gz' do
|
||||
require_proxy_auth
|
||||
sites = Site.ssl_sites
|
||||
|
||||
nginx_config = ''
|
||||
|
||||
tar = StringIO.new
|
||||
|
||||
Gem::Package::TarWriter.new(tar) do |writer|
|
||||
writer.mkdir 'sslcerts', 0740
|
||||
writer.mkdir 'sslcerts/certs', 0740
|
||||
|
||||
sites.each do |site|
|
||||
writer.add_file "sslcerts/certs/#{site.username}.key", 0640 do |f|
|
||||
f.write site.ssl_key
|
||||
end
|
||||
|
||||
writer.add_file "sslcerts/certs/#{site.username}.crt", 0640 do |f|
|
||||
f.write site.ssl_cert
|
||||
end
|
||||
|
||||
nginx_config << %{
|
||||
server {
|
||||
listen 443 ssl;
|
||||
server_name #{site.domain} *.#{site.domain};
|
||||
ssl_certificate sslsites/certs/#{site.username}.crt;
|
||||
ssl_certificate_key sslsites/certs/#{site.username}.key;
|
||||
|
||||
location / {
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host #{site.username}.neocities.org;
|
||||
proxy_pass http://127.0.0.1$request_uri;
|
||||
}
|
||||
}
|
||||
}.unindent
|
||||
end
|
||||
|
||||
writer.add_file "sslcerts/sslsites.conf", 0640 do |f|
|
||||
f.write nginx_config
|
||||
end
|
||||
end
|
||||
|
||||
tar.rewind
|
||||
|
||||
package = StringIO.new 'b'
|
||||
package.set_encoding 'binary'
|
||||
gzip = Zlib::GzipWriter.new package
|
||||
gzip.write tar.read
|
||||
tar.close
|
||||
gzip.finish
|
||||
package.rewind
|
||||
|
||||
attachment
|
||||
package.read
|
||||
end
|
||||
|
||||
class ProxyAccessViolation < StandardError; end
|
||||
|
||||
def require_proxy_auth
|
||||
begin
|
||||
auth = request.env['HTTP_AUTHORIZATION']
|
||||
user, pass = Base64.decode64(auth.match(/Basic (.+)/)[1]).split(':')
|
||||
raise ProxyAccessViolation unless pass == $config['proxy_pass']
|
||||
rescue
|
||||
raise ProxyAccessViolation, "Violator: #{request.ip}" unless pass == $config['proxy_pass']
|
||||
end
|
||||
end
|
31
app/tags.rb
Normal file
31
app/tags.rb
Normal file
|
@ -0,0 +1,31 @@
|
|||
post '/tags/add' do
|
||||
require_login
|
||||
current_site.new_tags_string = params[:tags]
|
||||
|
||||
if current_site.valid?
|
||||
current_site.save_tags
|
||||
else
|
||||
flash[:errors] = current_site.errors.first.last.first
|
||||
end
|
||||
|
||||
redirect request.referer
|
||||
end
|
||||
|
||||
post '/tags/remove' do
|
||||
require_login
|
||||
|
||||
if params[:tags].is_a?(Array)
|
||||
DB.transaction {
|
||||
params[:tags].each do |tag|
|
||||
tag_to_remove = current_site.tags.select {|t| t.name == tag}.first
|
||||
current_site.remove_tag(tag_to_remove) if tag_to_remove
|
||||
end
|
||||
}
|
||||
end
|
||||
|
||||
redirect request.referer
|
||||
end
|
||||
|
||||
get '/tags/autocomplete/:name.json' do |name|
|
||||
Tag.autocomplete(name).collect {|t| t[:name]}.to_json
|
||||
end
|
52
app/tutorial.rb
Normal file
52
app/tutorial.rb
Normal file
|
@ -0,0 +1,52 @@
|
|||
def default_tutorial_html
|
||||
<<-EOT.strip
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<body>
|
||||
|
||||
|
||||
Hello World!
|
||||
|
||||
|
||||
</body>
|
||||
</html>
|
||||
EOT
|
||||
end
|
||||
|
||||
get '/tutorials' do
|
||||
erb :'tutorials'
|
||||
end
|
||||
|
||||
get '/tutorial/?' do
|
||||
require_login
|
||||
erb :'tutorial/index'
|
||||
end
|
||||
|
||||
get '/tutorial/:section/?' do
|
||||
require_login
|
||||
not_found unless %w{html}.include?(params[:section])
|
||||
redirect "/tutorial/#{params[:section]}/1"
|
||||
end
|
||||
|
||||
get '/tutorial/:section/:page/?' do
|
||||
require_login
|
||||
@page = params[:page]
|
||||
not_found unless @page.match?(/\A[1-9]\z|\A10\z/)
|
||||
not_found unless %w{html}.include?(params[:section])
|
||||
|
||||
@section = params[:section]
|
||||
|
||||
@title = "#{params[:section].upcase} Tutorial - #{@page}/10"
|
||||
|
||||
|
||||
if @page == '9'
|
||||
unless csrf_safe?
|
||||
signout
|
||||
redirect '/'
|
||||
end
|
||||
current_site.tutorial_required = false
|
||||
current_site.save_changes validate: false
|
||||
end
|
||||
|
||||
erb "tutorial/layout".to_sym
|
||||
end
|
153
app/webhooks.rb
Normal file
153
app/webhooks.rb
Normal file
|
@ -0,0 +1,153 @@
|
|||
post '/webhooks/paypal' do
|
||||
EmailWorker.perform_async({
|
||||
from: 'web@neocities.org',
|
||||
to: 'errors@neocities.org',
|
||||
subject: "[Neocities Paypal Webhook] Received a Webhook from Paypal",
|
||||
body: params.inspect,
|
||||
no_footer: true
|
||||
})
|
||||
|
||||
'ok'
|
||||
end
|
||||
|
||||
def valid_paypal_webhook_source?
|
||||
# https://www.paypal.com/us/smarthelp/article/what-are-the-ip-addresses-for-live-paypal-servers-ts1056
|
||||
request_ip = IPAddress::IPv4.new request.ip
|
||||
['127.0.0.1', '66.211.170.66', '173.0.81.0/24'].each do |ip|
|
||||
return true if IPAddress::IPv4.new(ip).include? request_ip
|
||||
end
|
||||
false
|
||||
end
|
||||
|
||||
post '/webhooks/paypal/tipping_notify' do
|
||||
return 403 unless valid_paypal_webhook_source?
|
||||
payload = JSON.parse Base64.strict_decode64(params[:custom]), symbolize_names: true
|
||||
|
||||
site = Site[payload[:site_id]]
|
||||
|
||||
@tip_hash = {
|
||||
message: (params[:memo] ? params[:memo] : nil),
|
||||
amount: params[:mc_gross],
|
||||
currency: params[:mc_currency],
|
||||
fee: params[:mc_fee],
|
||||
actioning_site: (payload[:actioning_site_id] ? Site[payload[:actioning_site_id]] : nil),
|
||||
paypal_payer_email: params[:payer_email],
|
||||
paypal_receiver_email: params[:receiver_email],
|
||||
paypal_txn_id: params[:txn_id],
|
||||
created_at: DateTime.strptime(params[:payment_date], "%H:%M:%S %b %e, %Y %Z").to_time
|
||||
}
|
||||
|
||||
@tip = site.add_tip @tip_hash
|
||||
|
||||
Event.create(
|
||||
site_id: @tip.site.id,
|
||||
actioning_site_id: (@tip.actioning_site ? @tip.actioning_site.id : nil),
|
||||
tip_id: @tip.id
|
||||
)
|
||||
|
||||
if @tip.actioning_site
|
||||
subject = "You received a #{@tip.amount_string} tip from #{@tip.actioning_site.username}!"
|
||||
else
|
||||
subject = "You received a #{@tip.amount_string} tip!"
|
||||
end
|
||||
|
||||
@tip.site.send_email(
|
||||
subject: subject,
|
||||
body: Tilt.new('./views/templates/email/tip_received.erb', pretty: true).render(self)
|
||||
)
|
||||
|
||||
EmailWorker.perform_async({
|
||||
from: 'web@neocities.org',
|
||||
to: params[:payer_email],
|
||||
subject: "You sent a #{@tip.amount_string} tip!",
|
||||
body: Tilt.new('./views/templates/email/tip_sent.erb', pretty: true).render(self)
|
||||
})
|
||||
end
|
||||
|
||||
post '/webhooks/stripe' do
|
||||
event = JSON.parse request.body.read
|
||||
if event['type'] == 'customer.created'
|
||||
username = event['data']['object']['description'].split(' - ').first
|
||||
email = event['data']['object']['email']
|
||||
|
||||
EmailWorker.perform_async({
|
||||
from: 'web@neocities.org',
|
||||
to: 'contact@neocities.org',
|
||||
subject: "[Neocities] New customer: #{username}",
|
||||
body: "#{username}\n#{email}\n#{Site[username: username].uri}",
|
||||
no_footer: true
|
||||
})
|
||||
end
|
||||
|
||||
if event['type'] == 'charge.failed'
|
||||
site = stripe_get_site_from_event event
|
||||
|
||||
EmailWorker.perform_async({
|
||||
from: 'web@neocities.org',
|
||||
to: site.email,
|
||||
subject: "[Neocities] There was an issue charging your card",
|
||||
body: Tilt.new('./views/templates/email/charge_failure.erb', pretty: true).render(self)
|
||||
})
|
||||
end
|
||||
|
||||
if event['type'] == 'customer.subscription.deleted'
|
||||
site = stripe_get_site_from_event event
|
||||
site.stripe_subscription_id = nil
|
||||
site.plan_type = nil
|
||||
site.plan_ended = true
|
||||
site.save_changes validate: false
|
||||
|
||||
EmailWorker.perform_async({
|
||||
from: 'web@neocities.org',
|
||||
to: site.email,
|
||||
subject: "[Neocities] Supporter plan has ended",
|
||||
body: Tilt.new('./views/templates/email/supporter_ended.erb', pretty: true).render(self)
|
||||
})
|
||||
end
|
||||
|
||||
if event['type'] == 'invoice.payment_succeeded'
|
||||
site = stripe_get_site_from_event event
|
||||
|
||||
if site.email_invoice && site.stripe_paying_supporter?
|
||||
invoice_obj = event['data']['object']
|
||||
|
||||
EmailWorker.perform_async({
|
||||
from: 'web@neocities.org',
|
||||
to: site.email,
|
||||
subject: "[Neocities] Invoice",
|
||||
body: Tilt.new('./views/templates/email/invoice.erb', pretty: true).render(
|
||||
self,
|
||||
site: site,
|
||||
amount: invoice_obj['amount_due'],
|
||||
period_start: Time.at(invoice_obj['period_start']),
|
||||
period_end: Time.at(invoice_obj['period_end']),
|
||||
date: Time.at(invoice_obj['date'])
|
||||
)
|
||||
})
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
'ok'
|
||||
end
|
||||
|
||||
def stripe_get_site_from_event(event)
|
||||
customer_id = event['data']['object']['customer']
|
||||
halt 'ok' if customer_id.nil? # Likely a fraudulent card report
|
||||
customer = Stripe::Customer.retrieve customer_id
|
||||
|
||||
# Some old accounts only have a username for the desc
|
||||
desc_split = customer.description.split(' - ')
|
||||
|
||||
if desc_split.length == 1
|
||||
site_where = {username: desc_split.first}
|
||||
end
|
||||
|
||||
if desc_split.last.not_an_integer?
|
||||
site_where = {username: desc_split.first}
|
||||
else
|
||||
site_where = {id: desc_split.last}
|
||||
end
|
||||
|
||||
Site.where(site_where).first
|
||||
end
|
138
app_helpers.rb
Normal file
138
app_helpers.rb
Normal file
|
@ -0,0 +1,138 @@
|
|||
def dashboard_if_signed_in
|
||||
redirect '/dashboard' if signed_in?
|
||||
end
|
||||
|
||||
def csrf_safe?
|
||||
csrf_token == params[:csrf_token] || csrf_token == request.env['HTTP_X_CSRF_TOKEN']
|
||||
end
|
||||
|
||||
def csrf_token
|
||||
session[:_csrf_token] ||= SecureRandom.base64(32)
|
||||
end
|
||||
|
||||
def is_education?
|
||||
current_site && current_site.is_education
|
||||
end
|
||||
|
||||
def require_login
|
||||
redirect '/' unless signed_in? && current_site
|
||||
end
|
||||
|
||||
def signed_in?
|
||||
return false if current_site.nil?
|
||||
true
|
||||
end
|
||||
|
||||
def signout
|
||||
@_site = nil
|
||||
@_parent_site = nil
|
||||
session[:id] = nil
|
||||
session.clear
|
||||
response.delete_cookie 'neocities', path: '/'
|
||||
request.env['rack.session.options'][:drop] = true
|
||||
end
|
||||
|
||||
def current_site
|
||||
return nil if session[:id].nil?
|
||||
@_site ||= Site[id: session[:id]]
|
||||
@_parent_site ||= @_site.parent
|
||||
|
||||
if @_site.is_banned || @_site.is_deleted || (@_parent_site && (@_parent_site.is_banned || @_parent_site.is_deleted))
|
||||
signout
|
||||
end
|
||||
|
||||
@_site
|
||||
end
|
||||
|
||||
def parent_site
|
||||
@_parent_site || current_site
|
||||
end
|
||||
|
||||
def meta_robots(newtag=nil)
|
||||
if newtag
|
||||
@_meta_robots = newtag
|
||||
end
|
||||
|
||||
@_meta_robots
|
||||
end
|
||||
|
||||
def title
|
||||
out = "Neocities"
|
||||
return out if request.path == '/'
|
||||
return "#{out} - #{@title}" if @title
|
||||
"#{out} - #{request.path.gsub('/', '').capitalize}"
|
||||
end
|
||||
|
||||
def encoding_fix(file)
|
||||
begin
|
||||
Rack::Utils.escape_html file
|
||||
rescue ArgumentError => e
|
||||
if e.message =~ /invalid byte sequence in UTF-8/ ||
|
||||
e.message =~ /incompatible character encodings/
|
||||
return Rack::Utils.escape_html(file.force_encoding('BINARY'))
|
||||
end
|
||||
fail
|
||||
end
|
||||
end
|
||||
|
||||
def send_confirmation_email(site=current_site)
|
||||
if site.email_confirmation_count > Site::MAXIMUM_EMAIL_CONFIRMATIONS
|
||||
flash[:error] = 'You sent too many email confirmation requests, cannot continue.'
|
||||
redirect request.referrer
|
||||
end
|
||||
|
||||
DB['UPDATE sites set email_confirmation_count=email_confirmation_count+1 WHERE id=?', site.id].first
|
||||
|
||||
EmailWorker.perform_async({
|
||||
from: 'web@neocities.org',
|
||||
reply_to: 'contact@neocities.org',
|
||||
to: site.email,
|
||||
subject: "[Neocities] Confirm your email address",
|
||||
body: Tilt.new('./views/templates/email/confirm.erb', pretty: true).render(self, site: site)
|
||||
})
|
||||
end
|
||||
|
||||
def dont_browser_cache
|
||||
headers['Cache-Control'] = 'private, no-store, max-age=0, no-cache, must-revalidate, post-check=0, pre-check=0'
|
||||
headers['Pragma'] = 'no-cache'
|
||||
headers['Expires'] = 'Fri, 01 Jan 1990 00:00:00 GMT'
|
||||
@dont_browser_cache = true
|
||||
end
|
||||
|
||||
def sanitize_comment(text)
|
||||
Rinku.auto_link Sanitize.fragment(text), :all, 'target="_blank" rel="nofollow"'
|
||||
end
|
||||
|
||||
def flash_display(opts={})
|
||||
erb :'_flash', layout: false, locals: {opts: opts}
|
||||
end
|
||||
|
||||
def hcaptcha_valid?
|
||||
return true if ENV['RACK_ENV'] == 'test' || ENV['CI']
|
||||
return false unless params[:'h-captcha-response']
|
||||
|
||||
resp = HTTP.get('https://hcaptcha.com/siteverify', params: {
|
||||
secret: $config['hcaptcha_secret_key'],
|
||||
response: params[:'h-captcha-response']
|
||||
})
|
||||
|
||||
resp = JSON.parse resp
|
||||
|
||||
if resp['success'] == true
|
||||
true
|
||||
else
|
||||
false
|
||||
end
|
||||
end
|
||||
|
||||
JS_ESCAPE_MAP = {"\\" => "\\\\", "</" => '<\/', "\r\n" => '\n', "\n" => '\n', "\r" => '\n', '"' => '\\"', "'" => "\\'", "`" => "\\`", "$" => "\\$"}
|
||||
|
||||
def escape_javascript(javascript)
|
||||
javascript = javascript.to_s
|
||||
if javascript.empty?
|
||||
result = ""
|
||||
else
|
||||
result = javascript.gsub(/(\\|<\/|\r\n|\342\200\250|\342\200\251|[\n\r"']|[`]|[$])/u, JS_ESCAPE_MAP)
|
||||
end
|
||||
result
|
||||
end
|
9
code-of-conduct.txt
Normal file
9
code-of-conduct.txt
Normal file
|
@ -0,0 +1,9 @@
|
|||
These guidelines apply to Neocities contributor communications, including
|
||||
messages in code repositories, collaboration tools, outreach channels, etc.
|
||||
|
||||
* Language and actions must be free of personal attacks, harassment,
|
||||
discrimination, and NSFW content.
|
||||
|
||||
Instances of unacceptable behavior can be reported by contacting the project
|
||||
team at abuse@neocities.org. All complaints will be investigated and reporter
|
||||
confidentiality will be maintained.
|
101
config.ru
101
config.ru
|
@ -1,7 +1,104 @@
|
|||
require 'rubygems'
|
||||
require './app.rb'
|
||||
require 'sidekiq/web'
|
||||
require 'airbrake/sidekiq'
|
||||
|
||||
map('/') { run Sinatra::Application }
|
||||
use Airbrake::Rack::Middleware
|
||||
|
||||
map('/') do
|
||||
run Sinatra::Application
|
||||
end
|
||||
|
||||
map '/webdav' do
|
||||
use Rack::Auth::Basic do |username, password|
|
||||
@site = Site.get_site_from_login(username, password)
|
||||
@site ? true : false
|
||||
end
|
||||
|
||||
run lambda { |env|
|
||||
request_method = env['REQUEST_METHOD']
|
||||
path = env['PATH_INFO']
|
||||
|
||||
unless @site.owner.supporter?
|
||||
return [
|
||||
402,
|
||||
{
|
||||
'Content-Type' => 'application/xml',
|
||||
'X-Upgrade-Required' => 'https://neocities.org/supporter'
|
||||
},
|
||||
[
|
||||
<<~XML
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<error xmlns="DAV:">
|
||||
<message>WebDAV access requires a supporter account.</message>
|
||||
</error>
|
||||
XML
|
||||
]
|
||||
]
|
||||
end
|
||||
|
||||
case request_method
|
||||
when 'OPTIONS'
|
||||
return [200, {'Allow' => 'OPTIONS, GET, HEAD, PUT, DELETE, PROPFIND, MKCOL, MOVE', 'DAV' => '1,2'}, ['']]
|
||||
|
||||
when 'PUT'
|
||||
tmpfile = Tempfile.new('davfile', encoding: 'binary')
|
||||
tmpfile.write(env['rack.input'].read)
|
||||
tmpfile.close
|
||||
|
||||
return [507, {}, ['']] if @site.file_size_too_large?(tmpfile.size)
|
||||
|
||||
if @site.okay_to_upload?(filename: path, tempfile: tmpfile)
|
||||
@site.store_files([{ filename: path, tempfile: tmpfile }])
|
||||
return [201, {}, ['']]
|
||||
else
|
||||
return [415, {}, ['']]
|
||||
end
|
||||
|
||||
when 'MKCOL'
|
||||
@site.create_directory(path)
|
||||
return [201, {}, ['']]
|
||||
|
||||
when 'MOVE'
|
||||
destination = env['HTTP_DESTINATION'][/\/webdav(.+)$/i, 1]
|
||||
return [400, {}, ['Bad Request']] unless destination
|
||||
|
||||
path.sub!(/^\//, '') # Remove leading slash if present
|
||||
site_file = @site.site_files.find { |s| s.path == path }
|
||||
return [404, {}, ['']] unless site_file
|
||||
|
||||
site_file.rename(destination)
|
||||
return [201, {}, ['']]
|
||||
|
||||
when 'DELETE'
|
||||
@site.delete_file(path)
|
||||
return [201, {}, ['']]
|
||||
|
||||
else
|
||||
unless ['PROPFIND', 'GET', 'HEAD'].include? request_method
|
||||
return [501, {}, ['Not Implemented']]
|
||||
end
|
||||
|
||||
env['PATH_INFO'] = "/#{@site.scrubbed_path(path)}" unless path.empty?
|
||||
|
||||
# Terrible hack to fix WebDAV for the VSC plugin
|
||||
if env['CONTENT_LENGTH'] == "0"
|
||||
env['rack.input'] = StringIO.new('<?xml version="1.0" encoding="utf-8"?>
|
||||
<propfind xmlns="DAV:"><prop>
|
||||
<getcontentlength xmlns="DAV:"/>
|
||||
<getlastmodified xmlns="DAV:"/>
|
||||
<resourcetype xmlns="DAV:"/>
|
||||
</prop></propfind>')
|
||||
env['CONTENT_LENGTH'] = env['rack.input'].length.to_s
|
||||
end
|
||||
|
||||
DAV4Rack::Handler.new(
|
||||
root: @site.files_path,
|
||||
root_uri_path: '/webdav'
|
||||
).call(env)
|
||||
end
|
||||
}
|
||||
end
|
||||
|
||||
map '/sidekiq' do
|
||||
use Rack::Auth::Basic, "Protected Area" do |username, password|
|
||||
|
@ -9,5 +106,7 @@ map '/sidekiq' do
|
|||
username == $config['sidekiq_user'] && password == $config['sidekiq_pass']
|
||||
end
|
||||
|
||||
use Rack::Session::Cookie, key: 'sidekiq.session', secret: Base64.strict_decode64($config['session_secret'])
|
||||
use Rack::Protection::AuthenticityToken
|
||||
run Sidekiq::Web
|
||||
end
|
||||
|
|
31
config.yml.ci
Normal file
31
config.yml.ci
Normal file
|
@ -0,0 +1,31 @@
|
|||
database: 'postgres://postgres:citestpassword@localhost/ci_test'
|
||||
database_pool: 1
|
||||
session_secret: 'SSBqdXN0IHdhbnRlZCB0byBzZWUgd2hhdCB5b3UgbG9va2VkIGxpa2UgaW4gYSBkcmVzcywgRGFkZSBNdXJwaHk='
|
||||
email_unsubscribe_token: "somethingrandomderrrrp"
|
||||
paypal_api_username: derp
|
||||
paypal_api_password: ing
|
||||
paypal_api_signature: tonz
|
||||
logs_path: "/tmp/neocitiestestlogs"
|
||||
letsencrypt_key: ./tests/files/letsencrypt.key
|
||||
letsencrypt_endpoint: https://acme-staging.api.letsencrypt.org/
|
||||
proxy_ips:
|
||||
- 10.0.0.1
|
||||
- 10.0.0.2
|
||||
education_tag_whitelist:
|
||||
- mrteacher
|
||||
stop_forum_spam_api_key: testkey
|
||||
screenshot_urls:
|
||||
- http://screenshots:derp@screenshotssite.com
|
||||
cache_control_ips:
|
||||
- 1.2.3.4
|
||||
- 4.5.6.7
|
||||
hcaptcha_site_key: "10000000-ffff-ffff-ffff-000000000001"
|
||||
hcaptcha_secret_key: "0x0000000000000000000000000000000000000000"
|
||||
twilio_account_sid: ACEDERPDERP
|
||||
twilio_auth_token: derpderpderp
|
||||
twilio_service_sid: VADERPDERPDERP
|
||||
minfraud_account_id: 696969420
|
||||
minfraud_license_key: DERPDERPDERP
|
||||
google_custom_search_key: herpderp
|
||||
google_custom_search_cx: herpderp
|
||||
google_custom_search_query_limit: 69
|
|
@ -1,24 +1,70 @@
|
|||
development:
|
||||
database: 'postgres://neocities@127.0.0.1/neocities'
|
||||
database: 'postgres://localhost/neocities'
|
||||
database_pool: 1
|
||||
session_secret: SECRET GOES HERE
|
||||
recaptcha_public_key: ENTER PUBLIC KEY HERE
|
||||
recaptcha_private_key: ENTER PRIVATE KEY HERE
|
||||
sidekiq_user: ENTER USER HERE
|
||||
sidekiq_pass: ENTER PASS HERE
|
||||
phantomjs_url:
|
||||
- http://localhost:8910
|
||||
stripe_publishable_key: fillout
|
||||
stripe_api_key: fillout
|
||||
redis_url: "redis://localhost"
|
||||
session_secret: "SSBqdXN0IHdhbnRlZCB0byBzZWUgd2hhdCB5b3UgbG9va2VkIGxpa2UgaW4gYSBkcmVzcywgRGFkZSBNdXJwaHk="
|
||||
hcaptcha_site_key: "10000000-ffff-ffff-ffff-000000000001"
|
||||
hcaptcha_secret_key: "0x0000000000000000000000000000000000000000"
|
||||
sidekiq_user: "ENTER USER HERE"
|
||||
sidekiq_pass: "ENTER PASS HERE"
|
||||
stripe_publishable_key: "ENTER KEY HERE"
|
||||
stripe_api_key: "ENTER KEY HERE"
|
||||
ip_hash_salt: "400$8$1$fc21863da5d531c1"
|
||||
proxy_pass: 'somethinglongandrandom'
|
||||
email_unsubscribe_token: 'somethingrandom'
|
||||
logs_path: /path/to/nginx/logs
|
||||
paypal_api_username: derp
|
||||
paypal_api_password: ing
|
||||
paypal_api_signature: tonz
|
||||
letsencrypt_key: ./tests/files/letsencrypt.key
|
||||
letsencrypt_endpoint: https://acme-staging.api.letsencrypt.org/
|
||||
minfraud_account_id: 696969420
|
||||
minfraud_license_key: DERPDERPDERP
|
||||
proxy_ips:
|
||||
- 10.0.0.1
|
||||
- 10.0.0.2
|
||||
education_tag_whitelist:
|
||||
- mrteacher
|
||||
screenshot_urls:
|
||||
- http://screenshots:derp@127.0.0.1:12345
|
||||
stop_forum_spam_api_key: testkey
|
||||
google_custom_search_key: herpderp
|
||||
google_custom_search_cx: herpderp
|
||||
google_custom_search_query_limit: 69
|
||||
test:
|
||||
database: 'postgres://neocities@127.0.0.1/neocities_test'
|
||||
database: 'postgres://localhost/neocities_test'
|
||||
database_pool: 1
|
||||
session_secret: SECRET GOES HERE
|
||||
recaptcha_public_key: ENTER PUBLIC KEY HERE
|
||||
recaptcha_private_key: ENTER PRIVATE KEY HERE
|
||||
sidekiq_user: ENTER USER HERE
|
||||
sidekiq_pass: ENTER PASS HERE
|
||||
phantomjs_url:
|
||||
- http://localhost:8910
|
||||
stripe_publishable_key: fillout
|
||||
stripe_api_key: fillout
|
||||
session_secret: "SSBqdXN0IHdhbnRlZCB0byBzZWUgd2hhdCB5b3UgbG9va2VkIGxpa2UgaW4gYSBkcmVzcywgRGFkZSBNdXJwaHk="
|
||||
hcaptcha_site_key: "10000000-ffff-ffff-ffff-000000000001"
|
||||
hcaptcha_secret_key: "0x0000000000000000000000000000000000000000"
|
||||
sidekiq_user: "ENTER USER HERE"
|
||||
sidekiq_pass: "ENTER PASS HERE"
|
||||
stripe_publishable_key: "ENTER KEY HERE"
|
||||
stripe_api_key: "ENTER KEY HERE"
|
||||
ip_hash_salt: "400$8$1$fc21863da5d531c1"
|
||||
proxy_pass: 'somethinglongandrandom'
|
||||
email_unsubscribe_token: 'somethingrandom'
|
||||
paypal_api_username: derp
|
||||
paypal_api_password: ing
|
||||
paypal_api_signature: tonz
|
||||
letsencrypt_key: ./tests/files/letsencrypt.key
|
||||
letsencrypt_endpoint: https://acme-staging.api.letsencrypt.org/
|
||||
proxy_ips:
|
||||
- 10.0.0.1
|
||||
- 10.0.0.2
|
||||
education_tag_whitelist:
|
||||
- mrteacher
|
||||
stop_forum_spam_api_key: testkey
|
||||
screenshot_urls:
|
||||
- http://screenshots:derp@screenshotssite.com
|
||||
cache_control_ips:
|
||||
- 1.2.3.4
|
||||
- 4.5.6.7
|
||||
twilio_account_sid: ACEDERPDERP
|
||||
twilio_auth_token: derpderpderp
|
||||
twilio_service_sid: VADERPDERPDERP
|
||||
minfraud_account_id: 696969420
|
||||
minfraud_license_key: DERPDERPDERP
|
||||
google_custom_search_key: herpderp
|
||||
google_custom_search_cx: herpderp
|
||||
google_custom_search_query_limit: 69
|
|
@ -1,7 +0,0 @@
|
|||
database: 'postgres://postgres@localhost/travis_ci_test'
|
||||
database_pool: 1
|
||||
session_secret: 's3cr3t'
|
||||
recaptcha_public_key: '1234'
|
||||
recaptcha_private_key: '5678'
|
||||
phantomjs_url:
|
||||
- http://localhost:8910
|
167
environment.rb
167
environment.rb
|
@ -1,3 +1,5 @@
|
|||
# frozen_string_literal: true
|
||||
RubyVM::YJIT.enable
|
||||
ENV['RACK_ENV'] ||= 'development'
|
||||
ENV['TZ'] = 'UTC'
|
||||
DIR_ROOT = File.expand_path File.dirname(__FILE__)
|
||||
|
@ -7,13 +9,25 @@ Encoding.default_external = 'UTF-8'
|
|||
require 'yaml'
|
||||
require 'json'
|
||||
require 'logger'
|
||||
require 'zip'
|
||||
|
||||
Bundler.require
|
||||
Bundler.require :development if ENV['RACK_ENV'] == 'development'
|
||||
|
||||
if ENV['TRAVIS']
|
||||
$config = YAML.load_file File.join(DIR_ROOT, 'config.yml.travis')
|
||||
require 'tilt/erubi'
|
||||
require 'active_support'
|
||||
require 'active_support/time'
|
||||
|
||||
class File
|
||||
def self.exists?(val)
|
||||
self.exist?(val)
|
||||
end
|
||||
end
|
||||
|
||||
Dir['./ext/**/*.rb'].each {|f| require f}
|
||||
|
||||
# :nocov:
|
||||
if ENV['CI']
|
||||
$config = YAML.load_file File.join(DIR_ROOT, 'config.yml.ci')
|
||||
else
|
||||
begin
|
||||
$config = YAML.load_file(File.join(DIR_ROOT, 'config.yml'))[ENV['RACK_ENV']]
|
||||
|
@ -22,90 +36,163 @@ else
|
|||
exit
|
||||
end
|
||||
end
|
||||
# :nocov:
|
||||
|
||||
DB = Sequel.connect $config['database'], sslmode: 'disable', max_connections: $config['database_pool']
|
||||
DB.extension :pagination
|
||||
DB.extension :auto_literal_strings
|
||||
Sequel.split_symbols = true
|
||||
Sidekiq.strict_args!(false)
|
||||
|
||||
Dir.glob('workers/*.rb').each {|w| require File.join(DIR_ROOT, "/#{w}") }
|
||||
require 'will_paginate/sequel'
|
||||
|
||||
# :nocov:
|
||||
=begin
|
||||
if defined?(Pry)
|
||||
Pry.commands.alias_command 'c', 'continue'
|
||||
Pry.commands.alias_command 's', 'step'
|
||||
Pry.commands.alias_command 'n', 'next'
|
||||
Pry.commands.alias_command 'f', 'finish'
|
||||
end
|
||||
=end
|
||||
# :nocov:
|
||||
|
||||
Sidekiq::Logging.logger = nil unless ENV['RACK_ENV'] == 'production'
|
||||
unless ENV['RACK_ENV'] == 'production'
|
||||
Sidekiq.configure_server do |config|
|
||||
config.logger = nil
|
||||
end
|
||||
end
|
||||
|
||||
sidekiq_redis_config = {namespace: 'neocitiesworker'}
|
||||
sidekiq_redis_config = {}
|
||||
sidekiq_redis_config[:url] = $config['sidekiq_url'] if $config['sidekiq_url']
|
||||
|
||||
# :nocov:
|
||||
Sidekiq.configure_server do |config|
|
||||
config.redis = sidekiq_redis_config
|
||||
end
|
||||
# :nocov:
|
||||
|
||||
Sidekiq.configure_client do |config|
|
||||
config.logger = nil
|
||||
config.redis = sidekiq_redis_config
|
||||
end
|
||||
|
||||
require File.join(DIR_ROOT, 'workers', 'thumbnail_worker.rb')
|
||||
require File.join(DIR_ROOT, 'workers', 'screenshot_worker.rb')
|
||||
require File.join(DIR_ROOT, 'workers', 'email_worker.rb')
|
||||
if ENV['RACK_ENV'] == 'test'
|
||||
$redis = MockRedis.new
|
||||
else
|
||||
$redis = Redis.new url: $config['redis_url']
|
||||
end
|
||||
|
||||
$redis_cache = Redis::Namespace.new :cache, redis: $redis
|
||||
|
||||
if ENV['RACK_ENV'] == 'test'
|
||||
$redis_proxy = MockRedis.new
|
||||
else
|
||||
$redis_proxy = Redis.new url: $config['redis_proxy']
|
||||
end
|
||||
|
||||
# :nocov:
|
||||
if ENV['RACK_ENV'] == 'development'
|
||||
# Run async jobs immediately in development.
|
||||
=begin
|
||||
module Sidekiq
|
||||
module Worker
|
||||
module ClassMethods
|
||||
def perform_async(*args)
|
||||
Thread.new {
|
||||
self.new.perform *args
|
||||
}
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
=end
|
||||
end
|
||||
# :nocov:
|
||||
|
||||
Sequel.datetime_class = Time
|
||||
Sequel.extension :core_extensions
|
||||
Sequel.extension :migration
|
||||
Sequel::Model.plugin :validation_helpers
|
||||
Sequel::Model.plugin :force_encoding, 'UTF-8'
|
||||
Sequel::Model.plugin :timestamps, create: :created_at, update: :updated_at
|
||||
Sequel::Model.plugin :defaults_setter
|
||||
Sequel.default_timezone = 'UTC'
|
||||
Sequel::Model.plugin :create_timestamp
|
||||
Sequel.default_timezone = :utc
|
||||
Sequel::Migrator.apply DB, './migrations'
|
||||
|
||||
Stripe.api_key = $config['stripe_api_key']
|
||||
|
||||
Dir.glob('models/*.rb').each {|m| require File.join(DIR_ROOT, "#{m}") }
|
||||
Dir.glob('workers/*.rb').each {|w| require File.join(DIR_ROOT, "/#{w}") }
|
||||
|
||||
DB.loggers << Logger.new(STDOUT) if ENV['RACK_ENV'] == 'development'
|
||||
|
||||
if ENV['RACK_ENV'] == 'development'
|
||||
# If new, throw up a random Server for development.
|
||||
if Server.count == 0
|
||||
Server.create ip: '127.0.0.1', slots_available: 999999
|
||||
end
|
||||
end
|
||||
|
||||
Mail.defaults do
|
||||
#options = { :address => "smtp.gmail.com",
|
||||
# :port => 587,
|
||||
# :domain => 'your.host.name',
|
||||
# :user_name => '<username>',
|
||||
# :password => '<password>',
|
||||
# :authentication => 'plain',
|
||||
# :enable_starttls_auto => true }
|
||||
|
||||
options = {}
|
||||
delivery_method :sendmail, options
|
||||
end
|
||||
|
||||
Sinatra::Application.set :erb, escape_html: true
|
||||
|
||||
# Session fix for Internet Fucking Explorer https://github.com/rkh/rack-protection/issues/11
|
||||
Sinatra::Application.set :protection, except: :session_hijacking
|
||||
require 'sass/plugin/rack'
|
||||
Sinatra::Application.use Sass::Plugin::Rack
|
||||
|
||||
class Sinatra::Base
|
||||
alias_method :render_original, :render
|
||||
def render(engine, data, options = {}, locals = {}, &block)
|
||||
options.merge!(pretty: self.class.development?) if engine == :slim && options[:pretty].nil?
|
||||
render_original engine, data, options, locals, &block
|
||||
end
|
||||
Sass::Plugin.options[:template_location] = 'sass'
|
||||
Sass::Plugin.options[:css_location] = './public/css'
|
||||
Sass::Plugin.options[:style] = :nested
|
||||
|
||||
if ENV['RACK_ENV'] != 'development'
|
||||
Sass::Plugin.options[:style] = :compressed
|
||||
# Sass::Plugin.options[:never_update] = true
|
||||
Sass::Plugin.options[:full_exception] = false
|
||||
end
|
||||
|
||||
class Numeric
|
||||
def roundup(nearest=10)
|
||||
self % nearest == 0 ? self : self + nearest - (self % nearest)
|
||||
PayPal::Recurring.configure do |config|
|
||||
config.sandbox = false
|
||||
config.username = $config['paypal_api_username']
|
||||
config.password = $config['paypal_api_password']
|
||||
config.signature = $config['paypal_api_signature']
|
||||
end
|
||||
|
||||
require 'csv'
|
||||
|
||||
$country_codes = {}
|
||||
|
||||
CSV.foreach("./files/country_codes.csv") do |row|
|
||||
$country_codes[row.last] = row.first
|
||||
end
|
||||
|
||||
gandi_opts = {}
|
||||
gandi_opts[:env] = :test # unless ENV['RACK_ENV'] == 'production'
|
||||
$gandi = Gandi::Session.new $config['gandi_api_key'], gandi_opts
|
||||
|
||||
$image_optim = ImageOptim.new pngout: false, svgo: false
|
||||
|
||||
Money.locale_backend = nil
|
||||
Money.default_currency = Money::Currency.new("USD")
|
||||
Money.rounding_mode = BigDecimal::ROUND_HALF_UP
|
||||
|
||||
$twilio = Twilio::REST::Client.new $config['twilio_account_sid'], $config['twilio_auth_token']
|
||||
|
||||
Minfraud.configure do |c|
|
||||
c.account_id = $config['minfraud_account_id']
|
||||
c.license_key = $config['minfraud_license_key']
|
||||
c.enable_validation = true
|
||||
end
|
||||
|
||||
|
||||
Airbrake.configure do |c|
|
||||
c.project_id = $config['airbrake_project_id']
|
||||
c.project_key = $config['airbrake_project_key']
|
||||
end
|
||||
|
||||
Airbrake.add_filter do |notice|
|
||||
if notice[:params][:password]
|
||||
# Filter out password.
|
||||
notice[:params][:password] = '[Filtered]'
|
||||
end
|
||||
|
||||
def rounddown(nearest=10)
|
||||
self % nearest == 0 ? self : self - (self % nearest)
|
||||
end
|
||||
notice.ignore! if notice.stash[:exception].is_a?(Sinatra::NotFound)
|
||||
end
|
||||
|
||||
Airbrake.add_filter Airbrake::Sidekiq::RetryableJobsFilter.new
|
||||
|
|
13
ext/NilClass.rb
Normal file
13
ext/NilClass.rb
Normal file
|
@ -0,0 +1,13 @@
|
|||
class NilClass
|
||||
def empty?
|
||||
true
|
||||
end
|
||||
|
||||
def blank?
|
||||
true
|
||||
end
|
||||
|
||||
def not_an_integer?
|
||||
true
|
||||
end
|
||||
end
|
169
ext/ago.rb
Normal file
169
ext/ago.rb
Normal file
|
@ -0,0 +1,169 @@
|
|||
|
||||
module Ago
|
||||
module VERSION
|
||||
MAJOR = 0
|
||||
MINOR = 1
|
||||
TINY = 5
|
||||
|
||||
class << self
|
||||
def pretty
|
||||
"#{MAJOR}.#{MINOR}.#{TINY}"
|
||||
end
|
||||
alias_method :print, :pretty
|
||||
end
|
||||
end
|
||||
|
||||
Ago::Order = [:year, :month, :week, :day, :hour, :minute, :second]
|
||||
Ago::Units = {
|
||||
:year => {
|
||||
:basic => 60 * 60 * 24 * 365,
|
||||
:gregorian => 86400 * 365.2425,
|
||||
},
|
||||
:month => {
|
||||
:basic => 60 * 60 * 24 * 30,
|
||||
:gregorian => 86400 * 30.436875,
|
||||
},
|
||||
:week => {
|
||||
:basic => 60 * 60 * 24 * 7,
|
||||
:gregorian => 86400 * 7.02389423076923,
|
||||
},
|
||||
:day => {
|
||||
:basic => 60 * 60 * 24
|
||||
},
|
||||
:hour => {
|
||||
:basic => 60 * 60
|
||||
},
|
||||
:minute => {
|
||||
:basic => 60
|
||||
},
|
||||
:second => {
|
||||
:basic => 1
|
||||
}
|
||||
}
|
||||
|
||||
def Ago.calendar_check(calendar)
|
||||
error = ":calendar => value must be either :basic or :gregorian."
|
||||
unless calendar == :basic || calendar == :gregorian
|
||||
raise ArgumentError, error
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
module Ago::TimeAgo
|
||||
# Generate List of valid unit :symbols
|
||||
valids = ""
|
||||
Ago::Order.each do |u|
|
||||
unless u == :second
|
||||
valids += ":#{u.to_s}, "
|
||||
else
|
||||
valids += "and :#{u.to_s}."
|
||||
end
|
||||
end
|
||||
Valids = valids
|
||||
|
||||
def ago_in_words(opts={})
|
||||
# Process options {hash}
|
||||
focus = opts[:focus] ? opts[:focus] : 0
|
||||
start_at = opts[:start_at] ? opts[:start_at] : :year
|
||||
now = opts[:now] ? opts[:now] : Time.now
|
||||
in_time = opts[:in_time] ? opts[:in_time] : :past
|
||||
calendar = opts[:calendar] ? opts[:calendar] : :basic
|
||||
|
||||
# Filter out invalid arguments for :in_time
|
||||
in_time_error = ":in_time => value must be either :past or :future, " \
|
||||
+ "depending on whether the Time object is before or after Time.now."
|
||||
unless in_time == :past || in_time == :future
|
||||
raise ArgumentError, in_time_error
|
||||
end
|
||||
|
||||
# Filter out invalid arguments for :calendar
|
||||
Ago.calendar_check(calendar)
|
||||
|
||||
# Filter out invalid arguments for :start_at and :focus
|
||||
base_error = " => value must either be a number " +
|
||||
"between 0 and 6 (inclusive),\nor one of the following " +
|
||||
"symbols: " + Valids
|
||||
{:focus => focus, :start_at => start_at}.each do |key, opt|
|
||||
opt_error = ":" + key.to_s + base_error
|
||||
if opt.class == Integer
|
||||
raise ArgumentError, opt_error unless opt >= 0 && opt <= 6
|
||||
elsif opt.class == Symbol
|
||||
raise ArgumentError, opt_error unless Ago::Units[opt]
|
||||
else
|
||||
raise ArgumentError, opt_error
|
||||
end
|
||||
end
|
||||
|
||||
# Create Variables necessary for processing.
|
||||
frags = []
|
||||
output = ""
|
||||
count = 0
|
||||
|
||||
now = calendar == :basic ? now.to_i : now.to_f
|
||||
my_time = calendar == :basic ? self.to_i : self.to_f
|
||||
if now > my_time
|
||||
diff = now - my_time
|
||||
tail = " ago"
|
||||
elsif my_time > now
|
||||
diff = my_time - now
|
||||
tail = " from now"
|
||||
else
|
||||
diff = 0
|
||||
tail = "just now"
|
||||
end
|
||||
|
||||
# Begin Ago.ago processing
|
||||
Ago::Order.each do |u|
|
||||
if calendar == :gregorian && Ago::Units[u][:gregorian]
|
||||
value = Ago::Units[u][:gregorian]
|
||||
else
|
||||
value = Ago::Units[u][:basic]
|
||||
end
|
||||
count += 1
|
||||
|
||||
# Move further ahead in the Ago::Units array if start_at is farther back than
|
||||
# the current point in the array.
|
||||
if start_at.class == Integer
|
||||
next if count <= start_at
|
||||
elsif start_at.class == Symbol
|
||||
next if Ago::Order.index(u) < Ago::Order.index(start_at)
|
||||
end
|
||||
|
||||
n = (diff/value).floor
|
||||
if n > 0
|
||||
plural = n > 1 ? "s" : ""
|
||||
frags << "#{n} #{u.to_s + plural}"
|
||||
|
||||
# If the argument passed into ago() is a symbol, focus the ago statement
|
||||
# down to the level specified in the symbol
|
||||
if focus.class == Symbol
|
||||
break if u == focus || u == :second
|
||||
elsif focus.class == Integer
|
||||
if focus == 0 || u == :second
|
||||
break
|
||||
else
|
||||
focus -= 1
|
||||
end
|
||||
end
|
||||
diff -= n * value
|
||||
end
|
||||
end
|
||||
|
||||
# Der Kommissar
|
||||
frags.size.times do |n|
|
||||
output += frags[n]
|
||||
output += ", " unless n == frags.size - 1
|
||||
end
|
||||
|
||||
return output + "#{tail}"
|
||||
end
|
||||
|
||||
def from_now_in_words(opts={})
|
||||
ago_in_words(opts)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
class Time
|
||||
include Ago::TimeAgo
|
||||
end
|
13
ext/float.rb
Normal file
13
ext/float.rb
Normal file
|
@ -0,0 +1,13 @@
|
|||
class Float
|
||||
def round_to(x)
|
||||
(self * 10**x).round.to_f / 10**x
|
||||
end
|
||||
|
||||
def ceil_to(x)
|
||||
(self * 10**x).ceil.to_f / 10**x
|
||||
end
|
||||
|
||||
def floor_to(x)
|
||||
(self * 10**x).floor.to_f / 10**x
|
||||
end
|
||||
end
|
5
ext/mock_redis.rb
Normal file
5
ext/mock_redis.rb
Normal file
|
@ -0,0 +1,5 @@
|
|||
class MockRedis
|
||||
def publish(channel, message)
|
||||
# TODO make actually useful
|
||||
end
|
||||
end
|
83
ext/numeric.rb
Normal file
83
ext/numeric.rb
Normal file
|
@ -0,0 +1,83 @@
|
|||
class Numeric
|
||||
ONE_MEGABYTE = 1000000
|
||||
|
||||
def roundup(nearest=10)
|
||||
self % nearest == 0 ? self : self + nearest - (self % nearest)
|
||||
end
|
||||
|
||||
def to_mb
|
||||
self/ONE_MEGABYTE.to_f
|
||||
end
|
||||
|
||||
def to_bytes_pretty
|
||||
computed = nil
|
||||
unit = nil
|
||||
{
|
||||
'B' => 1000,
|
||||
'KB' => 1000 * 1000,
|
||||
'MB' => 1000 * 1000 * 1000,
|
||||
'GB' => 1000 * 1000 * 1000 * 1000,
|
||||
'TB' => 1000 * 1000 * 1000 * 1000 * 1000
|
||||
}.each_pair { |e, s|
|
||||
if self < s
|
||||
computed = (self.to_f / (s / 1000)).round(2)
|
||||
unit = e
|
||||
break
|
||||
end
|
||||
}
|
||||
computed = computed.to_i if computed.modulo(1) == 0.0
|
||||
|
||||
"#{computed} #{unit}"
|
||||
end
|
||||
|
||||
def to_gigabytes_pretty
|
||||
self.to_gigabytes.to_s + ' GB'
|
||||
end
|
||||
|
||||
def to_gigabytes
|
||||
self / (1000**3)
|
||||
end
|
||||
|
||||
def to_comma_separated
|
||||
self.to_i.to_s.chars.to_a.reverse.each_slice(3).map(&:join).join(",").reverse
|
||||
end
|
||||
|
||||
def format_large_number
|
||||
return self.to_s.reverse.gsub(/(\d{3})(?=\d)/, '\\1,').reverse
|
||||
if self > 9999
|
||||
if self > 999999999
|
||||
unit_char = 'B' #billion
|
||||
unit_amount = 1000000000.0
|
||||
elsif self > 999999
|
||||
unit_char = 'M' #million
|
||||
unit_amount = 1000000.0
|
||||
elsif self > 9999
|
||||
unit_char = 'K' #thousand
|
||||
unit_amount = 1000.0
|
||||
end
|
||||
|
||||
self_divided = self.to_f / unit_amount
|
||||
self_rounded = self_divided.round(1)
|
||||
|
||||
if self_rounded.denominator == 1
|
||||
return sprintf ("%.0f" + unit_char), self_divided
|
||||
else
|
||||
return sprintf ("%.1f" + unit_char), self_divided
|
||||
end
|
||||
else
|
||||
if self > 999
|
||||
return self.to_s.reverse.gsub(/(\d{3})(?=\d)/, '\\1,').reverse
|
||||
else
|
||||
return self
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def to_space_pretty
|
||||
to_bytes_pretty
|
||||
end
|
||||
|
||||
def not_an_integer?
|
||||
!self.integer?
|
||||
end
|
||||
end
|
8
ext/sequel/plugins/create_timestamp.rb
Normal file
8
ext/sequel/plugins/create_timestamp.rb
Normal file
|
@ -0,0 +1,8 @@
|
|||
module Sequel::Plugins::CreateTimestamp
|
||||
module InstanceMethods
|
||||
def before_create
|
||||
self.created_at = Time.now if respond_to?(:created_at) && !self.created_at
|
||||
super
|
||||
end
|
||||
end
|
||||
end
|
33
ext/sequel/plugins/paranoid_delete.rb
Normal file
33
ext/sequel/plugins/paranoid_delete.rb
Normal file
|
@ -0,0 +1,33 @@
|
|||
module Sequel
|
||||
module ParanoidDelete
|
||||
|
||||
def self.included(base)
|
||||
base.extend(ClassMethods)
|
||||
end
|
||||
|
||||
# Instead of actually deleting, we just set is_deleted to true,
|
||||
# and look for it with our default dataset filter.
|
||||
def delete
|
||||
self.is_deleted = true
|
||||
save :validate => false
|
||||
true
|
||||
end
|
||||
|
||||
module ClassMethods
|
||||
|
||||
# There's no hook for setting default filters after inheritance (that I'm aware of),
|
||||
# so this adds the filter for the first time the class' dataset is accessed for the new model.
|
||||
def dataset
|
||||
if @_is_deleted_filter_set.nil?
|
||||
#KD: I turned this off because I think it's easier to do this manually.
|
||||
#@dataset.filter! is_deleted: false
|
||||
@_is_deleted_filter_set = true
|
||||
end
|
||||
super
|
||||
end
|
||||
|
||||
end
|
||||
end
|
||||
|
||||
# Model.include ParanoidDelete
|
||||
end
|
7
ext/sinatra/base.rb
Normal file
7
ext/sinatra/base.rb
Normal file
|
@ -0,0 +1,7 @@
|
|||
class Sinatra::Base
|
||||
alias_method :render_original, :render
|
||||
def render(engine, data, options = {}, locals = {}, &block)
|
||||
options.merge!(pretty: self.class.development?) if engine == :slim && options[:pretty].nil?
|
||||
render_original engine, data, options, locals, &block
|
||||
end
|
||||
end
|
5
ext/sinatra/indifferent_hash.rb
Normal file
5
ext/sinatra/indifferent_hash.rb
Normal file
|
@ -0,0 +1,5 @@
|
|||
class Sinatra::IndifferentHash
|
||||
def not_an_integer?
|
||||
true
|
||||
end
|
||||
end
|
26
ext/string.rb
Normal file
26
ext/string.rb
Normal file
|
@ -0,0 +1,26 @@
|
|||
class String
|
||||
def shorten(length, usedots=true)
|
||||
if usedots
|
||||
return self if self.length < length
|
||||
"#{self[0..length-3]}..."
|
||||
else
|
||||
self[0..length]
|
||||
end
|
||||
end
|
||||
|
||||
def unindent
|
||||
gsub /^#{scan(/^\s*/).min_by{|l|l.length}}/, ""
|
||||
end
|
||||
|
||||
def blank?
|
||||
return true if self == ''
|
||||
false
|
||||
end
|
||||
|
||||
def not_an_integer?
|
||||
Integer(self)
|
||||
false
|
||||
rescue ArgumentError
|
||||
true
|
||||
end
|
||||
end
|
7
ext/tempfile.rb
Normal file
7
ext/tempfile.rb
Normal file
|
@ -0,0 +1,7 @@
|
|||
class Tempfile
|
||||
alias_method :size_original, :size
|
||||
def size
|
||||
s = size_original
|
||||
s.nil? ? 0 : s
|
||||
end
|
||||
end
|
9
ext/time.rb
Normal file
9
ext/time.rb
Normal file
|
@ -0,0 +1,9 @@
|
|||
class Time
|
||||
def ago
|
||||
self.ago_in_words
|
||||
end
|
||||
|
||||
def from_now
|
||||
self.from_now_in_words
|
||||
end
|
||||
end
|
BIN
files/GeoLiteCity.dat
Normal file
BIN
files/GeoLiteCity.dat
Normal file
Binary file not shown.
250
files/country_codes.csv
Normal file
250
files/country_codes.csv
Normal file
|
@ -0,0 +1,250 @@
|
|||
Afghanistan,AF
|
||||
Åland Islands,AX
|
||||
Albania,AL
|
||||
Algeria,DZ
|
||||
American Samoa,AS
|
||||
Andorra,AD
|
||||
Angola,AO
|
||||
Anguilla,AI
|
||||
Antarctica,AQ
|
||||
Antigua and Barbuda,AG
|
||||
Argentina,AR
|
||||
Armenia,AM
|
||||
Aruba,AW
|
||||
Australia,AU
|
||||
Austria,AT
|
||||
Azerbaijan,AZ
|
||||
Bahamas,BS
|
||||
Bahrain,BH
|
||||
Bangladesh,BD
|
||||
Barbados,BB
|
||||
Belarus,BY
|
||||
Belgium,BE
|
||||
Belize,BZ
|
||||
Benin,BJ
|
||||
Bermuda,BM
|
||||
Bhutan,BT
|
||||
"Bolivia, Plurinational State of",BO
|
||||
"Bonaire, Sint Eustatius and Saba",BQ
|
||||
Bosnia and Herzegovina,BA
|
||||
Botswana,BW
|
||||
Bouvet Island,BV
|
||||
Brazil,BR
|
||||
British Indian Ocean Territory,IO
|
||||
Brunei Darussalam,BN
|
||||
Bulgaria,BG
|
||||
Burkina Faso,BF
|
||||
Burundi,BI
|
||||
Cambodia,KH
|
||||
Cameroon,CM
|
||||
Canada,CA
|
||||
Cape Verde,CV
|
||||
Cayman Islands,KY
|
||||
Central African Republic,CF
|
||||
Chad,TD
|
||||
Chile,CL
|
||||
China,CN
|
||||
Christmas Island,CX
|
||||
Cocos (Keeling) Islands,CC
|
||||
Colombia,CO
|
||||
Comoros,KM
|
||||
Congo,CG
|
||||
"Congo, the Democratic Republic of the",CD
|
||||
Cook Islands,CK
|
||||
Costa Rica,CR
|
||||
Côte d'Ivoire,CI
|
||||
Croatia,HR
|
||||
Cuba,CU
|
||||
Curaçao,CW
|
||||
Cyprus,CY
|
||||
Czech Republic,CZ
|
||||
Denmark,DK
|
||||
Djibouti,DJ
|
||||
Dominica,DM
|
||||
Dominican Republic,DO
|
||||
Ecuador,EC
|
||||
Egypt,EG
|
||||
El Salvador,SV
|
||||
Equatorial Guinea,GQ
|
||||
Eritrea,ER
|
||||
Estonia,EE
|
||||
Ethiopia,ET
|
||||
Falkland Islands (Malvinas),FK
|
||||
Faroe Islands,FO
|
||||
Fiji,FJ
|
||||
Finland,FI
|
||||
France,FR
|
||||
French Guiana,GF
|
||||
French Polynesia,PF
|
||||
French Southern Territories,TF
|
||||
Gabon,GA
|
||||
Gambia,GM
|
||||
Georgia,GE
|
||||
Germany,DE
|
||||
Ghana,GH
|
||||
Gibraltar,GI
|
||||
Greece,GR
|
||||
Greenland,GL
|
||||
Grenada,GD
|
||||
Guadeloupe,GP
|
||||
Guam,GU
|
||||
Guatemala,GT
|
||||
Guernsey,GG
|
||||
Guinea,GN
|
||||
Guinea-Bissau,GW
|
||||
Guyana,GY
|
||||
Haiti,HT
|
||||
Heard Island and McDonald Mcdonald Islands,HM
|
||||
Holy See (Vatican City State),VA
|
||||
Honduras,HN
|
||||
Hong Kong,HK
|
||||
Hungary,HU
|
||||
Iceland,IS
|
||||
India,IN
|
||||
Indonesia,ID
|
||||
"Iran, Islamic Republic of",IR
|
||||
Iraq,IQ
|
||||
Ireland,IE
|
||||
Isle of Man,IM
|
||||
Israel,IL
|
||||
Italy,IT
|
||||
Jamaica,JM
|
||||
Japan,JP
|
||||
Jersey,JE
|
||||
Jordan,JO
|
||||
Kazakhstan,KZ
|
||||
Kenya,KE
|
||||
Kiribati,KI
|
||||
"Korea, Democratic People's Republic of",KP
|
||||
"Korea, Republic of",KR
|
||||
Kuwait,KW
|
||||
Kyrgyzstan,KG
|
||||
Lao People's Democratic Republic,LA
|
||||
Latvia,LV
|
||||
Lebanon,LB
|
||||
Lesotho,LS
|
||||
Liberia,LR
|
||||
Libya,LY
|
||||
Liechtenstein,LI
|
||||
Lithuania,LT
|
||||
Luxembourg,LU
|
||||
Macao,MO
|
||||
"Macedonia, the Former Yugoslav Republic of",MK
|
||||
Madagascar,MG
|
||||
Malawi,MW
|
||||
Malaysia,MY
|
||||
Maldives,MV
|
||||
Mali,ML
|
||||
Malta,MT
|
||||
Marshall Islands,MH
|
||||
Martinique,MQ
|
||||
Mauritania,MR
|
||||
Mauritius,MU
|
||||
Mayotte,YT
|
||||
Mexico,MX
|
||||
"Micronesia, Federated States of",FM
|
||||
"Moldova, Republic of",MD
|
||||
Monaco,MC
|
||||
Mongolia,MN
|
||||
Montenegro,ME
|
||||
Montserrat,MS
|
||||
Morocco,MA
|
||||
Mozambique,MZ
|
||||
Myanmar,MM
|
||||
Namibia,NA
|
||||
Nauru,NR
|
||||
Nepal,NP
|
||||
Netherlands,NL
|
||||
New Caledonia,NC
|
||||
New Zealand,NZ
|
||||
Nicaragua,NI
|
||||
Niger,NE
|
||||
Nigeria,NG
|
||||
Niue,NU
|
||||
Norfolk Island,NF
|
||||
Northern Mariana Islands,MP
|
||||
Norway,NO
|
||||
Oman,OM
|
||||
Pakistan,PK
|
||||
Palau,PW
|
||||
"Palestine, State of",PS
|
||||
Panama,PA
|
||||
Papua New Guinea,PG
|
||||
Paraguay,PY
|
||||
Peru,PE
|
||||
Philippines,PH
|
||||
Pitcairn,PN
|
||||
Poland,PL
|
||||
Portugal,PT
|
||||
Puerto Rico,PR
|
||||
Qatar,QA
|
||||
Réunion,RE
|
||||
Romania,RO
|
||||
Russian Federation,RU
|
||||
Rwanda,RW
|
||||
Saint Barthélemy,BL
|
||||
"Saint Helena, Ascension and Tristan da Cunha",SH
|
||||
Saint Kitts and Nevis,KN
|
||||
Saint Lucia,LC
|
||||
Saint Martin (French part),MF
|
||||
Saint Pierre and Miquelon,PM
|
||||
Saint Vincent and the Grenadines,VC
|
||||
Samoa,WS
|
||||
San Marino,SM
|
||||
Sao Tome and Principe,ST
|
||||
Saudi Arabia,SA
|
||||
Senegal,SN
|
||||
Serbia,RS
|
||||
Seychelles,SC
|
||||
Sierra Leone,SL
|
||||
Singapore,SG
|
||||
Sint Maarten (Dutch part),SX
|
||||
Slovakia,SK
|
||||
Slovenia,SI
|
||||
Solomon Islands,SB
|
||||
Somalia,SO
|
||||
South Africa,ZA
|
||||
South Georgia and the South Sandwich Islands,GS
|
||||
South Sudan,SS
|
||||
Spain,ES
|
||||
Sri Lanka,LK
|
||||
Sudan,SD
|
||||
Suriname,SR
|
||||
Svalbard and Jan Mayen,SJ
|
||||
Swaziland,SZ
|
||||
Sweden,SE
|
||||
Switzerland,CH
|
||||
Syrian Arab Republic,SY
|
||||
"Taiwan, Province of China",TW
|
||||
Tajikistan,TJ
|
||||
"Tanzania, United Republic of",TZ
|
||||
Thailand,TH
|
||||
Timor-Leste,TL
|
||||
Togo,TG
|
||||
Tokelau,TK
|
||||
Tonga,TO
|
||||
Trinidad and Tobago,TT
|
||||
Tunisia,TN
|
||||
Turkey,TR
|
||||
Turkmenistan,TM
|
||||
Turks and Caicos Islands,TC
|
||||
Tuvalu,TV
|
||||
Uganda,UG
|
||||
Ukraine,UA
|
||||
United Arab Emirates,AE
|
||||
United Kingdom,GB
|
||||
United States,US
|
||||
United States Minor Outlying Islands,UM
|
||||
Uruguay,UY
|
||||
Uzbekistan,UZ
|
||||
Vanuatu,VU
|
||||
"Venezuela, Bolivarian Republic of",VE
|
||||
Viet Nam,VN
|
||||
"Virgin Islands, British",VG
|
||||
"Virgin Islands, U.S.",VI
|
||||
Wallis and Futuna,WF
|
||||
Western Sahara,EH
|
||||
Yemen,YE
|
||||
Zambia,ZM
|
||||
Zimbabwe,ZW
|
||||
European Union, EU
|
|
85
files/fullhitsmigration.rb
Normal file
85
files/fullhitsmigration.rb
Normal file
|
@ -0,0 +1,85 @@
|
|||
raise 'nope'
|
||||
|
||||
Sequel.migration do
|
||||
up {
|
||||
raise 'derp'
|
||||
DB.drop_table :stats
|
||||
DB.drop_table :stat_referrers
|
||||
DB.drop_table :stat_paths
|
||||
DB.drop_table :stat_locations
|
||||
|
||||
DB.create_table! :hits do
|
||||
primary_key :id
|
||||
Integer :site_id, index: true
|
||||
Integer :hit_referrer_id
|
||||
Integer :hit_path_id
|
||||
Integer :hit_location_id
|
||||
Bignum :bandwidth
|
||||
Time :accessed_at, index: true
|
||||
end
|
||||
|
||||
DB.create_table! :hit_referrers do
|
||||
primary_key :id
|
||||
String :uri, index: {unique: true}
|
||||
end
|
||||
|
||||
DB.create_table! :hit_locations do
|
||||
primary_key :id
|
||||
String :country_code2
|
||||
String :region_name
|
||||
String :city_name
|
||||
Float :latitude
|
||||
Float :longitude
|
||||
end
|
||||
|
||||
DB.create_table! :hit_paths do
|
||||
primary_key :id
|
||||
String :path, index: {unique: true}
|
||||
end
|
||||
}
|
||||
|
||||
down {
|
||||
raise 'No.' if ENV['RACK_ENV'] == 'production'
|
||||
|
||||
%i{hits hit_referrers hit_locations hit_paths}.each do |t|
|
||||
DB.drop_table t
|
||||
end
|
||||
|
||||
DB.create_table! :stats do
|
||||
primary_key :id
|
||||
Integer :site_id
|
||||
Date :created_at
|
||||
Integer :hits
|
||||
Integer :views
|
||||
Integer :comments
|
||||
Integer :follows
|
||||
Integer :site_updates
|
||||
end
|
||||
|
||||
DB.create_table! :stat_referrers do
|
||||
primary_key :id
|
||||
Integer :stat_id
|
||||
String :url
|
||||
String :views
|
||||
end
|
||||
|
||||
DB.create_table! :stat_locations do
|
||||
primary_key :id
|
||||
Integer :stat_id
|
||||
String :country_code2
|
||||
String :region_name
|
||||
String :city_name
|
||||
Decimal :latitude
|
||||
Decimal :longitude
|
||||
Integer :views
|
||||
end
|
||||
|
||||
DB.create_table :stat_paths do
|
||||
primary_key :id
|
||||
Integer :stat_id
|
||||
String :name
|
||||
Integer :views
|
||||
end
|
||||
}
|
||||
end
|
||||
|
|
@ -1,73 +0,0 @@
|
|||
server {
|
||||
listen 80;
|
||||
listen 443 ssl;
|
||||
ssl_certificate /etc/nginx/certs/neocities.org.crt;
|
||||
ssl_certificate_key /etc/nginx/certs/neocities.org.key;
|
||||
server_name www.neocities.org;
|
||||
rewrite ^(.*)$ $scheme://neocities.org$1 permanent;
|
||||
}
|
||||
|
||||
server {
|
||||
listen 80;
|
||||
listen 443 ssl;
|
||||
ssl_certificate /etc/nginx/certs/neocities.org.crt;
|
||||
ssl_certificate_key /etc/nginx/certs/neocities.org.key;
|
||||
|
||||
set $ssl off;
|
||||
if ($scheme = https) {
|
||||
set $ssl on;
|
||||
}
|
||||
|
||||
root /home/web/neocities-web/public;
|
||||
server_name neocities.org;
|
||||
access_log /var/log/nginx/neocities-web.log;
|
||||
|
||||
error_page 500 = /gateway_error.html;
|
||||
|
||||
# location ~* \.(jpg|jpeg|png|gif|ico|css|js)$ {
|
||||
# expires 60s;
|
||||
# log_not_found off;
|
||||
# }
|
||||
|
||||
try_files $uri @neocities;
|
||||
|
||||
location @neocities {
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header Host $http_host;
|
||||
proxy_set_header X-Forwarded-Ssl $ssl;
|
||||
proxy_max_temp_file_size 0;
|
||||
|
||||
# proxy_pass http://127.0.0.1:20000;
|
||||
proxy_pass http://unix:/var/run/neocities/neocities.sock;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
server {
|
||||
listen 80;
|
||||
listen 443 ssl;
|
||||
ssl_certificate /etc/nginx/certs/neocities.org.crt;
|
||||
ssl_certificate_key /etc/nginx/certs/neocities.org.key;
|
||||
|
||||
server_name ~^(?<subdomain>.+)\.neocities.org$;
|
||||
access_log /var/log/nginx/neocities-sites.log neocities;
|
||||
root /home/web/neocities-web/public/sites/$subdomain;
|
||||
index /index.html;
|
||||
|
||||
error_page 404 = @notfound;
|
||||
|
||||
location @notfound {
|
||||
try_files /not_found.html @notfound_root;
|
||||
}
|
||||
|
||||
location @notfound_root {
|
||||
root /home/web/neocities-web/public;
|
||||
try_files /web_site_not_found.html =404;
|
||||
}
|
||||
|
||||
location ~* \.(html|jpg|jpeg|png|gif|ico|css|js)$ {
|
||||
# expires 20s;
|
||||
log_not_found off;
|
||||
}
|
||||
}
|
|
@ -1,7 +0,0 @@
|
|||
/var/log/nginx/neocities-sites.log {
|
||||
rotate 14
|
||||
create 0640 www-data adm
|
||||
postrotate
|
||||
[ ! -f /var/run/nginx.pid ] || kill -USR1 `cat /var/run/nginx.pid`
|
||||
endscript
|
||||
}
|
111
files/nginx.conf
111
files/nginx.conf
|
@ -1,111 +0,0 @@
|
|||
user www-data www-data;
|
||||
worker_processes 8;
|
||||
pid /var/run/nginx.pid;
|
||||
|
||||
events {
|
||||
worker_connections 4000;
|
||||
multi_accept on;
|
||||
use epoll;
|
||||
}
|
||||
|
||||
http {
|
||||
|
||||
root /home/web/neocities-web/public;
|
||||
error_page 404 = /not_found.html;
|
||||
|
||||
log_format neocities '$time_iso8601 $subdomain $bytes_sent $request_uri ';
|
||||
|
||||
ssl_protocols SSLv3 TLSv1 TLSv1.1 TLSv1.2;
|
||||
ssl_ciphers ECDHE-RSA-AES128-SHA256:AES128-GCM-SHA256:RC4:HIGH:!MD5:!aNULL:!EDH;
|
||||
ssl_prefer_server_ciphers on;
|
||||
|
||||
sendfile on;
|
||||
tcp_nopush on;
|
||||
tcp_nodelay on;
|
||||
keepalive_timeout 65;
|
||||
types_hash_max_size 2048;
|
||||
# server_tokens off;
|
||||
|
||||
reset_timedout_connection on;
|
||||
charset utf-8;
|
||||
client_max_body_size 20m;
|
||||
|
||||
# server_names_hash_bucket_size 64;
|
||||
# server_name_in_redirect off;
|
||||
|
||||
include /etc/nginx/mime.types;
|
||||
default_type application/octet-stream;
|
||||
|
||||
open_file_cache max=200000 inactive=20s;
|
||||
open_file_cache_valid 30s;
|
||||
open_file_cache_min_uses 2;
|
||||
open_file_cache_errors on;
|
||||
|
||||
##
|
||||
# Logging Settings
|
||||
##
|
||||
|
||||
access_log /var/log/nginx/access.log;
|
||||
error_log /var/log/nginx/error.log;
|
||||
|
||||
##
|
||||
# Gzip Settings
|
||||
##
|
||||
|
||||
gzip on;
|
||||
gzip_disable "msie6";
|
||||
gzip_min_length 1024;
|
||||
|
||||
# gzip_vary on;
|
||||
# gzip_proxied any;
|
||||
gzip_comp_level 6;
|
||||
# gzip_buffers 16 8k;
|
||||
# gzip_http_version 1.1;
|
||||
gzip_types text/plain text/css application/json application/x-javascript text/xml application/xml application/xml+rss text/javascript;
|
||||
|
||||
##
|
||||
# nginx-naxsi config
|
||||
##
|
||||
# Uncomment it if you installed nginx-naxsi
|
||||
##
|
||||
|
||||
#include /etc/nginx/naxsi_core.rules;
|
||||
|
||||
##
|
||||
# nginx-passenger config
|
||||
##
|
||||
# Uncomment it if you installed nginx-passenger
|
||||
##
|
||||
|
||||
#passenger_root /usr;
|
||||
#passenger_ruby /usr/bin/ruby;
|
||||
|
||||
##
|
||||
# Virtual Host Configs
|
||||
##
|
||||
|
||||
include /etc/nginx/conf.d/*.conf;
|
||||
include /etc/nginx/sites-enabled/*;
|
||||
}
|
||||
|
||||
|
||||
#mail {
|
||||
# # See sample authentication script at:
|
||||
# # http://wiki.nginx.org/ImapAuthenticateWithApachePhpScript
|
||||
#
|
||||
# # auth_http localhost/auth.php;
|
||||
# # pop3_capabilities "TOP" "USER";
|
||||
# # imap_capabilities "IMAP4rev1" "UIDPLUS";
|
||||
#
|
||||
# server {
|
||||
# listen localhost:110;
|
||||
# protocol pop3;
|
||||
# proxy on;
|
||||
# }
|
||||
#
|
||||
# server {
|
||||
# listen localhost:143;
|
||||
# protocol imap;
|
||||
# proxy on;
|
||||
# }
|
||||
#}
|
56
files/phantomjs_screenshot.js
Normal file
56
files/phantomjs_screenshot.js
Normal file
|
@ -0,0 +1,56 @@
|
|||
var page = require('webpage').create()
|
||||
var system = require('system')
|
||||
|
||||
var maxTimeout = 15000
|
||||
|
||||
if (system.args.length === 1) {
|
||||
console.log('required args: <siteURL> <outputFilePath>');
|
||||
phantom.exit(1)
|
||||
}
|
||||
|
||||
var address = system.args[1]
|
||||
var outputPath = system.args[2]
|
||||
|
||||
page.viewportSize = { width: 1280, height: 960 }
|
||||
page.clipRect = { top: 0, left: 0, width: 1280, height: 960}
|
||||
|
||||
/*
|
||||
In development, not working yet.
|
||||
|
||||
page.settings.scriptTimeout = 1000
|
||||
|
||||
page.onLongRunningScript = function() {
|
||||
page.stopJavaScript()
|
||||
phantom.exit(4)
|
||||
}
|
||||
*/
|
||||
|
||||
var t = Date.now()
|
||||
|
||||
console.log('Loading ' + address)
|
||||
|
||||
setTimeout(function() {
|
||||
console.log('timeout')
|
||||
phantom.exit(62)
|
||||
}, maxTimeout)
|
||||
|
||||
page.settings.resourceTimeout = maxTimeout
|
||||
|
||||
page.onResourceTimeout = function(e) {
|
||||
console.log(e.errorCode)
|
||||
console.log(e.errorString)
|
||||
console.log(e.url)
|
||||
phantom.exit(3)
|
||||
}
|
||||
|
||||
page.open(address, function(status) {
|
||||
if(status !== 'success') {
|
||||
console.log('failed')
|
||||
phantom.exit(2)
|
||||
}
|
||||
|
||||
page.render(outputPath)
|
||||
console.log('Loading time ' + (Date.now() - t) + ' msec');
|
||||
phantom.exit()
|
||||
})
|
||||
|
24
files/root_site_uris.txt
Normal file
24
files/root_site_uris.txt
Normal file
|
@ -0,0 +1,24 @@
|
|||
1,weekly,https://neocities.org
|
||||
0.9,daily,https://neocities.org/browse
|
||||
0.9,monthly,https://neocities.org/signin
|
||||
0.9,hourly,https://neocities.org/activity
|
||||
0.9,monthly,https://neocities.org/tutorials
|
||||
0.9,monthly,https://neocities.org/supporter
|
||||
0.9,monthly,https://neocities.org/cli
|
||||
0.9,monthly,https://neocities.org/about
|
||||
0.9,monthly,https://neocities.org/donate
|
||||
0.9,monthly,https://neocities.org/api
|
||||
0.9,monthly,https://neocities.org/press
|
||||
0.8,monthly,https://neocities.org/terms
|
||||
0.8,weekly,https://blog.neocities.org
|
||||
0.7,monthly,https://neocities.org/contact
|
||||
0.6,daily,https://neocities.org/browse?sort_by=special_sauce&tag=
|
||||
0.6,hourly,https://neocities.org/browse?sort_by=last_updated&tag=
|
||||
0.6,daily,https://neocities.org/browse?sort_by=supporters&tag=
|
||||
0.6,weekly,https://neocities.org/browse?sort_by=featured&tag=
|
||||
0.6,daily,https://neocities.org/browse?sort_by=tipping_enabled&tag=
|
||||
0.6,weekly,https://neocities.org/browse?sort_by=views&tag=
|
||||
0.6,weekly,https://neocities.org/browse?sort_by=hits&tag=
|
||||
0.6,hourly,https://neocities.org/browse?sort_by=newest&tag=
|
||||
0.6,monthly,https://neocities.org/browse?sort_by=oldest&tag=
|
||||
0.6,always,https://neocities.org/browse?sort_by=random&tag=
|
|
@ -1,70 +0,0 @@
|
|||
#
|
||||
# /etc/sysctl.conf - Configuration file for setting system variables
|
||||
# See /etc/sysctl.d/ for additional system variables
|
||||
# See sysctl.conf (5) for information.
|
||||
#
|
||||
|
||||
#kernel.domainname = example.com
|
||||
|
||||
# Uncomment the following to stop low-level messages on console
|
||||
#kernel.printk = 3 4 1 3
|
||||
|
||||
##############################################################3
|
||||
# Functions previously found in netbase
|
||||
#
|
||||
|
||||
# Uncomment the next two lines to enable Spoof protection (reverse-path filter)
|
||||
# Turn on Source Address Verification in all interfaces to
|
||||
# prevent some spoofing attacks
|
||||
#net.ipv4.conf.default.rp_filter=1
|
||||
#net.ipv4.conf.all.rp_filter=1
|
||||
|
||||
# Uncomment the next line to enable TCP/IP SYN cookies
|
||||
# See http://lwn.net/Articles/277146/
|
||||
# Note: This may impact IPv6 TCP sessions too
|
||||
#net.ipv4.tcp_syncookies=1
|
||||
|
||||
# Uncomment the next line to enable packet forwarding for IPv4
|
||||
#net.ipv4.ip_forward=1
|
||||
|
||||
# Uncomment the next line to enable packet forwarding for IPv6
|
||||
# Enabling this option disables Stateless Address Autoconfiguration
|
||||
# based on Router Advertisements for this host
|
||||
#net.ipv6.conf.all.forwarding=1
|
||||
|
||||
|
||||
###################################################################
|
||||
# Additional settings - these settings can improve the network
|
||||
# security of the host and prevent against some network attacks
|
||||
# including spoofing attacks and man in the middle attacks through
|
||||
# redirection. Some network environments, however, require that these
|
||||
# settings are disabled so review and enable them as needed.
|
||||
#
|
||||
# Do not accept ICMP redirects (prevent MITM attacks)
|
||||
#net.ipv4.conf.all.accept_redirects = 0
|
||||
#net.ipv6.conf.all.accept_redirects = 0
|
||||
# _or_
|
||||
# Accept ICMP redirects only for gateways listed in our default
|
||||
# gateway list (enabled by default)
|
||||
# net.ipv4.conf.all.secure_redirects = 1
|
||||
#
|
||||
# Do not send ICMP redirects (we are not a router)
|
||||
#net.ipv4.conf.all.send_redirects = 0
|
||||
#
|
||||
# Do not accept IP source route packets (we are not a router)
|
||||
#net.ipv4.conf.all.accept_source_route = 0
|
||||
#net.ipv6.conf.all.accept_source_route = 0
|
||||
#
|
||||
# Log Martian Packets
|
||||
#net.ipv4.conf.all.log_martians = 1
|
||||
#
|
||||
|
||||
net.ipv4.tcp_max_syn_backlog = 3240000
|
||||
net.core.somaxconn = 3240000
|
||||
net.ipv4.tcp_max_tw_buckets = 1440000
|
||||
net.core.rmem_default = 8388608
|
||||
net.core.rmem_max = 16777216
|
||||
net.core.wmem_max = 16777216
|
||||
net.ipv4.tcp_rmem = 4096 87380 16777216
|
||||
net.ipv4.tcp_wmem = 4096 65536 16777216
|
||||
net.ipv4.tcp_congestion_control = cubic
|
BIN
files/wireframes/newindex01x.png
Normal file
BIN
files/wireframes/newindex01x.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 837 KiB |
9
migrations/022_fix_changes.rb
Normal file
9
migrations/022_fix_changes.rb
Normal file
|
@ -0,0 +1,9 @@
|
|||
Sequel.migration do
|
||||
up {
|
||||
DB.rename_table :changes, :site_changes
|
||||
}
|
||||
|
||||
down {
|
||||
DB.rename_table :site_changes, :changes
|
||||
}
|
||||
end
|
19
migrations/023_add_profile_comments.rb
Normal file
19
migrations/023_add_profile_comments.rb
Normal file
|
@ -0,0 +1,19 @@
|
|||
Sequel.migration do
|
||||
up {
|
||||
DB.rename_column :events, :comment_id, :profile_comment_id
|
||||
|
||||
DB.create_table! :profile_comments do
|
||||
primary_key :id
|
||||
Integer :site_id
|
||||
Integer :actioning_site_id
|
||||
Text :message
|
||||
DateTime :created_at
|
||||
DateTime :updated_at
|
||||
end
|
||||
}
|
||||
|
||||
down {
|
||||
DB.rename_column :events, :profile_comment_id, :comment_id
|
||||
DB.drop_table :profile_comments
|
||||
}
|
||||
end
|
9
migrations/024_add_paranoid_site_delete.rb
Normal file
9
migrations/024_add_paranoid_site_delete.rb
Normal file
|
@ -0,0 +1,9 @@
|
|||
Sequel.migration do
|
||||
up {
|
||||
DB.add_column :sites, :is_deleted, :boolean, default: false
|
||||
}
|
||||
|
||||
down {
|
||||
DB.drop_column :sites, :is_deleted
|
||||
}
|
||||
end
|
9
migrations/025_add_event_paranoid_delete.rb
Normal file
9
migrations/025_add_event_paranoid_delete.rb
Normal file
|
@ -0,0 +1,9 @@
|
|||
Sequel.migration do
|
||||
up {
|
||||
DB.add_column :events, :is_deleted, :boolean, default: false
|
||||
}
|
||||
|
||||
down {
|
||||
DB.drop_column :events, :is_deleted
|
||||
}
|
||||
end
|
9
migrations/026_add_comment_paranoid_delete.rb
Normal file
9
migrations/026_add_comment_paranoid_delete.rb
Normal file
|
@ -0,0 +1,9 @@
|
|||
Sequel.migration do
|
||||
up {
|
||||
DB.add_column :comments, :is_deleted, :boolean, default: false
|
||||
}
|
||||
|
||||
down {
|
||||
DB.drop_column :comments, :is_deleted
|
||||
}
|
||||
end
|
15
migrations/027_add_comment_likes.rb
Normal file
15
migrations/027_add_comment_likes.rb
Normal file
|
@ -0,0 +1,15 @@
|
|||
Sequel.migration do
|
||||
up {
|
||||
DB.create_table! :comment_likes do
|
||||
primary_key :id
|
||||
Integer :comment_id
|
||||
Integer :site_id
|
||||
Integer :actioning_site_id
|
||||
DateTime :created_at
|
||||
end
|
||||
}
|
||||
|
||||
down {
|
||||
DB.drop_table :comment_likes
|
||||
}
|
||||
end
|
9
migrations/028_remove_comment_like_site_id.rb
Normal file
9
migrations/028_remove_comment_like_site_id.rb
Normal file
|
@ -0,0 +1,9 @@
|
|||
Sequel.migration do
|
||||
up {
|
||||
DB.drop_column :comment_likes, :site_id
|
||||
}
|
||||
|
||||
down {
|
||||
DB.add_column :comment_likes, :site_id, :integer
|
||||
}
|
||||
end
|
9
migrations/029_add_actioning_site_to_events.rb
Normal file
9
migrations/029_add_actioning_site_to_events.rb
Normal file
|
@ -0,0 +1,9 @@
|
|||
Sequel.migration do
|
||||
up {
|
||||
DB.add_column :events, :actioning_site_id, :integer
|
||||
}
|
||||
|
||||
down {
|
||||
DB.drop_column :events, :actioning_site_id
|
||||
}
|
||||
end
|
11
migrations/030_fix_event_timestamp.rb
Normal file
11
migrations/030_fix_event_timestamp.rb
Normal file
|
@ -0,0 +1,11 @@
|
|||
Sequel.migration do
|
||||
up {
|
||||
DB.drop_column :events, :created_at
|
||||
DB.add_column :events, :created_at, :timestamp, index: true
|
||||
}
|
||||
|
||||
down {
|
||||
DB.drop_column :events, :created_at
|
||||
DB.add_column :events, :created_at, :integer, index: true
|
||||
}
|
||||
end
|
9
migrations/031_fix_event_site_update_id.rb
Normal file
9
migrations/031_fix_event_site_update_id.rb
Normal file
|
@ -0,0 +1,9 @@
|
|||
Sequel.migration do
|
||||
up {
|
||||
DB.rename_column :events, :site_update_id, :site_change_id
|
||||
}
|
||||
|
||||
down {
|
||||
DB.rename_column :events, :site_change_id, :site_update_id
|
||||
}
|
||||
end
|
14
migrations/032_add_site_change_files.rb
Normal file
14
migrations/032_add_site_change_files.rb
Normal file
|
@ -0,0 +1,14 @@
|
|||
Sequel.migration do
|
||||
up {
|
||||
DB.create_table! :site_change_files do
|
||||
primary_key :id
|
||||
Integer :site_change_id
|
||||
String :filename
|
||||
DateTime :created_at
|
||||
end
|
||||
}
|
||||
|
||||
down {
|
||||
DB.drop_table :site_change_files
|
||||
}
|
||||
end
|
9
migrations/033_add_site_id_to_site_change_files.rb
Normal file
9
migrations/033_add_site_id_to_site_change_files.rb
Normal file
|
@ -0,0 +1,9 @@
|
|||
Sequel.migration do
|
||||
up {
|
||||
DB.add_column :site_change_files, :site_id, :integer
|
||||
}
|
||||
|
||||
down {
|
||||
DB.drop_column :site_change_files, :site_id
|
||||
}
|
||||
end
|
18
migrations/034_add_reports.rb
Normal file
18
migrations/034_add_reports.rb
Normal file
|
@ -0,0 +1,18 @@
|
|||
Sequel.migration do
|
||||
up {
|
||||
DB.create_table! :reports do
|
||||
primary_key :id
|
||||
Integer :site_id
|
||||
Integer :reporting_site_id
|
||||
String :type
|
||||
Text :comments
|
||||
Text :action_taken
|
||||
String :ip
|
||||
DateTime :created_at
|
||||
end
|
||||
}
|
||||
|
||||
down {
|
||||
DB.drop_table :reports
|
||||
}
|
||||
end
|
10
migrations/035_add_missing_indexes.rb
Normal file
10
migrations/035_add_missing_indexes.rb
Normal file
|
@ -0,0 +1,10 @@
|
|||
Sequel.migration do
|
||||
change do
|
||||
#alter_table(:events) { add_index :created_at }
|
||||
alter_table(:sites) { add_index :updated_at }
|
||||
alter_table(:comment_likes) { add_index :comment_id }
|
||||
alter_table(:comment_likes) { add_index :actioning_site_id }
|
||||
alter_table(:sites_tags) { add_index :tag_id }
|
||||
alter_table(:tags) { add_index :name }
|
||||
end
|
||||
end
|
9
migrations/036_add_editor_theme.rb
Normal file
9
migrations/036_add_editor_theme.rb
Normal file
|
@ -0,0 +1,9 @@
|
|||
Sequel.migration do
|
||||
up {
|
||||
DB.add_column :sites, :editor_theme, :text
|
||||
}
|
||||
|
||||
down {
|
||||
DB.drop_column :sites, :editor_theme
|
||||
}
|
||||
end
|
11
migrations/037_email_confirmation.rb
Normal file
11
migrations/037_email_confirmation.rb
Normal file
|
@ -0,0 +1,11 @@
|
|||
Sequel.migration do
|
||||
up {
|
||||
DB.add_column :sites, :email_confirmation_token, :text
|
||||
DB.add_column :sites, :email_confirmed, :boolean, default: false
|
||||
}
|
||||
|
||||
down {
|
||||
DB.drop_column :sites, :email_confirmation_token
|
||||
DB.drop_column :sites, :email_confirmed
|
||||
}
|
||||
end
|
12
migrations/038_blocked_ips.rb
Normal file
12
migrations/038_blocked_ips.rb
Normal file
|
@ -0,0 +1,12 @@
|
|||
Sequel.migration do
|
||||
up {
|
||||
DB.create_table! :blocked_ips do
|
||||
String :ip, primary_key: true
|
||||
DateTime :created_at
|
||||
end
|
||||
}
|
||||
|
||||
down {
|
||||
DB.drop_table :blocked_ips
|
||||
}
|
||||
end
|
9
migrations/039_privacy.rb
Normal file
9
migrations/039_privacy.rb
Normal file
|
@ -0,0 +1,9 @@
|
|||
Sequel.migration do
|
||||
up {
|
||||
DB.add_column :sites, :profile_comments_enabled, :boolean, default: true
|
||||
}
|
||||
|
||||
down {
|
||||
DB.drop_column :sites, :profile_comments_enabled
|
||||
}
|
||||
end
|
12
migrations/040_add_ssl.rb
Normal file
12
migrations/040_add_ssl.rb
Normal file
|
@ -0,0 +1,12 @@
|
|||
Sequel.migration do
|
||||
up {
|
||||
DB.add_column :sites, :ssl_key, :text
|
||||
DB.add_column :sites, :ssl_cert, :text
|
||||
|
||||
}
|
||||
|
||||
down {
|
||||
DB.drop_column :sites, :ssl_key
|
||||
DB.drop_column :sites, :ssl_cert
|
||||
}
|
||||
end
|
9
migrations/041_add_intermediate_ssl.rb
Normal file
9
migrations/041_add_intermediate_ssl.rb
Normal file
|
@ -0,0 +1,9 @@
|
|||
Sequel.migration do
|
||||
up {
|
||||
DB.add_column :sites, :ssl_cert_intermediate, :text
|
||||
}
|
||||
|
||||
down {
|
||||
DB.drop_column :sites, :ssl_cert_intermediate
|
||||
}
|
||||
end
|
9
migrations/042_pull_intermediate_ssl.rb
Normal file
9
migrations/042_pull_intermediate_ssl.rb
Normal file
|
@ -0,0 +1,9 @@
|
|||
Sequel.migration do
|
||||
up {
|
||||
DB.drop_column :sites, :ssl_cert_intermediate
|
||||
}
|
||||
|
||||
down {
|
||||
DB.add_column :sites, :ssl_cert_intermediate, :text
|
||||
}
|
||||
end
|
9
migrations/043_add_commenting_allowed_flag.rb
Normal file
9
migrations/043_add_commenting_allowed_flag.rb
Normal file
|
@ -0,0 +1,9 @@
|
|||
Sequel.migration do
|
||||
up {
|
||||
DB.add_column :sites, :commenting_allowed, :boolean, default: false
|
||||
}
|
||||
|
||||
down {
|
||||
DB.drop_column :sites, :commenting_allowed
|
||||
}
|
||||
end
|
9
migrations/044_add_plan_type.rb
Normal file
9
migrations/044_add_plan_type.rb
Normal file
|
@ -0,0 +1,9 @@
|
|||
Sequel.migration do
|
||||
up {
|
||||
DB.add_column :sites, :plan_type, :text
|
||||
}
|
||||
|
||||
down {
|
||||
DB.drop_column :sites, :plan_type
|
||||
}
|
||||
end
|
15
migrations/045_remove_servers.rb
Normal file
15
migrations/045_remove_servers.rb
Normal file
|
@ -0,0 +1,15 @@
|
|||
Sequel.migration do
|
||||
up {
|
||||
DB.drop_table :servers
|
||||
}
|
||||
|
||||
down {
|
||||
DB.create_table! :servers do
|
||||
primary_key :id
|
||||
String :ip
|
||||
Integer :slots_available
|
||||
DateTime :created_at
|
||||
DateTime :updated_at
|
||||
end
|
||||
}
|
||||
end
|
9
migrations/046_add_site_parent_id.rb
Normal file
9
migrations/046_add_site_parent_id.rb
Normal file
|
@ -0,0 +1,9 @@
|
|||
Sequel.migration do
|
||||
up {
|
||||
DB.add_column :sites, :parent_site_id, :integer, index: true
|
||||
}
|
||||
|
||||
down {
|
||||
DB.drop_column :sites, :parent_site_id
|
||||
}
|
||||
end
|
9
migrations/047_add_nsfw_to_tags.rb
Normal file
9
migrations/047_add_nsfw_to_tags.rb
Normal file
|
@ -0,0 +1,9 @@
|
|||
Sequel.migration do
|
||||
up {
|
||||
DB.add_column :tags, :is_nsfw, :boolean, default: false, index: true
|
||||
}
|
||||
|
||||
down {
|
||||
DB.drop_column :tags, :is_nsfw
|
||||
}
|
||||
end
|
9
migrations/048_add_subscription_id_to_sites.rb
Normal file
9
migrations/048_add_subscription_id_to_sites.rb
Normal file
|
@ -0,0 +1,9 @@
|
|||
Sequel.migration do
|
||||
up {
|
||||
DB.add_column :sites, :stripe_subscription_id, :text
|
||||
}
|
||||
|
||||
down {
|
||||
DB.drop_column :sites, :stripe_subscription_id
|
||||
}
|
||||
end
|
9
migrations/049_add_space_used_to_sites.rb
Normal file
9
migrations/049_add_space_used_to_sites.rb
Normal file
|
@ -0,0 +1,9 @@
|
|||
Sequel.migration do
|
||||
up {
|
||||
DB.add_column :sites, :space_used, :bigint, default: 0, index: true
|
||||
}
|
||||
|
||||
down {
|
||||
DB.drop_column :sites, :space_used
|
||||
}
|
||||
end
|
17
migrations/050_add_site_files.rb
Normal file
17
migrations/050_add_site_files.rb
Normal file
|
@ -0,0 +1,17 @@
|
|||
Sequel.migration do
|
||||
up {
|
||||
DB.create_table! :site_files do
|
||||
Integer :site_id, index: true
|
||||
String :path
|
||||
Bigint :size
|
||||
String :sha1_hash
|
||||
Boolean :is_directory, default: false
|
||||
DateTime :created_at
|
||||
DateTime :updated_at
|
||||
end
|
||||
}
|
||||
|
||||
down {
|
||||
DB.drop_table :site_files
|
||||
}
|
||||
end
|
9
migrations/051_add_site_updated_at.rb
Normal file
9
migrations/051_add_site_updated_at.rb
Normal file
|
@ -0,0 +1,9 @@
|
|||
Sequel.migration do
|
||||
up {
|
||||
DB.add_column :sites, :site_updated_at, DateTime, index: true
|
||||
}
|
||||
|
||||
down {
|
||||
DB.drop_column :sites, :site_updated_at
|
||||
}
|
||||
end
|
30
migrations/052_site_files_composite_keys.rb
Normal file
30
migrations/052_site_files_composite_keys.rb
Normal file
|
@ -0,0 +1,30 @@
|
|||
Sequel.migration do
|
||||
up {
|
||||
DB.drop_table :site_files
|
||||
|
||||
DB.create_table! :site_files do
|
||||
Integer :site_id
|
||||
String :path
|
||||
Bigint :size
|
||||
String :sha1_hash
|
||||
Boolean :is_directory, default: false
|
||||
DateTime :created_at
|
||||
DateTime :updated_at
|
||||
primary_key [:site_id, :path], :name => :site_files_pk
|
||||
end
|
||||
}
|
||||
|
||||
down {
|
||||
DB.drop_table :site_files
|
||||
|
||||
DB.create_table! :site_files do
|
||||
Integer :site_id, index: true
|
||||
String :path
|
||||
Bigint :size
|
||||
String :sha1_hash
|
||||
Boolean :is_directory, default: false
|
||||
DateTime :created_at
|
||||
DateTime :updated_at
|
||||
end
|
||||
}
|
||||
end
|
13
migrations/053_add_email_controls.rb
Normal file
13
migrations/053_add_email_controls.rb
Normal file
|
@ -0,0 +1,13 @@
|
|||
Sequel.migration do
|
||||
up {
|
||||
DB.add_column :sites, :send_emails, :boolean, default: true
|
||||
DB.add_column :sites, :send_comment_emails, :boolean, default: true
|
||||
DB.add_column :sites, :send_follow_emails, :boolean, default: true
|
||||
}
|
||||
|
||||
down {
|
||||
DB.drop_column :sites, :send_emails
|
||||
DB.drop_column :sites, :send_comment_emails
|
||||
DB.drop_column :sites, :send_follow_emails
|
||||
}
|
||||
end
|
9
migrations/054_add_deleted_reason.rb
Normal file
9
migrations/054_add_deleted_reason.rb
Normal file
|
@ -0,0 +1,9 @@
|
|||
Sequel.migration do
|
||||
up {
|
||||
DB.add_column :sites, :deleted_reason, :text
|
||||
}
|
||||
|
||||
down {
|
||||
DB.drop_column :sites, :deleted_reason
|
||||
}
|
||||
end
|
9
migrations/055_add_featured_sites.rb
Normal file
9
migrations/055_add_featured_sites.rb
Normal file
|
@ -0,0 +1,9 @@
|
|||
Sequel.migration do
|
||||
up {
|
||||
DB.add_column :sites, :featured_at, DateTime, index: true
|
||||
}
|
||||
|
||||
down {
|
||||
DB.drop_column :sites, :featured_at
|
||||
}
|
||||
end
|
9
migrations/056_add_api_calls.rb
Normal file
9
migrations/056_add_api_calls.rb
Normal file
|
@ -0,0 +1,9 @@
|
|||
Sequel.migration do
|
||||
up {
|
||||
DB.add_column :sites, :api_calls, Integer, default: 0, index: true
|
||||
}
|
||||
|
||||
down {
|
||||
DB.drop_column :sites, :api_calls
|
||||
}
|
||||
end
|
11
migrations/057_add_paypal_data.rb
Normal file
11
migrations/057_add_paypal_data.rb
Normal file
|
@ -0,0 +1,11 @@
|
|||
Sequel.migration do
|
||||
up {
|
||||
DB.add_column :sites, :paypal_profile_id, String
|
||||
DB.add_column :sites, :paypal_token, String
|
||||
}
|
||||
|
||||
down {
|
||||
DB.drop_column :sites, :paypal_profile_id
|
||||
DB.drop_column :sites, :paypal_token
|
||||
}
|
||||
end
|
9
migrations/058_add_paypal_status.rb
Normal file
9
migrations/058_add_paypal_status.rb
Normal file
|
@ -0,0 +1,9 @@
|
|||
Sequel.migration do
|
||||
up {
|
||||
DB.add_column :sites, :paypal_active, :boolean, default: false
|
||||
}
|
||||
|
||||
down {
|
||||
DB.drop_column :sites, :paypal_active
|
||||
}
|
||||
end
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Reference in a new issue