From 90f058a4665200672ef07ee9e04a5d410fc0ed7a Mon Sep 17 00:00:00 2001 From: Sami Samhuri Date: Sat, 8 Mar 2014 16:49:30 -0800 Subject: [PATCH] update sublime config --- .../Package Control.sublime-package | Bin 0 -> 132375 bytes .../Default/Preferences.sublime-settings | 11 + .../Package Control/Default.sublime-commands | 6 +- .../Package Control/Package Control.ca-bundle | 43 + .../Package Control/Package Control.ca-list | 4 + .../Package Control/Package Control.py | 4911 +---------------- .../Package Control.sublime-settings | 304 +- .../Package Control/example-channel.json | 64 + .../Package Control/example-repository.json | 275 + .../Packages/Package Control/messages.json | 5 +- .../Package Control/messages/2.0.0.txt | 64 + .../Package Control/package-metadata.json | 6 +- .../package_control/__init__.py | 2 + .../package_control/automatic_upgrader.py | 215 + .../package_control/ca_certs.py | 378 ++ .../Package Control/package_control/cache.py | 168 + .../package_control/clear_directory.py | 37 + .../package_control/clients/__init__.py | 0 .../clients/bitbucket_client.py | 249 + .../clients/client_exception.py | 5 + .../package_control/clients/github_client.py | 284 + .../clients/json_api_client.py | 64 + .../package_control/clients/readme_client.py | 83 + .../Package Control/package_control/cmd.py | 167 + .../package_control/commands/__init__.py | 39 + .../commands/add_channel_command.py | 46 + .../commands/add_repository_command.py | 46 + .../commands/create_binary_package_command.py | 35 + .../commands/create_package_command.py | 32 + .../commands/disable_package_command.py | 48 + .../commands/discover_packages_command.py | 11 + .../commands/enable_package_command.py | 40 + .../commands/existing_packages_command.py | 69 + .../commands/grab_certs_command.py | 109 + .../commands/install_package_command.py | 50 + .../commands/list_packages_command.py | 63 + .../commands/package_message_command.py | 11 + .../commands/remove_package_command.py | 88 + .../commands/upgrade_all_packages_command.py | 77 + .../commands/upgrade_package_command.py | 81 + .../package_control/console_write.py | 20 + .../package_control/download_manager.py | 231 + .../package_control/downloaders/__init__.py | 11 + .../downloaders/background_downloader.py | 62 + .../downloaders/binary_not_found_error.py | 4 + .../downloaders/caching_downloader.py | 185 + .../downloaders/cert_provider.py | 203 + .../downloaders/cli_downloader.py | 81 + .../downloaders/curl_downloader.py | 267 + .../downloaders/decoding_downloader.py | 24 + .../downloaders/downloader_exception.py | 5 + .../package_control/downloaders/http_error.py | 9 + .../downloaders/limiting_downloader.py | 36 + .../downloaders/no_ca_cert_exception.py | 11 + .../downloaders/non_clean_exit_error.py | 13 + .../downloaders/non_http_error.py | 5 + .../downloaders/rate_limit_exception.py | 13 + .../downloaders/urllib_downloader.py | 291 + .../downloaders/wget_downloader.py | 347 ++ .../downloaders/wininet_downloader.py | 652 +++ .../package_control/file_not_found_error.py | 4 + .../package_control/http/__init__.py | 65 + .../http/debuggable_http_connection.py | 72 + .../http/debuggable_http_handler.py | 35 + .../http/debuggable_http_response.py | 66 + .../http/debuggable_https_response.py | 9 + .../http/invalid_certificate_exception.py | 25 + .../http/persistent_handler.py | 116 + .../http/validating_https_connection.py | 345 ++ .../http/validating_https_handler.py | 59 + .../package_control/http_cache.py | 75 + .../package_control/open_compat.py | 27 + .../package_control/package_cleanup.py | 107 + .../package_control/package_creator.py | 39 + .../package_control/package_installer.py | 247 + .../package_control/package_io.py | 126 + .../package_control/package_manager.py | 1026 ++++ .../package_control/package_renamer.py | 117 + .../package_control/preferences_filename.py | 11 + .../package_control/providers/__init__.py | 12 + .../bitbucket_repository_provider.py | 163 + .../providers/channel_provider.py | 312 ++ .../providers/github_repository_provider.py | 169 + .../providers/github_user_provider.py | 172 + .../providers/provider_exception.py | 5 + .../providers/release_selector.py | 125 + .../providers/repository_provider.py | 441 ++ .../package_control/reloader.py | 130 + .../Package Control/package_control/semver.py | 833 +++ .../package_control/show_error.py | 12 + .../package_control/sys_path.py | 27 + .../package_control/thread_progress.py | 46 + .../package_control/unicode.py | 49 + .../package_control/upgraders/__init__.py | 0 .../package_control/upgraders/git_upgrader.py | 106 + .../package_control/upgraders/hg_upgrader.py | 74 + .../package_control/upgraders/vcs_upgrader.py | 27 + .../package_control/versions.py | 81 + .../Packages/Package Control/readme.creole | 37 +- .../Theme - Default/arrow_right@2x.png | Bin 629 -> 1639 bytes .../Packages/User/Package Control.last-run | 2 +- .../User/Preferences.sublime-settings | 2 +- sublime/Packages/Vintage/vintage.py | 17 +- .../Pristine Packages/Default.sublime-package | Bin 266900 -> 267428 bytes .../Package Control.sublime-package | Bin 0 -> 132375 bytes .../Theme - Default.sublime-package | Bin 132752 -> 133762 bytes .../Pristine Packages/Vintage.sublime-package | Bin 108686 -> 108836 bytes 107 files changed, 11105 insertions(+), 4968 deletions(-) create mode 100644 sublime/Installed Packages/Package Control.sublime-package create mode 100644 sublime/Packages/Package Control/Package Control.ca-bundle create mode 100644 sublime/Packages/Package Control/Package Control.ca-list create mode 100644 sublime/Packages/Package Control/example-channel.json create mode 100644 sublime/Packages/Package Control/example-repository.json create mode 100644 sublime/Packages/Package Control/messages/2.0.0.txt create mode 100644 sublime/Packages/Package Control/package_control/__init__.py create mode 100644 sublime/Packages/Package Control/package_control/automatic_upgrader.py create mode 100644 sublime/Packages/Package Control/package_control/ca_certs.py create mode 100644 sublime/Packages/Package Control/package_control/cache.py create mode 100644 sublime/Packages/Package Control/package_control/clear_directory.py create mode 100644 sublime/Packages/Package Control/package_control/clients/__init__.py create mode 100644 sublime/Packages/Package Control/package_control/clients/bitbucket_client.py create mode 100644 sublime/Packages/Package Control/package_control/clients/client_exception.py create mode 100644 sublime/Packages/Package Control/package_control/clients/github_client.py create mode 100644 sublime/Packages/Package Control/package_control/clients/json_api_client.py create mode 100644 sublime/Packages/Package Control/package_control/clients/readme_client.py create mode 100644 sublime/Packages/Package Control/package_control/cmd.py create mode 100644 sublime/Packages/Package Control/package_control/commands/__init__.py create mode 100644 sublime/Packages/Package Control/package_control/commands/add_channel_command.py create mode 100644 sublime/Packages/Package Control/package_control/commands/add_repository_command.py create mode 100644 sublime/Packages/Package Control/package_control/commands/create_binary_package_command.py create mode 100644 sublime/Packages/Package Control/package_control/commands/create_package_command.py create mode 100644 sublime/Packages/Package Control/package_control/commands/disable_package_command.py create mode 100644 sublime/Packages/Package Control/package_control/commands/discover_packages_command.py create mode 100644 sublime/Packages/Package Control/package_control/commands/enable_package_command.py create mode 100644 sublime/Packages/Package Control/package_control/commands/existing_packages_command.py create mode 100644 sublime/Packages/Package Control/package_control/commands/grab_certs_command.py create mode 100644 sublime/Packages/Package Control/package_control/commands/install_package_command.py create mode 100644 sublime/Packages/Package Control/package_control/commands/list_packages_command.py create mode 100644 sublime/Packages/Package Control/package_control/commands/package_message_command.py create mode 100644 sublime/Packages/Package Control/package_control/commands/remove_package_command.py create mode 100644 sublime/Packages/Package Control/package_control/commands/upgrade_all_packages_command.py create mode 100644 sublime/Packages/Package Control/package_control/commands/upgrade_package_command.py create mode 100644 sublime/Packages/Package Control/package_control/console_write.py create mode 100644 sublime/Packages/Package Control/package_control/download_manager.py create mode 100644 sublime/Packages/Package Control/package_control/downloaders/__init__.py create mode 100644 sublime/Packages/Package Control/package_control/downloaders/background_downloader.py create mode 100644 sublime/Packages/Package Control/package_control/downloaders/binary_not_found_error.py create mode 100644 sublime/Packages/Package Control/package_control/downloaders/caching_downloader.py create mode 100644 sublime/Packages/Package Control/package_control/downloaders/cert_provider.py create mode 100644 sublime/Packages/Package Control/package_control/downloaders/cli_downloader.py create mode 100644 sublime/Packages/Package Control/package_control/downloaders/curl_downloader.py create mode 100644 sublime/Packages/Package Control/package_control/downloaders/decoding_downloader.py create mode 100644 sublime/Packages/Package Control/package_control/downloaders/downloader_exception.py create mode 100644 sublime/Packages/Package Control/package_control/downloaders/http_error.py create mode 100644 sublime/Packages/Package Control/package_control/downloaders/limiting_downloader.py create mode 100644 sublime/Packages/Package Control/package_control/downloaders/no_ca_cert_exception.py create mode 100644 sublime/Packages/Package Control/package_control/downloaders/non_clean_exit_error.py create mode 100644 sublime/Packages/Package Control/package_control/downloaders/non_http_error.py create mode 100644 sublime/Packages/Package Control/package_control/downloaders/rate_limit_exception.py create mode 100644 sublime/Packages/Package Control/package_control/downloaders/urllib_downloader.py create mode 100644 sublime/Packages/Package Control/package_control/downloaders/wget_downloader.py create mode 100644 sublime/Packages/Package Control/package_control/downloaders/wininet_downloader.py create mode 100644 sublime/Packages/Package Control/package_control/file_not_found_error.py create mode 100644 sublime/Packages/Package Control/package_control/http/__init__.py create mode 100644 sublime/Packages/Package Control/package_control/http/debuggable_http_connection.py create mode 100644 sublime/Packages/Package Control/package_control/http/debuggable_http_handler.py create mode 100644 sublime/Packages/Package Control/package_control/http/debuggable_http_response.py create mode 100644 sublime/Packages/Package Control/package_control/http/debuggable_https_response.py create mode 100644 sublime/Packages/Package Control/package_control/http/invalid_certificate_exception.py create mode 100644 sublime/Packages/Package Control/package_control/http/persistent_handler.py create mode 100644 sublime/Packages/Package Control/package_control/http/validating_https_connection.py create mode 100644 sublime/Packages/Package Control/package_control/http/validating_https_handler.py create mode 100644 sublime/Packages/Package Control/package_control/http_cache.py create mode 100644 sublime/Packages/Package Control/package_control/open_compat.py create mode 100644 sublime/Packages/Package Control/package_control/package_cleanup.py create mode 100644 sublime/Packages/Package Control/package_control/package_creator.py create mode 100644 sublime/Packages/Package Control/package_control/package_installer.py create mode 100644 sublime/Packages/Package Control/package_control/package_io.py create mode 100644 sublime/Packages/Package Control/package_control/package_manager.py create mode 100644 sublime/Packages/Package Control/package_control/package_renamer.py create mode 100644 sublime/Packages/Package Control/package_control/preferences_filename.py create mode 100644 sublime/Packages/Package Control/package_control/providers/__init__.py create mode 100644 sublime/Packages/Package Control/package_control/providers/bitbucket_repository_provider.py create mode 100644 sublime/Packages/Package Control/package_control/providers/channel_provider.py create mode 100644 sublime/Packages/Package Control/package_control/providers/github_repository_provider.py create mode 100644 sublime/Packages/Package Control/package_control/providers/github_user_provider.py create mode 100644 sublime/Packages/Package Control/package_control/providers/provider_exception.py create mode 100644 sublime/Packages/Package Control/package_control/providers/release_selector.py create mode 100644 sublime/Packages/Package Control/package_control/providers/repository_provider.py create mode 100644 sublime/Packages/Package Control/package_control/reloader.py create mode 100644 sublime/Packages/Package Control/package_control/semver.py create mode 100644 sublime/Packages/Package Control/package_control/show_error.py create mode 100644 sublime/Packages/Package Control/package_control/sys_path.py create mode 100644 sublime/Packages/Package Control/package_control/thread_progress.py create mode 100644 sublime/Packages/Package Control/package_control/unicode.py create mode 100644 sublime/Packages/Package Control/package_control/upgraders/__init__.py create mode 100644 sublime/Packages/Package Control/package_control/upgraders/git_upgrader.py create mode 100644 sublime/Packages/Package Control/package_control/upgraders/hg_upgrader.py create mode 100644 sublime/Packages/Package Control/package_control/upgraders/vcs_upgrader.py create mode 100644 sublime/Packages/Package Control/package_control/versions.py create mode 100644 sublime/Pristine Packages/Package Control.sublime-package diff --git a/sublime/Installed Packages/Package Control.sublime-package b/sublime/Installed Packages/Package Control.sublime-package new file mode 100644 index 0000000000000000000000000000000000000000..cc9aa190a01743ba9248f6cb412b73fe24b5dfb0 GIT binary patch literal 132375 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

MQ9p)fe^)C#xmVp;@WaD-H+b{7u8vpB8GTX>jk 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

jyNZi83%nHtxp4(c_{CuQl9pYTIdxuulz z(9O+@8y<4GWp(wbb3ECiYxmhwJ-KKmk>z!_AtYTA=F z6^eLkhISe~ya8Ri#xGv_9;UTqeMgN`vY4MbiWLXZ*P$>u4<^Yb7u%}x@@#LO7Kr2d z!W7st^ZnW1Z@(nCDS;k5>Y!<-#j=tzBUVN89{UC)nYH6*!6v*8sF1^R3&vQ#1+lyy zgeN>1S2Exo<~MoH+|^Oy;&hl+1s`!f-wv&p7F(zjJ`5m=sjrMUNBb=qP1=FaBbMfA7O#YQ&(Ro|EWmv-emz(SCEglck+ZMbdZ7GB%VKzj zc)~uYG+pQhdOjRuy&Px%KHLF;m5+{&o_}LrKq}X=6Nv>3k=Mzs{25>|-Z-=?3X}Oo z%VvOOp&?@t6QY@-m)ENZgV#1TA<|%(v-Nc2EY^V^qK|hZ1#A29P6i1-C2+F1_jw##4MUUNsYe z0C(Y8(xtRz2f0o&kO$~o1m!Buc|5p8jo-SPDeK8SgFiJDGwLzKLy6M`(CSj(z|};A6!?I0bwdycDRE8PiDQ z1n1`CQZ%tKO9D;C$tijHQRIi*oBMAV`Ap1I{0%gRZUp^}HpC;7a=A6pW9DM06v4`$ zVPo^Xl0G9O8u@C7-apDSK^`SF;{`y&81iPwErH-hOEFxsO=hB0bQ6;wwpAQ4Vbt>I zf@#bGNk{__Y}9=oWD-FLQs{^jB!6r@iN`R)tP3b+CB<-dlBw$yLrRh>9Ooa}Sj|Gm zsL!)y&F;l`Cb&55a@w1?M4x`Vu@i=r3Fq}glp&aqJ7pC;YQBCki+bWtJC(kabn4pH zg57i+o6pkwti}Gh9KX^HCgJg!S&ZyJ2>vGI8aGvzvM^3#H3B8}s1fqIy&+88Eh${j zpb%#j&Y&E{H3fsZU*nb>SF(M=zYMLEfR!!|p0)Vjr9~#V!5CWT?GaanNVxsdu?hrC zW}J+U4+QXOYTdmL!Nh~?6w`~CmFnm6%vYq9)RRz)xM3L-Lj65Zof6DMSt2m9BsZDC zsICw$HJ>Mv72$KK*Dm^RXIk-to84fIL^<|-@TcV`M%JigW>n0g4is}A-F`==Exzlq zyfnD|421(UhG(-PVcq>&Dge~=Km2Lsh#SF!@o<^RU-L2e*hK-s@W%uU^a6R{rw~p- z5aSGe-T~fXysQH9omCE>88hyVk0Y3}tJ1BgBsM61;22Yd*#&*BBAkdCKD;R{`PR85y39BF8o3dCL4Y98o;iFAvZIOTu|R(Xto!dm}C(0-GCv^%|r{C>jnlRRlPpWO->^=D#08;(VM5=If&&B zxPCE$(|W{~@{`*{BzYwR9e?eIG`ecmxQ} z5tRO%Z+diVo|B_k39s7lLYyDVx+rd*BWoJJ_oU8yYC&wvX&(Kp%!VVF%>3EGdFGz| z@;8E8V*w!i?Y3Y8q>fm2n=>~s4y5jwdS=Aj6#8$dHE1*sRU9RHgyDLssRN8$s2+c| z9QaNe!YQx%!TyqSH0k~O%FyU>gl6)X$Kk%=3DV287qu#;NxtU>euaGd2|>ZDLc(YE zMQP&9Ds|W>RX*z2Cj9wTOigOF+vjC`VBRG72w625H5a3Im2+bRx8k3sa#FR>vsyVc$8K*o+uD66AP(s(w3ttpOtzl69qX?R zy{%;$>THBw0)l#_kZT6Bcb1Rony%H1jWF<+*?kgy{shv;^U^83ey4YT-*cHYH5-aj z3yU{W0X>=+2O6YeD^;J|zB-^lKmf_z$c;2^ov)Mz7;|&3{pgW7#+CS;FPw~p(g%8- zc~moecAzJuEYO0;c`?OJ{YGsA)S)CyvD}MhhE=;EB(0ijHmqlU;Nc&sig>HyN(ts| zBT5B!|KqxwF6@f>O~@SMLt4$LajT;)M(?^7{yPCUx!-{OpBU~q*k8blJ3cNYUi=oz zlI0>dA8c9$`PZF<%Xat7Zdo+b9Wr*jz}t&3-+!FmJY0p+j6NikyR2nNqY;c~VvBR< zuy2^F&IBWy&{cyKGV0G@Td0z#tv9{Y9se%D zXjyfpQL@h$^Ozo#Vm5oC-5g<`zDhPk%aY6%R8?HoAMBvUlq%RW_#o;eCs7!up z{^-o{SGPF*iv(YRw5><#u7$euCD0|H$H*s0!9%=U;VOmH_o^g@d!)d$-v{5d2}kA2 z&H9v=%0EE1#kYW>E4tI6=esdt?$+T6xczY{*7NO0XEyO6NBrj& zXgj%t_x?SaAF&+0FosTf&}6n{bDq^bFY1i5F}4i)Wno~0Y#>a4&ld7Si4MFz+sC>r+g4a* zjM690*rfTRWWK1H`a1Eemf;*cuZP<#cY-h0>8_t?;9FD9#cg^YDV}SzABL<_lXrI2JQonr z69!msnRix?7pBk0(NU?biGR5pml(} zebm}jH2!g{gm*RV`4G4RZp!Y-k{23sza_a1oorfM4%KX&hXgp+Q<)y2-u9)2S8mY2h+7NQ z--W}s@G5aS@kX|YhLjK9$_vKvCXlrZ)ePuGB|-(43r5ouQq~h+W5Ium2aCHWac*zL z4WJSLK;e}7g9sLR!4r@1`0qPZj?7*~1gex|KlYpZlcs@i-tha6woZqP3!m3V>C#UZy1fwi7tM< z$2S;g%J@xF$f#4@BCLViZneekfOh|K=_@iiepmMT*E=RWMg=*{CFY`ioGd z+x#xn(r?>bLE4(aEp{iLo3zvgn0Rr{x@7aLFYHv8&KZhu2`=*CpAJfIJ!9op`x%6=h(baMrKs{Pjib84~xBfz! zAq5i+wyBCbc%US;9d!Lq45n4d(zsmKPr^@-6a(13)@o{X90Jkwhj1hjmB)1q=)Gk& ztlvINF5ww}nZFG0AP}>6HLBQ4-%sthv%x!-BIeFTuo9ew-^WA=L@(<;NDkT+o+d2x zZieG%>D=+viSCJ)8YpH*V!s`TpG6=};DEkz8goFK@Ou!^4%tJUi5T6>|BBPaGRO0W zhGvPj6F#|g0%@KqJ*I*vCQ>KJb!K~0z2-5+LAhO6fX?@X~J(~ zHke5>nKZcovty84CirpDC#k`=Ys^XY z+KNU+?xdt0(5=K#jSf_5Dkm7meS`?nBVwKtuK+gOh>c}Km&ur`ln_W}$jXp!F9Y^P zI0L~tE;nm>*s1DLj=}L~jFdXW2|Nn$qPP<~39l>(7^IR> zlT+GWq}?|I4_vHBN%zHPGTo=48*NQWWuc&ILUh8=0kIb81KA8YAveItkXFf0@CM%kCDq5Skela>79Lsk7k6W5}+O;u9`Y3kCnt75p-$D8{R(^}) zLF)E_bpoP5T_ifj#%Mz7dn4WGA6r4FVT6*rO0rxLLo>-RCgA-|B(^%7>zXi5doI_6 zMU#+z;1lhQwLQ_k=Y$O}hfOUHsqtxrZn(w#wfu7s1}WQ$Shi!tPxOE8?%HXL^nX!6 zqkoiS|C?ga$k@=<=>I3^v5x-_#bC(I8|n&M6>1@P8{t!(xt|B0AQsY=+(w(GT#8Z* zxKnQqlVvBvo*>W1-K%qfYcz4lG(W2V{w#Q*Bw^ZyCt!cgZt+odqaAuJb33jXxc7a5 z@#8R5Q7Cg_OQr$oZRtP=A*I0taD3rofU~CotTC|t5QIGD88>yk165nf^(GcMs>1$+ zY*kyR3|V5^Yw+EBSMn5Wd=aM(tSpF~>9)>C#d=)1WSL>?0#tefK4> zL=I~GT`#vZBcoExd?j&?-*Z|lXQJ(=i{(4a{s-{{vAj9iUis<{$lkLLFR`t{cO5d{ ziS9D&zj)6AFM8RV*Wa(ww{jQ{BX$4zcPLOx&6xibjp(2CziC|mvyl9QLM}NYPED&~ zH%>P@IW8+DPUBE5CPPCpJ~g}O*T_`ehT_L3E<&dB-#t|DxNjDyM*Ck_;Qs=* z7Neui-G8KrQn&yBKls-FSoRE!9eyIb9c*39e_V(DWiq6u8N0-S;*+bpC(|q;5DK91 z-mp+109_`AlHc$S!gNnE6SF>qH#`ovSn=IGy1FJ~f`On^K_GBBwZ?P8xq8Faw0&PA zszqJ)wS@S7*1c<`-y5dCJ(og_<~*vIbV6zxz*@BC+(bTR1?AJk`Ib^waCn9G;*$Ha z2!*mg;Z-l%($+Q1T9k*wu2eVvGg=xEr?YRj|7$CzTZTWfTtK2t_W^4_=dm!*W58Vp zH5afS&EDsxAA@ozDBi+-G?n8$PIpII#nlXbEH&&}wbw?p{Wyi(%95i&Jpl&oUsR=v z+gn(dasTA0@Vx#8)ZeH@ySy`M77fZ@2()C|7q00oMxd1f0`i2TueATZfur;Qx4jpl z(pGvY0_pMj7c@a+J|m@#oXFK;DKugUB1`3ug=ujB(-YKEOIic!IpL*O-I|xzz5B$_ z93Wb7Rj8bpc4}-g2Ph~ue;5>>42V90Z%IoF0@cLW03_$mK3wZ@UzseADz*Wrf47i% zvcRq{LX>whvinFtIUm$d@a2F7H>{5$K(-vF*_&W{e^mU);+#HJ7=*Cw?<1TmmLNb0 zhB>_2OnHa#&=P^D7c&}SaKuOBM4wZW4?Xxyzsip%!-JlWu$qqU$5n!)#<&@8O1@lY z&Or-4^w3|z#-g-hbd8cmGCpEU#1bQ_ zLNiIAX`nK+IY?HegwgVrlazBRyVcCGi)<`IY-CG{el`6mRPC6|mz?<bqv!e>HD4dX0dOyOO%>>|FYY%MDK6CAYdxo zA~4=efD0i>1nsbznLwazq*rZp59k-kw3J0yCcZ}Ku{A1rYe;yi3!<%+7pku}#S$h9 zKP>|A1?gGDFmh0*Dklm&4n#BJC#D`IMA~D8u$Y2CA~oLn&X7n58^b@ZO%Kvoh(I2z zOl<3K-|3t{NG-=Ltaic?Osu%yRakYrJIOGs1T}1Cgk9HJ=y8{*dSoz2icPs7zMans zzf(Q_6#TbFVoMpkkL+at2ByUqYGZeodf#po$0B{JFhy7k^FU!q4E}gg z(xwv@VIQ|7T7que(v*nb6wVJe9oe&pb#lIsRVx;Z17}%+&i`4lG-xpa7wtCqtfUue9E@>ErI(-va;BN|eR~mQ3~1*1nzNXyN`(Fcxr_8xeU{QQk|sCJ z3K(VMBQ0Ng$CjU_T`1rf@yy+AjY=^Cl{WW}4zc=h8k1Irf}#EZqqNlv?uPOvJ% zjdWW1K!F_HV#NNKW%z8@99^#4%3s^2nIM+et9z*~ZCd>X8Ebn!tH##`v(k$>fYm_- zb#1_yL-SSP85hUymE8A(F3343?un1|Pd*w;ANo)8o)EgXl!SI56-CL{KgXLi2WP)% z&2Q=%HrV6_Ku$!NA2FS0>?B9VlOxJndX&j-GSnOrZL< zB-6*$j7IYZZF!~KSK4s`7v#5`kl5HnXbV++f9SWO+*X2in5oH}3BF40HXv!W8>@D- z6dHrX;BwMEDW~L2zO%0tt&_FxhT*O!ZqN}_ zoIJU3245K_-K7Xhf}P5(28j-Gx4Yw!xqu_R|17@v4CoJaKPI$*B>%fpp5f1c=ATEM zYD{z6O}1xGAJElx0R_WK@eXa2{-Qvp#TgXMBL2jJPO;xWH1qXjzl%}J+eGHSdv5OL zgva3`Q0GL*-mVj6&nm_w#Yr?#Bbf|6%fGZ&Y}?m1HIv>Vg7mW-<4vhGQp=5oE?v>C z#=G>mA|90ai@8@dGJrfX((u7ZW)>*MSGgw52PB}Ka*BJGBacaD0cA%hV%J)gM5?GC z{(cD0%-UbzUUm(_0xK$v7f@^Oz1cGSGRkm-e!zU@*1?GeJvt^zy?KZrpO};x^B8YK ziXKY(#o;J|=9Z_D;vv2kAS6H0k4UUrXT?x`1c8OrhPdJEpn-peCwD0G^iFgn&ZL_4 zhG*l(FGxS*(2vrL25ZUdMSHKV1JFr2&yYT|NS%z`Aaj)9McGK5&H7Akwops?ZP#x! zxwGMRfe-4YHyG-SLd<5zPD8qk#Yu`}m8Kd;Xq5h&0kl>hI*ixJD3c*WrgX1kGALh+`u{(+()Z{Knuk)Fm7(-N0vcM zRbv;Vb57y|V?eu3Sy=0QO#lSwDD|qyp$cYTI1;stP%`jih%ptXA8_)Bz&-4^zskCq z?~r!WZ>>Ts(dK-8M%p!7wyecrmOu>6v?vg!{zSZV_VYl2fEjx}mY{zb-L3z*FyU^e zBDz%ai?P-ifbK9DXC735fV}0Ve&Zk0uLo-`K26$MYIpnzYJ{oaV-LhQUdVKLuyLC$ zy4mw7dI>ZSl?MPDwnv*xzcU>oki-`lX8pzQCp5GyS_|C?Z*r7Fg~U{N1of$O@iPtc zqXxVVLd940^A|NXEP=+Lhh*|Rb(o2I0#M@H(6?4Avr0pgOyRfjHCh)D_JhsPar@cN za~0a!-XL_62c(F~{xl(S&46o*^c-DxN+PUl3nFQbJIQbu9!)XZ z#6&PciC`q)wP7tYsO_0vljBLfjnaY*H2en5;!|M^g-OYGNqyuH*Wnm?#@>|@ER?61 z@}7V$OkAVi)1wg8p+Qr1Z#uv zAAuw}Z6T^*55j$caaN~fa>9}CrKH9f_gn42UHLg}`nJ_UV1$2B^%s@$u`1Q}yqqXZ z2xN7+?d^QL?OzkzD(S;*Fx>QgU!ibb>1;EWCU^<+*C~Qib%oa5+rZB` zQCABjuM;I}e!?wua6`D1gZ~Ei3#6cVw2mgR(%IJZ*!EM^)DiV*X0k%kKoMKa zEK}$L_S%G5G<((zO2T#FhXj}UjG>p#e+wn-G{w=oTv%~Ww*qc4-7ttLFL5S(tc29z z8?%+zOAWL5O}a|zcB+!s0$1(J`b*XymA1|e%Slxejo8_ zWn?Ue%6xY?0d-tWKFde|#khcBDaVF*-9QZTQ+vXSa-)1@9%H%T?e&(XH$15ihjSk zsmV{>!t=!`Tkn)m%2dn31y*cQ>eEhc7ZqGfAvR4ZgRukxU8nO$k}CY-8-zXW+v<7r zan+pV55EzsT0n4AZj!BO>a%N9PGp67VBgYEuC1x9WI?$D^RMh+cl_%VEpuozz55ra zEN?Ir1s|A6+&&3#YdqmvAph@reT^+ww;Vs|)>RcCp+_@xOh1J+^NI;^Oy{Iz7D1h- zAzm+MU?3Cq5{pWr>=^p!=%B#qCz)W#tnae=<*|$Ruib{Z8ADi8VveSzF4a&vZfk30 z&OH`;wwfXDdrzwGvSX-SYpNZUJK#Z0dd>W4P1erQqQx51TfjP`GmKb z01HK8&d)_+@QEr~>PfJV%iKW+n7Ka0g0JSOV5M=j>D4ZB1r8r>DT`3ny-y@${Ctwh zBRjk6TOy%&f*9-T#!Y9R4eOla-@GTZ2Q>q~8R94L%|IJ=rk_r?ZYA=B&h^VP7Igmq^ctV5< z6qj}l@=z|Tr{FyBk{-YnI%p;_Oo&EU%g<D{n^#ih zS#h==NbYLmv)#Hb1jwX|MnY4J$03!IIG!ZsBmdn?h>U|PP2d71C;ov}+^fAu4wkXr z#whMy2V7eWkb3&Y)D(z^xfl=G2~1<8EszI$H&!<=l8uIV*~SzLq^Swmc*VpJ)}I|x z0VfeF^n^O+x4F@2-4@mXXs_JnJoB`W?Cx0Mx1y)$^$q^d-Zut|+b875a6Ot70D$7Z zfp310{hyc}M>za`=qcIuei$hmXV$-$h(+fgo-6RPXy3%@w? z>pS6b_o;3%=I2?8wUoaWqq)zb8%0G;K(q}Qk9Alw=+EA5jIur?;UsO<;>NE(J;{YX6 zpwSI$`k`fw`L@?-E7nd1)dQuHoKUHt6{`oYfHpO~>+`#W)TmO4v@~%%q|&+@r*>ld zup~vQD}9Tgl$D$|m!f~|(1g;gej`UliuHFvJ5xd+GYVz1y@(OP3V^AB)G!rd^5JJQax_y1E zV>WIu!i_&_@+t^8`EQ_#%@hWAIr;8y87QQHsv6d!hm_OB*g&8+Y#_+DB_s#GgrD== zFQDCZuI*b>FFOuU=&D?F;zB)|y-9?-H3P1zc-tRZ!uthr^ELNzk`i{@gbB*J6**XK z@5aguV#beNc6dGx4;mc{Ug4;vTv6i@ga|qni_q<4Nz2cTXO2>gZ4g>Vs zV$l25BJwZLEx9Y=$W)T+#o2b&A!LEDK_yMXdBuL`W=qfVX2xb@xhOVw#hmnkQf04=3SiO23ieq`{i17#X!L6MTcbkSvP!BSy_1k zTT8PDa(CA+VfQ#hC=vUZ=z0W_1^Y#EsGT74S9DID;E=>IGiBfXA#N&^W|_21W{p*@ zCf9e-2;|VqfMu;N05apn2o#Ip?{=a;)wlQfE{)*EqI`26=W{`zlT;1J>b=Wx3j!x< z9x;)A-2S$mCwp~18Zs^|5RlpGu|C+QWP1{GV*i3MtOk=EP0^Zg%Lby?6 z-zxZ@FpSz;giBL2lLGY6&jKBk=f-ItHEJN@-pQCABWTXFzfXW`A?EtieKXM19Ey@i z)IoDq1RS`A&sfbxXws(#wfdHC)C?A~$9!RUs0CayL*|qilAIYA6y$2#m3YJg?;!DF zc(S0|t$9k{W+c`m;Vujc(EldQI7r0`ds-%#*#a^v*;LZKLT-s066dk^-;4 z4(i(OicqjMZg-es@5T`}_4DIdM1WE5X4KwU_77wD#7mriXg=SfQ;P_SmPq@^65OB6 z%>I8VTymYBAB?`f?Cwwb4Fo8J!s254b9Cau_#wmfHbY#x5RJ8qPhc+!A+VJj5RxMo z30+uT3=zb^3JFKBT<+vyw$oHle>FU+c=1F?%`fj_uyGGB0_>Ur!eXRvRt8w?phZJq z+scZ&Qr&&Oy!W2@(0y-w{yDL89bHj}0f!K<@oev{b*VYz)$;z+p3+(#offmS;!VJ) zLAOBz#PgV(=x1xN2M-Ah1H)gphiN%f3EX0_;?zGg>D*NkWZ9NNa+bf+UKi;X zfbcbgq=c?yVOhyd&4e@Pj<38WvdkGZ)ssL8+LCYpt7tv|O|O?jM+GR8$p~ei0ONTC zvDuW8tH(A*TO%}WfTfwf-_Qc>F|-VaE3J{oRFalGjg9u2h~bT!)h!xZKXqR3pfek3vxl+z;C7dIpL7hs*TJ`t=w2?Z z2dpnPj`M<|Y8rOAdZ}&kh*%?tL(^Sr*VZl2yQj^F-iiFn>SPH`g$s7wyj>j=PPSoy zWKJ>(21zo7?p>6zcx3jXPv0cGxrmR_mBW&RD1l+AUr%9(TetES(-M;|>$_Z8<#a+j zn6`FUQ*vH)7{MNF)6PYU|9&!yg1TB_e)XJ0XwgOVI|Q8d*|uLn)vD#!Um|fhfWFiY zUH~1D=AE-B$e1ro)!gOXk_yzwmgj3(P*r*VCHL7CeM6&*`#G_ac(8Y(lCI$kzLfTT zJRxo%1XZ+3J7c%&7FEwoKd?cS7&Pq5dGlkn@=$-!fiJ~TfSMg(fVPl+)Zj{4m;A=J z_>1@zylwD2H-O?N?|1x$c*Nex!IVfHE2vS9f_Vtd*p(z@XO>vAcBzbD z2XqA2f4vv(ru>DW2Fo+Htvu?!QfLyH*h`@mDajuO+N8e2c6mX-$qfYSR}OnG9(K7S-<{=UgrNaG5;8zaYxld z{p?bqAO1z$xz*5y3iStAJi@aJGXnDZcHp}gT`1c!eHW`zBPMOL-NS{%$KAr_qiv{T zUkb*7bR0h?__<{Ee23uLS@1&91t+{d@nFpqwb;0pVn$TuM~+C6_k2h_UL((yk4MnI z(JW&3IX9YqXchrKG>iYK0dp|cH?lVV&ju`5+1mEUPw}fp*Io=wYybySy3>GO52atj z3Nxf;EWt-yF*8Ai0bN{4+#+25r1wQEqJg9|n`ce*4|J~QwcDXGaS>QJplgDhjzs#v zYHa}D16G0Ws?04NfrdPICwInE;F{C z+nlvf0lOGAsK%;=sSvMa^(sXwCY=Zk1{$Eh!l`TRmaFw4b*(w{6^{0R1|r5i z9lh8dHeYtuTPsbOeGb@*$<%^oPP~v;ex)nenIqeih!9JZ z>#Z=30(CgKYln&Ht4~QKcSK~|oSv4Y(#cE4d=s9+y2SLI<3usmDK#na<31ikTM(Lf z>fz6LsUCWJzoCa2V2mvp?f$}4EgOlMO>zO;5{X}`i0A{zhymFl=<31FVt_g>d+p^r zM!O6w3+XTpLUR6f!7$-C=3cVre-Z8cOGrHe9QhEhH8Bcch3qCnxs69|4Nhy2vdTNP z%CzpfV<4;sHcF^i7r0;hLev|90-4#lLxn6UmHdzqRASOy)6tVVbod6n`vU1tq(|QX zgVuO;yP#`AG0XWa3Rth>r)XwQ#$Mt(@M3kd40kK$Bug5+fD0iR+bpkEoVogQi0hC; zW=&X`m0%wUnM%|^^h&hG)JXz2$qdD~5Ob^z&#bL)&gM>==CC6=_unKT<(@+5?#5Kt z6Z0MCtQ(v?P6JO-q0zg{TmesAljGgUHc{bfCJ>ey_ewJ>aVwBLTn$oJg(m4oB0Tld zgk_dyQ>oc~J)`$x;dX6#LfFd5&$j5E5F}_UMB25**B~39HFYW4!?i@wLU*wo!0Kc2 zii3;kaHu_y0-pSJi2m24uW?Kt7dz_ysq7*?iK&7xvM`f51TT(3=J9)p77Y9$tfo@W zbQfx<&(sXt8&3q03D z=OJ0PFM9CjtYdSL;-ao==lhSV^mu+wD7u|*_bVCRspt5@Ga2Zny;<|sjR)#9<@K2f z#u#vJ&Vucpt)J+B*(F!|kguTi!gHjjXrPTQgThx1h`P}|150=v1VNAh(8MGV{4PR9p$TnaN* z^Wn(U^&ZVOu0ZmLtM!%E3N18NZ0aj_>iG>pxY*etZ&=MxMt92ktq?URVU9}qpZj(< zr$ebRauoB^Dt8f$a%&GoLiCK-T%sJDeTBsDCEZt98g>Xq-0I4QxN694gSCg0&7LK) zI-P9b!JeR{!I3i$=k{jHVFi_f`%XjQMHuYL%}gq`l3SGHZoD+6WAtVD zVbQ)qdAY{>?&R%x@im0KYG+#}{9zR*1;6eiyMj7G8dLq4G~#%PBZtzZ_T0G{RTQyC z!L}S5qm2~W%B3UJfEMY`sJIunAfLLl-`-rKwzd%grAg%2B&MU4LaB7gNmTRPP-0PU zADBip#H#CAw-ldDHcvCIi%sbue8sut-2fkXl}_}&tstH1{Zw%~%4c6ncs&zSjY;&st>0!sxJPnR^z>HAKs7<7 z8;vtfRAX=(t<}z`O8HAK7e6q_{i%EKg{5d4|1G{soViCK^yjfgCitI=xq-QpfwQ6I zk4xKsViW4y|0<2XFS`6qzeo%twl4M8U_gX7oHax8X*M3m9dS1btZ5sH;+3*!PrrLO z6AHw{ZFq7v41C0>@2}muVt2O3y;PG05c@?6n<-pLq-)dSx~NI^OeT5gNVrI@&8HoQ zgO1)b2cW874U!#I%0eDuU2Ao-RX&1saH2(G$0^G}Cf731sfpQBBa$rHGEF!|_=tX; z#)ub4B%~gZs;3$>(RW6=yaY{&fbhhxha-JcDz8>FHcWOvr*7Q_+H_pmwU{ zNtim5s;z6HCh#K0nkZ7Efrh ze|kTWwF`GE^^_^wsfI^EW`2UFGH|(ML%M>jk}D3Keyck)FRe;T)~9bRC4|c+Vxv5t zec>Qesw8F>=M7uDBRr2r*ckP>n$!T~_+wPfnL(~YbR>CTT8leb9VqrcOI7&c9`>UJ z%cbgV{K|NU5^D*(H2Y2R1VclST4h+STjiMM=H6|f5lFditL`rfRRW401St=-6Ymd$ z%-Q-7SV#)yZ(mwP(D})RAhlcovFsV_b8M-Q1Y%&(EIn4slsdgov313k2RQbB5DP79 zYq>-B_R&S!^-&!sEwu}+IAUpQd~x}k6g^ICwQ29pDTE4GL%{_hD$?(J2}3dBqVSiP zs3@ux2zJkxzo(`6xp>IKfa)mQ%0dDx{@2lj*#VwZ;>vV@jV;vBNb}qng{M`ZaRXh_ z=zlb#*e}DHag%3}GbUb#c5ZNkSp5UJ64VcAk)};$OGkNUHEDm{k+G3AyhN7NV82W1h?$>=BOj54*sWPwn3t}u zHHs!uwv5I&cr}!NY>p5Bwu}`f9D~e{2`uJuuq#s7dWucJ*E*&jvzscs6(ZZ#eVB3O zF-JxXSz2ZxhX#->EglH{axRm?aVaiB>XCUV9zdwt{Q2FxV^89a3KStbN%$vr6(!@; z5y`rhyE&)H+I@GFtgK_s+ME6<8w}A{W>cZP4#aVwZn#egf8aSU?GM;Zufc6!Qy%a{ zU;Eh$%95z2FhWHyLsT#C`*-zS>H=0Lm&JynR@yEQKgxMRr%LVWbPuoZ{DoQPmdJF- zG8*t4W;c2k+7f(Ja&Qk*UQHh%X3q$iLCzBpO^$ckRB!GS4_Aoq!8jwX#f~tUH8+K> zr-NW6TVkI*jdaHkGBG@QV6!#OiGqD4+$}vB|qk8_Mv|EdM`XF(|6f6CNm>EEr6$49AAa4RkLy? z2g^c(h6OyE+Pq$0Z-|p)ZC;YXT5Ub5rCI>E zG2`TRx9!gQQ-E_)iW@kX=}L=*o2|}S?UG<=4qjyl_b8b86{xX=GP0)euhh=~5?0FQ z+{9AjWFmmS7Xyy-x(i%M4VQ4LSxTvnAKfeaMH%?peN^x3C}wOJ4vGvJJ*)tIB?Wzu z{=Djd^-97MOJR^MJYA9Y5C=@I{S z7JCfL1V$Z5%v7rm6-%+VXq3sEtHfF+vZ`IputWp_tM1iIXXuO;Vdqd1!ExNFwz~8W zIQzykgSL+2Do^3GI;pX{K+L*4?H5pT^Dv9;)vP;3HIuQb}dsOC_?DH7OyJU1TX)Mn+~Z zi(NG-LI|ZIw2UZ9kq}ChvSckpDy6hZ*(!eOf1V6xhUtI%yw6j4-*fJ{=bpQ~ckgZ6 zUl6v(DMx6*4|(68eyN}MIcFOjTz(M0E7#z4VBwr4?DD;OQx`$2UDn!?J2#YTGV!zcW?r$9rL*Hw zcx=&?Nm)Ko(wL109U7z0-PVtNU6W*k%Q%=8``t9BIqt?+d{MSvtA&W#sqcYJ9*g4@ ze7Mi9**br9r1?(GCq;ZKE0q3N1@%9{Y(m#al0-Srny3;^?IN@&YxjJP-EO2WkmV&h zYmS@B?0c(1lGSU{<(33D$(&G=DAFf37N4yC_7jp%on_`cP94l~l!UU8^ka>y zfd^M*Mt-<$_y2LzzVoyf=eyU2W$v$!TOZiErXL@1M@uPB#PhtAhuZZIR|Zc0=K5xg zFDid%;n7v~weOvFwDCHg>n<16^wdZ4t&@8$HmO^7o7cU}lB?d%n{`~kH#ao|mf1Vy z)H(@QEgs626?Zi$@du_gW~cZcN{6~~eD zhrSB6y=&9G*nZ`Rs_ue)UHv@%yE{ZDhaX6R^x5}`hQo)ukqej#WW(PAiLy3=GeF@$ zU26~g(D5D8TBOTr&tQYqW}}X_vpY;gPH=3~6)SyRo-_P;gdZhaX_Q_lwfNy^z{pU~ zu<bX}hReODvm;(PN25S-O_{#124+L$U~JJytDA~p zz==A_-7AFRrCd!7yhhzF-Ll5bLU->!ek^RHw$P*`*SW8)&HK3C(b3GScc^t{d3Ku1 zT+xAovb%E-~ncq2?m8SFQ3gL!kI9H@) zoV5XuvD$%^fs4rJ`PWB1Eu4F)CcUNZ*>m2qu2XZg;q^NEkmf_Pj;v2}_^O=Daf76flwTIzkvxX~p&<7l<17e*#J ziuA!TZ!dgDCLLc9w2y(jWiXl?fG+r?6@rCJL0p}iZmpJt-1VM{>Qz0>BbypO-88(Ptm&+t2P@^ z-mVFKl(}F-{Jfr09u7f{UuJH*!y68xl_I~b)2Oo??8H4hhm!V*QrV4j*)_ub=!!6**=H*lj55uWKC4)TBBc%W3Fte25=+%mD|k-f7&CH)vXs!>ELM5I$k;yC58tkP!TG6_wgW68zeysc!g zjb(FNY-&b-e$w3bUYuJ~C(Xvj-MQVcmrbj1*QX;HTXKb6mNz{WSA3MHvUim2*>#6c z-ktRi?LSA<<7)Wc6l4=hEA72zUuxxF>z8r1Q0#ZbH+!W%)0~~FgL5TTv<1kv=7;cK z@=9ynmczkaNPe!bznkR0M{M{|g0zIO;7T#|{s%jFUn=)Ds(scBYrn9-f3?nD8a9^c5%`L`b?3LGzCUuZ{31pB#Y@-DHAke58CDg<5?$+02@=^7y;px6 z)r|}tlr|W;XCO?BJ{&m^<4RI@XDyf`qa2Uez~{T z60SIMD?d)UIT+F*?R0bC?2AmP(Wcl1LZ7!K886b-xNF|I`nDbSi+3GQ{v`dERaVqm zAExaYLzdYev}9v7s#fYvg(nw}%Jz(C(!3i#S4Ex<@#)KKyVzb^XK^p&v{s<5^*(vY z){iR&vU7T`y$_!M#yLE%QNE%Bx2PYlTQwhmPv||9~G|uPcyYzp=V)> zp@fX%O3Uo`InD28lT>KEE}@BAdvEg@ljhf=hstu&`r=-M z7x!0v6OpZr^NrB>A#Po1qtGG5FK#+NgS)I>6Z>rS%P(cQ0@4XLe+UcT-n!0k4 zI4zEO=w~a14cpzUD$XA+$@%Vi((1t?4TG8}Crc9n$@e}kJl!3VDMuPVO4aFXJG^X- z%(qbcC*7}dRNwbYeKbE?^QZpf!X?JS#lFGYuaS=GR2}nlT zDQ$0gH#+!)C;iwjqv6ldqw~skCf!L;Vvm_G=ohs3va}%T5T8rJPE^tb(b;UDK&kYVomXC;kX!@dFDN$DlvdnW-jp7W(Ta`kfH6H%rLAyuoO7g~*Xd zUQ+KuS6#JqE$MV#ITY-=kzL#?*y3&h9K}{W9N*o}BfQJ0MtT2uy>%Bmgz7S0;#w%!pk3&*B!Y<|nlH}y7K4$qC# z=WA~VuUUolSby077HDKRyiz*pBL9h}4HboxzDfRD(06M+}( z@$ne_ddmD$LrVa4#M3>;weF-QwmYbIwf({r!`w^FyP_*Kq7}}g15|$)+KIaL`?0?j zc=f9K=Eg2RRSiwA6-G{t;fW{j{B$pg4#pZ)KQs-nUKYJ1$NkLhlDtPQwm#vck0ZKV zbs~7;)6xX8i(>46j$kIbx@-5Q!>?o2xvs?rtbb}7z0|P76FZ>wv6L^M`*=HMzkD$C)MwzfruUUK!b{hGuZTn$4+eyN#N zqGIYOi%0H0sDMnCpXbdb zx*%@V+F7{4`*XJMUm*Ss{j^OZs%WuTV{K%18m?S{Pa~f9LPk*0mLLlibF1fqcJsvy zLQ>b<=GU~5brdht_>^UA?)iS7Xv0=-!TUcWkI1&7^_CIVq(&PoEY@}voqxw%;r4ZYTUf%cya92}Nz)XvKI&R)!^!>Rd1&s{O%=@9=?8+QCj zXZ*!yY$x>lH_GF-72!H zJ)VPmUJq}y5a{4S$y_a+Q(n%2m3vtAZcFGA#ao``tT8-DX2kaj`G%xnB>_}H<-fY88FDq$M_X1&)pgDrvZKwTjZc4VvT-lK6ivAh+ zz1VmEf9}Hst<*i4bAK=A9~{wkeblsnd;NvNxASb<;s!UpU8|HGMSAJ{!cKREQoG?< zF3VqgtOs7I#{ZHvAP1wK9}-d*7`fiB)1@!9lhpGz^mk2gtXfvWfl8 z{P-c(v%SW}YkhDY$w}nKizN?sE!r`MXZTI6h3Ey{Q3KrURJE*nIlY78iw^z4usu{5 zJteqQ+(a~}N+9Tw`k<)>*_Hzx@v}Rtg)B19gKY=qWY`Z;)pu=9Zv*X?SoZD}Q@h4( zSt;vn>L!ENj+w9gd^CO4T&^|biq+BUy#r=aTUUJN)7yZU*nL>=XC z;eA}XRP5>nvu0Nxc7gTEc9IJvFacYt1-VKq*k+hi-1&c(ghSn zy?XoYS$anK!o^QS2!oV{gCUN~ySH5C=sujgamPmV-EQ-{H62+khiCgpwEq-1K<>EM zEmM(VHnd|lSNQG7B9nV1<%Lw=)6zcfC2Or;Xk2w$9BjA(Q)#%x{px1nS1C{blMPt; z`?ep>PpgDC=Ws!dhYx|SftnR-!Z`&O8Mc!L8TjBLcGu2_ld=yU9 zVVlLr4rj2T=Aw{T=(b{iKeArX87>~UXo*Teexi-B)*9Jc8fLEHkdcW*rMwnEp>#2>j${wggfh;aIGiKig-IC>;i&X&@T?JB z%O)5|*8GH~RmKBLBu&i`Qogpw@RG=hMDrk5i|8PI05NjJ;wSXLVesUsk0a(i-eJxi z55~AC&ZvX6$YcQ#FkYTSEXkAL&s0X=0djXkc0o3j=Z%Gbn`)XGREnr9b4I-U8I`Eo zqrBM(9`b^V-ba70n|NUcK1yOmfPE`pxKL_W{pLZ6X?}ZDNyiO!4g&Ph*qCHRJ5#2m}CpLMQib7Po z{PT}kC`^Z(1ZSZ~y`Ba&b_Nl0v?2yRznqFf4DIgzJvkMs(|s7sLw%S?RLTXYo}v1G2Ud zA?o3ss}vnW!ho4UGaVLBBsqF`Fu4Sx6=_SLkv@=2;95yXylcixbj5%{sRnr+mbU@6A)X{0Qi@!gc*A(2Xo5UE)pV%{NN5tTKrZw2P25L(lI|}6h&O)C_ z%Y)1qU=XoT9j4Jo?Iypa7Fc~0Y5}wY6NyUM62c534Y%bB~Z^g<5^u8GjVfWapcmGUQ^ z8APi~V@HaSy{1u&r_Ss5CNOybv={UdF%x3$#PbZn8tXX?DxNR-(l;23jZKK+Qm$DkKf$?$ps`V>q2W%Ytogu-KKVt$ zWbcTToJJ3i%dOCZZd%P~pun%JqL4P2-YHUsS)pf$*oalaWvjNqJRLrLE-O6EIq5zfWpo=4w*E2$h|r$kI*N4gy!1Bd>1Uy*%KNmXcNXpMnt4_M@AnEB9JmxK|3FW zsd3bL@JjkDpmBTgJz`ER;7{ZE)BKj3WVqBYFWQ%UB2)kF)=Wbt0@^ z{mcgK?}CM-XzIhAO6d_`Az+3&i?~d6t5D`HaLE{`QuHdngws!HS*MLzCR`F+#?c8n ztCPPlox>mkwhU(-5Cs7jn0!7A%3<>Pv9Y-PgaGKk{hN1vZA%NzXFuI`$VO78UDx?zgARC*I zz6MPM+^G}=ITljj09hh3@$&zO*eJJeb|Q$_48tFW=&!&+1TqQoFFaD-O$jFs$3c1D z2V2ocH*pvrO%^1BvyPYd z1OrAHOVZ_jQ~p|f+grREnFBqdu zH@KREo@1{JN}mO##psm20sG`lmik|>7{jx$tf}}`c^fGILlAE1Bd)~O8E8I^9$1*g zLh8t0Wg#Bpq2~*O%4wQt>gPSu%RyaVP*;IY-5S#w1Ta{Gl`&chyx%_t$FM-$n2xS! z`G3-3<`(8hr|Y#_-}-)37{F1`bB8YPM50pkt!99GA%klo36>r^%*?gR^c6uSIBq~K zp^r>iwli?C_%W-Gbt&2Q)K#UG^^T+|?e$E)L85~|Wg4gUEz^lMK^rjE?W-VZh&9r?bV+OK% zEp|$Q=g2_wtUO^FB)npYtocsRq25OrJJo*DX46#Jus+`20qDTgz<82AfW0EKmVgYc z|CxbTy#dW9L2)}U68%(~L=bB{i#WFP*3Hn%07YIT&=>2-KcIL|hw+J{8H*K(>aVr$ z#34hmK)C2DZC@~Jt;aiF|DH)*bk%eH4ZsUQ9_U-Sn+I5nK;rPUv-gPRH~8G*vjGdO z1A}QY4zk7~yFZW3fq`WZuO|?uo!wwOsUBpFB`PK35NiR`o?Fl8@QkO@OyUL4vc^w$ zMhzpKaX0KqI$IiRI@8|RjA+J(_%!*FVicYN4nhKuo6Y)u~STozi#|)Dn zj59vZ_(k*N^EX@Qo*)0ddFt~>cms6gf_{_`{%?X_aco`0VqQp@s(pzW> GQ2ztv=R4E@ literal 0 HcmV?d00001 diff --git a/sublime/Packages/Default/Preferences.sublime-settings b/sublime/Packages/Default/Preferences.sublime-settings index bf86fc6..ae3eb45 100644 --- a/sublime/Packages/Default/Preferences.sublime-settings +++ b/sublime/Packages/Default/Preferences.sublime-settings @@ -228,6 +228,10 @@ // inserting tabs. "shift_tab_unindent": false, + // If true, the copy and cut commands will operate on the current line + // when the selection is empty, rather than doing nothing. + "copy_with_empty_selection": true, + // If true, the selected text will be copied into the find panel when it's // shown. // On OS X, this value is overridden in the platform specific settings, so @@ -264,6 +268,13 @@ // Sublime Text must be restarted for this to take effect. "use_simple_full_screen": false, + // OS X only. Valid values are true, false, and "auto". Auto will enable + // the setting when running on a screen 2880 pixels or wider (i.e., a + // Retina display). When this setting is enabled, OpenGL is used to + // accelerate drawing. Sublime Text must be restarted for changes to take + // effect. + "gpu_window_buffer": "auto", + // Valid values are "system", "enabled" and "disabled" "overlay_scroll_bars": "system", diff --git a/sublime/Packages/Package Control/Default.sublime-commands b/sublime/Packages/Package Control/Default.sublime-commands index 7d823ee..dd5513c 100644 --- a/sublime/Packages/Package Control/Default.sublime-commands +++ b/sublime/Packages/Package Control/Default.sublime-commands @@ -5,7 +5,7 @@ }, { "caption": "Package Control: Add Channel", - "command": "add_repository_channel" + "command": "add_channel" }, { "caption": "Package Control: Create Binary Package File", @@ -27,6 +27,10 @@ "caption": "Package Control: Enable Package", "command": "enable_package" }, + { + "caption": "Package Control: Grab CA Certs", + "command": "grab_certs" + }, { "caption": "Package Control: Install Package", "command": "install_package" diff --git a/sublime/Packages/Package Control/Package Control.ca-bundle b/sublime/Packages/Package Control/Package Control.ca-bundle new file mode 100644 index 0000000..b718caa --- /dev/null +++ b/sublime/Packages/Package Control/Package Control.ca-bundle @@ -0,0 +1,43 @@ +----BEGIN CERTIFICATE----- +MIIDVDCCAjygAwIBAgIDAjRWMA0GCSqGSIb3DQEBBQUAMEIxCzAJBgNVBAYTAlVT +MRYwFAYDVQQKEw1HZW9UcnVzdCBJbmMuMRswGQYDVQQDExJHZW9UcnVzdCBHbG9i +YWwgQ0EwHhcNMDIwNTIxMDQwMDAwWhcNMjIwNTIxMDQwMDAwWjBCMQswCQYDVQQG +EwJVUzEWMBQGA1UEChMNR2VvVHJ1c3QgSW5jLjEbMBkGA1UEAxMSR2VvVHJ1c3Qg +R2xvYmFsIENBMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA2swYYzD9 +9BcjGlZ+W988bDjkcbd4kdS8odhM+KhDtgPpTSEHCIjaWC9mOSm9BXiLnTjoBbdq +fnGk5sRgprDvgOSJKA+eJdbtg/OtppHHmMlCGDUUna2YRpIuT8rxh0PBFpVXLVDv +iS2Aelet8u5fa9IAjbkU+BQVNdnARqN7csiRv8lVK83Qlz6cJmTM386DGXHKTubU +1XupGc1V3sjs0l44U+VcT4wt/lAjNvxm5suOpDkZALeVAjmRCw7+OC7RHQWa9k0+ +bw8HHa8sHo9gOeL6NlMTOdReJivbPagUvTLrGAMoUgRx5aszPeE4uwc2hGKceeoW +MPRfwCvocWvk+QIDAQABo1MwUTAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBTA +ephojYn7qwVkDBF9qn1luMrMTjAfBgNVHSMEGDAWgBTAephojYn7qwVkDBF9qn1l +uMrMTjANBgkqhkiG9w0BAQUFAAOCAQEANeMpauUvXVSOKVCUn5kaFOSPeCpilKIn +Z57QzxpeR+nBsqTP3UEaBU6bS+5Kb1VSsyShNwrrZHYqLizz/Tt1kL/6cdjHPTfS +tQWVYrmm3ok9Nns4d0iXrKYgjy6myQzCsplFAMfOEVEiIuCl6rYVSAlk6l5PdPcF +PseKUgzbFbS9bZvlxrFUaKnjaZC2mqUPuLk/IH2uSrW4nOQdtqvmlKXBx4Ot2/Un +hw4EbNX/3aBd7YdStysVAq45pmp06drE57xNNB6pXE0zX5IJL4hmXXeXxx12E6nV +5fEWCRE11azbJHFwLJhWC9kXtNHjUStedejV0NxPNO3CBWaAocvmMw== +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIDxTCCAq2gAwIBAgIQAqxcJmoLQJuPC3nyrkYldzANBgkqhkiG9w0BAQUFADBs +MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3 +d3cuZGlnaWNlcnQuY29tMSswKQYDVQQDEyJEaWdpQ2VydCBIaWdoIEFzc3VyYW5j +ZSBFViBSb290IENBMB4XDTA2MTExMDAwMDAwMFoXDTMxMTExMDAwMDAwMFowbDEL +MAkGA1UEBhMCVVMxFTATBgNVBAoTDERpZ2lDZXJ0IEluYzEZMBcGA1UECxMQd3d3 +LmRpZ2ljZXJ0LmNvbTErMCkGA1UEAxMiRGlnaUNlcnQgSGlnaCBBc3N1cmFuY2Ug +RVYgUm9vdCBDQTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAMbM5XPm ++9S75S0tMqbf5YE/yc0lSbZxKsPVlDRnogocsF9ppkCxxLeyj9CYpKlBWTrT3JTW +PNt0OKRKzE0lgvdKpVMSOO7zSW1xkX5jtqumX8OkhPhPYlG++MXs2ziS4wblCJEM +xChBVfvLWokVfnHoNb9Ncgk9vjo4UFt3MRuNs8ckRZqnrG0AFFoEt7oT61EKmEFB +Ik5lYYeBQVCmeVyJ3hlKV9Uu5l0cUyx+mM0aBhakaHPQNAQTXKFx01p8VdteZOE3 +hzBWBOURtCmAEvF5OYiiAhF8J2a3iLd48soKqDirCmTCv2ZdlYTBoSUeh10aUAsg +EsxBu24LUTi4S8sCAwEAAaNjMGEwDgYDVR0PAQH/BAQDAgGGMA8GA1UdEwEB/wQF +MAMBAf8wHQYDVR0OBBYEFLE+w2kD+L9HAdSYJhoIAu9jZCvDMB8GA1UdIwQYMBaA +FLE+w2kD+L9HAdSYJhoIAu9jZCvDMA0GCSqGSIb3DQEBBQUAA4IBAQAcGgaX3Nec +nzyIZgYIVyHbIUf4KmeqvxgydkAQV8GK83rZEWWONfqe/EW1ntlMMUu4kehDLI6z +eM7b41N5cdblIZQB2lWHmiRk9opmzN6cN82oNLFpmyPInngiK3BD41VHMWEZ71jF +hS9OMPagMRYjyOfiZRYzy78aG6A9+MpeizGLYAiJLQwGXFK3xPkKmNEVX58Svnw2 +Yzi9RKR/5CYrCsSXaQ3pjOLAEFe4yHYSkVXySGnYvCoCWw9E1CAx2/S6cCZdkGCe +vEsXCS+0yx5DaMkHJ8HSXPfqIbloEpw8nL+e/IBcm2PN7EeqJSdnoDfzAIJ9VNep ++OkuE6N36B9K +-----END CERTIFICATE----- diff --git a/sublime/Packages/Package Control/Package Control.ca-list b/sublime/Packages/Package Control/Package Control.ca-list new file mode 100644 index 0000000..93aa232 --- /dev/null +++ b/sublime/Packages/Package Control/Package Control.ca-list @@ -0,0 +1,4 @@ +[ + "221e907bdfff70d71cea42361ae209d5", + "7d0986b90061d60c8c02aa3b1cf23850" +] diff --git a/sublime/Packages/Package Control/Package Control.py b/sublime/Packages/Package Control/Package Control.py index 515159a..9c47377 100644 --- a/sublime/Packages/Package Control/Package Control.py +++ b/sublime/Packages/Package Control/Package Control.py @@ -1,4810 +1,101 @@ -# coding=utf-8 -import sublime -import sublime_plugin -import os -import sys -import subprocess -import zipfile -import urllib -import urllib2 -import json -from fnmatch import fnmatch -import re -import threading -import datetime -import time -import shutil -import tempfile -import httplib -import socket -import hashlib -import base64 -import locale -import urlparse -import gzip -import StringIO -import zlib - - -if os.name == 'nt': - from ctypes import windll, create_unicode_buffer - - -def add_to_path(path): - # Python 2.x on Windows can't properly import from non-ASCII paths, so - # this code added the DOC 8.3 version of the lib folder to the path in - # case the user's username includes non-ASCII characters - if os.name == 'nt': - buf = create_unicode_buffer(512) - if windll.kernel32.GetShortPathNameW(path, buf, len(buf)): - path = buf.value - - if path not in sys.path: - sys.path.append(path) - - -lib_folder = os.path.join(sublime.packages_path(), 'Package Control', 'lib') -add_to_path(os.path.join(lib_folder, 'all')) - - -import semver - - -if os.name == 'nt': - add_to_path(os.path.join(lib_folder, 'windows')) - from ntlm import ntlm - - -def unicode_from_os(e): - # This is needed as some exceptions coming from the OS are - # already encoded and so just calling unicode(e) will result - # in an UnicodeDecodeError as the string isn't in ascii form. - try: - # Sublime Text on OS X does not seem to report the correct encoding - # so we hard-code that to UTF-8 - encoding = 'UTF-8' if os.name == 'darwin' else locale.getpreferredencoding() - return unicode(str(e), encoding) - - # If the "correct" encoding did not work, try some defaults, and then just - # obliterate characters that we can't seen to decode properly - except UnicodeDecodeError: - encodings = ['utf-8', 'cp1252'] - for encoding in encodings: - try: - return unicode(str(e), encoding, errors='strict') - except: - pass - return unicode(str(e), errors='replace') - - -def create_cmd(args, basename_binary=False): - if basename_binary: - args[0] = os.path.basename(args[0]) - - if os.name == 'nt': - return subprocess.list2cmdline(args) - else: - escaped_args = [] - for arg in args: - if re.search('^[a-zA-Z0-9/_^\\-\\.:=]+$', arg) == None: - arg = u"'" + arg.replace(u"'", u"'\\''") + u"'" - escaped_args.append(arg) - return u' '.join(escaped_args) - - -# Monkey patch AbstractBasicAuthHandler to prevent infinite recursion -def non_recursive_http_error_auth_reqed(self, authreq, host, req, headers): - authreq = headers.get(authreq, None) - - if not hasattr(self, 'retried'): - self.retried = 0 - - if self.retried > 5: - raise urllib2.HTTPError(req.get_full_url(), 401, "basic auth failed", - headers, None) - else: - self.retried += 1 - - if authreq: - mo = urllib2.AbstractBasicAuthHandler.rx.search(authreq) - if mo: - scheme, quote, realm = mo.groups() - if scheme.lower() == 'basic': - return self.retry_http_basic_auth(host, req, realm) - -urllib2.AbstractBasicAuthHandler.http_error_auth_reqed = non_recursive_http_error_auth_reqed - - -class DebuggableHTTPResponse(httplib.HTTPResponse): - """ - A custom HTTPResponse that formats debugging info for Sublime Text - """ - - _debug_protocol = 'HTTP' - - def __init__(self, sock, debuglevel=0, strict=0, method=None): - # We have to use a positive debuglevel to get it passed to here, - # however we don't want to use it because by default debugging prints - # to the stdout and we can't capture it, so we use a special -1 value - if debuglevel == 5: - debuglevel = -1 - httplib.HTTPResponse.__init__(self, sock, debuglevel, strict, method) - - def begin(self): - return_value = httplib.HTTPResponse.begin(self) - if self.debuglevel == -1: - print '%s: Urllib2 %s Debug Read' % (__name__, self._debug_protocol) - headers = self.msg.headers - versions = { - 9: 'HTTP/0.9', - 10: 'HTTP/1.0', - 11: 'HTTP/1.1' - } - status_line = versions[self.version] + ' ' + str(self.status) + ' ' + self.reason - headers.insert(0, status_line) - for line in headers: - print u" %s" % line.rstrip() - return return_value - - def read(self, *args): - try: - return httplib.HTTPResponse.read(self, *args) - except (httplib.IncompleteRead) as (e): - return e.partial - - -class DebuggableHTTPSResponse(DebuggableHTTPResponse): - """ - A version of DebuggableHTTPResponse that sets the debug protocol to HTTPS - """ - - _debug_protocol = 'HTTPS' - - -class DebuggableHTTPConnection(httplib.HTTPConnection): - """ - A custom HTTPConnection that formats debugging info for Sublime Text - """ - - response_class = DebuggableHTTPResponse - _debug_protocol = 'HTTP' - - def __init__(self, host, port=None, strict=None, - timeout=socket._GLOBAL_DEFAULT_TIMEOUT, **kwargs): - self.passwd = kwargs.get('passwd') - - # Python 2.6.1 on OS X 10.6 does not include these - self._tunnel_host = None - self._tunnel_port = None - self._tunnel_headers = {} - - httplib.HTTPConnection.__init__(self, host, port, strict, timeout) - - def connect(self): - if self.debuglevel == -1: - print '%s: Urllib2 %s Debug General' % (__name__, self._debug_protocol) - print u" Connecting to %s on port %s" % (self.host, self.port) - httplib.HTTPConnection.connect(self) - - def send(self, string): - # We have to use a positive debuglevel to get it passed to the - # HTTPResponse object, however we don't want to use it because by - # default debugging prints to the stdout and we can't capture it, so - # we temporarily set it to -1 for the standard httplib code - reset_debug = False - if self.debuglevel == 5: - reset_debug = 5 - self.debuglevel = -1 - httplib.HTTPConnection.send(self, string) - if reset_debug or self.debuglevel == -1: - if len(string.strip()) > 0: - print '%s: Urllib2 %s Debug Write' % (__name__, self._debug_protocol) - for line in string.strip().splitlines(): - print ' ' + line - if reset_debug: - self.debuglevel = reset_debug - - def request(self, method, url, body=None, headers={}): - original_headers = headers.copy() - - # Handles the challenge request response cycle before the real request - proxy_auth = headers.get('Proxy-Authorization') - if os.name == 'nt' and proxy_auth and proxy_auth.lstrip()[0:4] == 'NTLM': - # The default urllib2.AbstractHTTPHandler automatically sets the - # Connection header to close because of urllib.addinfourl(), but in - # this case we are going to do some back and forth first for the NTLM - # proxy auth - headers['Connection'] = 'Keep-Alive' - self._send_request(method, url, body, headers) - - response = self.getresponse() - - content_length = int(response.getheader('content-length', 0)) - if content_length: - response._safe_read(content_length) - - proxy_authenticate = response.getheader('proxy-authenticate', None) - if not proxy_authenticate: - raise URLError('Invalid NTLM proxy authentication response') - ntlm_challenge = re.sub('^\s*NTLM\s+', '', proxy_authenticate) - - if self.host.find(':') != -1: - host_port = self.host - else: - host_port = "%s:%s" % (self.host, self.port) - username, password = self.passwd.find_user_password(None, host_port) - domain = '' - user = username - if username.find('\\') != -1: - domain, user = username.split('\\', 1) - - challenge, negotiate_flags = ntlm.parse_NTLM_CHALLENGE_MESSAGE(ntlm_challenge) - new_proxy_authorization = 'NTLM %s' % ntlm.create_NTLM_AUTHENTICATE_MESSAGE(challenge, user, - domain, password, negotiate_flags) - original_headers['Proxy-Authorization'] = new_proxy_authorization - response.close() - - httplib.HTTPConnection.request(self, method, url, body, original_headers) - - -class DebuggableHTTPHandler(urllib2.HTTPHandler): - """ - A custom HTTPHandler that formats debugging info for Sublime Text - """ - - def __init__(self, debuglevel=0, debug=False, **kwargs): - # This is a special value that will not trigger the standard debug - # functionality, but custom code where we can format the output - if debug: - self._debuglevel = 5 - else: - self._debuglevel = debuglevel - self.passwd = kwargs.get('passwd') - - def http_open(self, req): - def http_class_wrapper(host, **kwargs): - kwargs['passwd'] = self.passwd - return DebuggableHTTPConnection(host, **kwargs) - - return self.do_open(http_class_wrapper, req) - - -class RateLimitException(httplib.HTTPException, urllib2.URLError): - """ - An exception for when the rate limit of an API has been exceeded. - """ - - def __init__(self, host, limit): - httplib.HTTPException.__init__(self) - self.host = host - self.limit = limit - - def __str__(self): - return ('Rate limit of %s exceeded for %s' % (self.limit, self.host)) - - -if os.name == 'nt': - class ProxyNtlmAuthHandler(urllib2.BaseHandler): - - handler_order = 300 - auth_header = 'Proxy-Authorization' - - def __init__(self, password_manager=None): - if password_manager is None: - password_manager = HTTPPasswordMgr() - self.passwd = password_manager - self.retried = 0 - - def http_error_407(self, req, fp, code, msg, headers): - proxy_authenticate = headers.get('proxy-authenticate') - if os.name != 'nt' or proxy_authenticate[0:4] != 'NTLM': - return None - - type1_flags = ntlm.NTLM_TYPE1_FLAGS - - if req.host.find(':') != -1: - host_port = req.host - else: - host_port = "%s:%s" % (req.host, req.port) - username, password = self.passwd.find_user_password(None, host_port) - if not username: - return None - - if username.find('\\') == -1: - type1_flags &= ~ntlm.NTLM_NegotiateOemDomainSupplied - - negotiate_message = ntlm.create_NTLM_NEGOTIATE_MESSAGE(username, type1_flags) - auth = 'NTLM %s' % negotiate_message - if req.headers.get(self.auth_header, None) == auth: - return None - req.add_unredirected_header(self.auth_header, auth) - return self.parent.open(req, timeout=req.timeout) - - -# The following code is wrapped in a try because the Linux versions of Sublime -# Text do not include the ssl module due to the fact that different distros -# have different versions -try: - import ssl - - class InvalidCertificateException(httplib.HTTPException, urllib2.URLError): - """ - An exception for when an SSL certification is not valid for the URL - it was presented for. - """ - - def __init__(self, host, cert, reason): - httplib.HTTPException.__init__(self) - self.host = host - self.cert = cert - self.reason = reason - - def __str__(self): - return ('Host %s returned an invalid certificate (%s) %s\n' % - (self.host, self.reason, self.cert)) - - - class ValidatingHTTPSConnection(DebuggableHTTPConnection): - """ - A custom HTTPConnection class that validates SSL certificates, and - allows proxy authentication for HTTPS connections. - """ - - default_port = httplib.HTTPS_PORT - - response_class = DebuggableHTTPSResponse - _debug_protocol = 'HTTPS' - - def __init__(self, host, port=None, key_file=None, cert_file=None, - ca_certs=None, strict=None, **kwargs): - passed_args = {} - if 'timeout' in kwargs: - passed_args['timeout'] = kwargs['timeout'] - DebuggableHTTPConnection.__init__(self, host, port, strict, **passed_args) - - self.passwd = kwargs.get('passwd') - self.key_file = key_file - self.cert_file = cert_file - self.ca_certs = ca_certs - if 'user_agent' in kwargs: - self.user_agent = kwargs['user_agent'] - if self.ca_certs: - self.cert_reqs = ssl.CERT_REQUIRED - else: - self.cert_reqs = ssl.CERT_NONE - - def get_valid_hosts_for_cert(self, cert): - """ - Returns a list of valid hostnames for an SSL certificate - - :param cert: A dict from SSLSocket.getpeercert() - - :return: An array of hostnames - """ - - if 'subjectAltName' in cert: - return [x[1] for x in cert['subjectAltName'] - if x[0].lower() == 'dns'] - else: - return [x[0][1] for x in cert['subject'] - if x[0][0].lower() == 'commonname'] - - def validate_cert_host(self, cert, hostname): - """ - Checks if the cert is valid for the hostname - - :param cert: A dict from SSLSocket.getpeercert() - - :param hostname: A string hostname to check - - :return: A boolean if the cert is valid for the hostname - """ - - hosts = self.get_valid_hosts_for_cert(cert) - for host in hosts: - host_re = host.replace('.', '\.').replace('*', '[^.]*') - if re.search('^%s$' % (host_re,), hostname, re.I): - return True - return False - - def _tunnel(self, ntlm_follow_up=False): - """ - This custom _tunnel method allows us to read and print the debug - log for the whole response before throwing an error, and adds - support for proxy authentication - """ - - self._proxy_host = self.host - self._proxy_port = self.port - self._set_hostport(self._tunnel_host, self._tunnel_port) - - self._tunnel_headers['Host'] = u"%s:%s" % (self.host, self.port) - self._tunnel_headers['User-Agent'] = self.user_agent - self._tunnel_headers['Proxy-Connection'] = 'Keep-Alive' - - request = "CONNECT %s:%d HTTP/1.1\r\n" % (self.host, self.port) - for header, value in self._tunnel_headers.iteritems(): - request += "%s: %s\r\n" % (header, value) - self.send(request + "\r\n") - - response = self.response_class(self.sock, strict=self.strict, - method=self._method) - (version, code, message) = response._read_status() - - status_line = u"%s %s %s" % (version, code, message.rstrip()) - headers = [status_line] - - if self.debuglevel in [-1, 5]: - print '%s: Urllib2 %s Debug Read' % (__name__, self._debug_protocol) - print u" %s" % status_line - - content_length = 0 - close_connection = False - while True: - line = response.fp.readline() - if line == '\r\n': break - - headers.append(line.rstrip()) - - parts = line.rstrip().split(': ', 1) - name = parts[0].lower() - value = parts[1].lower().strip() - if name == 'content-length': - content_length = int(value) - - if name in ['connection', 'proxy-connection'] and value == 'close': - close_connection = True - - if self.debuglevel in [-1, 5]: - print u" %s" % line.rstrip() - - # Handle proxy auth for SSL connections since regular urllib2 punts on this - if code == 407 and self.passwd and ('Proxy-Authorization' not in self._tunnel_headers or ntlm_follow_up): - if content_length: - response._safe_read(content_length) - - supported_auth_methods = {} - for line in headers: - parts = line.split(': ', 1) - if parts[0].lower() != 'proxy-authenticate': - continue - details = parts[1].split(' ', 1) - supported_auth_methods[details[0].lower()] = details[1] if len(details) > 1 else '' - - username, password = self.passwd.find_user_password(None, "%s:%s" % ( - self._proxy_host, self._proxy_port)) - - do_ntlm_follow_up = False - - if 'digest' in supported_auth_methods: - response_value = self.build_digest_response( - supported_auth_methods['digest'], username, password) - if response_value: - self._tunnel_headers['Proxy-Authorization'] = u"Digest %s" % response_value - - elif 'basic' in supported_auth_methods: - response_value = u"%s:%s" % (username, password) - response_value = base64.b64encode(response_value).strip() - self._tunnel_headers['Proxy-Authorization'] = u"Basic %s" % response_value - - elif 'ntlm' in supported_auth_methods and os.name == 'nt': - ntlm_challenge = supported_auth_methods['ntlm'] - if not len(ntlm_challenge): - type1_flags = ntlm.NTLM_TYPE1_FLAGS - if username.find('\\') == -1: - type1_flags &= ~ntlm.NTLM_NegotiateOemDomainSupplied - - negotiate_message = ntlm.create_NTLM_NEGOTIATE_MESSAGE(username, type1_flags) - self._tunnel_headers['Proxy-Authorization'] = 'NTLM %s' % negotiate_message - do_ntlm_follow_up = True - else: - domain = '' - user = username - if username.find('\\') != -1: - domain, user = username.split('\\', 1) - - challenge, negotiate_flags = ntlm.parse_NTLM_CHALLENGE_MESSAGE(ntlm_challenge) - self._tunnel_headers['Proxy-Authorization'] = 'NTLM %s' % ntlm.create_NTLM_AUTHENTICATE_MESSAGE(challenge, user, - domain, password, negotiate_flags) - - if 'Proxy-Authorization' in self._tunnel_headers: - self.host = self._proxy_host - self.port = self._proxy_port - - # If the proxy wanted the connection closed, we need to make a new connection - if close_connection: - self.sock.close() - self.sock = socket.create_connection((self.host, self.port), self.timeout) - - return self._tunnel(do_ntlm_follow_up) - - if code != 200: - self.close() - raise socket.error("Tunnel connection failed: %d %s" % (code, - message.strip())) - - def build_digest_response(self, fields, username, password): - """ - Takes a Proxy-Authenticate: Digest header and creates a response - header - - :param fields: - The string portion of the Proxy-Authenticate header after - "Digest " - - :param username: - The username to use for the response - - :param password: - The password to use for the response - - :return: - None if invalid Proxy-Authenticate header, otherwise the - string of fields for the Proxy-Authorization: Digest header - """ - - fields = urllib2.parse_keqv_list(urllib2.parse_http_list(fields)) - - realm = fields.get('realm') - nonce = fields.get('nonce') - qop = fields.get('qop') - algorithm = fields.get('algorithm') - if algorithm: - algorithm = algorithm.lower() - opaque = fields.get('opaque') - - if algorithm in ['md5', None]: - def hash(string): - return hashlib.md5(string).hexdigest() - elif algorithm == 'sha': - def hash(string): - return hashlib.sha1(string).hexdigest() - else: - return None - - host_port = u"%s:%s" % (self.host, self.port) - - a1 = "%s:%s:%s" % (username, realm, password) - a2 = "CONNECT:%s" % host_port - ha1 = hash(a1) - ha2 = hash(a2) - - if qop == None: - response = hash(u"%s:%s:%s" % (ha1, nonce, ha2)) - elif qop == 'auth': - nc = '00000001' - cnonce = hash(urllib2.randombytes(8))[:8] - response = hash(u"%s:%s:%s:%s:%s:%s" % (ha1, nonce, nc, cnonce, qop, ha2)) - else: - return None - - response_fields = { - 'username': username, - 'realm': realm, - 'nonce': nonce, - 'response': response, - 'uri': host_port - } - if algorithm: - response_fields['algorithm'] = algorithm - if qop == 'auth': - response_fields['nc'] = nc - response_fields['cnonce'] = cnonce - response_fields['qop'] = qop - if opaque: - response_fields['opaque'] = opaque - - return ', '.join([u"%s=\"%s\"" % (field, response_fields[field]) for field in response_fields]) - - def connect(self): - """ - Adds debugging and SSL certification validation - """ - - if self.debuglevel == -1: - print '%s: Urllib2 HTTPS Debug General' % __name__ - print u" Connecting to %s on port %s" % (self.host, self.port) - - self.sock = socket.create_connection((self.host, self.port), self.timeout) - if self._tunnel_host: - self._tunnel() - - if self.debuglevel == -1: - print u"%s: Urllib2 HTTPS Debug General" % __name__ - print u" Connecting to %s on port %s" % (self.host, self.port) - print u" CA certs file at %s" % (self.ca_certs) - - self.sock = ssl.wrap_socket(self.sock, keyfile=self.key_file, - certfile=self.cert_file, cert_reqs=self.cert_reqs, - ca_certs=self.ca_certs) - - if self.debuglevel == -1: - print u" Successfully upgraded connection to %s:%s with SSL" % ( - self.host, self.port) - - # This debugs and validates the SSL certificate - if self.cert_reqs & ssl.CERT_REQUIRED: - cert = self.sock.getpeercert() - - if self.debuglevel == -1: - subjectMap = { - 'organizationName': 'O', - 'commonName': 'CN', - 'organizationalUnitName': 'OU', - 'countryName': 'C', - 'serialNumber': 'serialNumber', - 'commonName': 'CN', - 'localityName': 'L', - 'stateOrProvinceName': 'S' - } - subject_list = list(cert['subject']) - subject_list.reverse() - subject_parts = [] - for pair in subject_list: - if pair[0][0] in subjectMap: - field_name = subjectMap[pair[0][0]] - else: - field_name = pair[0][0] - subject_parts.append(field_name + '=' + pair[0][1]) - - print u" Server SSL certificate:" - print u" subject: " + ','.join(subject_parts) - if 'subjectAltName' in cert: - print u" common name: " + cert['subjectAltName'][0][1] - if 'notAfter' in cert: - print u" expire date: " + cert['notAfter'] - - hostname = self.host.split(':', 0)[0] - - if not self.validate_cert_host(cert, hostname): - if self.debuglevel == -1: - print u" Certificate INVALID" - - raise InvalidCertificateException(hostname, cert, - 'hostname mismatch') - - if self.debuglevel == -1: - print u" Certificate validated for %s" % hostname - - if hasattr(urllib2, 'HTTPSHandler'): - class ValidatingHTTPSHandler(urllib2.HTTPSHandler): - """ - A urllib2 handler that validates SSL certificates for HTTPS requests - """ - - def __init__(self, **kwargs): - # This is a special value that will not trigger the standard debug - # functionality, but custom code where we can format the output - self._debuglevel = 0 - if 'debug' in kwargs and kwargs['debug']: - self._debuglevel = 5 - elif 'debuglevel' in kwargs: - self._debuglevel = kwargs['debuglevel'] - self._connection_args = kwargs - - def https_open(self, req): - def http_class_wrapper(host, **kwargs): - full_kwargs = dict(self._connection_args) - full_kwargs.update(kwargs) - return ValidatingHTTPSConnection(host, **full_kwargs) - - try: - return self.do_open(http_class_wrapper, req) - except urllib2.URLError, e: - if type(e.reason) == ssl.SSLError and e.reason.args[0] == 1: - raise InvalidCertificateException(req.host, '', - e.reason.args[1]) - raise - - https_request = urllib2.AbstractHTTPHandler.do_request_ - -except (ImportError): - pass - - -def preferences_filename(): - """:return: The appropriate settings filename based on the version of Sublime Text""" - - if int(sublime.version()) >= 2174: - return 'Preferences.sublime-settings' - return 'Global.sublime-settings' - - -class ThreadProgress(): - """ - Animates an indicator, [= ], in the status area while a thread runs - - :param thread: - The thread to track for activity - - :param message: - The message to display next to the activity indicator - - :param success_message: - The message to display once the thread is complete - """ - - def __init__(self, thread, message, success_message): - self.thread = thread - self.message = message - self.success_message = success_message - self.addend = 1 - self.size = 8 - sublime.set_timeout(lambda: self.run(0), 100) - - def run(self, i): - if not self.thread.is_alive(): - if hasattr(self.thread, 'result') and not self.thread.result: - sublime.status_message('') - return - sublime.status_message(self.success_message) - return - - before = i % self.size - after = (self.size - 1) - before - - sublime.status_message('%s [%s=%s]' % \ - (self.message, ' ' * before, ' ' * after)) - - if not after: - self.addend = -1 - if not before: - self.addend = 1 - i += self.addend - - sublime.set_timeout(lambda: self.run(i), 100) - - -class PlatformComparator(): - def get_best_platform(self, platforms): - ids = [sublime.platform() + '-' + sublime.arch(), sublime.platform(), - '*'] - - for id in ids: - if id in platforms: - return id - - return None - - -class ChannelProvider(PlatformComparator): - """ - Retrieves a channel and provides an API into the information - - The current channel/repository infrastructure caches repository info into - the channel to improve the Package Control client performance. This also - has the side effect of lessening the load on the GitHub and BitBucket APIs - and getting around not-infrequent HTTP 503 errors from those APIs. - - :param channel: - The URL of the channel - - :param package_manager: - An instance of :class:`PackageManager` used to download the file - """ - - def __init__(self, channel, package_manager): - self.channel_info = None - self.channel = channel - self.package_manager = package_manager - self.unavailable_packages = [] - - def match_url(self): - """Indicates if this provider can handle the provided channel""" - - return True - - def fetch_channel(self): - """Retrieves and loads the JSON for other methods to use""" - - if self.channel_info != None: - return - - channel_json = self.package_manager.download_url(self.channel, - 'Error downloading channel.') - if channel_json == False: - self.channel_info = False - return - - try: - channel_info = json.loads(channel_json) - except (ValueError): - print '%s: Error parsing JSON from channel %s.' % (__name__, - self.channel) - channel_info = False - - self.channel_info = channel_info - - def get_name_map(self): - """:return: A dict of the mapping for URL slug -> package name""" - - self.fetch_channel() - if self.channel_info == False: - return False - return self.channel_info.get('package_name_map', {}) - - def get_renamed_packages(self): - """:return: A dict of the packages that have been renamed""" - - self.fetch_channel() - if self.channel_info == False: - return False - return self.channel_info.get('renamed_packages', {}) - - def get_repositories(self): - """:return: A list of the repository URLs""" - - self.fetch_channel() - if self.channel_info == False: - return False - return self.channel_info['repositories'] - - def get_certs(self): - """ - Provides a secure way for distribution of SSL CA certificates - - Unfortunately Python does not include a bundle of CA certs with urllib2 - to perform SSL certificate validation. To circumvent this issue, - Package Control acts as a distributor of the CA certs for all HTTPS - URLs of package downloads. - - The default channel scrapes and caches info about all packages - periodically, and in the process it checks the CA certs for all of - the HTTPS URLs listed in the repositories. The contents of the CA cert - files are then hashed, and the CA cert is stored in a filename with - that hash. This is a fingerprint to ensure that Package Control has - the appropriate CA cert for a domain name. - - Next, the default channel file serves up a JSON object of the domain - names and the hashes of their current CA cert files. If Package Control - does not have the appropriate hash for a domain, it may retrieve it - from the channel server. To ensure that Package Control is talking to - a trusted authority to get the CA certs from, the CA cert for - sublime.wbond.net is bundled with Package Control. Then when downloading - the channel file, Package Control can ensure that the channel file's - SSL certificate is valid, thus ensuring the resulting CA certs are - legitimate. - - As a matter of optimization, the distribution of Package Control also - includes the current CA certs for all known HTTPS domains that are - included in the channel, as of the time when Package Control was - last released. - - :return: A dict of {'Domain Name': ['cert_file_hash', 'cert_file_download_url']} - """ - - self.fetch_channel() - if self.channel_info == False: - return False - return self.channel_info.get('certs', {}) - - def get_packages(self, repo): - """ - Provides access to the repository info that is cached in a channel - - :param repo: - The URL of the repository to get the cached info of - - :return: - A dict in the format: - { - 'Package Name': { - # Package details - see example-packages.json for format - }, - ... - } - or False if there is an error - """ - - self.fetch_channel() - if self.channel_info == False: - return False - if self.channel_info.get('packages', False) == False: - return False - if self.channel_info['packages'].get(repo, False) == False: - return False - - output = {} - for package in self.channel_info['packages'][repo]: - copy = package.copy() - - platforms = copy['platforms'].keys() - best_platform = self.get_best_platform(platforms) - - if not best_platform: - self.unavailable_packages.append(copy['name']) - continue - - copy['downloads'] = copy['platforms'][best_platform] - - del copy['platforms'] - - copy['url'] = copy['homepage'] - del copy['homepage'] - - output[copy['name']] = copy - - return output - - def get_unavailable_packages(self): - """ - Provides a list of packages that are unavailable for the current - platform/architecture that Sublime Text is running on. - - This list will be empty unless get_packages() is called first. - - :return: A list of package names - """ - - return self.unavailable_packages - - -# The providers (in order) to check when trying to download a channel -_channel_providers = [ChannelProvider] - - -class PackageProvider(PlatformComparator): - """ - Generic repository downloader that fetches package info - - With the current channel/repository architecture where the channel file - caches info from all includes repositories, these package providers just - serve the purpose of downloading packages not in the default channel. - - The structure of the JSON a repository should contain is located in - example-packages.json. - - :param repo: - The URL of the package repository - - :param package_manager: - An instance of :class:`PackageManager` used to download the file - """ - def __init__(self, repo, package_manager): - self.repo_info = None - self.repo = repo - self.package_manager = package_manager - self.unavailable_packages = [] - - def match_url(self): - """Indicates if this provider can handle the provided repo""" - - return True - - def fetch_repo(self): - """Retrieves and loads the JSON for other methods to use""" - - if self.repo_info != None: - return - - repository_json = self.package_manager.download_url(self.repo, - 'Error downloading repository.') - if repository_json == False: - self.repo_info = False - return - - try: - self.repo_info = json.loads(repository_json) - except (ValueError): - print '%s: Error parsing JSON from repository %s.' % (__name__, - self.repo) - self.repo_info = False - - def get_packages(self): - """ - Provides access to the repository info that is cached in a channel - - :return: - A dict in the format: - { - 'Package Name': { - # Package details - see example-packages.json for format - }, - ... - } - or False if there is an error - """ - - self.fetch_repo() - if self.repo_info == False: - return False - - output = {} - - for package in self.repo_info['packages']: - - platforms = package['platforms'].keys() - best_platform = self.get_best_platform(platforms) - - if not best_platform: - self.unavailable_packages.append(package['name']) - continue - - # Rewrites the legacy "zipball" URLs to the new "zip" format - downloads = package['platforms'][best_platform] - rewritten_downloads = [] - for download in downloads: - download['url'] = re.sub( - '^(https://nodeload.github.com/[^/]+/[^/]+/)zipball(/.*)$', - '\\1zip\\2', download['url']) - rewritten_downloads.append(download) - - info = { - 'name': package['name'], - 'description': package.get('description'), - 'url': package.get('homepage', self.repo), - 'author': package.get('author'), - 'last_modified': package.get('last_modified'), - 'downloads': rewritten_downloads - } - - output[package['name']] = info - - return output - - def get_renamed_packages(self): - """:return: A dict of the packages that have been renamed""" - - return self.repo_info.get('renamed_packages', {}) - - def get_unavailable_packages(self): - """ - Provides a list of packages that are unavailable for the current - platform/architecture that Sublime Text is running on. - - This list will be empty unless get_packages() is called first. - - :return: A list of package names - """ - - return self.unavailable_packages - - -class NonCachingProvider(): - """ - Base for package providers that do not need to cache the JSON - """ - - def fetch_json(self, url): - """ - Retrieves and parses the JSON from a URL - - :return: A dict or list from the JSON, or False on error - """ - - repository_json = self.package_manager.download_url(url, - 'Error downloading repository.') - if repository_json == False: - return False - try: - return json.loads(repository_json) - except (ValueError): - print '%s: Error parsing JSON from repository %s.' % (__name__, - url) - return False - - def get_unavailable_packages(self): - """ - Method for compatibility with PackageProvider class. These providers - are based on API calls, and thus do not support different platform - downloads, making it impossible for there to be unavailable packages. - - :return: An empty list - """ - - return [] - - -class GitHubPackageProvider(NonCachingProvider): - """ - Allows using a public GitHub repository as the source for a single package - - :param repo: - The public web URL to the GitHub repository. Should be in the format - `https://github.com/user/package` for the master branch, or - `https://github.com/user/package/tree/{branch_name}` for any other - branch. - - :param package_manager: - An instance of :class:`PackageManager` used to access the API - """ - - def __init__(self, repo, package_manager): - # Clean off the trailing .git to be more forgiving - self.repo = re.sub('\.git$', '', repo) - self.package_manager = package_manager - - def match_url(self): - """Indicates if this provider can handle the provided repo""" - - master = re.search('^https?://github.com/[^/]+/[^/]+/?$', self.repo) - branch = re.search('^https?://github.com/[^/]+/[^/]+/tree/[^/]+/?$', - self.repo) - return master != None or branch != None - - def get_packages(self): - """Uses the GitHub API to construct necessary info for a package""" - - branch = 'master' - branch_match = re.search( - '^https?://github.com/[^/]+/[^/]+/tree/([^/]+)/?$', self.repo) - if branch_match != None: - branch = branch_match.group(1) - - api_url = re.sub('^https?://github.com/([^/]+)/([^/]+)($|/.*$)', - 'https://api.github.com/repos/\\1/\\2', self.repo) - - repo_info = self.fetch_json(api_url) - if repo_info == False: - return False - - # In addition to hitting the main API endpoint for this repo, we - # also have to list the commits to get the timestamp of the last - # commit since we use that to generate a version number - commit_api_url = api_url + '/commits?' + \ - urllib.urlencode({'sha': branch, 'per_page': 1}) - - commit_info = self.fetch_json(commit_api_url) - if commit_info == False: - return False - - # We specifically use nodeload.github.com here because the download - # URLs all redirect there, and some of the downloaders don't follow - # HTTP redirect headers - download_url = 'https://nodeload.github.com/' + \ - repo_info['owner']['login'] + '/' + \ - repo_info['name'] + '/zip/' + urllib.quote(branch) - - commit_date = commit_info[0]['commit']['committer']['date'] - timestamp = datetime.datetime.strptime(commit_date[0:19], - '%Y-%m-%dT%H:%M:%S') - utc_timestamp = timestamp.strftime( - '%Y.%m.%d.%H.%M.%S') - - homepage = repo_info['homepage'] - if not homepage: - homepage = repo_info['html_url'] - - package = { - 'name': repo_info['name'], - 'description': repo_info['description'] if \ - repo_info['description'] else 'No description provided', - 'url': homepage, - 'author': repo_info['owner']['login'], - 'last_modified': timestamp.strftime('%Y-%m-%d %H:%M:%S'), - 'downloads': [ - { - 'version': utc_timestamp, - 'url': download_url - } - ] - } - return {package['name']: package} - - def get_renamed_packages(self): - """For API-compatibility with :class:`PackageProvider`""" - - return {} - - -class GitHubUserProvider(NonCachingProvider): - """ - Allows using a GitHub user/organization as the source for multiple packages - - :param repo: - The public web URL to the GitHub user/org. Should be in the format - `https://github.com/user`. - - :param package_manager: - An instance of :class:`PackageManager` used to access the API - """ - - def __init__(self, repo, package_manager): - self.repo = repo - self.package_manager = package_manager - - def match_url(self): - """Indicates if this provider can handle the provided repo""" - - return re.search('^https?://github.com/[^/]+/?$', self.repo) != None - - def get_packages(self): - """Uses the GitHub API to construct necessary info for all packages""" - - user_match = re.search('^https?://github.com/([^/]+)/?$', self.repo) - user = user_match.group(1) - - api_url = 'https://api.github.com/users/%s/repos?per_page=100' % user - - repo_info = self.fetch_json(api_url) - if repo_info == False: - return False - - packages = {} - for package_info in repo_info: - # All packages for the user are made available, and always from - # the master branch. Anything else requires a custom packages.json - commit_api_url = ('https://api.github.com/repos/%s/%s/commits' + \ - '?sha=master&per_page=1') % (user, package_info['name']) - - commit_info = self.fetch_json(commit_api_url) - if commit_info == False: - return False - - commit_date = commit_info[0]['commit']['committer']['date'] - timestamp = datetime.datetime.strptime(commit_date[0:19], - '%Y-%m-%dT%H:%M:%S') - utc_timestamp = timestamp.strftime( - '%Y.%m.%d.%H.%M.%S') - - homepage = package_info['homepage'] - if not homepage: - homepage = package_info['html_url'] - - package = { - 'name': package_info['name'], - 'description': package_info['description'] if \ - package_info['description'] else 'No description provided', - 'url': homepage, - 'author': package_info['owner']['login'], - 'last_modified': timestamp.strftime('%Y-%m-%d %H:%M:%S'), - 'downloads': [ - { - 'version': utc_timestamp, - # We specifically use nodeload.github.com here because - # the download URLs all redirect there, and some of the - # downloaders don't follow HTTP redirect headers - 'url': 'https://nodeload.github.com/' + \ - package_info['owner']['login'] + '/' + \ - package_info['name'] + '/zip/master' - } - ] - } - packages[package['name']] = package - return packages - - def get_renamed_packages(self): - """For API-compatibility with :class:`PackageProvider`""" - - return {} - - -class BitBucketPackageProvider(NonCachingProvider): - """ - Allows using a public BitBucket repository as the source for a single package - - :param repo: - The public web URL to the BitBucket repository. Should be in the format - `https://bitbucket.org/user/package`. - - :param package_manager: - An instance of :class:`PackageManager` used to access the API - """ - - def __init__(self, repo, package_manager): - self.repo = repo - self.package_manager = package_manager - - def match_url(self): - """Indicates if this provider can handle the provided repo""" - - return re.search('^https?://bitbucket.org', self.repo) != None - - def get_packages(self): - """Uses the BitBucket API to construct necessary info for a package""" - - api_url = re.sub('^https?://bitbucket.org/', - 'https://api.bitbucket.org/1.0/repositories/', self.repo) - api_url = api_url.rstrip('/') - - repo_info = self.fetch_json(api_url) - if repo_info == False: - return False - - # Since HG allows for arbitrary main branch names, we have to hit - # this URL just to get that info - main_branch_url = api_url + '/main-branch/' - main_branch_info = self.fetch_json(main_branch_url) - if main_branch_info == False: - return False - - # Grabbing the changesets is necessary because we construct the - # version number from the last commit timestamp - changeset_url = api_url + '/changesets/' + main_branch_info['name'] - last_commit = self.fetch_json(changeset_url) - if last_commit == False: - return False - - commit_date = last_commit['timestamp'] - timestamp = datetime.datetime.strptime(commit_date[0:19], - '%Y-%m-%d %H:%M:%S') - utc_timestamp = timestamp.strftime( - '%Y.%m.%d.%H.%M.%S') - - homepage = repo_info['website'] - if not homepage: - homepage = self.repo - package = { - 'name': repo_info['name'], - 'description': repo_info['description'] if \ - repo_info['description'] else 'No description provided', - 'url': homepage, - 'author': repo_info['owner'], - 'last_modified': timestamp.strftime('%Y-%m-%d %H:%M:%S'), - 'downloads': [ - { - 'version': utc_timestamp, - 'url': self.repo + '/get/' + \ - last_commit['node'] + '.zip' - } - ] - } - return {package['name']: package} - - def get_renamed_packages(self): - """For API-compatibility with :class:`PackageProvider`""" - - return {} - - -# The providers (in order) to check when trying to download repository info -_package_providers = [BitBucketPackageProvider, GitHubPackageProvider, - GitHubUserProvider, PackageProvider] - - -class BinaryNotFoundError(Exception): - """If a necessary executable is not found in the PATH on the system""" - - pass - - -class NonCleanExitError(Exception): - """ - When an subprocess does not exit cleanly - - :param returncode: - The command line integer return code of the subprocess - """ - - def __init__(self, returncode): - self.returncode = returncode - - def __str__(self): - return repr(self.returncode) - - -class NonHttpError(Exception): - """If a downloader had a non-clean exit, but it was not due to an HTTP error""" - - pass - - -class Downloader(): - """ - A base downloader that actually performs downloading URLs - - The SSL module is not included with the bundled Python for Linux - users of Sublime Text, so Linux machines will fall back to using curl - or wget for HTTPS URLs. - """ - - def check_certs(self, domain, timeout): - """ - Ensures that the SSL CA cert for a domain is present on the machine - - :param domain: - The domain to ensure there is a CA cert for - - :param timeout: - The int timeout for downloading the CA cert from the channel - - :return: - The CA cert bundle path on success, or False on error - """ - - cert_match = False - - certs_list = self.settings.get('certs', {}) - certs_dir = os.path.join(sublime.packages_path(), 'Package Control', - 'certs') - ca_bundle_path = os.path.join(certs_dir, 'ca-bundle.crt') - - cert_info = certs_list.get(domain) - if cert_info: - cert_match = self.locate_cert(certs_dir, cert_info[0], cert_info[1]) - - wildcard_info = certs_list.get('*') - if wildcard_info: - cert_match = self.locate_cert(certs_dir, wildcard_info[0], wildcard_info[1]) or cert_match - - if not cert_match: - print '%s: No CA certs available for %s.' % (__name__, domain) - return False - - return ca_bundle_path - - def locate_cert(self, certs_dir, cert_id, location): - """ - Makes sure the SSL cert specified has been added to the CA cert - bundle that is present on the machine - - :param certs_dir: - The path of the folder that contains the cert files - - :param cert_id: - The identifier for CA cert(s). For those provided by the channel - system, this will be an md5 of the contents of the cert(s). For - user-provided certs, this is something they provide. - - :param location: - An http(s) URL, or absolute filesystem path to the CA cert(s) - - :return: - If the cert specified (by cert_id) is present on the machine and - part of the ca-bundle.crt file in the certs_dir - """ - - cert_path = os.path.join(certs_dir, cert_id) - if not os.path.exists(cert_path): - if str(location) != '': - if re.match('^https?://', location): - contents = self.download_cert(cert_id, location) - else: - contents = self.load_cert(cert_id, location) - if contents: - self.save_cert(certs_dir, cert_id, contents) - return True - return False - return True - - def download_cert(self, cert_id, url): - """ - Downloads CA cert(s) from a URL - - :param cert_id: - The identifier for CA cert(s). For those provided by the channel - system, this will be an md5 of the contents of the cert(s). For - user-provided certs, this is something they provide. - - :param url: - An http(s) URL to the CA cert(s) - - :return: - The contents of the CA cert(s) - """ - - cert_downloader = self.__class__(self.settings) - return cert_downloader.download(url, - 'Error downloading CA certs for %s.' % (domain), timeout, 1) - - def load_cert(self, cert_id, path): - """ - Copies CA cert(s) from a file path - - :param cert_id: - The identifier for CA cert(s). For those provided by the channel - system, this will be an md5 of the contents of the cert(s). For - user-provided certs, this is something they provide. - - :param path: - The absolute filesystem path to a file containing the CA cert(s) - - :return: - The contents of the CA cert(s) - """ - - if os.path.exists(path): - with open(path, 'rb') as f: - return f.read() - - def save_cert(self, certs_dir, cert_id, contents): - """ - Saves CA cert(s) to the certs_dir (and ca-bundle.crt file) - - :param certs_dir: - The path of the folder that contains the cert files - - :param cert_id: - The identifier for CA cert(s). For those provided by the channel - system, this will be an md5 of the contents of the cert(s). For - user-provided certs, this is something they provide. - - :param contents: - The contents of the CA cert(s) - """ - - ca_bundle_path = os.path.join(certs_dir, 'ca-bundle.crt') - cert_path = os.path.join(certs_dir, cert_id) - with open(cert_path, 'wb') as f: - f.write(contents) - with open(ca_bundle_path, 'ab') as f: - f.write("\n" + contents) - - def decode_response(self, encoding, response): - if encoding == 'gzip': - return gzip.GzipFile(fileobj=StringIO.StringIO(response)).read() - elif encoding == 'deflate': - decompresser = zlib.decompressobj(-zlib.MAX_WBITS) - return decompresser.decompress(response) + decompresser.flush() - return response - - -class CliDownloader(Downloader): - """ - Base for downloaders that use a command line program - - :param settings: - A dict of the various Package Control settings. The Sublime Text - Settings API is not used because this code is run in a thread. - """ - - def __init__(self, settings): - self.settings = settings - - def clean_tmp_file(self): - if os.path.exists(self.tmp_file): - os.remove(self.tmp_file) - - def find_binary(self, name): - """ - Finds the given executable name in the system PATH - - :param name: - The exact name of the executable to find - - :return: - The absolute path to the executable - - :raises: - BinaryNotFoundError when the executable can not be found - """ - - for dir in os.environ['PATH'].split(os.pathsep): - path = os.path.join(dir, name) - if os.path.exists(path): - return path - - raise BinaryNotFoundError('The binary %s could not be located' % name) - - def execute(self, args): - """ - Runs the executable and args and returns the result - - :param args: - A list of the executable path and all arguments to be passed to it - - :return: - The text output of the executable - - :raises: - NonCleanExitError when the executable exits with an error - """ - - if self.settings.get('debug'): - print u"%s: Trying to execute command %s" % ( - __name__, create_cmd(args)) - - proc = subprocess.Popen(args, stdin=subprocess.PIPE, - stdout=subprocess.PIPE, stderr=subprocess.PIPE) - - output = proc.stdout.read() - self.stderr = proc.stderr.read() - returncode = proc.wait() - if returncode != 0: - error = NonCleanExitError(returncode) - error.stderr = self.stderr - error.stdout = output - raise error - return output - - -class UrlLib2Downloader(Downloader): - """ - A downloader that uses the Python urllib2 module - - :param settings: - A dict of the various Package Control settings. The Sublime Text - Settings API is not used because this code is run in a thread. - """ - - def __init__(self, settings): - self.settings = settings - - def download(self, url, error_message, timeout, tries): - """ - Downloads a URL and returns the contents - - Uses the proxy settings from the Package Control.sublime-settings file, - however there seem to be a decent number of proxies that this code - does not work with. Patches welcome! - - :param url: - The URL to download - - :param error_message: - A string to include in the console error that is printed - when an error occurs - - :param timeout: - The int number of seconds to set the timeout to - - :param tries: - The int number of times to try and download the URL in the case of - a timeout or HTTP 503 error - - :return: - The string contents of the URL, or False on error - """ - - http_proxy = self.settings.get('http_proxy') - https_proxy = self.settings.get('https_proxy') - if http_proxy or https_proxy: - proxies = {} - if http_proxy: - proxies['http'] = http_proxy - if https_proxy: - proxies['https'] = https_proxy - proxy_handler = urllib2.ProxyHandler(proxies) - else: - proxy_handler = urllib2.ProxyHandler() - - password_manager = urllib2.HTTPPasswordMgrWithDefaultRealm() - proxy_username = self.settings.get('proxy_username') - proxy_password = self.settings.get('proxy_password') - if proxy_username and proxy_password: - if http_proxy: - password_manager.add_password(None, http_proxy, proxy_username, - proxy_password) - if https_proxy: - password_manager.add_password(None, https_proxy, proxy_username, - proxy_password) - - handlers = [proxy_handler] - if os.name == 'nt': - ntlm_auth_handler = ProxyNtlmAuthHandler(password_manager) - handlers.append(ntlm_auth_handler) - - basic_auth_handler = urllib2.ProxyBasicAuthHandler(password_manager) - digest_auth_handler = urllib2.ProxyDigestAuthHandler(password_manager) - handlers.extend([digest_auth_handler, basic_auth_handler]) - - debug = self.settings.get('debug') - - if debug: - print u"%s: Urllib2 Debug Proxy" % __name__ - print u" http_proxy: %s" % http_proxy - print u" https_proxy: %s" % https_proxy - print u" proxy_username: %s" % proxy_username - print u" proxy_password: %s" % proxy_password - - secure_url_match = re.match('^https://([^/]+)', url) - if secure_url_match != None: - secure_domain = secure_url_match.group(1) - bundle_path = self.check_certs(secure_domain, timeout) - if not bundle_path: - return False - bundle_path = bundle_path.encode(sys.getfilesystemencoding()) - handlers.append(ValidatingHTTPSHandler(ca_certs=bundle_path, - debug=debug, passwd=password_manager, - user_agent=self.settings.get('user_agent'))) - else: - handlers.append(DebuggableHTTPHandler(debug=debug, - passwd=password_manager)) - urllib2.install_opener(urllib2.build_opener(*handlers)) - - while tries > 0: - tries -= 1 - try: - request = urllib2.Request(url, headers={ - "User-Agent": self.settings.get('user_agent'), - # Don't be alarmed if the response from the server does not - # select one of these since the server runs a relatively new - # version of OpenSSL which supports compression on the SSL - # layer, and Apache will use that instead of HTTP-level - # encoding. - "Accept-Encoding": "gzip,deflate"}) - http_file = urllib2.urlopen(request, timeout=timeout) - self.handle_rate_limit(http_file, url) - result = http_file.read() - encoding = http_file.headers.get('Content-Encoding') - return self.decode_response(encoding, result) - - except (httplib.HTTPException) as (e): - print '%s: %s HTTP exception %s (%s) downloading %s.' % ( - __name__, error_message, e.__class__.__name__, - unicode_from_os(e), url) - - except (urllib2.HTTPError) as (e): - # Make sure we obey Github's rate limiting headers - self.handle_rate_limit(e, url) - - # Bitbucket and Github return 503 a decent amount - if unicode_from_os(e.code) == '503': - print ('%s: Downloading %s was rate limited, ' + - 'trying again') % (__name__, url) - continue - print '%s: %s HTTP error %s downloading %s.' % (__name__, - error_message, unicode_from_os(e.code), url) - - except (urllib2.URLError) as (e): - - # Bitbucket and Github timeout a decent amount - if unicode_from_os(e.reason) == 'The read operation timed out' \ - or unicode_from_os(e.reason) == 'timed out': - print (u'%s: Downloading %s timed out, trying ' + - u'again') % (__name__, url) - continue - print u'%s: %s URL error %s downloading %s.' % (__name__, - error_message, unicode_from_os(e.reason), url) - break - return False - - def handle_rate_limit(self, response, url): - """ - Checks the headers of a respone object to make sure we are obeying the - rate limit - - :param response: - The response object that has a headers dict - - :param url: - The URL that was requested - - :raises: - RateLimitException when the rate limit has been hit - """ - - limit_remaining = response.headers.get('X-RateLimit-Remaining', 1) - if str(limit_remaining) == '0': - hostname = urlparse.urlparse(url).hostname - limit = response.headers.get('X-RateLimit-Limit', 1) - raise RateLimitException(hostname, limit) - - -class WgetDownloader(CliDownloader): - """ - A downloader that uses the command line program wget - - :param settings: - A dict of the various Package Control settings. The Sublime Text - Settings API is not used because this code is run in a thread. - """ - - def __init__(self, settings): - self.settings = settings - self.debug = settings.get('debug') - self.wget = self.find_binary('wget') - - def download(self, url, error_message, timeout, tries): - """ - Downloads a URL and returns the contents - - :param url: - The URL to download - - :param error_message: - A string to include in the console error that is printed - when an error occurs - - :param timeout: - The int number of seconds to set the timeout to - - :param tries: - The int number of times to try and download the URL in the case of - a timeout or HTTP 503 error - - :return: - The string contents of the URL, or False on error - """ - - if not self.wget: - return False - - self.tmp_file = tempfile.NamedTemporaryFile().name - command = [self.wget, '--connect-timeout=' + str(int(timeout)), '-o', - self.tmp_file, '-O', '-', '-U', - self.settings.get('user_agent'), '--header', - # Don't be alarmed if the response from the server does not select - # one of these since the server runs a relatively new version of - # OpenSSL which supports compression on the SSL layer, and Apache - # will use that instead of HTTP-level encoding. - 'Accept-Encoding: gzip,deflate'] - - secure_url_match = re.match('^https://([^/]+)', url) - if secure_url_match != None: - secure_domain = secure_url_match.group(1) - bundle_path = self.check_certs(secure_domain, timeout) - if not bundle_path: - return False - command.append(u'--ca-certificate=' + bundle_path) - - if self.debug: - command.append('-d') - else: - command.append('-S') - - http_proxy = self.settings.get('http_proxy') - https_proxy = self.settings.get('https_proxy') - proxy_username = self.settings.get('proxy_username') - proxy_password = self.settings.get('proxy_password') - - if proxy_username: - command.append(u"--proxy-user=%s" % proxy_username) - if proxy_password: - command.append(u"--proxy-password=%s" % proxy_password) - - if self.debug: - print u"%s: Wget Debug Proxy" % __name__ - print u" http_proxy: %s" % http_proxy - print u" https_proxy: %s" % https_proxy - print u" proxy_username: %s" % proxy_username - print u" proxy_password: %s" % proxy_password - - command.append(url) - - if http_proxy: - os.putenv('http_proxy', http_proxy) - if https_proxy: - os.putenv('https_proxy', https_proxy) - - while tries > 0: - tries -= 1 - try: - result = self.execute(command) - - general, headers = self.parse_output() - encoding = headers.get('content-encoding') - if encoding: - result = self.decode_response(encoding, result) - - return result - - except (NonCleanExitError) as (e): - - try: - general, headers = self.parse_output() - self.handle_rate_limit(headers, url) - - if general['status'] == '503': - # GitHub and BitBucket seem to rate limit via 503 - print ('%s: Downloading %s was rate limited' + - ', trying again') % (__name__, url) - continue - - error_string = 'HTTP error %s %s' % (general['status'], - general['message']) - - except (NonHttpError) as (e): - - error_string = unicode_from_os(e) - - # GitHub and BitBucket seem to time out a lot - if error_string.find('timed out') != -1: - print ('%s: Downloading %s timed out, ' + - 'trying again') % (__name__, url) - continue - - print (u'%s: %s %s downloading %s.' % (__name__, error_message, - error_string, url)).encode('UTF-8') - - break - return False - - def parse_output(self): - with open(self.tmp_file, 'r') as f: - output = f.read().splitlines() - self.clean_tmp_file() - - error = None - header_lines = [] - if self.debug: - section = 'General' - last_section = None - for line in output: - if section == 'General': - if self.skippable_line(line): - continue - - # Skip blank lines - if line.strip() == '': - continue - - # Error lines - if line[0:5] == 'wget:': - error = line[5:].strip() - if line[0:7] == 'failed:': - error = line[7:].strip() - - if line == '---request begin---': - section = 'Write' - continue - elif line == '---request end---': - section = 'General' - continue - elif line == '---response begin---': - section = 'Read' - continue - elif line == '---response end---': - section = 'General' - continue - - if section != last_section: - print "%s: Wget HTTP Debug %s" % (__name__, section) - - if section == 'Read': - header_lines.append(line) - - print ' ' + line - last_section = section - - else: - for line in output: - if self.skippable_line(line): - continue - - # Check the resolving and connecting to lines for errors - if re.match('(Resolving |Connecting to )', line): - failed_match = re.search(' failed: (.*)$', line) - if failed_match: - error = failed_match.group(1).strip() - - # Error lines - if line[0:5] == 'wget:': - error = line[5:].strip() - if line[0:7] == 'failed:': - error = line[7:].strip() - - if line[0:2] == ' ': - header_lines.append(line.lstrip()) - - if error: - raise NonHttpError(error) - - return self.parse_headers(header_lines) - - def skippable_line(self, line): - # Skip date lines - if re.match('--\d{4}-\d{2}-\d{2}', line): - return True - if re.match('\d{4}-\d{2}-\d{2}', line): - return True - # Skip HTTP status code lines since we already have that info - if re.match('\d{3} ', line): - return True - # Skip Saving to and progress lines - if re.match('(Saving to:|\s*\d+K)', line): - return True - # Skip notice about ignoring body on HTTP error - if re.match('Skipping \d+ byte', line): - return True - - def parse_headers(self, output=None): - if not output: - with open(self.tmp_file, 'r') as f: - output = f.read().splitlines() - self.clean_tmp_file() - - general = { - 'version': '0.9', - 'status': '200', - 'message': 'OK' - } - headers = {} - for line in output: - # When using the -S option, headers have two spaces before them, - # additionally, valid headers won't have spaces, so this is always - # a safe operation to perform - line = line.lstrip() - if line.find('HTTP/') == 0: - match = re.match('HTTP/(\d\.\d)\s+(\d+)\s+(.*)$', line) - general['version'] = match.group(1) - general['status'] = match.group(2) - general['message'] = match.group(3) - else: - name, value = line.split(':', 1) - headers[name.lower()] = value.strip() - - return (general, headers) - - def handle_rate_limit(self, headers, url): - limit_remaining = headers.get('x-ratelimit-remaining', '1') - limit = headers.get('x-ratelimit-limit', '1') - - if str(limit_remaining) == '0': - hostname = urlparse.urlparse(url).hostname - raise RateLimitException(hostname, limit) - - -class CurlDownloader(CliDownloader): - """ - A downloader that uses the command line program curl - - :param settings: - A dict of the various Package Control settings. The Sublime Text - Settings API is not used because this code is run in a thread. - """ - - def __init__(self, settings): - self.settings = settings - self.curl = self.find_binary('curl') - - def download(self, url, error_message, timeout, tries): - """ - Downloads a URL and returns the contents - - :param url: - The URL to download - - :param error_message: - A string to include in the console error that is printed - when an error occurs - - :param timeout: - The int number of seconds to set the timeout to - - :param tries: - The int number of times to try and download the URL in the case of - a timeout or HTTP 503 error - - :return: - The string contents of the URL, or False on error - """ - - if not self.curl: - return False - - self.tmp_file = tempfile.NamedTemporaryFile().name - command = [self.curl, '--user-agent', self.settings.get('user_agent'), - '--connect-timeout', str(int(timeout)), '-sSL', - # Don't be alarmed if the response from the server does not select - # one of these since the server runs a relatively new version of - # OpenSSL which supports compression on the SSL layer, and Apache - # will use that instead of HTTP-level encoding. - '--compressed', - # We have to capture the headers to check for rate limit info - '--dump-header', self.tmp_file] - - secure_url_match = re.match('^https://([^/]+)', url) - if secure_url_match != None: - secure_domain = secure_url_match.group(1) - bundle_path = self.check_certs(secure_domain, timeout) - if not bundle_path: - return False - command.extend(['--cacert', bundle_path]) - - debug = self.settings.get('debug') - if debug: - command.append('-v') - - http_proxy = self.settings.get('http_proxy') - https_proxy = self.settings.get('https_proxy') - proxy_username = self.settings.get('proxy_username') - proxy_password = self.settings.get('proxy_password') - - if debug: - print u"%s: Curl Debug Proxy" % __name__ - print u" http_proxy: %s" % http_proxy - print u" https_proxy: %s" % https_proxy - print u" proxy_username: %s" % proxy_username - print u" proxy_password: %s" % proxy_password - - if http_proxy or https_proxy: - command.append('--proxy-anyauth') - - if proxy_username or proxy_password: - command.extend(['-U', u"%s:%s" % (proxy_username, proxy_password)]) - - if http_proxy: - os.putenv('http_proxy', http_proxy) - if https_proxy: - os.putenv('HTTPS_PROXY', https_proxy) - - command.append(url) - - while tries > 0: - tries -= 1 - try: - output = self.execute(command) - - with open(self.tmp_file, 'r') as f: - headers = f.read() - self.clean_tmp_file() - - limit = 1 - limit_remaining = 1 - status = '200 OK' - for header in headers.splitlines(): - if header[0:5] == 'HTTP/': - status = re.sub('^HTTP/\d\.\d\s+', '', header) - if header.lower()[0:22] == 'x-ratelimit-remaining:': - limit_remaining = header.lower()[22:].strip() - if header.lower()[0:18] == 'x-ratelimit-limit:': - limit = header.lower()[18:].strip() - - if debug: - self.print_debug(self.stderr) - - if str(limit_remaining) == '0': - hostname = urlparse.urlparse(url).hostname - raise RateLimitException(hostname, limit) - - if status != '200 OK': - e = NonCleanExitError(22) - e.stderr = status - raise e - - return output - - except (NonCleanExitError) as (e): - # Stderr is used for both the error message and the debug info - # so we need to process it to extra the debug info - if self.settings.get('debug'): - e.stderr = self.print_debug(e.stderr) - - self.clean_tmp_file() - - if e.returncode == 22: - code = re.sub('^.*?(\d+)([\w\s]+)?$', '\\1', e.stderr) - if code == '503': - # GitHub and BitBucket seem to rate limit via 503 - print ('%s: Downloading %s was rate limited' + - ', trying again') % (__name__, url) - continue - error_string = 'HTTP error ' + code - elif e.returncode == 6: - error_string = 'URL error host not found' - elif e.returncode == 28: - # GitHub and BitBucket seem to time out a lot - print ('%s: Downloading %s timed out, trying ' + - 'again') % (__name__, url) - continue - else: - error_string = e.stderr.rstrip() - - print '%s: %s %s downloading %s.' % (__name__, error_message, - error_string, url) - break - - return False - - def print_debug(self, string): - section = 'General' - last_section = None - - output = '' - - for line in string.splitlines(): - # Placeholder for body of request - if line and line[0:2] == '{ ': - continue - - if len(line) > 1: - subtract = 0 - if line[0:2] == '* ': - section = 'General' - subtract = 2 - elif line[0:2] == '> ': - section = 'Write' - subtract = 2 - elif line[0:2] == '< ': - section = 'Read' - subtract = 2 - line = line[subtract:] - - # If the line does not start with "* ", "< ", "> " or " " - # then it is a real stderr message - if subtract == 0 and line[0:2] != ' ': - output += line - continue - - if line.strip() == '': - continue - - if section != last_section: - print "%s: Curl HTTP Debug %s" % (__name__, section) - - print ' ' + line - last_section = section - - return output - - -# A cache of channel and repository info to allow users to install multiple -# packages without having to wait for the metadata to be downloaded more -# than once. The keys are managed locally by the utilizing code. -_channel_repository_cache = {} - - -class RepositoryDownloader(threading.Thread): - """ - Downloads information about a repository in the background - - :param package_manager: - An instance of :class:`PackageManager` used to download files - - :param name_map: - The dict of name mapping for URL slug -> package name - - :param repo: - The URL of the repository to download info about - """ - - def __init__(self, package_manager, name_map, repo): - self.package_manager = package_manager - self.repo = repo - self.packages = {} - self.name_map = name_map - threading.Thread.__init__(self) - - def run(self): - for provider_class in _package_providers: - provider = provider_class(self.repo, self.package_manager) - if provider.match_url(): - break - packages = provider.get_packages() - if packages == False: - self.packages = False - return - - mapped_packages = {} - for package in packages.keys(): - mapped_package = self.name_map.get(package, package) - mapped_packages[mapped_package] = packages[package] - mapped_packages[mapped_package]['name'] = mapped_package - packages = mapped_packages - - self.packages = packages - self.renamed_packages = provider.get_renamed_packages() - self.unavailable_packages = provider.get_unavailable_packages() - - -class VcsUpgrader(): - """ - Base class for updating packages that are a version control repository on local disk - - :param vcs_binary: - The full filesystem path to the executable for the version control - system. May be set to None to allow the code to try and find it. - - :param update_command: - The command to pass to the version control executable to update the - repository. - - :param working_copy: - The local path to the working copy/package directory - - :param cache_length: - The lenth of time to cache if incoming changesets are available - """ - - def __init__(self, vcs_binary, update_command, working_copy, cache_length, debug): - self.binary = vcs_binary - self.update_command = update_command - self.working_copy = working_copy - self.cache_length = cache_length - self.debug = debug - - def execute(self, args, dir): - """ - Creates a subprocess with the executable/args - - :param args: - A list of the executable path and all arguments to it - - :param dir: - The directory in which to run the executable - - :return: A string of the executable output - """ - - startupinfo = None - if os.name == 'nt': - startupinfo = subprocess.STARTUPINFO() - startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW - - if self.debug: - print u"%s: Trying to execute command %s" % ( - __name__, create_cmd(args)) - proc = subprocess.Popen(args, stdin=subprocess.PIPE, - stdout=subprocess.PIPE, stderr=subprocess.STDOUT, - startupinfo=startupinfo, cwd=dir) - - return proc.stdout.read().replace('\r\n', '\n').rstrip(' \n\r') - - def find_binary(self, name): - """ - Locates the executable by looking in the PATH and well-known directories - - :param name: - The string filename of the executable - - :return: The filesystem path to the executable, or None if not found - """ - - if self.binary: - if self.debug: - print u"%s: Using \"%s_binary\" from settings \"%s\"" % ( - __name__, self.vcs_type, self.binary) - return self.binary - - # Try the path first - for dir in os.environ['PATH'].split(os.pathsep): - path = os.path.join(dir, name) - if os.path.exists(path): - if self.debug: - print u"%s: Found %s at \"%s\"" % (__name__, self.vcs_type, - path) - return path - - # This is left in for backwards compatibility and for windows - # users who may have the binary, albeit in a common dir that may - # not be part of the PATH - if os.name == 'nt': - dirs = ['C:\\Program Files\\Git\\bin', - 'C:\\Program Files (x86)\\Git\\bin', - 'C:\\Program Files\\TortoiseGit\\bin', - 'C:\\Program Files\\Mercurial', - 'C:\\Program Files (x86)\\Mercurial', - 'C:\\Program Files (x86)\\TortoiseHg', - 'C:\\Program Files\\TortoiseHg', - 'C:\\cygwin\\bin'] - else: - dirs = ['/usr/local/git/bin'] - - for dir in dirs: - path = os.path.join(dir, name) - if os.path.exists(path): - if self.debug: - print u"%s: Found %s at \"%s\"" % (__name__, self.vcs_type, - path) - return path - - if self.debug: - print u"%s: Could not find %s on your machine" % (__name__, - self.vcs_type) - return None - - -class GitUpgrader(VcsUpgrader): - """ - Allows upgrading a local git-repository-based package - """ - - vcs_type = 'git' - - def retrieve_binary(self): - """ - Returns the path to the git executable - - :return: The string path to the executable or False on error - """ - - name = 'git' - if os.name == 'nt': - name += '.exe' - binary = self.find_binary(name) - if binary and os.path.isdir(binary): - full_path = os.path.join(binary, name) - if os.path.exists(full_path): - binary = full_path - if not binary: - sublime.error_message(('%s: Unable to find %s. ' + - 'Please set the git_binary setting by accessing the ' + - 'Preferences > Package Settings > %s > ' + - u'Settings – User menu entry. The Settings – Default entry ' + - 'can be used for reference, but changes to that will be ' + - 'overwritten upon next upgrade.') % (__name__, name, __name__)) - return False - - if os.name == 'nt': - tortoise_plink = self.find_binary('TortoisePlink.exe') - if tortoise_plink: - os.environ.setdefault('GIT_SSH', tortoise_plink) - return binary - - def run(self): - """ - Updates the repository with remote changes - - :return: False or error, or True on success - """ - - binary = self.retrieve_binary() - if not binary: - return False - args = [binary] - args.extend(self.update_command) - self.execute(args, self.working_copy) - return True - - def incoming(self): - """:return: bool if remote revisions are available""" - - cache_key = self.working_copy + '.incoming' - working_copy_cache = _channel_repository_cache.get(cache_key) - if working_copy_cache and working_copy_cache.get('time') > \ - time.time(): - return working_copy_cache.get('data') - - binary = self.retrieve_binary() - if not binary: - return False - self.execute([binary, 'fetch'], self.working_copy) - args = [binary, 'log'] - args.append('..' + '/'.join(self.update_command[-2:])) - output = self.execute(args, self.working_copy) - incoming = len(output) > 0 - - _channel_repository_cache[cache_key] = { - 'time': time.time() + self.cache_length, - 'data': incoming - } - return incoming - - -class HgUpgrader(VcsUpgrader): - """ - Allows upgrading a local mercurial-repository-based package - """ - - vcs_type = 'hg' - - def retrieve_binary(self): - """ - Returns the path to the hg executable - - :return: The string path to the executable or False on error - """ - - name = 'hg' - if os.name == 'nt': - name += '.exe' - binary = self.find_binary(name) - if binary and os.path.isdir(binary): - full_path = os.path.join(binary, name) - if os.path.exists(full_path): - binary = full_path - if not binary: - sublime.error_message(('%s: Unable to find %s. ' + - 'Please set the hg_binary setting by accessing the ' + - 'Preferences > Package Settings > %s > ' + - u'Settings – User menu entry. The Settings – Default entry ' + - 'can be used for reference, but changes to that will be ' + - 'overwritten upon next upgrade.') % (__name__, name, __name__)) - return False - return binary - - def run(self): - """ - Updates the repository with remote changes - - :return: False or error, or True on success - """ - - binary = self.retrieve_binary() - if not binary: - return False - args = [binary] - args.extend(self.update_command) - self.execute(args, self.working_copy) - return True - - def incoming(self): - """:return: bool if remote revisions are available""" - - cache_key = self.working_copy + '.incoming' - working_copy_cache = _channel_repository_cache.get(cache_key) - if working_copy_cache and working_copy_cache.get('time') > \ - time.time(): - return working_copy_cache.get('data') - - binary = self.retrieve_binary() - if not binary: - return False - args = [binary, 'in', '-q'] - args.append(self.update_command[-1]) - output = self.execute(args, self.working_copy) - incoming = len(output) > 0 - - _channel_repository_cache[cache_key] = { - 'time': time.time() + self.cache_length, - 'data': incoming - } - return incoming - - -def clear_directory(directory, ignore_paths=None): - was_exception = False - for root, dirs, files in os.walk(directory, topdown=False): - paths = [os.path.join(root, f) for f in files] - paths.extend([os.path.join(root, d) for d in dirs]) - - for path in paths: - try: - # Don't delete the metadata file, that way we have it - # when the reinstall happens, and the appropriate - # usage info can be sent back to the server - if ignore_paths and path in ignore_paths: - continue - if os.path.isdir(path): - os.rmdir(path) - else: - os.remove(path) - except (OSError, IOError) as (e): - was_exception = True - - return not was_exception - - - -class PackageManager(): - """ - Allows downloading, creating, installing, upgrading, and deleting packages - - Delegates metadata retrieval to the _channel_providers and - _package_providers classes. Uses VcsUpgrader-based classes for handling - git and hg repositories in the Packages folder. Downloader classes are - utilized to fetch contents of URLs. - - Also handles displaying package messaging, and sending usage information to - the usage server. - """ - - def __init__(self): - # Here we manually copy the settings since sublime doesn't like - # code accessing settings from threads - self.settings = {} - settings = sublime.load_settings(__name__ + '.sublime-settings') - for setting in ['timeout', 'repositories', 'repository_channels', - 'package_name_map', 'dirs_to_ignore', 'files_to_ignore', - 'package_destination', 'cache_length', 'auto_upgrade', - 'files_to_ignore_binary', 'files_to_keep', 'dirs_to_keep', - 'git_binary', 'git_update_command', 'hg_binary', - 'hg_update_command', 'http_proxy', 'https_proxy', - 'auto_upgrade_ignore', 'auto_upgrade_frequency', - 'submit_usage', 'submit_url', 'renamed_packages', - 'files_to_include', 'files_to_include_binary', 'certs', - 'ignore_vcs_packages', 'proxy_username', 'proxy_password', - 'debug', 'user_agent']: - if settings.get(setting) == None: - continue - self.settings[setting] = settings.get(setting) - - # https_proxy will inherit from http_proxy unless it is set to a - # string value or false - no_https_proxy = self.settings.get('https_proxy') in ["", None] - if no_https_proxy and self.settings.get('http_proxy'): - self.settings['https_proxy'] = self.settings.get('http_proxy') - if self.settings['https_proxy'] == False: - self.settings['https_proxy'] = '' - - self.settings['platform'] = sublime.platform() - self.settings['version'] = sublime.version() - - def compare_versions(self, version1, version2): - """ - Compares to version strings to see which is greater - - Date-based version numbers (used by GitHub and BitBucket providers) - are automatically pre-pended with a 0 so they are always less than - version 1.0. - - :return: - -1 if version1 is less than version2 - 0 if they are equal - 1 if version1 is greater than version2 - """ - - def date_compat(v): - # We prepend 0 to all date-based version numbers so that developers - # may switch to explicit versioning from GitHub/BitBucket - # versioning based on commit dates - date_match = re.match('(\d{4})\.(\d{2})\.(\d{2})\.(\d{2})\.(\d{2})\.(\d{2})$', v) - if date_match: - v = '0.%s.%s.%s.%s.%s.%s' % date_match.groups() - return v - - def semver_compat(v): - # When translating dates into semver, the way to get each date - # segment into the version is to treat the year and month as - # minor and patch, and then the rest as a numeric build version - # with four different parts. The result looks like: - # 0.2012.11+10.31.23.59 - date_match = re.match('(\d{4}(?:\.\d{2}){2})\.(\d{2}(?:\.\d{2}){3})$', v) - if date_match: - v = '%s+%s' % (date_match.group(1), date_match.group(2)) - - # Semver must have major, minor, patch - elif re.match('^\d+$', v): - v += '.0.0' - elif re.match('^\d+\.\d+$', v): - v += '.0' - return v - - def cmp_compat(v): - return [int(x) for x in re.sub(r'(\.0+)*$', '', v).split(".")] - - version1 = date_compat(version1) - version2 = date_compat(version2) - try: - return semver.compare(semver_compat(version1), semver_compat(version2)) - except (ValueError): - return cmp(cmp_compat(version1), cmp_compat(version2)) - - def download_url(self, url, error_message): - """ - Downloads a URL and returns the contents - - :param url: - The string URL to download - - :param error_message: - The error message to include if the download fails - - :return: - The string contents of the URL, or False on error - """ - - has_ssl = 'ssl' in sys.modules and hasattr(urllib2, 'HTTPSHandler') - is_ssl = re.search('^https://', url) != None - downloader = None - - if (is_ssl and has_ssl) or not is_ssl: - downloader = UrlLib2Downloader(self.settings) - else: - for downloader_class in [CurlDownloader, WgetDownloader]: - try: - downloader = downloader_class(self.settings) - break - except (BinaryNotFoundError): - pass - - if not downloader: - sublime.error_message(('%s: Unable to download %s due to no ' + - 'ssl module available and no capable program found. Please ' + - 'install curl or wget.') % (__name__, url)) - return False - - url = url.replace(' ', '%20') - hostname = urlparse.urlparse(url).hostname.lower() - timeout = self.settings.get('timeout', 3) - - rate_limited_cache = _channel_repository_cache.get('rate_limited_domains', {}) - if rate_limited_cache.get('time') and rate_limited_cache.get('time') > time.time(): - rate_limited_domains = rate_limited_cache.get('data', []) - else: - rate_limited_domains = [] - - if self.settings.get('debug'): - try: - ip = socket.gethostbyname(hostname) - except (socket.gaierror) as (e): - ip = unicode_from_os(e) - - print u"%s: Download Debug" % __name__ - print u" URL: %s" % url - print u" Resolved IP: %s" % ip - print u" Timeout: %s" % str(timeout) - - if hostname in rate_limited_domains: - if self.settings.get('debug'): - print u" Skipping due to hitting rate limit for %s" % hostname - return False - - try: - return downloader.download(url, error_message, timeout, 3) - except (RateLimitException) as (e): - - rate_limited_domains.append(hostname) - _channel_repository_cache['rate_limited_domains'] = { - 'data': rate_limited_domains, - 'time': time.time() + self.settings.get('cache_length', - 300) - } - - print ('%s: Hit rate limit of %s for %s, skipping all futher ' + - 'download requests for this domain') % (__name__, - e.limit, e.host) - return False - - def get_metadata(self, package): - """ - Returns the package metadata for an installed package - - :return: - A dict with the keys: - version - url - description - or an empty dict on error - """ - - metadata_filename = os.path.join(self.get_package_dir(package), - 'package-metadata.json') - if os.path.exists(metadata_filename): - with open(metadata_filename) as f: - try: - return json.load(f) - except (ValueError): - return {} - return {} - - def list_repositories(self): - """ - Returns a master list of all repositories pulled from all sources - - These repositories come from the channels specified in the - "repository_channels" setting, plus any repositories listed in the - "repositories" setting. - - :return: - A list of all available repositories - """ - - repositories = self.settings.get('repositories') - repository_channels = self.settings.get('repository_channels') - for channel in repository_channels: - channel = channel.strip() - - channel_repositories = None - - # Caches various info from channels for performance - cache_key = channel + '.repositories' - repositories_cache = _channel_repository_cache.get(cache_key) - if repositories_cache and repositories_cache.get('time') > \ - time.time(): - channel_repositories = repositories_cache.get('data') - - name_map_cache_key = channel + '.package_name_map' - name_map_cache = _channel_repository_cache.get( - name_map_cache_key) - if name_map_cache and name_map_cache.get('time') > \ - time.time(): - name_map = name_map_cache.get('data') - name_map.update(self.settings.get('package_name_map', {})) - self.settings['package_name_map'] = name_map - - renamed_cache_key = channel + '.renamed_packages' - renamed_cache = _channel_repository_cache.get( - renamed_cache_key) - if renamed_cache and renamed_cache.get('time') > \ - time.time(): - renamed_packages = renamed_cache.get('data') - renamed_packages.update(self.settings.get('renamed_packages', - {})) - self.settings['renamed_packages'] = renamed_packages - - unavailable_cache_key = channel + '.unavailable_packages' - unavailable_cache = _channel_repository_cache.get( - unavailable_cache_key) - if unavailable_cache and unavailable_cache.get('time') > \ - time.time(): - unavailable_packages = unavailable_cache.get('data') - unavailable_packages.extend(self.settings.get('unavailable_packages', - [])) - self.settings['unavailable_packages'] = unavailable_packages - - certs_cache_key = channel + '.certs' - certs_cache = _channel_repository_cache.get(certs_cache_key) - if certs_cache and certs_cache.get('time') > time.time(): - certs = self.settings.get('certs', {}) - certs.update(certs_cache.get('data')) - self.settings['certs'] = certs - - # If any of the info was not retrieved from the cache, we need to - # grab the channel to get it - if channel_repositories == None or \ - self.settings.get('package_name_map') == None or \ - self.settings.get('renamed_packages') == None: - - for provider_class in _channel_providers: - provider = provider_class(channel, self) - if provider.match_url(): - break - - channel_repositories = provider.get_repositories() - - if channel_repositories == False: - continue - _channel_repository_cache[cache_key] = { - 'time': time.time() + self.settings.get('cache_length', - 300), - 'data': channel_repositories - } - - for repo in channel_repositories: - if provider.get_packages(repo) == False: - continue - packages_cache_key = repo + '.packages' - _channel_repository_cache[packages_cache_key] = { - 'time': time.time() + self.settings.get('cache_length', - 300), - 'data': provider.get_packages(repo) - } - - # Have the local name map override the one from the channel - name_map = provider.get_name_map() - name_map.update(self.settings.get('package_name_map', {})) - self.settings['package_name_map'] = name_map - _channel_repository_cache[name_map_cache_key] = { - 'time': time.time() + self.settings.get('cache_length', - 300), - 'data': name_map - } - - renamed_packages = provider.get_renamed_packages() - _channel_repository_cache[renamed_cache_key] = { - 'time': time.time() + self.settings.get('cache_length', - 300), - 'data': renamed_packages - } - if renamed_packages: - self.settings['renamed_packages'] = self.settings.get( - 'renamed_packages', {}) - self.settings['renamed_packages'].update(renamed_packages) - - unavailable_packages = provider.get_unavailable_packages() - _channel_repository_cache[unavailable_cache_key] = { - 'time': time.time() + self.settings.get('cache_length', - 300), - 'data': unavailable_packages - } - if unavailable_packages: - self.settings['unavailable_packages'] = self.settings.get( - 'unavailable_packages', []) - self.settings['unavailable_packages'].extend(unavailable_packages) - - certs = provider.get_certs() - _channel_repository_cache[certs_cache_key] = { - 'time': time.time() + self.settings.get('cache_length', - 300), - 'data': certs - } - if certs: - self.settings['certs'] = self.settings.get('certs', {}) - self.settings['certs'].update(certs) - - repositories.extend(channel_repositories) - return [repo.strip() for repo in repositories] - - def list_available_packages(self): - """ - Returns a master list of every available package from all sources - - :return: - A dict in the format: - { - 'Package Name': { - # Package details - see example-packages.json for format - }, - ... - } - """ - - repositories = self.list_repositories() - packages = {} - downloaders = [] - grouped_downloaders = {} - - # Repositories are run in reverse order so that the ones first - # on the list will overwrite those last on the list - for repo in repositories[::-1]: - repository_packages = None - - cache_key = repo + '.packages' - packages_cache = _channel_repository_cache.get(cache_key) - if packages_cache and packages_cache.get('time') > \ - time.time(): - repository_packages = packages_cache.get('data') - packages.update(repository_packages) - - if repository_packages == None: - downloader = RepositoryDownloader(self, - self.settings.get('package_name_map', {}), repo) - domain = re.sub('^https?://[^/]*?(\w+\.\w+)($|/.*$)', '\\1', - repo) - - # downloaders are grouped by domain so that multiple can - # be run in parallel without running into API access limits - if not grouped_downloaders.get(domain): - grouped_downloaders[domain] = [] - grouped_downloaders[domain].append(downloader) - - # Allows creating a separate named function for each downloader - # delay. Not having this contained in a function causes all of the - # schedules to reference the same downloader.start() - def schedule(downloader, delay): - downloader.has_started = False - - def inner(): - downloader.start() - downloader.has_started = True - sublime.set_timeout(inner, delay) - - # Grabs every repo grouped by domain and delays the start - # of each download from that domain by a fixed amount - for domain_downloaders in grouped_downloaders.values(): - for i in range(len(domain_downloaders)): - downloader = domain_downloaders[i] - downloaders.append(downloader) - schedule(downloader, i * 150) - - complete = [] - - # Wait for all of the downloaders to finish - while downloaders: - downloader = downloaders.pop() - if downloader.has_started: - downloader.join() - complete.append(downloader) - else: - downloaders.insert(0, downloader) - - # Grabs the results and stuff if all in the cache - for downloader in complete: - repository_packages = downloader.packages - if repository_packages == False: - continue - cache_key = downloader.repo + '.packages' - _channel_repository_cache[cache_key] = { - 'time': time.time() + self.settings.get('cache_length', 300), - 'data': repository_packages - } - packages.update(repository_packages) - - renamed_packages = downloader.renamed_packages - if renamed_packages == False: - continue - renamed_cache_key = downloader.repo + '.renamed_packages' - _channel_repository_cache[renamed_cache_key] = { - 'time': time.time() + self.settings.get('cache_length', 300), - 'data': renamed_packages - } - if renamed_packages: - self.settings['renamed_packages'] = self.settings.get( - 'renamed_packages', {}) - self.settings['renamed_packages'].update(renamed_packages) - - unavailable_packages = downloader.unavailable_packages - unavailable_cache_key = downloader.repo + '.unavailable_packages' - _channel_repository_cache[unavailable_cache_key] = { - 'time': time.time() + self.settings.get('cache_length', 300), - 'data': unavailable_packages - } - if unavailable_packages: - self.settings['unavailable_packages'] = self.settings.get( - 'unavailable_packages', []) - self.settings['unavailable_packages'].extend(unavailable_packages) - - return packages - - def list_packages(self): - """ :return: A list of all installed, non-default, package names""" - - package_names = os.listdir(sublime.packages_path()) - package_names = [path for path in package_names if - os.path.isdir(os.path.join(sublime.packages_path(), path))] - - # Ignore things to be deleted - ignored = [] - for package in package_names: - cleanup_file = os.path.join(sublime.packages_path(), package, - 'package-control.cleanup') - if os.path.exists(cleanup_file): - ignored.append(package) - - packages = list(set(package_names) - set(ignored) - - set(self.list_default_packages())) - packages = sorted(packages, key=lambda s: s.lower()) - - return packages - - def list_all_packages(self): - """ :return: A list of all installed package names, including default packages""" - - packages = os.listdir(sublime.packages_path()) - packages = sorted(packages, key=lambda s: s.lower()) - return packages - - def list_default_packages(self): - """ :return: A list of all default package names""" - - files = os.listdir(os.path.join(os.path.dirname( - sublime.packages_path()), 'Pristine Packages')) - files = list(set(files) - set(os.listdir( - sublime.installed_packages_path()))) - packages = [file.replace('.sublime-package', '') for file in files] - packages = sorted(packages, key=lambda s: s.lower()) - return packages - - def get_package_dir(self, package): - """:return: The full filesystem path to the package directory""" - - return os.path.join(sublime.packages_path(), package) - - def get_mapped_name(self, package): - """:return: The name of the package after passing through mapping rules""" - - return self.settings.get('package_name_map', {}).get(package, package) - - def create_package(self, package_name, package_destination, - binary_package=False): - """ - Creates a .sublime-package file from the running Packages directory - - :param package_name: - The package to create a .sublime-package file for - - :param package_destination: - The full filesystem path of the directory to save the new - .sublime-package file in. - - :param binary_package: - If the created package should follow the binary package include/ - exclude patterns from the settings. These normally include a setup - to exclude .py files and include .pyc files, but that can be - changed via settings. - - :return: bool if the package file was successfully created - """ - - package_dir = self.get_package_dir(package_name) + '/' - - if not os.path.exists(package_dir): - sublime.error_message(('%s: The folder for the package name ' + - 'specified, %s, does not exist in %s') % - (__name__, package_name, sublime.packages_path())) - return False - - package_filename = package_name + '.sublime-package' - package_path = os.path.join(package_destination, - package_filename) - - if not os.path.exists(sublime.installed_packages_path()): - os.mkdir(sublime.installed_packages_path()) - - if os.path.exists(package_path): - os.remove(package_path) - - try: - package_file = zipfile.ZipFile(package_path, "w", - compression=zipfile.ZIP_DEFLATED) - except (OSError, IOError) as (exception): - sublime.error_message(('%s: An error occurred creating the ' + - 'package file %s in %s. %s') % (__name__, package_filename, - package_destination, unicode_from_os(exception))) - return False - - dirs_to_ignore = self.settings.get('dirs_to_ignore', []) - if not binary_package: - files_to_ignore = self.settings.get('files_to_ignore', []) - files_to_include = self.settings.get('files_to_include', []) - else: - files_to_ignore = self.settings.get('files_to_ignore_binary', []) - files_to_include = self.settings.get('files_to_include_binary', []) - - package_dir_regex = re.compile('^' + re.escape(package_dir)) - for root, dirs, files in os.walk(package_dir): - [dirs.remove(dir) for dir in dirs if dir in dirs_to_ignore] - paths = dirs - paths.extend(files) - for path in paths: - full_path = os.path.join(root, path) - relative_path = re.sub(package_dir_regex, '', full_path) - - ignore_matches = [fnmatch(relative_path, p) for p in files_to_ignore] - include_matches = [fnmatch(relative_path, p) for p in files_to_include] - if any(ignore_matches) and not any(include_matches): - continue - - if os.path.isdir(full_path): - continue - package_file.write(full_path, relative_path) - - package_file.close() - - return True - - def install_package(self, package_name): - """ - Downloads and installs (or upgrades) a package - - Uses the self.list_available_packages() method to determine where to - retrieve the package file from. - - The install process consists of: - - 1. Finding the package - 2. Downloading the .sublime-package/.zip file - 3. Extracting the package file - 4. Showing install/upgrade messaging - 5. Submitting usage info - 6. Recording that the package is installed - - :param package_name: - The package to download and install - - :return: bool if the package was successfully installed - """ - - packages = self.list_available_packages() - - if package_name in self.settings.get('unavailable_packages', []): - print ('%s: The package "%s" is not available ' + - 'on this platform.') % (__name__, package_name) - return False - - if package_name not in packages.keys(): - sublime.error_message(('%s: The package specified, %s, is ' + - 'not available.') % (__name__, package_name)) - return False - - download = packages[package_name]['downloads'][0] - url = download['url'] - - package_filename = package_name + \ - '.sublime-package' - package_path = os.path.join(sublime.installed_packages_path(), - package_filename) - pristine_package_path = os.path.join(os.path.dirname( - sublime.packages_path()), 'Pristine Packages', package_filename) - - package_dir = self.get_package_dir(package_name) - - package_metadata_file = os.path.join(package_dir, - 'package-metadata.json') - - if os.path.exists(os.path.join(package_dir, '.git')): - if self.settings.get('ignore_vcs_packages'): - sublime.error_message(('%s: Skipping git package %s since ' + - 'the setting ignore_vcs_packages is set to true') % - (__name__, package_name)) - return False - return GitUpgrader(self.settings['git_binary'], - self.settings['git_update_command'], package_dir, - self.settings['cache_length'], self.settings['debug']).run() - elif os.path.exists(os.path.join(package_dir, '.hg')): - if self.settings.get('ignore_vcs_packages'): - sublime.error_message(('%s: Skipping hg package %s since ' + - 'the setting ignore_vcs_packages is set to true') % - (__name__, package_name)) - return False - return HgUpgrader(self.settings['hg_binary'], - self.settings['hg_update_command'], package_dir, - self.settings['cache_length'], self.settings['debug']).run() - - is_upgrade = os.path.exists(package_metadata_file) - old_version = None - if is_upgrade: - old_version = self.get_metadata(package_name).get('version') - - package_bytes = self.download_url(url, 'Error downloading package.') - if package_bytes == False: - return False - with open(package_path, "wb") as package_file: - package_file.write(package_bytes) - - if not os.path.exists(package_dir): - os.mkdir(package_dir) - - # We create a backup copy incase something was edited - else: - try: - backup_dir = os.path.join(os.path.dirname( - sublime.packages_path()), 'Backup', - datetime.datetime.now().strftime('%Y%m%d%H%M%S')) - if not os.path.exists(backup_dir): - os.makedirs(backup_dir) - package_backup_dir = os.path.join(backup_dir, package_name) - shutil.copytree(package_dir, package_backup_dir) - except (OSError, IOError) as (exception): - sublime.error_message(('%s: An error occurred while trying ' + - 'to backup the package directory for %s. %s') % - (__name__, package_name, unicode_from_os(exception))) - shutil.rmtree(package_backup_dir) - return False - - try: - package_zip = zipfile.ZipFile(package_path, 'r') - except (zipfile.BadZipfile): - sublime.error_message(('%s: An error occurred while ' + - 'trying to unzip the package file for %s. Please try ' + - 'installing the package again.') % (__name__, package_name)) - return False - - root_level_paths = [] - last_path = None - for path in package_zip.namelist(): - last_path = path - if path.find('/') in [len(path) - 1, -1]: - root_level_paths.append(path) - if path[0] == '/' or path.find('../') != -1 or path.find('..\\') != -1: - sublime.error_message(('%s: The package specified, %s, ' + - 'contains files outside of the package dir and cannot ' + - 'be safely installed.') % (__name__, package_name)) - return False - - if last_path and len(root_level_paths) == 0: - root_level_paths.append(last_path[0:last_path.find('/') + 1]) - - os.chdir(package_dir) - - overwrite_failed = False - - # Here we don’t use .extractall() since it was having issues on OS X - skip_root_dir = len(root_level_paths) == 1 and \ - root_level_paths[0].endswith('/') - extracted_paths = [] - for path in package_zip.namelist(): - dest = path - try: - if not isinstance(dest, unicode): - dest = unicode(dest, 'utf-8', 'strict') - except (UnicodeDecodeError): - dest = unicode(dest, 'cp1252', 'replace') - - if os.name == 'nt': - regex = ':|\*|\?|"|<|>|\|' - if re.search(regex, dest) != None: - print ('%s: Skipping file from package named %s due to ' + - 'an invalid filename') % (__name__, path) - continue - - # If there was only a single directory in the package, we remove - # that folder name from the paths as we extract entries - if skip_root_dir: - dest = dest[len(root_level_paths[0]):] - - if os.name == 'nt': - dest = dest.replace('/', '\\') - else: - dest = dest.replace('\\', '/') - - dest = os.path.join(package_dir, dest) - - def add_extracted_dirs(dir): - while dir not in extracted_paths: - extracted_paths.append(dir) - dir = os.path.dirname(dir) - if dir == package_dir: - break - - if path.endswith('/'): - if not os.path.exists(dest): - os.makedirs(dest) - add_extracted_dirs(dest) - else: - dest_dir = os.path.dirname(dest) - if not os.path.exists(dest_dir): - os.makedirs(dest_dir) - add_extracted_dirs(dest_dir) - extracted_paths.append(dest) - try: - open(dest, 'wb').write(package_zip.read(path)) - except (IOError) as (e): - message = unicode_from_os(e) - if re.search('[Ee]rrno 13', message): - overwrite_failed = True - break - print ('%s: Skipping file from package named %s due to ' + - 'an invalid filename') % (__name__, path) - - except (UnicodeDecodeError): - print ('%s: Skipping file from package named %s due to ' + - 'an invalid filename') % (__name__, path) - package_zip.close() - - - # If upgrading failed, queue the package to upgrade upon next start - if overwrite_failed: - reinstall_file = os.path.join(package_dir, 'package-control.reinstall') - open(reinstall_file, 'w').close() - - # Don't delete the metadata file, that way we have it - # when the reinstall happens, and the appropriate - # usage info can be sent back to the server - clear_directory(package_dir, [reinstall_file, package_metadata_file]) - - sublime.error_message(('%s: An error occurred while trying to ' + - 'upgrade %s. Please restart Sublime Text to finish the ' + - 'upgrade.') % (__name__, package_name)) - return False - - - # Here we clean out any files that were not just overwritten. It is ok - # if there is an error removing a file. The next time there is an - # upgrade, it should be cleaned out successfully then. - clear_directory(package_dir, extracted_paths) - - - self.print_messages(package_name, package_dir, is_upgrade, old_version) - - with open(package_metadata_file, 'w') as f: - metadata = { - "version": packages[package_name]['downloads'][0]['version'], - "url": packages[package_name]['url'], - "description": packages[package_name]['description'] - } - json.dump(metadata, f) - - # Submit install and upgrade info - if is_upgrade: - params = { - 'package': package_name, - 'operation': 'upgrade', - 'version': packages[package_name]['downloads'][0]['version'], - 'old_version': old_version - } - else: - params = { - 'package': package_name, - 'operation': 'install', - 'version': packages[package_name]['downloads'][0]['version'] - } - self.record_usage(params) - - # Record the install in the settings file so that you can move - # settings across computers and have the same packages installed - def save_package(): - settings = sublime.load_settings(__name__ + '.sublime-settings') - installed_packages = settings.get('installed_packages', []) - if not installed_packages: - installed_packages = [] - installed_packages.append(package_name) - installed_packages = list(set(installed_packages)) - installed_packages = sorted(installed_packages, - key=lambda s: s.lower()) - settings.set('installed_packages', installed_packages) - sublime.save_settings(__name__ + '.sublime-settings') - sublime.set_timeout(save_package, 1) - - # Here we delete the package file from the installed packages directory - # since we don't want to accidentally overwrite user changes - os.remove(package_path) - # We have to remove the pristine package too or else Sublime Text 2 - # will silently delete the package - if os.path.exists(pristine_package_path): - os.remove(pristine_package_path) - - os.chdir(sublime.packages_path()) - return True - - def print_messages(self, package, package_dir, is_upgrade, old_version): - """ - Prints out package install and upgrade messages - - The functionality provided by this allows package maintainers to - show messages to the user when a package is installed, or when - certain version upgrade occur. - - :param package: - The name of the package the message is for - - :param package_dir: - The full filesystem path to the package directory - - :param is_upgrade: - If the install was actually an upgrade - - :param old_version: - The string version of the package before the upgrade occurred - """ - - messages_file = os.path.join(package_dir, 'messages.json') - if not os.path.exists(messages_file): - return - - messages_fp = open(messages_file, 'r') - try: - message_info = json.load(messages_fp) - except (ValueError): - print '%s: Error parsing messages.json for %s' % (__name__, package) - return - messages_fp.close() - - output = '' - if not is_upgrade and message_info.get('install'): - install_messages = os.path.join(package_dir, - message_info.get('install')) - message = '\n\n%s:\n%s\n\n ' % (package, - ('-' * len(package))) - with open(install_messages, 'r') as f: - message += unicode(f.read(), 'utf-8', errors='replace').replace('\n', '\n ') - output += message + '\n' - - elif is_upgrade and old_version: - upgrade_messages = list(set(message_info.keys()) - - set(['install'])) - upgrade_messages = sorted(upgrade_messages, - cmp=self.compare_versions, reverse=True) - for version in upgrade_messages: - if self.compare_versions(old_version, version) >= 0: - break - if not output: - message = '\n\n%s:\n%s\n' % (package, - ('-' * len(package))) - output += message - upgrade_messages = os.path.join(package_dir, - message_info.get(version)) - message = '\n ' - with open(upgrade_messages, 'r') as f: - message += unicode(f.read(), 'utf-8', errors='replace').replace('\n', '\n ') - output += message + '\n' - - if not output: - return - - def print_to_panel(): - window = sublime.active_window() - - views = window.views() - view = None - for _view in views: - if _view.name() == 'Package Control Messages': - view = _view - break - - if not view: - view = window.new_file() - view.set_name('Package Control Messages') - view.set_scratch(True) - - def write(string): - edit = view.begin_edit() - view.insert(edit, view.size(), string) - view.end_edit(edit) - - if not view.size(): - view.settings().set("word_wrap", True) - write('Package Control Messages\n' + - '========================') - - write(output) - sublime.set_timeout(print_to_panel, 1) - - def remove_package(self, package_name): - """ - Deletes a package - - The deletion process consists of: - - 1. Deleting the directory (or marking it for deletion if deletion fails) - 2. Submitting usage info - 3. Removing the package from the list of installed packages - - :param package_name: - The package to delete - - :return: bool if the package was successfully deleted - """ - - installed_packages = self.list_packages() - - if package_name not in installed_packages: - sublime.error_message(('%s: The package specified, %s, is not ' + - 'installed.') % (__name__, package_name)) - return False - - os.chdir(sublime.packages_path()) - - # Give Sublime Text some time to ignore the package - time.sleep(1) - - package_filename = package_name + '.sublime-package' - package_path = os.path.join(sublime.installed_packages_path(), - package_filename) - installed_package_path = os.path.join(os.path.dirname( - sublime.packages_path()), 'Installed Packages', package_filename) - pristine_package_path = os.path.join(os.path.dirname( - sublime.packages_path()), 'Pristine Packages', package_filename) - package_dir = self.get_package_dir(package_name) - - version = self.get_metadata(package_name).get('version') - - try: - if os.path.exists(package_path): - os.remove(package_path) - except (OSError, IOError) as (exception): - sublime.error_message(('%s: An error occurred while trying to ' + - 'remove the package file for %s. %s') % (__name__, - package_name, unicode_from_os(exception))) - return False - - try: - if os.path.exists(installed_package_path): - os.remove(installed_package_path) - except (OSError, IOError) as (exception): - sublime.error_message(('%s: An error occurred while trying to ' + - 'remove the installed package file for %s. %s') % (__name__, - package_name, unicode_from_os(exception))) - return False - - try: - if os.path.exists(pristine_package_path): - os.remove(pristine_package_path) - except (OSError, IOError) as (exception): - sublime.error_message(('%s: An error occurred while trying to ' + - 'remove the pristine package file for %s. %s') % (__name__, - package_name, unicode_from_os(exception))) - return False - - # We don't delete the actual package dir immediately due to a bug - # in sublime_plugin.py - can_delete_dir = True - if not clear_directory(package_dir): - # If there is an error deleting now, we will mark it for - # cleanup the next time Sublime Text starts - open(os.path.join(package_dir, 'package-control.cleanup'), - 'w').close() - can_delete_dir = False - - params = { - 'package': package_name, - 'operation': 'remove', - 'version': version - } - self.record_usage(params) - - # Remove the package from the installed packages list - def clear_package(): - settings = sublime.load_settings('%s.sublime-settings' % __name__) - installed_packages = settings.get('installed_packages', []) - if not installed_packages: - installed_packages = [] - installed_packages.remove(package_name) - settings.set('installed_packages', installed_packages) - sublime.save_settings('%s.sublime-settings' % __name__) - sublime.set_timeout(clear_package, 1) - - if can_delete_dir: - os.rmdir(package_dir) - - return True - - def record_usage(self, params): - """ - Submits install, upgrade and delete actions to a usage server - - The usage information is currently displayed on the Package Control - community package list at http://wbond.net/sublime_packages/community - - :param params: - A dict of the information to submit - """ - - if not self.settings.get('submit_usage'): - return - params['package_control_version'] = \ - self.get_metadata('Package Control').get('version') - params['sublime_platform'] = self.settings.get('platform') - params['sublime_version'] = self.settings.get('version') - url = self.settings.get('submit_url') + '?' + urllib.urlencode(params) - - result = self.download_url(url, 'Error submitting usage information.') - if result == False: - return - - try: - result = json.loads(result) - if result['result'] != 'success': - raise ValueError() - except (ValueError): - print '%s: Error submitting usage information for %s' % (__name__, - params['package']) - - -class PackageCreator(): - """ - Abstract class for commands that create .sublime-package files - """ - - def show_panel(self): - """ - Shows a list of packages that can be turned into a .sublime-package file - """ - - self.manager = PackageManager() - self.packages = self.manager.list_packages() - if not self.packages: - sublime.error_message(('%s: There are no packages available to ' + - 'be packaged.') % (__name__)) - return - self.window.show_quick_panel(self.packages, self.on_done) - - def get_package_destination(self): - """ - Retrieves the destination for .sublime-package files - - :return: - A string - the path to the folder to save .sublime-package files in - """ - - destination = self.manager.settings.get('package_destination') - - # We check destination via an if statement instead of using - # the dict.get() method since the key may be set, but to a blank value - if not destination: - destination = os.path.join(os.path.expanduser('~'), 'Desktop') - - return destination - - -class CreatePackageCommand(sublime_plugin.WindowCommand, PackageCreator): - """ - Command to create a regular .sublime-package file - """ - - def run(self): - self.show_panel() - - def on_done(self, picked): - """ - Quick panel user selection handler - processes the user package - selection and create the package file - - :param picked: - An integer of the 0-based package name index from the presented - list. -1 means the user cancelled. - """ - - if picked == -1: - return - package_name = self.packages[picked] - package_destination = self.get_package_destination() - - if self.manager.create_package(package_name, package_destination): - self.window.run_command('open_dir', {"dir": - package_destination, "file": package_name + - '.sublime-package'}) - - -class CreateBinaryPackageCommand(sublime_plugin.WindowCommand, PackageCreator): - """ - Command to create a binary .sublime-package file. Binary packages in - general exclude the .py source files and instead include the .pyc files. - Actual included and excluded files are controlled by settings. - """ - - def run(self): - self.show_panel() - - def on_done(self, picked): - """ - Quick panel user selection handler - processes the user package - selection and create the package file - - :param picked: - An integer of the 0-based package name index from the presented - list. -1 means the user cancelled. - """ - - if picked == -1: - return - package_name = self.packages[picked] - package_destination = self.get_package_destination() - - if self.manager.create_package(package_name, package_destination, - binary_package=True): - self.window.run_command('open_dir', {"dir": - package_destination, "file": package_name + - '.sublime-package'}) - - -class PackageRenamer(): - """ - Class to handle renaming packages via the renamed_packages setting - gathered from channels and repositories. - """ - - def load_settings(self): - """ - Loads the list of installed packages from the - Package Control.sublime-settings file. - """ - - self.settings_file = '%s.sublime-settings' % __name__ - self.settings = sublime.load_settings(self.settings_file) - self.installed_packages = self.settings.get('installed_packages', []) - if not isinstance(self.installed_packages, list): - self.installed_packages = [] - - def rename_packages(self, installer): - """ - Renames any installed packages that the user has installed. - - :param installer: - An instance of :class:`PackageInstaller` - """ - - # Fetch the packages since that will pull in the renamed packages list - installer.manager.list_available_packages() - renamed_packages = installer.manager.settings.get('renamed_packages', {}) - if not renamed_packages: - renamed_packages = {} - - # These are packages that have been tracked as installed - installed_pkgs = self.installed_packages - # There are the packages actually present on the filesystem - present_packages = installer.manager.list_packages() - - # Rename directories for packages that have changed names - for package_name in renamed_packages: - package_dir = os.path.join(sublime.packages_path(), package_name) - metadata_path = os.path.join(package_dir, 'package-metadata.json') - if not os.path.exists(metadata_path): - continue - - new_package_name = renamed_packages[package_name] - new_package_dir = os.path.join(sublime.packages_path(), - new_package_name) - - changing_case = package_name.lower() == new_package_name.lower() - case_insensitive_fs = sublime.platform() in ['windows', 'osx'] - - # Since Windows and OSX use case-insensitive filesystems, we have to - # scan through the list of installed packages if the rename of the - # package is just changing the case of it. If we don't find the old - # name for it, we continue the loop since os.path.exists() will return - # true due to the case-insensitive nature of the filesystems. - if case_insensitive_fs and changing_case: - has_old = False - for present_package_name in present_packages: - if present_package_name == package_name: - has_old = True - break - if not has_old: - continue - - if not os.path.exists(new_package_dir) or (case_insensitive_fs and changing_case): - - # Windows will not allow you to rename to the same name with - # a different case, so we work around that with a temporary name - if os.name == 'nt' and changing_case: - temp_package_name = '__' + new_package_name - temp_package_dir = os.path.join(sublime.packages_path(), - temp_package_name) - os.rename(package_dir, temp_package_dir) - package_dir = temp_package_dir - - os.rename(package_dir, new_package_dir) - installed_pkgs.append(new_package_name) - - print '%s: Renamed %s to %s' % (__name__, package_name, - new_package_name) - - else: - installer.manager.remove_package(package_name) - print ('%s: Removed %s since package with new name (%s) ' + - 'already exists') % (__name__, package_name, - new_package_name) - - try: - installed_pkgs.remove(package_name) - except (ValueError): - pass - - sublime.set_timeout(lambda: self.save_packages(installed_pkgs), 10) - - def save_packages(self, installed_packages): - """ - Saves the list of installed packages (after having been appropriately - renamed) - - :param installed_packages: - The new list of installed packages - """ - - installed_packages = list(set(installed_packages)) - installed_packages = sorted(installed_packages, - key=lambda s: s.lower()) - - if installed_packages != self.installed_packages: - self.settings.set('installed_packages', installed_packages) - sublime.save_settings(self.settings_file) - - -class PackageInstaller(): - """ - Provides helper functionality related to installing packages - """ - - def __init__(self): - self.manager = PackageManager() - - def make_package_list(self, ignore_actions=[], override_action=None, - ignore_packages=[]): - """ - Creates a list of packages and what operation would be performed for - each. Allows filtering by the applicable action or package name. - Returns the information in a format suitable for displaying in the - quick panel. - - :param ignore_actions: - A list of actions to ignore packages by. Valid actions include: - `install`, `upgrade`, `downgrade`, `reinstall`, `overwrite`, - `pull` and `none`. `pull` andd `none` are for Git and Hg - repositories. `pull` is present when incoming changes are detected, - where as `none` is selected if no commits are available. `overwrite` - is for packages that do not include version information via the - `package-metadata.json` file. - - :param override_action: - A string action name to override the displayed action for all listed - packages. - - :param ignore_packages: - A list of packages names that should not be returned in the list - - :return: - A list of lists, each containing three strings: - 0 - package name - 1 - package description - 2 - action; [extra info;] package url - """ - - packages = self.manager.list_available_packages() - installed_packages = self.manager.list_packages() - - package_list = [] - for package in sorted(packages.iterkeys(), key=lambda s: s.lower()): - if ignore_packages and package in ignore_packages: - continue - package_entry = [package] - info = packages[package] - download = info['downloads'][0] - - if package in installed_packages: - installed = True - metadata = self.manager.get_metadata(package) - if metadata.get('version'): - installed_version = metadata['version'] - else: - installed_version = None - else: - installed = False - - installed_version_name = 'v' + installed_version if \ - installed and installed_version else 'unknown version' - new_version = 'v' + download['version'] - - vcs = None - package_dir = self.manager.get_package_dir(package) - settings = self.manager.settings - - if override_action: - action = override_action - extra = '' - - else: - if os.path.exists(os.path.join(sublime.packages_path(), - package, '.git')): - if settings.get('ignore_vcs_packages'): - continue - vcs = 'git' - incoming = GitUpgrader(settings.get('git_binary'), - settings.get('git_update_command'), package_dir, - settings.get('cache_length'), settings.get('debug') - ).incoming() - elif os.path.exists(os.path.join(sublime.packages_path(), - package, '.hg')): - if settings.get('ignore_vcs_packages'): - continue - vcs = 'hg' - incoming = HgUpgrader(settings.get('hg_binary'), - settings.get('hg_update_command'), package_dir, - settings.get('cache_length'), settings.get('debug') - ).incoming() - - if installed: - if not installed_version: - if vcs: - if incoming: - action = 'pull' - extra = ' with ' + vcs - else: - action = 'none' - extra = '' - else: - action = 'overwrite' - extra = ' %s with %s' % (installed_version_name, - new_version) - else: - res = self.manager.compare_versions( - installed_version, download['version']) - if res < 0: - action = 'upgrade' - extra = ' to %s from %s' % (new_version, - installed_version_name) - elif res > 0: - action = 'downgrade' - extra = ' to %s from %s' % (new_version, - installed_version_name) - else: - action = 'reinstall' - extra = ' %s' % new_version - else: - action = 'install' - extra = ' %s' % new_version - extra += ';' - - if action in ignore_actions: - continue - - description = info.get('description') - if not description: - description = 'No description provided' - package_entry.append(description) - package_entry.append(action + extra + ' ' + - re.sub('^https?://', '', info['url'])) - package_list.append(package_entry) - return package_list - - def disable_package(self, package): - """ - Disables a package before installing or upgrading to prevent errors - where Sublime Text tries to read files that no longer exist, or read a - half-written file. - - :param package: The string package name - """ - - # Don't disable Package Control so it does not get stuck disabled - if package == 'Package Control': - return False - - settings = sublime.load_settings(preferences_filename()) - ignored = settings.get('ignored_packages') - if not ignored: - ignored = [] - if not package in ignored: - ignored.append(package) - settings.set('ignored_packages', ignored) - sublime.save_settings(preferences_filename()) - return True - return False - - def reenable_package(self, package): - """ - Re-enables a package after it has been installed or upgraded - - :param package: The string package name - """ - - settings = sublime.load_settings(preferences_filename()) - ignored = settings.get('ignored_packages') - if not ignored: - return - if package in ignored: - settings.set('ignored_packages', - list(set(ignored) - set([package]))) - sublime.save_settings(preferences_filename()) - - def on_done(self, picked): - """ - Quick panel user selection handler - disables a package, installs or - upgrades it, then re-enables the package - - :param picked: - An integer of the 0-based package name index from the presented - list. -1 means the user cancelled. - """ - - if picked == -1: - return - name = self.package_list[picked][0] - - if self.disable_package(name): - on_complete = lambda: self.reenable_package(name) - else: - on_complete = None - - thread = PackageInstallerThread(self.manager, name, on_complete) - thread.start() - ThreadProgress(thread, 'Installing package %s' % name, - 'Package %s successfully %s' % (name, self.completion_type)) - - -class PackageInstallerThread(threading.Thread): - """ - A thread to run package install/upgrade operations in so that the main - Sublime Text thread does not get blocked and freeze the UI - """ - - def __init__(self, manager, package, on_complete): - """ - :param manager: - An instance of :class:`PackageManager` - - :param package: - The string package name to install/upgrade - - :param on_complete: - A callback to run after installing/upgrading the package - """ - - self.package = package - self.manager = manager - self.on_complete = on_complete - threading.Thread.__init__(self) - - def run(self): - try: - self.result = self.manager.install_package(self.package) - finally: - if self.on_complete: - sublime.set_timeout(self.on_complete, 1) - - -class InstallPackageCommand(sublime_plugin.WindowCommand): - """ - A command that presents the list of available packages and allows the - user to pick one to install. - """ - - def run(self): - thread = InstallPackageThread(self.window) - thread.start() - ThreadProgress(thread, 'Loading repositories', '') - - -class InstallPackageThread(threading.Thread, PackageInstaller): - """ - A thread to run the action of retrieving available packages in. Uses the - default PackageInstaller.on_done quick panel handler. - """ - - def __init__(self, window): - """ - :param window: - An instance of :class:`sublime.Window` that represents the Sublime - Text window to show the available package list in. - """ - - self.window = window - self.completion_type = 'installed' - threading.Thread.__init__(self) - PackageInstaller.__init__(self) - - def run(self): - self.package_list = self.make_package_list(['upgrade', 'downgrade', - 'reinstall', 'pull', 'none']) - - def show_quick_panel(): - if not self.package_list: - sublime.error_message(('%s: There are no packages ' + - 'available for installation.') % __name__) - return - self.window.show_quick_panel(self.package_list, self.on_done) - sublime.set_timeout(show_quick_panel, 10) - - -class DiscoverPackagesCommand(sublime_plugin.WindowCommand): - """ - A command that opens the community package list webpage - """ - - def run(self): - self.window.run_command('open_url', - {'url': 'http://wbond.net/sublime_packages/community'}) - - -class UpgradePackageCommand(sublime_plugin.WindowCommand): - """ - A command that presents the list of installed packages that can be upgraded. - """ - - def run(self): - package_renamer = PackageRenamer() - package_renamer.load_settings() - - thread = UpgradePackageThread(self.window, package_renamer) - thread.start() - ThreadProgress(thread, 'Loading repositories', '') - - -class UpgradePackageThread(threading.Thread, PackageInstaller): - """ - A thread to run the action of retrieving upgradable packages in. - """ - - def __init__(self, window, package_renamer): - """ - :param window: - An instance of :class:`sublime.Window` that represents the Sublime - Text window to show the list of upgradable packages in. - - :param package_renamer: - An instance of :class:`PackageRenamer` - """ - self.window = window - self.package_renamer = package_renamer - self.completion_type = 'upgraded' - threading.Thread.__init__(self) - PackageInstaller.__init__(self) - - def run(self): - self.package_renamer.rename_packages(self) - - self.package_list = self.make_package_list(['install', 'reinstall', - 'none']) - - def show_quick_panel(): - if not self.package_list: - sublime.error_message(('%s: There are no packages ' + - 'ready for upgrade.') % __name__) - return - self.window.show_quick_panel(self.package_list, self.on_done) - sublime.set_timeout(show_quick_panel, 10) - - def on_done(self, picked): - """ - Quick panel user selection handler - disables a package, upgrades it, - then re-enables the package - - :param picked: - An integer of the 0-based package name index from the presented - list. -1 means the user cancelled. - """ - - if picked == -1: - return - name = self.package_list[picked][0] - - if self.disable_package(name): - on_complete = lambda: self.reenable_package(name) - else: - on_complete = None - - thread = PackageInstallerThread(self.manager, name, on_complete) - thread.start() - ThreadProgress(thread, 'Upgrading package %s' % name, - 'Package %s successfully %s' % (name, self.completion_type)) - - -class UpgradeAllPackagesCommand(sublime_plugin.WindowCommand): - """ - A command to automatically upgrade all installed packages that are - upgradable. - """ - - def run(self): - package_renamer = PackageRenamer() - package_renamer.load_settings() - - thread = UpgradeAllPackagesThread(self.window, package_renamer) - thread.start() - ThreadProgress(thread, 'Loading repositories', '') - - -class UpgradeAllPackagesThread(threading.Thread, PackageInstaller): - """ - A thread to run the action of retrieving upgradable packages in. - """ - - def __init__(self, window, package_renamer): - self.window = window - self.package_renamer = package_renamer - self.completion_type = 'upgraded' - threading.Thread.__init__(self) - PackageInstaller.__init__(self) - - def run(self): - self.package_renamer.rename_packages(self) - package_list = self.make_package_list(['install', 'reinstall', 'none']) - - disabled_packages = {} - - def do_upgrades(): - # Pause so packages can be disabled - time.sleep(0.5) - - # We use a function to generate the on-complete lambda because if - # we don't, the lambda will bind to info at the current scope, and - # thus use the last value of info from the loop - def make_on_complete(name): - return lambda: self.reenable_package(name) - - for info in package_list: - if disabled_packages.get(info[0]): - on_complete = make_on_complete(info[0]) - else: - on_complete = None - thread = PackageInstallerThread(self.manager, info[0], on_complete) - thread.start() - ThreadProgress(thread, 'Upgrading package %s' % info[0], - 'Package %s successfully %s' % (info[0], self.completion_type)) - - # Disabling a package means changing settings, which can only be done - # in the main thread. We then create a new background thread so that - # the upgrade process does not block the UI. - def disable_packages(): - for info in package_list: - disabled_packages[info[0]] = self.disable_package(info[0]) - threading.Thread(target=do_upgrades).start() - - sublime.set_timeout(disable_packages, 1) - - -class ExistingPackagesCommand(): - """ - Allows listing installed packages and their current version - """ - - def __init__(self): - self.manager = PackageManager() - - def make_package_list(self, action=''): - """ - Returns a list of installed packages suitable for displaying in the - quick panel. - - :param action: - An action to display at the beginning of the third element of the - list returned for each package - - :return: - A list of lists, each containing three strings: - 0 - package name - 1 - package description - 2 - [action] installed version; package url - """ - - packages = self.manager.list_packages() - - if action: - action += ' ' - - package_list = [] - for package in sorted(packages, key=lambda s: s.lower()): - package_entry = [package] - metadata = self.manager.get_metadata(package) - package_dir = os.path.join(sublime.packages_path(), package) - - description = metadata.get('description') - if not description: - description = 'No description provided' - package_entry.append(description) - - version = metadata.get('version') - if not version and os.path.exists(os.path.join(package_dir, - '.git')): - installed_version = 'git repository' - elif not version and os.path.exists(os.path.join(package_dir, - '.hg')): - installed_version = 'hg repository' - else: - installed_version = 'v' + version if version else \ - 'unknown version' - - url = metadata.get('url') - if url: - url = '; ' + re.sub('^https?://', '', url) - else: - url = '' - - package_entry.append(action + installed_version + url) - package_list.append(package_entry) - - return package_list - - -class ListPackagesCommand(sublime_plugin.WindowCommand): - """ - A command that shows a list of all installed packages in the quick panel - """ - - def run(self): - ListPackagesThread(self.window).start() - - -class ListPackagesThread(threading.Thread, ExistingPackagesCommand): - """ - A thread to prevent the listing of existing packages from freezing the UI - """ - - def __init__(self, window): - """ - :param window: - An instance of :class:`sublime.Window` that represents the Sublime - Text window to show the list of installed packages in. - """ - - self.window = window - threading.Thread.__init__(self) - ExistingPackagesCommand.__init__(self) - - def run(self): - self.package_list = self.make_package_list() - - def show_quick_panel(): - if not self.package_list: - sublime.error_message(('%s: There are no packages ' + - 'to list.') % __name__) - return - self.window.show_quick_panel(self.package_list, self.on_done) - sublime.set_timeout(show_quick_panel, 10) - - def on_done(self, picked): - """ - Quick panel user selection handler - opens the homepage for any - selected package in the user's browser - - :param picked: - An integer of the 0-based package name index from the presented - list. -1 means the user cancelled. - """ - - if picked == -1: - return - package_name = self.package_list[picked][0] - - def open_dir(): - self.window.run_command('open_dir', - {"dir": os.path.join(sublime.packages_path(), package_name)}) - sublime.set_timeout(open_dir, 10) - - -class RemovePackageCommand(sublime_plugin.WindowCommand, - ExistingPackagesCommand): - """ - A command that presents a list of installed packages, allowing the user to - select one to remove - """ - - def __init__(self, window): - """ - :param window: - An instance of :class:`sublime.Window` that represents the Sublime - Text window to show the list of installed packages in. - """ - - self.window = window - ExistingPackagesCommand.__init__(self) - - def run(self): - self.package_list = self.make_package_list('remove') - if not self.package_list: - sublime.error_message(('%s: There are no packages ' + - 'that can be removed.') % __name__) - return - self.window.show_quick_panel(self.package_list, self.on_done) - - def on_done(self, picked): - """ - Quick panel user selection handler - deletes the selected package - - :param picked: - An integer of the 0-based package name index from the presented - list. -1 means the user cancelled. - """ - - if picked == -1: - return - package = self.package_list[picked][0] - - # Don't disable Package Control so it does not get stuck disabled - if package != 'Package Control': - settings = sublime.load_settings(preferences_filename()) - ignored = settings.get('ignored_packages') - if not ignored: - ignored = [] - if not package in ignored: - ignored.append(package) - settings.set('ignored_packages', ignored) - sublime.save_settings(preferences_filename()) - - ignored.remove(package) - thread = RemovePackageThread(self.manager, package, - ignored) - thread.start() - ThreadProgress(thread, 'Removing package %s' % package, - 'Package %s successfully removed' % package) - - -class RemovePackageThread(threading.Thread): - """ - A thread to run the remove package operation in so that the Sublime Text - UI does not become frozen - """ - - def __init__(self, manager, package, ignored): - self.manager = manager - self.package = package - self.ignored = ignored - threading.Thread.__init__(self) - - def run(self): - self.result = self.manager.remove_package(self.package) - - def unignore_package(): - settings = sublime.load_settings(preferences_filename()) - settings.set('ignored_packages', self.ignored) - sublime.save_settings(preferences_filename()) - sublime.set_timeout(unignore_package, 10) - - -class AddRepositoryChannelCommand(sublime_plugin.WindowCommand): - """ - A command to add a new channel (list of repositories) to the user's machine - """ - - def run(self): - self.window.show_input_panel('Channel JSON URL', '', - self.on_done, self.on_change, self.on_cancel) - - def on_done(self, input): - """ - Input panel handler - adds the provided URL as a channel - - :param input: - A string of the URL to the new channel - """ - - settings = sublime.load_settings('%s.sublime-settings' % __name__) - repository_channels = settings.get('repository_channels', []) - if not repository_channels: - repository_channels = [] - repository_channels.append(input) - settings.set('repository_channels', repository_channels) - sublime.save_settings('%s.sublime-settings' % __name__) - sublime.status_message(('Channel %s successfully ' + - 'added') % input) - - def on_change(self, input): - pass - - def on_cancel(self): - pass - - -class AddRepositoryCommand(sublime_plugin.WindowCommand): - """ - A command to add a new repository to the user's machine - """ - - def run(self): - self.window.show_input_panel('GitHub or BitBucket Web URL, or Custom' + - ' JSON Repository URL', '', self.on_done, - self.on_change, self.on_cancel) - - def on_done(self, input): - """ - Input panel handler - adds the provided URL as a repository - - :param input: - A string of the URL to the new repository - """ - - settings = sublime.load_settings('%s.sublime-settings' % __name__) - repositories = settings.get('repositories', []) - if not repositories: - repositories = [] - repositories.append(input) - settings.set('repositories', repositories) - sublime.save_settings('%s.sublime-settings' % __name__) - sublime.status_message('Repository %s successfully added' % input) - - def on_change(self, input): - pass - - def on_cancel(self): - pass - - -class DisablePackageCommand(sublime_plugin.WindowCommand): - """ - A command that adds a package to Sublime Text's ignored packages list - """ - - def run(self): - manager = PackageManager() - packages = manager.list_all_packages() - self.settings = sublime.load_settings(preferences_filename()) - ignored = self.settings.get('ignored_packages') - if not ignored: - ignored = [] - self.package_list = list(set(packages) - set(ignored)) - self.package_list.sort() - if not self.package_list: - sublime.error_message(('%s: There are no enabled packages' + - 'to disable.') % __name__) - return - self.window.show_quick_panel(self.package_list, self.on_done) - - def on_done(self, picked): - """ - Quick panel user selection handler - disables the selected package - - :param picked: - An integer of the 0-based package name index from the presented - list. -1 means the user cancelled. - """ - - if picked == -1: - return - package = self.package_list[picked] - ignored = self.settings.get('ignored_packages') - if not ignored: - ignored = [] - ignored.append(package) - self.settings.set('ignored_packages', ignored) - sublime.save_settings(preferences_filename()) - sublime.status_message(('Package %s successfully added to list of ' + - 'disabled packages - restarting Sublime Text may be required') % - package) - - -class EnablePackageCommand(sublime_plugin.WindowCommand): - """ - A command that removes a package from Sublime Text's ignored packages list - """ - - def run(self): - self.settings = sublime.load_settings(preferences_filename()) - self.disabled_packages = self.settings.get('ignored_packages') - self.disabled_packages.sort() - if not self.disabled_packages: - sublime.error_message(('%s: There are no disabled packages ' + - 'to enable.') % __name__) - return - self.window.show_quick_panel(self.disabled_packages, self.on_done) - - def on_done(self, picked): - """ - Quick panel user selection handler - enables the selected package - - :param picked: - An integer of the 0-based package name index from the presented - list. -1 means the user cancelled. - """ - - if picked == -1: - return - package = self.disabled_packages[picked] - ignored = self.settings.get('ignored_packages') - self.settings.set('ignored_packages', - list(set(ignored) - set([package]))) - sublime.save_settings(preferences_filename()) - sublime.status_message(('Package %s successfully removed from list ' + - 'of disabled packages - restarting Sublime Text may be required') % - package) - - -class AutomaticUpgrader(threading.Thread): - """ - Automatically checks for updated packages and installs them. controlled - by the `auto_upgrade`, `auto_upgrade_ignore`, `auto_upgrade_frequency` and - `auto_upgrade_last_run` settings. - """ - - def __init__(self, found_packages): - """ - :param found_packages: - A list of package names for the packages that were found to be - installed on the machine. - """ - - self.installer = PackageInstaller() - self.manager = self.installer.manager - - self.load_settings() - - self.package_renamer = PackageRenamer() - self.package_renamer.load_settings() - - self.auto_upgrade = self.settings.get('auto_upgrade') - self.auto_upgrade_ignore = self.settings.get('auto_upgrade_ignore') - - self.next_run = int(time.time()) - self.last_run = None - last_run_file = os.path.join(sublime.packages_path(), 'User', - 'Package Control.last-run') - - if os.path.isfile(last_run_file): - with open(last_run_file) as fobj: - try: - self.last_run = int(fobj.read()) - except ValueError: - pass - - frequency = self.settings.get('auto_upgrade_frequency') - if frequency: - if self.last_run: - self.next_run = int(self.last_run) + (frequency * 60 * 60) - else: - self.next_run = time.time() - - # Detect if a package is missing that should be installed - self.missing_packages = list(set(self.installed_packages) - - set(found_packages)) - - if self.auto_upgrade and self.next_run <= time.time(): - with open(last_run_file, 'w') as fobj: - fobj.write(str(int(time.time()))) - - threading.Thread.__init__(self) - - def load_settings(self): - """ - Loads the list of installed packages from the - Package Control.sublime-settings file - """ - - self.settings_file = '%s.sublime-settings' % __name__ - self.settings = sublime.load_settings(self.settings_file) - self.installed_packages = self.settings.get('installed_packages', []) - self.should_install_missing = self.settings.get('install_missing') - if not isinstance(self.installed_packages, list): - self.installed_packages = [] - - def run(self): - self.install_missing() - - if self.next_run > time.time(): - self.print_skip() - return - - self.upgrade_packages() - - def install_missing(self): - """ - Installs all packages that were listed in the list of - `installed_packages` from Package Control.sublime-settings but were not - found on the filesystem and passed as `found_packages`. - """ - - if not self.missing_packages or not self.should_install_missing: - return - - print '%s: Installing %s missing packages' % \ - (__name__, len(self.missing_packages)) - for package in self.missing_packages: - if self.installer.manager.install_package(package): - print '%s: Installed missing package %s' % \ - (__name__, package) - - def print_skip(self): - """ - Prints a notice in the console if the automatic upgrade is skipped - due to already having been run in the last `auto_upgrade_frequency` - hours. - """ - - last_run = datetime.datetime.fromtimestamp(self.last_run) - next_run = datetime.datetime.fromtimestamp(self.next_run) - date_format = '%Y-%m-%d %H:%M:%S' - print ('%s: Skipping automatic upgrade, last run at ' + - '%s, next run at %s or after') % (__name__, - last_run.strftime(date_format), next_run.strftime(date_format)) - - def upgrade_packages(self): - """ - Upgrades all packages that are not currently upgraded to the lastest - version. Also renames any installed packages to their new names. - """ - - if not self.auto_upgrade: - return - - self.package_renamer.rename_packages(self.installer) - - packages = self.installer.make_package_list(['install', - 'reinstall', 'downgrade', 'overwrite', 'none'], - ignore_packages=self.auto_upgrade_ignore) - - # If Package Control is being upgraded, just do that and restart - for package in packages: - if package[0] != __name__: - continue - - def reset_last_run(): - settings = sublime.load_settings(self.settings_file) - settings.set('auto_upgrade_last_run', None) - sublime.save_settings(self.settings_file) - sublime.set_timeout(reset_last_run, 1) - packages = [package] - break - - if not packages: - print '%s: No updated packages' % __name__ - return - - print '%s: Installing %s upgrades' % (__name__, len(packages)) - for package in packages: - self.installer.manager.install_package(package[0]) - version = re.sub('^.*?(v[\d\.]+).*?$', '\\1', package[2]) - if version == package[2] and version.find('pull with') != -1: - vcs = re.sub('^pull with (\w+).*?$', '\\1', version) - version = 'latest %s commit' % vcs - print '%s: Upgraded %s to %s' % (__name__, package[0], version) - - -class PackageCleanup(threading.Thread, PackageRenamer): - """ - Cleans up folders for packages that were removed, but that still have files - in use. - """ - - def __init__(self): - self.manager = PackageManager() - self.load_settings() - threading.Thread.__init__(self) - - def run(self): - found_pkgs = [] - installed_pkgs = self.installed_packages - for package_name in os.listdir(sublime.packages_path()): - package_dir = os.path.join(sublime.packages_path(), package_name) - metadata_path = os.path.join(package_dir, 'package-metadata.json') - - # Cleanup packages that could not be removed due to in-use files - cleanup_file = os.path.join(package_dir, 'package-control.cleanup') - if os.path.exists(cleanup_file): - try: - shutil.rmtree(package_dir) - print '%s: Removed old directory for package %s' % \ - (__name__, package_name) - except (OSError) as (e): - if not os.path.exists(cleanup_file): - open(cleanup_file, 'w').close() - print ('%s: Unable to remove old directory for package ' + - '%s - deferring until next start: %s') % (__name__, - package_name, unicode_from_os(e)) - - # Finish reinstalling packages that could not be upgraded due to - # in-use files - reinstall = os.path.join(package_dir, 'package-control.reinstall') - if os.path.exists(reinstall): - if not clear_directory(package_dir, [metadata_path]): - if not os.path.exists(reinstall): - open(reinstall, 'w').close() - # Assigning this here prevents the callback from referencing the value - # of the "package_name" variable when it is executed - restart_message = ('%s: An error occurred while trying to ' + - 'finish the upgrade of %s. You will most likely need to ' + - 'restart your computer to complete the upgrade.') % ( - __name__, package_name) - def show_still_locked(): - sublime.error_message(restart_message) - sublime.set_timeout(show_still_locked, 10) - else: - self.manager.install_package(package_name) - - # This adds previously installed packages from old versions of PC - if os.path.exists(metadata_path) and \ - package_name not in self.installed_packages: - installed_pkgs.append(package_name) - params = { - 'package': package_name, - 'operation': 'install', - 'version': \ - self.manager.get_metadata(package_name).get('version') - } - self.manager.record_usage(params) - - found_pkgs.append(package_name) - - sublime.set_timeout(lambda: self.finish(installed_pkgs, found_pkgs), 10) - - def finish(self, installed_pkgs, found_pkgs): - """ - A callback that can be run the main UI thread to perform saving of the - Package Control.sublime-settings file. Also fires off the - :class:`AutomaticUpgrader`. - - :param installed_pkgs: - A list of the string package names of all "installed" packages, - even ones that do not appear to be in the filesystem. - - :param found_pkgs: - A list of the string package names of all packages that are - currently installed on the filesystem. - """ - - self.save_packages(installed_pkgs) - AutomaticUpgrader(found_pkgs).start() - - -# Start shortly after Sublime starts so package renames don't cause errors -# with keybindings, settings, etc disappearing in the middle of parsing -sublime.set_timeout(lambda: PackageCleanup().start(), 2000) +import sublime +import sys +import os +import locale + + +st_version = 2 + +# Warn about out-dated versions of ST3 +if sublime.version() == '': + st_version = 3 + print('Package Control: Please upgrade to Sublime Text 3 build 3012 or newer') + +elif int(sublime.version()) > 3000: + st_version = 3 + + +if st_version == 3: + installed_dir, _ = __name__.split('.') +elif st_version == 2: + installed_dir = os.path.basename(os.getcwd()) + + +# Ensure the user has installed Package Control properly +if installed_dir != 'Package Control': + message = (u"Package Control\n\nThis package appears to be installed " + + u"incorrectly.\n\nIt should be installed as \"Package Control\", " + + u"but seems to be installed as \"%s\".\n\n" % installed_dir) + # If installed unpacked + if os.path.exists(os.path.join(sublime.packages_path(), installed_dir)): + message += (u"Please use the Preferences > Browse Packages... menu " + + u"entry to open the \"Packages/\" folder and rename" + + u"\"%s/\" to \"Package Control/\" " % installed_dir) + # If installed as a .sublime-package file + else: + message += (u"Please use the Preferences > Browse Packages... menu " + + u"entry to open the \"Packages/\" folder, then browse up a " + + u"folder and into the \"Installed Packages/\" folder.\n\n" + + u"Inside of \"Installed Packages/\", rename " + + u"\"%s.sublime-package\" to " % installed_dir + + u"\"Package Control.sublime-package\" ") + message += u"and restart Sublime Text." + sublime.error_message(message) + +# Normal execution will finish setting up the package +else: + reloader_name = 'package_control.reloader' + + # ST3 loads each package as a module, so it needs an extra prefix + if st_version == 3: + reloader_name = 'Package Control.' + reloader_name + from imp import reload + + # Make sure all dependencies are reloaded on upgrade + if reloader_name in sys.modules: + reload(sys.modules[reloader_name]) + + + try: + # Python 3 + from .package_control import reloader + + from .package_control.commands import * + from .package_control.package_cleanup import PackageCleanup + + except (ValueError): + # Python 2 + from package_control import reloader + from package_control import sys_path + + from package_control.commands import * + from package_control.package_cleanup import PackageCleanup + + + def plugin_loaded(): + # Make sure the user's locale can handle non-ASCII. A whole bunch of + # work was done to try and make Package Control work even if the locale + # was poorly set, by manually encoding all file paths, but it ended up + # being a fool's errand since the package loading code built into + # Sublime Text is not written to work that way, and although packages + # could be installed, they could not be loaded properly. + try: + os.path.exists(os.path.join(sublime.packages_path(), u"fran\u00e7ais")) + except (UnicodeEncodeError) as e: + message = (u"Package Control\n\nYour system's locale is set to a " + + u"value that can not handle non-ASCII characters. Package " + + u"Control can not properly work unless this is fixed.\n\n" + + u"On Linux, please reference your distribution's docs for " + + u"information on properly setting the LANG environmental " + + u"variable. As a temporary work-around, you can launch " + + u"Sublime Text from the terminal with:\n\n" + + u"LANG=en_US.UTF-8 sublime_text") + sublime.error_message(message) + return + + # Start shortly after Sublime starts so package renames don't cause errors + # with keybindings, settings, etc disappearing in the middle of parsing + sublime.set_timeout(lambda: PackageCleanup().start(), 2000) + + if st_version == 2: + plugin_loaded() diff --git a/sublime/Packages/Package Control/Package Control.sublime-settings b/sublime/Packages/Package Control/Package Control.sublime-settings index 7008abe..03e1594 100644 --- a/sublime/Packages/Package Control/Package Control.sublime-settings +++ b/sublime/Packages/Package Control/Package Control.sublime-settings @@ -1,138 +1,166 @@ -{ - // A list of URLs that each contain a JSON file with a list of repositories. - // The repositories from these channels are placed in order after the - // repositories from the "repositories" setting - "repository_channels": [ - "https://sublime.wbond.net/repositories.json" - ], - - // A list of URLs that contain a packages JSON file. These repositories - // are placed in order before repositories from the "repository_channels" - // setting - "repositories": [], - - // A list of CA certs needed for domains. The default channel provides a - // list of domains and an identifier (the md5 hash) for the CA cert(s) - // necessary for each. - // - // If a custom cert is required for a proxy or for an alternate channel - // or repository domain name, it should be added in one of the two forms: - // - // "*": ["my_identifier", "https://example.com/url/of/cert_file"] - // "*": ["my_identifier_2", "/absolute/filesystem/path/to/cert_file"] - // - // In both cases the literal "*" means the cert will be checked to ensure - // it is present for accessing any URL. This is necessary for proxy - // connections, but also useful if you want to provide you own - // ca-bundle.crt file. - // - // The "my_identifier" and "my_identifier_2" can be any unique string - // that Package Control can use as a filename, and ensures that it has - // merged the cert file with the ca-bundle.crt file in the certs/ directory - // since that is what is passed to the downloaders. - "certs": { - "api.bitbucket.org": ["d867a7b2aecc46f9c31afc4f2f50de05", ""], - "api.github.com": ["1c5282418e2cb4989cd6beddcdbab0b5", ""], - "bitbucket.org": ["897abe0b41fd2f64e9e2e351cbc36d76", ""], - "nodeload.github.com": ["1c5282418e2cb4989cd6beddcdbab0b5", ""], - "raw.github.com": ["1c5282418e2cb4989cd6beddcdbab0b5", ""], - "sublime.wbond.net": ["7f4f8622b4fd001c7f648e09aae7edaa", ""] - }, - - // If debugging information for HTTP/HTTPS connections should be printed - // to the Sublime Text console - "debug": false, - - // This helps solve naming issues where a repository it not named the - // same as the package should be. This is primarily only useful for - // GitHub and BitBucket repositories. This mapping will override the - // mapping that is retrieved from the repository channels. - "package_name_map": {}, - - // If package install, upgrade and removal info should be submitted to - // the channel for aggregated statistics - "submit_usage": true, - - // The URL to post install, upgrade and removal notices to - "submit_url": "https://sublime.wbond.net/submit", - - // If packages should be automatically upgraded when ST2 starts - "auto_upgrade": true, - - // If missing packages should be automatically installed when ST2 starts - "install_missing": true, - - // The minimum frequency in hours in which to check for automatic upgrades, - // setting this to 0 will always check for automatic upgrades - "auto_upgrade_frequency": 1, - - // Packages to not auto upgrade - "auto_upgrade_ignore": [], - - // Timeout for downloading channels, repositories and packages - "timeout": 30, - - // The number of seconds to cache repository and package info for - "cache_length": 300, - - // An HTTP proxy server to use for requests - "http_proxy": "", - // An HTTPS proxy server to use for requests - this will inherit from - // http_proxy if it is set to "" or null and http_proxy has a value. You - // can set this to false to prevent inheriting from http_proxy. - "https_proxy": "", - - // Username and password for both http_proxy and https_proxy - "proxy_username": "", - "proxy_password": "", - - // User agent for HTTP requests - "user_agent": "Sublime Package Control", - - // Setting this to true will cause Package Control to ignore all git - // and hg repositories - this may help if trying to list packages to install - // hangs - "ignore_vcs_packages": false, - - // Custom paths to VCS binaries for when they can't be automatically - // found on the system and a package includes a VCS metadata directory - "git_binary": "", - "git_update_command": ["pull", "origin", "master", "--ff", "--commit"], - - "hg_binary": "", - - // Be sure to keep the remote name as the last argument - "hg_update_command": ["pull", "--update", "default"], - - // Directories to ignore when creating a package - "dirs_to_ignore": [ - ".hg", ".git", ".svn", "_darcs", "CVS" - ], - - // Files to ignore when creating a package - "files_to_ignore": [ - ".hgignore", ".gitignore", ".bzrignore", "*.pyc", "*.sublime-project", - "*.sublime-workspace", "*.tmTheme.cache" - ], - - // Files to include, even if they match a pattern in files_to_ignore - "files_to_include": [], - - // Files to ignore when creating a binary package. By default binary - // packages ship with .pyc files instead of .py files. If an __init__.py - // file exists, it will always be included, even if it matches one of - // these patterns. - "files_to_ignore_binary": [ - ".hgignore", ".gitignore", ".bzrignore", "*.py", "*.sublime-project", - "*.sublime-workspace", "*.tmTheme.cache" - ], - - // Files to include for a binary package, even if they match a pattern i - // files_to_ignore_binary - "files_to_include_binary": [ - "__init__.py" - ], - - // When a package is created, copy it to this folder - defaults to Desktop - "package_destination": "" -} \ No newline at end of file +{ + // A list of URLs that each contain a JSON file with a list of repositories. + // The repositories from these channels are placed in order after the + // repositories from the "repositories" setting + "channels": [ + "https://sublime.wbond.net/channel.json" + ], + + // A list of URLs that contain a packages JSON file. These repositories + // are placed in order before repositories from the "channels" + // setting + "repositories": [], + + // A list of CA certs needed for domains. The default channel provides a + // list of domains and an identifier (the md5 hash) for the CA cert(s) + // necessary for each. Not used on Windows since the system CA cert list + // is automatically used via WinINet. + // + // If a custom cert is required for a proxy or for an alternate channel + // or repository domain name, it should be added in one of the two forms: + // + // "*": ["my_identifier", "https://example.com/url/of/cert_file"] + // "*": ["my_identifier_2", "/absolute/filesystem/path/to/cert_file"] + // + // In both cases the literal "*" means the cert will be checked to ensure + // it is present for accessing any URL. This is necessary for proxy + // connections, but also useful if you want to provide you own + // Pckage Control.ca-bundle file. + // + // The "my_identifier" and "my_identifier_2" can be any unique string + // that Package Control can use as a filename, and ensures that it has + // merged the cert file with the ca-bundle.crt file in the certs/ directory + // since that is what is passed to the downloaders. + "certs": { + "api.bitbucket.org": ["7d0986b90061d60c8c02aa3b1cf23850", "https://sublime.wbond.net/certs/7d0986b90061d60c8c02aa3b1cf23850"], + "api.github.com": ["7d0986b90061d60c8c02aa3b1cf23850", "https://sublime.wbond.net/certs/7d0986b90061d60c8c02aa3b1cf23850"], + "bitbucket.org": ["7d0986b90061d60c8c02aa3b1cf23850", "https://sublime.wbond.net/certs/7d0986b90061d60c8c02aa3b1cf23850"], + "codeload.github.com": ["7d0986b90061d60c8c02aa3b1cf23850", "https://sublime.wbond.net/certs/7d0986b90061d60c8c02aa3b1cf23850"], + "downloads.sourceforge.net": ["221e907bdfff70d71cea42361ae209d5", "https://sublime.wbond.net/certs/221e907bdfff70d71cea42361ae209d5"], + "github.com": ["7d0986b90061d60c8c02aa3b1cf23850", "https://sublime.wbond.net/certs/7d0986b90061d60c8c02aa3b1cf23850"], + "nodeload.github.com": ["7d0986b90061d60c8c02aa3b1cf23850", "https://sublime.wbond.net/certs/7d0986b90061d60c8c02aa3b1cf23850"], + "raw.github.com": ["7d0986b90061d60c8c02aa3b1cf23850", "https://sublime.wbond.net/certs/7d0986b90061d60c8c02aa3b1cf23850"], + "sublime.wbond.net": ["221e907bdfff70d71cea42361ae209d5", "https://sublime.wbond.net/certs/221e907bdfff70d71cea42361ae209d5"] + }, + + // Install pre-release versions of packages. If this is false, versions + // under 1.0.0 will still be installed. Only packages using the SemVer + // -prerelease suffixes will be ignored. + "install_prereleases": false, + + // If debugging information for HTTP/HTTPS connections should be printed + // to the Sublime Text console + "debug": false, + + // This helps solve naming issues where a repository it not named the + // same as the package should be. This is primarily only useful for + // GitHub and BitBucket repositories. This mapping will override the + // mapping that is retrieved from the repository channels. + "package_name_map": {}, + + // If package install, upgrade and removal info should be submitted to + // the channel for aggregated statistics + "submit_usage": true, + + // The URL to post install, upgrade and removal notices to + "submit_url": "https://sublime.wbond.net/submit", + + // If packages should be automatically upgraded when ST2 starts + "auto_upgrade": true, + + // If missing packages should be automatically installed when ST2 starts + "install_missing": true, + + // The minimum frequency in hours in which to check for automatic upgrades, + // setting this to 0 will always check for automatic upgrades + "auto_upgrade_frequency": 1, + + // Packages to not auto upgrade + "auto_upgrade_ignore": [], + + // Timeout for downloading channels, repositories and packages. Doesn't + // have an effect on Windows due to a bug in WinINet. + "timeout": 30, + + // The number of seconds to cache repository and package info for + "cache_length": 300, + + // An HTTP proxy server to use for requests. Not used on Windows since the + // system proxy configuration is utilized via WinINet. + "http_proxy": "", + // An HTTPS proxy server to use for requests - this will inherit from + // http_proxy if it is set to "" or null and http_proxy has a value. You + // can set this to false to prevent inheriting from http_proxy. Not used on + // Windows since the system proxy configuration is utilized via WinINet. + "https_proxy": "", + + // Username and password for both http_proxy and https_proxy. May be used + // with WinINet to set credentials for system-level proxy config. + "proxy_username": "", + "proxy_password": "", + + // If HTTP responses should be cached to disk + "http_cache": true, + + // Number of seconds to cache HTTP responses for, defaults to one week + "http_cache_length": 604800, + + // User agent for HTTP requests. If "%s" is present, will be replaced + // with the current version. + "user_agent": "Sublime Package Control v%s", + + // Setting this to true will cause Package Control to ignore all git + // and hg repositories - this may help if trying to list packages to install + // hangs + "ignore_vcs_packages": false, + + // Custom paths to VCS binaries for when they can't be automatically + // found on the system and a package includes a VCS metadata directory + "git_binary": "", + + // This should NOT contain the name of the remote or branch - that will + // be automatically determined. + "git_update_command": ["pull", "--ff", "--commit"], + + "hg_binary": "", + + // For HG repositories, be sure to use "default" as the remote URL. + // This is the default behavior when cloning an HG repo. + "hg_update_command": ["pull", "--update"], + + // Full path to the openssl binary, if not found on your machine. This is + // only used when running the Grab CA Certs command. + "openssl_binary": "", + + // Directories to ignore when creating a package + "dirs_to_ignore": [ + ".hg", ".git", ".svn", "_darcs", "CVS" + ], + + // Files to ignore when creating a package + "files_to_ignore": [ + ".hgignore", ".gitignore", ".bzrignore", "*.pyc", "*.sublime-project", + "*.sublime-workspace", "*.tmTheme.cache" + ], + + // Files to include, even if they match a pattern in files_to_ignore + "files_to_include": [], + + // Files to ignore when creating a binary package. By default binary + // packages ship with .pyc files instead of .py files. If an __init__.py + // file exists, it will always be included, even if it matches one of + // these patterns. + "files_to_ignore_binary": [ + ".hgignore", ".gitignore", ".bzrignore", "*.py", "*.sublime-project", + "*.sublime-workspace", "*.tmTheme.cache" + ], + + // Files to include for a binary package, even if they match a pattern i + // files_to_ignore_binary + "files_to_include_binary": [ + "__init__.py" + ], + + // When a package is created, copy it to this folder - defaults to Desktop + "package_destination": "" +} diff --git a/sublime/Packages/Package Control/example-channel.json b/sublime/Packages/Package Control/example-channel.json new file mode 100644 index 0000000..75aeac3 --- /dev/null +++ b/sublime/Packages/Package Control/example-channel.json @@ -0,0 +1,64 @@ +{ + "schema_version": "2.0", + + // All repositories must be an HTTP or HTTPS URL. HTTPS is vastly superior + // since verification of the source server is performed on SSL certificates. + "repositories": [ + "http://sublime.wbond.net/packages.json", + "https://github.com/buymeasoda/soda-theme", + "https://github.com/SublimeText" + ], + + // The "packages_cache" is completely optional, but allows the + // channel to cache and deliver package data from multiple + // repositories in a single HTTP request, allowing for significantly + // improved performance. + "packages_cache": { + + // The first level keys are the repository URLs + "http://sublime.wbond.net/packages.json": [ + + // Each repository has an array of packages with their fully + // expanded info. This means that the "details" key must be expanded + // into the various keys it provides. + { + "name": "Alignment", + "description": "Multi-line and multiple selection alignment plugin", + "author": "wbond", + "homepage": "http://wbond.net/sublime_packages/alignment", + "releases": [ + { + "version": "2.0.0", + "url": "https://sublime.wbond.net/Alignment.sublime-package", + "date": "2011-09-18 20:12:41" + } + ] + } + ] + }, + + // Package Control ships with the SSL Certificate Authority (CA) cert for the + // SSL certificate that secures and identifies sublime.wbond.net. After this + // initial connection is made, the channel server provides a list of CA certs + // for the various URLs that Package Control need to connect to. This way the + // default channel (https://sublime.wbond.net/channel.json) can provide + // real-time updates to CA certs in the case that a CA is compromised. The + // CA certs are extracted from openssl, and the server runs on an LTS version + // of Ubuntu, which automatically applies security patches from the official + // Ubuntu repositories. This architecture helps to ensure that the packages + // being downloaded are from the source listed and that users are very + // unlikely to be the subject of the man-in-the-middle attack. + "certs": { + + // All certs have the domain they apply to as the key + "sublime.wbond.net": [ + // The value is an array of two elements, the first being an md5 + // hash of the contents of the certificate. This helps in detecting + // CA cert changes. The second element is the URL where the cert + // can be downloaded, if it is not already installed on the user’s + // copy of Sublime Text. + "7f4f8622b4fd001c7f648e09aae7edaa", + "https://sublime.wbond.net/certs/7f4f8622b4fd001c7f648e09aae7edaa" + ] + } +} \ No newline at end of file diff --git a/sublime/Packages/Package Control/example-repository.json b/sublime/Packages/Package Control/example-repository.json new file mode 100644 index 0000000..39fe43d --- /dev/null +++ b/sublime/Packages/Package Control/example-repository.json @@ -0,0 +1,275 @@ +{ + "schema_version": "2.0", + + // Packages can be specified with a simple URL to a GitHub or BitBucket + // repository, but details can be overridden for every field. It is + // also possible not utilize GitHub or BitBucket at all, but just to + // host your packages on any server with an SSL certificate. + "packages": [ + + // This is what most packages should aim to model. + // + // The majority of the information about a package ("name", + // "description", "author") are all pulled from the GitHub (or + // BitBucket) repository info. + // + // If the word "sublime" exists in the repository name, the name + // can be overridden by the "name" key. + // + // A release is created from the the tag that is the highest semantic + // versioning version number in the list of tags. + { + "name": "Alignment", + "details": "https://github.com/wbond/sublime_alignment", + "releases": [ + { + "details": "https://github.com/wbond/sublime_alignment/tags" + } + ] + }, + + // Here is an equivalent package being pulled from BitBucket + { + "name": "Alignment", + "details": "https://bitbucket.org/wbond/sublime_alignment", + "releases": [ + { + "details": "https://bitbucket.org/wbond/sublime_alignment#tags" + } + ] + }, + + // Pull most details from GitHub, releases from master branch. + // This form is discouraged because users will upgrade to every single + // commit you make to master. + { + "details": "https://github.com/wbond/sublime_alignment" + }, + + // Pull most details from a BitBucket repository and releases from + // the branch "default" or "master", depending on how your repository + // is configured. + // Similar to the above example, this form is discouraged because users + // will upgrade to every single commit you make to master. + { + "details": "https://bitbucket.org/wbond/sublime_alignment" + }, + + // Use a custom name instead of just the URL slug + { + "name": "Alignment", + "details": "https://github.com/wbond/sublime_alignment" + }, + + // You can also override the homepage and author + { + "name": "Alignment", + "details": "https://github.com/wbond/sublime_alignment", + "homepage": "http://wbond.net/sublime_packages/alignment", + "author": "wbond" + }, + + // It is possible to provide the URL to a readme file. This URL + // should be to the raw source of the file, not rendered HTML. + // GitHub and BitBucket repositories will automatically provide + // these. + // + // The following extensions will be rendered: + // + // .markdown, .mdown, .mkd, .md + // .texttile + // .creole + // .rst + // + // All others are treated as plaintext. + { + "details": "https://github.com/wbond/sublime_alignment", + "readme": "https://raw.github.com/wbond/sublime_alignment/master/readme.creole" + }, + + // If a package has a public bug tracker, the URL should be + // included via the "issues" key. Both GitHub and BitBucket + // repositories will automatically provide this if they have + // issues enabled. + { + "details": "https://github.com/wbond/sublime_alignment", + "issues": "https://github.com/wbond/sublime_alignment/issues" + }, + + // The URL to donate to support the development of a package. + // GitHub and BitBucket repositories will default to: + // + // https://www.gittip.com/{username}/ + // + // Other URLs with special integration include: + // + // https://flattr.com/profile/{username} + // https://www.dwolla.com/hub/{username} + // + // This may also contain a URL to another other donation-type site + // where users may support the author for their development of the + // package. + { + "details": "https://github.com/wbond/sublime_alignment", + "donate": "https://www.gittip.com/wbond/" + }, + + // The URL to purchase a license to the package + { + "details": "https://github.com/wbond/sublime_alignment", + "buy": "https://wbond.net/sublime_packages/alignment/buy" + }, + + // If you rename a package, you can provide the previous name(s) + // so that users with the old package name can be automatically + // upgraded to the new one. + { + "name": "Alignment", + "details": "https://github.com/wbond/sublime_alignment", + "previous_names": ["sublime_alignment"] + }, + + // Packages can be labelled for the purpose of creating a + // folksonomy so users may more easily find relevant packages. + // Labels should be all lower case and should use spaces instead + // of _ or - to separate words. + // + // Some suggested labels are listed below, however, anything can + // be used as a label: + // + // auto-complete + // browser integration + // build system + // code navigation + // code sharing + // color scheme + // deprecated + // diff/merge + // editor emulation + // file creation + // file navigation + // formatting + // ftp + // language syntax + // linting + // minification + // search + // snippets + // terminal/shell/repl + // testing + // text manipulation + // text navigation + // theme + // todo + // vcs + { + "details": "https://github.com/wbond/sublime_alignment", + "labels": ["text manipulation", "formatting"] + }, + + // In addition to the recommendation above of pulling releases + // from tags that are semantic version numbers, releases can also + // comefrom a custom branch. + { + "details": "https://github.com/wbond/sublime_alignment", + "releases": [ + { + "details": "https://github.com/wbond/sublime_alignment/tree/custom_branch" + } + ] + }, + + // An equivalent package being pulled from BitBucket. + { + "details": "https://bitbucket.org/wbond/sublime_alignment", + "releases": [ + { + "details": "https://bitbucket.org/wbond/sublime_alignment/src/custom_branch" + } + ] + }, + + // If your package is only compatible with specific builds of + // Sublime Text, this will cause the package to be hidden from + // users with incompatible versions. + { + "details": "https://github.com/wbond/sublime_alignment", + "releases": [ + { + // Could also be >2999 for ST3. Leaving this out indicates + // the package works with both ST2 and ST3. + "sublime_text": "<3000", + "details": "https://github.com/wbond/sublime_alignment" + } + ] + }, + + // The "platforms" key allows specifying what platform(s) the release + // is valid for. As shown, there can be multiple releases of a package + // at any given time. However, only the latest version for any given + // platform/arch will be shown to the user. + // + // The "platforms" key allows specifying a single platform, or a list + // of platforms. Valid platform indentifiers include: + // + // "*" + // "windows", "windows-x64", "windows-x32" + // "osx", "osx-x64" + // "linux", "linux-x32", "linux-x64" + { + "details": "https://github.com/wbond/sublime_alignment", + "releases": [ + { + // Defaults to "*", or all platforms. + "platforms": ["osx", "linux"], + "details": "https://github.com/wbond/sublime_alignment/tree/posix" + }, + { + "platforms": "windows", + "details": "https://github.com/wbond/sublime_alignment/tree/win32" + } + ] + }, + + // If you don't use a "details" key for a "releases" entry, you need to + // specify the "version", "url" and "date" manually. + { + "details": "https://github.com/wbond/sublime_alignment", + "releases": [ + { + // The version number needs to be a semantic version number per + // http://semver.org 2.x.x + "version": "2.0.0", + + // The URL needs to be a zip file containing the package. It is permissible + // for the zip file to contain a single root folder with any name. All + // file will be extracted out of this single root folder. This allows + // zip files from GitHub and BitBucket to be used a sources. + "url": "https://codeload.github.com/wbond/sublime_alignment/zip/v2.0.0", + + // The date MUST be in the form "YYYY-MM-DD HH:MM:SS" and SHOULD be UTC + "date": "2011-09-18 20:12:41" + } + ] + } + ], + + // If you need/want to split your repository up into multiple smaller files + // for the sake of organization, the "includes" key allows you to enter + // URL paths that will be combined together and dynamically inserted + // into the "packages" key. These URLs these can be relative or absolute. + "includes": [ + + // Here is an example of how relative paths work for URLs. If this file + // was loaded from: + // "https://sublime.wbond.net/example-repository.json" + // then the following files would be loaded from: + // "https://sublime.wbond.net/repository/0-9.json" + // "https://sublime.wbond.net/repository/a.json" + "./repository/0-9.json", + "./repository/a.json", + + // An example of an absolute URL + "https://sublime.wbond.net/repository/b.json" + ] +} diff --git a/sublime/Packages/Package Control/messages.json b/sublime/Packages/Package Control/messages.json index 00340ab..8c26db4 100644 --- a/sublime/Packages/Package Control/messages.json +++ b/sublime/Packages/Package Control/messages.json @@ -1,4 +1,5 @@ { "1.3.0": "messages/1.3.0.txt", - "1.6.0": "messages/1.6.0.txt" -} \ No newline at end of file + "1.6.0": "messages/1.6.0.txt", + "2.0.0": "messages/2.0.0.txt" +} diff --git a/sublime/Packages/Package Control/messages/2.0.0.txt b/sublime/Packages/Package Control/messages/2.0.0.txt new file mode 100644 index 0000000..59524ea --- /dev/null +++ b/sublime/Packages/Package Control/messages/2.0.0.txt @@ -0,0 +1,64 @@ +Package Control 2.0.0 Changelog: + + +Today I'd like to announce two big milestones: + + - Package Control 2.0 for ST2 and ST3 + - A new Package Control website at https://sublime.wbond.net + +The full announcement about the PC 2.0 release is available on the news page at +https://sublime.wbond.net/news. + +If you are running the "testing" version of Package Control (1.6.9 - 1.6.11), +you will likely need to restart Sublime Text before Package Control will work +properly. + + +Giving Back + +Part of the new Package Control website is in-depth information about each +package. The new package pages include a link where you can give a tip to the +developer/maintainer of your favorite packages. + +The donate links go to https://www.gittip.com, which is building an excellent, +and open platform for users to say "thank you" to open source developers. It +is possible to give a small amount each week, such as $0.25, however these small +amounts multiplied by the large size of the community can be a big thank you! + +One of the less glamorous jobs involved with making Package Control happen is +reviewing and giving package developers feedback before adding their packages +to the default channel. The follow contributors deserve a big thank you: + +FichteFoll - https://www.gittip.com/FichteFoll/ +joneshf - https://www.gittip.com/on/github/joneshf/ +sentience - https://www.gittip.com/on/github/sentience/ + +Finally, I'm looking to raise some money to obtain a Mac Mini for the purposes +of supporting ST3 on OS X and a license for a Windows 8 VM. If you are inclined +to donate to those, or want to just buy me a beer, check out: + +https://sublime.wbond.net/say_thanks + + +Notable Features + + - A new Windows downloader that uses WinINet and should provide much better + proxy support + + - Using operating system-supplied SSL CA certs on all platforms, with a + deprecated fallback to certificates served through the channel + + - Proxy server fixes for OS X + + - A completely revamped channel and repository system with support for more + information about packages including labels; readme, issues, donate and buy + URLs; tag-based releases; platform targetting without a custom packages.json + file; and Sublime Text version targetting + + - Support for installing via .sublime-package files in ST3, which allows users + to easily override specific files from the package. Package developers who + need a loose folder of files may include a .no-sublime-package file in their + repo. + + - In the coming days the new Package Control website will be released as open + source on GitHub diff --git a/sublime/Packages/Package Control/package-metadata.json b/sublime/Packages/Package Control/package-metadata.json index adff7f1..c8258ac 100644 --- a/sublime/Packages/Package Control/package-metadata.json +++ b/sublime/Packages/Package Control/package-metadata.json @@ -1 +1,5 @@ -{"url": "http://wbond.net/sublime_packages/package_control", "version": "1.6.3", "description": "A full-featured package manager"} \ No newline at end of file +{ + "version": "2.0.0", + "url": "https://sublime.wbond.net", + "description": "A full-featured package manager" +} diff --git a/sublime/Packages/Package Control/package_control/__init__.py b/sublime/Packages/Package Control/package_control/__init__.py new file mode 100644 index 0000000..b541c64 --- /dev/null +++ b/sublime/Packages/Package Control/package_control/__init__.py @@ -0,0 +1,2 @@ +__version__ = "2.0.0" +__version_info__ = (2, 0, 0) diff --git a/sublime/Packages/Package Control/package_control/automatic_upgrader.py b/sublime/Packages/Package Control/package_control/automatic_upgrader.py new file mode 100644 index 0000000..bbebd8a --- /dev/null +++ b/sublime/Packages/Package Control/package_control/automatic_upgrader.py @@ -0,0 +1,215 @@ +import threading +import re +import os +import datetime +import time + +import sublime + +from .console_write import console_write +from .package_installer import PackageInstaller +from .package_renamer import PackageRenamer +from .open_compat import open_compat, read_compat + + +class AutomaticUpgrader(threading.Thread): + """ + Automatically checks for updated packages and installs them. controlled + by the `auto_upgrade`, `auto_upgrade_ignore`, and `auto_upgrade_frequency` + settings. + """ + + def __init__(self, found_packages): + """ + :param found_packages: + A list of package names for the packages that were found to be + installed on the machine. + """ + + self.installer = PackageInstaller() + self.manager = self.installer.manager + + self.load_settings() + + self.package_renamer = PackageRenamer() + self.package_renamer.load_settings() + + self.auto_upgrade = self.settings.get('auto_upgrade') + self.auto_upgrade_ignore = self.settings.get('auto_upgrade_ignore') + + self.load_last_run() + self.determine_next_run() + + # Detect if a package is missing that should be installed + self.missing_packages = list(set(self.installed_packages) - + set(found_packages)) + + if self.auto_upgrade and self.next_run <= time.time(): + self.save_last_run(time.time()) + + threading.Thread.__init__(self) + + def load_last_run(self): + """ + Loads the last run time from disk into memory + """ + + self.last_run = None + + self.last_run_file = os.path.join(sublime.packages_path(), 'User', + 'Package Control.last-run') + + if os.path.isfile(self.last_run_file): + with open_compat(self.last_run_file) as fobj: + try: + self.last_run = int(read_compat(fobj)) + except ValueError: + pass + + def determine_next_run(self): + """ + Figure out when the next run should happen + """ + + self.next_run = int(time.time()) + + frequency = self.settings.get('auto_upgrade_frequency') + if frequency: + if self.last_run: + self.next_run = int(self.last_run) + (frequency * 60 * 60) + else: + self.next_run = time.time() + + def save_last_run(self, last_run): + """ + Saves a record of when the last run was + + :param last_run: + The unix timestamp of when to record the last run as + """ + + with open_compat(self.last_run_file, 'w') as fobj: + fobj.write(str(int(last_run))) + + + def load_settings(self): + """ + Loads the list of installed packages from the + Package Control.sublime-settings file + """ + + self.settings_file = 'Package Control.sublime-settings' + self.settings = sublime.load_settings(self.settings_file) + self.installed_packages = self.settings.get('installed_packages', []) + self.should_install_missing = self.settings.get('install_missing') + if not isinstance(self.installed_packages, list): + self.installed_packages = [] + + def run(self): + self.install_missing() + + if self.next_run > time.time(): + self.print_skip() + return + + self.upgrade_packages() + + def install_missing(self): + """ + Installs all packages that were listed in the list of + `installed_packages` from Package Control.sublime-settings but were not + found on the filesystem and passed as `found_packages`. + """ + + if not self.missing_packages or not self.should_install_missing: + return + + console_write(u'Installing %s missing packages' % len(self.missing_packages), True) + for package in self.missing_packages: + if self.installer.manager.install_package(package): + console_write(u'Installed missing package %s' % package, True) + + def print_skip(self): + """ + Prints a notice in the console if the automatic upgrade is skipped + due to already having been run in the last `auto_upgrade_frequency` + hours. + """ + + last_run = datetime.datetime.fromtimestamp(self.last_run) + next_run = datetime.datetime.fromtimestamp(self.next_run) + date_format = '%Y-%m-%d %H:%M:%S' + message_string = u'Skipping automatic upgrade, last run at %s, next run at %s or after' % ( + last_run.strftime(date_format), next_run.strftime(date_format)) + console_write(message_string, True) + + def upgrade_packages(self): + """ + Upgrades all packages that are not currently upgraded to the lastest + version. Also renames any installed packages to their new names. + """ + + if not self.auto_upgrade: + return + + self.package_renamer.rename_packages(self.installer) + + package_list = self.installer.make_package_list(['install', + 'reinstall', 'downgrade', 'overwrite', 'none'], + ignore_packages=self.auto_upgrade_ignore) + + # If Package Control is being upgraded, just do that and restart + for package in package_list: + if package[0] != 'Package Control': + continue + + def reset_last_run(): + # Re-save the last run time so it runs again after PC has + # been updated + self.save_last_run(self.last_run) + sublime.set_timeout(reset_last_run, 1) + package_list = [package] + break + + if not package_list: + console_write(u'No updated packages', True) + return + + console_write(u'Installing %s upgrades' % len(package_list), True) + + disabled_packages = [] + + def do_upgrades(): + # Wait so that the ignored packages can be "unloaded" + time.sleep(0.5) + + # We use a function to generate the on-complete lambda because if + # we don't, the lambda will bind to info at the current scope, and + # thus use the last value of info from the loop + def make_on_complete(name): + return lambda: self.installer.reenable_package(name) + + for info in package_list: + if info[0] in disabled_packages: + on_complete = make_on_complete(info[0]) + else: + on_complete = None + + self.installer.manager.install_package(info[0]) + + version = re.sub('^.*?(v[\d\.]+).*?$', '\\1', info[2]) + if version == info[2] and version.find('pull with') != -1: + vcs = re.sub('^pull with (\w+).*?$', '\\1', version) + version = 'latest %s commit' % vcs + message_string = u'Upgraded %s to %s' % (info[0], version) + console_write(message_string, True) + if on_complete: + sublime.set_timeout(on_complete, 1) + + # Disabling a package means changing settings, which can only be done + # in the main thread. We then create a new background thread so that + # the upgrade process does not block the UI. + def disable_packages(): + disabled_packages.extend(self.installer.disable_packages([info[0] for info in package_list])) + threading.Thread(target=do_upgrades).start() + sublime.set_timeout(disable_packages, 1) diff --git a/sublime/Packages/Package Control/package_control/ca_certs.py b/sublime/Packages/Package Control/package_control/ca_certs.py new file mode 100644 index 0000000..d29d2e0 --- /dev/null +++ b/sublime/Packages/Package Control/package_control/ca_certs.py @@ -0,0 +1,378 @@ +import hashlib +import os +import re +import time +import sys + +from .cmd import Cli +from .console_write import console_write +from .open_compat import open_compat, read_compat + + +# Have somewhere to store the CA bundle, even when not running in Sublime Text +try: + import sublime + ca_bundle_dir = None +except (ImportError): + ca_bundle_dir = os.path.join(os.path.expanduser('~'), '.package_control') + if not os.path.exists(ca_bundle_dir): + os.mkdir(ca_bundle_dir) + + +def find_root_ca_cert(settings, domain): + runner = OpensslCli(settings.get('openssl_binary'), settings.get('debug')) + binary = runner.retrieve_binary() + + args = [binary, 's_client', '-showcerts', '-connect', domain + ':443'] + result = runner.execute(args, os.path.dirname(binary)) + + certs = [] + temp = [] + + in_block = False + for line in result.splitlines(): + if line.find('BEGIN CERTIFICATE') != -1: + in_block = True + if in_block: + temp.append(line) + if line.find('END CERTIFICATE') != -1: + in_block = False + certs.append(u"\n".join(temp)) + temp = [] + + # Remove the cert for the domain itself, just leaving the + # chain cert and the CA cert + certs.pop(0) + + # Look for the "parent" root CA cert + subject = openssl_get_cert_subject(settings, certs[-1]) + issuer = openssl_get_cert_issuer(settings, certs[-1]) + + cert = get_ca_cert_by_subject(settings, issuer) + cert_hash = hashlib.md5(cert.encode('utf-8')).hexdigest() + + return [cert, cert_hash] + + + +def get_system_ca_bundle_path(settings): + """ + Get the filesystem path to the system CA bundle. On Linux it looks in a + number of predefined places, however on OS X it has to be programatically + exported from the SystemRootCertificates.keychain. Windows does not ship + with a CA bundle, but also we use WinINet on Windows, so we don't need to + worry about CA certs. + + :param settings: + A dict to look in for `debug` and `openssl_binary` keys + + :return: + The full filesystem path to the .ca-bundle file, or False on error + """ + + # If the sublime module is available, we bind this value at run time + # since the sublime.packages_path() is not available at import time + global ca_bundle_dir + + platform = sys.platform + debug = settings.get('debug') + + ca_path = False + + if platform == 'win32': + console_write(u"Unable to get system CA cert path since Windows does not ship with them", True) + return False + + # OS X + if platform == 'darwin': + if not ca_bundle_dir: + ca_bundle_dir = os.path.join(sublime.packages_path(), 'User') + ca_path = os.path.join(ca_bundle_dir, 'Package Control.system-ca-bundle') + + exists = os.path.exists(ca_path) + # The bundle is old if it is a week or more out of date + is_old = exists and os.stat(ca_path).st_mtime < time.time() - 604800 + + if not exists or is_old: + if debug: + console_write(u"Generating new CA bundle from system keychain", True) + _osx_create_ca_bundle(settings, ca_path) + if debug: + console_write(u"Finished generating new CA bundle at %s" % ca_path, True) + elif debug: + console_write(u"Found previously exported CA bundle at %s" % ca_path, True) + + # Linux + else: + # Common CA cert paths + paths = [ + '/usr/lib/ssl/certs/ca-certificates.crt', + '/etc/ssl/certs/ca-certificates.crt', + '/etc/pki/tls/certs/ca-bundle.crt', + '/etc/ssl/ca-bundle.pem' + ] + for path in paths: + if os.path.exists(path): + ca_path = path + break + + if debug and ca_path: + console_write(u"Found system CA bundle at %s" % ca_path, True) + + return ca_path + + +def get_ca_cert_by_subject(settings, subject): + bundle_path = get_system_ca_bundle_path(settings) + + with open_compat(bundle_path, 'r') as f: + contents = read_compat(f) + + temp = [] + + in_block = False + for line in contents.splitlines(): + if line.find('BEGIN CERTIFICATE') != -1: + in_block = True + + if in_block: + temp.append(line) + + if line.find('END CERTIFICATE') != -1: + in_block = False + cert = u"\n".join(temp) + temp = [] + + if openssl_get_cert_subject(settings, cert) == subject: + return cert + + return False + + +def openssl_get_cert_issuer(settings, cert): + """ + Uses the openssl command line client to extract the issuer of an x509 + certificate. + + :param settings: + A dict to look in for `debug` and `openssl_binary` keys + + :param cert: + A string containing the PEM-encoded x509 certificate to extract the + issuer from + + :return: + The cert issuer + """ + + runner = OpensslCli(settings.get('openssl_binary'), settings.get('debug')) + binary = runner.retrieve_binary() + args = [binary, 'x509', '-noout', '-issuer'] + output = runner.execute(args, os.path.dirname(binary), cert) + return re.sub('^issuer=\s*', '', output) + + +def openssl_get_cert_name(settings, cert): + """ + Uses the openssl command line client to extract the name of an x509 + certificate. If the commonName is set, that is used, otherwise the first + organizationalUnitName is used. This mirrors what OS X uses for storing + trust preferences. + + :param settings: + A dict to look in for `debug` and `openssl_binary` keys + + :param cert: + A string containing the PEM-encoded x509 certificate to extract the + name from + + :return: + The cert subject name, which is the commonName (if available), or the + first organizationalUnitName + """ + + runner = OpensslCli(settings.get('openssl_binary'), settings.get('debug')) + + binary = runner.retrieve_binary() + + args = [binary, 'x509', '-noout', '-subject', '-nameopt', + 'sep_multiline,lname,utf8'] + result = runner.execute(args, os.path.dirname(binary), cert) + + # First look for the common name + cn = None + # If there is no common name for the cert, the trust prefs use the first + # orginizational unit name + first_ou = None + + for line in result.splitlines(): + match = re.match('^\s+commonName=(.*)$', line) + if match: + cn = match.group(1) + break + match = re.match('^\s+organizationalUnitName=(.*)$', line) + if first_ou is None and match: + first_ou = match.group(1) + continue + + # This is the name of the cert that would be used in the trust prefs + return cn or first_ou + + +def openssl_get_cert_subject(settings, cert): + """ + Uses the openssl command line client to extract the subject of an x509 + certificate. + + :param settings: + A dict to look in for `debug` and `openssl_binary` keys + + :param cert: + A string containing the PEM-encoded x509 certificate to extract the + subject from + + :return: + The cert subject + """ + + runner = OpensslCli(settings.get('openssl_binary'), settings.get('debug')) + binary = runner.retrieve_binary() + args = [binary, 'x509', '-noout', '-subject'] + output = runner.execute(args, os.path.dirname(binary), cert) + return re.sub('^subject=\s*', '', output) + + +def _osx_create_ca_bundle(settings, destination): + """ + Uses the OS X `security` command line tool to export the system's list of + CA certs from /System/Library/Keychains/SystemRootCertificates.keychain. + Checks the cert names against the trust preferences, ensuring that + distrusted certs are not exported. + + :param settings: + A dict to look in for `debug` and `openssl_binary` keys + + :param destination: + The full filesystem path to the destination .ca-bundle file + """ + + distrusted_certs = _osx_get_distrusted_certs(settings) + + # Export the root certs + args = ['/usr/bin/security', 'export', '-k', + '/System/Library/Keychains/SystemRootCertificates.keychain', '-t', + 'certs', '-p'] + result = Cli(None, settings.get('debug')).execute(args, '/usr/bin') + + certs = [] + temp = [] + + in_block = False + for line in result.splitlines(): + if line.find('BEGIN CERTIFICATE') != -1: + in_block = True + + if in_block: + temp.append(line) + + if line.find('END CERTIFICATE') != -1: + in_block = False + cert = u"\n".join(temp) + temp = [] + + if distrusted_certs: + # If it is a distrusted cert, we move on to the next + cert_name = openssl_get_cert_name(settings, cert) + if cert_name in distrusted_certs: + if settings.get('debug'): + console_write(u'Skipping root certficate %s because it is distrusted' % cert_name, True) + continue + + certs.append(cert) + + with open_compat(destination, 'w') as f: + f.write(u"\n".join(certs)) + + +def _osx_get_distrusted_certs(settings): + """ + Uses the OS X `security` binary to get a list of admin trust settings, + which is what is set when a user changes the trust setting on a root + certificate. By looking at the SSL policy, we can properly exclude + distrusted certs from out export. + + Tested on OS X 10.6 and 10.8 + + :param settings: + A dict to look in for `debug` key + + :return: + A list of CA cert names, where the name is the commonName (if + available), or the first organizationalUnitName + """ + + args = ['/usr/bin/security', 'dump-trust-settings', '-d'] + result = Cli(None, settings.get('debug')).execute(args, '/usr/bin') + + distrusted_certs = [] + cert_name = None + ssl_policy = False + for line in result.splitlines(): + if line == '': + continue + + # Reset for each cert + match = re.match('Cert\s+\d+:\s+(.*)$', line) + if match: + cert_name = match.group(1) + continue + + # Reset for each trust setting + if re.match('^\s+Trust\s+Setting\s+\d+:', line): + ssl_policy = False + continue + + # We are only interested in SSL policies + if re.match('^\s+Policy\s+OID\s+:\s+SSL', line): + ssl_policy = True + continue + + distrusted = re.match('^\s+Result\s+Type\s+:\s+kSecTrustSettingsResultDeny', line) + if ssl_policy and distrusted and cert_name not in distrusted_certs: + if settings.get('debug'): + console_write(u'Found SSL distrust setting for root certificate %s' % cert_name, True) + distrusted_certs.append(cert_name) + + return distrusted_certs + + +class OpensslCli(Cli): + + cli_name = 'openssl' + + def retrieve_binary(self): + """ + Returns the path to the openssl executable + + :return: The string path to the executable or False on error + """ + + name = 'openssl' + if os.name == 'nt': + name += '.exe' + + binary = self.find_binary(name) + if binary and os.path.isdir(binary): + full_path = os.path.join(binary, name) + if os.path.exists(full_path): + binary = full_path + + if not binary: + show_error((u'Unable to find %s. Please set the openssl_binary ' + + u'setting by accessing the Preferences > Package Settings > ' + + u'Package Control > Settings \u2013 User menu entry. The ' + + u'Settings \u2013 Default entry can be used for reference, ' + + u'but changes to that will be overwritten upon next upgrade.') % name) + return False + + return binary diff --git a/sublime/Packages/Package Control/package_control/cache.py b/sublime/Packages/Package Control/package_control/cache.py new file mode 100644 index 0000000..4b8021f --- /dev/null +++ b/sublime/Packages/Package Control/package_control/cache.py @@ -0,0 +1,168 @@ +import time + + +# A cache of channel and repository info to allow users to install multiple +# packages without having to wait for the metadata to be downloaded more +# than once. The keys are managed locally by the utilizing code. +_channel_repository_cache = {} + + +def clear_cache(): + global _channel_repository_cache + _channel_repository_cache = {} + + +def get_cache(key, default=None): + """ + Gets an in-memory cache value + + :param key: + The string key + + :param default: + The value to return if the key has not been set, or the ttl expired + + :return: + The cached value, or default + """ + + struct = _channel_repository_cache.get(key, {}) + expires = struct.get('expires') + if expires and expires > time.time(): + return struct.get('data') + return default + + +def merge_cache_over_settings(destination, setting, key_prefix): + """ + Take the cached value of `key` and put it into the key `setting` of + the destination.settings dict. Merge the values by overlaying the + cached setting over the existing info. + + :param destination: + An object that has a `.settings` attribute that is a dict + + :param setting: + The dict key to use when pushing the value into the settings dict + + :param key_prefix: + The string to prefix to `setting` to make the cache key + """ + + existing = destination.settings.get(setting, {}) + value = get_cache(key_prefix + '.' + setting, {}) + if value: + existing.update(value) + destination.settings[setting] = existing + + +def merge_cache_under_settings(destination, setting, key_prefix, list_=False): + """ + Take the cached value of `key` and put it into the key `setting` of + the destination.settings dict. Merge the values by overlaying the + existing setting value over the cached info. + + :param destination: + An object that has a `.settings` attribute that is a dict + + :param setting: + The dict key to use when pushing the value into the settings dict + + :param key_prefix: + The string to prefix to `setting` to make the cache key + + :param list_: + If a list should be used instead of a dict + """ + + default = {} if not list_ else [] + existing = destination.settings.get(setting) + value = get_cache(key_prefix + '.' + setting, default) + if value: + if existing: + if list_: + value.extend(existing) + else: + value.update(existing) + destination.settings[setting] = value + + +def set_cache(key, data, ttl=300): + """ + Sets an in-memory cache value + + :param key: + The string key + + :param data: + The data to cache + + :param ttl: + The integer number of second to cache the data for + """ + + _channel_repository_cache[key] = { + 'data': data, + 'expires': time.time() + ttl + } + + +def set_cache_over_settings(destination, setting, key_prefix, value, ttl): + """ + Take the value passed, and merge it over the current `setting`. Once + complete, take the value and set the cache `key` and destination.settings + `setting` to that value, using the `ttl` for set_cache(). + + :param destination: + An object that has a `.settings` attribute that is a dict + + :param setting: + The dict key to use when pushing the value into the settings dict + + :param key_prefix: + The string to prefix to `setting` to make the cache key + + :param value: + The value to set + + :param ttl: + The cache ttl to use + """ + + existing = destination.settings.get(setting, {}) + existing.update(value) + set_cache(key_prefix + '.' + setting, value, ttl) + destination.settings[setting] = value + + +def set_cache_under_settings(destination, setting, key_prefix, value, ttl, list_=False): + """ + Take the value passed, and merge the current `setting` over it. Once + complete, take the value and set the cache `key` and destination.settings + `setting` to that value, using the `ttl` for set_cache(). + + :param destination: + An object that has a `.settings` attribute that is a dict + + :param setting: + The dict key to use when pushing the value into the settings dict + + :param key_prefix: + The string to prefix to `setting` to make the cache key + + :param value: + The value to set + + :param ttl: + The cache ttl to use + """ + + default = {} if not list_ else [] + existing = destination.settings.get(setting, default) + if value: + if list_: + value.extend(existing) + else: + value.update(existing) + set_cache(key_prefix + '.' + setting, value, ttl) + destination.settings[setting] = value diff --git a/sublime/Packages/Package Control/package_control/clear_directory.py b/sublime/Packages/Package Control/package_control/clear_directory.py new file mode 100644 index 0000000..4ddfc07 --- /dev/null +++ b/sublime/Packages/Package Control/package_control/clear_directory.py @@ -0,0 +1,37 @@ +import os + + +def clear_directory(directory, ignore_paths=None): + """ + Tries to delete all files and folders from a directory + + :param directory: + The string directory path + + :param ignore_paths: + An array of paths to ignore while deleting files + + :return: + If all of the files and folders were successfully deleted + """ + + was_exception = False + for root, dirs, files in os.walk(directory, topdown=False): + paths = [os.path.join(root, f) for f in files] + paths.extend([os.path.join(root, d) for d in dirs]) + + for path in paths: + try: + # Don't delete the metadata file, that way we have it + # when the reinstall happens, and the appropriate + # usage info can be sent back to the server + if ignore_paths and path in ignore_paths: + continue + if os.path.isdir(path): + os.rmdir(path) + else: + os.remove(path) + except (OSError, IOError): + was_exception = True + + return not was_exception diff --git a/sublime/Packages/Package Control/package_control/clients/__init__.py b/sublime/Packages/Package Control/package_control/clients/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/sublime/Packages/Package Control/package_control/clients/bitbucket_client.py b/sublime/Packages/Package Control/package_control/clients/bitbucket_client.py new file mode 100644 index 0000000..d76b019 --- /dev/null +++ b/sublime/Packages/Package Control/package_control/clients/bitbucket_client.py @@ -0,0 +1,249 @@ +import re + +from ..versions import version_sort, version_filter +from .json_api_client import JSONApiClient + + +# A predefined list of readme filenames to look for +_readme_filenames = [ + 'readme', + 'readme.txt', + 'readme.md', + 'readme.mkd', + 'readme.mdown', + 'readme.markdown', + 'readme.textile', + 'readme.creole', + 'readme.rst' +] + + +class BitBucketClient(JSONApiClient): + + def download_info(self, url): + """ + Retrieve information about downloading a package + + :param url: + The URL of the repository, in one of the forms: + https://bitbucket.org/{user}/{repo} + https://bitbucket.org/{user}/{repo}/src/{branch} + https://bitbucket.org/{user}/{repo}/#tags + If the last option, grabs the info from the newest + tag that is a valid semver version. + + :raises: + DownloaderException: when there is an error downloading + ClientException: when there is an error parsing the response + + :return: + None if no match, False if no commit, or a dict with the following keys: + `version` - the version number of the download + `url` - the download URL of a zip file of the package + `date` - the ISO-8601 timestamp string when the version was published + """ + + commit_info = self._commit_info(url) + if not commit_info: + return commit_info + + commit_date = commit_info['timestamp'][0:19] + + return { + 'version': re.sub('[\-: ]', '.', commit_date), + 'url': 'https://bitbucket.org/%s/get/%s.zip' % (commit_info['user_repo'], commit_info['commit']), + 'date': commit_date + } + + def repo_info(self, url): + """ + Retrieve general information about a repository + + :param url: + The URL to the repository, in one of the forms: + https://bitbucket.org/{user}/{repo} + https://bitbucket.org/{user}/{repo}/src/{branch} + + :raises: + DownloaderException: when there is an error downloading + ClientException: when there is an error parsing the response + + :return: + None if no match, or a dict with the following keys: + `name` + `description` + `homepage` - URL of the homepage + `author` + `readme` - URL of the readme + `issues` - URL of bug tracker + `donate` - URL of a donate page + """ + + user_repo, branch = self._user_repo_branch(url) + if not user_repo: + return user_repo + + api_url = self._make_api_url(user_repo) + + info = self.fetch_json(api_url) + + issues_url = u'https://bitbucket.org/%s/issues' % user_repo + + return { + 'name': info['name'], + 'description': info['description'] or 'No description provided', + 'homepage': info['website'] or url, + 'author': info['owner'], + 'donate': u'https://www.gittip.com/on/bitbucket/%s/' % info['owner'], + 'readme': self._readme_url(user_repo, branch), + 'issues': issues_url if info['has_issues'] else None + } + + def _commit_info(self, url): + """ + Fetches info about the latest commit to a repository + + :param url: + The URL to the repository, in one of the forms: + https://bitbucket.org/{user}/{repo} + https://bitbucket.org/{user}/{repo}/src/{branch} + https://bitbucket.org/{user}/{repo}/#tags + If the last option, grabs the info from the newest + tag that is a valid semver version. + + :raises: + DownloaderException: when there is an error downloading + ClientException: when there is an error parsing the response + + :return: + None if no match, False if no commit, or a dict with the following keys: + `user_repo` - the user/repo name + `timestamp` - the ISO-8601 UTC timestamp string + `commit` - the branch or tag name + """ + + tags_match = re.match('https?://bitbucket.org/([^/]+/[^#/]+)/?#tags$', url) + + if tags_match: + user_repo = tags_match.group(1) + tags_url = self._make_api_url(user_repo, '/tags') + tags_list = self.fetch_json(tags_url) + tags = version_filter(tags_list.keys(), self.settings.get('install_prereleases')) + tags = version_sort(tags, reverse=True) + if not tags: + return False + commit = tags[0] + + else: + user_repo, commit = self._user_repo_branch(url) + if not user_repo: + return user_repo + + changeset_url = self._make_api_url(user_repo, '/changesets/%s' % commit) + commit_info = self.fetch_json(changeset_url) + + return { + 'user_repo': user_repo, + 'timestamp': commit_info['timestamp'], + 'commit': commit + } + + def _main_branch_name(self, user_repo): + """ + Fetch the name of the default branch + + :param user_repo: + The user/repo name to get the main branch for + + :raises: + DownloaderException: when there is an error downloading + ClientException: when there is an error parsing the response + + :return: + The name of the main branch - `master` or `default` + """ + + main_branch_url = self._make_api_url(user_repo, '/main-branch') + main_branch_info = self.fetch_json(main_branch_url, True) + return main_branch_info['name'] + + def _make_api_url(self, user_repo, suffix=''): + """ + Generate a URL for the BitBucket API + + :param user_repo: + The user/repo of the repository + + :param suffix: + The extra API path info to add to the URL + + :return: + The API URL + """ + + return 'https://api.bitbucket.org/1.0/repositories/%s%s' % (user_repo, suffix) + + def _readme_url(self, user_repo, branch, prefer_cached=False): + """ + Parse the root directory listing for the repo and return the URL + to any file that looks like a readme + + :param user_repo: + The user/repo string + + :param branch: + The branch to fetch the readme from + + :param prefer_cached: + If a cached directory listing should be used instead of a new HTTP request + + :raises: + DownloaderException: when there is an error downloading + ClientException: when there is an error parsing the response + + :return: + The URL to the readme file, or None + """ + + listing_url = self._make_api_url(user_repo, '/src/%s/' % branch) + root_dir_info = self.fetch_json(listing_url, prefer_cached) + + for entry in root_dir_info['files']: + if entry['path'].lower() in _readme_filenames: + return 'https://bitbucket.org/%s/raw/%s/%s' % (user_repo, + branch, entry['path']) + + return None + + def _user_repo_branch(self, url): + """ + Extract the username/repo and branch name from the URL + + :param url: + The URL to extract the info from, in one of the forms: + https://bitbucket.org/{user}/{repo} + https://bitbucket.org/{user}/{repo}/src/{branch} + + :raises: + DownloaderException: when there is an error downloading + ClientException: when there is an error parsing the response + + :return: + A tuple of (user/repo, branch name) or (None, None) if not matching + """ + + repo_match = re.match('https?://bitbucket.org/([^/]+/[^/]+)/?$', url) + branch_match = re.match('https?://bitbucket.org/([^/]+/[^/]+)/src/([^/]+)/?$', url) + + if repo_match: + user_repo = repo_match.group(1) + branch = self._main_branch_name(user_repo) + + elif branch_match: + user_repo = branch_match.group(1) + branch = branch_match.group(2) + + else: + return (None, None) + + return (user_repo, branch) diff --git a/sublime/Packages/Package Control/package_control/clients/client_exception.py b/sublime/Packages/Package Control/package_control/clients/client_exception.py new file mode 100644 index 0000000..fb8dd72 --- /dev/null +++ b/sublime/Packages/Package Control/package_control/clients/client_exception.py @@ -0,0 +1,5 @@ +class ClientException(Exception): + """If a client could not fetch information""" + + def __str__(self): + return self.args[0] diff --git a/sublime/Packages/Package Control/package_control/clients/github_client.py b/sublime/Packages/Package Control/package_control/clients/github_client.py new file mode 100644 index 0000000..9c1fd61 --- /dev/null +++ b/sublime/Packages/Package Control/package_control/clients/github_client.py @@ -0,0 +1,284 @@ +import re + +try: + # Python 3 + from urllib.parse import urlencode, quote +except (ImportError): + # Python 2 + from urllib import urlencode, quote + +from ..versions import version_sort, version_filter +from .json_api_client import JSONApiClient +from ..downloaders.downloader_exception import DownloaderException + + +class GitHubClient(JSONApiClient): + + def download_info(self, url): + """ + Retrieve information about downloading a package + + :param url: + The URL of the repository, in one of the forms: + https://github.com/{user}/{repo} + https://github.com/{user}/{repo}/tree/{branch} + https://github.com/{user}/{repo}/tags + If the last option, grabs the info from the newest + tag that is a valid semver version. + + :raises: + DownloaderException: when there is an error downloading + ClientException: when there is an error parsing the response + + :return: + None if no match, False if no commit, or a dict with the following keys: + `version` - the version number of the download + `url` - the download URL of a zip file of the package + `date` - the ISO-8601 timestamp string when the version was published + """ + + commit_info = self._commit_info(url) + if not commit_info: + return commit_info + + commit_date = commit_info['timestamp'][0:19].replace('T', ' ') + + return { + 'version': re.sub('[\-: ]', '.', commit_date), + # We specifically use codeload.github.com here because the download + # URLs all redirect there, and some of the downloaders don't follow + # HTTP redirect headers + 'url': 'https://codeload.github.com/%s/zip/%s' % (commit_info['user_repo'], quote(commit_info['commit'])), + 'date': commit_date + } + + def repo_info(self, url): + """ + Retrieve general information about a repository + + :param url: + The URL to the repository, in one of the forms: + https://github.com/{user}/{repo} + https://github.com/{user}/{repo}/tree/{branch} + + :raises: + DownloaderException: when there is an error downloading + ClientException: when there is an error parsing the response + + :return: + None if no match, or a dict with the following keys: + `name` + `description` + `homepage` - URL of the homepage + `author` + `readme` - URL of the readme + `issues` - URL of bug tracker + `donate` - URL of a donate page + """ + + user_repo, branch = self._user_repo_branch(url) + if not user_repo: + return user_repo + + api_url = self._make_api_url(user_repo) + + info = self.fetch_json(api_url) + + output = self._extract_repo_info(info) + output['readme'] = None + + readme_info = self._readme_info(user_repo, branch) + if not readme_info: + return output + + output['readme'] = 'https://raw.github.com/%s/%s/%s' % (user_repo, + branch, readme_info['path']) + return output + + def user_info(self, url): + """ + Retrieve general information about all repositories that are + part of a user/organization. + + :param url: + The URL to the user/organization, in the following form: + https://github.com/{user} + + :raises: + DownloaderException: when there is an error downloading + ClientException: when there is an error parsing the response + + :return: + None if no match, or am list of dicts with the following keys: + `name` + `description` + `homepage` - URL of the homepage + `author` + `readme` - URL of the readme + `issues` - URL of bug tracker + `donate` - URL of a donate page + """ + + user_match = re.match('https?://github.com/([^/]+)/?$', url) + if user_match == None: + return None + + user = user_match.group(1) + api_url = self._make_api_url(user) + + repos_info = self.fetch_json(api_url) + + output = [] + for info in repos_info: + output.append(self._extract_repo_info(info)) + return output + + def _commit_info(self, url): + """ + Fetches info about the latest commit to a repository + + :param url: + The URL to the repository, in one of the forms: + https://github.com/{user}/{repo} + https://github.com/{user}/{repo}/tree/{branch} + https://github.com/{user}/{repo}/tags + If the last option, grabs the info from the newest + tag that is a valid semver version. + + :raises: + DownloaderException: when there is an error downloading + ClientException: when there is an error parsing the response + + :return: + None if no match, False is no commit, or a dict with the following keys: + `user_repo` - the user/repo name + `timestamp` - the ISO-8601 UTC timestamp string + `commit` - the branch or tag name + """ + + tags_match = re.match('https?://github.com/([^/]+/[^/]+)/tags/?$', url) + + if tags_match: + user_repo = tags_match.group(1) + tags_url = self._make_api_url(user_repo, '/tags') + tags_list = self.fetch_json(tags_url) + tags = [tag['name'] for tag in tags_list] + tags = version_filter(tags, self.settings.get('install_prereleases')) + tags = version_sort(tags, reverse=True) + if not tags: + return False + commit = tags[0] + + else: + user_repo, commit = self._user_repo_branch(url) + if not user_repo: + return user_repo + + query_string = urlencode({'sha': commit, 'per_page': 1}) + commit_url = self._make_api_url(user_repo, '/commits?%s' % query_string) + commit_info = self.fetch_json(commit_url) + + return { + 'user_repo': user_repo, + 'timestamp': commit_info[0]['commit']['committer']['date'], + 'commit': commit + } + + def _extract_repo_info(self, result): + """ + Extracts information about a repository from the API result + + :param result: + A dict representing the data returned from the GitHub API + + :return: + A dict with the following keys: + `name` + `description` + `homepage` - URL of the homepage + `author` + `issues` - URL of bug tracker + `donate` - URL of a donate page + """ + + issues_url = u'https://github.com/%s/%s/issues' % (result['owner']['login'], result['name']) + + return { + 'name': result['name'], + 'description': result['description'] or 'No description provided', + 'homepage': result['homepage'] or result['html_url'], + 'author': result['owner']['login'], + 'issues': issues_url if result['has_issues'] else None, + 'donate': u'https://www.gittip.com/on/github/%s/' % result['owner']['login'] + } + + def _make_api_url(self, user_repo, suffix=''): + """ + Generate a URL for the BitBucket API + + :param user_repo: + The user/repo of the repository + + :param suffix: + The extra API path info to add to the URL + + :return: + The API URL + """ + + return 'https://api.github.com/repos/%s%s' % (user_repo, suffix) + + def _readme_info(self, user_repo, branch, prefer_cached=False): + """ + Fetches the raw GitHub API information about a readme + + :param user_repo: + The user/repo of the repository + + :param branch: + The branch to pull the readme from + + :param prefer_cached: + If a cached version of the info should be returned instead of making a new HTTP request + + :raises: + DownloaderException: when there is an error downloading + ClientException: when there is an error parsing the response + + :return: + A dict containing all of the info from the GitHub API, or None if no readme exists + """ + + query_string = urlencode({'ref': branch}) + readme_url = self._make_api_url(user_repo, '/readme?%s' % query_string) + try: + return self.fetch_json(readme_url, prefer_cached) + except (DownloaderException) as e: + if str(e).find('HTTP error 404') != -1: + return None + raise + + def _user_repo_branch(self, url): + """ + Extract the username/repo and branch name from the URL + + :param url: + The URL to extract the info from, in one of the forms: + https://github.com/{user}/{repo} + https://github.com/{user}/{repo}/tree/{branch} + + :return: + A tuple of (user/repo, branch name) or (None, None) if no match + """ + + branch = 'master' + branch_match = re.match('https?://github.com/[^/]+/[^/]+/tree/([^/]+)/?$', url) + if branch_match != None: + branch = branch_match.group(1) + + repo_match = re.match('https?://github.com/([^/]+/[^/]+)($|/.*$)', url) + if repo_match == None: + return (None, None) + + user_repo = repo_match.group(1) + return (user_repo, branch) diff --git a/sublime/Packages/Package Control/package_control/clients/json_api_client.py b/sublime/Packages/Package Control/package_control/clients/json_api_client.py new file mode 100644 index 0000000..a847302 --- /dev/null +++ b/sublime/Packages/Package Control/package_control/clients/json_api_client.py @@ -0,0 +1,64 @@ +import json + +try: + # Python 3 + from urllib.parse import urlencode, urlparse +except (ImportError): + # Python 2 + from urllib import urlencode + from urlparse import urlparse + +from .client_exception import ClientException +from ..download_manager import downloader + + +class JSONApiClient(): + def __init__(self, settings): + self.settings = settings + + def fetch(self, url, prefer_cached=False): + """ + Retrieves the contents of a URL + + :param url: + The URL to download the content from + + :param prefer_cached: + If a cached copy of the content is preferred + + :return: The bytes/string + """ + + # If there are extra params for the domain name, add them + extra_params = self.settings.get('query_string_params') + domain_name = urlparse(url).netloc + if extra_params and domain_name in extra_params: + params = urlencode(extra_params[domain_name]) + joiner = '?%s' if url.find('?') == -1 else '&%s' + url += joiner % params + + with downloader(url, self.settings) as manager: + content = manager.fetch(url, 'Error downloading repository.', + prefer_cached) + return content + + def fetch_json(self, url, prefer_cached=False): + """ + Retrieves and parses the JSON from a URL + + :param url: + The URL to download the JSON from + + :param prefer_cached: + If a cached copy of the JSON is preferred + + :return: A dict or list from the JSON + """ + + repository_json = self.fetch(url, prefer_cached) + + try: + return json.loads(repository_json.decode('utf-8')) + except (ValueError): + error_string = u'Error parsing JSON from URL %s.' % url + raise ClientException(error_string) diff --git a/sublime/Packages/Package Control/package_control/clients/readme_client.py b/sublime/Packages/Package Control/package_control/clients/readme_client.py new file mode 100644 index 0000000..47e2a7b --- /dev/null +++ b/sublime/Packages/Package Control/package_control/clients/readme_client.py @@ -0,0 +1,83 @@ +import re +import os +import base64 + +try: + # Python 3 + from urllib.parse import urlencode +except (ImportError): + # Python 2 + from urllib import urlencode + +from .json_api_client import JSONApiClient +from ..downloaders.downloader_exception import DownloaderException + + +# Used to map file extensions to formats +_readme_formats = { + '.md': 'markdown', + '.mkd': 'markdown', + '.mdown': 'markdown', + '.markdown': 'markdown', + '.textile': 'textile', + '.creole': 'creole', + '.rst': 'rst' +} + + +class ReadmeClient(JSONApiClient): + + def readme_info(self, url): + """ + Retrieve the readme and info about it + + :param url: + The URL of the readme file + + :raises: + DownloaderException: if there is an error downloading the readme + ClientException: if there is an error parsing the response + + :return: + A dict with the following keys: + `filename` + `format` - `markdown`, `textile`, `creole`, `rst` or `txt` + `contents` - contents of the readme as str/unicode + """ + + contents = None + + # Try to grab the contents of a GitHub-based readme by grabbing the cached + # content of the readme API call + github_match = re.match('https://raw.github.com/([^/]+/[^/]+)/([^/]+)/readme(\.(md|mkd|mdown|markdown|textile|creole|rst|txt))?$', url, re.I) + if github_match: + user_repo = github_match.group(1) + branch = github_match.group(2) + + query_string = urlencode({'ref': branch}) + readme_json_url = 'https://api.github.com/repos/%s/readme?%s' % (user_repo, query_string) + try: + info = self.fetch_json(readme_json_url, prefer_cached=True) + contents = base64.b64decode(info['content']) + except (ValueError) as e: + pass + + if not contents: + contents = self.fetch(url) + + basename, ext = os.path.splitext(url) + format = 'txt' + ext = ext.lower() + if ext in _readme_formats: + format = _readme_formats[ext] + + try: + contents = contents.decode('utf-8') + except (UnicodeDecodeError) as e: + contents = contents.decode('cp1252', errors='replace') + + return { + 'filename': os.path.basename(url), + 'format': format, + 'contents': contents + } diff --git a/sublime/Packages/Package Control/package_control/cmd.py b/sublime/Packages/Package Control/package_control/cmd.py new file mode 100644 index 0000000..0d5c999 --- /dev/null +++ b/sublime/Packages/Package Control/package_control/cmd.py @@ -0,0 +1,167 @@ +import os +import subprocess +import re + +if os.name == 'nt': + from ctypes import windll, create_unicode_buffer + +from .console_write import console_write +from .unicode import unicode_from_os +from .show_error import show_error + +try: + # Python 2 + str_cls = unicode +except (NameError): + # Python 3 + str_cls = str + + +def create_cmd(args, basename_binary=False): + """ + Takes an array of strings to be passed to subprocess.Popen and creates + a string that can be pasted into a terminal + + :param args: + The array containing the binary name/path and all arguments + + :param basename_binary: + If only the basename of the binary should be disabled instead of the full path + + :return: + The command string + """ + + if basename_binary: + args[0] = os.path.basename(args[0]) + + if os.name == 'nt': + return subprocess.list2cmdline(args) + else: + escaped_args = [] + for arg in args: + if re.search('^[a-zA-Z0-9/_^\\-\\.:=]+$', arg) == None: + arg = u"'" + arg.replace(u"'", u"'\\''") + u"'" + escaped_args.append(arg) + return u' '.join(escaped_args) + + +class Cli(object): + """ + Base class for running command line apps + + :param binary: + The full filesystem path to the executable for the version control + system. May be set to None to allow the code to try and find it. + """ + + cli_name = None + + def __init__(self, binary, debug): + self.binary = binary + self.debug = debug + + def execute(self, args, cwd, input=None): + """ + Creates a subprocess with the executable/args + + :param args: + A list of the executable path and all arguments to it + + :param cwd: + The directory in which to run the executable + + :param input: + The input text to send to the program + + :return: A string of the executable output + """ + + startupinfo = None + if os.name == 'nt': + startupinfo = subprocess.STARTUPINFO() + startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW + + # Make sure the cwd is ascii + try: + cwd.encode('ascii') + except UnicodeEncodeError: + buf = create_unicode_buffer(512) + if windll.kernel32.GetShortPathNameW(cwd, buf, len(buf)): + cwd = buf.value + + if self.debug: + console_write(u"Trying to execute command %s" % create_cmd(args), True) + + try: + proc = subprocess.Popen(args, stdin=subprocess.PIPE, + stdout=subprocess.PIPE, stderr=subprocess.STDOUT, + startupinfo=startupinfo, cwd=cwd) + + if input and isinstance(input, str_cls): + input = input.encode('utf-8') + output, _ = proc.communicate(input) + output = output.decode('utf-8') + output = output.replace('\r\n', '\n').rstrip(' \n\r') + + return output + + except (OSError) as e: + cmd = create_cmd(args) + error = unicode_from_os(e) + message = u"Error executing: %s\n%s\n\nTry checking your \"%s_binary\" setting?" % (cmd, error, self.cli_name) + show_error(message) + return False + + def find_binary(self, name): + """ + Locates the executable by looking in the PATH and well-known directories + + :param name: + The string filename of the executable + + :return: The filesystem path to the executable, or None if not found + """ + + if self.binary: + if self.debug: + error_string = u"Using \"%s_binary\" from settings \"%s\"" % ( + self.cli_name, self.binary) + console_write(error_string, True) + return self.binary + + # Try the path first + for dir_ in os.environ['PATH'].split(os.pathsep): + path = os.path.join(dir_, name) + if os.path.exists(path): + if self.debug: + console_write(u"Found %s at \"%s\"" % (self.cli_name, path), True) + return path + + # This is left in for backwards compatibility and for windows + # users who may have the binary, albeit in a common dir that may + # not be part of the PATH + if os.name == 'nt': + dirs = ['C:\\Program Files\\Git\\bin', + 'C:\\Program Files (x86)\\Git\\bin', + 'C:\\Program Files\\TortoiseGit\\bin', + 'C:\\Program Files\\Mercurial', + 'C:\\Program Files (x86)\\Mercurial', + 'C:\\Program Files (x86)\\TortoiseHg', + 'C:\\Program Files\\TortoiseHg', + 'C:\\cygwin\\bin'] + else: + # ST seems to launch with a minimal set of environmental variables + # on OS X, so we add some common paths for it + dirs = ['/usr/local/git/bin', '/usr/local/bin'] + + for dir_ in dirs: + path = os.path.join(dir_, name) + if os.path.exists(path): + if self.debug: + console_write(u"Found %s at \"%s\"" % (self.cli_name, path), True) + return path + + if self.debug: + console_write(u"Could not find %s on your machine" % self.cli_name, True) + return None diff --git a/sublime/Packages/Package Control/package_control/commands/__init__.py b/sublime/Packages/Package Control/package_control/commands/__init__.py new file mode 100644 index 0000000..dde03d4 --- /dev/null +++ b/sublime/Packages/Package Control/package_control/commands/__init__.py @@ -0,0 +1,39 @@ +import os + +from .add_channel_command import AddChannelCommand +from .add_repository_command import AddRepositoryCommand +from .create_binary_package_command import CreateBinaryPackageCommand +from .create_package_command import CreatePackageCommand +from .disable_package_command import DisablePackageCommand +from .discover_packages_command import DiscoverPackagesCommand +from .enable_package_command import EnablePackageCommand +from .grab_certs_command import GrabCertsCommand +from .install_package_command import InstallPackageCommand +from .list_packages_command import ListPackagesCommand +from .remove_package_command import RemovePackageCommand +from .upgrade_all_packages_command import UpgradeAllPackagesCommand +from .upgrade_package_command import UpgradePackageCommand +from .package_message_command import PackageMessageCommand + + +__all__ = [ + 'AddChannelCommand', + 'AddRepositoryCommand', + 'CreateBinaryPackageCommand', + 'CreatePackageCommand', + 'DisablePackageCommand', + 'DiscoverPackagesCommand', + 'EnablePackageCommand', + 'InstallPackageCommand', + 'ListPackagesCommand', + 'RemovePackageCommand', + 'UpgradeAllPackagesCommand', + 'UpgradePackageCommand', + 'PackageMessageCommand' +] + +# Windows uses the wininet downloader, so it doesn't use the CA cert bundle +# and thus does not need the ability to grab to CA certs. Additionally, +# there is no openssl.exe on Windows. +if os.name != 'nt': + __all__.append('GrabCertsCommand') diff --git a/sublime/Packages/Package Control/package_control/commands/add_channel_command.py b/sublime/Packages/Package Control/package_control/commands/add_channel_command.py new file mode 100644 index 0000000..5e1b8d1 --- /dev/null +++ b/sublime/Packages/Package Control/package_control/commands/add_channel_command.py @@ -0,0 +1,46 @@ +import re + +import sublime +import sublime_plugin + +from ..show_error import show_error + + +class AddChannelCommand(sublime_plugin.WindowCommand): + """ + A command to add a new channel (list of repositories) to the user's machine + """ + + def run(self): + self.window.show_input_panel('Channel JSON URL', '', + self.on_done, self.on_change, self.on_cancel) + + def on_done(self, input): + """ + Input panel handler - adds the provided URL as a channel + + :param input: + A string of the URL to the new channel + """ + + input = input.strip() + + if re.match('https?://', input, re.I) == None: + show_error(u"Unable to add the channel \"%s\" since it does not appear to be served via HTTP (http:// or https://)." % input) + return + + settings = sublime.load_settings('Package Control.sublime-settings') + channels = settings.get('channels', []) + if not channels: + channels = [] + channels.append(input) + settings.set('channels', channels) + sublime.save_settings('Package Control.sublime-settings') + sublime.status_message(('Channel %s successfully ' + + 'added') % input) + + def on_change(self, input): + pass + + def on_cancel(self): + pass diff --git a/sublime/Packages/Package Control/package_control/commands/add_repository_command.py b/sublime/Packages/Package Control/package_control/commands/add_repository_command.py new file mode 100644 index 0000000..3d04323 --- /dev/null +++ b/sublime/Packages/Package Control/package_control/commands/add_repository_command.py @@ -0,0 +1,46 @@ +import re + +import sublime +import sublime_plugin + +from ..show_error import show_error + + +class AddRepositoryCommand(sublime_plugin.WindowCommand): + """ + A command to add a new repository to the user's machine + """ + + def run(self): + self.window.show_input_panel('GitHub or BitBucket Web URL, or Custom' + + ' JSON Repository URL', '', self.on_done, + self.on_change, self.on_cancel) + + def on_done(self, input): + """ + Input panel handler - adds the provided URL as a repository + + :param input: + A string of the URL to the new repository + """ + + input = input.strip() + + if re.match('https?://', input, re.I) == None: + show_error(u"Unable to add the repository \"%s\" since it does not appear to be served via HTTP (http:// or https://)." % input) + return + + settings = sublime.load_settings('Package Control.sublime-settings') + repositories = settings.get('repositories', []) + if not repositories: + repositories = [] + repositories.append(input) + settings.set('repositories', repositories) + sublime.save_settings('Package Control.sublime-settings') + sublime.status_message('Repository %s successfully added' % input) + + def on_change(self, input): + pass + + def on_cancel(self): + pass diff --git a/sublime/Packages/Package Control/package_control/commands/create_binary_package_command.py b/sublime/Packages/Package Control/package_control/commands/create_binary_package_command.py new file mode 100644 index 0000000..491dd1c --- /dev/null +++ b/sublime/Packages/Package Control/package_control/commands/create_binary_package_command.py @@ -0,0 +1,35 @@ +import sublime_plugin + +from ..package_creator import PackageCreator + + +class CreateBinaryPackageCommand(sublime_plugin.WindowCommand, PackageCreator): + """ + Command to create a binary .sublime-package file. Binary packages in + general exclude the .py source files and instead include the .pyc files. + Actual included and excluded files are controlled by settings. + """ + + def run(self): + self.show_panel() + + def on_done(self, picked): + """ + Quick panel user selection handler - processes the user package + selection and create the package file + + :param picked: + An integer of the 0-based package name index from the presented + list. -1 means the user cancelled. + """ + + if picked == -1: + return + package_name = self.packages[picked] + package_destination = self.get_package_destination() + + if self.manager.create_package(package_name, package_destination, + binary_package=True): + self.window.run_command('open_dir', {"dir": + package_destination, "file": package_name + + '.sublime-package'}) diff --git a/sublime/Packages/Package Control/package_control/commands/create_package_command.py b/sublime/Packages/Package Control/package_control/commands/create_package_command.py new file mode 100644 index 0000000..8b0524a --- /dev/null +++ b/sublime/Packages/Package Control/package_control/commands/create_package_command.py @@ -0,0 +1,32 @@ +import sublime_plugin + +from ..package_creator import PackageCreator + + +class CreatePackageCommand(sublime_plugin.WindowCommand, PackageCreator): + """ + Command to create a regular .sublime-package file + """ + + def run(self): + self.show_panel() + + def on_done(self, picked): + """ + Quick panel user selection handler - processes the user package + selection and create the package file + + :param picked: + An integer of the 0-based package name index from the presented + list. -1 means the user cancelled. + """ + + if picked == -1: + return + package_name = self.packages[picked] + package_destination = self.get_package_destination() + + if self.manager.create_package(package_name, package_destination): + self.window.run_command('open_dir', {"dir": + package_destination, "file": package_name + + '.sublime-package'}) diff --git a/sublime/Packages/Package Control/package_control/commands/disable_package_command.py b/sublime/Packages/Package Control/package_control/commands/disable_package_command.py new file mode 100644 index 0000000..d5ebd97 --- /dev/null +++ b/sublime/Packages/Package Control/package_control/commands/disable_package_command.py @@ -0,0 +1,48 @@ +import sublime +import sublime_plugin + +from ..show_error import show_error +from ..package_manager import PackageManager +from ..preferences_filename import preferences_filename + + +class DisablePackageCommand(sublime_plugin.WindowCommand): + """ + A command that adds a package to Sublime Text's ignored packages list + """ + + def run(self): + manager = PackageManager() + packages = manager.list_all_packages() + self.settings = sublime.load_settings(preferences_filename()) + ignored = self.settings.get('ignored_packages') + if not ignored: + ignored = [] + self.package_list = list(set(packages) - set(ignored)) + self.package_list.sort() + if not self.package_list: + show_error('There are no enabled packages to disable.') + return + self.window.show_quick_panel(self.package_list, self.on_done) + + def on_done(self, picked): + """ + Quick panel user selection handler - disables the selected package + + :param picked: + An integer of the 0-based package name index from the presented + list. -1 means the user cancelled. + """ + + if picked == -1: + return + package = self.package_list[picked] + ignored = self.settings.get('ignored_packages') + if not ignored: + ignored = [] + ignored.append(package) + self.settings.set('ignored_packages', ignored) + sublime.save_settings(preferences_filename()) + sublime.status_message(('Package %s successfully added to list of ' + + 'disabled packages - restarting Sublime Text may be required') % + package) diff --git a/sublime/Packages/Package Control/package_control/commands/discover_packages_command.py b/sublime/Packages/Package Control/package_control/commands/discover_packages_command.py new file mode 100644 index 0000000..78d9812 --- /dev/null +++ b/sublime/Packages/Package Control/package_control/commands/discover_packages_command.py @@ -0,0 +1,11 @@ +import sublime_plugin + + +class DiscoverPackagesCommand(sublime_plugin.WindowCommand): + """ + A command that opens the community package list webpage + """ + + def run(self): + self.window.run_command('open_url', + {'url': 'http://wbond.net/sublime_packages/community'}) diff --git a/sublime/Packages/Package Control/package_control/commands/enable_package_command.py b/sublime/Packages/Package Control/package_control/commands/enable_package_command.py new file mode 100644 index 0000000..2e5e6d1 --- /dev/null +++ b/sublime/Packages/Package Control/package_control/commands/enable_package_command.py @@ -0,0 +1,40 @@ +import sublime +import sublime_plugin + +from ..show_error import show_error +from ..preferences_filename import preferences_filename + + +class EnablePackageCommand(sublime_plugin.WindowCommand): + """ + A command that removes a package from Sublime Text's ignored packages list + """ + + def run(self): + self.settings = sublime.load_settings(preferences_filename()) + self.disabled_packages = self.settings.get('ignored_packages') + self.disabled_packages.sort() + if not self.disabled_packages: + show_error('There are no disabled packages to enable.') + return + self.window.show_quick_panel(self.disabled_packages, self.on_done) + + def on_done(self, picked): + """ + Quick panel user selection handler - enables the selected package + + :param picked: + An integer of the 0-based package name index from the presented + list. -1 means the user cancelled. + """ + + if picked == -1: + return + package = self.disabled_packages[picked] + ignored = self.settings.get('ignored_packages') + self.settings.set('ignored_packages', + list(set(ignored) - set([package]))) + sublime.save_settings(preferences_filename()) + sublime.status_message(('Package %s successfully removed from list ' + + 'of disabled packages - restarting Sublime Text may be required') % + package) diff --git a/sublime/Packages/Package Control/package_control/commands/existing_packages_command.py b/sublime/Packages/Package Control/package_control/commands/existing_packages_command.py new file mode 100644 index 0000000..78615d6 --- /dev/null +++ b/sublime/Packages/Package Control/package_control/commands/existing_packages_command.py @@ -0,0 +1,69 @@ +import os +import re + +import sublime + +from ..package_manager import PackageManager + + +class ExistingPackagesCommand(): + """ + Allows listing installed packages and their current version + """ + + def __init__(self): + self.manager = PackageManager() + + def make_package_list(self, action=''): + """ + Returns a list of installed packages suitable for displaying in the + quick panel. + + :param action: + An action to display at the beginning of the third element of the + list returned for each package + + :return: + A list of lists, each containing three strings: + 0 - package name + 1 - package description + 2 - [action] installed version; package url + """ + + packages = self.manager.list_packages() + + if action: + action += ' ' + + package_list = [] + for package in sorted(packages, key=lambda s: s.lower()): + package_entry = [package] + metadata = self.manager.get_metadata(package) + package_dir = os.path.join(sublime.packages_path(), package) + + description = metadata.get('description') + if not description: + description = 'No description provided' + package_entry.append(description) + + version = metadata.get('version') + if not version and os.path.exists(os.path.join(package_dir, + '.git')): + installed_version = 'git repository' + elif not version and os.path.exists(os.path.join(package_dir, + '.hg')): + installed_version = 'hg repository' + else: + installed_version = 'v' + version if version else \ + 'unknown version' + + url = metadata.get('url') + if url: + url = '; ' + re.sub('^https?://', '', url) + else: + url = '' + + package_entry.append(action + installed_version + url) + package_list.append(package_entry) + + return package_list diff --git a/sublime/Packages/Package Control/package_control/commands/grab_certs_command.py b/sublime/Packages/Package Control/package_control/commands/grab_certs_command.py new file mode 100644 index 0000000..4eb77e0 --- /dev/null +++ b/sublime/Packages/Package Control/package_control/commands/grab_certs_command.py @@ -0,0 +1,109 @@ +import os +import re +import socket +import threading + +try: + # Python 3 + from urllib.parse import urlparse +except (ImportError): + # Python 2 + from urlparse import urlparse + +import sublime +import sublime_plugin + +from ..show_error import show_error +from ..open_compat import open_compat +from ..ca_certs import find_root_ca_cert +from ..thread_progress import ThreadProgress +from ..package_manager import PackageManager + + +class GrabCertsCommand(sublime_plugin.WindowCommand): + """ + A command that extracts the CA certs for a domain name, allowing a user to + fetch packages from sources other than those used by the default channel + """ + + def run(self): + panel = self.window.show_input_panel('Domain Name', 'example.com', self.on_done, + None, None) + panel.sel().add(sublime.Region(0, panel.size())) + + def on_done(self, domain): + """ + Input panel handler - grabs the CA certs for the domain name presented + + :param domain: + A string of the domain name + """ + + domain = domain.strip() + + # Make sure the user enters something + if domain == '': + show_error(u"Please enter a domain name, or press cancel") + self.run() + return + + # If the user inputs a URL, extract the domain name + if domain.find('/') != -1: + parts = urlparse(domain) + if parts.hostname: + domain = parts.hostname + + # Allow _ even though it technically isn't valid, this is really + # just to try and prevent people from typing in gibberish + if re.match('^(?:[a-zA-Z0-9]+(?:[\-_]*[a-zA-Z0-9]+)*\.)+[a-zA-Z]{2,6}$', domain, re.I) == None: + show_error(u"Unable to get the CA certs for \"%s\" since it does not appear to be a validly formed domain name" % domain) + return + + # Make sure it is a real domain + try: + socket.gethostbyname(domain) + except (socket.gaierror) as e: + error = unicode_from_os(e) + show_error(u"Error trying to lookup \"%s\":\n\n%s" % (domain, error)) + return + + manager = PackageManager() + + thread = GrabCertsThread(manager.settings, domain) + thread.start() + ThreadProgress(thread, 'Grabbing CA certs for %s' % domain, + 'CA certs for %s added to settings' % domain) + + +class GrabCertsThread(threading.Thread): + """ + A thread to run openssl so that the Sublime Text UI does not become frozen + """ + + def __init__(self, settings, domain): + self.settings = settings + self.domain = domain + threading.Thread.__init__(self) + + def run(self): + cert, cert_hash = find_root_ca_cert(self.settings, self.domain) + + certs_dir = os.path.join(sublime.packages_path(), 'User', + 'Package Control.ca-certs') + if not os.path.exists(certs_dir): + os.mkdir(certs_dir) + + cert_path = os.path.join(certs_dir, self.domain + '-ca.crt') + with open_compat(cert_path, 'w') as f: + f.write(cert) + + def save_certs(): + settings = sublime.load_settings('Package Control.sublime-settings') + certs = settings.get('certs', {}) + if not certs: + certs = {} + certs[self.domain] = [cert_hash, cert_path] + settings.set('certs', certs) + sublime.save_settings('Package Control.sublime-settings') + + sublime.set_timeout(save_certs, 10) diff --git a/sublime/Packages/Package Control/package_control/commands/install_package_command.py b/sublime/Packages/Package Control/package_control/commands/install_package_command.py new file mode 100644 index 0000000..bbe9031 --- /dev/null +++ b/sublime/Packages/Package Control/package_control/commands/install_package_command.py @@ -0,0 +1,50 @@ +import threading + +import sublime +import sublime_plugin + +from ..show_error import show_error +from ..package_installer import PackageInstaller +from ..thread_progress import ThreadProgress + + +class InstallPackageCommand(sublime_plugin.WindowCommand): + """ + A command that presents the list of available packages and allows the + user to pick one to install. + """ + + def run(self): + thread = InstallPackageThread(self.window) + thread.start() + ThreadProgress(thread, 'Loading repositories', '') + + +class InstallPackageThread(threading.Thread, PackageInstaller): + """ + A thread to run the action of retrieving available packages in. Uses the + default PackageInstaller.on_done quick panel handler. + """ + + def __init__(self, window): + """ + :param window: + An instance of :class:`sublime.Window` that represents the Sublime + Text window to show the available package list in. + """ + + self.window = window + self.completion_type = 'installed' + threading.Thread.__init__(self) + PackageInstaller.__init__(self) + + def run(self): + self.package_list = self.make_package_list(['upgrade', 'downgrade', + 'reinstall', 'pull', 'none']) + + def show_quick_panel(): + if not self.package_list: + show_error('There are no packages available for installation') + return + self.window.show_quick_panel(self.package_list, self.on_done) + sublime.set_timeout(show_quick_panel, 10) diff --git a/sublime/Packages/Package Control/package_control/commands/list_packages_command.py b/sublime/Packages/Package Control/package_control/commands/list_packages_command.py new file mode 100644 index 0000000..84c57e4 --- /dev/null +++ b/sublime/Packages/Package Control/package_control/commands/list_packages_command.py @@ -0,0 +1,63 @@ +import threading +import os + +import sublime +import sublime_plugin + +from ..show_error import show_error +from .existing_packages_command import ExistingPackagesCommand + + +class ListPackagesCommand(sublime_plugin.WindowCommand): + """ + A command that shows a list of all installed packages in the quick panel + """ + + def run(self): + ListPackagesThread(self.window).start() + + +class ListPackagesThread(threading.Thread, ExistingPackagesCommand): + """ + A thread to prevent the listing of existing packages from freezing the UI + """ + + def __init__(self, window): + """ + :param window: + An instance of :class:`sublime.Window` that represents the Sublime + Text window to show the list of installed packages in. + """ + + self.window = window + threading.Thread.__init__(self) + ExistingPackagesCommand.__init__(self) + + def run(self): + self.package_list = self.make_package_list() + + def show_quick_panel(): + if not self.package_list: + show_error('There are no packages to list') + return + self.window.show_quick_panel(self.package_list, self.on_done) + sublime.set_timeout(show_quick_panel, 10) + + def on_done(self, picked): + """ + Quick panel user selection handler - opens the homepage for any + selected package in the user's browser + + :param picked: + An integer of the 0-based package name index from the presented + list. -1 means the user cancelled. + """ + + if picked == -1: + return + package_name = self.package_list[picked][0] + + def open_dir(): + self.window.run_command('open_dir', + {"dir": os.path.join(sublime.packages_path(), package_name)}) + sublime.set_timeout(open_dir, 10) diff --git a/sublime/Packages/Package Control/package_control/commands/package_message_command.py b/sublime/Packages/Package Control/package_control/commands/package_message_command.py new file mode 100644 index 0000000..6e083df --- /dev/null +++ b/sublime/Packages/Package Control/package_control/commands/package_message_command.py @@ -0,0 +1,11 @@ +import sublime +import sublime_plugin + + +class PackageMessageCommand(sublime_plugin.TextCommand): + """ + A command to write a package message to the Package Control messaging buffer + """ + + def run(self, edit, string=''): + self.view.insert(edit, self.view.size(), string) diff --git a/sublime/Packages/Package Control/package_control/commands/remove_package_command.py b/sublime/Packages/Package Control/package_control/commands/remove_package_command.py new file mode 100644 index 0000000..df0350c --- /dev/null +++ b/sublime/Packages/Package Control/package_control/commands/remove_package_command.py @@ -0,0 +1,88 @@ +import threading + +import sublime +import sublime_plugin + +from ..show_error import show_error +from .existing_packages_command import ExistingPackagesCommand +from ..preferences_filename import preferences_filename +from ..thread_progress import ThreadProgress + + +class RemovePackageCommand(sublime_plugin.WindowCommand, + ExistingPackagesCommand): + """ + A command that presents a list of installed packages, allowing the user to + select one to remove + """ + + def __init__(self, window): + """ + :param window: + An instance of :class:`sublime.Window` that represents the Sublime + Text window to show the list of installed packages in. + """ + + self.window = window + ExistingPackagesCommand.__init__(self) + + def run(self): + self.package_list = self.make_package_list('remove') + if not self.package_list: + show_error('There are no packages that can be removed.') + return + self.window.show_quick_panel(self.package_list, self.on_done) + + def on_done(self, picked): + """ + Quick panel user selection handler - deletes the selected package + + :param picked: + An integer of the 0-based package name index from the presented + list. -1 means the user cancelled. + """ + + if picked == -1: + return + package = self.package_list[picked][0] + + settings = sublime.load_settings(preferences_filename()) + ignored = settings.get('ignored_packages') + if not ignored: + ignored = [] + + # Don't disable Package Control so it does not get stuck disabled + if package != 'Package Control': + if not package in ignored: + ignored.append(package) + settings.set('ignored_packages', ignored) + sublime.save_settings(preferences_filename()) + ignored.remove(package) + + thread = RemovePackageThread(self.manager, package, + ignored) + thread.start() + ThreadProgress(thread, 'Removing package %s' % package, + 'Package %s successfully removed' % package) + + +class RemovePackageThread(threading.Thread): + """ + A thread to run the remove package operation in so that the Sublime Text + UI does not become frozen + """ + + def __init__(self, manager, package, ignored): + self.manager = manager + self.package = package + self.ignored = ignored + threading.Thread.__init__(self) + + def run(self): + self.result = self.manager.remove_package(self.package) + + def unignore_package(): + settings = sublime.load_settings(preferences_filename()) + settings.set('ignored_packages', self.ignored) + sublime.save_settings(preferences_filename()) + sublime.set_timeout(unignore_package, 10) diff --git a/sublime/Packages/Package Control/package_control/commands/upgrade_all_packages_command.py b/sublime/Packages/Package Control/package_control/commands/upgrade_all_packages_command.py new file mode 100644 index 0000000..a4a730d --- /dev/null +++ b/sublime/Packages/Package Control/package_control/commands/upgrade_all_packages_command.py @@ -0,0 +1,77 @@ +import time +import threading + +import sublime +import sublime_plugin + +from ..thread_progress import ThreadProgress +from ..package_installer import PackageInstaller, PackageInstallerThread +from ..package_renamer import PackageRenamer + + +class UpgradeAllPackagesCommand(sublime_plugin.WindowCommand): + """ + A command to automatically upgrade all installed packages that are + upgradable. + """ + + def run(self): + package_renamer = PackageRenamer() + package_renamer.load_settings() + + thread = UpgradeAllPackagesThread(self.window, package_renamer) + thread.start() + ThreadProgress(thread, 'Loading repositories', '') + + +class UpgradeAllPackagesThread(threading.Thread, PackageInstaller): + """ + A thread to run the action of retrieving upgradable packages in. + """ + + def __init__(self, window, package_renamer): + self.window = window + self.package_renamer = package_renamer + self.completion_type = 'upgraded' + threading.Thread.__init__(self) + PackageInstaller.__init__(self) + + def run(self): + self.package_renamer.rename_packages(self) + package_list = self.make_package_list(['install', 'reinstall', 'none']) + + disabled_packages = [] + + def do_upgrades(): + # Pause so packages can be disabled + time.sleep(0.5) + + # We use a function to generate the on-complete lambda because if + # we don't, the lambda will bind to info at the current scope, and + # thus use the last value of info from the loop + def make_on_complete(name): + return lambda: self.reenable_package(name) + + for info in package_list: + if info[0] in disabled_packages: + on_complete = make_on_complete(info[0]) + else: + on_complete = None + thread = PackageInstallerThread(self.manager, info[0], + on_complete) + thread.start() + ThreadProgress(thread, 'Upgrading package %s' % info[0], + 'Package %s successfully %s' % (info[0], + self.completion_type)) + + # Disabling a package means changing settings, which can only be done + # in the main thread. We then create a new background thread so that + # the upgrade process does not block the UI. + def disable_packages(): + package_names = [] + for info in package_list: + package_names.append(info[0]) + disabled_packages.extend(self.disable_packages(package_names)) + threading.Thread(target=do_upgrades).start() + + sublime.set_timeout(disable_packages, 1) diff --git a/sublime/Packages/Package Control/package_control/commands/upgrade_package_command.py b/sublime/Packages/Package Control/package_control/commands/upgrade_package_command.py new file mode 100644 index 0000000..6c478e6 --- /dev/null +++ b/sublime/Packages/Package Control/package_control/commands/upgrade_package_command.py @@ -0,0 +1,81 @@ +import threading + +import sublime +import sublime_plugin + +from ..show_error import show_error +from ..thread_progress import ThreadProgress +from ..package_installer import PackageInstaller, PackageInstallerThread +from ..package_renamer import PackageRenamer + + +class UpgradePackageCommand(sublime_plugin.WindowCommand): + """ + A command that presents the list of installed packages that can be upgraded + """ + + def run(self): + package_renamer = PackageRenamer() + package_renamer.load_settings() + + thread = UpgradePackageThread(self.window, package_renamer) + thread.start() + ThreadProgress(thread, 'Loading repositories', '') + + +class UpgradePackageThread(threading.Thread, PackageInstaller): + """ + A thread to run the action of retrieving upgradable packages in. + """ + + def __init__(self, window, package_renamer): + """ + :param window: + An instance of :class:`sublime.Window` that represents the Sublime + Text window to show the list of upgradable packages in. + + :param package_renamer: + An instance of :class:`PackageRenamer` + """ + self.window = window + self.package_renamer = package_renamer + self.completion_type = 'upgraded' + threading.Thread.__init__(self) + PackageInstaller.__init__(self) + + def run(self): + self.package_renamer.rename_packages(self) + + self.package_list = self.make_package_list(['install', 'reinstall', + 'none']) + + def show_quick_panel(): + if not self.package_list: + show_error('There are no packages ready for upgrade') + return + self.window.show_quick_panel(self.package_list, self.on_done) + sublime.set_timeout(show_quick_panel, 10) + + def on_done(self, picked): + """ + Quick panel user selection handler - disables a package, upgrades it, + then re-enables the package + + :param picked: + An integer of the 0-based package name index from the presented + list. -1 means the user cancelled. + """ + + if picked == -1: + return + name = self.package_list[picked][0] + + if name in self.disable_packages(name): + on_complete = lambda: self.reenable_package(name) + else: + on_complete = None + + thread = PackageInstallerThread(self.manager, name, on_complete) + thread.start() + ThreadProgress(thread, 'Upgrading package %s' % name, + 'Package %s successfully %s' % (name, self.completion_type)) diff --git a/sublime/Packages/Package Control/package_control/console_write.py b/sublime/Packages/Package Control/package_control/console_write.py new file mode 100644 index 0000000..5fb0796 --- /dev/null +++ b/sublime/Packages/Package Control/package_control/console_write.py @@ -0,0 +1,20 @@ +import sys + + +def console_write(string, prefix=False): + """ + Writes a value to the Sublime Text console, encoding unicode to utf-8 first + + :param string: + The value to write + + :param prefix: + If the string "Package Control: " should be prefixed to the string + """ + + if sys.version_info < (3,): + if isinstance(string, unicode): + string = string.encode('UTF-8') + if prefix: + sys.stdout.write('Package Control: ') + print(string) diff --git a/sublime/Packages/Package Control/package_control/download_manager.py b/sublime/Packages/Package Control/package_control/download_manager.py new file mode 100644 index 0000000..a4d028d --- /dev/null +++ b/sublime/Packages/Package Control/package_control/download_manager.py @@ -0,0 +1,231 @@ +import sys +import re +import socket +from threading import Lock, Timer +from contextlib import contextmanager + +try: + # Python 3 + from urllib.parse import urlparse +except (ImportError): + # Python 2 + from urlparse import urlparse + +from . import __version__ + +from .show_error import show_error +from .console_write import console_write +from .cache import set_cache, get_cache +from .unicode import unicode_from_os + +from .downloaders import DOWNLOADERS +from .downloaders.binary_not_found_error import BinaryNotFoundError +from .downloaders.rate_limit_exception import RateLimitException +from .downloaders.no_ca_cert_exception import NoCaCertException +from .downloaders.downloader_exception import DownloaderException +from .http_cache import HttpCache + + +# A dict of domains - each points to a list of downloaders +_managers = {} + +# How many managers are currently checked out +_in_use = 0 + +# Make sure connection management doesn't run into threading issues +_lock = Lock() + +# A timer used to disconnect all managers after a period of no usage +_timer = None + + +@contextmanager +def downloader(url, settings): + try: + manager = _grab(url, settings) + yield manager + + finally: + _release(url, manager) + + +def _grab(url, settings): + global _managers, _lock, _in_use, _timer + + _lock.acquire() + try: + if _timer: + _timer.cancel() + _timer = None + + hostname = urlparse(url).hostname.lower() + if hostname not in _managers: + _managers[hostname] = [] + + if not _managers[hostname]: + _managers[hostname].append(DownloadManager(settings)) + + _in_use += 1 + + return _managers[hostname].pop() + + finally: + _lock.release() + + +def _release(url, manager): + global _managers, _lock, _in_use, _timer + + _lock.acquire() + try: + hostname = urlparse(url).hostname.lower() + _managers[hostname].insert(0, manager) + + _in_use -= 1 + + if _timer: + _timer.cancel() + _timer = None + + if _in_use == 0: + _timer = Timer(5.0, close_all_connections) + _timer.start() + + finally: + _lock.release() + + +def close_all_connections(): + global _managers, _lock, _in_use, _timer + + _lock.acquire() + try: + if _timer: + _timer.cancel() + _timer = None + + for domain, managers in _managers.items(): + for manager in managers: + manager.close() + _managers = {} + + finally: + _lock.release() + + +class DownloadManager(object): + def __init__(self, settings): + # Cache the downloader for re-use + self.downloader = None + + user_agent = settings.get('user_agent') + if user_agent and user_agent.find('%s') != -1: + settings['user_agent'] = user_agent % __version__ + + self.settings = settings + if settings.get('http_cache'): + cache_length = settings.get('http_cache_length', 604800) + self.settings['cache'] = HttpCache(cache_length) + + def close(self): + if self.downloader: + self.downloader.close() + self.downloader = None + + def fetch(self, url, error_message, prefer_cached=False): + """ + Downloads a URL and returns the contents + + :param url: + The string URL to download + + :param error_message: + The error message to include if the download fails + + :param prefer_cached: + If cached version of the URL content is preferred over a new request + + :raises: + DownloaderException: if there was an error downloading the URL + + :return: + The string contents of the URL + """ + + is_ssl = re.search('^https://', url) != None + + # Make sure we have a downloader, and it supports SSL if we need it + if not self.downloader or (is_ssl and not self.downloader.supports_ssl()): + for downloader_class in DOWNLOADERS: + try: + downloader = downloader_class(self.settings) + if is_ssl and not downloader.supports_ssl(): + continue + self.downloader = downloader + break + except (BinaryNotFoundError): + pass + + if not self.downloader: + error_string = u'Unable to download %s due to no ssl module available and no capable program found. Please install curl or wget.' % url + show_error(error_string) + raise DownloaderException(error_string) + + url = url.replace(' ', '%20') + hostname = urlparse(url).hostname + if hostname: + hostname = hostname.lower() + timeout = self.settings.get('timeout', 3) + + rate_limited_domains = get_cache('rate_limited_domains', []) + no_ca_cert_domains = get_cache('no_ca_cert_domains', []) + + if self.settings.get('debug'): + try: + ip = socket.gethostbyname(hostname) + except (socket.gaierror) as e: + ip = unicode_from_os(e) + except (TypeError) as e: + ip = None + + console_write(u"Download Debug", True) + console_write(u" URL: %s" % url) + console_write(u" Resolved IP: %s" % ip) + console_write(u" Timeout: %s" % str(timeout)) + + if hostname in rate_limited_domains: + error_string = u"Skipping due to hitting rate limit for %s" % hostname + if self.settings.get('debug'): + console_write(u" %s" % error_string) + raise DownloaderException(error_string) + + if hostname in no_ca_cert_domains: + error_string = u" Skipping since there are no CA certs for %s" % hostname + if self.settings.get('debug'): + console_write(u" %s" % error_string) + raise DownloaderException(error_string) + + try: + return self.downloader.download(url, error_message, timeout, 3, prefer_cached) + + except (RateLimitException) as e: + + rate_limited_domains.append(hostname) + set_cache('rate_limited_domains', rate_limited_domains, self.settings.get('cache_length')) + + error_string = (u'Hit rate limit of %s for %s, skipping all futher ' + + u'download requests for this domain') % (e.limit, e.domain) + console_write(error_string, True) + raise + + except (NoCaCertException) as e: + + no_ca_cert_domains.append(hostname) + set_cache('no_ca_cert_domains', no_ca_cert_domains, self.settings.get('cache_length')) + + error_string = (u'No CA certs available for %s, skipping all futher ' + + u'download requests for this domain. If you are on a trusted ' + + u'network, you can add the CA certs by running the "Grab ' + + u'CA Certs" command from the command palette.') % e.domain + console_write(error_string, True) + raise diff --git a/sublime/Packages/Package Control/package_control/downloaders/__init__.py b/sublime/Packages/Package Control/package_control/downloaders/__init__.py new file mode 100644 index 0000000..fb68aef --- /dev/null +++ b/sublime/Packages/Package Control/package_control/downloaders/__init__.py @@ -0,0 +1,11 @@ +import os + +if os.name == 'nt': + from .wininet_downloader import WinINetDownloader + DOWNLOADERS = [WinINetDownloader] + +else: + from .urllib_downloader import UrlLibDownloader + from .curl_downloader import CurlDownloader + from .wget_downloader import WgetDownloader + DOWNLOADERS = [UrlLibDownloader, CurlDownloader, WgetDownloader] diff --git a/sublime/Packages/Package Control/package_control/downloaders/background_downloader.py b/sublime/Packages/Package Control/package_control/downloaders/background_downloader.py new file mode 100644 index 0000000..250d2de --- /dev/null +++ b/sublime/Packages/Package Control/package_control/downloaders/background_downloader.py @@ -0,0 +1,62 @@ +import threading + + +class BackgroundDownloader(threading.Thread): + """ + Downloads information from one or more URLs in the background. + Normal usage is to use one BackgroundDownloader per domain name. + + :param settings: + A dict containing at least the following fields: + `cache_length`, + `debug`, + `timeout`, + `user_agent`, + `http_proxy`, + `https_proxy`, + `proxy_username`, + `proxy_password` + + :param providers: + An array of providers that can download the URLs + """ + + def __init__(self, settings, providers): + self.settings = settings + self.urls = [] + self.providers = providers + self.used_providers = {} + threading.Thread.__init__(self) + + def add_url(self, url): + """ + Adds a URL to the list to download + + :param url: + The URL to download info about + """ + + self.urls.append(url) + + def get_provider(self, url): + """ + Returns the provider for the URL specified + + :param url: + The URL to return the provider for + + :return: + The provider object for the URL + """ + + return self.used_providers[url] + + def run(self): + for url in self.urls: + for provider_class in self.providers: + if provider_class.match_url(url): + provider = provider_class(url, self.settings) + break + + provider.prefetch() + self.used_providers[url] = provider diff --git a/sublime/Packages/Package Control/package_control/downloaders/binary_not_found_error.py b/sublime/Packages/Package Control/package_control/downloaders/binary_not_found_error.py new file mode 100644 index 0000000..a7955b9 --- /dev/null +++ b/sublime/Packages/Package Control/package_control/downloaders/binary_not_found_error.py @@ -0,0 +1,4 @@ +class BinaryNotFoundError(Exception): + """If a necessary executable is not found in the PATH on the system""" + + pass diff --git a/sublime/Packages/Package Control/package_control/downloaders/caching_downloader.py b/sublime/Packages/Package Control/package_control/downloaders/caching_downloader.py new file mode 100644 index 0000000..ab3d71f --- /dev/null +++ b/sublime/Packages/Package Control/package_control/downloaders/caching_downloader.py @@ -0,0 +1,185 @@ +import sys +import re +import json +import hashlib + +from ..console_write import console_write + + +class CachingDownloader(object): + """ + A base downloader that will use a caching backend to cache HTTP requests + and make conditional requests. + """ + + def add_conditional_headers(self, url, headers): + """ + Add `If-Modified-Since` and `If-None-Match` headers to a request if a + cached copy exists + + :param headers: + A dict with the request headers + + :return: + The request headers dict, possibly with new headers added + """ + + if not self.settings.get('cache'): + return headers + + info_key = self.generate_key(url, '.info') + info_json = self.settings['cache'].get(info_key) + + if not info_json: + return headers + + # Make sure we have the cached content to use if we get a 304 + key = self.generate_key(url) + if not self.settings['cache'].has(key): + return headers + + try: + info = json.loads(info_json.decode('utf-8')) + except ValueError: + return headers + + etag = info.get('etag') + if etag: + headers['If-None-Match'] = etag + + last_modified = info.get('last-modified') + if last_modified: + headers['If-Modified-Since'] = last_modified + + return headers + + def cache_result(self, method, url, status, headers, content): + """ + Processes a request result, either caching the result, or returning + the cached version of the url. + + :param method: + The HTTP method used for the request + + :param url: + The url of the request + + :param status: + The numeric response status of the request + + :param headers: + A dict of reponse headers, with keys being lowercase + + :param content: + The response content + + :return: + The response content + """ + + debug = self.settings.get('debug', False) + + if not self.settings.get('cache'): + if debug: + console_write(u"Skipping cache since there is no cache object", True) + return content + + if method.lower() != 'get': + if debug: + console_write(u"Skipping cache since the HTTP method != GET", True) + return content + + status = int(status) + + # Don't do anything unless it was successful or not modified + if status not in [200, 304]: + if debug: + console_write(u"Skipping cache since the HTTP status code not one of: 200, 304", True) + return content + + key = self.generate_key(url) + + if status == 304: + cached_content = self.settings['cache'].get(key) + if cached_content: + if debug: + console_write(u"Using cached content for %s" % url, True) + return cached_content + + # If we got a 304, but did not have the cached content + # stop here so we don't cache an empty response + return content + + # If we got here, the status is 200 + + # Respect some basic cache control headers + cache_control = headers.get('cache-control', '') + if cache_control: + fields = re.split(',\s*', cache_control) + for field in fields: + if field == 'no-store': + return content + + # Don't ever cache zip/binary files for the sake of hard drive space + if headers.get('content-type') in ['application/zip', 'application/octet-stream']: + if debug: + console_write(u"Skipping cache since the response is a zip file", True) + return content + + etag = headers.get('etag') + last_modified = headers.get('last-modified') + + if not etag and not last_modified: + return content + + struct = {'etag': etag, 'last-modified': last_modified} + struct_json = json.dumps(struct, indent=4) + + info_key = self.generate_key(url, '.info') + if debug: + console_write(u"Caching %s in %s" % (url, key), True) + + self.settings['cache'].set(info_key, struct_json.encode('utf-8')) + self.settings['cache'].set(key, content) + + return content + + def generate_key(self, url, suffix=''): + """ + Generates a key to store the cache under + + :param url: + The URL being cached + + :param suffix: + A string to append to the key + + :return: + A string key for the URL + """ + + if sys.version_info >= (3,) or isinstance(url, unicode): + url = url.encode('utf-8') + + key = hashlib.md5(url).hexdigest() + return key + suffix + + def retrieve_cached(self, url): + """ + Tries to return the cached content for a URL + + :param url: + The URL to get the cached content for + + :return: + The cached content + """ + + key = self.generate_key(url) + if not self.settings['cache'].has(key): + return False + + if self.settings.get('debug'): + console_write(u"Using cached content for %s" % url, True) + + return self.settings['cache'].get(key) diff --git a/sublime/Packages/Package Control/package_control/downloaders/cert_provider.py b/sublime/Packages/Package Control/package_control/downloaders/cert_provider.py new file mode 100644 index 0000000..f8c8c3b --- /dev/null +++ b/sublime/Packages/Package Control/package_control/downloaders/cert_provider.py @@ -0,0 +1,203 @@ +import os +import re +import json + +import sublime + +from ..console_write import console_write +from ..open_compat import open_compat, read_compat +from ..package_io import read_package_file +from ..cache import get_cache +from ..ca_certs import get_system_ca_bundle_path +from .no_ca_cert_exception import NoCaCertException +from .downloader_exception import DownloaderException + + +class CertProvider(object): + """ + A base downloader that provides access to a ca-bundle for validating + SSL certificates. + """ + + def check_certs(self, domain, timeout): + """ + Ensures that the SSL CA cert for a domain is present on the machine + + :param domain: + The domain to ensure there is a CA cert for + + :param timeout: + The int timeout for downloading the CA cert from the channel + + :raises: + NoCaCertException: when a suitable CA cert could not be found + + :return: + The CA cert bundle path + """ + + # Try to use the system CA bundle + ca_bundle_path = get_system_ca_bundle_path(self.settings) + if ca_bundle_path: + return ca_bundle_path + + # If the system bundle did not work, fall back to our CA distribution + # system. Hopefully this will be going away soon. + if self.settings.get('debug'): + console_write(u'Unable to find system CA cert bundle, falling back to certs provided by Package Control') + + cert_match = False + + certs_list = get_cache('*.certs', self.settings.get('certs', {})) + + ca_bundle_path = os.path.join(sublime.packages_path(), 'User', 'Package Control.ca-bundle') + if not os.path.exists(ca_bundle_path) or os.stat(ca_bundle_path).st_size == 0: + bundle_contents = read_package_file('Package Control', 'Package Control.ca-bundle', True) + if not bundle_contents: + raise NoCaCertException(u'Unable to copy distributed Package Control.ca-bundle', domain) + with open_compat(ca_bundle_path, 'wb') as f: + f.write(bundle_contents) + + cert_info = certs_list.get(domain) + if cert_info: + cert_match = self.locate_cert(cert_info[0], + cert_info[1], domain, timeout) + + wildcard_info = certs_list.get('*') + if wildcard_info: + cert_match = self.locate_cert(wildcard_info[0], + wildcard_info[1], domain, timeout) or cert_match + + if not cert_match: + raise NoCaCertException(u'No CA certs available for %s' % domain, domain) + + return ca_bundle_path + + def locate_cert(self, cert_id, location, domain, timeout): + """ + Makes sure the SSL cert specified has been added to the CA cert + bundle that is present on the machine + + :param cert_id: + The identifier for CA cert(s). For those provided by the channel + system, this will be an md5 of the contents of the cert(s). For + user-provided certs, this is something they provide. + + :param location: + An http(s) URL, or absolute filesystem path to the CA cert(s) + + :param domain: + The domain to ensure there is a CA cert for + + :param timeout: + The int timeout for downloading the CA cert from the channel + + :return: + If the cert specified (by cert_id) is present on the machine and + part of the Package Control.ca-bundle file in the User package folder + """ + + ca_list_path = os.path.join(sublime.packages_path(), 'User', 'Package Control.ca-list') + if not os.path.exists(ca_list_path) or os.stat(ca_list_path).st_size == 0: + list_contents = read_package_file('Package Control', 'Package Control.ca-list') + if not list_contents: + raise NoCaCertException(u'Unable to copy distributed Package Control.ca-list', domain) + with open_compat(ca_list_path, 'w') as f: + f.write(list_contents) + + ca_certs = [] + with open_compat(ca_list_path, 'r') as f: + ca_certs = json.loads(read_compat(f)) + + if not cert_id in ca_certs: + if str(location) != '': + if re.match('^https?://', location): + contents = self.download_cert(cert_id, location, domain, + timeout) + else: + contents = self.load_cert(cert_id, location, domain) + if contents: + self.save_cert(cert_id, contents) + return True + return False + return True + + def download_cert(self, cert_id, url, domain, timeout): + """ + Downloads CA cert(s) from a URL + + :param cert_id: + The identifier for CA cert(s). For those provided by the channel + system, this will be an md5 of the contents of the cert(s). For + user-provided certs, this is something they provide. + + :param url: + An http(s) URL to the CA cert(s) + + :param domain: + The domain to ensure there is a CA cert for + + :param timeout: + The int timeout for downloading the CA cert from the channel + + :return: + The contents of the CA cert(s) + """ + + cert_downloader = self.__class__(self.settings) + if self.settings.get('debug'): + console_write(u"Downloading CA cert for %s from \"%s\"" % (domain, url), True) + return cert_downloader.download(url, + 'Error downloading CA certs for %s.' % domain, timeout, 1) + + def load_cert(self, cert_id, path, domain): + """ + Copies CA cert(s) from a file path + + :param cert_id: + The identifier for CA cert(s). For those provided by the channel + system, this will be an md5 of the contents of the cert(s). For + user-provided certs, this is something they provide. + + :param path: + The absolute filesystem path to a file containing the CA cert(s) + + :param domain: + The domain name the cert is for + + :return: + The contents of the CA cert(s) + """ + + if os.path.exists(path): + if self.settings.get('debug'): + console_write(u"Copying CA cert for %s from \"%s\"" % (domain, path), True) + with open_compat(path, 'rb') as f: + return f.read() + else: + raise NoCaCertException(u"Unable to find CA cert for %s at \"%s\"" % (domain, path), domain) + + def save_cert(self, cert_id, contents): + """ + Saves CA cert(s) to the Package Control.ca-bundle + + :param cert_id: + The identifier for CA cert(s). For those provided by the channel + system, this will be an md5 of the contents of the cert(s). For + user-provided certs, this is something they provide. + + :param contents: + The contents of the CA cert(s) + """ + + + ca_bundle_path = os.path.join(sublime.packages_path(), 'User', 'Package Control.ca-bundle') + with open_compat(ca_bundle_path, 'ab') as f: + f.write(b"\n" + contents) + + ca_list_path = os.path.join(sublime.packages_path(), 'User', 'Package Control.ca-list') + with open_compat(ca_list_path, 'r') as f: + ca_certs = json.loads(read_compat(f)) + ca_certs.append(cert_id) + with open_compat(ca_list_path, 'w') as f: + f.write(json.dumps(ca_certs, indent=4)) diff --git a/sublime/Packages/Package Control/package_control/downloaders/cli_downloader.py b/sublime/Packages/Package Control/package_control/downloaders/cli_downloader.py new file mode 100644 index 0000000..76c42dd --- /dev/null +++ b/sublime/Packages/Package Control/package_control/downloaders/cli_downloader.py @@ -0,0 +1,81 @@ +import os +import subprocess + +from ..console_write import console_write +from ..cmd import create_cmd +from .non_clean_exit_error import NonCleanExitError +from .binary_not_found_error import BinaryNotFoundError + + +class CliDownloader(object): + """ + Base for downloaders that use a command line program + + :param settings: + A dict of the various Package Control settings. The Sublime Text + Settings API is not used because this code is run in a thread. + """ + + def __init__(self, settings): + self.settings = settings + + def clean_tmp_file(self): + if os.path.exists(self.tmp_file): + os.remove(self.tmp_file) + + def find_binary(self, name): + """ + Finds the given executable name in the system PATH + + :param name: + The exact name of the executable to find + + :return: + The absolute path to the executable + + :raises: + BinaryNotFoundError when the executable can not be found + """ + + dirs = os.environ['PATH'].split(os.pathsep) + if os.name != 'nt': + # This is mostly for OS X, which seems to launch ST with a + # minimal set of environmental variables + dirs.append('/usr/local/bin') + + for dir_ in dirs: + path = os.path.join(dir_, name) + if os.path.exists(path): + return path + + raise BinaryNotFoundError('The binary %s could not be located' % name) + + def execute(self, args): + """ + Runs the executable and args and returns the result + + :param args: + A list of the executable path and all arguments to be passed to it + + :return: + The text output of the executable + + :raises: + NonCleanExitError when the executable exits with an error + """ + + if self.settings.get('debug'): + console_write(u"Trying to execute command %s" % create_cmd(args), True) + + proc = subprocess.Popen(args, stdin=subprocess.PIPE, + stdout=subprocess.PIPE, stderr=subprocess.PIPE) + + output = proc.stdout.read() + self.stderr = proc.stderr.read() + returncode = proc.wait() + if returncode != 0: + error = NonCleanExitError(returncode) + error.stderr = self.stderr + error.stdout = output + raise error + return output diff --git a/sublime/Packages/Package Control/package_control/downloaders/curl_downloader.py b/sublime/Packages/Package Control/package_control/downloaders/curl_downloader.py new file mode 100644 index 0000000..b09d448 --- /dev/null +++ b/sublime/Packages/Package Control/package_control/downloaders/curl_downloader.py @@ -0,0 +1,267 @@ +import tempfile +import re +import os + +from ..console_write import console_write +from ..open_compat import open_compat, read_compat +from .cli_downloader import CliDownloader +from .non_clean_exit_error import NonCleanExitError +from .rate_limit_exception import RateLimitException +from .downloader_exception import DownloaderException +from .cert_provider import CertProvider +from .limiting_downloader import LimitingDownloader +from .caching_downloader import CachingDownloader + + +class CurlDownloader(CliDownloader, CertProvider, LimitingDownloader, CachingDownloader): + """ + A downloader that uses the command line program curl + + :param settings: + A dict of the various Package Control settings. The Sublime Text + Settings API is not used because this code is run in a thread. + + :raises: + BinaryNotFoundError: when curl can not be found + """ + + def __init__(self, settings): + self.settings = settings + self.curl = self.find_binary('curl') + + def close(self): + """ + No-op for compatibility with UrllibDownloader and WinINetDownloader + """ + + pass + + def download(self, url, error_message, timeout, tries, prefer_cached=False): + """ + Downloads a URL and returns the contents + + :param url: + The URL to download + + :param error_message: + A string to include in the console error that is printed + when an error occurs + + :param timeout: + The int number of seconds to set the timeout to + + :param tries: + The int number of times to try and download the URL in the case of + a timeout or HTTP 503 error + + :param prefer_cached: + If a cached version should be returned instead of trying a new request + + :raises: + NoCaCertException: when no CA certs can be found for the url + RateLimitException: when a rate limit is hit + DownloaderException: when any other download error occurs + + :return: + The string contents of the URL + """ + + if prefer_cached: + cached = self.retrieve_cached(url) + if cached: + return cached + + self.tmp_file = tempfile.NamedTemporaryFile().name + command = [self.curl, '--user-agent', self.settings.get('user_agent'), + '--connect-timeout', str(int(timeout)), '-sSL', + # Don't be alarmed if the response from the server does not select + # one of these since the server runs a relatively new version of + # OpenSSL which supports compression on the SSL layer, and Apache + # will use that instead of HTTP-level encoding. + '--compressed', + # We have to capture the headers to check for rate limit info + '--dump-header', self.tmp_file] + + request_headers = self.add_conditional_headers(url, {}) + + for name, value in request_headers.items(): + command.extend(['--header', "%s: %s" % (name, value)]) + + secure_url_match = re.match('^https://([^/]+)', url) + if secure_url_match != None: + secure_domain = secure_url_match.group(1) + bundle_path = self.check_certs(secure_domain, timeout) + command.extend(['--cacert', bundle_path]) + + debug = self.settings.get('debug') + if debug: + command.append('-v') + + http_proxy = self.settings.get('http_proxy') + https_proxy = self.settings.get('https_proxy') + proxy_username = self.settings.get('proxy_username') + proxy_password = self.settings.get('proxy_password') + + if debug: + console_write(u"Curl Debug Proxy", True) + console_write(u" http_proxy: %s" % http_proxy) + console_write(u" https_proxy: %s" % https_proxy) + console_write(u" proxy_username: %s" % proxy_username) + console_write(u" proxy_password: %s" % proxy_password) + + if http_proxy or https_proxy: + command.append('--proxy-anyauth') + + if proxy_username or proxy_password: + command.extend(['-U', u"%s:%s" % (proxy_username, proxy_password)]) + + if http_proxy: + os.putenv('http_proxy', http_proxy) + if https_proxy: + os.putenv('HTTPS_PROXY', https_proxy) + + command.append(url) + + error_string = None + while tries > 0: + tries -= 1 + try: + output = self.execute(command) + + with open_compat(self.tmp_file, 'r') as f: + headers_str = read_compat(f) + self.clean_tmp_file() + + message = 'OK' + status = 200 + headers = {} + for header in headers_str.splitlines(): + if header[0:5] == 'HTTP/': + message = re.sub('^HTTP/\d\.\d\s+\d+\s*', '', header) + status = int(re.sub('^HTTP/\d\.\d\s+(\d+)(\s+.*)?$', '\\1', header)) + continue + if header.strip() == '': + continue + name, value = header.split(':', 1) + headers[name.lower()] = value.strip() + + if debug: + self.print_debug(self.stderr.decode('utf-8')) + + self.handle_rate_limit(headers, url) + + if status not in [200, 304]: + e = NonCleanExitError(22) + e.stderr = "%s %s" % (status, message) + raise e + + output = self.cache_result('get', url, status, headers, output) + + return output + + except (NonCleanExitError) as e: + # Stderr is used for both the error message and the debug info + # so we need to process it to extra the debug info + if self.settings.get('debug'): + if hasattr(e.stderr, 'decode'): + e.stderr = e.stderr.decode('utf-8') + e.stderr = self.print_debug(e.stderr) + + self.clean_tmp_file() + + if e.returncode == 22: + code = re.sub('^.*?(\d+)([\w\s]+)?$', '\\1', e.stderr) + if code == '503' and tries != 0: + # GitHub and BitBucket seem to rate limit via 503 + error_string = u'Downloading %s was rate limited' % url + if tries: + error_string += ', trying again' + if debug: + console_write(error_string, True) + continue + + download_error = u'HTTP error ' + code + + elif e.returncode == 6: + download_error = u'URL error host not found' + + elif e.returncode == 28: + # GitHub and BitBucket seem to time out a lot + error_string = u'Downloading %s timed out' % url + if tries: + error_string += ', trying again' + if debug: + console_write(error_string, True) + continue + + else: + download_error = e.stderr.rstrip() + + error_string = u'%s %s downloading %s.' % (error_message, download_error, url) + + break + + raise DownloaderException(error_string) + + def supports_ssl(self): + """ + Indicates if the object can handle HTTPS requests + + :return: + If the object supports HTTPS requests + """ + + return True + + def print_debug(self, string): + """ + Takes debug output from curl and groups and prints it + + :param string: + The complete debug output from curl + + :return: + A string containing any stderr output + """ + + section = 'General' + last_section = None + + output = '' + + for line in string.splitlines(): + # Placeholder for body of request + if line and line[0:2] == '{ ': + continue + if line and line[0:18] == '} [data not shown]': + continue + + if len(line) > 1: + subtract = 0 + if line[0:2] == '* ': + section = 'General' + subtract = 2 + elif line[0:2] == '> ': + section = 'Write' + subtract = 2 + elif line[0:2] == '< ': + section = 'Read' + subtract = 2 + line = line[subtract:] + + # If the line does not start with "* ", "< ", "> " or " " + # then it is a real stderr message + if subtract == 0 and line[0:2] != ' ': + output += line.rstrip() + ' ' + continue + + if line.strip() == '': + continue + + if section != last_section: + console_write(u"Curl HTTP Debug %s" % section, True) + + console_write(u' ' + line) + last_section = section + + return output.rstrip() diff --git a/sublime/Packages/Package Control/package_control/downloaders/decoding_downloader.py b/sublime/Packages/Package Control/package_control/downloaders/decoding_downloader.py new file mode 100644 index 0000000..bc1acf3 --- /dev/null +++ b/sublime/Packages/Package Control/package_control/downloaders/decoding_downloader.py @@ -0,0 +1,24 @@ +import gzip +import zlib + +try: + # Python 3 + from io import BytesIO as StringIO +except (ImportError): + # Python 2 + from StringIO import StringIO + + +class DecodingDownloader(object): + """ + A base for downloaders that provides the ability to decode gzipped + or deflated content. + """ + + def decode_response(self, encoding, response): + if encoding == 'gzip': + return gzip.GzipFile(fileobj=StringIO(response)).read() + elif encoding == 'deflate': + decompresser = zlib.decompressobj(-zlib.MAX_WBITS) + return decompresser.decompress(response) + decompresser.flush() + return response diff --git a/sublime/Packages/Package Control/package_control/downloaders/downloader_exception.py b/sublime/Packages/Package Control/package_control/downloaders/downloader_exception.py new file mode 100644 index 0000000..7519d8f --- /dev/null +++ b/sublime/Packages/Package Control/package_control/downloaders/downloader_exception.py @@ -0,0 +1,5 @@ +class DownloaderException(Exception): + """If a downloader could not download a URL""" + + def __str__(self): + return self.args[0] diff --git a/sublime/Packages/Package Control/package_control/downloaders/http_error.py b/sublime/Packages/Package Control/package_control/downloaders/http_error.py new file mode 100644 index 0000000..996e46d --- /dev/null +++ b/sublime/Packages/Package Control/package_control/downloaders/http_error.py @@ -0,0 +1,9 @@ +class HttpError(Exception): + """If a downloader was able to download a URL, but the result was not a 200 or 304""" + + def __init__(self, message, code): + self.code = code + super(HttpError, self).__init__(message) + + def __str__(self): + return self.args[0] diff --git a/sublime/Packages/Package Control/package_control/downloaders/limiting_downloader.py b/sublime/Packages/Package Control/package_control/downloaders/limiting_downloader.py new file mode 100644 index 0000000..10d2f1f --- /dev/null +++ b/sublime/Packages/Package Control/package_control/downloaders/limiting_downloader.py @@ -0,0 +1,36 @@ +try: + # Python 3 + from urllib.parse import urlparse +except (ImportError): + # Python 2 + from urlparse import urlparse + +from .rate_limit_exception import RateLimitException + + +class LimitingDownloader(object): + """ + A base for downloaders that checks for rate limiting headers. + """ + + def handle_rate_limit(self, headers, url): + """ + Checks the headers of a response object to make sure we are obeying the + rate limit + + :param headers: + The dict-like object that contains lower-cased headers + + :param url: + The URL that was requested + + :raises: + RateLimitException when the rate limit has been hit + """ + + limit_remaining = headers.get('x-ratelimit-remaining', '1') + limit = headers.get('x-ratelimit-limit', '1') + + if str(limit_remaining) == '0': + hostname = urlparse(url).hostname + raise RateLimitException(hostname, limit) diff --git a/sublime/Packages/Package Control/package_control/downloaders/no_ca_cert_exception.py b/sublime/Packages/Package Control/package_control/downloaders/no_ca_cert_exception.py new file mode 100644 index 0000000..8452bd9 --- /dev/null +++ b/sublime/Packages/Package Control/package_control/downloaders/no_ca_cert_exception.py @@ -0,0 +1,11 @@ +from .downloader_exception import DownloaderException + + +class NoCaCertException(DownloaderException): + """ + An exception for when there is no CA cert for a domain name + """ + + def __init__(self, message, domain): + self.domain = domain + super(NoCaCertException, self).__init__(message) diff --git a/sublime/Packages/Package Control/package_control/downloaders/non_clean_exit_error.py b/sublime/Packages/Package Control/package_control/downloaders/non_clean_exit_error.py new file mode 100644 index 0000000..a932363 --- /dev/null +++ b/sublime/Packages/Package Control/package_control/downloaders/non_clean_exit_error.py @@ -0,0 +1,13 @@ +class NonCleanExitError(Exception): + """ + When an subprocess does not exit cleanly + + :param returncode: + The command line integer return code of the subprocess + """ + + def __init__(self, returncode): + self.returncode = returncode + + def __str__(self): + return repr(self.returncode) diff --git a/sublime/Packages/Package Control/package_control/downloaders/non_http_error.py b/sublime/Packages/Package Control/package_control/downloaders/non_http_error.py new file mode 100644 index 0000000..8a45595 --- /dev/null +++ b/sublime/Packages/Package Control/package_control/downloaders/non_http_error.py @@ -0,0 +1,5 @@ +class NonHttpError(Exception): + """If a downloader had a non-clean exit, but it was not due to an HTTP error""" + + def __str__(self): + return self.args[0] diff --git a/sublime/Packages/Package Control/package_control/downloaders/rate_limit_exception.py b/sublime/Packages/Package Control/package_control/downloaders/rate_limit_exception.py new file mode 100644 index 0000000..18d2b9e --- /dev/null +++ b/sublime/Packages/Package Control/package_control/downloaders/rate_limit_exception.py @@ -0,0 +1,13 @@ +from .downloader_exception import DownloaderException + + +class RateLimitException(DownloaderException): + """ + An exception for when the rate limit of an API has been exceeded. + """ + + def __init__(self, domain, limit): + self.domain = domain + self.limit = limit + message = u'Rate limit of %s exceeded for %s' % (limit, domain) + super(RateLimitException, self).__init__(message) diff --git a/sublime/Packages/Package Control/package_control/downloaders/urllib_downloader.py b/sublime/Packages/Package Control/package_control/downloaders/urllib_downloader.py new file mode 100644 index 0000000..aa04d31 --- /dev/null +++ b/sublime/Packages/Package Control/package_control/downloaders/urllib_downloader.py @@ -0,0 +1,291 @@ +import re +import os +import sys + +from .. import http + +try: + # Python 3 + from http.client import HTTPException, BadStatusLine + from urllib.request import ProxyHandler, HTTPPasswordMgrWithDefaultRealm, ProxyBasicAuthHandler, ProxyDigestAuthHandler, build_opener, Request + from urllib.error import HTTPError, URLError + import urllib.request as urllib_compat +except (ImportError): + # Python 2 + from httplib import HTTPException, BadStatusLine + from urllib2 import ProxyHandler, HTTPPasswordMgrWithDefaultRealm, ProxyBasicAuthHandler, ProxyDigestAuthHandler, build_opener, Request + from urllib2 import HTTPError, URLError + import urllib2 as urllib_compat + +try: + # Python 3.3 + import ConnectionError +except (ImportError): + # Python 2.6-3.2 + from socket import error as ConnectionError + +from ..console_write import console_write +from ..unicode import unicode_from_os +from ..http.validating_https_handler import ValidatingHTTPSHandler +from ..http.debuggable_http_handler import DebuggableHTTPHandler +from .rate_limit_exception import RateLimitException +from .downloader_exception import DownloaderException +from .cert_provider import CertProvider +from .decoding_downloader import DecodingDownloader +from .limiting_downloader import LimitingDownloader +from .caching_downloader import CachingDownloader + + +class UrlLibDownloader(CertProvider, DecodingDownloader, LimitingDownloader, CachingDownloader): + """ + A downloader that uses the Python urllib module + + :param settings: + A dict of the various Package Control settings. The Sublime Text + Settings API is not used because this code is run in a thread. + """ + + def __init__(self, settings): + self.opener = None + self.settings = settings + + def close(self): + """ + Closes any persistent/open connections + """ + + if not self.opener: + return + handler = self.get_handler() + if handler: + handler.close() + self.opener = None + + def download(self, url, error_message, timeout, tries, prefer_cached=False): + """ + Downloads a URL and returns the contents + + Uses the proxy settings from the Package Control.sublime-settings file, + however there seem to be a decent number of proxies that this code + does not work with. Patches welcome! + + :param url: + The URL to download + + :param error_message: + A string to include in the console error that is printed + when an error occurs + + :param timeout: + The int number of seconds to set the timeout to + + :param tries: + The int number of times to try and download the URL in the case of + a timeout or HTTP 503 error + + :param prefer_cached: + If a cached version should be returned instead of trying a new request + + :raises: + NoCaCertException: when no CA certs can be found for the url + RateLimitException: when a rate limit is hit + DownloaderException: when any other download error occurs + + :return: + The string contents of the URL + """ + + if prefer_cached: + cached = self.retrieve_cached(url) + if cached: + return cached + + self.setup_opener(url, timeout) + + debug = self.settings.get('debug') + error_string = None + while tries > 0: + tries -= 1 + try: + request_headers = { + "User-Agent": self.settings.get('user_agent'), + # Don't be alarmed if the response from the server does not + # select one of these since the server runs a relatively new + # version of OpenSSL which supports compression on the SSL + # layer, and Apache will use that instead of HTTP-level + # encoding. + "Accept-Encoding": "gzip,deflate" + } + request_headers = self.add_conditional_headers(url, request_headers) + request = Request(url, headers=request_headers) + http_file = self.opener.open(request, timeout=timeout) + self.handle_rate_limit(http_file.headers, url) + + result = http_file.read() + # Make sure the response is closed so we can re-use the connection + http_file.close() + + encoding = http_file.headers.get('content-encoding') + result = self.decode_response(encoding, result) + + return self.cache_result('get', url, http_file.getcode(), + http_file.headers, result) + + except (HTTPException) as e: + # Since we use keep-alives, it is possible the other end closed + # the connection, and we may just need to re-open + if isinstance(e, BadStatusLine): + handler = self.get_handler() + if handler and handler.use_count > 1: + self.close() + self.setup_opener(url, timeout) + tries += 1 + continue + + error_string = u'%s HTTP exception %s (%s) downloading %s.' % ( + error_message, e.__class__.__name__, unicode_from_os(e), url) + + except (HTTPError) as e: + # Make sure the response is closed so we can re-use the connection + e.read() + e.close() + + # Make sure we obey Github's rate limiting headers + self.handle_rate_limit(e.headers, url) + + # Handle cached responses + if unicode_from_os(e.code) == '304': + return self.cache_result('get', url, int(e.code), e.headers, b'') + + # Bitbucket and Github return 503 a decent amount + if unicode_from_os(e.code) == '503' and tries != 0: + error_string = u'Downloading %s was rate limited' % url + if tries: + error_string += ', trying again' + if debug: + console_write(error_string, True) + continue + + error_string = u'%s HTTP error %s downloading %s.' % ( + error_message, unicode_from_os(e.code), url) + + except (URLError) as e: + + # Bitbucket and Github timeout a decent amount + if unicode_from_os(e.reason) == 'The read operation timed out' \ + or unicode_from_os(e.reason) == 'timed out': + error_string = u'Downloading %s timed out' % url + if tries: + error_string += ', trying again' + if debug: + console_write(error_string, True) + continue + + error_string = u'%s URL error %s downloading %s.' % ( + error_message, unicode_from_os(e.reason), url) + + except (ConnectionError): + # Handle broken pipes/reset connections by creating a new opener, and + # thus getting new handlers and a new connection + error_string = u'Connection went away while trying to download %s, trying again' % url + if debug: + console_write(error_string, True) + + self.opener = None + self.setup_opener(url, timeout) + tries += 1 + + continue + + break + + raise DownloaderException(error_string) + + def get_handler(self): + """ + Get the HTTPHandler object for the current connection + """ + + if not self.opener: + return None + + for handler in self.opener.handlers: + if isinstance(handler, ValidatingHTTPSHandler) or isinstance(handler, DebuggableHTTPHandler): + return handler + + def setup_opener(self, url, timeout): + """ + Sets up a urllib OpenerDirector to be used for requests. There is a + fair amount of custom urllib code in Package Control, and part of it + is to handle proxies and keep-alives. Creating an opener the way + below is because the handlers have been customized to send the + "Connection: Keep-Alive" header and hold onto connections so they + can be re-used. + + :param url: + The URL to download + + :param timeout: + The int number of seconds to set the timeout to + """ + + if not self.opener: + http_proxy = self.settings.get('http_proxy') + https_proxy = self.settings.get('https_proxy') + if http_proxy or https_proxy: + proxies = {} + if http_proxy: + proxies['http'] = http_proxy + if https_proxy: + proxies['https'] = https_proxy + proxy_handler = ProxyHandler(proxies) + else: + proxy_handler = ProxyHandler() + + password_manager = HTTPPasswordMgrWithDefaultRealm() + proxy_username = self.settings.get('proxy_username') + proxy_password = self.settings.get('proxy_password') + if proxy_username and proxy_password: + if http_proxy: + password_manager.add_password(None, http_proxy, proxy_username, + proxy_password) + if https_proxy: + password_manager.add_password(None, https_proxy, proxy_username, + proxy_password) + + handlers = [proxy_handler] + + basic_auth_handler = ProxyBasicAuthHandler(password_manager) + digest_auth_handler = ProxyDigestAuthHandler(password_manager) + handlers.extend([digest_auth_handler, basic_auth_handler]) + + debug = self.settings.get('debug') + + if debug: + console_write(u"Urllib Debug Proxy", True) + console_write(u" http_proxy: %s" % http_proxy) + console_write(u" https_proxy: %s" % https_proxy) + console_write(u" proxy_username: %s" % proxy_username) + console_write(u" proxy_password: %s" % proxy_password) + + secure_url_match = re.match('^https://([^/]+)', url) + if secure_url_match != None: + secure_domain = secure_url_match.group(1) + bundle_path = self.check_certs(secure_domain, timeout) + bundle_path = bundle_path.encode(sys.getfilesystemencoding()) + handlers.append(ValidatingHTTPSHandler(ca_certs=bundle_path, + debug=debug, passwd=password_manager, + user_agent=self.settings.get('user_agent'))) + else: + handlers.append(DebuggableHTTPHandler(debug=debug, + passwd=password_manager)) + self.opener = build_opener(*handlers) + + def supports_ssl(self): + """ + Indicates if the object can handle HTTPS requests + + :return: + If the object supports HTTPS requests + """ + return 'ssl' in sys.modules and hasattr(urllib_compat, 'HTTPSHandler') diff --git a/sublime/Packages/Package Control/package_control/downloaders/wget_downloader.py b/sublime/Packages/Package Control/package_control/downloaders/wget_downloader.py new file mode 100644 index 0000000..fb83d1b --- /dev/null +++ b/sublime/Packages/Package Control/package_control/downloaders/wget_downloader.py @@ -0,0 +1,347 @@ +import tempfile +import re +import os + +from ..console_write import console_write +from ..unicode import unicode_from_os +from ..open_compat import open_compat, read_compat +from .cli_downloader import CliDownloader +from .non_http_error import NonHttpError +from .non_clean_exit_error import NonCleanExitError +from .rate_limit_exception import RateLimitException +from .downloader_exception import DownloaderException +from .cert_provider import CertProvider +from .decoding_downloader import DecodingDownloader +from .limiting_downloader import LimitingDownloader +from .caching_downloader import CachingDownloader + + +class WgetDownloader(CliDownloader, CertProvider, DecodingDownloader, LimitingDownloader, CachingDownloader): + """ + A downloader that uses the command line program wget + + :param settings: + A dict of the various Package Control settings. The Sublime Text + Settings API is not used because this code is run in a thread. + + :raises: + BinaryNotFoundError: when wget can not be found + """ + + def __init__(self, settings): + self.settings = settings + self.debug = settings.get('debug') + self.wget = self.find_binary('wget') + + def close(self): + """ + No-op for compatibility with UrllibDownloader and WinINetDownloader + """ + + pass + + def download(self, url, error_message, timeout, tries, prefer_cached=False): + """ + Downloads a URL and returns the contents + + :param url: + The URL to download + + :param error_message: + A string to include in the console error that is printed + when an error occurs + + :param timeout: + The int number of seconds to set the timeout to + + :param tries: + The int number of times to try and download the URL in the case of + a timeout or HTTP 503 error + + :param prefer_cached: + If a cached version should be returned instead of trying a new request + + :raises: + NoCaCertException: when no CA certs can be found for the url + RateLimitException: when a rate limit is hit + DownloaderException: when any other download error occurs + + :return: + The string contents of the URL + """ + + if prefer_cached: + cached = self.retrieve_cached(url) + if cached: + return cached + + self.tmp_file = tempfile.NamedTemporaryFile().name + command = [self.wget, '--connect-timeout=' + str(int(timeout)), '-o', + self.tmp_file, '-O', '-', '-U', self.settings.get('user_agent')] + + request_headers = { + # Don't be alarmed if the response from the server does not select + # one of these since the server runs a relatively new version of + # OpenSSL which supports compression on the SSL layer, and Apache + # will use that instead of HTTP-level encoding. + 'Accept-Encoding': 'gzip,deflate' + } + request_headers = self.add_conditional_headers(url, request_headers) + + for name, value in request_headers.items(): + command.extend(['--header', "%s: %s" % (name, value)]) + + secure_url_match = re.match('^https://([^/]+)', url) + if secure_url_match != None: + secure_domain = secure_url_match.group(1) + bundle_path = self.check_certs(secure_domain, timeout) + command.append(u'--ca-certificate=' + bundle_path) + + if self.debug: + command.append('-d') + else: + command.append('-S') + + http_proxy = self.settings.get('http_proxy') + https_proxy = self.settings.get('https_proxy') + proxy_username = self.settings.get('proxy_username') + proxy_password = self.settings.get('proxy_password') + + if proxy_username: + command.append(u"--proxy-user=%s" % proxy_username) + if proxy_password: + command.append(u"--proxy-password=%s" % proxy_password) + + if self.debug: + console_write(u"Wget Debug Proxy", True) + console_write(u" http_proxy: %s" % http_proxy) + console_write(u" https_proxy: %s" % https_proxy) + console_write(u" proxy_username: %s" % proxy_username) + console_write(u" proxy_password: %s" % proxy_password) + + command.append(url) + + if http_proxy: + os.putenv('http_proxy', http_proxy) + if https_proxy: + os.putenv('https_proxy', https_proxy) + + error_string = None + while tries > 0: + tries -= 1 + try: + result = self.execute(command) + + general, headers = self.parse_output() + encoding = headers.get('content-encoding') + if encoding: + result = self.decode_response(encoding, result) + + result = self.cache_result('get', url, general['status'], + headers, result) + + return result + + except (NonCleanExitError) as e: + + try: + general, headers = self.parse_output() + self.handle_rate_limit(headers, url) + + if general['status'] == 304: + return self.cache_result('get', url, general['status'], + headers, None) + + if general['status'] == 503 and tries != 0: + # GitHub and BitBucket seem to rate limit via 503 + error_string = u'Downloading %s was rate limited' % url + if tries: + error_string += ', trying again' + if self.debug: + console_write(error_string, True) + continue + + download_error = 'HTTP error %s' % general['status'] + + except (NonHttpError) as e: + + download_error = unicode_from_os(e) + + # GitHub and BitBucket seem to time out a lot + if download_error.find('timed out') != -1: + error_string = u'Downloading %s timed out' % url + if tries: + error_string += ', trying again' + if self.debug: + console_write(error_string, True) + continue + + error_string = u'%s %s downloading %s.' % (error_message, download_error, url) + + break + + raise DownloaderException(error_string) + + def supports_ssl(self): + """ + Indicates if the object can handle HTTPS requests + + :return: + If the object supports HTTPS requests + """ + + return True + + def parse_output(self): + """ + Parses the wget output file, prints debug information and returns headers + + :return: + A tuple of (general, headers) where general is a dict with the keys: + `version` - HTTP version number (string) + `status` - HTTP status code (integer) + `message` - HTTP status message (string) + And headers is a dict with the keys being lower-case version of the + HTTP header names. + """ + + with open_compat(self.tmp_file, 'r') as f: + output = read_compat(f).splitlines() + self.clean_tmp_file() + + error = None + header_lines = [] + if self.debug: + section = 'General' + last_section = None + for line in output: + if section == 'General': + if self.skippable_line(line): + continue + + # Skip blank lines + if line.strip() == '': + continue + + # Error lines + if line[0:5] == 'wget:': + error = line[5:].strip() + if line[0:7] == 'failed:': + error = line[7:].strip() + + if line == '---request begin---': + section = 'Write' + continue + elif line == '---request end---': + section = 'General' + continue + elif line == '---response begin---': + section = 'Read' + continue + elif line == '---response end---': + section = 'General' + continue + + if section != last_section: + console_write(u"Wget HTTP Debug %s" % section, True) + + if section == 'Read': + header_lines.append(line) + + console_write(u' ' + line) + last_section = section + + else: + for line in output: + if self.skippable_line(line): + continue + + # Check the resolving and connecting to lines for errors + if re.match('(Resolving |Connecting to )', line): + failed_match = re.search(' failed: (.*)$', line) + if failed_match: + error = failed_match.group(1).strip() + + # Error lines + if line[0:5] == 'wget:': + error = line[5:].strip() + if line[0:7] == 'failed:': + error = line[7:].strip() + + if line[0:2] == ' ': + header_lines.append(line.lstrip()) + + if error: + raise NonHttpError(error) + + return self.parse_headers(header_lines) + + def skippable_line(self, line): + """ + Determines if a debug line is skippable - usually because of extraneous + or duplicate information. + + :param line: + The debug line to check + + :return: + True if the line is skippable, otherwise None + """ + + # Skip date lines + if re.match('--\d{4}-\d{2}-\d{2}', line): + return True + if re.match('\d{4}-\d{2}-\d{2}', line): + return True + # Skip HTTP status code lines since we already have that info + if re.match('\d{3} ', line): + return True + # Skip Saving to and progress lines + if re.match('(Saving to:|\s*\d+K)', line): + return True + # Skip notice about ignoring body on HTTP error + if re.match('Skipping \d+ byte', line): + return True + + def parse_headers(self, output=None): + """ + Parses HTTP headers into two dict objects + + :param output: + An array of header lines, if None, loads from temp output file + + :return: + A tuple of (general, headers) where general is a dict with the keys: + `version` - HTTP version number (string) + `status` - HTTP status code (integer) + `message` - HTTP status message (string) + And headers is a dict with the keys being lower-case version of the + HTTP header names. + """ + + if not output: + with open_compat(self.tmp_file, 'r') as f: + output = read_compat(f).splitlines() + self.clean_tmp_file() + + general = { + 'version': '0.9', + 'status': 200, + 'message': 'OK' + } + headers = {} + for line in output: + # When using the -S option, headers have two spaces before them, + # additionally, valid headers won't have spaces, so this is always + # a safe operation to perform + line = line.lstrip() + if line.find('HTTP/') == 0: + match = re.match('HTTP/(\d\.\d)\s+(\d+)(?:\s+(.*))?$', line) + general['version'] = match.group(1) + general['status'] = int(match.group(2)) + general['message'] = match.group(3) or '' + else: + name, value = line.split(':', 1) + headers[name.lower()] = value.strip() + + return (general, headers) diff --git a/sublime/Packages/Package Control/package_control/downloaders/wininet_downloader.py b/sublime/Packages/Package Control/package_control/downloaders/wininet_downloader.py new file mode 100644 index 0000000..7134db9 --- /dev/null +++ b/sublime/Packages/Package Control/package_control/downloaders/wininet_downloader.py @@ -0,0 +1,652 @@ +from ctypes import windll, wintypes +import ctypes +import time +import re +import datetime +import struct +import locale + +wininet = windll.wininet + +try: + # Python 3 + from urllib.parse import urlparse +except (ImportError): + # Python 2 + from urlparse import urlparse + +from ..console_write import console_write +from ..unicode import unicode_from_os +from .non_http_error import NonHttpError +from .http_error import HttpError +from .rate_limit_exception import RateLimitException +from .downloader_exception import DownloaderException +from .decoding_downloader import DecodingDownloader +from .limiting_downloader import LimitingDownloader +from .caching_downloader import CachingDownloader + + +class WinINetDownloader(DecodingDownloader, LimitingDownloader, CachingDownloader): + """ + A downloader that uses the Windows WinINet DLL to perform downloads. This + has the benefit of utilizing system-level proxy configuration and CA certs. + + :param settings: + A dict of the various Package Control settings. The Sublime Text + Settings API is not used because this code is run in a thread. + """ + + # General constants + ERROR_INSUFFICIENT_BUFFER = 122 + + # InternetOpen constants + INTERNET_OPEN_TYPE_PRECONFIG = 0 + + # InternetConnect constants + INTERNET_SERVICE_HTTP = 3 + INTERNET_FLAG_EXISTING_CONNECT = 0x20000000 + INTERNET_FLAG_IGNORE_REDIRECT_TO_HTTPS = 0x00004000 + + # InternetSetOption constants + INTERNET_OPTION_CONNECT_TIMEOUT = 2 + INTERNET_OPTION_SEND_TIMEOUT = 5 + INTERNET_OPTION_RECEIVE_TIMEOUT = 6 + + # InternetQueryOption constants + INTERNET_OPTION_SECURITY_CERTIFICATE_STRUCT = 32 + INTERNET_OPTION_PROXY = 38 + INTERNET_OPTION_PROXY_USERNAME = 43 + INTERNET_OPTION_PROXY_PASSWORD = 44 + INTERNET_OPTION_CONNECTED_STATE = 50 + + # HttpOpenRequest constants + INTERNET_FLAG_KEEP_CONNECTION = 0x00400000 + INTERNET_FLAG_RELOAD = 0x80000000 + INTERNET_FLAG_NO_CACHE_WRITE = 0x04000000 + INTERNET_FLAG_PRAGMA_NOCACHE = 0x00000100 + INTERNET_FLAG_SECURE = 0x00800000 + + # HttpQueryInfo constants + HTTP_QUERY_RAW_HEADERS_CRLF = 22 + + # InternetConnectedState constants + INTERNET_STATE_CONNECTED = 1 + INTERNET_STATE_DISCONNECTED = 2 + INTERNET_STATE_DISCONNECTED_BY_USER = 0x10 + INTERNET_STATE_IDLE = 0x100 + INTERNET_STATE_BUSY = 0x200 + + + def __init__(self, settings): + self.settings = settings + self.debug = settings.get('debug') + self.network_connection = None + self.tcp_connection = None + self.use_count = 0 + self.hostname = None + self.port = None + self.scheme = None + self.was_offline = None + + def close(self): + """ + Closes any persistent/open connections + """ + + closed = False + changed_state_back = False + + if self.tcp_connection: + wininet.InternetCloseHandle(self.tcp_connection) + self.tcp_connection = None + closed = True + + if self.network_connection: + wininet.InternetCloseHandle(self.network_connection) + self.network_connection = None + closed = True + + if self.was_offline: + dw_connected_state = wintypes.DWORD(self.INTERNET_STATE_DISCONNECTED_BY_USER) + dw_flags = wintypes.DWORD(0) + connected_info = InternetConnectedInfo(dw_connected_state, dw_flags) + wininet.InternetSetOptionA(None, + self.INTERNET_OPTION_CONNECTED_STATE, ctypes.byref(connected_info), ctypes.sizeof(connected_info)) + changed_state_back = True + + if self.debug: + s = '' if self.use_count == 1 else 's' + console_write(u"WinINet %s Debug General" % self.scheme.upper(), True) + console_write(u" Closing connection to %s on port %s after %s request%s" % ( + self.hostname, self.port, self.use_count, s)) + if changed_state_back: + console_write(u" Changed Internet Explorer back to Work Offline") + + self.hostname = None + self.port = None + self.scheme = None + self.use_count = 0 + self.was_offline = None + + def download(self, url, error_message, timeout, tries, prefer_cached=False): + """ + Downloads a URL and returns the contents + + :param url: + The URL to download + + :param error_message: + A string to include in the console error that is printed + when an error occurs + + :param timeout: + The int number of seconds to set the timeout to + + :param tries: + The int number of times to try and download the URL in the case of + a timeout or HTTP 503 error + + :param prefer_cached: + If a cached version should be returned instead of trying a new request + + :raises: + RateLimitException: when a rate limit is hit + DownloaderException: when any other download error occurs + + :return: + The string contents of the URL + """ + + if prefer_cached: + cached = self.retrieve_cached(url) + if cached: + return cached + + url_info = urlparse(url) + + if not url_info.port: + port = 443 if url_info.scheme == 'https' else 80 + hostname = url_info.netloc + else: + port = url_info.port + hostname = url_info.hostname + + path = url_info.path + if url_info.params: + path += ';' + url_info.params + if url_info.query: + path += '?' + url_info.query + + request_headers = { + 'Accept-Encoding': 'gzip,deflate' + } + request_headers = self.add_conditional_headers(url, request_headers) + + created_connection = False + # If we switched Internet Explorer out of "Work Offline" mode + changed_to_online = False + + # If the user is requesting a connection to another server, close the connection + if (self.hostname and self.hostname != hostname) or (self.port and self.port != port): + self.close() + + # Reset the error info to a known clean state + ctypes.windll.kernel32.SetLastError(0) + + # Save the internet setup in the class for re-use + if not self.tcp_connection: + created_connection = True + + # Connect to the internet if necessary + state = self.read_option(None, self.INTERNET_OPTION_CONNECTED_STATE) + state = ord(state) + if state & self.INTERNET_STATE_DISCONNECTED or state & self.INTERNET_STATE_DISCONNECTED_BY_USER: + # Track the previous state so we can go back once complete + self.was_offline = True + + dw_connected_state = wintypes.DWORD(self.INTERNET_STATE_CONNECTED) + dw_flags = wintypes.DWORD(0) + connected_info = InternetConnectedInfo(dw_connected_state, dw_flags) + wininet.InternetSetOptionA(None, + self.INTERNET_OPTION_CONNECTED_STATE, ctypes.byref(connected_info), ctypes.sizeof(connected_info)) + changed_to_online = True + + self.network_connection = wininet.InternetOpenW(self.settings.get('user_agent'), + self.INTERNET_OPEN_TYPE_PRECONFIG, None, None, 0) + + if not self.network_connection: + error_string = u'%s %s during network phase of downloading %s.' % (error_message, self.extract_error(), url) + raise DownloaderException(error_string) + + win_timeout = wintypes.DWORD(int(timeout) * 1000) + # Apparently INTERNET_OPTION_CONNECT_TIMEOUT just doesn't work, leaving it in hoping they may fix in the future + wininet.InternetSetOptionA(self.network_connection, + self.INTERNET_OPTION_CONNECT_TIMEOUT, win_timeout, ctypes.sizeof(win_timeout)) + wininet.InternetSetOptionA(self.network_connection, + self.INTERNET_OPTION_SEND_TIMEOUT, win_timeout, ctypes.sizeof(win_timeout)) + wininet.InternetSetOptionA(self.network_connection, + self.INTERNET_OPTION_RECEIVE_TIMEOUT, win_timeout, ctypes.sizeof(win_timeout)) + + # Don't allow HTTPS sites to redirect to HTTP sites + tcp_flags = self.INTERNET_FLAG_IGNORE_REDIRECT_TO_HTTPS + # Try to re-use an existing connection to the server + tcp_flags |= self.INTERNET_FLAG_EXISTING_CONNECT + self.tcp_connection = wininet.InternetConnectW(self.network_connection, + hostname, port, None, None, self.INTERNET_SERVICE_HTTP, tcp_flags, 0) + + if not self.tcp_connection: + error_string = u'%s %s during connection phase of downloading %s.' % (error_message, self.extract_error(), url) + raise DownloaderException(error_string) + + # Normally the proxy info would come from IE, but this allows storing it in + # the Package Control settings file. + proxy_username = self.settings.get('proxy_username') + proxy_password = self.settings.get('proxy_password') + if proxy_username and proxy_password: + username = ctypes.c_wchar_p(proxy_username) + password = ctypes.c_wchar_p(proxy_password) + wininet.InternetSetOptionW(self.tcp_connection, + self.INTERNET_OPTION_PROXY_USERNAME, ctypes.cast(username, ctypes.c_void_p), len(proxy_username)) + wininet.InternetSetOptionW(self.tcp_connection, + self.INTERNET_OPTION_PROXY_PASSWORD, ctypes.cast(password, ctypes.c_void_p), len(proxy_password)) + + self.hostname = hostname + self.port = port + self.scheme = url_info.scheme + + else: + if self.debug: + console_write(u"WinINet %s Debug General" % self.scheme.upper(), True) + console_write(u" Re-using connection to %s on port %s for request #%s" % ( + self.hostname, self.port, self.use_count)) + + error_string = None + while tries > 0: + tries -= 1 + try: + http_connection = None + + # Keep-alive for better performance + http_flags = self.INTERNET_FLAG_KEEP_CONNECTION + # Prevent caching/retrieving from cache + http_flags |= self.INTERNET_FLAG_RELOAD + http_flags |= self.INTERNET_FLAG_NO_CACHE_WRITE + http_flags |= self.INTERNET_FLAG_PRAGMA_NOCACHE + # Use SSL + if self.scheme == 'https': + http_flags |= self.INTERNET_FLAG_SECURE + + http_connection = wininet.HttpOpenRequestW(self.tcp_connection, u'GET', path, u'HTTP/1.1', None, None, http_flags, 0) + if not http_connection: + error_string = u'%s %s during HTTP connection phase of downloading %s.' % (error_message, self.extract_error(), url) + raise DownloaderException(error_string) + + request_header_lines = [] + for header, value in request_headers.items(): + request_header_lines.append(u"%s: %s" % (header, value)) + request_header_lines = u"\r\n".join(request_header_lines) + + success = wininet.HttpSendRequestW(http_connection, request_header_lines, len(request_header_lines), None, 0) + + if not success: + error_string = u'%s %s during HTTP write phase of downloading %s.' % (error_message, self.extract_error(), url) + raise DownloaderException(error_string) + + # If we try to query before here, the proxy info will not be available to the first request + if self.debug: + proxy_struct = self.read_option(self.network_connection, self.INTERNET_OPTION_PROXY) + proxy = '' + if proxy_struct.lpszProxy: + proxy = proxy_struct.lpszProxy.decode('cp1252') + proxy_bypass = '' + if proxy_struct.lpszProxyBypass: + proxy_bypass = proxy_struct.lpszProxyBypass.decode('cp1252') + + proxy_username = self.read_option(self.tcp_connection, self.INTERNET_OPTION_PROXY_USERNAME) + proxy_password = self.read_option(self.tcp_connection, self.INTERNET_OPTION_PROXY_PASSWORD) + + console_write(u"WinINet Debug Proxy", True) + console_write(u" proxy: %s" % proxy) + console_write(u" proxy bypass: %s" % proxy_bypass) + console_write(u" proxy username: %s" % proxy_username) + console_write(u" proxy password: %s" % proxy_password) + + self.use_count += 1 + + if self.debug and created_connection: + if self.scheme == 'https': + cert_struct = self.read_option(http_connection, self.INTERNET_OPTION_SECURITY_CERTIFICATE_STRUCT) + + if cert_struct.lpszIssuerInfo: + issuer_info = cert_struct.lpszIssuerInfo.decode('cp1252') + issuer_parts = issuer_info.split("\r\n") + else: + issuer_parts = ['No issuer info'] + + if cert_struct.lpszSubjectInfo: + subject_info = cert_struct.lpszSubjectInfo.decode('cp1252') + subject_parts = subject_info.split("\r\n") + else: + subject_parts = ["No subject info"] + + common_name = subject_parts[-1] + + if cert_struct.ftStart.dwLowDateTime != 0 and cert_struct.ftStart.dwHighDateTime != 0: + issue_date = self.convert_filetime_to_datetime(cert_struct.ftStart) + issue_date = issue_date.strftime('%a, %d %b %Y %H:%M:%S GMT') + else: + issue_date = u"No issue date" + + if cert_struct.ftExpiry.dwLowDateTime != 0 and cert_struct.ftExpiry.dwHighDateTime != 0: + expiration_date = self.convert_filetime_to_datetime(cert_struct.ftExpiry) + expiration_date = expiration_date.strftime('%a, %d %b %Y %H:%M:%S GMT') + else: + expiration_date = u"No expiration date" + + console_write(u"WinINet HTTPS Debug General", True) + if changed_to_online: + console_write(u" Internet Explorer was set to Work Offline, temporarily going online") + console_write(u" Server SSL Certificate:") + console_write(u" subject: %s" % ", ".join(subject_parts)) + console_write(u" issuer: %s" % ", ".join(issuer_parts)) + console_write(u" common name: %s" % common_name) + console_write(u" issue date: %s" % issue_date) + console_write(u" expire date: %s" % expiration_date) + + elif changed_to_online: + console_write(u"WinINet HTTP Debug General", True) + console_write(u" Internet Explorer was set to Work Offline, temporarily going online") + + if self.debug: + console_write(u"WinINet %s Debug Write" % self.scheme.upper(), True) + # Add in some known headers that WinINet sends since we can't get the real list + console_write(u" GET %s HTTP/1.1" % path) + for header, value in request_headers.items(): + console_write(u" %s: %s" % (header, value)) + console_write(u" User-Agent: %s" % self.settings.get('user_agent')) + console_write(u" Host: %s" % hostname) + console_write(u" Connection: Keep-Alive") + console_write(u" Cache-Control: no-cache") + + header_buffer_size = 8192 + + try_again = True + while try_again: + try_again = False + + to_read_was_read = wintypes.DWORD(header_buffer_size) + headers_buffer = ctypes.create_string_buffer(header_buffer_size) + + success = wininet.HttpQueryInfoA(http_connection, self.HTTP_QUERY_RAW_HEADERS_CRLF, ctypes.byref(headers_buffer), ctypes.byref(to_read_was_read), None) + if not success: + if ctypes.GetLastError() != self.ERROR_INSUFFICIENT_BUFFER: + error_string = u'%s %s during header read phase of downloading %s.' % (error_message, self.extract_error(), url) + raise DownloaderException(error_string) + # The error was a buffer that was too small, so try again + header_buffer_size = to_read_was_read.value + try_again = True + continue + + headers = b'' + if to_read_was_read.value > 0: + headers += headers_buffer.raw[:to_read_was_read.value] + headers = headers.decode('iso-8859-1').rstrip("\r\n").split("\r\n") + + if self.debug: + console_write(u"WinINet %s Debug Read" % self.scheme.upper(), True) + for header in headers: + console_write(u" %s" % header) + + buffer_length = 65536 + output_buffer = ctypes.create_string_buffer(buffer_length) + bytes_read = wintypes.DWORD() + + result = b'' + try_again = True + while try_again: + try_again = False + wininet.InternetReadFile(http_connection, output_buffer, buffer_length, ctypes.byref(bytes_read)) + if bytes_read.value > 0: + result += output_buffer.raw[:bytes_read.value] + try_again = True + + general, headers = self.parse_headers(headers) + self.handle_rate_limit(headers, url) + + if general['status'] == 503 and tries != 0: + # GitHub and BitBucket seem to rate limit via 503 + error_string = u'Downloading %s was rate limited' % url + if tries: + error_string += ', trying again' + if self.debug: + console_write(error_string, True) + continue + + encoding = headers.get('content-encoding') + if encoding: + result = self.decode_response(encoding, result) + + result = self.cache_result('get', url, general['status'], + headers, result) + + if general['status'] not in [200, 304]: + raise HttpError("HTTP error %s" % general['status'], general['status']) + + return result + + except (NonHttpError, HttpError) as e: + + # GitHub and BitBucket seem to time out a lot + if str(e).find('timed out') != -1: + error_string = u'Downloading %s timed out' % url + if tries: + error_string += ', trying again' + if self.debug: + console_write(error_string, True) + continue + + error_string = u'%s %s downloading %s.' % (error_message, e, url) + + finally: + if http_connection: + wininet.InternetCloseHandle(http_connection) + + break + + raise DownloaderException(error_string) + + def convert_filetime_to_datetime(self, filetime): + """ + Windows returns times as 64-bit unsigned longs that are the number + of hundreds of nanoseconds since Jan 1 1601. This converts it to + a datetime object. + + :param filetime: + A FileTime struct object + + :return: + A (UTC) datetime object + """ + + hundreds_nano_seconds = struct.unpack('>Q', struct.pack('>LL', filetime.dwHighDateTime, filetime.dwLowDateTime))[0] + seconds_since_1601 = hundreds_nano_seconds / 10000000 + epoch_seconds = seconds_since_1601 - 11644473600 # Seconds from Jan 1 1601 to Jan 1 1970 + return datetime.datetime.fromtimestamp(epoch_seconds) + + def extract_error(self): + """ + Retrieves and formats an error from WinINet + + :return: + A string with a nice description of the error + """ + + error_num = ctypes.GetLastError() + raw_error_string = ctypes.FormatError(error_num) + + error_string = unicode_from_os(raw_error_string) + + # Try to fill in some known errors + if error_string == u"": + error_lookup = { + 12007: u'host not found', + 12029: u'connection refused', + 12057: u'error checking for server certificate revocation', + 12169: u'invalid secure certificate', + 12157: u'secure channel error, server not providing SSL', + 12002: u'operation timed out' + } + if error_num in error_lookup: + error_string = error_lookup[error_num] + + if error_string == u"": + return u"(errno %s)" % error_num + + error_string = error_string[0].upper() + error_string[1:] + return u"%s (errno %s)" % (error_string, error_num) + + def supports_ssl(self): + """ + Indicates if the object can handle HTTPS requests + + :return: + If the object supports HTTPS requests + """ + + return True + + def read_option(self, handle, option): + """ + Reads information about the internet connection, which may be a string or struct + + :param handle: + The handle to query for the info + + :param option: + The (int) option to get + + :return: + A string, or one of the InternetCertificateInfo or InternetProxyInfo structs + """ + + option_buffer_size = 8192 + try_again = True + + while try_again: + try_again = False + + to_read_was_read = wintypes.DWORD(option_buffer_size) + option_buffer = ctypes.create_string_buffer(option_buffer_size) + ref = ctypes.byref(option_buffer) + + success = wininet.InternetQueryOptionA(handle, option, ref, ctypes.byref(to_read_was_read)) + if not success: + if ctypes.GetLastError() != self.ERROR_INSUFFICIENT_BUFFER: + raise NonHttpError(self.extract_error()) + # The error was a buffer that was too small, so try again + option_buffer_size = to_read_was_read.value + try_again = True + continue + + if option == self.INTERNET_OPTION_SECURITY_CERTIFICATE_STRUCT: + length = min(len(option_buffer), ctypes.sizeof(InternetCertificateInfo)) + cert_info = InternetCertificateInfo() + ctypes.memmove(ctypes.addressof(cert_info), option_buffer, length) + return cert_info + elif option == self.INTERNET_OPTION_PROXY: + length = min(len(option_buffer), ctypes.sizeof(InternetProxyInfo)) + proxy_info = InternetProxyInfo() + ctypes.memmove(ctypes.addressof(proxy_info), option_buffer, length) + return proxy_info + else: + option = b'' + if to_read_was_read.value > 0: + option += option_buffer.raw[:to_read_was_read.value] + return option.decode('cp1252').rstrip("\x00") + + def parse_headers(self, output): + """ + Parses HTTP headers into two dict objects + + :param output: + An array of header lines + + :return: + A tuple of (general, headers) where general is a dict with the keys: + `version` - HTTP version number (string) + `status` - HTTP status code (integer) + `message` - HTTP status message (string) + And headers is a dict with the keys being lower-case version of the + HTTP header names. + """ + + general = { + 'version': '0.9', + 'status': 200, + 'message': 'OK' + } + headers = {} + for line in output: + line = line.lstrip() + if line.find('HTTP/') == 0: + match = re.match('HTTP/(\d\.\d)\s+(\d+)\s+(.*)$', line) + general['version'] = match.group(1) + general['status'] = int(match.group(2)) + general['message'] = match.group(3) + else: + name, value = line.split(':', 1) + headers[name.lower()] = value.strip() + + return (general, headers) + + +class FileTime(ctypes.Structure): + """ + A Windows struct used by InternetCertificateInfo for certificate + date information + """ + + _fields_ = [ + ("dwLowDateTime", wintypes.DWORD), + ("dwHighDateTime", wintypes.DWORD) + ] + + +class InternetCertificateInfo(ctypes.Structure): + """ + A Windows struct used to store information about an SSL certificate + """ + + _fields_ = [ + ("ftExpiry", FileTime), + ("ftStart", FileTime), + ("lpszSubjectInfo", ctypes.c_char_p), + ("lpszIssuerInfo", ctypes.c_char_p), + ("lpszProtocolName", ctypes.c_char_p), + ("lpszSignatureAlgName", ctypes.c_char_p), + ("lpszEncryptionAlgName", ctypes.c_char_p), + ("dwKeySize", wintypes.DWORD) + ] + + +class InternetProxyInfo(ctypes.Structure): + """ + A Windows struct usd to store information about the configured proxy server + """ + + _fields_ = [ + ("dwAccessType", wintypes.DWORD), + ("lpszProxy", ctypes.c_char_p), + ("lpszProxyBypass", ctypes.c_char_p) + ] + + +class InternetConnectedInfo(ctypes.Structure): + """ + A Windows struct usd to store information about the global internet connection state + """ + + _fields_ = [ + ("dwConnectedState", wintypes.DWORD), + ("dwFlags", wintypes.DWORD) + ] diff --git a/sublime/Packages/Package Control/package_control/file_not_found_error.py b/sublime/Packages/Package Control/package_control/file_not_found_error.py new file mode 100644 index 0000000..3fd4da5 --- /dev/null +++ b/sublime/Packages/Package Control/package_control/file_not_found_error.py @@ -0,0 +1,4 @@ +class FileNotFoundError(Exception): + """If a file is not found""" + + pass diff --git a/sublime/Packages/Package Control/package_control/http/__init__.py b/sublime/Packages/Package Control/package_control/http/__init__.py new file mode 100644 index 0000000..e3358df --- /dev/null +++ b/sublime/Packages/Package Control/package_control/http/__init__.py @@ -0,0 +1,65 @@ +import sys + +try: + # Python 2 + import urllib2 + import httplib + + # Monkey patch AbstractBasicAuthHandler to prevent infinite recursion + def non_recursive_http_error_auth_reqed(self, authreq, host, req, headers): + authreq = headers.get(authreq, None) + + if not hasattr(self, 'retried'): + self.retried = 0 + + if self.retried > 5: + raise urllib2.HTTPError(req.get_full_url(), 401, "basic auth failed", + headers, None) + else: + self.retried += 1 + + if authreq: + mo = urllib2.AbstractBasicAuthHandler.rx.search(authreq) + if mo: + scheme, quote, realm = mo.groups() + if scheme.lower() == 'basic': + return self.retry_http_basic_auth(host, req, realm) + + urllib2.AbstractBasicAuthHandler.http_error_auth_reqed = non_recursive_http_error_auth_reqed + + # Money patch urllib2.Request and httplib.HTTPConnection so that + # HTTPS proxies work in Python 2.6.1-2 + if sys.version_info < (2, 6, 3): + + urllib2.Request._tunnel_host = None + + def py268_set_proxy(self, host, type): + if self.type == 'https' and not self._tunnel_host: + self._tunnel_host = self.host + else: + self.type = type + # The _Request prefix is to handle python private name mangling + self._Request__r_host = self._Request__original + self.host = host + urllib2.Request.set_proxy = py268_set_proxy + + if sys.version_info < (2, 6, 5): + + def py268_set_tunnel(self, host, port=None, headers=None): + """ Sets up the host and the port for the HTTP CONNECT Tunnelling. + + The headers argument should be a mapping of extra HTTP headers + to send with the CONNECT request. + """ + self._tunnel_host = host + self._tunnel_port = port + if headers: + self._tunnel_headers = headers + else: + self._tunnel_headers.clear() + httplib.HTTPConnection._set_tunnel = py268_set_tunnel + + +except (ImportError): + # Python 3 does not need to be patched + pass diff --git a/sublime/Packages/Package Control/package_control/http/debuggable_http_connection.py b/sublime/Packages/Package Control/package_control/http/debuggable_http_connection.py new file mode 100644 index 0000000..e0044a9 --- /dev/null +++ b/sublime/Packages/Package Control/package_control/http/debuggable_http_connection.py @@ -0,0 +1,72 @@ +import os +import re +import socket + +try: + # Python 3 + from http.client import HTTPConnection + from urllib.error import URLError +except (ImportError): + # Python 2 + from httplib import HTTPConnection + from urllib2 import URLError + +from ..console_write import console_write +from .debuggable_http_response import DebuggableHTTPResponse + + +class DebuggableHTTPConnection(HTTPConnection): + """ + A custom HTTPConnection that formats debugging info for Sublime Text + """ + + response_class = DebuggableHTTPResponse + _debug_protocol = 'HTTP' + + def __init__(self, host, port=None, timeout=socket._GLOBAL_DEFAULT_TIMEOUT, + **kwargs): + self.passwd = kwargs.get('passwd') + + # Python 2.6.1 on OS X 10.6 does not include these + self._tunnel_host = None + self._tunnel_port = None + self._tunnel_headers = {} + if 'debug' in kwargs and kwargs['debug']: + self.debuglevel = 5 + elif 'debuglevel' in kwargs: + self.debuglevel = kwargs['debuglevel'] + + HTTPConnection.__init__(self, host, port=port, timeout=timeout) + + def connect(self): + if self.debuglevel == -1: + console_write(u'Urllib %s Debug General' % self._debug_protocol, True) + console_write(u" Connecting to %s on port %s" % (self.host, self.port)) + HTTPConnection.connect(self) + + def send(self, string): + # We have to use a positive debuglevel to get it passed to the + # HTTPResponse object, however we don't want to use it because by + # default debugging prints to the stdout and we can't capture it, so + # we temporarily set it to -1 for the standard httplib code + reset_debug = False + if self.debuglevel == 5: + reset_debug = 5 + self.debuglevel = -1 + HTTPConnection.send(self, string) + if reset_debug or self.debuglevel == -1: + if len(string.strip()) > 0: + console_write(u'Urllib %s Debug Write' % self._debug_protocol, True) + for line in string.strip().splitlines(): + console_write(u' ' + line.decode('iso-8859-1')) + if reset_debug: + self.debuglevel = reset_debug + + def request(self, method, url, body=None, headers={}): + original_headers = headers.copy() + + # By default urllib2 and urllib.request override the Connection header, + # however, it is preferred to be able to re-use it + original_headers['Connection'] = 'Keep-Alive' + + HTTPConnection.request(self, method, url, body, original_headers) diff --git a/sublime/Packages/Package Control/package_control/http/debuggable_http_handler.py b/sublime/Packages/Package Control/package_control/http/debuggable_http_handler.py new file mode 100644 index 0000000..ae4b8d1 --- /dev/null +++ b/sublime/Packages/Package Control/package_control/http/debuggable_http_handler.py @@ -0,0 +1,35 @@ +import sys + +try: + # Python 3 + from urllib.request import HTTPHandler +except (ImportError): + # Python 2 + from urllib2 import HTTPHandler + +from .debuggable_http_connection import DebuggableHTTPConnection +from .persistent_handler import PersistentHandler + + +class DebuggableHTTPHandler(PersistentHandler, HTTPHandler): + """ + A custom HTTPHandler that formats debugging info for Sublime Text + """ + + def __init__(self, debuglevel=0, debug=False, **kwargs): + # This is a special value that will not trigger the standard debug + # functionality, but custom code where we can format the output + if debug: + self._debuglevel = 5 + else: + self._debuglevel = debuglevel + self.passwd = kwargs.get('passwd') + + def http_open(self, req): + def http_class_wrapper(host, **kwargs): + kwargs['passwd'] = self.passwd + if 'debuglevel' not in kwargs: + kwargs['debuglevel'] = self._debuglevel + return DebuggableHTTPConnection(host, **kwargs) + + return self.do_open(http_class_wrapper, req) diff --git a/sublime/Packages/Package Control/package_control/http/debuggable_http_response.py b/sublime/Packages/Package Control/package_control/http/debuggable_http_response.py new file mode 100644 index 0000000..2dd3af6 --- /dev/null +++ b/sublime/Packages/Package Control/package_control/http/debuggable_http_response.py @@ -0,0 +1,66 @@ +try: + # Python 3 + from http.client import HTTPResponse, IncompleteRead +except (ImportError): + # Python 2 + from httplib import HTTPResponse, IncompleteRead + +from ..console_write import console_write + + +class DebuggableHTTPResponse(HTTPResponse): + """ + A custom HTTPResponse that formats debugging info for Sublime Text + """ + + _debug_protocol = 'HTTP' + + def __init__(self, sock, debuglevel=0, method=None, **kwargs): + # We have to use a positive debuglevel to get it passed to here, + # however we don't want to use it because by default debugging prints + # to the stdout and we can't capture it, so we use a special -1 value + if debuglevel == 5: + debuglevel = -1 + HTTPResponse.__init__(self, sock, debuglevel=debuglevel, method=method) + + def begin(self): + return_value = HTTPResponse.begin(self) + if self.debuglevel == -1: + console_write(u'Urllib %s Debug Read' % self._debug_protocol, True) + + # Python 2 + if hasattr(self.msg, 'headers'): + headers = self.msg.headers + # Python 3 + else: + headers = [] + for header in self.msg: + headers.append("%s: %s" % (header, self.msg[header])) + + versions = { + 9: 'HTTP/0.9', + 10: 'HTTP/1.0', + 11: 'HTTP/1.1' + } + status_line = versions[self.version] + ' ' + str(self.status) + ' ' + self.reason + headers.insert(0, status_line) + for line in headers: + console_write(u" %s" % line.rstrip()) + return return_value + + def is_keep_alive(self): + # Python 2 + if hasattr(self.msg, 'headers'): + connection = self.msg.getheader('connection') + # Python 3 + else: + connection = self.msg['connection'] + if connection and connection.lower() == 'keep-alive': + return True + return False + + def read(self, *args): + try: + return HTTPResponse.read(self, *args) + except (IncompleteRead) as e: + return e.partial diff --git a/sublime/Packages/Package Control/package_control/http/debuggable_https_response.py b/sublime/Packages/Package Control/package_control/http/debuggable_https_response.py new file mode 100644 index 0000000..edc9fb0 --- /dev/null +++ b/sublime/Packages/Package Control/package_control/http/debuggable_https_response.py @@ -0,0 +1,9 @@ +from .debuggable_http_response import DebuggableHTTPResponse + + +class DebuggableHTTPSResponse(DebuggableHTTPResponse): + """ + A version of DebuggableHTTPResponse that sets the debug protocol to HTTPS + """ + + _debug_protocol = 'HTTPS' diff --git a/sublime/Packages/Package Control/package_control/http/invalid_certificate_exception.py b/sublime/Packages/Package Control/package_control/http/invalid_certificate_exception.py new file mode 100644 index 0000000..2715707 --- /dev/null +++ b/sublime/Packages/Package Control/package_control/http/invalid_certificate_exception.py @@ -0,0 +1,25 @@ +try: + # Python 3 + from http.client import HTTPException + from urllib.error import URLError +except (ImportError): + # Python 2 + from httplib import HTTPException + from urllib2 import URLError + + +class InvalidCertificateException(HTTPException, URLError): + """ + An exception for when an SSL certification is not valid for the URL + it was presented for. + """ + + def __init__(self, host, cert, reason): + HTTPException.__init__(self) + self.host = host + self.cert = cert + self.reason = reason + + def __str__(self): + return ('Host %s returned an invalid certificate (%s) %s\n' % + (self.host, self.reason, self.cert)) diff --git a/sublime/Packages/Package Control/package_control/http/persistent_handler.py b/sublime/Packages/Package Control/package_control/http/persistent_handler.py new file mode 100644 index 0000000..4bfd3d7 --- /dev/null +++ b/sublime/Packages/Package Control/package_control/http/persistent_handler.py @@ -0,0 +1,116 @@ +import sys +import socket + +try: + # Python 3 + from urllib.error import URLError +except ImportError: + # Python 2 + from urllib2 import URLError + from urllib import addinfourl + +from ..console_write import console_write + + +class PersistentHandler: + connection = None + use_count = 0 + + def close(self): + if self.connection: + if self._debuglevel == 5: + s = '' if self.use_count == 1 else 's' + console_write(u"Urllib %s Debug General" % self.connection._debug_protocol, True) + console_write(u" Closing connection to %s on port %s after %s request%s" % ( + self.connection.host, self.connection.port, self.use_count, s)) + self.connection.close() + self.connection = None + self.use_count = 0 + + def do_open(self, http_class, req): + # Large portions from Python 3.3 Lib/urllib/request.py and + # Python 2.6 Lib/urllib2.py + + if sys.version_info >= (3,): + host = req.host + else: + host = req.get_host() + + if not host: + raise URLError('no host given') + + if self.connection and self.connection.host != host: + self.close() + + # Re-use the connection if possible + self.use_count += 1 + if not self.connection: + h = http_class(host, timeout=req.timeout) + else: + h = self.connection + if self._debuglevel == 5: + console_write(u"Urllib %s Debug General" % h._debug_protocol, True) + console_write(u" Re-using connection to %s on port %s for request #%s" % ( + h.host, h.port, self.use_count)) + + if sys.version_info >= (3,): + headers = dict(req.unredirected_hdrs) + headers.update(dict((k, v) for k, v in req.headers.items() + if k not in headers)) + headers = dict((name.title(), val) for name, val in headers.items()) + + else: + h.set_debuglevel(self._debuglevel) + + headers = dict(req.headers) + headers.update(req.unredirected_hdrs) + headers = dict( + (name.title(), val) for name, val in headers.items()) + + if req._tunnel_host and not self.connection: + tunnel_headers = {} + proxy_auth_hdr = "Proxy-Authorization" + if proxy_auth_hdr in headers: + tunnel_headers[proxy_auth_hdr] = headers[proxy_auth_hdr] + del headers[proxy_auth_hdr] + + if sys.version_info >= (3,): + h.set_tunnel(req._tunnel_host, headers=tunnel_headers) + else: + h._set_tunnel(req._tunnel_host, headers=tunnel_headers) + + try: + if sys.version_info >= (3,): + h.request(req.get_method(), req.selector, req.data, headers) + else: + h.request(req.get_method(), req.get_selector(), req.data, headers) + except socket.error as err: # timeout error + h.close() + raise URLError(err) + else: + r = h.getresponse() + + # Keep the connection around for re-use + if r.is_keep_alive(): + self.connection = h + else: + if self._debuglevel == 5: + s = '' if self.use_count == 1 else 's' + console_write(u"Urllib %s Debug General" % h._debug_protocol, True) + console_write(u" Closing connection to %s on port %s after %s request%s" % ( + h.host, h.port, self.use_count, s)) + self.use_count = 0 + self.connection = None + + if sys.version_info >= (3,): + r.url = req.get_full_url() + r.msg = r.reason + return r + + r.recv = r.read + fp = socket._fileobject(r, close=True) + + resp = addinfourl(fp, r.msg, req.get_full_url()) + resp.code = r.status + resp.msg = r.reason + return resp diff --git a/sublime/Packages/Package Control/package_control/http/validating_https_connection.py b/sublime/Packages/Package Control/package_control/http/validating_https_connection.py new file mode 100644 index 0000000..a01afdb --- /dev/null +++ b/sublime/Packages/Package Control/package_control/http/validating_https_connection.py @@ -0,0 +1,345 @@ +import re +import socket +import base64 +import hashlib +import os +import sys + +try: + # Python 3 + from http.client import HTTPS_PORT + from urllib.request import parse_keqv_list, parse_http_list +except (ImportError): + # Python 2 + from httplib import HTTPS_PORT + from urllib2 import parse_keqv_list, parse_http_list + +from ..console_write import console_write +from .debuggable_https_response import DebuggableHTTPSResponse +from .debuggable_http_connection import DebuggableHTTPConnection +from .invalid_certificate_exception import InvalidCertificateException + + +# The following code is wrapped in a try because the Linux versions of Sublime +# Text do not include the ssl module due to the fact that different distros +# have different versions +try: + import ssl + + class ValidatingHTTPSConnection(DebuggableHTTPConnection): + """ + A custom HTTPConnection class that validates SSL certificates, and + allows proxy authentication for HTTPS connections. + """ + + default_port = HTTPS_PORT + + response_class = DebuggableHTTPSResponse + _debug_protocol = 'HTTPS' + + def __init__(self, host, port=None, key_file=None, cert_file=None, + ca_certs=None, **kwargs): + passed_args = {} + if 'timeout' in kwargs: + passed_args['timeout'] = kwargs['timeout'] + if 'debug' in kwargs: + passed_args['debug'] = kwargs['debug'] + DebuggableHTTPConnection.__init__(self, host, port, **passed_args) + + self.passwd = kwargs.get('passwd') + self.key_file = key_file + self.cert_file = cert_file + self.ca_certs = ca_certs + if 'user_agent' in kwargs: + self.user_agent = kwargs['user_agent'] + if self.ca_certs: + self.cert_reqs = ssl.CERT_REQUIRED + else: + self.cert_reqs = ssl.CERT_NONE + + def get_valid_hosts_for_cert(self, cert): + """ + Returns a list of valid hostnames for an SSL certificate + + :param cert: A dict from SSLSocket.getpeercert() + + :return: An array of hostnames + """ + + if 'subjectAltName' in cert: + return [x[1] for x in cert['subjectAltName'] + if x[0].lower() == 'dns'] + else: + return [x[0][1] for x in cert['subject'] + if x[0][0].lower() == 'commonname'] + + def validate_cert_host(self, cert, hostname): + """ + Checks if the cert is valid for the hostname + + :param cert: A dict from SSLSocket.getpeercert() + + :param hostname: A string hostname to check + + :return: A boolean if the cert is valid for the hostname + """ + + hosts = self.get_valid_hosts_for_cert(cert) + for host in hosts: + host_re = host.replace('.', '\.').replace('*', '[^.]*') + if re.search('^%s$' % (host_re,), hostname, re.I): + return True + return False + + def _tunnel(self): + """ + This custom _tunnel method allows us to read and print the debug + log for the whole response before throwing an error, and adds + support for proxy authentication + """ + + self._proxy_host = self.host + self._proxy_port = self.port + self._set_hostport(self._tunnel_host, self._tunnel_port) + + self._tunnel_headers['Host'] = u"%s:%s" % (self.host, self.port) + self._tunnel_headers['User-Agent'] = self.user_agent + self._tunnel_headers['Proxy-Connection'] = 'Keep-Alive' + + request = "CONNECT %s:%d HTTP/1.1\r\n" % (self.host, self.port) + for header, value in self._tunnel_headers.items(): + request += "%s: %s\r\n" % (header, value) + request += "\r\n" + + if sys.version_info >= (3,): + request = bytes(request, 'iso-8859-1') + + self.send(request) + + response = self.response_class(self.sock, method=self._method) + (version, code, message) = response._read_status() + + status_line = u"%s %s %s" % (version, code, message.rstrip()) + headers = [status_line] + + if self.debuglevel in [-1, 5]: + console_write(u'Urllib %s Debug Read' % self._debug_protocol, True) + console_write(u" %s" % status_line) + + content_length = 0 + close_connection = False + while True: + line = response.fp.readline() + + if sys.version_info >= (3,): + line = str(line, encoding='iso-8859-1') + + if line == '\r\n': + break + + headers.append(line.rstrip()) + + parts = line.rstrip().split(': ', 1) + name = parts[0].lower() + value = parts[1].lower().strip() + if name == 'content-length': + content_length = int(value) + + if name in ['connection', 'proxy-connection'] and value == 'close': + close_connection = True + + if self.debuglevel in [-1, 5]: + console_write(u" %s" % line.rstrip()) + + # Handle proxy auth for SSL connections since regular urllib punts on this + if code == 407 and self.passwd and 'Proxy-Authorization' not in self._tunnel_headers: + if content_length: + response._safe_read(content_length) + + supported_auth_methods = {} + for line in headers: + parts = line.split(': ', 1) + if parts[0].lower() != 'proxy-authenticate': + continue + details = parts[1].split(' ', 1) + supported_auth_methods[details[0].lower()] = details[1] if len(details) > 1 else '' + + username, password = self.passwd.find_user_password(None, "%s:%s" % ( + self._proxy_host, self._proxy_port)) + + if 'digest' in supported_auth_methods: + response_value = self.build_digest_response( + supported_auth_methods['digest'], username, password) + if response_value: + self._tunnel_headers['Proxy-Authorization'] = u"Digest %s" % response_value + + elif 'basic' in supported_auth_methods: + response_value = u"%s:%s" % (username, password) + response_value = base64.b64encode(response_value).strip() + self._tunnel_headers['Proxy-Authorization'] = u"Basic %s" % response_value + + if 'Proxy-Authorization' in self._tunnel_headers: + self.host = self._proxy_host + self.port = self._proxy_port + + # If the proxy wanted the connection closed, we need to make a new connection + if close_connection: + self.sock.close() + self.sock = socket.create_connection((self.host, self.port), self.timeout) + + return self._tunnel() + + if code != 200: + self.close() + raise socket.error("Tunnel connection failed: %d %s" % (code, + message.strip())) + + def build_digest_response(self, fields, username, password): + """ + Takes a Proxy-Authenticate: Digest header and creates a response + header + + :param fields: + The string portion of the Proxy-Authenticate header after + "Digest " + + :param username: + The username to use for the response + + :param password: + The password to use for the response + + :return: + None if invalid Proxy-Authenticate header, otherwise the + string of fields for the Proxy-Authorization: Digest header + """ + + fields = parse_keqv_list(parse_http_list(fields)) + + realm = fields.get('realm') + nonce = fields.get('nonce') + qop = fields.get('qop') + algorithm = fields.get('algorithm') + if algorithm: + algorithm = algorithm.lower() + opaque = fields.get('opaque') + + if algorithm in ['md5', None]: + def md5hash(string): + return hashlib.md5(string).hexdigest() + hash = md5hash + + elif algorithm == 'sha': + def sha1hash(string): + return hashlib.sha1(string).hexdigest() + hash = sha1hash + + else: + return None + + host_port = u"%s:%s" % (self.host, self.port) + + a1 = "%s:%s:%s" % (username, realm, password) + a2 = "CONNECT:%s" % host_port + ha1 = hash(a1) + ha2 = hash(a2) + + if qop == None: + response = hash(u"%s:%s:%s" % (ha1, nonce, ha2)) + elif qop == 'auth': + nc = '00000001' + cnonce = hash(os.urandom(8))[:8] + response = hash(u"%s:%s:%s:%s:%s:%s" % (ha1, nonce, nc, cnonce, qop, ha2)) + else: + return None + + response_fields = { + 'username': username, + 'realm': realm, + 'nonce': nonce, + 'response': response, + 'uri': host_port + } + if algorithm: + response_fields['algorithm'] = algorithm + if qop == 'auth': + response_fields['nc'] = nc + response_fields['cnonce'] = cnonce + response_fields['qop'] = qop + if opaque: + response_fields['opaque'] = opaque + + return ', '.join([u"%s=\"%s\"" % (field, response_fields[field]) for field in response_fields]) + + def connect(self): + """ + Adds debugging and SSL certification validation + """ + + if self.debuglevel == -1: + console_write(u"Urllib HTTPS Debug General", True) + console_write(u" Connecting to %s on port %s" % (self.host, self.port)) + + self.sock = socket.create_connection((self.host, self.port), self.timeout) + if self._tunnel_host: + self._tunnel() + + if self.debuglevel == -1: + console_write(u"Urllib HTTPS Debug General", True) + console_write(u" Connecting to %s on port %s" % (self.host, self.port)) + console_write(u" CA certs file at %s" % (self.ca_certs.decode(sys.getfilesystemencoding()))) + + self.sock = ssl.wrap_socket(self.sock, keyfile=self.key_file, + certfile=self.cert_file, cert_reqs=self.cert_reqs, + ca_certs=self.ca_certs) + + if self.debuglevel == -1: + console_write(u" Successfully upgraded connection to %s:%s with SSL" % ( + self.host, self.port)) + + # This debugs and validates the SSL certificate + if self.cert_reqs & ssl.CERT_REQUIRED: + cert = self.sock.getpeercert() + + if self.debuglevel == -1: + subjectMap = { + 'organizationName': 'O', + 'commonName': 'CN', + 'organizationalUnitName': 'OU', + 'countryName': 'C', + 'serialNumber': 'serialNumber', + 'commonName': 'CN', + 'localityName': 'L', + 'stateOrProvinceName': 'S' + } + subject_list = list(cert['subject']) + subject_list.reverse() + subject_parts = [] + for pair in subject_list: + if pair[0][0] in subjectMap: + field_name = subjectMap[pair[0][0]] + else: + field_name = pair[0][0] + subject_parts.append(field_name + '=' + pair[0][1]) + + console_write(u" Server SSL certificate:") + console_write(u" subject: " + ','.join(subject_parts)) + if 'subjectAltName' in cert: + console_write(u" common name: " + cert['subjectAltName'][0][1]) + if 'notAfter' in cert: + console_write(u" expire date: " + cert['notAfter']) + + hostname = self.host.split(':', 0)[0] + + if not self.validate_cert_host(cert, hostname): + if self.debuglevel == -1: + console_write(u" Certificate INVALID") + + raise InvalidCertificateException(hostname, cert, + 'hostname mismatch') + + if self.debuglevel == -1: + console_write(u" Certificate validated for %s" % hostname) + +except (ImportError): + pass diff --git a/sublime/Packages/Package Control/package_control/http/validating_https_handler.py b/sublime/Packages/Package Control/package_control/http/validating_https_handler.py new file mode 100644 index 0000000..5b02c7a --- /dev/null +++ b/sublime/Packages/Package Control/package_control/http/validating_https_handler.py @@ -0,0 +1,59 @@ +try: + # Python 3 + from urllib.error import URLError + import urllib.request as urllib_compat +except (ImportError): + # Python 2 + from urllib2 import URLError + import urllib2 as urllib_compat + + +# The following code is wrapped in a try because the Linux versions of Sublime +# Text do not include the ssl module due to the fact that different distros +# have different versions +try: + import ssl + + from .validating_https_connection import ValidatingHTTPSConnection + from .invalid_certificate_exception import InvalidCertificateException + from .persistent_handler import PersistentHandler + + if hasattr(urllib_compat, 'HTTPSHandler'): + class ValidatingHTTPSHandler(PersistentHandler, urllib_compat.HTTPSHandler): + """ + A urllib handler that validates SSL certificates for HTTPS requests + """ + + def __init__(self, **kwargs): + # This is a special value that will not trigger the standard debug + # functionality, but custom code where we can format the output + self._debuglevel = 0 + if 'debug' in kwargs and kwargs['debug']: + self._debuglevel = 5 + elif 'debuglevel' in kwargs: + self._debuglevel = kwargs['debuglevel'] + self._connection_args = kwargs + + def https_open(self, req): + def http_class_wrapper(host, **kwargs): + full_kwargs = dict(self._connection_args) + full_kwargs.update(kwargs) + return ValidatingHTTPSConnection(host, **full_kwargs) + + try: + return self.do_open(http_class_wrapper, req) + except URLError as e: + if type(e.reason) == ssl.SSLError and e.reason.args[0] == 1: + raise InvalidCertificateException(req.host, '', + e.reason.args[1]) + raise + + https_request = urllib_compat.AbstractHTTPHandler.do_request_ + else: + raise ImportError() + +except (ImportError) as e: + + class ValidatingHTTPSHandler(): + def __init__(self, **kwargs): + raise e diff --git a/sublime/Packages/Package Control/package_control/http_cache.py b/sublime/Packages/Package Control/package_control/http_cache.py new file mode 100644 index 0000000..2f6f3a2 --- /dev/null +++ b/sublime/Packages/Package Control/package_control/http_cache.py @@ -0,0 +1,75 @@ +import os +import time + +import sublime + +from .open_compat import open_compat, read_compat + + +class HttpCache(object): + """ + A data store for caching HTTP response data. + """ + + def __init__(self, ttl): + self.base_path = os.path.join(sublime.packages_path(), 'User', 'Package Control.cache') + if not os.path.exists(self.base_path): + os.mkdir(self.base_path) + self.clear(int(ttl)) + + def clear(self, ttl): + """ + Removes all cache entries older than the TTL + + :param ttl: + The number of seconds a cache entry should be valid for + """ + + ttl = int(ttl) + + for filename in os.listdir(self.base_path): + path = os.path.join(self.base_path, filename) + # There should not be any folders in the cache dir, but we + # ignore to prevent an exception + if os.path.isdir(path): + continue + mtime = os.stat(path).st_mtime + if mtime < time.time() - ttl: + os.unlink(path) + + def get(self, key): + """ + Returns a cached value + + :param key: + The key to fetch the cache for + + :return: + The (binary) cached value, or False + """ + + cache_file = os.path.join(self.base_path, key) + if not os.path.exists(cache_file): + return False + + with open_compat(cache_file, 'rb') as f: + return read_compat(f) + + def has(self, key): + cache_file = os.path.join(self.base_path, key) + return os.path.exists(cache_file) + + def set(self, key, content): + """ + Saves a value in the cache + + :param key: + The key to save the cache with + + :param content: + The (binary) content to cache + """ + + cache_file = os.path.join(self.base_path, key) + with open_compat(cache_file, 'wb') as f: + f.write(content) diff --git a/sublime/Packages/Package Control/package_control/open_compat.py b/sublime/Packages/Package Control/package_control/open_compat.py new file mode 100644 index 0000000..b22f066 --- /dev/null +++ b/sublime/Packages/Package Control/package_control/open_compat.py @@ -0,0 +1,27 @@ +import os +import sys + +from .file_not_found_error import FileNotFoundError + + +def open_compat(path, mode='r'): + if mode in ['r', 'rb'] and not os.path.exists(path): + raise FileNotFoundError(u"The file \"%s\" could not be found" % path) + + if sys.version_info >= (3,): + encoding = 'utf-8' + errors = 'replace' + if mode in ['rb', 'wb', 'ab']: + encoding = None + errors = None + return open(path, mode, encoding=encoding, errors=errors) + + else: + return open(path, mode) + + +def read_compat(file_obj): + if sys.version_info >= (3,): + return file_obj.read() + else: + return unicode(file_obj.read(), 'utf-8', errors='replace') diff --git a/sublime/Packages/Package Control/package_control/package_cleanup.py b/sublime/Packages/Package Control/package_control/package_cleanup.py new file mode 100644 index 0000000..352f4d4 --- /dev/null +++ b/sublime/Packages/Package Control/package_control/package_cleanup.py @@ -0,0 +1,107 @@ +import threading +import os +import shutil + +import sublime + +from .show_error import show_error +from .console_write import console_write +from .unicode import unicode_from_os +from .clear_directory import clear_directory +from .automatic_upgrader import AutomaticUpgrader +from .package_manager import PackageManager +from .package_renamer import PackageRenamer +from .open_compat import open_compat +from .package_io import package_file_exists + + +class PackageCleanup(threading.Thread, PackageRenamer): + """ + Cleans up folders for packages that were removed, but that still have files + in use. + """ + + def __init__(self): + self.manager = PackageManager() + self.load_settings() + threading.Thread.__init__(self) + + def run(self): + found_pkgs = [] + installed_pkgs = list(self.installed_packages) + for package_name in os.listdir(sublime.packages_path()): + package_dir = os.path.join(sublime.packages_path(), package_name) + + # Cleanup packages that could not be removed due to in-use files + cleanup_file = os.path.join(package_dir, 'package-control.cleanup') + if os.path.exists(cleanup_file): + try: + shutil.rmtree(package_dir) + console_write(u'Removed old directory for package %s' % package_name, True) + + except (OSError) as e: + if not os.path.exists(cleanup_file): + open_compat(cleanup_file, 'w').close() + + error_string = (u'Unable to remove old directory for package ' + + u'%s - deferring until next start: %s') % ( + package_name, unicode_from_os(e)) + console_write(error_string, True) + + # Finish reinstalling packages that could not be upgraded due to + # in-use files + reinstall = os.path.join(package_dir, 'package-control.reinstall') + if os.path.exists(reinstall): + metadata_path = os.path.join(package_dir, 'package-metadata.json') + if not clear_directory(package_dir, [metadata_path]): + if not os.path.exists(reinstall): + open_compat(reinstall, 'w').close() + # Assigning this here prevents the callback from referencing the value + # of the "package_name" variable when it is executed + restart_message = (u'An error occurred while trying to ' + + u'finish the upgrade of %s. You will most likely need to ' + + u'restart your computer to complete the upgrade.') % package_name + + def show_still_locked(): + show_error(restart_message) + sublime.set_timeout(show_still_locked, 10) + else: + self.manager.install_package(package_name) + + # This adds previously installed packages from old versions of PC + if package_file_exists(package_name, 'package-metadata.json') and \ + package_name not in self.installed_packages: + installed_pkgs.append(package_name) + params = { + 'package': package_name, + 'operation': 'install', + 'version': \ + self.manager.get_metadata(package_name).get('version') + } + self.manager.record_usage(params) + + found_pkgs.append(package_name) + + if int(sublime.version()) >= 3000: + package_files = os.listdir(sublime.installed_packages_path()) + found_pkgs += [file.replace('.sublime-package', '') for file in package_files] + + sublime.set_timeout(lambda: self.finish(installed_pkgs, found_pkgs), 10) + + def finish(self, installed_pkgs, found_pkgs): + """ + A callback that can be run the main UI thread to perform saving of the + Package Control.sublime-settings file. Also fires off the + :class:`AutomaticUpgrader`. + + :param installed_pkgs: + A list of the string package names of all "installed" packages, + even ones that do not appear to be in the filesystem. + + :param found_pkgs: + A list of the string package names of all packages that are + currently installed on the filesystem. + """ + + self.save_packages(installed_pkgs) + AutomaticUpgrader(found_pkgs).start() diff --git a/sublime/Packages/Package Control/package_control/package_creator.py b/sublime/Packages/Package Control/package_control/package_creator.py new file mode 100644 index 0000000..47a3087 --- /dev/null +++ b/sublime/Packages/Package Control/package_control/package_creator.py @@ -0,0 +1,39 @@ +import os + +from .show_error import show_error +from .package_manager import PackageManager + + +class PackageCreator(): + """ + Abstract class for commands that create .sublime-package files + """ + + def show_panel(self): + """ + Shows a list of packages that can be turned into a .sublime-package file + """ + + self.manager = PackageManager() + self.packages = self.manager.list_packages(unpacked_only=True) + if not self.packages: + show_error('There are no packages available to be packaged') + return + self.window.show_quick_panel(self.packages, self.on_done) + + def get_package_destination(self): + """ + Retrieves the destination for .sublime-package files + + :return: + A string - the path to the folder to save .sublime-package files in + """ + + destination = self.manager.settings.get('package_destination') + + # We check destination via an if statement instead of using + # the dict.get() method since the key may be set, but to a blank value + if not destination: + destination = os.path.join(os.path.expanduser('~'), 'Desktop') + + return destination diff --git a/sublime/Packages/Package Control/package_control/package_installer.py b/sublime/Packages/Package Control/package_control/package_installer.py new file mode 100644 index 0000000..9c8809c --- /dev/null +++ b/sublime/Packages/Package Control/package_control/package_installer.py @@ -0,0 +1,247 @@ +import os +import re +import threading + +import sublime + +from .preferences_filename import preferences_filename +from .thread_progress import ThreadProgress +from .package_manager import PackageManager +from .upgraders.git_upgrader import GitUpgrader +from .upgraders.hg_upgrader import HgUpgrader +from .versions import version_comparable + + +class PackageInstaller(): + """ + Provides helper functionality related to installing packages + """ + + def __init__(self): + self.manager = PackageManager() + + def make_package_list(self, ignore_actions=[], override_action=None, + ignore_packages=[]): + """ + Creates a list of packages and what operation would be performed for + each. Allows filtering by the applicable action or package name. + Returns the information in a format suitable for displaying in the + quick panel. + + :param ignore_actions: + A list of actions to ignore packages by. Valid actions include: + `install`, `upgrade`, `downgrade`, `reinstall`, `overwrite`, + `pull` and `none`. `pull` andd `none` are for Git and Hg + repositories. `pull` is present when incoming changes are detected, + where as `none` is selected if no commits are available. `overwrite` + is for packages that do not include version information via the + `package-metadata.json` file. + + :param override_action: + A string action name to override the displayed action for all listed + packages. + + :param ignore_packages: + A list of packages names that should not be returned in the list + + :return: + A list of lists, each containing three strings: + 0 - package name + 1 - package description + 2 - action; [extra info;] package url + """ + + packages = self.manager.list_available_packages() + installed_packages = self.manager.list_packages() + + package_list = [] + for package in sorted(iter(packages.keys()), key=lambda s: s.lower()): + if ignore_packages and package in ignore_packages: + continue + package_entry = [package] + info = packages[package] + download = info['download'] + + if package in installed_packages: + installed = True + metadata = self.manager.get_metadata(package) + if metadata.get('version'): + installed_version = metadata['version'] + else: + installed_version = None + else: + installed = False + + installed_version_name = 'v' + installed_version if \ + installed and installed_version else 'unknown version' + new_version = 'v' + download['version'] + + vcs = None + package_dir = self.manager.get_package_dir(package) + settings = self.manager.settings + + if override_action: + action = override_action + extra = '' + + else: + if os.path.exists(os.path.join(package_dir, '.git')): + if settings.get('ignore_vcs_packages'): + continue + vcs = 'git' + incoming = GitUpgrader(settings.get('git_binary'), + settings.get('git_update_command'), package_dir, + settings.get('cache_length'), settings.get('debug') + ).incoming() + elif os.path.exists(os.path.join(package_dir, '.hg')): + if settings.get('ignore_vcs_packages'): + continue + vcs = 'hg' + incoming = HgUpgrader(settings.get('hg_binary'), + settings.get('hg_update_command'), package_dir, + settings.get('cache_length'), settings.get('debug') + ).incoming() + + if installed: + if vcs: + if incoming: + action = 'pull' + extra = ' with ' + vcs + else: + action = 'none' + extra = '' + elif not installed_version: + action = 'overwrite' + extra = ' %s with %s' % (installed_version_name, + new_version) + else: + installed_version = version_comparable(installed_version) + download_version = version_comparable(download['version']) + if download_version > installed_version: + action = 'upgrade' + extra = ' to %s from %s' % (new_version, + installed_version_name) + elif download_version < installed_version: + action = 'downgrade' + extra = ' to %s from %s' % (new_version, + installed_version_name) + else: + action = 'reinstall' + extra = ' %s' % new_version + else: + action = 'install' + extra = ' %s' % new_version + extra += ';' + + if action in ignore_actions: + continue + + description = info.get('description') + if not description: + description = 'No description provided' + package_entry.append(description) + package_entry.append(action + extra + ' ' + + re.sub('^https?://', '', info['homepage'])) + package_list.append(package_entry) + return package_list + + def disable_packages(self, packages): + """ + Disables one or more packages before installing or upgrading to prevent + errors where Sublime Text tries to read files that no longer exist, or + read a half-written file. + + :param packages: The string package name, or an array of strings + """ + + if not isinstance(packages, list): + packages = [packages] + + # Don't disable Package Control so it does not get stuck disabled + if 'Package Control' in packages: + packages.remove('Package Control') + + disabled = [] + + settings = sublime.load_settings(preferences_filename()) + ignored = settings.get('ignored_packages') + if not ignored: + ignored = [] + for package in packages: + if not package in ignored: + ignored.append(package) + disabled.append(package) + settings.set('ignored_packages', ignored) + sublime.save_settings(preferences_filename()) + return disabled + + def reenable_package(self, package): + """ + Re-enables a package after it has been installed or upgraded + + :param package: The string package name + """ + + settings = sublime.load_settings(preferences_filename()) + ignored = settings.get('ignored_packages') + if not ignored: + return + if package in ignored: + settings.set('ignored_packages', + list(set(ignored) - set([package]))) + sublime.save_settings(preferences_filename()) + + def on_done(self, picked): + """ + Quick panel user selection handler - disables a package, installs or + upgrades it, then re-enables the package + + :param picked: + An integer of the 0-based package name index from the presented + list. -1 means the user cancelled. + """ + + if picked == -1: + return + name = self.package_list[picked][0] + + if name in self.disable_packages(name): + on_complete = lambda: self.reenable_package(name) + else: + on_complete = None + + thread = PackageInstallerThread(self.manager, name, on_complete) + thread.start() + ThreadProgress(thread, 'Installing package %s' % name, + 'Package %s successfully %s' % (name, self.completion_type)) + + +class PackageInstallerThread(threading.Thread): + """ + A thread to run package install/upgrade operations in so that the main + Sublime Text thread does not get blocked and freeze the UI + """ + + def __init__(self, manager, package, on_complete): + """ + :param manager: + An instance of :class:`PackageManager` + + :param package: + The string package name to install/upgrade + + :param on_complete: + A callback to run after installing/upgrading the package + """ + + self.package = package + self.manager = manager + self.on_complete = on_complete + threading.Thread.__init__(self) + + def run(self): + try: + self.result = self.manager.install_package(self.package) + finally: + if self.on_complete: + sublime.set_timeout(self.on_complete, 1) diff --git a/sublime/Packages/Package Control/package_control/package_io.py b/sublime/Packages/Package Control/package_control/package_io.py new file mode 100644 index 0000000..14ab134 --- /dev/null +++ b/sublime/Packages/Package Control/package_control/package_io.py @@ -0,0 +1,126 @@ +import os +import zipfile + +import sublime + +from .console_write import console_write +from .open_compat import open_compat, read_compat +from .unicode import unicode_from_os +from .file_not_found_error import FileNotFoundError + + +def read_package_file(package, relative_path, binary=False, debug=False): + package_dir = _get_package_dir(package) + file_path = os.path.join(package_dir, relative_path) + + if os.path.exists(package_dir): + result = _read_regular_file(package, relative_path, binary, debug) + if result != False: + return result + + if int(sublime.version()) >= 3000: + result = _read_zip_file(package, relative_path, binary, debug) + if result != False: + return result + + if debug: + console_write(u"Unable to find file %s in the package %s" % (relative_path, package), True) + return False + + +def package_file_exists(package, relative_path): + package_dir = _get_package_dir(package) + file_path = os.path.join(package_dir, relative_path) + + if os.path.exists(package_dir): + result = _regular_file_exists(package, relative_path) + if result: + return result + + if int(sublime.version()) >= 3000: + return _zip_file_exists(package, relative_path) + + return False + + +def _get_package_dir(package): + """:return: The full filesystem path to the package directory""" + + return os.path.join(sublime.packages_path(), package) + + +def _read_regular_file(package, relative_path, binary=False, debug=False): + package_dir = _get_package_dir(package) + file_path = os.path.join(package_dir, relative_path) + try: + with open_compat(file_path, ('rb' if binary else 'r')) as f: + return read_compat(f) + + except (FileNotFoundError) as e: + if debug: + console_write(u"Unable to find file %s in the package folder for %s" % (relative_path, package), True) + return False + + +def _read_zip_file(package, relative_path, binary=False, debug=False): + zip_path = os.path.join(sublime.installed_packages_path(), + package + '.sublime-package') + + if not os.path.exists(zip_path): + if debug: + console_write(u"Unable to find a sublime-package file for %s" % package, True) + return False + + try: + package_zip = zipfile.ZipFile(zip_path, 'r') + + except (zipfile.BadZipfile): + console_write(u'An error occurred while trying to unzip the sublime-package file for %s.' % package, True) + return False + + try: + contents = package_zip.read(relative_path) + if not binary: + contents = contents.decode('utf-8') + return contents + + except (KeyError) as e: + if debug: + console_write(u"Unable to find file %s in the sublime-package file for %s" % (relative_path, package), True) + + except (IOError) as e: + message = unicode_from_os(e) + console_write(u'Unable to read file from sublime-package file for %s due to an invalid filename' % package, True) + + except (UnicodeDecodeError): + console_write(u'Unable to read file from sublime-package file for %s due to an invalid filename or character encoding issue' % package, True) + + return False + + +def _regular_file_exists(package, relative_path): + package_dir = _get_package_dir(package) + file_path = os.path.join(package_dir, relative_path) + return os.path.exists(file_path) + + +def _zip_file_exists(package, relative_path): + zip_path = os.path.join(sublime.installed_packages_path(), + package + '.sublime-package') + + if not os.path.exists(zip_path): + return False + + try: + package_zip = zipfile.ZipFile(zip_path, 'r') + + except (zipfile.BadZipfile): + console_write(u'An error occurred while trying to unzip the sublime-package file for %s.' % package_name, True) + return False + + try: + package_zip.getinfo(relative_path) + return True + + except (KeyError) as e: + return False diff --git a/sublime/Packages/Package Control/package_control/package_manager.py b/sublime/Packages/Package Control/package_control/package_manager.py new file mode 100644 index 0000000..c013254 --- /dev/null +++ b/sublime/Packages/Package Control/package_control/package_manager.py @@ -0,0 +1,1026 @@ +import sys +import os +import re +import socket +import json +import time +import zipfile +import shutil +from fnmatch import fnmatch +import datetime +import tempfile +import locale + +try: + # Python 3 + from urllib.parse import urlencode, urlparse + import compileall + str_cls = str +except (ImportError): + # Python 2 + from urllib import urlencode + from urlparse import urlparse + str_cls = unicode + +import sublime + +from .show_error import show_error +from .console_write import console_write +from .open_compat import open_compat, read_compat +from .unicode import unicode_from_os +from .clear_directory import clear_directory +from .cache import (clear_cache, set_cache, get_cache, merge_cache_under_settings, + merge_cache_over_settings, set_cache_under_settings, set_cache_over_settings) +from .versions import version_comparable, version_sort +from .downloaders.background_downloader import BackgroundDownloader +from .downloaders.downloader_exception import DownloaderException +from .providers.provider_exception import ProviderException +from .clients.client_exception import ClientException +from .download_manager import downloader +from .providers.channel_provider import ChannelProvider +from .upgraders.git_upgrader import GitUpgrader +from .upgraders.hg_upgrader import HgUpgrader +from .package_io import read_package_file +from .providers import CHANNEL_PROVIDERS, REPOSITORY_PROVIDERS +from . import __version__ + + +class PackageManager(): + """ + Allows downloading, creating, installing, upgrading, and deleting packages + + Delegates metadata retrieval to the CHANNEL_PROVIDERS classes. + Uses VcsUpgrader-based classes for handling git and hg repositories in the + Packages folder. Downloader classes are utilized to fetch contents of URLs. + + Also handles displaying package messaging, and sending usage information to + the usage server. + """ + + def __init__(self): + # Here we manually copy the settings since sublime doesn't like + # code accessing settings from threads + self.settings = {} + settings = sublime.load_settings('Package Control.sublime-settings') + for setting in ['timeout', 'repositories', 'channels', + 'package_name_map', 'dirs_to_ignore', 'files_to_ignore', + 'package_destination', 'cache_length', 'auto_upgrade', + 'files_to_ignore_binary', 'files_to_keep', 'dirs_to_keep', + 'git_binary', 'git_update_command', 'hg_binary', + 'hg_update_command', 'http_proxy', 'https_proxy', + 'auto_upgrade_ignore', 'auto_upgrade_frequency', + 'submit_usage', 'submit_url', 'renamed_packages', + 'files_to_include', 'files_to_include_binary', 'certs', + 'ignore_vcs_packages', 'proxy_username', 'proxy_password', + 'debug', 'user_agent', 'http_cache', 'http_cache_length', + 'install_prereleases', 'openssl_binary']: + if settings.get(setting) == None: + continue + self.settings[setting] = settings.get(setting) + + # https_proxy will inherit from http_proxy unless it is set to a + # string value or false + no_https_proxy = self.settings.get('https_proxy') in ["", None] + if no_https_proxy and self.settings.get('http_proxy'): + self.settings['https_proxy'] = self.settings.get('http_proxy') + if self.settings.get('https_proxy') == False: + self.settings['https_proxy'] = '' + + self.settings['platform'] = sublime.platform() + self.settings['version'] = sublime.version() + + # Use the cache to see if settings have changed since the last + # time the package manager was created, and clearing any cached + # values if they have. + previous_settings = get_cache('filtered_settings', {}) + + # Reduce the settings down to exclude channel info since that will + # make the settings always different + filtered_settings = self.settings.copy() + for key in ['repositories', 'channels', 'package_name_map', 'cache']: + if key in filtered_settings: + del filtered_settings[key] + + if filtered_settings != previous_settings and previous_settings != {}: + console_write(u'Settings change detected, clearing cache', True) + clear_cache() + set_cache('filtered_settings', filtered_settings) + + def get_metadata(self, package): + """ + Returns the package metadata for an installed package + + :return: + A dict with the keys: + version + url + description + or an empty dict on error + """ + + try: + debug = self.settings.get('debug') + metadata_json = read_package_file(package, 'package-metadata.json', debug=debug) + if metadata_json: + return json.loads(metadata_json) + + except (IOError, ValueError) as e: + pass + + return {} + + def list_repositories(self): + """ + Returns a master list of all repositories pulled from all sources + + These repositories come from the channels specified in the + "channels" setting, plus any repositories listed in the + "repositories" setting. + + :return: + A list of all available repositories + """ + + cache_ttl = self.settings.get('cache_length') + + repositories = self.settings.get('repositories') + channels = self.settings.get('channels') + for channel in channels: + channel = channel.strip() + + # Caches various info from channels for performance + cache_key = channel + '.repositories' + channel_repositories = get_cache(cache_key) + + merge_cache_under_settings(self, 'package_name_map', channel) + merge_cache_under_settings(self, 'renamed_packages', channel) + merge_cache_under_settings(self, 'unavailable_packages', channel, list_=True) + + # If any of the info was not retrieved from the cache, we need to + # grab the channel to get it + if channel_repositories == None or \ + self.settings.get('package_name_map') == None or \ + self.settings.get('renamed_packages') == None: + + for provider_class in CHANNEL_PROVIDERS: + if provider_class.match_url(channel): + provider = provider_class(channel, self.settings) + break + + try: + channel_repositories = provider.get_repositories() + set_cache(cache_key, channel_repositories, cache_ttl) + + for repo in channel_repositories: + repo_packages = provider.get_packages(repo) + packages_cache_key = repo + '.packages' + set_cache(packages_cache_key, repo_packages, cache_ttl) + + # Have the local name map override the one from the channel + name_map = provider.get_name_map() + set_cache_under_settings(self, 'package_name_map', channel, name_map, cache_ttl) + + renamed_packages = provider.get_renamed_packages() + set_cache_under_settings(self, 'renamed_packages', channel, renamed_packages, cache_ttl) + + unavailable_packages = provider.get_unavailable_packages() + set_cache_under_settings(self, 'unavailable_packages', channel, unavailable_packages, cache_ttl, list_=True) + + provider_certs = provider.get_certs() + certs = self.settings.get('certs', {}).copy() + certs.update(provider_certs) + # Save the master list of certs, used by downloaders/cert_provider.py + set_cache('*.certs', certs, cache_ttl) + + except (DownloaderException, ClientException, ProviderException) as e: + console_write(e, True) + continue + + repositories.extend(channel_repositories) + return [repo.strip() for repo in repositories] + + def list_available_packages(self): + """ + Returns a master list of every available package from all sources + + :return: + A dict in the format: + { + 'Package Name': { + # Package details - see example-packages.json for format + }, + ... + } + """ + + if self.settings.get('debug'): + console_write(u"Fetching list of available packages", True) + console_write(u" Platform: %s-%s" % (sublime.platform(),sublime.arch())) + console_write(u" Sublime Text Version: %s" % sublime.version()) + console_write(u" Package Control Version: %s" % __version__) + + cache_ttl = self.settings.get('cache_length') + repositories = self.list_repositories() + packages = {} + bg_downloaders = {} + active = [] + repos_to_download = [] + name_map = self.settings.get('package_name_map', {}) + + # Repositories are run in reverse order so that the ones first + # on the list will overwrite those last on the list + for repo in repositories[::-1]: + cache_key = repo + '.packages' + repository_packages = get_cache(cache_key) + + if repository_packages != None: + packages.update(repository_packages) + + else: + domain = urlparse(repo).hostname + if domain not in bg_downloaders: + bg_downloaders[domain] = BackgroundDownloader( + self.settings, REPOSITORY_PROVIDERS) + bg_downloaders[domain].add_url(repo) + repos_to_download.append(repo) + + for bg_downloader in list(bg_downloaders.values()): + bg_downloader.start() + active.append(bg_downloader) + + # Wait for all of the downloaders to finish + while active: + bg_downloader = active.pop() + bg_downloader.join() + + # Grabs the results and stuff it all in the cache + for repo in repos_to_download: + domain = urlparse(repo).hostname + bg_downloader = bg_downloaders[domain] + provider = bg_downloader.get_provider(repo) + + # Allow name mapping of packages for schema version < 2.0 + repository_packages = {} + for name, info in provider.get_packages(): + name = name_map.get(name, name) + info['name'] = name + repository_packages[name] = info + + # Display errors we encountered while fetching package info + for url, exception in provider.get_failed_sources(): + console_write(exception, True) + for name, exception in provider.get_broken_packages(): + console_write(exception, True) + + cache_key = repo + '.packages' + set_cache(cache_key, repository_packages, cache_ttl) + packages.update(repository_packages) + + renamed_packages = provider.get_renamed_packages() + set_cache_under_settings(self, 'renamed_packages', repo, renamed_packages, cache_ttl) + + unavailable_packages = provider.get_unavailable_packages() + set_cache_under_settings(self, 'unavailable_packages', repo, unavailable_packages, cache_ttl, list_=True) + + return packages + + def list_packages(self, unpacked_only=False): + """ + :param unpacked_only: + Only list packages that are not inside of .sublime-package files + + :return: A list of all installed, non-default, package names + """ + + package_names = os.listdir(sublime.packages_path()) + package_names = [path for path in package_names if + os.path.isdir(os.path.join(sublime.packages_path(), path))] + + if int(sublime.version()) > 3000 and unpacked_only == False: + package_files = os.listdir(sublime.installed_packages_path()) + package_names += [f.replace('.sublime-package', '') for f in package_files if re.search('\.sublime-package$', f) != None] + + # Ignore things to be deleted + ignored = ['User'] + for package in package_names: + cleanup_file = os.path.join(sublime.packages_path(), package, + 'package-control.cleanup') + if os.path.exists(cleanup_file): + ignored.append(package) + + packages = list(set(package_names) - set(ignored) - + set(self.list_default_packages())) + packages = sorted(packages, key=lambda s: s.lower()) + + return packages + + def list_all_packages(self): + """ :return: A list of all installed package names, including default packages""" + + packages = self.list_default_packages() + self.list_packages() + packages = sorted(packages, key=lambda s: s.lower()) + return packages + + def list_default_packages(self): + """ :return: A list of all default package names""" + + if int(sublime.version()) > 3000: + bundled_packages_path = os.path.join(os.path.dirname(sublime.executable_path()), + 'Packages') + files = os.listdir(bundled_packages_path) + + else: + files = os.listdir(os.path.join(os.path.dirname( + sublime.packages_path()), 'Pristine Packages')) + files = list(set(files) - set(os.listdir( + sublime.installed_packages_path()))) + packages = [file.replace('.sublime-package', '') for file in files] + packages = sorted(packages, key=lambda s: s.lower()) + return packages + + def get_package_dir(self, package): + """:return: The full filesystem path to the package directory""" + + return os.path.join(sublime.packages_path(), package) + + def get_mapped_name(self, package): + """:return: The name of the package after passing through mapping rules""" + + return self.settings.get('package_name_map', {}).get(package, package) + + def create_package(self, package_name, package_destination, + binary_package=False): + """ + Creates a .sublime-package file from the running Packages directory + + :param package_name: + The package to create a .sublime-package file for + + :param package_destination: + The full filesystem path of the directory to save the new + .sublime-package file in. + + :param binary_package: + If the created package should follow the binary package include/ + exclude patterns from the settings. These normally include a setup + to exclude .py files and include .pyc files, but that can be + changed via settings. + + :return: bool if the package file was successfully created + """ + + package_dir = self.get_package_dir(package_name) + + if not os.path.exists(package_dir): + show_error(u'The folder for the package name specified, %s, does not exist in %s' % ( + package_name, sublime.packages_path())) + return False + + package_filename = package_name + '.sublime-package' + package_path = os.path.join(package_destination, + package_filename) + + if not os.path.exists(sublime.installed_packages_path()): + os.mkdir(sublime.installed_packages_path()) + + if os.path.exists(package_path): + os.remove(package_path) + + try: + package_file = zipfile.ZipFile(package_path, "w", + compression=zipfile.ZIP_DEFLATED) + except (OSError, IOError) as e: + show_error(u'An error occurred creating the package file %s in %s.\n\n%s' % ( + package_filename, package_destination, unicode_from_os(e))) + return False + + if int(sublime.version()) >= 3000: + compileall.compile_dir(package_dir, quiet=True, legacy=True, optimize=2) + + dirs_to_ignore = self.settings.get('dirs_to_ignore', []) + if not binary_package: + files_to_ignore = self.settings.get('files_to_ignore', []) + files_to_include = self.settings.get('files_to_include', []) + else: + files_to_ignore = self.settings.get('files_to_ignore_binary', []) + files_to_include = self.settings.get('files_to_include_binary', []) + + slash = '\\' if os.name == 'nt' else '/' + trailing_package_dir = package_dir + slash if package_dir[-1] != slash else package_dir + package_dir_regex = re.compile('^' + re.escape(trailing_package_dir)) + for root, dirs, files in os.walk(package_dir): + [dirs.remove(dir_) for dir_ in dirs if dir_ in dirs_to_ignore] + paths = dirs + paths.extend(files) + for path in paths: + full_path = os.path.join(root, path) + relative_path = re.sub(package_dir_regex, '', full_path) + + ignore_matches = [fnmatch(relative_path, p) for p in files_to_ignore] + include_matches = [fnmatch(relative_path, p) for p in files_to_include] + if any(ignore_matches) and not any(include_matches): + continue + + if os.path.isdir(full_path): + continue + package_file.write(full_path, relative_path) + + package_file.close() + + return True + + def install_package(self, package_name): + """ + Downloads and installs (or upgrades) a package + + Uses the self.list_available_packages() method to determine where to + retrieve the package file from. + + The install process consists of: + + 1. Finding the package + 2. Downloading the .sublime-package/.zip file + 3. Extracting the package file + 4. Showing install/upgrade messaging + 5. Submitting usage info + 6. Recording that the package is installed + + :param package_name: + The package to download and install + + :return: bool if the package was successfully installed + """ + + packages = self.list_available_packages() + + is_available = package_name in list(packages.keys()) + is_unavailable = package_name in self.settings.get('unavailable_packages', []) + + if is_unavailable and not is_available: + console_write(u'The package "%s" is not available on this platform.' % package_name, True) + return False + + if not is_available: + show_error(u'The package specified, %s, is not available' % package_name) + return False + + url = packages[package_name]['download']['url'] + package_filename = package_name + '.sublime-package' + + tmp_dir = tempfile.mkdtemp() + + try: + # This is refers to the zipfile later on, so we define it here so we can + # close the zip file if set during the finally clause + package_zip = None + + tmp_package_path = os.path.join(tmp_dir, package_filename) + + unpacked_package_dir = self.get_package_dir(package_name) + package_path = os.path.join(sublime.installed_packages_path(), + package_filename) + pristine_package_path = os.path.join(os.path.dirname( + sublime.packages_path()), 'Pristine Packages', package_filename) + + if os.path.exists(os.path.join(unpacked_package_dir, '.git')): + if self.settings.get('ignore_vcs_packages'): + show_error(u'Skipping git package %s since the setting ignore_vcs_packages is set to true' % package_name) + return False + return GitUpgrader(self.settings['git_binary'], + self.settings['git_update_command'], unpacked_package_dir, + self.settings['cache_length'], self.settings['debug']).run() + elif os.path.exists(os.path.join(unpacked_package_dir, '.hg')): + if self.settings.get('ignore_vcs_packages'): + show_error(u'Skipping hg package %s since the setting ignore_vcs_packages is set to true' % package_name) + return False + return HgUpgrader(self.settings['hg_binary'], + self.settings['hg_update_command'], unpacked_package_dir, + self.settings['cache_length'], self.settings['debug']).run() + + old_version = self.get_metadata(package_name).get('version') + is_upgrade = old_version != None + + # Download the sublime-package or zip file + try: + with downloader(url, self.settings) as manager: + package_bytes = manager.fetch(url, 'Error downloading package.') + except (DownloaderException) as e: + console_write(e, True) + show_error(u'Unable to download %s. Please view the console for more details.' % package_name) + return False + + with open_compat(tmp_package_path, "wb") as package_file: + package_file.write(package_bytes) + + # Try to open it as a zip file + try: + package_zip = zipfile.ZipFile(tmp_package_path, 'r') + except (zipfile.BadZipfile): + show_error(u'An error occurred while trying to unzip the package file for %s. Please try installing the package again.' % package_name) + return False + + # Scan through the root level of the zip file to gather some info + root_level_paths = [] + last_path = None + for path in package_zip.namelist(): + try: + if not isinstance(path, str_cls): + path = path.decode('utf-8', 'strict') + except (UnicodeDecodeError): + console_write(u'One or more of the zip file entries in %s is not encoded using UTF-8, aborting' % package_name, True) + return False + + last_path = path + + if path.find('/') in [len(path) - 1, -1]: + root_level_paths.append(path) + # Make sure there are no paths that look like security vulnerabilities + if path[0] == '/' or path.find('../') != -1 or path.find('..\\') != -1: + show_error(u'The package specified, %s, contains files outside of the package dir and cannot be safely installed.' % package_name) + return False + + if last_path and len(root_level_paths) == 0: + root_level_paths.append(last_path[0:last_path.find('/') + 1]) + + # If there is only a single directory at the top leve, the file + # is most likely a zip from BitBucket or GitHub and we need + # to skip the top-level dir when extracting + skip_root_dir = len(root_level_paths) == 1 and \ + root_level_paths[0].endswith('/') + + no_package_file_zip_path = '.no-sublime-package' + if skip_root_dir: + no_package_file_zip_path = root_level_paths[0] + no_package_file_zip_path + + # If we should extract unpacked or as a .sublime-package file + unpack = True + + # By default, ST3 prefers .sublime-package files since this allows + # overriding files in the Packages/{package_name}/ folder + if int(sublime.version()) >= 3000: + unpack = False + + # If the package maintainer doesn't want a .sublime-package + try: + package_zip.getinfo(no_package_file_zip_path) + unpack = True + except (KeyError): + pass + + # If we already have a package-metadata.json file in + # Packages/{package_name}/, the only way to successfully upgrade + # will be to unpack + unpacked_metadata_file = os.path.join(unpacked_package_dir, + 'package-metadata.json') + if os.path.exists(unpacked_metadata_file): + unpack = True + + # If we determined it should be unpacked, we extract directly + # into the Packages/{package_name}/ folder + if unpack: + self.backup_package_dir(package_name) + package_dir = unpacked_package_dir + + # Otherwise we go into a temp dir since we will be creating a + # new .sublime-package file later + else: + tmp_working_dir = os.path.join(tmp_dir, 'working') + os.mkdir(tmp_working_dir) + package_dir = tmp_working_dir + + package_metadata_file = os.path.join(package_dir, + 'package-metadata.json') + + if not os.path.exists(package_dir): + os.mkdir(package_dir) + + os.chdir(package_dir) + + # Here we don't use .extractall() since it was having issues on OS X + overwrite_failed = False + extracted_paths = [] + for path in package_zip.namelist(): + dest = path + + try: + if not isinstance(dest, str_cls): + dest = dest.decode('utf-8', 'strict') + except (UnicodeDecodeError): + console_write(u'One or more of the zip file entries in %s is not encoded using UTF-8, aborting' % package_name, True) + return False + + if os.name == 'nt': + regex = ':|\*|\?|"|<|>|\|' + if re.search(regex, dest) != None: + console_write(u'Skipping file from package named %s due to an invalid filename' % package_name, True) + continue + + # If there was only a single directory in the package, we remove + # that folder name from the paths as we extract entries + if skip_root_dir: + dest = dest[len(root_level_paths[0]):] + + if os.name == 'nt': + dest = dest.replace('/', '\\') + else: + dest = dest.replace('\\', '/') + + dest = os.path.join(package_dir, dest) + + def add_extracted_dirs(dir_): + while dir_ not in extracted_paths: + extracted_paths.append(dir_) + dir_ = os.path.dirname(dir_) + if dir_ == package_dir: + break + + if path.endswith('/'): + if not os.path.exists(dest): + os.makedirs(dest) + add_extracted_dirs(dest) + else: + dest_dir = os.path.dirname(dest) + if not os.path.exists(dest_dir): + os.makedirs(dest_dir) + add_extracted_dirs(dest_dir) + extracted_paths.append(dest) + try: + open_compat(dest, 'wb').write(package_zip.read(path)) + except (IOError) as e: + message = unicode_from_os(e) + if re.search('[Ee]rrno 13', message): + overwrite_failed = True + break + console_write(u'Skipping file from package named %s due to an invalid filename' % package_name, True) + + except (UnicodeDecodeError): + console_write(u'Skipping file from package named %s due to an invalid filename' % package_name, True) + + package_zip.close() + package_zip = None + + # If upgrading failed, queue the package to upgrade upon next start + if overwrite_failed: + reinstall_file = os.path.join(package_dir, 'package-control.reinstall') + open_compat(reinstall_file, 'w').close() + + # Don't delete the metadata file, that way we have it + # when the reinstall happens, and the appropriate + # usage info can be sent back to the server + clear_directory(package_dir, [reinstall_file, package_metadata_file]) + + show_error(u'An error occurred while trying to upgrade %s. Please restart Sublime Text to finish the upgrade.' % package_name) + return False + + # Here we clean out any files that were not just overwritten. It is ok + # if there is an error removing a file. The next time there is an + # upgrade, it should be cleaned out successfully then. + clear_directory(package_dir, extracted_paths) + + self.print_messages(package_name, package_dir, is_upgrade, old_version) + + with open_compat(package_metadata_file, 'w') as f: + metadata = { + "version": packages[package_name]['download']['version'], + "url": packages[package_name]['homepage'], + "description": packages[package_name]['description'] + } + json.dump(metadata, f) + + # Submit install and upgrade info + if is_upgrade: + params = { + 'package': package_name, + 'operation': 'upgrade', + 'version': packages[package_name]['download']['version'], + 'old_version': old_version + } + else: + params = { + 'package': package_name, + 'operation': 'install', + 'version': packages[package_name]['download']['version'] + } + self.record_usage(params) + + # Record the install in the settings file so that you can move + # settings across computers and have the same packages installed + def save_package(): + settings = sublime.load_settings('Package Control.sublime-settings') + installed_packages = settings.get('installed_packages', []) + if not installed_packages: + installed_packages = [] + installed_packages.append(package_name) + installed_packages = list(set(installed_packages)) + installed_packages = sorted(installed_packages, + key=lambda s: s.lower()) + settings.set('installed_packages', installed_packages) + sublime.save_settings('Package Control.sublime-settings') + sublime.set_timeout(save_package, 1) + + # If we didn't extract directly into the Packages/{package_name}/ + # folder, we need to create a .sublime-package file and install it + if not unpack: + try: + # Remove the downloaded file since we are going to overwrite it + os.remove(tmp_package_path) + package_zip = zipfile.ZipFile(tmp_package_path, "w", + compression=zipfile.ZIP_DEFLATED) + except (OSError, IOError) as e: + show_error(u'An error occurred creating the package file %s in %s.\n\n%s' % ( + package_filename, tmp_dir, unicode_from_os(e))) + return False + + package_dir_regex = re.compile('^' + re.escape(package_dir)) + for root, dirs, files in os.walk(package_dir): + paths = dirs + paths.extend(files) + for path in paths: + full_path = os.path.join(root, path) + relative_path = re.sub(package_dir_regex, '', full_path) + if os.path.isdir(full_path): + continue + package_zip.write(full_path, relative_path) + + package_zip.close() + package_zip = None + + if os.path.exists(package_path): + os.remove(package_path) + shutil.move(tmp_package_path, package_path) + + # We have to remove the pristine package too or else Sublime Text 2 + # will silently delete the package + if os.path.exists(pristine_package_path): + os.remove(pristine_package_path) + + os.chdir(sublime.packages_path()) + return True + + finally: + # We need to make sure the zipfile is closed to + # help prevent permissions errors on Windows + if package_zip: + package_zip.close() + + # Try to remove the tmp dir after a second to make sure + # a virus scanner is holding a reference to the zipfile + # after we close it. + def remove_tmp_dir(): + try: + shutil.rmtree(tmp_dir) + except (PermissionError): + # If we can't remove the tmp dir, don't let an uncaught exception + # fall through and break the install process + pass + sublime.set_timeout(remove_tmp_dir, 1000) + + def backup_package_dir(self, package_name): + """ + Does a full backup of the Packages/{package}/ dir to Backup/ + + :param package_name: + The name of the package to back up + + :return: + If the backup succeeded + """ + + package_dir = os.path.join(sublime.packages_path(), package_name) + if not os.path.exists(package_dir): + return True + + try: + backup_dir = os.path.join(os.path.dirname( + sublime.packages_path()), 'Backup', + datetime.datetime.now().strftime('%Y%m%d%H%M%S')) + if not os.path.exists(backup_dir): + os.makedirs(backup_dir) + package_backup_dir = os.path.join(backup_dir, package_name) + if os.path.exists(package_backup_dir): + console_write(u"FOLDER %s ALREADY EXISTS!" % package_backup_dir) + shutil.copytree(package_dir, package_backup_dir) + return True + + except (OSError, IOError) as e: + show_error(u'An error occurred while trying to backup the package directory for %s.\n\n%s' % ( + package_name, unicode_from_os(e))) + if os.path.exists(package_backup_dir): + shutil.rmtree(package_backup_dir) + return False + + def print_messages(self, package, package_dir, is_upgrade, old_version): + """ + Prints out package install and upgrade messages + + The functionality provided by this allows package maintainers to + show messages to the user when a package is installed, or when + certain version upgrade occur. + + :param package: + The name of the package the message is for + + :param package_dir: + The full filesystem path to the package directory + + :param is_upgrade: + If the install was actually an upgrade + + :param old_version: + The string version of the package before the upgrade occurred + """ + + messages_file = os.path.join(package_dir, 'messages.json') + if not os.path.exists(messages_file): + return + + messages_fp = open_compat(messages_file, 'r') + try: + message_info = json.loads(read_compat(messages_fp)) + except (ValueError): + console_write(u'Error parsing messages.json for %s' % package, True) + return + messages_fp.close() + + output = '' + if not is_upgrade and message_info.get('install'): + install_messages = os.path.join(package_dir, + message_info.get('install')) + message = '\n\n%s:\n%s\n\n ' % (package, + ('-' * len(package))) + with open_compat(install_messages, 'r') as f: + message += read_compat(f).replace('\n', '\n ') + output += message + '\n' + + elif is_upgrade and old_version: + upgrade_messages = list(set(message_info.keys()) - + set(['install'])) + upgrade_messages = version_sort(upgrade_messages, reverse=True) + old_version_cmp = version_comparable(old_version) + + for version in upgrade_messages: + if version_comparable(version) <= old_version_cmp: + break + if not output: + message = '\n\n%s:\n%s\n' % (package, + ('-' * len(package))) + output += message + upgrade_message_path = os.path.join(package_dir, + message_info.get(version)) + message = '\n ' + with open_compat(upgrade_message_path, 'r') as f: + message += read_compat(f).replace('\n', '\n ') + output += message + '\n' + + if not output: + return + + def print_to_panel(): + window = sublime.active_window() + + views = window.views() + view = None + for _view in views: + if _view.name() == 'Package Control Messages': + view = _view + break + + if not view: + view = window.new_file() + view.set_name('Package Control Messages') + view.set_scratch(True) + + def write(string): + view.run_command('package_message', {'string': string}) + + if not view.size(): + view.settings().set("word_wrap", True) + write('Package Control Messages\n' + + '========================') + + write(output) + sublime.set_timeout(print_to_panel, 1) + + def remove_package(self, package_name): + """ + Deletes a package + + The deletion process consists of: + + 1. Deleting the directory (or marking it for deletion if deletion fails) + 2. Submitting usage info + 3. Removing the package from the list of installed packages + + :param package_name: + The package to delete + + :return: bool if the package was successfully deleted + """ + + installed_packages = self.list_packages() + + if package_name not in installed_packages: + show_error(u'The package specified, %s, is not installed' % package_name) + return False + + os.chdir(sublime.packages_path()) + + # Give Sublime Text some time to ignore the package + time.sleep(1) + + package_filename = package_name + '.sublime-package' + installed_package_path = os.path.join(sublime.installed_packages_path(), + package_filename) + pristine_package_path = os.path.join(os.path.dirname( + sublime.packages_path()), 'Pristine Packages', package_filename) + package_dir = self.get_package_dir(package_name) + + version = self.get_metadata(package_name).get('version') + + try: + if os.path.exists(installed_package_path): + os.remove(installed_package_path) + except (OSError, IOError) as e: + show_error(u'An error occurred while trying to remove the installed package file for %s.\n\n%s' % ( + package_name, unicode_from_os(e))) + return False + + try: + if os.path.exists(pristine_package_path): + os.remove(pristine_package_path) + except (OSError, IOError) as e: + show_error(u'An error occurred while trying to remove the pristine package file for %s.\n\n%s' % ( + package_name, unicode_from_os(e))) + return False + + # We don't delete the actual package dir immediately due to a bug + # in sublime_plugin.py + can_delete_dir = True + if not clear_directory(package_dir): + # If there is an error deleting now, we will mark it for + # cleanup the next time Sublime Text starts + open_compat(os.path.join(package_dir, 'package-control.cleanup'), + 'w').close() + can_delete_dir = False + + params = { + 'package': package_name, + 'operation': 'remove', + 'version': version + } + self.record_usage(params) + + # Remove the package from the installed packages list + def clear_package(): + settings = sublime.load_settings('Package Control.sublime-settings') + installed_packages = settings.get('installed_packages', []) + if not installed_packages: + installed_packages = [] + installed_packages.remove(package_name) + settings.set('installed_packages', installed_packages) + sublime.save_settings('Package Control.sublime-settings') + sublime.set_timeout(clear_package, 1) + + if can_delete_dir and os.path.exists(package_dir): + os.rmdir(package_dir) + + return True + + def record_usage(self, params): + """ + Submits install, upgrade and delete actions to a usage server + + The usage information is currently displayed on the Package Control + community package list at http://wbond.net/sublime_packages/community + + :param params: + A dict of the information to submit + """ + + if not self.settings.get('submit_usage'): + return + params['package_control_version'] = \ + self.get_metadata('Package Control').get('version') + params['sublime_platform'] = self.settings.get('platform') + params['sublime_version'] = self.settings.get('version') + + # For Python 2, we need to explicitly encoding the params + for param in params: + if isinstance(params[param], str_cls): + params[param] = params[param].encode('utf-8') + + url = self.settings.get('submit_url') + '?' + urlencode(params) + + try: + with downloader(url, self.settings) as manager: + result = manager.fetch(url, 'Error submitting usage information.') + except (DownloaderException) as e: + console_write(e, True) + return + + try: + result = json.loads(result.decode('utf-8')) + if result['result'] != 'success': + raise ValueError() + except (ValueError): + console_write(u'Error submitting usage information for %s' % params['package'], True) diff --git a/sublime/Packages/Package Control/package_control/package_renamer.py b/sublime/Packages/Package Control/package_control/package_renamer.py new file mode 100644 index 0000000..73e83fd --- /dev/null +++ b/sublime/Packages/Package Control/package_control/package_renamer.py @@ -0,0 +1,117 @@ +import os + +import sublime + +from .console_write import console_write +from .package_io import package_file_exists + + +class PackageRenamer(): + """ + Class to handle renaming packages via the renamed_packages setting + gathered from channels and repositories. + """ + + def load_settings(self): + """ + Loads the list of installed packages from the + Package Control.sublime-settings file. + """ + + self.settings_file = 'Package Control.sublime-settings' + self.settings = sublime.load_settings(self.settings_file) + self.installed_packages = self.settings.get('installed_packages', []) + if not isinstance(self.installed_packages, list): + self.installed_packages = [] + + def rename_packages(self, installer): + """ + Renames any installed packages that the user has installed. + + :param installer: + An instance of :class:`PackageInstaller` + """ + + # Fetch the packages since that will pull in the renamed packages list + installer.manager.list_available_packages() + renamed_packages = installer.manager.settings.get('renamed_packages', {}) + if not renamed_packages: + renamed_packages = {} + + # These are packages that have been tracked as installed + installed_pkgs = self.installed_packages + # There are the packages actually present on the filesystem + present_packages = installer.manager.list_packages() + + # Rename directories for packages that have changed names + for package_name in renamed_packages: + package_dir = os.path.join(sublime.packages_path(), package_name) + if not package_file_exists(package_name, 'package-metadata.json'): + continue + + new_package_name = renamed_packages[package_name] + new_package_dir = os.path.join(sublime.packages_path(), + new_package_name) + + changing_case = package_name.lower() == new_package_name.lower() + case_insensitive_fs = sublime.platform() in ['windows', 'osx'] + + # Since Windows and OSX use case-insensitive filesystems, we have to + # scan through the list of installed packages if the rename of the + # package is just changing the case of it. If we don't find the old + # name for it, we continue the loop since os.path.exists() will return + # true due to the case-insensitive nature of the filesystems. + if case_insensitive_fs and changing_case: + has_old = False + for present_package_name in present_packages: + if present_package_name == package_name: + has_old = True + break + if not has_old: + continue + + if not os.path.exists(new_package_dir) or (case_insensitive_fs and changing_case): + + # Windows will not allow you to rename to the same name with + # a different case, so we work around that with a temporary name + if os.name == 'nt' and changing_case: + temp_package_name = '__' + new_package_name + temp_package_dir = os.path.join(sublime.packages_path(), + temp_package_name) + os.rename(package_dir, temp_package_dir) + package_dir = temp_package_dir + + os.rename(package_dir, new_package_dir) + installed_pkgs.append(new_package_name) + + console_write(u'Renamed %s to %s' % (package_name, new_package_name), True) + + else: + installer.manager.remove_package(package_name) + message_string = u'Removed %s since package with new name (%s) already exists' % ( + package_name, new_package_name) + console_write(message_string, True) + + try: + installed_pkgs.remove(package_name) + except (ValueError): + pass + + sublime.set_timeout(lambda: self.save_packages(installed_pkgs), 10) + + def save_packages(self, installed_packages): + """ + Saves the list of installed packages (after having been appropriately + renamed) + + :param installed_packages: + The new list of installed packages + """ + + installed_packages = list(set(installed_packages)) + installed_packages = sorted(installed_packages, + key=lambda s: s.lower()) + + if installed_packages != self.installed_packages: + self.settings.set('installed_packages', installed_packages) + sublime.save_settings(self.settings_file) diff --git a/sublime/Packages/Package Control/package_control/preferences_filename.py b/sublime/Packages/Package Control/package_control/preferences_filename.py new file mode 100644 index 0000000..7091dd9 --- /dev/null +++ b/sublime/Packages/Package Control/package_control/preferences_filename.py @@ -0,0 +1,11 @@ +import sublime + + +def preferences_filename(): + """ + :return: The appropriate settings filename based on the version of Sublime Text + """ + + if int(sublime.version()) >= 2174: + return 'Preferences.sublime-settings' + return 'Global.sublime-settings' diff --git a/sublime/Packages/Package Control/package_control/providers/__init__.py b/sublime/Packages/Package Control/package_control/providers/__init__.py new file mode 100644 index 0000000..cfea3bd --- /dev/null +++ b/sublime/Packages/Package Control/package_control/providers/__init__.py @@ -0,0 +1,12 @@ +from .bitbucket_repository_provider import BitBucketRepositoryProvider +from .github_repository_provider import GitHubRepositoryProvider +from .github_user_provider import GitHubUserProvider +from .repository_provider import RepositoryProvider + +from .channel_provider import ChannelProvider + + +REPOSITORY_PROVIDERS = [BitBucketRepositoryProvider, GitHubRepositoryProvider, + GitHubUserProvider, RepositoryProvider] + +CHANNEL_PROVIDERS = [ChannelProvider] diff --git a/sublime/Packages/Package Control/package_control/providers/bitbucket_repository_provider.py b/sublime/Packages/Package Control/package_control/providers/bitbucket_repository_provider.py new file mode 100644 index 0000000..b5d603f --- /dev/null +++ b/sublime/Packages/Package Control/package_control/providers/bitbucket_repository_provider.py @@ -0,0 +1,163 @@ +import re + +from ..clients.bitbucket_client import BitBucketClient +from ..downloaders.downloader_exception import DownloaderException +from ..clients.client_exception import ClientException +from .provider_exception import ProviderException + + +class BitBucketRepositoryProvider(): + """ + Allows using a public BitBucket repository as the source for a single package. + For legacy purposes, this can also be treated as the source for a Package + Control "repository". + + :param repo: + The public web URL to the BitBucket repository. Should be in the format + `https://bitbucket.org/user/package`. + + :param settings: + A dict containing at least the following fields: + `cache_length`, + `debug`, + `timeout`, + `user_agent` + Optional fields: + `http_proxy`, + `https_proxy`, + `proxy_username`, + `proxy_password`, + `query_string_params` + `install_prereleases` + """ + + def __init__(self, repo, settings): + self.cache = {} + self.repo = repo + self.settings = settings + self.failed_sources = {} + + @classmethod + def match_url(cls, repo): + """Indicates if this provider can handle the provided repo""" + + return re.search('^https?://bitbucket.org/([^/]+/[^/]+)/?$', repo) != None + + def prefetch(self): + """ + Go out and perform HTTP operations, caching the result + + :raises: + DownloaderException: when there is an issue download package info + ClientException: when there is an issue parsing package info + """ + + [name for name, info in self.get_packages()] + + def get_failed_sources(self): + """ + List of any URLs that could not be accessed while accessing this repository + + :return: + A generator of ("https://bitbucket.org/user/repo", Exception()) tuples + """ + + return self.failed_sources.items() + + def get_broken_packages(self): + """ + For API-compatibility with RepositoryProvider + """ + + return {}.items() + + def get_packages(self, invalid_sources=None): + """ + Uses the BitBucket API to construct necessary info for a package + + :param invalid_sources: + A list of URLs that should be ignored + + :raises: + DownloaderException: when there is an issue download package info + ClientException: when there is an issue parsing package info + + :return: + A generator of + ( + 'Package Name', + { + 'name': name, + 'description': description, + 'author': author, + 'homepage': homepage, + 'last_modified': last modified date, + 'download': { + 'url': url, + 'date': date, + 'version': version + }, + 'previous_names': [], + 'labels': [], + 'sources': [the repo URL], + 'readme': url, + 'issues': url, + 'donate': url, + 'buy': None + } + ) + tuples + """ + + if 'get_packages' in self.cache: + for key, value in self.cache['get_packages'].items(): + yield (key, value) + return + + client = BitBucketClient(self.settings) + + if invalid_sources != None and self.repo in invalid_sources: + raise StopIteration() + + try: + repo_info = client.repo_info(self.repo) + download = client.download_info(self.repo) + + name = repo_info['name'] + details = { + 'name': name, + 'description': repo_info['description'], + 'homepage': repo_info['homepage'], + 'author': repo_info['author'], + 'last_modified': download.get('date'), + 'download': download, + 'previous_names': [], + 'labels': [], + 'sources': [self.repo], + 'readme': repo_info['readme'], + 'issues': repo_info['issues'], + 'donate': repo_info['donate'], + 'buy': None + } + self.cache['get_packages'] = {name: details} + yield (name, details) + + except (DownloaderException, ClientException, ProviderException) as e: + self.failed_sources[self.repo] = e + self.cache['get_packages'] = {} + raise StopIteration() + + def get_renamed_packages(self): + """For API-compatibility with RepositoryProvider""" + + return {} + + def get_unavailable_packages(self): + """ + Method for compatibility with RepositoryProvider class. These providers + are based on API calls, and thus do not support different platform + downloads, making it impossible for there to be unavailable packages. + + :return: An empty list + """ + return [] diff --git a/sublime/Packages/Package Control/package_control/providers/channel_provider.py b/sublime/Packages/Package Control/package_control/providers/channel_provider.py new file mode 100644 index 0000000..5543bdc --- /dev/null +++ b/sublime/Packages/Package Control/package_control/providers/channel_provider.py @@ -0,0 +1,312 @@ +import json +import os +import re + +try: + # Python 3 + from urllib.parse import urlparse +except (ImportError): + # Python 2 + from urlparse import urlparse + +from ..console_write import console_write +from .release_selector import ReleaseSelector +from .provider_exception import ProviderException +from ..downloaders.downloader_exception import DownloaderException +from ..clients.client_exception import ClientException +from ..download_manager import downloader + + +class ChannelProvider(ReleaseSelector): + """ + Retrieves a channel and provides an API into the information + + The current channel/repository infrastructure caches repository info into + the channel to improve the Package Control client performance. This also + has the side effect of lessening the load on the GitHub and BitBucket APIs + and getting around not-infrequent HTTP 503 errors from those APIs. + + :param channel: + The URL of the channel + + :param settings: + A dict containing at least the following fields: + `cache_length`, + `debug`, + `timeout`, + `user_agent` + Optional fields: + `http_proxy`, + `https_proxy`, + `proxy_username`, + `proxy_password`, + `query_string_params` + `install_prereleases` + """ + + def __init__(self, channel, settings): + self.channel_info = None + self.schema_version = 0.0 + self.channel = channel + self.settings = settings + self.unavailable_packages = [] + + @classmethod + def match_url(cls, channel): + """Indicates if this provider can handle the provided channel""" + + return True + + def prefetch(self): + """ + Go out and perform HTTP operations, caching the result + + :raises: + ProviderException: when an error occurs trying to open a file + DownloaderException: when an error occurs trying to open a URL + """ + + self.fetch() + + def fetch(self): + """ + Retrieves and loads the JSON for other methods to use + + :raises: + ProviderException: when an error occurs with the channel contents + DownloaderException: when an error occurs trying to open a URL + """ + + if self.channel_info != None: + return + + if re.match('https?://', self.channel, re.I): + with downloader(self.channel, self.settings) as manager: + channel_json = manager.fetch(self.channel, + 'Error downloading channel.') + + # All other channels are expected to be filesystem paths + else: + if not os.path.exists(self.channel): + raise ProviderException(u'Error, file %s does not exist' % self.channel) + + if self.settings.get('debug'): + console_write(u'Loading %s as a channel' % self.channel, True) + + # We open as binary so we get bytes like the DownloadManager + with open(self.channel, 'rb') as f: + channel_json = f.read() + + try: + channel_info = json.loads(channel_json.decode('utf-8')) + except (ValueError): + raise ProviderException(u'Error parsing JSON from channel %s.' % self.channel) + + schema_error = u'Channel %s does not appear to be a valid channel file because ' % self.channel + + if 'schema_version' not in channel_info: + raise ProviderException(u'%s the "schema_version" JSON key is missing.' % schema_error) + + try: + self.schema_version = float(channel_info.get('schema_version')) + except (ValueError): + raise ProviderException(u'%s the "schema_version" is not a valid number.' % schema_error) + + if self.schema_version not in [1.0, 1.1, 1.2, 2.0]: + raise ProviderException(u'%s the "schema_version" is not recognized. Must be one of: 1.0, 1.1, 1.2 or 2.0.' % schema_error) + + self.channel_info = channel_info + + def get_name_map(self): + """ + :raises: + ProviderException: when an error occurs with the channel contents + DownloaderException: when an error occurs trying to open a URL + + :return: + A dict of the mapping for URL slug -> package name + """ + + self.fetch() + + if self.schema_version >= 2.0: + return {} + + return self.channel_info.get('package_name_map', {}) + + def get_renamed_packages(self): + """ + :raises: + ProviderException: when an error occurs with the channel contents + DownloaderException: when an error occurs trying to open a URL + + :return: + A dict of the packages that have been renamed + """ + + self.fetch() + + if self.schema_version >= 2.0: + return {} + + return self.channel_info.get('renamed_packages', {}) + + def get_repositories(self): + """ + :raises: + ProviderException: when an error occurs with the channel contents + DownloaderException: when an error occurs trying to open a URL + + :return: + A list of the repository URLs + """ + + self.fetch() + + if 'repositories' not in self.channel_info: + raise ProviderException(u'Channel %s does not appear to be a valid channel file because the "repositories" JSON key is missing.' % self.channel) + + # Determine a relative root so repositories can be defined + # relative to the location of the channel file. + if re.match('https?://', self.channel, re.I): + url_pieces = urlparse(self.channel) + domain = url_pieces.scheme + '://' + url_pieces.netloc + path = '/' if url_pieces.path == '' else url_pieces.path + if path[-1] != '/': + path = os.path.dirname(path) + relative_base = domain + path + else: + relative_base = os.path.dirname(self.channel) + '/' + + output = [] + repositories = self.channel_info.get('repositories', []) + for repository in repositories: + if re.match('^\./|\.\./', repository): + repository = os.path.normpath(relative_base + repository) + output.append(repository) + + return output + + def get_certs(self): + """ + Provides a secure way for distribution of SSL CA certificates + + Unfortunately Python does not include a bundle of CA certs with urllib + to perform SSL certificate validation. To circumvent this issue, + Package Control acts as a distributor of the CA certs for all HTTPS + URLs of package downloads. + + The default channel scrapes and caches info about all packages + periodically, and in the process it checks the CA certs for all of + the HTTPS URLs listed in the repositories. The contents of the CA cert + files are then hashed, and the CA cert is stored in a filename with + that hash. This is a fingerprint to ensure that Package Control has + the appropriate CA cert for a domain name. + + Next, the default channel file serves up a JSON object of the domain + names and the hashes of their current CA cert files. If Package Control + does not have the appropriate hash for a domain, it may retrieve it + from the channel server. To ensure that Package Control is talking to + a trusted authority to get the CA certs from, the CA cert for + sublime.wbond.net is bundled with Package Control. Then when downloading + the channel file, Package Control can ensure that the channel file's + SSL certificate is valid, thus ensuring the resulting CA certs are + legitimate. + + As a matter of optimization, the distribution of Package Control also + includes the current CA certs for all known HTTPS domains that are + included in the channel, as of the time when Package Control was + last released. + + :raises: + ProviderException: when an error occurs with the channel contents + DownloaderException: when an error occurs trying to open a URL + + :return: + A dict of {'Domain Name': ['cert_file_hash', 'cert_file_download_url']} + """ + + self.fetch() + + return self.channel_info.get('certs', {}) + + def get_packages(self, repo): + """ + Provides access to the repository info that is cached in a channel + + :param repo: + The URL of the repository to get the cached info of + + :raises: + ProviderException: when an error occurs with the channel contents + DownloaderException: when an error occurs trying to open a URL + + :return: + A dict in the format: + { + 'Package Name': { + 'name': name, + 'description': description, + 'author': author, + 'homepage': homepage, + 'last_modified': last modified date, + 'download': { + 'url': url, + 'date': date, + 'version': version + }, + 'previous_names': [old_name, ...], + 'labels': [label, ...], + 'readme': url, + 'issues': url, + 'donate': url, + 'buy': url + }, + ... + } + """ + + self.fetch() + + # The 2.0 channel schema renamed the key cached package info was + # stored under in order to be more clear to new users. + packages_key = 'packages_cache' if self.schema_version >= 2.0 else 'packages' + + if self.channel_info.get(packages_key, False) == False: + return {} + + if self.channel_info[packages_key].get(repo, False) == False: + return {} + + output = {} + for package in self.channel_info[packages_key][repo]: + copy = package.copy() + + # In schema version 2.0, we store a list of dicts containing info + # about all available releases. These include "version" and + # "platforms" keys that are used to pick the download for the + # current machine. + if self.schema_version >= 2.0: + copy = self.select_release(copy) + else: + copy = self.select_platform(copy) + + if not copy: + self.unavailable_packages.append(package['name']) + continue + + output[copy['name']] = copy + + return output + + def get_unavailable_packages(self): + """ + Provides a list of packages that are unavailable for the current + platform/architecture that Sublime Text is running on. + + This list will be empty unless get_packages() is called first. + + :return: A list of package names + """ + + return self.unavailable_packages diff --git a/sublime/Packages/Package Control/package_control/providers/github_repository_provider.py b/sublime/Packages/Package Control/package_control/providers/github_repository_provider.py new file mode 100644 index 0000000..158c850 --- /dev/null +++ b/sublime/Packages/Package Control/package_control/providers/github_repository_provider.py @@ -0,0 +1,169 @@ +import re + +from ..clients.github_client import GitHubClient +from ..downloaders.downloader_exception import DownloaderException +from ..clients.client_exception import ClientException +from .provider_exception import ProviderException + + +class GitHubRepositoryProvider(): + """ + Allows using a public GitHub repository as the source for a single package. + For legacy purposes, this can also be treated as the source for a Package + Control "repository". + + :param repo: + The public web URL to the GitHub repository. Should be in the format + `https://github.com/user/package` for the master branch, or + `https://github.com/user/package/tree/{branch_name}` for any other + branch. + + :param settings: + A dict containing at least the following fields: + `cache_length`, + `debug`, + `timeout`, + `user_agent` + Optional fields: + `http_proxy`, + `https_proxy`, + `proxy_username`, + `proxy_password`, + `query_string_params` + `install_prereleases` + """ + + def __init__(self, repo, settings): + self.cache = {} + # Clean off the trailing .git to be more forgiving + self.repo = re.sub('\.git$', '', repo) + self.settings = settings + self.failed_sources = {} + + @classmethod + def match_url(cls, repo): + """Indicates if this provider can handle the provided repo""" + + master = re.search('^https?://github.com/[^/]+/[^/]+/?$', repo) + branch = re.search('^https?://github.com/[^/]+/[^/]+/tree/[^/]+/?$', + repo) + return master != None or branch != None + + def prefetch(self): + """ + Go out and perform HTTP operations, caching the result + + :raises: + DownloaderException: when there is an issue download package info + ClientException: when there is an issue parsing package info + """ + + [name for name, info in self.get_packages()] + + def get_failed_sources(self): + """ + List of any URLs that could not be accessed while accessing this repository + + :return: + A generator of ("https://github.com/user/repo", Exception()) tuples + """ + + return self.failed_sources.items() + + def get_broken_packages(self): + """ + For API-compatibility with RepositoryProvider + """ + + return {}.items() + + def get_packages(self, invalid_sources=None): + """ + Uses the GitHub API to construct necessary info for a package + + :param invalid_sources: + A list of URLs that should be ignored + + :raises: + DownloaderException: when there is an issue download package info + ClientException: when there is an issue parsing package info + + :return: + A generator of + ( + 'Package Name', + { + 'name': name, + 'description': description, + 'author': author, + 'homepage': homepage, + 'last_modified': last modified date, + 'download': { + 'url': url, + 'date': date, + 'version': version + }, + 'previous_names': [], + 'labels': [], + 'sources': [the repo URL], + 'readme': url, + 'issues': url, + 'donate': url, + 'buy': None + } + ) + tuples + """ + + if 'get_packages' in self.cache: + for key, value in self.cache['get_packages'].items(): + yield (key, value) + return + + client = GitHubClient(self.settings) + + if invalid_sources != None and self.repo in invalid_sources: + raise StopIteration() + + try: + repo_info = client.repo_info(self.repo) + download = client.download_info(self.repo) + + name = repo_info['name'] + details = { + 'name': name, + 'description': repo_info['description'], + 'homepage': repo_info['homepage'], + 'author': repo_info['author'], + 'last_modified': download.get('date'), + 'download': download, + 'previous_names': [], + 'labels': [], + 'sources': [self.repo], + 'readme': repo_info['readme'], + 'issues': repo_info['issues'], + 'donate': repo_info['donate'], + 'buy': None + } + self.cache['get_packages'] = {name: details} + yield (name, details) + + except (DownloaderException, ClientException, ProviderException) as e: + self.failed_sources[self.repo] = e + self.cache['get_packages'] = {} + raise StopIteration() + + def get_renamed_packages(self): + """For API-compatibility with RepositoryProvider""" + + return {} + + def get_unavailable_packages(self): + """ + Method for compatibility with RepositoryProvider class. These providers + are based on API calls, and thus do not support different platform + downloads, making it impossible for there to be unavailable packages. + + :return: An empty list + """ + return [] diff --git a/sublime/Packages/Package Control/package_control/providers/github_user_provider.py b/sublime/Packages/Package Control/package_control/providers/github_user_provider.py new file mode 100644 index 0000000..6af60be --- /dev/null +++ b/sublime/Packages/Package Control/package_control/providers/github_user_provider.py @@ -0,0 +1,172 @@ +import re + +from ..clients.github_client import GitHubClient +from ..downloaders.downloader_exception import DownloaderException +from ..clients.client_exception import ClientException +from .provider_exception import ProviderException + + +class GitHubUserProvider(): + """ + Allows using a GitHub user/organization as the source for multiple packages, + or in Package Control terminology, a "repository". + + :param repo: + The public web URL to the GitHub user/org. Should be in the format + `https://github.com/user`. + + :param settings: + A dict containing at least the following fields: + `cache_length`, + `debug`, + `timeout`, + `user_agent`, + Optional fields: + `http_proxy`, + `https_proxy`, + `proxy_username`, + `proxy_password`, + `query_string_params` + `install_prereleases` + """ + + def __init__(self, repo, settings): + self.cache = {} + self.repo = repo + self.settings = settings + self.failed_sources = {} + + @classmethod + def match_url(cls, repo): + """Indicates if this provider can handle the provided repo""" + + return re.search('^https?://github.com/[^/]+/?$', repo) != None + + def prefetch(self): + """ + Go out and perform HTTP operations, caching the result + """ + + [name for name, info in self.get_packages()] + + def get_failed_sources(self): + """ + List of any URLs that could not be accessed while accessing this repository + + :raises: + DownloaderException: when there is an issue download package info + ClientException: when there is an issue parsing package info + + :return: + A generator of ("https://github.com/user/repo", Exception()) tuples + """ + + return self.failed_sources.items() + + def get_broken_packages(self): + """ + For API-compatibility with RepositoryProvider + """ + + return {}.items() + + def get_packages(self, invalid_sources=None): + """ + Uses the GitHub API to construct necessary info for all packages + + :param invalid_sources: + A list of URLs that should be ignored + + :raises: + DownloaderException: when there is an issue download package info + ClientException: when there is an issue parsing package info + + :return: + A generator of + ( + 'Package Name', + { + 'name': name, + 'description': description, + 'author': author, + 'homepage': homepage, + 'last_modified': last modified date, + 'download': { + 'url': url, + 'date': date, + 'version': version + }, + 'previous_names': [], + 'labels': [], + 'sources': [the user URL], + 'readme': url, + 'issues': url, + 'donate': url, + 'buy': None + } + ) + tuples + """ + + if 'get_packages' in self.cache: + for key, value in self.cache['get_packages'].items(): + yield (key, value) + return + + client = GitHubClient(self.settings) + + if invalid_sources != None and self.repo in invalid_sources: + raise StopIteration() + + try: + user_repos = client.user_info(self.repo) + except (DownloaderException, ClientException, ProviderException) as e: + self.failed_sources = [self.repo] + self.cache['get_packages'] = e + raise e + + output = {} + for repo_info in user_repos: + try: + name = repo_info['name'] + repo_url = 'https://github.com/' + repo_info['user_repo'] + + download = client.download_info(repo_url) + + details = { + 'name': name, + 'description': repo_info['description'], + 'homepage': repo_info['homepage'], + 'author': repo_info['author'], + 'last_modified': download.get('date'), + 'download': download, + 'previous_names': [], + 'labels': [], + 'sources': [self.repo], + 'readme': repo_info['readme'], + 'issues': repo_info['issues'], + 'donate': repo_info['donate'], + 'buy': None + } + output[name] = details + yield (name, details) + + except (DownloaderException, ClientException, ProviderException) as e: + self.failed_sources[repo_url] = e + + self.cache['get_packages'] = output + + def get_renamed_packages(self): + """For API-compatibility with RepositoryProvider""" + + return {} + + def get_unavailable_packages(self): + """ + Method for compatibility with RepositoryProvider class. These providers + are based on API calls, and thus do not support different platform + downloads, making it impossible for there to be unavailable packages. + + :return: An empty list + """ + return [] diff --git a/sublime/Packages/Package Control/package_control/providers/provider_exception.py b/sublime/Packages/Package Control/package_control/providers/provider_exception.py new file mode 100644 index 0000000..e98295f --- /dev/null +++ b/sublime/Packages/Package Control/package_control/providers/provider_exception.py @@ -0,0 +1,5 @@ +class ProviderException(Exception): + """If a provider could not return information""" + + def __str__(self): + return self.args[0] diff --git a/sublime/Packages/Package Control/package_control/providers/release_selector.py b/sublime/Packages/Package Control/package_control/providers/release_selector.py new file mode 100644 index 0000000..5305468 --- /dev/null +++ b/sublime/Packages/Package Control/package_control/providers/release_selector.py @@ -0,0 +1,125 @@ +import re +import sublime + +from ..versions import version_sort, version_exclude_prerelease + + +class ReleaseSelector(): + """ + A base class for finding the best version of a package for the current machine + """ + + def select_release(self, package_info): + """ + Returns a modified package info dict for package from package schema version 2.0 + + :param package_info: + A package info dict with a "releases" key + + :return: + The package info dict with the "releases" key deleted, and a + "download" key added that contains a dict with "version", "url" and + "date" keys. + None if no compatible relases are available. + """ + + releases = version_sort(package_info['releases']) + if not self.settings.get('install_prereleases'): + releases = version_exclude_prerelease(releases) + + for release in releases: + platforms = release.get('platforms', '*') + if not isinstance(platforms, list): + platforms = [platforms] + + best_platform = self.get_best_platform(platforms) + if not best_platform: + continue + + if not self.is_compatible_version(release.get('sublime_text', '<3000')): + continue + + package_info['download'] = release + package_info['last_modified'] = release.get('date') + del package_info['releases'] + + return package_info + + return None + + def select_platform(self, package_info): + """ + Returns a modified package info dict for package from package schema version <= 1.2 + + :param package_info: + A package info dict with a "platforms" key + + :return: + The package info dict with the "platforms" key deleted, and a + "download" key added that contains a dict with "version" and "url" + keys. + None if no compatible platforms. + """ + platforms = list(package_info['platforms'].keys()) + best_platform = self.get_best_platform(platforms) + if not best_platform: + return None + + package_info['download'] = package_info['platforms'][best_platform][0] + package_info['download']['date'] = package_info.get('last_modified') + del package_info['platforms'] + + return package_info + + def get_best_platform(self, platforms): + """ + Returns the most specific platform that matches the current machine + + :param platforms: + An array of platform names for a package. E.g. ['*', 'windows', 'linux-x64'] + + :return: A string reprenting the most specific matching platform + """ + + ids = [sublime.platform() + '-' + sublime.arch(), sublime.platform(), + '*'] + + for id in ids: + if id in platforms: + return id + + return None + + def is_compatible_version(self, version_range): + min_version = float("-inf") + max_version = float("inf") + + if version_range == '*': + return True + + gt_match = re.match('>(\d+)$', version_range) + ge_match = re.match('>=(\d+)$', version_range) + lt_match = re.match('<(\d+)$', version_range) + le_match = re.match('<=(\d+)$', version_range) + range_match = re.match('(\d+) - (\d+)$', version_range) + + if gt_match: + min_version = int(gt_match.group(1)) + 1 + elif ge_match: + min_version = int(ge_match.group(1)) + elif lt_match: + max_version = int(lt_match.group(1)) - 1 + elif le_match: + max_version = int(le_match.group(1)) + elif range_match: + min_version = int(range_match.group(1)) + max_version = int(range_match.group(2)) + else: + return None + + if min_version > int(sublime.version()): + return False + if max_version < int(sublime.version()): + return False + + return True diff --git a/sublime/Packages/Package Control/package_control/providers/repository_provider.py b/sublime/Packages/Package Control/package_control/providers/repository_provider.py new file mode 100644 index 0000000..01a5ad9 --- /dev/null +++ b/sublime/Packages/Package Control/package_control/providers/repository_provider.py @@ -0,0 +1,441 @@ +import json +import re +import os +from itertools import chain + +try: + # Python 3 + from urllib.parse import urlparse +except (ImportError): + # Python 2 + from urlparse import urlparse + +from ..console_write import console_write +from .release_selector import ReleaseSelector +from .provider_exception import ProviderException +from ..downloaders.downloader_exception import DownloaderException +from ..clients.client_exception import ClientException +from ..clients.github_client import GitHubClient +from ..clients.bitbucket_client import BitBucketClient +from ..download_manager import downloader + + +class RepositoryProvider(ReleaseSelector): + """ + Generic repository downloader that fetches package info + + With the current channel/repository architecture where the channel file + caches info from all includes repositories, these package providers just + serve the purpose of downloading packages not in the default channel. + + The structure of the JSON a repository should contain is located in + example-packages.json. + + :param repo: + The URL of the package repository + + :param settings: + A dict containing at least the following fields: + `cache_length`, + `debug`, + `timeout`, + `user_agent` + Optional fields: + `http_proxy`, + `https_proxy`, + `proxy_username`, + `proxy_password`, + `query_string_params` + `install_prereleases` + """ + + def __init__(self, repo, settings): + self.cache = {} + self.repo_info = None + self.schema_version = 0.0 + self.repo = repo + self.settings = settings + self.unavailable_packages = [] + self.failed_sources = {} + self.broken_packages = {} + + @classmethod + def match_url(cls, repo): + """Indicates if this provider can handle the provided repo""" + + return True + + def prefetch(self): + """ + Go out and perform HTTP operations, caching the result + + :raises: + DownloaderException: when there is an issue download package info + ClientException: when there is an issue parsing package info + """ + + [name for name, info in self.get_packages()] + + def get_failed_sources(self): + """ + List of any URLs that could not be accessed while accessing this repository + + :return: + A generator of ("https://example.com", Exception()) tuples + """ + + return self.failed_sources.items() + + def get_broken_packages(self): + """ + List of package names for packages that are missing information + + :return: + A generator of ("Package Name", Exception()) tuples + """ + + return self.broken_packages.items() + + def fetch(self): + """ + Retrieves and loads the JSON for other methods to use + + :raises: + ProviderException: when an error occurs trying to open a file + DownloaderException: when an error occurs trying to open a URL + """ + + if self.repo_info != None: + return + + self.repo_info = self.fetch_location(self.repo) + + if 'includes' not in self.repo_info: + return + + # Allow repositories to include other repositories + if re.match('https?://', self.repo, re.I): + url_pieces = urlparse(self.repo) + domain = url_pieces.scheme + '://' + url_pieces.netloc + path = '/' if url_pieces.path == '' else url_pieces.path + if path[-1] != '/': + path = os.path.dirname(path) + relative_base = domain + path + else: + relative_base = os.path.dirname(self.repo) + '/' + + includes = self.repo_info.get('includes', []) + del self.repo_info['includes'] + for include in includes: + if re.match('^\./|\.\./', include): + include = os.path.normpath(relative_base + include) + include_info = self.fetch_location(include) + included_packages = include_info.get('packages', []) + self.repo_info['packages'].extend(included_packages) + + def fetch_location(self, location): + """ + Fetches the contents of a URL of file path + + :param location: + The URL or file path + + :raises: + ProviderException: when an error occurs trying to open a file + DownloaderException: when an error occurs trying to open a URL + + :return: + A dict of the parsed JSON + """ + + if re.match('https?://', self.repo, re.I): + with downloader(location, self.settings) as manager: + json_string = manager.fetch(location, 'Error downloading repository.') + + # Anything that is not a URL is expected to be a filesystem path + else: + if not os.path.exists(location): + raise ProviderException(u'Error, file %s does not exist' % location) + + if self.settings.get('debug'): + console_write(u'Loading %s as a repository' % location, True) + + # We open as binary so we get bytes like the DownloadManager + with open(location, 'rb') as f: + json_string = f.read() + + try: + return json.loads(json_string.decode('utf-8')) + except (ValueError): + raise ProviderException(u'Error parsing JSON from repository %s.' % location) + + def get_packages(self, invalid_sources=None): + """ + Provides access to the packages in this repository + + :param invalid_sources: + A list of URLs that are permissible to fetch data from + + :raises: + ProviderException: when an error occurs trying to open a file + DownloaderException: when there is an issue download package info + ClientException: when there is an issue parsing package info + + :return: + A generator of + ( + 'Package Name', + { + 'name': name, + 'description': description, + 'author': author, + 'homepage': homepage, + 'last_modified': last modified date, + 'download': { + 'url': url, + 'date': date, + 'version': version + }, + 'previous_names': [old_name, ...], + 'labels': [label, ...], + 'sources': [url, ...], + 'readme': url, + 'issues': url, + 'donate': url, + 'buy': url + } + ) + tuples + """ + + if 'get_packages' in self.cache: + for key, value in self.cache['get_packages'].items(): + yield (key, value) + return + + if invalid_sources != None and self.repo in invalid_sources: + raise StopIteration() + + self.fetch() + + def fail(message): + exception = ProviderException(message) + self.failed_sources[self.repo] = exception + self.cache['get_packages'] = {} + return + schema_error = u'Repository %s does not appear to be a valid repository file because ' % self.repo + + if 'schema_version' not in self.repo_info: + error_string = u'%s the "schema_version" JSON key is missing.' % schema_error + fail(error_string) + return + + try: + self.schema_version = float(self.repo_info.get('schema_version')) + except (ValueError): + error_string = u'%s the "schema_version" is not a valid number.' % schema_error + fail(error_string) + return + + if self.schema_version not in [1.0, 1.1, 1.2, 2.0]: + error_string = u'%s the "schema_version" is not recognized. Must be one of: 1.0, 1.1, 1.2 or 2.0.' % schema_error + fail(error_string) + return + + if 'packages' not in self.repo_info: + error_string = u'%s the "packages" JSON key is missing.' % schema_error + fail(error_string) + return + + github_client = GitHubClient(self.settings) + bitbucket_client = BitBucketClient(self.settings) + + # Backfill the "previous_names" keys for old schemas + previous_names = {} + if self.schema_version < 2.0: + renamed = self.get_renamed_packages() + for old_name in renamed: + new_name = renamed[old_name] + if new_name not in previous_names: + previous_names[new_name] = [] + previous_names[new_name].append(old_name) + + output = {} + for package in self.repo_info['packages']: + info = { + 'sources': [self.repo] + } + + for field in ['name', 'description', 'author', 'last_modified', 'previous_names', + 'labels', 'homepage', 'readme', 'issues', 'donate', 'buy']: + if package.get(field): + info[field] = package.get(field) + + # Schema version 2.0 allows for grabbing details about a pacakge, or its + # download from "details" urls. See the GitHubClient and BitBucketClient + # classes for valid URLs. + if self.schema_version >= 2.0: + details = package.get('details') + releases = package.get('releases') + + # Try to grab package-level details from GitHub or BitBucket + if details: + if invalid_sources != None and details in invalid_sources: + continue + + info['sources'].append(details) + + try: + github_repo_info = github_client.repo_info(details) + bitbucket_repo_info = bitbucket_client.repo_info(details) + + # When grabbing details, prefer explicit field values over the values + # from the GitHub or BitBucket API + if github_repo_info: + info = dict(chain(github_repo_info.items(), info.items())) + elif bitbucket_repo_info: + info = dict(chain(bitbucket_repo_info.items(), info.items())) + else: + raise ProviderException(u'Invalid "details" value "%s" for one of the packages in the repository %s.' % (details, self.repo)) + + except (DownloaderException, ClientException, ProviderException) as e: + if 'name' in info: + self.broken_packages[info['name']] = e + self.failed_sources[details] = e + continue + + # If no releases info was specified, also grab the download info from GH or BB + if not releases and details: + releases = [{'details': details}] + + # This allows developers to specify a GH or BB location to get releases from, + # especially tags URLs (https://github.com/user/repo/tags or + # https://bitbucket.org/user/repo#tags) + info['releases'] = [] + for release in releases: + download_details = None + download_info = {} + + # Make sure that explicit fields are copied over + for field in ['platforms', 'sublime_text', 'version', 'url', 'date']: + if field in release: + download_info[field] = release[field] + + if 'details' in release: + download_details = release['details'] + + try: + github_download = github_client.download_info(download_details) + bitbucket_download = bitbucket_client.download_info(download_details) + + # Overlay the explicit field values over values fetched from the APIs + if github_download: + download_info = dict(chain(github_download.items(), download_info.items())) + # No matching tags + elif github_download == False: + download_info = {} + elif bitbucket_download: + download_info = dict(chain(bitbucket_download.items(), download_info.items())) + # No matching tags + elif bitbucket_download == False: + download_info = {} + else: + raise ProviderException(u'Invalid "details" value "%s" under the "releases" key for the package "%s" in the repository %s.' % (download_details, info['name'], self.repo)) + + except (DownloaderException, ClientException, ProviderException) as e: + if 'name' in info: + self.broken_packages[info['name']] = e + self.failed_sources[download_details] = e + continue + + if download_info: + info['releases'].append(download_info) + + info = self.select_release(info) + + # Schema version 1.0, 1.1 and 1.2 just require that all values be + # explicitly specified in the package JSON + else: + info['platforms'] = package.get('platforms') + info = self.select_platform(info) + + if not info: + self.unavailable_packages.append(package['name']) + continue + + if 'download' not in info and 'releases' not in info: + self.broken_packages[info['name']] = ProviderException(u'No "releases" key for the package "%s" in the repository %s.' % (info['name'], self.repo)) + continue + + for field in ['previous_names', 'labels']: + if field not in info: + info[field] = [] + + for field in ['readme', 'issues', 'donate', 'buy']: + if field not in info: + info[field] = None + + if 'homepage' not in info: + info['homepage'] = self.repo + + if 'download' in info: + # Rewrites the legacy "zipball" URLs to the new "zip" format + info['download']['url'] = re.sub( + '^(https://nodeload.github.com/[^/]+/[^/]+/)zipball(/.*)$', + '\\1zip\\2', info['download']['url']) + + # Rewrites the legacy "nodeload" URLs to the new "codeload" subdomain + info['download']['url'] = info['download']['url'].replace( + 'nodeload.github.com', 'codeload.github.com') + + # Extract the date from the download + if 'last_modified' not in info: + info['last_modified'] = info['download']['date'] + + elif 'releases' in info and 'last_modified' not in info: + # Extract a date from the newest download + date = '1970-01-01 00:00:00' + for release in info['releases']: + if 'date' in release and release['date'] > date: + date = release['date'] + info['last_modified'] = date + + if info['name'] in previous_names: + info['previous_names'].extend(previous_names[info['name']]) + + output[info['name']] = info + yield (info['name'], info) + + self.cache['get_packages'] = output + + def get_renamed_packages(self): + """:return: A dict of the packages that have been renamed""" + + if self.schema_version < 2.0: + return self.repo_info.get('renamed_packages', {}) + + output = {} + for package in self.repo_info['packages']: + if 'previous_names' not in package: + continue + + previous_names = package['previous_names'] + if not isinstance(previous_names, list): + previous_names = [previous_names] + + for previous_name in previous_names: + output[previous_name] = package['name'] + + return output + + def get_unavailable_packages(self): + """ + Provides a list of packages that are unavailable for the current + platform/architecture that Sublime Text is running on. + + This list will be empty unless get_packages() is called first. + + :return: A list of package names + """ + + return self.unavailable_packages diff --git a/sublime/Packages/Package Control/package_control/reloader.py b/sublime/Packages/Package Control/package_control/reloader.py new file mode 100644 index 0000000..0696022 --- /dev/null +++ b/sublime/Packages/Package Control/package_control/reloader.py @@ -0,0 +1,130 @@ +import sys + +import sublime + + +st_version = 2 +# With the way ST3 works, the sublime module is not "available" at startup +# which results in an empty version number +if sublime.version() == '' or int(sublime.version()) > 3000: + st_version = 3 + from imp import reload + + +# Python allows reloading modules on the fly, which allows us to do live upgrades. +# The only caveat to this is that you have to reload in the dependency order. +# +# Thus is module A depends on B and we don't reload B before A, when A is reloaded +# it will still have a reference to the old B. Thus we hard-code the dependency +# order of the various Package Control modules so they get reloaded properly. +# +# There are solutions for doing this all programatically, but this is much easier +# to understand. + +reload_mods = [] +for mod in sys.modules: + if mod[0:15].lower().replace(' ', '_') == 'package_control' and sys.modules[mod] != None: + reload_mods.append(mod) + +mod_prefix = 'package_control' +if st_version == 3: + mod_prefix = 'Package Control.' + mod_prefix + +mods_load_order = [ + '', + + '.sys_path', + '.cache', + '.http_cache', + '.ca_certs', + '.clear_directory', + '.cmd', + '.console_write', + '.preferences_filename', + '.show_error', + '.unicode', + '.thread_progress', + '.package_io', + '.semver', + '.versions', + + '.http', + '.http.invalid_certificate_exception', + '.http.debuggable_http_response', + '.http.debuggable_https_response', + '.http.debuggable_http_connection', + '.http.persistent_handler', + '.http.debuggable_http_handler', + '.http.validating_https_connection', + '.http.validating_https_handler', + + '.clients', + '.clients.client_exception', + '.clients.bitbucket_client', + '.clients.github_client', + '.clients.readme_client', + '.clients.json_api_client', + + '.providers', + '.providers.provider_exception', + '.providers.bitbucket_repository_provider', + '.providers.channel_provider', + '.providers.github_repository_provider', + '.providers.github_user_provider', + '.providers.repository_provider', + '.providers.release_selector', + + '.download_manager', + + '.downloaders', + '.downloaders.downloader_exception', + '.downloaders.rate_limit_exception', + '.downloaders.binary_not_found_error', + '.downloaders.non_clean_exit_error', + '.downloaders.non_http_error', + '.downloaders.caching_downloader', + '.downloaders.decoding_downloader', + '.downloaders.limiting_downloader', + '.downloaders.cert_provider', + '.downloaders.urllib_downloader', + '.downloaders.cli_downloader', + '.downloaders.curl_downloader', + '.downloaders.wget_downloader', + '.downloaders.wininet_downloader', + '.downloaders.background_downloader', + + '.upgraders', + '.upgraders.vcs_upgrader', + '.upgraders.git_upgrader', + '.upgraders.hg_upgrader', + + '.package_manager', + '.package_creator', + '.package_installer', + '.package_renamer', + + '.commands', + '.commands.add_channel_command', + '.commands.add_repository_command', + '.commands.create_binary_package_command', + '.commands.create_package_command', + '.commands.disable_package_command', + '.commands.discover_packages_command', + '.commands.enable_package_command', + '.commands.existing_packages_command', + '.commands.grab_certs_command', + '.commands.install_package_command', + '.commands.list_packages_command', + '.commands.package_message_command', + '.commands.remove_package_command', + '.commands.upgrade_all_packages_command', + '.commands.upgrade_package_command', + + '.package_cleanup', + '.automatic_upgrader' +] + +for suffix in mods_load_order: + mod = mod_prefix + suffix + if mod in reload_mods: + reload(sys.modules[mod]) diff --git a/sublime/Packages/Package Control/package_control/semver.py b/sublime/Packages/Package Control/package_control/semver.py new file mode 100644 index 0000000..917fa77 --- /dev/null +++ b/sublime/Packages/Package Control/package_control/semver.py @@ -0,0 +1,833 @@ +"""pysemver: Semantic Version comparing for Python. + +Provides comparing of semantic versions by using SemVer objects using rich comperations plus the +possibility to match a selector string against versions. Interesting for version dependencies. +Versions look like: "1.7.12+b.133" +Selectors look like: ">1.7.0 || 1.6.9+b.111 - 1.6.9+b.113" + +Example usages: + >>> SemVer(1, 2, 3, build=13) + SemVer("1.2.3+13") + >>> SemVer.valid("1.2.3.4") + False + >>> SemVer.clean("this is unimportant text 1.2.3-2 and will be stripped") + "1.2.3-2" + >>> SemVer("1.7.12+b.133").satisfies(">1.7.0 || 1.6.9+b.111 - 1.6.9+b.113") + True + >>> SemSel(">1.7.0 || 1.6.9+b.111 - 1.6.9+b.113").matches(SemVer("1.7.12+b.133"), + ... SemVer("1.6.9+b.112"), SemVer("1.6.10")) + [SemVer("1.7.12+b.133"), SemVer("1.6.9+b.112")] + >>> min(_) + SemVer("1.6.9+b.112") + >>> _.patch + 9 + +Exported classes: + * SemVer(collections.namedtuple()) + Parses semantic versions and defines methods for them. Supports rich comparisons. + * SemSel(tuple) + Parses semantic version selector strings and defines methods for them. + * SelParseError(Exception) + An error among others raised when parsing a semantic version selector failed. + +Other classes: + * SemComparator(object) + * SemSelAndChunk(list) + * SemSelOrChunk(list) + +Functions/Variables/Constants: + none + + +Copyright (c) 2013 Zachary King, FichteFoll + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and +associated documentation files (the "Software"), to deal in the Software without restriction, +including without limitation the rights to use, copy, modify, merge, publish, distribute, +sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: The above copyright notice and this +permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT +NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES +OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +""" + +import re +import sys +from collections import namedtuple # Python >=2.6 + + +__all__ = ('SemVer', 'SemSel', 'SelParseError') + + +if sys.version_info[0] == 3: + basestring = str + cmp = lambda a, b: (a > b) - (a < b) + + +# @functools.total_ordering would be nice here but was added in 2.7, __cmp__ is not Py3 +class SemVer(namedtuple("_SemVer", 'major, minor, patch, prerelease, build')): + """Semantic Version, consists of 3 to 5 components defining the version's adicity. + + See http://semver.org/ (2.0.0-rc.1) for the standard mainly used for this implementation, few + changes have been made. + + Information on this particular class and their instances: + - Immutable and hashable. + - Subclasses `collections.namedtuple`. + - Always `True` in boolean context. + - len() returns an int between 3 and 5; 4 when a pre-release is set and 5 when a build is + set. Note: Still returns 5 when build is set but not pre-release. + - Parts of the semantic version can be accessed by integer indexing, key (string) indexing, + slicing and getting an attribute. Returned slices are tuple. Leading '-' and '+' of + optional components are not stripped. Supported keys/attributes: + major, minor, patch, prerelease, build. + + Examples: + s = SemVer("1.2.3-4.5+6") + s[2] == 3 + s[:3] == (1, 2, 3) + s['build'] == '-4.5' + s.major == 1 + + Short information on semantic version structure: + + Semantic versions consist of: + * a major component (numeric) + * a minor component (numeric) + * a patch component (numeric) + * a pre-release component [optional] + * a build component [optional] + + The pre-release component is indicated by a hyphen '-' and followed by alphanumeric[1] sequences + separated by dots '.'. Sequences are compared numerically if applicable (both sequences of two + versions are numeric) or lexicographically. May also include hyphens. The existence of a + pre-release component lowers the actual version; the shorter pre-release component is considered + lower. An 'empty' pre-release component is considered to be the least version for this + major-minor-patch combination (e.g. "1.0.0-"). + + The build component may follow the optional pre-release component and is indicated by a plus '+' + followed by sequences, just as the pre-release component. Comparing works similarly. However the + existence of a build component raises the actual version and may also raise a pre-release. An + 'empty' build component is considered to be the highest version for this + major-minor-patch-prerelease combination (e.g. "1.2.3+"). + + + [1]: Regexp for a sequence: r'[0-9A-Za-z-]+'. + """ + + # Static class variables + _base_regex = r'''(?x) + (?P[0-9]+) + \.(?P[0-9]+) + \.(?P[0-9]+) + (?:\-(?P(?:[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?))? + (?:\+(?P(?:[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?))?''' + _search_regex = re.compile(_base_regex) + _match_regex = re.compile('^%s$' % _base_regex) # required because of $ anchor + + # "Constructor" + def __new__(cls, *args, **kwargs): + """There are two different constructor styles that are allowed: + - Option 1 allows specification of a semantic version as a string and the option to "clean" + the string before parsing it. + - Option 2 allows specification of each component separately as one parameter. + + Note that all the parameters specified in the following sections can be passed either as + positional or as named parameters while considering the usual Python rules for this. As + such, `SemVer(1, 2, minor=1)` will result in an exception and not in `SemVer("1.1.2")`. + + Option 1: + Constructor examples: + SemVer("1.0.1") + SemVer("this version 1.0.1-pre.1 here", True) + SemVer(ver="0.0.9-pre-alpha+34", clean=False) + + Parameters: + * ver (str) + The string containing the version. + * clean = `False` (bool; optional) + If this is true in boolean context, `SemVer.clean(ver)` is called before + parsing. + + Option 2: + Constructor examples: + SemVer(1, 0, 1) + SemVer(1, '0', prerelease='pre-alpha', patch=1, build=34) + SemVer(**dict(minor=2, major=1, patch=3)) + + Parameters: + * major (int, str, float ...) + * minor (...) + * patch (...) + Major to patch components must be an integer or convertable to an int (e.g. a + string or another number type). + + * prerelease = `None` (str, int, float ...; optional) + * build = `None` (...; optional) + Pre-release and build components should be a string (or number) type. + Will be passed to `str()` if not already a string but the final string must + match '^[0-9A-Za-z.-]*$' + + Raises: + * TypeError + Invalid parameter type(s) or combination (e.g. option 1 and 2). + * ValueError + Invalid semantic version or option 2 parameters unconvertable. + """ + ver, clean, comps = None, False, None + kw, l = kwargs.copy(), len(args) + len(kwargs) + + def inv(): + raise TypeError("Invalid parameter combination: args=%s; kwargs=%s" % (args, kwargs)) + + # Do validation and parse the parameters + if l == 0 or l > 5: + raise TypeError("SemVer accepts at least 1 and at most 5 arguments (%d given)" % l) + + elif l < 3: + if len(args) == 2: + ver, clean = args + else: + ver = args[0] if args else kw.pop('ver', None) + clean = kw.pop('clean', clean) + if kw: + inv() + + else: + comps = list(args) + [kw.pop(cls._fields[k], None) for k in range(len(args), 5)] + if kw or any(comps[i] is None for i in range(3)): + inv() + + typecheck = (int,) * 3 + (basestring,) * 2 + for i, (v, t) in enumerate(zip(comps, typecheck)): + if v is None: + continue + elif not isinstance(v, t): + try: + if i < 3: + v = typecheck[i](v) + else: # The real `basestring` can not be instatiated (Py2) + v = str(v) + except ValueError as e: + # Modify the exception message. I can't believe this actually works + e.args = ("Parameter #%d must be of type %s or convertable" + % (i, t.__name__),) + raise + else: + comps[i] = v + if t is basestring and not re.match(r"^[0-9A-Za-z.-]*$", v): + raise ValueError("Build and pre-release strings must match '^[0-9A-Za-z.-]*$'") + + # Final adjustments + if not comps: + if ver is None or clean is None: + inv() + ver = clean and cls.clean(ver) or ver + comps = cls._parse(ver) + + # Create the obj + return super(SemVer, cls).__new__(cls, *comps) + + # Magic methods + def __str__(self): + return ('.'.join(map(str, self[:3])) + + ('-' + self.prerelease if self.prerelease is not None else '') + + ('+' + self.build if self.build is not None else '')) + + def __repr__(self): + # Use the shortest representation - what would you prefer? + return 'SemVer("%s")' % str(self) + # return 'SemVer(%s)' % ', '.join('%s=%r' % (k, getattr(self, k)) for k in self._fields) + + def __len__(self): + return 3 + (self.build is not None and 2 or self.prerelease is not None) + + # Magic rich comparing methods + def __gt__(self, other): + return self._compare(other) == 1 if isinstance(other, SemVer) else NotImplemented + + def __eq__(self, other): + return self._compare(other) == 0 if isinstance(other, SemVer) else NotImplemented + + def __lt__(self, other): + return not (self > other or self == other) + + def __ge__(self, other): + return not (self < other) + + def __le__(self, other): + return not (self > other) + + def __ne__(self, other): + return not (self == other) + + # Utility (class-)methods + def satisfies(self, sel): + """Alias for `bool(sel.matches(self))` or `bool(SemSel(sel).matches(self))`. + + See `SemSel.__init__()` and `SemSel.matches(*vers)` for possible exceptions. + + Returns: + * bool: `True` if the version matches the passed selector, `False` otherwise. + """ + if not isinstance(sel, SemSel): + sel = SemSel(sel) # just "re-raise" exceptions + + return bool(sel.matches(self)) + + @classmethod + def valid(cls, ver): + """Check if `ver` is a valid semantic version. Classmethod. + + Parameters: + * ver (str) + The string that should be stripped. + + Raises: + * TypeError + Invalid parameter type. + + Returns: + * bool: `True` if it is valid, `False` otherwise. + """ + if not isinstance(ver, basestring): + raise TypeError("%r is not a string" % ver) + + if cls._match_regex.match(ver): + return True + else: + return False + + @classmethod + def clean(cls, vers): + """Remove everything before and after a valid version string. Classmethod. + + Parameters: + * vers (str) + The string that should be stripped. + + Raises: + * TypeError + Invalid parameter type. + + Returns: + * str: The stripped version string. Only the first version is matched. + * None: No version found in the string. + """ + if not isinstance(vers, basestring): + raise TypeError("%r is not a string" % vers) + m = cls._search_regex.search(vers) + if m: + return vers[m.start():m.end()] + else: + return None + + # Private (class-)methods + @classmethod + def _parse(cls, ver): + """Private. Do not touch. Classmethod. + """ + if not isinstance(ver, basestring): + raise TypeError("%r is not a string" % ver) + + match = cls._match_regex.match(ver) + + if match is None: + raise ValueError("'%s' is not a valid SemVer string" % ver) + + g = list(match.groups()) + for i in range(3): + g[i] = int(g[i]) + + return g # Will be passed as namedtuple(...)(*g) + + def _compare(self, other): + """Private. Do not touch. + self > other: 1 + self = other: 0 + self < other: -1 + """ + # Shorthand lambdas + cp_len = lambda t, i=0: cmp(len(t[i]), len(t[not i])) + + for i, (x1, x2) in enumerate(zip(self, other)): + if i > 2: + if x1 is None and x2 is None: + continue + + # self is greater when other has a prerelease but self doesn't + # self is less when other has a build but self doesn't + if x1 is None or x2 is None: + return int(2 * (i - 3.5)) * (1 - 2 * (x1 is None)) + + # self is less when other's build is empty + if i == 4 and (not x1 or not x2) and x1 != x2: + return 1 - 2 * bool(x1) + + # Split by '.' and use numeric comp or lexicographical order + t2 = [x1.split('.'), x2.split('.')] + for y1, y2 in zip(*t2): + if y1.isdigit() and y2.isdigit(): + y1 = int(y1) + y2 = int(y2) + if y1 > y2: + return 1 + elif y1 < y2: + return -1 + + # The "longer" sub-version is greater + d = cp_len(t2) + if d: + return d + else: + if x1 > x2: + return 1 + elif x1 < x2: + return -1 + + # The versions equal + return 0 + + +class SemComparator(object): + """Holds a SemVer object and a comparing operator and can match these against a given version. + + Constructor: SemComparator('<=', SemVer("1.2.3")) + + Methods: + * matches(ver) + """ + # Private properties + _ops = { + '>=': '__ge__', + '<=': '__le__', + '>': '__gt__', + '<': '__lt__', + '=': '__eq__', + '!=': '__ne__' + } + _ops_satisfy = ('~', '!') + + # Constructor + def __init__(self, op, ver): + """Constructor examples: + SemComparator('<=', SemVer("1.2.3")) + SemComparator('!=', SemVer("2.3.4")) + + Parameters: + * op (str, False, None) + One of [>=, <=, >, <, =, !=, !, ~] or evaluates to `False` which defaults to '~'. + '~' means a "satisfy" operation where pre-releases and builds are ignored. + '!' is a negative "~". + * ver (SemVer) + Holds the version to compare with. + + Raises: + * ValueError + Invalid `op` parameter. + * TypeError + Invalid `ver` parameter. + """ + super(SemComparator, self).__init__() + + if op and op not in self._ops_satisfy and op not in self._ops: + raise ValueError("Invalid value for `op` parameter.") + if not isinstance(ver, SemVer): + raise TypeError("`ver` parameter is not instance of SemVer.") + + # Default to '~' for versions with no build or pre-release + op = op or '~' + # Fallback to '=' and '!=' if len > 3 + if len(ver) != 3: + if op == '~': + op = '=' + if op == '!': + op = '!=' + + self.op = op + self.ver = ver + + # Magic methods + def __str__(self): + return (self.op or "") + str(self.ver) + + # Utility methods + def matches(self, ver): + """Match the internal version (constructor) against `ver`. + + Parameters: + * ver (SemVer) + + Raises: + * TypeError + Could not compare `ver` against the version passed in the constructor with the + passed operator. + + Returns: + * bool + `True` if the version matched the specified operator and internal version, `False` + otherwise. + """ + if self.op in self._ops_satisfy: + # Compare only the first three parts (which are tuples) and directly + return bool((self.ver[:3] == ver[:3]) + (self.op == '!') * -1) + ret = getattr(ver, self._ops[self.op])(self.ver) + if ret == NotImplemented: + raise TypeError("Unable to compare %r with operator '%s'" % (ver, self.op)) + return ret + + +class SemSelAndChunk(list): + """Extends list and defines a few methods used for matching versions. + + New elements should be added by calling `.add_child(op, ver)` which creates a SemComparator + instance and adds that to itself. + + Methods: + * matches(ver) + * add_child(op, ver) + """ + # Magic methods + def __str__(self): + return ' '.join(map(str, self)) + + # Utitlity methods + def matches(self, ver): + """Match all of the added children against `ver`. + + Parameters: + * ver (SemVer) + + Raises: + * TypeError + Invalid `ver` parameter. + + Returns: + * bool: + `True` if *all* of the SemComparator children match `ver`, `False` otherwise. + """ + if not isinstance(ver, SemVer): + raise TypeError("`ver` parameter is not instance of SemVer.") + return all(cp.matches(ver) for cp in self) + + def add_child(self, op, ver): + """Create a SemComparator instance with the given parameters and appends that to self. + + Parameters: + * op (str) + * ver (SemVer) + Both parameters are forwarded to `SemComparator.__init__`, see there for a more detailed + description. + + Raises: + Exceptions raised by `SemComparator.__init__`. + """ + self.append(SemComparator(op, SemVer(ver))) + + +class SemSelOrChunk(list): + """Extends list and defines a few methods used for matching versions. + + New elements should be added by calling `.new_child()` which returns a SemSelAndChunk + instance. + + Methods: + * matches(ver) + * new_child() + """ + # Magic methods + def __str__(self): + return ' || '.join(map(str, self)) + + # Utility methods + def matches(self, ver): + """Match all of the added children against `ver`. + + Parameters: + * ver (SemVer) + + Raises: + * TypeError + Invalid `ver` parameter. + + Returns: + * bool + `True` if *any* of the SemSelAndChunk children matches `ver`. + `False` otherwise. + """ + if not isinstance(ver, SemVer): + raise TypeError("`ver` parameter is not instance of SemVer.") + return any(ch.matches(ver) for ch in self) + + def new_child(self): + """Creates a new SemSelAndChunk instance, appends it to self and returns it. + + Returns: + * SemSelAndChunk: An empty instance. + """ + ch = SemSelAndChunk() + self.append(ch) + return ch + + +class SelParseError(Exception): + """An Exception raised when parsing a semantic selector failed. + """ + pass + + +# Subclass `tuple` because this is a somewhat simple method to make this immutable +class SemSel(tuple): + """A Semantic Version Selector, holds a selector and can match it against semantic versions. + + Information on this particular class and their instances: + - Immutable but not hashable because the content within might have changed. + - Subclasses `tuple` but does not behave like one. + - Always `True` in boolean context. + - len() returns the number of containing *and chunks* (see below). + - Iterable, iterates over containing *and chunks*. + + When talking about "versions" it refers to a semantic version (SemVer). For information on how + versions compare to one another, see SemVer's doc string. + + List for **comparators**: + "1.0.0" matches the version 1.0.0 and all its pre-release and build variants + "!1.0.0" matches any version that is not 1.0.0 or any of its variants + "=1.0.0" matches only the version 1.0.0 + "!=1.0.0" matches any version that is not 1.0.0 + ">=1.0.0" matches versions greater than or equal 1.0.0 + "<1.0.0" matches versions smaller than 1.0.0 + "1.0.0 - 1.0.3" matches versions greater than or equal 1.0.0 thru 1.0.3 + "~1.0" matches versions greater than or equal 1.0.0 thru 1.0.9999 (and more) + "~1", "1.x", "1.*" match versions greater than or equal 1.0.0 thru 1.9999.9999 (and more) + "~1.1.2" matches versions greater than or equal 1.1.2 thru 1.1.9999 (and more) + "~1.1.2+any" matches versions greater than or equal 1.1.2+any thru 1.1.9999 (and more) + "*", "~", "~x" match any version + + Multiple comparators can be combined by using ' ' spaces and every comparator must match to make + the **and chunk** match a version. + Multiple and chunks can be combined to **or chunks** using ' || ' and match if any of the and + chunks split by these matches. + + A complete example would look like: + ~1 || 0.0.3 || <0.0.2 >0.0.1+b.1337 || 2.0.x || 2.1.0 - 2.1.0+b.12 !=2.1.0+b.9 + + Methods: + * matches(*vers) + """ + # Private properties + _fuzzy_regex = re.compile(r'''(?x)^ + (?P[<>]=?|~>?=?)? + (?:(?P\d+) + (?:\.(?P\d+) + (?:\.(?P\d+) + (?P[-+][a-zA-Z0-9-+.]*)? + )? + )? + )?$''') + _xrange_regex = re.compile(r'''(?x)^ + (?P[<>]=?|~>?=?)? + (?:(?P\d+|[xX*]) + (?:\.(?P\d+|[xX*]) + (?:\.(?P\d+|[xX*]))? + )? + ) + (?P.*)$''') + _split_op_regex = re.compile(r'^(?P=|[<>!]=?)?(?P.*)$') + + # "Constructor" + def __new__(cls, sel): + """Constructor examples: + SemSel(">1.0.0") + SemSel("~1.2.9 !=1.2.12") + + Parameters: + * sel (str) + A version selector string. + + Raises: + * TypeError + `sel` parameter is not a string. + * ValueError + A version in the selector could not be matched as a SemVer. + * SemParseError + The version selector's syntax is unparsable; invalid ranges (fuzzy, xrange or + explicit range) or invalid '||' + """ + chunk = cls._parse(sel) + return super(SemSel, cls).__new__(cls, (chunk,)) + + # Magic methods + def __str__(self): + return str(self._chunk) + + def __repr__(self): + return 'SemSel("%s")' % self._chunk + + def __len__(self): + # What would you expect? + return len(self._chunk) + + def __iter__(self): + return iter(self._chunk) + + # Read-only (private) attributes + @property + def _chunk(self): + return self[0] + + # Utility methods + def matches(self, *vers): + """Match the selector against a selection of versions. + + Parameters: + * *vers (str, SemVer) + Versions can be passed as strings and SemVer objects will be created with them. + May also be a mixed list. + + Raises: + * TypeError + A version is not an instance of str (basestring) or SemVer. + * ValueError + A string version could not be parsed as a SemVer. + + Returns: + * list + A list with all the versions that matched, may be empty. Use `max()` to determine + the highest matching version, or `min()` for the lowest. + """ + ret = [] + for v in vers: + if isinstance(v, str): + t = self._chunk.matches(SemVer(v)) + elif isinstance(v, SemVer): + t = self._chunk.matches(v) + else: + raise TypeError("Invalid parameter type '%s': %s" % (v, type(v))) + if t: + ret.append(v) + + return ret + + # Private methods + @classmethod + def _parse(cls, sel): + """Private. Do not touch. + + 1. split by whitespace into tokens + a. start new and_chunk on ' || ' + b. parse " - " ranges + c. replace "xX*" ranges with "~" equivalent + d. parse "~" ranges + e. parse unmatched token as comparator + ~. append to current and_chunk + 2. return SemSelOrChunk + + Raises TypeError, ValueError or SelParseError. + """ + if not isinstance(sel, basestring): + raise TypeError("Selector must be a string") + if not sel: + raise ValueError("String must not be empty") + + # Split selector by spaces and crawl the tokens + tokens = sel.split() + i = -1 + or_chunk = SemSelOrChunk() + and_chunk = or_chunk.new_child() + + while i + 1 < len(tokens): + i += 1 + t = tokens[i] + + # Replace x ranges with ~ selector + m = cls._xrange_regex.match(t) + m = m and m.groups('') + if m and any(not x.isdigit() for x in m[1:4]) and not m[0].startswith('>'): + # (do not match '>1.0' or '>*') + if m[4]: + raise SelParseError("XRanges do not allow pre-release or build components") + + # Only use digit parts and fail if digit found after non-digit + mm, xran = [], False + for x in m[1:4]: + if x.isdigit(): + if xran: + raise SelParseError("Invalid fuzzy range or XRange '%s'" % tokens[i]) + mm.append(x) + else: + xran = True + t = m[0] + '.'.join(mm) # x for x in m[1:4] if x.isdigit()) + # Append "~" if not already present + if not t.startswith('~'): + t = '~' + t + + # switch t: + if t == '||': + if i == 0 or tokens[i - 1] == '||' or i + 1 == len(tokens): + raise SelParseError("OR range must not be empty") + # Start a new and_chunk + and_chunk = or_chunk.new_child() + + elif t == '-': + # ' - ' range + i += 1 + invalid = False + try: + # If these result in exceptions, you know you're doing it wrong + t = tokens[i] + c = and_chunk[-1] + except: + raise SelParseError("Invalid ' - ' range position") + + # If there is an op in front of one of the bound versions + invalid = (c.op not in ('=', '~') + or cls._split_op_regex.match(t).group(1) not in (None, '=')) + if invalid: + raise SelParseError("Invalid ' - ' range '%s - %s'" + % (tokens[i - 2], tokens[i])) + + c.op = ">=" + and_chunk.add_child('<=', t) + + elif t == '': + # Multiple spaces + pass + + elif t.startswith('~'): + m = cls._fuzzy_regex.match(t) + if not m: + raise SelParseError("Invalid fuzzy range or XRange '%s'" % tokens[i]) + + mm, m = m.groups('')[1:4], m.groupdict('') # mm: major to patch + + # Minimum requirement + min_ver = ('.'.join(x or '0' for x in mm) + '-' + if not m['other'] + else cls._split_op_regex(t[1:]).group('ver')) + and_chunk.add_child('>=', min_ver) + + if m['major']: + # Increase version before none (or second to last if '~1.2.3') + e = [0, 0, 0] + for j, d in enumerate(mm): + if not d or j == len(mm) - 1: + e[j - 1] = e[j - 1] + 1 + break + e[j] = int(d) + + and_chunk.add_child('<', '.'.join(str(x) for x in e) + '-') + + # else: just plain '~' or '*', or '~>X' which are already handled + + else: + # A normal comparator + m = cls._split_op_regex.match(t).groupdict() # this regex can't fail + and_chunk.add_child(**m) + + # Finally return the or_chunk + return or_chunk \ No newline at end of file diff --git a/sublime/Packages/Package Control/package_control/show_error.py b/sublime/Packages/Package Control/package_control/show_error.py new file mode 100644 index 0000000..b8169c9 --- /dev/null +++ b/sublime/Packages/Package Control/package_control/show_error.py @@ -0,0 +1,12 @@ +import sublime + + +def show_error(string): + """ + Displays an error message with a standard "Package Control" header + + :param string: + The error to display + """ + + sublime.error_message(u'Package Control\n\n%s' % string) diff --git a/sublime/Packages/Package Control/package_control/sys_path.py b/sublime/Packages/Package Control/package_control/sys_path.py new file mode 100644 index 0000000..10daa3d --- /dev/null +++ b/sublime/Packages/Package Control/package_control/sys_path.py @@ -0,0 +1,27 @@ +import sys +import os + +if os.name == 'nt': + from ctypes import windll, create_unicode_buffer + +import sublime + + +def add_to_path(path): + # Python 2.x on Windows can't properly import from non-ASCII paths, so + # this code added the DOC 8.3 version of the lib folder to the path in + # case the user's username includes non-ASCII characters + if os.name == 'nt': + buf = create_unicode_buffer(512) + if windll.kernel32.GetShortPathNameW(path, buf, len(buf)): + path = buf.value + + if path not in sys.path: + sys.path.append(path) + + +lib_folder = os.path.join(sublime.packages_path(), 'Package Control', 'lib') +add_to_path(os.path.join(lib_folder, 'all')) + +if os.name == 'nt': + add_to_path(os.path.join(lib_folder, 'windows')) diff --git a/sublime/Packages/Package Control/package_control/thread_progress.py b/sublime/Packages/Package Control/package_control/thread_progress.py new file mode 100644 index 0000000..b40c564 --- /dev/null +++ b/sublime/Packages/Package Control/package_control/thread_progress.py @@ -0,0 +1,46 @@ +import sublime + + +class ThreadProgress(): + """ + Animates an indicator, [= ], in the status area while a thread runs + + :param thread: + The thread to track for activity + + :param message: + The message to display next to the activity indicator + + :param success_message: + The message to display once the thread is complete + """ + + def __init__(self, thread, message, success_message): + self.thread = thread + self.message = message + self.success_message = success_message + self.addend = 1 + self.size = 8 + sublime.set_timeout(lambda: self.run(0), 100) + + def run(self, i): + if not self.thread.is_alive(): + if hasattr(self.thread, 'result') and not self.thread.result: + sublime.status_message('') + return + sublime.status_message(self.success_message) + return + + before = i % self.size + after = (self.size - 1) - before + + sublime.status_message('%s [%s=%s]' % \ + (self.message, ' ' * before, ' ' * after)) + + if not after: + self.addend = -1 + if not before: + self.addend = 1 + i += self.addend + + sublime.set_timeout(lambda: self.run(i), 100) diff --git a/sublime/Packages/Package Control/package_control/unicode.py b/sublime/Packages/Package Control/package_control/unicode.py new file mode 100644 index 0000000..f0464a2 --- /dev/null +++ b/sublime/Packages/Package Control/package_control/unicode.py @@ -0,0 +1,49 @@ +import os +import locale +import sys + + +# Sublime Text on OS X does not seem to report the correct encoding +# so we hard-code that to UTF-8 +_encoding = 'utf-8' if sys.platform == 'darwin' else locale.getpreferredencoding() + +_fallback_encodings = ['utf-8', 'cp1252'] + + +def unicode_from_os(e): + """ + This is needed as some exceptions coming from the OS are + already encoded and so just calling unicode(e) will result + in an UnicodeDecodeError as the string isn't in ascii form. + + :param e: + The exception to get the value of + + :return: + The unicode version of the exception message + """ + + if sys.version_info >= (3,): + return str(e) + + try: + if isinstance(e, Exception): + e = e.message + + if isinstance(e, unicode): + return e + + if isinstance(e, int): + e = str(e) + + return unicode(e, _encoding) + + # If the "correct" encoding did not work, try some defaults, and then just + # obliterate characters that we can't seen to decode properly + except UnicodeDecodeError: + for encoding in _fallback_encodings: + try: + return unicode(e, encoding, errors='strict') + except: + pass + return unicode(e, errors='replace') diff --git a/sublime/Packages/Package Control/package_control/upgraders/__init__.py b/sublime/Packages/Package Control/package_control/upgraders/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/sublime/Packages/Package Control/package_control/upgraders/git_upgrader.py b/sublime/Packages/Package Control/package_control/upgraders/git_upgrader.py new file mode 100644 index 0000000..878b1fd --- /dev/null +++ b/sublime/Packages/Package Control/package_control/upgraders/git_upgrader.py @@ -0,0 +1,106 @@ +import os + +from ..cache import set_cache, get_cache +from ..show_error import show_error +from .vcs_upgrader import VcsUpgrader + + +class GitUpgrader(VcsUpgrader): + """ + Allows upgrading a local git-repository-based package + """ + + cli_name = 'git' + + def retrieve_binary(self): + """ + Returns the path to the git executable + + :return: The string path to the executable or False on error + """ + + name = 'git' + if os.name == 'nt': + name += '.exe' + binary = self.find_binary(name) + if binary and os.path.isdir(binary): + full_path = os.path.join(binary, name) + if os.path.exists(full_path): + binary = full_path + if not binary: + show_error((u'Unable to find %s. Please set the git_binary setting by accessing the ' + + u'Preferences > Package Settings > Package Control > Settings \u2013 User menu entry. ' + + u'The Settings \u2013 Default entry can be used for reference, but changes to that will be ' + + u'overwritten upon next upgrade.') % name) + return False + + if os.name == 'nt': + tortoise_plink = self.find_binary('TortoisePlink.exe') + if tortoise_plink: + os.environ.setdefault('GIT_SSH', tortoise_plink) + return binary + + def get_working_copy_info(self): + binary = self.retrieve_binary() + if not binary: + return False + + # Get the current branch name + res = self.execute([binary, 'symbolic-ref', '-q', 'HEAD'], self.working_copy) + branch = res.replace('refs/heads/', '') + + # Figure out the remote and the branch name on the remote + remote = self.execute([binary, 'config', '--get', 'branch.%s.remote' % branch], self.working_copy) + res = self.execute([binary, 'config', '--get', 'branch.%s.merge' % branch], self.working_copy) + remote_branch = res.replace('refs/heads/', '') + + return { + 'branch': branch, + 'remote': remote, + 'remote_branch': remote_branch + } + + def run(self): + """ + Updates the repository with remote changes + + :return: False or error, or True on success + """ + + binary = self.retrieve_binary() + if not binary: + return False + + info = self.get_working_copy_info() + + args = [binary] + args.extend(self.update_command) + args.extend([info['remote'], info['remote_branch']]) + self.execute(args, self.working_copy) + return True + + def incoming(self): + """:return: bool if remote revisions are available""" + + cache_key = self.working_copy + '.incoming' + incoming = get_cache(cache_key) + if incoming != None: + return incoming + + binary = self.retrieve_binary() + if not binary: + return False + + info = self.get_working_copy_info() + + res = self.execute([binary, 'fetch', info['remote']], self.working_copy) + if res == False: + return False + + args = [binary, 'log'] + args.append('..%s/%s' % (info['remote'], info['remote_branch'])) + output = self.execute(args, self.working_copy) + incoming = len(output) > 0 + + set_cache(cache_key, incoming, self.cache_length) + return incoming diff --git a/sublime/Packages/Package Control/package_control/upgraders/hg_upgrader.py b/sublime/Packages/Package Control/package_control/upgraders/hg_upgrader.py new file mode 100644 index 0000000..36dfb48 --- /dev/null +++ b/sublime/Packages/Package Control/package_control/upgraders/hg_upgrader.py @@ -0,0 +1,74 @@ +import os + +from ..cache import set_cache, get_cache +from ..show_error import show_error +from .vcs_upgrader import VcsUpgrader + + +class HgUpgrader(VcsUpgrader): + """ + Allows upgrading a local mercurial-repository-based package + """ + + cli_name = 'hg' + + def retrieve_binary(self): + """ + Returns the path to the hg executable + + :return: The string path to the executable or False on error + """ + + name = 'hg' + if os.name == 'nt': + name += '.exe' + binary = self.find_binary(name) + if binary and os.path.isdir(binary): + full_path = os.path.join(binary, name) + if os.path.exists(full_path): + binary = full_path + if not binary: + show_error((u'Unable to find %s. Please set the hg_binary setting by accessing the ' + + u'Preferences > Package Settings > Package Control > Settings \u2013 User menu entry. ' + + u'The Settings \u2013 Default entry can be used for reference, but changes to that will be ' + + u'overwritten upon next upgrade.') % name) + return False + return binary + + def run(self): + """ + Updates the repository with remote changes + + :return: False or error, or True on success + """ + + binary = self.retrieve_binary() + if not binary: + return False + args = [binary] + args.extend(self.update_command) + args.append('default') + self.execute(args, self.working_copy) + return True + + def incoming(self): + """:return: bool if remote revisions are available""" + + cache_key = self.working_copy + '.incoming' + incoming = get_cache(cache_key) + if incoming != None: + return incoming + + binary = self.retrieve_binary() + if not binary: + return False + + args = [binary, 'in', '-q', 'default'] + output = self.execute(args, self.working_copy) + if output == False: + return False + + incoming = len(output) > 0 + + set_cache(cache_key, incoming, self.cache_length) + return incoming diff --git a/sublime/Packages/Package Control/package_control/upgraders/vcs_upgrader.py b/sublime/Packages/Package Control/package_control/upgraders/vcs_upgrader.py new file mode 100644 index 0000000..d82abe7 --- /dev/null +++ b/sublime/Packages/Package Control/package_control/upgraders/vcs_upgrader.py @@ -0,0 +1,27 @@ +from ..cmd import create_cmd, Cli + + +class VcsUpgrader(Cli): + """ + Base class for updating packages that are a version control repository on local disk + + :param vcs_binary: + The full filesystem path to the executable for the version control + system. May be set to None to allow the code to try and find it. + + :param update_command: + The command to pass to the version control executable to update the + repository. + + :param working_copy: + The local path to the working copy/package directory + + :param cache_length: + The lenth of time to cache if incoming changesets are available + """ + + def __init__(self, vcs_binary, update_command, working_copy, cache_length, debug): + self.update_command = update_command + self.working_copy = working_copy + self.cache_length = cache_length + super(VcsUpgrader, self).__init__(vcs_binary, debug) diff --git a/sublime/Packages/Package Control/package_control/versions.py b/sublime/Packages/Package Control/package_control/versions.py new file mode 100644 index 0000000..90a5ef6 --- /dev/null +++ b/sublime/Packages/Package Control/package_control/versions.py @@ -0,0 +1,81 @@ +import re + +from .semver import SemVer +from .console_write import console_write + + +def semver_compat(v): + if isinstance(v, SemVer): + return str(v) + + # Allowing passing in a dict containing info about a package + if isinstance(v, dict): + if 'version' not in v: + return '0' + v = v['version'] + + # Trim v off of the front + v = re.sub('^v', '', v) + + # We prepend 0 to all date-based version numbers so that developers + # may switch to explicit versioning from GitHub/BitBucket + # versioning based on commit dates. + # + # When translating dates into semver, the way to get each date + # segment into the version is to treat the year and month as + # minor and patch, and then the rest as a numeric build version + # with four different parts. The result looks like: + # 0.2012.11+10.31.23.59 + date_match = re.match('(\d{4})\.(\d{2})\.(\d{2})\.(\d{2})\.(\d{2})\.(\d{2})$', v) + if date_match: + v = '0.%s.%s+%s.%s.%s.%s' % date_match.groups() + + # This handles version that were valid pre-semver with 4+ dotted + # groups, such as 1.6.9.0 + four_plus_match = re.match('(\d+\.\d+\.\d+)[T\.](\d+(\.\d+)*)$', v) + if four_plus_match: + v = '%s+%s' % (four_plus_match.group(1), four_plus_match.group(2)) + + # Semver must have major, minor, patch + elif re.match('^\d+$', v): + v += '.0.0' + elif re.match('^\d+\.\d+$', v): + v += '.0' + return v + + +def version_comparable(string): + return SemVer(semver_compat(string)) + + +def version_exclude_prerelease(versions): + output = [] + for version in versions: + if SemVer(semver_compat(version)).prerelease != None: + continue + output.append(version) + return output + + +def version_filter(versions, allow_prerelease=False): + output = [] + for version in versions: + no_v_version = re.sub('^v', '', version) + if not SemVer.valid(no_v_version): + continue + if not allow_prerelease and SemVer(no_v_version).prerelease != None: + continue + output.append(version) + return output + + +def _version_sort_key(item): + return SemVer(semver_compat(item)) + + +def version_sort(sortable, **kwargs): + try: + return sorted(sortable, key=_version_sort_key, **kwargs) + except (ValueError) as e: + console_write(u"Error sorting versions - %s" % e, True) + return [] diff --git a/sublime/Packages/Package Control/readme.creole b/sublime/Packages/Package Control/readme.creole index eb37565..50b3d69 100644 --- a/sublime/Packages/Package Control/readme.creole +++ b/sublime/Packages/Package Control/readme.creole @@ -1,7 +1,7 @@ = Sublime Package Control -A Sublime Text 2 (http://www.sublimetext.com/2) package manager for easily -discovering, install, upgrading and removing packages. Also includes an +A Sublime Text 2/3 (http://www.sublimetext.com) package manager for easily +discovering, installing, upgrading and removing packages. Also includes an automatic updater and package creation tool. Packages can be installed from GitHub, BitBucket or custom package repositories. @@ -14,10 +14,12 @@ instructions, screenshots and documentation. == License -Sublime Package Control (except for the ntlm library) is licensed under the MIT -license. +Sublime Package Control is licensed under the MIT license. - Copyright (c) 2011-2012 Will Bond +All of the source code (except for package_control/semver.py), is under the +license: + + Copyright (c) 2011-2013 Will Bond Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal @@ -37,7 +39,24 @@ license. OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -The ntlm library (for Windows) is from the python-ntlm project -(http://code.google.com/p/python-ntlm/) and is licensed under the GNU Lesser -General Public License (LGPL). Details can be found in the source files -located in lib/windows/ntlm/. \ No newline at end of file +package_control/semver.py is under the license: + + Copyright (c) 2013 Zachary King, FichteFoll + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + THE SOFTWARE. diff --git a/sublime/Packages/Theme - Default/arrow_right@2x.png b/sublime/Packages/Theme - Default/arrow_right@2x.png index 9f40cd3362349827d967c03464ddddf4336f46d9..b1d1a117baf76a2b16d04f0c93f55f08a4a142c4 100644 GIT binary patch literal 1639 zcmeAS@N?(olHy`uVBq!ia0vp^Ne?g*F{zaLoz;FhIrk$aUK86^Q z55aPgL8-<0Ii+Cr#%88w2KvSpFn!2!==z-Va|?=6i;F?_0K*Y%7P1(+dW1Ehd6^|} zr=ba>>xw|g+o8J-Sq`d?YC)e`k%=SdK{A*@4-&Nkr;rGj%-qyGaIhEy(+E}xBwvE% zo%3_@i-HRhlT+=?Oihh}!HF)6t^;gUB*H2qGi0lfB=80dB)EY2*p3UBnqg&v9aoox z%|iwT#_OIgjv*erZ>Jb~A9fHqK0Bc+fJOb_%EeYccw_!ZG+o%};do)`mP}6BNnB12 z1p-SRsnjuuJIch|Y4Kas?ac1@DkLDo>(&3M)8Zz5i*xdSaiO_+_xF2y|LecM>-zfp z@0FDjzib=?4l}9T>0G)&anJgzS*P0)-#pg4JA2tpuHT_=Z(b8zsD43!J-kqvgLY)|r=PWpY{WG0F;ObjZmwp{+SSd6_xE5@S#fW6-L((hwBi-}pA^xm4MDOHftd>FVdQ&MBb@ E03Y)?6#xJL delta 585 zcmaFP^Oa?SiablYqpu?a!^VE@KZ&di3=C{Z-tI08|3PrU-sO`gDvH-L)_J-(hIkx* zJ8i#rbfQSx{O_mx*G`_jAb4TG94pK8qEgY;XZ0DzGaNXcbPI^H7xp@KfL@xR~uyx>8rsOx5vqY{h~eC-TdqSoGV zJ+!6GZ+Y_)Er*>kYzuDYJktxBQLn}r!_Yn{j(I;<>!J4oOjEsD4<>Bb<07|F|7A#! zeoMoWcK>PYM=gJEZ}fc-b!p<;x*oyX3?@>&OI0Rq;>%%3pQP9L{nG#XeRuQpbtg^< z_q;px%dv0smN4HgH+1t_D#WlgDtBS0&!@0U)}GfDTap_tX0SMHza6`Z<3QA>nh9U( zC0Fzxf4np0lThUuzfI-)mK&W{oz&^tQM2(b^R}*|H`5hgU-9{*zS8!#JY!I2q`Er) zMEPy`4M5{38TICeP5iwi?&d$1Im>Tvja#2yl_)kb`hk8y%5MHAn;tzE{mJ!*WtG40 z15Ksc+v*3l+X&4_dDS}Yj>^>(74fpAYtO9waj5>(C;rI&otOUYm)QY~Ak`Arh?11V zl2ohYqEsNoU}RuusB2)ZYh)T?Xl!L*X=P%lZD?R+VBpnWE{UQcH$NpatrDuiMAyhL z1gOr$%*qI=q3o2$S)c|>CnN}>f(oLCYI_MHDu|%h2se6@prT$Qgzl^#56#P&%RT>d{{J8M`n}ZM2dVkR zhDGv}u8k!Qw>Ats=`6(T9!;}*s@uoui_VJfdS&2_0eUDwcWqn!2T(LE3tUh5Tnca< zkOiR{d|x;cEYlIqpuX;IC>F%}Wgsk}OH;L)0SMcZs@VvFvT4H1I+dZP;_JmCzFMO4B-7$DuvIb7Ej zvpQhI=fwyMOon;Ov;~|LB?)#INXQH94{5SrR}g1adKFLdb6dH@%#6am!E@FN6-~!* zi@AKLF8oa21{ieD(B0KLA-#R$zgK`0aW+ks7w}P>9VfK~T#K_~Wa=W0w6FnkG zu_Wsy?_c6zlJ${^*EpACN1MYpRGTb2l9!MtSUavJSqF)|#l{pHBH!QPSW1aHKHySR ze}2MODKFU387 PRO?(TOEvlpOw+V~UmyUe delta 310 zcmZ3oQeetbfrb{w7N#xCe|`AQ1;2Kfw(qqFEMMRn`-xuM%J}XZM`N%ut+PG`*3NMP&LP0TwBy z8*8RZ2(nl(?c6;*NRY*f$^O9fK0y{&uGB-!48fIoC5aW&3k6str+*VVHPK*rFW;l0nye^rW=T`xG=N)WS(yL8>qBTghhZY=|3|A0|NlPFlEdD diff --git a/sublime/Pristine Packages/Package Control.sublime-package b/sublime/Pristine Packages/Package Control.sublime-package new file mode 100644 index 0000000000000000000000000000000000000000..cc9aa190a01743ba9248f6cb412b73fe24b5dfb0 GIT binary patch literal 132375 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

MQ9p)fe^)C#xmVp;@WaD-H+b{7u8vpB8GTX>jk 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

jyNZi83%nHtxp4(c_{CuQl9pYTIdxuulz z(9O+@8y<4GWp(wbb3ECiYxmhwJ-KKmk>z!_AtYTA=F z6^eLkhISe~ya8Ri#xGv_9;UTqeMgN`vY4MbiWLXZ*P$>u4<^Yb7u%}x@@#LO7Kr2d z!W7st^ZnW1Z@(nCDS;k5>Y!<-#j=tzBUVN89{UC)nYH6*!6v*8sF1^R3&vQ#1+lyy zgeN>1S2Exo<~MoH+|^Oy;&hl+1s`!f-wv&p7F(zjJ`5m=sjrMUNBb=qP1=FaBbMfA7O#YQ&(Ro|EWmv-emz(SCEglck+ZMbdZ7GB%VKzj zc)~uYG+pQhdOjRuy&Px%KHLF;m5+{&o_}LrKq}X=6Nv>3k=Mzs{25>|-Z-=?3X}Oo z%VvOOp&?@t6QY@-m)ENZgV#1TA<|%(v-Nc2EY^V^qK|hZ1#A29P6i1-C2+F1_jw##4MUUNsYe z0C(Y8(xtRz2f0o&kO$~o1m!Buc|5p8jo-SPDeK8SgFiJDGwLzKLy6M`(CSj(z|};A6!?I0bwdycDRE8PiDQ z1n1`CQZ%tKO9D;C$tijHQRIi*oBMAV`Ap1I{0%gRZUp^}HpC;7a=A6pW9DM06v4`$ zVPo^Xl0G9O8u@C7-apDSK^`SF;{`y&81iPwErH-hOEFxsO=hB0bQ6;wwpAQ4Vbt>I zf@#bGNk{__Y}9=oWD-FLQs{^jB!6r@iN`R)tP3b+CB<-dlBw$yLrRh>9Ooa}Sj|Gm zsL!)y&F;l`Cb&55a@w1?M4x`Vu@i=r3Fq}glp&aqJ7pC;YQBCki+bWtJC(kabn4pH zg57i+o6pkwti}Gh9KX^HCgJg!S&ZyJ2>vGI8aGvzvM^3#H3B8}s1fqIy&+88Eh${j zpb%#j&Y&E{H3fsZU*nb>SF(M=zYMLEfR!!|p0)Vjr9~#V!5CWT?GaanNVxsdu?hrC zW}J+U4+QXOYTdmL!Nh~?6w`~CmFnm6%vYq9)RRz)xM3L-Lj65Zof6DMSt2m9BsZDC zsICw$HJ>Mv72$KK*Dm^RXIk-to84fIL^<|-@TcV`M%JigW>n0g4is}A-F`==Exzlq zyfnD|421(UhG(-PVcq>&Dge~=Km2Lsh#SF!@o<^RU-L2e*hK-s@W%uU^a6R{rw~p- z5aSGe-T~fXysQH9omCE>88hyVk0Y3}tJ1BgBsM61;22Yd*#&*BBAkdCKD;R{`PR85y39BF8o3dCL4Y98o;iFAvZIOTu|R(Xto!dm}C(0-GCv^%|r{C>jnlRRlPpWO->^=D#08;(VM5=If&&B zxPCE$(|W{~@{`*{BzYwR9e?eIG`ecmxQ} z5tRO%Z+diVo|B_k39s7lLYyDVx+rd*BWoJJ_oU8yYC&wvX&(Kp%!VVF%>3EGdFGz| z@;8E8V*w!i?Y3Y8q>fm2n=>~s4y5jwdS=Aj6#8$dHE1*sRU9RHgyDLssRN8$s2+c| z9QaNe!YQx%!TyqSH0k~O%FyU>gl6)X$Kk%=3DV287qu#;NxtU>euaGd2|>ZDLc(YE zMQP&9Ds|W>RX*z2Cj9wTOigOF+vjC`VBRG72w625H5a3Im2+bRx8k3sa#FR>vsyVc$8K*o+uD66AP(s(w3ttpOtzl69qX?R zy{%;$>THBw0)l#_kZT6Bcb1Rony%H1jWF<+*?kgy{shv;^U^83ey4YT-*cHYH5-aj z3yU{W0X>=+2O6YeD^;J|zB-^lKmf_z$c;2^ov)Mz7;|&3{pgW7#+CS;FPw~p(g%8- zc~moecAzJuEYO0;c`?OJ{YGsA)S)CyvD}MhhE=;EB(0ijHmqlU;Nc&sig>HyN(ts| zBT5B!|KqxwF6@f>O~@SMLt4$LajT;)M(?^7{yPCUx!-{OpBU~q*k8blJ3cNYUi=oz zlI0>dA8c9$`PZF<%Xat7Zdo+b9Wr*jz}t&3-+!FmJY0p+j6NikyR2nNqY;c~VvBR< zuy2^F&IBWy&{cyKGV0G@Td0z#tv9{Y9se%D zXjyfpQL@h$^Ozo#Vm5oC-5g<`zDhPk%aY6%R8?HoAMBvUlq%RW_#o;eCs7!up z{^-o{SGPF*iv(YRw5><#u7$euCD0|H$H*s0!9%=U;VOmH_o^g@d!)d$-v{5d2}kA2 z&H9v=%0EE1#kYW>E4tI6=esdt?$+T6xczY{*7NO0XEyO6NBrj& zXgj%t_x?SaAF&+0FosTf&}6n{bDq^bFY1i5F}4i)Wno~0Y#>a4&ld7Si4MFz+sC>r+g4a* zjM690*rfTRWWK1H`a1Eemf;*cuZP<#cY-h0>8_t?;9FD9#cg^YDV}SzABL<_lXrI2JQonr z69!msnRix?7pBk0(NU?biGR5pml(} zebm}jH2!g{gm*RV`4G4RZp!Y-k{23sza_a1oorfM4%KX&hXgp+Q<)y2-u9)2S8mY2h+7NQ z--W}s@G5aS@kX|YhLjK9$_vKvCXlrZ)ePuGB|-(43r5ouQq~h+W5Ium2aCHWac*zL z4WJSLK;e}7g9sLR!4r@1`0qPZj?7*~1gex|KlYpZlcs@i-tha6woZqP3!m3V>C#UZy1fwi7tM< z$2S;g%J@xF$f#4@BCLViZneekfOh|K=_@iiepmMT*E=RWMg=*{CFY`ioGd z+x#xn(r?>bLE4(aEp{iLo3zvgn0Rr{x@7aLFYHv8&KZhu2`=*CpAJfIJ!9op`x%6=h(baMrKs{Pjib84~xBfz! zAq5i+wyBCbc%US;9d!Lq45n4d(zsmKPr^@-6a(13)@o{X90Jkwhj1hjmB)1q=)Gk& ztlvINF5ww}nZFG0AP}>6HLBQ4-%sthv%x!-BIeFTuo9ew-^WA=L@(<;NDkT+o+d2x zZieG%>D=+viSCJ)8YpH*V!s`TpG6=};DEkz8goFK@Ou!^4%tJUi5T6>|BBPaGRO0W zhGvPj6F#|g0%@KqJ*I*vCQ>KJb!K~0z2-5+LAhO6fX?@X~J(~ zHke5>nKZcovty84CirpDC#k`=Ys^XY z+KNU+?xdt0(5=K#jSf_5Dkm7meS`?nBVwKtuK+gOh>c}Km&ur`ln_W}$jXp!F9Y^P zI0L~tE;nm>*s1DLj=}L~jFdXW2|Nn$qPP<~39l>(7^IR> zlT+GWq}?|I4_vHBN%zHPGTo=48*NQWWuc&ILUh8=0kIb81KA8YAveItkXFf0@CM%kCDq5Skela>79Lsk7k6W5}+O;u9`Y3kCnt75p-$D8{R(^}) zLF)E_bpoP5T_ifj#%Mz7dn4WGA6r4FVT6*rO0rxLLo>-RCgA-|B(^%7>zXi5doI_6 zMU#+z;1lhQwLQ_k=Y$O}hfOUHsqtxrZn(w#wfu7s1}WQ$Shi!tPxOE8?%HXL^nX!6 zqkoiS|C?ga$k@=<=>I3^v5x-_#bC(I8|n&M6>1@P8{t!(xt|B0AQsY=+(w(GT#8Z* zxKnQqlVvBvo*>W1-K%qfYcz4lG(W2V{w#Q*Bw^ZyCt!cgZt+odqaAuJb33jXxc7a5 z@#8R5Q7Cg_OQr$oZRtP=A*I0taD3rofU~CotTC|t5QIGD88>yk165nf^(GcMs>1$+ zY*kyR3|V5^Yw+EBSMn5Wd=aM(tSpF~>9)>C#d=)1WSL>?0#tefK4> zL=I~GT`#vZBcoExd?j&?-*Z|lXQJ(=i{(4a{s-{{vAj9iUis<{$lkLLFR`t{cO5d{ ziS9D&zj)6AFM8RV*Wa(ww{jQ{BX$4zcPLOx&6xibjp(2CziC|mvyl9QLM}NYPED&~ zH%>P@IW8+DPUBE5CPPCpJ~g}O*T_`ehT_L3E<&dB-#t|DxNjDyM*Ck_;Qs=* z7Neui-G8KrQn&yBKls-FSoRE!9eyIb9c*39e_V(DWiq6u8N0-S;*+bpC(|q;5DK91 z-mp+109_`AlHc$S!gNnE6SF>qH#`ovSn=IGy1FJ~f`On^K_GBBwZ?P8xq8Faw0&PA zszqJ)wS@S7*1c<`-y5dCJ(og_<~*vIbV6zxz*@BC+(bTR1?AJk`Ib^waCn9G;*$Ha z2!*mg;Z-l%($+Q1T9k*wu2eVvGg=xEr?YRj|7$CzTZTWfTtK2t_W^4_=dm!*W58Vp zH5afS&EDsxAA@ozDBi+-G?n8$PIpII#nlXbEH&&}wbw?p{Wyi(%95i&Jpl&oUsR=v z+gn(dasTA0@Vx#8)ZeH@ySy`M77fZ@2()C|7q00oMxd1f0`i2TueATZfur;Qx4jpl z(pGvY0_pMj7c@a+J|m@#oXFK;DKugUB1`3ug=ujB(-YKEOIic!IpL*O-I|xzz5B$_ z93Wb7Rj8bpc4}-g2Ph~ue;5>>42V90Z%IoF0@cLW03_$mK3wZ@UzseADz*Wrf47i% zvcRq{LX>whvinFtIUm$d@a2F7H>{5$K(-vF*_&W{e^mU);+#HJ7=*Cw?<1TmmLNb0 zhB>_2OnHa#&=P^D7c&}SaKuOBM4wZW4?Xxyzsip%!-JlWu$qqU$5n!)#<&@8O1@lY z&Or-4^w3|z#-g-hbd8cmGCpEU#1bQ_ zLNiIAX`nK+IY?HegwgVrlazBRyVcCGi)<`IY-CG{el`6mRPC6|mz?<bqv!e>HD4dX0dOyOO%>>|FYY%MDK6CAYdxo zA~4=efD0i>1nsbznLwazq*rZp59k-kw3J0yCcZ}Ku{A1rYe;yi3!<%+7pku}#S$h9 zKP>|A1?gGDFmh0*Dklm&4n#BJC#D`IMA~D8u$Y2CA~oLn&X7n58^b@ZO%Kvoh(I2z zOl<3K-|3t{NG-=Ltaic?Osu%yRakYrJIOGs1T}1Cgk9HJ=y8{*dSoz2icPs7zMans zzf(Q_6#TbFVoMpkkL+at2ByUqYGZeodf#po$0B{JFhy7k^FU!q4E}gg z(xwv@VIQ|7T7que(v*nb6wVJe9oe&pb#lIsRVx;Z17}%+&i`4lG-xpa7wtCqtfUue9E@>ErI(-va;BN|eR~mQ3~1*1nzNXyN`(Fcxr_8xeU{QQk|sCJ z3K(VMBQ0Ng$CjU_T`1rf@yy+AjY=^Cl{WW}4zc=h8k1Irf}#EZqqNlv?uPOvJ% zjdWW1K!F_HV#NNKW%z8@99^#4%3s^2nIM+et9z*~ZCd>X8Ebn!tH##`v(k$>fYm_- zb#1_yL-SSP85hUymE8A(F3343?un1|Pd*w;ANo)8o)EgXl!SI56-CL{KgXLi2WP)% z&2Q=%HrV6_Ku$!NA2FS0>?B9VlOxJndX&j-GSnOrZL< zB-6*$j7IYZZF!~KSK4s`7v#5`kl5HnXbV++f9SWO+*X2in5oH}3BF40HXv!W8>@D- z6dHrX;BwMEDW~L2zO%0tt&_FxhT*O!ZqN}_ zoIJU3245K_-K7Xhf}P5(28j-Gx4Yw!xqu_R|17@v4CoJaKPI$*B>%fpp5f1c=ATEM zYD{z6O}1xGAJElx0R_WK@eXa2{-Qvp#TgXMBL2jJPO;xWH1qXjzl%}J+eGHSdv5OL zgva3`Q0GL*-mVj6&nm_w#Yr?#Bbf|6%fGZ&Y}?m1HIv>Vg7mW-<4vhGQp=5oE?v>C z#=G>mA|90ai@8@dGJrfX((u7ZW)>*MSGgw52PB}Ka*BJGBacaD0cA%hV%J)gM5?GC z{(cD0%-UbzUUm(_0xK$v7f@^Oz1cGSGRkm-e!zU@*1?GeJvt^zy?KZrpO};x^B8YK ziXKY(#o;J|=9Z_D;vv2kAS6H0k4UUrXT?x`1c8OrhPdJEpn-peCwD0G^iFgn&ZL_4 zhG*l(FGxS*(2vrL25ZUdMSHKV1JFr2&yYT|NS%z`Aaj)9McGK5&H7Akwops?ZP#x! zxwGMRfe-4YHyG-SLd<5zPD8qk#Yu`}m8Kd;Xq5h&0kl>hI*ixJD3c*WrgX1kGALh+`u{(+()Z{Knuk)Fm7(-N0vcM zRbv;Vb57y|V?eu3Sy=0QO#lSwDD|qyp$cYTI1;stP%`jih%ptXA8_)Bz&-4^zskCq z?~r!WZ>>Ts(dK-8M%p!7wyecrmOu>6v?vg!{zSZV_VYl2fEjx}mY{zb-L3z*FyU^e zBDz%ai?P-ifbK9DXC735fV}0Ve&Zk0uLo-`K26$MYIpnzYJ{oaV-LhQUdVKLuyLC$ zy4mw7dI>ZSl?MPDwnv*xzcU>oki-`lX8pzQCp5GyS_|C?Z*r7Fg~U{N1of$O@iPtc zqXxVVLd940^A|NXEP=+Lhh*|Rb(o2I0#M@H(6?4Avr0pgOyRfjHCh)D_JhsPar@cN za~0a!-XL_62c(F~{xl(S&46o*^c-DxN+PUl3nFQbJIQbu9!)XZ z#6&PciC`q)wP7tYsO_0vljBLfjnaY*H2en5;!|M^g-OYGNqyuH*Wnm?#@>|@ER?61 z@}7V$OkAVi)1wg8p+Qr1Z#uv zAAuw}Z6T^*55j$caaN~fa>9}CrKH9f_gn42UHLg}`nJ_UV1$2B^%s@$u`1Q}yqqXZ z2xN7+?d^QL?OzkzD(S;*Fx>QgU!ibb>1;EWCU^<+*C~Qib%oa5+rZB` zQCABjuM;I}e!?wua6`D1gZ~Ei3#6cVw2mgR(%IJZ*!EM^)DiV*X0k%kKoMKa zEK}$L_S%G5G<((zO2T#FhXj}UjG>p#e+wn-G{w=oTv%~Ww*qc4-7ttLFL5S(tc29z z8?%+zOAWL5O}a|zcB+!s0$1(J`b*XymA1|e%Slxejo8_ zWn?Ue%6xY?0d-tWKFde|#khcBDaVF*-9QZTQ+vXSa-)1@9%H%T?e&(XH$15ihjSk zsmV{>!t=!`Tkn)m%2dn31y*cQ>eEhc7ZqGfAvR4ZgRukxU8nO$k}CY-8-zXW+v<7r zan+pV55EzsT0n4AZj!BO>a%N9PGp67VBgYEuC1x9WI?$D^RMh+cl_%VEpuozz55ra zEN?Ir1s|A6+&&3#YdqmvAph@reT^+ww;Vs|)>RcCp+_@xOh1J+^NI;^Oy{Iz7D1h- zAzm+MU?3Cq5{pWr>=^p!=%B#qCz)W#tnae=<*|$Ruib{Z8ADi8VveSzF4a&vZfk30 z&OH`;wwfXDdrzwGvSX-SYpNZUJK#Z0dd>W4P1erQqQx51TfjP`GmKb z01HK8&d)_+@QEr~>PfJV%iKW+n7Ka0g0JSOV5M=j>D4ZB1r8r>DT`3ny-y@${Ctwh zBRjk6TOy%&f*9-T#!Y9R4eOla-@GTZ2Q>q~8R94L%|IJ=rk_r?ZYA=B&h^VP7Igmq^ctV5< z6qj}l@=z|Tr{FyBk{-YnI%p;_Oo&EU%g<D{n^#ih zS#h==NbYLmv)#Hb1jwX|MnY4J$03!IIG!ZsBmdn?h>U|PP2d71C;ov}+^fAu4wkXr z#whMy2V7eWkb3&Y)D(z^xfl=G2~1<8EszI$H&!<=l8uIV*~SzLq^Swmc*VpJ)}I|x z0VfeF^n^O+x4F@2-4@mXXs_JnJoB`W?Cx0Mx1y)$^$q^d-Zut|+b875a6Ot70D$7Z zfp310{hyc}M>za`=qcIuei$hmXV$-$h(+fgo-6RPXy3%@w? z>pS6b_o;3%=I2?8wUoaWqq)zb8%0G;K(q}Qk9Alw=+EA5jIur?;UsO<;>NE(J;{YX6 zpwSI$`k`fw`L@?-E7nd1)dQuHoKUHt6{`oYfHpO~>+`#W)TmO4v@~%%q|&+@r*>ld zup~vQD}9Tgl$D$|m!f~|(1g;gej`UliuHFvJ5xd+GYVz1y@(OP3V^AB)G!rd^5JJQax_y1E zV>WIu!i_&_@+t^8`EQ_#%@hWAIr;8y87QQHsv6d!hm_OB*g&8+Y#_+DB_s#GgrD== zFQDCZuI*b>FFOuU=&D?F;zB)|y-9?-H3P1zc-tRZ!uthr^ELNzk`i{@gbB*J6**XK z@5aguV#beNc6dGx4;mc{Ug4;vTv6i@ga|qni_q<4Nz2cTXO2>gZ4g>Vs zV$l25BJwZLEx9Y=$W)T+#o2b&A!LEDK_yMXdBuL`W=qfVX2xb@xhOVw#hmnkQf04=3SiO23ieq`{i17#X!L6MTcbkSvP!BSy_1k zTT8PDa(CA+VfQ#hC=vUZ=z0W_1^Y#EsGT74S9DID;E=>IGiBfXA#N&^W|_21W{p*@ zCf9e-2;|VqfMu;N05apn2o#Ip?{=a;)wlQfE{)*EqI`26=W{`zlT;1J>b=Wx3j!x< z9x;)A-2S$mCwp~18Zs^|5RlpGu|C+QWP1{GV*i3MtOk=EP0^Zg%Lby?6 z-zxZ@FpSz;giBL2lLGY6&jKBk=f-ItHEJN@-pQCABWTXFzfXW`A?EtieKXM19Ey@i z)IoDq1RS`A&sfbxXws(#wfdHC)C?A~$9!RUs0CayL*|qilAIYA6y$2#m3YJg?;!DF zc(S0|t$9k{W+c`m;Vujc(EldQI7r0`ds-%#*#a^v*;LZKLT-s066dk^-;4 z4(i(OicqjMZg-es@5T`}_4DIdM1WE5X4KwU_77wD#7mriXg=SfQ;P_SmPq@^65OB6 z%>I8VTymYBAB?`f?Cwwb4Fo8J!s254b9Cau_#wmfHbY#x5RJ8qPhc+!A+VJj5RxMo z30+uT3=zb^3JFKBT<+vyw$oHle>FU+c=1F?%`fj_uyGGB0_>Ur!eXRvRt8w?phZJq z+scZ&Qr&&Oy!W2@(0y-w{yDL89bHj}0f!K<@oev{b*VYz)$;z+p3+(#offmS;!VJ) zLAOBz#PgV(=x1xN2M-Ah1H)gphiN%f3EX0_;?zGg>D*NkWZ9NNa+bf+UKi;X zfbcbgq=c?yVOhyd&4e@Pj<38WvdkGZ)ssL8+LCYpt7tv|O|O?jM+GR8$p~ei0ONTC zvDuW8tH(A*TO%}WfTfwf-_Qc>F|-VaE3J{oRFalGjg9u2h~bT!)h!xZKXqR3pfek3vxl+z;C7dIpL7hs*TJ`t=w2?Z z2dpnPj`M<|Y8rOAdZ}&kh*%?tL(^Sr*VZl2yQj^F-iiFn>SPH`g$s7wyj>j=PPSoy zWKJ>(21zo7?p>6zcx3jXPv0cGxrmR_mBW&RD1l+AUr%9(TetES(-M;|>$_Z8<#a+j zn6`FUQ*vH)7{MNF)6PYU|9&!yg1TB_e)XJ0XwgOVI|Q8d*|uLn)vD#!Um|fhfWFiY zUH~1D=AE-B$e1ro)!gOXk_yzwmgj3(P*r*VCHL7CeM6&*`#G_ac(8Y(lCI$kzLfTT zJRxo%1XZ+3J7c%&7FEwoKd?cS7&Pq5dGlkn@=$-!fiJ~TfSMg(fVPl+)Zj{4m;A=J z_>1@zylwD2H-O?N?|1x$c*Nex!IVfHE2vS9f_Vtd*p(z@XO>vAcBzbD z2XqA2f4vv(ru>DW2Fo+Htvu?!QfLyH*h`@mDajuO+N8e2c6mX-$qfYSR}OnG9(K7S-<{=UgrNaG5;8zaYxld z{p?bqAO1z$xz*5y3iStAJi@aJGXnDZcHp}gT`1c!eHW`zBPMOL-NS{%$KAr_qiv{T zUkb*7bR0h?__<{Ee23uLS@1&91t+{d@nFpqwb;0pVn$TuM~+C6_k2h_UL((yk4MnI z(JW&3IX9YqXchrKG>iYK0dp|cH?lVV&ju`5+1mEUPw}fp*Io=wYybySy3>GO52atj z3Nxf;EWt-yF*8Ai0bN{4+#+25r1wQEqJg9|n`ce*4|J~QwcDXGaS>QJplgDhjzs#v zYHa}D16G0Ws?04NfrdPICwInE;F{C z+nlvf0lOGAsK%;=sSvMa^(sXwCY=Zk1{$Eh!l`TRmaFw4b*(w{6^{0R1|r5i z9lh8dHeYtuTPsbOeGb@*$<%^oPP~v;ex)nenIqeih!9JZ z>#Z=30(CgKYln&Ht4~QKcSK~|oSv4Y(#cE4d=s9+y2SLI<3usmDK#na<31ikTM(Lf z>fz6LsUCWJzoCa2V2mvp?f$}4EgOlMO>zO;5{X}`i0A{zhymFl=<31FVt_g>d+p^r zM!O6w3+XTpLUR6f!7$-C=3cVre-Z8cOGrHe9QhEhH8Bcch3qCnxs69|4Nhy2vdTNP z%CzpfV<4;sHcF^i7r0;hLev|90-4#lLxn6UmHdzqRASOy)6tVVbod6n`vU1tq(|QX zgVuO;yP#`AG0XWa3Rth>r)XwQ#$Mt(@M3kd40kK$Bug5+fD0iR+bpkEoVogQi0hC; zW=&X`m0%wUnM%|^^h&hG)JXz2$qdD~5Ob^z&#bL)&gM>==CC6=_unKT<(@+5?#5Kt z6Z0MCtQ(v?P6JO-q0zg{TmesAljGgUHc{bfCJ>ey_ewJ>aVwBLTn$oJg(m4oB0Tld zgk_dyQ>oc~J)`$x;dX6#LfFd5&$j5E5F}_UMB25**B~39HFYW4!?i@wLU*wo!0Kc2 zii3;kaHu_y0-pSJi2m24uW?Kt7dz_ysq7*?iK&7xvM`f51TT(3=J9)p77Y9$tfo@W zbQfx<&(sXt8&3q03D z=OJ0PFM9CjtYdSL;-ao==lhSV^mu+wD7u|*_bVCRspt5@Ga2Zny;<|sjR)#9<@K2f z#u#vJ&Vucpt)J+B*(F!|kguTi!gHjjXrPTQgThx1h`P}|150=v1VNAh(8MGV{4PR9p$TnaN* z^Wn(U^&ZVOu0ZmLtM!%E3N18NZ0aj_>iG>pxY*etZ&=MxMt92ktq?URVU9}qpZj(< zr$ebRauoB^Dt8f$a%&GoLiCK-T%sJDeTBsDCEZt98g>Xq-0I4QxN694gSCg0&7LK) zI-P9b!JeR{!I3i$=k{jHVFi_f`%XjQMHuYL%}gq`l3SGHZoD+6WAtVD zVbQ)qdAY{>?&R%x@im0KYG+#}{9zR*1;6eiyMj7G8dLq4G~#%PBZtzZ_T0G{RTQyC z!L}S5qm2~W%B3UJfEMY`sJIunAfLLl-`-rKwzd%grAg%2B&MU4LaB7gNmTRPP-0PU zADBip#H#CAw-ldDHcvCIi%sbue8sut-2fkXl}_}&tstH1{Zw%~%4c6ncs&zSjY;&st>0!sxJPnR^z>HAKs7<7 z8;vtfRAX=(t<}z`O8HAK7e6q_{i%EKg{5d4|1G{soViCK^yjfgCitI=xq-QpfwQ6I zk4xKsViW4y|0<2XFS`6qzeo%twl4M8U_gX7oHax8X*M3m9dS1btZ5sH;+3*!PrrLO z6AHw{ZFq7v41C0>@2}muVt2O3y;PG05c@?6n<-pLq-)dSx~NI^OeT5gNVrI@&8HoQ zgO1)b2cW874U!#I%0eDuU2Ao-RX&1saH2(G$0^G}Cf731sfpQBBa$rHGEF!|_=tX; z#)ub4B%~gZs;3$>(RW6=yaY{&fbhhxha-JcDz8>FHcWOvr*7Q_+H_pmwU{ zNtim5s;z6HCh#K0nkZ7Efrh ze|kTWwF`GE^^_^wsfI^EW`2UFGH|(ML%M>jk}D3Keyck)FRe;T)~9bRC4|c+Vxv5t zec>Qesw8F>=M7uDBRr2r*ckP>n$!T~_+wPfnL(~YbR>CTT8leb9VqrcOI7&c9`>UJ z%cbgV{K|NU5^D*(H2Y2R1VclST4h+STjiMM=H6|f5lFditL`rfRRW401St=-6Ymd$ z%-Q-7SV#)yZ(mwP(D})RAhlcovFsV_b8M-Q1Y%&(EIn4slsdgov313k2RQbB5DP79 zYq>-B_R&S!^-&!sEwu}+IAUpQd~x}k6g^ICwQ29pDTE4GL%{_hD$?(J2}3dBqVSiP zs3@ux2zJkxzo(`6xp>IKfa)mQ%0dDx{@2lj*#VwZ;>vV@jV;vBNb}qng{M`ZaRXh_ z=zlb#*e}DHag%3}GbUb#c5ZNkSp5UJ64VcAk)};$OGkNUHEDm{k+G3AyhN7NV82W1h?$>=BOj54*sWPwn3t}u zHHs!uwv5I&cr}!NY>p5Bwu}`f9D~e{2`uJuuq#s7dWucJ*E*&jvzscs6(ZZ#eVB3O zF-JxXSz2ZxhX#->EglH{axRm?aVaiB>XCUV9zdwt{Q2FxV^89a3KStbN%$vr6(!@; z5y`rhyE&)H+I@GFtgK_s+ME6<8w}A{W>cZP4#aVwZn#egf8aSU?GM;Zufc6!Qy%a{ zU;Eh$%95z2FhWHyLsT#C`*-zS>H=0Lm&JynR@yEQKgxMRr%LVWbPuoZ{DoQPmdJF- zG8*t4W;c2k+7f(Ja&Qk*UQHh%X3q$iLCzBpO^$ckRB!GS4_Aoq!8jwX#f~tUH8+K> zr-NW6TVkI*jdaHkGBG@QV6!#OiGqD4+$}vB|qk8_Mv|EdM`XF(|6f6CNm>EEr6$49AAa4RkLy? z2g^c(h6OyE+Pq$0Z-|p)ZC;YXT5Ub5rCI>E zG2`TRx9!gQQ-E_)iW@kX=}L=*o2|}S?UG<=4qjyl_b8b86{xX=GP0)euhh=~5?0FQ z+{9AjWFmmS7Xyy-x(i%M4VQ4LSxTvnAKfeaMH%?peN^x3C}wOJ4vGvJJ*)tIB?Wzu z{=Djd^-97MOJR^MJYA9Y5C=@I{S z7JCfL1V$Z5%v7rm6-%+VXq3sEtHfF+vZ`IputWp_tM1iIXXuO;Vdqd1!ExNFwz~8W zIQzykgSL+2Do^3GI;pX{K+L*4?H5pT^Dv9;)vP;3HIuQb}dsOC_?DH7OyJU1TX)Mn+~Z zi(NG-LI|ZIw2UZ9kq}ChvSckpDy6hZ*(!eOf1V6xhUtI%yw6j4-*fJ{=bpQ~ckgZ6 zUl6v(DMx6*4|(68eyN}MIcFOjTz(M0E7#z4VBwr4?DD;OQx`$2UDn!?J2#YTGV!zcW?r$9rL*Hw zcx=&?Nm)Ko(wL109U7z0-PVtNU6W*k%Q%=8``t9BIqt?+d{MSvtA&W#sqcYJ9*g4@ ze7Mi9**br9r1?(GCq;ZKE0q3N1@%9{Y(m#al0-Srny3;^?IN@&YxjJP-EO2WkmV&h zYmS@B?0c(1lGSU{<(33D$(&G=DAFf37N4yC_7jp%on_`cP94l~l!UU8^ka>y zfd^M*Mt-<$_y2LzzVoyf=eyU2W$v$!TOZiErXL@1M@uPB#PhtAhuZZIR|Zc0=K5xg zFDid%;n7v~weOvFwDCHg>n<16^wdZ4t&@8$HmO^7o7cU}lB?d%n{`~kH#ao|mf1Vy z)H(@QEgs626?Zi$@du_gW~cZcN{6~~eD zhrSB6y=&9G*nZ`Rs_ue)UHv@%yE{ZDhaX6R^x5}`hQo)ukqej#WW(PAiLy3=GeF@$ zU26~g(D5D8TBOTr&tQYqW}}X_vpY;gPH=3~6)SyRo-_P;gdZhaX_Q_lwfNy^z{pU~ zu<bX}hReODvm;(PN25S-O_{#124+L$U~JJytDA~p zz==A_-7AFRrCd!7yhhzF-Ll5bLU->!ek^RHw$P*`*SW8)&HK3C(b3GScc^t{d3Ku1 zT+xAovb%E-~ncq2?m8SFQ3gL!kI9H@) zoV5XuvD$%^fs4rJ`PWB1Eu4F)CcUNZ*>m2qu2XZg;q^NEkmf_Pj;v2}_^O=Daf76flwTIzkvxX~p&<7l<17e*#J ziuA!TZ!dgDCLLc9w2y(jWiXl?fG+r?6@rCJL0p}iZmpJt-1VM{>Qz0>BbypO-88(Ptm&+t2P@^ z-mVFKl(}F-{Jfr09u7f{UuJH*!y68xl_I~b)2Oo??8H4hhm!V*QrV4j*)_ub=!!6**=H*lj55uWKC4)TBBc%W3Fte25=+%mD|k-f7&CH)vXs!>ELM5I$k;yC58tkP!TG6_wgW68zeysc!g zjb(FNY-&b-e$w3bUYuJ~C(Xvj-MQVcmrbj1*QX;HTXKb6mNz{WSA3MHvUim2*>#6c z-ktRi?LSA<<7)Wc6l4=hEA72zUuxxF>z8r1Q0#ZbH+!W%)0~~FgL5TTv<1kv=7;cK z@=9ynmczkaNPe!bznkR0M{M{|g0zIO;7T#|{s%jFUn=)Ds(scBYrn9-f3?nD8a9^c5%`L`b?3LGzCUuZ{31pB#Y@-DHAke58CDg<5?$+02@=^7y;px6 z)r|}tlr|W;XCO?BJ{&m^<4RI@XDyf`qa2Uez~{T z60SIMD?d)UIT+F*?R0bC?2AmP(Wcl1LZ7!K886b-xNF|I`nDbSi+3GQ{v`dERaVqm zAExaYLzdYev}9v7s#fYvg(nw}%Jz(C(!3i#S4Ex<@#)KKyVzb^XK^p&v{s<5^*(vY z){iR&vU7T`y$_!M#yLE%QNE%Bx2PYlTQwhmPv||9~G|uPcyYzp=V)> zp@fX%O3Uo`InD28lT>KEE}@BAdvEg@ljhf=hstu&`r=-M z7x!0v6OpZr^NrB>A#Po1qtGG5FK#+NgS)I>6Z>rS%P(cQ0@4XLe+UcT-n!0k4 zI4zEO=w~a14cpzUD$XA+$@%Vi((1t?4TG8}Crc9n$@e}kJl!3VDMuPVO4aFXJG^X- z%(qbcC*7}dRNwbYeKbE?^QZpf!X?JS#lFGYuaS=GR2}nlT zDQ$0gH#+!)C;iwjqv6ldqw~skCf!L;Vvm_G=ohs3va}%T5T8rJPE^tb(b;UDK&kYVomXC;kX!@dFDN$DlvdnW-jp7W(Ta`kfH6H%rLAyuoO7g~*Xd zUQ+KuS6#JqE$MV#ITY-=kzL#?*y3&h9K}{W9N*o}BfQJ0MtT2uy>%Bmgz7S0;#w%!pk3&*B!Y<|nlH}y7K4$qC# z=WA~VuUUolSby077HDKRyiz*pBL9h}4HboxzDfRD(06M+}( z@$ne_ddmD$LrVa4#M3>;weF-QwmYbIwf({r!`w^FyP_*Kq7}}g15|$)+KIaL`?0?j zc=f9K=Eg2RRSiwA6-G{t;fW{j{B$pg4#pZ)KQs-nUKYJ1$NkLhlDtPQwm#vck0ZKV zbs~7;)6xX8i(>46j$kIbx@-5Q!>?o2xvs?rtbb}7z0|P76FZ>wv6L^M`*=HMzkD$C)MwzfruUUK!b{hGuZTn$4+eyN#N zqGIYOi%0H0sDMnCpXbdb zx*%@V+F7{4`*XJMUm*Ss{j^OZs%WuTV{K%18m?S{Pa~f9LPk*0mLLlibF1fqcJsvy zLQ>b<=GU~5brdht_>^UA?)iS7Xv0=-!TUcWkI1&7^_CIVq(&PoEY@}voqxw%;r4ZYTUf%cya92}Nz)XvKI&R)!^!>Rd1&s{O%=@9=?8+QCj zXZ*!yY$x>lH_GF-72!H zJ)VPmUJq}y5a{4S$y_a+Q(n%2m3vtAZcFGA#ao``tT8-DX2kaj`G%xnB>_}H<-fY88FDq$M_X1&)pgDrvZKwTjZc4VvT-lK6ivAh+ zz1VmEf9}Hst<*i4bAK=A9~{wkeblsnd;NvNxASb<;s!UpU8|HGMSAJ{!cKREQoG?< zF3VqgtOs7I#{ZHvAP1wK9}-d*7`fiB)1@!9lhpGz^mk2gtXfvWfl8 z{P-c(v%SW}YkhDY$w}nKizN?sE!r`MXZTI6h3Ey{Q3KrURJE*nIlY78iw^z4usu{5 zJteqQ+(a~}N+9Tw`k<)>*_Hzx@v}Rtg)B19gKY=qWY`Z;)pu=9Zv*X?SoZD}Q@h4( zSt;vn>L!ENj+w9gd^CO4T&^|biq+BUy#r=aTUUJN)7yZU*nL>=XC z;eA}XRP5>nvu0Nxc7gTEc9IJvFacYt1-VKq*k+hi-1&c(ghSn zy?XoYS$anK!o^QS2!oV{gCUN~ySH5C=sujgamPmV-EQ-{H62+khiCgpwEq-1K<>EM zEmM(VHnd|lSNQG7B9nV1<%Lw=)6zcfC2Or;Xk2w$9BjA(Q)#%x{px1nS1C{blMPt; z`?ep>PpgDC=Ws!dhYx|SftnR-!Z`&O8Mc!L8TjBLcGu2_ld=yU9 zVVlLr4rj2T=Aw{T=(b{iKeArX87>~UXo*Teexi-B)*9Jc8fLEHkdcW*rMwnEp>#2>j${wggfh;aIGiKig-IC>;i&X&@T?JB z%O)5|*8GH~RmKBLBu&i`Qogpw@RG=hMDrk5i|8PI05NjJ;wSXLVesUsk0a(i-eJxi z55~AC&ZvX6$YcQ#FkYTSEXkAL&s0X=0djXkc0o3j=Z%Gbn`)XGREnr9b4I-U8I`Eo zqrBM(9`b^V-ba70n|NUcK1yOmfPE`pxKL_W{pLZ6X?}ZDNyiO!4g&Ph*qCHRJ5#2m}CpLMQib7Po z{PT}kC`^Z(1ZSZ~y`Ba&b_Nl0v?2yRznqFf4DIgzJvkMs(|s7sLw%S?RLTXYo}v1G2Ud zA?o3ss}vnW!ho4UGaVLBBsqF`Fu4Sx6=_SLkv@=2;95yXylcixbj5%{sRnr+mbU@6A)X{0Qi@!gc*A(2Xo5UE)pV%{NN5tTKrZw2P25L(lI|}6h&O)C_ z%Y)1qU=XoT9j4Jo?Iypa7Fc~0Y5}wY6NyUM62c534Y%bB~Z^g<5^u8GjVfWapcmGUQ^ z8APi~V@HaSy{1u&r_Ss5CNOybv={UdF%x3$#PbZn8tXX?DxNR-(l;23jZKK+Qm$DkKf$?$ps`V>q2W%Ytogu-KKVt$ zWbcTToJJ3i%dOCZZd%P~pun%JqL4P2-YHUsS)pf$*oalaWvjNqJRLrLE-O6EIq5zfWpo=4w*E2$h|r$kI*N4gy!1Bd>1Uy*%KNmXcNXpMnt4_M@AnEB9JmxK|3FW zsd3bL@JjkDpmBTgJz`ER;7{ZE)BKj3WVqBYFWQ%UB2)kF)=Wbt0@^ z{mcgK?}CM-XzIhAO6d_`Az+3&i?~d6t5D`HaLE{`QuHdngws!HS*MLzCR`F+#?c8n ztCPPlox>mkwhU(-5Cs7jn0!7A%3<>Pv9Y-PgaGKk{hN1vZA%NzXFuI`$VO78UDx?zgARC*I zz6MPM+^G}=ITljj09hh3@$&zO*eJJeb|Q$_48tFW=&!&+1TqQoFFaD-O$jFs$3c1D z2V2ocH*pvrO%^1BvyPYd z1OrAHOVZ_jQ~p|f+grREnFBqdu zH@KREo@1{JN}mO##psm20sG`lmik|>7{jx$tf}}`c^fGILlAE1Bd)~O8E8I^9$1*g zLh8t0Wg#Bpq2~*O%4wQt>gPSu%RyaVP*;IY-5S#w1Ta{Gl`&chyx%_t$FM-$n2xS! z`G3-3<`(8hr|Y#_-}-)37{F1`bB8YPM50pkt!99GA%klo36>r^%*?gR^c6uSIBq~K zp^r>iwli?C_%W-Gbt&2Q)K#UG^^T+|?e$E)L85~|Wg4gUEz^lMK^rjE?W-VZh&9r?bV+OK% zEp|$Q=g2_wtUO^FB)npYtocsRq25OrJJo*DX46#Jus+`20qDTgz<82AfW0EKmVgYc z|CxbTy#dW9L2)}U68%(~L=bB{i#WFP*3Hn%07YIT&=>2-KcIL|hw+J{8H*K(>aVr$ z#34hmK)C2DZC@~Jt;aiF|DH)*bk%eH4ZsUQ9_U-Sn+I5nK;rPUv-gPRH~8G*vjGdO z1A}QY4zk7~yFZW3fq`WZuO|?uo!wwOsUBpFB`PK35NiR`o?Fl8@QkO@OyUL4vc^w$ zMhzpKaX0KqI$IiRI@8|RjA+J(_%!*FVicYN4nhKuo6Y)u~STozi#|)Dn zj59vZ_(k*N^EX@Qo*)0ddFt~>cms6gf_{_`{%?X_aco`0VqQp@s(pzW> GQ2ztv=R4E@ literal 0 HcmV?d00001 diff --git a/sublime/Pristine Packages/Theme - Default.sublime-package b/sublime/Pristine Packages/Theme - Default.sublime-package index 84f197e66f6aef76316bfea8847d2961e7e13b00..6a905dcf949c8518ff8623c03c4021cef867ff70 100644 GIT binary patch delta 2266 zcmZuy3s6&M8ol423qk~&P#_Ss0;R=aOF|Msz!(CeBBFxe0~f(S0t5oV5am${hz`3# zYn2qgY28&zh0=BhVYa2aKF0cDA6TXBsAUwTtFvO&E)d=X*gs%}(%zZOz4x5+o$vL} z&Ea?K%4_VJniChiysbOjqFWP{Zp$#~wx$b-a##D@$b>wGS#Zhv1DB;yWemdxVwfky z*aV0qA}PMK97^!I7f3}L^lEKdVT8)4QAqtIGCzN*pHvzz2?+6*hWH1_#Rb{9Au}56 z@q$IX7;S+@m#s4D4IWZzw+t7(3SvCw1v1vCO-sXXyuh{8`XrW!{KZ}huE)=p+=_;5 zGH5k9MwL;k&rwL^;$LBN^$D9bd3xQRDZMN(Akbed3;H>3Ha9!2)S76OK0DW-$;-1Q zqP>Y{}6YU(_}So*T7&hF82O&*^!(StP#Y1+lEAK>K_`)~8=j$clzc zO?Z2RHd~W3RlIDOT>P8FtXih{ReGJ?5SOb;)hGf30%Wx5x$uh!v4?!dGGQi{G;nsV z83O%Yg}~BEH~9|?1Gm>jgvCcXK6_8H{<6~fAIEl8#;`sYKlvzR*fG)KR^Hwm8P;C+ zxz>JhH8)(D%RBWS?-E83wkYxLfsM6QtLBCM7$1`n_2XpC(WL6Zr0`AM?fdti9lCdZ zQvAQp)lY`TJ}Gn?l`DCdiTB-ubxoeR5Jb_5C-p zT-jPU{Gr=;@nBaG=NkVf{+qXhB3)Vg*RQ_awUv2L|J`H3otFF{$@`_%ksgY>AC%>t z>uA-Kgr9AUE&cNs&A}6uHt&D?ftDM5PMWz5l)4;={lY&n>UPnU(<#R}4W8WHS+3q` zSzaMQLoSjiho{+_V)#Ytgp8v2e&}D~ee<0<|5#;n(Y`U0!(1sW3(EfTPRo-s7x=HW zepMcm<*(NMbEN&*-uXv0=i7)z;i~3dZE!phS&}G1y(ZIZcX47zTs+^C5A8jwP_tYg zXT!c0y@Ov~^wU7myqlR{th0}IbY;a|^GY{-JJ$L}E!o!4+93Tj=zGDDOShAi%Wpq6 z21eCweh})>oz>m>yRlCboSJdZ(TD4bnvc9bh_E~ICc~&zhAb={hQ(OeM=7=hYx<~t zMVYCo*cl)9fiG(*XBgCvLLhE1Q;BVbDYn85yGE&9jTP{z#AY+BCEf4fs2KvuqeCWs zl{>EQ2VXMuuF3Ol0UokG?;FK4{U9f2$8fkG!im#3$_F5n9OB|z10b{e+k9Zs^g8u) zrPl^PXqUpH&>o!*OD(Vv?FT`~kvRb-EdV!E3{p>?GiDA#1Tnc_(;x)0pSc2K^6wN_ zvri8O`9rkEhXV8)f)yN&JD|t~X7klnev1$rDbHFK(s)2OY_|XpU4|)@uoxx7u$mn3 z!h&H?a6b12yzc=^aiy6`?7U(2+@JcO;|QHax}Wv#j$bLj2CrL%2S%vZ$5K2w0?Ww_ z8Fq|-f}LDO(#uvvrHq#C|$P} zca6as@^v&ej6o#V91BdgR;{KxpBh`$reBn(k3)%t%HNI0LJO?0OWbOe=V-Ba2)wb& zLLKjKLyHB%$notMI!;^Pk$@HB^ziX_VB0vXKXwFc6>zxOs*VrK1+o5oOECe+Vl*gbs&Oie1r(urVLum*XWM^Kzup2nShcs${Xn$Y0Jcu zK*Gt99DE2wz93i+OdkD5-IJG`qB3AE5NCAJ{bI5sdQ2i@4LNAQdO{TJWh1qZZS}OS za=IFH=g_qM`4so~Sb!TjH0^fA%&4p~9U%h19hD~Y)ZSFj|MXc3E9 z62bnvmd3XA3h^FG)zU`f+7KnF{5wY5&|=M3@URVuAkpo3%Z5gszk>o>vYyBuq1u+b LZd>06^b_%ag?TU& delta 1571 zcmZ8fc}!Gy5Pmc7!7dBJf*`Ughf>jvf+rD07Z1>0JS7_3%PsYeK~2nhXcv47b2&&zzjneUtVX1>&m%MOY! z)F~_v6}0)bE5kB1$Z;CfH7UGxaWV3|W{$IL2D=$6^EnQ3R|NX43Az2cqA4^k$+qaY z;r^j>x+5o|sw1lwd#SQCc7-qUQs-Ctik}~l+KaXv445t|c7N^s)%9)P)Nb`V5K!G! zAw02cSnC?9?GA74;_1?4cdNGzFEhIe?Sd{XXqgBTEsMF!vF9EKHD4}@IJu`LPwsn7 zVS3_``i1-I%ITosinZ~4lcccutrYbu3vVV~_&(6F&f~8&t3CGS*A%a+5Vd#>T`dUj zifu1{om*<%!7Y#-taa61v{Z8{t$Tr?eKfDJGu>l$`Qf!II`zG``j6G{9och(SJv8a z^|70)OTv1h+n20(=(0CCzb#eB^S#=!?u__GY)?*EufwTBx1U{8_So!fjkuWIcyiv4 zpJ_{i_T|)d@>lk^JW{!I{~Xric6w=t6Ra+YadWdOce;|1&*-)D4rWA`57op!8WE~a zcGSn8SM5rgP#)Lik*U0H_59)=FYVq-hJ`b$!n_@W|_?({+c zj;^P1ec*#ON}Y46J;nBd1!eYuBg#}%)(49)EsO5;fhRuLs(YF(BcEZA(yV@viH~P< zoH8jZjkXNK6iQOxQsHXiJTQjP{xh{XWQS>KRW?L$HLmwgYl@0Q)iJy+bS| zc>2#% zBOntNdNRo8?(8ugVvxDU3%JcG%8i=M3ABz`PA?eB7y1Al_J?W4ElA08l+|7eV6}P{!|G1ymXZll03_jz=T)X}AP8t=6ByKztpg ze}jH_b`8x148qheXa`^bIz>|h;BvvRmPvFUcc4xlEoe5PBmNLW;fTTJW^uq}Y){Ej zC+(zsz$tW{z4Ac{wy`9zrFV#nc)xZ3M$Tj9Yk8Is8V?k^p55UHvWuwYQHD_obd$$m zOiUy*0lkD-NlbLKc>*Z}l#6>=l&MKhqqoS;VvZ>abWT8-nRPO7Sw^ebnD%3MH^YTS zC97^a;=qQr7=vv$eX>2<4HRL5{`hDEX^sEmo9I^)^v30BWNV6xQKY6wQ`QcpQ>7{T z2yL0b(dUc7#kix7l;%cTaT}2J4tAZsB9!w_S@g~r6|xAUHB={JHkakl8xbz$VIPpR zl=&i;u(7l0la8`=lrF|V;kRR~^@ctf{Um0DzGr||eakM(#*7hG){~DJ`txv;5w)m*M?~WG2B--tQSR{3WYVGa1v%Z-CwGKh-%ig8%>k diff --git a/sublime/Pristine Packages/Vintage.sublime-package b/sublime/Pristine Packages/Vintage.sublime-package index bb824169efa53b76c980d8302c73e14f8fb29d04..a1c984359be84251ad98d5e030238d099b22e858 100644 GIT binary patch delta 399 zcmeCX$hPDX+lB}Gc?Bo`clecfmSz2728Q*U-|p9E6b^EA_Y4kk4T|>*_3=@#RZx#t z-zu3Q(Y|5R#EutN=tM8L0|+r8zkYMXBkT#U-gl)(R>43VHb@ z8JT(MdPo|4^NUgyGV{{%frLU>X0DZjRYqz~fr3&zTv1VKNoi3Y7nq-tnp2Xfu>GJV zV*&F3JEgQZHNLoVySXi6tHksTg^VK8lo>guhX*h!Z|@9a{1bp0s?#SeXY^!TJN?db dMpq_})alAA80{IiO;29IsLCd~jFEwX0RZbgeBA&5 delta 205 zcmZ2-iLLJ<+lB}Gc~5Oz<#1E%^J~e)3=EQ+-|p9E+-z`ge#Q21E5sPxFoTtL`MNkX>OMfXZ#a@ p>Za)$D;Pb2wiK;kbY