1
0
Fork 0
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:
epriestley 2018-09-18 10:37:45 -07:00
parent f0608eef9b
commit 8e0e07664a
813 changed files with 203996 additions and 75 deletions

26
.gitignore vendored
View file

@ -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

View 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
View 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.

View 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());
}
}
}

View 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';
}
}
}

View 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;
}
}

View 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
View 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
View 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
View 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

File diff suppressed because it is too large Load diff

45
resources/ssl/README Normal file
View 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

File diff suppressed because it is too large Load diff

View 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"
}

View file

@ -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
View file

@ -0,0 +1,7 @@
#!/usr/bin/env php
<?php
require_once dirname(__FILE__).'/__init_script__.php';
PhutilXHPASTBinary::build();
echo pht('Build successful!')."\n";

View 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();

View 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();

View 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);
}

View file

@ -1,3 +1,4 @@
<?php
require_once dirname(__FILE__).'/init-script.php';
require_once dirname(dirname(dirname(__FILE__))).'/support/ArcanistRuntime.php';

View 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
View 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
View 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
View 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
View 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
View 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());

View 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
View 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
View 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
View 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
View 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
View 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));

View 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());

View 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
View 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
View 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);

View 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
View 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
View 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
View 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;
}

View file

@ -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

View 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';
}
}
}

View 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');
}
}
}

View file

@ -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));
}
}
}
}
}

View 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;
}
}

View 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,
);
}
}

View file

@ -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);
}
}
}

View file

@ -0,0 +1,5 @@
--ABCDEFG
Content-Disposition: form-data; name="a"
b
--ABCDEFG--

View 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;
}
}

View 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();
}
}

View file

@ -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()'));
}
}
}

View file

@ -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;
}
}

View file

@ -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.'));
}
}

View file

@ -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);
}
}

View file

@ -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;
}
}

View file

@ -0,0 +1,4 @@
<?php
final class AphrontAccessDeniedQueryException
extends AphrontQueryException {}

View file

@ -0,0 +1,3 @@
<?php
final class AphrontCharacterSetQueryException extends AphrontQueryException {}

View file

@ -0,0 +1,4 @@
<?php
final class AphrontConnectionLostQueryException
extends AphrontRecoverableQueryException {}

View file

@ -0,0 +1,3 @@
<?php
final class AphrontConnectionQueryException extends AphrontQueryException {}

View file

@ -0,0 +1,3 @@
<?php
final class AphrontCountQueryException extends AphrontQueryException {}

View file

@ -0,0 +1,4 @@
<?php
final class AphrontDeadlockQueryException
extends AphrontRecoverableQueryException {}

View file

@ -0,0 +1,3 @@
<?php
final class AphrontDuplicateKeyQueryException extends AphrontQueryException {}

View file

@ -0,0 +1,4 @@
<?php
final class AphrontInvalidCredentialsQueryException
extends AphrontQueryException {}

View file

@ -0,0 +1,4 @@
<?php
final class AphrontLockTimeoutQueryException
extends AphrontRecoverableQueryException {}

View file

@ -0,0 +1,3 @@
<?php
final class AphrontNotSupportedQueryException extends AphrontQueryException {}

View file

@ -0,0 +1,3 @@
<?php
final class AphrontObjectMissingQueryException extends AphrontQueryException {}

View file

@ -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;
}
}

View file

@ -0,0 +1,6 @@
<?php
/**
* @concrete-extensible
*/
class AphrontQueryException extends Exception {}

View file

@ -0,0 +1,4 @@
<?php
final class AphrontQueryTimeoutQueryException
extends AphrontRecoverableQueryException {}

View file

@ -0,0 +1,3 @@
<?php
abstract class AphrontRecoverableQueryException extends AphrontQueryException {}

View file

@ -0,0 +1,3 @@
<?php
final class AphrontSchemaQueryException extends AphrontQueryException {}

View file

@ -0,0 +1,9 @@
<?php
final class AphrontScopedUnguardedWriteCapability extends Phobject {
public function __destruct() {
AphrontWriteGuard::endUnguardedWrites();
}
}

View 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;
}
}

View 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);
}
}
}

View 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();
}
}

View 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;
}
}

View 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;
}
}

View 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);
}
}
}

View 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;
}
}

View 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;
}
}

View 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);
}
}
}

View 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));
}
}
}

View 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');
}
}

View 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);
}
}

View 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;
}
}

View 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);
}
}

View 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, '/');
}
}

View 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();
}
}

View 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();
}
}

View 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;
}
}

View 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();
}
}

View file

@ -0,0 +1,6 @@
<?php
/**
* Authentication is not configured correctly.
*/
final class PhutilAuthConfigurationException extends PhutilAuthException {}

View file

@ -0,0 +1,6 @@
<?php
/**
* The user provided invalid credentials.
*/
final class PhutilAuthCredentialException extends PhutilAuthException {}

View file

@ -0,0 +1,7 @@
<?php
/**
* Abstract exception class for errors encountered during authentication
* workflows.
*/
abstract class PhutilAuthException extends Exception {}

View 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
View 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');
}
}

View 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;
}
}

View 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
View 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