mirror of
https://we.phorge.it/source/arcanist.git
synced 2024-11-09 16:32:39 +01:00
[Wilds] Remove libphutil
Summary: Ref T13098. Historically, Phabricator was split into three parts: - Phabricator, the server. - Arcanist, the client. - libphutil, libraries shared between the client and server. One imagined use case for this was that `libphutil` might become a general-purpose library that other projects would use. However, this didn't really happen, and it seems unlikely to at this point: Phabricator has become a relatively more sophisticated application platform; we didn't end up seeing or encouraging much custom development; what custom development there is basically embraces all of Phabricator since there are huge advantages to doing so; and a general "open source is awful" sort of factor here in the sense that open source users often don't have goals well aligned to our goals. Turning "arc" into a client platform and building package management solidify us in this direction of being a standalone platform, not a standalone utility library. Phabricator also depends on `arcanist/`. If it didn't, there would be a small advantage to saying "shared code + client for client, shared code + server for server", but there's no such distinction and it seems unlikely that one will ever exist. Even if it did, I think this has little value. Nowadays, I think this separation has no advantages for us and one significant cost: it makes installing `arcanist` more difficult for end-users. This will need some more finesssing (Phabricator will need some changes for compatibility, and a lot of stuff that still says "libphutil" or "phutil" may eventually want to say "arcanist"), and some stuff (like xhpast) is probably straight-up broken right now and needs some tweaking, but I don't anticipate any major issues here. There was never anything particularly magical about libphutil as a separate standalone library. Test Plan: Ran `arc`, it gets about as far as it did before. Reviewers: amckinley Reviewed By: amckinley Maniphest Tasks: T13098 Differential Revision: https://secure.phabricator.com/D19688
This commit is contained in:
parent
f0608eef9b
commit
8e0e07664a
813 changed files with 203996 additions and 75 deletions
26
.gitignore
vendored
26
.gitignore
vendored
|
@ -12,3 +12,29 @@
|
|||
# User extensions
|
||||
/externals/includes/*
|
||||
/src/extensions/*
|
||||
|
||||
# XHPAST
|
||||
/support/xhpast/*.a
|
||||
/support/xhpast/*.o
|
||||
/support/xhpast/parser.yacc.output
|
||||
/support/xhpast/node_names.hpp
|
||||
/support/xhpast/xhpast
|
||||
/support/xhpast/xhpast.exe
|
||||
/src/parser/xhpast/bin/xhpast
|
||||
|
||||
## NOTE: Don't .gitignore these files! Even though they're build artifacts, we
|
||||
## want to check them in so users can build xhpast without flex/bison.
|
||||
# /support/xhpast/parser.yacc.cpp
|
||||
# /support/xhpast/parser.yacc.hpp
|
||||
# /support/xhpast/scanner.lex.cpp
|
||||
# /support/xhpast/scanner.lex.hpp
|
||||
|
||||
# This is an OS X build artifact.
|
||||
/support/xhpast/xhpast.dSYM
|
||||
|
||||
# libphutil
|
||||
/support/phutiltestlib/.phutil_module_cache
|
||||
|
||||
# This file overrides "default.pem" if present.
|
||||
/resources/ssl/custom.pem
|
||||
|
||||
|
|
769
externals/cldr/cldr_windows_timezones.xml
vendored
Normal file
769
externals/cldr/cldr_windows_timezones.xml
vendored
Normal file
|
@ -0,0 +1,769 @@
|
|||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<!DOCTYPE supplementalData SYSTEM "../../common/dtd/ldmlSupplemental.dtd">
|
||||
<!--
|
||||
Copyright © 1991-2013 Unicode, Inc.
|
||||
CLDR data files are interpreted according to the LDML specification (http://unicode.org/reports/tr35/)
|
||||
For terms of use, see http://www.unicode.org/copyright.html
|
||||
-->
|
||||
|
||||
<supplementalData>
|
||||
<version number="$Revision$"/>
|
||||
<windowsZones>
|
||||
<mapTimezones otherVersion="7e00402" typeVersion="2016i">
|
||||
|
||||
<!-- (UTC-12:00) International Date Line West -->
|
||||
<mapZone other="Dateline Standard Time" territory="001" type="Etc/GMT+12"/>
|
||||
<mapZone other="Dateline Standard Time" territory="ZZ" type="Etc/GMT+12"/>
|
||||
|
||||
<!-- (UTC-11:00) Coordinated Universal Time-11 -->
|
||||
<mapZone other="UTC-11" territory="001" type="Etc/GMT+11"/>
|
||||
<mapZone other="UTC-11" territory="AS" type="Pacific/Pago_Pago"/>
|
||||
<mapZone other="UTC-11" territory="NU" type="Pacific/Niue"/>
|
||||
<mapZone other="UTC-11" territory="UM" type="Pacific/Midway"/>
|
||||
<mapZone other="UTC-11" territory="ZZ" type="Etc/GMT+11"/>
|
||||
|
||||
<!-- (UTC-10:00) Aleutian Islands -->
|
||||
<mapZone other="Aleutian Standard Time" territory="001" type="America/Adak"/>
|
||||
<mapZone other="Aleutian Standard Time" territory="US" type="America/Adak"/>
|
||||
|
||||
<!-- (UTC-10:00) Hawaii -->
|
||||
<mapZone other="Hawaiian Standard Time" territory="001" type="Pacific/Honolulu"/>
|
||||
<mapZone other="Hawaiian Standard Time" territory="CK" type="Pacific/Rarotonga"/>
|
||||
<mapZone other="Hawaiian Standard Time" territory="PF" type="Pacific/Tahiti"/>
|
||||
<mapZone other="Hawaiian Standard Time" territory="UM" type="Pacific/Johnston"/>
|
||||
<mapZone other="Hawaiian Standard Time" territory="US" type="Pacific/Honolulu"/>
|
||||
<mapZone other="Hawaiian Standard Time" territory="ZZ" type="Etc/GMT+10"/>
|
||||
|
||||
<!-- (UTC-09:30) Marquesas Islands -->
|
||||
<mapZone other="Marquesas Standard Time" territory="001" type="Pacific/Marquesas"/>
|
||||
<mapZone other="Marquesas Standard Time" territory="PF" type="Pacific/Marquesas"/>
|
||||
|
||||
<!-- (UTC-09:00) Alaska -->
|
||||
<mapZone other="Alaskan Standard Time" territory="001" type="America/Anchorage"/>
|
||||
<mapZone other="Alaskan Standard Time" territory="US" type="America/Anchorage America/Juneau America/Metlakatla America/Nome America/Sitka America/Yakutat"/>
|
||||
|
||||
<!-- (UTC-09:00) Coordinated Universal Time-09 -->
|
||||
<mapZone other="UTC-09" territory="001" type="Etc/GMT+9"/>
|
||||
<mapZone other="UTC-09" territory="PF" type="Pacific/Gambier"/>
|
||||
<mapZone other="UTC-09" territory="ZZ" type="Etc/GMT+9"/>
|
||||
|
||||
<!-- (UTC-08:00) Baja California -->
|
||||
<mapZone other="Pacific Standard Time (Mexico)" territory="001" type="America/Tijuana"/>
|
||||
<mapZone other="Pacific Standard Time (Mexico)" territory="MX" type="America/Tijuana America/Santa_Isabel"/>
|
||||
|
||||
<!-- (UTC-08:00) Coordinated Universal Time-08 -->
|
||||
<mapZone other="UTC-08" territory="001" type="Etc/GMT+8"/>
|
||||
<mapZone other="UTC-08" territory="PN" type="Pacific/Pitcairn"/>
|
||||
<mapZone other="UTC-08" territory="ZZ" type="Etc/GMT+8"/>
|
||||
|
||||
<!-- (UTC-08:00) Pacific Time (US & Canada) -->
|
||||
<mapZone other="Pacific Standard Time" territory="001" type="America/Los_Angeles"/>
|
||||
<mapZone other="Pacific Standard Time" territory="CA" type="America/Vancouver America/Dawson America/Whitehorse"/>
|
||||
<mapZone other="Pacific Standard Time" territory="US" type="America/Los_Angeles"/>
|
||||
<mapZone other="Pacific Standard Time" territory="ZZ" type="PST8PDT"/>
|
||||
|
||||
<!-- (UTC-07:00) Arizona -->
|
||||
<mapZone other="US Mountain Standard Time" territory="001" type="America/Phoenix"/>
|
||||
<mapZone other="US Mountain Standard Time" territory="CA" type="America/Dawson_Creek America/Creston America/Fort_Nelson"/>
|
||||
<mapZone other="US Mountain Standard Time" territory="MX" type="America/Hermosillo"/>
|
||||
<mapZone other="US Mountain Standard Time" territory="US" type="America/Phoenix"/>
|
||||
<mapZone other="US Mountain Standard Time" territory="ZZ" type="Etc/GMT+7"/>
|
||||
|
||||
<!-- (UTC-07:00) Chihuahua, La Paz, Mazatlan -->
|
||||
<mapZone other="Mountain Standard Time (Mexico)" territory="001" type="America/Chihuahua"/>
|
||||
<mapZone other="Mountain Standard Time (Mexico)" territory="MX" type="America/Chihuahua America/Mazatlan"/>
|
||||
|
||||
<!-- (UTC-07:00) Mountain Time (US & Canada) -->
|
||||
<mapZone other="Mountain Standard Time" territory="001" type="America/Denver"/>
|
||||
<mapZone other="Mountain Standard Time" territory="CA" type="America/Edmonton America/Cambridge_Bay America/Inuvik America/Yellowknife"/>
|
||||
<mapZone other="Mountain Standard Time" territory="MX" type="America/Ojinaga"/>
|
||||
<mapZone other="Mountain Standard Time" territory="US" type="America/Denver America/Boise"/>
|
||||
<mapZone other="Mountain Standard Time" territory="ZZ" type="MST7MDT"/>
|
||||
|
||||
<!-- (UTC-06:00) Central America -->
|
||||
<mapZone other="Central America Standard Time" territory="001" type="America/Guatemala"/>
|
||||
<mapZone other="Central America Standard Time" territory="BZ" type="America/Belize"/>
|
||||
<mapZone other="Central America Standard Time" territory="CR" type="America/Costa_Rica"/>
|
||||
<mapZone other="Central America Standard Time" territory="EC" type="Pacific/Galapagos"/>
|
||||
<mapZone other="Central America Standard Time" territory="GT" type="America/Guatemala"/>
|
||||
<mapZone other="Central America Standard Time" territory="HN" type="America/Tegucigalpa"/>
|
||||
<mapZone other="Central America Standard Time" territory="NI" type="America/Managua"/>
|
||||
<mapZone other="Central America Standard Time" territory="SV" type="America/El_Salvador"/>
|
||||
<mapZone other="Central America Standard Time" territory="ZZ" type="Etc/GMT+6"/>
|
||||
|
||||
<!-- (UTC-06:00) Central Time (US & Canada) -->
|
||||
<mapZone other="Central Standard Time" territory="001" type="America/Chicago"/>
|
||||
<mapZone other="Central Standard Time" territory="CA" type="America/Winnipeg America/Rainy_River America/Rankin_Inlet America/Resolute"/>
|
||||
<mapZone other="Central Standard Time" territory="MX" type="America/Matamoros"/>
|
||||
<mapZone other="Central Standard Time" territory="US" type="America/Chicago America/Indiana/Knox America/Indiana/Tell_City America/Menominee America/North_Dakota/Beulah America/North_Dakota/Center America/North_Dakota/New_Salem"/>
|
||||
<mapZone other="Central Standard Time" territory="ZZ" type="CST6CDT"/>
|
||||
|
||||
<!-- (UTC-06:00) Easter Island -->
|
||||
<mapZone other="Easter Island Standard Time" territory="001" type="Pacific/Easter"/>
|
||||
<mapZone other="Easter Island Standard Time" territory="CL" type="Pacific/Easter"/>
|
||||
|
||||
<!-- (UTC-06:00) Guadalajara, Mexico City, Monterrey -->
|
||||
<mapZone other="Central Standard Time (Mexico)" territory="001" type="America/Mexico_City"/>
|
||||
<mapZone other="Central Standard Time (Mexico)" territory="MX" type="America/Mexico_City America/Bahia_Banderas America/Merida America/Monterrey"/>
|
||||
|
||||
<!-- (UTC-06:00) Saskatchewan -->
|
||||
<mapZone other="Canada Central Standard Time" territory="001" type="America/Regina"/>
|
||||
<mapZone other="Canada Central Standard Time" territory="CA" type="America/Regina America/Swift_Current"/>
|
||||
|
||||
<!-- (UTC-05:00) Bogota, Lima, Quito, Rio Branco -->
|
||||
<mapZone other="SA Pacific Standard Time" territory="001" type="America/Bogota"/>
|
||||
<mapZone other="SA Pacific Standard Time" territory="BR" type="America/Rio_Branco America/Eirunepe"/>
|
||||
<mapZone other="SA Pacific Standard Time" territory="CA" type="America/Coral_Harbour"/>
|
||||
<mapZone other="SA Pacific Standard Time" territory="CO" type="America/Bogota"/>
|
||||
<mapZone other="SA Pacific Standard Time" territory="EC" type="America/Guayaquil"/>
|
||||
<mapZone other="SA Pacific Standard Time" territory="JM" type="America/Jamaica"/>
|
||||
<mapZone other="SA Pacific Standard Time" territory="KY" type="America/Cayman"/>
|
||||
<mapZone other="SA Pacific Standard Time" territory="PA" type="America/Panama"/>
|
||||
<mapZone other="SA Pacific Standard Time" territory="PE" type="America/Lima"/>
|
||||
<mapZone other="SA Pacific Standard Time" territory="ZZ" type="Etc/GMT+5"/>
|
||||
|
||||
<!-- (UTC-05:00) Chetumal -->
|
||||
<mapZone other="Eastern Standard Time (Mexico)" territory="001" type="America/Cancun"/>
|
||||
<mapZone other="Eastern Standard Time (Mexico)" territory="MX" type="America/Cancun"/>
|
||||
|
||||
<!-- (UTC-05:00) Eastern Time (US & Canada) -->
|
||||
<mapZone other="Eastern Standard Time" territory="001" type="America/New_York"/>
|
||||
<mapZone other="Eastern Standard Time" territory="BS" type="America/Nassau"/>
|
||||
<mapZone other="Eastern Standard Time" territory="CA" type="America/Toronto America/Iqaluit America/Montreal America/Nipigon America/Pangnirtung America/Thunder_Bay"/>
|
||||
<mapZone other="Eastern Standard Time" territory="US" type="America/New_York America/Detroit America/Indiana/Petersburg America/Indiana/Vincennes America/Indiana/Winamac America/Kentucky/Monticello America/Louisville"/>
|
||||
<mapZone other="Eastern Standard Time" territory="ZZ" type="EST5EDT"/>
|
||||
|
||||
<!-- (UTC-05:00) Haiti -->
|
||||
<mapZone other="Haiti Standard Time" territory="001" type="America/Port-au-Prince"/>
|
||||
<mapZone other="Haiti Standard Time" territory="HT" type="America/Port-au-Prince"/>
|
||||
|
||||
<!-- (UTC-05:00) Havana -->
|
||||
<mapZone other="Cuba Standard Time" territory="001" type="America/Havana"/>
|
||||
<mapZone other="Cuba Standard Time" territory="CU" type="America/Havana"/>
|
||||
|
||||
<!-- (UTC-05:00) Indiana (East) -->
|
||||
<mapZone other="US Eastern Standard Time" territory="001" type="America/Indianapolis"/>
|
||||
<mapZone other="US Eastern Standard Time" territory="US" type="America/Indianapolis America/Indiana/Marengo America/Indiana/Vevay"/>
|
||||
|
||||
<!-- (UTC-04:00) Asuncion -->
|
||||
<mapZone other="Paraguay Standard Time" territory="001" type="America/Asuncion"/>
|
||||
<mapZone other="Paraguay Standard Time" territory="PY" type="America/Asuncion"/>
|
||||
|
||||
<!-- (UTC-04:00) Atlantic Time (Canada) -->
|
||||
<mapZone other="Atlantic Standard Time" territory="001" type="America/Halifax"/>
|
||||
<mapZone other="Atlantic Standard Time" territory="BM" type="Atlantic/Bermuda"/>
|
||||
<mapZone other="Atlantic Standard Time" territory="CA" type="America/Halifax America/Glace_Bay America/Goose_Bay America/Moncton"/>
|
||||
<mapZone other="Atlantic Standard Time" territory="GL" type="America/Thule"/>
|
||||
|
||||
<!-- (UTC-04:00) Caracas -->
|
||||
<mapZone other="Venezuela Standard Time" territory="001" type="America/Caracas"/>
|
||||
<mapZone other="Venezuela Standard Time" territory="VE" type="America/Caracas"/>
|
||||
|
||||
<!-- (UTC-04:00) Cuiaba -->
|
||||
<mapZone other="Central Brazilian Standard Time" territory="001" type="America/Cuiaba"/>
|
||||
<mapZone other="Central Brazilian Standard Time" territory="BR" type="America/Cuiaba America/Campo_Grande"/>
|
||||
|
||||
<!-- (UTC-04:00) Georgetown, La Paz, Manaus, San Juan -->
|
||||
<mapZone other="SA Western Standard Time" territory="001" type="America/La_Paz"/>
|
||||
<mapZone other="SA Western Standard Time" territory="AG" type="America/Antigua"/>
|
||||
<mapZone other="SA Western Standard Time" territory="AI" type="America/Anguilla"/>
|
||||
<mapZone other="SA Western Standard Time" territory="AW" type="America/Aruba"/>
|
||||
<mapZone other="SA Western Standard Time" territory="BB" type="America/Barbados"/>
|
||||
<mapZone other="SA Western Standard Time" territory="BL" type="America/St_Barthelemy"/>
|
||||
<mapZone other="SA Western Standard Time" territory="BO" type="America/La_Paz"/>
|
||||
<mapZone other="SA Western Standard Time" territory="BQ" type="America/Kralendijk"/>
|
||||
<mapZone other="SA Western Standard Time" territory="BR" type="America/Manaus America/Boa_Vista America/Porto_Velho"/>
|
||||
<mapZone other="SA Western Standard Time" territory="CA" type="America/Blanc-Sablon"/>
|
||||
<mapZone other="SA Western Standard Time" territory="CW" type="America/Curacao"/>
|
||||
<mapZone other="SA Western Standard Time" territory="DM" type="America/Dominica"/>
|
||||
<mapZone other="SA Western Standard Time" territory="DO" type="America/Santo_Domingo"/>
|
||||
<mapZone other="SA Western Standard Time" territory="GD" type="America/Grenada"/>
|
||||
<mapZone other="SA Western Standard Time" territory="GP" type="America/Guadeloupe"/>
|
||||
<mapZone other="SA Western Standard Time" territory="GY" type="America/Guyana"/>
|
||||
<mapZone other="SA Western Standard Time" territory="KN" type="America/St_Kitts"/>
|
||||
<mapZone other="SA Western Standard Time" territory="LC" type="America/St_Lucia"/>
|
||||
<mapZone other="SA Western Standard Time" territory="MF" type="America/Marigot"/>
|
||||
<mapZone other="SA Western Standard Time" territory="MQ" type="America/Martinique"/>
|
||||
<mapZone other="SA Western Standard Time" territory="MS" type="America/Montserrat"/>
|
||||
<mapZone other="SA Western Standard Time" territory="PR" type="America/Puerto_Rico"/>
|
||||
<mapZone other="SA Western Standard Time" territory="SX" type="America/Lower_Princes"/>
|
||||
<mapZone other="SA Western Standard Time" territory="TT" type="America/Port_of_Spain"/>
|
||||
<mapZone other="SA Western Standard Time" territory="VC" type="America/St_Vincent"/>
|
||||
<mapZone other="SA Western Standard Time" territory="VG" type="America/Tortola"/>
|
||||
<mapZone other="SA Western Standard Time" territory="VI" type="America/St_Thomas"/>
|
||||
<mapZone other="SA Western Standard Time" territory="ZZ" type="Etc/GMT+4"/>
|
||||
|
||||
<!-- (UTC-04:00) Santiago -->
|
||||
<mapZone other="Pacific SA Standard Time" territory="001" type="America/Santiago"/>
|
||||
<mapZone other="Pacific SA Standard Time" territory="AQ" type="Antarctica/Palmer"/>
|
||||
<mapZone other="Pacific SA Standard Time" territory="CL" type="America/Santiago"/>
|
||||
|
||||
<!-- (UTC-04:00) Turks and Caicos -->
|
||||
<mapZone other="Turks And Caicos Standard Time" territory="001" type="America/Grand_Turk"/>
|
||||
<mapZone other="Turks And Caicos Standard Time" territory="TC" type="America/Grand_Turk"/>
|
||||
|
||||
<!-- (UTC-03:30) Newfoundland -->
|
||||
<mapZone other="Newfoundland Standard Time" territory="001" type="America/St_Johns"/>
|
||||
<mapZone other="Newfoundland Standard Time" territory="CA" type="America/St_Johns"/>
|
||||
|
||||
<!-- (UTC-03:00) Araguaina -->
|
||||
<mapZone other="Tocantins Standard Time" territory="001" type="America/Araguaina"/>
|
||||
<mapZone other="Tocantins Standard Time" territory="BR" type="America/Araguaina"/>
|
||||
|
||||
<!-- (UTC-03:00) Brasilia -->
|
||||
<mapZone other="E. South America Standard Time" territory="001" type="America/Sao_Paulo"/>
|
||||
<mapZone other="E. South America Standard Time" territory="BR" type="America/Sao_Paulo"/>
|
||||
|
||||
<!-- (UTC-03:00) Cayenne, Fortaleza -->
|
||||
<mapZone other="SA Eastern Standard Time" territory="001" type="America/Cayenne"/>
|
||||
<mapZone other="SA Eastern Standard Time" territory="AQ" type="Antarctica/Rothera"/>
|
||||
<mapZone other="SA Eastern Standard Time" territory="BR" type="America/Fortaleza America/Belem America/Maceio America/Recife America/Santarem"/>
|
||||
<mapZone other="SA Eastern Standard Time" territory="FK" type="Atlantic/Stanley"/>
|
||||
<mapZone other="SA Eastern Standard Time" territory="GF" type="America/Cayenne"/>
|
||||
<mapZone other="SA Eastern Standard Time" territory="SR" type="America/Paramaribo"/>
|
||||
<mapZone other="SA Eastern Standard Time" territory="ZZ" type="Etc/GMT+3"/>
|
||||
|
||||
<!-- (UTC-03:00) City of Buenos Aires -->
|
||||
<mapZone other="Argentina Standard Time" territory="001" type="America/Buenos_Aires"/>
|
||||
<mapZone other="Argentina Standard Time" territory="AR" type="America/Buenos_Aires America/Argentina/La_Rioja America/Argentina/Rio_Gallegos America/Argentina/Salta America/Argentina/San_Juan America/Argentina/San_Luis America/Argentina/Tucuman America/Argentina/Ushuaia America/Catamarca America/Cordoba America/Jujuy America/Mendoza"/>
|
||||
|
||||
<!-- (UTC-03:00) Greenland -->
|
||||
<mapZone other="Greenland Standard Time" territory="001" type="America/Godthab"/>
|
||||
<mapZone other="Greenland Standard Time" territory="GL" type="America/Godthab"/>
|
||||
|
||||
<!-- (UTC-03:00) Montevideo -->
|
||||
<mapZone other="Montevideo Standard Time" territory="001" type="America/Montevideo"/>
|
||||
<mapZone other="Montevideo Standard Time" territory="UY" type="America/Montevideo"/>
|
||||
|
||||
<!-- (UTC-03:00) Saint Pierre and Miquelon -->
|
||||
<mapZone other="Saint Pierre Standard Time" territory="001" type="America/Miquelon"/>
|
||||
<mapZone other="Saint Pierre Standard Time" territory="PM" type="America/Miquelon"/>
|
||||
|
||||
<!-- (UTC-03:00) Salvador -->
|
||||
<mapZone other="Bahia Standard Time" territory="001" type="America/Bahia"/>
|
||||
<mapZone other="Bahia Standard Time" territory="BR" type="America/Bahia"/>
|
||||
|
||||
<!-- (UTC-02:00) Coordinated Universal Time-02 -->
|
||||
<mapZone other="UTC-02" territory="001" type="Etc/GMT+2"/>
|
||||
<mapZone other="UTC-02" territory="BR" type="America/Noronha"/>
|
||||
<mapZone other="UTC-02" territory="GS" type="Atlantic/South_Georgia"/>
|
||||
<mapZone other="UTC-02" territory="ZZ" type="Etc/GMT+2"/>
|
||||
|
||||
<!-- (UTC-01:00) Azores -->
|
||||
<mapZone other="Azores Standard Time" territory="001" type="Atlantic/Azores"/>
|
||||
<mapZone other="Azores Standard Time" territory="GL" type="America/Scoresbysund"/>
|
||||
<mapZone other="Azores Standard Time" territory="PT" type="Atlantic/Azores"/>
|
||||
|
||||
<!-- (UTC-01:00) Cabo Verde Is. -->
|
||||
<mapZone other="Cape Verde Standard Time" territory="001" type="Atlantic/Cape_Verde"/>
|
||||
<mapZone other="Cape Verde Standard Time" territory="CV" type="Atlantic/Cape_Verde"/>
|
||||
<mapZone other="Cape Verde Standard Time" territory="ZZ" type="Etc/GMT+1"/>
|
||||
|
||||
<!-- (UTC) Coordinated Universal Time -->
|
||||
<mapZone other="UTC" territory="001" type="Etc/GMT"/>
|
||||
<mapZone other="UTC" territory="GL" type="America/Danmarkshavn"/>
|
||||
<mapZone other="UTC" territory="ZZ" type="Etc/GMT"/>
|
||||
|
||||
<!-- (UTC+00:00) Casablanca -->
|
||||
<mapZone other="Morocco Standard Time" territory="001" type="Africa/Casablanca"/>
|
||||
<mapZone other="Morocco Standard Time" territory="EH" type="Africa/El_Aaiun"/>
|
||||
<mapZone other="Morocco Standard Time" territory="MA" type="Africa/Casablanca"/>
|
||||
|
||||
<!-- (UTC+00:00) Dublin, Edinburgh, Lisbon, London -->
|
||||
<mapZone other="GMT Standard Time" territory="001" type="Europe/London"/>
|
||||
<mapZone other="GMT Standard Time" territory="ES" type="Atlantic/Canary"/>
|
||||
<mapZone other="GMT Standard Time" territory="FO" type="Atlantic/Faeroe"/>
|
||||
<mapZone other="GMT Standard Time" territory="GB" type="Europe/London"/>
|
||||
<mapZone other="GMT Standard Time" territory="GG" type="Europe/Guernsey"/>
|
||||
<mapZone other="GMT Standard Time" territory="IE" type="Europe/Dublin"/>
|
||||
<mapZone other="GMT Standard Time" territory="IM" type="Europe/Isle_of_Man"/>
|
||||
<mapZone other="GMT Standard Time" territory="JE" type="Europe/Jersey"/>
|
||||
<mapZone other="GMT Standard Time" territory="PT" type="Europe/Lisbon Atlantic/Madeira"/>
|
||||
|
||||
<!-- (UTC+00:00) Monrovia, Reykjavik -->
|
||||
<mapZone other="Greenwich Standard Time" territory="001" type="Atlantic/Reykjavik"/>
|
||||
<mapZone other="Greenwich Standard Time" territory="BF" type="Africa/Ouagadougou"/>
|
||||
<mapZone other="Greenwich Standard Time" territory="CI" type="Africa/Abidjan"/>
|
||||
<mapZone other="Greenwich Standard Time" territory="GH" type="Africa/Accra"/>
|
||||
<mapZone other="Greenwich Standard Time" territory="GM" type="Africa/Banjul"/>
|
||||
<mapZone other="Greenwich Standard Time" territory="GN" type="Africa/Conakry"/>
|
||||
<mapZone other="Greenwich Standard Time" territory="GW" type="Africa/Bissau"/>
|
||||
<mapZone other="Greenwich Standard Time" territory="IS" type="Atlantic/Reykjavik"/>
|
||||
<mapZone other="Greenwich Standard Time" territory="LR" type="Africa/Monrovia"/>
|
||||
<mapZone other="Greenwich Standard Time" territory="ML" type="Africa/Bamako"/>
|
||||
<mapZone other="Greenwich Standard Time" territory="MR" type="Africa/Nouakchott"/>
|
||||
<mapZone other="Greenwich Standard Time" territory="SH" type="Atlantic/St_Helena"/>
|
||||
<mapZone other="Greenwich Standard Time" territory="SL" type="Africa/Freetown"/>
|
||||
<mapZone other="Greenwich Standard Time" territory="SN" type="Africa/Dakar"/>
|
||||
<mapZone other="Greenwich Standard Time" territory="ST" type="Africa/Sao_Tome"/>
|
||||
<mapZone other="Greenwich Standard Time" territory="TG" type="Africa/Lome"/>
|
||||
|
||||
<!-- (UTC+01:00) Amsterdam, Berlin, Bern, Rome, Stockholm, Vienna -->
|
||||
<mapZone other="W. Europe Standard Time" territory="001" type="Europe/Berlin"/>
|
||||
<mapZone other="W. Europe Standard Time" territory="AD" type="Europe/Andorra"/>
|
||||
<mapZone other="W. Europe Standard Time" territory="AT" type="Europe/Vienna"/>
|
||||
<mapZone other="W. Europe Standard Time" territory="CH" type="Europe/Zurich"/>
|
||||
<mapZone other="W. Europe Standard Time" territory="DE" type="Europe/Berlin Europe/Busingen"/>
|
||||
<mapZone other="W. Europe Standard Time" territory="GI" type="Europe/Gibraltar"/>
|
||||
<mapZone other="W. Europe Standard Time" territory="IT" type="Europe/Rome"/>
|
||||
<mapZone other="W. Europe Standard Time" territory="LI" type="Europe/Vaduz"/>
|
||||
<mapZone other="W. Europe Standard Time" territory="LU" type="Europe/Luxembourg"/>
|
||||
<mapZone other="W. Europe Standard Time" territory="MC" type="Europe/Monaco"/>
|
||||
<mapZone other="W. Europe Standard Time" territory="MT" type="Europe/Malta"/>
|
||||
<mapZone other="W. Europe Standard Time" territory="NL" type="Europe/Amsterdam"/>
|
||||
<mapZone other="W. Europe Standard Time" territory="NO" type="Europe/Oslo"/>
|
||||
<mapZone other="W. Europe Standard Time" territory="SE" type="Europe/Stockholm"/>
|
||||
<mapZone other="W. Europe Standard Time" territory="SJ" type="Arctic/Longyearbyen"/>
|
||||
<mapZone other="W. Europe Standard Time" territory="SM" type="Europe/San_Marino"/>
|
||||
<mapZone other="W. Europe Standard Time" territory="VA" type="Europe/Vatican"/>
|
||||
|
||||
<!-- (UTC+01:00) Belgrade, Bratislava, Budapest, Ljubljana, Prague -->
|
||||
<mapZone other="Central Europe Standard Time" territory="001" type="Europe/Budapest"/>
|
||||
<mapZone other="Central Europe Standard Time" territory="AL" type="Europe/Tirane"/>
|
||||
<mapZone other="Central Europe Standard Time" territory="CZ" type="Europe/Prague"/>
|
||||
<mapZone other="Central Europe Standard Time" territory="HU" type="Europe/Budapest"/>
|
||||
<mapZone other="Central Europe Standard Time" territory="ME" type="Europe/Podgorica"/>
|
||||
<mapZone other="Central Europe Standard Time" territory="RS" type="Europe/Belgrade"/>
|
||||
<mapZone other="Central Europe Standard Time" territory="SI" type="Europe/Ljubljana"/>
|
||||
<mapZone other="Central Europe Standard Time" territory="SK" type="Europe/Bratislava"/>
|
||||
|
||||
<!-- (UTC+01:00) Brussels, Copenhagen, Madrid, Paris -->
|
||||
<mapZone other="Romance Standard Time" territory="001" type="Europe/Paris"/>
|
||||
<mapZone other="Romance Standard Time" territory="BE" type="Europe/Brussels"/>
|
||||
<mapZone other="Romance Standard Time" territory="DK" type="Europe/Copenhagen"/>
|
||||
<mapZone other="Romance Standard Time" territory="ES" type="Europe/Madrid Africa/Ceuta"/>
|
||||
<mapZone other="Romance Standard Time" territory="FR" type="Europe/Paris"/>
|
||||
|
||||
<!-- (UTC+01:00) Sarajevo, Skopje, Warsaw, Zagreb -->
|
||||
<mapZone other="Central European Standard Time" territory="001" type="Europe/Warsaw"/>
|
||||
<mapZone other="Central European Standard Time" territory="BA" type="Europe/Sarajevo"/>
|
||||
<mapZone other="Central European Standard Time" territory="HR" type="Europe/Zagreb"/>
|
||||
<mapZone other="Central European Standard Time" territory="MK" type="Europe/Skopje"/>
|
||||
<mapZone other="Central European Standard Time" territory="PL" type="Europe/Warsaw"/>
|
||||
|
||||
<!-- (UTC+01:00) West Central Africa -->
|
||||
<mapZone other="W. Central Africa Standard Time" territory="001" type="Africa/Lagos"/>
|
||||
<mapZone other="W. Central Africa Standard Time" territory="AO" type="Africa/Luanda"/>
|
||||
<mapZone other="W. Central Africa Standard Time" territory="BJ" type="Africa/Porto-Novo"/>
|
||||
<mapZone other="W. Central Africa Standard Time" territory="CD" type="Africa/Kinshasa"/>
|
||||
<mapZone other="W. Central Africa Standard Time" territory="CF" type="Africa/Bangui"/>
|
||||
<mapZone other="W. Central Africa Standard Time" territory="CG" type="Africa/Brazzaville"/>
|
||||
<mapZone other="W. Central Africa Standard Time" territory="CM" type="Africa/Douala"/>
|
||||
<mapZone other="W. Central Africa Standard Time" territory="DZ" type="Africa/Algiers"/>
|
||||
<mapZone other="W. Central Africa Standard Time" territory="GA" type="Africa/Libreville"/>
|
||||
<mapZone other="W. Central Africa Standard Time" territory="GQ" type="Africa/Malabo"/>
|
||||
<mapZone other="W. Central Africa Standard Time" territory="NE" type="Africa/Niamey"/>
|
||||
<mapZone other="W. Central Africa Standard Time" territory="NG" type="Africa/Lagos"/>
|
||||
<mapZone other="W. Central Africa Standard Time" territory="TD" type="Africa/Ndjamena"/>
|
||||
<mapZone other="W. Central Africa Standard Time" territory="TN" type="Africa/Tunis"/>
|
||||
<mapZone other="W. Central Africa Standard Time" territory="ZZ" type="Etc/GMT-1"/>
|
||||
|
||||
<!-- (UTC+01:00) Windhoek -->
|
||||
<mapZone other="Namibia Standard Time" territory="001" type="Africa/Windhoek"/>
|
||||
<mapZone other="Namibia Standard Time" territory="NA" type="Africa/Windhoek"/>
|
||||
|
||||
<!-- (UTC+02:00) Amman -->
|
||||
<mapZone other="Jordan Standard Time" territory="001" type="Asia/Amman"/>
|
||||
<mapZone other="Jordan Standard Time" territory="JO" type="Asia/Amman"/>
|
||||
|
||||
<!-- (UTC+02:00) Athens, Bucharest -->
|
||||
<mapZone other="GTB Standard Time" territory="001" type="Europe/Bucharest"/>
|
||||
<mapZone other="GTB Standard Time" territory="CY" type="Asia/Nicosia"/>
|
||||
<mapZone other="GTB Standard Time" territory="GR" type="Europe/Athens"/>
|
||||
<mapZone other="GTB Standard Time" territory="RO" type="Europe/Bucharest"/>
|
||||
|
||||
<!-- (UTC+02:00) Beirut -->
|
||||
<mapZone other="Middle East Standard Time" territory="001" type="Asia/Beirut"/>
|
||||
<mapZone other="Middle East Standard Time" territory="LB" type="Asia/Beirut"/>
|
||||
|
||||
<!-- (UTC+02:00) Cairo -->
|
||||
<mapZone other="Egypt Standard Time" territory="001" type="Africa/Cairo"/>
|
||||
<mapZone other="Egypt Standard Time" territory="EG" type="Africa/Cairo"/>
|
||||
|
||||
<!-- (UTC+02:00) Chisinau -->
|
||||
<mapZone other="E. Europe Standard Time" territory="001" type="Europe/Chisinau"/>
|
||||
<mapZone other="E. Europe Standard Time" territory="MD" type="Europe/Chisinau"/>
|
||||
|
||||
<!-- (UTC+02:00) Damascus -->
|
||||
<mapZone other="Syria Standard Time" territory="001" type="Asia/Damascus"/>
|
||||
<mapZone other="Syria Standard Time" territory="SY" type="Asia/Damascus"/>
|
||||
|
||||
<!-- (UTC+02:00) Gaza, Hebron -->
|
||||
<mapZone other="West Bank Standard Time" territory="001" type="Asia/Hebron"/>
|
||||
<mapZone other="West Bank Standard Time" territory="PS" type="Asia/Hebron Asia/Gaza"/>
|
||||
|
||||
<!-- (UTC+02:00) Harare, Pretoria -->
|
||||
<mapZone other="South Africa Standard Time" territory="001" type="Africa/Johannesburg"/>
|
||||
<mapZone other="South Africa Standard Time" territory="BI" type="Africa/Bujumbura"/>
|
||||
<mapZone other="South Africa Standard Time" territory="BW" type="Africa/Gaborone"/>
|
||||
<mapZone other="South Africa Standard Time" territory="CD" type="Africa/Lubumbashi"/>
|
||||
<mapZone other="South Africa Standard Time" territory="LS" type="Africa/Maseru"/>
|
||||
<mapZone other="South Africa Standard Time" territory="MW" type="Africa/Blantyre"/>
|
||||
<mapZone other="South Africa Standard Time" territory="MZ" type="Africa/Maputo"/>
|
||||
<mapZone other="South Africa Standard Time" territory="RW" type="Africa/Kigali"/>
|
||||
<mapZone other="South Africa Standard Time" territory="SZ" type="Africa/Mbabane"/>
|
||||
<mapZone other="South Africa Standard Time" territory="ZA" type="Africa/Johannesburg"/>
|
||||
<mapZone other="South Africa Standard Time" territory="ZM" type="Africa/Lusaka"/>
|
||||
<mapZone other="South Africa Standard Time" territory="ZW" type="Africa/Harare"/>
|
||||
<mapZone other="South Africa Standard Time" territory="ZZ" type="Etc/GMT-2"/>
|
||||
|
||||
<!-- (UTC+02:00) Helsinki, Kyiv, Riga, Sofia, Tallinn, Vilnius -->
|
||||
<mapZone other="FLE Standard Time" territory="001" type="Europe/Kiev"/>
|
||||
<mapZone other="FLE Standard Time" territory="AX" type="Europe/Mariehamn"/>
|
||||
<mapZone other="FLE Standard Time" territory="BG" type="Europe/Sofia"/>
|
||||
<mapZone other="FLE Standard Time" territory="EE" type="Europe/Tallinn"/>
|
||||
<mapZone other="FLE Standard Time" territory="FI" type="Europe/Helsinki"/>
|
||||
<mapZone other="FLE Standard Time" territory="LT" type="Europe/Vilnius"/>
|
||||
<mapZone other="FLE Standard Time" territory="LV" type="Europe/Riga"/>
|
||||
<mapZone other="FLE Standard Time" territory="UA" type="Europe/Kiev Europe/Uzhgorod Europe/Zaporozhye"/>
|
||||
|
||||
<!-- (UTC+02:00) Istanbul -->
|
||||
<mapZone other="Turkey Standard Time" territory="001" type="Europe/Istanbul"/>
|
||||
<mapZone other="Turkey Standard Time" territory="TR" type="Europe/Istanbul"/>
|
||||
|
||||
<!-- (UTC+02:00) Jerusalem -->
|
||||
<mapZone other="Israel Standard Time" territory="001" type="Asia/Jerusalem"/>
|
||||
<mapZone other="Israel Standard Time" territory="IL" type="Asia/Jerusalem"/>
|
||||
|
||||
<!-- (UTC+02:00) Kaliningrad -->
|
||||
<mapZone other="Kaliningrad Standard Time" territory="001" type="Europe/Kaliningrad"/>
|
||||
<mapZone other="Kaliningrad Standard Time" territory="RU" type="Europe/Kaliningrad"/>
|
||||
|
||||
<!-- (UTC+02:00) Tripoli -->
|
||||
<mapZone other="Libya Standard Time" territory="001" type="Africa/Tripoli"/>
|
||||
<mapZone other="Libya Standard Time" territory="LY" type="Africa/Tripoli"/>
|
||||
|
||||
<!-- (UTC+03:00) Baghdad -->
|
||||
<mapZone other="Arabic Standard Time" territory="001" type="Asia/Baghdad"/>
|
||||
<mapZone other="Arabic Standard Time" territory="IQ" type="Asia/Baghdad"/>
|
||||
|
||||
<!-- (UTC+03:00) Kuwait, Riyadh -->
|
||||
<mapZone other="Arab Standard Time" territory="001" type="Asia/Riyadh"/>
|
||||
<mapZone other="Arab Standard Time" territory="BH" type="Asia/Bahrain"/>
|
||||
<mapZone other="Arab Standard Time" territory="KW" type="Asia/Kuwait"/>
|
||||
<mapZone other="Arab Standard Time" territory="QA" type="Asia/Qatar"/>
|
||||
<mapZone other="Arab Standard Time" territory="SA" type="Asia/Riyadh"/>
|
||||
<mapZone other="Arab Standard Time" territory="YE" type="Asia/Aden"/>
|
||||
|
||||
<!-- (UTC+03:00) Minsk -->
|
||||
<mapZone other="Belarus Standard Time" territory="001" type="Europe/Minsk"/>
|
||||
<mapZone other="Belarus Standard Time" territory="BY" type="Europe/Minsk"/>
|
||||
|
||||
<!-- (UTC+03:00) Moscow, St. Petersburg, Volgograd -->
|
||||
<mapZone other="Russian Standard Time" territory="001" type="Europe/Moscow"/>
|
||||
<mapZone other="Russian Standard Time" territory="RU" type="Europe/Moscow Europe/Kirov Europe/Volgograd"/>
|
||||
<mapZone other="Russian Standard Time" territory="UA" type="Europe/Simferopol"/>
|
||||
|
||||
<!-- (UTC+03:00) Nairobi -->
|
||||
<mapZone other="E. Africa Standard Time" territory="001" type="Africa/Nairobi"/>
|
||||
<mapZone other="E. Africa Standard Time" territory="AQ" type="Antarctica/Syowa"/>
|
||||
<mapZone other="E. Africa Standard Time" territory="DJ" type="Africa/Djibouti"/>
|
||||
<mapZone other="E. Africa Standard Time" territory="ER" type="Africa/Asmera"/>
|
||||
<mapZone other="E. Africa Standard Time" territory="ET" type="Africa/Addis_Ababa"/>
|
||||
<mapZone other="E. Africa Standard Time" territory="KE" type="Africa/Nairobi"/>
|
||||
<mapZone other="E. Africa Standard Time" territory="KM" type="Indian/Comoro"/>
|
||||
<mapZone other="E. Africa Standard Time" territory="MG" type="Indian/Antananarivo"/>
|
||||
<mapZone other="E. Africa Standard Time" territory="SD" type="Africa/Khartoum"/>
|
||||
<mapZone other="E. Africa Standard Time" territory="SO" type="Africa/Mogadishu"/>
|
||||
<mapZone other="E. Africa Standard Time" territory="SS" type="Africa/Juba"/>
|
||||
<mapZone other="E. Africa Standard Time" territory="TZ" type="Africa/Dar_es_Salaam"/>
|
||||
<mapZone other="E. Africa Standard Time" territory="UG" type="Africa/Kampala"/>
|
||||
<mapZone other="E. Africa Standard Time" territory="YT" type="Indian/Mayotte"/>
|
||||
<mapZone other="E. Africa Standard Time" territory="ZZ" type="Etc/GMT-3"/>
|
||||
|
||||
<!-- (UTC+03:30) Tehran -->
|
||||
<mapZone other="Iran Standard Time" territory="001" type="Asia/Tehran"/>
|
||||
<mapZone other="Iran Standard Time" territory="IR" type="Asia/Tehran"/>
|
||||
|
||||
<!-- (UTC+04:00) Abu Dhabi, Muscat -->
|
||||
<mapZone other="Arabian Standard Time" territory="001" type="Asia/Dubai"/>
|
||||
<mapZone other="Arabian Standard Time" territory="AE" type="Asia/Dubai"/>
|
||||
<mapZone other="Arabian Standard Time" territory="OM" type="Asia/Muscat"/>
|
||||
<mapZone other="Arabian Standard Time" territory="ZZ" type="Etc/GMT-4"/>
|
||||
|
||||
<!-- (UTC+04:00) Astrakhan, Ulyanovsk -->
|
||||
<mapZone other="Astrakhan Standard Time" territory="001" type="Europe/Astrakhan"/>
|
||||
<mapZone other="Astrakhan Standard Time" territory="RU" type="Europe/Astrakhan Europe/Ulyanovsk"/>
|
||||
|
||||
<!-- (UTC+04:00) Baku -->
|
||||
<mapZone other="Azerbaijan Standard Time" territory="001" type="Asia/Baku"/>
|
||||
<mapZone other="Azerbaijan Standard Time" territory="AZ" type="Asia/Baku"/>
|
||||
|
||||
<!-- (UTC+04:00) Izhevsk, Samara -->
|
||||
<mapZone other="Russia Time Zone 3" territory="001" type="Europe/Samara"/>
|
||||
<mapZone other="Russia Time Zone 3" territory="RU" type="Europe/Samara"/>
|
||||
|
||||
<!-- (UTC+04:00) Port Louis -->
|
||||
<mapZone other="Mauritius Standard Time" territory="001" type="Indian/Mauritius"/>
|
||||
<mapZone other="Mauritius Standard Time" territory="MU" type="Indian/Mauritius"/>
|
||||
<mapZone other="Mauritius Standard Time" territory="RE" type="Indian/Reunion"/>
|
||||
<mapZone other="Mauritius Standard Time" territory="SC" type="Indian/Mahe"/>
|
||||
|
||||
<!-- (UTC+04:00) Tbilisi -->
|
||||
<mapZone other="Georgian Standard Time" territory="001" type="Asia/Tbilisi"/>
|
||||
<mapZone other="Georgian Standard Time" territory="GE" type="Asia/Tbilisi"/>
|
||||
|
||||
<!-- (UTC+04:00) Yerevan -->
|
||||
<mapZone other="Caucasus Standard Time" territory="001" type="Asia/Yerevan"/>
|
||||
<mapZone other="Caucasus Standard Time" territory="AM" type="Asia/Yerevan"/>
|
||||
|
||||
<!-- (UTC+04:30) Kabul -->
|
||||
<mapZone other="Afghanistan Standard Time" territory="001" type="Asia/Kabul"/>
|
||||
<mapZone other="Afghanistan Standard Time" territory="AF" type="Asia/Kabul"/>
|
||||
|
||||
<!-- (UTC+05:00) Ashgabat, Tashkent -->
|
||||
<mapZone other="West Asia Standard Time" territory="001" type="Asia/Tashkent"/>
|
||||
<mapZone other="West Asia Standard Time" territory="AQ" type="Antarctica/Mawson"/>
|
||||
<mapZone other="West Asia Standard Time" territory="KZ" type="Asia/Oral Asia/Aqtau Asia/Aqtobe"/>
|
||||
<mapZone other="West Asia Standard Time" territory="MV" type="Indian/Maldives"/>
|
||||
<mapZone other="West Asia Standard Time" territory="TF" type="Indian/Kerguelen"/>
|
||||
<mapZone other="West Asia Standard Time" territory="TJ" type="Asia/Dushanbe"/>
|
||||
<mapZone other="West Asia Standard Time" territory="TM" type="Asia/Ashgabat"/>
|
||||
<mapZone other="West Asia Standard Time" territory="UZ" type="Asia/Tashkent Asia/Samarkand"/>
|
||||
<mapZone other="West Asia Standard Time" territory="ZZ" type="Etc/GMT-5"/>
|
||||
|
||||
<!-- (UTC+05:00) Ekaterinburg -->
|
||||
<mapZone other="Ekaterinburg Standard Time" territory="001" type="Asia/Yekaterinburg"/>
|
||||
<mapZone other="Ekaterinburg Standard Time" territory="RU" type="Asia/Yekaterinburg"/>
|
||||
|
||||
<!-- (UTC+05:00) Islamabad, Karachi -->
|
||||
<mapZone other="Pakistan Standard Time" territory="001" type="Asia/Karachi"/>
|
||||
<mapZone other="Pakistan Standard Time" territory="PK" type="Asia/Karachi"/>
|
||||
|
||||
<!-- (UTC+05:30) Chennai, Kolkata, Mumbai, New Delhi -->
|
||||
<mapZone other="India Standard Time" territory="001" type="Asia/Calcutta"/>
|
||||
<mapZone other="India Standard Time" territory="IN" type="Asia/Calcutta"/>
|
||||
|
||||
<!-- (UTC+05:30) Sri Jayawardenepura -->
|
||||
<mapZone other="Sri Lanka Standard Time" territory="001" type="Asia/Colombo"/>
|
||||
<mapZone other="Sri Lanka Standard Time" territory="LK" type="Asia/Colombo"/>
|
||||
|
||||
<!-- (UTC+05:45) Kathmandu -->
|
||||
<mapZone other="Nepal Standard Time" territory="001" type="Asia/Katmandu"/>
|
||||
<mapZone other="Nepal Standard Time" territory="NP" type="Asia/Katmandu"/>
|
||||
|
||||
<!-- (UTC+06:00) Astana -->
|
||||
<mapZone other="Central Asia Standard Time" territory="001" type="Asia/Almaty"/>
|
||||
<mapZone other="Central Asia Standard Time" territory="AQ" type="Antarctica/Vostok"/>
|
||||
<mapZone other="Central Asia Standard Time" territory="CN" type="Asia/Urumqi"/>
|
||||
<mapZone other="Central Asia Standard Time" territory="IO" type="Indian/Chagos"/>
|
||||
<mapZone other="Central Asia Standard Time" territory="KG" type="Asia/Bishkek"/>
|
||||
<mapZone other="Central Asia Standard Time" territory="KZ" type="Asia/Almaty Asia/Qyzylorda"/>
|
||||
<mapZone other="Central Asia Standard Time" territory="ZZ" type="Etc/GMT-6"/>
|
||||
|
||||
<!-- (UTC+06:00) Dhaka -->
|
||||
<mapZone other="Bangladesh Standard Time" territory="001" type="Asia/Dhaka"/>
|
||||
<mapZone other="Bangladesh Standard Time" territory="BD" type="Asia/Dhaka"/>
|
||||
<mapZone other="Bangladesh Standard Time" territory="BT" type="Asia/Thimphu"/>
|
||||
|
||||
<!-- (UTC+06:00) Omsk -->
|
||||
<mapZone other="Omsk Standard Time" territory="001" type="Asia/Omsk"/>
|
||||
<mapZone other="Omsk Standard Time" territory="RU" type="Asia/Omsk"/>
|
||||
|
||||
<!-- (UTC+06:30) Yangon (Rangoon) -->
|
||||
<mapZone other="Myanmar Standard Time" territory="001" type="Asia/Rangoon"/>
|
||||
<mapZone other="Myanmar Standard Time" territory="CC" type="Indian/Cocos"/>
|
||||
<mapZone other="Myanmar Standard Time" territory="MM" type="Asia/Rangoon"/>
|
||||
|
||||
<!-- (UTC+07:00) Bangkok, Hanoi, Jakarta -->
|
||||
<mapZone other="SE Asia Standard Time" territory="001" type="Asia/Bangkok"/>
|
||||
<mapZone other="SE Asia Standard Time" territory="AQ" type="Antarctica/Davis"/>
|
||||
<mapZone other="SE Asia Standard Time" territory="CX" type="Indian/Christmas"/>
|
||||
<mapZone other="SE Asia Standard Time" territory="ID" type="Asia/Jakarta Asia/Pontianak"/>
|
||||
<mapZone other="SE Asia Standard Time" territory="KH" type="Asia/Phnom_Penh"/>
|
||||
<mapZone other="SE Asia Standard Time" territory="LA" type="Asia/Vientiane"/>
|
||||
<mapZone other="SE Asia Standard Time" territory="TH" type="Asia/Bangkok"/>
|
||||
<mapZone other="SE Asia Standard Time" territory="VN" type="Asia/Saigon"/>
|
||||
<mapZone other="SE Asia Standard Time" territory="ZZ" type="Etc/GMT-7"/>
|
||||
|
||||
<!-- (UTC+07:00) Barnaul, Gorno-Altaysk -->
|
||||
<mapZone other="Altai Standard Time" territory="001" type="Asia/Barnaul"/>
|
||||
<mapZone other="Altai Standard Time" territory="RU" type="Asia/Barnaul"/>
|
||||
|
||||
<!-- (UTC+07:00) Hovd -->
|
||||
<mapZone other="W. Mongolia Standard Time" territory="001" type="Asia/Hovd"/>
|
||||
<mapZone other="W. Mongolia Standard Time" territory="MN" type="Asia/Hovd"/>
|
||||
|
||||
<!-- (UTC+07:00) Krasnoyarsk -->
|
||||
<mapZone other="North Asia Standard Time" territory="001" type="Asia/Krasnoyarsk"/>
|
||||
<mapZone other="North Asia Standard Time" territory="RU" type="Asia/Krasnoyarsk Asia/Novokuznetsk"/>
|
||||
|
||||
<!-- (UTC+07:00) Novosibirsk -->
|
||||
<mapZone other="N. Central Asia Standard Time" territory="001" type="Asia/Novosibirsk"/>
|
||||
<mapZone other="N. Central Asia Standard Time" territory="RU" type="Asia/Novosibirsk"/>
|
||||
|
||||
<!-- (UTC+07:00) Tomsk -->
|
||||
<mapZone other="Tomsk Standard Time" territory="001" type="Asia/Tomsk"/>
|
||||
<mapZone other="Tomsk Standard Time" territory="RU" type="Asia/Tomsk"/>
|
||||
|
||||
<!-- (UTC+08:00) Beijing, Chongqing, Hong Kong, Urumqi -->
|
||||
<mapZone other="China Standard Time" territory="001" type="Asia/Shanghai"/>
|
||||
<mapZone other="China Standard Time" territory="CN" type="Asia/Shanghai"/>
|
||||
<mapZone other="China Standard Time" territory="HK" type="Asia/Hong_Kong"/>
|
||||
<mapZone other="China Standard Time" territory="MO" type="Asia/Macau"/>
|
||||
|
||||
<!-- (UTC+08:00) Irkutsk -->
|
||||
<mapZone other="North Asia East Standard Time" territory="001" type="Asia/Irkutsk"/>
|
||||
<mapZone other="North Asia East Standard Time" territory="RU" type="Asia/Irkutsk"/>
|
||||
|
||||
<!-- (UTC+08:00) Kuala Lumpur, Singapore -->
|
||||
<mapZone other="Singapore Standard Time" territory="001" type="Asia/Singapore"/>
|
||||
<mapZone other="Singapore Standard Time" territory="BN" type="Asia/Brunei"/>
|
||||
<mapZone other="Singapore Standard Time" territory="ID" type="Asia/Makassar"/>
|
||||
<mapZone other="Singapore Standard Time" territory="MY" type="Asia/Kuala_Lumpur Asia/Kuching"/>
|
||||
<mapZone other="Singapore Standard Time" territory="PH" type="Asia/Manila"/>
|
||||
<mapZone other="Singapore Standard Time" territory="SG" type="Asia/Singapore"/>
|
||||
<mapZone other="Singapore Standard Time" territory="ZZ" type="Etc/GMT-8"/>
|
||||
|
||||
<!-- (UTC+08:00) Perth -->
|
||||
<mapZone other="W. Australia Standard Time" territory="001" type="Australia/Perth"/>
|
||||
<mapZone other="W. Australia Standard Time" territory="AU" type="Australia/Perth"/>
|
||||
|
||||
<!-- (UTC+08:00) Taipei -->
|
||||
<mapZone other="Taipei Standard Time" territory="001" type="Asia/Taipei"/>
|
||||
<mapZone other="Taipei Standard Time" territory="TW" type="Asia/Taipei"/>
|
||||
|
||||
<!-- (UTC+08:00) Ulaanbaatar -->
|
||||
<mapZone other="Ulaanbaatar Standard Time" territory="001" type="Asia/Ulaanbaatar"/>
|
||||
<mapZone other="Ulaanbaatar Standard Time" territory="MN" type="Asia/Ulaanbaatar Asia/Choibalsan"/>
|
||||
|
||||
<!-- (UTC+08:30) Pyongyang -->
|
||||
<mapZone other="North Korea Standard Time" territory="001" type="Asia/Pyongyang"/>
|
||||
<mapZone other="North Korea Standard Time" territory="KP" type="Asia/Pyongyang"/>
|
||||
|
||||
<!-- (UTC+08:45) Eucla -->
|
||||
<mapZone other="Aus Central W. Standard Time" territory="001" type="Australia/Eucla"/>
|
||||
<mapZone other="Aus Central W. Standard Time" territory="AU" type="Australia/Eucla"/>
|
||||
|
||||
<!-- (UTC+09:00) Chita -->
|
||||
<mapZone other="Transbaikal Standard Time" territory="001" type="Asia/Chita"/>
|
||||
<mapZone other="Transbaikal Standard Time" territory="RU" type="Asia/Chita"/>
|
||||
|
||||
<!-- (UTC+09:00) Osaka, Sapporo, Tokyo -->
|
||||
<mapZone other="Tokyo Standard Time" territory="001" type="Asia/Tokyo"/>
|
||||
<mapZone other="Tokyo Standard Time" territory="ID" type="Asia/Jayapura"/>
|
||||
<mapZone other="Tokyo Standard Time" territory="JP" type="Asia/Tokyo"/>
|
||||
<mapZone other="Tokyo Standard Time" territory="PW" type="Pacific/Palau"/>
|
||||
<mapZone other="Tokyo Standard Time" territory="TL" type="Asia/Dili"/>
|
||||
<mapZone other="Tokyo Standard Time" territory="ZZ" type="Etc/GMT-9"/>
|
||||
|
||||
<!-- (UTC+09:00) Seoul -->
|
||||
<mapZone other="Korea Standard Time" territory="001" type="Asia/Seoul"/>
|
||||
<mapZone other="Korea Standard Time" territory="KR" type="Asia/Seoul"/>
|
||||
|
||||
<!-- (UTC+09:00) Yakutsk -->
|
||||
<mapZone other="Yakutsk Standard Time" territory="001" type="Asia/Yakutsk"/>
|
||||
<mapZone other="Yakutsk Standard Time" territory="RU" type="Asia/Yakutsk Asia/Khandyga"/>
|
||||
|
||||
<!-- (UTC+09:30) Adelaide -->
|
||||
<mapZone other="Cen. Australia Standard Time" territory="001" type="Australia/Adelaide"/>
|
||||
<mapZone other="Cen. Australia Standard Time" territory="AU" type="Australia/Adelaide Australia/Broken_Hill"/>
|
||||
|
||||
<!-- (UTC+09:30) Darwin -->
|
||||
<mapZone other="AUS Central Standard Time" territory="001" type="Australia/Darwin"/>
|
||||
<mapZone other="AUS Central Standard Time" territory="AU" type="Australia/Darwin"/>
|
||||
|
||||
<!-- (UTC+10:00) Brisbane -->
|
||||
<mapZone other="E. Australia Standard Time" territory="001" type="Australia/Brisbane"/>
|
||||
<mapZone other="E. Australia Standard Time" territory="AU" type="Australia/Brisbane Australia/Lindeman"/>
|
||||
|
||||
<!-- (UTC+10:00) Canberra, Melbourne, Sydney -->
|
||||
<mapZone other="AUS Eastern Standard Time" territory="001" type="Australia/Sydney"/>
|
||||
<mapZone other="AUS Eastern Standard Time" territory="AU" type="Australia/Sydney Australia/Melbourne"/>
|
||||
|
||||
<!-- (UTC+10:00) Guam, Port Moresby -->
|
||||
<mapZone other="West Pacific Standard Time" territory="001" type="Pacific/Port_Moresby"/>
|
||||
<mapZone other="West Pacific Standard Time" territory="AQ" type="Antarctica/DumontDUrville"/>
|
||||
<mapZone other="West Pacific Standard Time" territory="FM" type="Pacific/Truk"/>
|
||||
<mapZone other="West Pacific Standard Time" territory="GU" type="Pacific/Guam"/>
|
||||
<mapZone other="West Pacific Standard Time" territory="MP" type="Pacific/Saipan"/>
|
||||
<mapZone other="West Pacific Standard Time" territory="PG" type="Pacific/Port_Moresby"/>
|
||||
<mapZone other="West Pacific Standard Time" territory="ZZ" type="Etc/GMT-10"/>
|
||||
|
||||
<!-- (UTC+10:00) Hobart -->
|
||||
<mapZone other="Tasmania Standard Time" territory="001" type="Australia/Hobart"/>
|
||||
<mapZone other="Tasmania Standard Time" territory="AU" type="Australia/Hobart Australia/Currie"/>
|
||||
|
||||
<!-- (UTC+10:00) Vladivostok -->
|
||||
<mapZone other="Vladivostok Standard Time" territory="001" type="Asia/Vladivostok"/>
|
||||
<mapZone other="Vladivostok Standard Time" territory="RU" type="Asia/Vladivostok Asia/Ust-Nera"/>
|
||||
|
||||
<!-- (UTC+10:30) Lord Howe Island -->
|
||||
<mapZone other="Lord Howe Standard Time" territory="001" type="Australia/Lord_Howe"/>
|
||||
<mapZone other="Lord Howe Standard Time" territory="AU" type="Australia/Lord_Howe"/>
|
||||
|
||||
<!-- (UTC+11:00) Bougainville Island -->
|
||||
<mapZone other="Bougainville Standard Time" territory="001" type="Pacific/Bougainville"/>
|
||||
<mapZone other="Bougainville Standard Time" territory="PG" type="Pacific/Bougainville"/>
|
||||
|
||||
<!-- (UTC+11:00) Chokurdakh -->
|
||||
<mapZone other="Russia Time Zone 10" territory="001" type="Asia/Srednekolymsk"/>
|
||||
<mapZone other="Russia Time Zone 10" territory="RU" type="Asia/Srednekolymsk"/>
|
||||
|
||||
<!-- (UTC+11:00) Magadan -->
|
||||
<mapZone other="Magadan Standard Time" territory="001" type="Asia/Magadan"/>
|
||||
<mapZone other="Magadan Standard Time" territory="RU" type="Asia/Magadan"/>
|
||||
|
||||
<!-- (UTC+11:00) Norfolk Island -->
|
||||
<mapZone other="Norfolk Standard Time" territory="001" type="Pacific/Norfolk"/>
|
||||
<mapZone other="Norfolk Standard Time" territory="NF" type="Pacific/Norfolk"/>
|
||||
|
||||
<!-- (UTC+11:00) Sakhalin -->
|
||||
<mapZone other="Sakhalin Standard Time" territory="001" type="Asia/Sakhalin"/>
|
||||
<mapZone other="Sakhalin Standard Time" territory="RU" type="Asia/Sakhalin"/>
|
||||
|
||||
<!-- (UTC+11:00) Solomon Is., New Caledonia -->
|
||||
<mapZone other="Central Pacific Standard Time" territory="001" type="Pacific/Guadalcanal"/>
|
||||
<mapZone other="Central Pacific Standard Time" territory="AQ" type="Antarctica/Casey"/>
|
||||
<mapZone other="Central Pacific Standard Time" territory="AU" type="Antarctica/Macquarie"/>
|
||||
<mapZone other="Central Pacific Standard Time" territory="FM" type="Pacific/Ponape Pacific/Kosrae"/>
|
||||
<mapZone other="Central Pacific Standard Time" territory="NC" type="Pacific/Noumea"/>
|
||||
<mapZone other="Central Pacific Standard Time" territory="SB" type="Pacific/Guadalcanal"/>
|
||||
<mapZone other="Central Pacific Standard Time" territory="VU" type="Pacific/Efate"/>
|
||||
<mapZone other="Central Pacific Standard Time" territory="ZZ" type="Etc/GMT-11"/>
|
||||
|
||||
<!-- (UTC+12:00) Anadyr, Petropavlovsk-Kamchatsky -->
|
||||
<mapZone other="Russia Time Zone 11" territory="001" type="Asia/Kamchatka"/>
|
||||
<mapZone other="Russia Time Zone 11" territory="RU" type="Asia/Kamchatka Asia/Anadyr"/>
|
||||
|
||||
<!-- (UTC+12:00) Auckland, Wellington -->
|
||||
<mapZone other="New Zealand Standard Time" territory="001" type="Pacific/Auckland"/>
|
||||
<mapZone other="New Zealand Standard Time" territory="AQ" type="Antarctica/McMurdo"/>
|
||||
<mapZone other="New Zealand Standard Time" territory="NZ" type="Pacific/Auckland"/>
|
||||
|
||||
<!-- (UTC+12:00) Coordinated Universal Time+12 -->
|
||||
<mapZone other="UTC+12" territory="001" type="Etc/GMT-12"/>
|
||||
<mapZone other="UTC+12" territory="KI" type="Pacific/Tarawa"/>
|
||||
<mapZone other="UTC+12" territory="MH" type="Pacific/Majuro Pacific/Kwajalein"/>
|
||||
<mapZone other="UTC+12" territory="NR" type="Pacific/Nauru"/>
|
||||
<mapZone other="UTC+12" territory="TV" type="Pacific/Funafuti"/>
|
||||
<mapZone other="UTC+12" territory="UM" type="Pacific/Wake"/>
|
||||
<mapZone other="UTC+12" territory="WF" type="Pacific/Wallis"/>
|
||||
<mapZone other="UTC+12" territory="ZZ" type="Etc/GMT-12"/>
|
||||
|
||||
<!-- (UTC+12:00) Fiji -->
|
||||
<mapZone other="Fiji Standard Time" territory="001" type="Pacific/Fiji"/>
|
||||
<mapZone other="Fiji Standard Time" territory="FJ" type="Pacific/Fiji"/>
|
||||
|
||||
<!-- (UTC+12:45) Chatham Islands -->
|
||||
<mapZone other="Chatham Islands Standard Time" territory="001" type="Pacific/Chatham"/>
|
||||
<mapZone other="Chatham Islands Standard Time" territory="NZ" type="Pacific/Chatham"/>
|
||||
|
||||
<!-- (UTC+13:00) Nuku'alofa -->
|
||||
<mapZone other="Tonga Standard Time" territory="001" type="Pacific/Tongatapu"/>
|
||||
<mapZone other="Tonga Standard Time" territory="KI" type="Pacific/Enderbury"/>
|
||||
<mapZone other="Tonga Standard Time" territory="TK" type="Pacific/Fakaofo"/>
|
||||
<mapZone other="Tonga Standard Time" territory="TO" type="Pacific/Tongatapu"/>
|
||||
<mapZone other="Tonga Standard Time" territory="ZZ" type="Etc/GMT-13"/>
|
||||
|
||||
<!-- (UTC+13:00) Samoa -->
|
||||
<mapZone other="Samoa Standard Time" territory="001" type="Pacific/Apia"/>
|
||||
<mapZone other="Samoa Standard Time" territory="WS" type="Pacific/Apia"/>
|
||||
|
||||
<!-- (UTC+14:00) Kiritimati Island -->
|
||||
<mapZone other="Line Islands Standard Time" territory="001" type="Pacific/Kiritimati"/>
|
||||
<mapZone other="Line Islands Standard Time" territory="KI" type="Pacific/Kiritimati"/>
|
||||
<mapZone other="Line Islands Standard Time" territory="ZZ" type="Etc/GMT-14"/>
|
||||
</mapTimezones>
|
||||
</windowsZones>
|
||||
</supplementalData>
|
19
externals/jsonlint/LICENSE
vendored
Normal file
19
externals/jsonlint/LICENSE
vendored
Normal file
|
@ -0,0 +1,19 @@
|
|||
Copyright (c) 2011 Jordi Boggiano
|
||||
|
||||
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.
|
488
externals/jsonlint/src/Seld/JsonLint/JsonParser.php
vendored
Normal file
488
externals/jsonlint/src/Seld/JsonLint/JsonParser.php
vendored
Normal file
|
@ -0,0 +1,488 @@
|
|||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the JSON Lint package.
|
||||
*
|
||||
* (c) Jordi Boggiano <j.boggiano@seld.be>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Parser class
|
||||
*
|
||||
* Example:
|
||||
*
|
||||
* $parser = new JsonParser();
|
||||
* // returns null if it's valid json, or an error object
|
||||
* $parser->lint($json);
|
||||
* // returns parsed json, like json_decode does, but slower, throws exceptions on failure.
|
||||
* $parser->parse($json);
|
||||
*
|
||||
* Ported from https://github.com/zaach/jsonlint
|
||||
*/
|
||||
class JsonLintJsonParser
|
||||
{
|
||||
const DETECT_KEY_CONFLICTS = 1;
|
||||
const ALLOW_DUPLICATE_KEYS = 2;
|
||||
const PARSE_TO_ASSOC = 4;
|
||||
|
||||
private $lexer;
|
||||
|
||||
private $flags;
|
||||
private $stack;
|
||||
private $vstack; // semantic value stack
|
||||
private $lstack; // location stack
|
||||
|
||||
private $symbols = array(
|
||||
'error' => 2,
|
||||
'JSONString' => 3,
|
||||
'STRING' => 4,
|
||||
'JSONNumber' => 5,
|
||||
'NUMBER' => 6,
|
||||
'JSONNullLiteral' => 7,
|
||||
'NULL' => 8,
|
||||
'JSONBooleanLiteral' => 9,
|
||||
'TRUE' => 10,
|
||||
'FALSE' => 11,
|
||||
'JSONText' => 12,
|
||||
'JSONValue' => 13,
|
||||
'EOF' => 14,
|
||||
'JSONObject' => 15,
|
||||
'JSONArray' => 16,
|
||||
'{' => 17,
|
||||
'}' => 18,
|
||||
'JSONMemberList' => 19,
|
||||
'JSONMember' => 20,
|
||||
':' => 21,
|
||||
',' => 22,
|
||||
'[' => 23,
|
||||
']' => 24,
|
||||
'JSONElementList' => 25,
|
||||
'$accept' => 0,
|
||||
'$end' => 1,
|
||||
);
|
||||
|
||||
private $terminals_ = array(
|
||||
2 => "error",
|
||||
4 => "STRING",
|
||||
6 => "NUMBER",
|
||||
8 => "NULL",
|
||||
10 => "TRUE",
|
||||
11 => "FALSE",
|
||||
14 => "EOF",
|
||||
17 => "{",
|
||||
18 => "}",
|
||||
21 => ":",
|
||||
22 => ",",
|
||||
23 => "[",
|
||||
24 => "]",
|
||||
);
|
||||
|
||||
private $productions_ = array(
|
||||
0,
|
||||
array(3, 1),
|
||||
array(5, 1),
|
||||
array(7, 1),
|
||||
array(9, 1),
|
||||
array(9, 1),
|
||||
array(12, 2),
|
||||
array(13, 1),
|
||||
array(13, 1),
|
||||
array(13, 1),
|
||||
array(13, 1),
|
||||
array(13, 1),
|
||||
array(13, 1),
|
||||
array(15, 2),
|
||||
array(15, 3),
|
||||
array(20, 3),
|
||||
array(19, 1),
|
||||
array(19, 3),
|
||||
array(16, 2),
|
||||
array(16, 3),
|
||||
array(25, 1),
|
||||
array(25, 3)
|
||||
);
|
||||
|
||||
private $table = array(array(3 => 5, 4 => array(1,12), 5 => 6, 6 => array(1,13), 7 => 3, 8 => array(1,9), 9 => 4, 10 => array(1,10), 11 => array(1,11), 12 => 1, 13 => 2, 15 => 7, 16 => 8, 17 => array(1,14), 23 => array(1,15)), array( 1 => array(3)), array( 14 => array(1,16)), array( 14 => array(2,7), 18 => array(2,7), 22 => array(2,7), 24 => array(2,7)), array( 14 => array(2,8), 18 => array(2,8), 22 => array(2,8), 24 => array(2,8)), array( 14 => array(2,9), 18 => array(2,9), 22 => array(2,9), 24 => array(2,9)), array( 14 => array(2,10), 18 => array(2,10), 22 => array(2,10), 24 => array(2,10)), array( 14 => array(2,11), 18 => array(2,11), 22 => array(2,11), 24 => array(2,11)), array( 14 => array(2,12), 18 => array(2,12), 22 => array(2,12), 24 => array(2,12)), array( 14 => array(2,3), 18 => array(2,3), 22 => array(2,3), 24 => array(2,3)), array( 14 => array(2,4), 18 => array(2,4), 22 => array(2,4), 24 => array(2,4)), array( 14 => array(2,5), 18 => array(2,5), 22 => array(2,5), 24 => array(2,5)), array( 14 => array(2,1), 18 => array(2,1), 21 => array(2,1), 22 => array(2,1), 24 => array(2,1)), array( 14 => array(2,2), 18 => array(2,2), 22 => array(2,2), 24 => array(2,2)), array( 3 => 20, 4 => array(1,12), 18 => array(1,17), 19 => 18, 20 => 19 ), array( 3 => 5, 4 => array(1,12), 5 => 6, 6 => array(1,13), 7 => 3, 8 => array(1,9), 9 => 4, 10 => array(1,10), 11 => array(1,11), 13 => 23, 15 => 7, 16 => 8, 17 => array(1,14), 23 => array(1,15), 24 => array(1,21), 25 => 22 ), array( 1 => array(2,6)), array( 14 => array(2,13), 18 => array(2,13), 22 => array(2,13), 24 => array(2,13)), array( 18 => array(1,24), 22 => array(1,25)), array( 18 => array(2,16), 22 => array(2,16)), array( 21 => array(1,26)), array( 14 => array(2,18), 18 => array(2,18), 22 => array(2,18), 24 => array(2,18)), array( 22 => array(1,28), 24 => array(1,27)), array( 22 => array(2,20), 24 => array(2,20)), array( 14 => array(2,14), 18 => array(2,14), 22 => array(2,14), 24 => array(2,14)), array( 3 => 20, 4 => array(1,12), 20 => 29 ), array( 3 => 5, 4 => array(1,12), 5 => 6, 6 => array(1,13), 7 => 3, 8 => array(1,9), 9 => 4, 10 => array(1,10), 11 => array(1,11), 13 => 30, 15 => 7, 16 => 8, 17 => array(1,14), 23 => array(1,15)), array( 14 => array(2,19), 18 => array(2,19), 22 => array(2,19), 24 => array(2,19)), array( 3 => 5, 4 => array(1,12), 5 => 6, 6 => array(1,13), 7 => 3, 8 => array(1,9), 9 => 4, 10 => array(1,10), 11 => array(1,11), 13 => 31, 15 => 7, 16 => 8, 17 => array(1,14), 23 => array(1,15)), array( 18 => array(2,17), 22 => array(2,17)), array( 18 => array(2,15), 22 => array(2,15)), array( 22 => array(2,21), 24 => array(2,21)),
|
||||
);
|
||||
|
||||
private $defaultActions = array(
|
||||
16 => array(2, 6)
|
||||
);
|
||||
|
||||
/**
|
||||
* @param string $input JSON string
|
||||
* @return null|JsonLintParsingException null if no error is found, a JsonLintParsingException containing all details otherwise
|
||||
*/
|
||||
public function lint($input)
|
||||
{
|
||||
try {
|
||||
$this->parse($input);
|
||||
} catch (JsonLintParsingException $e) {
|
||||
return $e;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $input JSON string
|
||||
* @return mixed
|
||||
* @throws JsonLintParsingException
|
||||
*/
|
||||
public function parse($input, $flags = 0)
|
||||
{
|
||||
$this->failOnBOM($input);
|
||||
|
||||
$this->flags = $flags;
|
||||
|
||||
$this->stack = array(0);
|
||||
$this->vstack = array(null);
|
||||
$this->lstack = array();
|
||||
|
||||
$yytext = '';
|
||||
$yylineno = 0;
|
||||
$yyleng = 0;
|
||||
$recovering = 0;
|
||||
$TERROR = 2;
|
||||
$EOF = 1;
|
||||
|
||||
$this->lexer = new JsonLintLexer();
|
||||
$this->lexer->setInput($input);
|
||||
|
||||
$yyloc = $this->lexer->yylloc;
|
||||
$this->lstack[] = $yyloc;
|
||||
|
||||
$symbol = null;
|
||||
$preErrorSymbol = null;
|
||||
$state = null;
|
||||
$action = null;
|
||||
$a = null;
|
||||
$r = null;
|
||||
$yyval = new stdClass;
|
||||
$p = null;
|
||||
$len = null;
|
||||
$newState = null;
|
||||
$expected = null;
|
||||
$errStr = null;
|
||||
|
||||
while (true) {
|
||||
// retrieve state number from top of stack
|
||||
$state = $this->stack[count($this->stack)-1];
|
||||
|
||||
// use default actions if available
|
||||
if (isset($this->defaultActions[$state])) {
|
||||
$action = $this->defaultActions[$state];
|
||||
} else {
|
||||
if ($symbol == null) {
|
||||
$symbol = $this->lex();
|
||||
}
|
||||
// read action for current state and first input
|
||||
$action = isset($this->table[$state][$symbol]) ? $this->table[$state][$symbol] : false;
|
||||
}
|
||||
|
||||
// handle parse error
|
||||
if (!$action || !$action[0]) {
|
||||
if (!$recovering) {
|
||||
// Report error
|
||||
$expected = array();
|
||||
foreach ($this->table[$state] as $p => $ignore) {
|
||||
if (isset($this->terminals_[$p]) && $p > 2) {
|
||||
$expected[] = "'" . $this->terminals_[$p] . "'";
|
||||
}
|
||||
}
|
||||
|
||||
$message = null;
|
||||
if (in_array("'STRING'", $expected) && in_array(substr($this->lexer->match, 0, 1), array('"', "'"))) {
|
||||
$message = "Invalid string";
|
||||
if ("'" === substr($this->lexer->match, 0, 1)) {
|
||||
$message .= ", it appears you used single quotes instead of double quotes";
|
||||
} elseif (preg_match('{".+?(\\\\[^"bfnrt/\\\\u])}', $this->lexer->getUpcomingInput(), $match)) {
|
||||
$message .= ", it appears you have an unescaped backslash at: ".$match[1];
|
||||
} elseif (preg_match('{"(?:[^"]+|\\\\")*$}m', $this->lexer->getUpcomingInput())) {
|
||||
$message .= ", it appears you forgot to terminate the string, or attempted to write a multiline string which is invalid";
|
||||
}
|
||||
}
|
||||
|
||||
$errStr = 'Parse error on line ' . ($yylineno+1) . ":\n";
|
||||
$errStr .= $this->lexer->showPosition() . "\n";
|
||||
if ($message) {
|
||||
$errStr .= $message;
|
||||
} else {
|
||||
$errStr .= (count($expected) > 1) ? "Expected one of: " : "Expected: ";
|
||||
$errStr .= implode(', ', $expected);
|
||||
}
|
||||
|
||||
if (',' === substr(trim($this->lexer->getPastInput()), -1)) {
|
||||
$errStr .= " - It appears you have an extra trailing comma";
|
||||
}
|
||||
|
||||
$this->parseError($errStr, array(
|
||||
'text' => $this->lexer->match,
|
||||
'token' => !empty($this->terminals_[$symbol]) ? $this->terminals_[$symbol] : $symbol,
|
||||
'line' => $this->lexer->yylineno,
|
||||
'loc' => $yyloc,
|
||||
'expected' => $expected,
|
||||
));
|
||||
}
|
||||
|
||||
// just recovered from another error
|
||||
if ($recovering == 3) {
|
||||
if ($symbol == $EOF) {
|
||||
throw new JsonLintParsingException($errStr ? $errStr : 'Parsing halted.');
|
||||
}
|
||||
|
||||
// discard current lookahead and grab another
|
||||
$yyleng = $this->lexer->yyleng;
|
||||
$yytext = $this->lexer->yytext;
|
||||
$yylineno = $this->lexer->yylineno;
|
||||
$yyloc = $this->lexer->yylloc;
|
||||
$symbol = $this->lex();
|
||||
}
|
||||
|
||||
// try to recover from error
|
||||
while (true) {
|
||||
// check for error recovery rule in this state
|
||||
if (array_key_exists($TERROR, $this->table[$state])) {
|
||||
break;
|
||||
}
|
||||
if ($state == 0) {
|
||||
throw new JsonLintParsingException($errStr ? $errStr : 'Parsing halted.');
|
||||
}
|
||||
$this->popStack(1);
|
||||
$state = $this->stack[count($this->stack)-1];
|
||||
}
|
||||
|
||||
$preErrorSymbol = $symbol; // save the lookahead token
|
||||
$symbol = $TERROR; // insert generic error symbol as new lookahead
|
||||
$state = $this->stack[count($this->stack)-1];
|
||||
$action = isset($this->table[$state][$TERROR]) ? $this->table[$state][$TERROR] : false;
|
||||
$recovering = 3; // allow 3 real symbols to be shifted before reporting a new error
|
||||
}
|
||||
|
||||
// this shouldn't happen, unless resolve defaults are off
|
||||
if (is_array($action[0]) && count($action) > 1) {
|
||||
throw new JsonLintParsingException('Parse Error: multiple actions possible at state: ' . $state . ', token: ' . $symbol);
|
||||
}
|
||||
|
||||
switch ($action[0]) {
|
||||
case 1: // shift
|
||||
$this->stack[] = $symbol;
|
||||
$this->vstack[] = $this->lexer->yytext;
|
||||
$this->lstack[] = $this->lexer->yylloc;
|
||||
$this->stack[] = $action[1]; // push state
|
||||
$symbol = null;
|
||||
if (!$preErrorSymbol) { // normal execution/no error
|
||||
$yyleng = $this->lexer->yyleng;
|
||||
$yytext = $this->lexer->yytext;
|
||||
$yylineno = $this->lexer->yylineno;
|
||||
$yyloc = $this->lexer->yylloc;
|
||||
if ($recovering > 0) {
|
||||
$recovering--;
|
||||
}
|
||||
} else { // error just occurred, resume old lookahead f/ before error
|
||||
$symbol = $preErrorSymbol;
|
||||
$preErrorSymbol = null;
|
||||
}
|
||||
break;
|
||||
|
||||
case 2: // reduce
|
||||
$len = $this->productions_[$action[1]][1];
|
||||
|
||||
// perform semantic action
|
||||
$yyval->token = $this->vstack[count($this->vstack) - $len]; // default to $$ = $1
|
||||
// default location, uses first token for firsts, last for lasts
|
||||
$yyval->store = array( // _$ = store
|
||||
'first_line' => $this->lstack[count($this->lstack) - ($len ? $len : 1)]['first_line'],
|
||||
'last_line' => $this->lstack[count($this->lstack) - 1]['last_line'],
|
||||
'first_column' => $this->lstack[count($this->lstack) - ($len ? $len : 1)]['first_column'],
|
||||
'last_column' => $this->lstack[count($this->lstack) - 1]['last_column'],
|
||||
);
|
||||
$r = $this->performAction($yyval, $yytext, $yyleng, $yylineno, $action[1], $this->vstack, $this->lstack);
|
||||
|
||||
if (!$r instanceof JsonLintUndefined) {
|
||||
return $r;
|
||||
}
|
||||
|
||||
if ($len) {
|
||||
$this->popStack($len);
|
||||
}
|
||||
|
||||
$this->stack[] = $this->productions_[$action[1]][0]; // push nonterminal (reduce)
|
||||
$this->vstack[] = $yyval->token;
|
||||
$this->lstack[] = $yyval->store;
|
||||
$newState = $this->table[$this->stack[count($this->stack)-2]][$this->stack[count($this->stack)-1]];
|
||||
$this->stack[] = $newState;
|
||||
break;
|
||||
|
||||
case 3: // accept
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
protected function parseError($str, $hash)
|
||||
{
|
||||
throw new JsonLintParsingException($str, $hash);
|
||||
}
|
||||
|
||||
// $$ = $tokens // needs to be passed by ref?
|
||||
// $ = $token
|
||||
// _$ removed, useless?
|
||||
private function performAction(stdClass $yyval, $yytext, $yyleng, $yylineno, $yystate, &$tokens)
|
||||
{
|
||||
// $0 = $len
|
||||
$len = count($tokens) - 1;
|
||||
switch ($yystate) {
|
||||
case 1:
|
||||
$yytext = preg_replace_callback('{(?:\\\\["bfnrt/\\\\]|\\\\u[a-fA-F0-9]{4})}', array($this, 'stringInterpolation'), $yytext);
|
||||
$yyval->token = $yytext;
|
||||
break;
|
||||
case 2:
|
||||
if (strpos($yytext, 'e') !== false || strpos($yytext, 'E') !== false) {
|
||||
$yyval->token = floatval($yytext);
|
||||
} else {
|
||||
$yyval->token = strpos($yytext, '.') === false ? intval($yytext) : floatval($yytext);
|
||||
}
|
||||
break;
|
||||
case 3:
|
||||
$yyval->token = null;
|
||||
break;
|
||||
case 4:
|
||||
$yyval->token = true;
|
||||
break;
|
||||
case 5:
|
||||
$yyval->token = false;
|
||||
break;
|
||||
case 6:
|
||||
return $yyval->token = $tokens[$len-1];
|
||||
case 13:
|
||||
if ($this->flags & self::PARSE_TO_ASSOC) {
|
||||
$yyval->token = array();
|
||||
} else {
|
||||
$yyval->token = new stdClass;
|
||||
}
|
||||
break;
|
||||
case 14:
|
||||
$yyval->token = $tokens[$len-1];
|
||||
break;
|
||||
case 15:
|
||||
$yyval->token = array($tokens[$len-2], $tokens[$len]);
|
||||
break;
|
||||
case 16:
|
||||
$property = $tokens[$len][0];
|
||||
if ($this->flags & self::PARSE_TO_ASSOC) {
|
||||
$yyval->token = array();
|
||||
$yyval->token[$property] = $tokens[$len][1];
|
||||
} else {
|
||||
$yyval->token = new stdClass;
|
||||
$yyval->token->$property = $tokens[$len][1];
|
||||
}
|
||||
break;
|
||||
case 17:
|
||||
if ($this->flags & self::PARSE_TO_ASSOC) {
|
||||
$yyval->token =& $tokens[$len-2];
|
||||
$key = $tokens[$len][0];
|
||||
if (($this->flags & self::DETECT_KEY_CONFLICTS) && isset($tokens[$len-2][$key])) {
|
||||
$errStr = 'Parse error on line ' . ($yylineno+1) . ":\n";
|
||||
$errStr .= $this->lexer->showPosition() . "\n";
|
||||
$errStr .= "Duplicate key: ".$tokens[$len][0];
|
||||
throw new JsonLintParsingException($errStr);
|
||||
} elseif (($this->flags & self::ALLOW_DUPLICATE_KEYS) && isset($tokens[$len-2][$key])) {
|
||||
// Forget about it...
|
||||
}
|
||||
$tokens[$len-2][$key] = $tokens[$len][1];
|
||||
} else {
|
||||
$yyval->token = $tokens[$len-2];
|
||||
$key = $tokens[$len][0];
|
||||
if (($this->flags & self::DETECT_KEY_CONFLICTS) && isset($tokens[$len-2]->{$key})) {
|
||||
$errStr = 'Parse error on line ' . ($yylineno+1) . ":\n";
|
||||
$errStr .= $this->lexer->showPosition() . "\n";
|
||||
$errStr .= "Duplicate key: ".$tokens[$len][0];
|
||||
throw new JsonLintParsingException($errStr);
|
||||
} elseif (($this->flags & self::ALLOW_DUPLICATE_KEYS) && isset($tokens[$len-2]->{$key})) {
|
||||
$duplicateCount = 1;
|
||||
do {
|
||||
$duplicateKey = $key . '.' . $duplicateCount++;
|
||||
} while (isset($tokens[$len-2]->$duplicateKey));
|
||||
$key = $duplicateKey;
|
||||
}
|
||||
$tokens[$len-2]->$key = $tokens[$len][1];
|
||||
}
|
||||
break;
|
||||
case 18:
|
||||
$yyval->token = array();
|
||||
break;
|
||||
case 19:
|
||||
$yyval->token = $tokens[$len-1];
|
||||
break;
|
||||
case 20:
|
||||
$yyval->token = array($tokens[$len]);
|
||||
break;
|
||||
case 21:
|
||||
$tokens[$len-2][] = $tokens[$len];
|
||||
$yyval->token = $tokens[$len-2];
|
||||
break;
|
||||
}
|
||||
|
||||
return new JsonLintUndefined();
|
||||
}
|
||||
|
||||
private function stringInterpolation($match)
|
||||
{
|
||||
switch ($match[0]) {
|
||||
case '\\\\':
|
||||
return '\\';
|
||||
case '\"':
|
||||
return '"';
|
||||
case '\b':
|
||||
return chr(8);
|
||||
case '\f':
|
||||
return chr(12);
|
||||
case '\n':
|
||||
return "\n";
|
||||
case '\r':
|
||||
return "\r";
|
||||
case '\t':
|
||||
return "\t";
|
||||
case '\/':
|
||||
return "/";
|
||||
default:
|
||||
return html_entity_decode('&#x'.ltrim(substr($match[0], 2), '0').';', 0, 'UTF-8');
|
||||
}
|
||||
}
|
||||
|
||||
private function popStack($n)
|
||||
{
|
||||
$this->stack = array_slice($this->stack, 0, - (2 * $n));
|
||||
$this->vstack = array_slice($this->vstack, 0, - $n);
|
||||
$this->lstack = array_slice($this->lstack, 0, - $n);
|
||||
}
|
||||
|
||||
private function lex()
|
||||
{
|
||||
$token = $this->lexer->lex();
|
||||
if (!$token) {
|
||||
$token = 1;
|
||||
}
|
||||
// if token isn't its numeric value, convert
|
||||
if (!is_numeric($token)) {
|
||||
$token = isset($this->symbols[$token]) ? $this->symbols[$token] : $token;
|
||||
}
|
||||
|
||||
return $token;
|
||||
}
|
||||
|
||||
private function failOnBOM($input)
|
||||
{
|
||||
// UTF-8 ByteOrderMark sequence
|
||||
$bom = "\xEF\xBB\xBF";
|
||||
|
||||
if (substr($input, 0, 3) === $bom) {
|
||||
$this->parseError("BOM detected, make sure your input does not include a Unicode Byte-Order-Mark", array());
|
||||
}
|
||||
}
|
||||
}
|
215
externals/jsonlint/src/Seld/JsonLint/Lexer.php
vendored
Normal file
215
externals/jsonlint/src/Seld/JsonLint/Lexer.php
vendored
Normal file
|
@ -0,0 +1,215 @@
|
|||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the JSON Lint package.
|
||||
*
|
||||
* (c) Jordi Boggiano <j.boggiano@seld.be>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Lexer class
|
||||
*
|
||||
* Ported from https://github.com/zaach/jsonlint
|
||||
*/
|
||||
class JsonLintLexer
|
||||
{
|
||||
private $EOF = 1;
|
||||
private $rules = array(
|
||||
0 => '/^\s+/',
|
||||
1 => '/^-?([0-9]|[1-9][0-9]+)(\.[0-9]+)?([eE][+-]?[0-9]+)?\b/',
|
||||
2 => '{^"(?>\\\\["bfnrt/\\\\]|\\\\u[a-fA-F0-9]{4}|[^\0-\x1f\\\\"]++)*+"}',
|
||||
3 => '/^\{/',
|
||||
4 => '/^\}/',
|
||||
5 => '/^\[/',
|
||||
6 => '/^\]/',
|
||||
7 => '/^,/',
|
||||
8 => '/^:/',
|
||||
9 => '/^true\b/',
|
||||
10 => '/^false\b/',
|
||||
11 => '/^null\b/',
|
||||
12 => '/^$/',
|
||||
13 => '/^./',
|
||||
);
|
||||
|
||||
private $conditions = array(
|
||||
"INITIAL" => array(
|
||||
"rules" => array(0,1,2,3,4,5,6,7,8,9,10,11,12,13),
|
||||
"inclusive" => true,
|
||||
),
|
||||
);
|
||||
|
||||
private $conditionStack;
|
||||
private $input;
|
||||
private $more;
|
||||
private $done;
|
||||
private $matched;
|
||||
|
||||
public $match;
|
||||
public $yylineno;
|
||||
public $yyleng;
|
||||
public $yytext;
|
||||
public $yylloc;
|
||||
|
||||
public function lex()
|
||||
{
|
||||
$r = $this->next();
|
||||
if (!$r instanceof JsonLintUndefined) {
|
||||
return $r;
|
||||
}
|
||||
|
||||
return $this->lex();
|
||||
}
|
||||
|
||||
public function setInput($input)
|
||||
{
|
||||
$this->input = $input;
|
||||
$this->more = false;
|
||||
$this->done = false;
|
||||
$this->yylineno = $this->yyleng = 0;
|
||||
$this->yytext = $this->matched = $this->match = '';
|
||||
$this->conditionStack = array('INITIAL');
|
||||
$this->yylloc = array('first_line' => 1, 'first_column' => 0, 'last_line' => 1, 'last_column' => 0);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function showPosition()
|
||||
{
|
||||
$pre = str_replace("\n", '', $this->getPastInput());
|
||||
$c = str_repeat('-', max(0, strlen($pre) - 1)); // new Array(pre.length + 1).join("-");
|
||||
|
||||
return $pre . str_replace("\n", '', $this->getUpcomingInput()) . "\n" . $c . "^";
|
||||
}
|
||||
|
||||
public function getPastInput()
|
||||
{
|
||||
$past = substr($this->matched, 0, strlen($this->matched) - strlen($this->match));
|
||||
|
||||
return (strlen($past) > 20 ? '...' : '') . substr($past, -20);
|
||||
}
|
||||
|
||||
public function getUpcomingInput()
|
||||
{
|
||||
$next = $this->match;
|
||||
if (strlen($next) < 20) {
|
||||
$next .= substr($this->input, 0, 20 - strlen($next));
|
||||
}
|
||||
|
||||
return substr($next, 0, 20) . (strlen($next) > 20 ? '...' : '');
|
||||
}
|
||||
|
||||
protected function parseError($str, $hash)
|
||||
{
|
||||
throw new Exception($str);
|
||||
}
|
||||
|
||||
private function next()
|
||||
{
|
||||
if ($this->done) {
|
||||
return $this->EOF;
|
||||
}
|
||||
if (!$this->input) {
|
||||
$this->done = true;
|
||||
}
|
||||
|
||||
$token = null;
|
||||
$match = null;
|
||||
$col = null;
|
||||
$lines = null;
|
||||
|
||||
if (!$this->more) {
|
||||
$this->yytext = '';
|
||||
$this->match = '';
|
||||
}
|
||||
|
||||
$rules = $this->getCurrentRules();
|
||||
$rulesLen = count($rules);
|
||||
|
||||
for ($i=0; $i < $rulesLen; $i++) {
|
||||
if (preg_match($this->rules[$rules[$i]], $this->input, $match)) {
|
||||
preg_match_all('/\n.*/', $match[0], $lines);
|
||||
$lines = $lines[0];
|
||||
if ($lines) {
|
||||
$this->yylineno += count($lines);
|
||||
}
|
||||
|
||||
$this->yylloc = array(
|
||||
'first_line' => $this->yylloc['last_line'],
|
||||
'last_line' => $this->yylineno+1,
|
||||
'first_column' => $this->yylloc['last_column'],
|
||||
'last_column' => $lines ? strlen($lines[count($lines) - 1]) - 1 : $this->yylloc['last_column'] + strlen($match[0]),
|
||||
);
|
||||
$this->yytext .= $match[0];
|
||||
$this->match .= $match[0];
|
||||
$this->yyleng = strlen($this->yytext);
|
||||
$this->more = false;
|
||||
$this->input = substr($this->input, strlen($match[0]));
|
||||
$this->matched .= $match[0];
|
||||
$token = $this->performAction($rules[$i], $this->conditionStack[count($this->conditionStack)-1]);
|
||||
if ($token) {
|
||||
return $token;
|
||||
}
|
||||
|
||||
return new JsonLintUndefined();
|
||||
}
|
||||
}
|
||||
|
||||
if ($this->input === "") {
|
||||
return $this->EOF;
|
||||
}
|
||||
|
||||
$this->parseError(
|
||||
'Lexical error on line ' . ($this->yylineno+1) . ". Unrecognized text.\n" . $this->showPosition(),
|
||||
array(
|
||||
'text' => "",
|
||||
'token' => null,
|
||||
'line' => $this->yylineno,
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
private function getCurrentRules()
|
||||
{
|
||||
return $this->conditions[$this->conditionStack[count($this->conditionStack)-1]]['rules'];
|
||||
}
|
||||
|
||||
private function performAction($avoiding_name_collisions, $YY_START)
|
||||
{
|
||||
switch ($avoiding_name_collisions) {
|
||||
case 0:/* skip whitespace */
|
||||
break;
|
||||
case 1:
|
||||
return 6;
|
||||
break;
|
||||
case 2:
|
||||
$this->yytext = substr($this->yytext, 1, $this->yyleng-2);
|
||||
|
||||
return 4;
|
||||
case 3:
|
||||
return 17;
|
||||
case 4:
|
||||
return 18;
|
||||
case 5:
|
||||
return 23;
|
||||
case 6:
|
||||
return 24;
|
||||
case 7:
|
||||
return 22;
|
||||
case 8:
|
||||
return 21;
|
||||
case 9:
|
||||
return 10;
|
||||
case 10:
|
||||
return 11;
|
||||
case 11:
|
||||
return 8;
|
||||
case 12:
|
||||
return 14;
|
||||
case 13:
|
||||
return 'INVALID';
|
||||
}
|
||||
}
|
||||
}
|
26
externals/jsonlint/src/Seld/JsonLint/ParsingException.php
vendored
Normal file
26
externals/jsonlint/src/Seld/JsonLint/ParsingException.php
vendored
Normal file
|
@ -0,0 +1,26 @@
|
|||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the JSON Lint package.
|
||||
*
|
||||
* (c) Jordi Boggiano <j.boggiano@seld.be>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
class JsonLintParsingException extends Exception
|
||||
{
|
||||
protected $details;
|
||||
|
||||
public function __construct($message, $details = array())
|
||||
{
|
||||
$this->details = $details;
|
||||
parent::__construct($message);
|
||||
}
|
||||
|
||||
public function getDetails()
|
||||
{
|
||||
return $this->details;
|
||||
}
|
||||
}
|
14
externals/jsonlint/src/Seld/JsonLint/Undefined.php
vendored
Normal file
14
externals/jsonlint/src/Seld/JsonLint/Undefined.php
vendored
Normal file
|
@ -0,0 +1,14 @@
|
|||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the JSON Lint package.
|
||||
*
|
||||
* (c) Jordi Boggiano <j.boggiano@seld.be>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
class JsonLintUndefined
|
||||
{
|
||||
}
|
20
externals/porter-stemmer/LICENSE
vendored
Normal file
20
externals/porter-stemmer/LICENSE
vendored
Normal file
|
@ -0,0 +1,20 @@
|
|||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2005-2016 Richard Heyes (http://www.phpguru.org/)
|
||||
|
||||
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.
|
42
externals/porter-stemmer/README.md
vendored
Normal file
42
externals/porter-stemmer/README.md
vendored
Normal file
|
@ -0,0 +1,42 @@
|
|||
# Porter Stemmer by Richard Heyes
|
||||
|
||||
# Installation (with composer)
|
||||
|
||||
```json
|
||||
{
|
||||
"require": {
|
||||
"camspiers/porter-stemmer": "1.0.0"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
$ composer install
|
||||
|
||||
# Usage
|
||||
|
||||
```php
|
||||
$stem = Porter::Stem($word);
|
||||
```
|
||||
|
||||
# License
|
||||
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2005-2016 Richard Heyes (http://www.phpguru.org/)
|
||||
|
||||
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.
|
426
externals/porter-stemmer/src/Porter.php
vendored
Normal file
426
externals/porter-stemmer/src/Porter.php
vendored
Normal file
|
@ -0,0 +1,426 @@
|
|||
<?php
|
||||
|
||||
# vim: set expandtab tabstop=4 shiftwidth=4 softtabstop=4:
|
||||
|
||||
/**
|
||||
* Copyright (c) 2005-2016 Richard Heyes (http://www.phpguru.org/)
|
||||
*
|
||||
* Portions Copyright 2003-2007 Jon Abernathy <jon@chuggnutt.com>
|
||||
*
|
||||
* Originally available under the GPL 2 or greater. Relicensed with permission
|
||||
* of original authors under the MIT License in 2016.
|
||||
*
|
||||
* All rights reserved.
|
||||
*
|
||||
* @package PorterStemmer
|
||||
* @author Richard Heyes
|
||||
* @author Jon Abernathy <jon@chuggnutt.com>
|
||||
* @copyright 2005-2016 Richard Heyes (http://www.phpguru.org/)
|
||||
* @license http://www.opensource.org/licenses/mit-license.html MIT License
|
||||
*/
|
||||
|
||||
/**
|
||||
* PHP 5 Implementation of the Porter Stemmer algorithm. Certain elements
|
||||
* were borrowed from the (broken) implementation by Jon Abernathy.
|
||||
*
|
||||
* See http://tartarus.org/~martin/PorterStemmer/ for a description of the
|
||||
* algorithm.
|
||||
*
|
||||
* Usage:
|
||||
*
|
||||
* $stem = PorterStemmer::Stem($word);
|
||||
*
|
||||
* How easy is that?
|
||||
*
|
||||
* @package PorterStemmer
|
||||
* @author Richard Heyes
|
||||
* @author Jon Abernathy <jon@chuggnutt.com>
|
||||
* @copyright 2005-2016 Richard Heyes (http://www.phpguru.org/)
|
||||
* @license http://www.opensource.org/licenses/mit-license.html MIT License
|
||||
*/
|
||||
class Porter
|
||||
{
|
||||
/**
|
||||
* Regex for matching a consonant
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
private static $regex_consonant = '(?:[bcdfghjklmnpqrstvwxz]|(?<=[aeiou])y|^y)';
|
||||
|
||||
/**
|
||||
* Regex for matching a vowel
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
private static $regex_vowel = '(?:[aeiou]|(?<![aeiou])y)';
|
||||
|
||||
/**
|
||||
* Stems a word. Simple huh?
|
||||
*
|
||||
* @param string $word Word to stem
|
||||
*
|
||||
* @return string Stemmed word
|
||||
*/
|
||||
public static function Stem($word)
|
||||
{
|
||||
if (strlen($word) <= 2) {
|
||||
return $word;
|
||||
}
|
||||
|
||||
$word = self::step1ab($word);
|
||||
$word = self::step1c($word);
|
||||
$word = self::step2($word);
|
||||
$word = self::step3($word);
|
||||
$word = self::step4($word);
|
||||
$word = self::step5($word);
|
||||
|
||||
return $word;
|
||||
}
|
||||
|
||||
/**
|
||||
* Step 1
|
||||
*/
|
||||
private static function step1ab($word)
|
||||
{
|
||||
// Part a
|
||||
if (substr($word, -1) == 's') {
|
||||
|
||||
self::replace($word, 'sses', 'ss')
|
||||
OR self::replace($word, 'ies', 'i')
|
||||
OR self::replace($word, 'ss', 'ss')
|
||||
OR self::replace($word, 's', '');
|
||||
}
|
||||
|
||||
// Part b
|
||||
if (substr($word, -2, 1) != 'e' OR !self::replace($word, 'eed', 'ee', 0)) { // First rule
|
||||
$v = self::$regex_vowel;
|
||||
|
||||
// ing and ed
|
||||
if ( preg_match("#$v+#", substr($word, 0, -3)) && self::replace($word, 'ing', '')
|
||||
OR preg_match("#$v+#", substr($word, 0, -2)) && self::replace($word, 'ed', '')) { // Note use of && and OR, for precedence reasons
|
||||
|
||||
// If one of above two test successful
|
||||
if ( !self::replace($word, 'at', 'ate')
|
||||
AND !self::replace($word, 'bl', 'ble')
|
||||
AND !self::replace($word, 'iz', 'ize')) {
|
||||
|
||||
// Double consonant ending
|
||||
if ( self::doubleConsonant($word)
|
||||
AND substr($word, -2) != 'll'
|
||||
AND substr($word, -2) != 'ss'
|
||||
AND substr($word, -2) != 'zz') {
|
||||
|
||||
$word = substr($word, 0, -1);
|
||||
|
||||
} elseif (self::m($word) == 1 AND self::cvc($word)) {
|
||||
$word .= 'e';
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $word;
|
||||
}
|
||||
|
||||
/**
|
||||
* Step 1c
|
||||
*
|
||||
* @param string $word Word to stem
|
||||
*/
|
||||
private static function step1c($word)
|
||||
{
|
||||
$v = self::$regex_vowel;
|
||||
|
||||
if (substr($word, -1) == 'y' && preg_match("#$v+#", substr($word, 0, -1))) {
|
||||
self::replace($word, 'y', 'i');
|
||||
}
|
||||
|
||||
return $word;
|
||||
}
|
||||
|
||||
/**
|
||||
* Step 2
|
||||
*
|
||||
* @param string $word Word to stem
|
||||
*/
|
||||
private static function step2($word)
|
||||
{
|
||||
switch (substr($word, -2, 1)) {
|
||||
case 'a':
|
||||
self::replace($word, 'ational', 'ate', 0)
|
||||
OR self::replace($word, 'tional', 'tion', 0);
|
||||
break;
|
||||
|
||||
case 'c':
|
||||
self::replace($word, 'enci', 'ence', 0)
|
||||
OR self::replace($word, 'anci', 'ance', 0);
|
||||
break;
|
||||
|
||||
case 'e':
|
||||
self::replace($word, 'izer', 'ize', 0);
|
||||
break;
|
||||
|
||||
case 'g':
|
||||
self::replace($word, 'logi', 'log', 0);
|
||||
break;
|
||||
|
||||
case 'l':
|
||||
self::replace($word, 'entli', 'ent', 0)
|
||||
OR self::replace($word, 'ousli', 'ous', 0)
|
||||
OR self::replace($word, 'alli', 'al', 0)
|
||||
OR self::replace($word, 'bli', 'ble', 0)
|
||||
OR self::replace($word, 'eli', 'e', 0);
|
||||
break;
|
||||
|
||||
case 'o':
|
||||
self::replace($word, 'ization', 'ize', 0)
|
||||
OR self::replace($word, 'ation', 'ate', 0)
|
||||
OR self::replace($word, 'ator', 'ate', 0);
|
||||
break;
|
||||
|
||||
case 's':
|
||||
self::replace($word, 'iveness', 'ive', 0)
|
||||
OR self::replace($word, 'fulness', 'ful', 0)
|
||||
OR self::replace($word, 'ousness', 'ous', 0)
|
||||
OR self::replace($word, 'alism', 'al', 0);
|
||||
break;
|
||||
|
||||
case 't':
|
||||
self::replace($word, 'biliti', 'ble', 0)
|
||||
OR self::replace($word, 'aliti', 'al', 0)
|
||||
OR self::replace($word, 'iviti', 'ive', 0);
|
||||
break;
|
||||
}
|
||||
|
||||
return $word;
|
||||
}
|
||||
|
||||
/**
|
||||
* Step 3
|
||||
*
|
||||
* @param string $word String to stem
|
||||
*/
|
||||
private static function step3($word)
|
||||
{
|
||||
switch (substr($word, -2, 1)) {
|
||||
case 'a':
|
||||
self::replace($word, 'ical', 'ic', 0);
|
||||
break;
|
||||
|
||||
case 's':
|
||||
self::replace($word, 'ness', '', 0);
|
||||
break;
|
||||
|
||||
case 't':
|
||||
self::replace($word, 'icate', 'ic', 0)
|
||||
OR self::replace($word, 'iciti', 'ic', 0);
|
||||
break;
|
||||
|
||||
case 'u':
|
||||
self::replace($word, 'ful', '', 0);
|
||||
break;
|
||||
|
||||
case 'v':
|
||||
self::replace($word, 'ative', '', 0);
|
||||
break;
|
||||
|
||||
case 'z':
|
||||
self::replace($word, 'alize', 'al', 0);
|
||||
break;
|
||||
}
|
||||
|
||||
return $word;
|
||||
}
|
||||
|
||||
/**
|
||||
* Step 4
|
||||
*
|
||||
* @param string $word Word to stem
|
||||
*/
|
||||
private static function step4($word)
|
||||
{
|
||||
switch (substr($word, -2, 1)) {
|
||||
case 'a':
|
||||
self::replace($word, 'al', '', 1);
|
||||
break;
|
||||
|
||||
case 'c':
|
||||
self::replace($word, 'ance', '', 1)
|
||||
OR self::replace($word, 'ence', '', 1);
|
||||
break;
|
||||
|
||||
case 'e':
|
||||
self::replace($word, 'er', '', 1);
|
||||
break;
|
||||
|
||||
case 'i':
|
||||
self::replace($word, 'ic', '', 1);
|
||||
break;
|
||||
|
||||
case 'l':
|
||||
self::replace($word, 'able', '', 1)
|
||||
OR self::replace($word, 'ible', '', 1);
|
||||
break;
|
||||
|
||||
case 'n':
|
||||
self::replace($word, 'ant', '', 1)
|
||||
OR self::replace($word, 'ement', '', 1)
|
||||
OR self::replace($word, 'ment', '', 1)
|
||||
OR self::replace($word, 'ent', '', 1);
|
||||
break;
|
||||
|
||||
case 'o':
|
||||
if (substr($word, -4) == 'tion' OR substr($word, -4) == 'sion') {
|
||||
self::replace($word, 'ion', '', 1);
|
||||
} else {
|
||||
self::replace($word, 'ou', '', 1);
|
||||
}
|
||||
break;
|
||||
|
||||
case 's':
|
||||
self::replace($word, 'ism', '', 1);
|
||||
break;
|
||||
|
||||
case 't':
|
||||
self::replace($word, 'ate', '', 1)
|
||||
OR self::replace($word, 'iti', '', 1);
|
||||
break;
|
||||
|
||||
case 'u':
|
||||
self::replace($word, 'ous', '', 1);
|
||||
break;
|
||||
|
||||
case 'v':
|
||||
self::replace($word, 'ive', '', 1);
|
||||
break;
|
||||
|
||||
case 'z':
|
||||
self::replace($word, 'ize', '', 1);
|
||||
break;
|
||||
}
|
||||
|
||||
return $word;
|
||||
}
|
||||
|
||||
/**
|
||||
* Step 5
|
||||
*
|
||||
* @param string $word Word to stem
|
||||
*/
|
||||
private static function step5($word)
|
||||
{
|
||||
// Part a
|
||||
if (substr($word, -1) == 'e') {
|
||||
if (self::m(substr($word, 0, -1)) > 1) {
|
||||
self::replace($word, 'e', '');
|
||||
|
||||
} elseif (self::m(substr($word, 0, -1)) == 1) {
|
||||
|
||||
if (!self::cvc(substr($word, 0, -1))) {
|
||||
self::replace($word, 'e', '');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Part b
|
||||
if (self::m($word) > 1 AND self::doubleConsonant($word) AND substr($word, -1) == 'l') {
|
||||
$word = substr($word, 0, -1);
|
||||
}
|
||||
|
||||
return $word;
|
||||
}
|
||||
|
||||
/**
|
||||
* Replaces the first string with the second, at the end of the string
|
||||
*
|
||||
* If third arg is given, then the preceding string must match that m
|
||||
* count at least.
|
||||
*
|
||||
* @param string $str String to check
|
||||
* @param string $check Ending to check for
|
||||
* @param string $repl Replacement string
|
||||
* @param int $m Optional minimum number of m() to meet
|
||||
*
|
||||
* @return bool Whether the $check string was at the end of the $str
|
||||
* string. True does not necessarily mean that it was
|
||||
* replaced.
|
||||
*/
|
||||
private static function replace(&$str, $check, $repl, $m = null)
|
||||
{
|
||||
$len = 0 - strlen($check);
|
||||
|
||||
if (substr($str, $len) == $check) {
|
||||
$substr = substr($str, 0, $len);
|
||||
if (is_null($m) OR self::m($substr) > $m) {
|
||||
$str = $substr . $repl;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* What, you mean it's not obvious from the name?
|
||||
*
|
||||
* m() measures the number of consonant sequences in $str. if c is
|
||||
* a consonant sequence and v a vowel sequence, and <..> indicates arbitrary
|
||||
* presence,
|
||||
*
|
||||
* <c><v> gives 0
|
||||
* <c>vc<v> gives 1
|
||||
* <c>vcvc<v> gives 2
|
||||
* <c>vcvcvc<v> gives 3
|
||||
*
|
||||
* @param string $str The string to return the m count for
|
||||
*
|
||||
* @return int The m count
|
||||
*/
|
||||
private static function m($str)
|
||||
{
|
||||
$c = self::$regex_consonant;
|
||||
$v = self::$regex_vowel;
|
||||
|
||||
$str = preg_replace("#^$c+#", '', $str);
|
||||
$str = preg_replace("#$v+$#", '', $str);
|
||||
|
||||
preg_match_all("#($v+$c+)#", $str, $matches);
|
||||
|
||||
return count($matches[1]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true/false as to whether the given string contains two
|
||||
* of the same consonant next to each other at the end of the string.
|
||||
*
|
||||
* @param string $str String to check
|
||||
*
|
||||
* @return bool Result
|
||||
*/
|
||||
private static function doubleConsonant($str)
|
||||
{
|
||||
$c = self::$regex_consonant;
|
||||
|
||||
return preg_match("#$c{2}$#", $str, $matches) AND $matches[0]{0} == $matches[0]{1};
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks for ending CVC sequence where second C is not W, X or Y
|
||||
*
|
||||
* @param string $str String to check
|
||||
*
|
||||
* @return bool Result
|
||||
*/
|
||||
private static function cvc($str)
|
||||
{
|
||||
$c = self::$regex_consonant;
|
||||
$v = self::$regex_vowel;
|
||||
|
||||
return preg_match("#($c$v$c)$#", $str, $matches)
|
||||
AND strlen($matches[1]) == 3
|
||||
AND $matches[1]{2} != 'w'
|
||||
AND $matches[1]{2} != 'x'
|
||||
AND $matches[1]{2} != 'y';
|
||||
}
|
||||
}
|
64351
resources/php_compat_info.json
Normal file
64351
resources/php_compat_info.json
Normal file
File diff suppressed because it is too large
Load diff
45
resources/ssl/README
Normal file
45
resources/ssl/README
Normal file
|
@ -0,0 +1,45 @@
|
|||
This document describes how to set Certificate Authority information.
|
||||
Usually, you need to do this only if you're using a self-signed certificate.
|
||||
|
||||
|
||||
OSX after Yosemite
|
||||
==================
|
||||
|
||||
If you're using a version of Mac OSX after Yosemite, you can not configure
|
||||
certificates from the command line. All libphutil and arcanist options
|
||||
related to CA configuration are ignored.
|
||||
|
||||
Instead, you need to add them to the system keychain. The easiest way to do this
|
||||
is to visit the site in Safari and choose to permanently accept the certificate.
|
||||
|
||||
You can also use `security add-trusted-cert` from the command line.
|
||||
|
||||
|
||||
All Other Systems
|
||||
=================
|
||||
|
||||
If "curl.cainfo" is not set (or you are using PHP older than 5.3.7, where the
|
||||
option was introduced), libphutil uses the "default.pem" certificate authority
|
||||
bundle when making HTTPS requests with cURL. This bundle is extracted from
|
||||
Mozilla's certificates by cURL:
|
||||
|
||||
http://curl.haxx.se/docs/caextract.html
|
||||
|
||||
If you want to use a different CA bundle (for example, because you use
|
||||
self-signed certificates), set "curl.cainfo" if you're using PHP 5.3.7 or newer,
|
||||
or create a file (or symlink) in this directory named "custom.pem".
|
||||
|
||||
If "custom.pem" is present, that file will be used instead of "default.pem".
|
||||
|
||||
If you receive errors using your "custom.pem" file, you can test it directly
|
||||
with `curl` by running a command like this:
|
||||
|
||||
curl -v --cacert path/to/your/custom.pem https://phabricator.example.com/
|
||||
|
||||
Replace "path/to/your/custom.pem" with the path to your "custom.pem" file,
|
||||
and replace "https://phabricator.example.com" with the real URL of your
|
||||
Phabricator install.
|
||||
|
||||
The initial lines of output from `curl` should give you information about the
|
||||
SSL handshake and certificate verification, which may be helpful in resolving
|
||||
the issue.
|
3893
resources/ssl/default.pem
Normal file
3893
resources/ssl/default.pem
Normal file
File diff suppressed because it is too large
Load diff
126
resources/timezones/windows_timezones.json
Normal file
126
resources/timezones/windows_timezones.json
Normal file
|
@ -0,0 +1,126 @@
|
|||
{
|
||||
"Egypt Standard Time": "Africa/Cairo",
|
||||
"Morocco Standard Time": "Africa/Casablanca",
|
||||
"South Africa Standard Time": "Africa/Johannesburg",
|
||||
"W. Central Africa Standard Time": "Africa/Lagos",
|
||||
"E. Africa Standard Time": "Africa/Nairobi",
|
||||
"Libya Standard Time": "Africa/Tripoli",
|
||||
"Namibia Standard Time": "Africa/Windhoek",
|
||||
"Aleutian Standard Time": "America/Adak",
|
||||
"Alaskan Standard Time": "America/Anchorage",
|
||||
"Tocantins Standard Time": "America/Araguaina",
|
||||
"Paraguay Standard Time": "America/Asuncion",
|
||||
"Bahia Standard Time": "America/Bahia",
|
||||
"SA Pacific Standard Time": "America/Bogota",
|
||||
"Argentina Standard Time": "America/Buenos_Aires",
|
||||
"Eastern Standard Time (Mexico)": "America/Cancun",
|
||||
"Venezuela Standard Time": "America/Caracas",
|
||||
"SA Eastern Standard Time": "America/Cayenne",
|
||||
"Central Standard Time": "America/Chicago",
|
||||
"Mountain Standard Time (Mexico)": "America/Chihuahua",
|
||||
"Central Brazilian Standard Time": "America/Cuiaba",
|
||||
"Mountain Standard Time": "America/Denver",
|
||||
"Greenland Standard Time": "America/Godthab",
|
||||
"Turks And Caicos Standard Time": "America/Grand_Turk",
|
||||
"Central America Standard Time": "America/Guatemala",
|
||||
"Atlantic Standard Time": "America/Halifax",
|
||||
"Cuba Standard Time": "America/Havana",
|
||||
"US Eastern Standard Time": "America/Indianapolis",
|
||||
"SA Western Standard Time": "America/La_Paz",
|
||||
"Pacific Standard Time": "America/Los_Angeles",
|
||||
"Central Standard Time (Mexico)": "America/Mexico_City",
|
||||
"Saint Pierre Standard Time": "America/Miquelon",
|
||||
"Montevideo Standard Time": "America/Montevideo",
|
||||
"Eastern Standard Time": "America/New_York",
|
||||
"US Mountain Standard Time": "America/Phoenix",
|
||||
"Haiti Standard Time": "America/Port-au-Prince",
|
||||
"Canada Central Standard Time": "America/Regina",
|
||||
"Pacific SA Standard Time": "America/Santiago",
|
||||
"E. South America Standard Time": "America/Sao_Paulo",
|
||||
"Newfoundland Standard Time": "America/St_Johns",
|
||||
"Pacific Standard Time (Mexico)": "America/Tijuana",
|
||||
"Central Asia Standard Time": "Asia/Almaty",
|
||||
"Jordan Standard Time": "Asia/Amman",
|
||||
"Arabic Standard Time": "Asia/Baghdad",
|
||||
"Azerbaijan Standard Time": "Asia/Baku",
|
||||
"SE Asia Standard Time": "Asia/Bangkok",
|
||||
"Altai Standard Time": "Asia/Barnaul",
|
||||
"Middle East Standard Time": "Asia/Beirut",
|
||||
"India Standard Time": "Asia/Calcutta",
|
||||
"Transbaikal Standard Time": "Asia/Chita",
|
||||
"Sri Lanka Standard Time": "Asia/Colombo",
|
||||
"Syria Standard Time": "Asia/Damascus",
|
||||
"Bangladesh Standard Time": "Asia/Dhaka",
|
||||
"Arabian Standard Time": "Asia/Dubai",
|
||||
"West Bank Standard Time": "Asia/Hebron",
|
||||
"W. Mongolia Standard Time": "Asia/Hovd",
|
||||
"North Asia East Standard Time": "Asia/Irkutsk",
|
||||
"Israel Standard Time": "Asia/Jerusalem",
|
||||
"Afghanistan Standard Time": "Asia/Kabul",
|
||||
"Russia Time Zone 11": "Asia/Kamchatka",
|
||||
"Pakistan Standard Time": "Asia/Karachi",
|
||||
"Nepal Standard Time": "Asia/Katmandu",
|
||||
"North Asia Standard Time": "Asia/Krasnoyarsk",
|
||||
"Magadan Standard Time": "Asia/Magadan",
|
||||
"N. Central Asia Standard Time": "Asia/Novosibirsk",
|
||||
"Omsk Standard Time": "Asia/Omsk",
|
||||
"North Korea Standard Time": "Asia/Pyongyang",
|
||||
"Myanmar Standard Time": "Asia/Rangoon",
|
||||
"Arab Standard Time": "Asia/Riyadh",
|
||||
"Sakhalin Standard Time": "Asia/Sakhalin",
|
||||
"Korea Standard Time": "Asia/Seoul",
|
||||
"China Standard Time": "Asia/Shanghai",
|
||||
"Singapore Standard Time": "Asia/Singapore",
|
||||
"Russia Time Zone 10": "Asia/Srednekolymsk",
|
||||
"Taipei Standard Time": "Asia/Taipei",
|
||||
"West Asia Standard Time": "Asia/Tashkent",
|
||||
"Georgian Standard Time": "Asia/Tbilisi",
|
||||
"Iran Standard Time": "Asia/Tehran",
|
||||
"Tokyo Standard Time": "Asia/Tokyo",
|
||||
"Tomsk Standard Time": "Asia/Tomsk",
|
||||
"Ulaanbaatar Standard Time": "Asia/Ulaanbaatar",
|
||||
"Vladivostok Standard Time": "Asia/Vladivostok",
|
||||
"Yakutsk Standard Time": "Asia/Yakutsk",
|
||||
"Ekaterinburg Standard Time": "Asia/Yekaterinburg",
|
||||
"Caucasus Standard Time": "Asia/Yerevan",
|
||||
"Azores Standard Time": "Atlantic/Azores",
|
||||
"Cape Verde Standard Time": "Atlantic/Cape_Verde",
|
||||
"Greenwich Standard Time": "Atlantic/Reykjavik",
|
||||
"Cen. Australia Standard Time": "Australia/Adelaide",
|
||||
"E. Australia Standard Time": "Australia/Brisbane",
|
||||
"AUS Central Standard Time": "Australia/Darwin",
|
||||
"Aus Central W. Standard Time": "Australia/Eucla",
|
||||
"Tasmania Standard Time": "Australia/Hobart",
|
||||
"Lord Howe Standard Time": "Australia/Lord_Howe",
|
||||
"W. Australia Standard Time": "Australia/Perth",
|
||||
"AUS Eastern Standard Time": "Australia/Sydney",
|
||||
"Dateline Standard Time": "Etc/GMT+12",
|
||||
"Astrakhan Standard Time": "Europe/Astrakhan",
|
||||
"W. Europe Standard Time": "Europe/Berlin",
|
||||
"GTB Standard Time": "Europe/Bucharest",
|
||||
"Central Europe Standard Time": "Europe/Budapest",
|
||||
"E. Europe Standard Time": "Europe/Chisinau",
|
||||
"Turkey Standard Time": "Europe/Istanbul",
|
||||
"Kaliningrad Standard Time": "Europe/Kaliningrad",
|
||||
"FLE Standard Time": "Europe/Kiev",
|
||||
"GMT Standard Time": "Europe/London",
|
||||
"Belarus Standard Time": "Europe/Minsk",
|
||||
"Russian Standard Time": "Europe/Moscow",
|
||||
"Romance Standard Time": "Europe/Paris",
|
||||
"Russia Time Zone 3": "Europe/Samara",
|
||||
"Central European Standard Time": "Europe/Warsaw",
|
||||
"Mauritius Standard Time": "Indian/Mauritius",
|
||||
"Samoa Standard Time": "Pacific/Apia",
|
||||
"New Zealand Standard Time": "Pacific/Auckland",
|
||||
"Bougainville Standard Time": "Pacific/Bougainville",
|
||||
"Chatham Islands Standard Time": "Pacific/Chatham",
|
||||
"Easter Island Standard Time": "Pacific/Easter",
|
||||
"Fiji Standard Time": "Pacific/Fiji",
|
||||
"Central Pacific Standard Time": "Pacific/Guadalcanal",
|
||||
"Hawaiian Standard Time": "Pacific/Honolulu",
|
||||
"Line Islands Standard Time": "Pacific/Kiritimati",
|
||||
"Marquesas Standard Time": "Pacific/Marquesas",
|
||||
"Norfolk Standard Time": "Pacific/Norfolk",
|
||||
"West Pacific Standard Time": "Pacific/Port_Moresby",
|
||||
"Tonga Standard Time": "Pacific/Tongatapu"
|
||||
}
|
|
@ -1,58 +0,0 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* Adjust 'include_path' to add locations where we'll search for libphutil.
|
||||
* We look in these places:
|
||||
*
|
||||
* - Next to 'arcanist/'.
|
||||
* - Anywhere in the normal PHP 'include_path'.
|
||||
* - Inside 'arcanist/externals/includes/'.
|
||||
*
|
||||
* When looking in these places, we expect to find a 'libphutil/' directory.
|
||||
*/
|
||||
function arcanist_adjust_php_include_path() {
|
||||
// The 'arcanist/' directory.
|
||||
$arcanist_dir = dirname(dirname(__FILE__));
|
||||
|
||||
// The parent directory of 'arcanist/'.
|
||||
$parent_dir = dirname($arcanist_dir);
|
||||
|
||||
// The 'arcanist/externals/includes/' directory.
|
||||
$include_dir = implode(
|
||||
DIRECTORY_SEPARATOR,
|
||||
array(
|
||||
$arcanist_dir,
|
||||
'externals',
|
||||
'includes',
|
||||
));
|
||||
|
||||
$php_include_path = ini_get('include_path');
|
||||
$php_include_path = implode(
|
||||
PATH_SEPARATOR,
|
||||
array(
|
||||
$parent_dir,
|
||||
$php_include_path,
|
||||
$include_dir,
|
||||
));
|
||||
|
||||
ini_set('include_path', $php_include_path);
|
||||
}
|
||||
arcanist_adjust_php_include_path();
|
||||
|
||||
if (getenv('ARC_PHUTIL_PATH')) {
|
||||
@include_once getenv('ARC_PHUTIL_PATH').'/scripts/__init_script__.php';
|
||||
} else {
|
||||
@include_once 'libphutil/scripts/__init_script__.php';
|
||||
}
|
||||
if (!@constant('__LIBPHUTIL__')) {
|
||||
echo "ERROR: Unable to load libphutil. Put libphutil/ next to arcanist/, or ".
|
||||
"update your PHP 'include_path' to include the parent directory of ".
|
||||
"libphutil/, or symlink libphutil/ into arcanist/externals/includes/.\n";
|
||||
exit(1);
|
||||
}
|
||||
|
||||
phutil_load_library(dirname(dirname(__FILE__)).'/src/');
|
||||
|
||||
PhutilTranslator::getInstance()
|
||||
->setLocale(PhutilLocale::loadLocale('en_US'))
|
||||
->setTranslations(PhutilTranslation::getTranslationMapForLocale('en_US'));
|
7
scripts/build_xhpast.php
Executable file
7
scripts/build_xhpast.php
Executable file
|
@ -0,0 +1,7 @@
|
|||
#!/usr/bin/env php
|
||||
<?php
|
||||
|
||||
require_once dirname(__FILE__).'/__init_script__.php';
|
||||
|
||||
PhutilXHPASTBinary::build();
|
||||
echo pht('Build successful!')."\n";
|
131
scripts/daemon/exec/exec_daemon.php
Executable file
131
scripts/daemon/exec/exec_daemon.php
Executable file
|
@ -0,0 +1,131 @@
|
|||
#!/usr/bin/env php
|
||||
<?php
|
||||
|
||||
if (function_exists('pcntl_async_signals')) {
|
||||
pcntl_async_signals(true);
|
||||
} else {
|
||||
declare(ticks = 1);
|
||||
}
|
||||
|
||||
require_once dirname(__FILE__).'/../../__init_script__.php';
|
||||
|
||||
if (!posix_isatty(STDOUT)) {
|
||||
$sid = posix_setsid();
|
||||
if ($sid <= 0) {
|
||||
throw new Exception(pht('Failed to create new process session!'));
|
||||
}
|
||||
}
|
||||
|
||||
$args = new PhutilArgumentParser($argv);
|
||||
$args->setTagline(pht('daemon executor'));
|
||||
$args->setSynopsis(<<<EOHELP
|
||||
**exec_daemon.php** [__options__] __daemon__ ...
|
||||
Run an instance of __daemon__.
|
||||
EOHELP
|
||||
);
|
||||
$args->parse(
|
||||
array(
|
||||
array(
|
||||
'name' => 'trace',
|
||||
'help' => pht('Enable debug tracing.'),
|
||||
),
|
||||
array(
|
||||
'name' => 'trace-memory',
|
||||
'help' => pht('Enable debug memory tracing.'),
|
||||
),
|
||||
array(
|
||||
'name' => 'verbose',
|
||||
'help' => pht('Enable verbose activity logging.'),
|
||||
),
|
||||
array(
|
||||
'name' => 'label',
|
||||
'short' => 'l',
|
||||
'param' => 'label',
|
||||
'help' => pht(
|
||||
'Optional process label. Makes "%s" nicer, no behavioral effects.',
|
||||
'ps'),
|
||||
),
|
||||
array(
|
||||
'name' => 'daemon',
|
||||
'wildcard' => true,
|
||||
),
|
||||
));
|
||||
|
||||
$trace_memory = $args->getArg('trace-memory');
|
||||
$trace_mode = $args->getArg('trace') || $trace_memory;
|
||||
$verbose = $args->getArg('verbose');
|
||||
|
||||
if (function_exists('posix_isatty') && posix_isatty(STDIN)) {
|
||||
fprintf(STDERR, pht('Reading daemon configuration from stdin...')."\n");
|
||||
}
|
||||
$config = @file_get_contents('php://stdin');
|
||||
$config = id(new PhutilJSONParser())->parse($config);
|
||||
|
||||
PhutilTypeSpec::checkMap(
|
||||
$config,
|
||||
array(
|
||||
'log' => 'optional string|null',
|
||||
'argv' => 'optional list<wild>',
|
||||
'load' => 'optional list<string>',
|
||||
'down' => 'optional int',
|
||||
));
|
||||
|
||||
$log = idx($config, 'log');
|
||||
|
||||
if ($log) {
|
||||
ini_set('error_log', $log);
|
||||
PhutilErrorHandler::setErrorListener(array('PhutilDaemon', 'errorListener'));
|
||||
}
|
||||
|
||||
$load = idx($config, 'load', array());
|
||||
foreach ($load as $library) {
|
||||
$library = Filesystem::resolvePath($library);
|
||||
phutil_load_library($library);
|
||||
}
|
||||
|
||||
PhutilErrorHandler::initialize();
|
||||
|
||||
$daemon = $args->getArg('daemon');
|
||||
if (!$daemon) {
|
||||
throw new PhutilArgumentUsageException(
|
||||
pht('Specify which class of daemon to start.'));
|
||||
} else if (count($daemon) > 1) {
|
||||
throw new PhutilArgumentUsageException(
|
||||
pht('Specify exactly one daemon to start.'));
|
||||
} else {
|
||||
$daemon = head($daemon);
|
||||
if (!class_exists($daemon)) {
|
||||
throw new PhutilArgumentUsageException(
|
||||
pht(
|
||||
'No class "%s" exists in any known library.',
|
||||
$daemon));
|
||||
} else if (!is_subclass_of($daemon, 'PhutilDaemon')) {
|
||||
throw new PhutilArgumentUsageException(
|
||||
pht(
|
||||
'Class "%s" is not a subclass of "%s".',
|
||||
$daemon,
|
||||
'PhutilDaemon'));
|
||||
}
|
||||
}
|
||||
|
||||
$argv = idx($config, 'argv', array());
|
||||
$daemon = newv($daemon, array($argv));
|
||||
|
||||
if ($trace_mode) {
|
||||
$daemon->setTraceMode();
|
||||
}
|
||||
|
||||
if ($trace_memory) {
|
||||
$daemon->setTraceMemory();
|
||||
}
|
||||
|
||||
if ($verbose) {
|
||||
$daemon->setVerbose(true);
|
||||
}
|
||||
|
||||
$down_duration = idx($config, 'down');
|
||||
if ($down_duration) {
|
||||
$daemon->setScaledownDuration($down_duration);
|
||||
}
|
||||
|
||||
$daemon->execute();
|
13
scripts/daemon/launch_daemon.php
Executable file
13
scripts/daemon/launch_daemon.php
Executable file
|
@ -0,0 +1,13 @@
|
|||
#!/usr/bin/env php
|
||||
<?php
|
||||
|
||||
if (function_exists('pcntl_async_signals')) {
|
||||
pcntl_async_signals(true);
|
||||
} else {
|
||||
declare(ticks = 1);
|
||||
}
|
||||
|
||||
$root = dirname(dirname(dirname(__FILE__)));
|
||||
require_once $root.'/scripts/__init_script__.php';
|
||||
$overseer = new PhutilDaemonOverseer($argv);
|
||||
$overseer->run();
|
21
scripts/daemon/torture/resist-death.php
Executable file
21
scripts/daemon/torture/resist-death.php
Executable file
|
@ -0,0 +1,21 @@
|
|||
#!/usr/bin/env php
|
||||
<?php
|
||||
|
||||
require_once dirname(__FILE__).'/../../__init_script__.php';
|
||||
|
||||
// This script just creates a process which is difficult to terminate. It is
|
||||
// used for daemon resilience tests.
|
||||
|
||||
declare(ticks = 1);
|
||||
pcntl_signal(SIGTERM, 'ignore');
|
||||
pcntl_signal(SIGINT, 'ignore');
|
||||
|
||||
function ignore($signo) {
|
||||
return;
|
||||
}
|
||||
|
||||
echo pht('Resisting death; sleeping forever...')."\n";
|
||||
|
||||
while (true) {
|
||||
sleep(60);
|
||||
}
|
|
@ -1,3 +1,4 @@
|
|||
<?php
|
||||
|
||||
require_once dirname(__FILE__).'/init-script.php';
|
||||
require_once dirname(dirname(dirname(__FILE__))).'/support/ArcanistRuntime.php';
|
||||
|
|
97
scripts/init/init-script.php
Normal file
97
scripts/init/init-script.php
Normal file
|
@ -0,0 +1,97 @@
|
|||
<?php
|
||||
|
||||
function __arcanist_init_script__() {
|
||||
// Adjust the runtime language configuration to be reasonable and inline with
|
||||
// expectations. We do this first, then load libraries.
|
||||
|
||||
// There may be some kind of auto-prepend script configured which starts an
|
||||
// output buffer. Discard any such output buffers so messages can be sent to
|
||||
// stdout (if a user wants to capture output from a script, there are a large
|
||||
// number of ways they can accomplish it legitimately; historically, we ran
|
||||
// into this on only one install which had some bizarre configuration, but it
|
||||
// was difficult to diagnose because the symptom is "no messages of any
|
||||
// kind").
|
||||
while (ob_get_level() > 0) {
|
||||
ob_end_clean();
|
||||
}
|
||||
|
||||
error_reporting(E_ALL | E_STRICT);
|
||||
|
||||
$config_map = array(
|
||||
// Always display script errors. Without this, they may not appear, which is
|
||||
// unhelpful when users encounter a problem. On the web this is a security
|
||||
// concern because you don't want to expose errors to clients, but in a
|
||||
// script context we always want to show errors.
|
||||
'display_errors' => true,
|
||||
|
||||
// Send script error messages to the server's `error_log` setting.
|
||||
'log_errors' => true,
|
||||
|
||||
// Set the error log to the default, so errors go to stderr. Without this
|
||||
// errors may end up in some log, and users may not know where the log is
|
||||
// or check it.
|
||||
'error_log' => null,
|
||||
|
||||
// XDebug raises a fatal error if the call stack gets too deep, but the
|
||||
// default setting is 100, which we may exceed legitimately with module
|
||||
// includes (and in other cases, like recursive filesystem operations
|
||||
// applied to 100+ levels of directory nesting). Stop it from triggering:
|
||||
// we explicitly limit recursive algorithms which should be limited.
|
||||
//
|
||||
// After Feb 2014, XDebug interprets a value of 0 to mean "do not allow any
|
||||
// function calls". Previously, 0 effectively disabled this check. For
|
||||
// context, see T5027.
|
||||
'xdebug.max_nesting_level' => PHP_INT_MAX,
|
||||
|
||||
// Don't limit memory, doing so just generally just prevents us from
|
||||
// processing large inputs without many tangible benefits.
|
||||
'memory_limit' => -1,
|
||||
);
|
||||
|
||||
foreach ($config_map as $config_key => $config_value) {
|
||||
ini_set($config_key, $config_value);
|
||||
}
|
||||
|
||||
if (!ini_get('date.timezone')) {
|
||||
// If the timezone isn't set, PHP issues a warning whenever you try to parse
|
||||
// a date (like those from Git or Mercurial logs), even if the date contains
|
||||
// timezone information (like "PST" or "-0700") which makes the
|
||||
// environmental timezone setting is completely irrelevant. We never rely on
|
||||
// the system timezone setting in any capacity, so prevent PHP from flipping
|
||||
// out by setting it to a safe default (UTC) if it isn't set to some other
|
||||
// value.
|
||||
date_default_timezone_set('UTC');
|
||||
}
|
||||
|
||||
// Adjust `include_path`.
|
||||
ini_set('include_path', implode(PATH_SEPARATOR, array(
|
||||
dirname(dirname(__FILE__)).'/externals/includes',
|
||||
ini_get('include_path'),
|
||||
)));
|
||||
|
||||
// Disable the insanely dangerous XML entity loader by default.
|
||||
if (function_exists('libxml_disable_entity_loader')) {
|
||||
libxml_disable_entity_loader(true);
|
||||
}
|
||||
|
||||
// Now, load the library.
|
||||
|
||||
$root = dirname(dirname(dirname(__FILE__)));
|
||||
require_once $root.'/src/__phutil_library_init__.php';
|
||||
|
||||
PhutilErrorHandler::initialize();
|
||||
|
||||
// If "variables_order" excludes "E", silently repair it so that $_ENV has
|
||||
// the values we expect.
|
||||
PhutilExecutionEnvironment::repairMissingVariablesOrder();
|
||||
|
||||
$router = PhutilSignalRouter::initialize();
|
||||
|
||||
$handler = new PhutilBacktraceSignalHandler();
|
||||
$router->installHandler('phutil.backtrace', $handler);
|
||||
|
||||
$handler = new PhutilConsoleMetricsSignalHandler();
|
||||
$router->installHandler('phutil.winch', $handler);
|
||||
}
|
||||
|
||||
__arcanist_init_script__();
|
78
scripts/phutil_rebuild_map.php
Executable file
78
scripts/phutil_rebuild_map.php
Executable file
|
@ -0,0 +1,78 @@
|
|||
#!/usr/bin/env php
|
||||
<?php
|
||||
|
||||
require_once dirname(__FILE__).'/__init_script__.php';
|
||||
|
||||
$args = new PhutilArgumentParser($argv);
|
||||
$args->setTagline(pht('rebuild the library map file'));
|
||||
$args->setSynopsis(<<<EOHELP
|
||||
**phutil_rebuild_map.php** [__options__] __root__
|
||||
Rebuild the library map file for a libphutil library.
|
||||
|
||||
EOHELP
|
||||
);
|
||||
|
||||
$args->parseStandardArguments();
|
||||
$args->parse(
|
||||
array(
|
||||
array(
|
||||
'name' => 'quiet',
|
||||
'help' => pht('Do not write status messages to stderr.'),
|
||||
),
|
||||
array(
|
||||
'name' => 'drop-cache',
|
||||
'help' => pht(
|
||||
'Drop the symbol cache and rebuild the entire map from scratch.'),
|
||||
),
|
||||
array(
|
||||
'name' => 'limit',
|
||||
'param' => 'N',
|
||||
'default' => 8,
|
||||
'help' => pht(
|
||||
'Controls the number of symbol mapper subprocesses run at once. '.
|
||||
'Defaults to 8.'),
|
||||
),
|
||||
array(
|
||||
'name' => 'show',
|
||||
'help' => pht(
|
||||
'Print symbol map to stdout instead of writing it to the map file.'),
|
||||
),
|
||||
array(
|
||||
'name' => 'ugly',
|
||||
'help' => pht(
|
||||
'Use faster but less readable serialization for %s.',
|
||||
'--show'),
|
||||
),
|
||||
array(
|
||||
'name' => 'root',
|
||||
'wildcard' => true,
|
||||
),
|
||||
));
|
||||
|
||||
$root = $args->getArg('root');
|
||||
if (count($root) !== 1) {
|
||||
throw new Exception(pht('Provide exactly one library root!'));
|
||||
}
|
||||
$root = Filesystem::resolvePath(head($root));
|
||||
|
||||
$builder = new PhutilLibraryMapBuilder($root);
|
||||
$builder->setQuiet($args->getArg('quiet'));
|
||||
$builder->setSubprocessLimit($args->getArg('limit'));
|
||||
|
||||
if ($args->getArg('drop-cache')) {
|
||||
$builder->dropSymbolCache();
|
||||
}
|
||||
|
||||
if ($args->getArg('show')) {
|
||||
$library_map = $builder->buildMap();
|
||||
|
||||
if ($args->getArg('ugly')) {
|
||||
echo json_encode($library_map);
|
||||
} else {
|
||||
echo id(new PhutilJSON())->encodeFormatted($library_map);
|
||||
}
|
||||
} else {
|
||||
$builder->buildAndWriteMap();
|
||||
}
|
||||
|
||||
exit(0);
|
586
scripts/phutil_symbols.php
Executable file
586
scripts/phutil_symbols.php
Executable file
|
@ -0,0 +1,586 @@
|
|||
#!/usr/bin/env php
|
||||
<?php
|
||||
|
||||
// We have to do this first before we load any symbols, because we define the
|
||||
// built-in symbol list through introspection.
|
||||
$builtins = phutil_symbols_get_builtins();
|
||||
|
||||
require_once dirname(__FILE__).'/__init_script__.php';
|
||||
|
||||
$args = new PhutilArgumentParser($argv);
|
||||
$args->setTagline(pht('identify symbols in a PHP source file'));
|
||||
$args->setSynopsis(<<<EOHELP
|
||||
**phutil_symbols.php** [__options__] __path.php__
|
||||
Identify the symbols (clases, functions and interfaces) in a PHP
|
||||
source file. Symbols are divided into "have" symbols (symbols the file
|
||||
declares) and "need" symbols (symbols the file depends on). For example,
|
||||
class declarations are "have" symbols, while object instantiations
|
||||
with "new X()" are "need" symbols.
|
||||
|
||||
Dependencies on builtins and symbols marked '@phutil-external-symbol'
|
||||
in docblocks are omitted without __--all__.
|
||||
|
||||
Symbols are reported in JSON on stdout.
|
||||
|
||||
This script is used internally by libphutil/arcanist to build maps of
|
||||
library symbols.
|
||||
|
||||
It would be nice to eventually implement this as a C++ xhpast binary,
|
||||
as it's relatively stable and performance is currently awful
|
||||
(500ms+ for moderately large files).
|
||||
|
||||
EOHELP
|
||||
);
|
||||
$args->parseStandardArguments();
|
||||
$args->parse(
|
||||
array(
|
||||
array(
|
||||
'name' => 'all',
|
||||
'help' => pht(
|
||||
'Report all symbols, including built-ins and declared externals.'),
|
||||
),
|
||||
array(
|
||||
'name' => 'ugly',
|
||||
'help' => pht('Do not prettify JSON output.'),
|
||||
),
|
||||
array(
|
||||
'name' => 'path',
|
||||
'wildcard' => true,
|
||||
'help' => pht('PHP Source file to analyze.'),
|
||||
),
|
||||
));
|
||||
|
||||
$paths = $args->getArg('path');
|
||||
if (count($paths) !== 1) {
|
||||
throw new Exception(pht('Specify exactly one path!'));
|
||||
}
|
||||
$path = Filesystem::resolvePath(head($paths));
|
||||
|
||||
$show_all = $args->getArg('all');
|
||||
|
||||
$source_code = Filesystem::readFile($path);
|
||||
|
||||
try {
|
||||
$tree = XHPASTTree::newFromData($source_code);
|
||||
} catch (XHPASTSyntaxErrorException $ex) {
|
||||
$result = array(
|
||||
'error' => $ex->getMessage(),
|
||||
'line' => $ex->getErrorLine(),
|
||||
'file' => $path,
|
||||
);
|
||||
$json = new PhutilJSON();
|
||||
echo $json->encodeFormatted($result);
|
||||
exit(0);
|
||||
}
|
||||
|
||||
$root = $tree->getRootNode();
|
||||
$root->buildSelectCache();
|
||||
|
||||
// -( Unsupported Constructs )------------------------------------------------
|
||||
|
||||
$namespaces = $root->selectDescendantsOfType('n_NAMESPACE');
|
||||
foreach ($namespaces as $namespace) {
|
||||
phutil_fail_on_unsupported_feature($namespace, $path, pht('namespaces'));
|
||||
}
|
||||
|
||||
$uses = $root->selectDescendantsOfType('n_USE');
|
||||
foreach ($namespaces as $namespace) {
|
||||
phutil_fail_on_unsupported_feature(
|
||||
$namespace, $path, pht('namespace `%s` statements', 'use'));
|
||||
}
|
||||
|
||||
$possible_traits = $root->selectDescendantsOfType('n_CLASS_DECLARATION');
|
||||
foreach ($possible_traits as $possible_trait) {
|
||||
$attributes = $possible_trait->getChildByIndex(0);
|
||||
// Can't use getChildByIndex here because not all classes have attributes
|
||||
foreach ($attributes->getChildren() as $attribute) {
|
||||
if (strtolower($attribute->getConcreteString()) === 'trait') {
|
||||
phutil_fail_on_unsupported_feature($possible_trait, $path, pht('traits'));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// -( Marked Externals )------------------------------------------------------
|
||||
|
||||
|
||||
// Identify symbols marked with "@phutil-external-symbol", so we exclude them
|
||||
// from the dependency list.
|
||||
|
||||
$externals = array();
|
||||
$doc_parser = new PhutilDocblockParser();
|
||||
foreach ($root->getTokens() as $token) {
|
||||
if ($token->getTypeName() === 'T_DOC_COMMENT') {
|
||||
list($block, $special) = $doc_parser->parse($token->getValue());
|
||||
|
||||
$ext_list = idx($special, 'phutil-external-symbol');
|
||||
$ext_list = (array)$ext_list;
|
||||
$ext_list = array_filter($ext_list);
|
||||
|
||||
foreach ($ext_list as $ext_ref) {
|
||||
$matches = null;
|
||||
if (preg_match('/^\s*(\S+)\s+(\S+)/', $ext_ref, $matches)) {
|
||||
$externals[$matches[1]][$matches[2]] = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// -( Declarations and Dependencies )-----------------------------------------
|
||||
|
||||
|
||||
// The first stage of analysis is to find all the symbols we declare in the
|
||||
// file (like functions and classes) and all the symbols we use in the file
|
||||
// (like calling functions and invoking classes). Later, we filter this list
|
||||
// to exclude builtins.
|
||||
|
||||
|
||||
$have = array(); // For symbols we declare.
|
||||
$need = array(); // For symbols we use.
|
||||
$xmap = array(); // For extended classes and implemented interfaces.
|
||||
|
||||
|
||||
// -( Functions )-------------------------------------------------------------
|
||||
|
||||
|
||||
// Find functions declared in this file.
|
||||
|
||||
// This is "function f() { ... }".
|
||||
$functions = $root->selectDescendantsOfType('n_FUNCTION_DECLARATION');
|
||||
foreach ($functions as $function) {
|
||||
$name = $function->getChildByIndex(2);
|
||||
if ($name->getTypeName() === 'n_EMPTY') {
|
||||
// This is an anonymous function; don't record it into the symbol
|
||||
// index.
|
||||
continue;
|
||||
}
|
||||
$have[] = array(
|
||||
'type' => 'function',
|
||||
'symbol' => $name,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
// Find functions used by this file. Uses:
|
||||
//
|
||||
// - Explicit Call
|
||||
// - String literal passed to call_user_func() or call_user_func_array()
|
||||
// - String literal in array literal in call_user_func()/call_user_func_array()
|
||||
//
|
||||
// TODO: Possibly support these:
|
||||
//
|
||||
// - String literal in ReflectionFunction().
|
||||
|
||||
// This is "f();".
|
||||
$calls = $root->selectDescendantsOfType('n_FUNCTION_CALL');
|
||||
foreach ($calls as $call) {
|
||||
$name = $call->getChildByIndex(0);
|
||||
if ($name->getTypeName() === 'n_VARIABLE' ||
|
||||
$name->getTypeName() === 'n_VARIABLE_VARIABLE') {
|
||||
// Ignore these, we can't analyze them.
|
||||
continue;
|
||||
}
|
||||
if ($name->getTypeName() === 'n_CLASS_STATIC_ACCESS') {
|
||||
// These are "C::f()", we'll pick this up later on.
|
||||
continue;
|
||||
}
|
||||
$call_name = $name->getConcreteString();
|
||||
if ($call_name === 'call_user_func' ||
|
||||
$call_name === 'call_user_func_array') {
|
||||
$params = $call->getChildByIndex(1)->getChildren();
|
||||
if (!count($params)) {
|
||||
// This is a bare call_user_func() with no arguments; just ignore it.
|
||||
continue;
|
||||
}
|
||||
$symbol = array_shift($params);
|
||||
$type = 'function';
|
||||
$symbol_value = $symbol->getStringLiteralValue();
|
||||
$pos = strpos($symbol_value, '::');
|
||||
if ($pos) {
|
||||
$type = 'class';
|
||||
$symbol_value = substr($symbol_value, 0, $pos);
|
||||
} else if ($symbol->getTypeName() === 'n_ARRAY_LITERAL') {
|
||||
try {
|
||||
$type = 'class';
|
||||
$symbol_value = idx($symbol->evalStatic(), 0);
|
||||
} catch (Exception $ex) {}
|
||||
}
|
||||
if ($symbol_value && strpos($symbol_value, '$') === false) {
|
||||
$need[] = array(
|
||||
'type' => $type,
|
||||
'name' => $symbol_value,
|
||||
'symbol' => $symbol,
|
||||
);
|
||||
}
|
||||
} else {
|
||||
$need[] = array(
|
||||
'type' => 'function',
|
||||
'symbol' => $name,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// -( Classes )---------------------------------------------------------------
|
||||
|
||||
|
||||
// Find classes declared by this file.
|
||||
|
||||
|
||||
// This is "class X ... { ... }".
|
||||
$classes = $root->selectDescendantsOfType('n_CLASS_DECLARATION');
|
||||
foreach ($classes as $class) {
|
||||
$class_name = $class->getChildByIndex(1);
|
||||
$have[] = array(
|
||||
'type' => 'class',
|
||||
'symbol' => $class_name,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
// Find classes used by this file. We identify these:
|
||||
//
|
||||
// - class ... extends X
|
||||
// - new X
|
||||
// - Static method call
|
||||
// - Static property access
|
||||
// - Use of class constant
|
||||
// - typehints
|
||||
// - catch
|
||||
// - instanceof
|
||||
// - newv()
|
||||
//
|
||||
// TODO: Possibly support these:
|
||||
//
|
||||
// - String literal in ReflectionClass().
|
||||
|
||||
|
||||
// This is "class X ... { ... }".
|
||||
$classes = $root->selectDescendantsOfType('n_CLASS_DECLARATION');
|
||||
foreach ($classes as $class) {
|
||||
$class_name = $class->getChildByIndex(1)->getConcreteString();
|
||||
$extends = $class->getChildByIndex(2);
|
||||
foreach ($extends->selectDescendantsOfType('n_CLASS_NAME') as $parent) {
|
||||
$need[] = array(
|
||||
'type' => 'class',
|
||||
'symbol' => $parent,
|
||||
);
|
||||
|
||||
// Track all 'extends' in the extension map.
|
||||
$xmap[$class_name][] = $parent->getConcreteString();
|
||||
}
|
||||
}
|
||||
|
||||
// This is "new X()".
|
||||
$uses_of_new = $root->selectDescendantsOfType('n_NEW');
|
||||
foreach ($uses_of_new as $new_operator) {
|
||||
$name = $new_operator->getChildByIndex(0);
|
||||
if ($name->getTypeName() === 'n_VARIABLE' ||
|
||||
$name->getTypeName() === 'n_VARIABLE_VARIABLE') {
|
||||
continue;
|
||||
}
|
||||
$need[] = array(
|
||||
'type' => 'class',
|
||||
'symbol' => $name,
|
||||
);
|
||||
}
|
||||
|
||||
// This covers all of "X::$y", "X::y()" and "X::CONST".
|
||||
$static_uses = $root->selectDescendantsOfType('n_CLASS_STATIC_ACCESS');
|
||||
foreach ($static_uses as $static_use) {
|
||||
$name = $static_use->getChildByIndex(0);
|
||||
if ($name->getTypeName() !== 'n_CLASS_NAME') {
|
||||
continue;
|
||||
}
|
||||
$need[] = array(
|
||||
'type' => 'class',
|
||||
'symbol' => $name,
|
||||
);
|
||||
}
|
||||
|
||||
// This is "function (X $x)".
|
||||
$parameters = $root->selectDescendantsOfType('n_DECLARATION_PARAMETER');
|
||||
foreach ($parameters as $parameter) {
|
||||
$hint = $parameter->getChildByIndex(0);
|
||||
if ($hint->getTypeName() !== 'n_CLASS_NAME') {
|
||||
continue;
|
||||
}
|
||||
$need[] = array(
|
||||
'type' => 'class/interface',
|
||||
'symbol' => $hint,
|
||||
);
|
||||
}
|
||||
|
||||
$returns = $root->selectDescendantsOfType('n_DECLARATION_RETURN');
|
||||
foreach ($returns as $return) {
|
||||
$hint = $return->getChildByIndex(0);
|
||||
if ($hint->getTypeName() !== 'n_CLASS_NAME') {
|
||||
continue;
|
||||
}
|
||||
$need[] = array(
|
||||
'type' => 'class/interface',
|
||||
'symbol' => $hint,
|
||||
);
|
||||
}
|
||||
|
||||
// This is "catch (Exception $ex)".
|
||||
$catches = $root->selectDescendantsOfType('n_CATCH');
|
||||
foreach ($catches as $catch) {
|
||||
$need[] = array(
|
||||
'type' => 'class/interface',
|
||||
'symbol' => $catch->getChildOfType(0, 'n_CLASS_NAME'),
|
||||
);
|
||||
}
|
||||
|
||||
// This is "$x instanceof X".
|
||||
$instanceofs = $root->selectDescendantsOfType('n_BINARY_EXPRESSION');
|
||||
foreach ($instanceofs as $instanceof) {
|
||||
$operator = $instanceof->getChildOfType(1, 'n_OPERATOR');
|
||||
if ($operator->getConcreteString() !== 'instanceof') {
|
||||
continue;
|
||||
}
|
||||
$class = $instanceof->getChildByIndex(2);
|
||||
if ($class->getTypeName() !== 'n_CLASS_NAME') {
|
||||
continue;
|
||||
}
|
||||
$need[] = array(
|
||||
'type' => 'class/interface',
|
||||
'symbol' => $class,
|
||||
);
|
||||
}
|
||||
|
||||
// This is "newv('X')".
|
||||
$calls = $root->selectDescendantsOfType('n_FUNCTION_CALL');
|
||||
foreach ($calls as $call) {
|
||||
$call_name = $call->getChildByIndex(0)->getConcreteString();
|
||||
if ($call_name !== 'newv') {
|
||||
continue;
|
||||
}
|
||||
$params = $call->getChildByIndex(1)->getChildren();
|
||||
if (!count($params)) {
|
||||
continue;
|
||||
}
|
||||
$symbol = reset($params);
|
||||
$symbol_value = $symbol->getStringLiteralValue();
|
||||
if ($symbol_value && strpos($symbol_value, '$') === false) {
|
||||
$need[] = array(
|
||||
'type' => 'class',
|
||||
'name' => $symbol_value,
|
||||
'symbol' => $symbol,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// -( Interfaces )------------------------------------------------------------
|
||||
|
||||
|
||||
// Find interfaces declared in this file.
|
||||
|
||||
|
||||
// This is "interface X .. { ... }".
|
||||
$interfaces = $root->selectDescendantsOfType('n_INTERFACE_DECLARATION');
|
||||
foreach ($interfaces as $interface) {
|
||||
$interface_name = $interface->getChildByIndex(1);
|
||||
$have[] = array(
|
||||
'type' => 'interface',
|
||||
'symbol' => $interface_name,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
// Find interfaces used by this file. We identify these:
|
||||
//
|
||||
// - class ... implements X
|
||||
// - interface ... extends X
|
||||
|
||||
|
||||
// This is "class X ... { ... }".
|
||||
$classes = $root->selectDescendantsOfType('n_CLASS_DECLARATION');
|
||||
foreach ($classes as $class) {
|
||||
$class_name = $class->getChildByIndex(1)->getConcreteString();
|
||||
$implements = $class->getChildByIndex(3);
|
||||
$interfaces = $implements->selectDescendantsOfType('n_CLASS_NAME');
|
||||
foreach ($interfaces as $interface) {
|
||||
$need[] = array(
|
||||
'type' => 'interface',
|
||||
'symbol' => $interface,
|
||||
);
|
||||
|
||||
// Track 'class ... implements' in the extension map.
|
||||
$xmap[$class_name][] = $interface->getConcreteString();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// This is "interface X ... { ... }".
|
||||
$interfaces = $root->selectDescendantsOfType('n_INTERFACE_DECLARATION');
|
||||
foreach ($interfaces as $interface) {
|
||||
$interface_name = $interface->getChildByIndex(1)->getConcreteString();
|
||||
|
||||
$extends = $interface->getChildByIndex(2);
|
||||
foreach ($extends->selectDescendantsOfType('n_CLASS_NAME') as $parent) {
|
||||
$need[] = array(
|
||||
'type' => 'interface',
|
||||
'symbol' => $parent,
|
||||
);
|
||||
|
||||
// Track 'interface ... extends' in the extension map.
|
||||
$xmap[$interface_name][] = $parent->getConcreteString();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// -( Analysis )--------------------------------------------------------------
|
||||
|
||||
|
||||
$declared_symbols = array();
|
||||
foreach ($have as $key => $spec) {
|
||||
$name = $spec['symbol']->getConcreteString();
|
||||
$declared_symbols[$spec['type']][$name] = $spec['symbol']->getOffset();
|
||||
}
|
||||
|
||||
$required_symbols = array();
|
||||
foreach ($need as $key => $spec) {
|
||||
$name = idx($spec, 'name');
|
||||
if (!$name) {
|
||||
$name = $spec['symbol']->getConcreteString();
|
||||
}
|
||||
|
||||
$type = $spec['type'];
|
||||
foreach (explode('/', $type) as $libtype) {
|
||||
if (!$show_all) {
|
||||
if (!empty($externals[$libtype][$name])) {
|
||||
// Ignore symbols declared as externals.
|
||||
continue 2;
|
||||
}
|
||||
if (!empty($builtins[$libtype][$name])) {
|
||||
// Ignore symbols declared as builtins.
|
||||
continue 2;
|
||||
}
|
||||
}
|
||||
if (!empty($declared_symbols[$libtype][$name])) {
|
||||
// We declare this symbol, so don't treat it as a requirement.
|
||||
continue 2;
|
||||
}
|
||||
}
|
||||
if (!empty($required_symbols[$type][$name])) {
|
||||
// Report only the first use of a symbol, since reporting all of them
|
||||
// isn't terribly informative.
|
||||
continue;
|
||||
}
|
||||
$required_symbols[$type][$name] = $spec['symbol']->getOffset();
|
||||
}
|
||||
|
||||
$result = array(
|
||||
'have' => $declared_symbols,
|
||||
'need' => $required_symbols,
|
||||
'xmap' => $xmap,
|
||||
);
|
||||
|
||||
|
||||
// -( Output )----------------------------------------------------------------
|
||||
|
||||
|
||||
if ($args->getArg('ugly')) {
|
||||
echo json_encode($result);
|
||||
} else {
|
||||
$json = new PhutilJSON();
|
||||
echo $json->encodeFormatted($result);
|
||||
}
|
||||
|
||||
|
||||
// -( Library )---------------------------------------------------------------
|
||||
|
||||
function phutil_fail_on_unsupported_feature(XHPASTNode $node, $file, $what) {
|
||||
$line = $node->getLineNumber();
|
||||
$message = phutil_console_wrap(
|
||||
pht(
|
||||
'`%s` has limited support for features introduced after PHP 5.2.3. '.
|
||||
'This library uses an unsupported feature (%s) on line %d of %s.',
|
||||
'arc liberate',
|
||||
$what,
|
||||
$line,
|
||||
Filesystem::readablePath($file)));
|
||||
|
||||
$result = array(
|
||||
'error' => $message,
|
||||
'line' => $line,
|
||||
'file' => $file,
|
||||
);
|
||||
$json = new PhutilJSON();
|
||||
echo $json->encodeFormatted($result);
|
||||
exit(0);
|
||||
}
|
||||
|
||||
function phutil_symbols_get_builtins() {
|
||||
$builtin = array();
|
||||
$builtin['classes'] = get_declared_classes();
|
||||
$builtin['interfaces'] = get_declared_interfaces();
|
||||
|
||||
$funcs = get_defined_functions();
|
||||
$builtin['functions'] = $funcs['internal'];
|
||||
|
||||
$compat = json_decode(
|
||||
file_get_contents(
|
||||
dirname(__FILE__).'/../resources/php_compat_info.json'),
|
||||
true);
|
||||
|
||||
foreach (array('functions', 'classes', 'interfaces') as $type) {
|
||||
// Developers may not have every extension that a library potentially uses
|
||||
// installed. We supplement the list of declared functions and classes with
|
||||
// a list of known extension functions to avoid raising false positives just
|
||||
// because you don't have pcntl, etc.
|
||||
$extensions = array_keys($compat[$type]);
|
||||
$builtin[$type] = array_merge($builtin[$type], $extensions);
|
||||
}
|
||||
|
||||
return array(
|
||||
'class' => array_fill_keys($builtin['classes'], true) + array(
|
||||
'static' => true,
|
||||
'parent' => true,
|
||||
'self' => true,
|
||||
|
||||
'PhutilBootloader' => true,
|
||||
|
||||
// PHP7 defines these new parent classes of "Exception", but they do not
|
||||
// exist prior to PHP7. It's possible to use them safely in PHP5, in
|
||||
// some cases, to write code which is compatible with either PHP5 or
|
||||
// PHP7, but it's hard for us tell if a particular use is safe or not.
|
||||
// For now, assume users know what they're doing and that uses are safe.
|
||||
// For discussion, see T12855.
|
||||
'Throwable' => true,
|
||||
'Error' => true,
|
||||
'ParseError' => true,
|
||||
|
||||
// PHP7 types.
|
||||
'bool' => true,
|
||||
'float' => true,
|
||||
'int' => true,
|
||||
'string' => true,
|
||||
'iterable' => true,
|
||||
'object' => true,
|
||||
'void' => true,
|
||||
),
|
||||
'function' => array_filter(
|
||||
array(
|
||||
'empty' => true,
|
||||
'isset' => true,
|
||||
'die' => true,
|
||||
|
||||
// These are provided by libphutil but not visible in the map.
|
||||
|
||||
'phutil_is_windows' => true,
|
||||
'phutil_load_library' => true,
|
||||
'phutil_is_hiphop_runtime' => true,
|
||||
|
||||
// HPHP/i defines these functions as 'internal', but they are NOT
|
||||
// builtins and do not exist in vanilla PHP. Make sure we don't mark
|
||||
// them as builtin since we need to add dependencies for them.
|
||||
'idx' => false,
|
||||
'id' => false,
|
||||
) + array_fill_keys($builtin['functions'], true)),
|
||||
'interface' => array_fill_keys($builtin['interfaces'], true),
|
||||
);
|
||||
}
|
9
scripts/test/deferred_log.php
Executable file
9
scripts/test/deferred_log.php
Executable file
|
@ -0,0 +1,9 @@
|
|||
#!/usr/bin/env php
|
||||
<?php
|
||||
|
||||
require_once dirname(__FILE__).'/../__init_script__.php';
|
||||
|
||||
$logs = array();
|
||||
for ($ii = 0; $ii < $argv[1]; $ii++) {
|
||||
$logs[] = new PhutilDeferredLog($argv[2], 'abcdefghijklmnopqrstuvwxyz');
|
||||
}
|
28
scripts/test/highlight.php
Executable file
28
scripts/test/highlight.php
Executable file
|
@ -0,0 +1,28 @@
|
|||
#!/usr/bin/env php
|
||||
<?php
|
||||
|
||||
require_once dirname(__FILE__).'/../__init_script__.php';
|
||||
|
||||
$args = new PhutilArgumentParser($argv);
|
||||
$args->setTagline(pht('test syntax highlighters'));
|
||||
$args->setSynopsis(<<<EOHELP
|
||||
**highlight.php** [__options__]
|
||||
Syntax highlight a corpus read from stdin.
|
||||
EOHELP
|
||||
);
|
||||
$args->parseStandardArguments();
|
||||
$args->parse(
|
||||
array(
|
||||
array(
|
||||
'name' => 'language',
|
||||
'param' => 'language',
|
||||
'help' => pht('Choose the highlight language.'),
|
||||
),
|
||||
));
|
||||
|
||||
$language = $args->getArg('language');
|
||||
$corpus = file_get_contents('php://stdin');
|
||||
|
||||
echo id(new PhutilDefaultSyntaxHighlighterEngine())
|
||||
->setConfig('pygments.enabled', true)
|
||||
->highlightSource($language, $corpus);
|
45
scripts/test/http.php
Executable file
45
scripts/test/http.php
Executable file
|
@ -0,0 +1,45 @@
|
|||
#!/usr/bin/env php
|
||||
<?php
|
||||
|
||||
require_once dirname(__FILE__).'/../__init_script__.php';
|
||||
|
||||
$args = new PhutilArgumentParser($argv);
|
||||
$args->parseStandardArguments();
|
||||
$args->parse(
|
||||
array(
|
||||
array(
|
||||
'name' => 'attach',
|
||||
'param' => 'file',
|
||||
'help' => pht('Attach a file to the request.'),
|
||||
),
|
||||
array(
|
||||
'name' => 'url',
|
||||
'wildcard' => true,
|
||||
),
|
||||
));
|
||||
|
||||
$uri = $args->getArg('url');
|
||||
if (count($uri) !== 1) {
|
||||
throw new PhutilArgumentUsageException(
|
||||
pht('Specify exactly one URL to retrieve.'));
|
||||
}
|
||||
$uri = head($uri);
|
||||
|
||||
$method = 'GET';
|
||||
$data = '';
|
||||
$timeout = 30;
|
||||
|
||||
$future = id(new HTTPSFuture($uri, $data))
|
||||
->setMethod($method)
|
||||
->setTimeout($timeout);
|
||||
|
||||
$attach_file = $args->getArg('attach');
|
||||
if ($attach_file !== null) {
|
||||
$future->attachFileData(
|
||||
'file',
|
||||
Filesystem::readFile($attach_file),
|
||||
basename($attach_file),
|
||||
Filesystem::getMimeType($attach_file));
|
||||
}
|
||||
|
||||
print_r($future->resolve());
|
59
scripts/test/interactive_editor.php
Executable file
59
scripts/test/interactive_editor.php
Executable file
|
@ -0,0 +1,59 @@
|
|||
#!/usr/bin/env php
|
||||
<?php
|
||||
|
||||
require_once dirname(__FILE__).'/../__init_script__.php';
|
||||
|
||||
$args = new PhutilArgumentParser($argv);
|
||||
$args->setTagline(pht('test %s class', 'InteractiveEditor'));
|
||||
$args->setSynopsis(<<<EOHELP
|
||||
**interactive_editor.php** [__options__]
|
||||
Edit some content via the InteractiveEditor class. This script
|
||||
makes it easier to test changes to InteractiveEditor, which is
|
||||
difficult to unit test.
|
||||
EOHELP
|
||||
);
|
||||
$args->parseStandardArguments();
|
||||
$args->parse(
|
||||
array(
|
||||
array(
|
||||
'name' => 'fallback',
|
||||
'param' => 'editor',
|
||||
'help' => pht('Set the fallback editor.'),
|
||||
),
|
||||
array(
|
||||
'name' => 'line',
|
||||
'short' => 'l',
|
||||
'param' => 'number',
|
||||
'help' => pht('Open at line number __number__.'),
|
||||
),
|
||||
array(
|
||||
'name' => 'name',
|
||||
'param' => 'filename',
|
||||
'help' => pht('Set edited file name.'),
|
||||
),
|
||||
));
|
||||
|
||||
if ($args->getArg('help')) {
|
||||
$args->printHelpAndExit();
|
||||
}
|
||||
|
||||
$editor = new PhutilInteractiveEditor(
|
||||
pht("The wizard quickly\njinxed the gnomes\nbefore they vaporized."));
|
||||
|
||||
$name = $args->getArg('name');
|
||||
if ($name) {
|
||||
$editor->setName($name);
|
||||
}
|
||||
|
||||
$line = $args->getArg('line');
|
||||
if ($line) {
|
||||
$editor->setLineOffset($line);
|
||||
}
|
||||
|
||||
$fallback = $args->getArg('fallback');
|
||||
if ($fallback) {
|
||||
$editor->setFallbackEditor($fallback);
|
||||
}
|
||||
|
||||
$result = $editor->editInteractively();
|
||||
echo pht('Edited Text:')."\n{$result}\n";
|
46
scripts/test/lipsum.php
Executable file
46
scripts/test/lipsum.php
Executable file
|
@ -0,0 +1,46 @@
|
|||
#!/usr/bin/env php
|
||||
<?php
|
||||
|
||||
require_once dirname(__FILE__).'/../__init_script__.php';
|
||||
|
||||
$args = new PhutilArgumentParser($argv);
|
||||
$args->setTagline(pht('test context-free grammars'));
|
||||
$args->setSynopsis(<<<EOHELP
|
||||
**lipsum.php** __class__
|
||||
Generate output from a named context-free grammar.
|
||||
EOHELP
|
||||
);
|
||||
$args->parseStandardArguments();
|
||||
$args->parse(
|
||||
array(
|
||||
array(
|
||||
'name' => 'class',
|
||||
'wildcard' => true,
|
||||
),
|
||||
));
|
||||
|
||||
$class = $args->getArg('class');
|
||||
if (count($class) !== 1) {
|
||||
$args->printHelpAndExit();
|
||||
}
|
||||
$class = reset($class);
|
||||
|
||||
$symbols = id(new PhutilClassMapQuery())
|
||||
->setAncestorClass('PhutilContextFreeGrammar')
|
||||
->execute();
|
||||
|
||||
$symbols = ipull($symbols, 'name', 'name');
|
||||
|
||||
if (empty($symbols[$class])) {
|
||||
$available = implode(', ', array_keys($symbols));
|
||||
throw new PhutilArgumentUsageException(
|
||||
pht(
|
||||
"Class '%s' is not a defined, concrete subclass of %s. ".
|
||||
"Available classes are: %s",
|
||||
$class,
|
||||
'PhutilContextFreeGrammar',
|
||||
$available));
|
||||
}
|
||||
|
||||
$object = newv($class, array());
|
||||
echo $object->generate()."\n";
|
40
scripts/test/mime.php
Executable file
40
scripts/test/mime.php
Executable file
|
@ -0,0 +1,40 @@
|
|||
#!/usr/bin/env php
|
||||
<?php
|
||||
|
||||
require_once dirname(__FILE__).'/../__init_script__.php';
|
||||
|
||||
$args = new PhutilArgumentParser($argv);
|
||||
$args->setTagline(pht('test %s', 'Filesystem::getMimeType()'));
|
||||
$args->setSynopsis(<<<EOHELP
|
||||
**mime.php** [__options__] __file__
|
||||
Determine the mime type of a file.
|
||||
EOHELP
|
||||
);
|
||||
$args->parseStandardArguments();
|
||||
$args->parse(
|
||||
array(
|
||||
array(
|
||||
'name' => 'default',
|
||||
'param' => 'mimetype',
|
||||
'help' => pht(
|
||||
'Use __mimetype__ as default instead of built-in default.'),
|
||||
),
|
||||
array(
|
||||
'name' => 'file',
|
||||
'wildcard' => true,
|
||||
),
|
||||
));
|
||||
|
||||
$file = $args->getArg('file');
|
||||
if (count($file) !== 1) {
|
||||
$args->printHelpAndExit();
|
||||
}
|
||||
|
||||
$file = reset($file);
|
||||
|
||||
$default = $args->getArg('default');
|
||||
if ($default) {
|
||||
echo Filesystem::getMimeType($file, $default)."\n";
|
||||
} else {
|
||||
echo Filesystem::getMimeType($file)."\n";
|
||||
}
|
24
scripts/test/paypal.php
Executable file
24
scripts/test/paypal.php
Executable file
|
@ -0,0 +1,24 @@
|
|||
#!/usr/bin/env php
|
||||
<?php
|
||||
|
||||
require_once dirname(__FILE__).'/../__init_script__.php';
|
||||
|
||||
// NOTE: These credentials are global test credentials provided by PayPal.
|
||||
|
||||
$future = id(new PhutilPayPalAPIFuture())
|
||||
->setHost('https://api-3t.sandbox.paypal.com/nvp')
|
||||
->setAPIUsername('sdk-three_api1.sdk.com')
|
||||
->setAPIPassword('QFZCWN5HZM8VBG7Q')
|
||||
->setAPISignature('A-IzJhZZjhg29XQ2qnhapuwxIDzyAZQ92FRP5dqBzVesOkzbdUONzmOU');
|
||||
|
||||
$future->setRawPayPalQuery(
|
||||
'SetExpressCheckout',
|
||||
array(
|
||||
'PAYMENTREQUEST_0_AMT' => '1.23',
|
||||
'PAYMENTREQUEST_0_CURRENCYCODE' => 'USD',
|
||||
'RETURNURL' => 'http://www.example.com/?return=1',
|
||||
'CANCELURL' => 'http://www.example.com/?cancel=1',
|
||||
'PAYMENTREQUEST_0_PAYMENTACTION' => 'Sale',
|
||||
));
|
||||
|
||||
print_r($future->resolve());
|
71
scripts/test/progress_bar.php
Executable file
71
scripts/test/progress_bar.php
Executable file
|
@ -0,0 +1,71 @@
|
|||
#!/usr/bin/env php
|
||||
<?php
|
||||
|
||||
require_once dirname(__FILE__).'/../__init_script__.php';
|
||||
|
||||
$args = new PhutilArgumentParser($argv);
|
||||
$args->parseStandardArguments();
|
||||
|
||||
echo pht(
|
||||
"PROGRESS BAR TEST SCRIPT\n\n".
|
||||
"This script is a test script for `%s`. It will draw some progress bars, ".
|
||||
"and generally allow you to test bar behaviors and changes.",
|
||||
'PhutilConsoleProgressBar');
|
||||
echo "\n\n";
|
||||
echo pht(
|
||||
"GENERAL NOTES\n\n".
|
||||
" - When run as `%s`, no progress bars should be shown ".
|
||||
"(stderr is not a tty).\n".
|
||||
" - When run in a narrow terminal, the bar should resize automatically ".
|
||||
"to fit the terminal.\n".
|
||||
" - When run with `%s`, the bar should not be drawn.\n",
|
||||
'php -f progress_bar.php 2>&1 | more',
|
||||
'--trace');
|
||||
echo "\n\n";
|
||||
|
||||
echo pht('STANDARD PROGRESS BAR')."\n";
|
||||
$n = 80;
|
||||
$bar = id(new PhutilConsoleProgressBar())
|
||||
->setTotal($n);
|
||||
for ($ii = 0; $ii < $n; $ii++) {
|
||||
$bar->update(1);
|
||||
usleep(10000);
|
||||
}
|
||||
$bar->done();
|
||||
|
||||
echo "\n".pht(
|
||||
"INTERRUPTED PROGRESS BAR\n".
|
||||
"This bar will be interrupted by an exception.\n".
|
||||
"It should clean itself up.")."\n";
|
||||
try {
|
||||
run_interrupt_bar();
|
||||
} catch (Exception $ex) {
|
||||
echo pht('Caught exception!')."\n";
|
||||
}
|
||||
|
||||
echo "\n".pht(
|
||||
"RESIZING BARS\n".
|
||||
"If you resize the window while a progress bars draws, it should more or ".
|
||||
"less detect the change.");
|
||||
|
||||
$n = 1024;
|
||||
$bar = id(new PhutilConsoleProgressBar())
|
||||
->setTotal($n);
|
||||
for ($ii = 0; $ii < $n; $ii++) {
|
||||
$bar->update(1);
|
||||
usleep(10000);
|
||||
}
|
||||
$bar->done();
|
||||
|
||||
function run_interrupt_bar() {
|
||||
$bar = id(new PhutilConsoleProgressBar())
|
||||
->setTotal(100);
|
||||
|
||||
for ($ii = 0; $ii < 100; $ii++) {
|
||||
if ($ii === 20) {
|
||||
throw new Exception(pht('Boo!'));
|
||||
}
|
||||
$bar->update(1);
|
||||
usleep(10000);
|
||||
}
|
||||
}
|
35
scripts/test/prompt.php
Executable file
35
scripts/test/prompt.php
Executable file
|
@ -0,0 +1,35 @@
|
|||
#!/usr/bin/env php
|
||||
<?php
|
||||
|
||||
require_once dirname(__FILE__).'/../__init_script__.php';
|
||||
|
||||
$args = new PhutilArgumentParser($argv);
|
||||
$args->setTagline(pht('test console prompting'));
|
||||
$args->setSynopsis(<<<EOHELP
|
||||
**prompt.php** __options__
|
||||
Test console prompting.
|
||||
EOHELP
|
||||
);
|
||||
$args->parseStandardArguments();
|
||||
$args->parse(
|
||||
array(
|
||||
array(
|
||||
'name' => 'history',
|
||||
'param' => 'file',
|
||||
'default' => '',
|
||||
'help' => pht('Use specified history __file__.'),
|
||||
),
|
||||
array(
|
||||
'name' => 'prompt',
|
||||
'param' => 'text',
|
||||
'default' => pht('Enter some text:'),
|
||||
'help' => pht('Change the prompt text to __text__.'),
|
||||
),
|
||||
));
|
||||
|
||||
$result = phutil_console_prompt(
|
||||
$args->getArg('prompt'),
|
||||
$args->getArg('history'));
|
||||
|
||||
$console = PhutilConsole::getConsole();
|
||||
$console->writeOut("%s\n", pht('Input is: %s', $result));
|
15
scripts/test/service_profiler.php
Executable file
15
scripts/test/service_profiler.php
Executable file
|
@ -0,0 +1,15 @@
|
|||
#!/usr/bin/env php
|
||||
<?php
|
||||
|
||||
require_once dirname(__FILE__).'/../__init_script__.php';
|
||||
|
||||
// Simple test script for PhutilServiceProfiler.
|
||||
|
||||
PhutilServiceProfiler::installEchoListener();
|
||||
|
||||
execx('ls %s', '/tmp');
|
||||
exec_manual('sleep %d', 1);
|
||||
phutil_passthru('cat');
|
||||
|
||||
echo "\n\n".pht('SERVICE CALL LOG')."\n";
|
||||
var_dump(PhutilServiceProfiler::getInstance()->getServiceCallLog());
|
46
scripts/timezones/generate_windows_timezone_map.php
Executable file
46
scripts/timezones/generate_windows_timezone_map.php
Executable file
|
@ -0,0 +1,46 @@
|
|||
#!/usr/bin/env php
|
||||
<?php
|
||||
|
||||
$root = dirname(dirname(dirname(__FILE__)));
|
||||
require_once $root.'/scripts/__init_script__.php';
|
||||
|
||||
$xml = $root.'/externals/cldr/cldr_windows_timezones.xml';
|
||||
$xml = Filesystem::readFile($xml);
|
||||
$xml = new SimpleXMLElement($xml);
|
||||
|
||||
$result_map = array();
|
||||
|
||||
$ignore = array(
|
||||
'UTC',
|
||||
'UTC-11',
|
||||
'UTC-02',
|
||||
'UTC-08',
|
||||
'UTC-09',
|
||||
'UTC+12',
|
||||
);
|
||||
$ignore = array_fuse($ignore);
|
||||
|
||||
$zones = $xml->windowsZones->mapTimezones->mapZone;
|
||||
foreach ($zones as $zone) {
|
||||
$windows_name = (string)$zone['other'];
|
||||
$target_name = (string)$zone['type'];
|
||||
|
||||
// Ignore the offset-based timezones from the CLDR map, since we handle
|
||||
// these later.
|
||||
if (isset($ignore[$windows_name])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// We've already seen this timezone so we don't need to add it to the map
|
||||
// again.
|
||||
if (isset($result_map[$windows_name])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$result_map[$windows_name] = $target_name;
|
||||
}
|
||||
|
||||
asort($result_map);
|
||||
|
||||
echo id(new PhutilJSON())
|
||||
->encodeFormatted($result_map);
|
140
scripts/update_compat_info.php
Executable file
140
scripts/update_compat_info.php
Executable file
|
@ -0,0 +1,140 @@
|
|||
#!/usr/bin/env php
|
||||
<?php
|
||||
|
||||
require_once dirname(__FILE__).'/__init_script__.php';
|
||||
|
||||
$target = 'resources/php_compat_info.json';
|
||||
echo phutil_console_format(
|
||||
"%s\n",
|
||||
pht(
|
||||
'Purpose: Updates %s used by %s.',
|
||||
$target,
|
||||
'ArcanistXHPASTLinter'));
|
||||
|
||||
// PHP CompatInfo is installed via Composer.
|
||||
//
|
||||
// You should symlink the Composer vendor directory to
|
||||
// libphutil/externals/includes/vendor`.
|
||||
require_once 'vendor/autoload.php';
|
||||
|
||||
$output = array();
|
||||
$output['@'.'generated'] = true;
|
||||
$output['params'] = array();
|
||||
$output['functions'] = array();
|
||||
$output['classes'] = array();
|
||||
$output['interfaces'] = array();
|
||||
$output['constants'] = array();
|
||||
|
||||
/**
|
||||
* Transform compatibility info into a slightly different format.
|
||||
*
|
||||
* The data returned by PHP CompatInfo is slightly odd in that null data is
|
||||
* represented by an empty string.
|
||||
*
|
||||
* @param map<string, string>
|
||||
* @return map<string, string | null>
|
||||
*/
|
||||
function parse_compat_info(array $compat) {
|
||||
return array(
|
||||
'ext.name' => $compat['ext.name'],
|
||||
'ext.min' => nonempty($compat['ext.min'], null),
|
||||
'ext.max' => nonempty($compat['ext.max'], null),
|
||||
'php.min' => nonempty($compat['php.min'], null),
|
||||
'php.max' => nonempty($compat['php.max'], null),
|
||||
);
|
||||
}
|
||||
|
||||
$client = new \Bartlett\Reflect\Client();
|
||||
$api = $client->api('reference');
|
||||
|
||||
foreach ($api->dir() as $extension) {
|
||||
$result = $api->show(
|
||||
$extension->name,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true);
|
||||
|
||||
foreach ($result['constants'] as $constant => $compat) {
|
||||
$output['constants'][$constant] = parse_compat_info($compat);
|
||||
}
|
||||
|
||||
foreach ($result['functions'] as $function => $compat) {
|
||||
$output['functions'][$function] = parse_compat_info($compat);
|
||||
|
||||
if (idx($compat, 'parameters')) {
|
||||
$output['params'][$function] = explode(', ', $compat['parameters']);
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($result['classes'] as $class => $compat) {
|
||||
$output['classes'][$class] = parse_compat_info($compat);
|
||||
}
|
||||
|
||||
foreach ($result['interfaces'] as $interface => $compat) {
|
||||
$output['interfaces'][$interface] = parse_compat_info($compat);
|
||||
}
|
||||
|
||||
foreach ($result['methods'] as $class => $methods) {
|
||||
$output['methods'][$class] = array();
|
||||
|
||||
foreach ($methods as $method => $compat) {
|
||||
$output['methods'][$class][$method] = parse_compat_info($compat);
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($result['static methods'] as $class => $methods) {
|
||||
$output['static_methods'][$class] = array();
|
||||
|
||||
foreach ($methods as $method => $compat) {
|
||||
$output['static_methods'][$class][$method] = parse_compat_info($compat);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ksort($output['params']);
|
||||
ksort($output['functions']);
|
||||
ksort($output['classes']);
|
||||
ksort($output['interfaces']);
|
||||
ksort($output['constants']);
|
||||
|
||||
// Grepped from PHP Manual.
|
||||
// TODO: Can we get this from PHP CompatInfo?
|
||||
// See https://github.com/llaville/php-compat-info/issues/185.
|
||||
$output['functions_windows'] = array(
|
||||
'apache_child_terminate' => false,
|
||||
'chroot' => false,
|
||||
'getrusage' => false,
|
||||
'imagecreatefromxpm' => false,
|
||||
'lchgrp' => false,
|
||||
'lchown' => false,
|
||||
'nl_langinfo' => false,
|
||||
'strptime' => false,
|
||||
'sys_getloadavg' => false,
|
||||
'checkdnsrr' => '5.3.0',
|
||||
'dns_get_record' => '5.3.0',
|
||||
'fnmatch' => '5.3.0',
|
||||
'getmxrr' => '5.3.0',
|
||||
'getopt' => '5.3.0',
|
||||
'imagecolorclosesthwb' => '5.3.0',
|
||||
'inet_ntop' => '5.3.0',
|
||||
'inet_pton' => '5.3.0',
|
||||
'link' => '5.3.0',
|
||||
'linkinfo' => '5.3.0',
|
||||
'readlink' => '5.3.0',
|
||||
'socket_create_pair' => '5.3.0',
|
||||
'stream_socket_pair' => '5.3.0',
|
||||
'symlink' => '5.3.0',
|
||||
'time_nanosleep' => '5.3.0',
|
||||
'time_sleep_until' => '5.3.0',
|
||||
);
|
||||
|
||||
Filesystem::writeFile(
|
||||
phutil_get_library_root('phutil').'/../'.$target,
|
||||
id(new PhutilJSON())->encodeFormatted($output));
|
||||
|
||||
echo pht('Done.')."\n";
|
22
scripts/utils/aws-s3.php
Executable file
22
scripts/utils/aws-s3.php
Executable file
|
@ -0,0 +1,22 @@
|
|||
#!/usr/bin/env php
|
||||
<?php
|
||||
|
||||
$root = dirname(dirname(dirname(__FILE__)));
|
||||
require_once $root.'/scripts/__init_script__.php';
|
||||
|
||||
$args = new PhutilArgumentParser($argv);
|
||||
$args->setTagline(pht('AWS CLI Client for S3'));
|
||||
$args->setSynopsis(<<<EOSYNOPSIS
|
||||
**aws-s3** __command__ [__options__]
|
||||
Upload and download data from Amazon Simple Storage Service (S3).
|
||||
|
||||
EOSYNOPSIS
|
||||
);
|
||||
$args->parseStandardArguments();
|
||||
|
||||
$workflows = id(new PhutilClassMapQuery())
|
||||
->setAncestorClass('PhutilAWSS3ManagementWorkflow')
|
||||
->execute();
|
||||
|
||||
$workflows[] = new PhutilHelpArgumentWorkflow();
|
||||
$args->parseWorkflows($workflows);
|
94
scripts/utils/directory_fixture.php
Executable file
94
scripts/utils/directory_fixture.php
Executable file
|
@ -0,0 +1,94 @@
|
|||
#!/usr/bin/env php
|
||||
<?php
|
||||
|
||||
require_once dirname(__FILE__).'/../__init_script__.php';
|
||||
|
||||
$args = new PhutilArgumentParser($argv);
|
||||
$args->setTagline(pht('edit directory fixtures'));
|
||||
$args->setSynopsis(<<<EOHELP
|
||||
**directory_fixture.php** __file__ --create
|
||||
Create a new directory fixture.
|
||||
|
||||
**directory_fixture.php** __file__
|
||||
Edit an existing directory fixture.
|
||||
|
||||
EOHELP
|
||||
);
|
||||
$args->parseStandardArguments();
|
||||
$args->parse(array(
|
||||
array(
|
||||
'name' => 'create',
|
||||
'help' => pht('Create a new fixture.'),
|
||||
),
|
||||
array(
|
||||
'name' => 'read-only',
|
||||
'help' => pht('Do not save changes made to the fixture.'),
|
||||
),
|
||||
array(
|
||||
'name' => 'files',
|
||||
'wildcard' => true,
|
||||
),
|
||||
));
|
||||
|
||||
$is_create = $args->getArg('create');
|
||||
$is_read_only = $args->getArg('read-only');
|
||||
$console = PhutilConsole::getConsole();
|
||||
|
||||
$files = $args->getArg('files');
|
||||
if (count($files) !== 1) {
|
||||
throw new PhutilArgumentUsageException(
|
||||
pht('Specify exactly one file to create or edit.'));
|
||||
}
|
||||
$file = head($files);
|
||||
|
||||
if ($is_create) {
|
||||
if (Filesystem::pathExists($file)) {
|
||||
throw new PhutilArgumentUsageException(
|
||||
pht(
|
||||
'File "%s" already exists, so you can not %s it.',
|
||||
$file,
|
||||
'--create'));
|
||||
}
|
||||
$fixture = PhutilDirectoryFixture::newEmptyFixture();
|
||||
} else {
|
||||
if (!Filesystem::pathExists($file)) {
|
||||
throw new PhutilArgumentUsageException(
|
||||
pht(
|
||||
'File "%s" does not exist! Use %s to create a new fixture.',
|
||||
$file,
|
||||
'--create'));
|
||||
}
|
||||
$fixture = PhutilDirectoryFixture::newFromArchive($file);
|
||||
}
|
||||
|
||||
$console->writeOut(
|
||||
"%s\n\n",
|
||||
pht('Spawning an interactive shell. Working directory is:'));
|
||||
$console->writeOut(
|
||||
" %s\n\n",
|
||||
$fixture->getPath());
|
||||
if ($is_read_only) {
|
||||
$console->writeOut(
|
||||
"%s\n",
|
||||
pht('Exit the shell when done (this fixture is read-only).'));
|
||||
} else {
|
||||
$console->writeOut(
|
||||
"%s\n",
|
||||
pht('Exit the shell after making changes.'));
|
||||
}
|
||||
|
||||
$err = phutil_passthru('cd %s && sh', $fixture->getPath());
|
||||
if ($err) {
|
||||
$console->writeOut(
|
||||
"%s\n",
|
||||
pht('Shell exited with error %d, discarding changes.', $err));
|
||||
exit($err);
|
||||
} else if ($is_read_only) {
|
||||
$console->writeOut(
|
||||
"%s\n",
|
||||
pht('Invoked in read-only mode, discarding changes.'));
|
||||
} else {
|
||||
$console->writeOut("%s\n", pht('Updating archive...'));
|
||||
$fixture->saveToArchive($file);
|
||||
$console->writeOut("%s\n", pht('Done.'));
|
||||
}
|
82
scripts/utils/lock.php
Executable file
82
scripts/utils/lock.php
Executable file
|
@ -0,0 +1,82 @@
|
|||
#!/usr/bin/env php
|
||||
<?php
|
||||
|
||||
require_once dirname(__FILE__).'/../__init_script__.php';
|
||||
|
||||
$args = new PhutilArgumentParser($argv);
|
||||
$args->setTagline(pht('acquire and hold a lockfile'));
|
||||
$args->setSynopsis(<<<EOHELP
|
||||
**lock.php** __file__ [__options__]
|
||||
Acquire a lockfile and hold it until told to unlock it.
|
||||
|
||||
EOHELP
|
||||
);
|
||||
|
||||
$args->parseStandardArguments();
|
||||
$args->parse(array(
|
||||
array(
|
||||
'name' => 'test',
|
||||
'help' => pht('Instead of holding the lock, release it and exit.'),
|
||||
),
|
||||
array(
|
||||
'name' => 'hold',
|
||||
'help' => pht('Hold indefinitely without prompting.'),
|
||||
),
|
||||
array(
|
||||
'name' => 'wait',
|
||||
'param' => 'n',
|
||||
'help' => pht('Block for up to __n__ seconds waiting for the lock.'),
|
||||
'default' => 0,
|
||||
),
|
||||
array(
|
||||
'name' => 'file',
|
||||
'wildcard' => true,
|
||||
),
|
||||
));
|
||||
|
||||
|
||||
$file = $args->getArg('file');
|
||||
if (count($file) !== 1) {
|
||||
$args->printHelpAndExit();
|
||||
}
|
||||
$file = head($file);
|
||||
|
||||
$console = PhutilConsole::getConsole();
|
||||
$console->writeOut(
|
||||
"%s\n",
|
||||
pht('This process has PID %d. Acquiring lock...', getmypid()));
|
||||
|
||||
$lock = PhutilFileLock::newForPath($file);
|
||||
|
||||
try {
|
||||
$lock->lock($args->getArg('wait'));
|
||||
} catch (PhutilFileLockException $ex) {
|
||||
$console->writeOut(
|
||||
"**%s** %s\n",
|
||||
pht('UNABLE TO ACQUIRE LOCK:'),
|
||||
pht('Lock is already held.'));
|
||||
exit(1);
|
||||
}
|
||||
|
||||
// NOTE: This string is magic, the unit tests look for it.
|
||||
$console->writeOut("%s\n", pht('LOCK ACQUIRED'));
|
||||
if ($args->getArg('test')) {
|
||||
$lock->unlock();
|
||||
exit(0);
|
||||
}
|
||||
|
||||
if ($args->getArg('hold')) {
|
||||
while (true) {
|
||||
sleep(1);
|
||||
}
|
||||
}
|
||||
|
||||
while (!$console->confirm(pht('Release lock?'))) {
|
||||
// Keep asking until they say yes.
|
||||
}
|
||||
|
||||
$console->writeOut("%s\n", pht('Unlocking...'));
|
||||
$lock->unlock();
|
||||
|
||||
$console->writeOut("%s\n", pht('Done.'));
|
||||
exit(0);
|
50
scripts/utils/prosediff.php
Executable file
50
scripts/utils/prosediff.php
Executable file
|
@ -0,0 +1,50 @@
|
|||
#!/usr/bin/env php
|
||||
<?php
|
||||
|
||||
require_once dirname(__FILE__).'/../__init_script__.php';
|
||||
|
||||
$args = new PhutilArgumentParser($argv);
|
||||
$args->setTagline(pht('show prose differences'));
|
||||
$args->setSynopsis(<<<EOHELP
|
||||
**prosediff.php** __old__ __new__ [__options__]
|
||||
Diff two prose files.
|
||||
|
||||
EOHELP
|
||||
);
|
||||
|
||||
$args->parseStandardArguments();
|
||||
$args->parse(
|
||||
array(
|
||||
array(
|
||||
'name' => 'files',
|
||||
'wildcard' => true,
|
||||
),
|
||||
));
|
||||
|
||||
$files = $args->getArg('files');
|
||||
if (count($files) !== 2) {
|
||||
$args->printHelpAndExit();
|
||||
}
|
||||
$old_file = head($files);
|
||||
$new_file = last($files);
|
||||
|
||||
$old_data = Filesystem::readFile($old_file);
|
||||
$new_data = Filesystem::readFile($new_file);
|
||||
|
||||
$engine = new PhutilProseDifferenceEngine();
|
||||
|
||||
$prose_diff = $engine->getDiff($old_data, $new_data);
|
||||
|
||||
foreach ($prose_diff->getParts() as $part) {
|
||||
switch ($part['type']) {
|
||||
case '-':
|
||||
echo tsprintf('<bg:red>%B</bg>', $part['text']);
|
||||
break;
|
||||
case '+':
|
||||
echo tsprintf('<bg:green>%B</bg>', $part['text']);
|
||||
break;
|
||||
case '=':
|
||||
echo tsprintf('%B', $part['text']);
|
||||
break;
|
||||
}
|
||||
}
|
170
scripts/utils/utf8.php
Executable file
170
scripts/utils/utf8.php
Executable file
|
@ -0,0 +1,170 @@
|
|||
#!/usr/bin/env php
|
||||
<?php
|
||||
|
||||
require_once dirname(dirname(__FILE__)).'/__init_script__.php';
|
||||
|
||||
$args = new PhutilArgumentParser($argv);
|
||||
$args->setTagline(pht('utf8 charset test script'));
|
||||
$args->setSynopsis(<<<EOHELP
|
||||
**utf8.php** [-C n] __file__ ...
|
||||
Show regions in files which are not valid UTF-8. With "-C n",
|
||||
show __n__ lines of context instead of the default of 3. Use
|
||||
"-" to read stdin.
|
||||
|
||||
**utf8.php** --test __file__ ...
|
||||
Test for files which are not valid UTF-8. For example, this
|
||||
will find all ".php" files under the working directory which
|
||||
aren't valid UTF-8:
|
||||
|
||||
find . -type f -name '*.php' | xargs -n256 ./utf8.php -t
|
||||
|
||||
If the script exits with no output, all input files were
|
||||
valid UTF-8.
|
||||
EOHELP
|
||||
);
|
||||
|
||||
$args->parseStandardArguments();
|
||||
$args->parse(array(
|
||||
array(
|
||||
'name' => 'context',
|
||||
'short' => 'C',
|
||||
'param' => 'lines',
|
||||
'default' => 3,
|
||||
'help' => pht(
|
||||
'Show __lines__ lines of context instead of the default 3.'),
|
||||
'conflicts' => array(
|
||||
'test' => pht('with %s, context is not shown.', '--test'),
|
||||
),
|
||||
),
|
||||
array(
|
||||
'name' => 'test',
|
||||
'short' => 't',
|
||||
'help' => pht('Print file names containing invalid UTF-8 to stdout.'),
|
||||
),
|
||||
array(
|
||||
'name' => 'files',
|
||||
'wildcard' => true,
|
||||
),
|
||||
));
|
||||
|
||||
|
||||
$is_test = $args->getArg('test');
|
||||
$context = $args->getArg('context');
|
||||
$files = $args->getArg('files');
|
||||
|
||||
if (empty($files)) {
|
||||
$args->printHelpAndExit();
|
||||
}
|
||||
|
||||
if ($is_test) {
|
||||
$err = test($files);
|
||||
} else {
|
||||
$err = show($files, $context);
|
||||
}
|
||||
exit($err);
|
||||
|
||||
|
||||
function read($file) {
|
||||
if ($file === '-') {
|
||||
return file_get_contents('php://stdin');
|
||||
} else {
|
||||
return Filesystem::readFile($file);
|
||||
}
|
||||
}
|
||||
|
||||
function name($file) {
|
||||
if ($file === '-') {
|
||||
return 'stdin';
|
||||
} else {
|
||||
return $file;
|
||||
}
|
||||
}
|
||||
|
||||
function test(array $files) {
|
||||
foreach ($files as $file) {
|
||||
$data = read($file);
|
||||
if (!phutil_is_utf8($data)) {
|
||||
echo name($file)."\n";
|
||||
}
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
function show(array $files, $context) {
|
||||
foreach ($files as $file) {
|
||||
$data = read($file);
|
||||
$ok = phutil_is_utf8($data);
|
||||
if ($ok) {
|
||||
echo pht('OKAY');
|
||||
} else {
|
||||
echo pht('FAIL');
|
||||
}
|
||||
echo ' '.name($file)."\n";
|
||||
|
||||
if (!$ok) {
|
||||
$lines = explode("\n", $data);
|
||||
$len = count($lines);
|
||||
$map = array();
|
||||
$bad = array();
|
||||
foreach ($lines as $n => $line) {
|
||||
if (phutil_is_utf8($line)) {
|
||||
continue;
|
||||
}
|
||||
$bad[$n] = true;
|
||||
for ($jj = max(0, $n - $context);
|
||||
$jj < min($len, $n + 1 + $context);
|
||||
$jj++) {
|
||||
$map[$jj] = true;
|
||||
}
|
||||
}
|
||||
|
||||
$width = strlen(max(array_keys($map)));
|
||||
|
||||
// Set $last such that we print a newline on the first iteration through
|
||||
// the loop.
|
||||
$last = -2;
|
||||
foreach ($map as $idx => $ignored) {
|
||||
if ($idx !== $last + 1) {
|
||||
echo "\n";
|
||||
}
|
||||
$last = $idx;
|
||||
|
||||
$line = $lines[$idx];
|
||||
if (!empty($bad[$idx])) {
|
||||
$line = show_problems($line);
|
||||
}
|
||||
|
||||
printf(" % {$width}d %s\n", $idx + 1, $line);
|
||||
}
|
||||
echo "\n";
|
||||
}
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
function show_problems($line) {
|
||||
$regex =
|
||||
"/^(".
|
||||
"[\x01-\x7F]+".
|
||||
"|([\xC2-\xDF][\x80-\xBF])".
|
||||
"|([\xE0-\xEF][\x80-\xBF][\x80-\xBF])".
|
||||
"|([\xF0-\xF4][\x80-\xBF][\x80-\xBF][\x80-\xBF]))/";
|
||||
|
||||
$out = '';
|
||||
while (strlen($line)) {
|
||||
$match = null;
|
||||
if (preg_match($regex, $line, $match)) {
|
||||
$out .= $match[1];
|
||||
$line = substr($line, strlen($match[1]));
|
||||
} else {
|
||||
$chr = sprintf('<0x%0X>', ord($line[0]));
|
||||
$chr = phutil_console_format('##%s##', $chr);
|
||||
$out .= $chr;
|
||||
$line = substr($line, 1);
|
||||
}
|
||||
}
|
||||
|
||||
return $out;
|
||||
}
|
|
@ -1,3 +1,70 @@
|
|||
<?php
|
||||
|
||||
define('__LIBPHUTIL__', true);
|
||||
|
||||
$root = dirname(__FILE__);
|
||||
require_once $root.'/moduleutils/core.php';
|
||||
require_once $root.'/moduleutils/PhutilBootloader.php';
|
||||
require_once $root.'/moduleutils/PhutilBootloaderException.php';
|
||||
require_once $root.'/moduleutils/PhutilLibraryConflictException.php';
|
||||
|
||||
function __phutil_autoload($class_name) {
|
||||
// Occurs in PHP 5.2 with `call_user_func(array($this, 'self::f'))`.
|
||||
if ($class_name === 'self' || $class_name === 'parent') {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
$loader = new PhutilSymbolLoader();
|
||||
$symbols = $loader
|
||||
->setType('class')
|
||||
->setName($class_name)
|
||||
->selectAndLoadSymbols();
|
||||
|
||||
if (!$symbols) {
|
||||
throw new PhutilMissingSymbolException(
|
||||
$class_name,
|
||||
pht('class or interface'),
|
||||
pht(
|
||||
"the class or interface '%s' is not defined in the library ".
|
||||
"map for any loaded %s library.",
|
||||
$class_name,
|
||||
'phutil'));
|
||||
}
|
||||
} catch (PhutilMissingSymbolException $ex) {
|
||||
$should_throw = true;
|
||||
|
||||
foreach (debug_backtrace() as $backtrace) {
|
||||
if (empty($backtrace['function'])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
switch ($backtrace['function']) {
|
||||
case 'class_exists':
|
||||
case 'interface_exists':
|
||||
case 'method_exists':
|
||||
case 'property_exists':
|
||||
case 'trait_exists':
|
||||
$should_throw = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!$should_throw) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// If there are other SPL autoloaders installed, we need to give them a
|
||||
// chance to load the class. Throw the exception if we're the last
|
||||
// autoloader; if not, swallow it and let them take a shot.
|
||||
$autoloaders = spl_autoload_functions();
|
||||
$last = end($autoloaders);
|
||||
if ($last == __FUNCTION__) {
|
||||
throw $ex;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
spl_autoload_register('__phutil_autoload', $throw = true);
|
||||
|
||||
phutil_register_library('arcanist', __FILE__);
|
||||
|
|
File diff suppressed because it is too large
Load diff
191
src/__tests__/PhutilLibraryTestCase.php
Normal file
191
src/__tests__/PhutilLibraryTestCase.php
Normal file
|
@ -0,0 +1,191 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* @concrete-extensible
|
||||
*/
|
||||
class PhutilLibraryTestCase extends PhutilTestCase {
|
||||
|
||||
/**
|
||||
* This is more of an acceptance test case instead of a unit test. It verifies
|
||||
* that all symbols can be loaded correctly. It can catch problems like
|
||||
* missing methods in descendants of abstract base classes.
|
||||
*/
|
||||
public function testEverythingImplemented() {
|
||||
id(new PhutilSymbolLoader())
|
||||
->setLibrary($this->getLibraryName())
|
||||
->selectAndLoadSymbols();
|
||||
$this->assertTrue(true);
|
||||
}
|
||||
|
||||
/**
|
||||
* This is more of an acceptance test case instead of a unit test. It verifies
|
||||
* that all the library map is up-to-date.
|
||||
*/
|
||||
public function testLibraryMap() {
|
||||
$root = $this->getLibraryRoot();
|
||||
$library = phutil_get_library_name_for_root($root);
|
||||
|
||||
$new_library_map = id(new PhutilLibraryMapBuilder($root))
|
||||
->buildMap();
|
||||
|
||||
$bootloader = PhutilBootloader::getInstance();
|
||||
$old_library_map = $bootloader->getLibraryMapWithoutExtensions($library);
|
||||
unset($old_library_map[PhutilLibraryMapBuilder::LIBRARY_MAP_VERSION_KEY]);
|
||||
|
||||
$identical = ($new_library_map === $old_library_map);
|
||||
if (!$identical) {
|
||||
$differences = $this->getMapDifferences(
|
||||
$old_library_map,
|
||||
$new_library_map);
|
||||
sort($differences);
|
||||
} else {
|
||||
$differences = array();
|
||||
}
|
||||
|
||||
$this->assertTrue(
|
||||
$identical,
|
||||
pht(
|
||||
"The library map is out of date. Rebuild it with `%s`.\n".
|
||||
"These entries differ: %s.",
|
||||
'arc liberate',
|
||||
implode(', ', $differences)));
|
||||
}
|
||||
|
||||
|
||||
private function getMapDifferences($old, $new) {
|
||||
$changed = array();
|
||||
|
||||
$all = $old + $new;
|
||||
foreach ($all as $key => $value) {
|
||||
$old_exists = array_key_exists($key, $old);
|
||||
$new_exists = array_key_exists($key, $new);
|
||||
|
||||
// One map has it and the other does not, so mark it as changed.
|
||||
if ($old_exists != $new_exists) {
|
||||
$changed[] = $key;
|
||||
continue;
|
||||
}
|
||||
|
||||
$oldv = idx($old, $key);
|
||||
$newv = idx($new, $key);
|
||||
if ($oldv === $newv) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (is_array($oldv) && is_array($newv)) {
|
||||
$child_changed = $this->getMapDifferences($oldv, $newv);
|
||||
foreach ($child_changed as $child) {
|
||||
$changed[] = $key.'.'.$child;
|
||||
}
|
||||
} else {
|
||||
$changed[] = $key;
|
||||
}
|
||||
}
|
||||
|
||||
return $changed;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* This is more of an acceptance test case instead of a unit test. It verifies
|
||||
* that methods in subclasses have the same visibility as the method in the
|
||||
* parent class.
|
||||
*/
|
||||
public function testMethodVisibility() {
|
||||
$symbols = id(new PhutilSymbolLoader())
|
||||
->setLibrary($this->getLibraryName())
|
||||
->selectSymbolsWithoutLoading();
|
||||
|
||||
$classes = array();
|
||||
foreach ($symbols as $symbol) {
|
||||
if ($symbol['type'] == 'class') {
|
||||
$classes[$symbol['name']] = new ReflectionClass($symbol['name']);
|
||||
}
|
||||
}
|
||||
|
||||
$failures = array();
|
||||
|
||||
foreach ($classes as $class_name => $class) {
|
||||
$parents = array();
|
||||
$parent = $class;
|
||||
while ($parent = $parent->getParentClass()) {
|
||||
$parents[] = $parent;
|
||||
}
|
||||
|
||||
$interfaces = $class->getInterfaces();
|
||||
|
||||
foreach ($class->getMethods() as $method) {
|
||||
$method_name = $method->getName();
|
||||
|
||||
foreach (array_merge($parents, $interfaces) as $extends) {
|
||||
if ($extends->hasMethod($method_name)) {
|
||||
$xmethod = $extends->getMethod($method_name);
|
||||
|
||||
if (!$this->compareVisibility($xmethod, $method)) {
|
||||
$failures[] = pht(
|
||||
'Class "%s" implements method "%s" with the wrong visibility. '.
|
||||
'The method has visibility "%s", but it is defined in parent '.
|
||||
'"%s" with visibility "%s". In Phabricator, a method which '.
|
||||
'overrides another must always have the same visibility.',
|
||||
$class_name,
|
||||
$method_name,
|
||||
$this->getVisibility($method),
|
||||
$extends->getName(),
|
||||
$this->getVisibility($xmethod));
|
||||
}
|
||||
|
||||
// We found a declaration somewhere, so stop looking.
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$this->assertTrue(
|
||||
empty($failures),
|
||||
"\n\n".implode("\n\n", $failures));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the name of the library currently being tested.
|
||||
*/
|
||||
protected function getLibraryName() {
|
||||
return phutil_get_library_name_for_root($this->getLibraryRoot());
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the root directory for the library currently being tested.
|
||||
*/
|
||||
protected function getLibraryRoot() {
|
||||
$caller = id(new ReflectionClass($this))->getFileName();
|
||||
return phutil_get_library_root_for_path($caller);
|
||||
}
|
||||
|
||||
private function compareVisibility(
|
||||
ReflectionMethod $parent_method,
|
||||
ReflectionMethod $method) {
|
||||
|
||||
static $bitmask;
|
||||
|
||||
if ($bitmask === null) {
|
||||
$bitmask = ReflectionMethod::IS_PUBLIC;
|
||||
$bitmask += ReflectionMethod::IS_PROTECTED;
|
||||
$bitmask += ReflectionMethod::IS_PRIVATE;
|
||||
}
|
||||
|
||||
$parent_modifiers = $parent_method->getModifiers();
|
||||
$modifiers = $method->getModifiers();
|
||||
return !(($parent_modifiers ^ $modifiers) & $bitmask);
|
||||
}
|
||||
|
||||
private function getVisibility(ReflectionMethod $method) {
|
||||
if ($method->isPrivate()) {
|
||||
return 'private';
|
||||
} else if ($method->isProtected()) {
|
||||
return 'protected';
|
||||
} else {
|
||||
return 'public';
|
||||
}
|
||||
}
|
||||
|
||||
}
|
150
src/aphront/headerparser/AphrontHTTPHeaderParser.php
Normal file
150
src/aphront/headerparser/AphrontHTTPHeaderParser.php
Normal file
|
@ -0,0 +1,150 @@
|
|||
<?php
|
||||
|
||||
final class AphrontHTTPHeaderParser extends Phobject {
|
||||
|
||||
private $name;
|
||||
private $content;
|
||||
private $pairs;
|
||||
|
||||
public function parseRawHeader($raw_header) {
|
||||
$this->name = null;
|
||||
$this->content = null;
|
||||
|
||||
$parts = explode(':', $raw_header, 2);
|
||||
$this->name = trim($parts[0]);
|
||||
if (count($parts) > 1) {
|
||||
$this->content = trim($parts[1]);
|
||||
}
|
||||
|
||||
$this->pairs = null;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getHeaderName() {
|
||||
$this->requireParse();
|
||||
return $this->name;
|
||||
}
|
||||
|
||||
public function getHeaderContent() {
|
||||
$this->requireParse();
|
||||
return $this->content;
|
||||
}
|
||||
|
||||
public function getHeaderContentAsPairs() {
|
||||
$content = $this->getHeaderContent();
|
||||
|
||||
|
||||
$state = 'prekey';
|
||||
$length = strlen($content);
|
||||
|
||||
$pair_name = null;
|
||||
$pair_value = null;
|
||||
|
||||
$pairs = array();
|
||||
$ii = 0;
|
||||
while ($ii < $length) {
|
||||
$c = $content[$ii];
|
||||
|
||||
switch ($state) {
|
||||
case 'prekey';
|
||||
// We're eating space in front of a key.
|
||||
if ($c == ' ') {
|
||||
$ii++;
|
||||
break;
|
||||
}
|
||||
$pair_name = '';
|
||||
$state = 'key';
|
||||
break;
|
||||
case 'key';
|
||||
// We're parsing a key name until we find "=" or ";".
|
||||
if ($c == ';') {
|
||||
$state = 'done';
|
||||
break;
|
||||
}
|
||||
|
||||
if ($c == '=') {
|
||||
$ii++;
|
||||
$state = 'value';
|
||||
break;
|
||||
}
|
||||
|
||||
$ii++;
|
||||
$pair_name .= $c;
|
||||
break;
|
||||
case 'value':
|
||||
// We found an "=", so now figure out if the value is quoted
|
||||
// or not.
|
||||
if ($c == '"') {
|
||||
$ii++;
|
||||
$state = 'quoted';
|
||||
break;
|
||||
}
|
||||
$state = 'unquoted';
|
||||
break;
|
||||
case 'quoted':
|
||||
// We're in a quoted string, parse until we find the closing quote.
|
||||
if ($c == '"') {
|
||||
$ii++;
|
||||
$state = 'done';
|
||||
break;
|
||||
}
|
||||
|
||||
$ii++;
|
||||
$pair_value .= $c;
|
||||
break;
|
||||
case 'unquoted':
|
||||
// We're in an unquoted string, parse until we find a space or a
|
||||
// semicolon.
|
||||
if ($c == ' ' || $c == ';') {
|
||||
$state = 'done';
|
||||
break;
|
||||
}
|
||||
$ii++;
|
||||
$pair_value .= $c;
|
||||
break;
|
||||
case 'done':
|
||||
// We parsed something, so eat any trailing whitespace and semicolons
|
||||
// and look for a new value.
|
||||
if ($c == ' ' || $c == ';') {
|
||||
$ii++;
|
||||
break;
|
||||
}
|
||||
|
||||
$pairs[] = array(
|
||||
$pair_name,
|
||||
$pair_value,
|
||||
);
|
||||
|
||||
$pair_name = null;
|
||||
$pair_value = null;
|
||||
|
||||
$state = 'prekey';
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if ($state == 'quoted') {
|
||||
throw new Exception(
|
||||
pht(
|
||||
'Header has unterminated double quote for key "%s".',
|
||||
$pair_name));
|
||||
}
|
||||
|
||||
if ($pair_name !== null) {
|
||||
$pairs[] = array(
|
||||
$pair_name,
|
||||
$pair_value,
|
||||
);
|
||||
}
|
||||
|
||||
return $pairs;
|
||||
}
|
||||
|
||||
private function requireParse() {
|
||||
if ($this->name === null) {
|
||||
throw new PhutilInvalidStateException('parseRawHeader');
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,108 @@
|
|||
<?php
|
||||
|
||||
final class AphrontHTTPHeaderParserTestCase extends PhutilTestCase {
|
||||
|
||||
public function testHeaderParser() {
|
||||
$cases = array(
|
||||
array(
|
||||
'Key: x; y; z',
|
||||
'Key',
|
||||
'x; y; z',
|
||||
array(
|
||||
array('x', null),
|
||||
array('y', null),
|
||||
array('z', null),
|
||||
),
|
||||
),
|
||||
array(
|
||||
'Content-Disposition: form-data; name="label"',
|
||||
'Content-Disposition',
|
||||
'form-data; name="label"',
|
||||
array(
|
||||
array('form-data', null),
|
||||
array('name', 'label'),
|
||||
),
|
||||
),
|
||||
array(
|
||||
'Content-Type: multipart/form-data; charset=utf-8',
|
||||
'Content-Type',
|
||||
'multipart/form-data; charset=utf-8',
|
||||
array(
|
||||
array('multipart/form-data', null),
|
||||
array('charset', 'utf-8'),
|
||||
),
|
||||
),
|
||||
array(
|
||||
'Content-Type: application/octet-stream; charset="ut',
|
||||
'Content-Type',
|
||||
'application/octet-stream; charset="ut',
|
||||
false,
|
||||
),
|
||||
array(
|
||||
'Content-Type: multipart/form-data; boundary=ABCDEFG',
|
||||
'Content-Type',
|
||||
'multipart/form-data; boundary=ABCDEFG',
|
||||
array(
|
||||
array('multipart/form-data', null),
|
||||
array('boundary', 'ABCDEFG'),
|
||||
),
|
||||
),
|
||||
array(
|
||||
'Content-Type: multipart/form-data; boundary="ABCDEFG"',
|
||||
'Content-Type',
|
||||
'multipart/form-data; boundary="ABCDEFG"',
|
||||
array(
|
||||
array('multipart/form-data', null),
|
||||
array('boundary', 'ABCDEFG'),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
foreach ($cases as $case) {
|
||||
$input = $case[0];
|
||||
$expect_name = $case[1];
|
||||
$expect_content = $case[2];
|
||||
|
||||
$parser = id(new AphrontHTTPHeaderParser())
|
||||
->parseRawHeader($input);
|
||||
|
||||
$actual_name = $parser->getHeaderName();
|
||||
$actual_content = $parser->getHeaderContent();
|
||||
|
||||
$this->assertEqual(
|
||||
$expect_name,
|
||||
$actual_name,
|
||||
pht('Header name for: %s', $input));
|
||||
|
||||
$this->assertEqual(
|
||||
$expect_content,
|
||||
$actual_content,
|
||||
pht('Header content for: %s', $input));
|
||||
|
||||
if (isset($case[3])) {
|
||||
$expect_pairs = $case[3];
|
||||
|
||||
$caught = null;
|
||||
try {
|
||||
$actual_pairs = $parser->getHeaderContentAsPairs();
|
||||
} catch (Exception $ex) {
|
||||
$caught = $ex;
|
||||
}
|
||||
|
||||
if ($expect_pairs === false) {
|
||||
$this->assertEqual(
|
||||
true,
|
||||
($caught instanceof Exception),
|
||||
pht('Expect exception for header pairs of: %s', $input));
|
||||
} else {
|
||||
$this->assertEqual(
|
||||
$expect_pairs,
|
||||
$actual_pairs,
|
||||
pht('Header pairs for: %s', $input));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
249
src/aphront/multipartparser/AphrontMultipartParser.php
Normal file
249
src/aphront/multipartparser/AphrontMultipartParser.php
Normal file
|
@ -0,0 +1,249 @@
|
|||
<?php
|
||||
|
||||
final class AphrontMultipartParser extends Phobject {
|
||||
|
||||
private $contentType;
|
||||
private $boundary;
|
||||
|
||||
private $buffer;
|
||||
private $body;
|
||||
private $state;
|
||||
|
||||
private $part;
|
||||
private $parts;
|
||||
|
||||
public function setContentType($content_type) {
|
||||
$this->contentType = $content_type;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getContentType() {
|
||||
return $this->contentType;
|
||||
}
|
||||
|
||||
public function beginParse() {
|
||||
$content_type = $this->getContentType();
|
||||
if ($content_type === null) {
|
||||
throw new PhutilInvalidStateException('setContentType');
|
||||
}
|
||||
|
||||
if (!preg_match('(^multipart/form-data)', $content_type)) {
|
||||
throw new Exception(
|
||||
pht(
|
||||
'Expected "multipart/form-data" content type when executing a '.
|
||||
'multipart body read.'));
|
||||
}
|
||||
|
||||
$type_parts = preg_split('(\s*;\s*)', $content_type);
|
||||
$boundary = null;
|
||||
foreach ($type_parts as $type_part) {
|
||||
$matches = null;
|
||||
if (preg_match('(^boundary=(.*))', $type_part, $matches)) {
|
||||
$boundary = $matches[1];
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if ($boundary === null) {
|
||||
throw new Exception(
|
||||
pht('Received "multipart/form-data" request with no "boundary".'));
|
||||
}
|
||||
|
||||
$this->parts = array();
|
||||
$this->part = null;
|
||||
|
||||
$this->buffer = '';
|
||||
$this->boundary = $boundary;
|
||||
|
||||
// We're looking for a (usually empty) body before the first boundary.
|
||||
$this->state = 'bodynewline';
|
||||
}
|
||||
|
||||
public function continueParse($bytes) {
|
||||
$this->buffer .= $bytes;
|
||||
|
||||
$continue = true;
|
||||
while ($continue) {
|
||||
switch ($this->state) {
|
||||
case 'endboundary':
|
||||
// We've just parsed a boundary. Next, we expect either "--" (which
|
||||
// indicates we've reached the end of the parts) or "\r\n" (which
|
||||
// indicates we should read the headers for the next part).
|
||||
|
||||
if (strlen($this->buffer) < 2) {
|
||||
// We don't have enough bytes yet, so wait for more.
|
||||
$continue = false;
|
||||
break;
|
||||
}
|
||||
|
||||
if (!strncmp($this->buffer, '--', 2)) {
|
||||
// This is "--" after a boundary, so we're done. We'll read the
|
||||
// rest of the body (the "epilogue") and discard it.
|
||||
$this->buffer = substr($this->buffer, 2);
|
||||
$this->state = 'epilogue';
|
||||
|
||||
$this->part = null;
|
||||
break;
|
||||
}
|
||||
|
||||
if (!strncmp($this->buffer, "\r\n", 2)) {
|
||||
// This is "\r\n" after a boundary, so we're going to going to
|
||||
// read the headers for a part.
|
||||
$this->buffer = substr($this->buffer, 2);
|
||||
$this->state = 'header';
|
||||
|
||||
// Create the object to hold the part we're about to read.
|
||||
$part = new AphrontMultipartPart();
|
||||
$this->parts[] = $part;
|
||||
$this->part = $part;
|
||||
break;
|
||||
}
|
||||
|
||||
throw new Exception(
|
||||
pht('Expected "\r\n" or "--" after multipart data boundary.'));
|
||||
case 'header':
|
||||
// We've just parsed a boundary, followed by "\r\n". We are going
|
||||
// to read the headers for this part. They are in the form of HTTP
|
||||
// headers and terminated by "\r\n". The section is terminated by
|
||||
// a line with no header on it.
|
||||
|
||||
if (strlen($this->buffer) < 2) {
|
||||
// We don't have enough data to find a "\r\n", so wait for more.
|
||||
$continue = false;
|
||||
break;
|
||||
}
|
||||
|
||||
if (!strncmp("\r\n", $this->buffer, 2)) {
|
||||
// This line immediately began "\r\n", so we're done with parsing
|
||||
// headers. Start parsing the body.
|
||||
$this->buffer = substr($this->buffer, 2);
|
||||
$this->state = 'body';
|
||||
break;
|
||||
}
|
||||
|
||||
// This is an actual header, so look for the end of it.
|
||||
$header_len = strpos($this->buffer, "\r\n");
|
||||
if ($header_len === false) {
|
||||
// We don't have a full header yet, so wait for more data.
|
||||
$continue = false;
|
||||
break;
|
||||
}
|
||||
|
||||
$header_buf = substr($this->buffer, 0, $header_len);
|
||||
$this->part->appendRawHeader($header_buf);
|
||||
|
||||
$this->buffer = substr($this->buffer, $header_len + 2);
|
||||
break;
|
||||
case 'body':
|
||||
// We've parsed a boundary and headers, and are parsing the data for
|
||||
// this part. The data is terminated by "\r\n--", then the boundary.
|
||||
|
||||
// We'll look for "\r\n", then switch to the "bodynewline" state if
|
||||
// we find it.
|
||||
|
||||
$marker = "\r";
|
||||
$marker_pos = strpos($this->buffer, $marker);
|
||||
|
||||
if ($marker_pos === false) {
|
||||
// There's no "\r" anywhere in the buffer, so we can just read it
|
||||
// as provided. Then, since we read all the data, we're done until
|
||||
// we get more.
|
||||
|
||||
// Note that if we're in the preamble, we won't have a "part"
|
||||
// object and will just discard the data.
|
||||
if ($this->part) {
|
||||
$this->part->appendData($this->buffer);
|
||||
}
|
||||
$this->buffer = '';
|
||||
$continue = false;
|
||||
break;
|
||||
}
|
||||
|
||||
if ($marker_pos > 0) {
|
||||
// If there are bytes before the "\r",
|
||||
if ($this->part) {
|
||||
$this->part->appendData(substr($this->buffer, 0, $marker_pos));
|
||||
}
|
||||
$this->buffer = substr($this->buffer, $marker_pos);
|
||||
}
|
||||
|
||||
$expect = "\r\n";
|
||||
$expect_len = strlen($expect);
|
||||
if (strlen($this->buffer) < $expect_len) {
|
||||
// We don't have enough bytes yet to know if this is "\r\n"
|
||||
// or not.
|
||||
$continue = false;
|
||||
break;
|
||||
}
|
||||
|
||||
if (strncmp($this->buffer, $expect, $expect_len)) {
|
||||
// The next two bytes aren't "\r\n", so eat them and go looking
|
||||
// for more newlines.
|
||||
if ($this->part) {
|
||||
$this->part->appendData(substr($this->buffer, 0, $expect_len));
|
||||
}
|
||||
$this->buffer = substr($this->buffer, $expect_len);
|
||||
break;
|
||||
}
|
||||
|
||||
// Eat the "\r\n".
|
||||
$this->buffer = substr($this->buffer, $expect_len);
|
||||
$this->state = 'bodynewline';
|
||||
break;
|
||||
case 'bodynewline':
|
||||
// We've parsed a newline in a body, or we just started parsing the
|
||||
// request. In either case, we're looking for "--", then the boundary.
|
||||
// If we find it, this section is done. If we don't, we consume the
|
||||
// bytes and move on.
|
||||
|
||||
$expect = '--'.$this->boundary;
|
||||
$expect_len = strlen($expect);
|
||||
|
||||
if (strlen($this->buffer) < $expect_len) {
|
||||
// We don't have enough bytes yet, so wait for more.
|
||||
$continue = false;
|
||||
break;
|
||||
}
|
||||
|
||||
if (strncmp($this->buffer, $expect, $expect_len)) {
|
||||
// This wasn't the boundary, so return to the "body" state and
|
||||
// consume it. (But first, we need to append the "\r\n" which we
|
||||
// ate earlier.)
|
||||
if ($this->part) {
|
||||
$this->part->appendData("\r\n");
|
||||
}
|
||||
$this->state = 'body';
|
||||
break;
|
||||
}
|
||||
|
||||
// This is the boundary, so toss it and move on.
|
||||
$this->buffer = substr($this->buffer, $expect_len);
|
||||
$this->state = 'endboundary';
|
||||
break;
|
||||
case 'epilogue':
|
||||
// We just discard any epilogue.
|
||||
$this->buffer = '';
|
||||
$continue = false;
|
||||
break;
|
||||
default:
|
||||
throw new Exception(
|
||||
pht(
|
||||
'Unknown parser state "%s".\n',
|
||||
$this->state));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function endParse() {
|
||||
if ($this->state !== 'epilogue') {
|
||||
throw new Exception(
|
||||
pht(
|
||||
'Expected "multipart/form-data" parse to end '.
|
||||
'in state "epilogue".'));
|
||||
}
|
||||
|
||||
return $this->parts;
|
||||
}
|
||||
|
||||
|
||||
}
|
96
src/aphront/multipartparser/AphrontMultipartPart.php
Normal file
96
src/aphront/multipartparser/AphrontMultipartPart.php
Normal file
|
@ -0,0 +1,96 @@
|
|||
<?php
|
||||
|
||||
final class AphrontMultipartPart extends Phobject {
|
||||
|
||||
private $headers = array();
|
||||
private $value = '';
|
||||
|
||||
private $name;
|
||||
private $filename;
|
||||
private $tempFile;
|
||||
private $byteSize = 0;
|
||||
|
||||
public function appendRawHeader($bytes) {
|
||||
$parser = id(new AphrontHTTPHeaderParser())
|
||||
->parseRawHeader($bytes);
|
||||
|
||||
$header_name = $parser->getHeaderName();
|
||||
|
||||
$this->headers[] = array(
|
||||
$header_name,
|
||||
$parser->getHeaderContent(),
|
||||
);
|
||||
|
||||
if (strtolower($header_name) === 'content-disposition') {
|
||||
$pairs = $parser->getHeaderContentAsPairs();
|
||||
foreach ($pairs as $pair) {
|
||||
list($key, $value) = $pair;
|
||||
switch ($key) {
|
||||
case 'filename':
|
||||
$this->filename = $value;
|
||||
break;
|
||||
case 'name':
|
||||
$this->name = $value;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function appendData($bytes) {
|
||||
$this->byteSize += strlen($bytes);
|
||||
|
||||
if ($this->isVariable()) {
|
||||
$this->value .= $bytes;
|
||||
} else {
|
||||
if (!$this->tempFile) {
|
||||
$this->tempFile = new TempFile(getmypid().'.upload');
|
||||
}
|
||||
Filesystem::appendFile($this->tempFile, $bytes);
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function isVariable() {
|
||||
return ($this->filename === null);
|
||||
}
|
||||
|
||||
public function getName() {
|
||||
return $this->name;
|
||||
}
|
||||
|
||||
public function getVariableValue() {
|
||||
if (!$this->isVariable()) {
|
||||
throw new Exception(pht('This part is not a variable!'));
|
||||
}
|
||||
|
||||
return $this->value;
|
||||
}
|
||||
|
||||
public function getPHPFileDictionary() {
|
||||
if (!$this->tempFile) {
|
||||
$this->appendData('');
|
||||
}
|
||||
|
||||
$mime_type = 'application/octet-stream';
|
||||
foreach ($this->headers as $header) {
|
||||
list($name, $value) = $header;
|
||||
if (strtolower($name) == 'content-type') {
|
||||
$mime_type = $value;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return array(
|
||||
'name' => $this->filename,
|
||||
'type' => $mime_type,
|
||||
'tmp_name' => (string)$this->tempFile,
|
||||
'error' => 0,
|
||||
'size' => $this->byteSize,
|
||||
);
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,45 @@
|
|||
<?php
|
||||
|
||||
final class AphrontMultipartParserTestCase extends PhutilTestCase {
|
||||
|
||||
public function testParser() {
|
||||
$map = array(
|
||||
array(
|
||||
'data' => 'simple.txt',
|
||||
'variables' => array(
|
||||
array('a', 'b'),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
$data_dir = dirname(__FILE__).'/data/';
|
||||
foreach ($map as $test_case) {
|
||||
$data = Filesystem::readFile($data_dir.$test_case['data']);
|
||||
$data = str_replace("\n", "\r\n", $data);
|
||||
|
||||
$parser = id(new AphrontMultipartParser())
|
||||
->setContentType('multipart/form-data; boundary=ABCDEFG');
|
||||
$parser->beginParse();
|
||||
$parser->continueParse($data);
|
||||
$parts = $parser->endParse();
|
||||
|
||||
$variables = array();
|
||||
foreach ($parts as $part) {
|
||||
if (!$part->isVariable()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$variables[] = array(
|
||||
$part->getName(),
|
||||
$part->getVariableValue(),
|
||||
);
|
||||
}
|
||||
|
||||
$expect_variables = idx($test_case, 'variables', array());
|
||||
$this->assertEqual($expect_variables, $variables);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
5
src/aphront/multipartparser/__tests__/data/simple.txt
Normal file
5
src/aphront/multipartparser/__tests__/data/simple.txt
Normal file
|
@ -0,0 +1,5 @@
|
|||
--ABCDEFG
|
||||
Content-Disposition: form-data; name="a"
|
||||
|
||||
b
|
||||
--ABCDEFG--
|
92
src/aphront/requeststream/AphrontRequestStream.php
Normal file
92
src/aphront/requeststream/AphrontRequestStream.php
Normal file
|
@ -0,0 +1,92 @@
|
|||
<?php
|
||||
|
||||
final class AphrontRequestStream extends Phobject {
|
||||
|
||||
private $encoding;
|
||||
private $stream;
|
||||
private $closed;
|
||||
private $iterator;
|
||||
|
||||
public function setEncoding($encoding) {
|
||||
$this->encoding = $encoding;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getEncoding() {
|
||||
return $this->encoding;
|
||||
}
|
||||
|
||||
public function getIterator() {
|
||||
if (!$this->iterator) {
|
||||
$this->iterator = new PhutilStreamIterator($this->getStream());
|
||||
}
|
||||
return $this->iterator;
|
||||
}
|
||||
|
||||
public function readData() {
|
||||
if (!$this->iterator) {
|
||||
$iterator = $this->getIterator();
|
||||
$iterator->rewind();
|
||||
} else {
|
||||
$iterator = $this->getIterator();
|
||||
}
|
||||
|
||||
if (!$iterator->valid()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$data = $iterator->current();
|
||||
$iterator->next();
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
private function getStream() {
|
||||
if (!$this->stream) {
|
||||
$this->stream = $this->newStream();
|
||||
}
|
||||
|
||||
return $this->stream;
|
||||
}
|
||||
|
||||
private function newStream() {
|
||||
$stream = fopen('php://input', 'rb');
|
||||
if (!$stream) {
|
||||
throw new Exception(
|
||||
pht(
|
||||
'Failed to open stream "%s" for reading.',
|
||||
'php://input'));
|
||||
}
|
||||
|
||||
$encoding = $this->getEncoding();
|
||||
if ($encoding === 'gzip') {
|
||||
// This parameter is magic. Values 0-15 express a time/memory tradeoff,
|
||||
// but the largest value (15) corresponds to only 32KB of memory and
|
||||
// data encoded with a smaller window size than the one we pass can not
|
||||
// be decompressed. Always pass the maximum window size.
|
||||
|
||||
// Additionally, you can add 16 (to enable gzip) or 32 (to enable both
|
||||
// gzip and zlib). Add 32 to support both.
|
||||
$zlib_window = 15 + 32;
|
||||
|
||||
$ok = stream_filter_append(
|
||||
$stream,
|
||||
'zlib.inflate',
|
||||
STREAM_FILTER_READ,
|
||||
array(
|
||||
'window' => $zlib_window,
|
||||
));
|
||||
if (!$ok) {
|
||||
throw new Exception(
|
||||
pht(
|
||||
'Failed to append filter "%s" to input stream while processing '.
|
||||
'a request with "%s" encoding.',
|
||||
'zlib.inflate',
|
||||
$encoding));
|
||||
}
|
||||
}
|
||||
|
||||
return $stream;
|
||||
}
|
||||
|
||||
}
|
284
src/aphront/storage/connection/AphrontDatabaseConnection.php
Normal file
284
src/aphront/storage/connection/AphrontDatabaseConnection.php
Normal file
|
@ -0,0 +1,284 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* @task xaction Transaction Management
|
||||
*/
|
||||
abstract class AphrontDatabaseConnection
|
||||
extends Phobject
|
||||
implements PhutilQsprintfInterface {
|
||||
|
||||
private $transactionState;
|
||||
private $readOnly;
|
||||
private $queryTimeout;
|
||||
private $locks = array();
|
||||
private $lastActiveEpoch;
|
||||
private $persistent;
|
||||
|
||||
abstract public function getInsertID();
|
||||
abstract public function getAffectedRows();
|
||||
abstract public function selectAllResults();
|
||||
abstract public function executeRawQuery($raw_query);
|
||||
abstract public function executeRawQueries(array $raw_queries);
|
||||
abstract public function close();
|
||||
abstract public function openConnection();
|
||||
|
||||
public function __destruct() {
|
||||
// NOTE: This does not actually close persistent connections: PHP maintains
|
||||
// them in the connection pool.
|
||||
$this->close();
|
||||
}
|
||||
|
||||
final public function setLastActiveEpoch($epoch) {
|
||||
$this->lastActiveEpoch = $epoch;
|
||||
return $this;
|
||||
}
|
||||
|
||||
final public function getLastActiveEpoch() {
|
||||
return $this->lastActiveEpoch;
|
||||
}
|
||||
|
||||
final public function setPersistent($persistent) {
|
||||
$this->persistent = $persistent;
|
||||
return $this;
|
||||
}
|
||||
|
||||
final public function getPersistent() {
|
||||
return $this->persistent;
|
||||
}
|
||||
|
||||
public function queryData($pattern/* , $arg, $arg, ... */) {
|
||||
$args = func_get_args();
|
||||
array_unshift($args, $this);
|
||||
return call_user_func_array('queryfx_all', $args);
|
||||
}
|
||||
|
||||
public function query($pattern/* , $arg, $arg, ... */) {
|
||||
$args = func_get_args();
|
||||
array_unshift($args, $this);
|
||||
return call_user_func_array('queryfx', $args);
|
||||
}
|
||||
|
||||
|
||||
public function supportsAsyncQueries() {
|
||||
return false;
|
||||
}
|
||||
|
||||
public function supportsParallelQueries() {
|
||||
return false;
|
||||
}
|
||||
|
||||
public function setReadOnly($read_only) {
|
||||
$this->readOnly = $read_only;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getReadOnly() {
|
||||
return $this->readOnly;
|
||||
}
|
||||
|
||||
public function setQueryTimeout($query_timeout) {
|
||||
$this->queryTimeout = $query_timeout;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getQueryTimeout() {
|
||||
return $this->queryTimeout;
|
||||
}
|
||||
|
||||
public function asyncQuery($raw_query) {
|
||||
throw new Exception(pht('Async queries are not supported.'));
|
||||
}
|
||||
|
||||
public static function resolveAsyncQueries(array $conns, array $asyncs) {
|
||||
throw new Exception(pht('Async queries are not supported.'));
|
||||
}
|
||||
|
||||
|
||||
/* -( Global Locks )------------------------------------------------------- */
|
||||
|
||||
|
||||
public function rememberLock($lock) {
|
||||
if (isset($this->locks[$lock])) {
|
||||
throw new Exception(
|
||||
pht(
|
||||
'Trying to remember lock "%s", but this lock has already been '.
|
||||
'remembered.',
|
||||
$lock));
|
||||
}
|
||||
|
||||
$this->locks[$lock] = true;
|
||||
return $this;
|
||||
}
|
||||
|
||||
|
||||
public function forgetLock($lock) {
|
||||
if (empty($this->locks[$lock])) {
|
||||
throw new Exception(
|
||||
pht(
|
||||
'Trying to forget lock "%s", but this connection does not remember '.
|
||||
'that lock.',
|
||||
$lock));
|
||||
}
|
||||
|
||||
unset($this->locks[$lock]);
|
||||
return $this;
|
||||
}
|
||||
|
||||
|
||||
public function forgetAllLocks() {
|
||||
$this->locks = array();
|
||||
return $this;
|
||||
}
|
||||
|
||||
|
||||
public function isHoldingAnyLock() {
|
||||
return (bool)$this->locks;
|
||||
}
|
||||
|
||||
|
||||
/* -( Transaction Management )--------------------------------------------- */
|
||||
|
||||
|
||||
/**
|
||||
* Begin a transaction, or set a savepoint if the connection is already
|
||||
* transactional.
|
||||
*
|
||||
* @return this
|
||||
* @task xaction
|
||||
*/
|
||||
public function openTransaction() {
|
||||
$state = $this->getTransactionState();
|
||||
$point = $state->getSavepointName();
|
||||
$depth = $state->getDepth();
|
||||
|
||||
$new_transaction = ($depth == 0);
|
||||
if ($new_transaction) {
|
||||
$this->query('START TRANSACTION');
|
||||
} else {
|
||||
$this->query('SAVEPOINT '.$point);
|
||||
}
|
||||
|
||||
$state->increaseDepth();
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Commit a transaction, or stage a savepoint for commit once the entire
|
||||
* transaction completes if inside a transaction stack.
|
||||
*
|
||||
* @return this
|
||||
* @task xaction
|
||||
*/
|
||||
public function saveTransaction() {
|
||||
$state = $this->getTransactionState();
|
||||
$depth = $state->decreaseDepth();
|
||||
|
||||
if ($depth == 0) {
|
||||
$this->query('COMMIT');
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Rollback a transaction, or unstage the last savepoint if inside a
|
||||
* transaction stack.
|
||||
*
|
||||
* @return this
|
||||
*/
|
||||
public function killTransaction() {
|
||||
$state = $this->getTransactionState();
|
||||
$depth = $state->decreaseDepth();
|
||||
|
||||
if ($depth == 0) {
|
||||
$this->query('ROLLBACK');
|
||||
} else {
|
||||
$this->query('ROLLBACK TO SAVEPOINT '.$state->getSavepointName());
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Returns true if the connection is transactional.
|
||||
*
|
||||
* @return bool True if the connection is currently transactional.
|
||||
* @task xaction
|
||||
*/
|
||||
public function isInsideTransaction() {
|
||||
$state = $this->getTransactionState();
|
||||
return ($state->getDepth() > 0);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Get the current @{class:AphrontDatabaseTransactionState} object, or create
|
||||
* one if none exists.
|
||||
*
|
||||
* @return AphrontDatabaseTransactionState Current transaction state.
|
||||
* @task xaction
|
||||
*/
|
||||
protected function getTransactionState() {
|
||||
if (!$this->transactionState) {
|
||||
$this->transactionState = new AphrontDatabaseTransactionState();
|
||||
}
|
||||
return $this->transactionState;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @task xaction
|
||||
*/
|
||||
public function beginReadLocking() {
|
||||
$this->getTransactionState()->beginReadLocking();
|
||||
return $this;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @task xaction
|
||||
*/
|
||||
public function endReadLocking() {
|
||||
$this->getTransactionState()->endReadLocking();
|
||||
return $this;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @task xaction
|
||||
*/
|
||||
public function isReadLocking() {
|
||||
return $this->getTransactionState()->isReadLocking();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @task xaction
|
||||
*/
|
||||
public function beginWriteLocking() {
|
||||
$this->getTransactionState()->beginWriteLocking();
|
||||
return $this;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @task xaction
|
||||
*/
|
||||
public function endWriteLocking() {
|
||||
$this->getTransactionState()->endWriteLocking();
|
||||
return $this;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @task xaction
|
||||
*/
|
||||
public function isWriteLocking() {
|
||||
return $this->getTransactionState()->isWriteLocking();
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,105 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* Represents current transaction state of a connection.
|
||||
*/
|
||||
final class AphrontDatabaseTransactionState extends Phobject {
|
||||
|
||||
private $depth = 0;
|
||||
private $readLockLevel = 0;
|
||||
private $writeLockLevel = 0;
|
||||
|
||||
public function getDepth() {
|
||||
return $this->depth;
|
||||
}
|
||||
|
||||
public function increaseDepth() {
|
||||
return ++$this->depth;
|
||||
}
|
||||
|
||||
public function decreaseDepth() {
|
||||
if ($this->depth == 0) {
|
||||
throw new Exception(
|
||||
pht(
|
||||
'Too many calls to %s or %s!',
|
||||
'saveTransaction()',
|
||||
'killTransaction()'));
|
||||
}
|
||||
|
||||
return --$this->depth;
|
||||
}
|
||||
|
||||
public function getSavepointName() {
|
||||
return 'Aphront_Savepoint_'.$this->depth;
|
||||
}
|
||||
|
||||
public function beginReadLocking() {
|
||||
$this->readLockLevel++;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function endReadLocking() {
|
||||
if ($this->readLockLevel == 0) {
|
||||
throw new Exception(
|
||||
pht(
|
||||
'Too many calls to %s!',
|
||||
__FUNCTION__.'()'));
|
||||
}
|
||||
$this->readLockLevel--;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function isReadLocking() {
|
||||
return ($this->readLockLevel > 0);
|
||||
}
|
||||
|
||||
public function beginWriteLocking() {
|
||||
$this->writeLockLevel++;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function endWriteLocking() {
|
||||
if ($this->writeLockLevel == 0) {
|
||||
throw new Exception(
|
||||
pht(
|
||||
'Too many calls to %s!',
|
||||
__FUNCTION__.'()'));
|
||||
}
|
||||
$this->writeLockLevel--;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function isWriteLocking() {
|
||||
return ($this->writeLockLevel > 0);
|
||||
}
|
||||
|
||||
public function __destruct() {
|
||||
if ($this->depth) {
|
||||
throw new Exception(
|
||||
pht(
|
||||
'Process exited with an open transaction! The transaction '.
|
||||
'will be implicitly rolled back. Calls to %s must always be '.
|
||||
'paired with a call to %s or %s.',
|
||||
'openTransaction()',
|
||||
'saveTransaction()',
|
||||
'killTransaction()'));
|
||||
}
|
||||
if ($this->readLockLevel) {
|
||||
throw new Exception(
|
||||
pht(
|
||||
'Process exited with an open read lock! Call to %s '.
|
||||
'must always be paired with a call to %s.',
|
||||
'beginReadLocking()',
|
||||
'endReadLocking()'));
|
||||
}
|
||||
if ($this->writeLockLevel) {
|
||||
throw new Exception(
|
||||
pht(
|
||||
'Process exited with an open write lock! Call to %s '.
|
||||
'must always be paired with a call to %s.',
|
||||
'beginWriteLocking()',
|
||||
'endWriteLocking()'));
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,129 @@
|
|||
<?php
|
||||
|
||||
final class AphrontIsolatedDatabaseConnection
|
||||
extends AphrontDatabaseConnection {
|
||||
|
||||
private $configuration;
|
||||
private static $nextInsertID;
|
||||
private $insertID;
|
||||
|
||||
private $transcript = array();
|
||||
|
||||
private $allResults;
|
||||
private $affectedRows;
|
||||
|
||||
public function __construct(array $configuration) {
|
||||
$this->configuration = $configuration;
|
||||
|
||||
if (self::$nextInsertID === null) {
|
||||
// Generate test IDs into a distant ID space to reduce the risk of
|
||||
// collisions and make them distinctive.
|
||||
self::$nextInsertID = 55555000000 + mt_rand(0, 1000);
|
||||
}
|
||||
}
|
||||
|
||||
public function openConnection() {
|
||||
return;
|
||||
}
|
||||
|
||||
public function close() {
|
||||
return;
|
||||
}
|
||||
|
||||
public function escapeUTF8String($string) {
|
||||
return '<S>';
|
||||
}
|
||||
|
||||
public function escapeBinaryString($string) {
|
||||
return '<B>';
|
||||
}
|
||||
|
||||
public function escapeColumnName($name) {
|
||||
return '<C>';
|
||||
}
|
||||
|
||||
public function escapeMultilineComment($comment) {
|
||||
return '<K>';
|
||||
}
|
||||
|
||||
public function escapeStringForLikeClause($value) {
|
||||
return '<L>';
|
||||
}
|
||||
|
||||
private function getConfiguration($key, $default = null) {
|
||||
return idx($this->configuration, $key, $default);
|
||||
}
|
||||
|
||||
public function getInsertID() {
|
||||
return $this->insertID;
|
||||
}
|
||||
|
||||
public function getAffectedRows() {
|
||||
return $this->affectedRows;
|
||||
}
|
||||
|
||||
public function selectAllResults() {
|
||||
return $this->allResults;
|
||||
}
|
||||
|
||||
public function executeRawQuery($raw_query) {
|
||||
|
||||
// NOTE: "[\s<>K]*" allows any number of (properly escaped) comments to
|
||||
// appear prior to the allowed keyword, since this connection escapes
|
||||
// them as "<K>" (above).
|
||||
|
||||
$keywords = array(
|
||||
'INSERT',
|
||||
'UPDATE',
|
||||
'DELETE',
|
||||
'START',
|
||||
'SAVEPOINT',
|
||||
'COMMIT',
|
||||
'ROLLBACK',
|
||||
);
|
||||
$preg_keywords = array();
|
||||
foreach ($keywords as $key => $word) {
|
||||
$preg_keywords[] = preg_quote($word, '/');
|
||||
}
|
||||
$preg_keywords = implode('|', $preg_keywords);
|
||||
|
||||
if (!preg_match('/^[\s<>K]*('.$preg_keywords.')\s*/i', $raw_query)) {
|
||||
throw new AphrontNotSupportedQueryException(
|
||||
pht(
|
||||
"Database isolation currently only supports some queries. You are ".
|
||||
"trying to issue a query which does not begin with an allowed ".
|
||||
"keyword (%s): '%s'.",
|
||||
implode(', ', $keywords),
|
||||
$raw_query));
|
||||
}
|
||||
|
||||
$this->transcript[] = $raw_query;
|
||||
|
||||
// NOTE: This method is intentionally simplified for now, since we're only
|
||||
// using it to stub out inserts/updates. In the future it will probably need
|
||||
// to grow more powerful.
|
||||
|
||||
$this->allResults = array();
|
||||
|
||||
// NOTE: We jitter the insert IDs to keep tests honest; a test should cover
|
||||
// the relationship between objects, not their exact insertion order. This
|
||||
// guarantees that IDs are unique but makes it impossible to hard-code tests
|
||||
// against this specific implementation detail.
|
||||
self::$nextInsertID += mt_rand(1, 10);
|
||||
$this->insertID = self::$nextInsertID;
|
||||
$this->affectedRows = 1;
|
||||
}
|
||||
|
||||
public function executeRawQueries(array $raw_queries) {
|
||||
$results = array();
|
||||
foreach ($raw_queries as $id => $raw_query) {
|
||||
$results[$id] = array();
|
||||
}
|
||||
return $results;
|
||||
}
|
||||
|
||||
public function getQueryTranscript() {
|
||||
return $this->transcript;
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,384 @@
|
|||
<?php
|
||||
|
||||
abstract class AphrontBaseMySQLDatabaseConnection
|
||||
extends AphrontDatabaseConnection {
|
||||
|
||||
private $configuration;
|
||||
private $connection;
|
||||
private $connectionPool = array();
|
||||
private $lastResult;
|
||||
|
||||
private $nextError;
|
||||
|
||||
abstract protected function connect();
|
||||
abstract protected function rawQuery($raw_query);
|
||||
abstract protected function rawQueries(array $raw_queries);
|
||||
abstract protected function fetchAssoc($result);
|
||||
abstract protected function getErrorCode($connection);
|
||||
abstract protected function getErrorDescription($connection);
|
||||
abstract protected function closeConnection();
|
||||
abstract protected function freeResult($result);
|
||||
|
||||
public function __construct(array $configuration) {
|
||||
$this->configuration = $configuration;
|
||||
}
|
||||
|
||||
public function __clone() {
|
||||
$this->establishConnection();
|
||||
}
|
||||
|
||||
public function openConnection() {
|
||||
$this->requireConnection();
|
||||
}
|
||||
|
||||
public function close() {
|
||||
if ($this->lastResult) {
|
||||
$this->lastResult = null;
|
||||
}
|
||||
if ($this->connection) {
|
||||
$this->closeConnection();
|
||||
$this->connection = null;
|
||||
}
|
||||
}
|
||||
|
||||
public function escapeColumnName($name) {
|
||||
return '`'.str_replace('`', '``', $name).'`';
|
||||
}
|
||||
|
||||
|
||||
public function escapeMultilineComment($comment) {
|
||||
// These can either terminate a comment, confuse the hell out of the parser,
|
||||
// make MySQL execute the comment as a query, or, in the case of semicolon,
|
||||
// are quasi-dangerous because the semicolon could turn a broken query into
|
||||
// a working query plus an ignored query.
|
||||
|
||||
static $map = array(
|
||||
'--' => '(DOUBLEDASH)',
|
||||
'*/' => '(STARSLASH)',
|
||||
'//' => '(SLASHSLASH)',
|
||||
'#' => '(HASH)',
|
||||
'!' => '(BANG)',
|
||||
';' => '(SEMICOLON)',
|
||||
);
|
||||
|
||||
$comment = str_replace(
|
||||
array_keys($map),
|
||||
array_values($map),
|
||||
$comment);
|
||||
|
||||
// For good measure, kill anything else that isn't a nice printable
|
||||
// character.
|
||||
$comment = preg_replace('/[^\x20-\x7F]+/', ' ', $comment);
|
||||
|
||||
return '/* '.$comment.' */';
|
||||
}
|
||||
|
||||
public function escapeStringForLikeClause($value) {
|
||||
$value = addcslashes($value, '\%_');
|
||||
$value = $this->escapeUTF8String($value);
|
||||
return $value;
|
||||
}
|
||||
|
||||
protected function getConfiguration($key, $default = null) {
|
||||
return idx($this->configuration, $key, $default);
|
||||
}
|
||||
|
||||
private function establishConnection() {
|
||||
$host = $this->getConfiguration('host');
|
||||
$database = $this->getConfiguration('database');
|
||||
|
||||
$profiler = PhutilServiceProfiler::getInstance();
|
||||
$call_id = $profiler->beginServiceCall(
|
||||
array(
|
||||
'type' => 'connect',
|
||||
'host' => $host,
|
||||
'database' => $database,
|
||||
));
|
||||
|
||||
$retries = max(1, $this->getConfiguration('retries', 3));
|
||||
while ($retries--) {
|
||||
try {
|
||||
$conn = $this->connect();
|
||||
$profiler->endServiceCall($call_id, array());
|
||||
break;
|
||||
} catch (AphrontQueryException $ex) {
|
||||
if ($retries && $ex->getCode() == 2003) {
|
||||
$class = get_class($ex);
|
||||
$message = $ex->getMessage();
|
||||
phlog(pht('Retrying (%d) after %s: %s', $retries, $class, $message));
|
||||
} else {
|
||||
$profiler->endServiceCall($call_id, array());
|
||||
throw $ex;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$this->connection = $conn;
|
||||
}
|
||||
|
||||
protected function requireConnection() {
|
||||
if (!$this->connection) {
|
||||
if ($this->connectionPool) {
|
||||
$this->connection = array_pop($this->connectionPool);
|
||||
} else {
|
||||
$this->establishConnection();
|
||||
}
|
||||
}
|
||||
return $this->connection;
|
||||
}
|
||||
|
||||
protected function beginAsyncConnection() {
|
||||
$connection = $this->requireConnection();
|
||||
$this->connection = null;
|
||||
return $connection;
|
||||
}
|
||||
|
||||
protected function endAsyncConnection($connection) {
|
||||
if ($this->connection) {
|
||||
$this->connectionPool[] = $this->connection;
|
||||
}
|
||||
$this->connection = $connection;
|
||||
}
|
||||
|
||||
public function selectAllResults() {
|
||||
$result = array();
|
||||
$res = $this->lastResult;
|
||||
if ($res == null) {
|
||||
throw new Exception(pht('No query result to fetch from!'));
|
||||
}
|
||||
while (($row = $this->fetchAssoc($res))) {
|
||||
$result[] = $row;
|
||||
}
|
||||
return $result;
|
||||
}
|
||||
|
||||
public function executeRawQuery($raw_query) {
|
||||
$this->lastResult = null;
|
||||
$retries = max(1, $this->getConfiguration('retries', 3));
|
||||
while ($retries--) {
|
||||
try {
|
||||
$this->requireConnection();
|
||||
$is_write = $this->checkWrite($raw_query);
|
||||
|
||||
$profiler = PhutilServiceProfiler::getInstance();
|
||||
$call_id = $profiler->beginServiceCall(
|
||||
array(
|
||||
'type' => 'query',
|
||||
'config' => $this->configuration,
|
||||
'query' => $raw_query,
|
||||
'write' => $is_write,
|
||||
));
|
||||
|
||||
$result = $this->rawQuery($raw_query);
|
||||
|
||||
$profiler->endServiceCall($call_id, array());
|
||||
|
||||
if ($this->nextError) {
|
||||
$result = null;
|
||||
}
|
||||
|
||||
if ($result) {
|
||||
$this->lastResult = $result;
|
||||
break;
|
||||
}
|
||||
|
||||
$this->throwQueryException($this->connection);
|
||||
} catch (AphrontConnectionLostQueryException $ex) {
|
||||
$can_retry = ($retries > 0);
|
||||
|
||||
if ($this->isInsideTransaction()) {
|
||||
// Zero out the transaction state to prevent a second exception
|
||||
// ("program exited with open transaction") from being thrown, since
|
||||
// we're about to throw a more relevant/useful one instead.
|
||||
$state = $this->getTransactionState();
|
||||
while ($state->getDepth()) {
|
||||
$state->decreaseDepth();
|
||||
}
|
||||
|
||||
$can_retry = false;
|
||||
}
|
||||
|
||||
if ($this->isHoldingAnyLock()) {
|
||||
$this->forgetAllLocks();
|
||||
$can_retry = false;
|
||||
}
|
||||
|
||||
$this->close();
|
||||
|
||||
if (!$can_retry) {
|
||||
throw $ex;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function executeRawQueries(array $raw_queries) {
|
||||
if (!$raw_queries) {
|
||||
return array();
|
||||
}
|
||||
|
||||
$is_write = false;
|
||||
foreach ($raw_queries as $key => $raw_query) {
|
||||
$is_write = $is_write || $this->checkWrite($raw_query);
|
||||
$raw_queries[$key] = rtrim($raw_query, "\r\n\t ;");
|
||||
}
|
||||
|
||||
$profiler = PhutilServiceProfiler::getInstance();
|
||||
$call_id = $profiler->beginServiceCall(
|
||||
array(
|
||||
'type' => 'multi-query',
|
||||
'config' => $this->configuration,
|
||||
'queries' => $raw_queries,
|
||||
'write' => $is_write,
|
||||
));
|
||||
|
||||
$results = $this->rawQueries($raw_queries);
|
||||
|
||||
$profiler->endServiceCall($call_id, array());
|
||||
|
||||
return $results;
|
||||
}
|
||||
|
||||
protected function processResult($result) {
|
||||
if (!$result) {
|
||||
try {
|
||||
$this->throwQueryException($this->requireConnection());
|
||||
} catch (Exception $ex) {
|
||||
return $ex;
|
||||
}
|
||||
} else if (is_bool($result)) {
|
||||
return $this->getAffectedRows();
|
||||
}
|
||||
$rows = array();
|
||||
while (($row = $this->fetchAssoc($result))) {
|
||||
$rows[] = $row;
|
||||
}
|
||||
$this->freeResult($result);
|
||||
return $rows;
|
||||
}
|
||||
|
||||
protected function checkWrite($raw_query) {
|
||||
// NOTE: The opening "(" allows queries in the form of:
|
||||
//
|
||||
// (SELECT ...) UNION (SELECT ...)
|
||||
$is_write = !preg_match('/^[(]*(SELECT|SHOW|EXPLAIN)\s/', $raw_query);
|
||||
if ($is_write) {
|
||||
if ($this->getReadOnly()) {
|
||||
throw new Exception(
|
||||
pht(
|
||||
'Attempting to issue a write query on a read-only '.
|
||||
'connection (to database "%s")!',
|
||||
$this->getConfiguration('database')));
|
||||
}
|
||||
AphrontWriteGuard::willWrite();
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
protected function throwQueryException($connection) {
|
||||
if ($this->nextError) {
|
||||
$errno = $this->nextError;
|
||||
$error = pht('Simulated error.');
|
||||
$this->nextError = null;
|
||||
} else {
|
||||
$errno = $this->getErrorCode($connection);
|
||||
$error = $this->getErrorDescription($connection);
|
||||
}
|
||||
$this->throwQueryCodeException($errno, $error);
|
||||
}
|
||||
|
||||
private function throwCommonException($errno, $error) {
|
||||
$message = pht('#%d: %s', $errno, $error);
|
||||
|
||||
switch ($errno) {
|
||||
case 2013: // Connection Dropped
|
||||
throw new AphrontConnectionLostQueryException($message);
|
||||
case 2006: // Gone Away
|
||||
$more = pht(
|
||||
"This error may occur if your MySQL '%s' or '%s' ".
|
||||
"configuration values are set too low.",
|
||||
'wait_timeout',
|
||||
'max_allowed_packet');
|
||||
throw new AphrontConnectionLostQueryException("{$message}\n\n{$more}");
|
||||
case 1213: // Deadlock
|
||||
throw new AphrontDeadlockQueryException($message);
|
||||
case 1205: // Lock wait timeout exceeded
|
||||
throw new AphrontLockTimeoutQueryException($message);
|
||||
case 1062: // Duplicate Key
|
||||
// NOTE: In some versions of MySQL we get a key name back here, but
|
||||
// older versions just give us a key index ("key 2") so it's not
|
||||
// portable to parse the key out of the error and attach it to the
|
||||
// exception.
|
||||
throw new AphrontDuplicateKeyQueryException($message);
|
||||
case 1044: // Access denied to database
|
||||
case 1142: // Access denied to table
|
||||
case 1143: // Access denied to column
|
||||
case 1227: // Access denied (e.g., no SUPER for SHOW SLAVE STATUS).
|
||||
throw new AphrontAccessDeniedQueryException($message);
|
||||
case 1045: // Access denied (auth)
|
||||
throw new AphrontInvalidCredentialsQueryException($message);
|
||||
case 1146: // No such table
|
||||
case 1049: // No such database
|
||||
case 1054: // Unknown column "..." in field list
|
||||
throw new AphrontSchemaQueryException($message);
|
||||
}
|
||||
|
||||
// TODO: 1064 is syntax error, and quite terrible in production.
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
protected function throwConnectionException($errno, $error, $user, $host) {
|
||||
$this->throwCommonException($errno, $error);
|
||||
|
||||
$message = pht(
|
||||
'Attempt to connect to %s@%s failed with error #%d: %s.',
|
||||
$user,
|
||||
$host,
|
||||
$errno,
|
||||
$error);
|
||||
|
||||
throw new AphrontConnectionQueryException($message, $errno);
|
||||
}
|
||||
|
||||
|
||||
protected function throwQueryCodeException($errno, $error) {
|
||||
$this->throwCommonException($errno, $error);
|
||||
|
||||
$message = pht(
|
||||
'#%d: %s',
|
||||
$errno,
|
||||
$error);
|
||||
|
||||
throw new AphrontQueryException($message, $errno);
|
||||
}
|
||||
|
||||
/**
|
||||
* Force the next query to fail with a simulated error. This should be used
|
||||
* ONLY for unit tests.
|
||||
*/
|
||||
public function simulateErrorOnNextQuery($error) {
|
||||
$this->nextError = $error;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check inserts for characters outside of the BMP. Even with the strictest
|
||||
* settings, MySQL will silently truncate data when it encounters these, which
|
||||
* can lead to data loss and security problems.
|
||||
*/
|
||||
protected function validateUTF8String($string) {
|
||||
if (phutil_is_utf8($string)) {
|
||||
return;
|
||||
}
|
||||
|
||||
throw new AphrontCharacterSetQueryException(
|
||||
pht(
|
||||
'Attempting to construct a query using a non-utf8 string when '.
|
||||
'utf8 is expected. Use the `%%B` conversion to escape binary '.
|
||||
'strings data.'));
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,233 @@
|
|||
<?php
|
||||
|
||||
final class AphrontMySQLDatabaseConnection
|
||||
extends AphrontBaseMySQLDatabaseConnection {
|
||||
|
||||
public function escapeUTF8String($string) {
|
||||
$this->validateUTF8String($string);
|
||||
return $this->escapeBinaryString($string);
|
||||
}
|
||||
|
||||
public function escapeBinaryString($string) {
|
||||
return mysql_real_escape_string($string, $this->requireConnection());
|
||||
}
|
||||
|
||||
public function getInsertID() {
|
||||
return mysql_insert_id($this->requireConnection());
|
||||
}
|
||||
|
||||
public function getAffectedRows() {
|
||||
return mysql_affected_rows($this->requireConnection());
|
||||
}
|
||||
|
||||
protected function closeConnection() {
|
||||
mysql_close($this->requireConnection());
|
||||
}
|
||||
|
||||
protected function connect() {
|
||||
if (!function_exists('mysql_connect')) {
|
||||
// We have to '@' the actual call since it can spew all sorts of silly
|
||||
// noise, but it will also silence fatals caused by not having MySQL
|
||||
// installed, which has bitten me on three separate occasions. Make sure
|
||||
// such failures are explicit and loud.
|
||||
throw new Exception(
|
||||
pht(
|
||||
'About to call %s, but the PHP MySQL extension is not available!',
|
||||
'mysql_connect()'));
|
||||
}
|
||||
|
||||
$user = $this->getConfiguration('user');
|
||||
$host = $this->getConfiguration('host');
|
||||
$port = $this->getConfiguration('port');
|
||||
|
||||
if ($port) {
|
||||
$host .= ':'.$port;
|
||||
}
|
||||
|
||||
$database = $this->getConfiguration('database');
|
||||
|
||||
$pass = $this->getConfiguration('pass');
|
||||
if ($pass instanceof PhutilOpaqueEnvelope) {
|
||||
$pass = $pass->openEnvelope();
|
||||
}
|
||||
|
||||
$timeout = $this->getConfiguration('timeout');
|
||||
$timeout_ini = 'mysql.connect_timeout';
|
||||
if ($timeout) {
|
||||
$old_timeout = ini_get($timeout_ini);
|
||||
ini_set($timeout_ini, $timeout);
|
||||
}
|
||||
|
||||
try {
|
||||
$conn = @mysql_connect(
|
||||
$host,
|
||||
$user,
|
||||
$pass,
|
||||
$new_link = true,
|
||||
$flags = 0);
|
||||
} catch (Exception $ex) {
|
||||
if ($timeout) {
|
||||
ini_set($timeout_ini, $old_timeout);
|
||||
}
|
||||
throw $ex;
|
||||
}
|
||||
|
||||
if ($timeout) {
|
||||
ini_set($timeout_ini, $old_timeout);
|
||||
}
|
||||
|
||||
if (!$conn) {
|
||||
$errno = mysql_errno();
|
||||
$error = mysql_error();
|
||||
$this->throwConnectionException($errno, $error, $user, $host);
|
||||
}
|
||||
|
||||
if ($database !== null) {
|
||||
$ret = @mysql_select_db($database, $conn);
|
||||
if (!$ret) {
|
||||
$this->throwQueryException($conn);
|
||||
}
|
||||
}
|
||||
|
||||
$ok = @mysql_set_charset('utf8mb4', $conn);
|
||||
if (!$ok) {
|
||||
mysql_set_charset('binary', $conn);
|
||||
}
|
||||
|
||||
return $conn;
|
||||
}
|
||||
|
||||
protected function rawQuery($raw_query) {
|
||||
return @mysql_query($raw_query, $this->requireConnection());
|
||||
}
|
||||
|
||||
/**
|
||||
* @phutil-external-symbol function mysql_multi_query
|
||||
* @phutil-external-symbol function mysql_fetch_result
|
||||
* @phutil-external-symbol function mysql_more_results
|
||||
* @phutil-external-symbol function mysql_next_result
|
||||
*/
|
||||
protected function rawQueries(array $raw_queries) {
|
||||
$conn = $this->requireConnection();
|
||||
$results = array();
|
||||
|
||||
if (!function_exists('mysql_multi_query')) {
|
||||
foreach ($raw_queries as $key => $raw_query) {
|
||||
$results[$key] = $this->processResult($this->rawQuery($raw_query));
|
||||
}
|
||||
return $results;
|
||||
}
|
||||
|
||||
if (!mysql_multi_query(implode("\n;\n\n", $raw_queries), $conn)) {
|
||||
$ex = $this->processResult(false);
|
||||
return array_fill_keys(array_keys($raw_queries), $ex);
|
||||
}
|
||||
|
||||
$processed_all = false;
|
||||
foreach ($raw_queries as $key => $raw_query) {
|
||||
$results[$key] = $this->processResult(@mysql_fetch_result($conn));
|
||||
if (!mysql_more_results($conn)) {
|
||||
$processed_all = true;
|
||||
break;
|
||||
}
|
||||
mysql_next_result($conn);
|
||||
}
|
||||
|
||||
if (!$processed_all) {
|
||||
throw new Exception(
|
||||
pht('There are some results left in the result set.'));
|
||||
}
|
||||
|
||||
return $results;
|
||||
}
|
||||
|
||||
protected function freeResult($result) {
|
||||
mysql_free_result($result);
|
||||
}
|
||||
|
||||
public function supportsParallelQueries() {
|
||||
// fb_parallel_query() doesn't support results with different columns.
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @phutil-external-symbol function fb_parallel_query
|
||||
*/
|
||||
public function executeParallelQueries(
|
||||
array $queries,
|
||||
array $conns = array()) {
|
||||
assert_instances_of($conns, __CLASS__);
|
||||
|
||||
$map = array();
|
||||
$is_write = false;
|
||||
foreach ($queries as $id => $query) {
|
||||
$is_write = $is_write || $this->checkWrite($query);
|
||||
$conn = idx($conns, $id, $this);
|
||||
|
||||
$host = $conn->getConfiguration('host');
|
||||
$port = 0;
|
||||
$match = null;
|
||||
if (preg_match('/(.+):(.+)/', $host, $match)) {
|
||||
list(, $host, $port) = $match;
|
||||
}
|
||||
|
||||
$pass = $conn->getConfiguration('pass');
|
||||
if ($pass instanceof PhutilOpaqueEnvelope) {
|
||||
$pass = $pass->openEnvelope();
|
||||
}
|
||||
|
||||
$map[$id] = array(
|
||||
'sql' => $query,
|
||||
'ip' => $host,
|
||||
'port' => $port,
|
||||
'username' => $conn->getConfiguration('user'),
|
||||
'password' => $pass,
|
||||
'db' => $conn->getConfiguration('database'),
|
||||
);
|
||||
}
|
||||
|
||||
$profiler = PhutilServiceProfiler::getInstance();
|
||||
$call_id = $profiler->beginServiceCall(
|
||||
array(
|
||||
'type' => 'multi-query',
|
||||
'queries' => $queries,
|
||||
'write' => $is_write,
|
||||
));
|
||||
|
||||
$map = fb_parallel_query($map);
|
||||
|
||||
$profiler->endServiceCall($call_id, array());
|
||||
|
||||
$results = array();
|
||||
$pos = 0;
|
||||
$err_pos = 0;
|
||||
foreach ($queries as $id => $query) {
|
||||
$errno = idx(idx($map, 'errno', array()), $err_pos);
|
||||
$err_pos++;
|
||||
if ($errno) {
|
||||
try {
|
||||
$this->throwQueryCodeException($errno, $map['error'][$id]);
|
||||
} catch (Exception $ex) {
|
||||
$results[$id] = $ex;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
$results[$id] = $map['result'][$pos];
|
||||
$pos++;
|
||||
}
|
||||
return $results;
|
||||
}
|
||||
|
||||
protected function fetchAssoc($result) {
|
||||
return mysql_fetch_assoc($result);
|
||||
}
|
||||
|
||||
protected function getErrorCode($connection) {
|
||||
return mysql_errno($connection);
|
||||
}
|
||||
|
||||
protected function getErrorDescription($connection) {
|
||||
return mysql_error($connection);
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,210 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* @phutil-external-symbol class mysqli
|
||||
*/
|
||||
final class AphrontMySQLiDatabaseConnection
|
||||
extends AphrontBaseMySQLDatabaseConnection {
|
||||
|
||||
private $connectionOpen = false;
|
||||
|
||||
public function escapeUTF8String($string) {
|
||||
$this->validateUTF8String($string);
|
||||
return $this->escapeBinaryString($string);
|
||||
}
|
||||
|
||||
public function escapeBinaryString($string) {
|
||||
return $this->requireConnection()->escape_string($string);
|
||||
}
|
||||
|
||||
public function getInsertID() {
|
||||
return $this->requireConnection()->insert_id;
|
||||
}
|
||||
|
||||
public function getAffectedRows() {
|
||||
return $this->requireConnection()->affected_rows;
|
||||
}
|
||||
|
||||
protected function closeConnection() {
|
||||
if ($this->connectionOpen) {
|
||||
$this->requireConnection()->close();
|
||||
$this->connectionOpen = false;
|
||||
}
|
||||
}
|
||||
|
||||
protected function connect() {
|
||||
if (!class_exists('mysqli', false)) {
|
||||
throw new Exception(pht(
|
||||
'About to call new %s, but the PHP MySQLi extension is not available!',
|
||||
'mysqli()'));
|
||||
}
|
||||
|
||||
$user = $this->getConfiguration('user');
|
||||
$host = $this->getConfiguration('host');
|
||||
$port = $this->getConfiguration('port');
|
||||
$database = $this->getConfiguration('database');
|
||||
|
||||
$pass = $this->getConfiguration('pass');
|
||||
if ($pass instanceof PhutilOpaqueEnvelope) {
|
||||
$pass = $pass->openEnvelope();
|
||||
}
|
||||
|
||||
// If the host is "localhost", the port is ignored and mysqli attempts to
|
||||
// connect over a socket.
|
||||
if ($port) {
|
||||
if ($host === 'localhost' || $host === null) {
|
||||
$host = '127.0.0.1';
|
||||
}
|
||||
}
|
||||
|
||||
$conn = mysqli_init();
|
||||
|
||||
$timeout = $this->getConfiguration('timeout');
|
||||
if ($timeout) {
|
||||
$conn->options(MYSQLI_OPT_CONNECT_TIMEOUT, $timeout);
|
||||
}
|
||||
|
||||
if ($this->getPersistent()) {
|
||||
$host = 'p:'.$host;
|
||||
}
|
||||
|
||||
@$conn->real_connect(
|
||||
$host,
|
||||
$user,
|
||||
$pass,
|
||||
$database,
|
||||
$port);
|
||||
|
||||
$errno = $conn->connect_errno;
|
||||
if ($errno) {
|
||||
$error = $conn->connect_error;
|
||||
$this->throwConnectionException($errno, $error, $user, $host);
|
||||
}
|
||||
|
||||
$this->connectionOpen = true;
|
||||
|
||||
$ok = @$conn->set_charset('utf8mb4');
|
||||
if (!$ok) {
|
||||
$ok = $conn->set_charset('binary');
|
||||
}
|
||||
|
||||
return $conn;
|
||||
}
|
||||
|
||||
protected function rawQuery($raw_query) {
|
||||
$conn = $this->requireConnection();
|
||||
$time_limit = $this->getQueryTimeout();
|
||||
|
||||
// If we have a query time limit, run this query synchronously but use
|
||||
// the async API. This allows us to kill queries which take too long
|
||||
// without requiring any configuration on the server side.
|
||||
if ($time_limit && $this->supportsAsyncQueries()) {
|
||||
$conn->query($raw_query, MYSQLI_ASYNC);
|
||||
|
||||
$read = array($conn);
|
||||
$error = array($conn);
|
||||
$reject = array($conn);
|
||||
|
||||
$result = mysqli::poll($read, $error, $reject, $time_limit);
|
||||
|
||||
if ($result === false) {
|
||||
$this->closeConnection();
|
||||
throw new Exception(
|
||||
pht('Failed to poll mysqli connection!'));
|
||||
} else if ($result === 0) {
|
||||
$this->closeConnection();
|
||||
throw new AphrontQueryTimeoutQueryException(
|
||||
pht(
|
||||
'Query timed out after %s second(s)!',
|
||||
new PhutilNumber($time_limit)));
|
||||
}
|
||||
|
||||
return @$conn->reap_async_query();
|
||||
}
|
||||
|
||||
return @$conn->query($raw_query);
|
||||
}
|
||||
|
||||
protected function rawQueries(array $raw_queries) {
|
||||
$conn = $this->requireConnection();
|
||||
|
||||
$have_result = false;
|
||||
$results = array();
|
||||
|
||||
foreach ($raw_queries as $key => $raw_query) {
|
||||
if (!$have_result) {
|
||||
// End line in front of semicolon to allow single line comments at the
|
||||
// end of queries.
|
||||
$have_result = $conn->multi_query(implode("\n;\n\n", $raw_queries));
|
||||
} else {
|
||||
$have_result = $conn->next_result();
|
||||
}
|
||||
|
||||
array_shift($raw_queries);
|
||||
|
||||
$result = $conn->store_result();
|
||||
if (!$result && !$this->getErrorCode($conn)) {
|
||||
$result = true;
|
||||
}
|
||||
$results[$key] = $this->processResult($result);
|
||||
}
|
||||
|
||||
if ($conn->more_results()) {
|
||||
throw new Exception(
|
||||
pht('There are some results left in the result set.'));
|
||||
}
|
||||
|
||||
return $results;
|
||||
}
|
||||
|
||||
protected function freeResult($result) {
|
||||
$result->free_result();
|
||||
}
|
||||
|
||||
protected function fetchAssoc($result) {
|
||||
return $result->fetch_assoc();
|
||||
}
|
||||
|
||||
protected function getErrorCode($connection) {
|
||||
return $connection->errno;
|
||||
}
|
||||
|
||||
protected function getErrorDescription($connection) {
|
||||
return $connection->error;
|
||||
}
|
||||
|
||||
public function supportsAsyncQueries() {
|
||||
return defined('MYSQLI_ASYNC');
|
||||
}
|
||||
|
||||
public function asyncQuery($raw_query) {
|
||||
$this->checkWrite($raw_query);
|
||||
$async = $this->beginAsyncConnection();
|
||||
$async->query($raw_query, MYSQLI_ASYNC);
|
||||
return $async;
|
||||
}
|
||||
|
||||
public static function resolveAsyncQueries(array $conns, array $asyncs) {
|
||||
assert_instances_of($conns, __CLASS__);
|
||||
assert_instances_of($asyncs, 'mysqli');
|
||||
|
||||
$read = $error = $reject = array();
|
||||
foreach ($asyncs as $async) {
|
||||
$read[] = $error[] = $reject[] = $async;
|
||||
}
|
||||
|
||||
if (!mysqli::poll($read, $error, $reject, 0)) {
|
||||
return array();
|
||||
}
|
||||
|
||||
$results = array();
|
||||
foreach ($read as $async) {
|
||||
$key = array_search($async, $asyncs, $strict = true);
|
||||
$conn = $conns[$key];
|
||||
$conn->endAsyncConnection($async);
|
||||
$results[$key] = $conn->processResult($async->reap_async_query());
|
||||
}
|
||||
return $results;
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,4 @@
|
|||
<?php
|
||||
|
||||
final class AphrontAccessDeniedQueryException
|
||||
extends AphrontQueryException {}
|
|
@ -0,0 +1,3 @@
|
|||
<?php
|
||||
|
||||
final class AphrontCharacterSetQueryException extends AphrontQueryException {}
|
|
@ -0,0 +1,4 @@
|
|||
<?php
|
||||
|
||||
final class AphrontConnectionLostQueryException
|
||||
extends AphrontRecoverableQueryException {}
|
|
@ -0,0 +1,3 @@
|
|||
<?php
|
||||
|
||||
final class AphrontConnectionQueryException extends AphrontQueryException {}
|
|
@ -0,0 +1,3 @@
|
|||
<?php
|
||||
|
||||
final class AphrontCountQueryException extends AphrontQueryException {}
|
|
@ -0,0 +1,4 @@
|
|||
<?php
|
||||
|
||||
final class AphrontDeadlockQueryException
|
||||
extends AphrontRecoverableQueryException {}
|
|
@ -0,0 +1,3 @@
|
|||
<?php
|
||||
|
||||
final class AphrontDuplicateKeyQueryException extends AphrontQueryException {}
|
|
@ -0,0 +1,4 @@
|
|||
<?php
|
||||
|
||||
final class AphrontInvalidCredentialsQueryException
|
||||
extends AphrontQueryException {}
|
|
@ -0,0 +1,4 @@
|
|||
<?php
|
||||
|
||||
final class AphrontLockTimeoutQueryException
|
||||
extends AphrontRecoverableQueryException {}
|
|
@ -0,0 +1,3 @@
|
|||
<?php
|
||||
|
||||
final class AphrontNotSupportedQueryException extends AphrontQueryException {}
|
|
@ -0,0 +1,3 @@
|
|||
<?php
|
||||
|
||||
final class AphrontObjectMissingQueryException extends AphrontQueryException {}
|
|
@ -0,0 +1,16 @@
|
|||
<?php
|
||||
|
||||
final class AphrontParameterQueryException extends AphrontQueryException {
|
||||
|
||||
private $query;
|
||||
|
||||
public function __construct($query, $message) {
|
||||
parent::__construct(pht('%s Query: %s', $message, $query));
|
||||
$this->query = $query;
|
||||
}
|
||||
|
||||
public function getQuery() {
|
||||
return $this->query;
|
||||
}
|
||||
|
||||
}
|
6
src/aphront/storage/exception/AphrontQueryException.php
Normal file
6
src/aphront/storage/exception/AphrontQueryException.php
Normal file
|
@ -0,0 +1,6 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* @concrete-extensible
|
||||
*/
|
||||
class AphrontQueryException extends Exception {}
|
|
@ -0,0 +1,4 @@
|
|||
<?php
|
||||
|
||||
final class AphrontQueryTimeoutQueryException
|
||||
extends AphrontRecoverableQueryException {}
|
|
@ -0,0 +1,3 @@
|
|||
<?php
|
||||
|
||||
abstract class AphrontRecoverableQueryException extends AphrontQueryException {}
|
|
@ -0,0 +1,3 @@
|
|||
<?php
|
||||
|
||||
final class AphrontSchemaQueryException extends AphrontQueryException {}
|
|
@ -0,0 +1,9 @@
|
|||
<?php
|
||||
|
||||
final class AphrontScopedUnguardedWriteCapability extends Phobject {
|
||||
|
||||
public function __destruct() {
|
||||
AphrontWriteGuard::endUnguardedWrites();
|
||||
}
|
||||
|
||||
}
|
267
src/aphront/writeguard/AphrontWriteGuard.php
Normal file
267
src/aphront/writeguard/AphrontWriteGuard.php
Normal file
|
@ -0,0 +1,267 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* Guard writes against CSRF. The Aphront structure takes care of most of this
|
||||
* for you, you just need to call:
|
||||
*
|
||||
* AphrontWriteGuard::willWrite();
|
||||
*
|
||||
* ...before executing a write against any new kind of storage engine. MySQL
|
||||
* databases and the default file storage engines are already covered, but if
|
||||
* you introduce new types of datastores make sure their writes are guarded. If
|
||||
* you don't guard writes and make a mistake doing CSRF checks in a controller,
|
||||
* a CSRF vulnerability can escape undetected.
|
||||
*
|
||||
* If you need to execute writes on a page which doesn't have CSRF tokens (for
|
||||
* example, because you need to do logging), you can temporarily disable the
|
||||
* write guard by calling:
|
||||
*
|
||||
* AphrontWriteGuard::beginUnguardedWrites();
|
||||
* do_logging_write();
|
||||
* AphrontWriteGuard::endUnguardedWrites();
|
||||
*
|
||||
* This is dangerous, because it disables the backup layer of CSRF protection
|
||||
* this class provides. You should need this only very, very rarely.
|
||||
*
|
||||
* @task protect Protecting Writes
|
||||
* @task disable Disabling Protection
|
||||
* @task manage Managing Write Guards
|
||||
* @task internal Internals
|
||||
*/
|
||||
final class AphrontWriteGuard extends Phobject {
|
||||
|
||||
private static $instance;
|
||||
private static $allowUnguardedWrites = false;
|
||||
|
||||
private $callback;
|
||||
private $allowDepth = 0;
|
||||
|
||||
|
||||
/* -( Managing Write Guards )---------------------------------------------- */
|
||||
|
||||
|
||||
/**
|
||||
* Construct a new write guard for a request. Only one write guard may be
|
||||
* active at a time. You must explicitly call @{method:dispose} when you are
|
||||
* done with a write guard:
|
||||
*
|
||||
* $guard = new AphrontWriteGuard($callback);
|
||||
* // ...
|
||||
* $guard->dispose();
|
||||
*
|
||||
* Normally, you do not need to manage guards yourself -- the Aphront stack
|
||||
* handles it for you.
|
||||
*
|
||||
* This class accepts a callback, which will be invoked when a write is
|
||||
* attempted. The callback should validate the presence of a CSRF token in
|
||||
* the request, or abort the request (e.g., by throwing an exception) if a
|
||||
* valid token isn't present.
|
||||
*
|
||||
* @param callable CSRF callback.
|
||||
* @return this
|
||||
* @task manage
|
||||
*/
|
||||
public function __construct($callback) {
|
||||
if (self::$instance) {
|
||||
throw new Exception(
|
||||
pht(
|
||||
'An %s already exists. Dispose of the previous guard '.
|
||||
'before creating a new one.',
|
||||
__CLASS__));
|
||||
}
|
||||
if (self::$allowUnguardedWrites) {
|
||||
throw new Exception(
|
||||
pht(
|
||||
'An %s is being created in a context which permits '.
|
||||
'unguarded writes unconditionally. This is not allowed and '.
|
||||
'indicates a serious error.',
|
||||
__CLASS__));
|
||||
}
|
||||
$this->callback = $callback;
|
||||
self::$instance = $this;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Dispose of the active write guard. You must call this method when you are
|
||||
* done with a write guard. You do not normally need to call this yourself.
|
||||
*
|
||||
* @return void
|
||||
* @task manage
|
||||
*/
|
||||
public function dispose() {
|
||||
if (!self::$instance) {
|
||||
throw new Exception(pht(
|
||||
'Attempting to dispose of write guard, but no write guard is active!'));
|
||||
}
|
||||
|
||||
if ($this->allowDepth > 0) {
|
||||
throw new Exception(
|
||||
pht(
|
||||
'Imbalanced %s: more %s calls than %s calls.',
|
||||
__CLASS__,
|
||||
'beginUnguardedWrites()',
|
||||
'endUnguardedWrites()'));
|
||||
}
|
||||
self::$instance = null;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Determine if there is an active write guard.
|
||||
*
|
||||
* @return bool
|
||||
* @task manage
|
||||
*/
|
||||
public static function isGuardActive() {
|
||||
return (bool)self::$instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return on instance of AphrontWriteGuard if it's active, or null
|
||||
*
|
||||
* @return AphrontWriteGuard|null
|
||||
*/
|
||||
public static function getInstance() {
|
||||
return self::$instance;
|
||||
}
|
||||
|
||||
|
||||
/* -( Protecting Writes )-------------------------------------------------- */
|
||||
|
||||
|
||||
/**
|
||||
* Declare intention to perform a write, validating that writes are allowed.
|
||||
* You should call this method before executing a write whenever you implement
|
||||
* a new storage engine where information can be permanently kept.
|
||||
*
|
||||
* Writes are permitted if:
|
||||
*
|
||||
* - The request has valid CSRF tokens.
|
||||
* - Unguarded writes have been temporarily enabled by a call to
|
||||
* @{method:beginUnguardedWrites}.
|
||||
* - All write guarding has been disabled with
|
||||
* @{method:allowDangerousUnguardedWrites}.
|
||||
*
|
||||
* If none of these conditions are true, this method will throw and prevent
|
||||
* the write.
|
||||
*
|
||||
* @return void
|
||||
* @task protect
|
||||
*/
|
||||
public static function willWrite() {
|
||||
if (!self::$instance) {
|
||||
if (!self::$allowUnguardedWrites) {
|
||||
throw new Exception(
|
||||
pht(
|
||||
'Unguarded write! There must be an active %s to perform writes.',
|
||||
__CLASS__));
|
||||
} else {
|
||||
// Unguarded writes are being allowed unconditionally.
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
$instance = self::$instance;
|
||||
if ($instance->allowDepth == 0) {
|
||||
call_user_func($instance->callback);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/* -( Disabling Write Protection )----------------------------------------- */
|
||||
|
||||
|
||||
/**
|
||||
* Enter a scope which permits unguarded writes. This works like
|
||||
* @{method:beginUnguardedWrites} but returns an object which will end
|
||||
* the unguarded write scope when its __destruct() method is called. This
|
||||
* is useful to more easily handle exceptions correctly in unguarded write
|
||||
* blocks:
|
||||
*
|
||||
* // Restores the guard even if do_logging() throws.
|
||||
* function unguarded_scope() {
|
||||
* $unguarded = AphrontWriteGuard::beginScopedUnguardedWrites();
|
||||
* do_logging();
|
||||
* }
|
||||
*
|
||||
* @return AphrontScopedUnguardedWriteCapability Object which ends unguarded
|
||||
* writes when it leaves scope.
|
||||
* @task disable
|
||||
*/
|
||||
public static function beginScopedUnguardedWrites() {
|
||||
self::beginUnguardedWrites();
|
||||
return new AphrontScopedUnguardedWriteCapability();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Begin a block which permits unguarded writes. You should use this very
|
||||
* sparingly, and only for things like logging where CSRF is not a concern.
|
||||
*
|
||||
* You must pair every call to @{method:beginUnguardedWrites} with a call to
|
||||
* @{method:endUnguardedWrites}:
|
||||
*
|
||||
* AphrontWriteGuard::beginUnguardedWrites();
|
||||
* do_logging();
|
||||
* AphrontWriteGuard::endUnguardedWrites();
|
||||
*
|
||||
* @return void
|
||||
* @task disable
|
||||
*/
|
||||
public static function beginUnguardedWrites() {
|
||||
if (!self::$instance) {
|
||||
return;
|
||||
}
|
||||
self::$instance->allowDepth++;
|
||||
}
|
||||
|
||||
/**
|
||||
* Declare that you have finished performing unguarded writes. You must
|
||||
* call this exactly once for each call to @{method:beginUnguardedWrites}.
|
||||
*
|
||||
* @return void
|
||||
* @task disable
|
||||
*/
|
||||
public static function endUnguardedWrites() {
|
||||
if (!self::$instance) {
|
||||
return;
|
||||
}
|
||||
if (self::$instance->allowDepth <= 0) {
|
||||
throw new Exception(
|
||||
pht(
|
||||
'Imbalanced %s: more %s calls than %s calls.',
|
||||
__CLASS__,
|
||||
'endUnguardedWrites()',
|
||||
'beginUnguardedWrites()'));
|
||||
}
|
||||
self::$instance->allowDepth--;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Allow execution of unguarded writes. This is ONLY appropriate for use in
|
||||
* script contexts or other contexts where you are guaranteed to never be
|
||||
* vulnerable to CSRF concerns. Calling this method is EXTREMELY DANGEROUS
|
||||
* if you do not understand the consequences.
|
||||
*
|
||||
* If you need to perform unguarded writes on an otherwise guarded workflow
|
||||
* which is vulnerable to CSRF, use @{method:beginUnguardedWrites}.
|
||||
*
|
||||
* @return void
|
||||
* @task disable
|
||||
*/
|
||||
public static function allowDangerousUnguardedWrites($allow) {
|
||||
if (self::$instance) {
|
||||
throw new Exception(
|
||||
pht(
|
||||
'You can not unconditionally disable %s by calling %s while a write '.
|
||||
'guard is active. Use %s to temporarily allow unguarded writes.',
|
||||
__CLASS__,
|
||||
__FUNCTION__.'()',
|
||||
'beginUnguardedWrites()'));
|
||||
}
|
||||
self::$allowUnguardedWrites = true;
|
||||
}
|
||||
|
||||
}
|
80
src/auth/PhutilAmazonAuthAdapter.php
Normal file
80
src/auth/PhutilAmazonAuthAdapter.php
Normal file
|
@ -0,0 +1,80 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* Authentication adapter for Amazon OAuth2.
|
||||
*/
|
||||
final class PhutilAmazonAuthAdapter extends PhutilOAuthAuthAdapter {
|
||||
|
||||
public function getAdapterType() {
|
||||
return 'amazon';
|
||||
}
|
||||
|
||||
public function getAdapterDomain() {
|
||||
return 'amazon.com';
|
||||
}
|
||||
|
||||
public function getAccountID() {
|
||||
return $this->getOAuthAccountData('user_id');
|
||||
}
|
||||
|
||||
public function getAccountEmail() {
|
||||
return $this->getOAuthAccountData('email');
|
||||
}
|
||||
|
||||
public function getAccountName() {
|
||||
return null;
|
||||
}
|
||||
|
||||
public function getAccountImageURI() {
|
||||
return null;
|
||||
}
|
||||
|
||||
public function getAccountURI() {
|
||||
return null;
|
||||
}
|
||||
|
||||
public function getAccountRealName() {
|
||||
return $this->getOAuthAccountData('name');
|
||||
}
|
||||
|
||||
protected function getAuthenticateBaseURI() {
|
||||
return 'https://www.amazon.com/ap/oa';
|
||||
}
|
||||
|
||||
protected function getTokenBaseURI() {
|
||||
return 'https://api.amazon.com/auth/o2/token';
|
||||
}
|
||||
|
||||
public function getScope() {
|
||||
return 'profile';
|
||||
}
|
||||
|
||||
public function getExtraAuthenticateParameters() {
|
||||
return array(
|
||||
'response_type' => 'code',
|
||||
);
|
||||
}
|
||||
|
||||
public function getExtraTokenParameters() {
|
||||
return array(
|
||||
'grant_type' => 'authorization_code',
|
||||
);
|
||||
}
|
||||
|
||||
protected function loadOAuthAccountData() {
|
||||
$uri = new PhutilURI('https://api.amazon.com/user/profile');
|
||||
$uri->setQueryParam('access_token', $this->getAccessToken());
|
||||
|
||||
$future = new HTTPSFuture($uri);
|
||||
list($body) = $future->resolvex();
|
||||
|
||||
try {
|
||||
return phutil_json_decode($body);
|
||||
} catch (PhutilJSONParserException $ex) {
|
||||
throw new PhutilProxyException(
|
||||
pht('Expected valid JSON response from Amazon account data request.'),
|
||||
$ex);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
86
src/auth/PhutilAsanaAuthAdapter.php
Normal file
86
src/auth/PhutilAsanaAuthAdapter.php
Normal file
|
@ -0,0 +1,86 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* Authentication adapter for Asana OAuth2.
|
||||
*/
|
||||
final class PhutilAsanaAuthAdapter extends PhutilOAuthAuthAdapter {
|
||||
|
||||
public function getAdapterType() {
|
||||
return 'asana';
|
||||
}
|
||||
|
||||
public function getAdapterDomain() {
|
||||
return 'asana.com';
|
||||
}
|
||||
|
||||
public function getAccountID() {
|
||||
return $this->getOAuthAccountData('id');
|
||||
}
|
||||
|
||||
public function getAccountEmail() {
|
||||
return $this->getOAuthAccountData('email');
|
||||
}
|
||||
|
||||
public function getAccountName() {
|
||||
return null;
|
||||
}
|
||||
|
||||
public function getAccountImageURI() {
|
||||
$photo = $this->getOAuthAccountData('photo', array());
|
||||
if (is_array($photo)) {
|
||||
return idx($photo, 'image_128x128');
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public function getAccountURI() {
|
||||
return null;
|
||||
}
|
||||
|
||||
public function getAccountRealName() {
|
||||
return $this->getOAuthAccountData('name');
|
||||
}
|
||||
|
||||
protected function getAuthenticateBaseURI() {
|
||||
return 'https://app.asana.com/-/oauth_authorize';
|
||||
}
|
||||
|
||||
protected function getTokenBaseURI() {
|
||||
return 'https://app.asana.com/-/oauth_token';
|
||||
}
|
||||
|
||||
public function getScope() {
|
||||
return null;
|
||||
}
|
||||
|
||||
public function getExtraAuthenticateParameters() {
|
||||
return array(
|
||||
'response_type' => 'code',
|
||||
);
|
||||
}
|
||||
|
||||
public function getExtraTokenParameters() {
|
||||
return array(
|
||||
'grant_type' => 'authorization_code',
|
||||
);
|
||||
}
|
||||
|
||||
public function getExtraRefreshParameters() {
|
||||
return array(
|
||||
'grant_type' => 'refresh_token',
|
||||
);
|
||||
}
|
||||
|
||||
public function supportsTokenRefresh() {
|
||||
return true;
|
||||
}
|
||||
|
||||
protected function loadOAuthAccountData() {
|
||||
return id(new PhutilAsanaFuture())
|
||||
->setAccessToken($this->getAccessToken())
|
||||
->setRawAsanaQuery('users/me')
|
||||
->resolve();
|
||||
}
|
||||
|
||||
}
|
123
src/auth/PhutilAuthAdapter.php
Normal file
123
src/auth/PhutilAuthAdapter.php
Normal file
|
@ -0,0 +1,123 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* Abstract interface to an identity provider or authentication source, like
|
||||
* Twitter, Facebook or Google.
|
||||
*
|
||||
* Generally, adapters are handed some set of credentials particular to the
|
||||
* provider they adapt, and they turn those credentials into standard
|
||||
* information about the user's identity. For example, the LDAP adapter is given
|
||||
* a username and password (and some other configuration information), uses them
|
||||
* to talk to the LDAP server, and produces a username, email, and so forth.
|
||||
*
|
||||
* Since the credentials a provider requires are specific to each provider, the
|
||||
* base adapter does not specify how an adapter should be constructed or
|
||||
* configured -- only what information it is expected to be able to provide once
|
||||
* properly configured.
|
||||
*/
|
||||
abstract class PhutilAuthAdapter extends Phobject {
|
||||
|
||||
/**
|
||||
* Get a unique identifier associated with the identity. For most providers,
|
||||
* this is an account ID.
|
||||
*
|
||||
* The account ID needs to be unique within this adapter's configuration, such
|
||||
* that `<adapterKey, accountID>` is globally unique and always identifies the
|
||||
* same identity.
|
||||
*
|
||||
* If the adapter was unable to authenticate an identity, it should return
|
||||
* `null`.
|
||||
*
|
||||
* @return string|null Unique account identifier, or `null` if authentication
|
||||
* failed.
|
||||
*/
|
||||
abstract public function getAccountID();
|
||||
|
||||
|
||||
/**
|
||||
* Get a string identifying this adapter, like "ldap". This string should be
|
||||
* unique to the adapter class.
|
||||
*
|
||||
* @return string Unique adapter identifier.
|
||||
*/
|
||||
abstract public function getAdapterType();
|
||||
|
||||
|
||||
/**
|
||||
* Get a string identifying the domain this adapter is acting on. This allows
|
||||
* an adapter (like LDAP) to act against different identity domains without
|
||||
* conflating credentials. For providers like Facebook or Google, the adapters
|
||||
* just return the relevant domain name.
|
||||
*
|
||||
* @return string Domain the adapter is associated with.
|
||||
*/
|
||||
abstract public function getAdapterDomain();
|
||||
|
||||
|
||||
/**
|
||||
* Generate a string uniquely identifying this adapter configuration. Within
|
||||
* the scope of a given key, all account IDs must uniquely identify exactly
|
||||
* one identity.
|
||||
*
|
||||
* @return string Unique identifier for this adapter configuration.
|
||||
*/
|
||||
public function getAdapterKey() {
|
||||
return $this->getAdapterType().':'.$this->getAdapterDomain();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Optionally, return an email address associated with this account.
|
||||
*
|
||||
* @return string|null An email address associated with the account, or
|
||||
* `null` if data is not available.
|
||||
*/
|
||||
public function getAccountEmail() {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Optionally, return a human readable username associated with this account.
|
||||
*
|
||||
* @return string|null Account username, or `null` if data isn't available.
|
||||
*/
|
||||
public function getAccountName() {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Optionally, return a URI corresponding to a human-viewable profile for
|
||||
* this account.
|
||||
*
|
||||
* @return string|null A profile URI associated with this account, or
|
||||
* `null` if the data isn't available.
|
||||
*/
|
||||
public function getAccountURI() {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Optionally, return a profile image URI associated with this account.
|
||||
*
|
||||
* @return string|null URI for an account profile image, or `null` if one is
|
||||
* not available.
|
||||
*/
|
||||
public function getAccountImageURI() {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Optionally, return a real name associated with this account.
|
||||
*
|
||||
* @return string|null A human real name, or `null` if this data is not
|
||||
* available.
|
||||
*/
|
||||
public function getAccountRealName() {
|
||||
return null;
|
||||
}
|
||||
|
||||
}
|
73
src/auth/PhutilBitbucketAuthAdapter.php
Normal file
73
src/auth/PhutilBitbucketAuthAdapter.php
Normal file
|
@ -0,0 +1,73 @@
|
|||
<?php
|
||||
|
||||
final class PhutilBitbucketAuthAdapter extends PhutilOAuth1AuthAdapter {
|
||||
|
||||
private $userInfo;
|
||||
|
||||
public function getAccountID() {
|
||||
return idx($this->getUserInfo(), 'username');
|
||||
}
|
||||
|
||||
public function getAccountName() {
|
||||
return idx($this->getUserInfo(), 'display_name');
|
||||
}
|
||||
|
||||
public function getAccountURI() {
|
||||
$name = $this->getAccountID();
|
||||
if (strlen($name)) {
|
||||
return 'https://bitbucket.org/'.$name;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public function getAccountImageURI() {
|
||||
return idx($this->getUserInfo(), 'avatar');
|
||||
}
|
||||
|
||||
public function getAccountRealName() {
|
||||
$parts = array(
|
||||
idx($this->getUserInfo(), 'first_name'),
|
||||
idx($this->getUserInfo(), 'last_name'),
|
||||
);
|
||||
$parts = array_filter($parts);
|
||||
return implode(' ', $parts);
|
||||
}
|
||||
|
||||
public function getAdapterType() {
|
||||
return 'bitbucket';
|
||||
}
|
||||
|
||||
public function getAdapterDomain() {
|
||||
return 'bitbucket.org';
|
||||
}
|
||||
|
||||
protected function getRequestTokenURI() {
|
||||
return 'https://bitbucket.org/api/1.0/oauth/request_token';
|
||||
}
|
||||
|
||||
protected function getAuthorizeTokenURI() {
|
||||
return 'https://bitbucket.org/api/1.0/oauth/authenticate';
|
||||
}
|
||||
|
||||
protected function getValidateTokenURI() {
|
||||
return 'https://bitbucket.org/api/1.0/oauth/access_token';
|
||||
}
|
||||
|
||||
private function getUserInfo() {
|
||||
if ($this->userInfo === null) {
|
||||
// We don't need any of the data in the handshake, but do need to
|
||||
// finish the process. This makes sure we've completed the handshake.
|
||||
$this->getHandshakeData();
|
||||
|
||||
$uri = new PhutilURI('https://bitbucket.org/api/1.0/user');
|
||||
|
||||
$data = $this->newOAuth1Future($uri)
|
||||
->setMethod('GET')
|
||||
->resolveJSON();
|
||||
|
||||
$this->userInfo = idx($data, 'user', array());
|
||||
}
|
||||
return $this->userInfo;
|
||||
}
|
||||
|
||||
}
|
84
src/auth/PhutilDisqusAuthAdapter.php
Normal file
84
src/auth/PhutilDisqusAuthAdapter.php
Normal file
|
@ -0,0 +1,84 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* Authentication adapter for Disqus OAuth2.
|
||||
*/
|
||||
final class PhutilDisqusAuthAdapter extends PhutilOAuthAuthAdapter {
|
||||
|
||||
public function getAdapterType() {
|
||||
return 'disqus';
|
||||
}
|
||||
|
||||
public function getAdapterDomain() {
|
||||
return 'disqus.com';
|
||||
}
|
||||
|
||||
public function getAccountID() {
|
||||
return $this->getOAuthAccountData('id');
|
||||
}
|
||||
|
||||
public function getAccountEmail() {
|
||||
return $this->getOAuthAccountData('email');
|
||||
}
|
||||
|
||||
public function getAccountName() {
|
||||
return $this->getOAuthAccountData('username');
|
||||
}
|
||||
|
||||
public function getAccountImageURI() {
|
||||
return $this->getOAuthAccountData('avatar', 'permalink');
|
||||
}
|
||||
|
||||
public function getAccountURI() {
|
||||
return $this->getOAuthAccountData('profileUrl');
|
||||
}
|
||||
|
||||
public function getAccountRealName() {
|
||||
return $this->getOAuthAccountData('name');
|
||||
}
|
||||
|
||||
protected function getAuthenticateBaseURI() {
|
||||
return 'https://disqus.com/api/oauth/2.0/authorize/';
|
||||
}
|
||||
|
||||
protected function getTokenBaseURI() {
|
||||
return 'https://disqus.com/api/oauth/2.0/access_token/';
|
||||
}
|
||||
|
||||
public function getScope() {
|
||||
return 'read';
|
||||
}
|
||||
|
||||
public function getExtraAuthenticateParameters() {
|
||||
return array(
|
||||
'response_type' => 'code',
|
||||
);
|
||||
}
|
||||
|
||||
public function getExtraTokenParameters() {
|
||||
return array(
|
||||
'grant_type' => 'authorization_code',
|
||||
);
|
||||
}
|
||||
|
||||
protected function loadOAuthAccountData() {
|
||||
$uri = new PhutilURI('https://disqus.com/api/3.0/users/details.json');
|
||||
$uri->setQueryParam('api_key', $this->getClientID());
|
||||
$uri->setQueryParam('access_token', $this->getAccessToken());
|
||||
$uri = (string)$uri;
|
||||
|
||||
$future = new HTTPSFuture($uri);
|
||||
$future->setMethod('GET');
|
||||
list($body) = $future->resolvex();
|
||||
|
||||
try {
|
||||
$data = phutil_json_decode($body);
|
||||
return $data['response'];
|
||||
} catch (PhutilJSONParserException $ex) {
|
||||
throw new PhutilProxyException(
|
||||
pht('Expected valid JSON response from Disqus account data request.'),
|
||||
$ex);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
42
src/auth/PhutilEmptyAuthAdapter.php
Normal file
42
src/auth/PhutilEmptyAuthAdapter.php
Normal file
|
@ -0,0 +1,42 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* Empty authentication adapter with no logic.
|
||||
*
|
||||
* This adapter can be used when you need an adapter for some technical reason
|
||||
* but it doesn't make sense to put logic inside it.
|
||||
*/
|
||||
final class PhutilEmptyAuthAdapter extends PhutilAuthAdapter {
|
||||
|
||||
private $accountID;
|
||||
private $adapterType;
|
||||
private $adapterDomain;
|
||||
|
||||
public function setAdapterDomain($adapter_domain) {
|
||||
$this->adapterDomain = $adapter_domain;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getAdapterDomain() {
|
||||
return $this->adapterDomain;
|
||||
}
|
||||
|
||||
public function setAdapterType($adapter_type) {
|
||||
$this->adapterType = $adapter_type;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getAdapterType() {
|
||||
return $this->adapterType;
|
||||
}
|
||||
|
||||
public function setAccountID($account_id) {
|
||||
$this->accountID = $account_id;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getAccountID() {
|
||||
return $this->accountID;
|
||||
}
|
||||
|
||||
}
|
114
src/auth/PhutilFacebookAuthAdapter.php
Normal file
114
src/auth/PhutilFacebookAuthAdapter.php
Normal file
|
@ -0,0 +1,114 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* Authentication adapter for Facebook OAuth2.
|
||||
*/
|
||||
final class PhutilFacebookAuthAdapter extends PhutilOAuthAuthAdapter {
|
||||
|
||||
private $requireSecureBrowsing;
|
||||
|
||||
public function setRequireSecureBrowsing($require_secure_browsing) {
|
||||
$this->requireSecureBrowsing = $require_secure_browsing;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getAdapterType() {
|
||||
return 'facebook';
|
||||
}
|
||||
|
||||
public function getAdapterDomain() {
|
||||
return 'facebook.com';
|
||||
}
|
||||
|
||||
public function getAccountID() {
|
||||
return $this->getOAuthAccountData('id');
|
||||
}
|
||||
|
||||
public function getAccountEmail() {
|
||||
return $this->getOAuthAccountData('email');
|
||||
}
|
||||
|
||||
public function getAccountName() {
|
||||
$link = $this->getOAuthAccountData('link');
|
||||
if (!$link) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$matches = null;
|
||||
if (!preg_match('@/([^/]+)$@', $link, $matches)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $matches[1];
|
||||
}
|
||||
|
||||
public function getAccountImageURI() {
|
||||
$picture = $this->getOAuthAccountData('picture');
|
||||
if ($picture) {
|
||||
$picture_data = idx($picture, 'data');
|
||||
if ($picture_data) {
|
||||
return idx($picture_data, 'url');
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public function getAccountURI() {
|
||||
return $this->getOAuthAccountData('link');
|
||||
}
|
||||
|
||||
public function getAccountRealName() {
|
||||
return $this->getOAuthAccountData('name');
|
||||
}
|
||||
|
||||
public function getAccountSecuritySettings() {
|
||||
return $this->getOAuthAccountData('security_settings');
|
||||
}
|
||||
|
||||
protected function getAuthenticateBaseURI() {
|
||||
return 'https://www.facebook.com/dialog/oauth';
|
||||
}
|
||||
|
||||
protected function getTokenBaseURI() {
|
||||
return 'https://graph.facebook.com/oauth/access_token';
|
||||
}
|
||||
|
||||
protected function loadOAuthAccountData() {
|
||||
$fields = array(
|
||||
'id',
|
||||
'name',
|
||||
'email',
|
||||
'link',
|
||||
'security_settings',
|
||||
'picture',
|
||||
);
|
||||
|
||||
$uri = new PhutilURI('https://graph.facebook.com/me');
|
||||
$uri->setQueryParam('access_token', $this->getAccessToken());
|
||||
$uri->setQueryParam('fields', implode(',', $fields));
|
||||
list($body) = id(new HTTPSFuture($uri))->resolvex();
|
||||
|
||||
$data = null;
|
||||
try {
|
||||
$data = phutil_json_decode($body);
|
||||
} catch (PhutilJSONParserException $ex) {
|
||||
throw new PhutilProxyException(
|
||||
pht('Expected valid JSON response from Facebook account data request.'),
|
||||
$ex);
|
||||
}
|
||||
|
||||
if ($this->requireSecureBrowsing) {
|
||||
if (empty($data['security_settings']['secure_browsing']['enabled'])) {
|
||||
throw new Exception(
|
||||
pht(
|
||||
'This Phabricator install requires you to enable Secure Browsing '.
|
||||
'on your Facebook account in order to use it to log in to '.
|
||||
'Phabricator. For more information, see %s',
|
||||
'https://www.facebook.com/help/156201551113407/'));
|
||||
}
|
||||
}
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
}
|
72
src/auth/PhutilGitHubAuthAdapter.php
Normal file
72
src/auth/PhutilGitHubAuthAdapter.php
Normal file
|
@ -0,0 +1,72 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* Authentication adapter for Github OAuth2.
|
||||
*/
|
||||
final class PhutilGitHubAuthAdapter extends PhutilOAuthAuthAdapter {
|
||||
|
||||
public function getAdapterType() {
|
||||
return 'github';
|
||||
}
|
||||
|
||||
public function getAdapterDomain() {
|
||||
return 'github.com';
|
||||
}
|
||||
|
||||
public function getAccountID() {
|
||||
return $this->getOAuthAccountData('id');
|
||||
}
|
||||
|
||||
public function getAccountEmail() {
|
||||
return $this->getOAuthAccountData('email');
|
||||
}
|
||||
|
||||
public function getAccountName() {
|
||||
return $this->getOAuthAccountData('login');
|
||||
}
|
||||
|
||||
public function getAccountImageURI() {
|
||||
return $this->getOAuthAccountData('avatar_url');
|
||||
}
|
||||
|
||||
public function getAccountURI() {
|
||||
$name = $this->getAccountName();
|
||||
if (strlen($name)) {
|
||||
return 'https://github.com/'.$name;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public function getAccountRealName() {
|
||||
return $this->getOAuthAccountData('name');
|
||||
}
|
||||
|
||||
protected function getAuthenticateBaseURI() {
|
||||
return 'https://github.com/login/oauth/authorize';
|
||||
}
|
||||
|
||||
protected function getTokenBaseURI() {
|
||||
return 'https://github.com/login/oauth/access_token';
|
||||
}
|
||||
|
||||
protected function loadOAuthAccountData() {
|
||||
$uri = new PhutilURI('https://api.github.com/user');
|
||||
$uri->setQueryParam('access_token', $this->getAccessToken());
|
||||
|
||||
$future = new HTTPSFuture($uri);
|
||||
|
||||
// NOTE: GitHub requires a User-Agent string.
|
||||
$future->addHeader('User-Agent', __CLASS__);
|
||||
|
||||
list($body) = $future->resolvex();
|
||||
|
||||
try{
|
||||
return phutil_json_decode($body);
|
||||
} catch (PhutilJSONParserException $ex) {
|
||||
throw new PhutilProxyException(
|
||||
pht('Expected valid JSON response from GitHub account data request.'),
|
||||
$ex);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
181
src/auth/PhutilGoogleAuthAdapter.php
Normal file
181
src/auth/PhutilGoogleAuthAdapter.php
Normal file
|
@ -0,0 +1,181 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* Authentication adapter for Google OAuth2.
|
||||
*/
|
||||
final class PhutilGoogleAuthAdapter extends PhutilOAuthAuthAdapter {
|
||||
|
||||
public function getAdapterType() {
|
||||
return 'google';
|
||||
}
|
||||
|
||||
public function getAdapterDomain() {
|
||||
return 'google.com';
|
||||
}
|
||||
|
||||
public function getAccountID() {
|
||||
$emails = $this->getOAuthAccountData('emails', array());
|
||||
foreach ($emails as $email) {
|
||||
if (idx($email, 'type') == 'account') {
|
||||
return idx($email, 'value');
|
||||
}
|
||||
}
|
||||
|
||||
throw new Exception(
|
||||
pht(
|
||||
'Expected to retrieve an "account" email from Google Plus API call '.
|
||||
'to identify account, but failed.'));
|
||||
}
|
||||
|
||||
public function getAccountEmail() {
|
||||
return $this->getAccountID();
|
||||
}
|
||||
|
||||
public function getAccountName() {
|
||||
// Guess account name from email address, this is just a hint anyway.
|
||||
$email = $this->getAccountEmail();
|
||||
$email = explode('@', $email);
|
||||
$email = head($email);
|
||||
return $email;
|
||||
}
|
||||
|
||||
public function getAccountImageURI() {
|
||||
$image = $this->getOAuthAccountData('image', array());
|
||||
$uri = idx($image, 'url');
|
||||
|
||||
// Change the "sz" parameter ("size") from the default to 100 to ask for
|
||||
// a 100x100px image.
|
||||
if ($uri !== null) {
|
||||
$uri = new PhutilURI($uri);
|
||||
$uri->setQueryParam('sz', 100);
|
||||
$uri = (string)$uri;
|
||||
}
|
||||
|
||||
return $uri;
|
||||
}
|
||||
|
||||
public function getAccountURI() {
|
||||
return $this->getOAuthAccountData('url');
|
||||
}
|
||||
|
||||
public function getAccountRealName() {
|
||||
$name = $this->getOAuthAccountData('name', array());
|
||||
|
||||
// TODO: This could probably be made cleaner by looking up the API, but
|
||||
// this should work to unbreak logins.
|
||||
|
||||
$parts = array();
|
||||
$parts[] = idx($name, 'givenName');
|
||||
unset($name['givenName']);
|
||||
$parts[] = idx($name, 'familyName');
|
||||
unset($name['familyName']);
|
||||
$parts = array_merge($parts, $name);
|
||||
$parts = array_filter($parts);
|
||||
|
||||
return implode(' ', $parts);
|
||||
}
|
||||
|
||||
protected function getAuthenticateBaseURI() {
|
||||
return 'https://accounts.google.com/o/oauth2/auth';
|
||||
}
|
||||
|
||||
protected function getTokenBaseURI() {
|
||||
return 'https://accounts.google.com/o/oauth2/token';
|
||||
}
|
||||
|
||||
public function getScope() {
|
||||
$scopes = array(
|
||||
'email',
|
||||
'profile',
|
||||
);
|
||||
|
||||
return implode(' ', $scopes);
|
||||
}
|
||||
|
||||
public function getExtraAuthenticateParameters() {
|
||||
return array(
|
||||
'response_type' => 'code',
|
||||
);
|
||||
}
|
||||
|
||||
public function getExtraTokenParameters() {
|
||||
return array(
|
||||
'grant_type' => 'authorization_code',
|
||||
);
|
||||
}
|
||||
|
||||
protected function loadOAuthAccountData() {
|
||||
$uri = new PhutilURI('https://www.googleapis.com/plus/v1/people/me');
|
||||
$uri->setQueryParam('access_token', $this->getAccessToken());
|
||||
|
||||
$future = new HTTPSFuture($uri);
|
||||
list($status, $body) = $future->resolve();
|
||||
|
||||
if ($status->isError()) {
|
||||
$this->tryToThrowSpecializedError($status, $body);
|
||||
throw $status;
|
||||
}
|
||||
|
||||
try {
|
||||
return phutil_json_decode($body);
|
||||
} catch (PhutilJSONParserException $ex) {
|
||||
throw new PhutilProxyException(
|
||||
pht('Expected valid JSON response from Google account data request.'),
|
||||
$ex);
|
||||
}
|
||||
}
|
||||
|
||||
private function tryToThrowSpecializedError($status, $raw_body) {
|
||||
if (!($status instanceof HTTPFutureHTTPResponseStatus)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ($status->getStatusCode() != 403) {
|
||||
return;
|
||||
}
|
||||
|
||||
$body = phutil_json_decode($raw_body);
|
||||
if (!$body) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (empty($body['error']['errors'][0])) {
|
||||
return;
|
||||
}
|
||||
|
||||
$error = $body['error']['errors'][0];
|
||||
$domain = idx($error, 'domain');
|
||||
$reason = idx($error, 'reason');
|
||||
|
||||
if ($domain == 'usageLimits' && $reason == 'accessNotConfigured') {
|
||||
throw new PhutilAuthConfigurationException(
|
||||
pht(
|
||||
'Google returned an "%s" error. This usually means you need to '.
|
||||
'enable the "Google+ API" in your Google Cloud Console, under '.
|
||||
'"APIs".'.
|
||||
"\n\n".
|
||||
'Around March 2014, Google made some API changes which require this '.
|
||||
'configuration adjustment.'.
|
||||
"\n\n".
|
||||
'Normally, you can resolve this issue by going to %s, then '.
|
||||
'clicking "API Project", then "APIs & auth", then turning the '.
|
||||
'"Google+ API" on. The names you see on the console may be '.
|
||||
'different depending on how your integration is set up. If you '.
|
||||
'are not sure, you can hunt through the projects until you find '.
|
||||
'the one associated with the right Application ID under '.
|
||||
'"Credentials". The Application ID this install is using is "%s".'.
|
||||
"\n\n".
|
||||
'(If you are unable to log into Phabricator, you can use '.
|
||||
'"%s" to recover access to an administrator account.)'.
|
||||
"\n\n".
|
||||
'Full HTTP Response'.
|
||||
"\n\n%s",
|
||||
'accessNotConfigured',
|
||||
'https://console.developers.google.com/',
|
||||
$this->getClientID(),
|
||||
'bin/auth recover',
|
||||
$raw_body));
|
||||
}
|
||||
}
|
||||
|
||||
}
|
162
src/auth/PhutilJIRAAuthAdapter.php
Normal file
162
src/auth/PhutilJIRAAuthAdapter.php
Normal file
|
@ -0,0 +1,162 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* Authentication adapter for JIRA OAuth1.
|
||||
*/
|
||||
final class PhutilJIRAAuthAdapter extends PhutilOAuth1AuthAdapter {
|
||||
|
||||
// TODO: JIRA tokens expire (after 5 years) and we could surface and store
|
||||
// that.
|
||||
|
||||
private $jiraBaseURI;
|
||||
private $adapterDomain;
|
||||
private $currentSession;
|
||||
private $userInfo;
|
||||
|
||||
public function setJIRABaseURI($jira_base_uri) {
|
||||
$this->jiraBaseURI = $jira_base_uri;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getJIRABaseURI() {
|
||||
return $this->jiraBaseURI;
|
||||
}
|
||||
|
||||
public function getAccountID() {
|
||||
// Make sure the handshake is finished; this method is used for its
|
||||
// side effect by Auth providers.
|
||||
$this->getHandshakeData();
|
||||
|
||||
return idx($this->getUserInfo(), 'key');
|
||||
}
|
||||
|
||||
public function getAccountName() {
|
||||
return idx($this->getUserInfo(), 'name');
|
||||
}
|
||||
|
||||
public function getAccountImageURI() {
|
||||
$avatars = idx($this->getUserInfo(), 'avatarUrls');
|
||||
if ($avatars) {
|
||||
return idx($avatars, '48x48');
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public function getAccountRealName() {
|
||||
return idx($this->getUserInfo(), 'displayName');
|
||||
}
|
||||
|
||||
public function getAccountEmail() {
|
||||
return idx($this->getUserInfo(), 'emailAddress');
|
||||
}
|
||||
|
||||
public function getAdapterType() {
|
||||
return 'jira';
|
||||
}
|
||||
|
||||
public function getAdapterDomain() {
|
||||
return $this->adapterDomain;
|
||||
}
|
||||
|
||||
public function setAdapterDomain($domain) {
|
||||
$this->adapterDomain = $domain;
|
||||
return $this;
|
||||
}
|
||||
|
||||
protected function getSignatureMethod() {
|
||||
return 'RSA-SHA1';
|
||||
}
|
||||
|
||||
protected function getRequestTokenURI() {
|
||||
return $this->getJIRAURI('plugins/servlet/oauth/request-token');
|
||||
}
|
||||
|
||||
protected function getAuthorizeTokenURI() {
|
||||
return $this->getJIRAURI('plugins/servlet/oauth/authorize');
|
||||
}
|
||||
|
||||
protected function getValidateTokenURI() {
|
||||
return $this->getJIRAURI('plugins/servlet/oauth/access-token');
|
||||
}
|
||||
|
||||
private function getJIRAURI($path) {
|
||||
return rtrim($this->jiraBaseURI, '/').'/'.ltrim($path, '/');
|
||||
}
|
||||
|
||||
private function getUserInfo() {
|
||||
if ($this->userInfo === null) {
|
||||
$this->currentSession = $this->newJIRAFuture('rest/auth/1/session', 'GET')
|
||||
->resolveJSON();
|
||||
|
||||
// The session call gives us the username, but not the user key or other
|
||||
// information. Make a second call to get additional information.
|
||||
|
||||
$params = array(
|
||||
'username' => $this->currentSession['name'],
|
||||
);
|
||||
|
||||
$this->userInfo = $this->newJIRAFuture('rest/api/2/user', 'GET', $params)
|
||||
->resolveJSON();
|
||||
}
|
||||
|
||||
return $this->userInfo;
|
||||
}
|
||||
|
||||
public static function newJIRAKeypair() {
|
||||
$config = array(
|
||||
'digest_alg' => 'sha512',
|
||||
'private_key_bits' => 4096,
|
||||
'private_key_type' => OPENSSL_KEYTYPE_RSA,
|
||||
);
|
||||
|
||||
$res = openssl_pkey_new($config);
|
||||
if (!$res) {
|
||||
throw new Exception(pht('%s failed!', 'openssl_pkey_new()'));
|
||||
}
|
||||
|
||||
$private_key = null;
|
||||
$ok = openssl_pkey_export($res, $private_key);
|
||||
if (!$ok) {
|
||||
throw new Exception(pht('%s failed!', 'openssl_pkey_export()'));
|
||||
}
|
||||
|
||||
$public_key = openssl_pkey_get_details($res);
|
||||
if (!$ok || empty($public_key['key'])) {
|
||||
throw new Exception(pht('%s failed!', 'openssl_pkey_get_details()'));
|
||||
}
|
||||
$public_key = $public_key['key'];
|
||||
|
||||
return array($public_key, $private_key);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* JIRA indicates that the user has clicked the "Deny" button by passing a
|
||||
* well known `oauth_verifier` value ("denied"), which we check for here.
|
||||
*/
|
||||
protected function willFinishOAuthHandshake() {
|
||||
$jira_magic_word = 'denied';
|
||||
if ($this->getVerifier() == $jira_magic_word) {
|
||||
throw new PhutilAuthUserAbortedException();
|
||||
}
|
||||
}
|
||||
|
||||
public function newJIRAFuture($path, $method, $params = array()) {
|
||||
$uri = new PhutilURI($this->getJIRAURI($path));
|
||||
if ($method == 'GET') {
|
||||
$uri->setQueryParams($params);
|
||||
$params = array();
|
||||
} else {
|
||||
// For other types of requests, JIRA expects the request body to be
|
||||
// JSON encoded.
|
||||
$params = json_encode($params);
|
||||
}
|
||||
|
||||
// JIRA returns a 415 error if we don't provide a Content-Type header.
|
||||
|
||||
return $this->newOAuth1Future($uri, $params)
|
||||
->setMethod($method)
|
||||
->addHeader('Content-Type', 'application/json');
|
||||
}
|
||||
|
||||
}
|
505
src/auth/PhutilLDAPAuthAdapter.php
Normal file
505
src/auth/PhutilLDAPAuthAdapter.php
Normal file
|
@ -0,0 +1,505 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* Retrieve identify information from LDAP accounts.
|
||||
*/
|
||||
final class PhutilLDAPAuthAdapter extends PhutilAuthAdapter {
|
||||
|
||||
private $hostname;
|
||||
private $port = 389;
|
||||
|
||||
private $baseDistinguishedName;
|
||||
private $searchAttributes = array();
|
||||
private $usernameAttribute;
|
||||
private $realNameAttributes = array();
|
||||
private $ldapVersion = 3;
|
||||
private $ldapReferrals;
|
||||
private $ldapStartTLS;
|
||||
private $anonymousUsername;
|
||||
private $anonymousPassword;
|
||||
private $activeDirectoryDomain;
|
||||
private $alwaysSearch;
|
||||
|
||||
private $loginUsername;
|
||||
private $loginPassword;
|
||||
|
||||
private $ldapUserData;
|
||||
private $ldapConnection;
|
||||
|
||||
public function getAdapterType() {
|
||||
return 'ldap';
|
||||
}
|
||||
|
||||
public function setHostname($host) {
|
||||
$this->hostname = $host;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function setPort($port) {
|
||||
$this->port = $port;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getAdapterDomain() {
|
||||
return 'self';
|
||||
}
|
||||
|
||||
public function setBaseDistinguishedName($base_distinguished_name) {
|
||||
$this->baseDistinguishedName = $base_distinguished_name;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function setSearchAttributes(array $search_attributes) {
|
||||
$this->searchAttributes = $search_attributes;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function setUsernameAttribute($username_attribute) {
|
||||
$this->usernameAttribute = $username_attribute;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function setRealNameAttributes(array $attributes) {
|
||||
$this->realNameAttributes = $attributes;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function setLDAPVersion($ldap_version) {
|
||||
$this->ldapVersion = $ldap_version;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function setLDAPReferrals($ldap_referrals) {
|
||||
$this->ldapReferrals = $ldap_referrals;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function setLDAPStartTLS($ldap_start_tls) {
|
||||
$this->ldapStartTLS = $ldap_start_tls;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function setAnonymousUsername($anonymous_username) {
|
||||
$this->anonymousUsername = $anonymous_username;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function setAnonymousPassword(
|
||||
PhutilOpaqueEnvelope $anonymous_password) {
|
||||
$this->anonymousPassword = $anonymous_password;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function setLoginUsername($login_username) {
|
||||
$this->loginUsername = $login_username;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function setLoginPassword(PhutilOpaqueEnvelope $login_password) {
|
||||
$this->loginPassword = $login_password;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function setActiveDirectoryDomain($domain) {
|
||||
$this->activeDirectoryDomain = $domain;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function setAlwaysSearch($always_search) {
|
||||
$this->alwaysSearch = $always_search;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getAccountID() {
|
||||
return $this->readLDAPRecordAccountID($this->getLDAPUserData());
|
||||
}
|
||||
|
||||
public function getAccountName() {
|
||||
return $this->readLDAPRecordAccountName($this->getLDAPUserData());
|
||||
}
|
||||
|
||||
public function getAccountRealName() {
|
||||
return $this->readLDAPRecordRealName($this->getLDAPUserData());
|
||||
}
|
||||
|
||||
public function getAccountEmail() {
|
||||
return $this->readLDAPRecordEmail($this->getLDAPUserData());
|
||||
}
|
||||
|
||||
public function readLDAPRecordAccountID(array $record) {
|
||||
$key = $this->usernameAttribute;
|
||||
if (!strlen($key)) {
|
||||
$key = head($this->searchAttributes);
|
||||
}
|
||||
return $this->readLDAPData($record, $key);
|
||||
}
|
||||
|
||||
public function readLDAPRecordAccountName(array $record) {
|
||||
return $this->readLDAPRecordAccountID($record);
|
||||
}
|
||||
|
||||
public function readLDAPRecordRealName(array $record) {
|
||||
$parts = array();
|
||||
foreach ($this->realNameAttributes as $attribute) {
|
||||
$parts[] = $this->readLDAPData($record, $attribute);
|
||||
}
|
||||
$parts = array_filter($parts);
|
||||
|
||||
if ($parts) {
|
||||
return implode(' ', $parts);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public function readLDAPRecordEmail(array $record) {
|
||||
return $this->readLDAPData($record, 'mail');
|
||||
}
|
||||
|
||||
private function getLDAPUserData() {
|
||||
if ($this->ldapUserData === null) {
|
||||
$this->ldapUserData = $this->loadLDAPUserData();
|
||||
}
|
||||
|
||||
return $this->ldapUserData;
|
||||
}
|
||||
|
||||
private function readLDAPData(array $data, $key, $default = null) {
|
||||
$list = idx($data, $key);
|
||||
if ($list === null) {
|
||||
// At least in some cases (and maybe in all cases) the results from
|
||||
// ldap_search() are keyed in lowercase. If we missed on the first
|
||||
// try, retry with a lowercase key.
|
||||
$list = idx($data, phutil_utf8_strtolower($key));
|
||||
}
|
||||
|
||||
// NOTE: In most cases, the property is an array, like:
|
||||
//
|
||||
// array(
|
||||
// 'count' => 1,
|
||||
// 0 => 'actual-value-we-want',
|
||||
// )
|
||||
//
|
||||
// However, in at least the case of 'dn', the property is a bare string.
|
||||
|
||||
if (is_scalar($list) && strlen($list)) {
|
||||
return $list;
|
||||
} else if (is_array($list)) {
|
||||
return $list[0];
|
||||
} else {
|
||||
return $default;
|
||||
}
|
||||
}
|
||||
|
||||
private function formatLDAPAttributeSearch($attribute, $login_user) {
|
||||
// If the attribute contains the literal token "${login}", treat it as a
|
||||
// query and substitute the user's login name for the token.
|
||||
|
||||
if (strpos($attribute, '${login}') !== false) {
|
||||
$escaped_user = ldap_sprintf('%S', $login_user);
|
||||
$attribute = str_replace('${login}', $escaped_user, $attribute);
|
||||
return $attribute;
|
||||
}
|
||||
|
||||
// Otherwise, treat it as a simple attribute search.
|
||||
|
||||
return ldap_sprintf(
|
||||
'%Q=%S',
|
||||
$attribute,
|
||||
$login_user);
|
||||
}
|
||||
|
||||
private function loadLDAPUserData() {
|
||||
$conn = $this->establishConnection();
|
||||
|
||||
$login_user = $this->loginUsername;
|
||||
$login_pass = $this->loginPassword;
|
||||
|
||||
if ($this->shouldBindWithoutIdentity()) {
|
||||
$distinguished_name = null;
|
||||
$search_query = null;
|
||||
foreach ($this->searchAttributes as $attribute) {
|
||||
$search_query = $this->formatLDAPAttributeSearch(
|
||||
$attribute,
|
||||
$login_user);
|
||||
$record = $this->searchLDAPForRecord($search_query);
|
||||
if ($record) {
|
||||
$distinguished_name = $this->readLDAPData($record, 'dn');
|
||||
break;
|
||||
}
|
||||
}
|
||||
if ($distinguished_name === null) {
|
||||
throw new PhutilAuthCredentialException();
|
||||
}
|
||||
} else {
|
||||
$search_query = $this->formatLDAPAttributeSearch(
|
||||
head($this->searchAttributes),
|
||||
$login_user);
|
||||
if ($this->activeDirectoryDomain) {
|
||||
$distinguished_name = ldap_sprintf(
|
||||
'%s@%Q',
|
||||
$login_user,
|
||||
$this->activeDirectoryDomain);
|
||||
} else {
|
||||
$distinguished_name = ldap_sprintf(
|
||||
'%Q,%Q',
|
||||
$search_query,
|
||||
$this->baseDistinguishedName);
|
||||
}
|
||||
}
|
||||
|
||||
$this->bindLDAP($conn, $distinguished_name, $login_pass);
|
||||
|
||||
$result = $this->searchLDAPForRecord($search_query);
|
||||
if (!$result) {
|
||||
// This is unusual (since the bind succeeded) but we've seen it at least
|
||||
// once in the wild, where the anonymous user is allowed to search but
|
||||
// the credentialed user is not.
|
||||
|
||||
// If we don't have anonymous credentials, raise an explicit exception
|
||||
// here since we'll fail a typehint if we don't return an array anyway
|
||||
// and this is a more useful error.
|
||||
|
||||
// If we do have anonymous credentials, we'll rebind and try the search
|
||||
// again below. Doing this automatically means things work correctly more
|
||||
// often without requiring additional configuration.
|
||||
if (!$this->shouldBindWithoutIdentity()) {
|
||||
// No anonymous credentials, so we just fail here.
|
||||
throw new Exception(
|
||||
pht(
|
||||
'LDAP: Failed to retrieve record for user "%s" when searching. '.
|
||||
'Credentialed users may not be able to search your LDAP server. '.
|
||||
'Try configuring anonymous credentials or fully anonymous binds.',
|
||||
$login_user));
|
||||
} else {
|
||||
// Rebind as anonymous and try the search again.
|
||||
$user = $this->anonymousUsername;
|
||||
$pass = $this->anonymousPassword;
|
||||
$this->bindLDAP($conn, $user, $pass);
|
||||
|
||||
$result = $this->searchLDAPForRecord($search_query);
|
||||
if (!$result) {
|
||||
throw new Exception(
|
||||
pht(
|
||||
'LDAP: Failed to retrieve record for user "%s" when searching '.
|
||||
'with both user and anonymous credentials.',
|
||||
$login_user));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
private function establishConnection() {
|
||||
if (!$this->ldapConnection) {
|
||||
$host = $this->hostname;
|
||||
$port = $this->port;
|
||||
|
||||
$profiler = PhutilServiceProfiler::getInstance();
|
||||
$call_id = $profiler->beginServiceCall(
|
||||
array(
|
||||
'type' => 'ldap',
|
||||
'call' => 'connect',
|
||||
'host' => $host,
|
||||
'port' => $this->port,
|
||||
));
|
||||
|
||||
$conn = @ldap_connect($host, $this->port);
|
||||
|
||||
$profiler->endServiceCall(
|
||||
$call_id,
|
||||
array(
|
||||
'ok' => (bool)$conn,
|
||||
));
|
||||
|
||||
if (!$conn) {
|
||||
throw new Exception(
|
||||
pht('Unable to connect to LDAP server (%s:%d).', $host, $port));
|
||||
}
|
||||
|
||||
$options = array(
|
||||
LDAP_OPT_PROTOCOL_VERSION => (int)$this->ldapVersion,
|
||||
LDAP_OPT_REFERRALS => (int)$this->ldapReferrals,
|
||||
);
|
||||
|
||||
foreach ($options as $name => $value) {
|
||||
$ok = @ldap_set_option($conn, $name, $value);
|
||||
if (!$ok) {
|
||||
$this->raiseConnectionException(
|
||||
$conn,
|
||||
pht(
|
||||
"Unable to set LDAP option '%s' to value '%s'!",
|
||||
$name,
|
||||
$value));
|
||||
}
|
||||
}
|
||||
|
||||
if ($this->ldapStartTLS) {
|
||||
$profiler = PhutilServiceProfiler::getInstance();
|
||||
$call_id = $profiler->beginServiceCall(
|
||||
array(
|
||||
'type' => 'ldap',
|
||||
'call' => 'start-tls',
|
||||
));
|
||||
|
||||
// NOTE: This boils down to a function call to ldap_start_tls_s() in
|
||||
// C, which is a service call.
|
||||
$ok = @ldap_start_tls($conn);
|
||||
|
||||
$profiler->endServiceCall(
|
||||
$call_id,
|
||||
array());
|
||||
|
||||
if (!$ok) {
|
||||
$this->raiseConnectionException(
|
||||
$conn,
|
||||
pht('Unable to start TLS connection when connecting to LDAP.'));
|
||||
}
|
||||
}
|
||||
|
||||
if ($this->shouldBindWithoutIdentity()) {
|
||||
$user = $this->anonymousUsername;
|
||||
$pass = $this->anonymousPassword;
|
||||
$this->bindLDAP($conn, $user, $pass);
|
||||
}
|
||||
|
||||
$this->ldapConnection = $conn;
|
||||
}
|
||||
|
||||
return $this->ldapConnection;
|
||||
}
|
||||
|
||||
|
||||
private function searchLDAPForRecord($dn) {
|
||||
$conn = $this->establishConnection();
|
||||
|
||||
$results = $this->searchLDAP('%Q', $dn);
|
||||
|
||||
if (!$results) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (count($results) > 1) {
|
||||
throw new Exception(
|
||||
pht(
|
||||
'LDAP record query returned more than one result. The query must '.
|
||||
'uniquely identify a record.'));
|
||||
}
|
||||
|
||||
return head($results);
|
||||
}
|
||||
|
||||
public function searchLDAP($pattern /* ... */) {
|
||||
$args = func_get_args();
|
||||
$query = call_user_func_array('ldap_sprintf', $args);
|
||||
|
||||
$conn = $this->establishConnection();
|
||||
|
||||
$profiler = PhutilServiceProfiler::getInstance();
|
||||
$call_id = $profiler->beginServiceCall(
|
||||
array(
|
||||
'type' => 'ldap',
|
||||
'call' => 'search',
|
||||
'dn' => $this->baseDistinguishedName,
|
||||
'query' => $query,
|
||||
));
|
||||
|
||||
$result = @ldap_search($conn, $this->baseDistinguishedName, $query);
|
||||
|
||||
$profiler->endServiceCall($call_id, array());
|
||||
|
||||
if (!$result) {
|
||||
$this->raiseConnectionException(
|
||||
$conn,
|
||||
pht('LDAP search failed.'));
|
||||
}
|
||||
|
||||
$entries = @ldap_get_entries($conn, $result);
|
||||
|
||||
if (!$entries) {
|
||||
$this->raiseConnectionException(
|
||||
$conn,
|
||||
pht('Failed to get LDAP entries from search result.'));
|
||||
}
|
||||
|
||||
$results = array();
|
||||
for ($ii = 0; $ii < $entries['count']; $ii++) {
|
||||
$results[] = $entries[$ii];
|
||||
}
|
||||
|
||||
return $results;
|
||||
}
|
||||
|
||||
private function raiseConnectionException($conn, $message) {
|
||||
$errno = @ldap_errno($conn);
|
||||
$error = @ldap_error($conn);
|
||||
|
||||
// This is `LDAP_INVALID_CREDENTIALS`.
|
||||
if ($errno == 49) {
|
||||
throw new PhutilAuthCredentialException();
|
||||
}
|
||||
|
||||
if ($errno || $error) {
|
||||
$full_message = pht(
|
||||
"LDAP Exception: %s\nLDAP Error #%d: %s",
|
||||
$message,
|
||||
$errno,
|
||||
$error);
|
||||
} else {
|
||||
$full_message = pht(
|
||||
'LDAP Exception: %s',
|
||||
$message);
|
||||
}
|
||||
|
||||
throw new Exception($full_message);
|
||||
}
|
||||
|
||||
private function bindLDAP($conn, $user, PhutilOpaqueEnvelope $pass) {
|
||||
$profiler = PhutilServiceProfiler::getInstance();
|
||||
$call_id = $profiler->beginServiceCall(
|
||||
array(
|
||||
'type' => 'ldap',
|
||||
'call' => 'bind',
|
||||
'user' => $user,
|
||||
));
|
||||
|
||||
// NOTE: ldap_bind() dumps cleartext passwords into logs by default. Keep
|
||||
// it quiet.
|
||||
if (strlen($user)) {
|
||||
$ok = @ldap_bind($conn, $user, $pass->openEnvelope());
|
||||
} else {
|
||||
$ok = @ldap_bind($conn);
|
||||
}
|
||||
|
||||
$profiler->endServiceCall($call_id, array());
|
||||
|
||||
if (!$ok) {
|
||||
if (strlen($user)) {
|
||||
$this->raiseConnectionException(
|
||||
$conn,
|
||||
pht('Failed to bind to LDAP server (as user "%s").', $user));
|
||||
} else {
|
||||
$this->raiseConnectionException(
|
||||
$conn,
|
||||
pht('Failed to bind to LDAP server (without username).'));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Determine if this adapter should attempt to bind to the LDAP server
|
||||
* without a user identity.
|
||||
*
|
||||
* Generally, we can bind directly if we have a username/password, or if the
|
||||
* "Always Search" flag is set, indicating that the empty username and
|
||||
* password are sufficient.
|
||||
*
|
||||
* @return bool True if the adapter should perform binds without identity.
|
||||
*/
|
||||
private function shouldBindWithoutIdentity() {
|
||||
return $this->alwaysSearch || strlen($this->anonymousUsername);
|
||||
}
|
||||
|
||||
}
|
211
src/auth/PhutilOAuth1AuthAdapter.php
Normal file
211
src/auth/PhutilOAuth1AuthAdapter.php
Normal file
|
@ -0,0 +1,211 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* Abstract adapter for OAuth1 providers.
|
||||
*/
|
||||
abstract class PhutilOAuth1AuthAdapter extends PhutilAuthAdapter {
|
||||
|
||||
private $consumerKey;
|
||||
private $consumerSecret;
|
||||
private $token;
|
||||
private $tokenSecret;
|
||||
private $verifier;
|
||||
private $handshakeData;
|
||||
private $callbackURI;
|
||||
private $privateKey;
|
||||
|
||||
public function setPrivateKey(PhutilOpaqueEnvelope $private_key) {
|
||||
$this->privateKey = $private_key;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getPrivateKey() {
|
||||
return $this->privateKey;
|
||||
}
|
||||
|
||||
public function setCallbackURI($callback_uri) {
|
||||
$this->callbackURI = $callback_uri;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getCallbackURI() {
|
||||
return $this->callbackURI;
|
||||
}
|
||||
|
||||
public function setVerifier($verifier) {
|
||||
$this->verifier = $verifier;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getVerifier() {
|
||||
return $this->verifier;
|
||||
}
|
||||
|
||||
public function setConsumerSecret(PhutilOpaqueEnvelope $consumer_secret) {
|
||||
$this->consumerSecret = $consumer_secret;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getConsumerSecret() {
|
||||
return $this->consumerSecret;
|
||||
}
|
||||
|
||||
public function setConsumerKey($consumer_key) {
|
||||
$this->consumerKey = $consumer_key;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getConsumerKey() {
|
||||
return $this->consumerKey;
|
||||
}
|
||||
|
||||
public function setTokenSecret($token_secret) {
|
||||
$this->tokenSecret = $token_secret;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getTokenSecret() {
|
||||
return $this->tokenSecret;
|
||||
}
|
||||
|
||||
public function setToken($token) {
|
||||
$this->token = $token;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getToken() {
|
||||
return $this->token;
|
||||
}
|
||||
|
||||
protected function getHandshakeData() {
|
||||
if ($this->handshakeData === null) {
|
||||
$this->finishOAuthHandshake();
|
||||
}
|
||||
return $this->handshakeData;
|
||||
}
|
||||
|
||||
abstract protected function getRequestTokenURI();
|
||||
abstract protected function getAuthorizeTokenURI();
|
||||
abstract protected function getValidateTokenURI();
|
||||
|
||||
protected function getSignatureMethod() {
|
||||
return 'HMAC-SHA1';
|
||||
}
|
||||
|
||||
public function getContentSecurityPolicyFormActions() {
|
||||
return array(
|
||||
$this->getAuthorizeTokenURI(),
|
||||
);
|
||||
}
|
||||
|
||||
protected function newOAuth1Future($uri, $data = array()) {
|
||||
$future = id(new PhutilOAuth1Future($uri, $data))
|
||||
->setMethod('POST')
|
||||
->setSignatureMethod($this->getSignatureMethod());
|
||||
|
||||
$consumer_key = $this->getConsumerKey();
|
||||
if (strlen($consumer_key)) {
|
||||
$future->setConsumerKey($consumer_key);
|
||||
} else {
|
||||
throw new Exception(
|
||||
pht(
|
||||
'%s is required!',
|
||||
'setConsumerKey()'));
|
||||
}
|
||||
|
||||
$consumer_secret = $this->getConsumerSecret();
|
||||
if ($consumer_secret) {
|
||||
$future->setConsumerSecret($consumer_secret);
|
||||
}
|
||||
|
||||
if (strlen($this->getToken())) {
|
||||
$future->setToken($this->getToken());
|
||||
}
|
||||
|
||||
if (strlen($this->getTokenSecret())) {
|
||||
$future->setTokenSecret($this->getTokenSecret());
|
||||
}
|
||||
|
||||
if ($this->getPrivateKey()) {
|
||||
$future->setPrivateKey($this->getPrivateKey());
|
||||
}
|
||||
|
||||
return $future;
|
||||
}
|
||||
|
||||
public function getClientRedirectURI() {
|
||||
$request_token_uri = $this->getRequestTokenURI();
|
||||
|
||||
$future = $this->newOAuth1Future($request_token_uri);
|
||||
if (strlen($this->getCallbackURI())) {
|
||||
$future->setCallbackURI($this->getCallbackURI());
|
||||
}
|
||||
|
||||
list($body) = $future->resolvex();
|
||||
$data = id(new PhutilQueryStringParser())->parseQueryString($body);
|
||||
|
||||
// NOTE: Per the spec, this value MUST be the string 'true'.
|
||||
$confirmed = idx($data, 'oauth_callback_confirmed');
|
||||
if ($confirmed !== 'true') {
|
||||
throw new Exception(
|
||||
pht("Expected '%s' to be '%s'!", 'oauth_callback_confirmed', 'true'));
|
||||
}
|
||||
|
||||
$this->readTokenAndTokenSecret($data);
|
||||
|
||||
$authorize_token_uri = new PhutilURI($this->getAuthorizeTokenURI());
|
||||
$authorize_token_uri->setQueryParam('oauth_token', $this->getToken());
|
||||
|
||||
return (string)$authorize_token_uri;
|
||||
}
|
||||
|
||||
protected function finishOAuthHandshake() {
|
||||
$this->willFinishOAuthHandshake();
|
||||
|
||||
if (!$this->getToken()) {
|
||||
throw new Exception(pht('Expected token to finish OAuth handshake!'));
|
||||
}
|
||||
if (!$this->getVerifier()) {
|
||||
throw new Exception(pht('Expected verifier to finish OAuth handshake!'));
|
||||
}
|
||||
|
||||
$validate_uri = $this->getValidateTokenURI();
|
||||
$params = array(
|
||||
'oauth_verifier' => $this->getVerifier(),
|
||||
);
|
||||
|
||||
list($body) = $this->newOAuth1Future($validate_uri, $params)->resolvex();
|
||||
$data = id(new PhutilQueryStringParser())->parseQueryString($body);
|
||||
|
||||
$this->readTokenAndTokenSecret($data);
|
||||
|
||||
$this->handshakeData = $data;
|
||||
}
|
||||
|
||||
private function readTokenAndTokenSecret(array $data) {
|
||||
$token = idx($data, 'oauth_token');
|
||||
if (!$token) {
|
||||
throw new Exception(pht("Expected '%s' in response!", 'oauth_token'));
|
||||
}
|
||||
|
||||
$token_secret = idx($data, 'oauth_token_secret');
|
||||
if (!$token_secret) {
|
||||
throw new Exception(
|
||||
pht("Expected '%s' in response!", 'oauth_token_secret'));
|
||||
}
|
||||
|
||||
$this->setToken($token);
|
||||
$this->setTokenSecret($token_secret);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook that allows subclasses to take actions before the OAuth handshake
|
||||
* is completed.
|
||||
*/
|
||||
protected function willFinishOAuthHandshake() {
|
||||
return;
|
||||
}
|
||||
|
||||
}
|
229
src/auth/PhutilOAuthAuthAdapter.php
Normal file
229
src/auth/PhutilOAuthAuthAdapter.php
Normal file
|
@ -0,0 +1,229 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* Abstract adapter for OAuth2 providers.
|
||||
*/
|
||||
abstract class PhutilOAuthAuthAdapter extends PhutilAuthAdapter {
|
||||
|
||||
private $clientID;
|
||||
private $clientSecret;
|
||||
private $redirectURI;
|
||||
private $scope;
|
||||
private $state;
|
||||
private $code;
|
||||
|
||||
private $accessTokenData;
|
||||
private $oauthAccountData;
|
||||
|
||||
abstract protected function getAuthenticateBaseURI();
|
||||
abstract protected function getTokenBaseURI();
|
||||
abstract protected function loadOAuthAccountData();
|
||||
|
||||
public function getAuthenticateURI() {
|
||||
$uri = new PhutilURI($this->getAuthenticateBaseURI());
|
||||
$uri->setQueryParam('client_id', $this->getClientID());
|
||||
$uri->setQueryParam('scope', $this->getScope());
|
||||
$uri->setQueryParam('redirect_uri', $this->getRedirectURI());
|
||||
$uri->setQueryParam('state', $this->getState());
|
||||
|
||||
foreach ($this->getExtraAuthenticateParameters() as $key => $value) {
|
||||
$uri->setQueryParam($key, $value);
|
||||
}
|
||||
|
||||
return (string)$uri;
|
||||
}
|
||||
|
||||
public function getAdapterType() {
|
||||
$this_class = get_class($this);
|
||||
$type_name = str_replace('PhutilAuthAdapterOAuth', '', $this_class);
|
||||
return strtolower($type_name);
|
||||
}
|
||||
|
||||
public function setState($state) {
|
||||
$this->state = $state;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getState() {
|
||||
return $this->state;
|
||||
}
|
||||
|
||||
public function setCode($code) {
|
||||
$this->code = $code;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getCode() {
|
||||
return $this->code;
|
||||
}
|
||||
|
||||
public function setRedirectURI($redirect_uri) {
|
||||
$this->redirectURI = $redirect_uri;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getRedirectURI() {
|
||||
return $this->redirectURI;
|
||||
}
|
||||
|
||||
public function getExtraAuthenticateParameters() {
|
||||
return array();
|
||||
}
|
||||
|
||||
public function getExtraTokenParameters() {
|
||||
return array();
|
||||
}
|
||||
|
||||
public function getExtraRefreshParameters() {
|
||||
return array();
|
||||
}
|
||||
|
||||
public function setScope($scope) {
|
||||
$this->scope = $scope;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getScope() {
|
||||
return $this->scope;
|
||||
}
|
||||
|
||||
public function setClientSecret(PhutilOpaqueEnvelope $client_secret) {
|
||||
$this->clientSecret = $client_secret;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getClientSecret() {
|
||||
return $this->clientSecret;
|
||||
}
|
||||
|
||||
public function setClientID($client_id) {
|
||||
$this->clientID = $client_id;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getClientID() {
|
||||
return $this->clientID;
|
||||
}
|
||||
|
||||
public function getAccessToken() {
|
||||
return $this->getAccessTokenData('access_token');
|
||||
}
|
||||
|
||||
public function getAccessTokenExpires() {
|
||||
return $this->getAccessTokenData('expires_epoch');
|
||||
}
|
||||
|
||||
public function getRefreshToken() {
|
||||
return $this->getAccessTokenData('refresh_token');
|
||||
}
|
||||
|
||||
protected function getAccessTokenData($key, $default = null) {
|
||||
if ($this->accessTokenData === null) {
|
||||
$this->accessTokenData = $this->loadAccessTokenData();
|
||||
}
|
||||
|
||||
return idx($this->accessTokenData, $key, $default);
|
||||
}
|
||||
|
||||
public function supportsTokenRefresh() {
|
||||
return false;
|
||||
}
|
||||
|
||||
public function refreshAccessToken($refresh_token) {
|
||||
$this->accessTokenData = $this->loadRefreshTokenData($refresh_token);
|
||||
return $this;
|
||||
}
|
||||
|
||||
protected function loadRefreshTokenData($refresh_token) {
|
||||
$params = array(
|
||||
'refresh_token' => $refresh_token,
|
||||
) + $this->getExtraRefreshParameters();
|
||||
|
||||
// NOTE: Make sure we return the refresh_token so that subsequent
|
||||
// calls to getRefreshToken() return it; providers normally do not echo
|
||||
// it back for token refresh requests.
|
||||
|
||||
return $this->makeTokenRequest($params) + array(
|
||||
'refresh_token' => $refresh_token,
|
||||
);
|
||||
}
|
||||
|
||||
protected function loadAccessTokenData() {
|
||||
$code = $this->getCode();
|
||||
if (!$code) {
|
||||
throw new PhutilInvalidStateException('setCode');
|
||||
}
|
||||
|
||||
$params = array(
|
||||
'code' => $this->getCode(),
|
||||
) + $this->getExtraTokenParameters();
|
||||
|
||||
return $this->makeTokenRequest($params);
|
||||
}
|
||||
|
||||
private function makeTokenRequest(array $params) {
|
||||
$uri = $this->getTokenBaseURI();
|
||||
$query_data = array(
|
||||
'client_id' => $this->getClientID(),
|
||||
'client_secret' => $this->getClientSecret()->openEnvelope(),
|
||||
'redirect_uri' => $this->getRedirectURI(),
|
||||
) + $params;
|
||||
|
||||
$future = new HTTPSFuture($uri, $query_data);
|
||||
$future->setMethod('POST');
|
||||
list($body) = $future->resolvex();
|
||||
|
||||
$data = $this->readAccessTokenResponse($body);
|
||||
|
||||
if (isset($data['expires_in'])) {
|
||||
$data['expires_epoch'] = $data['expires_in'];
|
||||
} else if (isset($data['expires'])) {
|
||||
$data['expires_epoch'] = $data['expires'];
|
||||
}
|
||||
|
||||
// If we got some "expires" value back, interpret it as an epoch timestamp
|
||||
// if it's after the year 2010 and as a relative number of seconds
|
||||
// otherwise.
|
||||
if (isset($data['expires_epoch'])) {
|
||||
if ($data['expires_epoch'] < (60 * 60 * 24 * 365 * 40)) {
|
||||
$data['expires_epoch'] += time();
|
||||
}
|
||||
}
|
||||
|
||||
if (isset($data['error'])) {
|
||||
throw new Exception(pht('Access token error: %s', $data['error']));
|
||||
}
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
protected function readAccessTokenResponse($body) {
|
||||
// NOTE: Most providers either return JSON or HTTP query strings, so try
|
||||
// both mechanisms. If your provider does something else, override this
|
||||
// method.
|
||||
|
||||
$data = json_decode($body, true);
|
||||
|
||||
if (!is_array($data)) {
|
||||
$data = array();
|
||||
parse_str($body, $data);
|
||||
}
|
||||
|
||||
if (empty($data['access_token']) &&
|
||||
empty($data['error'])) {
|
||||
throw new Exception(
|
||||
pht('Failed to decode OAuth access token response: %s', $body));
|
||||
}
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
protected function getOAuthAccountData($key, $default = null) {
|
||||
if ($this->oauthAccountData === null) {
|
||||
$this->oauthAccountData = $this->loadOAuthAccountData();
|
||||
}
|
||||
|
||||
return idx($this->oauthAccountData, $key, $default);
|
||||
}
|
||||
|
||||
}
|
102
src/auth/PhutilPhabricatorAuthAdapter.php
Normal file
102
src/auth/PhutilPhabricatorAuthAdapter.php
Normal file
|
@ -0,0 +1,102 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* Authentication adapter for Phabricator OAuth2.
|
||||
*/
|
||||
final class PhutilPhabricatorAuthAdapter extends PhutilOAuthAuthAdapter {
|
||||
|
||||
private $phabricatorBaseURI;
|
||||
private $adapterDomain;
|
||||
|
||||
public function setPhabricatorBaseURI($uri) {
|
||||
$this->phabricatorBaseURI = $uri;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getPhabricatorBaseURI() {
|
||||
return $this->phabricatorBaseURI;
|
||||
}
|
||||
|
||||
public function getAdapterDomain() {
|
||||
return $this->adapterDomain;
|
||||
}
|
||||
|
||||
public function setAdapterDomain($domain) {
|
||||
$this->adapterDomain = $domain;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getAdapterType() {
|
||||
return 'phabricator';
|
||||
}
|
||||
|
||||
public function getAccountID() {
|
||||
return $this->getOAuthAccountData('phid');
|
||||
}
|
||||
|
||||
public function getAccountEmail() {
|
||||
return $this->getOAuthAccountData('primaryEmail');
|
||||
}
|
||||
|
||||
public function getAccountName() {
|
||||
return $this->getOAuthAccountData('userName');
|
||||
}
|
||||
|
||||
public function getAccountImageURI() {
|
||||
return $this->getOAuthAccountData('image');
|
||||
}
|
||||
|
||||
public function getAccountURI() {
|
||||
return $this->getOAuthAccountData('uri');
|
||||
}
|
||||
|
||||
public function getAccountRealName() {
|
||||
return $this->getOAuthAccountData('realName');
|
||||
}
|
||||
|
||||
protected function getAuthenticateBaseURI() {
|
||||
return $this->getPhabricatorURI('oauthserver/auth/');
|
||||
}
|
||||
|
||||
protected function getTokenBaseURI() {
|
||||
return $this->getPhabricatorURI('oauthserver/token/');
|
||||
}
|
||||
|
||||
public function getScope() {
|
||||
return '';
|
||||
}
|
||||
|
||||
public function getExtraAuthenticateParameters() {
|
||||
return array(
|
||||
'response_type' => 'code',
|
||||
);
|
||||
}
|
||||
|
||||
public function getExtraTokenParameters() {
|
||||
return array(
|
||||
'grant_type' => 'authorization_code',
|
||||
);
|
||||
}
|
||||
|
||||
protected function loadOAuthAccountData() {
|
||||
$uri = id(new PhutilURI($this->getPhabricatorURI('api/user.whoami')))
|
||||
->setQueryParam('access_token', $this->getAccessToken());
|
||||
list($body) = id(new HTTPSFuture($uri))->resolvex();
|
||||
|
||||
try {
|
||||
$data = phutil_json_decode($body);
|
||||
return $data['result'];
|
||||
} catch (PhutilJSONParserException $ex) {
|
||||
throw new Exception(
|
||||
pht(
|
||||
'Expected valid JSON response from Phabricator %s request.',
|
||||
'user.whoami'),
|
||||
$ex);
|
||||
}
|
||||
}
|
||||
|
||||
private function getPhabricatorURI($path) {
|
||||
return rtrim($this->phabricatorBaseURI, '/').'/'.ltrim($path, '/');
|
||||
}
|
||||
|
||||
}
|
61
src/auth/PhutilSlackAuthAdapter.php
Normal file
61
src/auth/PhutilSlackAuthAdapter.php
Normal file
|
@ -0,0 +1,61 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* Authentication adapter for Slack OAuth2.
|
||||
*/
|
||||
final class PhutilSlackAuthAdapter extends PhutilOAuthAuthAdapter {
|
||||
|
||||
public function getAdapterType() {
|
||||
return 'Slack';
|
||||
}
|
||||
|
||||
public function getAdapterDomain() {
|
||||
return 'slack.com';
|
||||
}
|
||||
|
||||
public function getAccountID() {
|
||||
$user = $this->getOAuthAccountData('user');
|
||||
return idx($user, 'id');
|
||||
}
|
||||
|
||||
public function getAccountEmail() {
|
||||
$user = $this->getOAuthAccountData('user');
|
||||
return idx($user, 'email');
|
||||
}
|
||||
|
||||
public function getAccountImageURI() {
|
||||
$user = $this->getOAuthAccountData('user');
|
||||
return idx($user, 'image_512');
|
||||
}
|
||||
|
||||
public function getAccountRealName() {
|
||||
$user = $this->getOAuthAccountData('user');
|
||||
return idx($user, 'name');
|
||||
}
|
||||
|
||||
protected function getAuthenticateBaseURI() {
|
||||
return 'https://slack.com/oauth/authorize';
|
||||
}
|
||||
|
||||
protected function getTokenBaseURI() {
|
||||
return 'https://slack.com/api/oauth.access';
|
||||
}
|
||||
|
||||
public function getScope() {
|
||||
return 'identity.basic,identity.team,identity.avatar';
|
||||
}
|
||||
|
||||
public function getExtraAuthenticateParameters() {
|
||||
return array(
|
||||
'response_type' => 'code',
|
||||
);
|
||||
}
|
||||
|
||||
protected function loadOAuthAccountData() {
|
||||
return id(new PhutilSlackFuture())
|
||||
->setAccessToken($this->getAccessToken())
|
||||
->setRawSlackQuery('users.identity')
|
||||
->resolve();
|
||||
}
|
||||
|
||||
}
|
76
src/auth/PhutilTwitchAuthAdapter.php
Normal file
76
src/auth/PhutilTwitchAuthAdapter.php
Normal file
|
@ -0,0 +1,76 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* Authentication adapter for Twitch.tv OAuth2.
|
||||
*/
|
||||
final class PhutilTwitchAuthAdapter extends PhutilOAuthAuthAdapter {
|
||||
|
||||
public function getAdapterType() {
|
||||
return 'twitch';
|
||||
}
|
||||
|
||||
public function getAdapterDomain() {
|
||||
return 'twitch.tv';
|
||||
}
|
||||
|
||||
public function getAccountID() {
|
||||
return $this->getOAuthAccountData('_id');
|
||||
}
|
||||
|
||||
public function getAccountEmail() {
|
||||
return $this->getOAuthAccountData('email');
|
||||
}
|
||||
|
||||
public function getAccountName() {
|
||||
return $this->getOAuthAccountData('name');
|
||||
}
|
||||
|
||||
public function getAccountImageURI() {
|
||||
return $this->getOAuthAccountData('logo');
|
||||
}
|
||||
|
||||
public function getAccountURI() {
|
||||
$name = $this->getAccountName();
|
||||
if ($name) {
|
||||
return 'http://www.twitch.tv/'.$name;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public function getAccountRealName() {
|
||||
return $this->getOAuthAccountData('display_name');
|
||||
}
|
||||
|
||||
protected function getAuthenticateBaseURI() {
|
||||
return 'https://api.twitch.tv/kraken/oauth2/authorize';
|
||||
}
|
||||
|
||||
protected function getTokenBaseURI() {
|
||||
return 'https://api.twitch.tv/kraken/oauth2/token';
|
||||
}
|
||||
|
||||
public function getScope() {
|
||||
return 'user_read';
|
||||
}
|
||||
|
||||
public function getExtraAuthenticateParameters() {
|
||||
return array(
|
||||
'response_type' => 'code',
|
||||
);
|
||||
}
|
||||
|
||||
public function getExtraTokenParameters() {
|
||||
return array(
|
||||
'grant_type' => 'authorization_code',
|
||||
);
|
||||
}
|
||||
|
||||
protected function loadOAuthAccountData() {
|
||||
return id(new PhutilTwitchFuture())
|
||||
->setClientID($this->getClientID())
|
||||
->setAccessToken($this->getAccessToken())
|
||||
->setRawTwitchQuery('user')
|
||||
->resolve();
|
||||
}
|
||||
|
||||
}
|
73
src/auth/PhutilTwitterAuthAdapter.php
Normal file
73
src/auth/PhutilTwitterAuthAdapter.php
Normal file
|
@ -0,0 +1,73 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* Authentication adapter for Twitter OAuth1.
|
||||
*/
|
||||
final class PhutilTwitterAuthAdapter extends PhutilOAuth1AuthAdapter {
|
||||
|
||||
private $userInfo;
|
||||
|
||||
public function getAccountID() {
|
||||
return idx($this->getHandshakeData(), 'user_id');
|
||||
}
|
||||
|
||||
public function getAccountName() {
|
||||
return idx($this->getHandshakeData(), 'screen_name');
|
||||
}
|
||||
|
||||
public function getAccountURI() {
|
||||
$name = $this->getAccountName();
|
||||
if (strlen($name)) {
|
||||
return 'https://twitter.com/'.$name;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public function getAccountImageURI() {
|
||||
$info = $this->getUserInfo();
|
||||
return idx($info, 'profile_image_url');
|
||||
}
|
||||
|
||||
public function getAccountRealName() {
|
||||
$info = $this->getUserInfo();
|
||||
return idx($info, 'name');
|
||||
}
|
||||
|
||||
public function getAdapterType() {
|
||||
return 'twitter';
|
||||
}
|
||||
|
||||
public function getAdapterDomain() {
|
||||
return 'twitter.com';
|
||||
}
|
||||
|
||||
protected function getRequestTokenURI() {
|
||||
return 'https://api.twitter.com/oauth/request_token';
|
||||
}
|
||||
|
||||
protected function getAuthorizeTokenURI() {
|
||||
return 'https://api.twitter.com/oauth/authorize';
|
||||
}
|
||||
|
||||
protected function getValidateTokenURI() {
|
||||
return 'https://api.twitter.com/oauth/access_token';
|
||||
}
|
||||
|
||||
private function getUserInfo() {
|
||||
if ($this->userInfo === null) {
|
||||
$uri = new PhutilURI('https://api.twitter.com/1.1/users/show.json');
|
||||
$uri->setQueryParams(
|
||||
array(
|
||||
'user_id' => $this->getAccountID(),
|
||||
));
|
||||
|
||||
$data = $this->newOAuth1Future($uri)
|
||||
->setMethod('GET')
|
||||
->resolveJSON();
|
||||
|
||||
$this->userInfo = $data;
|
||||
}
|
||||
return $this->userInfo;
|
||||
}
|
||||
|
||||
}
|
73
src/auth/PhutilWordPressAuthAdapter.php
Normal file
73
src/auth/PhutilWordPressAuthAdapter.php
Normal file
|
@ -0,0 +1,73 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* Authentication adapter for WordPress.com OAuth2.
|
||||
*/
|
||||
final class PhutilWordPressAuthAdapter extends PhutilOAuthAuthAdapter {
|
||||
|
||||
public function getAdapterType() {
|
||||
return 'wordpress';
|
||||
}
|
||||
|
||||
public function getAdapterDomain() {
|
||||
return 'wordpress.com';
|
||||
}
|
||||
|
||||
public function getAccountID() {
|
||||
return $this->getOAuthAccountData('ID');
|
||||
}
|
||||
|
||||
public function getAccountEmail() {
|
||||
return $this->getOAuthAccountData('email');
|
||||
}
|
||||
|
||||
public function getAccountName() {
|
||||
return $this->getOAuthAccountData('username');
|
||||
}
|
||||
|
||||
public function getAccountImageURI() {
|
||||
return $this->getOAuthAccountData('avatar_URL');
|
||||
}
|
||||
|
||||
public function getAccountURI() {
|
||||
return $this->getOAuthAccountData('profile_URL');
|
||||
}
|
||||
|
||||
public function getAccountRealName() {
|
||||
return $this->getOAuthAccountData('display_name');
|
||||
}
|
||||
|
||||
protected function getAuthenticateBaseURI() {
|
||||
return 'https://public-api.wordpress.com/oauth2/authorize';
|
||||
}
|
||||
|
||||
protected function getTokenBaseURI() {
|
||||
return 'https://public-api.wordpress.com/oauth2/token';
|
||||
}
|
||||
|
||||
public function getScope() {
|
||||
return 'user_read';
|
||||
}
|
||||
|
||||
public function getExtraAuthenticateParameters() {
|
||||
return array(
|
||||
'response_type' => 'code',
|
||||
'blog_id' => 0,
|
||||
);
|
||||
}
|
||||
|
||||
public function getExtraTokenParameters() {
|
||||
return array(
|
||||
'grant_type' => 'authorization_code',
|
||||
);
|
||||
}
|
||||
|
||||
protected function loadOAuthAccountData() {
|
||||
return id(new PhutilWordPressFuture())
|
||||
->setClientID($this->getClientID())
|
||||
->setAccessToken($this->getAccessToken())
|
||||
->setRawWordPressQuery('/me/')
|
||||
->resolve();
|
||||
}
|
||||
|
||||
}
|
6
src/auth/exception/PhutilAuthConfigurationException.php
Normal file
6
src/auth/exception/PhutilAuthConfigurationException.php
Normal file
|
@ -0,0 +1,6 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* Authentication is not configured correctly.
|
||||
*/
|
||||
final class PhutilAuthConfigurationException extends PhutilAuthException {}
|
6
src/auth/exception/PhutilAuthCredentialException.php
Normal file
6
src/auth/exception/PhutilAuthCredentialException.php
Normal file
|
@ -0,0 +1,6 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* The user provided invalid credentials.
|
||||
*/
|
||||
final class PhutilAuthCredentialException extends PhutilAuthException {}
|
7
src/auth/exception/PhutilAuthException.php
Normal file
7
src/auth/exception/PhutilAuthException.php
Normal file
|
@ -0,0 +1,7 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* Abstract exception class for errors encountered during authentication
|
||||
* workflows.
|
||||
*/
|
||||
abstract class PhutilAuthException extends Exception {}
|
14
src/auth/exception/PhutilAuthUserAbortedException.php
Normal file
14
src/auth/exception/PhutilAuthUserAbortedException.php
Normal file
|
@ -0,0 +1,14 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* The user aborted the authentication workflow, by clicking "Cancel" or "Deny"
|
||||
* or taking some similar action.
|
||||
*
|
||||
* For example, in OAuth/OAuth2 workflows, the authentication provider
|
||||
* generally presents the user with a confirmation dialog with two options,
|
||||
* "Approve" and "Deny".
|
||||
*
|
||||
* If an adapter detects that the user has explicitly bailed out of the
|
||||
* workflow, it should throw this exception.
|
||||
*/
|
||||
final class PhutilAuthUserAbortedException extends PhutilAuthException {}
|
97
src/cache/PhutilAPCKeyValueCache.php
vendored
Normal file
97
src/cache/PhutilAPCKeyValueCache.php
vendored
Normal file
|
@ -0,0 +1,97 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* Interface to the APC key-value cache. This is a very high-performance cache
|
||||
* which is local to the current machine.
|
||||
*/
|
||||
final class PhutilAPCKeyValueCache extends PhutilKeyValueCache {
|
||||
|
||||
|
||||
/* -( Key-Value Cache Implementation )------------------------------------- */
|
||||
|
||||
|
||||
public function isAvailable() {
|
||||
return (function_exists('apc_fetch') || function_exists('apcu_fetch')) &&
|
||||
ini_get('apc.enabled') &&
|
||||
(ini_get('apc.enable_cli') || php_sapi_name() != 'cli');
|
||||
}
|
||||
|
||||
public function getKeys(array $keys, $ttl = null) {
|
||||
static $is_apcu;
|
||||
if ($is_apcu === null) {
|
||||
$is_apcu = self::isAPCu();
|
||||
}
|
||||
|
||||
$results = array();
|
||||
$fetched = false;
|
||||
foreach ($keys as $key) {
|
||||
if ($is_apcu) {
|
||||
$result = apcu_fetch($key, $fetched);
|
||||
} else {
|
||||
$result = apc_fetch($key, $fetched);
|
||||
}
|
||||
|
||||
if ($fetched) {
|
||||
$results[$key] = $result;
|
||||
}
|
||||
}
|
||||
return $results;
|
||||
}
|
||||
|
||||
public function setKeys(array $keys, $ttl = null) {
|
||||
static $is_apcu;
|
||||
if ($is_apcu === null) {
|
||||
$is_apcu = self::isAPCu();
|
||||
}
|
||||
|
||||
// NOTE: Although modern APC supports passing an array to `apc_store()`,
|
||||
// it is not supported by older version of APC or by HPHP.
|
||||
|
||||
foreach ($keys as $key => $value) {
|
||||
if ($is_apcu) {
|
||||
apcu_store($key, $value, $ttl);
|
||||
} else {
|
||||
apc_store($key, $value, $ttl);
|
||||
}
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function deleteKeys(array $keys) {
|
||||
static $is_apcu;
|
||||
if ($is_apcu === null) {
|
||||
$is_apcu = self::isAPCu();
|
||||
}
|
||||
|
||||
foreach ($keys as $key) {
|
||||
if ($is_apcu) {
|
||||
apcu_delete($key);
|
||||
} else {
|
||||
apc_delete($key);
|
||||
}
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function destroyCache() {
|
||||
static $is_apcu;
|
||||
if ($is_apcu === null) {
|
||||
$is_apcu = self::isAPCu();
|
||||
}
|
||||
|
||||
if ($is_apcu) {
|
||||
apcu_clear_cache();
|
||||
} else {
|
||||
apc_clear_cache('user');
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
private static function isAPCu() {
|
||||
return function_exists('apcu_fetch');
|
||||
}
|
||||
|
||||
}
|
244
src/cache/PhutilDirectoryKeyValueCache.php
vendored
Normal file
244
src/cache/PhutilDirectoryKeyValueCache.php
vendored
Normal file
|
@ -0,0 +1,244 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* Interface to a directory-based disk cache. Storage persists across requests.
|
||||
*
|
||||
* This cache is very very slow, and most suitable for command line scripts
|
||||
* which need to build large caches derived from sources like working copies
|
||||
* (for example, Diviner). This cache performs better for large amounts of
|
||||
* data than @{class:PhutilOnDiskKeyValueCache} because each key is serialized
|
||||
* individually, but this comes at the cost of having even slower reads and
|
||||
* writes.
|
||||
*
|
||||
* In addition to having slow reads and writes, this entire cache locks for
|
||||
* any read or write activity.
|
||||
*
|
||||
* Keys for this cache treat the character "/" specially, and encode it as
|
||||
* a new directory on disk. This can help keep the cache organized and keep the
|
||||
* number of items in any single directory under control, by using keys like
|
||||
* "ab/cd/efghijklmn".
|
||||
*
|
||||
* @task kvimpl Key-Value Cache Implementation
|
||||
* @task storage Cache Storage
|
||||
*/
|
||||
final class PhutilDirectoryKeyValueCache extends PhutilKeyValueCache {
|
||||
|
||||
private $lock;
|
||||
private $cacheDirectory;
|
||||
|
||||
|
||||
/* -( Key-Value Cache Implementation )------------------------------------- */
|
||||
|
||||
|
||||
public function isAvailable() {
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
public function getKeys(array $keys) {
|
||||
$this->validateKeys($keys);
|
||||
|
||||
try {
|
||||
$this->lockCache();
|
||||
} catch (PhutilLockException $ex) {
|
||||
return array();
|
||||
}
|
||||
|
||||
$now = time();
|
||||
|
||||
$results = array();
|
||||
foreach ($keys as $key) {
|
||||
$key_file = $this->getKeyFile($key);
|
||||
try {
|
||||
$data = Filesystem::readFile($key_file);
|
||||
} catch (FilesystemException $ex) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$data = unserialize($data);
|
||||
if (!$data) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (isset($data['ttl']) && $data['ttl'] < $now) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$results[$key] = $data['value'];
|
||||
}
|
||||
|
||||
$this->unlockCache();
|
||||
|
||||
return $results;
|
||||
}
|
||||
|
||||
|
||||
public function setKeys(array $keys, $ttl = null) {
|
||||
$this->validateKeys(array_keys($keys));
|
||||
|
||||
$this->lockCache(15);
|
||||
|
||||
if ($ttl) {
|
||||
$ttl_epoch = time() + $ttl;
|
||||
} else {
|
||||
$ttl_epoch = null;
|
||||
}
|
||||
|
||||
foreach ($keys as $key => $value) {
|
||||
$dict = array(
|
||||
'value' => $value,
|
||||
);
|
||||
if ($ttl_epoch) {
|
||||
$dict['ttl'] = $ttl_epoch;
|
||||
}
|
||||
|
||||
try {
|
||||
$key_file = $this->getKeyFile($key);
|
||||
$key_dir = dirname($key_file);
|
||||
if (!Filesystem::pathExists($key_dir)) {
|
||||
Filesystem::createDirectory(
|
||||
$key_dir,
|
||||
$mask = 0755,
|
||||
$recursive = true);
|
||||
}
|
||||
|
||||
$new_file = $key_file.'.new';
|
||||
Filesystem::writeFile($new_file, serialize($dict));
|
||||
Filesystem::rename($new_file, $key_file);
|
||||
} catch (FilesystemException $ex) {
|
||||
phlog($ex);
|
||||
}
|
||||
}
|
||||
|
||||
$this->unlockCache();
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
|
||||
public function deleteKeys(array $keys) {
|
||||
$this->validateKeys($keys);
|
||||
|
||||
$this->lockCache(15);
|
||||
|
||||
foreach ($keys as $key) {
|
||||
$path = $this->getKeyFile($key);
|
||||
Filesystem::remove($path);
|
||||
|
||||
// If removing this key leaves the directory empty, clean it up. Then
|
||||
// clean up any empty parent directories.
|
||||
$path = dirname($path);
|
||||
do {
|
||||
if (!Filesystem::isDescendant($path, $this->getCacheDirectory())) {
|
||||
break;
|
||||
}
|
||||
if (Filesystem::listDirectory($path, true)) {
|
||||
break;
|
||||
}
|
||||
Filesystem::remove($path);
|
||||
$path = dirname($path);
|
||||
} while (true);
|
||||
}
|
||||
|
||||
$this->unlockCache();
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
|
||||
public function destroyCache() {
|
||||
Filesystem::remove($this->getCacheDirectory());
|
||||
return $this;
|
||||
}
|
||||
|
||||
|
||||
/* -( Cache Storage )------------------------------------------------------ */
|
||||
|
||||
|
||||
/**
|
||||
* @task storage
|
||||
*/
|
||||
public function setCacheDirectory($directory) {
|
||||
$this->cacheDirectory = rtrim($directory, '/').'/';
|
||||
return $this;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @task storage
|
||||
*/
|
||||
private function getCacheDirectory() {
|
||||
if (!$this->cacheDirectory) {
|
||||
throw new PhutilInvalidStateException('setCacheDirectory');
|
||||
}
|
||||
return $this->cacheDirectory;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @task storage
|
||||
*/
|
||||
private function getKeyFile($key) {
|
||||
// Colon is a drive separator on Windows.
|
||||
$key = str_replace(':', '_', $key);
|
||||
|
||||
// NOTE: We add ".cache" to each file so we don't get a collision if you
|
||||
// set the keys "a" and "a/b". Without ".cache", the file "a" would need
|
||||
// to be both a file and a directory.
|
||||
return $this->getCacheDirectory().$key.'.cache';
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @task storage
|
||||
*/
|
||||
private function validateKeys(array $keys) {
|
||||
foreach ($keys as $key) {
|
||||
// NOTE: Use of "." is reserved for ".lock", "key.new" and "key.cache".
|
||||
// Use of "_" is reserved for converting ":".
|
||||
if (!preg_match('@^[a-zA-Z0-9/:-]+$@', $key)) {
|
||||
throw new Exception(
|
||||
pht(
|
||||
"Invalid key '%s': directory caches may only contain letters, ".
|
||||
"numbers, hyphen, colon and slash.",
|
||||
$key));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @task storage
|
||||
*/
|
||||
private function lockCache($wait = 0) {
|
||||
if ($this->lock) {
|
||||
throw new Exception(
|
||||
pht(
|
||||
'Trying to %s with a lock!',
|
||||
__FUNCTION__.'()'));
|
||||
}
|
||||
|
||||
if (!Filesystem::pathExists($this->getCacheDirectory())) {
|
||||
Filesystem::createDirectory($this->getCacheDirectory(), 0755, true);
|
||||
}
|
||||
|
||||
$lock = PhutilFileLock::newForPath($this->getCacheDirectory().'.lock');
|
||||
$lock->lock($wait);
|
||||
|
||||
$this->lock = $lock;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @task storage
|
||||
*/
|
||||
private function unlockCache() {
|
||||
if (!$this->lock) {
|
||||
throw new PhutilInvalidStateException('lockCache');
|
||||
}
|
||||
|
||||
$this->lock->unlock();
|
||||
$this->lock = null;
|
||||
}
|
||||
|
||||
}
|
118
src/cache/PhutilInRequestKeyValueCache.php
vendored
Normal file
118
src/cache/PhutilInRequestKeyValueCache.php
vendored
Normal file
|
@ -0,0 +1,118 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* Key-value cache implemented in the current request. All storage is local
|
||||
* to this request (i.e., the current page) and destroyed after the request
|
||||
* exits. This means the first request to this cache for a given key on a page
|
||||
* will ALWAYS miss.
|
||||
*
|
||||
* This cache exists mostly to support unit tests. In a well-designed
|
||||
* applications, you generally should not be fetching the same data over and
|
||||
* over again in one request, so this cache should be of limited utility.
|
||||
* If using this cache improves application performance, especially if it
|
||||
* improves it significantly, it may indicate an architectural problem in your
|
||||
* application.
|
||||
*/
|
||||
final class PhutilInRequestKeyValueCache extends PhutilKeyValueCache {
|
||||
|
||||
private $cache = array();
|
||||
private $ttl = array();
|
||||
private $limit = 0;
|
||||
|
||||
|
||||
/**
|
||||
* Set a limit on the number of keys this cache may contain.
|
||||
*
|
||||
* When too many keys are inserted, the oldest keys are removed from the
|
||||
* cache. Setting a limit of `0` disables the cache.
|
||||
*
|
||||
* @param int Maximum number of items to store in the cache.
|
||||
* @return this
|
||||
*/
|
||||
public function setLimit($limit) {
|
||||
$this->limit = $limit;
|
||||
return $this;
|
||||
}
|
||||
|
||||
|
||||
/* -( Key-Value Cache Implementation )------------------------------------- */
|
||||
|
||||
|
||||
public function isAvailable() {
|
||||
return true;
|
||||
}
|
||||
|
||||
public function getKeys(array $keys) {
|
||||
$results = array();
|
||||
$now = time();
|
||||
foreach ($keys as $key) {
|
||||
if (!isset($this->cache[$key]) && !array_key_exists($key, $this->cache)) {
|
||||
continue;
|
||||
}
|
||||
if (isset($this->ttl[$key]) && ($this->ttl[$key] < $now)) {
|
||||
continue;
|
||||
}
|
||||
$results[$key] = $this->cache[$key];
|
||||
}
|
||||
|
||||
return $results;
|
||||
}
|
||||
|
||||
public function setKeys(array $keys, $ttl = null) {
|
||||
|
||||
foreach ($keys as $key => $value) {
|
||||
$this->cache[$key] = $value;
|
||||
}
|
||||
|
||||
if ($ttl) {
|
||||
$end = time() + $ttl;
|
||||
foreach ($keys as $key => $value) {
|
||||
$this->ttl[$key] = $end;
|
||||
}
|
||||
} else {
|
||||
foreach ($keys as $key => $value) {
|
||||
unset($this->ttl[$key]);
|
||||
}
|
||||
}
|
||||
|
||||
if ($this->limit) {
|
||||
$count = count($this->cache);
|
||||
if ($count > $this->limit) {
|
||||
$remove = array();
|
||||
foreach ($this->cache as $key => $value) {
|
||||
$remove[] = $key;
|
||||
|
||||
$count--;
|
||||
if ($count <= $this->limit) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
$this->deleteKeys($remove);
|
||||
}
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function deleteKeys(array $keys) {
|
||||
foreach ($keys as $key) {
|
||||
unset($this->cache[$key]);
|
||||
unset($this->ttl[$key]);
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getAllKeys() {
|
||||
return $this->cache;
|
||||
}
|
||||
|
||||
public function destroyCache() {
|
||||
$this->cache = array();
|
||||
$this->ttl = array();
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
}
|
121
src/cache/PhutilKeyValueCache.php
vendored
Normal file
121
src/cache/PhutilKeyValueCache.php
vendored
Normal file
|
@ -0,0 +1,121 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* Interface to a key-value cache like Memcache or APC. This class provides a
|
||||
* uniform interface to multiple different key-value caches and integration
|
||||
* with PhutilServiceProfiler.
|
||||
*
|
||||
* @task kvimpl Key-Value Cache Implementation
|
||||
*/
|
||||
abstract class PhutilKeyValueCache extends Phobject {
|
||||
|
||||
|
||||
/* -( Key-Value Cache Implementation )------------------------------------- */
|
||||
|
||||
|
||||
/**
|
||||
* Determine if the cache is available. For example, the APC cache tests if
|
||||
* APC is installed. If this method returns false, the cache is not
|
||||
* operational and can not be used.
|
||||
*
|
||||
* @return bool True if the cache can be used.
|
||||
* @task kvimpl
|
||||
*/
|
||||
public function isAvailable() {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Get a single key from cache. See @{method:getKeys} to get multiple keys at
|
||||
* once.
|
||||
*
|
||||
* @param string Key to retrieve.
|
||||
* @param wild Optional value to return if the key is not found. By
|
||||
* default, returns null.
|
||||
* @return wild Cache value (on cache hit) or default value (on cache
|
||||
* miss).
|
||||
* @task kvimpl
|
||||
*/
|
||||
final public function getKey($key, $default = null) {
|
||||
$map = $this->getKeys(array($key));
|
||||
return idx($map, $key, $default);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Set a single key in cache. See @{method:setKeys} to set multiple keys at
|
||||
* once.
|
||||
*
|
||||
* See @{method:setKeys} for a description of TTLs.
|
||||
*
|
||||
* @param string Key to set.
|
||||
* @param wild Value to set.
|
||||
* @param int|null Optional TTL.
|
||||
* @return this
|
||||
* @task kvimpl
|
||||
*/
|
||||
final public function setKey($key, $value, $ttl = null) {
|
||||
return $this->setKeys(array($key => $value), $ttl);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Delete a key from the cache. See @{method:deleteKeys} to delete multiple
|
||||
* keys at once.
|
||||
*
|
||||
* @param string Key to delete.
|
||||
* @return this
|
||||
* @task kvimpl
|
||||
*/
|
||||
final public function deleteKey($key) {
|
||||
return $this->deleteKeys(array($key));
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Get data from the cache.
|
||||
*
|
||||
* @param list<string> List of cache keys to retrieve.
|
||||
* @return dict<string, wild> Dictionary of keys that were found in the
|
||||
* cache. Keys not present in the cache are
|
||||
* omitted, so you can detect a cache miss.
|
||||
* @task kvimpl
|
||||
*/
|
||||
abstract public function getKeys(array $keys);
|
||||
|
||||
|
||||
/**
|
||||
* Put data into the key-value cache.
|
||||
*
|
||||
* With a TTL ("time to live"), the cache will automatically delete the key
|
||||
* after a specified number of seconds. By default, there is no expiration
|
||||
* policy and data will persist in cache indefinitely.
|
||||
*
|
||||
* @param dict<string, wild> Map of cache keys to values.
|
||||
* @param int|null TTL for cache keys, in seconds.
|
||||
* @return this
|
||||
* @task kvimpl
|
||||
*/
|
||||
abstract public function setKeys(array $keys, $ttl = null);
|
||||
|
||||
|
||||
/**
|
||||
* Delete a list of keys from the cache.
|
||||
*
|
||||
* @param list<string> List of keys to delete.
|
||||
* @return this
|
||||
* @task kvimpl
|
||||
*/
|
||||
abstract public function deleteKeys(array $keys);
|
||||
|
||||
|
||||
/**
|
||||
* Completely destroy all data in the cache.
|
||||
*
|
||||
* @return this
|
||||
* @task kvimpl
|
||||
*/
|
||||
abstract public function destroyCache();
|
||||
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue