1
0
Fork 0
mirror of https://github.com/DanielnetoDotCom/YouPHPTube synced 2025-10-03 01:39:24 +02:00
This commit is contained in:
Daniel 2021-11-08 13:58:46 -03:00
parent 3749669821
commit 5c6e1b987b
182 changed files with 23961 additions and 2945 deletions

2
vendor/autoload.php vendored
View file

@ -4,4 +4,4 @@
require_once __DIR__ . '/composer/autoload_real.php';
return ComposerAutoloaderInit4eca095a43f7c8c6d8c0f7669b7856c7::getLoader();
return ComposerAutoloaderInitc3a61dba26daada128f5891f1a57c504::getLoader();

View file

@ -1,4 +1,4 @@
@ECHO OFF
setlocal DISABLEDELAYEDEXPANSION
SET BIN_TARGET=%~dp0/../nesbot/carbon/bin/carbon
php "%BIN_TARGET%" %*
@ECHO OFF
setlocal DISABLEDELAYEDEXPANSION
SET BIN_TARGET=%~dp0/../nesbot/carbon/bin/carbon
php "%BIN_TARGET%" %*

View file

@ -1,4 +1,4 @@
@ECHO OFF
setlocal DISABLEDELAYEDEXPANSION
SET BIN_TARGET=%~dp0/../mtdowling/jmespath.php/bin/jp.php
php "%BIN_TARGET%" %*
@ECHO OFF
setlocal DISABLEDELAYEDEXPANSION
SET BIN_TARGET=%~dp0/../mtdowling/jmespath.php/bin/jp.php
php "%BIN_TARGET%" %*

View file

@ -1,5 +1,5 @@
phpunit.xml
reports
sandbox
vendor
phpunit.xml
reports
sandbox
vendor
composer.lock

View file

@ -1,359 +1,359 @@
PHP ICO - The PHP ICO Generator
Copyright 2011-2016 Chris Jean
PHP ICO is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
PHP ICO is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with PHP ICO; if not, write to the Free Software
Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
GNU GENERAL PUBLIC LICENSE
Version 2, June 1991
Copyright (C) 1989, 1991 Free Software Foundation, Inc.,
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
Preamble
The licenses for most software are designed to take away your
freedom to share and change it. By contrast, the GNU General Public
License is intended to guarantee your freedom to share and change free
software--to make sure the software is free for all its users. This
General Public License applies to most of the Free Software
Foundation's software and to any other program whose authors commit to
using it. (Some other Free Software Foundation software is covered by
the GNU Lesser General Public License instead.) You can apply it to
your programs, too.
When we speak of free software, we are referring to freedom, not
price. Our General Public Licenses are designed to make sure that you
have the freedom to distribute copies of free software (and charge for
this service if you wish), that you receive source code or can get it
if you want it, that you can change the software or use pieces of it
in new free programs; and that you know you can do these things.
To protect your rights, we need to make restrictions that forbid
anyone to deny you these rights or to ask you to surrender the rights.
These restrictions translate to certain responsibilities for you if you
distribute copies of the software, or if you modify it.
For example, if you distribute copies of such a program, whether
gratis or for a fee, you must give the recipients all the rights that
you have. You must make sure that they, too, receive or can get the
source code. And you must show them these terms so they know their
rights.
We protect your rights with two steps: (1) copyright the software, and
(2) offer you this license which gives you legal permission to copy,
distribute and/or modify the software.
Also, for each author's protection and ours, we want to make certain
that everyone understands that there is no warranty for this free
software. If the software is modified by someone else and passed on, we
want its recipients to know that what they have is not the original, so
that any problems introduced by others will not reflect on the original
authors' reputations.
Finally, any free program is threatened constantly by software
patents. We wish to avoid the danger that redistributors of a free
program will individually obtain patent licenses, in effect making the
program proprietary. To prevent this, we have made it clear that any
patent must be licensed for everyone's free use or not licensed at all.
The precise terms and conditions for copying, distribution and
modification follow.
GNU GENERAL PUBLIC LICENSE
TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
0. This License applies to any program or other work which contains
a notice placed by the copyright holder saying it may be distributed
under the terms of this General Public License. The "Program", below,
refers to any such program or work, and a "work based on the Program"
means either the Program or any derivative work under copyright law:
that is to say, a work containing the Program or a portion of it,
either verbatim or with modifications and/or translated into another
language. (Hereinafter, translation is included without limitation in
the term "modification".) Each licensee is addressed as "you".
Activities other than copying, distribution and modification are not
covered by this License; they are outside its scope. The act of
running the Program is not restricted, and the output from the Program
is covered only if its contents constitute a work based on the
Program (independent of having been made by running the Program).
Whether that is true depends on what the Program does.
1. You may copy and distribute verbatim copies of the Program's
source code as you receive it, in any medium, provided that you
conspicuously and appropriately publish on each copy an appropriate
copyright notice and disclaimer of warranty; keep intact all the
notices that refer to this License and to the absence of any warranty;
and give any other recipients of the Program a copy of this License
along with the Program.
You may charge a fee for the physical act of transferring a copy, and
you may at your option offer warranty protection in exchange for a fee.
2. You may modify your copy or copies of the Program or any portion
of it, thus forming a work based on the Program, and copy and
distribute such modifications or work under the terms of Section 1
above, provided that you also meet all of these conditions:
a) You must cause the modified files to carry prominent notices
stating that you changed the files and the date of any change.
b) You must cause any work that you distribute or publish, that in
whole or in part contains or is derived from the Program or any
part thereof, to be licensed as a whole at no charge to all third
parties under the terms of this License.
c) If the modified program normally reads commands interactively
when run, you must cause it, when started running for such
interactive use in the most ordinary way, to print or display an
announcement including an appropriate copyright notice and a
notice that there is no warranty (or else, saying that you provide
a warranty) and that users may redistribute the program under
these conditions, and telling the user how to view a copy of this
License. (Exception: if the Program itself is interactive but
does not normally print such an announcement, your work based on
the Program is not required to print an announcement.)
These requirements apply to the modified work as a whole. If
identifiable sections of that work are not derived from the Program,
and can be reasonably considered independent and separate works in
themselves, then this License, and its terms, do not apply to those
sections when you distribute them as separate works. But when you
distribute the same sections as part of a whole which is a work based
on the Program, the distribution of the whole must be on the terms of
this License, whose permissions for other licensees extend to the
entire whole, and thus to each and every part regardless of who wrote it.
Thus, it is not the intent of this section to claim rights or contest
your rights to work written entirely by you; rather, the intent is to
exercise the right to control the distribution of derivative or
collective works based on the Program.
In addition, mere aggregation of another work not based on the Program
with the Program (or with a work based on the Program) on a volume of
a storage or distribution medium does not bring the other work under
the scope of this License.
3. You may copy and distribute the Program (or a work based on it,
under Section 2) in object code or executable form under the terms of
Sections 1 and 2 above provided that you also do one of the following:
a) Accompany it with the complete corresponding machine-readable
source code, which must be distributed under the terms of Sections
1 and 2 above on a medium customarily used for software interchange; or,
b) Accompany it with a written offer, valid for at least three
years, to give any third party, for a charge no more than your
cost of physically performing source distribution, a complete
machine-readable copy of the corresponding source code, to be
distributed under the terms of Sections 1 and 2 above on a medium
customarily used for software interchange; or,
c) Accompany it with the information you received as to the offer
to distribute corresponding source code. (This alternative is
allowed only for noncommercial distribution and only if you
received the program in object code or executable form with such
an offer, in accord with Subsection b above.)
The source code for a work means the preferred form of the work for
making modifications to it. For an executable work, complete source
code means all the source code for all modules it contains, plus any
associated interface definition files, plus the scripts used to
control compilation and installation of the executable. However, as a
special exception, the source code distributed need not include
anything that is normally distributed (in either source or binary
form) with the major components (compiler, kernel, and so on) of the
operating system on which the executable runs, unless that component
itself accompanies the executable.
If distribution of executable or object code is made by offering
access to copy from a designated place, then offering equivalent
access to copy the source code from the same place counts as
distribution of the source code, even though third parties are not
compelled to copy the source along with the object code.
4. You may not copy, modify, sublicense, or distribute the Program
except as expressly provided under this License. Any attempt
otherwise to copy, modify, sublicense or distribute the Program is
void, and will automatically terminate your rights under this License.
However, parties who have received copies, or rights, from you under
this License will not have their licenses terminated so long as such
parties remain in full compliance.
5. You are not required to accept this License, since you have not
signed it. However, nothing else grants you permission to modify or
distribute the Program or its derivative works. These actions are
prohibited by law if you do not accept this License. Therefore, by
modifying or distributing the Program (or any work based on the
Program), you indicate your acceptance of this License to do so, and
all its terms and conditions for copying, distributing or modifying
the Program or works based on it.
6. Each time you redistribute the Program (or any work based on the
Program), the recipient automatically receives a license from the
original licensor to copy, distribute or modify the Program subject to
these terms and conditions. You may not impose any further
restrictions on the recipients' exercise of the rights granted herein.
You are not responsible for enforcing compliance by third parties to
this License.
7. If, as a consequence of a court judgment or allegation of patent
infringement or for any other reason (not limited to patent issues),
conditions are imposed on you (whether by court order, agreement or
otherwise) that contradict the conditions of this License, they do not
excuse you from the conditions of this License. If you cannot
distribute so as to satisfy simultaneously your obligations under this
License and any other pertinent obligations, then as a consequence you
may not distribute the Program at all. For example, if a patent
license would not permit royalty-free redistribution of the Program by
all those who receive copies directly or indirectly through you, then
the only way you could satisfy both it and this License would be to
refrain entirely from distribution of the Program.
If any portion of this section is held invalid or unenforceable under
any particular circumstance, the balance of the section is intended to
apply and the section as a whole is intended to apply in other
circumstances.
It is not the purpose of this section to induce you to infringe any
patents or other property right claims or to contest validity of any
such claims; this section has the sole purpose of protecting the
integrity of the free software distribution system, which is
implemented by public license practices. Many people have made
generous contributions to the wide range of software distributed
through that system in reliance on consistent application of that
system; it is up to the author/donor to decide if he or she is willing
to distribute software through any other system and a licensee cannot
impose that choice.
This section is intended to make thoroughly clear what is believed to
be a consequence of the rest of this License.
8. If the distribution and/or use of the Program is restricted in
certain countries either by patents or by copyrighted interfaces, the
original copyright holder who places the Program under this License
may add an explicit geographical distribution limitation excluding
those countries, so that distribution is permitted only in or among
countries not thus excluded. In such case, this License incorporates
the limitation as if written in the body of this License.
9. The Free Software Foundation may publish revised and/or new versions
of the General Public License from time to time. Such new versions will
be similar in spirit to the present version, but may differ in detail to
address new problems or concerns.
Each version is given a distinguishing version number. If the Program
specifies a version number of this License which applies to it and "any
later version", you have the option of following the terms and conditions
either of that version or of any later version published by the Free
Software Foundation. If the Program does not specify a version number of
this License, you may choose any version ever published by the Free Software
Foundation.
10. If you wish to incorporate parts of the Program into other free
programs whose distribution conditions are different, write to the author
to ask for permission. For software which is copyrighted by the Free
Software Foundation, write to the Free Software Foundation; we sometimes
make exceptions for this. Our decision will be guided by the two goals
of preserving the free status of all derivatives of our free software and
of promoting the sharing and reuse of software generally.
NO WARRANTY
11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN
OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED
OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS
TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE
PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING,
REPAIR OR CORRECTION.
12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES,
INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING
OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED
TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY
YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER
PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE
POSSIBILITY OF SUCH DAMAGES.
END OF TERMS AND CONDITIONS
How to Apply These Terms to Your New Programs
If you develop a new program, and you want it to be of the greatest
possible use to the public, the best way to achieve this is to make it
free software which everyone can redistribute and change under these terms.
To do so, attach the following notices to the program. It is safest
to attach them to the start of each source file to most effectively
convey the exclusion of warranty; and each file should have at least
the "copyright" line and a pointer to where the full notice is found.
<one line to give the program's name and a brief idea of what it does.>
Copyright (C) <year> <name of author>
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License along
with this program; if not, write to the Free Software Foundation, Inc.,
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
Also add information on how to contact you by electronic and paper mail.
If the program is interactive, make it output a short notice like this
when it starts in an interactive mode:
Gnomovision version 69, Copyright (C) year name of author
Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
This is free software, and you are welcome to redistribute it
under certain conditions; type `show c' for details.
The hypothetical commands `show w' and `show c' should show the appropriate
parts of the General Public License. Of course, the commands you use may
be called something other than `show w' and `show c'; they could even be
mouse-clicks or menu items--whatever suits your program.
You should also get your employer (if you work as a programmer) or your
school, if any, to sign a "copyright disclaimer" for the program, if
necessary. Here is a sample; alter the names:
Yoyodyne, Inc., hereby disclaims all copyright interest in the program
`Gnomovision' (which makes passes at compilers) written by James Hacker.
<signature of Ty Coon>, 1 April 1989
Ty Coon, President of Vice
This General Public License does not permit incorporating your program into
proprietary programs. If your program is a subroutine library, you may
consider it more useful to permit linking proprietary applications with the
library. If this is what you want to do, use the GNU Lesser General
Public License instead of this License.
PHP ICO - The PHP ICO Generator
Copyright 2011-2016 Chris Jean
PHP ICO is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
PHP ICO is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with PHP ICO; if not, write to the Free Software
Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
GNU GENERAL PUBLIC LICENSE
Version 2, June 1991
Copyright (C) 1989, 1991 Free Software Foundation, Inc.,
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
Preamble
The licenses for most software are designed to take away your
freedom to share and change it. By contrast, the GNU General Public
License is intended to guarantee your freedom to share and change free
software--to make sure the software is free for all its users. This
General Public License applies to most of the Free Software
Foundation's software and to any other program whose authors commit to
using it. (Some other Free Software Foundation software is covered by
the GNU Lesser General Public License instead.) You can apply it to
your programs, too.
When we speak of free software, we are referring to freedom, not
price. Our General Public Licenses are designed to make sure that you
have the freedom to distribute copies of free software (and charge for
this service if you wish), that you receive source code or can get it
if you want it, that you can change the software or use pieces of it
in new free programs; and that you know you can do these things.
To protect your rights, we need to make restrictions that forbid
anyone to deny you these rights or to ask you to surrender the rights.
These restrictions translate to certain responsibilities for you if you
distribute copies of the software, or if you modify it.
For example, if you distribute copies of such a program, whether
gratis or for a fee, you must give the recipients all the rights that
you have. You must make sure that they, too, receive or can get the
source code. And you must show them these terms so they know their
rights.
We protect your rights with two steps: (1) copyright the software, and
(2) offer you this license which gives you legal permission to copy,
distribute and/or modify the software.
Also, for each author's protection and ours, we want to make certain
that everyone understands that there is no warranty for this free
software. If the software is modified by someone else and passed on, we
want its recipients to know that what they have is not the original, so
that any problems introduced by others will not reflect on the original
authors' reputations.
Finally, any free program is threatened constantly by software
patents. We wish to avoid the danger that redistributors of a free
program will individually obtain patent licenses, in effect making the
program proprietary. To prevent this, we have made it clear that any
patent must be licensed for everyone's free use or not licensed at all.
The precise terms and conditions for copying, distribution and
modification follow.
GNU GENERAL PUBLIC LICENSE
TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
0. This License applies to any program or other work which contains
a notice placed by the copyright holder saying it may be distributed
under the terms of this General Public License. The "Program", below,
refers to any such program or work, and a "work based on the Program"
means either the Program or any derivative work under copyright law:
that is to say, a work containing the Program or a portion of it,
either verbatim or with modifications and/or translated into another
language. (Hereinafter, translation is included without limitation in
the term "modification".) Each licensee is addressed as "you".
Activities other than copying, distribution and modification are not
covered by this License; they are outside its scope. The act of
running the Program is not restricted, and the output from the Program
is covered only if its contents constitute a work based on the
Program (independent of having been made by running the Program).
Whether that is true depends on what the Program does.
1. You may copy and distribute verbatim copies of the Program's
source code as you receive it, in any medium, provided that you
conspicuously and appropriately publish on each copy an appropriate
copyright notice and disclaimer of warranty; keep intact all the
notices that refer to this License and to the absence of any warranty;
and give any other recipients of the Program a copy of this License
along with the Program.
You may charge a fee for the physical act of transferring a copy, and
you may at your option offer warranty protection in exchange for a fee.
2. You may modify your copy or copies of the Program or any portion
of it, thus forming a work based on the Program, and copy and
distribute such modifications or work under the terms of Section 1
above, provided that you also meet all of these conditions:
a) You must cause the modified files to carry prominent notices
stating that you changed the files and the date of any change.
b) You must cause any work that you distribute or publish, that in
whole or in part contains or is derived from the Program or any
part thereof, to be licensed as a whole at no charge to all third
parties under the terms of this License.
c) If the modified program normally reads commands interactively
when run, you must cause it, when started running for such
interactive use in the most ordinary way, to print or display an
announcement including an appropriate copyright notice and a
notice that there is no warranty (or else, saying that you provide
a warranty) and that users may redistribute the program under
these conditions, and telling the user how to view a copy of this
License. (Exception: if the Program itself is interactive but
does not normally print such an announcement, your work based on
the Program is not required to print an announcement.)
These requirements apply to the modified work as a whole. If
identifiable sections of that work are not derived from the Program,
and can be reasonably considered independent and separate works in
themselves, then this License, and its terms, do not apply to those
sections when you distribute them as separate works. But when you
distribute the same sections as part of a whole which is a work based
on the Program, the distribution of the whole must be on the terms of
this License, whose permissions for other licensees extend to the
entire whole, and thus to each and every part regardless of who wrote it.
Thus, it is not the intent of this section to claim rights or contest
your rights to work written entirely by you; rather, the intent is to
exercise the right to control the distribution of derivative or
collective works based on the Program.
In addition, mere aggregation of another work not based on the Program
with the Program (or with a work based on the Program) on a volume of
a storage or distribution medium does not bring the other work under
the scope of this License.
3. You may copy and distribute the Program (or a work based on it,
under Section 2) in object code or executable form under the terms of
Sections 1 and 2 above provided that you also do one of the following:
a) Accompany it with the complete corresponding machine-readable
source code, which must be distributed under the terms of Sections
1 and 2 above on a medium customarily used for software interchange; or,
b) Accompany it with a written offer, valid for at least three
years, to give any third party, for a charge no more than your
cost of physically performing source distribution, a complete
machine-readable copy of the corresponding source code, to be
distributed under the terms of Sections 1 and 2 above on a medium
customarily used for software interchange; or,
c) Accompany it with the information you received as to the offer
to distribute corresponding source code. (This alternative is
allowed only for noncommercial distribution and only if you
received the program in object code or executable form with such
an offer, in accord with Subsection b above.)
The source code for a work means the preferred form of the work for
making modifications to it. For an executable work, complete source
code means all the source code for all modules it contains, plus any
associated interface definition files, plus the scripts used to
control compilation and installation of the executable. However, as a
special exception, the source code distributed need not include
anything that is normally distributed (in either source or binary
form) with the major components (compiler, kernel, and so on) of the
operating system on which the executable runs, unless that component
itself accompanies the executable.
If distribution of executable or object code is made by offering
access to copy from a designated place, then offering equivalent
access to copy the source code from the same place counts as
distribution of the source code, even though third parties are not
compelled to copy the source along with the object code.
4. You may not copy, modify, sublicense, or distribute the Program
except as expressly provided under this License. Any attempt
otherwise to copy, modify, sublicense or distribute the Program is
void, and will automatically terminate your rights under this License.
However, parties who have received copies, or rights, from you under
this License will not have their licenses terminated so long as such
parties remain in full compliance.
5. You are not required to accept this License, since you have not
signed it. However, nothing else grants you permission to modify or
distribute the Program or its derivative works. These actions are
prohibited by law if you do not accept this License. Therefore, by
modifying or distributing the Program (or any work based on the
Program), you indicate your acceptance of this License to do so, and
all its terms and conditions for copying, distributing or modifying
the Program or works based on it.
6. Each time you redistribute the Program (or any work based on the
Program), the recipient automatically receives a license from the
original licensor to copy, distribute or modify the Program subject to
these terms and conditions. You may not impose any further
restrictions on the recipients' exercise of the rights granted herein.
You are not responsible for enforcing compliance by third parties to
this License.
7. If, as a consequence of a court judgment or allegation of patent
infringement or for any other reason (not limited to patent issues),
conditions are imposed on you (whether by court order, agreement or
otherwise) that contradict the conditions of this License, they do not
excuse you from the conditions of this License. If you cannot
distribute so as to satisfy simultaneously your obligations under this
License and any other pertinent obligations, then as a consequence you
may not distribute the Program at all. For example, if a patent
license would not permit royalty-free redistribution of the Program by
all those who receive copies directly or indirectly through you, then
the only way you could satisfy both it and this License would be to
refrain entirely from distribution of the Program.
If any portion of this section is held invalid or unenforceable under
any particular circumstance, the balance of the section is intended to
apply and the section as a whole is intended to apply in other
circumstances.
It is not the purpose of this section to induce you to infringe any
patents or other property right claims or to contest validity of any
such claims; this section has the sole purpose of protecting the
integrity of the free software distribution system, which is
implemented by public license practices. Many people have made
generous contributions to the wide range of software distributed
through that system in reliance on consistent application of that
system; it is up to the author/donor to decide if he or she is willing
to distribute software through any other system and a licensee cannot
impose that choice.
This section is intended to make thoroughly clear what is believed to
be a consequence of the rest of this License.
8. If the distribution and/or use of the Program is restricted in
certain countries either by patents or by copyrighted interfaces, the
original copyright holder who places the Program under this License
may add an explicit geographical distribution limitation excluding
those countries, so that distribution is permitted only in or among
countries not thus excluded. In such case, this License incorporates
the limitation as if written in the body of this License.
9. The Free Software Foundation may publish revised and/or new versions
of the General Public License from time to time. Such new versions will
be similar in spirit to the present version, but may differ in detail to
address new problems or concerns.
Each version is given a distinguishing version number. If the Program
specifies a version number of this License which applies to it and "any
later version", you have the option of following the terms and conditions
either of that version or of any later version published by the Free
Software Foundation. If the Program does not specify a version number of
this License, you may choose any version ever published by the Free Software
Foundation.
10. If you wish to incorporate parts of the Program into other free
programs whose distribution conditions are different, write to the author
to ask for permission. For software which is copyrighted by the Free
Software Foundation, write to the Free Software Foundation; we sometimes
make exceptions for this. Our decision will be guided by the two goals
of preserving the free status of all derivatives of our free software and
of promoting the sharing and reuse of software generally.
NO WARRANTY
11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN
OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED
OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS
TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE
PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING,
REPAIR OR CORRECTION.
12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES,
INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING
OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED
TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY
YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER
PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE
POSSIBILITY OF SUCH DAMAGES.
END OF TERMS AND CONDITIONS
How to Apply These Terms to Your New Programs
If you develop a new program, and you want it to be of the greatest
possible use to the public, the best way to achieve this is to make it
free software which everyone can redistribute and change under these terms.
To do so, attach the following notices to the program. It is safest
to attach them to the start of each source file to most effectively
convey the exclusion of warranty; and each file should have at least
the "copyright" line and a pointer to where the full notice is found.
<one line to give the program's name and a brief idea of what it does.>
Copyright (C) <year> <name of author>
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License along
with this program; if not, write to the Free Software Foundation, Inc.,
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
Also add information on how to contact you by electronic and paper mail.
If the program is interactive, make it output a short notice like this
when it starts in an interactive mode:
Gnomovision version 69, Copyright (C) year name of author
Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
This is free software, and you are welcome to redistribute it
under certain conditions; type `show c' for details.
The hypothetical commands `show w' and `show c' should show the appropriate
parts of the General Public License. Of course, the commands you use may
be called something other than `show w' and `show c'; they could even be
mouse-clicks or menu items--whatever suits your program.
You should also get your employer (if you work as a programmer) or your
school, if any, to sign a "copyright disclaimer" for the program, if
necessary. Here is a sample; alter the names:
Yoyodyne, Inc., hereby disclaims all copyright interest in the program
`Gnomovision' (which makes passes at compilers) written by James Hacker.
<signature of Ty Coon>, 1 April 1989
Ty Coon, President of Vice
This General Public License does not permit incorporating your program into
proprietary programs. If your program is a subroutine library, you may
consider it more useful to permit linking proprietary applications with the
library. If this is what you want to do, use the GNU Lesser General
Public License instead of this License.

View file

@ -30,7 +30,7 @@ private static $installed = array (
'aliases' =>
array (
),
'reference' => '0a4e58f0ead4918360ca9c06839e1566f75ecc75',
'reference' => '374966982138bc982c115f6d3c71a71403f7a983',
'name' => 'wwbn/avideo',
),
'versions' =>
@ -692,7 +692,7 @@ private static $installed = array (
'aliases' =>
array (
),
'reference' => '0a4e58f0ead4918360ca9c06839e1566f75ecc75',
'reference' => '374966982138bc982c115f6d3c71a71403f7a983',
),
),
);

View file

@ -12,10 +12,10 @@ return array(
'c964ee0ededf28c96ebd9db5099ef910' => $vendorDir . '/guzzlehttp/promises/src/functions_include.php',
'a4a119a56e50fbb293281d9a48007e0e' => $vendorDir . '/symfony/polyfill-php80/bootstrap.php',
'6e3fae29631ef280660b3cdad06f25a8' => $vendorDir . '/symfony/deprecation-contracts/function.php',
'972fda704d680a3a53c68e34e193cb22' => $vendorDir . '/react/promise-timer/src/functions_include.php',
'0e6d7bf4a5811bfa5cf40c5ccd6fae6a' => $vendorDir . '/symfony/polyfill-mbstring/bootstrap.php',
'e69f7f6ee287b969198c3c9d6777bd38' => $vendorDir . '/symfony/polyfill-intl-normalizer/bootstrap.php',
'972fda704d680a3a53c68e34e193cb22' => $vendorDir . '/react/promise-timer/src/functions_include.php',
'25072dd6e2470089de65ae7bf11d3109' => $vendorDir . '/symfony/polyfill-php72/bootstrap.php',
'e69f7f6ee287b969198c3c9d6777bd38' => $vendorDir . '/symfony/polyfill-intl-normalizer/bootstrap.php',
'f598d06aa772fa33d905e87be6398fb1' => $vendorDir . '/symfony/polyfill-intl-idn/bootstrap.php',
'37a3dc5111fe8f707ab4c132ef1dbc62' => $vendorDir . '/guzzlehttp/guzzle/src/functions_include.php',
'decc78cc4436b1292c6c0d151b19445c' => $vendorDir . '/phpseclib/phpseclib/phpseclib/bootstrap.php',

View file

@ -34,7 +34,7 @@ return array(
'Ratchet\\Client\\' => array($vendorDir . '/ratchet/pawl/src'),
'Ratchet\\' => array($vendorDir . '/cboden/ratchet/src/Ratchet'),
'Psr\\Log\\' => array($vendorDir . '/psr/log/Psr/Log'),
'Psr\\Http\\Message\\' => array($vendorDir . '/psr/http-factory/src', $vendorDir . '/psr/http-message/src'),
'Psr\\Http\\Message\\' => array($vendorDir . '/psr/http-message/src', $vendorDir . '/psr/http-factory/src'),
'Psr\\Http\\Client\\' => array($vendorDir . '/psr/http-client/src'),
'Psr\\Container\\' => array($vendorDir . '/psr/container/src'),
'Psr\\Cache\\' => array($vendorDir . '/psr/cache/src'),

View file

@ -2,7 +2,7 @@
// autoload_real.php @generated by Composer
class ComposerAutoloaderInit4eca095a43f7c8c6d8c0f7669b7856c7
class ComposerAutoloaderInitc3a61dba26daada128f5891f1a57c504
{
private static $loader;
@ -22,15 +22,15 @@ class ComposerAutoloaderInit4eca095a43f7c8c6d8c0f7669b7856c7
return self::$loader;
}
spl_autoload_register(array('ComposerAutoloaderInit4eca095a43f7c8c6d8c0f7669b7856c7', 'loadClassLoader'), true, true);
spl_autoload_register(array('ComposerAutoloaderInitc3a61dba26daada128f5891f1a57c504', 'loadClassLoader'), true, true);
self::$loader = $loader = new \Composer\Autoload\ClassLoader(\dirname(\dirname(__FILE__)));
spl_autoload_unregister(array('ComposerAutoloaderInit4eca095a43f7c8c6d8c0f7669b7856c7', 'loadClassLoader'));
spl_autoload_unregister(array('ComposerAutoloaderInitc3a61dba26daada128f5891f1a57c504', 'loadClassLoader'));
$useStaticLoader = PHP_VERSION_ID >= 50600 && !defined('HHVM_VERSION') && (!function_exists('zend_loader_file_encoded') || !zend_loader_file_encoded());
if ($useStaticLoader) {
require __DIR__ . '/autoload_static.php';
call_user_func(\Composer\Autoload\ComposerStaticInit4eca095a43f7c8c6d8c0f7669b7856c7::getInitializer($loader));
call_user_func(\Composer\Autoload\ComposerStaticInitc3a61dba26daada128f5891f1a57c504::getInitializer($loader));
} else {
$map = require __DIR__ . '/autoload_namespaces.php';
foreach ($map as $namespace => $path) {
@ -51,19 +51,19 @@ class ComposerAutoloaderInit4eca095a43f7c8c6d8c0f7669b7856c7
$loader->register(true);
if ($useStaticLoader) {
$includeFiles = Composer\Autoload\ComposerStaticInit4eca095a43f7c8c6d8c0f7669b7856c7::$files;
$includeFiles = Composer\Autoload\ComposerStaticInitc3a61dba26daada128f5891f1a57c504::$files;
} else {
$includeFiles = require __DIR__ . '/autoload_files.php';
}
foreach ($includeFiles as $fileIdentifier => $file) {
composerRequire4eca095a43f7c8c6d8c0f7669b7856c7($fileIdentifier, $file);
composerRequirec3a61dba26daada128f5891f1a57c504($fileIdentifier, $file);
}
return $loader;
}
}
function composerRequire4eca095a43f7c8c6d8c0f7669b7856c7($fileIdentifier, $file)
function composerRequirec3a61dba26daada128f5891f1a57c504($fileIdentifier, $file)
{
if (empty($GLOBALS['__composer_autoload_files'][$fileIdentifier])) {
require $file;

View file

@ -4,7 +4,7 @@
namespace Composer\Autoload;
class ComposerStaticInit4eca095a43f7c8c6d8c0f7669b7856c7
class ComposerStaticInitc3a61dba26daada128f5891f1a57c504
{
public static $files = array (
'ad155f8f1cf0d418fe49e248db8c661b' => __DIR__ . '/..' . '/react/promise/src/functions_include.php',
@ -13,10 +13,10 @@ class ComposerStaticInit4eca095a43f7c8c6d8c0f7669b7856c7
'c964ee0ededf28c96ebd9db5099ef910' => __DIR__ . '/..' . '/guzzlehttp/promises/src/functions_include.php',
'a4a119a56e50fbb293281d9a48007e0e' => __DIR__ . '/..' . '/symfony/polyfill-php80/bootstrap.php',
'6e3fae29631ef280660b3cdad06f25a8' => __DIR__ . '/..' . '/symfony/deprecation-contracts/function.php',
'972fda704d680a3a53c68e34e193cb22' => __DIR__ . '/..' . '/react/promise-timer/src/functions_include.php',
'0e6d7bf4a5811bfa5cf40c5ccd6fae6a' => __DIR__ . '/..' . '/symfony/polyfill-mbstring/bootstrap.php',
'e69f7f6ee287b969198c3c9d6777bd38' => __DIR__ . '/..' . '/symfony/polyfill-intl-normalizer/bootstrap.php',
'972fda704d680a3a53c68e34e193cb22' => __DIR__ . '/..' . '/react/promise-timer/src/functions_include.php',
'25072dd6e2470089de65ae7bf11d3109' => __DIR__ . '/..' . '/symfony/polyfill-php72/bootstrap.php',
'e69f7f6ee287b969198c3c9d6777bd38' => __DIR__ . '/..' . '/symfony/polyfill-intl-normalizer/bootstrap.php',
'f598d06aa772fa33d905e87be6398fb1' => __DIR__ . '/..' . '/symfony/polyfill-intl-idn/bootstrap.php',
'37a3dc5111fe8f707ab4c132ef1dbc62' => __DIR__ . '/..' . '/guzzlehttp/guzzle/src/functions_include.php',
'decc78cc4436b1292c6c0d151b19445c' => __DIR__ . '/..' . '/phpseclib/phpseclib/phpseclib/bootstrap.php',
@ -249,8 +249,8 @@ class ComposerStaticInit4eca095a43f7c8c6d8c0f7669b7856c7
),
'Psr\\Http\\Message\\' =>
array (
0 => __DIR__ . '/..' . '/psr/http-factory/src',
1 => __DIR__ . '/..' . '/psr/http-message/src',
0 => __DIR__ . '/..' . '/psr/http-message/src',
1 => __DIR__ . '/..' . '/psr/http-factory/src',
),
'Psr\\Http\\Client\\' =>
array (
@ -578,10 +578,10 @@ class ComposerStaticInit4eca095a43f7c8c6d8c0f7669b7856c7
public static function getInitializer(ClassLoader $loader)
{
return \Closure::bind(function () use ($loader) {
$loader->prefixLengthsPsr4 = ComposerStaticInit4eca095a43f7c8c6d8c0f7669b7856c7::$prefixLengthsPsr4;
$loader->prefixDirsPsr4 = ComposerStaticInit4eca095a43f7c8c6d8c0f7669b7856c7::$prefixDirsPsr4;
$loader->prefixesPsr0 = ComposerStaticInit4eca095a43f7c8c6d8c0f7669b7856c7::$prefixesPsr0;
$loader->classMap = ComposerStaticInit4eca095a43f7c8c6d8c0f7669b7856c7::$classMap;
$loader->prefixLengthsPsr4 = ComposerStaticInitc3a61dba26daada128f5891f1a57c504::$prefixLengthsPsr4;
$loader->prefixDirsPsr4 = ComposerStaticInitc3a61dba26daada128f5891f1a57c504::$prefixDirsPsr4;
$loader->prefixesPsr0 = ComposerStaticInitc3a61dba26daada128f5891f1a57c504::$prefixesPsr0;
$loader->classMap = ComposerStaticInitc3a61dba26daada128f5891f1a57c504::$classMap;
}, null, ClassLoader::class);
}

View file

@ -6,7 +6,7 @@
'aliases' =>
array (
),
'reference' => '0a4e58f0ead4918360ca9c06839e1566f75ecc75',
'reference' => '374966982138bc982c115f6d3c71a71403f7a983',
'name' => 'wwbn/avideo',
),
'versions' =>
@ -668,7 +668,7 @@
'aliases' =>
array (
),
'reference' => '0a4e58f0ead4918360ca9c06839e1566f75ecc75',
'reference' => '374966982138bc982c115f6d3c71a71403f7a983',
),
),
);

View file

@ -1,107 +1,107 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 19.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1"
id="Слой_1" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:cc="http://creativecommons.org/ns#" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" width="24px" height="24px"
viewBox="-374.034 286.28 24 24" style="enable-background:new -374.034 286.28 24 24;" xml:space="preserve">
<style type="text/css">
.st0{fill:url(#SVGID_1_);}
.st1{fill:#1A1626;}
.st2{fill:url(#SVGID_2_);}
.st3{fill:#B3C2C6;}
.st4{opacity:0.3;fill:url(#SVGID_3_);enable-background:new ;}
</style>
<g>
<radialGradient id="SVGID_1_" cx="-273.9293" cy="283.2144" r="13.0467" gradientTransform="matrix(1.0506 0 0 -1.0506 35.403 593.0075)" gradientUnits="userSpaceOnUse">
<stop offset="0" style="stop-color:#FF8800"/>
<stop offset="0.2913" style="stop-color:#FC8500"/>
<stop offset="0.5282" style="stop-color:#F27E00"/>
<stop offset="0.7453" style="stop-color:#E17000"/>
<stop offset="0.9273" style="stop-color:#CC6000"/>
</radialGradient>
<circle class="st0" cx="-250.215" cy="298.28" r="12"/>
<path class="st1" d="M-244.272,301.023c-4.416-1.463-8.23-2.726-9.737-5.76c-0.101-0.203-0.184-0.412-0.252-0.625
c5.741-2.206,12.524-2.715,15.4-0.253c-0.1-0.292-0.21-0.578-0.332-0.858c-1.388-0.942-3.755-1.482-6.411-1.374
c-2.78,0.113-5.943,0.798-8.805,1.88c-0.214-1.258,0.104-2.612,0.929-3.808c1.42-2.058,3.935-3.264,6.918-3.377
c-0.37-0.118-0.747-0.219-1.132-0.301c-3.021,0.281-5.06,1.528-6.299,3.325c-0.947,1.374-1.292,2.942-1.006,4.393
c-0.489,0.197-0.969,0.405-1.434,0.625c-0.347,0.164-0.68,0.332-1,0.502c-0.325-1.315-0.498-2.592-0.481-3.762
c0.015-0.992,0.181-2.171,0.744-3.128c-0.271,0.193-0.533,0.397-0.787,0.612c-1.009,2.316-0.405,5.122-0.032,6.584
c-0.627,0.358-1.196,0.726-1.704,1.103c-0.324-1.153-0.808-2.243-1.572-3.21c-0.096,0.226-0.185,0.456-0.267,0.688
c0.626,0.89,1.029,1.887,1.303,2.941c-0.871,0.719-1.521,1.461-1.934,2.206c0.028,0.302,0.068,0.601,0.119,0.896
c0.352-0.701,0.924-1.523,1.971-2.432c0.252,1.21,0.368,2.478,0.481,3.729c0.169,1.869,0.4,4.327,1.315,5.553
c0.325,0.295,0.667,0.573,1.024,0.831c-1.277-1.573-1.487-3.885-1.718-6.44c-0.125-1.382-0.252-2.782-0.557-4.116
c0.5-0.384,1.071-0.761,1.708-1.13c0.515,1.8,1.29,3.686,2.28,5.49c1.993,3.633,4.65,7.323,8.641,8.011
c0.342-0.098,0.678-0.211,1.008-0.339c-0.762-0.01-1.56-0.218-2.392-0.626c-3.698-1.818-7.505-7.55-8.986-12.843
c0.35-0.189,0.718-0.375,1.104-0.557c0.432-0.204,0.874-0.398,1.323-0.582c0.075,0.228,0.167,0.453,0.276,0.672
c1.622,3.266,5.551,4.568,10.099,6.074c1.663,0.551,3.37,1.117,5.095,1.813c0.089-0.188,0.174-0.377,0.253-0.57
C-240.869,302.152-242.592,301.58-244.272,301.023z"/>
</g>
<path d="M-362.034,286.471c-6.512,0-11.81,5.297-11.81,11.809c0,6.511,5.298,11.809,11.81,11.809s11.81-5.298,11.81-11.809
C-350.224,291.768-355.522,286.471-362.034,286.471z M-352.229,302.031c-1.178-0.464-3.67-1.406-7.588-2.693
c-1.718-0.564-3.107-1.309-4.131-2.213c-0.637-0.565-1.089-1.286-1.357-2.063c1.248-0.442,2.674-0.83,4.321-1.11
c4.701-0.8,7.352-0.115,8.773,0.628c0.436,1.153,0.686,2.398,0.686,3.702C-351.525,299.603-351.781,300.864-352.229,302.031z
M-353.059,292.841c-2.134-0.667-4.857-0.732-8.144-0.171c-1.647,0.281-3.087,0.666-4.363,1.108
c-0.042-0.673,0.051-1.352,0.309-1.984c1.096-2.679,4.346-3.48,5.691-3.701c0.01-0.002,0.017-0.008,0.027-0.01
C-356.801,288.753-354.487,290.495-353.059,292.841z M-368.421,289.954c1.534-1.179,3.395-1.949,5.428-2.134
c-1.378,0.675-2.759,1.753-3.466,3.483c-0.389,0.953-0.497,1.968-0.373,2.956c-0.609,0.253-1.174,0.516-1.693,0.788
C-369.016,292.507-368.69,290.799-368.421,289.954z M-370.03,291.478c-0.098,1.091-0.059,2.513,0.32,4.243
c-0.337,0.21-0.656,0.42-0.947,0.63c-0.376-1.065-0.797-1.794-1.05-2.176C-371.287,293.188-370.716,292.283-370.03,291.478z
M-371.271,299.562c0.061,0.609,0.105,1.16,0.147,1.669c0.089,1.08,0.167,1.988,0.343,2.862c-0.846-1.269-1.413-2.733-1.641-4.307
c0.237-0.31,0.571-0.701,1.024-1.139C-371.348,298.938-371.303,299.24-371.271,299.562z M-372.525,297.938
c0.023-0.71,0.115-1.402,0.273-2.071c0.171,0.365,0.347,0.812,0.509,1.338C-372.039,297.461-372.295,297.705-372.525,297.938z
M-368.472,306.568c-0.009-0.02-0.009-0.042-0.02-0.062c-1.045-1.829-1.154-3.166-1.336-5.382c-0.042-0.516-0.088-1.074-0.15-1.691
c-0.064-0.641-0.172-1.217-0.297-1.751c0.276-0.212,0.581-0.427,0.909-0.643c0.346,1.167,0.836,2.446,1.535,3.844
c2.354,4.709,4.82,6.874,6.733,7.859c-0.309,0.027-0.62,0.047-0.936,0.047C-364.46,308.788-366.691,307.954-368.472,306.568z
M-358.651,308.219c-0.029-0.009-0.054-0.027-0.085-0.032c-1.616-0.252-4.795-1.612-7.933-7.888c-0.73-1.46-1.223-2.776-1.55-3.95
c0.512-0.281,1.074-0.555,1.691-0.817c0.342,0.96,0.913,1.851,1.718,2.565c1.156,1.023,2.699,1.856,4.586,2.475
c3.835,1.26,6.278,2.181,7.461,2.645C-354.011,305.551-356.115,307.353-358.651,308.219z"/>
<g>
<radialGradient id="SVGID_2_" cx="-298.1872" cy="283.9428" r="11.9595" gradientTransform="matrix(1.0506 0 0 -1.0506 35.403 593.0075)" gradientUnits="userSpaceOnUse">
<stop offset="0.2727" style="stop-color:#FAFDFF"/>
<stop offset="0.4936" style="stop-color:#F7FAFC"/>
<stop offset="0.6732" style="stop-color:#EDF2F4"/>
<stop offset="0.8379" style="stop-color:#DCE3E7"/>
<stop offset="0.9273" style="stop-color:#CFD8DD"/>
</radialGradient>
<circle class="st2" cx="-275.882" cy="297.28" r="11"/>
<path class="st3" d="M-279.516,290.078c-2.133-0.776-3.766-0.544-3.783-0.542l-0.041-0.272c0.069-0.01,1.72-0.244,3.917,0.555
L-279.516,290.078z"/>
<path class="st3" d="M-266.591,293.035c-3.241-1.925-5.943-1.651-5.97-1.649l-0.03-0.273c0.114-0.013,2.823-0.286,6.141,1.686
L-266.591,293.035z"/>
<path class="st3" d="M-272.771,301.55c-0.621-2.033-2.623-7.077-2.643-7.128l0.256-0.102c0.02,0.051,2.026,5.105,2.651,7.149
L-272.771,301.55z"/>
<path class="st3" d="M-268.521,301.522l-0.183-0.206c2.195-1.944,2.77-4.569,2.775-4.594l0.269,0.056
C-265.682,296.889-266.255,299.515-268.521,301.522z"/>
<path class="st3" d="M-275.166,304.799c-4.634-0.194-7.09-1.654-7.193-1.716l0.143-0.235c0.024,0.015,2.505,1.485,7.062,1.676
L-275.166,304.799z"/>
<path class="st3" d="M-283.976,299.317l-0.243-0.128c0.014-0.028,1.497-2.83,4.711-6.083l0.195,0.193
C-282.498,296.524-283.961,299.289-283.976,299.317z"/>
<path class="st3" d="M-275.562,288.15c0.005-0.012,0.491-1.07,1.092-1.777c-0.107-0.014-0.215-0.029-0.324-0.039
c-0.569,0.721-0.996,1.653-1.019,1.702L-275.562,288.15z"/>
<path class="st3" d="M-269.523,305.4c0.283,0.087,0.616,0.13,0.889,0.151c0.097-0.084,0.191-0.171,0.285-0.259
c-0.274-0.011-0.736-0.045-1.093-0.155L-269.523,305.4z"/>
<path class="st3" d="M-273.057,306.556l-0.264-0.076c-0.004,0.015-0.387,1.29-1.865,1.776c0.274-0.017,0.545-0.044,0.814-0.082
C-273.356,307.539-273.061,306.569-273.057,306.556z"/>
<path class="st3" d="M-286.617,297.886l0.272-0.039c-0.29-2.006-0.046-3.595,0.256-4.667c-0.298,0.74-0.517,1.518-0.649,2.327
C-286.769,296.218-286.744,297.01-286.617,297.886z"/>
<path class="st1" d="M-268.219,301.032c-0.455-0.019-3.69-0.329-4.843-0.711c-0.386,0.456-3.079,3.552-3.881,4.186
c0.283,0.38,1.581,2.177,3.176,3.002c0.505-0.069,2.905-0.458,4.914-2.123C-268.177,303.46-268.097,302.384-268.219,301.032z"/>
<path class="st1" d="M-271.62,291.248c-0.309-0.456-2.326-3.071-3.747-3.69c-0.612,0.1-3.627,0.693-5.368,1.982
c-0.094,1.066-0.1,3.557,0.172,4.956c0.562,0.104,3.552,0.649,5.768,1.05C-274.269,294.813-272.204,292.033-271.62,291.248z"/>
<path class="st1" d="M-282.727,288.673c-1.442,1.148-2.589,2.649-3.309,4.374c0.569-0.979,2.227-3.251,3.078-3.644
C-283.004,289.02-282.822,288.802-282.727,288.673z"/>
<path class="st1" d="M-283.357,298.053c-1.192-0.145-2.959-0.776-3.195-0.917c-0.208,0.428-0.252,0.712-0.298,0.916
c0.202,2.915,1.54,5.517,3.574,7.367c0.055-0.682,1.791-1.719,2.071-1.928C-282.26,301.956-283.221,298.595-283.357,298.053z"/>
<path class="st1" d="M-267.707,289.921c0.435,0.617,0.378,1.471-0.195,2.318c1.261,2.028,1.808,5.02,1.847,5.372
c0.51-0.177,1.086,0.041,1.155,0.292c0.012-0.207,0.019-0.415,0.019-0.624C-264.882,294.45-265.951,291.869-267.707,289.921z"/>
<radialGradient id="SVGID_3_" cx="639.9374" cy="854.3203" r="8.0916" gradientTransform="matrix(0.3583 0.4654 0.7924 -0.61 -1185.5491 516.2996)" gradientUnits="userSpaceOnUse">
<stop offset="0" style="stop-color:#FFFFFF"/>
<stop offset="1" style="stop-color:#FFFFFF;stop-opacity:0"/>
</radialGradient>
<path class="st4" d="M-276.397,296.757c-3.541,2.726-7.71,3.25-9.311,1.17c-1.602-2.08-0.029-5.976,3.512-8.702
s7.71-3.25,9.311-1.17C-271.283,290.135-272.855,294.031-276.397,296.757z"/>
</g>
</svg>
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 19.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1"
id="Слой_1" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:cc="http://creativecommons.org/ns#" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" width="24px" height="24px"
viewBox="-374.034 286.28 24 24" style="enable-background:new -374.034 286.28 24 24;" xml:space="preserve">
<style type="text/css">
.st0{fill:url(#SVGID_1_);}
.st1{fill:#1A1626;}
.st2{fill:url(#SVGID_2_);}
.st3{fill:#B3C2C6;}
.st4{opacity:0.3;fill:url(#SVGID_3_);enable-background:new ;}
</style>
<g>
<radialGradient id="SVGID_1_" cx="-273.9293" cy="283.2144" r="13.0467" gradientTransform="matrix(1.0506 0 0 -1.0506 35.403 593.0075)" gradientUnits="userSpaceOnUse">
<stop offset="0" style="stop-color:#FF8800"/>
<stop offset="0.2913" style="stop-color:#FC8500"/>
<stop offset="0.5282" style="stop-color:#F27E00"/>
<stop offset="0.7453" style="stop-color:#E17000"/>
<stop offset="0.9273" style="stop-color:#CC6000"/>
</radialGradient>
<circle class="st0" cx="-250.215" cy="298.28" r="12"/>
<path class="st1" d="M-244.272,301.023c-4.416-1.463-8.23-2.726-9.737-5.76c-0.101-0.203-0.184-0.412-0.252-0.625
c5.741-2.206,12.524-2.715,15.4-0.253c-0.1-0.292-0.21-0.578-0.332-0.858c-1.388-0.942-3.755-1.482-6.411-1.374
c-2.78,0.113-5.943,0.798-8.805,1.88c-0.214-1.258,0.104-2.612,0.929-3.808c1.42-2.058,3.935-3.264,6.918-3.377
c-0.37-0.118-0.747-0.219-1.132-0.301c-3.021,0.281-5.06,1.528-6.299,3.325c-0.947,1.374-1.292,2.942-1.006,4.393
c-0.489,0.197-0.969,0.405-1.434,0.625c-0.347,0.164-0.68,0.332-1,0.502c-0.325-1.315-0.498-2.592-0.481-3.762
c0.015-0.992,0.181-2.171,0.744-3.128c-0.271,0.193-0.533,0.397-0.787,0.612c-1.009,2.316-0.405,5.122-0.032,6.584
c-0.627,0.358-1.196,0.726-1.704,1.103c-0.324-1.153-0.808-2.243-1.572-3.21c-0.096,0.226-0.185,0.456-0.267,0.688
c0.626,0.89,1.029,1.887,1.303,2.941c-0.871,0.719-1.521,1.461-1.934,2.206c0.028,0.302,0.068,0.601,0.119,0.896
c0.352-0.701,0.924-1.523,1.971-2.432c0.252,1.21,0.368,2.478,0.481,3.729c0.169,1.869,0.4,4.327,1.315,5.553
c0.325,0.295,0.667,0.573,1.024,0.831c-1.277-1.573-1.487-3.885-1.718-6.44c-0.125-1.382-0.252-2.782-0.557-4.116
c0.5-0.384,1.071-0.761,1.708-1.13c0.515,1.8,1.29,3.686,2.28,5.49c1.993,3.633,4.65,7.323,8.641,8.011
c0.342-0.098,0.678-0.211,1.008-0.339c-0.762-0.01-1.56-0.218-2.392-0.626c-3.698-1.818-7.505-7.55-8.986-12.843
c0.35-0.189,0.718-0.375,1.104-0.557c0.432-0.204,0.874-0.398,1.323-0.582c0.075,0.228,0.167,0.453,0.276,0.672
c1.622,3.266,5.551,4.568,10.099,6.074c1.663,0.551,3.37,1.117,5.095,1.813c0.089-0.188,0.174-0.377,0.253-0.57
C-240.869,302.152-242.592,301.58-244.272,301.023z"/>
</g>
<path d="M-362.034,286.471c-6.512,0-11.81,5.297-11.81,11.809c0,6.511,5.298,11.809,11.81,11.809s11.81-5.298,11.81-11.809
C-350.224,291.768-355.522,286.471-362.034,286.471z M-352.229,302.031c-1.178-0.464-3.67-1.406-7.588-2.693
c-1.718-0.564-3.107-1.309-4.131-2.213c-0.637-0.565-1.089-1.286-1.357-2.063c1.248-0.442,2.674-0.83,4.321-1.11
c4.701-0.8,7.352-0.115,8.773,0.628c0.436,1.153,0.686,2.398,0.686,3.702C-351.525,299.603-351.781,300.864-352.229,302.031z
M-353.059,292.841c-2.134-0.667-4.857-0.732-8.144-0.171c-1.647,0.281-3.087,0.666-4.363,1.108
c-0.042-0.673,0.051-1.352,0.309-1.984c1.096-2.679,4.346-3.48,5.691-3.701c0.01-0.002,0.017-0.008,0.027-0.01
C-356.801,288.753-354.487,290.495-353.059,292.841z M-368.421,289.954c1.534-1.179,3.395-1.949,5.428-2.134
c-1.378,0.675-2.759,1.753-3.466,3.483c-0.389,0.953-0.497,1.968-0.373,2.956c-0.609,0.253-1.174,0.516-1.693,0.788
C-369.016,292.507-368.69,290.799-368.421,289.954z M-370.03,291.478c-0.098,1.091-0.059,2.513,0.32,4.243
c-0.337,0.21-0.656,0.42-0.947,0.63c-0.376-1.065-0.797-1.794-1.05-2.176C-371.287,293.188-370.716,292.283-370.03,291.478z
M-371.271,299.562c0.061,0.609,0.105,1.16,0.147,1.669c0.089,1.08,0.167,1.988,0.343,2.862c-0.846-1.269-1.413-2.733-1.641-4.307
c0.237-0.31,0.571-0.701,1.024-1.139C-371.348,298.938-371.303,299.24-371.271,299.562z M-372.525,297.938
c0.023-0.71,0.115-1.402,0.273-2.071c0.171,0.365,0.347,0.812,0.509,1.338C-372.039,297.461-372.295,297.705-372.525,297.938z
M-368.472,306.568c-0.009-0.02-0.009-0.042-0.02-0.062c-1.045-1.829-1.154-3.166-1.336-5.382c-0.042-0.516-0.088-1.074-0.15-1.691
c-0.064-0.641-0.172-1.217-0.297-1.751c0.276-0.212,0.581-0.427,0.909-0.643c0.346,1.167,0.836,2.446,1.535,3.844
c2.354,4.709,4.82,6.874,6.733,7.859c-0.309,0.027-0.62,0.047-0.936,0.047C-364.46,308.788-366.691,307.954-368.472,306.568z
M-358.651,308.219c-0.029-0.009-0.054-0.027-0.085-0.032c-1.616-0.252-4.795-1.612-7.933-7.888c-0.73-1.46-1.223-2.776-1.55-3.95
c0.512-0.281,1.074-0.555,1.691-0.817c0.342,0.96,0.913,1.851,1.718,2.565c1.156,1.023,2.699,1.856,4.586,2.475
c3.835,1.26,6.278,2.181,7.461,2.645C-354.011,305.551-356.115,307.353-358.651,308.219z"/>
<g>
<radialGradient id="SVGID_2_" cx="-298.1872" cy="283.9428" r="11.9595" gradientTransform="matrix(1.0506 0 0 -1.0506 35.403 593.0075)" gradientUnits="userSpaceOnUse">
<stop offset="0.2727" style="stop-color:#FAFDFF"/>
<stop offset="0.4936" style="stop-color:#F7FAFC"/>
<stop offset="0.6732" style="stop-color:#EDF2F4"/>
<stop offset="0.8379" style="stop-color:#DCE3E7"/>
<stop offset="0.9273" style="stop-color:#CFD8DD"/>
</radialGradient>
<circle class="st2" cx="-275.882" cy="297.28" r="11"/>
<path class="st3" d="M-279.516,290.078c-2.133-0.776-3.766-0.544-3.783-0.542l-0.041-0.272c0.069-0.01,1.72-0.244,3.917,0.555
L-279.516,290.078z"/>
<path class="st3" d="M-266.591,293.035c-3.241-1.925-5.943-1.651-5.97-1.649l-0.03-0.273c0.114-0.013,2.823-0.286,6.141,1.686
L-266.591,293.035z"/>
<path class="st3" d="M-272.771,301.55c-0.621-2.033-2.623-7.077-2.643-7.128l0.256-0.102c0.02,0.051,2.026,5.105,2.651,7.149
L-272.771,301.55z"/>
<path class="st3" d="M-268.521,301.522l-0.183-0.206c2.195-1.944,2.77-4.569,2.775-4.594l0.269,0.056
C-265.682,296.889-266.255,299.515-268.521,301.522z"/>
<path class="st3" d="M-275.166,304.799c-4.634-0.194-7.09-1.654-7.193-1.716l0.143-0.235c0.024,0.015,2.505,1.485,7.062,1.676
L-275.166,304.799z"/>
<path class="st3" d="M-283.976,299.317l-0.243-0.128c0.014-0.028,1.497-2.83,4.711-6.083l0.195,0.193
C-282.498,296.524-283.961,299.289-283.976,299.317z"/>
<path class="st3" d="M-275.562,288.15c0.005-0.012,0.491-1.07,1.092-1.777c-0.107-0.014-0.215-0.029-0.324-0.039
c-0.569,0.721-0.996,1.653-1.019,1.702L-275.562,288.15z"/>
<path class="st3" d="M-269.523,305.4c0.283,0.087,0.616,0.13,0.889,0.151c0.097-0.084,0.191-0.171,0.285-0.259
c-0.274-0.011-0.736-0.045-1.093-0.155L-269.523,305.4z"/>
<path class="st3" d="M-273.057,306.556l-0.264-0.076c-0.004,0.015-0.387,1.29-1.865,1.776c0.274-0.017,0.545-0.044,0.814-0.082
C-273.356,307.539-273.061,306.569-273.057,306.556z"/>
<path class="st3" d="M-286.617,297.886l0.272-0.039c-0.29-2.006-0.046-3.595,0.256-4.667c-0.298,0.74-0.517,1.518-0.649,2.327
C-286.769,296.218-286.744,297.01-286.617,297.886z"/>
<path class="st1" d="M-268.219,301.032c-0.455-0.019-3.69-0.329-4.843-0.711c-0.386,0.456-3.079,3.552-3.881,4.186
c0.283,0.38,1.581,2.177,3.176,3.002c0.505-0.069,2.905-0.458,4.914-2.123C-268.177,303.46-268.097,302.384-268.219,301.032z"/>
<path class="st1" d="M-271.62,291.248c-0.309-0.456-2.326-3.071-3.747-3.69c-0.612,0.1-3.627,0.693-5.368,1.982
c-0.094,1.066-0.1,3.557,0.172,4.956c0.562,0.104,3.552,0.649,5.768,1.05C-274.269,294.813-272.204,292.033-271.62,291.248z"/>
<path class="st1" d="M-282.727,288.673c-1.442,1.148-2.589,2.649-3.309,4.374c0.569-0.979,2.227-3.251,3.078-3.644
C-283.004,289.02-282.822,288.802-282.727,288.673z"/>
<path class="st1" d="M-283.357,298.053c-1.192-0.145-2.959-0.776-3.195-0.917c-0.208,0.428-0.252,0.712-0.298,0.916
c0.202,2.915,1.54,5.517,3.574,7.367c0.055-0.682,1.791-1.719,2.071-1.928C-282.26,301.956-283.221,298.595-283.357,298.053z"/>
<path class="st1" d="M-267.707,289.921c0.435,0.617,0.378,1.471-0.195,2.318c1.261,2.028,1.808,5.02,1.847,5.372
c0.51-0.177,1.086,0.041,1.155,0.292c0.012-0.207,0.019-0.415,0.019-0.624C-264.882,294.45-265.951,291.869-267.707,289.921z"/>
<radialGradient id="SVGID_3_" cx="639.9374" cy="854.3203" r="8.0916" gradientTransform="matrix(0.3583 0.4654 0.7924 -0.61 -1185.5491 516.2996)" gradientUnits="userSpaceOnUse">
<stop offset="0" style="stop-color:#FFFFFF"/>
<stop offset="1" style="stop-color:#FFFFFF;stop-opacity:0"/>
</radialGradient>
<path class="st4" d="M-276.397,296.757c-3.541,2.726-7.71,3.25-9.311,1.17c-1.602-2.08-0.029-5.976,3.512-8.702
s7.71-3.25,9.311-1.17C-271.283,290.135-272.855,294.031-276.397,296.757z"/>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 8.7 KiB

After

Width:  |  Height:  |  Size: 8.6 KiB

Before After
Before After

View file

@ -1,11 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 19.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1"
id="Слой_1" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns:cc="http://creativecommons.org/ns#" xmlns:svg="http://www.w3.org/2000/svg" xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" width="24px" height="24px"
viewBox="-370.6 286.3 24 24" style="enable-background:new -370.6 286.3 24 24;" xml:space="preserve">
<path d="M-348.5,292.7c-3.2-2.3-6.3-2.1-9.4-1.9c-3.5,0.2-6.8,0.4-10.1-3c0,0,0,0,0,0v-0.2c0-0.4-0.3-0.7-0.7-0.7s-0.7,0.3-0.7,0.7
V309c0,0.4,0.3,0.7,0.7,0.7s0.7-0.3,0.7-0.7v-7c0.6,0.2,1.2,0.3,1.9,0.3c3,0,5.8-1.9,8.7-3.8c2.7-1.8,5.5-3.7,8.2-3.6
c0.5,0,1-0.3,1.2-0.8C-347.9,293.6-348,293-348.5,292.7z M-358.2,297.4c-3.4,2.3-6.6,4.4-9.8,3.2v-11c3.4,3,6.9,2.8,10.2,2.6
c2.8-0.2,5.5-0.3,8.2,1.4C-352.5,293.6-355.4,295.5-358.2,297.4z"/>
</svg>
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 19.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1"
id="Слой_1" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns:cc="http://creativecommons.org/ns#" xmlns:svg="http://www.w3.org/2000/svg" xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" width="24px" height="24px"
viewBox="-370.6 286.3 24 24" style="enable-background:new -370.6 286.3 24 24;" xml:space="preserve">
<path d="M-348.5,292.7c-3.2-2.3-6.3-2.1-9.4-1.9c-3.5,0.2-6.8,0.4-10.1-3c0,0,0,0,0,0v-0.2c0-0.4-0.3-0.7-0.7-0.7s-0.7,0.3-0.7,0.7
V309c0,0.4,0.3,0.7,0.7,0.7s0.7-0.3,0.7-0.7v-7c0.6,0.2,1.2,0.3,1.9,0.3c3,0,5.8-1.9,8.7-3.8c2.7-1.8,5.5-3.7,8.2-3.6
c0.5,0,1-0.3,1.2-0.8C-347.9,293.6-348,293-348.5,292.7z M-358.2,297.4c-3.4,2.3-6.6,4.4-9.8,3.2v-11c3.4,3,6.9,2.8,10.2,2.6
c2.8-0.2,5.5-0.3,8.2,1.4C-352.5,293.6-355.4,295.5-358.2,297.4z"/>
</svg>

Before

Width:  |  Height:  |  Size: 1 KiB

After

Width:  |  Height:  |  Size: 1,017 B

Before After
Before After

View file

@ -1,34 +1,34 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 19.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1"
id="Слой_1" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:cc="http://creativecommons.org/ns#" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" width="24px" height="24px"
viewBox="-417.263 286.28 24 24" style="enable-background:new -417.263 286.28 24 24;" xml:space="preserve">
<g>
<path d="M-393.767,303.396c0-0.382-0.149-0.914-0.752-1.439c0.199-0.307,0.316-0.652,0.316-1.044c0-2.985-3.069-5.414-6.841-5.414
c-0.822,0-1.607,0.121-2.338,0.332c0.14-1.334,0.254-2.142,0.295-2.434l0.033-0.246c0-0.733-0.535-1.478-3.935-1.736l0.877-2.943
l5.089,0.765c0.356,0.054,0.686-0.19,0.739-0.546c0.054-0.355-0.191-0.686-0.546-0.739l-5.226-0.786
c-0.554-0.082-1.104,0.263-1.264,0.804l-1.007,3.377c-0.487-0.016-1.003-0.027-1.582-0.027c-6.039,0-6.853,0.926-6.853,1.832
l0.031,0.244c0.295,2.521,0.839,7.809,1.532,14.89c0.002,0.019,0.012,0.035,0.016,0.053c0.094,0.67,0.965,0.823,2.016,0.931
c0.863,0.089,2.021,0.138,3.258,0.138c3.375,0,4.594-0.267,5.038-0.598c0.942,0.367,2.181,0.598,3.825,0.598
c4.858,0,6.245-1.991,6.552-3.659c0.015-0.076,0.03-0.304,0.041-0.5c0,0,0.011-0.377-0.012-0.456
C-394.024,304.394-393.767,303.929-393.767,303.396z M-407.021,303.396c0-0.157,0.161-0.359,0.427-0.558
c0.295,0.177,0.644,0.347,1.08,0.498c1.189,0.412,2.777,0.639,4.469,0.639c1.997,0,4.204-0.323,5.558-1.132
c0.262,0.197,0.419,0.397,0.419,0.553c0,0.608-2.11,1.792-5.977,1.792S-407.021,304.004-407.021,303.396z M-395.502,300.912
c0,0.852-2.227,1.763-5.542,1.763s-5.542-0.911-5.542-1.763c0-2.269,2.486-4.114,5.542-4.114
C-397.988,296.798-395.502,298.644-395.502,300.912z M-405.301,292.942c-0.551,0.083-1.32,0.144-2.196,0.182l0.128-0.428
C-406.461,292.756-405.78,292.845-405.301,292.942z M-408.708,292.635l-0.158,0.53c-2.109,0.036-4.42-0.038-5.645-0.223
c0.849-0.172,2.297-0.323,4.605-0.323C-409.476,292.619-409.081,292.625-408.708,292.635z M-413.933,307.844
c-0.61-6.221-1.101-11.027-1.407-13.747c0.387,0.097,0.853,0.155,1.352,0.206c1.081,0.111,2.53,0.172,4.082,0.172
c2.933,0,4.534-0.157,5.415-0.38c-0.068,0.538-0.155,1.289-0.251,2.269c-1.888,0.965-3.143,2.641-3.143,4.548
c0,0.311,0.085,0.673,0.322,1.039c-0.608,0.528-0.757,1.061-0.757,1.444c0,0.534,0.258,0.999,0.697,1.397
c-0.023,0.078-0.013,0.449-0.013,0.449c0.01,0.195,0.026,0.426,0.04,0.504c0.135,0.734,0.48,1.53,1.237,2.192
C-408.192,308.196-412.769,308.164-413.933,307.844z M-401.043,308.106c-3.134,0-4.896-0.851-5.25-2.51
c1.391,0.588,3.324,0.892,5.249,0.892c1.925,0,3.857-0.304,5.248-0.892C-396.159,307.259-397.918,308.106-401.043,308.106z"/>
<circle cx="-397.252" cy="300.406" r="0.588"/>
<circle cx="-401.151" cy="301.26" r="0.588"/>
<circle cx="-404.836" cy="300.406" r="0.588"/>
<circle cx="-402.731" cy="298.478" r="0.588"/>
<circle cx="-399.523" cy="298.478" r="0.588"/>
</g>
</svg>
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 19.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1"
id="Слой_1" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:cc="http://creativecommons.org/ns#" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" width="24px" height="24px"
viewBox="-417.263 286.28 24 24" style="enable-background:new -417.263 286.28 24 24;" xml:space="preserve">
<g>
<path d="M-393.767,303.396c0-0.382-0.149-0.914-0.752-1.439c0.199-0.307,0.316-0.652,0.316-1.044c0-2.985-3.069-5.414-6.841-5.414
c-0.822,0-1.607,0.121-2.338,0.332c0.14-1.334,0.254-2.142,0.295-2.434l0.033-0.246c0-0.733-0.535-1.478-3.935-1.736l0.877-2.943
l5.089,0.765c0.356,0.054,0.686-0.19,0.739-0.546c0.054-0.355-0.191-0.686-0.546-0.739l-5.226-0.786
c-0.554-0.082-1.104,0.263-1.264,0.804l-1.007,3.377c-0.487-0.016-1.003-0.027-1.582-0.027c-6.039,0-6.853,0.926-6.853,1.832
l0.031,0.244c0.295,2.521,0.839,7.809,1.532,14.89c0.002,0.019,0.012,0.035,0.016,0.053c0.094,0.67,0.965,0.823,2.016,0.931
c0.863,0.089,2.021,0.138,3.258,0.138c3.375,0,4.594-0.267,5.038-0.598c0.942,0.367,2.181,0.598,3.825,0.598
c4.858,0,6.245-1.991,6.552-3.659c0.015-0.076,0.03-0.304,0.041-0.5c0,0,0.011-0.377-0.012-0.456
C-394.024,304.394-393.767,303.929-393.767,303.396z M-407.021,303.396c0-0.157,0.161-0.359,0.427-0.558
c0.295,0.177,0.644,0.347,1.08,0.498c1.189,0.412,2.777,0.639,4.469,0.639c1.997,0,4.204-0.323,5.558-1.132
c0.262,0.197,0.419,0.397,0.419,0.553c0,0.608-2.11,1.792-5.977,1.792S-407.021,304.004-407.021,303.396z M-395.502,300.912
c0,0.852-2.227,1.763-5.542,1.763s-5.542-0.911-5.542-1.763c0-2.269,2.486-4.114,5.542-4.114
C-397.988,296.798-395.502,298.644-395.502,300.912z M-405.301,292.942c-0.551,0.083-1.32,0.144-2.196,0.182l0.128-0.428
C-406.461,292.756-405.78,292.845-405.301,292.942z M-408.708,292.635l-0.158,0.53c-2.109,0.036-4.42-0.038-5.645-0.223
c0.849-0.172,2.297-0.323,4.605-0.323C-409.476,292.619-409.081,292.625-408.708,292.635z M-413.933,307.844
c-0.61-6.221-1.101-11.027-1.407-13.747c0.387,0.097,0.853,0.155,1.352,0.206c1.081,0.111,2.53,0.172,4.082,0.172
c2.933,0,4.534-0.157,5.415-0.38c-0.068,0.538-0.155,1.289-0.251,2.269c-1.888,0.965-3.143,2.641-3.143,4.548
c0,0.311,0.085,0.673,0.322,1.039c-0.608,0.528-0.757,1.061-0.757,1.444c0,0.534,0.258,0.999,0.697,1.397
c-0.023,0.078-0.013,0.449-0.013,0.449c0.01,0.195,0.026,0.426,0.04,0.504c0.135,0.734,0.48,1.53,1.237,2.192
C-408.192,308.196-412.769,308.164-413.933,307.844z M-401.043,308.106c-3.134,0-4.896-0.851-5.25-2.51
c1.391,0.588,3.324,0.892,5.249,0.892c1.925,0,3.857-0.304,5.248-0.892C-396.159,307.259-397.918,308.106-401.043,308.106z"/>
<circle cx="-397.252" cy="300.406" r="0.588"/>
<circle cx="-401.151" cy="301.26" r="0.588"/>
<circle cx="-404.836" cy="300.406" r="0.588"/>
<circle cx="-402.731" cy="298.478" r="0.588"/>
<circle cx="-399.523" cy="298.478" r="0.588"/>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 3 KiB

After

Width:  |  Height:  |  Size: 3 KiB

Before After
Before After

View file

@ -1,39 +1,39 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 19.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1"
id="Слой_1" xmlns:cc="http://creativecommons.org/ns#" xmlns:svg="http://www.w3.org/2000/svg" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" width="24px" height="24px"
viewBox="-243.5 286.28 24 24" enable-background="new -243.5 286.28 24 24" xml:space="preserve">
<g>
<path d="M-219.75,305.024l-0.031-0.957c-0.005-0.148-0.124-3.574-1.288-6.818c0.654-2.156,1.299-9.727-0.311-10.484
c-1.981-0.93-5.527,1.601-7.634,3.358c-1.786-0.504-3.178-0.506-4.972,0c-2.108-1.758-5.652-4.288-7.634-3.358
c-1.612,0.757-0.966,8.327-0.311,10.483c-1.165,3.244-1.284,6.67-1.288,6.818l-0.029,0.957l0.896-0.337
c0.012-0.004,0.811-0.3,1.81-0.425c-0.546,0.886-0.864,1.883-0.915,2.048l-0.526,1.711l1.494-0.987
c1.088-0.717,2.972-1.525,3.556-0.984c0.217,0.201,0.351,0.555,0.507,0.967c0.475,1.257,1.128,2.979,4.928,2.983
c3.798-0.004,4.451-1.726,4.926-2.983c0.156-0.412,0.29-0.767,0.508-0.968c0.587-0.546,2.468,0.268,3.555,0.984l1.495,0.987
l-0.528-1.711c-0.051-0.165-0.37-1.162-0.915-2.048c0.999,0.125,1.798,0.421,1.81,0.425L-219.75,305.024z M-223.97,302.957
l-1.906,0.202l1.643,0.987c0.263,0.157,0.528,0.509,0.764,0.921c-1.137-0.478-2.564-0.796-3.467,0.04
c-0.439,0.406-0.64,0.939-0.836,1.455c-0.418,1.107-0.814,2.152-3.726,2.154c-2.914-0.002-3.309-1.047-3.728-2.154
c-0.196-0.516-0.397-1.049-0.835-1.454c-0.406-0.376-0.918-0.52-1.462-0.52c-0.666,0-1.381,0.215-2.007,0.479
c0.235-0.413,0.499-0.764,0.763-0.922l1.645-0.987l-1.907-0.202c-1.05-0.112-2.115,0.069-2.844,0.25
c0.113-1.243,0.419-3.576,1.224-5.725l0.086-0.229l-0.088-0.228c-0.785-2.031-0.897-8.197-0.351-9.128
c1.019-0.356,3.639,1.034,6.454,3.428l0.269,0.229l0.337-0.105c0.888-0.277,1.733-0.424,2.443-0.424
c0.712,0,1.556,0.147,2.442,0.424l0.338,0.106l0.269-0.229c2.812-2.394,5.434-3.786,6.454-3.428
c0.546,0.932,0.433,7.098-0.351,9.129l-0.088,0.228l0.086,0.229c0.803,2.148,1.111,4.48,1.224,5.724
C-221.855,303.027-222.915,302.845-223.97,302.957z"/>
<path d="M-231.5,304.314c-1.372,0-2.486,0.219-2.486,1.365c0,0.912,1.98,1.721,2.486,1.721c0.508,0,2.485-0.808,2.485-1.721
C-229.015,304.534-230.126,304.314-231.5,304.314z"/>
<ellipse cx="-235.816" cy="299.071" rx="1.665" ry="2.141"/>
<ellipse cx="-227.184" cy="299.072" rx="1.665" ry="2.14"/>
<path d="M-236.935,292.221c-0.402-0.887-1.01-1.691-1.758-2.326l-1.365-1.16l0.318,1.764c0.001,0.006,0.08,0.45,0.124,1.099
c-0.091-0.027-0.182-0.048-0.276-0.064l-0.935-0.158l0.201,0.927c0.002,0.01,0.22,1.046,0.017,2.324
c-0.056,0.35,0.183,0.679,0.533,0.735c0.034,0.005,0.067,0.008,0.101,0.008c0.31,0,0.582-0.225,0.633-0.541
c0.085-0.531,0.108-1.025,0.102-1.445l0.889,0.954l0.042-1.566c0.004-0.141,0.006-0.28,0.005-0.414
c0.079,0.142,0.146,0.274,0.2,0.394c0.146,0.322,0.527,0.464,0.849,0.319C-236.931,292.924-236.789,292.544-236.935,292.221z"/>
<path d="M-222.375,292.304l0.201-0.929l-0.935,0.16c-0.094,0.016-0.186,0.037-0.276,0.064c0.044-0.649,0.123-1.093,0.124-1.099
l0.318-1.764l-1.365,1.16c-0.946,0.803-1.477,1.708-1.757,2.326c-0.147,0.323-0.003,0.703,0.32,0.849
c0.321,0.145,0.702,0.004,0.849-0.32c0.054-0.119,0.12-0.251,0.199-0.392c-0.001,0.135,0.001,0.273,0.005,0.414l0.042,1.568
l0.889-0.956c-0.006,0.42,0.017,0.914,0.102,1.445c0.05,0.316,0.323,0.541,0.633,0.541c0.034,0,0.067-0.002,0.101-0.008
c0.35-0.056,0.589-0.385,0.533-0.735C-222.594,293.35-222.377,292.314-222.375,292.304z"/>
</g>
</svg>
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 19.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1"
id="Слой_1" xmlns:cc="http://creativecommons.org/ns#" xmlns:svg="http://www.w3.org/2000/svg" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" width="24px" height="24px"
viewBox="-243.5 286.28 24 24" enable-background="new -243.5 286.28 24 24" xml:space="preserve">
<g>
<path d="M-219.75,305.024l-0.031-0.957c-0.005-0.148-0.124-3.574-1.288-6.818c0.654-2.156,1.299-9.727-0.311-10.484
c-1.981-0.93-5.527,1.601-7.634,3.358c-1.786-0.504-3.178-0.506-4.972,0c-2.108-1.758-5.652-4.288-7.634-3.358
c-1.612,0.757-0.966,8.327-0.311,10.483c-1.165,3.244-1.284,6.67-1.288,6.818l-0.029,0.957l0.896-0.337
c0.012-0.004,0.811-0.3,1.81-0.425c-0.546,0.886-0.864,1.883-0.915,2.048l-0.526,1.711l1.494-0.987
c1.088-0.717,2.972-1.525,3.556-0.984c0.217,0.201,0.351,0.555,0.507,0.967c0.475,1.257,1.128,2.979,4.928,2.983
c3.798-0.004,4.451-1.726,4.926-2.983c0.156-0.412,0.29-0.767,0.508-0.968c0.587-0.546,2.468,0.268,3.555,0.984l1.495,0.987
l-0.528-1.711c-0.051-0.165-0.37-1.162-0.915-2.048c0.999,0.125,1.798,0.421,1.81,0.425L-219.75,305.024z M-223.97,302.957
l-1.906,0.202l1.643,0.987c0.263,0.157,0.528,0.509,0.764,0.921c-1.137-0.478-2.564-0.796-3.467,0.04
c-0.439,0.406-0.64,0.939-0.836,1.455c-0.418,1.107-0.814,2.152-3.726,2.154c-2.914-0.002-3.309-1.047-3.728-2.154
c-0.196-0.516-0.397-1.049-0.835-1.454c-0.406-0.376-0.918-0.52-1.462-0.52c-0.666,0-1.381,0.215-2.007,0.479
c0.235-0.413,0.499-0.764,0.763-0.922l1.645-0.987l-1.907-0.202c-1.05-0.112-2.115,0.069-2.844,0.25
c0.113-1.243,0.419-3.576,1.224-5.725l0.086-0.229l-0.088-0.228c-0.785-2.031-0.897-8.197-0.351-9.128
c1.019-0.356,3.639,1.034,6.454,3.428l0.269,0.229l0.337-0.105c0.888-0.277,1.733-0.424,2.443-0.424
c0.712,0,1.556,0.147,2.442,0.424l0.338,0.106l0.269-0.229c2.812-2.394,5.434-3.786,6.454-3.428
c0.546,0.932,0.433,7.098-0.351,9.129l-0.088,0.228l0.086,0.229c0.803,2.148,1.111,4.48,1.224,5.724
C-221.855,303.027-222.915,302.845-223.97,302.957z"/>
<path d="M-231.5,304.314c-1.372,0-2.486,0.219-2.486,1.365c0,0.912,1.98,1.721,2.486,1.721c0.508,0,2.485-0.808,2.485-1.721
C-229.015,304.534-230.126,304.314-231.5,304.314z"/>
<ellipse cx="-235.816" cy="299.071" rx="1.665" ry="2.141"/>
<ellipse cx="-227.184" cy="299.072" rx="1.665" ry="2.14"/>
<path d="M-236.935,292.221c-0.402-0.887-1.01-1.691-1.758-2.326l-1.365-1.16l0.318,1.764c0.001,0.006,0.08,0.45,0.124,1.099
c-0.091-0.027-0.182-0.048-0.276-0.064l-0.935-0.158l0.201,0.927c0.002,0.01,0.22,1.046,0.017,2.324
c-0.056,0.35,0.183,0.679,0.533,0.735c0.034,0.005,0.067,0.008,0.101,0.008c0.31,0,0.582-0.225,0.633-0.541
c0.085-0.531,0.108-1.025,0.102-1.445l0.889,0.954l0.042-1.566c0.004-0.141,0.006-0.28,0.005-0.414
c0.079,0.142,0.146,0.274,0.2,0.394c0.146,0.322,0.527,0.464,0.849,0.319C-236.931,292.924-236.789,292.544-236.935,292.221z"/>
<path d="M-222.375,292.304l0.201-0.929l-0.935,0.16c-0.094,0.016-0.186,0.037-0.276,0.064c0.044-0.649,0.123-1.093,0.124-1.099
l0.318-1.764l-1.365,1.16c-0.946,0.803-1.477,1.708-1.757,2.326c-0.147,0.323-0.003,0.703,0.32,0.849
c0.321,0.145,0.702,0.004,0.849-0.32c0.054-0.119,0.12-0.251,0.199-0.392c-0.001,0.135,0.001,0.273,0.005,0.414l0.042,1.568
l0.889-0.956c-0.006,0.42,0.017,0.914,0.102,1.445c0.05,0.316,0.323,0.541,0.633,0.541c0.034,0,0.067-0.002,0.101-0.008
c0.35-0.056,0.589-0.385,0.533-0.735C-222.594,293.35-222.377,292.314-222.375,292.304z"/>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 3.6 KiB

After

Width:  |  Height:  |  Size: 3.6 KiB

Before After
Before After

View file

@ -1,30 +1,30 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 19.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1"
id="Слой_1" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:cc="http://creativecommons.org/ns#" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" width="24px" height="24px"
viewBox="-219.75 286.28 24 24" style="enable-background:new -219.75 286.28 24 24;" xml:space="preserve">
<path d="M-207.75,286.42c-4.539,0-8.232,3.282-8.232,7.315c0,2.194,1.165,3.995,2.291,5.737c0.461,0.714,0.896,1.387,1.237,2.066
c0.103,0.203,0.1,0.362,0.096,0.582c-0.005,0.33-0.011,0.808,0.428,1.273c0.119,0.656,0.492,2.655,0.745,3.395
c0.153,0.449,0.469,0.83,0.896,1.13v0.056c0,1.194,1.143,2.167,2.546,2.167s2.546-0.973,2.546-2.167v-0.056
c0.428-0.3,0.744-0.68,0.896-1.129c0.255-0.747,0.631-2.77,0.747-3.411c0.425-0.46,0.417-0.932,0.412-1.258
c-0.004-0.22-0.007-0.379,0.096-0.582c0.341-0.679,0.776-1.353,1.237-2.065c1.126-1.742,2.291-3.543,2.291-5.738
C-199.518,289.702-203.211,286.42-207.75,286.42z M-210.487,304.039c1.74,0.465,3.747,0.466,5.487-0.001
c-0.06,0.31-0.127,0.643-0.194,0.961c-2.262,0.775-4.417,0.221-5.098,0.002C-210.36,304.682-210.427,304.349-210.487,304.039z
M-207.777,303.084c-0.32-1.199-1.187-4.02-1.496-5.02c0.876-0.481,1.304-0.733,1.522-0.874c0.219,0.141,0.646,0.393,1.522,0.874
c-0.309,1-1.176,3.823-1.496,5.021C-207.741,303.084-207.759,303.084-207.777,303.084z M-207.722,307.324h-0.044
c-1.028-0.005-1.9-0.366-2.147-0.873c0.58,0.133,1.352,0.256,2.242,0.256c0.648,0,1.358-0.074,2.093-0.248
C-205.83,306.961-206.698,307.319-207.722,307.324z M-206.93,308.624c-0.439,0.268-1.188,0.268-1.627,0h0.813h0.001H-206.93z
M-202.9,298.768c-0.46,0.71-0.935,1.446-1.308,2.187c-0.246,0.489-0.239,0.894-0.233,1.188c0.004,0.234,0.001,0.289-0.075,0.363
c-0.562,0.224-1.183,0.378-1.829,0.471c0.37-1.309,1.086-3.64,1.36-4.525c0.251-0.074,0.547-0.192,0.758-0.373
c0.41-0.378,0.593-0.886,0.502-1.385c-0.073-0.432-0.397-0.789-0.816-0.908c-0.223-0.068-0.444-0.073-0.743,0.003
c-0.185,0.065-0.352,0.17-0.492,0.311c-0.169,0.175-0.278,0.348-0.365,0.523c-0.162-0.091-0.317-0.178-0.451-0.254
c0.061-0.123,0.138-0.304,0.163-0.523c0.033-0.279-0.029-0.563-0.182-0.805c-0.164-0.255-0.389-0.439-0.689-0.552
c-0.277-0.094-0.607-0.101-0.916,0.008c-0.28,0.106-0.505,0.289-0.673,0.552c-0.146,0.233-0.208,0.515-0.177,0.797
c0.026,0.215,0.099,0.389,0.163,0.521c-0.137,0.078-0.29,0.166-0.45,0.255c-0.082-0.165-0.194-0.344-0.37-0.526
c-0.132-0.134-0.301-0.24-0.554-0.326c-0.234-0.056-0.457-0.051-0.668,0.014c-0.429,0.123-0.753,0.48-0.823,0.901
c-0.095,0.508,0.088,1.018,0.519,1.412c0.199,0.17,0.489,0.284,0.738,0.356c0.274,0.885,0.99,3.215,1.359,4.523
c-0.65-0.094-1.274-0.25-1.837-0.476c-0.069-0.07-0.071-0.128-0.067-0.356c0.006-0.295,0.013-0.699-0.233-1.188
c-0.373-0.742-0.849-1.477-1.308-2.188c-1.024-1.583-2.082-3.22-2.082-5.031c0-3.317,3.109-6.016,6.932-6.016
s6.932,2.699,6.932,6.016C-200.818,295.546-201.877,297.184-202.9,298.768z"/>
</svg>
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 19.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1"
id="Слой_1" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:cc="http://creativecommons.org/ns#" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" width="24px" height="24px"
viewBox="-219.75 286.28 24 24" style="enable-background:new -219.75 286.28 24 24;" xml:space="preserve">
<path d="M-207.75,286.42c-4.539,0-8.232,3.282-8.232,7.315c0,2.194,1.165,3.995,2.291,5.737c0.461,0.714,0.896,1.387,1.237,2.066
c0.103,0.203,0.1,0.362,0.096,0.582c-0.005,0.33-0.011,0.808,0.428,1.273c0.119,0.656,0.492,2.655,0.745,3.395
c0.153,0.449,0.469,0.83,0.896,1.13v0.056c0,1.194,1.143,2.167,2.546,2.167s2.546-0.973,2.546-2.167v-0.056
c0.428-0.3,0.744-0.68,0.896-1.129c0.255-0.747,0.631-2.77,0.747-3.411c0.425-0.46,0.417-0.932,0.412-1.258
c-0.004-0.22-0.007-0.379,0.096-0.582c0.341-0.679,0.776-1.353,1.237-2.065c1.126-1.742,2.291-3.543,2.291-5.738
C-199.518,289.702-203.211,286.42-207.75,286.42z M-210.487,304.039c1.74,0.465,3.747,0.466,5.487-0.001
c-0.06,0.31-0.127,0.643-0.194,0.961c-2.262,0.775-4.417,0.221-5.098,0.002C-210.36,304.682-210.427,304.349-210.487,304.039z
M-207.777,303.084c-0.32-1.199-1.187-4.02-1.496-5.02c0.876-0.481,1.304-0.733,1.522-0.874c0.219,0.141,0.646,0.393,1.522,0.874
c-0.309,1-1.176,3.823-1.496,5.021C-207.741,303.084-207.759,303.084-207.777,303.084z M-207.722,307.324h-0.044
c-1.028-0.005-1.9-0.366-2.147-0.873c0.58,0.133,1.352,0.256,2.242,0.256c0.648,0,1.358-0.074,2.093-0.248
C-205.83,306.961-206.698,307.319-207.722,307.324z M-206.93,308.624c-0.439,0.268-1.188,0.268-1.627,0h0.813h0.001H-206.93z
M-202.9,298.768c-0.46,0.71-0.935,1.446-1.308,2.187c-0.246,0.489-0.239,0.894-0.233,1.188c0.004,0.234,0.001,0.289-0.075,0.363
c-0.562,0.224-1.183,0.378-1.829,0.471c0.37-1.309,1.086-3.64,1.36-4.525c0.251-0.074,0.547-0.192,0.758-0.373
c0.41-0.378,0.593-0.886,0.502-1.385c-0.073-0.432-0.397-0.789-0.816-0.908c-0.223-0.068-0.444-0.073-0.743,0.003
c-0.185,0.065-0.352,0.17-0.492,0.311c-0.169,0.175-0.278,0.348-0.365,0.523c-0.162-0.091-0.317-0.178-0.451-0.254
c0.061-0.123,0.138-0.304,0.163-0.523c0.033-0.279-0.029-0.563-0.182-0.805c-0.164-0.255-0.389-0.439-0.689-0.552
c-0.277-0.094-0.607-0.101-0.916,0.008c-0.28,0.106-0.505,0.289-0.673,0.552c-0.146,0.233-0.208,0.515-0.177,0.797
c0.026,0.215,0.099,0.389,0.163,0.521c-0.137,0.078-0.29,0.166-0.45,0.255c-0.082-0.165-0.194-0.344-0.37-0.526
c-0.132-0.134-0.301-0.24-0.554-0.326c-0.234-0.056-0.457-0.051-0.668,0.014c-0.429,0.123-0.753,0.48-0.823,0.901
c-0.095,0.508,0.088,1.018,0.519,1.412c0.199,0.17,0.489,0.284,0.738,0.356c0.274,0.885,0.99,3.215,1.359,4.523
c-0.65-0.094-1.274-0.25-1.837-0.476c-0.069-0.07-0.071-0.128-0.067-0.356c0.006-0.295,0.013-0.699-0.233-1.188
c-0.373-0.742-0.849-1.477-1.308-2.188c-1.024-1.583-2.082-3.22-2.082-5.031c0-3.317,3.109-6.016,6.932-6.016
s6.932,2.699,6.932,6.016C-200.818,295.546-201.877,297.184-202.9,298.768z"/>
</svg>

Before

Width:  |  Height:  |  Size: 3.1 KiB

After

Width:  |  Height:  |  Size: 3.1 KiB

Before After
Before After

View file

@ -1,17 +1,17 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 19.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1"
id="Слой_1" xmlns:cc="http://creativecommons.org/ns#" xmlns:svg="http://www.w3.org/2000/svg" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" width="24px" height="24px"
viewBox="-322.458 286.28 24 24" enable-background="new -322.458 286.28 24 24" xml:space="preserve">
<g>
<path d="M-310.458,286.663c-6.406,0-11.618,5.211-11.618,11.618c0,6.406,5.212,11.618,11.618,11.618
c6.405,0,11.617-5.212,11.617-11.618C-298.841,291.874-304.053,286.663-310.458,286.663z M-310.458,308.597
c-5.689,0-10.317-4.628-10.317-10.317c0-5.689,4.628-10.318,10.317-10.318c5.688,0,10.316,4.628,10.316,10.318
C-300.142,303.969-304.77,308.597-310.458,308.597z"/>
<circle cx="-314.114" cy="296.269" r="1.645"/>
<circle cx="-306.802" cy="296.269" r="1.645"/>
<path d="M-305.709,301.348c-2.893,2.007-6.608,2.006-9.499,0c-0.409-0.283-0.46,0.023-0.333,0.264c2.14,4.093,8.027,4.092,10.168,0
C-305.248,301.37-305.298,301.064-305.709,301.348z"/>
</g>
</svg>
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 19.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1"
id="Слой_1" xmlns:cc="http://creativecommons.org/ns#" xmlns:svg="http://www.w3.org/2000/svg" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" width="24px" height="24px"
viewBox="-322.458 286.28 24 24" enable-background="new -322.458 286.28 24 24" xml:space="preserve">
<g>
<path d="M-310.458,286.663c-6.406,0-11.618,5.211-11.618,11.618c0,6.406,5.212,11.618,11.618,11.618
c6.405,0,11.617-5.212,11.617-11.618C-298.841,291.874-304.053,286.663-310.458,286.663z M-310.458,308.597
c-5.689,0-10.317-4.628-10.317-10.317c0-5.689,4.628-10.318,10.317-10.318c5.688,0,10.316,4.628,10.316,10.318
C-300.142,303.969-304.77,308.597-310.458,308.597z"/>
<circle cx="-314.114" cy="296.269" r="1.645"/>
<circle cx="-306.802" cy="296.269" r="1.645"/>
<path d="M-305.709,301.348c-2.893,2.007-6.608,2.006-9.499,0c-0.409-0.283-0.46,0.023-0.333,0.264c2.14,4.093,8.027,4.092,10.168,0
C-305.248,301.37-305.298,301.064-305.709,301.348z"/>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 1.2 KiB

Before After
Before After

View file

@ -1,43 +1,43 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 19.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1"
id="Слой_1" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:cc="http://creativecommons.org/ns#" xmlns:svg="http://www.w3.org/2000/svg" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" width="24px" height="24px"
viewBox="-408 286.28 24 24" style="enable-background:new -408 286.28 24 24;" xml:space="preserve">
<g>
<g>
<path d="M-385.49,300.152c-0.254-0.254-0.666-0.254-0.92,0l-7.751,7.751c-0.254,0.254-0.254,0.666,0,0.92
c0.127,0.127,0.294,0.19,0.46,0.19s0.333-0.063,0.46-0.19l7.751-7.751C-385.237,300.818-385.237,300.406-385.49,300.152z"/>
<path d="M-392.708,303.888c0.585,0,1.171-0.223,1.616-0.668c0.89-0.891,0.89-2.34,0-3.229c-0.892-0.892-2.342-0.89-3.231,0
c-0.89,0.89-0.89,2.339,0.001,3.23C-393.878,303.665-393.293,303.888-392.708,303.888z M-393.405,300.91
c0.186-0.187,0.433-0.289,0.696-0.289s0.51,0.103,0.695,0.289c0.384,0.383,0.384,1.007,0.001,1.391
c-0.384,0.383-1.009,0.385-1.393,0.001C-393.788,301.917-393.788,301.293-393.405,300.91z"/>
<path d="M-385.327,305.755c-0.891-0.891-2.34-0.89-3.23,0.001c-0.89,0.891-0.89,2.34,0,3.229c0.432,0.433,1.005,0.67,1.615,0.67
s1.184-0.237,1.614-0.669c0.433-0.432,0.67-1.005,0.67-1.615C-384.659,306.761-384.896,306.186-385.327,305.755z
M-386.247,308.066c-0.371,0.371-1.02,0.372-1.391-0.001c-0.384-0.383-0.384-1.007,0-1.391c0.186-0.186,0.433-0.288,0.695-0.288
s0.51,0.103,0.695,0.288c0.187,0.186,0.289,0.433,0.289,0.695S-386.061,307.88-386.247,308.066z"/>
</g>
<path d="M-398.294,306.82l0.557-0.453c0.277-0.225,0.318-0.629,0.09-0.903c-0.228-0.276-0.639-0.315-0.914-0.089l-0.757,0.614
l-2.961-2.402c0.423-0.323,0.935-0.73,1.163-0.977c0.27-0.291,0.4-0.673,0.4-1.169c0-1.205-1.145-2.184-2.551-2.184
c-1.405,0-2.55,0.979-2.55,2.184c0,0.554,0.24,1.081,0.709,1.512l0.756,0.613l-1.331,1.08l-0.001,0.001
c-0.688,0.559-1.067,1.313-1.066,2.123c0,0.81,0.379,1.564,1.067,2.121c0.678,0.55,1.547,0.825,2.416,0.825
c0.87,0,1.739-0.275,2.417-0.825l1.531-1.242l1.488,1.207c0.12,0.098,0.267,0.147,0.412,0.147c0.188,0,0.374-0.08,0.502-0.236
c0.229-0.274,0.188-0.679-0.09-0.903L-398.294,306.82z M-404.251,301.989c-0.121-0.112-0.265-0.297-0.265-0.547
c0-0.487,0.571-0.899,1.249-0.899s1.25,0.411,1.25,0.899c0,0.215-0.04,0.282-0.058,0.301l-0.001,0.001
c-0.175,0.189-0.729,0.624-1.235,1.007l-0.017-0.014l0,0L-404.251,301.989z M-401.673,307.901c-0.863,0.7-2.321,0.698-3.185,0
c-0.382-0.31-0.592-0.711-0.592-1.129c0-0.419,0.21-0.82,0.593-1.131l1.53-1.241l2.984,2.421L-401.673,307.901z"/>
<g>
<path d="M-401.76,296.597l-1.12-0.908c-2.905-2.346-4.505-3.639-4.505-5.611c0-1.471,0.935-2.686,2.383-3.095
c1.197-0.34,2.414-0.042,3.241,0.743c0.827-0.784,2.044-1.082,3.243-0.743c1.447,0.41,2.383,1.625,2.383,3.095
c0,1.972-1.6,3.264-4.505,5.61L-401.76,296.597z M-404.027,288.057c-0.223,0-0.442,0.031-0.645,0.089
c-0.726,0.205-1.503,0.821-1.503,1.932c0,1.395,1.355,2.49,4.054,4.671l0.36,0.292l0.362-0.293
c2.699-2.181,4.054-3.275,4.054-4.67c0-1.111-0.776-1.727-1.503-1.932c-0.795-0.227-1.846-0.05-2.399,0.849l-0.515,0.836
l-0.515-0.837C-402.688,288.325-403.375,288.057-404.027,288.057z"/>
</g>
<path d="M-385.448,287.738c-0.901-0.894-2.182-0.893-2.24-0.894h-1.171v5.379c-0.652-0.408-1.51-0.661-2.473-0.661
c-2.078,0-3.705,1.14-3.705,2.596c0,1.455,1.627,2.596,3.705,2.596s3.705-1.14,3.705-2.596v-6.08
c0.196,0.013,0.86,0.087,1.311,0.534c0.312,0.309,0.469,0.76,0.469,1.337c0,0.34,0.276,0.616,0.616,0.616s0.616-0.276,0.616-0.616
C-384.613,289.031-384.895,288.287-385.448,287.738z M-391.331,295.522c-1.457,0-2.473-0.719-2.473-1.364
c0-0.645,1.016-1.364,2.473-1.364s2.473,0.719,2.473,1.364S-389.875,295.522-391.331,295.522z"/>
</g>
</svg>
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 19.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1"
id="Слой_1" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:cc="http://creativecommons.org/ns#" xmlns:svg="http://www.w3.org/2000/svg" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" width="24px" height="24px"
viewBox="-408 286.28 24 24" style="enable-background:new -408 286.28 24 24;" xml:space="preserve">
<g>
<g>
<path d="M-385.49,300.152c-0.254-0.254-0.666-0.254-0.92,0l-7.751,7.751c-0.254,0.254-0.254,0.666,0,0.92
c0.127,0.127,0.294,0.19,0.46,0.19s0.333-0.063,0.46-0.19l7.751-7.751C-385.237,300.818-385.237,300.406-385.49,300.152z"/>
<path d="M-392.708,303.888c0.585,0,1.171-0.223,1.616-0.668c0.89-0.891,0.89-2.34,0-3.229c-0.892-0.892-2.342-0.89-3.231,0
c-0.89,0.89-0.89,2.339,0.001,3.23C-393.878,303.665-393.293,303.888-392.708,303.888z M-393.405,300.91
c0.186-0.187,0.433-0.289,0.696-0.289s0.51,0.103,0.695,0.289c0.384,0.383,0.384,1.007,0.001,1.391
c-0.384,0.383-1.009,0.385-1.393,0.001C-393.788,301.917-393.788,301.293-393.405,300.91z"/>
<path d="M-385.327,305.755c-0.891-0.891-2.34-0.89-3.23,0.001c-0.89,0.891-0.89,2.34,0,3.229c0.432,0.433,1.005,0.67,1.615,0.67
s1.184-0.237,1.614-0.669c0.433-0.432,0.67-1.005,0.67-1.615C-384.659,306.761-384.896,306.186-385.327,305.755z
M-386.247,308.066c-0.371,0.371-1.02,0.372-1.391-0.001c-0.384-0.383-0.384-1.007,0-1.391c0.186-0.186,0.433-0.288,0.695-0.288
s0.51,0.103,0.695,0.288c0.187,0.186,0.289,0.433,0.289,0.695S-386.061,307.88-386.247,308.066z"/>
</g>
<path d="M-398.294,306.82l0.557-0.453c0.277-0.225,0.318-0.629,0.09-0.903c-0.228-0.276-0.639-0.315-0.914-0.089l-0.757,0.614
l-2.961-2.402c0.423-0.323,0.935-0.73,1.163-0.977c0.27-0.291,0.4-0.673,0.4-1.169c0-1.205-1.145-2.184-2.551-2.184
c-1.405,0-2.55,0.979-2.55,2.184c0,0.554,0.24,1.081,0.709,1.512l0.756,0.613l-1.331,1.08l-0.001,0.001
c-0.688,0.559-1.067,1.313-1.066,2.123c0,0.81,0.379,1.564,1.067,2.121c0.678,0.55,1.547,0.825,2.416,0.825
c0.87,0,1.739-0.275,2.417-0.825l1.531-1.242l1.488,1.207c0.12,0.098,0.267,0.147,0.412,0.147c0.188,0,0.374-0.08,0.502-0.236
c0.229-0.274,0.188-0.679-0.09-0.903L-398.294,306.82z M-404.251,301.989c-0.121-0.112-0.265-0.297-0.265-0.547
c0-0.487,0.571-0.899,1.249-0.899s1.25,0.411,1.25,0.899c0,0.215-0.04,0.282-0.058,0.301l-0.001,0.001
c-0.175,0.189-0.729,0.624-1.235,1.007l-0.017-0.014l0,0L-404.251,301.989z M-401.673,307.901c-0.863,0.7-2.321,0.698-3.185,0
c-0.382-0.31-0.592-0.711-0.592-1.129c0-0.419,0.21-0.82,0.593-1.131l1.53-1.241l2.984,2.421L-401.673,307.901z"/>
<g>
<path d="M-401.76,296.597l-1.12-0.908c-2.905-2.346-4.505-3.639-4.505-5.611c0-1.471,0.935-2.686,2.383-3.095
c1.197-0.34,2.414-0.042,3.241,0.743c0.827-0.784,2.044-1.082,3.243-0.743c1.447,0.41,2.383,1.625,2.383,3.095
c0,1.972-1.6,3.264-4.505,5.61L-401.76,296.597z M-404.027,288.057c-0.223,0-0.442,0.031-0.645,0.089
c-0.726,0.205-1.503,0.821-1.503,1.932c0,1.395,1.355,2.49,4.054,4.671l0.36,0.292l0.362-0.293
c2.699-2.181,4.054-3.275,4.054-4.67c0-1.111-0.776-1.727-1.503-1.932c-0.795-0.227-1.846-0.05-2.399,0.849l-0.515,0.836
l-0.515-0.837C-402.688,288.325-403.375,288.057-404.027,288.057z"/>
</g>
<path d="M-385.448,287.738c-0.901-0.894-2.182-0.893-2.24-0.894h-1.171v5.379c-0.652-0.408-1.51-0.661-2.473-0.661
c-2.078,0-3.705,1.14-3.705,2.596c0,1.455,1.627,2.596,3.705,2.596s3.705-1.14,3.705-2.596v-6.08
c0.196,0.013,0.86,0.087,1.311,0.534c0.312,0.309,0.469,0.76,0.469,1.337c0,0.34,0.276,0.616,0.616,0.616s0.616-0.276,0.616-0.616
C-384.613,289.031-384.895,288.287-385.448,287.738z M-391.331,295.522c-1.457,0-2.473-0.719-2.473-1.364
c0-0.645,1.016-1.364,2.473-1.364s2.473,0.719,2.473,1.364S-389.875,295.522-391.331,295.522z"/>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 3.8 KiB

After

Width:  |  Height:  |  Size: 3.8 KiB

Before After
Before After

View file

@ -1,33 +1,33 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 19.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1"
id="Слой_1" xmlns:cc="http://creativecommons.org/ns#" xmlns:svg="http://www.w3.org/2000/svg" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" width="24px" height="24px"
viewBox="-278.583 314.162 24 24" enable-background="new -278.583 314.162 24 24" xml:space="preserve">
<g>
<path d="M-254.972,323.47c0-0.544-0.294-1.435-2.163-1.588c-0.133-0.395-0.245-0.8-0.354-1.194
c-0.312-1.121-0.605-2.181-1.328-2.938c-1.139-1.192-3.533-1.729-7.767-1.737c-4.231,0.009-6.627,0.545-7.767,1.738
c-0.722,0.757-1.016,1.814-1.327,2.936c-0.109,0.396-0.222,0.801-0.355,1.196c-1.868,0.153-2.162,1.044-2.162,1.588
c0,0.439,0.17,1.173,1.2,1.583c-0.127,1.666-0.106,4.17-0.095,5.731l0.006,0.967c0,0.023,0.003,0.043,0.004,0.065
c-0.002,0.018-0.011,0.034-0.011,0.053v3.023c0,0.783,0.656,1.42,1.463,1.42h2.029c0.807,0,1.463-0.638,1.463-1.421v-0.873
c1.429,0.074,3.24,0.101,5.553,0.101c2.31,0,4.12-0.027,5.549-0.101v0.872c0,0.783,0.656,1.421,1.462,1.421h2.03
c0.806,0,1.462-0.637,1.462-1.42v-3.626l0.003-0.483c0.012-1.561,0.032-4.064-0.095-5.73
C-255.141,324.643-254.972,323.909-254.972,323.47z M-257.513,324.658c0.141,1.264,0.15,3.413,0.142,5.092h-0.013
c0,0.693-1.466,1.069-9.199,1.069c-7.734,0-9.2-0.375-9.2-1.068h-0.013c-0.009-1.679,0-3.828,0.142-5.093l0.045-0.403h18.051
L-257.513,324.658z M-274.425,321.033c0.267-0.959,0.519-1.865,1.015-2.386c0.839-0.879,3.137-1.328,6.826-1.335
c3.691,0.007,5.988,0.456,6.826,1.334c0.497,0.521,0.749,1.429,1.016,2.389c0.155,0.558,0.314,1.134,0.537,1.709l0.081,0.21
h-16.921l0.081-0.21C-274.74,322.169-274.58,321.592-274.425,321.033z M-273.436,334.892c0,0.057-0.066,0.12-0.162,0.12h-2.029
c-0.096,0-0.162-0.063-0.162-0.119v-1.399c0.547,0.198,1.313,0.338,2.354,0.434V334.892z M-275.783,331.751l-0.002-0.256
c1.375,0.495,4.079,0.625,9.202,0.625c5.122,0,7.826-0.129,9.201-0.625l-0.002,0.255c0,0.693-1.466,1.069-9.199,1.069
C-274.317,332.819-275.783,332.444-275.783,331.751z M-257.542,335.012h-2.03c-0.097,0-0.161-0.062-0.161-0.12v-0.964
c1.04-0.096,1.805-0.235,2.353-0.433v1.398C-257.381,334.949-257.447,335.012-257.542,335.012z"/>
<path d="M-272.79,330.163c1.377,0,2.376-0.625,2.376-1.485c0-1.207-1.605-2.738-3.41-2.738c-0.895,0-1.544,0.749-1.544,1.781
c0,0.604,0.23,1.202,0.632,1.643C-274.404,329.727-273.796,330.163-272.79,330.163z M-273.824,327.24
c1.085,0,1.999,0.927,2.101,1.363c-0.118,0.094-0.473,0.259-1.066,0.259c-0.427,0-0.759-0.126-0.985-0.375
c-0.187-0.204-0.293-0.483-0.293-0.767C-274.068,327.587-274.024,327.24-273.824,327.24z"/>
<path d="M-260.377,330.163c1.007,0,1.614-0.436,1.946-0.801c0.401-0.44,0.632-1.039,0.632-1.643c0-1.031-0.648-1.78-1.543-1.78
c-1.805,0-3.411,1.532-3.411,2.739C-262.753,329.539-261.754,330.163-260.377,330.163z M-259.342,327.24
c0.199,0,0.243,0.347,0.243,0.479c0,0.284-0.106,0.563-0.292,0.768c-0.228,0.249-0.56,0.375-0.986,0.375
c-0.593,0-0.948-0.164-1.066-0.259C-261.341,328.167-260.427,327.24-259.342,327.24z"/>
</g>
</svg>
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 19.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1"
id="Слой_1" xmlns:cc="http://creativecommons.org/ns#" xmlns:svg="http://www.w3.org/2000/svg" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" width="24px" height="24px"
viewBox="-278.583 314.162 24 24" enable-background="new -278.583 314.162 24 24" xml:space="preserve">
<g>
<path d="M-254.972,323.47c0-0.544-0.294-1.435-2.163-1.588c-0.133-0.395-0.245-0.8-0.354-1.194
c-0.312-1.121-0.605-2.181-1.328-2.938c-1.139-1.192-3.533-1.729-7.767-1.737c-4.231,0.009-6.627,0.545-7.767,1.738
c-0.722,0.757-1.016,1.814-1.327,2.936c-0.109,0.396-0.222,0.801-0.355,1.196c-1.868,0.153-2.162,1.044-2.162,1.588
c0,0.439,0.17,1.173,1.2,1.583c-0.127,1.666-0.106,4.17-0.095,5.731l0.006,0.967c0,0.023,0.003,0.043,0.004,0.065
c-0.002,0.018-0.011,0.034-0.011,0.053v3.023c0,0.783,0.656,1.42,1.463,1.42h2.029c0.807,0,1.463-0.638,1.463-1.421v-0.873
c1.429,0.074,3.24,0.101,5.553,0.101c2.31,0,4.12-0.027,5.549-0.101v0.872c0,0.783,0.656,1.421,1.462,1.421h2.03
c0.806,0,1.462-0.637,1.462-1.42v-3.626l0.003-0.483c0.012-1.561,0.032-4.064-0.095-5.73
C-255.141,324.643-254.972,323.909-254.972,323.47z M-257.513,324.658c0.141,1.264,0.15,3.413,0.142,5.092h-0.013
c0,0.693-1.466,1.069-9.199,1.069c-7.734,0-9.2-0.375-9.2-1.068h-0.013c-0.009-1.679,0-3.828,0.142-5.093l0.045-0.403h18.051
L-257.513,324.658z M-274.425,321.033c0.267-0.959,0.519-1.865,1.015-2.386c0.839-0.879,3.137-1.328,6.826-1.335
c3.691,0.007,5.988,0.456,6.826,1.334c0.497,0.521,0.749,1.429,1.016,2.389c0.155,0.558,0.314,1.134,0.537,1.709l0.081,0.21
h-16.921l0.081-0.21C-274.74,322.169-274.58,321.592-274.425,321.033z M-273.436,334.892c0,0.057-0.066,0.12-0.162,0.12h-2.029
c-0.096,0-0.162-0.063-0.162-0.119v-1.399c0.547,0.198,1.313,0.338,2.354,0.434V334.892z M-275.783,331.751l-0.002-0.256
c1.375,0.495,4.079,0.625,9.202,0.625c5.122,0,7.826-0.129,9.201-0.625l-0.002,0.255c0,0.693-1.466,1.069-9.199,1.069
C-274.317,332.819-275.783,332.444-275.783,331.751z M-257.542,335.012h-2.03c-0.097,0-0.161-0.062-0.161-0.12v-0.964
c1.04-0.096,1.805-0.235,2.353-0.433v1.398C-257.381,334.949-257.447,335.012-257.542,335.012z"/>
<path d="M-272.79,330.163c1.377,0,2.376-0.625,2.376-1.485c0-1.207-1.605-2.738-3.41-2.738c-0.895,0-1.544,0.749-1.544,1.781
c0,0.604,0.23,1.202,0.632,1.643C-274.404,329.727-273.796,330.163-272.79,330.163z M-273.824,327.24
c1.085,0,1.999,0.927,2.101,1.363c-0.118,0.094-0.473,0.259-1.066,0.259c-0.427,0-0.759-0.126-0.985-0.375
c-0.187-0.204-0.293-0.483-0.293-0.767C-274.068,327.587-274.024,327.24-273.824,327.24z"/>
<path d="M-260.377,330.163c1.007,0,1.614-0.436,1.946-0.801c0.401-0.44,0.632-1.039,0.632-1.643c0-1.031-0.648-1.78-1.543-1.78
c-1.805,0-3.411,1.532-3.411,2.739C-262.753,329.539-261.754,330.163-260.377,330.163z M-259.342,327.24
c0.199,0,0.243,0.347,0.243,0.479c0,0.284-0.106,0.563-0.292,0.768c-0.228,0.249-0.56,0.375-0.986,0.375
c-0.593,0-0.948-0.164-1.066-0.259C-261.341,328.167-260.427,327.24-259.342,327.24z"/>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 3.2 KiB

After

Width:  |  Height:  |  Size: 3.2 KiB

Before After
Before After

File diff suppressed because it is too large Load diff

View file

@ -1,28 +1,28 @@
{
"name": "paypal/rest-api-sdk-php",
"description": "PayPal's PHP SDK for REST APIs",
"keywords": ["paypal", "payments", "rest", "sdk"],
"type": "library",
"license": "Apache-2.0",
"homepage": "http://paypal.github.io/PayPal-PHP-SDK/",
"authors": [
{
"name": "PayPal",
"homepage": "https://github.com/paypal/rest-api-sdk-php/contributors"
}
],
"require": {
"php": ">=5.3.0",
"ext-curl": "*",
"ext-json": "*",
"psr/log": "^1.0.0"
},
"require-dev": {
"phpunit/phpunit": "^4.8.35"
},
"autoload": {
"psr-0": {
"PayPal": "lib/"
}
}
}
{
"name": "paypal/rest-api-sdk-php",
"description": "PayPal's PHP SDK for REST APIs",
"keywords": ["paypal", "payments", "rest", "sdk"],
"type": "library",
"license": "Apache-2.0",
"homepage": "http://paypal.github.io/PayPal-PHP-SDK/",
"authors": [
{
"name": "PayPal",
"homepage": "https://github.com/paypal/rest-api-sdk-php/contributors"
}
],
"require": {
"php": ">=5.3.0",
"ext-curl": "*",
"ext-json": "*",
"psr/log": "^1.0.0"
},
"require-dev": {
"phpunit/phpunit": "^4.8.35"
},
"autoload": {
"psr-0": {
"PayPal": "lib/"
}
}
}

View file

@ -0,0 +1,175 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\HttpClient;
use Amp\CancelledException;
use Amp\Http\Client\DelegateHttpClient;
use Amp\Http\Client\InterceptedHttpClient;
use Amp\Http\Client\PooledHttpClient;
use Amp\Http\Client\Request;
use Amp\Http\Tunnel\Http1TunnelConnector;
use Psr\Log\LoggerAwareInterface;
use Psr\Log\LoggerAwareTrait;
use Symfony\Component\HttpClient\Exception\TransportException;
use Symfony\Component\HttpClient\Internal\AmpClientState;
use Symfony\Component\HttpClient\Response\AmpResponse;
use Symfony\Component\HttpClient\Response\ResponseStream;
use Symfony\Contracts\HttpClient\HttpClientInterface;
use Symfony\Contracts\HttpClient\ResponseInterface;
use Symfony\Contracts\HttpClient\ResponseStreamInterface;
use Symfony\Contracts\Service\ResetInterface;
if (!interface_exists(DelegateHttpClient::class)) {
throw new \LogicException('You cannot use "Symfony\Component\HttpClient\AmpHttpClient" as the "amphp/http-client" package is not installed. Try running "composer require amphp/http-client".');
}
/**
* A portable implementation of the HttpClientInterface contracts based on Amp's HTTP client.
*
* @author Nicolas Grekas <p@tchwork.com>
*/
final class AmpHttpClient implements HttpClientInterface, LoggerAwareInterface, ResetInterface
{
use HttpClientTrait;
use LoggerAwareTrait;
private $defaultOptions = self::OPTIONS_DEFAULTS;
/** @var AmpClientState */
private $multi;
/**
* @param array $defaultOptions Default requests' options
* @param callable $clientConfigurator A callable that builds a {@see DelegateHttpClient} from a {@see PooledHttpClient};
* passing null builds an {@see InterceptedHttpClient} with 2 retries on failures
* @param int $maxHostConnections The maximum number of connections to a single host
* @param int $maxPendingPushes The maximum number of pushed responses to accept in the queue
*
* @see HttpClientInterface::OPTIONS_DEFAULTS for available options
*/
public function __construct(array $defaultOptions = [], callable $clientConfigurator = null, int $maxHostConnections = 6, int $maxPendingPushes = 50)
{
$this->defaultOptions['buffer'] = $this->defaultOptions['buffer'] ?? \Closure::fromCallable([__CLASS__, 'shouldBuffer']);
if ($defaultOptions) {
[, $this->defaultOptions] = self::prepareRequest(null, null, $defaultOptions, $this->defaultOptions);
}
$this->multi = new AmpClientState($clientConfigurator, $maxHostConnections, $maxPendingPushes, $this->logger);
}
/**
* @see HttpClientInterface::OPTIONS_DEFAULTS for available options
*
* {@inheritdoc}
*/
public function request(string $method, string $url, array $options = []): ResponseInterface
{
[$url, $options] = self::prepareRequest($method, $url, $options, $this->defaultOptions);
$options['proxy'] = self::getProxy($options['proxy'], $url, $options['no_proxy']);
if (null !== $options['proxy'] && !class_exists(Http1TunnelConnector::class)) {
throw new \LogicException('You cannot use the "proxy" option as the "amphp/http-tunnel" package is not installed. Try running "composer require amphp/http-tunnel".');
}
if ($options['bindto']) {
if (0 === strpos($options['bindto'], 'if!')) {
throw new TransportException(__CLASS__.' cannot bind to network interfaces, use e.g. CurlHttpClient instead.');
}
if (0 === strpos($options['bindto'], 'host!')) {
$options['bindto'] = substr($options['bindto'], 5);
}
}
if ('' !== $options['body'] && 'POST' === $method && !isset($options['normalized_headers']['content-type'])) {
$options['headers'][] = 'Content-Type: application/x-www-form-urlencoded';
}
if (!isset($options['normalized_headers']['user-agent'])) {
$options['headers'][] = 'User-Agent: Symfony HttpClient/Amp';
}
if (0 < $options['max_duration']) {
$options['timeout'] = min($options['max_duration'], $options['timeout']);
}
if ($options['resolve']) {
$this->multi->dnsCache = $options['resolve'] + $this->multi->dnsCache;
}
if ($options['peer_fingerprint'] && !isset($options['peer_fingerprint']['pin-sha256'])) {
throw new TransportException(__CLASS__.' supports only "pin-sha256" fingerprints.');
}
$request = new Request(implode('', $url), $method);
if ($options['http_version']) {
switch ((float) $options['http_version']) {
case 1.0: $request->setProtocolVersions(['1.0']); break;
case 1.1: $request->setProtocolVersions(['1.1', '1.0']); break;
default: $request->setProtocolVersions(['2', '1.1', '1.0']); break;
}
}
foreach ($options['headers'] as $v) {
$h = explode(': ', $v, 2);
$request->addHeader($h[0], $h[1]);
}
$request->setTcpConnectTimeout(1000 * $options['timeout']);
$request->setTlsHandshakeTimeout(1000 * $options['timeout']);
$request->setTransferTimeout(1000 * $options['max_duration']);
if (method_exists($request, 'setInactivityTimeout')) {
$request->setInactivityTimeout(0);
}
if ('' !== $request->getUri()->getUserInfo() && !$request->hasHeader('authorization')) {
$auth = explode(':', $request->getUri()->getUserInfo(), 2);
$auth = array_map('rawurldecode', $auth) + [1 => ''];
$request->setHeader('Authorization', 'Basic '.base64_encode(implode(':', $auth)));
}
return new AmpResponse($this->multi, $request, $options, $this->logger);
}
/**
* {@inheritdoc}
*/
public function stream($responses, float $timeout = null): ResponseStreamInterface
{
if ($responses instanceof AmpResponse) {
$responses = [$responses];
} elseif (!is_iterable($responses)) {
throw new \TypeError(sprintf('"%s()" expects parameter 1 to be an iterable of AmpResponse objects, "%s" given.', __METHOD__, get_debug_type($responses)));
}
return new ResponseStream(AmpResponse::stream($responses, $timeout));
}
public function reset()
{
$this->multi->dnsCache = [];
foreach ($this->multi->pushedResponses as $authority => $pushedResponses) {
foreach ($pushedResponses as [$pushedUrl, $pushDeferred]) {
$pushDeferred->fail(new CancelledException());
if ($this->logger) {
$this->logger->debug(sprintf('Unused pushed response: "%s"', $pushedUrl));
}
}
}
$this->multi->pushedResponses = [];
}
}

View file

@ -0,0 +1,48 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\HttpClient;
use Symfony\Component\HttpClient\Response\AsyncResponse;
use Symfony\Component\HttpClient\Response\ResponseStream;
use Symfony\Contracts\HttpClient\ResponseInterface;
use Symfony\Contracts\HttpClient\ResponseStreamInterface;
/**
* Eases with processing responses while streaming them.
*
* @author Nicolas Grekas <p@tchwork.com>
*/
trait AsyncDecoratorTrait
{
use DecoratorTrait;
/**
* {@inheritdoc}
*
* @return AsyncResponse
*/
abstract public function request(string $method, string $url, array $options = []): ResponseInterface;
/**
* {@inheritdoc}
*/
public function stream($responses, float $timeout = null): ResponseStreamInterface
{
if ($responses instanceof AsyncResponse) {
$responses = [$responses];
} elseif (!is_iterable($responses)) {
throw new \TypeError(sprintf('"%s()" expects parameter 1 to be an iterable of AsyncResponse objects, "%s" given.', __METHOD__, get_debug_type($responses)));
}
return new ResponseStream(AsyncResponse::stream($responses, $timeout, static::class));
}
}

49
vendor/symfony/http-client/CHANGELOG.md vendored Normal file
View file

@ -0,0 +1,49 @@
CHANGELOG
=========
5.3
---
* Implement `HttpClientInterface::withOptions()` from `symfony/contracts` v2.4
* Add `DecoratorTrait` to ease writing simple decorators
5.2.0
-----
* added `AsyncDecoratorTrait` to ease processing responses without breaking async
* added support for pausing responses with a new `pause_handler` callable exposed as an info item
* added `StreamableInterface` to ease turning responses into PHP streams
* added `MockResponse::getRequestMethod()` and `getRequestUrl()` to allow inspecting which request has been sent
* added `EventSourceHttpClient` a Server-Sent events stream implementing the [EventSource specification](https://www.w3.org/TR/eventsource/#eventsource)
* added option "extra.curl" to allow setting additional curl options in `CurlHttpClient`
* added `RetryableHttpClient` to automatically retry failed HTTP requests.
* added `extra.trace_content` option to `TraceableHttpClient` to prevent it from keeping the content in memory
5.1.0
-----
* added `NoPrivateNetworkHttpClient` decorator
* added `AmpHttpClient`, a portable HTTP/2 implementation based on Amp
* added `LoggerAwareInterface` to `ScopingHttpClient` and `TraceableHttpClient`
* made `HttpClient::create()` return an `AmpHttpClient` when `amphp/http-client` is found but curl is not or too old
4.4.0
-----
* added `canceled` to `ResponseInterface::getInfo()`
* added `HttpClient::createForBaseUri()`
* added `HttplugClient` with support for sync and async requests
* added `max_duration` option
* added support for NTLM authentication
* added `StreamWrapper` to cast any `ResponseInterface` instances to PHP streams.
* added `$response->toStream()` to cast responses to regular PHP streams
* made `Psr18Client` implement relevant PSR-17 factories and have streaming responses
* added `TraceableHttpClient`, `HttpClientDataCollector` and `HttpClientPass` to integrate with the web profiler
* allow enabling buffering conditionally with a Closure
* allow option "buffer" to be a stream resource
* allow arbitrary values for the "json" option
4.3.0
-----
* added the component

View file

@ -0,0 +1,144 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\HttpClient;
use Symfony\Component\HttpClient\Response\MockResponse;
use Symfony\Component\HttpClient\Response\ResponseStream;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpKernel\HttpCache\HttpCache;
use Symfony\Component\HttpKernel\HttpCache\StoreInterface;
use Symfony\Component\HttpKernel\HttpClientKernel;
use Symfony\Contracts\HttpClient\HttpClientInterface;
use Symfony\Contracts\HttpClient\ResponseInterface;
use Symfony\Contracts\HttpClient\ResponseStreamInterface;
/**
* Adds caching on top of an HTTP client.
*
* The implementation buffers responses in memory and doesn't stream directly from the network.
* You can disable/enable this layer by setting option "no_cache" under "extra" to true/false.
* By default, caching is enabled unless the "buffer" option is set to false.
*
* @author Nicolas Grekas <p@tchwork.com>
*/
class CachingHttpClient implements HttpClientInterface
{
use HttpClientTrait;
private $client;
private $cache;
private $defaultOptions = self::OPTIONS_DEFAULTS;
public function __construct(HttpClientInterface $client, StoreInterface $store, array $defaultOptions = [])
{
if (!class_exists(HttpClientKernel::class)) {
throw new \LogicException(sprintf('Using "%s" requires that the HttpKernel component version 4.3 or higher is installed, try running "composer require symfony/http-kernel:^4.3".', __CLASS__));
}
$this->client = $client;
$kernel = new HttpClientKernel($client);
$this->cache = new HttpCache($kernel, $store, null, $defaultOptions);
unset($defaultOptions['debug']);
unset($defaultOptions['default_ttl']);
unset($defaultOptions['private_headers']);
unset($defaultOptions['allow_reload']);
unset($defaultOptions['allow_revalidate']);
unset($defaultOptions['stale_while_revalidate']);
unset($defaultOptions['stale_if_error']);
unset($defaultOptions['trace_level']);
unset($defaultOptions['trace_header']);
if ($defaultOptions) {
[, $this->defaultOptions] = self::prepareRequest(null, null, $defaultOptions, $this->defaultOptions);
}
}
/**
* {@inheritdoc}
*/
public function request(string $method, string $url, array $options = []): ResponseInterface
{
[$url, $options] = $this->prepareRequest($method, $url, $options, $this->defaultOptions, true);
$url = implode('', $url);
if (!empty($options['body']) || !empty($options['extra']['no_cache']) || !\in_array($method, ['GET', 'HEAD', 'OPTIONS'])) {
return $this->client->request($method, $url, $options);
}
$request = Request::create($url, $method);
$request->attributes->set('http_client_options', $options);
foreach ($options['normalized_headers'] as $name => $values) {
if ('cookie' !== $name) {
foreach ($values as $value) {
$request->headers->set($name, substr($value, 2 + \strlen($name)), false);
}
continue;
}
foreach ($values as $cookies) {
foreach (explode('; ', substr($cookies, \strlen('Cookie: '))) as $cookie) {
if ('' !== $cookie) {
$cookie = explode('=', $cookie, 2);
$request->cookies->set($cookie[0], $cookie[1] ?? '');
}
}
}
}
$response = $this->cache->handle($request);
$response = new MockResponse($response->getContent(), [
'http_code' => $response->getStatusCode(),
'response_headers' => $response->headers->allPreserveCase(),
]);
return MockResponse::fromRequest($method, $url, $options, $response);
}
/**
* {@inheritdoc}
*/
public function stream($responses, float $timeout = null): ResponseStreamInterface
{
if ($responses instanceof ResponseInterface) {
$responses = [$responses];
} elseif (!is_iterable($responses)) {
throw new \TypeError(sprintf('"%s()" expects parameter 1 to be an iterable of ResponseInterface objects, "%s" given.', __METHOD__, get_debug_type($responses)));
}
$mockResponses = [];
$clientResponses = [];
foreach ($responses as $response) {
if ($response instanceof MockResponse) {
$mockResponses[] = $response;
} else {
$clientResponses[] = $response;
}
}
if (!$mockResponses) {
return $this->client->stream($clientResponses, $timeout);
}
if (!$clientResponses) {
return new ResponseStream(MockResponse::stream($mockResponses, $timeout));
}
return new ResponseStream((function () use ($mockResponses, $clientResponses, $timeout) {
yield from MockResponse::stream($mockResponses, $timeout);
yield $this->client->stream($clientResponses, $timeout);
})());
}
}

View file

@ -0,0 +1,87 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\HttpClient\Chunk;
use Symfony\Contracts\HttpClient\ChunkInterface;
/**
* @author Nicolas Grekas <p@tchwork.com>
*
* @internal
*/
class DataChunk implements ChunkInterface
{
private $offset = 0;
private $content = '';
public function __construct(int $offset = 0, string $content = '')
{
$this->offset = $offset;
$this->content = $content;
}
/**
* {@inheritdoc}
*/
public function isTimeout(): bool
{
return false;
}
/**
* {@inheritdoc}
*/
public function isFirst(): bool
{
return false;
}
/**
* {@inheritdoc}
*/
public function isLast(): bool
{
return false;
}
/**
* {@inheritdoc}
*/
public function getInformationalStatus(): ?array
{
return null;
}
/**
* {@inheritdoc}
*/
public function getContent(): string
{
return $this->content;
}
/**
* {@inheritdoc}
*/
public function getOffset(): int
{
return $this->offset;
}
/**
* {@inheritdoc}
*/
public function getError(): ?string
{
return null;
}
}

View file

@ -0,0 +1,143 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\HttpClient\Chunk;
use Symfony\Component\HttpClient\Exception\TimeoutException;
use Symfony\Component\HttpClient\Exception\TransportException;
use Symfony\Contracts\HttpClient\ChunkInterface;
/**
* @author Nicolas Grekas <p@tchwork.com>
*
* @internal
*/
class ErrorChunk implements ChunkInterface
{
private $didThrow = false;
private $offset;
private $errorMessage;
private $error;
/**
* @param \Throwable|string $error
*/
public function __construct(int $offset, $error)
{
$this->offset = $offset;
if (\is_string($error)) {
$this->errorMessage = $error;
} else {
$this->error = $error;
$this->errorMessage = $error->getMessage();
}
}
/**
* {@inheritdoc}
*/
public function isTimeout(): bool
{
$this->didThrow = true;
if (null !== $this->error) {
throw new TransportException($this->errorMessage, 0, $this->error);
}
return true;
}
/**
* {@inheritdoc}
*/
public function isFirst(): bool
{
$this->didThrow = true;
throw null !== $this->error ? new TransportException($this->errorMessage, 0, $this->error) : new TimeoutException($this->errorMessage);
}
/**
* {@inheritdoc}
*/
public function isLast(): bool
{
$this->didThrow = true;
throw null !== $this->error ? new TransportException($this->errorMessage, 0, $this->error) : new TimeoutException($this->errorMessage);
}
/**
* {@inheritdoc}
*/
public function getInformationalStatus(): ?array
{
$this->didThrow = true;
throw null !== $this->error ? new TransportException($this->errorMessage, 0, $this->error) : new TimeoutException($this->errorMessage);
}
/**
* {@inheritdoc}
*/
public function getContent(): string
{
$this->didThrow = true;
throw null !== $this->error ? new TransportException($this->errorMessage, 0, $this->error) : new TimeoutException($this->errorMessage);
}
/**
* {@inheritdoc}
*/
public function getOffset(): int
{
return $this->offset;
}
/**
* {@inheritdoc}
*/
public function getError(): ?string
{
return $this->errorMessage;
}
/**
* @return bool Whether the wrapped error has been thrown or not
*/
public function didThrow(bool $didThrow = null): bool
{
if (null !== $didThrow && $this->didThrow !== $didThrow) {
return !$this->didThrow = $didThrow;
}
return $this->didThrow;
}
/**
* @return array
*/
public function __sleep()
{
throw new \BadMethodCallException('Cannot serialize '.__CLASS__);
}
public function __wakeup()
{
throw new \BadMethodCallException('Cannot unserialize '.__CLASS__);
}
public function __destruct()
{
if (!$this->didThrow) {
$this->didThrow = true;
throw null !== $this->error ? new TransportException($this->errorMessage, 0, $this->error) : new TimeoutException($this->errorMessage);
}
}
}

View file

@ -0,0 +1,28 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\HttpClient\Chunk;
/**
* @author Nicolas Grekas <p@tchwork.com>
*
* @internal
*/
class FirstChunk extends DataChunk
{
/**
* {@inheritdoc}
*/
public function isFirst(): bool
{
return true;
}
}

View file

@ -0,0 +1,35 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\HttpClient\Chunk;
/**
* @author Nicolas Grekas <p@tchwork.com>
*
* @internal
*/
class InformationalChunk extends DataChunk
{
private $status;
public function __construct(int $statusCode, array $headers)
{
$this->status = [$statusCode, $headers];
}
/**
* {@inheritdoc}
*/
public function getInformationalStatus(): ?array
{
return $this->status;
}
}

View file

@ -0,0 +1,28 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\HttpClient\Chunk;
/**
* @author Nicolas Grekas <p@tchwork.com>
*
* @internal
*/
class LastChunk extends DataChunk
{
/**
* {@inheritdoc}
*/
public function isLast(): bool
{
return true;
}
}

View file

@ -0,0 +1,79 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\HttpClient\Chunk;
use Symfony\Contracts\HttpClient\ChunkInterface;
/**
* @author Antoine Bluchet <soyuka@gmail.com>
* @author Nicolas Grekas <p@tchwork.com>
*/
final class ServerSentEvent extends DataChunk implements ChunkInterface
{
private $data = '';
private $id = '';
private $type = 'message';
private $retry = 0;
public function __construct(string $content)
{
parent::__construct(-1, $content);
// remove BOM
if (0 === strpos($content, "\xEF\xBB\xBF")) {
$content = substr($content, 3);
}
foreach (preg_split("/(?:\r\n|[\r\n])/", $content) as $line) {
if (0 === $i = strpos($line, ':')) {
continue;
}
$i = false === $i ? \strlen($line) : $i;
$field = substr($line, 0, $i);
$i += 1 + (' ' === ($line[1 + $i] ?? ''));
switch ($field) {
case 'id': $this->id = substr($line, $i); break;
case 'event': $this->type = substr($line, $i); break;
case 'data': $this->data .= ('' === $this->data ? '' : "\n").substr($line, $i); break;
case 'retry':
$retry = substr($line, $i);
if ('' !== $retry && \strlen($retry) === strspn($retry, '0123456789')) {
$this->retry = $retry / 1000.0;
}
break;
}
}
}
public function getId(): string
{
return $this->id;
}
public function getType(): string
{
return $this->type;
}
public function getData(): string
{
return $this->data;
}
public function getRetry(): float
{
return $this->retry;
}
}

View file

@ -0,0 +1,599 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\HttpClient;
use Psr\Log\LoggerAwareInterface;
use Psr\Log\LoggerAwareTrait;
use Symfony\Component\HttpClient\Exception\InvalidArgumentException;
use Symfony\Component\HttpClient\Exception\TransportException;
use Symfony\Component\HttpClient\Internal\CurlClientState;
use Symfony\Component\HttpClient\Internal\PushedResponse;
use Symfony\Component\HttpClient\Response\CurlResponse;
use Symfony\Component\HttpClient\Response\ResponseStream;
use Symfony\Contracts\HttpClient\HttpClientInterface;
use Symfony\Contracts\HttpClient\ResponseInterface;
use Symfony\Contracts\HttpClient\ResponseStreamInterface;
use Symfony\Contracts\Service\ResetInterface;
/**
* A performant implementation of the HttpClientInterface contracts based on the curl extension.
*
* This provides fully concurrent HTTP requests, with transparent
* HTTP/2 push when a curl version that supports it is installed.
*
* @author Nicolas Grekas <p@tchwork.com>
*/
final class CurlHttpClient implements HttpClientInterface, LoggerAwareInterface, ResetInterface
{
use HttpClientTrait;
use LoggerAwareTrait;
private $defaultOptions = self::OPTIONS_DEFAULTS + [
'auth_ntlm' => null, // array|string - an array containing the username as first value, and optionally the
// password as the second one; or string like username:password - enabling NTLM auth
'extra' => [
'curl' => [], // A list of extra curl options indexed by their corresponding CURLOPT_*
],
];
/**
* An internal object to share state between the client and its responses.
*
* @var CurlClientState
*/
private $multi;
private static $curlVersion;
/**
* @param array $defaultOptions Default request's options
* @param int $maxHostConnections The maximum number of connections to a single host
* @param int $maxPendingPushes The maximum number of pushed responses to accept in the queue
*
* @see HttpClientInterface::OPTIONS_DEFAULTS for available options
*/
public function __construct(array $defaultOptions = [], int $maxHostConnections = 6, int $maxPendingPushes = 50)
{
if (!\extension_loaded('curl')) {
throw new \LogicException('You cannot use the "Symfony\Component\HttpClient\CurlHttpClient" as the "curl" extension is not installed.');
}
$this->defaultOptions['buffer'] = $this->defaultOptions['buffer'] ?? \Closure::fromCallable([__CLASS__, 'shouldBuffer']);
if ($defaultOptions) {
[, $this->defaultOptions] = self::prepareRequest(null, null, $defaultOptions, $this->defaultOptions);
}
$this->multi = new CurlClientState();
self::$curlVersion = self::$curlVersion ?? curl_version();
// Don't enable HTTP/1.1 pipelining: it forces responses to be sent in order
if (\defined('CURLPIPE_MULTIPLEX')) {
curl_multi_setopt($this->multi->handle, \CURLMOPT_PIPELINING, \CURLPIPE_MULTIPLEX);
}
if (\defined('CURLMOPT_MAX_HOST_CONNECTIONS')) {
$maxHostConnections = curl_multi_setopt($this->multi->handle, \CURLMOPT_MAX_HOST_CONNECTIONS, 0 < $maxHostConnections ? $maxHostConnections : \PHP_INT_MAX) ? 0 : $maxHostConnections;
}
if (\defined('CURLMOPT_MAXCONNECTS') && 0 < $maxHostConnections) {
curl_multi_setopt($this->multi->handle, \CURLMOPT_MAXCONNECTS, $maxHostConnections);
}
// Skip configuring HTTP/2 push when it's unsupported or buggy, see https://bugs.php.net/77535
if (0 >= $maxPendingPushes || \PHP_VERSION_ID < 70217 || (\PHP_VERSION_ID >= 70300 && \PHP_VERSION_ID < 70304)) {
return;
}
// HTTP/2 push crashes before curl 7.61
if (!\defined('CURLMOPT_PUSHFUNCTION') || 0x073d00 > self::$curlVersion['version_number'] || !(\CURL_VERSION_HTTP2 & self::$curlVersion['features'])) {
return;
}
curl_multi_setopt($this->multi->handle, \CURLMOPT_PUSHFUNCTION, function ($parent, $pushed, array $requestHeaders) use ($maxPendingPushes) {
return $this->handlePush($parent, $pushed, $requestHeaders, $maxPendingPushes);
});
}
/**
* @see HttpClientInterface::OPTIONS_DEFAULTS for available options
*
* {@inheritdoc}
*/
public function request(string $method, string $url, array $options = []): ResponseInterface
{
[$url, $options] = self::prepareRequest($method, $url, $options, $this->defaultOptions);
$scheme = $url['scheme'];
$authority = $url['authority'];
$host = parse_url($authority, \PHP_URL_HOST);
$url = implode('', $url);
if (!isset($options['normalized_headers']['user-agent'])) {
$options['headers'][] = 'User-Agent: Symfony HttpClient/Curl';
}
$curlopts = [
\CURLOPT_URL => $url,
\CURLOPT_TCP_NODELAY => true,
\CURLOPT_PROTOCOLS => \CURLPROTO_HTTP | \CURLPROTO_HTTPS,
\CURLOPT_REDIR_PROTOCOLS => \CURLPROTO_HTTP | \CURLPROTO_HTTPS,
\CURLOPT_FOLLOWLOCATION => true,
\CURLOPT_MAXREDIRS => 0 < $options['max_redirects'] ? $options['max_redirects'] : 0,
\CURLOPT_COOKIEFILE => '', // Keep track of cookies during redirects
\CURLOPT_TIMEOUT => 0,
\CURLOPT_PROXY => $options['proxy'],
\CURLOPT_NOPROXY => $options['no_proxy'] ?? $_SERVER['no_proxy'] ?? $_SERVER['NO_PROXY'] ?? '',
\CURLOPT_SSL_VERIFYPEER => $options['verify_peer'],
\CURLOPT_SSL_VERIFYHOST => $options['verify_host'] ? 2 : 0,
\CURLOPT_CAINFO => $options['cafile'],
\CURLOPT_CAPATH => $options['capath'],
\CURLOPT_SSL_CIPHER_LIST => $options['ciphers'],
\CURLOPT_SSLCERT => $options['local_cert'],
\CURLOPT_SSLKEY => $options['local_pk'],
\CURLOPT_KEYPASSWD => $options['passphrase'],
\CURLOPT_CERTINFO => $options['capture_peer_cert_chain'],
];
if (1.0 === (float) $options['http_version']) {
$curlopts[\CURLOPT_HTTP_VERSION] = \CURL_HTTP_VERSION_1_0;
} elseif (1.1 === (float) $options['http_version']) {
$curlopts[\CURLOPT_HTTP_VERSION] = \CURL_HTTP_VERSION_1_1;
} elseif (\defined('CURL_VERSION_HTTP2') && (\CURL_VERSION_HTTP2 & self::$curlVersion['features']) && ('https:' === $scheme || 2.0 === (float) $options['http_version'])) {
$curlopts[\CURLOPT_HTTP_VERSION] = \CURL_HTTP_VERSION_2_0;
}
if (isset($options['auth_ntlm'])) {
$curlopts[\CURLOPT_HTTPAUTH] = \CURLAUTH_NTLM;
$curlopts[\CURLOPT_HTTP_VERSION] = \CURL_HTTP_VERSION_1_1;
if (\is_array($options['auth_ntlm'])) {
$count = \count($options['auth_ntlm']);
if ($count <= 0 || $count > 2) {
throw new InvalidArgumentException(sprintf('Option "auth_ntlm" must contain 1 or 2 elements, %d given.', $count));
}
$options['auth_ntlm'] = implode(':', $options['auth_ntlm']);
}
if (!\is_string($options['auth_ntlm'])) {
throw new InvalidArgumentException(sprintf('Option "auth_ntlm" must be a string or an array, "%s" given.', get_debug_type($options['auth_ntlm'])));
}
$curlopts[\CURLOPT_USERPWD] = $options['auth_ntlm'];
}
if (!\ZEND_THREAD_SAFE) {
$curlopts[\CURLOPT_DNS_USE_GLOBAL_CACHE] = false;
}
if (\defined('CURLOPT_HEADEROPT') && \defined('CURLHEADER_SEPARATE')) {
$curlopts[\CURLOPT_HEADEROPT] = \CURLHEADER_SEPARATE;
}
// curl's resolve feature varies by host:port but ours varies by host only, let's handle this with our own DNS map
if (isset($this->multi->dnsCache->hostnames[$host])) {
$options['resolve'] += [$host => $this->multi->dnsCache->hostnames[$host]];
}
if ($options['resolve'] || $this->multi->dnsCache->evictions) {
// First reset any old DNS cache entries then add the new ones
$resolve = $this->multi->dnsCache->evictions;
$this->multi->dnsCache->evictions = [];
$port = parse_url($authority, \PHP_URL_PORT) ?: ('http:' === $scheme ? 80 : 443);
if ($resolve && 0x072a00 > self::$curlVersion['version_number']) {
// DNS cache removals require curl 7.42 or higher
// On lower versions, we have to create a new multi handle
curl_multi_close($this->multi->handle);
$this->multi->handle = (new self())->multi->handle;
}
foreach ($options['resolve'] as $host => $ip) {
$resolve[] = null === $ip ? "-$host:$port" : "$host:$port:$ip";
$this->multi->dnsCache->hostnames[$host] = $ip;
$this->multi->dnsCache->removals["-$host:$port"] = "-$host:$port";
}
$curlopts[\CURLOPT_RESOLVE] = $resolve;
}
if ('POST' === $method) {
// Use CURLOPT_POST to have browser-like POST-to-GET redirects for 301, 302 and 303
$curlopts[\CURLOPT_POST] = true;
} elseif ('HEAD' === $method) {
$curlopts[\CURLOPT_NOBODY] = true;
} else {
$curlopts[\CURLOPT_CUSTOMREQUEST] = $method;
}
if ('\\' !== \DIRECTORY_SEPARATOR && $options['timeout'] < 1) {
$curlopts[\CURLOPT_NOSIGNAL] = true;
}
if (\extension_loaded('zlib') && !isset($options['normalized_headers']['accept-encoding'])) {
$options['headers'][] = 'Accept-Encoding: gzip'; // Expose only one encoding, some servers mess up when more are provided
}
foreach ($options['headers'] as $header) {
if (':' === $header[-2] && \strlen($header) - 2 === strpos($header, ': ')) {
// curl requires a special syntax to send empty headers
$curlopts[\CURLOPT_HTTPHEADER][] = substr_replace($header, ';', -2);
} else {
$curlopts[\CURLOPT_HTTPHEADER][] = $header;
}
}
// Prevent curl from sending its default Accept and Expect headers
foreach (['accept', 'expect'] as $header) {
if (!isset($options['normalized_headers'][$header][0])) {
$curlopts[\CURLOPT_HTTPHEADER][] = $header.':';
}
}
if (!\is_string($body = $options['body'])) {
if (\is_resource($body)) {
$curlopts[\CURLOPT_INFILE] = $body;
} else {
$eof = false;
$buffer = '';
$curlopts[\CURLOPT_READFUNCTION] = static function ($ch, $fd, $length) use ($body, &$buffer, &$eof) {
return self::readRequestBody($length, $body, $buffer, $eof);
};
}
if (isset($options['normalized_headers']['content-length'][0])) {
$curlopts[\CURLOPT_INFILESIZE] = substr($options['normalized_headers']['content-length'][0], \strlen('Content-Length: '));
} elseif (!isset($options['normalized_headers']['transfer-encoding'])) {
$curlopts[\CURLOPT_HTTPHEADER][] = 'Transfer-Encoding: chunked'; // Enable chunked request bodies
}
if ('POST' !== $method) {
$curlopts[\CURLOPT_UPLOAD] = true;
}
} elseif ('' !== $body || 'POST' === $method) {
$curlopts[\CURLOPT_POSTFIELDS] = $body;
}
if ($options['peer_fingerprint']) {
if (!isset($options['peer_fingerprint']['pin-sha256'])) {
throw new TransportException(__CLASS__.' supports only "pin-sha256" fingerprints.');
}
$curlopts[\CURLOPT_PINNEDPUBLICKEY] = 'sha256//'.implode(';sha256//', $options['peer_fingerprint']['pin-sha256']);
}
if ($options['bindto']) {
if (file_exists($options['bindto'])) {
$curlopts[\CURLOPT_UNIX_SOCKET_PATH] = $options['bindto'];
} elseif (!str_starts_with($options['bindto'], 'if!') && preg_match('/^(.*):(\d+)$/', $options['bindto'], $matches)) {
$curlopts[\CURLOPT_INTERFACE] = $matches[1];
$curlopts[\CURLOPT_LOCALPORT] = $matches[2];
} else {
$curlopts[\CURLOPT_INTERFACE] = $options['bindto'];
}
}
if (0 < $options['max_duration']) {
$curlopts[\CURLOPT_TIMEOUT_MS] = 1000 * $options['max_duration'];
}
if (!empty($options['extra']['curl']) && \is_array($options['extra']['curl'])) {
$this->validateExtraCurlOptions($options['extra']['curl']);
$curlopts += $options['extra']['curl'];
}
if ($pushedResponse = $this->multi->pushedResponses[$url] ?? null) {
unset($this->multi->pushedResponses[$url]);
if (self::acceptPushForRequest($method, $options, $pushedResponse)) {
$this->logger && $this->logger->debug(sprintf('Accepting pushed response: "%s %s"', $method, $url));
// Reinitialize the pushed response with request's options
$ch = $pushedResponse->handle;
$pushedResponse = $pushedResponse->response;
$pushedResponse->__construct($this->multi, $url, $options, $this->logger);
} else {
$this->logger && $this->logger->debug(sprintf('Rejecting pushed response: "%s"', $url));
$pushedResponse = null;
}
}
if (!$pushedResponse) {
$ch = curl_init();
$this->logger && $this->logger->info(sprintf('Request: "%s %s"', $method, $url));
}
foreach ($curlopts as $opt => $value) {
if (null !== $value && !curl_setopt($ch, $opt, $value) && \CURLOPT_CERTINFO !== $opt && (!\defined('CURLOPT_HEADEROPT') || \CURLOPT_HEADEROPT !== $opt)) {
$constantName = $this->findConstantName($opt);
throw new TransportException(sprintf('Curl option "%s" is not supported.', $constantName ?? $opt));
}
}
return $pushedResponse ?? new CurlResponse($this->multi, $ch, $options, $this->logger, $method, self::createRedirectResolver($options, $host), self::$curlVersion['version_number']);
}
/**
* {@inheritdoc}
*/
public function stream($responses, float $timeout = null): ResponseStreamInterface
{
if ($responses instanceof CurlResponse) {
$responses = [$responses];
} elseif (!is_iterable($responses)) {
throw new \TypeError(sprintf('"%s()" expects parameter 1 to be an iterable of CurlResponse objects, "%s" given.', __METHOD__, get_debug_type($responses)));
}
if (\is_resource($this->multi->handle) || $this->multi->handle instanceof \CurlMultiHandle) {
$active = 0;
while (\CURLM_CALL_MULTI_PERFORM === curl_multi_exec($this->multi->handle, $active));
}
return new ResponseStream(CurlResponse::stream($responses, $timeout));
}
public function reset()
{
$this->multi->logger = $this->logger;
$this->multi->reset();
}
/**
* @return array
*/
public function __sleep()
{
throw new \BadMethodCallException('Cannot serialize '.__CLASS__);
}
public function __wakeup()
{
throw new \BadMethodCallException('Cannot unserialize '.__CLASS__);
}
public function __destruct()
{
$this->multi->logger = $this->logger;
}
private function handlePush($parent, $pushed, array $requestHeaders, int $maxPendingPushes): int
{
$headers = [];
$origin = curl_getinfo($parent, \CURLINFO_EFFECTIVE_URL);
foreach ($requestHeaders as $h) {
if (false !== $i = strpos($h, ':', 1)) {
$headers[substr($h, 0, $i)][] = substr($h, 1 + $i);
}
}
if (!isset($headers[':method']) || !isset($headers[':scheme']) || !isset($headers[':authority']) || !isset($headers[':path'])) {
$this->logger && $this->logger->debug(sprintf('Rejecting pushed response from "%s": pushed headers are invalid', $origin));
return \CURL_PUSH_DENY;
}
$url = $headers[':scheme'][0].'://'.$headers[':authority'][0];
// curl before 7.65 doesn't validate the pushed ":authority" header,
// but this is a MUST in the HTTP/2 RFC; let's restrict pushes to the original host,
// ignoring domains mentioned as alt-name in the certificate for now (same as curl).
if (!str_starts_with($origin, $url.'/')) {
$this->logger && $this->logger->debug(sprintf('Rejecting pushed response from "%s": server is not authoritative for "%s"', $origin, $url));
return \CURL_PUSH_DENY;
}
if ($maxPendingPushes <= \count($this->multi->pushedResponses)) {
$fifoUrl = key($this->multi->pushedResponses);
unset($this->multi->pushedResponses[$fifoUrl]);
$this->logger && $this->logger->debug(sprintf('Evicting oldest pushed response: "%s"', $fifoUrl));
}
$url .= $headers[':path'][0];
$this->logger && $this->logger->debug(sprintf('Queueing pushed response: "%s"', $url));
$this->multi->pushedResponses[$url] = new PushedResponse(new CurlResponse($this->multi, $pushed), $headers, $this->multi->openHandles[(int) $parent][1] ?? [], $pushed);
return \CURL_PUSH_OK;
}
/**
* Accepts pushed responses only if their headers related to authentication match the request.
*/
private static function acceptPushForRequest(string $method, array $options, PushedResponse $pushedResponse): bool
{
if ('' !== $options['body'] || $method !== $pushedResponse->requestHeaders[':method'][0]) {
return false;
}
foreach (['proxy', 'no_proxy', 'bindto', 'local_cert', 'local_pk'] as $k) {
if ($options[$k] !== $pushedResponse->parentOptions[$k]) {
return false;
}
}
foreach (['authorization', 'cookie', 'range', 'proxy-authorization'] as $k) {
$normalizedHeaders = $options['normalized_headers'][$k] ?? [];
foreach ($normalizedHeaders as $i => $v) {
$normalizedHeaders[$i] = substr($v, \strlen($k) + 2);
}
if (($pushedResponse->requestHeaders[$k] ?? []) !== $normalizedHeaders) {
return false;
}
}
return true;
}
/**
* Wraps the request's body callback to allow it to return strings longer than curl requested.
*/
private static function readRequestBody(int $length, \Closure $body, string &$buffer, bool &$eof): string
{
if (!$eof && \strlen($buffer) < $length) {
if (!\is_string($data = $body($length))) {
throw new TransportException(sprintf('The return value of the "body" option callback must be a string, "%s" returned.', get_debug_type($data)));
}
$buffer .= $data;
$eof = '' === $data;
}
$data = substr($buffer, 0, $length);
$buffer = substr($buffer, $length);
return $data;
}
/**
* Resolves relative URLs on redirects and deals with authentication headers.
*
* Work around CVE-2018-1000007: Authorization and Cookie headers should not follow redirects - fixed in Curl 7.64
*/
private static function createRedirectResolver(array $options, string $host): \Closure
{
$redirectHeaders = [];
if (0 < $options['max_redirects']) {
$redirectHeaders['host'] = $host;
$redirectHeaders['with_auth'] = $redirectHeaders['no_auth'] = array_filter($options['headers'], static function ($h) {
return 0 !== stripos($h, 'Host:');
});
if (isset($options['normalized_headers']['authorization'][0]) || isset($options['normalized_headers']['cookie'][0])) {
$redirectHeaders['no_auth'] = array_filter($options['headers'], static function ($h) {
return 0 !== stripos($h, 'Authorization:') && 0 !== stripos($h, 'Cookie:');
});
}
}
return static function ($ch, string $location) use ($redirectHeaders) {
try {
$location = self::parseUrl($location);
} catch (InvalidArgumentException $e) {
return null;
}
if ($redirectHeaders && $host = parse_url('http:'.$location['authority'], \PHP_URL_HOST)) {
$requestHeaders = $redirectHeaders['host'] === $host ? $redirectHeaders['with_auth'] : $redirectHeaders['no_auth'];
curl_setopt($ch, \CURLOPT_HTTPHEADER, $requestHeaders);
}
$url = self::parseUrl(curl_getinfo($ch, \CURLINFO_EFFECTIVE_URL));
return implode('', self::resolveUrl($location, $url));
};
}
private function findConstantName(int $opt): ?string
{
$constants = array_filter(get_defined_constants(), static function ($v, $k) use ($opt) {
return $v === $opt && 'C' === $k[0] && (str_starts_with($k, 'CURLOPT_') || str_starts_with($k, 'CURLINFO_'));
}, \ARRAY_FILTER_USE_BOTH);
return key($constants);
}
/**
* Prevents overriding options that are set internally throughout the request.
*/
private function validateExtraCurlOptions(array $options): void
{
$curloptsToConfig = [
//options used in CurlHttpClient
\CURLOPT_HTTPAUTH => 'auth_ntlm',
\CURLOPT_USERPWD => 'auth_ntlm',
\CURLOPT_RESOLVE => 'resolve',
\CURLOPT_NOSIGNAL => 'timeout',
\CURLOPT_HTTPHEADER => 'headers',
\CURLOPT_INFILE => 'body',
\CURLOPT_READFUNCTION => 'body',
\CURLOPT_INFILESIZE => 'body',
\CURLOPT_POSTFIELDS => 'body',
\CURLOPT_UPLOAD => 'body',
\CURLOPT_PINNEDPUBLICKEY => 'peer_fingerprint',
\CURLOPT_UNIX_SOCKET_PATH => 'bindto',
\CURLOPT_INTERFACE => 'bindto',
\CURLOPT_TIMEOUT_MS => 'max_duration',
\CURLOPT_TIMEOUT => 'max_duration',
\CURLOPT_MAXREDIRS => 'max_redirects',
\CURLOPT_PROXY => 'proxy',
\CURLOPT_NOPROXY => 'no_proxy',
\CURLOPT_SSL_VERIFYPEER => 'verify_peer',
\CURLOPT_SSL_VERIFYHOST => 'verify_host',
\CURLOPT_CAINFO => 'cafile',
\CURLOPT_CAPATH => 'capath',
\CURLOPT_SSL_CIPHER_LIST => 'ciphers',
\CURLOPT_SSLCERT => 'local_cert',
\CURLOPT_SSLKEY => 'local_pk',
\CURLOPT_KEYPASSWD => 'passphrase',
\CURLOPT_CERTINFO => 'capture_peer_cert_chain',
\CURLOPT_USERAGENT => 'normalized_headers',
\CURLOPT_REFERER => 'headers',
//options used in CurlResponse
\CURLOPT_NOPROGRESS => 'on_progress',
\CURLOPT_PROGRESSFUNCTION => 'on_progress',
];
$curloptsToCheck = [
\CURLOPT_PRIVATE,
\CURLOPT_HEADERFUNCTION,
\CURLOPT_WRITEFUNCTION,
\CURLOPT_VERBOSE,
\CURLOPT_STDERR,
\CURLOPT_RETURNTRANSFER,
\CURLOPT_URL,
\CURLOPT_FOLLOWLOCATION,
\CURLOPT_HEADER,
\CURLOPT_CONNECTTIMEOUT,
\CURLOPT_CONNECTTIMEOUT_MS,
\CURLOPT_HTTP_VERSION,
\CURLOPT_PORT,
\CURLOPT_DNS_USE_GLOBAL_CACHE,
\CURLOPT_PROTOCOLS,
\CURLOPT_REDIR_PROTOCOLS,
\CURLOPT_COOKIEFILE,
\CURLINFO_REDIRECT_COUNT,
];
if (\defined('CURLOPT_HTTP09_ALLOWED')) {
$curloptsToCheck[] = \CURLOPT_HTTP09_ALLOWED;
}
if (\defined('CURLOPT_HEADEROPT')) {
$curloptsToCheck[] = \CURLOPT_HEADEROPT;
}
$methodOpts = [
\CURLOPT_POST,
\CURLOPT_PUT,
\CURLOPT_CUSTOMREQUEST,
\CURLOPT_HTTPGET,
\CURLOPT_NOBODY,
];
foreach ($options as $opt => $optValue) {
if (isset($curloptsToConfig[$opt])) {
$constName = $this->findConstantName($opt) ?? $opt;
throw new InvalidArgumentException(sprintf('Cannot set "%s" with "extra.curl", use option "%s" instead.', $constName, $curloptsToConfig[$opt]));
}
if (\in_array($opt, $methodOpts)) {
throw new InvalidArgumentException('The HTTP method cannot be overridden using "extra.curl".');
}
if (\in_array($opt, $curloptsToCheck)) {
$constName = $this->findConstantName($opt) ?? $opt;
throw new InvalidArgumentException(sprintf('Cannot set "%s" with "extra.curl".', $constName));
}
}
}
}

View file

@ -0,0 +1,170 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\HttpClient\DataCollector;
use Symfony\Component\HttpClient\TraceableHttpClient;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\DataCollector\DataCollector;
use Symfony\Component\HttpKernel\DataCollector\LateDataCollectorInterface;
use Symfony\Component\VarDumper\Caster\ImgStub;
/**
* @author Jérémy Romey <jeremy@free-agent.fr>
*/
final class HttpClientDataCollector extends DataCollector implements LateDataCollectorInterface
{
/**
* @var TraceableHttpClient[]
*/
private $clients = [];
public function registerClient(string $name, TraceableHttpClient $client)
{
$this->clients[$name] = $client;
}
/**
* {@inheritdoc}
*/
public function collect(Request $request, Response $response, \Throwable $exception = null)
{
$this->reset();
foreach ($this->clients as $name => $client) {
[$errorCount, $traces] = $this->collectOnClient($client);
$this->data['clients'][$name] = [
'traces' => $traces,
'error_count' => $errorCount,
];
$this->data['request_count'] += \count($traces);
$this->data['error_count'] += $errorCount;
}
}
public function lateCollect()
{
foreach ($this->clients as $client) {
$client->reset();
}
}
public function getClients(): array
{
return $this->data['clients'] ?? [];
}
public function getRequestCount(): int
{
return $this->data['request_count'] ?? 0;
}
public function getErrorCount(): int
{
return $this->data['error_count'] ?? 0;
}
/**
* {@inheritdoc}
*/
public function getName(): string
{
return 'http_client';
}
public function reset()
{
$this->data = [
'clients' => [],
'request_count' => 0,
'error_count' => 0,
];
}
private function collectOnClient(TraceableHttpClient $client): array
{
$traces = $client->getTracedRequests();
$errorCount = 0;
$baseInfo = [
'response_headers' => 1,
'retry_count' => 1,
'redirect_count' => 1,
'redirect_url' => 1,
'user_data' => 1,
'error' => 1,
'url' => 1,
];
foreach ($traces as $i => $trace) {
if (400 <= ($trace['info']['http_code'] ?? 0)) {
++$errorCount;
}
$info = $trace['info'];
$traces[$i]['http_code'] = $info['http_code'] ?? 0;
unset($info['filetime'], $info['http_code'], $info['ssl_verify_result'], $info['content_type']);
if (($info['http_method'] ?? null) === $trace['method']) {
unset($info['http_method']);
}
if (($info['url'] ?? null) === $trace['url']) {
unset($info['url']);
}
foreach ($info as $k => $v) {
if (!$v || (is_numeric($v) && 0 > $v)) {
unset($info[$k]);
}
}
if (\is_string($content = $trace['content'])) {
$contentType = 'application/octet-stream';
foreach ($info['response_headers'] ?? [] as $h) {
if (0 === stripos($h, 'content-type: ')) {
$contentType = substr($h, \strlen('content-type: '));
break;
}
}
if (0 === strpos($contentType, 'image/') && class_exists(ImgStub::class)) {
$content = new ImgStub($content, $contentType, '');
} else {
$content = [$content];
}
$content = ['response_content' => $content];
} elseif (\is_array($content)) {
$content = ['response_json' => $content];
} else {
$content = [];
}
if (isset($info['retry_count'])) {
$content['retries'] = $info['previous_info'];
unset($info['previous_info']);
}
$debugInfo = array_diff_key($info, $baseInfo);
$info = ['info' => $debugInfo] + array_diff_key($info, $debugInfo) + $content;
unset($traces[$i]['info']); // break PHP reference used by TraceableHttpClient
$traces[$i]['info'] = $this->cloneVar($info);
$traces[$i]['options'] = $this->cloneVar($trace['options']);
}
return [$errorCount, $traces];
}
}

View file

@ -0,0 +1,58 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\HttpClient;
use Symfony\Contracts\HttpClient\HttpClientInterface;
use Symfony\Contracts\HttpClient\ResponseInterface;
use Symfony\Contracts\HttpClient\ResponseStreamInterface;
/**
* Eases with writing decorators.
*
* @author Nicolas Grekas <p@tchwork.com>
*/
trait DecoratorTrait
{
private $client;
public function __construct(HttpClientInterface $client = null)
{
$this->client = $client ?? HttpClient::create();
}
/**
* {@inheritdoc}
*/
public function request(string $method, string $url, array $options = []): ResponseInterface
{
return $this->client->request($method, $url, $options);
}
/**
* {@inheritdoc}
*/
public function stream($responses, float $timeout = null): ResponseStreamInterface
{
return $this->client->stream($responses, $timeout);
}
/**
* {@inheritdoc}
*/
public function withOptions(array $options): self
{
$clone = clone $this;
$clone->client = $this->client->withOptions($options);
return $clone;
}
}

View file

@ -0,0 +1,51 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\HttpClient\DependencyInjection;
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\DependencyInjection\Reference;
use Symfony\Component\HttpClient\TraceableHttpClient;
final class HttpClientPass implements CompilerPassInterface
{
private $clientTag;
public function __construct(string $clientTag = 'http_client.client')
{
if (0 < \func_num_args()) {
trigger_deprecation('symfony/http-client', '5.3', 'Configuring "%s" is deprecated.', __CLASS__);
}
$this->clientTag = $clientTag;
}
/**
* {@inheritdoc}
*/
public function process(ContainerBuilder $container)
{
if (!$container->hasDefinition('data_collector.http_client')) {
return;
}
foreach ($container->findTaggedServiceIds($this->clientTag) as $id => $tags) {
$container->register('.debug.'.$id, TraceableHttpClient::class)
->setArguments([new Reference('.debug.'.$id.'.inner'), new Reference('debug.stopwatch', ContainerInterface::IGNORE_ON_INVALID_REFERENCE)])
->addTag('kernel.reset', ['method' => 'reset'])
->setDecoratedService($id);
$container->getDefinition('data_collector.http_client')
->addMethodCall('registerClient', [$id, new Reference('.debug.'.$id)]);
}
}
}

View file

@ -0,0 +1,158 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\HttpClient;
use Symfony\Component\HttpClient\Chunk\ServerSentEvent;
use Symfony\Component\HttpClient\Exception\EventSourceException;
use Symfony\Component\HttpClient\Response\AsyncContext;
use Symfony\Component\HttpClient\Response\AsyncResponse;
use Symfony\Contracts\HttpClient\ChunkInterface;
use Symfony\Contracts\HttpClient\Exception\TransportExceptionInterface;
use Symfony\Contracts\HttpClient\HttpClientInterface;
use Symfony\Contracts\HttpClient\ResponseInterface;
/**
* @author Antoine Bluchet <soyuka@gmail.com>
* @author Nicolas Grekas <p@tchwork.com>
*/
final class EventSourceHttpClient implements HttpClientInterface
{
use AsyncDecoratorTrait, HttpClientTrait {
AsyncDecoratorTrait::withOptions insteadof HttpClientTrait;
}
private $reconnectionTime;
public function __construct(HttpClientInterface $client = null, float $reconnectionTime = 10.0)
{
$this->client = $client ?? HttpClient::create();
$this->reconnectionTime = $reconnectionTime;
}
public function connect(string $url, array $options = []): ResponseInterface
{
return $this->request('GET', $url, self::mergeDefaultOptions($options, [
'buffer' => false,
'headers' => [
'Accept' => 'text/event-stream',
'Cache-Control' => 'no-cache',
],
], true));
}
public function request(string $method, string $url, array $options = []): ResponseInterface
{
$state = new class() {
public $buffer = null;
public $lastEventId = null;
public $reconnectionTime;
public $lastError = null;
};
$state->reconnectionTime = $this->reconnectionTime;
if ($accept = self::normalizeHeaders($options['headers'] ?? [])['accept'] ?? []) {
$state->buffer = \in_array($accept, [['Accept: text/event-stream'], ['accept: text/event-stream']], true) ? '' : null;
if (null !== $state->buffer) {
$options['extra']['trace_content'] = false;
}
}
return new AsyncResponse($this->client, $method, $url, $options, static function (ChunkInterface $chunk, AsyncContext $context) use ($state, $method, $url, $options) {
if (null !== $state->buffer) {
$context->setInfo('reconnection_time', $state->reconnectionTime);
$isTimeout = false;
}
$lastError = $state->lastError;
$state->lastError = null;
try {
$isTimeout = $chunk->isTimeout();
if (null !== $chunk->getInformationalStatus() || $context->getInfo('canceled')) {
yield $chunk;
return;
}
} catch (TransportExceptionInterface $e) {
$state->lastError = $lastError ?? microtime(true);
if (null === $state->buffer || ($isTimeout && microtime(true) - $state->lastError < $state->reconnectionTime)) {
yield $chunk;
} else {
$options['headers']['Last-Event-ID'] = $state->lastEventId;
$state->buffer = '';
$state->lastError = microtime(true);
$context->getResponse()->cancel();
$context->replaceRequest($method, $url, $options);
if ($isTimeout) {
yield $chunk;
} else {
$context->pause($state->reconnectionTime);
}
}
return;
}
if ($chunk->isFirst()) {
if (preg_match('/^text\/event-stream(;|$)/i', $context->getHeaders()['content-type'][0] ?? '')) {
$state->buffer = '';
} elseif (null !== $lastError || (null !== $state->buffer && 200 === $context->getStatusCode())) {
throw new EventSourceException(sprintf('Response content-type is "%s" while "text/event-stream" was expected for "%s".', $context->getHeaders()['content-type'][0] ?? '', $context->getInfo('url')));
} else {
$context->passthru();
}
if (null === $lastError) {
yield $chunk;
}
return;
}
$rx = '/((?:\r\n|[\r\n]){2,})/';
$content = $state->buffer.$chunk->getContent();
if ($chunk->isLast()) {
$rx = substr_replace($rx, '|$', -2, 0);
}
$events = preg_split($rx, $content, -1, \PREG_SPLIT_DELIM_CAPTURE);
$state->buffer = array_pop($events);
for ($i = 0; isset($events[$i]); $i += 2) {
$event = new ServerSentEvent($events[$i].$events[1 + $i]);
if ('' !== $event->getId()) {
$context->setInfo('last_event_id', $state->lastEventId = $event->getId());
}
if ($event->getRetry()) {
$context->setInfo('reconnection_time', $state->reconnectionTime = $event->getRetry());
}
yield $event;
}
if (preg_match('/^(?::[^\r\n]*+(?:\r\n|[\r\n]))+$/m', $state->buffer)) {
$content = $state->buffer;
$state->buffer = '';
yield $context->createChunk($content);
}
if ($chunk->isLast()) {
yield $chunk;
}
});
}
}

View file

@ -0,0 +1,24 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\HttpClient\Exception;
use Symfony\Contracts\HttpClient\Exception\ClientExceptionInterface;
/**
* Represents a 4xx response.
*
* @author Nicolas Grekas <p@tchwork.com>
*/
final class ClientException extends \RuntimeException implements ClientExceptionInterface
{
use HttpExceptionTrait;
}

View file

@ -0,0 +1,21 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\HttpClient\Exception;
use Symfony\Contracts\HttpClient\Exception\DecodingExceptionInterface;
/**
* @author Nicolas Grekas <p@tchwork.com>
*/
final class EventSourceException extends \RuntimeException implements DecodingExceptionInterface
{
}

View file

@ -0,0 +1,78 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\HttpClient\Exception;
use Symfony\Contracts\HttpClient\ResponseInterface;
/**
* @author Nicolas Grekas <p@tchwork.com>
*
* @internal
*/
trait HttpExceptionTrait
{
private $response;
public function __construct(ResponseInterface $response)
{
$this->response = $response;
$code = $response->getInfo('http_code');
$url = $response->getInfo('url');
$message = sprintf('HTTP %d returned for "%s".', $code, $url);
$httpCodeFound = false;
$isJson = false;
foreach (array_reverse($response->getInfo('response_headers')) as $h) {
if (str_starts_with($h, 'HTTP/')) {
if ($httpCodeFound) {
break;
}
$message = sprintf('%s returned for "%s".', $h, $url);
$httpCodeFound = true;
}
if (0 === stripos($h, 'content-type:')) {
if (preg_match('/\bjson\b/i', $h)) {
$isJson = true;
}
if ($httpCodeFound) {
break;
}
}
}
// Try to guess a better error message using common API error formats
// The MIME type isn't explicitly checked because some formats inherit from others
// Ex: JSON:API follows RFC 7807 semantics, Hydra can be used in any JSON-LD-compatible format
if ($isJson && $body = json_decode($response->getContent(false), true)) {
if (isset($body['hydra:title']) || isset($body['hydra:description'])) {
// see http://www.hydra-cg.com/spec/latest/core/#description-of-http-status-codes-and-errors
$separator = isset($body['hydra:title'], $body['hydra:description']) ? "\n\n" : '';
$message = ($body['hydra:title'] ?? '').$separator.($body['hydra:description'] ?? '');
} elseif ((isset($body['title']) || isset($body['detail']))
&& (is_scalar($body['title'] ?? '') && is_scalar($body['detail'] ?? ''))) {
// see RFC 7807 and https://jsonapi.org/format/#error-objects
$separator = isset($body['title'], $body['detail']) ? "\n\n" : '';
$message = ($body['title'] ?? '').$separator.($body['detail'] ?? '');
}
}
parent::__construct($message, $code);
}
public function getResponse(): ResponseInterface
{
return $this->response;
}
}

View file

@ -0,0 +1,21 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\HttpClient\Exception;
use Symfony\Contracts\HttpClient\Exception\TransportExceptionInterface;
/**
* @author Nicolas Grekas <p@tchwork.com>
*/
final class InvalidArgumentException extends \InvalidArgumentException implements TransportExceptionInterface
{
}

View file

@ -0,0 +1,23 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\HttpClient\Exception;
use Symfony\Contracts\HttpClient\Exception\DecodingExceptionInterface;
/**
* Thrown by responses' toArray() method when their content cannot be JSON-decoded.
*
* @author Nicolas Grekas <p@tchwork.com>
*/
final class JsonException extends \JsonException implements DecodingExceptionInterface
{
}

View file

@ -0,0 +1,24 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\HttpClient\Exception;
use Symfony\Contracts\HttpClient\Exception\RedirectionExceptionInterface;
/**
* Represents a 3xx response.
*
* @author Nicolas Grekas <p@tchwork.com>
*/
final class RedirectionException extends \RuntimeException implements RedirectionExceptionInterface
{
use HttpExceptionTrait;
}

View file

@ -0,0 +1,24 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\HttpClient\Exception;
use Symfony\Contracts\HttpClient\Exception\ServerExceptionInterface;
/**
* Represents a 5xx response.
*
* @author Nicolas Grekas <p@tchwork.com>
*/
final class ServerException extends \RuntimeException implements ServerExceptionInterface
{
use HttpExceptionTrait;
}

View file

@ -0,0 +1,21 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\HttpClient\Exception;
use Symfony\Contracts\HttpClient\Exception\TimeoutExceptionInterface;
/**
* @author Nicolas Grekas <p@tchwork.com>
*/
final class TimeoutException extends TransportException implements TimeoutExceptionInterface
{
}

View file

@ -0,0 +1,21 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\HttpClient\Exception;
use Symfony\Contracts\HttpClient\Exception\TransportExceptionInterface;
/**
* @author Nicolas Grekas <p@tchwork.com>
*/
class TransportException extends \RuntimeException implements TransportExceptionInterface
{
}

View file

@ -0,0 +1,78 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\HttpClient;
use Amp\Http\Client\Connection\ConnectionLimitingPool;
use Symfony\Contracts\HttpClient\HttpClientInterface;
/**
* A factory to instantiate the best possible HTTP client for the runtime.
*
* @author Nicolas Grekas <p@tchwork.com>
*/
final class HttpClient
{
/**
* @param array $defaultOptions Default request's options
* @param int $maxHostConnections The maximum number of connections to a single host
* @param int $maxPendingPushes The maximum number of pushed responses to accept in the queue
*
* @see HttpClientInterface::OPTIONS_DEFAULTS for available options
*/
public static function create(array $defaultOptions = [], int $maxHostConnections = 6, int $maxPendingPushes = 50): HttpClientInterface
{
if ($amp = class_exists(ConnectionLimitingPool::class)) {
if (!\extension_loaded('curl')) {
return new AmpHttpClient($defaultOptions, null, $maxHostConnections, $maxPendingPushes);
}
// Skip curl when HTTP/2 push is unsupported or buggy, see https://bugs.php.net/77535
if (\PHP_VERSION_ID < 70217 || (\PHP_VERSION_ID >= 70300 && \PHP_VERSION_ID < 70304) || !\defined('CURLMOPT_PUSHFUNCTION')) {
return new AmpHttpClient($defaultOptions, null, $maxHostConnections, $maxPendingPushes);
}
static $curlVersion = null;
$curlVersion = $curlVersion ?? curl_version();
// HTTP/2 push crashes before curl 7.61
if (0x073d00 > $curlVersion['version_number'] || !(\CURL_VERSION_HTTP2 & $curlVersion['features'])) {
return new AmpHttpClient($defaultOptions, null, $maxHostConnections, $maxPendingPushes);
}
}
if (\extension_loaded('curl')) {
if ('\\' !== \DIRECTORY_SEPARATOR || isset($defaultOptions['cafile']) || isset($defaultOptions['capath']) || ini_get('curl.cainfo') || ini_get('openssl.cafile') || ini_get('openssl.capath')) {
return new CurlHttpClient($defaultOptions, $maxHostConnections, $maxPendingPushes);
}
@trigger_error('Configure the "curl.cainfo", "openssl.cafile" or "openssl.capath" php.ini setting to enable the CurlHttpClient', \E_USER_WARNING);
}
if ($amp) {
return new AmpHttpClient($defaultOptions, null, $maxHostConnections, $maxPendingPushes);
}
@trigger_error((\extension_loaded('curl') ? 'Upgrade' : 'Install').' the curl extension or run "composer require amphp/http-client" to perform async HTTP operations, including full HTTP/2 support', \E_USER_NOTICE);
return new NativeHttpClient($defaultOptions, $maxHostConnections);
}
/**
* Creates a client that adds options (e.g. authentication headers) only when the request URL matches the provided base URI.
*/
public static function createForBaseUri(string $baseUri, array $defaultOptions = [], int $maxHostConnections = 6, int $maxPendingPushes = 50): HttpClientInterface
{
$client = self::create([], $maxHostConnections, $maxPendingPushes);
return ScopingHttpClient::forBaseUri($client, $baseUri, $defaultOptions);
}
}

View file

@ -0,0 +1,639 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\HttpClient;
use Symfony\Component\HttpClient\Exception\InvalidArgumentException;
use Symfony\Component\HttpClient\Exception\TransportException;
/**
* Provides the common logic from writing HttpClientInterface implementations.
*
* All private methods are static to prevent implementers from creating memory leaks via circular references.
*
* @author Nicolas Grekas <p@tchwork.com>
*/
trait HttpClientTrait
{
private static $CHUNK_SIZE = 16372;
/**
* {@inheritdoc}
*/
public function withOptions(array $options): self
{
$clone = clone $this;
$clone->defaultOptions = self::mergeDefaultOptions($options, $this->defaultOptions);
return $clone;
}
/**
* Validates and normalizes method, URL and options, and merges them with defaults.
*
* @throws InvalidArgumentException When a not-supported option is found
*/
private static function prepareRequest(?string $method, ?string $url, array $options, array $defaultOptions = [], bool $allowExtraOptions = false): array
{
if (null !== $method) {
if (\strlen($method) !== strspn($method, 'ABCDEFGHIJKLMNOPQRSTUVWXYZ')) {
throw new InvalidArgumentException(sprintf('Invalid HTTP method "%s", only uppercase letters are accepted.', $method));
}
if (!$method) {
throw new InvalidArgumentException('The HTTP method can not be empty.');
}
}
$options = self::mergeDefaultOptions($options, $defaultOptions, $allowExtraOptions);
$buffer = $options['buffer'] ?? true;
if ($buffer instanceof \Closure) {
$options['buffer'] = static function (array $headers) use ($buffer) {
if (!\is_bool($buffer = $buffer($headers))) {
if (!\is_array($bufferInfo = @stream_get_meta_data($buffer))) {
throw new \LogicException(sprintf('The closure passed as option "buffer" must return bool or stream resource, got "%s".', get_debug_type($buffer)));
}
if (false === strpbrk($bufferInfo['mode'], 'acew+')) {
throw new \LogicException(sprintf('The stream returned by the closure passed as option "buffer" must be writeable, got mode "%s".', $bufferInfo['mode']));
}
}
return $buffer;
};
} elseif (!\is_bool($buffer)) {
if (!\is_array($bufferInfo = @stream_get_meta_data($buffer))) {
throw new InvalidArgumentException(sprintf('Option "buffer" must be bool, stream resource or Closure, "%s" given.', get_debug_type($buffer)));
}
if (false === strpbrk($bufferInfo['mode'], 'acew+')) {
throw new InvalidArgumentException(sprintf('The stream in option "buffer" must be writeable, mode "%s" given.', $bufferInfo['mode']));
}
}
if (isset($options['json'])) {
if (isset($options['body']) && '' !== $options['body']) {
throw new InvalidArgumentException('Define either the "json" or the "body" option, setting both is not supported.');
}
$options['body'] = self::jsonEncode($options['json']);
unset($options['json']);
if (!isset($options['normalized_headers']['content-type'])) {
$options['normalized_headers']['content-type'] = [$options['headers'][] = 'Content-Type: application/json'];
}
}
if (!isset($options['normalized_headers']['accept'])) {
$options['normalized_headers']['accept'] = [$options['headers'][] = 'Accept: */*'];
}
if (isset($options['body'])) {
$options['body'] = self::normalizeBody($options['body']);
}
if (isset($options['peer_fingerprint'])) {
$options['peer_fingerprint'] = self::normalizePeerFingerprint($options['peer_fingerprint']);
}
// Validate on_progress
if (!\is_callable($onProgress = $options['on_progress'] ?? 'var_dump')) {
throw new InvalidArgumentException(sprintf('Option "on_progress" must be callable, "%s" given.', get_debug_type($onProgress)));
}
if (\is_array($options['auth_basic'] ?? null)) {
$count = \count($options['auth_basic']);
if ($count <= 0 || $count > 2) {
throw new InvalidArgumentException(sprintf('Option "auth_basic" must contain 1 or 2 elements, "%s" given.', $count));
}
$options['auth_basic'] = implode(':', $options['auth_basic']);
}
if (!\is_string($options['auth_basic'] ?? '')) {
throw new InvalidArgumentException(sprintf('Option "auth_basic" must be string or an array, "%s" given.', get_debug_type($options['auth_basic'])));
}
if (isset($options['auth_bearer'])) {
if (!\is_string($options['auth_bearer'])) {
throw new InvalidArgumentException(sprintf('Option "auth_bearer" must be a string, "%s" given.', get_debug_type($options['auth_bearer'])));
}
if (preg_match('{[^\x21-\x7E]}', $options['auth_bearer'])) {
throw new InvalidArgumentException('Invalid character found in option "auth_bearer": '.json_encode($options['auth_bearer']).'.');
}
}
if (isset($options['auth_basic'], $options['auth_bearer'])) {
throw new InvalidArgumentException('Define either the "auth_basic" or the "auth_bearer" option, setting both is not supported.');
}
if (null !== $url) {
// Merge auth with headers
if (($options['auth_basic'] ?? false) && !($options['normalized_headers']['authorization'] ?? false)) {
$options['normalized_headers']['authorization'] = [$options['headers'][] = 'Authorization: Basic '.base64_encode($options['auth_basic'])];
}
// Merge bearer with headers
if (($options['auth_bearer'] ?? false) && !($options['normalized_headers']['authorization'] ?? false)) {
$options['normalized_headers']['authorization'] = [$options['headers'][] = 'Authorization: Bearer '.$options['auth_bearer']];
}
unset($options['auth_basic'], $options['auth_bearer']);
// Parse base URI
if (\is_string($options['base_uri'])) {
$options['base_uri'] = self::parseUrl($options['base_uri']);
}
// Validate and resolve URL
$url = self::parseUrl($url, $options['query']);
$url = self::resolveUrl($url, $options['base_uri'], $defaultOptions['query'] ?? []);
}
// Finalize normalization of options
$options['http_version'] = (string) ($options['http_version'] ?? '') ?: null;
$options['timeout'] = (float) ($options['timeout'] ?? ini_get('default_socket_timeout'));
$options['max_duration'] = isset($options['max_duration']) ? (float) $options['max_duration'] : 0;
return [$url, $options];
}
/**
* @throws InvalidArgumentException When an invalid option is found
*/
private static function mergeDefaultOptions(array $options, array $defaultOptions, bool $allowExtraOptions = false): array
{
$options['normalized_headers'] = self::normalizeHeaders($options['headers'] ?? []);
if ($defaultOptions['headers'] ?? false) {
$options['normalized_headers'] += self::normalizeHeaders($defaultOptions['headers']);
}
$options['headers'] = array_merge(...array_values($options['normalized_headers']) ?: [[]]);
if ($resolve = $options['resolve'] ?? false) {
$options['resolve'] = [];
foreach ($resolve as $k => $v) {
$options['resolve'][substr(self::parseUrl('http://'.$k)['authority'], 2)] = (string) $v;
}
}
// Option "query" is never inherited from defaults
$options['query'] = $options['query'] ?? [];
foreach ($defaultOptions as $k => $v) {
if ('normalized_headers' !== $k && !isset($options[$k])) {
$options[$k] = $v;
}
}
if (isset($defaultOptions['extra'])) {
$options['extra'] += $defaultOptions['extra'];
}
if ($resolve = $defaultOptions['resolve'] ?? false) {
foreach ($resolve as $k => $v) {
$options['resolve'] += [substr(self::parseUrl('http://'.$k)['authority'], 2) => (string) $v];
}
}
if ($allowExtraOptions || !$defaultOptions) {
return $options;
}
// Look for unsupported options
foreach ($options as $name => $v) {
if (\array_key_exists($name, $defaultOptions) || 'normalized_headers' === $name) {
continue;
}
if ('auth_ntlm' === $name) {
if (!\extension_loaded('curl')) {
$msg = 'try installing the "curl" extension to use "%s" instead.';
} else {
$msg = 'try using "%s" instead.';
}
throw new InvalidArgumentException(sprintf('Option "auth_ntlm" is not supported by "%s", '.$msg, __CLASS__, CurlHttpClient::class));
}
$alternatives = [];
foreach ($defaultOptions as $key => $v) {
if (levenshtein($name, $key) <= \strlen($name) / 3 || str_contains($key, $name)) {
$alternatives[] = $key;
}
}
throw new InvalidArgumentException(sprintf('Unsupported option "%s" passed to "%s", did you mean "%s"?', $name, __CLASS__, implode('", "', $alternatives ?: array_keys($defaultOptions))));
}
return $options;
}
/**
* @return string[][]
*
* @throws InvalidArgumentException When an invalid header is found
*/
private static function normalizeHeaders(array $headers): array
{
$normalizedHeaders = [];
foreach ($headers as $name => $values) {
if (\is_object($values) && method_exists($values, '__toString')) {
$values = (string) $values;
}
if (\is_int($name)) {
if (!\is_string($values)) {
throw new InvalidArgumentException(sprintf('Invalid value for header "%s": expected string, "%s" given.', $name, get_debug_type($values)));
}
[$name, $values] = explode(':', $values, 2);
$values = [ltrim($values)];
} elseif (!is_iterable($values)) {
if (\is_object($values)) {
throw new InvalidArgumentException(sprintf('Invalid value for header "%s": expected string, "%s" given.', $name, get_debug_type($values)));
}
$values = (array) $values;
}
$lcName = strtolower($name);
$normalizedHeaders[$lcName] = [];
foreach ($values as $value) {
$normalizedHeaders[$lcName][] = $value = $name.': '.$value;
if (\strlen($value) !== strcspn($value, "\r\n\0")) {
throw new InvalidArgumentException(sprintf('Invalid header: CR/LF/NUL found in "%s".', $value));
}
}
}
return $normalizedHeaders;
}
/**
* @param array|string|resource|\Traversable|\Closure $body
*
* @return string|resource|\Closure
*
* @throws InvalidArgumentException When an invalid body is passed
*/
private static function normalizeBody($body)
{
if (\is_array($body)) {
return http_build_query($body, '', '&', \PHP_QUERY_RFC1738);
}
if (\is_string($body)) {
return $body;
}
$generatorToCallable = static function (\Generator $body): \Closure {
return static function () use ($body) {
while ($body->valid()) {
$chunk = $body->current();
$body->next();
if ('' !== $chunk) {
return $chunk;
}
}
return '';
};
};
if ($body instanceof \Generator) {
return $generatorToCallable($body);
}
if ($body instanceof \Traversable) {
return $generatorToCallable((static function ($body) { yield from $body; })($body));
}
if ($body instanceof \Closure) {
$r = new \ReflectionFunction($body);
$body = $r->getClosure();
if ($r->isGenerator()) {
$body = $body(self::$CHUNK_SIZE);
return $generatorToCallable($body);
}
return $body;
}
if (!\is_array(@stream_get_meta_data($body))) {
throw new InvalidArgumentException(sprintf('Option "body" must be string, stream resource, iterable or callable, "%s" given.', get_debug_type($body)));
}
return $body;
}
/**
* @param string|string[] $fingerprint
*
* @throws InvalidArgumentException When an invalid fingerprint is passed
*/
private static function normalizePeerFingerprint($fingerprint): array
{
if (\is_string($fingerprint)) {
switch (\strlen($fingerprint = str_replace(':', '', $fingerprint))) {
case 32: $fingerprint = ['md5' => $fingerprint]; break;
case 40: $fingerprint = ['sha1' => $fingerprint]; break;
case 44: $fingerprint = ['pin-sha256' => [$fingerprint]]; break;
case 64: $fingerprint = ['sha256' => $fingerprint]; break;
default: throw new InvalidArgumentException(sprintf('Cannot auto-detect fingerprint algorithm for "%s".', $fingerprint));
}
} elseif (\is_array($fingerprint)) {
foreach ($fingerprint as $algo => $hash) {
$fingerprint[$algo] = 'pin-sha256' === $algo ? (array) $hash : str_replace(':', '', $hash);
}
} else {
throw new InvalidArgumentException(sprintf('Option "peer_fingerprint" must be string or array, "%s" given.', get_debug_type($fingerprint)));
}
return $fingerprint;
}
/**
* @param mixed $value
*
* @throws InvalidArgumentException When the value cannot be json-encoded
*/
private static function jsonEncode($value, int $flags = null, int $maxDepth = 512): string
{
$flags = $flags ?? (\JSON_HEX_TAG | \JSON_HEX_APOS | \JSON_HEX_AMP | \JSON_HEX_QUOT | \JSON_PRESERVE_ZERO_FRACTION);
try {
$value = json_encode($value, $flags | (\PHP_VERSION_ID >= 70300 ? \JSON_THROW_ON_ERROR : 0), $maxDepth);
} catch (\JsonException $e) {
throw new InvalidArgumentException('Invalid value for "json" option: '.$e->getMessage());
}
if (\PHP_VERSION_ID < 70300 && \JSON_ERROR_NONE !== json_last_error() && (false === $value || !($flags & \JSON_PARTIAL_OUTPUT_ON_ERROR))) {
throw new InvalidArgumentException('Invalid value for "json" option: '.json_last_error_msg());
}
return $value;
}
/**
* Resolves a URL against a base URI.
*
* @see https://tools.ietf.org/html/rfc3986#section-5.2.2
*
* @throws InvalidArgumentException When an invalid URL is passed
*/
private static function resolveUrl(array $url, ?array $base, array $queryDefaults = []): array
{
if (null !== $base && '' === ($base['scheme'] ?? '').($base['authority'] ?? '')) {
throw new InvalidArgumentException(sprintf('Invalid "base_uri" option: host or scheme is missing in "%s".', implode('', $base)));
}
if (null === $url['scheme'] && (null === $base || null === $base['scheme'])) {
throw new InvalidArgumentException(sprintf('Invalid URL: scheme is missing in "%s". Did you forget to add "http(s)://"?', implode('', $base ?? $url)));
}
if (null === $base && '' === $url['scheme'].$url['authority']) {
throw new InvalidArgumentException(sprintf('Invalid URL: no "base_uri" option was provided and host or scheme is missing in "%s".', implode('', $url)));
}
if (null !== $url['scheme']) {
$url['path'] = self::removeDotSegments($url['path'] ?? '');
} else {
if (null !== $url['authority']) {
$url['path'] = self::removeDotSegments($url['path'] ?? '');
} else {
if (null === $url['path']) {
$url['path'] = $base['path'];
$url['query'] = $url['query'] ?? $base['query'];
} else {
if ('/' !== $url['path'][0]) {
if (null === $base['path']) {
$url['path'] = '/'.$url['path'];
} else {
$segments = explode('/', $base['path']);
array_splice($segments, -1, 1, [$url['path']]);
$url['path'] = implode('/', $segments);
}
}
$url['path'] = self::removeDotSegments($url['path']);
}
$url['authority'] = $base['authority'];
if ($queryDefaults) {
$url['query'] = '?'.self::mergeQueryString(substr($url['query'] ?? '', 1), $queryDefaults, false);
}
}
$url['scheme'] = $base['scheme'];
}
if ('' === ($url['path'] ?? '')) {
$url['path'] = '/';
}
if ('?' === ($url['query'] ?? '')) {
$url['query'] = null;
}
return $url;
}
/**
* Parses a URL and fixes its encoding if needed.
*
* @throws InvalidArgumentException When an invalid URL is passed
*/
private static function parseUrl(string $url, array $query = [], array $allowedSchemes = ['http' => 80, 'https' => 443]): array
{
if (false === $parts = parse_url($url)) {
throw new InvalidArgumentException(sprintf('Malformed URL "%s".', $url));
}
if ($query) {
$parts['query'] = self::mergeQueryString($parts['query'] ?? null, $query, true);
}
$port = $parts['port'] ?? 0;
if (null !== $scheme = $parts['scheme'] ?? null) {
if (!isset($allowedSchemes[$scheme = strtolower($scheme)])) {
throw new InvalidArgumentException(sprintf('Unsupported scheme in "%s".', $url));
}
$port = $allowedSchemes[$scheme] === $port ? 0 : $port;
$scheme .= ':';
}
if (null !== $host = $parts['host'] ?? null) {
if (!\defined('INTL_IDNA_VARIANT_UTS46') && preg_match('/[\x80-\xFF]/', $host)) {
throw new InvalidArgumentException(sprintf('Unsupported IDN "%s", try enabling the "intl" PHP extension or running "composer require symfony/polyfill-intl-idn".', $host));
}
$host = \defined('INTL_IDNA_VARIANT_UTS46') ? idn_to_ascii($host, \IDNA_DEFAULT, \INTL_IDNA_VARIANT_UTS46) ?: strtolower($host) : strtolower($host);
$host .= $port ? ':'.$port : '';
}
foreach (['user', 'pass', 'path', 'query', 'fragment'] as $part) {
if (!isset($parts[$part])) {
continue;
}
if (str_contains($parts[$part], '%')) {
// https://tools.ietf.org/html/rfc3986#section-2.3
$parts[$part] = preg_replace_callback('/%(?:2[DE]|3[0-9]|[46][1-9A-F]|5F|[57][0-9A]|7E)++/i', function ($m) { return rawurldecode($m[0]); }, $parts[$part]);
}
// https://tools.ietf.org/html/rfc3986#section-3.3
$parts[$part] = preg_replace_callback("#[^-A-Za-z0-9._~!$&/'()*+,;=:@%]++#", function ($m) { return rawurlencode($m[0]); }, $parts[$part]);
}
return [
'scheme' => $scheme,
'authority' => null !== $host ? '//'.(isset($parts['user']) ? $parts['user'].(isset($parts['pass']) ? ':'.$parts['pass'] : '').'@' : '').$host : null,
'path' => isset($parts['path'][0]) ? $parts['path'] : null,
'query' => isset($parts['query']) ? '?'.$parts['query'] : null,
'fragment' => isset($parts['fragment']) ? '#'.$parts['fragment'] : null,
];
}
/**
* Removes dot-segments from a path.
*
* @see https://tools.ietf.org/html/rfc3986#section-5.2.4
*/
private static function removeDotSegments(string $path)
{
$result = '';
while (!\in_array($path, ['', '.', '..'], true)) {
if ('.' === $path[0] && (str_starts_with($path, $p = '../') || str_starts_with($path, $p = './'))) {
$path = substr($path, \strlen($p));
} elseif ('/.' === $path || str_starts_with($path, '/./')) {
$path = substr_replace($path, '/', 0, 3);
} elseif ('/..' === $path || str_starts_with($path, '/../')) {
$i = strrpos($result, '/');
$result = $i ? substr($result, 0, $i) : '';
$path = substr_replace($path, '/', 0, 4);
} else {
$i = strpos($path, '/', 1) ?: \strlen($path);
$result .= substr($path, 0, $i);
$path = substr($path, $i);
}
}
return $result;
}
/**
* Merges and encodes a query array with a query string.
*
* @throws InvalidArgumentException When an invalid query-string value is passed
*/
private static function mergeQueryString(?string $queryString, array $queryArray, bool $replace): ?string
{
if (!$queryArray) {
return $queryString;
}
$query = [];
if (null !== $queryString) {
foreach (explode('&', $queryString) as $v) {
if ('' !== $v) {
$k = urldecode(explode('=', $v, 2)[0]);
$query[$k] = (isset($query[$k]) ? $query[$k].'&' : '').$v;
}
}
}
if ($replace) {
foreach ($queryArray as $k => $v) {
if (null === $v) {
unset($query[$k]);
}
}
}
$queryString = http_build_query($queryArray, '', '&', \PHP_QUERY_RFC3986);
$queryArray = [];
if ($queryString) {
foreach (explode('&', $queryString) as $v) {
$queryArray[rawurldecode(explode('=', $v, 2)[0])] = $v;
}
}
return implode('&', $replace ? array_replace($query, $queryArray) : ($query + $queryArray));
}
/**
* Loads proxy configuration from the same environment variables as curl when no proxy is explicitly set.
*/
private static function getProxy(?string $proxy, array $url, ?string $noProxy): ?array
{
if (null === $proxy) {
// Ignore HTTP_PROXY except on the CLI to work around httpoxy set of vulnerabilities
$proxy = $_SERVER['http_proxy'] ?? (\in_array(\PHP_SAPI, ['cli', 'phpdbg'], true) ? $_SERVER['HTTP_PROXY'] ?? null : null) ?? $_SERVER['all_proxy'] ?? $_SERVER['ALL_PROXY'] ?? null;
if ('https:' === $url['scheme']) {
$proxy = $_SERVER['https_proxy'] ?? $_SERVER['HTTPS_PROXY'] ?? $proxy;
}
}
if (null === $proxy) {
return null;
}
$proxy = (parse_url($proxy) ?: []) + ['scheme' => 'http'];
if (!isset($proxy['host'])) {
throw new TransportException('Invalid HTTP proxy: host is missing.');
}
if ('http' === $proxy['scheme']) {
$proxyUrl = 'tcp://'.$proxy['host'].':'.($proxy['port'] ?? '80');
} elseif ('https' === $proxy['scheme']) {
$proxyUrl = 'ssl://'.$proxy['host'].':'.($proxy['port'] ?? '443');
} else {
throw new TransportException(sprintf('Unsupported proxy scheme "%s": "http" or "https" expected.', $proxy['scheme']));
}
$noProxy = $noProxy ?? $_SERVER['no_proxy'] ?? $_SERVER['NO_PROXY'] ?? '';
$noProxy = $noProxy ? preg_split('/[\s,]+/', $noProxy) : [];
return [
'url' => $proxyUrl,
'auth' => isset($proxy['user']) ? 'Basic '.base64_encode(rawurldecode($proxy['user']).':'.rawurldecode($proxy['pass'] ?? '')) : null,
'no_proxy' => $noProxy,
];
}
private static function shouldBuffer(array $headers): bool
{
if (null === $contentType = $headers['content-type'][0] ?? null) {
return false;
}
if (false !== $i = strpos($contentType, ';')) {
$contentType = substr($contentType, 0, $i);
}
return $contentType && preg_match('#^(?:text/|application/(?:.+\+)?(?:json|xml)$)#i', $contentType);
}
}

View file

@ -0,0 +1,321 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\HttpClient;
use Symfony\Contracts\HttpClient\HttpClientInterface;
/**
* A helper providing autocompletion for available options.
*
* @see HttpClientInterface for a description of each options.
*
* @author Nicolas Grekas <p@tchwork.com>
*/
class HttpOptions
{
private $options = [];
public function toArray(): array
{
return $this->options;
}
/**
* @return $this
*/
public function setAuthBasic(string $user, string $password = '')
{
$this->options['auth_basic'] = $user;
if ('' !== $password) {
$this->options['auth_basic'] .= ':'.$password;
}
return $this;
}
/**
* @return $this
*/
public function setAuthBearer(string $token)
{
$this->options['auth_bearer'] = $token;
return $this;
}
/**
* @return $this
*/
public function setQuery(array $query)
{
$this->options['query'] = $query;
return $this;
}
/**
* @return $this
*/
public function setHeaders(iterable $headers)
{
$this->options['headers'] = $headers;
return $this;
}
/**
* @param array|string|resource|\Traversable|\Closure $body
*
* @return $this
*/
public function setBody($body)
{
$this->options['body'] = $body;
return $this;
}
/**
* @param mixed $json
*
* @return $this
*/
public function setJson($json)
{
$this->options['json'] = $json;
return $this;
}
/**
* @return $this
*/
public function setUserData($data)
{
$this->options['user_data'] = $data;
return $this;
}
/**
* @return $this
*/
public function setMaxRedirects(int $max)
{
$this->options['max_redirects'] = $max;
return $this;
}
/**
* @return $this
*/
public function setHttpVersion(string $version)
{
$this->options['http_version'] = $version;
return $this;
}
/**
* @return $this
*/
public function setBaseUri(string $uri)
{
$this->options['base_uri'] = $uri;
return $this;
}
/**
* @return $this
*/
public function buffer(bool $buffer)
{
$this->options['buffer'] = $buffer;
return $this;
}
/**
* @return $this
*/
public function setOnProgress(callable $callback)
{
$this->options['on_progress'] = $callback;
return $this;
}
/**
* @return $this
*/
public function resolve(array $hostIps)
{
$this->options['resolve'] = $hostIps;
return $this;
}
/**
* @return $this
*/
public function setProxy(string $proxy)
{
$this->options['proxy'] = $proxy;
return $this;
}
/**
* @return $this
*/
public function setNoProxy(string $noProxy)
{
$this->options['no_proxy'] = $noProxy;
return $this;
}
/**
* @return $this
*/
public function setTimeout(float $timeout)
{
$this->options['timeout'] = $timeout;
return $this;
}
/**
* @return $this
*/
public function bindTo(string $bindto)
{
$this->options['bindto'] = $bindto;
return $this;
}
/**
* @return $this
*/
public function verifyPeer(bool $verify)
{
$this->options['verify_peer'] = $verify;
return $this;
}
/**
* @return $this
*/
public function verifyHost(bool $verify)
{
$this->options['verify_host'] = $verify;
return $this;
}
/**
* @return $this
*/
public function setCaFile(string $cafile)
{
$this->options['cafile'] = $cafile;
return $this;
}
/**
* @return $this
*/
public function setCaPath(string $capath)
{
$this->options['capath'] = $capath;
return $this;
}
/**
* @return $this
*/
public function setLocalCert(string $cert)
{
$this->options['local_cert'] = $cert;
return $this;
}
/**
* @return $this
*/
public function setLocalPk(string $pk)
{
$this->options['local_pk'] = $pk;
return $this;
}
/**
* @return $this
*/
public function setPassphrase(string $passphrase)
{
$this->options['passphrase'] = $passphrase;
return $this;
}
/**
* @return $this
*/
public function setCiphers(string $ciphers)
{
$this->options['ciphers'] = $ciphers;
return $this;
}
/**
* @param string|array $fingerprint
*
* @return $this
*/
public function setPeerFingerprint($fingerprint)
{
$this->options['peer_fingerprint'] = $fingerprint;
return $this;
}
/**
* @return $this
*/
public function capturePeerCertChain(bool $capture)
{
$this->options['capture_peer_cert_chain'] = $capture;
return $this;
}
/**
* @return $this
*/
public function setExtra(string $name, $value)
{
$this->options['extra'][$name] = $value;
return $this;
}
}

View file

@ -0,0 +1,260 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\HttpClient;
use GuzzleHttp\Promise\Promise as GuzzlePromise;
use GuzzleHttp\Promise\RejectedPromise;
use Http\Client\Exception\NetworkException;
use Http\Client\Exception\RequestException;
use Http\Client\HttpAsyncClient;
use Http\Client\HttpClient as HttplugInterface;
use Http\Discovery\Exception\NotFoundException;
use Http\Discovery\Psr17FactoryDiscovery;
use Http\Message\RequestFactory;
use Http\Message\StreamFactory;
use Http\Message\UriFactory;
use Http\Promise\Promise;
use Nyholm\Psr7\Factory\Psr17Factory;
use Nyholm\Psr7\Request;
use Nyholm\Psr7\Uri;
use Psr\Http\Message\RequestFactoryInterface;
use Psr\Http\Message\RequestInterface;
use Psr\Http\Message\ResponseFactoryInterface;
use Psr\Http\Message\ResponseInterface as Psr7ResponseInterface;
use Psr\Http\Message\StreamFactoryInterface;
use Psr\Http\Message\StreamInterface;
use Psr\Http\Message\UriFactoryInterface;
use Psr\Http\Message\UriInterface;
use Symfony\Component\HttpClient\Internal\HttplugWaitLoop;
use Symfony\Component\HttpClient\Response\HttplugPromise;
use Symfony\Contracts\HttpClient\Exception\TransportExceptionInterface;
use Symfony\Contracts\HttpClient\HttpClientInterface;
use Symfony\Contracts\HttpClient\ResponseInterface;
if (!interface_exists(HttplugInterface::class)) {
throw new \LogicException('You cannot use "Symfony\Component\HttpClient\HttplugClient" as the "php-http/httplug" package is not installed. Try running "composer require php-http/httplug".');
}
if (!interface_exists(RequestFactory::class)) {
throw new \LogicException('You cannot use "Symfony\Component\HttpClient\HttplugClient" as the "php-http/message-factory" package is not installed. Try running "composer require nyholm/psr7".');
}
/**
* An adapter to turn a Symfony HttpClientInterface into an Httplug client.
*
* Run "composer require nyholm/psr7" to install an efficient implementation of response
* and stream factories with flex-provided autowiring aliases.
*
* @author Nicolas Grekas <p@tchwork.com>
*/
final class HttplugClient implements HttplugInterface, HttpAsyncClient, RequestFactory, StreamFactory, UriFactory
{
private $client;
private $responseFactory;
private $streamFactory;
private $promisePool;
private $waitLoop;
public function __construct(HttpClientInterface $client = null, ResponseFactoryInterface $responseFactory = null, StreamFactoryInterface $streamFactory = null)
{
$this->client = $client ?? HttpClient::create();
$this->responseFactory = $responseFactory;
$this->streamFactory = $streamFactory ?? ($responseFactory instanceof StreamFactoryInterface ? $responseFactory : null);
$this->promisePool = \function_exists('GuzzleHttp\Promise\queue') ? new \SplObjectStorage() : null;
if (null === $this->responseFactory || null === $this->streamFactory) {
if (!class_exists(Psr17Factory::class) && !class_exists(Psr17FactoryDiscovery::class)) {
throw new \LogicException('You cannot use the "Symfony\Component\HttpClient\HttplugClient" as no PSR-17 factories have been provided. Try running "composer require nyholm/psr7".');
}
try {
$psr17Factory = class_exists(Psr17Factory::class, false) ? new Psr17Factory() : null;
$this->responseFactory = $this->responseFactory ?? $psr17Factory ?? Psr17FactoryDiscovery::findResponseFactory();
$this->streamFactory = $this->streamFactory ?? $psr17Factory ?? Psr17FactoryDiscovery::findStreamFactory();
} catch (NotFoundException $e) {
throw new \LogicException('You cannot use the "Symfony\Component\HttpClient\HttplugClient" as no PSR-17 factories have been found. Try running "composer require nyholm/psr7".', 0, $e);
}
}
$this->waitLoop = new HttplugWaitLoop($this->client, $this->promisePool, $this->responseFactory, $this->streamFactory);
}
/**
* {@inheritdoc}
*/
public function sendRequest(RequestInterface $request): Psr7ResponseInterface
{
try {
return $this->waitLoop->createPsr7Response($this->sendPsr7Request($request));
} catch (TransportExceptionInterface $e) {
throw new NetworkException($e->getMessage(), $request, $e);
}
}
/**
* {@inheritdoc}
*
* @return HttplugPromise
*/
public function sendAsyncRequest(RequestInterface $request): Promise
{
if (!$promisePool = $this->promisePool) {
throw new \LogicException(sprintf('You cannot use "%s()" as the "guzzlehttp/promises" package is not installed. Try running "composer require guzzlehttp/promises".', __METHOD__));
}
try {
$response = $this->sendPsr7Request($request, true);
} catch (NetworkException $e) {
return new HttplugPromise(new RejectedPromise($e));
}
$waitLoop = $this->waitLoop;
$promise = new GuzzlePromise(static function () use ($response, $waitLoop) {
$waitLoop->wait($response);
}, static function () use ($response, $promisePool) {
$response->cancel();
unset($promisePool[$response]);
});
$promisePool[$response] = [$request, $promise];
return new HttplugPromise($promise);
}
/**
* Resolves pending promises that complete before the timeouts are reached.
*
* When $maxDuration is null and $idleTimeout is reached, promises are rejected.
*
* @return int The number of remaining pending promises
*/
public function wait(float $maxDuration = null, float $idleTimeout = null): int
{
return $this->waitLoop->wait(null, $maxDuration, $idleTimeout);
}
/**
* {@inheritdoc}
*/
public function createRequest($method, $uri, array $headers = [], $body = null, $protocolVersion = '1.1'): RequestInterface
{
if ($this->responseFactory instanceof RequestFactoryInterface) {
$request = $this->responseFactory->createRequest($method, $uri);
} elseif (class_exists(Request::class)) {
$request = new Request($method, $uri);
} elseif (class_exists(Psr17FactoryDiscovery::class)) {
$request = Psr17FactoryDiscovery::findRequestFactory()->createRequest($method, $uri);
} else {
throw new \LogicException(sprintf('You cannot use "%s()" as the "nyholm/psr7" package is not installed. Try running "composer require nyholm/psr7".', __METHOD__));
}
$request = $request
->withProtocolVersion($protocolVersion)
->withBody($this->createStream($body))
;
foreach ($headers as $name => $value) {
$request = $request->withAddedHeader($name, $value);
}
return $request;
}
/**
* {@inheritdoc}
*/
public function createStream($body = null): StreamInterface
{
if ($body instanceof StreamInterface) {
return $body;
}
if (\is_string($body ?? '')) {
$stream = $this->streamFactory->createStream($body ?? '');
} elseif (\is_resource($body)) {
$stream = $this->streamFactory->createStreamFromResource($body);
} else {
throw new \InvalidArgumentException(sprintf('"%s()" expects string, resource or StreamInterface, "%s" given.', __METHOD__, get_debug_type($body)));
}
if ($stream->isSeekable()) {
$stream->seek(0);
}
return $stream;
}
/**
* {@inheritdoc}
*/
public function createUri($uri): UriInterface
{
if ($uri instanceof UriInterface) {
return $uri;
}
if ($this->responseFactory instanceof UriFactoryInterface) {
return $this->responseFactory->createUri($uri);
}
if (class_exists(Uri::class)) {
return new Uri($uri);
}
if (class_exists(Psr17FactoryDiscovery::class)) {
return Psr17FactoryDiscovery::findUrlFactory()->createUri($uri);
}
throw new \LogicException(sprintf('You cannot use "%s()" as the "nyholm/psr7" package is not installed. Try running "composer require nyholm/psr7".', __METHOD__));
}
/**
* @return array
*/
public function __sleep()
{
throw new \BadMethodCallException('Cannot serialize '.__CLASS__);
}
public function __wakeup()
{
throw new \BadMethodCallException('Cannot unserialize '.__CLASS__);
}
public function __destruct()
{
$this->wait();
}
private function sendPsr7Request(RequestInterface $request, bool $buffer = null): ResponseInterface
{
try {
$body = $request->getBody();
if ($body->isSeekable()) {
$body->seek(0);
}
return $this->client->request($request->getMethod(), (string) $request->getUri(), [
'headers' => $request->getHeaders(),
'body' => $body->getContents(),
'http_version' => '1.0' === $request->getProtocolVersion() ? '1.0' : null,
'buffer' => $buffer,
]);
} catch (\InvalidArgumentException $e) {
throw new RequestException($e->getMessage(), $request, $e);
} catch (TransportExceptionInterface $e) {
throw new NetworkException($e->getMessage(), $request, $e);
}
}
}

View file

@ -0,0 +1,141 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\HttpClient\Internal;
use Amp\ByteStream\InputStream;
use Amp\ByteStream\ResourceInputStream;
use Amp\Http\Client\RequestBody;
use Amp\Promise;
use Amp\Success;
use Symfony\Component\HttpClient\Exception\TransportException;
/**
* @author Nicolas Grekas <p@tchwork.com>
*
* @internal
*/
class AmpBody implements RequestBody, InputStream
{
private $body;
private $onProgress;
private $offset = 0;
private $length = -1;
private $uploaded;
public function __construct($body, &$info, \Closure $onProgress)
{
$this->body = $body;
$this->info = &$info;
$this->onProgress = $onProgress;
if (\is_resource($body)) {
$this->offset = ftell($body);
$this->length = fstat($body)['size'];
$this->body = new ResourceInputStream($body);
} elseif (\is_string($body)) {
$this->length = \strlen($body);
}
}
public function createBodyStream(): InputStream
{
if (null !== $this->uploaded) {
$this->uploaded = null;
if (\is_string($this->body)) {
$this->offset = 0;
} elseif ($this->body instanceof ResourceInputStream) {
fseek($this->body->getResource(), $this->offset);
}
}
return $this;
}
public function getHeaders(): Promise
{
return new Success([]);
}
public function getBodyLength(): Promise
{
return new Success($this->length - $this->offset);
}
public function read(): Promise
{
$this->info['size_upload'] += $this->uploaded;
$this->uploaded = 0;
($this->onProgress)();
$chunk = $this->doRead();
$chunk->onResolve(function ($e, $data) {
if (null !== $data) {
$this->uploaded = \strlen($data);
} else {
$this->info['upload_content_length'] = $this->info['size_upload'];
}
});
return $chunk;
}
public static function rewind(RequestBody $body): RequestBody
{
if (!$body instanceof self) {
return $body;
}
$body->uploaded = null;
if ($body->body instanceof ResourceInputStream) {
fseek($body->body->getResource(), $body->offset);
return new $body($body->body, $body->info, $body->onProgress);
}
if (\is_string($body->body)) {
$body->offset = 0;
}
return $body;
}
private function doRead(): Promise
{
if ($this->body instanceof ResourceInputStream) {
return $this->body->read();
}
if (null === $this->offset || !$this->length) {
return new Success();
}
if (\is_string($this->body)) {
$this->offset = null;
return new Success($this->body);
}
if ('' === $data = ($this->body)(16372)) {
$this->offset = null;
return new Success();
}
if (!\is_string($data)) {
throw new TransportException(sprintf('Return value of the "body" option callback must be string, "%s" returned.', get_debug_type($data)));
}
return new Success($data);
}
}

View file

@ -0,0 +1,217 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\HttpClient\Internal;
use Amp\CancellationToken;
use Amp\Deferred;
use Amp\Http\Client\Connection\ConnectionLimitingPool;
use Amp\Http\Client\Connection\DefaultConnectionFactory;
use Amp\Http\Client\InterceptedHttpClient;
use Amp\Http\Client\Interceptor\RetryRequests;
use Amp\Http\Client\PooledHttpClient;
use Amp\Http\Client\Request;
use Amp\Http\Client\Response;
use Amp\Http\Tunnel\Http1TunnelConnector;
use Amp\Http\Tunnel\Https1TunnelConnector;
use Amp\Promise;
use Amp\Socket\Certificate;
use Amp\Socket\ClientTlsContext;
use Amp\Socket\ConnectContext;
use Amp\Socket\Connector;
use Amp\Socket\DnsConnector;
use Amp\Socket\SocketAddress;
use Amp\Success;
use Psr\Log\LoggerInterface;
/**
* Internal representation of the Amp client's state.
*
* @author Nicolas Grekas <p@tchwork.com>
*
* @internal
*/
final class AmpClientState extends ClientState
{
public $dnsCache = [];
public $responseCount = 0;
public $pushedResponses = [];
private $clients = [];
private $clientConfigurator;
private $maxHostConnections;
private $maxPendingPushes;
private $logger;
public function __construct(?callable $clientConfigurator, int $maxHostConnections, int $maxPendingPushes, ?LoggerInterface &$logger)
{
$this->clientConfigurator = $clientConfigurator ?? static function (PooledHttpClient $client) {
return new InterceptedHttpClient($client, new RetryRequests(2));
};
$this->maxHostConnections = $maxHostConnections;
$this->maxPendingPushes = $maxPendingPushes;
$this->logger = &$logger;
}
/**
* @return Promise<Response>
*/
public function request(array $options, Request $request, CancellationToken $cancellation, array &$info, \Closure $onProgress, &$handle): Promise
{
if ($options['proxy']) {
if ($request->hasHeader('proxy-authorization')) {
$options['proxy']['auth'] = $request->getHeader('proxy-authorization');
}
// Matching "no_proxy" should follow the behavior of curl
$host = $request->getUri()->getHost();
foreach ($options['proxy']['no_proxy'] as $rule) {
$dotRule = '.'.ltrim($rule, '.');
if ('*' === $rule || $host === $rule || substr($host, -\strlen($dotRule)) === $dotRule) {
$options['proxy'] = null;
break;
}
}
}
$request = clone $request;
if ($request->hasHeader('proxy-authorization')) {
$request->removeHeader('proxy-authorization');
}
if ($options['capture_peer_cert_chain']) {
$info['peer_certificate_chain'] = [];
}
$request->addEventListener(new AmpListener($info, $options['peer_fingerprint']['pin-sha256'] ?? [], $onProgress, $handle));
$request->setPushHandler(function ($request, $response) use ($options): Promise {
return $this->handlePush($request, $response, $options);
});
($request->hasHeader('content-length') ? new Success((int) $request->getHeader('content-length')) : $request->getBody()->getBodyLength())
->onResolve(static function ($e, $bodySize) use (&$info) {
if (null !== $bodySize && 0 <= $bodySize) {
$info['upload_content_length'] = ((1 + $info['upload_content_length']) ?? 1) - 1 + $bodySize;
}
});
[$client, $connector] = $this->getClient($options);
$response = $client->request($request, $cancellation);
$response->onResolve(static function ($e) use ($connector, &$handle) {
if (null === $e) {
$handle = $connector->handle;
}
});
return $response;
}
private function getClient(array $options): array
{
$options = [
'bindto' => $options['bindto'] ?: '0',
'verify_peer' => $options['verify_peer'],
'capath' => $options['capath'],
'cafile' => $options['cafile'],
'local_cert' => $options['local_cert'],
'local_pk' => $options['local_pk'],
'ciphers' => $options['ciphers'],
'capture_peer_cert_chain' => $options['capture_peer_cert_chain'] || $options['peer_fingerprint'],
'proxy' => $options['proxy'],
];
$key = md5(serialize($options));
if (isset($this->clients[$key])) {
return $this->clients[$key];
}
$context = new ClientTlsContext('');
$options['verify_peer'] || $context = $context->withoutPeerVerification();
$options['cafile'] && $context = $context->withCaFile($options['cafile']);
$options['capath'] && $context = $context->withCaPath($options['capath']);
$options['local_cert'] && $context = $context->withCertificate(new Certificate($options['local_cert'], $options['local_pk']));
$options['ciphers'] && $context = $context->withCiphers($options['ciphers']);
$options['capture_peer_cert_chain'] && $context = $context->withPeerCapturing();
$connector = $handleConnector = new class() implements Connector {
public $connector;
public $uri;
public $handle;
public function connect(string $uri, ConnectContext $context = null, CancellationToken $token = null): Promise
{
$result = $this->connector->connect($this->uri ?? $uri, $context, $token);
$result->onResolve(function ($e, $socket) {
$this->handle = null !== $socket ? $socket->getResource() : false;
});
return $result;
}
};
$connector->connector = new DnsConnector(new AmpResolver($this->dnsCache));
$context = (new ConnectContext())
->withTcpNoDelay()
->withTlsContext($context);
if ($options['bindto']) {
if (file_exists($options['bindto'])) {
$connector->uri = 'unix://'.$options['bindto'];
} else {
$context = $context->withBindTo($options['bindto']);
}
}
if ($options['proxy']) {
$proxyUrl = parse_url($options['proxy']['url']);
$proxySocket = new SocketAddress($proxyUrl['host'], $proxyUrl['port']);
$proxyHeaders = $options['proxy']['auth'] ? ['Proxy-Authorization' => $options['proxy']['auth']] : [];
if ('ssl' === $proxyUrl['scheme']) {
$connector = new Https1TunnelConnector($proxySocket, $context->getTlsContext(), $proxyHeaders, $connector);
} else {
$connector = new Http1TunnelConnector($proxySocket, $proxyHeaders, $connector);
}
}
$maxHostConnections = 0 < $this->maxHostConnections ? $this->maxHostConnections : \PHP_INT_MAX;
$pool = new DefaultConnectionFactory($connector, $context);
$pool = ConnectionLimitingPool::byAuthority($maxHostConnections, $pool);
return $this->clients[$key] = [($this->clientConfigurator)(new PooledHttpClient($pool)), $handleConnector];
}
private function handlePush(Request $request, Promise $response, array $options): Promise
{
$deferred = new Deferred();
$authority = $request->getUri()->getAuthority();
if ($this->maxPendingPushes <= \count($this->pushedResponses[$authority] ?? [])) {
$fifoUrl = key($this->pushedResponses[$authority]);
unset($this->pushedResponses[$authority][$fifoUrl]);
$this->logger && $this->logger->debug(sprintf('Evicting oldest pushed response: "%s"', $fifoUrl));
}
$url = (string) $request->getUri();
$this->logger && $this->logger->debug(sprintf('Queueing pushed response: "%s"', $url));
$this->pushedResponses[$authority][] = [$url, $deferred, $request, $response, [
'proxy' => $options['proxy'],
'bindto' => $options['bindto'],
'local_cert' => $options['local_cert'],
'local_pk' => $options['local_pk'],
]];
return $deferred->promise();
}
}

View file

@ -0,0 +1,183 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\HttpClient\Internal;
use Amp\Http\Client\Connection\Stream;
use Amp\Http\Client\EventListener;
use Amp\Http\Client\Request;
use Amp\Promise;
use Amp\Success;
use Symfony\Component\HttpClient\Exception\TransportException;
/**
* @author Nicolas Grekas <p@tchwork.com>
*
* @internal
*/
class AmpListener implements EventListener
{
private $info;
private $pinSha256;
private $onProgress;
private $handle;
public function __construct(array &$info, array $pinSha256, \Closure $onProgress, &$handle)
{
$info += [
'connect_time' => 0.0,
'pretransfer_time' => 0.0,
'starttransfer_time' => 0.0,
'total_time' => 0.0,
'namelookup_time' => 0.0,
'primary_ip' => '',
'primary_port' => 0,
];
$this->info = &$info;
$this->pinSha256 = $pinSha256;
$this->onProgress = $onProgress;
$this->handle = &$handle;
}
public function startRequest(Request $request): Promise
{
$this->info['start_time'] = $this->info['start_time'] ?? microtime(true);
($this->onProgress)();
return new Success();
}
public function startDnsResolution(Request $request): Promise
{
($this->onProgress)();
return new Success();
}
public function startConnectionCreation(Request $request): Promise
{
($this->onProgress)();
return new Success();
}
public function startTlsNegotiation(Request $request): Promise
{
($this->onProgress)();
return new Success();
}
public function startSendingRequest(Request $request, Stream $stream): Promise
{
$host = $stream->getRemoteAddress()->getHost();
if (false !== strpos($host, ':')) {
$host = '['.$host.']';
}
$this->info['primary_ip'] = $host;
$this->info['primary_port'] = $stream->getRemoteAddress()->getPort();
$this->info['pretransfer_time'] = microtime(true) - $this->info['start_time'];
$this->info['debug'] .= sprintf("* Connected to %s (%s) port %d\n", $request->getUri()->getHost(), $host, $this->info['primary_port']);
if ((isset($this->info['peer_certificate_chain']) || $this->pinSha256) && null !== $tlsInfo = $stream->getTlsInfo()) {
foreach ($tlsInfo->getPeerCertificates() as $cert) {
$this->info['peer_certificate_chain'][] = openssl_x509_read($cert->toPem());
}
if ($this->pinSha256) {
$pin = openssl_pkey_get_public($this->info['peer_certificate_chain'][0]);
$pin = openssl_pkey_get_details($pin)['key'];
$pin = \array_slice(explode("\n", $pin), 1, -2);
$pin = base64_decode(implode('', $pin));
$pin = base64_encode(hash('sha256', $pin, true));
if (!\in_array($pin, $this->pinSha256, true)) {
throw new TransportException(sprintf('SSL public key does not match pinned public key for "%s".', $this->info['url']));
}
}
}
($this->onProgress)();
$uri = $request->getUri();
$requestUri = $uri->getPath() ?: '/';
if ('' !== $query = $uri->getQuery()) {
$requestUri .= '?'.$query;
}
if ('CONNECT' === $method = $request->getMethod()) {
$requestUri = $uri->getHost().': '.($uri->getPort() ?? ('https' === $uri->getScheme() ? 443 : 80));
}
$this->info['debug'] .= sprintf("> %s %s HTTP/%s \r\n", $method, $requestUri, $request->getProtocolVersions()[0]);
foreach ($request->getRawHeaders() as [$name, $value]) {
$this->info['debug'] .= $name.': '.$value."\r\n";
}
$this->info['debug'] .= "\r\n";
return new Success();
}
public function completeSendingRequest(Request $request, Stream $stream): Promise
{
($this->onProgress)();
return new Success();
}
public function startReceivingResponse(Request $request, Stream $stream): Promise
{
$this->info['starttransfer_time'] = microtime(true) - $this->info['start_time'];
($this->onProgress)();
return new Success();
}
public function completeReceivingResponse(Request $request, Stream $stream): Promise
{
$this->handle = null;
($this->onProgress)();
return new Success();
}
public function completeDnsResolution(Request $request): Promise
{
$this->info['namelookup_time'] = microtime(true) - $this->info['start_time'];
($this->onProgress)();
return new Success();
}
public function completeConnectionCreation(Request $request): Promise
{
$this->info['connect_time'] = microtime(true) - $this->info['start_time'];
($this->onProgress)();
return new Success();
}
public function completeTlsNegotiation(Request $request): Promise
{
($this->onProgress)();
return new Success();
}
public function abort(Request $request, \Throwable $cause): Promise
{
return new Success();
}
}

View file

@ -0,0 +1,52 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\HttpClient\Internal;
use Amp\Dns;
use Amp\Dns\Record;
use Amp\Promise;
use Amp\Success;
/**
* Handles local overrides for the DNS resolver.
*
* @author Nicolas Grekas <p@tchwork.com>
*
* @internal
*/
class AmpResolver implements Dns\Resolver
{
private $dnsMap;
public function __construct(array &$dnsMap)
{
$this->dnsMap = &$dnsMap;
}
public function resolve(string $name, int $typeRestriction = null): Promise
{
if (!isset($this->dnsMap[$name]) || !\in_array($typeRestriction, [Record::A, null], true)) {
return Dns\resolver()->resolve($name, $typeRestriction);
}
return new Success([new Record($this->dnsMap[$name], Record::A, null)]);
}
public function query(string $name, int $type): Promise
{
if (!isset($this->dnsMap[$name]) || Record::A !== $type) {
return Dns\resolver()->query($name, $type);
}
return new Success([new Record($this->dnsMap[$name], Record::A, null)]);
}
}

View file

@ -0,0 +1,40 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\HttpClient\Internal;
/**
* @author Nicolas Grekas <p@tchwork.com>
*
* @internal
*/
final class Canary
{
private $canceller;
public function __construct(\Closure $canceller)
{
$this->canceller = $canceller;
}
public function cancel()
{
if (($canceller = $this->canceller) instanceof \Closure) {
$this->canceller = null;
$canceller();
}
}
public function __destruct()
{
$this->cancel();
}
}

View file

@ -0,0 +1,26 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\HttpClient\Internal;
/**
* Internal representation of the client state.
*
* @author Alexander M. Turek <me@derrabus.de>
*
* @internal
*/
class ClientState
{
public $handlesActivity = [];
public $openHandles = [];
public $lastTimeout;
}

View file

@ -0,0 +1,85 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\HttpClient\Internal;
use Psr\Log\LoggerInterface;
/**
* Internal representation of the cURL client's state.
*
* @author Alexander M. Turek <me@derrabus.de>
*
* @internal
*/
final class CurlClientState extends ClientState
{
/** @var \CurlMultiHandle|resource */
public $handle;
/** @var PushedResponse[] */
public $pushedResponses = [];
/** @var DnsCache */
public $dnsCache;
/** @var float[] */
public $pauseExpiries = [];
public $execCounter = \PHP_INT_MIN;
/** @var LoggerInterface|null */
public $logger;
public function __construct()
{
$this->handle = curl_multi_init();
$this->dnsCache = new DnsCache();
}
public function reset()
{
if ($this->logger) {
foreach ($this->pushedResponses as $url => $response) {
$this->logger->debug(sprintf('Unused pushed response: "%s"', $url));
}
}
$this->pushedResponses = [];
$this->dnsCache->evictions = $this->dnsCache->evictions ?: $this->dnsCache->removals;
$this->dnsCache->removals = $this->dnsCache->hostnames = [];
if (\is_resource($this->handle) || $this->handle instanceof \CurlMultiHandle) {
if (\defined('CURLMOPT_PUSHFUNCTION')) {
curl_multi_setopt($this->handle, \CURLMOPT_PUSHFUNCTION, null);
}
$active = 0;
while (\CURLM_CALL_MULTI_PERFORM === curl_multi_exec($this->handle, $active));
}
foreach ($this->openHandles as [$ch]) {
if (\is_resource($ch) || $ch instanceof \CurlHandle) {
curl_setopt($ch, \CURLOPT_VERBOSE, false);
}
}
}
public function __sleep(): array
{
throw new \BadMethodCallException('Cannot serialize '.__CLASS__);
}
public function __wakeup()
{
throw new \BadMethodCallException('Cannot unserialize '.__CLASS__);
}
public function __destruct()
{
$this->reset();
}
}

View file

@ -0,0 +1,39 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\HttpClient\Internal;
/**
* Cache for resolved DNS queries.
*
* @author Alexander M. Turek <me@derrabus.de>
*
* @internal
*/
final class DnsCache
{
/**
* Resolved hostnames (hostname => IP address).
*
* @var string[]
*/
public $hostnames = [];
/**
* @var string[]
*/
public $removals = [];
/**
* @var string[]
*/
public $evictions = [];
}

View file

@ -0,0 +1,136 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\HttpClient\Internal;
use Http\Client\Exception\NetworkException;
use Psr\Http\Message\ResponseFactoryInterface;
use Psr\Http\Message\ResponseInterface as Psr7ResponseInterface;
use Psr\Http\Message\StreamFactoryInterface;
use Symfony\Component\HttpClient\Response\StreamableInterface;
use Symfony\Component\HttpClient\Response\StreamWrapper;
use Symfony\Contracts\HttpClient\Exception\TransportExceptionInterface;
use Symfony\Contracts\HttpClient\HttpClientInterface;
use Symfony\Contracts\HttpClient\ResponseInterface;
/**
* @author Nicolas Grekas <p@tchwork.com>
*
* @internal
*/
final class HttplugWaitLoop
{
private $client;
private $promisePool;
private $responseFactory;
private $streamFactory;
public function __construct(HttpClientInterface $client, ?\SplObjectStorage $promisePool, ResponseFactoryInterface $responseFactory, StreamFactoryInterface $streamFactory)
{
$this->client = $client;
$this->promisePool = $promisePool;
$this->responseFactory = $responseFactory;
$this->streamFactory = $streamFactory;
}
public function wait(?ResponseInterface $pendingResponse, float $maxDuration = null, float $idleTimeout = null): int
{
if (!$this->promisePool) {
return 0;
}
$guzzleQueue = \GuzzleHttp\Promise\queue();
if (0.0 === $remainingDuration = $maxDuration) {
$idleTimeout = 0.0;
} elseif (null !== $maxDuration) {
$startTime = microtime(true);
$idleTimeout = max(0.0, min($maxDuration / 5, $idleTimeout ?? $maxDuration));
}
do {
foreach ($this->client->stream($this->promisePool, $idleTimeout) as $response => $chunk) {
try {
if (null !== $maxDuration && $chunk->isTimeout()) {
goto check_duration;
}
if ($chunk->isFirst()) {
// Deactivate throwing on 3/4/5xx
$response->getStatusCode();
}
if (!$chunk->isLast()) {
goto check_duration;
}
if ([, $promise] = $this->promisePool[$response] ?? null) {
unset($this->promisePool[$response]);
$promise->resolve($this->createPsr7Response($response, true));
}
} catch (\Exception $e) {
if ([$request, $promise] = $this->promisePool[$response] ?? null) {
unset($this->promisePool[$response]);
if ($e instanceof TransportExceptionInterface) {
$e = new NetworkException($e->getMessage(), $request, $e);
}
$promise->reject($e);
}
}
$guzzleQueue->run();
if ($pendingResponse === $response) {
return $this->promisePool->count();
}
check_duration:
if (null !== $maxDuration && $idleTimeout && $idleTimeout > $remainingDuration = max(0.0, $maxDuration - microtime(true) + $startTime)) {
$idleTimeout = $remainingDuration / 5;
break;
}
}
if (!$count = $this->promisePool->count()) {
return 0;
}
} while (null === $maxDuration || 0 < $remainingDuration);
return $count;
}
public function createPsr7Response(ResponseInterface $response, bool $buffer = false): Psr7ResponseInterface
{
$psrResponse = $this->responseFactory->createResponse($response->getStatusCode());
foreach ($response->getHeaders(false) as $name => $values) {
foreach ($values as $value) {
$psrResponse = $psrResponse->withAddedHeader($name, $value);
}
}
if ($response instanceof StreamableInterface) {
$body = $this->streamFactory->createStreamFromResource($response->toStream(false));
} elseif (!$buffer) {
$body = $this->streamFactory->createStreamFromResource(StreamWrapper::createResource($response, $this->client));
} else {
$body = $this->streamFactory->createStream($response->getContent(false));
}
if ($body->isSeekable()) {
$body->seek(0);
}
return $psrResponse->withBody($body);
}
}

View file

@ -0,0 +1,40 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\HttpClient\Internal;
/**
* Internal representation of the native client's state.
*
* @author Alexander M. Turek <me@derrabus.de>
*
* @internal
*/
final class NativeClientState extends ClientState
{
/** @var int */
public $id;
/** @var int */
public $maxHostConnections = \PHP_INT_MAX;
/** @var int */
public $responseCount = 0;
/** @var string[] */
public $dnsCache = [];
/** @var bool */
public $sleep = false;
/** @var int[] */
public $hosts = [];
public function __construct()
{
$this->id = random_int(\PHP_INT_MIN, \PHP_INT_MAX);
}
}

View file

@ -0,0 +1,41 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\HttpClient\Internal;
use Symfony\Component\HttpClient\Response\CurlResponse;
/**
* A pushed response with its request headers.
*
* @author Alexander M. Turek <me@derrabus.de>
*
* @internal
*/
final class PushedResponse
{
public $response;
/** @var string[] */
public $requestHeaders;
public $parentOptions = [];
public $handle;
public function __construct(CurlResponse $response, array $requestHeaders, array $parentOptions, $handle)
{
$this->response = $response;
$this->requestHeaders = $requestHeaders;
$this->parentOptions = $parentOptions;
$this->handle = $handle;
}
}

19
vendor/symfony/http-client/LICENSE vendored Normal file
View file

@ -0,0 +1,19 @@
Copyright (c) 2018-2021 Fabien Potencier
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,110 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\HttpClient;
use Symfony\Component\HttpClient\Exception\TransportException;
use Symfony\Component\HttpClient\Response\MockResponse;
use Symfony\Component\HttpClient\Response\ResponseStream;
use Symfony\Contracts\HttpClient\HttpClientInterface;
use Symfony\Contracts\HttpClient\ResponseInterface;
use Symfony\Contracts\HttpClient\ResponseStreamInterface;
/**
* A test-friendly HttpClient that doesn't make actual HTTP requests.
*
* @author Nicolas Grekas <p@tchwork.com>
*/
class MockHttpClient implements HttpClientInterface
{
use HttpClientTrait;
private $responseFactory;
private $requestsCount = 0;
private $defaultOptions = [];
/**
* @param callable|callable[]|ResponseInterface|ResponseInterface[]|iterable|null $responseFactory
*/
public function __construct($responseFactory = null, string $baseUri = null)
{
if ($responseFactory instanceof ResponseInterface) {
$responseFactory = [$responseFactory];
}
if (!$responseFactory instanceof \Iterator && null !== $responseFactory && !\is_callable($responseFactory)) {
$responseFactory = (static function () use ($responseFactory) {
yield from $responseFactory;
})();
}
$this->responseFactory = $responseFactory;
$this->defaultOptions['base_uri'] = $baseUri;
}
/**
* {@inheritdoc}
*/
public function request(string $method, string $url, array $options = []): ResponseInterface
{
[$url, $options] = $this->prepareRequest($method, $url, $options, $this->defaultOptions, true);
$url = implode('', $url);
if (null === $this->responseFactory) {
$response = new MockResponse();
} elseif (\is_callable($this->responseFactory)) {
$response = ($this->responseFactory)($method, $url, $options);
} elseif (!$this->responseFactory->valid()) {
throw new TransportException('The response factory iterator passed to MockHttpClient is empty.');
} else {
$responseFactory = $this->responseFactory->current();
$response = \is_callable($responseFactory) ? $responseFactory($method, $url, $options) : $responseFactory;
$this->responseFactory->next();
}
++$this->requestsCount;
if (!$response instanceof ResponseInterface) {
throw new TransportException(sprintf('The response factory passed to MockHttpClient must return/yield an instance of ResponseInterface, "%s" given.', \is_object($response) ? \get_class($response) : \gettype($response)));
}
return MockResponse::fromRequest($method, $url, $options, $response);
}
/**
* {@inheritdoc}
*/
public function stream($responses, float $timeout = null): ResponseStreamInterface
{
if ($responses instanceof ResponseInterface) {
$responses = [$responses];
} elseif (!is_iterable($responses)) {
throw new \TypeError(sprintf('"%s()" expects parameter 1 to be an iterable of MockResponse objects, "%s" given.', __METHOD__, get_debug_type($responses)));
}
return new ResponseStream(MockResponse::stream($responses, $timeout));
}
public function getRequestsCount(): int
{
return $this->requestsCount;
}
/**
* {@inheritdoc}
*/
public function withOptions(array $options): self
{
$clone = clone $this;
$clone->defaultOptions = self::mergeDefaultOptions($options, $this->defaultOptions, true);
return $clone;
}
}

View file

@ -0,0 +1,447 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\HttpClient;
use Psr\Log\LoggerAwareInterface;
use Psr\Log\LoggerAwareTrait;
use Symfony\Component\HttpClient\Exception\InvalidArgumentException;
use Symfony\Component\HttpClient\Exception\TransportException;
use Symfony\Component\HttpClient\Internal\NativeClientState;
use Symfony\Component\HttpClient\Response\NativeResponse;
use Symfony\Component\HttpClient\Response\ResponseStream;
use Symfony\Contracts\HttpClient\HttpClientInterface;
use Symfony\Contracts\HttpClient\ResponseInterface;
use Symfony\Contracts\HttpClient\ResponseStreamInterface;
/**
* A portable implementation of the HttpClientInterface contracts based on PHP stream wrappers.
*
* PHP stream wrappers are able to fetch response bodies concurrently,
* but each request is opened synchronously.
*
* @author Nicolas Grekas <p@tchwork.com>
*/
final class NativeHttpClient implements HttpClientInterface, LoggerAwareInterface
{
use HttpClientTrait;
use LoggerAwareTrait;
private $defaultOptions = self::OPTIONS_DEFAULTS;
/** @var NativeClientState */
private $multi;
/**
* @param array $defaultOptions Default request's options
* @param int $maxHostConnections The maximum number of connections to open
*
* @see HttpClientInterface::OPTIONS_DEFAULTS for available options
*/
public function __construct(array $defaultOptions = [], int $maxHostConnections = 6)
{
$this->defaultOptions['buffer'] = $this->defaultOptions['buffer'] ?? \Closure::fromCallable([__CLASS__, 'shouldBuffer']);
if ($defaultOptions) {
[, $this->defaultOptions] = self::prepareRequest(null, null, $defaultOptions, $this->defaultOptions);
}
$this->multi = new NativeClientState();
$this->multi->maxHostConnections = 0 < $maxHostConnections ? $maxHostConnections : \PHP_INT_MAX;
}
/**
* @see HttpClientInterface::OPTIONS_DEFAULTS for available options
*
* {@inheritdoc}
*/
public function request(string $method, string $url, array $options = []): ResponseInterface
{
[$url, $options] = self::prepareRequest($method, $url, $options, $this->defaultOptions);
if ($options['bindto']) {
if (file_exists($options['bindto'])) {
throw new TransportException(__CLASS__.' cannot bind to local Unix sockets, use e.g. CurlHttpClient instead.');
}
if (str_starts_with($options['bindto'], 'if!')) {
throw new TransportException(__CLASS__.' cannot bind to network interfaces, use e.g. CurlHttpClient instead.');
}
if (str_starts_with($options['bindto'], 'host!')) {
$options['bindto'] = substr($options['bindto'], 5);
}
}
$options['body'] = self::getBodyAsString($options['body']);
if ('' !== $options['body'] && 'POST' === $method && !isset($options['normalized_headers']['content-type'])) {
$options['headers'][] = 'Content-Type: application/x-www-form-urlencoded';
}
if (\extension_loaded('zlib') && !isset($options['normalized_headers']['accept-encoding'])) {
// gzip is the most widely available algo, no need to deal with deflate
$options['headers'][] = 'Accept-Encoding: gzip';
}
if ($options['peer_fingerprint']) {
if (isset($options['peer_fingerprint']['pin-sha256']) && 1 === \count($options['peer_fingerprint'])) {
throw new TransportException(__CLASS__.' cannot verify "pin-sha256" fingerprints, please provide a "sha256" one.');
}
unset($options['peer_fingerprint']['pin-sha256']);
}
$info = [
'response_headers' => [],
'url' => $url,
'error' => null,
'canceled' => false,
'http_method' => $method,
'http_code' => 0,
'redirect_count' => 0,
'start_time' => 0.0,
'connect_time' => 0.0,
'redirect_time' => 0.0,
'pretransfer_time' => 0.0,
'starttransfer_time' => 0.0,
'total_time' => 0.0,
'namelookup_time' => 0.0,
'size_upload' => 0,
'size_download' => 0,
'size_body' => \strlen($options['body']),
'primary_ip' => '',
'primary_port' => 'http:' === $url['scheme'] ? 80 : 443,
'debug' => \extension_loaded('curl') ? '' : "* Enable the curl extension for better performance\n",
];
if ($onProgress = $options['on_progress']) {
// Memoize the last progress to ease calling the callback periodically when no network transfer happens
$lastProgress = [0, 0];
$maxDuration = 0 < $options['max_duration'] ? $options['max_duration'] : \INF;
$onProgress = static function (...$progress) use ($onProgress, &$lastProgress, &$info, $maxDuration) {
if ($info['total_time'] >= $maxDuration) {
throw new TransportException(sprintf('Max duration was reached for "%s".', implode('', $info['url'])));
}
$progressInfo = $info;
$progressInfo['url'] = implode('', $info['url']);
unset($progressInfo['size_body']);
if ($progress && -1 === $progress[0]) {
// Response completed
$lastProgress[0] = max($lastProgress);
} else {
$lastProgress = $progress ?: $lastProgress;
}
$onProgress($lastProgress[0], $lastProgress[1], $progressInfo);
};
} elseif (0 < $options['max_duration']) {
$maxDuration = $options['max_duration'];
$onProgress = static function () use (&$info, $maxDuration): void {
if ($info['total_time'] >= $maxDuration) {
throw new TransportException(sprintf('Max duration was reached for "%s".', implode('', $info['url'])));
}
};
}
// Always register a notification callback to compute live stats about the response
$notification = static function (int $code, int $severity, ?string $msg, int $msgCode, int $dlNow, int $dlSize) use ($onProgress, &$info) {
$info['total_time'] = microtime(true) - $info['start_time'];
if (\STREAM_NOTIFY_PROGRESS === $code) {
$info['starttransfer_time'] = $info['starttransfer_time'] ?: $info['total_time'];
$info['size_upload'] += $dlNow ? 0 : $info['size_body'];
$info['size_download'] = $dlNow;
} elseif (\STREAM_NOTIFY_CONNECT === $code) {
$info['connect_time'] = $info['total_time'];
$info['debug'] .= $info['request_header'];
unset($info['request_header']);
} else {
return;
}
if ($onProgress) {
$onProgress($dlNow, $dlSize);
}
};
if ($options['resolve']) {
$this->multi->dnsCache = $options['resolve'] + $this->multi->dnsCache;
}
$this->logger && $this->logger->info(sprintf('Request: "%s %s"', $method, implode('', $url)));
if (!isset($options['normalized_headers']['user-agent'])) {
$options['headers'][] = 'User-Agent: Symfony HttpClient/Native';
}
if (0 < $options['max_duration']) {
$options['timeout'] = min($options['max_duration'], $options['timeout']);
}
$bindto = $options['bindto'];
if (!$bindto && (70322 === \PHP_VERSION_ID || 70410 === \PHP_VERSION_ID)) {
$bindto = '0:0';
}
$context = [
'http' => [
'protocol_version' => min($options['http_version'] ?: '1.1', '1.1'),
'method' => $method,
'content' => $options['body'],
'ignore_errors' => true,
'curl_verify_ssl_peer' => $options['verify_peer'],
'curl_verify_ssl_host' => $options['verify_host'],
'auto_decode' => false, // Disable dechunk filter, it's incompatible with stream_select()
'timeout' => $options['timeout'],
'follow_location' => false, // We follow redirects ourselves - the native logic is too limited
],
'ssl' => array_filter([
'verify_peer' => $options['verify_peer'],
'verify_peer_name' => $options['verify_host'],
'cafile' => $options['cafile'],
'capath' => $options['capath'],
'local_cert' => $options['local_cert'],
'local_pk' => $options['local_pk'],
'passphrase' => $options['passphrase'],
'ciphers' => $options['ciphers'],
'peer_fingerprint' => $options['peer_fingerprint'],
'capture_peer_cert_chain' => $options['capture_peer_cert_chain'],
'allow_self_signed' => (bool) $options['peer_fingerprint'],
'SNI_enabled' => true,
'disable_compression' => true,
], static function ($v) { return null !== $v; }),
'socket' => [
'bindto' => $bindto,
'tcp_nodelay' => true,
],
];
$context = stream_context_create($context, ['notification' => $notification]);
$resolver = static function ($multi) use ($context, $options, $url, &$info, $onProgress) {
[$host, $port] = self::parseHostPort($url, $info);
if (!isset($options['normalized_headers']['host'])) {
$options['headers'][] = 'Host: '.$host.$port;
}
$proxy = self::getProxy($options['proxy'], $url, $options['no_proxy']);
if (!self::configureHeadersAndProxy($context, $host, $options['headers'], $proxy, 'https:' === $url['scheme'])) {
$ip = self::dnsResolve($host, $multi, $info, $onProgress);
$url['authority'] = substr_replace($url['authority'], $ip, -\strlen($host) - \strlen($port), \strlen($host));
}
return [self::createRedirectResolver($options, $host, $proxy, $info, $onProgress), implode('', $url)];
};
return new NativeResponse($this->multi, $context, implode('', $url), $options, $info, $resolver, $onProgress, $this->logger);
}
/**
* {@inheritdoc}
*/
public function stream($responses, float $timeout = null): ResponseStreamInterface
{
if ($responses instanceof NativeResponse) {
$responses = [$responses];
} elseif (!is_iterable($responses)) {
throw new \TypeError(sprintf('"%s()" expects parameter 1 to be an iterable of NativeResponse objects, "%s" given.', __METHOD__, get_debug_type($responses)));
}
return new ResponseStream(NativeResponse::stream($responses, $timeout));
}
private static function getBodyAsString($body): string
{
if (\is_resource($body)) {
return stream_get_contents($body);
}
if (!$body instanceof \Closure) {
return $body;
}
$result = '';
while ('' !== $data = $body(self::$CHUNK_SIZE)) {
if (!\is_string($data)) {
throw new TransportException(sprintf('Return value of the "body" option callback must be string, "%s" returned.', get_debug_type($data)));
}
$result .= $data;
}
return $result;
}
/**
* Extracts the host and the port from the URL.
*/
private static function parseHostPort(array $url, array &$info): array
{
if ($port = parse_url($url['authority'], \PHP_URL_PORT) ?: '') {
$info['primary_port'] = $port;
$port = ':'.$port;
} else {
$info['primary_port'] = 'http:' === $url['scheme'] ? 80 : 443;
}
return [parse_url($url['authority'], \PHP_URL_HOST), $port];
}
/**
* Resolves the IP of the host using the local DNS cache if possible.
*/
private static function dnsResolve($host, NativeClientState $multi, array &$info, ?\Closure $onProgress): string
{
if (null === $ip = $multi->dnsCache[$host] ?? null) {
$info['debug'] .= "* Hostname was NOT found in DNS cache\n";
$now = microtime(true);
if (!$ip = gethostbynamel($host)) {
throw new TransportException(sprintf('Could not resolve host "%s".', $host));
}
$info['namelookup_time'] = microtime(true) - ($info['start_time'] ?: $now);
$multi->dnsCache[$host] = $ip = $ip[0];
$info['debug'] .= "* Added {$host}:0:{$ip} to DNS cache\n";
} else {
$info['debug'] .= "* Hostname was found in DNS cache\n";
}
$info['primary_ip'] = $ip;
if ($onProgress) {
// Notify DNS resolution
$onProgress();
}
return $ip;
}
/**
* Handles redirects - the native logic is too buggy to be used.
*/
private static function createRedirectResolver(array $options, string $host, ?array $proxy, array &$info, ?\Closure $onProgress): \Closure
{
$redirectHeaders = [];
if (0 < $maxRedirects = $options['max_redirects']) {
$redirectHeaders = ['host' => $host];
$redirectHeaders['with_auth'] = $redirectHeaders['no_auth'] = array_filter($options['headers'], static function ($h) {
return 0 !== stripos($h, 'Host:');
});
if (isset($options['normalized_headers']['authorization']) || isset($options['normalized_headers']['cookie'])) {
$redirectHeaders['no_auth'] = array_filter($redirectHeaders['no_auth'], static function ($h) {
return 0 !== stripos($h, 'Authorization:') && 0 !== stripos($h, 'Cookie:');
});
}
}
return static function (NativeClientState $multi, ?string $location, $context) use ($redirectHeaders, $proxy, &$info, $maxRedirects, $onProgress): ?string {
if (null === $location || $info['http_code'] < 300 || 400 <= $info['http_code']) {
$info['redirect_url'] = null;
return null;
}
try {
$url = self::parseUrl($location);
} catch (InvalidArgumentException $e) {
$info['redirect_url'] = null;
return null;
}
$url = self::resolveUrl($url, $info['url']);
$info['redirect_url'] = implode('', $url);
if ($info['redirect_count'] >= $maxRedirects) {
return null;
}
$info['url'] = $url;
++$info['redirect_count'];
$info['redirect_time'] = microtime(true) - $info['start_time'];
// Do like curl and browsers: turn POST to GET on 301, 302 and 303
if (\in_array($info['http_code'], [301, 302, 303], true)) {
$options = stream_context_get_options($context)['http'];
if ('POST' === $options['method'] || 303 === $info['http_code']) {
$info['http_method'] = $options['method'] = 'HEAD' === $options['method'] ? 'HEAD' : 'GET';
$options['content'] = '';
$options['header'] = array_filter($options['header'], static function ($h) {
return 0 !== stripos($h, 'Content-Length:') && 0 !== stripos($h, 'Content-Type:');
});
stream_context_set_option($context, ['http' => $options]);
}
}
[$host, $port] = self::parseHostPort($url, $info);
if (false !== (parse_url($location, \PHP_URL_HOST) ?? false)) {
// Authorization and Cookie headers MUST NOT follow except for the initial host name
$requestHeaders = $redirectHeaders['host'] === $host ? $redirectHeaders['with_auth'] : $redirectHeaders['no_auth'];
$requestHeaders[] = 'Host: '.$host.$port;
$dnsResolve = !self::configureHeadersAndProxy($context, $host, $requestHeaders, $proxy, 'https:' === $url['scheme']);
} else {
$dnsResolve = isset(stream_context_get_options($context)['ssl']['peer_name']);
}
if ($dnsResolve) {
$ip = self::dnsResolve($host, $multi, $info, $onProgress);
$url['authority'] = substr_replace($url['authority'], $ip, -\strlen($host) - \strlen($port), \strlen($host));
}
return implode('', $url);
};
}
private static function configureHeadersAndProxy($context, string $host, array $requestHeaders, ?array $proxy, bool $isSsl): bool
{
if (null === $proxy) {
stream_context_set_option($context, 'http', 'header', $requestHeaders);
stream_context_set_option($context, 'ssl', 'peer_name', $host);
return false;
}
// Matching "no_proxy" should follow the behavior of curl
foreach ($proxy['no_proxy'] as $rule) {
$dotRule = '.'.ltrim($rule, '.');
if ('*' === $rule || $host === $rule || str_ends_with($host, $dotRule)) {
stream_context_set_option($context, 'http', 'proxy', null);
stream_context_set_option($context, 'http', 'request_fulluri', false);
stream_context_set_option($context, 'http', 'header', $requestHeaders);
stream_context_set_option($context, 'ssl', 'peer_name', $host);
return false;
}
}
if (null !== $proxy['auth']) {
$requestHeaders[] = 'Proxy-Authorization: '.$proxy['auth'];
}
stream_context_set_option($context, 'http', 'proxy', $proxy['url']);
stream_context_set_option($context, 'http', 'request_fulluri', !$isSsl);
stream_context_set_option($context, 'http', 'header', $requestHeaders);
stream_context_set_option($context, 'ssl', 'peer_name', null);
return true;
}
}

View file

@ -0,0 +1,124 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\HttpClient;
use Psr\Log\LoggerAwareInterface;
use Psr\Log\LoggerInterface;
use Symfony\Component\HttpClient\Exception\InvalidArgumentException;
use Symfony\Component\HttpClient\Exception\TransportException;
use Symfony\Component\HttpFoundation\IpUtils;
use Symfony\Contracts\HttpClient\HttpClientInterface;
use Symfony\Contracts\HttpClient\ResponseInterface;
use Symfony\Contracts\HttpClient\ResponseStreamInterface;
/**
* Decorator that blocks requests to private networks by default.
*
* @author Hallison Boaventura <hallisonboaventura@gmail.com>
*/
final class NoPrivateNetworkHttpClient implements HttpClientInterface, LoggerAwareInterface
{
use HttpClientTrait;
private const PRIVATE_SUBNETS = [
'127.0.0.0/8',
'10.0.0.0/8',
'192.168.0.0/16',
'172.16.0.0/12',
'169.254.0.0/16',
'0.0.0.0/8',
'240.0.0.0/4',
'::1/128',
'fc00::/7',
'fe80::/10',
'::ffff:0:0/96',
'::/128',
];
private $client;
private $subnets;
/**
* @param string|array|null $subnets String or array of subnets using CIDR notation that will be used by IpUtils.
* If null is passed, the standard private subnets will be used.
*/
public function __construct(HttpClientInterface $client, $subnets = null)
{
if (!(\is_array($subnets) || \is_string($subnets) || null === $subnets)) {
throw new \TypeError(sprintf('Argument 2 passed to "%s()" must be of the type array, string or null. "%s" given.', __METHOD__, get_debug_type($subnets)));
}
if (!class_exists(IpUtils::class)) {
throw new \LogicException(sprintf('You can not use "%s" if the HttpFoundation component is not installed. Try running "composer require symfony/http-foundation".', __CLASS__));
}
$this->client = $client;
$this->subnets = $subnets;
}
/**
* {@inheritdoc}
*/
public function request(string $method, string $url, array $options = []): ResponseInterface
{
$onProgress = $options['on_progress'] ?? null;
if (null !== $onProgress && !\is_callable($onProgress)) {
throw new InvalidArgumentException(sprintf('Option "on_progress" must be callable, "%s" given.', get_debug_type($onProgress)));
}
$subnets = $this->subnets;
$lastPrimaryIp = '';
$options['on_progress'] = function (int $dlNow, int $dlSize, array $info) use ($onProgress, $subnets, &$lastPrimaryIp): void {
if ($info['primary_ip'] !== $lastPrimaryIp) {
if (IpUtils::checkIp($info['primary_ip'], $subnets ?? self::PRIVATE_SUBNETS)) {
throw new TransportException(sprintf('IP "%s" is blocked for "%s".', $info['primary_ip'], $info['url']));
}
$lastPrimaryIp = $info['primary_ip'];
}
null !== $onProgress && $onProgress($dlNow, $dlSize, $info);
};
return $this->client->request($method, $url, $options);
}
/**
* {@inheritdoc}
*/
public function stream($responses, float $timeout = null): ResponseStreamInterface
{
return $this->client->stream($responses, $timeout);
}
/**
* {@inheritdoc}
*/
public function setLogger(LoggerInterface $logger): void
{
if ($this->client instanceof LoggerAwareInterface) {
$this->client->setLogger($logger);
}
}
/**
* {@inheritdoc}
*/
public function withOptions(array $options): self
{
$clone = clone $this;
$clone->client = $this->client->withOptions($options);
return $clone;
}
}

View file

@ -0,0 +1,231 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\HttpClient;
use Http\Discovery\Exception\NotFoundException;
use Http\Discovery\Psr17FactoryDiscovery;
use Nyholm\Psr7\Factory\Psr17Factory;
use Nyholm\Psr7\Request;
use Nyholm\Psr7\Uri;
use Psr\Http\Client\ClientInterface;
use Psr\Http\Client\NetworkExceptionInterface;
use Psr\Http\Client\RequestExceptionInterface;
use Psr\Http\Message\RequestFactoryInterface;
use Psr\Http\Message\RequestInterface;
use Psr\Http\Message\ResponseFactoryInterface;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\StreamFactoryInterface;
use Psr\Http\Message\StreamInterface;
use Psr\Http\Message\UriFactoryInterface;
use Psr\Http\Message\UriInterface;
use Symfony\Component\HttpClient\Response\StreamableInterface;
use Symfony\Component\HttpClient\Response\StreamWrapper;
use Symfony\Contracts\HttpClient\Exception\TransportExceptionInterface;
use Symfony\Contracts\HttpClient\HttpClientInterface;
if (!interface_exists(RequestFactoryInterface::class)) {
throw new \LogicException('You cannot use the "Symfony\Component\HttpClient\Psr18Client" as the "psr/http-factory" package is not installed. Try running "composer require nyholm/psr7".');
}
if (!interface_exists(ClientInterface::class)) {
throw new \LogicException('You cannot use the "Symfony\Component\HttpClient\Psr18Client" as the "psr/http-client" package is not installed. Try running "composer require psr/http-client".');
}
/**
* An adapter to turn a Symfony HttpClientInterface into a PSR-18 ClientInterface.
*
* Run "composer require psr/http-client" to install the base ClientInterface. Run
* "composer require nyholm/psr7" to install an efficient implementation of response
* and stream factories with flex-provided autowiring aliases.
*
* @author Nicolas Grekas <p@tchwork.com>
*/
final class Psr18Client implements ClientInterface, RequestFactoryInterface, StreamFactoryInterface, UriFactoryInterface
{
private $client;
private $responseFactory;
private $streamFactory;
public function __construct(HttpClientInterface $client = null, ResponseFactoryInterface $responseFactory = null, StreamFactoryInterface $streamFactory = null)
{
$this->client = $client ?? HttpClient::create();
$this->responseFactory = $responseFactory;
$this->streamFactory = $streamFactory ?? ($responseFactory instanceof StreamFactoryInterface ? $responseFactory : null);
if (null !== $this->responseFactory && null !== $this->streamFactory) {
return;
}
if (!class_exists(Psr17Factory::class) && !class_exists(Psr17FactoryDiscovery::class)) {
throw new \LogicException('You cannot use the "Symfony\Component\HttpClient\Psr18Client" as no PSR-17 factories have been provided. Try running "composer require nyholm/psr7".');
}
try {
$psr17Factory = class_exists(Psr17Factory::class, false) ? new Psr17Factory() : null;
$this->responseFactory = $this->responseFactory ?? $psr17Factory ?? Psr17FactoryDiscovery::findResponseFactory();
$this->streamFactory = $this->streamFactory ?? $psr17Factory ?? Psr17FactoryDiscovery::findStreamFactory();
} catch (NotFoundException $e) {
throw new \LogicException('You cannot use the "Symfony\Component\HttpClient\HttplugClient" as no PSR-17 factories have been found. Try running "composer require nyholm/psr7".', 0, $e);
}
}
/**
* {@inheritdoc}
*/
public function sendRequest(RequestInterface $request): ResponseInterface
{
try {
$body = $request->getBody();
if ($body->isSeekable()) {
$body->seek(0);
}
$response = $this->client->request($request->getMethod(), (string) $request->getUri(), [
'headers' => $request->getHeaders(),
'body' => $body->getContents(),
'http_version' => '1.0' === $request->getProtocolVersion() ? '1.0' : null,
]);
$psrResponse = $this->responseFactory->createResponse($response->getStatusCode());
foreach ($response->getHeaders(false) as $name => $values) {
foreach ($values as $value) {
$psrResponse = $psrResponse->withAddedHeader($name, $value);
}
}
$body = $response instanceof StreamableInterface ? $response->toStream(false) : StreamWrapper::createResource($response, $this->client);
$body = $this->streamFactory->createStreamFromResource($body);
if ($body->isSeekable()) {
$body->seek(0);
}
return $psrResponse->withBody($body);
} catch (TransportExceptionInterface $e) {
if ($e instanceof \InvalidArgumentException) {
throw new Psr18RequestException($e, $request);
}
throw new Psr18NetworkException($e, $request);
}
}
/**
* {@inheritdoc}
*/
public function createRequest(string $method, $uri): RequestInterface
{
if ($this->responseFactory instanceof RequestFactoryInterface) {
return $this->responseFactory->createRequest($method, $uri);
}
if (class_exists(Request::class)) {
return new Request($method, $uri);
}
if (class_exists(Psr17FactoryDiscovery::class)) {
return Psr17FactoryDiscovery::findRequestFactory()->createRequest($method, $uri);
}
throw new \LogicException(sprintf('You cannot use "%s()" as the "nyholm/psr7" package is not installed. Try running "composer require nyholm/psr7".', __METHOD__));
}
/**
* {@inheritdoc}
*/
public function createStream(string $content = ''): StreamInterface
{
$stream = $this->streamFactory->createStream($content);
if ($stream->isSeekable()) {
$stream->seek(0);
}
return $stream;
}
/**
* {@inheritdoc}
*/
public function createStreamFromFile(string $filename, string $mode = 'r'): StreamInterface
{
return $this->streamFactory->createStreamFromFile($filename, $mode);
}
/**
* {@inheritdoc}
*/
public function createStreamFromResource($resource): StreamInterface
{
return $this->streamFactory->createStreamFromResource($resource);
}
/**
* {@inheritdoc}
*/
public function createUri(string $uri = ''): UriInterface
{
if ($this->responseFactory instanceof UriFactoryInterface) {
return $this->responseFactory->createUri($uri);
}
if (class_exists(Uri::class)) {
return new Uri($uri);
}
if (class_exists(Psr17FactoryDiscovery::class)) {
return Psr17FactoryDiscovery::findUrlFactory()->createUri($uri);
}
throw new \LogicException(sprintf('You cannot use "%s()" as the "nyholm/psr7" package is not installed. Try running "composer require nyholm/psr7".', __METHOD__));
}
}
/**
* @internal
*/
class Psr18NetworkException extends \RuntimeException implements NetworkExceptionInterface
{
private $request;
public function __construct(TransportExceptionInterface $e, RequestInterface $request)
{
parent::__construct($e->getMessage(), 0, $e);
$this->request = $request;
}
public function getRequest(): RequestInterface
{
return $this->request;
}
}
/**
* @internal
*/
class Psr18RequestException extends \InvalidArgumentException implements RequestExceptionInterface
{
private $request;
public function __construct(TransportExceptionInterface $e, RequestInterface $request)
{
parent::__construct($e->getMessage(), 0, $e);
$this->request = $request;
}
public function getRequest(): RequestInterface
{
return $this->request;
}
}

13
vendor/symfony/http-client/README.md vendored Normal file
View file

@ -0,0 +1,13 @@
HttpClient component
====================
The HttpClient component provides powerful methods to fetch HTTP resources synchronously or asynchronously.
Resources
---------
* [Documentation](https://symfony.com/doc/current/components/http_client.html)
* [Contributing](https://symfony.com/doc/current/contributing/index.html)
* [Report issues](https://github.com/symfony/symfony/issues) and
[send Pull Requests](https://github.com/symfony/symfony/pulls)
in the [main Symfony repository](https://github.com/symfony/symfony)

View file

@ -0,0 +1,462 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\HttpClient\Response;
use Amp\ByteStream\StreamException;
use Amp\CancellationTokenSource;
use Amp\Coroutine;
use Amp\Deferred;
use Amp\Http\Client\HttpException;
use Amp\Http\Client\Request;
use Amp\Http\Client\Response;
use Amp\Loop;
use Amp\Promise;
use Amp\Success;
use Psr\Log\LoggerInterface;
use Symfony\Component\HttpClient\Chunk\FirstChunk;
use Symfony\Component\HttpClient\Chunk\InformationalChunk;
use Symfony\Component\HttpClient\Exception\InvalidArgumentException;
use Symfony\Component\HttpClient\Exception\TransportException;
use Symfony\Component\HttpClient\HttpClientTrait;
use Symfony\Component\HttpClient\Internal\AmpBody;
use Symfony\Component\HttpClient\Internal\AmpClientState;
use Symfony\Component\HttpClient\Internal\Canary;
use Symfony\Component\HttpClient\Internal\ClientState;
use Symfony\Contracts\HttpClient\ResponseInterface;
/**
* @author Nicolas Grekas <p@tchwork.com>
*
* @internal
*/
final class AmpResponse implements ResponseInterface, StreamableInterface
{
use CommonResponseTrait;
use TransportResponseTrait;
private static $nextId = 'a';
private $multi;
private $options;
private $canceller;
private $onProgress;
private static $delay;
/**
* @internal
*/
public function __construct(AmpClientState $multi, Request $request, array $options, ?LoggerInterface $logger)
{
$this->multi = $multi;
$this->options = &$options;
$this->logger = $logger;
$this->timeout = $options['timeout'];
$this->shouldBuffer = $options['buffer'];
if ($this->inflate = \extension_loaded('zlib') && !$request->hasHeader('accept-encoding')) {
$request->setHeader('Accept-Encoding', 'gzip');
}
$this->initializer = static function (self $response) {
return null !== $response->options;
};
$info = &$this->info;
$headers = &$this->headers;
$canceller = $this->canceller = new CancellationTokenSource();
$handle = &$this->handle;
$info['url'] = (string) $request->getUri();
$info['http_method'] = $request->getMethod();
$info['start_time'] = null;
$info['redirect_url'] = null;
$info['redirect_time'] = 0.0;
$info['redirect_count'] = 0;
$info['size_upload'] = 0.0;
$info['size_download'] = 0.0;
$info['upload_content_length'] = -1.0;
$info['download_content_length'] = -1.0;
$info['user_data'] = $options['user_data'];
$info['debug'] = '';
$onProgress = $options['on_progress'] ?? static function () {};
$onProgress = $this->onProgress = static function () use (&$info, $onProgress) {
$info['total_time'] = microtime(true) - $info['start_time'];
$onProgress((int) $info['size_download'], ((int) (1 + $info['download_content_length']) ?: 1) - 1, (array) $info);
};
$pauseDeferred = new Deferred();
$pause = new Success();
$throttleWatcher = null;
$this->id = $id = self::$nextId++;
Loop::defer(static function () use ($request, $multi, &$id, &$info, &$headers, $canceller, &$options, $onProgress, &$handle, $logger, &$pause) {
return new Coroutine(self::generateResponse($request, $multi, $id, $info, $headers, $canceller, $options, $onProgress, $handle, $logger, $pause));
});
$info['pause_handler'] = static function (float $duration) use (&$throttleWatcher, &$pauseDeferred, &$pause) {
if (null !== $throttleWatcher) {
Loop::cancel($throttleWatcher);
}
$pause = $pauseDeferred->promise();
if ($duration <= 0) {
$deferred = $pauseDeferred;
$pauseDeferred = new Deferred();
$deferred->resolve();
} else {
$throttleWatcher = Loop::delay(ceil(1000 * $duration), static function () use (&$pauseDeferred) {
$deferred = $pauseDeferred;
$pauseDeferred = new Deferred();
$deferred->resolve();
});
}
};
$multi->openHandles[$id] = $id;
++$multi->responseCount;
$this->canary = new Canary(static function () use ($canceller, $multi, $id) {
$canceller->cancel();
unset($multi->openHandles[$id], $multi->handlesActivity[$id]);
});
}
/**
* {@inheritdoc}
*/
public function getInfo(string $type = null)
{
return null !== $type ? $this->info[$type] ?? null : $this->info;
}
/**
* @return array
*/
public function __sleep()
{
throw new \BadMethodCallException('Cannot serialize '.__CLASS__);
}
public function __wakeup()
{
throw new \BadMethodCallException('Cannot unserialize '.__CLASS__);
}
public function __destruct()
{
try {
$this->doDestruct();
} finally {
// Clear the DNS cache when all requests completed
if (0 >= --$this->multi->responseCount) {
$this->multi->responseCount = 0;
$this->multi->dnsCache = [];
}
}
}
/**
* {@inheritdoc}
*/
private static function schedule(self $response, array &$runningResponses): void
{
if (isset($runningResponses[0])) {
$runningResponses[0][1][$response->id] = $response;
} else {
$runningResponses[0] = [$response->multi, [$response->id => $response]];
}
if (!isset($response->multi->openHandles[$response->id])) {
$response->multi->handlesActivity[$response->id][] = null;
$response->multi->handlesActivity[$response->id][] = null !== $response->info['error'] ? new TransportException($response->info['error']) : null;
}
}
/**
* {@inheritdoc}
*
* @param AmpClientState $multi
*/
private static function perform(ClientState $multi, array &$responses = null): void
{
if ($responses) {
foreach ($responses as $response) {
try {
if ($response->info['start_time']) {
$response->info['total_time'] = microtime(true) - $response->info['start_time'];
($response->onProgress)();
}
} catch (\Throwable $e) {
$multi->handlesActivity[$response->id][] = null;
$multi->handlesActivity[$response->id][] = $e;
}
}
}
}
/**
* {@inheritdoc}
*
* @param AmpClientState $multi
*/
private static function select(ClientState $multi, float $timeout): int
{
$timeout += microtime(true);
self::$delay = Loop::defer(static function () use ($timeout) {
if (0 < $timeout -= microtime(true)) {
self::$delay = Loop::delay(ceil(1000 * $timeout), [Loop::class, 'stop']);
} else {
Loop::stop();
}
});
Loop::run();
return null === self::$delay ? 1 : 0;
}
private static function generateResponse(Request $request, AmpClientState $multi, string $id, array &$info, array &$headers, CancellationTokenSource $canceller, array &$options, \Closure $onProgress, &$handle, ?LoggerInterface $logger, Promise &$pause)
{
$request->setInformationalResponseHandler(static function (Response $response) use ($multi, $id, &$info, &$headers) {
self::addResponseHeaders($response, $info, $headers);
$multi->handlesActivity[$id][] = new InformationalChunk($response->getStatus(), $response->getHeaders());
self::stopLoop();
});
try {
/* @var Response $response */
if (null === $response = yield from self::getPushedResponse($request, $multi, $info, $headers, $options, $logger)) {
$logger && $logger->info(sprintf('Request: "%s %s"', $info['http_method'], $info['url']));
$response = yield from self::followRedirects($request, $multi, $info, $headers, $canceller, $options, $onProgress, $handle, $logger, $pause);
}
$options = null;
$multi->handlesActivity[$id][] = new FirstChunk();
if ('HEAD' === $response->getRequest()->getMethod() || \in_array($info['http_code'], [204, 304], true)) {
$multi->handlesActivity[$id][] = null;
$multi->handlesActivity[$id][] = null;
self::stopLoop();
return;
}
if ($response->hasHeader('content-length')) {
$info['download_content_length'] = (float) $response->getHeader('content-length');
}
$body = $response->getBody();
while (true) {
self::stopLoop();
yield $pause;
if (null === $data = yield $body->read()) {
break;
}
$info['size_download'] += \strlen($data);
$multi->handlesActivity[$id][] = $data;
}
$multi->handlesActivity[$id][] = null;
$multi->handlesActivity[$id][] = null;
} catch (\Throwable $e) {
$multi->handlesActivity[$id][] = null;
$multi->handlesActivity[$id][] = $e;
} finally {
$info['download_content_length'] = $info['size_download'];
}
self::stopLoop();
}
private static function followRedirects(Request $originRequest, AmpClientState $multi, array &$info, array &$headers, CancellationTokenSource $canceller, array $options, \Closure $onProgress, &$handle, ?LoggerInterface $logger, Promise &$pause)
{
yield $pause;
$originRequest->setBody(new AmpBody($options['body'], $info, $onProgress));
$response = yield $multi->request($options, $originRequest, $canceller->getToken(), $info, $onProgress, $handle);
$previousUrl = null;
while (true) {
self::addResponseHeaders($response, $info, $headers);
$status = $response->getStatus();
if (!\in_array($status, [301, 302, 303, 307, 308], true) || null === $location = $response->getHeader('location')) {
return $response;
}
$urlResolver = new class() {
use HttpClientTrait {
parseUrl as public;
resolveUrl as public;
}
};
try {
$previousUrl = $previousUrl ?? $urlResolver::parseUrl($info['url']);
$location = $urlResolver::parseUrl($location);
$location = $urlResolver::resolveUrl($location, $previousUrl);
$info['redirect_url'] = implode('', $location);
} catch (InvalidArgumentException $e) {
return $response;
}
if (0 >= $options['max_redirects'] || $info['redirect_count'] >= $options['max_redirects']) {
return $response;
}
$logger && $logger->info(sprintf('Redirecting: "%s %s"', $status, $info['url']));
try {
// Discard body of redirects
while (null !== yield $response->getBody()->read()) {
}
} catch (HttpException | StreamException $e) {
// Ignore streaming errors on previous responses
}
++$info['redirect_count'];
$info['url'] = $info['redirect_url'];
$info['redirect_url'] = null;
$previousUrl = $location;
$request = new Request($info['url'], $info['http_method']);
$request->setProtocolVersions($originRequest->getProtocolVersions());
$request->setTcpConnectTimeout($originRequest->getTcpConnectTimeout());
$request->setTlsHandshakeTimeout($originRequest->getTlsHandshakeTimeout());
$request->setTransferTimeout($originRequest->getTransferTimeout());
if (\in_array($status, [301, 302, 303], true)) {
$originRequest->removeHeader('transfer-encoding');
$originRequest->removeHeader('content-length');
$originRequest->removeHeader('content-type');
// Do like curl and browsers: turn POST to GET on 301, 302 and 303
if ('POST' === $response->getRequest()->getMethod() || 303 === $status) {
$info['http_method'] = 'HEAD' === $response->getRequest()->getMethod() ? 'HEAD' : 'GET';
$request->setMethod($info['http_method']);
}
} else {
$request->setBody(AmpBody::rewind($response->getRequest()->getBody()));
}
foreach ($originRequest->getRawHeaders() as [$name, $value]) {
$request->setHeader($name, $value);
}
if ($request->getUri()->getAuthority() !== $originRequest->getUri()->getAuthority()) {
$request->removeHeader('authorization');
$request->removeHeader('cookie');
$request->removeHeader('host');
}
yield $pause;
$response = yield $multi->request($options, $request, $canceller->getToken(), $info, $onProgress, $handle);
$info['redirect_time'] = microtime(true) - $info['start_time'];
}
}
private static function addResponseHeaders(Response $response, array &$info, array &$headers): void
{
$info['http_code'] = $response->getStatus();
if ($headers) {
$info['debug'] .= "< \r\n";
$headers = [];
}
$h = sprintf('HTTP/%s %s %s', $response->getProtocolVersion(), $response->getStatus(), $response->getReason());
$info['debug'] .= "< {$h}\r\n";
$info['response_headers'][] = $h;
foreach ($response->getRawHeaders() as [$name, $value]) {
$headers[strtolower($name)][] = $value;
$h = $name.': '.$value;
$info['debug'] .= "< {$h}\r\n";
$info['response_headers'][] = $h;
}
$info['debug'] .= "< \r\n";
}
/**
* Accepts pushed responses only if their headers related to authentication match the request.
*/
private static function getPushedResponse(Request $request, AmpClientState $multi, array &$info, array &$headers, array $options, ?LoggerInterface $logger)
{
if ('' !== $options['body']) {
return null;
}
$authority = $request->getUri()->getAuthority();
foreach ($multi->pushedResponses[$authority] ?? [] as $i => [$pushedUrl, $pushDeferred, $pushedRequest, $pushedResponse, $parentOptions]) {
if ($info['url'] !== $pushedUrl || $info['http_method'] !== $pushedRequest->getMethod()) {
continue;
}
foreach ($parentOptions as $k => $v) {
if ($options[$k] !== $v) {
continue 2;
}
}
foreach (['authorization', 'cookie', 'range', 'proxy-authorization'] as $k) {
if ($pushedRequest->getHeaderArray($k) !== $request->getHeaderArray($k)) {
continue 2;
}
}
$response = yield $pushedResponse;
foreach ($response->getHeaderArray('vary') as $vary) {
foreach (preg_split('/\s*+,\s*+/', $vary) as $v) {
if ('*' === $v || ($pushedRequest->getHeaderArray($v) !== $request->getHeaderArray($v) && 'accept-encoding' !== strtolower($v))) {
$logger && $logger->debug(sprintf('Skipping pushed response: "%s"', $info['url']));
continue 3;
}
}
}
$pushDeferred->resolve();
$logger && $logger->debug(sprintf('Accepting pushed response: "%s %s"', $info['http_method'], $info['url']));
self::addResponseHeaders($response, $info, $headers);
unset($multi->pushedResponses[$authority][$i]);
if (!$multi->pushedResponses[$authority]) {
unset($multi->pushedResponses[$authority]);
}
return $response;
}
}
private static function stopLoop(): void
{
if (null !== self::$delay) {
Loop::cancel(self::$delay);
self::$delay = null;
}
Loop::defer([Loop::class, 'stop']);
}
}

View file

@ -0,0 +1,181 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\HttpClient\Response;
use Symfony\Component\HttpClient\Chunk\DataChunk;
use Symfony\Component\HttpClient\Chunk\LastChunk;
use Symfony\Contracts\HttpClient\ChunkInterface;
use Symfony\Contracts\HttpClient\HttpClientInterface;
use Symfony\Contracts\HttpClient\ResponseInterface;
/**
* A DTO to work with AsyncResponse.
*
* @author Nicolas Grekas <p@tchwork.com>
*/
final class AsyncContext
{
private $passthru;
private $client;
private $response;
private $info = [];
private $content;
private $offset;
public function __construct(&$passthru, HttpClientInterface $client, ResponseInterface &$response, array &$info, $content, int $offset)
{
$this->passthru = &$passthru;
$this->client = $client;
$this->response = &$response;
$this->info = &$info;
$this->content = $content;
$this->offset = $offset;
}
/**
* Returns the HTTP status without consuming the response.
*/
public function getStatusCode(): int
{
return $this->response->getInfo('http_code');
}
/**
* Returns the headers without consuming the response.
*/
public function getHeaders(): array
{
$headers = [];
foreach ($this->response->getInfo('response_headers') as $h) {
if (11 <= \strlen($h) && '/' === $h[4] && preg_match('#^HTTP/\d+(?:\.\d+)? ([123456789]\d\d)(?: |$)#', $h, $m)) {
$headers = [];
} elseif (2 === \count($m = explode(':', $h, 2))) {
$headers[strtolower($m[0])][] = ltrim($m[1]);
}
}
return $headers;
}
/**
* @return resource|null The PHP stream resource where the content is buffered, if it is
*/
public function getContent()
{
return $this->content;
}
/**
* Creates a new chunk of content.
*/
public function createChunk(string $data): ChunkInterface
{
return new DataChunk($this->offset, $data);
}
/**
* Pauses the request for the given number of seconds.
*/
public function pause(float $duration): void
{
if (\is_callable($pause = $this->response->getInfo('pause_handler'))) {
$pause($duration);
} elseif (0 < $duration) {
usleep(1E6 * $duration);
}
}
/**
* Cancels the request and returns the last chunk to yield.
*/
public function cancel(): ChunkInterface
{
$this->info['canceled'] = true;
$this->info['error'] = 'Response has been canceled.';
$this->response->cancel();
return new LastChunk();
}
/**
* Returns the current info of the response.
*/
public function getInfo(string $type = null)
{
if (null !== $type) {
return $this->info[$type] ?? $this->response->getInfo($type);
}
return $this->info + $this->response->getInfo();
}
/**
* Attaches an info to the response.
*/
public function setInfo(string $type, $value): self
{
if ('canceled' === $type && $value !== $this->info['canceled']) {
throw new \LogicException('You cannot set the "canceled" info directly.');
}
if (null === $value) {
unset($this->info[$type]);
} else {
$this->info[$type] = $value;
}
return $this;
}
/**
* Returns the currently processed response.
*/
public function getResponse(): ResponseInterface
{
return $this->response;
}
/**
* Replaces the currently processed response by doing a new request.
*/
public function replaceRequest(string $method, string $url, array $options = []): ResponseInterface
{
$this->info['previous_info'][] = $this->response->getInfo();
if (null !== $onProgress = $options['on_progress'] ?? null) {
$thisInfo = &$this->info;
$options['on_progress'] = static function (int $dlNow, int $dlSize, array $info) use (&$thisInfo, $onProgress) {
$onProgress($dlNow, $dlSize, $thisInfo + $info);
};
}
return $this->response = $this->client->request($method, $url, ['buffer' => false] + $options);
}
/**
* Replaces the currently processed response by another one.
*/
public function replaceResponse(ResponseInterface $response): ResponseInterface
{
$this->info['previous_info'][] = $this->response->getInfo();
return $this->response = $response;
}
/**
* Replaces or removes the chunk filter iterator.
*/
public function passthru(callable $passthru = null): void
{
$this->passthru = $passthru;
}
}

View file

@ -0,0 +1,469 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\HttpClient\Response;
use Symfony\Component\HttpClient\Chunk\ErrorChunk;
use Symfony\Component\HttpClient\Chunk\FirstChunk;
use Symfony\Component\HttpClient\Chunk\LastChunk;
use Symfony\Component\HttpClient\Exception\TransportException;
use Symfony\Contracts\HttpClient\ChunkInterface;
use Symfony\Contracts\HttpClient\Exception\ExceptionInterface;
use Symfony\Contracts\HttpClient\Exception\HttpExceptionInterface;
use Symfony\Contracts\HttpClient\Exception\TransportExceptionInterface;
use Symfony\Contracts\HttpClient\HttpClientInterface;
use Symfony\Contracts\HttpClient\ResponseInterface;
/**
* Provides a single extension point to process a response's content stream.
*
* @author Nicolas Grekas <p@tchwork.com>
*/
final class AsyncResponse implements ResponseInterface, StreamableInterface
{
use CommonResponseTrait;
private const FIRST_CHUNK_YIELDED = 1;
private const LAST_CHUNK_YIELDED = 2;
private $client;
private $response;
private $info = ['canceled' => false];
private $passthru;
private $stream;
private $yieldedState;
/**
* @param ?callable(ChunkInterface, AsyncContext): ?\Iterator $passthru
*/
public function __construct(HttpClientInterface $client, string $method, string $url, array $options, callable $passthru = null)
{
$this->client = $client;
$this->shouldBuffer = $options['buffer'] ?? true;
if (null !== $onProgress = $options['on_progress'] ?? null) {
$thisInfo = &$this->info;
$options['on_progress'] = static function (int $dlNow, int $dlSize, array $info) use (&$thisInfo, $onProgress) {
$onProgress($dlNow, $dlSize, $thisInfo + $info);
};
}
$this->response = $client->request($method, $url, ['buffer' => false] + $options);
$this->passthru = $passthru;
$this->initializer = static function (self $response, float $timeout = null) {
if (null === $response->shouldBuffer) {
return false;
}
while (true) {
foreach (self::stream([$response], $timeout) as $chunk) {
if ($chunk->isTimeout() && $response->passthru) {
foreach (self::passthru($response->client, $response, new ErrorChunk($response->offset, new TransportException($chunk->getError()))) as $chunk) {
if ($chunk->isFirst()) {
return false;
}
}
continue 2;
}
if ($chunk->isFirst()) {
return false;
}
}
return false;
}
};
if (\array_key_exists('user_data', $options)) {
$this->info['user_data'] = $options['user_data'];
}
}
public function getStatusCode(): int
{
if ($this->initializer) {
self::initialize($this);
}
return $this->response->getStatusCode();
}
public function getHeaders(bool $throw = true): array
{
if ($this->initializer) {
self::initialize($this);
}
$headers = $this->response->getHeaders(false);
if ($throw) {
$this->checkStatusCode();
}
return $headers;
}
public function getInfo(string $type = null)
{
if (null !== $type) {
return $this->info[$type] ?? $this->response->getInfo($type);
}
return $this->info + $this->response->getInfo();
}
/**
* {@inheritdoc}
*/
public function toStream(bool $throw = true)
{
if ($throw) {
// Ensure headers arrived
$this->getHeaders(true);
}
$handle = function () {
$stream = $this->response instanceof StreamableInterface ? $this->response->toStream(false) : StreamWrapper::createResource($this->response);
return stream_get_meta_data($stream)['wrapper_data']->stream_cast(\STREAM_CAST_FOR_SELECT);
};
$stream = StreamWrapper::createResource($this);
stream_get_meta_data($stream)['wrapper_data']
->bindHandles($handle, $this->content);
return $stream;
}
/**
* {@inheritdoc}
*/
public function cancel(): void
{
if ($this->info['canceled']) {
return;
}
$this->info['canceled'] = true;
$this->info['error'] = 'Response has been canceled.';
$this->close();
$client = $this->client;
$this->client = null;
if (!$this->passthru) {
return;
}
try {
foreach (self::passthru($client, $this, new LastChunk()) as $chunk) {
// no-op
}
$this->passthru = null;
} catch (ExceptionInterface $e) {
// ignore any errors when canceling
}
}
public function __destruct()
{
$httpException = null;
if ($this->initializer && null === $this->getInfo('error')) {
try {
self::initialize($this, -0.0);
$this->getHeaders(true);
} catch (HttpExceptionInterface $httpException) {
// no-op
}
}
if ($this->passthru && null === $this->getInfo('error')) {
$this->info['canceled'] = true;
try {
foreach (self::passthru($this->client, $this, new LastChunk()) as $chunk) {
// no-op
}
} catch (ExceptionInterface $e) {
// ignore any errors when destructing
}
}
if (null !== $httpException) {
throw $httpException;
}
}
/**
* @internal
*/
public static function stream(iterable $responses, float $timeout = null, string $class = null): \Generator
{
while ($responses) {
$wrappedResponses = [];
$asyncMap = new \SplObjectStorage();
$client = null;
foreach ($responses as $r) {
if (!$r instanceof self) {
throw new \TypeError(sprintf('"%s::stream()" expects parameter 1 to be an iterable of AsyncResponse objects, "%s" given.', $class ?? static::class, get_debug_type($r)));
}
if (null !== $e = $r->info['error'] ?? null) {
yield $r => $chunk = new ErrorChunk($r->offset, new TransportException($e));
$chunk->didThrow() ?: $chunk->getContent();
continue;
}
if (null === $client) {
$client = $r->client;
} elseif ($r->client !== $client) {
throw new TransportException('Cannot stream AsyncResponse objects with many clients.');
}
$asyncMap[$r->response] = $r;
$wrappedResponses[] = $r->response;
if ($r->stream) {
yield from self::passthruStream($response = $r->response, $r, new FirstChunk(), $asyncMap);
if (!isset($asyncMap[$response])) {
array_pop($wrappedResponses);
}
if ($r->response !== $response && !isset($asyncMap[$r->response])) {
$asyncMap[$r->response] = $r;
$wrappedResponses[] = $r->response;
}
}
}
if (!$client || !$wrappedResponses) {
return;
}
foreach ($client->stream($wrappedResponses, $timeout) as $response => $chunk) {
$r = $asyncMap[$response];
if (null === $chunk->getError()) {
if ($chunk->isFirst()) {
// Ensure no exception is thrown on destruct for the wrapped response
$r->response->getStatusCode();
} elseif (0 === $r->offset && null === $r->content && $chunk->isLast()) {
$r->content = fopen('php://memory', 'w+');
}
}
if (!$r->passthru) {
if (null !== $chunk->getError() || $chunk->isLast()) {
unset($asyncMap[$response]);
} elseif (null !== $r->content && '' !== ($content = $chunk->getContent()) && \strlen($content) !== fwrite($r->content, $content)) {
$chunk = new ErrorChunk($r->offset, new TransportException(sprintf('Failed writing %d bytes to the response buffer.', \strlen($content))));
$r->info['error'] = $chunk->getError();
$r->response->cancel();
}
yield $r => $chunk;
continue;
}
if (null !== $chunk->getError()) {
// no-op
} elseif ($chunk->isFirst()) {
$r->yieldedState = self::FIRST_CHUNK_YIELDED;
} elseif (self::FIRST_CHUNK_YIELDED !== $r->yieldedState && null === $chunk->getInformationalStatus()) {
throw new \LogicException(sprintf('Instance of "%s" is already consumed and cannot be managed by "%s". A decorated client should not call any of the response\'s methods in its "request()" method.', get_debug_type($response), $class ?? static::class));
}
foreach (self::passthru($r->client, $r, $chunk, $asyncMap) as $chunk) {
yield $r => $chunk;
}
if ($r->response !== $response && isset($asyncMap[$response])) {
break;
}
}
if (null === $chunk->getError() && $chunk->isLast()) {
$r->yieldedState = self::LAST_CHUNK_YIELDED;
}
if (null === $chunk->getError() && self::LAST_CHUNK_YIELDED !== $r->yieldedState && $r->response === $response && null !== $r->client) {
throw new \LogicException('A chunk passthru must yield an "isLast()" chunk before ending a stream.');
}
$responses = [];
foreach ($asyncMap as $response) {
$r = $asyncMap[$response];
if (null !== $r->client) {
$responses[] = $asyncMap[$response];
}
}
}
}
private static function passthru(HttpClientInterface $client, self $r, ChunkInterface $chunk, \SplObjectStorage $asyncMap = null): \Generator
{
$r->stream = null;
$response = $r->response;
$context = new AsyncContext($r->passthru, $client, $r->response, $r->info, $r->content, $r->offset);
if (null === $stream = ($r->passthru)($chunk, $context)) {
if ($r->response === $response && (null !== $chunk->getError() || $chunk->isLast())) {
throw new \LogicException('A chunk passthru cannot swallow the last chunk.');
}
return;
}
if (!$stream instanceof \Iterator) {
throw new \LogicException(sprintf('A chunk passthru must return an "Iterator", "%s" returned.', get_debug_type($stream)));
}
$r->stream = $stream;
yield from self::passthruStream($response, $r, null, $asyncMap);
}
private static function passthruStream(ResponseInterface $response, self $r, ?ChunkInterface $chunk, ?\SplObjectStorage $asyncMap): \Generator
{
while (true) {
try {
if (null !== $chunk && $r->stream) {
$r->stream->next();
}
if (!$r->stream || !$r->stream->valid() || !$r->stream) {
$r->stream = null;
break;
}
} catch (\Throwable $e) {
unset($asyncMap[$response]);
$r->stream = null;
$r->info['error'] = $e->getMessage();
$r->response->cancel();
yield $r => $chunk = new ErrorChunk($r->offset, $e);
$chunk->didThrow() ?: $chunk->getContent();
break;
}
$chunk = $r->stream->current();
if (!$chunk instanceof ChunkInterface) {
throw new \LogicException(sprintf('A chunk passthru must yield instances of "%s", "%s" yielded.', ChunkInterface::class, get_debug_type($chunk)));
}
if (null !== $chunk->getError()) {
// no-op
} elseif ($chunk->isFirst()) {
$e = $r->openBuffer();
yield $r => $chunk;
if ($r->initializer && null === $r->getInfo('error')) {
// Ensure the HTTP status code is always checked
$r->getHeaders(true);
}
if (null === $e) {
continue;
}
$r->response->cancel();
$chunk = new ErrorChunk($r->offset, $e);
} elseif ('' !== $content = $chunk->getContent()) {
if (null !== $r->shouldBuffer) {
throw new \LogicException('A chunk passthru must yield an "isFirst()" chunk before any content chunk.');
}
if (null !== $r->content && \strlen($content) !== fwrite($r->content, $content)) {
$chunk = new ErrorChunk($r->offset, new TransportException(sprintf('Failed writing %d bytes to the response buffer.', \strlen($content))));
$r->info['error'] = $chunk->getError();
$r->response->cancel();
}
}
if (null !== $chunk->getError() || $chunk->isLast()) {
$stream = $r->stream;
$r->stream = null;
unset($asyncMap[$response]);
}
if (null === $chunk->getError()) {
$r->offset += \strlen($content);
yield $r => $chunk;
if (!$chunk->isLast()) {
continue;
}
$stream->next();
if ($stream->valid()) {
throw new \LogicException('A chunk passthru cannot yield after an "isLast()" chunk.');
}
$r->passthru = null;
} else {
if ($chunk instanceof ErrorChunk) {
$chunk->didThrow(false);
} else {
try {
$chunk = new ErrorChunk($chunk->getOffset(), !$chunk->isTimeout() ?: $chunk->getError());
} catch (TransportExceptionInterface $e) {
$chunk = new ErrorChunk($chunk->getOffset(), $e);
}
}
yield $r => $chunk;
$chunk->didThrow() ?: $chunk->getContent();
}
break;
}
}
private function openBuffer(): ?\Throwable
{
if (null === $shouldBuffer = $this->shouldBuffer) {
throw new \LogicException('A chunk passthru cannot yield more than one "isFirst()" chunk.');
}
$e = $this->shouldBuffer = null;
if ($shouldBuffer instanceof \Closure) {
try {
$shouldBuffer = $shouldBuffer($this->getHeaders(false));
if (null !== $e = $this->response->getInfo('error')) {
throw new TransportException($e);
}
} catch (\Throwable $e) {
$this->info['error'] = $e->getMessage();
$this->response->cancel();
}
}
if (true === $shouldBuffer) {
$this->content = fopen('php://temp', 'w+');
} elseif (\is_resource($shouldBuffer)) {
$this->content = $shouldBuffer;
}
return $e;
}
private function close(): void
{
$this->response->cancel();
}
}

View file

@ -0,0 +1,188 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\HttpClient\Response;
use Symfony\Component\HttpClient\Exception\ClientException;
use Symfony\Component\HttpClient\Exception\JsonException;
use Symfony\Component\HttpClient\Exception\RedirectionException;
use Symfony\Component\HttpClient\Exception\ServerException;
use Symfony\Component\HttpClient\Exception\TransportException;
/**
* Implements common logic for response classes.
*
* @author Nicolas Grekas <p@tchwork.com>
*
* @internal
*/
trait CommonResponseTrait
{
/**
* @var callable|null A callback that tells whether we're waiting for response headers
*/
private $initializer;
private $shouldBuffer;
private $content;
private $offset = 0;
private $jsonData;
/**
* {@inheritdoc}
*/
public function getContent(bool $throw = true): string
{
if ($this->initializer) {
self::initialize($this);
}
if ($throw) {
$this->checkStatusCode();
}
if (null === $this->content) {
$content = null;
foreach (self::stream([$this]) as $chunk) {
if (!$chunk->isLast()) {
$content .= $chunk->getContent();
}
}
if (null !== $content) {
return $content;
}
if (null === $this->content) {
throw new TransportException('Cannot get the content of the response twice: buffering is disabled.');
}
} else {
foreach (self::stream([$this]) as $chunk) {
// Chunks are buffered in $this->content already
}
}
rewind($this->content);
return stream_get_contents($this->content);
}
/**
* {@inheritdoc}
*/
public function toArray(bool $throw = true): array
{
if ('' === $content = $this->getContent($throw)) {
throw new JsonException('Response body is empty.');
}
if (null !== $this->jsonData) {
return $this->jsonData;
}
try {
$content = json_decode($content, true, 512, \JSON_BIGINT_AS_STRING | (\PHP_VERSION_ID >= 70300 ? \JSON_THROW_ON_ERROR : 0));
} catch (\JsonException $e) {
throw new JsonException($e->getMessage().sprintf(' for "%s".', $this->getInfo('url')), $e->getCode());
}
if (\PHP_VERSION_ID < 70300 && \JSON_ERROR_NONE !== json_last_error()) {
throw new JsonException(json_last_error_msg().sprintf(' for "%s".', $this->getInfo('url')), json_last_error());
}
if (!\is_array($content)) {
throw new JsonException(sprintf('JSON content was expected to decode to an array, "%s" returned for "%s".', get_debug_type($content), $this->getInfo('url')));
}
if (null !== $this->content) {
// Option "buffer" is true
return $this->jsonData = $content;
}
return $content;
}
/**
* {@inheritdoc}
*/
public function toStream(bool $throw = true)
{
if ($throw) {
// Ensure headers arrived
$this->getHeaders($throw);
}
$stream = StreamWrapper::createResource($this);
stream_get_meta_data($stream)['wrapper_data']
->bindHandles($this->handle, $this->content);
return $stream;
}
/**
* @return array
*/
public function __sleep()
{
throw new \BadMethodCallException('Cannot serialize '.__CLASS__);
}
public function __wakeup()
{
throw new \BadMethodCallException('Cannot unserialize '.__CLASS__);
}
/**
* Closes the response and all its network handles.
*/
abstract protected function close(): void;
private static function initialize(self $response, float $timeout = null): void
{
if (null !== $response->getInfo('error')) {
throw new TransportException($response->getInfo('error'));
}
try {
if (($response->initializer)($response, $timeout)) {
foreach (self::stream([$response], $timeout) as $chunk) {
if ($chunk->isFirst()) {
break;
}
}
}
} catch (\Throwable $e) {
// Persist timeouts thrown during initialization
$response->info['error'] = $e->getMessage();
$response->close();
throw $e;
}
$response->initializer = null;
}
private function checkStatusCode()
{
$code = $this->getInfo('http_code');
if (500 <= $code) {
throw new ServerException($this);
}
if (400 <= $code) {
throw new ClientException($this);
}
if (300 <= $code) {
throw new RedirectionException($this);
}
}
}

View file

@ -0,0 +1,465 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\HttpClient\Response;
use Psr\Log\LoggerInterface;
use Symfony\Component\HttpClient\Chunk\FirstChunk;
use Symfony\Component\HttpClient\Chunk\InformationalChunk;
use Symfony\Component\HttpClient\Exception\TransportException;
use Symfony\Component\HttpClient\Internal\Canary;
use Symfony\Component\HttpClient\Internal\ClientState;
use Symfony\Component\HttpClient\Internal\CurlClientState;
use Symfony\Contracts\HttpClient\ResponseInterface;
/**
* @author Nicolas Grekas <p@tchwork.com>
*
* @internal
*/
final class CurlResponse implements ResponseInterface, StreamableInterface
{
use CommonResponseTrait {
getContent as private doGetContent;
}
use TransportResponseTrait;
private static $performing = false;
private $multi;
private $debugBuffer;
/**
* @param \CurlHandle|resource|string $ch
*
* @internal
*/
public function __construct(CurlClientState $multi, $ch, array $options = null, LoggerInterface $logger = null, string $method = 'GET', callable $resolveRedirect = null, int $curlVersion = null)
{
$this->multi = $multi;
if (\is_resource($ch) || $ch instanceof \CurlHandle) {
$this->handle = $ch;
$this->debugBuffer = fopen('php://temp', 'w+');
if (0x074000 === $curlVersion) {
fwrite($this->debugBuffer, 'Due to a bug in curl 7.64.0, the debug log is disabled; use another version to work around the issue.');
} else {
curl_setopt($ch, \CURLOPT_VERBOSE, true);
curl_setopt($ch, \CURLOPT_STDERR, $this->debugBuffer);
}
} else {
$this->info['url'] = $ch;
$ch = $this->handle;
}
$this->id = $id = (int) $ch;
$this->logger = $logger;
$this->shouldBuffer = $options['buffer'] ?? true;
$this->timeout = $options['timeout'] ?? null;
$this->info['http_method'] = $method;
$this->info['user_data'] = $options['user_data'] ?? null;
$this->info['start_time'] = $this->info['start_time'] ?? microtime(true);
$info = &$this->info;
$headers = &$this->headers;
$debugBuffer = $this->debugBuffer;
if (!$info['response_headers']) {
// Used to keep track of what we're waiting for
curl_setopt($ch, \CURLOPT_PRIVATE, \in_array($method, ['GET', 'HEAD', 'OPTIONS', 'TRACE'], true) && 1.0 < (float) ($options['http_version'] ?? 1.1) ? 'H2' : 'H0'); // H = headers + retry counter
}
curl_setopt($ch, \CURLOPT_HEADERFUNCTION, static function ($ch, string $data) use (&$info, &$headers, $options, $multi, $id, &$location, $resolveRedirect, $logger): int {
if (0 !== substr_compare($data, "\r\n", -2)) {
return 0;
}
$len = 0;
foreach (explode("\r\n", substr($data, 0, -2)) as $data) {
$len += 2 + self::parseHeaderLine($ch, $data, $info, $headers, $options, $multi, $id, $location, $resolveRedirect, $logger);
}
return $len;
});
if (null === $options) {
// Pushed response: buffer until requested
curl_setopt($ch, \CURLOPT_WRITEFUNCTION, static function ($ch, string $data) use ($multi, $id): int {
$multi->handlesActivity[$id][] = $data;
curl_pause($ch, \CURLPAUSE_RECV);
return \strlen($data);
});
return;
}
$execCounter = $multi->execCounter;
$this->info['pause_handler'] = static function (float $duration) use ($ch, $multi, $execCounter) {
if (0 < $duration) {
if ($execCounter === $multi->execCounter) {
$multi->execCounter = !\is_float($execCounter) ? 1 + $execCounter : \PHP_INT_MIN;
curl_multi_remove_handle($multi->handle, $ch);
}
$lastExpiry = end($multi->pauseExpiries);
$multi->pauseExpiries[(int) $ch] = $duration += microtime(true);
if (false !== $lastExpiry && $lastExpiry > $duration) {
asort($multi->pauseExpiries);
}
curl_pause($ch, \CURLPAUSE_ALL);
} else {
unset($multi->pauseExpiries[(int) $ch]);
curl_pause($ch, \CURLPAUSE_CONT);
curl_multi_add_handle($multi->handle, $ch);
}
};
$this->inflate = !isset($options['normalized_headers']['accept-encoding']);
curl_pause($ch, \CURLPAUSE_CONT);
if ($onProgress = $options['on_progress']) {
$url = isset($info['url']) ? ['url' => $info['url']] : [];
curl_setopt($ch, \CURLOPT_NOPROGRESS, false);
curl_setopt($ch, \CURLOPT_PROGRESSFUNCTION, static function ($ch, $dlSize, $dlNow) use ($onProgress, &$info, $url, $multi, $debugBuffer) {
try {
rewind($debugBuffer);
$debug = ['debug' => stream_get_contents($debugBuffer)];
$onProgress($dlNow, $dlSize, $url + curl_getinfo($ch) + $info + $debug);
} catch (\Throwable $e) {
$multi->handlesActivity[(int) $ch][] = null;
$multi->handlesActivity[(int) $ch][] = $e;
return 1; // Abort the request
}
return null;
});
}
curl_setopt($ch, \CURLOPT_WRITEFUNCTION, static function ($ch, string $data) use ($multi, $id): int {
if ('H' === (curl_getinfo($ch, \CURLINFO_PRIVATE)[0] ?? null)) {
$multi->handlesActivity[$id][] = null;
$multi->handlesActivity[$id][] = new TransportException(sprintf('Unsupported protocol for "%s"', curl_getinfo($ch, \CURLINFO_EFFECTIVE_URL)));
return 0;
}
curl_setopt($ch, \CURLOPT_WRITEFUNCTION, static function ($ch, string $data) use ($multi, $id): int {
$multi->handlesActivity[$id][] = $data;
return \strlen($data);
});
$multi->handlesActivity[$id][] = $data;
return \strlen($data);
});
$this->initializer = static function (self $response) {
$waitFor = curl_getinfo($ch = $response->handle, \CURLINFO_PRIVATE);
return 'H' === $waitFor[0];
};
// Schedule the request in a non-blocking way
$multi->openHandles[$id] = [$ch, $options];
curl_multi_add_handle($multi->handle, $ch);
$this->canary = new Canary(static function () use ($ch, $multi, $id) {
unset($multi->pauseExpiries[$id], $multi->openHandles[$id], $multi->handlesActivity[$id]);
curl_setopt($ch, \CURLOPT_PRIVATE, '_0');
if (self::$performing) {
return;
}
curl_multi_remove_handle($multi->handle, $ch);
curl_setopt_array($ch, [
\CURLOPT_NOPROGRESS => true,
\CURLOPT_PROGRESSFUNCTION => null,
\CURLOPT_HEADERFUNCTION => null,
\CURLOPT_WRITEFUNCTION => null,
\CURLOPT_READFUNCTION => null,
\CURLOPT_INFILE => null,
]);
if (!$multi->openHandles) {
// Schedule DNS cache eviction for the next request
$multi->dnsCache->evictions = $multi->dnsCache->evictions ?: $multi->dnsCache->removals;
$multi->dnsCache->removals = $multi->dnsCache->hostnames = [];
}
});
}
/**
* {@inheritdoc}
*/
public function getInfo(string $type = null)
{
if (!$info = $this->finalInfo) {
$info = array_merge($this->info, curl_getinfo($this->handle));
$info['url'] = $this->info['url'] ?? $info['url'];
$info['redirect_url'] = $this->info['redirect_url'] ?? null;
// workaround curl not subtracting the time offset for pushed responses
if (isset($this->info['url']) && $info['start_time'] / 1000 < $info['total_time']) {
$info['total_time'] -= $info['starttransfer_time'] ?: $info['total_time'];
$info['starttransfer_time'] = 0.0;
}
rewind($this->debugBuffer);
$info['debug'] = stream_get_contents($this->debugBuffer);
$waitFor = curl_getinfo($this->handle, \CURLINFO_PRIVATE);
if ('H' !== $waitFor[0] && 'C' !== $waitFor[0]) {
curl_setopt($this->handle, \CURLOPT_VERBOSE, false);
rewind($this->debugBuffer);
ftruncate($this->debugBuffer, 0);
$this->finalInfo = $info;
}
}
return null !== $type ? $info[$type] ?? null : $info;
}
/**
* {@inheritdoc}
*/
public function getContent(bool $throw = true): string
{
$performing = self::$performing;
self::$performing = $performing || '_0' === curl_getinfo($this->handle, \CURLINFO_PRIVATE);
try {
return $this->doGetContent($throw);
} finally {
self::$performing = $performing;
}
}
public function __destruct()
{
try {
if (null === $this->timeout) {
return; // Unused pushed response
}
$this->doDestruct();
} finally {
curl_setopt($this->handle, \CURLOPT_VERBOSE, false);
}
}
/**
* {@inheritdoc}
*/
private static function schedule(self $response, array &$runningResponses): void
{
if (isset($runningResponses[$i = (int) $response->multi->handle])) {
$runningResponses[$i][1][$response->id] = $response;
} else {
$runningResponses[$i] = [$response->multi, [$response->id => $response]];
}
if ('_0' === curl_getinfo($ch = $response->handle, \CURLINFO_PRIVATE)) {
// Response already completed
$response->multi->handlesActivity[$response->id][] = null;
$response->multi->handlesActivity[$response->id][] = null !== $response->info['error'] ? new TransportException($response->info['error']) : null;
}
}
/**
* {@inheritdoc}
*
* @param CurlClientState $multi
*/
private static function perform(ClientState $multi, array &$responses = null): void
{
if (self::$performing) {
if ($responses) {
$response = current($responses);
$multi->handlesActivity[(int) $response->handle][] = null;
$multi->handlesActivity[(int) $response->handle][] = new TransportException(sprintf('Userland callback cannot use the client nor the response while processing "%s".', curl_getinfo($response->handle, \CURLINFO_EFFECTIVE_URL)));
}
return;
}
try {
self::$performing = true;
++$multi->execCounter;
$active = 0;
while (\CURLM_CALL_MULTI_PERFORM === curl_multi_exec($multi->handle, $active));
while ($info = curl_multi_info_read($multi->handle)) {
$result = $info['result'];
$id = (int) $ch = $info['handle'];
$waitFor = @curl_getinfo($ch, \CURLINFO_PRIVATE) ?: '_0';
if (\in_array($result, [\CURLE_SEND_ERROR, \CURLE_RECV_ERROR, /*CURLE_HTTP2*/ 16, /*CURLE_HTTP2_STREAM*/ 92], true) && $waitFor[1] && 'C' !== $waitFor[0]) {
curl_multi_remove_handle($multi->handle, $ch);
$waitFor[1] = (string) ((int) $waitFor[1] - 1); // decrement the retry counter
curl_setopt($ch, \CURLOPT_PRIVATE, $waitFor);
curl_setopt($ch, \CURLOPT_FORBID_REUSE, true);
if (0 === curl_multi_add_handle($multi->handle, $ch)) {
continue;
}
}
if (\CURLE_RECV_ERROR === $result && 'H' === $waitFor[0] && 400 <= ($responses[(int) $ch]->info['http_code'] ?? 0)) {
$multi->handlesActivity[$id][] = new FirstChunk();
}
$multi->handlesActivity[$id][] = null;
$multi->handlesActivity[$id][] = \in_array($result, [\CURLE_OK, \CURLE_TOO_MANY_REDIRECTS], true) || '_0' === $waitFor || curl_getinfo($ch, \CURLINFO_SIZE_DOWNLOAD) === curl_getinfo($ch, \CURLINFO_CONTENT_LENGTH_DOWNLOAD) ? null : new TransportException(sprintf('%s for "%s".', curl_strerror($result), curl_getinfo($ch, \CURLINFO_EFFECTIVE_URL)));
}
} finally {
self::$performing = false;
}
}
/**
* {@inheritdoc}
*
* @param CurlClientState $multi
*/
private static function select(ClientState $multi, float $timeout): int
{
if (\PHP_VERSION_ID < 70211) {
// workaround https://bugs.php.net/76480
$timeout = min($timeout, 0.01);
}
if ($multi->pauseExpiries) {
$now = microtime(true);
foreach ($multi->pauseExpiries as $id => $pauseExpiry) {
if ($now < $pauseExpiry) {
$timeout = min($timeout, $pauseExpiry - $now);
break;
}
unset($multi->pauseExpiries[$id]);
curl_pause($multi->openHandles[$id][0], \CURLPAUSE_CONT);
curl_multi_add_handle($multi->handle, $multi->openHandles[$id][0]);
}
}
if (0 !== $selected = curl_multi_select($multi->handle, $timeout)) {
return $selected;
}
if ($multi->pauseExpiries && 0 < $timeout -= microtime(true) - $now) {
usleep((int) (1E6 * $timeout));
}
return 0;
}
/**
* Parses header lines as curl yields them to us.
*/
private static function parseHeaderLine($ch, string $data, array &$info, array &$headers, ?array $options, CurlClientState $multi, int $id, ?string &$location, ?callable $resolveRedirect, ?LoggerInterface $logger): int
{
$waitFor = @curl_getinfo($ch, \CURLINFO_PRIVATE) ?: '_0';
if ('H' !== $waitFor[0]) {
return \strlen($data); // Ignore HTTP trailers
}
if ('' !== $data) {
try {
// Regular header line: add it to the list
self::addResponseHeaders([$data], $info, $headers);
} catch (TransportException $e) {
$multi->handlesActivity[$id][] = null;
$multi->handlesActivity[$id][] = $e;
return \strlen($data);
}
if (!str_starts_with($data, 'HTTP/')) {
if (0 === stripos($data, 'Location:')) {
$location = trim(substr($data, 9));
}
return \strlen($data);
}
if (\function_exists('openssl_x509_read') && $certinfo = curl_getinfo($ch, \CURLINFO_CERTINFO)) {
$info['peer_certificate_chain'] = array_map('openssl_x509_read', array_column($certinfo, 'Cert'));
}
if (300 <= $info['http_code'] && $info['http_code'] < 400) {
if (curl_getinfo($ch, \CURLINFO_REDIRECT_COUNT) === $options['max_redirects']) {
curl_setopt($ch, \CURLOPT_FOLLOWLOCATION, false);
} elseif (303 === $info['http_code'] || ('POST' === $info['http_method'] && \in_array($info['http_code'], [301, 302], true))) {
$info['http_method'] = 'HEAD' === $info['http_method'] ? 'HEAD' : 'GET';
curl_setopt($ch, \CURLOPT_POSTFIELDS, '');
}
}
return \strlen($data);
}
// End of headers: handle informational responses, redirects, etc.
if (200 > $statusCode = curl_getinfo($ch, \CURLINFO_RESPONSE_CODE)) {
$multi->handlesActivity[$id][] = new InformationalChunk($statusCode, $headers);
$location = null;
return \strlen($data);
}
$info['redirect_url'] = null;
if (300 <= $statusCode && $statusCode < 400 && null !== $location) {
if (null === $info['redirect_url'] = $resolveRedirect($ch, $location)) {
$options['max_redirects'] = curl_getinfo($ch, \CURLINFO_REDIRECT_COUNT);
curl_setopt($ch, \CURLOPT_FOLLOWLOCATION, false);
curl_setopt($ch, \CURLOPT_MAXREDIRS, $options['max_redirects']);
} else {
$url = parse_url($location ?? ':');
if (isset($url['host']) && null !== $ip = $multi->dnsCache->hostnames[$url['host'] = strtolower($url['host'])] ?? null) {
// Populate DNS cache for redirects if needed
$port = $url['port'] ?? ('http' === ($url['scheme'] ?? parse_url(curl_getinfo($ch, \CURLINFO_EFFECTIVE_URL), \PHP_URL_SCHEME)) ? 80 : 443);
curl_setopt($ch, \CURLOPT_RESOLVE, ["{$url['host']}:$port:$ip"]);
$multi->dnsCache->removals["-{$url['host']}:$port"] = "-{$url['host']}:$port";
}
}
}
if (401 === $statusCode && isset($options['auth_ntlm']) && 0 === strncasecmp($headers['www-authenticate'][0] ?? '', 'NTLM ', 5)) {
// Continue with NTLM auth
} elseif ($statusCode < 300 || 400 <= $statusCode || null === $location || curl_getinfo($ch, \CURLINFO_REDIRECT_COUNT) === $options['max_redirects']) {
// Headers and redirects completed, time to get the response's content
$multi->handlesActivity[$id][] = new FirstChunk();
if ('HEAD' === $info['http_method'] || \in_array($statusCode, [204, 304], true)) {
$waitFor = '_0'; // no content expected
$multi->handlesActivity[$id][] = null;
$multi->handlesActivity[$id][] = null;
} else {
$waitFor[0] = 'C'; // C = content
}
curl_setopt($ch, \CURLOPT_PRIVATE, $waitFor);
} elseif (null !== $info['redirect_url'] && $logger) {
$logger->info(sprintf('Redirecting: "%s %s"', $info['http_code'], $info['redirect_url']));
}
$location = null;
return \strlen($data);
}
}

View file

@ -0,0 +1,80 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\HttpClient\Response;
use function GuzzleHttp\Promise\promise_for;
use GuzzleHttp\Promise\PromiseInterface as GuzzlePromiseInterface;
use Http\Promise\Promise as HttplugPromiseInterface;
use Psr\Http\Message\ResponseInterface as Psr7ResponseInterface;
/**
* @author Tobias Nyholm <tobias.nyholm@gmail.com>
*
* @internal
*/
final class HttplugPromise implements HttplugPromiseInterface
{
private $promise;
public function __construct(GuzzlePromiseInterface $promise)
{
$this->promise = $promise;
}
public function then(callable $onFulfilled = null, callable $onRejected = null): self
{
return new self($this->promise->then(
$this->wrapThenCallback($onFulfilled),
$this->wrapThenCallback($onRejected)
));
}
public function cancel(): void
{
$this->promise->cancel();
}
/**
* {@inheritdoc}
*/
public function getState(): string
{
return $this->promise->getState();
}
/**
* {@inheritdoc}
*
* @return Psr7ResponseInterface|mixed
*/
public function wait($unwrap = true)
{
$result = $this->promise->wait($unwrap);
while ($result instanceof HttplugPromiseInterface || $result instanceof GuzzlePromiseInterface) {
$result = $result->wait($unwrap);
}
return $result;
}
private function wrapThenCallback(?callable $callback): ?callable
{
if (null === $callback) {
return null;
}
return static function ($value) use ($callback) {
return promise_for($callback($value));
};
}
}

View file

@ -0,0 +1,326 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\HttpClient\Response;
use Symfony\Component\HttpClient\Chunk\ErrorChunk;
use Symfony\Component\HttpClient\Chunk\FirstChunk;
use Symfony\Component\HttpClient\Exception\InvalidArgumentException;
use Symfony\Component\HttpClient\Exception\TransportException;
use Symfony\Component\HttpClient\Internal\ClientState;
use Symfony\Contracts\HttpClient\ResponseInterface;
/**
* A test-friendly response.
*
* @author Nicolas Grekas <p@tchwork.com>
*/
class MockResponse implements ResponseInterface, StreamableInterface
{
use CommonResponseTrait;
use TransportResponseTrait {
doDestruct as public __destruct;
}
private $body;
private $requestOptions = [];
private $requestUrl;
private $requestMethod;
private static $mainMulti;
private static $idSequence = 0;
/**
* @param string|string[]|iterable $body The response body as a string or an iterable of strings,
* yielding an empty string simulates an idle timeout,
* exceptions are turned to TransportException
*
* @see ResponseInterface::getInfo() for possible info, e.g. "response_headers"
*/
public function __construct($body = '', array $info = [])
{
$this->body = is_iterable($body) ? $body : (string) $body;
$this->info = $info + ['http_code' => 200] + $this->info;
if (!isset($info['response_headers'])) {
return;
}
$responseHeaders = [];
foreach ($info['response_headers'] as $k => $v) {
foreach ((array) $v as $v) {
$responseHeaders[] = (\is_string($k) ? $k.': ' : '').$v;
}
}
$this->info['response_headers'] = [];
self::addResponseHeaders($responseHeaders, $this->info, $this->headers);
}
/**
* Returns the options used when doing the request.
*/
public function getRequestOptions(): array
{
return $this->requestOptions;
}
/**
* Returns the URL used when doing the request.
*/
public function getRequestUrl(): string
{
return $this->requestUrl;
}
/**
* Returns the method used when doing the request.
*/
public function getRequestMethod(): string
{
return $this->requestMethod;
}
/**
* {@inheritdoc}
*/
public function getInfo(string $type = null)
{
return null !== $type ? $this->info[$type] ?? null : $this->info;
}
/**
* {@inheritdoc}
*/
public function cancel(): void
{
$this->info['canceled'] = true;
$this->info['error'] = 'Response has been canceled.';
try {
$this->body = null;
} catch (TransportException $e) {
// ignore errors when canceling
}
}
/**
* {@inheritdoc}
*/
protected function close(): void
{
$this->inflate = null;
$this->body = [];
}
/**
* @internal
*/
public static function fromRequest(string $method, string $url, array $options, ResponseInterface $mock): self
{
$response = new self([]);
$response->requestOptions = $options;
$response->id = ++self::$idSequence;
$response->shouldBuffer = $options['buffer'] ?? true;
$response->initializer = static function (self $response) {
return \is_array($response->body[0] ?? null);
};
$response->info['redirect_count'] = 0;
$response->info['redirect_url'] = null;
$response->info['start_time'] = microtime(true);
$response->info['http_method'] = $method;
$response->info['http_code'] = 0;
$response->info['user_data'] = $options['user_data'] ?? null;
$response->info['url'] = $url;
if ($mock instanceof self) {
$mock->requestOptions = $response->requestOptions;
$mock->requestMethod = $method;
$mock->requestUrl = $url;
}
self::writeRequest($response, $options, $mock);
$response->body[] = [$options, $mock];
return $response;
}
/**
* {@inheritdoc}
*/
protected static function schedule(self $response, array &$runningResponses): void
{
if (!$response->id) {
throw new InvalidArgumentException('MockResponse instances must be issued by MockHttpClient before processing.');
}
$multi = self::$mainMulti ?? self::$mainMulti = new ClientState();
if (!isset($runningResponses[0])) {
$runningResponses[0] = [$multi, []];
}
$runningResponses[0][1][$response->id] = $response;
}
/**
* {@inheritdoc}
*/
protected static function perform(ClientState $multi, array &$responses): void
{
foreach ($responses as $response) {
$id = $response->id;
if (null === $response->body) {
// Canceled response
$response->body = [];
} elseif ([] === $response->body) {
// Error chunk
$multi->handlesActivity[$id][] = null;
$multi->handlesActivity[$id][] = null !== $response->info['error'] ? new TransportException($response->info['error']) : null;
} elseif (null === $chunk = array_shift($response->body)) {
// Last chunk
$multi->handlesActivity[$id][] = null;
$multi->handlesActivity[$id][] = array_shift($response->body);
} elseif (\is_array($chunk)) {
// First chunk
try {
$offset = 0;
$chunk[1]->getStatusCode();
$chunk[1]->getHeaders(false);
self::readResponse($response, $chunk[0], $chunk[1], $offset);
$multi->handlesActivity[$id][] = new FirstChunk();
$buffer = $response->requestOptions['buffer'] ?? null;
if ($buffer instanceof \Closure && $response->content = $buffer($response->headers) ?: null) {
$response->content = \is_resource($response->content) ? $response->content : fopen('php://temp', 'w+');
}
} catch (\Throwable $e) {
$multi->handlesActivity[$id][] = null;
$multi->handlesActivity[$id][] = $e;
}
} else {
// Data or timeout chunk
$multi->handlesActivity[$id][] = $chunk;
}
}
}
/**
* {@inheritdoc}
*/
protected static function select(ClientState $multi, float $timeout): int
{
return 42;
}
/**
* Simulates sending the request.
*/
private static function writeRequest(self $response, array $options, ResponseInterface $mock)
{
$onProgress = $options['on_progress'] ?? static function () {};
$response->info += $mock->getInfo() ?: [];
// simulate "size_upload" if it is set
if (isset($response->info['size_upload'])) {
$response->info['size_upload'] = 0.0;
}
// simulate "total_time" if it is not set
if (!isset($response->info['total_time'])) {
$response->info['total_time'] = microtime(true) - $response->info['start_time'];
}
// "notify" DNS resolution
$onProgress(0, 0, $response->info);
// consume the request body
if (\is_resource($body = $options['body'] ?? '')) {
$data = stream_get_contents($body);
if (isset($response->info['size_upload'])) {
$response->info['size_upload'] += \strlen($data);
}
} elseif ($body instanceof \Closure) {
while ('' !== $data = $body(16372)) {
if (!\is_string($data)) {
throw new TransportException(sprintf('Return value of the "body" option callback must be string, "%s" returned.', get_debug_type($data)));
}
// "notify" upload progress
if (isset($response->info['size_upload'])) {
$response->info['size_upload'] += \strlen($data);
}
$onProgress(0, 0, $response->info);
}
}
}
/**
* Simulates reading the response.
*/
private static function readResponse(self $response, array $options, ResponseInterface $mock, int &$offset)
{
$onProgress = $options['on_progress'] ?? static function () {};
// populate info related to headers
$info = $mock->getInfo() ?: [];
$response->info['http_code'] = ($info['http_code'] ?? 0) ?: $mock->getStatusCode() ?: 200;
$response->addResponseHeaders($info['response_headers'] ?? [], $response->info, $response->headers);
$dlSize = isset($response->headers['content-encoding']) || 'HEAD' === $response->info['http_method'] || \in_array($response->info['http_code'], [204, 304], true) ? 0 : (int) ($response->headers['content-length'][0] ?? 0);
$response->info = [
'start_time' => $response->info['start_time'],
'user_data' => $response->info['user_data'],
'http_code' => $response->info['http_code'],
] + $info + $response->info;
if (!isset($response->info['total_time'])) {
$response->info['total_time'] = microtime(true) - $response->info['start_time'];
}
// "notify" headers arrival
$onProgress(0, $dlSize, $response->info);
// cast response body to activity list
$body = $mock instanceof self ? $mock->body : $mock->getContent(false);
if (!\is_string($body)) {
foreach ($body as $chunk) {
if ('' === $chunk = (string) $chunk) {
// simulate an idle timeout
$response->body[] = new ErrorChunk($offset, sprintf('Idle timeout reached for "%s".', $response->info['url']));
} else {
$response->body[] = $chunk;
$offset += \strlen($chunk);
// "notify" download progress
$onProgress($offset, $dlSize, $response->info);
}
}
} elseif ('' !== $body) {
$response->body[] = $body;
$offset = \strlen($body);
}
if (!isset($response->info['total_time'])) {
$response->info['total_time'] = microtime(true) - $response->info['start_time'];
}
// "notify" completion
$onProgress($offset, $dlSize, $response->info);
if ($dlSize && $offset !== $dlSize) {
throw new TransportException(sprintf('Transfer closed with %d bytes remaining to read.', $dlSize - $offset));
}
}
}

View file

@ -0,0 +1,376 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\HttpClient\Response;
use Psr\Log\LoggerInterface;
use Symfony\Component\HttpClient\Chunk\FirstChunk;
use Symfony\Component\HttpClient\Exception\TransportException;
use Symfony\Component\HttpClient\Internal\Canary;
use Symfony\Component\HttpClient\Internal\ClientState;
use Symfony\Component\HttpClient\Internal\NativeClientState;
use Symfony\Contracts\HttpClient\ResponseInterface;
/**
* @author Nicolas Grekas <p@tchwork.com>
*
* @internal
*/
final class NativeResponse implements ResponseInterface, StreamableInterface
{
use CommonResponseTrait;
use TransportResponseTrait;
private $context;
private $url;
private $resolver;
private $onProgress;
private $remaining;
private $buffer;
private $multi;
private $debugBuffer;
private $shouldBuffer;
private $pauseExpiry = 0;
/**
* @internal
*/
public function __construct(NativeClientState $multi, $context, string $url, array $options, array &$info, callable $resolver, ?callable $onProgress, ?LoggerInterface $logger)
{
$this->multi = $multi;
$this->id = $id = (int) $context;
$this->context = $context;
$this->url = $url;
$this->logger = $logger;
$this->timeout = $options['timeout'];
$this->info = &$info;
$this->resolver = $resolver;
$this->onProgress = $onProgress;
$this->inflate = !isset($options['normalized_headers']['accept-encoding']);
$this->shouldBuffer = $options['buffer'] ?? true;
// Temporary resource to dechunk the response stream
$this->buffer = fopen('php://temp', 'w+');
$info['user_data'] = $options['user_data'];
++$multi->responseCount;
$this->initializer = static function (self $response) {
return null === $response->remaining;
};
$pauseExpiry = &$this->pauseExpiry;
$info['pause_handler'] = static function (float $duration) use (&$pauseExpiry) {
$pauseExpiry = 0 < $duration ? microtime(true) + $duration : 0;
};
$this->canary = new Canary(static function () use ($multi, $id) {
if (null !== ($host = $multi->openHandles[$id][6] ?? null) && 0 >= --$multi->hosts[$host]) {
unset($multi->hosts[$host]);
}
unset($multi->openHandles[$id], $multi->handlesActivity[$id]);
});
}
/**
* {@inheritdoc}
*/
public function getInfo(string $type = null)
{
if (!$info = $this->finalInfo) {
$info = $this->info;
$info['url'] = implode('', $info['url']);
unset($info['size_body'], $info['request_header']);
if (null === $this->buffer) {
$this->finalInfo = $info;
}
}
return null !== $type ? $info[$type] ?? null : $info;
}
public function __destruct()
{
try {
$this->doDestruct();
} finally {
// Clear the DNS cache when all requests completed
if (0 >= --$this->multi->responseCount) {
$this->multi->responseCount = 0;
$this->multi->dnsCache = [];
}
}
}
private function open(): void
{
$url = $this->url;
set_error_handler(function ($type, $msg) use (&$url) {
if (\E_NOTICE !== $type || 'fopen(): Content-type not specified assuming application/x-www-form-urlencoded' !== $msg) {
throw new TransportException($msg);
}
$this->logger && $this->logger->info(sprintf('%s for "%s".', $msg, $url ?? $this->url));
});
try {
$this->info['start_time'] = microtime(true);
[$resolver, $url] = ($this->resolver)($this->multi);
while (true) {
$context = stream_context_get_options($this->context);
if ($proxy = $context['http']['proxy'] ?? null) {
$this->info['debug'] .= "* Establish HTTP proxy tunnel to {$proxy}\n";
$this->info['request_header'] = $url;
} else {
$this->info['debug'] .= "* Trying {$this->info['primary_ip']}...\n";
$this->info['request_header'] = $this->info['url']['path'].$this->info['url']['query'];
}
$this->info['request_header'] = sprintf("> %s %s HTTP/%s \r\n", $context['http']['method'], $this->info['request_header'], $context['http']['protocol_version']);
$this->info['request_header'] .= implode("\r\n", $context['http']['header'])."\r\n\r\n";
if (\array_key_exists('peer_name', $context['ssl']) && null === $context['ssl']['peer_name']) {
unset($context['ssl']['peer_name']);
$this->context = stream_context_create([], ['options' => $context] + stream_context_get_params($this->context));
}
// Send request and follow redirects when needed
$this->handle = $h = fopen($url, 'r', false, $this->context);
self::addResponseHeaders(stream_get_meta_data($h)['wrapper_data'], $this->info, $this->headers, $this->info['debug']);
$url = $resolver($this->multi, $this->headers['location'][0] ?? null, $this->context);
if (null === $url) {
break;
}
$this->logger && $this->logger->info(sprintf('Redirecting: "%s %s"', $this->info['http_code'], $url ?? $this->url));
}
} catch (\Throwable $e) {
$this->close();
$this->multi->handlesActivity[$this->id][] = null;
$this->multi->handlesActivity[$this->id][] = $e;
return;
} finally {
$this->info['pretransfer_time'] = $this->info['total_time'] = microtime(true) - $this->info['start_time'];
restore_error_handler();
}
if (isset($context['ssl']['capture_peer_cert_chain']) && isset(($context = stream_context_get_options($this->context))['ssl']['peer_certificate_chain'])) {
$this->info['peer_certificate_chain'] = $context['ssl']['peer_certificate_chain'];
}
stream_set_blocking($h, false);
$this->context = $this->resolver = null;
// Create dechunk buffers
if (isset($this->headers['content-length'])) {
$this->remaining = (int) $this->headers['content-length'][0];
} elseif ('chunked' === ($this->headers['transfer-encoding'][0] ?? null)) {
stream_filter_append($this->buffer, 'dechunk', \STREAM_FILTER_WRITE);
$this->remaining = -1;
} else {
$this->remaining = -2;
}
$this->multi->handlesActivity[$this->id] = [new FirstChunk()];
if ('HEAD' === $context['http']['method'] || \in_array($this->info['http_code'], [204, 304], true)) {
$this->multi->handlesActivity[$this->id][] = null;
$this->multi->handlesActivity[$this->id][] = null;
return;
}
$host = parse_url($this->info['redirect_url'] ?? $this->url, \PHP_URL_HOST);
$this->multi->openHandles[$this->id] = [&$this->pauseExpiry, $h, $this->buffer, $this->onProgress, &$this->remaining, &$this->info, $host];
$this->multi->hosts[$host] = 1 + ($this->multi->hosts[$host] ?? 0);
}
/**
* {@inheritdoc}
*/
private function close(): void
{
$this->canary->cancel();
$this->handle = $this->buffer = $this->inflate = $this->onProgress = null;
}
/**
* {@inheritdoc}
*/
private static function schedule(self $response, array &$runningResponses): void
{
if (!isset($runningResponses[$i = $response->multi->id])) {
$runningResponses[$i] = [$response->multi, []];
}
$runningResponses[$i][1][$response->id] = $response;
if (null === $response->buffer) {
// Response already completed
$response->multi->handlesActivity[$response->id][] = null;
$response->multi->handlesActivity[$response->id][] = null !== $response->info['error'] ? new TransportException($response->info['error']) : null;
}
}
/**
* {@inheritdoc}
*
* @param NativeClientState $multi
*/
private static function perform(ClientState $multi, array &$responses = null): void
{
foreach ($multi->openHandles as $i => [$pauseExpiry, $h, $buffer, $onProgress]) {
if ($pauseExpiry) {
if (microtime(true) < $pauseExpiry) {
continue;
}
$multi->openHandles[$i][0] = 0;
}
$hasActivity = false;
$remaining = &$multi->openHandles[$i][4];
$info = &$multi->openHandles[$i][5];
$e = null;
// Read incoming buffer and write it to the dechunk one
try {
if ($remaining && '' !== $data = (string) fread($h, 0 > $remaining ? 16372 : $remaining)) {
fwrite($buffer, $data);
$hasActivity = true;
$multi->sleep = false;
if (-1 !== $remaining) {
$remaining -= \strlen($data);
}
}
} catch (\Throwable $e) {
$hasActivity = $onProgress = false;
}
if (!$hasActivity) {
if ($onProgress) {
try {
// Notify the progress callback so that it can e.g. cancel
// the request if the stream is inactive for too long
$info['total_time'] = microtime(true) - $info['start_time'];
$onProgress();
} catch (\Throwable $e) {
// no-op
}
}
} elseif ('' !== $data = stream_get_contents($buffer, -1, 0)) {
rewind($buffer);
ftruncate($buffer, 0);
if (null === $e) {
$multi->handlesActivity[$i][] = $data;
}
}
if (null !== $e || !$remaining || feof($h)) {
// Stream completed
$info['total_time'] = microtime(true) - $info['start_time'];
$info['starttransfer_time'] = $info['starttransfer_time'] ?: $info['total_time'];
if ($onProgress) {
try {
$onProgress(-1);
} catch (\Throwable $e) {
// no-op
}
}
if (null === $e) {
if (0 < $remaining) {
$e = new TransportException(sprintf('Transfer closed with %s bytes remaining to read.', $remaining));
} elseif (-1 === $remaining && fwrite($buffer, '-') && '' !== stream_get_contents($buffer, -1, 0)) {
$e = new TransportException('Transfer closed with outstanding data remaining from chunked response.');
}
}
$multi->handlesActivity[$i][] = null;
$multi->handlesActivity[$i][] = $e;
if (null !== ($host = $multi->openHandles[$i][6] ?? null) && 0 >= --$multi->hosts[$host]) {
unset($multi->hosts[$host]);
}
unset($multi->openHandles[$i]);
$multi->sleep = false;
}
}
if (null === $responses) {
return;
}
$maxHosts = $multi->maxHostConnections;
foreach ($responses as $i => $response) {
if (null !== $response->remaining || null === $response->buffer) {
continue;
}
if ($response->pauseExpiry && microtime(true) < $response->pauseExpiry) {
// Create empty open handles to tell we still have pending requests
$multi->openHandles[$i] = [\INF, null, null, null];
} elseif ($maxHosts && $maxHosts > ($multi->hosts[parse_url($response->url, \PHP_URL_HOST)] ?? 0)) {
// Open the next pending request - this is a blocking operation so we do only one of them
$response->open();
$multi->sleep = false;
self::perform($multi);
$maxHosts = 0;
}
}
}
/**
* {@inheritdoc}
*
* @param NativeClientState $multi
*/
private static function select(ClientState $multi, float $timeout): int
{
if (!$multi->sleep = !$multi->sleep) {
return -1;
}
$_ = $handles = [];
$now = null;
foreach ($multi->openHandles as [$pauseExpiry, $h]) {
if (null === $h) {
continue;
}
if ($pauseExpiry && ($now ?? $now = microtime(true)) < $pauseExpiry) {
$timeout = min($timeout, $pauseExpiry - $now);
continue;
}
$handles[] = $h;
}
if (!$handles) {
usleep((int) (1E6 * $timeout));
return 0;
}
return stream_select($handles, $_, $_, (int) $timeout, (int) (1E6 * ($timeout - (int) $timeout)));
}
}

View file

@ -0,0 +1,54 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\HttpClient\Response;
use Symfony\Contracts\HttpClient\ChunkInterface;
use Symfony\Contracts\HttpClient\ResponseInterface;
use Symfony\Contracts\HttpClient\ResponseStreamInterface;
/**
* @author Nicolas Grekas <p@tchwork.com>
*/
final class ResponseStream implements ResponseStreamInterface
{
private $generator;
public function __construct(\Generator $generator)
{
$this->generator = $generator;
}
public function key(): ResponseInterface
{
return $this->generator->key();
}
public function current(): ChunkInterface
{
return $this->generator->current();
}
public function next(): void
{
$this->generator->next();
}
public function rewind(): void
{
$this->generator->rewind();
}
public function valid(): bool
{
return $this->generator->valid();
}
}

View file

@ -0,0 +1,304 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\HttpClient\Response;
use Symfony\Contracts\HttpClient\Exception\ExceptionInterface;
use Symfony\Contracts\HttpClient\HttpClientInterface;
use Symfony\Contracts\HttpClient\ResponseInterface;
/**
* Allows turning ResponseInterface instances to PHP streams.
*
* @author Nicolas Grekas <p@tchwork.com>
*/
class StreamWrapper
{
/** @var resource|string|null */
public $context;
/** @var HttpClientInterface */
private $client;
/** @var ResponseInterface */
private $response;
/** @var resource|null */
private $content;
/** @var resource|null */
private $handle;
private $blocking = true;
private $timeout;
private $eof = false;
private $offset = 0;
/**
* Creates a PHP stream resource from a ResponseInterface.
*
* @return resource
*/
public static function createResource(ResponseInterface $response, HttpClientInterface $client = null)
{
if ($response instanceof StreamableInterface) {
$stack = debug_backtrace(\DEBUG_BACKTRACE_PROVIDE_OBJECT | \DEBUG_BACKTRACE_IGNORE_ARGS, 2);
if ($response !== ($stack[1]['object'] ?? null)) {
return $response->toStream(false);
}
}
if (null === $client && !method_exists($response, 'stream')) {
throw new \InvalidArgumentException(sprintf('Providing a client to "%s()" is required when the response doesn\'t have any "stream()" method.', __CLASS__));
}
if (false === stream_wrapper_register('symfony', __CLASS__)) {
throw new \RuntimeException(error_get_last()['message'] ?? 'Registering the "symfony" stream wrapper failed.');
}
try {
$context = [
'client' => $client ?? $response,
'response' => $response,
];
return fopen('symfony://'.$response->getInfo('url'), 'r', false, stream_context_create(['symfony' => $context])) ?: null;
} finally {
stream_wrapper_unregister('symfony');
}
}
public function getResponse(): ResponseInterface
{
return $this->response;
}
/**
* @param resource|callable|null $handle The resource handle that should be monitored when
* stream_select() is used on the created stream
* @param resource|null $content The seekable resource where the response body is buffered
*/
public function bindHandles(&$handle, &$content): void
{
$this->handle = &$handle;
$this->content = &$content;
}
public function stream_open(string $path, string $mode, int $options): bool
{
if ('r' !== $mode) {
if ($options & \STREAM_REPORT_ERRORS) {
trigger_error(sprintf('Invalid mode "%s": only "r" is supported.', $mode), \E_USER_WARNING);
}
return false;
}
$context = stream_context_get_options($this->context)['symfony'] ?? null;
$this->client = $context['client'] ?? null;
$this->response = $context['response'] ?? null;
$this->context = null;
if (null !== $this->client && null !== $this->response) {
return true;
}
if ($options & \STREAM_REPORT_ERRORS) {
trigger_error('Missing options "client" or "response" in "symfony" stream context.', \E_USER_WARNING);
}
return false;
}
public function stream_read(int $count)
{
if (\is_resource($this->content)) {
// Empty the internal activity list
foreach ($this->client->stream([$this->response], 0) as $chunk) {
try {
if (!$chunk->isTimeout() && $chunk->isFirst()) {
$this->response->getStatusCode(); // ignore 3/4/5xx
}
} catch (ExceptionInterface $e) {
trigger_error($e->getMessage(), \E_USER_WARNING);
return false;
}
}
if (0 !== fseek($this->content, $this->offset)) {
return false;
}
if ('' !== $data = fread($this->content, $count)) {
fseek($this->content, 0, \SEEK_END);
$this->offset += \strlen($data);
return $data;
}
}
if (\is_string($this->content)) {
if (\strlen($this->content) <= $count) {
$data = $this->content;
$this->content = null;
} else {
$data = substr($this->content, 0, $count);
$this->content = substr($this->content, $count);
}
$this->offset += \strlen($data);
return $data;
}
foreach ($this->client->stream([$this->response], $this->blocking ? $this->timeout : 0) as $chunk) {
try {
$this->eof = true;
$this->eof = !$chunk->isTimeout();
$this->eof = $chunk->isLast();
if ($chunk->isFirst()) {
$this->response->getStatusCode(); // ignore 3/4/5xx
}
if ('' !== $data = $chunk->getContent()) {
if (\strlen($data) > $count) {
if (null === $this->content) {
$this->content = substr($data, $count);
}
$data = substr($data, 0, $count);
}
$this->offset += \strlen($data);
return $data;
}
} catch (ExceptionInterface $e) {
trigger_error($e->getMessage(), \E_USER_WARNING);
return false;
}
}
return '';
}
public function stream_set_option(int $option, int $arg1, ?int $arg2): bool
{
if (\STREAM_OPTION_BLOCKING === $option) {
$this->blocking = (bool) $arg1;
} elseif (\STREAM_OPTION_READ_TIMEOUT === $option) {
$this->timeout = $arg1 + $arg2 / 1e6;
} else {
return false;
}
return true;
}
public function stream_tell(): int
{
return $this->offset;
}
public function stream_eof(): bool
{
return $this->eof && !\is_string($this->content);
}
public function stream_seek(int $offset, int $whence = \SEEK_SET): bool
{
if (!\is_resource($this->content) || 0 !== fseek($this->content, 0, \SEEK_END)) {
return false;
}
$size = ftell($this->content);
if (\SEEK_CUR === $whence) {
$offset += $this->offset;
}
if (\SEEK_END === $whence || $size < $offset) {
foreach ($this->client->stream([$this->response]) as $chunk) {
try {
if ($chunk->isFirst()) {
$this->response->getStatusCode(); // ignore 3/4/5xx
}
// Chunks are buffered in $this->content already
$size += \strlen($chunk->getContent());
if (\SEEK_END !== $whence && $offset <= $size) {
break;
}
} catch (ExceptionInterface $e) {
trigger_error($e->getMessage(), \E_USER_WARNING);
return false;
}
}
if (\SEEK_END === $whence) {
$offset += $size;
}
}
if (0 <= $offset && $offset <= $size) {
$this->eof = false;
$this->offset = $offset;
return true;
}
return false;
}
public function stream_cast(int $castAs)
{
if (\STREAM_CAST_FOR_SELECT === $castAs) {
$this->response->getHeaders(false);
return (\is_callable($this->handle) ? ($this->handle)() : $this->handle) ?? false;
}
return false;
}
public function stream_stat(): array
{
try {
$headers = $this->response->getHeaders(false);
} catch (ExceptionInterface $e) {
trigger_error($e->getMessage(), \E_USER_WARNING);
$headers = [];
}
return [
'dev' => 0,
'ino' => 0,
'mode' => 33060,
'nlink' => 0,
'uid' => 0,
'gid' => 0,
'rdev' => 0,
'size' => (int) ($headers['content-length'][0] ?? -1),
'atime' => 0,
'mtime' => strtotime($headers['last-modified'][0] ?? '') ?: 0,
'ctime' => 0,
'blksize' => 0,
'blocks' => 0,
];
}
private function __construct()
{
}
}

View file

@ -0,0 +1,35 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\HttpClient\Response;
use Symfony\Contracts\HttpClient\Exception\ClientExceptionInterface;
use Symfony\Contracts\HttpClient\Exception\RedirectionExceptionInterface;
use Symfony\Contracts\HttpClient\Exception\ServerExceptionInterface;
use Symfony\Contracts\HttpClient\Exception\TransportExceptionInterface;
/**
* @author Nicolas Grekas <p@tchwork.com>
*/
interface StreamableInterface
{
/**
* Casts the response to a PHP stream resource.
*
* @return resource
*
* @throws TransportExceptionInterface When a network error occurs
* @throws RedirectionExceptionInterface On a 3xx when $throw is true and the "max_redirects" option has been reached
* @throws ClientExceptionInterface On a 4xx when $throw is true
* @throws ServerExceptionInterface On a 5xx when $throw is true
*/
public function toStream(bool $throw = true);
}

View file

@ -0,0 +1,222 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\HttpClient\Response;
use Symfony\Component\HttpClient\Chunk\ErrorChunk;
use Symfony\Component\HttpClient\Exception\ClientException;
use Symfony\Component\HttpClient\Exception\RedirectionException;
use Symfony\Component\HttpClient\Exception\ServerException;
use Symfony\Component\HttpClient\TraceableHttpClient;
use Symfony\Component\Stopwatch\StopwatchEvent;
use Symfony\Contracts\HttpClient\Exception\ClientExceptionInterface;
use Symfony\Contracts\HttpClient\Exception\RedirectionExceptionInterface;
use Symfony\Contracts\HttpClient\Exception\ServerExceptionInterface;
use Symfony\Contracts\HttpClient\Exception\TransportExceptionInterface;
use Symfony\Contracts\HttpClient\HttpClientInterface;
use Symfony\Contracts\HttpClient\ResponseInterface;
/**
* @author Nicolas Grekas <p@tchwork.com>
*
* @internal
*/
class TraceableResponse implements ResponseInterface, StreamableInterface
{
private $client;
private $response;
private $content;
private $event;
public function __construct(HttpClientInterface $client, ResponseInterface $response, &$content, StopwatchEvent $event = null)
{
$this->client = $client;
$this->response = $response;
$this->content = &$content;
$this->event = $event;
}
/**
* @return array
*/
public function __sleep()
{
throw new \BadMethodCallException('Cannot serialize '.__CLASS__);
}
public function __wakeup()
{
throw new \BadMethodCallException('Cannot unserialize '.__CLASS__);
}
public function __destruct()
{
try {
$this->response->__destruct();
} finally {
if ($this->event && $this->event->isStarted()) {
$this->event->stop();
}
}
}
public function getStatusCode(): int
{
try {
return $this->response->getStatusCode();
} finally {
if ($this->event && $this->event->isStarted()) {
$this->event->lap();
}
}
}
public function getHeaders(bool $throw = true): array
{
try {
return $this->response->getHeaders($throw);
} finally {
if ($this->event && $this->event->isStarted()) {
$this->event->lap();
}
}
}
public function getContent(bool $throw = true): string
{
try {
if (false === $this->content) {
return $this->response->getContent($throw);
}
return $this->content = $this->response->getContent(false);
} finally {
if ($this->event && $this->event->isStarted()) {
$this->event->stop();
}
if ($throw) {
$this->checkStatusCode($this->response->getStatusCode());
}
}
}
public function toArray(bool $throw = true): array
{
try {
if (false === $this->content) {
return $this->response->toArray($throw);
}
return $this->content = $this->response->toArray(false);
} finally {
if ($this->event && $this->event->isStarted()) {
$this->event->stop();
}
if ($throw) {
$this->checkStatusCode($this->response->getStatusCode());
}
}
}
public function cancel(): void
{
$this->response->cancel();
if ($this->event && $this->event->isStarted()) {
$this->event->stop();
}
}
public function getInfo(string $type = null)
{
return $this->response->getInfo($type);
}
/**
* Casts the response to a PHP stream resource.
*
* @return resource
*
* @throws TransportExceptionInterface When a network error occurs
* @throws RedirectionExceptionInterface On a 3xx when $throw is true and the "max_redirects" option has been reached
* @throws ClientExceptionInterface On a 4xx when $throw is true
* @throws ServerExceptionInterface On a 5xx when $throw is true
*/
public function toStream(bool $throw = true)
{
if ($throw) {
// Ensure headers arrived
$this->response->getHeaders(true);
}
if ($this->response instanceof StreamableInterface) {
return $this->response->toStream(false);
}
return StreamWrapper::createResource($this->response, $this->client);
}
/**
* @internal
*/
public static function stream(HttpClientInterface $client, iterable $responses, ?float $timeout): \Generator
{
$wrappedResponses = [];
$traceableMap = new \SplObjectStorage();
foreach ($responses as $r) {
if (!$r instanceof self) {
throw new \TypeError(sprintf('"%s::stream()" expects parameter 1 to be an iterable of TraceableResponse objects, "%s" given.', TraceableHttpClient::class, get_debug_type($r)));
}
$traceableMap[$r->response] = $r;
$wrappedResponses[] = $r->response;
if ($r->event && !$r->event->isStarted()) {
$r->event->start();
}
}
foreach ($client->stream($wrappedResponses, $timeout) as $r => $chunk) {
if ($traceableMap[$r]->event && $traceableMap[$r]->event->isStarted()) {
try {
if ($chunk->isTimeout() || !$chunk->isLast()) {
$traceableMap[$r]->event->lap();
} else {
$traceableMap[$r]->event->stop();
}
} catch (TransportExceptionInterface $e) {
$traceableMap[$r]->event->stop();
if ($chunk instanceof ErrorChunk) {
$chunk->didThrow(false);
} else {
$chunk = new ErrorChunk($chunk->getOffset(), $e);
}
}
}
yield $traceableMap[$r] => $chunk;
}
}
private function checkStatusCode(int $code)
{
if (500 <= $code) {
throw new ServerException($this);
}
if (400 <= $code) {
throw new ClientException($this);
}
if (300 <= $code) {
throw new RedirectionException($this);
}
}
}

View file

@ -0,0 +1,315 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\HttpClient\Response;
use Symfony\Component\HttpClient\Chunk\DataChunk;
use Symfony\Component\HttpClient\Chunk\ErrorChunk;
use Symfony\Component\HttpClient\Chunk\FirstChunk;
use Symfony\Component\HttpClient\Chunk\LastChunk;
use Symfony\Component\HttpClient\Exception\TransportException;
use Symfony\Component\HttpClient\Internal\ClientState;
/**
* Implements common logic for transport-level response classes.
*
* @author Nicolas Grekas <p@tchwork.com>
*
* @internal
*/
trait TransportResponseTrait
{
private $headers = [];
private $info = [
'response_headers' => [],
'http_code' => 0,
'error' => null,
'canceled' => false,
];
/** @var object|resource */
private $handle;
private $id;
private $timeout = 0;
private $inflate;
private $finalInfo;
private $canary;
private $logger;
/**
* {@inheritdoc}
*/
public function getStatusCode(): int
{
if ($this->initializer) {
self::initialize($this);
}
return $this->info['http_code'];
}
/**
* {@inheritdoc}
*/
public function getHeaders(bool $throw = true): array
{
if ($this->initializer) {
self::initialize($this);
}
if ($throw) {
$this->checkStatusCode();
}
return $this->headers;
}
/**
* {@inheritdoc}
*/
public function cancel(): void
{
$this->info['canceled'] = true;
$this->info['error'] = 'Response has been canceled.';
$this->close();
}
/**
* Closes the response and all its network handles.
*/
protected function close(): void
{
$this->canary->cancel();
$this->inflate = null;
}
/**
* Adds pending responses to the activity list.
*/
abstract protected static function schedule(self $response, array &$runningResponses): void;
/**
* Performs all pending non-blocking operations.
*/
abstract protected static function perform(ClientState $multi, array &$responses): void;
/**
* Waits for network activity.
*/
abstract protected static function select(ClientState $multi, float $timeout): int;
private static function addResponseHeaders(array $responseHeaders, array &$info, array &$headers, string &$debug = ''): void
{
foreach ($responseHeaders as $h) {
if (11 <= \strlen($h) && '/' === $h[4] && preg_match('#^HTTP/\d+(?:\.\d+)? ([1-9]\d\d)(?: |$)#', $h, $m)) {
if ($headers) {
$debug .= "< \r\n";
$headers = [];
}
$info['http_code'] = (int) $m[1];
} elseif (2 === \count($m = explode(':', $h, 2))) {
$headers[strtolower($m[0])][] = ltrim($m[1]);
}
$debug .= "< {$h}\r\n";
$info['response_headers'][] = $h;
}
$debug .= "< \r\n";
if (!$info['http_code']) {
throw new TransportException(sprintf('Invalid or missing HTTP status line for "%s".', implode('', $info['url'])));
}
}
/**
* Ensures the request is always sent and that the response code was checked.
*/
private function doDestruct()
{
$this->shouldBuffer = true;
if ($this->initializer && null === $this->info['error']) {
self::initialize($this, -0.0);
$this->checkStatusCode();
}
}
/**
* Implements an event loop based on a buffer activity queue.
*
* @internal
*/
public static function stream(iterable $responses, float $timeout = null): \Generator
{
$runningResponses = [];
foreach ($responses as $response) {
self::schedule($response, $runningResponses);
}
$lastActivity = microtime(true);
$elapsedTimeout = 0;
if ($fromLastTimeout = 0.0 === $timeout && '-0' === (string) $timeout) {
$timeout = null;
} elseif ($fromLastTimeout = 0 > $timeout) {
$timeout = -$timeout;
}
while (true) {
$hasActivity = false;
$timeoutMax = 0;
$timeoutMin = $timeout ?? \INF;
/** @var ClientState $multi */
foreach ($runningResponses as $i => [$multi]) {
$responses = &$runningResponses[$i][1];
self::perform($multi, $responses);
foreach ($responses as $j => $response) {
$timeoutMax = $timeout ?? max($timeoutMax, $response->timeout);
$timeoutMin = min($timeoutMin, $response->timeout, 1);
if ($fromLastTimeout && null !== $multi->lastTimeout) {
$elapsedTimeout = microtime(true) - $multi->lastTimeout;
}
$chunk = false;
if (isset($multi->handlesActivity[$j])) {
$multi->lastTimeout = null;
} elseif (!isset($multi->openHandles[$j])) {
unset($responses[$j]);
continue;
} elseif ($elapsedTimeout >= $timeoutMax) {
$multi->handlesActivity[$j] = [new ErrorChunk($response->offset, sprintf('Idle timeout reached for "%s".', $response->getInfo('url')))];
$multi->lastTimeout ?? $multi->lastTimeout = $lastActivity;
} else {
continue;
}
while ($multi->handlesActivity[$j] ?? false) {
$hasActivity = true;
$elapsedTimeout = 0;
if (\is_string($chunk = array_shift($multi->handlesActivity[$j]))) {
if (null !== $response->inflate && false === $chunk = @inflate_add($response->inflate, $chunk)) {
$multi->handlesActivity[$j] = [null, new TransportException(sprintf('Error while processing content unencoding for "%s".', $response->getInfo('url')))];
continue;
}
if ('' !== $chunk && null !== $response->content && \strlen($chunk) !== fwrite($response->content, $chunk)) {
$multi->handlesActivity[$j] = [null, new TransportException(sprintf('Failed writing %d bytes to the response buffer.', \strlen($chunk)))];
continue;
}
$chunkLen = \strlen($chunk);
$chunk = new DataChunk($response->offset, $chunk);
$response->offset += $chunkLen;
} elseif (null === $chunk) {
$e = $multi->handlesActivity[$j][0];
unset($responses[$j], $multi->handlesActivity[$j]);
$response->close();
if (null !== $e) {
$response->info['error'] = $e->getMessage();
if ($e instanceof \Error) {
throw $e;
}
$chunk = new ErrorChunk($response->offset, $e);
} else {
if (0 === $response->offset && null === $response->content) {
$response->content = fopen('php://memory', 'w+');
}
$chunk = new LastChunk($response->offset);
}
} elseif ($chunk instanceof ErrorChunk) {
unset($responses[$j]);
$elapsedTimeout = $timeoutMax;
} elseif ($chunk instanceof FirstChunk) {
if ($response->logger) {
$info = $response->getInfo();
$response->logger->info(sprintf('Response: "%s %s"', $info['http_code'], $info['url']));
}
$response->inflate = \extension_loaded('zlib') && $response->inflate && 'gzip' === ($response->headers['content-encoding'][0] ?? null) ? inflate_init(\ZLIB_ENCODING_GZIP) : null;
if ($response->shouldBuffer instanceof \Closure) {
try {
$response->shouldBuffer = ($response->shouldBuffer)($response->headers);
if (null !== $response->info['error']) {
throw new TransportException($response->info['error']);
}
} catch (\Throwable $e) {
$response->close();
$multi->handlesActivity[$j] = [null, $e];
}
}
if (true === $response->shouldBuffer) {
$response->content = fopen('php://temp', 'w+');
} elseif (\is_resource($response->shouldBuffer)) {
$response->content = $response->shouldBuffer;
}
$response->shouldBuffer = null;
yield $response => $chunk;
if ($response->initializer && null === $response->info['error']) {
// Ensure the HTTP status code is always checked
$response->getHeaders(true);
}
continue;
}
yield $response => $chunk;
}
unset($multi->handlesActivity[$j]);
if ($chunk instanceof ErrorChunk && !$chunk->didThrow()) {
// Ensure transport exceptions are always thrown
$chunk->getContent();
}
}
if (!$responses) {
unset($runningResponses[$i]);
}
// Prevent memory leaks
$multi->handlesActivity = $multi->handlesActivity ?: [];
$multi->openHandles = $multi->openHandles ?: [];
}
if (!$runningResponses) {
break;
}
if ($hasActivity) {
$lastActivity = microtime(true);
continue;
}
if (-1 === self::select($multi, min($timeoutMin, $timeoutMax - $elapsedTimeout))) {
usleep(min(500, 1E6 * $timeoutMin));
}
$elapsedTimeout = microtime(true) - $lastActivity;
}
}
}

View file

@ -0,0 +1,115 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\HttpClient\Retry;
use Symfony\Component\HttpClient\Exception\InvalidArgumentException;
use Symfony\Component\HttpClient\Response\AsyncContext;
use Symfony\Contracts\HttpClient\Exception\TransportExceptionInterface;
/**
* Decides to retry the request when HTTP status codes belong to the given list of codes.
*
* @author Jérémy Derussé <jeremy@derusse.com>
*/
class GenericRetryStrategy implements RetryStrategyInterface
{
public const IDEMPOTENT_METHODS = ['GET', 'HEAD', 'PUT', 'DELETE', 'OPTIONS', 'TRACE'];
public const DEFAULT_RETRY_STATUS_CODES = [
0 => self::IDEMPOTENT_METHODS, // for transport exceptions
423,
425,
429,
500 => self::IDEMPOTENT_METHODS,
502,
503,
504 => self::IDEMPOTENT_METHODS,
507 => self::IDEMPOTENT_METHODS,
510 => self::IDEMPOTENT_METHODS,
];
private $statusCodes;
private $delayMs;
private $multiplier;
private $maxDelayMs;
private $jitter;
/**
* @param array $statusCodes List of HTTP status codes that trigger a retry
* @param int $delayMs Amount of time to delay (or the initial value when multiplier is used)
* @param float $multiplier Multiplier to apply to the delay each time a retry occurs
* @param int $maxDelayMs Maximum delay to allow (0 means no maximum)
* @param float $jitter Probability of randomness int delay (0 = none, 1 = 100% random)
*/
public function __construct(array $statusCodes = self::DEFAULT_RETRY_STATUS_CODES, int $delayMs = 1000, float $multiplier = 2.0, int $maxDelayMs = 0, float $jitter = 0.1)
{
$this->statusCodes = $statusCodes;
if ($delayMs < 0) {
throw new InvalidArgumentException(sprintf('Delay must be greater than or equal to zero: "%s" given.', $delayMs));
}
$this->delayMs = $delayMs;
if ($multiplier < 1) {
throw new InvalidArgumentException(sprintf('Multiplier must be greater than or equal to one: "%s" given.', $multiplier));
}
$this->multiplier = $multiplier;
if ($maxDelayMs < 0) {
throw new InvalidArgumentException(sprintf('Max delay must be greater than or equal to zero: "%s" given.', $maxDelayMs));
}
$this->maxDelayMs = $maxDelayMs;
if ($jitter < 0 || $jitter > 1) {
throw new InvalidArgumentException(sprintf('Jitter must be between 0 and 1: "%s" given.', $jitter));
}
$this->jitter = $jitter;
}
public function shouldRetry(AsyncContext $context, ?string $responseContent, ?TransportExceptionInterface $exception): ?bool
{
$statusCode = $context->getStatusCode();
if (\in_array($statusCode, $this->statusCodes, true)) {
return true;
}
if (isset($this->statusCodes[$statusCode]) && \is_array($this->statusCodes[$statusCode])) {
return \in_array($context->getInfo('http_method'), $this->statusCodes[$statusCode], true);
}
if (null === $exception) {
return false;
}
if (\in_array(0, $this->statusCodes, true)) {
return true;
}
if (isset($this->statusCodes[0]) && \is_array($this->statusCodes[0])) {
return \in_array($context->getInfo('http_method'), $this->statusCodes[0], true);
}
return false;
}
public function getDelay(AsyncContext $context, ?string $responseContent, ?TransportExceptionInterface $exception): int
{
$delay = $this->delayMs * $this->multiplier ** $context->getInfo('retry_count');
if ($this->jitter > 0) {
$randomness = $delay * $this->jitter;
$delay = $delay + random_int(-$randomness, +$randomness);
}
if ($delay > $this->maxDelayMs && 0 !== $this->maxDelayMs) {
return $this->maxDelayMs;
}
return (int) $delay;
}
}

View file

@ -0,0 +1,36 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\HttpClient\Retry;
use Symfony\Component\HttpClient\Response\AsyncContext;
use Symfony\Contracts\HttpClient\Exception\TransportExceptionInterface;
/**
* @author Jérémy Derussé <jeremy@derusse.com>
* @author Nicolas Grekas <p@tchwork.com>
*/
interface RetryStrategyInterface
{
/**
* Returns whether the request should be retried.
*
* @param ?string $responseContent Null is passed when the body did not arrive yet
*
* @return ?bool Returns null to signal that the body is required to take a decision
*/
public function shouldRetry(AsyncContext $context, ?string $responseContent, ?TransportExceptionInterface $exception): ?bool;
/**
* Returns the time to wait in milliseconds.
*/
public function getDelay(AsyncContext $context, ?string $responseContent, ?TransportExceptionInterface $exception): int;
}

View file

@ -0,0 +1,168 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\HttpClient;
use Psr\Log\LoggerInterface;
use Psr\Log\NullLogger;
use Symfony\Component\HttpClient\Response\AsyncContext;
use Symfony\Component\HttpClient\Response\AsyncResponse;
use Symfony\Component\HttpClient\Retry\GenericRetryStrategy;
use Symfony\Component\HttpClient\Retry\RetryStrategyInterface;
use Symfony\Contracts\HttpClient\ChunkInterface;
use Symfony\Contracts\HttpClient\Exception\TransportExceptionInterface;
use Symfony\Contracts\HttpClient\HttpClientInterface;
use Symfony\Contracts\HttpClient\ResponseInterface;
/**
* Automatically retries failing HTTP requests.
*
* @author Jérémy Derussé <jeremy@derusse.com>
*/
class RetryableHttpClient implements HttpClientInterface
{
use AsyncDecoratorTrait;
private $strategy;
private $maxRetries;
private $logger;
/**
* @param int $maxRetries The maximum number of times to retry
*/
public function __construct(HttpClientInterface $client, RetryStrategyInterface $strategy = null, int $maxRetries = 3, LoggerInterface $logger = null)
{
$this->client = $client;
$this->strategy = $strategy ?? new GenericRetryStrategy();
$this->maxRetries = $maxRetries;
$this->logger = $logger ?? new NullLogger();
}
public function request(string $method, string $url, array $options = []): ResponseInterface
{
if ($this->maxRetries <= 0) {
return new AsyncResponse($this->client, $method, $url, $options);
}
$retryCount = 0;
$content = '';
$firstChunk = null;
return new AsyncResponse($this->client, $method, $url, $options, function (ChunkInterface $chunk, AsyncContext $context) use ($method, $url, $options, &$retryCount, &$content, &$firstChunk) {
$exception = null;
try {
if ($chunk->isTimeout() || null !== $chunk->getInformationalStatus() || $context->getInfo('canceled')) {
yield $chunk;
return;
}
} catch (TransportExceptionInterface $exception) {
// catch TransportExceptionInterface to send it to the strategy
}
if (null !== $exception) {
// always retry request that fail to resolve DNS
if ('' !== $context->getInfo('primary_ip')) {
$shouldRetry = $this->strategy->shouldRetry($context, null, $exception);
if (null === $shouldRetry) {
throw new \LogicException(sprintf('The "%s::shouldRetry()" method must not return null when called with an exception.', \get_class($this->decider)));
}
if (false === $shouldRetry) {
yield from $this->passthru($context, $firstChunk, $content, $chunk);
return;
}
}
} elseif ($chunk->isFirst()) {
if (false === $shouldRetry = $this->strategy->shouldRetry($context, null, null)) {
yield from $this->passthru($context, $firstChunk, $content, $chunk);
return;
}
// Body is needed to decide
if (null === $shouldRetry) {
$firstChunk = $chunk;
$content = '';
return;
}
} else {
if (!$chunk->isLast()) {
$content .= $chunk->getContent();
return;
}
if (null === $shouldRetry = $this->strategy->shouldRetry($context, $content, null)) {
throw new \LogicException(sprintf('The "%s::shouldRetry()" method must not return null when called with a body.', \get_class($this->strategy)));
}
if (false === $shouldRetry) {
yield from $this->passthru($context, $firstChunk, $content, $chunk);
return;
}
}
$context->getResponse()->cancel();
$delay = $this->getDelayFromHeader($context->getHeaders()) ?? $this->strategy->getDelay($context, !$exception && $chunk->isLast() ? $content : null, $exception);
++$retryCount;
$this->logger->info('Try #{count} after {delay}ms'.($exception ? ': '.$exception->getMessage() : ', status code: '.$context->getStatusCode()), [
'count' => $retryCount,
'delay' => $delay,
]);
$context->setInfo('retry_count', $retryCount);
$context->replaceRequest($method, $url, $options);
$context->pause($delay / 1000);
if ($retryCount >= $this->maxRetries) {
$context->passthru();
}
});
}
private function getDelayFromHeader(array $headers): ?int
{
if (null !== $after = $headers['retry-after'][0] ?? null) {
if (is_numeric($after)) {
return (int) $after * 1000;
}
if (false !== $time = strtotime($after)) {
return max(0, $time - time()) * 1000;
}
}
return null;
}
private function passthru(AsyncContext $context, ?ChunkInterface $firstChunk, string &$content, ChunkInterface $lastChunk): \Generator
{
$context->passthru();
if (null !== $firstChunk) {
yield $firstChunk;
}
if ('' !== $content) {
$chunk = $context->createChunk($content);
$content = '';
yield $chunk;
}
yield $lastChunk;
}
}

View file

@ -0,0 +1,131 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\HttpClient;
use Psr\Log\LoggerAwareInterface;
use Psr\Log\LoggerInterface;
use Symfony\Component\HttpClient\Exception\InvalidArgumentException;
use Symfony\Contracts\HttpClient\HttpClientInterface;
use Symfony\Contracts\HttpClient\ResponseInterface;
use Symfony\Contracts\HttpClient\ResponseStreamInterface;
use Symfony\Contracts\Service\ResetInterface;
/**
* Auto-configure the default options based on the requested URL.
*
* @author Anthony Martin <anthony.martin@sensiolabs.com>
*/
class ScopingHttpClient implements HttpClientInterface, ResetInterface, LoggerAwareInterface
{
use HttpClientTrait;
private $client;
private $defaultOptionsByRegexp;
private $defaultRegexp;
public function __construct(HttpClientInterface $client, array $defaultOptionsByRegexp, string $defaultRegexp = null)
{
$this->client = $client;
$this->defaultOptionsByRegexp = $defaultOptionsByRegexp;
$this->defaultRegexp = $defaultRegexp;
if (null !== $defaultRegexp && !isset($defaultOptionsByRegexp[$defaultRegexp])) {
throw new InvalidArgumentException(sprintf('No options are mapped to the provided "%s" default regexp.', $defaultRegexp));
}
}
public static function forBaseUri(HttpClientInterface $client, string $baseUri, array $defaultOptions = [], string $regexp = null): self
{
if (null === $regexp) {
$regexp = preg_quote(implode('', self::resolveUrl(self::parseUrl('.'), self::parseUrl($baseUri))));
}
$defaultOptions['base_uri'] = $baseUri;
return new self($client, [$regexp => $defaultOptions], $regexp);
}
/**
* {@inheritdoc}
*/
public function request(string $method, string $url, array $options = []): ResponseInterface
{
$e = null;
$url = self::parseUrl($url, $options['query'] ?? []);
if (\is_string($options['base_uri'] ?? null)) {
$options['base_uri'] = self::parseUrl($options['base_uri']);
}
try {
$url = implode('', self::resolveUrl($url, $options['base_uri'] ?? null));
} catch (InvalidArgumentException $e) {
if (null === $this->defaultRegexp) {
throw $e;
}
$defaultOptions = $this->defaultOptionsByRegexp[$this->defaultRegexp];
$options = self::mergeDefaultOptions($options, $defaultOptions, true);
if (\is_string($options['base_uri'] ?? null)) {
$options['base_uri'] = self::parseUrl($options['base_uri']);
}
$url = implode('', self::resolveUrl($url, $options['base_uri'] ?? null, $defaultOptions['query'] ?? []));
}
foreach ($this->defaultOptionsByRegexp as $regexp => $defaultOptions) {
if (preg_match("{{$regexp}}A", $url)) {
if (null === $e || $regexp !== $this->defaultRegexp) {
$options = self::mergeDefaultOptions($options, $defaultOptions, true);
}
break;
}
}
return $this->client->request($method, $url, $options);
}
/**
* {@inheritdoc}
*/
public function stream($responses, float $timeout = null): ResponseStreamInterface
{
return $this->client->stream($responses, $timeout);
}
public function reset()
{
if ($this->client instanceof ResetInterface) {
$this->client->reset();
}
}
/**
* {@inheritdoc}
*/
public function setLogger(LoggerInterface $logger): void
{
if ($this->client instanceof LoggerAwareInterface) {
$this->client->setLogger($logger);
}
}
/**
* {@inheritdoc}
*/
public function withOptions(array $options): self
{
$clone = clone $this;
$clone->client = $this->client->withOptions($options);
return $clone;
}
}

View file

@ -0,0 +1,119 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\HttpClient;
use Psr\Log\LoggerAwareInterface;
use Psr\Log\LoggerInterface;
use Symfony\Component\HttpClient\Response\ResponseStream;
use Symfony\Component\HttpClient\Response\TraceableResponse;
use Symfony\Component\Stopwatch\Stopwatch;
use Symfony\Contracts\HttpClient\HttpClientInterface;
use Symfony\Contracts\HttpClient\ResponseInterface;
use Symfony\Contracts\HttpClient\ResponseStreamInterface;
use Symfony\Contracts\Service\ResetInterface;
/**
* @author Jérémy Romey <jeremy@free-agent.fr>
*/
final class TraceableHttpClient implements HttpClientInterface, ResetInterface, LoggerAwareInterface
{
private $client;
private $tracedRequests = [];
private $stopwatch;
public function __construct(HttpClientInterface $client, Stopwatch $stopwatch = null)
{
$this->client = $client;
$this->stopwatch = $stopwatch;
}
/**
* {@inheritdoc}
*/
public function request(string $method, string $url, array $options = []): ResponseInterface
{
$content = null;
$traceInfo = [];
$this->tracedRequests[] = [
'method' => $method,
'url' => $url,
'options' => $options,
'info' => &$traceInfo,
'content' => &$content,
];
$onProgress = $options['on_progress'] ?? null;
if (false === ($options['extra']['trace_content'] ?? true)) {
unset($content);
$content = false;
}
$options['on_progress'] = function (int $dlNow, int $dlSize, array $info) use (&$traceInfo, $onProgress) {
$traceInfo = $info;
if (null !== $onProgress) {
$onProgress($dlNow, $dlSize, $info);
}
};
return new TraceableResponse($this->client, $this->client->request($method, $url, $options), $content, null === $this->stopwatch ? null : $this->stopwatch->start("$method $url", 'http_client'));
}
/**
* {@inheritdoc}
*/
public function stream($responses, float $timeout = null): ResponseStreamInterface
{
if ($responses instanceof TraceableResponse) {
$responses = [$responses];
} elseif (!is_iterable($responses)) {
throw new \TypeError(sprintf('"%s()" expects parameter 1 to be an iterable of TraceableResponse objects, "%s" given.', __METHOD__, get_debug_type($responses)));
}
return new ResponseStream(TraceableResponse::stream($this->client, $responses, $timeout));
}
public function getTracedRequests(): array
{
return $this->tracedRequests;
}
public function reset()
{
if ($this->client instanceof ResetInterface) {
$this->client->reset();
}
$this->tracedRequests = [];
}
/**
* {@inheritdoc}
*/
public function setLogger(LoggerInterface $logger): void
{
if ($this->client instanceof LoggerAwareInterface) {
$this->client->setLogger($logger);
}
}
/**
* {@inheritdoc}
*/
public function withOptions(array $options): self
{
$clone = clone $this;
$clone->client = $this->client->withOptions($options);
return $clone;
}
}

View file

@ -0,0 +1,53 @@
{
"name": "symfony/http-client",
"type": "library",
"description": "Provides powerful methods to fetch HTTP resources synchronously or asynchronously",
"homepage": "https://symfony.com",
"license": "MIT",
"authors": [
{
"name": "Nicolas Grekas",
"email": "p@tchwork.com"
},
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"provide": {
"php-http/async-client-implementation": "*",
"php-http/client-implementation": "*",
"psr/http-client-implementation": "1.0",
"symfony/http-client-implementation": "2.4"
},
"require": {
"php": ">=7.2.5",
"psr/log": "^1|^2|^3",
"symfony/deprecation-contracts": "^2.1",
"symfony/http-client-contracts": "^2.4",
"symfony/polyfill-php73": "^1.11",
"symfony/polyfill-php80": "^1.16",
"symfony/service-contracts": "^1.0|^2"
},
"require-dev": {
"amphp/amp": "^2.5",
"amphp/http-client": "^4.2.1",
"amphp/http-tunnel": "^1.0",
"amphp/socket": "^1.1",
"guzzlehttp/promises": "^1.4",
"nyholm/psr7": "^1.0",
"php-http/httplug": "^1.0|^2.0",
"psr/http-client": "^1.0",
"symfony/dependency-injection": "^4.4|^5.0",
"symfony/http-kernel": "^4.4.13|^5.1.5",
"symfony/process": "^4.4|^5.0",
"symfony/stopwatch": "^4.4|^5.0"
},
"autoload": {
"psr-4": { "Symfony\\Component\\HttpClient\\": "" },
"exclude-from-classmap": [
"/Tests/"
]
},
"minimum-stability": "dev"
}

169
vendor/symfony/translation/CHANGELOG.md vendored Normal file
View file

@ -0,0 +1,169 @@
CHANGELOG
=========
5.3
---
* Add `translation:pull` and `translation:push` commands to manage translations with third-party providers
* Add `TranslatorBagInterface::getCatalogues` method
* Add support to load XLIFF string in `XliffFileLoader`
5.2.0
-----
* added support for calling `trans` with ICU formatted messages
* added `PseudoLocalizationTranslator`
* added `TranslatableMessage` objects that represent a message that can be translated
* added the `t()` function to easily create `TranslatableMessage` objects
* Added support for extracting messages from `TranslatableMessage` objects
5.1.0
-----
* added support for `name` attribute on `unit` element from xliff2 to be used as a translation key instead of always the `source` element
5.0.0
-----
* removed support for using `null` as the locale in `Translator`
* removed `TranslatorInterface`
* removed `MessageSelector`
* removed `ChoiceMessageFormatterInterface`
* removed `PluralizationRule`
* removed `Interval`
* removed `transChoice()` methods, use the trans() method instead with a %count% parameter
* removed `FileDumper::setBackup()` and `TranslationWriter::disableBackup()`
* removed `MessageFormatter::choiceFormat()`
* added argument `$filename` to `PhpExtractor::parseTokens()`
* removed support for implicit STDIN usage in the `lint:xliff` command, use `lint:xliff -` (append a dash) instead to make it explicit.
4.4.0
-----
* deprecated support for using `null` as the locale in `Translator`
* deprecated accepting STDIN implicitly when using the `lint:xliff` command, use `lint:xliff -` (append a dash) instead to make it explicit.
* Marked the `TranslationDataCollector` class as `@final`.
4.3.0
-----
* Improved Xliff 1.2 loader to load the original file's metadata
* Added `TranslatorPathsPass`
4.2.0
-----
* Started using ICU parent locales as fallback locales.
* allow using the ICU message format using domains with the "+intl-icu" suffix
* deprecated `Translator::transChoice()` in favor of using `Translator::trans()` with a `%count%` parameter
* deprecated `TranslatorInterface` in favor of `Symfony\Contracts\Translation\TranslatorInterface`
* deprecated `MessageSelector`, `Interval` and `PluralizationRules`; use `IdentityTranslator` instead
* Added `IntlFormatter` and `IntlFormatterInterface`
* added support for multiple files and directories in `XliffLintCommand`
* Marked `Translator::getFallbackLocales()` and `TranslationDataCollector::getFallbackLocales()` as internal
4.1.0
-----
* The `FileDumper::setBackup()` method is deprecated.
* The `TranslationWriter::disableBackup()` method is deprecated.
* The `XliffFileDumper` will write "name" on the "unit" node when dumping XLIFF 2.0.
4.0.0
-----
* removed the backup feature of the `FileDumper` class
* removed `TranslationWriter::writeTranslations()` method
* removed support for passing `MessageSelector` instances to the constructor of the `Translator` class
3.4.0
-----
* Added `TranslationDumperPass`
* Added `TranslationExtractorPass`
* Added `TranslatorPass`
* Added `TranslationReader` and `TranslationReaderInterface`
* Added `<notes>` section to the Xliff 2.0 dumper.
* Improved Xliff 2.0 loader to load `<notes>` section.
* Added `TranslationWriterInterface`
* Deprecated `TranslationWriter::writeTranslations` in favor of `TranslationWriter::write`
* added support for adding custom message formatter and decoupling the default one.
* Added `PhpExtractor`
* Added `PhpStringTokenParser`
3.2.0
-----
* Added support for escaping `|` in plural translations with double pipe.
3.1.0
-----
* Deprecated the backup feature of the file dumper classes.
3.0.0
-----
* removed `FileDumper::format()` method.
* Changed the visibility of the locale property in `Translator` from protected to private.
2.8.0
-----
* deprecated FileDumper::format(), overwrite FileDumper::formatCatalogue() instead.
* deprecated Translator::getMessages(), rely on TranslatorBagInterface::getCatalogue() instead.
* added `FileDumper::formatCatalogue` which allows format the catalogue without dumping it into file.
* added option `json_encoding` to JsonFileDumper
* added options `as_tree`, `inline` to YamlFileDumper
* added support for XLIFF 2.0.
* added support for XLIFF target and tool attributes.
* added message parameters to DataCollectorTranslator.
* [DEPRECATION] The `DiffOperation` class has been deprecated and
will be removed in Symfony 3.0, since its operation has nothing to do with 'diff',
so the class name is misleading. The `TargetOperation` class should be used for
this use-case instead.
2.7.0
-----
* added DataCollectorTranslator for collecting the translated messages.
2.6.0
-----
* added possibility to cache catalogues
* added TranslatorBagInterface
* added LoggingTranslator
* added Translator::getMessages() for retrieving the message catalogue as an array
2.5.0
-----
* added relative file path template to the file dumpers
* added optional backup to the file dumpers
* changed IcuResFileDumper to extend FileDumper
2.3.0
-----
* added classes to make operations on catalogues (like making a diff or a merge on 2 catalogues)
* added Translator::getFallbackLocales()
* deprecated Translator::setFallbackLocale() in favor of the new Translator::setFallbackLocales() method
2.2.0
-----
* QtTranslationsLoader class renamed to QtFileLoader. QtTranslationsLoader is deprecated and will be removed in 2.3.
* [BC BREAK] uniformized the exception thrown by the load() method when an error occurs. The load() method now
throws Symfony\Component\Translation\Exception\NotFoundResourceException when a resource cannot be found
and Symfony\Component\Translation\Exception\InvalidResourceException when a resource is invalid.
* changed the exception class thrown by some load() methods from \RuntimeException to \InvalidArgumentException
(IcuDatFileLoader, IcuResFileLoader and QtFileLoader)
2.1.0
-----
* added support for more than one fallback locale
* added support for extracting translation messages from templates (Twig and PHP)
* added dumpers for translation catalogs
* added support for QT, gettext, and ResourceBundles

View file

@ -0,0 +1,192 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Translation\Catalogue;
use Symfony\Component\Translation\Exception\InvalidArgumentException;
use Symfony\Component\Translation\Exception\LogicException;
use Symfony\Component\Translation\MessageCatalogue;
use Symfony\Component\Translation\MessageCatalogueInterface;
/**
* Base catalogues binary operation class.
*
* A catalogue binary operation performs operation on
* source (the left argument) and target (the right argument) catalogues.
*
* @author Jean-François Simon <contact@jfsimon.fr>
*/
abstract class AbstractOperation implements OperationInterface
{
public const OBSOLETE_BATCH = 'obsolete';
public const NEW_BATCH = 'new';
public const ALL_BATCH = 'all';
protected $source;
protected $target;
protected $result;
/**
* @var array|null The domains affected by this operation
*/
private $domains;
/**
* This array stores 'all', 'new' and 'obsolete' messages for all valid domains.
*
* The data structure of this array is as follows:
*
* [
* 'domain 1' => [
* 'all' => [...],
* 'new' => [...],
* 'obsolete' => [...]
* ],
* 'domain 2' => [
* 'all' => [...],
* 'new' => [...],
* 'obsolete' => [...]
* ],
* ...
* ]
*
* @var array The array that stores 'all', 'new' and 'obsolete' messages
*/
protected $messages;
/**
* @throws LogicException
*/
public function __construct(MessageCatalogueInterface $source, MessageCatalogueInterface $target)
{
if ($source->getLocale() !== $target->getLocale()) {
throw new LogicException('Operated catalogues must belong to the same locale.');
}
$this->source = $source;
$this->target = $target;
$this->result = new MessageCatalogue($source->getLocale());
$this->messages = [];
}
/**
* {@inheritdoc}
*/
public function getDomains()
{
if (null === $this->domains) {
$this->domains = array_values(array_unique(array_merge($this->source->getDomains(), $this->target->getDomains())));
}
return $this->domains;
}
/**
* {@inheritdoc}
*/
public function getMessages(string $domain)
{
if (!\in_array($domain, $this->getDomains())) {
throw new InvalidArgumentException(sprintf('Invalid domain: "%s".', $domain));
}
if (!isset($this->messages[$domain][self::ALL_BATCH])) {
$this->processDomain($domain);
}
return $this->messages[$domain][self::ALL_BATCH];
}
/**
* {@inheritdoc}
*/
public function getNewMessages(string $domain)
{
if (!\in_array($domain, $this->getDomains())) {
throw new InvalidArgumentException(sprintf('Invalid domain: "%s".', $domain));
}
if (!isset($this->messages[$domain][self::NEW_BATCH])) {
$this->processDomain($domain);
}
return $this->messages[$domain][self::NEW_BATCH];
}
/**
* {@inheritdoc}
*/
public function getObsoleteMessages(string $domain)
{
if (!\in_array($domain, $this->getDomains())) {
throw new InvalidArgumentException(sprintf('Invalid domain: "%s".', $domain));
}
if (!isset($this->messages[$domain][self::OBSOLETE_BATCH])) {
$this->processDomain($domain);
}
return $this->messages[$domain][self::OBSOLETE_BATCH];
}
/**
* {@inheritdoc}
*/
public function getResult()
{
foreach ($this->getDomains() as $domain) {
if (!isset($this->messages[$domain])) {
$this->processDomain($domain);
}
}
return $this->result;
}
/**
* @param self::*_BATCH $batch
*/
public function moveMessagesToIntlDomainsIfPossible(string $batch = self::ALL_BATCH): void
{
// If MessageFormatter class does not exists, intl domains are not supported.
if (!class_exists(\MessageFormatter::class)) {
return;
}
foreach ($this->getDomains() as $domain) {
$intlDomain = $domain.MessageCatalogueInterface::INTL_DOMAIN_SUFFIX;
switch ($batch) {
case self::OBSOLETE_BATCH: $messages = $this->getObsoleteMessages($domain); break;
case self::NEW_BATCH: $messages = $this->getNewMessages($domain); break;
case self::ALL_BATCH: $messages = $this->getMessages($domain); break;
default: throw new \InvalidArgumentException(sprintf('$batch argument must be one of ["%s", "%s", "%s"].', self::ALL_BATCH, self::NEW_BATCH, self::OBSOLETE_BATCH));
}
if (!$messages || (!$this->source->all($intlDomain) && $this->source->all($domain))) {
continue;
}
$result = $this->getResult();
$allIntlMessages = $result->all($intlDomain);
$currentMessages = array_diff_key($messages, $result->all($domain));
$result->replace($currentMessages, $domain);
$result->replace($allIntlMessages + $messages, $intlDomain);
}
}
/**
* Performs operation on source and target catalogues for the given domain and
* stores the results.
*
* @param string $domain The domain which the operation will be performed for
*/
abstract protected function processDomain(string $domain);
}

View file

@ -0,0 +1,60 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Translation\Catalogue;
use Symfony\Component\Translation\MessageCatalogueInterface;
/**
* Merge operation between two catalogues as follows:
* all = source target = {x: x source x target}
* new = all source = {x: x target x source}
* obsolete = source all = {x: x source x source x target} =
* Basically, the result contains messages from both catalogues.
*
* @author Jean-François Simon <contact@jfsimon.fr>
*/
class MergeOperation extends AbstractOperation
{
/**
* {@inheritdoc}
*/
protected function processDomain(string $domain)
{
$this->messages[$domain] = [
'all' => [],
'new' => [],
'obsolete' => [],
];
$intlDomain = $domain.MessageCatalogueInterface::INTL_DOMAIN_SUFFIX;
foreach ($this->source->all($domain) as $id => $message) {
$this->messages[$domain]['all'][$id] = $message;
$d = $this->source->defines($id, $intlDomain) ? $intlDomain : $domain;
$this->result->add([$id => $message], $d);
if (null !== $keyMetadata = $this->source->getMetadata($id, $d)) {
$this->result->setMetadata($id, $keyMetadata, $d);
}
}
foreach ($this->target->all($domain) as $id => $message) {
if (!$this->source->has($id, $domain)) {
$this->messages[$domain]['all'][$id] = $message;
$this->messages[$domain]['new'][$id] = $message;
$d = $this->target->defines($id, $intlDomain) ? $intlDomain : $domain;
$this->result->add([$id => $message], $d);
if (null !== $keyMetadata = $this->target->getMetadata($id, $d)) {
$this->result->setMetadata($id, $keyMetadata, $d);
}
}
}
}
}

View file

@ -0,0 +1,71 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Translation\Catalogue;
use Symfony\Component\Translation\MessageCatalogueInterface;
/**
* Represents an operation on catalogue(s).
*
* An instance of this interface performs an operation on one or more catalogues and
* stores intermediate and final results of the operation.
*
* The first catalogue in its argument(s) is called the 'source catalogue' or 'source' and
* the following results are stored:
*
* Messages: also called 'all', are valid messages for the given domain after the operation is performed.
*
* New Messages: also called 'new' (new = all source = {x: x all x source}).
*
* Obsolete Messages: also called 'obsolete' (obsolete = source all = {x: x source x all}).
*
* Result: also called 'result', is the resulting catalogue for the given domain that holds the same messages as 'all'.
*
* @author Jean-François Simon <jeanfrancois.simon@sensiolabs.com>
*/
interface OperationInterface
{
/**
* Returns domains affected by operation.
*
* @return array
*/
public function getDomains();
/**
* Returns all valid messages ('all') after operation.
*
* @return array
*/
public function getMessages(string $domain);
/**
* Returns new messages ('new') after operation.
*
* @return array
*/
public function getNewMessages(string $domain);
/**
* Returns obsolete messages ('obsolete') after operation.
*
* @return array
*/
public function getObsoleteMessages(string $domain);
/**
* Returns resulting catalogue ('result').
*
* @return MessageCatalogueInterface
*/
public function getResult();
}

View file

@ -0,0 +1,74 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Translation\Catalogue;
use Symfony\Component\Translation\MessageCatalogueInterface;
/**
* Target operation between two catalogues:
* intersection = source target = {x: x source x target}
* all = intersection (target intersection) = target
* new = all source = {x: x target x source}
* obsolete = source all = source target = {x: x source x target}
* Basically, the result contains messages from the target catalogue.
*
* @author Michael Lee <michael.lee@zerustech.com>
*/
class TargetOperation extends AbstractOperation
{
/**
* {@inheritdoc}
*/
protected function processDomain(string $domain)
{
$this->messages[$domain] = [
'all' => [],
'new' => [],
'obsolete' => [],
];
$intlDomain = $domain.MessageCatalogueInterface::INTL_DOMAIN_SUFFIX;
// For 'all' messages, the code can't be simplified as ``$this->messages[$domain]['all'] = $target->all($domain);``,
// because doing so will drop messages like {x: x ∈ source ∧ x ∉ target.all ∧ x ∈ target.fallback}
//
// For 'new' messages, the code can't be simplified as ``array_diff_assoc($this->target->all($domain), $this->source->all($domain));``
// because doing so will not exclude messages like {x: x ∈ target ∧ x ∉ source.all ∧ x ∈ source.fallback}
//
// For 'obsolete' messages, the code can't be simplified as ``array_diff_assoc($this->source->all($domain), $this->target->all($domain))``
// because doing so will not exclude messages like {x: x ∈ source ∧ x ∉ target.all ∧ x ∈ target.fallback}
foreach ($this->source->all($domain) as $id => $message) {
if ($this->target->has($id, $domain)) {
$this->messages[$domain]['all'][$id] = $message;
$d = $this->target->defines($id, $intlDomain) ? $intlDomain : $domain;
$this->result->add([$id => $message], $d);
if (null !== $keyMetadata = $this->source->getMetadata($id, $d)) {
$this->result->setMetadata($id, $keyMetadata, $d);
}
} else {
$this->messages[$domain]['obsolete'][$id] = $message;
}
}
foreach ($this->target->all($domain) as $id => $message) {
if (!$this->source->has($id, $domain)) {
$this->messages[$domain]['all'][$id] = $message;
$this->messages[$domain]['new'][$id] = $message;
$d = $this->target->defines($id, $intlDomain) ? $intlDomain : $domain;
$this->result->add([$id => $message], $d);
if (null !== $keyMetadata = $this->target->getMetadata($id, $d)) {
$this->result->setMetadata($id, $keyMetadata, $d);
}
}
}
}
}

View file

@ -0,0 +1,158 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Translation\Command;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle;
use Symfony\Component\Translation\Catalogue\TargetOperation;
use Symfony\Component\Translation\MessageCatalogue;
use Symfony\Component\Translation\Provider\TranslationProviderCollection;
use Symfony\Component\Translation\Reader\TranslationReaderInterface;
use Symfony\Component\Translation\Writer\TranslationWriterInterface;
/**
* @author Mathieu Santostefano <msantostefano@protonmail.com>
*
* @experimental in 5.3
*/
final class TranslationPullCommand extends Command
{
use TranslationTrait;
protected static $defaultName = 'translation:pull';
protected static $defaultDescription = 'Pull translations from a given provider.';
private $providerCollection;
private $writer;
private $reader;
private $defaultLocale;
private $transPaths;
private $enabledLocales;
public function __construct(TranslationProviderCollection $providerCollection, TranslationWriterInterface $writer, TranslationReaderInterface $reader, string $defaultLocale, array $transPaths = [], array $enabledLocales = [])
{
$this->providerCollection = $providerCollection;
$this->writer = $writer;
$this->reader = $reader;
$this->defaultLocale = $defaultLocale;
$this->transPaths = $transPaths;
$this->enabledLocales = $enabledLocales;
parent::__construct();
}
/**
* {@inheritdoc}
*/
protected function configure()
{
$keys = $this->providerCollection->keys();
$defaultProvider = 1 === \count($keys) ? $keys[0] : null;
$this
->setDefinition([
new InputArgument('provider', null !== $defaultProvider ? InputArgument::OPTIONAL : InputArgument::REQUIRED, 'The provider to pull translations from.', $defaultProvider),
new InputOption('force', null, InputOption::VALUE_NONE, 'Override existing translations with provider ones (it will delete not synchronized messages).'),
new InputOption('intl-icu', null, InputOption::VALUE_NONE, 'Associated to --force option, it will write messages in "%domain%+intl-icu.%locale%.xlf" files.'),
new InputOption('domains', null, InputOption::VALUE_OPTIONAL | InputOption::VALUE_IS_ARRAY, 'Specify the domains to pull.'),
new InputOption('locales', null, InputOption::VALUE_OPTIONAL | InputOption::VALUE_IS_ARRAY, 'Specify the locales to pull.'),
new InputOption('format', null, InputOption::VALUE_OPTIONAL, 'Override the default output format.', 'xlf12'),
])
->setHelp(<<<'EOF'
The <info>%command.name%</> command pulls translations from the given provider. Only
new translations are pulled, existing ones are not overwritten.
You can overwrite existing translations (and remove the missing ones on local side) by using the <comment>--force</> flag:
<info>php %command.full_name% --force provider</>
Full example:
<info>php %command.full_name% provider --force --domains=messages,validators --locales=en</>
This command pulls all translations associated with the <comment>messages</> and <comment>validators</> domains for the <comment>en</> locale.
Local translations for the specified domains and locale are deleted if they're not present on the provider and overwritten if it's the case.
Local translations for others domains and locales are ignored.
EOF
)
;
}
/**
* {@inheritdoc}
*/
protected function execute(InputInterface $input, OutputInterface $output): int
{
$io = new SymfonyStyle($input, $output);
$provider = $this->providerCollection->get($input->getArgument('provider'));
$force = $input->getOption('force');
$intlIcu = $input->getOption('intl-icu');
$locales = $input->getOption('locales') ?: $this->enabledLocales;
$domains = $input->getOption('domains');
$format = $input->getOption('format');
$xliffVersion = '1.2';
if ($intlIcu && !$force) {
$io->note('--intl-icu option only has an effect when used with --force. Here, it will be ignored.');
}
switch ($format) {
case 'xlf20': $xliffVersion = '2.0';
// no break
case 'xlf12': $format = 'xlf';
}
$writeOptions = [
'path' => end($this->transPaths),
'xliff_version' => $xliffVersion,
'default_locale' => $this->defaultLocale,
];
if (!$domains) {
$domains = $provider->getDomains();
}
$providerTranslations = $provider->read($domains, $locales);
if ($force) {
foreach ($providerTranslations->getCatalogues() as $catalogue) {
$operation = new TargetOperation((new MessageCatalogue($catalogue->getLocale())), $catalogue);
if ($intlIcu) {
$operation->moveMessagesToIntlDomainsIfPossible();
}
$this->writer->write($operation->getResult(), $format, $writeOptions);
}
$io->success(sprintf('Local translations has been updated from "%s" (for "%s" locale(s), and "%s" domain(s)).', parse_url($provider, \PHP_URL_SCHEME), implode(', ', $locales), implode(', ', $domains)));
return 0;
}
$localTranslations = $this->readLocalTranslations($locales, $domains, $this->transPaths);
// Append pulled translations to local ones.
$localTranslations->addBag($providerTranslations->diff($localTranslations));
foreach ($localTranslations->getCatalogues() as $catalogue) {
$this->writer->write($catalogue, $format, $writeOptions);
}
$io->success(sprintf('New translations from "%s" has been written locally (for "%s" locale(s), and "%s" domain(s)).', parse_url($provider, \PHP_URL_SCHEME), implode(', ', $locales), implode(', ', $domains)));
return 0;
}
}

View file

@ -0,0 +1,158 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Translation\Command;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Exception\InvalidArgumentException;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle;
use Symfony\Component\Translation\Provider\TranslationProviderCollection;
use Symfony\Component\Translation\Reader\TranslationReaderInterface;
use Symfony\Component\Translation\TranslatorBag;
/**
* @author Mathieu Santostefano <msantostefano@protonmail.com>
*
* @experimental in 5.3
*/
final class TranslationPushCommand extends Command
{
use TranslationTrait;
protected static $defaultName = 'translation:push';
protected static $defaultDescription = 'Push translations to a given provider.';
private $providers;
private $reader;
private $transPaths;
private $enabledLocales;
public function __construct(TranslationProviderCollection $providers, TranslationReaderInterface $reader, array $transPaths = [], array $enabledLocales = [])
{
$this->providers = $providers;
$this->reader = $reader;
$this->transPaths = $transPaths;
$this->enabledLocales = $enabledLocales;
parent::__construct();
}
/**
* {@inheritdoc}
*/
protected function configure()
{
$keys = $this->providers->keys();
$defaultProvider = 1 === \count($keys) ? $keys[0] : null;
$this
->setDefinition([
new InputArgument('provider', null !== $defaultProvider ? InputArgument::OPTIONAL : InputArgument::REQUIRED, 'The provider to push translations to.', $defaultProvider),
new InputOption('force', null, InputOption::VALUE_NONE, 'Override existing translations with local ones (it will delete not synchronized messages).'),
new InputOption('delete-missing', null, InputOption::VALUE_NONE, 'Delete translations available on provider but not locally.'),
new InputOption('domains', null, InputOption::VALUE_OPTIONAL | InputOption::VALUE_IS_ARRAY, 'Specify the domains to push.'),
new InputOption('locales', null, InputOption::VALUE_OPTIONAL | InputOption::VALUE_IS_ARRAY, 'Specify the locales to push.', $this->enabledLocales),
])
->setHelp(<<<'EOF'
The <info>%command.name%</> command pushes translations to the given provider. Only new
translations are pushed, existing ones are not overwritten.
You can overwrite existing translations by using the <comment>--force</> flag:
<info>php %command.full_name% --force provider</>
You can delete provider translations which are not present locally by using the <comment>--delete-missing</> flag:
<info>php %command.full_name% --delete-missing provider</>
Full example:
<info>php %command.full_name% provider --force --delete-missing --domains=messages,validators --locales=en</>
This command pushes all translations associated with the <comment>messages</> and <comment>validators</> domains for the <comment>en</> locale.
Provider translations for the specified domains and locale are deleted if they're not present locally and overwritten if it's the case.
Provider translations for others domains and locales are ignored.
EOF
)
;
}
/**
* {@inheritdoc}
*/
protected function execute(InputInterface $input, OutputInterface $output): int
{
$provider = $this->providers->get($input->getArgument('provider'));
if (!$this->enabledLocales) {
throw new InvalidArgumentException(sprintf('You must define "framework.translator.enabled_locales" or "framework.translator.providers.%s.locales" config key in order to work with translation providers.', parse_url($provider, \PHP_URL_SCHEME)));
}
$io = new SymfonyStyle($input, $output);
$domains = $input->getOption('domains');
$locales = $input->getOption('locales');
$force = $input->getOption('force');
$deleteMissing = $input->getOption('delete-missing');
$localTranslations = $this->readLocalTranslations($locales, $domains, $this->transPaths);
if (!$domains) {
$domains = $this->getDomainsFromTranslatorBag($localTranslations);
}
if (!$deleteMissing && $force) {
$provider->write($localTranslations);
$io->success(sprintf('All local translations has been sent to "%s" (for "%s" locale(s), and "%s" domain(s)).', parse_url($provider, \PHP_URL_SCHEME), implode(', ', $locales), implode(', ', $domains)));
return 0;
}
$providerTranslations = $provider->read($domains, $locales);
if ($deleteMissing) {
$provider->delete($providerTranslations->diff($localTranslations));
$io->success(sprintf('Missing translations on "%s" has been deleted (for "%s" locale(s), and "%s" domain(s)).', parse_url($provider, \PHP_URL_SCHEME), implode(', ', $locales), implode(', ', $domains)));
// Read provider translations again, after missing translations deletion,
// to avoid push freshly deleted translations.
$providerTranslations = $provider->read($domains, $locales);
}
$translationsToWrite = $localTranslations->diff($providerTranslations);
if ($force) {
$translationsToWrite->addBag($localTranslations->intersect($providerTranslations));
}
$provider->write($translationsToWrite);
$io->success(sprintf('%s local translations has been sent to "%s" (for "%s" locale(s), and "%s" domain(s)).', $force ? 'All' : 'New', parse_url($provider, \PHP_URL_SCHEME), implode(', ', $locales), implode(', ', $domains)));
return 0;
}
private function getDomainsFromTranslatorBag(TranslatorBag $translatorBag): array
{
$domains = [];
foreach ($translatorBag->getCatalogues() as $catalogue) {
$domains += $catalogue->getDomains();
}
return array_unique($domains);
}
}

View file

@ -0,0 +1,78 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Translation\Command;
use Symfony\Component\Translation\MessageCatalogue;
use Symfony\Component\Translation\MessageCatalogueInterface;
use Symfony\Component\Translation\TranslatorBag;
/**
* @internal
*/
trait TranslationTrait
{
private function readLocalTranslations(array $locales, array $domains, array $transPaths): TranslatorBag
{
$bag = new TranslatorBag();
foreach ($locales as $locale) {
$catalogue = new MessageCatalogue($locale);
foreach ($transPaths as $path) {
$this->reader->read($path, $catalogue);
}
if ($domains) {
foreach ($domains as $domain) {
$catalogue = $this->filterCatalogue($catalogue, $domain);
$bag->addCatalogue($catalogue);
}
} else {
$bag->addCatalogue($catalogue);
}
}
return $bag;
}
private function filterCatalogue(MessageCatalogue $catalogue, string $domain): MessageCatalogue
{
$filteredCatalogue = new MessageCatalogue($catalogue->getLocale());
// extract intl-icu messages only
$intlDomain = $domain.MessageCatalogueInterface::INTL_DOMAIN_SUFFIX;
if ($intlMessages = $catalogue->all($intlDomain)) {
$filteredCatalogue->add($intlMessages, $intlDomain);
}
// extract all messages and subtract intl-icu messages
if ($messages = array_diff($catalogue->all($domain), $intlMessages)) {
$filteredCatalogue->add($messages, $domain);
}
foreach ($catalogue->getResources() as $resource) {
$filteredCatalogue->addResource($resource);
}
if ($metadata = $catalogue->getMetadata('', $intlDomain)) {
foreach ($metadata as $k => $v) {
$filteredCatalogue->setMetadata($k, $v, $intlDomain);
}
}
if ($metadata = $catalogue->getMetadata('', $domain)) {
foreach ($metadata as $k => $v) {
$filteredCatalogue->setMetadata($k, $v, $domain);
}
}
return $filteredCatalogue;
}
}

View file

@ -0,0 +1,267 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Translation\Command;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Exception\RuntimeException;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle;
use Symfony\Component\Translation\Exception\InvalidArgumentException;
use Symfony\Component\Translation\Util\XliffUtils;
/**
* Validates XLIFF files syntax and outputs encountered errors.
*
* @author Grégoire Pineau <lyrixx@lyrixx.info>
* @author Robin Chalas <robin.chalas@gmail.com>
* @author Javier Eguiluz <javier.eguiluz@gmail.com>
*/
class XliffLintCommand extends Command
{
protected static $defaultName = 'lint:xliff';
protected static $defaultDescription = 'Lint an XLIFF file and outputs encountered errors';
private $format;
private $displayCorrectFiles;
private $directoryIteratorProvider;
private $isReadableProvider;
private $requireStrictFileNames;
public function __construct(string $name = null, callable $directoryIteratorProvider = null, callable $isReadableProvider = null, bool $requireStrictFileNames = true)
{
parent::__construct($name);
$this->directoryIteratorProvider = $directoryIteratorProvider;
$this->isReadableProvider = $isReadableProvider;
$this->requireStrictFileNames = $requireStrictFileNames;
}
/**
* {@inheritdoc}
*/
protected function configure()
{
$this
->setDescription(self::$defaultDescription)
->addArgument('filename', InputArgument::IS_ARRAY, 'A file, a directory or "-" for reading from STDIN')
->addOption('format', null, InputOption::VALUE_REQUIRED, 'The output format', 'txt')
->setHelp(<<<EOF
The <info>%command.name%</info> command lints an XLIFF file and outputs to STDOUT
the first encountered syntax error.
You can validates XLIFF contents passed from STDIN:
<info>cat filename | php %command.full_name% -</info>
You can also validate the syntax of a file:
<info>php %command.full_name% filename</info>
Or of a whole directory:
<info>php %command.full_name% dirname</info>
<info>php %command.full_name% dirname --format=json</info>
EOF
)
;
}
protected function execute(InputInterface $input, OutputInterface $output)
{
$io = new SymfonyStyle($input, $output);
$filenames = (array) $input->getArgument('filename');
$this->format = $input->getOption('format');
$this->displayCorrectFiles = $output->isVerbose();
if (['-'] === $filenames) {
return $this->display($io, [$this->validate(file_get_contents('php://stdin'))]);
}
if (!$filenames) {
throw new RuntimeException('Please provide a filename or pipe file content to STDIN.');
}
$filesInfo = [];
foreach ($filenames as $filename) {
if (!$this->isReadable($filename)) {
throw new RuntimeException(sprintf('File or directory "%s" is not readable.', $filename));
}
foreach ($this->getFiles($filename) as $file) {
$filesInfo[] = $this->validate(file_get_contents($file), $file);
}
}
return $this->display($io, $filesInfo);
}
private function validate(string $content, string $file = null): array
{
$errors = [];
// Avoid: Warning DOMDocument::loadXML(): Empty string supplied as input
if ('' === trim($content)) {
return ['file' => $file, 'valid' => true];
}
$internal = libxml_use_internal_errors(true);
$document = new \DOMDocument();
$document->loadXML($content);
if (null !== $targetLanguage = $this->getTargetLanguageFromFile($document)) {
$normalizedLocalePattern = sprintf('(%s|%s)', preg_quote($targetLanguage, '/'), preg_quote(str_replace('-', '_', $targetLanguage), '/'));
// strict file names require translation files to be named '____.locale.xlf'
// otherwise, both '____.locale.xlf' and 'locale.____.xlf' are allowed
// also, the regexp matching must be case-insensitive, as defined for 'target-language' values
// http://docs.oasis-open.org/xliff/v1.2/os/xliff-core.html#target-language
$expectedFilenamePattern = $this->requireStrictFileNames ? sprintf('/^.*\.(?i:%s)\.(?:xlf|xliff)/', $normalizedLocalePattern) : sprintf('/^(?:.*\.(?i:%s)|(?i:%s)\..*)\.(?:xlf|xliff)/', $normalizedLocalePattern, $normalizedLocalePattern);
if (0 === preg_match($expectedFilenamePattern, basename($file))) {
$errors[] = [
'line' => -1,
'column' => -1,
'message' => sprintf('There is a mismatch between the language included in the file name ("%s") and the "%s" value used in the "target-language" attribute of the file.', basename($file), $targetLanguage),
];
}
}
foreach (XliffUtils::validateSchema($document) as $xmlError) {
$errors[] = [
'line' => $xmlError['line'],
'column' => $xmlError['column'],
'message' => $xmlError['message'],
];
}
libxml_clear_errors();
libxml_use_internal_errors($internal);
return ['file' => $file, 'valid' => 0 === \count($errors), 'messages' => $errors];
}
private function display(SymfonyStyle $io, array $files)
{
switch ($this->format) {
case 'txt':
return $this->displayTxt($io, $files);
case 'json':
return $this->displayJson($io, $files);
default:
throw new InvalidArgumentException(sprintf('The format "%s" is not supported.', $this->format));
}
}
private function displayTxt(SymfonyStyle $io, array $filesInfo)
{
$countFiles = \count($filesInfo);
$erroredFiles = 0;
foreach ($filesInfo as $info) {
if ($info['valid'] && $this->displayCorrectFiles) {
$io->comment('<info>OK</info>'.($info['file'] ? sprintf(' in %s', $info['file']) : ''));
} elseif (!$info['valid']) {
++$erroredFiles;
$io->text('<error> ERROR </error>'.($info['file'] ? sprintf(' in %s', $info['file']) : ''));
$io->listing(array_map(function ($error) {
// general document errors have a '-1' line number
return -1 === $error['line'] ? $error['message'] : sprintf('Line %d, Column %d: %s', $error['line'], $error['column'], $error['message']);
}, $info['messages']));
}
}
if (0 === $erroredFiles) {
$io->success(sprintf('All %d XLIFF files contain valid syntax.', $countFiles));
} else {
$io->warning(sprintf('%d XLIFF files have valid syntax and %d contain errors.', $countFiles - $erroredFiles, $erroredFiles));
}
return min($erroredFiles, 1);
}
private function displayJson(SymfonyStyle $io, array $filesInfo)
{
$errors = 0;
array_walk($filesInfo, function (&$v) use (&$errors) {
$v['file'] = (string) $v['file'];
if (!$v['valid']) {
++$errors;
}
});
$io->writeln(json_encode($filesInfo, \JSON_PRETTY_PRINT | \JSON_UNESCAPED_SLASHES));
return min($errors, 1);
}
private function getFiles(string $fileOrDirectory)
{
if (is_file($fileOrDirectory)) {
yield new \SplFileInfo($fileOrDirectory);
return;
}
foreach ($this->getDirectoryIterator($fileOrDirectory) as $file) {
if (!\in_array($file->getExtension(), ['xlf', 'xliff'])) {
continue;
}
yield $file;
}
}
private function getDirectoryIterator(string $directory)
{
$default = function ($directory) {
return new \RecursiveIteratorIterator(
new \RecursiveDirectoryIterator($directory, \FilesystemIterator::SKIP_DOTS | \FilesystemIterator::FOLLOW_SYMLINKS),
\RecursiveIteratorIterator::LEAVES_ONLY
);
};
if (null !== $this->directoryIteratorProvider) {
return ($this->directoryIteratorProvider)($directory, $default);
}
return $default($directory);
}
private function isReadable(string $fileOrDirectory)
{
$default = function ($fileOrDirectory) {
return is_readable($fileOrDirectory);
};
if (null !== $this->isReadableProvider) {
return ($this->isReadableProvider)($fileOrDirectory, $default);
}
return $default($fileOrDirectory);
}
private function getTargetLanguageFromFile(\DOMDocument $xliffContents): ?string
{
foreach ($xliffContents->getElementsByTagName('file')[0]->attributes ?? [] as $attribute) {
if ('target-language' === $attribute->nodeName) {
return $attribute->nodeValue;
}
}
return null;
}
}

View file

@ -0,0 +1,172 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Translation\DataCollector;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\DataCollector\DataCollector;
use Symfony\Component\HttpKernel\DataCollector\LateDataCollectorInterface;
use Symfony\Component\Translation\DataCollectorTranslator;
use Symfony\Component\VarDumper\Cloner\Data;
/**
* @author Abdellatif Ait boudad <a.aitboudad@gmail.com>
*
* @final
*/
class TranslationDataCollector extends DataCollector implements LateDataCollectorInterface
{
private $translator;
public function __construct(DataCollectorTranslator $translator)
{
$this->translator = $translator;
}
/**
* {@inheritdoc}
*/
public function lateCollect()
{
$messages = $this->sanitizeCollectedMessages($this->translator->getCollectedMessages());
$this->data += $this->computeCount($messages);
$this->data['messages'] = $messages;
$this->data = $this->cloneVar($this->data);
}
/**
* {@inheritdoc}
*/
public function collect(Request $request, Response $response, \Throwable $exception = null)
{
$this->data['locale'] = $this->translator->getLocale();
$this->data['fallback_locales'] = $this->translator->getFallbackLocales();
}
/**
* {@inheritdoc}
*/
public function reset()
{
$this->data = [];
}
/**
* @return array|Data
*/
public function getMessages()
{
return $this->data['messages'] ?? [];
}
/**
* @return int
*/
public function getCountMissings()
{
return $this->data[DataCollectorTranslator::MESSAGE_MISSING] ?? 0;
}
/**
* @return int
*/
public function getCountFallbacks()
{
return $this->data[DataCollectorTranslator::MESSAGE_EQUALS_FALLBACK] ?? 0;
}
/**
* @return int
*/
public function getCountDefines()
{
return $this->data[DataCollectorTranslator::MESSAGE_DEFINED] ?? 0;
}
public function getLocale()
{
return !empty($this->data['locale']) ? $this->data['locale'] : null;
}
/**
* @internal
*/
public function getFallbackLocales()
{
return (isset($this->data['fallback_locales']) && \count($this->data['fallback_locales']) > 0) ? $this->data['fallback_locales'] : [];
}
/**
* {@inheritdoc}
*/
public function getName()
{
return 'translation';
}
private function sanitizeCollectedMessages(array $messages)
{
$result = [];
foreach ($messages as $key => $message) {
$messageId = $message['locale'].$message['domain'].$message['id'];
if (!isset($result[$messageId])) {
$message['count'] = 1;
$message['parameters'] = !empty($message['parameters']) ? [$message['parameters']] : [];
$messages[$key]['translation'] = $this->sanitizeString($message['translation']);
$result[$messageId] = $message;
} else {
if (!empty($message['parameters'])) {
$result[$messageId]['parameters'][] = $message['parameters'];
}
++$result[$messageId]['count'];
}
unset($messages[$key]);
}
return $result;
}
private function computeCount(array $messages)
{
$count = [
DataCollectorTranslator::MESSAGE_DEFINED => 0,
DataCollectorTranslator::MESSAGE_MISSING => 0,
DataCollectorTranslator::MESSAGE_EQUALS_FALLBACK => 0,
];
foreach ($messages as $message) {
++$count[$message['state']];
}
return $count;
}
private function sanitizeString(string $string, int $length = 80)
{
$string = trim(preg_replace('/\s+/', ' ', $string));
if (false !== $encoding = mb_detect_encoding($string, null, true)) {
if (mb_strlen($string, $encoding) > $length) {
return mb_substr($string, 0, $length - 3, $encoding).'...';
}
} elseif (\strlen($string) > $length) {
return substr($string, 0, $length - 3).'...';
}
return $string;
}
}

View file

@ -0,0 +1,171 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Translation;
use Symfony\Component\HttpKernel\CacheWarmer\WarmableInterface;
use Symfony\Component\Translation\Exception\InvalidArgumentException;
use Symfony\Contracts\Translation\LocaleAwareInterface;
use Symfony\Contracts\Translation\TranslatorInterface;
/**
* @author Abdellatif Ait boudad <a.aitboudad@gmail.com>
*/
class DataCollectorTranslator implements TranslatorInterface, TranslatorBagInterface, LocaleAwareInterface, WarmableInterface
{
public const MESSAGE_DEFINED = 0;
public const MESSAGE_MISSING = 1;
public const MESSAGE_EQUALS_FALLBACK = 2;
/**
* @var TranslatorInterface|TranslatorBagInterface
*/
private $translator;
private $messages = [];
/**
* @param TranslatorInterface $translator The translator must implement TranslatorBagInterface
*/
public function __construct(TranslatorInterface $translator)
{
if (!$translator instanceof TranslatorBagInterface || !$translator instanceof LocaleAwareInterface) {
throw new InvalidArgumentException(sprintf('The Translator "%s" must implement TranslatorInterface, TranslatorBagInterface and LocaleAwareInterface.', get_debug_type($translator)));
}
$this->translator = $translator;
}
/**
* {@inheritdoc}
*/
public function trans(?string $id, array $parameters = [], string $domain = null, string $locale = null)
{
$trans = $this->translator->trans($id = (string) $id, $parameters, $domain, $locale);
$this->collectMessage($locale, $domain, $id, $trans, $parameters);
return $trans;
}
/**
* {@inheritdoc}
*/
public function setLocale(string $locale)
{
$this->translator->setLocale($locale);
}
/**
* {@inheritdoc}
*/
public function getLocale()
{
return $this->translator->getLocale();
}
/**
* {@inheritdoc}
*/
public function getCatalogue(string $locale = null)
{
return $this->translator->getCatalogue($locale);
}
/**
* {@inheritdoc}
*/
public function getCatalogues(): array
{
return $this->translator->getCatalogues();
}
/**
* {@inheritdoc}
*
* @return string[]
*/
public function warmUp(string $cacheDir)
{
if ($this->translator instanceof WarmableInterface) {
return (array) $this->translator->warmUp($cacheDir);
}
return [];
}
/**
* Gets the fallback locales.
*
* @return array The fallback locales
*/
public function getFallbackLocales()
{
if ($this->translator instanceof Translator || method_exists($this->translator, 'getFallbackLocales')) {
return $this->translator->getFallbackLocales();
}
return [];
}
/**
* Passes through all unknown calls onto the translator object.
*/
public function __call(string $method, array $args)
{
return $this->translator->{$method}(...$args);
}
/**
* @return array
*/
public function getCollectedMessages()
{
return $this->messages;
}
private function collectMessage(?string $locale, ?string $domain, string $id, string $translation, ?array $parameters = [])
{
if (null === $domain) {
$domain = 'messages';
}
$catalogue = $this->translator->getCatalogue($locale);
$locale = $catalogue->getLocale();
$fallbackLocale = null;
if ($catalogue->defines($id, $domain)) {
$state = self::MESSAGE_DEFINED;
} elseif ($catalogue->has($id, $domain)) {
$state = self::MESSAGE_EQUALS_FALLBACK;
$fallbackCatalogue = $catalogue->getFallbackCatalogue();
while ($fallbackCatalogue) {
if ($fallbackCatalogue->defines($id, $domain)) {
$fallbackLocale = $fallbackCatalogue->getLocale();
break;
}
$fallbackCatalogue = $fallbackCatalogue->getFallbackCatalogue();
}
} else {
$state = self::MESSAGE_MISSING;
}
$this->messages[] = [
'locale' => $locale,
'fallbackLocale' => $fallbackLocale,
'domain' => $domain,
'id' => $id,
'translation' => $translation,
'parameters' => $parameters,
'state' => $state,
'transChoiceNumber' => isset($parameters['%count%']) && is_numeric($parameters['%count%']) ? $parameters['%count%'] : null,
];
}
}

View file

@ -0,0 +1,48 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Translation\DependencyInjection;
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Reference;
/**
* Adds tagged translation.formatter services to translation writer.
*/
class TranslationDumperPass implements CompilerPassInterface
{
private $writerServiceId;
private $dumperTag;
public function __construct(string $writerServiceId = 'translation.writer', string $dumperTag = 'translation.dumper')
{
if (1 < \func_num_args()) {
trigger_deprecation('symfony/translation', '5.3', 'Configuring "%s" is deprecated.', __CLASS__);
}
$this->writerServiceId = $writerServiceId;
$this->dumperTag = $dumperTag;
}
public function process(ContainerBuilder $container)
{
if (!$container->hasDefinition($this->writerServiceId)) {
return;
}
$definition = $container->getDefinition($this->writerServiceId);
foreach ($container->findTaggedServiceIds($this->dumperTag, true) as $id => $attributes) {
$definition->addMethodCall('addDumper', [$attributes[0]['alias'], new Reference($id)]);
}
}
}

View file

@ -0,0 +1,53 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Translation\DependencyInjection;
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Exception\RuntimeException;
use Symfony\Component\DependencyInjection\Reference;
/**
* Adds tagged translation.extractor services to translation extractor.
*/
class TranslationExtractorPass implements CompilerPassInterface
{
private $extractorServiceId;
private $extractorTag;
public function __construct(string $extractorServiceId = 'translation.extractor', string $extractorTag = 'translation.extractor')
{
if (0 < \func_num_args()) {
trigger_deprecation('symfony/translation', '5.3', 'Configuring "%s" is deprecated.', __CLASS__);
}
$this->extractorServiceId = $extractorServiceId;
$this->extractorTag = $extractorTag;
}
public function process(ContainerBuilder $container)
{
if (!$container->hasDefinition($this->extractorServiceId)) {
return;
}
$definition = $container->getDefinition($this->extractorServiceId);
foreach ($container->findTaggedServiceIds($this->extractorTag, true) as $id => $attributes) {
if (!isset($attributes[0]['alias'])) {
throw new RuntimeException(sprintf('The alias for the tag "translation.extractor" of service "%s" must be set.', $id));
}
$definition->addMethodCall('addExtractor', [$attributes[0]['alias'], new Reference($id)]);
}
}
}

View file

@ -0,0 +1,93 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Translation\DependencyInjection;
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\Compiler\ServiceLocatorTagPass;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Reference;
class TranslatorPass implements CompilerPassInterface
{
private $translatorServiceId;
private $readerServiceId;
private $loaderTag;
private $debugCommandServiceId;
private $updateCommandServiceId;
public function __construct(string $translatorServiceId = 'translator.default', string $readerServiceId = 'translation.reader', string $loaderTag = 'translation.loader', string $debugCommandServiceId = 'console.command.translation_debug', string $updateCommandServiceId = 'console.command.translation_update')
{
if (0 < \func_num_args()) {
trigger_deprecation('symfony/translation', '5.3', 'Configuring "%s" is deprecated.', __CLASS__);
}
$this->translatorServiceId = $translatorServiceId;
$this->readerServiceId = $readerServiceId;
$this->loaderTag = $loaderTag;
$this->debugCommandServiceId = $debugCommandServiceId;
$this->updateCommandServiceId = $updateCommandServiceId;
}
public function process(ContainerBuilder $container)
{
if (!$container->hasDefinition($this->translatorServiceId)) {
return;
}
$loaders = [];
$loaderRefs = [];
foreach ($container->findTaggedServiceIds($this->loaderTag, true) as $id => $attributes) {
$loaderRefs[$id] = new Reference($id);
$loaders[$id][] = $attributes[0]['alias'];
if (isset($attributes[0]['legacy-alias'])) {
$loaders[$id][] = $attributes[0]['legacy-alias'];
}
}
if ($container->hasDefinition($this->readerServiceId)) {
$definition = $container->getDefinition($this->readerServiceId);
foreach ($loaders as $id => $formats) {
foreach ($formats as $format) {
$definition->addMethodCall('addLoader', [$format, $loaderRefs[$id]]);
}
}
}
$container
->findDefinition($this->translatorServiceId)
->replaceArgument(0, ServiceLocatorTagPass::register($container, $loaderRefs))
->replaceArgument(3, $loaders)
;
if (!$container->hasParameter('twig.default_path')) {
return;
}
$paths = array_keys($container->getDefinition('twig.template_iterator')->getArgument(1));
if ($container->hasDefinition($this->debugCommandServiceId)) {
$definition = $container->getDefinition($this->debugCommandServiceId);
$definition->replaceArgument(4, $container->getParameter('twig.default_path'));
if (\count($definition->getArguments()) > 6) {
$definition->replaceArgument(6, $paths);
}
}
if ($container->hasDefinition($this->updateCommandServiceId)) {
$definition = $container->getDefinition($this->updateCommandServiceId);
$definition->replaceArgument(5, $container->getParameter('twig.default_path'));
if (\count($definition->getArguments()) > 7) {
$definition->replaceArgument(7, $paths);
}
}
}
}

View file

@ -0,0 +1,151 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Translation\DependencyInjection;
use Symfony\Component\DependencyInjection\Compiler\AbstractRecursivePass;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Definition;
use Symfony\Component\DependencyInjection\Reference;
use Symfony\Component\DependencyInjection\ServiceLocator;
/**
* @author Yonel Ceruto <yonelceruto@gmail.com>
*/
class TranslatorPathsPass extends AbstractRecursivePass
{
private $translatorServiceId;
private $debugCommandServiceId;
private $updateCommandServiceId;
private $resolverServiceId;
private $level = 0;
private $paths = [];
private $definitions = [];
private $controllers = [];
public function __construct(string $translatorServiceId = 'translator', string $debugCommandServiceId = 'console.command.translation_debug', string $updateCommandServiceId = 'console.command.translation_update', string $resolverServiceId = 'argument_resolver.service')
{
if (0 < \func_num_args()) {
trigger_deprecation('symfony/translation', '5.3', 'Configuring "%s" is deprecated.', __CLASS__);
}
$this->translatorServiceId = $translatorServiceId;
$this->debugCommandServiceId = $debugCommandServiceId;
$this->updateCommandServiceId = $updateCommandServiceId;
$this->resolverServiceId = $resolverServiceId;
}
public function process(ContainerBuilder $container)
{
if (!$container->hasDefinition($this->translatorServiceId)) {
return;
}
foreach ($this->findControllerArguments($container) as $controller => $argument) {
$id = substr($controller, 0, strpos($controller, ':') ?: \strlen($controller));
if ($container->hasDefinition($id)) {
[$locatorRef] = $argument->getValues();
$this->controllers[(string) $locatorRef][$container->getDefinition($id)->getClass()] = true;
}
}
try {
parent::process($container);
$paths = [];
foreach ($this->paths as $class => $_) {
if (($r = $container->getReflectionClass($class)) && !$r->isInterface()) {
$paths[] = $r->getFileName();
foreach ($r->getTraits() as $trait) {
$paths[] = $trait->getFileName();
}
}
}
if ($paths) {
if ($container->hasDefinition($this->debugCommandServiceId)) {
$definition = $container->getDefinition($this->debugCommandServiceId);
$definition->replaceArgument(6, array_merge($definition->getArgument(6), $paths));
}
if ($container->hasDefinition($this->updateCommandServiceId)) {
$definition = $container->getDefinition($this->updateCommandServiceId);
$definition->replaceArgument(7, array_merge($definition->getArgument(7), $paths));
}
}
} finally {
$this->level = 0;
$this->paths = [];
$this->definitions = [];
}
}
protected function processValue($value, bool $isRoot = false)
{
if ($value instanceof Reference) {
if ((string) $value === $this->translatorServiceId) {
for ($i = $this->level - 1; $i >= 0; --$i) {
$class = $this->definitions[$i]->getClass();
if (ServiceLocator::class === $class) {
if (!isset($this->controllers[$this->currentId])) {
continue;
}
foreach ($this->controllers[$this->currentId] as $class => $_) {
$this->paths[$class] = true;
}
} else {
$this->paths[$class] = true;
}
break;
}
}
return $value;
}
if ($value instanceof Definition) {
$this->definitions[$this->level++] = $value;
$value = parent::processValue($value, $isRoot);
unset($this->definitions[--$this->level]);
return $value;
}
return parent::processValue($value, $isRoot);
}
private function findControllerArguments(ContainerBuilder $container): array
{
if ($container->hasDefinition($this->resolverServiceId)) {
$argument = $container->getDefinition($this->resolverServiceId)->getArgument(0);
if ($argument instanceof Reference) {
$argument = $container->getDefinition($argument);
}
return $argument->getArgument(0);
}
if ($container->hasDefinition('debug.'.$this->resolverServiceId)) {
$argument = $container->getDefinition('debug.'.$this->resolverServiceId)->getArgument(0);
if ($argument instanceof Reference) {
$argument = $container->getDefinition($argument);
}
$argument = $argument->getArgument(0);
if ($argument instanceof Reference) {
$argument = $container->getDefinition($argument);
}
return $argument->getArgument(0);
}
return [];
}
}

Some files were not shown because too many files have changed in this diff Show more