#!/usr/bin/env php w` wp-cli.phar#vendor/wp-cli/wp-cli/php/wp-cli.phpa1vendor/wp-cli/wp-cli/php/WP_CLI/ExitException.phpSaS{uׄ2vendor/wp-cli/wp-cli/php/WP_CLI/SynopsisParser.phppapW]4vendor/wp-cli/wp-cli/php/WP_CLI/Context/Frontend.phpaBr1vendor/wp-cli/wp-cli/php/WP_CLI/Context/Admin.php% a% eeȤ/vendor/wp-cli/wp-cli/php/WP_CLI/Context/Cli.phpaA0vendor/wp-cli/wp-cli/php/WP_CLI/Context/Auto.phpa2+vendor/wp-cli/wp-cli/php/WP_CLI/Context.phpMaM[NG-vendor/wp-cli/wp-cli/php/WP_CLI/Formatter.php%a%W0vendor/wp-cli/wp-cli/php/WP_CLI/UpgraderSkin.phpaJvendor/wp-cli/wp-cli/php/WP_CLI/Compat/Min_PHP_5_6/FeedbackMethodTrait.phpavendor/wp-cli/wp-cli/php/WP_CLI/Compat/FeedbackMethodTrait.phpa閤5vendor/wp-cli/wp-cli/php/WP_CLI/Loggers/Execution.phpaaatW3vendor/wp-cli/wp-cli/php/WP_CLI/Loggers/Regular.phpa5!0vendor/wp-cli/wp-cli/php/WP_CLI/Loggers/Base.phpaEe1vendor/wp-cli/wp-cli/php/WP_CLI/Loggers/Quiet.php a -n2vendor/wp-cli/wp-cli/php/WP_CLI/ContextManager.phpaZϤ6vendor/wp-cli/wp-cli/php/WP_CLI/WpHttpCacheManager.phpk ak j*vendor/wp-cli/wp-cli/php/WP_CLI/Runner.phpa< 5vendor/wp-cli/wp-cli/php/WP_CLI/SynopsisValidator.phpak%/vendor/wp-cli/wp-cli/php/WP_CLI/Completions.php9a9jۤ.vendor/wp-cli/wp-cli/php/WP_CLI/Autoloader.php-a- lѤ-vendor/wp-cli/wp-cli/php/WP_CLI/DocParser.phpa xP@vendor/wp-cli/wp-cli/php/WP_CLI/Bootstrap/InitializeContexts.phpa0Ca:vendor/wp-cli/wp-cli/php/WP_CLI/Bootstrap/LaunchRunner.phpaA^U<vendor/wp-cli/wp-cli/php/WP_CLI/Bootstrap/BootstrapState.phpaפHvendor/wp-cli/wp-cli/php/WP_CLI/Bootstrap/DeclareAbstractBaseCommand.phpAaA@<@>vendor/wp-cli/wp-cli/php/WP_CLI/Bootstrap/DeclareMainClass.phpaTj<vendor/wp-cli/wp-cli/php/WP_CLI/Bootstrap/LoadDispatcher.php5a5U4Dvendor/wp-cli/wp-cli/php/WP_CLI/Bootstrap/InitializeColorization.phpLaL'==vendor/wp-cli/wp-cli/php/WP_CLI/Bootstrap/ConfigureRunner.php5a5߶-Evendor/wp-cli/wp-cli/php/WP_CLI/Bootstrap/DefineProtectedCommands.phpa8>2Fvendor/wp-cli/wp-cli/php/WP_CLI/Bootstrap/DeclareFallbackFunctions.phpCaC@*Bg=vendor/wp-cli/wp-cli/php/WP_CLI/Bootstrap/LoadExecCommand.phpWaWy~;vendor/wp-cli/wp-cli/php/WP_CLI/Bootstrap/BootstrapStep.phpaA7/ Avendor/wp-cli/wp-cli/php/WP_CLI/Bootstrap/LoadRequiredCommand.phpaӤBvendor/wp-cli/wp-cli/php/WP_CLI/Bootstrap/LoadUtilityFunctions.php)a)TYMݤ<vendor/wp-cli/wp-cli/php/WP_CLI/Bootstrap/AutoloaderStep.php) a) ¬Gvendor/wp-cli/wp-cli/php/WP_CLI/Bootstrap/RegisterFrameworkCommands.phpgag>V>vendor/wp-cli/wp-cli/php/WP_CLI/Bootstrap/InitializeLogger.php*a*\3Hvendor/wp-cli/wp-cli/php/WP_CLI/Bootstrap/IncludeFrameworkAutoloader.phpat?ߤFvendor/wp-cli/wp-cli/php/WP_CLI/Bootstrap/RegisterDeferredCommands.phpak Gvendor/wp-cli/wp-cli/php/WP_CLI/Bootstrap/IncludeFallbackAutoloader.phpaSFvendor/wp-cli/wp-cli/php/WP_CLI/Bootstrap/IncludePackageAutoloader.phpaF<vendor/wp-cli/wp-cli/php/WP_CLI/Bootstrap/RunnerInstance.phpak1vendor/wp-cli/wp-cli/php/WP_CLI/Fetchers/Base.phpaU1vendor/wp-cli/wp-cli/php/WP_CLI/Fetchers/Post.php9a91vendor/wp-cli/wp-cli/php/WP_CLI/Fetchers/User.php0a04vendor/wp-cli/wp-cli/php/WP_CLI/Fetchers/Comment.php}a}1vendor/wp-cli/wp-cli/php/WP_CLI/Fetchers/Site.phpaxkX3vendor/wp-cli/wp-cli/php/WP_CLI/Iterators/Query.php a ֽ7vendor/wp-cli/wp-cli/php/WP_CLI/Iterators/Transform.phpaӀtY3vendor/wp-cli/wp-cli/php/WP_CLI/Iterators/Table.phpe ae @+1vendor/wp-cli/wp-cli/php/WP_CLI/Iterators/CSV.phpabO7vendor/wp-cli/wp-cli/php/WP_CLI/Iterators/Exception.phphah0L6Ҥ-vendor/wp-cli/wp-cli/php/WP_CLI/Extractor.phpB'aB'vendor/wp-cli/wp-cli/php/WP_CLI/Dispatcher/CommandAddition.php#a#躛(vendor/wp-cli/wp-cli/php/WP_CLI/NoOp.phpaD1-vendor/wp-cli/wp-cli/php/WP_CLI/Inflector.php?a?b+vendor/wp-cli/wp-cli/php/WP_CLI/Process.phpT aT Dk-vendor/wp-cli/wp-cli/php/WP_CLI/FileCache.php!a!k Ӥ.vendor/wp-cli/wp-cli/php/WP_CLI/ComposerIO.phpa+Q%vendor/wp-cli/wp-cli/php/utils-wp.php9a9Ӏ#vendor/wp-cli/wp-cli/php/compat.phpnanze/vendor/wp-cli/wp-cli/php/fallback-functions.phpa[F,vendor/wp-cli/wp-cli/php/wp-settings-cli.phpoCaoCg)vendor/wp-cli/wp-cli/php/class-wp-cli.phpavy"vendor/wp-cli/wp-cli/php/utils.phpai&vendor/wp-cli/wp-cli/php/bootstrap.phpa^Yf(vendor/wp-cli/wp-cli/php/config-spec.phpa1vendor/wp-cli/wp-cli/php/class-wp-cli-command.phpa*'vendor/wp-cli/wp-cli/php/dispatcher.phpMaM:J$vendor/wp-cli/wp-cli/php/boot-fs.phpa<ײ;vendor/wp-cli/wp-cli/php/commands/src/CLI_Cache_Command.phpan?;vendor/wp-cli/wp-cli/php/commands/src/CLI_Alias_Command.phpy3ay3>i5vendor/wp-cli/wp-cli/php/commands/src/CLI_Command.phpNaNga6vendor/wp-cli/wp-cli/php/commands/src/Help_Command.phpWaWq*vendor/wp-cli/wp-cli/php/commands/help.phpalD)vendor/wp-cli/wp-cli/php/commands/cli.phpajnphp/boot-phar.php9a9Ys0vendor/mustache/mustache/bin/build_bootstrap.php;a;0vendor/mustache/mustache/src/Mustache/Loader.phpBaBQy0vendor/mustache/mustache/src/Mustache/Engine.phpcac51vendor/mustache/mustache/src/Mustache/Context.phpaD0vendor/mustache/mustache/src/Mustache/Logger.php$ a$ @Y92vendor/mustache/mustache/src/Mustache/Compiler.php.Xa.X-Avendor/mustache/mustache/src/Mustache/Loader/FilesystemLoader.phpaL$y@vendor/mustache/mustache/src/Mustache/Loader/CascadingLoader.phpa=vendor/mustache/mustache/src/Mustache/Loader/InlineLoader.phpa\ >vendor/mustache/mustache/src/Mustache/Loader/MutableLoader.phpagKvendor/mustache/mustache/src/Mustache/Loader/ProductionFilesystemLoader.phpf af <vendor/mustache/mustache/src/Mustache/Loader/ArrayLoader.phpa!=vendor/mustache/mustache/src/Mustache/Loader/StringLoader.phpa<%0vendor/mustache/mustache/src/Mustache/Parser.php*a*VʄLvendor/mustache/mustache/src/Mustache/Exception/InvalidArgumentException.phpaQBvendor/mustache/mustache/src/Mustache/Exception/LogicException.phpa}ACvendor/mustache/mustache/src/Mustache/Exception/SyntaxException.phpaNLvendor/mustache/mustache/src/Mustache/Exception/UnknownTemplateException.phpa-[Jvendor/mustache/mustache/src/Mustache/Exception/UnknownFilterException.phpafޤJvendor/mustache/mustache/src/Mustache/Exception/UnknownHelperException.phpaHjDvendor/mustache/mustache/src/Mustache/Exception/RuntimeException.phpa1]Ӥ=vendor/mustache/mustache/src/Mustache/Cache/AbstractCache.phpaY`9vendor/mustache/mustache/src/Mustache/Cache/NoopCache.phpAaA٤?vendor/mustache/mustache/src/Mustache/Cache/FilesystemCache.phphah(}4/vendor/mustache/mustache/src/Mustache/Cache.phpaW0vendor/mustache/mustache/src/Mustache/Source.phpab׶m=vendor/mustache/mustache/src/Mustache/Logger/StreamLogger.phpaJ?vendor/mustache/mustache/src/Mustache/Logger/AbstractLogger.phpv av nY66vendor/mustache/mustache/src/Mustache/LambdaHelper.phpapդ:vendor/mustache/mustache/src/Mustache/HelperCollection.phpaӤ2vendor/mustache/mustache/src/Mustache/Template.phpa84vendor/mustache/mustache/src/Mustache/Autoloader.phpa=c֤Avendor/mustache/mustache/src/Mustache/Source/FilesystemSource.phpaV3vendor/mustache/mustache/src/Mustache/Exception.phpRaR]u3vendor/mustache/mustache/src/Mustache/Tokenizer.php-a-!X6vendor/rmccue/requests/library/Requests/Cookie/Jar.phpa4vendor/rmccue/requests/library/Requests/Response.php a q1vendor/rmccue/requests/library/Requests/Hooks.phpxaxKl5/vendor/rmccue/requests/library/Requests/SSL.phpa6ʤ2vendor/rmccue/requests/library/Requests/Hooker.phpa=u:vendor/rmccue/requests/library/Requests/Transport/cURL.phpBaB(}?vendor/rmccue/requests/library/Requests/Transport/fsockopen.php2a2mDvendor/rmccue/requests/library/Requests/Exception/Transport/cURL.phpa]񐒤:vendor/rmccue/requests/library/Requests/Exception/HTTP.phparBvendor/rmccue/requests/library/Requests/Exception/HTTP/Unknown.phpdad>vendor/rmccue/requests/library/Requests/Exception/HTTP/410.phpoao_7>vendor/rmccue/requests/library/Requests/Exception/HTTP/428.phpaJ2>vendor/rmccue/requests/library/Requests/Exception/HTTP/409.php{a{wZB>vendor/rmccue/requests/library/Requests/Exception/HTTP/504.phpaY>vendor/rmccue/requests/library/Requests/Exception/HTTP/416.phpar>vendor/rmccue/requests/library/Requests/Exception/HTTP/405.phpaz]>vendor/rmccue/requests/library/Requests/Exception/HTTP/501.phpaa4>vendor/rmccue/requests/library/Requests/Exception/HTTP/505.phpa+f>vendor/rmccue/requests/library/Requests/Exception/HTTP/418.phpa݄>vendor/rmccue/requests/library/Requests/Exception/HTTP/407.phpako>vendor/rmccue/requests/library/Requests/Exception/HTTP/402.phpaHa>vendor/rmccue/requests/library/Requests/Exception/HTTP/415.phpaR;פ>vendor/rmccue/requests/library/Requests/Exception/HTTP/404.php~a~_>vendor/rmccue/requests/library/Requests/Exception/HTTP/408.phpa1">vendor/rmccue/requests/library/Requests/Exception/HTTP/431.phpa֍>vendor/rmccue/requests/library/Requests/Exception/HTTP/403.php~a~D1>vendor/rmccue/requests/library/Requests/Exception/HTTP/400.phpaDiQ>vendor/rmccue/requests/library/Requests/Exception/HTTP/306.phpaMo>vendor/rmccue/requests/library/Requests/Exception/HTTP/511.phpaf>vendor/rmccue/requests/library/Requests/Exception/HTTP/503.phpaI>vendor/rmccue/requests/library/Requests/Exception/HTTP/401.phpajX>vendor/rmccue/requests/library/Requests/Exception/HTTP/502.phpa.>vendor/rmccue/requests/library/Requests/Exception/HTTP/413.phpaH>vendor/rmccue/requests/library/Requests/Exception/HTTP/414.phpaΰ>vendor/rmccue/requests/library/Requests/Exception/HTTP/304.phpa9AJ>vendor/rmccue/requests/library/Requests/Exception/HTTP/412.phpa>vendor/rmccue/requests/library/Requests/Exception/HTTP/305.php~a~4-O>vendor/rmccue/requests/library/Requests/Exception/HTTP/500.phpag?>vendor/rmccue/requests/library/Requests/Exception/HTTP/411.phpay>vendor/rmccue/requests/library/Requests/Exception/HTTP/429.php&a&mMc'>vendor/rmccue/requests/library/Requests/Exception/HTTP/406.phpaV">vendor/rmccue/requests/library/Requests/Exception/HTTP/417.phpa E?vendor/rmccue/requests/library/Requests/Exception/Transport.phpJaJ9[6vendor/rmccue/requests/library/Requests/Auth/Basic.phpaJA0vendor/rmccue/requests/library/Requests/Auth.php*a*u6vendor/rmccue/requests/library/Requests/Proxy/HTTP.php a A$0vendor/rmccue/requests/library/Requests/IPv6.phpaj{m1vendor/rmccue/requests/library/Requests/Proxy.php-a-ɧ7vendor/rmccue/requests/library/Requests/IDNAEncoder.php-a-̓5vendor/rmccue/requests/library/Requests/Exception.phpa%Ф2vendor/rmccue/requests/library/Requests/Cookie.php2a23vendor/rmccue/requests/library/Requests/Session.phpapMvendor/rmccue/requests/library/Requests/Utility/CaseInsensitiveDictionary.phpaDDvendor/rmccue/requests/library/Requests/Utility/FilteredIterator.phpa ^/vendor/rmccue/requests/library/Requests/IRI.phpoao n <vendor/rmccue/requests/library/Requests/Response/Headers.phpWaW=Q75vendor/rmccue/requests/library/Requests/Transport.phpa*+vendor/rmccue/requests/library/Requests.phpvavLvendor/composer/ClassLoader.php>a>5Ky>vendor/composer/composer/src/Composer/Autoload/ClassLoader.php>a>5KyAvendor/composer/composer/src/Composer/Autoload/PhpFileCleaner.phpaKآDvendor/composer/composer/src/Composer/Autoload/AutoloadGenerator.php%a%H Dvendor/composer/composer/src/Composer/Autoload/ClassMapGenerator.php1a1RFvendor/composer/composer/src/Composer/Config/ConfigSourceInterface.php a oAvendor/composer/composer/src/Composer/Config/JsonConfigSource.phpB)aB)PCvendor/composer/composer/src/Composer/Downloader/PathDownloader.php.a.g;xEvendor/composer/composer/src/Composer/Downloader/FossilDownloader.phpaAvendor/composer/composer/src/Composer/Downloader/HgDownloader.php a |pCvendor/composer/composer/src/Composer/Downloader/GzipDownloader.phpxax˕Hvendor/composer/composer/src/Composer/Downloader/FilesystemException.php#a#Q{(Fvendor/composer/composer/src/Composer/Downloader/ArchiveDownloader.php!a!3(iBvendor/composer/composer/src/Composer/Downloader/RarDownloader.php a KAvendor/composer/composer/src/Composer/Downloader/XzDownloader.phpaʤBvendor/composer/composer/src/Composer/Downloader/ZipDownloader.php ,a ,G[cHvendor/composer/composer/src/Composer/Downloader/DownloaderInterface.phpaxDGvendor/composer/composer/src/Composer/Downloader/PerforceDownloader.php/ a/ :Cvendor/composer/composer/src/Composer/Downloader/FileDownloader.phpwLawLBvendor/composer/composer/src/Composer/Downloader/SvnDownloader.php"a"8+Lvendor/composer/composer/src/Composer/Downloader/DvcsDownloaderInterface.phpav(Dvendor/composer/composer/src/Composer/Downloader/DownloadManager.phpa>aa>Jvendor/composer/composer/src/Composer/Downloader/ChangeReportInterface.phpa)ŸQvendor/composer/composer/src/Composer/Downloader/MaxFileSizeExceededException.phpoao:Cvendor/composer/composer/src/Composer/Downloader/PharDownloader.phpa`Bvendor/composer/composer/src/Composer/Downloader/TarDownloader.php$a$3Bvendor/composer/composer/src/Composer/Downloader/GitDownloader.phpaaa7yjGvendor/composer/composer/src/Composer/Downloader/TransportException.php)a)4NBvendor/composer/composer/src/Composer/Downloader/VcsDownloader.phpJ2aJ2{Rvendor/composer/composer/src/Composer/Downloader/VcsCapableDownloaderInterface.phpa Mvendor/composer/composer/src/Composer/Question/StrictConfirmationQuestion.php a O=vendor/composer/composer/src/Composer/Plugin/CommandEvent.phpaY>Fvendor/composer/composer/src/Composer/Plugin/PostFileDownloadEvent.php a Q=vendor/composer/composer/src/Composer/Plugin/PluginEvents.php'a'=Evendor/composer/composer/src/Composer/Plugin/PreFileDownloadEvent.phpaRFvendor/composer/composer/src/Composer/Plugin/Capability/Capability.phpaY93Kvendor/composer/composer/src/Composer/Plugin/Capability/CommandProvider.php_a_μCvendor/composer/composer/src/Composer/Plugin/PrePoolCreateEvent.phpar@vendor/composer/composer/src/Composer/Plugin/PluginInterface.phpaL3]8vendor/composer/composer/src/Composer/Plugin/Capable.phpa'}>vendor/composer/composer/src/Composer/Plugin/PluginManager.phpzazNyhCvendor/composer/composer/src/Composer/Plugin/PreCommandRunEvent.phpMaMÒ@ =vendor/composer/composer/src/Composer/Util/NoProxyPattern.php,a,g>vendor/composer/composer/src/Composer/Util/ProcessExecutor.phpW:aW:p21vendor/composer/composer/src/Composer/Util/Hg.phpK aK 2t7vendor/composer/composer/src/Composer/Util/Perforce.phpQaQzl7^;vendor/composer/composer/src/Composer/Util/ErrorHandler.php a {02vendor/composer/composer/src/Composer/Util/Svn.phpi(ai(mޣ5vendor/composer/composer/src/Composer/Util/GitLab.phpa⏤2vendor/composer/composer/src/Composer/Util/Zip.php a fU:2vendor/composer/composer/src/Composer/Util/Git.phpQaQ"Ȥ?vendor/composer/composer/src/Composer/Util/RemoteFilesystem.phpar9vendor/composer/composer/src/Composer/Util/AuthHelper.php3a3m>vendor/composer/composer/src/Composer/Util/ConfigValidator.php"a"^ 7vendor/composer/composer/src/Composer/Util/Silencer.phpa>8vendor/composer/composer/src/Composer/Util/TlsHelper.phpqaqz w8vendor/composer/composer/src/Composer/Util/Bitbucket.phpr$ar$AZԤ=vendor/composer/composer/src/Composer/Util/HttpDownloader.phpUGaUG+?vendor/composer/composer/src/Composer/Util/MetadataMinifier.phpqaqtX<vendor/composer/composer/src/Composer/Util/Http/Response.php a ڦ?vendor/composer/composer/src/Composer/Util/Http/ProxyHelper.phpa;:h@vendor/composer/composer/src/Composer/Util/Http/ProxyManager.phpat¤@vendor/composer/composer/src/Composer/Util/Http/RequestProxy.phpa,:NBvendor/composer/composer/src/Composer/Util/Http/CurlDownloader.phplalO@vendor/composer/composer/src/Composer/Util/Http/CurlResponse.phpat2vendor/composer/composer/src/Composer/Util/Tar.phpXaXW W9vendor/composer/composer/src/Composer/Util/Filesystem.php6oa6o Ƥ7vendor/composer/composer/src/Composer/Util/Platform.phpaOCvendor/composer/composer/src/Composer/Util/StreamContextFactory.phpF%aF%{)Az9vendor/composer/composer/src/Composer/Util/SyncHelper.php a ۊd5vendor/composer/composer/src/Composer/Util/GitHub.php[a[8vendor/composer/composer/src/Composer/Util/IniHelper.phpcac,=vendor/composer/composer/src/Composer/Util/ComposerMirror.php a D<vendor/composer/composer/src/Composer/Util/PackageSorter.php a )\3vendor/composer/composer/src/Composer/Util/Loop.phpa=,ͤ2vendor/composer/composer/src/Composer/Util/Url.phpaL0vendor/composer/composer/src/Composer/Config.phpXaXWA6vendor/composer/composer/src/Composer/Script/Event.php[ a[ &mԤ=vendor/composer/composer/src/Composer/Script/ScriptEvents.php'a' Ƥ2vendor/composer/composer/src/Composer/Compiler.phpV0aV0PڈBvendor/composer/composer/src/Composer/Exception/NoSslException.phpa8M5Rvendor/composer/composer/src/Composer/Exception/IrrecoverableDownloadException.phpa-;vendor/composer/composer/src/Composer/InstalledVersions.php:a:T"/vendor/composer/composer/src/Composer/Cache.php(a(Gvendor/composer/composer/src/Composer/Repository/PlatformRepository.php}a}ߤGvendor/composer/composer/src/Composer/Repository/ComposerRepository.php"a"'%zHvendor/composer/composer/src/Composer/Repository/LockArrayRepository.phpmam!Mvendor/composer/composer/src/Composer/Repository/InstalledArrayRepository.phpaiJvendor/composer/composer/src/Composer/Repository/RootPackageRepository.php a bS"Bvendor/composer/composer/src/Composer/Repository/VcsRepository.phpQaQ(uDvendor/composer/composer/src/Composer/Repository/ArrayRepository.php)a)bZHvendor/composer/composer/src/Composer/Repository/InstalledRepository.php2a28hTvendor/composer/composer/src/Composer/Repository/ConfigurableRepositoryInterface.phpaEvendor/composer/composer/src/Composer/Repository/FilterRepository.phpa-Gvendor/composer/composer/src/Composer/Repository/ArtifactRepository.php_a_1Ovendor/composer/composer/src/Composer/Repository/InvalidRepositoryException.phpa$Lvendor/composer/composer/src/Composer/Repository/WritableArrayRepository.phpa'apPvendor/composer/composer/src/Composer/Repository/WritableRepositoryInterface.phpa)놤Jvendor/composer/composer/src/Composer/Repository/VersionCacheInterface.phpamYPvendor/composer/composer/src/Composer/Repository/RepositorySecurityException.phpa<9Hvendor/composer/composer/src/Composer/Repository/RepositoryInterface.phpa9Cvendor/composer/composer/src/Composer/Repository/PathRepository.php: a: suPyRvendor/composer/composer/src/Composer/Repository/InstalledFilesystemRepository.phpaޡxFvendor/composer/composer/src/Composer/Repository/RepositoryFactory.phpaWla?Cvendor/composer/composer/src/Composer/Repository/PearRepository.php2a2'Fvendor/composer/composer/src/Composer/Repository/PackageRepository.phpa~ФHvendor/composer/composer/src/Composer/Repository/CompositeRepository.phpkak!`UQvendor/composer/composer/src/Composer/Repository/InstalledRepositoryInterface.phpaIvendor/composer/composer/src/Composer/Repository/FilesystemRepository.php/a/bˤEvendor/composer/composer/src/Composer/Repository/Vcs/GitHubDriver.php7Ra7R+uGvendor/composer/composer/src/Composer/Repository/Vcs/PerforceDriver.phpahcBvendor/composer/composer/src/Composer/Repository/Vcs/GitDriver.phpa)LpBvendor/composer/composer/src/Composer/Repository/Vcs/SvnDriver.php0a0;Bvendor/composer/composer/src/Composer/Repository/Vcs/VcsDriver.phpa7RmKvendor/composer/composer/src/Composer/Repository/Vcs/VcsDriverInterface.php% a% %Avendor/composer/composer/src/Composer/Repository/Vcs/HgDriver.phpa2eEvendor/composer/composer/src/Composer/Repository/Vcs/GitLabDriver.phpOaOn6Kvendor/composer/composer/src/Composer/Repository/Vcs/GitBitbucketDriver.php8?a8?WEvendor/composer/composer/src/Composer/Repository/Vcs/FossilDriver.phpadrFvendor/composer/composer/src/Composer/Repository/RepositoryManager.phpag}Bvendor/composer/composer/src/Composer/Repository/RepositorySet.php2a2Hvendor/composer/composer/src/Composer/Installer/MetapackageInstaller.php a {Dvendor/composer/composer/src/Composer/Installer/LibraryInstaller.phpy)ay)iCvendor/composer/composer/src/Composer/Installer/PluginInstaller.phpaAvendor/composer/composer/src/Composer/Installer/NoopInstaller.php a Fvendor/composer/composer/src/Composer/Installer/InstallerInterface.phpa}{Bvendor/composer/composer/src/Composer/Installer/InstallerEvent.phpa~,Cvendor/composer/composer/src/Composer/Installer/BinaryInstaller.phpA9aA9ȋ@vendor/composer/composer/src/Composer/Installer/PackageEvent.phpM aM ?y%Dvendor/composer/composer/src/Composer/Installer/ProjectInstaller.php a 3+Kvendor/composer/composer/src/Composer/Installer/BinaryPresenceInterface.phpaMvendor/composer/composer/src/Composer/Installer/SuggestedPackagesReporter.php!a!K)SGvendor/composer/composer/src/Composer/Installer/InstallationManager.phpbabu#Cvendor/composer/composer/src/Composer/Installer/InstallerEvents.php{a{3Avendor/composer/composer/src/Composer/Installer/PackageEvents.phpaZV=vendor/composer/composer/src/Composer/SelfUpdate/Versions.php6 a6 EȤ9vendor/composer/composer/src/Composer/SelfUpdate/Keys.phpaDnvendor/composer/composer/src/Composer/Filter/PlatformRequirementFilter/IgnoreListPlatformRequirementFilter.php a ikvendor/composer/composer/src/Composer/Filter/PlatformRequirementFilter/PlatformRequirementFilterFactory.phpa~ mvendor/composer/composer/src/Composer/Filter/PlatformRequirementFilter/PlatformRequirementFilterInterface.phpaVqvendor/composer/composer/src/Composer/Filter/PlatformRequirementFilter/IgnoreNothingPlatformRequirementFilter.php'a'u8mvendor/composer/composer/src/Composer/Filter/PlatformRequirementFilter/IgnoreAllPlatformRequirementFilter.phpuau v Rvendor/composer/composer/src/Composer/EventDispatcher/ScriptExecutionException.phpa\!'Ƥ?vendor/composer/composer/src/Composer/EventDispatcher/Event.phpaJvURvendor/composer/composer/src/Composer/EventDispatcher/EventSubscriberInterface.php\a\*Ivendor/composer/composer/src/Composer/EventDispatcher/EventDispatcher.phpg]ag]SaפKvendor/composer/composer/src/Composer/DependencyResolver/RuleWatchGraph.phpWaWA\ФJvendor/composer/composer/src/Composer/DependencyResolver/PoolOptimizer.phpEaEӒSCvendor/composer/composer/src/Composer/DependencyResolver/Solver.phphahAvendor/composer/composer/src/Composer/DependencyResolver/Rule.phpOaOdkTvendor/composer/composer/src/Composer/DependencyResolver/SolverProblemsException.phpaRJvendor/composer/composer/src/Composer/DependencyResolver/Rule2Literals.php| a| Jvendor/composer/composer/src/Composer/DependencyResolver/RuleWatchNode.phpt at gDvendor/composer/composer/src/Composer/DependencyResolver/RuleSet.phpa)yAvendor/composer/composer/src/Composer/DependencyResolver/Pool.php!a!"gpNvendor/composer/composer/src/Composer/DependencyResolver/MultiConflictRule.php7 a7 lXdvendor/composer/composer/src/Composer/DependencyResolver/Operation/MarkAliasUninstalledOperation.php a 8ˤVvendor/composer/composer/src/Composer/DependencyResolver/Operation/SolverOperation.phpai`<Vvendor/composer/composer/src/Composer/DependencyResolver/Operation/UpdateOperation.phpN aN }Yvendor/composer/composer/src/Composer/DependencyResolver/Operation/OperationInterface.phpa Y/zbvendor/composer/composer/src/Composer/DependencyResolver/Operation/MarkAliasInstalledOperation.phpaD ФYvendor/composer/composer/src/Composer/DependencyResolver/Operation/UninstallOperation.php*a*.nWvendor/composer/composer/src/Composer/DependencyResolver/Operation/InstallOperation.php@a@$Z@Qvendor/composer/composer/src/Composer/DependencyResolver/LocalRepoTransaction.phpa,4Lvendor/composer/composer/src/Composer/DependencyResolver/LockTransaction.php_a_DXLvendor/composer/composer/src/Composer/DependencyResolver/RuleSetIterator.phpp ap kKvendor/composer/composer/src/Composer/DependencyResolver/RuleWatchChain.phpaջHvendor/composer/composer/src/Composer/DependencyResolver/GenericRule.phpaP{Fvendor/composer/composer/src/Composer/DependencyResolver/Decisions.phpa=ΤLvendor/composer/composer/src/Composer/DependencyResolver/PolicyInterface.phpaPjOvendor/composer/composer/src/Composer/DependencyResolver/SolverBugException.php,a,foHvendor/composer/composer/src/Composer/DependencyResolver/PoolBuilder.phpxaxDvendor/composer/composer/src/Composer/DependencyResolver/Problem.php$ja$j:\Mvendor/composer/composer/src/Composer/DependencyResolver/RuleSetGenerator.php4a4ӟHvendor/composer/composer/src/Composer/DependencyResolver/Transaction.php6a6Dvendor/composer/composer/src/Composer/DependencyResolver/Request.php* a* 5&>wJvendor/composer/composer/src/Composer/DependencyResolver/DefaultPolicy.phpaZŤEvendor/composer/composer/src/Composer/Console/HtmlOutputFormatter.php a :B=vendor/composer/composer/src/Composer/Console/Application.phpbabBCvendor/composer/composer/src/Composer/Console/GithubActionError.php$ a$ THBvendor/composer/composer/src/Composer/Command/RunScriptCommand.phpgagⶤ=vendor/composer/composer/src/Composer/Command/InitCommand.phpadN"Dvendor/composer/composer/src/Composer/Command/ScriptAliasCommand.phpa^f̤@vendor/composer/composer/src/Composer/Command/RequireCommand.phpXaXG@vendor/composer/composer/src/Composer/Command/DependsCommand.phpamFvendor/composer/composer/src/Composer/Command/CreateProjectCommand.php^a^8WeEvendor/composer/composer/src/Composer/Command/DumpAutoloadCommand.phpa(4Bvendor/composer/composer/src/Composer/Command/ReinstallCommand.phpapvCvendor/composer/composer/src/Composer/Command/ClearCacheCommand.phpaX0<ˤ?vendor/composer/composer/src/Composer/Command/RemoveCommand.php.8a.8ڳդ@vendor/composer/composer/src/Composer/Command/InstallCommand.php5a5ȯAvendor/composer/composer/src/Composer/Command/OutdatedCommand.phpaLAvendor/composer/composer/src/Composer/Command/DiagnoseCommand.phplal=墤Bvendor/composer/composer/src/Composer/Command/ProhibitsCommand.phpaDD?vendor/composer/composer/src/Composer/Command/SearchCommand.phpaA?vendor/composer/composer/src/Composer/Command/ConfigCommand.php{a{s>=vendor/composer/composer/src/Composer/Command/BaseCommand.php(&a(& =vendor/composer/composer/src/Composer/Command/ExecCommand.php a Β=vendor/composer/composer/src/Composer/Command/HomeCommand.phpa|̐ؤ?vendor/composer/composer/src/Composer/Command/UpdateCommand.phpAaAJ~X=vendor/composer/composer/src/Composer/Command/ShowCommand.phpaQuդ?vendor/composer/composer/src/Composer/Command/StatusCommand.php/!a/!Avendor/composer/composer/src/Composer/Command/LicensesCommand.phpOaOw=vendor/composer/composer/src/Composer/Command/FundCommand.php$a$2CJvendor/composer/composer/src/Composer/Command/CheckPlatformReqsCommand.phpCaCš?vendor/composer/composer/src/Composer/Command/GlobalCommand.phpaz@ ~@vendor/composer/composer/src/Composer/Command/ArchiveCommand.phpuau{<>vendor/composer/composer/src/Composer/Command/AboutCommand.phpaqy9Cvendor/composer/composer/src/Composer/Command/SelfUpdateCommand.phpdadJGvendor/composer/composer/src/Composer/Command/BaseDependencyCommand.php#a#nG:Avendor/composer/composer/src/Composer/Command/SuggestsCommand.phpaQcAvendor/composer/composer/src/Composer/Command/ValidateCommand.php+a+EM1vendor/composer/composer/src/Composer/Factory.phpoaoU03vendor/composer/composer/src/Composer/Installer.php$a$ቧ6vendor/composer/composer/src/Composer/IO/ConsoleIO.php*a*j&U3vendor/composer/composer/src/Composer/IO/NullIO.phpaD3vendor/composer/composer/src/Composer/IO/BaseIO.phptatx8vendor/composer/composer/src/Composer/IO/IOInterface.php a Z 5vendor/composer/composer/src/Composer/IO/BufferIO.php a IL[Bvendor/composer/composer/src/Composer/Package/PackageInterface.php-a-Avendor/composer/composer/src/Composer/Package/CompletePackage.phpUaUdo֤Hvendor/composer/composer/src/Composer/Package/Loader/LoaderInterface.phpzaz3HPvendor/composer/composer/src/Composer/Package/Loader/InvalidPackageException.php&a&ICvendor/composer/composer/src/Composer/Package/Loader/JsonLoader.phpaYNvendor/composer/composer/src/Composer/Package/Loader/ValidatingArrayLoader.phpZqaZq`6eDvendor/composer/composer/src/Composer/Package/Loader/ArrayLoader.phpEaEJvendor/composer/composer/src/Composer/Package/Loader/RootPackageLoader.php,a,Y*ޤ>vendor/composer/composer/src/Composer/Package/AliasPackage.php%a%+8vendor/composer/composer/src/Composer/Package/Locker.php"Da"D )9vendor/composer/composer/src/Composer/Package/Package.phpFaF_NFvendor/composer/composer/src/Composer/Package/CompleteAliasPackage.phppapN\Cvendor/composer/composer/src/Composer/Package/Comparer/Comparer.php7a7J鱄=vendor/composer/composer/src/Composer/Package/RootPackage.php a zBvendor/composer/composer/src/Composer/Package/RootAliasPackage.phpa;+Fvendor/composer/composer/src/Composer/Package/RootPackageInterface.phpa?s?Gvendor/composer/composer/src/Composer/Package/Archiver/PharArchiver.php a R}Pvendor/composer/composer/src/Composer/Package/Archiver/ArchivableFilesFinder.phpj aj _]Lvendor/composer/composer/src/Composer/Package/Archiver/BaseExcludeFilter.phpam嶤Fvendor/composer/composer/src/Composer/Package/Archiver/ZipArchiver.php a ).Lvendor/composer/composer/src/Composer/Package/Archiver/ArchiverInterface.phpjaj]aCIvendor/composer/composer/src/Composer/Package/Archiver/ArchiveManager.phpaqۤPvendor/composer/composer/src/Composer/Package/Archiver/ComposerExcludeFilter.phpZaZkPvendor/composer/composer/src/Composer/Package/Archiver/ArchivableFilesFilter.php]a]aKvendor/composer/composer/src/Composer/Package/Archiver/GitExcludeFilter.phpàդJvendor/composer/composer/src/Composer/Package/CompletePackageInterface.phpaG6vendor/composer/composer/src/Composer/Package/Link.phpaOEGvendor/composer/composer/src/Composer/Package/Version/VersionParser.php a 3MդHvendor/composer/composer/src/Composer/Package/Version/VersionGuesser.php?a? Ivendor/composer/composer/src/Composer/Package/Version/VersionSelector.php(a(Zb]Ivendor/composer/composer/src/Composer/Package/Version/StabilityFilter.php)a)?ZՠDvendor/composer/composer/src/Composer/Package/Dumper/ArrayDumper.php3a3#=vendor/composer/composer/src/Composer/Package/BasePackage.phpaH^2vendor/composer/composer/src/Composer/Composer.phpa:Fvendor/composer/composer/src/Composer/Json/JsonValidationException.phpeaey̤>vendor/composer/composer/src/Composer/Json/JsonManipulator.php+Qa+Q@<vendor/composer/composer/src/Composer/Json/JsonFormatter.phpaD|7vendor/composer/composer/src/Composer/Json/JsonFile.phpK+aK+[9?vendor/composer/composer/src/Composer/Platform/HhvmDetector.phpac˲:vendor/composer/composer/src/Composer/Platform/Version.php a 4n:vendor/composer/composer/src/Composer/Platform/Runtime.phpN aN *`*vendor/composer/composer/src/bootstrap.phpau)!vendor/composer/autoload_real.phpE aE dV%vendor/composer/autoload_classmap.phpa;봤2vendor/composer/spdx-licenses/src/SpdxLicenses.phpw&aw&N*vendor/composer/ca-bundle/src/CaBundle.phpDaDAʤvendor/composer/installed.phpQSaQSוD-vendor/composer/xdebug-handler/src/Status.php+a+&e0vendor/composer/xdebug-handler/src/PhpConfig.phpaQ=D4vendor/composer/xdebug-handler/src/XdebugHandler.phpoUaoU.vendor/composer/xdebug-handler/src/Process.phpw aw ð #vendor/composer/autoload_static.php.a.!"Ds*vendor/composer/pcre/src/PcreException.phpae,6vendor/composer/pcre/src/MatchAllWithOffsetsResult.phpa-"vendor/composer/pcre/src/Regex.phpaaaX~!vendor/composer/pcre/src/Preg.php>*a>*6% Ƥ(vendor/composer/pcre/src/MatchResult.phpa+vendor/composer/pcre/src/MatchAllResult.phpvav*:*vendor/composer/pcre/src/ReplaceResult.phpaK23vendor/composer/pcre/src/MatchWithOffsetsResult.phpapa8w'vendor/composer/autoload_namespaces.phpa'o泤%vendor/composer/InstalledVersions.php:a:T""vendor/composer/autoload_files.php a m,vendor/composer/semver/src/VersionParser.phpTaTwN'vendor/composer/semver/src/Interval.php~a~E|V4vendor/composer/semver/src/Constraint/Constraint.php1a1.9vendor/composer/semver/src/Constraint/MultiConstraint.phpA%aA%w=vendor/composer/semver/src/Constraint/ConstraintInterface.phpavx/vendor/composer/semver/src/Constraint/Bound.phpe ae M=vendor/composer/semver/src/Constraint/MatchNoneConstraint.phpa?T><vendor/composer/semver/src/Constraint/MatchAllConstraint.phpaD(vendor/composer/semver/src/Intervals.phpOaO3)vendor/composer/semver/src/Comparator.phpD aD Ș܁/vendor/composer/semver/src/CompilingMatcher.phpa%vendor/composer/semver/src/Semver.php? a? =:vendor/composer/metadata-minifier/src/MetadataMinifier.php a l3!vendor/composer/autoload_psr4.php\ a\ 3դ"vendor/composer/platform_check.phpaTt'vendor/symfony/polyfill-ctype/Ctype.php}a}\谤+vendor/symfony/polyfill-ctype/bootstrap.phpa01)vendor/symfony/filesystem/LockHandler.php>a>8H <vendor/symfony/filesystem/Exception/IOExceptionInterface.phpai3vendor/symfony/filesystem/Exception/IOException.phpaёA:vendor/symfony/filesystem/Exception/ExceptionInterface.phpah=vendor/symfony/filesystem/Exception/FileNotFoundException.phpa0z¤(vendor/symfony/filesystem/Filesystem.phpGtaGt.WXDvendor/symfony/console/DependencyInjection/AddConsoleCommandPass.phpaA|\0vendor/symfony/console/Input/InputDefinition.php+a+|,vendor/symfony/console/Input/StringInput.phpF aF c:.vendor/symfony/console/Input/InputArgument.phpO aO (&vendor/symfony/console/Input/Input.phprarUjx9vendor/symfony/console/Input/StreamableInputInterface.phpiai/vendor/symfony/console/Input/InputInterface.phpaΈڤ4vendor/symfony/console/Input/InputAwareInterface.php:a: '+vendor/symfony/console/Input/ArrayInput.php0a0mQj*vendor/symfony/console/Input/ArgvInput.php&/a&/,vendor/symfony/console/Input/InputOption.phpa6],vendor/symfony/console/Question/Question.phpaΊ2vendor/symfony/console/Question/ChoiceQuestion.php]a]5'8vendor/symfony/console/Question/ConfirmationQuestion.php#a#ρ&vendor/symfony/console/Application.php6a6Ac/vendor/symfony/console/Tester/CommandTester.phpaDd3vendor/symfony/console/Tester/ApplicationTester.php a Z=vendor/symfony/console/Exception/InvalidArgumentException.phpau i=vendor/symfony/console/Exception/CommandNotFoundException.phpa.o3vendor/symfony/console/Exception/LogicException.phpaSML7vendor/symfony/console/Exception/ExceptionInterface.phpaU;vendor/symfony/console/Exception/InvalidOptionException.phpa;5vendor/symfony/console/Exception/RuntimeException.phpa*b8vendor/symfony/console/Descriptor/MarkdownDescriptor.phpaf 0>3vendor/symfony/console/Descriptor/XmlDescriptor.php#a#dK9vendor/symfony/console/Descriptor/DescriptorInterface.phpaPZ4vendor/symfony/console/Descriptor/JsonDescriptor.phpan<vendor/symfony/console/Descriptor/ApplicationDescription.phpa0vendor/symfony/console/Descriptor/Descriptor.phpd ad T4vendor/symfony/console/Descriptor/TextDescriptor.phpf0af0W6vendor/symfony/console/EventListener/ErrorListener.php a e?ߤ6vendor/symfony/console/Helper/DebugFormatterHelper.phpHaH]R`A(vendor/symfony/console/Helper/Helper.phpa}Ku-vendor/symfony/console/Helper/ProgressBar.php?Ca?C81vendor/symfony/console/Helper/HelperInterface.phppapn,vendor/symfony/console/Helper/TableStyle.php{a{pn0vendor/symfony/console/Helper/QuestionHelper.phpBaBݗ^'vendor/symfony/console/Helper/Table.phpMaM!<2vendor/symfony/console/Helper/InputAwareHelper.phpa0vendor/symfony/console/Helper/TableSeparator.phpa& 3vendor/symfony/console/Helper/ProgressIndicator.phpaДP+vendor/symfony/console/Helper/TableCell.phpRaRJ{1vendor/symfony/console/Helper/FormatterHelper.php a U+vendor/symfony/console/Helper/HelperSet.php a 3C 2vendor/symfony/console/Helper/DescriptorHelper.php a 7vendor/symfony/console/Helper/SymfonyQuestionHelper.phpa9/vendor/symfony/console/Helper/ProcessHelper.phpat?vendor/symfony/console/CommandLoader/CommandLoaderInterface.phpa?vendor/symfony/console/CommandLoader/ContainerCommandLoader.php>a>#*=vendor/symfony/console/CommandLoader/FactoryCommandLoader.php7a7m_/vendor/symfony/console/Logger/ConsoleLogger.phpaU/vendor/symfony/console/Output/ConsoleOutput.phpaJDb1vendor/symfony/console/Output/OutputInterface.php a &0vendor/symfony/console/Output/BufferedOutput.phpIaI\(vendor/symfony/console/Output/Output.php{a{78vendor/symfony/console/Output/ConsoleOutputInterface.phpa.vendor/symfony/console/Output/StreamOutput.phpoao ~,vendor/symfony/console/Output/NullOutput.phpnantD#vendor/symfony/console/Terminal.phpau.vendor/symfony/console/Command/HelpCommand.php? a? '*vendor/symfony/console/Command/Command.phpLaLF0vendor/symfony/console/Command/LockableTrait.phpaO.vendor/symfony/console/Command/ListCommand.php a #/vendor/symfony/console/Style/StyleInterface.php( a( 5 -vendor/symfony/console/Style/SymfonyStyle.phpO/aO/A z,vendor/symfony/console/Style/OutputStyle.php a ~9vendor/symfony/console/Formatter/OutputFormatterStyle.php2a2SBvendor/symfony/console/Formatter/OutputFormatterStyleInterface.php<a<Z4vendor/symfony/console/Formatter/OutputFormatter.phpaNx>vendor/symfony/console/Formatter/OutputFormatterStyleStack.php a Ҥ=vendor/symfony/console/Formatter/OutputFormatterInterface.phpa#7u6vendor/symfony/console/Event/ConsoleTerminateEvent.phpa{e-vendor/symfony/console/Event/ConsoleEvent.phpaxS*6vendor/symfony/console/Event/ConsoleExceptionEvent.phpTaT2vendor/symfony/console/Event/ConsoleErrorEvent.phpal4vendor/symfony/console/Event/ConsoleCommandEvent.php%a%{˾(vendor/symfony/console/ConsoleEvents.php!a!Ω%vendor/symfony/finder/SplFileInfo.phpWaW!tVb6vendor/symfony/finder/Exception/ExceptionInterface.php"a"ڀfW9vendor/symfony/finder/Exception/AccessDeniedException.phpacWޤ3vendor/symfony/finder/Comparator/DateComparator.phpaGä/vendor/symfony/finder/Comparator/Comparator.phpa~4ؤ5vendor/symfony/finder/Comparator/NumberComparator.php a :vendor/symfony/finder/Iterator/DateRangeFilterIterator.phpa$99=vendor/symfony/finder/Iterator/RecursiveDirectoryIterator.php.a.sӤ9vendor/symfony/finder/Iterator/FilenameFilterIterator.phpa p:vendor/symfony/finder/Iterator/SizeRangeFilterIterator.phpa8K7vendor/symfony/finder/Iterator/CustomFilterIterator.phpa 3vendor/symfony/finder/Iterator/SortableIterator.php@ a@ [ <vendor/symfony/finder/Iterator/FilecontentFilterIterator.phpar~1vendor/symfony/finder/Iterator/FilterIterator.phpaVASsAvendor/symfony/finder/Iterator/ExcludeDirectoryFilterIterator.php a t褤=vendor/symfony/finder/Iterator/MultiplePcreFilterIterator.phpz az 6즤5vendor/symfony/finder/Iterator/PathFilterIterator.phpa};vendor/symfony/finder/Iterator/DepthRangeFilterIterator.phpa*9vendor/symfony/finder/Iterator/FileTypeFilterIterator.php>a>pY vendor/symfony/finder/Finder.php{Na{NIvendor/symfony/finder/Glob.phpa{Dvendor/symfony/polyfill-mbstring/Resources/mb_convert_variables.php8?a?Fvendor/symfony/polyfill-mbstring/Resources/unidata/titleCaseRegexp.php9a9>|zK@vendor/symfony/polyfill-mbstring/Resources/unidata/upperCase.php`a`S@vendor/symfony/polyfill-mbstring/Resources/unidata/lowerCase.php_a_Z.vendor/symfony/polyfill-mbstring/bootstrap.phpam|D-vendor/symfony/polyfill-mbstring/Mbstring.phpnanp^C%vendor/symfony/process/PhpProcess.php a )vendor/symfony/process/ProcessBuilder.php a j'Uh.vendor/symfony/process/PhpExecutableFinder.phpG aG _#=vendor/symfony/process/Exception/InvalidArgumentException.phpa˅3vendor/symfony/process/Exception/LogicException.phpaW;vendor/symfony/process/Exception/ProcessFailedException.phpaP57vendor/symfony/process/Exception/ExceptionInterface.phpa<=vendor/symfony/process/Exception/ProcessTimedOutException.php{a{45vendor/symfony/process/Exception/RuntimeException.phpa>H/vendor/symfony/process/Pipes/PipesInterface.phpa+*vendor/symfony/process/Pipes/UnixPipes.php,a,q-vendor/symfony/process/Pipes/WindowsPipes.phpuauu.vendor/symfony/process/Pipes/AbstractPipes.phpa<&vendor/symfony/process/InputStream.php a w'Ȥ+vendor/symfony/process/ExecutableFinder.php" a" %"x'vendor/symfony/process/ProcessUtils.phpaaXa"vendor/symfony/process/Process.phpa̞',vendor/react/promise/src/RejectedPromise.phpaM0%vendor/react/promise/src/Deferred.phpa ̤&vendor/react/promise/src/functions.php/a/ hz8vendor/react/promise/src/CancellablePromiseInterface.phpa\lb.vendor/react/promise/src/PromisorInterface.phpaw;-vendor/react/promise/src/PromiseInterface.phpa^ Ǥ6vendor/react/promise/src/Exception/LengthException.php^a^?q$vendor/react/promise/src/Promise.php"a"m5vendor/react/promise/src/ExtendedPromiseInterface.phpv av I(vendor/react/promise/src/LazyPromise.phpaMJ8vendor/react/promise/src/UnhandledRejectionException.phpeaeg4ߤ.vendor/react/promise/src/functions_include.phpaaa|-vendor/react/promise/src/FulfilledPromise.php^a^v.vendor/react/promise/src/CancellationQueue.phpuaun6vendor/wp-cli/entity-command/src/Menu_Item_Command.php9a9h6vendor/wp-cli/entity-command/src/Network_Namespace.php*a*{d?vendor/wp-cli/entity-command/src/WP_CLI/CommandWithDBObject.phpa#Jvendor/wp-cli/entity-command/src/WP_CLI/Entity/NonExistentKeyException.phpa̱hRvendor/wp-cli/entity-command/src/WP_CLI/Entity/RecursiveDataStructureTraverser.phpaLm8vendor/wp-cli/entity-command/src/WP_CLI/Entity/Utils.phpay㦤<vendor/wp-cli/entity-command/src/WP_CLI/CommandWithTerms.phpeae(Q>;vendor/wp-cli/entity-command/src/WP_CLI/CommandWithMeta.php;9a;9y 6vendor/wp-cli/entity-command/src/Post_Meta_Command.phpa1vendor/wp-cli/entity-command/src/User_Command.phpʼnaʼn2p1vendor/wp-cli/entity-command/src/Post_Command.phpqaqn9vendor/wp-cli/entity-command/src/Comment_Meta_Command.phpaCq4vendor/wp-cli/entity-command/src/Comment_Command.phpBaB\K1vendor/wp-cli/entity-command/src/Site_Command.phpUaUνFvendor/wp-cli/entity-command/src/User_Application_Password_Command.phpR@aR@Y>6vendor/wp-cli/entity-command/src/Post_Term_Command.phpa)79vendor/wp-cli/entity-command/src/User_Session_Command.phpai8vendor/wp-cli/entity-command/src/Site_Option_Command.php'a'WW6vendor/wp-cli/entity-command/src/User_Meta_Command.php$a$RI|K1vendor/wp-cli/entity-command/src/Menu_Command.php a ͤ6vendor/wp-cli/entity-command/src/Term_Meta_Command.phpaBs:vendor/wp-cli/entity-command/src/Menu_Location_Command.phpad1vendor/wp-cli/entity-command/src/Term_Command.phpKaKG3vendor/wp-cli/entity-command/src/Option_Command.php,?a,?u9vendor/wp-cli/entity-command/src/Network_Meta_Command.php]a]kN5vendor/wp-cli/entity-command/src/Taxonomy_Command.phpao?6vendor/wp-cli/entity-command/src/User_Term_Command.php&a&{b6vendor/wp-cli/entity-command/src/Site_Meta_Command.phpav6vendor/wp-cli/entity-command/src/Post_Type_Command.phpaz_/vendor/wp-cli/entity-command/entity-command.php a =3vendor/wp-cli/i18n-command/src/PhpCodeExtractor.phpag1vendor/wp-cli/i18n-command/src/BlockExtractor.phpaj{T3vendor/wp-cli/i18n-command/src/MapCodeExtractor.phpacd8vendor/wp-cli/i18n-command/src/IterableCodeExtractor.php"a"Pr5vendor/wp-cli/i18n-command/src/JsFunctionsScanner.php8.a8.8K2vendor/wp-cli/i18n-command/src/JsCodeExtractor.phpa-K3vendor/wp-cli/i18n-command/src/CommandNamespace.phpalh6vendor/wp-cli/i18n-command/src/PhpFunctionsScanner.php- a- >5vendor/wp-cli/i18n-command/src/ThemeJsonExtractor.phpaKw1vendor/wp-cli/i18n-command/src/MakePotCommand.php~a~&/vendor/wp-cli/i18n-command/src/PotGenerator.phpaä2vendor/wp-cli/i18n-command/src/MakeJsonCommand.php)/a)/xk/vendor/wp-cli/i18n-command/src/JedGenerator.phpmamʓ0vendor/wp-cli/i18n-command/src/MakeMoCommand.phpFaF&+vendor/wp-cli/i18n-command/i18n-command.phpap3vendor/wp-cli/config-command/src/Config_Command.phpnan;\/vendor/wp-cli/config-command/config-command.phpa7Q4vendor/wp-cli/widget-command/src/Sidebar_Command.phpad3vendor/wp-cli/widget-command/src/Widget_Command.phpjGajG/vendor/wp-cli/widget-command/widget-command.phpDaD Evendor/wp-cli/maintenance-mode-command/src/MaintenanceModeCommand.php7 a7 02=@Cvendor/wp-cli/maintenance-mode-command/maintenance-mode-command.phpLaL9v45vendor/wp-cli/shell-command/src/WP_CLI/Shell/REPL.php a Db1vendor/wp-cli/shell-command/src/Shell_Command.phpUaUn*-vendor/wp-cli/shell-command/shell-command.php a ٷ#vendor/wp-cli/wp-cli/utils/find-php\a\\)+vendor/wp-cli/wp-cli/utils/contrib-list.phpaTE:@vendor/wp-cli/wp-cli/utils/get-package-require-from-composer.phpa Bvendor/wp-cli/search-replace-command/src/WP_CLI/SearchReplacer.phpRaR$eCvendor/wp-cli/search-replace-command/src/Search_Replace_Command.php_a_$?vendor/wp-cli/search-replace-command/search-replace-command.php7a7<vendor/wp-cli/package-command/src/WP_CLI/JsonManipulator.phpSaSϢ5vendor/wp-cli/package-command/src/Package_Command.phpHaH;O1vendor/wp-cli/package-command/package-command.php:a:`7,vendor/wp-cli/php-cli-tools/http-console.phpa>$vendor/wp-cli/php-cli-tools/test.phpiai3M0vendor/wp-cli/php-cli-tools/lib/cli/Progress.php a a-vendor/wp-cli/php-cli-tools/lib/cli/Shell.phpF aF 6}^4vendor/wp-cli/php-cli-tools/lib/cli/progress/Bar.php a n(O6vendor/wp-cli/php-cli-tools/lib/cli/notify/Spinner.phpa8u3vendor/wp-cli/php-cli-tools/lib/cli/notify/Dots.phpdadwz.vendor/wp-cli/php-cli-tools/lib/cli/Colors.phpk!ak!y6)1vendor/wp-cli/php-cli-tools/lib/cli/Arguments.php/a/,7{-vendor/wp-cli/php-cli-tools/lib/cli/Table.phpat*/vendor/wp-cli/php-cli-tools/lib/cli/Streams.php#a#6o-Bvendor/wp-cli/php-cli-tools/lib/cli/arguments/InvalidArguments.phpa#:vendor/wp-cli/php-cli-tools/lib/cli/arguments/Argument.php a =7vendor/wp-cli/php-cli-tools/lib/cli/arguments/Lexer.php^ a^ G<vendor/wp-cli/php-cli-tools/lib/cli/arguments/HelpScreen.php a O$v/vendor/wp-cli/php-cli-tools/lib/cli/Memoize.phpa5vendor/wp-cli/php-cli-tools/lib/cli/tree/Renderer.phpOaOc"Ӥ2vendor/wp-cli/php-cli-tools/lib/cli/tree/Ascii.phpxax95vendor/wp-cli/php-cli-tools/lib/cli/tree/Markdown.phpaT魤+vendor/wp-cli/php-cli-tools/lib/cli/cli.php@a@~zפ6vendor/wp-cli/php-cli-tools/lib/cli/table/Renderer.phpGaGYߒ5vendor/wp-cli/php-cli-tools/lib/cli/table/Tabular.phpal3vendor/wp-cli/php-cli-tools/lib/cli/table/Ascii.php a @%=,vendor/wp-cli/php-cli-tools/lib/cli/Tree.phpa@.vendor/wp-cli/php-cli-tools/lib/cli/Notify.phpa,5vendor/wp-cli/php-cli-tools/lib/cli/unicode/regex.phpbabɶ9ͤ1vendor/wp-cli/embed-command/src/Fetch_Command.php$a$1vendor/wp-cli/embed-command/src/Cache_Command.phpamP4vendor/wp-cli/embed-command/src/Embeds_Namespace.phpaq3vendor/wp-cli/embed-command/src/Handler_Command.phpa`W4vendor/wp-cli/embed-command/src/Provider_Command.phpa}t4*vendor/wp-cli/embed-command/src/oEmbed.phpap>-vendor/wp-cli/embed-command/embed-command.phpxax#Τ+vendor/wp-cli/core-command/core-command.phpa:Ivendor/wp-cli/core-command/src/WP_CLI/Core/NonDestructiveCoreUpgrader.phpa!z~;vendor/wp-cli/core-command/src/WP_CLI/Core/CoreUpgrader.phpaZN*/vendor/wp-cli/core-command/src/Core_Command.phpa׮1vendor/wp-cli/cache-command/src/Cache_Command.php1a15Ť5vendor/wp-cli/cache-command/src/Transient_Command.phpDaDJo-vendor/wp-cli/cache-command/cache-command.phpCaC&'e1vendor/wp-cli/media-command/src/Media_Command.php&a&v-vendor/wp-cli/media-command/media-command.phpa6XGvendor/wp-cli/checksum-command/src/WP_CLI/Fetchers/UnfilteredPlugin.php:a:<g<vendor/wp-cli/checksum-command/src/Checksum_Base_Command.php a N<vendor/wp-cli/checksum-command/src/Checksum_Core_Command.phpa!M>vendor/wp-cli/checksum-command/src/Checksum_Plugin_Command.php1!a1!3g21?vendor/wp-cli/checksum-command/src/Plugin_Command_Namespace.phpa:f=vendor/wp-cli/checksum-command/src/Core_Command_Namespace.phpa{3vendor/wp-cli/checksum-command/checksum-command.phpa[zs-vendor/wp-cli/mustangostang-spyc/src/Spyc.php˅a˅~)vendor/wp-cli/mustangostang-spyc/Spyc.phpax7vendor/wp-cli/mustangostang-spyc/includes/functions.phpa:/vendor/wp-cli/mustangostang-spyc/php4/spyc.php4IuaIu-..vendor/wp-cli/mustangostang-spyc/php4/5to4.phpauS])/vendor/wp-cli/mustangostang-spyc/php4/test.php4a f3P?vendor/wp-cli/wp-config-transformer/src/WPConfigTransformer.php)a)c5vendor/wp-cli/cron-command/src/Cron_Event_Command.php9a9Ż8vendor/wp-cli/cron-command/src/Cron_Schedule_Command.phpi ai @/vendor/wp-cli/cron-command/src/Cron_Command.php a S+vendor/wp-cli/cron-command/cron-command.phpa.5vendor/wp-cli/rewrite-command/src/Rewrite_Command.php0a0;1vendor/wp-cli/rewrite-command/rewrite-command.phpaK+vendor/wp-cli/eval-command/eval-command.php=a=Ak3vendor/wp-cli/eval-command/src/EvalFile_Command.php+a+7>/vendor/wp-cli/eval-command/src/Eval_Command.php]a]'3vendor/wp-cli/scaffold-command/scaffold-command.phpa洃7vendor/wp-cli/scaffold-command/src/Scaffold_Command.phpaEA;vendor/wp-cli/scaffold-command/templates/block-php.mustache.a.F^9vendor/wp-cli/super-admin-command/super-admin-command.phpa=vendor/wp-cli/super-admin-command/src/Super_Admin_Command.phpaECvendor/wp-cli/extension-command/src/WP_CLI/ParsePluginNameInput.phpaG0-Hvendor/wp-cli/extension-command/src/WP_CLI/DestructivePluginUpgrader.phpaIɤGvendor/wp-cli/extension-command/src/WP_CLI/DestructiveThemeUpgrader.phpaS>vendor/wp-cli/extension-command/src/WP_CLI/Fetchers/Plugin.php"a" F =vendor/wp-cli/extension-command/src/WP_CLI/Fetchers/Theme.php a 3Bvendor/wp-cli/extension-command/src/WP_CLI/ParseThemeNameInput.phphahGAvendor/wp-cli/extension-command/src/WP_CLI/CommandWithUpgrade.php\a\Ť9vendor/wp-cli/extension-command/src/Theme_Mod_Command.phpa{(6vendor/wp-cli/extension-command/src/Plugin_Command.php6a6ƒBvendor/wp-cli/extension-command/src/Plugin_AutoUpdates_Command.php`a`(5vendor/wp-cli/extension-command/src/Theme_Command.php(`a(`Ş3Avendor/wp-cli/extension-command/src/Theme_AutoUpdates_Command.phpa5vendor/wp-cli/extension-command/extension-command.php)a)4vendor/wp-cli/export-command/src/WP_Map_Iterator.php@a@ѱ3vendor/wp-cli/export-command/src/Export_Command.phpZ3aZ3Ic~4vendor/wp-cli/export-command/src/WP_Export_Query.php25a257vendor/wp-cli/export-command/src/WP_Export_Returner.phpBaBM=vendor/wp-cli/export-command/src/WP_Export_Term_Exception.phpCaCǤAvendor/wp-cli/export-command/src/WP_Export_Split_Files_Writer.php- a- %@i:vendor/wp-cli/export-command/src/WP_Export_File_Writer.php>a>R}T5vendor/wp-cli/export-command/src/WP_Export_Oxymel.phpal;:vendor/wp-cli/export-command/src/WP_Export_Base_Writer.phpaǴb<vendor/wp-cli/export-command/src/WP_Export_WXR_Formatter.php&a&[{4:vendor/wp-cli/export-command/src/WP_Iterator_Exception.php9a9w9vendor/wp-cli/export-command/src/WP_Post_IDs_Iterator.phpIaIC8vendor/wp-cli/export-command/src/WP_Export_Exception.php>a>M<vendor/wp-cli/export-command/src/WP_Export_XML_Over_HTTP.phpaaa]*vendor/wp-cli/export-command/functions.phppap/vendor/wp-cli/export-command/export-command.phpa2d3vendor/wp-cli/server-command/src/Server_Command.php, a, 6\'vendor/wp-cli/server-command/router.phpNaN7/vendor/wp-cli/server-command/server-command.phpa+Dvendor/wp-cli/language-command/src/WP_CLI/CommandWithTranslation.php a ,٤Bvendor/wp-cli/language-command/src/WP_CLI/LanguagePackUpgrader.php a n<vendor/wp-cli/language-command/src/Core_Language_Command.php&a&) ʤ>vendor/wp-cli/language-command/src/Plugin_Language_Command.php3a3BMCvendor/wp-cli/language-command/src/Site_Switch_Language_Command.phpa1=vendor/wp-cli/language-command/src/Theme_Language_Command.php]1a]1k9vendor/wp-cli/language-command/src/Language_Namespace.phpa)|3vendor/wp-cli/language-command/language-command.php1a17g7vendor/wp-cli/role-command/src/Capabilities_Command.php3a3/vendor/wp-cli/role-command/src/Role_Command.phpr)ar)/Ԥ+vendor/wp-cli/role-command/role-command.php;a;av3vendor/wp-cli/import-command/src/Import_Command.php9a9/vendor/wp-cli/import-command/import-command.phpaʤ+vendor/wp-cli/db-command/src/DB_Command.phpeaeψ'vendor/wp-cli/db-command/db-command.phpakvendor/nb/oxymel/Oxymel.php)a)(rդvendor/nb/oxymel/OxymelTest.phpaBLV3vendor/psr/log/Psr/Log/InvalidArgumentException.php`a` X1/vendor/psr/log/Psr/Log/LoggerAwareInterface.php)a)j +vendor/psr/log/Psr/Log/LoggerAwareTrait.phpaQ')vendor/psr/log/Psr/Log/AbstractLogger.php a G*vendor/psr/log/Psr/Log/LoggerInterface.php* a* 1b!q%vendor/psr/log/Psr/Log/NullLogger.phpaI#vendor/psr/log/Psr/Log/LogLevel.phpPaP&vendor/psr/log/Psr/Log/LoggerTrait.phpW aW Wj5vendor/seld/jsonlint/src/Seld/JsonLint/JsonParser.php(Qa(Q{0vendor/seld/jsonlint/src/Seld/JsonLint/Lexer.phpa`4vendor/seld/jsonlint/src/Seld/JsonLint/Undefined.phpag@vendor/seld/jsonlint/src/Seld/JsonLint/DuplicateKeyException.phpRaRy#;vendor/seld/jsonlint/src/Seld/JsonLint/ParsingException.phpa;)vendor/seld/phar-utils/src/Timestamps.php'a'%v%vendor/seld/phar-utils/src/Linter.php a '4#Fvendor/justinrainbow/json-schema/src/JsonSchema/Entity/JsonPointer.php> a> WIvendor/justinrainbow/json-schema/src/JsonSchema/UriRetrieverInterface.phpa5|[;vendor/justinrainbow/json-schema/src/JsonSchema/Rfc3339.phpvavx$Avendor/justinrainbow/json-schema/src/JsonSchema/SchemaStorage.php+a+_Vvendor/justinrainbow/json-schema/src/JsonSchema/Exception/InvalidArgumentException.phpyayWΖ]vendor/justinrainbow/json-schema/src/JsonSchema/Exception/InvalidSchemaMediaTypeException.phpWaW%*)֤Tvendor/justinrainbow/json-schema/src/JsonSchema/Exception/InvalidConfigException.phpQaQJRvendor/justinrainbow/json-schema/src/JsonSchema/Exception/UriResolverException.phpJaJ-Pvendor/justinrainbow/json-schema/src/JsonSchema/Exception/ExceptionInterface.phpIaI%|^vendor/justinrainbow/json-schema/src/JsonSchema/Exception/UnresolvableJsonPointerException.phpa.miTvendor/justinrainbow/json-schema/src/JsonSchema/Exception/InvalidSchemaException.phpNaNݠSvendor/justinrainbow/json-schema/src/JsonSchema/Exception/JsonDecodingException.phpa٣Qvendor/justinrainbow/json-schema/src/JsonSchema/Exception/ValidationException.phpaEB*Nvendor/justinrainbow/json-schema/src/JsonSchema/Exception/RuntimeException.phpaaa`Wvendor/justinrainbow/json-schema/src/JsonSchema/Exception/ResourceNotFoundException.phpTaT:Wvendor/justinrainbow/json-schema/src/JsonSchema/Exception/InvalidSourceUriException.php\a\iRHvendor/justinrainbow/json-schema/src/JsonSchema/UriResolverInterface.phpaIѤJvendor/justinrainbow/json-schema/src/JsonSchema/Constraints/Constraint.phpaדPvendor/justinrainbow/json-schema/src/JsonSchema/Constraints/SchemaConstraint.phpafK Tvendor/justinrainbow/json-schema/src/JsonSchema/Constraints/CollectionConstraint.phpaN\vendor/justinrainbow/json-schema/src/JsonSchema/Constraints/TypeCheck/TypeCheckInterface.phpac>ϤYvendor/justinrainbow/json-schema/src/JsonSchema/Constraints/TypeCheck/StrictTypeCheck.php+a+QܤXvendor/justinrainbow/json-schema/src/JsonSchema/Constraints/TypeCheck/LooseTypeCheck.phpa Pvendor/justinrainbow/json-schema/src/JsonSchema/Constraints/FormatConstraint.php"a"ͥ#KPvendor/justinrainbow/json-schema/src/JsonSchema/Constraints/NumberConstraint.php a 9|Svendor/justinrainbow/json-schema/src/JsonSchema/Constraints/ConstraintInterface.phpatePvendor/justinrainbow/json-schema/src/JsonSchema/Constraints/StringConstraint.phpa~GGvendor/justinrainbow/json-schema/src/JsonSchema/Constraints/Factory.phpFaF:0/Pvendor/justinrainbow/json-schema/src/JsonSchema/Constraints/ObjectConstraint.phpaFgcSvendor/justinrainbow/json-schema/src/JsonSchema/Constraints/UndefinedConstraint.php>a>}Nvendor/justinrainbow/json-schema/src/JsonSchema/Constraints/TypeConstraint.php&a&ٌ]Nvendor/justinrainbow/json-schema/src/JsonSchema/Constraints/EnumConstraint.phpqaq` Nvendor/justinrainbow/json-schema/src/JsonSchema/Constraints/BaseConstraint.php%a%2Kvendor/justinrainbow/json-schema/src/JsonSchema/Iterator/ObjectIterator.php a HCvendor/justinrainbow/json-schema/src/JsonSchema/Uri/UriResolver.php;a;Dvendor/justinrainbow/json-schema/src/JsonSchema/Uri/UriRetriever.php$a$XhXvendor/justinrainbow/json-schema/src/JsonSchema/Uri/Retrievers/UriRetrieverInterface.phpa?r@Rvendor/justinrainbow/json-schema/src/JsonSchema/Uri/Retrievers/PredefinedArray.phpa0Rvendor/justinrainbow/json-schema/src/JsonSchema/Uri/Retrievers/FileGetContents.phpj aj [}x#Gvendor/justinrainbow/json-schema/src/JsonSchema/Uri/Retrievers/Curl.phpoaonTvendor/justinrainbow/json-schema/src/JsonSchema/Uri/Retrievers/AbstractRetriever.phprarM=vendor/justinrainbow/json-schema/src/JsonSchema/Validator.php a 8Jvendor/justinrainbow/json-schema/src/JsonSchema/SchemaStorageInterface.php"a"x&72vendor/gettext/gettext/src/TranslatorInterface.php% a% *vendor/gettext/gettext/src/Translation.php+a+93vendor/gettext/gettext/src/Utils/ParsedFunction.phpab5vendor/gettext/gettext/src/Utils/FunctionsScanner.phpIaIahk$7vendor/gettext/gettext/src/Utils/JsFunctionsScanner.php$a$(d2vendor/gettext/gettext/src/Utils/ParsedComment.php\ a\ ܺC4vendor/gettext/gettext/src/Utils/DictionaryTrait.phpa'8vendor/gettext/gettext/src/Utils/PhpFunctionsScanner.phpa(lФ1vendor/gettext/gettext/src/Utils/StringReader.phpaP:vendor/gettext/gettext/src/Utils/HeadersExtractorTrait.php~a~Ф?vendor/gettext/gettext/src/Utils/MultidimensionalArrayTrait.php a i$a-vendor/gettext/gettext/src/Utils/CsvTrait.phpaS*:vendor/gettext/gettext/src/Utils/HeadersGeneratorTrait.php7a76ÉG2vendor/gettext/gettext/src/Generators/PhpArray.phpaH;7vendor/gettext/gettext/src/Generators/CsvDictionary.phpaCT 8vendor/gettext/gettext/src/Generators/JsonDictionary.php=a={-vendor/gettext/gettext/src/Generators/Jed.phppap,k./vendor/gettext/gettext/src/Generators/Xliff.php6a6þQ<vendor/gettext/gettext/src/Generators/GeneratorInterface.phpa g,vendor/gettext/gettext/src/Generators/Mo.phpa0Ф-vendor/gettext/gettext/src/Generators/Csv.phpa{.vendor/gettext/gettext/src/Generators/Json.phpOaOf'N3vendor/gettext/gettext/src/Generators/Generator.phpa38vendor/gettext/gettext/src/Generators/YamlDictionary.phpaL5.vendor/gettext/gettext/src/Generators/Yaml.phpa,vendor/gettext/gettext/src/Generators/Po.php a (S+vendor/gettext/gettext/src/Translations.php@a@;)vendor/gettext/gettext/src/Translator.phpaJ>0vendor/gettext/gettext/src/GettextTranslator.phpa޼|$vendor/gettext/gettext/src/Merge.phpDaDm6P3vendor/gettext/gettext/src/translator_functions.php0a0Ȥ2vendor/gettext/gettext/src/Extractors/PhpArray.phpGaG/DѤ7vendor/gettext/gettext/src/Extractors/CsvDictionary.phpRaRWG.vendor/gettext/gettext/src/Extractors/Twig.php<a<K8vendor/gettext/gettext/src/Extractors/JsonDictionary.php(a(-*<vendor/gettext/gettext/src/Extractors/ExtractorInterface.php a p}YФ/vendor/gettext/gettext/src/Extractors/VueJs.phpn6an6>Avendor/gettext/gettext/src/Extractors/ExtractorMultiInterface.phpa]-vendor/gettext/gettext/src/Extractors/Jed.phpa{c/vendor/gettext/gettext/src/Extractors/Xliff.php a W}0vendor/gettext/gettext/src/Extractors/JsCode.php(a(8vU,vendor/gettext/gettext/src/Extractors/Mo.phpmamep-vendor/gettext/gettext/src/Extractors/Csv.phpam8¤.vendor/gettext/gettext/src/Extractors/Json.php.a.w=u1vendor/gettext/gettext/src/Extractors/PhpCode.php?a?…'/vendor/gettext/gettext/src/Extractors/Blade.phpa5 x8vendor/gettext/gettext/src/Extractors/YamlDictionary.phpQaQts3vendor/gettext/gettext/src/Extractors/Extractor.phpa {n.vendor/gettext/gettext/src/Extractors/Yaml.php]a]jя",vendor/gettext/gettext/src/Extractors/Po.phpaۘ-vendor/gettext/gettext/src/BaseTranslator.phpa3vĤ)vendor/gettext/gettext/src/autoloader.phpaAp)vendor/gettext/languages/src/CldrData.phpM6aM6-)vendor/gettext/languages/src/Category.phpaj1vendor/gettext/languages/src/FormulaConverter.phpa3|L)vendor/gettext/languages/src/Language.php>a>z:+vendor/gettext/languages/src/autoloader.phpZaZ.r-vendor/gettext/languages/src/Exporter/Xml.phpae$e4vendor/gettext/languages/src/Exporter/Prettyjson.phpDaDV& .vendor/gettext/languages/src/Exporter/Docs.php}a}Y-vendor/gettext/languages/src/Exporter/Php.phpaԭE.vendor/gettext/languages/src/Exporter/Json.phpanG2vendor/gettext/languages/src/Exporter/Exporter.phpa<..vendor/gettext/languages/src/Exporter/Ruby.phpa5 U.vendor/gettext/languages/src/Exporter/Html.php a K/,vendor/gettext/languages/src/Exporter/Po.phpaA)vendor/mck89/peast/lib/Peast/Renderer.phpAaA(5&vendor/mck89/peast/lib/Peast/Peast.phpa}p&vendor/mck89/peast/lib/Peast/Query.php@ a@ ޖؤ8vendor/mck89/peast/lib/Peast/Selector/Node/Part/Part.phprarY'?vendor/mck89/peast/lib/Peast/Selector/Node/Part/PseudoIndex.php- a- F@vendor/mck89/peast/lib/Peast/Selector/Node/Part/PseudoSimple.phpkakx׷Bvendor/mck89/peast/lib/Peast/Selector/Node/Part/PseudoSelector.php_a_˗ :vendor/mck89/peast/lib/Peast/Selector/Node/Part/Pseudo.phpa@-핤=vendor/mck89/peast/lib/Peast/Selector/Node/Part/Attribute.phpa28vendor/mck89/peast/lib/Peast/Selector/Node/Part/Type.phpa)4vendor/mck89/peast/lib/Peast/Selector/Node/Group.php a H;7vendor/mck89/peast/lib/Peast/Selector/Node/Selector.phpa:rդ9vendor/mck89/peast/lib/Peast/Selector/Node/Combinator.phpSaS0vendor/mck89/peast/lib/Peast/Selector/Parser.phpBaBG d3vendor/mck89/peast/lib/Peast/Selector/Exception.phpaBQ:1vendor/mck89/peast/lib/Peast/Selector/Matches.phpaކM*vendor/mck89/peast/lib/Peast/Traverser.phpa8+7vendor/mck89/peast/lib/Peast/Syntax/ES2021/Features.phpaKYA/vendor/mck89/peast/lib/Peast/Syntax/Scanner.phpa v87vendor/mck89/peast/lib/Peast/Syntax/ES2022/Features.phpCaC 7vendor/mck89/peast/lib/Peast/Syntax/ES2015/Features.phpa@6vendor/mck89/peast/lib/Peast/Syntax/ParserAbstract.php^$a^$43vendor/mck89/peast/lib/Peast/Syntax/JSX/Scanner.phps as ֙Ԥ2vendor/mck89/peast/lib/Peast/Syntax/JSX/Parser.php6a6դ8vendor/mck89/peast/lib/Peast/Syntax/CommentsRegistry.php(a(#<vendor/mck89/peast/lib/Peast/Syntax/Node/SwitchStatement.phpa:vendor/mck89/peast/lib/Peast/Syntax/Node/NewExpression.phpa>4vendor/mck89/peast/lib/Peast/Syntax/Node/Program.php a Ԁv<vendor/mck89/peast/lib/Peast/Syntax/Node/ImportSpecifier.phpa!I;vendor/mck89/peast/lib/Peast/Syntax/Node/ThrowStatement.phplal;vendor/mck89/peast/lib/Peast/Syntax/Node/ForInStatement.php a P㛤?vendor/mck89/peast/lib/Peast/Syntax/Node/PropertyDefinition.php a HCvendor/mck89/peast/lib/Peast/Syntax/Node/ImportDefaultSpecifier.phpaJ:vendor/mck89/peast/lib/Peast/Syntax/Node/WithStatement.phpiai$6vendor/mck89/peast/lib/Peast/Syntax/Node/Function_.php a wv=vendor/mck89/peast/lib/Peast/Syntax/Node/BinaryExpression.phpavX $9vendor/mck89/peast/lib/Peast/Syntax/Node/ForStatement.phpC aC 18vendor/mck89/peast/lib/Peast/Syntax/Node/RestElement.phpa7^g;vendor/mck89/peast/lib/Peast/Syntax/Node/NumericLiteral.phpa <vendor/mck89/peast/lib/Peast/Syntax/Node/UnaryExpression.phpR aR +%69vendor/mck89/peast/lib/Peast/Syntax/Node/TryStatement.php a 8"Evendor/mck89/peast/lib/Peast/Syntax/Node/ImportNamespaceSpecifier.phpaMc<vendor/mck89/peast/lib/Peast/Syntax/Node/AwaitExpression.phpUaUCvendor/mck89/peast/lib/Peast/Syntax/Node/ExportNamedDeclaration.php a <vendor/mck89/peast/lib/Peast/Syntax/Node/ArrayExpression.phpat8@vendor/mck89/peast/lib/Peast/Syntax/Node/FunctionDeclaration.phpa7 9vendor/mck89/peast/lib/Peast/Syntax/Node/MetaProperty.phpa: 8vendor/mck89/peast/lib/Peast/Syntax/Node/IfStatement.php a %=vendor/mck89/peast/lib/Peast/Syntax/Node/ClassDeclaration.phpa%^j?vendor/mck89/peast/lib/Peast/Syntax/Node/VariableDeclarator.phpab٤>vendor/mck89/peast/lib/Peast/Syntax/Node/PrivateIdentifier.phpao >vendor/mck89/peast/lib/Peast/Syntax/Node/JSX/JSXIdentifier.phpaL=vendor/mck89/peast/lib/Peast/Syntax/Node/JSX/JSXAttribute.php}a}Cvendor/mck89/peast/lib/Peast/Syntax/Node/JSX/JSXSpreadAttribute.phpaK&QBvendor/mck89/peast/lib/Peast/Syntax/Node/JSX/JSXClosingElement.phpafGvendor/mck89/peast/lib/Peast/Syntax/Node/JSX/JSXExpressionContainer.phpa]{W?vendor/mck89/peast/lib/Peast/Syntax/Node/JSX/JSXSpreadChild.phpaiMb8vendor/mck89/peast/lib/Peast/Syntax/Node/JSX/JSXText.phpaƑCvendor/mck89/peast/lib/Peast/Syntax/Node/JSX/JSXOpeningFragment.phpa<vendor/mck89/peast/lib/Peast/Syntax/Node/JSX/JSXFragment.php a ̌ԷCvendor/mck89/peast/lib/Peast/Syntax/Node/JSX/JSXClosingFragment.phpaN\;vendor/mck89/peast/lib/Peast/Syntax/Node/JSX/JSXElement.php a .*kBCvendor/mck89/peast/lib/Peast/Syntax/Node/JSX/JSXEmptyExpression.phpa:8Bvendor/mck89/peast/lib/Peast/Syntax/Node/JSX/JSXNamespacedName.phpzazԄ=Bvendor/mck89/peast/lib/Peast/Syntax/Node/JSX/JSXOpeningElement.phpaD&Cvendor/mck89/peast/lib/Peast/Syntax/Node/JSX/JSXBoundaryElement.phpaDvendor/mck89/peast/lib/Peast/Syntax/Node/JSX/JSXMemberExpression.phpfafh=vendor/mck89/peast/lib/Peast/Syntax/Node/MethodDefinition.phpaw\@vendor/mck89/peast/lib/Peast/Syntax/Node/ExpressionStatement.phpa[m6vendor/mck89/peast/lib/Peast/Syntax/Node/ClassBody.php,a,78vendor/mck89/peast/lib/Peast/Syntax/Node/NullLiteral.phpaK=vendor/mck89/peast/lib/Peast/Syntax/Node/DoWhileStatement.php2a2:vendor/mck89/peast/lib/Peast/Syntax/Node/ObjectPattern.phpaܤ;vendor/mck89/peast/lib/Peast/Syntax/Node/ForOfStatement.phpa,4vendor/mck89/peast/lib/Peast/Syntax/Node/Pattern.phpa1vendor/mck89/peast/lib/Peast/Syntax/Node/Node.php a \Evendor/mck89/peast/lib/Peast/Syntax/Node/TaggedTemplateExpression.php\a\-<vendor/mck89/peast/lib/Peast/Syntax/Node/TemplateElement.php a 7Ѥ<vendor/mck89/peast/lib/Peast/Syntax/Node/TemplateLiteral.phpk ak ;`;vendor/mck89/peast/lib/Peast/Syntax/Node/WhileStatement.php*a*ꮓ8vendor/mck89/peast/lib/Peast/Syntax/Node/StaticBlock.phpaX~V>vendor/mck89/peast/lib/Peast/Syntax/Node/LogicalExpression.phpZaZS:vendor/mck89/peast/lib/Peast/Syntax/Node/BigIntLiteral.phpaAͤ<vendor/mck89/peast/lib/Peast/Syntax/Node/ChainExpression.phpae7vendor/mck89/peast/lib/Peast/Syntax/Node/SwitchCase.phpXaXE :vendor/mck89/peast/lib/Peast/Syntax/Node/RegExpLiteral.phpJ aJ tqDvendor/mck89/peast/lib/Peast/Syntax/Node/ArrowFunctionExpression.phpaх>vendor/mck89/peast/lib/Peast/Syntax/Node/ContinueStatement.phpaZY4vendor/mck89/peast/lib/Peast/Syntax/Node/Literal.phpa5ʤ;vendor/mck89/peast/lib/Peast/Syntax/Node/CallExpression.phpWaW 7vendor/mck89/peast/lib/Peast/Syntax/Node/Identifier.phpaO7vendor/mck89/peast/lib/Peast/Syntax/Node/Expression.phpazw!>vendor/mck89/peast/lib/Peast/Syntax/Node/ImportDeclaration.phpaX99vendor/mck89/peast/lib/Peast/Syntax/Node/ArrayPattern.phpaʑ(:vendor/mck89/peast/lib/Peast/Syntax/Node/StringLiteral.php a 5vEvendor/mck89/peast/lib/Peast/Syntax/Node/ExportDefaultDeclaration.php"a"S(6=vendor/mck89/peast/lib/Peast/Syntax/Node/LabeledStatement.phpa(6vendor/mck89/peast/lib/Peast/Syntax/Node/Statement.phpa>vendor/mck89/peast/lib/Peast/Syntax/Node/ModuleDeclaration.phpant8vendor/mck89/peast/lib/Peast/Syntax/Node/CatchClause.phpa@vendor/mck89/peast/lib/Peast/Syntax/Node/VariableDeclaration.phpcacI;vendor/mck89/peast/lib/Peast/Syntax/Node/BooleanLiteral.phpIaIm|Dvendor/mck89/peast/lib/Peast/Syntax/Node/ParenthesizedExpression.phpa(Ӥ;vendor/mck89/peast/lib/Peast/Syntax/Node/BreakStatement.phpyay 2<vendor/mck89/peast/lib/Peast/Syntax/Node/YieldExpression.php0a0)Avendor/mck89/peast/lib/Peast/Syntax/Node/ExportAllDeclaration.phpa";vendor/mck89/peast/lib/Peast/Syntax/Node/ThisExpression.phpa%a:vendor/mck89/peast/lib/Peast/Syntax/Node/SpreadElement.phpa$Ug9vendor/mck89/peast/lib/Peast/Syntax/Node/ChainElement.phpa" +<vendor/mck89/peast/lib/Peast/Syntax/Node/ExportSpecifier.phpa(2vendor/mck89/peast/lib/Peast/Syntax/Node/Super.phpaRC(;vendor/mck89/peast/lib/Peast/Syntax/Node/EmptyStatement.phpan>vendor/mck89/peast/lib/Peast/Syntax/Node/DebuggerStatement.phpadi4vendor/mck89/peast/lib/Peast/Syntax/Node/Comment.phpsasɖ?vendor/mck89/peast/lib/Peast/Syntax/Node/SequenceExpression.phpa=vendor/mck89/peast/lib/Peast/Syntax/Node/ObjectExpression.phpa'!E<vendor/mck89/peast/lib/Peast/Syntax/Node/ModuleSpecifier.phpaGjCBvendor/mck89/peast/lib/Peast/Syntax/Node/ConditionalExpression.php a =vendor/mck89/peast/lib/Peast/Syntax/Node/UpdateExpression.php; a; =vendor/mck89/peast/lib/Peast/Syntax/Node/ImportExpression.phpOaOL8vendor/mck89/peast/lib/Peast/Syntax/Node/Declaration.phpa845vendor/mck89/peast/lib/Peast/Syntax/Node/Property.phpar=vendor/mck89/peast/lib/Peast/Syntax/Node/MemberExpression.php` a` G?vendor/mck89/peast/lib/Peast/Syntax/Node/FunctionExpression.phpaU<vendor/mck89/peast/lib/Peast/Syntax/Node/ClassExpression.phpap~ä>vendor/mck89/peast/lib/Peast/Syntax/Node/AssignmentPattern.phpaRY<vendor/mck89/peast/lib/Peast/Syntax/Node/ReturnStatement.phpa:CAvendor/mck89/peast/lib/Peast/Syntax/Node/AssignmentExpression.php2 a2 T?vendor/mck89/peast/lib/Peast/Syntax/Node/AssignmentProperty.phpaAۤ;vendor/mck89/peast/lib/Peast/Syntax/Node/BlockStatement.phpa3vendor/mck89/peast/lib/Peast/Syntax/Node/Class_.phpyayn.vendor/mck89/peast/lib/Peast/Syntax/Parser.php7a7`p0vendor/mck89/peast/lib/Peast/Syntax/Features.php a ӛ-vendor/mck89/peast/lib/Peast/Syntax/Token.phpaB0vendor/mck89/peast/lib/Peast/Syntax/Position.phpakwG5vendor/mck89/peast/lib/Peast/Syntax/EventsEmitter.phpa{u1vendor/mck89/peast/lib/Peast/Syntax/Exception.php a 7]i7vendor/mck89/peast/lib/Peast/Syntax/ES2019/Features.phpa0^-vendor/mck89/peast/lib/Peast/Syntax/Utils.php&a&Ls6vendor/mck89/peast/lib/Peast/Syntax/SourceLocation.phpa<+vendor/mck89/peast/lib/Peast/Syntax/LSM.phpa/9vendor/mck89/peast/lib/Peast/Syntax/EncodingException.phpaLp7vendor/mck89/peast/lib/Peast/Syntax/ES2018/Features.php(a(pf7vendor/mck89/peast/lib/Peast/Syntax/ES2017/Features.phpaH@7vendor/mck89/peast/lib/Peast/Syntax/ES2016/Features.phpat~7vendor/mck89/peast/lib/Peast/Syntax/ES2020/Features.phpaO3vendor/mck89/peast/lib/Peast/Formatter/Expanded.phpax2vendor/mck89/peast/lib/Peast/Formatter/Compact.phpat/vendor/mck89/peast/lib/Peast/Formatter/Base.php a 6Ԥ6vendor/mck89/peast/lib/Peast/Formatter/PrettyPrint.phprarjϤ+vendor/wp-cli/wp-cli/templates/man.mustacheaI<2vendor/wp-cli/wp-cli/templates/man-params.mustache?a?n9vendor/wp-cli/config-command/templates/wp-config.mustache6 a6 xǤ6vendor/wp-cli/core-command/templates/versions.mustacheaSU"?vendor/wp-cli/extension-command/templates/theme-status.mustachexaxǁgѤ@vendor/wp-cli/extension-command/templates/plugin-status.mustacheao[5>vendor/wp-cli/scaffold-command/templates/theme-status.mustachexaxǁgѤ9vendor/wp-cli/scaffold-command/templates/phpunit.xml.dista%FBvendor/wp-cli/scaffold-command/templates/plugin-gitignore.mustache^a^怤Avendor/wp-cli/scaffold-command/templates/plugin-packages.mustache^a^5+Avendor/wp-cli/scaffold-command/templates/theme-bootstrap.mustachea>ʤDvendor/wp-cli/scaffold-command/templates/plugin-test-sample.mustache'a'\~Cvendor/wp-cli/scaffold-command/templates/taxonomy_extended.mustachea{,8vendor/wp-cli/scaffold-command/templates/plugin.mustacheqaqGqҤ8vendor/wp-cli/scaffold-command/templates/.phpcs.xml.dist=a=[-Bvendor/wp-cli/scaffold-command/templates/plugin-gruntfile.mustacheaTCvendor/wp-cli/scaffold-command/templates/theme-test-sample.mustache&a&:vendor/wp-cli/scaffold-command/templates/taxonomy.mustachei ai Rx6vendor/wp-cli/scaffold-command/templates/.editorconfigahѤBvendor/wp-cli/scaffold-command/templates/block-editor-css.mustachea,V]Avendor/wp-cli/scaffold-command/templates/block-style-css.mustacheaؤ?vendor/wp-cli/scaffold-command/templates/plugin-circle.mustache< a< y5Cvendor/wp-cli/scaffold-command/templates/plugin-distignore.mustacheHaH1EA@vendor/wp-cli/scaffold-command/templates/block-index-js.mustache a "0Bvendor/wp-cli/scaffold-command/templates/plugin-bitbucket.mustacheaZBgmDvendor/wp-cli/scaffold-command/templates/post_type_extended.mustachea;~eBvendor/wp-cli/scaffold-command/templates/plugin-bootstrap.mustachea?vendor/wp-cli/scaffold-command/templates/plugin-gitlab.mustache\a\ <vendor/wp-cli/scaffold-command/templates/install-wp-tests.shtat?vendor/wp-cli/scaffold-command/templates/plugin-readme.mustacheab =vendor/wp-cli/scaffold-command/templates/child_theme.mustacheaGvendor/wp-cli/scaffold-command/templates/child_theme_functions.mustacheNaN.;vendor/wp-cli/scaffold-command/templates/post_type.mustache a 3$?vendor/wp-cli/scaffold-command/templates/plugin-travis.mustacheaaGRvendor/autoload.phpa Q vendor/composer/composer/LICENSE,a,Vg1vendor/composer/composer/res/composer-schema.jsona-<vendor/rmccue/requests/library/Requests/Transport/cacert.pem>a>UV&vendor/wp-cli/wp-cli/COMPOSER_VERSIONSaiyvendor/wp-cli/wp-cli/VERSIONaTdP..." * into [ optional=>false, type=>positional, repeating=>true, name=>object-id ] */ class SynopsisParser { /** * @param string $synopsis A synopsis * @return array List of parameters */ public static function parse( $synopsis ) { $tokens = array_filter( preg_split( '/[\s\t]+/', $synopsis ) ); $params = []; foreach ( $tokens as $token ) { $param = self::classify_token( $token ); // Some types of parameters shouldn't be mandatory if ( isset( $param['optional'] ) && ! $param['optional'] ) { if ( 'flag' === $param['type'] || ( 'assoc' === $param['type'] && $param['value']['optional'] ) ) { $param['type'] = 'unknown'; } } $param['token'] = $token; $params[] = $param; } return $params; } /** * Render the Synopsis into a format string. * * @param array $synopsis A structured synopsis. This might get reordered * to match the parsed output. * @return string Rendered synopsis. */ public static function render( &$synopsis ) { if ( ! is_array( $synopsis ) ) { return ''; } $bits = [ 'positional' => '', 'assoc' => '', 'generic' => '', 'flag' => '', ]; $reordered_synopsis = [ 'positional' => [], 'assoc' => [], 'generic' => [], 'flag' => [], ]; foreach ( $bits as $key => &$value ) { foreach ( $synopsis as $arg ) { if ( empty( $arg['type'] ) || $key !== $arg['type'] ) { continue; } if ( empty( $arg['name'] ) && 'generic' !== $arg['type'] ) { continue; } if ( 'positional' === $key ) { $rendered_arg = "<{$arg['name']}>"; $reordered_synopsis['positional'] [] = $arg; } elseif ( 'assoc' === $key ) { $arg_value = isset( $arg['value']['name'] ) ? $arg['value']['name'] : $arg['name']; $rendered_arg = "--{$arg['name']}=<{$arg_value}>"; $reordered_synopsis['assoc'] [] = $arg; } elseif ( 'generic' === $key ) { $rendered_arg = '--='; $reordered_synopsis['generic'] [] = $arg; } elseif ( 'flag' === $key ) { $rendered_arg = "--{$arg['name']}"; $reordered_synopsis['flag'] [] = $arg; } if ( ! empty( $arg['repeating'] ) ) { $rendered_arg = "{$rendered_arg}..."; } if ( ! empty( $arg['optional'] ) ) { $rendered_arg = "[{$rendered_arg}]"; } $value .= "{$rendered_arg} "; } } $rendered = ''; foreach ( $bits as $v ) { if ( ! empty( $v ) ) { $rendered .= $v; } } $synopsis = array_merge( $reordered_synopsis['positional'], $reordered_synopsis['assoc'], $reordered_synopsis['generic'], $reordered_synopsis['flag'] ); return rtrim( $rendered, ' ' ); } /** * Classify argument attributes based on its syntax. * * @param string $token * @return array */ private static function classify_token( $token ) { $param = []; list( $param['optional'], $token ) = self::is_optional( $token ); list( $param['repeating'], $token ) = self::is_repeating( $token ); $p_name = '([a-z-_0-9]+)'; $p_value = '([a-zA-Z-_|,0-9]+)'; if ( '--=' === $token ) { $param['type'] = 'generic'; } elseif ( preg_match( "/^<($p_value)>$/", $token, $matches ) ) { $param['type'] = 'positional'; $param['name'] = $matches[1]; } elseif ( preg_match( "/^--(?:\\[no-\\])?$p_name/", $token, $matches ) ) { $param['name'] = $matches[1]; $value = substr( $token, strlen( $matches[0] ) ); // substr returns false <= PHP 5.6, and '' PHP 7+ if ( false === $value || '' === $value ) { $param['type'] = 'flag'; } else { $param['type'] = 'assoc'; list( $param['value']['optional'], $value ) = self::is_optional( $value ); if ( preg_match( "/^=<$p_value>$/", $value, $matches ) ) { $param['value']['name'] = $matches[1]; } else { $param = [ 'type' => 'unknown', ]; } } } else { $param['type'] = 'unknown'; } return $param; } /** * An optional parameter is surrounded by square brackets. * * @param string $token * @return array */ private static function is_optional( $token ) { if ( '[' === substr( $token, 0, 1 ) && ']' === substr( $token, -1 ) ) { return [ true, substr( $token, 1, -1 ) ]; } return [ false, $token ]; } /** * A repeating parameter is followed by an ellipsis. * * @param string $token * @return array */ private static function is_repeating( $token ) { if ( '...' === substr( $token, -3 ) ) { return [ true, substr( $token, 0, -3 ) ]; } return [ false, $token ]; } } log_in_as_admin_user(); require_once ABSPATH . 'wp-admin/admin.php'; }, 0 ); } /** * Ensure the current request is done under a logged-in administrator * account. * * A lot of premium plugins/themes have their custom update routines locked * behind an is_admin() call. * * @return void */ private function log_in_as_admin_user() { // TODO: Add logic to find an administrator user. $admin_user_id = 1; wp_set_current_user( $admin_user_id ); $expiration = time() + DAY_IN_SECONDS; $manager = WP_Session_Tokens::get_instance( $admin_user_id ); $token = $manager->create( $expiration ); $_COOKIE[ AUTH_COOKIE ] = wp_generate_auth_cookie( $admin_user_id, $expiration, 'auth', $token ); $_COOKIE[ SECURE_AUTH_COOKIE ] = wp_generate_auth_cookie( $admin_user_id, $expiration, 'secure_auth', $token ); } } */ const COMMANDS_TO_RUN_AS_ADMIN = [ [ 'plugin' ], [ 'theme' ], ]; /** * Context manager instance to use. * * @var ContextManager */ private $context_manager; /** * Instantiate an Auto object. * * @param ContextManager $context_manager Context manager instance to use. */ public function __construct( ContextManager $context_manager ) { $this->context_manager = $context_manager; } /** * Process the context to set up the environment correctly. * * @param array $config Associative array of configuration data. * @return void * @throws WP_CLI\ExitException If an invalid context was deduced. */ public function process( $config ) { $config['context'] = $this->deduce_best_context(); $this->context_manager->switch_context( $config ); } /** * Deduce the best context to run the current command in. * * @return string Context to use. */ private function deduce_best_context() { if ( $this->is_command_to_run_as_admin() ) { return Context::ADMIN; } return Context::CLI; } /** * Check whether the current WP-CLI command is amongst those we want to * run as admin. * * @return bool Whether the current command should be run as admin. */ private function is_command_to_run_as_admin() { $command = WP_CLI::get_runner()->arguments; foreach ( self::COMMANDS_TO_RUN_AS_ADMIN as $command_to_run_as_admin ) { if ( array_slice( $command, 0, count( $command_to_run_as_admin ) ) === $command_to_run_as_admin ) { WP_CLI::debug( 'Detected a command to be intercepted: ' . implode( ' ', $command ), Context::DEBUG_GROUP ); return true; } } return false; } } 'table', 'fields' => $fields, 'field' => null, ]; foreach ( [ 'format', 'fields', 'field' ] as $key ) { if ( isset( $assoc_args[ $key ] ) ) { $format_args[ $key ] = $assoc_args[ $key ]; unset( $assoc_args[ $key ] ); } } if ( ! is_array( $format_args['fields'] ) ) { $format_args['fields'] = explode( ',', $format_args['fields'] ); } $format_args['fields'] = array_map( 'trim', $format_args['fields'] ); $this->args = $format_args; $this->prefix = $prefix; } /** * Magic getter for arguments. * * @param string $key * @return mixed */ public function __get( $key ) { return $this->args[ $key ]; } /** * Display multiple items according to the output arguments. * * @param array|Iterator $items The items to display. * @param bool|array $ascii_pre_colorized Optional. A boolean or an array of booleans to pass to `format()` if items in the table are pre-colorized. Default false. */ public function display_items( $items, $ascii_pre_colorized = false ) { if ( $this->args['field'] ) { $this->show_single_field( $items, $this->args['field'] ); } else { if ( in_array( $this->args['format'], [ 'csv', 'json', 'table' ], true ) ) { $item = is_array( $items ) && ! empty( $items ) ? array_shift( $items ) : false; if ( $item && ! empty( $this->args['fields'] ) ) { foreach ( $this->args['fields'] as &$field ) { $field = $this->find_item_key( $item, $field ); } array_unshift( $items, $item ); } } if ( in_array( $this->args['format'], [ 'table', 'csv' ], true ) ) { if ( $items instanceof Iterator ) { $items = Utils\iterator_map( $items, [ $this, 'transform_item_values_to_json' ] ); } else { $items = array_map( [ $this, 'transform_item_values_to_json' ], $items ); } } $this->format( $items, $ascii_pre_colorized ); } } /** * Display a single item according to the output arguments. * * @param mixed $item * @param bool|array $ascii_pre_colorized Optional. A boolean or an array of booleans to pass to `show_multiple_fields()` if the item in the table is pre-colorized. Default false. */ public function display_item( $item, $ascii_pre_colorized = false ) { if ( isset( $this->args['field'] ) ) { $item = (object) $item; $key = $this->find_item_key( $item, $this->args['field'] ); $value = $item->$key; if ( in_array( $this->args['format'], [ 'table', 'csv' ], true ) && ( is_object( $value ) || is_array( $value ) ) ) { $value = json_encode( $value ); } WP_CLI::print_value( $value, [ 'format' => $this->args['format'], ] ); } else { $this->show_multiple_fields( $item, $this->args['format'], $ascii_pre_colorized ); } } /** * Format items according to arguments. * * @param array $items * @param bool|array $ascii_pre_colorized Optional. A boolean or an array of booleans to pass to `show_table()` if items in the table are pre-colorized. Default false. */ private function format( $items, $ascii_pre_colorized = false ) { $fields = $this->args['fields']; switch ( $this->args['format'] ) { case 'count': if ( ! is_array( $items ) ) { $items = iterator_to_array( $items ); } echo count( $items ); break; case 'ids': if ( ! is_array( $items ) ) { $items = iterator_to_array( $items ); } echo implode( ' ', $items ); break; case 'table': self::show_table( $items, $fields, $ascii_pre_colorized ); break; case 'csv': Utils\write_csv( STDOUT, $items, $fields ); break; case 'json': case 'yaml': $out = []; foreach ( $items as $item ) { $out[] = Utils\pick_fields( $item, $fields ); } if ( 'json' === $this->args['format'] ) { if ( defined( 'JSON_PARTIAL_OUTPUT_ON_ERROR' ) ) { // phpcs:ignore PHPCompatibility.Constants.NewConstants.json_partial_output_on_errorFound echo json_encode( $out, JSON_PARTIAL_OUTPUT_ON_ERROR ); } else { echo json_encode( $out ); } } elseif ( 'yaml' === $this->args['format'] ) { echo Spyc::YAMLDump( $out, 2, 0 ); } break; default: WP_CLI::error( 'Invalid format: ' . $this->args['format'] ); } } /** * Show a single field from a list of items. * * @param array $items Array of objects to show fields from * @param string $field The field to show */ private function show_single_field( $items, $field ) { $key = null; $values = []; foreach ( $items as $item ) { $item = (object) $item; if ( null === $key ) { $key = $this->find_item_key( $item, $field ); } if ( 'json' === $this->args['format'] ) { $values[] = $item->$key; } else { WP_CLI::print_value( $item->$key, [ 'format' => $this->args['format'], ] ); } } if ( 'json' === $this->args['format'] ) { echo json_encode( $values ); } } /** * Find an object's key. * If $prefix is set, a key with that prefix will be prioritized. * * @param object $item * @param string $field * @return string */ private function find_item_key( $item, $field ) { foreach ( [ $field, $this->prefix . '_' . $field ] as $maybe_key ) { if ( ( is_object( $item ) && ( property_exists( $item, $maybe_key ) || isset( $item->$maybe_key ) ) ) || ( is_array( $item ) && array_key_exists( $maybe_key, $item ) ) ) { $key = $maybe_key; break; } } if ( ! isset( $key ) ) { WP_CLI::error( "Invalid field: $field." ); } return $key; } /** * Show multiple fields of an object. * * @param object|array $data Data to display * @param string $format Format to display the data in * @param bool|array $ascii_pre_colorized Optional. A boolean or an array of booleans to pass to `show_table()` if the item in the table is pre-colorized. Default false. */ private function show_multiple_fields( $data, $format, $ascii_pre_colorized = false ) { $true_fields = []; foreach ( $this->args['fields'] as $field ) { $true_fields[] = $this->find_item_key( $data, $field ); } foreach ( $data as $key => $value ) { if ( ! in_array( $key, $true_fields, true ) ) { if ( is_array( $data ) ) { unset( $data[ $key ] ); } elseif ( is_object( $data ) ) { unset( $data->$key ); } } } switch ( $format ) { case 'table': case 'csv': $rows = $this->assoc_array_to_rows( $data ); $fields = [ 'Field', 'Value' ]; if ( 'table' === $format ) { self::show_table( $rows, $fields, $ascii_pre_colorized ); } elseif ( 'csv' === $format ) { Utils\write_csv( STDOUT, $rows, $fields ); } break; case 'yaml': case 'json': WP_CLI::print_value( $data, [ 'format' => $format, ] ); break; default: WP_CLI::error( 'Invalid format: ' . $format ); break; } } /** * Show items in a \cli\Table. * * @param array $items * @param array $fields * @param bool|array $ascii_pre_colorized Optional. A boolean or an array of booleans to pass to `Table::setAsciiPreColorized()` if items in the table are pre-colorized. Default false. */ private static function show_table( $items, $fields, $ascii_pre_colorized = false ) { $table = new Table(); $enabled = Colors::shouldColorize(); if ( $enabled ) { Colors::disable( true ); } $table->setAsciiPreColorized( $ascii_pre_colorized ); $table->setHeaders( $fields ); foreach ( $items as $item ) { $table->addRow( array_values( Utils\pick_fields( $item, $fields ) ) ); } foreach ( $table->getDisplayLines() as $line ) { WP_CLI::line( $line ); } if ( $enabled ) { Colors::enable( true ); } } /** * Format an associative array as a table. * * @param array $fields Fields and values to format * @return array */ private function assoc_array_to_rows( $fields ) { $rows = []; foreach ( $fields as $field => $value ) { if ( ! is_string( $value ) ) { $value = json_encode( $value ); } $rows[] = (object) [ 'Field' => $field, 'Value' => $value, ]; } return $rows; } /** * Transforms objects and arrays to JSON as necessary * * @param mixed $item * @return mixed */ public function transform_item_values_to_json( $item ) { foreach ( $this->args['fields'] as $field ) { $true_field = $this->find_item_key( $item, $field ); $value = is_object( $item ) ? $item->$true_field : $item[ $true_field ]; if ( is_array( $value ) || is_object( $value ) ) { if ( is_object( $item ) ) { $item->$true_field = json_encode( $value ); } elseif ( is_array( $item ) ) { $item[ $true_field ] = json_encode( $value ); } } } return $item; } } upgrader->strings[ $error ] ) ) { $error = $this->upgrader->strings[ $error ]; } // TODO: show all errors, not just the first one WP_CLI::warning( $error ); } /** * Process the feedback collected through the compat indirection. * * @param string $string String to use as feedback message. * @param array $args Array of additional arguments to process. */ public function process_feedback( $string, $args ) { if ( 'parent_theme_prepare_install' === $string ) { WP_CLI::get_http_cache_manager()->whitelist_package( $this->api->download_link, 'theme', $this->api->slug, $this->api->version ); } if ( isset( $this->upgrader->strings[ $string ] ) ) { $string = $this->upgrader->strings[ $string ]; } if ( ! empty( $args ) && strpos( $string, '%' ) !== false ) { $string = vsprintf( $string, $args ); } if ( empty( $string ) ) { return; } $string = str_replace( '…', '...', Utils\strip_tags( $string ) ); $string = html_entity_decode( $string, ENT_QUOTES, get_bloginfo( 'charset' ) ); WP_CLI::log( $string ); } } process_feedback( $string, $args ); } } process_feedback( $string, $args ); } } = 50600 ) { require_once __DIR__ . '/Min_PHP_5_6/FeedbackMethodTrait.php'; trait FeedbackMethodTrait { use Min_PHP_5_6\FeedbackMethodTrait; } return; } require_once __DIR__ . '/Min_PHP_5_4/FeedbackMethodTrait.php'; trait FeedbackMethodTrait { use Min_PHP_5_4\FeedbackMethodTrait; } write( STDERR, WP_CLI::colorize( "%RError:%n\n$message\n" ) ); $this->write( STDERR, WP_CLI::colorize( "%R---------%n\n\n" ) ); } /** * Write a string to a resource. * * @param resource $handle Commonly STDOUT or STDERR. * @param string $str Message to write. */ protected function write( $handle, $str ) { switch ( $handle ) { case STDOUT: $this->stdout .= $str; break; case STDERR: $this->stderr .= $str; break; } } /** * Starts output buffering, using a callback to capture output from `echo`, `print`, `printf` (which write to the output buffer 'php://output' rather than STDOUT). */ public function ob_start() { ob_start( [ $this, 'ob_start_callback' ], 1 ); } /** * Callback for `ob_start()`. * * @param string $str String to write. * @return string Returns zero-length string so nothing gets written to the output buffer. */ public function ob_start_callback( $str ) { $this->write( STDOUT, $str ); return ''; } /** * To match `ob_start() above. Does an `ob_end_flush()`. */ public function ob_end() { ob_end_flush(); } } in_color = $in_color; } /** * Write an informational message to STDOUT. * * @param string $message Message to write. */ public function info( $message ) { $this->write( STDOUT, $message . "\n" ); } /** * Write a success message, prefixed with "Success: ". * * @param string $message Message to write. */ public function success( $message ) { $this->_line( $message, 'Success', '%G' ); } /** * Write a warning message to STDERR, prefixed with "Warning: ". * * @param string $message Message to write. */ public function warning( $message ) { $this->_line( $message, 'Warning', '%C', STDERR ); } /** * Write an message to STDERR, prefixed with "Error: ". * * @param string $message Message to write. */ public function error( $message ) { $this->_line( $message, 'Error', '%R', STDERR ); } /** * Similar to error( $message ), but outputs $message in a red box. * * @param array $message_lines Message to write. */ public function error_multi_line( $message_lines ) { // Convert tabs to four spaces, as some shells will output the tabs as variable-length. $message_lines = array_map( function( $line ) { return str_replace( "\t", ' ', $line ); }, $message_lines ); $longest = max( array_map( 'strlen', $message_lines ) ); // Write an empty line before the message. $empty_line = Colors::colorize( '%w%1 ' . str_repeat( ' ', $longest ) . ' %n' ); $this->write( STDERR, "\n\t$empty_line\n" ); foreach ( $message_lines as $line ) { $padding = str_repeat( ' ', $longest - strlen( $line ) ); $line = Colors::colorize( "%w%1 $line $padding%n" ); $this->write( STDERR, "\t$line\n" ); } // Write an empty line after the message. $this->write( STDERR, "\t$empty_line\n\n" ); } } get_runner()->config['debug']; if ( ! $debug ) { return; } if ( true !== $debug && $group !== $debug ) { return; } $time = round( microtime( true ) - ( defined( 'WP_CLI_START_MICROTIME' ) ? WP_CLI_START_MICROTIME : $start_time ), 3 ); $prefix = 'Debug'; if ( $group && true === $debug ) { $prefix = 'Debug (' . $group . ')'; } $this->_line( "$message ({$time}s)", $prefix, '%B', STDERR ); } /** * Write a string to a resource. * * @param resource $handle Commonly STDOUT or STDERR. * @param string $str Message to write. */ protected function write( $handle, $str ) { fwrite( $handle, $str ); } /** * Output one line of message to a resource. * * @param string $message Message to write. * @param string $label Prefix message with a label. * @param string $color Colorize label with a given color. * @param resource $handle Resource to write to. Defaults to STDOUT. */ protected function _line( $message, $label, $color, $handle = STDOUT ) { // phpcs:ignore PSR2.Methods.MethodDeclaration.Underscore -- Used in third party extensions. if ( class_exists( 'cli\Colors' ) ) { $label = Colors::colorize( "$color$label:%n", $this->in_color ); } else { $label = "$label:"; } $this->write( $handle, "$label $message\n" ); } } in_color = $in_color; } /** * Informational messages aren't logged. * * @param string $message Message to write. */ public function info( $message ) { // Nothing. } /** * Success messages aren't logged. * * @param string $message Message to write. */ public function success( $message ) { // Nothing. } /** * Warning messages aren't logged. * * @param string $message Message to write. */ public function warning( $message ) { // Nothing. } /** * Write an error message to STDERR, prefixed with "Error: ". * * @param string $message Message to write. */ public function error( $message ) { $this->_line( $message, 'Error', '%R', STDERR ); } /** * Similar to error( $message ), but outputs $message in a red box. * * @param array $message_lines Message to write. */ public function error_multi_line( $message_lines ) { $message = implode( "\n", $message_lines ); $this->_line( $message, 'Error', '%R', STDERR ); $this->_line( '', '---------', '%R', STDERR ); } } */ private $contexts = []; /** * Store the current context. * * @var string Current context. */ private $current_context = Context::CLI; /** * Register a context with WP-CLI. * * @param string $name Name of the context. * @param Context $implementation Implementation of the context. */ public function register_context( $name, Context $implementation ) { $this->contexts[ $name ] = $implementation; } /** * Switch the context in which to run WP-CLI. * * @param array $config Associative array of configuration data. * @return void * * @throws ExitException When an invalid context was requested. */ public function switch_context( $config ) { $context = isset( $config['context'] ) ? $config['context'] : $this->current_context; if ( ! array_key_exists( $context, $this->contexts ) ) { WP_CLI::error( "Unknown context '{$context}'" ); } WP_CLI::debug( "Using context '{$context}'", Context::DEBUG_GROUP ); $this->current_context = $context; $this->contexts[ $context ]->process( $config ); } /** * Return the current context. * * @return string Current context. */ public function get_context() { return $this->current_context; } } cache = $cache; // hook into wp http api add_filter( 'pre_http_request', [ $this, 'filter_pre_http_request' ], 10, 3 ); add_filter( 'http_response', [ $this, 'filter_http_response' ], 10, 3 ); } /** * short circuit wp http api with cached file */ public function filter_pre_http_request( $response, $args, $url ) { // check if whitelisted if ( ! isset( $this->whitelist[ $url ] ) ) { return $response; } // check if downloading if ( 'GET' !== $args['method'] || empty( $args['filename'] ) ) { return $response; } // check cache and export to designated location $filename = $this->cache->has( $this->whitelist[ $url ]['key'], $this->whitelist[ $url ]['ttl'] ); if ( $filename ) { WP_CLI::log( sprintf( 'Using cached file \'%s\'...', $filename ) ); if ( copy( $filename, $args['filename'] ) ) { // simulate successful download response return [ 'response' => [ 'code' => 200, 'message' => 'OK', ], 'filename' => $args['filename'], ]; } WP_CLI::error( sprintf( 'Error copying cached file %s to %s', $filename, $url ) ); } return $response; } /** * cache wp http api downloads * * @param array $response * @param array $args * @param string $url * @return array */ public function filter_http_response( $response, $args, $url ) { // check if whitelisted if ( ! isset( $this->whitelist[ $url ] ) ) { return $response; } // check if downloading if ( 'GET' !== $args['method'] || empty( $args['filename'] ) ) { return $response; } // check if download was successful if ( is_wp_error( $response ) || 200 !== wp_remote_retrieve_response_code( $response ) ) { return $response; } // cache downloaded file $this->cache->import( $this->whitelist[ $url ]['key'], $response['filename'] ); return $response; } /** * whitelist a package url * * @param string $url * @param string $group package group (themes, plugins, ...) * @param string $slug package slug * @param string $version package version * @param int $ttl */ public function whitelist_package( $url, $group, $slug, $version, $ttl = null ) { $ext = pathinfo( Utils\parse_url( $url, PHP_URL_PATH ), PATHINFO_EXTENSION ); $key = "$group/$slug-$version.$ext"; $this->whitelist_url( $url, $key, $ttl ); wp_update_plugins(); } /** * whitelist a url * * @param string $url * @param string $key * @param int $ttl */ public function whitelist_url( $url, $key = null, $ttl = null ) { $key = $key ? : $url; $this->whitelist[ $url ] = compact( 'key', 'ttl' ); } /** * check if url is whitelisted * * @param string $url * @return bool */ public function is_whitelisted( $url ) { return isset( $this->whitelist[ $url ] ); } } */ const BYTE_ORDER_MARKS = [ 'UTF-8' => "\xEF\xBB\xBF", 'UTF-16 (BE)' => "\xFE\xFF", 'UTF-16 (LE)' => "\xFF\xFE", ]; private $global_config_path; private $project_config_path; private $config; private $extra_config; private $context_manager; private $alias; private $aliases; private $arguments; private $assoc_args; private $runtime_config; private $colorize = false; private $early_invoke = []; private $global_config_path_debug; private $project_config_path_debug; private $required_files; public function __get( $key ) { if ( '_' === $key[0] ) { return null; } return $this->$key; } public function register_context_manager( ContextManager $context_manager ) { $this->context_manager = $context_manager; } /** * Register a command for early invocation, generally before WordPress loads. * * @param string $when Named execution hook * @param Subcommand $command */ public function register_early_invoke( $when, $command ) { $this->early_invoke[ $when ][] = array_slice( Dispatcher\get_path( $command ), 1 ); } /** * Perform the early invocation of a command. * * @param string $when Named execution hook */ private function do_early_invoke( $when ) { if ( ! isset( $this->early_invoke[ $when ] ) ) { return; } // Search the value of @when from the command method. $real_when = ''; $r = $this->find_command_to_run( $this->arguments ); if ( is_array( $r ) ) { list( $command, $final_args, $cmd_path ) = $r; foreach ( $this->early_invoke as $_when => $_path ) { foreach ( $_path as $cmd ) { if ( $cmd === $cmd_path ) { $real_when = $_when; } } } } foreach ( $this->early_invoke[ $when ] as $path ) { if ( $this->cmd_starts_with( $path ) ) { if ( empty( $real_when ) || ( $real_when && $real_when === $when ) ) { $this->run_command_and_exit(); } } } } /** * Get the path to the global configuration YAML file. * * @param bool $create_config_file Optional. If a config file doesn't exist, * should it be created? Defaults to false. * * @return string|false */ public function get_global_config_path( $create_config_file = false ) { if ( getenv( 'WP_CLI_CONFIG_PATH' ) ) { $config_path = getenv( 'WP_CLI_CONFIG_PATH' ); $this->global_config_path_debug = 'Using global config from WP_CLI_CONFIG_PATH env var: ' . $config_path; } else { $config_path = Utils\get_home_dir() . '/.wp-cli/config.yml'; $this->global_config_path_debug = 'Using default global config: ' . $config_path; } // If global config doesn't exist create one. if ( true === $create_config_file && ! file_exists( $config_path ) ) { $this->global_config_path_debug = "Default global config doesn't exist, creating one in {$config_path}"; Process::create( Utils\esc_cmd( 'touch %s', $config_path ) )->run(); } if ( is_readable( $config_path ) ) { return $config_path; } $this->global_config_path_debug = 'No readable global config found'; return false; } /** * Get the path to the project-specific configuration * YAML file. * wp-cli.local.yml takes priority over wp-cli.yml. * * @return string|false */ public function get_project_config_path() { $config_files = [ 'wp-cli.local.yml', 'wp-cli.yml', ]; // Stop looking upward when we find we have emerged from a subdirectory // installation into a parent installation $project_config_path = Utils\find_file_upward( $config_files, getcwd(), static function ( $dir ) { static $wp_load_count = 0; $wp_load_path = $dir . DIRECTORY_SEPARATOR . 'wp-load.php'; if ( file_exists( $wp_load_path ) ) { ++ $wp_load_count; } return $wp_load_count > 1; } ); $this->project_config_path_debug = 'No project config found'; if ( ! empty( $project_config_path ) ) { $this->project_config_path_debug = 'Using project config: ' . $project_config_path; } return $project_config_path; } /** * Get the path to the packages directory * * @return string */ public function get_packages_dir_path() { if ( getenv( 'WP_CLI_PACKAGES_DIR' ) ) { $packages_dir = Utils\trailingslashit( getenv( 'WP_CLI_PACKAGES_DIR' ) ); } else { $packages_dir = Utils\get_home_dir() . '/.wp-cli/packages/'; } return $packages_dir; } /** * Attempts to find the path to the WP installation inside index.php * * @param string $index_path * @return string|false */ private static function extract_subdir_path( $index_path ) { $index_code = file_get_contents( $index_path ); if ( ! preg_match( '|^\s*require\s*\(?\s*(.+?)/wp-blog-header\.php([\'"])|m', $index_code, $matches ) ) { return false; } $wp_path_src = $matches[1] . $matches[2]; $wp_path_src = Utils\replace_path_consts( $wp_path_src, $index_path ); $wp_path = eval( "return $wp_path_src;" ); // phpcs:ignore Squiz.PHP.Eval.Discouraged if ( ! Utils\is_path_absolute( $wp_path ) ) { $wp_path = dirname( $index_path ) . "/$wp_path"; } return $wp_path; } /** * Find the directory that contains the WordPress files. * Defaults to the current working dir. * * @return string An absolute path */ private function find_wp_root() { if ( ! empty( $this->config['path'] ) ) { $path = $this->config['path']; if ( ! Utils\is_path_absolute( $path ) ) { $path = getcwd() . '/' . $path; } return $path; } if ( $this->cmd_starts_with( [ 'core', 'download' ] ) ) { return getcwd(); } $dir = getcwd(); while ( is_readable( $dir ) ) { if ( file_exists( "$dir/wp-load.php" ) ) { return $dir; } if ( file_exists( "$dir/index.php" ) ) { $path = self::extract_subdir_path( "$dir/index.php" ); if ( ! empty( $path ) ) { return $path; } } $parent_dir = dirname( $dir ); if ( empty( $parent_dir ) || $parent_dir === $dir ) { break; } $dir = $parent_dir; } } /** * Set WordPress root as a given path. * * @param string $path */ private static function set_wp_root( $path ) { if ( ! defined( 'ABSPATH' ) ) { // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedConstantFound -- Declaring a WP native constant. define( 'ABSPATH', Utils\normalize_path( Utils\trailingslashit( $path ) ) ); } elseif ( ! is_null( $path ) ) { WP_CLI::error_multi_line( [ 'The --path parameter cannot be used when ABSPATH is already defined elsewhere', 'ABSPATH is defined as: "' . ABSPATH . '"', ] ); } WP_CLI::debug( 'ABSPATH defined: ' . ABSPATH, 'bootstrap' ); $_SERVER['DOCUMENT_ROOT'] = realpath( $path ); } /** * Guess which URL context WP-CLI has been invoked under. * * @param array $assoc_args * @return string|false */ private static function guess_url( $assoc_args ) { if ( isset( $assoc_args['blog'] ) ) { $assoc_args['url'] = $assoc_args['blog']; } if ( isset( $assoc_args['url'] ) ) { $url = $assoc_args['url']; if ( true === $url ) { WP_CLI::warning( 'The --url parameter expects a value.' ); } return $url; } return false; } private function cmd_starts_with( $prefix ) { return array_slice( $this->arguments, 0, count( $prefix ) ) === $prefix; } /** * Given positional arguments, find the command to execute. * * @param array $args * @return array|string Command, args, and path on success; error message on failure */ public function find_command_to_run( $args ) { $command = WP_CLI::get_root_command(); WP_CLI::do_hook( 'find_command_to_run_pre' ); $cmd_path = []; while ( ! empty( $args ) && $command->can_have_subcommands() ) { $cmd_path[] = $args[0]; $full_name = implode( ' ', $cmd_path ); $subcommand = $command->find_subcommand( $args ); if ( ! $subcommand ) { if ( count( $cmd_path ) > 1 ) { $child = array_pop( $cmd_path ); $parent_name = implode( ' ', $cmd_path ); $suggestion = $this->get_subcommand_suggestion( $child, $command ); return sprintf( "'%s' is not a registered subcommand of '%s'. See 'wp help %s' for available subcommands.%s", $child, $parent_name, $parent_name, ! empty( $suggestion ) ? PHP_EOL . "Did you mean '{$suggestion}'?" : '' ); } $suggestion = $this->get_subcommand_suggestion( $full_name, $command ); return sprintf( "'%s' is not a registered wp command. See 'wp help' for available commands.%s", $full_name, ! empty( $suggestion ) ? PHP_EOL . "Did you mean '{$suggestion}'?" : '' ); } if ( $this->is_command_disabled( $subcommand ) ) { return sprintf( "The '%s' command has been disabled from the config file.", $full_name ); } $command = $subcommand; } return [ $command, $args, $cmd_path ]; } /** * Find the WP-CLI command to run given arguments, and invoke it. * * @param array $args Positional arguments including command name * @param array $assoc_args Associative arguments for the command. * @param array $options Configuration options for the function. */ public function run_command( $args, $assoc_args = [], $options = [] ) { WP_CLI::do_hook( 'before_run_command', $args, $assoc_args, $options ); if ( ! empty( $options['back_compat_conversions'] ) ) { list( $args, $assoc_args ) = self::back_compat_conversions( $args, $assoc_args ); } $r = $this->find_command_to_run( $args ); if ( is_string( $r ) ) { WP_CLI::error( $r ); } list( $command, $final_args, $cmd_path ) = $r; $name = implode( ' ', $cmd_path ); $extra_args = []; if ( isset( $this->extra_config[ $name ] ) ) { $extra_args = $this->extra_config[ $name ]; } WP_CLI::debug( 'Running command: ' . $name, 'bootstrap' ); try { $command->invoke( $final_args, $assoc_args, $extra_args ); } catch ( Exception $e ) { WP_CLI::error( $e->getMessage() ); } } /** * Show synopsis if the called command is a composite command */ public function show_synopsis_if_composite_command() { $r = $this->find_command_to_run( $this->arguments ); if ( is_array( $r ) ) { list( $command ) = $r; if ( $command->can_have_subcommands() ) { $command->show_usage(); exit; } } } private function run_command_and_exit( $help_exit_warning = '' ) { $this->show_synopsis_if_composite_command(); $this->run_command( $this->arguments, $this->assoc_args ); if ( $this->cmd_starts_with( [ 'help' ] ) ) { // Help couldn't find the command so exit with suggestion. $suggestion_or_disabled = $this->find_command_to_run( array_slice( $this->arguments, 1 ) ); if ( is_string( $suggestion_or_disabled ) ) { if ( $help_exit_warning ) { WP_CLI::warning( $help_exit_warning ); } WP_CLI::error( $suggestion_or_disabled ); } // Should never get here. } exit; } /** * Perform a command against a remote server over SSH (or a container using * scheme of "docker" or "docker-compose"). * * @param string $connection_string Passed connection string. * @return void */ private function run_ssh_command( $connection_string ) { WP_CLI::do_hook( 'before_ssh' ); $bits = Utils\parse_ssh_url( $connection_string ); $pre_cmd = getenv( 'WP_CLI_SSH_PRE_CMD' ); if ( $pre_cmd ) { $message = WP_CLI::warning( "WP_CLI_SSH_PRE_CMD found, executing the following command(s) on the remote machine:\n $pre_cmd" ); WP_CLI::log( $message ); $pre_cmd = rtrim( $pre_cmd, ';' ) . '; '; } if ( ! empty( $bits['path'] ) ) { $pre_cmd .= 'cd ' . escapeshellarg( $bits['path'] ) . '; '; } $env_vars = ''; if ( getenv( 'WP_CLI_STRICT_ARGS_MODE' ) ) { $env_vars .= 'WP_CLI_STRICT_ARGS_MODE=1 '; } $wp_binary = 'wp'; $wp_args = array_slice( $GLOBALS['argv'], 1 ); if ( $this->alias && ! empty( $wp_args[0] ) && $this->alias === $wp_args[0] ) { array_shift( $wp_args ); $runtime_alias = []; foreach ( $this->aliases[ $this->alias ] as $key => $value ) { if ( 'ssh' === $key ) { continue; } $runtime_alias[ $key ] = $value; } if ( ! empty( $runtime_alias ) ) { $encoded_alias = json_encode( [ $this->alias => $runtime_alias, ] ); $wp_binary = "WP_CLI_RUNTIME_ALIAS='{$encoded_alias}' {$wp_binary} {$this->alias}"; } } foreach ( $wp_args as $k => $v ) { if ( preg_match( '#--ssh=#', $v ) ) { unset( $wp_args[ $k ] ); } } $wp_command = $pre_cmd . $env_vars . $wp_binary . ' ' . implode( ' ', array_map( 'escapeshellarg', $wp_args ) ); $escaped_command = $this->generate_ssh_command( $bits, $wp_command ); passthru( $escaped_command, $exit_code ); if ( 255 === $exit_code ) { WP_CLI::error( 'Cannot connect over SSH using provided configuration.', 255 ); } else { exit( $exit_code ); } } /** * Generate a shell command from the parsed connection string. * * @param array $bits Parsed connection string. * @param string $wp_command WP-CLI command to run. * @return string */ private function generate_ssh_command( $bits, $wp_command ) { $escaped_command = ''; // Set default values. foreach ( [ 'scheme', 'user', 'host', 'port', 'path', 'key' ] as $bit ) { if ( ! isset( $bits[ $bit ] ) ) { $bits[ $bit ] = null; } WP_CLI::debug( 'SSH ' . $bit . ': ' . $bits[ $bit ], 'bootstrap' ); } $is_tty = function_exists( 'posix_isatty' ) && posix_isatty( STDOUT ); if ( 'docker' === $bits['scheme'] ) { $command = 'docker exec %s%s%s sh -c %s'; $escaped_command = sprintf( $command, $bits['user'] ? '--user ' . escapeshellarg( $bits['user'] ) . ' ' : '', $is_tty ? '-t ' : '', escapeshellarg( $bits['host'] ), escapeshellarg( $wp_command ) ); } if ( 'docker-compose' === $bits['scheme'] ) { $command = 'docker-compose exec %s%s%s sh -c %s'; $escaped_command = sprintf( $command, $bits['user'] ? '--user ' . escapeshellarg( $bits['user'] ) . ' ' : '', $is_tty ? '' : '-T ', escapeshellarg( $bits['host'] ), escapeshellarg( $wp_command ) ); } // Vagrant ssh-config. if ( 'vagrant' === $bits['scheme'] ) { $cache = WP_CLI::get_cache(); $cache_key = 'vagrant:' . $this->project_config_path; if ( $cache->has( $cache_key ) ) { $cached = $cache->read( $cache_key ); $values = json_decode( $cached, true ); } else { $ssh_config = shell_exec( 'vagrant ssh-config 2>/dev/null' ); if ( preg_match_all( '#\s*(?[a-zA-Z]+)\s(?.+)\s*#', $ssh_config, $matches ) ) { $values = array_combine( $matches['NAME'], $matches['VALUE'] ); $cache->write( $cache_key, json_encode( $values ) ); } } if ( empty( $bits['host'] ) || ( isset( $values['Host'] ) && $bits['host'] === $values['Host'] ) ) { $bits['scheme'] = 'ssh'; $bits['host'] = $values['HostName']; $bits['port'] = $values['Port']; $bits['user'] = $values['User']; $bits['key'] = $values['IdentityFile']; } // If we could not resolve the bits still, fallback to just `vagrant ssh` if ( 'vagrant' === $bits['scheme'] ) { $command = 'vagrant ssh -c %s %s'; $escaped_command = sprintf( $command, escapeshellarg( $wp_command ), escapeshellarg( $bits['host'] ) ); } } // Default scheme is SSH. if ( 'ssh' === $bits['scheme'] || null === $bits['scheme'] ) { $command = 'ssh -q %s %s %s'; if ( $bits['user'] ) { $bits['host'] = $bits['user'] . '@' . $bits['host']; } $command_args = [ $bits['port'] ? '-p ' . (int) $bits['port'] . ' ' : '', $bits['key'] ? sprintf( '-i %s', $bits['key'] ) : '', $is_tty ? '-t' : '-T', ]; $escaped_command = sprintf( $command, implode( ' ', array_filter( $command_args ) ), escapeshellarg( $bits['host'] ), escapeshellarg( $wp_command ) ); } WP_CLI::debug( 'Running SSH command: ' . $escaped_command, 'bootstrap' ); return $escaped_command; } /** * Check whether a given command is disabled by the config. * * @return bool */ public function is_command_disabled( $command ) { $path = implode( ' ', array_slice( Dispatcher\get_path( $command ), 1 ) ); return in_array( $path, $this->config['disabled_commands'], true ); } /** * Returns wp-config.php code, skipping the loading of wp-settings.php. * * @param string $wp_config_path Optional. Config file path. If left empty, it tries to * locate the wp-config.php file automatically. * * @return string */ public function get_wp_config_code( $wp_config_path = '' ) { if ( empty( $wp_config_path ) ) { $wp_config_path = Utils\locate_wp_config(); } $wp_config_code = explode( "\n", file_get_contents( $wp_config_path ) ); // Detect and strip byte-order marks (BOMs). // This code assumes they can only be found on the first line. foreach ( self::BYTE_ORDER_MARKS as $bom_name => $bom_sequence ) { WP_CLI::debug( "Looking for {$bom_name} BOM", 'bootstrap' ); $length = strlen( $bom_sequence ); while ( substr( $wp_config_code[0], 0, $length ) === $bom_sequence ) { WP_CLI::warning( "{$bom_name} byte-order mark (BOM) detected in wp-config.php file, stripping it for parsing." ); $wp_config_code[0] = substr( $wp_config_code[0], $length ); } } $found_wp_settings = false; $lines_to_run = []; foreach ( $wp_config_code as $line ) { if ( preg_match( '/^\s*require.+wp-settings\.php/', $line ) ) { $found_wp_settings = true; continue; } $lines_to_run[] = $line; } if ( ! $found_wp_settings ) { WP_CLI::error( 'Strange wp-config.php file: wp-settings.php is not loaded directly.' ); } $source = implode( "\n", $lines_to_run ); $source = Utils\replace_path_consts( $source, $wp_config_path ); return preg_replace( '|^\s*\<\?php\s*|', '', $source ); } /** * Transparently convert deprecated syntaxes * * @param array $args * @param array $assoc_args * @return array */ private static function back_compat_conversions( $args, $assoc_args ) { $top_level_aliases = [ 'sql' => 'db', 'blog' => 'site', ]; if ( count( $args ) > 0 ) { foreach ( $top_level_aliases as $old => $new ) { if ( $old === $args[0] ) { $args[0] = $new; break; } } } // *-meta -> * meta if ( ! empty( $args ) && preg_match( '/(post|comment|user|network)-meta/', $args[0], $matches ) ) { array_shift( $args ); array_unshift( $args, 'meta' ); array_unshift( $args, $matches[1] ); } // cli aliases -> cli alias list if ( [ 'cli', 'aliases' ] === array_slice( $args, 0, 2 ) ) { list( $args[0], $args[1], $args[2] ) = [ 'cli', 'alias', 'list' ]; } // core (multsite-)install --admin_name= -> --admin_user= if ( count( $args ) > 0 && 'core' === $args[0] && isset( $assoc_args['admin_name'] ) ) { $assoc_args['admin_user'] = $assoc_args['admin_name']; unset( $assoc_args['admin_name'] ); } // core config -> config create if ( [ 'core', 'config' ] === array_slice( $args, 0, 2 ) ) { list( $args[0], $args[1] ) = [ 'config', 'create' ]; } // core language -> language core if ( [ 'core', 'language' ] === array_slice( $args, 0, 2 ) ) { list( $args[0], $args[1] ) = [ 'language', 'core' ]; } // checksum core -> core verify-checksums if ( [ 'checksum', 'core' ] === array_slice( $args, 0, 2 ) ) { list( $args[0], $args[1] ) = [ 'core', 'verify-checksums' ]; } // checksum plugin -> plugin verify-checksums if ( [ 'checksum', 'plugin' ] === array_slice( $args, 0, 2 ) ) { list( $args[0], $args[1] ) = [ 'plugin', 'verify-checksums' ]; } // site create --site_id= -> site create --network_id= if ( count( $args ) >= 2 && 'site' === $args[0] && 'create' === $args[1] && isset( $assoc_args['site_id'] ) ) { $assoc_args['network_id'] = $assoc_args['site_id']; unset( $assoc_args['site_id'] ); } // {plugin|theme} update-all -> {plugin|theme} update --all if ( count( $args ) > 1 && in_array( $args[0], [ 'plugin', 'theme' ], true ) && 'update-all' === $args[1] ) { $args[1] = 'update'; $assoc_args['all'] = true; } // transient delete-expired -> transient delete --expired if ( count( $args ) > 1 && 'transient' === $args[0] && 'delete-expired' === $args[1] ) { $args[1] = 'delete'; $assoc_args['expired'] = true; } // transient delete-all -> transient delete --all if ( count( $args ) > 1 && 'transient' === $args[0] && 'delete-all' === $args[1] ) { $args[1] = 'delete'; $assoc_args['all'] = true; } // plugin scaffold -> scaffold plugin if ( [ 'plugin', 'scaffold' ] === array_slice( $args, 0, 2 ) ) { list( $args[0], $args[1] ) = [ $args[1], $args[0] ]; } // foo --help -> help foo if ( isset( $assoc_args['help'] ) ) { array_unshift( $args, 'help' ); unset( $assoc_args['help'] ); } // {post|user} list --ids -> {post|user} list --format=ids if ( count( $args ) > 1 && in_array( $args[0], [ 'post', 'user' ], true ) && 'list' === $args[1] && isset( $assoc_args['ids'] ) ) { $assoc_args['format'] = 'ids'; unset( $assoc_args['ids'] ); } // --json -> --format=json if ( isset( $assoc_args['json'] ) ) { $assoc_args['format'] = 'json'; unset( $assoc_args['json'] ); } // --{version|info} -> cli {version|info} if ( empty( $args ) ) { $special_flags = [ 'version', 'info' ]; foreach ( $special_flags as $key ) { if ( isset( $assoc_args[ $key ] ) ) { $args = [ 'cli', $key ]; unset( $assoc_args[ $key ] ); break; } } } // (post|comment|site|term) url --> (post|comment|site|term) list --*__in --field=url if ( count( $args ) >= 2 && in_array( $args[0], [ 'post', 'comment', 'site', 'term' ], true ) && 'url' === $args[1] ) { switch ( $args[0] ) { case 'post': $post_ids = array_slice( $args, 2 ); $args = [ 'post', 'list' ]; $assoc_args['post__in'] = implode( ',', $post_ids ); $assoc_args['post_type'] = 'any'; $assoc_args['orderby'] = 'post__in'; $assoc_args['field'] = 'url'; break; case 'comment': $comment_ids = array_slice( $args, 2 ); $args = [ 'comment', 'list' ]; $assoc_args['comment__in'] = implode( ',', $comment_ids ); $assoc_args['orderby'] = 'comment__in'; $assoc_args['field'] = 'url'; break; case 'site': $site_ids = array_slice( $args, 2 ); $args = [ 'site', 'list' ]; $assoc_args['site__in'] = implode( ',', $site_ids ); $assoc_args['field'] = 'url'; break; case 'term': $taxonomy = ''; if ( isset( $args[2] ) ) { $taxonomy = $args[2]; } $term_ids = array_slice( $args, 3 ); $args = [ 'term', 'list', $taxonomy ]; $assoc_args['include'] = implode( ',', $term_ids ); $assoc_args['orderby'] = 'include'; $assoc_args['field'] = 'url'; break; } } // config get --[global|constant]= --> config get --type=constant|variable // config get --> config list if ( count( $args ) === 2 && 'config' === $args[0] && 'get' === $args[1] ) { if ( isset( $assoc_args['global'] ) ) { $name = $assoc_args['global']; $type = 'variable'; unset( $assoc_args['global'] ); } elseif ( isset( $assoc_args['constant'] ) ) { $name = $assoc_args['constant']; $type = 'constant'; unset( $assoc_args['constant'] ); } if ( ! empty( $name ) && ! empty( $type ) ) { $args[] = $name; $assoc_args['type'] = $type; } else { // We had a 'config get' without a '', so assume 'list' was wanted. $args[1] = 'list'; } } return [ $args, $assoc_args ]; } /** * Whether or not the output should be rendered in color * * @return bool */ public function in_color() { return $this->colorize; } public function init_colorization() { if ( 'auto' === $this->config['color'] ) { $this->colorize = ( ! Utils\isPiped() && ! Utils\is_windows() ); } else { $this->colorize = $this->config['color']; } } public function init_logger() { if ( $this->config['quiet'] ) { $logger = new Loggers\Quiet( $this->in_color() ); } else { $logger = new Loggers\Regular( $this->in_color() ); } WP_CLI::set_logger( $logger ); } public function get_required_files() { return $this->required_files; } /** * Do WordPress core files exist? * * @return bool */ private function wp_exists() { return file_exists( ABSPATH . 'wp-includes/version.php' ); } /** * Are WordPress core files readable? * * @return bool */ private function wp_is_readable() { return is_readable( ABSPATH . 'wp-includes/version.php' ); } private function check_wp_version() { $wp_exists = $this->wp_exists(); $wp_is_readable = $this->wp_is_readable(); if ( ! $wp_exists || ! $wp_is_readable ) { $this->show_synopsis_if_composite_command(); // If the command doesn't exist use as error. $args = $this->cmd_starts_with( [ 'help' ] ) ? array_slice( $this->arguments, 1 ) : $this->arguments; $suggestion_or_disabled = $this->find_command_to_run( $args ); if ( is_string( $suggestion_or_disabled ) ) { if ( ! preg_match( '/disabled from the config file.$/', $suggestion_or_disabled ) ) { WP_CLI::warning( "No WordPress installation found. If the command '" . implode( ' ', $args ) . "' is in a plugin or theme, pass --path=`path/to/wordpress`." ); } WP_CLI::error( $suggestion_or_disabled ); } if ( $wp_exists && ! $wp_is_readable ) { WP_CLI::error( 'It seems, the WordPress core files do not have the proper file permissions.' ); } WP_CLI::error( "This does not seem to be a WordPress installation.\n" . 'Pass --path=`path/to/wordpress` or run `wp core download`.' ); } global $wp_version; include ABSPATH . 'wp-includes/version.php'; $minimum_version = '3.7'; if ( version_compare( $wp_version, $minimum_version, '<' ) ) { WP_CLI::error( "WP-CLI needs WordPress $minimum_version or later to work properly. " . "The version currently installed is $wp_version.\n" . 'Try running `wp core download --force`.' ); } } public function init_config() { $configurator = WP_CLI::get_configurator(); $argv = array_slice( $GLOBALS['argv'], 1 ); $this->alias = null; if ( ! empty( $argv[0] ) && preg_match( '#' . Configurator::ALIAS_REGEX . '#', $argv[0], $matches ) ) { $this->alias = array_shift( $argv ); } // File config { $this->global_config_path = $this->get_global_config_path(); $this->project_config_path = $this->get_project_config_path(); $configurator->merge_yml( $this->global_config_path, $this->alias ); $config = $configurator->to_array(); $this->required_files['global'] = $config[0]['require']; $configurator->merge_yml( $this->project_config_path, $this->alias ); $config = $configurator->to_array(); $this->required_files['project'] = $config[0]['require']; } // Runtime config and args { list( $args, $assoc_args, $this->runtime_config ) = $configurator->parse_args( $argv ); list( $this->arguments, $this->assoc_args ) = self::back_compat_conversions( $args, $assoc_args ); $configurator->merge_array( $this->runtime_config ); } list( $this->config, $this->extra_config ) = $configurator->to_array(); $this->aliases = $configurator->get_aliases(); if ( count( $this->aliases ) && ! isset( $this->aliases['@all'] ) ) { $this->aliases = array_reverse( $this->aliases ); $this->aliases['@all'] = 'Run command against every registered alias.'; $this->aliases = array_reverse( $this->aliases ); } $this->required_files['runtime'] = $this->config['require']; } private function check_root() { if ( $this->config['allow-root'] || getenv( 'WP_CLI_ALLOW_ROOT' ) ) { return; # they're aware of the risks! } if ( count( $this->arguments ) >= 2 && 'cli' === $this->arguments[0] && in_array( $this->arguments[1], [ 'update', 'info' ], true ) ) { return; # make it easier to update root-owned copies } if ( ! function_exists( 'posix_geteuid' ) ) { return; # posix functions not available } if ( posix_geteuid() !== 0 ) { return; # not root } WP_CLI::error( "YIKES! It looks like you're running this as root. You probably meant to " . "run this as the user that your WordPress installation exists under.\n" . "\n" . "If you REALLY mean to run this as root, we won't stop you, but just " . 'bear in mind that any code on this site will then have full control of ' . "your server, making it quite DANGEROUS.\n" . "\n" . "If you'd like to continue as root, please run this again, adding this " . "flag: --allow-root\n" . "\n" . "If you'd like to run it as the user that this site is under, you can " . "run the following to become the respective user:\n" . "\n" . " sudo -u USER -i -- wp \n" . "\n" ); } private function run_alias_group( $aliases ) { Utils\check_proc_available( 'group alias' ); $php_bin = escapeshellarg( Utils\get_php_binary() ); $script_path = $GLOBALS['argv'][0]; if ( getenv( 'WP_CLI_CONFIG_PATH' ) ) { $config_path = getenv( 'WP_CLI_CONFIG_PATH' ); } else { $config_path = Utils\get_home_dir() . '/.wp-cli/config.yml'; } $config_path = escapeshellarg( $config_path ); foreach ( $aliases as $alias ) { WP_CLI::log( $alias ); $args = implode( ' ', array_map( 'escapeshellarg', $this->arguments ) ); $assoc_args = Utils\assoc_args_to_str( $this->assoc_args ); $runtime_config = Utils\assoc_args_to_str( $this->runtime_config ); $full_command = "WP_CLI_CONFIG_PATH={$config_path} {$php_bin} {$script_path} {$alias} {$args}{$assoc_args}{$runtime_config}"; $pipes = []; $proc = Utils\proc_open_compat( $full_command, [ STDIN, STDOUT, STDERR ], $pipes ); proc_close( $proc ); } } private function set_alias( $alias ) { $orig_config = $this->config; $alias_config = $this->aliases[ $alias ]; $this->config = array_merge( $orig_config, $alias_config ); foreach ( $alias_config as $key => $_ ) { if ( isset( $orig_config[ $key ] ) && ! is_null( $orig_config[ $key ] ) ) { $this->assoc_args[ $key ] = $orig_config[ $key ]; } } } public function start() { // Enable PHP error reporting to stderr if testing. Will need to be re-enabled after WP loads. if ( getenv( 'BEHAT_RUN' ) ) { $this->enable_error_reporting(); } WP_CLI::debug( $this->global_config_path_debug, 'bootstrap' ); WP_CLI::debug( $this->project_config_path_debug, 'bootstrap' ); WP_CLI::debug( 'argv: ' . implode( ' ', $GLOBALS['argv'] ), 'bootstrap' ); $this->check_root(); if ( $this->alias ) { if ( '@all' === $this->alias && ! isset( $this->aliases['@all'] ) ) { WP_CLI::error( "Cannot use '@all' when no aliases are registered." ); } if ( '@all' === $this->alias && is_string( $this->aliases['@all'] ) ) { $aliases = array_keys( $this->aliases ); $k = array_search( '@all', $aliases, true ); unset( $aliases[ $k ] ); $this->run_alias_group( $aliases ); exit; } if ( ! array_key_exists( $this->alias, $this->aliases ) ) { $error_msg = "Alias '{$this->alias}' not found."; $suggestion = Utils\get_suggestion( $this->alias, array_keys( $this->aliases ), $threshold = 2 ); if ( $suggestion ) { $error_msg .= PHP_EOL . "Did you mean '{$suggestion}'?"; } WP_CLI::error( $error_msg ); } // Numerically indexed means a group of aliases if ( isset( $this->aliases[ $this->alias ][0] ) ) { $group_aliases = $this->aliases[ $this->alias ]; $all_aliases = array_keys( $this->aliases ); $diff = array_diff( $group_aliases, $all_aliases ); if ( ! empty( $diff ) ) { WP_CLI::error( "Group '{$this->alias}' contains one or more invalid aliases: " . implode( ', ', $diff ) ); } $this->run_alias_group( $group_aliases ); exit; } $this->set_alias( $this->alias ); } if ( empty( $this->arguments ) ) { $this->arguments[] = 'help'; } // Protect 'cli info' from most of the runtime, // except when the command will be run over SSH if ( 'cli' === $this->arguments[0] && ! empty( $this->arguments[1] ) && 'info' === $this->arguments[1] && ! $this->config['ssh'] ) { $this->run_command_and_exit(); } if ( isset( $this->config['http'] ) && ! class_exists( '\WP_REST_CLI\Runner' ) ) { WP_CLI::error( "RESTful WP-CLI needs to be installed. Try 'wp package install wp-cli/restful'." ); } if ( $this->config['ssh'] ) { $this->run_ssh_command( $this->config['ssh'] ); return; } // Handle --path parameter self::set_wp_root( $this->find_wp_root() ); // First try at showing man page - if help command and either haven't found 'version.php' or 'wp-config.php' (so won't be loading WP & adding commands) or help on subcommand. if ( $this->cmd_starts_with( [ 'help' ] ) && ( ! $this->wp_exists() || ! Utils\locate_wp_config() || count( $this->arguments ) > 2 ) ) { $this->auto_check_update(); $this->run_command( $this->arguments, $this->assoc_args ); // Help didn't exit so failed to find the command at this stage. } // Handle --url parameter $url = self::guess_url( $this->config ); if ( $url ) { WP_CLI::set_url( $url ); } $this->do_early_invoke( 'before_wp_load' ); $this->check_wp_version(); if ( $this->cmd_starts_with( [ 'config', 'create' ] ) ) { $this->run_command_and_exit(); } if ( ! Utils\locate_wp_config() ) { WP_CLI::error( "'wp-config.php' not found.\n" . 'Either create one manually or use `wp config create`.' ); } /* * Set the MySQLi error reporting off because WordPress handles its own. * This is due to the default value change from `MYSQLI_REPORT_OFF` * to `MYSQLI_REPORT_ERROR|MYSQLI_REPORT_STRICT` in PHP 8.1. */ if ( function_exists( 'mysqli_report' ) ) { mysqli_report( 0 ); // phpcs:ignore WordPress.DB.RestrictedFunctions.mysql_mysqli_report } // phpcs:disable WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedConstantFound -- Declaring WP native constants. if ( $this->cmd_starts_with( [ 'core', 'is-installed' ] ) || $this->cmd_starts_with( [ 'core', 'update-db' ] ) ) { define( 'WP_INSTALLING', true ); } if ( count( $this->arguments ) >= 2 && 'core' === $this->arguments[0] && in_array( $this->arguments[1], [ 'install', 'multisite-install' ], true ) ) { define( 'WP_INSTALLING', true ); // We really need a URL here if ( ! isset( $_SERVER['HTTP_HOST'] ) ) { $url = 'https://example.com'; WP_CLI::set_url( $url ); } if ( 'multisite-install' === $this->arguments[1] ) { // need to fake some globals to skip the checks in wp-includes/ms-settings.php $url_parts = Utils\parse_url( $url ); self::fake_current_site_blog( $url_parts ); if ( ! defined( 'COOKIEHASH' ) ) { define( 'COOKIEHASH', md5( $url_parts['host'] ) ); } } } if ( $this->cmd_starts_with( [ 'import' ] ) ) { define( 'WP_LOAD_IMPORTERS', true ); define( 'WP_IMPORTING', true ); } if ( $this->cmd_starts_with( [ 'cron', 'event', 'run' ] ) ) { define( 'DOING_CRON', true ); } // phpcs:enable WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedConstantFound $this->load_wordpress(); $this->run_command_and_exit(); } /** * Load WordPress, if it hasn't already been loaded */ public function load_wordpress() { static $wp_cli_is_loaded; // Globals not explicitly globalized in WordPress global $site_id, $wpdb, $public, $current_site, $current_blog, $path, $shortcode_tags; if ( ! empty( $wp_cli_is_loaded ) ) { return; } $wp_cli_is_loaded = true; // Handle --context flag. $this->context_manager->switch_context( $this->config ); WP_CLI::debug( 'Begin WordPress load', 'bootstrap' ); WP_CLI::do_hook( 'before_wp_load' ); $this->check_wp_version(); $wp_config_path = Utils\locate_wp_config(); if ( ! $wp_config_path ) { WP_CLI::error( "'wp-config.php' not found.\n" . 'Either create one manually or use `wp config create`.' ); } WP_CLI::debug( 'wp-config.php path: ' . $wp_config_path, 'bootstrap' ); WP_CLI::do_hook( 'before_wp_config_load' ); // Load wp-config.php code, in the global scope $wp_cli_original_defined_vars = get_defined_vars(); eval( $this->get_wp_config_code() ); // phpcs:ignore Squiz.PHP.Eval.Discouraged foreach ( get_defined_vars() as $key => $var ) { if ( array_key_exists( $key, $wp_cli_original_defined_vars ) || 'wp_cli_original_defined_vars' === $key ) { continue; } // phpcs:ignore PHPCompatibility.Variables.ForbiddenGlobalVariableVariable.NonBareVariableFound global ${$key}; // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedVariableFound ${$key} = $var; } $this->maybe_update_url_from_domain_constant(); WP_CLI::do_hook( 'after_wp_config_load' ); $this->do_early_invoke( 'after_wp_config_load' ); // Prevent error notice from wp_guess_url() when core isn't installed if ( $this->cmd_starts_with( [ 'core', 'is-installed' ] ) && ! defined( 'COOKIEHASH' ) ) { define( 'COOKIEHASH', md5( 'wp-cli' ) ); } // Load WP-CLI utilities require WP_CLI_ROOT . '/php/utils-wp.php'; // Set up WordPress bootstrap actions and filters $this->setup_bootstrap_hooks(); // Load Core, mu-plugins, plugins, themes etc. if ( Utils\wp_version_compare( '4.6-alpha-37575', '>=' ) ) { if ( $this->cmd_starts_with( [ 'help' ] ) ) { // Hack: define `WP_DEBUG` and `WP_DEBUG_DISPLAY` to get `wpdb::bail()` to `wp_die()`. if ( ! defined( 'WP_DEBUG' ) ) { define( 'WP_DEBUG', true ); } if ( ! defined( 'WP_DEBUG_DISPLAY' ) ) { define( 'WP_DEBUG_DISPLAY', true ); } } require ABSPATH . 'wp-settings.php'; } else { require WP_CLI_ROOT . '/php/wp-settings-cli.php'; } // Fix memory limit. See https://core.trac.wordpress.org/ticket/14889 // phpcs:ignore WordPress.PHP.IniSet.memory_limit_Blacklisted -- This is perfectly fine for CLI usage. ini_set( 'memory_limit', -1 ); // Load all the admin APIs, for convenience require ABSPATH . 'wp-admin/includes/admin.php'; add_filter( 'filesystem_method', static function () { return 'direct'; }, 99 ); // Re-enable PHP error reporting to stderr if testing. if ( getenv( 'BEHAT_RUN' ) ) { $this->enable_error_reporting(); } WP_CLI::debug( 'Loaded WordPress', 'bootstrap' ); WP_CLI::do_hook( 'after_wp_load' ); } private static function fake_current_site_blog( $url_parts ) { global $current_site, $current_blog; if ( ! isset( $url_parts['path'] ) ) { $url_parts['path'] = '/'; } // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited -- Intentional override. $current_site = (object) [ 'id' => 1, 'blog_id' => 1, 'domain' => $url_parts['host'], 'path' => $url_parts['path'], 'cookie_domain' => $url_parts['host'], 'site_name' => 'WordPress', ]; // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited -- Intentional override. $current_blog = (object) [ 'blog_id' => 1, 'site_id' => 1, 'domain' => $url_parts['host'], 'path' => $url_parts['path'], 'public' => '1', 'archived' => '0', 'mature' => '0', 'spam' => '0', 'deleted' => '0', 'lang_id' => '0', ]; } /** * Called after wp-config.php is eval'd, to potentially reset `--url` */ private function maybe_update_url_from_domain_constant() { if ( ! empty( $this->config['url'] ) || ! empty( $this->config['blog'] ) ) { return; } if ( defined( 'DOMAIN_CURRENT_SITE' ) ) { $url = DOMAIN_CURRENT_SITE; if ( defined( 'PATH_CURRENT_SITE' ) ) { $url .= PATH_CURRENT_SITE; } WP_CLI::set_url( $url ); } } /** * Set up hooks meant to run during the WordPress bootstrap process */ private function setup_bootstrap_hooks() { if ( $this->config['skip-plugins'] ) { $this->setup_skip_plugins_filters(); } if ( $this->config['skip-themes'] ) { WP_CLI::add_wp_hook( 'setup_theme', [ $this, 'action_setup_theme_wp_cli_skip_themes' ], 999 ); } if ( $this->cmd_starts_with( [ 'help' ] ) ) { // Try to trap errors on help. $help_handler = [ $this, 'help_wp_die_handler' ]; // Avoid any cross PHP version issues by not using $this in anon function. WP_CLI::add_wp_hook( 'wp_die_handler', function () use ( $help_handler ) { return $help_handler; } ); } else { WP_CLI::add_wp_hook( 'wp_die_handler', static function () { return '\WP_CLI\Utils\wp_die_handler'; } ); } // Prevent code from performing a redirect WP_CLI::add_wp_hook( 'wp_redirect', 'WP_CLI\\Utils\\wp_redirect_handler' ); WP_CLI::add_wp_hook( 'nocache_headers', static function ( $headers ) { // WordPress might be calling nocache_headers() because of a dead db global $wpdb; if ( ! empty( $wpdb->error ) ) { Utils\wp_die_handler( $wpdb->error ); } // Otherwise, WP might be calling nocache_headers() because WP isn't installed Utils\wp_not_installed(); return $headers; } ); WP_CLI::add_wp_hook( 'setup_theme', static function () { // Polyfill is_customize_preview(), as it is needed by TwentyTwenty to // check for starter content. if ( ! function_exists( 'is_customize_preview' ) ) { function is_customize_preview() { return false; } } }, 0 ); // ALTERNATE_WP_CRON might trigger a redirect, which we can't handle if ( defined( 'ALTERNATE_WP_CRON' ) && ALTERNATE_WP_CRON ) { WP_CLI::add_wp_hook( 'muplugins_loaded', static function () { remove_action( 'init', 'wp_cron' ); } ); } // Get rid of warnings when converting single site to multisite if ( defined( 'WP_INSTALLING' ) && $this->is_multisite() ) { $values = [ 'ms_files_rewriting' => null, 'active_sitewide_plugins' => [], '_site_transient_update_core' => null, '_site_transient_update_themes' => null, '_site_transient_update_plugins' => null, 'WPLANG' => '', ]; foreach ( $values as $key => $value ) { WP_CLI::add_wp_hook( "pre_site_option_$key", static function () use ( $values, $key ) { return $values[ $key ]; } ); } } // Always permit operations against sites, regardless of status WP_CLI::add_wp_hook( 'ms_site_check', '__return_true' ); // Always permit operations against WordPress, regardless of maintenance mode WP_CLI::add_wp_hook( 'enable_maintenance_mode', static function () { return false; } ); // Use our own debug mode handling instead of WP core WP_CLI::add_wp_hook( 'enable_wp_debug_mode_checks', static function ( $ret ) { Utils\wp_debug_mode(); return false; } ); // Never load advanced-cache.php drop-in when WP-CLI is operating WP_CLI::add_wp_hook( 'enable_loading_advanced_cache_dropin', static function () { return false; } ); // In a multisite installation, die if unable to find site given in --url parameter if ( $this->is_multisite() ) { $run_on_site_not_found = false; if ( $this->cmd_starts_with( [ 'cache', 'flush' ] ) ) { $run_on_site_not_found = 'cache flush'; } if ( $this->cmd_starts_with( [ 'search-replace' ] ) ) { // Table-specified // Bits: search-replace [...] // Or not against a specific blog if ( count( $this->arguments ) > 3 || ! empty( $this->assoc_args['network'] ) || ! empty( $this->assoc_args['all-tables'] ) || ! empty( $this->assoc_args['all-tables-with-prefix'] ) ) { $run_on_site_not_found = 'search-replace'; } } if ( $run_on_site_not_found && Utils\wp_version_compare( '4.0', '>=' ) ) { WP_CLI::add_wp_hook( 'ms_site_not_found', static function () use ( $run_on_site_not_found ) { // esc_sql() isn't yet loaded, but needed. if ( 'search-replace' === $run_on_site_not_found ) { require_once ABSPATH . WPINC . '/formatting.php'; } // PHP 5.3 compatible implementation of run_command_and_exit(). $runner = WP_CLI::get_runner(); $runner->run_command( $runner->arguments, $runner->assoc_args ); exit; }, 1 ); } WP_CLI::add_wp_hook( 'ms_site_not_found', static function ( $current_site, $domain, $path ) { $url = $domain . $path; $message = $url ? "Site '{$url}' not found." : 'Site not found.'; $has_param = isset( WP_CLI::get_runner()->config['url'] ); $has_const = defined( 'DOMAIN_CURRENT_SITE' ); $explanation = ''; if ( $has_param ) { $explanation = 'Verify `--url=` matches an existing site.'; } else { $explanation = "Define DOMAIN_CURRENT_SITE in 'wp-config.php' or use `--url=` to override."; if ( $has_const ) { $explanation = 'Verify DOMAIN_CURRENT_SITE matches an existing site or use `--url=` to override.'; } } if ( $explanation ) { $message .= ' ' . $explanation; } WP_CLI::error( $message ); }, 10, 3 ); } // The APC cache is not available on the command-line, so bail, to prevent cache poisoning WP_CLI::add_wp_hook( 'muplugins_loaded', static function () { if ( $GLOBALS['_wp_using_ext_object_cache'] && class_exists( 'APC_Object_Cache' ) ) { WP_CLI::warning( 'Running WP-CLI while the APC object cache is activated can result in cache corruption.' ); WP_CLI::confirm( 'Given the consequences, do you wish to continue?' ); } }, 0 ); // Handle --user parameter if ( ! defined( 'WP_INSTALLING' ) ) { $config = $this->config; WP_CLI::add_wp_hook( 'init', static function () use ( $config ) { if ( isset( $config['user'] ) ) { $fetcher = new Fetchers\User(); $user = $fetcher->get_check( $config['user'] ); wp_set_current_user( $user->ID ); } else { add_action( 'init', 'kses_remove_filters', 11 ); } }, 0 ); } // Avoid uncaught exception when using wp_mail() without defined $_SERVER['SERVER_NAME'] WP_CLI::add_wp_hook( 'wp_mail_from', static function ( $from_email ) { if ( 'wordpress@' === $from_email ) { $sitename = strtolower( Utils\parse_url( site_url(), PHP_URL_HOST ) ); if ( substr( $sitename, 0, 4 ) === 'www.' ) { $sitename = substr( $sitename, 4 ); } $from_email = 'wordpress@' . $sitename; } return $from_email; } ); // Don't apply set_url_scheme in get_home_url() or get_site_url(). WP_CLI::add_wp_hook( 'home_url', static function ( $url, $path, $scheme, $blog_id ) { if ( empty( $blog_id ) || ! is_multisite() ) { $url = get_option( 'home' ); } else { switch_to_blog( $blog_id ); $url = get_option( 'home' ); restore_current_blog(); } if ( $path && is_string( $path ) ) { $url .= '/' . ltrim( $path, '/' ); } return $url; }, 0, 4 ); WP_CLI::add_wp_hook( 'site_url', static function ( $url, $path, $scheme, $blog_id ) { if ( empty( $blog_id ) || ! is_multisite() ) { $url = get_option( 'siteurl' ); } else { switch_to_blog( $blog_id ); $url = get_option( 'siteurl' ); restore_current_blog(); } if ( $path && is_string( $path ) ) { $url .= '/' . ltrim( $path, '/' ); } return $url; }, 0, 4 ); // Set up hook for plugins and themes to conditionally add WP-CLI commands. WP_CLI::add_wp_hook( 'init', static function () { do_action( 'cli_init' ); } ); } /** * Set up the filters to skip the loaded plugins */ private function setup_skip_plugins_filters() { $wp_cli_filter_active_plugins = static function ( $plugins ) { $skipped_plugins = WP_CLI::get_runner()->config['skip-plugins']; if ( true === $skipped_plugins ) { return []; } if ( ! is_array( $plugins ) ) { return $plugins; } foreach ( $plugins as $a => $b ) { // active_sitewide_plugins stores plugin name as the key. if ( false !== strpos( current_filter(), 'active_sitewide_plugins' ) && Utils\is_plugin_skipped( $a ) ) { unset( $plugins[ $a ] ); // active_plugins stores plugin name as the value. } elseif ( false !== strpos( current_filter(), 'active_plugins' ) && Utils\is_plugin_skipped( $b ) ) { unset( $plugins[ $a ] ); } } // Reindex because active_plugins expects a numeric index. if ( false !== strpos( current_filter(), 'active_plugins' ) ) { $plugins = array_values( $plugins ); } return $plugins; }; $hooks = [ 'pre_site_option_active_sitewide_plugins', 'site_option_active_sitewide_plugins', 'pre_option_active_plugins', 'option_active_plugins', ]; foreach ( $hooks as $hook ) { WP_CLI::add_wp_hook( $hook, $wp_cli_filter_active_plugins, 999 ); } WP_CLI::add_wp_hook( 'plugins_loaded', static function () use ( $hooks, $wp_cli_filter_active_plugins ) { foreach ( $hooks as $hook ) { remove_filter( $hook, $wp_cli_filter_active_plugins, 999 ); } }, 0 ); } /** * Set up the filters to skip the loaded theme */ public function action_setup_theme_wp_cli_skip_themes() { $wp_cli_filter_active_theme = static function ( $value ) { $skipped_themes = WP_CLI::get_runner()->config['skip-themes']; if ( true === $skipped_themes ) { return ''; } if ( ! is_array( $skipped_themes ) ) { $skipped_themes = explode( ',', $skipped_themes ); } $checked_value = $value; // Always check against the stylesheet value // This ensures a child theme can be skipped when template differs if ( false !== stripos( current_filter(), 'option_template' ) ) { $checked_value = get_option( 'stylesheet' ); } if ( '' === $checked_value || in_array( $checked_value, $skipped_themes, true ) ) { return ''; } return $value; }; $hooks = [ 'pre_option_template', 'option_template', 'pre_option_stylesheet', 'option_stylesheet', ]; foreach ( $hooks as $hook ) { add_filter( $hook, $wp_cli_filter_active_theme, 999 ); } // Clean up after the TEMPLATEPATH and STYLESHEETPATH constants are defined WP_CLI::add_wp_hook( 'after_setup_theme', static function () use ( $hooks, $wp_cli_filter_active_theme ) { foreach ( $hooks as $hook ) { remove_filter( $hook, $wp_cli_filter_active_theme, 999 ); } }, 0 ); } /** * Whether or not this WordPress installation is multisite. * * For use after wp-config.php has loaded, but before the rest of WordPress * is loaded. */ private function is_multisite() { if ( defined( 'MULTISITE' ) ) { return MULTISITE; } if ( defined( 'SUBDOMAIN_INSTALL' ) || defined( 'VHOST' ) || defined( 'SUNRISE' ) ) { return true; } return false; } /** * Error handler for `wp_die()` when the command is help to try to trap errors (db connection failure in particular) during WordPress load. */ public function help_wp_die_handler( $message ) { $help_exit_warning = 'Error during WordPress load.'; if ( $message instanceof WP_Error ) { $help_exit_warning = Utils\wp_clean_error_message( $message->get_error_message() ); } elseif ( is_string( $message ) ) { $help_exit_warning = Utils\wp_clean_error_message( $message ); } $this->run_command_and_exit( $help_exit_warning ); } /** * Check whether there's a WP-CLI update available, and suggest update if so. */ private function auto_check_update() { // `wp cli update` only works with Phars at this time. if ( ! Utils\inside_phar() ) { return; } $existing_phar = realpath( $_SERVER['argv'][0] ); // Phar needs to be writable to be easily updateable. if ( ! is_writable( $existing_phar ) || ! is_writable( dirname( $existing_phar ) ) ) { return; } // Only check for update when a human is operating. if ( ! function_exists( 'posix_isatty' ) || ! posix_isatty( STDOUT ) ) { return; } // Allow hosts and other providers to disable automatic check update. if ( getenv( 'WP_CLI_DISABLE_AUTO_CHECK_UPDATE' ) ) { return; } // Permit configuration of number of days between checks. $days_between_checks = getenv( 'WP_CLI_AUTO_CHECK_UPDATE_DAYS' ); if ( false === $days_between_checks ) { $days_between_checks = 1; } $cache = WP_CLI::get_cache(); $cache_key = 'wp-cli-update-check'; // Bail early on the first check, so we don't always check on an unwritable cache. if ( ! $cache->has( $cache_key ) ) { $cache->write( $cache_key, time() ); return; } // Bail if last check is still within our update check time period. $last_check = (int) $cache->read( $cache_key ); if ( ( time() - ( 24 * 60 * 60 * $days_between_checks ) ) < $last_check ) { return; } // In case the operation fails, ensure the timestamp has been updated. $cache->write( $cache_key, time() ); // Check whether any updates are available. ob_start(); WP_CLI::run_command( [ 'cli', 'check-update' ], [ 'format' => 'count', ] ); $count = ob_get_clean(); if ( ! $count ) { return; } // Looks like an update is available, so let's prompt to update. WP_CLI::run_command( [ 'cli', 'update' ] ); // If the Phar was replaced, we can't proceed with the original process. exit; } /** * Get a suggestion on similar (sub)commands when the user entered an * unknown (sub)command. * * @param string $entry User entry that didn't match an * existing command. * @param CompositeCommand $root_command Root command to start search for * suggestions at. * * @return string Suggestion that fits the user entry, or an empty string. */ private function get_subcommand_suggestion( $entry, CompositeCommand $root_command = null ) { $commands = []; $this->enumerate_commands( $root_command ?: WP_CLI::get_root_command(), $commands ); return Utils\get_suggestion( $entry, $commands, $threshold = 2 ); } /** * Recursive method to enumerate all known commands. * * @param CompositeCommand $command Composite command to recurse over. * @param array $list Reference to list accumulating results. * @param string $parent Parent command to use as prefix. */ private function enumerate_commands( CompositeCommand $command, array &$list, $parent = '' ) { foreach ( $command->get_subcommands() as $subcommand ) { /** @var CompositeCommand $subcommand */ $command_string = empty( $parent ) ? $subcommand->get_name() : "{$parent} {$subcommand->get_name()}"; $list[] = $command_string; $this->enumerate_commands( $subcommand, $list, $command_string ); } } /** * Enables (almost) full PHP error reporting to stderr. */ private function enable_error_reporting() { if ( E_ALL !== error_reporting() ) { // Don't enable E_DEPRECATED as old versions of WP use PHP 4 style constructors and the mysql extension. error_reporting( E_ALL & ~E_DEPRECATED ); } ini_set( 'display_errors', 'stderr' ); // phpcs:ignore WordPress.PHP.IniSet.display_errors_Blacklisted } } spec = SynopsisParser::parse( $synopsis ); } /** * Get any unknown arguments. * * @return array */ public function get_unknown() { return array_column( $this->query_spec( [ 'type' => 'unknown', ] ), 'token' ); } /** * Check whether there are enough positional arguments. * * @param array $args Positional arguments. * @return bool */ public function enough_positionals( $args ) { $positional = $this->query_spec( [ 'type' => 'positional', 'optional' => false, ] ); return count( $args ) >= count( $positional ); } /** * Check for any unknown positionals. * * @param array $args Positional arguments. * @return array */ public function unknown_positionals( $args ) { $positional_repeating = $this->query_spec( [ 'type' => 'positional', 'repeating' => true, ] ); // At least one positional supports as many as possible. if ( ! empty( $positional_repeating ) ) { return []; } $positional = $this->query_spec( [ 'type' => 'positional', 'repeating' => false, ] ); return array_slice( $args, count( $positional ) ); } /** * Check that all required keys are present and that they have values. * * @param array $assoc_args Parameters passed to command. * @return array */ public function validate_assoc( $assoc_args ) { $assoc_spec = $this->query_spec( [ 'type' => 'assoc', ] ); $errors = [ 'fatal' => [], 'warning' => [], ]; $to_unset = []; foreach ( $assoc_spec as $param ) { $key = $param['name']; if ( ! isset( $assoc_args[ $key ] ) ) { if ( ! $param['optional'] ) { $errors['fatal'][ $key ] = "missing --$key parameter"; } } else { if ( true === $assoc_args[ $key ] && ! $param['value']['optional'] ) { $error_type = ( ! $param['optional'] ) ? 'fatal' : 'warning'; $errors[ $error_type ][ $key ] = "--$key parameter needs a value"; $to_unset[] = $key; } } } return [ $errors, $to_unset ]; } /** * Check whether there are unknown parameters supplied. * * @param array $assoc_args Parameters passed to command. * @return array|false */ public function unknown_assoc( $assoc_args ) { $generic = $this->query_spec( [ 'type' => 'generic', ] ); if ( count( $generic ) ) { return []; } $known_assoc = []; foreach ( $this->spec as $param ) { if ( in_array( $param['type'], [ 'assoc', 'flag' ], true ) ) { $known_assoc[] = $param['name']; } } return array_diff( array_keys( $assoc_args ), $known_assoc ); } /** * Filters a list of associative arrays, based on a set of key => value arguments. * * @param array $args An array of key => value arguments to match against * @param string $operator * @return array */ private function query_spec( $args, $operator = 'AND' ) { $operator = strtoupper( $operator ); $count = count( $args ); $filtered = []; foreach ( $this->spec as $key => $to_match ) { $matched = 0; foreach ( $args as $m_key => $m_value ) { if ( array_key_exists( $m_key, $to_match ) && $m_value === $to_match[ $m_key ] ) { $matched++; } } if ( ( 'AND' === $operator && $matched === $count ) || ( 'OR' === $operator && $matched > 0 ) || ( 'NOT' === $operator && 0 === $matched ) ) { $filtered[ $key ] = $to_match; } } return $filtered; } } words = explode( ' ', $line ); // First word is always `wp`. array_shift( $this->words ); // Last word is either empty or an incomplete subcommand. $this->cur_word = end( $this->words ); if ( '' !== $this->cur_word && ! preg_match( '/^\-/', $this->cur_word ) ) { array_pop( $this->words ); } $is_alias = false; $is_help = false; if ( ! empty( $this->words[0] ) && preg_match( '/^@/', $this->words[0] ) ) { array_shift( $this->words ); // `wp @al` is false, but `wp @all ` is true. if ( count( $this->words ) ) { $is_alias = true; } } elseif ( ! empty( $this->words[0] ) && 'help' === $this->words[0] ) { array_shift( $this->words ); $is_help = true; } $r = $this->get_command( $this->words ); if ( ! is_array( $r ) ) { return; } list( $command, $args, $assoc_args ) = $r; $spec = SynopsisParser::parse( $command->get_synopsis() ); foreach ( $spec as $arg ) { if ( 'positional' === $arg['type'] && 'file' === $arg['name'] ) { $this->add( ' ' ); return; } } if ( $command->can_have_subcommands() ) { // Add completion when command is `wp` and alias isn't set. if ( 'wp' === $command->get_name() && false === $is_alias && false === $is_help ) { $aliases = WP_CLI::get_configurator()->get_aliases(); foreach ( $aliases as $name => $_ ) { $this->add( "$name " ); } } foreach ( $command->get_subcommands() as $name => $_ ) { $this->add( "$name " ); } } else { foreach ( $spec as $arg ) { if ( in_array( $arg['type'], [ 'flag', 'assoc' ], true ) ) { if ( isset( $assoc_args[ $arg['name'] ] ) ) { continue; } $opt = "--{$arg['name']}"; if ( 'flag' === $arg['type'] ) { $opt .= ' '; } elseif ( ! $arg['value']['optional'] ) { $opt .= '='; } $this->add( $opt ); } } foreach ( $this->get_global_parameters() as $param => $runtime ) { if ( isset( $assoc_args[ $param ] ) ) { continue; } $opt = "--{$param}"; if ( '' === $runtime || ! is_string( $runtime ) ) { $opt .= ' '; } else { $opt .= '='; } $this->add( $opt ); } } } /** * Get the specific WP-CLI command that is being referenced. * * @param array $words Individual input line words. * * @return array|mixed Array with command and arguments, or error result if command detection failed. */ private function get_command( $words ) { $positional_args = []; $assoc_args = []; foreach ( $words as $arg ) { if ( preg_match( '|^--([^=]+)=?|', $arg, $matches ) ) { $assoc_args[ $matches[1] ] = true; } else { $positional_args[] = $arg; } } $r = WP_CLI::get_runner()->find_command_to_run( $positional_args ); if ( ! is_array( $r ) && array_pop( $positional_args ) === $this->cur_word ) { $r = WP_CLI::get_runner()->find_command_to_run( $positional_args ); } if ( ! is_array( $r ) ) { return $r; } list( $command, $args ) = $r; return [ $command, $args, $assoc_args ]; } /** * Get global parameters. * * @return array Associative array of global parameters. */ private function get_global_parameters() { $params = []; foreach ( WP_CLI::get_configurator()->get_spec() as $key => $details ) { if ( false === $details['runtime'] ) { continue; } if ( isset( $details['deprecated'] ) ) { continue; } if ( isset( $details['hidden'] ) ) { continue; } $params[ $key ] = $details['runtime']; // Add additional option like `--[no-]color`. if ( true === $details['runtime'] ) { $params[ 'no-' . $key ] = ''; } } return $params; } /** * Store individual option. * * @param string $opt Option to store. * * @return void */ private function add( $opt ) { if ( '' !== $this->cur_word ) { if ( 0 !== strpos( $opt, $this->cur_word ) ) { return; } } $this->opts[] = $opt; } /** * Render the stored options. * * @return void */ public function render() { foreach ( $this->opts as $opt ) { WP_CLI::line( $opt ); } } } unregister(); } /** * Registers the autoload callback with the SPL autoload system. */ public function register() { spl_autoload_register( [ $this, 'autoload' ] ); } /** * Unregisters the autoload callback with the SPL autoload system. */ public function unregister() { spl_autoload_unregister( [ $this, 'autoload' ] ); } /** * Add a specific namespace structure with our custom autoloader. * * @param string $root Root namespace name. * @param string $base_dir Directory containing the class files. * @param string $prefix Prefix to be added before the class. * @param string $suffix Suffix to be added after the class. * @param boolean $lowercase Whether the class should be changed to * lowercase. * @param boolean $underscores Whether the underscores should be changed to * hyphens. * * @return self */ public function add_namespace( $root, $base_dir, $prefix = '', $suffix = '.php', $lowercase = false, $underscores = false ) { $this->namespaces[] = [ 'root' => $this->normalize_root( (string) $root ), 'base_dir' => $this->add_trailing_slash( (string) $base_dir ), 'prefix' => (string) $prefix, 'suffix' => (string) $suffix, 'lowercase' => (bool) $lowercase, 'underscores' => (bool) $underscores, ]; return $this; } /** * The autoload function that gets registered with the SPL Autoloader * system. * * @param string $class The class that got requested by the spl_autoloader. */ public function autoload( $class ) { // Iterate over namespaces to find a match. foreach ( $this->namespaces as $namespace ) { // Move on if the object does not belong to the current namespace. if ( 0 !== strpos( $class, $namespace['root'] ) ) { continue; } // Remove namespace root level to correspond with root filesystem, and // replace the namespace separator "\" by the system-dependent directory separator. $filename = str_replace( [ $namespace['root'], '\\' ], [ '', DIRECTORY_SEPARATOR ], $class ); // Remove a leading backslash from the class name. $filename = $this->remove_leading_backslash( $filename ); // Change to lower case if requested. if ( $namespace['lowercase'] ) { $filename = strtolower( $filename ); } // Change underscores into hyphens if requested. if ( $namespace['underscores'] ) { $filename = str_replace( '_', '-', $filename ); } // Add base_dir, prefix and suffix. $filepath = $namespace['base_dir'] . $namespace['prefix'] . $filename . $namespace['suffix']; // Throw an exception if the file does not exist or is not readable. if ( is_readable( $filepath ) ) { require_once $filepath; } } } /** * Normalize a namespace root. * * @param string $root Namespace root that needs to be normalized. * * @return string Normalized namespace root. */ protected function normalize_root( $root ) { $root = $this->remove_leading_backslash( $root ); return $this->add_trailing_backslash( $root ); } /** * Remove a leading backslash from a string. * * @param string $string String to remove the leading backslash from. * * @return string Modified string. */ protected function remove_leading_backslash( $string ) { return ltrim( $string, '\\' ); } /** * Make sure a string ends with a trailing backslash. * * @param string $string String to check the trailing backslash of. * * @return string Modified string. */ protected function add_trailing_backslash( $string ) { return rtrim( $string, '\\' ) . '\\'; } /** * Make sure a string ends with a trailing slash. * * @param string $string String to check the trailing slash of. * * @return string Modified string. */ protected function add_trailing_slash( $string ) { return rtrim( $string, '/\\' ) . '/'; } } doc_comment = self::remove_decorations( $doc_comment ); } /** * Remove unused cruft from PHPdoc comment. * * @param string $comment PHPdoc comment. * @return string */ private static function remove_decorations( $comment ) { $comment = preg_replace( '|^/\*\*[\r\n]+|', '', $comment ); $comment = preg_replace( '|\n[\t ]*\*/$|', '', $comment ); $comment = preg_replace( '|^[\t ]*\* ?|m', '', $comment ); return $comment; } /** * Get the command's short description (e.g. summary). * * @return string */ public function get_shortdesc() { if ( ! preg_match( '|^([^@][^\n]+)\n*|', $this->doc_comment, $matches ) ) { return ''; } return $matches[1]; } /** * Get the command's full description * * @return string */ public function get_longdesc() { $shortdesc = $this->get_shortdesc(); if ( ! $shortdesc ) { return ''; } $longdesc = substr( $this->doc_comment, strlen( $shortdesc ) ); $lines = []; foreach ( explode( "\n", $longdesc ) as $line ) { if ( 0 === strpos( $line, '@' ) ) { break; } $lines[] = $line; } return trim( implode( "\n", $lines ) ); } /** * Get the value for a given tag (e.g. "@alias" or "@subcommand") * * @param string $name Name for the tag, without '@' * @return string */ public function get_tag( $name ) { if ( preg_match( '|^@' . $name . '\s+([a-z-_0-9]+)|m', $this->doc_comment, $matches ) ) { return $matches[1]; } return ''; } /** * Get the command's synopsis. * * @return string */ public function get_synopsis() { if ( ! preg_match( '|^@synopsis\s+(.+)|m', $this->doc_comment, $matches ) ) { return ''; } return $matches[1]; } /** * Get the description for a given argument. * * @param string $name Argument's doc name. * @return string */ public function get_arg_desc( $name ) { if ( preg_match( "/\[?<{$name}>.+\n: (.+?)(\n|$)/", $this->doc_comment, $matches ) ) { return $matches[1]; } return ''; } /** * Get the arguments for a given argument. * * @param string $name Argument's doc name. * @return mixed|null */ public function get_arg_args( $name ) { return $this->get_arg_or_param_args( "/^\[?<{$name}>.*/" ); } /** * Get the description for a given parameter. * * @param string $key Parameter's key. * @return string */ public function get_param_desc( $key ) { if ( preg_match( "/\[?--{$key}=.+\n: (.+?)(\n|$)/", $this->doc_comment, $matches ) ) { return $matches[1]; } return ''; } /** * Get the arguments for a given parameter. * * @param string $key Parameter's key. * @return mixed|null */ public function get_param_args( $key ) { return $this->get_arg_or_param_args( "/^\[?--{$key}=.*/" ); } /** * Get the args for an arg or param * * @param string $regex Pattern to match against * @return array|null Interpreted YAML document, or null. */ private function get_arg_or_param_args( $regex ) { $bits = explode( "\n", $this->doc_comment ); $within_arg = false; $within_doc = false; $document = []; foreach ( $bits as $bit ) { if ( preg_match( $regex, $bit ) ) { $within_arg = true; } if ( $within_arg && $within_doc && '---' === $bit ) { $within_doc = false; } if ( $within_arg && ! $within_doc && '---' === $bit ) { $within_doc = true; } if ( $within_doc ) { $document[] = $bit; } if ( $within_arg && '' === $bit ) { $within_arg = false; break; } } if ( $document ) { return Spyc::YAMLLoadString( implode( "\n", $document ) ); } return null; } } new Context\Cli(), Context::ADMIN => new Context\Admin(), Context::FRONTEND => new Context\Frontend(), Context::AUTO => new Context\Auto( $context_manager ), ]; $contexts = WP_CLI::do_hook( 'before_registering_contexts', $contexts ); foreach ( $contexts as $name => $implementation ) { $context_manager->register_context( $name, $implementation ); } $state->setValue( 'context_manager', $context_manager ); return $state; } } register_context_manager( $state->getValue( 'context_manager' ) ); $runner()->start(); return $state; } } state ) ? $this->state[ $key ] : $fallback; } /** * Set the state value for a given key. * * @param string $key Key to set the state for. * @param mixed $value Value to set the state for the given key to. * * @return void */ public function setValue( $key, $value ) { $this->state[ $key ] = $value; } } init_colorization(); return $state; } } init_config(); return $state; } } get_protected_commands(); $current_command = $this->get_current_command(); foreach ( $commands as $command ) { if ( 0 === strpos( $current_command, $command ) ) { $state->setValue( BootstrapState::IS_PROTECTED_COMMAND, true ); } } return $state; } /** * Get the list of protected commands. * * @return array */ private function get_protected_commands() { return [ 'cli info', 'package', ]; } /** * Get the current command as a string. * * @return string Current command to be executed. */ private function get_current_command() { $runner = new RunnerInstance(); return implode( ' ', (array) $runner()->arguments ); } } ` option. * * @package WP_CLI\Bootstrap */ final class LoadExecCommand implements BootstrapStep { /** * Process this single bootstrapping step. * * @param BootstrapState $state Contextual state to pass into the step. * * @return BootstrapState Modified state to pass to the next step. */ public function process( BootstrapState $state ) { if ( $state->getValue( BootstrapState::IS_PROTECTED_COMMAND, false ) ) { return $state; } $runner = new RunnerInstance(); if ( ! isset( $runner()->config['exec'] ) ) { return $state; } foreach ( $runner()->config['exec'] as $php_code ) { eval( $php_code ); // phpcs:ignore Squiz.PHP.Eval.Discouraged } return $state; } } ` option. * * @package WP_CLI\Bootstrap */ final class LoadRequiredCommand implements BootstrapStep { /** * Process this single bootstrapping step. * * @param BootstrapState $state Contextual state to pass into the step. * * @return BootstrapState Modified state to pass to the next step. */ public function process( BootstrapState $state ) { if ( $state->getValue( BootstrapState::IS_PROTECTED_COMMAND, false ) ) { return $state; } $runner = new RunnerInstance(); if ( ! isset( $runner()->config['require'] ) ) { return $state; } foreach ( $runner()->config['require'] as $path ) { if ( ! file_exists( $path ) ) { $context = ''; $required_files = $runner()->get_required_files(); foreach ( [ 'global', 'project', 'runtime' ] as $scope ) { if ( in_array( $path, $required_files[ $scope ], true ) ) { switch ( $scope ) { case 'global': $context = ' (from global ' . Utils\basename( $runner()->get_global_config_path() ) . ')'; break; case 'project': $context = ' (from project\'s ' . Utils\basename( $runner()->get_project_config_path() ) . ')'; break; case 'runtime': $context = ' (from runtime argument)'; break; } break; } } WP_CLI::error( sprintf( "Required file '%s' doesn't exist%s.", Utils\basename( $path ), $context ) ); } Utils\load_file( $path ); WP_CLI::debug( 'Required file from config: ' . $path, 'bootstrap' ); } return $state; } } state = $state; $found_autoloader = false; $autoloader_paths = $this->get_autoloader_paths(); if ( false === $autoloader_paths ) { // Skip this autoloading step. return $state; } foreach ( $autoloader_paths as $autoloader_path ) { if ( is_readable( $autoloader_path ) ) { try { WP_CLI::debug( sprintf( 'Loading detected autoloader: %s', $autoloader_path ), 'bootstrap' ); require $autoloader_path; $found_autoloader = true; } catch ( Exception $exception ) { WP_CLI::warning( "Failed to load autoloader '{$autoloader_path}'. Reason: " . $exception->getMessage() ); } } } if ( ! $found_autoloader ) { $this->handle_failure(); } return $this->state; } /** * Get the name of the custom vendor folder as set in `composer.json`. * * @return string|false Name of the custom vendor folder or false if none. */ protected function get_custom_vendor_folder() { $maybe_composer_json = WP_CLI_ROOT . '/../../../composer.json'; if ( ! is_readable( $maybe_composer_json ) ) { return false; } $composer = json_decode( file_get_contents( $maybe_composer_json ) ); if ( ! empty( $composer->config ) && ! empty( $composer->config->{'vendor-dir'} ) ) { return $composer->config->{'vendor-dir'}; } return false; } /** * Handle the failure to find an autoloader. * * @return void */ protected function handle_failure() { } /** * Get the autoloader paths to scan for an autoloader. * * @return string[]|false Array of strings with autoloader paths, or false * to skip. */ abstract protected function get_autoloader_paths(); } getMessage() ); } } return $state; } } declare_loggers(); $runner = new RunnerInstance(); $runner()->init_logger(); return $state; } /** * Load the class declarations for the loggers. */ private function declare_loggers() { $logger_dir = WP_CLI_ROOT . '/php/WP_CLI/Loggers'; $iterator = new DirectoryIterator( $logger_dir ); // Make sure the base class is declared first. include_once "$logger_dir/Base.php"; foreach ( $iterator as $filename ) { if ( '.php' !== substr( $filename, - 4 ) ) { continue; } include_once "$logger_dir/$filename"; } } } WP_CLI_ROOT . '/php/WP_CLI', 'cli' => WP_CLI_VENDOR_DIR . '/wp-cli/php-cli-tools/lib/cli', 'Requests' => WP_CLI_VENDOR_DIR . '/rmccue/requests/library/Requests', 'Symfony\Component\Finder' => WP_CLI_VENDOR_DIR . '/symfony/finder/', ]; foreach ( $mappings as $namespace => $folder ) { $autoloader->add_namespace( $namespace, $folder ); } include_once WP_CLI_VENDOR_DIR . '/rmccue/requests/library/Requests.php'; include_once WP_CLI_VENDOR_DIR . '/wp-cli/mustangostang-spyc/Spyc.php'; $autoloader->register(); return $state; } } add_deferred_commands(); // Process deferred command additions for commands added through // plugins. WP_CLI::add_hook( 'before_run_command', [ $this, 'add_deferred_commands' ] ); return $state; } /** * Add deferred commands that are still waiting to be processed. */ public function add_deferred_commands() { $deferred_additions = WP_CLI::get_deferred_additions(); foreach ( $deferred_additions as $name => $addition ) { $addition_data = []; foreach ( $addition as $addition_key => $addition_value ) { // Describe the callable as a string instead of directly printing it // for better debug info. if ( 'callable' === $addition_key ) { $addition_value = Utils\describe_callable( $addition_value ); } elseif ( is_array( $addition_value ) ) { $addition_value = json_encode( $addition_value ); } $addition_data[] = sprintf( '%s: %s', $addition_key, $addition_value ); } WP_CLI::debug( sprintf( 'Adding deferred command: %s (%s)', $name, implode( ', ', $addition_data ) ), 'bootstrap' ); WP_CLI::add_command( $name, $addition['callable'], $addition['args'] ); } } } get_custom_vendor_folder(); if ( false !== $custom_vendor ) { array_unshift( $autoloader_paths, WP_CLI_ROOT . '/../../../' . $custom_vendor . '/autoload.php' ); } WP_CLI::debug( sprintf( 'Fallback autoloader paths: %s', implode( ', ', $autoloader_paths ) ), 'bootstrap' ); return $autoloader_paths; } } state->getValue( BootstrapState::IS_PROTECTED_COMMAND, false ) ) { return false; } $runner = new RunnerInstance(); $skip_packages = $runner()->config['skip-packages']; if ( true === $skip_packages ) { WP_CLI::debug( 'Skipped loading packages.', 'bootstrap' ); return false; } $autoloader_path = $runner()->get_packages_dir_path() . 'vendor/autoload.php'; if ( is_readable( $autoloader_path ) ) { WP_CLI::debug( 'Loading packages from: ' . $autoloader_path, 'bootstrap' ); return [ $autoloader_path, ]; } return false; } /** * Handle the failure to find an autoloader. * * @return void */ protected function handle_failure() { WP_CLI::debug( 'No package autoload found to load.', 'bootstrap' ); } } get( $arg ); if ( ! $item ) { WP_CLI::error( sprintf( $this->msg, $arg ) ); } return $item; } /** * Get multiple items. * * @param array $args The raw CLI arguments. * @return array The list of found items. */ public function get_many( $args ) { $items = []; foreach ( $args as $arg ) { $item = $this->get( $arg ); if ( $item ) { $items[] = $item; } else { WP_CLI::warning( sprintf( $this->msg, $arg ) ); } } return $items; } } get_site( $site_id ); } /** * Get site (blog) data for a given id. * * @param string $arg The raw CLI argument. * @return array|false The item if found; false otherwise. */ private function get_site( $arg ) { global $wpdb; // Load site data $site = $wpdb->get_row( $wpdb->prepare( "SELECT * FROM {$wpdb->blogs} WHERE blog_id = %d", $arg ) ); if ( ! empty( $site ) ) { // Only care about domain and path which are set here return $site; } return false; } } * foreach( new Iterators\Query( 'SELECT * FROM users', 100 ) as $user ) { * tickle( $user ); * } * * * @param string $query The query as a string. It shouldn't include any LIMIT clauses * @param int $chunk_size How many rows to retrieve at once; default value is 500 (optional) */ public function __construct( $query, $chunk_size = 500 ) { $this->query = $query; $this->count_query = preg_replace( '/^.*? FROM /', 'SELECT COUNT(*) FROM ', $query, 1, $replacements ); if ( 1 !== $replacements ) { $this->count_query = ''; } $this->chunk_size = $chunk_size; $this->db = $GLOBALS['wpdb']; } /** * Reduces the offset when the query row count shrinks * * In cases where the iterated rows are being updated such that they will no * longer be returned by the original query, the offset must be reduced to * iterate over all remaining rows. */ private function adjust_offset_for_shrinking_result_set() { if ( empty( $this->count_query ) ) { return; } $row_count = $this->db->get_var( $this->count_query ); if ( $row_count < $this->row_count ) { $this->offset -= $this->row_count - $row_count; } $this->row_count = $row_count; } private function load_items_from_db() { $this->adjust_offset_for_shrinking_result_set(); $query = $this->query . sprintf( ' LIMIT %d OFFSET %d', $this->chunk_size, $this->offset ); $this->results = $this->db->get_results( $query ); if ( ! $this->results ) { if ( $this->db->last_error ) { throw new Exception( 'Database error: ' . $this->db->last_error ); } return false; } $this->offset += $this->chunk_size; return true; } public function current() { return $this->results[ $this->index_in_results ]; } public function key() { return $this->global_index; } public function next() { $this->index_in_results++; $this->global_index++; } public function rewind() { $this->results = []; $this->global_index = 0; $this->index_in_results = 0; $this->offset = 0; $this->depleted = false; } public function valid() { if ( $this->depleted ) { return false; } if ( ! isset( $this->results[ $this->index_in_results ] ) ) { $items_loaded = $this->load_items_from_db(); if ( ! $items_loaded ) { $this->rewind(); $this->depleted = true; return false; } $this->index_in_results = 0; } return true; } } transformers[] = $fn; } public function current() { $value = parent::current(); foreach ( $this->transformers as $fn ) { $value = call_user_func( $fn, $value ); } return $value; } } * foreach( new Iterators\Table( array( 'table' => $wpdb->posts, 'fields' => array( 'ID', 'post_content' ) ) ) as $post ) { * count_words_for( $post->ID, $post->post_content ); * } * * * * foreach( new Iterators\Table( array( 'table' => $wpdb->posts, 'where' => 'ID = 8 OR post_status = "publish"' ) ) as $post ) { * … * } * * * * foreach( new PostIterator( array( 'table' => $wpdb->posts, 'where' => array( 'post_status' => 'publish', 'post_date_gmt BETWEEN x AND y' ) ) ) as $post ) { * … * } * * * @param array $args Supported arguments: * table – the name of the database table * fields – an array of columns to get from the table, '*' is a valid value and the default * where – conditions for filtering rows. Supports two formats: * = string – this will be the where clause * = array – each element is treated as a condition if it's positional, or as column => value if * it's a key/value pair. In the latter case the value is automatically quoted and escaped * append - add arbitrary extra SQL */ public function __construct( $args = [] ) { $defaults = [ 'fields' => '*', 'where' => [], 'append' => '', 'table' => null, 'chunk_size' => 500, ]; $table = $args['table']; $args = array_merge( $defaults, $args ); $fields = self::build_fields( $args['fields'] ); $conditions = self::build_where_conditions( $args['where'] ); $where_sql = $conditions ? " WHERE $conditions" : ''; $query = "SELECT $fields FROM `$table` $where_sql {$args['append']}"; parent::__construct( $query, $args['chunk_size'] ); } private static function build_fields( $fields ) { if ( '*' === $fields ) { return $fields; } return implode( ', ', array_map( function ( $v ) { return "`$v`"; }, $fields ) ); } private static function build_where_conditions( $where ) { global $wpdb; if ( is_array( $where ) ) { $conditions = []; foreach ( $where as $key => $value ) { if ( is_array( $value ) ) { $conditions[] = $key . ' IN (' . esc_sql( implode( ',', $value ) ) . ')'; } elseif ( is_numeric( $key ) ) { $conditions[] = $value; } else { $conditions[] = $key . $wpdb->prepare( ' = %s', $value ); } } $where = implode( ' AND ', $conditions ); } return $where; } } filename = $filename; $this->file_pointer = fopen( $filename, 'rb' ); if ( ! $this->file_pointer ) { WP_CLI::error( sprintf( 'Could not open file: %s', $filename ) ); } $this->delimiter = $delimiter; } public function rewind() { rewind( $this->file_pointer ); $this->columns = fgetcsv( $this->file_pointer, self::ROW_SIZE, $this->delimiter ); $this->current_index = -1; $this->next(); } public function current() { return $this->current_element; } public function key() { return $this->current_index; } public function next() { $this->current_element = false; while ( true ) { $str = fgets( $this->file_pointer ); if ( false === $str ) { break; } $row = str_getcsv( $str, $this->delimiter ); $element = []; foreach ( $this->columns as $i => $key ) { if ( isset( $row[ $i ] ) ) { $element[ $key ] = $row[ $i ]; } } if ( ! empty( $element ) ) { $this->current_element = $element; $this->current_index++; break; } } } public function count() { $file = new SplFileObject( $this->filename, 'r' ); $file->seek( PHP_INT_MAX ); return $file->key() + 1; } public function valid() { return is_array( $this->current_element ); } } open( $zipfile ); if ( true === $res ) { $name = Utils\basename( $zipfile ); $tempdir = Utils\get_temp_dir() . uniqid( 'wp-cli-extract-zipfile-', true ) . "-{$name}"; $zip->extractTo( $tempdir ); $zip->close(); self::copy_overwrite_files( self::get_first_subfolder( $tempdir ), $dest ); self::rmdir( $tempdir ); } else { throw new Exception( sprintf( "ZipArchive failed to unzip '%s': %s.", $zipfile, self::zip_error_msg( $res ) ) ); } } /** * Extract a tarball to a specific destination. * * @param string $tarball * @param string $dest */ private static function extract_tarball( $tarball, $dest ) { // Ensure the destination folder exists or can be created. if ( ! self::ensure_dir_exists( $dest ) ) { throw new Exception( "Could not create folder '{$dest}'." ); } if ( class_exists( 'PharData' ) ) { try { $phar = new PharData( $tarball ); $name = Utils\basename( $tarball ); $tempdir = Utils\get_temp_dir() . uniqid( 'wp-cli-extract-tarball-', true ) . "-{$name}"; $phar->extractTo( $tempdir ); self::copy_overwrite_files( self::get_first_subfolder( $tempdir ), $dest ); self::rmdir( $tempdir ); return; } catch ( Exception $e ) { WP_CLI::warning( "PharData failed, falling back to 'tar xz' (" . $e->getMessage() . ')' ); // Fall through to trying `tar xz` below. } } // Ensure relative paths cannot be misinterpreted as hostnames. // Prepending `./` will force tar to interpret it as a filesystem path. if ( self::path_is_relative( $tarball ) ) { $tarball = "./{$tarball}"; } if ( ! file( $tarball ) || ! is_readable( $tarball ) || filesize( $tarball ) <= 0 ) { throw new Exception( "Invalid zip file '{$tarball}'." ); } // Note: directory must exist for tar --directory to work. $cmd = Utils\esc_cmd( 'tar xz --strip-components=1 --directory=%s -f %s', $dest, $tarball ); $process_run = WP_CLI::launch( $cmd, false /*exit_on_error*/, true /*return_detailed*/ ); if ( 0 !== $process_run->return_code ) { throw new Exception( sprintf( 'Failed to execute `%s`: %s.', $cmd, self::tar_error_msg( $process_run ) ) ); } } /** * Copy files from source directory to destination directory. Source * directory must exist. * * @param string $source * @param string $dest */ public static function copy_overwrite_files( $source, $dest ) { $iterator = new RecursiveIteratorIterator( new RecursiveDirectoryIterator( $source, RecursiveDirectoryIterator::SKIP_DOTS ), RecursiveIteratorIterator::SELF_FIRST ); $error = 0; if ( ! is_dir( $dest ) ) { mkdir( $dest, 0777, true ); } foreach ( $iterator as $item ) { $dest_path = $dest . DIRECTORY_SEPARATOR . $iterator->getSubPathName(); if ( $item->isDir() ) { if ( ! is_dir( $dest_path ) ) { mkdir( $dest_path ); } } else { if ( file_exists( $dest_path ) && is_writable( $dest_path ) ) { copy( $item, $dest_path ); } elseif ( ! file_exists( $dest_path ) ) { copy( $item, $dest_path ); } else { $error = 1; WP_CLI::warning( "Unable to copy '" . $iterator->getSubPathName() . "' to current directory." ); } } } if ( $error ) { throw new Exception( 'There was an error overwriting existing files.' ); } } /** * Delete all files and directories recursively from directory. Directory * must exist. * * @param string $dir */ public static function rmdir( $dir ) { $files = new RecursiveIteratorIterator( new RecursiveDirectoryIterator( $dir, RecursiveDirectoryIterator::SKIP_DOTS ), RecursiveIteratorIterator::CHILD_FIRST ); foreach ( $files as $fileinfo ) { $todo = $fileinfo->isDir() ? 'rmdir' : 'unlink'; $path = $fileinfo->getRealPath(); if ( 0 !== strpos( $path, $fileinfo->getRealPath() ) ) { WP_CLI::warning( "Temporary file or folder to be removed was found outside of temporary folder, aborting removal: '{$path}'" ); } $todo( $path ); } rmdir( $dir ); } /** * Return formatted ZipArchive error message from error code. * * @param int $error_code * @return string|int The error message corresponding to the specified * code, if found; Other wise the same error code, * unmodified. */ public static function zip_error_msg( $error_code ) { // From https://github.com/php/php-src/blob/php-5.3.0/ext/zip/php_zip.c#L2623-L2646. static $zip_err_msgs = [ ZipArchive::ER_OK => 'No error', ZipArchive::ER_MULTIDISK => 'Multi-disk zip archives not supported', ZipArchive::ER_RENAME => 'Renaming temporary file failed', ZipArchive::ER_CLOSE => 'Closing zip archive failed', ZipArchive::ER_SEEK => 'Seek error', ZipArchive::ER_READ => 'Read error', ZipArchive::ER_WRITE => 'Write error', ZipArchive::ER_CRC => 'CRC error', ZipArchive::ER_ZIPCLOSED => 'Containing zip archive was closed', ZipArchive::ER_NOENT => 'No such file', ZipArchive::ER_EXISTS => 'File already exists', ZipArchive::ER_OPEN => 'Can\'t open file', ZipArchive::ER_TMPOPEN => 'Failure to create temporary file', ZipArchive::ER_ZLIB => 'Zlib error', ZipArchive::ER_MEMORY => 'Malloc failure', ZipArchive::ER_CHANGED => 'Entry has been changed', ZipArchive::ER_COMPNOTSUPP => 'Compression method not supported', ZipArchive::ER_EOF => 'Premature EOF', ZipArchive::ER_INVAL => 'Invalid argument', ZipArchive::ER_NOZIP => 'Not a zip archive', ZipArchive::ER_INTERNAL => 'Internal error', ZipArchive::ER_INCONS => 'Zip archive inconsistent', ZipArchive::ER_REMOVE => 'Can\'t remove file', ZipArchive::ER_DELETED => 'Entry has been deleted', ]; if ( isset( $zip_err_msgs[ $error_code ] ) ) { return sprintf( '%s (%d)', $zip_err_msgs[ $error_code ], $error_code ); } return $error_code; } /** * Return formatted error message from ProcessRun of tar command. * * @param Processrun $process_run * @return string|int The error message of the process, if available; * otherwise the return code. */ public static function tar_error_msg( $process_run ) { $stderr = trim( $process_run->stderr ); $nl_pos = strpos( $stderr, "\n" ); if ( false !== $nl_pos ) { $stderr = trim( substr( $stderr, 0, $nl_pos ) ); } if ( $stderr ) { return sprintf( '%s (%d)', $stderr, $process_run->return_code ); } return $process_run->return_code; } /** * Return the first subfolder within a given path. * * Falls back to the provided path if no subfolder was detected. * * @param string $path Path to find the first subfolder in. * @return string First subfolder, or same as $path if none found. */ private static function get_first_subfolder( $path ) { $iterator = new DirectoryIterator( $path ); foreach ( $iterator as $fileinfo ) { if ( $fileinfo->isDir() && ! $fileinfo->isDot() ) { return "{$path}/{$fileinfo->getFilename()}"; } } return $path; } /** * Ensure directory exists. * * @param string $dir Directory to ensure the existence of. * @return bool Whether the existence could be asserted. */ private static function ensure_dir_exists( $dir ) { if ( ! is_dir( $dir ) ) { if ( ! @mkdir( $dir, 0777, true ) ) { $error = error_get_last(); WP_CLI::warning( sprintf( "Failed to create directory '%s': %s.", $dir, $error['message'] ) ); return false; } } return true; } /** * Check whether a path is relative- * * @param string $path Path to check. * @return bool Whether the path is relative. */ private static function path_is_relative( $path ) { if ( '' === $path ) { return true; } // Strip scheme. $scheme_position = strpos( $path, '://' ); if ( false !== $scheme_position ) { $path = substr( $path, $scheme_position + 3 ); } // UNIX root "/" or "\" (Windows style). if ( '/' === $path[0] || '\\' === $path[0] ) { return false; } // Windows root. if ( strlen( $path ) > 1 && ctype_alpha( $path[0] ) && ':' === $path[1] ) { // Special case: only drive letter, like "C:". if ( 2 === strlen( $path ) ) { return false; } // Regular Windows path starting with drive letter, like "C:/ or "C:\". if ( '/' === $path[2] || '\\' === $path[2] ) { return false; } } return true; } } spec = include $path; $defaults = [ 'runtime' => false, 'file' => false, 'synopsis' => '', 'default' => null, 'multiple' => false, ]; foreach ( $this->spec as $key => &$details ) { $details = array_merge( $defaults, $details ); $this->config[ $key ] = $details['default']; } } /** * Get declared configuration values as an array. * * @return array */ public function to_array() { return [ $this->config, $this->extra_config ]; } /** * Get configuration specification, i.e. list of accepted keys. * * @return array */ public function get_spec() { return $this->spec; } /** * Get any aliases defined in config files. * * @return array */ public function get_aliases() { $runtime_alias = getenv( 'WP_CLI_RUNTIME_ALIAS' ); if ( false !== $runtime_alias ) { $returned_aliases = []; foreach ( json_decode( $runtime_alias, true ) as $key => $value ) { if ( preg_match( '#' . self::ALIAS_REGEX . '#', $key ) ) { $returned_aliases[ $key ] = []; foreach ( self::$alias_spec as $i ) { if ( isset( $value[ $i ] ) ) { $returned_aliases[ $key ][ $i ] = $value[ $i ]; } } } } return $returned_aliases; } return $this->aliases; } /** * Splits a list of arguments into positional, associative and config. * * @param array(string) $arguments * @return array(array) */ public function parse_args( $arguments ) { list( $positional_args, $mixed_args, $global_assoc, $local_assoc ) = self::extract_assoc( $arguments ); list( $assoc_args, $runtime_config ) = $this->unmix_assoc_args( $mixed_args, $global_assoc, $local_assoc ); return [ $positional_args, $assoc_args, $runtime_config ]; } /** * Splits positional args from associative args. * * @param array $arguments * @return array(array) */ public static function extract_assoc( $arguments ) { $positional_args = []; $assoc_args = []; $global_assoc = []; $local_assoc = []; foreach ( $arguments as $arg ) { $positional = null; $assoc_arg = null; if ( preg_match( '|^--no-([^=]+)$|', $arg, $matches ) ) { $assoc_arg = [ $matches[1], false ]; } elseif ( preg_match( '|^--([^=]+)$|', $arg, $matches ) ) { $assoc_arg = [ $matches[1], true ]; } elseif ( preg_match( '|^--([^=]+)=(.*)|s', $arg, $matches ) ) { $assoc_arg = [ $matches[1], $matches[2] ]; } else { $positional = $arg; } if ( ! is_null( $assoc_arg ) ) { $assoc_args[] = $assoc_arg; if ( count( $positional_args ) ) { $local_assoc[] = $assoc_arg; } else { $global_assoc[] = $assoc_arg; } } elseif ( ! is_null( $positional ) ) { $positional_args[] = $positional; } } return [ $positional_args, $assoc_args, $global_assoc, $local_assoc ]; } /** * Separate runtime parameters from command-specific parameters. * * @param array $mixed_args * @return array */ private function unmix_assoc_args( $mixed_args, $global_assoc = [], $local_assoc = [] ) { $assoc_args = []; $runtime_config = []; if ( getenv( 'WP_CLI_STRICT_ARGS_MODE' ) ) { foreach ( $global_assoc as $tmp ) { list( $key, $value ) = $tmp; if ( isset( $this->spec[ $key ] ) && false !== $this->spec[ $key ]['runtime'] ) { $this->assoc_arg_to_runtime_config( $key, $value, $runtime_config ); } } foreach ( $local_assoc as $tmp ) { $assoc_args[ $tmp[0] ] = $tmp[1]; } } else { foreach ( $mixed_args as $tmp ) { list( $key, $value ) = $tmp; if ( isset( $this->spec[ $key ] ) && false !== $this->spec[ $key ]['runtime'] ) { $this->assoc_arg_to_runtime_config( $key, $value, $runtime_config ); } else { $assoc_args[ $key ] = $value; } } } return [ $assoc_args, $runtime_config ]; } /** * Handle turning an $assoc_arg into a runtime arg. */ private function assoc_arg_to_runtime_config( $key, $value, &$runtime_config ) { $details = $this->spec[ $key ]; if ( isset( $details['deprecated'] ) ) { fwrite( STDERR, "WP-CLI: The --{$key} global parameter is deprecated. {$details['deprecated']}\n" ); } if ( $details['multiple'] ) { $runtime_config[ $key ][] = $value; } else { $runtime_config[ $key ] = $value; } } /** * Load a YAML file of parameters into scope. * * @param string $path Path to YAML file. */ public function merge_yml( $path, $current_alias = null ) { $yaml = self::load_yml( $path ); if ( ! empty( $yaml['_']['inherit'] ) ) { $this->merge_yml( $yaml['_']['inherit'], $current_alias ); } // Prepare the base path for absolutized alias paths. $yml_file_dir = $path ? dirname( $path ) : false; foreach ( $yaml as $key => $value ) { if ( preg_match( '#' . self::ALIAS_REGEX . '#', $key ) ) { $this->aliases[ $key ] = []; $is_alias = false; foreach ( self::$alias_spec as $i ) { if ( isset( $value[ $i ] ) ) { if ( 'path' === $i && ! isset( $value['ssh'] ) ) { self::absolutize( $value[ $i ], $yml_file_dir ); } $this->aliases[ $key ][ $i ] = $value[ $i ]; $is_alias = true; } } // If it's not an alias, it might be a group of aliases. if ( ! $is_alias && is_array( $value ) ) { $alias_group = []; foreach ( $value as $k ) { if ( preg_match( '#' . self::ALIAS_REGEX . '#', $k ) ) { $alias_group[] = $k; } } $this->aliases[ $key ] = $alias_group; } } elseif ( ! isset( $this->spec[ $key ] ) || false === $this->spec[ $key ]['file'] ) { if ( isset( $this->extra_config[ $key ] ) && ! empty( $yaml['_']['merge'] ) && is_array( $this->extra_config[ $key ] ) && is_array( $value ) ) { $this->extra_config[ $key ] = array_merge( $this->extra_config[ $key ], $value ); } else { $this->extra_config[ $key ] = $value; } } elseif ( $this->spec[ $key ]['multiple'] ) { self::arrayify( $value ); $this->config[ $key ] = array_merge( $this->config[ $key ], $value ); } else { if ( $current_alias && in_array( $key, self::$alias_spec, true ) ) { continue; } $this->config[ $key ] = $value; } } } /** * Merge an array of values into the configurator config. * * @param array $config */ public function merge_array( $config ) { foreach ( $this->spec as $key => $details ) { if ( false !== $details['runtime'] && isset( $config[ $key ] ) ) { $value = $config[ $key ]; if ( 'require' === $key ) { $value = Utils\expand_globs( $value ); } if ( $details['multiple'] ) { self::arrayify( $value ); $this->config[ $key ] = array_merge( $this->config[ $key ], $value ); } else { $this->config[ $key ] = $value; } } } } /** * Load values from a YAML file. * * @param string $yml_file Path to the YAML file * @return array Declared configuration values */ private static function load_yml( $yml_file ) { if ( ! $yml_file ) { return []; } $config = Spyc::YAMLLoad( $yml_file ); // Make sure config-file-relative paths are made absolute. $yml_file_dir = dirname( $yml_file ); if ( isset( $config['path'] ) ) { self::absolutize( $config['path'], $yml_file_dir ); } if ( isset( $config['require'] ) ) { self::arrayify( $config['require'] ); $config['require'] = Utils\expand_globs( $config['require'] ); foreach ( $config['require'] as &$path ) { self::absolutize( $path, $yml_file_dir ); } } // Backwards compat // Command 'core config' was moved to 'config create'. if ( isset( $config['core config'] ) ) { $config['config create'] = $config['core config']; unset( $config['core config'] ); } // Command 'checksum core' was moved to 'core verify-checksums'. if ( isset( $config['checksum core'] ) ) { $config['core verify-checksums'] = $config['checksum core']; unset( $config['checksum core'] ); } // Command 'checksum plugin' was moved to 'plugin verify-checksums'. if ( isset( $config['checksum plugin'] ) ) { $config['plugin verify-checksums'] = $config['checksum plugin']; unset( $config['checksum plugin'] ); } return $config; } /** * Conform a variable to an array. * * @param mixed $val A string or an array */ private static function arrayify( &$val ) { $val = (array) $val; } /** * Make a path absolute. * * @param string $path Path to file. * @param string $base Base path to prepend. */ private static function absolutize( &$path, $base ) { if ( ! empty( $path ) && ! Utils\is_path_absolute( $path ) ) { $path = $base . DIRECTORY_SEPARATOR . $path; } } } 'pre_install', PackageEvents::POST_PACKAGE_INSTALL => 'post_install', ]; } /** * Pre-install operation message. * * @param \Composer\Installer\PackageEvent $event * * @return void */ public static function pre_install( PackageEvent $event ) { $operation_message = $event->getOperation()->__toString(); WP_CLI::log( ' - ' . $operation_message ); } /** * Post-install operation log. * * @param \Composer\Installer\PackageEvent $event * * @return void */ public static function post_install( PackageEvent $event ) { $operation = $event->getOperation(); // getReason() was removed in Composer v2 without replacement. if ( ! method_exists( $operation, 'getReason' ) ) { return; } $reason = $operation->getReason(); if ( $reason instanceof Rule ) { switch ( $reason->getReason() ) { case Rule::RULE_PACKAGE_CONFLICT: case Rule::RULE_PACKAGE_SAME_NAME: case Rule::RULE_PACKAGE_REQUIRES: $composer_error = $reason->getPrettyString( $event->getPool() ); break; } if ( ! empty( $composer_error ) ) { WP_CLI::log( sprintf( ' - Warning: %s', $composer_error ) ); } } } } options = $options; } /** * Gets the checksums for the given version of WordPress core. * * @param string $version Version string to query. * @param string $locale Optional. Locale to query. Defaults to 'en_US'. * @return bool|array False on failure. An array of checksums on success. * @throws RuntimeException If the remote request fails. */ public function get_core_checksums( $version, $locale = 'en_US' ) { $url = sprintf( '%s?%s', self::CORE_CHECKSUMS_ENDPOINT, http_build_query( compact( 'version', 'locale' ), null, '&' ) ); $response = $this->json_get_request( $url ); if ( ! is_array( $response ) || ! isset( $response['checksums'] ) || ! is_array( $response['checksums'] ) ) { return false; } return $response['checksums']; } /** * Gets a core version check. * * @param string $locale Optional. Locale to request a version check for. Defaults to 'en_US'. * @return array|false False on failure. Associative array of the offer on success. * @throws RuntimeException If the remote request failed. */ public function get_core_version_check( $locale = 'en_US' ) { $url = sprintf( '%s?%s', self::VERSION_CHECK_ENDPOINT, http_build_query( compact( 'locale' ), null, '&' ) ); $response = $this->json_get_request( $url ); if ( ! is_array( $response ) ) { return false; } return $response; } /** * Gets a download offer. * * @param string $locale Optional. Locale to request an offer from. Defaults to 'en_US'. * @return array|false False on failure. Associative array of the offer on success. * @throws RuntimeException If the remote request failed. */ public function get_core_download_offer( $locale = 'en_US' ) { $response = $this->get_core_version_check( $locale ); if ( ! is_array( $response ) || ! isset( $response['offers'] ) || ! is_array( $response['offers'] ) ) { return false; } $offer = $response['offers'][0]; if ( ! array_key_exists( 'locale', $offer ) || $locale !== $offer['locale'] ) { return false; } return $offer; } /** * Gets the checksums for the given version of plugin. * * @param string $plugin Plugin slug to query. * @param string $version Version string to query. * @return bool|array False on failure. An array of checksums on success. * @throws RuntimeException If the remote request fails. */ public function get_plugin_checksums( $plugin, $version ) { $url = sprintf( '%s%s/%s.json', self::PLUGIN_CHECKSUMS_ENDPOINT, $plugin, $version ); $response = $this->json_get_request( $url ); if ( ! is_array( $response ) || ! isset( $response['files'] ) || ! is_array( $response['files'] ) ) { return false; } return $response['files']; } /** * Gets a plugin's info. * * @param string $plugin Plugin slug to query. * @param string $locale Optional. Locale to request info for. Defaults to 'en_US'. * @return array|false False on failure. Associative array of the offer on success. * @throws RuntimeException If the remote request failed. */ public function get_plugin_info( $plugin, $locale = 'en_US' ) { $action = 'plugin_information'; $request = [ 'locale' => $locale, 'slug' => $plugin, ]; $url = sprintf( '%s?%s', self::PLUGIN_INFO_ENDPOINT, http_build_query( compact( 'action', 'request' ), null, '&' ) ); $response = $this->json_get_request( $url ); if ( ! is_array( $response ) ) { return false; } return $response; } /** * Gets a theme's info. * * @param string $theme Theme slug to query. * @param string $locale Optional. Locale to request info for. Defaults to 'en_US'. * @return array|false False on failure. Associative array of the offer on success. * @throws RuntimeException If the remote request failed. */ public function get_theme_info( $theme, $locale = 'en_US' ) { $action = 'theme_information'; $request = [ 'locale' => $locale, 'slug' => $theme, ]; $url = sprintf( '%s?%s', self::THEME_INFO_ENDPOINT, http_build_query( compact( 'action', 'request' ), null, '&' ) ); $response = $this->json_get_request( $url ); if ( ! is_array( $response ) ) { return false; } return $response; } /** * Gets a set of salts in the format required by `wp-config.php`. * * @return bool|string False on failure. A string of PHP define() statements on success. * @throws RuntimeException If the remote request fails. */ public function get_salts() { return $this->get_request( self::SALT_ENDPOINT ); } /** * Execute a remote GET request. * * @param string $url URL to execute the GET request on. * @param array $headers Optional. Associative array of headers. * @param array $options Optional. Associative array of options. * @return mixed|false False on failure. Decoded JSON on success. * @throws RuntimeException If the JSON could not be decoded. */ private function json_get_request( $url, $headers = [], $options = [] ) { $headers = array_merge( [ 'Accept' => 'application/json', ], $headers ); $response = $this->get_request( $url, $headers, $options ); if ( false === $response ) { return $response; } $data = json_decode( $response, true ); if ( JSON_ERROR_NONE !== json_last_error() ) { throw new RuntimeException( 'Failed to decode JSON: ' . json_last_error_msg() ); } return $data; } /** * Execute a remote GET request. * * @param string $url URL to execute the GET request on. * @param array $headers Optional. Associative array of headers. * @param array $options Optional. Associative array of options. * @return string|false False on failure. Response body string on success. * @throws RuntimeException If the remote request fails. */ private function get_request( $url, $headers = [], $options = [] ) { $options = array_merge( $this->options, [ 'halt_on_error' => false, ], $options ); $response = Utils\http_request( 'GET', $url, null, $headers, $options ); if ( ! $response->success || 200 > (int) $response->status_code || 300 <= $response->status_code ) { throw new RuntimeException( "Couldn't fetch response from {$url} (HTTP code {$response->status_code})." ); } return trim( $response->body ); } } $value ) { $this->$key = $value; } } /** * Return properties of executed command as a string. * * @return string */ public function __toString() { $out = "$ $this->command\n"; $out .= "$this->stdout\n$this->stderr"; $out .= "cwd: $this->cwd\n"; $out .= "run time: $this->run_time\n"; $out .= "exit status: $this->return_code"; return $out; } } getMethod( $callable[1] ) ); } else { $reflection = new ReflectionClass( $callable ); if ( $reflection->isSubclassOf( '\WP_CLI\Dispatcher\CommandNamespace' ) ) { $command = self::create_namespace( $parent, $name, $callable ); } elseif ( $reflection->hasMethod( '__invoke' ) ) { $class = is_object( $callable ) ? $callable : $reflection->name; $command = self::create_subcommand( $parent, $name, [ $class, '__invoke' ], $reflection->getMethod( '__invoke' ) ); } else { $command = self::create_composite_command( $parent, $name, $callable ); } } return $command; } /** * Clear the file contents cache. */ public static function clear_file_contents_cache() { self::$file_contents = []; } /** * Create a new Subcommand instance. * * @param mixed $parent The new command's parent Composite command * @param string|bool $name Represents how the command should be invoked. * If false, will be determined from the documented subject, represented by `$reflection`. * @param mixed $callable A callable function or closure, or class name and method * @param object $reflection Reflection instance, for doc parsing */ private static function create_subcommand( $parent, $name, $callable, $reflection ) { $doc_comment = self::get_doc_comment( $reflection ); $docparser = new DocParser( $doc_comment ); if ( is_array( $callable ) ) { if ( ! $name ) { $name = $docparser->get_tag( 'subcommand' ); } if ( ! $name ) { $name = $reflection->name; } } if ( ! $doc_comment ) { WP_CLI::debug( null === $doc_comment ? "Failed to get doc comment for {$name}." : "No doc comment for {$name}.", 'commandfactory' ); } $when_invoked = function ( $args, $assoc_args ) use ( $callable ) { if ( is_array( $callable ) ) { $callable[0] = is_object( $callable[0] ) ? $callable[0] : new $callable[0](); call_user_func( [ $callable[0], $callable[1] ], $args, $assoc_args ); } else { call_user_func( $callable, $args, $assoc_args ); } }; return new Subcommand( $parent, $name, $docparser, $when_invoked ); } /** * Create a new Composite command instance. * * @param mixed $parent The new command's parent Root or Composite command * @param string $name Represents how the command should be invoked * @param mixed $callable */ private static function create_composite_command( $parent, $name, $callable ) { $reflection = new ReflectionClass( $callable ); $doc_comment = self::get_doc_comment( $reflection ); if ( ! $doc_comment ) { WP_CLI::debug( null === $doc_comment ? "Failed to get doc comment for {$name}." : "No doc comment for {$name}.", 'commandfactory' ); } $docparser = new DocParser( $doc_comment ); $container = new CompositeCommand( $parent, $name, $docparser ); foreach ( $reflection->getMethods() as $method ) { if ( ! self::is_good_method( $method ) ) { continue; } $class = is_object( $callable ) ? $callable : $reflection->name; $subcommand = self::create_subcommand( $container, false, [ $class, $method->name ], $method ); $subcommand_name = $subcommand->get_name(); $container->add_subcommand( $subcommand_name, $subcommand ); } return $container; } /** * Create a new command namespace instance. * * @param mixed $parent The new namespace's parent Root or Composite command. * @param string $name Represents how the command should be invoked * @param mixed $callable */ private static function create_namespace( $parent, $name, $callable ) { $reflection = new ReflectionClass( $callable ); $doc_comment = self::get_doc_comment( $reflection ); if ( ! $doc_comment ) { WP_CLI::debug( null === $doc_comment ? "Failed to get doc comment for {$name}." : "No doc comment for {$name}.", 'commandfactory' ); } $docparser = new DocParser( $doc_comment ); return new CommandNamespace( $parent, $name, $docparser ); } /** * Check whether a method is actually callable. * * @param ReflectionMethod $method * @return bool */ private static function is_good_method( $method ) { return $method->isPublic() && ! $method->isStatic() && 0 !== strpos( $method->getName(), '__' ); } /** * Gets the document comment. Caters for PHP directive `opcache.save comments` being disabled. * * @param ReflectionMethod|ReflectionClass|ReflectionFunction $reflection Reflection instance. * @return string|false|null Doc comment string if any, false if none (same as `Reflection*::getDocComment()`), null if error. */ private static function get_doc_comment( $reflection ) { $contents = null; $doc_comment = $reflection->getDocComment(); if ( false !== $doc_comment || ! ( ini_get( 'opcache.enable_cli' ) && ! ini_get( 'opcache.save_comments' ) ) ) { // phpcs:ignore PHPCompatibility.IniDirectives.NewIniDirectives // Either have doc comment, or no doc comment and save comments enabled - standard situation. if ( ! getenv( 'WP_CLI_TEST_GET_DOC_COMMENT' ) ) { return $doc_comment; } } $filename = $reflection->getFileName(); if ( isset( self::$file_contents[ $filename ] ) ) { $contents = self::$file_contents[ $filename ]; } elseif ( is_readable( $filename ) ) { $contents = file_get_contents( $filename ); if ( is_string( $contents ) && '' !== $contents ) { $contents = explode( "\n", $contents ); self::$file_contents[ $filename ] = $contents; } } if ( ! empty( $contents ) ) { return self::extract_last_doc_comment( implode( "\n", array_slice( $contents, 0, $reflection->getStartLine() ) ) ); } WP_CLI::debug( "Could not read contents for filename '{$filename}'.", 'commandfactory' ); return null; } /** * Returns the last doc comment if any in `$content`. * * @param string $content The content, which should end at the class or function declaration. * @return string|bool The last doc comment if any, or false if none. */ private static function extract_last_doc_comment( $content ) { $content = trim( $content ); $comment_end_pos = strrpos( $content, '*/' ); if ( false === $comment_end_pos ) { return false; } // Make sure comment end belongs to this class/function. if ( preg_match_all( '/(?:^|[\s;}])(?:class|function)\s+/', substr( $content, $comment_end_pos + 2 ), $dummy /*needed for PHP 5.3*/ ) > 1 ) { return false; } $content = substr( $content, 0, $comment_end_pos + 2 ); $comment_start_pos = strrpos( $content, '/**' ); if ( false === $comment_start_pos || ( $comment_start_pos + 2 ) === $comment_end_pos ) { return false; } // Make sure comment start belongs to this comment end. $comment_end2_pos = strpos( substr( $content, $comment_start_pos ), '*/' ); if ( false !== $comment_end2_pos && ( $comment_start_pos + $comment_end2_pos ) < $comment_end_pos ) { return false; } // Allow for '/**' within doc comment. $subcontent = substr( $content, 0, $comment_start_pos ); while ( false !== ( $comment_start2_pos = strrpos( $subcontent, '/**' ) ) && false === strpos( $subcontent, '*/', $comment_start2_pos ) ) { $comment_start_pos = $comment_start2_pos; $subcontent = substr( $subcontent, 0, $comment_start_pos ); } return substr( $content, $comment_start_pos, $comment_end_pos + 2 ); } } get_subcommands(); $i = 0; $count = 0; foreach ( $methods as $subcommand ) { $prefix = ( 0 === $i ) ? 'usage: ' : ' or: '; $i++; if ( \WP_CLI::get_runner()->is_command_disabled( $subcommand ) ) { continue; } \WP_CLI::line( $subcommand->get_usage( $prefix ) ); $count++; } $cmd_name = implode( ' ', array_slice( get_path( $this ), 1 ) ); $message = $count > 0 ? "See 'wp help $cmd_name ' for more information on a specific command." : "The namespace $cmd_name does not contain any usable commands in the current context."; \WP_CLI::line(); \WP_CLI::line( $message ); } } parent = $parent; $this->name = $name; $this->shortdesc = $docparser->get_shortdesc(); $this->longdesc = $docparser->get_longdesc(); $this->docparser = $docparser; $when_to_invoke = $docparser->get_tag( 'when' ); if ( $when_to_invoke ) { WP_CLI::get_runner()->register_early_invoke( $when_to_invoke, $this ); } } /** * Get the parent composite (or root) command * * @return mixed */ public function get_parent() { return $this->parent; } /** * Add a named subcommand to this composite command's * set of contained subcommands. * * @param string $name Represents how subcommand should be invoked * @param Subcommand|CompositeCommand $command Cub-command to add. * @param bool $override Optional. Whether to override an existing subcommand of the same * name. */ public function add_subcommand( $name, $command, $override = true ) { if ( $override || ! array_key_exists( $name, $this->subcommands ) ) { $this->subcommands[ $name ] = $command; } } /** * Remove a named subcommand from this composite command's set of contained * subcommands * * @param string $name Represents how subcommand should be invoked */ public function remove_subcommand( $name ) { if ( isset( $this->subcommands[ $name ] ) ) { unset( $this->subcommands[ $name ] ); } } /** * Composite commands always contain subcommands. * * @return true */ public function can_have_subcommands() { return true; } /** * Get the subcommands contained by this composite * command. * * @return array */ public function get_subcommands() { ksort( $this->subcommands ); return $this->subcommands; } /** * Get the name of this composite command. * * @return string */ public function get_name() { return $this->name; } /** * Get the short description for this composite * command. * * @return string */ public function get_shortdesc() { return $this->shortdesc; } /** * Set the short description for this composite command. * * @param string $shortdesc */ public function set_shortdesc( $shortdesc ) { $this->shortdesc = Utils\normalize_eols( $shortdesc ); } /** * Get the long description for this composite * command. * * @return string */ public function get_longdesc() { return $this->longdesc . $this->get_global_params(); } /** * Set the long description for this composite command * * @param string $longdesc */ public function set_longdesc( $longdesc ) { $this->longdesc = Utils\normalize_eols( $longdesc ); } /** * Get the synopsis for this composite command. * As a collection of subcommands, the composite * command is only intended to invoke those * subcommands. * * @return string */ public function get_synopsis() { return ''; } /** * Get the usage for this composite command. * * @return string */ public function get_usage( $prefix ) { return sprintf( '%s%s %s', $prefix, implode( ' ', get_path( $this ) ), $this->get_synopsis() ); } /** * Show the usage for all subcommands contained * by the composite command. */ public function show_usage() { $methods = $this->get_subcommands(); $i = 0; foreach ( $methods as $subcommand ) { $prefix = ( 0 === $i ) ? 'usage: ' : ' or: '; $i++; if ( WP_CLI::get_runner()->is_command_disabled( $subcommand ) ) { continue; } WP_CLI::line( $subcommand->get_usage( $prefix ) ); } $cmd_name = implode( ' ', array_slice( get_path( $this ), 1 ) ); WP_CLI::line(); WP_CLI::line( "See 'wp help $cmd_name ' for more information on a specific command." ); } /** * When a composite command is invoked, it shows usage * docs for its subcommands. * * @param array $args * @param array $assoc_args * @param array $extra_args */ public function invoke( $args, $assoc_args, $extra_args ) { $this->show_usage(); } /** * Given supplied arguments, find a contained * subcommand * * @param array $args * @return Subcommand|false */ public function find_subcommand( &$args ) { $name = array_shift( $args ); $subcommands = $this->get_subcommands(); if ( ! isset( $subcommands[ $name ] ) ) { $aliases = self::get_aliases( $subcommands ); if ( isset( $aliases[ $name ] ) ) { $name = $aliases[ $name ]; } } if ( ! isset( $subcommands[ $name ] ) ) { return false; } return $subcommands[ $name ]; } /** * Get any registered aliases for this composite command's * subcommands. * * @param array $subcommands * @return array */ private static function get_aliases( $subcommands ) { $aliases = []; foreach ( $subcommands as $name => $subcommand ) { $alias = $subcommand->get_alias(); if ( $alias ) { $aliases[ $alias ] = $name; } } return $aliases; } /** * Composite commands can only be known by one name. * * @return false */ public function get_alias() { return false; } /*** * Get the list of global parameters * * @param string $root_command whether to include or not root command specific description * @return string */ protected function get_global_params( $root_command = false ) { $binding = []; $binding['root_command'] = $root_command; if ( ! $this->can_have_subcommands() || ( is_object( $this->parent ) && get_class( $this->parent ) === 'WP_CLI\Dispatcher\CompositeCommand' ) ) { $binding['is_subcommand'] = true; } foreach ( WP_CLI::get_configurator()->get_spec() as $key => $details ) { if ( false === $details['runtime'] ) { continue; } if ( isset( $details['deprecated'] ) ) { continue; } if ( isset( $details['hidden'] ) ) { continue; } if ( true === $details['runtime'] ) { $synopsis = "--[no-]$key"; } else { $synopsis = "--$key" . $details['runtime']; } // Check if global parameters synopsis should be displayed or not. if ( 'true' !== getenv( 'WP_CLI_SUPPRESS_GLOBAL_PARAMS' ) ) { $binding['parameters'][] = [ 'synopsis' => $synopsis, 'desc' => $details['desc'], ]; $binding['has_parameters'] = true; } } if ( $this->get_subcommands() ) { $binding['has_subcommands'] = true; } return Utils\mustache_render( 'man-params.mustache', $binding ); } } parent = false; $this->name = 'wp'; $this->shortdesc = 'Manage WordPress through the command-line.'; } /** * Get the human-readable long description. * * @return string */ public function get_longdesc() { return $this->get_global_params( true ); } /** * Find a subcommand registered on the root * command. * * @param array $args * @return Subcommand|false */ public function find_subcommand( &$args ) { $command = array_shift( $args ); Utils\load_command( $command ); if ( ! isset( $this->subcommands[ $command ] ) ) { return false; } return $this->subcommands[ $command ]; } } when_invoked = $when_invoked; $this->alias = $docparser->get_tag( 'alias' ); $this->synopsis = $docparser->get_synopsis(); if ( ! $this->synopsis && $this->longdesc ) { $this->synopsis = self::extract_synopsis( $this->longdesc ); } } /** * Extract the synopsis from PHPdoc string. * * @param string $longdesc Command docs via PHPdoc * @return string */ private static function extract_synopsis( $longdesc ) { preg_match_all( '/(.+?)[\r\n]+:/', $longdesc, $matches ); return implode( ' ', $matches[1] ); } /** * Subcommands can't have subcommands because they * represent code to be executed. * * @return bool */ public function can_have_subcommands() { return false; } /** * Get the synopsis string for this subcommand. * A synopsis defines what runtime arguments are * expected, useful to humans and argument validation. * * @return string */ public function get_synopsis() { return $this->synopsis; } /** * Set the synopsis string for this subcommand. * * @param string $synopsis */ public function set_synopsis( $synopsis ) { $this->synopsis = $synopsis; } /** * If an alias is set, grant access to it. * Aliases permit subcommands to be instantiated * with a secondary identity. * * @return string */ public function get_alias() { return $this->alias; } /** * Print the usage details to the end user. * * @param string $prefix */ public function show_usage( $prefix = 'usage: ' ) { \WP_CLI::line( $this->get_usage( $prefix ) ); } /** * Get the usage of the subcommand as a formatted string. * * @param string $prefix * @return string */ public function get_usage( $prefix ) { return sprintf( '%s%s %s', $prefix, implode( ' ', get_path( $this ) ), $this->get_synopsis() ); } /** * Wrapper for CLI Tools' prompt() method. * * @param string $question * @param string $default * @return string|false */ private function prompt( $question, $default ) { $question .= ': '; if ( function_exists( 'readline' ) ) { return readline( $question ); } echo $question; $ret = stream_get_line( STDIN, 1024, "\n" ); if ( Utils\is_windows() && "\r" === substr( $ret, -1 ) ) { $ret = substr( $ret, 0, -1 ); } return $ret; } /** * Interactively prompt the user for input * based on defined synopsis and passed arguments. * * @param array $args * @param array $assoc_args * @return array */ private function prompt_args( $args, $assoc_args ) { $synopsis = $this->get_synopsis(); if ( ! $synopsis ) { return [ $args, $assoc_args ]; } // To skip the already provided positional arguments, we need to count // how many we had already received. $arg_index = 0; $spec = array_filter( SynopsisParser::parse( $synopsis ), function( $spec_arg ) use ( $args, $assoc_args, &$arg_index ) { switch ( $spec_arg['type'] ) { case 'positional': // Only prompt for the positional arguments that are not // yet provided, based purely on number. return $arg_index++ >= count( $args ); case 'generic': // Always prompt for generic arguments. return true; case 'assoc': case 'flag': default: // Prompt for the specific flags that were not provided // yet, based on name. return ! isset( $assoc_args[ $spec_arg['name'] ] ); } } ); $spec = array_values( $spec ); $prompt_args = WP_CLI::get_config( 'prompt' ); if ( true !== $prompt_args ) { $prompt_args = explode( ',', $prompt_args ); } // 'positional' arguments are positional (aka zero-indexed) // so $args needs to be reset before prompting for new arguments $args = []; foreach ( $spec as $key => $spec_arg ) { // When prompting for specific arguments (e.g. --prompt=user_pass), // ignore all arguments that don't match. if ( is_array( $prompt_args ) ) { if ( 'assoc' !== $spec_arg['type'] ) { continue; } if ( ! in_array( $spec_arg['name'], $prompt_args, true ) ) { continue; } } $current_prompt = ( $key + 1 ) . '/' . count( $spec ) . ' '; $default = $spec_arg['optional'] ? '' : false; // 'generic' permits arbitrary key=value (e.g. [--=] ) if ( 'generic' === $spec_arg['type'] ) { list( $key_token, $value_token ) = explode( '=', $spec_arg['token'] ); $repeat = false; do { if ( ! $repeat ) { $key_prompt = $current_prompt . $key_token; } else { $key_prompt = str_repeat( ' ', strlen( $current_prompt ) ) . $key_token; } $key = $this->prompt( $key_prompt, $default ); if ( false === $key ) { return [ $args, $assoc_args ]; } if ( $key ) { $key_prompt_count = strlen( $key_prompt ) - strlen( $value_token ) - 1; $value_prompt = str_repeat( ' ', $key_prompt_count ) . '=' . $value_token; $value = $this->prompt( $value_prompt, $default ); if ( false === $value ) { return [ $args, $assoc_args ]; } $assoc_args[ $key ] = $value; $repeat = true; } else { $repeat = false; } } while ( $repeat ); } else { $prompt = $current_prompt . $spec_arg['token']; if ( 'flag' === $spec_arg['type'] ) { $prompt .= ' (Y/n)'; } $response = $this->prompt( $prompt, $default ); if ( false === $response ) { return [ $args, $assoc_args ]; } if ( $response ) { switch ( $spec_arg['type'] ) { case 'positional': if ( $spec_arg['repeating'] ) { $response = explode( ' ', $response ); } else { $response = [ $response ]; } $args = array_merge( $args, $response ); break; case 'assoc': $assoc_args[ $spec_arg['name'] ] = $response; break; case 'flag': if ( 'Y' === strtoupper( $response ) ) { $assoc_args[ $spec_arg['name'] ] = true; } break; } } } } return [ $args, $assoc_args ]; } /** * Validate the supplied arguments to the command. * Throws warnings or errors if arguments are missing * or invalid. * * @param array $args * @param array $assoc_args * @param array $extra_args * @return array list of invalid $assoc_args keys to unset */ private function validate_args( $args, $assoc_args, $extra_args ) { $synopsis = $this->get_synopsis(); if ( ! $synopsis ) { return [ [], $args, $assoc_args, $extra_args ]; } $validator = new SynopsisValidator( $synopsis ); $cmd_path = implode( ' ', get_path( $this ) ); foreach ( $validator->get_unknown() as $token ) { \WP_CLI::warning( sprintf( 'The `%s` command has an invalid synopsis part: %s', $cmd_path, $token ) ); } if ( ! $validator->enough_positionals( $args ) ) { $this->show_usage(); exit( 1 ); } $unknown_positionals = $validator->unknown_positionals( $args ); if ( ! empty( $unknown_positionals ) ) { \WP_CLI::error( 'Too many positional arguments: ' . implode( ' ', $unknown_positionals ) ); } $synopsis_spec = SynopsisParser::parse( $synopsis ); $i = 0; $errors = [ 'fatal' => [], 'warning' => [], ]; $mock_doc = [ $this->get_shortdesc(), '' ]; $mock_doc = array_merge( $mock_doc, explode( "\n", $this->get_longdesc() ) ); $mock_doc = '/**' . PHP_EOL . '* ' . implode( PHP_EOL . '* ', $mock_doc ) . PHP_EOL . '*/'; $docparser = new DocParser( $mock_doc ); foreach ( $synopsis_spec as $spec ) { if ( 'positional' === $spec['type'] ) { $spec_args = $docparser->get_arg_args( $spec['name'] ); if ( ! isset( $args[ $i ] ) ) { if ( isset( $spec_args['default'] ) ) { $args[ $i ] = $spec_args['default']; } } if ( isset( $spec_args['options'] ) ) { if ( ! empty( $spec['repeating'] ) ) { do { // phpcs:ignore WordPress.PHP.StrictInArray.MissingTrueStrict -- This is a loose comparison by design. if ( isset( $args[ $i ] ) && ! in_array( $args[ $i ], $spec_args['options'] ) ) { \WP_CLI::error( 'Invalid value specified for positional arg.' ); } $i++; } while ( isset( $args[ $i ] ) ); } else { // phpcs:ignore WordPress.PHP.StrictInArray.MissingTrueStrict -- This is a loose comparison by design. if ( isset( $args[ $i ] ) && ! in_array( $args[ $i ], $spec_args['options'] ) ) { \WP_CLI::error( 'Invalid value specified for positional arg.' ); } } } $i++; } elseif ( 'assoc' === $spec['type'] ) { $spec_args = $docparser->get_param_args( $spec['name'] ); if ( ! isset( $assoc_args[ $spec['name'] ] ) && ! isset( $extra_args[ $spec['name'] ] ) ) { if ( isset( $spec_args['default'] ) ) { $assoc_args[ $spec['name'] ] = $spec_args['default']; } } if ( isset( $assoc_args[ $spec['name'] ] ) && isset( $spec_args['options'] ) ) { $value = $assoc_args[ $spec['name'] ]; $options = $spec_args['options']; // phpcs:ignore WordPress.PHP.StrictInArray.MissingTrueStrict -- This is a loose comparison by design. if ( ! in_array( $value, $options ) ) { // Try whether it might be a comma-separated list of multiple values. $values = array_map( 'trim', explode( ',', $value ) ); $count = count( $values ); if ( $count > 1 && count( array_filter( $values, static function ( $value ) use ( $options ) { return in_array( $value, $options, true ); } ) ) === $count ) { continue; } $errors['fatal'][ $spec['name'] ] = "Invalid value specified for '{$spec['name']}'"; } } } } list( $returned_errors, $to_unset ) = $validator->validate_assoc( array_merge( \WP_CLI::get_config(), $extra_args, $assoc_args ) ); foreach ( [ 'fatal', 'warning' ] as $error_type ) { $errors[ $error_type ] = array_merge( $errors[ $error_type ], $returned_errors[ $error_type ] ); } if ( 'help' !== $this->name ) { foreach ( $validator->unknown_assoc( $assoc_args ) as $key ) { $suggestion = Utils\get_suggestion( $key, $this->get_parameters( $synopsis_spec ), $threshold = 2 ); $errors['fatal'][] = sprintf( 'unknown --%s parameter%s', $key, ! empty( $suggestion ) ? PHP_EOL . "Did you mean '--{$suggestion}'?" : '' ); } } if ( ! empty( $errors['fatal'] ) ) { $out = 'Parameter errors:'; foreach ( $errors['fatal'] as $key => $error ) { $out .= "\n {$error}"; $desc = $docparser->get_param_desc( $key ); if ( '' !== $desc ) { $out .= " ({$desc})"; } } \WP_CLI::error( $out ); } array_map( '\\WP_CLI::warning', $errors['warning'] ); return [ $to_unset, $args, $assoc_args, $extra_args ]; } /** * Invoke the subcommand with the supplied arguments. * Given a --prompt argument, interactively request input * from the end user. * * @param array $args * @param array $assoc_args */ public function invoke( $args, $assoc_args, $extra_args ) { static $prompted_once = false; if ( 'help' !== $this->name ) { if ( \WP_CLI::get_config( 'prompt' ) && ! $prompted_once ) { list( $_args, $assoc_args ) = $this->prompt_args( $args, $assoc_args ); $args = array_merge( $args, $_args ); $prompted_once = true; } } $extra_positionals = []; foreach ( $extra_args as $k => $v ) { if ( is_numeric( $k ) ) { if ( ! isset( $args[ $k ] ) ) { $extra_positionals[ $k ] = $v; } unset( $extra_args[ $k ] ); } } $args += $extra_positionals; list( $to_unset, $args, $assoc_args, $extra_args ) = $this->validate_args( $args, $assoc_args, $extra_args ); foreach ( $to_unset as $key ) { unset( $assoc_args[ $key ] ); } $path = get_path( $this->get_parent() ); $parent = implode( ' ', array_slice( $path, 1 ) ); $cmd = $this->name; if ( $parent ) { WP_CLI::do_hook( "before_invoke:{$parent}" ); $cmd = $parent . ' ' . $cmd; } WP_CLI::do_hook( "before_invoke:{$cmd}" ); // Check if `--prompt` arg passed or not. if ( $prompted_once ) { // Unset empty args. $actual_args = $assoc_args; foreach ( $actual_args as $key ) { if ( empty( $actual_args[ $key ] ) ) { unset( $actual_args[ $key ] ); } } WP_CLI::log( sprintf( 'wp %s %s', $cmd, ltrim( Utils\assoc_args_to_str( $actual_args ), ' ' ) ) ); } call_user_func( $this->when_invoked, $args, array_merge( $extra_args, $assoc_args ) ); if ( $parent ) { WP_CLI::do_hook( "after_invoke:{$parent}" ); } WP_CLI::do_hook( "after_invoke:{$cmd}" ); } /** * Get an array of parameter names, by merging the command-specific and the * global parameters. * * @param array $spec Optional. Specification of the current command. * * @return array Array of parameter names */ private function get_parameters( $spec = [] ) { $local_parameters = array_column( $spec, 'name' ); $global_parameters = array_column( SynopsisParser::parse( $this->get_global_params() ), 'name' ); return array_unique( array_merge( $local_parameters, $global_parameters ) ); } } abort = true; $this->reason = (string) $reason; } /** * Check whether the command addition was aborted. * * @return bool */ public function was_aborted() { return $this->abort; } /** * Get the reason as to why the addition was aborted. * * @return string */ public function get_reason() { return $this->reason; } } . */ namespace WP_CLI; /** * Doctrine inflector has static methods for inflecting text. * * The methods in these classes are from several different sources collected * across several different php projects and several different authors. The * original author names and emails are not known. * * Pluralize & Singularize implementation are borrowed from CakePHP with some modifications. * * @link www.doctrine-project.org * @since 1.0 * @author Konsta Vesterinen * @author Jonathan H. Wage */ class Inflector { /** * Plural inflector rules. * * @var array */ private static $plural = [ 'rules' => [ '/(s)tatus$/i' => '\1\2tatuses', '/(quiz)$/i' => '\1zes', '/^(ox)$/i' => '\1\2en', '/([m|l])ouse$/i' => '\1ice', '/(matr|vert|ind)(ix|ex)$/i' => '\1ices', '/(x|ch|ss|sh)$/i' => '\1es', '/([^aeiouy]|qu)y$/i' => '\1ies', '/(hive)$/i' => '\1s', '/(?:([^f])fe|([lr])f)$/i' => '\1\2ves', '/sis$/i' => 'ses', '/([ti])um$/i' => '\1a', '/(p)erson$/i' => '\1eople', '/(m)an$/i' => '\1en', '/(c)hild$/i' => '\1hildren', '/(f)oot$/i' => '\1eet', '/(buffal|her|potat|tomat|volcan)o$/i' => '\1\2oes', '/(alumn|bacill|cact|foc|fung|nucle|radi|stimul|syllab|termin|vir)us$/i' => '\1i', '/us$/i' => 'uses', '/(alias)$/i' => '\1es', '/(analys|ax|cris|test|thes)is$/i' => '\1es', '/s$/' => 's', '/^$/' => '', '/$/' => 's', ], 'uninflected' => [ '.*[nrlm]ese', '.*deer', '.*fish', '.*measles', '.*ois', '.*pox', '.*sheep', 'people', 'cookie', ], 'irregular' => [ 'atlas' => 'atlases', 'axe' => 'axes', 'beef' => 'beefs', 'brother' => 'brothers', 'cafe' => 'cafes', 'chateau' => 'chateaux', 'child' => 'children', 'cookie' => 'cookies', 'corpus' => 'corpuses', 'cow' => 'cows', 'criterion' => 'criteria', 'curriculum' => 'curricula', 'demo' => 'demos', 'domino' => 'dominoes', 'echo' => 'echoes', 'foot' => 'feet', 'fungus' => 'fungi', 'ganglion' => 'ganglions', 'genie' => 'genies', 'genus' => 'genera', 'graffito' => 'graffiti', 'hippopotamus' => 'hippopotami', 'hoof' => 'hoofs', 'human' => 'humans', 'iris' => 'irises', 'leaf' => 'leaves', 'loaf' => 'loaves', 'man' => 'men', 'medium' => 'media', 'memorandum' => 'memoranda', 'money' => 'monies', 'mongoose' => 'mongooses', 'motto' => 'mottoes', 'move' => 'moves', 'mythos' => 'mythoi', 'niche' => 'niches', 'nucleus' => 'nuclei', 'numen' => 'numina', 'occiput' => 'occiputs', 'octopus' => 'octopuses', 'opus' => 'opuses', 'ox' => 'oxen', 'penis' => 'penises', 'person' => 'people', 'plateau' => 'plateaux', 'runner-up' => 'runners-up', 'sex' => 'sexes', 'soliloquy' => 'soliloquies', 'son-in-law' => 'sons-in-law', 'syllabus' => 'syllabi', 'testis' => 'testes', 'thief' => 'thieves', 'tooth' => 'teeth', 'tornado' => 'tornadoes', 'trilby' => 'trilbys', 'turf' => 'turfs', 'volcano' => 'volcanoes', ], ]; /** * Singular inflector rules. * * @var array */ private static $singular = [ 'rules' => [ '/(s)tatuses$/i' => '\1\2tatus', '/^(.*)(menu)s$/i' => '\1\2', '/(quiz)zes$/i' => '\\1', '/(matr)ices$/i' => '\1ix', '/(vert|ind)ices$/i' => '\1ex', '/^(ox)en/i' => '\1', '/(alias)(es)*$/i' => '\1', '/(buffal|her|potat|tomat|volcan)oes$/i' => '\1o', '/(alumn|bacill|cact|foc|fung|nucle|radi|stimul|syllab|termin|viri?)i$/i' => '\1us', '/([ftw]ax)es/i' => '\1', '/(analys|ax|cris|test|thes)es$/i' => '\1is', '/(shoe|slave)s$/i' => '\1', '/(o)es$/i' => '\1', '/ouses$/' => 'ouse', '/([^a])uses$/' => '\1us', '/([m|l])ice$/i' => '\1ouse', '/(x|ch|ss|sh)es$/i' => '\1', '/(m)ovies$/i' => '\1\2ovie', '/(s)eries$/i' => '\1\2eries', '/([^aeiouy]|qu)ies$/i' => '\1y', '/([lr])ves$/i' => '\1f', '/(tive)s$/i' => '\1', '/(hive)s$/i' => '\1', '/(drive)s$/i' => '\1', '/([^fo])ves$/i' => '\1fe', '/(^analy)ses$/i' => '\1sis', '/(analy|diagno|^ba|(p)arenthe|(p)rogno|(s)ynop|(t)he)ses$/i' => '\1\2sis', '/([ti])a$/i' => '\1um', '/(p)eople$/i' => '\1\2erson', '/(m)en$/i' => '\1an', '/(c)hildren$/i' => '\1\2hild', '/(f)eet$/i' => '\1oot', '/(n)ews$/i' => '\1\2ews', '/eaus$/' => 'eau', '/^(.*us)$/' => '\\1', '/s$/i' => '', ], 'uninflected' => [ '.*[nrlm]ese', '.*deer', '.*fish', '.*measles', '.*ois', '.*pox', '.*sheep', '.*ss', ], 'irregular' => [ 'criteria' => 'criterion', 'curves' => 'curve', 'emphases' => 'emphasis', 'foes' => 'foe', 'hoaxes' => 'hoax', 'media' => 'medium', 'neuroses' => 'neurosis', 'waves' => 'wave', 'oases' => 'oasis', ], ]; /** * Words that should not be inflected. * * @var array */ private static $uninflected = [ 'Amoyese', 'bison', 'Borghese', 'bream', 'breeches', 'britches', 'buffalo', 'cantus', 'carp', 'chassis', 'clippers', 'cod', 'coitus', 'Congoese', 'contretemps', 'corps', 'debris', 'diabetes', 'djinn', 'eland', 'elk', 'equipment', 'Faroese', 'flounder', 'Foochowese', 'gallows', 'Genevese', 'Genoese', 'Gilbertese', 'graffiti', 'headquarters', 'herpes', 'hijinks', 'Hottentotese', 'information', 'innings', 'jackanapes', 'Kiplingese', 'Kongoese', 'Lucchese', 'mackerel', 'Maltese', '.*?media', 'mews', 'moose', 'mumps', 'Nankingese', 'news', 'nexus', 'Niasese', 'Pekingese', 'Piedmontese', 'pincers', 'Pistoiese', 'pliers', 'Portuguese', 'proceedings', 'rabies', 'rice', 'rhinoceros', 'salmon', 'Sarawakese', 'scissors', 'sea[- ]bass', 'series', 'Shavese', 'shears', 'siemens', 'species', 'staff', 'swine', 'testes', 'trousers', 'trout', 'tuna', 'Vermontese', 'Wenchowese', 'whiting', 'wildebeest', 'Yengeese', ]; /** * Method cache array. * * @var array */ private static $cache = []; /** * The initial state of Inflector so reset() works. * * @var array */ private static $initial_state = []; /** * Converts a word into the format for a Doctrine table name. Converts 'ModelName' to 'model_name'. * * @param string $word The word to tableize. * * @return string The tableized word. */ public static function tableize( $word ) { return strtolower( preg_replace( '~(?<=\\w)([A-Z])~', '_$1', $word ) ); } /** * Converts a word into the format for a Doctrine class name. Converts 'table_name' to 'TableName'. * * @param string $word The word to classify. * * @return string The classified word. */ public static function classify( $word ) { return str_replace( ' ', '', ucwords( strtr( $word, '_-', ' ' ) ) ); } /** * Camelizes a word. This uses the classify() method and turns the first character to lowercase. * * @param string $word The word to camelize. * * @return string The camelized word. */ public static function camelize( $word ) { return lcfirst( self::classify( $word ) ); } /** * Uppercases words with configurable delimeters between words. * * Takes a string and capitalizes all of the words, like PHP's built-in * ucwords function. This extends that behavior, however, by allowing the * word delimeters to be configured, rather than only separating on * whitespace. * * Here is an example: * * * * * @param string $string The string to operate on. * @param string $delimiters A list of word separators. * * @return string The string with all delimeter-separated words capitalized. */ public static function ucwords( $string, $delimiters = " \n\t\r\0\x0B-" ) { return preg_replace_callback( '/[^' . preg_quote( $delimiters, '/' ) . ']+/', function( $matches ) { return ucfirst( $matches[0] ); }, $string ); } /** * Clears Inflectors inflected value caches, and resets the inflection * rules to the initial values. * * @return void */ public static function reset() { if ( empty( self::$initial_state ) ) { self::$initial_state = get_class_vars( 'Inflector' ); return; } foreach ( self::$initial_state as $key => $val ) { if ( 'initial_state' !== $key ) { self::${$key} = $val; } } } /** * Adds custom inflection $rules, of either 'plural' or 'singular' $type. * * ### Usage: * * {{{ * Inflector::rules('plural', array('/^(inflect)or$/i' => '\1ables')); * Inflector::rules('plural', array( * 'rules' => array('/^(inflect)ors$/i' => '\1ables'), * 'uninflected' => array('dontinflectme'), * 'irregular' => array('red' => 'redlings') * )); * }}} * * @param string $type The type of inflection, either 'plural' or 'singular' * @param array $rules An array of rules to be added. * @param boolean $reset If true, will unset default inflections for all * new rules that are being defined in $rules. * * @return void */ public static function rules( $type, $rules, $reset = false ) { foreach ( $rules as $rule => $pattern ) { if ( ! is_array( $pattern ) ) { continue; } if ( $reset ) { self::${$type}[ $rule ] = $pattern; } else { self::${$type}[ $rule ] = ( 'uninflected' === $rule ) ? array_merge( $pattern, self::${$type}[ $rule ] ) : $pattern + self::${$type}[ $rule ]; } unset( $rules[ $rule ], self::${$type}[ 'cache' . ucfirst( $rule ) ] ); if ( isset( self::${$type}['merged'][ $rule ] ) ) { unset( self::${$type}['merged'][ $rule ] ); } if ( 'plural' === $type ) { self::$cache['pluralize'] = []; self::$cache['tableize'] = []; } elseif ( 'singular' === $type ) { self::$cache['singularize'] = []; } } self::${$type}['rules'] = $rules + self::${$type}['rules']; } /** * Returns a word in plural form. * * @param string $word The word in singular form. * * @return string The word in plural form. */ public static function pluralize( $word ) { if ( isset( self::$cache['pluralize'][ $word ] ) ) { return self::$cache['pluralize'][ $word ]; } if ( ! isset( self::$plural['merged']['irregular'] ) ) { self::$plural['merged']['irregular'] = self::$plural['irregular']; } if ( ! isset( self::$plural['merged']['uninflected'] ) ) { self::$plural['merged']['uninflected'] = array_merge( self::$plural['uninflected'], self::$uninflected ); } if ( ! isset( self::$plural['cacheUninflected'] ) || ! isset( self::$plural['cacheIrregular'] ) ) { self::$plural['cacheUninflected'] = '(?:' . implode( '|', self::$plural['merged']['uninflected'] ) . ')'; self::$plural['cacheIrregular'] = '(?:' . implode( '|', array_keys( self::$plural['merged']['irregular'] ) ) . ')'; } if ( preg_match( '/(.*)\\b(' . self::$plural['cacheIrregular'] . ')$/i', $word, $regs ) ) { self::$cache['pluralize'][ $word ] = $regs[1] . substr( $word, 0, 1 ) . substr( self::$plural['merged']['irregular'][ strtolower( $regs[2] ) ], 1 ); return self::$cache['pluralize'][ $word ]; } if ( preg_match( '/^(' . self::$plural['cacheUninflected'] . ')$/i', $word, $regs ) ) { self::$cache['pluralize'][ $word ] = $word; return $word; } foreach ( self::$plural['rules'] as $rule => $replacement ) { if ( preg_match( $rule, $word ) ) { self::$cache['pluralize'][ $word ] = preg_replace( $rule, $replacement, $word ); return self::$cache['pluralize'][ $word ]; } } } /** * Returns a word in singular form. * * @param string $word The word in plural form. * * @return string The word in singular form. */ public static function singularize( $word ) { if ( isset( self::$cache['singularize'][ $word ] ) ) { return self::$cache['singularize'][ $word ]; } if ( ! isset( self::$singular['merged']['uninflected'] ) ) { self::$singular['merged']['uninflected'] = array_merge( self::$singular['uninflected'], self::$uninflected ); } if ( ! isset( self::$singular['merged']['irregular'] ) ) { self::$singular['merged']['irregular'] = array_merge( self::$singular['irregular'], array_flip( self::$plural['irregular'] ) ); } if ( ! isset( self::$singular['cacheUninflected'] ) || ! isset( self::$singular['cacheIrregular'] ) ) { self::$singular['cacheUninflected'] = '(?:' . join( '|', self::$singular['merged']['uninflected'] ) . ')'; self::$singular['cacheIrregular'] = '(?:' . join( '|', array_keys( self::$singular['merged']['irregular'] ) ) . ')'; } if ( preg_match( '/(.*)\\b(' . self::$singular['cacheIrregular'] . ')$/i', $word, $regs ) ) { self::$cache['singularize'][ $word ] = $regs[1] . substr( $word, 0, 1 ) . substr( self::$singular['merged']['irregular'][ strtolower( $regs[2] ) ], 1 ); return self::$cache['singularize'][ $word ]; } if ( preg_match( '/^(' . self::$singular['cacheUninflected'] . ')$/i', $word, $regs ) ) { self::$cache['singularize'][ $word ] = $word; return $word; } foreach ( self::$singular['rules'] as $rule => $replacement ) { if ( preg_match( $rule, $word ) ) { self::$cache['singularize'][ $word ] = preg_replace( $rule, $replacement, $word ); return self::$cache['singularize'][ $word ]; } } self::$cache['singularize'][ $word ] = $word; return $word; } } STDIN, 1 => [ 'pipe', 'w' ], 2 => [ 'pipe', 'w' ], ]; /** * @var bool Whether to log run time info or not. */ public static $log_run_times = false; /** * @var array Array of process run time info, keyed by process command, each a 2-element array containing run time and run count. */ public static $run_times = []; /** * @param string $command Command to execute. * @param string $cwd Directory to execute the command in. * @param array $env Environment variables to set when running the command. * * @return Process */ public static function create( $command, $cwd = null, $env = [] ) { $proc = new self(); $proc->command = $command; $proc->cwd = $cwd; $proc->env = $env; return $proc; } private function __construct() {} /** * Run the command. * * @return ProcessRun */ public function run() { $start_time = microtime( true ); $pipes = []; $proc = Utils\proc_open_compat( $this->command, self::$descriptors, $pipes, $this->cwd, $this->env ); $stdout = stream_get_contents( $pipes[1] ); fclose( $pipes[1] ); $stderr = stream_get_contents( $pipes[2] ); fclose( $pipes[2] ); $return_code = proc_close( $proc ); $run_time = microtime( true ) - $start_time; if ( self::$log_run_times ) { if ( ! isset( self::$run_times[ $this->command ] ) ) { self::$run_times[ $this->command ] = [ 0, 0 ]; } self::$run_times[ $this->command ][0] += $run_time; self::$run_times[ $this->command ][1]++; } return new ProcessRun( [ 'stdout' => $stdout, 'stderr' => $stderr, 'return_code' => $return_code, 'command' => $this->command, 'cwd' => $this->cwd, 'env' => $this->env, 'run_time' => $run_time, ] ); } /** * Run the command, but throw an Exception on error. * * @return ProcessRun */ public function run_check() { $r = $this->run(); if ( $r->return_code ) { throw new RuntimeException( $r ); } return $r; } /** * Run the command, but throw an Exception on error. * Same as `run_check()` above, but checks the correct stderr. * * @return ProcessRun */ public function run_check_stderr() { $r = $this->run(); if ( $r->return_code || ! empty( $r->stderr ) ) { throw new RuntimeException( $r ); } return $r; } } * Jordi Boggiano */ namespace WP_CLI; use DateTime; use Exception; use Symfony\Component\Finder\Finder; use WP_CLI; /** * Reads/writes to a filesystem cache */ class FileCache { /** * @var string cache path */ protected $root; /** * @var bool */ protected $enabled = true; /** * @var int files time to live */ protected $ttl; /** * @var int max total size */ protected $max_size; /** * @var string key allowed chars (regex class) */ protected $whitelist; /** * @param string $cache_dir location of the cache * @param int $ttl cache files default time to live (expiration) * @param int $max_size max total cache size * @param string $whitelist List of characters that are allowed in path names (used in a regex character class) */ public function __construct( $cache_dir, $ttl, $max_size, $whitelist = 'a-z0-9._-' ) { $this->root = Utils\trailingslashit( $cache_dir ); $this->ttl = (int) $ttl; $this->max_size = (int) $max_size; $this->whitelist = $whitelist; if ( ! $this->ensure_dir_exists( $this->root ) ) { $this->enabled = false; } } /** * Cache is enabled * * @return bool */ public function is_enabled() { return $this->enabled; } /** * Cache root * * @return string */ public function get_root() { return $this->root; } /** * Check if a file is in cache and return its filename * * @param string $key cache key * @param int $ttl time to live * @return bool|string filename or false */ public function has( $key, $ttl = null ) { if ( ! $this->enabled ) { return false; } $filename = $this->filename( $key ); if ( ! file_exists( $filename ) ) { return false; } // Use ttl param or global ttl. if ( null === $ttl ) { $ttl = $this->ttl; } elseif ( $this->ttl > 0 ) { $ttl = min( (int) $ttl, $this->ttl ); } else { $ttl = (int) $ttl; } // if ( $ttl > 0 && ( filemtime( $filename ) + $ttl ) < time() ) { if ( $this->ttl > 0 && $ttl >= $this->ttl ) { unlink( $filename ); } return false; } return $filename; } /** * Write to cache file * * @param string $key cache key * @param string $contents file contents * @return bool */ public function write( $key, $contents ) { $filename = $this->prepare_write( $key ); if ( $filename ) { return file_put_contents( $filename, $contents ) && touch( $filename ); } return false; } /** * Read from cache file * * @param string $key cache key * @param int $ttl time to live * @return bool|string file contents or false */ public function read( $key, $ttl = null ) { $filename = $this->has( $key, $ttl ); if ( $filename ) { return file_get_contents( $filename ); } return false; } /** * Copy a file into the cache * * @param string $key cache key * @param string $source source filename * @return bool */ public function import( $key, $source ) { $filename = $this->prepare_write( $key ); if ( $filename ) { return copy( $source, $filename ) && touch( $filename ); } return false; } /** * Copy a file out of the cache * * @param string $key cache key * @param string $target target filename * @param int $ttl time to live * @return bool */ public function export( $key, $target, $ttl = null ) { $filename = $this->has( $key, $ttl ); if ( $filename && $this->ensure_dir_exists( dirname( $target ) ) ) { return copy( $filename, $target ); } return false; } /** * Remove file from cache * * @param string $key cache key * @return bool */ public function remove( $key ) { if ( ! $this->enabled ) { return false; } $filename = $this->filename( $key ); if ( file_exists( $filename ) ) { return unlink( $filename ); } return false; } /** * Clean cache based on time to live and max size * * @return bool */ public function clean() { if ( ! $this->enabled ) { return false; } $ttl = $this->ttl; $max_size = $this->max_size; // Unlink expired files. if ( $ttl > 0 ) { try { $expire = new DateTime(); $expire->modify( '-' . $ttl . ' seconds' ); $finder = $this->get_finder()->date( 'until ' . $expire->format( 'Y-m-d H:i:s' ) ); foreach ( $finder as $file ) { unlink( $file->getRealPath() ); } } catch ( Exception $e ) { WP_CLI::error( $e->getMessage() ); } } // Unlink older files if max cache size is exceeded. if ( $max_size > 0 ) { $files = array_reverse( iterator_to_array( $this->get_finder()->sortByAccessedTime()->getIterator() ) ); $total = 0; foreach ( $files as $file ) { if ( ( $total + $file->getSize() ) <= $max_size ) { $total += $file->getSize(); } else { unlink( $file->getRealPath() ); } } } return true; } /** * Remove all cached files. * * @return bool */ public function clear() { if ( ! $this->enabled ) { return false; } $finder = $this->get_finder(); foreach ( $finder as $file ) { unlink( $file->getRealPath() ); } return true; } /** * Remove all cached files except for the newest version of one. * * @return bool */ public function prune() { if ( ! $this->enabled ) { return false; } /** @var Finder $finder */ $finder = $this->get_finder()->sortByName(); $files_to_delete = []; foreach ( $finder as $file ) { $pieces = explode( '-', $file->getBasename( $file->getExtension() ) ); $timestamp = end( $pieces ); // No way to compare versions, do nothing. if ( ! is_numeric( $timestamp ) ) { continue; } $basename_without_timestamp = str_replace( '-' . $timestamp, '', $file->getBasename() ); // There's a file with an older timestamp, delete it. if ( isset( $files_to_delete[ $basename_without_timestamp ] ) ) { unlink( $files_to_delete[ $basename_without_timestamp ] ); } $files_to_delete[ $basename_without_timestamp ] = $file->getRealPath(); } return true; } /** * Ensure directory exists * * @param string $dir directory * @return bool */ protected function ensure_dir_exists( $dir ) { if ( ! is_dir( $dir ) ) { // Disable the cache if a null device like /dev/null is being used. if ( preg_match( '{(^|[\\\\/])(\$null|nul|NUL|/dev/null)([\\\\/]|$)}', $dir ) ) { return false; } if ( ! @mkdir( $dir, 0777, true ) ) { $message = "Failed to create directory '{$dir}'"; $error = error_get_last(); if ( is_array( $error ) && array_key_exists( 'message', $error ) ) { $message .= ": {$error['message']}"; } WP_CLI::warning( "{$message}." ); return false; } } return true; } /** * Prepare cache write * * @param string $key cache key * @return bool|string filename or false */ protected function prepare_write( $key ) { if ( ! $this->enabled ) { return false; } $filename = $this->filename( $key ); if ( ! $this->ensure_dir_exists( dirname( $filename ) ) ) { return false; } return $filename; } /** * Validate cache key * * @param string $key cache key * @return string relative filename */ protected function validate_key( $key ) { $url_parts = Utils\parse_url( $key, -1, false ); if ( array_key_exists( 'path', $url_parts ) && ! empty( $url_parts['scheme'] ) ) { // is url $parts = [ 'misc' ]; $parts[] = $url_parts['scheme'] . ( empty( $url_parts['host'] ) ? '' : '-' . $url_parts['host'] ) . ( empty( $url_parts['port'] ) ? '' : '-' . $url_parts['port'] ); $parts[] = substr( $url_parts['path'], 1 ) . ( empty( $url_parts['query'] ) ? '' : '-' . $url_parts['query'] ); } else { $key = str_replace( '\\', '/', $key ); $parts = explode( '/', ltrim( $key ) ); } $parts = preg_replace( "#[^{$this->whitelist}]#i", '-', $parts ); return implode( '/', $parts ); } /** * Filename from key * * @param string $key * @return string filename */ protected function filename( $key ) { return $this->root . $this->validate_key( $key ); } /** * Get a Finder that iterates in cache root only the files * * @return Finder */ protected function get_finder() { return Finder::create()->in( $this->root )->files(); } } ]+)>#', '$1$2', $messages ); foreach ( $messages as $message ) { // phpcs:ignore WordPress.WP.AlternativeFunctions.strip_tags_strip_tags WP_CLI::log( strip_tags( trim( $message ) ) ); } } } get_col( "SHOW TABLES LIKE '%_options'" ); $found_prefixes = []; if ( count( $tables ) ) { foreach ( $tables as $table ) { $maybe_prefix = substr( $table, 0, - strlen( 'options' ) ); if ( $maybe_prefix !== $table_prefix ) { $found_prefixes[] = $maybe_prefix; } } } if ( count( $found_prefixes ) ) { $prefix_list = implode( ', ', $found_prefixes ); $install_label = count( $found_prefixes ) > 1 ? 'installations' : 'installation'; WP_CLI::error( "The site you have requested is not installed.\n" . "Your table prefix is '{$table_prefix}'. Found {$install_label} with table prefix: {$prefix_list}.\n" . 'Or, run `wp core install` to create database tables.' ); } else { WP_CLI::error( "The site you have requested is not installed.\n" . 'Run `wp core install` to create database tables.' ); } } } // phpcs:disable WordPress.PHP.IniSet -- Intentional & correct usage. function wp_debug_mode() { if ( WP_CLI::get_config( 'debug' ) ) { if ( ! defined( 'WP_DEBUG' ) ) { define( 'WP_DEBUG', true ); } error_reporting( E_ALL & ~E_DEPRECATED & ~E_STRICT ); } else { if ( WP_DEBUG ) { error_reporting( E_ALL ); if ( WP_DEBUG_DISPLAY ) { ini_set( 'display_errors', 1 ); } elseif ( null !== WP_DEBUG_DISPLAY ) { ini_set( 'display_errors', 0 ); } if ( in_array( strtolower( (string) WP_DEBUG_LOG ), [ 'true', '1' ], true ) ) { $log_path = WP_CONTENT_DIR . '/debug.log'; } elseif ( is_string( WP_DEBUG_LOG ) ) { $log_path = WP_DEBUG_LOG; } else { $log_path = false; } if ( false !== $log_path ) { ini_set( 'log_errors', 1 ); ini_set( 'error_log', $log_path ); } } else { error_reporting( E_CORE_ERROR | E_CORE_WARNING | E_COMPILE_ERROR | E_ERROR | E_WARNING | E_PARSE | E_USER_ERROR | E_USER_WARNING | E_RECOVERABLE_ERROR ); } if ( defined( 'XMLRPC_REQUEST' ) || defined( 'REST_REQUEST' ) || ( defined( 'DOING_AJAX' ) && DOING_AJAX ) ) { ini_set( 'display_errors', 0 ); } } // XDebug already sends errors to STDERR. ini_set( 'display_errors', function_exists( 'xdebug_debug_zval' ) ? false : 'STDERR' ); } // phpcs:enable function replace_wp_die_handler() { \remove_filter( 'wp_die_handler', '_default_wp_die_handler' ); \add_filter( 'wp_die_handler', function() { return __NAMESPACE__ . '\\wp_die_handler'; } ); } function wp_die_handler( $message ) { if ( $message instanceof \WP_Error ) { $message = $message->get_error_message(); } $message = wp_clean_error_message( $message ); WP_CLI::error( $message ); } /** * Clean HTML error message so suitable for text display. */ function wp_clean_error_message( $message ) { $original_message = trim( $message ); $message = $original_message; if ( preg_match( '|^\

(.+?)

|', $original_message, $matches ) ) { $message = $matches[1] . '.'; } if ( preg_match( '|\

(.+?)

|', $original_message, $matches ) ) { $message .= ' ' . $matches[1]; } $search_replace = [ '' => '`', '' => '`', ]; $message = str_replace( array_keys( $search_replace ), array_values( $search_replace ), $message ); $message = namespace\strip_tags( $message ); $message = html_entity_decode( $message, ENT_COMPAT, 'UTF-8' ); return $message; } function wp_redirect_handler( $url ) { WP_CLI::warning( 'Some code is trying to do a URL redirect. Backtrace:' ); ob_start(); debug_print_backtrace(); fwrite( STDERR, ob_get_clean() ); return $url; } function maybe_require( $since, $path ) { if ( wp_version_compare( $since, '>=' ) ) { require $path; } } function get_upgrader( $class, $insecure = false ) { if ( ! class_exists( '\WP_Upgrader' ) ) { require_once ABSPATH . 'wp-admin/includes/class-wp-upgrader.php'; } $uses_insecure_flag = false; $reflection = new ReflectionClass( $class ); if ( $reflection ) { $constructor = $reflection->getConstructor(); if ( $constructor ) { $arguments = $constructor->getParameters(); /** @var ReflectionParameter $argument */ foreach ( $arguments as $argument ) { if ( 'insecure' === $argument->name ) { $uses_insecure_flag = true; break; } } } } if ( $uses_insecure_flag ) { return new $class( new UpgraderSkin(), $insecure ); } else { return new $class( new UpgraderSkin() ); } } /** * Converts a plugin basename back into a friendly slug. */ function get_plugin_name( $basename ) { if ( false === strpos( $basename, '/' ) ) { $name = basename( $basename, '.php' ); } else { $name = dirname( $basename ); } return $name; } function is_plugin_skipped( $file ) { $name = get_plugin_name( str_replace( WP_PLUGIN_DIR . '/', '', $file ) ); $skipped_plugins = WP_CLI::get_runner()->config['skip-plugins']; if ( true === $skipped_plugins ) { return true; } if ( ! is_array( $skipped_plugins ) ) { $skipped_plugins = explode( ',', $skipped_plugins ); } return in_array( $name, array_filter( $skipped_plugins ), true ); } function get_theme_name( $path ) { return basename( $path ); } function is_theme_skipped( $path ) { $name = get_theme_name( $path ); $skipped_themes = WP_CLI::get_runner()->config['skip-themes']; if ( true === $skipped_themes ) { return true; } if ( ! is_array( $skipped_themes ) ) { $skipped_themes = explode( ',', $skipped_themes ); } return in_array( $name, array_filter( $skipped_themes ), true ); } /** * Register the sidebar for unused widgets. * Core does this in /wp-admin/widgets.php, which isn't helpful. */ function wp_register_unused_sidebar() { register_sidebar( [ 'name' => __( 'Inactive Widgets' ), 'id' => 'wp_inactive_widgets', 'class' => 'inactive-sidebar', 'description' => __( 'Drag widgets here to remove them from the sidebar but keep their settings.' ), 'before_widget' => '', 'after_widget' => '', 'before_title' => '', 'after_title' => '', ] ); } /** * Attempts to determine which object cache is being used. * * Note that the guesses made by this function are based on the WP_Object_Cache classes * that define the 3rd party object cache extension. Changes to those classes could render * problems with this function's ability to determine which object cache is being used. * * @return string */ function wp_get_cache_type() { global $_wp_using_ext_object_cache, $wp_object_cache; if ( ! empty( $_wp_using_ext_object_cache ) ) { // Test for Memcached PECL extension memcached object cache (https://github.com/tollmanz/wordpress-memcached-backend) if ( isset( $wp_object_cache->m ) && $wp_object_cache->m instanceof \Memcached ) { $message = 'Memcached'; // Test for Memcache PECL extension memcached object cache (https://wordpress.org/extend/plugins/memcached/) } elseif ( isset( $wp_object_cache->mc ) ) { $is_memcache = true; foreach ( $wp_object_cache->mc as $bucket ) { if ( ! $bucket instanceof \Memcache && ! $bucket instanceof \Memcached ) { $is_memcache = false; } } if ( $is_memcache ) { $message = 'Memcache'; } // Test for Xcache object cache (https://plugins.svn.wordpress.org/xcache/trunk/object-cache.php) } elseif ( $wp_object_cache instanceof \XCache_Object_Cache ) { $message = 'Xcache'; // Test for WinCache object cache (https://wordpress.org/extend/plugins/wincache-object-cache-backend/) } elseif ( class_exists( 'WinCache_Object_Cache' ) ) { $message = 'WinCache'; // Test for APC object cache (https://wordpress.org/extend/plugins/apc/) } elseif ( class_exists( 'APC_Object_Cache' ) ) { $message = 'APC'; // Test for WP Redis (https://wordpress.org/plugins/wp-redis/) } elseif ( isset( $wp_object_cache->redis ) && $wp_object_cache->redis instanceof \Redis ) { $message = 'Redis'; // Test for Redis Object Cache (https://wordpress.org/plugins/redis-cache/) } elseif ( method_exists( $wp_object_cache, 'redis_instance' ) && method_exists( $wp_object_cache, 'redis_status' ) ) { $message = 'Redis'; // Test for WP LCache Object cache (https://github.com/lcache/wp-lcache) } elseif ( isset( $wp_object_cache->lcache ) && $wp_object_cache->lcache instanceof \LCache\Integrated ) { $message = 'WP LCache'; } elseif ( function_exists( 'w3_instance' ) ) { $config = w3_instance( 'W3_Config' ); $message = 'Unknown'; if ( $config->get_boolean( 'objectcache.enabled' ) ) { $message = 'W3TC ' . $config->get_string( 'objectcache.engine' ); } } else { $message = 'Unknown'; } } else { $message = 'Default'; } return $message; } /** * Clear WordPress internal object caches. * * In long-running scripts, the internal caches on `$wp_object_cache` and `$wpdb` * can grow to consume gigabytes of memory. Periodically calling this utility * can help with memory management. * * @access public * @category System * @deprecated 1.5.0 */ function wp_clear_object_cache() { global $wpdb, $wp_object_cache; $wpdb->queries = []; if ( ! is_object( $wp_object_cache ) ) { return; } // The following are Memcached (Redux) plugin specific (see https://core.trac.wordpress.org/ticket/31463). if ( isset( $wp_object_cache->group_ops ) ) { $wp_object_cache->group_ops = []; } if ( isset( $wp_object_cache->stats ) ) { $wp_object_cache->stats = []; } if ( isset( $wp_object_cache->memcache_debug ) ) { $wp_object_cache->memcache_debug = []; } // Used by `WP_Object_Cache` also. if ( isset( $wp_object_cache->cache ) ) { $wp_object_cache->cache = []; } } /** * Get a set of tables in the database. * * Interprets common command-line options into a resolved set of table names. * * @param array $args Provided table names, or tables with wildcards. * @param array $assoc_args Optional flags for groups of tables (e.g. --network) * @return array */ function wp_get_table_names( $args, $assoc_args = [] ) { global $wpdb; $tables = []; // Abort if incompatible args supplied. if ( get_flag_value( $assoc_args, 'base-tables-only' ) && get_flag_value( $assoc_args, 'views-only' ) ) { WP_CLI::error( 'You cannot supply --base-tables-only and --views-only at the same time.' ); } // Pre-load tables SQL query with Views restriction if needed. if ( get_flag_value( $assoc_args, 'base-tables-only' ) ) { $tables_sql = 'SHOW FULL TABLES WHERE Table_Type = "BASE TABLE"'; } elseif ( get_flag_value( $assoc_args, 'views-only' ) ) { $tables_sql = 'SHOW FULL TABLES WHERE Table_Type = "VIEW"'; } if ( get_flag_value( $assoc_args, 'all-tables' ) ) { if ( empty( $tables_sql ) ) { $tables_sql = 'SHOW TABLES'; } // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- Query is safe, see above. $tables = $wpdb->get_col( $tables_sql, 0 ); } elseif ( get_flag_value( $assoc_args, 'all-tables-with-prefix' ) ) { if ( empty( $tables_sql ) ) { $tables_sql = $wpdb->prepare( 'SHOW TABLES LIKE %s', esc_like( $wpdb->get_blog_prefix() ) . '%' ); } else { $tables_sql .= sprintf( " AND %s LIKE '%s'", esc_sql_ident( 'Tables_in_' . $wpdb->dbname ), esc_like( $wpdb->get_blog_prefix() ) . '%' ); } // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- Query is prepared, see above. $tables = $wpdb->get_col( $tables_sql, 0 ); } else { $scope = get_flag_value( $assoc_args, 'scope', 'all' ); // Note: BC change 1.5.0, taking scope into consideration for network also. if ( get_flag_value( $assoc_args, 'network' ) && is_multisite() ) { $network_global_scope = in_array( $scope, [ 'all', 'global', 'ms_global' ], true ) ? ( 'all' === $scope ? 'global' : $scope ) : ''; $wp_tables = array_values( $wpdb->tables( $network_global_scope ) ); if ( in_array( $scope, [ 'all', 'blog' ], true ) ) { // Do directly for compat with old WP versions. Note: private, deleted, archived sites are not excluded. $blog_ids = $wpdb->get_col( "SELECT blog_id FROM $wpdb->blogs WHERE site_id = $wpdb->siteid" ); foreach ( $blog_ids as $blog_id ) { $wp_tables = array_merge( $wp_tables, array_values( $wpdb->tables( 'blog', true /*prefix*/, $blog_id ) ) ); } } } else { $wp_tables = array_values( $wpdb->tables( $scope ) ); } if ( ! global_terms_enabled() ) { // Only include sitecategories when it's actually enabled. $wp_tables = array_values( array_diff( $wp_tables, [ $wpdb->sitecategories ] ) ); } // Note: BC change 1.5.0, tables are sorted (via TABLES view). // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- uses esc_sql_ident() and $wpdb->_escape(). $tables = $wpdb->get_col( sprintf( "SHOW TABLES WHERE %s IN ('%s')", esc_sql_ident( 'Tables_in_' . $wpdb->dbname ), implode( "', '", $wpdb->_escape( $wp_tables ) ) ) ); if ( get_flag_value( $assoc_args, 'base-tables-only' ) || get_flag_value( $assoc_args, 'views-only' ) ) { // Apply Views restriction args if needed. // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- Query is prepared, see above. $views_query_tables = $wpdb->get_col( $tables_sql, 0 ); $tables = array_intersect( $tables, $views_query_tables ); } } // Filter by `$args`. if ( $args ) { $args_tables = []; foreach ( $args as $arg ) { if ( false !== strpos( $arg, '*' ) || false !== strpos( $arg, '?' ) ) { $args_tables = array_merge( $args_tables, array_filter( $tables, function ( $v ) use ( $arg ) { return fnmatch( $arg, $v ); } ) ); } else { $args_tables[] = $arg; } } $args_tables = array_values( array_unique( $args_tables ) ); $tables = array_values( array_intersect( $tables, $args_tables ) ); if ( empty( $tables ) ) { WP_CLI::error( sprintf( "Couldn't find any tables matching: %s", implode( ' ', $args ) ) ); } } return $tables; } /** * Failsafe use of the WordPress wp_strip_all_tags() function. * * Automatically falls back to strip_tags() function if the WP function is not * available. * * @param string $string String to strip the tags from. * @return string String devoid of tags. */ function strip_tags( $string ) { if ( function_exists( 'wp_strip_all_tags' ) ) { return \wp_strip_all_tags( $string ); } $string = preg_replace( '@<(script|style)[^>]*?>.*?@si', '', $string ); // phpcs:ignore WordPress.WP.AlternativeFunctions.strip_tags_strip_tags -- Fallback. $string = \strip_tags( $string ); return trim( $string ); } = 8 && ! function_exists( 'ini_set' ) ) { function ini_set( $option, $value ) { return false; } } error ) ) { wp_die( $wpdb->error ); } // Set the database table prefix and the format specifiers for database table columns. $GLOBALS['table_prefix'] = $table_prefix; wp_set_wpdb_vars(); // Start the WordPress object cache, or an external object cache if the drop-in is present. wp_start_object_cache(); // Attach the default filters. require ABSPATH . WPINC . '/default-filters.php'; // Initialize multisite if enabled. if ( is_multisite() ) { Utils\maybe_require( '4.6-alpha-37575', ABSPATH . WPINC . '/class-wp-site-query.php' ); Utils\maybe_require( '4.6-alpha-37896', ABSPATH . WPINC . '/class-wp-network-query.php' ); require ABSPATH . WPINC . '/ms-blogs.php'; require ABSPATH . WPINC . '/ms-settings.php'; } elseif ( ! defined( 'MULTISITE' ) ) { define( 'MULTISITE', false ); } register_shutdown_function( 'shutdown_action_hook' ); // Stop most of WordPress from being loaded if we just want the basics. if ( SHORTINIT ) { return false; } // Load the L10n library. require_once ABSPATH . WPINC . '/l10n.php'; // WP-CLI: Permit Utils\wp_not_installed() to run on < WP 4.0 apply_filters( 'nocache_headers', [] ); // Run the installer if WordPress is not installed. wp_not_installed(); // Load most of WordPress. require ABSPATH . WPINC . '/class-wp-walker.php'; require ABSPATH . WPINC . '/class-wp-ajax-response.php'; require ABSPATH . WPINC . '/formatting.php'; require ABSPATH . WPINC . '/capabilities.php'; Utils\maybe_require( '4.4-beta4-35719', ABSPATH . WPINC . '/class-wp-roles.php' ); Utils\maybe_require( '4.4-beta4-35719', ABSPATH . WPINC . '/class-wp-role.php' ); Utils\maybe_require( '4.4-beta4-35719', ABSPATH . WPINC . '/class-wp-user.php' ); require ABSPATH . WPINC . '/query.php'; Utils\maybe_require( '3.7-alpha-25139', ABSPATH . WPINC . '/date.php' ); require ABSPATH . WPINC . '/theme.php'; require ABSPATH . WPINC . '/class-wp-theme.php'; require ABSPATH . WPINC . '/template.php'; require ABSPATH . WPINC . '/user.php'; Utils\maybe_require( '4.4-beta4-35719', ABSPATH . WPINC . '/class-wp-user-query.php' ); Utils\maybe_require( '4.0', ABSPATH . WPINC . '/session.php' ); require ABSPATH . WPINC . '/meta.php'; Utils\maybe_require( '4.4-beta4-35719', ABSPATH . WPINC . '/class-wp-meta-query.php' ); Utils\maybe_require( '4.5-alpha-35776', ABSPATH . WPINC . '/class-wp-metadata-lazyloader.php' ); require ABSPATH . WPINC . '/general-template.php'; require ABSPATH . WPINC . '/link-template.php'; require ABSPATH . WPINC . '/author-template.php'; require ABSPATH . WPINC . '/post.php'; Utils\maybe_require( '4.4-beta4-35719', ABSPATH . WPINC . '/class-walker-page.php' ); Utils\maybe_require( '4.4-beta4-35719', ABSPATH . WPINC . '/class-walker-page-dropdown.php' ); Utils\maybe_require( '4.6-alpha-37890', ABSPATH . WPINC . '/class-wp-post-type.php' ); Utils\maybe_require( '4.4-beta4-35719', ABSPATH . WPINC . '/class-wp-post.php' ); require ABSPATH . WPINC . '/post-template.php'; Utils\maybe_require( '3.6-alpha-23451', ABSPATH . WPINC . '/revision.php' ); Utils\maybe_require( '3.6-alpha-23451', ABSPATH . WPINC . '/post-formats.php' ); require ABSPATH . WPINC . '/post-thumbnail-template.php'; require ABSPATH . WPINC . '/category.php'; Utils\maybe_require( '4.4-beta4-35719', ABSPATH . WPINC . '/class-walker-category.php' ); Utils\maybe_require( '4.4-beta4-35719', ABSPATH . WPINC . '/class-walker-category-dropdown.php' ); require ABSPATH . WPINC . '/category-template.php'; require ABSPATH . WPINC . '/comment.php'; Utils\maybe_require( '4.4-beta4-35719', ABSPATH . WPINC . '/class-wp-comment.php' ); Utils\maybe_require( '4.4-beta4-35719', ABSPATH . WPINC . '/class-wp-comment-query.php' ); Utils\maybe_require( '4.4-beta4-35719', ABSPATH . WPINC . '/class-walker-comment.php' ); require ABSPATH . WPINC . '/comment-template.php'; require ABSPATH . WPINC . '/rewrite.php'; Utils\maybe_require( '4.4-beta4-35719', ABSPATH . WPINC . '/class-wp-rewrite.php' ); require ABSPATH . WPINC . '/feed.php'; require ABSPATH . WPINC . '/bookmark.php'; require ABSPATH . WPINC . '/bookmark-template.php'; require ABSPATH . WPINC . '/kses.php'; require ABSPATH . WPINC . '/cron.php'; require ABSPATH . WPINC . '/deprecated.php'; require ABSPATH . WPINC . '/script-loader.php'; require ABSPATH . WPINC . '/taxonomy.php'; Utils\maybe_require( '4.4-beta4-35719', ABSPATH . WPINC . '/class-wp-term.php' ); Utils\maybe_require( '4.6-alpha-37575', ABSPATH . WPINC . '/class-wp-term-query.php' ); Utils\maybe_require( '4.4-beta4-35719', ABSPATH . WPINC . '/class-wp-tax-query.php' ); require ABSPATH . WPINC . '/update.php'; require ABSPATH . WPINC . '/canonical.php'; require ABSPATH . WPINC . '/shortcodes.php'; Utils\maybe_require( '4.4-beta4-35719', ABSPATH . WPINC . '/embed.php' ); require ABSPATH . WPINC . '/class-wp-embed.php'; require ABSPATH . WPINC . '/media.php'; Utils\maybe_require( '4.4-alpha-34903', ABSPATH . WPINC . '/class-wp-oembed-controller.php' ); require ABSPATH . WPINC . '/http.php'; require_once ABSPATH . WPINC . '/class-http.php'; Utils\maybe_require( '4.4-beta4-35719', ABSPATH . WPINC . '/class-wp-http-streams.php' ); Utils\maybe_require( '4.4-beta4-35719', ABSPATH . WPINC . '/class-wp-http-curl.php' ); Utils\maybe_require( '4.4-beta4-35719', ABSPATH . WPINC . '/class-wp-http-proxy.php' ); Utils\maybe_require( '4.4-beta4-35719', ABSPATH . WPINC . '/class-wp-http-cookie.php' ); Utils\maybe_require( '4.4-beta4-35719', ABSPATH . WPINC . '/class-wp-http-encoding.php' ); Utils\maybe_require( '4.4-beta4-35719', ABSPATH . WPINC . '/class-wp-http-response.php' ); Utils\maybe_require( '4.6-alpha-37438', ABSPATH . WPINC . '/class-wp-http-requests-response.php' ); require ABSPATH . WPINC . '/widgets.php'; Utils\maybe_require( '4.4-beta4-35719', ABSPATH . WPINC . '/class-wp-widget.php' ); Utils\maybe_require( '4.4-beta4-35719', ABSPATH . WPINC . '/class-wp-widget-factory.php' ); require ABSPATH . WPINC . '/nav-menu.php'; require ABSPATH . WPINC . '/nav-menu-template.php'; require ABSPATH . WPINC . '/admin-bar.php'; Utils\maybe_require( '4.4-alpha-34928', ABSPATH . WPINC . '/rest-api.php' ); Utils\maybe_require( '4.4-beta4-35719', ABSPATH . WPINC . '/rest-api/class-wp-rest-server.php' ); Utils\maybe_require( '4.4-beta4-35719', ABSPATH . WPINC . '/rest-api/class-wp-rest-response.php' ); Utils\maybe_require( '4.4-beta4-35719', ABSPATH . WPINC . '/rest-api/class-wp-rest-request.php' ); // Load multisite-specific files. if ( is_multisite() ) { require ABSPATH . WPINC . '/ms-functions.php'; require ABSPATH . WPINC . '/ms-default-filters.php'; require ABSPATH . WPINC . '/ms-deprecated.php'; } // Define constants that rely on the API to obtain the default value. // Define must-use plugin directory constants, which may be overridden in the sunrise.php drop-in. wp_plugin_directory_constants(); $symlinked_plugins_supported = function_exists( 'wp_register_plugin_realpath' ); if ( $symlinked_plugins_supported ) { $GLOBALS['wp_plugin_paths'] = []; } // Load must-use plugins. foreach ( wp_get_mu_plugins() as $mu_plugin ) { include_once $mu_plugin; } unset( $mu_plugin ); // Load network activated plugins. if ( is_multisite() ) { foreach ( wp_get_active_network_plugins() as $network_plugin ) { if ( $symlinked_plugins_supported ) { wp_register_plugin_realpath( $network_plugin ); } include_once $network_plugin; } unset( $network_plugin ); } do_action( 'muplugins_loaded' ); if ( is_multisite() ) { ms_cookie_constants(); } // Define constants after multisite is loaded. Cookie-related constants may be overridden in ms_network_cookies(). wp_cookie_constants(); // Define and enforce our SSL constants. wp_ssl_constants(); // Create common globals. require ABSPATH . WPINC . '/vars.php'; // Make taxonomies and posts available to plugins and themes. // @plugin authors: warning: these get registered again on the init hook. create_initial_taxonomies(); create_initial_post_types(); // Register the default theme directory root register_theme_directory( get_theme_root() ); // Load active plugins. foreach ( wp_get_active_and_valid_plugins() as $plugin ) { if ( $symlinked_plugins_supported ) { wp_register_plugin_realpath( $plugin ); } include_once $plugin; } unset( $plugin, $symlinked_plugins_supported ); // Load pluggable functions. require ABSPATH . WPINC . '/pluggable.php'; require ABSPATH . WPINC . '/pluggable-deprecated.php'; // Set internal encoding. wp_set_internal_encoding(); // Run wp_cache_postload() if object cache is enabled and the function exists. if ( WP_CACHE && function_exists( 'wp_cache_postload' ) ) { wp_cache_postload(); } do_action( 'plugins_loaded' ); // Define constants which affect functionality if not already defined. wp_functionality_constants(); if ( ! function_exists( 'get_magic_quotes_gpc' ) ) { // Provide compat fallback for newer PHP version (7.4+) on older WordPress core versions. function get_magic_quotes_gpc() { return false; } } // Add magic quotes and set up $_REQUEST ( $_GET + $_POST ) wp_magic_quotes(); do_action( 'sanitize_comment_cookies' ); /** * WordPress Query object * @global object $wp_the_query * @since 2.0.0 */ $GLOBALS['wp_the_query'] = new WP_Query(); /** * Holds the reference to @see $wp_the_query * Use this global for WordPress queries * @global object $wp_query * @since 1.5.0 */ $GLOBALS['wp_query'] = $GLOBALS['wp_the_query']; /** * Holds the WordPress Rewrite object for creating pretty URLs * @global object $wp_rewrite * @since 1.5.0 */ $GLOBALS['wp_rewrite'] = new WP_Rewrite(); /** * WordPress Object * @global object $wp * @since 2.0.0 */ $GLOBALS['wp'] = new WP(); /** * WordPress Widget Factory Object * @global object $wp_widget_factory * @since 2.8.0 */ $GLOBALS['wp_widget_factory'] = new WP_Widget_Factory(); /** * WordPress User Roles * @global object $wp_roles * @since 2.0.0 */ $GLOBALS['wp_roles'] = new WP_Roles(); do_action( 'setup_theme' ); // Define the template related constants. wp_templating_constants(); // Load the default text localization domain. load_default_textdomain(); $locale = get_locale(); $locale_file = WP_LANG_DIR . "/$locale.php"; if ( ( 0 === validate_file( $locale ) ) && is_readable( $locale_file ) ) { require $locale_file; } unset( $locale_file ); // Pull in locale data after loading text domain. require_once ABSPATH . WPINC . '/locale.php'; /** * WordPress Locale object for loading locale domain date and various strings. * @global object $wp_locale * @since 2.1.0 */ $GLOBALS['wp_locale'] = new WP_Locale(); // Load the functions for the active theme, for both parent and child theme if applicable. // phpcs:disable WordPress.WP.DiscouragedConstants.STYLESHEETPATHUsageFound,WordPress.WP.DiscouragedConstants.TEMPLATEPATHUsageFound global $pagenow; if ( ! defined( 'WP_INSTALLING' ) || 'wp-activate.php' === $pagenow ) { if ( TEMPLATEPATH !== STYLESHEETPATH && file_exists( STYLESHEETPATH . '/functions.php' ) ) { include STYLESHEETPATH . '/functions.php'; } if ( file_exists( TEMPLATEPATH . '/functions.php' ) ) { include TEMPLATEPATH . '/functions.php'; } } // phpcs:enable WordPress.WP.DiscouragedConstants do_action( 'after_setup_theme' ); // Set up current user. $GLOBALS['wp']->init(); /** * Most of WP is loaded at this stage, and the user is authenticated. WP continues * to load on the init hook that follows (e.g. widgets), and many plugins instantiate * themselves on it for all sorts of reasons (e.g. they need a user, a taxonomy, etc.). * * If you wish to plug an action once WP is loaded, use the wp_loaded hook below. */ do_action( 'init' ); // Check site status # if ( is_multisite() ) { // WP-CLI if ( is_multisite() && ! defined( 'WP_INSTALLING' ) ) { $file = ms_site_check(); if ( true !== $file ) { require $file; die(); } unset( $file ); } /** * This hook is fired once WP, all plugins, and the theme are fully loaded and instantiated. * * AJAX requests should use wp-admin/admin-ajax.php. admin-ajax.php can handle requests for * users not logged in. * * @link https://codex.wordpress.org/AJAX_in_Plugins * * @since 3.0.0 */ do_action( 'wp_loaded' ); clean(); } ); } } return $cache; } /** * Set the context in which WP-CLI should be run */ public static function set_url( $url ) { self::debug( 'Set URL: ' . $url, 'bootstrap' ); $url_parts = Utils\parse_url( $url ); self::set_url_params( $url_parts ); } private static function set_url_params( $url_parts ) { $f = function( $key ) use ( $url_parts ) { return Utils\get_flag_value( $url_parts, $key, '' ); }; if ( isset( $url_parts['host'] ) ) { if ( isset( $url_parts['scheme'] ) && 'https' === strtolower( $url_parts['scheme'] ) ) { $_SERVER['HTTPS'] = 'on'; } $_SERVER['HTTP_HOST'] = $url_parts['host']; if ( isset( $url_parts['port'] ) ) { $_SERVER['HTTP_HOST'] .= ':' . $url_parts['port']; } $_SERVER['SERVER_NAME'] = $url_parts['host']; } $_SERVER['REQUEST_URI'] = $f( 'path' ) . ( isset( $url_parts['query'] ) ? '?' . $url_parts['query'] : '' ); $_SERVER['SERVER_PORT'] = Utils\get_flag_value( $url_parts, 'port', '80' ); $_SERVER['QUERY_STRING'] = $f( 'query' ); } /** * @return WpHttpCacheManager */ public static function get_http_cache_manager() { static $http_cacher; if ( ! $http_cacher ) { $http_cacher = new WpHttpCacheManager( self::get_cache() ); } return $http_cacher; } /** * Colorize a string for output. * * Yes, you can change the color of command line text too. For instance, * here's how `WP_CLI::success()` colorizes "Success: " * * ``` * WP_CLI::colorize( "%GSuccess:%n " ) * ``` * * Uses `\cli\Colors::colorize()` to transform color tokens to display * settings. Choose from the following tokens (and note 'reset'): * * * %y => ['color' => 'yellow'], * * %g => ['color' => 'green'], * * %b => ['color' => 'blue'], * * %r => ['color' => 'red'], * * %p => ['color' => 'magenta'], * * %m => ['color' => 'magenta'], * * %c => ['color' => 'cyan'], * * %w => ['color' => 'grey'], * * %k => ['color' => 'black'], * * %n => ['color' => 'reset'], * * %Y => ['color' => 'yellow', 'style' => 'bright'], * * %G => ['color' => 'green', 'style' => 'bright'], * * %B => ['color' => 'blue', 'style' => 'bright'], * * %R => ['color' => 'red', 'style' => 'bright'], * * %P => ['color' => 'magenta', 'style' => 'bright'], * * %M => ['color' => 'magenta', 'style' => 'bright'], * * %C => ['color' => 'cyan', 'style' => 'bright'], * * %W => ['color' => 'grey', 'style' => 'bright'], * * %K => ['color' => 'black', 'style' => 'bright'], * * %N => ['color' => 'reset', 'style' => 'bright'], * * %3 => ['background' => 'yellow'], * * %2 => ['background' => 'green'], * * %4 => ['background' => 'blue'], * * %1 => ['background' => 'red'], * * %5 => ['background' => 'magenta'], * * %6 => ['background' => 'cyan'], * * %7 => ['background' => 'grey'], * * %0 => ['background' => 'black'], * * %F => ['style' => 'blink'], * * %U => ['style' => 'underline'], * * %8 => ['style' => 'inverse'], * * %9 => ['style' => 'bright'], * * %_ => ['style' => 'bright'] * * @access public * @category Output * * @param string $string String to colorize for output, with color tokens. * @return string Colorized string. */ public static function colorize( $string ) { return Colors::colorize( $string, self::get_runner()->in_color() ); } /** * Schedule a callback to be executed at a certain point. * * Hooks conceptually are very similar to WordPress actions. WP-CLI hooks * are typically called before WordPress is loaded. * * WP-CLI hooks include: * * * `before_add_command:` - Before the command is added. * * `after_add_command:` - After the command was added. * * `before_invoke:` - Just before a command is invoked. * * `after_invoke:` - Just after a command is invoked. * * `find_command_to_run_pre` - Just before WP-CLI finds the command to run. * * `before_registering_contexts` (1) - Before the contexts are registered. * * `before_wp_load` - Just before the WP load process begins. * * `before_wp_config_load` - After wp-config.php has been located. * * `after_wp_config_load` - After wp-config.php has been loaded into scope. * * `after_wp_load` - Just after the WP load process has completed. * * `before_run_command` (3) - Just before the command is executed. * * The parentheses behind the hook name denote the number of arguments * being passed into the hook. For such hooks, the callback should return * the first argument again, making them work like a WP filter. * * WP-CLI commands can create their own hooks with `WP_CLI::do_hook()`. * * If additional arguments are passed through the `WP_CLI::do_hook()` call, * these will be passed on to the callback provided by `WP_CLI::add_hook()`. * * ``` * # `wp network meta` confirms command is executing in multisite context. * WP_CLI::add_command( 'network meta', 'Network_Meta_Command', array( * 'before_invoke' => function () { * if ( !is_multisite() ) { * WP_CLI::error( 'This is not a multisite installation.' ); * } * } * ) ); * ``` * * @access public * @category Registration * * @param string $when Identifier for the hook. * @param mixed $callback Callback to execute when hook is called. * @return null */ public static function add_hook( $when, $callback ) { if ( array_key_exists( $when, self::$hooks_passed ) ) { self::debug( sprintf( 'Immediately invoking on passed hook "%s": %s', $when, Utils\describe_callable( $callback ) ), 'hooks' ); call_user_func_array( $callback, (array) self::$hooks_passed[ $when ] ); } self::$hooks[ $when ][] = $callback; } /** * Execute callbacks registered to a given hook. * * See `WP_CLI::add_hook()` for details on WP-CLI's internal hook system. * Commands can provide and call their own hooks. * * @access public * @category Registration * * @param string $when Identifier for the hook. * @param mixed ...$args Optional. Arguments that will be passed onto the * callback provided by `WP_CLI::add_hook()`. * @return null|mixed Returns the first optional argument if optional * arguments were passed, otherwise returns null. */ public static function do_hook( $when, ...$args ) { self::$hooks_passed[ $when ] = $args; $has_args = count( $args ) > 0; if ( ! isset( self::$hooks[ $when ] ) ) { if ( $has_args ) { return $args[0]; } return null; } self::debug( sprintf( 'Processing hook "%s" with %d callbacks', $when, count( self::$hooks[ $when ] ) ), 'hooks' ); foreach ( self::$hooks[ $when ] as $callback ) { self::debug( sprintf( 'On hook "%s": %s', $when, Utils\describe_callable( $callback ) ), 'hooks' ); if ( $has_args ) { $return_value = $callback( ...$args ); if ( isset( $return_value ) ) { $args[0] = $return_value; } } else { $callback(); } } if ( $has_args ) { return $args[0]; } return null; } /** * Add a callback to a WordPress action or filter. * * `add_action()` without needing access to `add_action()`. If WordPress is * already loaded though, you should use `add_action()` (and `add_filter()`) * instead. * * @access public * @category Registration * * @param string $tag Named WordPress action or filter. * @param mixed $function_to_add Callable to execute when the action or filter is evaluated. * @param integer $priority Priority to add the callback as. * @param integer $accepted_args Number of arguments to pass to callback. * @return true */ public static function add_wp_hook( $tag, $function_to_add, $priority = 10, $accepted_args = 1 ) { global $wp_filter, $merged_filters; if ( function_exists( 'add_filter' ) ) { add_filter( $tag, $function_to_add, $priority, $accepted_args ); } else { $idx = self::wp_hook_build_unique_id( $tag, $function_to_add, $priority ); // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited -- This is intentional & the purpose of this function. $wp_filter[ $tag ][ $priority ][ $idx ] = [ 'function' => $function_to_add, 'accepted_args' => $accepted_args, ]; unset( $merged_filters[ $tag ] ); } return true; } /** * Build Unique ID for storage and retrieval. * * Essentially _wp_filter_build_unique_id() without needing access to _wp_filter_build_unique_id() */ private static function wp_hook_build_unique_id( $tag, $function, $priority ) { global $wp_filter; static $filter_id_count = 0; if ( is_string( $function ) ) { return $function; } if ( is_object( $function ) ) { // Closures are currently implemented as objects $function = [ $function, '' ]; } else { $function = (array) $function; } if ( is_object( $function[0] ) ) { // Object Class Calling if ( function_exists( 'spl_object_hash' ) ) { return spl_object_hash( $function[0] ) . $function[1]; } $obj_idx = get_class( $function[0] ) . $function[1]; if ( ! isset( $function[0]->wp_filter_id ) ) { if ( false === $priority ) { return false; } $obj_idx .= isset( $wp_filter[ $tag ][ $priority ] ) ? count( (array) $wp_filter[ $tag ][ $priority ] ) : $filter_id_count; $function[0]->wp_filter_id = $filter_id_count; ++$filter_id_count; } else { $obj_idx .= $function[0]->wp_filter_id; } return $obj_idx; } if ( is_string( $function[0] ) ) { // Static Calling return $function[0] . '::' . $function[1]; } } /** * Register a command to WP-CLI. * * WP-CLI supports using any callable class, function, or closure as a * command. `WP_CLI::add_command()` is used for both internal and * third-party command registration. * * Command arguments are parsed from PHPDoc by default, but also can be * supplied as an optional third argument during registration. * * ``` * # Register a custom 'foo' command to output a supplied positional param. * # * # $ wp foo bar --append=qux * # Success: bar qux * * /** * * My awesome closure command * * * * * * : An awesome message to display * * * * --append= * * : An awesome message to append to the original message. * * * * @when before_wp_load * *\/ * $foo = function( $args, $assoc_args ) { * WP_CLI::success( $args[0] . ' ' . $assoc_args['append'] ); * }; * WP_CLI::add_command( 'foo', $foo ); * ``` * * @access public * @category Registration * * @param string $name Name for the command (e.g. "post list" or "site empty"). * @param callable $callable Command implementation as a class, function or closure. * @param array $args { * Optional. An associative array with additional registration parameters. * * @type callable $before_invoke Callback to execute before invoking the command. * @type callable $after_invoke Callback to execute after invoking the command. * @type string $shortdesc Short description (80 char or less) for the command. * @type string $longdesc Description of arbitrary length for examples, etc. * @type string $synopsis The synopsis for the command (string or array). * @type string $when Execute callback on a named WP-CLI hook (e.g. before_wp_load). * @type bool $is_deferred Whether the command addition had already been deferred. * } * @return bool True on success, false if deferred, hard error if registration failed. */ public static function add_command( $name, $callable, $args = [] ) { // Bail immediately if the WP-CLI executable has not been run. if ( ! defined( 'WP_CLI' ) ) { return false; } $valid = false; if ( is_callable( $callable ) ) { $valid = true; } elseif ( is_string( $callable ) && class_exists( (string) $callable ) ) { $valid = true; } elseif ( is_object( $callable ) ) { $valid = true; } elseif ( Utils\is_valid_class_and_method_pair( $callable ) ) { $valid = true; } if ( ! $valid ) { if ( is_array( $callable ) ) { $callable[0] = is_object( $callable[0] ) ? get_class( $callable[0] ) : $callable[0]; $callable = [ $callable[0], $callable[1] ]; } self::error( sprintf( 'Callable %s does not exist, and cannot be registered as `wp %s`.', json_encode( $callable ), $name ) ); } $addition = new CommandAddition(); self::do_hook( "before_add_command:{$name}", $addition ); if ( $addition->was_aborted() ) { self::warning( "Aborting the addition of the command '{$name}' with reason: {$addition->get_reason()}." ); return false; } foreach ( [ 'before_invoke', 'after_invoke' ] as $when ) { if ( isset( $args[ $when ] ) ) { self::add_hook( "{$when}:{$name}", $args[ $when ] ); } } $path = preg_split( '/\s+/', $name ); $leaf_name = array_pop( $path ); $command = self::get_root_command(); while ( ! empty( $path ) ) { $subcommand_name = $path[0]; $parent = implode( ' ', $path ); $subcommand = $command->find_subcommand( $path ); // Parent not found. Defer addition or create an empty container as // needed. if ( ! $subcommand ) { if ( isset( $args['is_deferred'] ) && $args['is_deferred'] ) { $subcommand = new CompositeCommand( $command, $subcommand_name, new DocParser( '' ) ); self::debug( "Adding empty container for deferred command: {$name}", 'commands' ); $command->add_subcommand( $subcommand_name, $subcommand ); } else { self::debug( "Deferring command: {$name}", 'commands' ); self::defer_command_addition( $name, $parent, $callable, $args ); return false; } } $command = $subcommand; } $leaf_command = CommandFactory::create( $leaf_name, $callable, $command ); // Only add a command namespace if the command itself does not exist yet. if ( $leaf_command instanceof CommandNamespace && array_key_exists( $leaf_name, $command->get_subcommands() ) ) { return false; } // Reattach commands attached to namespace to real command. $subcommand_name = (array) $leaf_name; $existing_command = $command->find_subcommand( $subcommand_name ); if ( $existing_command instanceof CompositeCommand && $existing_command->can_have_subcommands() ) { if ( $leaf_command instanceof CommandNamespace || ! $leaf_command->can_have_subcommands() ) { $command_to_keep = $existing_command; } else { $command_to_keep = $leaf_command; } self::merge_sub_commands( $command_to_keep, $existing_command, $leaf_command ); } /** @var Dispatcher\Subcommand|Dispatcher\CompositeCommand|Dispatcher\CommandNamespace $leaf_command */ if ( ! $command->can_have_subcommands() ) { throw new Exception( sprintf( "'%s' can't have subcommands.", implode( ' ', get_path( $command ) ) ) ); } if ( isset( $args['shortdesc'] ) ) { $leaf_command->set_shortdesc( $args['shortdesc'] ); } if ( isset( $args['longdesc'] ) ) { $leaf_command->set_longdesc( $args['longdesc'] ); } if ( isset( $args['synopsis'] ) ) { if ( is_string( $args['synopsis'] ) ) { $leaf_command->set_synopsis( $args['synopsis'] ); } elseif ( is_array( $args['synopsis'] ) ) { $synopsis = SynopsisParser::render( $args['synopsis'] ); $leaf_command->set_synopsis( $synopsis ); $long_desc = ''; $bits = explode( ' ', $synopsis ); foreach ( $args['synopsis'] as $key => $arg ) { $long_desc .= $bits[ $key ] . "\n"; if ( ! empty( $arg['description'] ) ) { $long_desc .= ': ' . $arg['description'] . "\n"; } $yamlify = []; foreach ( [ 'default', 'options' ] as $key ) { if ( isset( $arg[ $key ] ) ) { $yamlify[ $key ] = $arg[ $key ]; } } if ( ! empty( $yamlify ) ) { $long_desc .= Spyc::YAMLDump( $yamlify ); $long_desc .= '---' . "\n"; } $long_desc .= "\n"; } if ( ! empty( $long_desc ) ) { $long_desc = rtrim( $long_desc, "\r\n" ); $long_desc = '## OPTIONS' . "\n\n" . $long_desc; if ( ! empty( $args['longdesc'] ) ) { $long_desc .= "\n\n" . ltrim( $args['longdesc'], "\r\n" ); } $leaf_command->set_longdesc( $long_desc ); } } } if ( isset( $args['when'] ) ) { self::get_runner()->register_early_invoke( $args['when'], $leaf_command ); } if ( ! empty( $parent ) ) { $sub_command = trim( str_replace( $parent, '', $name ) ); self::debug( "Adding command: {$sub_command} in {$parent} Namespace", 'commands' ); } else { self::debug( "Adding command: {$name}", 'commands' ); } $command->add_subcommand( $leaf_name, $leaf_command ); self::do_hook( "after_add_command:{$name}" ); return true; } /** * Merge the sub-commands of two commands into a single command to keep. * * @param CompositeCommand $command_to_keep Command to merge the sub commands into. This is typically one of the * two others. * @param CompositeCommand $old_command Command that was already registered. * @param CompositeCommand $new_command New command that is being added. */ private static function merge_sub_commands( CompositeCommand $command_to_keep, CompositeCommand $old_command, CompositeCommand $new_command ) { foreach ( $old_command->get_subcommands() as $subname => $subcommand ) { $command_to_keep->add_subcommand( $subname, $subcommand, false ); } foreach ( $new_command->get_subcommands() as $subname => $subcommand ) { $command_to_keep->add_subcommand( $subname, $subcommand, true ); } } /** * Defer command addition for a sub-command if the parent command is not yet * registered. * * @param string $name Name for the sub-command. * @param string $parent Name for the parent command. * @param string $callable Command implementation as a class, function or closure. * @param array $args Optional. See `WP_CLI::add_command()` for details. */ private static function defer_command_addition( $name, $parent, $callable, $args = [] ) { $args['is_deferred'] = true; self::$deferred_additions[ $name ] = [ 'parent' => $parent, 'callable' => $callable, 'args' => $args, ]; self::add_hook( "after_add_command:$parent", function () use ( $name ) { $deferred_additions = WP_CLI::get_deferred_additions(); if ( ! array_key_exists( $name, $deferred_additions ) ) { return; } $callable = $deferred_additions[ $name ]['callable']; $args = $deferred_additions[ $name ]['args']; WP_CLI::remove_deferred_addition( $name ); WP_CLI::add_command( $name, $callable, $args ); } ); } /** * Get the list of outstanding deferred command additions. * * @return array Array of outstanding command additions. */ public static function get_deferred_additions() { return self::$deferred_additions; } /** * Remove a command addition from the list of outstanding deferred additions. */ public static function remove_deferred_addition( $name ) { if ( ! array_key_exists( $name, self::$deferred_additions ) ) { self::warning( "Trying to remove a non-existent command addition '{$name}'." ); } unset( self::$deferred_additions[ $name ] ); } /** * Display informational message without prefix, and ignore `--quiet`. * * Message is written to STDOUT. `WP_CLI::log()` is typically recommended; * `WP_CLI::line()` is included for historical compat. * * @access public * @category Output * * @param string $message Message to display to the end user. * @return null */ public static function line( $message = '' ) { echo $message . "\n"; } /** * Display informational message without prefix. * * Message is written to STDOUT, or discarded when `--quiet` flag is supplied. * * ``` * # `wp cli update` lets user know of each step in the update process. * WP_CLI::log( sprintf( 'Downloading from %s...', $download_url ) ); * ``` * * @access public * @category Output * * @param string $message Message to write to STDOUT. */ public static function log( $message ) { if ( null === self::$logger ) { return; } self::$logger->info( $message ); } /** * Display success message prefixed with "Success: ". * * Success message is written to STDOUT. * * Typically recommended to inform user of successful script conclusion. * * ``` * # wp rewrite flush expects 'rewrite_rules' option to be set after flush. * flush_rewrite_rules( \WP_CLI\Utils\get_flag_value( $assoc_args, 'hard' ) ); * if ( ! get_option( 'rewrite_rules' ) ) { * WP_CLI::warning( "Rewrite rules are empty." ); * } else { * WP_CLI::success( 'Rewrite rules flushed.' ); * } * ``` * * @access public * @category Output * * @param string $message Message to write to STDOUT. * @return null */ public static function success( $message ) { if ( null === self::$logger ) { return; } self::$logger->success( $message ); } /** * Display debug message prefixed with "Debug: " when `--debug` is used. * * Debug message is written to STDERR, and includes script execution time. * * Helpful for optionally showing greater detail when needed. Used throughout * WP-CLI bootstrap process for easier debugging and profiling. * * ``` * # Called in `WP_CLI\Runner::set_wp_root()`. * private static function set_wp_root( $path ) { * define( 'ABSPATH', Utils\trailingslashit( $path ) ); * WP_CLI::debug( 'ABSPATH defined: ' . ABSPATH ); * $_SERVER['DOCUMENT_ROOT'] = realpath( $path ); * } * * # Debug details only appear when `--debug` is used. * # $ wp --debug * # [...] * # Debug: ABSPATH defined: /srv/www/wordpress-develop.dev/src/ (0.225s) * ``` * * @access public * @category Output * * @param string|WP_Error|Exception|Throwable $message Message to write to STDERR. * @param string|bool $group Organize debug message to a specific group. * Use `false` to not group the message. * @return null */ public static function debug( $message, $group = false ) { static $storage = []; if ( ! self::$logger ) { $storage[] = [ $message, $group ]; return; } if ( ! empty( $storage ) && self::$logger ) { foreach ( $storage as $entry ) { list( $stored_message, $stored_group ) = $entry; self::$logger->debug( self::error_to_string( $stored_message ), $stored_group ); } $storage = []; } self::$logger->debug( self::error_to_string( $message ), $group ); } /** * Display warning message prefixed with "Warning: ". * * Warning message is written to STDERR. * * Use instead of `WP_CLI::debug()` when script execution should be permitted * to continue. * * ``` * # `wp plugin activate` skips activation when plugin is network active. * $status = $this->get_status( $plugin->file ); * // Network-active is the highest level of activation status * if ( 'active-network' === $status ) { * WP_CLI::warning( "Plugin '{$plugin->name}' is already network active." ); * continue; * } * ``` * * @access public * @category Output * * @param string|WP_Error|Exception|Throwable $message Message to write to STDERR. * @return null */ public static function warning( $message ) { if ( null === self::$logger ) { return; } self::$logger->warning( self::error_to_string( $message ) ); } /** * Display error message prefixed with "Error: " and exit script. * * Error message is written to STDERR. Defaults to halting script execution * with return code 1. * * Use `WP_CLI::warning()` instead when script execution should be permitted * to continue. * * ``` * # `wp cache flush` considers flush failure to be a fatal error. * if ( false === wp_cache_flush() ) { * WP_CLI::error( 'The object cache could not be flushed.' ); * } * ``` * * @access public * @category Output * * @param string|WP_Error|Exception|Throwable $message Message to write to STDERR. * @param boolean|integer $exit True defaults to exit(1). * @return null */ public static function error( $message, $exit = true ) { if ( null !== self::$logger && ! isset( self::get_runner()->assoc_args['completions'] ) ) { self::$logger->error( self::error_to_string( $message ) ); } $return_code = false; if ( true === $exit ) { $return_code = 1; } elseif ( is_int( $exit ) && $exit >= 1 ) { $return_code = $exit; } if ( $return_code ) { if ( self::$capture_exit ) { throw new ExitException( null, $return_code ); } exit( $return_code ); } } /** * Halt script execution with a specific return code. * * Permits script execution to be overloaded by `WP_CLI::runcommand()` * * @access public * @category Output * * @param integer $return_code */ public static function halt( $return_code ) { if ( self::$capture_exit ) { throw new ExitException( null, $return_code ); } exit( $return_code ); } /** * Display a multi-line error message in a red box. Doesn't exit script. * * Error message is written to STDERR. * * @access public * @category Output * * @param array $message_lines Multi-line error message to be displayed. */ public static function error_multi_line( $message_lines ) { if ( null === self::$logger ) { return; } if ( ! isset( self::get_runner()->assoc_args['completions'] ) && is_array( $message_lines ) ) { self::$logger->error_multi_line( array_map( [ __CLASS__, 'error_to_string' ], $message_lines ) ); } } /** * Ask for confirmation before running a destructive operation. * * If 'y' is provided to the question, the script execution continues. If * 'n' or any other response is provided to the question, script exits. * * ``` * # `wp db drop` asks for confirmation before dropping the database. * * WP_CLI::confirm( "Are you sure you want to drop the database?", $assoc_args ); * ``` * * @access public * @category Input * * @param string $question Question to display before the prompt. * @param array $assoc_args Skips prompt if 'yes' is provided. */ public static function confirm( $question, $assoc_args = [] ) { if ( ! Utils\get_flag_value( $assoc_args, 'yes' ) ) { fwrite( STDOUT, $question . ' [y/n] ' ); $answer = strtolower( trim( fgets( STDIN ) ) ); if ( 'y' !== $answer ) { exit; } } } /** * Read value from a positional argument or from STDIN. * * @param array $args The list of positional arguments. * @param int $index At which position to check for the value. * * @return string */ public static function get_value_from_arg_or_stdin( $args, $index ) { if ( isset( $args[ $index ] ) ) { $raw_value = $args[ $index ]; } else { // We don't use file_get_contents() here because it doesn't handle // Ctrl-D properly, when typing in the value interactively. $raw_value = ''; while ( false !== ( $line = fgets( STDIN ) ) ) { $raw_value .= $line; } } return $raw_value; } /** * Read a value, from various formats. * * @access public * @category Input * * @param mixed $raw_value * @param array $assoc_args */ public static function read_value( $raw_value, $assoc_args = [] ) { if ( Utils\get_flag_value( $assoc_args, 'format' ) === 'json' ) { $value = json_decode( $raw_value, true ); if ( null === $value ) { self::error( sprintf( 'Invalid JSON: %s', $raw_value ) ); } } else { $value = $raw_value; } return $value; } /** * Display a value, in various formats * * @param mixed $value Value to display. * @param array $assoc_args Arguments passed to the command, determining format. */ public static function print_value( $value, $assoc_args = [] ) { if ( Utils\get_flag_value( $assoc_args, 'format' ) === 'json' ) { $value = json_encode( $value ); } elseif ( Utils\get_flag_value( $assoc_args, 'format' ) === 'yaml' ) { $value = Spyc::YAMLDump( $value, 2, 0 ); } elseif ( is_array( $value ) || is_object( $value ) ) { $value = var_export( $value, true ); } echo $value . "\n"; } /** * Convert a WP_Error or Exception into a string * * @param string|WP_Error|Exception|Throwable $errors * @throws InvalidArgumentException * * @return string */ public static function error_to_string( $errors ) { if ( is_string( $errors ) ) { return $errors; } // Only json_encode() the data when it needs it $render_data = function( $data ) { if ( is_array( $data ) || is_object( $data ) ) { return json_encode( $data ); } return '"' . $data . '"'; }; if ( $errors instanceof WP_Error ) { foreach ( $errors->get_error_messages() as $message ) { if ( $errors->get_error_data() ) { return $message . ' ' . $render_data( $errors->get_error_data() ); } return $message; } } // PHP 7+: internal and user exceptions must implement Throwable interface. // PHP 5: internal and user exceptions must extend Exception class. if ( interface_exists( 'Throwable' ) && ( $errors instanceof Throwable ) || ( $errors instanceof Exception ) ) { return get_class( $errors ) . ': ' . $errors->getMessage(); } throw new InvalidArgumentException( sprintf( "Unsupported argument type passed to WP_CLI::error_to_string(): '%s'", gettype( $errors ) ) ); } /** * Launch an arbitrary external process that takes over I/O. * * ``` * # `wp core download` falls back to the `tar` binary when PharData isn't available * if ( ! class_exists( 'PharData' ) ) { * $cmd = "tar xz --strip-components=1 --directory=%s -f $tarball"; * WP_CLI::launch( Utils\esc_cmd( $cmd, $dest ) ); * return; * } * ``` * * @access public * @category Execution * * @param string $command External process to launch. * @param boolean $exit_on_error Whether to exit if the command returns an elevated return code. * @param boolean $return_detailed Whether to return an exit status (default) or detailed execution results. * @return int|ProcessRun The command exit status, or a ProcessRun object for full details. */ public static function launch( $command, $exit_on_error = true, $return_detailed = false ) { Utils\check_proc_available( 'launch' ); $proc = Process::create( $command ); $results = $proc->run(); if ( -1 === $results->return_code ) { self::warning( "Spawned process returned exit code {$results->return_code}, which could be caused by a custom compiled version of PHP that uses the --enable-sigchild option." ); } if ( $results->return_code && $exit_on_error ) { exit( $results->return_code ); } if ( $return_detailed ) { return $results; } return $results->return_code; } /** * Run a WP-CLI command in a new process reusing the current runtime arguments. * * Use `WP_CLI::runcommand()` instead, which is easier to use and works better. * * Note: While this command does persist a limited set of runtime arguments, * it *does not* persist environment variables. Practically speaking, WP-CLI * packages won't be loaded when using WP_CLI::launch_self() because the * launched process doesn't have access to the current process $HOME. * * @access public * @category Execution * * @param string $command WP-CLI command to call. * @param array $args Positional arguments to include when calling the command. * @param array $assoc_args Associative arguments to include when calling the command. * @param bool $exit_on_error Whether to exit if the command returns an elevated return code. * @param bool $return_detailed Whether to return an exit status (default) or detailed execution results. * @param array $runtime_args Override one or more global args (path,url,user,allow-root) * @return int|ProcessRun The command exit status, or a ProcessRun instance */ public static function launch_self( $command, $args = [], $assoc_args = [], $exit_on_error = true, $return_detailed = false, $runtime_args = [] ) { $reused_runtime_args = [ 'path', 'url', 'user', 'allow-root', ]; foreach ( $reused_runtime_args as $key ) { if ( isset( $runtime_args[ $key ] ) ) { $assoc_args[ $key ] = $runtime_args[ $key ]; continue; } $value = self::get_runner()->config[ $key ]; if ( $value ) { $assoc_args[ $key ] = $value; } } $php_bin = escapeshellarg( Utils\get_php_binary() ); $script_path = $GLOBALS['argv'][0]; if ( getenv( 'WP_CLI_CONFIG_PATH' ) ) { $config_path = getenv( 'WP_CLI_CONFIG_PATH' ); } else { $config_path = Utils\get_home_dir() . '/.wp-cli/config.yml'; } $config_path = escapeshellarg( $config_path ); $args = implode( ' ', array_map( 'escapeshellarg', $args ) ); $assoc_args = Utils\assoc_args_to_str( $assoc_args ); $full_command = "WP_CLI_CONFIG_PATH={$config_path} {$php_bin} {$script_path} {$command} {$args} {$assoc_args}"; return self::launch( $full_command, $exit_on_error, $return_detailed ); } /** * Get the path to the PHP binary used when executing WP-CLI. * * Environment values permit specific binaries to be indicated. * * Note: moved to Utils, left for BC. * * @access public * @category System * * @return string */ public static function get_php_binary() { return Utils\get_php_binary(); } /** * Confirm that a global configuration parameter does exist. * * @access public * @category Input * * @param string $key Config parameter key to check. * * @return bool */ public static function has_config( $key ) { return array_key_exists( $key, self::get_runner()->config ); } /** * Get values of global configuration parameters. * * Provides access to `--path=`, `--url=`, and other values of * the [global configuration parameters](https://wp-cli.org/config/). * * ``` * WP_CLI::log( 'The --url= value is: ' . WP_CLI::get_config( 'url' ) ); * ``` * * @access public * @category Input * * @param string $key Get value for a specific global configuration parameter. * @return mixed */ public static function get_config( $key = null ) { if ( null === $key ) { return self::get_runner()->config; } if ( ! isset( self::get_runner()->config[ $key ] ) ) { self::warning( "Unknown config option '$key'." ); return null; } return self::get_runner()->config[ $key ]; } /** * Run a WP-CLI command. * * Launches a new child process to run a specified WP-CLI command. * Optionally: * * * Run the command in an existing process. * * Prevent halting script execution on error. * * Capture and return STDOUT, or full details about command execution. * * Parse JSON output if the command rendered it. * * ``` * $options = array( * 'return' => true, // Return 'STDOUT'; use 'all' for full object. * 'parse' => 'json', // Parse captured STDOUT to JSON array. * 'launch' => false, // Reuse the current process. * 'exit_error' => true, // Halt script execution on error. * ); * $plugins = WP_CLI::runcommand( 'plugin list --format=json', $options ); * ``` * * @access public * @category Execution * * @param string $command WP-CLI command to run, including arguments. * @param array $options Configuration options for command execution. * @return mixed */ public static function runcommand( $command, $options = [] ) { $defaults = [ 'launch' => true, // Launch a new process, or reuse the existing. 'exit_error' => true, // Exit on error by default. 'return' => false, // Capture and return output, or render in realtime. 'parse' => false, // Parse returned output as a particular format. ]; $options = array_merge( $defaults, $options ); $launch = $options['launch']; $exit_error = $options['exit_error']; $return = $options['return']; $parse = $options['parse']; $retval = null; if ( $launch ) { Utils\check_proc_available( 'launch option' ); $descriptors = [ 0 => STDIN, 1 => STDOUT, 2 => STDERR, ]; if ( $return ) { $descriptors = [ 0 => STDIN, 1 => [ 'pipe', 'w' ], 2 => [ 'pipe', 'w' ], ]; } $php_bin = escapeshellarg( Utils\get_php_binary() ); $script_path = $GLOBALS['argv'][0]; // Persist runtime arguments unless they've been specified otherwise. $configurator = self::get_configurator(); $argv = array_slice( $GLOBALS['argv'], 1 ); list( $ignore1, $ignore2, $runtime_config ) = $configurator->parse_args( $argv ); foreach ( $runtime_config as $k => $v ) { if ( preg_match( "|^--{$k}=?$|", $command ) ) { unset( $runtime_config[ $k ] ); } } $runtime_config = Utils\assoc_args_to_str( $runtime_config ); $runcommand = "{$php_bin} {$script_path} {$runtime_config} {$command}"; $pipes = []; $proc = Utils\proc_open_compat( $runcommand, $descriptors, $pipes, getcwd() ); if ( $return ) { $stdout = stream_get_contents( $pipes[1] ); fclose( $pipes[1] ); $stderr = stream_get_contents( $pipes[2] ); fclose( $pipes[2] ); } $return_code = proc_close( $proc ); if ( -1 === $return_code ) { self::warning( 'Spawned process returned exit code -1, which could be caused by a custom compiled version of PHP that uses the --enable-sigchild option.' ); } elseif ( $return_code && $exit_error ) { exit( $return_code ); } if ( true === $return || 'stdout' === $return ) { $retval = trim( $stdout ); } elseif ( 'stderr' === $return ) { $retval = trim( $stderr ); } elseif ( 'return_code' === $return ) { $retval = $return_code; } elseif ( 'all' === $return ) { $retval = (object) [ 'stdout' => trim( $stdout ), 'stderr' => trim( $stderr ), 'return_code' => $return_code, ]; } } else { $configurator = self::get_configurator(); $argv = Utils\parse_str_to_argv( $command ); list( $args, $assoc_args, $runtime_config ) = $configurator->parse_args( $argv ); if ( $return ) { $existing_logger = self::$logger; self::$logger = new Execution(); self::$logger->ob_start(); } if ( ! $exit_error ) { self::$capture_exit = true; } try { self::get_runner()->run_command( $args, $assoc_args, [ 'back_compat_conversions' => true, ] ); $return_code = 0; } catch ( ExitException $e ) { $return_code = $e->getCode(); } if ( $return ) { $execution_logger = self::$logger; $execution_logger->ob_end(); self::$logger = $existing_logger; $stdout = $execution_logger->stdout; $stderr = $execution_logger->stderr; if ( true === $return || 'stdout' === $return ) { $retval = trim( $stdout ); } elseif ( 'stderr' === $return ) { $retval = trim( $stderr ); } elseif ( 'return_code' === $return ) { $retval = $return_code; } elseif ( 'all' === $return ) { $retval = (object) [ 'stdout' => trim( $stdout ), 'stderr' => trim( $stderr ), 'return_code' => $return_code, ]; } } if ( ! $exit_error ) { self::$capture_exit = false; } } if ( ( true === $return || 'stdout' === $return ) && 'json' === $parse ) { $retval = json_decode( $retval, true ); } return $retval; } /** * Run a given command within the current process using the same global * parameters. * * Use `WP_CLI::runcommand()` instead, which is easier to use and works better. * * To run a command using a new process with the same global parameters, * use WP_CLI::launch_self(). To run a command using a new process with * different global parameters, use WP_CLI::launch(). * * ``` * ob_start(); * WP_CLI::run_command( array( 'cli', 'cmd-dump' ) ); * $ret = ob_get_clean(); * ``` * * @access public * @category Execution * * @param array $args Positional arguments including command name. * @param array $assoc_args */ public static function run_command( $args, $assoc_args = [] ) { self::get_runner()->run_command( $args, $assoc_args ); } // DEPRECATED STUFF public static function add_man_dir() { trigger_error( 'WP_CLI::add_man_dir() is deprecated. Add docs inline.', E_USER_WARNING ); } // back-compat public static function out( $str ) { fwrite( STDOUT, $str ); } // back-compat // phpcs:ignore WordPress.NamingConventions.ValidFunctionName.MethodNameInvalid -- Deprecated method. public static function addCommand( $name, $class ) { trigger_error( sprintf( 'wp %s: %s is deprecated. use WP_CLI::add_command() instead.', $name, __FUNCTION__ ), E_USER_WARNING ); self::add_command( $name, $class ); } } #.*?$)|(?>//.*?$)|(?>/\*.*?\*/)|(?>\'(?:(?=(\\\\?))\1.)*?\')|(?>"(?:(?=(\\\\?))\2.)*?")|(?\b__FILE__\b)|(?\b__DIR__\b)%ms'; function inside_phar() { return 0 === strpos( WP_CLI_ROOT, PHAR_STREAM_PREFIX ); } // Files that need to be read by external programs have to be extracted from the Phar archive. function extract_from_phar( $path ) { if ( ! inside_phar() ) { return $path; } $fname = basename( $path ); $tmp_path = get_temp_dir() . uniqid( 'wp-cli-extract-from-phar-', true ) . "-$fname"; copy( $path, $tmp_path ); register_shutdown_function( function() use ( $tmp_path ) { if ( file_exists( $tmp_path ) ) { unlink( $tmp_path ); } } ); return $tmp_path; } function load_dependencies() { if ( inside_phar() ) { if ( file_exists( WP_CLI_ROOT . '/vendor/autoload.php' ) ) { require WP_CLI_ROOT . '/vendor/autoload.php'; } elseif ( file_exists( dirname( dirname( WP_CLI_ROOT ) ) . '/autoload.php' ) ) { require dirname( dirname( WP_CLI_ROOT ) ) . '/autoload.php'; } return; } $has_autoload = false; foreach ( get_vendor_paths() as $vendor_path ) { if ( file_exists( $vendor_path . '/autoload.php' ) ) { require $vendor_path . '/autoload.php'; $has_autoload = true; break; } } if ( ! $has_autoload ) { fwrite( STDERR, "Internal error: Can't find Composer autoloader.\nTry running: composer install\n" ); exit( 3 ); } } function get_vendor_paths() { $vendor_paths = [ WP_CLI_ROOT . '/../../../vendor', // Part of a larger project / installed via Composer (preferred). WP_CLI_ROOT . '/vendor', // Top-level project / installed as Git clone. ]; $maybe_composer_json = WP_CLI_ROOT . '/../../../composer.json'; if ( file_exists( $maybe_composer_json ) && is_readable( $maybe_composer_json ) ) { $composer = json_decode( file_get_contents( $maybe_composer_json ) ); if ( ! empty( $composer->config ) && ! empty( $composer->config->{'vendor-dir'} ) ) { array_unshift( $vendor_paths, WP_CLI_ROOT . '/../../../' . $composer->config->{'vendor-dir'} ); } } return $vendor_paths; } // Using require() directly inside a class grants access to private methods to the loaded code. function load_file( $path ) { require_once $path; } function load_command( $name ) { $path = WP_CLI_ROOT . "/php/commands/$name.php"; if ( is_readable( $path ) ) { include_once $path; } } /** * Like array_map(), except it returns a new iterator, instead of a modified array. * * Example: * * $arr = array('Football', 'Socker'); * * $it = iterator_map($arr, 'strtolower', function($val) { * return str_replace('foo', 'bar', $val); * }); * * foreach ( $it as $val ) { * var_dump($val); * } * * @param array|object $it Either a plain array or another iterator. * @param callback $fn The function to apply to an element. * @return object An iterator that applies the given callback(s). */ function iterator_map( $it, $fn ) { if ( is_array( $it ) ) { $it = new ArrayIterator( $it ); } if ( ! method_exists( $it, 'add_transform' ) ) { $it = new Transform( $it ); } foreach ( array_slice( func_get_args(), 1 ) as $fn ) { $it->add_transform( $fn ); } return $it; } /** * Search for file by walking up the directory tree until the first file is found or until $stop_check($dir) returns true. * * @param string|array $files The files (or file) to search for. * @param string|null $dir The directory to start searching from; defaults to CWD. * @param callable $stop_check Function which is passed the current dir each time a directory level is traversed. * @return null|string Null if the file was not found. */ function find_file_upward( $files, $dir = null, $stop_check = null ) { $files = (array) $files; if ( is_null( $dir ) ) { $dir = getcwd(); } while ( is_readable( $dir ) ) { // Stop walking up when the supplied callable returns true being passed the $dir if ( is_callable( $stop_check ) && call_user_func( $stop_check, $dir ) ) { return null; } foreach ( $files as $file ) { $path = $dir . DIRECTORY_SEPARATOR . $file; if ( file_exists( $path ) ) { return $path; } } $parent_dir = dirname( $dir ); if ( empty( $parent_dir ) || $parent_dir === $dir ) { break; } $dir = $parent_dir; } return null; } function is_path_absolute( $path ) { // Windows if ( isset( $path[1] ) && ':' === $path[1] ) { return true; } return '/' === $path[0]; } /** * Composes positional arguments into a command string. * * @param array $args Positional arguments to compose. * @return string */ function args_to_str( $args ) { return ' ' . implode( ' ', array_map( 'escapeshellarg', $args ) ); } /** * Composes associative arguments into a command string. * * @param array $assoc_args Associative arguments to compose. * @return string */ function assoc_args_to_str( $assoc_args ) { $str = ''; foreach ( $assoc_args as $key => $value ) { if ( true === $value ) { $str .= " --$key"; } elseif ( is_array( $value ) ) { foreach ( $value as $v ) { $str .= assoc_args_to_str( [ $key => $v, ] ); } } else { $str .= " --$key=" . escapeshellarg( $value ); } } return $str; } /** * Given a template string and an arbitrary number of arguments, * returns the final command, with the parameters escaped. */ function esc_cmd( $cmd ) { if ( func_num_args() < 2 ) { trigger_error( 'esc_cmd() requires at least two arguments.', E_USER_WARNING ); } $args = func_get_args(); $cmd = array_shift( $args ); return vsprintf( $cmd, array_map( 'escapeshellarg', $args ) ); } /** * Gets path to WordPress configuration. * * @return string */ function locate_wp_config() { static $path; if ( null === $path ) { $path = false; if ( getenv( 'WP_CONFIG_PATH' ) && file_exists( getenv( 'WP_CONFIG_PATH' ) ) ) { $path = getenv( 'WP_CONFIG_PATH' ); } elseif ( file_exists( ABSPATH . 'wp-config.php' ) ) { $path = ABSPATH . 'wp-config.php'; } elseif ( file_exists( dirname( ABSPATH ) . '/wp-config.php' ) && ! file_exists( dirname( ABSPATH ) . '/wp-settings.php' ) ) { $path = dirname( ABSPATH ) . '/wp-config.php'; } if ( $path ) { $path = realpath( $path ); } } return $path; } function wp_version_compare( $since, $operator ) { $wp_version = str_replace( '-src', '', $GLOBALS['wp_version'] ); $since = str_replace( '-src', '', $since ); return version_compare( $wp_version, $since, $operator ); } /** * Render a collection of items as an ASCII table, JSON, CSV, YAML, list of ids, or count. * * Given a collection of items with a consistent data structure: * * ``` * $items = array( * array( * 'key' => 'foo', * 'value' => 'bar', * ) * ); * ``` * * Render `$items` as an ASCII table: * * ``` * WP_CLI\Utils\format_items( 'table', $items, array( 'key', 'value' ) ); * * # +-----+-------+ * # | key | value | * # +-----+-------+ * # | foo | bar | * # +-----+-------+ * ``` * * Or render `$items` as YAML: * * ``` * WP_CLI\Utils\format_items( 'yaml', $items, array( 'key', 'value' ) ); * * # --- * # - * # key: foo * # value: bar * ``` * * @access public * @category Output * * @param string $format Format to use: 'table', 'json', 'csv', 'yaml', 'ids', 'count'. * @param array $items An array of items to output. * @param array|string $fields Named fields for each item of data. Can be array or comma-separated list. */ function format_items( $format, $items, $fields ) { $assoc_args = compact( 'format', 'fields' ); $formatter = new Formatter( $assoc_args ); $formatter->display_items( $items ); } /** * Write data as CSV to a given file. * * @access public * * @param resource $fd File descriptor. * @param array $rows Array of rows to output. * @param array $headers List of CSV columns (optional). */ function write_csv( $fd, $rows, $headers = [] ) { if ( ! empty( $headers ) ) { fputcsv( $fd, $headers ); } foreach ( $rows as $row ) { if ( ! empty( $headers ) ) { $row = pick_fields( $row, $headers ); } fputcsv( $fd, array_values( $row ) ); } } /** * Pick fields from an associative array or object. * * @param array|object $item Associative array or object to pick fields from. * @param array $fields List of fields to pick. * @return array */ function pick_fields( $item, $fields ) { $values = []; if ( is_object( $item ) ) { foreach ( $fields as $field ) { $values[ $field ] = isset( $item->$field ) ? $item->$field : null; } } else { foreach ( $fields as $field ) { $values[ $field ] = isset( $item[ $field ] ) ? $item[ $field ] : null; } } return $values; } /** * Launch system's $EDITOR for the user to edit some text. * * @access public * @category Input * * @param string $input Some form of text to edit (e.g. post content). * @param string $title Title to display in the editor. * @param string $ext Extension to use with the temp file. * @return string|bool Edited text, if file is saved from editor; false, if no change to file. */ function launch_editor_for_input( $input, $title = 'WP-CLI', $ext = 'tmp' ) { check_proc_available( 'launch_editor_for_input' ); $tmpdir = get_temp_dir(); do { $tmpfile = basename( $title ); $tmpfile = preg_replace( '|\.[^.]*$|', '', $tmpfile ); $tmpfile .= '-' . substr( md5( mt_rand() ), 0, 6 ); // phpcs:ignore WordPress.WP.AlternativeFunctions.rand_mt_rand -- no crypto and WP not loaded. $tmpfile = $tmpdir . $tmpfile . '.' . $ext; $fp = fopen( $tmpfile, 'xb' ); if ( ! $fp && is_writable( $tmpdir ) && file_exists( $tmpfile ) ) { $tmpfile = ''; continue; } if ( $fp ) { fclose( $fp ); } } while ( ! $tmpfile ); if ( ! $tmpfile ) { WP_CLI::error( 'Error creating temporary file.' ); } file_put_contents( $tmpfile, $input ); $editor = getenv( 'EDITOR' ); if ( ! $editor ) { $editor = is_windows() ? 'notepad' : 'vi'; } $descriptorspec = [ STDIN, STDOUT, STDERR ]; $process = proc_open_compat( "$editor " . escapeshellarg( $tmpfile ), $descriptorspec, $pipes ); $r = proc_close( $process ); if ( $r ) { exit( $r ); } $output = file_get_contents( $tmpfile ); unlink( $tmpfile ); if ( $output === $input ) { return false; } return $output; } /** * @param string $raw_host MySQL host string, as defined in wp-config.php. * * @return array */ function mysql_host_to_cli_args( $raw_host ) { $assoc_args = []; /** * If the host string begins with 'p:' for a persistent db connection, * replace 'p:' with nothing. */ if ( substr( $raw_host, 0, 2 ) === 'p:' ) { $raw_host = substr_replace( $raw_host, '', 0, 2 ); } $host_parts = explode( ':', $raw_host ); if ( count( $host_parts ) === 2 ) { list( $assoc_args['host'], $extra ) = $host_parts; $extra = trim( $extra ); if ( is_numeric( $extra ) ) { $assoc_args['port'] = (int) $extra; $assoc_args['protocol'] = 'tcp'; } elseif ( '' !== $extra ) { $assoc_args['socket'] = $extra; } } else { $assoc_args['host'] = $raw_host; } return $assoc_args; } /** * Run a MySQL command and optionally return the output. * * @since v2.5.0 Deprecated $descriptors argument. * * @param string $cmd Command to run. * @param array $assoc_args Associative array of arguments to use. * @param mixed $_ Deprecated. Former $descriptors argument. * @param bool $send_to_shell Optional. Whether to send STDOUT and STDERR * immediately to the shell. Defaults to true. * @param bool $interactive Optional. Whether MySQL is meant to be * executed as an interactive process. Defaults * to false. * * @return array { * Associative array containing STDOUT and STDERR output. * * @type string $stdout Output that was sent to STDOUT. * @type string $stderr Output that was sent to STDERR. * @type int $exit_code Exit code of the process. * } */ function run_mysql_command( $cmd, $assoc_args, $_ = null, $send_to_shell = true, $interactive = false ) { check_proc_available( 'run_mysql_command' ); $descriptors = ( $interactive || $send_to_shell ) ? [ 0 => STDIN, 1 => STDOUT, 2 => STDERR, ] : [ 0 => STDIN, 1 => [ 'pipe', 'w' ], 2 => [ 'pipe', 'w' ], ]; $stdout = ''; $stderr = ''; $pipes = []; if ( isset( $assoc_args['host'] ) ) { // phpcs:ignore WordPress.DB.RestrictedFunctions.mysql_mysql_host_to_cli_args -- Misidentified as PHP native MySQL function. $assoc_args = array_merge( $assoc_args, mysql_host_to_cli_args( $assoc_args['host'] ) ); } if ( isset( $assoc_args['pass'] ) ) { $old_password = getenv( 'MYSQL_PWD' ); putenv( 'MYSQL_PWD=' . $assoc_args['pass'] ); unset( $assoc_args['pass'] ); } $final_cmd = force_env_on_nix_systems( $cmd ) . assoc_args_to_str( $assoc_args ); WP_CLI::debug( 'Final MySQL command: ' . $final_cmd, 'db' ); $process = proc_open_compat( $final_cmd, $descriptors, $pipes ); if ( isset( $old_password ) ) { putenv( 'MYSQL_PWD=' . $old_password ); } if ( ! $process ) { WP_CLI::debug( 'Failed to create a valid process using proc_open_compat()', 'db' ); exit( 1 ); } if ( is_resource( $process ) && ! $send_to_shell && ! $interactive ) { $stdout = stream_get_contents( $pipes[1] ); $stderr = stream_get_contents( $pipes[2] ); fclose( $pipes[1] ); fclose( $pipes[2] ); } $exit_code = proc_close( $process ); if ( $exit_code && ( $send_to_shell || $interactive ) ) { exit( $exit_code ); } return [ $stdout, $stderr, $exit_code, ]; } /** * Render PHP or other types of files using Mustache templates. * * IMPORTANT: Automatic HTML escaping is disabled! */ function mustache_render( $template_name, $data = [] ) { if ( ! file_exists( $template_name ) ) { $template_name = WP_CLI_ROOT . "/templates/$template_name"; } $template = file_get_contents( $template_name ); $mustache = new Mustache_Engine( [ 'escape' => function ( $val ) { return $val; }, ] ); return $mustache->render( $template, $data ); } /** * Create a progress bar to display percent completion of a given operation. * * Progress bar is written to STDOUT, and disabled when command is piped. Progress * advances with `$progress->tick()`, and completes with `$progress->finish()`. * Process bar also indicates elapsed time and expected total time. * * ``` * # `wp user generate` ticks progress bar each time a new user is created. * # * # $ wp user generate --count=500 * # Generating users 22 % [=======> ] 0:05 / 0:23 * * $progress = \WP_CLI\Utils\make_progress_bar( 'Generating users', $count ); * for ( $i = 0; $i < $count; $i++ ) { * // uses wp_insert_user() to insert the user * $progress->tick(); * } * $progress->finish(); * ``` * * @access public * @category Output * * @param string $message Text to display before the progress bar. * @param integer $count Total number of ticks to be performed. * @param int $interval Optional. The interval in milliseconds between updates. Default 100. * @return cli\progress\Bar|NoOp */ function make_progress_bar( $message, $count, $interval = 100 ) { if ( Shell::isPiped() ) { return new NoOp(); } return new Bar( $message, $count, $interval ); } /** * Helper function to use wp_parse_url when available or fall back to PHP's * parse_url if not. * * Additionally, this adds 'http://' to the URL if no scheme was found. * * @param string $url The URL to parse. * @param int $component Optional. The specific component to retrieve. * Use one of the PHP predefined constants to * specify which one. Defaults to -1 (= return * all parts as an array). * @param bool $auto_add_scheme Optional. Automatically add an http:// scheme if * none was found. Defaults to true. * @return mixed False on parse failure; Array of URL components on success; * When a specific component has been requested: null if the * component doesn't exist in the given URL; a string or - in the * case of PHP_URL_PORT - integer when it does. See parse_url()'s * return values. */ function parse_url( $url, $component = - 1, $auto_add_scheme = true ) { if ( function_exists( 'wp_parse_url' ) && ( -1 === $component || wp_version_compare( '4.7', '>=' ) ) ) { $url_parts = wp_parse_url( $url, $component ); } else { // phpcs:ignore WordPress.WP.AlternativeFunctions.parse_url_parse_url -- Fallback. $url_parts = \parse_url( $url, $component ); } // phpcs:ignore WordPress.WP.AlternativeFunctions.parse_url_parse_url -- Own version based on WP one. if ( $auto_add_scheme && ! parse_url( $url, PHP_URL_SCHEME, false ) ) { // phpcs:ignore WordPress.WP.AlternativeFunctions.parse_url_parse_url -- Own version based on WP one. $url_parts = parse_url( 'http://' . $url, $component, false ); } return $url_parts; } /** * Check if we're running in a Windows environment (cmd.exe). * * @return bool */ function is_windows() { $test_is_windows = getenv( 'WP_CLI_TEST_IS_WINDOWS' ); return false !== $test_is_windows ? (bool) $test_is_windows : strtoupper( substr( PHP_OS, 0, 3 ) ) === 'WIN'; } /** * Replace magic constants in some PHP source code. * * Replaces the __FILE__ and __DIR__ magic constants with the values they are * supposed to represent at runtime. * * @param string $source The PHP code to manipulate. * @param string $path The path to use instead of the magic constants. * @return string Adapted PHP code. */ function replace_path_consts( $source, $path ) { // Solve issue with Windows allowing single quotes in account names. $file = addslashes( $path ); if ( file_exists( $file ) ) { $file = realpath( $file ); } $dir = dirname( $file ); // Replace __FILE__ and __DIR__ constants with value of $file or $dir. return preg_replace_callback( FILE_DIR_PATTERN, static function ( $matches ) use ( $file, $dir ) { if ( ! empty( $matches['file'] ) ) { return "'{$file}'"; } if ( ! empty( $matches['dir'] ) ) { return "'{$dir}'"; } return $matches[0]; }, $source ); } /** * Make a HTTP request to a remote URL. * * Wraps the Requests HTTP library to ensure every request includes a cert. * * ``` * # `wp core download` verifies the hash for a downloaded WordPress archive * * $md5_response = Utils\http_request( 'GET', $download_url . '.md5' ); * if ( 20 != substr( $md5_response->status_code, 0, 2 ) ) { * WP_CLI::error( "Couldn't access md5 hash for release (HTTP code {$response->status_code})" ); * } * ``` * * @access public * * @param string $method HTTP method (GET, POST, DELETE, etc.). * @param string $url URL to make the HTTP request to. * @param array $headers Add specific headers to the request. * @param array $options { * Optional. An associative array of additional request options. * * @type bool $halt_on_error Whether or not command execution should be halted on error. Default: true * @type bool|string $verify A boolean to use enable/disable SSL verification * or string absolute path to CA cert to use. * Defaults to detected CA cert bundled with the Requests library. * @type bool $insecure Whether to retry automatically without certificate validation. * } * @return object * @throws RuntimeException If the request failed. * @throws ExitException If the request failed and $halt_on_error is true. */ function http_request( $method, $url, $data = null, $headers = [], $options = [] ) { if ( ! class_exists( 'Requests_Hooks' ) ) { // Autoloader for the Requests library has not been registered yet. Requests::register_autoloader(); } $insecure = isset( $options['insecure'] ) && (bool) $options['insecure']; $halt_on_error = ! isset( $options['halt_on_error'] ) || (bool) $options['halt_on_error']; unset( $options['halt_on_error'] ); if ( ! isset( $options['verify'] ) ) { // 'curl.cainfo' enforces the CA file to use, otherwise fallback to system-wide defaults then use the embedded CA file. $options['verify'] = ! empty( ini_get( 'curl.cainfo' ) ) ? ini_get( 'curl.cainfo' ) : true; } try { try { return Requests::request( $url, $headers, $data, $method, $options ); } catch ( Requests_Exception $ex ) { if ( true !== $options['verify'] || 'curlerror' !== $ex->getType() || curl_errno( $ex->getData() ) !== CURLE_SSL_CACERT ) { throw $ex; } $options['verify'] = get_default_cacert( $halt_on_error ); return Requests::request( $url, $headers, $data, $method, $options ); } } catch ( Requests_Exception $ex ) { // CURLE_SSL_CACERT_BADFILE only defined for PHP >= 7. if ( ! $insecure || 'curlerror' !== $ex->getType() || ! in_array( curl_errno( $ex->getData() ), [ CURLE_SSL_CONNECT_ERROR, CURLE_SSL_CERTPROBLEM, 77 /*CURLE_SSL_CACERT_BADFILE*/ ], true ) ) { $error_msg = sprintf( "Failed to get url '%s': %s.", $url, $ex->getMessage() ); if ( $halt_on_error ) { WP_CLI::error( $error_msg ); } throw new RuntimeException( $error_msg, null, $ex ); } $warning = sprintf( "Re-trying without verify after failing to get verified url '%s' %s.", $url, $ex->getMessage() ); WP_CLI::warning( $warning ); // Disable certificate validation for the next try. $options['verify'] = false; try { return Requests::request( $url, $headers, $data, $method, $options ); } catch ( Requests_Exception $ex ) { $error_msg = sprintf( "Failed to get non-verified url '%s' %s.", $url, $ex->getMessage() ); if ( $halt_on_error ) { WP_CLI::error( $error_msg ); } throw new RuntimeException( $error_msg, null, $ex ); } } } /** * Gets the full path to the default CA cert. * * @param bool $halt_on_error Whether or not command execution should be halted on error. Default: false * @return string Absolute path to the default CA cert. * @throws RuntimeException If unable to locate the cert. * @throws ExitException If unable to locate the cert and $halt_on_error is true. */ function get_default_cacert( $halt_on_error = false ) { $cert_path = '/rmccue/requests/library/Requests/Transport/cacert.pem'; $error_msg = 'Cannot find SSL certificate.'; if ( inside_phar() ) { // cURL can't read Phar archives return extract_from_phar( WP_CLI_VENDOR_DIR . $cert_path ); } foreach ( get_vendor_paths() as $vendor_path ) { if ( file_exists( $vendor_path . $cert_path ) ) { return $vendor_path . $cert_path; } } if ( $halt_on_error ) { WP_CLI::error( $error_msg ); } throw new RuntimeException( $error_msg ); } /** * Increments a version string using the "x.y.z-pre" format. * * Can increment the major, minor or patch number by one. * If $new_version == "same" the version string is not changed. * If $new_version is not a known keyword, it will be used as the new version string directly. * * @param string $current_version * @param string $new_version * @return string */ function increment_version( $current_version, $new_version ) { // split version assuming the format is x.y.z-pre $current_version = explode( '-', $current_version, 2 ); $current_version[0] = explode( '.', $current_version[0] ); switch ( $new_version ) { case 'same': // do nothing break; case 'patch': $current_version[0][2]++; $current_version = [ $current_version[0] ]; // Drop possible pre-release info. break; case 'minor': $current_version[0][1]++; $current_version[0][2] = 0; $current_version = [ $current_version[0] ]; // Drop possible pre-release info. break; case 'major': $current_version[0][0]++; $current_version[0][1] = 0; $current_version[0][2] = 0; $current_version = [ $current_version[0] ]; // Drop possible pre-release info. break; default: // not a keyword $current_version = [ [ $new_version ] ]; break; } // Reconstruct version string. $current_version[0] = implode( '.', $current_version[0] ); $current_version = implode( '-', $current_version ); return $current_version; } /** * Compare two version strings to get the named semantic version. * * @access public * * @param string $new_version * @param string $original_version * @return string 'major', 'minor', 'patch' */ function get_named_sem_ver( $new_version, $original_version ) { if ( ! Comparator::greaterThan( $new_version, $original_version ) ) { return ''; } $parts = explode( '-', $original_version ); $bits = explode( '.', $parts[0] ); $major = $bits[0]; if ( isset( $bits[1] ) ) { $minor = $bits[1]; } if ( isset( $bits[2] ) ) { $patch = $bits[2]; } if ( isset( $minor ) && Semver::satisfies( $new_version, "{$major}.{$minor}.x" ) ) { return 'patch'; } if ( Semver::satisfies( $new_version, "{$major}.x.x" ) ) { return 'minor'; } return 'major'; } /** * Return the flag value or, if it's not set, the $default value. * * Because flags can be negated (e.g. --no-quiet to negate --quiet), this * function provides a safer alternative to using * `isset( $assoc_args['quiet'] )` or similar. * * @access public * @category Input * * @param array $assoc_args Arguments array. * @param string $flag Flag to get the value. * @param mixed $default Default value for the flag. Default: NULL. * @return mixed */ function get_flag_value( $assoc_args, $flag, $default = null ) { return isset( $assoc_args[ $flag ] ) ? $assoc_args[ $flag ] : $default; } /** * Get the home directory. * * @access public * @category System * * @return string */ function get_home_dir() { $home = getenv( 'HOME' ); if ( ! $home ) { // In Windows $HOME may not be defined $home = getenv( 'HOMEDRIVE' ) . getenv( 'HOMEPATH' ); } return rtrim( $home, '/\\' ); } /** * Appends a trailing slash. * * @access public * @category System * * @param string $string What to add the trailing slash to. * @return string String with trailing slash added. */ function trailingslashit( $string ) { return rtrim( $string, '/\\' ) . '/'; } /** * Normalize a filesystem path. * * On Windows systems, replaces backslashes with forward slashes * and forces upper-case drive letters. * Allows for two leading slashes for Windows network shares, but * ensures that all other duplicate slashes are reduced to a single one. * Ensures upper-case drive letters on Windows systems. * * @access public * @category System * * @param string $path Path to normalize. * @return string Normalized path. */ function normalize_path( $path ) { $path = str_replace( '\\', '/', $path ); $path = preg_replace( '|(?<=.)/+|', '/', $path ); if ( ':' === substr( $path, 1, 1 ) ) { $path = ucfirst( $path ); } return $path; } /** * Convert Windows EOLs to *nix. * * @param string $str String to convert. * @return string String with carriage return / newline pairs reduced to newlines. */ function normalize_eols( $str ) { return str_replace( "\r\n", "\n", $str ); } /** * Get the system's temp directory. Warns user if it isn't writable. * * @access public * @category System * * @return string */ function get_temp_dir() { static $temp = ''; if ( $temp ) { return $temp; } // `sys_get_temp_dir()` introduced PHP 5.2.1. Will always return something. $temp = trailingslashit( sys_get_temp_dir() ); if ( ! is_writable( $temp ) ) { WP_CLI::warning( "Temp directory isn't writable: {$temp}" ); } return $temp; } /** * Parse a SSH url for its host, port, and path. * * Similar to parse_url(), but adds support for defined SSH aliases. * * ``` * host OR host/path/to/wordpress OR host:port/path/to/wordpress * ``` * * @access public * * @return mixed */ function parse_ssh_url( $url, $component = -1 ) { preg_match( '#^((docker|docker\-compose|ssh|vagrant):)?(([^@:]+)@)?([^:/~]+)(:([\d]*))?((/|~)(.+))?$#', $url, $matches ); $bits = []; foreach ( [ 2 => 'scheme', 4 => 'user', 5 => 'host', 7 => 'port', 8 => 'path', ] as $i => $key ) { if ( ! empty( $matches[ $i ] ) ) { $bits[ $key ] = $matches[ $i ]; } } // Find the hostname from `vagrant ssh-config` automatically. if ( preg_match( '/^vagrant:?/', $url ) ) { if ( 'vagrant' === $bits['host'] && empty( $bits['scheme'] ) ) { $bits['scheme'] = 'vagrant'; $bits['host'] = ''; } } switch ( $component ) { case PHP_URL_SCHEME: return isset( $bits['scheme'] ) ? $bits['scheme'] : null; case PHP_URL_USER: return isset( $bits['user'] ) ? $bits['user'] : null; case PHP_URL_HOST: return isset( $bits['host'] ) ? $bits['host'] : null; case PHP_URL_PATH: return isset( $bits['path'] ) ? $bits['path'] : null; case PHP_URL_PORT: return isset( $bits['port'] ) ? $bits['port'] : null; default: return $bits; } } /** * Report the results of the same operation against multiple resources. * * @access public * @category Input * * @param string $noun Resource being affected (e.g. plugin). * @param string $verb Type of action happening to the noun (e.g. activate). * @param integer $total Total number of resource being affected. * @param integer $successes Number of successful operations. * @param integer $failures Number of failures. * @param null|integer $skips Optional. Number of skipped operations. Default null (don't show skips). */ function report_batch_operation_results( $noun, $verb, $total, $successes, $failures, $skips = null ) { $plural_noun = $noun . 's'; $past_tense_verb = past_tense_verb( $verb ); $past_tense_verb_upper = ucfirst( $past_tense_verb ); if ( $failures ) { $failed_skipped_message = null === $skips ? '' : " ({$failures} failed" . ( $skips ? ", {$skips} skipped" : '' ) . ')'; if ( $successes ) { WP_CLI::error( "Only {$past_tense_verb} {$successes} of {$total} {$plural_noun}{$failed_skipped_message}." ); } else { WP_CLI::error( "No {$plural_noun} {$past_tense_verb}{$failed_skipped_message}." ); } } else { $skipped_message = $skips ? " ({$skips} skipped)" : ''; if ( $successes || $skips ) { WP_CLI::success( "{$past_tense_verb_upper} {$successes} of {$total} {$plural_noun}{$skipped_message}." ); } else { $message = $total > 1 ? ucfirst( $plural_noun ) : ucfirst( $noun ); WP_CLI::success( "{$message} already {$past_tense_verb}." ); } } } /** * Parse a string of command line arguments into an $argv-esqe variable. * * @access public * @category Input * * @param string $arguments * @return array */ function parse_str_to_argv( $arguments ) { preg_match_all( '/(?:--[^\s=]+=(["\'])((\\{2})*|(?:[^\1]+?[^\\\\](\\{2})*))\1|--[^\s=]+=[^\s]+|--[^\s=]+|(["\'])((\\{2})*|(?:[^\5]+?[^\\\\](\\{2})*))\5|[^\s]+)/', $arguments, $matches ); $argv = isset( $matches[0] ) ? $matches[0] : []; return array_map( static function ( $arg ) { foreach ( [ '"', "'" ] as $char ) { if ( substr( $arg, 0, 1 ) === $char && substr( $arg, -1 ) === $char ) { $arg = substr( $arg, 1, -1 ); break; } } return $arg; }, $argv ); } /** * Locale-independent version of basename() * * @access public * * @param string $path * @param string $suffix * @return string */ function basename( $path, $suffix = '' ) { // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.urlencode_urlencode -- Format required by wordpress.org API. return urldecode( \basename( str_replace( [ '%2F', '%5C' ], '/', urlencode( $path ) ), $suffix ) ); } /** * Checks whether the output of the current script is a TTY or a pipe / redirect * * Returns true if STDOUT output is being redirected to a pipe or a file; false is * output is being sent directly to the terminal. * * If an env variable SHELL_PIPE exists, returned result depends on its * value. Strings like 1, 0, yes, no, that validate to booleans are accepted. * * To enable ASCII formatting even when the shell is piped, use the * ENV variable SHELL_PIPE=0. * * @access public * * @return bool */ function isPiped() { // phpcs:ignore WordPress.NamingConventions.ValidFunctionName.FunctionNameInvalid -- Renaming would break BC. $shell_pipe = getenv( 'SHELL_PIPE' ); if ( false !== $shell_pipe ) { return filter_var( $shell_pipe, FILTER_VALIDATE_BOOLEAN ); } return function_exists( 'posix_isatty' ) && ! posix_isatty( STDOUT ); } /** * Expand within paths to their matching paths. * * Has no effect on paths which do not use glob patterns. * * @param string|array $paths Single path as a string, or an array of paths. * @param int $flags Optional. Flags to pass to glob. Defaults to GLOB_BRACE. * @return array Expanded paths. */ function expand_globs( $paths, $flags = 'default' ) { // Compatibility for systems without GLOB_BRACE. $glob_func = 'glob'; if ( 'default' === $flags ) { if ( ! defined( 'GLOB_BRACE' ) || getenv( 'WP_CLI_TEST_EXPAND_GLOBS_NO_GLOB_BRACE' ) ) { $glob_func = 'WP_CLI\Utils\glob_brace'; } else { $flags = GLOB_BRACE; } } $expanded = []; foreach ( (array) $paths as $path ) { $matching = [ $path ]; if ( preg_match( '/[' . preg_quote( '*?[]{}!', '/' ) . ']/', $path ) ) { $matching = $glob_func( $path, $flags ) ?: []; } $expanded = array_merge( $expanded, $matching ); } return array_values( array_unique( $expanded ) ); } /** * Simulate a `glob()` with the `GLOB_BRACE` flag set. For systems (eg Alpine Linux) built against a libc library (eg https://www.musl-libc.org/) that lacks it. * Copied and adapted from Zend Framework's `Glob::fallbackGlob()` and Glob::nextBraceSub()`. * * Zend Framework (https://framework.zend.com/) * * @link https://github.com/zendframework/zf2 for the canonical source repository * @copyright Copyright (c) 2005-2015 Zend Technologies USA Inc. (https://www.zend.com) * @license https://framework.zend.com/license/new-bsd New BSD License * * @param string $pattern Filename pattern. * @param void $dummy_flags Not used. * @return array Array of paths. */ function glob_brace( $pattern, $dummy_flags = null ) { static $next_brace_sub; if ( ! $next_brace_sub ) { // Find the end of the subpattern in a brace expression. $next_brace_sub = function ( $pattern, $current ) { $length = strlen( $pattern ); $depth = 0; while ( $current < $length ) { if ( '\\' === $pattern[ $current ] ) { if ( ++$current === $length ) { break; } $current++; } else { if ( ( '}' === $pattern[ $current ] && 0 === $depth-- ) || ( ',' === $pattern[ $current ] && 0 === $depth ) ) { break; } if ( '{' === $pattern[ $current++ ] ) { $depth++; } } } return $current < $length ? $current : null; }; } $length = strlen( $pattern ); // Find first opening brace. for ( $begin = 0; $begin < $length; $begin++ ) { if ( '\\' === $pattern[ $begin ] ) { $begin++; } elseif ( '{' === $pattern[ $begin ] ) { break; } } // Find comma or matching closing brace. $next = $next_brace_sub( $pattern, $begin + 1 ); if ( null === $next ) { return glob( $pattern ); } $rest = $next; // Point `$rest` to matching closing brace. while ( '}' !== $pattern[ $rest ] ) { $rest = $next_brace_sub( $pattern, $rest + 1 ); if ( null === $rest ) { return glob( $pattern ); } } $paths = []; $p = $begin + 1; // For each comma-separated subpattern. do { $subpattern = substr( $pattern, 0, $begin ) . substr( $pattern, $p, $next - $p ) . substr( $pattern, $rest + 1 ); $result = glob_brace( $subpattern ); if ( ! empty( $result ) ) { $paths = array_merge( $paths, $result ); } if ( '}' === $pattern[ $next ] ) { break; } $p = $next + 1; $next = $next_brace_sub( $pattern, $p ); } while ( null !== $next ); return array_values( array_unique( $paths ) ); } /** * Get the closest suggestion for a mis-typed target term amongst a list of * options. * * Uses the Levenshtein algorithm to calculate the relative "distance" between * terms. * * If the "distance" to the closest term is higher than the threshold, an empty * string is returned. * * @param string $target Target term to get a suggestion for. * @param array $options Array with possible options. * @param int $threshold Threshold above which to return an empty string. * @return string */ function get_suggestion( $target, array $options, $threshold = 2 ) { $suggestion_map = [ 'add' => 'create', 'check' => 'check-update', 'capability' => 'cap', 'clear' => 'flush', 'decrement' => 'decr', 'del' => 'delete', 'directory' => 'dir', 'exec' => 'eval', 'exec-file' => 'eval-file', 'increment' => 'incr', 'language' => 'locale', 'lang' => 'locale', 'new' => 'create', 'number' => 'count', 'remove' => 'delete', 'regen' => 'regenerate', 'rep' => 'replace', 'repl' => 'replace', 'trash' => 'delete', 'v' => 'version', ]; if ( array_key_exists( $target, $suggestion_map ) && in_array( $suggestion_map[ $target ], $options, true ) ) { return $suggestion_map[ $target ]; } if ( empty( $options ) ) { return ''; } foreach ( $options as $option ) { $distance = levenshtein( $option, $target ); $levenshtein[ $option ] = $distance; } // Sort known command strings by distance to user entry. asort( $levenshtein ); // Fetch the closest command string. reset( $levenshtein ); $suggestion = key( $levenshtein ); // Only return a suggestion if below a given threshold. return $levenshtein[ $suggestion ] <= $threshold && $suggestion !== $target ? (string) $suggestion : ''; } /** * Get a Phar-safe version of a path. * * For paths inside a Phar, this strips the outer filesystem's location to * reduce the path to what it needs to be within the Phar archive. * * Use the __FILE__ or __DIR__ constants as a starting point. * * @param string $path An absolute path that might be within a Phar. * @return string A Phar-safe version of the path. */ function phar_safe_path( $path ) { if ( ! inside_phar() ) { return $path; } return str_replace( PHAR_STREAM_PREFIX . WP_CLI_PHAR_PATH . '/', PHAR_STREAM_PREFIX, $path ); } /** * Maybe prefix command string with "/usr/bin/env". * Removes (if there) if Windows, adds (if not there) if not. * * @param string $command * @return string */ function force_env_on_nix_systems( $command ) { $env_prefix = '/usr/bin/env '; $env_prefix_len = strlen( $env_prefix ); if ( is_windows() ) { if ( 0 === strncmp( $command, $env_prefix, $env_prefix_len ) ) { $command = substr( $command, $env_prefix_len ); } } elseif ( 0 !== strncmp( $command, $env_prefix, $env_prefix_len ) ) { $command = $env_prefix . $command; } return $command; } /** * Check that `proc_open()` and `proc_close()` haven't been disabled. * * @param string $context Optional. If set will appear in error message. Default null. * @param bool $return Optional. If set will return false rather than error out. Default false. * @return bool */ function check_proc_available( $context = null, $return = false ) { if ( ! function_exists( 'proc_open' ) || ! function_exists( 'proc_close' ) ) { if ( $return ) { return false; } $msg = 'The PHP functions `proc_open()` and/or `proc_close()` are disabled. Please check your PHP ini directive `disable_functions` or suhosin settings.'; if ( $context ) { WP_CLI::error( sprintf( "Cannot do '%s': %s", $context, $msg ) ); } else { WP_CLI::error( $msg ); } } return true; } /** * Returns past tense of verb, with limited accuracy. Only regular verbs catered for, apart from "reset". * * @param string $verb Verb to return past tense of. * @return string */ function past_tense_verb( $verb ) { static $irregular = [ 'reset' => 'reset', ]; if ( isset( $irregular[ $verb ] ) ) { return $irregular[ $verb ]; } $last = substr( $verb, -1 ); if ( 'e' === $last ) { $verb = substr( $verb, 0, -1 ); } elseif ( 'y' === $last && ! preg_match( '/[aeiou]y$/', $verb ) ) { $verb = substr( $verb, 0, -1 ) . 'i'; } elseif ( preg_match( '/^[^aeiou]*[aeiou][^aeiouhwxy]$/', $verb ) ) { // Rule of thumb that most (all?) one-voweled regular verbs ending in vowel + consonant (excluding "h", "w", "x", "y") double their final consonant - misses many cases (eg "submit"). $verb .= $last; } return $verb . 'ed'; } /** * Get the path to the PHP binary used when executing WP-CLI. * * Environment values permit specific binaries to be indicated. * * @access public * @category System * * @return string */ function get_php_binary() { $wp_cli_php_used = getenv( 'WP_CLI_PHP_USED' ); if ( false !== $wp_cli_php_used ) { return $wp_cli_php_used; } $wp_cli_php = getenv( 'WP_CLI_PHP' ); if ( false !== $wp_cli_php ) { return $wp_cli_php; } return PHP_BINARY; } /** * Windows compatible `proc_open()`. * Works around bug in PHP, and also deals with *nix-like `ENV_VAR=blah cmd` environment variable prefixes. * * @access public * * @param string $cmd Command to execute. * @param array $descriptorspec Indexed array of descriptor numbers and their values. * @param array &$pipes Indexed array of file pointers that correspond to PHP's end of any pipes that are created. * @param string $cwd Initial working directory for the command. * @param array $env Array of environment variables. * @param array $other_options Array of additional options (Windows only). * @return resource Command stripped of any environment variable settings. */ function proc_open_compat( $cmd, $descriptorspec, &$pipes, $cwd = null, $env = null, $other_options = null ) { if ( is_windows() ) { // Need to encompass the whole command in double quotes - PHP bug https://bugs.php.net/bug.php?id=49139 $cmd = '"' . _proc_open_compat_win_env( $cmd, $env ) . '"'; } return proc_open( $cmd, $descriptorspec, $pipes, $cwd, $env, $other_options ); } /** * For use by `proc_open_compat()` only. Separated out for ease of testing. Windows only. * Turns *nix-like `ENV_VAR=blah command` environment variable prefixes into stripped `cmd` with prefixed environment variables added to passed in environment array. * * @access private * * @param string $cmd Command to execute. * @param array &$env Array of existing environment variables. Will be modified if any settings in command. * @return string Command stripped of any environment variable settings. */ function _proc_open_compat_win_env( $cmd, &$env ) { if ( false !== strpos( $cmd, '=' ) ) { while ( preg_match( '/^([A-Za-z_][A-Za-z0-9_]*)=("[^"]*"|[^ ]*) /', $cmd, $matches ) ) { $cmd = substr( $cmd, strlen( $matches[0] ) ); if ( null === $env ) { $env = []; } $env[ $matches[1] ] = isset( $matches[2][0] ) && '"' === $matches[2][0] ? substr( $matches[2], 1, -1 ) : $matches[2]; } } return $cmd; } /** * First half of escaping for LIKE special characters % and _ before preparing for MySQL. * * Use this only before wpdb::prepare() or esc_sql(). Reversing the order is very bad for security. * * Copied from core "wp-includes/wp-db.php". Avoids dependency on WP 4.4 wpdb. * * @access public * * @param string $text The raw text to be escaped. The input typed by the user should have no * extra or deleted slashes. * @return string Text in the form of a LIKE phrase. The output is not SQL safe. Call $wpdb::prepare() * or real_escape next. */ function esc_like( $text ) { return addcslashes( $text, '_%\\' ); } /** * Escapes (backticks) MySQL identifiers (aka schema object names) - i.e. column names, table names, and database/index/alias/view etc names. * See https://dev.mysql.com/doc/refman/5.5/en/identifiers.html * * @param string|array $idents A single identifier or an array of identifiers. * @return string|array An escaped string if given a string, or an array of escaped strings if given an array of strings. */ function esc_sql_ident( $idents ) { $backtick = static function ( $v ) { // Escape any backticks in the identifier by doubling. return '`' . str_replace( '`', '``', $v ) . '`'; }; if ( is_string( $idents ) ) { return $backtick( $idents ); } return array_map( $backtick, $idents ); } /** * Check whether a given string is a valid JSON representation. * * @param string $argument String to evaluate. * @param bool $ignore_scalars Optional. Whether to ignore scalar values. * Defaults to true. * @return bool Whether the provided string is a valid JSON representation. */ function is_json( $argument, $ignore_scalars = true ) { if ( ! is_string( $argument ) || '' === $argument ) { return false; } if ( $ignore_scalars && ! in_array( $argument[0], [ '{', '[' ], true ) ) { return false; } json_decode( $argument, $assoc = true ); return json_last_error() === JSON_ERROR_NONE; } /** * Parse known shell arrays included in the $assoc_args array. * * @param array $assoc_args Associative array of arguments. * @param array $array_arguments Array of argument keys that should receive an * array through the shell. * @return array */ function parse_shell_arrays( $assoc_args, $array_arguments ) { if ( empty( $assoc_args ) || empty( $array_arguments ) ) { return $assoc_args; } foreach ( $array_arguments as $key ) { if ( array_key_exists( $key, $assoc_args ) && is_json( $assoc_args[ $key ] ) ) { $assoc_args[ $key ] = json_decode( $assoc_args[ $key ], $assoc = true ); } } return $assoc_args; } /** * Describe a callable as a string. * * @param callable $callable The callable to describe. * @return string String description of the callable. */ function describe_callable( $callable ) { try { if ( $callable instanceof Closure ) { $reflection = new ReflectionFunction( $callable ); return "Closure in file {$reflection->getFileName()} at line {$reflection->getStartLine()}"; } if ( is_array( $callable ) ) { if ( is_object( $callable[0] ) ) { return sprintf( '%s->%s()', get_class( $callable[0] ), $callable[1] ); } return sprintf( '%s::%s()', $callable[0], $callable[1] ); } return gettype( $callable ); } catch ( Exception $exception ) { return 'Callable of unknown type'; } } /** * Checks if the given class and method pair is a valid callable. * * This accommodates changes to `is_callable()` in PHP 8 that mean an array of a * classname and instance method is no longer callable. * * @param array $pair The class and method pair to check. * @return bool */ function is_valid_class_and_method_pair( $pair ) { if ( ! is_array( $pair ) || 2 !== count( $pair ) ) { return false; } if ( ! is_string( $pair[0] ) || ! is_string( $pair[1] ) ) { return false; } if ( ! class_exists( $pair[0] ) ) { return false; } if ( ! method_exists( $pair[0], $pair[1] ) ) { return false; } return true; } /** * Pluralizes a noun in a grammatically correct way. * * @param string $noun Noun to be pluralized. Needs to be in singular form. * @param int|null $count Optional. Count of the nouns, to decide whether to * pluralize. Will pluralize unconditionally if none * provided. * @return string Pluralized noun. */ function pluralize( $noun, $count = null ) { if ( 1 === $count ) { return $noun; } return Inflector::pluralize( $noun ); } /** * Get the path to the mysql binary. * * @return string Path to the mysql binary, or an empty string if not found. */ function get_mysql_binary_path() { static $path = null; if ( null === $path ) { $result = Process::create( '/usr/bin/env which mysql', null, null )->run(); if ( 0 !== $result->return_code ) { $path = ''; } else { $path = trim( $result->stdout ); } } return $path; } /** * Get the version of the MySQL database. * * @return string Version of the MySQL database, or an empty string if not * found. */ function get_mysql_version() { static $version = null; if ( null === $version ) { $result = Process::create( '/usr/bin/env mysql --version', null, null )->run(); if ( 0 !== $result->return_code ) { $version = ''; } else { $version = trim( $result->stdout ); } } return $version; } /** * Get the SQL modes of the MySQL session. * * @return string[] Array of SQL modes, or an empty array if they couldn't be * read. */ function get_sql_modes() { static $sql_modes = null; if ( null === $sql_modes ) { $result = Process::create( '/usr/bin/env mysql --no-auto-rehash --batch --skip-column-names --execute="SELECT @@SESSION.sql_mode"', null, null )->run(); if ( 0 !== $result->return_code ) { $sql_modes = []; } else { $sql_modes = array_filter( array_map( 'trim', preg_split( "/\r\n|\n|\r/", $result->stdout ) ) ); } } return $sql_modes; } add_namespace( 'WP_CLI\Bootstrap', WP_CLI_ROOT . '/php/WP_CLI/Bootstrap' )->register(); } /** * Initialize and return the bootstrap state to pass from step to step. * * @return BootstrapState */ function initialize_bootstrap_state() { return new BootstrapState(); } /** * Process the bootstrapping steps. * * Loops over each of the provided steps, instantiates it and then calls its * `process()` method. */ function bootstrap() { prepare_bootstrap(); $state = initialize_bootstrap_state(); foreach ( get_bootstrap_steps() as $step ) { /** @var BootstrapStep $step_instance */ $step_instance = new $step(); $state = $step_instance->process( $state ); } } [ 'runtime' => '=', 'file' => '', 'desc' => 'Path to the WordPress files.', ], 'url' => [ 'runtime' => '=', 'file' => '', 'desc' => 'Pretend request came from given URL. In multisite, this argument is how the target site is specified.', ], 'ssh' => [ 'runtime' => '=[:][@][:][]', 'file' => '[:][@][:][]', 'desc' => 'Perform operation against a remote server over SSH (or a container using scheme of "docker", "docker-compose", "vagrant").', ], 'http' => [ 'runtime' => '=', 'file' => '', 'desc' => 'Perform operation against a remote WordPress installation over HTTP.', ], 'blog' => [ 'deprecated' => 'Use --url instead.', 'runtime' => '=', ], 'user' => [ 'runtime' => '=', 'file' => '', 'desc' => 'Set the WordPress user.', ], 'skip-plugins' => [ 'runtime' => '[=]', 'file' => '', 'desc' => 'Skip loading all plugins, or a comma-separated list of plugins. Note: mu-plugins are still loaded.', 'default' => '', ], 'skip-themes' => [ 'runtime' => '[=]', 'file' => '', 'desc' => 'Skip loading all themes, or a comma-separated list of themes.', 'default' => '', ], 'skip-packages' => [ 'runtime' => '', 'file' => '', 'desc' => 'Skip loading all installed packages.', 'default' => false, ], 'require' => [ 'runtime' => '=', 'file' => '', 'desc' => 'Load PHP file before running the command (may be used more than once).', 'multiple' => true, 'default' => [], ], 'exec' => [ 'runtime' => '=', 'file' => '', 'desc' => 'Execute PHP code before running the command (may be used more than once).', 'multiple' => true, 'default' => [], ], 'context' => [ 'runtime' => '=', 'file' => '', 'default' => 'cli', 'desc' => 'Load WordPress in a given context.', ], 'disabled_commands' => [ 'file' => '', 'default' => [], 'desc' => '(Sub)commands to disable.', ], 'color' => [ 'runtime' => true, 'file' => '', 'default' => 'auto', 'desc' => 'Whether to colorize the output.', ], 'debug' => [ 'runtime' => '[=]', 'file' => '', 'default' => false, 'desc' => 'Show all PHP errors and add verbosity to WP-CLI output. Built-in groups include: bootstrap, commandfactory, and help.', ], 'prompt' => [ 'runtime' => '[=]', 'file' => false, 'default' => false, 'desc' => 'Prompt the user to enter values for all command arguments, or a subset specified as comma-separated values.', ], 'quiet' => [ 'runtime' => '', 'file' => '', 'default' => false, 'desc' => 'Suppress informational messages.', ], 'apache_modules' => [ 'file' => '', 'desc' => 'List of Apache Modules that are to be reported as loaded.', 'multiple' => true, 'default' => [], ], # --allow-root => (NOT RECOMMENDED) Allow wp-cli to run as root. This poses # a security risk, so you probably do not want to do this. 'allow-root' => [ 'file' => false, # Explicit. Just in case the default changes. 'runtime' => '', 'hidden' => true, ], ]; get_name() ); } while ( $command = $command->get_parent() ); return $path; } is_enabled() ) { WP_CLI::error( 'Cache directory does not exist.' ); } $cache->clear(); WP_CLI::success( 'Cache cleared.' ); } /** * Prunes the internal cache. * * Removes all cached files except for the newest version of each one. * * ## EXAMPLES * * $ wp cli cache prune * Success: Cache pruned. * * @subcommand prune */ public function cache_prune() { $cache = WP_CLI::get_cache(); if ( ! $cache->is_enabled() ) { WP_CLI::error( 'Cache directory does not exist.' ); } $cache->prune(); WP_CLI::success( 'Cache pruned.' ); } } ] * : Render output in a particular format. * --- * default: yaml * options: * - yaml * - json * - var_export * --- * * ## EXAMPLES * * # List all available aliases. * $ wp cli alias list * --- * @all: Run command against every registered alias. * @prod: * ssh: runcommand@runcommand.io~/webapps/production * @dev: * ssh: vagrant@192.168.50.10/srv/www/runcommand.dev * @both: * - @prod * - @dev * * @subcommand list */ public function list_( $args, $assoc_args ) { WP_CLI::print_value( WP_CLI::get_runner()->aliases, $assoc_args ); } /** * Gets the value for an alias. * * ## OPTIONS * * * : Key for the alias. * * ## EXAMPLES * * # Get alias. * $ wp cli alias get @prod * ssh: dev@somedeve.env:12345/home/dev/ */ public function get( $args, $assoc_args ) { list( $alias ) = $args; $aliases = WP_CLI::get_runner()->aliases; if ( empty( $aliases[ $alias ] ) ) { WP_CLI::error( "No alias found with key '{$alias}'." ); } foreach ( $aliases[ $alias ] as $key => $value ) { WP_CLI::log( "{$key}: {$value}" ); } } /** * Creates an alias. * * ## OPTIONS * * * : Key for the alias. * * [--set-user=] * : Set user for alias. * * [--set-url=] * : Set url for alias. * * [--set-path=] * : Set path for alias. * * [--set-ssh=] * : Set ssh for alias. * * [--set-http=] * : Set http for alias. * * [--grouping=] * : For grouping multiple aliases. * * [--config=] * : Config file to be considered for operations. * --- * default: global * options: * - global * - project * --- * * ## EXAMPLES * * # Add alias to global config. * $ wp cli alias add @prod --set-ssh=login@host --set-path=/path/to/wordpress/install/ --set-user=wpcli * Success: Added '@prod' alias. * * # Add alias to project config. * $ wp cli alias add @prod --set-ssh=login@host --set-path=/path/to/wordpress/install/ --set-user=wpcli --config=project * Success: Added '@prod' alias. * * # Add group of aliases. * $ wp cli alias add @multiservers --grouping=servera,serverb * Success: Added '@multiservers' alias. */ public function add( $args, $assoc_args ) { $config = ( ! empty( $assoc_args['config'] ) ? $assoc_args['config'] : 'global' ); list( $config_path, $aliases ) = $this->get_aliases_data( $config, '', true ); $this->validate_config_file( $config_path ); $alias = $args[0]; $grouping = Utils\get_flag_value( $assoc_args, 'grouping' ); $this->validate_input( $assoc_args, $grouping ); if ( isset( $aliases[ $alias ] ) ) { WP_CLI::error( "Key '{$alias}' exists already." ); } if ( null === $grouping ) { $aliases = $this->build_aliases( $aliases, $alias, $assoc_args, false ); } else { $aliases = $this->build_aliases( $aliases, $alias, $assoc_args, true, $grouping ); } $this->process_aliases( $aliases, $alias, $config_path, 'Added' ); } /** * Deletes an alias. * * ## OPTIONS * * * : Key for the alias. * * [--config=] * : Config file to be considered for operations. * --- * options: * - global * - project * --- * * ## EXAMPLES * * # Delete alias. * $ wp cli alias delete @prod * Success: Deleted '@prod' alias. * * # Delete project alias. * $ wp cli alias delete @prod --config=project * Success: Deleted '@prod' alias. */ public function delete( $args, $assoc_args ) { list( $alias ) = $args; $config = ( ! empty( $assoc_args['config'] ) ? $assoc_args['config'] : '' ); list( $config_path, $aliases ) = $this->get_aliases_data( $config, $alias ); $this->validate_config_file( $config_path ); if ( empty( $aliases[ $alias ] ) ) { WP_CLI::error( "No alias found with key '{$alias}'." ); } unset( $aliases[ $alias ] ); $this->process_aliases( $aliases, $alias, $config_path, 'Deleted' ); } /** * Updates an alias. * * ## OPTIONS * * * : Key for the alias. * * [--set-user=] * : Set user for alias. * * [--set-url=] * : Set url for alias. * * [--set-path=] * : Set path for alias. * * [--set-ssh=] * : Set ssh for alias. * * [--set-http=] * : Set http for alias. * * [--grouping=] * : For grouping multiple aliases. * * [--config=] * : Config file to be considered for operations. * --- * options: * - global * - project * --- * * ## EXAMPLES * * # Update alias. * $ wp cli alias update @prod --set-user=newuser --set-path=/new/path/to/wordpress/install/ * Success: Updated 'prod' alias. * * # Update project alias. * $ wp cli alias update @prod --set-user=newuser --set-path=/new/path/to/wordpress/install/ --config=project * Success: Updated 'prod' alias. */ public function update( $args, $assoc_args ) { $config = ( ! empty( $assoc_args['config'] ) ? $assoc_args['config'] : '' ); $alias = $args[0]; $grouping = Utils\get_flag_value( $assoc_args, 'grouping' ); list( $config_path, $aliases ) = $this->get_aliases_data( $config, $alias, true ); $this->validate_config_file( $config_path ); $this->validate_input( $assoc_args, $grouping ); if ( empty( $aliases[ $alias ] ) ) { WP_CLI::error( "No alias found with key '{$alias}'." ); } if ( null === $grouping ) { $aliases = $this->build_aliases( $aliases, $alias, $assoc_args, false, '', true ); } else { $aliases = $this->build_aliases( $aliases, $alias, $assoc_args, true, $grouping, true ); } $this->process_aliases( $aliases, $alias, $config_path, 'Updated' ); } /** * Get config path and aliases data based on config type. * * @param string $config Type of config to get data from. * @param string $alias Alias to be used for Add/Update/Delete. * @param bool $create_config_file Optional. If a config file doesn't exist, * should it be created? Defaults to false. * * @return array Config Path and Aliases in it. * @throws ExitException */ private function get_aliases_data( $config, $alias, $create_config_file = false ) { $global_config_path = WP_CLI::get_runner()->get_global_config_path( $create_config_file ); $global_aliases = Spyc::YAMLLoad( $global_config_path ); $project_config_path = WP_CLI::get_runner()->get_project_config_path(); $project_aliases = Spyc::YAMLLoad( $project_config_path ); if ( 'global' === $config ) { $config_path = $global_config_path; $aliases = $global_aliases; } elseif ( 'project' === $config ) { $config_path = $project_config_path; $aliases = $project_aliases; } else { $is_global_alias = array_key_exists( $alias, $global_aliases ); $is_project_alias = array_key_exists( $alias, $project_aliases ); if ( $is_global_alias && $is_project_alias ) { WP_CLI::error( "Key '{$alias}' found in more than one path. Please pass --config param." ); } elseif ( $is_global_alias ) { $config_path = $global_config_path; $aliases = $global_aliases; } else { $config_path = $project_config_path; $aliases = $project_aliases; } } return [ $config_path, $aliases ]; } /** * Check if the config file exists and is writable. * * @param string $config_path Path to config file. * * @return void */ private function validate_config_file( $config_path ) { if ( ! file_exists( $config_path ) || ! is_writable( $config_path ) ) { WP_CLI::error( "Config file does not exist: {$config_path}" ); } } /** * Return aliases array. * * @param array $aliases Current aliases data. * @param string $alias Name of alias. * @param array $assoc_args Associative arguments. * @param bool $is_grouping Check if its a grouping operation. * @param string $grouping Grouping value. * @param bool $is_update Is this an update operation? * * @return mixed */ private function build_aliases( $aliases, $alias, $assoc_args, $is_grouping, $grouping = '', $is_update = false ) { if ( $is_grouping ) { $valid_assoc_args = [ 'config', 'grouping' ]; $invalid_args = array_diff( array_keys( $assoc_args ), $valid_assoc_args ); // Check for invalid args. if ( ! empty( $invalid_args ) ) { $args_info = implode( ',', $invalid_args ); WP_CLI::error( "--grouping argument works alone. Found invalid arg(s) '$args_info'." ); } } if ( $is_update ) { $this->validate_alias_type( $aliases, $alias, $assoc_args, $grouping ); } if ( ! $is_grouping ) { foreach ( $assoc_args as $key => $value ) { if ( strpos( $key, 'set-' ) !== false ) { $alias_key_info = explode( '-', $key ); $alias_key = empty( $alias_key_info[1] ) ? '' : $alias_key_info[1]; if ( ! empty( $alias_key ) && ! empty( $value ) ) { $aliases[ $alias ][ $alias_key ] = $value; } } } } else { if ( ! empty( $grouping ) ) { $group_alias_list = explode( ',', $grouping ); $group_alias = array_map( function ( $current_alias ) { return '@' . ltrim( $current_alias, '@' ); }, $group_alias_list ); $aliases[ $alias ] = $group_alias; } } return $aliases; } /** * Validate input of passed arguments. * * @param array $assoc_args Arguments array. * @param string $grouping Grouping argument value. * * @throws ExitException */ private function validate_input( $assoc_args, $grouping ) { // Check if valid arguments were passed. $arg_match = preg_grep( '/^set-(\w+)/i', array_keys( $assoc_args ) ); // Verify passed-arguments. if ( empty( $grouping ) && empty( $arg_match ) ) { WP_CLI::error( 'No valid arguments passed.' ); } // Check whether passed arguments contain value or not. $assoc_arg_values = array_filter( array_intersect_key( $assoc_args, array_flip( $arg_match ) ) ); if ( empty( $grouping ) && empty( $assoc_arg_values ) ) { WP_CLI::error( 'No value passed to arguments.' ); } } /** * Validate alias type before update. * * @param array $aliases Existing aliases data. * @param string $alias Alias Name. * @param array $assoc_args Arguments array. * @param string $grouping Grouping argument value. * * @throws ExitException */ private function validate_alias_type( $aliases, $alias, $assoc_args, $grouping ) { $alias_data = $aliases[ $alias ]; $group_aliases_match = preg_grep( '/^@(\w+)/i', $alias_data ); $arg_match = preg_grep( '/^set-(\w+)/i', array_keys( $assoc_args ) ); if ( ! empty( $group_aliases_match ) && ! empty( $arg_match ) ) { WP_CLI::error( 'Trying to update group alias with invalid arguments.' ); } elseif ( empty( $group_aliases_match ) && ! empty( $grouping ) ) { WP_CLI::error( 'Trying to update simple alias with invalid --grouping argument.' ); } } /** * Save aliases data to config file. * * @param array $aliases Current aliases data. * @param string $alias Name of alias. * @param string $config_path Path to config file. * @param string $operation Current operation string fro message. */ private function process_aliases( $aliases, $alias, $config_path, $operation = '' ) { // Convert data to YAML string. $yaml_data = Spyc::YAMLDump( $aliases ); // Add data in config file. if ( file_put_contents( $config_path, $yaml_data ) ) { WP_CLI::success( "$operation '{$alias}' alias." ); } } } $command->get_name(), 'description' => $command->get_shortdesc(), 'longdesc' => $command->get_longdesc(), ]; foreach ( $command->get_subcommands() as $subcommand ) { $dump['subcommands'][] = $this->command_to_array( $subcommand ); } if ( empty( $dump['subcommands'] ) ) { $dump['synopsis'] = (string) $command->get_synopsis(); } return $dump; } /** * Prints WP-CLI version. * * ## EXAMPLES * * # Display CLI version. * $ wp cli version * WP-CLI 0.24.1 */ public function version() { WP_CLI::line( 'WP-CLI ' . WP_CLI_VERSION ); } /** * Prints various details about the WP-CLI environment. * * Helpful for diagnostic purposes, this command shares: * * * OS information. * * Shell information. * * PHP binary used. * * PHP binary version. * * php.ini configuration file used (which is typically different than web). * * WP-CLI root dir: where WP-CLI is installed (if non-Phar install). * * WP-CLI global config: where the global config YAML file is located. * * WP-CLI project config: where the project config YAML file is located. * * WP-CLI version: currently installed version. * * See [config docs](https://wp-cli.org/config/) for more details on global * and project config YAML files. * * ## OPTIONS * * [--format=] * : Render output in a particular format. * --- * default: list * options: * - list * - json * --- * * ## EXAMPLES * * # Display various data about the CLI environment. * $ wp cli info * OS: Linux 4.10.0-42-generic #46~16.04.1-Ubuntu SMP Mon Dec 4 15:57:59 UTC 2017 x86_64 * Shell: /usr/bin/zsh * PHP binary: /usr/bin/php * PHP version: 7.1.12-1+ubuntu16.04.1+deb.sury.org+1 * php.ini used: /etc/php/7.1/cli/php.ini * WP-CLI root dir: phar://wp-cli.phar * WP-CLI packages dir: /home/person/.wp-cli/packages/ * WP-CLI global config: * WP-CLI project config: * WP-CLI version: 1.5.0 */ public function info( $_, $assoc_args ) { // php_uname() $mode argument was only added with PHP 7.0+. Fall back to // entire string for older versions. $system_os = PHP_MAJOR_VERSION < 7 ? php_uname() : sprintf( '%s %s %s %s', php_uname( 's' ), php_uname( 'r' ), php_uname( 'v' ), php_uname( 'm' ) ); $shell = getenv( 'SHELL' ); if ( ! $shell && Utils\is_windows() ) { $shell = getenv( 'ComSpec' ); } $php_bin = Utils\get_php_binary(); $runner = WP_CLI::get_runner(); $packages_dir = $runner->get_packages_dir_path(); if ( ! is_dir( $packages_dir ) ) { $packages_dir = null; } if ( Utils\get_flag_value( $assoc_args, 'format' ) === 'json' ) { $info = [ 'system_os' => $system_os, 'shell' => $shell, 'php_binary_path' => $php_bin, 'php_version' => PHP_VERSION, 'php_ini_used' => get_cfg_var( 'cfg_file_path' ), 'mysql_binary_path' => Utils\get_mysql_binary_path(), 'mysql_version' => Utils\get_mysql_version(), 'sql_modes' => Utils\get_sql_modes(), 'wp_cli_dir_path' => WP_CLI_ROOT, 'wp_cli_vendor_path' => WP_CLI_VENDOR_DIR, 'wp_cli_phar_path' => defined( 'WP_CLI_PHAR_PATH' ) ? WP_CLI_PHAR_PATH : '', 'wp_cli_packages_dir_path' => $packages_dir, 'global_config_path' => $runner->global_config_path, 'project_config_path' => $runner->project_config_path, 'wp_cli_version' => WP_CLI_VERSION, ]; WP_CLI::line( json_encode( $info ) ); } else { WP_CLI::line( "OS:\t" . $system_os ); WP_CLI::line( "Shell:\t" . $shell ); WP_CLI::line( "PHP binary:\t" . $php_bin ); WP_CLI::line( "PHP version:\t" . PHP_VERSION ); WP_CLI::line( "php.ini used:\t" . get_cfg_var( 'cfg_file_path' ) ); WP_CLI::line( "MySQL binary:\t" . Utils\get_mysql_binary_path() ); WP_CLI::line( "MySQL version:\t" . Utils\get_mysql_version() ); WP_CLI::line( "SQL modes:\t" . implode( ',', Utils\get_sql_modes() ) ); WP_CLI::line( "WP-CLI root dir:\t" . WP_CLI_ROOT ); WP_CLI::line( "WP-CLI vendor dir:\t" . WP_CLI_VENDOR_DIR ); WP_CLI::line( "WP_CLI phar path:\t" . ( defined( 'WP_CLI_PHAR_PATH' ) ? WP_CLI_PHAR_PATH : '' ) ); WP_CLI::line( "WP-CLI packages dir:\t" . $packages_dir ); WP_CLI::line( "WP-CLI global config:\t" . $runner->global_config_path ); WP_CLI::line( "WP-CLI project config:\t" . $runner->project_config_path ); WP_CLI::line( "WP-CLI version:\t" . WP_CLI_VERSION ); } } /** * Checks to see if there is a newer version of WP-CLI available. * * Queries the Github releases API. Returns available versions if there are * updates available, or success message if using the latest release. * * ## OPTIONS * * [--patch] * : Only list patch updates. * * [--minor] * : Only list minor updates. * * [--major] * : Only list major updates. * * [--field=] * : Prints the value of a single field for each update. * * [--fields=] * : Limit the output to specific object fields. Defaults to version,update_type,package_url. * * [--format=] * : Render output in a particular format. * --- * default: table * options: * - table * - csv * - json * - count * - yaml * --- * * ## EXAMPLES * * # Check for update. * $ wp cli check-update * Success: WP-CLI is at the latest version. * * # Check for update and new version is available. * $ wp cli check-update * +---------+-------------+-------------------------------------------------------------------------------+ * | version | update_type | package_url | * +---------+-------------+-------------------------------------------------------------------------------+ * | 0.24.1 | patch | https://github.com/wp-cli/wp-cli/releases/download/v0.24.1/wp-cli-0.24.1.phar | * +---------+-------------+-------------------------------------------------------------------------------+ * * @subcommand check-update */ public function check_update( $_, $assoc_args ) { $updates = $this->get_updates( $assoc_args ); if ( $updates ) { $formatter = new Formatter( $assoc_args, [ 'version', 'update_type', 'package_url' ] ); $formatter->display_items( $updates ); } elseif ( empty( $assoc_args['format'] ) || 'table' === $assoc_args['format'] ) { $update_type = $this->get_update_type_str( $assoc_args ); WP_CLI::success( "WP-CLI is at the latest{$update_type}version." ); } } /** * Updates WP-CLI to the latest release. * * Default behavior is to check the releases API for the newest stable * version, and prompt if one is available. * * Use `--stable` to install or reinstall the latest stable version. * * Use `--nightly` to install the latest built version of the master branch. * While not recommended for production, nightly contains the latest and * greatest, and should be stable enough for development and staging * environments. * * Only works for the Phar installation mechanism. * * ## OPTIONS * * [--patch] * : Only perform patch updates. * * [--minor] * : Only perform minor updates. * * [--major] * : Only perform major updates. * * [--stable] * : Update to the latest stable release. Skips update check. * * [--nightly] * : Update to the latest built version of the master branch. Potentially unstable. * * [--yes] * : Do not prompt for confirmation. * * [--insecure] * : Retry without certificate validation if TLS handshake fails. Note: This makes the request vulnerable to a MITM attack. * * ## EXAMPLES * * # Update CLI. * $ wp cli update * You have version 0.24.0. Would you like to update to 0.24.1? [y/n] y * Downloading from https://github.com/wp-cli/wp-cli/releases/download/v0.24.1/wp-cli-0.24.1.phar... * New version works. Proceeding to replace. * Success: Updated WP-CLI to 0.24.1. */ public function update( $_, $assoc_args ) { if ( ! Utils\inside_phar() ) { WP_CLI::error( 'You can only self-update Phar files.' ); } $old_phar = realpath( $_SERVER['argv'][0] ); if ( ! is_writable( $old_phar ) ) { WP_CLI::error( sprintf( '%s is not writable by current user.', $old_phar ) ); } elseif ( ! is_writable( dirname( $old_phar ) ) ) { WP_CLI::error( sprintf( '%s is not writable by current user.', dirname( $old_phar ) ) ); } if ( Utils\get_flag_value( $assoc_args, 'nightly' ) ) { WP_CLI::confirm( sprintf( 'You have version %s. Would you like to update to the latest nightly?', WP_CLI_VERSION ), $assoc_args ); $download_url = 'https://raw.githubusercontent.com/wp-cli/builds/gh-pages/phar/wp-cli-nightly.phar'; $md5_url = 'https://raw.githubusercontent.com/wp-cli/builds/gh-pages/phar/wp-cli-nightly.phar.md5'; } elseif ( Utils\get_flag_value( $assoc_args, 'stable' ) ) { WP_CLI::confirm( sprintf( 'You have version %s. Would you like to update to the latest stable release?', WP_CLI_VERSION ), $assoc_args ); $download_url = 'https://raw.githubusercontent.com/wp-cli/builds/gh-pages/phar/wp-cli.phar'; $md5_url = 'https://raw.githubusercontent.com/wp-cli/builds/gh-pages/phar/wp-cli.phar.md5'; } else { $updates = $this->get_updates( $assoc_args ); if ( empty( $updates ) ) { $update_type = $this->get_update_type_str( $assoc_args ); WP_CLI::success( "WP-CLI is at the latest{$update_type}version." ); return; } $newest = $updates[0]; WP_CLI::confirm( sprintf( 'You have version %s. Would you like to update to %s?', WP_CLI_VERSION, $newest['version'] ), $assoc_args ); $download_url = $newest['package_url']; $md5_url = str_replace( '.phar', '.phar.md5', $download_url ); } WP_CLI::log( sprintf( 'Downloading from %s...', $download_url ) ); $temp = Utils\get_temp_dir() . uniqid( 'wp_', true ) . '.phar'; $headers = []; $options = [ 'timeout' => 600, // 10 minutes ought to be enough for everybody. 'filename' => $temp, 'insecure' => (bool) Utils\get_flag_value( $assoc_args, 'insecure', false ), ]; Utils\http_request( 'GET', $download_url, null, $headers, $options ); unset( $options['filename'] ); $md5_response = Utils\http_request( 'GET', $md5_url, null, $headers, $options ); if ( '20' !== substr( $md5_response->status_code, 0, 2 ) ) { WP_CLI::error( "Couldn't access md5 hash for release (HTTP code {$md5_response->status_code})." ); } $md5_file = md5_file( $temp ); $release_hash = trim( $md5_response->body ); if ( $md5_file === $release_hash ) { WP_CLI::log( 'md5 hash verified: ' . $release_hash ); } else { WP_CLI::error( "md5 hash for download ({$md5_file}) is different than the release hash ({$release_hash})." ); } $allow_root = WP_CLI::get_runner()->config['allow-root'] ? '--allow-root' : ''; $php_binary = Utils\get_php_binary(); $process = Process::create( "{$php_binary} $temp --info {$allow_root}" ); $result = $process->run(); if ( 0 !== $result->return_code || false === stripos( $result->stdout, 'WP-CLI version' ) ) { $multi_line = explode( PHP_EOL, $result->stderr ); WP_CLI::error_multi_line( $multi_line ); WP_CLI::error( 'The downloaded PHAR is broken, try running wp cli update again.' ); } WP_CLI::log( 'New version works. Proceeding to replace.' ); $mode = fileperms( $old_phar ) & 511; if ( false === chmod( $temp, $mode ) ) { WP_CLI::error( sprintf( 'Cannot chmod %s.', $temp ) ); } class_exists( '\cli\Colors' ); // This autoloads \cli\Colors - after we move the file we no longer have access to this class. if ( false === rename( $temp, $old_phar ) ) { WP_CLI::error( sprintf( 'Cannot move %s to %s', $temp, $old_phar ) ); } if ( Utils\get_flag_value( $assoc_args, 'nightly', false ) ) { $updated_version = 'the latest nightly release'; } elseif ( Utils\get_flag_value( $assoc_args, 'stable', false ) ) { $updated_version = 'the latest stable release'; } else { $updated_version = isset( $newest['version'] ) ? $newest['version'] : ''; } WP_CLI::success( sprintf( 'Updated WP-CLI to %s.', $updated_version ) ); } /** * Returns update information. */ private function get_updates( $assoc_args ) { $url = 'https://api.github.com/repos/wp-cli/wp-cli/releases?per_page=100'; $options = [ 'timeout' => 30, 'insecure' => (bool) Utils\get_flag_value( $assoc_args, 'insecure', false ), ]; $headers = [ 'Accept' => 'application/json', ]; $github_token = getenv( 'GITHUB_TOKEN' ); if ( false !== $github_token ) { $headers['Authorization'] = 'token ' . $github_token; } $response = Utils\http_request( 'GET', $url, null, $headers, $options ); if ( ! $response->success || 200 !== $response->status_code ) { WP_CLI::error( sprintf( 'Failed to get latest version (HTTP code %d).', $response->status_code ) ); } $release_data = json_decode( $response->body ); $updates = [ 'major' => false, 'minor' => false, 'patch' => false, ]; foreach ( $release_data as $release ) { // Get rid of leading "v" if there is one set. $release_version = $release->tag_name; if ( 'v' === substr( $release_version, 0, 1 ) ) { $release_version = ltrim( $release_version, 'v' ); } $update_type = Utils\get_named_sem_ver( $release_version, WP_CLI_VERSION ); if ( ! $update_type ) { continue; } if ( ! isset( $release->assets[0]->browser_download_url ) ) { continue; } if ( ! empty( $updates[ $update_type ] ) && ! Comparator::greaterThan( $release_version, $updates[ $update_type ]['version'] ) ) { continue; } $updates[ $update_type ] = [ 'version' => $release_version, 'update_type' => $update_type, 'package_url' => $release->assets[0]->browser_download_url, ]; } foreach ( $updates as $type => $value ) { if ( empty( $value ) ) { unset( $updates[ $type ] ); } } foreach ( [ 'major', 'minor', 'patch' ] as $type ) { if ( true === Utils\get_flag_value( $assoc_args, $type ) ) { return ! empty( $updates[ $type ] ) ? [ $updates[ $type ] ] : false; } } if ( empty( $updates ) && preg_match( '#-alpha-(.+)$#', WP_CLI_VERSION, $matches ) ) { $version_url = 'https://raw.githubusercontent.com/wp-cli/builds/gh-pages/phar/NIGHTLY_VERSION'; $response = Utils\http_request( 'GET', $version_url, null, [], $options ); if ( ! $response->success || 200 !== $response->status_code ) { WP_CLI::error( sprintf( 'Failed to get current nightly version (HTTP code %d)', $response->status_code ) ); } $nightly_version = trim( $response->body ); if ( WP_CLI_VERSION !== $nightly_version ) { $updates['nightly'] = [ 'version' => $nightly_version, 'update_type' => 'nightly', 'package_url' => 'https://raw.githubusercontent.com/wp-cli/builds/gh-pages/phar/wp-cli-nightly.phar', ]; } } return array_values( $updates ); } /** * Dumps the list of global parameters, as JSON or in var_export format. * * ## OPTIONS * * [--with-values] * : Display current values also. * * [--format=] * : Render output in a particular format. * --- * default: json * options: * - var_export * - json * --- * * ## EXAMPLES * * # Dump the list of global parameters. * $ wp cli param-dump --format=var_export * array ( * 'path' => * array ( * 'runtime' => '=', * 'file' => '', * 'synopsis' => '', * 'default' => NULL, * 'multiple' => false, * 'desc' => 'Path to the WordPress files.', * ), * 'url' => * array ( * * @subcommand param-dump */ public function param_dump( $_, $assoc_args ) { $spec = WP_CLI::get_configurator()->get_spec(); if ( Utils\get_flag_value( $assoc_args, 'with-values' ) ) { $config = WP_CLI::get_configurator()->to_array(); // Copy current config values to $spec. foreach ( $spec as $key => $value ) { $current = null; if ( isset( $config[0][ $key ] ) ) { $current = $config[0][ $key ]; } $spec[ $key ]['current'] = $current; } } if ( 'var_export' === Utils\get_flag_value( $assoc_args, 'format' ) ) { var_export( $spec ); } else { echo json_encode( $spec ); } } /** * Dumps the list of installed commands, as JSON. * * ## EXAMPLES * * # Dump the list of installed commands. * $ wp cli cmd-dump * {"name":"wp","description":"Manage WordPress through the command-line.","longdesc":"\n\n## GLOBAL PARAMETERS\n\n --path=\n Path to the WordPress files.\n\n --ssh=\n Perform operation against a remote server over SSH (or a container using scheme of "docker" or "docker-compose").\n\n --url=\n Pretend request came from given URL. In multisite, this argument is how the target site is specified. \n\n --user=\n * * @subcommand cmd-dump */ public function cmd_dump() { echo json_encode( $this->command_to_array( WP_CLI::get_root_command() ) ); } /** * Generates tab completion strings. * * ## OPTIONS * * --line= * : The current command line to be executed. * * --point= * : The index to the current cursor position relative to the beginning of the command. * * ## EXAMPLES * * # Generate tab completion strings. * $ wp cli completions --line='wp eva' --point=100 * eval * eval-file */ public function completions( $_, $assoc_args ) { $line = substr( $assoc_args['line'], 0, $assoc_args['point'] ); $compl = new Completions( $line ); $compl->render(); } /** * Get a string representing the type of update being checked for. */ private function get_update_type_str( $assoc_args ) { $update_type = ' '; foreach ( [ 'major', 'minor', 'patch' ] as $type ) { if ( true === Utils\get_flag_value( $assoc_args, $type ) ) { $update_type = ' ' . $type . ' '; break; } } return $update_type; } /** * Detects if a command exists * * This commands checks if a command is registered with WP-CLI. * If the command is found then it returns with exit status 0. * If the command doesn't exist, then it will exit with status 1. * * ## OPTIONS * ... * : The command * * ## EXAMPLES * * # The "site delete" command is registered. * $ wp cli has-command "site delete" * $ echo $? * 0 * * # The "foo bar" command is not registered. * $ wp cli has-command "foo bar" * $ echo $? * 1 * * @subcommand has-command * * @when after_wp_load */ public function has_command( $_, $assoc_args ) { // If command is input as a string, then explode it into array. $command = explode( ' ', implode( ' ', $_ ) ); WP_CLI::halt( is_array( WP_CLI::get_runner()->find_command_to_run( $command ) ) ? 0 : 1 ); } } ...] * : Get help on a specific command. * * ## EXAMPLES * * # get help for `core` command * wp help core * * # get help for `core download` subcommand * wp help core download */ public function __invoke( $args, $assoc_args ) { $r = WP_CLI::get_runner()->find_command_to_run( $args ); if ( is_array( $r ) ) { list( $command ) = $r; self::show_help( $command ); exit; } } private static function show_help( $command ) { $out = self::get_initial_markdown( $command ); // Remove subcommands if in columns - will wordwrap separately. $subcommands = ''; $column_subpattern = '[ \t]+[^\t]+\t+'; if ( preg_match( '/(^## SUBCOMMANDS[^\n]*\n+' . $column_subpattern . '.+?)(?:^##|\z)/ms', $out, $matches, PREG_OFFSET_CAPTURE ) ) { $subcommands = $matches[1][0]; $subcommands_header = "## SUBCOMMANDS\n"; $out = substr_replace( $out, $subcommands_header, $matches[1][1], strlen( $subcommands ) ); } $out .= self::parse_reference_links( $command->get_longdesc() ); // Definition lists. $out = preg_replace_callback( '/([^\n]+)\n: (.+?)(\n\n|$)/s', [ __CLASS__, 'rewrap_param_desc' ], $out ); // Ensure lines with no leading whitespace that aren't section headers are indented. $out = preg_replace( '/^((?! |\t|##).)/m', "\t$1", $out ); $tab = str_repeat( ' ', 2 ); // Need to de-tab for wordwrapping to work properly. $out = str_replace( "\t", $tab, $out ); $wordwrap_width = Shell::columns(); // Wordwrap with indent. $out = preg_replace_callback( '/^( *)([^\n]+)\n/m', function ( $matches ) use ( $wordwrap_width ) { return $matches[1] . str_replace( "\n", "\n{$matches[1]}", wordwrap( $matches[2], $wordwrap_width - strlen( $matches[1] ) ) ) . "\n"; }, $out ); if ( $subcommands ) { // Wordwrap with column indent. $subcommands = preg_replace_callback( '/^(' . $column_subpattern . ')([^\n]+)\n/m', function ( $matches ) use ( $wordwrap_width, $tab ) { // Need to de-tab for wordwrapping to work properly. $matches[1] = str_replace( "\t", $tab, $matches[1] ); $matches[2] = str_replace( "\t", $tab, $matches[2] ); $padding_len = strlen( $matches[1] ); $padding = str_repeat( ' ', $padding_len ); return $matches[1] . str_replace( "\n", "\n$padding", wordwrap( $matches[2], $wordwrap_width - $padding_len ) ) . "\n"; }, $subcommands ); // Put subcommands back. $out = str_replace( $subcommands_header, $subcommands, $out ); } // Section headers. $out = preg_replace( '/^## ([A-Z ]+)/m', WP_CLI::colorize( '%9\1%n' ), $out ); self::pass_through_pager( $out ); } private static function rewrap_param_desc( $matches ) { $param = $matches[1]; $desc = self::indent( "\t\t", $matches[2] ); return "\t$param\n$desc\n\n"; } private static function indent( $whitespace, $text ) { $lines = explode( "\n", $text ); foreach ( $lines as &$line ) { $line = $whitespace . $line; } return implode( "\n", $lines ); } private static function pass_through_pager( $out ) { if ( ! Utils\check_proc_available( null /*context*/, true /*return*/ ) ) { WP_CLI::line( $out ); WP_CLI::debug( 'Warning: check_proc_available() failed in pass_through_pager().', 'help' ); return -1; } $pager = getenv( 'PAGER' ); if ( false === $pager ) { $pager = Utils\is_windows() ? 'more' : 'less -r'; } // For Windows 7 need to set code page to something other than Unicode (65001) to get around "Not enough memory." error with `more.com` on PHP 7.1+. if ( 'more' === $pager && defined( 'PHP_WINDOWS_VERSION_MAJOR' ) && PHP_WINDOWS_VERSION_MAJOR < 10 && function_exists( 'sapi_windows_cp_set' ) ) { // Note will also apply to Windows 8 (see https://msdn.microsoft.com/en-us/library/windows/desktop/ms724832.aspx) but probably harmless anyway. $cp = getenv( 'WP_CLI_WINDOWS_CODE_PAGE' ) ?: 1252; // Code page 1252 is the most used so probably the most compat. // phpcs:ignore PHPCompatibility.FunctionUse.NewFunctions -- Wrapped in function_exists() call. sapi_windows_cp_set( $cp ); // `sapi_windows_cp_set()` introduced PHP 7.1. } // Convert string to file handle. $fd = fopen( 'php://temp', 'r+b' ); fwrite( $fd, $out ); rewind( $fd ); $descriptorspec = [ 0 => $fd, 1 => STDOUT, 2 => STDERR, ]; return proc_close( Utils\proc_open_compat( $pager, $descriptorspec, $pipes ) ); } private static function get_initial_markdown( $command ) { $name = implode( ' ', Dispatcher\get_path( $command ) ); $binding = [ 'name' => $name, 'shortdesc' => $command->get_shortdesc(), ]; $binding['synopsis'] = "$name " . $command->get_synopsis(); $alias = $command->get_alias(); if ( $alias ) { $binding['alias'] = $alias; } if ( $command->can_have_subcommands() ) { $binding['has-subcommands']['subcommands'] = self::render_subcommands( $command ); } return Utils\mustache_render( 'man.mustache', $binding ); } private static function render_subcommands( $command ) { $subcommands = []; foreach ( $command->get_subcommands() as $subcommand ) { if ( WP_CLI::get_runner()->is_command_disabled( $subcommand ) ) { continue; } $subcommands[ $subcommand->get_name() ] = $subcommand->get_shortdesc(); } $max_len = self::get_max_len( array_keys( $subcommands ) ); $lines = []; foreach ( $subcommands as $name => $desc ) { $lines[] = str_pad( $name, $max_len ) . "\t\t\t" . $desc; } return $lines; } private static function get_max_len( $strings ) { $max_len = 0; foreach ( $strings as $str ) { $len = strlen( $str ); if ( $len > $max_len ) { $max_len = $len; } } return $max_len; } /** * Parse reference links from longdescription. * * @param string $longdesc The longdescription from the `$command->get_longdesc()`. * @return string The longdescription which has links as footnote. */ private static function parse_reference_links( $longdesc ) { $description = ''; foreach ( explode( "\n", $longdesc ) as $line ) { if ( 0 === strpos( $line, '#' ) ) { break; } $description .= $line . "\n"; } // Fires if it has description text at the head of `$longdesc`. if ( $description ) { $links = []; // An array of URLs from the description. $pattern = '/\[.+?\]\((https?:\/\/.+?)\)/'; $newdesc = preg_replace_callback( $pattern, function( $matches ) use ( &$links ) { static $count = 0; $count++; $links[] = $matches[1]; return str_replace( '(' . $matches[1] . ')', '[' . $count . ']', $matches[0] ); }, $description ); $footnote = ''; $link_count = count( $links ); for ( $i = 0; $i < $link_count; $i++ ) { $n = $i + 1; $footnote .= '[' . $n . '] ' . $links[ $i ] . "\n"; } if ( $footnote ) { $newdesc = trim( $newdesc ) . "\n\n---\n" . $footnote; $longdesc = str_replace( trim( $description ), trim( $newdesc ), $longdesc ); } } return $longdesc; } } */ class SymfonyClassCollectionLoader { private static $loaded; const HEADER = <<<'EOS' \s*$/'), '', file_get_contents($r->getFileName())); } $cache = $cacheDir . '/' . $name . $extension; $header = sprintf(self::HEADER, strftime('%Y')); self::writeCacheFile($cache, $header . substr(self::stripComments(' true, self::PRAGMA_BLOCKS => true, self::PRAGMA_ANCHORED_DOT => true, ); // Template cache private $templates = array(); // Environment private $templateClassPrefix = '__Mustache_'; private $cache; private $lambdaCache; private $cacheLambdaTemplates = false; private $loader; private $partialsLoader; private $helpers; private $escape; private $entityFlags = ENT_COMPAT; private $charset = 'UTF-8'; private $logger; private $strictCallables = false; private $pragmas = array(); private $delimiters; // Services private $tokenizer; private $parser; private $compiler; /** * Mustache class constructor. * * Passing an $options array allows overriding certain Mustache options during instantiation: * * $options = array( * // The class prefix for compiled templates. Defaults to '__Mustache_'. * 'template_class_prefix' => '__MyTemplates_', * * // A Mustache cache instance or a cache directory string for compiled templates. * // Mustache will not cache templates unless this is set. * 'cache' => dirname(__FILE__).'/tmp/cache/mustache', * * // Override default permissions for cache files. Defaults to using the system-defined umask. It is * // *strongly* recommended that you configure your umask properly rather than overriding permissions here. * 'cache_file_mode' => 0666, * * // Optionally, enable caching for lambda section templates. This is generally not recommended, as lambda * // sections are often too dynamic to benefit from caching. * 'cache_lambda_templates' => true, * * // Customize the tag delimiters used by this engine instance. Note that overriding here changes the * // delimiters used to parse all templates and partials loaded by this instance. To override just for a * // single template, use an inline "change delimiters" tag at the start of the template file: * // * // {{=<% %>=}} * // * 'delimiters' => '<% %>', * * // A Mustache template loader instance. Uses a StringLoader if not specified. * 'loader' => new Mustache_Loader_FilesystemLoader(dirname(__FILE__).'/views'), * * // A Mustache loader instance for partials. * 'partials_loader' => new Mustache_Loader_FilesystemLoader(dirname(__FILE__).'/views/partials'), * * // An array of Mustache partials. Useful for quick-and-dirty string template loading, but not as * // efficient or lazy as a Filesystem (or database) loader. * 'partials' => array('foo' => file_get_contents(dirname(__FILE__).'/views/partials/foo.mustache')), * * // An array of 'helpers'. Helpers can be global variables or objects, closures (e.g. for higher order * // sections), or any other valid Mustache context value. They will be prepended to the context stack, * // so they will be available in any template loaded by this Mustache instance. * 'helpers' => array('i18n' => function ($text) { * // do something translatey here... * }), * * // An 'escape' callback, responsible for escaping double-mustache variables. * 'escape' => function ($value) { * return htmlspecialchars($buffer, ENT_COMPAT, 'UTF-8'); * }, * * // Type argument for `htmlspecialchars`. Defaults to ENT_COMPAT. You may prefer ENT_QUOTES. * 'entity_flags' => ENT_QUOTES, * * // Character set for `htmlspecialchars`. Defaults to 'UTF-8'. Use 'UTF-8'. * 'charset' => 'ISO-8859-1', * * // A Mustache Logger instance. No logging will occur unless this is set. Using a PSR-3 compatible * // logging library -- such as Monolog -- is highly recommended. A simple stream logger implementation is * // available as well: * 'logger' => new Mustache_Logger_StreamLogger('php://stderr'), * * // Only treat Closure instances and invokable classes as callable. If true, values like * // `array('ClassName', 'methodName')` and `array($classInstance, 'methodName')`, which are traditionally * // "callable" in PHP, are not called to resolve variables for interpolation or section contexts. This * // helps protect against arbitrary code execution when user input is passed directly into the template. * // This currently defaults to false, but will default to true in v3.0. * 'strict_callables' => true, * * // Enable pragmas across all templates, regardless of the presence of pragma tags in the individual * // templates. * 'pragmas' => [Mustache_Engine::PRAGMA_FILTERS], * ); * * @throws Mustache_Exception_InvalidArgumentException If `escape` option is not callable * * @param array $options (default: array()) */ public function __construct(array $options = array()) { if (isset($options['template_class_prefix'])) { if ((string) $options['template_class_prefix'] === '') { throw new Mustache_Exception_InvalidArgumentException('Mustache Constructor "template_class_prefix" must not be empty'); } $this->templateClassPrefix = $options['template_class_prefix']; } if (isset($options['cache'])) { $cache = $options['cache']; if (is_string($cache)) { $mode = isset($options['cache_file_mode']) ? $options['cache_file_mode'] : null; $cache = new Mustache_Cache_FilesystemCache($cache, $mode); } $this->setCache($cache); } if (isset($options['cache_lambda_templates'])) { $this->cacheLambdaTemplates = (bool) $options['cache_lambda_templates']; } if (isset($options['loader'])) { $this->setLoader($options['loader']); } if (isset($options['partials_loader'])) { $this->setPartialsLoader($options['partials_loader']); } if (isset($options['partials'])) { $this->setPartials($options['partials']); } if (isset($options['helpers'])) { $this->setHelpers($options['helpers']); } if (isset($options['escape'])) { if (!is_callable($options['escape'])) { throw new Mustache_Exception_InvalidArgumentException('Mustache Constructor "escape" option must be callable'); } $this->escape = $options['escape']; } if (isset($options['entity_flags'])) { $this->entityFlags = $options['entity_flags']; } if (isset($options['charset'])) { $this->charset = $options['charset']; } if (isset($options['logger'])) { $this->setLogger($options['logger']); } if (isset($options['strict_callables'])) { $this->strictCallables = $options['strict_callables']; } if (isset($options['delimiters'])) { $this->delimiters = $options['delimiters']; } if (isset($options['pragmas'])) { foreach ($options['pragmas'] as $pragma) { if (!isset(self::$knownPragmas[$pragma])) { throw new Mustache_Exception_InvalidArgumentException(sprintf('Unknown pragma: "%s".', $pragma)); } $this->pragmas[$pragma] = true; } } } /** * Shortcut 'render' invocation. * * Equivalent to calling `$mustache->loadTemplate($template)->render($context);` * * @see Mustache_Engine::loadTemplate * @see Mustache_Template::render * * @param string $template * @param mixed $context (default: array()) * * @return string Rendered template */ public function render($template, $context = array()) { return $this->loadTemplate($template)->render($context); } /** * Get the current Mustache escape callback. * * @return callable|null */ public function getEscape() { return $this->escape; } /** * Get the current Mustache entitity type to escape. * * @return int */ public function getEntityFlags() { return $this->entityFlags; } /** * Get the current Mustache character set. * * @return string */ public function getCharset() { return $this->charset; } /** * Get the current globally enabled pragmas. * * @return array */ public function getPragmas() { return array_keys($this->pragmas); } /** * Set the Mustache template Loader instance. * * @param Mustache_Loader $loader */ public function setLoader(Mustache_Loader $loader) { $this->loader = $loader; } /** * Get the current Mustache template Loader instance. * * If no Loader instance has been explicitly specified, this method will instantiate and return * a StringLoader instance. * * @return Mustache_Loader */ public function getLoader() { if (!isset($this->loader)) { $this->loader = new Mustache_Loader_StringLoader(); } return $this->loader; } /** * Set the Mustache partials Loader instance. * * @param Mustache_Loader $partialsLoader */ public function setPartialsLoader(Mustache_Loader $partialsLoader) { $this->partialsLoader = $partialsLoader; } /** * Get the current Mustache partials Loader instance. * * If no Loader instance has been explicitly specified, this method will instantiate and return * an ArrayLoader instance. * * @return Mustache_Loader */ public function getPartialsLoader() { if (!isset($this->partialsLoader)) { $this->partialsLoader = new Mustache_Loader_ArrayLoader(); } return $this->partialsLoader; } /** * Set partials for the current partials Loader instance. * * @throws Mustache_Exception_RuntimeException If the current Loader instance is immutable * * @param array $partials (default: array()) */ public function setPartials(array $partials = array()) { if (!isset($this->partialsLoader)) { $this->partialsLoader = new Mustache_Loader_ArrayLoader(); } if (!$this->partialsLoader instanceof Mustache_Loader_MutableLoader) { throw new Mustache_Exception_RuntimeException('Unable to set partials on an immutable Mustache Loader instance'); } $this->partialsLoader->setTemplates($partials); } /** * Set an array of Mustache helpers. * * An array of 'helpers'. Helpers can be global variables or objects, closures (e.g. for higher order sections), or * any other valid Mustache context value. They will be prepended to the context stack, so they will be available in * any template loaded by this Mustache instance. * * @throws Mustache_Exception_InvalidArgumentException if $helpers is not an array or Traversable * * @param array|Traversable $helpers */ public function setHelpers($helpers) { if (!is_array($helpers) && !$helpers instanceof Traversable) { throw new Mustache_Exception_InvalidArgumentException('setHelpers expects an array of helpers'); } $this->getHelpers()->clear(); foreach ($helpers as $name => $helper) { $this->addHelper($name, $helper); } } /** * Get the current set of Mustache helpers. * * @see Mustache_Engine::setHelpers * * @return Mustache_HelperCollection */ public function getHelpers() { if (!isset($this->helpers)) { $this->helpers = new Mustache_HelperCollection(); } return $this->helpers; } /** * Add a new Mustache helper. * * @see Mustache_Engine::setHelpers * * @param string $name * @param mixed $helper */ public function addHelper($name, $helper) { $this->getHelpers()->add($name, $helper); } /** * Get a Mustache helper by name. * * @see Mustache_Engine::setHelpers * * @param string $name * * @return mixed Helper */ public function getHelper($name) { return $this->getHelpers()->get($name); } /** * Check whether this Mustache instance has a helper. * * @see Mustache_Engine::setHelpers * * @param string $name * * @return bool True if the helper is present */ public function hasHelper($name) { return $this->getHelpers()->has($name); } /** * Remove a helper by name. * * @see Mustache_Engine::setHelpers * * @param string $name */ public function removeHelper($name) { $this->getHelpers()->remove($name); } /** * Set the Mustache Logger instance. * * @throws Mustache_Exception_InvalidArgumentException If logger is not an instance of Mustache_Logger or Psr\Log\LoggerInterface * * @param Mustache_Logger|Psr\Log\LoggerInterface $logger */ public function setLogger($logger = null) { if ($logger !== null && !($logger instanceof Mustache_Logger || is_a($logger, 'Psr\\Log\\LoggerInterface'))) { throw new Mustache_Exception_InvalidArgumentException('Expected an instance of Mustache_Logger or Psr\\Log\\LoggerInterface.'); } if ($this->getCache()->getLogger() === null) { $this->getCache()->setLogger($logger); } $this->logger = $logger; } /** * Get the current Mustache Logger instance. * * @return Mustache_Logger|Psr\Log\LoggerInterface */ public function getLogger() { return $this->logger; } /** * Set the Mustache Tokenizer instance. * * @param Mustache_Tokenizer $tokenizer */ public function setTokenizer(Mustache_Tokenizer $tokenizer) { $this->tokenizer = $tokenizer; } /** * Get the current Mustache Tokenizer instance. * * If no Tokenizer instance has been explicitly specified, this method will instantiate and return a new one. * * @return Mustache_Tokenizer */ public function getTokenizer() { if (!isset($this->tokenizer)) { $this->tokenizer = new Mustache_Tokenizer(); } return $this->tokenizer; } /** * Set the Mustache Parser instance. * * @param Mustache_Parser $parser */ public function setParser(Mustache_Parser $parser) { $this->parser = $parser; } /** * Get the current Mustache Parser instance. * * If no Parser instance has been explicitly specified, this method will instantiate and return a new one. * * @return Mustache_Parser */ public function getParser() { if (!isset($this->parser)) { $this->parser = new Mustache_Parser(); } return $this->parser; } /** * Set the Mustache Compiler instance. * * @param Mustache_Compiler $compiler */ public function setCompiler(Mustache_Compiler $compiler) { $this->compiler = $compiler; } /** * Get the current Mustache Compiler instance. * * If no Compiler instance has been explicitly specified, this method will instantiate and return a new one. * * @return Mustache_Compiler */ public function getCompiler() { if (!isset($this->compiler)) { $this->compiler = new Mustache_Compiler(); } return $this->compiler; } /** * Set the Mustache Cache instance. * * @param Mustache_Cache $cache */ public function setCache(Mustache_Cache $cache) { if (isset($this->logger) && $cache->getLogger() === null) { $cache->setLogger($this->getLogger()); } $this->cache = $cache; } /** * Get the current Mustache Cache instance. * * If no Cache instance has been explicitly specified, this method will instantiate and return a new one. * * @return Mustache_Cache */ public function getCache() { if (!isset($this->cache)) { $this->setCache(new Mustache_Cache_NoopCache()); } return $this->cache; } /** * Get the current Lambda Cache instance. * * If 'cache_lambda_templates' is enabled, this is the default cache instance. Otherwise, it is a NoopCache. * * @see Mustache_Engine::getCache * * @return Mustache_Cache */ protected function getLambdaCache() { if ($this->cacheLambdaTemplates) { return $this->getCache(); } if (!isset($this->lambdaCache)) { $this->lambdaCache = new Mustache_Cache_NoopCache(); } return $this->lambdaCache; } /** * Helper method to generate a Mustache template class. * * This method must be updated any time options are added which make it so * the same template could be parsed and compiled multiple different ways. * * @param string|Mustache_Source $source * * @return string Mustache Template class name */ public function getTemplateClassName($source) { // For the most part, adding a new option here should do the trick. // // Pick a value here which is unique for each possible way the template // could be compiled... but not necessarily unique per option value. See // escape below, which only needs to differentiate between 'custom' and // 'default' escapes. // // Keep this list in alphabetical order :) $chunks = array( 'charset' => $this->charset, 'delimiters' => $this->delimiters ? $this->delimiters : '{{ }}', 'entityFlags' => $this->entityFlags, 'escape' => isset($this->escape) ? 'custom' : 'default', 'key' => ($source instanceof Mustache_Source) ? $source->getKey() : 'source', 'pragmas' => $this->getPragmas(), 'strictCallables' => $this->strictCallables, 'version' => self::VERSION, ); $key = json_encode($chunks); // Template Source instances have already provided their own source key. For strings, just include the whole // source string in the md5 hash. if (!$source instanceof Mustache_Source) { $key .= "\n" . $source; } return $this->templateClassPrefix . md5($key); } /** * Load a Mustache Template by name. * * @param string $name * * @return Mustache_Template */ public function loadTemplate($name) { return $this->loadSource($this->getLoader()->load($name)); } /** * Load a Mustache partial Template by name. * * This is a helper method used internally by Template instances for loading partial templates. You can most likely * ignore it completely. * * @param string $name * * @return Mustache_Template */ public function loadPartial($name) { try { if (isset($this->partialsLoader)) { $loader = $this->partialsLoader; } elseif (isset($this->loader) && !$this->loader instanceof Mustache_Loader_StringLoader) { $loader = $this->loader; } else { throw new Mustache_Exception_UnknownTemplateException($name); } return $this->loadSource($loader->load($name)); } catch (Mustache_Exception_UnknownTemplateException $e) { // If the named partial cannot be found, log then return null. $this->log( Mustache_Logger::WARNING, 'Partial not found: "{name}"', array('name' => $e->getTemplateName()) ); } } /** * Load a Mustache lambda Template by source. * * This is a helper method used by Template instances to generate subtemplates for Lambda sections. You can most * likely ignore it completely. * * @param string $source * @param string $delims (default: null) * * @return Mustache_Template */ public function loadLambda($source, $delims = null) { if ($delims !== null) { $source = $delims . "\n" . $source; } return $this->loadSource($source, $this->getLambdaCache()); } /** * Instantiate and return a Mustache Template instance by source. * * Optionally provide a Mustache_Cache instance. This is used internally by Mustache_Engine::loadLambda to respect * the 'cache_lambda_templates' configuration option. * * @see Mustache_Engine::loadTemplate * @see Mustache_Engine::loadPartial * @see Mustache_Engine::loadLambda * * @param string|Mustache_Source $source * @param Mustache_Cache $cache (default: null) * * @return Mustache_Template */ private function loadSource($source, Mustache_Cache $cache = null) { $className = $this->getTemplateClassName($source); if (!isset($this->templates[$className])) { if ($cache === null) { $cache = $this->getCache(); } if (!class_exists($className, false)) { if (!$cache->load($className)) { $compiled = $this->compile($source); $cache->cache($className, $compiled); } } $this->log( Mustache_Logger::DEBUG, 'Instantiating template: "{className}"', array('className' => $className) ); $this->templates[$className] = new $className($this); } return $this->templates[$className]; } /** * Helper method to tokenize a Mustache template. * * @see Mustache_Tokenizer::scan * * @param string $source * * @return array Tokens */ private function tokenize($source) { return $this->getTokenizer()->scan($source, $this->delimiters); } /** * Helper method to parse a Mustache template. * * @see Mustache_Parser::parse * * @param string $source * * @return array Token tree */ private function parse($source) { $parser = $this->getParser(); $parser->setPragmas($this->getPragmas()); return $parser->parse($this->tokenize($source)); } /** * Helper method to compile a Mustache template. * * @see Mustache_Compiler::compile * * @param string|Mustache_Source $source * * @return string generated Mustache template class code */ private function compile($source) { $name = $this->getTemplateClassName($source); $this->log( Mustache_Logger::INFO, 'Compiling template to "{className}" class', array('className' => $name) ); if ($source instanceof Mustache_Source) { $source = $source->getSource(); } $tree = $this->parse($source); $compiler = $this->getCompiler(); $compiler->setPragmas($this->getPragmas()); return $compiler->compile($source, $tree, $name, isset($this->escape), $this->charset, $this->strictCallables, $this->entityFlags); } /** * Add a log record if logging is enabled. * * @param int $level The logging level * @param string $message The log message * @param array $context The log context */ private function log($level, $message, array $context = array()) { if (isset($this->logger)) { $this->logger->log($level, $message, $context); } } } stack = array($context); } } /** * Push a new Context frame onto the stack. * * @param mixed $value Object or array to use for context */ public function push($value) { array_push($this->stack, $value); } /** * Push a new Context frame onto the block context stack. * * @param mixed $value Object or array to use for block context */ public function pushBlockContext($value) { array_push($this->blockStack, $value); } /** * Pop the last Context frame from the stack. * * @return mixed Last Context frame (object or array) */ public function pop() { return array_pop($this->stack); } /** * Pop the last block Context frame from the stack. * * @return mixed Last block Context frame (object or array) */ public function popBlockContext() { return array_pop($this->blockStack); } /** * Get the last Context frame. * * @return mixed Last Context frame (object or array) */ public function last() { return end($this->stack); } /** * Find a variable in the Context stack. * * Starting with the last Context frame (the context of the innermost section), and working back to the top-level * rendering context, look for a variable with the given name: * * * If the Context frame is an associative array which contains the key $id, returns the value of that element. * * If the Context frame is an object, this will check first for a public method, then a public property named * $id. Failing both of these, it will try `__isset` and `__get` magic methods. * * If a value named $id is not found in any Context frame, returns an empty string. * * @param string $id Variable name * * @return mixed Variable value, or '' if not found */ public function find($id) { return $this->findVariableInStack($id, $this->stack); } /** * Find a 'dot notation' variable in the Context stack. * * Note that dot notation traversal bubbles through scope differently than the regular find method. After finding * the initial chunk of the dotted name, each subsequent chunk is searched for only within the value of the previous * result. For example, given the following context stack: * * $data = array( * 'name' => 'Fred', * 'child' => array( * 'name' => 'Bob' * ), * ); * * ... and the Mustache following template: * * {{ child.name }} * * ... the `name` value is only searched for within the `child` value of the global Context, not within parent * Context frames. * * @param string $id Dotted variable selector * * @return mixed Variable value, or '' if not found */ public function findDot($id) { $chunks = explode('.', $id); $first = array_shift($chunks); $value = $this->findVariableInStack($first, $this->stack); foreach ($chunks as $chunk) { if ($value === '') { return $value; } $value = $this->findVariableInStack($chunk, array($value)); } return $value; } /** * Find an 'anchored dot notation' variable in the Context stack. * * This is the same as findDot(), except it looks in the top of the context * stack for the first value, rather than searching the whole context stack * and starting from there. * * @see Mustache_Context::findDot * * @throws Mustache_Exception_InvalidArgumentException if given an invalid anchored dot $id * * @param string $id Dotted variable selector * * @return mixed Variable value, or '' if not found */ public function findAnchoredDot($id) { $chunks = explode('.', $id); $first = array_shift($chunks); if ($first !== '') { throw new Mustache_Exception_InvalidArgumentException(sprintf('Unexpected id for findAnchoredDot: %s', $id)); } $value = $this->last(); foreach ($chunks as $chunk) { if ($value === '') { return $value; } $value = $this->findVariableInStack($chunk, array($value)); } return $value; } /** * Find an argument in the block context stack. * * @param string $id * * @return mixed Variable value, or '' if not found */ public function findInBlock($id) { foreach ($this->blockStack as $context) { if (array_key_exists($id, $context)) { return $context[$id]; } } return ''; } /** * Helper function to find a variable in the Context stack. * * @see Mustache_Context::find * * @param string $id Variable name * @param array $stack Context stack * * @return mixed Variable value, or '' if not found */ private function findVariableInStack($id, array $stack) { for ($i = count($stack) - 1; $i >= 0; $i--) { $frame = &$stack[$i]; switch (gettype($frame)) { case 'object': if (!($frame instanceof Closure)) { // Note that is_callable() *will not work here* // See https://github.com/bobthecow/mustache.php/wiki/Magic-Methods if (method_exists($frame, $id)) { return $frame->$id(); } if (isset($frame->$id)) { return $frame->$id; } if ($frame instanceof ArrayAccess && isset($frame[$id])) { return $frame[$id]; } } break; case 'array': if (array_key_exists($id, $frame)) { return $frame[$id]; } break; } } return ''; } } pragmas = $this->defaultPragmas; $this->sections = array(); $this->blocks = array(); $this->source = $source; $this->indentNextLine = true; $this->customEscape = $customEscape; $this->entityFlags = $entityFlags; $this->charset = $charset; $this->strictCallables = $strictCallables; return $this->writeCode($tree, $name); } /** * Enable pragmas across all templates, regardless of the presence of pragma * tags in the individual templates. * * @internal Users should set global pragmas in Mustache_Engine, not here :) * * @param string[] $pragmas */ public function setPragmas(array $pragmas) { $this->pragmas = array(); foreach ($pragmas as $pragma) { $this->pragmas[$pragma] = true; } $this->defaultPragmas = $this->pragmas; } /** * Helper function for walking the Mustache token parse tree. * * @throws Mustache_Exception_SyntaxException upon encountering unknown token types * * @param array $tree Parse tree of Mustache tokens * @param int $level (default: 0) * * @return string Generated PHP source code */ private function walk(array $tree, $level = 0) { $code = ''; $level++; foreach ($tree as $node) { switch ($node[Mustache_Tokenizer::TYPE]) { case Mustache_Tokenizer::T_PRAGMA: $this->pragmas[$node[Mustache_Tokenizer::NAME]] = true; break; case Mustache_Tokenizer::T_SECTION: $code .= $this->section( $node[Mustache_Tokenizer::NODES], $node[Mustache_Tokenizer::NAME], isset($node[Mustache_Tokenizer::FILTERS]) ? $node[Mustache_Tokenizer::FILTERS] : array(), $node[Mustache_Tokenizer::INDEX], $node[Mustache_Tokenizer::END], $node[Mustache_Tokenizer::OTAG], $node[Mustache_Tokenizer::CTAG], $level ); break; case Mustache_Tokenizer::T_INVERTED: $code .= $this->invertedSection( $node[Mustache_Tokenizer::NODES], $node[Mustache_Tokenizer::NAME], isset($node[Mustache_Tokenizer::FILTERS]) ? $node[Mustache_Tokenizer::FILTERS] : array(), $level ); break; case Mustache_Tokenizer::T_PARTIAL: $code .= $this->partial( $node[Mustache_Tokenizer::NAME], isset($node[Mustache_Tokenizer::INDENT]) ? $node[Mustache_Tokenizer::INDENT] : '', $level ); break; case Mustache_Tokenizer::T_PARENT: $code .= $this->parent( $node[Mustache_Tokenizer::NAME], isset($node[Mustache_Tokenizer::INDENT]) ? $node[Mustache_Tokenizer::INDENT] : '', $node[Mustache_Tokenizer::NODES], $level ); break; case Mustache_Tokenizer::T_BLOCK_ARG: $code .= $this->blockArg( $node[Mustache_Tokenizer::NODES], $node[Mustache_Tokenizer::NAME], $node[Mustache_Tokenizer::INDEX], $node[Mustache_Tokenizer::END], $node[Mustache_Tokenizer::OTAG], $node[Mustache_Tokenizer::CTAG], $level ); break; case Mustache_Tokenizer::T_BLOCK_VAR: $code .= $this->blockVar( $node[Mustache_Tokenizer::NODES], $node[Mustache_Tokenizer::NAME], $node[Mustache_Tokenizer::INDEX], $node[Mustache_Tokenizer::END], $node[Mustache_Tokenizer::OTAG], $node[Mustache_Tokenizer::CTAG], $level ); break; case Mustache_Tokenizer::T_COMMENT: break; case Mustache_Tokenizer::T_ESCAPED: case Mustache_Tokenizer::T_UNESCAPED: case Mustache_Tokenizer::T_UNESCAPED_2: $code .= $this->variable( $node[Mustache_Tokenizer::NAME], isset($node[Mustache_Tokenizer::FILTERS]) ? $node[Mustache_Tokenizer::FILTERS] : array(), $node[Mustache_Tokenizer::TYPE] === Mustache_Tokenizer::T_ESCAPED, $level ); break; case Mustache_Tokenizer::T_TEXT: $code .= $this->text($node[Mustache_Tokenizer::VALUE], $level); break; default: throw new Mustache_Exception_SyntaxException(sprintf('Unknown token type: %s', $node[Mustache_Tokenizer::TYPE]), $node); } } return $code; } const KLASS = 'lambdaHelper = new Mustache_LambdaHelper($this->mustache, $context); $buffer = \'\'; %s return $buffer; } %s %s }'; const KLASS_NO_LAMBDAS = 'walk($tree); $sections = implode("\n", $this->sections); $blocks = implode("\n", $this->blocks); $klass = empty($this->sections) && empty($this->blocks) ? self::KLASS_NO_LAMBDAS : self::KLASS; $callable = $this->strictCallables ? $this->prepare(self::STRICT_CALLABLE) : ''; return sprintf($this->prepare($klass, 0, false, true), $name, $callable, $code, $sections, $blocks); } const BLOCK_VAR = ' $blockFunction = $context->findInBlock(%s); if (is_callable($blockFunction)) { $buffer .= call_user_func($blockFunction, $context); %s} '; const BLOCK_VAR_ELSE = '} else {%s'; /** * Generate Mustache Template inheritance block variable PHP source. * * @param array $nodes Array of child tokens * @param string $id Section name * @param int $start Section start offset * @param int $end Section end offset * @param string $otag Current Mustache opening tag * @param string $ctag Current Mustache closing tag * @param int $level * * @return string Generated PHP source code */ private function blockVar($nodes, $id, $start, $end, $otag, $ctag, $level) { $id = var_export($id, true); $else = $this->walk($nodes, $level); if ($else !== '') { $else = sprintf($this->prepare(self::BLOCK_VAR_ELSE, $level + 1, false, true), $else); } return sprintf($this->prepare(self::BLOCK_VAR, $level), $id, $else); } const BLOCK_ARG = '%s => array($this, \'block%s\'),'; /** * Generate Mustache Template inheritance block argument PHP source. * * @param array $nodes Array of child tokens * @param string $id Section name * @param int $start Section start offset * @param int $end Section end offset * @param string $otag Current Mustache opening tag * @param string $ctag Current Mustache closing tag * @param int $level * * @return string Generated PHP source code */ private function blockArg($nodes, $id, $start, $end, $otag, $ctag, $level) { $key = $this->block($nodes); $id = var_export($id, true); return sprintf($this->prepare(self::BLOCK_ARG, $level), $id, $key); } const BLOCK_FUNCTION = ' public function block%s($context) { $indent = $buffer = \'\';%s return $buffer; } '; /** * Generate Mustache Template inheritance block function PHP source. * * @param array $nodes Array of child tokens * * @return string key of new block function */ private function block($nodes) { $code = $this->walk($nodes, 0); $key = ucfirst(md5($code)); if (!isset($this->blocks[$key])) { $this->blocks[$key] = sprintf($this->prepare(self::BLOCK_FUNCTION, 0), $key, $code); } return $key; } const SECTION_CALL = ' $value = $context->%s(%s);%s $buffer .= $this->section%s($context, $indent, $value); '; const SECTION = ' private function section%s(Mustache_Context $context, $indent, $value) { $buffer = \'\'; if (%s) { $source = %s; $result = (string) call_user_func($value, $source, %s); if (strpos($result, \'{{\') === false) { $buffer .= $result; } else { $buffer .= $this->mustache ->loadLambda($result%s) ->renderInternal($context); } } elseif (!empty($value)) { $values = $this->isIterable($value) ? $value : array($value); foreach ($values as $value) { $context->push($value); %s $context->pop(); } } return $buffer; } '; /** * Generate Mustache Template section PHP source. * * @param array $nodes Array of child tokens * @param string $id Section name * @param string[] $filters Array of filters * @param int $start Section start offset * @param int $end Section end offset * @param string $otag Current Mustache opening tag * @param string $ctag Current Mustache closing tag * @param int $level * * @return string Generated section PHP source code */ private function section($nodes, $id, $filters, $start, $end, $otag, $ctag, $level) { $source = var_export(substr($this->source, $start, $end - $start), true); $callable = $this->getCallable(); if ($otag !== '{{' || $ctag !== '}}') { $delimTag = var_export(sprintf('{{= %s %s =}}', $otag, $ctag), true); $helper = sprintf('$this->lambdaHelper->withDelimiters(%s)', $delimTag); $delims = ', ' . $delimTag; } else { $helper = '$this->lambdaHelper'; $delims = ''; } $key = ucfirst(md5($delims . "\n" . $source)); if (!isset($this->sections[$key])) { $this->sections[$key] = sprintf($this->prepare(self::SECTION), $key, $callable, $source, $helper, $delims, $this->walk($nodes, 2)); } $method = $this->getFindMethod($id); $id = var_export($id, true); $filters = $this->getFilters($filters, $level); return sprintf($this->prepare(self::SECTION_CALL, $level), $method, $id, $filters, $key); } const INVERTED_SECTION = ' $value = $context->%s(%s);%s if (empty($value)) { %s } '; /** * Generate Mustache Template inverted section PHP source. * * @param array $nodes Array of child tokens * @param string $id Section name * @param string[] $filters Array of filters * @param int $level * * @return string Generated inverted section PHP source code */ private function invertedSection($nodes, $id, $filters, $level) { $method = $this->getFindMethod($id); $id = var_export($id, true); $filters = $this->getFilters($filters, $level); return sprintf($this->prepare(self::INVERTED_SECTION, $level), $method, $id, $filters, $this->walk($nodes, $level)); } const PARTIAL_INDENT = ', $indent . %s'; const PARTIAL = ' if ($partial = $this->mustache->loadPartial(%s)) { $buffer .= $partial->renderInternal($context%s); } '; /** * Generate Mustache Template partial call PHP source. * * @param string $id Partial name * @param string $indent Whitespace indent to apply to partial * @param int $level * * @return string Generated partial call PHP source code */ private function partial($id, $indent, $level) { if ($indent !== '') { $indentParam = sprintf(self::PARTIAL_INDENT, var_export($indent, true)); } else { $indentParam = ''; } return sprintf( $this->prepare(self::PARTIAL, $level), var_export($id, true), $indentParam ); } const PARENT = ' if ($parent = $this->mustache->loadPartial(%s)) { $context->pushBlockContext(array(%s )); $buffer .= $parent->renderInternal($context, $indent); $context->popBlockContext(); } '; const PARENT_NO_CONTEXT = ' if ($parent = $this->mustache->loadPartial(%s)) { $buffer .= $parent->renderInternal($context, $indent); } '; /** * Generate Mustache Template inheritance parent call PHP source. * * @param string $id Parent tag name * @param string $indent Whitespace indent to apply to parent * @param array $children Child nodes * @param int $level * * @return string Generated PHP source code */ private function parent($id, $indent, array $children, $level) { $realChildren = array_filter($children, array(__CLASS__, 'onlyBlockArgs')); if (empty($realChildren)) { return sprintf($this->prepare(self::PARENT_NO_CONTEXT, $level), var_export($id, true)); } return sprintf( $this->prepare(self::PARENT, $level), var_export($id, true), $this->walk($realChildren, $level + 1) ); } /** * Helper method for filtering out non-block-arg tokens. * * @param array $node * * @return bool True if $node is a block arg token */ private static function onlyBlockArgs(array $node) { return $node[Mustache_Tokenizer::TYPE] === Mustache_Tokenizer::T_BLOCK_ARG; } const VARIABLE = ' $value = $this->resolveValue($context->%s(%s), $context);%s $buffer .= %s($value === null ? \'\' : %s); '; /** * Generate Mustache Template variable interpolation PHP source. * * @param string $id Variable name * @param string[] $filters Array of filters * @param bool $escape Escape the variable value for output? * @param int $level * * @return string Generated variable interpolation PHP source */ private function variable($id, $filters, $escape, $level) { $method = $this->getFindMethod($id); $id = ($method !== 'last') ? var_export($id, true) : ''; $filters = $this->getFilters($filters, $level); $value = $escape ? $this->getEscape() : '$value'; return sprintf($this->prepare(self::VARIABLE, $level), $method, $id, $filters, $this->flushIndent(), $value); } const FILTER = ' $filter = $context->%s(%s); if (!(%s)) { throw new Mustache_Exception_UnknownFilterException(%s); } $value = call_user_func($filter, $value);%s '; /** * Generate Mustache Template variable filtering PHP source. * * @param string[] $filters Array of filters * @param int $level * * @return string Generated filter PHP source */ private function getFilters(array $filters, $level) { if (empty($filters)) { return ''; } $name = array_shift($filters); $method = $this->getFindMethod($name); $filter = ($method !== 'last') ? var_export($name, true) : ''; $callable = $this->getCallable('$filter'); $msg = var_export($name, true); return sprintf($this->prepare(self::FILTER, $level), $method, $filter, $callable, $msg, $this->getFilters($filters, $level)); } const LINE = '$buffer .= "\n";'; const TEXT = '$buffer .= %s%s;'; /** * Generate Mustache Template output Buffer call PHP source. * * @param string $text * @param int $level * * @return string Generated output Buffer call PHP source */ private function text($text, $level) { $indentNextLine = (substr($text, -1) === "\n"); $code = sprintf($this->prepare(self::TEXT, $level), $this->flushIndent(), var_export($text, true)); $this->indentNextLine = $indentNextLine; return $code; } /** * Prepare PHP source code snippet for output. * * @param string $text * @param int $bonus Additional indent level (default: 0) * @param bool $prependNewline Prepend a newline to the snippet? (default: true) * @param bool $appendNewline Append a newline to the snippet? (default: false) * * @return string PHP source code snippet */ private function prepare($text, $bonus = 0, $prependNewline = true, $appendNewline = false) { $text = ($prependNewline ? "\n" : '') . trim($text); if ($prependNewline) { $bonus++; } if ($appendNewline) { $text .= "\n"; } return preg_replace("/\n( {8})?/", "\n" . str_repeat(' ', $bonus * 4), $text); } const DEFAULT_ESCAPE = 'htmlspecialchars(%s, %s, %s)'; const CUSTOM_ESCAPE = 'call_user_func($this->mustache->getEscape(), %s)'; /** * Get the current escaper. * * @param string $value (default: '$value') * * @return string Either a custom callback, or an inline call to `htmlspecialchars` */ private function getEscape($value = '$value') { if ($this->customEscape) { return sprintf(self::CUSTOM_ESCAPE, $value); } return sprintf(self::DEFAULT_ESCAPE, $value, var_export($this->entityFlags, true), var_export($this->charset, true)); } /** * Select the appropriate Context `find` method for a given $id. * * The return value will be one of `find`, `findDot`, `findAnchoredDot` or `last`. * * @see Mustache_Context::find * @see Mustache_Context::findDot * @see Mustache_Context::last * * @param string $id Variable name * * @return string `find` method name */ private function getFindMethod($id) { if ($id === '.') { return 'last'; } if (isset($this->pragmas[Mustache_Engine::PRAGMA_ANCHORED_DOT]) && $this->pragmas[Mustache_Engine::PRAGMA_ANCHORED_DOT]) { if (substr($id, 0, 1) === '.') { return 'findAnchoredDot'; } } if (strpos($id, '.') === false) { return 'find'; } return 'findDot'; } const IS_CALLABLE = '!is_string(%s) && is_callable(%s)'; const STRICT_IS_CALLABLE = 'is_object(%s) && is_callable(%s)'; /** * Helper function to compile strict vs lax "is callable" logic. * * @param string $variable (default: '$value') * * @return string "is callable" logic */ private function getCallable($variable = '$value') { $tpl = $this->strictCallables ? self::STRICT_IS_CALLABLE : self::IS_CALLABLE; return sprintf($tpl, $variable, $variable); } const LINE_INDENT = '$indent . '; /** * Get the current $indent prefix to write to the buffer. * * @return string "$indent . " or "" */ private function flushIndent() { if (!$this->indentNextLine) { return ''; } $this->indentNextLine = false; return self::LINE_INDENT; } } load('foo'); // equivalent to `file_get_contents(dirname(__FILE__).'/views/foo.mustache'); * * This is probably the most useful Mustache Loader implementation. It can be used for partials and normal Templates: * * $m = new Mustache(array( * 'loader' => new Mustache_Loader_FilesystemLoader(dirname(__FILE__).'/views'), * 'partials_loader' => new Mustache_Loader_FilesystemLoader(dirname(__FILE__).'/views/partials'), * )); */ class Mustache_Loader_FilesystemLoader implements Mustache_Loader { private $baseDir; private $extension = '.mustache'; private $templates = array(); /** * Mustache filesystem Loader constructor. * * Passing an $options array allows overriding certain Loader options during instantiation: * * $options = array( * // The filename extension used for Mustache templates. Defaults to '.mustache' * 'extension' => '.ms', * ); * * @throws Mustache_Exception_RuntimeException if $baseDir does not exist * * @param string $baseDir Base directory containing Mustache template files * @param array $options Array of Loader options (default: array()) */ public function __construct($baseDir, array $options = array()) { $this->baseDir = $baseDir; if (strpos($this->baseDir, '://') === false) { $this->baseDir = realpath($this->baseDir); } if ($this->shouldCheckPath() && !is_dir($this->baseDir)) { throw new Mustache_Exception_RuntimeException(sprintf('FilesystemLoader baseDir must be a directory: %s', $baseDir)); } if (array_key_exists('extension', $options)) { if (empty($options['extension'])) { $this->extension = ''; } else { $this->extension = '.' . ltrim($options['extension'], '.'); } } } /** * Load a Template by name. * * $loader = new Mustache_Loader_FilesystemLoader(dirname(__FILE__).'/views'); * $loader->load('admin/dashboard'); // loads "./views/admin/dashboard.mustache"; * * @param string $name * * @return string Mustache Template source */ public function load($name) { if (!isset($this->templates[$name])) { $this->templates[$name] = $this->loadFile($name); } return $this->templates[$name]; } /** * Helper function for loading a Mustache file by name. * * @throws Mustache_Exception_UnknownTemplateException If a template file is not found * * @param string $name * * @return string Mustache Template source */ protected function loadFile($name) { $fileName = $this->getFileName($name); if ($this->shouldCheckPath() && !file_exists($fileName)) { throw new Mustache_Exception_UnknownTemplateException($name); } return file_get_contents($fileName); } /** * Helper function for getting a Mustache template file name. * * @param string $name * * @return string Template file name */ protected function getFileName($name) { $fileName = $this->baseDir . '/' . $name; if (substr($fileName, 0 - strlen($this->extension)) !== $this->extension) { $fileName .= $this->extension; } return $fileName; } /** * Only check if baseDir is a directory and requested templates are files if * baseDir is using the filesystem stream wrapper. * * @return bool Whether to check `is_dir` and `file_exists` */ protected function shouldCheckPath() { return strpos($this->baseDir, '://') === false || strpos($this->baseDir, 'file://') === 0; } } loaders = array(); foreach ($loaders as $loader) { $this->addLoader($loader); } } /** * Add a Loader instance. * * @param Mustache_Loader $loader */ public function addLoader(Mustache_Loader $loader) { $this->loaders[] = $loader; } /** * Load a Template by name. * * @throws Mustache_Exception_UnknownTemplateException If a template file is not found * * @param string $name * * @return string Mustache Template source */ public function load($name) { foreach ($this->loaders as $loader) { try { return $loader->load($name); } catch (Mustache_Exception_UnknownTemplateException $e) { // do nothing, check the next loader. } } throw new Mustache_Exception_UnknownTemplateException($name); } } load('hello'); * $goodbye = $loader->load('goodbye'); * * __halt_compiler(); * * @@ hello * Hello, {{ planet }}! * * @@ goodbye * Goodbye, cruel {{ planet }} * * Templates are deliniated by lines containing only `@@ name`. * * The InlineLoader is well-suited to micro-frameworks such as Silex: * * $app->register(new MustacheServiceProvider, array( * 'mustache.loader' => new Mustache_Loader_InlineLoader(__FILE__, __COMPILER_HALT_OFFSET__) * )); * * $app->get('/{name}', function ($name) use ($app) { * return $app['mustache']->render('hello', compact('name')); * }) * ->value('name', 'world'); * * // ... * * __halt_compiler(); * * @@ hello * Hello, {{ name }}! */ class Mustache_Loader_InlineLoader implements Mustache_Loader { protected $fileName; protected $offset; protected $templates; /** * The InlineLoader requires a filename and offset to process templates. * * The magic constants `__FILE__` and `__COMPILER_HALT_OFFSET__` are usually * perfectly suited to the job: * * $loader = new Mustache_Loader_InlineLoader(__FILE__, __COMPILER_HALT_OFFSET__); * * Note that this only works if the loader is instantiated inside the same * file as the inline templates. If the templates are located in another * file, it would be necessary to manually specify the filename and offset. * * @param string $fileName The file to parse for inline templates * @param int $offset A string offset for the start of the templates. * This usually coincides with the `__halt_compiler` * call, and the `__COMPILER_HALT_OFFSET__` */ public function __construct($fileName, $offset) { if (!is_file($fileName)) { throw new Mustache_Exception_InvalidArgumentException('InlineLoader expects a valid filename.'); } if (!is_int($offset) || $offset < 0) { throw new Mustache_Exception_InvalidArgumentException('InlineLoader expects a valid file offset.'); } $this->fileName = $fileName; $this->offset = $offset; } /** * Load a Template by name. * * @throws Mustache_Exception_UnknownTemplateException If a template file is not found * * @param string $name * * @return string Mustache Template source */ public function load($name) { $this->loadTemplates(); if (!array_key_exists($name, $this->templates)) { throw new Mustache_Exception_UnknownTemplateException($name); } return $this->templates[$name]; } /** * Parse and load templates from the end of a source file. */ protected function loadTemplates() { if ($this->templates === null) { $this->templates = array(); $data = file_get_contents($this->fileName, false, null, $this->offset); foreach (preg_split("/^@@(?= [\w\d\.]+$)/m", $data, -1) as $chunk) { if (trim($chunk)) { list($name, $content) = explode("\n", $chunk, 2); $this->templates[trim($name)] = trim($content); } } } } } '.ms', * 'stat_props' => array('size', 'mtime'), * ); * * Specifying 'stat_props' overrides the stat properties used to invalidate the template cache. By default, this * uses 'mtime' and 'size', but this can be set to any of the properties supported by stat(): * * http://php.net/manual/en/function.stat.php * * You can also disable filesystem stat entirely: * * $options = array('stat_props' => null); * * But with great power comes great responsibility. Namely, if you disable stat-based cache invalidation, * YOU MUST CLEAR THE TEMPLATE CACHE YOURSELF when your templates change. Make it part of your build or deploy * process so you don't forget! * * @throws Mustache_Exception_RuntimeException if $baseDir does not exist. * * @param string $baseDir Base directory containing Mustache template files. * @param array $options Array of Loader options (default: array()) */ public function __construct($baseDir, array $options = array()) { parent::__construct($baseDir, $options); if (array_key_exists('stat_props', $options)) { if (empty($options['stat_props'])) { $this->statProps = array(); } else { $this->statProps = $options['stat_props']; } } else { $this->statProps = array('size', 'mtime'); } } /** * Helper function for loading a Mustache file by name. * * @throws Mustache_Exception_UnknownTemplateException If a template file is not found. * * @param string $name * * @return Mustache_Source Mustache Template source */ protected function loadFile($name) { $fileName = $this->getFileName($name); if (!file_exists($fileName)) { throw new Mustache_Exception_UnknownTemplateException($name); } return new Mustache_Source_FilesystemSource($fileName, $this->statProps); } } '{{ bar }}', * 'baz' => 'Hey {{ qux }}!' * ); * * $tpl = $loader->load('foo'); // '{{ bar }}' * * The ArrayLoader is used internally as a partials loader by Mustache_Engine instance when an array of partials * is set. It can also be used as a quick-and-dirty Template loader. */ class Mustache_Loader_ArrayLoader implements Mustache_Loader, Mustache_Loader_MutableLoader { private $templates; /** * ArrayLoader constructor. * * @param array $templates Associative array of Template source (default: array()) */ public function __construct(array $templates = array()) { $this->templates = $templates; } /** * Load a Template. * * @throws Mustache_Exception_UnknownTemplateException If a template file is not found * * @param string $name * * @return string Mustache Template source */ public function load($name) { if (!isset($this->templates[$name])) { throw new Mustache_Exception_UnknownTemplateException($name); } return $this->templates[$name]; } /** * Set an associative array of Template sources for this loader. * * @param array $templates */ public function setTemplates(array $templates) { $this->templates = $templates; } /** * Set a Template source by name. * * @param string $name * @param string $template Mustache Template source */ public function setTemplate($name, $template) { $this->templates[$name] = $template; } } load('{{ foo }}'); // '{{ foo }}' * * This is the default Template Loader instance used by Mustache: * * $m = new Mustache; * $tpl = $m->loadTemplate('{{ foo }}'); * echo $tpl->render(array('foo' => 'bar')); // "bar" */ class Mustache_Loader_StringLoader implements Mustache_Loader { /** * Load a Template by source. * * @param string $name Mustache Template source * * @return string Mustache Template source */ public function load($name) { return $name; } } lineNum = -1; $this->lineTokens = 0; $this->pragmas = $this->defaultPragmas; $this->pragmaFilters = isset($this->pragmas[Mustache_Engine::PRAGMA_FILTERS]); $this->pragmaBlocks = isset($this->pragmas[Mustache_Engine::PRAGMA_BLOCKS]); return $this->buildTree($tokens); } /** * Enable pragmas across all templates, regardless of the presence of pragma * tags in the individual templates. * * @internal Users should set global pragmas in Mustache_Engine, not here :) * * @param string[] $pragmas */ public function setPragmas(array $pragmas) { $this->pragmas = array(); foreach ($pragmas as $pragma) { $this->enablePragma($pragma); } $this->defaultPragmas = $this->pragmas; } /** * Helper method for recursively building a parse tree. * * @throws Mustache_Exception_SyntaxException when nesting errors or mismatched section tags are encountered * * @param array &$tokens Set of Mustache tokens * @param array $parent Parent token (default: null) * * @return array Mustache Token parse tree */ private function buildTree(array &$tokens, array $parent = null) { $nodes = array(); while (!empty($tokens)) { $token = array_shift($tokens); if ($token[Mustache_Tokenizer::LINE] === $this->lineNum) { $this->lineTokens++; } else { $this->lineNum = $token[Mustache_Tokenizer::LINE]; $this->lineTokens = 0; } if ($this->pragmaFilters && isset($token[Mustache_Tokenizer::NAME])) { list($name, $filters) = $this->getNameAndFilters($token[Mustache_Tokenizer::NAME]); if (!empty($filters)) { $token[Mustache_Tokenizer::NAME] = $name; $token[Mustache_Tokenizer::FILTERS] = $filters; } } switch ($token[Mustache_Tokenizer::TYPE]) { case Mustache_Tokenizer::T_DELIM_CHANGE: $this->checkIfTokenIsAllowedInParent($parent, $token); $this->clearStandaloneLines($nodes, $tokens); break; case Mustache_Tokenizer::T_SECTION: case Mustache_Tokenizer::T_INVERTED: $this->checkIfTokenIsAllowedInParent($parent, $token); $this->clearStandaloneLines($nodes, $tokens); $nodes[] = $this->buildTree($tokens, $token); break; case Mustache_Tokenizer::T_END_SECTION: if (!isset($parent)) { $msg = sprintf( 'Unexpected closing tag: /%s on line %d', $token[Mustache_Tokenizer::NAME], $token[Mustache_Tokenizer::LINE] ); throw new Mustache_Exception_SyntaxException($msg, $token); } if ($token[Mustache_Tokenizer::NAME] !== $parent[Mustache_Tokenizer::NAME]) { $msg = sprintf( 'Nesting error: %s (on line %d) vs. %s (on line %d)', $parent[Mustache_Tokenizer::NAME], $parent[Mustache_Tokenizer::LINE], $token[Mustache_Tokenizer::NAME], $token[Mustache_Tokenizer::LINE] ); throw new Mustache_Exception_SyntaxException($msg, $token); } $this->clearStandaloneLines($nodes, $tokens); $parent[Mustache_Tokenizer::END] = $token[Mustache_Tokenizer::INDEX]; $parent[Mustache_Tokenizer::NODES] = $nodes; return $parent; case Mustache_Tokenizer::T_PARTIAL: $this->checkIfTokenIsAllowedInParent($parent, $token); //store the whitespace prefix for laters! if ($indent = $this->clearStandaloneLines($nodes, $tokens)) { $token[Mustache_Tokenizer::INDENT] = $indent[Mustache_Tokenizer::VALUE]; } $nodes[] = $token; break; case Mustache_Tokenizer::T_PARENT: $this->checkIfTokenIsAllowedInParent($parent, $token); $nodes[] = $this->buildTree($tokens, $token); break; case Mustache_Tokenizer::T_BLOCK_VAR: if ($this->pragmaBlocks) { // BLOCKS pragma is enabled, let's do this! if (isset($parent) && $parent[Mustache_Tokenizer::TYPE] === Mustache_Tokenizer::T_PARENT) { $token[Mustache_Tokenizer::TYPE] = Mustache_Tokenizer::T_BLOCK_ARG; } $this->clearStandaloneLines($nodes, $tokens); $nodes[] = $this->buildTree($tokens, $token); } else { // pretend this was just a normal "escaped" token... $token[Mustache_Tokenizer::TYPE] = Mustache_Tokenizer::T_ESCAPED; // TODO: figure out how to figure out if there was a space after this dollar: $token[Mustache_Tokenizer::NAME] = '$' . $token[Mustache_Tokenizer::NAME]; $nodes[] = $token; } break; case Mustache_Tokenizer::T_PRAGMA: $this->enablePragma($token[Mustache_Tokenizer::NAME]); // no break case Mustache_Tokenizer::T_COMMENT: $this->clearStandaloneLines($nodes, $tokens); $nodes[] = $token; break; default: $nodes[] = $token; break; } } if (isset($parent)) { $msg = sprintf( 'Missing closing tag: %s opened on line %d', $parent[Mustache_Tokenizer::NAME], $parent[Mustache_Tokenizer::LINE] ); throw new Mustache_Exception_SyntaxException($msg, $parent); } return $nodes; } /** * Clear standalone line tokens. * * Returns a whitespace token for indenting partials, if applicable. * * @param array $nodes Parsed nodes * @param array $tokens Tokens to be parsed * * @return array|null Resulting indent token, if any */ private function clearStandaloneLines(array &$nodes, array &$tokens) { if ($this->lineTokens > 1) { // this is the third or later node on this line, so it can't be standalone return; } $prev = null; if ($this->lineTokens === 1) { // this is the second node on this line, so it can't be standalone // unless the previous node is whitespace. if ($prev = end($nodes)) { if (!$this->tokenIsWhitespace($prev)) { return; } } } if ($next = reset($tokens)) { // If we're on a new line, bail. if ($next[Mustache_Tokenizer::LINE] !== $this->lineNum) { return; } // If the next token isn't whitespace, bail. if (!$this->tokenIsWhitespace($next)) { return; } if (count($tokens) !== 1) { // Unless it's the last token in the template, the next token // must end in newline for this to be standalone. if (substr($next[Mustache_Tokenizer::VALUE], -1) !== "\n") { return; } } // Discard the whitespace suffix array_shift($tokens); } if ($prev) { // Return the whitespace prefix, if any return array_pop($nodes); } } /** * Check whether token is a whitespace token. * * True if token type is T_TEXT and value is all whitespace characters. * * @param array $token * * @return bool True if token is a whitespace token */ private function tokenIsWhitespace(array $token) { if ($token[Mustache_Tokenizer::TYPE] === Mustache_Tokenizer::T_TEXT) { return preg_match('/^\s*$/', $token[Mustache_Tokenizer::VALUE]); } return false; } /** * Check whether a token is allowed inside a parent tag. * * @throws Mustache_Exception_SyntaxException if an invalid token is found inside a parent tag * * @param array|null $parent * @param array $token */ private function checkIfTokenIsAllowedInParent($parent, array $token) { if (isset($parent) && $parent[Mustache_Tokenizer::TYPE] === Mustache_Tokenizer::T_PARENT) { throw new Mustache_Exception_SyntaxException('Illegal content in < parent tag', $token); } } /** * Split a tag name into name and filters. * * @param string $name * * @return array [Tag name, Array of filters] */ private function getNameAndFilters($name) { $filters = array_map('trim', explode('|', $name)); $name = array_shift($filters); return array($name, $filters); } /** * Enable a pragma. * * @param string $name */ private function enablePragma($name) { $this->pragmas[$name] = true; switch ($name) { case Mustache_Engine::PRAGMA_BLOCKS: $this->pragmaBlocks = true; break; case Mustache_Engine::PRAGMA_FILTERS: $this->pragmaFilters = true; break; } } } token = $token; if (version_compare(PHP_VERSION, '5.3.0', '>=')) { parent::__construct($msg, 0, $previous); } else { parent::__construct($msg); // @codeCoverageIgnore } } /** * @return array */ public function getToken() { return $this->token; } } templateName = $templateName; $message = sprintf('Unknown template: %s', $templateName); if (version_compare(PHP_VERSION, '5.3.0', '>=')) { parent::__construct($message, 0, $previous); } else { parent::__construct($message); // @codeCoverageIgnore } } public function getTemplateName() { return $this->templateName; } } filterName = $filterName; $message = sprintf('Unknown filter: %s', $filterName); if (version_compare(PHP_VERSION, '5.3.0', '>=')) { parent::__construct($message, 0, $previous); } else { parent::__construct($message); // @codeCoverageIgnore } } public function getFilterName() { return $this->filterName; } } helperName = $helperName; $message = sprintf('Unknown helper: %s', $helperName); if (version_compare(PHP_VERSION, '5.3.0', '>=')) { parent::__construct($message, 0, $previous); } else { parent::__construct($message); // @codeCoverageIgnore } } public function getHelperName() { return $this->helperName; } } logger; } /** * Set a logger instance. * * @param Mustache_Logger|Psr\Log\LoggerInterface $logger */ public function setLogger($logger = null) { if ($logger !== null && !($logger instanceof Mustache_Logger || is_a($logger, 'Psr\\Log\\LoggerInterface'))) { throw new Mustache_Exception_InvalidArgumentException('Expected an instance of Mustache_Logger or Psr\\Log\\LoggerInterface.'); } $this->logger = $logger; } /** * Add a log record if logging is enabled. * * @param string $level The logging level * @param string $message The log message * @param array $context The log context */ protected function log($level, $message, array $context = array()) { if (isset($this->logger)) { $this->logger->log($level, $message, $context); } } } log( Mustache_Logger::WARNING, 'Template cache disabled, evaluating "{className}" class at runtime', array('className' => $key) ); eval('?>' . $value); } } cache($className, $compiledSource); * * The FilesystemCache benefits from any opcode caching that may be setup in your environment. So do that, k? */ class Mustache_Cache_FilesystemCache extends Mustache_Cache_AbstractCache { private $baseDir; private $fileMode; /** * Filesystem cache constructor. * * @param string $baseDir Directory for compiled templates * @param int $fileMode Override default permissions for cache files. Defaults to using the system-defined umask */ public function __construct($baseDir, $fileMode = null) { $this->baseDir = $baseDir; $this->fileMode = $fileMode; } /** * Load the class from cache using `require_once`. * * @param string $key * * @return bool */ public function load($key) { $fileName = $this->getCacheFilename($key); if (!is_file($fileName)) { return false; } require_once $fileName; return true; } /** * Cache and load the compiled class. * * @param string $key * @param string $value */ public function cache($key, $value) { $fileName = $this->getCacheFilename($key); $this->log( Mustache_Logger::DEBUG, 'Writing to template cache: "{fileName}"', array('fileName' => $fileName) ); $this->writeFile($fileName, $value); $this->load($key); } /** * Build the cache filename. * Subclasses should override for custom cache directory structures. * * @param string $name * * @return string */ protected function getCacheFilename($name) { return sprintf('%s/%s.php', $this->baseDir, $name); } /** * Create cache directory. * * @throws Mustache_Exception_RuntimeException If unable to create directory * * @param string $fileName * * @return string */ private function buildDirectoryForFilename($fileName) { $dirName = dirname($fileName); if (!is_dir($dirName)) { $this->log( Mustache_Logger::INFO, 'Creating Mustache template cache directory: "{dirName}"', array('dirName' => $dirName) ); @mkdir($dirName, 0777, true); // @codeCoverageIgnoreStart if (!is_dir($dirName)) { throw new Mustache_Exception_RuntimeException(sprintf('Failed to create cache directory "%s".', $dirName)); } // @codeCoverageIgnoreEnd } return $dirName; } /** * Write cache file. * * @throws Mustache_Exception_RuntimeException If unable to write file * * @param string $fileName * @param string $value */ private function writeFile($fileName, $value) { $dirName = $this->buildDirectoryForFilename($fileName); $this->log( Mustache_Logger::DEBUG, 'Caching compiled template to "{fileName}"', array('fileName' => $fileName) ); $tempFile = tempnam($dirName, basename($fileName)); if (false !== @file_put_contents($tempFile, $value)) { if (@rename($tempFile, $fileName)) { $mode = isset($this->fileMode) ? $this->fileMode : (0666 & ~umask()); @chmod($fileName, $mode); return; } // @codeCoverageIgnoreStart $this->log( Mustache_Logger::ERROR, 'Unable to rename Mustache temp cache file: "{tempName}" -> "{fileName}"', array('tempName' => $tempFile, 'fileName' => $fileName) ); // @codeCoverageIgnoreEnd } // @codeCoverageIgnoreStart throw new Mustache_Exception_RuntimeException(sprintf('Failed to write cache file "%s".', $fileName)); // @codeCoverageIgnoreEnd } } 100, self::INFO => 200, self::NOTICE => 250, self::WARNING => 300, self::ERROR => 400, self::CRITICAL => 500, self::ALERT => 550, self::EMERGENCY => 600, ); protected $level; protected $stream = null; protected $url = null; /** * @throws InvalidArgumentException if the logging level is unknown * * @param resource|string $stream Resource instance or URL * @param int $level The minimum logging level at which this handler will be triggered */ public function __construct($stream, $level = Mustache_Logger::ERROR) { $this->setLevel($level); if (is_resource($stream)) { $this->stream = $stream; } else { $this->url = $stream; } } /** * Close stream resources. */ public function __destruct() { if (is_resource($this->stream)) { fclose($this->stream); } } /** * Set the minimum logging level. * * @throws Mustache_Exception_InvalidArgumentException if the logging level is unknown * * @param int $level The minimum logging level which will be written */ public function setLevel($level) { if (!array_key_exists($level, self::$levels)) { throw new Mustache_Exception_InvalidArgumentException(sprintf('Unexpected logging level: %s', $level)); } $this->level = $level; } /** * Get the current minimum logging level. * * @return int */ public function getLevel() { return $this->level; } /** * Logs with an arbitrary level. * * @throws Mustache_Exception_InvalidArgumentException if the logging level is unknown * * @param mixed $level * @param string $message * @param array $context */ public function log($level, $message, array $context = array()) { if (!array_key_exists($level, self::$levels)) { throw new Mustache_Exception_InvalidArgumentException(sprintf('Unexpected logging level: %s', $level)); } if (self::$levels[$level] >= self::$levels[$this->level]) { $this->writeLog($level, $message, $context); } } /** * Write a record to the log. * * @throws Mustache_Exception_LogicException If neither a stream resource nor url is present * @throws Mustache_Exception_RuntimeException If the stream url cannot be opened * * @param int $level The logging level * @param string $message The log message * @param array $context The log context */ protected function writeLog($level, $message, array $context = array()) { if (!is_resource($this->stream)) { if (!isset($this->url)) { throw new Mustache_Exception_LogicException('Missing stream url, the stream can not be opened. This may be caused by a premature call to close().'); } $this->stream = fopen($this->url, 'a'); if (!is_resource($this->stream)) { // @codeCoverageIgnoreStart throw new Mustache_Exception_RuntimeException(sprintf('The stream or file "%s" could not be opened.', $this->url)); // @codeCoverageIgnoreEnd } } fwrite($this->stream, self::formatLine($level, $message, $context)); } /** * Gets the name of the logging level. * * @throws InvalidArgumentException if the logging level is unknown * * @param int $level * * @return string */ protected static function getLevelName($level) { return strtoupper($level); } /** * Format a log line for output. * * @param int $level The logging level * @param string $message The log message * @param array $context The log context * * @return string */ protected static function formatLine($level, $message, array $context = array()) { return sprintf( "%s: %s\n", self::getLevelName($level), self::interpolateMessage($message, $context) ); } /** * Interpolate context values into the message placeholders. * * @param string $message * @param array $context * * @return string */ protected static function interpolateMessage($message, array $context = array()) { if (strpos($message, '{') === false) { return $message; } // build a replacement array with braces around the context keys $replace = array(); foreach ($context as $key => $val) { $replace['{' . $key . '}'] = $val; } // interpolate replacement values into the the message and return return strtr($message, $replace); } } log(Mustache_Logger::EMERGENCY, $message, $context); } /** * Action must be taken immediately. * * Example: Entire website down, database unavailable, etc. This should * trigger the SMS alerts and wake you up. * * @param string $message * @param array $context */ public function alert($message, array $context = array()) { $this->log(Mustache_Logger::ALERT, $message, $context); } /** * Critical conditions. * * Example: Application component unavailable, unexpected exception. * * @param string $message * @param array $context */ public function critical($message, array $context = array()) { $this->log(Mustache_Logger::CRITICAL, $message, $context); } /** * Runtime errors that do not require immediate action but should typically * be logged and monitored. * * @param string $message * @param array $context */ public function error($message, array $context = array()) { $this->log(Mustache_Logger::ERROR, $message, $context); } /** * Exceptional occurrences that are not errors. * * Example: Use of deprecated APIs, poor use of an API, undesirable things * that are not necessarily wrong. * * @param string $message * @param array $context */ public function warning($message, array $context = array()) { $this->log(Mustache_Logger::WARNING, $message, $context); } /** * Normal but significant events. * * @param string $message * @param array $context */ public function notice($message, array $context = array()) { $this->log(Mustache_Logger::NOTICE, $message, $context); } /** * Interesting events. * * Example: User logs in, SQL logs. * * @param string $message * @param array $context */ public function info($message, array $context = array()) { $this->log(Mustache_Logger::INFO, $message, $context); } /** * Detailed debug information. * * @param string $message * @param array $context */ public function debug($message, array $context = array()) { $this->log(Mustache_Logger::DEBUG, $message, $context); } } =}}`. (default: null) */ public function __construct(Mustache_Engine $mustache, Mustache_Context $context, $delims = null) { $this->mustache = $mustache; $this->context = $context; $this->delims = $delims; } /** * Render a string as a Mustache template with the current rendering context. * * @param string $string * * @return string Rendered template */ public function render($string) { return $this->mustache ->loadLambda((string) $string, $this->delims) ->renderInternal($this->context); } /** * Render a string as a Mustache template with the current rendering context. * * @param string $string * * @return string Rendered template */ public function __invoke($string) { return $this->render($string); } /** * Get a Lambda Helper with custom delimiters. * * @param string $delims Custom delimiters, in the format `{{= <% %> =}}` * * @return Mustache_LambdaHelper */ public function withDelimiters($delims) { return new self($this->mustache, $this->context, $delims); } } $helper` pairs. * * @throws Mustache_Exception_InvalidArgumentException if the $helpers argument isn't an array or Traversable * * @param array|Traversable $helpers (default: null) */ public function __construct($helpers = null) { if ($helpers === null) { return; } if (!is_array($helpers) && !$helpers instanceof Traversable) { throw new Mustache_Exception_InvalidArgumentException('HelperCollection constructor expects an array of helpers'); } foreach ($helpers as $name => $helper) { $this->add($name, $helper); } } /** * Magic mutator. * * @see Mustache_HelperCollection::add * * @param string $name * @param mixed $helper */ public function __set($name, $helper) { $this->add($name, $helper); } /** * Add a helper to this collection. * * @param string $name * @param mixed $helper */ public function add($name, $helper) { $this->helpers[$name] = $helper; } /** * Magic accessor. * * @see Mustache_HelperCollection::get * * @param string $name * * @return mixed Helper */ public function __get($name) { return $this->get($name); } /** * Get a helper by name. * * @throws Mustache_Exception_UnknownHelperException If helper does not exist * * @param string $name * * @return mixed Helper */ public function get($name) { if (!$this->has($name)) { throw new Mustache_Exception_UnknownHelperException($name); } return $this->helpers[$name]; } /** * Magic isset(). * * @see Mustache_HelperCollection::has * * @param string $name * * @return bool True if helper is present */ public function __isset($name) { return $this->has($name); } /** * Check whether a given helper is present in the collection. * * @param string $name * * @return bool True if helper is present */ public function has($name) { return array_key_exists($name, $this->helpers); } /** * Magic unset(). * * @see Mustache_HelperCollection::remove * * @param string $name */ public function __unset($name) { $this->remove($name); } /** * Check whether a given helper is present in the collection. * * @throws Mustache_Exception_UnknownHelperException if the requested helper is not present * * @param string $name */ public function remove($name) { if (!$this->has($name)) { throw new Mustache_Exception_UnknownHelperException($name); } unset($this->helpers[$name]); } /** * Clear the helper collection. * * Removes all helpers from this collection */ public function clear() { $this->helpers = array(); } /** * Check whether the helper collection is empty. * * @return bool True if the collection is empty */ public function isEmpty() { return empty($this->helpers); } } mustache = $mustache; } /** * Mustache Template instances can be treated as a function and rendered by simply calling them. * * $m = new Mustache_Engine; * $tpl = $m->loadTemplate('Hello, {{ name }}!'); * echo $tpl(array('name' => 'World')); // "Hello, World!" * * @see Mustache_Template::render * * @param mixed $context Array or object rendering context (default: array()) * * @return string Rendered template */ public function __invoke($context = array()) { return $this->render($context); } /** * Render this template given the rendering context. * * @param mixed $context Array or object rendering context (default: array()) * * @return string Rendered template */ public function render($context = array()) { return $this->renderInternal( $this->prepareContextStack($context) ); } /** * Internal rendering method implemented by Mustache Template concrete subclasses. * * This is where the magic happens :) * * NOTE: This method is not part of the Mustache.php public API. * * @param Mustache_Context $context * @param string $indent (default: '') * * @return string Rendered template */ abstract public function renderInternal(Mustache_Context $context, $indent = ''); /** * Tests whether a value should be iterated over (e.g. in a section context). * * In most languages there are two distinct array types: list and hash (or whatever you want to call them). Lists * should be iterated, hashes should be treated as objects. Mustache follows this paradigm for Ruby, Javascript, * Java, Python, etc. * * PHP, however, treats lists and hashes as one primitive type: array. So Mustache.php needs a way to distinguish * between between a list of things (numeric, normalized array) and a set of variables to be used as section context * (associative array). In other words, this will be iterated over: * * $items = array( * array('name' => 'foo'), * array('name' => 'bar'), * array('name' => 'baz'), * ); * * ... but this will be used as a section context block: * * $items = array( * 1 => array('name' => 'foo'), * 'banana' => array('name' => 'bar'), * 42 => array('name' => 'baz'), * ); * * @param mixed $value * * @return bool True if the value is 'iterable' */ protected function isIterable($value) { switch (gettype($value)) { case 'object': return $value instanceof Traversable; case 'array': $i = 0; foreach ($value as $k => $v) { if ($k !== $i++) { return false; } } return true; default: return false; } } /** * Helper method to prepare the Context stack. * * Adds the Mustache HelperCollection to the stack's top context frame if helpers are present. * * @param mixed $context Optional first context frame (default: null) * * @return Mustache_Context */ protected function prepareContextStack($context = null) { $stack = new Mustache_Context(); $helpers = $this->mustache->getHelpers(); if (!$helpers->isEmpty()) { $stack->push($helpers); } if (!empty($context)) { $stack->push($context); } return $stack; } /** * Resolve a context value. * * Invoke the value if it is callable, otherwise return the value. * * @param mixed $value * @param Mustache_Context $context * * @return string */ protected function resolveValue($value, Mustache_Context $context) { if (($this->strictCallables ? is_object($value) : !is_string($value)) && is_callable($value)) { return $this->mustache ->loadLambda((string) call_user_func($value)) ->renderInternal($context); } return $value; } } baseDir = $realDir; } else { $this->baseDir = $baseDir; } } /** * Register a new instance as an SPL autoloader. * * @param string $baseDir Mustache library base directory (default: dirname(__FILE__).'/..') * * @return Mustache_Autoloader Registered Autoloader instance */ public static function register($baseDir = null) { $key = $baseDir ? $baseDir : 0; if (!isset(self::$instances[$key])) { self::$instances[$key] = new self($baseDir); } $loader = self::$instances[$key]; spl_autoload_register(array($loader, 'autoload')); return $loader; } /** * Autoload Mustache classes. * * @param string $class */ public function autoload($class) { if ($class[0] === '\\') { $class = substr($class, 1); } if (strpos($class, 'Mustache') !== 0) { return; } $file = sprintf('%s/%s.php', $this->baseDir, str_replace('_', '/', $class)); if (is_file($file)) { require $file; } } } fileName = $fileName; $this->statProps = $statProps; } /** * Get the Source key (used to generate the compiled class name). * * @throws Mustache_Exception_RuntimeException when a source file cannot be read * * @return string */ public function getKey() { $chunks = array( 'fileName' => $this->fileName, ); if (!empty($this->statProps)) { if (!isset($this->stat)) { $this->stat = @stat($this->fileName); } if ($this->stat === false) { throw new Mustache_Exception_RuntimeException(sprintf('Failed to read source file "%s".', $this->fileName)); } foreach ($this->statProps as $prop) { $chunks[$prop] = $this->stat[$prop]; } } return json_encode($chunks); } /** * Get the template Source. * * @return string */ public function getSource() { return file_get_contents($this->fileName); } } '; const T_PARENT = '<'; const T_DELIM_CHANGE = '='; const T_ESCAPED = '_v'; const T_UNESCAPED = '{'; const T_UNESCAPED_2 = '&'; const T_TEXT = '_t'; const T_PRAGMA = '%'; const T_BLOCK_VAR = '$'; const T_BLOCK_ARG = '$arg'; // Valid token types private static $tagTypes = array( self::T_SECTION => true, self::T_INVERTED => true, self::T_END_SECTION => true, self::T_COMMENT => true, self::T_PARTIAL => true, self::T_PARENT => true, self::T_DELIM_CHANGE => true, self::T_ESCAPED => true, self::T_UNESCAPED => true, self::T_UNESCAPED_2 => true, self::T_PRAGMA => true, self::T_BLOCK_VAR => true, ); // Token properties const TYPE = 'type'; const NAME = 'name'; const OTAG = 'otag'; const CTAG = 'ctag'; const LINE = 'line'; const INDEX = 'index'; const END = 'end'; const INDENT = 'indent'; const NODES = 'nodes'; const VALUE = 'value'; const FILTERS = 'filters'; private $state; private $tagType; private $buffer; private $tokens; private $seenTag; private $line; private $otag; private $otagChar; private $otagLen; private $ctag; private $ctagChar; private $ctagLen; /** * Scan and tokenize template source. * * @throws Mustache_Exception_SyntaxException when mismatched section tags are encountered * @throws Mustache_Exception_InvalidArgumentException when $delimiters string is invalid * * @param string $text Mustache template source to tokenize * @param string $delimiters Optionally, pass initial opening and closing delimiters (default: empty string) * * @return array Set of Mustache tokens */ public function scan($text, $delimiters = '') { // Setting mbstring.func_overload makes things *really* slow. // Let's do everyone a favor and scan this string as ASCII instead. // // The INI directive was removed in PHP 8.0 so we don't need to check there (and can drop it // when we remove support for older versions of PHP). // // @codeCoverageIgnoreStart $encoding = null; if (version_compare(PHP_VERSION, '8.0.0', '<')) { if (function_exists('mb_internal_encoding') && ini_get('mbstring.func_overload') & 2) { $encoding = mb_internal_encoding(); mb_internal_encoding('ASCII'); } } // @codeCoverageIgnoreEnd $this->reset(); if (is_string($delimiters) && $delimiters = trim($delimiters)) { $this->setDelimiters($delimiters); } $len = strlen($text); for ($i = 0; $i < $len; $i++) { switch ($this->state) { case self::IN_TEXT: $char = $text[$i]; // Test whether it's time to change tags. if ($char === $this->otagChar && substr($text, $i, $this->otagLen) === $this->otag) { $i--; $this->flushBuffer(); $this->state = self::IN_TAG_TYPE; } else { $this->buffer .= $char; if ($char === "\n") { $this->flushBuffer(); $this->line++; } } break; case self::IN_TAG_TYPE: $i += $this->otagLen - 1; $char = $text[$i + 1]; if (isset(self::$tagTypes[$char])) { $tag = $char; $this->tagType = $tag; } else { $tag = null; $this->tagType = self::T_ESCAPED; } if ($this->tagType === self::T_DELIM_CHANGE) { $i = $this->changeDelimiters($text, $i); $this->state = self::IN_TEXT; } elseif ($this->tagType === self::T_PRAGMA) { $i = $this->addPragma($text, $i); $this->state = self::IN_TEXT; } else { if ($tag !== null) { $i++; } $this->state = self::IN_TAG; } $this->seenTag = $i; break; default: $char = $text[$i]; // Test whether it's time to change tags. if ($char === $this->ctagChar && substr($text, $i, $this->ctagLen) === $this->ctag) { $token = array( self::TYPE => $this->tagType, self::NAME => trim($this->buffer), self::OTAG => $this->otag, self::CTAG => $this->ctag, self::LINE => $this->line, self::INDEX => ($this->tagType === self::T_END_SECTION) ? $this->seenTag - $this->otagLen : $i + $this->ctagLen, ); if ($this->tagType === self::T_UNESCAPED) { // Clean up `{{{ tripleStache }}}` style tokens. if ($this->ctag === '}}') { if (($i + 2 < $len) && $text[$i + 2] === '}') { $i++; } else { $msg = sprintf( 'Mismatched tag delimiters: %s on line %d', $token[self::NAME], $token[self::LINE] ); throw new Mustache_Exception_SyntaxException($msg, $token); } } else { $lastName = $token[self::NAME]; if (substr($lastName, -1) === '}') { $token[self::NAME] = trim(substr($lastName, 0, -1)); } else { $msg = sprintf( 'Mismatched tag delimiters: %s on line %d', $token[self::NAME], $token[self::LINE] ); throw new Mustache_Exception_SyntaxException($msg, $token); } } } $this->buffer = ''; $i += $this->ctagLen - 1; $this->state = self::IN_TEXT; $this->tokens[] = $token; } else { $this->buffer .= $char; } break; } } $this->flushBuffer(); // Restore the user's encoding... // @codeCoverageIgnoreStart if ($encoding) { mb_internal_encoding($encoding); } // @codeCoverageIgnoreEnd return $this->tokens; } /** * Helper function to reset tokenizer internal state. */ private function reset() { $this->state = self::IN_TEXT; $this->tagType = null; $this->buffer = ''; $this->tokens = array(); $this->seenTag = false; $this->line = 0; $this->otag = '{{'; $this->otagChar = '{'; $this->otagLen = 2; $this->ctag = '}}'; $this->ctagChar = '}'; $this->ctagLen = 2; } /** * Flush the current buffer to a token. */ private function flushBuffer() { if (strlen($this->buffer) > 0) { $this->tokens[] = array( self::TYPE => self::T_TEXT, self::LINE => $this->line, self::VALUE => $this->buffer, ); $this->buffer = ''; } } /** * Change the current Mustache delimiters. Set new `otag` and `ctag` values. * * @throws Mustache_Exception_SyntaxException when delimiter string is invalid * * @param string $text Mustache template source * @param int $index Current tokenizer index * * @return int New index value */ private function changeDelimiters($text, $index) { $startIndex = strpos($text, '=', $index) + 1; $close = '=' . $this->ctag; $closeIndex = strpos($text, $close, $index); $token = array( self::TYPE => self::T_DELIM_CHANGE, self::LINE => $this->line, ); try { $this->setDelimiters(trim(substr($text, $startIndex, $closeIndex - $startIndex))); } catch (Mustache_Exception_InvalidArgumentException $e) { throw new Mustache_Exception_SyntaxException($e->getMessage(), $token); } $this->tokens[] = $token; return $closeIndex + strlen($close) - 1; } /** * Set the current Mustache `otag` and `ctag` delimiters. * * @throws Mustache_Exception_InvalidArgumentException when delimiter string is invalid * * @param string $delimiters */ private function setDelimiters($delimiters) { if (!preg_match('/^\s*(\S+)\s+(\S+)\s*$/', $delimiters, $matches)) { throw new Mustache_Exception_InvalidArgumentException(sprintf('Invalid delimiters: %s', $delimiters)); } list($_, $otag, $ctag) = $matches; $this->otag = $otag; $this->otagChar = $otag[0]; $this->otagLen = strlen($otag); $this->ctag = $ctag; $this->ctagChar = $ctag[0]; $this->ctagLen = strlen($ctag); } /** * Add pragma token. * * Pragmas are hoisted to the front of the template, so all pragma tokens * will appear at the front of the token list. * * @param string $text * @param int $index * * @return int New index value */ private function addPragma($text, $index) { $end = strpos($text, $this->ctag, $index); $pragma = trim(substr($text, $index + 2, $end - $index - 2)); // Pragmas are hoisted to the front of the template. array_unshift($this->tokens, array( self::TYPE => self::T_PRAGMA, self::NAME => $pragma, self::LINE => 0, )); return $end + $this->ctagLen - 1; } } cookies = $cookies; } /** * Normalise cookie data into a Requests_Cookie * * @param string|Requests_Cookie $cookie * @return Requests_Cookie */ public function normalize_cookie($cookie, $key = null) { if ($cookie instanceof Requests_Cookie) { return $cookie; } return Requests_Cookie::parse($cookie, $key); } /** * Normalise cookie data into a Requests_Cookie * * @codeCoverageIgnore * @deprecated Use {@see Requests_Cookie_Jar::normalize_cookie} * @return Requests_Cookie */ public function normalizeCookie($cookie, $key = null) { return $this->normalize_cookie($cookie, $key); } /** * Check if the given item exists * * @param string $key Item key * @return boolean Does the item exist? */ public function offsetExists($key) { return isset($this->cookies[$key]); } /** * Get the value for the item * * @param string $key Item key * @return string|null Item value (null if offsetExists is false) */ public function offsetGet($key) { if (!isset($this->cookies[$key])) { return null; } return $this->cookies[$key]; } /** * Set the given item * * @throws Requests_Exception On attempting to use dictionary as list (`invalidset`) * * @param string $key Item name * @param string $value Item value */ public function offsetSet($key, $value) { if ($key === null) { throw new Requests_Exception('Object is a dictionary, not a list', 'invalidset'); } $this->cookies[$key] = $value; } /** * Unset the given header * * @param string $key */ public function offsetUnset($key) { unset($this->cookies[$key]); } /** * Get an iterator for the data * * @return ArrayIterator */ public function getIterator() { return new ArrayIterator($this->cookies); } /** * Register the cookie handler with the request's hooking system * * @param Requests_Hooker $hooks Hooking system */ public function register(Requests_Hooker $hooks) { $hooks->register('requests.before_request', array($this, 'before_request')); $hooks->register('requests.before_redirect_check', array($this, 'before_redirect_check')); } /** * Add Cookie header to a request if we have any * * As per RFC 6265, cookies are separated by '; ' * * @param string $url * @param array $headers * @param array $data * @param string $type * @param array $options */ public function before_request($url, &$headers, &$data, &$type, &$options) { if (!$url instanceof Requests_IRI) { $url = new Requests_IRI($url); } if (!empty($this->cookies)) { $cookies = array(); foreach ($this->cookies as $key => $cookie) { $cookie = $this->normalize_cookie($cookie, $key); // Skip expired cookies if ($cookie->is_expired()) { continue; } if ($cookie->domain_matches($url->host)) { $cookies[] = $cookie->format_for_header(); } } $headers['Cookie'] = implode('; ', $cookies); } } /** * Parse all cookies from a response and attach them to the response * * @var Requests_Response $response */ public function before_redirect_check(Requests_Response $return) { $url = $return->url; if (!$url instanceof Requests_IRI) { $url = new Requests_IRI($url); } $cookies = Requests_Cookie::parse_from_headers($return->headers, $url); $this->cookies = array_merge($this->cookies, $cookies); $return->cookies = $this; } } headers = new Requests_Response_Headers(); $this->cookies = new Requests_Cookie_Jar(); } /** * Response body * * @var string */ public $body = ''; /** * Raw HTTP data from the transport * * @var string */ public $raw = ''; /** * Headers, as an associative array * * @var Requests_Response_Headers Array-like object representing headers */ public $headers = array(); /** * Status code, false if non-blocking * * @var integer|boolean */ public $status_code = false; /** * Protocol version, false if non-blocking * * @var float|boolean */ public $protocol_version = false; /** * Whether the request succeeded or not * * @var boolean */ public $success = false; /** * Number of redirects the request used * * @var integer */ public $redirects = 0; /** * URL requested * * @var string */ public $url = ''; /** * Previous requests (from redirects) * * @var array Array of Requests_Response objects */ public $history = array(); /** * Cookies from the request * * @var Requests_Cookie_Jar Array-like object representing a cookie jar */ public $cookies = array(); /** * Is the response a redirect? * * @return boolean True if redirect (3xx status), false if not. */ public function is_redirect() { $code = $this->status_code; return in_array($code, array(300, 301, 302, 303, 307), true) || $code > 307 && $code < 400; } /** * Throws an exception if the request was not successful * * @throws Requests_Exception If `$allow_redirects` is false, and code is 3xx (`response.no_redirects`) * @throws Requests_Exception_HTTP On non-successful status code. Exception class corresponds to code (e.g. {@see Requests_Exception_HTTP_404}) * @param boolean $allow_redirects Set to false to throw on a 3xx as well */ public function throw_for_status($allow_redirects = true) { if ($this->is_redirect()) { if (!$allow_redirects) { throw new Requests_Exception('Redirection not allowed', 'response.no_redirects', $this); } } elseif (!$this->success) { $exception = Requests_Exception_HTTP::get_class($this->status_code); throw new $exception(null, $this); } } } 0 is executed later */ public function register($hook, $callback, $priority = 0) { if (!isset($this->hooks[$hook])) { $this->hooks[$hook] = array(); } if (!isset($this->hooks[$hook][$priority])) { $this->hooks[$hook][$priority] = array(); } $this->hooks[$hook][$priority][] = $callback; } /** * Dispatch a message * * @param string $hook Hook name * @param array $parameters Parameters to pass to callbacks * @return boolean Successfulness */ public function dispatch($hook, $parameters = array()) { if (empty($this->hooks[$hook])) { return false; } foreach ($this->hooks[$hook] as $priority => $hooked) { foreach ($hooked as $callback) { call_user_func_array($callback, $parameters); } } return true; } } 0 is executed later */ public function register($hook, $callback, $priority = 0); /** * Dispatch a message * * @param string $hook Hook name * @param array $parameters Parameters to pass to callbacks * @return boolean Successfulness */ public function dispatch($hook, $parameters = array()); } version = $curl['version_number']; $this->handle = curl_init(); curl_setopt($this->handle, CURLOPT_HEADER, false); curl_setopt($this->handle, CURLOPT_RETURNTRANSFER, 1); if ($this->version >= self::CURL_7_10_5) { curl_setopt($this->handle, CURLOPT_ENCODING, ''); } if (defined('CURLOPT_PROTOCOLS')) { // phpcs:ignore PHPCompatibility.Constants.NewConstants.curlopt_protocolsFound curl_setopt($this->handle, CURLOPT_PROTOCOLS, CURLPROTO_HTTP | CURLPROTO_HTTPS); } if (defined('CURLOPT_REDIR_PROTOCOLS')) { // phpcs:ignore PHPCompatibility.Constants.NewConstants.curlopt_redir_protocolsFound curl_setopt($this->handle, CURLOPT_REDIR_PROTOCOLS, CURLPROTO_HTTP | CURLPROTO_HTTPS); } } /** * Destructor */ public function __destruct() { if (is_resource($this->handle)) { curl_close($this->handle); } } /** * Perform a request * * @throws Requests_Exception On a cURL error (`curlerror`) * * @param string $url URL to request * @param array $headers Associative array of request headers * @param string|array $data Data to send either as the POST body, or as parameters in the URL for a GET/HEAD * @param array $options Request options, see {@see Requests::response()} for documentation * @return string Raw HTTP result */ public function request($url, $headers = array(), $data = array(), $options = array()) { $this->hooks = $options['hooks']; $this->setup_handle($url, $headers, $data, $options); $options['hooks']->dispatch('curl.before_send', array(&$this->handle)); if ($options['filename'] !== false) { $this->stream_handle = fopen($options['filename'], 'wb'); } $this->response_data = ''; $this->response_bytes = 0; $this->response_byte_limit = false; if ($options['max_bytes'] !== false) { $this->response_byte_limit = $options['max_bytes']; } if (isset($options['verify'])) { if ($options['verify'] === false) { curl_setopt($this->handle, CURLOPT_SSL_VERIFYHOST, 0); curl_setopt($this->handle, CURLOPT_SSL_VERIFYPEER, 0); } elseif (is_string($options['verify'])) { curl_setopt($this->handle, CURLOPT_CAINFO, $options['verify']); } } if (isset($options['verifyname']) && $options['verifyname'] === false) { curl_setopt($this->handle, CURLOPT_SSL_VERIFYHOST, 0); } curl_exec($this->handle); $response = $this->response_data; $options['hooks']->dispatch('curl.after_send', array()); if (curl_errno($this->handle) === 23 || curl_errno($this->handle) === 61) { // Reset encoding and try again curl_setopt($this->handle, CURLOPT_ENCODING, 'none'); $this->response_data = ''; $this->response_bytes = 0; curl_exec($this->handle); $response = $this->response_data; } $this->process_response($response, $options); // Need to remove the $this reference from the curl handle. // Otherwise Requests_Transport_cURL wont be garbage collected and the curl_close() will never be called. curl_setopt($this->handle, CURLOPT_HEADERFUNCTION, null); curl_setopt($this->handle, CURLOPT_WRITEFUNCTION, null); return $this->headers; } /** * Send multiple requests simultaneously * * @param array $requests Request data * @param array $options Global options * @return array Array of Requests_Response objects (may contain Requests_Exception or string responses as well) */ public function request_multiple($requests, $options) { // If you're not requesting, we can't get any responses ¯\_(ツ)_/¯ if (empty($requests)) { return array(); } $multihandle = curl_multi_init(); $subrequests = array(); $subhandles = array(); $class = get_class($this); foreach ($requests as $id => $request) { $subrequests[$id] = new $class(); $subhandles[$id] = $subrequests[$id]->get_subrequest_handle($request['url'], $request['headers'], $request['data'], $request['options']); $request['options']['hooks']->dispatch('curl.before_multi_add', array(&$subhandles[$id])); curl_multi_add_handle($multihandle, $subhandles[$id]); } $completed = 0; $responses = array(); $subrequestcount = count($subrequests); $request['options']['hooks']->dispatch('curl.before_multi_exec', array(&$multihandle)); do { $active = 0; do { $status = curl_multi_exec($multihandle, $active); } while ($status === CURLM_CALL_MULTI_PERFORM); $to_process = array(); // Read the information as needed while ($done = curl_multi_info_read($multihandle)) { $key = array_search($done['handle'], $subhandles, true); if (!isset($to_process[$key])) { $to_process[$key] = $done; } } // Parse the finished requests before we start getting the new ones foreach ($to_process as $key => $done) { $options = $requests[$key]['options']; if ($done['result'] !== CURLE_OK) { //get error string for handle. $reason = curl_error($done['handle']); $exception = new Requests_Exception_Transport_cURL( $reason, Requests_Exception_Transport_cURL::EASY, $done['handle'], $done['result'] ); $responses[$key] = $exception; $options['hooks']->dispatch('transport.internal.parse_error', array(&$responses[$key], $requests[$key])); } else { $responses[$key] = $subrequests[$key]->process_response($subrequests[$key]->response_data, $options); $options['hooks']->dispatch('transport.internal.parse_response', array(&$responses[$key], $requests[$key])); } curl_multi_remove_handle($multihandle, $done['handle']); curl_close($done['handle']); if (!is_string($responses[$key])) { $options['hooks']->dispatch('multiple.request.complete', array(&$responses[$key], $key)); } $completed++; } } while ($active || $completed < $subrequestcount); $request['options']['hooks']->dispatch('curl.after_multi_exec', array(&$multihandle)); curl_multi_close($multihandle); return $responses; } /** * Get the cURL handle for use in a multi-request * * @param string $url URL to request * @param array $headers Associative array of request headers * @param string|array $data Data to send either as the POST body, or as parameters in the URL for a GET/HEAD * @param array $options Request options, see {@see Requests::response()} for documentation * @return resource Subrequest's cURL handle */ public function &get_subrequest_handle($url, $headers, $data, $options) { $this->setup_handle($url, $headers, $data, $options); if ($options['filename'] !== false) { $this->stream_handle = fopen($options['filename'], 'wb'); } $this->response_data = ''; $this->response_bytes = 0; $this->response_byte_limit = false; if ($options['max_bytes'] !== false) { $this->response_byte_limit = $options['max_bytes']; } $this->hooks = $options['hooks']; return $this->handle; } /** * Setup the cURL handle for the given data * * @param string $url URL to request * @param array $headers Associative array of request headers * @param string|array $data Data to send either as the POST body, or as parameters in the URL for a GET/HEAD * @param array $options Request options, see {@see Requests::response()} for documentation */ protected function setup_handle($url, $headers, $data, $options) { $options['hooks']->dispatch('curl.before_request', array(&$this->handle)); // Force closing the connection for old versions of cURL (<7.22). if (!isset($headers['Connection'])) { $headers['Connection'] = 'close'; } /** * Add "Expect" header. * * By default, cURL adds a "Expect: 100-Continue" to most requests. This header can * add as much as a second to the time it takes for cURL to perform a request. To * prevent this, we need to set an empty "Expect" header. To match the behaviour of * Guzzle, we'll add the empty header to requests that are smaller than 1 MB and use * HTTP/1.1. * * https://curl.se/mail/lib-2017-07/0013.html */ if (!isset($headers['Expect']) && $options['protocol_version'] === 1.1) { $headers['Expect'] = $this->get_expect_header($data); } $headers = Requests::flatten($headers); if (!empty($data)) { $data_format = $options['data_format']; if ($data_format === 'query') { $url = self::format_get($url, $data); $data = ''; } elseif (!is_string($data)) { $data = http_build_query($data, null, '&'); } } switch ($options['type']) { case Requests::POST: curl_setopt($this->handle, CURLOPT_POST, true); curl_setopt($this->handle, CURLOPT_POSTFIELDS, $data); break; case Requests::HEAD: curl_setopt($this->handle, CURLOPT_CUSTOMREQUEST, $options['type']); curl_setopt($this->handle, CURLOPT_NOBODY, true); break; case Requests::TRACE: curl_setopt($this->handle, CURLOPT_CUSTOMREQUEST, $options['type']); break; case Requests::PATCH: case Requests::PUT: case Requests::DELETE: case Requests::OPTIONS: default: curl_setopt($this->handle, CURLOPT_CUSTOMREQUEST, $options['type']); if (!empty($data)) { curl_setopt($this->handle, CURLOPT_POSTFIELDS, $data); } } // cURL requires a minimum timeout of 1 second when using the system // DNS resolver, as it uses `alarm()`, which is second resolution only. // There's no way to detect which DNS resolver is being used from our // end, so we need to round up regardless of the supplied timeout. // // https://github.com/curl/curl/blob/4f45240bc84a9aa648c8f7243be7b79e9f9323a5/lib/hostip.c#L606-L609 $timeout = max($options['timeout'], 1); if (is_int($timeout) || $this->version < self::CURL_7_16_2) { curl_setopt($this->handle, CURLOPT_TIMEOUT, ceil($timeout)); } else { // phpcs:ignore PHPCompatibility.Constants.NewConstants.curlopt_timeout_msFound curl_setopt($this->handle, CURLOPT_TIMEOUT_MS, round($timeout * 1000)); } if (is_int($options['connect_timeout']) || $this->version < self::CURL_7_16_2) { curl_setopt($this->handle, CURLOPT_CONNECTTIMEOUT, ceil($options['connect_timeout'])); } else { // phpcs:ignore PHPCompatibility.Constants.NewConstants.curlopt_connecttimeout_msFound curl_setopt($this->handle, CURLOPT_CONNECTTIMEOUT_MS, round($options['connect_timeout'] * 1000)); } curl_setopt($this->handle, CURLOPT_URL, $url); curl_setopt($this->handle, CURLOPT_REFERER, $url); curl_setopt($this->handle, CURLOPT_USERAGENT, $options['useragent']); if (!empty($headers)) { curl_setopt($this->handle, CURLOPT_HTTPHEADER, $headers); } if ($options['protocol_version'] === 1.1) { curl_setopt($this->handle, CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_1_1); } else { curl_setopt($this->handle, CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_1_0); } if ($options['blocking'] === true) { curl_setopt($this->handle, CURLOPT_HEADERFUNCTION, array($this, 'stream_headers')); curl_setopt($this->handle, CURLOPT_WRITEFUNCTION, array($this, 'stream_body')); curl_setopt($this->handle, CURLOPT_BUFFERSIZE, Requests::BUFFER_SIZE); } } /** * Process a response * * @param string $response Response data from the body * @param array $options Request options * @return string|false HTTP response data including headers. False if non-blocking. * @throws Requests_Exception */ public function process_response($response, $options) { if ($options['blocking'] === false) { $fake_headers = ''; $options['hooks']->dispatch('curl.after_request', array(&$fake_headers)); return false; } if ($options['filename'] !== false && $this->stream_handle) { fclose($this->stream_handle); $this->headers = trim($this->headers); } else { $this->headers .= $response; } if (curl_errno($this->handle)) { $error = sprintf( 'cURL error %s: %s', curl_errno($this->handle), curl_error($this->handle) ); throw new Requests_Exception($error, 'curlerror', $this->handle); } $this->info = curl_getinfo($this->handle); $options['hooks']->dispatch('curl.after_request', array(&$this->headers, &$this->info)); return $this->headers; } /** * Collect the headers as they are received * * @param resource $handle cURL resource * @param string $headers Header string * @return integer Length of provided header */ public function stream_headers($handle, $headers) { // Why do we do this? cURL will send both the final response and any // interim responses, such as a 100 Continue. We don't need that. // (We may want to keep this somewhere just in case) if ($this->done_headers) { $this->headers = ''; $this->done_headers = false; } $this->headers .= $headers; if ($headers === "\r\n") { $this->done_headers = true; } return strlen($headers); } /** * Collect data as it's received * * @since 1.6.1 * * @param resource $handle cURL resource * @param string $data Body data * @return integer Length of provided data */ public function stream_body($handle, $data) { $this->hooks->dispatch('request.progress', array($data, $this->response_bytes, $this->response_byte_limit)); $data_length = strlen($data); // Are we limiting the response size? if ($this->response_byte_limit) { if ($this->response_bytes === $this->response_byte_limit) { // Already at maximum, move on return $data_length; } if (($this->response_bytes + $data_length) > $this->response_byte_limit) { // Limit the length $limited_length = ($this->response_byte_limit - $this->response_bytes); $data = substr($data, 0, $limited_length); } } if ($this->stream_handle) { fwrite($this->stream_handle, $data); } else { $this->response_data .= $data; } $this->response_bytes += strlen($data); return $data_length; } /** * Format a URL given GET data * * @param string $url * @param array|object $data Data to build query using, see {@see https://secure.php.net/http_build_query} * @return string URL with data */ protected static function format_get($url, $data) { if (!empty($data)) { $query = ''; $url_parts = parse_url($url); if (empty($url_parts['query'])) { $url_parts['query'] = ''; } else { $query = $url_parts['query']; } $query .= '&' . http_build_query($data, null, '&'); $query = trim($query, '&'); if (empty($url_parts['query'])) { $url .= '?' . $query; } else { $url = str_replace($url_parts['query'], $query, $url); } } return $url; } /** * Whether this transport is valid * * @codeCoverageIgnore * @return boolean True if the transport is valid, false otherwise. */ public static function test($capabilities = array()) { if (!function_exists('curl_init') || !function_exists('curl_exec')) { return false; } // If needed, check that our installed curl version supports SSL if (isset($capabilities['ssl']) && $capabilities['ssl']) { $curl_version = curl_version(); if (!(CURL_VERSION_SSL & $curl_version['features'])) { return false; } } return true; } /** * Get the correct "Expect" header for the given request data. * * @param string|array $data Data to send either as the POST body, or as parameters in the URL for a GET/HEAD. * @return string The "Expect" header. */ protected function get_expect_header($data) { if (!is_array($data)) { return strlen((string) $data) >= 1048576 ? '100-Continue' : ''; } $bytesize = 0; $iterator = new RecursiveIteratorIterator(new RecursiveArrayIterator($data)); foreach ($iterator as $datum) { $bytesize += strlen((string) $datum); if ($bytesize >= 1048576) { return '100-Continue'; } } return ''; } } dispatch('fsockopen.before_request'); $url_parts = parse_url($url); if (empty($url_parts)) { throw new Requests_Exception('Invalid URL.', 'invalidurl', $url); } $host = $url_parts['host']; $context = stream_context_create(); $verifyname = false; $case_insensitive_headers = new Requests_Utility_CaseInsensitiveDictionary($headers); // HTTPS support if (isset($url_parts['scheme']) && strtolower($url_parts['scheme']) === 'https') { $remote_socket = 'ssl://' . $host; if (!isset($url_parts['port'])) { $url_parts['port'] = 443; } $context_options = array( 'verify_peer' => true, 'capture_peer_cert' => true, ); $verifyname = true; // SNI, if enabled (OpenSSL >=0.9.8j) // phpcs:ignore PHPCompatibility.Constants.NewConstants.openssl_tlsext_server_nameFound if (defined('OPENSSL_TLSEXT_SERVER_NAME') && OPENSSL_TLSEXT_SERVER_NAME) { $context_options['SNI_enabled'] = true; if (isset($options['verifyname']) && $options['verifyname'] === false) { $context_options['SNI_enabled'] = false; } } if (isset($options['verify'])) { if ($options['verify'] === false) { $context_options['verify_peer'] = false; $context_options['verify_peer_name'] = false; $verifyname = false; } elseif (is_string($options['verify'])) { $context_options['cafile'] = $options['verify']; } } if (isset($options['verifyname']) && $options['verifyname'] === false) { $context_options['verify_peer_name'] = false; $verifyname = false; } stream_context_set_option($context, array('ssl' => $context_options)); } else { $remote_socket = 'tcp://' . $host; } $this->max_bytes = $options['max_bytes']; if (!isset($url_parts['port'])) { $url_parts['port'] = 80; } $remote_socket .= ':' . $url_parts['port']; // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_set_error_handler set_error_handler(array($this, 'connect_error_handler'), E_WARNING | E_NOTICE); $options['hooks']->dispatch('fsockopen.remote_socket', array(&$remote_socket)); $socket = stream_socket_client($remote_socket, $errno, $errstr, ceil($options['connect_timeout']), STREAM_CLIENT_CONNECT, $context); restore_error_handler(); if ($verifyname && !$this->verify_certificate_from_context($host, $context)) { throw new Requests_Exception('SSL certificate did not match the requested domain name', 'ssl.no_match'); } if (!$socket) { if ($errno === 0) { // Connection issue throw new Requests_Exception(rtrim($this->connect_error), 'fsockopen.connect_error'); } throw new Requests_Exception($errstr, 'fsockopenerror', null, $errno); } $data_format = $options['data_format']; if ($data_format === 'query') { $path = self::format_get($url_parts, $data); $data = ''; } else { $path = self::format_get($url_parts, array()); } $options['hooks']->dispatch('fsockopen.remote_host_path', array(&$path, $url)); $request_body = ''; $out = sprintf("%s %s HTTP/%.1F\r\n", $options['type'], $path, $options['protocol_version']); if ($options['type'] !== Requests::TRACE) { if (is_array($data)) { $request_body = http_build_query($data, '', '&'); } else { $request_body = $data; } // Always include Content-length on POST requests to prevent // 411 errors from some servers when the body is empty. if (!empty($data) || $options['type'] === Requests::POST) { if (!isset($case_insensitive_headers['Content-Length'])) { $headers['Content-Length'] = strlen($request_body); } if (!isset($case_insensitive_headers['Content-Type'])) { $headers['Content-Type'] = 'application/x-www-form-urlencoded; charset=UTF-8'; } } } if (!isset($case_insensitive_headers['Host'])) { $out .= sprintf('Host: %s', $url_parts['host']); if ((strtolower($url_parts['scheme']) === 'http' && $url_parts['port'] !== 80) || (strtolower($url_parts['scheme']) === 'https' && $url_parts['port'] !== 443)) { $out .= ':' . $url_parts['port']; } $out .= "\r\n"; } if (!isset($case_insensitive_headers['User-Agent'])) { $out .= sprintf("User-Agent: %s\r\n", $options['useragent']); } $accept_encoding = $this->accept_encoding(); if (!isset($case_insensitive_headers['Accept-Encoding']) && !empty($accept_encoding)) { $out .= sprintf("Accept-Encoding: %s\r\n", $accept_encoding); } $headers = Requests::flatten($headers); if (!empty($headers)) { $out .= implode("\r\n", $headers) . "\r\n"; } $options['hooks']->dispatch('fsockopen.after_headers', array(&$out)); if (substr($out, -2) !== "\r\n") { $out .= "\r\n"; } if (!isset($case_insensitive_headers['Connection'])) { $out .= "Connection: Close\r\n"; } $out .= "\r\n" . $request_body; $options['hooks']->dispatch('fsockopen.before_send', array(&$out)); fwrite($socket, $out); $options['hooks']->dispatch('fsockopen.after_send', array($out)); if (!$options['blocking']) { fclose($socket); $fake_headers = ''; $options['hooks']->dispatch('fsockopen.after_request', array(&$fake_headers)); return ''; } $timeout_sec = (int) floor($options['timeout']); if ($timeout_sec === $options['timeout']) { $timeout_msec = 0; } else { $timeout_msec = self::SECOND_IN_MICROSECONDS * $options['timeout'] % self::SECOND_IN_MICROSECONDS; } stream_set_timeout($socket, $timeout_sec, $timeout_msec); $response = ''; $body = ''; $headers = ''; $this->info = stream_get_meta_data($socket); $size = 0; $doingbody = false; $download = false; if ($options['filename']) { $download = fopen($options['filename'], 'wb'); } while (!feof($socket)) { $this->info = stream_get_meta_data($socket); if ($this->info['timed_out']) { throw new Requests_Exception('fsocket timed out', 'timeout'); } $block = fread($socket, Requests::BUFFER_SIZE); if (!$doingbody) { $response .= $block; if (strpos($response, "\r\n\r\n")) { list($headers, $block) = explode("\r\n\r\n", $response, 2); $doingbody = true; } } // Are we in body mode now? if ($doingbody) { $options['hooks']->dispatch('request.progress', array($block, $size, $this->max_bytes)); $data_length = strlen($block); if ($this->max_bytes) { // Have we already hit a limit? if ($size === $this->max_bytes) { continue; } if (($size + $data_length) > $this->max_bytes) { // Limit the length $limited_length = ($this->max_bytes - $size); $block = substr($block, 0, $limited_length); } } $size += strlen($block); if ($download) { fwrite($download, $block); } else { $body .= $block; } } } $this->headers = $headers; if ($download) { fclose($download); } else { $this->headers .= "\r\n\r\n" . $body; } fclose($socket); $options['hooks']->dispatch('fsockopen.after_request', array(&$this->headers, &$this->info)); return $this->headers; } /** * Send multiple requests simultaneously * * @param array $requests Request data (array of 'url', 'headers', 'data', 'options') as per {@see Requests_Transport::request} * @param array $options Global options, see {@see Requests::response()} for documentation * @return array Array of Requests_Response objects (may contain Requests_Exception or string responses as well) */ public function request_multiple($requests, $options) { $responses = array(); $class = get_class($this); foreach ($requests as $id => $request) { try { $handler = new $class(); $responses[$id] = $handler->request($request['url'], $request['headers'], $request['data'], $request['options']); $request['options']['hooks']->dispatch('transport.internal.parse_response', array(&$responses[$id], $request)); } catch (Requests_Exception $e) { $responses[$id] = $e; } if (!is_string($responses[$id])) { $request['options']['hooks']->dispatch('multiple.request.complete', array(&$responses[$id], $id)); } } return $responses; } /** * Retrieve the encodings we can accept * * @return string Accept-Encoding header value */ protected static function accept_encoding() { $type = array(); if (function_exists('gzinflate')) { $type[] = 'deflate;q=1.0'; } if (function_exists('gzuncompress')) { $type[] = 'compress;q=0.5'; } $type[] = 'gzip;q=0.5'; return implode(', ', $type); } /** * Format a URL given GET data * * @param array $url_parts * @param array|object $data Data to build query using, see {@see https://secure.php.net/http_build_query} * @return string URL with data */ protected static function format_get($url_parts, $data) { if (!empty($data)) { if (empty($url_parts['query'])) { $url_parts['query'] = ''; } $url_parts['query'] .= '&' . http_build_query($data, '', '&'); $url_parts['query'] = trim($url_parts['query'], '&'); } if (isset($url_parts['path'])) { if (isset($url_parts['query'])) { $get = $url_parts['path'] . '?' . $url_parts['query']; } else { $get = $url_parts['path']; } } else { $get = '/'; } return $get; } /** * Error handler for stream_socket_client() * * @param int $errno Error number (e.g. E_WARNING) * @param string $errstr Error message */ public function connect_error_handler($errno, $errstr) { // Double-check we can handle it if (($errno & E_WARNING) === 0 && ($errno & E_NOTICE) === 0) { // Return false to indicate the default error handler should engage return false; } $this->connect_error .= $errstr . "\n"; return true; } /** * Verify the certificate against common name and subject alternative names * * Unfortunately, PHP doesn't check the certificate against the alternative * names, leading things like 'https://www.github.com/' to be invalid. * Instead * * @see https://tools.ietf.org/html/rfc2818#section-3.1 RFC2818, Section 3.1 * * @throws Requests_Exception On failure to connect via TLS (`fsockopen.ssl.connect_error`) * @throws Requests_Exception On not obtaining a match for the host (`fsockopen.ssl.no_match`) * @param string $host Host name to verify against * @param resource $context Stream context * @return bool */ public function verify_certificate_from_context($host, $context) { $meta = stream_context_get_options($context); // If we don't have SSL options, then we couldn't make the connection at // all if (empty($meta) || empty($meta['ssl']) || empty($meta['ssl']['peer_certificate'])) { throw new Requests_Exception(rtrim($this->connect_error), 'ssl.connect_error'); } $cert = openssl_x509_parse($meta['ssl']['peer_certificate']); return Requests_SSL::verify_certificate($host, $cert); } /** * Whether this transport is valid * * @codeCoverageIgnore * @return boolean True if the transport is valid, false otherwise. */ public static function test($capabilities = array()) { if (!function_exists('fsockopen')) { return false; } // If needed, check that streams support SSL if (isset($capabilities['ssl']) && $capabilities['ssl']) { if (!extension_loaded('openssl') || !function_exists('openssl_x509_parse')) { return false; } // Currently broken, thanks to https://github.com/facebook/hhvm/issues/2156 if (defined('HHVM_VERSION')) { return false; } } return true; } } type = $type; } if ($code !== null) { $this->code = $code; } if ($message !== null) { $this->reason = $message; } $message = sprintf('%d %s', $this->code, $this->reason); parent::__construct($message, $this->type, $data, $this->code); } /** * Get the error message */ public function getReason() { return $this->reason; } } reason = $reason; } $message = sprintf('%d %s', $this->code, $this->reason); parent::__construct($message, 'httpresponse', $data, $this->code); } /** * Get the status message */ public function getReason() { return $this->reason; } /** * Get the correct exception class for a given error code * * @param int|bool $code HTTP status code, or false if unavailable * @return string Exception class name to use */ public static function get_class($code) { if (!$code) { return 'Requests_Exception_HTTP_Unknown'; } $class = sprintf('Requests_Exception_HTTP_%d', $code); if (class_exists($class)) { return $class; } return 'Requests_Exception_HTTP_Unknown'; } } code = $data->status_code; } parent::__construct($reason, $data); } } user, $this->pass) = $args; } } /** * Register the necessary callbacks * * @see curl_before_send * @see fsockopen_header * @param Requests_Hooks $hooks Hook system */ public function register(Requests_Hooks $hooks) { $hooks->register('curl.before_send', array($this, 'curl_before_send')); $hooks->register('fsockopen.after_headers', array($this, 'fsockopen_header')); } /** * Set cURL parameters before the data is sent * * @param resource $handle cURL resource */ public function curl_before_send(&$handle) { curl_setopt($handle, CURLOPT_HTTPAUTH, CURLAUTH_BASIC); curl_setopt($handle, CURLOPT_USERPWD, $this->getAuthString()); } /** * Add extra headers to the request before sending * * @param string $out HTTP header string */ public function fsockopen_header(&$out) { $out .= sprintf("Authorization: Basic %s\r\n", base64_encode($this->getAuthString())); } /** * Get the authentication string (user:pass) * * @return string */ public function getAuthString() { return $this->user . ':' . $this->pass; } } proxy = $args; } elseif (is_array($args)) { if (count($args) === 1) { list($this->proxy) = $args; } elseif (count($args) === 3) { list($this->proxy, $this->user, $this->pass) = $args; $this->use_authentication = true; } else { throw new Requests_Exception('Invalid number of arguments', 'proxyhttpbadargs'); } } } /** * Register the necessary callbacks * * @since 1.6 * @see curl_before_send * @see fsockopen_remote_socket * @see fsockopen_remote_host_path * @see fsockopen_header * @param Requests_Hooks $hooks Hook system */ public function register(Requests_Hooks $hooks) { $hooks->register('curl.before_send', array($this, 'curl_before_send')); $hooks->register('fsockopen.remote_socket', array($this, 'fsockopen_remote_socket')); $hooks->register('fsockopen.remote_host_path', array($this, 'fsockopen_remote_host_path')); if ($this->use_authentication) { $hooks->register('fsockopen.after_headers', array($this, 'fsockopen_header')); } } /** * Set cURL parameters before the data is sent * * @since 1.6 * @param resource $handle cURL resource */ public function curl_before_send(&$handle) { curl_setopt($handle, CURLOPT_PROXYTYPE, CURLPROXY_HTTP); curl_setopt($handle, CURLOPT_PROXY, $this->proxy); if ($this->use_authentication) { curl_setopt($handle, CURLOPT_PROXYAUTH, CURLAUTH_ANY); curl_setopt($handle, CURLOPT_PROXYUSERPWD, $this->get_auth_string()); } } /** * Alter remote socket information before opening socket connection * * @since 1.6 * @param string $remote_socket Socket connection string */ public function fsockopen_remote_socket(&$remote_socket) { $remote_socket = $this->proxy; } /** * Alter remote path before getting stream data * * @since 1.6 * @param string $path Path to send in HTTP request string ("GET ...") * @param string $url Full URL we're requesting */ public function fsockopen_remote_host_path(&$path, $url) { $path = $url; } /** * Add extra headers to the request before sending * * @since 1.6 * @param string $out HTTP header string */ public function fsockopen_header(&$out) { $out .= sprintf("Proxy-Authorization: Basic %s\r\n", base64_encode($this->get_auth_string())); } /** * Get the authentication string (user:pass) * * @since 1.6 * @return string */ public function get_auth_string() { return $this->user . ':' . $this->pass; } } FF01:0:0:0:0:0:0:101 * ::1 -> 0:0:0:0:0:0:0:1 * * @author Alexander Merz * @author elfrink at introweb dot nl * @author Josh Peck * @copyright 2003-2005 The PHP Group * @license http://www.opensource.org/licenses/bsd-license.php * @param string $ip An IPv6 address * @return string The uncompressed IPv6 address */ public static function uncompress($ip) { if (substr_count($ip, '::') !== 1) { return $ip; } list($ip1, $ip2) = explode('::', $ip); $c1 = ($ip1 === '') ? -1 : substr_count($ip1, ':'); $c2 = ($ip2 === '') ? -1 : substr_count($ip2, ':'); if (strpos($ip2, '.') !== false) { $c2++; } // :: if ($c1 === -1 && $c2 === -1) { $ip = '0:0:0:0:0:0:0:0'; } // ::xxx elseif ($c1 === -1) { $fill = str_repeat('0:', 7 - $c2); $ip = str_replace('::', $fill, $ip); } // xxx:: elseif ($c2 === -1) { $fill = str_repeat(':0', 7 - $c1); $ip = str_replace('::', $fill, $ip); } // xxx::xxx else { $fill = ':' . str_repeat('0:', 6 - $c2 - $c1); $ip = str_replace('::', $fill, $ip); } return $ip; } /** * Compresses an IPv6 address * * RFC 4291 allows you to compress consecutive zero pieces in an address to * '::'. This method expects a valid IPv6 address and compresses consecutive * zero pieces to '::'. * * Example: FF01:0:0:0:0:0:0:101 -> FF01::101 * 0:0:0:0:0:0:0:1 -> ::1 * * @see uncompress() * @param string $ip An IPv6 address * @return string The compressed IPv6 address */ public static function compress($ip) { // Prepare the IP to be compressed $ip = self::uncompress($ip); $ip_parts = self::split_v6_v4($ip); // Replace all leading zeros $ip_parts[0] = preg_replace('/(^|:)0+([0-9])/', '\1\2', $ip_parts[0]); // Find bunches of zeros if (preg_match_all('/(?:^|:)(?:0(?::|$))+/', $ip_parts[0], $matches, PREG_OFFSET_CAPTURE)) { $max = 0; $pos = null; foreach ($matches[0] as $match) { if (strlen($match[0]) > $max) { $max = strlen($match[0]); $pos = $match[1]; } } $ip_parts[0] = substr_replace($ip_parts[0], '::', $pos, $max); } if ($ip_parts[1] !== '') { return implode(':', $ip_parts); } else { return $ip_parts[0]; } } /** * Splits an IPv6 address into the IPv6 and IPv4 representation parts * * RFC 4291 allows you to represent the last two parts of an IPv6 address * using the standard IPv4 representation * * Example: 0:0:0:0:0:0:13.1.68.3 * 0:0:0:0:0:FFFF:129.144.52.38 * * @param string $ip An IPv6 address * @return string[] [0] contains the IPv6 represented part, and [1] the IPv4 represented part */ protected static function split_v6_v4($ip) { if (strpos($ip, '.') !== false) { $pos = strrpos($ip, ':'); $ipv6_part = substr($ip, 0, $pos); $ipv4_part = substr($ip, $pos + 1); return array($ipv6_part, $ipv4_part); } else { return array($ip, ''); } } /** * Checks an IPv6 address * * Checks if the given IP is a valid IPv6 address * * @param string $ip An IPv6 address * @return bool true if $ip is a valid IPv6 address */ public static function check_ipv6($ip) { $ip = self::uncompress($ip); list($ipv6, $ipv4) = self::split_v6_v4($ip); $ipv6 = explode(':', $ipv6); $ipv4 = explode('.', $ipv4); if (count($ipv6) === 8 && count($ipv4) === 1 || count($ipv6) === 6 && count($ipv4) === 4) { foreach ($ipv6 as $ipv6_part) { // The section can't be empty if ($ipv6_part === '') { return false; } // Nor can it be over four characters if (strlen($ipv6_part) > 4) { return false; } // Remove leading zeros (this is safe because of the above) $ipv6_part = ltrim($ipv6_part, '0'); if ($ipv6_part === '') { $ipv6_part = '0'; } // Check the value is valid $value = hexdec($ipv6_part); if (dechex($value) !== strtolower($ipv6_part) || $value < 0 || $value > 0xFFFF) { return false; } } if (count($ipv4) === 4) { foreach ($ipv4 as $ipv4_part) { $value = (int) $ipv4_part; if ((string) $value !== $ipv4_part || $value < 0 || $value > 0xFF) { return false; } } } return true; } else { return false; } } } 0) { if ($position + $length > $strlen) { throw new Requests_Exception('Invalid Unicode codepoint', 'idna.invalidcodepoint', $character); } for ($position++; $remaining > 0; $position++) { $value = ord($input[$position]); // If it is invalid, count the sequence as invalid and reprocess the current byte: if (($value & 0xC0) !== 0x80) { throw new Requests_Exception('Invalid Unicode codepoint', 'idna.invalidcodepoint', $character); } --$remaining; $character |= ($value & 0x3F) << ($remaining * 6); } $position--; } if (// Non-shortest form sequences are invalid $length > 1 && $character <= 0x7F || $length > 2 && $character <= 0x7FF || $length > 3 && $character <= 0xFFFF // Outside of range of ucschar codepoints // Noncharacters || ($character & 0xFFFE) === 0xFFFE || $character >= 0xFDD0 && $character <= 0xFDEF || ( // Everything else not in ucschar $character > 0xD7FF && $character < 0xF900 || $character < 0x20 || $character > 0x7E && $character < 0xA0 || $character > 0xEFFFD ) ) { throw new Requests_Exception('Invalid Unicode codepoint', 'idna.invalidcodepoint', $character); } $codepoints[] = $character; } return $codepoints; } /** * RFC3492-compliant encoder * * @internal Pseudo-code from Section 6.3 is commented with "#" next to relevant code * @throws Requests_Exception On character outside of the domain (never happens with Punycode) (`idna.character_outside_domain`) * * @param string $input UTF-8 encoded string to encode * @return string Punycode-encoded string */ public static function punycode_encode($input) { $output = ''; // let n = initial_n $n = self::BOOTSTRAP_INITIAL_N; // let delta = 0 $delta = 0; // let bias = initial_bias $bias = self::BOOTSTRAP_INITIAL_BIAS; // let h = b = the number of basic code points in the input $h = 0; $b = 0; // see loop // copy them to the output in order $codepoints = self::utf8_to_codepoints($input); $extended = array(); foreach ($codepoints as $char) { if ($char < 128) { // Character is valid ASCII // TODO: this should also check if it's valid for a URL $output .= chr($char); $h++; } // Check if the character is non-ASCII, but below initial n // This never occurs for Punycode, so ignore in coverage // @codeCoverageIgnoreStart elseif ($char < $n) { throw new Requests_Exception('Invalid character', 'idna.character_outside_domain', $char); } // @codeCoverageIgnoreEnd else { $extended[$char] = true; } } $extended = array_keys($extended); sort($extended); $b = $h; // [copy them] followed by a delimiter if b > 0 if (strlen($output) > 0) { $output .= '-'; } // {if the input contains a non-basic code point < n then fail} // while h < length(input) do begin $codepointcount = count($codepoints); while ($h < $codepointcount) { // let m = the minimum code point >= n in the input $m = array_shift($extended); //printf('next code point to insert is %s' . PHP_EOL, dechex($m)); // let delta = delta + (m - n) * (h + 1), fail on overflow $delta += ($m - $n) * ($h + 1); // let n = m $n = $m; // for each code point c in the input (in order) do begin for ($num = 0; $num < $codepointcount; $num++) { $c = $codepoints[$num]; // if c < n then increment delta, fail on overflow if ($c < $n) { $delta++; } // if c == n then begin elseif ($c === $n) { // let q = delta $q = $delta; // for k = base to infinity in steps of base do begin for ($k = self::BOOTSTRAP_BASE; ; $k += self::BOOTSTRAP_BASE) { // let t = tmin if k <= bias {+ tmin}, or // tmax if k >= bias + tmax, or k - bias otherwise if ($k <= ($bias + self::BOOTSTRAP_TMIN)) { $t = self::BOOTSTRAP_TMIN; } elseif ($k >= ($bias + self::BOOTSTRAP_TMAX)) { $t = self::BOOTSTRAP_TMAX; } else { $t = $k - $bias; } // if q < t then break if ($q < $t) { break; } // output the code point for digit t + ((q - t) mod (base - t)) $digit = $t + (($q - $t) % (self::BOOTSTRAP_BASE - $t)); $output .= self::digit_to_char($digit); // let q = (q - t) div (base - t) $q = floor(($q - $t) / (self::BOOTSTRAP_BASE - $t)); } // end // output the code point for digit q $output .= self::digit_to_char($q); // let bias = adapt(delta, h + 1, test h equals b?) $bias = self::adapt($delta, $h + 1, $h === $b); // let delta = 0 $delta = 0; // increment h $h++; } // end } // end // increment delta and n $delta++; $n++; } // end return $output; } /** * Convert a digit to its respective character * * @see https://tools.ietf.org/html/rfc3492#section-5 * @throws Requests_Exception On invalid digit (`idna.invalid_digit`) * * @param int $digit Digit in the range 0-35 * @return string Single character corresponding to digit */ protected static function digit_to_char($digit) { // @codeCoverageIgnoreStart // As far as I know, this never happens, but still good to be sure. if ($digit < 0 || $digit > 35) { throw new Requests_Exception(sprintf('Invalid digit %d', $digit), 'idna.invalid_digit', $digit); } // @codeCoverageIgnoreEnd $digits = 'abcdefghijklmnopqrstuvwxyz0123456789'; return substr($digits, $digit, 1); } /** * Adapt the bias * * @see https://tools.ietf.org/html/rfc3492#section-6.1 * @param int $delta * @param int $numpoints * @param bool $firsttime * @return int New bias * * function adapt(delta,numpoints,firsttime): */ protected static function adapt($delta, $numpoints, $firsttime) { // if firsttime then let delta = delta div damp if ($firsttime) { $delta = floor($delta / self::BOOTSTRAP_DAMP); } // else let delta = delta div 2 else { $delta = floor($delta / 2); } // let delta = delta + (delta div numpoints) $delta += floor($delta / $numpoints); // let k = 0 $k = 0; // while delta > ((base - tmin) * tmax) div 2 do begin $max = floor(((self::BOOTSTRAP_BASE - self::BOOTSTRAP_TMIN) * self::BOOTSTRAP_TMAX) / 2); while ($delta > $max) { // let delta = delta div (base - tmin) $delta = floor($delta / (self::BOOTSTRAP_BASE - self::BOOTSTRAP_TMIN)); // let k = k + base $k += self::BOOTSTRAP_BASE; } // end // return k + (((base - tmin + 1) * delta) div (delta + skew)) return $k + floor(((self::BOOTSTRAP_BASE - self::BOOTSTRAP_TMIN + 1) * $delta) / ($delta + self::BOOTSTRAP_SKEW)); } } type = $type; $this->data = $data; } /** * Like {@see getCode()}, but a string code. * * @codeCoverageIgnore * @return string */ public function getType() { return $this->type; } /** * Gives any relevant data * * @codeCoverageIgnore * @return mixed */ public function getData() { return $this->data; } } name = $name; $this->value = $value; $this->attributes = $attributes; $default_flags = array( 'creation' => time(), 'last-access' => time(), 'persistent' => false, 'host-only' => true, ); $this->flags = array_merge($default_flags, $flags); $this->reference_time = time(); if ($reference_time !== null) { $this->reference_time = $reference_time; } $this->normalize(); } /** * Check if a cookie is expired. * * Checks the age against $this->reference_time to determine if the cookie * is expired. * * @return boolean True if expired, false if time is valid. */ public function is_expired() { // RFC6265, s. 4.1.2.2: // If a cookie has both the Max-Age and the Expires attribute, the Max- // Age attribute has precedence and controls the expiration date of the // cookie. if (isset($this->attributes['max-age'])) { $max_age = $this->attributes['max-age']; return $max_age < $this->reference_time; } if (isset($this->attributes['expires'])) { $expires = $this->attributes['expires']; return $expires < $this->reference_time; } return false; } /** * Check if a cookie is valid for a given URI * * @param Requests_IRI $uri URI to check * @return boolean Whether the cookie is valid for the given URI */ public function uri_matches(Requests_IRI $uri) { if (!$this->domain_matches($uri->host)) { return false; } if (!$this->path_matches($uri->path)) { return false; } return empty($this->attributes['secure']) || $uri->scheme === 'https'; } /** * Check if a cookie is valid for a given domain * * @param string $string Domain to check * @return boolean Whether the cookie is valid for the given domain */ public function domain_matches($string) { if (!isset($this->attributes['domain'])) { // Cookies created manually; cookies created by Requests will set // the domain to the requested domain return true; } $domain_string = $this->attributes['domain']; if ($domain_string === $string) { // The domain string and the string are identical. return true; } // If the cookie is marked as host-only and we don't have an exact // match, reject the cookie if ($this->flags['host-only'] === true) { return false; } if (strlen($string) <= strlen($domain_string)) { // For obvious reasons, the string cannot be a suffix if the domain // is shorter than the domain string return false; } if (substr($string, -1 * strlen($domain_string)) !== $domain_string) { // The domain string should be a suffix of the string. return false; } $prefix = substr($string, 0, strlen($string) - strlen($domain_string)); if (substr($prefix, -1) !== '.') { // The last character of the string that is not included in the // domain string should be a %x2E (".") character. return false; } // The string should be a host name (i.e., not an IP address). return !preg_match('#^(.+\.)\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$#', $string); } /** * Check if a cookie is valid for a given path * * From the path-match check in RFC 6265 section 5.1.4 * * @param string $request_path Path to check * @return boolean Whether the cookie is valid for the given path */ public function path_matches($request_path) { if (empty($request_path)) { // Normalize empty path to root $request_path = '/'; } if (!isset($this->attributes['path'])) { // Cookies created manually; cookies created by Requests will set // the path to the requested path return true; } $cookie_path = $this->attributes['path']; if ($cookie_path === $request_path) { // The cookie-path and the request-path are identical. return true; } if (strlen($request_path) > strlen($cookie_path) && substr($request_path, 0, strlen($cookie_path)) === $cookie_path) { if (substr($cookie_path, -1) === '/') { // The cookie-path is a prefix of the request-path, and the last // character of the cookie-path is %x2F ("/"). return true; } if (substr($request_path, strlen($cookie_path), 1) === '/') { // The cookie-path is a prefix of the request-path, and the // first character of the request-path that is not included in // the cookie-path is a %x2F ("/") character. return true; } } return false; } /** * Normalize cookie and attributes * * @return boolean Whether the cookie was successfully normalized */ public function normalize() { foreach ($this->attributes as $key => $value) { $orig_value = $value; $value = $this->normalize_attribute($key, $value); if ($value === null) { unset($this->attributes[$key]); continue; } if ($value !== $orig_value) { $this->attributes[$key] = $value; } } return true; } /** * Parse an individual cookie attribute * * Handles parsing individual attributes from the cookie values. * * @param string $name Attribute name * @param string|boolean $value Attribute value (string value, or true if empty/flag) * @return mixed Value if available, or null if the attribute value is invalid (and should be skipped) */ protected function normalize_attribute($name, $value) { switch (strtolower($name)) { case 'expires': // Expiration parsing, as per RFC 6265 section 5.2.1 if (is_int($value)) { return $value; } $expiry_time = strtotime($value); if ($expiry_time === false) { return null; } return $expiry_time; case 'max-age': // Expiration parsing, as per RFC 6265 section 5.2.2 if (is_int($value)) { return $value; } // Check that we have a valid age if (!preg_match('/^-?\d+$/', $value)) { return null; } $delta_seconds = (int) $value; if ($delta_seconds <= 0) { $expiry_time = 0; } else { $expiry_time = $this->reference_time + $delta_seconds; } return $expiry_time; case 'domain': // Domains are not required as per RFC 6265 section 5.2.3 if (empty($value)) { return null; } // Domain normalization, as per RFC 6265 section 5.2.3 if ($value[0] === '.') { $value = substr($value, 1); } return $value; default: return $value; } } /** * Format a cookie for a Cookie header * * This is used when sending cookies to a server. * * @return string Cookie formatted for Cookie header */ public function format_for_header() { return sprintf('%s=%s', $this->name, $this->value); } /** * Format a cookie for a Cookie header * * @codeCoverageIgnore * @deprecated Use {@see Requests_Cookie::format_for_header} * @return string */ public function formatForHeader() { return $this->format_for_header(); } /** * Format a cookie for a Set-Cookie header * * This is used when sending cookies to clients. This isn't really * applicable to client-side usage, but might be handy for debugging. * * @return string Cookie formatted for Set-Cookie header */ public function format_for_set_cookie() { $header_value = $this->format_for_header(); if (!empty($this->attributes)) { $parts = array(); foreach ($this->attributes as $key => $value) { // Ignore non-associative attributes if (is_numeric($key)) { $parts[] = $value; } else { $parts[] = sprintf('%s=%s', $key, $value); } } $header_value .= '; ' . implode('; ', $parts); } return $header_value; } /** * Format a cookie for a Set-Cookie header * * @codeCoverageIgnore * @deprecated Use {@see Requests_Cookie::format_for_set_cookie} * @return string */ public function formatForSetCookie() { return $this->format_for_set_cookie(); } /** * Get the cookie value * * Attributes and other data can be accessed via methods. */ public function __toString() { return $this->value; } /** * Parse a cookie string into a cookie object * * Based on Mozilla's parsing code in Firefox and related projects, which * is an intentional deviation from RFC 2109 and RFC 2616. RFC 6265 * specifies some of this handling, but not in a thorough manner. * * @param string Cookie header value (from a Set-Cookie header) * @return Requests_Cookie Parsed cookie object */ public static function parse($string, $name = '', $reference_time = null) { $parts = explode(';', $string); $kvparts = array_shift($parts); if (!empty($name)) { $value = $string; } elseif (strpos($kvparts, '=') === false) { // Some sites might only have a value without the equals separator. // Deviate from RFC 6265 and pretend it was actually a blank name // (`=foo`) // // https://bugzilla.mozilla.org/show_bug.cgi?id=169091 $name = ''; $value = $kvparts; } else { list($name, $value) = explode('=', $kvparts, 2); } $name = trim($name); $value = trim($value); // Attribute key are handled case-insensitively $attributes = new Requests_Utility_CaseInsensitiveDictionary(); if (!empty($parts)) { foreach ($parts as $part) { if (strpos($part, '=') === false) { $part_key = $part; $part_value = true; } else { list($part_key, $part_value) = explode('=', $part, 2); $part_value = trim($part_value); } $part_key = trim($part_key); $attributes[$part_key] = $part_value; } } return new Requests_Cookie($name, $value, $attributes, array(), $reference_time); } /** * Parse all Set-Cookie headers from request headers * * @param Requests_Response_Headers $headers Headers to parse from * @param Requests_IRI|null $origin URI for comparing cookie origins * @param int|null $time Reference time for expiration calculation * @return array */ public static function parse_from_headers(Requests_Response_Headers $headers, Requests_IRI $origin = null, $time = null) { $cookie_headers = $headers->getValues('Set-Cookie'); if (empty($cookie_headers)) { return array(); } $cookies = array(); foreach ($cookie_headers as $header) { $parsed = self::parse($header, '', $time); // Default domain/path attributes if (empty($parsed->attributes['domain']) && !empty($origin)) { $parsed->attributes['domain'] = $origin->host; $parsed->flags['host-only'] = true; } else { $parsed->flags['host-only'] = false; } $path_is_valid = (!empty($parsed->attributes['path']) && $parsed->attributes['path'][0] === '/'); if (!$path_is_valid && !empty($origin)) { $path = $origin->path; // Default path normalization as per RFC 6265 section 5.1.4 if (substr($path, 0, 1) !== '/') { // If the uri-path is empty or if the first character of // the uri-path is not a %x2F ("/") character, output // %x2F ("/") and skip the remaining steps. $path = '/'; } elseif (substr_count($path, '/') === 1) { // If the uri-path contains no more than one %x2F ("/") // character, output %x2F ("/") and skip the remaining // step. $path = '/'; } else { // Output the characters of the uri-path from the first // character up to, but not including, the right-most // %x2F ("/"). $path = substr($path, 0, strrpos($path, '/')); } $parsed->attributes['path'] = $path; } // Reject invalid cookie domains if (!empty($origin) && !$parsed->domain_matches($origin->host)) { continue; } $cookies[$parsed->name] = $parsed; } return $cookies; } /** * Parse all Set-Cookie headers from request headers * * @codeCoverageIgnore * @deprecated Use {@see Requests_Cookie::parse_from_headers} * @return array */ public static function parseFromHeaders(Requests_Response_Headers $headers) { return self::parse_from_headers($headers); } } useragent = 'X';` * * @var array */ public $options = array(); /** * Create a new session * * @param string|null $url Base URL for requests * @param array $headers Default headers for requests * @param array $data Default data for requests * @param array $options Default options for requests */ public function __construct($url = null, $headers = array(), $data = array(), $options = array()) { $this->url = $url; $this->headers = $headers; $this->data = $data; $this->options = $options; if (empty($this->options['cookies'])) { $this->options['cookies'] = new Requests_Cookie_Jar(); } } /** * Get a property's value * * @param string $key Property key * @return mixed|null Property value, null if none found */ public function __get($key) { if (isset($this->options[$key])) { return $this->options[$key]; } return null; } /** * Set a property's value * * @param string $key Property key * @param mixed $value Property value */ public function __set($key, $value) { $this->options[$key] = $value; } /** * Remove a property's value * * @param string $key Property key */ public function __isset($key) { return isset($this->options[$key]); } /** * Remove a property's value * * @param string $key Property key */ public function __unset($key) { if (isset($this->options[$key])) { unset($this->options[$key]); } } /**#@+ * @see request() * @param string $url * @param array $headers * @param array $options * @return Requests_Response */ /** * Send a GET request */ public function get($url, $headers = array(), $options = array()) { return $this->request($url, $headers, null, Requests::GET, $options); } /** * Send a HEAD request */ public function head($url, $headers = array(), $options = array()) { return $this->request($url, $headers, null, Requests::HEAD, $options); } /** * Send a DELETE request */ public function delete($url, $headers = array(), $options = array()) { return $this->request($url, $headers, null, Requests::DELETE, $options); } /**#@-*/ /**#@+ * @see request() * @param string $url * @param array $headers * @param array $data * @param array $options * @return Requests_Response */ /** * Send a POST request */ public function post($url, $headers = array(), $data = array(), $options = array()) { return $this->request($url, $headers, $data, Requests::POST, $options); } /** * Send a PUT request */ public function put($url, $headers = array(), $data = array(), $options = array()) { return $this->request($url, $headers, $data, Requests::PUT, $options); } /** * Send a PATCH request * * Note: Unlike {@see post} and {@see put}, `$headers` is required, as the * specification recommends that should send an ETag * * @link https://tools.ietf.org/html/rfc5789 */ public function patch($url, $headers, $data = array(), $options = array()) { return $this->request($url, $headers, $data, Requests::PATCH, $options); } /**#@-*/ /** * Main interface for HTTP requests * * This method initiates a request and sends it via a transport before * parsing. * * @see Requests::request() * * @throws Requests_Exception On invalid URLs (`nonhttp`) * * @param string $url URL to request * @param array $headers Extra headers to send with the request * @param array|null $data Data to send either as a query string for GET/HEAD requests, or in the body for POST requests * @param string $type HTTP request type (use Requests constants) * @param array $options Options for the request (see {@see Requests::request}) * @return Requests_Response */ public function request($url, $headers = array(), $data = array(), $type = Requests::GET, $options = array()) { $request = $this->merge_request(compact('url', 'headers', 'data', 'options')); return Requests::request($request['url'], $request['headers'], $request['data'], $type, $request['options']); } /** * Send multiple HTTP requests simultaneously * * @see Requests::request_multiple() * * @param array $requests Requests data (see {@see Requests::request_multiple}) * @param array $options Global and default options (see {@see Requests::request}) * @return array Responses (either Requests_Response or a Requests_Exception object) */ public function request_multiple($requests, $options = array()) { foreach ($requests as $key => $request) { $requests[$key] = $this->merge_request($request, false); } $options = array_merge($this->options, $options); // Disallow forcing the type, as that's a per request setting unset($options['type']); return Requests::request_multiple($requests, $options); } /** * Merge a request's data with the default data * * @param array $request Request data (same form as {@see request_multiple}) * @param boolean $merge_options Should we merge options as well? * @return array Request data */ protected function merge_request($request, $merge_options = true) { if ($this->url !== null) { $request['url'] = Requests_IRI::absolutize($this->url, $request['url']); $request['url'] = $request['url']->uri; } if (empty($request['headers'])) { $request['headers'] = array(); } $request['headers'] = array_merge($this->headers, $request['headers']); if (empty($request['data'])) { if (is_array($this->data)) { $request['data'] = $this->data; } } elseif (is_array($request['data']) && is_array($this->data)) { $request['data'] = array_merge($this->data, $request['data']); } if ($merge_options !== false) { $request['options'] = array_merge($this->options, $request['options']); // Disallow forcing the type, as that's a per request setting unset($request['options']['type']); } return $request; } } $value) { $this->offsetSet($key, $value); } } /** * Check if the given item exists * * @param string $key Item key * @return boolean Does the item exist? */ public function offsetExists($key) { $key = strtolower($key); return isset($this->data[$key]); } /** * Get the value for the item * * @param string $key Item key * @return string|null Item value (null if offsetExists is false) */ public function offsetGet($key) { $key = strtolower($key); if (!isset($this->data[$key])) { return null; } return $this->data[$key]; } /** * Set the given item * * @throws Requests_Exception On attempting to use dictionary as list (`invalidset`) * * @param string $key Item name * @param string $value Item value */ public function offsetSet($key, $value) { if ($key === null) { throw new Requests_Exception('Object is a dictionary, not a list', 'invalidset'); } $key = strtolower($key); $this->data[$key] = $value; } /** * Unset the given header * * @param string $key */ public function offsetUnset($key) { unset($this->data[strtolower($key)]); } /** * Get an iterator for the data * * @return ArrayIterator */ public function getIterator() { return new ArrayIterator($this->data); } /** * Get the headers as an array * * @return array Header data */ public function getAll() { return $this->data; } } callback = $callback; } /** * Get the current item's value after filtering * * @return string */ public function current() { $value = parent::current(); if (is_callable($this->callback)) { $value = call_user_func($this->callback, $value); } return $value; } /** * @inheritdoc */ public function unserialize($serialized) {} /** * @inheritdoc * * @phpcs:disable PHPCompatibility.FunctionNameRestrictions.NewMagicMethods.__unserializeFound */ public function __unserialize($serialized) {} public function __wakeup() { unset($this->callback); } } array( 'port' => 674 ), 'dict' => array( 'port' => 2628 ), 'file' => array( 'ihost' => 'localhost' ), 'http' => array( 'port' => 80, ), 'https' => array( 'port' => 443, ), ); /** * Return the entire IRI when you try and read the object as a string * * @return string */ public function __toString() { return $this->get_iri(); } /** * Overload __set() to provide access via properties * * @param string $name Property name * @param mixed $value Property value */ public function __set($name, $value) { if (method_exists($this, 'set_' . $name)) { call_user_func(array($this, 'set_' . $name), $value); } elseif ( $name === 'iauthority' || $name === 'iuserinfo' || $name === 'ihost' || $name === 'ipath' || $name === 'iquery' || $name === 'ifragment' ) { call_user_func(array($this, 'set_' . substr($name, 1)), $value); } } /** * Overload __get() to provide access via properties * * @param string $name Property name * @return mixed */ public function __get($name) { // isset() returns false for null, we don't want to do that // Also why we use array_key_exists below instead of isset() $props = get_object_vars($this); if ( $name === 'iri' || $name === 'uri' || $name === 'iauthority' || $name === 'authority' ) { $method = 'get_' . $name; $return = $this->$method(); } elseif (array_key_exists($name, $props)) { $return = $this->$name; } // host -> ihost elseif (($prop = 'i' . $name) && array_key_exists($prop, $props)) { $name = $prop; $return = $this->$prop; } // ischeme -> scheme elseif (($prop = substr($name, 1)) && array_key_exists($prop, $props)) { $name = $prop; $return = $this->$prop; } else { trigger_error('Undefined property: ' . get_class($this) . '::' . $name, E_USER_NOTICE); $return = null; } if ($return === null && isset($this->normalization[$this->scheme][$name])) { return $this->normalization[$this->scheme][$name]; } else { return $return; } } /** * Overload __isset() to provide access via properties * * @param string $name Property name * @return bool */ public function __isset($name) { return (method_exists($this, 'get_' . $name) || isset($this->$name)); } /** * Overload __unset() to provide access via properties * * @param string $name Property name */ public function __unset($name) { if (method_exists($this, 'set_' . $name)) { call_user_func(array($this, 'set_' . $name), ''); } } /** * Create a new IRI object, from a specified string * * @param string|null $iri */ public function __construct($iri = null) { $this->set_iri($iri); } /** * Create a new IRI object by resolving a relative IRI * * Returns false if $base is not absolute, otherwise an IRI. * * @param Requests_IRI|string $base (Absolute) Base IRI * @param Requests_IRI|string $relative Relative IRI * @return Requests_IRI|false */ public static function absolutize($base, $relative) { if (!($relative instanceof Requests_IRI)) { $relative = new Requests_IRI($relative); } if (!$relative->is_valid()) { return false; } elseif ($relative->scheme !== null) { return clone $relative; } if (!($base instanceof Requests_IRI)) { $base = new Requests_IRI($base); } if ($base->scheme === null || !$base->is_valid()) { return false; } if ($relative->get_iri() !== '') { if ($relative->iuserinfo !== null || $relative->ihost !== null || $relative->port !== null) { $target = clone $relative; $target->scheme = $base->scheme; } else { $target = new Requests_IRI; $target->scheme = $base->scheme; $target->iuserinfo = $base->iuserinfo; $target->ihost = $base->ihost; $target->port = $base->port; if ($relative->ipath !== '') { if ($relative->ipath[0] === '/') { $target->ipath = $relative->ipath; } elseif (($base->iuserinfo !== null || $base->ihost !== null || $base->port !== null) && $base->ipath === '') { $target->ipath = '/' . $relative->ipath; } elseif (($last_segment = strrpos($base->ipath, '/')) !== false) { $target->ipath = substr($base->ipath, 0, $last_segment + 1) . $relative->ipath; } else { $target->ipath = $relative->ipath; } $target->ipath = $target->remove_dot_segments($target->ipath); $target->iquery = $relative->iquery; } else { $target->ipath = $base->ipath; if ($relative->iquery !== null) { $target->iquery = $relative->iquery; } elseif ($base->iquery !== null) { $target->iquery = $base->iquery; } } $target->ifragment = $relative->ifragment; } } else { $target = clone $base; $target->ifragment = null; } $target->scheme_normalization(); return $target; } /** * Parse an IRI into scheme/authority/path/query/fragment segments * * @param string $iri * @return array */ protected function parse_iri($iri) { $iri = trim($iri, "\x20\x09\x0A\x0C\x0D"); $has_match = preg_match('/^((?P[^:\/?#]+):)?(\/\/(?P[^\/?#]*))?(?P[^?#]*)(\?(?P[^#]*))?(#(?P.*))?$/', $iri, $match); if (!$has_match) { throw new Requests_Exception('Cannot parse supplied IRI', 'iri.cannot_parse', $iri); } if ($match[1] === '') { $match['scheme'] = null; } if (!isset($match[3]) || $match[3] === '') { $match['authority'] = null; } if (!isset($match[5])) { $match['path'] = ''; } if (!isset($match[6]) || $match[6] === '') { $match['query'] = null; } if (!isset($match[8]) || $match[8] === '') { $match['fragment'] = null; } return $match; } /** * Remove dot segments from a path * * @param string $input * @return string */ protected function remove_dot_segments($input) { $output = ''; while (strpos($input, './') !== false || strpos($input, '/.') !== false || $input === '.' || $input === '..') { // A: If the input buffer begins with a prefix of "../" or "./", // then remove that prefix from the input buffer; otherwise, if (strpos($input, '../') === 0) { $input = substr($input, 3); } elseif (strpos($input, './') === 0) { $input = substr($input, 2); } // B: if the input buffer begins with a prefix of "/./" or "/.", // where "." is a complete path segment, then replace that prefix // with "/" in the input buffer; otherwise, elseif (strpos($input, '/./') === 0) { $input = substr($input, 2); } elseif ($input === '/.') { $input = '/'; } // C: if the input buffer begins with a prefix of "/../" or "/..", // where ".." is a complete path segment, then replace that prefix // with "/" in the input buffer and remove the last segment and its // preceding "/" (if any) from the output buffer; otherwise, elseif (strpos($input, '/../') === 0) { $input = substr($input, 3); $output = substr_replace($output, '', strrpos($output, '/')); } elseif ($input === '/..') { $input = '/'; $output = substr_replace($output, '', strrpos($output, '/')); } // D: if the input buffer consists only of "." or "..", then remove // that from the input buffer; otherwise, elseif ($input === '.' || $input === '..') { $input = ''; } // E: move the first path segment in the input buffer to the end of // the output buffer, including the initial "/" character (if any) // and any subsequent characters up to, but not including, the next // "/" character or the end of the input buffer elseif (($pos = strpos($input, '/', 1)) !== false) { $output .= substr($input, 0, $pos); $input = substr_replace($input, '', 0, $pos); } else { $output .= $input; $input = ''; } } return $output . $input; } /** * Replace invalid character with percent encoding * * @param string $string Input string * @param string $extra_chars Valid characters not in iunreserved or * iprivate (this is ASCII-only) * @param bool $iprivate Allow iprivate * @return string */ protected function replace_invalid_with_pct_encoding($string, $extra_chars, $iprivate = false) { // Normalize as many pct-encoded sections as possible $string = preg_replace_callback('/(?:%[A-Fa-f0-9]{2})+/', array($this, 'remove_iunreserved_percent_encoded'), $string); // Replace invalid percent characters $string = preg_replace('/%(?![A-Fa-f0-9]{2})/', '%25', $string); // Add unreserved and % to $extra_chars (the latter is safe because all // pct-encoded sections are now valid). $extra_chars .= 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-._~%'; // Now replace any bytes that aren't allowed with their pct-encoded versions $position = 0; $strlen = strlen($string); while (($position += strspn($string, $extra_chars, $position)) < $strlen) { $value = ord($string[$position]); // Start position $start = $position; // By default we are valid $valid = true; // No one byte sequences are valid due to the while. // Two byte sequence: if (($value & 0xE0) === 0xC0) { $character = ($value & 0x1F) << 6; $length = 2; $remaining = 1; } // Three byte sequence: elseif (($value & 0xF0) === 0xE0) { $character = ($value & 0x0F) << 12; $length = 3; $remaining = 2; } // Four byte sequence: elseif (($value & 0xF8) === 0xF0) { $character = ($value & 0x07) << 18; $length = 4; $remaining = 3; } // Invalid byte: else { $valid = false; $length = 1; $remaining = 0; } if ($remaining) { if ($position + $length <= $strlen) { for ($position++; $remaining; $position++) { $value = ord($string[$position]); // Check that the byte is valid, then add it to the character: if (($value & 0xC0) === 0x80) { $character |= ($value & 0x3F) << (--$remaining * 6); } // If it is invalid, count the sequence as invalid and reprocess the current byte: else { $valid = false; $position--; break; } } } else { $position = $strlen - 1; $valid = false; } } // Percent encode anything invalid or not in ucschar if ( // Invalid sequences !$valid // Non-shortest form sequences are invalid || $length > 1 && $character <= 0x7F || $length > 2 && $character <= 0x7FF || $length > 3 && $character <= 0xFFFF // Outside of range of ucschar codepoints // Noncharacters || ($character & 0xFFFE) === 0xFFFE || $character >= 0xFDD0 && $character <= 0xFDEF || ( // Everything else not in ucschar $character > 0xD7FF && $character < 0xF900 || $character < 0xA0 || $character > 0xEFFFD ) && ( // Everything not in iprivate, if it applies !$iprivate || $character < 0xE000 || $character > 0x10FFFD ) ) { // If we were a character, pretend we weren't, but rather an error. if ($valid) { $position--; } for ($j = $start; $j <= $position; $j++) { $string = substr_replace($string, sprintf('%%%02X', ord($string[$j])), $j, 1); $j += 2; $position += 2; $strlen += 2; } } } return $string; } /** * Callback function for preg_replace_callback. * * Removes sequences of percent encoded bytes that represent UTF-8 * encoded characters in iunreserved * * @param array $match PCRE match * @return string Replacement */ protected function remove_iunreserved_percent_encoded($match) { // As we just have valid percent encoded sequences we can just explode // and ignore the first member of the returned array (an empty string). $bytes = explode('%', $match[0]); // Initialize the new string (this is what will be returned) and that // there are no bytes remaining in the current sequence (unsurprising // at the first byte!). $string = ''; $remaining = 0; // Loop over each and every byte, and set $value to its value for ($i = 1, $len = count($bytes); $i < $len; $i++) { $value = hexdec($bytes[$i]); // If we're the first byte of sequence: if (!$remaining) { // Start position $start = $i; // By default we are valid $valid = true; // One byte sequence: if ($value <= 0x7F) { $character = $value; $length = 1; } // Two byte sequence: elseif (($value & 0xE0) === 0xC0) { $character = ($value & 0x1F) << 6; $length = 2; $remaining = 1; } // Three byte sequence: elseif (($value & 0xF0) === 0xE0) { $character = ($value & 0x0F) << 12; $length = 3; $remaining = 2; } // Four byte sequence: elseif (($value & 0xF8) === 0xF0) { $character = ($value & 0x07) << 18; $length = 4; $remaining = 3; } // Invalid byte: else { $valid = false; $remaining = 0; } } // Continuation byte: else { // Check that the byte is valid, then add it to the character: if (($value & 0xC0) === 0x80) { $remaining--; $character |= ($value & 0x3F) << ($remaining * 6); } // If it is invalid, count the sequence as invalid and reprocess the current byte as the start of a sequence: else { $valid = false; $remaining = 0; $i--; } } // If we've reached the end of the current byte sequence, append it to Unicode::$data if (!$remaining) { // Percent encode anything invalid or not in iunreserved if ( // Invalid sequences !$valid // Non-shortest form sequences are invalid || $length > 1 && $character <= 0x7F || $length > 2 && $character <= 0x7FF || $length > 3 && $character <= 0xFFFF // Outside of range of iunreserved codepoints || $character < 0x2D || $character > 0xEFFFD // Noncharacters || ($character & 0xFFFE) === 0xFFFE || $character >= 0xFDD0 && $character <= 0xFDEF // Everything else not in iunreserved (this is all BMP) || $character === 0x2F || $character > 0x39 && $character < 0x41 || $character > 0x5A && $character < 0x61 || $character > 0x7A && $character < 0x7E || $character > 0x7E && $character < 0xA0 || $character > 0xD7FF && $character < 0xF900 ) { for ($j = $start; $j <= $i; $j++) { $string .= '%' . strtoupper($bytes[$j]); } } else { for ($j = $start; $j <= $i; $j++) { $string .= chr(hexdec($bytes[$j])); } } } } // If we have any bytes left over they are invalid (i.e., we are // mid-way through a multi-byte sequence) if ($remaining) { for ($j = $start; $j < $len; $j++) { $string .= '%' . strtoupper($bytes[$j]); } } return $string; } protected function scheme_normalization() { if (isset($this->normalization[$this->scheme]['iuserinfo']) && $this->iuserinfo === $this->normalization[$this->scheme]['iuserinfo']) { $this->iuserinfo = null; } if (isset($this->normalization[$this->scheme]['ihost']) && $this->ihost === $this->normalization[$this->scheme]['ihost']) { $this->ihost = null; } if (isset($this->normalization[$this->scheme]['port']) && $this->port === $this->normalization[$this->scheme]['port']) { $this->port = null; } if (isset($this->normalization[$this->scheme]['ipath']) && $this->ipath === $this->normalization[$this->scheme]['ipath']) { $this->ipath = ''; } if (isset($this->ihost) && empty($this->ipath)) { $this->ipath = '/'; } if (isset($this->normalization[$this->scheme]['iquery']) && $this->iquery === $this->normalization[$this->scheme]['iquery']) { $this->iquery = null; } if (isset($this->normalization[$this->scheme]['ifragment']) && $this->ifragment === $this->normalization[$this->scheme]['ifragment']) { $this->ifragment = null; } } /** * Check if the object represents a valid IRI. This needs to be done on each * call as some things change depending on another part of the IRI. * * @return bool */ public function is_valid() { $isauthority = $this->iuserinfo !== null || $this->ihost !== null || $this->port !== null; if ($this->ipath !== '' && ( $isauthority && $this->ipath[0] !== '/' || ( $this->scheme === null && !$isauthority && strpos($this->ipath, ':') !== false && (strpos($this->ipath, '/') === false ? true : strpos($this->ipath, ':') < strpos($this->ipath, '/')) ) ) ) { return false; } return true; } /** * Set the entire IRI. Returns true on success, false on failure (if there * are any invalid characters). * * @param string $iri * @return bool */ protected function set_iri($iri) { static $cache; if (!$cache) { $cache = array(); } if ($iri === null) { return true; } if (isset($cache[$iri])) { list($this->scheme, $this->iuserinfo, $this->ihost, $this->port, $this->ipath, $this->iquery, $this->ifragment, $return) = $cache[$iri]; return $return; } $parsed = $this->parse_iri((string) $iri); $return = $this->set_scheme($parsed['scheme']) && $this->set_authority($parsed['authority']) && $this->set_path($parsed['path']) && $this->set_query($parsed['query']) && $this->set_fragment($parsed['fragment']); $cache[$iri] = array($this->scheme, $this->iuserinfo, $this->ihost, $this->port, $this->ipath, $this->iquery, $this->ifragment, $return); return $return; } /** * Set the scheme. Returns true on success, false on failure (if there are * any invalid characters). * * @param string $scheme * @return bool */ protected function set_scheme($scheme) { if ($scheme === null) { $this->scheme = null; } elseif (!preg_match('/^[A-Za-z][0-9A-Za-z+\-.]*$/', $scheme)) { $this->scheme = null; return false; } else { $this->scheme = strtolower($scheme); } return true; } /** * Set the authority. Returns true on success, false on failure (if there are * any invalid characters). * * @param string $authority * @return bool */ protected function set_authority($authority) { static $cache; if (!$cache) { $cache = array(); } if ($authority === null) { $this->iuserinfo = null; $this->ihost = null; $this->port = null; return true; } if (isset($cache[$authority])) { list($this->iuserinfo, $this->ihost, $this->port, $return) = $cache[$authority]; return $return; } $remaining = $authority; if (($iuserinfo_end = strrpos($remaining, '@')) !== false) { $iuserinfo = substr($remaining, 0, $iuserinfo_end); $remaining = substr($remaining, $iuserinfo_end + 1); } else { $iuserinfo = null; } if (($port_start = strpos($remaining, ':', strpos($remaining, ']'))) !== false) { $port = substr($remaining, $port_start + 1); if ($port === false || $port === '') { $port = null; } $remaining = substr($remaining, 0, $port_start); } else { $port = null; } $return = $this->set_userinfo($iuserinfo) && $this->set_host($remaining) && $this->set_port($port); $cache[$authority] = array($this->iuserinfo, $this->ihost, $this->port, $return); return $return; } /** * Set the iuserinfo. * * @param string $iuserinfo * @return bool */ protected function set_userinfo($iuserinfo) { if ($iuserinfo === null) { $this->iuserinfo = null; } else { $this->iuserinfo = $this->replace_invalid_with_pct_encoding($iuserinfo, '!$&\'()*+,;=:'); $this->scheme_normalization(); } return true; } /** * Set the ihost. Returns true on success, false on failure (if there are * any invalid characters). * * @param string $ihost * @return bool */ protected function set_host($ihost) { if ($ihost === null) { $this->ihost = null; return true; } if (substr($ihost, 0, 1) === '[' && substr($ihost, -1) === ']') { if (Requests_IPv6::check_ipv6(substr($ihost, 1, -1))) { $this->ihost = '[' . Requests_IPv6::compress(substr($ihost, 1, -1)) . ']'; } else { $this->ihost = null; return false; } } else { $ihost = $this->replace_invalid_with_pct_encoding($ihost, '!$&\'()*+,;='); // Lowercase, but ignore pct-encoded sections (as they should // remain uppercase). This must be done after the previous step // as that can add unescaped characters. $position = 0; $strlen = strlen($ihost); while (($position += strcspn($ihost, 'ABCDEFGHIJKLMNOPQRSTUVWXYZ%', $position)) < $strlen) { if ($ihost[$position] === '%') { $position += 3; } else { $ihost[$position] = strtolower($ihost[$position]); $position++; } } $this->ihost = $ihost; } $this->scheme_normalization(); return true; } /** * Set the port. Returns true on success, false on failure (if there are * any invalid characters). * * @param string $port * @return bool */ protected function set_port($port) { if ($port === null) { $this->port = null; return true; } if (strspn($port, '0123456789') === strlen($port)) { $this->port = (int) $port; $this->scheme_normalization(); return true; } $this->port = null; return false; } /** * Set the ipath. * * @param string $ipath * @return bool */ protected function set_path($ipath) { static $cache; if (!$cache) { $cache = array(); } $ipath = (string) $ipath; if (isset($cache[$ipath])) { $this->ipath = $cache[$ipath][(int) ($this->scheme !== null)]; } else { $valid = $this->replace_invalid_with_pct_encoding($ipath, '!$&\'()*+,;=@:/'); $removed = $this->remove_dot_segments($valid); $cache[$ipath] = array($valid, $removed); $this->ipath = ($this->scheme !== null) ? $removed : $valid; } $this->scheme_normalization(); return true; } /** * Set the iquery. * * @param string $iquery * @return bool */ protected function set_query($iquery) { if ($iquery === null) { $this->iquery = null; } else { $this->iquery = $this->replace_invalid_with_pct_encoding($iquery, '!$&\'()*+,;=:@/?', true); $this->scheme_normalization(); } return true; } /** * Set the ifragment. * * @param string $ifragment * @return bool */ protected function set_fragment($ifragment) { if ($ifragment === null) { $this->ifragment = null; } else { $this->ifragment = $this->replace_invalid_with_pct_encoding($ifragment, '!$&\'()*+,;=:@/?'); $this->scheme_normalization(); } return true; } /** * Convert an IRI to a URI (or parts thereof) * * @param string|bool IRI to convert (or false from {@see get_iri}) * @return string|false URI if IRI is valid, false otherwise. */ protected function to_uri($string) { if (!is_string($string)) { return false; } static $non_ascii; if (!$non_ascii) { $non_ascii = implode('', range("\x80", "\xFF")); } $position = 0; $strlen = strlen($string); while (($position += strcspn($string, $non_ascii, $position)) < $strlen) { $string = substr_replace($string, sprintf('%%%02X', ord($string[$position])), $position, 1); $position += 3; $strlen += 2; } return $string; } /** * Get the complete IRI * * @return string|false */ protected function get_iri() { if (!$this->is_valid()) { return false; } $iri = ''; if ($this->scheme !== null) { $iri .= $this->scheme . ':'; } if (($iauthority = $this->get_iauthority()) !== null) { $iri .= '//' . $iauthority; } $iri .= $this->ipath; if ($this->iquery !== null) { $iri .= '?' . $this->iquery; } if ($this->ifragment !== null) { $iri .= '#' . $this->ifragment; } return $iri; } /** * Get the complete URI * * @return string */ protected function get_uri() { return $this->to_uri($this->get_iri()); } /** * Get the complete iauthority * * @return string|null */ protected function get_iauthority() { if ($this->iuserinfo === null && $this->ihost === null && $this->port === null) { return null; } $iauthority = ''; if ($this->iuserinfo !== null) { $iauthority .= $this->iuserinfo . '@'; } if ($this->ihost !== null) { $iauthority .= $this->ihost; } if ($this->port !== null) { $iauthority .= ':' . $this->port; } return $iauthority; } /** * Get the complete authority * * @return string */ protected function get_authority() { $iauthority = $this->get_iauthority(); if (is_string($iauthority)) { return $this->to_uri($iauthority); } else { return $iauthority; } } } data[$key])) { return null; } return $this->flatten($this->data[$key]); } /** * Set the given item * * @throws Requests_Exception On attempting to use dictionary as list (`invalidset`) * * @param string $key Item name * @param string $value Item value */ public function offsetSet($key, $value) { if ($key === null) { throw new Requests_Exception('Object is a dictionary, not a list', 'invalidset'); } $key = strtolower($key); if (!isset($this->data[$key])) { $this->data[$key] = array(); } $this->data[$key][] = $value; } /** * Get all values for a given header * * @param string $key * @return array|null Header values */ public function getValues($key) { $key = strtolower($key); if (!isset($this->data[$key])) { return null; } return $this->data[$key]; } /** * Flattens a value into a string * * Converts an array into a string by imploding values with a comma, as per * RFC2616's rules for folding headers. * * @param string|array $value Value to flatten * @return string Flattened value */ public function flatten($value) { if (is_array($value)) { $value = implode(',', $value); } return $value; } /** * Get an iterator for the data * * Converts the internal * @return ArrayIterator */ public function getIterator() { return new Requests_Utility_FilteredIterator($this->data, array($this, 'flatten')); } } dispatch('requests.before_request', array(&$url, &$headers, &$data, &$type, &$options)); if (!empty($options['transport'])) { $transport = $options['transport']; if (is_string($options['transport'])) { $transport = new $transport(); } } else { $need_ssl = (stripos($url, 'https://') === 0); $capabilities = array('ssl' => $need_ssl); $transport = self::get_transport($capabilities); } $response = $transport->request($url, $headers, $data, $options); $options['hooks']->dispatch('requests.before_parse', array(&$response, $url, $headers, $data, $type, $options)); return self::parse_response($response, $url, $headers, $data, $options); } /** * Send multiple HTTP requests simultaneously * * The `$requests` parameter takes an associative or indexed array of * request fields. The key of each request can be used to match up the * request with the returned data, or with the request passed into your * `multiple.request.complete` callback. * * The request fields value is an associative array with the following keys: * * - `url`: Request URL Same as the `$url` parameter to * {@see Requests::request} * (string, required) * - `headers`: Associative array of header fields. Same as the `$headers` * parameter to {@see Requests::request} * (array, default: `array()`) * - `data`: Associative array of data fields or a string. Same as the * `$data` parameter to {@see Requests::request} * (array|string, default: `array()`) * - `type`: HTTP request type (use Requests constants). Same as the `$type` * parameter to {@see Requests::request} * (string, default: `Requests::GET`) * - `cookies`: Associative array of cookie name to value, or cookie jar. * (array|Requests_Cookie_Jar) * * If the `$options` parameter is specified, individual requests will * inherit options from it. This can be used to use a single hooking system, * or set all the types to `Requests::POST`, for example. * * In addition, the `$options` parameter takes the following global options: * * - `complete`: A callback for when a request is complete. Takes two * parameters, a Requests_Response/Requests_Exception reference, and the * ID from the request array (Note: this can also be overridden on a * per-request basis, although that's a little silly) * (callback) * * @param array $requests Requests data (see description for more information) * @param array $options Global and default options (see {@see Requests::request}) * @return array Responses (either Requests_Response or a Requests_Exception object) */ public static function request_multiple($requests, $options = array()) { $options = array_merge(self::get_default_options(true), $options); if (!empty($options['hooks'])) { $options['hooks']->register('transport.internal.parse_response', array('Requests', 'parse_multiple')); if (!empty($options['complete'])) { $options['hooks']->register('multiple.request.complete', $options['complete']); } } foreach ($requests as $id => &$request) { if (!isset($request['headers'])) { $request['headers'] = array(); } if (!isset($request['data'])) { $request['data'] = array(); } if (!isset($request['type'])) { $request['type'] = self::GET; } if (!isset($request['options'])) { $request['options'] = $options; $request['options']['type'] = $request['type']; } else { if (empty($request['options']['type'])) { $request['options']['type'] = $request['type']; } $request['options'] = array_merge($options, $request['options']); } self::set_defaults($request['url'], $request['headers'], $request['data'], $request['type'], $request['options']); // Ensure we only hook in once if ($request['options']['hooks'] !== $options['hooks']) { $request['options']['hooks']->register('transport.internal.parse_response', array('Requests', 'parse_multiple')); if (!empty($request['options']['complete'])) { $request['options']['hooks']->register('multiple.request.complete', $request['options']['complete']); } } } unset($request); if (!empty($options['transport'])) { $transport = $options['transport']; if (is_string($options['transport'])) { $transport = new $transport(); } } else { $transport = self::get_transport(); } $responses = $transport->request_multiple($requests, $options); foreach ($responses as $id => &$response) { // If our hook got messed with somehow, ensure we end up with the // correct response if (is_string($response)) { $request = $requests[$id]; self::parse_multiple($response, $request); $request['options']['hooks']->dispatch('multiple.request.complete', array(&$response, $id)); } } return $responses; } /** * Get the default options * * @see Requests::request() for values returned by this method * @param boolean $multirequest Is this a multirequest? * @return array Default option values */ protected static function get_default_options($multirequest = false) { $defaults = array( 'timeout' => 10, 'connect_timeout' => 10, 'useragent' => 'php-requests/' . self::VERSION, 'protocol_version' => 1.1, 'redirected' => 0, 'redirects' => 10, 'follow_redirects' => true, 'blocking' => true, 'type' => self::GET, 'filename' => false, 'auth' => false, 'proxy' => false, 'cookies' => false, 'max_bytes' => false, 'idn' => true, 'hooks' => null, 'transport' => null, 'verify' => self::get_certificate_path(), 'verifyname' => true, ); if ($multirequest !== false) { $defaults['complete'] = null; } return $defaults; } /** * Get default certificate path. * * @return string Default certificate path. */ public static function get_certificate_path() { if (!empty(self::$certificate_path)) { return self::$certificate_path; } return dirname(__FILE__) . '/Requests/Transport/cacert.pem'; } /** * Set default certificate path. * * @param string $path Certificate path, pointing to a PEM file. */ public static function set_certificate_path($path) { self::$certificate_path = $path; } /** * Set the default values * * @param string $url URL to request * @param array $headers Extra headers to send with the request * @param array|null $data Data to send either as a query string for GET/HEAD requests, or in the body for POST requests * @param string $type HTTP request type * @param array $options Options for the request * @return array $options */ protected static function set_defaults(&$url, &$headers, &$data, &$type, &$options) { if (!preg_match('/^http(s)?:\/\//i', $url, $matches)) { throw new Requests_Exception('Only HTTP(S) requests are handled.', 'nonhttp', $url); } if (empty($options['hooks'])) { $options['hooks'] = new Requests_Hooks(); } if (is_array($options['auth'])) { $options['auth'] = new Requests_Auth_Basic($options['auth']); } if ($options['auth'] !== false) { $options['auth']->register($options['hooks']); } if (is_string($options['proxy']) || is_array($options['proxy'])) { $options['proxy'] = new Requests_Proxy_HTTP($options['proxy']); } if ($options['proxy'] !== false) { $options['proxy']->register($options['hooks']); } if (is_array($options['cookies'])) { $options['cookies'] = new Requests_Cookie_Jar($options['cookies']); } elseif (empty($options['cookies'])) { $options['cookies'] = new Requests_Cookie_Jar(); } if ($options['cookies'] !== false) { $options['cookies']->register($options['hooks']); } if ($options['idn'] !== false) { $iri = new Requests_IRI($url); $iri->host = Requests_IDNAEncoder::encode($iri->ihost); $url = $iri->uri; } // Massage the type to ensure we support it. $type = strtoupper($type); if (!isset($options['data_format'])) { if (in_array($type, array(self::HEAD, self::GET, self::DELETE), true)) { $options['data_format'] = 'query'; } else { $options['data_format'] = 'body'; } } } /** * HTTP response parser * * @throws Requests_Exception On missing head/body separator (`requests.no_crlf_separator`) * @throws Requests_Exception On missing head/body separator (`noversion`) * @throws Requests_Exception On missing head/body separator (`toomanyredirects`) * * @param string $headers Full response text including headers and body * @param string $url Original request URL * @param array $req_headers Original $headers array passed to {@link request()}, in case we need to follow redirects * @param array $req_data Original $data array passed to {@link request()}, in case we need to follow redirects * @param array $options Original $options array passed to {@link request()}, in case we need to follow redirects * @return Requests_Response */ protected static function parse_response($headers, $url, $req_headers, $req_data, $options) { $return = new Requests_Response(); if (!$options['blocking']) { return $return; } $return->raw = $headers; $return->url = (string) $url; $return->body = ''; if (!$options['filename']) { $pos = strpos($headers, "\r\n\r\n"); if ($pos === false) { // Crap! throw new Requests_Exception('Missing header/body separator', 'requests.no_crlf_separator'); } $headers = substr($return->raw, 0, $pos); // Headers will always be separated from the body by two new lines - `\n\r\n\r`. $body = substr($return->raw, $pos + 4); if (!empty($body)) { $return->body = $body; } } // Pretend CRLF = LF for compatibility (RFC 2616, section 19.3) $headers = str_replace("\r\n", "\n", $headers); // Unfold headers (replace [CRLF] 1*( SP | HT ) with SP) as per RFC 2616 (section 2.2) $headers = preg_replace('/\n[ \t]/', ' ', $headers); $headers = explode("\n", $headers); preg_match('#^HTTP/(1\.\d)[ \t]+(\d+)#i', array_shift($headers), $matches); if (empty($matches)) { throw new Requests_Exception('Response could not be parsed', 'noversion', $headers); } $return->protocol_version = (float) $matches[1]; $return->status_code = (int) $matches[2]; if ($return->status_code >= 200 && $return->status_code < 300) { $return->success = true; } foreach ($headers as $header) { list($key, $value) = explode(':', $header, 2); $value = trim($value); preg_replace('#(\s+)#i', ' ', $value); $return->headers[$key] = $value; } if (isset($return->headers['transfer-encoding'])) { $return->body = self::decode_chunked($return->body); unset($return->headers['transfer-encoding']); } if (isset($return->headers['content-encoding'])) { $return->body = self::decompress($return->body); } //fsockopen and cURL compatibility if (isset($return->headers['connection'])) { unset($return->headers['connection']); } $options['hooks']->dispatch('requests.before_redirect_check', array(&$return, $req_headers, $req_data, $options)); if ($return->is_redirect() && $options['follow_redirects'] === true) { if (isset($return->headers['location']) && $options['redirected'] < $options['redirects']) { if ($return->status_code === 303) { $options['type'] = self::GET; } $options['redirected']++; $location = $return->headers['location']; if (strpos($location, 'http://') !== 0 && strpos($location, 'https://') !== 0) { // relative redirect, for compatibility make it absolute $location = Requests_IRI::absolutize($url, $location); $location = $location->uri; } $hook_args = array( &$location, &$req_headers, &$req_data, &$options, $return, ); $options['hooks']->dispatch('requests.before_redirect', $hook_args); $redirected = self::request($location, $req_headers, $req_data, $options['type'], $options); $redirected->history[] = $return; return $redirected; } elseif ($options['redirected'] >= $options['redirects']) { throw new Requests_Exception('Too many redirects', 'toomanyredirects', $return); } } $return->redirects = $options['redirected']; $options['hooks']->dispatch('requests.after_request', array(&$return, $req_headers, $req_data, $options)); return $return; } /** * Callback for `transport.internal.parse_response` * * Internal use only. Converts a raw HTTP response to a Requests_Response * while still executing a multiple request. * * @param string $response Full response text including headers and body (will be overwritten with Response instance) * @param array $request Request data as passed into {@see Requests::request_multiple()} * @return null `$response` is either set to a Requests_Response instance, or a Requests_Exception object */ public static function parse_multiple(&$response, $request) { try { $url = $request['url']; $headers = $request['headers']; $data = $request['data']; $options = $request['options']; $response = self::parse_response($response, $url, $headers, $data, $options); } catch (Requests_Exception $e) { $response = $e; } } /** * Decoded a chunked body as per RFC 2616 * * @see https://tools.ietf.org/html/rfc2616#section-3.6.1 * @param string $data Chunked body * @return string Decoded body */ protected static function decode_chunked($data) { if (!preg_match('/^([0-9a-f]+)(?:;(?:[\w-]*)(?:=(?:(?:[\w-]*)*|"(?:[^\r\n])*"))?)*\r\n/i', trim($data))) { return $data; } $decoded = ''; $encoded = $data; while (true) { $is_chunked = (bool) preg_match('/^([0-9a-f]+)(?:;(?:[\w-]*)(?:=(?:(?:[\w-]*)*|"(?:[^\r\n])*"))?)*\r\n/i', $encoded, $matches); if (!$is_chunked) { // Looks like it's not chunked after all return $data; } $length = hexdec(trim($matches[1])); if ($length === 0) { // Ignore trailer headers return $decoded; } $chunk_length = strlen($matches[0]); $decoded .= substr($encoded, $chunk_length, $length); $encoded = substr($encoded, $chunk_length + $length + 2); if (trim($encoded) === '0' || empty($encoded)) { return $decoded; } } // We'll never actually get down here // @codeCoverageIgnoreStart } // @codeCoverageIgnoreEnd /** * Convert a key => value array to a 'key: value' array for headers * * @param array $array Dictionary of header values * @return array List of headers */ public static function flatten($array) { $return = array(); foreach ($array as $key => $value) { $return[] = sprintf('%s: %s', $key, $value); } return $return; } /** * Convert a key => value array to a 'key: value' array for headers * * @codeCoverageIgnore * @deprecated Misspelling of {@see Requests::flatten} * @param array $array Dictionary of header values * @return array List of headers */ public static function flattern($array) { return self::flatten($array); } /** * Decompress an encoded body * * Implements gzip, compress and deflate. Guesses which it is by attempting * to decode. * * @param string $data Compressed data in one of the above formats * @return string Decompressed string */ public static function decompress($data) { if (substr($data, 0, 2) !== "\x1f\x8b" && substr($data, 0, 2) !== "\x78\x9c") { // Not actually compressed. Probably cURL ruining this for us. return $data; } if (function_exists('gzdecode')) { // phpcs:ignore PHPCompatibility.FunctionUse.NewFunctions.gzdecodeFound -- Wrapped in function_exists() for PHP 5.2. $decoded = @gzdecode($data); if ($decoded !== false) { return $decoded; } } if (function_exists('gzinflate')) { $decoded = @gzinflate($data); if ($decoded !== false) { return $decoded; } } $decoded = self::compatible_gzinflate($data); if ($decoded !== false) { return $decoded; } if (function_exists('gzuncompress')) { $decoded = @gzuncompress($data); if ($decoded !== false) { return $decoded; } } return $data; } /** * Decompression of deflated string while staying compatible with the majority of servers. * * Certain Servers will return deflated data with headers which PHP's gzinflate() * function cannot handle out of the box. The following function has been created from * various snippets on the gzinflate() PHP documentation. * * Warning: Magic numbers within. Due to the potential different formats that the compressed * data may be returned in, some "magic offsets" are needed to ensure proper decompression * takes place. For a simple progmatic way to determine the magic offset in use, see: * https://core.trac.wordpress.org/ticket/18273 * * @since 2.8.1 * @link https://core.trac.wordpress.org/ticket/18273 * @link https://secure.php.net/manual/en/function.gzinflate.php#70875 * @link https://secure.php.net/manual/en/function.gzinflate.php#77336 * * @param string $gz_data String to decompress. * @return string|bool False on failure. */ public static function compatible_gzinflate($gz_data) { // Compressed data might contain a full zlib header, if so strip it for // gzinflate() if (substr($gz_data, 0, 3) === "\x1f\x8b\x08") { $i = 10; $flg = ord(substr($gz_data, 3, 1)); if ($flg > 0) { if ($flg & 4) { list($xlen) = unpack('v', substr($gz_data, $i, 2)); $i += 2 + $xlen; } if ($flg & 8) { $i = strpos($gz_data, "\0", $i) + 1; } if ($flg & 16) { $i = strpos($gz_data, "\0", $i) + 1; } if ($flg & 2) { $i += 2; } } $decompressed = self::compatible_gzinflate(substr($gz_data, $i)); if ($decompressed !== false) { return $decompressed; } } // If the data is Huffman Encoded, we must first strip the leading 2 // byte Huffman marker for gzinflate() // The response is Huffman coded by many compressors such as // java.util.zip.Deflater, Ruby’s Zlib::Deflate, and .NET's // System.IO.Compression.DeflateStream. // // See https://decompres.blogspot.com/ for a quick explanation of this // data type $huffman_encoded = false; // low nibble of first byte should be 0x08 list(, $first_nibble) = unpack('h', $gz_data); // First 2 bytes should be divisible by 0x1F list(, $first_two_bytes) = unpack('n', $gz_data); if ($first_nibble === 0x08 && ($first_two_bytes % 0x1F) === 0) { $huffman_encoded = true; } if ($huffman_encoded) { $decompressed = @gzinflate(substr($gz_data, 2)); if ($decompressed !== false) { return $decompressed; } } if (substr($gz_data, 0, 4) === "\x50\x4b\x03\x04") { // ZIP file format header // Offset 6: 2 bytes, General-purpose field // Offset 26: 2 bytes, filename length // Offset 28: 2 bytes, optional field length // Offset 30: Filename field, followed by optional field, followed // immediately by data list(, $general_purpose_flag) = unpack('v', substr($gz_data, 6, 2)); // If the file has been compressed on the fly, 0x08 bit is set of // the general purpose field. We can use this to differentiate // between a compressed document, and a ZIP file $zip_compressed_on_the_fly = ((0x08 & $general_purpose_flag) === 0x08); if (!$zip_compressed_on_the_fly) { // Don't attempt to decode a compressed zip file return $gz_data; } // Determine the first byte of data, based on the above ZIP header // offsets: $first_file_start = array_sum(unpack('v2', substr($gz_data, 26, 4))); $decompressed = @gzinflate(substr($gz_data, 30 + $first_file_start)); if ($decompressed !== false) { return $decompressed; } return false; } // Finally fall back to straight gzinflate $decompressed = @gzinflate($gz_data); if ($decompressed !== false) { return $decompressed; } // Fallback for all above failing, not expected, but included for // debugging and preventing regressions and to track stats $decompressed = @gzinflate(substr($gz_data, 2)); if ($decompressed !== false) { return $decompressed; } return false; } public static function match_domain($host, $reference) { // Check for a direct match if ($host === $reference) { return true; } // Calculate the valid wildcard match if the host is not an IP address // Also validates that the host has 3 parts or more, as per Firefox's // ruleset. $parts = explode('.', $host); if (ip2long($host) === false && count($parts) >= 3) { $parts[0] = '*'; $wildcard = implode('.', $parts); if ($wildcard === $reference) { return true; } } return false; } } * Jordi Boggiano * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Composer\Autoload; /** * ClassLoader implements a PSR-0, PSR-4 and classmap class loader. * * $loader = new \Composer\Autoload\ClassLoader(); * * // register classes with namespaces * $loader->add('Symfony\Component', __DIR__.'/component'); * $loader->add('Symfony', __DIR__.'/framework'); * * // activate the autoloader * $loader->register(); * * // to enable searching the include path (eg. for PEAR packages) * $loader->setUseIncludePath(true); * * In this example, if you try to use a class in the Symfony\Component * namespace or one of its children (Symfony\Component\Console for instance), * the autoloader will first look for the class under the component/ * directory, and it will then fallback to the framework/ directory if not * found before giving up. * * This class is loosely based on the Symfony UniversalClassLoader. * * @author Fabien Potencier * @author Jordi Boggiano * @see https://www.php-fig.org/psr/psr-0/ * @see https://www.php-fig.org/psr/psr-4/ */ class ClassLoader { /** @var ?string */ private $vendorDir; // PSR-4 /** * @var array[] * @psalm-var array> */ private $prefixLengthsPsr4 = array(); /** * @var array[] * @psalm-var array> */ private $prefixDirsPsr4 = array(); /** * @var array[] * @psalm-var array */ private $fallbackDirsPsr4 = array(); // PSR-0 /** * @var array[] * @psalm-var array> */ private $prefixesPsr0 = array(); /** * @var array[] * @psalm-var array */ private $fallbackDirsPsr0 = array(); /** @var bool */ private $useIncludePath = false; /** * @var string[] * @psalm-var array */ private $classMap = array(); /** @var bool */ private $classMapAuthoritative = false; /** * @var bool[] * @psalm-var array */ private $missingClasses = array(); /** @var ?string */ private $apcuPrefix; /** * @var self[] */ private static $registeredLoaders = array(); /** * @param ?string $vendorDir */ public function __construct($vendorDir = null) { $this->vendorDir = $vendorDir; } /** * @return string[] */ public function getPrefixes() { if (!empty($this->prefixesPsr0)) { return call_user_func_array('array_merge', array_values($this->prefixesPsr0)); } return array(); } /** * @return array[] * @psalm-return array> */ public function getPrefixesPsr4() { return $this->prefixDirsPsr4; } /** * @return array[] * @psalm-return array */ public function getFallbackDirs() { return $this->fallbackDirsPsr0; } /** * @return array[] * @psalm-return array */ public function getFallbackDirsPsr4() { return $this->fallbackDirsPsr4; } /** * @return string[] Array of classname => path * @psalm-return array */ public function getClassMap() { return $this->classMap; } /** * @param string[] $classMap Class to filename map * @psalm-param array $classMap * * @return void */ public function addClassMap(array $classMap) { if ($this->classMap) { $this->classMap = array_merge($this->classMap, $classMap); } else { $this->classMap = $classMap; } } /** * Registers a set of PSR-0 directories for a given prefix, either * appending or prepending to the ones previously set for this prefix. * * @param string $prefix The prefix * @param string[]|string $paths The PSR-0 root directories * @param bool $prepend Whether to prepend the directories * * @return void */ public function add($prefix, $paths, $prepend = false) { if (!$prefix) { if ($prepend) { $this->fallbackDirsPsr0 = array_merge( (array) $paths, $this->fallbackDirsPsr0 ); } else { $this->fallbackDirsPsr0 = array_merge( $this->fallbackDirsPsr0, (array) $paths ); } return; } $first = $prefix[0]; if (!isset($this->prefixesPsr0[$first][$prefix])) { $this->prefixesPsr0[$first][$prefix] = (array) $paths; return; } if ($prepend) { $this->prefixesPsr0[$first][$prefix] = array_merge( (array) $paths, $this->prefixesPsr0[$first][$prefix] ); } else { $this->prefixesPsr0[$first][$prefix] = array_merge( $this->prefixesPsr0[$first][$prefix], (array) $paths ); } } /** * Registers a set of PSR-4 directories for a given namespace, either * appending or prepending to the ones previously set for this namespace. * * @param string $prefix The prefix/namespace, with trailing '\\' * @param string[]|string $paths The PSR-4 base directories * @param bool $prepend Whether to prepend the directories * * @throws \InvalidArgumentException * * @return void */ public function addPsr4($prefix, $paths, $prepend = false) { if (!$prefix) { // Register directories for the root namespace. if ($prepend) { $this->fallbackDirsPsr4 = array_merge( (array) $paths, $this->fallbackDirsPsr4 ); } else { $this->fallbackDirsPsr4 = array_merge( $this->fallbackDirsPsr4, (array) $paths ); } } elseif (!isset($this->prefixDirsPsr4[$prefix])) { // Register directories for a new namespace. $length = strlen($prefix); if ('\\' !== $prefix[$length - 1]) { throw new \InvalidArgumentException("A non-empty PSR-4 prefix must end with a namespace separator."); } $this->prefixLengthsPsr4[$prefix[0]][$prefix] = $length; $this->prefixDirsPsr4[$prefix] = (array) $paths; } elseif ($prepend) { // Prepend directories for an already registered namespace. $this->prefixDirsPsr4[$prefix] = array_merge( (array) $paths, $this->prefixDirsPsr4[$prefix] ); } else { // Append directories for an already registered namespace. $this->prefixDirsPsr4[$prefix] = array_merge( $this->prefixDirsPsr4[$prefix], (array) $paths ); } } /** * Registers a set of PSR-0 directories for a given prefix, * replacing any others previously set for this prefix. * * @param string $prefix The prefix * @param string[]|string $paths The PSR-0 base directories * * @return void */ public function set($prefix, $paths) { if (!$prefix) { $this->fallbackDirsPsr0 = (array) $paths; } else { $this->prefixesPsr0[$prefix[0]][$prefix] = (array) $paths; } } /** * Registers a set of PSR-4 directories for a given namespace, * replacing any others previously set for this namespace. * * @param string $prefix The prefix/namespace, with trailing '\\' * @param string[]|string $paths The PSR-4 base directories * * @throws \InvalidArgumentException * * @return void */ public function setPsr4($prefix, $paths) { if (!$prefix) { $this->fallbackDirsPsr4 = (array) $paths; } else { $length = strlen($prefix); if ('\\' !== $prefix[$length - 1]) { throw new \InvalidArgumentException("A non-empty PSR-4 prefix must end with a namespace separator."); } $this->prefixLengthsPsr4[$prefix[0]][$prefix] = $length; $this->prefixDirsPsr4[$prefix] = (array) $paths; } } /** * Turns on searching the include path for class files. * * @param bool $useIncludePath * * @return void */ public function setUseIncludePath($useIncludePath) { $this->useIncludePath = $useIncludePath; } /** * Can be used to check if the autoloader uses the include path to check * for classes. * * @return bool */ public function getUseIncludePath() { return $this->useIncludePath; } /** * Turns off searching the prefix and fallback directories for classes * that have not been registered with the class map. * * @param bool $classMapAuthoritative * * @return void */ public function setClassMapAuthoritative($classMapAuthoritative) { $this->classMapAuthoritative = $classMapAuthoritative; } /** * Should class lookup fail if not found in the current class map? * * @return bool */ public function isClassMapAuthoritative() { return $this->classMapAuthoritative; } /** * APCu prefix to use to cache found/not-found classes, if the extension is enabled. * * @param string|null $apcuPrefix * * @return void */ public function setApcuPrefix($apcuPrefix) { $this->apcuPrefix = function_exists('apcu_fetch') && filter_var(ini_get('apc.enabled'), FILTER_VALIDATE_BOOLEAN) ? $apcuPrefix : null; } /** * The APCu prefix in use, or null if APCu caching is not enabled. * * @return string|null */ public function getApcuPrefix() { return $this->apcuPrefix; } /** * Registers this instance as an autoloader. * * @param bool $prepend Whether to prepend the autoloader or not * * @return void */ public function register($prepend = false) { spl_autoload_register(array($this, 'loadClass'), true, $prepend); if (null === $this->vendorDir) { return; } if ($prepend) { self::$registeredLoaders = array($this->vendorDir => $this) + self::$registeredLoaders; } else { unset(self::$registeredLoaders[$this->vendorDir]); self::$registeredLoaders[$this->vendorDir] = $this; } } /** * Unregisters this instance as an autoloader. * * @return void */ public function unregister() { spl_autoload_unregister(array($this, 'loadClass')); if (null !== $this->vendorDir) { unset(self::$registeredLoaders[$this->vendorDir]); } } /** * Loads the given class or interface. * * @param string $class The name of the class * @return true|null True if loaded, null otherwise */ public function loadClass($class) { if ($file = $this->findFile($class)) { includeFile($file); return true; } return null; } /** * Finds the path to the file where the class is defined. * * @param string $class The name of the class * * @return string|false The path if found, false otherwise */ public function findFile($class) { // class map lookup if (isset($this->classMap[$class])) { return $this->classMap[$class]; } if ($this->classMapAuthoritative || isset($this->missingClasses[$class])) { return false; } if (null !== $this->apcuPrefix) { $file = apcu_fetch($this->apcuPrefix.$class, $hit); if ($hit) { return $file; } } $file = $this->findFileWithExtension($class, '.php'); // Search for Hack files if we are running on HHVM if (false === $file && defined('HHVM_VERSION')) { $file = $this->findFileWithExtension($class, '.hh'); } if (null !== $this->apcuPrefix) { apcu_add($this->apcuPrefix.$class, $file); } if (false === $file) { // Remember that this class does not exist. $this->missingClasses[$class] = true; } return $file; } /** * Returns the currently registered loaders indexed by their corresponding vendor directories. * * @return self[] */ public static function getRegisteredLoaders() { return self::$registeredLoaders; } /** * @param string $class * @param string $ext * @return string|false */ private function findFileWithExtension($class, $ext) { // PSR-4 lookup $logicalPathPsr4 = strtr($class, '\\', DIRECTORY_SEPARATOR) . $ext; $first = $class[0]; if (isset($this->prefixLengthsPsr4[$first])) { $subPath = $class; while (false !== $lastPos = strrpos($subPath, '\\')) { $subPath = substr($subPath, 0, $lastPos); $search = $subPath . '\\'; if (isset($this->prefixDirsPsr4[$search])) { $pathEnd = DIRECTORY_SEPARATOR . substr($logicalPathPsr4, $lastPos + 1); foreach ($this->prefixDirsPsr4[$search] as $dir) { if (file_exists($file = $dir . $pathEnd)) { return $file; } } } } } // PSR-4 fallback dirs foreach ($this->fallbackDirsPsr4 as $dir) { if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr4)) { return $file; } } // PSR-0 lookup if (false !== $pos = strrpos($class, '\\')) { // namespaced class name $logicalPathPsr0 = substr($logicalPathPsr4, 0, $pos + 1) . strtr(substr($logicalPathPsr4, $pos + 1), '_', DIRECTORY_SEPARATOR); } else { // PEAR-like class name $logicalPathPsr0 = strtr($class, '_', DIRECTORY_SEPARATOR) . $ext; } if (isset($this->prefixesPsr0[$first])) { foreach ($this->prefixesPsr0[$first] as $prefix => $dirs) { if (0 === strpos($class, $prefix)) { foreach ($dirs as $dir) { if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr0)) { return $file; } } } } } // PSR-0 fallback dirs foreach ($this->fallbackDirsPsr0 as $dir) { if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr0)) { return $file; } } // PSR-0 include paths. if ($this->useIncludePath && $file = stream_resolve_include_path($logicalPathPsr0)) { return $file; } return false; } } /** * Scope isolated include. * * Prevents access to $this/self from included files. * * @param string $file * @return void * @private */ function includeFile($file) { include $file; } * Jordi Boggiano * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Composer\Autoload; /** * ClassLoader implements a PSR-0, PSR-4 and classmap class loader. * * $loader = new \Composer\Autoload\ClassLoader(); * * // register classes with namespaces * $loader->add('Symfony\Component', __DIR__.'/component'); * $loader->add('Symfony', __DIR__.'/framework'); * * // activate the autoloader * $loader->register(); * * // to enable searching the include path (eg. for PEAR packages) * $loader->setUseIncludePath(true); * * In this example, if you try to use a class in the Symfony\Component * namespace or one of its children (Symfony\Component\Console for instance), * the autoloader will first look for the class under the component/ * directory, and it will then fallback to the framework/ directory if not * found before giving up. * * This class is loosely based on the Symfony UniversalClassLoader. * * @author Fabien Potencier * @author Jordi Boggiano * @see https://www.php-fig.org/psr/psr-0/ * @see https://www.php-fig.org/psr/psr-4/ */ class ClassLoader { /** @var ?string */ private $vendorDir; // PSR-4 /** * @var array[] * @psalm-var array> */ private $prefixLengthsPsr4 = array(); /** * @var array[] * @psalm-var array> */ private $prefixDirsPsr4 = array(); /** * @var array[] * @psalm-var array */ private $fallbackDirsPsr4 = array(); // PSR-0 /** * @var array[] * @psalm-var array> */ private $prefixesPsr0 = array(); /** * @var array[] * @psalm-var array */ private $fallbackDirsPsr0 = array(); /** @var bool */ private $useIncludePath = false; /** * @var string[] * @psalm-var array */ private $classMap = array(); /** @var bool */ private $classMapAuthoritative = false; /** * @var bool[] * @psalm-var array */ private $missingClasses = array(); /** @var ?string */ private $apcuPrefix; /** * @var self[] */ private static $registeredLoaders = array(); /** * @param ?string $vendorDir */ public function __construct($vendorDir = null) { $this->vendorDir = $vendorDir; } /** * @return string[] */ public function getPrefixes() { if (!empty($this->prefixesPsr0)) { return call_user_func_array('array_merge', array_values($this->prefixesPsr0)); } return array(); } /** * @return array[] * @psalm-return array> */ public function getPrefixesPsr4() { return $this->prefixDirsPsr4; } /** * @return array[] * @psalm-return array */ public function getFallbackDirs() { return $this->fallbackDirsPsr0; } /** * @return array[] * @psalm-return array */ public function getFallbackDirsPsr4() { return $this->fallbackDirsPsr4; } /** * @return string[] Array of classname => path * @psalm-return array */ public function getClassMap() { return $this->classMap; } /** * @param string[] $classMap Class to filename map * @psalm-param array $classMap * * @return void */ public function addClassMap(array $classMap) { if ($this->classMap) { $this->classMap = array_merge($this->classMap, $classMap); } else { $this->classMap = $classMap; } } /** * Registers a set of PSR-0 directories for a given prefix, either * appending or prepending to the ones previously set for this prefix. * * @param string $prefix The prefix * @param string[]|string $paths The PSR-0 root directories * @param bool $prepend Whether to prepend the directories * * @return void */ public function add($prefix, $paths, $prepend = false) { if (!$prefix) { if ($prepend) { $this->fallbackDirsPsr0 = array_merge( (array) $paths, $this->fallbackDirsPsr0 ); } else { $this->fallbackDirsPsr0 = array_merge( $this->fallbackDirsPsr0, (array) $paths ); } return; } $first = $prefix[0]; if (!isset($this->prefixesPsr0[$first][$prefix])) { $this->prefixesPsr0[$first][$prefix] = (array) $paths; return; } if ($prepend) { $this->prefixesPsr0[$first][$prefix] = array_merge( (array) $paths, $this->prefixesPsr0[$first][$prefix] ); } else { $this->prefixesPsr0[$first][$prefix] = array_merge( $this->prefixesPsr0[$first][$prefix], (array) $paths ); } } /** * Registers a set of PSR-4 directories for a given namespace, either * appending or prepending to the ones previously set for this namespace. * * @param string $prefix The prefix/namespace, with trailing '\\' * @param string[]|string $paths The PSR-4 base directories * @param bool $prepend Whether to prepend the directories * * @throws \InvalidArgumentException * * @return void */ public function addPsr4($prefix, $paths, $prepend = false) { if (!$prefix) { // Register directories for the root namespace. if ($prepend) { $this->fallbackDirsPsr4 = array_merge( (array) $paths, $this->fallbackDirsPsr4 ); } else { $this->fallbackDirsPsr4 = array_merge( $this->fallbackDirsPsr4, (array) $paths ); } } elseif (!isset($this->prefixDirsPsr4[$prefix])) { // Register directories for a new namespace. $length = strlen($prefix); if ('\\' !== $prefix[$length - 1]) { throw new \InvalidArgumentException("A non-empty PSR-4 prefix must end with a namespace separator."); } $this->prefixLengthsPsr4[$prefix[0]][$prefix] = $length; $this->prefixDirsPsr4[$prefix] = (array) $paths; } elseif ($prepend) { // Prepend directories for an already registered namespace. $this->prefixDirsPsr4[$prefix] = array_merge( (array) $paths, $this->prefixDirsPsr4[$prefix] ); } else { // Append directories for an already registered namespace. $this->prefixDirsPsr4[$prefix] = array_merge( $this->prefixDirsPsr4[$prefix], (array) $paths ); } } /** * Registers a set of PSR-0 directories for a given prefix, * replacing any others previously set for this prefix. * * @param string $prefix The prefix * @param string[]|string $paths The PSR-0 base directories * * @return void */ public function set($prefix, $paths) { if (!$prefix) { $this->fallbackDirsPsr0 = (array) $paths; } else { $this->prefixesPsr0[$prefix[0]][$prefix] = (array) $paths; } } /** * Registers a set of PSR-4 directories for a given namespace, * replacing any others previously set for this namespace. * * @param string $prefix The prefix/namespace, with trailing '\\' * @param string[]|string $paths The PSR-4 base directories * * @throws \InvalidArgumentException * * @return void */ public function setPsr4($prefix, $paths) { if (!$prefix) { $this->fallbackDirsPsr4 = (array) $paths; } else { $length = strlen($prefix); if ('\\' !== $prefix[$length - 1]) { throw new \InvalidArgumentException("A non-empty PSR-4 prefix must end with a namespace separator."); } $this->prefixLengthsPsr4[$prefix[0]][$prefix] = $length; $this->prefixDirsPsr4[$prefix] = (array) $paths; } } /** * Turns on searching the include path for class files. * * @param bool $useIncludePath * * @return void */ public function setUseIncludePath($useIncludePath) { $this->useIncludePath = $useIncludePath; } /** * Can be used to check if the autoloader uses the include path to check * for classes. * * @return bool */ public function getUseIncludePath() { return $this->useIncludePath; } /** * Turns off searching the prefix and fallback directories for classes * that have not been registered with the class map. * * @param bool $classMapAuthoritative * * @return void */ public function setClassMapAuthoritative($classMapAuthoritative) { $this->classMapAuthoritative = $classMapAuthoritative; } /** * Should class lookup fail if not found in the current class map? * * @return bool */ public function isClassMapAuthoritative() { return $this->classMapAuthoritative; } /** * APCu prefix to use to cache found/not-found classes, if the extension is enabled. * * @param string|null $apcuPrefix * * @return void */ public function setApcuPrefix($apcuPrefix) { $this->apcuPrefix = function_exists('apcu_fetch') && filter_var(ini_get('apc.enabled'), FILTER_VALIDATE_BOOLEAN) ? $apcuPrefix : null; } /** * The APCu prefix in use, or null if APCu caching is not enabled. * * @return string|null */ public function getApcuPrefix() { return $this->apcuPrefix; } /** * Registers this instance as an autoloader. * * @param bool $prepend Whether to prepend the autoloader or not * * @return void */ public function register($prepend = false) { spl_autoload_register(array($this, 'loadClass'), true, $prepend); if (null === $this->vendorDir) { return; } if ($prepend) { self::$registeredLoaders = array($this->vendorDir => $this) + self::$registeredLoaders; } else { unset(self::$registeredLoaders[$this->vendorDir]); self::$registeredLoaders[$this->vendorDir] = $this; } } /** * Unregisters this instance as an autoloader. * * @return void */ public function unregister() { spl_autoload_unregister(array($this, 'loadClass')); if (null !== $this->vendorDir) { unset(self::$registeredLoaders[$this->vendorDir]); } } /** * Loads the given class or interface. * * @param string $class The name of the class * @return true|null True if loaded, null otherwise */ public function loadClass($class) { if ($file = $this->findFile($class)) { includeFile($file); return true; } return null; } /** * Finds the path to the file where the class is defined. * * @param string $class The name of the class * * @return string|false The path if found, false otherwise */ public function findFile($class) { // class map lookup if (isset($this->classMap[$class])) { return $this->classMap[$class]; } if ($this->classMapAuthoritative || isset($this->missingClasses[$class])) { return false; } if (null !== $this->apcuPrefix) { $file = apcu_fetch($this->apcuPrefix.$class, $hit); if ($hit) { return $file; } } $file = $this->findFileWithExtension($class, '.php'); // Search for Hack files if we are running on HHVM if (false === $file && defined('HHVM_VERSION')) { $file = $this->findFileWithExtension($class, '.hh'); } if (null !== $this->apcuPrefix) { apcu_add($this->apcuPrefix.$class, $file); } if (false === $file) { // Remember that this class does not exist. $this->missingClasses[$class] = true; } return $file; } /** * Returns the currently registered loaders indexed by their corresponding vendor directories. * * @return self[] */ public static function getRegisteredLoaders() { return self::$registeredLoaders; } /** * @param string $class * @param string $ext * @return string|false */ private function findFileWithExtension($class, $ext) { // PSR-4 lookup $logicalPathPsr4 = strtr($class, '\\', DIRECTORY_SEPARATOR) . $ext; $first = $class[0]; if (isset($this->prefixLengthsPsr4[$first])) { $subPath = $class; while (false !== $lastPos = strrpos($subPath, '\\')) { $subPath = substr($subPath, 0, $lastPos); $search = $subPath . '\\'; if (isset($this->prefixDirsPsr4[$search])) { $pathEnd = DIRECTORY_SEPARATOR . substr($logicalPathPsr4, $lastPos + 1); foreach ($this->prefixDirsPsr4[$search] as $dir) { if (file_exists($file = $dir . $pathEnd)) { return $file; } } } } } // PSR-4 fallback dirs foreach ($this->fallbackDirsPsr4 as $dir) { if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr4)) { return $file; } } // PSR-0 lookup if (false !== $pos = strrpos($class, '\\')) { // namespaced class name $logicalPathPsr0 = substr($logicalPathPsr4, 0, $pos + 1) . strtr(substr($logicalPathPsr4, $pos + 1), '_', DIRECTORY_SEPARATOR); } else { // PEAR-like class name $logicalPathPsr0 = strtr($class, '_', DIRECTORY_SEPARATOR) . $ext; } if (isset($this->prefixesPsr0[$first])) { foreach ($this->prefixesPsr0[$first] as $prefix => $dirs) { if (0 === strpos($class, $prefix)) { foreach ($dirs as $dir) { if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr0)) { return $file; } } } } } // PSR-0 fallback dirs foreach ($this->fallbackDirsPsr0 as $dir) { if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr0)) { return $file; } } // PSR-0 include paths. if ($this->useIncludePath && $file = stream_resolve_include_path($logicalPathPsr0)) { return $file; } return false; } } /** * Scope isolated include. * * Prevents access to $this/self from included files. * * @param string $file * @return void * @private */ function includeFile($file) { include $file; } * Jordi Boggiano * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Composer\Autoload; use Composer\Pcre\Preg; /** * @author Jordi Boggiano * @internal */ class PhpFileCleaner { /** @var array */ private static $typeConfig; /** @var string */ private static $restPattern; /** * @readonly * @var string */ private $contents; /** * @readonly * @var int */ private $len; /** * @readonly * @var int */ private $maxMatches; /** @var int */ private $index = 0; /** * @param string[] $types * @return void */ public static function setTypeConfig($types) { foreach ($types as $type) { self::$typeConfig[$type[0]] = array( 'name' => $type, 'length' => \strlen($type), 'pattern' => '{.\b(?])'.$type.'\s++[a-zA-Z_\x7f-\xff:][a-zA-Z0-9_\x7f-\xff:\-]*+}Ais', ); } self::$restPattern = '{[^?"\'contents = $contents; $this->len = \strlen($this->contents); $this->maxMatches = $maxMatches; } /** * @return string */ public function clean() { $clean = ''; while ($this->index < $this->len) { $this->skipToPhp(); $clean .= 'index < $this->len) { $char = $this->contents[$this->index]; if ($char === '?' && $this->peek('>')) { $clean .= '?>'; $this->index += 2; continue 2; } if ($char === '"') { $this->skipString('"'); $clean .= 'null'; continue; } if ($char === "'") { $this->skipString("'"); $clean .= 'null'; continue; } if ($char === "<" && $this->peek('<') && $this->match('{<<<[ \t]*+([\'"]?)([a-zA-Z_\x80-\xff][a-zA-Z0-9_\x80-\xff]*+)\\1(?:\r\n|\n|\r)}A', $match)) { $this->index += \strlen($match[0]); $this->skipHeredoc($match[2]); $clean .= 'null'; continue; } if ($char === '/') { if ($this->peek('/')) { $this->skipToNewline(); continue; } if ($this->peek('*')) { $this->skipComment(); continue; } } if ($this->maxMatches === 1 && isset(self::$typeConfig[$char])) { $type = self::$typeConfig[$char]; if ( \substr($this->contents, $this->index, $type['length']) === $type['name'] && Preg::isMatch($type['pattern'], $this->contents, $match, 0, $this->index - 1) ) { $clean .= $match[0]; return $clean; } } $this->index += 1; if ($this->match(self::$restPattern, $match)) { $clean .= $char . $match[0]; $this->index += \strlen($match[0]); } else { $clean .= $char; } } } return $clean; } /** * @return void */ private function skipToPhp() { while ($this->index < $this->len) { if ($this->contents[$this->index] === '<' && $this->peek('?')) { $this->index += 2; break; } $this->index += 1; } } /** * @param string $delimiter * @return void */ private function skipString($delimiter) { $this->index += 1; while ($this->index < $this->len) { if ($this->contents[$this->index] === '\\' && ($this->peek('\\') || $this->peek($delimiter))) { $this->index += 2; continue; } if ($this->contents[$this->index] === $delimiter) { $this->index += 1; break; } $this->index += 1; } } /** * @return void */ private function skipComment() { $this->index += 2; while ($this->index < $this->len) { if ($this->contents[$this->index] === '*' && $this->peek('/')) { $this->index += 2; break; } $this->index += 1; } } /** * @return void */ private function skipToNewline() { while ($this->index < $this->len) { if ($this->contents[$this->index] === "\r" || $this->contents[$this->index] === "\n") { return; } $this->index += 1; } } /** * @param string $delimiter * @return void */ private function skipHeredoc($delimiter) { $firstDelimiterChar = $delimiter[0]; $delimiterLength = \strlen($delimiter); $delimiterPattern = '{'.preg_quote($delimiter).'(?![a-zA-Z0-9_\x80-\xff])}A'; while ($this->index < $this->len) { // check if we find the delimiter after some spaces/tabs switch ($this->contents[$this->index]) { case "\t": case " ": $this->index += 1; continue 2; case $firstDelimiterChar: if ( \substr($this->contents, $this->index, $delimiterLength) === $delimiter && $this->match($delimiterPattern) ) { $this->index += $delimiterLength; return; } break; } // skip the rest of the line while ($this->index < $this->len) { $this->skipToNewline(); // skip newlines while ($this->index < $this->len && ($this->contents[$this->index] === "\r" || $this->contents[$this->index] === "\n")) { $this->index += 1; } break; } } } /** * @param string $char * @return bool */ private function peek($char) { return $this->index + 1 < $this->len && $this->contents[$this->index + 1] === $char; } /** * @param string $regex * @param ?array $match * @return bool */ private function match($regex, array &$match = null) { return Preg::isMatch($regex, $this->contents, $match, 0, $this->index); } } * Jordi Boggiano * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Composer\Autoload; use Composer\Config; use Composer\EventDispatcher\EventDispatcher; use Composer\Filter\PlatformRequirementFilter\IgnoreAllPlatformRequirementFilter; use Composer\Filter\PlatformRequirementFilter\PlatformRequirementFilterFactory; use Composer\Filter\PlatformRequirementFilter\PlatformRequirementFilterInterface; use Composer\Installer\InstallationManager; use Composer\IO\IOInterface; use Composer\Package\AliasPackage; use Composer\Package\PackageInterface; use Composer\Package\RootPackageInterface; use Composer\Pcre\Preg; use Composer\Repository\InstalledRepositoryInterface; use Composer\Semver\Constraint\Bound; use Composer\Util\Filesystem; use Composer\Util\Platform; use Composer\Script\ScriptEvents; use Composer\Util\PackageSorter; use Composer\Json\JsonFile; /** * @author Igor Wiedler * @author Jordi Boggiano */ class AutoloadGenerator { /** * @var EventDispatcher */ private $eventDispatcher; /** * @var ?IOInterface */ private $io; /** * @var ?bool */ private $devMode = null; /** * @var bool */ private $classMapAuthoritative = false; /** * @var bool */ private $apcu = false; /** * @var string|null */ private $apcuPrefix; /** * @var bool */ private $runScripts = false; /** * @var PlatformRequirementFilterInterface */ private $platformRequirementFilter; public function __construct(EventDispatcher $eventDispatcher, IOInterface $io = null) { $this->eventDispatcher = $eventDispatcher; $this->io = $io; $this->platformRequirementFilter = PlatformRequirementFilterFactory::ignoreNothing(); } /** * @param bool $devMode * @return void */ public function setDevMode($devMode = true) { $this->devMode = (bool) $devMode; } /** * Whether generated autoloader considers the class map authoritative. * * @param bool $classMapAuthoritative * @return void */ public function setClassMapAuthoritative($classMapAuthoritative) { $this->classMapAuthoritative = (bool) $classMapAuthoritative; } /** * Whether generated autoloader considers APCu caching. * * @param bool $apcu * @param string|null $apcuPrefix * @return void */ public function setApcu($apcu, $apcuPrefix = null) { $this->apcu = (bool) $apcu; $this->apcuPrefix = $apcuPrefix !== null ? (string) $apcuPrefix : $apcuPrefix; } /** * Whether to run scripts or not * * @param bool $runScripts * @return void */ public function setRunScripts($runScripts = true) { $this->runScripts = (bool) $runScripts; } /** * Whether platform requirements should be ignored. * * If this is set to true, the platform check file will not be generated * If this is set to false, the platform check file will be generated with all requirements * If this is set to string[], those packages will be ignored from the platform check file * * @param bool|string[] $ignorePlatformReqs * @return void * * @deprecated use setPlatformRequirementFilter instead */ public function setIgnorePlatformRequirements($ignorePlatformReqs) { trigger_error('AutoloadGenerator::setIgnorePlatformRequirements is deprecated since Composer 2.2, use setPlatformRequirementFilter instead.', E_USER_DEPRECATED); $this->setPlatformRequirementFilter(PlatformRequirementFilterFactory::fromBoolOrList($ignorePlatformReqs)); } /** * @return void */ public function setPlatformRequirementFilter(PlatformRequirementFilterInterface $platformRequirementFilter) { $this->platformRequirementFilter = $platformRequirementFilter; } /** * @param string $targetDir * @param bool $scanPsrPackages * @param string $suffix * @return int * @throws \Seld\JsonLint\ParsingException */ public function dump(Config $config, InstalledRepositoryInterface $localRepo, RootPackageInterface $rootPackage, InstallationManager $installationManager, $targetDir, $scanPsrPackages = false, $suffix = '') { if ($this->classMapAuthoritative) { // Force scanPsrPackages when classmap is authoritative $scanPsrPackages = true; } // auto-set devMode based on whether dev dependencies are installed or not if (null === $this->devMode) { // we assume no-dev mode if no vendor dir is present or it is too old to contain dev information $this->devMode = false; $installedJson = new JsonFile($config->get('vendor-dir').'/composer/installed.json'); if ($installedJson->exists()) { $installedJson = $installedJson->read(); if (isset($installedJson['dev'])) { $this->devMode = $installedJson['dev']; } } } if ($this->runScripts) { // set COMPOSER_DEV_MODE in case not set yet so it is available in the dump-autoload event listeners if (!isset($_SERVER['COMPOSER_DEV_MODE'])) { Platform::putEnv('COMPOSER_DEV_MODE', $this->devMode ? '1' : '0'); } $this->eventDispatcher->dispatchScript(ScriptEvents::PRE_AUTOLOAD_DUMP, $this->devMode, array(), array( 'optimize' => (bool) $scanPsrPackages, )); } $filesystem = new Filesystem(); $filesystem->ensureDirectoryExists($config->get('vendor-dir')); // Do not remove double realpath() calls. // Fixes failing Windows realpath() implementation. // See https://bugs.php.net/bug.php?id=72738 $basePath = $filesystem->normalizePath(realpath(realpath(getcwd()))); $vendorPath = $filesystem->normalizePath(realpath(realpath($config->get('vendor-dir')))); $useGlobalIncludePath = (bool) $config->get('use-include-path'); $prependAutoloader = $config->get('prepend-autoloader') === false ? 'false' : 'true'; $targetDir = $vendorPath.'/'.$targetDir; $filesystem->ensureDirectoryExists($targetDir); $vendorPathCode = $filesystem->findShortestPathCode(realpath($targetDir), $vendorPath, true); $vendorPathCode52 = str_replace('__DIR__', 'dirname(__FILE__)', $vendorPathCode); $vendorPathToTargetDirCode = $filesystem->findShortestPathCode($vendorPath, realpath($targetDir), true); $appBaseDirCode = $filesystem->findShortestPathCode($vendorPath, $basePath, true); $appBaseDirCode = str_replace('__DIR__', '$vendorDir', $appBaseDirCode); $namespacesFile = <<getDevPackageNames(); $packageMap = $this->buildPackageMap($installationManager, $rootPackage, $localRepo->getCanonicalPackages()); if ($this->devMode) { // if dev mode is enabled, then we do not filter any dev packages out so disable this entirely $filteredDevPackages = false; } else { // if the list of dev package names is available we use that straight, otherwise pass true which means use legacy algo to figure them out $filteredDevPackages = $devPackageNames ?: true; } $autoloads = $this->parseAutoloads($packageMap, $rootPackage, $filteredDevPackages); // Process the 'psr-0' base directories. foreach ($autoloads['psr-0'] as $namespace => $paths) { $exportedPaths = array(); foreach ($paths as $path) { $exportedPaths[] = $this->getPathCode($filesystem, $basePath, $vendorPath, $path); } $exportedPrefix = var_export($namespace, true); $namespacesFile .= " $exportedPrefix => "; $namespacesFile .= "array(".implode(', ', $exportedPaths)."),\n"; } $namespacesFile .= ");\n"; // Process the 'psr-4' base directories. foreach ($autoloads['psr-4'] as $namespace => $paths) { $exportedPaths = array(); foreach ($paths as $path) { $exportedPaths[] = $this->getPathCode($filesystem, $basePath, $vendorPath, $path); } $exportedPrefix = var_export($namespace, true); $psr4File .= " $exportedPrefix => "; $psr4File .= "array(".implode(', ', $exportedPaths)."),\n"; } $psr4File .= ");\n"; $classmapFile = <<getAutoload(); if ($rootPackage->getTargetDir() && !empty($mainAutoload['psr-0'])) { $levels = substr_count($filesystem->normalizePath($rootPackage->getTargetDir()), '/') + 1; $prefixes = implode(', ', array_map(function ($prefix) { return var_export($prefix, true); }, array_keys($mainAutoload['psr-0']))); $baseDirFromTargetDirCode = $filesystem->findShortestPathCode($targetDir, $basePath, true); $targetDirLoader = <<addClassMapCode($filesystem, $basePath, $vendorPath, $dir, $excluded, null, null, $classMap, $ambiguousClasses, $scannedFiles); } if ($scanPsrPackages) { $namespacesToScan = array(); // Scan the PSR-0/4 directories for class files, and add them to the class map foreach (array('psr-4', 'psr-0') as $psrType) { foreach ($autoloads[$psrType] as $namespace => $paths) { $namespacesToScan[$namespace][] = array('paths' => $paths, 'type' => $psrType); } } krsort($namespacesToScan); foreach ($namespacesToScan as $namespace => $groups) { foreach ($groups as $group) { foreach ($group['paths'] as $dir) { $dir = $filesystem->normalizePath($filesystem->isAbsolutePath($dir) ? $dir : $basePath.'/'.$dir); if (!is_dir($dir)) { continue; } $classMap = $this->addClassMapCode($filesystem, $basePath, $vendorPath, $dir, $excluded, $namespace, $group['type'], $classMap, $ambiguousClasses, $scannedFiles); } } } } foreach ($ambiguousClasses as $className => $ambiguousPaths) { $cleanPath = str_replace(array('$vendorDir . \'', '$baseDir . \'', "',\n"), array($vendorPath, $basePath, ''), $classMap[$className]); $this->io->writeError( 'Warning: Ambiguous class resolution, "'.$className.'"'. ' was found '. (count($ambiguousPaths) + 1) .'x: in "'.$cleanPath.'" and "'. implode('", "', $ambiguousPaths) .'", the first will be used.' ); } $classMap['Composer\\InstalledVersions'] = "\$vendorDir . '/composer/InstalledVersions.php',\n"; ksort($classMap); foreach ($classMap as $class => $code) { $classmapFile .= ' '.var_export($class, true).' => '.$code; } $classmapFile .= ");\n"; if (!$suffix) { if (!$config->get('autoloader-suffix') && Filesystem::isReadable($vendorPath.'/autoload.php')) { $content = file_get_contents($vendorPath.'/autoload.php'); if (Preg::isMatch('{ComposerAutoloaderInit([^:\s]+)::}', $content, $match)) { $suffix = $match[1]; } } if (!$suffix) { $suffix = $config->get('autoloader-suffix') ?: md5(uniqid('', true)); } } $filesystem->filePutContentsIfModified($targetDir.'/autoload_namespaces.php', $namespacesFile); $filesystem->filePutContentsIfModified($targetDir.'/autoload_psr4.php', $psr4File); $filesystem->filePutContentsIfModified($targetDir.'/autoload_classmap.php', $classmapFile); $includePathFilePath = $targetDir.'/include_paths.php'; if ($includePathFileContents = $this->getIncludePathsFile($packageMap, $filesystem, $basePath, $vendorPath, $vendorPathCode52, $appBaseDirCode)) { $filesystem->filePutContentsIfModified($includePathFilePath, $includePathFileContents); } elseif (file_exists($includePathFilePath)) { unlink($includePathFilePath); } $includeFilesFilePath = $targetDir.'/autoload_files.php'; if ($includeFilesFileContents = $this->getIncludeFilesFile($autoloads['files'], $filesystem, $basePath, $vendorPath, $vendorPathCode52, $appBaseDirCode)) { $filesystem->filePutContentsIfModified($includeFilesFilePath, $includeFilesFileContents); } elseif (file_exists($includeFilesFilePath)) { unlink($includeFilesFilePath); } $filesystem->filePutContentsIfModified($targetDir.'/autoload_static.php', $this->getStaticFile($suffix, $targetDir, $vendorPath, $basePath, $staticPhpVersion)); $checkPlatform = $config->get('platform-check') && !($this->platformRequirementFilter instanceof IgnoreAllPlatformRequirementFilter); $platformCheckContent = null; if ($checkPlatform) { $platformCheckContent = $this->getPlatformCheck($packageMap, $config->get('platform-check'), $devPackageNames); if (null === $platformCheckContent) { $checkPlatform = false; } } if ($checkPlatform) { $filesystem->filePutContentsIfModified($targetDir.'/platform_check.php', $platformCheckContent); } elseif (file_exists($targetDir.'/platform_check.php')) { unlink($targetDir.'/platform_check.php'); } $filesystem->filePutContentsIfModified($vendorPath.'/autoload.php', $this->getAutoloadFile($vendorPathToTargetDirCode, $suffix)); $filesystem->filePutContentsIfModified($targetDir.'/autoload_real.php', $this->getAutoloadRealFile(true, (bool) $includePathFileContents, $targetDirLoader, (bool) $includeFilesFileContents, $vendorPathCode, $appBaseDirCode, $suffix, $useGlobalIncludePath, $prependAutoloader, $staticPhpVersion, $checkPlatform)); $filesystem->safeCopy(__DIR__.'/ClassLoader.php', $targetDir.'/ClassLoader.php'); $filesystem->safeCopy(__DIR__.'/../../../LICENSE', $targetDir.'/LICENSE'); if ($this->runScripts) { $this->eventDispatcher->dispatchScript(ScriptEvents::POST_AUTOLOAD_DUMP, $this->devMode, array(), array( 'optimize' => (bool) $scanPsrPackages, )); } return count($classMap); } /** * @param string $basePath * @param string $vendorPath * @param string $dir * @param ?array $excluded * @param ?string $namespaceFilter * @param ?string $autoloadType * @param array $classMap * @param array> $ambiguousClasses * @param array $scannedFiles * @return array */ private function addClassMapCode(Filesystem $filesystem, $basePath, $vendorPath, $dir, $excluded, $namespaceFilter, $autoloadType, array $classMap, array &$ambiguousClasses, array &$scannedFiles) { foreach ($this->generateClassMap($dir, $excluded, $namespaceFilter, $autoloadType, true, $scannedFiles) as $class => $path) { $pathCode = $this->getPathCode($filesystem, $basePath, $vendorPath, $path).",\n"; if (!isset($classMap[$class])) { $classMap[$class] = $pathCode; } elseif ($this->io && $classMap[$class] !== $pathCode && !Preg::isMatch('{/(test|fixture|example|stub)s?/}i', strtr($classMap[$class].' '.$path, '\\', '/'))) { $ambiguousClasses[$class][] = $path; } } return $classMap; } /** * @param string $dir * @param ?array $excluded * @param ?string $namespaceFilter * @param ?string $autoloadType * @param bool $showAmbiguousWarning * @param array $scannedFiles * @return array */ private function generateClassMap($dir, $excluded, $namespaceFilter, $autoloadType, $showAmbiguousWarning, array &$scannedFiles) { if ($excluded) { // filter excluded patterns here to only use those matching $dir // exclude-from-classmap patterns are all realpath'd so we can only filter them if $dir exists so that realpath($dir) will work // if $dir does not exist, it should anyway not find anything there so no trouble if (file_exists($dir)) { // transform $dir in the same way that exclude-from-classmap patterns are transformed so we can match them against each other $dirMatch = preg_quote(strtr(realpath($dir), '\\', '/')); foreach ($excluded as $index => $pattern) { // extract the constant string prefix of the pattern here, until we reach a non-escaped regex special character $pattern = Preg::replace('{^(([^.+*?\[^\]$(){}=!<>|:\\\\#-]+|\\\\[.+*?\[^\]$(){}=!<>|:#-])*).*}', '$1', $pattern); // if the pattern is not a subset or superset of $dir, it is unrelated and we skip it if (0 !== strpos($pattern, $dirMatch) && 0 !== strpos($dirMatch, $pattern)) { unset($excluded[$index]); } } } $excluded = $excluded ? '{(' . implode('|', $excluded) . ')}' : null; } return ClassMapGenerator::createMap($dir, $excluded, $showAmbiguousWarning ? $this->io : null, $namespaceFilter, $autoloadType, $scannedFiles); } /** * @param InstallationManager $installationManager * @param PackageInterface[] $packages * @return array */ public function buildPackageMap(InstallationManager $installationManager, PackageInterface $rootPackage, array $packages) { // build package => install path map $packageMap = array(array($rootPackage, '')); foreach ($packages as $package) { if ($package instanceof AliasPackage) { continue; } $this->validatePackage($package); $packageMap[] = array( $package, $installationManager->getInstallPath($package), ); } return $packageMap; } /** * @return void * @throws \InvalidArgumentException Throws an exception, if the package has illegal settings. */ protected function validatePackage(PackageInterface $package) { $autoload = $package->getAutoload(); if (!empty($autoload['psr-4']) && null !== $package->getTargetDir()) { $name = $package->getName(); $package->getTargetDir(); throw new \InvalidArgumentException("PSR-4 autoloading is incompatible with the target-dir property, remove the target-dir in package '$name'."); } if (!empty($autoload['psr-4'])) { foreach ($autoload['psr-4'] as $namespace => $dirs) { if ($namespace !== '' && '\\' !== substr($namespace, -1)) { throw new \InvalidArgumentException("psr-4 namespaces must end with a namespace separator, '$namespace' does not, use '$namespace\\'."); } } } } /** * Compiles an ordered list of namespace => path mappings * * @param array $packageMap array of array(package, installDir-relative-to-composer.json) * @param RootPackageInterface $rootPackage root package instance * @param bool|string[] $filteredDevPackages If an array, the list of packages that must be removed. If bool, whether to filter out require-dev packages * @return array * @phpstan-return array{ * 'psr-0': array>, * 'psr-4': array>, * 'classmap': array, * 'files': array, * 'exclude-from-classmap': array, * } */ public function parseAutoloads(array $packageMap, PackageInterface $rootPackage, $filteredDevPackages = false) { $rootPackageMap = array_shift($packageMap); if (is_array($filteredDevPackages)) { $packageMap = array_filter($packageMap, function ($item) use ($filteredDevPackages) { return !in_array($item[0]->getName(), $filteredDevPackages, true); }); } elseif ($filteredDevPackages) { $packageMap = $this->filterPackageMap($packageMap, $rootPackage); } $sortedPackageMap = $this->sortPackageMap($packageMap); $sortedPackageMap[] = $rootPackageMap; array_unshift($packageMap, $rootPackageMap); $psr0 = $this->parseAutoloadsType($packageMap, 'psr-0', $rootPackage); $psr4 = $this->parseAutoloadsType($packageMap, 'psr-4', $rootPackage); $classmap = $this->parseAutoloadsType(array_reverse($sortedPackageMap), 'classmap', $rootPackage); $files = $this->parseAutoloadsType($sortedPackageMap, 'files', $rootPackage); $exclude = $this->parseAutoloadsType($sortedPackageMap, 'exclude-from-classmap', $rootPackage); krsort($psr0); krsort($psr4); return array( 'psr-0' => $psr0, 'psr-4' => $psr4, 'classmap' => $classmap, 'files' => $files, 'exclude-from-classmap' => $exclude, ); } /** * Registers an autoloader based on an autoload-map returned by parseAutoloads * * @param array $autoloads see parseAutoloads return value * @param ?string $vendorDir * @return ClassLoader */ public function createLoader(array $autoloads, $vendorDir = null) { $loader = new ClassLoader($vendorDir); if (isset($autoloads['psr-0'])) { foreach ($autoloads['psr-0'] as $namespace => $path) { $loader->add($namespace, $path); } } if (isset($autoloads['psr-4'])) { foreach ($autoloads['psr-4'] as $namespace => $path) { $loader->addPsr4($namespace, $path); } } if (isset($autoloads['classmap'])) { $excluded = null; if (!empty($autoloads['exclude-from-classmap'])) { $excluded = $autoloads['exclude-from-classmap']; } $scannedFiles = array(); foreach ($autoloads['classmap'] as $dir) { try { $loader->addClassMap($this->generateClassMap($dir, $excluded, null, null, false, $scannedFiles)); } catch (\RuntimeException $e) { $this->io->writeError(''.$e->getMessage().''); } } } return $loader; } /** * @param array $packageMap * @param string $basePath * @param string $vendorPath * @param string $vendorPathCode * @param string $appBaseDirCode * @return ?string */ protected function getIncludePathsFile(array $packageMap, Filesystem $filesystem, $basePath, $vendorPath, $vendorPathCode, $appBaseDirCode) { $includePaths = array(); foreach ($packageMap as $item) { list($package, $installPath) = $item; if (null !== $package->getTargetDir() && strlen($package->getTargetDir()) > 0) { $installPath = substr($installPath, 0, -strlen('/'.$package->getTargetDir())); } foreach ($package->getIncludePaths() as $includePath) { $includePath = trim($includePath, '/'); $includePaths[] = empty($installPath) ? $includePath : $installPath.'/'.$includePath; } } if (!$includePaths) { return null; } $includePathsCode = ''; foreach ($includePaths as $path) { $includePathsCode .= " " . $this->getPathCode($filesystem, $basePath, $vendorPath, $path) . ",\n"; } return << $files * @param string $basePath * @param string $vendorPath * @param string $vendorPathCode * @param string $appBaseDirCode * @return ?string */ protected function getIncludeFilesFile(array $files, Filesystem $filesystem, $basePath, $vendorPath, $vendorPathCode, $appBaseDirCode) { $filesCode = ''; foreach ($files as $fileIdentifier => $functionFile) { $filesCode .= ' ' . var_export($fileIdentifier, true) . ' => ' . $this->getPathCode($filesystem, $basePath, $vendorPath, $functionFile) . ",\n"; } if (!$filesCode) { return null; } return <<isAbsolutePath($path)) { $path = $basePath . '/' . $path; } $path = $filesystem->normalizePath($path); $baseDir = ''; if (strpos($path.'/', $vendorPath.'/') === 0) { $path = substr($path, strlen($vendorPath)); $baseDir = '$vendorDir'; if ($path !== false) { $baseDir .= " . "; } } else { $path = $filesystem->normalizePath($filesystem->findShortestPath($basePath, $path, true)); if (!$filesystem->isAbsolutePath($path)) { $baseDir = '$baseDir . '; $path = '/' . $path; } } if (strpos($path, '.phar') !== false) { $baseDir = "'phar://' . " . $baseDir; } return $baseDir . var_export($path, true); } /** * @param array $packageMap * @param bool $checkPlatform * @param string[] $devPackageNames * @return ?string */ protected function getPlatformCheck(array $packageMap, $checkPlatform, array $devPackageNames) { $lowestPhpVersion = Bound::zero(); $requiredExtensions = array(); $extensionProviders = array(); foreach ($packageMap as $item) { $package = $item[0]; foreach (array_merge($package->getReplaces(), $package->getProvides()) as $link) { if (Preg::isMatch('{^ext-(.+)$}iD', $link->getTarget(), $match)) { $extensionProviders[$match[1]][] = $link->getConstraint(); } } } foreach ($packageMap as $item) { $package = $item[0]; // skip dev dependencies platform requirements as platform-check really should only be a production safeguard if (in_array($package->getName(), $devPackageNames, true)) { continue; } foreach ($package->getRequires() as $link) { if ($this->platformRequirementFilter->isIgnored($link->getTarget())) { continue; } if ('php' === $link->getTarget()) { $constraint = $link->getConstraint(); if ($constraint->getLowerBound()->compareTo($lowestPhpVersion, '>')) { $lowestPhpVersion = $constraint->getLowerBound(); } } if ($checkPlatform === true && Preg::isMatch('{^ext-(.+)$}iD', $link->getTarget(), $match)) { // skip extension checks if they have a valid provider/replacer if (isset($extensionProviders[$match[1]])) { foreach ($extensionProviders[$match[1]] as $provided) { if ($provided->matches($link->getConstraint())) { continue 2; } } } if ($match[1] === 'zend-opcache') { $match[1] = 'zend opcache'; } $extension = var_export($match[1], true); if ($match[1] === 'pcntl' || $match[1] === 'readline') { $requiredExtensions[$extension] = "PHP_SAPI !== 'cli' || extension_loaded($extension) || \$missingExtensions[] = $extension;\n"; } else { $requiredExtensions[$extension] = "extension_loaded($extension) || \$missingExtensions[] = $extension;\n"; } } } } ksort($requiredExtensions); $formatToPhpVersionId = function (Bound $bound) { if ($bound->isZero()) { return 0; } if ($bound->isPositiveInfinity()) { return 99999; } $version = str_replace('-', '.', $bound->getVersion()); $chunks = array_map('intval', explode('.', $version)); return $chunks[0] * 10000 + $chunks[1] * 100 + $chunks[2]; }; $formatToHumanReadable = function (Bound $bound) { if ($bound->isZero()) { return 0; } if ($bound->isPositiveInfinity()) { return 99999; } $version = str_replace('-', '.', $bound->getVersion()); $chunks = explode('.', $version); $chunks = array_slice($chunks, 0, 3); return implode('.', $chunks); }; $requiredPhp = ''; $requiredPhpError = ''; if (!$lowestPhpVersion->isZero()) { $operator = $lowestPhpVersion->isInclusive() ? '>=' : '>'; $requiredPhp = 'PHP_VERSION_ID '.$operator.' '.$formatToPhpVersionId($lowestPhpVersion); $requiredPhpError = '"'.$operator.' '.$formatToHumanReadable($lowestPhpVersion).'"'; } if ($requiredPhp) { $requiredPhp = <<= $staticPhpVersion && !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\ComposerStaticInit$suffix::getInitializer(\$loader)); } else { STATIC_INIT; if (!$this->classMapAuthoritative) { $file .= <<<'PSR04' $map = require __DIR__ . '/autoload_namespaces.php'; foreach ($map as $namespace => $path) { $loader->set($namespace, $path); } $map = require __DIR__ . '/autoload_psr4.php'; foreach ($map as $namespace => $path) { $loader->setPsr4($namespace, $path); } PSR04; } if ($useClassMap) { $file .= <<<'CLASSMAP' $classMap = require __DIR__ . '/autoload_classmap.php'; if ($classMap) { $loader->addClassMap($classMap); } CLASSMAP; } $file .= " }\n\n"; if ($this->classMapAuthoritative) { $file .= <<<'CLASSMAPAUTHORITATIVE' $loader->setClassMapAuthoritative(true); CLASSMAPAUTHORITATIVE; } if ($this->apcu) { $apcuPrefix = var_export(($this->apcuPrefix !== null ? $this->apcuPrefix : substr(base64_encode(md5(uniqid('', true), true)), 0, -3)), true); $file .= <<setApcuPrefix($apcuPrefix); APCU; } if ($useGlobalIncludePath) { $file .= <<<'INCLUDEPATH' $loader->setUseIncludePath(true); INCLUDEPATH; } if ($targetDirLoader) { $file .= <<register($prependAutoloader); REGISTER_LOADER; if ($useIncludeFiles) { $file .= << \$file) { composerRequire$suffix(\$fileIdentifier, \$file); } INCLUDE_FILES; } $file .= << $path) { $loader->set($namespace, $path); } $map = require $targetDir . '/autoload_psr4.php'; foreach ($map as $namespace => $path) { $loader->setPsr4($namespace, $path); } $classMap = require $targetDir . '/autoload_classmap.php'; if ($classMap) { $loader->addClassMap($classMap); } $filesystem = new Filesystem(); $vendorPathCode = ' => ' . $filesystem->findShortestPathCode(realpath($targetDir), $vendorPath, true, true) . " . '/"; $vendorPharPathCode = ' => \'phar://\' . ' . $filesystem->findShortestPathCode(realpath($targetDir), $vendorPath, true, true) . " . '/"; $appBaseDirCode = ' => ' . $filesystem->findShortestPathCode(realpath($targetDir), $basePath, true, true) . " . '/"; $appBaseDirPharCode = ' => \'phar://\' . ' . $filesystem->findShortestPathCode(realpath($targetDir), $basePath, true, true) . " . '/"; $absoluteVendorPathCode = ' => ' . substr(var_export(rtrim($vendorDir, '\\/') . '/', true), 0, -1); $absoluteVendorPharPathCode = ' => ' . substr(var_export(rtrim('phar://' . $vendorDir, '\\/') . '/', true), 0, -1); $absoluteAppBaseDirCode = ' => ' . substr(var_export(rtrim($baseDir, '\\/') . '/', true), 0, -1); $absoluteAppBaseDirPharCode = ' => ' . substr(var_export(rtrim('phar://' . $baseDir, '\\/') . '/', true), 0, -1); $initializer = ''; $prefix = "\0Composer\Autoload\ClassLoader\0"; $prefixLen = strlen($prefix); if (file_exists($targetDir . '/autoload_files.php')) { $maps = array('files' => require $targetDir . '/autoload_files.php'); } else { $maps = array(); } foreach ((array) $loader as $prop => $value) { if ($value && 0 === strpos($prop, $prefix)) { $maps[substr($prop, $prefixLen)] = $value; } } foreach ($maps as $prop => $value) { if (count($value) > 32767) { // Static arrays are limited to 32767 values on PHP 5.6 // See https://bugs.php.net/68057 $staticPhpVersion = 70000; } $value = strtr( var_export($value, true), array( $absoluteVendorPathCode => $vendorPathCode, $absoluteVendorPharPathCode => $vendorPharPathCode, $absoluteAppBaseDirCode => $appBaseDirCode, $absoluteAppBaseDirPharCode => $appBaseDirPharCode, ) ); $value = ltrim(Preg::replace('/^ */m', ' $0$0', $value)); $file .= sprintf(" public static $%s = %s;\n\n", $prop, $value); if ('files' !== $prop) { $initializer .= " \$loader->$prop = ComposerStaticInit$suffix::\$$prop;\n"; } } return $file . << $packageMap * @param string $type one of: 'psr-0'|'psr-4'|'classmap'|'files' * @return array|array>|array */ protected function parseAutoloadsType(array $packageMap, $type, RootPackageInterface $rootPackage) { $autoloads = array(); foreach ($packageMap as $item) { list($package, $installPath) = $item; $autoload = $package->getAutoload(); if ($this->devMode && $package === $rootPackage) { $autoload = array_merge_recursive($autoload, $package->getDevAutoload()); } // skip misconfigured packages if (!isset($autoload[$type]) || !is_array($autoload[$type])) { continue; } if (null !== $package->getTargetDir() && $package !== $rootPackage) { $installPath = substr($installPath, 0, -strlen('/'.$package->getTargetDir())); } foreach ($autoload[$type] as $namespace => $paths) { foreach ((array) $paths as $path) { if (($type === 'files' || $type === 'classmap' || $type === 'exclude-from-classmap') && $package->getTargetDir() && !Filesystem::isReadable($installPath.'/'.$path)) { // remove target-dir from file paths of the root package if ($package === $rootPackage) { $targetDir = str_replace('\\', '[\\\\/]', preg_quote(str_replace(array('/', '\\'), '', $package->getTargetDir()))); $path = ltrim(Preg::replace('{^'.$targetDir.'}', '', ltrim($path, '\\/')), '\\/'); } else { // add target-dir from file paths that don't have it $path = $package->getTargetDir() . '/' . $path; } } if ($type === 'exclude-from-classmap') { // first escape user input $path = Preg::replace('{/+}', '/', preg_quote(trim(strtr($path, '\\', '/'), '/'))); // add support for wildcards * and ** $path = strtr($path, array('\\*\\*' => '.+?', '\\*' => '[^/]+?')); // add support for up-level relative paths $updir = null; $path = Preg::replaceCallback( '{^((?:(?:\\\\\\.){1,2}+/)+)}', function ($matches) use (&$updir) { if (isset($matches[1])) { // undo preg_quote for the matched string $updir = str_replace('\\.', '.', $matches[1]); } return ''; }, $path ); if (empty($installPath)) { $installPath = strtr(getcwd(), '\\', '/'); } $resolvedPath = realpath($installPath . '/' . $updir); $autoloads[] = preg_quote(strtr($resolvedPath, '\\', '/')) . '/' . $path . '($|/)'; continue; } $relativePath = empty($installPath) ? (empty($path) ? '.' : $path) : $installPath.'/'.$path; if ($type === 'files') { $autoloads[$this->getFileIdentifier($package, $path)] = $relativePath; continue; } if ($type === 'classmap') { $autoloads[] = $relativePath; continue; } $autoloads[$namespace][] = $relativePath; } } } return $autoloads; } /** * @param string $path * @return string */ protected function getFileIdentifier(PackageInterface $package, $path) { return md5($package->getName() . ':' . $path); } /** * Filters out dev-dependencies * * @param array $packageMap * @param RootPackageInterface $rootPackage * @return array * * @phpstan-param array $packageMap */ protected function filterPackageMap(array $packageMap, RootPackageInterface $rootPackage) { $packages = array(); $include = array(); $replacedBy = array(); foreach ($packageMap as $item) { $package = $item[0]; $name = $package->getName(); $packages[$name] = $package; foreach ($package->getReplaces() as $replace) { $replacedBy[$replace->getTarget()] = $name; } } $add = function (PackageInterface $package) use (&$add, $packages, &$include, $replacedBy) { foreach ($package->getRequires() as $link) { $target = $link->getTarget(); if (isset($replacedBy[$target])) { $target = $replacedBy[$target]; } if (!isset($include[$target])) { $include[$target] = true; if (isset($packages[$target])) { $add($packages[$target]); } } } }; $add($rootPackage); return array_filter( $packageMap, function ($item) use ($include) { $package = $item[0]; foreach ($package->getNames() as $name) { if (isset($include[$name])) { return true; } } return false; } ); } /** * Sorts packages by dependency weight * * Packages of equal weight retain the original order * * @param array $packageMap * @return array */ protected function sortPackageMap(array $packageMap) { $packages = array(); $paths = array(); foreach ($packageMap as $item) { list($package, $path) = $item; $name = $package->getName(); $packages[$name] = $package; $paths[$name] = $path; } $sortedPackages = PackageSorter::sortPackages($packages); $sortedPackageMap = array(); foreach ($sortedPackages as $package) { $name = $package->getName(); $sortedPackageMap[] = array($packages[$name], $paths[$name]); } return $sortedPackageMap; } } /** * @param string $fileIdentifier * @param string $file * @return void */ function composerRequire($fileIdentifier, $file) { if (empty($GLOBALS['__composer_autoload_files'][$fileIdentifier])) { $GLOBALS['__composer_autoload_files'][$fileIdentifier] = true; require $file; } } * Jordi Boggiano * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ /* * This file is copied from the Symfony package. * * (c) Fabien Potencier */ namespace Composer\Autoload; use Composer\Pcre\Preg; use Symfony\Component\Finder\Finder; use Composer\IO\IOInterface; use Composer\Util\Filesystem; /** * ClassMapGenerator * * @author Gyula Sallai * @author Jordi Boggiano */ class ClassMapGenerator { /** * Generate a class map file * * @param \Traversable|array $dirs Directories or a single path to search in * @param string $file The name of the class map file * @return void */ public static function dump($dirs, $file) { $maps = array(); foreach ($dirs as $dir) { $maps = array_merge($maps, static::createMap($dir)); } file_put_contents($file, sprintf('|string|array $path The path to search in or an iterator * @param string $excluded Regex that matches file paths to be excluded from the classmap * @param ?IOInterface $io IO object * @param ?string $namespace Optional namespace prefix to filter by * @param ?string $autoloadType psr-0|psr-4 Optional autoload standard to use mapping rules * @param array $scannedFiles * @return array A class map array * @throws \RuntimeException When the path is neither an existing file nor directory */ public static function createMap($path, $excluded = null, IOInterface $io = null, $namespace = null, $autoloadType = null, &$scannedFiles = array()) { $basePath = $path; if (is_string($path)) { if (is_file($path)) { $path = array(new \SplFileInfo($path)); } elseif (is_dir($path) || strpos($path, '*') !== false) { $path = Finder::create()->files()->followLinks()->name('/\.(php|inc|hh)$/')->in($path); } else { throw new \RuntimeException( 'Could not scan for classes inside "'.$path. '" which does not appear to be a file nor a folder' ); } } elseif (null !== $autoloadType) { throw new \RuntimeException('Path must be a string when specifying an autoload type'); } $map = array(); $filesystem = new Filesystem(); $cwd = realpath(getcwd()); foreach ($path as $file) { $filePath = $file->getPathname(); if (!in_array(pathinfo($filePath, PATHINFO_EXTENSION), array('php', 'inc', 'hh'))) { continue; } if (!$filesystem->isAbsolutePath($filePath)) { $filePath = $cwd . '/' . $filePath; $filePath = $filesystem->normalizePath($filePath); } else { $filePath = Preg::replace('{[\\\\/]{2,}}', '/', $filePath); } $realPath = realpath($filePath); // if a list of scanned files is given, avoid scanning twice the same file to save cycles and avoid generating warnings // in case a PSR-0/4 declaration follows another more specific one, or a classmap declaration, which covered this file already if (isset($scannedFiles[$realPath])) { continue; } // check the realpath of the file against the excluded paths as the path might be a symlink and the excluded path is realpath'd so symlink are resolved if ($excluded && Preg::isMatch($excluded, strtr($realPath, '\\', '/'))) { continue; } // check non-realpath of file for directories symlink in project dir if ($excluded && Preg::isMatch($excluded, strtr($filePath, '\\', '/'))) { continue; } $classes = self::findClasses($filePath); if (null !== $autoloadType) { $classes = self::filterByNamespace($classes, $filePath, $namespace, $autoloadType, $basePath, $io); // if no valid class was found in the file then we do not mark it as scanned as it might still be matched by another rule later if ($classes) { $scannedFiles[$realPath] = true; } } else { // classmap autoload rules always collect all classes so for these we definitely do not want to scan again $scannedFiles[$realPath] = true; } foreach ($classes as $class) { // skip classes not within the given namespace prefix if (null === $autoloadType && null !== $namespace && '' !== $namespace && 0 !== strpos($class, $namespace)) { continue; } if (!isset($map[$class])) { $map[$class] = $filePath; } elseif ($io && $map[$class] !== $filePath && !Preg::isMatch('{/(test|fixture|example|stub)s?/}i', strtr($map[$class].' '.$filePath, '\\', '/'))) { $io->writeError( 'Warning: Ambiguous class resolution, "'.$class.'"'. ' was found in both "'.$map[$class].'" and "'.$filePath.'", the first will be used.' ); } } } return $map; } /** * Remove classes which could not have been loaded by namespace autoloaders * * @param array $classes found classes in given file * @param string $filePath current file * @param string $baseNamespace prefix of given autoload mapping * @param string $namespaceType psr-0|psr-4 * @param string $basePath root directory of given autoload mapping * @param ?IOInterface $io IO object * @return array valid classes */ private static function filterByNamespace($classes, $filePath, $baseNamespace, $namespaceType, $basePath, $io) { $validClasses = array(); $rejectedClasses = array(); $realSubPath = substr($filePath, strlen($basePath) + 1); $realSubPath = substr($realSubPath, 0, strrpos($realSubPath, '.')); foreach ($classes as $class) { // silently skip if ns doesn't have common root if ('' !== $baseNamespace && 0 !== strpos($class, $baseNamespace)) { continue; } // transform class name to file path and validate if ('psr-0' === $namespaceType) { $namespaceLength = strrpos($class, '\\'); if (false !== $namespaceLength) { $namespace = substr($class, 0, $namespaceLength + 1); $className = substr($class, $namespaceLength + 1); $subPath = str_replace('\\', DIRECTORY_SEPARATOR, $namespace) . str_replace('_', DIRECTORY_SEPARATOR, $className); } else { $subPath = str_replace('_', DIRECTORY_SEPARATOR, $class); } } elseif ('psr-4' === $namespaceType) { $subNamespace = ('' !== $baseNamespace) ? substr($class, strlen($baseNamespace)) : $class; $subPath = str_replace('\\', DIRECTORY_SEPARATOR, $subNamespace); } else { throw new \RuntimeException("namespaceType must be psr-0 or psr-4, $namespaceType given"); } if ($subPath === $realSubPath) { $validClasses[] = $class; } else { $rejectedClasses[] = $class; } } // warn only if no valid classes, else silently skip invalid if (empty($validClasses)) { foreach ($rejectedClasses as $class) { if ($io) { $io->writeError("Class $class located in ".Preg::replace('{^'.preg_quote(getcwd()).'}', '.', $filePath, 1)." does not comply with $namespaceType autoloading standard. Skipping."); } } return array(); } return $validClasses; } /** * Extract the classes in the given file * * @param string $path The file to check * @throws \RuntimeException * @return array The found classes */ private static function findClasses($path) { $extraTypes = self::getExtraTypes(); // Use @ here instead of Silencer to actively suppress 'unhelpful' output // @link https://github.com/composer/composer/pull/4886 $contents = @php_strip_whitespace($path); if (!$contents) { if (!file_exists($path)) { $message = 'File at "%s" does not exist, check your classmap definitions'; } elseif (!Filesystem::isReadable($path)) { $message = 'File at "%s" is not readable, check its permissions'; } elseif ('' === trim(file_get_contents($path))) { // The input file was really empty and thus contains no classes return array(); } else { $message = 'File at "%s" could not be parsed as PHP, it may be binary or corrupted'; } $error = error_get_last(); if (isset($error['message'])) { $message .= PHP_EOL . 'The following message may be helpful:' . PHP_EOL . $error['message']; } throw new \RuntimeException(sprintf($message, $path)); } // return early if there is no chance of matching anything in this file Preg::matchAll('{\b(?:class|interface'.$extraTypes.')\s}i', $contents, $matches); if (!$matches) { return array(); } $p = new PhpFileCleaner($contents, count($matches[0])); $contents = $p->clean(); unset($p); Preg::matchAll('{ (?: \b(?])(?Pclass|interface'.$extraTypes.') \s++ (?P[a-zA-Z_\x7f-\xff:][a-zA-Z0-9_\x7f-\xff:\-]*+) | \b(?])(?Pnamespace) (?P\s++[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*+(?:\s*+\\\\\s*+[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*+)*+)? \s*+ [\{;] ) }ix', $contents, $matches); $classes = array(); $namespace = ''; for ($i = 0, $len = count($matches['type']); $i < $len; $i++) { if (!empty($matches['ns'][$i])) { $namespace = str_replace(array(' ', "\t", "\r", "\n"), '', $matches['nsname'][$i]) . '\\'; } else { $name = $matches['name'][$i]; // skip anon classes extending/implementing if ($name === 'extends' || $name === 'implements') { continue; } if ($name[0] === ':') { // This is an XHP class, https://github.com/facebook/xhp $name = 'xhp'.substr(str_replace(array('-', ':'), array('_', '__'), $name), 1); } elseif ($matches['type'][$i] === 'enum') { // In Hack, something like: // enum Foo: int { HERP = '123'; } // The regex above captures the colon, which isn't part of // the class name. $name = rtrim($name, ':'); } $classes[] = ltrim($namespace . $name, '\\'); } } return $classes; } /** * @return string */ private static function getExtraTypes() { static $extraTypes = null; if (null === $extraTypes) { $extraTypes = PHP_VERSION_ID < 50400 ? '' : '|trait'; if (PHP_VERSION_ID >= 80100 || (defined('HHVM_VERSION') && version_compare(HHVM_VERSION, '3.3', '>='))) { $extraTypes .= '|enum'; } PhpFileCleaner::setTypeConfig(array_merge(array('class', 'interface'), array_filter(explode('|', $extraTypes)))); } return $extraTypes; } } * Jordi Boggiano * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Composer\Config; /** * Configuration Source Interface * * @author Jordi Boggiano * @author Beau Simensen */ interface ConfigSourceInterface { /** * Add a repository * * @param string $name Name * @param mixed[]|false $config Configuration * @param bool $append Whether the repo should be appended (true) or prepended (false) * * @return void */ public function addRepository($name, $config, $append = true); /** * Remove a repository * * @param string $name * * @return void */ public function removeRepository($name); /** * Add a config setting * * @param string $name Name * @param mixed $value Value * * @return void */ public function addConfigSetting($name, $value); /** * Remove a config setting * * @param string $name * * @return void */ public function removeConfigSetting($name); /** * Add a property * * @param string $name Name * @param string $value Value * * @return void */ public function addProperty($name, $value); /** * Remove a property * * @param string $name * * @return void */ public function removeProperty($name); /** * Add a package link * * @param string $type Type (require, require-dev, provide, suggest, replace, conflict) * @param string $name Name * @param string $value Value * * @return void */ public function addLink($type, $name, $value); /** * Remove a package link * * @param string $type Type (require, require-dev, provide, suggest, replace, conflict) * @param string $name Name * * @return void */ public function removeLink($type, $name); /** * Gives a user-friendly name to this source (file path or so) * * @return string */ public function getName(); } * Jordi Boggiano * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Composer\Config; use Composer\Json\JsonFile; use Composer\Json\JsonManipulator; use Composer\Json\JsonValidationException; use Composer\Pcre\Preg; use Composer\Util\Filesystem; use Composer\Util\Silencer; /** * JSON Configuration Source * * @author Jordi Boggiano * @author Beau Simensen */ class JsonConfigSource implements ConfigSourceInterface { /** * @var JsonFile */ private $file; /** * @var bool */ private $authConfig; /** * Constructor * * @param JsonFile $file * @param bool $authConfig */ public function __construct(JsonFile $file, $authConfig = false) { $this->file = $file; $this->authConfig = $authConfig; } /** * @inheritDoc */ public function getName() { return $this->file->getPath(); } /** * @inheritDoc */ public function addRepository($name, $config, $append = true) { $this->manipulateJson('addRepository', $name, $config, $append, function (&$config, $repo, $repoConfig) use ($append) { // if converting from an array format to hashmap format, and there is a {"packagist.org":false} repo, we have // to convert it to "packagist.org": false key on the hashmap otherwise it fails schema validation if (isset($config['repositories'])) { foreach ($config['repositories'] as $index => $val) { if ($index === $repo) { continue; } if (is_numeric($index) && ($val === array('packagist' => false) || $val === array('packagist.org' => false))) { unset($config['repositories'][$index]); $config['repositories']['packagist.org'] = false; break; } } } if ($append) { $config['repositories'][$repo] = $repoConfig; } else { $config['repositories'] = array($repo => $repoConfig) + $config['repositories']; } }); } /** * @inheritDoc */ public function removeRepository($name) { $this->manipulateJson('removeRepository', $name, function (&$config, $repo) { unset($config['repositories'][$repo]); }); } /** * @inheritDoc */ public function addConfigSetting($name, $value) { $authConfig = $this->authConfig; $this->manipulateJson('addConfigSetting', $name, $value, function (&$config, $key, $val) use ($authConfig) { if (Preg::isMatch('{^(bitbucket-oauth|github-oauth|gitlab-oauth|gitlab-token|bearer|http-basic|platform)\.}', $key)) { list($key, $host) = explode('.', $key, 2); if ($authConfig) { $config[$key][$host] = $val; } else { $config['config'][$key][$host] = $val; } } else { $config['config'][$key] = $val; } }); } /** * @inheritDoc */ public function removeConfigSetting($name) { $authConfig = $this->authConfig; $this->manipulateJson('removeConfigSetting', $name, function (&$config, $key) use ($authConfig) { if (Preg::isMatch('{^(bitbucket-oauth|github-oauth|gitlab-oauth|gitlab-token|bearer|http-basic|platform)\.}', $key)) { list($key, $host) = explode('.', $key, 2); if ($authConfig) { unset($config[$key][$host]); } else { unset($config['config'][$key][$host]); } } else { unset($config['config'][$key]); } }); } /** * @inheritDoc */ public function addProperty($name, $value) { $this->manipulateJson('addProperty', $name, $value, function (&$config, $key, $val) { if (strpos($key, 'extra.') === 0 || strpos($key, 'scripts.') === 0) { $bits = explode('.', $key); $last = array_pop($bits); $arr = &$config[reset($bits)]; foreach ($bits as $bit) { if (!isset($arr[$bit])) { $arr[$bit] = array(); } $arr = &$arr[$bit]; } $arr[$last] = $val; } else { $config[$key] = $val; } }); } /** * @inheritDoc */ public function removeProperty($name) { $this->manipulateJson('removeProperty', $name, function (&$config, $key) { if (strpos($key, 'extra.') === 0 || strpos($key, 'scripts.') === 0) { $bits = explode('.', $key); $last = array_pop($bits); $arr = &$config[reset($bits)]; foreach ($bits as $bit) { if (!isset($arr[$bit])) { return; } $arr = &$arr[$bit]; } unset($arr[$last]); } else { unset($config[$key]); } }); } /** * @inheritDoc */ public function addLink($type, $name, $value) { $this->manipulateJson('addLink', $type, $name, $value, function (&$config, $type, $name, $value) { $config[$type][$name] = $value; }); } /** * @inheritDoc */ public function removeLink($type, $name) { $this->manipulateJson('removeSubNode', $type, $name, function (&$config, $type, $name) { unset($config[$type][$name]); }); $this->manipulateJson('removeMainKeyIfEmpty', $type, function (&$config, $type) { if (0 === count($config[$type])) { unset($config[$type]); } }); } /** * @param string $method * @param mixed ...$args * @param callable $fallback * * @return void */ protected function manipulateJson($method, $args, $fallback) { $args = func_get_args(); // remove method & fallback array_shift($args); $fallback = array_pop($args); if ($this->file->exists()) { if (!is_writable($this->file->getPath())) { throw new \RuntimeException(sprintf('The file "%s" is not writable.', $this->file->getPath())); } if (!Filesystem::isReadable($this->file->getPath())) { throw new \RuntimeException(sprintf('The file "%s" is not readable.', $this->file->getPath())); } $contents = file_get_contents($this->file->getPath()); } elseif ($this->authConfig) { $contents = "{\n}\n"; } else { $contents = "{\n \"config\": {\n }\n}\n"; } $manipulator = new JsonManipulator($contents); $newFile = !$this->file->exists(); // override manipulator method for auth config files if ($this->authConfig && $method === 'addConfigSetting') { $method = 'addSubNode'; list($mainNode, $name) = explode('.', $args[0], 2); $args = array($mainNode, $name, $args[1]); } elseif ($this->authConfig && $method === 'removeConfigSetting') { $method = 'removeSubNode'; list($mainNode, $name) = explode('.', $args[0], 2); $args = array($mainNode, $name); } // try to update cleanly if (call_user_func_array(array($manipulator, $method), $args)) { file_put_contents($this->file->getPath(), $manipulator->getContents()); } else { // on failed clean update, call the fallback and rewrite the whole file $config = $this->file->read(); $this->arrayUnshiftRef($args, $config); call_user_func_array($fallback, $args); // avoid ending up with arrays for keys that should be objects foreach (array('require', 'require-dev', 'conflict', 'provide', 'replace', 'suggest', 'config', 'autoload', 'autoload-dev', 'scripts', 'scripts-descriptions', 'support') as $prop) { if (isset($config[$prop]) && $config[$prop] === array()) { $config[$prop] = new \stdClass; } } foreach (array('psr-0', 'psr-4') as $prop) { if (isset($config['autoload'][$prop]) && $config['autoload'][$prop] === array()) { $config['autoload'][$prop] = new \stdClass; } if (isset($config['autoload-dev'][$prop]) && $config['autoload-dev'][$prop] === array()) { $config['autoload-dev'][$prop] = new \stdClass; } } foreach (array('platform', 'http-basic', 'bearer', 'gitlab-token', 'gitlab-oauth', 'github-oauth', 'preferred-install') as $prop) { if (isset($config['config'][$prop]) && $config['config'][$prop] === array()) { $config['config'][$prop] = new \stdClass; } } $this->file->write($config); } try { $this->file->validateSchema(JsonFile::LAX_SCHEMA); } catch (JsonValidationException $e) { // restore contents to the original state file_put_contents($this->file->getPath(), $contents); throw new \RuntimeException('Failed to update composer.json with a valid format, reverting to the original content. Please report an issue to us with details (command you run and a copy of your composer.json). '.PHP_EOL.implode(PHP_EOL, $e->getErrors()), 0, $e); } if ($newFile) { Silencer::call('chmod', $this->file->getPath(), 0600); } } /** * Prepend a reference to an element to the beginning of an array. * * @param mixed[] $array * @param mixed $value * @return int */ private function arrayUnshiftRef(&$array, &$value) { $return = array_unshift($array, ''); $array[0] = &$value; return $return; } } * Jordi Boggiano * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Composer\Downloader; use Composer\Package\Archiver\ArchivableFilesFinder; use Composer\Package\Dumper\ArrayDumper; use Composer\Package\PackageInterface; use Composer\Package\Version\VersionGuesser; use Composer\Package\Version\VersionParser; use Composer\Util\Platform; use Composer\Util\Filesystem; use Symfony\Component\Filesystem\Exception\IOException; use Symfony\Component\Filesystem\Filesystem as SymfonyFilesystem; use Composer\DependencyResolver\Operation\InstallOperation; use Composer\DependencyResolver\Operation\UninstallOperation; /** * Download a package from a local path. * * @author Samuel Roze * @author Johann Reinke */ class PathDownloader extends FileDownloader implements VcsCapableDownloaderInterface { const STRATEGY_SYMLINK = 10; const STRATEGY_MIRROR = 20; /** * @inheritDoc */ public function download(PackageInterface $package, $path, PackageInterface $prevPackage = null, $output = true) { $path = Filesystem::trimTrailingSlash($path); $url = $package->getDistUrl(); $realUrl = realpath($url); if (false === $realUrl || !file_exists($realUrl) || !is_dir($realUrl)) { throw new \RuntimeException(sprintf( 'Source path "%s" is not found for package %s', $url, $package->getName() )); } if (realpath($path) === $realUrl) { return \React\Promise\resolve(); } if (strpos(realpath($path) . DIRECTORY_SEPARATOR, $realUrl . DIRECTORY_SEPARATOR) === 0) { // IMPORTANT NOTICE: If you wish to change this, don't. You are wasting your time and ours. // // Please see https://github.com/composer/composer/pull/5974 and https://github.com/composer/composer/pull/6174 // for previous attempts that were shut down because they did not work well enough or introduced too many risks. throw new \RuntimeException(sprintf( 'Package %s cannot install to "%s" inside its source at "%s"', $package->getName(), realpath($path), $realUrl )); } return \React\Promise\resolve(); } /** * @inheritDoc */ public function install(PackageInterface $package, $path, $output = true) { $path = Filesystem::trimTrailingSlash($path); $url = $package->getDistUrl(); $realUrl = realpath($url); if (realpath($path) === $realUrl) { if ($output) { $this->io->writeError(" - " . InstallOperation::format($package) . $this->getInstallOperationAppendix($package, $path)); } return \React\Promise\resolve(); } // Get the transport options with default values $transportOptions = $package->getTransportOptions() + array('relative' => true); list($currentStrategy, $allowedStrategies) = $this->computeAllowedStrategies($transportOptions); $symfonyFilesystem = new SymfonyFilesystem(); $this->filesystem->removeDirectory($path); if ($output) { $this->io->writeError(" - " . InstallOperation::format($package).': ', false); } $isFallback = false; if (self::STRATEGY_SYMLINK === $currentStrategy) { try { if (Platform::isWindows()) { // Implement symlinks as NTFS junctions on Windows if ($output) { $this->io->writeError(sprintf('Junctioning from %s', $url), false); } $this->filesystem->junction($realUrl, $path); } else { $absolutePath = $path; if (!$this->filesystem->isAbsolutePath($absolutePath)) { $absolutePath = getcwd() . DIRECTORY_SEPARATOR . $path; } $shortestPath = $this->filesystem->findShortestPath($absolutePath, $realUrl); $path = rtrim($path, "/"); if ($output) { $this->io->writeError(sprintf('Symlinking from %s', $url), false); } if ($transportOptions['relative']) { $symfonyFilesystem->symlink($shortestPath, $path); } else { $symfonyFilesystem->symlink($realUrl, $path); } } } catch (IOException $e) { if (in_array(self::STRATEGY_MIRROR, $allowedStrategies)) { if ($output) { $this->io->writeError(''); $this->io->writeError(' Symlink failed, fallback to use mirroring!'); } $currentStrategy = self::STRATEGY_MIRROR; $isFallback = true; } else { throw new \RuntimeException(sprintf('Symlink from "%s" to "%s" failed!', $realUrl, $path)); } } } // Fallback if symlink failed or if symlink is not allowed for the package if (self::STRATEGY_MIRROR === $currentStrategy) { $realUrl = $this->filesystem->normalizePath($realUrl); if ($output) { $this->io->writeError(sprintf('%sMirroring from %s', $isFallback ? ' ' : '', $url), false); } $iterator = new ArchivableFilesFinder($realUrl, array()); $symfonyFilesystem->mirror($realUrl, $path, $iterator); } if ($output) { $this->io->writeError(''); } return \React\Promise\resolve(); } /** * @inheritDoc */ public function remove(PackageInterface $package, $path, $output = true) { $path = Filesystem::trimTrailingSlash($path); /** * realpath() may resolve Windows junctions to the source path, so we'll check for a junction first * to prevent a false positive when checking if the dist and install paths are the same. * See https://bugs.php.net/bug.php?id=77639 * * For junctions don't blindly rely on Filesystem::removeDirectory as it may be overzealous. If a process * inadvertently locks the file the removal will fail, but it would fall back to recursive delete which * is disastrous within a junction. So in that case we have no other real choice but to fail hard. */ if (Platform::isWindows() && $this->filesystem->isJunction($path)) { if ($output) { $this->io->writeError(" - " . UninstallOperation::format($package).", source is still present in $path"); } if (!$this->filesystem->removeJunction($path)) { $this->io->writeError(" Could not remove junction at " . $path . " - is another process locking it?"); throw new \RuntimeException('Could not reliably remove junction for package ' . $package->getName()); } return \React\Promise\resolve(); } // ensure that the source path (dist url) is not the same as the install path, which // can happen when using custom installers, see https://github.com/composer/composer/pull/9116 // not using realpath here as we do not want to resolve the symlink to the original dist url // it points to $fs = new Filesystem; $absPath = $fs->isAbsolutePath($path) ? $path : getcwd() . '/' . $path; $absDistUrl = $fs->isAbsolutePath($package->getDistUrl()) ? $package->getDistUrl() : getcwd() . '/' . $package->getDistUrl(); if ($fs->normalizePath($absPath) === $fs->normalizePath($absDistUrl)) { if ($output) { $this->io->writeError(" - " . UninstallOperation::format($package).", source is still present in $path"); } return \React\Promise\resolve(); } return parent::remove($package, $path, $output); } /** * @inheritDoc */ public function getVcsReference(PackageInterface $package, $path) { $path = Filesystem::trimTrailingSlash($path); $parser = new VersionParser; $guesser = new VersionGuesser($this->config, $this->process, $parser); $dumper = new ArrayDumper; $packageConfig = $dumper->dump($package); if ($packageVersion = $guesser->guessVersion($packageConfig, $path)) { return $packageVersion['commit']; } return null; } /** * @inheritDoc */ protected function getInstallOperationAppendix(PackageInterface $package, $path) { $realUrl = realpath($package->getDistUrl()); if (realpath($path) === $realUrl) { return ': Source already present'; } list($currentStrategy) = $this->computeAllowedStrategies($package->getTransportOptions()); if ($currentStrategy === self::STRATEGY_SYMLINK) { if (Platform::isWindows()) { return ': Junctioning from '.$package->getDistUrl(); } return ': Symlinking from '.$package->getDistUrl(); } return ': Mirroring from '.$package->getDistUrl(); } /** * @param mixed[] $transportOptions * * @phpstan-return array{self::STRATEGY_*, non-empty-list} */ private function computeAllowedStrategies(array $transportOptions) { // When symlink transport option is null, both symlink and mirror are allowed $currentStrategy = self::STRATEGY_SYMLINK; $allowedStrategies = array(self::STRATEGY_SYMLINK, self::STRATEGY_MIRROR); $mirrorPathRepos = Platform::getEnv('COMPOSER_MIRROR_PATH_REPOS'); if ($mirrorPathRepos) { $currentStrategy = self::STRATEGY_MIRROR; } $symlinkOption = isset($transportOptions['symlink']) ? $transportOptions['symlink'] : null; if (true === $symlinkOption) { $currentStrategy = self::STRATEGY_SYMLINK; $allowedStrategies = array(self::STRATEGY_SYMLINK); } elseif (false === $symlinkOption) { $currentStrategy = self::STRATEGY_MIRROR; $allowedStrategies = array(self::STRATEGY_MIRROR); } // Check we can use junctions safely if we are on Windows if (Platform::isWindows() && self::STRATEGY_SYMLINK === $currentStrategy && !$this->safeJunctions()) { $currentStrategy = self::STRATEGY_MIRROR; $allowedStrategies = array(self::STRATEGY_MIRROR); } return array($currentStrategy, $allowedStrategies); } /** * Returns true if junctions can be created and safely used on Windows * * A PHP bug makes junction detection fragile, leading to possible data loss * when removing a package. See https://bugs.php.net/bug.php?id=77552 * * For safety we require a minimum version of Windows 7, so we can call the * system rmdir which will preserve target content if given a junction. * * The PHP bug was fixed in 7.2.16 and 7.3.3 (requires at least Windows 7). * * @return bool */ private function safeJunctions() { // We need to call mklink, and rmdir on Windows 7 (version 6.1) return function_exists('proc_open') && (PHP_WINDOWS_VERSION_MAJOR > 6 || (PHP_WINDOWS_VERSION_MAJOR === 6 && PHP_WINDOWS_VERSION_MINOR >= 1)); } } * Jordi Boggiano * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Composer\Downloader; use Composer\Package\PackageInterface; use Composer\Pcre\Preg; use Composer\Util\ProcessExecutor; /** * @author BohwaZ */ class FossilDownloader extends VcsDownloader { /** * @inheritDoc */ protected function doDownload(PackageInterface $package, $path, $url, PackageInterface $prevPackage = null) { return \React\Promise\resolve(); } /** * @inheritDoc */ protected function doInstall(PackageInterface $package, $path, $url) { // Ensure we are allowed to use this URL by config $this->config->prohibitUrlByConfig($url, $this->io); $url = ProcessExecutor::escape($url); $ref = ProcessExecutor::escape($package->getSourceReference()); $repoFile = $path . '.fossil'; $this->io->writeError("Cloning ".$package->getSourceReference()); $command = sprintf('fossil clone -- %s %s', $url, ProcessExecutor::escape($repoFile)); if (0 !== $this->process->execute($command, $ignoredOutput)) { throw new \RuntimeException('Failed to execute ' . $command . "\n\n" . $this->process->getErrorOutput()); } $command = sprintf('fossil open --nested -- %s', ProcessExecutor::escape($repoFile)); if (0 !== $this->process->execute($command, $ignoredOutput, realpath($path))) { throw new \RuntimeException('Failed to execute ' . $command . "\n\n" . $this->process->getErrorOutput()); } $command = sprintf('fossil update -- %s', $ref); if (0 !== $this->process->execute($command, $ignoredOutput, realpath($path))) { throw new \RuntimeException('Failed to execute ' . $command . "\n\n" . $this->process->getErrorOutput()); } return \React\Promise\resolve(); } /** * @inheritDoc */ protected function doUpdate(PackageInterface $initial, PackageInterface $target, $path, $url) { // Ensure we are allowed to use this URL by config $this->config->prohibitUrlByConfig($url, $this->io); $ref = ProcessExecutor::escape($target->getSourceReference()); $this->io->writeError(" Updating to ".$target->getSourceReference()); if (!$this->hasMetadataRepository($path)) { throw new \RuntimeException('The .fslckout file is missing from '.$path.', see https://getcomposer.org/commit-deps for more information'); } $command = sprintf('fossil pull && fossil up %s', $ref); if (0 !== $this->process->execute($command, $ignoredOutput, realpath($path))) { throw new \RuntimeException('Failed to execute ' . $command . "\n\n" . $this->process->getErrorOutput()); } return \React\Promise\resolve(); } /** * @inheritDoc */ public function getLocalChanges(PackageInterface $package, $path) { if (!$this->hasMetadataRepository($path)) { return null; } $this->process->execute('fossil changes', $output, realpath($path)); return trim($output) ?: null; } /** * @inheritDoc */ protected function getCommitLogs($fromReference, $toReference, $path) { $command = sprintf('fossil timeline -t ci -W 0 -n 0 before %s', ProcessExecutor::escape($toReference)); if (0 !== $this->process->execute($command, $output, realpath($path))) { throw new \RuntimeException('Failed to execute ' . $command . "\n\n" . $this->process->getErrorOutput()); } $log = ''; $match = '/\d\d:\d\d:\d\d\s+\[' . $toReference . '\]/'; foreach ($this->process->splitLines($output) as $line) { if (Preg::isMatch($match, $line)) { break; } $log .= $line; } return $log; } /** * @inheritDoc */ protected function hasMetadataRepository($path) { return is_file($path . '/.fslckout') || is_file($path . '/_FOSSIL_'); } } * Jordi Boggiano * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Composer\Downloader; use Composer\Package\PackageInterface; use Composer\Util\ProcessExecutor; use Composer\Util\Hg as HgUtils; /** * @author Per Bernhardt */ class HgDownloader extends VcsDownloader { /** * @inheritDoc */ protected function doDownload(PackageInterface $package, $path, $url, PackageInterface $prevPackage = null) { if (null === HgUtils::getVersion($this->process)) { throw new \RuntimeException('hg was not found in your PATH, skipping source download'); } return \React\Promise\resolve(); } /** * @inheritDoc */ protected function doInstall(PackageInterface $package, $path, $url) { $hgUtils = new HgUtils($this->io, $this->config, $this->process); $cloneCommand = function ($url) use ($path) { return sprintf('hg clone -- %s %s', ProcessExecutor::escape($url), ProcessExecutor::escape($path)); }; $hgUtils->runCommand($cloneCommand, $url, $path); $ref = ProcessExecutor::escape($package->getSourceReference()); $command = sprintf('hg up -- %s', $ref); if (0 !== $this->process->execute($command, $ignoredOutput, realpath($path))) { throw new \RuntimeException('Failed to execute ' . $command . "\n\n" . $this->process->getErrorOutput()); } return \React\Promise\resolve(); } /** * @inheritDoc */ protected function doUpdate(PackageInterface $initial, PackageInterface $target, $path, $url) { $hgUtils = new HgUtils($this->io, $this->config, $this->process); $ref = $target->getSourceReference(); $this->io->writeError(" Updating to ".$target->getSourceReference()); if (!$this->hasMetadataRepository($path)) { throw new \RuntimeException('The .hg directory is missing from '.$path.', see https://getcomposer.org/commit-deps for more information'); } $command = function ($url) use ($ref) { return sprintf('hg pull -- %s && hg up -- %s', ProcessExecutor::escape($url), ProcessExecutor::escape($ref)); }; $hgUtils->runCommand($command, $url, $path); return \React\Promise\resolve(); } /** * @inheritDoc */ public function getLocalChanges(PackageInterface $package, $path) { if (!is_dir($path.'/.hg')) { return null; } $this->process->execute('hg st', $output, realpath($path)); return trim($output) ?: null; } /** * @inheritDoc */ protected function getCommitLogs($fromReference, $toReference, $path) { $command = sprintf('hg log -r %s:%s --style compact', ProcessExecutor::escape($fromReference), ProcessExecutor::escape($toReference)); if (0 !== $this->process->execute($command, $output, realpath($path))) { throw new \RuntimeException('Failed to execute ' . $command . "\n\n" . $this->process->getErrorOutput()); } return $output; } /** * @inheritDoc */ protected function hasMetadataRepository($path) { return is_dir($path . '/.hg'); } } * Jordi Boggiano * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Composer\Downloader; use Composer\Package\PackageInterface; use Composer\Util\Platform; use Composer\Util\ProcessExecutor; /** * GZip archive downloader. * * @author Pavel Puchkin */ class GzipDownloader extends ArchiveDownloader { protected function extract(PackageInterface $package, $file, $path) { $filename = pathinfo(parse_url($package->getDistUrl(), PHP_URL_PATH), PATHINFO_FILENAME); $targetFilepath = $path . DIRECTORY_SEPARATOR . $filename; // Try to use gunzip on *nix if (!Platform::isWindows()) { $command = 'gzip -cd -- ' . ProcessExecutor::escape($file) . ' > ' . ProcessExecutor::escape($targetFilepath); if (0 === $this->process->execute($command, $ignoredOutput)) { return \React\Promise\resolve(); } if (extension_loaded('zlib')) { // Fallback to using the PHP extension. $this->extractUsingExt($file, $targetFilepath); return \React\Promise\resolve(); } $processError = 'Failed to execute ' . $command . "\n\n" . $this->process->getErrorOutput(); throw new \RuntimeException($processError); } // Windows version of PHP has built-in support of gzip functions $this->extractUsingExt($file, $targetFilepath); return \React\Promise\resolve(); } /** * @param string $file * @param string $targetFilepath * * @return void */ private function extractUsingExt($file, $targetFilepath) { $archiveFile = gzopen($file, 'rb'); $targetFile = fopen($targetFilepath, 'wb'); while ($string = gzread($archiveFile, 4096)) { fwrite($targetFile, $string, Platform::strlen($string)); } gzclose($archiveFile); fclose($targetFile); } } * Jordi Boggiano * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Composer\Downloader; /** * Exception thrown when issues exist on local filesystem * * @author Javier Spagnoletti */ class FilesystemException extends \Exception { /** * @param string $message * @param int $code * @param \Exception|null $previous */ public function __construct($message = '', $code = 0, \Exception $previous = null) { parent::__construct("Filesystem exception: \n".$message, $code, $previous); } } * Jordi Boggiano * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Composer\Downloader; use Composer\Package\PackageInterface; use Symfony\Component\Finder\Finder; use React\Promise\PromiseInterface; use Composer\DependencyResolver\Operation\InstallOperation; /** * Base downloader for archives * * @author Kirill chEbba Chebunin * @author Jordi Boggiano * @author François Pluchino */ abstract class ArchiveDownloader extends FileDownloader { /** * @var array * @protected */ public $cleanupExecuted = array(); /** * @return PromiseInterface|null */ public function prepare($type, PackageInterface $package, $path, PackageInterface $prevPackage = null) { unset($this->cleanupExecuted[$package->getName()]); return parent::prepare($type, $package, $path, $prevPackage); } /** * @return PromiseInterface|null */ public function cleanup($type, PackageInterface $package, $path, PackageInterface $prevPackage = null) { $this->cleanupExecuted[$package->getName()] = true; return parent::cleanup($type, $package, $path, $prevPackage); } /** * @inheritDoc * * @param bool $output * * @return PromiseInterface * * @throws \RuntimeException * @throws \UnexpectedValueException */ public function install(PackageInterface $package, $path, $output = true) { if ($output) { $this->io->writeError(" - " . InstallOperation::format($package) . $this->getInstallOperationAppendix($package, $path)); } $vendorDir = $this->config->get('vendor-dir'); // clean up the target directory, unless it contains the vendor dir, as the vendor dir contains // the archive to be extracted. This is the case when installing with create-project in the current directory // but in that case we ensure the directory is empty already in ProjectInstaller so no need to empty it here. if (false === strpos($this->filesystem->normalizePath($vendorDir), $this->filesystem->normalizePath($path.DIRECTORY_SEPARATOR))) { $this->filesystem->emptyDirectory($path); } do { $temporaryDir = $vendorDir.'/composer/'.substr(md5(uniqid('', true)), 0, 8); } while (is_dir($temporaryDir)); $this->addCleanupPath($package, $temporaryDir); // avoid cleaning up $path if installing in "." for eg create-project as we can not // delete the directory we are currently in on windows if (!is_dir($path) || realpath($path) !== getcwd()) { $this->addCleanupPath($package, $path); } $this->filesystem->ensureDirectoryExists($temporaryDir); $fileName = $this->getFileName($package, $path); $filesystem = $this->filesystem; $self = $this; $cleanup = function () use ($path, $filesystem, $temporaryDir, $package, $self) { // remove cache if the file was corrupted $self->clearLastCacheWrite($package); // clean up $filesystem->removeDirectory($temporaryDir); if (is_dir($path) && realpath($path) !== getcwd()) { $filesystem->removeDirectory($path); } $self->removeCleanupPath($package, $temporaryDir); $self->removeCleanupPath($package, realpath($path)); }; $promise = null; try { $promise = $this->extract($package, $fileName, $temporaryDir); } catch (\Exception $e) { $cleanup(); throw $e; } if (!$promise instanceof PromiseInterface) { $promise = \React\Promise\resolve(); } return $promise->then(function () use ($self, $package, $filesystem, $fileName, $temporaryDir, $path) { $filesystem->unlink($fileName); /** * Returns the folder content, excluding .DS_Store * * @param string $dir Directory * @return \SplFileInfo[] */ $getFolderContent = function ($dir) { $finder = Finder::create() ->ignoreVCS(false) ->ignoreDotFiles(false) ->notName('.DS_Store') ->depth(0) ->in($dir); return iterator_to_array($finder); }; $renameRecursively = null; /** * Renames (and recursively merges if needed) a folder into another one * * For custom installers, where packages may share paths, and given Composer 2's parallelism, we need to make sure * that the source directory gets merged into the target one if the target exists. Otherwise rename() by default would * put the source into the target e.g. src/ => target/src/ (assuming target exists) instead of src/ => target/ * * @param string $from Directory * @param string $to Directory * @return void */ $renameRecursively = function ($from, $to) use ($filesystem, $getFolderContent, $package, &$renameRecursively) { $contentDir = $getFolderContent($from); // move files back out of the temp dir foreach ($contentDir as $file) { $file = (string) $file; if (is_dir($to . '/' . basename($file))) { if (!is_dir($file)) { throw new \RuntimeException('Installing '.$package.' would lead to overwriting the '.$to.'/'.basename($file).' directory with a file from the package, invalid operation.'); } $renameRecursively($file, $to . '/' . basename($file)); } else { $filesystem->rename($file, $to . '/' . basename($file)); } } }; $renameAsOne = false; if (!file_exists($path)) { $renameAsOne = true; } elseif ($filesystem->isDirEmpty($path)) { try { if ($filesystem->removeDirectoryPhp($path)) { $renameAsOne = true; } } catch (\RuntimeException $e) { // ignore error, and simply do not renameAsOne } } $contentDir = $getFolderContent($temporaryDir); $singleDirAtTopLevel = 1 === count($contentDir) && is_dir(reset($contentDir)); if ($renameAsOne) { // if the target $path is clear, we can rename the whole package in one go instead of looping over the contents if ($singleDirAtTopLevel) { $extractedDir = (string) reset($contentDir); } else { $extractedDir = $temporaryDir; } $filesystem->rename($extractedDir, $path); } else { // only one dir in the archive, extract its contents out of it $from = $temporaryDir; if ($singleDirAtTopLevel) { $from = (string) reset($contentDir); } $renameRecursively($from, $path); } $promise = $filesystem->removeDirectoryAsync($temporaryDir); return $promise->then(function () use ($self, $package, $path, $temporaryDir) { $self->removeCleanupPath($package, $temporaryDir); $self->removeCleanupPath($package, $path); }); }, function ($e) use ($cleanup) { $cleanup(); throw $e; }); } /** * @inheritDoc */ protected function getInstallOperationAppendix(PackageInterface $package, $path) { return ': Extracting archive'; } /** * Extract file to directory * * @param string $file Extracted file * @param string $path Directory * * @throws \UnexpectedValueException If can not extract downloaded file to path * @return PromiseInterface|null */ abstract protected function extract(PackageInterface $package, $file, $path); } * Jordi Boggiano * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Composer\Downloader; use Composer\Util\IniHelper; use Composer\Util\Platform; use Composer\Util\ProcessExecutor; use Composer\Package\PackageInterface; use RarArchive; /** * RAR archive downloader. * * Based on previous work by Jordi Boggiano ({@see ZipDownloader}). * * @author Derrick Nelson */ class RarDownloader extends ArchiveDownloader { protected function extract(PackageInterface $package, $file, $path) { $processError = null; // Try to use unrar on *nix if (!Platform::isWindows()) { $command = 'unrar x -- ' . ProcessExecutor::escape($file) . ' ' . ProcessExecutor::escape($path) . ' >/dev/null && chmod -R u+w ' . ProcessExecutor::escape($path); if (0 === $this->process->execute($command, $ignoredOutput)) { return \React\Promise\resolve(); } $processError = 'Failed to execute ' . $command . "\n\n" . $this->process->getErrorOutput(); } if (!class_exists('RarArchive')) { // php.ini path is added to the error message to help users find the correct file $iniMessage = IniHelper::getMessage(); $error = "Could not decompress the archive, enable the PHP rar extension or install unrar.\n" . $iniMessage . "\n" . $processError; if (!Platform::isWindows()) { $error = "Could not decompress the archive, enable the PHP rar extension.\n" . $iniMessage; } throw new \RuntimeException($error); } $rarArchive = RarArchive::open($file); if (false === $rarArchive) { throw new \UnexpectedValueException('Could not open RAR archive: ' . $file); } $entries = $rarArchive->getEntries(); if (false === $entries) { throw new \RuntimeException('Could not retrieve RAR archive entries'); } foreach ($entries as $entry) { if (false === $entry->extract($path)) { throw new \RuntimeException('Could not extract entry'); } } $rarArchive->close(); return \React\Promise\resolve(); } } * Jordi Boggiano * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Composer\Downloader; use Composer\Package\PackageInterface; use Composer\Util\ProcessExecutor; /** * Xz archive downloader. * * @author Pavel Puchkin * @author Pierre Rudloff */ class XzDownloader extends ArchiveDownloader { protected function extract(PackageInterface $package, $file, $path) { $command = 'tar -xJf ' . ProcessExecutor::escape($file) . ' -C ' . ProcessExecutor::escape($path); if (0 === $this->process->execute($command, $ignoredOutput)) { return \React\Promise\resolve(); } $processError = 'Failed to execute ' . $command . "\n\n" . $this->process->getErrorOutput(); throw new \RuntimeException($processError); } } * Jordi Boggiano * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Composer\Downloader; use Composer\Package\PackageInterface; use Composer\Util\IniHelper; use Composer\Util\Platform; use Composer\Util\ProcessExecutor; use Symfony\Component\Process\ExecutableFinder; use React\Promise\PromiseInterface; use ZipArchive; /** * @author Jordi Boggiano */ class ZipDownloader extends ArchiveDownloader { /** @var array */ private static $unzipCommands; /** @var bool */ private static $hasZipArchive; /** @var bool */ private static $isWindows; /** @var ZipArchive|null */ private $zipArchiveObject; // @phpstan-ignore-line helper property that is set via reflection for testing purposes /** * @inheritDoc */ public function download(PackageInterface $package, $path, PackageInterface $prevPackage = null, $output = true) { if (null === self::$unzipCommands) { self::$unzipCommands = array(); $finder = new ExecutableFinder; if (Platform::isWindows() && ($cmd = $finder->find('7z', null, array('C:\Program Files\7-Zip')))) { self::$unzipCommands[] = array('7z', ProcessExecutor::escape($cmd).' x -bb0 -y %s -o%s'); } if ($cmd = $finder->find('unzip')) { self::$unzipCommands[] = array('unzip', ProcessExecutor::escape($cmd).' -qq %s -d %s'); } if (!Platform::isWindows() && ($cmd = $finder->find('7z'))) { // 7z linux/macOS support is only used if unzip is not present self::$unzipCommands[] = array('7z', ProcessExecutor::escape($cmd).' x -bb0 -y %s -o%s'); } if (!Platform::isWindows() && ($cmd = $finder->find('7zz'))) { // 7zz linux/macOS support is only used if unzip is not present self::$unzipCommands[] = array('7zz', ProcessExecutor::escape($cmd).' x -bb0 -y %s -o%s'); } } $procOpenMissing = false; if (!function_exists('proc_open')) { self::$unzipCommands = array(); $procOpenMissing = true; } if (null === self::$hasZipArchive) { self::$hasZipArchive = class_exists('ZipArchive'); } if (!self::$hasZipArchive && !self::$unzipCommands) { // php.ini path is added to the error message to help users find the correct file $iniMessage = IniHelper::getMessage(); if ($procOpenMissing) { $error = "The zip extension is missing and unzip/7z commands cannot be called as proc_open is disabled, skipping.\n" . $iniMessage; } else { $error = "The zip extension and unzip/7z commands are both missing, skipping.\n" . $iniMessage; } throw new \RuntimeException($error); } if (null === self::$isWindows) { self::$isWindows = Platform::isWindows(); if (!self::$isWindows && !self::$unzipCommands) { if ($procOpenMissing) { $this->io->writeError("proc_open is disabled so 'unzip' and '7z' commands cannot be used, zip files are being unpacked using the PHP zip extension."); $this->io->writeError("This may cause invalid reports of corrupted archives. Besides, any UNIX permissions (e.g. executable) defined in the archives will be lost."); $this->io->writeError("Enabling proc_open and installing 'unzip' or '7z' may remediate them."); } else { $this->io->writeError("As there is no 'unzip' nor '7z' command installed zip files are being unpacked using the PHP zip extension."); $this->io->writeError("This may cause invalid reports of corrupted archives. Besides, any UNIX permissions (e.g. executable) defined in the archives will be lost."); $this->io->writeError("Installing 'unzip' or '7z' may remediate them."); } } } return parent::download($package, $path, $prevPackage, $output); } /** * extract $file to $path with "unzip" command * * @param string $file File to extract * @param string $path Path where to extract file * @return PromiseInterface */ private function extractWithSystemUnzip(PackageInterface $package, $file, $path) { // Force Exception throwing if the other alternative extraction method is not available $isLastChance = !self::$hasZipArchive; if (!self::$unzipCommands) { // This was call as the favorite extract way, but is not available // We switch to the alternative return $this->extractWithZipArchive($package, $file, $path); } $commandSpec = reset(self::$unzipCommands); $command = sprintf($commandSpec[1], ProcessExecutor::escape($file), ProcessExecutor::escape($path)); // normalize separators to backslashes to avoid problems with 7-zip on windows // see https://github.com/composer/composer/issues/10058 if (Platform::isWindows()) { $command = sprintf($commandSpec[1], ProcessExecutor::escape(strtr($file, '/', '\\')), ProcessExecutor::escape(strtr($path, '/', '\\'))); } $executable = $commandSpec[0]; $self = $this; $io = $this->io; $tryFallback = function ($processError) use ($isLastChance, $io, $self, $file, $path, $package, $executable) { if ($isLastChance) { throw $processError; } if (!is_file($file)) { $io->writeError(' '.$processError->getMessage().''); $io->writeError(' This most likely is due to a custom installer plugin not handling the returned Promise from the downloader'); $io->writeError(' See https://github.com/composer/installers/commit/5006d0c28730ade233a8f42ec31ac68fb1c5c9bb for an example fix'); } else { $io->writeError(' '.$processError->getMessage().''); $io->writeError(' The archive may contain identical file names with different capitalization (which fails on case insensitive filesystems)'); $io->writeError(' Unzip with '.$executable.' command failed, falling back to ZipArchive class'); } return $self->extractWithZipArchive($package, $file, $path); }; try { $promise = $this->process->executeAsync($command); return $promise->then(function ($process) use ($tryFallback, $command, $package, $file, $self) { if (!$process->isSuccessful()) { if (isset($self->cleanupExecuted[$package->getName()])) { throw new \RuntimeException('Failed to extract '.$package->getName().' as the installation was aborted by another package operation.'); } $output = $process->getErrorOutput(); $output = str_replace(', '.$file.'.zip or '.$file.'.ZIP', '', $output); return $tryFallback(new \RuntimeException('Failed to extract '.$package->getName().': ('.$process->getExitCode().') '.$command."\n\n".$output)); } }); } catch (\Exception $e) { return $tryFallback($e); } catch (\Throwable $e) { return $tryFallback($e); } } /** * extract $file to $path with ZipArchive * * @param string $file File to extract * @param string $path Path where to extract file * @return PromiseInterface * * TODO v3 should make this private once we can drop PHP 5.3 support * @protected */ public function extractWithZipArchive(PackageInterface $package, $file, $path) { $processError = null; $zipArchive = $this->zipArchiveObject ?: new ZipArchive(); try { if (true === ($retval = $zipArchive->open($file))) { $extractResult = $zipArchive->extractTo($path); if (true === $extractResult) { $zipArchive->close(); return \React\Promise\resolve(); } $processError = new \RuntimeException(rtrim("There was an error extracting the ZIP file, it is either corrupted or using an invalid format.\n")); } else { $processError = new \UnexpectedValueException(rtrim($this->getErrorMessage($retval, $file)."\n"), $retval); } } catch (\ErrorException $e) { $processError = new \RuntimeException('The archive may contain identical file names with different capitalization (which fails on case insensitive filesystems): '.$e->getMessage(), 0, $e); } catch (\Exception $e) { $processError = $e; } catch (\Throwable $e) { $processError = $e; } throw $processError; } /** * extract $file to $path * * @param string $file File to extract * @param string $path Path where to extract file * @return PromiseInterface|null * * TODO v3 should make this private once we can drop PHP 5.3 support * @protected */ public function extract(PackageInterface $package, $file, $path) { return $this->extractWithSystemUnzip($package, $file, $path); } /** * Give a meaningful error message to the user. * * @param int $retval * @param string $file * @return string */ protected function getErrorMessage($retval, $file) { switch ($retval) { case ZipArchive::ER_EXISTS: return sprintf("File '%s' already exists.", $file); case ZipArchive::ER_INCONS: return sprintf("Zip archive '%s' is inconsistent.", $file); case ZipArchive::ER_INVAL: return sprintf("Invalid argument (%s)", $file); case ZipArchive::ER_MEMORY: return sprintf("Malloc failure (%s)", $file); case ZipArchive::ER_NOENT: return sprintf("No such zip file: '%s'", $file); case ZipArchive::ER_NOZIP: return sprintf("'%s' is not a zip archive.", $file); case ZipArchive::ER_OPEN: return sprintf("Can't open zip file: %s", $file); case ZipArchive::ER_READ: return sprintf("Zip read error (%s)", $file); case ZipArchive::ER_SEEK: return sprintf("Zip seek error (%s)", $file); default: return sprintf("'%s' is not a valid zip archive, got error code: %s", $file, $retval); } } } * Jordi Boggiano * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Composer\Downloader; use Composer\Package\PackageInterface; use React\Promise\PromiseInterface; /** * Downloader interface. * * @author Konstantin Kudryashov * @author Jordi Boggiano */ interface DownloaderInterface { /** * Returns installation source (either source or dist). * * @return string "source" or "dist" */ public function getInstallationSource(); /** * This should do any network-related tasks to prepare for an upcoming install/update * * @param string $path download path * @return PromiseInterface|null */ public function download(PackageInterface $package, $path, PackageInterface $prevPackage = null); /** * Do anything that needs to be done between all downloads have been completed and the actual operation is executed * * All packages get first downloaded, then all together prepared, then all together installed/updated/uninstalled. Therefore * for error recovery it is important to avoid failing during install/update/uninstall as much as possible, and risky things or * user prompts should happen in the prepare step rather. In case of failure, cleanup() will be called so that changes can * be undone as much as possible. * * @param string $type one of install/update/uninstall * @param PackageInterface $package package instance * @param string $path download path * @param PackageInterface $prevPackage previous package instance in case of an update * @return PromiseInterface|null */ public function prepare($type, PackageInterface $package, $path, PackageInterface $prevPackage = null); /** * Installs specific package into specific folder. * * @param PackageInterface $package package instance * @param string $path download path * @return PromiseInterface|null */ public function install(PackageInterface $package, $path); /** * Updates specific package in specific folder from initial to target version. * * @param PackageInterface $initial initial package * @param PackageInterface $target updated package * @param string $path download path * @return PromiseInterface|null */ public function update(PackageInterface $initial, PackageInterface $target, $path); /** * Removes specific package from specific folder. * * @param PackageInterface $package package instance * @param string $path download path * @return PromiseInterface|null */ public function remove(PackageInterface $package, $path); /** * Do anything to cleanup changes applied in the prepare or install/update/uninstall steps * * Note that cleanup will be called for all packages, either after install/update/uninstall is complete, * or if any package failed any operation. This is to give all installers a change to cleanup things * they did previously, so you need to keep track of changes applied in the installer/downloader themselves. * * @param string $type one of install/update/uninstall * @param PackageInterface $package package instance * @param string $path download path * @param PackageInterface $prevPackage previous package instance in case of an update * @return PromiseInterface|null */ public function cleanup($type, PackageInterface $package, $path, PackageInterface $prevPackage = null); } * Jordi Boggiano * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Composer\Downloader; use Composer\Package\PackageInterface; use Composer\Repository\VcsRepository; use Composer\Util\Perforce; /** * @author Matt Whittom */ class PerforceDownloader extends VcsDownloader { /** @var Perforce|null */ protected $perforce; /** * @inheritDoc */ protected function doDownload(PackageInterface $package, $path, $url, PackageInterface $prevPackage = null) { return \React\Promise\resolve(); } /** * @inheritDoc */ public function doInstall(PackageInterface $package, $path, $url) { $ref = $package->getSourceReference(); $label = $this->getLabelFromSourceReference((string) $ref); $this->io->writeError('Cloning ' . $ref); $this->initPerforce($package, $path, $url); $this->perforce->setStream($ref); $this->perforce->p4Login(); $this->perforce->writeP4ClientSpec(); $this->perforce->connectClient(); $this->perforce->syncCodeBase($label); $this->perforce->cleanupClientSpec(); return \React\Promise\resolve(); } /** * @param string $ref * * @return string|null */ private function getLabelFromSourceReference($ref) { $pos = strpos($ref, '@'); if (false !== $pos) { return substr($ref, $pos + 1); } return null; } /** * @param string $path * @param string $url * * @return void */ public function initPerforce(PackageInterface $package, $path, $url) { if (!empty($this->perforce)) { $this->perforce->initializePath($path); return; } $repository = $package->getRepository(); $repoConfig = null; if ($repository instanceof VcsRepository) { $repoConfig = $this->getRepoConfig($repository); } $this->perforce = Perforce::create($repoConfig, $url, $path, $this->process, $this->io); } /** * @return array */ private function getRepoConfig(VcsRepository $repository) { return $repository->getRepoConfig(); } /** * @inheritDoc */ protected function doUpdate(PackageInterface $initial, PackageInterface $target, $path, $url) { return $this->doInstall($target, $path, $url); } /** * @inheritDoc */ public function getLocalChanges(PackageInterface $package, $path) { $this->io->writeError('Perforce driver does not check for local changes before overriding'); return null; } /** * @inheritDoc */ protected function getCommitLogs($fromReference, $toReference, $path) { return $this->perforce->getCommitLogs($fromReference, $toReference); } /** * @return void */ public function setPerforce(Perforce $perforce) { $this->perforce = $perforce; } /** * @inheritDoc */ protected function hasMetadataRepository($path) { return true; } } * Jordi Boggiano * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Composer\Downloader; use Composer\Config; use Composer\Cache; use Composer\IO\IOInterface; use Composer\IO\NullIO; use Composer\Exception\IrrecoverableDownloadException; use Composer\Package\Comparer\Comparer; use Composer\DependencyResolver\Operation\UpdateOperation; use Composer\DependencyResolver\Operation\InstallOperation; use Composer\DependencyResolver\Operation\UninstallOperation; use Composer\Package\PackageInterface; use Composer\Plugin\PluginEvents; use Composer\Plugin\PostFileDownloadEvent; use Composer\Plugin\PreFileDownloadEvent; use Composer\EventDispatcher\EventDispatcher; use Composer\Util\Filesystem; use Composer\Util\Silencer; use Composer\Util\HttpDownloader; use Composer\Util\Url as UrlUtil; use Composer\Util\ProcessExecutor; use React\Promise\PromiseInterface; /** * Base downloader for files * * @author Kirill chEbba Chebunin * @author Jordi Boggiano * @author François Pluchino * @author Nils Adermann */ class FileDownloader implements DownloaderInterface, ChangeReportInterface { /** @var IOInterface */ protected $io; /** @var Config */ protected $config; /** @var HttpDownloader */ protected $httpDownloader; /** @var Filesystem */ protected $filesystem; /** @var ?Cache */ protected $cache; /** @var ?EventDispatcher */ protected $eventDispatcher; /** @var ProcessExecutor */ protected $process; /** * @var array * @private * @internal */ public static $downloadMetadata = array(); /** * @private this is only public for php 5.3 support in closures * * @var array Map of package name to cache key */ public $lastCacheWrites = array(); /** @var array Map of package name to list of paths */ private $additionalCleanupPaths = array(); /** * Constructor. * * @param IOInterface $io The IO instance * @param Config $config The config * @param HttpDownloader $httpDownloader The remote filesystem * @param EventDispatcher $eventDispatcher The event dispatcher * @param Cache $cache Cache instance * @param Filesystem $filesystem The filesystem */ public function __construct(IOInterface $io, Config $config, HttpDownloader $httpDownloader, EventDispatcher $eventDispatcher = null, Cache $cache = null, Filesystem $filesystem = null, ProcessExecutor $process = null) { $this->io = $io; $this->config = $config; $this->eventDispatcher = $eventDispatcher; $this->httpDownloader = $httpDownloader; $this->cache = $cache; $this->process = $process ?: new ProcessExecutor($io); $this->filesystem = $filesystem ?: new Filesystem($this->process); if ($this->cache && $this->cache->gcIsNecessary()) { $this->io->writeError('Running cache garbage collection', true, IOInterface::VERY_VERBOSE); $this->cache->gc((int) $config->get('cache-files-ttl'), (int) $config->get('cache-files-maxsize')); } } /** * @inheritDoc */ public function getInstallationSource() { return 'dist'; } /** * @inheritDoc * * @param bool $output */ public function download(PackageInterface $package, $path, PackageInterface $prevPackage = null, $output = true) { if (!$package->getDistUrl()) { throw new \InvalidArgumentException('The given package is missing url information'); } $cacheKeyGenerator = function (PackageInterface $package, $key) { $cacheKey = sha1($key); return $package->getName().'/'.$cacheKey.'.'.$package->getDistType(); }; $retries = 3; $distUrls = $package->getDistUrls(); /** @var array $urls */ $urls = array(); foreach ($distUrls as $index => $url) { $processedUrl = $this->processUrl($package, $url); $urls[$index] = array( 'base' => $url, 'processed' => $processedUrl, // we use the complete download url here to avoid conflicting entries // from different packages, which would potentially allow a given package // in a third party repo to pre-populate the cache for the same package in // packagist for example. 'cacheKey' => $cacheKeyGenerator($package, $processedUrl), ); } $fileName = $this->getFileName($package, $path); $this->filesystem->ensureDirectoryExists($path); $this->filesystem->ensureDirectoryExists(dirname($fileName)); $io = $this->io; $cache = $this->cache; $httpDownloader = $this->httpDownloader; $eventDispatcher = $this->eventDispatcher; $filesystem = $this->filesystem; $self = $this; $accept = null; $reject = null; $download = function () use ($io, $output, $httpDownloader, $cache, $cacheKeyGenerator, $eventDispatcher, $package, $fileName, &$urls, &$accept, &$reject, $self) { /** @var array{base: string, processed: string, cacheKey: string} $url */ $url = reset($urls); $index = key($urls); if ($eventDispatcher) { $preFileDownloadEvent = new PreFileDownloadEvent(PluginEvents::PRE_FILE_DOWNLOAD, $httpDownloader, $url['processed'], 'package', $package); $eventDispatcher->dispatch($preFileDownloadEvent->getName(), $preFileDownloadEvent); if ($preFileDownloadEvent->getCustomCacheKey() !== null) { $url['cacheKey'] = $cacheKeyGenerator($package, $preFileDownloadEvent->getCustomCacheKey()); } elseif ($preFileDownloadEvent->getProcessedUrl() !== $url['processed']) { $url['cacheKey'] = $cacheKeyGenerator($package, $preFileDownloadEvent->getProcessedUrl()); } $url['processed'] = $preFileDownloadEvent->getProcessedUrl(); } $urls[$index] = $url; $checksum = $package->getDistSha1Checksum(); $cacheKey = $url['cacheKey']; // use from cache if it is present and has a valid checksum or we have no checksum to check against if ($cache && (!$checksum || $checksum === $cache->sha1($cacheKey)) && $cache->copyTo($cacheKey, $fileName)) { if ($output) { $io->writeError(" - Loading " . $package->getName() . " (" . $package->getFullPrettyVersion() . ") from cache", true, IOInterface::VERY_VERBOSE); } // mark the file as having been written in cache even though it is only read from cache, so that if // the cache is corrupt the archive will be deleted and the next attempt will re-download it // see https://github.com/composer/composer/issues/10028 if (!$cache->isReadOnly()) { $self->lastCacheWrites[$package->getName()] = $cacheKey; } $result = \React\Promise\resolve($fileName); } else { if ($output) { $io->writeError(" - Downloading " . $package->getName() . " (" . $package->getFullPrettyVersion() . ")"); } $result = $httpDownloader->addCopy($url['processed'], $fileName, $package->getTransportOptions()) ->then($accept, $reject); } return $result->then(function ($result) use ($fileName, $checksum, $url, $package, $eventDispatcher) { // in case of retry, the first call's Promise chain finally calls this twice at the end, // once with $result being the returned $fileName from $accept, and then once for every // failed request with a null result, which can be skipped. if (null === $result) { return $fileName; } if (!file_exists($fileName)) { throw new \UnexpectedValueException($url['base'].' could not be saved to '.$fileName.', make sure the' .' directory is writable and you have internet connectivity'); } if ($checksum && hash_file('sha1', $fileName) !== $checksum) { throw new \UnexpectedValueException('The checksum verification of the file failed (downloaded from '.$url['base'].')'); } if ($eventDispatcher) { $postFileDownloadEvent = new PostFileDownloadEvent(PluginEvents::POST_FILE_DOWNLOAD, $fileName, $checksum, $url['processed'], 'package', $package); $eventDispatcher->dispatch($postFileDownloadEvent->getName(), $postFileDownloadEvent); } return $fileName; }); }; $accept = function ($response) use ($cache, $package, $fileName, $self, &$urls) { $url = reset($urls); $cacheKey = $url['cacheKey']; FileDownloader::$downloadMetadata[$package->getName()] = @filesize($fileName) ?: $response->getHeader('Content-Length') ?: '?'; if ($cache && !$cache->isReadOnly()) { $self->lastCacheWrites[$package->getName()] = $cacheKey; $cache->copyFrom($cacheKey, $fileName); } $response->collect(); return $fileName; }; $reject = function ($e) use ($io, &$urls, $download, $fileName, $package, &$retries, $filesystem, $self) { // clean up if (file_exists($fileName)) { $filesystem->unlink($fileName); } $self->clearLastCacheWrite($package); if ($e instanceof IrrecoverableDownloadException) { throw $e; } if ($e instanceof MaxFileSizeExceededException) { throw $e; } if ($e instanceof TransportException) { // if we got an http response with a proper code, then requesting again will probably not help, abort if ((0 !== $e->getCode() && !in_array($e->getCode(), array(500, 502, 503, 504))) || !$retries) { $retries = 0; } } // special error code returned when network is being artificially disabled if ($e instanceof TransportException && $e->getStatusCode() === 499) { $retries = 0; $urls = array(); } if ($retries) { usleep(500000); $retries--; return $download(); } array_shift($urls); if ($urls) { if ($io->isDebug()) { $io->writeError(' Failed downloading '.$package->getName().': ['.get_class($e).'] '.$e->getCode().': '.$e->getMessage()); $io->writeError(' Trying the next URL for '.$package->getName()); } else { $io->writeError(' Failed downloading '.$package->getName().', trying the next URL ('.$e->getCode().': '.$e->getMessage().')'); } $retries = 3; usleep(100000); return $download(); } throw $e; }; return $download(); } /** * @inheritDoc */ public function prepare($type, PackageInterface $package, $path, PackageInterface $prevPackage = null) { return \React\Promise\resolve(); } /** * @inheritDoc */ public function cleanup($type, PackageInterface $package, $path, PackageInterface $prevPackage = null) { $fileName = $this->getFileName($package, $path); if (file_exists($fileName)) { $this->filesystem->unlink($fileName); } $dirsToCleanUp = array( $this->config->get('vendor-dir').'/composer/', $this->config->get('vendor-dir'), $path, ); if (isset($this->additionalCleanupPaths[$package->getName()])) { foreach ($this->additionalCleanupPaths[$package->getName()] as $path) { $this->filesystem->remove($path); } } foreach ($dirsToCleanUp as $dir) { if (is_dir($dir) && $this->filesystem->isDirEmpty($dir) && realpath($dir) !== getcwd()) { $this->filesystem->removeDirectoryPhp($dir); } } return \React\Promise\resolve(); } /** * @inheritDoc * * @param bool $output */ public function install(PackageInterface $package, $path, $output = true) { if ($output) { $this->io->writeError(" - " . InstallOperation::format($package)); } $this->filesystem->emptyDirectory($path); $this->filesystem->ensureDirectoryExists($path); $this->filesystem->rename($this->getFileName($package, $path), $path . '/' . pathinfo(parse_url($package->getDistUrl(), PHP_URL_PATH), PATHINFO_BASENAME)); if ($package->getBinaries()) { // Single files can not have a mode set like files in archives // so we make sure if the file is a binary that it is executable foreach ($package->getBinaries() as $bin) { if (file_exists($path . '/' . $bin) && !is_executable($path . '/' . $bin)) { Silencer::call('chmod', $path . '/' . $bin, 0777 & ~umask()); } } } return \React\Promise\resolve(); } /** * TODO mark private in v3 * @protected This is public due to PHP 5.3 * * @return void */ public function clearLastCacheWrite(PackageInterface $package) { if ($this->cache && isset($this->lastCacheWrites[$package->getName()])) { $this->cache->remove($this->lastCacheWrites[$package->getName()]); unset($this->lastCacheWrites[$package->getName()]); } } /** * TODO mark private in v3 * @protected This is public due to PHP 5.3 * * @param string $path * * @return void */ public function addCleanupPath(PackageInterface $package, $path) { $this->additionalCleanupPaths[$package->getName()][] = $path; } /** * TODO mark private in v3 * @protected This is public due to PHP 5.3 * * @param string $path * * @return void */ public function removeCleanupPath(PackageInterface $package, $path) { if (isset($this->additionalCleanupPaths[$package->getName()])) { $idx = array_search($path, $this->additionalCleanupPaths[$package->getName()]); if (false !== $idx) { unset($this->additionalCleanupPaths[$package->getName()][$idx]); } } } /** * @inheritDoc */ public function update(PackageInterface $initial, PackageInterface $target, $path) { $this->io->writeError(" - " . UpdateOperation::format($initial, $target) . $this->getInstallOperationAppendix($target, $path)); $promise = $this->remove($initial, $path, false); if (!$promise instanceof PromiseInterface) { $promise = \React\Promise\resolve(); } $self = $this; $io = $this->io; return $promise->then(function () use ($self, $target, $path) { $promise = $self->install($target, $path, false); return $promise; }); } /** * @inheritDoc * * @param bool $output */ public function remove(PackageInterface $package, $path, $output = true) { if ($output) { $this->io->writeError(" - " . UninstallOperation::format($package)); } $promise = $this->filesystem->removeDirectoryAsync($path); return $promise->then(function ($result) use ($path) { if (!$result) { throw new \RuntimeException('Could not completely delete '.$path.', aborting.'); } }); } /** * Gets file name for specific package * * @param PackageInterface $package package instance * @param string $path download path * @return string file name */ protected function getFileName(PackageInterface $package, $path) { return rtrim($this->config->get('vendor-dir').'/composer/tmp-'.md5($package.spl_object_hash($package)).'.'.pathinfo(parse_url($package->getDistUrl(), PHP_URL_PATH), PATHINFO_EXTENSION), '.'); } /** * Gets appendix message to add to the "- Upgrading x" string being output on update * * @param PackageInterface $package package instance * @param string $path download path * @return string */ protected function getInstallOperationAppendix(PackageInterface $package, $path) { return ''; } /** * Process the download url * * @param PackageInterface $package package the url is coming from * @param string $url download url * @throws \RuntimeException If any problem with the url * @return string url */ protected function processUrl(PackageInterface $package, $url) { if (!extension_loaded('openssl') && 0 === strpos($url, 'https:')) { throw new \RuntimeException('You must enable the openssl extension to download files via https'); } if ($package->getDistReference()) { $url = UrlUtil::updateDistReference($this->config, $url, $package->getDistReference()); } return $url; } /** * @inheritDoc * @throws \RuntimeException */ public function getLocalChanges(PackageInterface $package, $targetDir) { $prevIO = $this->io; $this->io = new NullIO; $this->io->loadConfiguration($this->config); $e = null; $output = ''; $targetDir = Filesystem::trimTrailingSlash($targetDir); try { if (is_dir($targetDir.'_compare')) { $this->filesystem->removeDirectory($targetDir.'_compare'); } $this->download($package, $targetDir.'_compare', null, false); $this->httpDownloader->wait(); $this->install($package, $targetDir.'_compare', false); $this->process->wait(); $comparer = new Comparer(); $comparer->setSource($targetDir.'_compare'); $comparer->setUpdate($targetDir); $comparer->doCompare(); $output = $comparer->getChanged(true, true); $this->filesystem->removeDirectory($targetDir.'_compare'); } catch (\Exception $e) { } $this->io = $prevIO; if ($e) { throw $e; } return trim($output); } } * Jordi Boggiano * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Composer\Downloader; use Composer\Package\PackageInterface; use Composer\Pcre\Preg; use Composer\Util\Svn as SvnUtil; use Composer\Repository\VcsRepository; use Composer\Util\ProcessExecutor; use React\Promise\PromiseInterface; /** * @author Ben Bieker * @author Till Klampaeckel */ class SvnDownloader extends VcsDownloader { /** @var bool */ protected $cacheCredentials = true; /** * @inheritDoc */ protected function doDownload(PackageInterface $package, $path, $url, PackageInterface $prevPackage = null) { SvnUtil::cleanEnv(); $util = new SvnUtil($url, $this->io, $this->config, $this->process); if (null === $util->binaryVersion()) { throw new \RuntimeException('svn was not found in your PATH, skipping source download'); } return \React\Promise\resolve(); } /** * @inheritDoc */ protected function doInstall(PackageInterface $package, $path, $url) { SvnUtil::cleanEnv(); $ref = $package->getSourceReference(); $repo = $package->getRepository(); if ($repo instanceof VcsRepository) { $repoConfig = $repo->getRepoConfig(); if (array_key_exists('svn-cache-credentials', $repoConfig)) { $this->cacheCredentials = (bool) $repoConfig['svn-cache-credentials']; } } $this->io->writeError(" Checking out ".$package->getSourceReference()); $this->execute($package, $url, "svn co", sprintf("%s/%s", $url, $ref), null, $path); return \React\Promise\resolve(); } /** * @inheritDoc */ protected function doUpdate(PackageInterface $initial, PackageInterface $target, $path, $url) { SvnUtil::cleanEnv(); $ref = $target->getSourceReference(); if (!$this->hasMetadataRepository($path)) { throw new \RuntimeException('The .svn directory is missing from '.$path.', see https://getcomposer.org/commit-deps for more information'); } $util = new SvnUtil($url, $this->io, $this->config, $this->process); $flags = ""; if (version_compare($util->binaryVersion(), '1.7.0', '>=')) { $flags .= ' --ignore-ancestry'; } $this->io->writeError(" Checking out " . $ref); $this->execute($target, $url, "svn switch" . $flags, sprintf("%s/%s", $url, $ref), $path); return \React\Promise\resolve(); } /** * @inheritDoc */ public function getLocalChanges(PackageInterface $package, $path) { if (!$this->hasMetadataRepository($path)) { return null; } $this->process->execute('svn status --ignore-externals', $output, $path); return Preg::isMatch('{^ *[^X ] +}m', $output) ? $output : null; } /** * Execute an SVN command and try to fix up the process with credentials * if necessary. * * @param string $baseUrl Base URL of the repository * @param string $command SVN command to run * @param string $url SVN url * @param string $cwd Working directory * @param string $path Target for a checkout * @throws \RuntimeException * @return string */ protected function execute(PackageInterface $package, $baseUrl, $command, $url, $cwd = null, $path = null) { $util = new SvnUtil($baseUrl, $this->io, $this->config, $this->process); $util->setCacheCredentials($this->cacheCredentials); try { return $util->execute($command, $url, $cwd, $path, $this->io->isVerbose()); } catch (\RuntimeException $e) { throw new \RuntimeException( $package->getPrettyName().' could not be downloaded, '.$e->getMessage() ); } } /** * @inheritDoc */ protected function cleanChanges(PackageInterface $package, $path, $update) { if (!$changes = $this->getLocalChanges($package, $path)) { return \React\Promise\resolve(); } if (!$this->io->isInteractive()) { if (true === $this->config->get('discard-changes')) { return $this->discardChanges($path); } return parent::cleanChanges($package, $path, $update); } $changes = array_map(function ($elem) { return ' '.$elem; }, Preg::split('{\s*\r?\n\s*}', $changes)); $countChanges = count($changes); $this->io->writeError(sprintf(' '.$package->getPrettyName().' has modified file%s:', $countChanges === 1 ? '' : 's')); $this->io->writeError(array_slice($changes, 0, 10)); if ($countChanges > 10) { $remainingChanges = $countChanges - 10; $this->io->writeError( sprintf( ' '.$remainingChanges.' more file%s modified, choose "v" to view the full list', $remainingChanges === 1 ? '' : 's' ) ); } while (true) { switch ($this->io->ask(' Discard changes [y,n,v,?]? ', '?')) { case 'y': $this->discardChanges($path); break 2; case 'n': throw new \RuntimeException('Update aborted'); case 'v': $this->io->writeError($changes); break; case '?': default: $this->io->writeError(array( ' y - discard changes and apply the '.($update ? 'update' : 'uninstall'), ' n - abort the '.($update ? 'update' : 'uninstall').' and let you manually clean things up', ' v - view modified files', ' ? - print help', )); break; } } return \React\Promise\resolve(); } /** * @inheritDoc */ protected function getCommitLogs($fromReference, $toReference, $path) { if (Preg::isMatch('{@(\d+)$}', $fromReference) && Preg::isMatch('{@(\d+)$}', $toReference)) { // retrieve the svn base url from the checkout folder $command = sprintf('svn info --non-interactive --xml -- %s', ProcessExecutor::escape($path)); if (0 !== $this->process->execute($command, $output, $path)) { throw new \RuntimeException( 'Failed to execute ' . $command . "\n\n" . $this->process->getErrorOutput() ); } $urlPattern = '#(.*)#'; if (Preg::isMatch($urlPattern, $output, $matches)) { $baseUrl = $matches[1]; } else { throw new \RuntimeException( 'Unable to determine svn url for path '. $path ); } // strip paths from references and only keep the actual revision $fromRevision = Preg::replace('{.*@(\d+)$}', '$1', $fromReference); $toRevision = Preg::replace('{.*@(\d+)$}', '$1', $toReference); $command = sprintf('svn log -r%s:%s --incremental', ProcessExecutor::escape($fromRevision), ProcessExecutor::escape($toRevision)); $util = new SvnUtil($baseUrl, $this->io, $this->config, $this->process); $util->setCacheCredentials($this->cacheCredentials); try { return $util->executeLocal($command, $path, null, $this->io->isVerbose()); } catch (\RuntimeException $e) { throw new \RuntimeException( 'Failed to execute ' . $command . "\n\n".$e->getMessage() ); } } return "Could not retrieve changes between $fromReference and $toReference due to missing revision information"; } /** * @param string $path * * @return PromiseInterface */ protected function discardChanges($path) { if (0 !== $this->process->execute('svn revert -R .', $output, $path)) { throw new \RuntimeException("Could not reset changes\n\n:".$this->process->getErrorOutput()); } return \React\Promise\resolve(); } /** * @inheritDoc */ protected function hasMetadataRepository($path) { return is_dir($path.'/.svn'); } } * Jordi Boggiano * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Composer\Downloader; use Composer\Package\PackageInterface; /** * DVCS Downloader interface. * * @author James Titcumb */ interface DvcsDownloaderInterface { /** * Checks for unpushed changes to a current branch * * @param PackageInterface $package package directory * @param string $path package directory * @return string|null changes or null */ public function getUnpushedChanges(PackageInterface $package, $path); } * Jordi Boggiano * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Composer\Downloader; use Composer\Package\PackageInterface; use Composer\IO\IOInterface; use Composer\Pcre\Preg; use Composer\Util\Filesystem; use Composer\Exception\IrrecoverableDownloadException; use React\Promise\PromiseInterface; /** * Downloaders manager. * * @author Konstantin Kudryashov */ class DownloadManager { /** @var IOInterface */ private $io; /** @var bool */ private $preferDist = false; /** @var bool */ private $preferSource; /** @var array */ private $packagePreferences = array(); /** @var Filesystem */ private $filesystem; /** @var array */ private $downloaders = array(); /** * Initializes download manager. * * @param IOInterface $io The Input Output Interface * @param bool $preferSource prefer downloading from source * @param Filesystem|null $filesystem custom Filesystem object */ public function __construct(IOInterface $io, $preferSource = false, Filesystem $filesystem = null) { $this->io = $io; $this->preferSource = $preferSource; $this->filesystem = $filesystem ?: new Filesystem(); } /** * Makes downloader prefer source installation over the dist. * * @param bool $preferSource prefer downloading from source * @return DownloadManager */ public function setPreferSource($preferSource) { $this->preferSource = $preferSource; return $this; } /** * Makes downloader prefer dist installation over the source. * * @param bool $preferDist prefer downloading from dist * @return DownloadManager */ public function setPreferDist($preferDist) { $this->preferDist = $preferDist; return $this; } /** * Sets fine tuned preference settings for package level source/dist selection. * * @param array $preferences array of preferences by package patterns * * @return DownloadManager */ public function setPreferences(array $preferences) { $this->packagePreferences = $preferences; return $this; } /** * Sets installer downloader for a specific installation type. * * @param string $type installation type * @param DownloaderInterface $downloader downloader instance * @return DownloadManager */ public function setDownloader($type, DownloaderInterface $downloader) { $type = strtolower($type); $this->downloaders[$type] = $downloader; return $this; } /** * Returns downloader for a specific installation type. * * @param string $type installation type * @throws \InvalidArgumentException if downloader for provided type is not registered * @return DownloaderInterface */ public function getDownloader($type) { $type = strtolower($type); if (!isset($this->downloaders[$type])) { throw new \InvalidArgumentException(sprintf('Unknown downloader type: %s. Available types: %s.', $type, implode(', ', array_keys($this->downloaders)))); } return $this->downloaders[$type]; } /** * Returns downloader for already installed package. * * @param PackageInterface $package package instance * @throws \InvalidArgumentException if package has no installation source specified * @throws \LogicException if specific downloader used to load package with * wrong type * @return DownloaderInterface|null */ public function getDownloaderForPackage(PackageInterface $package) { $installationSource = $package->getInstallationSource(); if ('metapackage' === $package->getType()) { return null; } if ('dist' === $installationSource) { $downloader = $this->getDownloader($package->getDistType()); } elseif ('source' === $installationSource) { $downloader = $this->getDownloader($package->getSourceType()); } else { throw new \InvalidArgumentException( 'Package '.$package.' does not have an installation source set' ); } if ($installationSource !== $downloader->getInstallationSource()) { throw new \LogicException(sprintf( 'Downloader "%s" is a %s type downloader and can not be used to download %s for package %s', get_class($downloader), $downloader->getInstallationSource(), $installationSource, $package )); } return $downloader; } /** * @return string */ public function getDownloaderType(DownloaderInterface $downloader) { return array_search($downloader, $this->downloaders); } /** * Downloads package into target dir. * * @param PackageInterface $package package instance * @param string $targetDir target dir * @param PackageInterface|null $prevPackage previous package instance in case of updates * * @throws \InvalidArgumentException if package have no urls to download from * @throws \RuntimeException * @return PromiseInterface */ public function download(PackageInterface $package, $targetDir, PackageInterface $prevPackage = null) { $targetDir = $this->normalizeTargetDir($targetDir); $this->filesystem->ensureDirectoryExists(dirname($targetDir)); $sources = $this->getAvailableSources($package, $prevPackage); $io = $this->io; $self = $this; $download = function ($retry = false) use (&$sources, $io, $package, $self, $targetDir, &$download, $prevPackage) { $source = array_shift($sources); if ($retry) { $io->writeError(' Now trying to download from ' . $source . ''); } $package->setInstallationSource($source); $downloader = $self->getDownloaderForPackage($package); if (!$downloader) { return \React\Promise\resolve(); } $handleError = function ($e) use ($sources, $source, $package, $io, $download) { if ($e instanceof \RuntimeException && !$e instanceof IrrecoverableDownloadException) { if (!$sources) { throw $e; } $io->writeError( ' Failed to download '. $package->getPrettyName(). ' from ' . $source . ': '. $e->getMessage().'' ); return $download(true); } throw $e; }; try { $result = $downloader->download($package, $targetDir, $prevPackage); } catch (\Exception $e) { return $handleError($e); } if (!$result instanceof PromiseInterface) { return \React\Promise\resolve($result); } $res = $result->then(function ($res) { return $res; }, $handleError); return $res; }; return $download(); } /** * Prepares an operation execution * * @param string $type one of install/update/uninstall * @param PackageInterface $package package instance * @param string $targetDir target dir * @param PackageInterface|null $prevPackage previous package instance in case of updates * * @return PromiseInterface|null */ public function prepare($type, PackageInterface $package, $targetDir, PackageInterface $prevPackage = null) { $targetDir = $this->normalizeTargetDir($targetDir); $downloader = $this->getDownloaderForPackage($package); if ($downloader) { return $downloader->prepare($type, $package, $targetDir, $prevPackage); } return \React\Promise\resolve(); } /** * Installs package into target dir. * * @param PackageInterface $package package instance * @param string $targetDir target dir * * @throws \InvalidArgumentException if package have no urls to download from * @throws \RuntimeException * @return PromiseInterface|null */ public function install(PackageInterface $package, $targetDir) { $targetDir = $this->normalizeTargetDir($targetDir); $downloader = $this->getDownloaderForPackage($package); if ($downloader) { return $downloader->install($package, $targetDir); } return \React\Promise\resolve(); } /** * Updates package from initial to target version. * * @param PackageInterface $initial initial package version * @param PackageInterface $target target package version * @param string $targetDir target dir * * @throws \InvalidArgumentException if initial package is not installed * @return PromiseInterface|null */ public function update(PackageInterface $initial, PackageInterface $target, $targetDir) { $targetDir = $this->normalizeTargetDir($targetDir); $downloader = $this->getDownloaderForPackage($target); $initialDownloader = $this->getDownloaderForPackage($initial); // no downloaders present means update from metapackage to metapackage, nothing to do if (!$initialDownloader && !$downloader) { return \React\Promise\resolve(); } // if we have a downloader present before, but not after, the package became a metapackage and its files should be removed if (!$downloader) { return $initialDownloader->remove($initial, $targetDir); } $initialType = $this->getDownloaderType($initialDownloader); $targetType = $this->getDownloaderType($downloader); if ($initialType === $targetType) { try { return $downloader->update($initial, $target, $targetDir); } catch (\RuntimeException $e) { if (!$this->io->isInteractive()) { throw $e; } $this->io->writeError(' Update failed ('.$e->getMessage().')'); if (!$this->io->askConfirmation(' Would you like to try reinstalling the package instead [yes]? ')) { throw $e; } } } // if downloader type changed, or update failed and user asks for reinstall, // we wipe the dir and do a new install instead of updating it $promise = $initialDownloader->remove($initial, $targetDir); if ($promise) { $self = $this; return $promise->then(function ($res) use ($self, $target, $targetDir) { return $self->install($target, $targetDir); }); } return $this->install($target, $targetDir); } /** * Removes package from target dir. * * @param PackageInterface $package package instance * @param string $targetDir target dir * * @return PromiseInterface|null */ public function remove(PackageInterface $package, $targetDir) { $targetDir = $this->normalizeTargetDir($targetDir); $downloader = $this->getDownloaderForPackage($package); if ($downloader) { return $downloader->remove($package, $targetDir); } return \React\Promise\resolve(); } /** * Cleans up a failed operation * * @param string $type one of install/update/uninstall * @param PackageInterface $package package instance * @param string $targetDir target dir * @param PackageInterface|null $prevPackage previous package instance in case of updates * * @return PromiseInterface|null */ public function cleanup($type, PackageInterface $package, $targetDir, PackageInterface $prevPackage = null) { $targetDir = $this->normalizeTargetDir($targetDir); $downloader = $this->getDownloaderForPackage($package); if ($downloader) { return $downloader->cleanup($type, $package, $targetDir, $prevPackage); } return \React\Promise\resolve(); } /** * Determines the install preference of a package * * @param PackageInterface $package package instance * * @return string */ protected function resolvePackageInstallPreference(PackageInterface $package) { foreach ($this->packagePreferences as $pattern => $preference) { $pattern = '{^'.str_replace('\\*', '.*', preg_quote($pattern)).'$}i'; if (Preg::isMatch($pattern, $package->getName())) { if ('dist' === $preference || (!$package->isDev() && 'auto' === $preference)) { return 'dist'; } return 'source'; } } return $package->isDev() ? 'source' : 'dist'; } /** * @return string[] * @phpstan-return array<'dist'|'source'>&non-empty-array */ private function getAvailableSources(PackageInterface $package, PackageInterface $prevPackage = null) { $sourceType = $package->getSourceType(); $distType = $package->getDistType(); // add source before dist by default $sources = array(); if ($sourceType) { $sources[] = 'source'; } if ($distType) { $sources[] = 'dist'; } if (empty($sources)) { throw new \InvalidArgumentException('Package '.$package.' must have a source or dist specified'); } if ( $prevPackage // if we are updating, we want to keep the same source as the previously installed package (if available in the new one) && in_array($prevPackage->getInstallationSource(), $sources, true) // unless the previous package was stable dist (by default) and the new package is dev, then we allow the new default to take over && !(!$prevPackage->isDev() && $prevPackage->getInstallationSource() === 'dist' && $package->isDev()) ) { $prevSource = $prevPackage->getInstallationSource(); usort($sources, function ($a, $b) use ($prevSource) { return $a === $prevSource ? -1 : 1; }); return $sources; } // reverse sources in case dist is the preferred source for this package if (!$this->preferSource && ($this->preferDist || 'dist' === $this->resolvePackageInstallPreference($package))) { $sources = array_reverse($sources); } return $sources; } /** * Downloaders expect a /path/to/dir without trailing slash * * If any Installer provides a path with a trailing slash, this can cause bugs so make sure we remove them * * @param string $dir * * @return string */ private function normalizeTargetDir($dir) { if ($dir === '\\' || $dir === '/') { return $dir; } return rtrim($dir, '\\/'); } } * Jordi Boggiano * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Composer\Downloader; use Composer\Package\PackageInterface; /** * ChangeReport interface. * * @author Sascha Egerer */ interface ChangeReportInterface { /** * Checks for changes to the local copy * * @param PackageInterface $package package instance * @param string $path package directory * @return string|null changes or null */ public function getLocalChanges(PackageInterface $package, $path); } * Jordi Boggiano * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Composer\Downloader; class MaxFileSizeExceededException extends TransportException { } * Jordi Boggiano * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Composer\Downloader; use Composer\Package\PackageInterface; /** * Downloader for phar files * * @author Kirill chEbba Chebunin */ class PharDownloader extends ArchiveDownloader { /** * @inheritDoc */ protected function extract(PackageInterface $package, $file, $path) { // Can throw an UnexpectedValueException $archive = new \Phar($file); $archive->extractTo($path, null, true); /* TODO: handle openssl signed phars * https://github.com/composer/composer/pull/33#issuecomment-2250768 * https://github.com/koto/phar-util * http://blog.kotowicz.net/2010/08/hardening-php-how-to-securely-include.html */ return \React\Promise\resolve(); } } * Jordi Boggiano * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Composer\Downloader; use Composer\Package\PackageInterface; /** * Downloader for tar files: tar, tar.gz or tar.bz2 * * @author Kirill chEbba Chebunin */ class TarDownloader extends ArchiveDownloader { /** * @inheritDoc */ protected function extract(PackageInterface $package, $file, $path) { // Can throw an UnexpectedValueException $archive = new \PharData($file); $archive->extractTo($path, null, true); return \React\Promise\resolve(); } } * Jordi Boggiano * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Composer\Downloader; use Composer\Config; use Composer\IO\IOInterface; use Composer\Package\PackageInterface; use Composer\Pcre\Preg; use Composer\Util\Filesystem; use Composer\Util\Git as GitUtil; use Composer\Util\Url; use Composer\Util\Platform; use Composer\Util\ProcessExecutor; use Composer\Cache; use React\Promise\PromiseInterface; /** * @author Jordi Boggiano */ class GitDownloader extends VcsDownloader implements DvcsDownloaderInterface { /** * @var bool[] * @phpstan-var array */ private $hasStashedChanges = array(); /** * @var bool[] * @phpstan-var array */ private $hasDiscardedChanges = array(); /** * @var GitUtil */ private $gitUtil; /** * @var array * @phpstan-var array> */ private $cachedPackages = array(); public function __construct(IOInterface $io, Config $config, ProcessExecutor $process = null, Filesystem $fs = null) { parent::__construct($io, $config, $process, $fs); $this->gitUtil = new GitUtil($this->io, $this->config, $this->process, $this->filesystem); } /** * @inheritDoc */ protected function doDownload(PackageInterface $package, $path, $url, PackageInterface $prevPackage = null) { GitUtil::cleanEnv(); $cachePath = $this->config->get('cache-vcs-dir').'/'.Preg::replace('{[^a-z0-9.]}i', '-', $url).'/'; $gitVersion = GitUtil::getVersion($this->process); // --dissociate option is only available since git 2.3.0-rc0 if ($gitVersion && version_compare($gitVersion, '2.3.0-rc0', '>=') && Cache::isUsable($cachePath)) { $this->io->writeError(" - Syncing " . $package->getName() . " (" . $package->getFullPrettyVersion() . ") into cache"); $this->io->writeError(sprintf(' Cloning to cache at %s', ProcessExecutor::escape($cachePath)), true, IOInterface::DEBUG); $ref = $package->getSourceReference(); if ($this->gitUtil->fetchRefOrSyncMirror($url, $cachePath, $ref) && is_dir($cachePath)) { $this->cachedPackages[$package->getId()][$ref] = true; } } elseif (null === $gitVersion) { throw new \RuntimeException('git was not found in your PATH, skipping source download'); } return \React\Promise\resolve(); } /** * @inheritDoc */ protected function doInstall(PackageInterface $package, $path, $url) { GitUtil::cleanEnv(); $path = $this->normalizePath($path); $cachePath = $this->config->get('cache-vcs-dir').'/'.Preg::replace('{[^a-z0-9.]}i', '-', $url).'/'; $ref = $package->getSourceReference(); $flag = Platform::isWindows() ? '/D ' : ''; if (!empty($this->cachedPackages[$package->getId()][$ref])) { $msg = "Cloning ".$this->getShortHash($ref).' from cache'; $cloneFlags = '--dissociate --reference %cachePath% '; $transportOptions = $package->getTransportOptions(); if (isset($transportOptions['git']['single_use_clone']) && $transportOptions['git']['single_use_clone']) { $cloneFlags = ''; } $command = 'git clone --no-checkout %cachePath% %path% ' . $cloneFlags . '&& cd '.$flag.'%path% ' . '&& git remote set-url origin -- %sanitizedUrl% && git remote add composer -- %sanitizedUrl%'; } else { $msg = "Cloning ".$this->getShortHash($ref); $command = 'git clone --no-checkout -- %url% %path% && cd '.$flag.'%path% && git remote add composer -- %url% && git fetch composer && git remote set-url origin -- %sanitizedUrl% && git remote set-url composer -- %sanitizedUrl%'; if (Platform::getEnv('COMPOSER_DISABLE_NETWORK')) { throw new \RuntimeException('The required git reference for '.$package->getName().' is not in cache and network is disabled, aborting'); } } $this->io->writeError($msg); $commandCallable = function ($url) use ($path, $command, $cachePath) { return str_replace( array('%url%', '%path%', '%cachePath%', '%sanitizedUrl%'), array( ProcessExecutor::escape($url), ProcessExecutor::escape($path), ProcessExecutor::escape($cachePath), ProcessExecutor::escape(Preg::replace('{://([^@]+?):(.+?)@}', '://', $url)), ), $command ); }; $this->gitUtil->runCommand($commandCallable, $url, $path, true); $sourceUrl = $package->getSourceUrl(); if ($url !== $sourceUrl && $sourceUrl !== null) { $this->updateOriginUrl($path, $sourceUrl); } else { $this->setPushUrl($path, $url); } if ($newRef = $this->updateToCommit($path, $ref, $package->getPrettyVersion(), $package->getReleaseDate())) { if ($package->getDistReference() === $package->getSourceReference()) { $package->setDistReference($newRef); } $package->setSourceReference($newRef); } return \React\Promise\resolve(); } /** * @inheritDoc */ protected function doUpdate(PackageInterface $initial, PackageInterface $target, $path, $url) { GitUtil::cleanEnv(); $path = $this->normalizePath($path); if (!$this->hasMetadataRepository($path)) { throw new \RuntimeException('The .git directory is missing from '.$path.', see https://getcomposer.org/commit-deps for more information'); } $cachePath = $this->config->get('cache-vcs-dir').'/'.Preg::replace('{[^a-z0-9.]}i', '-', $url).'/'; $ref = $target->getSourceReference(); if (!empty($this->cachedPackages[$target->getId()][$ref])) { $msg = "Checking out ".$this->getShortHash($ref).' from cache'; $command = '(git rev-parse --quiet --verify %ref% || (git remote set-url composer -- %cachePath% && git fetch composer && git fetch --tags composer)) && git remote set-url composer -- %sanitizedUrl%'; } else { $msg = "Checking out ".$this->getShortHash($ref); $command = '(git remote set-url composer -- %url% && git rev-parse --quiet --verify %ref% || (git fetch composer && git fetch --tags composer)) && git remote set-url composer -- %sanitizedUrl%'; if (Platform::getEnv('COMPOSER_DISABLE_NETWORK')) { throw new \RuntimeException('The required git reference for '.$target->getName().' is not in cache and network is disabled, aborting'); } } $this->io->writeError($msg); $commandCallable = function ($url) use ($ref, $command, $cachePath) { return str_replace( array('%url%', '%ref%', '%cachePath%', '%sanitizedUrl%'), array( ProcessExecutor::escape($url), ProcessExecutor::escape($ref.'^{commit}'), ProcessExecutor::escape($cachePath), ProcessExecutor::escape(Preg::replace('{://([^@]+?):(.+?)@}', '://', $url)), ), $command ); }; $this->gitUtil->runCommand($commandCallable, $url, $path); if ($newRef = $this->updateToCommit($path, $ref, $target->getPrettyVersion(), $target->getReleaseDate())) { if ($target->getDistReference() === $target->getSourceReference()) { $target->setDistReference($newRef); } $target->setSourceReference($newRef); } $updateOriginUrl = false; if ( 0 === $this->process->execute('git remote -v', $output, $path) && Preg::isMatch('{^origin\s+(?P\S+)}m', $output, $originMatch) && Preg::isMatch('{^composer\s+(?P\S+)}m', $output, $composerMatch) ) { if ($originMatch['url'] === $composerMatch['url'] && $composerMatch['url'] !== $target->getSourceUrl()) { $updateOriginUrl = true; } } if ($updateOriginUrl && $target->getSourceUrl() !== null) { $this->updateOriginUrl($path, $target->getSourceUrl()); } return \React\Promise\resolve(); } /** * @inheritDoc */ public function getLocalChanges(PackageInterface $package, $path) { GitUtil::cleanEnv(); if (!$this->hasMetadataRepository($path)) { return null; } $command = 'git status --porcelain --untracked-files=no'; if (0 !== $this->process->execute($command, $output, $path)) { throw new \RuntimeException('Failed to execute ' . $command . "\n\n" . $this->process->getErrorOutput()); } return trim($output) ?: null; } /** * @return null|string */ public function getUnpushedChanges(PackageInterface $package, $path) { GitUtil::cleanEnv(); $path = $this->normalizePath($path); if (!$this->hasMetadataRepository($path)) { return null; } $command = 'git show-ref --head -d'; if (0 !== $this->process->execute($command, $output, $path)) { throw new \RuntimeException('Failed to execute ' . $command . "\n\n" . $this->process->getErrorOutput()); } $refs = trim($output); if (!Preg::isMatch('{^([a-f0-9]+) HEAD$}mi', $refs, $match)) { // could not match the HEAD for some reason return null; } $headRef = $match[1]; if (!Preg::isMatchAll('{^'.$headRef.' refs/heads/(.+)$}mi', $refs, $matches)) { // not on a branch, we are either on a not-modified tag or some sort of detached head, so skip this return null; } $candidateBranches = $matches[1]; // use the first match as branch name for now $branch = $candidateBranches[0]; $unpushedChanges = null; $branchNotFoundError = false; // do two passes, as if we find anything we want to fetch and then re-try for ($i = 0; $i <= 1; $i++) { $remoteBranches = array(); // try to find matching branch names in remote repos foreach ($candidateBranches as $candidate) { if (Preg::isMatchAll('{^[a-f0-9]+ refs/remotes/((?:[^/]+)/'.preg_quote($candidate).')$}mi', $refs, $matches)) { foreach ($matches[1] as $match) { $branch = $candidate; $remoteBranches[] = $match; } break; } } // if it doesn't exist, then we assume it is an unpushed branch // this is bad as we have no reference point to do a diff so we just bail listing // the branch as being unpushed if (!$remoteBranches) { $unpushedChanges = 'Branch ' . $branch . ' could not be found on any remote and appears to be unpushed'; $branchNotFoundError = true; } else { // if first iteration found no remote branch but it has now found some, reset $unpushedChanges // so we get the real diff output no matter its length if ($branchNotFoundError) { $unpushedChanges = null; } foreach ($remoteBranches as $remoteBranch) { $command = sprintf('git diff --name-status %s...%s --', $remoteBranch, $branch); if (0 !== $this->process->execute($command, $output, $path)) { throw new \RuntimeException('Failed to execute ' . $command . "\n\n" . $this->process->getErrorOutput()); } $output = trim($output); // keep the shortest diff from all remote branches we compare against if ($unpushedChanges === null || strlen($output) < strlen($unpushedChanges)) { $unpushedChanges = $output; } } } // first pass and we found unpushed changes, fetch from all remotes to make sure we have up to date // remotes and then try again as outdated remotes can sometimes cause false-positives if ($unpushedChanges && $i === 0) { $this->process->execute('git fetch --all', $output, $path); // update list of refs after fetching $command = 'git show-ref --head -d'; if (0 !== $this->process->execute($command, $output, $path)) { throw new \RuntimeException('Failed to execute ' . $command . "\n\n" . $this->process->getErrorOutput()); } $refs = trim($output); } // abort after first pass if we didn't find anything if (!$unpushedChanges) { break; } } return $unpushedChanges; } /** * @inheritDoc */ protected function cleanChanges(PackageInterface $package, $path, $update) { GitUtil::cleanEnv(); $path = $this->normalizePath($path); $unpushed = $this->getUnpushedChanges($package, $path); if ($unpushed && ($this->io->isInteractive() || $this->config->get('discard-changes') !== true)) { throw new \RuntimeException('Source directory ' . $path . ' has unpushed changes on the current branch: '."\n".$unpushed); } if (!$changes = $this->getLocalChanges($package, $path)) { return \React\Promise\resolve(); } if (!$this->io->isInteractive()) { $discardChanges = $this->config->get('discard-changes'); if (true === $discardChanges) { return $this->discardChanges($path); } if ('stash' === $discardChanges) { if (!$update) { return parent::cleanChanges($package, $path, $update); } return $this->stashChanges($path); } return parent::cleanChanges($package, $path, $update); } $changes = array_map(function ($elem) { return ' '.$elem; }, Preg::split('{\s*\r?\n\s*}', $changes)); $this->io->writeError(' '.$package->getPrettyName().' has modified files:'); $this->io->writeError(array_slice($changes, 0, 10)); if (count($changes) > 10) { $this->io->writeError(' ' . (count($changes) - 10) . ' more files modified, choose "v" to view the full list'); } while (true) { switch ($this->io->ask(' Discard changes [y,n,v,d,'.($update ? 's,' : '').'?]? ', '?')) { case 'y': $this->discardChanges($path); break 2; case 's': if (!$update) { goto help; } $this->stashChanges($path); break 2; case 'n': throw new \RuntimeException('Update aborted'); case 'v': $this->io->writeError($changes); break; case 'd': $this->viewDiff($path); break; case '?': default: help : $this->io->writeError(array( ' y - discard changes and apply the '.($update ? 'update' : 'uninstall'), ' n - abort the '.($update ? 'update' : 'uninstall').' and let you manually clean things up', ' v - view modified files', ' d - view local modifications (diff)', )); if ($update) { $this->io->writeError(' s - stash changes and try to reapply them after the update'); } $this->io->writeError(' ? - print help'); break; } } return \React\Promise\resolve(); } /** * @inheritDoc */ protected function reapplyChanges($path) { $path = $this->normalizePath($path); if (!empty($this->hasStashedChanges[$path])) { unset($this->hasStashedChanges[$path]); $this->io->writeError(' Re-applying stashed changes'); if (0 !== $this->process->execute('git stash pop', $output, $path)) { throw new \RuntimeException("Failed to apply stashed changes:\n\n".$this->process->getErrorOutput()); } } unset($this->hasDiscardedChanges[$path]); } /** * Updates the given path to the given commit ref * * @param string $path * @param string $reference * @param string $branch * @param \DateTime $date * @throws \RuntimeException * @return null|string if a string is returned, it is the commit reference that was checked out if the original could not be found */ protected function updateToCommit($path, $reference, $branch, $date) { $force = !empty($this->hasDiscardedChanges[$path]) || !empty($this->hasStashedChanges[$path]) ? '-f ' : ''; // This uses the "--" sequence to separate branch from file parameters. // // Otherwise git tries the branch name as well as file name. // If the non-existent branch is actually the name of a file, the file // is checked out. $template = 'git checkout '.$force.'%s -- && git reset --hard %1$s --'; $branch = Preg::replace('{(?:^dev-|(?:\.x)?-dev$)}i', '', $branch); $branches = null; if (0 === $this->process->execute('git branch -r', $output, $path)) { $branches = $output; } // check whether non-commitish are branches or tags, and fetch branches with the remote name $gitRef = $reference; if (!Preg::isMatch('{^[a-f0-9]{40}$}', $reference) && $branches && Preg::isMatch('{^\s+composer/'.preg_quote($reference).'$}m', $branches) ) { $command = sprintf('git checkout '.$force.'-B %s %s -- && git reset --hard %2$s --', ProcessExecutor::escape($branch), ProcessExecutor::escape('composer/'.$reference)); if (0 === $this->process->execute($command, $output, $path)) { return null; } } // try to checkout branch by name and then reset it so it's on the proper branch name if (Preg::isMatch('{^[a-f0-9]{40}$}', $reference)) { // add 'v' in front of the branch if it was stripped when generating the pretty name if (!Preg::isMatch('{^\s+composer/'.preg_quote($branch).'$}m', $branches) && Preg::isMatch('{^\s+composer/v'.preg_quote($branch).'$}m', $branches)) { $branch = 'v' . $branch; } $command = sprintf('git checkout %s --', ProcessExecutor::escape($branch)); $fallbackCommand = sprintf('git checkout '.$force.'-B %s %s --', ProcessExecutor::escape($branch), ProcessExecutor::escape('composer/'.$branch)); $resetCommand = sprintf('git reset --hard %s --', ProcessExecutor::escape($reference)); if (0 === $this->process->execute("($command || $fallbackCommand) && $resetCommand", $output, $path)) { return null; } } $command = sprintf($template, ProcessExecutor::escape($gitRef)); if (0 === $this->process->execute($command, $output, $path)) { return null; } // reference was not found (prints "fatal: reference is not a tree: $ref") if (false !== strpos($this->process->getErrorOutput(), $reference)) { $this->io->writeError(' '.$reference.' is gone (history was rewritten?)'); } throw new \RuntimeException(Url::sanitize('Failed to execute ' . $command . "\n\n" . $this->process->getErrorOutput())); } /** * @param string $path * @param string $url * * @return void */ protected function updateOriginUrl($path, $url) { $this->process->execute(sprintf('git remote set-url origin -- %s', ProcessExecutor::escape($url)), $output, $path); $this->setPushUrl($path, $url); } /** * @param string $path * @param string $url * * @return void */ protected function setPushUrl($path, $url) { // set push url for github projects if (Preg::isMatch('{^(?:https?|git)://'.GitUtil::getGitHubDomainsRegex($this->config).'/([^/]+)/([^/]+?)(?:\.git)?$}', $url, $match)) { $protocols = $this->config->get('github-protocols'); $pushUrl = 'git@'.$match[1].':'.$match[2].'/'.$match[3].'.git'; if (!in_array('ssh', $protocols, true)) { $pushUrl = 'https://' . $match[1] . '/'.$match[2].'/'.$match[3].'.git'; } $cmd = sprintf('git remote set-url --push origin -- %s', ProcessExecutor::escape($pushUrl)); $this->process->execute($cmd, $ignoredOutput, $path); } } /** * @inheritDoc */ protected function getCommitLogs($fromReference, $toReference, $path) { $path = $this->normalizePath($path); $command = sprintf('git log %s..%s --pretty=format:"%%h - %%an: %%s"'.GitUtil::getNoShowSignatureFlag($this->process), ProcessExecutor::escape($fromReference), ProcessExecutor::escape($toReference)); if (0 !== $this->process->execute($command, $output, $path)) { throw new \RuntimeException('Failed to execute ' . $command . "\n\n" . $this->process->getErrorOutput()); } return $output; } /** * @param string $path * * @return PromiseInterface * * @throws \RuntimeException */ protected function discardChanges($path) { $path = $this->normalizePath($path); if (0 !== $this->process->execute('git clean -df && git reset --hard', $output, $path)) { throw new \RuntimeException("Could not reset changes\n\n:".$output); } $this->hasDiscardedChanges[$path] = true; return \React\Promise\resolve(); } /** * @param string $path * * @return PromiseInterface * * @throws \RuntimeException */ protected function stashChanges($path) { $path = $this->normalizePath($path); if (0 !== $this->process->execute('git stash --include-untracked', $output, $path)) { throw new \RuntimeException("Could not stash changes\n\n:".$output); } $this->hasStashedChanges[$path] = true; return \React\Promise\resolve(); } /** * @param string $path * * @return void * * @throws \RuntimeException */ protected function viewDiff($path) { $path = $this->normalizePath($path); if (0 !== $this->process->execute('git diff HEAD', $output, $path)) { throw new \RuntimeException("Could not view diff\n\n:".$output); } $this->io->writeError($output); } /** * @param string $path * * @return string */ protected function normalizePath($path) { if (Platform::isWindows() && strlen($path) > 0) { $basePath = $path; $removed = array(); while (!is_dir($basePath) && $basePath !== '\\') { array_unshift($removed, basename($basePath)); $basePath = dirname($basePath); } if ($basePath === '\\') { return $path; } $path = rtrim(realpath($basePath) . '/' . implode('/', $removed), '/'); } return $path; } /** * @inheritDoc */ protected function hasMetadataRepository($path) { $path = $this->normalizePath($path); return is_dir($path.'/.git'); } /** * @param string $reference * @return string */ protected function getShortHash($reference) { if (!$this->io->isVerbose() && Preg::isMatch('{^[0-9a-f]{40}$}', $reference)) { return substr($reference, 0, 10); } return $reference; } } * Jordi Boggiano * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Composer\Downloader; /** * @author Jordi Boggiano */ class TransportException extends \RuntimeException { /** @var ?array */ protected $headers; /** @var ?string */ protected $response; /** @var ?int */ protected $statusCode; /** @var array */ protected $responseInfo = array(); /** * @param array $headers * * @return void */ public function setHeaders($headers) { $this->headers = $headers; } /** * @return ?array */ public function getHeaders() { return $this->headers; } /** * @param ?string $response * * @return void */ public function setResponse($response) { $this->response = $response; } /** * @return ?string */ public function getResponse() { return $this->response; } /** * @param ?int $statusCode * * @return void */ public function setStatusCode($statusCode) { $this->statusCode = $statusCode; } /** * @return ?int */ public function getStatusCode() { return $this->statusCode; } /** * @return array */ public function getResponseInfo() { return $this->responseInfo; } /** * @param array $responseInfo * * @return void */ public function setResponseInfo(array $responseInfo) { $this->responseInfo = $responseInfo; } } * Jordi Boggiano * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Composer\Downloader; use Composer\Config; use Composer\Package\Dumper\ArrayDumper; use Composer\Package\PackageInterface; use Composer\Package\Version\VersionGuesser; use Composer\Package\Version\VersionParser; use Composer\Util\ProcessExecutor; use Composer\IO\IOInterface; use Composer\Util\Filesystem; use React\Promise\PromiseInterface; use Composer\DependencyResolver\Operation\UpdateOperation; use Composer\DependencyResolver\Operation\InstallOperation; use Composer\DependencyResolver\Operation\UninstallOperation; /** * @author Jordi Boggiano */ abstract class VcsDownloader implements DownloaderInterface, ChangeReportInterface, VcsCapableDownloaderInterface { /** @var IOInterface */ protected $io; /** @var Config */ protected $config; /** @var ProcessExecutor */ protected $process; /** @var Filesystem */ protected $filesystem; /** @var array */ protected $hasCleanedChanges = array(); public function __construct(IOInterface $io, Config $config, ProcessExecutor $process = null, Filesystem $fs = null) { $this->io = $io; $this->config = $config; $this->process = $process ?: new ProcessExecutor($io); $this->filesystem = $fs ?: new Filesystem($this->process); } /** * @inheritDoc */ public function getInstallationSource() { return 'source'; } /** * @inheritDoc */ public function download(PackageInterface $package, $path, PackageInterface $prevPackage = null) { if (!$package->getSourceReference()) { throw new \InvalidArgumentException('Package '.$package->getPrettyName().' is missing reference information'); } $urls = $this->prepareUrls($package->getSourceUrls()); while ($url = array_shift($urls)) { try { return $this->doDownload($package, $path, $url, $prevPackage); } catch (\Exception $e) { // rethrow phpunit exceptions to avoid hard to debug bug failures if ($e instanceof \PHPUnit\Framework\Exception) { throw $e; } if ($this->io->isDebug()) { $this->io->writeError('Failed: ['.get_class($e).'] '.$e->getMessage()); } elseif (count($urls)) { $this->io->writeError(' Failed, trying the next URL'); } if (!count($urls)) { throw $e; } } } return \React\Promise\resolve(); } /** * @inheritDoc */ public function prepare($type, PackageInterface $package, $path, PackageInterface $prevPackage = null) { if ($type === 'update') { $this->cleanChanges($prevPackage, $path, true); $this->hasCleanedChanges[$prevPackage->getUniqueName()] = true; } elseif ($type === 'install') { $this->filesystem->emptyDirectory($path); } elseif ($type === 'uninstall') { $this->cleanChanges($package, $path, false); } return \React\Promise\resolve(); } /** * @inheritDoc */ public function cleanup($type, PackageInterface $package, $path, PackageInterface $prevPackage = null) { if ($type === 'update' && isset($this->hasCleanedChanges[$prevPackage->getUniqueName()])) { $this->reapplyChanges($path); unset($this->hasCleanedChanges[$prevPackage->getUniqueName()]); } return \React\Promise\resolve(); } /** * @inheritDoc */ public function install(PackageInterface $package, $path) { if (!$package->getSourceReference()) { throw new \InvalidArgumentException('Package '.$package->getPrettyName().' is missing reference information'); } $this->io->writeError(" - " . InstallOperation::format($package).': ', false); $urls = $this->prepareUrls($package->getSourceUrls()); while ($url = array_shift($urls)) { try { $this->doInstall($package, $path, $url); break; } catch (\Exception $e) { // rethrow phpunit exceptions to avoid hard to debug bug failures if ($e instanceof \PHPUnit\Framework\Exception) { throw $e; } if ($this->io->isDebug()) { $this->io->writeError('Failed: ['.get_class($e).'] '.$e->getMessage()); } elseif (count($urls)) { $this->io->writeError(' Failed, trying the next URL'); } if (!count($urls)) { throw $e; } } } return \React\Promise\resolve(); } /** * @inheritDoc */ public function update(PackageInterface $initial, PackageInterface $target, $path) { if (!$target->getSourceReference()) { throw new \InvalidArgumentException('Package '.$target->getPrettyName().' is missing reference information'); } $this->io->writeError(" - " . UpdateOperation::format($initial, $target).': ', false); $urls = $this->prepareUrls($target->getSourceUrls()); $exception = null; while ($url = array_shift($urls)) { try { $this->doUpdate($initial, $target, $path, $url); $exception = null; break; } catch (\Exception $exception) { // rethrow phpunit exceptions to avoid hard to debug bug failures if ($exception instanceof \PHPUnit\Framework\Exception) { throw $exception; } if ($this->io->isDebug()) { $this->io->writeError('Failed: ['.get_class($exception).'] '.$exception->getMessage()); } elseif (count($urls)) { $this->io->writeError(' Failed, trying the next URL'); } } } // print the commit logs if in verbose mode and VCS metadata is present // because in case of missing metadata code would trigger another exception if (!$exception && $this->io->isVerbose() && $this->hasMetadataRepository($path)) { $message = 'Pulling in changes:'; $logs = $this->getCommitLogs($initial->getSourceReference(), $target->getSourceReference(), $path); if (!trim($logs)) { $message = 'Rolling back changes:'; $logs = $this->getCommitLogs($target->getSourceReference(), $initial->getSourceReference(), $path); } if (trim($logs)) { $logs = implode("\n", array_map(function ($line) { return ' ' . $line; }, explode("\n", $logs))); // escape angle brackets for proper output in the console $logs = str_replace('<', '\<', $logs); $this->io->writeError(' '.$message); $this->io->writeError($logs); } } if (!$urls && $exception) { throw $exception; } return \React\Promise\resolve(); } /** * @inheritDoc */ public function remove(PackageInterface $package, $path) { $this->io->writeError(" - " . UninstallOperation::format($package)); $promise = $this->filesystem->removeDirectoryAsync($path); return $promise->then(function ($result) use ($path) { if (!$result) { throw new \RuntimeException('Could not completely delete '.$path.', aborting.'); } }); } /** * @inheritDoc */ public function getVcsReference(PackageInterface $package, $path) { $parser = new VersionParser; $guesser = new VersionGuesser($this->config, $this->process, $parser); $dumper = new ArrayDumper; $packageConfig = $dumper->dump($package); if ($packageVersion = $guesser->guessVersion($packageConfig, $path)) { return $packageVersion['commit']; } return null; } /** * Prompt the user to check if changes should be stashed/removed or the operation aborted * * @param PackageInterface $package * @param string $path * @param bool $update if true (update) the changes can be stashed and reapplied after an update, * if false (remove) the changes should be assumed to be lost if the operation is not aborted * * @return PromiseInterface * * @throws \RuntimeException in case the operation must be aborted */ protected function cleanChanges(PackageInterface $package, $path, $update) { // the default implementation just fails if there are any changes, override in child classes to provide stash-ability if (null !== $this->getLocalChanges($package, $path)) { throw new \RuntimeException('Source directory ' . $path . ' has uncommitted changes.'); } return \React\Promise\resolve(); } /** * Reapply previously stashes changes if applicable, only called after an update (regardless if successful or not) * * @param string $path * * @return void * * @throws \RuntimeException in case the operation must be aborted or the patch does not apply cleanly */ protected function reapplyChanges($path) { } /** * Downloads data needed to run an install/update later * * @param PackageInterface $package package instance * @param string $path download path * @param string $url package url * @param PackageInterface|null $prevPackage previous package (in case of an update) * * @return PromiseInterface|null */ abstract protected function doDownload(PackageInterface $package, $path, $url, PackageInterface $prevPackage = null); /** * Downloads specific package into specific folder. * * @param PackageInterface $package package instance * @param string $path download path * @param string $url package url * * @return PromiseInterface|null */ abstract protected function doInstall(PackageInterface $package, $path, $url); /** * Updates specific package in specific folder from initial to target version. * * @param PackageInterface $initial initial package * @param PackageInterface $target updated package * @param string $path download path * @param string $url package url * * @return PromiseInterface|null */ abstract protected function doUpdate(PackageInterface $initial, PackageInterface $target, $path, $url); /** * Fetches the commit logs between two commits * * @param string $fromReference the source reference * @param string $toReference the target reference * @param string $path the package path * @return string */ abstract protected function getCommitLogs($fromReference, $toReference, $path); /** * Checks if VCS metadata repository has been initialized * repository example: .git|.svn|.hg * * @param string $path * @return bool */ abstract protected function hasMetadataRepository($path); /** * @param string[] $urls * * @return string[] */ private function prepareUrls(array $urls) { foreach ($urls as $index => $url) { if (Filesystem::isLocalPath($url)) { // realpath() below will not understand // url that starts with "file://" $fileProtocol = 'file://'; $isFileProtocol = false; if (0 === strpos($url, $fileProtocol)) { $url = substr($url, strlen($fileProtocol)); $isFileProtocol = true; } // realpath() below will not understand %20 spaces etc. if (false !== strpos($url, '%')) { $url = rawurldecode($url); } $urls[$index] = realpath($url); if ($isFileProtocol) { $urls[$index] = $fileProtocol . $urls[$index]; } } } return $urls; } } * Jordi Boggiano * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Composer\Downloader; use Composer\Package\PackageInterface; /** * VCS Capable Downloader interface. * * @author Steve Buzonas */ interface VcsCapableDownloaderInterface { /** * Gets the VCS Reference for the package at path * * @param PackageInterface $package package directory * @param string $path package directory * @return string|null reference or null */ public function getVcsReference(PackageInterface $package, $path); } * Jordi Boggiano * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Composer\Question; use Composer\Pcre\Preg; use Symfony\Component\Console\Exception\InvalidArgumentException; use Symfony\Component\Console\Question\Question; /** * Represents a yes/no question * Enforces strict responses rather than non-standard answers counting as default * Based on Symfony\Component\Console\Question\ConfirmationQuestion * * @author Theo Tonge */ class StrictConfirmationQuestion extends Question { /** @var string */ private $trueAnswerRegex; /** @var string */ private $falseAnswerRegex; /** * Constructor.s * * @param string $question The question to ask to the user * @param bool $default The default answer to return, true or false * @param string $trueAnswerRegex A regex to match the "yes" answer * @param string $falseAnswerRegex A regex to match the "no" answer */ public function __construct($question, $default = true, $trueAnswerRegex = '/^y(?:es)?$/i', $falseAnswerRegex = '/^no?$/i') { parent::__construct($question, (bool) $default); $this->trueAnswerRegex = $trueAnswerRegex; $this->falseAnswerRegex = $falseAnswerRegex; $this->setNormalizer($this->getDefaultNormalizer()); $this->setValidator($this->getDefaultValidator()); } /** * Returns the default answer normalizer. * * @return callable */ private function getDefaultNormalizer() { $default = $this->getDefault(); $trueRegex = $this->trueAnswerRegex; $falseRegex = $this->falseAnswerRegex; return function ($answer) use ($default, $trueRegex, $falseRegex) { if (is_bool($answer)) { return $answer; } if (empty($answer) && !empty($default)) { return $default; } if (Preg::isMatch($trueRegex, $answer)) { return true; } if (Preg::isMatch($falseRegex, $answer)) { return false; } return null; }; } /** * Returns the default answer validator. * * @return callable */ private function getDefaultValidator() { return function ($answer) { if (!is_bool($answer)) { throw new InvalidArgumentException('Please answer yes, y, no, or n.'); } return $answer; }; } } * Jordi Boggiano * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Composer\Plugin; use Composer\EventDispatcher\Event; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; /** * An event for all commands. * * @author Nils Adermann */ class CommandEvent extends Event { /** * @var string */ private $commandName; /** * @var InputInterface */ private $input; /** * @var OutputInterface */ private $output; /** * Constructor. * * @param string $name The event name * @param string $commandName The command name * @param InputInterface $input * @param OutputInterface $output * @param mixed[] $args Arguments passed by the user * @param mixed[] $flags Optional flags to pass data not as argument */ public function __construct($name, $commandName, $input, $output, array $args = array(), array $flags = array()) { parent::__construct($name, $args, $flags); $this->commandName = $commandName; $this->input = $input; $this->output = $output; } /** * Returns the command input interface * * @return InputInterface */ public function getInput() { return $this->input; } /** * Retrieves the command output interface * * @return OutputInterface */ public function getOutput() { return $this->output; } /** * Retrieves the name of the command being run * * @return string */ public function getCommandName() { return $this->commandName; } } * Jordi Boggiano * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Composer\Plugin; use Composer\EventDispatcher\Event; use Composer\Package\PackageInterface; /** * The post file download event. * * @author Nils Adermann */ class PostFileDownloadEvent extends Event { /** * @var string */ private $fileName; /** * @var string|null */ private $checksum; /** * @var string */ private $url; /** * @var mixed */ private $context; /** * @var string */ private $type; /** * Constructor. * * @param string $name The event name * @param string|null $fileName The file name * @param string|null $checksum The checksum * @param string $url The processed url * @param string $type The type (package or metadata). * @param mixed $context Additional context for the download. */ public function __construct($name, $fileName, $checksum, $url, $type, $context = null) { /** @phpstan-ignore-next-line */ if ($context === null && $type instanceof PackageInterface) { $context = $type; $type = 'package'; trigger_error('PostFileDownloadEvent::__construct should receive a $type=package and the package object in $context since Composer 2.1.', E_USER_DEPRECATED); } parent::__construct($name); $this->fileName = $fileName; $this->checksum = $checksum; $this->url = $url; $this->context = $context; $this->type = $type; } /** * Retrieves the target file name location. * * If this download is of type metadata, null is returned. * * @return string|null */ public function getFileName() { return $this->fileName; } /** * Gets the checksum. * * @return string|null */ public function getChecksum() { return $this->checksum; } /** * Gets the processed URL. * * @return string */ public function getUrl() { return $this->url; } /** * Returns the context of this download, if any. * * If this download is of type package, the package object is returned. If * this download is of type metadata, an array{response: Response, repository: RepositoryInterface} is returned. * * @return mixed */ public function getContext() { return $this->context; } /** * Get the package. * * If this download is of type metadata, null is returned. * * @return \Composer\Package\PackageInterface|null The package. * @deprecated Use getContext instead */ public function getPackage() { trigger_error('PostFileDownloadEvent::getPackage is deprecated since Composer 2.1, use getContext instead.', E_USER_DEPRECATED); $context = $this->getContext(); return $context instanceof PackageInterface ? $context : null; } /** * Returns the type of this download (package, metadata). * * @return string */ public function getType() { return $this->type; } } * Jordi Boggiano * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Composer\Plugin; /** * The Plugin Events. * * @author Nils Adermann */ class PluginEvents { /** * The INIT event occurs after a Composer instance is done being initialized * * The event listener method receives a * Composer\EventDispatcher\Event instance. * * @var string */ const INIT = 'init'; /** * The COMMAND event occurs as a command begins * * The event listener method receives a * Composer\Plugin\CommandEvent instance. * * @var string */ const COMMAND = 'command'; /** * The PRE_FILE_DOWNLOAD event occurs before downloading a file * * The event listener method receives a * Composer\Plugin\PreFileDownloadEvent instance. * * @var string */ const PRE_FILE_DOWNLOAD = 'pre-file-download'; /** * The POST_FILE_DOWNLOAD event occurs after downloading a package dist file * * The event listener method receives a * Composer\Plugin\PostFileDownloadEvent instance. * * @var string */ const POST_FILE_DOWNLOAD = 'post-file-download'; /** * The PRE_COMMAND_RUN event occurs before a command is executed and lets you modify the input arguments/options * * The event listener method receives a * Composer\Plugin\PreCommandRunEvent instance. * * @var string */ const PRE_COMMAND_RUN = 'pre-command-run'; /** * The PRE_POOL_CREATE event occurs before the Pool of packages is created, and lets * you filter the list of packages which is going to enter the Solver * * The event listener method receives a * Composer\Plugin\PrePoolCreateEvent instance. * * @var string */ const PRE_POOL_CREATE = 'pre-pool-create'; } * Jordi Boggiano * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Composer\Plugin; use Composer\EventDispatcher\Event; use Composer\Util\HttpDownloader; /** * The pre file download event. * * @author Nils Adermann */ class PreFileDownloadEvent extends Event { /** * @var HttpDownloader */ private $httpDownloader; /** * @var string */ private $processedUrl; /** * @var string|null */ private $customCacheKey; /** * @var string */ private $type; /** * @var mixed */ private $context; /** * @var mixed[] */ private $transportOptions = array(); /** * Constructor. * * @param string $name The event name * @param HttpDownloader $httpDownloader * @param string $processedUrl * @param string $type * @param mixed $context */ public function __construct($name, HttpDownloader $httpDownloader, $processedUrl, $type, $context = null) { parent::__construct($name); $this->httpDownloader = $httpDownloader; $this->processedUrl = $processedUrl; $this->type = $type; $this->context = $context; } /** * @return HttpDownloader */ public function getHttpDownloader() { return $this->httpDownloader; } /** * Retrieves the processed URL that will be downloaded. * * @return string */ public function getProcessedUrl() { return $this->processedUrl; } /** * Sets the processed URL that will be downloaded. * * @param string $processedUrl New processed URL * * @return void */ public function setProcessedUrl($processedUrl) { $this->processedUrl = $processedUrl; } /** * Retrieves a custom package cache key for this download. * * @return string|null */ public function getCustomCacheKey() { return $this->customCacheKey; } /** * Sets a custom package cache key for this download. * * @param string|null $customCacheKey New cache key * * @return void */ public function setCustomCacheKey($customCacheKey) { $this->customCacheKey = $customCacheKey; } /** * Returns the type of this download (package, metadata). * * @return string */ public function getType() { return $this->type; } /** * Returns the context of this download, if any. * * If this download is of type package, the package object is returned. * If the type is metadata, an array{repository: RepositoryInterface} is returned. * * @return mixed */ public function getContext() { return $this->context; } /** * Returns transport options for the download. * * Only available for events with type metadata, for packages set the transport options on the package itself. * * @return mixed[] */ public function getTransportOptions() { return $this->transportOptions; } /** * Sets transport options for the download. * * Only available for events with type metadata, for packages set the transport options on the package itself. * * @param mixed[] $options * * @return void */ public function setTransportOptions(array $options) { $this->transportOptions = $options; } } * Jordi Boggiano * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Composer\Plugin\Capability; /** * Marker interface for Plugin capabilities. * Every new Capability which is added to the Plugin API must implement this interface. * * @api */ interface Capability { } * Jordi Boggiano * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Composer\Plugin\Capability; /** * Commands Provider Interface * * This capability will receive an array with 'composer' and 'io' keys as * constructor argument. Those contain Composer\Composer and Composer\IO\IOInterface * instances. It also contains a 'plugin' key containing the plugin instance that * created the capability. * * @author Jérémy Derussé */ interface CommandProvider extends Capability { /** * Retrieves an array of commands * * @return \Composer\Command\BaseCommand[] */ public function getCommands(); } * Jordi Boggiano * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Composer\Plugin; use Composer\EventDispatcher\Event; use Composer\Repository\RepositoryInterface; use Composer\DependencyResolver\Request; use Composer\Package\BasePackage; /** * The pre command run event. * * @author Jordi Boggiano */ class PrePoolCreateEvent extends Event { /** * @var RepositoryInterface[] */ private $repositories; /** * @var Request */ private $request; /** * @var int[] array of stability => BasePackage::STABILITY_* value * @phpstan-var array */ private $acceptableStabilities; /** * @var int[] array of package name => BasePackage::STABILITY_* value * @phpstan-var array */ private $stabilityFlags; /** * @var array[] of package => version => [alias, alias_normalized] * @phpstan-var array> */ private $rootAliases; /** * @var string[] * @phpstan-var array */ private $rootReferences; /** * @var BasePackage[] */ private $packages; /** * @var BasePackage[] */ private $unacceptableFixedPackages; /** * @param string $name The event name * @param RepositoryInterface[] $repositories * @param int[] $acceptableStabilities array of stability => BasePackage::STABILITY_* value * @param int[] $stabilityFlags array of package name => BasePackage::STABILITY_* value * @param array[] $rootAliases array of package => version => [alias, alias_normalized] * @param string[] $rootReferences * @param BasePackage[] $packages * @param BasePackage[] $unacceptableFixedPackages * * @phpstan-param array $acceptableStabilities * @phpstan-param array $stabilityFlags * @phpstan-param array> $rootAliases * @phpstan-param array $rootReferences */ public function __construct($name, array $repositories, Request $request, array $acceptableStabilities, array $stabilityFlags, array $rootAliases, array $rootReferences, array $packages, array $unacceptableFixedPackages) { parent::__construct($name); $this->repositories = $repositories; $this->request = $request; $this->acceptableStabilities = $acceptableStabilities; $this->stabilityFlags = $stabilityFlags; $this->rootAliases = $rootAliases; $this->rootReferences = $rootReferences; $this->packages = $packages; $this->unacceptableFixedPackages = $unacceptableFixedPackages; } /** * @return RepositoryInterface[] */ public function getRepositories() { return $this->repositories; } /** * @return Request */ public function getRequest() { return $this->request; } /** * @return int[] array of stability => BasePackage::STABILITY_* value * @phpstan-return array */ public function getAcceptableStabilities() { return $this->acceptableStabilities; } /** * @return int[] array of package name => BasePackage::STABILITY_* value * @phpstan-return array */ public function getStabilityFlags() { return $this->stabilityFlags; } /** * @return array[] of package => version => [alias, alias_normalized] * @phpstan-return array> */ public function getRootAliases() { return $this->rootAliases; } /** * @return string[] * @phpstan-return array */ public function getRootReferences() { return $this->rootReferences; } /** * @return BasePackage[] */ public function getPackages() { return $this->packages; } /** * @return BasePackage[] */ public function getUnacceptableFixedPackages() { return $this->unacceptableFixedPackages; } /** * @param BasePackage[] $packages * * @return void */ public function setPackages(array $packages) { $this->packages = $packages; } /** * @param BasePackage[] $packages * * @return void */ public function setUnacceptableFixedPackages(array $packages) { $this->unacceptableFixedPackages = $packages; } } * Jordi Boggiano * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Composer\Plugin; use Composer\Composer; use Composer\IO\IOInterface; /** * Plugin interface * * @author Nils Adermann */ interface PluginInterface { /** * Version number of the internal composer-plugin-api package * * This is used to denote the API version of Plugin specific * features, but is also bumped to a new major if Composer * includes a major break in internal APIs which are susceptible * to be used by plugins. * * @var string */ const PLUGIN_API_VERSION = '2.2.0'; /** * Apply plugin modifications to Composer * * @param Composer $composer * @param IOInterface $io * * @return void */ public function activate(Composer $composer, IOInterface $io); /** * Remove any hooks from Composer * * This will be called when a plugin is deactivated before being * uninstalled, but also before it gets upgraded to a new version * so the old one can be deactivated and the new one activated. * * @param Composer $composer * @param IOInterface $io * * @return void */ public function deactivate(Composer $composer, IOInterface $io); /** * Prepare the plugin to be uninstalled * * This will be called after deactivate. * * @param Composer $composer * @param IOInterface $io * * @return void */ public function uninstall(Composer $composer, IOInterface $io); } * Jordi Boggiano * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Composer\Plugin; /** * Plugins which need to expose various implementations * of the Composer Plugin Capabilities must have their * declared Plugin class implementing this interface. * * @api */ interface Capable { /** * Method by which a Plugin announces its API implementations, through an array * with a special structure. * * The key must be a string, representing a fully qualified class/interface name * which Composer Plugin API exposes. * The value must be a string as well, representing the fully qualified class name * of the implementing class. * * @tutorial * * return array( * 'Composer\Plugin\Capability\CommandProvider' => 'My\CommandProvider', * 'Composer\Plugin\Capability\Validator' => 'My\Validator', * ); * * @return string[] */ public function getCapabilities(); } * Jordi Boggiano * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Composer\Plugin; use Composer\Composer; use Composer\EventDispatcher\EventSubscriberInterface; use Composer\Installer\InstallerInterface; use Composer\IO\IOInterface; use Composer\Package\BasePackage; use Composer\Package\CompletePackage; use Composer\Package\Package; use Composer\Package\Version\VersionParser; use Composer\Pcre\Preg; use Composer\Repository\RepositoryInterface; use Composer\Repository\InstalledRepository; use Composer\Repository\RootPackageRepository; use Composer\Package\PackageInterface; use Composer\Package\Link; use Composer\Semver\Constraint\Constraint; use Composer\Plugin\Capability\Capability; use Composer\Util\PackageSorter; /** * Plugin manager * * @author Nils Adermann * @author Jordi Boggiano */ class PluginManager { /** @var Composer */ protected $composer; /** @var IOInterface */ protected $io; /** @var ?Composer */ protected $globalComposer; /** @var VersionParser */ protected $versionParser; /** @var bool */ protected $disablePlugins = false; /** @var array */ protected $plugins = array(); /** @var array */ protected $registeredPlugins = array(); /** * @var array|null */ private $allowPluginRules; /** * @var array|null */ private $allowGlobalPluginRules; /** @var int */ private static $classCounter = 0; /** * Initializes plugin manager * * @param IOInterface $io * @param Composer $composer * @param Composer $globalComposer * @param bool $disablePlugins */ public function __construct(IOInterface $io, Composer $composer, Composer $globalComposer = null, $disablePlugins = false) { $this->io = $io; $this->composer = $composer; $this->globalComposer = $globalComposer; $this->versionParser = new VersionParser(); $this->disablePlugins = $disablePlugins; $this->allowPluginRules = $this->parseAllowedPlugins($composer->getConfig()->get('allow-plugins')); $this->allowGlobalPluginRules = $this->parseAllowedPlugins($globalComposer !== null ? $globalComposer->getConfig()->get('allow-plugins') : false); } /** * Loads all plugins from currently installed plugin packages * * @return void */ public function loadInstalledPlugins() { if ($this->disablePlugins) { return; } $repo = $this->composer->getRepositoryManager()->getLocalRepository(); $globalRepo = $this->globalComposer !== null ? $this->globalComposer->getRepositoryManager()->getLocalRepository() : null; $this->loadRepository($repo, false); if ($globalRepo) { $this->loadRepository($globalRepo, true); } } /** * Deactivate all plugins from currently installed plugin packages * * @return void */ public function deactivateInstalledPlugins() { if ($this->disablePlugins) { return; } $repo = $this->composer->getRepositoryManager()->getLocalRepository(); $globalRepo = $this->globalComposer ? $this->globalComposer->getRepositoryManager()->getLocalRepository() : null; $this->deactivateRepository($repo, false); if ($globalRepo) { $this->deactivateRepository($globalRepo, true); } } /** * Gets all currently active plugin instances * * @return array plugins */ public function getPlugins() { return $this->plugins; } /** * Gets global composer or null when main composer is not fully loaded * * @return Composer|null */ public function getGlobalComposer() { return $this->globalComposer; } /** * Register a plugin package, activate it etc. * * If it's of type composer-installer it is registered as an installer * instead for BC * * @param PackageInterface $package * @param bool $failOnMissingClasses By default this silently skips plugins that can not be found, but if set to true it fails with an exception * @param bool $isGlobalPlugin Set to true to denote plugins which are installed in the global Composer directory * * @return void * * @throws \UnexpectedValueException */ public function registerPackage(PackageInterface $package, $failOnMissingClasses = false, $isGlobalPlugin = false) { if ($this->disablePlugins) { return; } if (!$this->isPluginAllowed($package->getName(), $isGlobalPlugin)) { $this->io->writeError('Skipped loading "'.$package->getName() . '" '.($isGlobalPlugin ? '(installed globally) ' : '').'as it is not in config.allow-plugins', true, IOInterface::DEBUG); return; } if ($package->getType() === 'composer-plugin') { $requiresComposer = null; foreach ($package->getRequires() as $link) { /** @var Link $link */ if ('composer-plugin-api' === $link->getTarget()) { $requiresComposer = $link->getConstraint(); break; } } if (!$requiresComposer) { throw new \RuntimeException("Plugin ".$package->getName()." is missing a require statement for a version of the composer-plugin-api package."); } $currentPluginApiVersion = $this->getPluginApiVersion(); $currentPluginApiConstraint = new Constraint('==', $this->versionParser->normalize($currentPluginApiVersion)); if ($requiresComposer->getPrettyString() === $this->getPluginApiVersion()) { $this->io->writeError('The "' . $package->getName() . '" plugin requires composer-plugin-api '.$this->getPluginApiVersion().', this *WILL* break in the future and it should be fixed ASAP (require ^'.$this->getPluginApiVersion().' instead for example).'); } elseif (!$requiresComposer->matches($currentPluginApiConstraint)) { $this->io->writeError('The "' . $package->getName() . '" plugin '.($isGlobalPlugin ? '(installed globally) ' : '').'was skipped because it requires a Plugin API version ("' . $requiresComposer->getPrettyString() . '") that does not match your Composer installation ("' . $currentPluginApiVersion . '"). You may need to run composer update with the "--no-plugins" option.'); return; } if ($package->getName() === 'symfony/flex' && Preg::isMatch('{^[0-9.]+$}', $package->getVersion()) && version_compare($package->getVersion(), '1.9.8', '<')) { $this->io->writeError('The "' . $package->getName() . '" plugin '.($isGlobalPlugin ? '(installed globally) ' : '').'was skipped because it is not compatible with Composer 2+. Make sure to update it to version 1.9.8 or greater.'); return; } } $oldInstallerPlugin = ($package->getType() === 'composer-installer'); if (isset($this->registeredPlugins[$package->getName()])) { return; } $extra = $package->getExtra(); if (empty($extra['class'])) { throw new \UnexpectedValueException('Error while installing '.$package->getPrettyName().', composer-plugin packages should have a class defined in their extra key to be usable.'); } $classes = is_array($extra['class']) ? $extra['class'] : array($extra['class']); $localRepo = $this->composer->getRepositoryManager()->getLocalRepository(); $globalRepo = $this->globalComposer !== null ? $this->globalComposer->getRepositoryManager()->getLocalRepository() : null; $rootPackage = clone $this->composer->getPackage(); // clear files autoload rules from the root package as the root dependencies are not // necessarily all present yet when booting this runtime autoloader $rootPackageAutoloads = $rootPackage->getAutoload(); $rootPackageAutoloads['files'] = array(); $rootPackage->setAutoload($rootPackageAutoloads); $rootPackageAutoloads = $rootPackage->getDevAutoload(); $rootPackageAutoloads['files'] = array(); $rootPackage->setDevAutoload($rootPackageAutoloads); unset($rootPackageAutoloads); $rootPackageRepo = new RootPackageRepository($rootPackage); $installedRepo = new InstalledRepository(array($localRepo, $rootPackageRepo)); if ($globalRepo) { $installedRepo->addRepository($globalRepo); } $autoloadPackages = array($package->getName() => $package); $autoloadPackages = $this->collectDependencies($installedRepo, $autoloadPackages, $package); $generator = $this->composer->getAutoloadGenerator(); $autoloads = array(array($rootPackage, '')); foreach ($autoloadPackages as $autoloadPackage) { if ($autoloadPackage === $rootPackage) { continue; } $downloadPath = $this->getInstallPath($autoloadPackage, $globalRepo && $globalRepo->hasPackage($autoloadPackage)); $autoloads[] = array($autoloadPackage, $downloadPath); } $map = $generator->parseAutoloads($autoloads, $rootPackage); $classLoader = $generator->createLoader($map, $this->composer->getConfig()->get('vendor-dir')); $classLoader->register(false); foreach ($map['files'] as $fileIdentifier => $file) { // exclude laminas/laminas-zendframework-bridge:src/autoload.php as it breaks Composer in some conditions // see https://github.com/composer/composer/issues/10349 and https://github.com/composer/composer/issues/10401 // this hack can be removed once this deprecated package stop being installed if ($fileIdentifier === '7e9bd612cc444b3eed788ebbe46263a0') { continue; } \Composer\Autoload\composerRequire($fileIdentifier, $file); } foreach ($classes as $class) { if (class_exists($class, false)) { $class = trim($class, '\\'); $path = $classLoader->findFile($class); $code = file_get_contents($path); $separatorPos = strrpos($class, '\\'); $className = $class; if ($separatorPos) { $className = substr($class, $separatorPos + 1); } $code = Preg::replace('{^((?:final\s+)?(?:\s*))class\s+('.preg_quote($className).')}mi', '$1class $2_composer_tmp'.self::$classCounter, $code, 1); $code = strtr($code, array( '__FILE__' => var_export($path, true), '__DIR__' => var_export(dirname($path), true), '__CLASS__' => var_export($class, true), )); $code = Preg::replace('/^\s*<\?(php)?/i', '', $code, 1); eval($code); $class .= '_composer_tmp'.self::$classCounter; self::$classCounter++; } if ($oldInstallerPlugin) { if (!is_a($class, 'Composer\Installer\InstallerInterface', true)) { throw new \RuntimeException('Could not activate plugin "'.$package->getName().'" as "'.$class.'" does not implement Composer\Installer\InstallerInterface'); } $this->io->writeError('Loading "'.$package->getName() . '" '.($isGlobalPlugin ? '(installed globally) ' : '').'which is a legacy composer-installer built for Composer 1.x, it is likely to cause issues as you are running Composer 2.x.'); $installer = new $class($this->io, $this->composer); $this->composer->getInstallationManager()->addInstaller($installer); $this->registeredPlugins[$package->getName()] = $installer; } elseif (class_exists($class)) { if (!is_a($class, 'Composer\Plugin\PluginInterface', true)) { throw new \RuntimeException('Could not activate plugin "'.$package->getName().'" as "'.$class.'" does not implement Composer\Plugin\PluginInterface'); } $plugin = new $class(); $this->addPlugin($plugin, $isGlobalPlugin, $package); $this->registeredPlugins[$package->getName()] = $plugin; } elseif ($failOnMissingClasses) { throw new \UnexpectedValueException('Plugin '.$package->getName().' could not be initialized, class not found: '.$class); } } } /** * Deactivates a plugin package * * If it's of type composer-installer it is unregistered from the installers * instead for BC * * @param PackageInterface $package * * @return void * * @throws \UnexpectedValueException */ public function deactivatePackage(PackageInterface $package) { if ($this->disablePlugins) { return; } if (!isset($this->registeredPlugins[$package->getName()])) { return; } $plugin = $this->registeredPlugins[$package->getName()]; unset($this->registeredPlugins[$package->getName()]); if ($plugin instanceof InstallerInterface) { $this->composer->getInstallationManager()->removeInstaller($plugin); } else { $this->removePlugin($plugin); } } /** * Uninstall a plugin package * * If it's of type composer-installer it is unregistered from the installers * instead for BC * * @param PackageInterface $package * * @return void * * @throws \UnexpectedValueException */ public function uninstallPackage(PackageInterface $package) { if ($this->disablePlugins) { return; } if (!isset($this->registeredPlugins[$package->getName()])) { return; } $plugin = $this->registeredPlugins[$package->getName()]; if ($plugin instanceof InstallerInterface) { $this->deactivatePackage($package); } else { unset($this->registeredPlugins[$package->getName()]); $this->removePlugin($plugin); $this->uninstallPlugin($plugin); } } /** * Returns the version of the internal composer-plugin-api package. * * @return string */ protected function getPluginApiVersion() { return PluginInterface::PLUGIN_API_VERSION; } /** * Adds a plugin, activates it and registers it with the event dispatcher * * Ideally plugin packages should be registered via registerPackage, but if you use Composer * programmatically and want to register a plugin class directly this is a valid way * to do it. * * @param PluginInterface $plugin plugin instance * @param bool $isGlobalPlugin * @param ?PackageInterface $sourcePackage Package from which the plugin comes from * * @return void */ public function addPlugin(PluginInterface $plugin, $isGlobalPlugin = false, PackageInterface $sourcePackage = null) { if ($sourcePackage === null) { trigger_error('Calling PluginManager::addPlugin without $sourcePackage is deprecated, if you are using this please get in touch with us to explain the use case', E_USER_DEPRECATED); } elseif (!$this->isPluginAllowed($sourcePackage->getName(), $isGlobalPlugin)) { $this->io->writeError('Skipped loading "'.get_class($plugin).' from '.$sourcePackage->getName() . '" '.($isGlobalPlugin ? '(installed globally) ' : '').' as it is not in config.allow-plugins', true, IOInterface::DEBUG); return; } $details = array(); if ($sourcePackage) { $details[] = 'from '.$sourcePackage->getName(); } if ($isGlobalPlugin) { $details[] = 'installed globally'; } $this->io->writeError('Loading plugin '.get_class($plugin).($details ? ' ('.implode(', ', $details).')' : ''), true, IOInterface::DEBUG); $this->plugins[] = $plugin; $plugin->activate($this->composer, $this->io); if ($plugin instanceof EventSubscriberInterface) { $this->composer->getEventDispatcher()->addSubscriber($plugin); } } /** * Removes a plugin, deactivates it and removes any listener the plugin has set on the plugin instance * * Ideally plugin packages should be deactivated via deactivatePackage, but if you use Composer * programmatically and want to deregister a plugin class directly this is a valid way * to do it. * * @param PluginInterface $plugin plugin instance * * @return void */ public function removePlugin(PluginInterface $plugin) { $index = array_search($plugin, $this->plugins, true); if ($index === false) { return; } $this->io->writeError('Unloading plugin '.get_class($plugin), true, IOInterface::DEBUG); unset($this->plugins[$index]); $plugin->deactivate($this->composer, $this->io); $this->composer->getEventDispatcher()->removeListener($plugin); } /** * Notifies a plugin it is being uninstalled and should clean up * * Ideally plugin packages should be uninstalled via uninstallPackage, but if you use Composer * programmatically and want to deregister a plugin class directly this is a valid way * to do it. * * @param PluginInterface $plugin plugin instance * * @return void */ public function uninstallPlugin(PluginInterface $plugin) { $this->io->writeError('Uninstalling plugin '.get_class($plugin), true, IOInterface::DEBUG); $plugin->uninstall($this->composer, $this->io); } /** * Load all plugins and installers from a repository * * If a plugin requires another plugin, the required one will be loaded first * * Note that plugins in the specified repository that rely on events that * have fired prior to loading will be missed. This means you likely want to * call this method as early as possible. * * @param RepositoryInterface $repo Repository to scan for plugins to install * @param bool $isGlobalRepo * * @return void * * @throws \RuntimeException */ private function loadRepository(RepositoryInterface $repo, $isGlobalRepo) { $packages = $repo->getPackages(); $sortedPackages = PackageSorter::sortPackages($packages); foreach ($sortedPackages as $package) { if (!($package instanceof CompletePackage)) { continue; } if ('composer-plugin' === $package->getType()) { $this->registerPackage($package, false, $isGlobalRepo); // Backward compatibility } elseif ('composer-installer' === $package->getType()) { $this->registerPackage($package, false, $isGlobalRepo); } } } /** * Deactivate all plugins and installers from a repository * * If a plugin requires another plugin, the required one will be deactivated last * * @param RepositoryInterface $repo Repository to scan for plugins to install * @param bool $isGlobalRepo * * @return void */ private function deactivateRepository(RepositoryInterface $repo, $isGlobalRepo) { $packages = $repo->getPackages(); $sortedPackages = array_reverse(PackageSorter::sortPackages($packages)); foreach ($sortedPackages as $package) { if (!($package instanceof CompletePackage)) { continue; } if ('composer-plugin' === $package->getType()) { $this->deactivatePackage($package); // Backward compatibility } elseif ('composer-installer' === $package->getType()) { $this->deactivatePackage($package); } } } /** * Recursively generates a map of package names to packages for all deps * * @param InstalledRepository $installedRepo Set of local repos * @param array $collected Current state of the map for recursion * @param PackageInterface $package The package to analyze * * @return array Map of package names to packages */ private function collectDependencies(InstalledRepository $installedRepo, array $collected, PackageInterface $package) { foreach ($package->getRequires() as $requireLink) { foreach ($installedRepo->findPackagesWithReplacersAndProviders($requireLink->getTarget()) as $requiredPackage) { if (!isset($collected[$requiredPackage->getName()])) { $collected[$requiredPackage->getName()] = $requiredPackage; $collected = $this->collectDependencies($installedRepo, $collected, $requiredPackage); } } } return $collected; } /** * Retrieves the path a package is installed to. * * @param PackageInterface $package * @param bool $global Whether this is a global package * * @return string Install path */ private function getInstallPath(PackageInterface $package, $global = false) { if (!$global) { return $this->composer->getInstallationManager()->getInstallPath($package); } return $this->globalComposer->getInstallationManager()->getInstallPath($package); } /** * @param PluginInterface $plugin * @param string $capability * @throws \RuntimeException On empty or non-string implementation class name value * @return null|string The fully qualified class of the implementation or null if Plugin is not of Capable type or does not provide it */ protected function getCapabilityImplementationClassName(PluginInterface $plugin, $capability) { if (!($plugin instanceof Capable)) { return null; } $capabilities = (array) $plugin->getCapabilities(); if (!empty($capabilities[$capability]) && is_string($capabilities[$capability]) && trim($capabilities[$capability])) { return trim($capabilities[$capability]); } if ( array_key_exists($capability, $capabilities) && (empty($capabilities[$capability]) || !is_string($capabilities[$capability]) || !trim($capabilities[$capability])) ) { throw new \UnexpectedValueException('Plugin '.get_class($plugin).' provided invalid capability class name(s), got '.var_export($capabilities[$capability], true)); } return null; } /** * @template CapabilityClass of Capability * @param PluginInterface $plugin * @param class-string $capabilityClassName The fully qualified name of the API interface which the plugin may provide * an implementation of. * @param array $ctorArgs Arguments passed to Capability's constructor. * Keeping it an array will allow future values to be passed w\o changing the signature. * @return null|Capability * @phpstan-param class-string $capabilityClassName * @phpstan-return null|CapabilityClass */ public function getPluginCapability(PluginInterface $plugin, $capabilityClassName, array $ctorArgs = array()) { if ($capabilityClass = $this->getCapabilityImplementationClassName($plugin, $capabilityClassName)) { if (!class_exists($capabilityClass)) { throw new \RuntimeException("Cannot instantiate Capability, as class $capabilityClass from plugin ".get_class($plugin)." does not exist."); } $ctorArgs['plugin'] = $plugin; $capabilityObj = new $capabilityClass($ctorArgs); // FIXME these could use is_a and do the check *before* instantiating once drop support for php<5.3.9 if (!$capabilityObj instanceof Capability || !$capabilityObj instanceof $capabilityClassName) { throw new \RuntimeException( 'Class ' . $capabilityClass . ' must implement both Composer\Plugin\Capability\Capability and '. $capabilityClassName . '.' ); } return $capabilityObj; } return null; } /** * @template CapabilityClass of Capability * @param class-string $capabilityClassName The fully qualified name of the API interface which the plugin may provide * an implementation of. * @param array $ctorArgs Arguments passed to Capability's constructor. * Keeping it an array will allow future values to be passed w\o changing the signature. * @return CapabilityClass[] */ public function getPluginCapabilities($capabilityClassName, array $ctorArgs = array()) { $capabilities = array(); foreach ($this->getPlugins() as $plugin) { if ($capability = $this->getPluginCapability($plugin, $capabilityClassName, $ctorArgs)) { $capabilities[] = $capability; } } return $capabilities; } /** * @param array|bool|null $allowPluginsConfig * @return array|null */ private function parseAllowedPlugins($allowPluginsConfig) { if (null === $allowPluginsConfig) { return null; } if (true === $allowPluginsConfig) { return array('{}' => true); } if (false === $allowPluginsConfig) { return array('{^$}D' => false); } $rules = array(); foreach ($allowPluginsConfig as $pattern => $allow) { $rules[BasePackage::packageNameToRegexp($pattern)] = $allow; } return $rules; } /** * @param string $package * @param bool $isGlobalPlugin * @return bool */ private function isPluginAllowed($package, $isGlobalPlugin) { static $warned = array(); $rules = $isGlobalPlugin ? $this->allowGlobalPluginRules : $this->allowPluginRules; if ($rules === null) { if (!$this->io->isInteractive()) { if (!isset($warned['all'])) { $this->io->writeError('For additional security you should declare the allow-plugins config with a list of packages names that are allowed to run code. See https://getcomposer.org/allow-plugins'); $this->io->writeError('You have until July 2022 to add the setting. Composer will then switch the default behavior to disallow all plugins.'); $warned['all'] = true; } // if no config is defined we allow all plugins for BC return true; } // keep going and prompt the user $rules = array(); } foreach ($rules as $pattern => $allow) { if (Preg::isMatch($pattern, $package)) { return $allow === true; } } if ($package === 'composer/package-versions-deprecated') { return false; } if (!isset($warned[$package])) { if ($this->io->isInteractive()) { $composer = $isGlobalPlugin && $this->globalComposer !== null ? $this->globalComposer : $this->composer; $this->io->writeError(''.$package.($isGlobalPlugin ? ' (installed globally)' : '').' contains a Composer plugin which is currently not in your allow-plugins config. See https://getcomposer.org/allow-plugins'); while (true) { switch ($answer = $this->io->ask('Do you trust "'.$package.'" to execute code and wish to enable it now? (writes "allow-plugins" to composer.json) [y,n,d,?] ', '?')) { case 'y': case 'n': case 'd': $allow = $answer === 'y'; // persist answer in current rules to avoid prompting again if the package gets reloaded if ($isGlobalPlugin) { $this->allowGlobalPluginRules[BasePackage::packageNameToRegexp($package)] = $allow; } else { $this->allowPluginRules[BasePackage::packageNameToRegexp($package)] = $allow; } // persist answer in composer.json if it wasn't simply discarded if ($answer === 'y' || $answer === 'n') { $composer->getConfig()->getConfigSource()->addConfigSetting('allow-plugins.'.$package, $allow); } return $allow; case '?': default: $this->io->writeError(array( 'y - add package to allow-plugins in composer.json and let it run immediately', 'n - add package (as disallowed) to allow-plugins in composer.json to suppress further prompts', 'd - discard this, do not change composer.json and do not allow the plugin to run', '? - print help' )); break; } } } else { $this->io->writeError(''.$package.($isGlobalPlugin ? ' (installed globally)' : '').' contains a Composer plugin which is blocked by your allow-plugins config. You may add it to the list if you consider it safe. See https://getcomposer.org/allow-plugins'); $this->io->writeError('You can run "composer '.($isGlobalPlugin ? 'global ' : '').'config --no-plugins allow-plugins.'.$package.' [true|false]" to enable it (true) or keep it disabled and suppress this warning (false)'); } $warned[$package] = true; } return false; } } * Jordi Boggiano * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Composer\Plugin; use Composer\EventDispatcher\Event; use Symfony\Component\Console\Input\InputInterface; /** * The pre command run event. * * @author Jordi Boggiano */ class PreCommandRunEvent extends Event { /** * @var InputInterface */ private $input; /** * @var string */ private $command; /** * Constructor. * * @param string $name The event name * @param InputInterface $input * @param string $command The command about to be executed */ public function __construct($name, InputInterface $input, $command) { parent::__construct($name); $this->input = $input; $this->command = $command; } /** * Returns the console input * * @return InputInterface */ public function getInput() { return $this->input; } /** * Returns the command about to be executed * * @return string */ public function getCommand() { return $this->command; } } * Jordi Boggiano * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Composer\Util; use Composer\Pcre\Preg; use stdClass; /** * Tests URLs against NO_PROXY patterns */ class NoProxyPattern { /** * @var string[] */ protected $hostNames = array(); /** * @var (null|object)[] */ protected $rules = array(); /** * @var bool */ protected $noproxy; /** * @param string $pattern NO_PROXY pattern */ public function __construct($pattern) { $this->hostNames = Preg::split('{[\s,]+}', $pattern, -1, PREG_SPLIT_NO_EMPTY); $this->noproxy = empty($this->hostNames) || '*' === $this->hostNames[0]; } /** * Returns true if a URL matches the NO_PROXY pattern * * @param string $url * * @return bool */ public function test($url) { if ($this->noproxy) { return true; } if (!$urlData = $this->getUrlData($url)) { return false; } foreach ($this->hostNames as $index => $hostName) { if ($this->match($index, $hostName, $urlData)) { return true; } } return false; } /** * Returns false is the url cannot be parsed, otherwise a data object * * @param string $url * * @return bool|stdClass */ protected function getUrlData($url) { if (!$host = parse_url($url, PHP_URL_HOST)) { return false; } $port = parse_url($url, PHP_URL_PORT); if (empty($port)) { switch (parse_url($url, PHP_URL_SCHEME)) { case 'http': $port = 80; break; case 'https': $port = 443; break; } } $hostName = $host . ($port ? ':' . $port : ''); list($host, $port, $err) = $this->splitHostPort($hostName); if ($err || !$this->ipCheckData($host, $ipdata)) { return false; } return $this->makeData($host, $port, $ipdata); } /** * Returns true if the url is matched by a rule * * @param int $index * @param string $hostName * @param stdClass $url * * @return bool */ protected function match($index, $hostName, $url) { if (!$rule = $this->getRule($index, $hostName)) { // Data must have been misformatted return false; } if ($rule->ipdata) { // Match ipdata first if (!$url->ipdata) { return false; } if ($rule->ipdata->netmask) { return $this->matchRange($rule->ipdata, $url->ipdata); } $match = $rule->ipdata->ip === $url->ipdata->ip; } else { // Match host and port $haystack = substr($url->name, -strlen($rule->name)); $match = stripos($haystack, $rule->name) === 0; } if ($match && $rule->port) { $match = $rule->port === $url->port; } return $match; } /** * Returns true if the target ip is in the network range * * @param stdClass $network * @param stdClass $target * * @return bool */ protected function matchRange(stdClass $network, stdClass $target) { $net = unpack('C*', $network->ip); $mask = unpack('C*', $network->netmask); $ip = unpack('C*', $target->ip); if (false === $net) { throw new \RuntimeException('Could not parse network IP '.$network->ip); } if (false === $mask) { throw new \RuntimeException('Could not parse netmask '.$network->netmask); } if (false === $ip) { throw new \RuntimeException('Could not parse target IP '.$target->ip); } for ($i = 1; $i < 17; ++$i) { if (($net[$i] & $mask[$i]) !== ($ip[$i] & $mask[$i])) { return false; } } return true; } /** * Finds or creates rule data for a hostname * * @param int $index * @param string $hostName * * @return null|stdClass Null if the hostname is invalid */ private function getRule($index, $hostName) { if (array_key_exists($index, $this->rules)) { return $this->rules[$index]; } $this->rules[$index] = null; list($host, $port, $err) = $this->splitHostPort($hostName); if ($err || !$this->ipCheckData($host, $ipdata, true)) { return null; } $this->rules[$index] = $this->makeData($host, $port, $ipdata); return $this->rules[$index]; } /** * Creates an object containing IP data if the host is an IP address * * @param string $host * @param null|stdClass $ipdata Set by method if IP address found * @param bool $allowPrefix Whether a CIDR prefix-length is expected * * @return bool False if the host contains invalid data */ private function ipCheckData($host, &$ipdata, $allowPrefix = false) { $ipdata = null; $netmask = null; $prefix = null; $modified = false; // Check for a CIDR prefix-length if (strpos($host, '/') !== false) { list($host, $prefix) = explode('/', $host); if (!$allowPrefix || !$this->validateInt($prefix, 0, 128)) { return false; } $prefix = (int) $prefix; $modified = true; } // See if this is an ip address if (!filter_var($host, FILTER_VALIDATE_IP)) { return !$modified; } list($ip, $size) = $this->ipGetAddr($host); if ($prefix !== null) { // Check for a valid prefix if ($prefix > $size * 8) { return false; } list($ip, $netmask) = $this->ipGetNetwork($ip, $size, $prefix); } $ipdata = $this->makeIpData($ip, $size, $netmask); return true; } /** * Returns an array of the IP in_addr and its byte size * * IPv4 addresses are always mapped to IPv6, which simplifies handling * and comparison. * * @param string $host * * @return mixed[] in_addr, size */ private function ipGetAddr($host) { $ip = inet_pton($host); $size = strlen($ip); $mapped = $this->ipMapTo6($ip, $size); return array($mapped, $size); } /** * Returns the binary network mask mapped to IPv6 * * @param int $prefix CIDR prefix-length * @param int $size Byte size of in_addr * * @return string */ private function ipGetMask($prefix, $size) { $mask = ''; if ($ones = floor($prefix / 8)) { $mask = str_repeat(chr(255), (int) $ones); } if ($remainder = $prefix % 8) { $mask .= chr(0xff ^ (0xff >> $remainder)); } $mask = str_pad($mask, $size, chr(0)); return $this->ipMapTo6($mask, $size); } /** * Calculates and returns the network and mask * * @param string $rangeIp IP in_addr * @param int $size Byte size of in_addr * @param int $prefix CIDR prefix-length * * @return string[] network in_addr, binary mask */ private function ipGetNetwork($rangeIp, $size, $prefix) { $netmask = $this->ipGetMask($prefix, $size); // Get the network from the address and mask $mask = unpack('C*', $netmask); $ip = unpack('C*', $rangeIp); $net = ''; if (false === $mask) { throw new \RuntimeException('Could not parse netmask '.$netmask); } if (false === $ip) { throw new \RuntimeException('Could not parse range IP '.$rangeIp); } for ($i = 1; $i < 17; ++$i) { $net .= chr($ip[$i] & $mask[$i]); } return array($net, $netmask); } /** * Maps an IPv4 address to IPv6 * * @param string $binary in_addr * @param int $size Byte size of in_addr * * @return string Mapped or existing in_addr */ private function ipMapTo6($binary, $size) { if ($size === 4) { $prefix = str_repeat(chr(0), 10) . str_repeat(chr(255), 2); $binary = $prefix . $binary; } return $binary; } /** * Creates a rule data object * * @param string $host * @param int $port * @param null|stdClass $ipdata * * @return stdClass */ private function makeData($host, $port, $ipdata) { return (object) array( 'host' => $host, 'name' => '.' . ltrim($host, '.'), 'port' => $port, 'ipdata' => $ipdata, ); } /** * Creates an ip data object * * @param string $ip in_addr * @param int $size Byte size of in_addr * @param null|string $netmask Network mask * * @return stdClass */ private function makeIpData($ip, $size, $netmask) { return (object) array( 'ip' => $ip, 'size' => $size, 'netmask' => $netmask, ); } /** * Splits the hostname into host and port components * * @param string $hostName * * @return mixed[] host, port, if there was error */ private function splitHostPort($hostName) { // host, port, err $error = array('', '', true); $port = 0; $ip6 = ''; // Check for square-bracket notation if ($hostName[0] === '[') { $index = strpos($hostName, ']'); // The smallest ip6 address is :: if (false === $index || $index < 3) { return $error; } $ip6 = substr($hostName, 1, $index - 1); $hostName = substr($hostName, $index + 1); if (strpbrk($hostName, '[]') !== false || substr_count($hostName, ':') > 1) { return $error; } } if (substr_count($hostName, ':') === 1) { $index = strpos($hostName, ':'); $port = substr($hostName, $index + 1); $hostName = substr($hostName, 0, $index); if (!$this->validateInt($port, 1, 65535)) { return $error; } $port = (int) $port; } $host = $ip6 . $hostName; return array($host, $port, false); } /** * Wrapper around filter_var FILTER_VALIDATE_INT * * @param string $int * @param int $min * @param int $max * * @return bool */ private function validateInt($int, $min, $max) { $options = array( 'options' => array( 'min_range' => $min, 'max_range' => $max, ), ); return false !== filter_var($int, FILTER_VALIDATE_INT, $options); } } * Jordi Boggiano * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Composer\Util; use Composer\IO\IOInterface; use Composer\Pcre\Preg; use Symfony\Component\Process\Process; use Symfony\Component\Process\Exception\RuntimeException; use React\Promise\Promise; use React\Promise\PromiseInterface; /** * @author Robert Schönthal * @author Jordi Boggiano */ class ProcessExecutor { const STATUS_QUEUED = 1; const STATUS_STARTED = 2; const STATUS_COMPLETED = 3; const STATUS_FAILED = 4; const STATUS_ABORTED = 5; /** @var int */ protected static $timeout = 300; /** @var bool */ protected $captureOutput = false; /** @var string */ protected $errorOutput = ''; /** @var ?IOInterface */ protected $io; /** * @phpstan-var array> */ private $jobs = array(); /** @var int */ private $runningJobs = 0; /** @var int */ private $maxJobs = 10; /** @var int */ private $idGen = 0; /** @var bool */ private $allowAsync = false; public function __construct(IOInterface $io = null) { $this->io = $io; } /** * runs a process on the commandline * * @param string $command the command to execute * @param mixed $output the output will be written into this var if passed by ref * if a callable is passed it will be used as output handler * @param ?string $cwd the working directory * @return int statuscode */ public function execute($command, &$output = null, $cwd = null) { if (func_num_args() > 1) { return $this->doExecute($command, $cwd, false, $output); } return $this->doExecute($command, $cwd, false); } /** * runs a process on the commandline in TTY mode * * @param string $command the command to execute * @param ?string $cwd the working directory * @return int statuscode */ public function executeTty($command, $cwd = null) { if (Platform::isTty()) { return $this->doExecute($command, $cwd, true); } return $this->doExecute($command, $cwd, false); } /** * @param string $command * @param ?string $cwd * @param bool $tty * @param mixed $output * @return int */ private function doExecute($command, $cwd, $tty, &$output = null) { $this->outputCommandRun($command, $cwd, false); // TODO in 2.2, these two checks can be dropped as Symfony 4+ supports them out of the box // make sure that null translate to the proper directory in case the dir is a symlink // and we call a git command, because msysgit does not handle symlinks properly if (null === $cwd && Platform::isWindows() && false !== strpos($command, 'git') && getcwd()) { $cwd = realpath(getcwd()); } if (null !== $cwd && !is_dir($cwd)) { throw new \RuntimeException('The given CWD for the process does not exist: '.$cwd); } $this->captureOutput = func_num_args() > 3; $this->errorOutput = ''; // TODO in v3, commands should be passed in as arrays of cmd + args if (method_exists('Symfony\Component\Process\Process', 'fromShellCommandline')) { $process = Process::fromShellCommandline($command, $cwd, null, null, static::getTimeout()); } else { /** @phpstan-ignore-next-line */ $process = new Process($command, $cwd, null, null, static::getTimeout()); } if (!Platform::isWindows() && $tty) { try { $process->setTty(true); } catch (RuntimeException $e) { // ignore TTY enabling errors } } $callback = is_callable($output) ? $output : array($this, 'outputHandler'); $process->run($callback); if ($this->captureOutput && !is_callable($output)) { $output = $process->getOutput(); } $this->errorOutput = $process->getErrorOutput(); return $process->getExitCode(); } /** * starts a process on the commandline in async mode * * @param string $command the command to execute * @param string $cwd the working directory * @return PromiseInterface */ public function executeAsync($command, $cwd = null) { if (!$this->allowAsync) { throw new \LogicException('You must use the ProcessExecutor instance which is part of a Composer\Loop instance to be able to run async processes'); } $job = array( 'id' => $this->idGen++, 'status' => self::STATUS_QUEUED, 'command' => $command, 'cwd' => $cwd, ); $resolver = function ($resolve, $reject) use (&$job) { $job['status'] = ProcessExecutor::STATUS_QUEUED; $job['resolve'] = $resolve; $job['reject'] = $reject; }; $self = $this; $canceler = function () use (&$job) { if ($job['status'] === ProcessExecutor::STATUS_QUEUED) { $job['status'] = ProcessExecutor::STATUS_ABORTED; } if ($job['status'] !== ProcessExecutor::STATUS_STARTED) { return; } $job['status'] = ProcessExecutor::STATUS_ABORTED; try { if (defined('SIGINT')) { $job['process']->signal(SIGINT); } } catch (\Exception $e) { // signal can throw in various conditions, but we don't care if it fails } $job['process']->stop(1); throw new \RuntimeException('Aborted process'); }; $promise = new Promise($resolver, $canceler); $promise = $promise->then(function () use (&$job, $self) { if ($job['process']->isSuccessful()) { $job['status'] = ProcessExecutor::STATUS_COMPLETED; } else { $job['status'] = ProcessExecutor::STATUS_FAILED; } // TODO 3.0 this should be done directly on $this when PHP 5.3 is dropped $self->markJobDone(); return $job['process']; }, function ($e) use (&$job, $self) { $job['status'] = ProcessExecutor::STATUS_FAILED; $self->markJobDone(); throw $e; }); $this->jobs[$job['id']] = &$job; if ($this->runningJobs < $this->maxJobs) { $this->startJob($job['id']); } return $promise; } /** * @param int $id * @return void */ private function startJob($id) { $job = &$this->jobs[$id]; if ($job['status'] !== self::STATUS_QUEUED) { return; } // start job $job['status'] = self::STATUS_STARTED; $this->runningJobs++; $command = $job['command']; $cwd = $job['cwd']; $this->outputCommandRun($command, $cwd, true); // TODO in 2.2, these two checks can be dropped as Symfony 4+ supports them out of the box // make sure that null translate to the proper directory in case the dir is a symlink // and we call a git command, because msysgit does not handle symlinks properly if (null === $cwd && Platform::isWindows() && false !== strpos($command, 'git') && getcwd()) { $cwd = realpath(getcwd()); } if (null !== $cwd && !is_dir($cwd)) { throw new \RuntimeException('The given CWD for the process does not exist: '.$cwd); } try { // TODO in v3, commands should be passed in as arrays of cmd + args if (method_exists('Symfony\Component\Process\Process', 'fromShellCommandline')) { $process = Process::fromShellCommandline($command, $cwd, null, null, static::getTimeout()); } else { $process = new Process($command, $cwd, null, null, static::getTimeout()); } } catch (\Exception $e) { call_user_func($job['reject'], $e); return; } catch (\Throwable $e) { call_user_func($job['reject'], $e); return; } $job['process'] = $process; try { $process->start(); } catch (\Exception $e) { call_user_func($job['reject'], $e); return; } catch (\Throwable $e) { call_user_func($job['reject'], $e); return; } } /** * @param ?int $index job id * @return void */ public function wait($index = null) { while (true) { if (!$this->countActiveJobs($index)) { return; } usleep(1000); } } /** * @internal * * @return void */ public function enableAsync() { $this->allowAsync = true; } /** * @internal * * @param ?int $index job id * @return int number of active (queued or started) jobs */ public function countActiveJobs($index = null) { // tick foreach ($this->jobs as $job) { if ($job['status'] === self::STATUS_STARTED) { if (!$job['process']->isRunning()) { call_user_func($job['resolve'], $job['process']); } $job['process']->checkTimeout(); } if ($this->runningJobs < $this->maxJobs) { if ($job['status'] === self::STATUS_QUEUED) { $this->startJob($job['id']); } } } if (null !== $index) { return $this->jobs[$index]['status'] < self::STATUS_COMPLETED ? 1 : 0; } $active = 0; foreach ($this->jobs as $job) { if ($job['status'] < self::STATUS_COMPLETED) { $active++; } else { unset($this->jobs[$job['id']]); } } return $active; } /** * @private * * @return void */ public function markJobDone() { $this->runningJobs--; } /** * @param ?string $output * @return string[] */ public function splitLines($output) { $output = trim((string) $output); return $output === '' ? array() : Preg::split('{\r?\n}', $output); } /** * Get any error output from the last command * * @return string */ public function getErrorOutput() { return $this->errorOutput; } /** * @private * * @param Process::ERR|Process::OUT $type * @param string $buffer * * @return void */ public function outputHandler($type, $buffer) { if ($this->captureOutput) { return; } if (null === $this->io) { echo $buffer; return; } if (Process::ERR === $type) { $this->io->writeErrorRaw($buffer, false); } else { $this->io->writeRaw($buffer, false); } } /** * @return int the timeout in seconds */ public static function getTimeout() { return static::$timeout; } /** * @param int $timeout the timeout in seconds * @return void */ public static function setTimeout($timeout) { static::$timeout = $timeout; } /** * Escapes a string to be used as a shell argument. * * @param string|false|null $argument The argument that will be escaped * * @return string The escaped argument */ public static function escape($argument) { return self::escapeArgument($argument); } /** * @param string $command * @param ?string $cwd * @param bool $async * @return void */ private function outputCommandRun($command, $cwd, $async) { if (null === $this->io || !$this->io->isDebug()) { return; } $safeCommand = Preg::replaceCallback('{://(?P[^:/\s]+):(?P[^@\s/]+)@}i', function ($m) { // if the username looks like a long (12char+) hex string, or a modern github token (e.g. ghp_xxx) we obfuscate that if (Preg::isMatch('{^([a-f0-9]{12,}|gh[a-z]_[a-zA-Z0-9_]+)$}', $m['user'])) { return '://***:***@'; } if (Preg::isMatch('{^[a-f0-9]{12,}$}', $m['user'])) { return '://***:***@'; } return '://'.$m['user'].':***@'; }, $command); $safeCommand = Preg::replace("{--password (.*[^\\\\]\') }", '--password \'***\' ', $safeCommand); $this->io->writeError('Executing'.($async ? ' async' : '').' command ('.($cwd ?: 'CWD').'): '.$safeCommand); } /** * Escapes a string to be used as a shell argument for Symfony Process. * * This method expects cmd.exe to be started with the /V:ON option, which * enables delayed environment variable expansion using ! as the delimiter. * If this is not the case, any escaped ^^!var^^! will be transformed to * ^!var^! and introduce two unintended carets. * * Modified from https://github.com/johnstevenson/winbox-args * MIT Licensed (c) John Stevenson * * @param string|false|null $argument * * @return string */ private static function escapeArgument($argument) { if ('' === ($argument = (string) $argument)) { return escapeshellarg($argument); } if (!Platform::isWindows()) { return "'".str_replace("'", "'\\''", $argument)."'"; } // New lines break cmd.exe command parsing $argument = strtr($argument, "\n", ' '); $quote = strpbrk($argument, " \t") !== false; $argument = Preg::replace('/(\\\\*)"/', '$1$1\\"', $argument, -1, $dquotes); $meta = $dquotes || Preg::isMatch('/%[^%]+%|![^!]+!/', $argument); if (!$meta && !$quote) { $quote = strpbrk($argument, '^&|<>()') !== false; } if ($quote) { $argument = '"'.Preg::replace('/(\\\\*)$/', '$1$1', $argument).'"'; } if ($meta) { $argument = Preg::replace('/(["^&|<>()%])/', '^$1', $argument); $argument = Preg::replace('/(!)/', '^^$1', $argument); } return $argument; } } * Jordi Boggiano * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Composer\Util; use Composer\Config; use Composer\IO\IOInterface; use Composer\Pcre\Preg; /** * @author Jonas Renaudot */ class Hg { /** @var string|false|null */ private static $version = false; /** * @var \Composer\IO\IOInterface */ private $io; /** * @var \Composer\Config */ private $config; /** * @var \Composer\Util\ProcessExecutor */ private $process; public function __construct(IOInterface $io, Config $config, ProcessExecutor $process) { $this->io = $io; $this->config = $config; $this->process = $process; } /** * @param callable $commandCallable * @param string $url * @param string|null $cwd * * @return void */ public function runCommand($commandCallable, $url, $cwd) { $this->config->prohibitUrlByConfig($url, $this->io); // Try as is $command = call_user_func($commandCallable, $url); if (0 === $this->process->execute($command, $ignoredOutput, $cwd)) { return; } // Try with the authentication information available if (Preg::isMatch('{^(https?)://((.+)(?:\:(.+))?@)?([^/]+)(/.*)?}mi', $url, $match) && $this->io->hasAuthentication($match[5])) { $auth = $this->io->getAuthentication($match[5]); $authenticatedUrl = $match[1] . '://' . rawurlencode($auth['username']) . ':' . rawurlencode($auth['password']) . '@' . $match[5] . (!empty($match[6]) ? $match[6] : null); $command = call_user_func($commandCallable, $authenticatedUrl); if (0 === $this->process->execute($command, $ignoredOutput, $cwd)) { return; } $error = $this->process->getErrorOutput(); } else { $error = 'The given URL (' . $url . ') does not match the required format (http(s)://(username:password@)example.com/path-to-repository)'; } $this->throwException('Failed to clone ' . $url . ', ' . "\n\n" . $error, $url); } /** * @param non-empty-string $message * @param string $url * * @return never */ private function throwException($message, $url) { if (null === self::getVersion($this->process)) { throw new \RuntimeException(Url::sanitize('Failed to clone ' . $url . ', hg was not found, check that it is installed and in your PATH env.' . "\n\n" . $this->process->getErrorOutput())); } throw new \RuntimeException(Url::sanitize($message)); } /** * Retrieves the current hg version. * * @return string|null The hg version number, if present. */ public static function getVersion(ProcessExecutor $process) { if (false === self::$version) { self::$version = null; if (0 === $process->execute('hg --version', $output) && Preg::isMatch('/^.+? (\d+(?:\.\d+)+)\)?\r?\n/', $output, $matches)) { self::$version = $matches[1]; } } return self::$version; } } * Jordi Boggiano * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Composer\Util; use Composer\IO\IOInterface; use Composer\Pcre\Preg; use Symfony\Component\Process\Process; /** * @author Matt Whittom * * @phpstan-type RepoConfig array{unique_perforce_client_name?: string, depot?: string, branch?: string, p4user?: string, p4password?: string} */ class Perforce { /** @var string */ protected $path; /** @var ?string */ protected $p4Depot; /** @var ?string */ protected $p4Client; /** @var ?string */ protected $p4User; /** @var ?string */ protected $p4Password; /** @var string */ protected $p4Port; /** @var ?string */ protected $p4Stream; /** @var string */ protected $p4ClientSpec; /** @var ?string */ protected $p4DepotType; /** @var ?string */ protected $p4Branch; /** @var ProcessExecutor */ protected $process; /** @var string */ protected $uniquePerforceClientName; /** @var bool */ protected $windowsFlag; /** @var string */ protected $commandResult; /** @var IOInterface */ protected $io; /** @var ?Filesystem */ protected $filesystem; /** * @phpstan-param RepoConfig $repoConfig * @param string $port * @param string $path * @param ProcessExecutor $process * @param bool $isWindows * @param IOInterface $io */ public function __construct($repoConfig, $port, $path, ProcessExecutor $process, $isWindows, IOInterface $io) { $this->windowsFlag = $isWindows; $this->p4Port = $port; $this->initializePath($path); $this->process = $process; $this->initialize($repoConfig); $this->io = $io; } /** * @phpstan-param RepoConfig $repoConfig * @param string $port * @param string $path * @param ProcessExecutor $process * @param IOInterface $io * * @return self */ public static function create($repoConfig, $port, $path, ProcessExecutor $process, IOInterface $io) { return new Perforce($repoConfig, $port, $path, $process, Platform::isWindows(), $io); } /** * @param string $url * @param ProcessExecutor $processExecutor * * @return bool */ public static function checkServerExists($url, ProcessExecutor $processExecutor) { $output = null; return 0 === $processExecutor->execute('p4 -p ' . ProcessExecutor::escape($url) . ' info -s', $output); } /** * @phpstan-param RepoConfig $repoConfig * * @return void */ public function initialize($repoConfig) { $this->uniquePerforceClientName = $this->generateUniquePerforceClientName(); if (!$repoConfig) { return; } if (isset($repoConfig['unique_perforce_client_name'])) { $this->uniquePerforceClientName = $repoConfig['unique_perforce_client_name']; } if (isset($repoConfig['depot'])) { $this->p4Depot = $repoConfig['depot']; } if (isset($repoConfig['branch'])) { $this->p4Branch = $repoConfig['branch']; } if (isset($repoConfig['p4user'])) { $this->p4User = $repoConfig['p4user']; } else { $this->p4User = $this->getP4variable('P4USER'); } if (isset($repoConfig['p4password'])) { $this->p4Password = $repoConfig['p4password']; } } /** * @param string|null $depot * @param string|null $branch * * @return void */ public function initializeDepotAndBranch($depot, $branch) { if (isset($depot)) { $this->p4Depot = $depot; } if (isset($branch)) { $this->p4Branch = $branch; } } /** * @return non-empty-string */ public function generateUniquePerforceClientName() { return gethostname() . "_" . time(); } /** * @return void */ public function cleanupClientSpec() { $client = $this->getClient(); $task = 'client -d ' . ProcessExecutor::escape($client); $useP4Client = false; $command = $this->generateP4Command($task, $useP4Client); $this->executeCommand($command); $clientSpec = $this->getP4ClientSpec(); $fileSystem = $this->getFilesystem(); $fileSystem->remove($clientSpec); } /** * @param non-empty-string $command * * @return int */ protected function executeCommand($command) { $this->commandResult = ''; return $this->process->execute($command, $this->commandResult); } /** * @return string */ public function getClient() { if (!isset($this->p4Client)) { $cleanStreamName = str_replace(array('//', '/', '@'), array('', '_', ''), $this->getStream()); $this->p4Client = 'composer_perforce_' . $this->uniquePerforceClientName . '_' . $cleanStreamName; } return $this->p4Client; } /** * @return string */ protected function getPath() { return $this->path; } /** * @param string $path * * @return void */ public function initializePath($path) { $this->path = $path; $fs = $this->getFilesystem(); $fs->ensureDirectoryExists($path); } /** * @return string */ protected function getPort() { return $this->p4Port; } /** * @param string $stream * * @return void */ public function setStream($stream) { $this->p4Stream = $stream; $index = strrpos($stream, '/'); //Stream format is //depot/stream, while non-streaming depot is //depot if ($index > 2) { $this->p4DepotType = 'stream'; } } /** * @return bool */ public function isStream() { return is_string($this->p4DepotType) && (strcmp($this->p4DepotType, 'stream') === 0); } /** * @return string */ public function getStream() { if (!isset($this->p4Stream)) { if ($this->isStream()) { $this->p4Stream = '//' . $this->p4Depot . '/' . $this->p4Branch; } else { $this->p4Stream = '//' . $this->p4Depot; } } return $this->p4Stream; } /** * @param string $stream * * @return string */ public function getStreamWithoutLabel($stream) { $index = strpos($stream, '@'); if ($index === false) { return $stream; } return substr($stream, 0, $index); } /** * @return non-empty-string */ public function getP4ClientSpec() { return $this->path . '/' . $this->getClient() . '.p4.spec'; } /** * @return string|null */ public function getUser() { return $this->p4User; } /** * @param string|null $user * * @return void */ public function setUser($user) { $this->p4User = $user; } /** * @return void */ public function queryP4User() { $this->getUser(); if (strlen((string) $this->p4User) > 0) { return; } $this->p4User = $this->getP4variable('P4USER'); if (strlen((string) $this->p4User) > 0) { return; } $this->p4User = $this->io->ask('Enter P4 User:'); if ($this->windowsFlag) { $command = 'p4 set P4USER=' . $this->p4User; } else { $command = 'export P4USER=' . $this->p4User; } $this->executeCommand($command); } /** * @param string $name * @return ?string */ protected function getP4variable($name) { if ($this->windowsFlag) { $command = 'p4 set'; $this->executeCommand($command); $result = trim($this->commandResult); $resArray = explode(PHP_EOL, $result); foreach ($resArray as $line) { $fields = explode('=', $line); if (strcmp($name, $fields[0]) == 0) { $index = strpos($fields[1], ' '); if ($index === false) { $value = $fields[1]; } else { $value = substr($fields[1], 0, $index); } $value = trim($value); return $value; } } return null; } $command = 'echo $' . $name; $this->executeCommand($command); $result = trim($this->commandResult); return $result; } /** * @return string|null */ public function queryP4Password() { if (isset($this->p4Password)) { return $this->p4Password; } $password = $this->getP4variable('P4PASSWD'); if (strlen((string) $password) <= 0) { $password = $this->io->askAndHideAnswer('Enter password for Perforce user ' . $this->getUser() . ': '); } $this->p4Password = $password; return $password; } /** * @param string $command * @param bool $useClient * * @return non-empty-string */ public function generateP4Command($command, $useClient = true) { $p4Command = 'p4 '; $p4Command .= '-u ' . $this->getUser() . ' '; if ($useClient) { $p4Command .= '-c ' . $this->getClient() . ' '; } $p4Command .= '-p ' . $this->getPort() . ' ' . $command; return $p4Command; } /** * @return bool */ public function isLoggedIn() { $command = $this->generateP4Command('login -s', false); $exitCode = $this->executeCommand($command); if ($exitCode) { $errorOutput = $this->process->getErrorOutput(); $index = strpos($errorOutput, $this->getUser()); if ($index === false) { $index = strpos($errorOutput, 'p4'); if ($index === false) { return false; } throw new \Exception('p4 command not found in path: ' . $errorOutput); } throw new \Exception('Invalid user name: ' . $this->getUser()); } return true; } /** * @return void */ public function connectClient() { $p4CreateClientCommand = $this->generateP4Command( 'client -i < ' . str_replace(" ", "\\ ", $this->getP4ClientSpec()) ); $this->executeCommand($p4CreateClientCommand); } /** * @param string|null $sourceReference * * @return void */ public function syncCodeBase($sourceReference) { $prevDir = getcwd(); chdir($this->path); $p4SyncCommand = $this->generateP4Command('sync -f '); if (null !== $sourceReference) { $p4SyncCommand .= '@' . $sourceReference; } $this->executeCommand($p4SyncCommand); chdir($prevDir); } /** * @param resource|false $spec * * @return void */ public function writeClientSpecToFile($spec) { fwrite($spec, 'Client: ' . $this->getClient() . PHP_EOL . PHP_EOL); fwrite($spec, 'Update: ' . date('Y/m/d H:i:s') . PHP_EOL . PHP_EOL); fwrite($spec, 'Access: ' . date('Y/m/d H:i:s') . PHP_EOL); fwrite($spec, 'Owner: ' . $this->getUser() . PHP_EOL . PHP_EOL); fwrite($spec, 'Description:' . PHP_EOL); fwrite($spec, ' Created by ' . $this->getUser() . ' from composer.' . PHP_EOL . PHP_EOL); fwrite($spec, 'Root: ' . $this->getPath() . PHP_EOL . PHP_EOL); fwrite($spec, 'Options: noallwrite noclobber nocompress unlocked modtime rmdir' . PHP_EOL . PHP_EOL); fwrite($spec, 'SubmitOptions: revertunchanged' . PHP_EOL . PHP_EOL); fwrite($spec, 'LineEnd: local' . PHP_EOL . PHP_EOL); if ($this->isStream()) { fwrite($spec, 'Stream:' . PHP_EOL); fwrite($spec, ' ' . $this->getStreamWithoutLabel($this->p4Stream) . PHP_EOL); } else { fwrite( $spec, 'View: ' . $this->getStream() . '/... //' . $this->getClient() . '/... ' . PHP_EOL ); } } /** * @return void */ public function writeP4ClientSpec() { $clientSpec = $this->getP4ClientSpec(); $spec = fopen($clientSpec, 'w'); try { $this->writeClientSpecToFile($spec); } catch (\Exception $e) { fclose($spec); throw $e; } fclose($spec); } /** * @param resource $pipe * @param mixed $name * * @return void */ protected function read($pipe, $name) { if (feof($pipe)) { return; } $line = fgets($pipe); while ($line !== false) { $line = fgets($pipe); } } /** * @param string|null $password * * @return int */ public function windowsLogin($password) { $command = $this->generateP4Command(' login -a'); // TODO in v3 generate command as an array if (method_exists('Symfony\Component\Process\Process', 'fromShellCommandline')) { $process = Process::fromShellCommandline($command, null, null, $password); } else { // @phpstan-ignore-next-line $process = new Process($command, null, null, $password); } return $process->run(); } /** * @return void */ public function p4Login() { $this->queryP4User(); if (!$this->isLoggedIn()) { $password = $this->queryP4Password(); if ($this->windowsFlag) { $this->windowsLogin($password); } else { $command = 'echo ' . ProcessExecutor::escape($password) . ' | ' . $this->generateP4Command(' login -a', false); $exitCode = $this->executeCommand($command); if ($exitCode) { throw new \Exception("Error logging in:" . $this->process->getErrorOutput()); } } } } /** * @param string $identifier * * @return mixed|void */ public function getComposerInformation($identifier) { $composerFileContent = $this->getFileContent('composer.json', $identifier); if (!$composerFileContent) { return; } return json_decode($composerFileContent, true); } /** * @param string $file * @param string $identifier * * @return string|null */ public function getFileContent($file, $identifier) { $path = $this->getFilePath($file, $identifier); $command = $this->generateP4Command(' print ' . ProcessExecutor::escape($path)); $this->executeCommand($command); $result = $this->commandResult; if (!trim($result)) { return null; } return $result; } /** * @param string $file * @param string $identifier * * @return string|null */ public function getFilePath($file, $identifier) { $index = strpos($identifier, '@'); if ($index === false) { return $identifier. '/' . $file; } $path = substr($identifier, 0, $index) . '/' . $file . substr($identifier, $index); $command = $this->generateP4Command(' files ' . ProcessExecutor::escape($path), false); $this->executeCommand($command); $result = $this->commandResult; $index2 = strpos($result, 'no such file(s).'); if ($index2 === false) { $index3 = strpos($result, 'change'); if ($index3 !== false) { $phrase = trim(substr($result, $index3)); $fields = explode(' ', $phrase); return substr($identifier, 0, $index) . '/' . $file . '@' . $fields[1]; } } return null; } /** * @return array{master: string} */ public function getBranches() { $possibleBranches = array(); if (!$this->isStream()) { $possibleBranches[$this->p4Branch] = $this->getStream(); } else { $command = $this->generateP4Command('streams '.ProcessExecutor::escape('//' . $this->p4Depot . '/...')); $this->executeCommand($command); $result = $this->commandResult; $resArray = explode(PHP_EOL, $result); foreach ($resArray as $line) { $resBits = explode(' ', $line); if (count($resBits) > 4) { $branch = Preg::replace('/[^A-Za-z0-9 ]/', '', $resBits[4]); $possibleBranches[$branch] = $resBits[1]; } } } $command = $this->generateP4Command('changes '. ProcessExecutor::escape($this->getStream() . '/...'), false); $this->executeCommand($command); $result = $this->commandResult; $resArray = explode(PHP_EOL, $result); $lastCommit = $resArray[0]; $lastCommitArr = explode(' ', $lastCommit); $lastCommitNum = $lastCommitArr[1]; return array('master' => $possibleBranches[$this->p4Branch] . '@'. $lastCommitNum); } /** * @return array */ public function getTags() { $command = $this->generateP4Command('labels'); $this->executeCommand($command); $result = $this->commandResult; $resArray = explode(PHP_EOL, $result); $tags = array(); foreach ($resArray as $line) { if (strpos($line, 'Label') !== false) { $fields = explode(' ', $line); $tags[$fields[1]] = $this->getStream() . '@' . $fields[1]; } } return $tags; } /** * @return bool */ public function checkStream() { $command = $this->generateP4Command('depots', false); $this->executeCommand($command); $result = $this->commandResult; $resArray = explode(PHP_EOL, $result); foreach ($resArray as $line) { if (strpos($line, 'Depot') !== false) { $fields = explode(' ', $line); if (strcmp($this->p4Depot, $fields[1]) === 0) { $this->p4DepotType = $fields[3]; return $this->isStream(); } } } return false; } /** * @param string $reference * @return mixed|null */ protected function getChangeList($reference) { $index = strpos($reference, '@'); if ($index === false) { return null; } $label = substr($reference, $index); $command = $this->generateP4Command(' changes -m1 ' . ProcessExecutor::escape($label)); $this->executeCommand($command); $changes = $this->commandResult; if (strpos($changes, 'Change') !== 0) { return null; } $fields = explode(' ', $changes); return $fields[1]; } /** * @param string $fromReference * @param string $toReference * @return mixed|null */ public function getCommitLogs($fromReference, $toReference) { $fromChangeList = $this->getChangeList($fromReference); if ($fromChangeList === null) { return null; } $toChangeList = $this->getChangeList($toReference); if ($toChangeList === null) { return null; } $index = strpos($fromReference, '@'); $main = substr($fromReference, 0, $index) . '/...'; $command = $this->generateP4Command('filelog ' . ProcessExecutor::escape($main . '@' . $fromChangeList. ',' . $toChangeList)); $this->executeCommand($command); return $this->commandResult; } /** * @return Filesystem */ public function getFilesystem() { if (null === $this->filesystem) { $this->filesystem = new Filesystem($this->process); } return $this->filesystem; } /** * @return void */ public function setFilesystem(Filesystem $fs) { $this->filesystem = $fs; } } * Jordi Boggiano * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Composer\Util; use Composer\IO\IOInterface; use Composer\Pcre\Preg; /** * Convert PHP errors into exceptions * * @author Artem Lopata */ class ErrorHandler { /** @var ?IOInterface */ private static $io; /** * Error handler * * @param int $level Level of the error raised * @param string $message Error message * @param string $file Filename that the error was raised in * @param int $line Line number the error was raised at * * @static * @throws \ErrorException * @return bool */ public static function handle($level, $message, $file, $line) { // error code is not included in error_reporting if (!(error_reporting() & $level)) { return true; } if (filter_var(ini_get('xdebug.scream'), FILTER_VALIDATE_BOOLEAN)) { $message .= "\n\nWarning: You have xdebug.scream enabled, the warning above may be". "\na legitimately suppressed error that you were not supposed to see."; } if ($level !== E_DEPRECATED && $level !== E_USER_DEPRECATED) { throw new \ErrorException($message, 0, $level, $file, $line); } if (self::$io) { // ignore symfony/* deprecation warnings // TODO remove in 2.3 if (Preg::isMatch('{^Return type of Symfony\\\\.*ReturnTypeWillChange}is', $message)) { return true; } if (strpos(strtr($file, '\\', '/'), 'vendor/symfony/') !== false) { return true; } self::$io->writeError('Deprecation Notice: '.$message.' in '.$file.':'.$line.''); if (self::$io->isVerbose()) { self::$io->writeError('Stack trace:'); self::$io->writeError(array_filter(array_map(function ($a) { if (isset($a['line'], $a['file'])) { return ' '.$a['file'].':'.$a['line'].''; } return null; }, array_slice(debug_backtrace(), 2)))); } } return true; } /** * Register error handler. * * @param IOInterface|null $io * * @return void */ public static function register(IOInterface $io = null) { set_error_handler(array(__CLASS__, 'handle')); error_reporting(E_ALL | E_STRICT); self::$io = $io; } } * Jordi Boggiano * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Composer\Util; use Composer\Config; use Composer\IO\IOInterface; use Composer\Pcre\Preg; /** * @author Till Klampaeckel * @author Jordi Boggiano */ class Svn { const MAX_QTY_AUTH_TRIES = 5; /** * @var ?array{username: string, password: string} */ protected $credentials; /** * @var bool */ protected $hasAuth; /** * @var \Composer\IO\IOInterface */ protected $io; /** * @var string */ protected $url; /** * @var bool */ protected $cacheCredentials = true; /** * @var ProcessExecutor */ protected $process; /** * @var int */ protected $qtyAuthTries = 0; /** * @var \Composer\Config */ protected $config; /** * @var string|null */ private static $version; /** * @param string $url * @param \Composer\IO\IOInterface $io * @param Config $config * @param ProcessExecutor $process */ public function __construct($url, IOInterface $io, Config $config, ProcessExecutor $process = null) { $this->url = $url; $this->io = $io; $this->config = $config; $this->process = $process ?: new ProcessExecutor($io); } /** * @return void */ public static function cleanEnv() { // clean up env for OSX, see https://github.com/composer/composer/issues/2146#issuecomment-35478940 Platform::clearEnv('DYLD_LIBRARY_PATH'); } /** * Execute an SVN remote command and try to fix up the process with credentials * if necessary. * * @param string $command SVN command to run * @param string $url SVN url * @param string $cwd Working directory * @param string $path Target for a checkout * @param bool $verbose Output all output to the user * * @throws \RuntimeException * @return string */ public function execute($command, $url, $cwd = null, $path = null, $verbose = false) { // Ensure we are allowed to use this URL by config $this->config->prohibitUrlByConfig($url, $this->io); return $this->executeWithAuthRetry($command, $cwd, $url, $path, $verbose); } /** * Execute an SVN local command and try to fix up the process with credentials * if necessary. * * @param string $command SVN command to run * @param string $path Path argument passed thru to the command * @param string $cwd Working directory * @param bool $verbose Output all output to the user * * @throws \RuntimeException * @return string */ public function executeLocal($command, $path, $cwd = null, $verbose = false) { // A local command has no remote url return $this->executeWithAuthRetry($command, $cwd, '', $path, $verbose); } /** * @param string $svnCommand * @param string $cwd * @param string $url * @param string $path * @param bool $verbose * * @return ?string */ private function executeWithAuthRetry($svnCommand, $cwd, $url, $path, $verbose) { // Regenerate the command at each try, to use the newly user-provided credentials $command = $this->getCommand($svnCommand, $url, $path); $output = null; $io = $this->io; $handler = function ($type, $buffer) use (&$output, $io, $verbose) { if ($type !== 'out') { return null; } if (strpos($buffer, 'Redirecting to URL ') === 0) { return null; } $output .= $buffer; if ($verbose) { $io->writeError($buffer, false); } }; $status = $this->process->execute($command, $handler, $cwd); if (0 === $status) { return $output; } $errorOutput = $this->process->getErrorOutput(); $fullOutput = implode("\n", array($output, $errorOutput)); // the error is not auth-related if (false === stripos($fullOutput, 'Could not authenticate to server:') && false === stripos($fullOutput, 'authorization failed') && false === stripos($fullOutput, 'svn: E170001:') && false === stripos($fullOutput, 'svn: E215004:')) { throw new \RuntimeException($fullOutput); } if (!$this->hasAuth()) { $this->doAuthDance(); } // try to authenticate if maximum quantity of tries not reached if ($this->qtyAuthTries++ < self::MAX_QTY_AUTH_TRIES) { // restart the process return $this->executeWithAuthRetry($svnCommand, $cwd, $url, $path, $verbose); } throw new \RuntimeException( 'wrong credentials provided ('.$fullOutput.')' ); } /** * @param bool $cacheCredentials * @return void */ public function setCacheCredentials($cacheCredentials) { $this->cacheCredentials = $cacheCredentials; } /** * Repositories requests credentials, let's put them in. * * @throws \RuntimeException * @return \Composer\Util\Svn */ protected function doAuthDance() { // cannot ask for credentials in non interactive mode if (!$this->io->isInteractive()) { throw new \RuntimeException( 'can not ask for authentication in non interactive mode' ); } $this->io->writeError("The Subversion server ({$this->url}) requested credentials:"); $this->hasAuth = true; $this->credentials = array( 'username' => (string) $this->io->ask("Username: ", ''), 'password' => (string) $this->io->askAndHideAnswer("Password: "), ); $this->cacheCredentials = $this->io->askConfirmation("Should Subversion cache these credentials? (yes/no) "); return $this; } /** * A method to create the svn commands run. * * @param string $cmd Usually 'svn ls' or something like that. * @param string $url Repo URL. * @param string $path Target for a checkout * * @return string */ protected function getCommand($cmd, $url, $path = null) { $cmd = sprintf( '%s %s%s -- %s', $cmd, '--non-interactive ', $this->getCredentialString(), ProcessExecutor::escape($url) ); if ($path) { $cmd .= ' ' . ProcessExecutor::escape($path); } return $cmd; } /** * Return the credential string for the svn command. * * Adds --no-auth-cache when credentials are present. * * @return string */ protected function getCredentialString() { if (!$this->hasAuth()) { return ''; } return sprintf( ' %s--username %s --password %s ', $this->getAuthCache(), ProcessExecutor::escape($this->getUsername()), ProcessExecutor::escape($this->getPassword()) ); } /** * Get the password for the svn command. Can be empty. * * @throws \LogicException * @return string */ protected function getPassword() { if ($this->credentials === null) { throw new \LogicException("No svn auth detected."); } return $this->credentials['password']; } /** * Get the username for the svn command. * * @throws \LogicException * @return string */ protected function getUsername() { if ($this->credentials === null) { throw new \LogicException("No svn auth detected."); } return $this->credentials['username']; } /** * Detect Svn Auth. * * @return bool */ protected function hasAuth() { if (null !== $this->hasAuth) { return $this->hasAuth; } if (false === $this->createAuthFromConfig()) { $this->createAuthFromUrl(); } return (bool) $this->hasAuth; } /** * Return the no-auth-cache switch. * * @return string */ protected function getAuthCache() { return $this->cacheCredentials ? '' : '--no-auth-cache '; } /** * Create the auth params from the configuration file. * * @return bool */ private function createAuthFromConfig() { if (!$this->config->has('http-basic')) { return $this->hasAuth = false; } $authConfig = $this->config->get('http-basic'); $host = parse_url($this->url, PHP_URL_HOST); if (isset($authConfig[$host])) { $this->credentials = array( 'username' => $authConfig[$host]['username'], 'password' => $authConfig[$host]['password'], ); return $this->hasAuth = true; } return $this->hasAuth = false; } /** * Create the auth params from the url * * @return bool */ private function createAuthFromUrl() { $uri = parse_url($this->url); if (empty($uri['user'])) { return $this->hasAuth = false; } $this->credentials = array( 'username' => $uri['user'], 'password' => !empty($uri['pass']) ? $uri['pass'] : '', ); return $this->hasAuth = true; } /** * Returns the version of the svn binary contained in PATH * * @return string|null */ public function binaryVersion() { if (!self::$version) { if (0 === $this->process->execute('svn --version', $output)) { if (Preg::isMatch('{(\d+(?:\.\d+)+)}', $output, $match)) { self::$version = $match[1]; } } } return self::$version; } } * Jordi Boggiano * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Composer\Util; use Composer\IO\IOInterface; use Composer\Config; use Composer\Factory; use Composer\Downloader\TransportException; use Composer\Pcre\Preg; /** * @author Roshan Gautam */ class GitLab { /** @var IOInterface */ protected $io; /** @var Config */ protected $config; /** @var ProcessExecutor */ protected $process; /** @var HttpDownloader */ protected $httpDownloader; /** * Constructor. * * @param IOInterface $io The IO instance * @param Config $config The composer configuration * @param ProcessExecutor $process Process instance, injectable for mocking * @param HttpDownloader $httpDownloader Remote Filesystem, injectable for mocking */ public function __construct(IOInterface $io, Config $config, ProcessExecutor $process = null, HttpDownloader $httpDownloader = null) { $this->io = $io; $this->config = $config; $this->process = $process ?: new ProcessExecutor($io); $this->httpDownloader = $httpDownloader ?: Factory::createHttpDownloader($this->io, $config); } /** * Attempts to authorize a GitLab domain via OAuth. * * @param string $originUrl The host this GitLab instance is located at * * @return bool true on success */ public function authorizeOAuth($originUrl) { // before composer 1.9, origin URLs had no port number in them $bcOriginUrl = Preg::replace('{:\d+}', '', $originUrl); if (!in_array($originUrl, $this->config->get('gitlab-domains'), true) && !in_array($bcOriginUrl, $this->config->get('gitlab-domains'), true)) { return false; } // if available use token from git config if (0 === $this->process->execute('git config gitlab.accesstoken', $output)) { $this->io->setAuthentication($originUrl, trim($output), 'oauth2'); return true; } // if available use deploy token from git config if (0 === $this->process->execute('git config gitlab.deploytoken.user', $tokenUser) && 0 === $this->process->execute('git config gitlab.deploytoken.token', $tokenPassword)) { $this->io->setAuthentication($originUrl, trim($tokenUser), trim($tokenPassword)); return true; } // if available use token from composer config $authTokens = $this->config->get('gitlab-token'); if (isset($authTokens[$originUrl])) { $token = $authTokens[$originUrl]; } if (isset($authTokens[$bcOriginUrl])) { $token = $authTokens[$bcOriginUrl]; } if (isset($token)) { $username = is_array($token) && array_key_exists("username", $token) ? $token["username"] : $token; $password = is_array($token) && array_key_exists("token", $token) ? $token["token"] : 'private-token'; $this->io->setAuthentication($originUrl, $username, $password); return true; } return false; } /** * Authorizes a GitLab domain interactively via OAuth. * * @param string $scheme Scheme used in the origin URL * @param string $originUrl The host this GitLab instance is located at * @param string $message The reason this authorization is required * * @throws \RuntimeException * @throws TransportException|\Exception * * @return bool true on success */ public function authorizeOAuthInteractively($scheme, $originUrl, $message = null) { if ($message) { $this->io->writeError($message); } $this->io->writeError(sprintf('A token will be created and stored in "%s", your password will never be stored', $this->config->getAuthConfigSource()->getName())); $this->io->writeError('To revoke access to this token you can visit '.$scheme.'://'.$originUrl.'/-/profile/personal_access_tokens'); $attemptCounter = 0; while ($attemptCounter++ < 5) { try { $response = $this->createToken($scheme, $originUrl); } catch (TransportException $e) { // 401 is bad credentials, // 403 is max login attempts exceeded if (in_array($e->getCode(), array(403, 401))) { if (401 === $e->getCode()) { $response = json_decode($e->getResponse(), true); if (isset($response['error']) && $response['error'] === 'invalid_grant') { $this->io->writeError('Bad credentials. If you have two factor authentication enabled you will have to manually create a personal access token'); } else { $this->io->writeError('Bad credentials.'); } } else { $this->io->writeError('Maximum number of login attempts exceeded. Please try again later.'); } $this->io->writeError('You can also manually create a personal access token enabling the "read_api" scope at '.$scheme.'://'.$originUrl.'/profile/personal_access_tokens'); $this->io->writeError('Add it using "composer config --global --auth gitlab-token.'.$originUrl.' "'); continue; } throw $e; } $this->io->setAuthentication($originUrl, $response['access_token'], 'oauth2'); // store value in user config in auth file $this->config->getAuthConfigSource()->addConfigSetting('gitlab-oauth.'.$originUrl, $response['access_token']); return true; } throw new \RuntimeException('Invalid GitLab credentials 5 times in a row, aborting.'); } /** * @param string $scheme * @param string $originUrl * * @return array{access_token: non-empty-string, token_type: non-empty-string, expires_in: positive-int} * * @see https://docs.gitlab.com/ee/api/oauth2.html#resource-owner-password-credentials-flow */ private function createToken($scheme, $originUrl) { $username = $this->io->ask('Username: '); $password = $this->io->askAndHideAnswer('Password: '); $headers = array('Content-Type: application/x-www-form-urlencoded'); $apiUrl = $originUrl; $data = http_build_query(array( 'username' => $username, 'password' => $password, 'grant_type' => 'password', ), '', '&'); $options = array( 'retry-auth-failure' => false, 'http' => array( 'method' => 'POST', 'header' => $headers, 'content' => $data, ), ); $token = $this->httpDownloader->get($scheme.'://'.$apiUrl.'/oauth/token', $options)->decodeJson(); $this->io->writeError('Token successfully created'); return $token; } } * Jordi Boggiano * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Composer\Util; /** * @author Andreas Schempp */ class Zip { /** * Gets content of the root composer.json inside a ZIP archive. * * @param string $pathToZip * * @return string|null */ public static function getComposerJson($pathToZip) { if (!extension_loaded('zip')) { throw new \RuntimeException('The Zip Util requires PHP\'s zip extension'); } $zip = new \ZipArchive(); if ($zip->open($pathToZip) !== true) { return null; } if (0 == $zip->numFiles) { $zip->close(); return null; } $foundFileIndex = self::locateFile($zip, 'composer.json'); $content = null; $configurationFileName = $zip->getNameIndex($foundFileIndex); $stream = $zip->getStream($configurationFileName); if (false !== $stream) { $content = stream_get_contents($stream); } $zip->close(); return $content; } /** * Find a file by name, returning the one that has the shortest path. * * @param \ZipArchive $zip * @param string $filename * @throws \RuntimeException * * @return int */ private static function locateFile(\ZipArchive $zip, $filename) { // return root composer.json if it is there and is a file if (false !== ($index = $zip->locateName($filename)) && $zip->getFromIndex($index) !== false) { return $index; } $topLevelPaths = array(); for ($i = 0; $i < $zip->numFiles; $i++) { $name = $zip->getNameIndex($i); $dirname = dirname($name); // ignore OSX specific resource fork folder if (strpos($name, '__MACOSX') !== false) { continue; } // handle archives with proper TOC if ($dirname === '.') { $topLevelPaths[$name] = true; if (\count($topLevelPaths) > 1) { throw new \RuntimeException('Archive has more than one top level directories, and no composer.json was found on the top level, so it\'s an invalid archive. Top level paths found were: '.implode(',', array_keys($topLevelPaths))); } continue; } // handle archives which do not have a TOC record for the directory itself if (false === strpos($dirname, '\\') && false === strpos($dirname, '/')) { $topLevelPaths[$dirname.'/'] = true; if (\count($topLevelPaths) > 1) { throw new \RuntimeException('Archive has more than one top level directories, and no composer.json was found on the top level, so it\'s an invalid archive. Top level paths found were: '.implode(',', array_keys($topLevelPaths))); } } } if ($topLevelPaths && false !== ($index = $zip->locateName(key($topLevelPaths).$filename)) && $zip->getFromIndex($index) !== false) { return $index; } throw new \RuntimeException('No composer.json found either at the top level or within the topmost directory'); } } * Jordi Boggiano * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Composer\Util; use Composer\Config; use Composer\IO\IOInterface; use Composer\Pcre\Preg; /** * @author Jordi Boggiano */ class Git { /** @var string|false|null */ private static $version = false; /** @var IOInterface */ protected $io; /** @var Config */ protected $config; /** @var ProcessExecutor */ protected $process; /** @var Filesystem */ protected $filesystem; public function __construct(IOInterface $io, Config $config, ProcessExecutor $process, Filesystem $fs) { $this->io = $io; $this->config = $config; $this->process = $process; $this->filesystem = $fs; } /** * @param callable $commandCallable * @param string $url * @param string|null $cwd * @param bool $initialClone * * @return void */ public function runCommand($commandCallable, $url, $cwd, $initialClone = false) { // Ensure we are allowed to use this URL by config $this->config->prohibitUrlByConfig($url, $this->io); if ($initialClone) { $origCwd = $cwd; $cwd = null; } if (Preg::isMatch('{^ssh://[^@]+@[^:]+:[^0-9]+}', $url)) { throw new \InvalidArgumentException('The source URL ' . $url . ' is invalid, ssh URLs should have a port number after ":".' . "\n" . 'Use ssh://git@example.com:22/path or just git@example.com:path if you do not want to provide a password or custom port.'); } if (!$initialClone) { // capture username/password from URL if there is one and we have no auth configured yet $this->process->execute('git remote -v', $output, $cwd); if (Preg::isMatch('{^(?:composer|origin)\s+https?://(.+):(.+)@([^/]+)}im', $output, $match) && !$this->io->hasAuthentication($match[3])) { $this->io->setAuthentication($match[3], rawurldecode($match[1]), rawurldecode($match[2])); } } $protocols = $this->config->get('github-protocols'); if (!is_array($protocols)) { throw new \RuntimeException('Config value "github-protocols" must be an array, got ' . gettype($protocols)); } // public github, autoswitch protocols if (Preg::isMatch('{^(?:https?|git)://' . self::getGitHubDomainsRegex($this->config) . '/(.*)}', $url, $match)) { $messages = array(); foreach ($protocols as $protocol) { if ('ssh' === $protocol) { $protoUrl = "git@" . $match[1] . ":" . $match[2]; } else { $protoUrl = $protocol . "://" . $match[1] . "/" . $match[2]; } if (0 === $this->process->execute(call_user_func($commandCallable, $protoUrl), $ignoredOutput, $cwd)) { return; } $messages[] = '- ' . $protoUrl . "\n" . Preg::replace('#^#m', ' ', $this->process->getErrorOutput()); if ($initialClone && isset($origCwd)) { $this->filesystem->removeDirectory($origCwd); } } // failed to checkout, first check git accessibility if (!$this->io->hasAuthentication($match[1]) && !$this->io->isInteractive()) { $this->throwException('Failed to clone ' . $url . ' via ' . implode(', ', $protocols) . ' protocols, aborting.' . "\n\n" . implode("\n", $messages), $url); } } // if we have a private github url and the ssh protocol is disabled then we skip it and directly fallback to https $bypassSshForGitHub = Preg::isMatch('{^git@' . self::getGitHubDomainsRegex($this->config) . ':(.+?)\.git$}i', $url) && !in_array('ssh', $protocols, true); $command = call_user_func($commandCallable, $url); $auth = null; $credentials = array(); if ($bypassSshForGitHub || 0 !== $this->process->execute($command, $ignoredOutput, $cwd)) { $errorMsg = $this->process->getErrorOutput(); // private github repository without ssh key access, try https with auth if (Preg::isMatch('{^git@' . self::getGitHubDomainsRegex($this->config) . ':(.+?)\.git$}i', $url, $match) || Preg::isMatch('{^https?://' . self::getGitHubDomainsRegex($this->config) . '/(.*?)(?:\.git)?$}i', $url, $match) ) { if (!$this->io->hasAuthentication($match[1])) { $gitHubUtil = new GitHub($this->io, $this->config, $this->process); $message = 'Cloning failed using an ssh key for authentication, enter your GitHub credentials to access private repos'; if (!$gitHubUtil->authorizeOAuth($match[1]) && $this->io->isInteractive()) { $gitHubUtil->authorizeOAuthInteractively($match[1], $message); } } if ($this->io->hasAuthentication($match[1])) { $auth = $this->io->getAuthentication($match[1]); $authUrl = 'https://' . rawurlencode($auth['username']) . ':' . rawurlencode($auth['password']) . '@' . $match[1] . '/' . $match[2] . '.git'; $command = call_user_func($commandCallable, $authUrl); if (0 === $this->process->execute($command, $ignoredOutput, $cwd)) { return; } $credentials = array(rawurlencode($auth['username']), rawurlencode($auth['password'])); $errorMsg = $this->process->getErrorOutput(); } } elseif (Preg::isMatch('{^https://(bitbucket\.org)/(.*?)(?:\.git)?$}i', $url, $match)) { //bitbucket oauth $bitbucketUtil = new Bitbucket($this->io, $this->config, $this->process); if (!$this->io->hasAuthentication($match[1])) { $message = 'Enter your Bitbucket credentials to access private repos'; if (!$bitbucketUtil->authorizeOAuth($match[1]) && $this->io->isInteractive()) { $bitbucketUtil->authorizeOAuthInteractively($match[1], $message); $accessToken = $bitbucketUtil->getToken(); $this->io->setAuthentication($match[1], 'x-token-auth', $accessToken); } } else { //We're authenticating with a locally stored consumer. $auth = $this->io->getAuthentication($match[1]); //We already have an access_token from a previous request. if ($auth['username'] !== 'x-token-auth') { $accessToken = $bitbucketUtil->requestToken($match[1], $auth['username'], $auth['password']); if (!empty($accessToken)) { $this->io->setAuthentication($match[1], 'x-token-auth', $accessToken); } } } if ($this->io->hasAuthentication($match[1])) { $auth = $this->io->getAuthentication($match[1]); $authUrl = 'https://' . rawurlencode($auth['username']) . ':' . rawurlencode($auth['password']) . '@' . $match[1] . '/' . $match[2] . '.git'; $command = call_user_func($commandCallable, $authUrl); if (0 === $this->process->execute($command, $ignoredOutput, $cwd)) { return; } $credentials = array(rawurlencode($auth['username']), rawurlencode($auth['password'])); $errorMsg = $this->process->getErrorOutput(); } else { // Falling back to ssh $sshUrl = 'git@bitbucket.org:' . $match[2] . '.git'; $this->io->writeError(' No bitbucket authentication configured. Falling back to ssh.'); $command = call_user_func($commandCallable, $sshUrl); if (0 === $this->process->execute($command, $ignoredOutput, $cwd)) { return; } $errorMsg = $this->process->getErrorOutput(); } } elseif ( Preg::isMatch('{^(git)@' . self::getGitLabDomainsRegex($this->config) . ':(.+?\.git)$}i', $url, $match) || Preg::isMatch('{^(https?)://' . self::getGitLabDomainsRegex($this->config) . '/(.*)}i', $url, $match) ) { if ($match[1] === 'git') { $match[1] = 'https'; } if (!$this->io->hasAuthentication($match[2])) { $gitLabUtil = new GitLab($this->io, $this->config, $this->process); $message = 'Cloning failed, enter your GitLab credentials to access private repos'; if (!$gitLabUtil->authorizeOAuth($match[2]) && $this->io->isInteractive()) { $gitLabUtil->authorizeOAuthInteractively($match[1], $match[2], $message); } } if ($this->io->hasAuthentication($match[2])) { $auth = $this->io->getAuthentication($match[2]); if ($auth['password'] === 'private-token' || $auth['password'] === 'oauth2' || $auth['password'] === 'gitlab-ci-token') { $authUrl = $match[1] . '://' . rawurlencode($auth['password']) . ':' . rawurlencode($auth['username']) . '@' . $match[2] . '/' . $match[3]; // swap username and password } else { $authUrl = $match[1] . '://' . rawurlencode($auth['username']) . ':' . rawurlencode($auth['password']) . '@' . $match[2] . '/' . $match[3]; } $command = call_user_func($commandCallable, $authUrl); if (0 === $this->process->execute($command, $ignoredOutput, $cwd)) { return; } $credentials = array(rawurlencode($auth['username']), rawurlencode($auth['password'])); $errorMsg = $this->process->getErrorOutput(); } } elseif ($this->isAuthenticationFailure($url, $match)) { // private non-github/gitlab/bitbucket repo that failed to authenticate if (strpos($match[2], '@')) { list($authParts, $match[2]) = explode('@', $match[2], 2); } $storeAuth = false; if ($this->io->hasAuthentication($match[2])) { $auth = $this->io->getAuthentication($match[2]); } elseif ($this->io->isInteractive()) { $defaultUsername = null; if (isset($authParts) && $authParts) { if (false !== strpos($authParts, ':')) { list($defaultUsername, ) = explode(':', $authParts, 2); } else { $defaultUsername = $authParts; } } $this->io->writeError(' Authentication required (' . $match[2] . '):'); $auth = array( 'username' => $this->io->ask(' Username: ', $defaultUsername), 'password' => $this->io->askAndHideAnswer(' Password: '), ); $storeAuth = $this->config->get('store-auths'); } if ($auth) { $authUrl = $match[1] . rawurlencode($auth['username']) . ':' . rawurlencode($auth['password']) . '@' . $match[2] . $match[3]; $command = call_user_func($commandCallable, $authUrl); if (0 === $this->process->execute($command, $ignoredOutput, $cwd)) { $this->io->setAuthentication($match[2], $auth['username'], $auth['password']); $authHelper = new AuthHelper($this->io, $this->config); $authHelper->storeAuth($match[2], $storeAuth); return; } $credentials = array(rawurlencode($auth['username']), rawurlencode($auth['password'])); $errorMsg = $this->process->getErrorOutput(); } } if ($initialClone && isset($origCwd)) { $this->filesystem->removeDirectory($origCwd); } if (count($credentials) > 0) { $command = $this->maskCredentials($command, $credentials); $errorMsg = $this->maskCredentials($errorMsg, $credentials); } $this->throwException('Failed to execute ' . $command . "\n\n" . $errorMsg, $url); } } /** * @param string $url * @param string $dir * * @return bool */ public function syncMirror($url, $dir) { if (Platform::getEnv('COMPOSER_DISABLE_NETWORK') && Platform::getEnv('COMPOSER_DISABLE_NETWORK') !== 'prime') { $this->io->writeError('Aborting git mirror sync of '.$url.' as network is disabled'); return false; } // update the repo if it is a valid git repository if (is_dir($dir) && 0 === $this->process->execute('git rev-parse --git-dir', $output, $dir) && trim($output) === '.') { try { $commandCallable = function ($url) { $sanitizedUrl = Preg::replace('{://([^@]+?):(.+?)@}', '://', $url); return sprintf('git remote set-url origin -- %s && git remote update --prune origin && git remote set-url origin -- %s && git gc --auto', ProcessExecutor::escape($url), ProcessExecutor::escape($sanitizedUrl)); }; $this->runCommand($commandCallable, $url, $dir); } catch (\Exception $e) { $this->io->writeError('Sync mirror failed: ' . $e->getMessage() . '', true, IOInterface::DEBUG); return false; } return true; } // clean up directory and do a fresh clone into it $this->filesystem->removeDirectory($dir); $commandCallable = function ($url) use ($dir) { return sprintf('git clone --mirror -- %s %s', ProcessExecutor::escape($url), ProcessExecutor::escape($dir)); }; $this->runCommand($commandCallable, $url, $dir, true); return true; } /** * @param string $url * @param string $dir * @param string $ref * * @return bool */ public function fetchRefOrSyncMirror($url, $dir, $ref) { if ($this->checkRefIsInMirror($dir, $ref)) { return true; } if ($this->syncMirror($url, $dir)) { return $this->checkRefIsInMirror($dir, $ref); } return false; } /** * @return string */ public static function getNoShowSignatureFlag(ProcessExecutor $process) { $gitVersion = self::getVersion($process); if ($gitVersion && version_compare($gitVersion, '2.10.0-rc0', '>=')) { return ' --no-show-signature'; } return ''; } /** * @param string $dir * @param string $ref * * @return bool */ private function checkRefIsInMirror($dir, $ref) { if (is_dir($dir) && 0 === $this->process->execute('git rev-parse --git-dir', $output, $dir) && trim($output) === '.') { $escapedRef = ProcessExecutor::escape($ref.'^{commit}'); $exitCode = $this->process->execute(sprintf('git rev-parse --quiet --verify %s', $escapedRef), $ignoredOutput, $dir); if ($exitCode === 0) { return true; } } return false; } /** * @param string $url * @param string[] $match * * @return bool */ private function isAuthenticationFailure($url, &$match) { if (!Preg::isMatch('{^(https?://)([^/]+)(.*)$}i', $url, $match)) { return false; } $authFailures = array( 'fatal: Authentication failed', 'remote error: Invalid username or password.', 'error: 401 Unauthorized', 'fatal: unable to access', 'fatal: could not read Username', ); $errorOutput = $this->process->getErrorOutput(); foreach ($authFailures as $authFailure) { if (strpos($errorOutput, $authFailure) !== false) { return true; } } return false; } /** * @return void */ public static function cleanEnv() { if (PHP_VERSION_ID < 50400 && ini_get('safe_mode') && false === strpos(ini_get('safe_mode_allowed_env_vars'), 'GIT_ASKPASS')) { throw new \RuntimeException('safe_mode is enabled and safe_mode_allowed_env_vars does not contain GIT_ASKPASS, can not set env var. You can disable safe_mode with "-dsafe_mode=0" when running composer'); } // added in git 1.7.1, prevents prompting the user for username/password if (Platform::getEnv('GIT_ASKPASS') !== 'echo') { Platform::putEnv('GIT_ASKPASS', 'echo'); } // clean up rogue git env vars in case this is running in a git hook if (Platform::getEnv('GIT_DIR')) { Platform::clearEnv('GIT_DIR'); } if (Platform::getEnv('GIT_WORK_TREE')) { Platform::clearEnv('GIT_WORK_TREE'); } // Run processes with predictable LANGUAGE if (Platform::getEnv('LANGUAGE') !== 'C') { Platform::putEnv('LANGUAGE', 'C'); } // clean up env for OSX, see https://github.com/composer/composer/issues/2146#issuecomment-35478940 Platform::clearEnv('DYLD_LIBRARY_PATH'); } /** * @return non-empty-string */ public static function getGitHubDomainsRegex(Config $config) { return '(' . implode('|', array_map('preg_quote', $config->get('github-domains'))) . ')'; } /** * @return non-empty-string */ public static function getGitLabDomainsRegex(Config $config) { return '(' . implode('|', array_map('preg_quote', $config->get('gitlab-domains'))) . ')'; } /** * @param non-empty-string $message * @param string $url * * @return never */ private function throwException($message, $url) { // git might delete a directory when it fails and php will not know clearstatcache(); if (0 !== $this->process->execute('git --version', $ignoredOutput)) { throw new \RuntimeException(Url::sanitize('Failed to clone ' . $url . ', git was not found, check that it is installed and in your PATH env.' . "\n\n" . $this->process->getErrorOutput())); } throw new \RuntimeException(Url::sanitize($message)); } /** * Retrieves the current git version. * * @return string|null The git version number, if present. */ public static function getVersion(ProcessExecutor $process) { if (false === self::$version) { self::$version = null; if (0 === $process->execute('git --version', $output) && Preg::isMatch('/^git version (\d+(?:\.\d+)+)/m', $output, $matches)) { self::$version = $matches[1]; } } return self::$version; } /** * @param string $error * @param string[] $credentials * * @return string */ private function maskCredentials($error, array $credentials) { $maskedCredentials = array(); foreach ($credentials as $credential) { if (in_array($credential, array('private-token', 'x-token-auth', 'oauth2', 'gitlab-ci-token', 'x-oauth-basic'))) { $maskedCredentials[] = $credential; } elseif (strlen($credential) > 6) { $maskedCredentials[] = substr($credential, 0, 3) . '...' . substr($credential, -3); } elseif (strlen($credential) > 3) { $maskedCredentials[] = substr($credential, 0, 3) . '...'; } else { $maskedCredentials[] = 'XXX'; } } return str_replace($credentials, $maskedCredentials, $error); } } * Jordi Boggiano * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Composer\Util; use Composer\Config; use Composer\Downloader\MaxFileSizeExceededException; use Composer\IO\IOInterface; use Composer\Downloader\TransportException; use Composer\CaBundle\CaBundle; use Composer\Pcre\Preg; use Composer\Util\Http\Response; use Composer\Util\Http\ProxyManager; /** * @internal * @author François Pluchino * @author Jordi Boggiano * @author Nils Adermann */ class RemoteFilesystem { /** @var IOInterface */ private $io; /** @var Config */ private $config; /** @var string */ private $scheme; /** @var int */ private $bytesMax; /** @var string */ private $originUrl; /** @var string */ private $fileUrl; /** @var ?string */ private $fileName; /** @var bool */ private $retry = false; /** @var bool */ private $progress; /** @var ?int */ private $lastProgress; /** @var mixed[] */ private $options = array(); /** @var array */ private $peerCertificateMap = array(); /** @var bool */ private $disableTls = false; /** @var string[] */ private $lastHeaders; /** @var bool */ private $storeAuth = false; /** @var AuthHelper */ private $authHelper; /** @var bool */ private $degradedMode = false; /** @var int */ private $redirects; /** @var int */ private $maxRedirects = 20; /** @var ProxyManager */ private $proxyManager; /** * Constructor. * * @param IOInterface $io The IO instance * @param Config $config The config * @param mixed[] $options The options * @param bool $disableTls * @param AuthHelper $authHelper */ public function __construct(IOInterface $io, Config $config, array $options = array(), $disableTls = false, AuthHelper $authHelper = null) { $this->io = $io; // Setup TLS options // The cafile option can be set via config.json if ($disableTls === false) { $this->options = StreamContextFactory::getTlsDefaults($options, $io); } else { $this->disableTls = true; } // handle the other externally set options normally. $this->options = array_replace_recursive($this->options, $options); $this->config = $config; $this->authHelper = isset($authHelper) ? $authHelper : new AuthHelper($io, $config); $this->proxyManager = ProxyManager::getInstance(); } /** * Copy the remote file in local. * * @param string $originUrl The origin URL * @param string $fileUrl The file URL * @param string $fileName the local filename * @param bool $progress Display the progression * @param mixed[] $options Additional context options * * @return bool true */ public function copy($originUrl, $fileUrl, $fileName, $progress = true, $options = array()) { return $this->get($originUrl, $fileUrl, $options, $fileName, $progress); } /** * Get the content. * * @param string $originUrl The origin URL * @param string $fileUrl The file URL * @param bool $progress Display the progression * @param mixed[] $options Additional context options * * @return bool|string The content */ public function getContents($originUrl, $fileUrl, $progress = true, $options = array()) { return $this->get($originUrl, $fileUrl, $options, null, $progress); } /** * Retrieve the options set in the constructor * * @return mixed[] Options */ public function getOptions() { return $this->options; } /** * Merges new options * * @param mixed[] $options * @return void */ public function setOptions(array $options) { $this->options = array_replace_recursive($this->options, $options); } /** * Check is disable TLS. * * @return bool */ public function isTlsDisabled() { return $this->disableTls === true; } /** * Returns the headers of the last request * * @return string[] */ public function getLastHeaders() { return $this->lastHeaders; } /** * @param string[] $headers array of returned headers like from getLastHeaders() * @return int|null */ public static function findStatusCode(array $headers) { $value = null; foreach ($headers as $header) { if (Preg::isMatch('{^HTTP/\S+ (\d+)}i', $header, $match)) { // In case of redirects, http_response_headers contains the headers of all responses // so we can not return directly and need to keep iterating $value = (int) $match[1]; } } return $value; } /** * @param string[] $headers array of returned headers like from getLastHeaders() * @return string|null */ public function findStatusMessage(array $headers) { $value = null; foreach ($headers as $header) { if (Preg::isMatch('{^HTTP/\S+ \d+}i', $header)) { // In case of redirects, http_response_headers contains the headers of all responses // so we can not return directly and need to keep iterating $value = $header; } } return $value; } /** * Get file content or copy action. * * @param string $originUrl The origin URL * @param string $fileUrl The file URL * @param mixed[] $additionalOptions context options * @param string $fileName the local filename * @param bool $progress Display the progression * * @throws TransportException|\Exception * @throws TransportException When the file could not be downloaded * * @return bool|string */ protected function get($originUrl, $fileUrl, $additionalOptions = array(), $fileName = null, $progress = true) { $this->scheme = parse_url($fileUrl, PHP_URL_SCHEME); $this->bytesMax = 0; $this->originUrl = $originUrl; $this->fileUrl = $fileUrl; $this->fileName = $fileName; $this->progress = $progress; $this->lastProgress = null; $retryAuthFailure = true; $this->lastHeaders = array(); $this->redirects = 1; // The first request counts. $tempAdditionalOptions = $additionalOptions; if (isset($tempAdditionalOptions['retry-auth-failure'])) { $retryAuthFailure = (bool) $tempAdditionalOptions['retry-auth-failure']; unset($tempAdditionalOptions['retry-auth-failure']); } $isRedirect = false; if (isset($tempAdditionalOptions['redirects'])) { $this->redirects = $tempAdditionalOptions['redirects']; $isRedirect = true; unset($tempAdditionalOptions['redirects']); } $options = $this->getOptionsForUrl($originUrl, $tempAdditionalOptions); unset($tempAdditionalOptions); $origFileUrl = $fileUrl; if (isset($options['gitlab-token'])) { $fileUrl .= (false === strpos($fileUrl, '?') ? '?' : '&') . 'access_token='.$options['gitlab-token']; unset($options['gitlab-token']); } if (isset($options['http'])) { $options['http']['ignore_errors'] = true; } if ($this->degradedMode && strpos($fileUrl, 'http://repo.packagist.org/') === 0) { // access packagist using the resolved IPv4 instead of the hostname to force IPv4 protocol $fileUrl = 'http://' . gethostbyname('repo.packagist.org') . substr($fileUrl, 20); $degradedPackagist = true; } $maxFileSize = null; if (isset($options['max_file_size'])) { $maxFileSize = $options['max_file_size']; unset($options['max_file_size']); } $ctx = StreamContextFactory::getContext($fileUrl, $options, array('notification' => array($this, 'callbackGet'))); $proxy = $this->proxyManager->getProxyForRequest($fileUrl); $usingProxy = $proxy->getFormattedUrl(' using proxy (%s)'); $this->io->writeError((strpos($origFileUrl, 'http') === 0 ? 'Downloading ' : 'Reading ') . Url::sanitize($origFileUrl) . $usingProxy, true, IOInterface::DEBUG); unset($origFileUrl, $proxy, $usingProxy); // Check for secure HTTP, but allow insecure Packagist calls to $hashed providers as file integrity is verified with sha256 if ((!Preg::isMatch('{^http://(repo\.)?packagist\.org/p/}', $fileUrl) || (false === strpos($fileUrl, '$') && false === strpos($fileUrl, '%24'))) && empty($degradedPackagist)) { $this->config->prohibitUrlByConfig($fileUrl, $this->io); } if ($this->progress && !$isRedirect) { $this->io->writeError("Downloading (connecting...)", false); } $errorMessage = ''; $errorCode = 0; $result = false; set_error_handler(function ($code, $msg) use (&$errorMessage) { if ($errorMessage) { $errorMessage .= "\n"; } $errorMessage .= Preg::replace('{^file_get_contents\(.*?\): }', '', $msg); return true; }); $http_response_header = array(); try { $result = $this->getRemoteContents($originUrl, $fileUrl, $ctx, $http_response_header, $maxFileSize); if (!empty($http_response_header[0])) { $statusCode = self::findStatusCode($http_response_header); if ($statusCode >= 400 && Response::findHeaderValue($http_response_header, 'content-type') === 'application/json') { HttpDownloader::outputWarnings($this->io, $originUrl, json_decode($result, true)); } if (in_array($statusCode, array(401, 403)) && $retryAuthFailure) { $this->promptAuthAndRetry($statusCode, $this->findStatusMessage($http_response_header), $http_response_header); } } $contentLength = !empty($http_response_header[0]) ? Response::findHeaderValue($http_response_header, 'content-length') : null; if ($contentLength && Platform::strlen($result) < $contentLength) { // alas, this is not possible via the stream callback because STREAM_NOTIFY_COMPLETED is documented, but not implemented anywhere in PHP $e = new TransportException('Content-Length mismatch, received '.Platform::strlen($result).' bytes out of the expected '.$contentLength); $e->setHeaders($http_response_header); $e->setStatusCode(self::findStatusCode($http_response_header)); try { $e->setResponse($this->decodeResult($result, $http_response_header)); } catch (\Exception $discarded) { $e->setResponse($this->normalizeResult($result)); } $this->io->writeError('Content-Length mismatch, received '.Platform::strlen($result).' out of '.$contentLength.' bytes: (' . base64_encode($result).')', true, IOInterface::DEBUG); throw $e; } if (PHP_VERSION_ID < 50600 && !empty($options['ssl']['peer_fingerprint'])) { // Emulate fingerprint validation on PHP < 5.6 $params = stream_context_get_params($ctx); $expectedPeerFingerprint = $options['ssl']['peer_fingerprint']; $peerFingerprint = TlsHelper::getCertificateFingerprint($params['options']['ssl']['peer_certificate']); // Constant time compare??! if ($expectedPeerFingerprint !== $peerFingerprint) { throw new TransportException('Peer fingerprint did not match'); } } } catch (\Exception $e) { if ($e instanceof TransportException && !empty($http_response_header[0])) { $e->setHeaders($http_response_header); $e->setStatusCode(self::findStatusCode($http_response_header)); } if ($e instanceof TransportException && $result !== false) { $e->setResponse($this->decodeResult($result, $http_response_header)); } $result = false; } if ($errorMessage && !filter_var(ini_get('allow_url_fopen'), FILTER_VALIDATE_BOOLEAN)) { $errorMessage = 'allow_url_fopen must be enabled in php.ini ('.$errorMessage.')'; } restore_error_handler(); if (isset($e) && !$this->retry) { if (!$this->degradedMode && false !== strpos($e->getMessage(), 'Operation timed out')) { $this->degradedMode = true; $this->io->writeError(''); $this->io->writeError(array( ''.$e->getMessage().'', 'Retrying with degraded mode, check https://getcomposer.org/doc/articles/troubleshooting.md#degraded-mode for more info', )); return $this->get($this->originUrl, $this->fileUrl, $additionalOptions, $this->fileName, $this->progress); } throw $e; } $statusCode = null; $contentType = null; $locationHeader = null; if (!empty($http_response_header[0])) { $statusCode = self::findStatusCode($http_response_header); $contentType = Response::findHeaderValue($http_response_header, 'content-type'); $locationHeader = Response::findHeaderValue($http_response_header, 'location'); } // check for bitbucket login page asking to authenticate if ($originUrl === 'bitbucket.org' && !$this->authHelper->isPublicBitBucketDownload($fileUrl) && substr($fileUrl, -4) === '.zip' && (!$locationHeader || substr(parse_url($locationHeader, PHP_URL_PATH), -4) !== '.zip') && $contentType && Preg::isMatch('{^text/html\b}i', $contentType) ) { $result = false; if ($retryAuthFailure) { $this->promptAuthAndRetry(401); } } // check for gitlab 404 when downloading archives if ($statusCode === 404 && in_array($originUrl, $this->config->get('gitlab-domains'), true) && false !== strpos($fileUrl, 'archive.zip') ) { $result = false; if ($retryAuthFailure) { $this->promptAuthAndRetry(401); } } // handle 3xx redirects, 304 Not Modified is excluded $hasFollowedRedirect = false; if ($statusCode >= 300 && $statusCode <= 399 && $statusCode !== 304 && $this->redirects < $this->maxRedirects) { $hasFollowedRedirect = true; $result = $this->handleRedirect($http_response_header, $additionalOptions, $result); } // fail 4xx and 5xx responses and capture the response if ($statusCode && $statusCode >= 400 && $statusCode <= 599) { if (!$this->retry) { if ($this->progress && !$isRedirect) { $this->io->overwriteError("Downloading (failed)", false); } $e = new TransportException('The "'.$this->fileUrl.'" file could not be downloaded ('.$http_response_header[0].')', $statusCode); $e->setHeaders($http_response_header); $e->setResponse($this->decodeResult($result, $http_response_header)); $e->setStatusCode($statusCode); throw $e; } $result = false; } if ($this->progress && !$this->retry && !$isRedirect) { $this->io->overwriteError("Downloading (".($result === false ? 'failed' : '100%').")", false); } // decode gzip if ($result && extension_loaded('zlib') && strpos($fileUrl, 'http') === 0 && !$hasFollowedRedirect) { try { $result = $this->decodeResult($result, $http_response_header); } catch (\Exception $e) { if ($this->degradedMode) { throw $e; } $this->degradedMode = true; $this->io->writeError(array( '', 'Failed to decode response: '.$e->getMessage().'', 'Retrying with degraded mode, check https://getcomposer.org/doc/articles/troubleshooting.md#degraded-mode for more info', )); return $this->get($this->originUrl, $this->fileUrl, $additionalOptions, $this->fileName, $this->progress); } } // handle copy command if download was successful if (false !== $result && null !== $fileName && !$isRedirect) { if ('' === $result) { throw new TransportException('"'.$this->fileUrl.'" appears broken, and returned an empty 200 response'); } $errorMessage = ''; set_error_handler(function ($code, $msg) use (&$errorMessage) { if ($errorMessage) { $errorMessage .= "\n"; } $errorMessage .= Preg::replace('{^file_put_contents\(.*?\): }', '', $msg); return true; }); $result = (bool) file_put_contents($fileName, $result); restore_error_handler(); if (false === $result) { throw new TransportException('The "'.$this->fileUrl.'" file could not be written to '.$fileName.': '.$errorMessage); } } // Handle SSL cert match issues if (false === $result && false !== strpos($errorMessage, 'Peer certificate') && PHP_VERSION_ID < 50600) { // Certificate name error, PHP doesn't support subjectAltName on PHP < 5.6 // The procedure to handle sAN for older PHP's is: // // 1. Open socket to remote server and fetch certificate (disabling peer // validation because PHP errors without giving up the certificate.) // // 2. Verifying the domain in the URL against the names in the sAN field. // If there is a match record the authority [host/port], certificate // common name, and certificate fingerprint. // // 3. Retry the original request but changing the CN_match parameter to // the common name extracted from the certificate in step 2. // // 4. To prevent any attempt at being hoodwinked by switching the // certificate between steps 2 and 3 the fingerprint of the certificate // presented in step 3 is compared against the one recorded in step 2. if (CaBundle::isOpensslParseSafe()) { $certDetails = $this->getCertificateCnAndFp($this->fileUrl, $options); if ($certDetails) { $this->peerCertificateMap[$this->getUrlAuthority($this->fileUrl)] = $certDetails; $this->retry = true; } } else { $this->io->writeError(''); $this->io->writeError(sprintf( 'Your version of PHP, %s, is affected by CVE-2013-6420 and cannot safely perform certificate validation, we strongly suggest you upgrade.', PHP_VERSION )); } } if ($this->retry) { $this->retry = false; $result = $this->get($this->originUrl, $this->fileUrl, $additionalOptions, $this->fileName, $this->progress); if ($this->storeAuth) { $this->authHelper->storeAuth($this->originUrl, $this->storeAuth); $this->storeAuth = false; } return $result; } if (false === $result) { $e = new TransportException('The "'.$this->fileUrl.'" file could not be downloaded: '.$errorMessage, $errorCode); if (!empty($http_response_header[0])) { $e->setHeaders($http_response_header); } if (!$this->degradedMode && false !== strpos($e->getMessage(), 'Operation timed out')) { $this->degradedMode = true; $this->io->writeError(''); $this->io->writeError(array( ''.$e->getMessage().'', 'Retrying with degraded mode, check https://getcomposer.org/doc/articles/troubleshooting.md#degraded-mode for more info', )); return $this->get($this->originUrl, $this->fileUrl, $additionalOptions, $this->fileName, $this->progress); } throw $e; } if (!empty($http_response_header[0])) { $this->lastHeaders = $http_response_header; } return $result; } /** * Get contents of remote URL. * * @param string $originUrl The origin URL * @param string $fileUrl The file URL * @param resource $context The stream context * @param string[] $responseHeaders * @param int $maxFileSize The maximum allowed file size * * @return string|false The response contents or false on failure */ protected function getRemoteContents($originUrl, $fileUrl, $context, array &$responseHeaders = null, $maxFileSize = null) { $result = false; try { $e = null; if ($maxFileSize !== null) { $result = file_get_contents($fileUrl, false, $context, 0, $maxFileSize); } else { // passing `null` to file_get_contents will convert `null` to `0` and return 0 bytes $result = file_get_contents($fileUrl, false, $context); } } catch (\Exception $e) { } catch (\Throwable $e) { } if ($maxFileSize !== null && Platform::strlen($result) >= $maxFileSize) { throw new MaxFileSizeExceededException('Maximum allowed download size reached. Downloaded ' . Platform::strlen($result) . ' of allowed ' . $maxFileSize . ' bytes'); } // https://www.php.net/manual/en/reserved.variables.httpresponseheader.php $responseHeaders = isset($http_response_header) ? $http_response_header : array(); if (null !== $e) { throw $e; } return $result; } /** * Get notification action. * * @param int $notificationCode The notification code * @param int $severity The severity level * @param string $message The message * @param int $messageCode The message code * @param int $bytesTransferred The loaded size * @param int $bytesMax The total size * * @return void * * @throws TransportException */ protected function callbackGet($notificationCode, $severity, $message, $messageCode, $bytesTransferred, $bytesMax) { switch ($notificationCode) { case STREAM_NOTIFY_FAILURE: if (400 === $messageCode) { // This might happen if your host is secured by ssl client certificate authentication // but you do not send an appropriate certificate throw new TransportException("The '" . $this->fileUrl . "' URL could not be accessed: " . $message, $messageCode); } break; case STREAM_NOTIFY_FILE_SIZE_IS: $this->bytesMax = $bytesMax; break; case STREAM_NOTIFY_PROGRESS: if ($this->bytesMax > 0 && $this->progress) { $progression = min(100, (int) round($bytesTransferred / $this->bytesMax * 100)); if ((0 === $progression % 5) && 100 !== $progression && $progression !== $this->lastProgress) { $this->lastProgress = $progression; $this->io->overwriteError("Downloading ($progression%)", false); } } break; default: break; } } /** * @param positive-int $httpStatus * @param string|null $reason * @param string[] $headers * * @return void */ protected function promptAuthAndRetry($httpStatus, $reason = null, $headers = array()) { $result = $this->authHelper->promptAuthIfNeeded($this->fileUrl, $this->originUrl, $httpStatus, $reason, $headers); $this->storeAuth = $result['storeAuth']; $this->retry = $result['retry']; if ($this->retry) { throw new TransportException('RETRY'); } } /** * @param string $originUrl * @param mixed[] $additionalOptions * * @return mixed[] */ protected function getOptionsForUrl($originUrl, $additionalOptions) { $tlsOptions = array(); // Setup remaining TLS options - the matching may need monitoring, esp. www vs none in CN if ($this->disableTls === false && PHP_VERSION_ID < 50600 && !stream_is_local($this->fileUrl)) { $host = parse_url($this->fileUrl, PHP_URL_HOST); if (PHP_VERSION_ID < 50304) { // PHP < 5.3.4 does not support follow_location, for those people // do some really nasty hard coded transformations. These will // still breakdown if the site redirects to a domain we don't // expect. if ($host === 'github.com' || $host === 'api.github.com') { $host = '*.github.com'; } } $tlsOptions['ssl']['CN_match'] = $host; $tlsOptions['ssl']['SNI_server_name'] = $host; $urlAuthority = $this->getUrlAuthority($this->fileUrl); if (isset($this->peerCertificateMap[$urlAuthority])) { // Handle subjectAltName on lesser PHP's. $certMap = $this->peerCertificateMap[$urlAuthority]; $this->io->writeError('', true, IOInterface::DEBUG); $this->io->writeError(sprintf( 'Using %s as CN for subjectAltName enabled host %s', $certMap['cn'], $urlAuthority ), true, IOInterface::DEBUG); $tlsOptions['ssl']['CN_match'] = $certMap['cn']; $tlsOptions['ssl']['peer_fingerprint'] = $certMap['fp']; } elseif (!CaBundle::isOpensslParseSafe() && $host === 'repo.packagist.org') { // handle subjectAltName for packagist.org's repo domain on very old PHPs $tlsOptions['ssl']['CN_match'] = 'packagist.org'; } } $headers = array(); if (extension_loaded('zlib')) { $headers[] = 'Accept-Encoding: gzip'; } $options = array_replace_recursive($this->options, $tlsOptions, $additionalOptions); if (!$this->degradedMode) { // degraded mode disables HTTP/1.1 which causes issues with some bad // proxies/software due to the use of chunked encoding $options['http']['protocol_version'] = 1.1; $headers[] = 'Connection: close'; } $headers = $this->authHelper->addAuthenticationHeader($headers, $originUrl, $this->fileUrl); $options['http']['follow_location'] = 0; if (isset($options['http']['header']) && !is_array($options['http']['header'])) { $options['http']['header'] = explode("\r\n", trim($options['http']['header'], "\r\n")); } foreach ($headers as $header) { $options['http']['header'][] = $header; } return $options; } /** * @param string[] $http_response_header * @param mixed[] $additionalOptions * @param string|false $result * * @return bool|string */ private function handleRedirect(array $http_response_header, array $additionalOptions, $result) { if ($locationHeader = Response::findHeaderValue($http_response_header, 'location')) { if (parse_url($locationHeader, PHP_URL_SCHEME)) { // Absolute URL; e.g. https://example.com/composer $targetUrl = $locationHeader; } elseif (parse_url($locationHeader, PHP_URL_HOST)) { // Scheme relative; e.g. //example.com/foo $targetUrl = $this->scheme.':'.$locationHeader; } elseif ('/' === $locationHeader[0]) { // Absolute path; e.g. /foo $urlHost = parse_url($this->fileUrl, PHP_URL_HOST); // Replace path using hostname as an anchor. $targetUrl = Preg::replace('{^(.+(?://|@)'.preg_quote($urlHost).'(?::\d+)?)(?:[/\?].*)?$}', '\1'.$locationHeader, $this->fileUrl); } else { // Relative path; e.g. foo // This actually differs from PHP which seems to add duplicate slashes. $targetUrl = Preg::replace('{^(.+/)[^/?]*(?:\?.*)?$}', '\1'.$locationHeader, $this->fileUrl); } } if (!empty($targetUrl)) { $this->redirects++; $this->io->writeError('', true, IOInterface::DEBUG); $this->io->writeError(sprintf('Following redirect (%u) %s', $this->redirects, Url::sanitize($targetUrl)), true, IOInterface::DEBUG); $additionalOptions['redirects'] = $this->redirects; return $this->get(parse_url($targetUrl, PHP_URL_HOST), $targetUrl, $additionalOptions, $this->fileName, $this->progress); } if (!$this->retry) { $e = new TransportException('The "'.$this->fileUrl.'" file could not be downloaded, got redirect without Location ('.$http_response_header[0].')'); $e->setHeaders($http_response_header); $e->setResponse($this->decodeResult($result, $http_response_header)); throw $e; } return false; } /** * Fetch certificate common name and fingerprint for validation of SAN. * * @todo Remove when PHP 5.6 is minimum supported version. * * @param string $url * @param mixed[] $options * * @return ?array{cn: string, fp: string} */ private function getCertificateCnAndFp($url, $options) { if (PHP_VERSION_ID >= 50600) { throw new \BadMethodCallException(sprintf( '%s must not be used on PHP >= 5.6', __METHOD__ )); } $context = StreamContextFactory::getContext($url, $options, array('options' => array( 'ssl' => array( 'capture_peer_cert' => true, 'verify_peer' => false, // Yes this is fucking insane! But PHP is lame. ), ), )); // Ideally this would just use stream_socket_client() to avoid sending a // HTTP request but that does not capture the certificate. if (false === $handle = @fopen($url, 'rb', false, $context)) { return null; } // Close non authenticated connection without reading any content. fclose($handle); $handle = null; $params = stream_context_get_params($context); if (!empty($params['options']['ssl']['peer_certificate'])) { $peerCertificate = $params['options']['ssl']['peer_certificate']; if (TlsHelper::checkCertificateHost($peerCertificate, parse_url($url, PHP_URL_HOST), $commonName)) { return array( 'cn' => $commonName, 'fp' => TlsHelper::getCertificateFingerprint($peerCertificate), ); } } return null; } /** * @param string $url * * @return string */ private function getUrlAuthority($url) { $defaultPorts = array( 'ftp' => 21, 'http' => 80, 'https' => 443, 'ssh2.sftp' => 22, 'ssh2.scp' => 22, ); $scheme = parse_url($url, PHP_URL_SCHEME); if (!isset($defaultPorts[$scheme])) { throw new \InvalidArgumentException(sprintf( 'Could not get default port for unknown scheme: %s', $scheme )); } $defaultPort = $defaultPorts[$scheme]; $port = parse_url($url, PHP_URL_PORT) ?: $defaultPort; return parse_url($url, PHP_URL_HOST).':'.$port; } /** * @param string|false $result * @param string[] $http_response_header * * @return string|null */ private function decodeResult($result, $http_response_header) { // decode gzip if ($result && extension_loaded('zlib')) { $contentEncoding = Response::findHeaderValue($http_response_header, 'content-encoding'); $decode = $contentEncoding && 'gzip' === strtolower($contentEncoding); if ($decode) { if (PHP_VERSION_ID >= 50400) { $result = zlib_decode($result); } else { // work around issue with gzuncompress & co that do not work with all gzip checksums $result = file_get_contents('compress.zlib://data:application/octet-stream;base64,'.base64_encode($result)); } if ($result === false) { throw new TransportException('Failed to decode zlib stream'); } } } return $this->normalizeResult($result); } /** * @param string|false $result * * @return string|null */ private function normalizeResult($result) { if ($result === false) { return null; } return $result; } } * Jordi Boggiano * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Composer\Util; use Composer\Config; use Composer\IO\IOInterface; use Composer\Downloader\TransportException; use Composer\Pcre\Preg; /** * @author Jordi Boggiano */ class AuthHelper { /** @var IOInterface */ protected $io; /** @var Config */ protected $config; /** @var array Map of origins to message displayed */ private $displayedOriginAuthentications = array(); public function __construct(IOInterface $io, Config $config) { $this->io = $io; $this->config = $config; } /** * @param string $origin * @param string|bool $storeAuth * * @return void */ public function storeAuth($origin, $storeAuth) { $store = false; $configSource = $this->config->getAuthConfigSource(); if ($storeAuth === true) { $store = $configSource; } elseif ($storeAuth === 'prompt') { $answer = $this->io->askAndValidate( 'Do you want to store credentials for '.$origin.' in '.$configSource->getName().' ? [Yn] ', function ($value) { $input = strtolower(substr(trim($value), 0, 1)); if (in_array($input, array('y','n'))) { return $input; } throw new \RuntimeException('Please answer (y)es or (n)o'); }, null, 'y' ); if ($answer === 'y') { $store = $configSource; } } if ($store) { $store->addConfigSetting( 'http-basic.'.$origin, $this->io->getAuthentication($origin) ); } } /** * @param string $url * @param string $origin * @param int $statusCode HTTP status code that triggered this call * @param string|null $reason a message/description explaining why this was called * @param string[] $headers * @return array|null containing retry (bool) and storeAuth (string|bool) keys, if retry is true the request should be * retried, if storeAuth is true then on a successful retry the authentication should be persisted to auth.json * @phpstan-return ?array{retry: bool, storeAuth: string|bool} */ public function promptAuthIfNeeded($url, $origin, $statusCode, $reason = null, $headers = array()) { $storeAuth = false; if (in_array($origin, $this->config->get('github-domains'), true)) { $gitHubUtil = new GitHub($this->io, $this->config, null); $message = "\n"; $rateLimited = $gitHubUtil->isRateLimited($headers); $requiresSso = $gitHubUtil->requiresSso($headers); if ($requiresSso) { $ssoUrl = $gitHubUtil->getSsoUrl($headers); $message = 'GitHub API token requires SSO authorization. Authorize this token at ' . $ssoUrl . "\n"; $this->io->writeError($message); if (!$this->io->isInteractive()) { throw new TransportException('Could not authenticate against ' . $origin, 403); } $this->io->ask('After authorizing your token, confirm that you would like to retry the request'); return array('retry' => true, 'storeAuth' => $storeAuth); } if ($rateLimited) { $rateLimit = $gitHubUtil->getRateLimit($headers); if ($this->io->hasAuthentication($origin)) { $message = 'Review your configured GitHub OAuth token or enter a new one to go over the API rate limit.'; } else { $message = 'Create a GitHub OAuth token to go over the API rate limit.'; } $message = sprintf( 'GitHub API limit (%d calls/hr) is exhausted, could not fetch '.$url.'. '.$message.' You can also wait until %s for the rate limit to reset.', $rateLimit['limit'], $rateLimit['reset'] )."\n"; } else { $message .= 'Could not fetch '.$url.', please '; if ($this->io->hasAuthentication($origin)) { $message .= 'review your configured GitHub OAuth token or enter a new one to access private repos'; } else { $message .= 'create a GitHub OAuth token to access private repos'; } } if (!$gitHubUtil->authorizeOAuth($origin) && (!$this->io->isInteractive() || !$gitHubUtil->authorizeOAuthInteractively($origin, $message)) ) { throw new TransportException('Could not authenticate against '.$origin, 401); } } elseif (in_array($origin, $this->config->get('gitlab-domains'), true)) { $message = "\n".'Could not fetch '.$url.', enter your ' . $origin . ' credentials ' .($statusCode === 401 ? 'to access private repos' : 'to go over the API rate limit'); $gitLabUtil = new GitLab($this->io, $this->config, null); if ($this->io->hasAuthentication($origin)) { $auth = $this->io->getAuthentication($origin); if (in_array($auth['password'], array('gitlab-ci-token', 'private-token', 'oauth2'), true)) { throw new TransportException("Invalid credentials for '" . $url . "', aborting.", $statusCode); } } if (!$gitLabUtil->authorizeOAuth($origin) && (!$this->io->isInteractive() || !$gitLabUtil->authorizeOAuthInteractively(parse_url($url, PHP_URL_SCHEME), $origin, $message)) ) { throw new TransportException('Could not authenticate against '.$origin, 401); } } elseif ($origin === 'bitbucket.org' || $origin === 'api.bitbucket.org') { $askForOAuthToken = true; $origin = 'bitbucket.org'; if ($this->io->hasAuthentication($origin)) { $auth = $this->io->getAuthentication($origin); if ($auth['username'] !== 'x-token-auth') { $bitbucketUtil = new Bitbucket($this->io, $this->config); $accessToken = $bitbucketUtil->requestToken($origin, $auth['username'], $auth['password']); if (!empty($accessToken)) { $this->io->setAuthentication($origin, 'x-token-auth', $accessToken); $askForOAuthToken = false; } } else { throw new TransportException('Could not authenticate against ' . $origin, 401); } } if ($askForOAuthToken) { $message = "\n".'Could not fetch ' . $url . ', please create a bitbucket OAuth token to ' . (($statusCode === 401 || $statusCode === 403) ? 'access private repos' : 'go over the API rate limit'); $bitBucketUtil = new Bitbucket($this->io, $this->config); if (!$bitBucketUtil->authorizeOAuth($origin) && (!$this->io->isInteractive() || !$bitBucketUtil->authorizeOAuthInteractively($origin, $message)) ) { throw new TransportException('Could not authenticate against ' . $origin, 401); } } } else { // 404s are only handled for github if ($statusCode === 404) { return null; } // fail if the console is not interactive if (!$this->io->isInteractive()) { if ($statusCode === 401) { $message = "The '" . $url . "' URL required authentication.\nYou must be using the interactive console to authenticate"; } elseif ($statusCode === 403) { $message = "The '" . $url . "' URL could not be accessed: " . $reason; } else { $message = "Unknown error code '" . $statusCode . "', reason: " . $reason; } throw new TransportException($message, $statusCode); } // fail if we already have auth if ($this->io->hasAuthentication($origin)) { throw new TransportException("Invalid credentials for '" . $url . "', aborting.", $statusCode); } $this->io->writeError(' Authentication required ('.$origin.'):'); $username = $this->io->ask(' Username: '); $password = $this->io->askAndHideAnswer(' Password: '); $this->io->setAuthentication($origin, $username, $password); $storeAuth = $this->config->get('store-auths'); } return array('retry' => true, 'storeAuth' => $storeAuth); } /** * @param string[] $headers * @param string $origin * @param string $url * * @return string[] updated headers array */ public function addAuthenticationHeader(array $headers, $origin, $url) { if ($this->io->hasAuthentication($origin)) { $authenticationDisplayMessage = null; $auth = $this->io->getAuthentication($origin); if ($auth['password'] === 'bearer') { $headers[] = 'Authorization: Bearer '.$auth['username']; } elseif ('github.com' === $origin && 'x-oauth-basic' === $auth['password']) { // only add the access_token if it is actually a github API URL if (Preg::isMatch('{^https?://api\.github\.com/}', $url)) { $headers[] = 'Authorization: token '.$auth['username']; $authenticationDisplayMessage = 'Using GitHub token authentication'; } } elseif ( in_array($origin, $this->config->get('gitlab-domains'), true) && in_array($auth['password'], array('oauth2', 'private-token', 'gitlab-ci-token'), true) ) { if ($auth['password'] === 'oauth2') { $headers[] = 'Authorization: Bearer '.$auth['username']; $authenticationDisplayMessage = 'Using GitLab OAuth token authentication'; } else { $headers[] = 'PRIVATE-TOKEN: '.$auth['username']; $authenticationDisplayMessage = 'Using GitLab private token authentication'; } } elseif ( 'bitbucket.org' === $origin && $url !== Bitbucket::OAUTH2_ACCESS_TOKEN_URL && 'x-token-auth' === $auth['username'] ) { if (!$this->isPublicBitBucketDownload($url)) { $headers[] = 'Authorization: Bearer ' . $auth['password']; $authenticationDisplayMessage = 'Using Bitbucket OAuth token authentication'; } } else { $authStr = base64_encode($auth['username'] . ':' . $auth['password']); $headers[] = 'Authorization: Basic '.$authStr; $authenticationDisplayMessage = 'Using HTTP basic authentication with username "' . $auth['username'] . '"'; } if ($authenticationDisplayMessage && (!isset($this->displayedOriginAuthentications[$origin]) || $this->displayedOriginAuthentications[$origin] !== $authenticationDisplayMessage)) { $this->io->writeError($authenticationDisplayMessage, true, IOInterface::DEBUG); $this->displayedOriginAuthentications[$origin] = $authenticationDisplayMessage; } } elseif (in_array($origin, array('api.bitbucket.org', 'api.github.com'), true)) { return $this->addAuthenticationHeader($headers, str_replace('api.', '', $origin), $url); } return $headers; } /** * @link https://github.com/composer/composer/issues/5584 * * @param string $urlToBitBucketFile URL to a file at bitbucket.org. * * @return bool Whether the given URL is a public BitBucket download which requires no authentication. */ public function isPublicBitBucketDownload($urlToBitBucketFile) { $domain = parse_url($urlToBitBucketFile, PHP_URL_HOST); if (strpos($domain, 'bitbucket.org') === false) { // Bitbucket downloads are hosted on amazonaws. // We do not need to authenticate there at all return true; } $path = parse_url($urlToBitBucketFile, PHP_URL_PATH); // Path for a public download follows this pattern /{user}/{repo}/downloads/{whatever} // {@link https://blog.bitbucket.org/2009/04/12/new-feature-downloads/} $pathParts = explode('/', $path); return count($pathParts) >= 4 && $pathParts[3] == 'downloads'; } } * Jordi Boggiano * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Composer\Util; use Composer\Package\Loader\ArrayLoader; use Composer\Package\Loader\ValidatingArrayLoader; use Composer\Package\Loader\InvalidPackageException; use Composer\Json\JsonValidationException; use Composer\IO\IOInterface; use Composer\Json\JsonFile; use Composer\Pcre\Preg; use Composer\Spdx\SpdxLicenses; /** * Validates a composer configuration. * * @author Robert Schönthal * @author Jordi Boggiano */ class ConfigValidator { const CHECK_VERSION = 1; /** @var IOInterface */ private $io; public function __construct(IOInterface $io) { $this->io = $io; } /** * Validates the config, and returns the result. * * @param string $file The path to the file * @param int $arrayLoaderValidationFlags Flags for ArrayLoader validation * @param int $flags Flags for validation * * @return array{list, list, list} a triple containing the errors, publishable errors, and warnings */ public function validate($file, $arrayLoaderValidationFlags = ValidatingArrayLoader::CHECK_ALL, $flags = self::CHECK_VERSION) { $errors = array(); $publishErrors = array(); $warnings = array(); // validate json schema $laxValid = false; try { $json = new JsonFile($file, null, $this->io); $manifest = $json->read(); $json->validateSchema(JsonFile::LAX_SCHEMA); $laxValid = true; $json->validateSchema(); } catch (JsonValidationException $e) { foreach ($e->getErrors() as $message) { if ($laxValid) { $publishErrors[] = $message; } else { $errors[] = $message; } } } catch (\Exception $e) { $errors[] = $e->getMessage(); return array($errors, $publishErrors, $warnings); } // validate actual data if (empty($manifest['license'])) { $warnings[] = 'No license specified, it is recommended to do so. For closed-source software you may use "proprietary" as license.'; } else { $licenses = (array) $manifest['license']; // strip proprietary since it's not a valid SPDX identifier, but is accepted by composer foreach ($licenses as $key => $license) { if ('proprietary' === $license) { unset($licenses[$key]); } } $licenseValidator = new SpdxLicenses(); foreach ($licenses as $license) { $spdxLicense = $licenseValidator->getLicenseByIdentifier($license); if ($spdxLicense && $spdxLicense[3]) { if (Preg::isMatch('{^[AL]?GPL-[123](\.[01])?\+$}i', $license)) { $warnings[] = sprintf( 'License "%s" is a deprecated SPDX license identifier, use "'.str_replace('+', '', $license).'-or-later" instead', $license ); } elseif (Preg::isMatch('{^[AL]?GPL-[123](\.[01])?$}i', $license)) { $warnings[] = sprintf( 'License "%s" is a deprecated SPDX license identifier, use "'.$license.'-only" or "'.$license.'-or-later" instead', $license ); } else { $warnings[] = sprintf( 'License "%s" is a deprecated SPDX license identifier, see https://spdx.org/licenses/', $license ); } } } } if (($flags & self::CHECK_VERSION) && isset($manifest['version'])) { $warnings[] = 'The version field is present, it is recommended to leave it out if the package is published on Packagist.'; } if (!empty($manifest['name']) && Preg::isMatch('{[A-Z]}', $manifest['name'])) { $suggestName = Preg::replace('{(?:([a-z])([A-Z])|([A-Z])([A-Z][a-z]))}', '\\1\\3-\\2\\4', $manifest['name']); $suggestName = strtolower($suggestName); $publishErrors[] = sprintf( 'Name "%s" does not match the best practice (e.g. lower-cased/with-dashes). We suggest using "%s" instead. As such you will not be able to submit it to Packagist.', $manifest['name'], $suggestName ); } if (!empty($manifest['type']) && $manifest['type'] == 'composer-installer') { $warnings[] = "The package type 'composer-installer' is deprecated. Please distribute your custom installers as plugins from now on. See https://getcomposer.org/doc/articles/plugins.md for plugin documentation."; } // check for require-dev overrides if (isset($manifest['require'], $manifest['require-dev'])) { $requireOverrides = array_intersect_key($manifest['require'], $manifest['require-dev']); if (!empty($requireOverrides)) { $plural = (count($requireOverrides) > 1) ? 'are' : 'is'; $warnings[] = implode(', ', array_keys($requireOverrides)). " {$plural} required both in require and require-dev, this can lead to unexpected behavior"; } } // check for meaningless provide/replace satisfying requirements foreach (array('provide', 'replace') as $linkType) { if (isset($manifest[$linkType])) { foreach (array('require', 'require-dev') as $requireType) { if (isset($manifest[$requireType])) { foreach ($manifest[$linkType] as $provide => $constraint) { if (isset($manifest[$requireType][$provide])) { $warnings[] = 'The package ' . $provide . ' in '.$requireType.' is also listed in '.$linkType.' which satisfies the requirement. Remove it from '.$linkType.' if you wish to install it.'; } } } } } } // check for commit references $require = isset($manifest['require']) ? $manifest['require'] : array(); $requireDev = isset($manifest['require-dev']) ? $manifest['require-dev'] : array(); $packages = array_merge($require, $requireDev); foreach ($packages as $package => $version) { if (Preg::isMatch('/#/', $version)) { $warnings[] = sprintf( 'The package "%s" is pointing to a commit-ref, this is bad practice and can cause unforeseen issues.', $package ); } } // report scripts-descriptions for non-existent scripts $scriptsDescriptions = isset($manifest['scripts-descriptions']) ? $manifest['scripts-descriptions'] : array(); $scripts = isset($manifest['scripts']) ? $manifest['scripts'] : array(); foreach ($scriptsDescriptions as $scriptName => $scriptDescription) { if (!array_key_exists($scriptName, $scripts)) { $warnings[] = sprintf( 'Description for non-existent script "%s" found in "scripts-descriptions"', $scriptName ); } } // check for empty psr-0/psr-4 namespace prefixes if (isset($manifest['autoload']['psr-0'][''])) { $warnings[] = "Defining autoload.psr-0 with an empty namespace prefix is a bad idea for performance"; } if (isset($manifest['autoload']['psr-4'][''])) { $warnings[] = "Defining autoload.psr-4 with an empty namespace prefix is a bad idea for performance"; } $loader = new ValidatingArrayLoader(new ArrayLoader(), true, null, $arrayLoaderValidationFlags); try { if (!isset($manifest['version'])) { $manifest['version'] = '1.0.0'; } if (!isset($manifest['name'])) { $manifest['name'] = 'dummy/dummy'; } $loader->load($manifest); } catch (InvalidPackageException $e) { $errors = array_merge($errors, $e->getErrors()); } $warnings = array_merge($warnings, $loader->getWarnings()); return array($errors, $publishErrors, $warnings); } } * Jordi Boggiano * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Composer\Util; /** * Temporarily suppress PHP error reporting, usually warnings and below. * * @author Niels Keurentjes */ class Silencer { /** * @var int[] Unpop stack */ private static $stack = array(); /** * Suppresses given mask or errors. * * @param int|null $mask Error levels to suppress, default value NULL indicates all warnings and below. * @return int The old error reporting level. */ public static function suppress($mask = null) { if (!isset($mask)) { $mask = E_WARNING | E_NOTICE | E_USER_WARNING | E_USER_NOTICE | E_DEPRECATED | E_USER_DEPRECATED | E_STRICT; } $old = error_reporting(); self::$stack[] = $old; error_reporting($old & ~$mask); return $old; } /** * Restores a single state. * * @return void */ public static function restore() { if (!empty(self::$stack)) { error_reporting(array_pop(self::$stack)); } } /** * Calls a specified function while silencing warnings and below. * * Future improvement: when PHP requirements are raised add Callable type hint (5.4) and variadic parameters (5.6) * * @param callable $callable Function to execute. * @throws \Exception Any exceptions from the callback are rethrown. * @return mixed Return value of the callback. */ public static function call($callable /*, ...$parameters */) { try { self::suppress(); $result = call_user_func_array($callable, array_slice(func_get_args(), 1)); self::restore(); return $result; } catch (\Exception $e) { // Use a finally block for this when requirements are raised to PHP 5.5 self::restore(); throw $e; } } } * Jordi Boggiano * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Composer\Util; use Composer\CaBundle\CaBundle; use Composer\Pcre\Preg; /** * @author Chris Smith */ final class TlsHelper { /** * Match hostname against a certificate. * * @param mixed $certificate X.509 certificate * @param string $hostname Hostname in the URL * @param string $cn Set to the common name of the certificate iff match found * * @return bool */ public static function checkCertificateHost($certificate, $hostname, &$cn = null) { $names = self::getCertificateNames($certificate); if (empty($names)) { return false; } $combinedNames = array_merge($names['san'], array($names['cn'])); $hostname = strtolower($hostname); foreach ($combinedNames as $certName) { $matcher = self::certNameMatcher($certName); if ($matcher && $matcher($hostname)) { $cn = $names['cn']; return true; } } return false; } /** * Extract DNS names out of an X.509 certificate. * * @param mixed $certificate X.509 certificate * * @return array{cn: string, san: string[]}|null */ public static function getCertificateNames($certificate) { if (is_array($certificate)) { $info = $certificate; } elseif (CaBundle::isOpensslParseSafe()) { $info = openssl_x509_parse($certificate, false); } if (!isset($info['subject']['commonName'])) { return null; } $commonName = strtolower($info['subject']['commonName']); $subjectAltNames = array(); if (isset($info['extensions']['subjectAltName'])) { $subjectAltNames = Preg::split('{\s*,\s*}', $info['extensions']['subjectAltName']); $subjectAltNames = array_filter(array_map(function ($name) { if (0 === strpos($name, 'DNS:')) { return strtolower(ltrim(substr($name, 4))); } return null; }, $subjectAltNames)); $subjectAltNames = array_values($subjectAltNames); } return array( 'cn' => $commonName, 'san' => $subjectAltNames, ); } /** * Get the certificate pin. * * By Kevin McArthur of StormTide Digital Studios Inc. * @KevinSMcArthur / https://github.com/StormTide * * See https://tools.ietf.org/html/draft-ietf-websec-key-pinning-02 * * This method was adapted from Sslurp. * https://github.com/EvanDotPro/Sslurp * * (c) Evan Coury * * For the full copyright and license information, please see below: * * Copyright (c) 2013, Evan Coury * All rights reserved. * * Redistribution and use in source and binary forms, with or without modification, * are permitted provided that the following conditions are met: * * * Redistributions of source code must retain the above copyright notice, * this list of conditions and the following disclaimer. * * * Redistributions in binary form must reproduce the above copyright notice, * this list of conditions and the following disclaimer in the documentation * and/or other materials provided with the distribution. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. * * @param string $certificate * @return string */ public static function getCertificateFingerprint($certificate) { $pubkeydetails = openssl_pkey_get_details(openssl_get_publickey($certificate)); $pubkeypem = $pubkeydetails['key']; //Convert PEM to DER before SHA1'ing $start = '-----BEGIN PUBLIC KEY-----'; $end = '-----END PUBLIC KEY-----'; $pemtrim = substr($pubkeypem, strpos($pubkeypem, $start) + strlen($start), (strlen($pubkeypem) - strpos($pubkeypem, $end)) * (-1)); $der = base64_decode($pemtrim); return sha1($der); } /** * Test if it is safe to use the PHP function openssl_x509_parse(). * * This checks if OpenSSL extensions is vulnerable to remote code execution * via the exploit documented as CVE-2013-6420. * * @return bool */ public static function isOpensslParseSafe() { return CaBundle::isOpensslParseSafe(); } /** * Convert certificate name into matching function. * * @param string $certName CN/SAN * * @return callable|null */ private static function certNameMatcher($certName) { $wildcards = substr_count($certName, '*'); if (0 === $wildcards) { // Literal match. return function ($hostname) use ($certName) { return $hostname === $certName; }; } if (1 === $wildcards) { $components = explode('.', $certName); if (3 > count($components)) { // Must have 3+ components return null; } $firstComponent = $components[0]; // Wildcard must be the last character. if ('*' !== $firstComponent[strlen($firstComponent) - 1]) { return null; } $wildcardRegex = preg_quote($certName); $wildcardRegex = str_replace('\\*', '[a-z0-9-]+', $wildcardRegex); $wildcardRegex = "{^{$wildcardRegex}$}"; return function ($hostname) use ($wildcardRegex) { return Preg::isMatch($wildcardRegex, $hostname); }; } return null; } } * Jordi Boggiano * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Composer\Util; use Composer\Factory; use Composer\IO\IOInterface; use Composer\Config; use Composer\Downloader\TransportException; /** * @author Paul Wenke */ class Bitbucket { /** @var IOInterface */ private $io; /** @var Config */ private $config; /** @var ProcessExecutor */ private $process; /** @var HttpDownloader */ private $httpDownloader; /** @var array{access_token: string, expires_in?: int}|null */ private $token = null; /** @var int|null */ private $time; const OAUTH2_ACCESS_TOKEN_URL = 'https://bitbucket.org/site/oauth2/access_token'; /** * Constructor. * * @param IOInterface $io The IO instance * @param Config $config The composer configuration * @param ProcessExecutor $process Process instance, injectable for mocking * @param HttpDownloader $httpDownloader Remote Filesystem, injectable for mocking * @param int $time Timestamp, injectable for mocking */ public function __construct(IOInterface $io, Config $config, ProcessExecutor $process = null, HttpDownloader $httpDownloader = null, $time = null) { $this->io = $io; $this->config = $config; $this->process = $process ?: new ProcessExecutor($io); $this->httpDownloader = $httpDownloader ?: Factory::createHttpDownloader($this->io, $config); $this->time = $time; } /** * @return string */ public function getToken() { if (!isset($this->token['access_token'])) { return ''; } return $this->token['access_token']; } /** * Attempts to authorize a Bitbucket domain via OAuth * * @param string $originUrl The host this Bitbucket instance is located at * @return bool true on success */ public function authorizeOAuth($originUrl) { if ($originUrl !== 'bitbucket.org') { return false; } // if available use token from git config if (0 === $this->process->execute('git config bitbucket.accesstoken', $output)) { $this->io->setAuthentication($originUrl, 'x-token-auth', trim($output)); return true; } return false; } /** * @return bool */ private function requestAccessToken() { try { $response = $this->httpDownloader->get(self::OAUTH2_ACCESS_TOKEN_URL, array( 'retry-auth-failure' => false, 'http' => array( 'method' => 'POST', 'content' => 'grant_type=client_credentials', ), )); $token = $response->decodeJson(); if (!isset($token['expires_in']) || !isset($token['access_token'])) { throw new \LogicException('Expected a token configured with expires_in and access_token present, got '.json_encode($token)); } $this->token = $token; } catch (TransportException $e) { if ($e->getCode() === 400) { $this->io->writeError('Invalid OAuth consumer provided.'); $this->io->writeError('This can have two reasons:'); $this->io->writeError('1. You are authenticating with a bitbucket username/password combination'); $this->io->writeError('2. You are using an OAuth consumer, but didn\'t configure a (dummy) callback url'); return false; } if (in_array($e->getCode(), array(403, 401))) { $this->io->writeError('Invalid OAuth consumer provided.'); $this->io->writeError('You can also add it manually later by using "composer config --global --auth bitbucket-oauth.bitbucket.org "'); return false; } throw $e; } return true; } /** * Authorizes a Bitbucket domain interactively via OAuth * * @param string $originUrl The host this Bitbucket instance is located at * @param string $message The reason this authorization is required * @throws \RuntimeException * @throws TransportException|\Exception * @return bool true on success */ public function authorizeOAuthInteractively($originUrl, $message = null) { if ($message) { $this->io->writeError($message); } $url = 'https://support.atlassian.com/bitbucket-cloud/docs/use-oauth-on-bitbucket-cloud/'; $this->io->writeError(sprintf('Follow the instructions on %s', $url)); $this->io->writeError(sprintf('to create a consumer. It will be stored in "%s" for future use by Composer.', $this->config->getAuthConfigSource()->getName())); $this->io->writeError('Ensure you enter a "Callback URL" (http://example.com is fine) or it will not be possible to create an Access Token (this callback url will not be used by composer)'); $consumerKey = trim((string) $this->io->askAndHideAnswer('Consumer Key (hidden): ')); if (!$consumerKey) { $this->io->writeError('No consumer key given, aborting.'); $this->io->writeError('You can also add it manually later by using "composer config --global --auth bitbucket-oauth.bitbucket.org "'); return false; } $consumerSecret = trim((string) $this->io->askAndHideAnswer('Consumer Secret (hidden): ')); if (!$consumerSecret) { $this->io->writeError('No consumer secret given, aborting.'); $this->io->writeError('You can also add it manually later by using "composer config --global --auth bitbucket-oauth.bitbucket.org "'); return false; } $this->io->setAuthentication($originUrl, $consumerKey, $consumerSecret); if (!$this->requestAccessToken()) { return false; } // store value in user config $this->storeInAuthConfig($originUrl, $consumerKey, $consumerSecret); // Remove conflicting basic auth credentials (if available) $this->config->getAuthConfigSource()->removeConfigSetting('http-basic.' . $originUrl); $this->io->writeError('Consumer stored successfully.'); return true; } /** * Retrieves an access token from Bitbucket. * * @param string $originUrl * @param string $consumerKey * @param string $consumerSecret * @return string */ public function requestToken($originUrl, $consumerKey, $consumerSecret) { if ($this->token !== null || $this->getTokenFromConfig($originUrl)) { return $this->token['access_token']; } $this->io->setAuthentication($originUrl, $consumerKey, $consumerSecret); if (!$this->requestAccessToken()) { return ''; } $this->storeInAuthConfig($originUrl, $consumerKey, $consumerSecret); if (!isset($this->token['access_token'])) { throw new \LogicException('Failed to initialize token above'); } return $this->token['access_token']; } /** * Store the new/updated credentials to the configuration * * @param string $originUrl * @param string $consumerKey * @param string $consumerSecret * * @return void */ private function storeInAuthConfig($originUrl, $consumerKey, $consumerSecret) { $this->config->getConfigSource()->removeConfigSetting('bitbucket-oauth.'.$originUrl); if (null === $this->token || !isset($this->token['expires_in'])) { throw new \LogicException('Expected a token configured with expires_in present, got '.json_encode($this->token)); } $time = null === $this->time ? time() : $this->time; $consumer = array( "consumer-key" => $consumerKey, "consumer-secret" => $consumerSecret, "access-token" => $this->token['access_token'], "access-token-expiration" => $time + $this->token['expires_in'], ); $this->config->getAuthConfigSource()->addConfigSetting('bitbucket-oauth.'.$originUrl, $consumer); } /** * @param string $originUrl * @return bool */ private function getTokenFromConfig($originUrl) { $authConfig = $this->config->get('bitbucket-oauth'); if ( !isset($authConfig[$originUrl]['access-token'], $authConfig[$originUrl]['access-token-expiration']) || time() > $authConfig[$originUrl]['access-token-expiration'] ) { return false; } $this->token = array( 'access_token' => $authConfig[$originUrl]['access-token'], ); return true; } } * Jordi Boggiano * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Composer\Util; use Composer\Config; use Composer\IO\IOInterface; use Composer\Downloader\TransportException; use Composer\Pcre\Preg; use Composer\Util\Http\Response; use Composer\Util\Http\CurlDownloader; use Composer\Composer; use Composer\Package\Version\VersionParser; use Composer\Semver\Constraint\Constraint; use Composer\Exception\IrrecoverableDownloadException; use React\Promise\Promise; use React\Promise\PromiseInterface; /** * @author Jordi Boggiano * @phpstan-type Request array{url: string, options?: mixed[], copyTo?: ?string} * @phpstan-type Job array{id: int, status: int, request: Request, sync: bool, origin: string, resolve?: callable, reject?: callable, curl_id?: int, response?: Response, exception?: TransportException} */ class HttpDownloader { const STATUS_QUEUED = 1; const STATUS_STARTED = 2; const STATUS_COMPLETED = 3; const STATUS_FAILED = 4; const STATUS_ABORTED = 5; /** @var IOInterface */ private $io; /** @var Config */ private $config; /** @var array */ private $jobs = array(); /** @var mixed[] */ private $options = array(); /** @var int */ private $runningJobs = 0; /** @var int */ private $maxJobs = 12; /** @var ?CurlDownloader */ private $curl; /** @var ?RemoteFilesystem */ private $rfs; /** @var int */ private $idGen = 0; /** @var bool */ private $disabled; /** @var bool */ private $allowAsync = false; /** * @param IOInterface $io The IO instance * @param Config $config The config * @param mixed[] $options The options * @param bool $disableTls */ public function __construct(IOInterface $io, Config $config, array $options = array(), $disableTls = false) { $this->io = $io; $this->disabled = (bool) Platform::getEnv('COMPOSER_DISABLE_NETWORK'); // Setup TLS options // The cafile option can be set via config.json if ($disableTls === false) { $this->options = StreamContextFactory::getTlsDefaults($options, $io); } // handle the other externally set options normally. $this->options = array_replace_recursive($this->options, $options); $this->config = $config; if (self::isCurlEnabled()) { $this->curl = new CurlDownloader($io, $config, $options, $disableTls); } $this->rfs = new RemoteFilesystem($io, $config, $options, $disableTls); if (is_numeric($maxJobs = Platform::getEnv('COMPOSER_MAX_PARALLEL_HTTP'))) { $this->maxJobs = max(1, min(50, (int) $maxJobs)); } } /** * Download a file synchronously * * @param string $url URL to download * @param mixed[] $options Stream context options e.g. https://www.php.net/manual/en/context.http.php * although not all options are supported when using the default curl downloader * @throws TransportException * @return Response */ public function get($url, $options = array()) { list($job) = $this->addJob(array('url' => $url, 'options' => $options, 'copyTo' => null), true); $this->wait($job['id']); $response = $this->getResponse($job['id']); // check for failed curl response (empty body but successful looking response) if ( $this->curl && PHP_VERSION_ID < 70000 && $response->getBody() === null && $response->getStatusCode() === 200 && $response->getHeader('content-length') !== '0' ) { $this->io->writeError('cURL downloader failed to return a response, disabling it and proceeding in slow mode.'); $this->curl = null; list($job) = $this->addJob(array('url' => $url, 'options' => $options, 'copyTo' => null), true); $this->wait($job['id']); $response = $this->getResponse($job['id']); } return $response; } /** * Create an async download operation * * @param string $url URL to download * @param mixed[] $options Stream context options e.g. https://www.php.net/manual/en/context.http.php * although not all options are supported when using the default curl downloader * @throws TransportException * @return PromiseInterface */ public function add($url, $options = array()) { list(, $promise) = $this->addJob(array('url' => $url, 'options' => $options, 'copyTo' => null)); return $promise; } /** * Copy a file synchronously * * @param string $url URL to download * @param string $to Path to copy to * @param mixed[] $options Stream context options e.g. https://www.php.net/manual/en/context.http.php * although not all options are supported when using the default curl downloader * @throws TransportException * @return Response */ public function copy($url, $to, $options = array()) { list($job) = $this->addJob(array('url' => $url, 'options' => $options, 'copyTo' => $to), true); $this->wait($job['id']); return $this->getResponse($job['id']); } /** * Create an async copy operation * * @param string $url URL to download * @param string $to Path to copy to * @param mixed[] $options Stream context options e.g. https://www.php.net/manual/en/context.http.php * although not all options are supported when using the default curl downloader * @throws TransportException * @return PromiseInterface */ public function addCopy($url, $to, $options = array()) { list(, $promise) = $this->addJob(array('url' => $url, 'options' => $options, 'copyTo' => $to)); return $promise; } /** * Retrieve the options set in the constructor * * @return mixed[] Options */ public function getOptions() { return $this->options; } /** * Merges new options * * @param mixed[] $options * @return void */ public function setOptions(array $options) { $this->options = array_replace_recursive($this->options, $options); } /** * @param Request $request * @param bool $sync * * @return array{Job, PromiseInterface} */ private function addJob($request, $sync = false) { $request['options'] = array_replace_recursive($this->options, $request['options']); /** @var Job */ $job = array( 'id' => $this->idGen++, 'status' => self::STATUS_QUEUED, 'request' => $request, 'sync' => $sync, 'origin' => Url::getOrigin($this->config, $request['url']), ); if (!$sync && !$this->allowAsync) { throw new \LogicException('You must use the HttpDownloader instance which is part of a Composer\Loop instance to be able to run async http requests'); } // capture username/password from URL if there is one if (Preg::isMatch('{^https?://([^:/]+):([^@/]+)@([^/]+)}i', $request['url'], $match)) { $this->io->setAuthentication($job['origin'], rawurldecode($match[1]), rawurldecode($match[2])); } $rfs = $this->rfs; if ($this->canUseCurl($job)) { $resolver = function ($resolve, $reject) use (&$job) { $job['status'] = HttpDownloader::STATUS_QUEUED; $job['resolve'] = $resolve; $job['reject'] = $reject; }; } else { $resolver = function ($resolve, $reject) use (&$job, $rfs) { // start job $url = $job['request']['url']; $options = $job['request']['options']; $job['status'] = HttpDownloader::STATUS_STARTED; if ($job['request']['copyTo']) { $rfs->copy($job['origin'], $url, $job['request']['copyTo'], false /* TODO progress */, $options); $headers = $rfs->getLastHeaders(); $response = new Http\Response($job['request'], $rfs->findStatusCode($headers), $headers, $job['request']['copyTo'].'~'); $resolve($response); } else { $body = $rfs->getContents($job['origin'], $url, false /* TODO progress */, $options); $headers = $rfs->getLastHeaders(); $response = new Http\Response($job['request'], $rfs->findStatusCode($headers), $headers, $body); $resolve($response); } }; } $downloader = $this; $curl = $this->curl; $canceler = function () use (&$job, $curl) { if ($job['status'] === HttpDownloader::STATUS_QUEUED) { $job['status'] = HttpDownloader::STATUS_ABORTED; } if ($job['status'] !== HttpDownloader::STATUS_STARTED) { return; } $job['status'] = HttpDownloader::STATUS_ABORTED; if (isset($job['curl_id'])) { $curl->abortRequest($job['curl_id']); } throw new IrrecoverableDownloadException('Download of ' . Url::sanitize($job['request']['url']) . ' canceled'); }; $promise = new Promise($resolver, $canceler); $promise = $promise->then(function ($response) use (&$job, $downloader) { $job['status'] = HttpDownloader::STATUS_COMPLETED; $job['response'] = $response; // TODO 3.0 this should be done directly on $this when PHP 5.3 is dropped $downloader->markJobDone(); return $response; }, function ($e) use (&$job, $downloader) { $job['status'] = HttpDownloader::STATUS_FAILED; $job['exception'] = $e; $downloader->markJobDone(); throw $e; }); $this->jobs[$job['id']] = &$job; if ($this->runningJobs < $this->maxJobs) { $this->startJob($job['id']); } return array($job, $promise); } /** * @param int $id * @return void */ private function startJob($id) { $job = &$this->jobs[$id]; if ($job['status'] !== self::STATUS_QUEUED) { return; } // start job $job['status'] = self::STATUS_STARTED; $this->runningJobs++; $resolve = $job['resolve']; $reject = $job['reject']; $url = $job['request']['url']; $options = $job['request']['options']; $origin = $job['origin']; if ($this->disabled) { if (isset($job['request']['options']['http']['header']) && false !== stripos(implode('', $job['request']['options']['http']['header']), 'if-modified-since')) { $resolve(new Response(array('url' => $url), 304, array(), '')); } else { $e = new TransportException('Network disabled, request canceled: '.Url::sanitize($url), 499); $e->setStatusCode(499); $reject($e); } return; } try { if ($job['request']['copyTo']) { $job['curl_id'] = $this->curl->download($resolve, $reject, $origin, $url, $options, $job['request']['copyTo']); } else { $job['curl_id'] = $this->curl->download($resolve, $reject, $origin, $url, $options); } } catch (\Exception $exception) { $reject($exception); } } /** * @private * @return void */ public function markJobDone() { $this->runningJobs--; } /** * Wait for current async download jobs to complete * * @param int|null $index For internal use only, the job id * * @return void */ public function wait($index = null) { do { $jobCount = $this->countActiveJobs($index); } while ($jobCount); } /** * @internal * * @return void */ public function enableAsync() { $this->allowAsync = true; } /** * @internal * * @param int|null $index For internal use only, the job id * @return int number of active (queued or started) jobs */ public function countActiveJobs($index = null) { if ($this->runningJobs < $this->maxJobs) { foreach ($this->jobs as $job) { if ($job['status'] === self::STATUS_QUEUED && $this->runningJobs < $this->maxJobs) { $this->startJob($job['id']); } } } if ($this->curl) { $this->curl->tick(); } if (null !== $index) { return $this->jobs[$index]['status'] < self::STATUS_COMPLETED ? 1 : 0; } $active = 0; foreach ($this->jobs as $job) { if ($job['status'] < self::STATUS_COMPLETED) { $active++; } elseif (!$job['sync']) { unset($this->jobs[$job['id']]); } } return $active; } /** * @param int $index Job id * @return Response */ private function getResponse($index) { if (!isset($this->jobs[$index])) { throw new \LogicException('Invalid request id'); } if ($this->jobs[$index]['status'] === self::STATUS_FAILED) { throw $this->jobs[$index]['exception']; } if (!isset($this->jobs[$index]['response'])) { throw new \LogicException('Response not available yet, call wait() first'); } $resp = $this->jobs[$index]['response']; unset($this->jobs[$index]); return $resp; } /** * @internal * * @param string $url * @param array{warning?: string, info?: string, warning-versions?: string, info-versions?: string, warnings?: array, infos?: array} $data * @return void */ public static function outputWarnings(IOInterface $io, $url, $data) { // legacy warning/info keys foreach (array('warning', 'info') as $type) { if (empty($data[$type])) { continue; } if (!empty($data[$type . '-versions'])) { $versionParser = new VersionParser(); $constraint = $versionParser->parseConstraints($data[$type . '-versions']); $composer = new Constraint('==', $versionParser->normalize(Composer::getVersion())); if (!$constraint->matches($composer)) { continue; } } $io->writeError('<'.$type.'>'.ucfirst($type).' from '.Url::sanitize($url).': '.$data[$type].''); } // modern Composer 2.2+ format with support for multiple warning/info messages foreach (array('warnings', 'infos') as $key) { if (empty($data[$key])) { continue; } $versionParser = new VersionParser(); foreach ($data[$key] as $spec) { $type = substr($key, 0, -1); $constraint = $versionParser->parseConstraints($spec['versions']); $composer = new Constraint('==', $versionParser->normalize(Composer::getVersion())); if (!$constraint->matches($composer)) { continue; } $io->writeError('<'.$type.'>'.ucfirst($type).' from '.Url::sanitize($url).': '.$spec['message'].''); } } } /** * @internal * * @return ?string[] */ public static function getExceptionHints(\Exception $e) { if (!$e instanceof TransportException) { return null; } if ( false !== strpos($e->getMessage(), 'Resolving timed out') || false !== strpos($e->getMessage(), 'Could not resolve host') ) { Silencer::suppress(); $testConnectivity = file_get_contents('https://8.8.8.8', false, stream_context_create(array( 'ssl' => array('verify_peer' => false), 'http' => array('follow_location' => false, 'ignore_errors' => true), ))); Silencer::restore(); if (false !== $testConnectivity) { return array( 'The following exception probably indicates you have misconfigured DNS resolver(s)', ); } return array( 'The following exception probably indicates you are offline or have misconfigured DNS resolver(s)', ); } return null; } /** * @param Job $job * @return bool */ private function canUseCurl(array $job) { if (!$this->curl) { return false; } if (!Preg::isMatch('{^https?://}i', $job['request']['url'])) { return false; } if (!empty($job['request']['options']['ssl']['allow_self_signed'])) { return false; } return true; } /** * @internal * @return bool */ public static function isCurlEnabled() { return \extension_loaded('curl') && \function_exists('curl_multi_exec') && \function_exists('curl_multi_init'); } } * Jordi Boggiano * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Composer\Util; @trigger_error('Composer\Util\MetadataMinifier is deprecated, use Composer\MetadataMinifier\MetadataMinifier from composer/metadata-minifier instead.', E_USER_DEPRECATED); /** * @deprecated Use Composer\MetadataMinifier\MetadataMinifier instead */ class MetadataMinifier extends \Composer\MetadataMinifier\MetadataMinifier { } * Jordi Boggiano * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Composer\Util\Http; use Composer\Json\JsonFile; use Composer\Pcre\Preg; use Composer\Util\HttpDownloader; /** * @phpstan-import-type Request from HttpDownloader */ class Response { /** @var Request */ private $request; /** @var int */ private $code; /** @var string[] */ private $headers; /** @var ?string */ private $body; /** * @param Request $request * @param int $code * @param string[] $headers * @param ?string $body */ public function __construct(array $request, $code, array $headers, $body) { if (!isset($request['url'])) { // @phpstan-ignore-line throw new \LogicException('url key missing from request array'); } $this->request = $request; $this->code = (int) $code; $this->headers = $headers; $this->body = $body; } /** * @return int */ public function getStatusCode() { return $this->code; } /** * @return string|null */ public function getStatusMessage() { $value = null; foreach ($this->headers as $header) { if (Preg::isMatch('{^HTTP/\S+ \d+}i', $header)) { // In case of redirects, headers contain the headers of all responses // so we can not return directly and need to keep iterating $value = $header; } } return $value; } /** * @return string[] */ public function getHeaders() { return $this->headers; } /** * @param string $name * @return ?string */ public function getHeader($name) { return self::findHeaderValue($this->headers, $name); } /** * @return ?string */ public function getBody() { return $this->body; } /** * @return mixed */ public function decodeJson() { return JsonFile::parseJson($this->body, $this->request['url']); } /** * @return void * @phpstan-impure */ public function collect() { /** @phpstan-ignore-next-line */ $this->request = $this->code = $this->headers = $this->body = null; } /** * @param string[] $headers array of returned headers like from getLastHeaders() * @param string $name header name (case insensitive) * @return string|null */ public static function findHeaderValue(array $headers, $name) { $value = null; foreach ($headers as $header) { if (Preg::isMatch('{^'.preg_quote($name).':\s*(.+?)\s*$}i', $header, $match)) { $value = $match[1]; } } return $value; } } * Jordi Boggiano * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Composer\Util\Http; /** * Proxy discovery and helper class * * @internal * @author John Stevenson */ class ProxyHelper { /** * Returns proxy environment values * * @return array{string|null, string|null, string|null} httpProxy, httpsProxy, noProxy values * * @throws \RuntimeException on malformed url */ public static function getProxyData() { $httpProxy = null; $httpsProxy = null; // Handle http_proxy/HTTP_PROXY on CLI only for security reasons if (PHP_SAPI === 'cli' || PHP_SAPI === 'phpdbg') { if ($env = self::getProxyEnv(array('http_proxy', 'HTTP_PROXY'), $name)) { $httpProxy = self::checkProxy($env, $name); } } // Prefer CGI_HTTP_PROXY if available if ($env = self::getProxyEnv(array('CGI_HTTP_PROXY'), $name)) { $httpProxy = self::checkProxy($env, $name); } // Handle https_proxy/HTTPS_PROXY if ($env = self::getProxyEnv(array('https_proxy', 'HTTPS_PROXY'), $name)) { $httpsProxy = self::checkProxy($env, $name); } else { $httpsProxy = $httpProxy; } // Handle no_proxy $noProxy = self::getProxyEnv(array('no_proxy', 'NO_PROXY'), $name); return array($httpProxy, $httpsProxy, $noProxy); } /** * Returns http context options for the proxy url * * @param string $proxyUrl * * @return array{http: array{proxy: string, header?: string}} */ public static function getContextOptions($proxyUrl) { $proxy = parse_url($proxyUrl); // Remove any authorization $proxyUrl = self::formatParsedUrl($proxy, false); $proxyUrl = str_replace(array('http://', 'https://'), array('tcp://', 'ssl://'), $proxyUrl); $options['http']['proxy'] = $proxyUrl; // Handle any authorization if (isset($proxy['user'])) { $auth = rawurldecode($proxy['user']); if (isset($proxy['pass'])) { $auth .= ':' . rawurldecode($proxy['pass']); } $auth = base64_encode($auth); // Set header as a string $options['http']['header'] = "Proxy-Authorization: Basic {$auth}"; } return $options; } /** * Sets/unsets request_fulluri value in http context options array * * @param string $requestUrl * @param mixed[] $options Set by method * * @return void */ public static function setRequestFullUri($requestUrl, array &$options) { if ('http' === parse_url($requestUrl, PHP_URL_SCHEME)) { $options['http']['request_fulluri'] = true; } else { unset($options['http']['request_fulluri']); } } /** * Searches $_SERVER for case-sensitive values * * @param string[] $names Names to search for * @param string|null $name Name of any found value * * @return string|null The found value */ private static function getProxyEnv(array $names, &$name) { foreach ($names as $name) { if (!empty($_SERVER[$name])) { return $_SERVER[$name]; } } return null; } /** * Checks and formats a proxy url from the environment * * @param string $proxyUrl * @param string $envName * @throws \RuntimeException on malformed url * @return string The formatted proxy url */ private static function checkProxy($proxyUrl, $envName) { $error = sprintf('malformed %s url', $envName); $proxy = parse_url($proxyUrl); // We need parse_url to have identified a host if (!isset($proxy['host'])) { throw new \RuntimeException($error); } $proxyUrl = self::formatParsedUrl($proxy, true); // We need a port because streams and curl use different defaults if (!parse_url($proxyUrl, PHP_URL_PORT)) { throw new \RuntimeException($error); } return $proxyUrl; } /** * Formats a url from its component parts * * @param array{scheme?: string, host: string, port?: int, user?: string, pass?: string} $proxy * @param bool $includeAuth * * @return string The formatted value */ private static function formatParsedUrl(array $proxy, $includeAuth) { $proxyUrl = isset($proxy['scheme']) ? strtolower($proxy['scheme']) . '://' : ''; if ($includeAuth && isset($proxy['user'])) { $proxyUrl .= $proxy['user']; if (isset($proxy['pass'])) { $proxyUrl .= ':' . $proxy['pass']; } $proxyUrl .= '@'; } $proxyUrl .= $proxy['host']; if (isset($proxy['port'])) { $proxyUrl .= ':' . $proxy['port']; } elseif (strpos($proxyUrl, 'http://') === 0) { $proxyUrl .= ':80'; } elseif (strpos($proxyUrl, 'https://') === 0) { $proxyUrl .= ':443'; } return $proxyUrl; } } * Jordi Boggiano * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Composer\Util\Http; use Composer\Downloader\TransportException; use Composer\Util\NoProxyPattern; use Composer\Util\Url; /** * @internal * @author John Stevenson */ class ProxyManager { /** @var ?string */ private $error = null; /** @var array{http: ?string, https: ?string} */ private $fullProxy; /** @var array{http: ?string, https: ?string} */ private $safeProxy; /** @var array{http: array{options: mixed[]|null}, https: array{options: mixed[]|null}} */ private $streams; /** @var bool */ private $hasProxy; /** @var ?string */ private $info = null; /** @var ?NoProxyPattern */ private $noProxyHandler = null; /** @var ?ProxyManager */ private static $instance = null; private function __construct() { $this->fullProxy = $this->safeProxy = array( 'http' => null, 'https' => null, ); $this->streams['http'] = $this->streams['https'] = array( 'options' => null, ); $this->hasProxy = false; $this->initProxyData(); } /** * @return ProxyManager */ public static function getInstance() { if (!self::$instance) { self::$instance = new self(); } return self::$instance; } /** * Clears the persistent instance * * @return void */ public static function reset() { self::$instance = null; } /** * Returns a RequestProxy instance for the request url * * @param string $requestUrl * @return RequestProxy */ public function getProxyForRequest($requestUrl) { if ($this->error) { throw new TransportException('Unable to use a proxy: '.$this->error); } $scheme = parse_url($requestUrl, PHP_URL_SCHEME) ?: 'http'; $proxyUrl = ''; $options = array(); $formattedProxyUrl = ''; if ($this->hasProxy && in_array($scheme, array('http', 'https'), true) && $this->fullProxy[$scheme]) { if ($this->noProxy($requestUrl)) { $formattedProxyUrl = 'excluded by no_proxy'; } else { $proxyUrl = $this->fullProxy[$scheme]; $options = $this->streams[$scheme]['options']; ProxyHelper::setRequestFullUri($requestUrl, $options); $formattedProxyUrl = $this->safeProxy[$scheme]; } } return new RequestProxy($proxyUrl, $options, $formattedProxyUrl); } /** * Returns true if a proxy is being used * * @return bool If false any error will be in $message */ public function isProxying() { return $this->hasProxy; } /** * Returns proxy configuration info which can be shown to the user * * @return string|null Safe proxy URL or an error message if setting up proxy failed or null if no proxy was configured */ public function getFormattedProxy() { return $this->hasProxy ? $this->info : $this->error; } /** * Initializes proxy values from the environment * * @return void */ private function initProxyData() { try { list($httpProxy, $httpsProxy, $noProxy) = ProxyHelper::getProxyData(); } catch (\RuntimeException $e) { $this->error = $e->getMessage(); return; } $info = array(); if ($httpProxy) { $info[] = $this->setData($httpProxy, 'http'); } if ($httpsProxy) { $info[] = $this->setData($httpsProxy, 'https'); } if ($this->hasProxy) { $this->info = implode(', ', $info); if ($noProxy) { $this->noProxyHandler = new NoProxyPattern($noProxy); } } } /** * Sets initial data * * @param non-empty-string $url Proxy url * @param 'http'|'https' $scheme Environment variable scheme * * @return non-empty-string */ private function setData($url, $scheme) { $safeProxy = Url::sanitize($url); $this->fullProxy[$scheme] = $url; $this->safeProxy[$scheme] = $safeProxy; $this->streams[$scheme]['options'] = ProxyHelper::getContextOptions($url); $this->hasProxy = true; return sprintf('%s=%s', $scheme, $safeProxy); } /** * Returns true if a url matches no_proxy value * * @param string $requestUrl * @return bool */ private function noProxy($requestUrl) { return $this->noProxyHandler && $this->noProxyHandler->test($requestUrl); } } * Jordi Boggiano * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Composer\Util\Http; use Composer\Util\Url; /** * @internal * @author John Stevenson */ class RequestProxy { /** @var mixed[] */ private $contextOptions; /** @var bool */ private $isSecure; /** @var string */ private $formattedUrl; /** @var string */ private $url; /** * @param string $url * @param mixed[] $contextOptions * @param string $formattedUrl */ public function __construct($url, array $contextOptions, $formattedUrl) { $this->url = $url; $this->contextOptions = $contextOptions; $this->formattedUrl = $formattedUrl; $this->isSecure = 0 === strpos($url, 'https://'); } /** * Returns an array of context options * * @return mixed[] */ public function getContextOptions() { return $this->contextOptions; } /** * Returns the safe proxy url from the last request * * @param string|null $format Output format specifier * @return string Safe proxy, no proxy or empty */ public function getFormattedUrl($format = '') { $result = ''; if ($this->formattedUrl) { $format = $format ?: '%s'; $result = sprintf($format, $this->formattedUrl); } return $result; } /** * Returns the proxy url * * @return string Proxy url or empty */ public function getUrl() { return $this->url; } /** * Returns true if this is a secure-proxy * * @return bool False if not secure or there is no proxy */ public function isSecure() { return $this->isSecure; } } * Jordi Boggiano * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Composer\Util\Http; use Composer\Config; use Composer\Downloader\MaxFileSizeExceededException; use Composer\IO\IOInterface; use Composer\Downloader\TransportException; use Composer\Pcre\Preg; use Composer\Util\StreamContextFactory; use Composer\Util\AuthHelper; use Composer\Util\Url; use Composer\Util\HttpDownloader; use React\Promise\Promise; /** * @internal * @author Jordi Boggiano * @author Nicolas Grekas * @phpstan-type Attributes array{retryAuthFailure: bool, redirects: int, retries: int, storeAuth: bool} * @phpstan-type Job array{url: string, origin: string, attributes: Attributes, options: mixed[], progress: mixed[], curlHandle: resource, filename: string|false, headerHandle: resource, bodyHandle: resource, resolve: callable, reject: callable} */ class CurlDownloader { /** @var ?resource */ private $multiHandle; /** @var ?resource */ private $shareHandle; /** @var Job[] */ private $jobs = array(); /** @var IOInterface */ private $io; /** @var Config */ private $config; /** @var AuthHelper */ private $authHelper; /** @var float */ private $selectTimeout = 5.0; /** @var int */ private $maxRedirects = 20; /** @var int */ private $maxRetries = 3; /** @var ProxyManager */ private $proxyManager; /** @var bool */ private $supportsSecureProxy; /** @var array */ protected $multiErrors = array( CURLM_BAD_HANDLE => array('CURLM_BAD_HANDLE', 'The passed-in handle is not a valid CURLM handle.'), CURLM_BAD_EASY_HANDLE => array('CURLM_BAD_EASY_HANDLE', "An easy handle was not good/valid. It could mean that it isn't an easy handle at all, or possibly that the handle already is in used by this or another multi handle."), CURLM_OUT_OF_MEMORY => array('CURLM_OUT_OF_MEMORY', 'You are doomed.'), CURLM_INTERNAL_ERROR => array('CURLM_INTERNAL_ERROR', 'This can only be returned if libcurl bugs. Please report it to us!'), ); /** @var mixed[] */ private static $options = array( 'http' => array( 'method' => CURLOPT_CUSTOMREQUEST, 'content' => CURLOPT_POSTFIELDS, 'header' => CURLOPT_HTTPHEADER, 'timeout' => CURLOPT_TIMEOUT, ), 'ssl' => array( 'cafile' => CURLOPT_CAINFO, 'capath' => CURLOPT_CAPATH, 'verify_peer' => CURLOPT_SSL_VERIFYPEER, 'verify_peer_name' => CURLOPT_SSL_VERIFYHOST, 'local_cert' => CURLOPT_SSLCERT, 'local_pk' => CURLOPT_SSLKEY, 'passphrase' => CURLOPT_SSLKEYPASSWD, ), ); /** @var array */ private static $timeInfo = array( 'total_time' => true, 'namelookup_time' => true, 'connect_time' => true, 'pretransfer_time' => true, 'starttransfer_time' => true, 'redirect_time' => true, ); /** * @param mixed[] $options * @param bool $disableTls */ public function __construct(IOInterface $io, Config $config, array $options = array(), $disableTls = false) { $this->io = $io; $this->config = $config; $this->multiHandle = $mh = curl_multi_init(); if (function_exists('curl_multi_setopt')) { curl_multi_setopt($mh, CURLMOPT_PIPELINING, PHP_VERSION_ID >= 70400 ? /* CURLPIPE_MULTIPLEX */ 2 : /*CURLPIPE_HTTP1 | CURLPIPE_MULTIPLEX*/ 3); if (defined('CURLMOPT_MAX_HOST_CONNECTIONS') && !defined('HHVM_VERSION')) { curl_multi_setopt($mh, CURLMOPT_MAX_HOST_CONNECTIONS, 8); } } if (function_exists('curl_share_init')) { $this->shareHandle = $sh = curl_share_init(); curl_share_setopt($sh, CURLSHOPT_SHARE, CURL_LOCK_DATA_COOKIE); curl_share_setopt($sh, CURLSHOPT_SHARE, CURL_LOCK_DATA_DNS); curl_share_setopt($sh, CURLSHOPT_SHARE, CURL_LOCK_DATA_SSL_SESSION); } $this->authHelper = new AuthHelper($io, $config); $this->proxyManager = ProxyManager::getInstance(); $version = curl_version(); $features = $version['features']; $this->supportsSecureProxy = defined('CURL_VERSION_HTTPS_PROXY') && ($features & CURL_VERSION_HTTPS_PROXY); } /** * @param callable $resolve * @param callable $reject * @param string $origin * @param string $url * @param mixed[] $options * @param ?string $copyTo * * @return int internal job id */ public function download($resolve, $reject, $origin, $url, $options, $copyTo = null) { $attributes = array(); if (isset($options['retry-auth-failure'])) { $attributes['retryAuthFailure'] = $options['retry-auth-failure']; unset($options['retry-auth-failure']); } return $this->initDownload($resolve, $reject, $origin, $url, $options, $copyTo, $attributes); } /** * @param callable $resolve * @param callable $reject * @param string $origin * @param string $url * @param mixed[] $options * @param ?string $copyTo * * @param array{retryAuthFailure?: bool, redirects?: int, retries?: int, storeAuth?: bool} $attributes * * @return int internal job id */ private function initDownload($resolve, $reject, $origin, $url, $options, $copyTo = null, array $attributes = array()) { $attributes = array_merge(array( 'retryAuthFailure' => true, 'redirects' => 0, 'retries' => 0, 'storeAuth' => false, ), $attributes); $originalOptions = $options; // check URL can be accessed (i.e. is not insecure), but allow insecure Packagist calls to $hashed providers as file integrity is verified with sha256 if (!Preg::isMatch('{^http://(repo\.)?packagist\.org/p/}', $url) || (false === strpos($url, '$') && false === strpos($url, '%24'))) { $this->config->prohibitUrlByConfig($url, $this->io); } $curlHandle = curl_init(); $headerHandle = fopen('php://temp/maxmemory:32768', 'w+b'); if (false === $headerHandle) { throw new \RuntimeException('Failed to open a temp stream to store curl headers'); } if ($copyTo) { $errorMessage = ''; // @phpstan-ignore-next-line set_error_handler(function ($code, $msg) use (&$errorMessage) { if ($errorMessage) { $errorMessage .= "\n"; } $errorMessage .= Preg::replace('{^fopen\(.*?\): }', '', $msg); }); $bodyHandle = fopen($copyTo.'~', 'w+b'); restore_error_handler(); if (!$bodyHandle) { throw new TransportException('The "'.$url.'" file could not be written to '.$copyTo.': '.$errorMessage); } } else { $bodyHandle = @fopen('php://temp/maxmemory:524288', 'w+b'); } curl_setopt($curlHandle, CURLOPT_URL, $url); curl_setopt($curlHandle, CURLOPT_FOLLOWLOCATION, false); curl_setopt($curlHandle, CURLOPT_CONNECTTIMEOUT, 10); curl_setopt($curlHandle, CURLOPT_TIMEOUT, max((int) ini_get("default_socket_timeout"), 300)); curl_setopt($curlHandle, CURLOPT_WRITEHEADER, $headerHandle); curl_setopt($curlHandle, CURLOPT_FILE, $bodyHandle); curl_setopt($curlHandle, CURLOPT_ENCODING, ""); // let cURL set the Accept-Encoding header to what it supports curl_setopt($curlHandle, CURLOPT_PROTOCOLS, CURLPROTO_HTTP | CURLPROTO_HTTPS); if (function_exists('curl_share_init')) { curl_setopt($curlHandle, CURLOPT_SHARE, $this->shareHandle); } if (!isset($options['http']['header'])) { $options['http']['header'] = array(); } $options['http']['header'] = array_diff($options['http']['header'], array('Connection: close')); $options['http']['header'][] = 'Connection: keep-alive'; $version = curl_version(); $features = $version['features']; if (0 === strpos($url, 'https://') && \defined('CURL_VERSION_HTTP2') && \defined('CURL_HTTP_VERSION_2_0') && (CURL_VERSION_HTTP2 & $features)) { curl_setopt($curlHandle, CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_2_0); } $options['http']['header'] = $this->authHelper->addAuthenticationHeader($options['http']['header'], $origin, $url); $options = StreamContextFactory::initOptions($url, $options, true); foreach (self::$options as $type => $curlOptions) { foreach ($curlOptions as $name => $curlOption) { if (isset($options[$type][$name])) { if ($type === 'ssl' && $name === 'verify_peer_name') { curl_setopt($curlHandle, $curlOption, $options[$type][$name] === true ? 2 : $options[$type][$name]); } else { curl_setopt($curlHandle, $curlOption, $options[$type][$name]); } } } } // Always set CURLOPT_PROXY to enable/disable proxy handling // Any proxy authorization is included in the proxy url $proxy = $this->proxyManager->getProxyForRequest($url); curl_setopt($curlHandle, CURLOPT_PROXY, $proxy->getUrl()); // Curl needs certificate locations for secure proxies. // CURLOPT_PROXY_SSL_VERIFY_PEER/HOST are enabled by default if ($proxy->isSecure()) { if (!$this->supportsSecureProxy) { throw new TransportException('Connecting to a secure proxy using curl is not supported on PHP versions below 7.3.0.'); } if (!empty($options['ssl']['cafile'])) { curl_setopt($curlHandle, CURLOPT_PROXY_CAINFO, $options['ssl']['cafile']); } if (!empty($options['ssl']['capath'])) { curl_setopt($curlHandle, CURLOPT_PROXY_CAPATH, $options['ssl']['capath']); } } $progress = array_diff_key(curl_getinfo($curlHandle), self::$timeInfo); $this->jobs[(int) $curlHandle] = array( 'url' => $url, 'origin' => $origin, 'attributes' => $attributes, 'options' => $originalOptions, 'progress' => $progress, 'curlHandle' => $curlHandle, 'filename' => $copyTo, 'headerHandle' => $headerHandle, 'bodyHandle' => $bodyHandle, 'resolve' => $resolve, 'reject' => $reject, ); $usingProxy = $proxy->getFormattedUrl(' using proxy (%s)'); $ifModified = false !== stripos(implode(',', $options['http']['header']), 'if-modified-since:') ? ' if modified' : ''; if ($attributes['redirects'] === 0 && $attributes['retries'] === 0) { $this->io->writeError('Downloading ' . Url::sanitize($url) . $usingProxy . $ifModified, true, IOInterface::DEBUG); } $this->checkCurlResult(curl_multi_add_handle($this->multiHandle, $curlHandle)); // TODO progress return (int) $curlHandle; } /** * @param int $id * @return void */ public function abortRequest($id) { if (isset($this->jobs[$id], $this->jobs[$id]['curlHandle'])) { $job = $this->jobs[$id]; curl_multi_remove_handle($this->multiHandle, $job['curlHandle']); curl_close($job['curlHandle']); if (is_resource($job['headerHandle'])) { fclose($job['headerHandle']); } if (is_resource($job['bodyHandle'])) { fclose($job['bodyHandle']); } if ($job['filename']) { @unlink($job['filename'].'~'); } unset($this->jobs[$id]); } } /** * @return void */ public function tick() { static $timeoutWarning = false; if (!$this->jobs) { return; } $active = true; $this->checkCurlResult(curl_multi_exec($this->multiHandle, $active)); if (-1 === curl_multi_select($this->multiHandle, $this->selectTimeout)) { // sleep in case select returns -1 as it can happen on old php versions or some platforms where curl does not manage to do the select usleep(150); } while ($progress = curl_multi_info_read($this->multiHandle)) { $curlHandle = $progress['handle']; $result = $progress['result']; $i = (int) $curlHandle; if (!isset($this->jobs[$i])) { continue; } $progress = curl_getinfo($curlHandle); $job = $this->jobs[$i]; unset($this->jobs[$i]); $error = curl_error($curlHandle); $errno = curl_errno($curlHandle); curl_multi_remove_handle($this->multiHandle, $curlHandle); curl_close($curlHandle); $headers = null; $statusCode = null; $response = null; try { // TODO progress if (CURLE_OK !== $errno || $error || $result !== CURLE_OK) { $errno = $errno ?: $result; if (!$error && function_exists('curl_strerror')) { $error = curl_strerror($errno); } $progress['error_code'] = $errno; if ( (!isset($job['options']['http']['method']) || $job['options']['http']['method'] === 'GET') && ( in_array($errno, array(7 /* CURLE_COULDNT_CONNECT */, 16 /* CURLE_HTTP2 */, 92 /* CURLE_HTTP2_STREAM */), true) || ($errno === 35 /* CURLE_SSL_CONNECT_ERROR */ && false !== strpos($error, 'Connection reset by peer')) ) && $job['attributes']['retries'] < $this->maxRetries ) { $this->io->writeError('Retrying ('.($job['attributes']['retries'] + 1).') ' . Url::sanitize($job['url']) . ' due to curl error '. $errno, true, IOInterface::DEBUG); $this->restartJob($job, $job['url'], array('retries' => $job['attributes']['retries'] + 1)); continue; } if ($errno === 28 /* CURLE_OPERATION_TIMEDOUT */ && isset($progress['namelookup_time']) && $progress['namelookup_time'] == 0 && !$timeoutWarning) { $timeoutWarning = true; $this->io->writeError('A connection timeout was encountered. If you intend to run Composer without connecting to the internet, run the command again prefixed with COMPOSER_DISABLE_NETWORK=1 to make Composer run in offline mode.'); } throw new TransportException('curl error '.$errno.' while downloading '.Url::sanitize($progress['url']).': '.$error); } $statusCode = $progress['http_code']; rewind($job['headerHandle']); $headers = explode("\r\n", rtrim(stream_get_contents($job['headerHandle']))); fclose($job['headerHandle']); if ($statusCode === 0) { throw new \LogicException('Received unexpected http status code 0 without error for '.Url::sanitize($progress['url']).': headers '.var_export($headers, true).' curl info '.var_export($progress, true)); } // prepare response object if ($job['filename']) { $contents = $job['filename'].'~'; if ($statusCode >= 300) { rewind($job['bodyHandle']); $contents = stream_get_contents($job['bodyHandle']); } $response = new CurlResponse(array('url' => $progress['url']), $statusCode, $headers, $contents, $progress); $this->io->writeError('['.$statusCode.'] '.Url::sanitize($progress['url']), true, IOInterface::DEBUG); } else { rewind($job['bodyHandle']); $contents = stream_get_contents($job['bodyHandle']); $response = new CurlResponse(array('url' => $progress['url']), $statusCode, $headers, $contents, $progress); $this->io->writeError('['.$statusCode.'] '.Url::sanitize($progress['url']), true, IOInterface::DEBUG); } fclose($job['bodyHandle']); if ($response->getStatusCode() >= 400 && $response->getHeader('content-type') === 'application/json') { HttpDownloader::outputWarnings($this->io, $job['origin'], json_decode($response->getBody(), true)); } $result = $this->isAuthenticatedRetryNeeded($job, $response); if ($result['retry']) { $this->restartJob($job, $job['url'], array('storeAuth' => $result['storeAuth'])); continue; } // handle 3xx redirects, 304 Not Modified is excluded if ($statusCode >= 300 && $statusCode <= 399 && $statusCode !== 304 && $job['attributes']['redirects'] < $this->maxRedirects) { $location = $this->handleRedirect($job, $response); if ($location) { $this->restartJob($job, $location, array('redirects' => $job['attributes']['redirects'] + 1)); continue; } } // fail 4xx and 5xx responses and capture the response if ($statusCode >= 400 && $statusCode <= 599) { if ( (!isset($job['options']['http']['method']) || $job['options']['http']['method'] === 'GET') && in_array($statusCode, array(423, 425, 500, 502, 503, 504, 507, 510), true) && $job['attributes']['retries'] < $this->maxRetries ) { $this->io->writeError('Retrying ('.($job['attributes']['retries'] + 1).') ' . Url::sanitize($job['url']) . ' due to status code '. $statusCode, true, IOInterface::DEBUG); $this->restartJob($job, $job['url'], array('retries' => $job['attributes']['retries'] + 1)); continue; } throw $this->failResponse($job, $response, $response->getStatusMessage()); } if ($job['attributes']['storeAuth']) { $this->authHelper->storeAuth($job['origin'], $job['attributes']['storeAuth']); } // resolve promise if ($job['filename']) { rename($job['filename'].'~', $job['filename']); call_user_func($job['resolve'], $response); } else { call_user_func($job['resolve'], $response); } } catch (\Exception $e) { if ($e instanceof TransportException && $headers) { $e->setHeaders($headers); $e->setStatusCode($statusCode); } if ($e instanceof TransportException && $response) { $e->setResponse($response->getBody()); } if ($e instanceof TransportException && $progress) { $e->setResponseInfo($progress); } $this->rejectJob($job, $e); } } foreach ($this->jobs as $i => $curlHandle) { if (!isset($this->jobs[$i])) { continue; } $curlHandle = $this->jobs[$i]['curlHandle']; $progress = array_diff_key(curl_getinfo($curlHandle), self::$timeInfo); if ($this->jobs[$i]['progress'] !== $progress) { $this->jobs[$i]['progress'] = $progress; if (isset($this->jobs[$i]['options']['max_file_size'])) { // Compare max_file_size with the content-length header this value will be -1 until the header is parsed if ($this->jobs[$i]['options']['max_file_size'] < $progress['download_content_length']) { $this->rejectJob($this->jobs[$i], new MaxFileSizeExceededException('Maximum allowed download size reached. Content-length header indicates ' . $progress['download_content_length'] . ' bytes. Allowed ' . $this->jobs[$i]['options']['max_file_size'] . ' bytes')); } // Compare max_file_size with the download size in bytes if ($this->jobs[$i]['options']['max_file_size'] < $progress['size_download']) { $this->rejectJob($this->jobs[$i], new MaxFileSizeExceededException('Maximum allowed download size reached. Downloaded ' . $progress['size_download'] . ' of allowed ' . $this->jobs[$i]['options']['max_file_size'] . ' bytes')); } } // TODO progress } } } /** * @param Job $job * @return string */ private function handleRedirect(array $job, Response $response) { if ($locationHeader = $response->getHeader('location')) { if (parse_url($locationHeader, PHP_URL_SCHEME)) { // Absolute URL; e.g. https://example.com/composer $targetUrl = $locationHeader; } elseif (parse_url($locationHeader, PHP_URL_HOST)) { // Scheme relative; e.g. //example.com/foo $targetUrl = parse_url($job['url'], PHP_URL_SCHEME).':'.$locationHeader; } elseif ('/' === $locationHeader[0]) { // Absolute path; e.g. /foo $urlHost = parse_url($job['url'], PHP_URL_HOST); // Replace path using hostname as an anchor. $targetUrl = Preg::replace('{^(.+(?://|@)'.preg_quote($urlHost).'(?::\d+)?)(?:[/\?].*)?$}', '\1'.$locationHeader, $job['url']); } else { // Relative path; e.g. foo // This actually differs from PHP which seems to add duplicate slashes. $targetUrl = Preg::replace('{^(.+/)[^/?]*(?:\?.*)?$}', '\1'.$locationHeader, $job['url']); } } if (!empty($targetUrl)) { $this->io->writeError(sprintf('Following redirect (%u) %s', $job['attributes']['redirects'] + 1, Url::sanitize($targetUrl)), true, IOInterface::DEBUG); return $targetUrl; } throw new TransportException('The "'.$job['url'].'" file could not be downloaded, got redirect without Location ('.$response->getStatusMessage().')'); } /** * @param Job $job * @return array{retry: bool, storeAuth: string|bool} */ private function isAuthenticatedRetryNeeded(array $job, Response $response) { if (in_array($response->getStatusCode(), array(401, 403)) && $job['attributes']['retryAuthFailure']) { $result = $this->authHelper->promptAuthIfNeeded($job['url'], $job['origin'], $response->getStatusCode(), $response->getStatusMessage(), $response->getHeaders()); if ($result['retry']) { return $result; } } $locationHeader = $response->getHeader('location'); $needsAuthRetry = false; // check for bitbucket login page asking to authenticate if ( $job['origin'] === 'bitbucket.org' && !$this->authHelper->isPublicBitBucketDownload($job['url']) && substr($job['url'], -4) === '.zip' && (!$locationHeader || substr($locationHeader, -4) !== '.zip') && Preg::isMatch('{^text/html\b}i', $response->getHeader('content-type')) ) { $needsAuthRetry = 'Bitbucket requires authentication and it was not provided'; } // check for gitlab 404 when downloading archives if ( $response->getStatusCode() === 404 && in_array($job['origin'], $this->config->get('gitlab-domains'), true) && false !== strpos($job['url'], 'archive.zip') ) { $needsAuthRetry = 'GitLab requires authentication and it was not provided'; } if ($needsAuthRetry) { if ($job['attributes']['retryAuthFailure']) { $result = $this->authHelper->promptAuthIfNeeded($job['url'], $job['origin'], 401); if ($result['retry']) { return $result; } } throw $this->failResponse($job, $response, $needsAuthRetry); } return array('retry' => false, 'storeAuth' => false); } /** * @param Job $job * @param string $url * * @param array{retryAuthFailure?: bool, redirects?: int, storeAuth?: bool} $attributes * * @return void */ private function restartJob(array $job, $url, array $attributes = array()) { if ($job['filename']) { @unlink($job['filename'].'~'); } $attributes = array_merge($job['attributes'], $attributes); $origin = Url::getOrigin($this->config, $url); $this->initDownload($job['resolve'], $job['reject'], $origin, $url, $job['options'], $job['filename'], $attributes); } /** * @param Job $job * @param string $errorMessage * @return TransportException */ private function failResponse(array $job, Response $response, $errorMessage) { if ($job['filename']) { @unlink($job['filename'].'~'); } $details = ''; if (in_array(strtolower($response->getHeader('content-type')), array('application/json', 'application/json; charset=utf-8'), true)) { $details = ':'.PHP_EOL.substr($response->getBody(), 0, 200).(strlen($response->getBody()) > 200 ? '...' : ''); } return new TransportException('The "'.$job['url'].'" file could not be downloaded ('.$errorMessage.')' . $details, $response->getStatusCode()); } /** * @param Job $job * @return void */ private function rejectJob(array $job, \Exception $e) { if (is_resource($job['headerHandle'])) { fclose($job['headerHandle']); } if (is_resource($job['bodyHandle'])) { fclose($job['bodyHandle']); } if ($job['filename']) { @unlink($job['filename'].'~'); } call_user_func($job['reject'], $e); } /** * @param int $code * @return void */ private function checkCurlResult($code) { if ($code != CURLM_OK && $code != CURLM_CALL_MULTI_PERFORM) { throw new \RuntimeException( isset($this->multiErrors[$code]) ? "cURL error: {$code} ({$this->multiErrors[$code][0]}): cURL message: {$this->multiErrors[$code][1]}" : 'Unexpected cURL error: ' . $code ); } } } * Jordi Boggiano * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Composer\Util\Http; /** * @phpstan-type CurlInfo array{url: mixed, content_type: mixed, http_code: mixed, header_size: mixed, request_size: mixed, filetime: mixed, ssl_verify_result: mixed, redirect_count: mixed, total_time: mixed, namelookup_time: mixed, connect_time: mixed, pretransfer_time: mixed, size_upload: mixed, size_download: mixed, speed_download: mixed, speed_upload: mixed, download_content_length: mixed, upload_content_length: mixed, starttransfer_time: mixed, redirect_time: mixed, certinfo: mixed, primary_ip: mixed, primary_port: mixed, local_ip: mixed, local_port: mixed, redirect_url: mixed} */ class CurlResponse extends Response { /** * @see https://www.php.net/curl_getinfo * @var CurlInfo */ private $curlInfo; /** * @param CurlInfo $curlInfo */ public function __construct(array $request, $code, array $headers, $body, array $curlInfo) { parent::__construct($request, $code, $headers, $body); $this->curlInfo = $curlInfo; } /** * @return CurlInfo */ public function getCurlInfo() { return $this->curlInfo; } } * Jordi Boggiano * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Composer\Util; /** * @author Wissem Riahi */ class Tar { /** * @param string $pathToArchive * * @return string|null */ public static function getComposerJson($pathToArchive) { $phar = new \PharData($pathToArchive); if (!$phar->valid()) { return null; } return self::extractComposerJsonFromFolder($phar); } /** * @param \PharData $phar * * @throws \RuntimeException * * @return string */ private static function extractComposerJsonFromFolder(\PharData $phar) { if (isset($phar['composer.json'])) { return $phar['composer.json']->getContent(); } $topLevelPaths = array(); foreach ($phar as $folderFile) { $name = $folderFile->getBasename(); if ($folderFile->isDir()) { $topLevelPaths[$name] = true; if (\count($topLevelPaths) > 1) { throw new \RuntimeException('Archive has more than one top level directories, and no composer.json was found on the top level, so it\'s an invalid archive. Top level paths found were: '.implode(',', array_keys($topLevelPaths))); } } } $composerJsonPath = key($topLevelPaths).'/composer.json'; if ($topLevelPaths && isset($phar[$composerJsonPath])) { return $phar[$composerJsonPath]->getContent(); } throw new \RuntimeException('No composer.json found either at the top level or within the topmost directory'); } } * Jordi Boggiano * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Composer\Util; use Composer\Pcre\Preg; use React\Promise\PromiseInterface; use RecursiveDirectoryIterator; use RecursiveIteratorIterator; use Symfony\Component\Filesystem\Exception\IOException; use Symfony\Component\Finder\Finder; /** * @author Jordi Boggiano * @author Johannes M. Schmitt */ class Filesystem { /** @var ?ProcessExecutor */ private $processExecutor; public function __construct(ProcessExecutor $executor = null) { $this->processExecutor = $executor; } /** * @param string $file * * @return bool */ public function remove($file) { if (is_dir($file)) { return $this->removeDirectory($file); } if (file_exists($file)) { return $this->unlink($file); } return false; } /** * Checks if a directory is empty * * @param string $dir * @return bool */ public function isDirEmpty($dir) { $finder = Finder::create() ->ignoreVCS(false) ->ignoreDotFiles(false) ->depth(0) ->in($dir); return \count($finder) === 0; } /** * @param string $dir * @param bool $ensureDirectoryExists * * @return void */ public function emptyDirectory($dir, $ensureDirectoryExists = true) { if (is_link($dir) && file_exists($dir)) { $this->unlink($dir); } if ($ensureDirectoryExists) { $this->ensureDirectoryExists($dir); } if (is_dir($dir)) { $finder = Finder::create() ->ignoreVCS(false) ->ignoreDotFiles(false) ->depth(0) ->in($dir); foreach ($finder as $path) { $this->remove((string) $path); } } } /** * Recursively remove a directory * * Uses the process component if proc_open is enabled on the PHP * installation. * * @param string $directory * @throws \RuntimeException * @return bool */ public function removeDirectory($directory) { $edgeCaseResult = $this->removeEdgeCases($directory); if ($edgeCaseResult !== null) { return $edgeCaseResult; } if (Platform::isWindows()) { $cmd = sprintf('rmdir /S /Q %s', ProcessExecutor::escape(realpath($directory))); } else { $cmd = sprintf('rm -rf %s', ProcessExecutor::escape($directory)); } $result = $this->getProcess()->execute($cmd, $output) === 0; // clear stat cache because external processes aren't tracked by the php stat cache clearstatcache(); if ($result && !is_dir($directory)) { return true; } return $this->removeDirectoryPhp($directory); } /** * Recursively remove a directory asynchronously * * Uses the process component if proc_open is enabled on the PHP * installation. * * @param string $directory * @throws \RuntimeException * @return PromiseInterface */ public function removeDirectoryAsync($directory) { $edgeCaseResult = $this->removeEdgeCases($directory); if ($edgeCaseResult !== null) { return \React\Promise\resolve($edgeCaseResult); } if (Platform::isWindows()) { $cmd = sprintf('rmdir /S /Q %s', ProcessExecutor::escape(realpath($directory))); } else { $cmd = sprintf('rm -rf %s', ProcessExecutor::escape($directory)); } $promise = $this->getProcess()->executeAsync($cmd); $self = $this; return $promise->then(function ($process) use ($directory, $self) { // clear stat cache because external processes aren't tracked by the php stat cache clearstatcache(); if ($process->isSuccessful()) { if (!is_dir($directory)) { return \React\Promise\resolve(true); } } return \React\Promise\resolve($self->removeDirectoryPhp($directory)); }); } /** * @param string $directory * @param bool $fallbackToPhp * * @return bool|null Returns null, when no edge case was hit. Otherwise a bool whether removal was successful */ private function removeEdgeCases($directory, $fallbackToPhp = true) { if ($this->isSymlinkedDirectory($directory)) { return $this->unlinkSymlinkedDirectory($directory); } if ($this->isJunction($directory)) { return $this->removeJunction($directory); } if (is_link($directory)) { return unlink($directory); } if (!is_dir($directory) || !file_exists($directory)) { return true; } if (Preg::isMatch('{^(?:[a-z]:)?[/\\\\]+$}i', $directory)) { throw new \RuntimeException('Aborting an attempted deletion of '.$directory.', this was probably not intended, if it is a real use case please report it.'); } if (!\function_exists('proc_open') && $fallbackToPhp) { return $this->removeDirectoryPhp($directory); } return null; } /** * Recursively delete directory using PHP iterators. * * Uses a CHILD_FIRST RecursiveIteratorIterator to sort files * before directories, creating a single non-recursive loop * to delete files/directories in the correct order. * * @param string $directory * @return bool */ public function removeDirectoryPhp($directory) { $edgeCaseResult = $this->removeEdgeCases($directory, false); if ($edgeCaseResult !== null) { return $edgeCaseResult; } try { $it = new RecursiveDirectoryIterator($directory, RecursiveDirectoryIterator::SKIP_DOTS); } catch (\UnexpectedValueException $e) { // re-try once after clearing the stat cache if it failed as it // sometimes fails without apparent reason, see https://github.com/composer/composer/issues/4009 clearstatcache(); usleep(100000); if (!is_dir($directory)) { return true; } $it = new RecursiveDirectoryIterator($directory, RecursiveDirectoryIterator::SKIP_DOTS); } $ri = new RecursiveIteratorIterator($it, RecursiveIteratorIterator::CHILD_FIRST); foreach ($ri as $file) { if ($file->isDir()) { $this->rmdir($file->getPathname()); } else { $this->unlink($file->getPathname()); } } // release locks on the directory, see https://github.com/composer/composer/issues/9945 unset($ri, $it, $file); return $this->rmdir($directory); } /** * @param string $directory * * @return void */ public function ensureDirectoryExists($directory) { if (!is_dir($directory)) { if (file_exists($directory)) { throw new \RuntimeException( $directory.' exists and is not a directory.' ); } if (!@mkdir($directory, 0777, true)) { throw new \RuntimeException( $directory.' does not exist and could not be created.' ); } } } /** * Attempts to unlink a file and in case of failure retries after 350ms on windows * * @param string $path * @throws \RuntimeException * @return bool */ public function unlink($path) { $unlinked = @$this->unlinkImplementation($path); if (!$unlinked) { // retry after a bit on windows since it tends to be touchy with mass removals if (Platform::isWindows()) { usleep(350000); $unlinked = @$this->unlinkImplementation($path); } if (!$unlinked) { $error = error_get_last(); $message = 'Could not delete '.$path.': ' . @$error['message']; if (Platform::isWindows()) { $message .= "\nThis can be due to an antivirus or the Windows Search Indexer locking the file while they are analyzed"; } throw new \RuntimeException($message); } } return true; } /** * Attempts to rmdir a file and in case of failure retries after 350ms on windows * * @param string $path * @throws \RuntimeException * @return bool */ public function rmdir($path) { $deleted = @rmdir($path); if (!$deleted) { // retry after a bit on windows since it tends to be touchy with mass removals if (Platform::isWindows()) { usleep(350000); $deleted = @rmdir($path); } if (!$deleted) { $error = error_get_last(); $message = 'Could not delete '.$path.': ' . @$error['message']; if (Platform::isWindows()) { $message .= "\nThis can be due to an antivirus or the Windows Search Indexer locking the file while they are analyzed"; } throw new \RuntimeException($message); } } return true; } /** * Copy then delete is a non-atomic version of {@link rename}. * * Some systems can't rename and also don't have proc_open, * which requires this solution. * * @param string $source * @param string $target * * @return void */ public function copyThenRemove($source, $target) { $this->copy($source, $target); if (!is_dir($source)) { $this->unlink($source); return; } $this->removeDirectoryPhp($source); } /** * Copies a file or directory from $source to $target. * * @param string $source * @param string $target * @return bool */ public function copy($source, $target) { if (!is_dir($source)) { return copy($source, $target); } $it = new RecursiveDirectoryIterator($source, RecursiveDirectoryIterator::SKIP_DOTS); $ri = new RecursiveIteratorIterator($it, RecursiveIteratorIterator::SELF_FIRST); $this->ensureDirectoryExists($target); $result = true; /** @var RecursiveDirectoryIterator $ri */ foreach ($ri as $file) { $targetPath = $target . DIRECTORY_SEPARATOR . $ri->getSubPathname(); if ($file->isDir()) { $this->ensureDirectoryExists($targetPath); } else { $result = $result && copy($file->getPathname(), $targetPath); } } return $result; } /** * @param string $source * @param string $target * * @return void */ public function rename($source, $target) { if (true === @rename($source, $target)) { return; } if (!\function_exists('proc_open')) { $this->copyThenRemove($source, $target); return; } if (Platform::isWindows()) { // Try to copy & delete - this is a workaround for random "Access denied" errors. $command = sprintf('xcopy %s %s /E /I /Q /Y', ProcessExecutor::escape($source), ProcessExecutor::escape($target)); $result = $this->getProcess()->execute($command, $output); // clear stat cache because external processes aren't tracked by the php stat cache clearstatcache(); if (0 === $result) { $this->remove($source); return; } } else { // We do not use PHP's "rename" function here since it does not support // the case where $source, and $target are located on different partitions. $command = sprintf('mv %s %s', ProcessExecutor::escape($source), ProcessExecutor::escape($target)); $result = $this->getProcess()->execute($command, $output); // clear stat cache because external processes aren't tracked by the php stat cache clearstatcache(); if (0 === $result) { return; } } $this->copyThenRemove($source, $target); } /** * Returns the shortest path from $from to $to * * @param string $from * @param string $to * @param bool $directories if true, the source/target are considered to be directories * @throws \InvalidArgumentException * @return string */ public function findShortestPath($from, $to, $directories = false) { if (!$this->isAbsolutePath($from) || !$this->isAbsolutePath($to)) { throw new \InvalidArgumentException(sprintf('$from (%s) and $to (%s) must be absolute paths.', $from, $to)); } $from = lcfirst($this->normalizePath($from)); $to = lcfirst($this->normalizePath($to)); if ($directories) { $from = rtrim($from, '/') . '/dummy_file'; } if (\dirname($from) === \dirname($to)) { return './'.basename($to); } $commonPath = $to; while (strpos($from.'/', $commonPath.'/') !== 0 && '/' !== $commonPath && !Preg::isMatch('{^[a-z]:/?$}i', $commonPath)) { $commonPath = strtr(\dirname($commonPath), '\\', '/'); } if (0 !== strpos($from, $commonPath) || '/' === $commonPath) { return $to; } $commonPath = rtrim($commonPath, '/') . '/'; $sourcePathDepth = substr_count(substr($from, \strlen($commonPath)), '/'); $commonPathCode = str_repeat('../', $sourcePathDepth); return ($commonPathCode . substr($to, \strlen($commonPath))) ?: './'; } /** * Returns PHP code that, when executed in $from, will return the path to $to * * @param string $from * @param string $to * @param bool $directories if true, the source/target are considered to be directories * @param bool $staticCode * @throws \InvalidArgumentException * @return string */ public function findShortestPathCode($from, $to, $directories = false, $staticCode = false) { if (!$this->isAbsolutePath($from) || !$this->isAbsolutePath($to)) { throw new \InvalidArgumentException(sprintf('$from (%s) and $to (%s) must be absolute paths.', $from, $to)); } $from = lcfirst($this->normalizePath($from)); $to = lcfirst($this->normalizePath($to)); if ($from === $to) { return $directories ? '__DIR__' : '__FILE__'; } $commonPath = $to; while (strpos($from.'/', $commonPath.'/') !== 0 && '/' !== $commonPath && !Preg::isMatch('{^[a-z]:/?$}i', $commonPath) && '.' !== $commonPath) { $commonPath = strtr(\dirname($commonPath), '\\', '/'); } if (0 !== strpos($from, $commonPath) || '/' === $commonPath || '.' === $commonPath) { return var_export($to, true); } $commonPath = rtrim($commonPath, '/') . '/'; if (strpos($to, $from.'/') === 0) { return '__DIR__ . '.var_export(substr($to, \strlen($from)), true); } $sourcePathDepth = substr_count(substr($from, \strlen($commonPath)), '/') + $directories; if ($staticCode) { $commonPathCode = "__DIR__ . '".str_repeat('/..', $sourcePathDepth)."'"; } else { $commonPathCode = str_repeat('dirname(', $sourcePathDepth).'__DIR__'.str_repeat(')', $sourcePathDepth); } $relTarget = substr($to, \strlen($commonPath)); return $commonPathCode . (\strlen($relTarget) ? '.' . var_export('/' . $relTarget, true) : ''); } /** * Checks if the given path is absolute * * @param string $path * @return bool */ public function isAbsolutePath($path) { return strpos($path, '/') === 0 || substr($path, 1, 1) === ':' || strpos($path, '\\\\') === 0; } /** * Returns size of a file or directory specified by path. If a directory is * given, its size will be computed recursively. * * @param string $path Path to the file or directory * @throws \RuntimeException * @return int */ public function size($path) { if (!file_exists($path)) { throw new \RuntimeException("$path does not exist."); } if (is_dir($path)) { return $this->directorySize($path); } return filesize($path); } /** * Normalize a path. This replaces backslashes with slashes, removes ending * slash and collapses redundant separators and up-level references. * * @param string $path Path to the file or directory * @return string */ public function normalizePath($path) { $parts = array(); $path = strtr($path, '\\', '/'); $prefix = ''; $absolute = ''; // extract windows UNC paths e.g. \\foo\bar if (strpos($path, '//') === 0 && \strlen($path) > 2) { $absolute = '//'; $path = substr($path, 2); } // extract a prefix being a protocol://, protocol:, protocol://drive: or simply drive: if (Preg::isMatch('{^( [0-9a-z]{2,}+: (?: // (?: [a-z]: )? )? | [a-z]: )}ix', $path, $match)) { $prefix = $match[1]; $path = substr($path, \strlen($prefix)); } if (strpos($path, '/') === 0) { $absolute = '/'; $path = substr($path, 1); } $up = false; foreach (explode('/', $path) as $chunk) { if ('..' === $chunk && ($absolute !== '' || $up)) { array_pop($parts); $up = !(empty($parts) || '..' === end($parts)); } elseif ('.' !== $chunk && '' !== $chunk) { $parts[] = $chunk; $up = '..' !== $chunk; } } return $prefix.((string) $absolute).implode('/', $parts); } /** * Remove trailing slashes if present to avoid issues with symlinks * * And other possible unforeseen disasters, see https://github.com/composer/composer/pull/9422 * * @param string $path * @return string */ public static function trimTrailingSlash($path) { if (!Preg::isMatch('{^[/\\\\]+$}', $path)) { $path = rtrim($path, '/\\'); } return $path; } /** * Return if the given path is local * * @param string $path * @return bool */ public static function isLocalPath($path) { return Preg::isMatch('{^(file://(?!//)|/(?!/)|/?[a-z]:[\\\\/]|\.\.[\\\\/]|[a-z0-9_.-]+[\\\\/])}i', $path); } /** * @param string $path * * @return string */ public static function getPlatformPath($path) { if (Platform::isWindows()) { $path = Preg::replace('{^(?:file:///([a-z]):?/)}i', 'file://$1:/', $path); } return (string) Preg::replace('{^file://}i', '', $path); } /** * Cross-platform safe version of is_readable() * * This will also check for readability by reading the file as is_readable can not be trusted on network-mounts * and \\wsl$ paths. See https://github.com/composer/composer/issues/8231 and https://bugs.php.net/bug.php?id=68926 * * @param string $path * @return bool */ public static function isReadable($path) { if (is_readable($path)) { return true; } if (is_file($path)) { return false !== Silencer::call('file_get_contents', $path, false, null, 0, 1); } if (is_dir($path)) { return false !== Silencer::call('opendir', $path); } // assume false otherwise return false; } /** * @param string $directory * * @return int */ protected function directorySize($directory) { $it = new RecursiveDirectoryIterator($directory, RecursiveDirectoryIterator::SKIP_DOTS); $ri = new RecursiveIteratorIterator($it, RecursiveIteratorIterator::CHILD_FIRST); $size = 0; foreach ($ri as $file) { if ($file->isFile()) { $size += $file->getSize(); } } return $size; } /** * @return ProcessExecutor */ protected function getProcess() { if (!$this->processExecutor) { $this->processExecutor = new ProcessExecutor(); } return $this->processExecutor; } /** * delete symbolic link implementation (commonly known as "unlink()") * * symbolic links on windows which link to directories need rmdir instead of unlink * * @param string $path * * @return bool */ private function unlinkImplementation($path) { if (Platform::isWindows() && is_dir($path) && is_link($path)) { return rmdir($path); } return unlink($path); } /** * Creates a relative symlink from $link to $target * * @param string $target The path of the binary file to be symlinked * @param string $link The path where the symlink should be created * @return bool */ public function relativeSymlink($target, $link) { if (!function_exists('symlink')) { return false; } $cwd = getcwd(); $relativePath = $this->findShortestPath($link, $target); chdir(\dirname($link)); $result = @symlink($relativePath, $link); chdir($cwd); return $result; } /** * return true if that directory is a symlink. * * @param string $directory * * @return bool */ public function isSymlinkedDirectory($directory) { if (!is_dir($directory)) { return false; } $resolved = $this->resolveSymlinkedDirectorySymlink($directory); return is_link($resolved); } /** * @param string $directory * * @return bool */ private function unlinkSymlinkedDirectory($directory) { $resolved = $this->resolveSymlinkedDirectorySymlink($directory); return $this->unlink($resolved); } /** * resolve pathname to symbolic link of a directory * * @param string $pathname directory path to resolve * * @return string resolved path to symbolic link or original pathname (unresolved) */ private function resolveSymlinkedDirectorySymlink($pathname) { if (!is_dir($pathname)) { return $pathname; } $resolved = rtrim($pathname, '/'); if (!\strlen($resolved)) { return $pathname; } return $resolved; } /** * Creates an NTFS junction. * * @param string $target * @param string $junction * * @return void */ public function junction($target, $junction) { if (!Platform::isWindows()) { throw new \LogicException(sprintf('Function %s is not available on non-Windows platform', __CLASS__)); } if (!is_dir($target)) { throw new IOException(sprintf('Cannot junction to "%s" as it is not a directory.', $target), 0, null, $target); } $cmd = sprintf( 'mklink /J %s %s', ProcessExecutor::escape(str_replace('/', DIRECTORY_SEPARATOR, $junction)), ProcessExecutor::escape(realpath($target)) ); if ($this->getProcess()->execute($cmd, $output) !== 0) { throw new IOException(sprintf('Failed to create junction to "%s" at "%s".', $target, $junction), 0, null, $target); } clearstatcache(true, $junction); } /** * Returns whether the target directory is a Windows NTFS Junction. * * We test if the path is a directory and not an ordinary link, then check * that the mode value returned from lstat (which gives the status of the * link itself) is not a directory, by replicating the POSIX S_ISDIR test. * * This logic works because PHP does not set the mode value for a junction, * since there is no universal file type flag for it. Unfortunately an * uninitialized variable in PHP prior to 7.2.16 and 7.3.3 may cause a * random value to be returned. See https://bugs.php.net/bug.php?id=77552 * * If this random value passes the S_ISDIR test, then a junction will not be * detected and a recursive delete operation could lead to loss of data in * the target directory. Note that Windows rmdir can handle this situation * and will only delete the junction (from Windows 7 onwards). * * @param string $junction Path to check. * @return bool */ public function isJunction($junction) { if (!Platform::isWindows()) { return false; } // Important to clear all caches first clearstatcache(true, $junction); if (!is_dir($junction) || is_link($junction)) { return false; } $stat = lstat($junction); // S_ISDIR test (S_IFDIR is 0x4000, S_IFMT is 0xF000 bitmask) return $stat ? 0x4000 !== ($stat['mode'] & 0xF000) : false; } /** * Removes a Windows NTFS junction. * * @param string $junction * @return bool */ public function removeJunction($junction) { if (!Platform::isWindows()) { return false; } $junction = rtrim(str_replace('/', DIRECTORY_SEPARATOR, $junction), DIRECTORY_SEPARATOR); if (!$this->isJunction($junction)) { throw new IOException(sprintf('%s is not a junction and thus cannot be removed as one', $junction)); } return $this->rmdir($junction); } /** * @param string $path * @param string $content * * @return int|false */ public function filePutContentsIfModified($path, $content) { $currentContent = @file_get_contents($path); if (!$currentContent || ($currentContent != $content)) { return file_put_contents($path, $content); } return 0; } /** * Copy file using stream_copy_to_stream to work around https://bugs.php.net/bug.php?id=6463 * * @param string $source * @param string $target * * @return void */ public function safeCopy($source, $target) { if (!file_exists($target) || !file_exists($source) || !$this->filesAreEqual($source, $target)) { $source = fopen($source, 'r'); $target = fopen($target, 'w+'); stream_copy_to_stream($source, $target); fclose($source); fclose($target); } } /** * compare 2 files * https://stackoverflow.com/questions/3060125/can-i-use-file-get-contents-to-compare-two-files * * @param string $a * @param string $b * * @return bool */ private function filesAreEqual($a, $b) { // Check if filesize is different if (filesize($a) !== filesize($b)) { return false; } // Check if content is different $ah = fopen($a, 'rb'); $bh = fopen($b, 'rb'); $result = true; while (!feof($ah)) { if (fread($ah, 8192) != fread($bh, 8192)) { $result = false; break; } } fclose($ah); fclose($bh); return $result; } } * Jordi Boggiano * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Composer\Util; use Composer\Pcre\Preg; /** * Platform helper for uniform platform-specific tests. * * @author Niels Keurentjes */ class Platform { /** @var ?bool */ private static $isVirtualBoxGuest = null; /** @var ?bool */ private static $isWindowsSubsystemForLinux = null; /** * getenv() equivalent but reads from the runtime global variables first * * @param string $name * @return string|false */ public static function getEnv($name) { if (array_key_exists($name, $_SERVER)) { return (string) $_SERVER[$name]; } if (array_key_exists($name, $_ENV)) { return (string) $_ENV[$name]; } return getenv($name); } /** * putenv() equivalent but updates the runtime global variables too * * @param string $name * @param string $value * @return void */ public static function putEnv($name, $value) { $value = (string) $value; putenv($name . '=' . $value); $_SERVER[$name] = $_ENV[$name] = $value; } /** * putenv('X') equivalent but updates the runtime global variables too * * @param string $name * @return void */ public static function clearEnv($name) { putenv($name); unset($_SERVER[$name], $_ENV[$name]); } /** * Parses tildes and environment variables in paths. * * @param string $path * @return string */ public static function expandPath($path) { if (Preg::isMatch('#^~[\\/]#', $path)) { return self::getUserDirectory() . substr($path, 1); } return Preg::replaceCallback('#^(\$|(?P%))(?P\w++)(?(percent)%)(?P.*)#', function ($matches) { // Treat HOME as an alias for USERPROFILE on Windows for legacy reasons if (Platform::isWindows() && $matches['var'] == 'HOME') { return (Platform::getEnv('HOME') ?: Platform::getEnv('USERPROFILE')) . $matches['path']; } return Platform::getEnv($matches['var']) . $matches['path']; }, $path); } /** * @throws \RuntimeException If the user home could not reliably be determined * @return string The formal user home as detected from environment parameters */ public static function getUserDirectory() { if (false !== ($home = self::getEnv('HOME'))) { return $home; } if (self::isWindows() && false !== ($home = self::getEnv('USERPROFILE'))) { return $home; } if (\function_exists('posix_getuid') && \function_exists('posix_getpwuid')) { $info = posix_getpwuid(posix_getuid()); return $info['dir']; } throw new \RuntimeException('Could not determine user directory'); } /** * @return bool Whether the host machine is running on the Windows Subsystem for Linux (WSL) */ public static function isWindowsSubsystemForLinux() { if (null === self::$isWindowsSubsystemForLinux) { self::$isWindowsSubsystemForLinux = false; // while WSL will be hosted within windows, WSL itself cannot be windows based itself. if (self::isWindows()) { return self::$isWindowsSubsystemForLinux = false; } if ( !ini_get('open_basedir') && is_readable('/proc/version') && false !== stripos(Silencer::call('file_get_contents', '/proc/version'), 'microsoft') && !file_exists('/.dockerenv') // docker running inside WSL should not be seen as WSL ) { return self::$isWindowsSubsystemForLinux = true; } } return self::$isWindowsSubsystemForLinux; } /** * @return bool Whether the host machine is running a Windows OS */ public static function isWindows() { return \defined('PHP_WINDOWS_VERSION_BUILD'); } /** * @param string $str * @return int return a guaranteed binary length of the string, regardless of silly mbstring configs */ public static function strlen($str) { static $useMbString = null; if (null === $useMbString) { $useMbString = \function_exists('mb_strlen') && ini_get('mbstring.func_overload'); } if ($useMbString) { return mb_strlen($str, '8bit'); } return \strlen($str); } /** * @param ?resource $fd Open file descriptor or null to default to STDOUT * @return bool */ public static function isTty($fd = null) { if ($fd === null) { $fd = defined('STDOUT') ? STDOUT : fopen('php://stdout', 'w'); } // detect msysgit/mingw and assume this is a tty because detection // does not work correctly, see https://github.com/composer/composer/issues/9690 if (in_array(strtoupper(self::getEnv('MSYSTEM') ?: ''), array('MINGW32', 'MINGW64'), true)) { return true; } // modern cross-platform function, includes the fstat // fallback so if it is present we trust it if (function_exists('stream_isatty')) { return stream_isatty($fd); } // only trusting this if it is positive, otherwise prefer fstat fallback if (function_exists('posix_isatty') && posix_isatty($fd)) { return true; } $stat = @fstat($fd); // Check if formatted mode is S_IFCHR return $stat ? 0020000 === ($stat['mode'] & 0170000) : false; } /** * @return void */ public static function workaroundFilesystemIssues() { if (self::isVirtualBoxGuest()) { usleep(200000); } } /** * Attempts detection of VirtualBox guest VMs * * This works based on the process' user being "vagrant", the COMPOSER_RUNTIME_ENV env var being set to "virtualbox", or lsmod showing the virtualbox guest additions are loaded * * @return bool */ private static function isVirtualBoxGuest() { if (null === self::$isVirtualBoxGuest) { self::$isVirtualBoxGuest = false; if (self::isWindows()) { return self::$isVirtualBoxGuest; } if (function_exists('posix_getpwuid') && function_exists('posix_geteuid')) { $processUser = posix_getpwuid(posix_geteuid()); if ($processUser && $processUser['name'] === 'vagrant') { return self::$isVirtualBoxGuest = true; } } if (self::getEnv('COMPOSER_RUNTIME_ENV') === 'virtualbox') { return self::$isVirtualBoxGuest = true; } if (defined('PHP_OS_FAMILY') && PHP_OS_FAMILY === 'Linux') { $process = new ProcessExecutor(); try { if (0 === $process->execute('lsmod | grep vboxguest', $ignoredOutput)) { return self::$isVirtualBoxGuest = true; } } catch (\Exception $e) { // noop } } } return self::$isVirtualBoxGuest; } } * Jordi Boggiano * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Composer\Util; use Composer\Composer; use Composer\CaBundle\CaBundle; use Composer\Downloader\TransportException; use Composer\Repository\PlatformRepository; use Composer\Util\Http\ProxyManager; use Psr\Log\LoggerInterface; /** * Allows the creation of a basic context supporting http proxy * * @author Jordan Alliot * @author Markus Tacker */ final class StreamContextFactory { /** * Creates a context supporting HTTP proxies * * @param string $url URL the context is to be used for * @phpstan-param array{http?: array{follow_location?: int, max_redirects?: int, header?: string|array}} $defaultOptions * @param mixed[] $defaultOptions Options to merge with the default * @param mixed[] $defaultParams Parameters to specify on the context * @throws \RuntimeException if https proxy required and OpenSSL uninstalled * @return resource Default context */ public static function getContext($url, array $defaultOptions = array(), array $defaultParams = array()) { $options = array('http' => array( // specify defaults again to try and work better with curlwrappers enabled 'follow_location' => 1, 'max_redirects' => 20, )); $options = array_replace_recursive($options, self::initOptions($url, $defaultOptions)); unset($defaultOptions['http']['header']); $options = array_replace_recursive($options, $defaultOptions); if (isset($options['http']['header'])) { $options['http']['header'] = self::fixHttpHeaderField($options['http']['header']); } return stream_context_create($options, $defaultParams); } /** * @param string $url * @param mixed[] $options * @param bool $forCurl When true, will not add proxy values as these are handled separately * @phpstan-return array{http: array{header: string[], proxy?: string, request_fulluri: bool}, ssl?: mixed[]} * @return array formatted as a stream context array */ public static function initOptions($url, array $options, $forCurl = false) { // Make sure the headers are in an array form if (!isset($options['http']['header'])) { $options['http']['header'] = array(); } if (is_string($options['http']['header'])) { $options['http']['header'] = explode("\r\n", $options['http']['header']); } // Add stream proxy options if there is a proxy if (!$forCurl) { $proxy = ProxyManager::getInstance()->getProxyForRequest($url); if ($proxyOptions = $proxy->getContextOptions()) { $isHttpsRequest = 0 === strpos($url, 'https://'); if ($proxy->isSecure()) { if (!extension_loaded('openssl')) { throw new TransportException('You must enable the openssl extension to use a secure proxy.'); } if ($isHttpsRequest) { throw new TransportException('You must enable the curl extension to make https requests through a secure proxy.'); } } elseif ($isHttpsRequest && !extension_loaded('openssl')) { throw new TransportException('You must enable the openssl extension to make https requests through a proxy.'); } // Header will be a Proxy-Authorization string or not set if (isset($proxyOptions['http']['header'])) { $options['http']['header'][] = $proxyOptions['http']['header']; unset($proxyOptions['http']['header']); } $options = array_replace_recursive($options, $proxyOptions); } } if (defined('HHVM_VERSION')) { $phpVersion = 'HHVM ' . HHVM_VERSION; } else { $phpVersion = 'PHP ' . PHP_MAJOR_VERSION . '.' . PHP_MINOR_VERSION . '.' . PHP_RELEASE_VERSION; } if ($forCurl) { $curl = curl_version(); $httpVersion = 'cURL '.$curl['version']; } else { $httpVersion = 'streams'; } if (!isset($options['http']['header']) || false === stripos(implode('', $options['http']['header']), 'user-agent')) { $platformPhpVersion = PlatformRepository::getPlatformPhpVersion(); $options['http']['header'][] = sprintf( 'User-Agent: Composer/%s (%s; %s; %s; %s%s%s)', Composer::getVersion(), function_exists('php_uname') ? php_uname('s') : 'Unknown', function_exists('php_uname') ? php_uname('r') : 'Unknown', $phpVersion, $httpVersion, $platformPhpVersion ? '; Platform-PHP '.$platformPhpVersion : '', Platform::getEnv('CI') ? '; CI' : '' ); } return $options; } /** * @param mixed[] $options * * @return mixed[] */ public static function getTlsDefaults(array $options, LoggerInterface $logger = null) { $ciphers = implode(':', array( 'ECDHE-RSA-AES128-GCM-SHA256', 'ECDHE-ECDSA-AES128-GCM-SHA256', 'ECDHE-RSA-AES256-GCM-SHA384', 'ECDHE-ECDSA-AES256-GCM-SHA384', 'DHE-RSA-AES128-GCM-SHA256', 'DHE-DSS-AES128-GCM-SHA256', 'kEDH+AESGCM', 'ECDHE-RSA-AES128-SHA256', 'ECDHE-ECDSA-AES128-SHA256', 'ECDHE-RSA-AES128-SHA', 'ECDHE-ECDSA-AES128-SHA', 'ECDHE-RSA-AES256-SHA384', 'ECDHE-ECDSA-AES256-SHA384', 'ECDHE-RSA-AES256-SHA', 'ECDHE-ECDSA-AES256-SHA', 'DHE-RSA-AES128-SHA256', 'DHE-RSA-AES128-SHA', 'DHE-DSS-AES128-SHA256', 'DHE-RSA-AES256-SHA256', 'DHE-DSS-AES256-SHA', 'DHE-RSA-AES256-SHA', 'AES128-GCM-SHA256', 'AES256-GCM-SHA384', 'AES128-SHA256', 'AES256-SHA256', 'AES128-SHA', 'AES256-SHA', 'AES', 'CAMELLIA', 'DES-CBC3-SHA', '!aNULL', '!eNULL', '!EXPORT', '!DES', '!RC4', '!MD5', '!PSK', '!aECDH', '!EDH-DSS-DES-CBC3-SHA', '!EDH-RSA-DES-CBC3-SHA', '!KRB5-DES-CBC3-SHA', )); /** * CN_match and SNI_server_name are only known once a URL is passed. * They will be set in the getOptionsForUrl() method which receives a URL. * * cafile or capath can be overridden by passing in those options to constructor. */ $defaults = array( 'ssl' => array( 'ciphers' => $ciphers, 'verify_peer' => true, 'verify_depth' => 7, 'SNI_enabled' => true, 'capture_peer_cert' => true, ), ); if (isset($options['ssl'])) { $defaults['ssl'] = array_replace_recursive($defaults['ssl'], $options['ssl']); } /** * Attempt to find a local cafile or throw an exception if none pre-set * The user may go download one if this occurs. */ if (!isset($defaults['ssl']['cafile']) && !isset($defaults['ssl']['capath'])) { $result = CaBundle::getSystemCaRootBundlePath($logger); if (is_dir($result)) { $defaults['ssl']['capath'] = $result; } else { $defaults['ssl']['cafile'] = $result; } } if (isset($defaults['ssl']['cafile']) && (!Filesystem::isReadable($defaults['ssl']['cafile']) || !CaBundle::validateCaFile($defaults['ssl']['cafile'], $logger))) { throw new TransportException('The configured cafile was not valid or could not be read.'); } if (isset($defaults['ssl']['capath']) && (!is_dir($defaults['ssl']['capath']) || !Filesystem::isReadable($defaults['ssl']['capath']))) { throw new TransportException('The configured capath was not valid or could not be read.'); } /** * Disable TLS compression to prevent CRIME attacks where supported. */ if (PHP_VERSION_ID >= 50413) { $defaults['ssl']['disable_compression'] = true; } return $defaults; } /** * A bug in PHP prevents the headers from correctly being sent when a content-type header is present and * NOT at the end of the array * * This method fixes the array by moving the content-type header to the end * * @link https://bugs.php.net/bug.php?id=61548 * @param string|string[] $header * @return string[] */ private static function fixHttpHeaderField($header) { if (!is_array($header)) { $header = explode("\r\n", $header); } uasort($header, function ($el) { return stripos($el, 'content-type') === 0 ? 1 : -1; }); return $header; } } * Jordi Boggiano * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Composer\Util; use Composer\Downloader\DownloaderInterface; use Composer\Package\PackageInterface; use React\Promise\PromiseInterface; class SyncHelper { /** * Helps you download + install a single package in a synchronous way * * This executes all the required steps and waits for promises to complete * * @param Loop $loop Loop instance which you can get from $composer->getLoop() * @param DownloaderInterface $downloader Downloader instance you can get from $composer->getDownloadManager()->getDownloader('zip') for example * @param string $path the installation path for the package * @param PackageInterface $package the package to install * @param PackageInterface|null $prevPackage the previous package if this is an update and not an initial installation * * @return void */ public static function downloadAndInstallPackageSync(Loop $loop, DownloaderInterface $downloader, $path, PackageInterface $package, PackageInterface $prevPackage = null) { $type = $prevPackage ? 'update' : 'install'; try { self::await($loop, $downloader->download($package, $path, $prevPackage)); self::await($loop, $downloader->prepare($type, $package, $path, $prevPackage)); if ($type === 'update') { self::await($loop, $downloader->update($package, $prevPackage, $path)); } else { self::await($loop, $downloader->install($package, $path)); } } catch (\Exception $e) { self::await($loop, $downloader->cleanup($type, $package, $path, $prevPackage)); throw $e; } self::await($loop, $downloader->cleanup($type, $package, $path, $prevPackage)); } /** * Waits for a promise to resolve * * @param Loop $loop Loop instance which you can get from $composer->getLoop() * @param PromiseInterface|null $promise * * @return void */ public static function await(Loop $loop, PromiseInterface $promise = null) { if ($promise) { $loop->wait(array($promise)); } } } * Jordi Boggiano * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Composer\Util; use Composer\Factory; use Composer\IO\IOInterface; use Composer\Config; use Composer\Downloader\TransportException; use Composer\Pcre\Preg; /** * @author Jordi Boggiano */ class GitHub { /** @var IOInterface */ protected $io; /** @var Config */ protected $config; /** @var ProcessExecutor */ protected $process; /** @var HttpDownloader */ protected $httpDownloader; /** * Constructor. * * @param IOInterface $io The IO instance * @param Config $config The composer configuration * @param ProcessExecutor $process Process instance, injectable for mocking * @param HttpDownloader $httpDownloader Remote Filesystem, injectable for mocking */ public function __construct(IOInterface $io, Config $config, ProcessExecutor $process = null, HttpDownloader $httpDownloader = null) { $this->io = $io; $this->config = $config; $this->process = $process ?: new ProcessExecutor($io); $this->httpDownloader = $httpDownloader ?: Factory::createHttpDownloader($this->io, $config); } /** * Attempts to authorize a GitHub domain via OAuth * * @param string $originUrl The host this GitHub instance is located at * @return bool true on success */ public function authorizeOAuth($originUrl) { if (!in_array($originUrl, $this->config->get('github-domains'))) { return false; } // if available use token from git config if (0 === $this->process->execute('git config github.accesstoken', $output)) { $this->io->setAuthentication($originUrl, trim($output), 'x-oauth-basic'); return true; } return false; } /** * Authorizes a GitHub domain interactively via OAuth * * @param string $originUrl The host this GitHub instance is located at * @param string $message The reason this authorization is required * @throws \RuntimeException * @throws TransportException|\Exception * @return bool true on success */ public function authorizeOAuthInteractively($originUrl, $message = null) { if ($message) { $this->io->writeError($message); } $note = 'Composer'; if ($this->config->get('github-expose-hostname') === true && 0 === $this->process->execute('hostname', $output)) { $note .= ' on ' . trim($output); } $note .= ' ' . date('Y-m-d Hi'); $url = 'https://'.$originUrl.'/settings/tokens/new?scopes=&description=' . str_replace('%20', '+', rawurlencode($note)); $this->io->writeError(sprintf('When working with _public_ GitHub repositories only, head to %s to retrieve a token.', $url)); $this->io->writeError('This token will have read-only permission for public information only.'); $url = 'https://'.$originUrl.'/settings/tokens/new?scopes=repo&description=' . str_replace('%20', '+', rawurlencode($note)); $this->io->writeError(sprintf('When you need to access _private_ GitHub repositories as well, go to %s', $url)); $this->io->writeError('Note that such tokens have broad read/write permissions on your behalf, even if not needed by Composer.'); $this->io->writeError(sprintf('Tokens will be stored in plain text in "%s" for future use by Composer.', $this->config->getAuthConfigSource()->getName())); $this->io->writeError('For additional information, check https://getcomposer.org/doc/articles/authentication-for-private-packages.md#github-oauth'); $token = trim($this->io->askAndHideAnswer('Token (hidden): ')); if (!$token) { $this->io->writeError('No token given, aborting.'); $this->io->writeError('You can also add it manually later by using "composer config --global --auth github-oauth.github.com "'); return false; } $this->io->setAuthentication($originUrl, $token, 'x-oauth-basic'); try { $apiUrl = ('github.com' === $originUrl) ? 'api.github.com/' : $originUrl . '/api/v3/'; $this->httpDownloader->get('https://'. $apiUrl, array( 'retry-auth-failure' => false, )); } catch (TransportException $e) { if (in_array($e->getCode(), array(403, 401))) { $this->io->writeError('Invalid token provided.'); $this->io->writeError('You can also add it manually later by using "composer config --global --auth github-oauth.github.com "'); return false; } throw $e; } // store value in user config $this->config->getConfigSource()->removeConfigSetting('github-oauth.'.$originUrl); $this->config->getAuthConfigSource()->addConfigSetting('github-oauth.'.$originUrl, $token); $this->io->writeError('Token stored successfully.'); return true; } /** * Extract rate limit from response. * * @param string[] $headers Headers from Composer\Downloader\TransportException. * * @return array{limit: int|'?', reset: string} */ public function getRateLimit(array $headers) { $rateLimit = array( 'limit' => '?', 'reset' => '?', ); foreach ($headers as $header) { $header = trim($header); if (false === strpos($header, 'X-RateLimit-')) { continue; } list($type, $value) = explode(':', $header, 2); switch ($type) { case 'X-RateLimit-Limit': $rateLimit['limit'] = (int) trim($value); break; case 'X-RateLimit-Reset': $rateLimit['reset'] = date('Y-m-d H:i:s', (int) trim($value)); break; } } return $rateLimit; } /** * Extract SSO URL from response. * * @param string[] $headers Headers from Composer\Downloader\TransportException. * * @return string|null */ public function getSsoUrl(array $headers) { foreach ($headers as $header) { $header = trim($header); if (false === stripos($header, 'x-github-sso: required')) { continue; } if (Preg::isMatch('{\burl=(?P[^\s;]+)}', $header, $match)) { return $match['url']; } } return null; } /** * Finds whether a request failed due to rate limiting * * @param string[] $headers Headers from Composer\Downloader\TransportException. * * @return bool */ public function isRateLimited(array $headers) { foreach ($headers as $header) { if (Preg::isMatch('{^X-RateLimit-Remaining: *0$}i', trim($header))) { return true; } } return false; } /** * Finds whether a request failed due to lacking SSO authorization * * @see https://docs.github.com/en/rest/overview/other-authentication-methods#authenticating-for-saml-sso * * @param string[] $headers Headers from Composer\Downloader\TransportException. * * @return bool */ public function requiresSso(array $headers) { foreach ($headers as $header) { if (Preg::isMatch('{^X-GitHub-SSO: required}i', trim($header))) { return true; } } return false; } } * Jordi Boggiano * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Composer\Util; use Composer\XdebugHandler\XdebugHandler; /** * Provides ini file location functions that work with and without a restart. * When the process has restarted it uses a tmp ini and stores the original * ini locations in an environment variable. * * @author John Stevenson */ class IniHelper { /** * Returns an array of php.ini locations with at least one entry * * The equivalent of calling php_ini_loaded_file then php_ini_scanned_files. * The loaded ini location is the first entry and may be empty. * * @return string[] */ public static function getAll() { return XdebugHandler::getAllIniFiles(); } /** * Describes the location of the loaded php.ini file(s) * * @return string */ public static function getMessage() { $paths = self::getAll(); if (empty($paths[0])) { array_shift($paths); } $ini = array_shift($paths); if (empty($ini)) { return 'A php.ini file does not exist. You will have to create one.'; } if (!empty($paths)) { return 'Your command-line PHP is using multiple ini files. Run `php --ini` to show them.'; } return 'The php.ini used by your command-line PHP is: '.$ini; } } * Jordi Boggiano * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Composer\Util; use Composer\Pcre\Preg; /** * Composer mirror utilities * * @author Jordi Boggiano */ class ComposerMirror { /** * @param string $mirrorUrl * @param string $packageName * @param string $version * @param string|null $reference * @param string|null $type * @param string|null $prettyVersion * * @return string */ public static function processUrl($mirrorUrl, $packageName, $version, $reference, $type, $prettyVersion = null) { if ($reference) { $reference = Preg::isMatch('{^([a-f0-9]*|%reference%)$}', $reference) ? $reference : md5($reference); } $version = strpos($version, '/') === false ? $version : md5($version); $from = array('%package%', '%version%', '%reference%', '%type%'); $to = array($packageName, $version, $reference, $type); if (null !== $prettyVersion) { $from[] = '%prettyVersion%'; $to[] = $prettyVersion; } return str_replace($from, $to, $mirrorUrl); } /** * @param string $mirrorUrl * @param string $packageName * @param string $url * @param string|null $type * * @return string */ public static function processGitUrl($mirrorUrl, $packageName, $url, $type) { if (Preg::isMatch('#^(?:(?:https?|git)://github\.com/|git@github\.com:)([^/]+)/(.+?)(?:\.git)?$#', $url, $match)) { $url = 'gh-'.$match[1].'/'.$match[2]; } elseif (Preg::isMatch('#^https://bitbucket\.org/([^/]+)/(.+?)(?:\.git)?/?$#', $url, $match)) { $url = 'bb-'.$match[1].'/'.$match[2]; } else { $url = Preg::replace('{[^a-z0-9_.-]}i', '-', trim($url, '/')); } return str_replace( array('%package%', '%normalizedUrl%', '%type%'), array($packageName, $url, $type), $mirrorUrl ); } /** * @param string $mirrorUrl * @param string $packageName * @param string $url * @param string $type * * @return string */ public static function processHgUrl($mirrorUrl, $packageName, $url, $type) { return self::processGitUrl($mirrorUrl, $packageName, $url, $type); } } * Jordi Boggiano * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Composer\Util; use Composer\Package\PackageInterface; class PackageSorter { /** * Sorts packages by dependency weight * * Packages of equal weight retain the original order * * @param PackageInterface[] $packages * @return PackageInterface[] sorted array */ public static function sortPackages(array $packages) { $usageList = array(); foreach ($packages as $package) { foreach (array_merge($package->getRequires(), $package->getDevRequires()) as $link) { $target = $link->getTarget(); $usageList[$target][] = $package->getName(); } } $computing = array(); $computed = array(); $computeImportance = function ($name) use (&$computeImportance, &$computing, &$computed, $usageList) { // reusing computed importance if (isset($computed[$name])) { return $computed[$name]; } // canceling circular dependency if (isset($computing[$name])) { return 0; } $computing[$name] = true; $weight = 0; if (isset($usageList[$name])) { foreach ($usageList[$name] as $user) { $weight -= 1 - $computeImportance($user); } } unset($computing[$name]); $computed[$name] = $weight; return $weight; }; $weightList = array(); foreach ($packages as $index => $package) { $weight = $computeImportance($package->getName()); $weightList[$index] = $weight; } $stable_sort = function (&$array) { static $transform, $restore; $i = 0; if (!$transform) { $transform = function (&$v, $k) use (&$i) { $v = array($v, ++$i, $k, $v); }; $restore = function (&$v) { $v = $v[3]; }; } array_walk($array, $transform); asort($array); array_walk($array, $restore); }; $stable_sort($weightList); $sortedPackages = array(); foreach (array_keys($weightList) as $index) { $sortedPackages[] = $packages[$index]; } return $sortedPackages; } } * Jordi Boggiano * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Composer\Util; use React\Promise\CancellablePromiseInterface; use Symfony\Component\Console\Helper\ProgressBar; use React\Promise\PromiseInterface; /** * @author Jordi Boggiano */ class Loop { /** @var HttpDownloader */ private $httpDownloader; /** @var ProcessExecutor|null */ private $processExecutor; /** @var PromiseInterface[][] */ private $currentPromises = array(); /** @var int */ private $waitIndex = 0; public function __construct(HttpDownloader $httpDownloader, ProcessExecutor $processExecutor = null) { $this->httpDownloader = $httpDownloader; $this->httpDownloader->enableAsync(); $this->processExecutor = $processExecutor; if ($this->processExecutor) { $this->processExecutor->enableAsync(); } } /** * @return HttpDownloader */ public function getHttpDownloader() { return $this->httpDownloader; } /** * @return ProcessExecutor|null */ public function getProcessExecutor() { return $this->processExecutor; } /** * @param PromiseInterface[] $promises * @param ?ProgressBar $progress * @return void */ public function wait(array $promises, ProgressBar $progress = null) { /** @var \Exception|null */ $uncaught = null; \React\Promise\all($promises)->then( function () { }, function ($e) use (&$uncaught) { $uncaught = $e; } ); // keep track of every group of promises that is waited on, so abortJobs can // cancel them all, even if wait() was called within a wait() $waitIndex = $this->waitIndex++; $this->currentPromises[$waitIndex] = $promises; if ($progress) { $totalJobs = 0; $totalJobs += $this->httpDownloader->countActiveJobs(); if ($this->processExecutor) { $totalJobs += $this->processExecutor->countActiveJobs(); } $progress->start($totalJobs); } $lastUpdate = 0; while (true) { $activeJobs = 0; $activeJobs += $this->httpDownloader->countActiveJobs(); if ($this->processExecutor) { $activeJobs += $this->processExecutor->countActiveJobs(); } if ($progress && microtime(true) - $lastUpdate > 0.1) { $lastUpdate = microtime(true); $progress->setProgress($progress->getMaxSteps() - $activeJobs); } if (!$activeJobs) { break; } } // as we skip progress updates if they are too quick, make sure we do one last one here at 100% if ($progress) { $progress->finish(); } unset($this->currentPromises[$waitIndex]); if ($uncaught) { throw $uncaught; } } /** * @return void */ public function abortJobs() { foreach ($this->currentPromises as $promiseGroup) { foreach ($promiseGroup as $promise) { if ($promise instanceof CancellablePromiseInterface) { $promise->cancel(); } } } } } * Jordi Boggiano * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Composer\Util; use Composer\Config; use Composer\Pcre\Preg; /** * @author Jordi Boggiano */ class Url { /** * @param Config $config * @param string $url * @param string $ref * @return string the updated URL */ public static function updateDistReference(Config $config, $url, $ref) { $host = parse_url($url, PHP_URL_HOST); if ($host === 'api.github.com' || $host === 'github.com' || $host === 'www.github.com') { if (Preg::isMatch('{^https?://(?:www\.)?github\.com/([^/]+)/([^/]+)/(zip|tar)ball/(.+)$}i', $url, $match)) { // update legacy github archives to API calls with the proper reference $url = 'https://api.github.com/repos/' . $match[1] . '/'. $match[2] . '/' . $match[3] . 'ball/' . $ref; } elseif (Preg::isMatch('{^https?://(?:www\.)?github\.com/([^/]+)/([^/]+)/archive/.+\.(zip|tar)(?:\.gz)?$}i', $url, $match)) { // update current github web archives to API calls with the proper reference $url = 'https://api.github.com/repos/' . $match[1] . '/'. $match[2] . '/' . $match[3] . 'ball/' . $ref; } elseif (Preg::isMatch('{^https?://api\.github\.com/repos/([^/]+)/([^/]+)/(zip|tar)ball(?:/.+)?$}i', $url, $match)) { // update api archives to the proper reference $url = 'https://api.github.com/repos/' . $match[1] . '/'. $match[2] . '/' . $match[3] . 'ball/' . $ref; } } elseif ($host === 'bitbucket.org' || $host === 'www.bitbucket.org') { if (Preg::isMatch('{^https?://(?:www\.)?bitbucket\.org/([^/]+)/([^/]+)/get/(.+)\.(zip|tar\.gz|tar\.bz2)$}i', $url, $match)) { // update Bitbucket archives to the proper reference $url = 'https://bitbucket.org/' . $match[1] . '/'. $match[2] . '/get/' . $ref . '.' . $match[4]; } } elseif ($host === 'gitlab.com' || $host === 'www.gitlab.com') { if (Preg::isMatch('{^https?://(?:www\.)?gitlab\.com/api/v[34]/projects/([^/]+)/repository/archive\.(zip|tar\.gz|tar\.bz2|tar)\?sha=.+$}i', $url, $match)) { // update Gitlab archives to the proper reference $url = 'https://gitlab.com/api/v4/projects/' . $match[1] . '/repository/archive.' . $match[2] . '?sha=' . $ref; } } elseif (in_array($host, $config->get('github-domains'), true)) { $url = Preg::replace('{(/repos/[^/]+/[^/]+/(zip|tar)ball)(?:/.+)?$}i', '$1/'.$ref, $url); } elseif (in_array($host, $config->get('gitlab-domains'), true)) { $url = Preg::replace('{(/api/v[34]/projects/[^/]+/repository/archive\.(?:zip|tar\.gz|tar\.bz2|tar)\?sha=).+$}i', '${1}'.$ref, $url); } return $url; } /** * @param string $url * @return string */ public static function getOrigin(Config $config, $url) { if (0 === strpos($url, 'file://')) { return $url; } $origin = (string) parse_url($url, PHP_URL_HOST); if ($port = parse_url($url, PHP_URL_PORT)) { $origin .= ':'.$port; } if (strpos($origin, '.github.com') === (strlen($origin) - 11)) { return 'github.com'; } if ($origin === 'repo.packagist.org') { return 'packagist.org'; } if ($origin === '') { $origin = $url; } // Gitlab can be installed in a non-root context (i.e. gitlab.com/foo). When downloading archives the originUrl // is the host without the path, so we look for the registered gitlab-domains matching the host here if ( is_array($config->get('gitlab-domains')) && false === strpos($origin, '/') && !in_array($origin, $config->get('gitlab-domains')) ) { foreach ($config->get('gitlab-domains') as $gitlabDomain) { if (0 === strpos($gitlabDomain, $origin)) { return $gitlabDomain; } } } return $origin; } /** * @param string $url * @return string */ public static function sanitize($url) { // GitHub repository rename result in redirect locations containing the access_token as GET parameter // e.g. https://api.github.com/repositories/9999999999?access_token=github_token $url = Preg::replace('{([&?]access_token=)[^&]+}', '$1***', $url); $url = Preg::replaceCallback('{^(?P[a-z0-9]+://)?(?P[^:/\s@]+):(?P[^@\s/]+)@}i', function ($m) { // if the username looks like a long (12char+) hex string, or a modern github token (e.g. ghp_xxx) we obfuscate that if (Preg::isMatch('{^([a-f0-9]{12,}|gh[a-z]_[a-zA-Z0-9_]+)$}', $m['user'])) { return $m['prefix'].'***:***@'; } return $m['prefix'].$m['user'].':***@'; }, $url); return $url; } } * Jordi Boggiano * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Composer; use Composer\Config\ConfigSourceInterface; use Composer\Downloader\TransportException; use Composer\IO\IOInterface; use Composer\Pcre\Preg; use Composer\Util\Platform; use Composer\Util\ProcessExecutor; /** * @author Jordi Boggiano */ class Config { const SOURCE_DEFAULT = 'default'; const SOURCE_COMMAND = 'command'; const SOURCE_UNKNOWN = 'unknown'; const RELATIVE_PATHS = 1; /** @var array */ public static $defaultConfig = array( 'process-timeout' => 300, 'use-include-path' => false, 'allow-plugins' => null, // null for BC for now, will become array() after July 2022 'use-parent-dir' => 'prompt', 'preferred-install' => 'dist', 'notify-on-install' => true, 'github-protocols' => array('https', 'ssh', 'git'), 'gitlab-protocol' => null, 'vendor-dir' => 'vendor', 'bin-dir' => '{$vendor-dir}/bin', 'cache-dir' => '{$home}/cache', 'data-dir' => '{$home}', 'cache-files-dir' => '{$cache-dir}/files', 'cache-repo-dir' => '{$cache-dir}/repo', 'cache-vcs-dir' => '{$cache-dir}/vcs', 'cache-ttl' => 15552000, // 6 months 'cache-files-ttl' => null, // fallback to cache-ttl 'cache-files-maxsize' => '300MiB', 'cache-read-only' => false, 'bin-compat' => 'auto', 'discard-changes' => false, 'autoloader-suffix' => null, 'sort-packages' => false, 'optimize-autoloader' => false, 'classmap-authoritative' => false, 'apcu-autoloader' => false, 'prepend-autoloader' => true, 'github-domains' => array('github.com'), 'bitbucket-expose-hostname' => true, 'disable-tls' => false, 'secure-http' => true, 'secure-svn-domains' => array(), 'cafile' => null, 'capath' => null, 'github-expose-hostname' => true, 'gitlab-domains' => array('gitlab.com'), 'store-auths' => 'prompt', 'platform' => array(), 'archive-format' => 'tar', 'archive-dir' => '.', 'htaccess-protect' => true, 'use-github-api' => true, 'lock' => true, 'platform-check' => 'php-only', // valid keys without defaults (auth config stuff): // bitbucket-oauth // github-oauth // gitlab-oauth // gitlab-token // http-basic // bearer ); /** @var array */ public static $defaultRepositories = array( 'packagist.org' => array( 'type' => 'composer', 'url' => 'https://repo.packagist.org', ), ); /** @var array */ private $config; /** @var ?string */ private $baseDir; /** @var array */ private $repositories; /** @var ConfigSourceInterface */ private $configSource; /** @var ConfigSourceInterface */ private $authConfigSource; /** @var bool */ private $useEnvironment; /** @var array */ private $warnedHosts = array(); /** @var array */ private $sourceOfConfigValue = array(); /** * @param bool $useEnvironment Use COMPOSER_ environment variables to replace config settings * @param string $baseDir Optional base directory of the config */ public function __construct($useEnvironment = true, $baseDir = null) { // load defaults $this->config = static::$defaultConfig; // TODO after July 2022 remove this and update the default value above in self::$defaultConfig + remove note from 06-config.md if (strtotime('2022-07-01') < time()) { $this->config['allow-plugins'] = array(); } $this->repositories = static::$defaultRepositories; $this->useEnvironment = (bool) $useEnvironment; $this->baseDir = $baseDir; foreach ($this->config as $configKey => $configValue) { $this->setSourceOfConfigValue($configValue, $configKey, self::SOURCE_DEFAULT); } foreach ($this->repositories as $configKey => $configValue) { $this->setSourceOfConfigValue($configValue, 'repositories.' . $configKey, self::SOURCE_DEFAULT); } } /** * @return void */ public function setConfigSource(ConfigSourceInterface $source) { $this->configSource = $source; } /** * @return ConfigSourceInterface */ public function getConfigSource() { return $this->configSource; } /** * @return void */ public function setAuthConfigSource(ConfigSourceInterface $source) { $this->authConfigSource = $source; } /** * @return ConfigSourceInterface */ public function getAuthConfigSource() { return $this->authConfigSource; } /** * Merges new config values with the existing ones (overriding) * * @param array{config?: array, repositories?: array} $config * @param string $source * * @return void */ public function merge($config, $source = self::SOURCE_UNKNOWN) { // override defaults with given config if (!empty($config['config']) && is_array($config['config'])) { foreach ($config['config'] as $key => $val) { if (in_array($key, array('bitbucket-oauth', 'github-oauth', 'gitlab-oauth', 'gitlab-token', 'http-basic', 'bearer'), true) && isset($this->config[$key])) { $this->config[$key] = array_merge($this->config[$key], $val); $this->setSourceOfConfigValue($val, $key, $source); } elseif (in_array($key, array('allow-plugins'), true) && isset($this->config[$key]) && is_array($this->config[$key])) { // merging $val first to get the local config on top of the global one, then appending the global config, // then merging local one again to make sure the values from local win over global ones for keys present in both $this->config[$key] = array_merge($val, $this->config[$key], $val); $this->setSourceOfConfigValue($val, $key, $source); } elseif (in_array($key, array('gitlab-domains', 'github-domains'), true) && isset($this->config[$key])) { $this->config[$key] = array_unique(array_merge($this->config[$key], $val)); $this->setSourceOfConfigValue($val, $key, $source); } elseif ('preferred-install' === $key && isset($this->config[$key])) { if (is_array($val) || is_array($this->config[$key])) { if (is_string($val)) { $val = array('*' => $val); } if (is_string($this->config[$key])) { $this->config[$key] = array('*' => $this->config[$key]); $this->sourceOfConfigValue[$key . '*'] = $source; } $this->config[$key] = array_merge($this->config[$key], $val); $this->setSourceOfConfigValue($val, $key, $source); // the full match pattern needs to be last if (isset($this->config[$key]['*'])) { $wildcard = $this->config[$key]['*']; unset($this->config[$key]['*']); $this->config[$key]['*'] = $wildcard; } } else { $this->config[$key] = $val; $this->setSourceOfConfigValue($val, $key, $source); } } else { $this->config[$key] = $val; $this->setSourceOfConfigValue($val, $key, $source); } } } if (!empty($config['repositories']) && is_array($config['repositories'])) { $this->repositories = array_reverse($this->repositories, true); $newRepos = array_reverse($config['repositories'], true); foreach ($newRepos as $name => $repository) { // disable a repository by name if (false === $repository) { $this->disableRepoByName((string) $name); continue; } // disable a repository with an anonymous {"name": false} repo if (is_array($repository) && 1 === count($repository) && false === current($repository)) { $this->disableRepoByName((string) key($repository)); continue; } // auto-deactivate the default packagist.org repo if it gets redefined if (isset($repository['type'], $repository['url']) && $repository['type'] === 'composer' && Preg::isMatch('{^https?://(?:[a-z0-9-.]+\.)?packagist.org(/|$)}', $repository['url'])) { $this->disableRepoByName('packagist.org'); } // store repo if (is_int($name)) { $this->repositories[] = $repository; $this->setSourceOfConfigValue($repository, 'repositories.' . array_search($repository, $this->repositories, true), $source); } else { if ($name === 'packagist') { // BC support for default "packagist" named repo $this->repositories[$name . '.org'] = $repository; $this->setSourceOfConfigValue($repository, 'repositories.' . $name . '.org', $source); } else { $this->repositories[$name] = $repository; $this->setSourceOfConfigValue($repository, 'repositories.' . $name, $source); } } } $this->repositories = array_reverse($this->repositories, true); } } /** * @return array */ public function getRepositories() { return $this->repositories; } /** * Returns a setting * * @param string $key * @param int $flags Options (see class constants) * @throws \RuntimeException * * @return mixed */ public function get($key, $flags = 0) { switch ($key) { // strings/paths with env var and {$refs} support case 'vendor-dir': case 'bin-dir': case 'process-timeout': case 'data-dir': case 'cache-dir': case 'cache-files-dir': case 'cache-repo-dir': case 'cache-vcs-dir': case 'cafile': case 'capath': // convert foo-bar to COMPOSER_FOO_BAR and check if it exists since it overrides the local config $env = 'COMPOSER_' . strtoupper(strtr($key, '-', '_')); $val = $this->getComposerEnv($env); if ($val !== false) { $this->setSourceOfConfigValue($val, $key, $env); } $val = rtrim((string) $this->process(false !== $val ? $val : $this->config[$key], $flags), '/\\'); $val = Platform::expandPath($val); if (substr($key, -4) !== '-dir') { return $val; } return (($flags & self::RELATIVE_PATHS) == self::RELATIVE_PATHS) ? $val : $this->realpath($val); // booleans with env var support case 'cache-read-only': case 'htaccess-protect': // convert foo-bar to COMPOSER_FOO_BAR and check if it exists since it overrides the local config $env = 'COMPOSER_' . strtoupper(strtr($key, '-', '_')); $val = $this->getComposerEnv($env); if (false === $val) { $val = $this->config[$key]; } else { $this->setSourceOfConfigValue($val, $key, $env); } return $val !== 'false' && (bool) $val; // booleans without env var support case 'disable-tls': case 'secure-http': case 'use-github-api': case 'lock': // special case for secure-http if ($key === 'secure-http' && $this->get('disable-tls') === true) { return false; } return $this->config[$key] !== 'false' && (bool) $this->config[$key]; // ints without env var support case 'cache-ttl': return (int) $this->config[$key]; // numbers with kb/mb/gb support, without env var support case 'cache-files-maxsize': if (!Preg::isMatch('/^\s*([0-9.]+)\s*(?:([kmg])(?:i?b)?)?\s*$/i', $this->config[$key], $matches)) { throw new \RuntimeException( "Could not parse the value of '$key': {$this->config[$key]}" ); } $size = $matches[1]; if (isset($matches[2])) { switch (strtolower($matches[2])) { case 'g': $size *= 1024; // intentional fallthrough // no break case 'm': $size *= 1024; // intentional fallthrough // no break case 'k': $size *= 1024; break; } } return $size; // special cases below case 'cache-files-ttl': if (isset($this->config[$key])) { return (int) $this->config[$key]; } return (int) $this->config['cache-ttl']; case 'home': $val = Preg::replace('#^(\$HOME|~)(/|$)#', rtrim(Platform::getEnv('HOME') ?: Platform::getEnv('USERPROFILE'), '/\\') . '/', $this->config[$key]); return rtrim($this->process($val, $flags), '/\\'); case 'bin-compat': $value = $this->getComposerEnv('COMPOSER_BIN_COMPAT') ?: $this->config[$key]; if (!in_array($value, array('auto', 'full', 'proxy', 'symlink'))) { throw new \RuntimeException( "Invalid value for 'bin-compat': {$value}. Expected auto, full or proxy" ); } if ($value === 'symlink') { trigger_error('config.bin-compat "symlink" is deprecated since Composer 2.2, use auto, full (for Windows compatibility) or proxy instead.', E_USER_DEPRECATED); } return $value; case 'discard-changes': if ($env = $this->getComposerEnv('COMPOSER_DISCARD_CHANGES')) { if (!in_array($env, array('stash', 'true', 'false', '1', '0'), true)) { throw new \RuntimeException( "Invalid value for COMPOSER_DISCARD_CHANGES: {$env}. Expected 1, 0, true, false or stash" ); } if ('stash' === $env) { return 'stash'; } // convert string value to bool return $env !== 'false' && (bool) $env; } if (!in_array($this->config[$key], array(true, false, 'stash'), true)) { throw new \RuntimeException( "Invalid value for 'discard-changes': {$this->config[$key]}. Expected true, false or stash" ); } return $this->config[$key]; case 'github-protocols': $protos = $this->config['github-protocols']; if ($this->config['secure-http'] && false !== ($index = array_search('git', $protos))) { unset($protos[$index]); } if (reset($protos) === 'http') { throw new \RuntimeException('The http protocol for github is not available anymore, update your config\'s github-protocols to use "https", "git" or "ssh"'); } return $protos; default: if (!isset($this->config[$key])) { return null; } return $this->process($this->config[$key], $flags); } } /** * @param int $flags * * @return array */ public function all($flags = 0) { $all = array( 'repositories' => $this->getRepositories(), ); foreach (array_keys($this->config) as $key) { $all['config'][$key] = $this->get($key, $flags); } return $all; } /** * @param string $key * @return string */ public function getSourceOfValue($key) { $this->get($key); return isset($this->sourceOfConfigValue[$key]) ? $this->sourceOfConfigValue[$key] : self::SOURCE_UNKNOWN; } /** * @param mixed $configValue * @param string $path * @param string $source * * @return void */ private function setSourceOfConfigValue($configValue, $path, $source) { $this->sourceOfConfigValue[$path] = $source; if (is_array($configValue)) { foreach ($configValue as $key => $value) { $this->setSourceOfConfigValue($value, $path . '.' . $key, $source); } } } /** * @return array */ public function raw() { return array( 'repositories' => $this->getRepositories(), 'config' => $this->config, ); } /** * Checks whether a setting exists * * @param string $key * @return bool */ public function has($key) { return array_key_exists($key, $this->config); } /** * Replaces {$refs} inside a config string * * @param string|int|null $value a config string that can contain {$refs-to-other-config} * @param int $flags Options (see class constants) * * @return string|int|null */ private function process($value, $flags) { $config = $this; if (!is_string($value)) { return $value; } return Preg::replaceCallback('#\{\$(.+)\}#', function ($match) use ($config, $flags) { return $config->get($match[1], $flags); }, $value); } /** * Turns relative paths in absolute paths without realpath() * * Since the dirs might not exist yet we can not call realpath or it will fail. * * @param string $path * @return string */ private function realpath($path) { if (Preg::isMatch('{^(?:/|[a-z]:|[a-z0-9.]+://)}i', $path)) { return $path; } return $this->baseDir ? $this->baseDir . '/' . $path : $path; } /** * Reads the value of a Composer environment variable * * This should be used to read COMPOSER_ environment variables * that overload config values. * * @param string $var * @return string|bool */ private function getComposerEnv($var) { if ($this->useEnvironment) { return Platform::getEnv($var); } return false; } /** * @param string $name * * @return void */ private function disableRepoByName($name) { if (isset($this->repositories[$name])) { unset($this->repositories[$name]); } elseif ($name === 'packagist') { // BC support for default "packagist" named repo unset($this->repositories['packagist.org']); } } /** * Validates that the passed URL is allowed to be used by current config, or throws an exception. * * @param string $url * @param IOInterface $io * * @return void */ public function prohibitUrlByConfig($url, IOInterface $io = null) { // Return right away if the URL is malformed or custom (see issue #5173) if (false === filter_var($url, FILTER_VALIDATE_URL)) { return; } // Extract scheme and throw exception on known insecure protocols $scheme = parse_url($url, PHP_URL_SCHEME); $hostname = parse_url($url, PHP_URL_HOST); if (in_array($scheme, array('http', 'git', 'ftp', 'svn'))) { if ($this->get('secure-http')) { if ($scheme === 'svn') { if (in_array($hostname, $this->get('secure-svn-domains'), true)) { return; } throw new TransportException("Your configuration does not allow connections to $url. See https://getcomposer.org/doc/06-config.md#secure-svn-domains for details."); } throw new TransportException("Your configuration does not allow connections to $url. See https://getcomposer.org/doc/06-config.md#secure-http for details."); } if ($io) { $host = parse_url($url, PHP_URL_HOST); if (is_string($host)) { if (!isset($this->warnedHosts[$host])) { $io->writeError("Warning: Accessing $host over $scheme which is an insecure protocol."); } $this->warnedHosts[$host] = true; } } } } /** * Used by long-running custom scripts in composer.json * * "scripts": { * "watch": [ * "Composer\\Config::disableProcessTimeout", * "vendor/bin/long-running-script --watch" * ] * } * * @return void */ public static function disableProcessTimeout() { // Override global timeout set earlier by environment or config ProcessExecutor::setTimeout(0); } } * Jordi Boggiano * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Composer\Script; use Composer\Composer; use Composer\IO\IOInterface; use Composer\EventDispatcher\Event as BaseEvent; /** * The script event class * * @author François Pluchino * @author Nils Adermann */ class Event extends BaseEvent { /** * @var Composer The composer instance */ private $composer; /** * @var IOInterface The IO instance */ private $io; /** * @var bool Dev mode flag */ private $devMode; /** * @var BaseEvent */ private $originatingEvent; /** * Constructor. * * @param string $name The event name * @param Composer $composer The composer object * @param IOInterface $io The IOInterface object * @param bool $devMode Whether or not we are in dev mode * @param array $args Arguments passed by the user * @param mixed[] $flags Optional flags to pass data not as argument */ public function __construct($name, Composer $composer, IOInterface $io, $devMode = false, array $args = array(), array $flags = array()) { parent::__construct($name, $args, $flags); $this->composer = $composer; $this->io = $io; $this->devMode = $devMode; } /** * Returns the composer instance. * * @return Composer */ public function getComposer() { return $this->composer; } /** * Returns the IO instance. * * @return IOInterface */ public function getIO() { return $this->io; } /** * Return the dev mode flag * * @return bool */ public function isDevMode() { return $this->devMode; } /** * Set the originating event. * * @return ?BaseEvent */ public function getOriginatingEvent() { return $this->originatingEvent; } /** * Set the originating event. * * @param BaseEvent $event * @return $this */ public function setOriginatingEvent(BaseEvent $event) { $this->originatingEvent = $this->calculateOriginatingEvent($event); return $this; } /** * Returns the upper-most event in chain. * * @param BaseEvent $event * @return BaseEvent */ private function calculateOriginatingEvent(BaseEvent $event) { if ($event instanceof Event && $event->getOriginatingEvent()) { return $this->calculateOriginatingEvent($event->getOriginatingEvent()); } return $event; } } * Jordi Boggiano * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Composer\Script; /** * The Script Events. * * @author François Pluchino * @author Jordi Boggiano */ class ScriptEvents { /** * The PRE_INSTALL_CMD event occurs before the install command is executed. * * The event listener method receives a Composer\Script\Event instance. * * @var string */ const PRE_INSTALL_CMD = 'pre-install-cmd'; /** * The POST_INSTALL_CMD event occurs after the install command is executed. * * The event listener method receives a Composer\Script\Event instance. * * @var string */ const POST_INSTALL_CMD = 'post-install-cmd'; /** * The PRE_UPDATE_CMD event occurs before the update command is executed. * * The event listener method receives a Composer\Script\Event instance. * * @var string */ const PRE_UPDATE_CMD = 'pre-update-cmd'; /** * The POST_UPDATE_CMD event occurs after the update command is executed. * * The event listener method receives a Composer\Script\Event instance. * * @var string */ const POST_UPDATE_CMD = 'post-update-cmd'; /** * The PRE_STATUS_CMD event occurs before the status command is executed. * * The event listener method receives a Composer\Script\Event instance. * * @var string */ const PRE_STATUS_CMD = 'pre-status-cmd'; /** * The POST_STATUS_CMD event occurs after the status command is executed. * * The event listener method receives a Composer\Script\Event instance. * * @var string */ const POST_STATUS_CMD = 'post-status-cmd'; /** * The PRE_AUTOLOAD_DUMP event occurs before the autoload file is generated. * * The event listener method receives a Composer\Script\Event instance. * * @var string */ const PRE_AUTOLOAD_DUMP = 'pre-autoload-dump'; /** * The POST_AUTOLOAD_DUMP event occurs after the autoload file has been generated. * * The event listener method receives a Composer\Script\Event instance. * * @var string */ const POST_AUTOLOAD_DUMP = 'post-autoload-dump'; /** * The POST_ROOT_PACKAGE_INSTALL event occurs after the root package has been installed. * * The event listener method receives a Composer\Script\Event instance. * * @var string */ const POST_ROOT_PACKAGE_INSTALL = 'post-root-package-install'; /** * The POST_CREATE_PROJECT event occurs after the create-project command has been executed. * Note: Event occurs after POST_INSTALL_CMD * * The event listener method receives a Composer\Script\Event instance. * * @var string */ const POST_CREATE_PROJECT_CMD = 'post-create-project-cmd'; /** * The PRE_ARCHIVE_CMD event occurs before the update command is executed. * * The event listener method receives a Composer\Script\Event instance. * * @var string */ const PRE_ARCHIVE_CMD = 'pre-archive-cmd'; /** * The POST_ARCHIVE_CMD event occurs after the status command is executed. * * The event listener method receives a Composer\Script\Event instance. * * @var string */ const POST_ARCHIVE_CMD = 'post-archive-cmd'; } * Jordi Boggiano * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Composer; use Composer\Json\JsonFile; use Composer\CaBundle\CaBundle; use Composer\Pcre\Preg; use Symfony\Component\Finder\Finder; use Symfony\Component\Process\Process; use Seld\PharUtils\Timestamps; use Seld\PharUtils\Linter; /** * The Compiler class compiles composer into a phar * * @author Fabien Potencier * @author Jordi Boggiano */ class Compiler { /** @var string */ private $version; /** @var string */ private $branchAliasVersion = ''; /** @var \DateTime */ private $versionDate; /** * Compiles composer into a single phar file * * @param string $pharFile The full path to the file to create * * @return void * * @throws \RuntimeException */ public function compile($pharFile = 'composer.phar') { if (file_exists($pharFile)) { unlink($pharFile); } // TODO in v2.3 always call with an array if (method_exists('Symfony\Component\Process\Process', 'fromShellCommandline')) { $process = new Process(array('git', 'log', '--pretty="%H"', '-n1', 'HEAD'), __DIR__); } else { // @phpstan-ignore-next-line $process = new Process('git log --pretty="%H" -n1 HEAD', __DIR__); } if ($process->run() != 0) { throw new \RuntimeException('Can\'t run git log. You must ensure to run compile from composer git repository clone and that git binary is available.'); } $this->version = trim($process->getOutput()); // TODO in v2.3 always call with an array if (method_exists('Symfony\Component\Process\Process', 'fromShellCommandline')) { $process = new Process(array('git', 'log', '-n1', '--pretty=%ci', 'HEAD'), __DIR__); } else { // @phpstan-ignore-next-line $process = new Process('git log -n1 --pretty=%ci HEAD', __DIR__); } if ($process->run() != 0) { throw new \RuntimeException('Can\'t run git log. You must ensure to run compile from composer git repository clone and that git binary is available.'); } $this->versionDate = new \DateTime(trim($process->getOutput())); $this->versionDate->setTimezone(new \DateTimeZone('UTC')); // TODO in v2.3 always call with an array if (method_exists('Symfony\Component\Process\Process', 'fromShellCommandline')) { $process = new Process(array('git', 'describe', '--tags', '--exact-match', 'HEAD'), __DIR__); } else { // @phpstan-ignore-next-line $process = new Process('git describe --tags --exact-match HEAD'); } if ($process->run() == 0) { $this->version = trim($process->getOutput()); } else { // get branch-alias defined in composer.json for dev-main (if any) $localConfig = __DIR__.'/../../composer.json'; $file = new JsonFile($localConfig); $localConfig = $file->read(); if (isset($localConfig['extra']['branch-alias']['dev-main'])) { $this->branchAliasVersion = $localConfig['extra']['branch-alias']['dev-main']; } } $phar = new \Phar($pharFile, 0, 'composer.phar'); $phar->setSignatureAlgorithm(\Phar::SHA512); $phar->startBuffering(); $finderSort = function ($a, $b) { return strcmp(strtr($a->getRealPath(), '\\', '/'), strtr($b->getRealPath(), '\\', '/')); }; // Add Composer sources $finder = new Finder(); $finder->files() ->ignoreVCS(true) ->name('*.php') ->notName('Compiler.php') ->notName('ClassLoader.php') ->notName('InstalledVersions.php') ->in(__DIR__.'/..') ->sort($finderSort) ; foreach ($finder as $file) { $this->addFile($phar, $file); } // Add runtime utilities separately to make sure they retains the docblocks as these will get copied into projects $this->addFile($phar, new \SplFileInfo(__DIR__ . '/Autoload/ClassLoader.php'), false); $this->addFile($phar, new \SplFileInfo(__DIR__ . '/InstalledVersions.php'), false); // Add Composer resources $finder = new Finder(); $finder->files() ->in(__DIR__.'/../../res') ->sort($finderSort) ; foreach ($finder as $file) { $this->addFile($phar, $file, false); } // Add vendor files $finder = new Finder(); $finder->files() ->ignoreVCS(true) ->notPath('/\/(composer\.(json|lock)|[A-Z]+\.md|\.gitignore|appveyor.yml|phpunit\.xml\.dist|phpstan\.neon\.dist|phpstan-config\.neon)$/') ->notPath('/bin\/(jsonlint|validate-json|simple-phpunit)(\.bat)?$/') ->notPath('symfony/debug/Resources/ext/') ->notPath('justinrainbow/json-schema/demo/') ->notPath('justinrainbow/json-schema/dist/') ->notPath('composer/installed.json') ->notPath('composer/LICENSE') ->exclude('Tests') ->exclude('tests') ->exclude('docs') ->in(__DIR__.'/../../vendor/') ->sort($finderSort) ; $extraFiles = array( realpath(__DIR__ . '/../../vendor/composer/spdx-licenses/res/spdx-exceptions.json'), realpath(__DIR__ . '/../../vendor/composer/spdx-licenses/res/spdx-licenses.json'), realpath(CaBundle::getBundledCaBundlePath()), realpath(__DIR__ . '/../../vendor/symfony/console/Resources/bin/hiddeninput.exe'), realpath(__DIR__ . '/../../vendor/symfony/polyfill-mbstring/Resources/mb_convert_variables.php8'), ); $unexpectedFiles = array(); foreach ($finder as $file) { if (in_array(realpath($file), $extraFiles, true)) { unset($extraFiles[array_search(realpath($file), $extraFiles, true)]); } elseif (!Preg::isMatch('{([/\\\\]LICENSE|\.php)$}', $file)) { $unexpectedFiles[] = (string) $file; } if (Preg::isMatch('{\.php[\d.]*$}', $file)) { $this->addFile($phar, $file); } else { $this->addFile($phar, $file, false); } } if ($extraFiles) { throw new \RuntimeException('These files were expected but not added to the phar, they might be excluded or gone from the source package:'.PHP_EOL.implode(PHP_EOL, $extraFiles)); } if ($unexpectedFiles) { throw new \RuntimeException('These files were unexpectedly added to the phar, make sure they are excluded or listed in $extraFiles:'.PHP_EOL.implode(PHP_EOL, $unexpectedFiles)); } // Add bin/composer $this->addComposerBin($phar); // Stubs $phar->setStub($this->getStub()); $phar->stopBuffering(); // disabled for interoperability with systems without gzip ext // $phar->compressFiles(\Phar::GZ); $this->addFile($phar, new \SplFileInfo(__DIR__.'/../../LICENSE'), false); unset($phar); // re-sign the phar with reproducible timestamp / signature $util = new Timestamps($pharFile); $util->updateTimestamps($this->versionDate); $util->save($pharFile, \Phar::SHA512); Linter::lint($pharFile); } /** * @param \SplFileInfo $file * @return string */ private function getRelativeFilePath($file) { $realPath = $file->getRealPath(); $pathPrefix = dirname(dirname(__DIR__)).DIRECTORY_SEPARATOR; $pos = strpos($realPath, $pathPrefix); $relativePath = ($pos !== false) ? substr_replace($realPath, '', $pos, strlen($pathPrefix)) : $realPath; return strtr($relativePath, '\\', '/'); } /** * @param bool $strip * * @return void */ private function addFile(\Phar $phar, \SplFileInfo $file, $strip = true) { $path = $this->getRelativeFilePath($file); $content = file_get_contents($file); if ($strip) { $content = $this->stripWhitespace($content); } elseif ('LICENSE' === basename($file)) { $content = "\n".$content."\n"; } if ($path === 'src/Composer/Composer.php') { $content = strtr( $content, array( '@package_version@' => $this->version, '@package_branch_alias_version@' => $this->branchAliasVersion, '@release_date@' => $this->versionDate->format('Y-m-d H:i:s'), ) ); $content = Preg::replace('{SOURCE_VERSION = \'[^\']+\';}', 'SOURCE_VERSION = \'\';', $content); } $phar->addFromString($path, $content); } /** * @return void */ private function addComposerBin(\Phar $phar) { $content = file_get_contents(__DIR__.'/../../bin/composer'); $content = Preg::replace('{^#!/usr/bin/env php\s*}', '', $content); $phar->addFromString('bin/composer', $content); } /** * Removes whitespace from a PHP source string while preserving line numbers. * * @param string $source A PHP string * @return string The PHP string with the whitespace removed */ private function stripWhitespace($source) { if (!function_exists('token_get_all')) { return $source; } $output = ''; foreach (token_get_all($source) as $token) { if (is_string($token)) { $output .= $token; } elseif (in_array($token[0], array(T_COMMENT, T_DOC_COMMENT))) { $output .= str_repeat("\n", substr_count($token[1], "\n")); } elseif (T_WHITESPACE === $token[0]) { // reduce wide spaces $whitespace = Preg::replace('{[ \t]+}', ' ', $token[1]); // normalize newlines to \n $whitespace = Preg::replace('{(?:\r\n|\r|\n)}', "\n", $whitespace); // trim leading spaces $whitespace = Preg::replace('{\n +}', "\n", $whitespace); $output .= $whitespace; } else { $output .= $token[1]; } } return $output; } /** * @return string */ private function getStub() { $stub = <<<'EOF' #!/usr/bin/env php * Jordi Boggiano * * For the full copyright and license information, please view * the license that is located at the bottom of this file. */ // Avoid APC causing random fatal errors per https://github.com/composer/composer/issues/264 if (extension_loaded('apc') && filter_var(ini_get('apc.enable_cli'), FILTER_VALIDATE_BOOLEAN) && filter_var(ini_get('apc.cache_by_default'), FILTER_VALIDATE_BOOLEAN)) { if (version_compare(phpversion('apc'), '3.0.12', '>=')) { ini_set('apc.cache_by_default', 0); } else { fwrite(STDERR, 'Warning: APC <= 3.0.12 may cause fatal errors when running composer commands.'.PHP_EOL); fwrite(STDERR, 'Update APC, or set apc.enable_cli or apc.cache_by_default to 0 in your php.ini.'.PHP_EOL); } } if (!class_exists('Phar')) { echo 'PHP\'s phar extension is missing. Composer requires it to run. Enable the extension or recompile php without --disable-phar then try again.' . PHP_EOL; exit(1); } Phar::mapPhar('composer.phar'); EOF; // add warning once the phar is older than 60 days if (Preg::isMatch('{^[a-f0-9]+$}', $this->version)) { $warningTime = ((int) $this->versionDate->format('U')) + 60 * 86400; $stub .= "define('COMPOSER_DEV_WARNING_TIME', $warningTime);\n"; } return $stub . <<<'EOF' require 'phar://composer.phar/bin/composer'; __HALT_COMPILER(); EOF; } } * Jordi Boggiano * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Composer\Exception; /** * Specific exception for Composer\Util\HttpDownloader creation. * * @author Jordi Boggiano */ class NoSslException extends \RuntimeException { } * Jordi Boggiano * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Composer\Exception; /** * @author Jordi Boggiano */ class IrrecoverableDownloadException extends \RuntimeException { } * Jordi Boggiano * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Composer; use Composer\Autoload\ClassLoader; use Composer\Semver\VersionParser; /** * This class is copied in every Composer installed project and available to all * * See also https://getcomposer.org/doc/07-runtime.md#installed-versions * * To require its presence, you can require `composer-runtime-api ^2.0` */ class InstalledVersions { /** * @var mixed[]|null * @psalm-var array{root: array{name: string, version: string, reference: string, pretty_version: string, aliases: string[], dev: bool, install_path: string, type: string}, versions: array}|array{}|null */ private static $installed; /** * @var bool|null */ private static $canGetVendors; /** * @var array[] * @psalm-var array}> */ private static $installedByVendor = array(); /** * Returns a list of all package names which are present, either by being installed, replaced or provided * * @return string[] * @psalm-return list */ public static function getInstalledPackages() { $packages = array(); foreach (self::getInstalled() as $installed) { $packages[] = array_keys($installed['versions']); } if (1 === \count($packages)) { return $packages[0]; } return array_keys(array_flip(\call_user_func_array('array_merge', $packages))); } /** * Returns a list of all package names with a specific type e.g. 'library' * * @param string $type * @return string[] * @psalm-return list */ public static function getInstalledPackagesByType($type) { $packagesByType = array(); foreach (self::getInstalled() as $installed) { foreach ($installed['versions'] as $name => $package) { if (isset($package['type']) && $package['type'] === $type) { $packagesByType[] = $name; } } } return $packagesByType; } /** * Checks whether the given package is installed * * This also returns true if the package name is provided or replaced by another package * * @param string $packageName * @param bool $includeDevRequirements * @return bool */ public static function isInstalled($packageName, $includeDevRequirements = true) { foreach (self::getInstalled() as $installed) { if (isset($installed['versions'][$packageName])) { return $includeDevRequirements || empty($installed['versions'][$packageName]['dev_requirement']); } } return false; } /** * Checks whether the given package satisfies a version constraint * * e.g. If you want to know whether version 2.3+ of package foo/bar is installed, you would call: * * Composer\InstalledVersions::satisfies(new VersionParser, 'foo/bar', '^2.3') * * @param VersionParser $parser Install composer/semver to have access to this class and functionality * @param string $packageName * @param string|null $constraint A version constraint to check for, if you pass one you have to make sure composer/semver is required by your package * @return bool */ public static function satisfies(VersionParser $parser, $packageName, $constraint) { $constraint = $parser->parseConstraints($constraint); $provided = $parser->parseConstraints(self::getVersionRanges($packageName)); return $provided->matches($constraint); } /** * Returns a version constraint representing all the range(s) which are installed for a given package * * It is easier to use this via isInstalled() with the $constraint argument if you need to check * whether a given version of a package is installed, and not just whether it exists * * @param string $packageName * @return string Version constraint usable with composer/semver */ public static function getVersionRanges($packageName) { foreach (self::getInstalled() as $installed) { if (!isset($installed['versions'][$packageName])) { continue; } $ranges = array(); if (isset($installed['versions'][$packageName]['pretty_version'])) { $ranges[] = $installed['versions'][$packageName]['pretty_version']; } if (array_key_exists('aliases', $installed['versions'][$packageName])) { $ranges = array_merge($ranges, $installed['versions'][$packageName]['aliases']); } if (array_key_exists('replaced', $installed['versions'][$packageName])) { $ranges = array_merge($ranges, $installed['versions'][$packageName]['replaced']); } if (array_key_exists('provided', $installed['versions'][$packageName])) { $ranges = array_merge($ranges, $installed['versions'][$packageName]['provided']); } return implode(' || ', $ranges); } throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed'); } /** * @param string $packageName * @return string|null If the package is being replaced or provided but is not really installed, null will be returned as version, use satisfies or getVersionRanges if you need to know if a given version is present */ public static function getVersion($packageName) { foreach (self::getInstalled() as $installed) { if (!isset($installed['versions'][$packageName])) { continue; } if (!isset($installed['versions'][$packageName]['version'])) { return null; } return $installed['versions'][$packageName]['version']; } throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed'); } /** * @param string $packageName * @return string|null If the package is being replaced or provided but is not really installed, null will be returned as version, use satisfies or getVersionRanges if you need to know if a given version is present */ public static function getPrettyVersion($packageName) { foreach (self::getInstalled() as $installed) { if (!isset($installed['versions'][$packageName])) { continue; } if (!isset($installed['versions'][$packageName]['pretty_version'])) { return null; } return $installed['versions'][$packageName]['pretty_version']; } throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed'); } /** * @param string $packageName * @return string|null If the package is being replaced or provided but is not really installed, null will be returned as reference */ public static function getReference($packageName) { foreach (self::getInstalled() as $installed) { if (!isset($installed['versions'][$packageName])) { continue; } if (!isset($installed['versions'][$packageName]['reference'])) { return null; } return $installed['versions'][$packageName]['reference']; } throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed'); } /** * @param string $packageName * @return string|null If the package is being replaced or provided but is not really installed, null will be returned as install path. Packages of type metapackages also have a null install path. */ public static function getInstallPath($packageName) { foreach (self::getInstalled() as $installed) { if (!isset($installed['versions'][$packageName])) { continue; } return isset($installed['versions'][$packageName]['install_path']) ? $installed['versions'][$packageName]['install_path'] : null; } throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed'); } /** * @return array * @psalm-return array{name: string, version: string, reference: string, pretty_version: string, aliases: string[], dev: bool, install_path: string, type: string} */ public static function getRootPackage() { $installed = self::getInstalled(); return $installed[0]['root']; } /** * Returns the raw installed.php data for custom implementations * * @deprecated Use getAllRawData() instead which returns all datasets for all autoloaders present in the process. getRawData only returns the first dataset loaded, which may not be what you expect. * @return array[] * @psalm-return array{root: array{name: string, version: string, reference: string, pretty_version: string, aliases: string[], dev: bool, install_path: string, type: string}, versions: array} */ public static function getRawData() { @trigger_error('getRawData only returns the first dataset loaded, which may not be what you expect. Use getAllRawData() instead which returns all datasets for all autoloaders present in the process.', E_USER_DEPRECATED); if (null === self::$installed) { // only require the installed.php file if this file is loaded from its dumped location, // and not from its source location in the composer/composer package, see https://github.com/composer/composer/issues/9937 if (substr(__DIR__, -8, 1) !== 'C') { self::$installed = include __DIR__ . '/installed.php'; } else { self::$installed = array(); } } return self::$installed; } /** * Returns the raw data of all installed.php which are currently loaded for custom implementations * * @return array[] * @psalm-return list}> */ public static function getAllRawData() { return self::getInstalled(); } /** * Lets you reload the static array from another file * * This is only useful for complex integrations in which a project needs to use * this class but then also needs to execute another project's autoloader in process, * and wants to ensure both projects have access to their version of installed.php. * * A typical case would be PHPUnit, where it would need to make sure it reads all * the data it needs from this class, then call reload() with * `require $CWD/vendor/composer/installed.php` (or similar) as input to make sure * the project in which it runs can then also use this class safely, without * interference between PHPUnit's dependencies and the project's dependencies. * * @param array[] $data A vendor/composer/installed.php data set * @return void * * @psalm-param array{root: array{name: string, version: string, reference: string, pretty_version: string, aliases: string[], dev: bool, install_path: string, type: string}, versions: array} $data */ public static function reload($data) { self::$installed = $data; self::$installedByVendor = array(); } /** * @return array[] * @psalm-return list}> */ private static function getInstalled() { if (null === self::$canGetVendors) { self::$canGetVendors = method_exists('Composer\Autoload\ClassLoader', 'getRegisteredLoaders'); } $installed = array(); if (self::$canGetVendors) { foreach (ClassLoader::getRegisteredLoaders() as $vendorDir => $loader) { if (isset(self::$installedByVendor[$vendorDir])) { $installed[] = self::$installedByVendor[$vendorDir]; } elseif (is_file($vendorDir.'/composer/installed.php')) { $installed[] = self::$installedByVendor[$vendorDir] = require $vendorDir.'/composer/installed.php'; if (null === self::$installed && strtr($vendorDir.'/composer', '\\', '/') === strtr(__DIR__, '\\', '/')) { self::$installed = $installed[count($installed) - 1]; } } } } if (null === self::$installed) { // only require the installed.php file if this file is loaded from its dumped location, // and not from its source location in the composer/composer package, see https://github.com/composer/composer/issues/9937 if (substr(__DIR__, -8, 1) !== 'C') { self::$installed = require __DIR__ . '/installed.php'; } else { self::$installed = array(); } } $installed[] = self::$installed; return $installed; } } * Jordi Boggiano * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Composer; use Composer\IO\IOInterface; use Composer\Pcre\Preg; use Composer\Util\Filesystem; use Composer\Util\Platform; use Composer\Util\Silencer; use Symfony\Component\Finder\Finder; /** * Reads/writes to a filesystem cache * * @author Jordi Boggiano */ class Cache { /** @var bool|null */ private static $cacheCollected = null; /** @var IOInterface */ private $io; /** @var string */ private $root; /** @var ?bool */ private $enabled = null; /** @var string */ private $allowlist; /** @var Filesystem */ private $filesystem; /** @var bool */ private $readOnly; /** * @param IOInterface $io * @param string $cacheDir location of the cache * @param string $allowlist List of characters that are allowed in path names (used in a regex character class) * @param Filesystem $filesystem optional filesystem instance * @param bool $readOnly whether the cache is in readOnly mode */ public function __construct(IOInterface $io, $cacheDir, $allowlist = 'a-z0-9.', Filesystem $filesystem = null, $readOnly = false) { $this->io = $io; $this->root = rtrim($cacheDir, '/\\') . '/'; $this->allowlist = $allowlist; $this->filesystem = $filesystem ?: new Filesystem(); $this->readOnly = (bool) $readOnly; if (!self::isUsable($cacheDir)) { $this->enabled = false; } } /** * @param bool $readOnly * * @return void */ public function setReadOnly($readOnly) { $this->readOnly = (bool) $readOnly; } /** * @return bool */ public function isReadOnly() { return $this->readOnly; } /** * @param string $path * * @return bool */ public static function isUsable($path) { return !Preg::isMatch('{(^|[\\\\/])(\$null|nul|NUL|/dev/null)([\\\\/]|$)}', $path); } /** * @return bool */ public function isEnabled() { if ($this->enabled === null) { $this->enabled = true; if ( (!is_dir($this->root) && !Silencer::call('mkdir', $this->root, 0777, true)) || !is_writable($this->root) ) { $this->io->writeError('Cannot create cache directory ' . $this->root . ', or directory is not writable. Proceeding without cache'); $this->enabled = false; } } return $this->enabled; } /** * @return string */ public function getRoot() { return $this->root; } /** * @param string $file * * @return string|false */ public function read($file) { if ($this->isEnabled()) { $file = Preg::replace('{[^'.$this->allowlist.']}i', '-', $file); if (file_exists($this->root . $file)) { $this->io->writeError('Reading '.$this->root . $file.' from cache', true, IOInterface::DEBUG); return file_get_contents($this->root . $file); } } return false; } /** * @param string $file * @param string $contents * * @return bool */ public function write($file, $contents) { if ($this->isEnabled() && !$this->readOnly) { $file = Preg::replace('{[^'.$this->allowlist.']}i', '-', $file); $this->io->writeError('Writing '.$this->root . $file.' into cache', true, IOInterface::DEBUG); $tempFileName = $this->root . $file . uniqid('.', true) . '.tmp'; try { return file_put_contents($tempFileName, $contents) !== false && rename($tempFileName, $this->root . $file); } catch (\ErrorException $e) { $this->io->writeError('Failed to write into cache: '.$e->getMessage().'', true, IOInterface::DEBUG); if (Preg::isMatch('{^file_put_contents\(\): Only ([0-9]+) of ([0-9]+) bytes written}', $e->getMessage(), $m)) { // Remove partial file. unlink($tempFileName); $message = sprintf( 'Writing %1$s into cache failed after %2$u of %3$u bytes written, only %4$u bytes of free space available', $tempFileName, $m[1], $m[2], @disk_free_space(dirname($tempFileName)) ); $this->io->writeError($message); return false; } throw $e; } } return false; } /** * Copy a file into the cache * * @param string $file * @param string $source * * @return bool */ public function copyFrom($file, $source) { if ($this->isEnabled() && !$this->readOnly) { $file = Preg::replace('{[^'.$this->allowlist.']}i', '-', $file); $this->filesystem->ensureDirectoryExists(dirname($this->root . $file)); if (!file_exists($source)) { $this->io->writeError(''.$source.' does not exist, can not write into cache'); } elseif ($this->io->isDebug()) { $this->io->writeError('Writing '.$this->root . $file.' into cache from '.$source); } return copy($source, $this->root . $file); } return false; } /** * Copy a file out of the cache * * @param string $file * @param string $target * * @return bool */ public function copyTo($file, $target) { if ($this->isEnabled()) { $file = Preg::replace('{[^'.$this->allowlist.']}i', '-', $file); if (file_exists($this->root . $file)) { try { touch($this->root . $file, filemtime($this->root . $file), time()); } catch (\ErrorException $e) { // fallback in case the above failed due to incorrect ownership // see https://github.com/composer/composer/issues/4070 Silencer::call('touch', $this->root . $file); } $this->io->writeError('Reading '.$this->root . $file.' from cache', true, IOInterface::DEBUG); return copy($this->root . $file, $target); } } return false; } /** * @return bool */ public function gcIsNecessary() { if (self::$cacheCollected) { return false; } self::$cacheCollected = true; if (Platform::getEnv('COMPOSER_TEST_SUITE')) { return false; } if (PHP_VERSION_ID > 70000) { return !random_int(0, 50); } return !mt_rand(0, 50); } /** * @param string $file * * @return bool */ public function remove($file) { if ($this->isEnabled()) { $file = Preg::replace('{[^'.$this->allowlist.']}i', '-', $file); if (file_exists($this->root . $file)) { return $this->filesystem->unlink($this->root . $file); } } return false; } /** * @return bool */ public function clear() { if ($this->isEnabled()) { $this->filesystem->emptyDirectory($this->root); return true; } return false; } /** * @param string $file * @return int|false * @phpstan-return int<0, max>|false */ public function getAge($file) { if ($this->isEnabled()) { $file = Preg::replace('{[^'.$this->allowlist.']}i', '-', $file); if (file_exists($this->root . $file) && ($mtime = filemtime($this->root . $file)) !== false) { return abs(time() - $mtime); } } return false; } /** * @param int $ttl * @param int $maxSize * * @return bool */ public function gc($ttl, $maxSize) { if ($this->isEnabled()) { $expire = new \DateTime(); $expire->modify('-'.$ttl.' seconds'); $finder = $this->getFinder()->date('until '.$expire->format('Y-m-d H:i:s')); foreach ($finder as $file) { $this->filesystem->unlink($file->getPathname()); } $totalSize = $this->filesystem->size($this->root); if ($totalSize > $maxSize) { $iterator = $this->getFinder()->sortByAccessedTime()->getIterator(); while ($totalSize > $maxSize && $iterator->valid()) { $filepath = $iterator->current()->getPathname(); $totalSize -= $this->filesystem->size($filepath); $this->filesystem->unlink($filepath); $iterator->next(); } } self::$cacheCollected = true; return true; } return false; } /** * @param string $file * * @return string|false */ public function sha1($file) { if ($this->isEnabled()) { $file = Preg::replace('{[^'.$this->allowlist.']}i', '-', $file); if (file_exists($this->root . $file)) { return sha1_file($this->root . $file); } } return false; } /** * @param string $file * * @return string|false */ public function sha256($file) { if ($this->isEnabled()) { $file = Preg::replace('{[^'.$this->allowlist.']}i', '-', $file); if (file_exists($this->root . $file)) { return hash_file('sha256', $this->root . $file); } } return false; } /** * @return Finder */ protected function getFinder() { return Finder::create()->in($this->root)->files(); } } * Jordi Boggiano * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Composer\Repository; use Composer\Composer; use Composer\Package\CompletePackage; use Composer\Package\CompletePackageInterface; use Composer\Package\Link; use Composer\Package\PackageInterface; use Composer\Package\Version\VersionParser; use Composer\Pcre\Preg; use Composer\Platform\HhvmDetector; use Composer\Platform\Runtime; use Composer\Platform\Version; use Composer\Plugin\PluginInterface; use Composer\Semver\Constraint\Constraint; use Composer\Util\Silencer; use Composer\XdebugHandler\XdebugHandler; /** * @author Jordi Boggiano */ class PlatformRepository extends ArrayRepository { const PLATFORM_PACKAGE_REGEX = '{^(?:php(?:-64bit|-ipv6|-zts|-debug)?|hhvm|(?:ext|lib)-[a-z0-9](?:[_.-]?[a-z0-9]+)*|composer(?:-(?:plugin|runtime)-api)?)$}iD'; /** * @var ?string */ private static $lastSeenPlatformPhp = null; /** * @var VersionParser */ private $versionParser; /** * Defines overrides so that the platform can be mocked * * Keyed by package name (lowercased) * * @var array */ private $overrides = array(); /** * Stores which packages have been disabled and their actual version * * @var array */ private $disabledPackages = array(); /** @var Runtime */ private $runtime; /** @var HhvmDetector */ private $hhvmDetector; /** * @param array $overrides */ public function __construct(array $packages = array(), array $overrides = array(), Runtime $runtime = null, HhvmDetector $hhvmDetector = null) { $this->runtime = $runtime ?: new Runtime(); $this->hhvmDetector = $hhvmDetector ?: new HhvmDetector(); foreach ($overrides as $name => $version) { if (!is_string($version) && false !== $version) { // @phpstan-ignore-line throw new \UnexpectedValueException('config.platform.'.$name.' should be a string or false, but got '.gettype($version).' '.var_export($version, true)); } if ($name === 'php' && $version === false) { throw new \UnexpectedValueException('config.platform.'.$name.' cannot be set to false as you cannot disable php entirely.'); } $this->overrides[strtolower($name)] = array('name' => $name, 'version' => $version); } parent::__construct($packages); } public function getRepoName() { return 'platform repo'; } /** * @param string $name * @return boolean */ public function isPlatformPackageDisabled($name) { return isset($this->disabledPackages[$name]); } /** * @return array */ public function getDisabledPackages() { return $this->disabledPackages; } protected function initialize() { parent::initialize(); $this->versionParser = new VersionParser(); // Add each of the override versions as options. // Later we might even replace the extensions instead. foreach ($this->overrides as $override) { // Check that it's a platform package. if (!self::isPlatformPackage($override['name'])) { throw new \InvalidArgumentException('Invalid platform package name in config.platform: '.$override['name']); } if ($override['version'] !== false) { $this->addOverriddenPackage($override); } } $prettyVersion = Composer::getVersion(); $version = $this->versionParser->normalize($prettyVersion); $composer = new CompletePackage('composer', $version, $prettyVersion); $composer->setDescription('Composer package'); $this->addPackage($composer); $prettyVersion = PluginInterface::PLUGIN_API_VERSION; $version = $this->versionParser->normalize($prettyVersion); $composerPluginApi = new CompletePackage('composer-plugin-api', $version, $prettyVersion); $composerPluginApi->setDescription('The Composer Plugin API'); $this->addPackage($composerPluginApi); $prettyVersion = Composer::RUNTIME_API_VERSION; $version = $this->versionParser->normalize($prettyVersion); $composerRuntimeApi = new CompletePackage('composer-runtime-api', $version, $prettyVersion); $composerRuntimeApi->setDescription('The Composer Runtime API'); $this->addPackage($composerRuntimeApi); try { $prettyVersion = $this->runtime->getConstant('PHP_VERSION'); $version = $this->versionParser->normalize($prettyVersion); } catch (\UnexpectedValueException $e) { $prettyVersion = Preg::replace('#^([^~+-]+).*$#', '$1', $this->runtime->getConstant('PHP_VERSION')); $version = $this->versionParser->normalize($prettyVersion); } $php = new CompletePackage('php', $version, $prettyVersion); $php->setDescription('The PHP interpreter'); $this->addPackage($php); if ($this->runtime->getConstant('PHP_DEBUG')) { $phpdebug = new CompletePackage('php-debug', $version, $prettyVersion); $phpdebug->setDescription('The PHP interpreter, with debugging symbols'); $this->addPackage($phpdebug); } if ($this->runtime->hasConstant('PHP_ZTS') && $this->runtime->getConstant('PHP_ZTS')) { $phpzts = new CompletePackage('php-zts', $version, $prettyVersion); $phpzts->setDescription('The PHP interpreter, with Zend Thread Safety'); $this->addPackage($phpzts); } if ($this->runtime->getConstant('PHP_INT_SIZE') === 8) { $php64 = new CompletePackage('php-64bit', $version, $prettyVersion); $php64->setDescription('The PHP interpreter, 64bit'); $this->addPackage($php64); } // The AF_INET6 constant is only defined if ext-sockets is available but // IPv6 support might still be available. if ($this->runtime->hasConstant('AF_INET6') || Silencer::call(array($this->runtime, 'invoke'), 'inet_pton', array('::')) !== false) { $phpIpv6 = new CompletePackage('php-ipv6', $version, $prettyVersion); $phpIpv6->setDescription('The PHP interpreter, with IPv6 support'); $this->addPackage($phpIpv6); } $loadedExtensions = $this->runtime->getExtensions(); // Extensions scanning foreach ($loadedExtensions as $name) { if (in_array($name, array('standard', 'Core'))) { continue; } $this->addExtension($name, $this->runtime->getExtensionVersion($name)); } // Check for Xdebug in a restarted process if (!in_array('xdebug', $loadedExtensions, true) && ($prettyVersion = XdebugHandler::getSkippedVersion())) { $this->addExtension('xdebug', $prettyVersion); } // Another quick loop, just for possible libraries // Doing it this way to know that functions or constants exist before // relying on them. foreach ($loadedExtensions as $name) { switch ($name) { case 'amqp': $info = $this->runtime->getExtensionInfo($name); // librabbitmq version => 0.9.0 if (Preg::isMatch('/^librabbitmq version => (?.+)$/im', $info, $librabbitmqMatches)) { $this->addLibrary($name.'-librabbitmq', $librabbitmqMatches['version'], 'AMQP librabbitmq version'); } // AMQP protocol version => 0-9-1 if (Preg::isMatch('/^AMQP protocol version => (?.+)$/im', $info, $protocolMatches)) { $this->addLibrary($name.'-protocol', str_replace('-', '.', $protocolMatches['version']), 'AMQP protocol version'); } break; case 'bz2': $info = $this->runtime->getExtensionInfo($name); // BZip2 Version => 1.0.6, 6-Sept-2010 if (Preg::isMatch('/^BZip2 Version => (?.*),/im', $info, $matches)) { $this->addLibrary($name, $matches['version']); } break; case 'curl': $curlVersion = $this->runtime->invoke('curl_version'); $this->addLibrary($name, $curlVersion['version']); $info = $this->runtime->getExtensionInfo($name); // SSL Version => OpenSSL/1.0.1t if (Preg::isMatch('{^SSL Version => (?[^/]+)/(?.+)$}im', $info, $sslMatches)) { $library = strtolower($sslMatches['library']); if ($library === 'openssl') { $parsedVersion = Version::parseOpenssl($sslMatches['version'], $isFips); $this->addLibrary($name.'-openssl'.($isFips ? '-fips' : ''), $parsedVersion, 'curl OpenSSL version ('.$parsedVersion.')', array(), $isFips ? array('curl-openssl') : array()); } else { $this->addLibrary($name.'-'.$library, $sslMatches['version'], 'curl '.$library.' version ('.$sslMatches['version'].')', array('curl-openssl')); } } // libSSH Version => libssh2/1.4.3 if (Preg::isMatch('{^libSSH Version => (?[^/]+)/(?.+?)(?:/.*)?$}im', $info, $sshMatches)) { $this->addLibrary($name.'-'.strtolower($sshMatches['library']), $sshMatches['version'], 'curl '.$sshMatches['library'].' version'); } // ZLib Version => 1.2.8 if (Preg::isMatch('{^ZLib Version => (?.+)$}im', $info, $zlibMatches)) { $this->addLibrary($name.'-zlib', $zlibMatches['version'], 'curl zlib version'); } break; case 'date': $info = $this->runtime->getExtensionInfo($name); // timelib version => 2018.03 if (Preg::isMatch('/^timelib version => (?.+)$/im', $info, $timelibMatches)) { $this->addLibrary($name.'-timelib', $timelibMatches['version'], 'date timelib version'); } // Timezone Database => internal if (Preg::isMatch('/^Timezone Database => (?internal|external)$/im', $info, $zoneinfoSourceMatches)) { $external = $zoneinfoSourceMatches['source'] === 'external'; if (Preg::isMatch('/^"Olson" Timezone Database Version => (?.+?)(\.system)?$/im', $info, $zoneinfoMatches)) { // If the timezonedb is provided by ext/timezonedb, register that version as a replacement if ($external && in_array('timezonedb', $loadedExtensions, true)) { $this->addLibrary('timezonedb-zoneinfo', $zoneinfoMatches['version'], 'zoneinfo ("Olson") database for date (replaced by timezonedb)', array($name.'-zoneinfo')); } else { $this->addLibrary($name.'-zoneinfo', $zoneinfoMatches['version'], 'zoneinfo ("Olson") database for date'); } } } break; case 'fileinfo': $info = $this->runtime->getExtensionInfo($name); // libmagic => 537 if (Preg::isMatch('/^libmagic => (?.+)$/im', $info, $magicMatches)) { $this->addLibrary($name.'-libmagic', $magicMatches['version'], 'fileinfo libmagic version'); } break; case 'gd': $this->addLibrary($name, $this->runtime->getConstant('GD_VERSION')); $info = $this->runtime->getExtensionInfo($name); if (Preg::isMatch('/^libJPEG Version => (?.+?)(?: compatible)?$/im', $info, $libjpegMatches)) { $this->addLibrary($name.'-libjpeg', Version::parseLibjpeg($libjpegMatches['version']), 'libjpeg version for gd'); } if (Preg::isMatch('/^libPNG Version => (?.+)$/im', $info, $libpngMatches)) { $this->addLibrary($name.'-libpng', $libpngMatches['version'], 'libpng version for gd'); } if (Preg::isMatch('/^FreeType Version => (?.+)$/im', $info, $freetypeMatches)) { $this->addLibrary($name.'-freetype', $freetypeMatches['version'], 'freetype version for gd'); } if (Preg::isMatch('/^libXpm Version => (?\d+)$/im', $info, $libxpmMatches)) { $this->addLibrary($name.'-libxpm', Version::convertLibxpmVersionId($libxpmMatches['versionId']), 'libxpm version for gd'); } break; case 'gmp': $this->addLibrary($name, $this->runtime->getConstant('GMP_VERSION')); break; case 'iconv': $this->addLibrary($name, $this->runtime->getConstant('ICONV_VERSION')); break; case 'intl': $info = $this->runtime->getExtensionInfo($name); $description = 'The ICU unicode and globalization support library'; // Truthy check is for testing only so we can make the condition fail if ($this->runtime->hasConstant('INTL_ICU_VERSION')) { $this->addLibrary('icu', $this->runtime->getConstant('INTL_ICU_VERSION'), $description); } elseif (Preg::isMatch('/^ICU version => (?.+)$/im', $info, $matches)) { $this->addLibrary('icu', $matches['version'], $description); } // ICU TZData version => 2019c if (Preg::isMatch('/^ICU TZData version => (?.*)$/im', $info, $zoneinfoMatches)) { $this->addLibrary('icu-zoneinfo', Version::parseZoneinfoVersion($zoneinfoMatches['version']), 'zoneinfo ("Olson") database for icu'); } // Add a separate version for the CLDR library version if ($this->runtime->hasClass('ResourceBundle')) { $cldrVersion = $this->runtime->invoke(array('ResourceBundle', 'create'), array('root', 'ICUDATA', false))->get('Version'); $this->addLibrary('icu-cldr', $cldrVersion, 'ICU CLDR project version'); } if ($this->runtime->hasClass('IntlChar')) { $this->addLibrary('icu-unicode', implode('.', array_slice($this->runtime->invoke(array('IntlChar', 'getUnicodeVersion')), 0, 3)), 'ICU unicode version'); } break; case 'imagick': $imageMagickVersion = $this->runtime->construct('Imagick')->getVersion(); // 6.x: ImageMagick 6.2.9 08/24/06 Q16 http://www.imagemagick.org // 7.x: ImageMagick 7.0.8-34 Q16 x86_64 2019-03-23 https://imagemagick.org Preg::match('/^ImageMagick (?[\d.]+)(?:-(?\d+))?/', $imageMagickVersion['versionString'], $matches); $version = $matches['version']; if (isset($matches['patch'])) { $version .= '.'.$matches['patch']; } $this->addLibrary($name.'-imagemagick', $version, null, array('imagick')); break; case 'ldap': $info = $this->runtime->getExtensionInfo($name); if (Preg::isMatch('/^Vendor Version => (?\d+)$/im', $info, $matches) && Preg::isMatch('/^Vendor Name => (?.+)$/im', $info, $vendorMatches)) { $this->addLibrary($name.'-'.strtolower($vendorMatches['vendor']), Version::convertOpenldapVersionId($matches['versionId']), $vendorMatches['vendor'].' version of ldap'); } break; case 'libxml': // ext/dom, ext/simplexml, ext/xmlreader and ext/xmlwriter use the same libxml as the ext/libxml $libxmlProvides = array_map(function ($extension) { return $extension . '-libxml'; }, array_intersect($loadedExtensions, array('dom', 'simplexml', 'xml', 'xmlreader', 'xmlwriter'))); $this->addLibrary($name, $this->runtime->getConstant('LIBXML_DOTTED_VERSION'), 'libxml library version', array(), $libxmlProvides); break; case 'mbstring': $info = $this->runtime->getExtensionInfo($name); // libmbfl version => 1.3.2 if (Preg::isMatch('/^libmbfl version => (?.+)$/im', $info, $libmbflMatches)) { $this->addLibrary($name.'-libmbfl', $libmbflMatches['version'], 'mbstring libmbfl version'); } if ($this->runtime->hasConstant('MB_ONIGURUMA_VERSION')) { $this->addLibrary($name.'-oniguruma', $this->runtime->getConstant('MB_ONIGURUMA_VERSION'), 'mbstring oniguruma version'); // Multibyte regex (oniguruma) version => 5.9.5 // oniguruma version => 6.9.0 } elseif (Preg::isMatch('/^(?:oniguruma|Multibyte regex \(oniguruma\)) version => (?.+)$/im', $info, $onigurumaMatches)) { $this->addLibrary($name.'-oniguruma', $onigurumaMatches['version'], 'mbstring oniguruma version'); } break; case 'memcached': $info = $this->runtime->getExtensionInfo($name); // libmemcached version => 1.0.18 if (Preg::isMatch('/^libmemcached version => (?.+)$/im', $info, $matches)) { $this->addLibrary($name.'-libmemcached', $matches['version'], 'libmemcached version'); } break; case 'openssl': // OpenSSL 1.1.1g 21 Apr 2020 if (Preg::isMatch('{^(?:OpenSSL|LibreSSL)?\s*(?\S+)}i', $this->runtime->getConstant('OPENSSL_VERSION_TEXT'), $matches)) { $parsedVersion = Version::parseOpenssl($matches['version'], $isFips); $this->addLibrary($name.($isFips ? '-fips' : ''), $parsedVersion, $this->runtime->getConstant('OPENSSL_VERSION_TEXT'), array(), $isFips ? array($name) : array()); } break; case 'pcre': $this->addLibrary($name, Preg::replace('{^(\S+).*}', '$1', $this->runtime->getConstant('PCRE_VERSION'))); $info = $this->runtime->getExtensionInfo($name); // PCRE Unicode Version => 12.1.0 if (Preg::isMatch('/^PCRE Unicode Version => (?.+)$/im', $info, $pcreUnicodeMatches)) { $this->addLibrary($name.'-unicode', $pcreUnicodeMatches['version'], 'PCRE Unicode version support'); } break; case 'mysqlnd': case 'pdo_mysql': $info = $this->runtime->getExtensionInfo($name); if (Preg::isMatch('/^(?:Client API version|Version) => mysqlnd (?.+?) /mi', $info, $matches)) { $this->addLibrary($name.'-mysqlnd', $matches['version'], 'mysqlnd library version for '.$name); } break; case 'mongodb': $info = $this->runtime->getExtensionInfo($name); if (Preg::isMatch('/^libmongoc bundled version => (?.+)$/im', $info, $libmongocMatches)) { $this->addLibrary($name.'-libmongoc', $libmongocMatches['version'], 'libmongoc version of mongodb'); } if (Preg::isMatch('/^libbson bundled version => (?.+)$/im', $info, $libbsonMatches)) { $this->addLibrary($name.'-libbson', $libbsonMatches['version'], 'libbson version of mongodb'); } break; case 'pgsql': case 'pdo_pgsql': $info = $this->runtime->getExtensionInfo($name); if (Preg::isMatch('/^PostgreSQL\(libpq\) Version => (?.*)$/im', $info, $matches)) { $this->addLibrary($name.'-libpq', $matches['version'], 'libpq for '.$name); } break; case 'libsodium': case 'sodium': if ($this->runtime->hasConstant('SODIUM_LIBRARY_VERSION')) { $this->addLibrary('libsodium', $this->runtime->getConstant('SODIUM_LIBRARY_VERSION')); } break; case 'sqlite3': case 'pdo_sqlite': $info = $this->runtime->getExtensionInfo($name); if (Preg::isMatch('/^SQLite Library => (?.+)$/im', $info, $matches)) { $this->addLibrary($name.'-sqlite', $matches['version']); } break; case 'ssh2': $info = $this->runtime->getExtensionInfo($name); if (Preg::isMatch('/^libssh2 version => (?.+)$/im', $info, $matches)) { $this->addLibrary($name.'-libssh2', $matches['version']); } break; case 'xsl': $this->addLibrary('libxslt', $this->runtime->getConstant('LIBXSLT_DOTTED_VERSION'), null, array('xsl')); $info = $this->runtime->getExtensionInfo('xsl'); if (Preg::isMatch('/^libxslt compiled against libxml Version => (?.+)$/im', $info, $matches)) { $this->addLibrary('libxslt-libxml', $matches['version'], 'libxml version libxslt is compiled against'); } break; case 'yaml': $info = $this->runtime->getExtensionInfo('yaml'); if (Preg::isMatch('/^LibYAML Version => (?.+)$/im', $info, $matches)) { $this->addLibrary($name.'-libyaml', $matches['version'], 'libyaml version of yaml'); } break; case 'zip': if ($this->runtime->hasConstant('LIBZIP_VERSION', 'ZipArchive')) { $this->addLibrary($name.'-libzip', $this->runtime->getConstant('LIBZIP_VERSION', 'ZipArchive'), null, array('zip')); } break; case 'zlib': if ($this->runtime->hasConstant('ZLIB_VERSION')) { $this->addLibrary($name, $this->runtime->getConstant('ZLIB_VERSION')); // Linked Version => 1.2.8 } elseif (Preg::isMatch('/^Linked Version => (?.+)$/im', $this->runtime->getExtensionInfo($name), $matches)) { $this->addLibrary($name, $matches['version']); } break; default: break; } } $hhvmVersion = $this->hhvmDetector->getVersion(); if ($hhvmVersion) { try { $prettyVersion = $hhvmVersion; $version = $this->versionParser->normalize($prettyVersion); } catch (\UnexpectedValueException $e) { $prettyVersion = Preg::replace('#^([^~+-]+).*$#', '$1', $hhvmVersion); $version = $this->versionParser->normalize($prettyVersion); } $hhvm = new CompletePackage('hhvm', $version, $prettyVersion); $hhvm->setDescription('The HHVM Runtime (64bit)'); $this->addPackage($hhvm); } } /** * @inheritDoc */ public function addPackage(PackageInterface $package) { if (!$package instanceof CompletePackage) { throw new \UnexpectedValueException('Expected CompletePackage but got '.get_class($package)); } // Skip if overridden if (isset($this->overrides[$package->getName()])) { if ($this->overrides[$package->getName()]['version'] === false) { $this->addDisabledPackage($package); return; } $overrider = $this->findPackage($package->getName(), '*'); if ($package->getVersion() === $overrider->getVersion()) { $actualText = 'same as actual'; } else { $actualText = 'actual: '.$package->getPrettyVersion(); } if ($overrider instanceof CompletePackageInterface) { $overrider->setDescription($overrider->getDescription().', '.$actualText); } return; } // Skip if PHP is overridden and we are adding a php-* package if (isset($this->overrides['php']) && 0 === strpos($package->getName(), 'php-')) { if (isset($this->overrides[$package->getName()]) && $this->overrides[$package->getName()]['version'] === false) { $this->addDisabledPackage($package); return; } $overrider = $this->addOverriddenPackage($this->overrides['php'], $package->getPrettyName()); if ($package->getVersion() === $overrider->getVersion()) { $actualText = 'same as actual'; } else { $actualText = 'actual: '.$package->getPrettyVersion(); } $overrider->setDescription($overrider->getDescription().', '.$actualText); return; } parent::addPackage($package); } /** * @param array{version: string, name: string} $override * @param string|null $name * * @return CompletePackage */ private function addOverriddenPackage(array $override, $name = null) { $version = $this->versionParser->normalize($override['version']); $package = new CompletePackage($name ?: $override['name'], $version, $override['version']); $package->setDescription('Package overridden via config.platform'); $package->setExtra(array('config.platform' => true)); parent::addPackage($package); if ($package->getName() === 'php') { self::$lastSeenPlatformPhp = implode('.', array_slice(explode('.', $package->getVersion()), 0, 3)); } return $package; } /** * @return void */ private function addDisabledPackage(CompletePackage $package) { $package->setDescription($package->getDescription().'. Package disabled via config.platform'); $package->setExtra(array('config.platform' => true)); $this->disabledPackages[$package->getName()] = $package; } /** * Parses the version and adds a new package to the repository * * @param string $name * @param null|string $prettyVersion * * @return void */ private function addExtension($name, $prettyVersion) { $extraDescription = null; try { $version = $this->versionParser->normalize($prettyVersion); } catch (\UnexpectedValueException $e) { $extraDescription = ' (actual version: '.$prettyVersion.')'; if (Preg::isMatch('{^(\d+\.\d+\.\d+(?:\.\d+)?)}', $prettyVersion, $match)) { $prettyVersion = $match[1]; } else { $prettyVersion = '0'; } $version = $this->versionParser->normalize($prettyVersion); } $packageName = $this->buildPackageName($name); $ext = new CompletePackage($packageName, $version, $prettyVersion); $ext->setDescription('The '.$name.' PHP extension'.$extraDescription); if ($name === 'uuid') { $ext->setReplaces(array( 'lib-uuid' => new Link('ext-uuid', 'lib-uuid', new Constraint('=', $version), Link::TYPE_REPLACE, $ext->getPrettyVersion()), )); } $this->addPackage($ext); } /** * @param string $name * @return string */ private function buildPackageName($name) { return 'ext-' . str_replace(' ', '-', strtolower($name)); } /** * @param string $name * @param string $prettyVersion * @param string|null $description * @param string[] $replaces * @param string[] $provides * * @return void */ private function addLibrary($name, $prettyVersion, $description = null, array $replaces = array(), array $provides = array()) { try { $version = $this->versionParser->normalize($prettyVersion); } catch (\UnexpectedValueException $e) { return; } if ($description === null) { $description = 'The '.$name.' library'; } $lib = new CompletePackage('lib-'.$name, $version, $prettyVersion); $lib->setDescription($description); $replaceLinks = array(); foreach ($replaces as $replace) { $replace = strtolower($replace); $replaceLinks[$replace] = new Link('lib-'.$name, 'lib-'.$replace, new Constraint('=', $version), Link::TYPE_REPLACE, $lib->getPrettyVersion()); } $provideLinks = array(); foreach ($provides as $provide) { $provide = strtolower($provide); $provideLinks[$provide] = new Link('lib-'.$name, 'lib-'.$provide, new Constraint('=', $version), Link::TYPE_PROVIDE, $lib->getPrettyVersion()); } $lib->setReplaces($replaceLinks); $lib->setProvides($provideLinks); $this->addPackage($lib); } /** * Check if a package name is a platform package. * * @param string $name * @return bool */ public static function isPlatformPackage($name) { static $cache = array(); if (isset($cache[$name])) { return $cache[$name]; } return $cache[$name] = Preg::isMatch(PlatformRepository::PLATFORM_PACKAGE_REGEX, $name); } /** * Returns the last seen config.platform.php version if defined * * This is a best effort attempt for internal purposes, retrieve the real * packages from a PlatformRepository instance if you need a version guaranteed to * be correct. * * @internal * @return string|null */ public static function getPlatformPhpVersion() { return self::$lastSeenPlatformPhp; } public function search($query, $mode = 0, $type = null) { // suppress vendor search as there are no vendors to match in platform packages if ($mode === self::SEARCH_VENDOR) { return array(); } return parent::search($query, $mode, $type); } } * Jordi Boggiano * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Composer\Repository; use Composer\Package\BasePackage; use Composer\Package\Loader\ArrayLoader; use Composer\Package\PackageInterface; use Composer\Package\AliasPackage; use Composer\Package\CompletePackage; use Composer\Package\CompleteAliasPackage; use Composer\Package\Version\VersionParser; use Composer\Package\Version\StabilityFilter; use Composer\Json\JsonFile; use Composer\Cache; use Composer\Config; use Composer\IO\IOInterface; use Composer\Pcre\Preg; use Composer\Plugin\PostFileDownloadEvent; use Composer\Semver\CompilingMatcher; use Composer\Util\HttpDownloader; use Composer\Util\Loop; use Composer\Plugin\PluginEvents; use Composer\Plugin\PreFileDownloadEvent; use Composer\EventDispatcher\EventDispatcher; use Composer\Downloader\TransportException; use Composer\Semver\Constraint\ConstraintInterface; use Composer\Semver\Constraint\Constraint; use Composer\Semver\Constraint\MatchAllConstraint; use Composer\Util\Http\Response; use Composer\MetadataMinifier\MetadataMinifier; use Composer\Util\Url; /** * @author Jordi Boggiano */ class ComposerRepository extends ArrayRepository implements ConfigurableRepositoryInterface { /** * @var mixed[] * @phpstan-var array{url: string, options?: mixed[], type?: 'composer', allow_ssl_downgrade?: bool} */ private $repoConfig; /** @var mixed[] */ private $options; /** @var string */ private $url; /** @var string */ private $baseUrl; /** @var IOInterface */ private $io; /** @var HttpDownloader */ private $httpDownloader; /** @var Loop */ private $loop; /** @var Cache */ protected $cache; /** @var ?string */ protected $notifyUrl = null; /** @var ?string */ protected $searchUrl = null; /** @var ?string a URL containing %package% which can be queried to get providers of a given name */ protected $providersApiUrl = null; /** @var bool */ protected $hasProviders = false; /** @var ?string */ protected $providersUrl = null; /** @var ?string */ protected $listUrl = null; /** @var bool Indicates whether a comprehensive list of packages this repository might provide is expressed in the repository root. **/ protected $hasAvailablePackageList = false; /** @var ?array */ protected $availablePackages = null; /** @var ?array */ protected $availablePackagePatterns = null; /** @var ?string */ protected $lazyProvidersUrl = null; /** @var ?array */ protected $providerListing; /** @var ArrayLoader */ protected $loader; /** @var bool */ private $allowSslDowngrade = false; /** @var ?EventDispatcher */ private $eventDispatcher; /** @var ?array> */ private $sourceMirrors; /** @var ?array */ private $distMirrors; /** @var bool */ private $degradedMode = false; /** @var mixed[]|true */ private $rootData; /** @var bool */ private $hasPartialPackages = false; /** @var ?array */ private $partialPackagesByName = null; /** * TODO v3 should make this private once we can drop PHP 5.3 support * @private * @var array list of package names which are fresh and can be loaded from the cache directly in case loadPackage is called several times * useful for v2 metadata repositories with lazy providers * @phpstan-var array */ public $freshMetadataUrls = array(); /** * TODO v3 should make this private once we can drop PHP 5.3 support * @private * @var array list of package names which returned a 404 and should not be re-fetched in case loadPackage is called several times * useful for v2 metadata repositories with lazy providers * @phpstan-var array */ public $packagesNotFoundCache = array(); /** * TODO v3 should make this private once we can drop PHP 5.3 support * @private * @var VersionParser */ public $versionParser; /** * @param array $repoConfig * @phpstan-param array{url: string, options?: mixed[], type?: 'composer', allow_ssl_downgrade?: bool} $repoConfig */ public function __construct(array $repoConfig, IOInterface $io, Config $config, HttpDownloader $httpDownloader, EventDispatcher $eventDispatcher = null) { parent::__construct(); if (!Preg::isMatch('{^[\w.]+\??://}', $repoConfig['url'])) { // assume http as the default protocol $repoConfig['url'] = 'http://'.$repoConfig['url']; } $repoConfig['url'] = rtrim($repoConfig['url'], '/'); if (strpos($repoConfig['url'], 'https?') === 0) { $repoConfig['url'] = (extension_loaded('openssl') ? 'https' : 'http') . substr($repoConfig['url'], 6); } $urlBits = parse_url($repoConfig['url']); if ($urlBits === false || empty($urlBits['scheme'])) { throw new \UnexpectedValueException('Invalid url given for Composer repository: '.$repoConfig['url']); } if (!isset($repoConfig['options'])) { $repoConfig['options'] = array(); } if (isset($repoConfig['allow_ssl_downgrade']) && true === $repoConfig['allow_ssl_downgrade']) { $this->allowSslDowngrade = true; } $this->options = $repoConfig['options']; $this->url = $repoConfig['url']; // force url for packagist.org to repo.packagist.org if (Preg::isMatch('{^(?Phttps?)://packagist\.org/?$}i', $this->url, $match)) { $this->url = $match['proto'].'://repo.packagist.org'; } $this->baseUrl = rtrim(Preg::replace('{(?:/[^/\\\\]+\.json)?(?:[?#].*)?$}', '', $this->url), '/'); $this->io = $io; $this->cache = new Cache($io, $config->get('cache-repo-dir').'/'.Preg::replace('{[^a-z0-9.]}i', '-', Url::sanitize($this->url)), 'a-z0-9.$~'); $this->cache->setReadOnly($config->get('cache-read-only')); $this->versionParser = new VersionParser(); $this->loader = new ArrayLoader($this->versionParser); $this->httpDownloader = $httpDownloader; $this->eventDispatcher = $eventDispatcher; $this->repoConfig = $repoConfig; $this->loop = new Loop($this->httpDownloader); } public function getRepoName() { return 'composer repo ('.Url::sanitize($this->url).')'; } public function getRepoConfig() { return $this->repoConfig; } /** * @inheritDoc */ public function findPackage($name, $constraint) { // this call initializes loadRootServerFile which is needed for the rest below to work $hasProviders = $this->hasProviders(); $name = strtolower($name); if (!$constraint instanceof ConstraintInterface) { $constraint = $this->versionParser->parseConstraints($constraint); } if ($this->lazyProvidersUrl) { if ($this->hasPartialPackages() && isset($this->partialPackagesByName[$name])) { return $this->filterPackages($this->whatProvides($name), $constraint, true); } if ($this->hasAvailablePackageList && !$this->lazyProvidersRepoContains($name)) { return null; } $packages = $this->loadAsyncPackages(array($name => $constraint)); return reset($packages['packages']); } if ($hasProviders) { foreach ($this->getProviderNames() as $providerName) { if ($name === $providerName) { return $this->filterPackages($this->whatProvides($providerName), $constraint, true); } } return null; } return parent::findPackage($name, $constraint); } /** * @inheritDoc */ public function findPackages($name, $constraint = null) { // this call initializes loadRootServerFile which is needed for the rest below to work $hasProviders = $this->hasProviders(); $name = strtolower($name); if (null !== $constraint && !$constraint instanceof ConstraintInterface) { $constraint = $this->versionParser->parseConstraints($constraint); } if ($this->lazyProvidersUrl) { if ($this->hasPartialPackages() && isset($this->partialPackagesByName[$name])) { return $this->filterPackages($this->whatProvides($name), $constraint); } if ($this->hasAvailablePackageList && !$this->lazyProvidersRepoContains($name)) { return array(); } $result = $this->loadAsyncPackages(array($name => $constraint)); return $result['packages']; } if ($hasProviders) { foreach ($this->getProviderNames() as $providerName) { if ($name === $providerName) { return $this->filterPackages($this->whatProvides($providerName), $constraint); } } return array(); } return parent::findPackages($name, $constraint); } /** * @param array $packages * @param ConstraintInterface|null $constraint * @param bool $returnFirstMatch * * @return BasePackage|array|null */ private function filterPackages(array $packages, $constraint = null, $returnFirstMatch = false) { if (null === $constraint) { if ($returnFirstMatch) { return reset($packages); } return $packages; } $filteredPackages = array(); foreach ($packages as $package) { $pkgConstraint = new Constraint('==', $package->getVersion()); if ($constraint->matches($pkgConstraint)) { if ($returnFirstMatch) { return $package; } $filteredPackages[] = $package; } } if ($returnFirstMatch) { return null; } return $filteredPackages; } public function getPackages() { $hasProviders = $this->hasProviders(); if ($this->lazyProvidersUrl) { if (is_array($this->availablePackages) && !$this->availablePackagePatterns) { $packageMap = array(); foreach ($this->availablePackages as $name) { $packageMap[$name] = new MatchAllConstraint(); } $result = $this->loadAsyncPackages($packageMap); return array_values($result['packages']); } if ($this->hasPartialPackages()) { if (!is_array($this->partialPackagesByName)) { throw new \LogicException('hasPartialPackages failed to initialize $this->partialPackagesByName'); } return $this->createPackages($this->partialPackagesByName, 'packages.json inline packages'); } throw new \LogicException('Composer repositories that have lazy providers and no available-packages list can not load the complete list of packages, use getPackageNames instead.'); } if ($hasProviders) { throw new \LogicException('Composer repositories that have providers can not load the complete list of packages, use getPackageNames instead.'); } return parent::getPackages(); } /** * @param string|null $packageFilter Package pattern filter which can include "*" as a wildcard * * @return string[] */ public function getPackageNames($packageFilter = null) { $hasProviders = $this->hasProviders(); $filterResults = /** * @param list $results * @return list */ function (array $results) { return $results; } ; if (null !== $packageFilter && '' !== $packageFilter) { $packageFilterRegex = BasePackage::packageNameToRegexp($packageFilter); $filterResults = /** * @param list $results * @return list */ function (array $results) use ($packageFilterRegex) { /** @var list $results */ return Preg::grep($packageFilterRegex, $results); } ; } if ($this->lazyProvidersUrl) { if (is_array($this->availablePackages)) { return $filterResults(array_keys($this->availablePackages)); } if ($this->listUrl) { // no need to call $filterResults here as the $packageFilter is applied in the function itself return $this->loadPackageList($packageFilter); } if ($this->hasPartialPackages() && $this->partialPackagesByName !== null) { return $filterResults(array_keys($this->partialPackagesByName)); } return array(); } if ($hasProviders) { return $filterResults($this->getProviderNames()); } $names = array(); foreach ($this->getPackages() as $package) { $names[] = $package->getPrettyName(); } return $filterResults($names); } /** * @return list */ private function getVendorNames() { $cacheKey = 'vendor-list.txt'; $cacheAge = $this->cache->getAge($cacheKey); if (false !== $cacheAge && $cacheAge < 600 && ($cachedData = $this->cache->read($cacheKey)) !== false) { $cachedData = explode("\n", $cachedData); return $cachedData; } $names = $this->getPackageNames(); $uniques = array(); foreach ($names as $name) { // @phpstan-ignore-next-line $uniques[substr($name, 0, strpos($name, '/'))] = true; } $vendors = array_keys($uniques); if (!$this->cache->isReadOnly()) { $this->cache->write($cacheKey, implode("\n", $vendors)); } return $vendors; } /** * @param string|null $packageFilter * @return list */ private function loadPackageList($packageFilter = null) { if (null === $this->listUrl) { throw new \LogicException('Make sure to call loadRootServerFile before loadPackageList'); } $url = $this->listUrl; if (is_string($packageFilter) && $packageFilter !== '') { $url .= '?filter='.urlencode($packageFilter); $result = $this->httpDownloader->get($url, $this->options)->decodeJson(); return $result['packageNames']; } $cacheKey = 'package-list.txt'; $cacheAge = $this->cache->getAge($cacheKey); if (false !== $cacheAge && $cacheAge < 600 && ($cachedData = $this->cache->read($cacheKey)) !== false) { $cachedData = explode("\n", $cachedData); return $cachedData; } $result = $this->httpDownloader->get($url, $this->options)->decodeJson(); if (!$this->cache->isReadOnly()) { $this->cache->write($cacheKey, implode("\n", $result['packageNames'])); } return $result['packageNames']; } public function loadPackages(array $packageNameMap, array $acceptableStabilities, array $stabilityFlags, array $alreadyLoaded = array()) { // this call initializes loadRootServerFile which is needed for the rest below to work $hasProviders = $this->hasProviders(); if (!$hasProviders && !$this->hasPartialPackages() && !$this->lazyProvidersUrl) { return parent::loadPackages($packageNameMap, $acceptableStabilities, $stabilityFlags, $alreadyLoaded); } $packages = array(); $namesFound = array(); if ($hasProviders || $this->hasPartialPackages()) { foreach ($packageNameMap as $name => $constraint) { $matches = array(); // if a repo has no providers but only partial packages and the partial packages are missing // then we don't want to call whatProvides as it would try to load from the providers and fail if (!$hasProviders && !isset($this->partialPackagesByName[$name])) { continue; } $candidates = $this->whatProvides($name, $acceptableStabilities, $stabilityFlags, $alreadyLoaded); foreach ($candidates as $candidate) { if ($candidate->getName() !== $name) { throw new \LogicException('whatProvides should never return a package with a different name than the requested one'); } $namesFound[$name] = true; if (!$constraint || $constraint->matches(new Constraint('==', $candidate->getVersion()))) { $matches[spl_object_hash($candidate)] = $candidate; if ($candidate instanceof AliasPackage && !isset($matches[spl_object_hash($candidate->getAliasOf())])) { $matches[spl_object_hash($candidate->getAliasOf())] = $candidate->getAliasOf(); } } } // add aliases of matched packages even if they did not match the constraint foreach ($candidates as $candidate) { if ($candidate instanceof AliasPackage) { if (isset($matches[spl_object_hash($candidate->getAliasOf())])) { $matches[spl_object_hash($candidate)] = $candidate; } } } $packages = array_merge($packages, $matches); unset($packageNameMap[$name]); } } if ($this->lazyProvidersUrl && count($packageNameMap)) { if ($this->hasAvailablePackageList) { foreach ($packageNameMap as $name => $constraint) { if (!$this->lazyProvidersRepoContains(strtolower($name))) { unset($packageNameMap[$name]); } } } $result = $this->loadAsyncPackages($packageNameMap, $acceptableStabilities, $stabilityFlags, $alreadyLoaded); $packages = array_merge($packages, $result['packages']); $namesFound = array_merge($namesFound, $result['namesFound']); } return array('namesFound' => array_keys($namesFound), 'packages' => $packages); } /** * @inheritDoc */ public function search($query, $mode = 0, $type = null) { $this->loadRootServerFile(600); if ($this->searchUrl && $mode === self::SEARCH_FULLTEXT) { $url = str_replace(array('%query%', '%type%'), array($query, $type), $this->searchUrl); $search = $this->httpDownloader->get($url, $this->options)->decodeJson(); if (empty($search['results'])) { return array(); } $results = array(); foreach ($search['results'] as $result) { // do not show virtual packages in results as they are not directly useful from a composer perspective if (!empty($result['virtual'])) { continue; } $results[] = $result; } return $results; } if ($mode === self::SEARCH_VENDOR) { $results = array(); $regex = '{(?:'.implode('|', Preg::split('{\s+}', $query)).')}i'; $vendorNames = $this->getVendorNames(); foreach (Preg::grep($regex, $vendorNames) as $name) { $results[] = array('name' => $name, 'description' => ''); } return $results; } if ($this->hasProviders() || $this->lazyProvidersUrl) { // optimize search for "^foo/bar" where at least "^foo/" is present by loading this directly from the listUrl if present if (Preg::isMatch('{^\^(?P(?P[a-z0-9_.-]+)/[a-z0-9_.-]*)\*?$}i', $query, $match) && $this->listUrl !== null) { $url = $this->listUrl . '?vendor='.urlencode($match['vendor']).'&filter='.urlencode($match['query'].'*'); $result = $this->httpDownloader->get($url, $this->options)->decodeJson(); $results = array(); foreach ($result['packageNames'] as $name) { $results[] = array('name' => $name, 'description' => ''); } return $results; } $results = array(); $regex = '{(?:'.implode('|', Preg::split('{\s+}', $query)).')}i'; $packageNames = $this->getPackageNames(); foreach (Preg::grep($regex, $packageNames) as $name) { $results[] = array('name' => $name, 'description' => ''); } return $results; } return parent::search($query, $mode); } public function getProviders($packageName) { $this->loadRootServerFile(); $result = array(); if ($this->providersApiUrl) { $apiResult = $this->httpDownloader->get(str_replace('%package%', $packageName, $this->providersApiUrl), $this->options)->decodeJson(); foreach ($apiResult['providers'] as $provider) { $result[$provider['name']] = $provider; } return $result; } if ($this->hasPartialPackages()) { if (!is_array($this->partialPackagesByName)) { throw new \LogicException('hasPartialPackages failed to initialize $this->partialPackagesByName'); } foreach ($this->partialPackagesByName as $versions) { foreach ($versions as $candidate) { if (isset($result[$candidate['name']]) || !isset($candidate['provide'][$packageName])) { continue; } $result[$candidate['name']] = array( 'name' => $candidate['name'], 'description' => isset($candidate['description']) ? $candidate['description'] : '', 'type' => isset($candidate['type']) ? $candidate['type'] : '', ); } } } if ($this->packages) { $result = array_merge($result, parent::getProviders($packageName)); } return $result; } /** * @return string[] */ private function getProviderNames() { $this->loadRootServerFile(); if (null === $this->providerListing) { $this->loadProviderListings($this->loadRootServerFile()); } if ($this->lazyProvidersUrl) { // Can not determine list of provided packages for lazy repositories return array(); } if ($this->providersUrl) { return array_keys($this->providerListing); } return array(); } /** * @return void */ protected function configurePackageTransportOptions(PackageInterface $package) { foreach ($package->getDistUrls() as $url) { if (strpos($url, $this->baseUrl) === 0) { $package->setTransportOptions($this->options); return; } } } /** * @return bool */ private function hasProviders() { $this->loadRootServerFile(); return $this->hasProviders; } /** * @param string $name package name * @param array|null $acceptableStabilities * @phpstan-param array|null $acceptableStabilities * @param array|null $stabilityFlags an array of package name => BasePackage::STABILITY_* value * @phpstan-param array|null $stabilityFlags * @param array> $alreadyLoaded * * @return array */ private function whatProvides($name, array $acceptableStabilities = null, array $stabilityFlags = null, array $alreadyLoaded = array()) { $packagesSource = null; if (!$this->hasPartialPackages() || !isset($this->partialPackagesByName[$name])) { // skip platform packages, root package and composer-plugin-api if (PlatformRepository::isPlatformPackage($name) || '__root__' === $name) { return array(); } if (null === $this->providerListing) { $this->loadProviderListings($this->loadRootServerFile()); } $useLastModifiedCheck = false; if ($this->lazyProvidersUrl && !isset($this->providerListing[$name])) { $hash = null; $url = str_replace('%package%', $name, $this->lazyProvidersUrl); $cacheKey = 'provider-'.strtr($name, '/', '$').'.json'; $useLastModifiedCheck = true; } elseif ($this->providersUrl) { // package does not exist in this repo if (!isset($this->providerListing[$name])) { return array(); } $hash = $this->providerListing[$name]['sha256']; $url = str_replace(array('%package%', '%hash%'), array($name, $hash), $this->providersUrl); $cacheKey = 'provider-'.strtr($name, '/', '$').'.json'; } else { return array(); } $packages = null; if (!$useLastModifiedCheck && $hash && $this->cache->sha256($cacheKey) === $hash) { $packages = json_decode($this->cache->read($cacheKey), true); $packagesSource = 'cached file ('.$cacheKey.' originating from '.Url::sanitize($url).')'; } elseif ($useLastModifiedCheck) { if ($contents = $this->cache->read($cacheKey)) { $contents = json_decode($contents, true); // we already loaded some packages from this file, so assume it is fresh and avoid fetching it again if (isset($alreadyLoaded[$name])) { $packages = $contents; $packagesSource = 'cached file ('.$cacheKey.' originating from '.Url::sanitize($url).')'; } elseif (isset($contents['last-modified'])) { $response = $this->fetchFileIfLastModified($url, $cacheKey, $contents['last-modified']); $packages = true === $response ? $contents : $response; $packagesSource = true === $response ? 'cached file ('.$cacheKey.' originating from '.Url::sanitize($url).')' : 'downloaded file ('.Url::sanitize($url).')'; } } } if (!$packages) { try { $packages = $this->fetchFile($url, $cacheKey, $hash, $useLastModifiedCheck); $packagesSource = 'downloaded file ('.Url::sanitize($url).')'; } catch (TransportException $e) { // 404s are acceptable for lazy provider repos if ($this->lazyProvidersUrl && in_array($e->getStatusCode(), array(404, 499), true)) { $packages = array('packages' => array()); $packagesSource = 'not-found file ('.Url::sanitize($url).')'; if ($e->getStatusCode() === 499) { $this->io->error('' . $e->getMessage() . ''); } } else { throw $e; } } } $loadingPartialPackage = false; } else { $packages = array('packages' => array('versions' => $this->partialPackagesByName[$name])); $packagesSource = 'root file ('.Url::sanitize($this->getPackagesJsonUrl()).')'; $loadingPartialPackage = true; } $result = array(); $versionsToLoad = array(); foreach ($packages['packages'] as $versions) { foreach ($versions as $version) { $normalizedName = strtolower($version['name']); // only load the actual named package, not other packages that might find themselves in the same file if ($normalizedName !== $name) { continue; } if (!$loadingPartialPackage && $this->hasPartialPackages() && isset($this->partialPackagesByName[$normalizedName])) { continue; } if (!isset($versionsToLoad[$version['uid']])) { if (!isset($version['version_normalized'])) { $version['version_normalized'] = $this->versionParser->normalize($version['version']); } elseif ($version['version_normalized'] === VersionParser::DEFAULT_BRANCH_ALIAS) { // handling of existing repos which need to remain composer v1 compatible, in case the version_normalized contained VersionParser::DEFAULT_BRANCH_ALIAS, we renormalize it $version['version_normalized'] = $this->versionParser->normalize($version['version']); } // avoid loading packages which have already been loaded if (isset($alreadyLoaded[$name][$version['version_normalized']])) { continue; } if ($this->isVersionAcceptable(null, $normalizedName, $version, $acceptableStabilities, $stabilityFlags)) { $versionsToLoad[$version['uid']] = $version; } } } } // load acceptable packages in the providers $loadedPackages = $this->createPackages($versionsToLoad, $packagesSource); $uids = array_keys($versionsToLoad); foreach ($loadedPackages as $index => $package) { $package->setRepository($this); $uid = $uids[$index]; if ($package instanceof AliasPackage) { $aliased = $package->getAliasOf(); $aliased->setRepository($this); $result[$uid] = $aliased; $result[$uid.'-alias'] = $package; } else { $result[$uid] = $package; } } return $result; } /** * @inheritDoc */ protected function initialize() { parent::initialize(); $repoData = $this->loadDataFromServer(); foreach ($this->createPackages($repoData, 'root file ('.Url::sanitize($this->getPackagesJsonUrl()).')') as $package) { $this->addPackage($package); } } /** * Adds a new package to the repository * * @param PackageInterface $package */ public function addPackage(PackageInterface $package) { parent::addPackage($package); $this->configurePackageTransportOptions($package); } /** * @param array $packageNames array of package name => ConstraintInterface|null - if a constraint is provided, only packages matching it will be loaded * @param array|null $acceptableStabilities * @phpstan-param array|null $acceptableStabilities * @param array|null $stabilityFlags an array of package name => BasePackage::STABILITY_* value * @phpstan-param array|null $stabilityFlags * @param array> $alreadyLoaded * * @return array{namesFound: array, packages: array} */ private function loadAsyncPackages(array $packageNames, array $acceptableStabilities = null, array $stabilityFlags = null, array $alreadyLoaded = array()) { $this->loadRootServerFile(); $packages = array(); $namesFound = array(); $promises = array(); $repo = $this; if (!$this->lazyProvidersUrl) { throw new \LogicException('loadAsyncPackages only supports v2 protocol composer repos with a metadata-url'); } // load ~dev versions of the packages as well if needed foreach ($packageNames as $name => $constraint) { if ($acceptableStabilities === null || $stabilityFlags === null || StabilityFilter::isPackageAcceptable($acceptableStabilities, $stabilityFlags, array($name), 'dev')) { $packageNames[$name.'~dev'] = $constraint; } // if only dev stability is requested, we skip loading the non dev file if (isset($acceptableStabilities['dev']) && count($acceptableStabilities) === 1 && count($stabilityFlags) === 0) { unset($packageNames[$name]); } } foreach ($packageNames as $name => $constraint) { $name = strtolower($name); $realName = Preg::replace('{~dev$}', '', $name); // skip platform packages, root package and composer-plugin-api if (PlatformRepository::isPlatformPackage($realName) || '__root__' === $realName) { continue; } $url = str_replace('%package%', $name, $this->lazyProvidersUrl); $cacheKey = 'provider-'.strtr($name, '/', '~').'.json'; $lastModified = null; if ($contents = $this->cache->read($cacheKey)) { $contents = json_decode($contents, true); $lastModified = isset($contents['last-modified']) ? $contents['last-modified'] : null; } $promises[] = $this->asyncFetchFile($url, $cacheKey, $lastModified) ->then(function ($response) use (&$packages, &$namesFound, $url, $cacheKey, $contents, $realName, $constraint, $repo, $acceptableStabilities, $stabilityFlags, $alreadyLoaded) { $packagesSource = 'downloaded file ('.Url::sanitize($url).')'; if (true === $response) { $packagesSource = 'cached file ('.$cacheKey.' originating from '.Url::sanitize($url).')'; $response = $contents; } if (!isset($response['packages'][$realName])) { return; } $versions = $response['packages'][$realName]; if (isset($response['minified']) && $response['minified'] === 'composer/2.0') { $versions = MetadataMinifier::expand($versions); } $namesFound[$realName] = true; $versionsToLoad = array(); foreach ($versions as $version) { if (!isset($version['version_normalized'])) { $version['version_normalized'] = $repo->versionParser->normalize($version['version']); } elseif ($version['version_normalized'] === VersionParser::DEFAULT_BRANCH_ALIAS) { // handling of existing repos which need to remain composer v1 compatible, in case the version_normalized contained VersionParser::DEFAULT_BRANCH_ALIAS, we renormalize it $version['version_normalized'] = $repo->versionParser->normalize($version['version']); } // avoid loading packages which have already been loaded if (isset($alreadyLoaded[$realName][$version['version_normalized']])) { continue; } if ($repo->isVersionAcceptable($constraint, $realName, $version, $acceptableStabilities, $stabilityFlags)) { $versionsToLoad[] = $version; } } $loadedPackages = $repo->createPackages($versionsToLoad, $packagesSource); foreach ($loadedPackages as $package) { $package->setRepository($repo); $packages[spl_object_hash($package)] = $package; if ($package instanceof AliasPackage && !isset($packages[spl_object_hash($package->getAliasOf())])) { $package->getAliasOf()->setRepository($repo); $packages[spl_object_hash($package->getAliasOf())] = $package->getAliasOf(); } } }); } $this->loop->wait($promises); return array('namesFound' => $namesFound, 'packages' => $packages); // RepositorySet should call loadMetadata, getMetadata when all promises resolved, then metadataComplete when done so we can GC the loaded json and whatnot then as needed } /** * TODO v3 should make this private once we can drop PHP 5.3 support * * @private * * @param ConstraintInterface|null $constraint * @param string $name package name (must be lowercased already) * @param array $versionData * @param array|null $acceptableStabilities * @phpstan-param array|null $acceptableStabilities * @param array|null $stabilityFlags an array of package name => BasePackage::STABILITY_* value * @phpstan-param array|null $stabilityFlags * * @return bool */ public function isVersionAcceptable($constraint, $name, $versionData, array $acceptableStabilities = null, array $stabilityFlags = null) { $versions = array($versionData['version_normalized']); if ($alias = $this->loader->getBranchAlias($versionData)) { $versions[] = $alias; } foreach ($versions as $version) { if (null !== $acceptableStabilities && null !== $stabilityFlags && !StabilityFilter::isPackageAcceptable($acceptableStabilities, $stabilityFlags, array($name), VersionParser::parseStability($version))) { continue; } if ($constraint && !CompilingMatcher::match($constraint, Constraint::OP_EQ, $version)) { continue; } return true; } return false; } /** * @return string */ private function getPackagesJsonUrl() { $jsonUrlParts = parse_url($this->url); if (isset($jsonUrlParts['path']) && false !== strpos($jsonUrlParts['path'], '.json')) { return $this->url; } return $this->url . '/packages.json'; } /** * @param int|null $rootMaxAge * @return array */ protected function loadRootServerFile($rootMaxAge = null) { if (null !== $this->rootData) { return $this->rootData; } if (!extension_loaded('openssl') && strpos($this->url, 'https') === 0) { throw new \RuntimeException('You must enable the openssl extension in your php.ini to load information from '.$this->url); } if ($cachedData = $this->cache->read('packages.json')) { $cachedData = json_decode($cachedData, true); if ($rootMaxAge !== null && ($age = $this->cache->getAge('packages.json')) !== false && $age <= $rootMaxAge) { $data = $cachedData; } elseif (isset($cachedData['last-modified'])) { $response = $this->fetchFileIfLastModified($this->getPackagesJsonUrl(), 'packages.json', $cachedData['last-modified']); $data = true === $response ? $cachedData : $response; } } if (!isset($data)) { $data = $this->fetchFile($this->getPackagesJsonUrl(), 'packages.json', null, true); } if (!empty($data['notify-batch'])) { $this->notifyUrl = $this->canonicalizeUrl($data['notify-batch']); } elseif (!empty($data['notify'])) { $this->notifyUrl = $this->canonicalizeUrl($data['notify']); } if (!empty($data['search'])) { $this->searchUrl = $this->canonicalizeUrl($data['search']); } if (!empty($data['mirrors'])) { foreach ($data['mirrors'] as $mirror) { if (!empty($mirror['git-url'])) { $this->sourceMirrors['git'][] = array('url' => $mirror['git-url'], 'preferred' => !empty($mirror['preferred'])); } if (!empty($mirror['hg-url'])) { $this->sourceMirrors['hg'][] = array('url' => $mirror['hg-url'], 'preferred' => !empty($mirror['preferred'])); } if (!empty($mirror['dist-url'])) { $this->distMirrors[] = array( 'url' => $this->canonicalizeUrl($mirror['dist-url']), 'preferred' => !empty($mirror['preferred']), ); } } } if (!empty($data['providers-lazy-url'])) { $this->lazyProvidersUrl = $this->canonicalizeUrl($data['providers-lazy-url']); $this->hasProviders = true; $this->hasPartialPackages = !empty($data['packages']) && is_array($data['packages']); } // metadata-url indicates V2 repo protocol so it takes over from all the V1 types // V2 only has lazyProviders and possibly partial packages, but no ability to process anything else, // V2 also supports async loading if (!empty($data['metadata-url'])) { $this->lazyProvidersUrl = $this->canonicalizeUrl($data['metadata-url']); $this->providersUrl = null; $this->hasProviders = false; $this->hasPartialPackages = !empty($data['packages']) && is_array($data['packages']); $this->allowSslDowngrade = false; // provides a list of package names that are available in this repo // this disables lazy-provider behavior in the sense that if a list is available we assume it is finite and won't search for other packages in that repo // while if no list is there lazyProvidersUrl is used when looking for any package name to see if the repo knows it if (!empty($data['available-packages'])) { $availPackages = array_map('strtolower', $data['available-packages']); $this->availablePackages = array_combine($availPackages, $availPackages); $this->hasAvailablePackageList = true; } // Provides a list of package name patterns (using * wildcards to match any substring, e.g. "vendor/*") that are available in this repo // Disables lazy-provider behavior as with available-packages, but may allow much more compact expression of packages covered by this repository. // Over-specifying covered packages is safe, but may result in increased traffic to your repository. if (!empty($data['available-package-patterns'])) { $this->availablePackagePatterns = array_map(function ($pattern) { return BasePackage::packageNameToRegexp($pattern); }, $data['available-package-patterns']); $this->hasAvailablePackageList = true; } // Remove legacy keys as most repos need to be compatible with Composer v1 // as well but we are not interested in the old format anymore at this point unset($data['providers-url'], $data['providers'], $data['providers-includes']); } if ($this->allowSslDowngrade) { $this->url = str_replace('https://', 'http://', $this->url); $this->baseUrl = str_replace('https://', 'http://', $this->baseUrl); } if (!empty($data['providers-url'])) { $this->providersUrl = $this->canonicalizeUrl($data['providers-url']); $this->hasProviders = true; } if (!empty($data['list'])) { $this->listUrl = $this->canonicalizeUrl($data['list']); } if (!empty($data['providers']) || !empty($data['providers-includes'])) { $this->hasProviders = true; } if (!empty($data['providers-api'])) { $this->providersApiUrl = $this->canonicalizeUrl($data['providers-api']); } return $this->rootData = $data; } /** * @param string $url * * @return string */ private function canonicalizeUrl($url) { if ('/' === $url[0]) { if (Preg::isMatch('{^[^:]++://[^/]*+}', $this->url, $matches)) { return $matches[0] . $url; } return $this->url; } return $url; } /** * @return mixed[] */ private function loadDataFromServer() { $data = $this->loadRootServerFile(); return $this->loadIncludes($data); } /** * @return bool */ private function hasPartialPackages() { if ($this->hasPartialPackages && null === $this->partialPackagesByName) { $this->initializePartialPackages(); } return $this->hasPartialPackages; } /** * @param array{providers?: mixed[], provider-includes?: mixed[]} $data * * @return void */ private function loadProviderListings($data) { if (isset($data['providers'])) { if (!is_array($this->providerListing)) { $this->providerListing = array(); } $this->providerListing = array_merge($this->providerListing, $data['providers']); } if ($this->providersUrl && isset($data['provider-includes'])) { $includes = $data['provider-includes']; foreach ($includes as $include => $metadata) { $url = $this->baseUrl . '/' . str_replace('%hash%', $metadata['sha256'], $include); $cacheKey = str_replace(array('%hash%','$'), '', $include); if ($this->cache->sha256($cacheKey) === $metadata['sha256']) { $includedData = json_decode($this->cache->read($cacheKey), true); } else { $includedData = $this->fetchFile($url, $cacheKey, $metadata['sha256']); } $this->loadProviderListings($includedData); } } } /** * @param mixed[] $data * * @return mixed[] */ private function loadIncludes($data) { $packages = array(); // legacy repo handling if (!isset($data['packages']) && !isset($data['includes'])) { foreach ($data as $pkg) { if (isset($pkg['versions']) && is_array($pkg['versions'])) { foreach ($pkg['versions'] as $metadata) { $packages[] = $metadata; } } } return $packages; } if (isset($data['packages'])) { foreach ($data['packages'] as $package => $versions) { foreach ($versions as $version => $metadata) { $packages[] = $metadata; } } } if (isset($data['includes'])) { foreach ($data['includes'] as $include => $metadata) { if (isset($metadata['sha1']) && $this->cache->sha1((string) $include) === $metadata['sha1']) { $includedData = json_decode($this->cache->read((string) $include), true); } else { $includedData = $this->fetchFile($include); } $packages = array_merge($packages, $this->loadIncludes($includedData)); } } return $packages; } /** * TODO v3 should make this private once we can drop PHP 5.3 support * @private * * @param mixed[] $packages * @param string|null $source * * @return list */ public function createPackages(array $packages, $source = null) { if (!$packages) { return array(); } try { foreach ($packages as &$data) { if (!isset($data['notification-url'])) { $data['notification-url'] = $this->notifyUrl; } } $packageInstances = $this->loader->loadPackages($packages); foreach ($packageInstances as $package) { if (isset($this->sourceMirrors[$package->getSourceType()])) { $package->setSourceMirrors($this->sourceMirrors[$package->getSourceType()]); } $package->setDistMirrors($this->distMirrors); $this->configurePackageTransportOptions($package); } return $packageInstances; } catch (\Exception $e) { throw new \RuntimeException('Could not load packages '.(isset($packages[0]['name']) ? $packages[0]['name'] : json_encode($packages)).' in '.$this->getRepoName().($source ? ' from '.$source : '').': ['.get_class($e).'] '.$e->getMessage(), 0, $e); } } /** * @param string $filename * @param string|null $cacheKey * @param string|null $sha256 * @param bool $storeLastModifiedTime * * @return array */ protected function fetchFile($filename, $cacheKey = null, $sha256 = null, $storeLastModifiedTime = false) { if (null === $cacheKey) { $cacheKey = $filename; $filename = $this->baseUrl.'/'.$filename; } // url-encode $ signs in URLs as bad proxies choke on them if (($pos = strpos($filename, '$')) && Preg::isMatch('{^https?://}i', $filename)) { $filename = substr($filename, 0, $pos) . '%24' . substr($filename, $pos + 1); } $retries = 3; while ($retries--) { try { $options = $this->options; if ($this->eventDispatcher) { $preFileDownloadEvent = new PreFileDownloadEvent(PluginEvents::PRE_FILE_DOWNLOAD, $this->httpDownloader, $filename, 'metadata', array('repository' => $this)); $preFileDownloadEvent->setTransportOptions($this->options); $this->eventDispatcher->dispatch($preFileDownloadEvent->getName(), $preFileDownloadEvent); $filename = $preFileDownloadEvent->getProcessedUrl(); $options = $preFileDownloadEvent->getTransportOptions(); } $response = $this->httpDownloader->get($filename, $options); $json = (string) $response->getBody(); if ($sha256 && $sha256 !== hash('sha256', $json)) { // undo downgrade before trying again if http seems to be hijacked or modifying content somehow if ($this->allowSslDowngrade) { $this->url = str_replace('http://', 'https://', $this->url); $this->baseUrl = str_replace('http://', 'https://', $this->baseUrl); $filename = str_replace('http://', 'https://', $filename); } if ($retries > 0) { usleep(100000); continue; } // TODO use scarier wording once we know for sure it doesn't do false positives anymore throw new RepositorySecurityException('The contents of '.$filename.' do not match its signature. This could indicate a man-in-the-middle attack or e.g. antivirus software corrupting files. Try running composer again and report this if you think it is a mistake.'); } if ($this->eventDispatcher) { $postFileDownloadEvent = new PostFileDownloadEvent(PluginEvents::POST_FILE_DOWNLOAD, null, $sha256, $filename, 'metadata', array('response' => $response, 'repository' => $this)); $this->eventDispatcher->dispatch($postFileDownloadEvent->getName(), $postFileDownloadEvent); } $data = $response->decodeJson(); HttpDownloader::outputWarnings($this->io, $this->url, $data); if ($cacheKey && !$this->cache->isReadOnly()) { if ($storeLastModifiedTime) { $lastModifiedDate = $response->getHeader('last-modified'); if ($lastModifiedDate) { $data['last-modified'] = $lastModifiedDate; $json = JsonFile::encode($data, 0); } } $this->cache->write($cacheKey, $json); } $response->collect(); break; } catch (\Exception $e) { if ($e instanceof \LogicException) { throw $e; } if ($e instanceof TransportException && $e->getStatusCode() === 404) { throw $e; } if ($e instanceof RepositorySecurityException) { throw $e; } if ($cacheKey && ($contents = $this->cache->read($cacheKey))) { if (!$this->degradedMode) { $this->io->writeError(''.$this->url.' could not be fully loaded ('.$e->getMessage().'), package information was loaded from the local cache and may be out of date'); } $this->degradedMode = true; $data = JsonFile::parseJson($contents, $this->cache->getRoot().$cacheKey); break; } throw $e; } } if (!isset($data)) { throw new \LogicException("ComposerRepository: Undefined \$data. Please report at https://github.com/composer/composer/issues/new."); } return $data; } /** * @param string $filename * @param string $cacheKey * @param string $lastModifiedTime * * @return array|true */ private function fetchFileIfLastModified($filename, $cacheKey, $lastModifiedTime) { try { $options = $this->options; if ($this->eventDispatcher) { $preFileDownloadEvent = new PreFileDownloadEvent(PluginEvents::PRE_FILE_DOWNLOAD, $this->httpDownloader, $filename, 'metadata', array('repository' => $this)); $preFileDownloadEvent->setTransportOptions($this->options); $this->eventDispatcher->dispatch($preFileDownloadEvent->getName(), $preFileDownloadEvent); $filename = $preFileDownloadEvent->getProcessedUrl(); $options = $preFileDownloadEvent->getTransportOptions(); } if (isset($options['http']['header'])) { $options['http']['header'] = (array) $options['http']['header']; } $options['http']['header'][] = 'If-Modified-Since: '.$lastModifiedTime; $response = $this->httpDownloader->get($filename, $options); $json = (string) $response->getBody(); if ($json === '' && $response->getStatusCode() === 304) { return true; } if ($this->eventDispatcher) { $postFileDownloadEvent = new PostFileDownloadEvent(PluginEvents::POST_FILE_DOWNLOAD, null, null, $filename, 'metadata', array('response' => $response, 'repository' => $this)); $this->eventDispatcher->dispatch($postFileDownloadEvent->getName(), $postFileDownloadEvent); } $data = $response->decodeJson(); HttpDownloader::outputWarnings($this->io, $this->url, $data); $lastModifiedDate = $response->getHeader('last-modified'); $response->collect(); if ($lastModifiedDate) { $data['last-modified'] = $lastModifiedDate; $json = JsonFile::encode($data, 0); } if (!$this->cache->isReadOnly()) { $this->cache->write($cacheKey, $json); } return $data; } catch (\Exception $e) { if ($e instanceof \LogicException) { throw $e; } if ($e instanceof TransportException && $e->getStatusCode() === 404) { throw $e; } if (!$this->degradedMode) { $this->io->writeError(''.$this->url.' could not be fully loaded ('.$e->getMessage().'), package information was loaded from the local cache and may be out of date'); } $this->degradedMode = true; return true; } } /** * @param string $filename * @param string $cacheKey * @param string|null $lastModifiedTime * * @return \React\Promise\PromiseInterface */ private function asyncFetchFile($filename, $cacheKey, $lastModifiedTime = null) { if (isset($this->packagesNotFoundCache[$filename])) { return \React\Promise\resolve(array('packages' => array())); } if (isset($this->freshMetadataUrls[$filename]) && $lastModifiedTime) { // make it look like we got a 304 response return \React\Promise\resolve(true); } $httpDownloader = $this->httpDownloader; $options = $this->options; if ($this->eventDispatcher) { $preFileDownloadEvent = new PreFileDownloadEvent(PluginEvents::PRE_FILE_DOWNLOAD, $this->httpDownloader, $filename, 'metadata', array('repository' => $this)); $preFileDownloadEvent->setTransportOptions($this->options); $this->eventDispatcher->dispatch($preFileDownloadEvent->getName(), $preFileDownloadEvent); $filename = $preFileDownloadEvent->getProcessedUrl(); $options = $preFileDownloadEvent->getTransportOptions(); } if ($lastModifiedTime) { if (isset($options['http']['header'])) { $options['http']['header'] = (array) $options['http']['header']; } $options['http']['header'][] = 'If-Modified-Since: '.$lastModifiedTime; } $io = $this->io; $url = $this->url; $cache = $this->cache; $degradedMode = &$this->degradedMode; $eventDispatcher = $this->eventDispatcher; $repo = $this; $accept = function ($response) use ($io, $url, $filename, $cache, $cacheKey, $eventDispatcher, $repo) { // package not found is acceptable for a v2 protocol repository if ($response->getStatusCode() === 404) { $repo->packagesNotFoundCache[$filename] = true; return array('packages' => array()); } $json = (string) $response->getBody(); if ($json === '' && $response->getStatusCode() === 304) { $repo->freshMetadataUrls[$filename] = true; return true; } if ($eventDispatcher) { $postFileDownloadEvent = new PostFileDownloadEvent(PluginEvents::POST_FILE_DOWNLOAD, null, null, $filename, 'metadata', array('response' => $response, 'repository' => $repo)); $eventDispatcher->dispatch($postFileDownloadEvent->getName(), $postFileDownloadEvent); } $data = $response->decodeJson(); HttpDownloader::outputWarnings($io, $url, $data); $lastModifiedDate = $response->getHeader('last-modified'); $response->collect(); if ($lastModifiedDate) { $data['last-modified'] = $lastModifiedDate; $json = JsonFile::encode($data, JsonFile::JSON_UNESCAPED_SLASHES | JsonFile::JSON_UNESCAPED_UNICODE); } if (!$cache->isReadOnly()) { $cache->write($cacheKey, $json); } $repo->freshMetadataUrls[$filename] = true; return $data; }; $reject = function ($e) use ($filename, $accept, $io, $url, &$degradedMode, $repo, $lastModifiedTime) { if ($e instanceof TransportException && $e->getStatusCode() === 404) { $repo->packagesNotFoundCache[$filename] = true; return false; } if (!$degradedMode) { $io->writeError(''.$url.' could not be fully loaded ('.$e->getMessage().'), package information was loaded from the local cache and may be out of date'); } $degradedMode = true; // if the file is in the cache, we fake a 304 Not Modified to allow the process to continue if ($lastModifiedTime) { return $accept(new Response(array('url' => $url), 304, array(), '')); } // special error code returned when network is being artificially disabled if ($e instanceof TransportException && $e->getStatusCode() === 499) { return $accept(new Response(array('url' => $url), 404, array(), '')); } throw $e; }; return $httpDownloader->add($filename, $options)->then($accept, $reject); } /** * This initializes the packages key of a partial packages.json that contain some packages inlined + a providers-lazy-url * * This should only be called once * * @return void */ private function initializePartialPackages() { $rootData = $this->loadRootServerFile(); $this->partialPackagesByName = array(); foreach ($rootData['packages'] as $package => $versions) { foreach ($versions as $version) { $this->partialPackagesByName[strtolower($version['name'])][] = $version; } } // wipe rootData as it is fully consumed at this point and this saves some memory $this->rootData = true; } /** * Checks if the package name is present in this lazy providers repo * * @param string $name * @return bool true if the package name is present in availablePackages or matched by availablePackagePatterns */ protected function lazyProvidersRepoContains($name) { if (!$this->hasAvailablePackageList) { throw new \LogicException('lazyProvidersRepoContains should not be called unless hasAvailablePackageList is true'); } if (is_array($this->availablePackages) && isset($this->availablePackages[$name])) { return true; } if (is_array($this->availablePackagePatterns)) { foreach ($this->availablePackagePatterns as $providerRegex) { if (Preg::isMatch($providerRegex, $name)) { return true; } } } return false; } } * Jordi Boggiano * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Composer\Repository; /** * Lock array repository. * * Regular array repository, only uses a different type to identify the lock file as the source of info * * @author Nils Adermann */ class LockArrayRepository extends ArrayRepository { public function getRepoName() { return 'lock repo'; } } * Jordi Boggiano * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Composer\Repository; /** * Installed array repository. * * This is used as an in-memory InstalledRepository mostly for testing purposes * * @author Jordi Boggiano */ class InstalledArrayRepository extends WritableArrayRepository implements InstalledRepositoryInterface { public function getRepoName() { return 'installed '.parent::getRepoName(); } /** * @inheritDoc */ public function isFresh() { // this is not a completely correct implementation but there is no way to // distinguish an empty repo and a newly created one given this is all in-memory return $this->count() === 0; } } * Jordi Boggiano * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Composer\Repository; use Composer\Package\RootPackageInterface; /** * Root package repository. * * This is used for serving the RootPackage inside an in-memory InstalledRepository * * @author Jordi Boggiano */ class RootPackageRepository extends ArrayRepository { public function __construct(RootPackageInterface $package) { parent::__construct(array($package)); } public function getRepoName() { return 'root package repo'; } } * Jordi Boggiano * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Composer\Repository; use Composer\Downloader\TransportException; use Composer\Pcre\Preg; use Composer\Repository\Vcs\VcsDriverInterface; use Composer\Package\Version\VersionParser; use Composer\Package\Loader\ArrayLoader; use Composer\Package\Loader\ValidatingArrayLoader; use Composer\Package\Loader\InvalidPackageException; use Composer\Package\Loader\LoaderInterface; use Composer\EventDispatcher\EventDispatcher; use Composer\Util\ProcessExecutor; use Composer\Util\HttpDownloader; use Composer\Util\Url; use Composer\Semver\Constraint\Constraint; use Composer\IO\IOInterface; use Composer\Config; /** * @author Jordi Boggiano */ class VcsRepository extends ArrayRepository implements ConfigurableRepositoryInterface { /** @var string */ protected $url; /** @var ?string */ protected $packageName; /** @var bool */ protected $isVerbose; /** @var bool */ protected $isVeryVerbose; /** @var IOInterface */ protected $io; /** @var Config */ protected $config; /** @var VersionParser */ protected $versionParser; /** @var string */ protected $type; /** @var ?LoaderInterface */ protected $loader; /** @var array */ protected $repoConfig; /** @var HttpDownloader */ protected $httpDownloader; /** @var ProcessExecutor */ protected $processExecutor; /** @var bool */ protected $branchErrorOccurred = false; /** @var array> */ private $drivers; /** @var ?VcsDriverInterface */ private $driver; /** @var ?VersionCacheInterface */ private $versionCache; /** @var string[] */ private $emptyReferences = array(); /** @var array<'tags'|'branches', array> */ private $versionTransportExceptions = array(); /** * @param array{url: string, type?: string}&array $repoConfig * @param array>|null $drivers */ public function __construct(array $repoConfig, IOInterface $io, Config $config, HttpDownloader $httpDownloader, EventDispatcher $dispatcher = null, ProcessExecutor $process = null, array $drivers = null, VersionCacheInterface $versionCache = null) { parent::__construct(); $this->drivers = $drivers ?: array( 'github' => 'Composer\Repository\Vcs\GitHubDriver', 'gitlab' => 'Composer\Repository\Vcs\GitLabDriver', 'bitbucket' => 'Composer\Repository\Vcs\GitBitbucketDriver', 'git-bitbucket' => 'Composer\Repository\Vcs\GitBitbucketDriver', 'git' => 'Composer\Repository\Vcs\GitDriver', 'hg' => 'Composer\Repository\Vcs\HgDriver', 'perforce' => 'Composer\Repository\Vcs\PerforceDriver', 'fossil' => 'Composer\Repository\Vcs\FossilDriver', // svn must be last because identifying a subversion server for sure is practically impossible 'svn' => 'Composer\Repository\Vcs\SvnDriver', ); $this->url = $repoConfig['url']; $this->io = $io; $this->type = isset($repoConfig['type']) ? $repoConfig['type'] : 'vcs'; $this->isVerbose = $io->isVerbose(); $this->isVeryVerbose = $io->isVeryVerbose(); $this->config = $config; $this->repoConfig = $repoConfig; $this->versionCache = $versionCache; $this->httpDownloader = $httpDownloader; $this->processExecutor = $process ?: new ProcessExecutor($io); } public function getRepoName() { $driverClass = get_class($this->getDriver()); $driverType = array_search($driverClass, $this->drivers); if (!$driverType) { $driverType = $driverClass; } return 'vcs repo ('.$driverType.' '.Url::sanitize($this->url).')'; } public function getRepoConfig() { return $this->repoConfig; } /** * @return void */ public function setLoader(LoaderInterface $loader) { $this->loader = $loader; } /** * @return VcsDriverInterface|null */ public function getDriver() { if ($this->driver) { return $this->driver; } if (isset($this->drivers[$this->type])) { $class = $this->drivers[$this->type]; $this->driver = new $class($this->repoConfig, $this->io, $this->config, $this->httpDownloader, $this->processExecutor); $this->driver->initialize(); return $this->driver; } foreach ($this->drivers as $driver) { if ($driver::supports($this->io, $this->config, $this->url)) { $this->driver = new $driver($this->repoConfig, $this->io, $this->config, $this->httpDownloader, $this->processExecutor); $this->driver->initialize(); return $this->driver; } } foreach ($this->drivers as $driver) { if ($driver::supports($this->io, $this->config, $this->url, true)) { $this->driver = new $driver($this->repoConfig, $this->io, $this->config, $this->httpDownloader, $this->processExecutor); $this->driver->initialize(); return $this->driver; } } return null; } /** * @return bool */ public function hadInvalidBranches() { return $this->branchErrorOccurred; } /** * @return string[] */ public function getEmptyReferences() { return $this->emptyReferences; } /** * @return array<'tags'|'branches', array> */ public function getVersionTransportExceptions() { return $this->versionTransportExceptions; } protected function initialize() { parent::initialize(); $isVerbose = $this->isVerbose; $isVeryVerbose = $this->isVeryVerbose; $driver = $this->getDriver(); if (!$driver) { throw new \InvalidArgumentException('No driver found to handle VCS repository '.$this->url); } $this->versionParser = new VersionParser; if (!$this->loader) { $this->loader = new ArrayLoader($this->versionParser); } $hasRootIdentifierComposerJson = false; try { $hasRootIdentifierComposerJson = $driver->hasComposerFile($driver->getRootIdentifier()); if ($hasRootIdentifierComposerJson) { $data = $driver->getComposerInformation($driver->getRootIdentifier()); $this->packageName = !empty($data['name']) ? $data['name'] : null; } } catch (\Exception $e) { if ($e instanceof TransportException && $this->shouldRethrowTransportException($e)) { throw $e; } if ($isVeryVerbose) { $this->io->writeError('Skipped parsing '.$driver->getRootIdentifier().', '.$e->getMessage().''); } } foreach ($driver->getTags() as $tag => $identifier) { $tag = (string) $tag; $msg = 'Reading composer.json of ' . ($this->packageName ?: $this->url) . ' (' . $tag . ')'; if ($isVeryVerbose) { $this->io->writeError($msg); } elseif ($isVerbose) { $this->io->overwriteError($msg, false); } // strip the release- prefix from tags if present $tag = str_replace('release-', '', $tag); $cachedPackage = $this->getCachedPackageVersion($tag, $identifier, $isVerbose, $isVeryVerbose); if ($cachedPackage) { $this->addPackage($cachedPackage); continue; } if ($cachedPackage === false) { $this->emptyReferences[] = $identifier; continue; } if (!$parsedTag = $this->validateTag($tag)) { if ($isVeryVerbose) { $this->io->writeError('Skipped tag '.$tag.', invalid tag name'); } continue; } try { if (!$data = $driver->getComposerInformation($identifier)) { if ($isVeryVerbose) { $this->io->writeError('Skipped tag '.$tag.', no composer file'); } $this->emptyReferences[] = $identifier; continue; } // manually versioned package if (isset($data['version'])) { $data['version_normalized'] = $this->versionParser->normalize($data['version']); } else { // auto-versioned package, read value from tag $data['version'] = $tag; $data['version_normalized'] = $parsedTag; } // make sure tag packages have no -dev flag $data['version'] = Preg::replace('{[.-]?dev$}i', '', $data['version']); $data['version_normalized'] = Preg::replace('{(^dev-|[.-]?dev$)}i', '', $data['version_normalized']); // make sure tag do not contain the default-branch marker unset($data['default-branch']); // broken package, version doesn't match tag if ($data['version_normalized'] !== $parsedTag) { if ($isVeryVerbose) { if (Preg::isMatch('{(^dev-|[.-]?dev$)}i', $parsedTag)) { $this->io->writeError('Skipped tag '.$tag.', invalid tag name, tags can not use dev prefixes or suffixes'); } else { $this->io->writeError('Skipped tag '.$tag.', tag ('.$parsedTag.') does not match version ('.$data['version_normalized'].') in composer.json'); } } continue; } $tagPackageName = $this->packageName ?: (isset($data['name']) ? $data['name'] : ''); if ($existingPackage = $this->findPackage($tagPackageName, $data['version_normalized'])) { if ($isVeryVerbose) { $this->io->writeError('Skipped tag '.$tag.', it conflicts with an another tag ('.$existingPackage->getPrettyVersion().') as both resolve to '.$data['version_normalized'].' internally'); } continue; } if ($isVeryVerbose) { $this->io->writeError('Importing tag '.$tag.' ('.$data['version_normalized'].')'); } $this->addPackage($this->loader->load($this->preProcess($driver, $data, $identifier))); } catch (\Exception $e) { if ($e instanceof TransportException) { $this->versionTransportExceptions['tags'][$tag] = $e; if ($e->getCode() === 404) { $this->emptyReferences[] = $identifier; } if ($this->shouldRethrowTransportException($e)) { throw $e; } } if ($isVeryVerbose) { $this->io->writeError('Skipped tag '.$tag.', '.($e instanceof TransportException ? 'no composer file was found (' . $e->getCode() . ' HTTP status code)' : $e->getMessage()).''); } continue; } } if (!$isVeryVerbose) { $this->io->overwriteError('', false); } $branches = $driver->getBranches(); // make sure the root identifier branch gets loaded first if ($hasRootIdentifierComposerJson && isset($branches[$driver->getRootIdentifier()])) { $branches = array($driver->getRootIdentifier() => $branches[$driver->getRootIdentifier()]) + $branches; } foreach ($branches as $branch => $identifier) { $branch = (string) $branch; $msg = 'Reading composer.json of ' . ($this->packageName ?: $this->url) . ' (' . $branch . ')'; if ($isVeryVerbose) { $this->io->writeError($msg); } elseif ($isVerbose) { $this->io->overwriteError($msg, false); } if (!$parsedBranch = $this->validateBranch($branch)) { if ($isVeryVerbose) { $this->io->writeError('Skipped branch '.$branch.', invalid name'); } continue; } // make sure branch packages have a dev flag if (strpos($parsedBranch, 'dev-') === 0 || VersionParser::DEFAULT_BRANCH_ALIAS === $parsedBranch) { $version = 'dev-' . $branch; } else { $prefix = strpos($branch, 'v') === 0 ? 'v' : ''; $version = $prefix . Preg::replace('{(\.9{7})+}', '.x', $parsedBranch); } $cachedPackage = $this->getCachedPackageVersion($version, $identifier, $isVerbose, $isVeryVerbose, $driver->getRootIdentifier() === $branch); if ($cachedPackage) { $this->addPackage($cachedPackage); continue; } if ($cachedPackage === false) { $this->emptyReferences[] = $identifier; continue; } try { if (!$data = $driver->getComposerInformation($identifier)) { if ($isVeryVerbose) { $this->io->writeError('Skipped branch '.$branch.', no composer file'); } $this->emptyReferences[] = $identifier; continue; } // branches are always auto-versioned, read value from branch name $data['version'] = $version; $data['version_normalized'] = $parsedBranch; unset($data['default-branch']); if ($driver->getRootIdentifier() === $branch) { $data['default-branch'] = true; } if ($isVeryVerbose) { $this->io->writeError('Importing branch '.$branch.' ('.$data['version'].')'); } $packageData = $this->preProcess($driver, $data, $identifier); $package = $this->loader->load($packageData); if ($this->loader instanceof ValidatingArrayLoader && $this->loader->getWarnings()) { throw new InvalidPackageException($this->loader->getErrors(), $this->loader->getWarnings(), $packageData); } $this->addPackage($package); } catch (TransportException $e) { $this->versionTransportExceptions['branches'][$branch] = $e; if ($e->getCode() === 404) { $this->emptyReferences[] = $identifier; } if ($this->shouldRethrowTransportException($e)) { throw $e; } if ($isVeryVerbose) { $this->io->writeError('Skipped branch '.$branch.', no composer file was found (' . $e->getCode() . ' HTTP status code)'); } continue; } catch (\Exception $e) { if (!$isVeryVerbose) { $this->io->writeError(''); } $this->branchErrorOccurred = true; $this->io->writeError('Skipped branch '.$branch.', '.$e->getMessage().''); $this->io->writeError(''); continue; } } $driver->cleanup(); if (!$isVeryVerbose) { $this->io->overwriteError('', false); } if (!$this->getPackages()) { throw new InvalidRepositoryException('No valid composer.json was found in any branch or tag of '.$this->url.', could not load a package from it.'); } } /** * @param VcsDriverInterface $driver * @param array{name?: string, dist?: array{type: string, url: string, reference: string, shasum: string}, source?: array{type: string, url: string, reference: string}} $data * @param string $identifier * * @return array{name: string|null, dist: array{type: string, url: string, reference: string, shasum: string}|null, source: array{type: string, url: string, reference: string}} */ protected function preProcess(VcsDriverInterface $driver, array $data, $identifier) { // keep the name of the main identifier for all packages // this ensures that a package can be renamed in one place and that all old tags // will still be installable using that new name without requiring re-tagging $dataPackageName = isset($data['name']) ? $data['name'] : null; $data['name'] = $this->packageName ?: $dataPackageName; if (!isset($data['dist'])) { $data['dist'] = $driver->getDist($identifier); } if (!isset($data['source'])) { $data['source'] = $driver->getSource($identifier); } return $data; } /** * @param string $branch * * @return string|false */ private function validateBranch($branch) { try { $normalizedBranch = $this->versionParser->normalizeBranch($branch); // validate that the branch name has no weird characters conflicting with constraints $this->versionParser->parseConstraints($normalizedBranch); return $normalizedBranch; } catch (\Exception $e) { } return false; } /** * @param string $version * * @return string|false */ private function validateTag($version) { try { return $this->versionParser->normalize($version); } catch (\Exception $e) { } return false; } /** * @param string $version * @param string $identifier * @param bool $isVerbose * @param bool $isVeryVerbose * @param bool $isDefaultBranch * * @return \Composer\Package\CompletePackage|\Composer\Package\CompleteAliasPackage|null|false null if no cache present, false if the absence of a version was cached */ private function getCachedPackageVersion($version, $identifier, $isVerbose, $isVeryVerbose, $isDefaultBranch = false) { if (!$this->versionCache) { return null; } $cachedPackage = $this->versionCache->getVersionPackage($version, $identifier); if ($cachedPackage === false) { if ($isVeryVerbose) { $this->io->writeError('Skipped '.$version.', no composer file (cached from ref '.$identifier.')'); } return false; } if ($cachedPackage) { $msg = 'Found cached composer.json of ' . ($this->packageName ?: $this->url) . ' (' . $version . ')'; if ($isVeryVerbose) { $this->io->writeError($msg); } elseif ($isVerbose) { $this->io->overwriteError($msg, false); } unset($cachedPackage['default-branch']); if ($isDefaultBranch) { $cachedPackage['default-branch'] = true; } if ($existingPackage = $this->findPackage($cachedPackage['name'], new Constraint('=', $cachedPackage['version_normalized']))) { if ($isVeryVerbose) { $this->io->writeError('Skipped cached version '.$version.', it conflicts with an another tag ('.$existingPackage->getPrettyVersion().') as both resolve to '.$cachedPackage['version_normalized'].' internally'); } $cachedPackage = null; } } if ($cachedPackage) { return $this->loader->load($cachedPackage); } return null; } /** * @return bool */ private function shouldRethrowTransportException(TransportException $e) { return in_array($e->getCode(), array(401, 403, 429), true) || $e->getCode() >= 500; } } * Jordi Boggiano * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Composer\Repository; use Composer\Package\AliasPackage; use Composer\Package\BasePackage; use Composer\Package\CompleteAliasPackage; use Composer\Package\CompletePackage; use Composer\Package\PackageInterface; use Composer\Package\CompletePackageInterface; use Composer\Package\Version\VersionParser; use Composer\Package\Version\StabilityFilter; use Composer\Pcre\Preg; use Composer\Semver\Constraint\ConstraintInterface; use Composer\Semver\Constraint\Constraint; /** * A repository implementation that simply stores packages in an array * * @author Nils Adermann */ class ArrayRepository implements RepositoryInterface { /** @var ?array */ protected $packages = null; /** * @var ?array indexed by package unique name and used to cache hasPackage calls */ protected $packageMap = null; /** * @param array $packages */ public function __construct(array $packages = array()) { foreach ($packages as $package) { $this->addPackage($package); } } public function getRepoName() { return 'array repo (defining '.$this->count().' package'.($this->count() > 1 ? 's' : '').')'; } /** * @inheritDoc */ public function loadPackages(array $packageMap, array $acceptableStabilities, array $stabilityFlags, array $alreadyLoaded = array()) { $packages = $this->getPackages(); $result = array(); $namesFound = array(); foreach ($packages as $package) { if (array_key_exists($package->getName(), $packageMap)) { if ( (!$packageMap[$package->getName()] || $packageMap[$package->getName()]->matches(new Constraint('==', $package->getVersion()))) && StabilityFilter::isPackageAcceptable($acceptableStabilities, $stabilityFlags, $package->getNames(), $package->getStability()) && !isset($alreadyLoaded[$package->getName()][$package->getVersion()]) ) { // add selected packages which match stability requirements $result[spl_object_hash($package)] = $package; // add the aliased package for packages where the alias matches if ($package instanceof AliasPackage && !isset($result[spl_object_hash($package->getAliasOf())])) { $result[spl_object_hash($package->getAliasOf())] = $package->getAliasOf(); } } $namesFound[$package->getName()] = true; } } // add aliases of packages that were selected, even if the aliases did not match foreach ($packages as $package) { if ($package instanceof AliasPackage) { if (isset($result[spl_object_hash($package->getAliasOf())])) { $result[spl_object_hash($package)] = $package; } } } return array('namesFound' => array_keys($namesFound), 'packages' => $result); } /** * @inheritDoc */ public function findPackage($name, $constraint) { $name = strtolower($name); if (!$constraint instanceof ConstraintInterface) { $versionParser = new VersionParser(); $constraint = $versionParser->parseConstraints($constraint); } foreach ($this->getPackages() as $package) { if ($name === $package->getName()) { $pkgConstraint = new Constraint('==', $package->getVersion()); if ($constraint->matches($pkgConstraint)) { return $package; } } } return null; } /** * @inheritDoc */ public function findPackages($name, $constraint = null) { // normalize name $name = strtolower($name); $packages = array(); if (null !== $constraint && !$constraint instanceof ConstraintInterface) { $versionParser = new VersionParser(); $constraint = $versionParser->parseConstraints($constraint); } foreach ($this->getPackages() as $package) { if ($name === $package->getName()) { if (null === $constraint || $constraint->matches(new Constraint('==', $package->getVersion()))) { $packages[] = $package; } } } return $packages; } /** * @inheritDoc */ public function search($query, $mode = 0, $type = null) { if ($mode === self::SEARCH_FULLTEXT) { $regex = '{(?:'.implode('|', Preg::split('{\s+}', preg_quote($query))).')}i'; } else { // vendor/name searches expect the caller to have preg_quoted the query $regex = '{(?:'.implode('|', Preg::split('{\s+}', $query)).')}i'; } $matches = array(); foreach ($this->getPackages() as $package) { $name = $package->getName(); if ($mode === self::SEARCH_VENDOR) { list($name) = explode('/', $name); } if (isset($matches[$name])) { continue; } if (null !== $type && $package->getType() !== $type) { continue; } if (Preg::isMatch($regex, $name) || ($mode === self::SEARCH_FULLTEXT && $package instanceof CompletePackageInterface && Preg::isMatch($regex, implode(' ', (array) $package->getKeywords()) . ' ' . $package->getDescription())) ) { if ($mode === self::SEARCH_VENDOR) { $matches[$name] = array( 'name' => $name, 'description' => null, ); } else { $matches[$name] = array( 'name' => $package->getPrettyName(), 'description' => $package instanceof CompletePackageInterface ? $package->getDescription() : null, ); if ($package instanceof CompletePackageInterface && $package->isAbandoned()) { $matches[$name]['abandoned'] = $package->getReplacementPackage() ?: true; } } } } return array_values($matches); } /** * @inheritDoc */ public function hasPackage(PackageInterface $package) { if ($this->packageMap === null) { $this->packageMap = array(); foreach ($this->getPackages() as $repoPackage) { $this->packageMap[$repoPackage->getUniqueName()] = $repoPackage; } } return isset($this->packageMap[$package->getUniqueName()]); } /** * Adds a new package to the repository * * @return void */ public function addPackage(PackageInterface $package) { if (!$package instanceof BasePackage) { throw new \InvalidArgumentException('Only subclasses of BasePackage are supported'); } if (null === $this->packages) { $this->initialize(); } $package->setRepository($this); $this->packages[] = $package; if ($package instanceof AliasPackage) { $aliasedPackage = $package->getAliasOf(); if (null === $aliasedPackage->getRepository()) { $this->addPackage($aliasedPackage); } } // invalidate package map cache $this->packageMap = null; } /** * @inheritDoc */ public function getProviders($packageName) { $result = array(); foreach ($this->getPackages() as $candidate) { if (isset($result[$candidate->getName()])) { continue; } foreach ($candidate->getProvides() as $link) { if ($packageName === $link->getTarget()) { $result[$candidate->getName()] = array( 'name' => $candidate->getName(), 'description' => $candidate instanceof CompletePackageInterface ? $candidate->getDescription() : null, 'type' => $candidate->getType(), ); continue 2; } } } return $result; } /** * @param string $alias * @param string $prettyAlias * * @return AliasPackage|CompleteAliasPackage */ protected function createAliasPackage(BasePackage $package, $alias, $prettyAlias) { while ($package instanceof AliasPackage) { $package = $package->getAliasOf(); } if ($package instanceof CompletePackage) { return new CompleteAliasPackage($package, $alias, $prettyAlias); } return new AliasPackage($package, $alias, $prettyAlias); } /** * Removes package from repository. * * @param PackageInterface $package package instance * * @return void */ public function removePackage(PackageInterface $package) { $packageId = $package->getUniqueName(); foreach ($this->getPackages() as $key => $repoPackage) { if ($packageId === $repoPackage->getUniqueName()) { array_splice($this->packages, $key, 1); // invalidate package map cache $this->packageMap = null; return; } } } /** * @inheritDoc */ public function getPackages() { if (null === $this->packages) { $this->initialize(); } if (null === $this->packages) { throw new \LogicException('initialize failed to initialize the packages array'); } return $this->packages; } /** * Returns the number of packages in this repository * * @return int Number of packages */ #[\ReturnTypeWillChange] public function count() { if (null === $this->packages) { $this->initialize(); } return count($this->packages); } /** * Initializes the packages array. Mostly meant as an extension point. * * @return void */ protected function initialize() { $this->packages = array(); } } * Jordi Boggiano * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Composer\Repository; use Composer\Package\BasePackage; use Composer\Package\PackageInterface; use Composer\Package\Version\VersionParser; use Composer\Semver\Constraint\ConstraintInterface; use Composer\Semver\Constraint\Constraint; use Composer\Semver\Constraint\MatchAllConstraint; use Composer\Package\RootPackageInterface; use Composer\Package\Link; /** * Installed repository is a composite of all installed repo types. * * The main use case is tagging a repo as an "installed" repository, and offering a way to get providers/replacers easily. * * Installed repos are LockArrayRepository, InstalledRepositoryInterface, RootPackageRepository and PlatformRepository * * @author Jordi Boggiano */ class InstalledRepository extends CompositeRepository { /** * @param string $name * @param ConstraintInterface|string|null $constraint * * @return BasePackage[] */ public function findPackagesWithReplacersAndProviders($name, $constraint = null) { $name = strtolower($name); if (null !== $constraint && !$constraint instanceof ConstraintInterface) { $versionParser = new VersionParser(); $constraint = $versionParser->parseConstraints($constraint); } $matches = array(); foreach ($this->getRepositories() as $repo) { foreach ($repo->getPackages() as $candidate) { if ($name === $candidate->getName()) { if (null === $constraint || $constraint->matches(new Constraint('==', $candidate->getVersion()))) { $matches[] = $candidate; } continue; } foreach (array_merge($candidate->getProvides(), $candidate->getReplaces()) as $link) { if ( $name === $link->getTarget() && ($constraint === null || $constraint->matches($link->getConstraint())) ) { $matches[] = $candidate; continue 2; } } } } return $matches; } /** * Returns a list of links causing the requested needle packages to be installed, as an associative array with the * dependent's name as key, and an array containing in order the PackageInterface and Link describing the relationship * as values. If recursive lookup was requested a third value is returned containing an identically formed array up * to the root package. That third value will be false in case a circular recursion was detected. * * @param string|string[] $needle The package name(s) to inspect. * @param ConstraintInterface|null $constraint Optional constraint to filter by. * @param bool $invert Whether to invert matches to discover reasons for the package *NOT* to be installed. * @param bool $recurse Whether to recursively expand the requirement tree up to the root package. * @param string[] $packagesFound Used internally when recurring * * @return array[] An associative array of arrays as described above. * @phpstan-return array */ public function getDependents($needle, $constraint = null, $invert = false, $recurse = true, $packagesFound = null) { $needles = array_map('strtolower', (array) $needle); $results = array(); // initialize the array with the needles before any recursion occurs if (null === $packagesFound) { $packagesFound = $needles; } // locate root package for use below $rootPackage = null; foreach ($this->getPackages() as $package) { if ($package instanceof RootPackageInterface) { $rootPackage = $package; break; } } // Loop over all currently installed packages. foreach ($this->getPackages() as $package) { $links = $package->getRequires(); // each loop needs its own "tree" as we want to show the complete dependent set of every needle // without warning all the time about finding circular deps $packagesInTree = $packagesFound; // Replacements are considered valid reasons for a package to be installed during forward resolution if (!$invert) { $links += $package->getReplaces(); // On forward search, check if any replaced package was required and add the replaced // packages to the list of needles. Contrary to the cross-reference link check below, // replaced packages are the target of links. foreach ($package->getReplaces() as $link) { foreach ($needles as $needle) { if ($link->getSource() === $needle) { if ($constraint === null || ($link->getConstraint()->matches($constraint) === true)) { // already displayed this node's dependencies, cutting short if (in_array($link->getTarget(), $packagesInTree)) { $results[] = array($package, $link, false); continue; } $packagesInTree[] = $link->getTarget(); $dependents = $recurse ? $this->getDependents($link->getTarget(), null, false, true, $packagesInTree) : array(); $results[] = array($package, $link, $dependents); $needles[] = $link->getTarget(); } } } } } // Require-dev is only relevant for the root package if ($package instanceof RootPackageInterface) { $links += $package->getDevRequires(); } // Cross-reference all discovered links to the needles foreach ($links as $link) { foreach ($needles as $needle) { if ($link->getTarget() === $needle) { if ($constraint === null || ($link->getConstraint()->matches($constraint) === !$invert)) { // already displayed this node's dependencies, cutting short if (in_array($link->getSource(), $packagesInTree)) { $results[] = array($package, $link, false); continue; } $packagesInTree[] = $link->getSource(); $dependents = $recurse ? $this->getDependents($link->getSource(), null, false, true, $packagesInTree) : array(); $results[] = array($package, $link, $dependents); } } } } // When inverting, we need to check for conflicts of the needles against installed packages if ($invert && in_array($package->getName(), $needles)) { foreach ($package->getConflicts() as $link) { foreach ($this->findPackages($link->getTarget()) as $pkg) { $version = new Constraint('=', $pkg->getVersion()); if ($link->getConstraint()->matches($version) === $invert) { $results[] = array($package, $link, false); } } } } // List conflicts against X as they may explain why the current version was selected, or explain why it is rejected if the conflict matched when inverting foreach ($package->getConflicts() as $link) { if (in_array($link->getTarget(), $needles)) { foreach ($this->findPackages($link->getTarget()) as $pkg) { $version = new Constraint('=', $pkg->getVersion()); if ($link->getConstraint()->matches($version) === $invert) { $results[] = array($package, $link, false); } } } } // When inverting, we need to check for conflicts of the needles' requirements against installed packages if ($invert && $constraint && in_array($package->getName(), $needles) && $constraint->matches(new Constraint('=', $package->getVersion()))) { foreach ($package->getRequires() as $link) { if (PlatformRepository::isPlatformPackage($link->getTarget())) { if ($this->findPackage($link->getTarget(), $link->getConstraint())) { continue; } $platformPkg = $this->findPackage($link->getTarget(), '*'); $description = $platformPkg ? 'but '.$platformPkg->getPrettyVersion().' is installed' : 'but it is missing'; $results[] = array($package, new Link($package->getName(), $link->getTarget(), new MatchAllConstraint, Link::TYPE_REQUIRE, $link->getPrettyConstraint().' '.$description), false); continue; } foreach ($this->getPackages() as $pkg) { if (!in_array($link->getTarget(), $pkg->getNames())) { continue; } $version = new Constraint('=', $pkg->getVersion()); if ($link->getTarget() !== $pkg->getName()) { foreach (array_merge($pkg->getReplaces(), $pkg->getProvides()) as $prov) { if ($link->getTarget() === $prov->getTarget()) { $version = $prov->getConstraint(); break; } } } if (!$link->getConstraint()->matches($version)) { // if we have a root package (we should but can not guarantee..) we show // the root requires as well to perhaps allow to find an issue there if ($rootPackage) { foreach (array_merge($rootPackage->getRequires(), $rootPackage->getDevRequires()) as $rootReq) { if (in_array($rootReq->getTarget(), $pkg->getNames()) && !$rootReq->getConstraint()->matches($link->getConstraint())) { $results[] = array($package, $link, false); $results[] = array($rootPackage, $rootReq, false); continue 3; } } $results[] = array($package, $link, false); $results[] = array($rootPackage, new Link($rootPackage->getName(), $link->getTarget(), new MatchAllConstraint, Link::TYPE_DOES_NOT_REQUIRE, 'but ' . $pkg->getPrettyVersion() . ' is installed'), false); } else { // no root so let's just print whatever we found $results[] = array($package, $link, false); } } continue 2; } } } } ksort($results); return $results; } public function getRepoName() { return 'installed repo ('.implode(', ', array_map(function ($repo) { return $repo->getRepoName(); }, $this->getRepositories())).')'; } /** * @inheritDoc */ public function addRepository(RepositoryInterface $repository) { if ( $repository instanceof LockArrayRepository || $repository instanceof InstalledRepositoryInterface || $repository instanceof RootPackageRepository || $repository instanceof PlatformRepository ) { parent::addRepository($repository); return; } throw new \LogicException('An InstalledRepository can not contain a repository of type '.get_class($repository).' ('.$repository->getRepoName().')'); } } * Jordi Boggiano * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Composer\Repository; /** * Configurable repository interface. * * @author Lukas Homza */ interface ConfigurableRepositoryInterface { /** * @return mixed[] */ public function getRepoConfig(); } * Jordi Boggiano * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Composer\Repository; use Composer\Package\PackageInterface; use Composer\Package\BasePackage; use Composer\Pcre\Preg; /** * Filters which packages are seen as canonical on this repo by loadPackages * * @author Jordi Boggiano */ class FilterRepository implements RepositoryInterface { /** @var ?string */ private $only = null; /** @var ?string */ private $exclude = null; /** @var bool */ private $canonical = true; /** @var RepositoryInterface */ private $repo; /** * @param array{only?: array, exclude?: array, canonical?: bool} $options */ public function __construct(RepositoryInterface $repo, array $options) { if (isset($options['only'])) { if (!is_array($options['only'])) { throw new \InvalidArgumentException('"only" key for repository '.$repo->getRepoName().' should be an array'); } $this->only = BasePackage::packageNamesToRegexp($options['only']); } if (isset($options['exclude'])) { if (!is_array($options['exclude'])) { throw new \InvalidArgumentException('"exclude" key for repository '.$repo->getRepoName().' should be an array'); } $this->exclude = BasePackage::packageNamesToRegexp($options['exclude']); } if ($this->exclude && $this->only) { throw new \InvalidArgumentException('Only one of "only" and "exclude" can be specified for repository '.$repo->getRepoName()); } if (isset($options['canonical'])) { if (!is_bool($options['canonical'])) { throw new \InvalidArgumentException('"canonical" key for repository '.$repo->getRepoName().' should be a boolean'); } $this->canonical = $options['canonical']; } $this->repo = $repo; } public function getRepoName() { return $this->repo->getRepoName(); } /** * Returns the wrapped repositories * * @return RepositoryInterface */ public function getRepository() { return $this->repo; } /** * @inheritDoc */ public function hasPackage(PackageInterface $package) { return $this->repo->hasPackage($package); } /** * @inheritDoc */ public function findPackage($name, $constraint) { if (!$this->isAllowed($name)) { return null; } return $this->repo->findPackage($name, $constraint); } /** * @inheritDoc */ public function findPackages($name, $constraint = null) { if (!$this->isAllowed($name)) { return array(); } return $this->repo->findPackages($name, $constraint); } /** * @inheritDoc */ public function loadPackages(array $packageMap, array $acceptableStabilities, array $stabilityFlags, array $alreadyLoaded = array()) { foreach ($packageMap as $name => $constraint) { if (!$this->isAllowed($name)) { unset($packageMap[$name]); } } if (!$packageMap) { return array('namesFound' => array(), 'packages' => array()); } $result = $this->repo->loadPackages($packageMap, $acceptableStabilities, $stabilityFlags, $alreadyLoaded); if (!$this->canonical) { $result['namesFound'] = array(); } return $result; } /** * @inheritDoc */ public function search($query, $mode = 0, $type = null) { $result = array(); foreach ($this->repo->search($query, $mode, $type) as $package) { if ($this->isAllowed($package['name'])) { $result[] = $package; } } return $result; } /** * @inheritDoc */ public function getPackages() { $result = array(); foreach ($this->repo->getPackages() as $package) { if ($this->isAllowed($package->getName())) { $result[] = $package; } } return $result; } /** * @inheritDoc */ public function getProviders($packageName) { $result = array(); foreach ($this->repo->getProviders($packageName) as $name => $provider) { if ($this->isAllowed($provider['name'])) { $result[$name] = $provider; } } return $result; } /** * @inheritDoc */ #[\ReturnTypeWillChange] public function count() { if ($this->repo->count() > 0) { return count($this->getPackages()); } return 0; } /** * @param string $name * * @return bool */ private function isAllowed($name) { if (!$this->only && !$this->exclude) { return true; } if ($this->only) { return Preg::isMatch($this->only, $name); } return !Preg::isMatch($this->exclude, $name); } } * Jordi Boggiano * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Composer\Repository; use Composer\IO\IOInterface; use Composer\Json\JsonFile; use Composer\Package\BasePackage; use Composer\Package\Loader\ArrayLoader; use Composer\Package\Loader\LoaderInterface; use Composer\Util\Tar; use Composer\Util\Zip; /** * @author Serge Smertin */ class ArtifactRepository extends ArrayRepository implements ConfigurableRepositoryInterface { /** @var LoaderInterface */ protected $loader; /** @var string */ protected $lookup; /** @var array{url: string} */ protected $repoConfig; /** @var IOInterface */ private $io; /** * @param array{url: string} $repoConfig */ public function __construct(array $repoConfig, IOInterface $io) { parent::__construct(); if (!extension_loaded('zip')) { throw new \RuntimeException('The artifact repository requires PHP\'s zip extension'); } $this->loader = new ArrayLoader(); $this->lookup = $repoConfig['url']; $this->io = $io; $this->repoConfig = $repoConfig; } public function getRepoName() { return 'artifact repo ('.$this->lookup.')'; } public function getRepoConfig() { return $this->repoConfig; } protected function initialize() { parent::initialize(); $this->scanDirectory($this->lookup); } /** * @param string $path * * @return void */ private function scanDirectory($path) { $io = $this->io; $directory = new \RecursiveDirectoryIterator($path, \RecursiveDirectoryIterator::FOLLOW_SYMLINKS); $iterator = new \RecursiveIteratorIterator($directory); $regex = new \RegexIterator($iterator, '/^.+\.(zip|tar|gz|tgz)$/i'); foreach ($regex as $file) { /* @var $file \SplFileInfo */ if (!$file->isFile()) { continue; } $package = $this->getComposerInformation($file); if (!$package) { $io->writeError("File {$file->getBasename()} doesn't seem to hold a package", true, IOInterface::VERBOSE); continue; } $template = 'Found package %s (%s) in file %s'; $io->writeError(sprintf($template, $package->getName(), $package->getPrettyVersion(), $file->getBasename()), true, IOInterface::VERBOSE); $this->addPackage($package); } } /** * @return ?BasePackage */ private function getComposerInformation(\SplFileInfo $file) { $json = null; $fileType = null; $fileExtension = pathinfo($file->getPathname(), PATHINFO_EXTENSION); if (in_array($fileExtension, array('gz', 'tar', 'tgz'), true)) { $fileType = 'tar'; } elseif ($fileExtension === 'zip') { $fileType = 'zip'; } else { throw new \RuntimeException('Files with "'.$fileExtension.'" extensions aren\'t supported. Only ZIP and TAR/TAR.GZ/TGZ archives are supported.'); } try { if ($fileType === 'tar') { $json = Tar::getComposerJson($file->getPathname()); } else { $json = Zip::getComposerJson($file->getPathname()); } } catch (\Exception $exception) { $this->io->write('Failed loading package '.$file->getPathname().': '.$exception->getMessage(), false, IOInterface::VERBOSE); } if (null === $json) { return null; } $package = JsonFile::parseJson($json, $file->getPathname().'#composer.json'); $package['dist'] = array( 'type' => $fileType, 'url' => strtr($file->getPathname(), '\\', '/'), 'shasum' => sha1_file($file->getRealPath()), ); try { $package = $this->loader->load($package); } catch (\UnexpectedValueException $e) { throw new \UnexpectedValueException('Failed loading package in '.$file.': '.$e->getMessage(), 0, $e); } return $package; } } * Jordi Boggiano * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Composer\Repository; /** * Exception thrown when a package repository is utterly broken * * @author Jordi Boggiano */ class InvalidRepositoryException extends \Exception { } * Jordi Boggiano * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Composer\Repository; use Composer\Package\AliasPackage; use Composer\Installer\InstallationManager; /** * Writable array repository. * * @author Jordi Boggiano */ class WritableArrayRepository extends ArrayRepository implements WritableRepositoryInterface { /** * @var string[] */ protected $devPackageNames = array(); /** * @inheritDoc */ public function setDevPackageNames(array $devPackageNames) { $this->devPackageNames = $devPackageNames; } /** * @inheritDoc */ public function getDevPackageNames() { return $this->devPackageNames; } /** * @inheritDoc */ public function write($devMode, InstallationManager $installationManager) { } /** * @inheritDoc */ public function reload() { } /** * @inheritDoc */ public function getCanonicalPackages() { $packages = $this->getPackages(); // get at most one package of each name, preferring non-aliased ones $packagesByName = array(); foreach ($packages as $package) { if (!isset($packagesByName[$package->getName()]) || $packagesByName[$package->getName()] instanceof AliasPackage) { $packagesByName[$package->getName()] = $package; } } $canonicalPackages = array(); // unfold aliased packages foreach ($packagesByName as $package) { while ($package instanceof AliasPackage) { $package = $package->getAliasOf(); } $canonicalPackages[] = $package; } return $canonicalPackages; } } * Jordi Boggiano * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Composer\Repository; use Composer\Package\PackageInterface; use Composer\Installer\InstallationManager; /** * Writable repository interface. * * @author Konstantin Kudryashov */ interface WritableRepositoryInterface extends RepositoryInterface { /** * Writes repository (f.e. to the disc). * * @param bool $devMode Whether dev requirements were included or not in this installation * @return void */ public function write($devMode, InstallationManager $installationManager); /** * Adds package to the repository. * * @param PackageInterface $package package instance * @return void */ public function addPackage(PackageInterface $package); /** * Removes package from the repository. * * @param PackageInterface $package package instance * @return void */ public function removePackage(PackageInterface $package); /** * Get unique packages (at most one package of each name), with aliases resolved and removed. * * @return PackageInterface[] */ public function getCanonicalPackages(); /** * Forces a reload of all packages. * * @return void */ public function reload(); /** * @param string[] $devPackageNames * @return void */ public function setDevPackageNames(array $devPackageNames); /** * @return string[] Names of dependencies installed through require-dev */ public function getDevPackageNames(); } * Jordi Boggiano * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Composer\Repository; interface VersionCacheInterface { /** * @param string $version * @param string $identifier * @return mixed[]|null|false Package version data if found, false to indicate the identifier is known but has no package, null for an unknown identifier */ public function getVersionPackage($version, $identifier); } * Jordi Boggiano * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Composer\Repository; /** * Thrown when a security problem, like a broken or missing signature * * @author Eric Daspet */ class RepositorySecurityException extends \Exception { } * Jordi Boggiano * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Composer\Repository; use Composer\Package\PackageInterface; use Composer\Package\BasePackage; use Composer\Semver\Constraint\ConstraintInterface; /** * Repository interface. * * @author Nils Adermann * @author Konstantin Kudryashov * @author Jordi Boggiano */ interface RepositoryInterface extends \Countable { const SEARCH_FULLTEXT = 0; const SEARCH_NAME = 1; const SEARCH_VENDOR = 2; /** * Checks if specified package registered (installed). * * @param PackageInterface $package package instance * * @return bool */ public function hasPackage(PackageInterface $package); /** * Searches for the first match of a package by name and version. * * @param string $name package name * @param string|ConstraintInterface $constraint package version or version constraint to match against * * @return BasePackage|null */ public function findPackage($name, $constraint); /** * Searches for all packages matching a name and optionally a version. * * @param string $name package name * @param string|ConstraintInterface $constraint package version or version constraint to match against * * @return BasePackage[] */ public function findPackages($name, $constraint = null); /** * Returns list of registered packages. * * @return BasePackage[] */ public function getPackages(); /** * Returns list of registered packages with the supplied name * * - The packages returned are the packages found which match the constraints, acceptable stability and stability flags provided * - The namesFound returned are names which should be considered as canonically found in this repository, that should not be looked up in any further lower priority repositories * * @param ConstraintInterface[] $packageNameMap package names pointing to constraints * @param array $acceptableStabilities array of stability => BasePackage::STABILITY_* value * @param array $stabilityFlags an array of package name => BasePackage::STABILITY_* value * @param array> $alreadyLoaded an array of package name => package version => package * * @return array * * @phpstan-param array $packageNameMap * @phpstan-return array{namesFound: array, packages: array} */ public function loadPackages(array $packageNameMap, array $acceptableStabilities, array $stabilityFlags, array $alreadyLoaded = array()); /** * Searches the repository for packages containing the query * * @param string $query search query, for SEARCH_NAME and SEARCH_VENDOR regular expressions metacharacters are supported by implementations, and user input should be escaped through preg_quote by callers * @param int $mode a set of SEARCH_* constants to search on, implementations should do a best effort only, default is SEARCH_FULLTEXT * @param string $type The type of package to search for. Defaults to all types of packages * * @return array[] an array of array('name' => '...', 'description' => '...'|null, 'abandoned' => 'string'|true|unset) For SEARCH_VENDOR the name will be in "vendor" form * @phpstan-return list */ public function search($query, $mode = 0, $type = null); /** * Returns a list of packages providing a given package name * * Packages which have the same name as $packageName should not be returned, only those that have a "provide" on it. * * @param string $packageName package name which must be provided * * @return array[] an array with the provider name as key and value of array('name' => '...', 'description' => '...', 'type' => '...') * @phpstan-return array */ public function getProviders($packageName); /** * Returns a name representing this repository to the user * * This is best effort and definitely can not always be very precise * * @return string */ public function getRepoName(); } * Jordi Boggiano * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Composer\Repository; use Composer\Config; use Composer\IO\IOInterface; use Composer\Json\JsonFile; use Composer\Package\CompleteAliasPackage; use Composer\Package\CompletePackage; use Composer\Package\Loader\ArrayLoader; use Composer\Package\Version\VersionGuesser; use Composer\Package\Version\VersionParser; use Composer\Pcre\Preg; use Composer\Util\Platform; use Composer\Util\ProcessExecutor; use Composer\Util\Filesystem; use Composer\Util\Url; use Composer\Util\Git as GitUtil; /** * This repository allows installing local packages that are not necessarily under their own VCS. * * The local packages will be symlinked when possible, else they will be copied. * * @code * "require": { * "/": "*" * }, * "repositories": [ * { * "type": "path", * "url": "../../relative/path/to/package/" * }, * { * "type": "path", * "url": "/absolute/path/to/package/" * }, * { * "type": "path", * "url": "/absolute/path/to/several/packages/*" * }, * { * "type": "path", * "url": "../../relative/path/to/package/", * "options": { * "symlink": false * } * }, * ] * @endcode * * @author Samuel Roze * @author Johann Reinke */ class PathRepository extends ArrayRepository implements ConfigurableRepositoryInterface { /** * @var ArrayLoader */ private $loader; /** * @var VersionGuesser */ private $versionGuesser; /** * @var string */ private $url; /** * @var mixed[] * @phpstan-var array{url: string, options?: array{symlink?: bool, relative?: bool, versions?: array}} */ private $repoConfig; /** * @var ProcessExecutor */ private $process; /** * @var array{symlink?: bool, relative?: bool, versions?: array} */ private $options; /** * Initializes path repository. * * @param array{url?: string, options?: array{symlink?: bool, relative?: bool, versions?: array}} $repoConfig * @param IOInterface $io * @param Config $config */ public function __construct(array $repoConfig, IOInterface $io, Config $config) { if (!isset($repoConfig['url'])) { throw new \RuntimeException('You must specify the `url` configuration for the path repository'); } $this->loader = new ArrayLoader(null, true); $this->url = Platform::expandPath($repoConfig['url']); $this->process = new ProcessExecutor($io); $this->versionGuesser = new VersionGuesser($config, $this->process, new VersionParser()); $this->repoConfig = $repoConfig; $this->options = isset($repoConfig['options']) ? $repoConfig['options'] : array(); if (!isset($this->options['relative'])) { $filesystem = new Filesystem(); $this->options['relative'] = !$filesystem->isAbsolutePath($this->url); } parent::__construct(); } public function getRepoName() { return 'path repo ('.Url::sanitize($this->repoConfig['url']).')'; } public function getRepoConfig() { return $this->repoConfig; } /** * Initializes path repository. * * This method will basically read the folder and add the found package. */ protected function initialize() { parent::initialize(); $urlMatches = $this->getUrlMatches(); if (empty($urlMatches)) { if (Preg::isMatch('{[*{}]}', $this->url)) { $url = $this->url; while (Preg::isMatch('{[*{}]}', $url)) { $url = dirname($url); } // the parent directory before any wildcard exists, so we assume it is correctly configured but simply empty if (is_dir($url)) { return; } } throw new \RuntimeException('The `url` supplied for the path (' . $this->url . ') repository does not exist'); } foreach ($urlMatches as $url) { $path = realpath($url) . DIRECTORY_SEPARATOR; $composerFilePath = $path.'composer.json'; if (!file_exists($composerFilePath)) { continue; } $json = file_get_contents($composerFilePath); $package = JsonFile::parseJson($json, $composerFilePath); $package['dist'] = array( 'type' => 'path', 'url' => $url, 'reference' => sha1($json . serialize($this->options)), ); $package['transport-options'] = $this->options; unset($package['transport-options']['versions']); // use the version provided as option if available if (isset($package['name'], $this->options['versions'][$package['name']])) { $package['version'] = $this->options['versions'][$package['name']]; } // carry over the root package version if this path repo is in the same git repository as root package if (!isset($package['version']) && ($rootVersion = Platform::getEnv('COMPOSER_ROOT_VERSION'))) { if ( 0 === $this->process->execute('git rev-parse HEAD', $ref1, $path) && 0 === $this->process->execute('git rev-parse HEAD', $ref2) && $ref1 === $ref2 ) { $package['version'] = $rootVersion; } } $output = ''; if (is_dir($path . DIRECTORY_SEPARATOR . '.git') && 0 === $this->process->execute('git log -n1 --pretty=%H'.GitUtil::getNoShowSignatureFlag($this->process), $output, $path)) { $package['dist']['reference'] = trim($output); } $needsAlias = false; if (!isset($package['version'])) { $versionData = $this->versionGuesser->guessVersion($package, $path); if (is_array($versionData) && $versionData['pretty_version']) { // if there is a feature branch detected, we add a second packages with the feature branch version if (!empty($versionData['feature_pretty_version'])) { $package['version'] = $versionData['feature_pretty_version']; $this->addPackage($this->loader->load($package)); } $package['version'] = $versionData['pretty_version']; } else { $package['version'] = 'dev-main'; $needsAlias = true; } } $package = $this->loader->load($package); if ($needsAlias && $package instanceof CompletePackage) { // keep a dev-master alias to dev-main for BC $package = new CompleteAliasPackage($package, 'dev-master', 'dev-master'); } $this->addPackage($package); } } /** * Get a list of all (possibly relative) path names matching given url (supports globbing). * * @return string[] */ private function getUrlMatches() { $flags = GLOB_MARK | GLOB_ONLYDIR; if (defined('GLOB_BRACE')) { $flags |= GLOB_BRACE; } elseif (strpos($this->url, '{') !== false || strpos($this->url, '}') !== false) { throw new \RuntimeException('The operating system does not support GLOB_BRACE which is required for the url '. $this->url); } // Ensure environment-specific path separators are normalized to URL separators return array_map(function ($val) { return rtrim(str_replace(DIRECTORY_SEPARATOR, '/', $val), '/'); }, glob($this->url, $flags)); } } * Jordi Boggiano * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Composer\Repository; /** * Installed filesystem repository. * * @author Jordi Boggiano */ class InstalledFilesystemRepository extends FilesystemRepository implements InstalledRepositoryInterface { public function getRepoName() { return 'installed '.parent::getRepoName(); } /** * @inheritDoc */ public function isFresh() { return !$this->file->exists(); } } * Jordi Boggiano * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Composer\Repository; use Composer\Factory; use Composer\IO\IOInterface; use Composer\Config; use Composer\EventDispatcher\EventDispatcher; use Composer\Pcre\Preg; use Composer\Util\HttpDownloader; use Composer\Util\ProcessExecutor; use Composer\Json\JsonFile; /** * @author Jordi Boggiano */ class RepositoryFactory { /** * @param IOInterface $io * @param Config $config * @param string $repository * @param bool $allowFilesystem * @return array|mixed */ public static function configFromString(IOInterface $io, Config $config, $repository, $allowFilesystem = false) { if (0 === strpos($repository, 'http')) { $repoConfig = array('type' => 'composer', 'url' => $repository); } elseif ("json" === pathinfo($repository, PATHINFO_EXTENSION)) { $json = new JsonFile($repository, Factory::createHttpDownloader($io, $config)); $data = $json->read(); if (!empty($data['packages']) || !empty($data['includes']) || !empty($data['provider-includes'])) { $repoConfig = array('type' => 'composer', 'url' => 'file://' . strtr(realpath($repository), '\\', '/')); } elseif ($allowFilesystem) { $repoConfig = array('type' => 'filesystem', 'json' => $json); } else { throw new \InvalidArgumentException("Invalid repository URL ($repository) given. This file does not contain a valid composer repository."); } } elseif (strpos($repository, '{') === 0) { // assume it is a json object that makes a repo config $repoConfig = JsonFile::parseJson($repository); } else { throw new \InvalidArgumentException("Invalid repository url ($repository) given. Has to be a .json file, an http url or a JSON object."); } return $repoConfig; } /** * @param IOInterface $io * @param Config $config * @param string $repository * @param bool $allowFilesystem * @return RepositoryInterface */ public static function fromString(IOInterface $io, Config $config, $repository, $allowFilesystem = false, RepositoryManager $rm = null) { $repoConfig = static::configFromString($io, $config, $repository, $allowFilesystem); return static::createRepo($io, $config, $repoConfig, $rm); } /** * @param IOInterface $io * @param Config $config * @param array $repoConfig * @return RepositoryInterface */ public static function createRepo(IOInterface $io, Config $config, array $repoConfig, RepositoryManager $rm = null) { if (!$rm) { $rm = static::manager($io, $config, Factory::createHttpDownloader($io, $config)); } $repos = self::createRepos($rm, array($repoConfig)); return reset($repos); } /** * @param IOInterface|null $io * @param Config|null $config * @param RepositoryManager|null $rm * @return RepositoryInterface[] */ public static function defaultRepos(IOInterface $io = null, Config $config = null, RepositoryManager $rm = null) { if (!$config) { $config = Factory::createConfig($io); } if ($io) { $io->loadConfiguration($config); } if (!$rm) { if (!$io) { throw new \InvalidArgumentException('This function requires either an IOInterface or a RepositoryManager'); } $rm = static::manager($io, $config, Factory::createHttpDownloader($io, $config)); } return self::createRepos($rm, $config->getRepositories()); } /** * @param IOInterface $io * @param Config $config * @param EventDispatcher $eventDispatcher * @param HttpDownloader $httpDownloader * @return RepositoryManager */ public static function manager(IOInterface $io, Config $config, HttpDownloader $httpDownloader, EventDispatcher $eventDispatcher = null, ProcessExecutor $process = null) { $rm = new RepositoryManager($io, $config, $httpDownloader, $eventDispatcher, $process); $rm->setRepositoryClass('composer', 'Composer\Repository\ComposerRepository'); $rm->setRepositoryClass('vcs', 'Composer\Repository\VcsRepository'); $rm->setRepositoryClass('package', 'Composer\Repository\PackageRepository'); $rm->setRepositoryClass('pear', 'Composer\Repository\PearRepository'); $rm->setRepositoryClass('git', 'Composer\Repository\VcsRepository'); $rm->setRepositoryClass('bitbucket', 'Composer\Repository\VcsRepository'); $rm->setRepositoryClass('git-bitbucket', 'Composer\Repository\VcsRepository'); $rm->setRepositoryClass('github', 'Composer\Repository\VcsRepository'); $rm->setRepositoryClass('gitlab', 'Composer\Repository\VcsRepository'); $rm->setRepositoryClass('svn', 'Composer\Repository\VcsRepository'); $rm->setRepositoryClass('fossil', 'Composer\Repository\VcsRepository'); $rm->setRepositoryClass('perforce', 'Composer\Repository\VcsRepository'); $rm->setRepositoryClass('hg', 'Composer\Repository\VcsRepository'); $rm->setRepositoryClass('artifact', 'Composer\Repository\ArtifactRepository'); $rm->setRepositoryClass('path', 'Composer\Repository\PathRepository'); return $rm; } /** * @param array $repoConfigs * * @return RepositoryInterface[] */ private static function createRepos(RepositoryManager $rm, array $repoConfigs) { $repos = array(); foreach ($repoConfigs as $index => $repo) { if (is_string($repo)) { throw new \UnexpectedValueException('"repositories" should be an array of repository definitions, only a single repository was given'); } if (!is_array($repo)) { throw new \UnexpectedValueException('Repository "'.$index.'" ('.json_encode($repo).') should be an array, '.gettype($repo).' given'); } if (!isset($repo['type'])) { throw new \UnexpectedValueException('Repository "'.$index.'" ('.json_encode($repo).') must have a type defined'); } $name = self::generateRepositoryName($index, $repo, $repos); if ($repo['type'] === 'filesystem') { $repos[$name] = new FilesystemRepository($repo['json']); } else { $repos[$name] = $rm->createRepository($repo['type'], $repo, $index); } } return $repos; } /** * @param int|string $index * @param array{url?: string} $repo * @param array $existingRepos * * @return string */ public static function generateRepositoryName($index, array $repo, array $existingRepos) { $name = is_int($index) && isset($repo['url']) ? Preg::replace('{^https?://}i', '', $repo['url']) : $index; while (isset($existingRepos[$name])) { $name .= '2'; } return $name; } } * Jordi Boggiano * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Composer\Repository; /** * Builds list of package from PEAR channel. * * Packages read from channel are named as 'pear-{channelName}/{packageName}' * and has aliased as 'pear-{channelAlias}/{packageName}' * * @author Benjamin Eberlei * @author Jordi Boggiano * @deprecated * @private */ class PearRepository extends ArrayRepository { public function __construct() { throw new \InvalidArgumentException('The PEAR repository has been removed from Composer 2.x'); } } * Jordi Boggiano * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Composer\Repository; use Composer\Package\Loader\ArrayLoader; use Composer\Package\Loader\ValidatingArrayLoader; use Composer\Pcre\Preg; /** * Package repository. * * @author Jordi Boggiano */ class PackageRepository extends ArrayRepository { /** @var mixed[] */ private $config; /** * Initializes filesystem repository. * * @param array{package: mixed[]} $config package definition */ public function __construct(array $config) { parent::__construct(); $this->config = $config['package']; // make sure we have an array of package definitions if (!is_numeric(key($this->config))) { $this->config = array($this->config); } } /** * Initializes repository (reads file, or remote address). */ protected function initialize() { parent::initialize(); $loader = new ValidatingArrayLoader(new ArrayLoader(null, true), true); foreach ($this->config as $package) { try { $package = $loader->load($package); } catch (\Exception $e) { throw new InvalidRepositoryException('A repository of type "package" contains an invalid package definition: '.$e->getMessage()."\n\nInvalid package definition:\n".json_encode($package)); } $this->addPackage($package); } } public function getRepoName() { return Preg::replace('{^array }', 'package ', parent::getRepoName()); } } * Jordi Boggiano * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Composer\Repository; use Composer\Package\PackageInterface; /** * Composite repository. * * @author Beau Simensen */ class CompositeRepository implements RepositoryInterface { /** * List of repositories * @var RepositoryInterface[] */ private $repositories; /** * Constructor * @param RepositoryInterface[] $repositories */ public function __construct(array $repositories) { $this->repositories = array(); foreach ($repositories as $repo) { $this->addRepository($repo); } } public function getRepoName() { return 'composite repo ('.implode(', ', array_map(function ($repo) { return $repo->getRepoName(); }, $this->repositories)).')'; } /** * Returns all the wrapped repositories * * @return RepositoryInterface[] */ public function getRepositories() { return $this->repositories; } /** * @inheritDoc */ public function hasPackage(PackageInterface $package) { foreach ($this->repositories as $repository) { /* @var $repository RepositoryInterface */ if ($repository->hasPackage($package)) { return true; } } return false; } /** * @inheritDoc */ public function findPackage($name, $constraint) { foreach ($this->repositories as $repository) { /* @var $repository RepositoryInterface */ $package = $repository->findPackage($name, $constraint); if (null !== $package) { return $package; } } return null; } /** * @inheritDoc */ public function findPackages($name, $constraint = null) { $packages = array(); foreach ($this->repositories as $repository) { /* @var $repository RepositoryInterface */ $packages[] = $repository->findPackages($name, $constraint); } return $packages ? call_user_func_array('array_merge', $packages) : array(); } /** * @inheritDoc */ public function loadPackages(array $packageMap, array $acceptableStabilities, array $stabilityFlags, array $alreadyLoaded = array()) { $packages = array(); $namesFound = array(); foreach ($this->repositories as $repository) { /* @var $repository RepositoryInterface */ $result = $repository->loadPackages($packageMap, $acceptableStabilities, $stabilityFlags, $alreadyLoaded); $packages[] = $result['packages']; $namesFound[] = $result['namesFound']; } return array( 'packages' => $packages ? call_user_func_array('array_merge', $packages) : array(), 'namesFound' => $namesFound ? array_unique(call_user_func_array('array_merge', $namesFound)) : array(), ); } /** * @inheritDoc */ public function search($query, $mode = 0, $type = null) { $matches = array(); foreach ($this->repositories as $repository) { /* @var $repository RepositoryInterface */ $matches[] = $repository->search($query, $mode, $type); } return $matches ? call_user_func_array('array_merge', $matches) : array(); } /** * @inheritDoc */ public function getPackages() { $packages = array(); foreach ($this->repositories as $repository) { /* @var $repository RepositoryInterface */ $packages[] = $repository->getPackages(); } return $packages ? call_user_func_array('array_merge', $packages) : array(); } /** * @inheritDoc */ public function getProviders($packageName) { $results = array(); foreach ($this->repositories as $repository) { /* @var $repository RepositoryInterface */ $results[] = $repository->getProviders($packageName); } return $results ? call_user_func_array('array_merge', $results) : array(); } /** * @return void */ public function removePackage(PackageInterface $package) { foreach ($this->repositories as $repository) { if ($repository instanceof WritableRepositoryInterface) { $repository->removePackage($package); } } } /** * @inheritDoc */ #[\ReturnTypeWillChange] public function count() { $total = 0; foreach ($this->repositories as $repository) { /* @var $repository RepositoryInterface */ $total += $repository->count(); } return $total; } /** * Add a repository. * @param RepositoryInterface $repository * * @return void */ public function addRepository(RepositoryInterface $repository) { if ($repository instanceof self) { foreach ($repository->getRepositories() as $repo) { $this->addRepository($repo); } } else { $this->repositories[] = $repository; } } } * Jordi Boggiano * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Composer\Repository; /** * Installable repository interface. * * Just used to tag installed repositories so the base classes can act differently on Alias packages * * @author Jordi Boggiano */ interface InstalledRepositoryInterface extends WritableRepositoryInterface { /** * @return bool true if packages were never installed in this repository */ public function isFresh(); } * Jordi Boggiano * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Composer\Repository; use Composer\Json\JsonFile; use Composer\Package\Loader\ArrayLoader; use Composer\Package\RootPackageInterface; use Composer\Package\AliasPackage; use Composer\Package\Dumper\ArrayDumper; use Composer\Installer\InstallationManager; use Composer\Util\Filesystem; /** * Filesystem repository. * * @author Konstantin Kudryashov * @author Jordi Boggiano */ class FilesystemRepository extends WritableArrayRepository { /** @var JsonFile */ protected $file; /** @var bool */ private $dumpVersions; /** @var ?RootPackageInterface */ private $rootPackage; /** @var Filesystem */ private $filesystem; /** * Initializes filesystem repository. * * @param JsonFile $repositoryFile repository json file * @param bool $dumpVersions * @param ?RootPackageInterface $rootPackage Must be provided if $dumpVersions is true */ public function __construct(JsonFile $repositoryFile, $dumpVersions = false, RootPackageInterface $rootPackage = null, Filesystem $filesystem = null) { parent::__construct(); $this->file = $repositoryFile; $this->dumpVersions = $dumpVersions; $this->rootPackage = $rootPackage; $this->filesystem = $filesystem ?: new Filesystem; if ($dumpVersions && !$rootPackage) { throw new \InvalidArgumentException('Expected a root package instance if $dumpVersions is true'); } } /** * Initializes repository (reads file, or remote address). */ protected function initialize() { parent::initialize(); if (!$this->file->exists()) { return; } try { $data = $this->file->read(); if (isset($data['packages'])) { $packages = $data['packages']; } else { $packages = $data; } if (isset($data['dev-package-names'])) { $this->setDevPackageNames($data['dev-package-names']); } if (!is_array($packages)) { throw new \UnexpectedValueException('Could not parse package list from the repository'); } } catch (\Exception $e) { throw new InvalidRepositoryException('Invalid repository data in '.$this->file->getPath().', packages could not be loaded: ['.get_class($e).'] '.$e->getMessage()); } $loader = new ArrayLoader(null, true); foreach ($packages as $packageData) { $package = $loader->load($packageData); $this->addPackage($package); } } public function reload() { $this->packages = null; $this->initialize(); } /** * Writes writable repository. */ public function write($devMode, InstallationManager $installationManager) { $data = array('packages' => array(), 'dev' => $devMode, 'dev-package-names' => array()); $dumper = new ArrayDumper(); // make sure the directory is created so we can realpath it // as realpath() does some additional normalizations with network paths that normalizePath does not // and we need to find shortest path correctly $repoDir = dirname($this->file->getPath()); $this->filesystem->ensureDirectoryExists($repoDir); $repoDir = $this->filesystem->normalizePath(realpath($repoDir)); $installPaths = array(); foreach ($this->getCanonicalPackages() as $package) { $pkgArray = $dumper->dump($package); $path = $installationManager->getInstallPath($package); $installPath = null; if ('' !== $path && null !== $path) { $normalizedPath = $this->filesystem->normalizePath($this->filesystem->isAbsolutePath($path) ? $path : getcwd() . '/' . $path); $installPath = $this->filesystem->findShortestPath($repoDir, $normalizedPath, true); } $installPaths[$package->getName()] = $installPath; $pkgArray['install-path'] = $installPath; $data['packages'][] = $pkgArray; // only write to the files the names which are really installed, as we receive the full list // of dev package names before they get installed during composer install if (in_array($package->getName(), $this->devPackageNames, true)) { $data['dev-package-names'][] = $package->getName(); } } sort($data['dev-package-names']); usort($data['packages'], function ($a, $b) { return strcmp($a['name'], $b['name']); }); $this->file->write($data); if ($this->dumpVersions) { $versions = $this->generateInstalledVersions($installationManager, $installPaths, $devMode, $repoDir); $this->filesystem->filePutContentsIfModified($repoDir.'/installed.php', 'dumpToPhpCode($versions) . ';'."\n"); $installedVersionsClass = file_get_contents(__DIR__.'/../InstalledVersions.php'); $this->filesystem->filePutContentsIfModified($repoDir.'/InstalledVersions.php', $installedVersionsClass); \Composer\InstalledVersions::reload($versions); } } /** * @param array $array * @param int $level * * @return string */ private function dumpToPhpCode(array $array = array(), $level = 0) { $lines = "array(\n"; $level++; foreach ($array as $key => $value) { $lines .= str_repeat(' ', $level); $lines .= is_int($key) ? $key . ' => ' : '\'' . $key . '\' => '; if (is_array($value)) { if (!empty($value)) { $lines .= $this->dumpToPhpCode($value, $level); } else { $lines .= "array(),\n"; } } elseif ($key === 'install_path' && is_string($value)) { if ($this->filesystem->isAbsolutePath($value)) { $lines .= var_export($value, true) . ",\n"; } else { $lines .= "__DIR__ . " . var_export('/' . $value, true) . ",\n"; } } else { $lines .= var_export($value, true) . ",\n"; } } $lines .= str_repeat(' ', $level - 1) . ')' . ($level - 1 == 0 ? '' : ",\n"); return $lines; } /** * @param array $installPaths * @param bool $devMode * @param string $repoDir * * @return ?array */ private function generateInstalledVersions(InstallationManager $installationManager, array $installPaths, $devMode, $repoDir) { if (!$this->dumpVersions) { return null; } $devPackages = array_flip($this->devPackageNames); $versions = array('versions' => array()); $packages = $this->getPackages(); $packages[] = $rootPackage = $this->rootPackage; while ($rootPackage instanceof AliasPackage) { $rootPackage = $rootPackage->getAliasOf(); $packages[] = $rootPackage; } // add real installed packages foreach ($packages as $package) { if ($package instanceof AliasPackage) { continue; } $reference = null; if ($package->getInstallationSource()) { $reference = $package->getInstallationSource() === 'source' ? $package->getSourceReference() : $package->getDistReference(); } if (null === $reference) { $reference = ($package->getSourceReference() ?: $package->getDistReference()) ?: null; } if ($package instanceof RootPackageInterface) { $to = $this->filesystem->normalizePath(realpath(getcwd())); $installPath = $this->filesystem->findShortestPath($repoDir, $to, true); } else { $installPath = $installPaths[$package->getName()]; } $versions['versions'][$package->getName()] = array( 'pretty_version' => $package->getPrettyVersion(), 'version' => $package->getVersion(), 'type' => $package->getType(), 'install_path' => $installPath, 'aliases' => array(), 'reference' => $reference, 'dev_requirement' => isset($devPackages[$package->getName()]), ); if ($package instanceof RootPackageInterface) { $versions['root'] = $versions['versions'][$package->getName()]; unset($versions['root']['dev_requirement']); $versions['root']['name'] = $package->getName(); $versions['root']['dev'] = $devMode; } } // add provided/replaced packages foreach ($packages as $package) { $isDevPackage = isset($devPackages[$package->getName()]); foreach ($package->getReplaces() as $replace) { // exclude platform replaces as when they are really there we can not check for their presence if (PlatformRepository::isPlatformPackage($replace->getTarget())) { continue; } if (!isset($versions['versions'][$replace->getTarget()]['dev_requirement'])) { $versions['versions'][$replace->getTarget()]['dev_requirement'] = $isDevPackage; } elseif (!$isDevPackage) { $versions['versions'][$replace->getTarget()]['dev_requirement'] = false; } $replaced = $replace->getPrettyConstraint(); if ($replaced === 'self.version') { $replaced = $package->getPrettyVersion(); } if (!isset($versions['versions'][$replace->getTarget()]['replaced']) || !in_array($replaced, $versions['versions'][$replace->getTarget()]['replaced'], true)) { $versions['versions'][$replace->getTarget()]['replaced'][] = $replaced; } } foreach ($package->getProvides() as $provide) { // exclude platform provides as when they are really there we can not check for their presence if (PlatformRepository::isPlatformPackage($provide->getTarget())) { continue; } if (!isset($versions['versions'][$provide->getTarget()]['dev_requirement'])) { $versions['versions'][$provide->getTarget()]['dev_requirement'] = $isDevPackage; } elseif (!$isDevPackage) { $versions['versions'][$provide->getTarget()]['dev_requirement'] = false; } $provided = $provide->getPrettyConstraint(); if ($provided === 'self.version') { $provided = $package->getPrettyVersion(); } if (!isset($versions['versions'][$provide->getTarget()]['provided']) || !in_array($provided, $versions['versions'][$provide->getTarget()]['provided'], true)) { $versions['versions'][$provide->getTarget()]['provided'][] = $provided; } } } // add aliases foreach ($packages as $package) { if (!$package instanceof AliasPackage) { continue; } $versions['versions'][$package->getName()]['aliases'][] = $package->getPrettyVersion(); if ($package instanceof RootPackageInterface) { $versions['root']['aliases'][] = $package->getPrettyVersion(); } } ksort($versions['versions']); ksort($versions); return $versions; } } * Jordi Boggiano * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Composer\Repository\Vcs; use Composer\Config; use Composer\Downloader\TransportException; use Composer\Json\JsonFile; use Composer\Cache; use Composer\IO\IOInterface; use Composer\Pcre\Preg; use Composer\Util\GitHub; use Composer\Util\Http\Response; /** * @author Jordi Boggiano */ class GitHubDriver extends VcsDriver { /** @var string */ protected $owner; /** @var string */ protected $repository; /** @var array Map of tag name to identifier */ protected $tags; /** @var array Map of branch name to identifier */ protected $branches; /** @var string */ protected $rootIdentifier; /** @var mixed[] */ protected $repoData; /** @var bool */ protected $hasIssues = false; /** @var bool */ protected $isPrivate = false; /** @var bool */ private $isArchived = false; /** @var array|false|null */ private $fundingInfo; /** * Git Driver * * @var ?GitDriver */ protected $gitDriver = null; /** * @inheritDoc */ public function initialize() { Preg::match('#^(?:(?:https?|git)://([^/]+)/|git@([^:]+):/?)([^/]+)/(.+?)(?:\.git|/)?$#', $this->url, $match); $this->owner = $match[3]; $this->repository = $match[4]; $this->originUrl = strtolower(!empty($match[1]) ? $match[1] : $match[2]); if ($this->originUrl === 'www.github.com') { $this->originUrl = 'github.com'; } $this->cache = new Cache($this->io, $this->config->get('cache-repo-dir').'/'.$this->originUrl.'/'.$this->owner.'/'.$this->repository); $this->cache->setReadOnly($this->config->get('cache-read-only')); if ($this->config->get('use-github-api') === false || (isset($this->repoConfig['no-api']) && $this->repoConfig['no-api'])) { $this->setupGitDriver($this->url); return; } $this->fetchRootIdentifier(); } /** * @return string */ public function getRepositoryUrl() { return 'https://'.$this->originUrl.'/'.$this->owner.'/'.$this->repository; } /** * @inheritDoc */ public function getRootIdentifier() { if ($this->gitDriver) { return $this->gitDriver->getRootIdentifier(); } return $this->rootIdentifier; } /** * @inheritDoc */ public function getUrl() { if ($this->gitDriver) { return $this->gitDriver->getUrl(); } return 'https://' . $this->originUrl . '/'.$this->owner.'/'.$this->repository.'.git'; } /** * @return string */ protected function getApiUrl() { if ('github.com' === $this->originUrl) { $apiUrl = 'api.github.com'; } else { $apiUrl = $this->originUrl . '/api/v3'; } return 'https://' . $apiUrl; } /** * @inheritDoc */ public function getSource($identifier) { if ($this->gitDriver) { return $this->gitDriver->getSource($identifier); } if ($this->isPrivate) { // Private GitHub repositories should be accessed using the // SSH version of the URL. $url = $this->generateSshUrl(); } else { $url = $this->getUrl(); } return array('type' => 'git', 'url' => $url, 'reference' => $identifier); } /** * @inheritDoc */ public function getDist($identifier) { $url = $this->getApiUrl() . '/repos/'.$this->owner.'/'.$this->repository.'/zipball/'.$identifier; return array('type' => 'zip', 'url' => $url, 'reference' => $identifier, 'shasum' => ''); } /** * @inheritDoc */ public function getComposerInformation($identifier) { if ($this->gitDriver) { return $this->gitDriver->getComposerInformation($identifier); } if (!isset($this->infoCache[$identifier])) { if ($this->shouldCache($identifier) && $res = $this->cache->read($identifier)) { $composer = JsonFile::parseJson($res); } else { $composer = $this->getBaseComposerInformation($identifier); if ($this->shouldCache($identifier)) { $this->cache->write($identifier, json_encode($composer)); } } if ($composer) { // specials for github if (!isset($composer['support']['source'])) { $label = array_search($identifier, $this->getTags()) ?: array_search($identifier, $this->getBranches()) ?: $identifier; $composer['support']['source'] = sprintf('https://%s/%s/%s/tree/%s', $this->originUrl, $this->owner, $this->repository, $label); } if (!isset($composer['support']['issues']) && $this->hasIssues) { $composer['support']['issues'] = sprintf('https://%s/%s/%s/issues', $this->originUrl, $this->owner, $this->repository); } if (!isset($composer['abandoned']) && $this->isArchived) { $composer['abandoned'] = true; } if (!isset($composer['funding']) && $funding = $this->getFundingInfo()) { $composer['funding'] = $funding; } } $this->infoCache[$identifier] = $composer; } return $this->infoCache[$identifier]; } /** * @return array|false */ private function getFundingInfo() { if (null !== $this->fundingInfo) { return $this->fundingInfo; } if ($this->originUrl !== 'github.com') { return $this->fundingInfo = false; } foreach (array($this->getApiUrl() . '/repos/'.$this->owner.'/'.$this->repository.'/contents/.github/FUNDING.yml', $this->getApiUrl() . '/repos/'.$this->owner.'/.github/contents/FUNDING.yml') as $file) { try { $response = $this->httpDownloader->get($file, array( 'retry-auth-failure' => false, ))->decodeJson(); } catch (TransportException $e) { continue; } if (empty($response['content']) || $response['encoding'] !== 'base64' || !($funding = base64_decode($response['content']))) { continue; } break; } if (empty($funding)) { return $this->fundingInfo = false; } $result = array(); $key = null; foreach (Preg::split('{\r?\n}', $funding) as $line) { $line = trim($line); if (Preg::isMatch('{^(\w+)\s*:\s*(.+)$}', $line, $match)) { if (Preg::isMatch('{^\[(.*)\](?:\s*#.*)?$}', $match[2], $match2)) { foreach (array_map('trim', Preg::split('{[\'"]?\s*,\s*[\'"]?}', $match2[1])) as $item) { $result[] = array('type' => $match[1], 'url' => trim($item, '"\' ')); } } elseif (Preg::isMatch('{^([^#].*?)(\s+#.*)?$}', $match[2], $match2)) { $result[] = array('type' => $match[1], 'url' => trim($match2[1], '"\' ')); } $key = null; } elseif (Preg::isMatch('{^(\w+)\s*:\s*#\s*$}', $line, $match)) { $key = $match[1]; } elseif ($key && Preg::isMatch('{^-\s*(.+)(\s+#.*)?$}', $line, $match)) { $result[] = array('type' => $key, 'url' => trim($match[1], '"\' ')); } } foreach ($result as $key => $item) { switch ($item['type']) { case 'tidelift': $result[$key]['url'] = 'https://tidelift.com/funding/github/' . $item['url']; break; case 'github': $result[$key]['url'] = 'https://github.com/' . basename($item['url']); break; case 'patreon': $result[$key]['url'] = 'https://www.patreon.com/' . basename($item['url']); break; case 'otechie': $result[$key]['url'] = 'https://otechie.com/' . basename($item['url']); break; case 'open_collective': $result[$key]['url'] = 'https://opencollective.com/' . basename($item['url']); break; case 'liberapay': $result[$key]['url'] = 'https://liberapay.com/' . basename($item['url']); break; case 'ko_fi': $result[$key]['url'] = 'https://ko-fi.com/' . basename($item['url']); break; case 'issuehunt': $result[$key]['url'] = 'https://issuehunt.io/r/' . $item['url']; break; case 'community_bridge': $result[$key]['url'] = 'https://funding.communitybridge.org/projects/' . basename($item['url']); break; } } return $this->fundingInfo = $result; } /** * @inheritDoc */ public function getFileContent($file, $identifier) { if ($this->gitDriver) { return $this->gitDriver->getFileContent($file, $identifier); } $resource = $this->getApiUrl() . '/repos/'.$this->owner.'/'.$this->repository.'/contents/' . $file . '?ref='.urlencode($identifier); $resource = $this->getContents($resource)->decodeJson(); if (empty($resource['content']) || $resource['encoding'] !== 'base64' || !($content = base64_decode($resource['content']))) { throw new \RuntimeException('Could not retrieve ' . $file . ' for '.$identifier); } return $content; } /** * @inheritDoc */ public function getChangeDate($identifier) { if ($this->gitDriver) { return $this->gitDriver->getChangeDate($identifier); } $resource = $this->getApiUrl() . '/repos/'.$this->owner.'/'.$this->repository.'/commits/'.urlencode($identifier); $commit = $this->getContents($resource)->decodeJson(); return new \DateTime($commit['commit']['committer']['date']); } /** * @inheritDoc */ public function getTags() { if ($this->gitDriver) { return $this->gitDriver->getTags(); } if (null === $this->tags) { $tags = array(); $resource = $this->getApiUrl() . '/repos/'.$this->owner.'/'.$this->repository.'/tags?per_page=100'; do { $response = $this->getContents($resource); $tagsData = $response->decodeJson(); foreach ($tagsData as $tag) { $tags[$tag['name']] = $tag['commit']['sha']; } $resource = $this->getNextPage($response); } while ($resource); $this->tags = $tags; } return $this->tags; } /** * @inheritDoc */ public function getBranches() { if ($this->gitDriver) { return $this->gitDriver->getBranches(); } if (null === $this->branches) { $branches = array(); $resource = $this->getApiUrl() . '/repos/'.$this->owner.'/'.$this->repository.'/git/refs/heads?per_page=100'; do { $response = $this->getContents($resource); $branchData = $response->decodeJson(); foreach ($branchData as $branch) { $name = substr($branch['ref'], 11); if ($name !== 'gh-pages') { $branches[$name] = $branch['object']['sha']; } } $resource = $this->getNextPage($response); } while ($resource); $this->branches = $branches; } return $this->branches; } /** * @inheritDoc */ public static function supports(IOInterface $io, Config $config, $url, $deep = false) { if (!Preg::isMatch('#^((?:https?|git)://([^/]+)/|git@([^:]+):/?)([^/]+)/(.+?)(?:\.git|/)?$#', $url, $matches)) { return false; } $originUrl = !empty($matches[2]) ? $matches[2] : $matches[3]; if (!in_array(strtolower(Preg::replace('{^www\.}i', '', $originUrl)), $config->get('github-domains'))) { return false; } if (!extension_loaded('openssl')) { $io->writeError('Skipping GitHub driver for '.$url.' because the OpenSSL PHP extension is missing.', true, IOInterface::VERBOSE); return false; } return true; } /** * Gives back the loaded /repos// result * * @return mixed[]|null */ public function getRepoData() { $this->fetchRootIdentifier(); return $this->repoData; } /** * Generate an SSH URL * * @return string */ protected function generateSshUrl() { if (false !== strpos($this->originUrl, ':')) { return 'ssh://git@' . $this->originUrl . '/'.$this->owner.'/'.$this->repository.'.git'; } return 'git@' . $this->originUrl . ':'.$this->owner.'/'.$this->repository.'.git'; } /** * @inheritDoc * * @param bool $fetchingRepoData */ protected function getContents($url, $fetchingRepoData = false) { try { return parent::getContents($url); } catch (TransportException $e) { $gitHubUtil = new GitHub($this->io, $this->config, $this->process, $this->httpDownloader); switch ($e->getCode()) { case 401: case 404: // try to authorize only if we are fetching the main /repos/foo/bar data, otherwise it must be a real 404 if (!$fetchingRepoData) { throw $e; } if ($gitHubUtil->authorizeOAuth($this->originUrl)) { return parent::getContents($url); } if (!$this->io->isInteractive()) { $this->attemptCloneFallback(); return new Response(array('url' => 'dummy'), 200, array(), 'null'); } $scopesIssued = array(); $scopesNeeded = array(); if ($headers = $e->getHeaders()) { if ($scopes = Response::findHeaderValue($headers, 'X-OAuth-Scopes')) { $scopesIssued = explode(' ', $scopes); } if ($scopes = Response::findHeaderValue($headers, 'X-Accepted-OAuth-Scopes')) { $scopesNeeded = explode(' ', $scopes); } } $scopesFailed = array_diff($scopesNeeded, $scopesIssued); // non-authenticated requests get no scopesNeeded, so ask for credentials // authenticated requests which failed some scopes should ask for new credentials too if (!$headers || !count($scopesNeeded) || count($scopesFailed)) { $gitHubUtil->authorizeOAuthInteractively($this->originUrl, 'Your GitHub credentials are required to fetch private repository metadata ('.$this->url.')'); } return parent::getContents($url); case 403: if (!$this->io->hasAuthentication($this->originUrl) && $gitHubUtil->authorizeOAuth($this->originUrl)) { return parent::getContents($url); } if (!$this->io->isInteractive() && $fetchingRepoData) { $this->attemptCloneFallback(); return new Response(array('url' => 'dummy'), 200, array(), 'null'); } $rateLimited = $gitHubUtil->isRateLimited((array) $e->getHeaders()); if (!$this->io->hasAuthentication($this->originUrl)) { if (!$this->io->isInteractive()) { $this->io->writeError('GitHub API limit exhausted. Failed to get metadata for the '.$this->url.' repository, try running in interactive mode so that you can enter your GitHub credentials to increase the API limit'); throw $e; } $gitHubUtil->authorizeOAuthInteractively($this->originUrl, 'API limit exhausted. Enter your GitHub credentials to get a larger API limit ('.$this->url.')'); return parent::getContents($url); } if ($rateLimited) { $rateLimit = $gitHubUtil->getRateLimit($e->getHeaders()); $this->io->writeError(sprintf( 'GitHub API limit (%d calls/hr) is exhausted. You are already authorized so you have to wait until %s before doing more requests', $rateLimit['limit'], $rateLimit['reset'] )); } throw $e; default: throw $e; } } } /** * Fetch root identifier from GitHub * * @return void * @throws TransportException */ protected function fetchRootIdentifier() { if ($this->repoData) { return; } $repoDataUrl = $this->getApiUrl() . '/repos/'.$this->owner.'/'.$this->repository; try { $this->repoData = $this->getContents($repoDataUrl, true)->decodeJson(); } catch (TransportException $e) { if ($e->getCode() === 499) { $this->attemptCloneFallback(); } else { throw $e; } } if (null === $this->repoData && null !== $this->gitDriver) { return; } $this->owner = $this->repoData['owner']['login']; $this->repository = $this->repoData['name']; $this->isPrivate = !empty($this->repoData['private']); if (isset($this->repoData['default_branch'])) { $this->rootIdentifier = $this->repoData['default_branch']; } elseif (isset($this->repoData['master_branch'])) { $this->rootIdentifier = $this->repoData['master_branch']; } else { $this->rootIdentifier = 'master'; } $this->hasIssues = !empty($this->repoData['has_issues']); $this->isArchived = !empty($this->repoData['archived']); } /** * @phpstan-impure * * @return true * @throws \RuntimeException */ protected function attemptCloneFallback() { $this->isPrivate = true; try { // If this repository may be private (hard to say for sure, // GitHub returns 404 for private repositories) and we // cannot ask for authentication credentials (because we // are not interactive) then we fallback to GitDriver. $this->setupGitDriver($this->generateSshUrl()); return true; } catch (\RuntimeException $e) { $this->gitDriver = null; $this->io->writeError('Failed to clone the '.$this->generateSshUrl().' repository, try running in interactive mode so that you can enter your GitHub credentials'); throw $e; } } /** * @param string $url * * @return void */ protected function setupGitDriver($url) { $this->gitDriver = new GitDriver( array('url' => $url), $this->io, $this->config, $this->httpDownloader, $this->process ); $this->gitDriver->initialize(); } /** * @return string|null */ protected function getNextPage(Response $response) { $header = $response->getHeader('link'); if (!$header) { return null; } $links = explode(',', $header); foreach ($links as $link) { if (Preg::isMatch('{<(.+?)>; *rel="next"}', $link, $match)) { return $match[1]; } } return null; } } * Jordi Boggiano * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Composer\Repository\Vcs; use Composer\Config; use Composer\Cache; use Composer\IO\IOInterface; use Composer\Pcre\Preg; use Composer\Util\ProcessExecutor; use Composer\Util\Perforce; /** * @author Matt Whittom */ class PerforceDriver extends VcsDriver { /** @var string */ protected $depot; /** @var string */ protected $branch; /** @var ?Perforce */ protected $perforce = null; /** * @inheritDoc */ public function initialize() { $this->depot = $this->repoConfig['depot']; $this->branch = ''; if (!empty($this->repoConfig['branch'])) { $this->branch = $this->repoConfig['branch']; } $this->initPerforce($this->repoConfig); $this->perforce->p4Login(); $this->perforce->checkStream(); $this->perforce->writeP4ClientSpec(); $this->perforce->connectClient(); } /** * @param array $repoConfig * * @return void */ private function initPerforce($repoConfig) { if (!empty($this->perforce)) { return; } if (!Cache::isUsable((string) $this->config->get('cache-vcs-dir'))) { throw new \RuntimeException('PerforceDriver requires a usable cache directory, and it looks like you set it to be disabled'); } $repoDir = $this->config->get('cache-vcs-dir') . '/' . $this->depot; $this->perforce = Perforce::create($repoConfig, $this->getUrl(), $repoDir, $this->process, $this->io); } /** * @inheritDoc */ public function getFileContent($file, $identifier) { return $this->perforce->getFileContent($file, $identifier); } /** * @inheritDoc */ public function getChangeDate($identifier) { return null; } /** * @inheritDoc */ public function getRootIdentifier() { return $this->branch; } /** * @inheritDoc */ public function getBranches() { return $this->perforce->getBranches(); } /** * @inheritDoc */ public function getTags() { return $this->perforce->getTags(); } /** * @inheritDoc */ public function getDist($identifier) { return null; } /** * @inheritDoc */ public function getSource($identifier) { return array( 'type' => 'perforce', 'url' => $this->repoConfig['url'], 'reference' => $identifier, 'p4user' => $this->perforce->getUser(), ); } /** * @inheritDoc */ public function getUrl() { return $this->url; } /** * @inheritDoc */ public function hasComposerFile($identifier) { $composerInfo = $this->perforce->getComposerInformation('//' . $this->depot . '/' . $identifier); return !empty($composerInfo); } /** * @inheritDoc */ public function getContents($url) { throw new \BadMethodCallException('Not implemented/used in PerforceDriver'); } /** * @inheritDoc */ public static function supports(IOInterface $io, Config $config, $url, $deep = false) { if ($deep || Preg::isMatch('#\b(perforce|p4)\b#i', $url)) { return Perforce::checkServerExists($url, new ProcessExecutor($io)); } return false; } /** * @inheritDoc */ public function cleanup() { $this->perforce->cleanupClientSpec(); $this->perforce = null; } /** * @return string */ public function getDepot() { return $this->depot; } /** * @return string */ public function getBranch() { return $this->branch; } } * Jordi Boggiano * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Composer\Repository\Vcs; use Composer\Pcre\Preg; use Composer\Util\ProcessExecutor; use Composer\Util\Filesystem; use Composer\Util\Url; use Composer\Util\Git as GitUtil; use Composer\IO\IOInterface; use Composer\Cache; use Composer\Config; /** * @author Jordi Boggiano */ class GitDriver extends VcsDriver { /** @var array Map of tag name (can be turned to an int by php if it is a numeric name) to identifier */ protected $tags; /** @var array Map of branch name (can be turned to an int by php if it is a numeric name) to identifier */ protected $branches; /** @var string */ protected $rootIdentifier; /** @var string */ protected $repoDir; /** * @inheritDoc */ public function initialize() { if (Filesystem::isLocalPath($this->url)) { $this->url = Preg::replace('{[\\/]\.git/?$}', '', $this->url); if (!is_dir($this->url)) { throw new \RuntimeException('Failed to read package information from '.$this->url.' as the path does not exist'); } $this->repoDir = $this->url; $cacheUrl = realpath($this->url); } else { if (!Cache::isUsable((string) $this->config->get('cache-vcs-dir'))) { throw new \RuntimeException('GitDriver requires a usable cache directory, and it looks like you set it to be disabled'); } $this->repoDir = $this->config->get('cache-vcs-dir') . '/' . Preg::replace('{[^a-z0-9.]}i', '-', $this->url) . '/'; GitUtil::cleanEnv(); $fs = new Filesystem(); $fs->ensureDirectoryExists(dirname($this->repoDir)); if (!is_writable(dirname($this->repoDir))) { throw new \RuntimeException('Can not clone '.$this->url.' to access package information. The "'.dirname($this->repoDir).'" directory is not writable by the current user.'); } if (Preg::isMatch('{^ssh://[^@]+@[^:]+:[^0-9]+}', $this->url)) { throw new \InvalidArgumentException('The source URL '.$this->url.' is invalid, ssh URLs should have a port number after ":".'."\n".'Use ssh://git@example.com:22/path or just git@example.com:path if you do not want to provide a password or custom port.'); } $gitUtil = new GitUtil($this->io, $this->config, $this->process, $fs); if (!$gitUtil->syncMirror($this->url, $this->repoDir)) { if (!is_dir($this->repoDir)) { throw new \RuntimeException('Failed to clone '.$this->url.' to read package information from it'); } $this->io->writeError('Failed to update '.$this->url.', package information from this repository may be outdated'); } $cacheUrl = $this->url; } $this->getTags(); $this->getBranches(); $this->cache = new Cache($this->io, $this->config->get('cache-repo-dir').'/'.Preg::replace('{[^a-z0-9.]}i', '-', Url::sanitize($cacheUrl))); $this->cache->setReadOnly($this->config->get('cache-read-only')); } /** * @inheritDoc */ public function getRootIdentifier() { if (null === $this->rootIdentifier) { $this->rootIdentifier = 'master'; // select currently checked out branch if master is not available $this->process->execute('git branch --no-color', $output, $this->repoDir); $branches = $this->process->splitLines($output); if (!in_array('* master', $branches)) { foreach ($branches as $branch) { if ($branch && Preg::isMatch('{^\* +(\S+)}', $branch, $match)) { $this->rootIdentifier = $match[1]; break; } } } } return $this->rootIdentifier; } /** * @inheritDoc */ public function getUrl() { return $this->url; } /** * @inheritDoc */ public function getSource($identifier) { return array('type' => 'git', 'url' => $this->getUrl(), 'reference' => $identifier); } /** * @inheritDoc */ public function getDist($identifier) { return null; } /** * @inheritDoc */ public function getFileContent($file, $identifier) { $resource = sprintf('%s:%s', ProcessExecutor::escape($identifier), ProcessExecutor::escape($file)); $this->process->execute(sprintf('git show %s', $resource), $content, $this->repoDir); if (!trim($content)) { return null; } return $content; } /** * @inheritDoc */ public function getChangeDate($identifier) { $this->process->execute(sprintf( 'git -c log.showSignature=false log -1 --format=%%at %s', ProcessExecutor::escape($identifier) ), $output, $this->repoDir); return new \DateTime('@'.trim($output), new \DateTimeZone('UTC')); } /** * @inheritDoc */ public function getTags() { if (null === $this->tags) { $this->tags = array(); $this->process->execute('git show-ref --tags --dereference', $output, $this->repoDir); foreach ($output = $this->process->splitLines($output) as $tag) { if ($tag && Preg::isMatch('{^([a-f0-9]{40}) refs/tags/(\S+?)(\^\{\})?$}', $tag, $match)) { $this->tags[$match[2]] = (string) $match[1]; } } } return $this->tags; } /** * @inheritDoc */ public function getBranches() { if (null === $this->branches) { $branches = array(); $this->process->execute('git branch --no-color --no-abbrev -v', $output, $this->repoDir); foreach ($this->process->splitLines($output) as $branch) { if ($branch && !Preg::isMatch('{^ *[^/]+/HEAD }', $branch)) { if (Preg::isMatch('{^(?:\* )? *(\S+) *([a-f0-9]+)(?: .*)?$}', $branch, $match)) { $branches[$match[1]] = $match[2]; } } } $this->branches = $branches; } return $this->branches; } /** * @inheritDoc */ public static function supports(IOInterface $io, Config $config, $url, $deep = false) { if (Preg::isMatch('#(^git://|\.git/?$|git(?:olite)?@|//git\.|//github.com/)#i', $url)) { return true; } // local filesystem if (Filesystem::isLocalPath($url)) { $url = Filesystem::getPlatformPath($url); if (!is_dir($url)) { return false; } $process = new ProcessExecutor($io); // check whether there is a git repo in that path if ($process->execute('git tag', $output, $url) === 0) { return true; } } if (!$deep) { return false; } $gitUtil = new GitUtil($io, $config, new ProcessExecutor($io), new Filesystem()); GitUtil::cleanEnv(); try { $gitUtil->runCommand(function ($url) { return 'git ls-remote --heads -- ' . ProcessExecutor::escape($url); }, $url, sys_get_temp_dir()); } catch (\RuntimeException $e) { return false; } return true; } } * Jordi Boggiano * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Composer\Repository\Vcs; use Composer\Cache; use Composer\Config; use Composer\Json\JsonFile; use Composer\Pcre\Preg; use Composer\Util\ProcessExecutor; use Composer\Util\Filesystem; use Composer\Util\Url; use Composer\Util\Svn as SvnUtil; use Composer\IO\IOInterface; use Composer\Downloader\TransportException; /** * @author Jordi Boggiano * @author Till Klampaeckel */ class SvnDriver extends VcsDriver { /** @var string */ protected $baseUrl; /** @var array Map of tag name to identifier */ protected $tags; /** @var array Map of branch name to identifier */ protected $branches; /** @var ?string */ protected $rootIdentifier; /** @var string|false */ protected $trunkPath = 'trunk'; /** @var string */ protected $branchesPath = 'branches'; /** @var string */ protected $tagsPath = 'tags'; /** @var string */ protected $packagePath = ''; /** @var bool */ protected $cacheCredentials = true; /** * @var \Composer\Util\Svn */ private $util; /** * @inheritDoc */ public function initialize() { $this->url = $this->baseUrl = rtrim(self::normalizeUrl($this->url), '/'); SvnUtil::cleanEnv(); if (isset($this->repoConfig['trunk-path'])) { $this->trunkPath = $this->repoConfig['trunk-path']; } if (isset($this->repoConfig['branches-path'])) { $this->branchesPath = $this->repoConfig['branches-path']; } if (isset($this->repoConfig['tags-path'])) { $this->tagsPath = $this->repoConfig['tags-path']; } if (array_key_exists('svn-cache-credentials', $this->repoConfig)) { $this->cacheCredentials = (bool) $this->repoConfig['svn-cache-credentials']; } if (isset($this->repoConfig['package-path'])) { $this->packagePath = '/' . trim($this->repoConfig['package-path'], '/'); } if (false !== ($pos = strrpos($this->url, '/' . $this->trunkPath))) { $this->baseUrl = substr($this->url, 0, $pos); } $this->cache = new Cache($this->io, $this->config->get('cache-repo-dir').'/'.Preg::replace('{[^a-z0-9.]}i', '-', Url::sanitize($this->baseUrl))); $this->cache->setReadOnly($this->config->get('cache-read-only')); $this->getBranches(); $this->getTags(); } /** * @inheritDoc */ public function getRootIdentifier() { return $this->rootIdentifier ?: $this->trunkPath; } /** * @inheritDoc */ public function getUrl() { return $this->url; } /** * @inheritDoc */ public function getSource($identifier) { return array('type' => 'svn', 'url' => $this->baseUrl, 'reference' => $identifier); } /** * @inheritDoc */ public function getDist($identifier) { return null; } /** * @inheritDoc */ protected function shouldCache($identifier) { return $this->cache && Preg::isMatch('{@\d+$}', $identifier); } /** * @inheritDoc */ public function getComposerInformation($identifier) { if (!isset($this->infoCache[$identifier])) { if ($this->shouldCache($identifier) && $res = $this->cache->read($identifier.'.json')) { return $this->infoCache[$identifier] = JsonFile::parseJson($res); } try { $composer = $this->getBaseComposerInformation($identifier); } catch (TransportException $e) { $message = $e->getMessage(); if (stripos($message, 'path not found') === false && stripos($message, 'svn: warning: W160013') === false) { throw $e; } // remember a not-existent composer.json $composer = ''; } if ($this->shouldCache($identifier)) { $this->cache->write($identifier.'.json', json_encode($composer)); } $this->infoCache[$identifier] = $composer; } return $this->infoCache[$identifier]; } /** * @param string $file * @param string $identifier */ public function getFileContent($file, $identifier) { $identifier = '/' . trim($identifier, '/') . '/'; Preg::match('{^(.+?)(@\d+)?/$}', $identifier, $match); if (!empty($match[2])) { $path = $match[1]; $rev = $match[2]; } else { $path = $identifier; $rev = ''; } try { $resource = $path.$file; $output = $this->execute('svn cat', $this->baseUrl . $resource . $rev); if (!trim($output)) { return null; } } catch (\RuntimeException $e) { throw new TransportException($e->getMessage()); } return $output; } /** * @inheritDoc */ public function getChangeDate($identifier) { $identifier = '/' . trim($identifier, '/') . '/'; Preg::match('{^(.+?)(@\d+)?/$}', $identifier, $match); if (!empty($match[2])) { $path = $match[1]; $rev = $match[2]; } else { $path = $identifier; $rev = ''; } $output = $this->execute('svn info', $this->baseUrl . $path . $rev); foreach ($this->process->splitLines($output) as $line) { if ($line && Preg::isMatch('{^Last Changed Date: ([^(]+)}', $line, $match)) { return new \DateTime($match[1], new \DateTimeZone('UTC')); } } return null; } /** * @inheritDoc */ public function getTags() { if (null === $this->tags) { $tags = array(); if ($this->tagsPath !== false) { $output = $this->execute('svn ls --verbose', $this->baseUrl . '/' . $this->tagsPath); if ($output) { foreach ($this->process->splitLines($output) as $line) { $line = trim($line); if ($line && Preg::isMatch('{^\s*(\S+).*?(\S+)\s*$}', $line, $match)) { if (isset($match[1], $match[2]) && $match[2] !== './') { $tags[rtrim($match[2], '/')] = $this->buildIdentifier( '/' . $this->tagsPath . '/' . $match[2], $match[1] ); } } } } } $this->tags = $tags; } return $this->tags; } /** * @inheritDoc */ public function getBranches() { if (null === $this->branches) { $branches = array(); if (false === $this->trunkPath) { $trunkParent = $this->baseUrl . '/'; } else { $trunkParent = $this->baseUrl . '/' . $this->trunkPath; } $output = $this->execute('svn ls --verbose', $trunkParent); if ($output) { foreach ($this->process->splitLines($output) as $line) { $line = trim($line); if ($line && Preg::isMatch('{^\s*(\S+).*?(\S+)\s*$}', $line, $match)) { if (isset($match[1], $match[2]) && $match[2] === './') { $branches['trunk'] = $this->buildIdentifier( '/' . $this->trunkPath, $match[1] ); $this->rootIdentifier = $branches['trunk']; break; } } } } unset($output); if ($this->branchesPath !== false) { $output = $this->execute('svn ls --verbose', $this->baseUrl . '/' . $this->branchesPath); if ($output) { foreach ($this->process->splitLines(trim($output)) as $line) { $line = trim($line); if ($line && Preg::isMatch('{^\s*(\S+).*?(\S+)\s*$}', $line, $match)) { if (isset($match[1], $match[2]) && $match[2] !== './') { $branches[rtrim($match[2], '/')] = $this->buildIdentifier( '/' . $this->branchesPath . '/' . $match[2], $match[1] ); } } } } } $this->branches = $branches; } return $this->branches; } /** * @inheritDoc */ public static function supports(IOInterface $io, Config $config, $url, $deep = false) { $url = self::normalizeUrl($url); if (Preg::isMatch('#(^svn://|^svn\+ssh://|svn\.)#i', $url)) { return true; } // proceed with deep check for local urls since they are fast to process if (!$deep && !Filesystem::isLocalPath($url)) { return false; } $process = new ProcessExecutor($io); $exit = $process->execute( "svn info --non-interactive -- ".ProcessExecutor::escape($url), $ignoredOutput ); if ($exit === 0) { // This is definitely a Subversion repository. return true; } // Subversion client 1.7 and older if (false !== stripos($process->getErrorOutput(), 'authorization failed:')) { // This is likely a remote Subversion repository that requires // authentication. We will handle actual authentication later. return true; } // Subversion client 1.8 and newer if (false !== stripos($process->getErrorOutput(), 'Authentication failed')) { // This is likely a remote Subversion or newer repository that requires // authentication. We will handle actual authentication later. return true; } return false; } /** * An absolute path (leading '/') is converted to a file:// url. * * @param string $url * * @return string */ protected static function normalizeUrl($url) { $fs = new Filesystem(); if ($fs->isAbsolutePath($url)) { return 'file://' . strtr($url, '\\', '/'); } return $url; } /** * Execute an SVN command and try to fix up the process with credentials * if necessary. * * @param string $command The svn command to run. * @param string $url The SVN URL. * @throws \RuntimeException * @return string */ protected function execute($command, $url) { if (null === $this->util) { $this->util = new SvnUtil($this->baseUrl, $this->io, $this->config, $this->process); $this->util->setCacheCredentials($this->cacheCredentials); } try { return $this->util->execute($command, $url); } catch (\RuntimeException $e) { if (null === $this->util->binaryVersion()) { throw new \RuntimeException('Failed to load '.$this->url.', svn was not found, check that it is installed and in your PATH env.' . "\n\n" . $this->process->getErrorOutput()); } throw new \RuntimeException( 'Repository '.$this->url.' could not be processed, '.$e->getMessage() ); } } /** * Build the identifier respecting "package-path" config option * * @param string $baseDir The path to trunk/branch/tag * @param int $revision The revision mark to add to identifier * * @return string */ protected function buildIdentifier($baseDir, $revision) { return rtrim($baseDir, '/') . $this->packagePath . '/@' . $revision; } } * Jordi Boggiano * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Composer\Repository\Vcs; use Composer\Cache; use Composer\Downloader\TransportException; use Composer\Config; use Composer\IO\IOInterface; use Composer\Json\JsonFile; use Composer\Pcre\Preg; use Composer\Util\ProcessExecutor; use Composer\Util\HttpDownloader; use Composer\Util\Filesystem; use Composer\Util\Http\Response; /** * A driver implementation for driver with authentication interaction. * * @author François Pluchino */ abstract class VcsDriver implements VcsDriverInterface { /** @var string */ protected $url; /** @var string */ protected $originUrl; /** @var array */ protected $repoConfig; /** @var IOInterface */ protected $io; /** @var Config */ protected $config; /** @var ProcessExecutor */ protected $process; /** @var HttpDownloader */ protected $httpDownloader; /** @var array */ protected $infoCache = array(); /** @var ?Cache */ protected $cache; /** * Constructor. * * @param array{url: string}&array $repoConfig The repository configuration * @param IOInterface $io The IO instance * @param Config $config The composer configuration * @param HttpDownloader $httpDownloader Remote Filesystem, injectable for mocking * @param ProcessExecutor $process Process instance, injectable for mocking */ final public function __construct(array $repoConfig, IOInterface $io, Config $config, HttpDownloader $httpDownloader, ProcessExecutor $process) { if (Filesystem::isLocalPath($repoConfig['url'])) { $repoConfig['url'] = Filesystem::getPlatformPath($repoConfig['url']); } $this->url = $repoConfig['url']; $this->originUrl = $repoConfig['url']; $this->repoConfig = $repoConfig; $this->io = $io; $this->config = $config; $this->httpDownloader = $httpDownloader; $this->process = $process; } /** * Returns whether or not the given $identifier should be cached or not. * * @param string $identifier * @return bool */ protected function shouldCache($identifier) { return $this->cache && Preg::isMatch('{^[a-f0-9]{40}$}iD', $identifier); } /** * @inheritDoc */ public function getComposerInformation($identifier) { if (!isset($this->infoCache[$identifier])) { if ($this->shouldCache($identifier) && $res = $this->cache->read($identifier)) { return $this->infoCache[$identifier] = JsonFile::parseJson($res); } $composer = $this->getBaseComposerInformation($identifier); if ($this->shouldCache($identifier)) { $this->cache->write($identifier, JsonFile::encode($composer, 0)); } $this->infoCache[$identifier] = $composer; } return $this->infoCache[$identifier]; } /** * @param string $identifier * * @return array|null */ protected function getBaseComposerInformation($identifier) { $composerFileContent = $this->getFileContent('composer.json', $identifier); if (!$composerFileContent) { return null; } $composer = JsonFile::parseJson($composerFileContent, $identifier . ':composer.json'); if (empty($composer['time']) && $changeDate = $this->getChangeDate($identifier)) { $composer['time'] = $changeDate->format(DATE_RFC3339); } return $composer; } /** * @inheritDoc */ public function hasComposerFile($identifier) { try { return (bool) $this->getComposerInformation($identifier); } catch (TransportException $e) { } return false; } /** * Get the https or http protocol depending on SSL support. * * Call this only if you know that the server supports both. * * @return string The correct type of protocol */ protected function getScheme() { if (extension_loaded('openssl')) { return 'https'; } return 'http'; } /** * Get the remote content. * * @param string $url The URL of content * * @return Response * @throws TransportException */ protected function getContents($url) { $options = isset($this->repoConfig['options']) ? $this->repoConfig['options'] : array(); return $this->httpDownloader->get($url, $options); } /** * @inheritDoc */ public function cleanup() { return; } } * Jordi Boggiano * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Composer\Repository\Vcs; use Composer\Config; use Composer\IO\IOInterface; /** * @author Jordi Boggiano */ interface VcsDriverInterface { /** * Initializes the driver (git clone, svn checkout, fetch info etc) * * @return void */ public function initialize(); /** * Return the composer.json file information * * @param string $identifier Any identifier to a specific branch/tag/commit * @return mixed[] containing all infos from the composer.json file */ public function getComposerInformation($identifier); /** * Return the content of $file or null if the file does not exist. * * @param string $file * @param string $identifier * @return string|null */ public function getFileContent($file, $identifier); /** * Get the changedate for $identifier. * * @param string $identifier * @return \DateTime|null */ public function getChangeDate($identifier); /** * Return the root identifier (trunk, master, default/tip ..) * * @return string Identifier */ public function getRootIdentifier(); /** * Return list of branches in the repository * * @return array Branch names as keys, identifiers as values */ public function getBranches(); /** * Return list of tags in the repository * * @return array Tag names as keys, identifiers as values */ public function getTags(); /** * @param string $identifier Any identifier to a specific branch/tag/commit * * @return array{type: string, url: string, reference: string, shasum: string}|null */ public function getDist($identifier); /** * @param string $identifier Any identifier to a specific branch/tag/commit * * @return array{type: string, url: string, reference: string} */ public function getSource($identifier); /** * Return the URL of the repository * * @return string */ public function getUrl(); /** * Return true if the repository has a composer file for a given identifier, * false otherwise. * * @param string $identifier Any identifier to a specific branch/tag/commit * @return bool Whether the repository has a composer file for a given identifier. */ public function hasComposerFile($identifier); /** * Performs any cleanup necessary as the driver is not longer needed * * @return void */ public function cleanup(); /** * Checks if this driver can handle a given url * * @param IOInterface $io IO instance * @param Config $config current $config * @param string $url URL to validate/check * @param bool $deep unless true, only shallow checks (url matching typically) should be done * @return bool */ public static function supports(IOInterface $io, Config $config, $url, $deep = false); } * Jordi Boggiano * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Composer\Repository\Vcs; use Composer\Config; use Composer\Cache; use Composer\Pcre\Preg; use Composer\Util\Hg as HgUtils; use Composer\Util\ProcessExecutor; use Composer\Util\Filesystem; use Composer\IO\IOInterface; /** * @author Per Bernhardt */ class HgDriver extends VcsDriver { /** @var array Map of tag name to identifier */ protected $tags; /** @var array Map of branch name to identifier */ protected $branches; /** @var string */ protected $rootIdentifier; /** @var string */ protected $repoDir; /** * @inheritDoc */ public function initialize() { if (Filesystem::isLocalPath($this->url)) { $this->repoDir = $this->url; } else { if (!Cache::isUsable((string) $this->config->get('cache-vcs-dir'))) { throw new \RuntimeException('HgDriver requires a usable cache directory, and it looks like you set it to be disabled'); } $cacheDir = $this->config->get('cache-vcs-dir'); $this->repoDir = $cacheDir . '/' . Preg::replace('{[^a-z0-9]}i', '-', $this->url) . '/'; $fs = new Filesystem(); $fs->ensureDirectoryExists($cacheDir); if (!is_writable(dirname($this->repoDir))) { throw new \RuntimeException('Can not clone '.$this->url.' to access package information. The "'.$cacheDir.'" directory is not writable by the current user.'); } // Ensure we are allowed to use this URL by config $this->config->prohibitUrlByConfig($this->url, $this->io); $hgUtils = new HgUtils($this->io, $this->config, $this->process); // update the repo if it is a valid hg repository if (is_dir($this->repoDir) && 0 === $this->process->execute('hg summary', $output, $this->repoDir)) { if (0 !== $this->process->execute('hg pull', $output, $this->repoDir)) { $this->io->writeError('Failed to update '.$this->url.', package information from this repository may be outdated ('.$this->process->getErrorOutput().')'); } } else { // clean up directory and do a fresh clone into it $fs->removeDirectory($this->repoDir); $repoDir = $this->repoDir; $command = function ($url) use ($repoDir) { return sprintf('hg clone --noupdate -- %s %s', ProcessExecutor::escape($url), ProcessExecutor::escape($repoDir)); }; $hgUtils->runCommand($command, $this->url, null); } } $this->getTags(); $this->getBranches(); } /** * @inheritDoc */ public function getRootIdentifier() { if (null === $this->rootIdentifier) { $this->process->execute(sprintf('hg tip --template "{node}"'), $output, $this->repoDir); $output = $this->process->splitLines($output); $this->rootIdentifier = $output[0]; } return $this->rootIdentifier; } /** * @inheritDoc */ public function getUrl() { return $this->url; } /** * @inheritDoc */ public function getSource($identifier) { return array('type' => 'hg', 'url' => $this->getUrl(), 'reference' => $identifier); } /** * @inheritDoc */ public function getDist($identifier) { return null; } /** * @inheritDoc */ public function getFileContent($file, $identifier) { $resource = sprintf('hg cat -r %s %s', ProcessExecutor::escape($identifier), ProcessExecutor::escape($file)); $this->process->execute($resource, $content, $this->repoDir); if (!trim($content)) { return null; } return $content; } /** * @inheritDoc */ public function getChangeDate($identifier) { $this->process->execute( sprintf( 'hg log --template "{date|rfc3339date}" -r %s', ProcessExecutor::escape($identifier) ), $output, $this->repoDir ); return new \DateTime(trim($output), new \DateTimeZone('UTC')); } /** * @inheritDoc */ public function getTags() { if (null === $this->tags) { $tags = array(); $this->process->execute('hg tags', $output, $this->repoDir); foreach ($this->process->splitLines($output) as $tag) { if ($tag && Preg::isMatch('(^([^\s]+)\s+\d+:(.*)$)', $tag, $match)) { $tags[$match[1]] = $match[2]; } } unset($tags['tip']); $this->tags = $tags; } return $this->tags; } /** * @inheritDoc */ public function getBranches() { if (null === $this->branches) { $branches = array(); $bookmarks = array(); $this->process->execute('hg branches', $output, $this->repoDir); foreach ($this->process->splitLines($output) as $branch) { if ($branch && Preg::isMatch('(^([^\s]+)\s+\d+:([a-f0-9]+))', $branch, $match)) { $branches[$match[1]] = $match[2]; } } $this->process->execute('hg bookmarks', $output, $this->repoDir); foreach ($this->process->splitLines($output) as $branch) { if ($branch && Preg::isMatch('(^(?:[\s*]*)([^\s]+)\s+\d+:(.*)$)', $branch, $match)) { $bookmarks[$match[1]] = $match[2]; } } // Branches will have preference over bookmarks $this->branches = array_merge($bookmarks, $branches); } return $this->branches; } /** * @inheritDoc */ public static function supports(IOInterface $io, Config $config, $url, $deep = false) { if (Preg::isMatch('#(^(?:https?|ssh)://(?:[^@]+@)?bitbucket.org|https://(?:.*?)\.kilnhg.com)#i', $url)) { return true; } // local filesystem if (Filesystem::isLocalPath($url)) { $url = Filesystem::getPlatformPath($url); if (!is_dir($url)) { return false; } $process = new ProcessExecutor($io); // check whether there is a hg repo in that path if ($process->execute('hg summary', $output, $url) === 0) { return true; } } if (!$deep) { return false; } $process = new ProcessExecutor($io); $exit = $process->execute(sprintf('hg identify -- %s', ProcessExecutor::escape($url)), $ignored); return $exit === 0; } } * Jordi Boggiano * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Composer\Repository\Vcs; use Composer\Config; use Composer\Cache; use Composer\IO\IOInterface; use Composer\Json\JsonFile; use Composer\Downloader\TransportException; use Composer\Pcre\Preg; use Composer\Util\HttpDownloader; use Composer\Util\GitLab; use Composer\Util\Http\Response; /** * Driver for GitLab API, use the Git driver for local checkouts. * * @author Henrik Bjørnskov * @author Jérôme Tamarelle */ class GitLabDriver extends VcsDriver { /** * @var string * @phpstan-var 'https'|'http' */ private $scheme; /** @var string */ private $namespace; /** @var string */ private $repository; /** * @var mixed[] Project data returned by GitLab API */ private $project; /** * @var array Keeps commits returned by GitLab API as commit id => info */ private $commits = array(); /** @var array Map of tag name to identifier */ private $tags; /** @var array Map of branch name to identifier */ private $branches; /** * Git Driver * * @var ?GitDriver */ protected $gitDriver = null; /** * Protocol to force use of for repository URLs. * * @var string One of ssh, http */ protected $protocol; /** * Defaults to true unless we can make sure it is public * * @var bool defines whether the repo is private or not */ private $isPrivate = true; /** * @var bool true if the origin has a port number or a path component in it */ private $hasNonstandardOrigin = false; const URL_REGEX = '#^(?:(?Phttps?)://(?P.+?)(?::(?P[0-9]+))?/|git@(?P[^:]+):)(?P.+)/(?P[^/]+?)(?:\.git|/)?$#'; /** * Extracts information from the repository url. * * SSH urls use https by default. Set "secure-http": false on the repository config to use http instead. * * @inheritDoc */ public function initialize() { if (!Preg::isMatch(self::URL_REGEX, $this->url, $match)) { throw new \InvalidArgumentException('The URL provided is invalid. It must be the HTTP URL of a GitLab project.'); } $guessedDomain = !empty($match['domain']) ? $match['domain'] : $match['domain2']; $configuredDomains = $this->config->get('gitlab-domains'); $urlParts = explode('/', $match['parts']); $this->scheme = !empty($match['scheme']) ? $match['scheme'] : (isset($this->repoConfig['secure-http']) && $this->repoConfig['secure-http'] === false ? 'http' : 'https') ; $this->originUrl = self::determineOrigin($configuredDomains, $guessedDomain, $urlParts, $match['port']); if ($protocol = $this->config->get('gitlab-protocol')) { // https treated as a synonym for http. if (!in_array($protocol, array('git', 'http', 'https'))) { throw new \RuntimeException('gitlab-protocol must be one of git, http.'); } $this->protocol = $protocol === 'git' ? 'ssh' : 'http'; } if (false !== strpos($this->originUrl, ':') || false !== strpos($this->originUrl, '/')) { $this->hasNonstandardOrigin = true; } $this->namespace = implode('/', $urlParts); $this->repository = Preg::replace('#(\.git)$#', '', $match['repo']); $this->cache = new Cache($this->io, $this->config->get('cache-repo-dir').'/'.$this->originUrl.'/'.$this->namespace.'/'.$this->repository); $this->cache->setReadOnly($this->config->get('cache-read-only')); $this->fetchProject(); } /** * Updates the HttpDownloader instance. * Mainly useful for tests. * * @internal * * @return void */ public function setHttpDownloader(HttpDownloader $httpDownloader) { $this->httpDownloader = $httpDownloader; } /** * @inheritDoc */ public function getComposerInformation($identifier) { if ($this->gitDriver) { return $this->gitDriver->getComposerInformation($identifier); } if (!isset($this->infoCache[$identifier])) { if ($this->shouldCache($identifier) && $res = $this->cache->read($identifier)) { $composer = JsonFile::parseJson($res); } else { $composer = $this->getBaseComposerInformation($identifier); if ($this->shouldCache($identifier)) { $this->cache->write($identifier, json_encode($composer)); } } if ($composer) { // specials for gitlab (this data is only available if authentication is provided) if (!isset($composer['support']['source']) && isset($this->project['web_url'])) { $label = array_search($identifier, $this->getTags(), true) ?: array_search($identifier, $this->getBranches(), true) ?: $identifier; $composer['support']['source'] = sprintf('%s/-/tree/%s', $this->project['web_url'], $label); } if (!isset($composer['support']['issues']) && !empty($this->project['issues_enabled']) && isset($this->project['web_url'])) { $composer['support']['issues'] = sprintf('%s/-/issues', $this->project['web_url']); } if (!isset($composer['abandoned']) && !empty($this->project['archived'])) { $composer['abandoned'] = true; } } $this->infoCache[$identifier] = $composer; } return $this->infoCache[$identifier]; } /** * @inheritDoc */ public function getFileContent($file, $identifier) { if ($this->gitDriver) { return $this->gitDriver->getFileContent($file, $identifier); } // Convert the root identifier to a cacheable commit id if (!Preg::isMatch('{[a-f0-9]{40}}i', $identifier)) { $branches = $this->getBranches(); if (isset($branches[$identifier])) { $identifier = $branches[$identifier]; } } $resource = $this->getApiUrl().'/repository/files/'.$this->urlEncodeAll($file).'/raw?ref='.$identifier; try { $content = $this->getContents($resource)->getBody(); } catch (TransportException $e) { if ($e->getCode() !== 404) { throw $e; } return null; } return $content; } /** * @inheritDoc */ public function getChangeDate($identifier) { if ($this->gitDriver) { return $this->gitDriver->getChangeDate($identifier); } if (isset($this->commits[$identifier])) { return new \DateTime($this->commits[$identifier]['committed_date']); } return new \DateTime(); } /** * @return string */ public function getRepositoryUrl() { if ($this->protocol) { return $this->project["{$this->protocol}_url_to_repo"]; } return $this->isPrivate ? $this->project['ssh_url_to_repo'] : $this->project['http_url_to_repo']; } /** * @inheritDoc */ public function getUrl() { if ($this->gitDriver) { return $this->gitDriver->getUrl(); } return $this->project['web_url']; } /** * @inheritDoc */ public function getDist($identifier) { $url = $this->getApiUrl().'/repository/archive.zip?sha='.$identifier; return array('type' => 'zip', 'url' => $url, 'reference' => $identifier, 'shasum' => ''); } /** * @inheritDoc */ public function getSource($identifier) { if ($this->gitDriver) { return $this->gitDriver->getSource($identifier); } return array('type' => 'git', 'url' => $this->getRepositoryUrl(), 'reference' => $identifier); } /** * @inheritDoc */ public function getRootIdentifier() { if ($this->gitDriver) { return $this->gitDriver->getRootIdentifier(); } return $this->project['default_branch']; } /** * @inheritDoc */ public function getBranches() { if ($this->gitDriver) { return $this->gitDriver->getBranches(); } if (!$this->branches) { $this->branches = $this->getReferences('branches'); } return $this->branches; } /** * @inheritDoc */ public function getTags() { if ($this->gitDriver) { return $this->gitDriver->getTags(); } if (!$this->tags) { $this->tags = $this->getReferences('tags'); } return $this->tags; } /** * @return string Base URL for GitLab API v3 */ public function getApiUrl() { return $this->scheme.'://'.$this->originUrl.'/api/v4/projects/'.$this->urlEncodeAll($this->namespace).'%2F'.$this->urlEncodeAll($this->repository); } /** * Urlencode all non alphanumeric characters. rawurlencode() can not be used as it does not encode `.` * * @param string $string * @return string */ private function urlEncodeAll($string) { $encoded = ''; for ($i = 0; isset($string[$i]); $i++) { $character = $string[$i]; if (!ctype_alnum($character) && !in_array($character, array('-', '_'), true)) { $character = '%' . sprintf('%02X', ord($character)); } $encoded .= $character; } return $encoded; } /** * @param string $type * * @return string[] where keys are named references like tags or branches and the value a sha */ protected function getReferences($type) { $perPage = 100; $resource = $this->getApiUrl().'/repository/'.$type.'?per_page='.$perPage; $references = array(); do { $response = $this->getContents($resource); $data = $response->decodeJson(); foreach ($data as $datum) { $references[$datum['name']] = $datum['commit']['id']; // Keep the last commit date of a reference to avoid // unnecessary API call when retrieving the composer file. $this->commits[$datum['commit']['id']] = $datum['commit']; } if (count($data) >= $perPage) { $resource = $this->getNextPage($response); } else { $resource = false; } } while ($resource); return $references; } /** * @return void */ protected function fetchProject() { // we need to fetch the default branch from the api $resource = $this->getApiUrl(); $this->project = $this->getContents($resource, true)->decodeJson(); if (isset($this->project['visibility'])) { $this->isPrivate = $this->project['visibility'] !== 'public'; } else { // client is not authenticated, therefore repository has to be public $this->isPrivate = false; } } /** * @phpstan-impure * * @return true * @throws \RuntimeException */ protected function attemptCloneFallback() { if ($this->isPrivate === false) { $url = $this->generatePublicUrl(); } else { $url = $this->generateSshUrl(); } try { // If this repository may be private and we // cannot ask for authentication credentials (because we // are not interactive) then we fallback to GitDriver. $this->setupGitDriver($url); return true; } catch (\RuntimeException $e) { $this->gitDriver = null; $this->io->writeError('Failed to clone the '.$url.' repository, try running in interactive mode so that you can enter your credentials'); throw $e; } } /** * Generate an SSH URL * * @return string */ protected function generateSshUrl() { if ($this->hasNonstandardOrigin) { return 'ssh://git@'.$this->originUrl.'/'.$this->namespace.'/'.$this->repository.'.git'; } return 'git@' . $this->originUrl . ':'.$this->namespace.'/'.$this->repository.'.git'; } /** * @return string */ protected function generatePublicUrl() { return $this->scheme . '://' . $this->originUrl . '/'.$this->namespace.'/'.$this->repository.'.git'; } /** * @param string $url * * @return void */ protected function setupGitDriver($url) { $this->gitDriver = new GitDriver( array('url' => $url), $this->io, $this->config, $this->httpDownloader, $this->process ); $this->gitDriver->initialize(); } /** * @inheritDoc * * @param bool $fetchingRepoData */ protected function getContents($url, $fetchingRepoData = false) { try { $response = parent::getContents($url); if ($fetchingRepoData) { $json = $response->decodeJson(); // Accessing the API with a token with Guest (10) access will return // more data than unauthenticated access but no default_branch data // accessing files via the API will then also fail if (!isset($json['default_branch']) && isset($json['permissions'])) { $this->isPrivate = $json['visibility'] !== 'public'; $moreThanGuestAccess = false; // Check both access levels (e.g. project, group) // - value will be null if no access is set // - value will be array with key access_level if set foreach ($json['permissions'] as $permission) { if ($permission && $permission['access_level'] > 10) { $moreThanGuestAccess = true; } } if (!$moreThanGuestAccess) { $this->io->writeError('GitLab token with Guest only access detected'); $this->attemptCloneFallback(); return new Response(array('url' => 'dummy'), 200, array(), 'null'); } } // force auth as the unauthenticated version of the API is broken if (!isset($json['default_branch'])) { // GitLab allows you to disable the repository inside a project to use a project only for issues and wiki if (isset($json['repository_access_level']) && $json['repository_access_level'] === 'disabled') { throw new TransportException('The GitLab repository is disabled in the project', 400); } if (!empty($json['id'])) { $this->isPrivate = false; } throw new TransportException('GitLab API seems to not be authenticated as it did not return a default_branch', 401); } } return $response; } catch (TransportException $e) { $gitLabUtil = new GitLab($this->io, $this->config, $this->process, $this->httpDownloader); switch ($e->getCode()) { case 401: case 404: // try to authorize only if we are fetching the main /repos/foo/bar data, otherwise it must be a real 404 if (!$fetchingRepoData) { throw $e; } if ($gitLabUtil->authorizeOAuth($this->originUrl)) { return parent::getContents($url); } if (!$this->io->isInteractive()) { $this->attemptCloneFallback(); return new Response(array('url' => 'dummy'), 200, array(), 'null'); } $this->io->writeError('Failed to download ' . $this->namespace . '/' . $this->repository . ':' . $e->getMessage() . ''); $gitLabUtil->authorizeOAuthInteractively($this->scheme, $this->originUrl, 'Your credentials are required to fetch private repository metadata ('.$this->url.')'); return parent::getContents($url); case 403: if (!$this->io->hasAuthentication($this->originUrl) && $gitLabUtil->authorizeOAuth($this->originUrl)) { return parent::getContents($url); } if (!$this->io->isInteractive() && $fetchingRepoData) { $this->attemptCloneFallback(); return new Response(array('url' => 'dummy'), 200, array(), 'null'); } throw $e; default: throw $e; } } } /** * Uses the config `gitlab-domains` to see if the driver supports the url for the * repository given. * * @inheritDoc */ public static function supports(IOInterface $io, Config $config, $url, $deep = false) { if (!Preg::isMatch(self::URL_REGEX, $url, $match)) { return false; } $scheme = !empty($match['scheme']) ? $match['scheme'] : null; $guessedDomain = !empty($match['domain']) ? $match['domain'] : $match['domain2']; $urlParts = explode('/', $match['parts']); if (false === self::determineOrigin((array) $config->get('gitlab-domains'), $guessedDomain, $urlParts, $match['port'])) { return false; } if ('https' === $scheme && !extension_loaded('openssl')) { $io->writeError('Skipping GitLab driver for '.$url.' because the OpenSSL PHP extension is missing.', true, IOInterface::VERBOSE); return false; } return true; } /** * @return string|null */ protected function getNextPage(Response $response) { $header = $response->getHeader('link'); $links = explode(',', $header); foreach ($links as $link) { if (Preg::isMatch('{<(.+?)>; *rel="next"}', $link, $match)) { return $match[1]; } } return null; } /** * @param array $configuredDomains * @param string $guessedDomain * @param array $urlParts * @param string $portNumber * * @return string|false */ private static function determineOrigin(array $configuredDomains, $guessedDomain, array &$urlParts, $portNumber) { $guessedDomain = strtolower($guessedDomain); if (in_array($guessedDomain, $configuredDomains) || ($portNumber && in_array($guessedDomain.':'.$portNumber, $configuredDomains))) { if ($portNumber) { return $guessedDomain.':'.$portNumber; } return $guessedDomain; } if ($portNumber) { $guessedDomain .= ':'.$portNumber; } while (null !== ($part = array_shift($urlParts))) { $guessedDomain .= '/' . $part; if (in_array($guessedDomain, $configuredDomains) || ($portNumber && in_array(Preg::replace('{:\d+}', '', $guessedDomain), $configuredDomains))) { return $guessedDomain; } } return false; } } * Jordi Boggiano * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Composer\Repository\Vcs; use Composer\Config; use Composer\IO\IOInterface; use Composer\Cache; use Composer\Downloader\TransportException; use Composer\Json\JsonFile; use Composer\Pcre\Preg; use Composer\Util\Bitbucket; use Composer\Util\Http\Response; /** * @author Per Bernhardt */ class GitBitbucketDriver extends VcsDriver { /** @var string */ protected $owner; /** @var string */ protected $repository; /** @var bool */ private $hasIssues = false; /** @var ?string */ private $rootIdentifier; /** @var array Map of tag name to identifier */ private $tags; /** @var array Map of branch name to identifier */ private $branches; /** @var string */ private $branchesUrl = ''; /** @var string */ private $tagsUrl = ''; /** @var string */ private $homeUrl = ''; /** @var string */ private $website = ''; /** @var string */ private $cloneHttpsUrl = ''; /** * @var ?VcsDriver */ protected $fallbackDriver = null; /** @var string|null if set either git or hg */ private $vcsType; /** * @inheritDoc */ public function initialize() { Preg::match('#^https?://bitbucket\.org/([^/]+)/([^/]+?)(\.git|/?)?$#i', $this->url, $match); $this->owner = $match[1]; $this->repository = $match[2]; $this->originUrl = 'bitbucket.org'; $this->cache = new Cache( $this->io, implode('/', array( $this->config->get('cache-repo-dir'), $this->originUrl, $this->owner, $this->repository, )) ); $this->cache->setReadOnly($this->config->get('cache-read-only')); } /** * @inheritDoc */ public function getUrl() { if ($this->fallbackDriver) { return $this->fallbackDriver->getUrl(); } return $this->cloneHttpsUrl; } /** * Attempts to fetch the repository data via the BitBucket API and * sets some parameters which are used in other methods * * @return bool * @phpstan-impure */ protected function getRepoData() { $resource = sprintf( 'https://api.bitbucket.org/2.0/repositories/%s/%s?%s', $this->owner, $this->repository, http_build_query( array('fields' => '-project,-owner'), '', '&' ) ); $repoData = $this->fetchWithOAuthCredentials($resource, true)->decodeJson(); if ($this->fallbackDriver) { return false; } $this->parseCloneUrls($repoData['links']['clone']); $this->hasIssues = !empty($repoData['has_issues']); $this->branchesUrl = $repoData['links']['branches']['href']; $this->tagsUrl = $repoData['links']['tags']['href']; $this->homeUrl = $repoData['links']['html']['href']; $this->website = $repoData['website']; $this->vcsType = $repoData['scm']; return true; } /** * @inheritDoc */ public function getComposerInformation($identifier) { if ($this->fallbackDriver) { return $this->fallbackDriver->getComposerInformation($identifier); } if (!isset($this->infoCache[$identifier])) { if ($this->shouldCache($identifier) && $res = $this->cache->read($identifier)) { $composer = JsonFile::parseJson($res); } else { $composer = $this->getBaseComposerInformation($identifier); if ($this->shouldCache($identifier)) { $this->cache->write($identifier, json_encode($composer)); } } if ($composer) { // specials for bitbucket if (!isset($composer['support']['source'])) { $label = array_search( $identifier, $this->getTags() ) ?: array_search( $identifier, $this->getBranches() ) ?: $identifier; if (array_key_exists($label, $tags = $this->getTags())) { $hash = $tags[$label]; } elseif (array_key_exists($label, $branches = $this->getBranches())) { $hash = $branches[$label]; } if (!isset($hash)) { $composer['support']['source'] = sprintf( 'https://%s/%s/%s/src', $this->originUrl, $this->owner, $this->repository ); } else { $composer['support']['source'] = sprintf( 'https://%s/%s/%s/src/%s/?at=%s', $this->originUrl, $this->owner, $this->repository, $hash, $label ); } } if (!isset($composer['support']['issues']) && $this->hasIssues) { $composer['support']['issues'] = sprintf( 'https://%s/%s/%s/issues', $this->originUrl, $this->owner, $this->repository ); } if (!isset($composer['homepage'])) { $composer['homepage'] = empty($this->website) ? $this->homeUrl : $this->website; } } $this->infoCache[$identifier] = $composer; } return $this->infoCache[$identifier]; } /** * @inheritDoc */ public function getFileContent($file, $identifier) { if ($this->fallbackDriver) { return $this->fallbackDriver->getFileContent($file, $identifier); } if (strpos($identifier, '/') !== false) { $branches = $this->getBranches(); if (isset($branches[$identifier])) { $identifier = $branches[$identifier]; } } $resource = sprintf( 'https://api.bitbucket.org/2.0/repositories/%s/%s/src/%s/%s', $this->owner, $this->repository, $identifier, $file ); return $this->fetchWithOAuthCredentials($resource)->getBody(); } /** * @inheritDoc */ public function getChangeDate($identifier) { if ($this->fallbackDriver) { return $this->fallbackDriver->getChangeDate($identifier); } if (strpos($identifier, '/') !== false) { $branches = $this->getBranches(); if (isset($branches[$identifier])) { $identifier = $branches[$identifier]; } } $resource = sprintf( 'https://api.bitbucket.org/2.0/repositories/%s/%s/commit/%s?fields=date', $this->owner, $this->repository, $identifier ); $commit = $this->fetchWithOAuthCredentials($resource)->decodeJson(); return new \DateTime($commit['date']); } /** * @inheritDoc */ public function getSource($identifier) { if ($this->fallbackDriver) { return $this->fallbackDriver->getSource($identifier); } return array('type' => $this->vcsType, 'url' => $this->getUrl(), 'reference' => $identifier); } /** * @inheritDoc */ public function getDist($identifier) { if ($this->fallbackDriver) { return $this->fallbackDriver->getDist($identifier); } $url = sprintf( 'https://bitbucket.org/%s/%s/get/%s.zip', $this->owner, $this->repository, $identifier ); return array('type' => 'zip', 'url' => $url, 'reference' => $identifier, 'shasum' => ''); } /** * @inheritDoc */ public function getTags() { if ($this->fallbackDriver) { return $this->fallbackDriver->getTags(); } if (null === $this->tags) { $tags = array(); $resource = sprintf( '%s?%s', $this->tagsUrl, http_build_query( array( 'pagelen' => 100, 'fields' => 'values.name,values.target.hash,next', 'sort' => '-target.date', ), '', '&' ) ); $hasNext = true; while ($hasNext) { $tagsData = $this->fetchWithOAuthCredentials($resource)->decodeJson(); foreach ($tagsData['values'] as $data) { $tags[$data['name']] = $data['target']['hash']; } if (empty($tagsData['next'])) { $hasNext = false; } else { $resource = $tagsData['next']; } } $this->tags = $tags; } return $this->tags; } /** * @inheritDoc */ public function getBranches() { if ($this->fallbackDriver) { return $this->fallbackDriver->getBranches(); } if (null === $this->branches) { $branches = array(); $resource = sprintf( '%s?%s', $this->branchesUrl, http_build_query( array( 'pagelen' => 100, 'fields' => 'values.name,values.target.hash,values.heads,next', 'sort' => '-target.date', ), '', '&' ) ); $hasNext = true; while ($hasNext) { $branchData = $this->fetchWithOAuthCredentials($resource)->decodeJson(); foreach ($branchData['values'] as $data) { $branches[$data['name']] = $data['target']['hash']; } if (empty($branchData['next'])) { $hasNext = false; } else { $resource = $branchData['next']; } } $this->branches = $branches; } return $this->branches; } /** * Get the remote content. * * @param string $url The URL of content * @param bool $fetchingRepoData * * @return Response The result * * @phpstan-impure */ protected function fetchWithOAuthCredentials($url, $fetchingRepoData = false) { try { return parent::getContents($url); } catch (TransportException $e) { $bitbucketUtil = new Bitbucket($this->io, $this->config, $this->process, $this->httpDownloader); if (403 === $e->getCode() || (401 === $e->getCode() && strpos($e->getMessage(), 'Could not authenticate against') === 0)) { if (!$this->io->hasAuthentication($this->originUrl) && $bitbucketUtil->authorizeOAuth($this->originUrl) ) { return parent::getContents($url); } if (!$this->io->isInteractive() && $fetchingRepoData) { $this->attemptCloneFallback(); return new Response(array('url' => 'dummy'), 200, array(), 'null'); } } throw $e; } } /** * Generate an SSH URL * * @return string */ protected function generateSshUrl() { return 'git@' . $this->originUrl . ':' . $this->owner.'/'.$this->repository.'.git'; } /** * @phpstan-impure * * @return true * @throws \RuntimeException */ protected function attemptCloneFallback() { try { $this->setupFallbackDriver($this->generateSshUrl()); return true; } catch (\RuntimeException $e) { $this->fallbackDriver = null; $this->io->writeError( 'Failed to clone the ' . $this->generateSshUrl() . ' repository, try running in interactive mode' . ' so that you can enter your Bitbucket OAuth consumer credentials' ); throw $e; } } /** * @param string $url * @return void */ protected function setupFallbackDriver($url) { $this->fallbackDriver = new GitDriver( array('url' => $url), $this->io, $this->config, $this->httpDownloader, $this->process ); $this->fallbackDriver->initialize(); } /** * @param array $cloneLinks * @return void */ protected function parseCloneUrls(array $cloneLinks) { foreach ($cloneLinks as $cloneLink) { if ($cloneLink['name'] === 'https') { // Format: https://(user@)bitbucket.org/{user}/{repo} // Strip username from URL (only present in clone URL's for private repositories) $this->cloneHttpsUrl = Preg::replace('/https:\/\/([^@]+@)?/', 'https://', $cloneLink['href']); } } } /** * @return (array{name: string}&mixed[])|null */ protected function getMainBranchData() { $resource = sprintf( 'https://api.bitbucket.org/2.0/repositories/%s/%s?fields=mainbranch', $this->owner, $this->repository ); $data = $this->fetchWithOAuthCredentials($resource)->decodeJson(); if (isset($data['mainbranch'])) { return $data['mainbranch']; } return null; } /** * @inheritDoc */ public function getRootIdentifier() { if ($this->fallbackDriver) { return $this->fallbackDriver->getRootIdentifier(); } if (null === $this->rootIdentifier) { if (!$this->getRepoData()) { if (!$this->fallbackDriver) { throw new \LogicException('A fallback driver should be setup if getRepoData returns false'); } return $this->fallbackDriver->getRootIdentifier(); } if ($this->vcsType !== 'git') { throw new \RuntimeException( $this->url.' does not appear to be a git repository, use '. $this->cloneHttpsUrl.' but remember that Bitbucket no longer supports the mercurial repositories. '. 'https://bitbucket.org/blog/sunsetting-mercurial-support-in-bitbucket' ); } $mainBranchData = $this->getMainBranchData(); $this->rootIdentifier = !empty($mainBranchData['name']) ? $mainBranchData['name'] : 'master'; } return $this->rootIdentifier; } /** * @inheritDoc */ public static function supports(IOInterface $io, Config $config, $url, $deep = false) { if (!Preg::isMatch('#^https?://bitbucket\.org/([^/]+)/([^/]+?)(\.git|/?)?$#i', $url)) { return false; } if (!extension_loaded('openssl')) { $io->writeError('Skipping Bitbucket git driver for '.$url.' because the OpenSSL PHP extension is missing.', true, IOInterface::VERBOSE); return false; } return true; } } * Jordi Boggiano * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Composer\Repository\Vcs; use Composer\Cache; use Composer\Config; use Composer\Pcre\Preg; use Composer\Util\ProcessExecutor; use Composer\Util\Filesystem; use Composer\IO\IOInterface; /** * @author BohwaZ */ class FossilDriver extends VcsDriver { /** @var array Map of tag name to identifier */ protected $tags; /** @var array Map of branch name to identifier */ protected $branches; /** @var ?string */ protected $rootIdentifier = null; /** @var ?string */ protected $repoFile = null; /** @var string */ protected $checkoutDir; /** * @inheritDoc */ public function initialize() { // Make sure fossil is installed and reachable. $this->checkFossil(); // Ensure we are allowed to use this URL by config. $this->config->prohibitUrlByConfig($this->url, $this->io); // Only if url points to a locally accessible directory, assume it's the checkout directory. // Otherwise, it should be something fossil can clone from. if (Filesystem::isLocalPath($this->url) && is_dir($this->url)) { $this->checkoutDir = $this->url; } else { if (!Cache::isUsable((string) $this->config->get('cache-repo-dir')) || !Cache::isUsable((string) $this->config->get('cache-vcs-dir'))) { throw new \RuntimeException('FossilDriver requires a usable cache directory, and it looks like you set it to be disabled'); } $localName = Preg::replace('{[^a-z0-9]}i', '-', $this->url); $this->repoFile = $this->config->get('cache-repo-dir') . '/' . $localName . '.fossil'; $this->checkoutDir = $this->config->get('cache-vcs-dir') . '/' . $localName . '/'; $this->updateLocalRepo(); } $this->getTags(); $this->getBranches(); } /** * Check that fossil can be invoked via command line. * * @return void */ protected function checkFossil() { if (0 !== $this->process->execute('fossil version', $ignoredOutput)) { throw new \RuntimeException("fossil was not found, check that it is installed and in your PATH env.\n\n" . $this->process->getErrorOutput()); } } /** * Clone or update existing local fossil repository. * * @return void */ protected function updateLocalRepo() { $fs = new Filesystem(); $fs->ensureDirectoryExists($this->checkoutDir); if (!is_writable(dirname($this->checkoutDir))) { throw new \RuntimeException('Can not clone '.$this->url.' to access package information. The "'.$this->checkoutDir.'" directory is not writable by the current user.'); } // update the repo if it is a valid fossil repository if (is_file($this->repoFile) && is_dir($this->checkoutDir) && 0 === $this->process->execute('fossil info', $output, $this->checkoutDir)) { if (0 !== $this->process->execute('fossil pull', $output, $this->checkoutDir)) { $this->io->writeError('Failed to update '.$this->url.', package information from this repository may be outdated ('.$this->process->getErrorOutput().')'); } } else { // clean up directory and do a fresh clone into it $fs->removeDirectory($this->checkoutDir); $fs->remove($this->repoFile); $fs->ensureDirectoryExists($this->checkoutDir); if (0 !== $this->process->execute(sprintf('fossil clone -- %s %s', ProcessExecutor::escape($this->url), ProcessExecutor::escape($this->repoFile)), $output)) { $output = $this->process->getErrorOutput(); throw new \RuntimeException('Failed to clone '.$this->url.' to repository ' . $this->repoFile . "\n\n" .$output); } if (0 !== $this->process->execute(sprintf('fossil open --nested -- %s', ProcessExecutor::escape($this->repoFile)), $output, $this->checkoutDir)) { $output = $this->process->getErrorOutput(); throw new \RuntimeException('Failed to open repository '.$this->repoFile.' in ' . $this->checkoutDir . "\n\n" .$output); } } } /** * @inheritDoc */ public function getRootIdentifier() { if (null === $this->rootIdentifier) { $this->rootIdentifier = 'trunk'; } return $this->rootIdentifier; } /** * @inheritDoc */ public function getUrl() { return $this->url; } /** * @inheritDoc */ public function getSource($identifier) { return array('type' => 'fossil', 'url' => $this->getUrl(), 'reference' => $identifier); } /** * @inheritDoc */ public function getDist($identifier) { return null; } /** * @inheritDoc */ public function getFileContent($file, $identifier) { $command = sprintf('fossil cat -r %s -- %s', ProcessExecutor::escape($identifier), ProcessExecutor::escape($file)); $this->process->execute($command, $content, $this->checkoutDir); if (!trim($content)) { return null; } return $content; } /** * @inheritDoc */ public function getChangeDate($identifier) { $this->process->execute('fossil finfo -b -n 1 composer.json', $output, $this->checkoutDir); list(, $date) = explode(' ', trim($output), 3); return new \DateTime($date, new \DateTimeZone('UTC')); } /** * @inheritDoc */ public function getTags() { if (null === $this->tags) { $tags = array(); $this->process->execute('fossil tag list', $output, $this->checkoutDir); foreach ($this->process->splitLines($output) as $tag) { $tags[$tag] = $tag; } $this->tags = $tags; } return $this->tags; } /** * @inheritDoc */ public function getBranches() { if (null === $this->branches) { $branches = array(); $this->process->execute('fossil branch list', $output, $this->checkoutDir); foreach ($this->process->splitLines($output) as $branch) { $branch = trim(Preg::replace('/^\*/', '', trim($branch))); $branches[$branch] = $branch; } $this->branches = $branches; } return $this->branches; } /** * @inheritDoc */ public static function supports(IOInterface $io, Config $config, $url, $deep = false) { if (Preg::isMatch('#(^(?:https?|ssh)://(?:[^@]@)?(?:chiselapp\.com|fossil\.))#i', $url)) { return true; } if (Preg::isMatch('!/fossil/|\.fossil!', $url)) { return true; } // local filesystem if (Filesystem::isLocalPath($url)) { $url = Filesystem::getPlatformPath($url); if (!is_dir($url)) { return false; } $process = new ProcessExecutor($io); // check whether there is a fossil repo in that path if ($process->execute('fossil info', $output, $url) === 0) { return true; } } return false; } } * Jordi Boggiano * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Composer\Repository; use Composer\IO\IOInterface; use Composer\Config; use Composer\EventDispatcher\EventDispatcher; use Composer\Package\PackageInterface; use Composer\Util\HttpDownloader; use Composer\Util\ProcessExecutor; /** * Repositories manager. * * @author Jordi Boggiano * @author Konstantin Kudryashov * @author François Pluchino */ class RepositoryManager { /** @var InstalledRepositoryInterface */ private $localRepository; /** @var list */ private $repositories = array(); /** @var array> */ private $repositoryClasses = array(); /** @var IOInterface */ private $io; /** @var Config */ private $config; /** @var HttpDownloader */ private $httpDownloader; /** @var ?EventDispatcher */ private $eventDispatcher; /** @var ProcessExecutor */ private $process; public function __construct(IOInterface $io, Config $config, HttpDownloader $httpDownloader, EventDispatcher $eventDispatcher = null, ProcessExecutor $process = null) { $this->io = $io; $this->config = $config; $this->httpDownloader = $httpDownloader; $this->eventDispatcher = $eventDispatcher; $this->process = $process ?: new ProcessExecutor($io); } /** * Searches for a package by its name and version in managed repositories. * * @param string $name package name * @param string|\Composer\Semver\Constraint\ConstraintInterface $constraint package version or version constraint to match against * * @return PackageInterface|null */ public function findPackage($name, $constraint) { foreach ($this->repositories as $repository) { /** @var RepositoryInterface $repository */ if ($package = $repository->findPackage($name, $constraint)) { return $package; } } return null; } /** * Searches for all packages matching a name and optionally a version in managed repositories. * * @param string $name package name * @param string|\Composer\Semver\Constraint\ConstraintInterface $constraint package version or version constraint to match against * * @return PackageInterface[] */ public function findPackages($name, $constraint) { $packages = array(); foreach ($this->getRepositories() as $repository) { $packages = array_merge($packages, $repository->findPackages($name, $constraint)); } return $packages; } /** * Adds repository * * @param RepositoryInterface $repository repository instance * * @return void */ public function addRepository(RepositoryInterface $repository) { $this->repositories[] = $repository; } /** * Adds a repository to the beginning of the chain * * This is useful when injecting additional repositories that should trump Packagist, e.g. from a plugin. * * @param RepositoryInterface $repository repository instance * * @return void */ public function prependRepository(RepositoryInterface $repository) { array_unshift($this->repositories, $repository); } /** * Returns a new repository for a specific installation type. * * @param string $type repository type * @param array $config repository configuration * @param string $name repository name * @throws \InvalidArgumentException if repository for provided type is not registered * @return RepositoryInterface */ public function createRepository($type, $config, $name = null) { if (!isset($this->repositoryClasses[$type])) { throw new \InvalidArgumentException('Repository type is not registered: '.$type); } if (isset($config['packagist']) && false === $config['packagist']) { $this->io->writeError('Repository "'.$name.'" ('.json_encode($config).') has a packagist key which should be in its own repository definition'); } $class = $this->repositoryClasses[$type]; if (isset($config['only']) || isset($config['exclude']) || isset($config['canonical'])) { $filterConfig = $config; unset($config['only'], $config['exclude'], $config['canonical']); } $repository = new $class($config, $this->io, $this->config, $this->httpDownloader, $this->eventDispatcher, $this->process); if (isset($filterConfig)) { $repository = new FilterRepository($repository, $filterConfig); } return $repository; } /** * Stores repository class for a specific installation type. * * @param string $type installation type * @param class-string $class class name of the repo implementation * * @return void */ public function setRepositoryClass($type, $class) { $this->repositoryClasses[$type] = $class; } /** * Returns all repositories, except local one. * * @return RepositoryInterface[] */ public function getRepositories() { return $this->repositories; } /** * Sets local repository for the project. * * @param InstalledRepositoryInterface $repository repository instance * * @return void */ public function setLocalRepository(InstalledRepositoryInterface $repository) { $this->localRepository = $repository; } /** * Returns local repository for the project. * * @return InstalledRepositoryInterface */ public function getLocalRepository() { return $this->localRepository; } } * Jordi Boggiano * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Composer\Repository; use Composer\DependencyResolver\PoolOptimizer; use Composer\DependencyResolver\PolicyInterface; use Composer\DependencyResolver\Pool; use Composer\DependencyResolver\PoolBuilder; use Composer\DependencyResolver\Request; use Composer\EventDispatcher\EventDispatcher; use Composer\IO\IOInterface; use Composer\IO\NullIO; use Composer\Package\BasePackage; use Composer\Package\AliasPackage; use Composer\Package\CompleteAliasPackage; use Composer\Package\CompletePackage; use Composer\Semver\Constraint\ConstraintInterface; use Composer\Package\Version\StabilityFilter; /** * @author Nils Adermann */ class RepositorySet { /** * Packages are returned even though their stability does not match the required stability */ const ALLOW_UNACCEPTABLE_STABILITIES = 1; /** * Packages will be looked up in all repositories, even after they have been found in a higher prio one */ const ALLOW_SHADOWED_REPOSITORIES = 2; /** * @var array[] * @phpstan-var array> */ private $rootAliases; /** * @var string[] * @phpstan-var array */ private $rootReferences; /** @var RepositoryInterface[] */ private $repositories = array(); /** * @var int[] array of stability => BasePackage::STABILITY_* value * @phpstan-var array */ private $acceptableStabilities; /** * @var int[] array of package name => BasePackage::STABILITY_* value * @phpstan-var array */ private $stabilityFlags; /** * @var ConstraintInterface[] * @phpstan-var array */ private $rootRequires; /** @var bool */ private $locked = false; /** @var bool */ private $allowInstalledRepositories = false; /** * In most cases if you are looking to use this class as a way to find packages from repositories * passing minimumStability is all you need to worry about. The rest is for advanced pool creation including * aliases, pinned references and other special cases. * * @param string $minimumStability * @param int[] $stabilityFlags an array of package name => BasePackage::STABILITY_* value * @phpstan-param array $stabilityFlags * @param array[] $rootAliases * @phpstan-param list $rootAliases * @param string[] $rootReferences an array of package name => source reference * @phpstan-param array $rootReferences * @param ConstraintInterface[] $rootRequires an array of package name => constraint from the root package * @phpstan-param array $rootRequires */ public function __construct($minimumStability = 'stable', array $stabilityFlags = array(), array $rootAliases = array(), array $rootReferences = array(), array $rootRequires = array()) { $this->rootAliases = self::getRootAliasesPerPackage($rootAliases); $this->rootReferences = $rootReferences; $this->acceptableStabilities = array(); foreach (BasePackage::$stabilities as $stability => $value) { if ($value <= BasePackage::$stabilities[$minimumStability]) { $this->acceptableStabilities[$stability] = $value; } } $this->stabilityFlags = $stabilityFlags; $this->rootRequires = $rootRequires; foreach ($rootRequires as $name => $constraint) { if (PlatformRepository::isPlatformPackage($name)) { unset($this->rootRequires[$name]); } } } /** * @param bool $allow * * @return void */ public function allowInstalledRepositories($allow = true) { $this->allowInstalledRepositories = $allow; } /** * @return ConstraintInterface[] an array of package name => constraint from the root package, platform requirements excluded * @phpstan-return array */ public function getRootRequires() { return $this->rootRequires; } /** * Adds a repository to this repository set * * The first repos added have a higher priority. As soon as a package is found in any * repository the search for that package ends, and following repos will not be consulted. * * @param RepositoryInterface $repo A package repository * * @return void */ public function addRepository(RepositoryInterface $repo) { if ($this->locked) { throw new \RuntimeException("Pool has already been created from this repository set, it cannot be modified anymore."); } if ($repo instanceof CompositeRepository) { $repos = $repo->getRepositories(); } else { $repos = array($repo); } foreach ($repos as $repo) { $this->repositories[] = $repo; } } /** * Find packages providing or matching a name and optionally meeting a constraint in all repositories * * Returned in the order of repositories, matching priority * * @param string $name * @param ConstraintInterface|null $constraint * @param int $flags any of the ALLOW_* constants from this class to tweak what is returned * @return BasePackage[] */ public function findPackages($name, ConstraintInterface $constraint = null, $flags = 0) { $ignoreStability = ($flags & self::ALLOW_UNACCEPTABLE_STABILITIES) !== 0; $loadFromAllRepos = ($flags & self::ALLOW_SHADOWED_REPOSITORIES) !== 0; $packages = array(); if ($loadFromAllRepos) { foreach ($this->repositories as $repository) { $packages[] = $repository->findPackages($name, $constraint) ?: array(); } } else { foreach ($this->repositories as $repository) { $result = $repository->loadPackages(array($name => $constraint), $ignoreStability ? BasePackage::$stabilities : $this->acceptableStabilities, $ignoreStability ? array() : $this->stabilityFlags); $packages[] = $result['packages']; foreach ($result['namesFound'] as $nameFound) { // avoid loading the same package again from other repositories once it has been found if ($name === $nameFound) { break 2; } } } } $candidates = $packages ? call_user_func_array('array_merge', $packages) : array(); // when using loadPackages above (!$loadFromAllRepos) the repos already filter for stability so no need to do it again if ($ignoreStability || !$loadFromAllRepos) { return $candidates; } $result = array(); foreach ($candidates as $candidate) { if ($this->isPackageAcceptable($candidate->getNames(), $candidate->getStability())) { $result[] = $candidate; } } return $result; } /** * @param string $packageName * * @return array[] an array with the provider name as key and value of array('name' => '...', 'description' => '...', 'type' => '...') * @phpstan-return array */ public function getProviders($packageName) { $providers = array(); foreach ($this->repositories as $repository) { if ($repoProviders = $repository->getProviders($packageName)) { $providers = array_merge($providers, $repoProviders); } } return $providers; } /** * Check for each given package name whether it would be accepted by this RepositorySet in the given $stability * * @param string[] $names * @param string $stability one of 'stable', 'RC', 'beta', 'alpha' or 'dev' * @return bool */ public function isPackageAcceptable($names, $stability) { return StabilityFilter::isPackageAcceptable($this->acceptableStabilities, $this->stabilityFlags, $names, $stability); } /** * Create a pool for dependency resolution from the packages in this repository set. * * @return Pool */ public function createPool(Request $request, IOInterface $io, EventDispatcher $eventDispatcher = null, PoolOptimizer $poolOptimizer = null) { $poolBuilder = new PoolBuilder($this->acceptableStabilities, $this->stabilityFlags, $this->rootAliases, $this->rootReferences, $io, $eventDispatcher, $poolOptimizer); foreach ($this->repositories as $repo) { if (($repo instanceof InstalledRepositoryInterface || $repo instanceof InstalledRepository) && !$this->allowInstalledRepositories) { throw new \LogicException('The pool can not accept packages from an installed repository'); } } $this->locked = true; return $poolBuilder->buildPool($this->repositories, $request); } /** * Create a pool for dependency resolution from the packages in this repository set. * * @return Pool */ public function createPoolWithAllPackages() { foreach ($this->repositories as $repo) { if (($repo instanceof InstalledRepositoryInterface || $repo instanceof InstalledRepository) && !$this->allowInstalledRepositories) { throw new \LogicException('The pool can not accept packages from an installed repository'); } } $this->locked = true; $packages = array(); foreach ($this->repositories as $repository) { foreach ($repository->getPackages() as $package) { $packages[] = $package; if (isset($this->rootAliases[$package->getName()][$package->getVersion()])) { $alias = $this->rootAliases[$package->getName()][$package->getVersion()]; while ($package instanceof AliasPackage) { $package = $package->getAliasOf(); } if ($package instanceof CompletePackage) { $aliasPackage = new CompleteAliasPackage($package, $alias['alias_normalized'], $alias['alias']); } else { $aliasPackage = new AliasPackage($package, $alias['alias_normalized'], $alias['alias']); } $aliasPackage->setRootPackageAlias(true); $packages[] = $aliasPackage; } } } return new Pool($packages); } /** * @param string $packageName * * @return Pool */ public function createPoolForPackage($packageName, LockArrayRepository $lockedRepo = null) { // TODO unify this with above in some simpler version without "request"? return $this->createPoolForPackages(array($packageName), $lockedRepo); } /** * @param string[] $packageNames * * @return Pool */ public function createPoolForPackages($packageNames, LockArrayRepository $lockedRepo = null) { $request = new Request($lockedRepo); foreach ($packageNames as $packageName) { if (PlatformRepository::isPlatformPackage($packageName)) { throw new \LogicException('createPoolForPackage(s) can not be used for platform packages, as they are never loaded by the PoolBuilder which expects them to be fixed. Use createPoolWithAllPackages or pass in a proper request with the platform packages you need fixed in it.'); } $request->requireName($packageName); } return $this->createPool($request, new NullIO()); } /** * @param array[] $aliases * @phpstan-param list $aliases * * @return array> */ private static function getRootAliasesPerPackage(array $aliases) { $normalizedAliases = array(); foreach ($aliases as $alias) { $normalizedAliases[$alias['package']][$alias['version']] = array( 'alias' => $alias['alias'], 'alias_normalized' => $alias['alias_normalized'], ); } return $normalizedAliases; } } * Jordi Boggiano * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Composer\Installer; use Composer\Repository\InstalledRepositoryInterface; use Composer\Package\PackageInterface; use Composer\IO\IOInterface; use Composer\DependencyResolver\Operation\UpdateOperation; use Composer\DependencyResolver\Operation\InstallOperation; use Composer\DependencyResolver\Operation\UninstallOperation; /** * Metapackage installation manager. * * @author Martin Hasoň */ class MetapackageInstaller implements InstallerInterface { /** @var IOInterface */ private $io; public function __construct(IOInterface $io) { $this->io = $io; } /** * @inheritDoc */ public function supports($packageType) { return $packageType === 'metapackage'; } /** * @inheritDoc */ public function isInstalled(InstalledRepositoryInterface $repo, PackageInterface $package) { return $repo->hasPackage($package); } /** * @inheritDoc */ public function download(PackageInterface $package, PackageInterface $prevPackage = null) { // noop return \React\Promise\resolve(); } /** * @inheritDoc */ public function prepare($type, PackageInterface $package, PackageInterface $prevPackage = null) { // noop return \React\Promise\resolve(); } /** * @inheritDoc */ public function cleanup($type, PackageInterface $package, PackageInterface $prevPackage = null) { // noop return \React\Promise\resolve(); } /** * @inheritDoc */ public function install(InstalledRepositoryInterface $repo, PackageInterface $package) { $this->io->writeError(" - " . InstallOperation::format($package)); $repo->addPackage(clone $package); return \React\Promise\resolve(); } /** * @inheritDoc */ public function update(InstalledRepositoryInterface $repo, PackageInterface $initial, PackageInterface $target) { if (!$repo->hasPackage($initial)) { throw new \InvalidArgumentException('Package is not installed: '.$initial); } $this->io->writeError(" - " . UpdateOperation::format($initial, $target)); $repo->removePackage($initial); $repo->addPackage(clone $target); return \React\Promise\resolve(); } /** * @inheritDoc */ public function uninstall(InstalledRepositoryInterface $repo, PackageInterface $package) { if (!$repo->hasPackage($package)) { throw new \InvalidArgumentException('Package is not installed: '.$package); } $this->io->writeError(" - " . UninstallOperation::format($package)); $repo->removePackage($package); return \React\Promise\resolve(); } /** * @inheritDoc */ public function getInstallPath(PackageInterface $package) { return ''; } } * Jordi Boggiano * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Composer\Installer; use Composer\Composer; use Composer\IO\IOInterface; use Composer\Pcre\Preg; use Composer\Repository\InstalledRepositoryInterface; use Composer\Package\PackageInterface; use Composer\Util\Filesystem; use Composer\Util\Silencer; use Composer\Util\Platform; use React\Promise\PromiseInterface; use Composer\Downloader\DownloadManager; /** * Package installation manager. * * @author Jordi Boggiano * @author Konstantin Kudryashov */ class LibraryInstaller implements InstallerInterface, BinaryPresenceInterface { /** @var Composer */ protected $composer; /** @var string */ protected $vendorDir; /** @var DownloadManager */ protected $downloadManager; /** @var IOInterface */ protected $io; /** @var string */ protected $type; /** @var Filesystem */ protected $filesystem; /** @var BinaryInstaller */ protected $binaryInstaller; /** * Initializes library installer. * * @param IOInterface $io * @param Composer $composer * @param string|null $type * @param Filesystem $filesystem * @param BinaryInstaller $binaryInstaller */ public function __construct(IOInterface $io, Composer $composer, $type = 'library', Filesystem $filesystem = null, BinaryInstaller $binaryInstaller = null) { $this->composer = $composer; $this->downloadManager = $composer->getDownloadManager(); $this->io = $io; $this->type = $type; $this->filesystem = $filesystem ?: new Filesystem(); $this->vendorDir = rtrim($composer->getConfig()->get('vendor-dir'), '/'); $this->binaryInstaller = $binaryInstaller ?: new BinaryInstaller($this->io, rtrim($composer->getConfig()->get('bin-dir'), '/'), $composer->getConfig()->get('bin-compat'), $this->filesystem, $this->vendorDir); } /** * @inheritDoc */ public function supports($packageType) { return $packageType === $this->type || null === $this->type; } /** * @inheritDoc */ public function isInstalled(InstalledRepositoryInterface $repo, PackageInterface $package) { if (!$repo->hasPackage($package)) { return false; } $installPath = $this->getInstallPath($package); if (Filesystem::isReadable($installPath)) { return true; } return (Platform::isWindows() && $this->filesystem->isJunction($installPath)) || is_link($installPath); } /** * @inheritDoc */ public function download(PackageInterface $package, PackageInterface $prevPackage = null) { $this->initializeVendorDir(); $downloadPath = $this->getInstallPath($package); return $this->downloadManager->download($package, $downloadPath, $prevPackage); } /** * @inheritDoc */ public function prepare($type, PackageInterface $package, PackageInterface $prevPackage = null) { $this->initializeVendorDir(); $downloadPath = $this->getInstallPath($package); return $this->downloadManager->prepare($type, $package, $downloadPath, $prevPackage); } /** * @inheritDoc */ public function cleanup($type, PackageInterface $package, PackageInterface $prevPackage = null) { $this->initializeVendorDir(); $downloadPath = $this->getInstallPath($package); return $this->downloadManager->cleanup($type, $package, $downloadPath, $prevPackage); } /** * @inheritDoc */ public function install(InstalledRepositoryInterface $repo, PackageInterface $package) { $this->initializeVendorDir(); $downloadPath = $this->getInstallPath($package); // remove the binaries if it appears the package files are missing if (!Filesystem::isReadable($downloadPath) && $repo->hasPackage($package)) { $this->binaryInstaller->removeBinaries($package); } $promise = $this->installCode($package); if (!$promise instanceof PromiseInterface) { $promise = \React\Promise\resolve(); } $binaryInstaller = $this->binaryInstaller; $installPath = $this->getInstallPath($package); return $promise->then(function () use ($binaryInstaller, $installPath, $package, $repo) { $binaryInstaller->installBinaries($package, $installPath); if (!$repo->hasPackage($package)) { $repo->addPackage(clone $package); } }); } /** * @inheritDoc */ public function update(InstalledRepositoryInterface $repo, PackageInterface $initial, PackageInterface $target) { if (!$repo->hasPackage($initial)) { throw new \InvalidArgumentException('Package is not installed: '.$initial); } $this->initializeVendorDir(); $this->binaryInstaller->removeBinaries($initial); $promise = $this->updateCode($initial, $target); if (!$promise instanceof PromiseInterface) { $promise = \React\Promise\resolve(); } $binaryInstaller = $this->binaryInstaller; $installPath = $this->getInstallPath($target); return $promise->then(function () use ($binaryInstaller, $installPath, $target, $initial, $repo) { $binaryInstaller->installBinaries($target, $installPath); $repo->removePackage($initial); if (!$repo->hasPackage($target)) { $repo->addPackage(clone $target); } }); } /** * @inheritDoc */ public function uninstall(InstalledRepositoryInterface $repo, PackageInterface $package) { if (!$repo->hasPackage($package)) { throw new \InvalidArgumentException('Package is not installed: '.$package); } $promise = $this->removeCode($package); if (!$promise instanceof PromiseInterface) { $promise = \React\Promise\resolve(); } $binaryInstaller = $this->binaryInstaller; $downloadPath = $this->getPackageBasePath($package); $filesystem = $this->filesystem; return $promise->then(function () use ($binaryInstaller, $filesystem, $downloadPath, $package, $repo) { $binaryInstaller->removeBinaries($package); $repo->removePackage($package); if (strpos($package->getName(), '/')) { $packageVendorDir = dirname($downloadPath); if (is_dir($packageVendorDir) && $filesystem->isDirEmpty($packageVendorDir)) { Silencer::call('rmdir', $packageVendorDir); } } }); } /** * @inheritDoc */ public function getInstallPath(PackageInterface $package) { $this->initializeVendorDir(); $basePath = ($this->vendorDir ? $this->vendorDir.'/' : '') . $package->getPrettyName(); $targetDir = $package->getTargetDir(); return $basePath . ($targetDir ? '/'.$targetDir : ''); } /** * Make sure binaries are installed for a given package. * * @param PackageInterface $package Package instance */ public function ensureBinariesPresence(PackageInterface $package) { $this->binaryInstaller->installBinaries($package, $this->getInstallPath($package), false); } /** * Returns the base path of the package without target-dir path * * It is used for BC as getInstallPath tends to be overridden by * installer plugins but not getPackageBasePath * * @param PackageInterface $package * @return string */ protected function getPackageBasePath(PackageInterface $package) { $installPath = $this->getInstallPath($package); $targetDir = $package->getTargetDir(); if ($targetDir) { return Preg::replace('{/*'.str_replace('/', '/+', preg_quote($targetDir)).'/?$}', '', $installPath); } return $installPath; } /** * @return PromiseInterface|null */ protected function installCode(PackageInterface $package) { $downloadPath = $this->getInstallPath($package); return $this->downloadManager->install($package, $downloadPath); } /** * @return PromiseInterface|null */ protected function updateCode(PackageInterface $initial, PackageInterface $target) { $initialDownloadPath = $this->getInstallPath($initial); $targetDownloadPath = $this->getInstallPath($target); if ($targetDownloadPath !== $initialDownloadPath) { // if the target and initial dirs intersect, we force a remove + install // to avoid the rename wiping the target dir as part of the initial dir cleanup if (strpos($initialDownloadPath, $targetDownloadPath) === 0 || strpos($targetDownloadPath, $initialDownloadPath) === 0 ) { $promise = $this->removeCode($initial); if (!$promise instanceof PromiseInterface) { $promise = \React\Promise\resolve(); } $self = $this; return $promise->then(function () use ($self, $target) { $reflMethod = new \ReflectionMethod($self, 'installCode'); $reflMethod->setAccessible(true); // equivalent of $this->installCode($target) with php 5.3 support // TODO remove this once 5.3 support is dropped return $reflMethod->invoke($self, $target); }); } $this->filesystem->rename($initialDownloadPath, $targetDownloadPath); } return $this->downloadManager->update($initial, $target, $targetDownloadPath); } /** * @return PromiseInterface|null */ protected function removeCode(PackageInterface $package) { $downloadPath = $this->getPackageBasePath($package); return $this->downloadManager->remove($package, $downloadPath); } /** * @return void */ protected function initializeVendorDir() { $this->filesystem->ensureDirectoryExists($this->vendorDir); $this->vendorDir = realpath($this->vendorDir); } } * Jordi Boggiano * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Composer\Installer; use Composer\Composer; use Composer\IO\IOInterface; use Composer\Repository\InstalledRepositoryInterface; use Composer\Package\PackageInterface; use Composer\Util\Filesystem; use Composer\Util\Platform; use React\Promise\PromiseInterface; /** * Installer for plugin packages * * @author Jordi Boggiano * @author Nils Adermann */ class PluginInstaller extends LibraryInstaller { /** * Initializes Plugin installer. * * @param IOInterface $io * @param Composer $composer */ public function __construct(IOInterface $io, Composer $composer, Filesystem $fs = null, BinaryInstaller $binaryInstaller = null) { parent::__construct($io, $composer, 'composer-plugin', $fs, $binaryInstaller); } /** * @inheritDoc */ public function supports($packageType) { return $packageType === 'composer-plugin' || $packageType === 'composer-installer'; } /** * @inheritDoc */ public function download(PackageInterface $package, PackageInterface $prevPackage = null) { $extra = $package->getExtra(); if (empty($extra['class'])) { throw new \UnexpectedValueException('Error while installing '.$package->getPrettyName().', composer-plugin packages should have a class defined in their extra key to be usable.'); } return parent::download($package, $prevPackage); } /** * @inheritDoc */ public function install(InstalledRepositoryInterface $repo, PackageInterface $package) { $promise = parent::install($repo, $package); if (!$promise instanceof PromiseInterface) { $promise = \React\Promise\resolve(); } $pluginManager = $this->composer->getPluginManager(); $self = $this; return $promise->then(function () use ($self, $pluginManager, $package, $repo) { try { Platform::workaroundFilesystemIssues(); $pluginManager->registerPackage($package, true); } catch (\Exception $e) { $self->rollbackInstall($e, $repo, $package); } }); } /** * @inheritDoc */ public function update(InstalledRepositoryInterface $repo, PackageInterface $initial, PackageInterface $target) { $promise = parent::update($repo, $initial, $target); if (!$promise instanceof PromiseInterface) { $promise = \React\Promise\resolve(); } $pluginManager = $this->composer->getPluginManager(); $self = $this; return $promise->then(function () use ($self, $pluginManager, $initial, $target, $repo) { try { Platform::workaroundFilesystemIssues(); $pluginManager->deactivatePackage($initial); $pluginManager->registerPackage($target, true); } catch (\Exception $e) { $self->rollbackInstall($e, $repo, $target); } }); } public function uninstall(InstalledRepositoryInterface $repo, PackageInterface $package) { $this->composer->getPluginManager()->uninstallPackage($package); return parent::uninstall($repo, $package); } /** * TODO v3 should make this private once we can drop PHP 5.3 support * @private * * @return void */ public function rollbackInstall(\Exception $e, InstalledRepositoryInterface $repo, PackageInterface $package) { $this->io->writeError('Plugin initialization failed ('.$e->getMessage().'), uninstalling plugin'); parent::uninstall($repo, $package); throw $e; } } * Jordi Boggiano * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Composer\Installer; use Composer\Repository\InstalledRepositoryInterface; use Composer\Package\PackageInterface; /** * Does not install anything but marks packages installed in the repo * * Useful for dry runs * * @author Jordi Boggiano */ class NoopInstaller implements InstallerInterface { /** * @inheritDoc */ public function supports($packageType) { return true; } /** * @inheritDoc */ public function isInstalled(InstalledRepositoryInterface $repo, PackageInterface $package) { return $repo->hasPackage($package); } /** * @inheritDoc */ public function download(PackageInterface $package, PackageInterface $prevPackage = null) { return \React\Promise\resolve(); } /** * @inheritDoc */ public function prepare($type, PackageInterface $package, PackageInterface $prevPackage = null) { return \React\Promise\resolve(); } /** * @inheritDoc */ public function cleanup($type, PackageInterface $package, PackageInterface $prevPackage = null) { return \React\Promise\resolve(); } /** * @inheritDoc */ public function install(InstalledRepositoryInterface $repo, PackageInterface $package) { if (!$repo->hasPackage($package)) { $repo->addPackage(clone $package); } return \React\Promise\resolve(); } /** * @inheritDoc */ public function update(InstalledRepositoryInterface $repo, PackageInterface $initial, PackageInterface $target) { if (!$repo->hasPackage($initial)) { throw new \InvalidArgumentException('Package is not installed: '.$initial); } $repo->removePackage($initial); if (!$repo->hasPackage($target)) { $repo->addPackage(clone $target); } return \React\Promise\resolve(); } /** * @inheritDoc */ public function uninstall(InstalledRepositoryInterface $repo, PackageInterface $package) { if (!$repo->hasPackage($package)) { throw new \InvalidArgumentException('Package is not installed: '.$package); } $repo->removePackage($package); return \React\Promise\resolve(); } /** * @inheritDoc */ public function getInstallPath(PackageInterface $package) { $targetDir = $package->getTargetDir(); return $package->getPrettyName() . ($targetDir ? '/'.$targetDir : ''); } } * Jordi Boggiano * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Composer\Installer; use Composer\Package\PackageInterface; use Composer\Repository\InstalledRepositoryInterface; use InvalidArgumentException; use React\Promise\PromiseInterface; /** * Interface for the package installation manager. * * @author Konstantin Kudryashov * @author Jordi Boggiano */ interface InstallerInterface { /** * Decides if the installer supports the given type * * @param string $packageType * @return bool */ public function supports($packageType); /** * Checks that provided package is installed. * * @param InstalledRepositoryInterface $repo repository in which to check * @param PackageInterface $package package instance * * @return bool */ public function isInstalled(InstalledRepositoryInterface $repo, PackageInterface $package); /** * Downloads the files needed to later install the given package. * * @param PackageInterface $package package instance * @param PackageInterface $prevPackage previous package instance in case of an update * @return PromiseInterface|null */ public function download(PackageInterface $package, PackageInterface $prevPackage = null); /** * Do anything that needs to be done between all downloads have been completed and the actual operation is executed * * All packages get first downloaded, then all together prepared, then all together installed/updated/uninstalled. Therefore * for error recovery it is important to avoid failing during install/update/uninstall as much as possible, and risky things or * user prompts should happen in the prepare step rather. In case of failure, cleanup() will be called so that changes can * be undone as much as possible. * * @param string $type one of install/update/uninstall * @param PackageInterface $package package instance * @param PackageInterface $prevPackage previous package instance in case of an update * @return PromiseInterface|null */ public function prepare($type, PackageInterface $package, PackageInterface $prevPackage = null); /** * Installs specific package. * * @param InstalledRepositoryInterface $repo repository in which to check * @param PackageInterface $package package instance * @return PromiseInterface|null */ public function install(InstalledRepositoryInterface $repo, PackageInterface $package); /** * Updates specific package. * * @param InstalledRepositoryInterface $repo repository in which to check * @param PackageInterface $initial already installed package version * @param PackageInterface $target updated version * @throws InvalidArgumentException if $initial package is not installed * @return PromiseInterface|null */ public function update(InstalledRepositoryInterface $repo, PackageInterface $initial, PackageInterface $target); /** * Uninstalls specific package. * * @param InstalledRepositoryInterface $repo repository in which to check * @param PackageInterface $package package instance * @return PromiseInterface|null */ public function uninstall(InstalledRepositoryInterface $repo, PackageInterface $package); /** * Do anything to cleanup changes applied in the prepare or install/update/uninstall steps * * Note that cleanup will be called for all packages regardless if they failed an operation or not, to give * all installers a change to cleanup things they did previously, so you need to keep track of changes * applied in the installer/downloader themselves. * * @param string $type one of install/update/uninstall * @param PackageInterface $package package instance * @param PackageInterface $prevPackage previous package instance in case of an update * @return PromiseInterface|null */ public function cleanup($type, PackageInterface $package, PackageInterface $prevPackage = null); /** * Returns the installation path of a package * * @param PackageInterface $package * @return string path to install to, which MUST not end with a slash */ public function getInstallPath(PackageInterface $package); } * Jordi Boggiano * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Composer\Installer; use Composer\Composer; use Composer\DependencyResolver\Transaction; use Composer\EventDispatcher\Event; use Composer\IO\IOInterface; class InstallerEvent extends Event { /** * @var Composer */ private $composer; /** * @var IOInterface */ private $io; /** * @var bool */ private $devMode; /** * @var bool */ private $executeOperations; /** * @var Transaction */ private $transaction; /** * Constructor. * * @param string $eventName * @param Composer $composer * @param IOInterface $io * @param bool $devMode * @param bool $executeOperations * @param Transaction $transaction */ public function __construct($eventName, Composer $composer, IOInterface $io, $devMode, $executeOperations, Transaction $transaction) { parent::__construct($eventName); $this->composer = $composer; $this->io = $io; $this->devMode = $devMode; $this->executeOperations = $executeOperations; $this->transaction = $transaction; } /** * @return Composer */ public function getComposer() { return $this->composer; } /** * @return IOInterface */ public function getIO() { return $this->io; } /** * @return bool */ public function isDevMode() { return $this->devMode; } /** * @return bool */ public function isExecutingOperations() { return $this->executeOperations; } /** * @return Transaction|null */ public function getTransaction() { return $this->transaction; } } * Jordi Boggiano * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Composer\Installer; use Composer\IO\IOInterface; use Composer\Package\PackageInterface; use Composer\Pcre\Preg; use Composer\Util\Filesystem; use Composer\Util\Platform; use Composer\Util\ProcessExecutor; use Composer\Util\Silencer; /** * Utility to handle installation of package "bin"/binaries * * @author Jordi Boggiano * @author Konstantin Kudryashov * @author Helmut Hummel */ class BinaryInstaller { /** @var string */ protected $binDir; /** @var string */ protected $binCompat; /** @var IOInterface */ protected $io; /** @var Filesystem */ protected $filesystem; /** @var string|null */ private $vendorDir; /** * @param IOInterface $io * @param string $binDir * @param string $binCompat * @param Filesystem $filesystem * @param string|null $vendorDir */ public function __construct(IOInterface $io, $binDir, $binCompat, Filesystem $filesystem = null, $vendorDir = null) { $this->binDir = $binDir; $this->binCompat = $binCompat; $this->io = $io; $this->filesystem = $filesystem ?: new Filesystem(); $this->vendorDir = $vendorDir; } /** * @param string $installPath * @param bool $warnOnOverwrite * * @return void */ public function installBinaries(PackageInterface $package, $installPath, $warnOnOverwrite = true) { $binaries = $this->getBinaries($package); if (!$binaries) { return; } Platform::workaroundFilesystemIssues(); foreach ($binaries as $bin) { $binPath = $installPath.'/'.$bin; if (!file_exists($binPath)) { $this->io->writeError(' Skipped installation of bin '.$bin.' for package '.$package->getName().': file not found in package'); continue; } if (is_dir($binPath)) { $this->io->writeError(' Skipped installation of bin '.$bin.' for package '.$package->getName().': found a directory at that path'); continue; } if (!$this->filesystem->isAbsolutePath($binPath)) { // in case a custom installer returned a relative path for the // $package, we can now safely turn it into a absolute path (as we // already checked the binary's existence). The following helpers // will require absolute paths to work properly. $binPath = realpath($binPath); } $this->initializeBinDir(); $link = $this->binDir.'/'.basename($bin); if (file_exists($link)) { if (!is_link($link)) { if ($warnOnOverwrite) { $this->io->writeError(' Skipped installation of bin '.$bin.' for package '.$package->getName().': name conflicts with an existing file'); } continue; } if (realpath($link) === realpath($binPath)) { // It is a linked binary from a previous installation, which can be replaced with a proxy file $this->filesystem->unlink($link); } } $binCompat = $this->binCompat; if ($binCompat === "auto" && (Platform::isWindows() || Platform::isWindowsSubsystemForLinux())) { $binCompat = 'full'; } if ($binCompat === "full") { $this->installFullBinaries($binPath, $link, $bin, $package); } else { $this->installUnixyProxyBinaries($binPath, $link); } Silencer::call('chmod', $binPath, 0777 & ~umask()); } } /** * @return void */ public function removeBinaries(PackageInterface $package) { $this->initializeBinDir(); $binaries = $this->getBinaries($package); if (!$binaries) { return; } foreach ($binaries as $bin) { $link = $this->binDir.'/'.basename($bin); if (is_link($link) || file_exists($link)) { // still checking for symlinks here for legacy support $this->filesystem->unlink($link); } if (is_file($link.'.bat')) { $this->filesystem->unlink($link.'.bat'); } } // attempt removing the bin dir in case it is left empty if (is_dir($this->binDir) && $this->filesystem->isDirEmpty($this->binDir)) { Silencer::call('rmdir', $this->binDir); } } /** * @param string $bin * * @return string */ public static function determineBinaryCaller($bin) { if ('.bat' === substr($bin, -4) || '.exe' === substr($bin, -4)) { return 'call'; } $handle = fopen($bin, 'r'); $line = fgets($handle); fclose($handle); if (Preg::isMatch('{^#!/(?:usr/bin/env )?(?:[^/]+/)*(.+)$}m', $line, $match)) { return trim($match[1]); } return 'php'; } /** * @return string[] */ protected function getBinaries(PackageInterface $package) { return $package->getBinaries(); } /** * @param string $binPath * @param string $link * @param string $bin * * @return void */ protected function installFullBinaries($binPath, $link, $bin, PackageInterface $package) { // add unixy support for cygwin and similar environments if ('.bat' !== substr($binPath, -4)) { $this->installUnixyProxyBinaries($binPath, $link); $link .= '.bat'; if (file_exists($link)) { $this->io->writeError(' Skipped installation of bin '.$bin.'.bat proxy for package '.$package->getName().': a .bat proxy was already installed'); } } if (!file_exists($link)) { file_put_contents($link, $this->generateWindowsProxyCode($binPath, $link)); Silencer::call('chmod', $link, 0777 & ~umask()); } } /** * @param string $binPath * @param string $link * * @return void */ protected function installUnixyProxyBinaries($binPath, $link) { file_put_contents($link, $this->generateUnixyProxyCode($binPath, $link)); Silencer::call('chmod', $link, 0777 & ~umask()); } /** * @return void */ protected function initializeBinDir() { $this->filesystem->ensureDirectoryExists($this->binDir); $this->binDir = realpath($this->binDir); } /** * @param string $bin * @param string $link * * @return string */ protected function generateWindowsProxyCode($bin, $link) { $binPath = $this->filesystem->findShortestPath($link, $bin); $caller = self::determineBinaryCaller($bin); // if the target is a php file, we run the unixy proxy file // to ensure that _composer_autoload_path gets defined, instead // of running the binary directly if ($caller === 'php') { return "@ECHO OFF\r\n". "setlocal DISABLEDELAYEDEXPANSION\r\n". "SET BIN_TARGET=%~dp0/".trim(ProcessExecutor::escape(basename($link, '.bat')), '"\'')."\r\n". "SET COMPOSER_BIN_DIR=%~dp0\r\n". "{$caller} \"%BIN_TARGET%\" %*\r\n"; } return "@ECHO OFF\r\n". "setlocal DISABLEDELAYEDEXPANSION\r\n". "SET BIN_TARGET=%~dp0/".trim(ProcessExecutor::escape($binPath), '"\'')."\r\n". "SET COMPOSER_BIN_DIR=%~dp0\r\n". "{$caller} \"%BIN_TARGET%\" %*\r\n"; } /** * @param string $bin * @param string $link * * @return string */ protected function generateUnixyProxyCode($bin, $link) { $binPath = $this->filesystem->findShortestPath($link, $bin); $binDir = ProcessExecutor::escape(dirname($binPath)); $binFile = basename($binPath); $binContents = file_get_contents($bin); // For php files, we generate a PHP proxy instead of a shell one, // which allows calling the proxy with a custom php process if (Preg::isMatch('{^(#!.*\r?\n)?[\r\n\t ]*<\?php}', $binContents, $match)) { // carry over the existing shebang if present, otherwise add our own $proxyCode = empty($match[1]) ? '#!/usr/bin/env php' : trim($match[1]); $binPathExported = $this->filesystem->findShortestPathCode($link, $bin, false, true); $streamProxyCode = $streamHint = ''; $globalsCode = '$GLOBALS[\'_composer_bin_dir\'] = __DIR__;'."\n"; $phpunitHack1 = $phpunitHack2 = ''; // Don't expose autoload path when vendor dir was not set in custom installers if ($this->vendorDir) { $globalsCode .= '$GLOBALS[\'_composer_autoload_path\'] = ' . $this->filesystem->findShortestPathCode($link, $this->vendorDir . '/autoload.php', false, true).";\n"; } // Add workaround for PHPUnit process isolation if ($this->filesystem->normalizePath($bin) === $this->filesystem->normalizePath($this->vendorDir.'/phpunit/phpunit/phpunit')) { // workaround issue on PHPUnit 6.5+ running on PHP 8+ $globalsCode .= '$GLOBALS[\'__PHPUNIT_ISOLATION_EXCLUDE_LIST\'] = $GLOBALS[\'__PHPUNIT_ISOLATION_BLACKLIST\'] = array(realpath('.$binPathExported.'));'."\n"; // workaround issue on all PHPUnit versions running on PHP <8 $phpunitHack1 = "'phpvfscomposer://'."; $phpunitHack2 = ' $data = str_replace(\'__DIR__\', var_export(dirname($this->realpath), true), $data); $data = str_replace(\'__FILE__\', var_export($this->realpath, true), $data);'; } if (trim($match[0]) !== 'realpath = realpath(\$opened_path) ?: \$opened_path; \$opened_path = $phpunitHack1\$this->realpath; \$this->handle = fopen(\$this->realpath, \$mode); \$this->position = 0; return (bool) \$this->handle; } public function stream_read(\$count) { \$data = fread(\$this->handle, \$count); if (\$this->position === 0) { \$data = preg_replace('{^#!.*\\r?\\n}', '', \$data); }$phpunitHack2 \$this->position += strlen(\$data); return \$data; } public function stream_cast(\$castAs) { return \$this->handle; } public function stream_close() { fclose(\$this->handle); } public function stream_lock(\$operation) { return \$operation ? flock(\$this->handle, \$operation) : true; } public function stream_seek(\$offset, \$whence) { if (0 === fseek(\$this->handle, \$offset, \$whence)) { \$this->position = ftell(\$this->handle); return true; } return false; } public function stream_tell() { return \$this->position; } public function stream_eof() { return feof(\$this->handle); } public function stream_stat() { return array(); } public function stream_set_option(\$option, \$arg1, \$arg2) { return true; } public function url_stat(\$path, \$flags) { \$path = substr(\$path, 17); if (file_exists(\$path)) { return stat(\$path); } return false; } } } if (function_exists('stream_wrapper_register') && stream_wrapper_register('phpvfscomposer', 'Composer\BinProxyWrapper')) { include("phpvfscomposer://" . $binPathExported); exit(0); } } STREAMPROXY; } return $proxyCode . "\n" . << /dev/null) if [ -z "\$self" ]; then self="\$selfArg" fi dir=\$(cd "\${self%[/\\\\]*}" > /dev/null; cd $binDir && pwd) if [ -d /proc/cygdrive ]; then case \$(which php) in \$(readlink -n /proc/cygdrive)/*) # We are in Cygwin using Windows php, so the path must be translated dir=\$(cygpath -m "\$dir"); ;; esac fi export COMPOSER_BIN_DIR=\$(cd "\${self%[/\\\\]*}" > /dev/null; pwd) # If bash is sourcing this file, we have to source the target as well bashSource="\$BASH_SOURCE" if [ -n "\$bashSource" ]; then if [ "\$bashSource" != "\$0" ]; then source "\${dir}/$binFile" "\$@" return fi fi "\${dir}/$binFile" "\$@" PROXY; } } * Jordi Boggiano * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Composer\Installer; use Composer\Composer; use Composer\IO\IOInterface; use Composer\DependencyResolver\Operation\OperationInterface; use Composer\Repository\RepositoryInterface; use Composer\EventDispatcher\Event; /** * The Package Event. * * @author Jordi Boggiano */ class PackageEvent extends Event { /** * @var Composer */ private $composer; /** * @var IOInterface */ private $io; /** * @var bool */ private $devMode; /** * @var RepositoryInterface */ private $localRepo; /** * @var OperationInterface[] */ private $operations; /** * @var OperationInterface The operation instance which is being executed */ private $operation; /** * Constructor. * * @param string $eventName * @param Composer $composer * @param IOInterface $io * @param bool $devMode * @param RepositoryInterface $localRepo * @param OperationInterface[] $operations * @param OperationInterface $operation */ public function __construct($eventName, Composer $composer, IOInterface $io, $devMode, RepositoryInterface $localRepo, array $operations, OperationInterface $operation) { parent::__construct($eventName); $this->composer = $composer; $this->io = $io; $this->devMode = $devMode; $this->localRepo = $localRepo; $this->operations = $operations; $this->operation = $operation; } /** * @return Composer */ public function getComposer() { return $this->composer; } /** * @return IOInterface */ public function getIO() { return $this->io; } /** * @return bool */ public function isDevMode() { return $this->devMode; } /** * @return RepositoryInterface */ public function getLocalRepo() { return $this->localRepo; } /** * @return OperationInterface[] */ public function getOperations() { return $this->operations; } /** * Returns the package instance. * * @return OperationInterface */ public function getOperation() { return $this->operation; } } * Jordi Boggiano * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Composer\Installer; use Composer\Package\PackageInterface; use Composer\Downloader\DownloadManager; use Composer\Repository\InstalledRepositoryInterface; use Composer\Util\Filesystem; /** * Project Installer is used to install a single package into a directory as * root project. * * @author Benjamin Eberlei */ class ProjectInstaller implements InstallerInterface { /** @var string */ private $installPath; /** @var DownloadManager */ private $downloadManager; /** @var Filesystem */ private $filesystem; /** * @param string $installPath */ public function __construct($installPath, DownloadManager $dm, Filesystem $fs) { $this->installPath = rtrim(strtr($installPath, '\\', '/'), '/').'/'; $this->downloadManager = $dm; $this->filesystem = $fs; } /** * Decides if the installer supports the given type * * @param string $packageType * @return bool */ public function supports($packageType) { return true; } /** * @inheritDoc */ public function isInstalled(InstalledRepositoryInterface $repo, PackageInterface $package) { return false; } /** * @inheritDoc */ public function download(PackageInterface $package, PackageInterface $prevPackage = null) { $installPath = $this->installPath; if (file_exists($installPath) && !$this->filesystem->isDirEmpty($installPath)) { throw new \InvalidArgumentException("Project directory $installPath is not empty."); } if (!is_dir($installPath)) { mkdir($installPath, 0777, true); } return $this->downloadManager->download($package, $installPath, $prevPackage); } /** * @inheritDoc */ public function prepare($type, PackageInterface $package, PackageInterface $prevPackage = null) { return $this->downloadManager->prepare($type, $package, $this->installPath, $prevPackage); } /** * @inheritDoc */ public function cleanup($type, PackageInterface $package, PackageInterface $prevPackage = null) { return $this->downloadManager->cleanup($type, $package, $this->installPath, $prevPackage); } /** * @inheritDoc */ public function install(InstalledRepositoryInterface $repo, PackageInterface $package) { return $this->downloadManager->install($package, $this->installPath); } /** * @inheritDoc */ public function update(InstalledRepositoryInterface $repo, PackageInterface $initial, PackageInterface $target) { throw new \InvalidArgumentException("not supported"); } /** * @inheritDoc */ public function uninstall(InstalledRepositoryInterface $repo, PackageInterface $package) { throw new \InvalidArgumentException("not supported"); } /** * Returns the installation path of a package * * @param PackageInterface $package * @return string path */ public function getInstallPath(PackageInterface $package) { return $this->installPath; } } * Jordi Boggiano * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Composer\Installer; use Composer\Package\PackageInterface; /** * Interface for the package installation manager that handle binary installation. * * @author Jordi Boggiano */ interface BinaryPresenceInterface { /** * Make sure binaries are installed for a given package. * * @param PackageInterface $package package instance * * @return void */ public function ensureBinariesPresence(PackageInterface $package); } * Jordi Boggiano * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Composer\Installer; use Composer\IO\IOInterface; use Composer\Package\PackageInterface; use Composer\Pcre\Preg; use Composer\Repository\InstalledRepository; use Symfony\Component\Console\Formatter\OutputFormatter; /** * Add suggested packages from different places to output them in the end. * * @author Haralan Dobrev */ class SuggestedPackagesReporter { const MODE_LIST = 1; const MODE_BY_PACKAGE = 2; const MODE_BY_SUGGESTION = 4; /** * @var array */ protected $suggestedPackages = array(); /** * @var IOInterface */ private $io; public function __construct(IOInterface $io) { $this->io = $io; } /** * @return array Suggested packages with source, target and reason keys. */ public function getPackages() { return $this->suggestedPackages; } /** * Add suggested packages to be listed after install * * Could be used to add suggested packages both from the installer * or from CreateProjectCommand. * * @param string $source Source package which made the suggestion * @param string $target Target package to be suggested * @param string $reason Reason the target package to be suggested * @return SuggestedPackagesReporter */ public function addPackage($source, $target, $reason) { $this->suggestedPackages[] = array( 'source' => $source, 'target' => $target, 'reason' => $reason, ); return $this; } /** * Add all suggestions from a package. * * @param PackageInterface $package * @return SuggestedPackagesReporter */ public function addSuggestionsFromPackage(PackageInterface $package) { $source = $package->getPrettyName(); foreach ($package->getSuggests() as $target => $reason) { $this->addPackage( $source, $target, $reason ); } return $this; } /** * Output suggested packages. * * Do not list the ones already installed if installed repository provided. * * @param int $mode One of the MODE_* constants from this class * @param InstalledRepository|null $installedRepo If passed in, suggested packages which are installed already will be skipped * @param PackageInterface|null $onlyDependentsOf If passed in, only the suggestions from direct dependents of that package, or from the package itself, will be shown * @return void */ public function output($mode, InstalledRepository $installedRepo = null, PackageInterface $onlyDependentsOf = null) { $suggestedPackages = $this->getFilteredSuggestions($installedRepo, $onlyDependentsOf); $suggesters = array(); $suggested = array(); foreach ($suggestedPackages as $suggestion) { $suggesters[$suggestion['source']][$suggestion['target']] = $suggestion['reason']; $suggested[$suggestion['target']][$suggestion['source']] = $suggestion['reason']; } ksort($suggesters); ksort($suggested); // Simple mode if ($mode & self::MODE_LIST) { foreach (array_keys($suggested) as $name) { $this->io->write(sprintf('%s', $name)); } return; } // Grouped by package if ($mode & self::MODE_BY_PACKAGE) { foreach ($suggesters as $suggester => $suggestions) { $this->io->write(sprintf('%s suggests:', $suggester)); foreach ($suggestions as $suggestion => $reason) { $this->io->write(sprintf(' - %s' . ($reason ? ': %s' : ''), $suggestion, $this->escapeOutput($reason))); } $this->io->write(''); } } // Grouped by suggestion if ($mode & self::MODE_BY_SUGGESTION) { // Improve readability in full mode if ($mode & self::MODE_BY_PACKAGE) { $this->io->write(str_repeat('-', 78)); } foreach ($suggested as $suggestion => $suggesters) { $this->io->write(sprintf('%s is suggested by:', $suggestion)); foreach ($suggesters as $suggester => $reason) { $this->io->write(sprintf(' - %s' . ($reason ? ': %s' : ''), $suggester, $this->escapeOutput($reason))); } $this->io->write(''); } } if ($onlyDependentsOf) { $allSuggestedPackages = $this->getFilteredSuggestions($installedRepo); $diff = count($allSuggestedPackages) - count($suggestedPackages); if ($diff) { $this->io->write(''.$diff.' additional suggestions by transitive dependencies can be shown with --all'); } } } /** * Output number of new suggested packages and a hint to use suggest command. * * @param InstalledRepository|null $installedRepo If passed in, suggested packages which are installed already will be skipped * @param PackageInterface|null $onlyDependentsOf If passed in, only the suggestions from direct dependents of that package, or from the package itself, will be shown * @return void */ public function outputMinimalistic(InstalledRepository $installedRepo = null, PackageInterface $onlyDependentsOf = null) { $suggestedPackages = $this->getFilteredSuggestions($installedRepo, $onlyDependentsOf); if ($suggestedPackages) { $this->io->writeError(''.count($suggestedPackages).' package suggestions were added by new dependencies, use `composer suggest` to see details.'); } } /** * @param InstalledRepository|null $installedRepo If passed in, suggested packages which are installed already will be skipped * @param PackageInterface|null $onlyDependentsOf If passed in, only the suggestions from direct dependents of that package, or from the package itself, will be shown * @return mixed[] */ private function getFilteredSuggestions(InstalledRepository $installedRepo = null, PackageInterface $onlyDependentsOf = null) { $suggestedPackages = $this->getPackages(); $installedNames = array(); if (null !== $installedRepo && !empty($suggestedPackages)) { foreach ($installedRepo->getPackages() as $package) { $installedNames = array_merge( $installedNames, $package->getNames() ); } } $sourceFilter = array(); if ($onlyDependentsOf) { $sourceFilter = array_map(function ($link) { return $link->getTarget(); }, array_merge($onlyDependentsOf->getRequires(), $onlyDependentsOf->getDevRequires())); $sourceFilter[] = $onlyDependentsOf->getName(); } $suggestions = array(); foreach ($suggestedPackages as $suggestion) { if (in_array($suggestion['target'], $installedNames) || ($sourceFilter && !in_array($suggestion['source'], $sourceFilter))) { continue; } $suggestions[] = $suggestion; } return $suggestions; } /** * @param string $string * @return string */ private function escapeOutput($string) { return OutputFormatter::escape( $this->removeControlCharacters($string) ); } /** * @param string $string * @return string */ private function removeControlCharacters($string) { return Preg::replace( '/[[:cntrl:]]/', '', str_replace("\n", ' ', $string) ); } } * Jordi Boggiano * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Composer\Installer; use Composer\IO\IOInterface; use Composer\IO\ConsoleIO; use Composer\Package\PackageInterface; use Composer\Package\AliasPackage; use Composer\Repository\InstalledRepositoryInterface; use Composer\DependencyResolver\Operation\OperationInterface; use Composer\DependencyResolver\Operation\InstallOperation; use Composer\DependencyResolver\Operation\UpdateOperation; use Composer\DependencyResolver\Operation\UninstallOperation; use Composer\DependencyResolver\Operation\MarkAliasInstalledOperation; use Composer\DependencyResolver\Operation\MarkAliasUninstalledOperation; use Composer\Downloader\FileDownloader; use Composer\EventDispatcher\EventDispatcher; use Composer\Util\Loop; use Composer\Util\Platform; use React\Promise\PromiseInterface; /** * Package operation manager. * * @author Konstantin Kudryashov * @author Jordi Boggiano * @author Nils Adermann */ class InstallationManager { /** @var array */ private $installers = array(); /** @var array */ private $cache = array(); /** @var array> */ private $notifiablePackages = array(); /** @var Loop */ private $loop; /** @var IOInterface */ private $io; /** @var ?EventDispatcher */ private $eventDispatcher; /** @var bool */ private $outputProgress; public function __construct(Loop $loop, IOInterface $io, EventDispatcher $eventDispatcher = null) { $this->loop = $loop; $this->io = $io; $this->eventDispatcher = $eventDispatcher; } /** * @return void */ public function reset() { $this->notifiablePackages = array(); FileDownloader::$downloadMetadata = array(); } /** * Adds installer * * @param InstallerInterface $installer installer instance * * @return void */ public function addInstaller(InstallerInterface $installer) { array_unshift($this->installers, $installer); $this->cache = array(); } /** * Removes installer * * @param InstallerInterface $installer installer instance * * @return void */ public function removeInstaller(InstallerInterface $installer) { if (false !== ($key = array_search($installer, $this->installers, true))) { array_splice($this->installers, $key, 1); $this->cache = array(); } } /** * Disables plugins. * * We prevent any plugins from being instantiated by simply * deactivating the installer for them. This ensure that no third-party * code is ever executed. * * @return void */ public function disablePlugins() { foreach ($this->installers as $i => $installer) { if (!$installer instanceof PluginInstaller) { continue; } unset($this->installers[$i]); } } /** * Returns installer for a specific package type. * * @param string $type package type * * @throws \InvalidArgumentException if installer for provided type is not registered * @return InstallerInterface */ public function getInstaller($type) { $type = strtolower($type); if (isset($this->cache[$type])) { return $this->cache[$type]; } foreach ($this->installers as $installer) { if ($installer->supports($type)) { return $this->cache[$type] = $installer; } } throw new \InvalidArgumentException('Unknown installer type: '.$type); } /** * Checks whether provided package is installed in one of the registered installers. * * @param InstalledRepositoryInterface $repo repository in which to check * @param PackageInterface $package package instance * * @return bool */ public function isPackageInstalled(InstalledRepositoryInterface $repo, PackageInterface $package) { if ($package instanceof AliasPackage) { return $repo->hasPackage($package) && $this->isPackageInstalled($repo, $package->getAliasOf()); } return $this->getInstaller($package->getType())->isInstalled($repo, $package); } /** * Install binary for the given package. * If the installer associated to this package doesn't handle that function, it'll do nothing. * * @param PackageInterface $package Package instance * * @return void */ public function ensureBinariesPresence(PackageInterface $package) { try { $installer = $this->getInstaller($package->getType()); } catch (\InvalidArgumentException $e) { // no installer found for the current package type (@see `getInstaller()`) return; } // if the given installer support installing binaries if ($installer instanceof BinaryPresenceInterface) { $installer->ensureBinariesPresence($package); } } /** * Executes solver operation. * * @param InstalledRepositoryInterface $repo repository in which to add/remove/update packages * @param OperationInterface[] $operations operations to execute * @param bool $devMode whether the install is being run in dev mode * @param bool $runScripts whether to dispatch script events * * @return void */ public function execute(InstalledRepositoryInterface $repo, array $operations, $devMode = true, $runScripts = true) { /** @var PromiseInterface[] */ $cleanupPromises = array(); $loop = $this->loop; $io = $this->io; $runCleanup = function () use (&$cleanupPromises, $loop) { $promises = array(); $loop->abortJobs(); foreach ($cleanupPromises as $cleanup) { $promises[] = new \React\Promise\Promise(function ($resolve, $reject) use ($cleanup) { $promise = $cleanup(); if (!$promise instanceof PromiseInterface) { $resolve(); } else { $promise->then(function () use ($resolve) { $resolve(); }); } }); } if (!empty($promises)) { $loop->wait($promises); } }; $handleInterruptsUnix = function_exists('pcntl_async_signals') && function_exists('pcntl_signal'); $handleInterruptsWindows = function_exists('sapi_windows_set_ctrl_handler') && PHP_SAPI === 'cli'; $prevHandler = null; $windowsHandler = null; if ($handleInterruptsUnix) { pcntl_async_signals(true); $prevHandler = pcntl_signal_get_handler(SIGINT); pcntl_signal(SIGINT, function ($sig) use ($runCleanup, $prevHandler, $io) { $io->writeError('Received SIGINT, aborting', true, IOInterface::DEBUG); $runCleanup(); if (!in_array($prevHandler, array(SIG_DFL, SIG_IGN), true)) { call_user_func($prevHandler, $sig); } exit(130); }); } if ($handleInterruptsWindows) { $windowsHandler = function ($event) use ($runCleanup, $io) { if ($event !== PHP_WINDOWS_EVENT_CTRL_C) { return; } $io->writeError('Received CTRL+C, aborting', true, IOInterface::DEBUG); $runCleanup(); exit(130); }; sapi_windows_set_ctrl_handler($windowsHandler); } try { // execute operations in batches to make sure download-modifying-plugins are installed // before the other packages get downloaded $batches = array(); $batch = array(); foreach ($operations as $index => $operation) { if ($operation instanceof UpdateOperation || $operation instanceof InstallOperation) { $package = $operation instanceof UpdateOperation ? $operation->getTargetPackage() : $operation->getPackage(); if ($package->getType() === 'composer-plugin' && ($extra = $package->getExtra()) && isset($extra['plugin-modifies-downloads']) && $extra['plugin-modifies-downloads'] === true) { if ($batch) { $batches[] = $batch; } $batches[] = array($index => $operation); $batch = array(); continue; } } $batch[$index] = $operation; } if ($batch) { $batches[] = $batch; } foreach ($batches as $batch) { $this->downloadAndExecuteBatch($repo, $batch, $cleanupPromises, $devMode, $runScripts, $operations); } } catch (\Exception $e) { $runCleanup(); if ($handleInterruptsUnix) { pcntl_signal(SIGINT, $prevHandler); } if ($handleInterruptsWindows) { sapi_windows_set_ctrl_handler($windowsHandler, false); } throw $e; } if ($handleInterruptsUnix) { pcntl_signal(SIGINT, $prevHandler); } if ($handleInterruptsWindows) { sapi_windows_set_ctrl_handler($windowsHandler, false); } // do a last write so that we write the repository even if nothing changed // as that can trigger an update of some files like InstalledVersions.php if // running a new composer version $repo->write($devMode, $this); } /** * @param OperationInterface[] $operations List of operations to execute in this batch * @param PromiseInterface[] $cleanupPromises * @param bool $devMode * @param bool $runScripts * @param OperationInterface[] $allOperations Complete list of operations to be executed in the install job, used for event listeners * * @return void */ private function downloadAndExecuteBatch(InstalledRepositoryInterface $repo, array $operations, array &$cleanupPromises, $devMode, $runScripts, array $allOperations) { $promises = array(); foreach ($operations as $index => $operation) { $opType = $operation->getOperationType(); // ignoring alias ops as they don't need to execute anything at this stage if (!in_array($opType, array('update', 'install', 'uninstall'))) { continue; } if ($opType === 'update') { /** @var UpdateOperation $operation */ $package = $operation->getTargetPackage(); $initialPackage = $operation->getInitialPackage(); } else { /** @var InstallOperation|MarkAliasInstalledOperation|MarkAliasUninstalledOperation|UninstallOperation $operation */ $package = $operation->getPackage(); $initialPackage = null; } $installer = $this->getInstaller($package->getType()); $cleanupPromises[$index] = function () use ($opType, $installer, $package, $initialPackage) { // avoid calling cleanup if the download was not even initialized for a package // as without installation source configured nothing will work if (!$package->getInstallationSource()) { return; } return $installer->cleanup($opType, $package, $initialPackage); }; if ($opType !== 'uninstall') { $promise = $installer->download($package, $initialPackage); if ($promise) { $promises[] = $promise; } } } // execute all downloads first if (count($promises)) { $this->waitOnPromises($promises); } // execute operations in batches to make sure every plugin is installed in the // right order and activated before the packages depending on it are installed $batches = array(); $batch = array(); foreach ($operations as $index => $operation) { if ($operation instanceof InstallOperation || $operation instanceof UpdateOperation) { $package = $operation instanceof UpdateOperation ? $operation->getTargetPackage() : $operation->getPackage(); if ($package->getType() === 'composer-plugin' || $package->getType() === 'composer-installer') { if ($batch) { $batches[] = $batch; } $batches[] = array($index => $operation); $batch = array(); continue; } } $batch[$index] = $operation; } if ($batch) { $batches[] = $batch; } foreach ($batches as $batch) { $this->executeBatch($repo, $batch, $cleanupPromises, $devMode, $runScripts, $allOperations); } } /** * @param OperationInterface[] $operations List of operations to execute in this batch * @param PromiseInterface[] $cleanupPromises * @param bool $devMode * @param bool $runScripts * @param OperationInterface[] $allOperations Complete list of operations to be executed in the install job, used for event listeners * * @return void */ private function executeBatch(InstalledRepositoryInterface $repo, array $operations, array $cleanupPromises, $devMode, $runScripts, array $allOperations) { $promises = array(); $postExecCallbacks = array(); foreach ($operations as $index => $operation) { $opType = $operation->getOperationType(); // ignoring alias ops as they don't need to execute anything if (!in_array($opType, array('update', 'install', 'uninstall'))) { // output alias ops in debug verbosity as they have no output otherwise if ($this->io->isDebug()) { $this->io->writeError(' - ' . $operation->show(false)); } $this->$opType($repo, $operation); continue; } if ($opType === 'update') { /** @var UpdateOperation $operation */ $package = $operation->getTargetPackage(); $initialPackage = $operation->getInitialPackage(); } else { /** @var InstallOperation|MarkAliasInstalledOperation|MarkAliasUninstalledOperation|UninstallOperation $operation */ $package = $operation->getPackage(); $initialPackage = null; } $installer = $this->getInstaller($package->getType()); $event = 'Composer\Installer\PackageEvents::PRE_PACKAGE_'.strtoupper($opType); if (defined($event) && $runScripts && $this->eventDispatcher) { $this->eventDispatcher->dispatchPackageEvent(constant($event), $devMode, $repo, $allOperations, $operation); } $dispatcher = $this->eventDispatcher; $installManager = $this; $io = $this->io; $promise = $installer->prepare($opType, $package, $initialPackage); if (!$promise instanceof PromiseInterface) { $promise = \React\Promise\resolve(); } $promise = $promise->then(function () use ($opType, $installManager, $repo, $operation) { return $installManager->$opType($repo, $operation); })->then($cleanupPromises[$index]) ->then(function () use ($installManager, $devMode, $repo) { $repo->write($devMode, $installManager); }, function ($e) use ($opType, $package, $io) { $io->writeError(' ' . ucfirst($opType) .' of '.$package->getPrettyName().' failed'); throw $e; }); $postExecCallbacks[] = function () use ($opType, $runScripts, $dispatcher, $devMode, $repo, $allOperations, $operation) { $event = 'Composer\Installer\PackageEvents::POST_PACKAGE_'.strtoupper($opType); if (defined($event) && $runScripts && $dispatcher) { $dispatcher->dispatchPackageEvent(constant($event), $devMode, $repo, $allOperations, $operation); } }; $promises[] = $promise; } // execute all prepare => installs/updates/removes => cleanup steps if (count($promises)) { $this->waitOnPromises($promises); } Platform::workaroundFilesystemIssues(); foreach ($postExecCallbacks as $cb) { $cb(); } } /** * @param PromiseInterface[] $promises * * @return void */ private function waitOnPromises(array $promises) { $progress = null; if ( $this->outputProgress && $this->io instanceof ConsoleIO && !Platform::getEnv('CI') && !$this->io->isDebug() && count($promises) > 1 ) { $progress = $this->io->getProgressBar(); } $this->loop->wait($promises, $progress); if ($progress) { $progress->clear(); // ProgressBar in non-decorated output does not output a final line-break and clear() does nothing if (!$this->io->isDecorated()) { $this->io->writeError(''); } } } /** * Executes install operation. * * @param InstalledRepositoryInterface $repo repository in which to check * @param InstallOperation $operation operation instance * * @return PromiseInterface|null */ public function install(InstalledRepositoryInterface $repo, InstallOperation $operation) { $package = $operation->getPackage(); $installer = $this->getInstaller($package->getType()); $promise = $installer->install($repo, $package); $this->markForNotification($package); return $promise; } /** * Executes update operation. * * @param InstalledRepositoryInterface $repo repository in which to check * @param UpdateOperation $operation operation instance * * @return PromiseInterface|null */ public function update(InstalledRepositoryInterface $repo, UpdateOperation $operation) { $initial = $operation->getInitialPackage(); $target = $operation->getTargetPackage(); $initialType = $initial->getType(); $targetType = $target->getType(); if ($initialType === $targetType) { $installer = $this->getInstaller($initialType); $promise = $installer->update($repo, $initial, $target); $this->markForNotification($target); } else { $promise = $this->getInstaller($initialType)->uninstall($repo, $initial); if (!$promise instanceof PromiseInterface) { $promise = \React\Promise\resolve(); } $installer = $this->getInstaller($targetType); $promise = $promise->then(function () use ($installer, $repo, $target) { return $installer->install($repo, $target); }); } return $promise; } /** * Uninstalls package. * * @param InstalledRepositoryInterface $repo repository in which to check * @param UninstallOperation $operation operation instance * * @return PromiseInterface|null */ public function uninstall(InstalledRepositoryInterface $repo, UninstallOperation $operation) { $package = $operation->getPackage(); $installer = $this->getInstaller($package->getType()); return $installer->uninstall($repo, $package); } /** * Executes markAliasInstalled operation. * * @param InstalledRepositoryInterface $repo repository in which to check * @param MarkAliasInstalledOperation $operation operation instance * * @return void */ public function markAliasInstalled(InstalledRepositoryInterface $repo, MarkAliasInstalledOperation $operation) { $package = $operation->getPackage(); if (!$repo->hasPackage($package)) { $repo->addPackage(clone $package); } } /** * Executes markAlias operation. * * @param InstalledRepositoryInterface $repo repository in which to check * @param MarkAliasUninstalledOperation $operation operation instance * * @return void */ public function markAliasUninstalled(InstalledRepositoryInterface $repo, MarkAliasUninstalledOperation $operation) { $package = $operation->getPackage(); $repo->removePackage($package); } /** * Returns the installation path of a package * * @param PackageInterface $package * @return string path */ public function getInstallPath(PackageInterface $package) { $installer = $this->getInstaller($package->getType()); return $installer->getInstallPath($package); } /** * @param bool $outputProgress * * @return void */ public function setOutputProgress($outputProgress) { $this->outputProgress = $outputProgress; } /** * @return void */ public function notifyInstalls(IOInterface $io) { $promises = array(); try { foreach ($this->notifiablePackages as $repoUrl => $packages) { // non-batch API, deprecated if (strpos($repoUrl, '%package%')) { foreach ($packages as $package) { $url = str_replace('%package%', $package->getPrettyName(), $repoUrl); $params = array( 'version' => $package->getPrettyVersion(), 'version_normalized' => $package->getVersion(), ); $opts = array( 'retry-auth-failure' => false, 'http' => array( 'method' => 'POST', 'header' => array('Content-type: application/x-www-form-urlencoded'), 'content' => http_build_query($params, '', '&'), 'timeout' => 3, ), ); $promises[] = $this->loop->getHttpDownloader()->add($url, $opts); } continue; } $postData = array('downloads' => array()); foreach ($packages as $package) { $packageNotification = array( 'name' => $package->getPrettyName(), 'version' => $package->getVersion(), ); if (strpos($repoUrl, 'packagist.org/') !== false) { if (isset(FileDownloader::$downloadMetadata[$package->getName()])) { $packageNotification['downloaded'] = FileDownloader::$downloadMetadata[$package->getName()]; } else { $packageNotification['downloaded'] = false; } } $postData['downloads'][] = $packageNotification; } $opts = array( 'retry-auth-failure' => false, 'http' => array( 'method' => 'POST', 'header' => array('Content-Type: application/json'), 'content' => json_encode($postData), 'timeout' => 6, ), ); $promises[] = $this->loop->getHttpDownloader()->add($repoUrl, $opts); } $this->loop->wait($promises); } catch (\Exception $e) { } $this->reset(); } /** * @return void */ private function markForNotification(PackageInterface $package) { if ($package->getNotificationUrl()) { $this->notifiablePackages[$package->getNotificationUrl()][$package->getName()] = $package; } } } * Jordi Boggiano * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Composer\Installer; class InstallerEvents { /** * The PRE_OPERATIONS_EXEC event occurs before the lock file gets * installed and operations are executed. * * The event listener method receives an Composer\Installer\InstallerEvent instance. * * @var string */ const PRE_OPERATIONS_EXEC = 'pre-operations-exec'; } * Jordi Boggiano * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Composer\Installer; /** * Package Events. * * @author Jordi Boggiano */ class PackageEvents { /** * The PRE_PACKAGE_INSTALL event occurs before a package is installed. * * The event listener method receives a Composer\Installer\PackageEvent instance. * * @var string */ const PRE_PACKAGE_INSTALL = 'pre-package-install'; /** * The POST_PACKAGE_INSTALL event occurs after a package is installed. * * The event listener method receives a Composer\Installer\PackageEvent instance. * * @var string */ const POST_PACKAGE_INSTALL = 'post-package-install'; /** * The PRE_PACKAGE_UPDATE event occurs before a package is updated. * * The event listener method receives a Composer\Installer\PackageEvent instance. * * @var string */ const PRE_PACKAGE_UPDATE = 'pre-package-update'; /** * The POST_PACKAGE_UPDATE event occurs after a package is updated. * * The event listener method receives a Composer\Installer\PackageEvent instance. * * @var string */ const POST_PACKAGE_UPDATE = 'post-package-update'; /** * The PRE_PACKAGE_UNINSTALL event occurs before a package has been uninstalled. * * The event listener method receives a Composer\Installer\PackageEvent instance. * * @var string */ const PRE_PACKAGE_UNINSTALL = 'pre-package-uninstall'; /** * The POST_PACKAGE_UNINSTALL event occurs after a package has been uninstalled. * * The event listener method receives a Composer\Installer\PackageEvent instance. * * @var string */ const POST_PACKAGE_UNINSTALL = 'post-package-uninstall'; } * Jordi Boggiano * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Composer\SelfUpdate; use Composer\Util\HttpDownloader; use Composer\Config; /** * @author Jordi Boggiano */ class Versions { /** @var string[] */ public static $channels = array('stable', 'preview', 'snapshot', '1', '2'); /** @var HttpDownloader */ private $httpDownloader; /** @var Config */ private $config; /** @var string */ private $channel; /** @var array> */ private $versionsData; public function __construct(Config $config, HttpDownloader $httpDownloader) { $this->httpDownloader = $httpDownloader; $this->config = $config; } /** * @return string */ public function getChannel() { if ($this->channel) { return $this->channel; } $channelFile = $this->config->get('home').'/update-channel'; if (file_exists($channelFile)) { $channel = trim(file_get_contents($channelFile)); if (in_array($channel, array('stable', 'preview', 'snapshot'), true)) { return $this->channel = $channel; } } return $this->channel = 'stable'; } /** * @param string $channel * * @return void */ public function setChannel($channel) { if (!in_array($channel, self::$channels, true)) { throw new \InvalidArgumentException('Invalid channel '.$channel.', must be one of: ' . implode(', ', self::$channels)); } $channelFile = $this->config->get('home').'/update-channel'; $this->channel = $channel; file_put_contents($channelFile, (is_numeric($channel) ? 'stable' : $channel).PHP_EOL); } /** * @param string|null $channel * * @return array{path: string, version: string, min-php: int} */ public function getLatest($channel = null) { $versions = $this->getVersionsData(); foreach ($versions[$channel ?: $this->getChannel()] as $version) { if ($version['min-php'] <= PHP_VERSION_ID) { return $version; } } throw new \UnexpectedValueException('There is no version of Composer available for your PHP version ('.PHP_VERSION.')'); } /** * @return array> */ private function getVersionsData() { if (!$this->versionsData) { if ($this->config->get('disable-tls') === true) { $protocol = 'http'; } else { $protocol = 'https'; } $this->versionsData = $this->httpDownloader->get($protocol . '://getcomposer.org/versions')->decodeJson(); } return $this->versionsData; } } * Jordi Boggiano * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Composer\SelfUpdate; use Composer\Pcre\Preg; /** * @author Jordi Boggiano */ class Keys { /** * @param string $path * * @return string */ public static function fingerprint($path) { $hash = strtoupper(hash('sha256', Preg::replace('{\s}', '', file_get_contents($path)))); return implode(' ', array( substr($hash, 0, 8), substr($hash, 8, 8), substr($hash, 16, 8), substr($hash, 24, 8), '', // Extra space substr($hash, 32, 8), substr($hash, 40, 8), substr($hash, 48, 8), substr($hash, 56, 8), )); } } ignoreRegex = BasePackage::packageNamesToRegexp($ignoreAll); $this->ignoreUpperBoundRegex = BasePackage::packageNamesToRegexp($ignoreUpperBound); } /** * @param string $req * @return bool */ public function isIgnored($req) { if (!PlatformRepository::isPlatformPackage($req)) { return false; } return Preg::isMatch($this->ignoreRegex, $req); } /** * @param string $req * @return ConstraintInterface */ public function filterConstraint($req, ConstraintInterface $constraint) { if (!PlatformRepository::isPlatformPackage($req)) { return $constraint; } if (!Preg::isMatch($this->ignoreUpperBoundRegex, $req)) { return $constraint; } if (Preg::isMatch($this->ignoreRegex, $req)) { return new MatchAllConstraint; } $intervals = Intervals::get($constraint); $last = end($intervals['numeric']); if ($last !== false && (string) $last->getEnd() !== (string) Interval::untilPositiveInfinity()) { $constraint = new MultiConstraint(array($constraint, new Constraint('>=', $last->getEnd()->getVersion())), false); } return $constraint; } } * Jordi Boggiano * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Composer\EventDispatcher; /** * @author Jordi Boggiano */ class ScriptExecutionException extends \RuntimeException { } * Jordi Boggiano * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Composer\EventDispatcher; /** * The base event class * * @author Nils Adermann */ class Event { /** * @var string This event's name */ protected $name; /** * @var string[] Arguments passed by the user, these will be forwarded to CLI script handlers */ protected $args; /** * @var mixed[] Flags usable in PHP script handlers */ protected $flags; /** * @var bool Whether the event should not be passed to more listeners */ private $propagationStopped = false; /** * Constructor. * * @param string $name The event name * @param string[] $args Arguments passed by the user * @param mixed[] $flags Optional flags to pass data not as argument */ public function __construct($name, array $args = array(), array $flags = array()) { $this->name = $name; $this->args = $args; $this->flags = $flags; } /** * Returns the event's name. * * @return string The event name */ public function getName() { return $this->name; } /** * Returns the event's arguments. * * @return string[] The event arguments */ public function getArguments() { return $this->args; } /** * Returns the event's flags. * * @return mixed[] The event flags */ public function getFlags() { return $this->flags; } /** * Checks if stopPropagation has been called * * @return bool Whether propagation has been stopped */ public function isPropagationStopped() { return $this->propagationStopped; } /** * Prevents the event from being passed to further listeners * * @return void */ public function stopPropagation() { $this->propagationStopped = true; } } * Jordi Boggiano * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Composer\EventDispatcher; /** * An EventSubscriber knows which events it is interested in. * * If an EventSubscriber is added to an EventDispatcher, the manager invokes * {@link getSubscribedEvents} and registers the subscriber as a listener for all * returned events. * * @author Guilherme Blanco * @author Jonathan Wage * @author Roman Borschel * @author Bernhard Schussek */ interface EventSubscriberInterface { /** * Returns an array of event names this subscriber wants to listen to. * * The array keys are event names and the value can be: * * * The method name to call (priority defaults to 0) * * An array composed of the method name to call and the priority * * An array of arrays composed of the method names to call and respective * priorities, or 0 if unset * * For instance: * * * array('eventName' => 'methodName') * * array('eventName' => array('methodName', $priority)) * * array('eventName' => array(array('methodName1', $priority), array('methodName2')) * * @return array> The event names to listen to */ public static function getSubscribedEvents(); } * Jordi Boggiano * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Composer\EventDispatcher; use Composer\DependencyResolver\Transaction; use Composer\Installer\InstallerEvent; use Composer\IO\IOInterface; use Composer\Composer; use Composer\Pcre\Preg; use Composer\Util\Platform; use Composer\DependencyResolver\Operation\OperationInterface; use Composer\Repository\RepositoryInterface; use Composer\Script; use Composer\Installer\PackageEvent; use Composer\Installer\BinaryInstaller; use Composer\Util\ProcessExecutor; use Composer\Script\Event as ScriptEvent; use Composer\Autoload\ClassLoader; use Symfony\Component\Process\PhpExecutableFinder; use Symfony\Component\Process\ExecutableFinder; /** * The Event Dispatcher. * * Example in command: * $dispatcher = new EventDispatcher($this->getComposer(), $this->getApplication()->getIO()); * // ... * $dispatcher->dispatch(ScriptEvents::POST_INSTALL_CMD); * // ... * * @author François Pluchino * @author Jordi Boggiano * @author Nils Adermann */ class EventDispatcher { /** @var Composer */ protected $composer; /** @var IOInterface */ protected $io; /** @var ?ClassLoader */ protected $loader; /** @var ProcessExecutor */ protected $process; /** @var array>> */ protected $listeners = array(); /** @var bool */ protected $runScripts = true; /** @var list */ private $eventStack; /** * Constructor. * * @param Composer $composer The composer instance * @param IOInterface $io The IOInterface instance * @param ProcessExecutor $process */ public function __construct(Composer $composer, IOInterface $io, ProcessExecutor $process = null) { $this->composer = $composer; $this->io = $io; $this->process = $process ?: new ProcessExecutor($io); $this->eventStack = array(); } /** * Set whether script handlers are active or not * * @param bool $runScripts * @return $this */ public function setRunScripts($runScripts = true) { $this->runScripts = (bool) $runScripts; return $this; } /** * Dispatch an event * * @param string $eventName An event name * @param Event $event * @return int return code of the executed script if any, for php scripts a false return * value is changed to 1, anything else to 0 */ public function dispatch($eventName, Event $event = null) { if (null === $event) { $event = new Event($eventName); } return $this->doDispatch($event); } /** * Dispatch a script event. * * @param string $eventName The constant in ScriptEvents * @param bool $devMode * @param array $additionalArgs Arguments passed by the user * @param array $flags Optional flags to pass data not as argument * @return int return code of the executed script if any, for php scripts a false return * value is changed to 1, anything else to 0 */ public function dispatchScript($eventName, $devMode = false, $additionalArgs = array(), $flags = array()) { return $this->doDispatch(new Script\Event($eventName, $this->composer, $this->io, $devMode, $additionalArgs, $flags)); } /** * Dispatch a package event. * * @param string $eventName The constant in PackageEvents * @param bool $devMode Whether or not we are in dev mode * @param RepositoryInterface $localRepo The installed repository * @param OperationInterface[] $operations The list of operations * @param OperationInterface $operation The package being installed/updated/removed * * @return int return code of the executed script if any, for php scripts a false return * value is changed to 1, anything else to 0 */ public function dispatchPackageEvent($eventName, $devMode, RepositoryInterface $localRepo, array $operations, OperationInterface $operation) { return $this->doDispatch(new PackageEvent($eventName, $this->composer, $this->io, $devMode, $localRepo, $operations, $operation)); } /** * Dispatch a installer event. * * @param string $eventName The constant in InstallerEvents * @param bool $devMode Whether or not we are in dev mode * @param bool $executeOperations True if operations will be executed, false in --dry-run * @param Transaction $transaction The transaction contains the list of operations * * @return int return code of the executed script if any, for php scripts a false return * value is changed to 1, anything else to 0 */ public function dispatchInstallerEvent($eventName, $devMode, $executeOperations, Transaction $transaction) { return $this->doDispatch(new InstallerEvent($eventName, $this->composer, $this->io, $devMode, $executeOperations, $transaction)); } /** * Triggers the listeners of an event. * * @param Event $event The event object to pass to the event handlers/listeners. * @throws \RuntimeException|\Exception * @return int return code of the executed script if any, for php scripts a false return * value is changed to 1, anything else to 0 */ protected function doDispatch(Event $event) { if (Platform::getEnv('COMPOSER_DEBUG_EVENTS')) { $details = null; if ($event instanceof PackageEvent) { $details = (string) $event->getOperation(); } $this->io->writeError('Dispatching '.$event->getName().''.($details ? ' ('.$details.')' : '').' event'); } $listeners = $this->getListeners($event); $this->pushEvent($event); try { $returnMax = 0; foreach ($listeners as $callable) { $return = 0; $this->ensureBinDirIsInPath(); if (!is_string($callable)) { if (!is_callable($callable)) { $className = is_object($callable[0]) ? get_class($callable[0]) : $callable[0]; throw new \RuntimeException('Subscriber '.$className.'::'.$callable[1].' for event '.$event->getName().' is not callable, make sure the function is defined and public'); } if (is_array($callable) && (is_string($callable[0]) || is_object($callable[0])) && is_string($callable[1])) { $this->io->writeError(sprintf('> %s: %s', $event->getName(), (is_object($callable[0]) ? get_class($callable[0]) : $callable[0]).'->'.$callable[1]), true, IOInterface::VERBOSE); } $return = false === call_user_func($callable, $event) ? 1 : 0; } elseif ($this->isComposerScript($callable)) { $this->io->writeError(sprintf('> %s: %s', $event->getName(), $callable), true, IOInterface::VERBOSE); $script = explode(' ', substr($callable, 1)); $scriptName = $script[0]; unset($script[0]); $args = array_merge($script, $event->getArguments()); $flags = $event->getFlags(); if (strpos($callable, '@composer ') === 0) { $exec = $this->getPhpExecCommand() . ' ' . ProcessExecutor::escape(Platform::getEnv('COMPOSER_BINARY')) . ' ' . implode(' ', $args); if (0 !== ($exitCode = $this->executeTty($exec))) { $this->io->writeError(sprintf('Script %s handling the %s event returned with error code '.$exitCode.'', $callable, $event->getName()), true, IOInterface::QUIET); throw new ScriptExecutionException('Error Output: '.$this->process->getErrorOutput(), $exitCode); } } else { if (!$this->getListeners(new Event($scriptName))) { $this->io->writeError(sprintf('You made a reference to a non-existent script %s', $callable), true, IOInterface::QUIET); } try { /** @var InstallerEvent $event */ $scriptEvent = new Script\Event($scriptName, $event->getComposer(), $event->getIO(), $event->isDevMode(), $args, $flags); $scriptEvent->setOriginatingEvent($event); $return = $this->dispatch($scriptName, $scriptEvent); } catch (ScriptExecutionException $e) { $this->io->writeError(sprintf('Script %s was called via %s', $callable, $event->getName()), true, IOInterface::QUIET); throw $e; } } } elseif ($this->isPhpScript($callable)) { $className = substr($callable, 0, strpos($callable, '::')); $methodName = substr($callable, strpos($callable, '::') + 2); if (!class_exists($className)) { $this->io->writeError('Class '.$className.' is not autoloadable, can not call '.$event->getName().' script', true, IOInterface::QUIET); continue; } if (!is_callable($callable)) { $this->io->writeError('Method '.$callable.' is not callable, can not call '.$event->getName().' script', true, IOInterface::QUIET); continue; } try { $return = false === $this->executeEventPhpScript($className, $methodName, $event) ? 1 : 0; } catch (\Exception $e) { $message = "Script %s handling the %s event terminated with an exception"; $this->io->writeError(''.sprintf($message, $callable, $event->getName()).'', true, IOInterface::QUIET); throw $e; } } else { $args = implode(' ', array_map(array('Composer\Util\ProcessExecutor', 'escape'), $event->getArguments())); $exec = $callable . ($args === '' ? '' : ' '.$args); if ($this->io->isVerbose()) { $this->io->writeError(sprintf('> %s: %s', $event->getName(), $exec)); } elseif ($event->getName() !== '__exec_command') { // do not output the command being run when using `composer exec` as it is fairly obvious the user is running it $this->io->writeError(sprintf('> %s', $exec)); } $possibleLocalBinaries = $this->composer->getPackage()->getBinaries(); if ($possibleLocalBinaries) { foreach ($possibleLocalBinaries as $localExec) { if (Preg::isMatch('{\b'.preg_quote($callable).'$}', $localExec)) { $caller = BinaryInstaller::determineBinaryCaller($localExec); $exec = Preg::replace('{^'.preg_quote($callable).'}', $caller . ' ' . $localExec, $exec); break; } } } if (strpos($exec, '@putenv ') === 0) { if (false === strpos($exec, '=')) { Platform::clearEnv(substr($exec, 8)); } else { list($var, $value) = explode('=', substr($exec, 8), 2); Platform::putEnv($var, $value); } continue; } if (strpos($exec, '@php ') === 0) { $pathAndArgs = substr($exec, 5); if (Platform::isWindows()) { $pathAndArgs = Preg::replaceCallback('{^\S+}', function ($path) { return str_replace('/', '\\', $path[0]); }, $pathAndArgs); } // match somename (not in quote, and not a qualified path) and if it is not a valid path from CWD then try to find it // in $PATH. This allows support for `@php foo` where foo is a binary name found in PATH but not an actual relative path $matched = Preg::isMatch('{^[^\'"\s/\\\\]+}', $pathAndArgs, $match); if ($matched && !file_exists($match[0])) { $finder = new ExecutableFinder; if ($pathToExec = $finder->find($match[0])) { $pathAndArgs = $pathToExec . substr($pathAndArgs, strlen($match[0])); } } $exec = $this->getPhpExecCommand() . ' ' . $pathAndArgs; } else { $finder = new PhpExecutableFinder(); $phpPath = $finder->find(false); if ($phpPath) { Platform::putEnv('PHP_BINARY', $phpPath); } if (Platform::isWindows()) { $exec = Preg::replaceCallback('{^\S+}', function ($path) { return str_replace('/', '\\', $path[0]); }, $exec); } } // if composer is being executed, make sure it runs the expected composer from current path // resolution, even if bin-dir contains composer too because the project requires composer/composer // see https://github.com/composer/composer/issues/8748 if (strpos($exec, 'composer ') === 0) { $exec = $this->getPhpExecCommand() . ' ' . ProcessExecutor::escape(Platform::getEnv('COMPOSER_BINARY')) . substr($exec, 8); } if (0 !== ($exitCode = $this->executeTty($exec))) { $this->io->writeError(sprintf('Script %s handling the %s event returned with error code '.$exitCode.'', $callable, $event->getName()), true, IOInterface::QUIET); throw new ScriptExecutionException('Error Output: '.$this->process->getErrorOutput(), $exitCode); } } $returnMax = max($returnMax, $return); if ($event->isPropagationStopped()) { break; } } } catch (\Exception $e) { // TODO Composer 2.2 turn all this into a finally $this->popEvent(); throw $e; } catch (\Throwable $e) { $this->popEvent(); throw $e; } $this->popEvent(); return $returnMax; } /** * @param string $exec * * @return int */ protected function executeTty($exec) { if ($this->io->isInteractive()) { return $this->process->executeTty($exec); } return $this->process->execute($exec); } /** * @return string */ protected function getPhpExecCommand() { $finder = new PhpExecutableFinder(); $phpPath = $finder->find(false); if (!$phpPath) { throw new \RuntimeException('Failed to locate PHP binary to execute '.$phpPath); } $phpArgs = $finder->findArguments(); $phpArgs = $phpArgs ? ' ' . implode(' ', $phpArgs) : ''; $allowUrlFOpenFlag = ' -d allow_url_fopen=' . ProcessExecutor::escape(ini_get('allow_url_fopen')); $disableFunctionsFlag = ' -d disable_functions=' . ProcessExecutor::escape(ini_get('disable_functions')); $memoryLimitFlag = ' -d memory_limit=' . ProcessExecutor::escape(ini_get('memory_limit')); return ProcessExecutor::escape($phpPath) . $phpArgs . $allowUrlFOpenFlag . $disableFunctionsFlag . $memoryLimitFlag; } /** * @param string $className * @param string $methodName * @param Event $event Event invoking the PHP callable * * @return mixed */ protected function executeEventPhpScript($className, $methodName, Event $event) { if ($this->io->isVerbose()) { $this->io->writeError(sprintf('> %s: %s::%s', $event->getName(), $className, $methodName)); } else { $this->io->writeError(sprintf('> %s::%s', $className, $methodName)); } return $className::$methodName($event); } /** * Add a listener for a particular event * * @param string $eventName The event name - typically a constant * @param callable $listener A callable expecting an event argument * @param int $priority A higher value represents a higher priority * * @return void */ public function addListener($eventName, $listener, $priority = 0) { $this->listeners[$eventName][$priority][] = $listener; } /** * @param callable|object $listener A callable or an object instance for which all listeners should be removed * * @return void */ public function removeListener($listener) { foreach ($this->listeners as $eventName => $priorities) { foreach ($priorities as $priority => $listeners) { foreach ($listeners as $index => $candidate) { if ($listener === $candidate || (is_array($candidate) && is_object($listener) && $candidate[0] === $listener)) { unset($this->listeners[$eventName][$priority][$index]); } } } } } /** * Adds object methods as listeners for the events in getSubscribedEvents * * @see EventSubscriberInterface * * @param EventSubscriberInterface $subscriber * * @return void */ public function addSubscriber(EventSubscriberInterface $subscriber) { foreach ($subscriber->getSubscribedEvents() as $eventName => $params) { if (is_string($params)) { $this->addListener($eventName, array($subscriber, $params)); } elseif (is_string($params[0])) { $this->addListener($eventName, array($subscriber, $params[0]), isset($params[1]) ? $params[1] : 0); } else { foreach ($params as $listener) { $this->addListener($eventName, array($subscriber, $listener[0]), isset($listener[1]) ? $listener[1] : 0); } } } } /** * Retrieves all listeners for a given event * * @param Event $event * @return array All listeners: callables and scripts */ protected function getListeners(Event $event) { $scriptListeners = $this->runScripts ? $this->getScriptListeners($event) : array(); if (!isset($this->listeners[$event->getName()][0])) { $this->listeners[$event->getName()][0] = array(); } krsort($this->listeners[$event->getName()]); $listeners = $this->listeners; $listeners[$event->getName()][0] = array_merge($listeners[$event->getName()][0], $scriptListeners); return call_user_func_array('array_merge', $listeners[$event->getName()]); } /** * Checks if an event has listeners registered * * @param Event $event * @return bool */ public function hasEventListeners(Event $event) { $listeners = $this->getListeners($event); return count($listeners) > 0; } /** * Finds all listeners defined as scripts in the package * * @param Event $event Event object * @return string[] Listeners */ protected function getScriptListeners(Event $event) { $package = $this->composer->getPackage(); $scripts = $package->getScripts(); if (empty($scripts[$event->getName()])) { return array(); } if ($this->loader) { $this->loader->unregister(); } $generator = $this->composer->getAutoloadGenerator(); if ($event instanceof ScriptEvent) { $generator->setDevMode($event->isDevMode()); } $packages = $this->composer->getRepositoryManager()->getLocalRepository()->getCanonicalPackages(); $packageMap = $generator->buildPackageMap($this->composer->getInstallationManager(), $package, $packages); $map = $generator->parseAutoloads($packageMap, $package); $this->loader = $generator->createLoader($map, $this->composer->getConfig()->get('vendor-dir')); $this->loader->register(false); return $scripts[$event->getName()]; } /** * Checks if string given references a class path and method * * @param string $callable * @return bool */ protected function isPhpScript($callable) { return false === strpos($callable, ' ') && false !== strpos($callable, '::'); } /** * Checks if string given references a composer run-script * * @param string $callable * @return bool */ protected function isComposerScript($callable) { return strpos($callable, '@') === 0 && strpos($callable, '@php ') !== 0 && strpos($callable, '@putenv ') !== 0; } /** * Push an event to the stack of active event * * @param Event $event * @throws \RuntimeException * @return int */ protected function pushEvent(Event $event) { $eventName = $event->getName(); if (in_array($eventName, $this->eventStack)) { throw new \RuntimeException(sprintf("Circular call to script handler '%s' detected", $eventName)); } return array_push($this->eventStack, $eventName); } /** * Pops the active event from the stack * * @return string|null */ protected function popEvent() { return array_pop($this->eventStack); } /** * @return void */ private function ensureBinDirIsInPath() { $pathEnv = 'PATH'; if (false === Platform::getEnv('PATH') && false !== Platform::getEnv('Path')) { $pathEnv = 'Path'; } // add the bin dir to the PATH to make local binaries of deps usable in scripts $binDir = $this->composer->getConfig()->get('bin-dir'); if (is_dir($binDir)) { $binDir = realpath($binDir); $pathValue = Platform::getEnv($pathEnv); if (!Preg::isMatch('{(^|'.PATH_SEPARATOR.')'.preg_quote($binDir).'($|'.PATH_SEPARATOR.')}', $pathValue)) { Platform::putEnv($pathEnv, $binDir.PATH_SEPARATOR.$pathValue); } } } } * Jordi Boggiano * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Composer\DependencyResolver; /** * The RuleWatchGraph efficiently propagates decisions to other rules * * All rules generated for solving a SAT problem should be inserted into the * graph. When a decision on a literal is made, the graph can be used to * propagate the decision to all other rules involving the literal, leading to * other trivial decisions resulting from unit clauses. * * @author Nils Adermann */ class RuleWatchGraph { /** @var array */ protected $watchChains = array(); /** * Inserts a rule node into the appropriate chains within the graph * * The node is prepended to the watch chains for each of the two literals it * watches. * * Assertions are skipped because they only depend on a single package and * have no alternative literal that could be true, so there is no need to * watch changes in any literals. * * @param RuleWatchNode $node The rule node to be inserted into the graph * @return void */ public function insert(RuleWatchNode $node) { if ($node->getRule()->isAssertion()) { return; } if (!$node->getRule() instanceof MultiConflictRule) { foreach (array($node->watch1, $node->watch2) as $literal) { if (!isset($this->watchChains[$literal])) { $this->watchChains[$literal] = new RuleWatchChain; } $this->watchChains[$literal]->unshift($node); } } else { foreach ($node->getRule()->getLiterals() as $literal) { if (!isset($this->watchChains[$literal])) { $this->watchChains[$literal] = new RuleWatchChain; } $this->watchChains[$literal]->unshift($node); } } } /** * Propagates a decision on a literal to all rules watching the literal * * If a decision, e.g. +A has been made, then all rules containing -A, e.g. * (-A|+B|+C) now need to satisfy at least one of the other literals, so * that the rule as a whole becomes true, since with +A applied the rule * is now (false|+B|+C) so essentially (+B|+C). * * This means that all rules watching the literal -A need to be updated to * watch 2 other literals which can still be satisfied instead. So literals * that conflict with previously made decisions are not an option. * * Alternatively it can occur that a unit clause results: e.g. if in the * above example the rule was (-A|+B), then A turning true means that * B must now be decided true as well. * * @param int $decidedLiteral The literal which was decided (A in our example) * @param int $level The level at which the decision took place and at which * all resulting decisions should be made. * @param Decisions $decisions Used to check previous decisions and to * register decisions resulting from propagation * @return Rule|null If a conflict is found the conflicting rule is returned */ public function propagateLiteral($decidedLiteral, $level, Decisions $decisions) { // we invert the decided literal here, example: // A was decided => (-A|B) now requires B to be true, so we look for // rules which are fulfilled by -A, rather than A. $literal = -$decidedLiteral; if (!isset($this->watchChains[$literal])) { return null; } $chain = $this->watchChains[$literal]; $chain->rewind(); while ($chain->valid()) { $node = $chain->current(); if (!$node->getRule() instanceof MultiConflictRule) { $otherWatch = $node->getOtherWatch($literal); if (!$node->getRule()->isDisabled() && !$decisions->satisfy($otherWatch)) { $ruleLiterals = $node->getRule()->getLiterals(); $alternativeLiterals = array_filter($ruleLiterals, function ($ruleLiteral) use ($literal, $otherWatch, $decisions) { return $literal !== $ruleLiteral && $otherWatch !== $ruleLiteral && !$decisions->conflict($ruleLiteral); }); if ($alternativeLiterals) { reset($alternativeLiterals); $this->moveWatch($literal, current($alternativeLiterals), $node); continue; } if ($decisions->conflict($otherWatch)) { return $node->getRule(); } $decisions->decide($otherWatch, $level, $node->getRule()); } } else { foreach ($node->getRule()->getLiterals() as $otherLiteral) { if ($literal !== $otherLiteral && !$decisions->satisfy($otherLiteral)) { if ($decisions->conflict($otherLiteral)) { return $node->getRule(); } $decisions->decide($otherLiteral, $level, $node->getRule()); } } } $chain->next(); } return null; } /** * Moves a rule node from one watch chain to another * * The rule node's watched literals are updated accordingly. * * @param int $fromLiteral A literal the node used to watch * @param int $toLiteral A literal the node should watch now * @param RuleWatchNode $node The rule node to be moved * @return void */ protected function moveWatch($fromLiteral, $toLiteral, RuleWatchNode $node) { if (!isset($this->watchChains[$toLiteral])) { $this->watchChains[$toLiteral] = new RuleWatchChain; } $node->moveWatch($fromLiteral, $toLiteral); $this->watchChains[$fromLiteral]->remove(); $this->watchChains[$toLiteral]->unshift($node); } } * Jordi Boggiano * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Composer\DependencyResolver; use Composer\Package\AliasPackage; use Composer\Package\BasePackage; use Composer\Package\Version\VersionParser; use Composer\Semver\CompilingMatcher; use Composer\Semver\Constraint\ConstraintInterface; use Composer\Semver\Constraint\Constraint; use Composer\Semver\Constraint\MultiConstraint; use Composer\Semver\Intervals; /** * Optimizes a given pool * * @author Yanick Witschi */ class PoolOptimizer { /** * @var PolicyInterface */ private $policy; /** * @var array */ private $irremovablePackages = array(); /** * @var array> */ private $requireConstraintsPerPackage = array(); /** * @var array> */ private $conflictConstraintsPerPackage = array(); /** * @var array */ private $packagesToRemove = array(); /** * @var array */ private $aliasesPerPackage = array(); /** * @var array> */ private $removedVersionsByPackage = array(); public function __construct(PolicyInterface $policy) { $this->policy = $policy; } /** * @return Pool */ public function optimize(Request $request, Pool $pool) { $this->prepare($request, $pool); $this->optimizeByIdenticalDependencies($request, $pool); $this->optimizeImpossiblePackagesAway($request, $pool); $optimizedPool = $this->applyRemovalsToPool($pool); // No need to run this recursively at the moment // because the current optimizations cannot provide // even more gains when ran again. Might change // in the future with additional optimizations. $this->irremovablePackages = array(); $this->requireConstraintsPerPackage = array(); $this->conflictConstraintsPerPackage = array(); $this->packagesToRemove = array(); $this->aliasesPerPackage = array(); $this->removedVersionsByPackage = array(); return $optimizedPool; } /** * @return void */ private function prepare(Request $request, Pool $pool) { $irremovablePackageConstraintGroups = array(); // Mark fixed or locked packages as irremovable foreach ($request->getFixedOrLockedPackages() as $package) { $irremovablePackageConstraintGroups[$package->getName()][] = new Constraint('==', $package->getVersion()); } // Extract requested package requirements foreach ($request->getRequires() as $require => $constraint) { $constraint = Intervals::compactConstraint($constraint); $this->requireConstraintsPerPackage[$require][(string) $constraint] = $constraint; } // First pass over all packages to extract information and mark package constraints irremovable foreach ($pool->getPackages() as $package) { // Extract package requirements foreach ($package->getRequires() as $link) { $constraint = Intervals::compactConstraint($link->getConstraint()); $this->requireConstraintsPerPackage[$link->getTarget()][(string) $constraint] = $constraint; } // Extract package conflicts foreach ($package->getConflicts() as $link) { $constraint = Intervals::compactConstraint($link->getConstraint()); $this->conflictConstraintsPerPackage[$link->getTarget()][(string) $constraint] = $constraint; } // Keep track of alias packages for every package so if either the alias or aliased is kept // we keep the others as they are a unit of packages really if ($package instanceof AliasPackage) { $this->aliasesPerPackage[$package->getAliasOf()->id][] = $package; } } $irremovablePackageConstraints = array(); foreach ($irremovablePackageConstraintGroups as $packageName => $constraints) { $irremovablePackageConstraints[$packageName] = 1 === \count($constraints) ? $constraints[0] : new MultiConstraint($constraints, false); } unset($irremovablePackageConstraintGroups); // Mark the packages as irremovable based on the constraints foreach ($pool->getPackages() as $package) { if (!isset($irremovablePackageConstraints[$package->getName()])) { continue; } if (CompilingMatcher::match($irremovablePackageConstraints[$package->getName()], Constraint::OP_EQ, $package->getVersion())) { $this->markPackageIrremovable($package); } } } /** * @return void */ private function markPackageIrremovable(BasePackage $package) { $this->irremovablePackages[$package->id] = true; if ($package instanceof AliasPackage) { // recursing here so aliasesPerPackage for the aliasOf can be checked // and all its aliases marked as irremovable as well $this->markPackageIrremovable($package->getAliasOf()); } if (isset($this->aliasesPerPackage[$package->id])) { foreach ($this->aliasesPerPackage[$package->id] as $aliasPackage) { $this->irremovablePackages[$aliasPackage->id] = true; } } } /** * @return Pool Optimized pool */ private function applyRemovalsToPool(Pool $pool) { $packages = array(); $removedVersions = array(); foreach ($pool->getPackages() as $package) { if (!isset($this->packagesToRemove[$package->id])) { $packages[] = $package; } else { $removedVersions[$package->getName()][$package->getVersion()] = $package->getPrettyVersion(); } } $optimizedPool = new Pool($packages, $pool->getUnacceptableFixedOrLockedPackages(), $removedVersions, $this->removedVersionsByPackage); return $optimizedPool; } /** * @return void */ private function optimizeByIdenticalDependencies(Request $request, Pool $pool) { $identicalDefinitionsPerPackage = array(); $packageIdenticalDefinitionLookup = array(); foreach ($pool->getPackages() as $package) { // If that package was already marked irremovable, we can skip // the entire process for it if (isset($this->irremovablePackages[$package->id])) { continue; } $this->markPackageForRemoval($package->id); $dependencyHash = $this->calculateDependencyHash($package); foreach ($package->getNames(false) as $packageName) { if (!isset($this->requireConstraintsPerPackage[$packageName])) { continue; } foreach ($this->requireConstraintsPerPackage[$packageName] as $requireConstraint) { $groupHashParts = array(); if (CompilingMatcher::match($requireConstraint, Constraint::OP_EQ, $package->getVersion())) { $groupHashParts[] = 'require:' . (string) $requireConstraint; } if ($package->getReplaces()) { foreach ($package->getReplaces() as $link) { if (CompilingMatcher::match($link->getConstraint(), Constraint::OP_EQ, $package->getVersion())) { // Use the same hash part as the regular require hash because that's what the replacement does $groupHashParts[] = 'require:' . (string) $link->getConstraint(); } } } if (isset($this->conflictConstraintsPerPackage[$packageName])) { foreach ($this->conflictConstraintsPerPackage[$packageName] as $conflictConstraint) { if (CompilingMatcher::match($conflictConstraint, Constraint::OP_EQ, $package->getVersion())) { $groupHashParts[] = 'conflict:' . (string) $conflictConstraint; } } } if (!$groupHashParts) { continue; } $groupHash = implode('', $groupHashParts); $identicalDefinitionsPerPackage[$packageName][$groupHash][$dependencyHash][] = $package; $packageIdenticalDefinitionLookup[$package->id][$packageName] = array('groupHash' => $groupHash, 'dependencyHash' => $dependencyHash); } } } foreach ($identicalDefinitionsPerPackage as $constraintGroups) { foreach ($constraintGroups as $constraintGroup) { foreach ($constraintGroup as $packages) { // Only one package in this constraint group has the same requirements, we're not allowed to remove that package if (1 === \count($packages)) { $this->keepPackage($packages[0], $identicalDefinitionsPerPackage, $packageIdenticalDefinitionLookup); continue; } // Otherwise we find out which one is the preferred package in this constraint group which is // then not allowed to be removed either $literals = array(); foreach ($packages as $package) { $literals[] = $package->id; } foreach ($this->policy->selectPreferredPackages($pool, $literals) as $preferredLiteral) { $this->keepPackage($pool->literalToPackage($preferredLiteral), $identicalDefinitionsPerPackage, $packageIdenticalDefinitionLookup); } } } } } /** * @return string */ private function calculateDependencyHash(BasePackage $package) { $hash = ''; $hashRelevantLinks = array( 'requires' => $package->getRequires(), 'conflicts' => $package->getConflicts(), 'replaces' => $package->getReplaces(), 'provides' => $package->getProvides() ); foreach ($hashRelevantLinks as $key => $links) { if (0 === \count($links)) { continue; } // start new hash section $hash .= $key . ':'; $subhash = array(); foreach ($links as $link) { // To get the best dependency hash matches we should use Intervals::compactConstraint() here. // However, the majority of projects are going to specify their constraints already pretty // much in the best variant possible. In other words, we'd be wasting time here and it would actually hurt // performance more than the additional few packages that could be filtered out would benefit the process. $subhash[$link->getTarget()] = (string) $link->getConstraint(); } // Sort for best result ksort($subhash); foreach ($subhash as $target => $constraint) { $hash .= $target . '@' . $constraint; } } return $hash; } /** * @param int $id * @return void */ private function markPackageForRemoval($id) { // We are not allowed to remove packages if they have been marked as irremovable if (isset($this->irremovablePackages[$id])) { throw new \LogicException('Attempted removing a package which was previously marked irremovable'); } $this->packagesToRemove[$id] = true; } /** * @param array>>> $identicalDefinitionsPerPackage * @param array> $packageIdenticalDefinitionLookup * @return void */ private function keepPackage(BasePackage $package, $identicalDefinitionsPerPackage, $packageIdenticalDefinitionLookup) { unset($this->packagesToRemove[$package->id]); if ($package instanceof AliasPackage) { // recursing here so aliasesPerPackage for the aliasOf can be checked // and all its aliases marked to be kept as well $this->keepPackage($package->getAliasOf(), $identicalDefinitionsPerPackage, $packageIdenticalDefinitionLookup); } // record all the versions of the package group so we can list them later in Problem output foreach ($package->getNames(false) as $name) { if (isset($packageIdenticalDefinitionLookup[$package->id][$name])) { $packageGroupPointers = $packageIdenticalDefinitionLookup[$package->id][$name]; $packageGroup = $identicalDefinitionsPerPackage[$name][$packageGroupPointers['groupHash']][$packageGroupPointers['dependencyHash']]; foreach ($packageGroup as $pkg) { if ($pkg instanceof AliasPackage && $pkg->getPrettyVersion() === VersionParser::DEFAULT_BRANCH_ALIAS) { $pkg = $pkg->getAliasOf(); } $this->removedVersionsByPackage[spl_object_hash($package)][$pkg->getVersion()] = $pkg->getPrettyVersion(); } } } if (isset($this->aliasesPerPackage[$package->id])) { foreach ($this->aliasesPerPackage[$package->id] as $aliasPackage) { unset($this->packagesToRemove[$aliasPackage->id]); // record all the versions of the package group so we can list them later in Problem output foreach ($aliasPackage->getNames(false) as $name) { if (isset($packageIdenticalDefinitionLookup[$aliasPackage->id][$name])) { $packageGroupPointers = $packageIdenticalDefinitionLookup[$aliasPackage->id][$name]; $packageGroup = $identicalDefinitionsPerPackage[$name][$packageGroupPointers['groupHash']][$packageGroupPointers['dependencyHash']]; foreach ($packageGroup as $pkg) { if ($pkg instanceof AliasPackage && $pkg->getPrettyVersion() === VersionParser::DEFAULT_BRANCH_ALIAS) { $pkg = $pkg->getAliasOf(); } $this->removedVersionsByPackage[spl_object_hash($aliasPackage)][$pkg->getVersion()] = $pkg->getPrettyVersion(); } } } } } } /** * Use the list of locked packages to constrain the loaded packages * This will reduce packages with significant numbers of historical versions to a smaller number * and reduce the resulting rule set that is generated * * @return void */ private function optimizeImpossiblePackagesAway(Request $request, Pool $pool) { if (count($request->getLockedPackages()) === 0) { return; } $packageIndex = array(); foreach ($pool->getPackages() as $package) { $id = $package->id; // Do not remove irremovable packages if (isset($this->irremovablePackages[$id])) { continue; } // Do not remove a package aliased by another package, nor aliases if (isset($this->aliasesPerPackage[$id]) || $package instanceof AliasPackage) { continue; } // Do not remove locked packages if ($request->isFixedPackage($package) || $request->isLockedPackage($package)) { continue; } $packageIndex[$package->getName()][$package->id] = $package; } foreach ($request->getLockedPackages() as $package) { // If this locked package is no longer required by root or anything in the pool, it may get uninstalled so do not apply its requirements // In a case where a requirement WERE to appear in the pool by a package that would not be used, it would've been unlocked and so not filtered still $isUnusedPackage = true; foreach ($package->getNames(false) as $packageName) { if (isset($this->requireConstraintsPerPackage[$packageName])) { $isUnusedPackage = false; break; } } if ($isUnusedPackage) { continue; } foreach ($package->getRequires() as $link) { $require = $link->getTarget(); if (!isset($packageIndex[$require])) { continue; } $linkConstraint = $link->getConstraint(); foreach ($packageIndex[$require] as $id => $requiredPkg) { if (false === CompilingMatcher::match($linkConstraint, Constraint::OP_EQ, $requiredPkg->getVersion())) { $this->markPackageForRemoval($id); unset($packageIndex[$require][$id]); } } } } } } * Jordi Boggiano * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Composer\DependencyResolver; use Composer\Filter\PlatformRequirementFilter\IgnoreListPlatformRequirementFilter; use Composer\Filter\PlatformRequirementFilter\PlatformRequirementFilterFactory; use Composer\Filter\PlatformRequirementFilter\PlatformRequirementFilterInterface; use Composer\IO\IOInterface; use Composer\Package\BasePackage; /** * @author Nils Adermann */ class Solver { const BRANCH_LITERALS = 0; const BRANCH_LEVEL = 1; /** @var PolicyInterface */ protected $policy; /** @var Pool */ protected $pool; /** @var RuleSet */ protected $rules; /** @var RuleWatchGraph */ protected $watchGraph; /** @var Decisions */ protected $decisions; /** @var BasePackage[] */ protected $fixedMap; /** @var int */ protected $propagateIndex; /** @var mixed[] */ protected $branches = array(); /** @var Problem[] */ protected $problems = array(); /** @var array */ protected $learnedPool = array(); /** @var array */ protected $learnedWhy = array(); /** @var bool */ public $testFlagLearnedPositiveLiteral = false; /** @var IOInterface */ protected $io; public function __construct(PolicyInterface $policy, Pool $pool, IOInterface $io) { $this->io = $io; $this->policy = $policy; $this->pool = $pool; } /** * @return int */ public function getRuleSetSize() { return \count($this->rules); } /** * @return Pool */ public function getPool() { return $this->pool; } // aka solver_makeruledecisions /** * @return void */ private function makeAssertionRuleDecisions() { $decisionStart = \count($this->decisions) - 1; $rulesCount = \count($this->rules); for ($ruleIndex = 0; $ruleIndex < $rulesCount; $ruleIndex++) { $rule = $this->rules->ruleById[$ruleIndex]; if (!$rule->isAssertion() || $rule->isDisabled()) { continue; } $literals = $rule->getLiterals(); $literal = $literals[0]; if (!$this->decisions->decided($literal)) { $this->decisions->decide($literal, 1, $rule); continue; } if ($this->decisions->satisfy($literal)) { continue; } // found a conflict if (RuleSet::TYPE_LEARNED === $rule->getType()) { $rule->disable(); continue; } $conflict = $this->decisions->decisionRule($literal); if ($conflict && RuleSet::TYPE_PACKAGE === $conflict->getType()) { $problem = new Problem(); $problem->addRule($rule); $problem->addRule($conflict); $rule->disable(); $this->problems[] = $problem; continue; } // conflict with another root require/fixed package $problem = new Problem(); $problem->addRule($rule); $problem->addRule($conflict); // push all of our rules (can only be root require/fixed package rules) // asserting this literal on the problem stack foreach ($this->rules->getIteratorFor(RuleSet::TYPE_REQUEST) as $assertRule) { if ($assertRule->isDisabled() || !$assertRule->isAssertion()) { continue; } $assertRuleLiterals = $assertRule->getLiterals(); $assertRuleLiteral = $assertRuleLiterals[0]; if (abs($literal) !== abs($assertRuleLiteral)) { continue; } $problem->addRule($assertRule); $assertRule->disable(); } $this->problems[] = $problem; $this->decisions->resetToOffset($decisionStart); $ruleIndex = -1; } } /** * @return void */ protected function setupFixedMap(Request $request) { $this->fixedMap = array(); foreach ($request->getFixedPackages() as $package) { $this->fixedMap[$package->id] = $package; } } /** * @return void */ protected function checkForRootRequireProblems(Request $request, PlatformRequirementFilterInterface $platformRequirementFilter) { foreach ($request->getRequires() as $packageName => $constraint) { if ($platformRequirementFilter->isIgnored($packageName)) { continue; } elseif ($platformRequirementFilter instanceof IgnoreListPlatformRequirementFilter) { $constraint = $platformRequirementFilter->filterConstraint($packageName, $constraint); } if (!$this->pool->whatProvides($packageName, $constraint)) { $problem = new Problem(); $problem->addRule(new GenericRule(array(), Rule::RULE_ROOT_REQUIRE, array('packageName' => $packageName, 'constraint' => $constraint))); $this->problems[] = $problem; } } } /** * @return LockTransaction */ public function solve(Request $request, PlatformRequirementFilterInterface $platformRequirementFilter = null) { $platformRequirementFilter = $platformRequirementFilter ?: PlatformRequirementFilterFactory::ignoreNothing(); $this->setupFixedMap($request); $this->io->writeError('Generating rules', true, IOInterface::DEBUG); $ruleSetGenerator = new RuleSetGenerator($this->policy, $this->pool); $this->rules = $ruleSetGenerator->getRulesFor($request, $platformRequirementFilter); unset($ruleSetGenerator); $this->checkForRootRequireProblems($request, $platformRequirementFilter); $this->decisions = new Decisions($this->pool); $this->watchGraph = new RuleWatchGraph; foreach ($this->rules as $rule) { $this->watchGraph->insert(new RuleWatchNode($rule)); } /* make decisions based on root require/fix assertions */ $this->makeAssertionRuleDecisions(); $this->io->writeError('Resolving dependencies through SAT', true, IOInterface::DEBUG); $before = microtime(true); $this->runSat(); $this->io->writeError('', true, IOInterface::DEBUG); $this->io->writeError(sprintf('Dependency resolution completed in %.3f seconds', microtime(true) - $before), true, IOInterface::VERBOSE); if ($this->problems) { throw new SolverProblemsException($this->problems, $this->learnedPool); } return new LockTransaction($this->pool, $request->getPresentMap(), $request->getFixedPackagesMap(), $this->decisions); } /** * Makes a decision and propagates it to all rules. * * Evaluates each term affected by the decision (linked through watches) * If we find unit rules we make new decisions based on them * * @param int $level * @return Rule|null A rule on conflict, otherwise null. */ protected function propagate($level) { while ($this->decisions->validOffset($this->propagateIndex)) { $decision = $this->decisions->atOffset($this->propagateIndex); $conflict = $this->watchGraph->propagateLiteral( $decision[Decisions::DECISION_LITERAL], $level, $this->decisions ); $this->propagateIndex++; if ($conflict) { return $conflict; } } return null; } /** * Reverts a decision at the given level. * * @param int $level * * @return void */ private function revert($level) { while (!$this->decisions->isEmpty()) { $literal = $this->decisions->lastLiteral(); if ($this->decisions->undecided($literal)) { break; } $decisionLevel = $this->decisions->decisionLevel($literal); if ($decisionLevel <= $level) { break; } $this->decisions->revertLast(); $this->propagateIndex = \count($this->decisions); } while (!empty($this->branches) && $this->branches[\count($this->branches) - 1][self::BRANCH_LEVEL] >= $level) { array_pop($this->branches); } } /** * setpropagatelearn * * add free decision (a positive literal) to decision queue * increase level and propagate decision * return if no conflict. * * in conflict case, analyze conflict rule, add resulting * rule to learnt rule set, make decision from learnt * rule (always unit) and re-propagate. * * returns the new solver level or 0 if unsolvable * * @param int $level * @param string|int $literal * @return int */ private function setPropagateLearn($level, $literal, Rule $rule) { $level++; $this->decisions->decide($literal, $level, $rule); while (true) { $rule = $this->propagate($level); if (!$rule) { break; } if ($level == 1) { return $this->analyzeUnsolvable($rule); } // conflict list($learnLiteral, $newLevel, $newRule, $why) = $this->analyze($level, $rule); if ($newLevel <= 0 || $newLevel >= $level) { throw new SolverBugException( "Trying to revert to invalid level ".$newLevel." from level ".$level."." ); } $level = $newLevel; $this->revert($level); $this->rules->add($newRule, RuleSet::TYPE_LEARNED); $this->learnedWhy[spl_object_hash($newRule)] = $why; $ruleNode = new RuleWatchNode($newRule); $ruleNode->watch2OnHighest($this->decisions); $this->watchGraph->insert($ruleNode); $this->decisions->decide($learnLiteral, $level, $newRule); } return $level; } /** * @param int $level * @param int[] $decisionQueue * @return int */ private function selectAndInstall($level, array $decisionQueue, Rule $rule) { // choose best package to install from decisionQueue $literals = $this->policy->selectPreferredPackages($this->pool, $decisionQueue, $rule->getRequiredPackage()); $selectedLiteral = array_shift($literals); // if there are multiple candidates, then branch if (\count($literals)) { $this->branches[] = array($literals, $level); } return $this->setPropagateLearn($level, $selectedLiteral, $rule); } /** * @param int $level * @return array{int, int, GenericRule, int} */ protected function analyze($level, Rule $rule) { $analyzedRule = $rule; $ruleLevel = 1; $num = 0; $l1num = 0; $seen = array(); $learnedLiterals = array(null); $decisionId = \count($this->decisions); $this->learnedPool[] = array(); while (true) { $this->learnedPool[\count($this->learnedPool) - 1][] = $rule; foreach ($rule->getLiterals() as $literal) { // multiconflictrule is really a bunch of rules in one, so some may not have finished propagating yet if ($rule instanceof MultiConflictRule && !$this->decisions->decided($literal)) { continue; } // skip the one true literal if ($this->decisions->satisfy($literal)) { continue; } if (isset($seen[abs($literal)])) { continue; } $seen[abs($literal)] = true; $l = $this->decisions->decisionLevel($literal); if (1 === $l) { $l1num++; } elseif ($level === $l) { $num++; } else { // not level1 or conflict level, add to new rule $learnedLiterals[] = $literal; if ($l > $ruleLevel) { $ruleLevel = $l; } } } unset($literal); $l1retry = true; while ($l1retry) { $l1retry = false; if (0 === $num && 0 === --$l1num) { // all level 1 literals done break 2; } while (true) { if ($decisionId <= 0) { throw new SolverBugException( "Reached invalid decision id $decisionId while looking through $rule for a literal present in the analyzed rule $analyzedRule." ); } $decisionId--; $decision = $this->decisions->atOffset($decisionId); $literal = $decision[Decisions::DECISION_LITERAL]; if (isset($seen[abs($literal)])) { break; } } unset($seen[abs($literal)]); if (0 !== $num && 0 === --$num) { if ($literal < 0) { $this->testFlagLearnedPositiveLiteral = true; } $learnedLiterals[0] = -$literal; if (!$l1num) { break 2; } foreach ($learnedLiterals as $i => $learnedLiteral) { if ($i !== 0) { unset($seen[abs($learnedLiteral)]); } } // only level 1 marks left $l1num++; $l1retry = true; } else { $decision = $this->decisions->atOffset($decisionId); $rule = $decision[Decisions::DECISION_REASON]; if ($rule instanceof MultiConflictRule) { // there is only ever exactly one positive decision in a multiconflict rule foreach ($rule->getLiterals() as $literal) { if (!isset($seen[abs($literal)]) && $this->decisions->satisfy(-$literal)) { $this->learnedPool[\count($this->learnedPool) - 1][] = $rule; $l = $this->decisions->decisionLevel($literal); if (1 === $l) { $l1num++; } elseif ($level === $l) { $num++; } else { // not level1 or conflict level, add to new rule $learnedLiterals[] = $literal; if ($l > $ruleLevel) { $ruleLevel = $l; } } $seen[abs($literal)] = true; break; } } $l1retry = true; } } } $decision = $this->decisions->atOffset($decisionId); $rule = $decision[Decisions::DECISION_REASON]; } $why = \count($this->learnedPool) - 1; if (!$learnedLiterals[0]) { throw new SolverBugException( "Did not find a learnable literal in analyzed rule $analyzedRule." ); } $newRule = new GenericRule($learnedLiterals, Rule::RULE_LEARNED, $why); return array($learnedLiterals[0], $ruleLevel, $newRule, $why); } /** * @param array $ruleSeen * @return void */ private function analyzeUnsolvableRule(Problem $problem, Rule $conflictRule, array &$ruleSeen) { $why = spl_object_hash($conflictRule); $ruleSeen[$why] = true; if ($conflictRule->getType() == RuleSet::TYPE_LEARNED) { $learnedWhy = $this->learnedWhy[$why]; $problemRules = $this->learnedPool[$learnedWhy]; foreach ($problemRules as $problemRule) { if (!isset($ruleSeen[spl_object_hash($problemRule)])) { $this->analyzeUnsolvableRule($problem, $problemRule, $ruleSeen); } } return; } if ($conflictRule->getType() == RuleSet::TYPE_PACKAGE) { // package rules cannot be part of a problem return; } $problem->nextSection(); $problem->addRule($conflictRule); } /** * @return int */ private function analyzeUnsolvable(Rule $conflictRule) { $problem = new Problem(); $problem->addRule($conflictRule); $ruleSeen = array(); $this->analyzeUnsolvableRule($problem, $conflictRule, $ruleSeen); $this->problems[] = $problem; $seen = array(); $literals = $conflictRule->getLiterals(); foreach ($literals as $literal) { // skip the one true literal if ($this->decisions->satisfy($literal)) { continue; } $seen[abs($literal)] = true; } foreach ($this->decisions as $decision) { $literal = $decision[Decisions::DECISION_LITERAL]; // skip literals that are not in this rule if (!isset($seen[abs($literal)])) { continue; } $why = $decision[Decisions::DECISION_REASON]; $problem->addRule($why); $this->analyzeUnsolvableRule($problem, $why, $ruleSeen); $literals = $why->getLiterals(); foreach ($literals as $literal) { // skip the one true literal if ($this->decisions->satisfy($literal)) { continue; } $seen[abs($literal)] = true; } } return 0; } /** * enable/disable learnt rules * * we have enabled or disabled some of our rules. We now re-enable all * of our learnt rules except the ones that were learnt from rules that * are now disabled. * * @return void */ private function enableDisableLearnedRules() { foreach ($this->rules->getIteratorFor(RuleSet::TYPE_LEARNED) as $rule) { $why = $this->learnedWhy[spl_object_hash($rule)]; $problemRules = $this->learnedPool[$why]; $foundDisabled = false; foreach ($problemRules as $problemRule) { if ($problemRule->isDisabled()) { $foundDisabled = true; break; } } if ($foundDisabled && $rule->isEnabled()) { $rule->disable(); } elseif (!$foundDisabled && $rule->isDisabled()) { $rule->enable(); } } } /** * @return void */ private function runSat() { $this->propagateIndex = 0; /* * here's the main loop: * 1) propagate new decisions (only needed once) * 2) fulfill root requires/fixed packages * 3) fulfill all unresolved rules * 4) minimalize solution if we had choices * if we encounter a problem, we rewind to a safe level and restart * with step 1 */ $level = 1; $systemLevel = $level + 1; while (true) { if (1 === $level) { $conflictRule = $this->propagate($level); if (null !== $conflictRule) { if ($this->analyzeUnsolvable($conflictRule)) { continue; } return; } } // handle root require/fixed package rules if ($level < $systemLevel) { $iterator = $this->rules->getIteratorFor(RuleSet::TYPE_REQUEST); foreach ($iterator as $rule) { if ($rule->isEnabled()) { $decisionQueue = array(); $noneSatisfied = true; foreach ($rule->getLiterals() as $literal) { if ($this->decisions->satisfy($literal)) { $noneSatisfied = false; break; } if ($literal > 0 && $this->decisions->undecided($literal)) { $decisionQueue[] = $literal; } } if ($noneSatisfied && \count($decisionQueue)) { // if any of the options in the decision queue are fixed, only use those $prunedQueue = array(); foreach ($decisionQueue as $literal) { if (isset($this->fixedMap[abs($literal)])) { $prunedQueue[] = $literal; } } if (!empty($prunedQueue)) { $decisionQueue = $prunedQueue; } } if ($noneSatisfied && \count($decisionQueue)) { $oLevel = $level; $level = $this->selectAndInstall($level, $decisionQueue, $rule); if (0 === $level) { return; } if ($level <= $oLevel) { break; } } } } $systemLevel = $level + 1; // root requires/fixed packages left $iterator->next(); if ($iterator->valid()) { continue; } } if ($level < $systemLevel) { $systemLevel = $level; } $rulesCount = \count($this->rules); $pass = 1; $this->io->writeError('Looking at all rules.', true, IOInterface::DEBUG); for ($i = 0, $n = 0; $n < $rulesCount; $i++, $n++) { if ($i == $rulesCount) { if (1 === $pass) { $this->io->writeError("Something's changed, looking at all rules again (pass #$pass)", false, IOInterface::DEBUG); } else { $this->io->overwriteError("Something's changed, looking at all rules again (pass #$pass)", false, null, IOInterface::DEBUG); } $i = 0; $pass++; } $rule = $this->rules->ruleById[$i]; $literals = $rule->getLiterals(); if ($rule->isDisabled()) { continue; } $decisionQueue = array(); // make sure that // * all negative literals are installed // * no positive literal is installed // i.e. the rule is not fulfilled and we // just need to decide on the positive literals // foreach ($literals as $literal) { if ($literal <= 0) { if (!$this->decisions->decidedInstall($literal)) { continue 2; // next rule } } else { if ($this->decisions->decidedInstall($literal)) { continue 2; // next rule } if ($this->decisions->undecided($literal)) { $decisionQueue[] = $literal; } } } // need to have at least 2 item to pick from if (\count($decisionQueue) < 2) { continue; } $level = $this->selectAndInstall($level, $decisionQueue, $rule); if (0 === $level) { return; } // something changed, so look at all rules again $rulesCount = \count($this->rules); $n = -1; } if ($level < $systemLevel) { continue; } // minimization step if (\count($this->branches)) { $lastLiteral = null; $lastLevel = null; $lastBranchIndex = 0; $lastBranchOffset = 0; for ($i = \count($this->branches) - 1; $i >= 0; $i--) { list($literals, $l) = $this->branches[$i]; foreach ($literals as $offset => $literal) { if ($literal && $literal > 0 && $this->decisions->decisionLevel($literal) > $l + 1) { $lastLiteral = $literal; $lastBranchIndex = $i; $lastBranchOffset = $offset; $lastLevel = $l; } } } if ($lastLiteral) { unset($this->branches[$lastBranchIndex][self::BRANCH_LITERALS][$lastBranchOffset]); $level = $lastLevel; $this->revert($level); $why = $this->decisions->lastReason(); $level = $this->setPropagateLearn($level, $lastLiteral, $why); if ($level == 0) { return; } continue; } } break; } } } * Jordi Boggiano * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Composer\DependencyResolver; use Composer\Package\AliasPackage; use Composer\Package\BasePackage; use Composer\Package\Link; use Composer\Repository\PlatformRepository; use Composer\Repository\RepositorySet; use Composer\Package\Version\VersionParser; use Composer\Semver\Constraint\Constraint; use Composer\Semver\Constraint\ConstraintInterface; /** * @author Nils Adermann * @author Ruben Gonzalez * @phpstan-type ReasonData Link|BasePackage|string|int|array{packageName: string, constraint: ConstraintInterface}|array{package: BasePackage} */ abstract class Rule { // reason constants and // their reason data contents const RULE_ROOT_REQUIRE = 2; // array{packageName: string, constraint: ConstraintInterface} const RULE_FIXED = 3; // array{package: BasePackage} const RULE_PACKAGE_CONFLICT = 6; // Link const RULE_PACKAGE_REQUIRES = 7; // Link const RULE_PACKAGE_SAME_NAME = 10; // string (package name) const RULE_LEARNED = 12; // int (rule id) const RULE_PACKAGE_ALIAS = 13; // BasePackage const RULE_PACKAGE_INVERSE_ALIAS = 14; // BasePackage // bitfield defs const BITFIELD_TYPE = 0; const BITFIELD_REASON = 8; const BITFIELD_DISABLED = 16; /** @var int */ protected $bitfield; /** @var Request */ protected $request; /** * @var Link|BasePackage|ConstraintInterface|string * @phpstan-var ReasonData */ protected $reasonData; /** * @param self::RULE_* $reason A RULE_* constant describing the reason for generating this rule * @param mixed $reasonData * * @phpstan-param ReasonData $reasonData */ public function __construct($reason, $reasonData) { $this->reasonData = $reasonData; $this->bitfield = (0 << self::BITFIELD_DISABLED) | ($reason << self::BITFIELD_REASON) | (255 << self::BITFIELD_TYPE); } /** * @return int[] */ abstract public function getLiterals(); /** * @return int|string */ abstract public function getHash(); abstract public function __toString(); /** * @param Rule $rule * @return bool */ abstract public function equals(Rule $rule); /** * @return int */ public function getReason() { return ($this->bitfield & (255 << self::BITFIELD_REASON)) >> self::BITFIELD_REASON; } /** * @phpstan-return ReasonData */ public function getReasonData() { return $this->reasonData; } /** * @return string|null */ public function getRequiredPackage() { $reason = $this->getReason(); if ($reason === self::RULE_ROOT_REQUIRE) { return $this->reasonData['packageName']; } if ($reason === self::RULE_FIXED) { return $this->reasonData['package']->getName(); } if ($reason === self::RULE_PACKAGE_REQUIRES) { return $this->reasonData->getTarget(); } return null; } /** * @param RuleSet::TYPE_* $type * @return void */ public function setType($type) { $this->bitfield = ($this->bitfield & ~(255 << self::BITFIELD_TYPE)) | ((255 & $type) << self::BITFIELD_TYPE); } /** * @return int */ public function getType() { return ($this->bitfield & (255 << self::BITFIELD_TYPE)) >> self::BITFIELD_TYPE; } /** * @return void */ public function disable() { $this->bitfield = ($this->bitfield & ~(255 << self::BITFIELD_DISABLED)) | (1 << self::BITFIELD_DISABLED); } /** * @return void */ public function enable() { $this->bitfield &= ~(255 << self::BITFIELD_DISABLED); } /** * @return bool */ public function isDisabled() { return (bool) (($this->bitfield & (255 << self::BITFIELD_DISABLED)) >> self::BITFIELD_DISABLED); } /** * @return bool */ public function isEnabled() { return !(($this->bitfield & (255 << self::BITFIELD_DISABLED)) >> self::BITFIELD_DISABLED); } /** * @return bool */ abstract public function isAssertion(); /** * @return bool */ public function isCausedByLock(RepositorySet $repositorySet, Request $request, Pool $pool) { if ($this->getReason() === self::RULE_PACKAGE_REQUIRES) { if (PlatformRepository::isPlatformPackage($this->reasonData->getTarget())) { return false; } if ($request->getLockedRepository()) { foreach ($request->getLockedRepository()->getPackages() as $package) { if ($package->getName() === $this->reasonData->getTarget()) { if ($pool->isUnacceptableFixedOrLockedPackage($package)) { return true; } if (!$this->reasonData->getConstraint()->matches(new Constraint('=', $package->getVersion()))) { return true; } // required package was locked but has been unlocked and still matches if (!$request->isLockedPackage($package)) { return true; } break; } } } } if ($this->getReason() === self::RULE_ROOT_REQUIRE) { if (PlatformRepository::isPlatformPackage($this->reasonData['packageName'])) { return false; } if ($request->getLockedRepository()) { foreach ($request->getLockedRepository()->getPackages() as $package) { if ($package->getName() === $this->reasonData['packageName']) { if ($pool->isUnacceptableFixedOrLockedPackage($package)) { return true; } if (!$this->reasonData['constraint']->matches(new Constraint('=', $package->getVersion()))) { return true; } break; } } } } return false; } /** * @internal * @return BasePackage */ public function getSourcePackage(Pool $pool) { $literals = $this->getLiterals(); switch ($this->getReason()) { case self::RULE_PACKAGE_CONFLICT: $package1 = $this->deduplicateDefaultBranchAlias($pool->literalToPackage($literals[0])); $package2 = $this->deduplicateDefaultBranchAlias($pool->literalToPackage($literals[1])); if ($reasonData = $this->getReasonData()) { // swap literals if they are not in the right order with package2 being the conflicter if ($reasonData->getSource() === $package1->getName()) { list($package2, $package1) = array($package1, $package2); } } return $package2; case self::RULE_PACKAGE_REQUIRES: $sourceLiteral = array_shift($literals); $sourcePackage = $this->deduplicateDefaultBranchAlias($pool->literalToPackage($sourceLiteral)); return $sourcePackage; default: throw new \LogicException('Not implemented'); } } /** * @param bool $isVerbose * @param BasePackage[] $installedMap * @param array $learnedPool * @return string */ public function getPrettyString(RepositorySet $repositorySet, Request $request, Pool $pool, $isVerbose, array $installedMap = array(), array $learnedPool = array()) { $literals = $this->getLiterals(); switch ($this->getReason()) { case self::RULE_ROOT_REQUIRE: $packageName = $this->reasonData['packageName']; $constraint = $this->reasonData['constraint']; $packages = $pool->whatProvides($packageName, $constraint); if (!$packages) { return 'No package found to satisfy root composer.json require '.$packageName.($constraint ? ' '.$constraint->getPrettyString() : ''); } $packagesNonAlias = array_values(array_filter($packages, function ($p) { return !($p instanceof AliasPackage); })); if (count($packagesNonAlias) === 1) { $package = $packagesNonAlias[0]; if ($request->isLockedPackage($package)) { return $package->getPrettyName().' is locked to version '.$package->getPrettyVersion()." and an update of this package was not requested."; } } return 'Root composer.json requires '.$packageName.($constraint ? ' '.$constraint->getPrettyString() : '').' -> satisfiable by '.$this->formatPackagesUnique($pool, $packages, $isVerbose, $constraint).'.'; case self::RULE_FIXED: $package = $this->deduplicateDefaultBranchAlias($this->reasonData['package']); if ($request->isLockedPackage($package)) { return $package->getPrettyName().' is locked to version '.$package->getPrettyVersion().' and an update of this package was not requested.'; } return $package->getPrettyName().' is present at version '.$package->getPrettyVersion() . ' and cannot be modified by Composer'; case self::RULE_PACKAGE_CONFLICT: $package1 = $this->deduplicateDefaultBranchAlias($pool->literalToPackage($literals[0])); $package2 = $this->deduplicateDefaultBranchAlias($pool->literalToPackage($literals[1])); $conflictTarget = $package1->getPrettyString(); if ($reasonData = $this->getReasonData()) { assert($reasonData instanceof Link); // swap literals if they are not in the right order with package2 being the conflicter if ($reasonData->getSource() === $package1->getName()) { list($package2, $package1) = array($package1, $package2); $conflictTarget = $package1->getPrettyName().' '.$reasonData->getPrettyConstraint(); } // if the conflict is not directly against the package but something it provides/replaces, // we try to find that link to display a better message if ($reasonData->getTarget() !== $package1->getName()) { $provideType = null; $provided = null; foreach ($package1->getProvides() as $provide) { if ($provide->getTarget() === $reasonData->getTarget()) { $provideType = 'provides'; $provided = $provide->getPrettyConstraint(); break; } } foreach ($package1->getReplaces() as $replace) { if ($replace->getTarget() === $reasonData->getTarget()) { $provideType = 'replaces'; $provided = $replace->getPrettyConstraint(); break; } } if (null !== $provideType) { $conflictTarget = $reasonData->getTarget().' '.$reasonData->getPrettyConstraint().' ('.$package1->getPrettyString().' '.$provideType.' '.$reasonData->getTarget().' '.$provided.')'; } } } return $package2->getPrettyString().' conflicts with '.$conflictTarget.'.'; case self::RULE_PACKAGE_REQUIRES: $sourceLiteral = array_shift($literals); $sourcePackage = $this->deduplicateDefaultBranchAlias($pool->literalToPackage($sourceLiteral)); /** @var Link */ $reasonData = $this->reasonData; $requires = array(); foreach ($literals as $literal) { $requires[] = $pool->literalToPackage($literal); } $text = $reasonData->getPrettyString($sourcePackage); if ($requires) { $text .= ' -> satisfiable by ' . $this->formatPackagesUnique($pool, $requires, $isVerbose, $this->reasonData->getConstraint()) . '.'; } else { $targetName = $reasonData->getTarget(); $reason = Problem::getMissingPackageReason($repositorySet, $request, $pool, $isVerbose, $targetName, $this->reasonData->getConstraint()); return $text . ' -> ' . $reason[1]; } return $text; case self::RULE_PACKAGE_SAME_NAME: $packageNames = array(); foreach ($literals as $literal) { $package = $pool->literalToPackage($literal); $packageNames[$package->getName()] = true; } $replacedName = $this->reasonData; if (count($packageNames) > 1) { $reason = null; if (!isset($packageNames[$replacedName])) { $reason = 'They '.(count($literals) == 2 ? 'both' : 'all').' replace '.$replacedName.' and thus cannot coexist.'; } else { $replacerNames = $packageNames; unset($replacerNames[$replacedName]); $replacerNames = array_keys($replacerNames); if (count($replacerNames) == 1) { $reason = $replacerNames[0] . ' replaces '; } else { $reason = '['.implode(', ', $replacerNames).'] replace '; } $reason .= $replacedName.' and thus cannot coexist with it.'; } $installedPackages = array(); $removablePackages = array(); foreach ($literals as $literal) { if (isset($installedMap[abs($literal)])) { $installedPackages[] = $pool->literalToPackage($literal); } else { $removablePackages[] = $pool->literalToPackage($literal); } } if ($installedPackages && $removablePackages) { return $this->formatPackagesUnique($pool, $removablePackages, $isVerbose, null, true).' cannot be installed as that would require removing '.$this->formatPackagesUnique($pool, $installedPackages, $isVerbose, null, true).'. '.$reason; } return 'Only one of these can be installed: '.$this->formatPackagesUnique($pool, $literals, $isVerbose, null, true).'. '.$reason; } return 'You can only install one version of a package, so only one of these can be installed: ' . $this->formatPackagesUnique($pool, $literals, $isVerbose, null, true) . '.'; case self::RULE_LEARNED: /** @TODO currently still generates way too much output to be helpful, and in some cases can even lead to endless recursion */ // if (isset($learnedPool[$this->reasonData])) { // echo $this->reasonData."\n"; // $learnedString = ', learned rules:' . Problem::formatDeduplicatedRules($learnedPool[$this->reasonData], ' ', $repositorySet, $request, $pool, $isVerbose, $installedMap, $learnedPool); // } else { // $learnedString = ' (reasoning unavailable)'; // } $learnedString = ' (conflict analysis result)'; if (count($literals) === 1) { $ruleText = $pool->literalToPrettyString($literals[0], $installedMap); } else { $groups = array(); foreach ($literals as $literal) { $package = $pool->literalToPackage($literal); if (isset($installedMap[$package->id])) { $group = $literal > 0 ? 'keep' : 'remove'; } else { $group = $literal > 0 ? 'install' : 'don\'t install'; } $groups[$group][] = $this->deduplicateDefaultBranchAlias($package); } $ruleTexts = array(); foreach ($groups as $group => $packages) { $ruleTexts[] = $group . (count($packages) > 1 ? ' one of' : '').' ' . $this->formatPackagesUnique($pool, $packages, $isVerbose); } $ruleText = implode(' | ', $ruleTexts); } return 'Conclusion: '.$ruleText.$learnedString; case self::RULE_PACKAGE_ALIAS: $aliasPackage = $pool->literalToPackage($literals[0]); // avoid returning content like "9999999-dev is an alias of dev-master" as it is useless if ($aliasPackage->getVersion() === VersionParser::DEFAULT_BRANCH_ALIAS) { return ''; } $package = $this->deduplicateDefaultBranchAlias($pool->literalToPackage($literals[1])); return $aliasPackage->getPrettyString() .' is an alias of '.$package->getPrettyString().' and thus requires it to be installed too.'; case self::RULE_PACKAGE_INVERSE_ALIAS: // inverse alias rules work the other way around than above $aliasPackage = $pool->literalToPackage($literals[1]); // avoid returning content like "9999999-dev is an alias of dev-master" as it is useless if ($aliasPackage->getVersion() === VersionParser::DEFAULT_BRANCH_ALIAS) { return ''; } $package = $this->deduplicateDefaultBranchAlias($pool->literalToPackage($literals[0])); return $aliasPackage->getPrettyString() .' is an alias of '.$package->getPrettyString().' and must be installed with it.'; default: $ruleText = ''; foreach ($literals as $i => $literal) { if ($i != 0) { $ruleText .= '|'; } $ruleText .= $pool->literalToPrettyString($literal, $installedMap); } return '('.$ruleText.')'; } } /** * @param array $packages An array containing packages or literals * @param bool $isVerbose * @param bool $useRemovedVersionGroup * @return string */ protected function formatPackagesUnique(Pool $pool, array $packages, $isVerbose, ConstraintInterface $constraint = null, $useRemovedVersionGroup = false) { foreach ($packages as $index => $package) { if (!\is_object($package)) { $packages[$index] = $pool->literalToPackage($package); } } return Problem::getPackageList($packages, $isVerbose, $pool, $constraint, $useRemovedVersionGroup); } /** * @return BasePackage */ private function deduplicateDefaultBranchAlias(BasePackage $package) { if ($package instanceof AliasPackage && $package->getPrettyVersion() === VersionParser::DEFAULT_BRANCH_ALIAS) { $package = $package->getAliasOf(); } return $package; } } * Jordi Boggiano * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Composer\DependencyResolver; use Composer\Util\IniHelper; use Composer\Repository\RepositorySet; /** * @author Nils Adermann * * @method self::ERROR_DEPENDENCY_RESOLUTION_FAILED getCode() */ class SolverProblemsException extends \RuntimeException { const ERROR_DEPENDENCY_RESOLUTION_FAILED = 2; /** @var Problem[] */ protected $problems; /** @var array */ protected $learnedPool; /** * @param Problem[] $problems * @param array $learnedPool */ public function __construct(array $problems, array $learnedPool) { $this->problems = $problems; $this->learnedPool = $learnedPool; parent::__construct('Failed resolving dependencies with '.count($problems).' problems, call getPrettyString to get formatted details', self::ERROR_DEPENDENCY_RESOLUTION_FAILED); } /** * @param bool $isVerbose * @param bool $isDevExtraction * @return string */ public function getPrettyString(RepositorySet $repositorySet, Request $request, Pool $pool, $isVerbose, $isDevExtraction = false) { $installedMap = $request->getPresentMap(true); $missingExtensions = array(); $isCausedByLock = false; $problems = array(); foreach ($this->problems as $problem) { $problems[] = $problem->getPrettyString($repositorySet, $request, $pool, $isVerbose, $installedMap, $this->learnedPool)."\n"; $missingExtensions = array_merge($missingExtensions, $this->getExtensionProblems($problem->getReasons())); $isCausedByLock = $isCausedByLock || $problem->isCausedByLock($repositorySet, $request, $pool); } $i = 1; $text = "\n"; foreach (array_unique($problems) as $problem) { $text .= " Problem ".($i++).$problem; } $hints = array(); if (!$isDevExtraction && (strpos($text, 'could not be found') || strpos($text, 'no matching package found'))) { $hints[] = "Potential causes:\n - A typo in the package name\n - The package is not available in a stable-enough version according to your minimum-stability setting\n see for more details.\n - It's a private package and you forgot to add a custom repository to find it\n\nRead for further common problems."; } if (!empty($missingExtensions)) { $hints[] = $this->createExtensionHint($missingExtensions); } if ($isCausedByLock && !$isDevExtraction && !$request->getUpdateAllowTransitiveRootDependencies()) { $hints[] = "Use the option --with-all-dependencies (-W) to allow upgrades, downgrades and removals for packages currently locked to specific versions."; } if (strpos($text, 'found composer-plugin-api[2.0.0] but it does not match') && strpos($text, '- ocramius/package-versions')) { $hints[] = "ocramius/package-versions only provides support for Composer 2 in 1.8+, which requires PHP 7.4.\nIf you can not upgrade PHP you can require composer/package-versions-deprecated to resolve this with PHP 7.0+."; } if (!class_exists('PHPUnit\Framework\TestCase', false)) { if (strpos($text, 'found composer-plugin-api[2.0.0] but it does not match')) { $hints[] = "You are using Composer 2, which some of your plugins seem to be incompatible with. Make sure you update your plugins or report a plugin-issue to ask them to support Composer 2."; } } if ($hints) { $text .= "\n" . implode("\n\n", $hints); } return $text; } /** * @return Problem[] */ public function getProblems() { return $this->problems; } /** * @param string[] $missingExtensions * @return string */ private function createExtensionHint(array $missingExtensions) { $paths = IniHelper::getAll(); if (count($paths) === 1 && empty($paths[0])) { return ''; } $ignoreExtensionsArguments = implode(" ", array_map(function ($extension) { return "--ignore-platform-req=$extension"; }, $missingExtensions)); $text = "To enable extensions, verify that they are enabled in your .ini files:\n - "; $text .= implode("\n - ", $paths); $text .= "\nYou can also run `php --ini` in a terminal to see which files are used by PHP in CLI mode."; $text .= "\nAlternatively, you can run Composer with `$ignoreExtensionsArguments` to temporarily ignore these required extensions."; return $text; } /** * @param Rule[][] $reasonSets * @return string[] */ private function getExtensionProblems(array $reasonSets) { $missingExtensions = array(); foreach ($reasonSets as $reasonSet) { foreach ($reasonSet as $rule) { $required = $rule->getRequiredPackage(); if (null !== $required && 0 === strpos($required, 'ext-')) { $missingExtensions[$required] = 1; } } } return array_keys($missingExtensions); } } * Jordi Boggiano * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Composer\DependencyResolver; /** * @author Nils Adermann * @phpstan-import-type ReasonData from Rule */ class Rule2Literals extends Rule { /** @var int */ protected $literal1; /** @var int */ protected $literal2; /** * @param int $literal1 * @param int $literal2 * @param Rule::RULE_* $reason A RULE_* constant * @param mixed $reasonData * * @phpstan-param ReasonData $reasonData */ public function __construct($literal1, $literal2, $reason, $reasonData) { parent::__construct($reason, $reasonData); if ($literal1 < $literal2) { $this->literal1 = $literal1; $this->literal2 = $literal2; } else { $this->literal1 = $literal2; $this->literal2 = $literal1; } } /** @return int[] */ public function getLiterals() { return array($this->literal1, $this->literal2); } /** * @inheritDoc */ public function getHash() { return $this->literal1.','.$this->literal2; } /** * Checks if this rule is equal to another one * * Ignores whether either of the rules is disabled. * * @param Rule $rule The rule to check against * @return bool Whether the rules are equal */ public function equals(Rule $rule) { // specialized fast-case if ($rule instanceof self) { if ($this->literal1 !== $rule->literal1) { return false; } if ($this->literal2 !== $rule->literal2) { return false; } return true; } $literals = $rule->getLiterals(); if (2 != \count($literals)) { return false; } if ($this->literal1 !== $literals[0]) { return false; } if ($this->literal2 !== $literals[1]) { return false; } return true; } /** @return false */ public function isAssertion() { return false; } /** * Formats a rule as a string of the format (Literal1|Literal2|...) * * @return string */ public function __toString() { $result = $this->isDisabled() ? 'disabled(' : '('; $result .= $this->literal1 . '|' . $this->literal2 . ')'; return $result; } } * Jordi Boggiano * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Composer\DependencyResolver; /** * Wrapper around a Rule which keeps track of the two literals it watches * * Used by RuleWatchGraph to store rules in two RuleWatchChains. * * @author Nils Adermann */ class RuleWatchNode { /** @var int */ public $watch1; /** @var int */ public $watch2; /** @var Rule */ protected $rule; /** * Creates a new node watching the first and second literals of the rule. * * @param Rule $rule The rule to wrap */ public function __construct(Rule $rule) { $this->rule = $rule; $literals = $rule->getLiterals(); $literalCount = \count($literals); $this->watch1 = $literalCount > 0 ? $literals[0] : 0; $this->watch2 = $literalCount > 1 ? $literals[1] : 0; } /** * Places the second watch on the rule's literal, decided at the highest level * * Useful for learned rules where the literal for the highest rule is most * likely to quickly lead to further decisions. * * @param Decisions $decisions The decisions made so far by the solver * @return void */ public function watch2OnHighest(Decisions $decisions) { $literals = $this->rule->getLiterals(); // if there are only 2 elements, both are being watched anyway if (\count($literals) < 3 || $this->rule instanceof MultiConflictRule) { return; } $watchLevel = 0; foreach ($literals as $literal) { $level = $decisions->decisionLevel($literal); if ($level > $watchLevel) { $this->watch2 = $literal; $watchLevel = $level; } } } /** * Returns the rule this node wraps * * @return Rule */ public function getRule() { return $this->rule; } /** * Given one watched literal, this method returns the other watched literal * * @param int $literal The watched literal that should not be returned * @return int A literal */ public function getOtherWatch($literal) { if ($this->watch1 == $literal) { return $this->watch2; } return $this->watch1; } /** * Moves a watch from one literal to another * * @param int $from The previously watched literal * @param int $to The literal to be watched now * @return void */ public function moveWatch($from, $to) { if ($this->watch1 == $from) { $this->watch1 = $to; } else { $this->watch2 = $to; } } } * Jordi Boggiano * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Composer\DependencyResolver; use Composer\Repository\RepositorySet; /** * @author Nils Adermann * @implements \IteratorAggregate */ class RuleSet implements \IteratorAggregate, \Countable { // highest priority => lowest number const TYPE_PACKAGE = 0; const TYPE_REQUEST = 1; const TYPE_LEARNED = 4; /** * READ-ONLY: Lookup table for rule id to rule object * * @var array */ public $ruleById = array(); /** @var array<0|1|4, string> */ protected static $types = array( self::TYPE_PACKAGE => 'PACKAGE', self::TYPE_REQUEST => 'REQUEST', self::TYPE_LEARNED => 'LEARNED', ); /** @var array */ protected $rules; /** @var int */ protected $nextRuleId = 0; /** @var array */ protected $rulesByHash = array(); public function __construct() { foreach ($this->getTypes() as $type) { $this->rules[$type] = array(); } } /** * @param self::TYPE_* $type * @return void */ public function add(Rule $rule, $type) { if (!isset(self::$types[$type])) { throw new \OutOfBoundsException('Unknown rule type: ' . $type); } $hash = $rule->getHash(); // Do not add if rule already exists if (isset($this->rulesByHash[$hash])) { $potentialDuplicates = $this->rulesByHash[$hash]; if (\is_array($potentialDuplicates)) { foreach ($potentialDuplicates as $potentialDuplicate) { if ($rule->equals($potentialDuplicate)) { return; } } } else { if ($rule->equals($potentialDuplicates)) { return; } } } if (!isset($this->rules[$type])) { $this->rules[$type] = array(); } $this->rules[$type][] = $rule; $this->ruleById[$this->nextRuleId] = $rule; $rule->setType($type); $this->nextRuleId++; if (!isset($this->rulesByHash[$hash])) { $this->rulesByHash[$hash] = $rule; } elseif (\is_array($this->rulesByHash[$hash])) { $this->rulesByHash[$hash][] = $rule; } else { $originalRule = $this->rulesByHash[$hash]; $this->rulesByHash[$hash] = array($originalRule, $rule); } } /** * @return int */ #[\ReturnTypeWillChange] public function count() { return $this->nextRuleId; } /** * @param int $id * @return Rule */ public function ruleById($id) { return $this->ruleById[$id]; } /** @return array */ public function getRules() { return $this->rules; } /** * @return RuleSetIterator */ #[\ReturnTypeWillChange] public function getIterator() { return new RuleSetIterator($this->getRules()); } /** * @param self::TYPE_*|array $types * @return RuleSetIterator */ public function getIteratorFor($types) { if (!\is_array($types)) { $types = array($types); } $allRules = $this->getRules(); /** @var array $rules */ $rules = array(); foreach ($types as $type) { $rules[$type] = $allRules[$type]; } return new RuleSetIterator($rules); } /** * @param array|self::TYPE_* $types * @return RuleSetIterator */ public function getIteratorWithout($types) { if (!\is_array($types)) { $types = array($types); } $rules = $this->getRules(); foreach ($types as $type) { unset($rules[$type]); } return new RuleSetIterator($rules); } /** @return array{0: 0, 1: 1, 2: 4} */ public function getTypes() { $types = self::$types; return array_keys($types); } /** * @param bool $isVerbose * @return string */ public function getPrettyString(RepositorySet $repositorySet = null, Request $request = null, Pool $pool = null, $isVerbose = false) { $string = "\n"; foreach ($this->rules as $type => $rules) { $string .= str_pad(self::$types[$type], 8, ' ') . ": "; foreach ($rules as $rule) { $string .= ($repositorySet && $request && $pool ? $rule->getPrettyString($repositorySet, $request, $pool, $isVerbose) : $rule)."\n"; } $string .= "\n\n"; } return $string; } public function __toString() { return $this->getPrettyString(); } } * Jordi Boggiano * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Composer\DependencyResolver; use Composer\Package\BasePackage; use Composer\Package\Version\VersionParser; use Composer\Semver\CompilingMatcher; use Composer\Semver\Constraint\ConstraintInterface; use Composer\Semver\Constraint\Constraint; /** * A package pool contains all packages for dependency resolution * * @author Nils Adermann * @author Jordi Boggiano */ class Pool implements \Countable { /** @var BasePackage[] */ protected $packages = array(); /** @var array */ protected $packageByName = array(); /** @var VersionParser */ protected $versionParser; /** @var array> */ protected $providerCache = array(); /** @var BasePackage[] */ protected $unacceptableFixedOrLockedPackages; /** @var array> Map of package name => normalized version => pretty version */ protected $removedVersions = array(); /** @var array> Map of package object hash => removed normalized versions => removed pretty version */ protected $removedVersionsByPackage = array(); /** * @param BasePackage[] $packages * @param BasePackage[] $unacceptableFixedOrLockedPackages * @param array> $removedVersions * @param array> $removedVersionsByPackage */ public function __construct(array $packages = array(), array $unacceptableFixedOrLockedPackages = array(), array $removedVersions = array(), array $removedVersionsByPackage = array()) { $this->versionParser = new VersionParser; $this->setPackages($packages); $this->unacceptableFixedOrLockedPackages = $unacceptableFixedOrLockedPackages; $this->removedVersions = $removedVersions; $this->removedVersionsByPackage = $removedVersionsByPackage; } /** * @param string $name * @return array */ public function getRemovedVersions($name, ConstraintInterface $constraint) { if (!isset($this->removedVersions[$name])) { return array(); } $result = array(); foreach ($this->removedVersions[$name] as $version => $prettyVersion) { if ($constraint->matches(new Constraint('==', $version))) { $result[$version] = $prettyVersion; } } return $result; } /** * @param string $objectHash * @return array */ public function getRemovedVersionsByPackage($objectHash) { if (!isset($this->removedVersionsByPackage[$objectHash])) { return array(); } return $this->removedVersionsByPackage[$objectHash]; } /** * @param BasePackage[] $packages * @return void */ private function setPackages(array $packages) { $id = 1; foreach ($packages as $package) { $this->packages[] = $package; $package->id = $id++; foreach ($package->getNames() as $provided) { $this->packageByName[$provided][] = $package; } } } /** * @return BasePackage[] */ public function getPackages() { return $this->packages; } /** * Retrieves the package object for a given package id. * * @param int $id * @return BasePackage */ public function packageById($id) { return $this->packages[$id - 1]; } /** * Returns how many packages have been loaded into the pool * @return int */ #[\ReturnTypeWillChange] public function count() { return \count($this->packages); } /** * Searches all packages providing the given package name and match the constraint * * @param string $name The package name to be searched for * @param ?ConstraintInterface $constraint A constraint that all returned * packages must match or null to return all * @return BasePackage[] A set of packages */ public function whatProvides($name, ConstraintInterface $constraint = null) { $key = (string) $constraint; if (isset($this->providerCache[$name][$key])) { return $this->providerCache[$name][$key]; } return $this->providerCache[$name][$key] = $this->computeWhatProvides($name, $constraint); } /** * @param string $name The package name to be searched for * @param ?ConstraintInterface $constraint A constraint that all returned * packages must match or null to return all * @return BasePackage[] */ private function computeWhatProvides($name, ConstraintInterface $constraint = null) { if (!isset($this->packageByName[$name])) { return array(); } $matches = array(); foreach ($this->packageByName[$name] as $candidate) { if ($this->match($candidate, $name, $constraint)) { $matches[] = $candidate; } } return $matches; } /** * @param int $literal * @return BasePackage */ public function literalToPackage($literal) { $packageId = abs($literal); return $this->packageById($packageId); } /** * @param int $literal * @param array $installedMap * @return string */ public function literalToPrettyString($literal, $installedMap) { $package = $this->literalToPackage($literal); if (isset($installedMap[$package->id])) { $prefix = ($literal > 0 ? 'keep' : 'remove'); } else { $prefix = ($literal > 0 ? 'install' : 'don\'t install'); } return $prefix.' '.$package->getPrettyString(); } /** * Checks if the package matches the given constraint directly or through * provided or replaced packages * * @param string $name Name of the package to be matched * @return bool */ public function match(BasePackage $candidate, $name, ConstraintInterface $constraint = null) { $candidateName = $candidate->getName(); $candidateVersion = $candidate->getVersion(); if ($candidateName === $name) { return $constraint === null || CompilingMatcher::match($constraint, Constraint::OP_EQ, $candidateVersion); } $provides = $candidate->getProvides(); $replaces = $candidate->getReplaces(); // aliases create multiple replaces/provides for one target so they can not use the shortcut below if (isset($replaces[0]) || isset($provides[0])) { foreach ($provides as $link) { if ($link->getTarget() === $name && ($constraint === null || $constraint->matches($link->getConstraint()))) { return true; } } foreach ($replaces as $link) { if ($link->getTarget() === $name && ($constraint === null || $constraint->matches($link->getConstraint()))) { return true; } } return false; } if (isset($provides[$name]) && ($constraint === null || $constraint->matches($provides[$name]->getConstraint()))) { return true; } if (isset($replaces[$name]) && ($constraint === null || $constraint->matches($replaces[$name]->getConstraint()))) { return true; } return false; } /** * @return bool */ public function isUnacceptableFixedOrLockedPackage(BasePackage $package) { return \in_array($package, $this->unacceptableFixedOrLockedPackages, true); } /** * @return BasePackage[] */ public function getUnacceptableFixedOrLockedPackages() { return $this->unacceptableFixedOrLockedPackages; } public function __toString() { $str = "Pool:\n"; foreach ($this->packages as $package) { $str .= '- '.str_pad((string) $package->id, 6, ' ', STR_PAD_LEFT).': '.$package->getName()."\n"; } return $str; } } * Jordi Boggiano * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Composer\DependencyResolver; /** * @author Nils Adermann * * MultiConflictRule([A, B, C]) acts as Rule([-A, -B]), Rule([-A, -C]), Rule([-B, -C]) */ class MultiConflictRule extends Rule { /** @var int[] */ protected $literals; /** * @param int[] $literals */ public function __construct(array $literals, $reason, $reasonData) { parent::__construct($reason, $reasonData); if (\count($literals) < 3) { throw new \RuntimeException("multi conflict rule requires at least 3 literals"); } // sort all packages ascending by id sort($literals); $this->literals = $literals; } /** * @return int[] */ public function getLiterals() { return $this->literals; } /** * @inheritDoc */ public function getHash() { $data = unpack('ihash', md5('c:'.implode(',', $this->literals), true)); return $data['hash']; } /** * Checks if this rule is equal to another one * * Ignores whether either of the rules is disabled. * * @param Rule $rule The rule to check against * @return bool Whether the rules are equal */ public function equals(Rule $rule) { if ($rule instanceof MultiConflictRule) { return $this->literals === $rule->getLiterals(); } return false; } /** * @return bool */ public function isAssertion() { return false; } /** * @return never * @throws \RuntimeException */ public function disable() { throw new \RuntimeException("Disabling multi conflict rules is not possible. Please contact composer at https://github.com/composer/composer to let us debug what lead to this situation."); } /** * Formats a rule as a string of the format (Literal1|Literal2|...) * * @return string */ public function __toString() { // TODO multi conflict? $result = $this->isDisabled() ? 'disabled(multi(' : '(multi('; foreach ($this->literals as $i => $literal) { if ($i != 0) { $result .= '|'; } $result .= $literal; } $result .= '))'; return $result; } } * Jordi Boggiano * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Composer\DependencyResolver\Operation; use Composer\Package\AliasPackage; /** * Solver install operation. * * @author Nils Adermann */ class MarkAliasUninstalledOperation extends SolverOperation implements OperationInterface { const TYPE = 'markAliasUninstalled'; /** * @var AliasPackage */ protected $package; public function __construct(AliasPackage $package) { $this->package = $package; } /** * Returns package instance. * * @return AliasPackage */ public function getPackage() { return $this->package; } /** * @inheritDoc */ public function show($lock) { return 'Marking '.$this->package->getPrettyName().' ('.$this->package->getFullPrettyVersion().') as uninstalled, alias of '.$this->package->getAliasOf()->getPrettyName().' ('.$this->package->getAliasOf()->getFullPrettyVersion().')'; } } * Jordi Boggiano * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Composer\DependencyResolver\Operation; /** * Abstract operation class. * * @author Aleksandr Bezpiatov */ abstract class SolverOperation implements OperationInterface { const TYPE = null; /** * Returns operation type. * * @return string */ public function getOperationType() { return static::TYPE; } /** * @inheritDoc */ public function __toString() { return $this->show(false); } } * Jordi Boggiano * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Composer\DependencyResolver\Operation; use Composer\Package\PackageInterface; use Composer\Package\Version\VersionParser; /** * Solver update operation. * * @author Konstantin Kudryashov */ class UpdateOperation extends SolverOperation implements OperationInterface { const TYPE = 'update'; /** * @var PackageInterface */ protected $initialPackage; /** * @var PackageInterface */ protected $targetPackage; /** * @param PackageInterface $initial initial package * @param PackageInterface $target target package (updated) */ public function __construct(PackageInterface $initial, PackageInterface $target) { $this->initialPackage = $initial; $this->targetPackage = $target; } /** * Returns initial package. * * @return PackageInterface */ public function getInitialPackage() { return $this->initialPackage; } /** * Returns target package. * * @return PackageInterface */ public function getTargetPackage() { return $this->targetPackage; } /** * @inheritDoc */ public function show($lock) { return self::format($this->initialPackage, $this->targetPackage, $lock); } /** * @param bool $lock * @return string */ public static function format(PackageInterface $initialPackage, PackageInterface $targetPackage, $lock = false) { $fromVersion = $initialPackage->getFullPrettyVersion(); $toVersion = $targetPackage->getFullPrettyVersion(); if ($fromVersion === $toVersion && $initialPackage->getSourceReference() !== $targetPackage->getSourceReference()) { $fromVersion = $initialPackage->getFullPrettyVersion(true, PackageInterface::DISPLAY_SOURCE_REF); $toVersion = $targetPackage->getFullPrettyVersion(true, PackageInterface::DISPLAY_SOURCE_REF); } elseif ($fromVersion === $toVersion && $initialPackage->getDistReference() !== $targetPackage->getDistReference()) { $fromVersion = $initialPackage->getFullPrettyVersion(true, PackageInterface::DISPLAY_DIST_REF); $toVersion = $targetPackage->getFullPrettyVersion(true, PackageInterface::DISPLAY_DIST_REF); } $actionName = VersionParser::isUpgrade($initialPackage->getVersion(), $targetPackage->getVersion()) ? 'Upgrading' : 'Downgrading'; return $actionName.' '.$initialPackage->getPrettyName().' ('.$fromVersion.' => '.$toVersion.')'; } } * Jordi Boggiano * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Composer\DependencyResolver\Operation; /** * Solver operation interface. * * @author Konstantin Kudryashov */ interface OperationInterface { /** * Returns operation type. * * @return string */ public function getOperationType(); /** * Serializes the operation in a human readable format * * @param bool $lock Whether this is an operation on the lock file * @return string */ public function show($lock); /** * Serializes the operation in a human readable format * * @return string */ public function __toString(); } * Jordi Boggiano * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Composer\DependencyResolver\Operation; use Composer\Package\AliasPackage; /** * Solver install operation. * * @author Nils Adermann */ class MarkAliasInstalledOperation extends SolverOperation implements OperationInterface { const TYPE = 'markAliasInstalled'; /** * @var AliasPackage */ protected $package; public function __construct(AliasPackage $package) { $this->package = $package; } /** * Returns package instance. * * @return AliasPackage */ public function getPackage() { return $this->package; } /** * @inheritDoc */ public function show($lock) { return 'Marking '.$this->package->getPrettyName().' ('.$this->package->getFullPrettyVersion().') as installed, alias of '.$this->package->getAliasOf()->getPrettyName().' ('.$this->package->getAliasOf()->getFullPrettyVersion().')'; } } * Jordi Boggiano * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Composer\DependencyResolver\Operation; use Composer\Package\PackageInterface; /** * Solver uninstall operation. * * @author Konstantin Kudryashov */ class UninstallOperation extends SolverOperation implements OperationInterface { const TYPE = 'uninstall'; /** * @var PackageInterface */ protected $package; public function __construct(PackageInterface $package) { $this->package = $package; } /** * Returns package instance. * * @return PackageInterface */ public function getPackage() { return $this->package; } /** * @inheritDoc */ public function show($lock) { return self::format($this->package, $lock); } /** * @param bool $lock * @return string */ public static function format(PackageInterface $package, $lock = false) { return 'Removing '.$package->getPrettyName().' ('.$package->getFullPrettyVersion().')'; } } * Jordi Boggiano * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Composer\DependencyResolver\Operation; use Composer\Package\PackageInterface; /** * Solver install operation. * * @author Konstantin Kudryashov */ class InstallOperation extends SolverOperation implements OperationInterface { const TYPE = 'install'; /** * @var PackageInterface */ protected $package; public function __construct(PackageInterface $package) { $this->package = $package; } /** * Returns package instance. * * @return PackageInterface */ public function getPackage() { return $this->package; } /** * @inheritDoc */ public function show($lock) { return self::format($this->package, $lock); } /** * @param bool $lock * @return string */ public static function format(PackageInterface $package, $lock = false) { return ($lock ? 'Locking ' : 'Installing ').''.$package->getPrettyName().' ('.$package->getFullPrettyVersion().')'; } } * Jordi Boggiano * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Composer\DependencyResolver; use Composer\Repository\InstalledRepositoryInterface; use Composer\Repository\RepositoryInterface; /** * @author Nils Adermann * @internal */ class LocalRepoTransaction extends Transaction { public function __construct(RepositoryInterface $lockedRepository, InstalledRepositoryInterface $localRepository) { parent::__construct( $localRepository->getPackages(), $lockedRepository->getPackages() ); } } * Jordi Boggiano * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Composer\DependencyResolver; use Composer\Package\AliasPackage; use Composer\Package\BasePackage; use Composer\Package\Package; /** * @author Nils Adermann * @internal */ class LockTransaction extends Transaction { /** * packages in current lock file, platform repo or otherwise present * * Indexed by spl_object_hash * * @var array */ protected $presentMap; /** * Packages which cannot be mapped, platform repo, root package, other fixed repos * * Indexed by package id * * @var array */ protected $unlockableMap; /** * @var array{dev: BasePackage[], non-dev: BasePackage[], all: BasePackage[]} */ protected $resultPackages; /** * @param array $presentMap * @param array $unlockableMap */ public function __construct(Pool $pool, array $presentMap, array $unlockableMap, Decisions $decisions) { $this->presentMap = $presentMap; $this->unlockableMap = $unlockableMap; $this->setResultPackages($pool, $decisions); parent::__construct($this->presentMap, $this->resultPackages['all']); } // TODO make this a bit prettier instead of the two text indexes? /** * @return void */ public function setResultPackages(Pool $pool, Decisions $decisions) { $this->resultPackages = array('all' => array(), 'non-dev' => array(), 'dev' => array()); foreach ($decisions as $i => $decision) { $literal = $decision[Decisions::DECISION_LITERAL]; if ($literal > 0) { $package = $pool->literalToPackage($literal); $this->resultPackages['all'][] = $package; if (!isset($this->unlockableMap[$package->id])) { $this->resultPackages['non-dev'][] = $package; } } } } /** * @return void */ public function setNonDevPackages(LockTransaction $extractionResult) { $packages = $extractionResult->getNewLockPackages(false); $this->resultPackages['dev'] = $this->resultPackages['non-dev']; $this->resultPackages['non-dev'] = array(); foreach ($packages as $package) { foreach ($this->resultPackages['dev'] as $i => $resultPackage) { // TODO this comparison is probably insufficient, aliases, what about modified versions? I guess they aren't possible? if ($package->getName() == $resultPackage->getName()) { $this->resultPackages['non-dev'][] = $resultPackage; unset($this->resultPackages['dev'][$i]); } } } } // TODO additionalFixedRepository needs to be looked at here as well? /** * @param bool $devMode * @param bool $updateMirrors * @return BasePackage[] */ public function getNewLockPackages($devMode, $updateMirrors = false) { $packages = array(); foreach ($this->resultPackages[$devMode ? 'dev' : 'non-dev'] as $package) { if (!$package instanceof AliasPackage) { // if we're just updating mirrors we need to reset references to the same as currently "present" packages' references to keep the lock file as-is // we do not reset references if the currently present package didn't have any, or if the type of VCS has changed if ($updateMirrors && !isset($this->presentMap[spl_object_hash($package)])) { foreach ($this->presentMap as $presentPackage) { if ($package->getName() == $presentPackage->getName() && $package->getVersion() == $presentPackage->getVersion()) { if ($presentPackage->getSourceReference() && $presentPackage->getSourceType() === $package->getSourceType()) { $package->setSourceDistReferences($presentPackage->getSourceReference()); } if ($presentPackage->getReleaseDate() && $package instanceof Package) { $package->setReleaseDate($presentPackage->getReleaseDate()); } } } } $packages[] = $package; } } return $packages; } /** * Checks which of the given aliases from composer.json are actually in use for the lock file * @param array $aliases * @return array */ public function getAliases($aliases) { $usedAliases = array(); foreach ($this->resultPackages['all'] as $package) { if ($package instanceof AliasPackage) { foreach ($aliases as $index => $alias) { if ($alias['package'] === $package->getName()) { $usedAliases[] = $alias; unset($aliases[$index]); } } } } usort($usedAliases, function ($a, $b) { return strcmp($a['package'], $b['package']); }); return $usedAliases; } } * Jordi Boggiano * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Composer\DependencyResolver; /** * @author Nils Adermann * @implements \Iterator */ class RuleSetIterator implements \Iterator { /** @var array */ protected $rules; /** @var array */ protected $types; /** @var int */ protected $currentOffset; /** @var RuleSet::TYPE_*|-1 */ protected $currentType; /** @var int */ protected $currentTypeOffset; /** * @param array $rules */ public function __construct(array $rules) { $this->rules = $rules; $this->types = array_keys($rules); sort($this->types); $this->rewind(); } /** * @return Rule */ #[\ReturnTypeWillChange] public function current() { return $this->rules[$this->currentType][$this->currentOffset]; } /** * @return RuleSet::TYPE_*|-1 */ #[\ReturnTypeWillChange] public function key() { return $this->currentType; } /** * @return void */ #[\ReturnTypeWillChange] public function next() { $this->currentOffset++; if (!isset($this->rules[$this->currentType])) { return; } if ($this->currentOffset >= \count($this->rules[$this->currentType])) { $this->currentOffset = 0; do { $this->currentTypeOffset++; if (!isset($this->types[$this->currentTypeOffset])) { $this->currentType = -1; break; } $this->currentType = $this->types[$this->currentTypeOffset]; } while (isset($this->types[$this->currentTypeOffset]) && !\count($this->rules[$this->currentType])); } } /** * @return void */ #[\ReturnTypeWillChange] public function rewind() { $this->currentOffset = 0; $this->currentTypeOffset = -1; $this->currentType = -1; do { $this->currentTypeOffset++; if (!isset($this->types[$this->currentTypeOffset])) { $this->currentType = -1; break; } $this->currentType = $this->types[$this->currentTypeOffset]; } while (isset($this->types[$this->currentTypeOffset]) && !\count($this->rules[$this->currentType])); } /** * @return bool */ #[\ReturnTypeWillChange] public function valid() { return isset($this->rules[$this->currentType], $this->rules[$this->currentType][$this->currentOffset]); } } * Jordi Boggiano * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Composer\DependencyResolver; /** * An extension of SplDoublyLinkedList with seek and removal of current element * * SplDoublyLinkedList only allows deleting a particular offset and has no * method to set the internal iterator to a particular offset. * * @author Nils Adermann * @extends \SplDoublyLinkedList */ class RuleWatchChain extends \SplDoublyLinkedList { /** * Moves the internal iterator to the specified offset * * @param int $offset The offset to seek to. * @return void */ public function seek($offset) { $this->rewind(); for ($i = 0; $i < $offset; $i++, $this->next()); } /** * Removes the current element from the list * * As SplDoublyLinkedList only allows deleting a particular offset and * incorrectly sets the internal iterator if you delete the current value * this method sets the internal iterator back to the following element * using the seek method. * * @return void */ public function remove() { $offset = $this->key(); $this->offsetUnset($offset); $this->seek($offset); } } * Jordi Boggiano * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Composer\DependencyResolver; /** * @author Nils Adermann */ class GenericRule extends Rule { /** @var int[] */ protected $literals; /** * @param int[] $literals */ public function __construct(array $literals, $reason, $reasonData) { parent::__construct($reason, $reasonData); // sort all packages ascending by id sort($literals); $this->literals = $literals; } /** * @return int[] */ public function getLiterals() { return $this->literals; } /** * @inheritDoc */ public function getHash() { $data = unpack('ihash', md5(implode(',', $this->literals), true)); return $data['hash']; } /** * Checks if this rule is equal to another one * * Ignores whether either of the rules is disabled. * * @param Rule $rule The rule to check against * @return bool Whether the rules are equal */ public function equals(Rule $rule) { return $this->literals === $rule->getLiterals(); } /** * @return bool */ public function isAssertion() { return 1 === \count($this->literals); } /** * Formats a rule as a string of the format (Literal1|Literal2|...) * * @return string */ public function __toString() { $result = $this->isDisabled() ? 'disabled(' : '('; foreach ($this->literals as $i => $literal) { if ($i != 0) { $result .= '|'; } $result .= $literal; } $result .= ')'; return $result; } } * Jordi Boggiano * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Composer\DependencyResolver; /** * Stores decisions on installing, removing or keeping packages * * @author Nils Adermann * @implements \Iterator */ class Decisions implements \Iterator, \Countable { const DECISION_LITERAL = 0; const DECISION_REASON = 1; /** @var Pool */ protected $pool; /** @var array */ protected $decisionMap; /** * @var array */ protected $decisionQueue = array(); public function __construct(Pool $pool) { $this->pool = $pool; $this->decisionMap = array(); } /** * @param int $literal * @param int $level * @return void */ public function decide($literal, $level, Rule $why) { $this->addDecision($literal, $level); $this->decisionQueue[] = array( self::DECISION_LITERAL => $literal, self::DECISION_REASON => $why, ); } /** * @param int $literal * @return bool */ public function satisfy($literal) { $packageId = abs($literal); return ( $literal > 0 && isset($this->decisionMap[$packageId]) && $this->decisionMap[$packageId] > 0 || $literal < 0 && isset($this->decisionMap[$packageId]) && $this->decisionMap[$packageId] < 0 ); } /** * @param int $literal * @return bool */ public function conflict($literal) { $packageId = abs($literal); return ( (isset($this->decisionMap[$packageId]) && $this->decisionMap[$packageId] > 0 && $literal < 0) || (isset($this->decisionMap[$packageId]) && $this->decisionMap[$packageId] < 0 && $literal > 0) ); } /** * @param int $literalOrPackageId * @return bool */ public function decided($literalOrPackageId) { return !empty($this->decisionMap[abs($literalOrPackageId)]); } /** * @param int $literalOrPackageId * @return bool */ public function undecided($literalOrPackageId) { return empty($this->decisionMap[abs($literalOrPackageId)]); } /** * @param int $literalOrPackageId * @return bool */ public function decidedInstall($literalOrPackageId) { $packageId = abs($literalOrPackageId); return isset($this->decisionMap[$packageId]) && $this->decisionMap[$packageId] > 0; } /** * @param int $literalOrPackageId * @return int */ public function decisionLevel($literalOrPackageId) { $packageId = abs($literalOrPackageId); if (isset($this->decisionMap[$packageId])) { return abs($this->decisionMap[$packageId]); } return 0; } /** * @param int $literalOrPackageId * @return Rule|null */ public function decisionRule($literalOrPackageId) { $packageId = abs($literalOrPackageId); foreach ($this->decisionQueue as $decision) { if ($packageId === abs($decision[self::DECISION_LITERAL])) { return $decision[self::DECISION_REASON]; } } return null; } /** * @param int $queueOffset * @return array{0: int, 1: Rule} a literal and decision reason */ public function atOffset($queueOffset) { return $this->decisionQueue[$queueOffset]; } /** * @param int $queueOffset * @return bool */ public function validOffset($queueOffset) { return $queueOffset >= 0 && $queueOffset < \count($this->decisionQueue); } /** * @return Rule */ public function lastReason() { return $this->decisionQueue[\count($this->decisionQueue) - 1][self::DECISION_REASON]; } /** * @return int */ public function lastLiteral() { return $this->decisionQueue[\count($this->decisionQueue) - 1][self::DECISION_LITERAL]; } /** * @return void */ public function reset() { while ($decision = array_pop($this->decisionQueue)) { $this->decisionMap[abs($decision[self::DECISION_LITERAL])] = 0; } } /** * @param int $offset * @return void */ public function resetToOffset($offset) { while (\count($this->decisionQueue) > $offset + 1) { $decision = array_pop($this->decisionQueue); $this->decisionMap[abs($decision[self::DECISION_LITERAL])] = 0; } } /** * @return void */ public function revertLast() { $this->decisionMap[abs($this->lastLiteral())] = 0; array_pop($this->decisionQueue); } /** * @return int */ #[\ReturnTypeWillChange] public function count() { return \count($this->decisionQueue); } /** * @return void */ #[\ReturnTypeWillChange] public function rewind() { end($this->decisionQueue); } /** * @return array{0: int, 1: Rule}|false */ #[\ReturnTypeWillChange] public function current() { return current($this->decisionQueue); } /** * @return ?int */ #[\ReturnTypeWillChange] public function key() { return key($this->decisionQueue); } /** * @return void */ #[\ReturnTypeWillChange] public function next() { prev($this->decisionQueue); } /** * @return bool */ #[\ReturnTypeWillChange] public function valid() { return false !== current($this->decisionQueue); } /** * @return bool */ public function isEmpty() { return \count($this->decisionQueue) === 0; } /** * @param int $literal * @param int $level * @return void */ protected function addDecision($literal, $level) { $packageId = abs($literal); $previousDecision = isset($this->decisionMap[$packageId]) ? $this->decisionMap[$packageId] : null; if ($previousDecision != 0) { $literalString = $this->pool->literalToPrettyString($literal, array()); $package = $this->pool->literalToPackage($literal); throw new SolverBugException( "Trying to decide $literalString on level $level, even though $package was previously decided as ".(int) $previousDecision."." ); } if ($literal > 0) { $this->decisionMap[$packageId] = $level; } else { $this->decisionMap[$packageId] = -$level; } } /** * @return string */ public function toString(Pool $pool = null) { $decisionMap = $this->decisionMap; ksort($decisionMap); $str = '['; foreach ($decisionMap as $packageId => $level) { $str .= (($pool) ? $pool->literalToPackage($packageId) : $packageId).':'.$level.','; } $str .= ']'; return $str; } public function __toString() { return $this->toString(); } } * Jordi Boggiano * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Composer\DependencyResolver; use Composer\Package\PackageInterface; use Composer\Semver\Constraint\Constraint; /** * @author Nils Adermann */ interface PolicyInterface { /** * @param string $operator * @return bool * * @phpstan-param Constraint::STR_OP_* $operator */ public function versionCompare(PackageInterface $a, PackageInterface $b, $operator); /** * @param int[] $literals * @param ?string $requiredPackage * @return int[] */ public function selectPreferredPackages(Pool $pool, array $literals, $requiredPackage = null); } * Jordi Boggiano * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Composer\DependencyResolver; /** * @author Nils Adermann */ class SolverBugException extends \RuntimeException { /** * @param string $message */ public function __construct($message) { parent::__construct( $message."\nThis exception was most likely caused by a bug in Composer.\n". "Please report the command you ran, the exact error you received, and your composer.json on https://github.com/composer/composer/issues - thank you!\n" ); } } * Jordi Boggiano * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Composer\DependencyResolver; use Composer\EventDispatcher\EventDispatcher; use Composer\IO\IOInterface; use Composer\Package\AliasPackage; use Composer\Package\BasePackage; use Composer\Package\CompleteAliasPackage; use Composer\Package\CompletePackage; use Composer\Package\PackageInterface; use Composer\Package\Version\StabilityFilter; use Composer\Pcre\Preg; use Composer\Plugin\PluginEvents; use Composer\Plugin\PrePoolCreateEvent; use Composer\Repository\PlatformRepository; use Composer\Repository\RepositoryInterface; use Composer\Repository\RootPackageRepository; use Composer\Semver\CompilingMatcher; use Composer\Semver\Constraint\Constraint; use Composer\Semver\Constraint\ConstraintInterface; use Composer\Semver\Constraint\MatchAllConstraint; use Composer\Semver\Constraint\MultiConstraint; use Composer\Semver\Intervals; /** * @author Nils Adermann */ class PoolBuilder { /** * @var int[] * @phpstan-var array */ private $acceptableStabilities; /** * @var int[] * @phpstan-var array */ private $stabilityFlags; /** * @var array[] * @phpstan-var array> */ private $rootAliases; /** * @var string[] * @phpstan-var array */ private $rootReferences; /** * @var ?EventDispatcher */ private $eventDispatcher; /** * @var PoolOptimizer|null */ private $poolOptimizer; /** * @var IOInterface */ private $io; /** * @var array[] * @phpstan-var array */ private $aliasMap = array(); /** * @var ConstraintInterface[] * @phpstan-var array */ private $packagesToLoad = array(); /** * @var ConstraintInterface[] * @phpstan-var array */ private $loadedPackages = array(); /** * @var array[] * @phpstan-var array>> */ private $loadedPerRepo = array(); /** * @var BasePackage[] */ private $packages = array(); /** * @var BasePackage[] */ private $unacceptableFixedOrLockedPackages = array(); /** @var string[] */ private $updateAllowList = array(); /** @var array> */ private $skippedLoad = array(); /** * Keeps a list of dependencies which are locked but were auto-unlocked as they are path repositories * * This half-unlocked state means the package itself will update but the UPDATE_LISTED_WITH_TRANSITIVE_DEPS* * flags will not apply until the package really gets unlocked in some other way than being a path repo * * @var array */ private $pathRepoUnlocked = array(); /** * Keeps a list of dependencies which are root requirements, and as such * have already their maximum required range loaded and can not be * extended by markPackageNameForLoading * * Packages get cleared from this list if they get unlocked as in that case * we need to actually load them * * @var array */ private $maxExtendedReqs = array(); /** * @var array * @phpstan-var array */ private $updateAllowWarned = array(); /** @var int */ private $indexCounter = 0; /** * @param int[] $acceptableStabilities array of stability => BasePackage::STABILITY_* value * @phpstan-param array $acceptableStabilities * @param int[] $stabilityFlags an array of package name => BasePackage::STABILITY_* value * @phpstan-param array $stabilityFlags * @param array[] $rootAliases * @phpstan-param array> $rootAliases * @param string[] $rootReferences an array of package name => source reference * @phpstan-param array $rootReferences */ public function __construct(array $acceptableStabilities, array $stabilityFlags, array $rootAliases, array $rootReferences, IOInterface $io, EventDispatcher $eventDispatcher = null, PoolOptimizer $poolOptimizer = null) { $this->acceptableStabilities = $acceptableStabilities; $this->stabilityFlags = $stabilityFlags; $this->rootAliases = $rootAliases; $this->rootReferences = $rootReferences; $this->eventDispatcher = $eventDispatcher; $this->poolOptimizer = $poolOptimizer; $this->io = $io; } /** * @param RepositoryInterface[] $repositories * @return Pool */ public function buildPool(array $repositories, Request $request) { if ($request->getUpdateAllowList()) { $this->updateAllowList = $request->getUpdateAllowList(); $this->warnAboutNonMatchingUpdateAllowList($request); foreach ($request->getLockedRepository()->getPackages() as $lockedPackage) { if (!$this->isUpdateAllowed($lockedPackage)) { // remember which packages we skipped loading remote content for in this partial update $this->skippedLoad[$lockedPackage->getName()][] = $lockedPackage; foreach ($lockedPackage->getReplaces() as $link) { $this->skippedLoad[$link->getTarget()][] = $lockedPackage; } // Path repo packages are never loaded from lock, to force them to always remain in sync // unless symlinking is disabled in which case we probably should rather treat them like // regular packages. We mark them specially so they can be reloaded fully including update propagation // if they do get unlocked, but by default they are unlocked without update propagation. if ($lockedPackage->getDistType() === 'path') { $transportOptions = $lockedPackage->getTransportOptions(); if (!isset($transportOptions['symlink']) || $transportOptions['symlink'] !== false) { $this->pathRepoUnlocked[$lockedPackage->getName()] = true; continue; } } $request->lockPackage($lockedPackage); } } } foreach ($request->getFixedOrLockedPackages() as $package) { // using MatchAllConstraint here because fixed packages do not need to retrigger // loading any packages $this->loadedPackages[$package->getName()] = new MatchAllConstraint(); // replace means conflict, so if a fixed package replaces a name, no need to load that one, packages would conflict anyways foreach ($package->getReplaces() as $link) { $this->loadedPackages[$link->getTarget()] = new MatchAllConstraint(); } // TODO in how far can we do the above for conflicts? It's more tricky cause conflicts can be limited to // specific versions while replace is a conflict with all versions of the name if ( $package->getRepository() instanceof RootPackageRepository || $package->getRepository() instanceof PlatformRepository || StabilityFilter::isPackageAcceptable($this->acceptableStabilities, $this->stabilityFlags, $package->getNames(), $package->getStability()) ) { $this->loadPackage($request, $repositories, $package, false); } else { $this->unacceptableFixedOrLockedPackages[] = $package; } } foreach ($request->getRequires() as $packageName => $constraint) { // fixed and locked packages have already been added, so if a root require needs one of them, no need to do anything if (isset($this->loadedPackages[$packageName])) { continue; } $this->packagesToLoad[$packageName] = $constraint; $this->maxExtendedReqs[$packageName] = true; } // clean up packagesToLoad for anything we manually marked loaded above foreach ($this->packagesToLoad as $name => $constraint) { if (isset($this->loadedPackages[$name])) { unset($this->packagesToLoad[$name]); } } while (!empty($this->packagesToLoad)) { $this->loadPackagesMarkedForLoading($request, $repositories); } foreach ($this->packages as $i => $package) { // we check all alias related packages at once, so no need to check individual aliases // isset also checks non-null value if (!$package instanceof AliasPackage) { $constraint = new Constraint('==', $package->getVersion()); $aliasedPackages = array($i => $package); if (isset($this->aliasMap[spl_object_hash($package)])) { $aliasedPackages += $this->aliasMap[spl_object_hash($package)]; } $found = false; foreach ($aliasedPackages as $packageOrAlias) { if (CompilingMatcher::match($constraint, Constraint::OP_EQ, $packageOrAlias->getVersion())) { $found = true; } } if (!$found) { foreach ($aliasedPackages as $index => $packageOrAlias) { unset($this->packages[$index]); } } } } if ($this->eventDispatcher) { $prePoolCreateEvent = new PrePoolCreateEvent( PluginEvents::PRE_POOL_CREATE, $repositories, $request, $this->acceptableStabilities, $this->stabilityFlags, $this->rootAliases, $this->rootReferences, $this->packages, $this->unacceptableFixedOrLockedPackages ); $this->eventDispatcher->dispatch($prePoolCreateEvent->getName(), $prePoolCreateEvent); $this->packages = $prePoolCreateEvent->getPackages(); $this->unacceptableFixedOrLockedPackages = $prePoolCreateEvent->getUnacceptableFixedPackages(); } $pool = new Pool($this->packages, $this->unacceptableFixedOrLockedPackages); $this->aliasMap = array(); $this->packagesToLoad = array(); $this->loadedPackages = array(); $this->loadedPerRepo = array(); $this->packages = array(); $this->unacceptableFixedOrLockedPackages = array(); $this->maxExtendedReqs = array(); $this->skippedLoad = array(); $this->indexCounter = 0; $pool = $this->runOptimizer($request, $pool); Intervals::clear(); return $pool; } /** * @param string $name * @return void */ private function markPackageNameForLoading(Request $request, $name, ConstraintInterface $constraint) { // Skip platform requires at this stage if (PlatformRepository::isPlatformPackage($name)) { return; } // Root require (which was not unlocked) already loaded the maximum range so no // need to check anything here if (isset($this->maxExtendedReqs[$name])) { return; } // Root requires can not be overruled by dependencies so there is no point in // extending the loaded constraint for those. // This is triggered when loading a root require which was locked but got unlocked, then // we make sure that we load at most the intervals covered by the root constraint. $rootRequires = $request->getRequires(); if (isset($rootRequires[$name]) && !Intervals::isSubsetOf($constraint, $rootRequires[$name])) { $constraint = $rootRequires[$name]; } // Not yet loaded or already marked for a reload, set the constraint to be loaded if (!isset($this->loadedPackages[$name])) { // Maybe it was already marked before but not loaded yet. In that case // we have to extend the constraint (we don't check if they are identical because // MultiConstraint::create() will optimize anyway) if (isset($this->packagesToLoad[$name])) { // Already marked for loading and this does not expand the constraint to be loaded, nothing to do if (Intervals::isSubsetOf($constraint, $this->packagesToLoad[$name])) { return; } // extend the constraint to be loaded $constraint = Intervals::compactConstraint(MultiConstraint::create(array($this->packagesToLoad[$name], $constraint), false)); } $this->packagesToLoad[$name] = $constraint; return; } // No need to load this package with this constraint because it is // a subset of the constraint with which we have already loaded packages if (Intervals::isSubsetOf($constraint, $this->loadedPackages[$name])) { return; } // We have already loaded that package but not in the constraint that's // required. We extend the constraint and mark that package as not being loaded // yet so we get the required package versions $this->packagesToLoad[$name] = Intervals::compactConstraint(MultiConstraint::create(array($this->loadedPackages[$name], $constraint), false)); unset($this->loadedPackages[$name]); } /** * @param RepositoryInterface[] $repositories * @return void */ private function loadPackagesMarkedForLoading(Request $request, array $repositories) { foreach ($this->packagesToLoad as $name => $constraint) { $this->loadedPackages[$name] = $constraint; } $packageBatch = $this->packagesToLoad; $this->packagesToLoad = array(); foreach ($repositories as $repoIndex => $repository) { if (empty($packageBatch)) { break; } // these repos have their packages fixed or locked if they need to be loaded so we // never need to load anything else from them if ($repository instanceof PlatformRepository || $repository === $request->getLockedRepository()) { continue; } $result = $repository->loadPackages($packageBatch, $this->acceptableStabilities, $this->stabilityFlags, isset($this->loadedPerRepo[$repoIndex]) ? $this->loadedPerRepo[$repoIndex] : array()); foreach ($result['namesFound'] as $name) { // avoid loading the same package again from other repositories once it has been found unset($packageBatch[$name]); } foreach ($result['packages'] as $package) { $this->loadedPerRepo[$repoIndex][$package->getName()][$package->getVersion()] = $package; $this->loadPackage($request, $repositories, $package, !isset($this->pathRepoUnlocked[$package->getName()])); } } } /** * @param bool $propagateUpdate * @param RepositoryInterface[] $repositories * @return void */ private function loadPackage(Request $request, array $repositories, BasePackage $package, $propagateUpdate) { $index = $this->indexCounter++; $this->packages[$index] = $package; if ($package instanceof AliasPackage) { $this->aliasMap[spl_object_hash($package->getAliasOf())][$index] = $package; } $name = $package->getName(); // we're simply setting the root references on all versions for a name here and rely on the solver to pick the // right version. It'd be more work to figure out which versions and which aliases of those versions this may // apply to if (isset($this->rootReferences[$name])) { // do not modify the references on already locked or fixed packages if (!$request->isLockedPackage($package) && !$request->isFixedPackage($package)) { $package->setSourceDistReferences($this->rootReferences[$name]); } } // if propagateUpdate is false we are loading a fixed or locked package, root aliases do not apply as they are // manually loaded as separate packages in this case if ($propagateUpdate && isset($this->rootAliases[$name][$package->getVersion()])) { $alias = $this->rootAliases[$name][$package->getVersion()]; if ($package instanceof AliasPackage) { $basePackage = $package->getAliasOf(); } else { $basePackage = $package; } if ($basePackage instanceof CompletePackage) { $aliasPackage = new CompleteAliasPackage($basePackage, $alias['alias_normalized'], $alias['alias']); } else { $aliasPackage = new AliasPackage($basePackage, $alias['alias_normalized'], $alias['alias']); } $aliasPackage->setRootPackageAlias(true); $newIndex = $this->indexCounter++; $this->packages[$newIndex] = $aliasPackage; $this->aliasMap[spl_object_hash($aliasPackage->getAliasOf())][$newIndex] = $aliasPackage; } foreach ($package->getRequires() as $link) { $require = $link->getTarget(); $linkConstraint = $link->getConstraint(); // if the required package is loaded as a locked package only and hasn't had its deps analyzed if (isset($this->skippedLoad[$require])) { // if we're doing a full update or this is a partial update with transitive deps and we're currently // looking at a package which needs to be updated we need to unlock the package we now know is a // dependency of another package which we are trying to update, and then attempt to load it again if ($propagateUpdate && $request->getUpdateAllowTransitiveDependencies()) { $skippedRootRequires = $this->getSkippedRootRequires($request, $require); if ($request->getUpdateAllowTransitiveRootDependencies() || !$skippedRootRequires) { $this->unlockPackage($request, $repositories, $require); $this->markPackageNameForLoading($request, $require, $linkConstraint); } else { foreach ($skippedRootRequires as $rootRequire) { if (!isset($this->updateAllowWarned[$rootRequire])) { $this->updateAllowWarned[$rootRequire] = true; $this->io->writeError('Dependency '.$rootRequire.' is also a root requirement. Package has not been listed as an update argument, so keeping locked at old version. Use --with-all-dependencies (-W) to include root dependencies.'); } } } } elseif (isset($this->pathRepoUnlocked[$require]) && !isset($this->loadedPackages[$require])) { // if doing a partial update and a package depends on a path-repo-unlocked package which is not referenced by the root, we need to ensure it gets loaded as it was not loaded by the request's root requirements // and would not be loaded above if update propagation is not allowed (which happens if the requirer is itself a path-repo-unlocked package) or if transitive deps are not allowed to be unlocked $this->markPackageNameForLoading($request, $require, $linkConstraint); } } else { $this->markPackageNameForLoading($request, $require, $linkConstraint); } } // if we're doing a partial update with deps we also need to unlock packages which are being replaced in case // they are currently locked and thus prevent this updateable package from being installable/updateable if ($propagateUpdate && $request->getUpdateAllowTransitiveDependencies()) { foreach ($package->getReplaces() as $link) { $replace = $link->getTarget(); if (isset($this->loadedPackages[$replace], $this->skippedLoad[$replace])) { $skippedRootRequires = $this->getSkippedRootRequires($request, $replace); if ($request->getUpdateAllowTransitiveRootDependencies() || !$skippedRootRequires) { $this->unlockPackage($request, $repositories, $replace); $this->markPackageNameForLoading($request, $replace, $link->getConstraint()); } else { foreach ($skippedRootRequires as $rootRequire) { if (!isset($this->updateAllowWarned[$rootRequire])) { $this->updateAllowWarned[$rootRequire] = true; $this->io->writeError('Dependency '.$rootRequire.' is also a root requirement. Package has not been listed as an update argument, so keeping locked at old version. Use --with-all-dependencies (-W) to include root dependencies.'); } } } } } } } /** * Checks if a particular name is required directly in the request * * @param string $name packageName * @return bool */ private function isRootRequire(Request $request, $name) { $rootRequires = $request->getRequires(); return isset($rootRequires[$name]); } /** * @param string $name * @return string[] */ private function getSkippedRootRequires(Request $request, $name) { if (!isset($this->skippedLoad[$name])) { return array(); } $rootRequires = $request->getRequires(); $matches = array(); if (isset($rootRequires[$name])) { return array_map(function (PackageInterface $package) use ($name) { if ($name !== $package->getName()) { return $package->getName() .' (via replace of '.$name.')'; } return $package->getName(); }, $this->skippedLoad[$name]); } foreach ($this->skippedLoad[$name] as $packageOrReplacer) { if (isset($rootRequires[$packageOrReplacer->getName()])) { $matches[] = $packageOrReplacer->getName(); } foreach ($packageOrReplacer->getReplaces() as $link) { if (isset($rootRequires[$link->getTarget()])) { if ($name !== $packageOrReplacer->getName()) { $matches[] = $packageOrReplacer->getName() .' (via replace of '.$name.')'; } else { $matches[] = $packageOrReplacer->getName(); } break; } } } return $matches; } /** * Checks whether the update allow list allows this package in the lock file to be updated * * @return bool */ private function isUpdateAllowed(BasePackage $package) { foreach ($this->updateAllowList as $pattern => $void) { $patternRegexp = BasePackage::packageNameToRegexp($pattern); if (Preg::isMatch($patternRegexp, $package->getName())) { return true; } } return false; } /** * @return void */ private function warnAboutNonMatchingUpdateAllowList(Request $request) { foreach ($this->updateAllowList as $pattern => $void) { $patternRegexp = BasePackage::packageNameToRegexp($pattern); // update pattern matches a locked package? => all good foreach ($request->getLockedRepository()->getPackages() as $package) { if (Preg::isMatch($patternRegexp, $package->getName())) { continue 2; } } // update pattern matches a root require? => all good, probably a new package foreach ($request->getRequires() as $packageName => $constraint) { if (Preg::isMatch($patternRegexp, $packageName)) { continue 2; } } if (strpos($pattern, '*') !== false) { $this->io->writeError('Pattern "' . $pattern . '" listed for update does not match any locked packages.'); } else { $this->io->writeError('Package "' . $pattern . '" listed for update is not locked.'); } } } /** * Reverts the decision to use a locked package if a partial update with transitive dependencies * found that this package actually needs to be updated * * @param RepositoryInterface[] $repositories * @param string $name * @return void */ private function unlockPackage(Request $request, array $repositories, $name) { foreach ($this->skippedLoad[$name] as $packageOrReplacer) { // if we unfixed a replaced package name, we also need to unfix the replacer itself // as long as it was not unfixed yet if ($packageOrReplacer->getName() !== $name && isset($this->skippedLoad[$packageOrReplacer->getName()])) { $replacerName = $packageOrReplacer->getName(); if ($request->getUpdateAllowTransitiveRootDependencies() || (!$this->isRootRequire($request, $name) && !$this->isRootRequire($request, $replacerName))) { $this->unlockPackage($request, $repositories, $replacerName); if ($this->isRootRequire($request, $replacerName)) { $this->markPackageNameForLoading($request, $replacerName, new MatchAllConstraint); } else { foreach ($this->packages as $loadedPackage) { $requires = $loadedPackage->getRequires(); if (isset($requires[$replacerName])) { $this->markPackageNameForLoading($request, $replacerName, $requires[$replacerName]->getConstraint()); } } } } } } if (isset($this->pathRepoUnlocked[$name])) { foreach ($this->packages as $index => $package) { if ($package->getName() === $name) { $this->removeLoadedPackage($request, $repositories, $package, $index); } } } unset($this->skippedLoad[$name], $this->loadedPackages[$name], $this->maxExtendedReqs[$name], $this->pathRepoUnlocked[$name]); // remove locked package by this name which was already initialized foreach ($request->getLockedPackages() as $lockedPackage) { if (!($lockedPackage instanceof AliasPackage) && $lockedPackage->getName() === $name) { if (false !== $index = array_search($lockedPackage, $this->packages, true)) { $request->unlockPackage($lockedPackage); $this->removeLoadedPackage($request, $repositories, $lockedPackage, $index); // make sure that any requirements for this package by other locked or fixed packages are now // also loaded, as they were previously ignored because the locked (now unlocked) package already // satisfied their requirements foreach ($request->getFixedOrLockedPackages() as $fixedOrLockedPackage) { if ($fixedOrLockedPackage === $lockedPackage) { continue; } if (isset($this->skippedLoad[$fixedOrLockedPackage->getName()])) { $requires = $fixedOrLockedPackage->getRequires(); if (isset($requires[$lockedPackage->getName()])) { $this->markPackageNameForLoading($request, $lockedPackage->getName(), $requires[$lockedPackage->getName()]->getConstraint()); } } } } } } } /** * @param RepositoryInterface[] $repositories * @param int $index * @return void */ private function removeLoadedPackage(Request $request, array $repositories, BasePackage $package, $index) { $repoIndex = array_search($package->getRepository(), $repositories, true); unset($this->loadedPerRepo[$repoIndex][$package->getName()][$package->getVersion()]); unset($this->packages[$index]); if (isset($this->aliasMap[spl_object_hash($package)])) { foreach ($this->aliasMap[spl_object_hash($package)] as $aliasIndex => $aliasPackage) { unset($this->loadedPerRepo[$repoIndex][$aliasPackage->getName()][$aliasPackage->getVersion()]); unset($this->packages[$aliasIndex]); } unset($this->aliasMap[spl_object_hash($package)]); } } /** * @return Pool */ private function runOptimizer(Request $request, Pool $pool) { if (null === $this->poolOptimizer) { return $pool; } $total = \count($pool->getPackages()); $pool = $this->poolOptimizer->optimize($request, $pool); $filtered = $total - \count($pool->getPackages()); if (0 === $filtered) { return $pool; } $this->io->write(sprintf( 'Found %s package versions referenced in your dependency graph. %s (%d%%) were optimized away.', number_format($total), number_format($filtered), round(100/$total*$filtered) ), true, IOInterface::VERY_VERBOSE); return $pool; } } * Jordi Boggiano * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Composer\DependencyResolver; use Composer\Package\CompletePackageInterface; use Composer\Package\AliasPackage; use Composer\Package\BasePackage; use Composer\Package\Link; use Composer\Package\PackageInterface; use Composer\Package\RootPackageInterface; use Composer\Pcre\Preg; use Composer\Repository\RepositorySet; use Composer\Repository\LockArrayRepository; use Composer\Semver\Constraint\Constraint; use Composer\Semver\Constraint\ConstraintInterface; use Composer\Package\Version\VersionParser; use Composer\Repository\PlatformRepository; /** * Represents a problem detected while solving dependencies * * @author Nils Adermann */ class Problem { /** * A map containing the id of each rule part of this problem as a key * @var array */ protected $reasonSeen; /** * A set of reasons for the problem, each is a rule or a root require and a rule * @var array> */ protected $reasons = array(); /** @var int */ protected $section = 0; /** * Add a rule as a reason * * @param Rule $rule A rule which is a reason for this problem * @return void */ public function addRule(Rule $rule) { $this->addReason(spl_object_hash($rule), $rule); } /** * Retrieve all reasons for this problem * * @return array> The problem's reasons */ public function getReasons() { return $this->reasons; } /** * A human readable textual representation of the problem's reasons * * @param bool $isVerbose * @param array $installedMap A map of all present packages * @param array $learnedPool * @return string */ public function getPrettyString(RepositorySet $repositorySet, Request $request, Pool $pool, $isVerbose, array $installedMap = array(), array $learnedPool = array()) { // TODO doesn't this entirely defeat the purpose of the problem sections? what's the point of sections? $reasons = call_user_func_array('array_merge', array_reverse($this->reasons)); if (count($reasons) === 1) { reset($reasons); $rule = current($reasons); if (!in_array($rule->getReason(), array(Rule::RULE_ROOT_REQUIRE, Rule::RULE_FIXED), true)) { throw new \LogicException("Single reason problems must contain a request rule."); } $reasonData = $rule->getReasonData(); $packageName = $reasonData['packageName']; $constraint = $reasonData['constraint']; if (isset($constraint)) { $packages = $pool->whatProvides($packageName, $constraint); } else { $packages = array(); } if (empty($packages)) { return "\n ".implode(self::getMissingPackageReason($repositorySet, $request, $pool, $isVerbose, $packageName, $constraint)); } } return self::formatDeduplicatedRules($reasons, ' ', $repositorySet, $request, $pool, $isVerbose, $installedMap, $learnedPool); } /** * @param Rule[] $rules * @param string $indent * @param bool $isVerbose * @param array $installedMap A map of all present packages * @param array $learnedPool * @return string * @internal */ public static function formatDeduplicatedRules($rules, $indent, RepositorySet $repositorySet, Request $request, Pool $pool, $isVerbose, array $installedMap = array(), array $learnedPool = array()) { $messages = array(); $templates = array(); $parser = new VersionParser; $deduplicatableRuleTypes = array(Rule::RULE_PACKAGE_REQUIRES, Rule::RULE_PACKAGE_CONFLICT); foreach ($rules as $rule) { $message = $rule->getPrettyString($repositorySet, $request, $pool, $isVerbose, $installedMap, $learnedPool); if (in_array($rule->getReason(), $deduplicatableRuleTypes, true) && Preg::isMatch('{^(?P\S+) (?P\S+) (?Prequires|conflicts)}', $message, $m)) { $template = Preg::replace('{^\S+ \S+ }', '%s%s ', $message); $messages[] = $template; $templates[$template][$m[1]][$parser->normalize($m[2])] = $m[2]; $sourcePackage = $rule->getSourcePackage($pool); foreach ($pool->getRemovedVersionsByPackage(spl_object_hash($sourcePackage)) as $version => $prettyVersion) { $templates[$template][$m[1]][$version] = $prettyVersion; } } elseif ($message !== '') { $messages[] = $message; } } $result = array(); foreach (array_unique($messages) as $message) { if (isset($templates[$message])) { foreach ($templates[$message] as $package => $versions) { uksort($versions, 'version_compare'); if (!$isVerbose) { $versions = self::condenseVersionList($versions, 1); } if (count($versions) > 1) { // remove the s from requires/conflicts to correct grammar $message = Preg::replace('{^(%s%s (?:require|conflict))s}', '$1', $message); $result[] = sprintf($message, $package, '['.implode(', ', $versions).']'); } else { $result[] = sprintf($message, $package, ' '.reset($versions)); } } } else { $result[] = $message; } } return "\n$indent- ".implode("\n$indent- ", $result); } /** * @return bool */ public function isCausedByLock(RepositorySet $repositorySet, Request $request, Pool $pool) { foreach ($this->reasons as $sectionRules) { foreach ($sectionRules as $rule) { if ($rule->isCausedByLock($repositorySet, $request, $pool)) { return true; } } } return false; } /** * Store a reason descriptor but ignore duplicates * * @param string $id A canonical identifier for the reason * @param Rule $reason The reason descriptor * @return void */ protected function addReason($id, Rule $reason) { // TODO: if a rule is part of a problem description in two sections, isn't this going to remove a message // that is important to understand the issue? if (!isset($this->reasonSeen[$id])) { $this->reasonSeen[$id] = true; $this->reasons[$this->section][] = $reason; } } /** * @return void */ public function nextSection() { $this->section++; } /** * @internal * @param bool $isVerbose * @param string $packageName * @return array{0: string, 1: string} */ public static function getMissingPackageReason(RepositorySet $repositorySet, Request $request, Pool $pool, $isVerbose, $packageName, ConstraintInterface $constraint = null) { if (PlatformRepository::isPlatformPackage($packageName)) { // handle php/php-*/hhvm if (0 === stripos($packageName, 'php') || $packageName === 'hhvm') { $version = self::getPlatformPackageVersion($pool, $packageName, phpversion()); $msg = "- Root composer.json requires ".$packageName.self::constraintToText($constraint).' but '; if (defined('HHVM_VERSION') || ($packageName === 'hhvm' && count($pool->whatProvides($packageName)) > 0)) { return array($msg, 'your HHVM version does not satisfy that requirement.'); } if ($packageName === 'hhvm') { return array($msg, 'HHVM was not detected on this machine, make sure it is in your PATH.'); } if (null === $version) { return array($msg, 'the '.$packageName.' package is disabled by your platform config. Enable it again with "composer config platform.'.$packageName.' --unset".'); } return array($msg, 'your '.$packageName.' version ('. $version .') does not satisfy that requirement.'); } // handle php extensions if (0 === stripos($packageName, 'ext-')) { if (false !== strpos($packageName, ' ')) { return array('- ', "PHP extension ".$packageName.' should be required as '.str_replace(' ', '-', $packageName).'.'); } $ext = substr($packageName, 4); $msg = "- Root composer.json requires PHP extension ".$packageName.self::constraintToText($constraint).' but '; $version = self::getPlatformPackageVersion($pool, $packageName, phpversion($ext) ?: '0'); if (null === $version) { if (extension_loaded($ext)) { return array( $msg, 'the '.$packageName.' package is disabled by your platform config. Enable it again with "composer config platform.'.$packageName.' --unset".', ); } return array($msg, 'it is missing from your system. Install or enable PHP\'s '.$ext.' extension.'); } return array($msg, 'it has the wrong version installed ('.$version.').'); } // handle linked libs if (0 === stripos($packageName, 'lib-')) { if (strtolower($packageName) === 'lib-icu') { $error = extension_loaded('intl') ? 'it has the wrong version installed, try upgrading the intl extension.' : 'it is missing from your system, make sure the intl extension is loaded.'; return array("- Root composer.json requires linked library ".$packageName.self::constraintToText($constraint).' but ', $error); } return array("- Root composer.json requires linked library ".$packageName.self::constraintToText($constraint).' but ', 'it has the wrong version installed or is missing from your system, make sure to load the extension providing it.'); } } $lockedPackage = null; foreach ($request->getLockedPackages() as $package) { if ($package->getName() === $packageName) { $lockedPackage = $package; if ($pool->isUnacceptableFixedOrLockedPackage($package)) { return array("- ", $package->getPrettyName().' is fixed to '.$package->getPrettyVersion().' (lock file version) by a partial update but that version is rejected by your minimum-stability. Make sure you list it as an argument for the update command.'); } break; } } // first check if the actual requested package is found in normal conditions // if so it must mean it is rejected by another constraint than the one given here if ($packages = $repositorySet->findPackages($packageName, $constraint)) { $rootReqs = $repositorySet->getRootRequires(); if (isset($rootReqs[$packageName])) { $filtered = array_filter($packages, function ($p) use ($rootReqs, $packageName) { return $rootReqs[$packageName]->matches(new Constraint('==', $p->getVersion())); }); if (0 === count($filtered)) { return array("- Root composer.json requires $packageName".self::constraintToText($constraint) . ', ', 'found '.self::getPackageList($packages, $isVerbose, $pool, $constraint).' but '.(self::hasMultipleNames($packages) ? 'these conflict' : 'it conflicts').' with your root composer.json require ('.$rootReqs[$packageName]->getPrettyString().').'); } } if ($lockedPackage) { $fixedConstraint = new Constraint('==', $lockedPackage->getVersion()); $filtered = array_filter($packages, function ($p) use ($fixedConstraint) { return $fixedConstraint->matches(new Constraint('==', $p->getVersion())); }); if (0 === count($filtered)) { return array("- Root composer.json requires $packageName".self::constraintToText($constraint) . ', ', 'found '.self::getPackageList($packages, $isVerbose, $pool, $constraint).' but the package is fixed to '.$lockedPackage->getPrettyVersion().' (lock file version) by a partial update and that version does not match. Make sure you list it as an argument for the update command.'); } } $nonLockedPackages = array_filter($packages, function ($p) { return !$p->getRepository() instanceof LockArrayRepository; }); if (!$nonLockedPackages) { return array("- Root composer.json requires $packageName".self::constraintToText($constraint) . ', ', 'found '.self::getPackageList($packages, $isVerbose, $pool, $constraint).' in the lock file but not in remote repositories, make sure you avoid updating this package to keep the one from the lock file.'); } return array("- Root composer.json requires $packageName".self::constraintToText($constraint) . ', ', 'found '.self::getPackageList($packages, $isVerbose, $pool, $constraint).' but these were not loaded, likely because '.(self::hasMultipleNames($packages) ? 'they conflict' : 'it conflicts').' with another require.'); } // check if the package is found when bypassing stability checks if ($packages = $repositorySet->findPackages($packageName, $constraint, RepositorySet::ALLOW_UNACCEPTABLE_STABILITIES)) { // we must first verify if a valid package would be found in a lower priority repository if ($allReposPackages = $repositorySet->findPackages($packageName, $constraint, RepositorySet::ALLOW_SHADOWED_REPOSITORIES)) { return self::computeCheckForLowerPrioRepo($pool, $isVerbose, $packageName, $packages, $allReposPackages, 'minimum-stability', $constraint); } return array("- Root composer.json requires $packageName".self::constraintToText($constraint) . ', ', 'found '.self::getPackageList($packages, $isVerbose, $pool, $constraint).' but '.(self::hasMultipleNames($packages) ? 'these do' : 'it does').' not match your minimum-stability.'); } // check if the package is found when bypassing the constraint and stability checks if ($packages = $repositorySet->findPackages($packageName, null, RepositorySet::ALLOW_UNACCEPTABLE_STABILITIES)) { // we must first verify if a valid package would be found in a lower priority repository if ($allReposPackages = $repositorySet->findPackages($packageName, $constraint, RepositorySet::ALLOW_SHADOWED_REPOSITORIES)) { return self::computeCheckForLowerPrioRepo($pool, $isVerbose, $packageName, $packages, $allReposPackages, 'constraint', $constraint); } $suffix = ''; if ($constraint instanceof Constraint && $constraint->getVersion() === 'dev-master') { foreach ($packages as $candidate) { if (in_array($candidate->getVersion(), array('dev-default', 'dev-main'), true)) { $suffix = ' Perhaps dev-master was renamed to '.$candidate->getPrettyVersion().'?'; break; } } } // check if the root package is a name match and hint the dependencies on root troubleshooting article $allReposPackages = $packages; $topPackage = reset($allReposPackages); if ($topPackage instanceof RootPackageInterface) { $suffix = ' See https://getcomposer.org/dep-on-root for details and assistance.'; } return array("- Root composer.json requires $packageName".self::constraintToText($constraint) . ', ', 'found '.self::getPackageList($packages, $isVerbose, $pool, $constraint).' but '.(self::hasMultipleNames($packages) ? 'these do' : 'it does').' not match the constraint.' . $suffix); } if (!Preg::isMatch('{^[A-Za-z0-9_./-]+$}', $packageName)) { $illegalChars = Preg::replace('{[A-Za-z0-9_./-]+}', '', $packageName); return array("- Root composer.json requires $packageName, it ", 'could not be found, it looks like its name is invalid, "'.$illegalChars.'" is not allowed in package names.'); } if ($providers = $repositorySet->getProviders($packageName)) { $maxProviders = 20; $providersStr = implode(array_map(function ($p) { $description = $p['description'] ? ' '.substr($p['description'], 0, 100) : ''; return " - ${p['name']}".$description."\n"; }, count($providers) > $maxProviders + 1 ? array_slice($providers, 0, $maxProviders) : $providers)); if (count($providers) > $maxProviders + 1) { $providersStr .= ' ... and '.(count($providers) - $maxProviders).' more.'."\n"; } return array("- Root composer.json requires $packageName".self::constraintToText($constraint).", it ", "could not be found in any version, but the following packages provide it:\n".$providersStr." Consider requiring one of these to satisfy the $packageName requirement."); } return array("- Root composer.json requires $packageName, it ", "could not be found in any version, there may be a typo in the package name."); } /** * @internal * @param PackageInterface[] $packages * @param bool $isVerbose * @param bool $useRemovedVersionGroup * @return string */ public static function getPackageList(array $packages, $isVerbose, Pool $pool = null, ConstraintInterface $constraint = null, $useRemovedVersionGroup = false) { $prepared = array(); $hasDefaultBranch = array(); foreach ($packages as $package) { $prepared[$package->getName()]['name'] = $package->getPrettyName(); $prepared[$package->getName()]['versions'][$package->getVersion()] = $package->getPrettyVersion().($package instanceof AliasPackage ? ' (alias of '.$package->getAliasOf()->getPrettyVersion().')' : ''); if ($pool && $constraint) { foreach ($pool->getRemovedVersions($package->getName(), $constraint) as $version => $prettyVersion) { $prepared[$package->getName()]['versions'][$version] = $prettyVersion; } } if ($pool && $useRemovedVersionGroup) { foreach ($pool->getRemovedVersionsByPackage(spl_object_hash($package)) as $version => $prettyVersion) { $prepared[$package->getName()]['versions'][$version] = $prettyVersion; } } if ($package->isDefaultBranch()) { $hasDefaultBranch[$package->getName()] = true; } } $preparedStrings = array(); foreach ($prepared as $name => $package) { // remove the implicit default branch alias to avoid cruft in the display if (isset($package['versions'][VersionParser::DEFAULT_BRANCH_ALIAS], $hasDefaultBranch[$name])) { unset($package['versions'][VersionParser::DEFAULT_BRANCH_ALIAS]); } uksort($package['versions'], 'version_compare'); if (!$isVerbose) { $package['versions'] = self::condenseVersionList($package['versions'], 4); } $preparedStrings[] = $package['name'].'['.implode(', ', $package['versions']).']'; } return implode(', ', $preparedStrings); } /** * @param string $packageName * @param string $version the effective runtime version of the platform package * @return ?string a version string or null if it appears the package was artificially disabled */ private static function getPlatformPackageVersion(Pool $pool, $packageName, $version) { $available = $pool->whatProvides($packageName); if (count($available)) { $selected = null; foreach ($available as $pkg) { if ($pkg->getRepository() instanceof PlatformRepository) { $selected = $pkg; break; } } if ($selected === null) { $selected = reset($available); } // must be a package providing/replacing and not a real platform package if ($selected->getName() !== $packageName) { /** @var Link $link */ foreach (array_merge(array_values($selected->getProvides()), array_values($selected->getReplaces())) as $link) { if ($link->getTarget() === $packageName) { return $link->getPrettyConstraint().' '.substr($link->getDescription(), 0, -1).'d by '.$selected->getPrettyString(); } } } $version = $selected->getPrettyVersion(); $extra = $selected->getExtra(); if ($selected instanceof CompletePackageInterface && isset($extra['config.platform']) && $extra['config.platform'] === true) { $version .= '; ' . str_replace('Package ', '', $selected->getDescription()); } } else { return null; } return $version; } /** * @param string[] $versions an array of pretty versions, with normalized versions as keys * @param int $max * @param int $maxDev * @return list a list of pretty versions and '...' where versions were removed */ private static function condenseVersionList(array $versions, $max, $maxDev = 16) { if (count($versions) <= $max) { return $versions; } $filtered = array(); $byMajor = array(); foreach ($versions as $version => $pretty) { if (0 === stripos($version, 'dev-')) { $byMajor['dev'][] = $pretty; } else { $byMajor[Preg::replace('{^(\d+)\..*}', '$1', $version)][] = $pretty; } } foreach ($byMajor as $majorVersion => $versionsForMajor) { $maxVersions = $majorVersion === 'dev' ? $maxDev : $max; if (count($versionsForMajor) > $maxVersions) { // output only 1st and last versions $filtered[] = $versionsForMajor[0]; $filtered[] = '...'; $filtered[] = $versionsForMajor[count($versionsForMajor) - 1]; } else { $filtered = array_merge($filtered, $versionsForMajor); } } return $filtered; } /** * @param PackageInterface[] $packages * @return bool */ private static function hasMultipleNames(array $packages) { $name = null; foreach ($packages as $package) { if ($name === null || $name === $package->getName()) { $name = $package->getName(); } else { return true; } } return false; } /** * @param bool $isVerbose * @param string $packageName * @param PackageInterface[] $higherRepoPackages * @param PackageInterface[] $allReposPackages * @param string $reason * @return array{0: string, 1: string} */ private static function computeCheckForLowerPrioRepo(Pool $pool, $isVerbose, $packageName, array $higherRepoPackages, array $allReposPackages, $reason, ConstraintInterface $constraint = null) { $nextRepoPackages = array(); $nextRepo = null; foreach ($allReposPackages as $package) { if ($nextRepo === null || $nextRepo === $package->getRepository()) { $nextRepoPackages[] = $package; $nextRepo = $package->getRepository(); } else { break; } } if ($higherRepoPackages) { $topPackage = reset($higherRepoPackages); if ($topPackage instanceof RootPackageInterface) { return array( "- Root composer.json requires $packageName".self::constraintToText($constraint).', it is ', 'satisfiable by '.self::getPackageList($nextRepoPackages, $isVerbose, $pool, $constraint).' from '.$nextRepo->getRepoName().' but '.$topPackage->getPrettyName().' is the root package and cannot be modified. See https://getcomposer.org/dep-on-root for details and assistance.', ); } } if ($nextRepo instanceof LockArrayRepository) { $singular = count($higherRepoPackages) === 1; return array("- Root composer.json requires $packageName".self::constraintToText($constraint) . ', it is ', 'found '.self::getPackageList($nextRepoPackages, $isVerbose, $pool, $constraint).' in the lock file and '.self::getPackageList($higherRepoPackages, $isVerbose, $pool, $constraint).' from '.reset($higherRepoPackages)->getRepository()->getRepoName().' but ' . ($singular ? 'it does' : 'these do') . ' not match your '.$reason.' and ' . ($singular ? 'is' : 'are') . ' therefore not installable. Make sure you either fix the '.$reason.' or avoid updating this package to keep the one from the lock file.', ); } return array("- Root composer.json requires $packageName".self::constraintToText($constraint) . ', it is ', 'satisfiable by '.self::getPackageList($nextRepoPackages, $isVerbose, $pool, $constraint).' from '.$nextRepo->getRepoName().' but '.self::getPackageList($higherRepoPackages, $isVerbose, $pool, $constraint).' from '.reset($higherRepoPackages)->getRepository()->getRepoName().' has higher repository priority. The packages from the higher priority repository do not match your '.$reason.' and are therefore not installable. That repository is canonical so the lower priority repo\'s packages are not installable. See https://getcomposer.org/repoprio for details and assistance.'); } /** * Turns a constraint into text usable in a sentence describing a request * * @return string */ protected static function constraintToText(ConstraintInterface $constraint = null) { return $constraint ? ' '.$constraint->getPrettyString() : ''; } } * Jordi Boggiano * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Composer\DependencyResolver; use Composer\Filter\PlatformRequirementFilter\IgnoreListPlatformRequirementFilter; use Composer\Filter\PlatformRequirementFilter\PlatformRequirementFilterFactory; use Composer\Filter\PlatformRequirementFilter\PlatformRequirementFilterInterface; use Composer\Package\BasePackage; use Composer\Package\AliasPackage; use Composer\Repository\PlatformRepository; /** * @author Nils Adermann * @phpstan-import-type ReasonData from Rule */ class RuleSetGenerator { /** @var PolicyInterface */ protected $policy; /** @var Pool */ protected $pool; /** @var RuleSet */ protected $rules; /** @var array */ protected $addedMap = array(); /** @var array */ protected $addedPackagesByNames = array(); public function __construct(PolicyInterface $policy, Pool $pool) { $this->policy = $policy; $this->pool = $pool; $this->rules = new RuleSet; } /** * Creates a new rule for the requirements of a package * * This rule is of the form (-A|B|C), where B and C are the providers of * one requirement of the package A. * * @param BasePackage $package The package with a requirement * @param BasePackage[] $providers The providers of the requirement * @param Rule::RULE_* $reason A RULE_* constant describing the reason for generating this rule * @param mixed $reasonData Any data, e.g. the requirement name, that goes with the reason * @return Rule|null The generated rule or null if tautological * * @phpstan-param ReasonData $reasonData */ protected function createRequireRule(BasePackage $package, array $providers, $reason, $reasonData = null) { $literals = array(-$package->id); foreach ($providers as $provider) { // self fulfilling rule? if ($provider === $package) { return null; } $literals[] = $provider->id; } return new GenericRule($literals, $reason, $reasonData); } /** * Creates a rule to install at least one of a set of packages * * The rule is (A|B|C) with A, B and C different packages. If the given * set of packages is empty an impossible rule is generated. * * @param BasePackage[] $packages The set of packages to choose from * @param Rule::RULE_* $reason A RULE_* constant describing the reason for * generating this rule * @param array $reasonData Additional data like the root require or fix request info * @return Rule The generated rule * * @phpstan-param ReasonData $reasonData */ protected function createInstallOneOfRule(array $packages, $reason, $reasonData) { $literals = array(); foreach ($packages as $package) { $literals[] = $package->id; } return new GenericRule($literals, $reason, $reasonData); } /** * Creates a rule for two conflicting packages * * The rule for conflicting packages A and B is (-A|-B). A is called the issuer * and B the provider. * * @param BasePackage $issuer The package declaring the conflict * @param BasePackage $provider The package causing the conflict * @param Rule::RULE_* $reason A RULE_* constant describing the reason for generating this rule * @param mixed $reasonData Any data, e.g. the package name, that goes with the reason * @return ?Rule The generated rule * * @phpstan-param ReasonData $reasonData */ protected function createRule2Literals(BasePackage $issuer, BasePackage $provider, $reason, $reasonData = null) { // ignore self conflict if ($issuer === $provider) { return null; } return new Rule2Literals(-$issuer->id, -$provider->id, $reason, $reasonData); } /** * @param BasePackage[] $packages * @param Rule::RULE_* $reason A RULE_* constant * @param mixed $reasonData * @return Rule * * @phpstan-param ReasonData $reasonData */ protected function createMultiConflictRule(array $packages, $reason, $reasonData) { $literals = array(); foreach ($packages as $package) { $literals[] = -$package->id; } if (\count($literals) == 2) { return new Rule2Literals($literals[0], $literals[1], $reason, $reasonData); } return new MultiConflictRule($literals, $reason, $reasonData); } /** * Adds a rule unless it duplicates an existing one of any type * * To be able to directly pass in the result of one of the rule creation * methods null is allowed which will not insert a rule. * * @param RuleSet::TYPE_* $type A TYPE_* constant defining the rule type * @param Rule $newRule The rule about to be added * * @return void */ private function addRule($type, Rule $newRule = null) { if (!$newRule) { return; } $this->rules->add($newRule, $type); } /** * @return void */ protected function addRulesForPackage(BasePackage $package, PlatformRequirementFilterInterface $platformRequirementFilter) { /** @var \SplQueue */ $workQueue = new \SplQueue; $workQueue->enqueue($package); while (!$workQueue->isEmpty()) { $package = $workQueue->dequeue(); if (isset($this->addedMap[$package->id])) { continue; } $this->addedMap[$package->id] = $package; if (!$package instanceof AliasPackage) { foreach ($package->getNames(false) as $name) { $this->addedPackagesByNames[$name][] = $package; } } else { $workQueue->enqueue($package->getAliasOf()); $this->addRule(RuleSet::TYPE_PACKAGE, $this->createRequireRule($package, array($package->getAliasOf()), Rule::RULE_PACKAGE_ALIAS, $package)); // aliases must be installed with their main package, so create a rule the other way around as well $this->addRule(RuleSet::TYPE_PACKAGE, $this->createRequireRule($package->getAliasOf(), array($package), Rule::RULE_PACKAGE_INVERSE_ALIAS, $package->getAliasOf())); // if alias package has no self.version requires, its requirements do not // need to be added as the aliased package processing will take care of it if (!$package->hasSelfVersionRequires()) { continue; } } foreach ($package->getRequires() as $link) { $constraint = $link->getConstraint(); if ($platformRequirementFilter->isIgnored($link->getTarget())) { continue; } elseif ($platformRequirementFilter instanceof IgnoreListPlatformRequirementFilter) { $constraint = $platformRequirementFilter->filterConstraint($link->getTarget(), $constraint); } $possibleRequires = $this->pool->whatProvides($link->getTarget(), $constraint); $this->addRule(RuleSet::TYPE_PACKAGE, $this->createRequireRule($package, $possibleRequires, Rule::RULE_PACKAGE_REQUIRES, $link)); foreach ($possibleRequires as $require) { $workQueue->enqueue($require); } } } } /** * @return void */ protected function addConflictRules(PlatformRequirementFilterInterface $platformRequirementFilter) { /** @var BasePackage $package */ foreach ($this->addedMap as $package) { foreach ($package->getConflicts() as $link) { // even if conlict ends up being with an alias, there would be at least one actual package by this name if (!isset($this->addedPackagesByNames[$link->getTarget()])) { continue; } $constraint = $link->getConstraint(); if ($platformRequirementFilter->isIgnored($link->getTarget())) { continue; } elseif ($platformRequirementFilter instanceof IgnoreListPlatformRequirementFilter) { $constraint = $platformRequirementFilter->filterConstraint($link->getTarget(), $constraint); } $conflicts = $this->pool->whatProvides($link->getTarget(), $constraint); foreach ($conflicts as $conflict) { // define the conflict rule for regular packages, for alias packages it's only needed if the name // matches the conflict exactly, otherwise the name match is by provide/replace which means the // package which this is an alias of will conflict anyway, so no need to create additional rules if (!$conflict instanceof AliasPackage || $conflict->getName() === $link->getTarget()) { $this->addRule(RuleSet::TYPE_PACKAGE, $this->createRule2Literals($package, $conflict, Rule::RULE_PACKAGE_CONFLICT, $link)); } } } } foreach ($this->addedPackagesByNames as $name => $packages) { if (\count($packages) > 1) { $reason = Rule::RULE_PACKAGE_SAME_NAME; $this->addRule(RuleSet::TYPE_PACKAGE, $this->createMultiConflictRule($packages, $reason, $name)); } } } /** * @return void */ protected function addRulesForRequest(Request $request, PlatformRequirementFilterInterface $platformRequirementFilter) { foreach ($request->getFixedPackages() as $package) { if ($package->id == -1) { // fixed package was not added to the pool as it did not pass the stability requirements, this is fine if ($this->pool->isUnacceptableFixedOrLockedPackage($package)) { continue; } // otherwise, looks like a bug throw new \LogicException("Fixed package ".$package->getPrettyString()." was not added to solver pool."); } $this->addRulesForPackage($package, $platformRequirementFilter); $rule = $this->createInstallOneOfRule(array($package), Rule::RULE_FIXED, array( 'package' => $package, )); $this->addRule(RuleSet::TYPE_REQUEST, $rule); } foreach ($request->getRequires() as $packageName => $constraint) { if ($platformRequirementFilter->isIgnored($packageName)) { continue; } elseif ($platformRequirementFilter instanceof IgnoreListPlatformRequirementFilter) { $constraint = $platformRequirementFilter->filterConstraint($packageName, $constraint); } $packages = $this->pool->whatProvides($packageName, $constraint); if ($packages) { foreach ($packages as $package) { $this->addRulesForPackage($package, $platformRequirementFilter); } $rule = $this->createInstallOneOfRule($packages, Rule::RULE_ROOT_REQUIRE, array( 'packageName' => $packageName, 'constraint' => $constraint, )); $this->addRule(RuleSet::TYPE_REQUEST, $rule); } } } /** * @return void */ protected function addRulesForRootAliases(PlatformRequirementFilterInterface $platformRequirementFilter) { foreach ($this->pool->getPackages() as $package) { // ensure that rules for root alias packages and aliases of packages which were loaded are also loaded // even if the alias itself isn't required, otherwise a package could be installed without its alias which // leads to unexpected behavior if (!isset($this->addedMap[$package->id]) && $package instanceof AliasPackage && ($package->isRootPackageAlias() || isset($this->addedMap[$package->getAliasOf()->id])) ) { $this->addRulesForPackage($package, $platformRequirementFilter); } } } /** * @return RuleSet */ public function getRulesFor(Request $request, PlatformRequirementFilterInterface $platformRequirementFilter = null) { $platformRequirementFilter = $platformRequirementFilter ?: PlatformRequirementFilterFactory::ignoreNothing(); $this->addRulesForRequest($request, $platformRequirementFilter); $this->addRulesForRootAliases($platformRequirementFilter); $this->addConflictRules($platformRequirementFilter); // Remove references to packages $this->addedMap = $this->addedPackagesByNames = array(); $rules = $this->rules; $this->rules = new RuleSet; return $rules; } } * Jordi Boggiano * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Composer\DependencyResolver; use Composer\Package\AliasPackage; use Composer\Package\Link; use Composer\Package\PackageInterface; use Composer\Repository\PlatformRepository; use Composer\DependencyResolver\Operation\OperationInterface; /** * @author Nils Adermann * @internal */ class Transaction { /** * @var OperationInterface[] */ protected $operations; /** * Packages present at the beginning of the transaction * @var PackageInterface[] */ protected $presentPackages; /** * Package set resulting from this transaction * @var array */ protected $resultPackageMap; /** * @var array */ protected $resultPackagesByName = array(); /** * @param PackageInterface[] $presentPackages * @param PackageInterface[] $resultPackages */ public function __construct($presentPackages, $resultPackages) { $this->presentPackages = $presentPackages; $this->setResultPackageMaps($resultPackages); $this->operations = $this->calculateOperations(); } /** * @return OperationInterface[] */ public function getOperations() { return $this->operations; } /** * @param PackageInterface[] $resultPackages * @return void */ private function setResultPackageMaps($resultPackages) { $packageSort = function (PackageInterface $a, PackageInterface $b) { // sort alias packages by the same name behind their non alias version if ($a->getName() == $b->getName()) { if ($a instanceof AliasPackage != $b instanceof AliasPackage) { return $a instanceof AliasPackage ? -1 : 1; } // if names are the same, compare version, e.g. to sort aliases reliably, actual order does not matter return strcmp($b->getVersion(), $a->getVersion()); } return strcmp($b->getName(), $a->getName()); }; $this->resultPackageMap = array(); foreach ($resultPackages as $package) { $this->resultPackageMap[spl_object_hash($package)] = $package; foreach ($package->getNames() as $name) { $this->resultPackagesByName[$name][] = $package; } } uasort($this->resultPackageMap, $packageSort); foreach ($this->resultPackagesByName as $name => $packages) { uasort($this->resultPackagesByName[$name], $packageSort); } } /** * @return OperationInterface[] */ protected function calculateOperations() { $operations = array(); $presentPackageMap = array(); $removeMap = array(); $presentAliasMap = array(); $removeAliasMap = array(); foreach ($this->presentPackages as $package) { if ($package instanceof AliasPackage) { $presentAliasMap[$package->getName().'::'.$package->getVersion()] = $package; $removeAliasMap[$package->getName().'::'.$package->getVersion()] = $package; } else { $presentPackageMap[$package->getName()] = $package; $removeMap[$package->getName()] = $package; } } $stack = $this->getRootPackages(); $visited = array(); $processed = array(); while (!empty($stack)) { $package = array_pop($stack); if (isset($processed[spl_object_hash($package)])) { continue; } if (!isset($visited[spl_object_hash($package)])) { $visited[spl_object_hash($package)] = true; $stack[] = $package; if ($package instanceof AliasPackage) { $stack[] = $package->getAliasOf(); } else { foreach ($package->getRequires() as $link) { $possibleRequires = $this->getProvidersInResult($link); foreach ($possibleRequires as $require) { $stack[] = $require; } } } } elseif (!isset($processed[spl_object_hash($package)])) { $processed[spl_object_hash($package)] = true; if ($package instanceof AliasPackage) { $aliasKey = $package->getName().'::'.$package->getVersion(); if (isset($presentAliasMap[$aliasKey])) { unset($removeAliasMap[$aliasKey]); } else { $operations[] = new Operation\MarkAliasInstalledOperation($package); } } else { if (isset($presentPackageMap[$package->getName()])) { $source = $presentPackageMap[$package->getName()]; // do we need to update? // TODO different for lock? if ($package->getVersion() != $presentPackageMap[$package->getName()]->getVersion() || $package->getDistReference() !== $presentPackageMap[$package->getName()]->getDistReference() || $package->getSourceReference() !== $presentPackageMap[$package->getName()]->getSourceReference() ) { $operations[] = new Operation\UpdateOperation($source, $package); } unset($removeMap[$package->getName()]); } else { $operations[] = new Operation\InstallOperation($package); unset($removeMap[$package->getName()]); } } } } foreach ($removeMap as $name => $package) { array_unshift($operations, new Operation\UninstallOperation($package)); } foreach ($removeAliasMap as $nameVersion => $package) { $operations[] = new Operation\MarkAliasUninstalledOperation($package); } $operations = $this->movePluginsToFront($operations); // TODO fix this: // we have to do this again here even though the above stack code did it because moving plugins moves them before uninstalls $operations = $this->moveUninstallsToFront($operations); // TODO skip updates which don't update? is this needed? we shouldn't schedule this update in the first place? /* if ('update' === $opType) { $targetPackage = $operation->getTargetPackage(); if ($targetPackage->isDev()) { $initialPackage = $operation->getInitialPackage(); if ($targetPackage->getVersion() === $initialPackage->getVersion() && (!$targetPackage->getSourceReference() || $targetPackage->getSourceReference() === $initialPackage->getSourceReference()) && (!$targetPackage->getDistReference() || $targetPackage->getDistReference() === $initialPackage->getDistReference()) ) { $this->io->writeError(' - Skipping update of ' . $targetPackage->getPrettyName() . ' to the same reference-locked version', true, IOInterface::DEBUG); $this->io->writeError('', true, IOInterface::DEBUG); continue; } } }*/ return $this->operations = $operations; } /** * Determine which packages in the result are not required by any other packages in it. * * These serve as a starting point to enumerate packages in a topological order despite potential cycles. * If there are packages with a cycle on the top level the package with the lowest name gets picked * * @return array */ protected function getRootPackages() { $roots = $this->resultPackageMap; foreach ($this->resultPackageMap as $packageHash => $package) { if (!isset($roots[$packageHash])) { continue; } foreach ($package->getRequires() as $link) { $possibleRequires = $this->getProvidersInResult($link); foreach ($possibleRequires as $require) { if ($require !== $package) { unset($roots[spl_object_hash($require)]); } } } } return $roots; } /** * @return PackageInterface[] */ protected function getProvidersInResult(Link $link) { if (!isset($this->resultPackagesByName[$link->getTarget()])) { return array(); } return $this->resultPackagesByName[$link->getTarget()]; } /** * Workaround: if your packages depend on plugins, we must be sure * that those are installed / updated first; else it would lead to packages * being installed multiple times in different folders, when running Composer * twice. * * While this does not fix the root-causes of https://github.com/composer/composer/issues/1147, * it at least fixes the symptoms and makes usage of composer possible (again) * in such scenarios. * * @param OperationInterface[] $operations * @return OperationInterface[] reordered operation list */ private function movePluginsToFront(array $operations) { $dlModifyingPluginsNoDeps = array(); $dlModifyingPluginsWithDeps = array(); $dlModifyingPluginRequires = array(); $pluginsNoDeps = array(); $pluginsWithDeps = array(); $pluginRequires = array(); foreach (array_reverse($operations, true) as $idx => $op) { if ($op instanceof Operation\InstallOperation) { $package = $op->getPackage(); } elseif ($op instanceof Operation\UpdateOperation) { $package = $op->getTargetPackage(); } else { continue; } $isDownloadsModifyingPlugin = $package->getType() === 'composer-plugin' && ($extra = $package->getExtra()) && isset($extra['plugin-modifies-downloads']) && $extra['plugin-modifies-downloads'] === true; // is this a downloads modifying plugin or a dependency of one? if ($isDownloadsModifyingPlugin || count(array_intersect($package->getNames(), $dlModifyingPluginRequires))) { // get the package's requires, but filter out any platform requirements $requires = array_filter(array_keys($package->getRequires()), function ($req) { return !PlatformRepository::isPlatformPackage($req); }); // is this a plugin with no meaningful dependencies? if ($isDownloadsModifyingPlugin && !count($requires)) { // plugins with no dependencies go to the very front array_unshift($dlModifyingPluginsNoDeps, $op); } else { // capture the requirements for this package so those packages will be moved up as well $dlModifyingPluginRequires = array_merge($dlModifyingPluginRequires, $requires); // move the operation to the front array_unshift($dlModifyingPluginsWithDeps, $op); } unset($operations[$idx]); continue; } // is this package a plugin? $isPlugin = $package->getType() === 'composer-plugin' || $package->getType() === 'composer-installer'; // is this a plugin or a dependency of a plugin? if ($isPlugin || count(array_intersect($package->getNames(), $pluginRequires))) { // get the package's requires, but filter out any platform requirements $requires = array_filter(array_keys($package->getRequires()), function ($req) { return !PlatformRepository::isPlatformPackage($req); }); // is this a plugin with no meaningful dependencies? if ($isPlugin && !count($requires)) { // plugins with no dependencies go to the very front array_unshift($pluginsNoDeps, $op); } else { // capture the requirements for this package so those packages will be moved up as well $pluginRequires = array_merge($pluginRequires, $requires); // move the operation to the front array_unshift($pluginsWithDeps, $op); } unset($operations[$idx]); } } return array_merge($dlModifyingPluginsNoDeps, $dlModifyingPluginsWithDeps, $pluginsNoDeps, $pluginsWithDeps, $operations); } /** * Removals of packages should be executed before installations in * case two packages resolve to the same path (due to custom installers) * * @param OperationInterface[] $operations * @return OperationInterface[] reordered operation list */ private function moveUninstallsToFront(array $operations) { $uninstOps = array(); foreach ($operations as $idx => $op) { if ($op instanceof Operation\UninstallOperation || $op instanceof Operation\MarkAliasUninstalledOperation) { $uninstOps[] = $op; unset($operations[$idx]); } } return array_merge($uninstOps, $operations); } } * Jordi Boggiano * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Composer\DependencyResolver; use Composer\Package\BasePackage; use Composer\Package\PackageInterface; use Composer\Repository\LockArrayRepository; use Composer\Semver\Constraint\ConstraintInterface; use Composer\Semver\Constraint\MatchAllConstraint; /** * @author Nils Adermann */ class Request { /** * Identifies a partial update for listed packages only, all dependencies will remain at locked versions */ const UPDATE_ONLY_LISTED = 0; /** * Identifies a partial update for listed packages and recursively all their dependencies, however dependencies * also directly required by the root composer.json and their dependencies will remain at the locked version. */ const UPDATE_LISTED_WITH_TRANSITIVE_DEPS_NO_ROOT_REQUIRE = 1; /** * Identifies a partial update for listed packages and recursively all their dependencies, even dependencies * also directly required by the root composer.json will be updated. */ const UPDATE_LISTED_WITH_TRANSITIVE_DEPS = 2; /** @var ?LockArrayRepository */ protected $lockedRepository; /** @var array */ protected $requires = array(); /** @var array */ protected $fixedPackages = array(); /** @var array */ protected $lockedPackages = array(); /** @var array */ protected $fixedLockedPackages = array(); /** @var string[] */ protected $updateAllowList = array(); /** @var false|self::UPDATE_* */ protected $updateAllowTransitiveDependencies = false; public function __construct(LockArrayRepository $lockedRepository = null) { $this->lockedRepository = $lockedRepository; } /** * @param string $packageName * @return void */ public function requireName($packageName, ConstraintInterface $constraint = null) { $packageName = strtolower($packageName); if ($constraint === null) { $constraint = new MatchAllConstraint(); } if (isset($this->requires[$packageName])) { throw new \LogicException('Overwriting requires seems like a bug ('.$packageName.' '.$this->requires[$packageName]->getPrettyString().' => '.$constraint->getPrettyString().', check why it is happening, might be a root alias'); } $this->requires[$packageName] = $constraint; } /** * Mark a package as currently present and having to remain installed * * This is used for platform packages which cannot be modified by Composer. A rule enforcing their installation is * generated for dependency resolution. Partial updates with dependencies cannot in any way modify these packages. * * @return void */ public function fixPackage(BasePackage $package) { $this->fixedPackages[spl_object_hash($package)] = $package; } /** * Mark a package as locked to a specific version but removable * * This is used for lock file packages which need to be treated similar to fixed packages by the pool builder in * that by default they should really only have the currently present version loaded and no remote alternatives. * * However unlike fixed packages there will not be a special rule enforcing their installation for the solver, so * if nothing requires these packages they will be removed. Additionally in a partial update these packages can be * unlocked, meaning other versions can be installed if explicitly requested as part of the update. * * @return void */ public function lockPackage(BasePackage $package) { $this->lockedPackages[spl_object_hash($package)] = $package; } /** * Marks a locked package fixed. So it's treated irremovable like a platform package. * * This is necessary for the composer install step which verifies the lock file integrity and should not allow * removal of any packages. At the same time lock packages there cannot simply be marked fixed, as error reporting * would then report them as platform packages, so this still marks them as locked packages at the same time. * * @return void */ public function fixLockedPackage(BasePackage $package) { $this->fixedPackages[spl_object_hash($package)] = $package; $this->fixedLockedPackages[spl_object_hash($package)] = $package; } /** * @return void */ public function unlockPackage(BasePackage $package) { unset($this->lockedPackages[spl_object_hash($package)]); } /** * @param string[] $updateAllowList * @param false|self::UPDATE_* $updateAllowTransitiveDependencies * @return void */ public function setUpdateAllowList($updateAllowList, $updateAllowTransitiveDependencies) { $this->updateAllowList = $updateAllowList; $this->updateAllowTransitiveDependencies = $updateAllowTransitiveDependencies; } /** * @return string[] */ public function getUpdateAllowList() { return $this->updateAllowList; } /** * @return bool */ public function getUpdateAllowTransitiveDependencies() { return $this->updateAllowTransitiveDependencies !== self::UPDATE_ONLY_LISTED; } /** * @return bool */ public function getUpdateAllowTransitiveRootDependencies() { return $this->updateAllowTransitiveDependencies === self::UPDATE_LISTED_WITH_TRANSITIVE_DEPS; } /** * @return array */ public function getRequires() { return $this->requires; } /** * @return array */ public function getFixedPackages() { return $this->fixedPackages; } /** * @return bool */ public function isFixedPackage(BasePackage $package) { return isset($this->fixedPackages[spl_object_hash($package)]); } /** * @return array */ public function getLockedPackages() { return $this->lockedPackages; } /** * @return bool */ public function isLockedPackage(PackageInterface $package) { return isset($this->lockedPackages[spl_object_hash($package)]) || isset($this->fixedLockedPackages[spl_object_hash($package)]); } /** * @return array */ public function getFixedOrLockedPackages() { return array_merge($this->fixedPackages, $this->lockedPackages); } /** * @param bool $packageIds * @return array * * @TODO look into removing the packageIds option, the only place true is used * is for the installed map in the solver problems. * Some locked packages may not be in the pool, * so they have a package->id of -1 */ public function getPresentMap($packageIds = false) { $presentMap = array(); if ($this->lockedRepository) { foreach ($this->lockedRepository->getPackages() as $package) { $presentMap[$packageIds ? $package->getId() : spl_object_hash($package)] = $package; } } foreach ($this->fixedPackages as $package) { $presentMap[$packageIds ? $package->getId() : spl_object_hash($package)] = $package; } return $presentMap; } /** * @return BasePackage[] */ public function getFixedPackagesMap() { $fixedPackagesMap = array(); foreach ($this->fixedPackages as $package) { $fixedPackagesMap[$package->getId()] = $package; } return $fixedPackagesMap; } /** * @return ?LockArrayRepository */ public function getLockedRepository() { return $this->lockedRepository; } } * Jordi Boggiano * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Composer\DependencyResolver; use Composer\Package\AliasPackage; use Composer\Package\BasePackage; use Composer\Package\PackageInterface; use Composer\Semver\Constraint\Constraint; /** * @author Nils Adermann * @author Jordi Boggiano */ class DefaultPolicy implements PolicyInterface { /** @var bool */ private $preferStable; /** @var bool */ private $preferLowest; /** * @param bool $preferStable * @param bool $preferLowest */ public function __construct($preferStable = false, $preferLowest = false) { $this->preferStable = $preferStable; $this->preferLowest = $preferLowest; } /** * @param string $operator One of Constraint::STR_OP_* * @return bool * * @phpstan-param Constraint::STR_OP_* $operator */ public function versionCompare(PackageInterface $a, PackageInterface $b, $operator) { if ($this->preferStable && ($stabA = $a->getStability()) !== ($stabB = $b->getStability())) { return BasePackage::$stabilities[$stabA] < BasePackage::$stabilities[$stabB]; } $constraint = new Constraint($operator, $b->getVersion()); $version = new Constraint('==', $a->getVersion()); return $constraint->matchSpecific($version, true); } /** * @param int[] $literals * @param string $requiredPackage * @return int[] */ public function selectPreferredPackages(Pool $pool, array $literals, $requiredPackage = null) { $packages = $this->groupLiteralsByName($pool, $literals); $policy = $this; foreach ($packages as &$nameLiterals) { usort($nameLiterals, function ($a, $b) use ($policy, $pool, $requiredPackage) { return $policy->compareByPriority($pool, $pool->literalToPackage($a), $pool->literalToPackage($b), $requiredPackage, true); }); } foreach ($packages as &$sortedLiterals) { $sortedLiterals = $this->pruneToBestVersion($pool, $sortedLiterals); $sortedLiterals = $this->pruneRemoteAliases($pool, $sortedLiterals); } $selected = \call_user_func_array('array_merge', array_values($packages)); // now sort the result across all packages to respect replaces across packages usort($selected, function ($a, $b) use ($policy, $pool, $requiredPackage) { return $policy->compareByPriority($pool, $pool->literalToPackage($a), $pool->literalToPackage($b), $requiredPackage); }); return $selected; } /** * @param int[] $literals * @return array */ protected function groupLiteralsByName(Pool $pool, $literals) { $packages = array(); foreach ($literals as $literal) { $packageName = $pool->literalToPackage($literal)->getName(); if (!isset($packages[$packageName])) { $packages[$packageName] = array(); } $packages[$packageName][] = $literal; } return $packages; } /** * @protected * @param ?string $requiredPackage * @param bool $ignoreReplace * @return int */ public function compareByPriority(Pool $pool, BasePackage $a, BasePackage $b, $requiredPackage = null, $ignoreReplace = false) { // prefer aliases to the original package if ($a->getName() === $b->getName()) { $aAliased = $a instanceof AliasPackage; $bAliased = $b instanceof AliasPackage; if ($aAliased && !$bAliased) { return -1; // use a } if (!$aAliased && $bAliased) { return 1; // use b } } if (!$ignoreReplace) { // return original, not replaced if ($this->replaces($a, $b)) { return 1; // use b } if ($this->replaces($b, $a)) { return -1; // use a } // for replacers not replacing each other, put a higher prio on replacing // packages with the same vendor as the required package if ($requiredPackage && false !== ($pos = strpos($requiredPackage, '/'))) { $requiredVendor = substr($requiredPackage, 0, $pos); $aIsSameVendor = strpos($a->getName(), $requiredVendor) === 0; $bIsSameVendor = strpos($b->getName(), $requiredVendor) === 0; if ($bIsSameVendor !== $aIsSameVendor) { return $aIsSameVendor ? -1 : 1; } } } // priority equal, sort by package id to make reproducible if ($a->id === $b->id) { return 0; } return ($a->id < $b->id) ? -1 : 1; } /** * Checks if source replaces a package with the same name as target. * * Replace constraints are ignored. This method should only be used for * prioritisation, not for actual constraint verification. * * @return bool */ protected function replaces(BasePackage $source, BasePackage $target) { foreach ($source->getReplaces() as $link) { if ($link->getTarget() === $target->getName() // && (null === $link->getConstraint() || // $link->getConstraint()->matches(new Constraint('==', $target->getVersion())))) { ) { return true; } } return false; } /** * @param int[] $literals * @return int[] */ protected function pruneToBestVersion(Pool $pool, $literals) { $operator = $this->preferLowest ? '<' : '>'; $bestLiterals = array($literals[0]); $bestPackage = $pool->literalToPackage($literals[0]); foreach ($literals as $i => $literal) { if (0 === $i) { continue; } $package = $pool->literalToPackage($literal); if ($this->versionCompare($package, $bestPackage, $operator)) { $bestPackage = $package; $bestLiterals = array($literal); } elseif ($this->versionCompare($package, $bestPackage, '==')) { $bestLiterals[] = $literal; } } return $bestLiterals; } /** * Assumes that locally aliased (in root package requires) packages take priority over branch-alias ones * * If no package is a local alias, nothing happens * * @param int[] $literals * @return int[] */ protected function pruneRemoteAliases(Pool $pool, array $literals) { $hasLocalAlias = false; foreach ($literals as $literal) { $package = $pool->literalToPackage($literal); if ($package instanceof AliasPackage && $package->isRootPackageAlias()) { $hasLocalAlias = true; break; } } if (!$hasLocalAlias) { return $literals; } $selected = array(); foreach ($literals as $literal) { $package = $pool->literalToPackage($literal); if ($package instanceof AliasPackage && $package->isRootPackageAlias()) { $selected[] = $literal; } } return $selected; } } * Jordi Boggiano * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Composer\Console; use Composer\Pcre\Preg; use Symfony\Component\Console\Formatter\OutputFormatter; use Symfony\Component\Console\Formatter\OutputFormatterStyle; /** * @author Jordi Boggiano */ class HtmlOutputFormatter extends OutputFormatter { /** @var array */ private static $availableForegroundColors = array( 30 => 'black', 31 => 'red', 32 => 'green', 33 => 'yellow', 34 => 'blue', 35 => 'magenta', 36 => 'cyan', 37 => 'white', ); /** @var array */ private static $availableBackgroundColors = array( 40 => 'black', 41 => 'red', 42 => 'green', 43 => 'yellow', 44 => 'blue', 45 => 'magenta', 46 => 'cyan', 47 => 'white', ); /** @var array */ private static $availableOptions = array( 1 => 'bold', 4 => 'underscore', //5 => 'blink', //7 => 'reverse', //8 => 'conceal' ); /** * @param array $styles Array of "name => FormatterStyle" instances */ public function __construct(array $styles = array()) { parent::__construct(true, $styles); } /** * @param ?string $message * * @return string */ public function format($message) { $formatted = parent::format($message); $clearEscapeCodes = '(?:39|49|0|22|24|25|27|28)'; // TODO in 2.3 replace with Closure::fromCallable and then use Preg::replaceCallback return preg_replace_callback("{\033\[([0-9;]+)m(.*?)\033\[(?:".$clearEscapeCodes.";)*?".$clearEscapeCodes."m}s", array($this, 'formatHtml'), $formatted); } /** * @param string[] $matches * * @return string */ private function formatHtml($matches) { $out = ''.$matches[2].''; } } * Jordi Boggiano * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Composer\Console; use Composer\IO\NullIO; use Composer\Util\Filesystem; use Composer\Util\Platform; use Composer\Util\Silencer; use Symfony\Component\Console\Application as BaseApplication; use Symfony\Component\Console\Exception\CommandNotFoundException; use Symfony\Component\Console\Helper\HelperSet; use Symfony\Component\Console\Helper\QuestionHelper; use Symfony\Component\Console\Input\InputDefinition; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; use Seld\JsonLint\ParsingException; use Composer\Command; use Composer\Composer; use Composer\Factory; use Composer\IO\IOInterface; use Composer\IO\ConsoleIO; use Composer\Json\JsonValidationException; use Composer\Util\ErrorHandler; use Composer\Util\HttpDownloader; use Composer\EventDispatcher\ScriptExecutionException; use Composer\Exception\NoSslException; use Composer\XdebugHandler\XdebugHandler; use Symfony\Component\Process\Exception\ProcessTimedOutException; /** * The console application that handles the commands * * @author Ryan Weaver * @author Jordi Boggiano * @author François Pluchino */ class Application extends BaseApplication { /** * @var ?Composer */ protected $composer; /** * @var IOInterface */ protected $io; /** @var string */ private static $logo = ' ______ / ____/___ ____ ___ ____ ____ ________ _____ / / / __ \/ __ `__ \/ __ \/ __ \/ ___/ _ \/ ___/ / /___/ /_/ / / / / / / /_/ / /_/ (__ ) __/ / \____/\____/_/ /_/ /_/ .___/\____/____/\___/_/ /_/ '; /** @var bool */ private $hasPluginCommands = false; /** @var bool */ private $disablePluginsByDefault = false; /** @var bool */ private $disableScriptsByDefault = false; /** * @var string Store the initial working directory at startup time */ private $initialWorkingDirectory; public function __construct() { static $shutdownRegistered = false; if (function_exists('ini_set') && extension_loaded('xdebug')) { ini_set('xdebug.show_exception_trace', '0'); ini_set('xdebug.scream', '0'); } if (function_exists('date_default_timezone_set') && function_exists('date_default_timezone_get')) { date_default_timezone_set(Silencer::call('date_default_timezone_get')); } if (!$shutdownRegistered) { if (function_exists('pcntl_async_signals') && function_exists('pcntl_signal')) { pcntl_async_signals(true); pcntl_signal(SIGINT, function ($sig) { exit(130); }); } $shutdownRegistered = true; register_shutdown_function(function () { $lastError = error_get_last(); if ($lastError && $lastError['message'] && (strpos($lastError['message'], 'Allowed memory') !== false /*Zend PHP out of memory error*/ || strpos($lastError['message'], 'exceeded memory') !== false /*HHVM out of memory errors*/)) { echo "\n". 'Check https://getcomposer.org/doc/articles/troubleshooting.md#memory-limit-errors for more info on how to handle out of memory errors.'; } }); } $this->io = new NullIO(); $this->initialWorkingDirectory = getcwd(); parent::__construct('Composer', Composer::getVersion()); } /** * @return int */ public function run(InputInterface $input = null, OutputInterface $output = null) { if (null === $output) { $output = Factory::createOutput(); } return parent::run($input, $output); } /** * @return int */ public function doRun(InputInterface $input, OutputInterface $output) { $this->disablePluginsByDefault = $input->hasParameterOption('--no-plugins'); $this->disableScriptsByDefault = $input->hasParameterOption('--no-scripts'); if (Platform::getEnv('COMPOSER_NO_INTERACTION') || !Platform::isTty(defined('STDIN') ? STDIN : fopen('php://stdin', 'r'))) { $input->setInteractive(false); } $io = $this->io = new ConsoleIO($input, $output, new HelperSet(array( new QuestionHelper(), ))); // Register error handler again to pass it the IO instance ErrorHandler::register($io); if ($input->hasParameterOption('--no-cache')) { $io->writeError('Disabling cache usage', true, IOInterface::DEBUG); Platform::putEnv('COMPOSER_CACHE_DIR', Platform::isWindows() ? 'nul' : '/dev/null'); } // switch working dir if ($newWorkDir = $this->getNewWorkingDir($input)) { $oldWorkingDir = getcwd(); chdir($newWorkDir); $this->initialWorkingDirectory = $newWorkDir; $io->writeError('Changed CWD to ' . getcwd(), true, IOInterface::DEBUG); } // determine command name to be executed without including plugin commands $commandName = ''; if ($name = $this->getCommandName($input)) { try { $commandName = $this->find($name)->getName(); } catch (CommandNotFoundException $e) { // we'll check command validity again later after plugins are loaded $commandName = false; } catch (\InvalidArgumentException $e) { } } // prompt user for dir change if no composer.json is present in current dir if ($io->isInteractive() && !$newWorkDir && !in_array($commandName, array('', 'list', 'init', 'about', 'help', 'diagnose', 'self-update', 'global', 'create-project', 'outdated'), true) && !file_exists(Factory::getComposerFile()) && ($useParentDirIfNoJsonAvailable = $this->getUseParentDirConfigValue()) !== false) { $dir = dirname(getcwd()); $home = realpath(Platform::getEnv('HOME') ?: Platform::getEnv('USERPROFILE') ?: '/'); // abort when we reach the home dir or top of the filesystem while (dirname($dir) !== $dir && $dir !== $home) { if (file_exists($dir.'/'.Factory::getComposerFile())) { if ($useParentDirIfNoJsonAvailable === true || $io->askConfirmation('No composer.json in current directory, do you want to use the one at '.$dir.'? [Y,n]? ')) { if ($useParentDirIfNoJsonAvailable === true) { $io->writeError('No composer.json in current directory, changing working directory to '.$dir.''); } else { $io->writeError('Always want to use the parent dir? Use "composer config --global use-parent-dir true" to change the default.'); } $oldWorkingDir = getcwd(); chdir($dir); } break; } $dir = dirname($dir); } } // avoid loading plugins/initializing the Composer instance earlier than necessary if no plugin command is needed // if showing the version, we never need plugin commands $mayNeedPluginCommand = false === $input->hasParameterOption(array('--version', '-V')) && ( // not a composer command, so try loading plugin ones false === $commandName // list command requires plugin commands to show them || in_array($commandName, array('', 'list', 'help'), true) ); if ($mayNeedPluginCommand && !$this->disablePluginsByDefault && !$this->hasPluginCommands) { try { foreach ($this->getPluginCommands() as $command) { if ($this->has($command->getName())) { $io->writeError('Plugin command '.$command->getName().' ('.get_class($command).') would override a Composer command and has been skipped'); } else { $this->add($command); } } } catch (NoSslException $e) { // suppress these as they are not relevant at this point } catch (ParsingException $e) { $details = $e->getDetails(); $file = realpath(Factory::getComposerFile()); $line = null; if ($details && isset($details['line'])) { $line = $details['line']; } $ghe = new GithubActionError($this->io); $ghe->emit($e->getMessage(), $file, $line); throw $e; } $this->hasPluginCommands = true; } // determine command name to be executed incl plugin commands, and check if it's a proxy command $isProxyCommand = false; if ($name = $this->getCommandName($input)) { try { $command = $this->find($name); $commandName = $command->getName(); $isProxyCommand = ($command instanceof Command\BaseCommand && $command->isProxyCommand()); } catch (\InvalidArgumentException $e) { } } if (!$isProxyCommand) { $io->writeError(sprintf( 'Running %s (%s) with %s on %s', Composer::getVersion(), Composer::RELEASE_DATE, defined('HHVM_VERSION') ? 'HHVM '.HHVM_VERSION : 'PHP '.PHP_VERSION, function_exists('php_uname') ? php_uname('s') . ' / ' . php_uname('r') : 'Unknown OS' ), true, IOInterface::DEBUG); if (PHP_VERSION_ID < 50302) { $io->writeError('Composer only officially supports PHP 5.3.2 and above, you will most likely encounter problems with your PHP '.PHP_VERSION.', upgrading is strongly recommended.'); } if (XdebugHandler::isXdebugActive() && !Platform::getEnv('COMPOSER_DISABLE_XDEBUG_WARN')) { $io->writeError('Composer is operating slower than normal because you have Xdebug enabled. See https://getcomposer.org/xdebug'); } if (defined('COMPOSER_DEV_WARNING_TIME') && $commandName !== 'self-update' && $commandName !== 'selfupdate' && time() > COMPOSER_DEV_WARNING_TIME) { $io->writeError(sprintf('Warning: This development build of Composer is over 60 days old. It is recommended to update it by running "%s self-update" to get the latest version.', $_SERVER['PHP_SELF'])); } if ( !Platform::isWindows() && function_exists('exec') && !Platform::getEnv('COMPOSER_ALLOW_SUPERUSER') && (ini_get('open_basedir') || !file_exists('/.dockerenv')) ) { if (function_exists('posix_getuid') && posix_getuid() === 0) { if ($commandName !== 'self-update' && $commandName !== 'selfupdate') { $io->writeError('Do not run Composer as root/super user! See https://getcomposer.org/root for details'); if ($io->isInteractive()) { if (!$io->askConfirmation('Continue as root/super user [yes]? ')) { return 1; } } } if ($uid = (int) Platform::getEnv('SUDO_UID')) { // Silently clobber any sudo credentials on the invoking user to avoid privilege escalations later on // ref. https://github.com/composer/composer/issues/5119 Silencer::call('exec', "sudo -u \\#{$uid} sudo -K > /dev/null 2>&1"); } } // Silently clobber any remaining sudo leases on the current user as well to avoid privilege escalations Silencer::call('exec', 'sudo -K > /dev/null 2>&1'); } // Check system temp folder for usability as it can cause weird runtime issues otherwise Silencer::call(function () use ($io) { $tempfile = sys_get_temp_dir() . '/temp-' . md5(microtime()); if (!(file_put_contents($tempfile, __FILE__) && (file_get_contents($tempfile) == __FILE__) && unlink($tempfile) && !file_exists($tempfile))) { $io->writeError(sprintf('PHP temp directory (%s) does not exist or is not writable to Composer. Set sys_temp_dir in your php.ini', sys_get_temp_dir())); } }); // add non-standard scripts as own commands $file = Factory::getComposerFile(); if (is_file($file) && Filesystem::isReadable($file) && is_array($composer = json_decode(file_get_contents($file), true))) { if (isset($composer['scripts']) && is_array($composer['scripts'])) { foreach ($composer['scripts'] as $script => $dummy) { if (!defined('Composer\Script\ScriptEvents::'.str_replace('-', '_', strtoupper($script)))) { if ($this->has($script)) { $io->writeError('A script named '.$script.' would override a Composer command and has been skipped'); } else { $description = null; if (isset($composer['scripts-descriptions'][$script])) { $description = $composer['scripts-descriptions'][$script]; } $this->add(new Command\ScriptAliasCommand($script, $description)); } } } } } } try { if ($input->hasParameterOption('--profile')) { $startTime = microtime(true); $this->io->enableDebugging($startTime); } $result = parent::doRun($input, $output); // chdir back to $oldWorkingDir if set if (isset($oldWorkingDir)) { Silencer::call('chdir', $oldWorkingDir); } if (isset($startTime)) { $io->writeError('Memory usage: '.round(memory_get_usage() / 1024 / 1024, 2).'MiB (peak: '.round(memory_get_peak_usage() / 1024 / 1024, 2).'MiB), time: '.round(microtime(true) - $startTime, 2).'s'); } restore_error_handler(); return $result; } catch (ScriptExecutionException $e) { return $e->getCode(); } catch (\Exception $e) { $ghe = new GithubActionError($this->io); $ghe->emit($e->getMessage()); $this->hintCommonErrors($e); restore_error_handler(); throw $e; } } /** * @param InputInterface $input * @throws \RuntimeException * @return string */ private function getNewWorkingDir(InputInterface $input) { $workingDir = $input->getParameterOption(array('--working-dir', '-d')); if (false !== $workingDir && !is_dir($workingDir)) { throw new \RuntimeException('Invalid working directory specified, '.$workingDir.' does not exist.'); } return $workingDir; } /** * @return void */ private function hintCommonErrors(\Exception $exception) { $io = $this->getIO(); Silencer::suppress(); try { $composer = $this->getComposer(false, true); if ($composer) { $config = $composer->getConfig(); $minSpaceFree = 1024 * 1024; if ((($df = disk_free_space($dir = $config->get('home'))) !== false && $df < $minSpaceFree) || (($df = disk_free_space($dir = $config->get('vendor-dir'))) !== false && $df < $minSpaceFree) || (($df = disk_free_space($dir = sys_get_temp_dir())) !== false && $df < $minSpaceFree) ) { $io->writeError('The disk hosting '.$dir.' is full, this may be the cause of the following exception', true, IOInterface::QUIET); } } } catch (\Exception $e) { } Silencer::restore(); if (Platform::isWindows() && false !== strpos($exception->getMessage(), 'The system cannot find the path specified')) { $io->writeError('The following exception may be caused by a stale entry in your cmd.exe AutoRun', true, IOInterface::QUIET); $io->writeError('Check https://getcomposer.org/doc/articles/troubleshooting.md#-the-system-cannot-find-the-path-specified-windows- for details', true, IOInterface::QUIET); } if (false !== strpos($exception->getMessage(), 'fork failed - Cannot allocate memory')) { $io->writeError('The following exception is caused by a lack of memory or swap, or not having swap configured', true, IOInterface::QUIET); $io->writeError('Check https://getcomposer.org/doc/articles/troubleshooting.md#proc-open-fork-failed-errors for details', true, IOInterface::QUIET); } if ($exception instanceof ProcessTimedOutException) { $io->writeError('The following exception is caused by a process timeout', true, IOInterface::QUIET); $io->writeError('Check https://getcomposer.org/doc/06-config.md#process-timeout for details', true, IOInterface::QUIET); } if ($hints = HttpDownloader::getExceptionHints($exception)) { foreach ($hints as $hint) { $io->writeError($hint, true, IOInterface::QUIET); } } } /** * @param bool $required * @param bool|null $disablePlugins * @param bool|null $disableScripts * @throws JsonValidationException * @throws \InvalidArgumentException * @return ?\Composer\Composer If $required is true then the return value is guaranteed */ public function getComposer($required = true, $disablePlugins = null, $disableScripts = null) { if (null === $disablePlugins) { $disablePlugins = $this->disablePluginsByDefault; } if (null === $disableScripts) { $disableScripts = $this->disableScriptsByDefault; } if (null === $this->composer) { try { $this->composer = Factory::create($this->io, null, $disablePlugins, $disableScripts); } catch (\InvalidArgumentException $e) { if ($required) { $this->io->writeError($e->getMessage()); // TODO composer 2.3 simplify to $this->areExceptionsCaught() if (!method_exists($this, 'areExceptionsCaught') || $this->areExceptionsCaught()) { exit(1); } throw $e; } } catch (JsonValidationException $e) { if ($required) { throw $e; } } } return $this->composer; } /** * Removes the cached composer instance * * @return void */ public function resetComposer() { $this->composer = null; if (method_exists($this->getIO(), 'resetAuthentications')) { $this->getIO()->resetAuthentications(); } } /** * @return IOInterface */ public function getIO() { return $this->io; } /** * @return string */ public function getHelp() { return self::$logo . parent::getHelp(); } /** * Initializes all the composer commands. * @return \Symfony\Component\Console\Command\Command[] */ protected function getDefaultCommands() { $commands = array_merge(parent::getDefaultCommands(), array( new Command\AboutCommand(), new Command\ConfigCommand(), new Command\DependsCommand(), new Command\ProhibitsCommand(), new Command\InitCommand(), new Command\InstallCommand(), new Command\CreateProjectCommand(), new Command\UpdateCommand(), new Command\SearchCommand(), new Command\ValidateCommand(), new Command\ShowCommand(), new Command\SuggestsCommand(), new Command\RequireCommand(), new Command\DumpAutoloadCommand(), new Command\StatusCommand(), new Command\ArchiveCommand(), new Command\DiagnoseCommand(), new Command\RunScriptCommand(), new Command\LicensesCommand(), new Command\GlobalCommand(), new Command\ClearCacheCommand(), new Command\RemoveCommand(), new Command\HomeCommand(), new Command\ExecCommand(), new Command\OutdatedCommand(), new Command\CheckPlatformReqsCommand(), new Command\FundCommand(), new Command\ReinstallCommand(), )); if (strpos(__FILE__, 'phar:') === 0) { $commands[] = new Command\SelfUpdateCommand(); } return $commands; } /** * @return string */ public function getLongVersion() { if (Composer::BRANCH_ALIAS_VERSION && Composer::BRANCH_ALIAS_VERSION !== '@package_branch_alias_version'.'@') { return sprintf( '%s version %s (%s) %s', $this->getName(), Composer::BRANCH_ALIAS_VERSION, $this->getVersion(), Composer::RELEASE_DATE ); } return parent::getLongVersion() . ' ' . Composer::RELEASE_DATE; } /** * @return InputDefinition */ protected function getDefaultInputDefinition() { $definition = parent::getDefaultInputDefinition(); $definition->addOption(new InputOption('--profile', null, InputOption::VALUE_NONE, 'Display timing and memory usage information')); $definition->addOption(new InputOption('--no-plugins', null, InputOption::VALUE_NONE, 'Whether to disable plugins.')); $definition->addOption(new InputOption('--no-scripts', null, InputOption::VALUE_NONE, 'Skips the execution of all scripts defined in composer.json file.')); $definition->addOption(new InputOption('--working-dir', '-d', InputOption::VALUE_REQUIRED, 'If specified, use the given directory as working directory.')); $definition->addOption(new InputOption('--no-cache', null, InputOption::VALUE_NONE, 'Prevent use of the cache')); return $definition; } /** * @return Command\BaseCommand[] */ private function getPluginCommands() { $commands = array(); $composer = $this->getComposer(false, false); if (null === $composer) { $composer = Factory::createGlobal($this->io); } if (null !== $composer) { $pm = $composer->getPluginManager(); foreach ($pm->getPluginCapabilities('Composer\Plugin\Capability\CommandProvider', array('composer' => $composer, 'io' => $this->io)) as $capability) { $newCommands = $capability->getCommands(); if (!is_array($newCommands)) { throw new \UnexpectedValueException('Plugin capability '.get_class($capability).' failed to return an array from getCommands'); } foreach ($newCommands as $command) { if (!$command instanceof Command\BaseCommand) { throw new \UnexpectedValueException('Plugin capability '.get_class($capability).' returned an invalid value, we expected an array of Composer\Command\BaseCommand objects'); } } $commands = array_merge($commands, $newCommands); } } return $commands; } /** * Get the working directory at startup time * * @return string */ public function getInitialWorkingDirectory() { return $this->initialWorkingDirectory; } /** * @return 'prompt'|bool */ private function getUseParentDirConfigValue() { $config = Factory::createConfig($this->io); return $config->get('use-parent-dir'); } } * Jordi Boggiano * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Composer\Console; use Composer\IO\IOInterface; use Composer\Util\Platform; final class GithubActionError { /** * @var IOInterface */ protected $io; public function __construct(IOInterface $io) { $this->io = $io; } /** * @param string $message * @param null|string $file * @param null|int $line * * @return void */ public function emit($message, $file = null, $line = null) { if (Platform::getEnv('GITHUB_ACTIONS') && !Platform::getEnv('COMPOSER_TESTS_ARE_RUNNING')) { $message = $this->escapeData($message); if ($file && $line) { $file = $this->escapeProperty($file); $this->io->write("::error file=". $file .",line=". $line ."::". $message); } elseif ($file) { $file = $this->escapeProperty($file); $this->io->write("::error file=". $file ."::". $message); } else { $this->io->write("::error ::". $message); } } } /** * @param string $data * @return string */ private function escapeData($data) { // see https://github.com/actions/toolkit/blob/4f7fb6513a355689f69f0849edeb369a4dc81729/packages/core/src/command.ts#L80-L85 $data = str_replace("%", '%25', $data); $data = str_replace("\r", '%0D', $data); $data = str_replace("\n", '%0A', $data); return $data; } /** * @param string $property * @return string */ private function escapeProperty($property) { // see https://github.com/actions/toolkit/blob/4f7fb6513a355689f69f0849edeb369a4dc81729/packages/core/src/command.ts#L87-L94 $property = str_replace("%", '%25', $property); $property = str_replace("\r", '%0D', $property); $property = str_replace("\n", '%0A', $property); $property = str_replace(":", '%3A', $property); $property = str_replace(",", '%2C', $property); return $property; } } * Jordi Boggiano * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Composer\Command; use Composer\Script\Event as ScriptEvent; use Composer\Script\ScriptEvents; use Composer\Util\ProcessExecutor; use Composer\Util\Platform; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Output\OutputInterface; /** * @author Fabien Potencier */ class RunScriptCommand extends BaseCommand { /** * @var string[] Array with command events */ protected $scriptEvents = array( ScriptEvents::PRE_INSTALL_CMD, ScriptEvents::POST_INSTALL_CMD, ScriptEvents::PRE_UPDATE_CMD, ScriptEvents::POST_UPDATE_CMD, ScriptEvents::PRE_STATUS_CMD, ScriptEvents::POST_STATUS_CMD, ScriptEvents::POST_ROOT_PACKAGE_INSTALL, ScriptEvents::POST_CREATE_PROJECT_CMD, ScriptEvents::PRE_ARCHIVE_CMD, ScriptEvents::POST_ARCHIVE_CMD, ScriptEvents::PRE_AUTOLOAD_DUMP, ScriptEvents::POST_AUTOLOAD_DUMP, ); /** * @return void */ protected function configure() { $this ->setName('run-script') ->setAliases(array('run')) ->setDescription('Runs the scripts defined in composer.json.') ->setDefinition(array( new InputArgument('script', InputArgument::OPTIONAL, 'Script name to run.'), new InputArgument('args', InputArgument::IS_ARRAY | InputArgument::OPTIONAL, ''), new InputOption('timeout', null, InputOption::VALUE_REQUIRED, 'Sets script timeout in seconds, or 0 for never.'), new InputOption('dev', null, InputOption::VALUE_NONE, 'Sets the dev mode.'), new InputOption('no-dev', null, InputOption::VALUE_NONE, 'Disables the dev mode.'), new InputOption('list', 'l', InputOption::VALUE_NONE, 'List scripts.'), )) ->setHelp( <<run-script command runs scripts defined in composer.json: php composer.phar run-script post-update-cmd Read more at https://getcomposer.org/doc/03-cli.md#run-script EOT ) ; } protected function execute(InputInterface $input, OutputInterface $output) { if ($input->getOption('list')) { return $this->listScripts($output); } if (!$input->getArgument('script')) { throw new \RuntimeException('Missing required argument "script"'); } $script = $input->getArgument('script'); if (!in_array($script, $this->scriptEvents)) { if (defined('Composer\Script\ScriptEvents::'.str_replace('-', '_', strtoupper($script)))) { throw new \InvalidArgumentException(sprintf('Script "%s" cannot be run with this command', $script)); } } $composer = $this->getComposer(); $devMode = $input->getOption('dev') || !$input->getOption('no-dev'); $event = new ScriptEvent($script, $composer, $this->getIO(), $devMode); $hasListeners = $composer->getEventDispatcher()->hasEventListeners($event); if (!$hasListeners) { throw new \InvalidArgumentException(sprintf('Script "%s" is not defined in this package', $script)); } $args = $input->getArgument('args'); if (null !== $timeout = $input->getOption('timeout')) { if (!ctype_digit($timeout)) { throw new \RuntimeException('Timeout value must be numeric and positive if defined, or 0 for forever'); } // Override global timeout set before in Composer by environment or config ProcessExecutor::setTimeout((int) $timeout); } Platform::putEnv('COMPOSER_DEV_MODE', $devMode ? '1' : '0'); return $composer->getEventDispatcher()->dispatchScript($script, $devMode, $args); } /** * @return int */ protected function listScripts(OutputInterface $output) { $scripts = $this->getComposer()->getPackage()->getScripts(); if (!count($scripts)) { return 0; } $io = $this->getIO(); $io->writeError('scripts:'); $table = array(); foreach ($scripts as $name => $script) { $description = ''; try { $cmd = $this->getApplication()->find($name); if ($cmd instanceof ScriptAliasCommand) { $description = $cmd->getDescription(); } } catch (\Symfony\Component\Console\Exception\CommandNotFoundException $e) { // ignore scripts that have no command associated, like native Composer script listeners } $table[] = array(' '.$name, $description); } $this->renderTable($table, $output); return 0; } } * Jordi Boggiano * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Composer\Command; use Composer\Factory; use Composer\Filter\PlatformRequirementFilter\IgnoreAllPlatformRequirementFilter; use Composer\Filter\PlatformRequirementFilter\PlatformRequirementFilterFactory; use Composer\Json\JsonFile; use Composer\Json\JsonValidationException; use Composer\Package\BasePackage; use Composer\Package\CompletePackageInterface; use Composer\Package\Package; use Composer\Package\PackageInterface; use Composer\Package\Version\VersionParser; use Composer\Package\Version\VersionSelector; use Composer\Pcre\Preg; use Composer\Repository\CompositeRepository; use Composer\Repository\PlatformRepository; use Composer\Repository\RepositoryFactory; use Composer\Repository\RepositorySet; use Composer\Util\Filesystem; use Composer\Util\ProcessExecutor; use Composer\Semver\Constraint\Constraint; use Composer\Util\Silencer; use Symfony\Component\Console\Input\ArrayInput; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Process\ExecutableFinder; use Symfony\Component\Process\Process; use Symfony\Component\Console\Helper\FormatterHelper; /** * @author Justin Rainbow * @author Jordi Boggiano */ class InitCommand extends BaseCommand { /** @var ?CompositeRepository */ protected $repos; /** @var array */ private $gitConfig; /** @var RepositorySet[] */ private $repositorySets; /** * @inheritDoc * * @return void */ protected function configure() { $this ->setName('init') ->setDescription('Creates a basic composer.json file in current directory.') ->setDefinition(array( new InputOption('name', null, InputOption::VALUE_REQUIRED, 'Name of the package'), new InputOption('description', null, InputOption::VALUE_REQUIRED, 'Description of package'), new InputOption('author', null, InputOption::VALUE_REQUIRED, 'Author name of package'), // new InputOption('version', null, InputOption::VALUE_NONE, 'Version of package'), new InputOption('type', null, InputOption::VALUE_OPTIONAL, 'Type of package (e.g. library, project, metapackage, composer-plugin)'), new InputOption('homepage', null, InputOption::VALUE_REQUIRED, 'Homepage of package'), new InputOption('require', null, InputOption::VALUE_IS_ARRAY | InputOption::VALUE_REQUIRED, 'Package to require with a version constraint, e.g. foo/bar:1.0.0 or foo/bar=1.0.0 or "foo/bar 1.0.0"'), new InputOption('require-dev', null, InputOption::VALUE_IS_ARRAY | InputOption::VALUE_REQUIRED, 'Package to require for development with a version constraint, e.g. foo/bar:1.0.0 or foo/bar=1.0.0 or "foo/bar 1.0.0"'), new InputOption('stability', 's', InputOption::VALUE_REQUIRED, 'Minimum stability (empty or one of: '.implode(', ', array_keys(BasePackage::$stabilities)).')'), new InputOption('license', 'l', InputOption::VALUE_REQUIRED, 'License of package'), new InputOption('repository', null, InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY, 'Add custom repositories, either by URL or using JSON arrays'), new InputOption('autoload', 'a', InputOption::VALUE_REQUIRED, 'Add PSR-4 autoload mapping. Maps your package\'s namespace to the provided directory. (Expects a relative path, e.g. src/)'), )) ->setHelp( <<init command creates a basic composer.json file in the current directory. php composer.phar init Read more at https://getcomposer.org/doc/03-cli.md#init EOT ) ; } /** * @inheritDoc * * @return int * @throws \Seld\JsonLint\ParsingException */ protected function execute(InputInterface $input, OutputInterface $output) { $io = $this->getIO(); $allowlist = array('name', 'description', 'author', 'type', 'homepage', 'require', 'require-dev', 'stability', 'license', 'autoload'); $options = array_filter(array_intersect_key($input->getOptions(), array_flip($allowlist))); if (isset($options['name']) && !Preg::isMatch('{^[a-z0-9_.-]+/[a-z0-9_.-]+$}D', $options['name'])) { throw new \InvalidArgumentException( 'The package name '.$options['name'].' is invalid, it should be lowercase and have a vendor name, a forward slash, and a package name, matching: [a-z0-9_.-]+/[a-z0-9_.-]+' ); } if (isset($options['author'])) { $options['authors'] = $this->formatAuthors($options['author']); unset($options['author']); } $repositories = $input->getOption('repository'); if ($repositories) { $config = Factory::createConfig($io); foreach ($repositories as $repo) { $options['repositories'][] = RepositoryFactory::configFromString($io, $config, $repo, true); } } if (isset($options['stability'])) { $options['minimum-stability'] = $options['stability']; unset($options['stability']); } $options['require'] = isset($options['require']) ? $this->formatRequirements($options['require']) : new \stdClass; if (array() === $options['require']) { $options['require'] = new \stdClass; } if (isset($options['require-dev'])) { $options['require-dev'] = $this->formatRequirements($options['require-dev']); if (array() === $options['require-dev']) { $options['require-dev'] = new \stdClass; } } // --autoload - create autoload object $autoloadPath = null; if (isset($options['autoload'])) { $autoloadPath = $options['autoload']; $namespace = $this->namespaceFromPackageName($input->getOption('name')); $options['autoload'] = (object) array( 'psr-4' => array( $namespace . '\\' => $autoloadPath, ), ); } $file = new JsonFile(Factory::getComposerFile()); $json = JsonFile::encode($options); if ($input->isInteractive()) { $io->writeError(array('', $json, '')); if (!$io->askConfirmation('Do you confirm generation [yes]? ')) { $io->writeError('Command aborted'); return 1; } } else { if (json_encode($options) === '{"require":{}}') { throw new \RuntimeException('You have to run this command in interactive mode, or specify at least some data using --name, --require, etc.'); } $io->writeError('Writing '.$file->getPath()); } $file->write($options); try { $file->validateSchema(JsonFile::LAX_SCHEMA); } catch (JsonValidationException $e) { $io->writeError('Schema validation error, aborting'); $errors = ' - ' . implode(PHP_EOL . ' - ', $e->getErrors()); $io->writeError($e->getMessage() . ':' . PHP_EOL . $errors); Silencer::call('unlink', $file->getPath()); return 1; } // --autoload - Create src folder if ($autoloadPath) { $filesystem = new Filesystem(); $filesystem->ensureDirectoryExists($autoloadPath); // dump-autoload only for projects without added dependencies. if (!$this->hasDependencies($options)) { $this->runDumpAutoloadCommand($output); } } if ($input->isInteractive() && is_dir('.git')) { $ignoreFile = realpath('.gitignore'); if (false === $ignoreFile) { $ignoreFile = realpath('.') . '/.gitignore'; } if (!$this->hasVendorIgnore($ignoreFile)) { $question = 'Would you like the vendor directory added to your .gitignore [yes]? '; if ($io->askConfirmation($question)) { $this->addVendorIgnore($ignoreFile); } } } $question = 'Would you like to install dependencies now [yes]? '; if ($input->isInteractive() && $this->hasDependencies($options) && $io->askConfirmation($question)) { $this->updateDependencies($output); } // --autoload - Show post-install configuration info if ($autoloadPath) { $namespace = $this->namespaceFromPackageName($input->getOption('name')); $io->writeError('PSR-4 autoloading configured. Use "namespace '.$namespace.';" in '.$autoloadPath); $io->writeError('Include the Composer autoloader with: require \'vendor/autoload.php\';'); } return 0; } /** * @inheritDoc * * @return void */ protected function interact(InputInterface $input, OutputInterface $output) { $git = $this->getGitConfig(); $io = $this->getIO(); /** @var FormatterHelper $formatter */ $formatter = $this->getHelperSet()->get('formatter'); // initialize repos if configured $repositories = $input->getOption('repository'); if ($repositories) { $config = Factory::createConfig($io); $repos = array(new PlatformRepository); $createDefaultPackagistRepo = true; foreach ($repositories as $repo) { $repoConfig = RepositoryFactory::configFromString($io, $config, $repo, true); if ( (isset($repoConfig['packagist']) && $repoConfig === array('packagist' => false)) || (isset($repoConfig['packagist.org']) && $repoConfig === array('packagist.org' => false)) ) { $createDefaultPackagistRepo = false; continue; } $repos[] = RepositoryFactory::createRepo($io, $config, $repoConfig); } if ($createDefaultPackagistRepo) { $repos[] = RepositoryFactory::createRepo($io, $config, array( 'type' => 'composer', 'url' => 'https://repo.packagist.org', )); } $this->repos = new CompositeRepository($repos); unset($repos, $config, $repositories); } $io->writeError(array( '', $formatter->formatBlock('Welcome to the Composer config generator', 'bg=blue;fg=white', true), '', )); // namespace $io->writeError(array( '', 'This command will guide you through creating your composer.json config.', '', )); $cwd = realpath("."); if (!$name = $input->getOption('name')) { $name = basename($cwd); $name = Preg::replace('{(?:([a-z])([A-Z])|([A-Z])([A-Z][a-z]))}', '\\1\\3-\\2\\4', $name); $name = strtolower($name); if (!empty($_SERVER['COMPOSER_DEFAULT_VENDOR'])) { $name = $_SERVER['COMPOSER_DEFAULT_VENDOR'] . '/' . $name; } elseif (isset($git['github.user'])) { $name = $git['github.user'] . '/' . $name; } elseif (!empty($_SERVER['USERNAME'])) { $name = $_SERVER['USERNAME'] . '/' . $name; } elseif (!empty($_SERVER['USER'])) { $name = $_SERVER['USER'] . '/' . $name; } elseif (get_current_user()) { $name = get_current_user() . '/' . $name; } else { // package names must be in the format foo/bar $name .= '/' . $name; } $name = strtolower($name); } $name = $io->askAndValidate( 'Package name (/) ['.$name.']: ', function ($value) use ($name) { if (null === $value) { return $name; } if (!Preg::isMatch('{^[a-z0-9_.-]+/[a-z0-9_.-]+$}D', $value)) { throw new \InvalidArgumentException( 'The package name '.$value.' is invalid, it should be lowercase and have a vendor name, a forward slash, and a package name, matching: [a-z0-9_.-]+/[a-z0-9_.-]+' ); } return $value; }, null, $name ); $input->setOption('name', $name); $description = $input->getOption('description') ?: false; $description = $io->ask( 'Description ['.$description.']: ', $description ); $input->setOption('description', $description); if (null === $author = $input->getOption('author')) { if (!empty($_SERVER['COMPOSER_DEFAULT_AUTHOR'])) { $author_name = $_SERVER['COMPOSER_DEFAULT_AUTHOR']; } elseif (isset($git['user.name'])) { $author_name = $git['user.name']; } if (!empty($_SERVER['COMPOSER_DEFAULT_EMAIL'])) { $author_email = $_SERVER['COMPOSER_DEFAULT_EMAIL']; } elseif (isset($git['user.email'])) { $author_email = $git['user.email']; } if (isset($author_name, $author_email)) { $author = sprintf('%s <%s>', $author_name, $author_email); } } $self = $this; $author = $io->askAndValidate( 'Author ['.$author.', n to skip]: ', function ($value) use ($self, $author) { if ($value === 'n' || $value === 'no') { return; } $value = $value ?: $author; $author = $self->parseAuthorString($value); return sprintf('%s <%s>', $author['name'], $author['email']); }, null, $author ); $input->setOption('author', $author); $minimumStability = $input->getOption('stability') ?: null; $minimumStability = $io->askAndValidate( 'Minimum Stability ['.$minimumStability.']: ', function ($value) use ($minimumStability) { if (null === $value) { return $minimumStability; } if (!isset(BasePackage::$stabilities[$value])) { throw new \InvalidArgumentException( 'Invalid minimum stability "'.$value.'". Must be empty or one of: '. implode(', ', array_keys(BasePackage::$stabilities)) ); } return $value; }, null, $minimumStability ); $input->setOption('stability', $minimumStability); $type = $input->getOption('type') ?: false; $type = $io->ask( 'Package Type (e.g. library, project, metapackage, composer-plugin) ['.$type.']: ', $type ); $input->setOption('type', $type); if (null === $license = $input->getOption('license')) { if (!empty($_SERVER['COMPOSER_DEFAULT_LICENSE'])) { $license = $_SERVER['COMPOSER_DEFAULT_LICENSE']; } } $license = $io->ask( 'License ['.$license.']: ', $license ); $input->setOption('license', $license); $io->writeError(array('', 'Define your dependencies.', '')); // prepare to resolve dependencies $repos = $this->getRepos(); $preferredStability = $minimumStability ?: 'stable'; $platformRepo = null; if ($repos instanceof CompositeRepository) { foreach ($repos->getRepositories() as $candidateRepo) { if ($candidateRepo instanceof PlatformRepository) { $platformRepo = $candidateRepo; break; } } } $question = 'Would you like to define your dependencies (require) interactively [yes]? '; $require = $input->getOption('require'); $requirements = array(); if ($require || $io->askConfirmation($question)) { $requirements = $this->determineRequirements($input, $output, $require, $platformRepo, $preferredStability); } $input->setOption('require', $requirements); $question = 'Would you like to define your dev dependencies (require-dev) interactively [yes]? '; $requireDev = $input->getOption('require-dev'); $devRequirements = array(); if ($requireDev || $io->askConfirmation($question)) { $devRequirements = $this->determineRequirements($input, $output, $requireDev, $platformRepo, $preferredStability); } $input->setOption('require-dev', $devRequirements); // --autoload - input and validation $autoload = $input->getOption('autoload') ?: 'src/'; $namespace = $this->namespaceFromPackageName($input->getOption('name')); $autoload = $io->askAndValidate( 'Add PSR-4 autoload mapping? Maps namespace "'.$namespace.'" to the entered relative path. ['.$autoload.', n to skip]: ', function ($value) use ($autoload) { if (null === $value) { return $autoload; } if ($value === 'n' || $value === 'no') { return; } $value = $value ?: $autoload; if (!Preg::isMatch('{^[^/][A-Za-z0-9\-_/]+/$}', $value)) { throw new \InvalidArgumentException(sprintf( 'The src folder name "%s" is invalid. Please add a relative path with tailing forward slash. [A-Za-z0-9_-/]+/', $value )); } return $value; }, null, $autoload ); $input->setOption('autoload', $autoload); } /** * @private * @param string $author * @return array{name: string, email: string} */ public function parseAuthorString($author) { if (Preg::isMatch('/^(?P[- .,\p{L}\p{N}\p{Mn}\'’"()]+) <(?P.+?)>$/u', $author, $match)) { if ($this->isValidEmail($match['email'])) { return array( 'name' => trim($match['name']), 'email' => $match['email'], ); } } throw new \InvalidArgumentException( 'Invalid author string. Must be in the format: '. 'John Smith ' ); } /** * @return CompositeRepository */ protected function getRepos() { if (!$this->repos) { $this->repos = new CompositeRepository(array_merge( array(new PlatformRepository), RepositoryFactory::defaultRepos($this->getIO()) )); } return $this->repos; } /** * @param array $requires * @param PlatformRepository|null $platformRepo * @param string $preferredStability * @param bool $checkProvidedVersions * @param bool $fixed * * @return array * @throws \Exception */ final protected function determineRequirements(InputInterface $input, OutputInterface $output, $requires = array(), PlatformRepository $platformRepo = null, $preferredStability = 'stable', $checkProvidedVersions = true, $fixed = false) { if ($requires) { $requires = $this->normalizeRequirements($requires); $result = array(); $io = $this->getIO(); foreach ($requires as $requirement) { if (!isset($requirement['version'])) { // determine the best version automatically list($name, $version) = $this->findBestVersionAndNameForPackage($input, $requirement['name'], $platformRepo, $preferredStability, null, null, $fixed); $requirement['version'] = $version; // replace package name from packagist.org $requirement['name'] = $name; $io->writeError(sprintf( 'Using version %s for %s', $requirement['version'], $requirement['name'] )); } else { // check that the specified version/constraint exists before we proceed list($name) = $this->findBestVersionAndNameForPackage($input, $requirement['name'], $platformRepo, $preferredStability, $checkProvidedVersions ? $requirement['version'] : null, 'dev', $fixed); // replace package name from packagist.org $requirement['name'] = $name; } $result[] = $requirement['name'] . ' ' . $requirement['version']; } return $result; } $versionParser = new VersionParser(); // Collect existing packages $composer = $this->getComposer(false); $installedRepo = $composer ? $composer->getRepositoryManager()->getLocalRepository() : null; $existingPackages = array(); if ($installedRepo) { foreach ($installedRepo->getPackages() as $package) { $existingPackages[] = $package->getName(); } } unset($composer, $installedRepo); $io = $this->getIO(); while (null !== $package = $io->ask('Search for a package: ')) { $matches = $this->getRepos()->search($package); if (count($matches)) { // Remove existing packages from search results. foreach ($matches as $position => $foundPackage) { if (in_array($foundPackage['name'], $existingPackages, true)) { unset($matches[$position]); } } $matches = array_values($matches); $exactMatch = null; $choices = array(); foreach ($matches as $position => $foundPackage) { $abandoned = ''; if (isset($foundPackage['abandoned'])) { if (is_string($foundPackage['abandoned'])) { $replacement = sprintf('Use %s instead', $foundPackage['abandoned']); } else { $replacement = 'No replacement was suggested'; } $abandoned = sprintf('Abandoned. %s.', $replacement); } $choices[] = sprintf(' %5s %s %s', "[$position]", $foundPackage['name'], $abandoned); if ($foundPackage['name'] === $package) { $exactMatch = true; break; } } // no match, prompt which to pick if (!$exactMatch) { $io->writeError(array( '', sprintf('Found %s packages matching %s', count($matches), $package), '', )); $io->writeError($choices); $io->writeError(''); $validator = function ($selection) use ($matches, $versionParser) { if ('' === $selection) { return false; } if (is_numeric($selection) && isset($matches[(int) $selection])) { $package = $matches[(int) $selection]; return $package['name']; } if (Preg::isMatch('{^\s*(?P[\S/]+)(?:\s+(?P\S+))?\s*$}', $selection, $packageMatches)) { if (isset($packageMatches['version'])) { // parsing `acme/example ~2.3` // validate version constraint $versionParser->parseConstraints($packageMatches['version']); return $packageMatches['name'].' '.$packageMatches['version']; } // parsing `acme/example` return $packageMatches['name']; } throw new \Exception('Not a valid selection'); }; $package = $io->askAndValidate( 'Enter package # to add, or the complete package name if it is not listed: ', $validator, 3, false ); } // no constraint yet, determine the best version automatically if (false !== $package && false === strpos($package, ' ')) { $validator = function ($input) { $input = trim($input); return $input ?: false; }; $constraint = $io->askAndValidate( 'Enter the version constraint to require (or leave blank to use the latest version): ', $validator, 3, false ); if (false === $constraint) { list(, $constraint) = $this->findBestVersionAndNameForPackage($input, $package, $platformRepo, $preferredStability); $io->writeError(sprintf( 'Using version %s for %s', $constraint, $package )); } $package .= ' '.$constraint; } if (false !== $package) { $requires[] = $package; $existingPackages[] = substr($package, 0, strpos($package, ' ')); } } } return $requires; } /** * @param string $author * * @return array */ protected function formatAuthors($author) { return array($this->parseAuthorString($author)); } /** * Extract namespace from package's vendor name. * * new_projects.acme-extra/package-name becomes "NewProjectsAcmeExtra\PackageName" * * @param string $packageName * * @return string|null */ public function namespaceFromPackageName($packageName) { if (!$packageName || strpos($packageName, '/') === false) { return null; } $namespace = array_map( function ($part) { $part = Preg::replace('/[^a-z0-9]/i', ' ', $part); $part = ucwords($part); return str_replace(' ', '', $part); }, explode('/', $packageName) ); return join('\\', $namespace); } /** * @return array */ protected function getGitConfig() { if (null !== $this->gitConfig) { return $this->gitConfig; } $finder = new ExecutableFinder(); $gitBin = $finder->find('git'); // TODO in v2.3 always call with an array if (method_exists('Symfony\Component\Process\Process', 'fromShellCommandline')) { $cmd = new Process(array($gitBin, 'config', '-l')); } else { // @phpstan-ignore-next-line $cmd = new Process(sprintf('%s config -l', ProcessExecutor::escape($gitBin))); } $cmd->run(); if ($cmd->isSuccessful()) { $this->gitConfig = array(); Preg::matchAll('{^([^=]+)=(.*)$}m', $cmd->getOutput(), $matches); foreach ($matches[1] as $key => $match) { $this->gitConfig[$match] = $matches[2][$key]; } return $this->gitConfig; } return $this->gitConfig = array(); } /** * Checks the local .gitignore file for the Composer vendor directory. * * Tested patterns include: * "/$vendor" * "$vendor" * "$vendor/" * "/$vendor/" * "/$vendor/*" * "$vendor/*" * * @param string $ignoreFile * @param string $vendor * * @return bool */ protected function hasVendorIgnore($ignoreFile, $vendor = 'vendor') { if (!file_exists($ignoreFile)) { return false; } $pattern = sprintf('{^/?%s(/\*?)?$}', preg_quote($vendor)); $lines = file($ignoreFile, FILE_IGNORE_NEW_LINES); foreach ($lines as $line) { if (Preg::isMatch($pattern, $line)) { return true; } } return false; } /** * @param string $ignoreFile * @param string $vendor * * @return void */ protected function addVendorIgnore($ignoreFile, $vendor = '/vendor/') { $contents = ""; if (file_exists($ignoreFile)) { $contents = file_get_contents($ignoreFile); if (strpos($contents, "\n") !== 0) { $contents .= "\n"; } } file_put_contents($ignoreFile, $contents . $vendor. "\n"); } /** * @param string $email * * @return bool */ protected function isValidEmail($email) { // assume it's valid if we can't validate it if (!function_exists('filter_var')) { return true; } // php <5.3.3 has a very broken email validator, so bypass checks if (PHP_VERSION_ID < 50303) { return true; } return false !== filter_var($email, FILTER_VALIDATE_EMAIL); } /** * @param string|null $minimumStability * * @return RepositorySet */ private function getRepositorySet(InputInterface $input, $minimumStability = null) { $key = $minimumStability ?: 'default'; if (!isset($this->repositorySets[$key])) { $this->repositorySets[$key] = $repositorySet = new RepositorySet($minimumStability ?: $this->getMinimumStability($input)); $repositorySet->addRepository($this->getRepos()); } return $this->repositorySets[$key]; } /** * @return string */ private function getMinimumStability(InputInterface $input) { if ($input->hasOption('stability')) { return VersionParser::normalizeStability($input->getOption('stability') ?: 'stable'); } $file = Factory::getComposerFile(); if (is_file($file) && Filesystem::isReadable($file) && is_array($composer = json_decode(file_get_contents($file), true))) { if (!empty($composer['minimum-stability'])) { return VersionParser::normalizeStability($composer['minimum-stability']); } } return 'stable'; } /** * Given a package name, this determines the best version to use in the require key. * * This returns a version with the ~ operator prefixed when possible. * * @param string $name * @param PlatformRepository|null $platformRepo * @param string $preferredStability * @param string|null $requiredVersion * @param string $minimumStability * @param bool $fixed * @throws \InvalidArgumentException * @return array{string, string} name version */ private function findBestVersionAndNameForPackage(InputInterface $input, $name, PlatformRepository $platformRepo = null, $preferredStability = 'stable', $requiredVersion = null, $minimumStability = null, $fixed = null) { // handle ignore-platform-reqs flag if present $ignorePlatformReqs = false; if ($input->hasOption('ignore-platform-reqs') && $input->hasOption('ignore-platform-req')) { $ignorePlatformReqs = $input->getOption('ignore-platform-reqs') ?: ($input->getOption('ignore-platform-req') ?: false); } $platformRequirementFilter = PlatformRequirementFilterFactory::fromBoolOrList($ignorePlatformReqs); // find the latest version allowed in this repo set $versionSelector = new VersionSelector($this->getRepositorySet($input, $minimumStability), $platformRepo); $effectiveMinimumStability = $minimumStability ?: $this->getMinimumStability($input); $package = $versionSelector->findBestCandidate($name, $requiredVersion, $preferredStability, $platformRequirementFilter); if (!$package) { // platform packages can not be found in the pool in versions other than the local platform's has // so if platform reqs are ignored we just take the user's word for it if ($platformRequirementFilter->isIgnored($name)) { return array($name, $requiredVersion ?: '*'); } // Check whether the package requirements were the problem if (!($platformRequirementFilter instanceof IgnoreAllPlatformRequirementFilter) && ($candidate = $versionSelector->findBestCandidate($name, $requiredVersion, $preferredStability, PlatformRequirementFilterFactory::ignoreAll()))) { throw new \InvalidArgumentException(sprintf( 'Package %s%s has requirements incompatible with your PHP version, PHP extensions and Composer version' . $this->getPlatformExceptionDetails($candidate, $platformRepo), $name, $requiredVersion ? ' at version '.$requiredVersion : '' )); } // Check whether the minimum stability was the problem but the package exists if ($package = $versionSelector->findBestCandidate($name, $requiredVersion, $preferredStability, $platformRequirementFilter, RepositorySet::ALLOW_UNACCEPTABLE_STABILITIES)) { // we must first verify if a valid package would be found in a lower priority repository if ($allReposPackage = $versionSelector->findBestCandidate($name, $requiredVersion, $preferredStability, $platformRequirementFilter, RepositorySet::ALLOW_SHADOWED_REPOSITORIES)) { throw new \InvalidArgumentException( 'Package '.$name.' exists in '.$allReposPackage->getRepository()->getRepoName().' and '.$package->getRepository()->getRepoName().' which has a higher repository priority. The packages from the higher priority repository do not match your minimum-stability and are therefore not installable. That repository is canonical so the lower priority repo\'s packages are not installable. See https://getcomposer.org/repoprio for details and assistance.' ); } throw new \InvalidArgumentException(sprintf( 'Could not find a version of package %s matching your minimum-stability (%s). Require it with an explicit version constraint allowing its desired stability.', $name, $effectiveMinimumStability )); } // Check whether the required version was the problem if ($requiredVersion && $package = $versionSelector->findBestCandidate($name, null, $preferredStability, $platformRequirementFilter)) { // we must first verify if a valid package would be found in a lower priority repository if ($allReposPackage = $versionSelector->findBestCandidate($name, $requiredVersion, $preferredStability, PlatformRequirementFilterFactory::ignoreNothing(), RepositorySet::ALLOW_SHADOWED_REPOSITORIES)) { throw new \InvalidArgumentException( 'Package '.$name.' exists in '.$allReposPackage->getRepository()->getRepoName().' and '.$package->getRepository()->getRepoName().' which has a higher repository priority. The packages from the higher priority repository do not match your constraint and are therefore not installable. That repository is canonical so the lower priority repo\'s packages are not installable. See https://getcomposer.org/repoprio for details and assistance.' ); } throw new \InvalidArgumentException(sprintf( 'Could not find package %s in a version matching "%s" and a stability matching "'.$effectiveMinimumStability.'".', $name, $requiredVersion )); } // Check whether the PHP version was the problem for all versions if (!($platformRequirementFilter instanceof IgnoreAllPlatformRequirementFilter) && ($candidate = $versionSelector->findBestCandidate($name, null, $preferredStability, PlatformRequirementFilterFactory::ignoreAll(), RepositorySet::ALLOW_UNACCEPTABLE_STABILITIES))) { $additional = ''; if (false === $versionSelector->findBestCandidate($name, null, $preferredStability, PlatformRequirementFilterFactory::ignoreAll())) { $additional = PHP_EOL.PHP_EOL.'Additionally, the package was only found with a stability of "'.$candidate->getStability().'" while your minimum stability is "'.$effectiveMinimumStability.'".'; } throw new \InvalidArgumentException(sprintf( 'Could not find package %s in any version matching your PHP version, PHP extensions and Composer version' . $this->getPlatformExceptionDetails($candidate, $platformRepo) . '%s', $name, $additional )); } // Check for similar names/typos $similar = $this->findSimilar($name); if ($similar) { if (in_array($name, $similar, true)) { throw new \InvalidArgumentException(sprintf( "Could not find package %s. It was however found via repository search, which indicates a consistency issue with the repository.", $name )); } throw new \InvalidArgumentException(sprintf( "Could not find package %s.\n\nDid you mean " . (count($similar) > 1 ? 'one of these' : 'this') . "?\n %s", $name, implode("\n ", $similar) )); } throw new \InvalidArgumentException(sprintf( 'Could not find a matching version of package %s. Check the package spelling, your version constraint and that the package is available in a stability which matches your minimum-stability (%s).', $name, $effectiveMinimumStability )); } return array( $package->getPrettyName(), $fixed ? $package->getPrettyVersion() : $versionSelector->findRecommendedRequireVersion($package), ); } /** * @return string */ private function getPlatformExceptionDetails(PackageInterface $candidate, PlatformRepository $platformRepo = null) { $details = array(); if (!$platformRepo) { return ''; } foreach ($candidate->getRequires() as $link) { if (!PlatformRepository::isPlatformPackage($link->getTarget())) { continue; } $platformPkg = $platformRepo->findPackage($link->getTarget(), '*'); if (!$platformPkg) { if ($platformRepo->isPlatformPackageDisabled($link->getTarget())) { $details[] = $candidate->getPrettyName().' '.$candidate->getPrettyVersion().' requires '.$link->getTarget().' '.$link->getPrettyConstraint().' but it is disabled by your platform config. Enable it again with "composer config platform.'.$link->getTarget().' --unset".'; } else { $details[] = $candidate->getPrettyName().' '.$candidate->getPrettyVersion().' requires '.$link->getTarget().' '.$link->getPrettyConstraint().' but it is not present.'; } continue; } if (!$link->getConstraint()->matches(new Constraint('==', $platformPkg->getVersion()))) { $platformPkgVersion = $platformPkg->getPrettyVersion(); $platformExtra = $platformPkg->getExtra(); if (isset($platformExtra['config.platform']) && $platformPkg instanceof CompletePackageInterface) { $platformPkgVersion .= ' ('.$platformPkg->getDescription().')'; } $details[] = $candidate->getPrettyName().' '.$candidate->getPrettyVersion().' requires '.$link->getTarget().' '.$link->getPrettyConstraint().' which does not match your installed version '.$platformPkgVersion.'.'; } } if (!$details) { return ''; } return ':'.PHP_EOL.' - ' . implode(PHP_EOL.' - ', $details); } /** * @param string $package * * @return array */ private function findSimilar($package) { try { $results = $this->repos->search($package); } catch (\Exception $e) { // ignore search errors return array(); } $similarPackages = array(); $installedRepo = $this->getComposer()->getRepositoryManager()->getLocalRepository(); foreach ($results as $result) { if ($installedRepo->findPackage($result['name'], '*')) { // Ignore installed package continue; } $similarPackages[$result['name']] = levenshtein($package, $result['name']); } asort($similarPackages); return array_keys(array_slice($similarPackages, 0, 5)); } /** * @return void */ private function updateDependencies(OutputInterface $output) { try { $updateCommand = $this->getApplication()->find('update'); $this->getApplication()->resetComposer(); $updateCommand->run(new ArrayInput(array()), $output); } catch (\Exception $e) { $this->getIO()->writeError('Could not update dependencies. Run `composer update` to see more information.'); } } /** * @return void */ private function runDumpAutoloadCommand(OutputInterface $output) { try { $command = $this->getApplication()->find('dump-autoload'); $this->getApplication()->resetComposer(); $command->run(new ArrayInput(array()), $output); } catch (\Exception $e) { $this->getIO()->writeError('Could not run dump-autoload.'); } } /** * @param array> $options * @return bool */ private function hasDependencies($options) { $requires = (array) $options['require']; $devRequires = isset($options['require-dev']) ? (array) $options['require-dev'] : array(); return !empty($requires) || !empty($devRequires); } } * Jordi Boggiano * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Composer\Command; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Output\OutputInterface; /** * @author Jordi Boggiano */ class ScriptAliasCommand extends BaseCommand { /** @var string */ private $script; /** @var string */ private $description; /** * @param string $script * @param string $description */ public function __construct($script, $description) { $this->script = $script; $this->description = empty($description) ? 'Runs the '.$script.' script as defined in composer.json.' : $description; parent::__construct(); } /** * @return void */ protected function configure() { $this ->setName($this->script) ->setDescription($this->description) ->setDefinition(array( new InputOption('dev', null, InputOption::VALUE_NONE, 'Sets the dev mode.'), new InputOption('no-dev', null, InputOption::VALUE_NONE, 'Disables the dev mode.'), new InputArgument('args', InputArgument::IS_ARRAY | InputArgument::OPTIONAL, ''), )) ->setHelp( <<run-script command runs scripts defined in composer.json: php composer.phar run-script post-update-cmd Read more at https://getcomposer.org/doc/03-cli.md#run-script EOT ) ; } /** * @return int */ protected function execute(InputInterface $input, OutputInterface $output) { $composer = $this->getComposer(); $args = $input->getArguments(); return $composer->getEventDispatcher()->dispatchScript($this->script, $input->getOption('dev') || !$input->getOption('no-dev'), $args['args']); } } * Jordi Boggiano * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Composer\Command; use Composer\DependencyResolver\Request; use Composer\Filter\PlatformRequirementFilter\PlatformRequirementFilterFactory; use Composer\Util\Filesystem; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; use Composer\Factory; use Composer\Installer; use Composer\Installer\InstallerEvents; use Composer\Json\JsonFile; use Composer\Json\JsonManipulator; use Composer\Package\Version\VersionParser; use Composer\Package\Loader\ArrayLoader; use Composer\Package\BasePackage; use Composer\Plugin\CommandEvent; use Composer\Plugin\PluginEvents; use Composer\Repository\CompositeRepository; use Composer\Repository\PlatformRepository; use Composer\IO\IOInterface; use Composer\Util\Silencer; /** * @author Jérémy Romey * @author Jordi Boggiano */ class RequireCommand extends InitCommand { /** @var bool */ private $newlyCreated; /** @var bool */ private $firstRequire; /** @var JsonFile */ private $json; /** @var string */ private $file; /** @var string */ private $composerBackup; /** @var string file name */ private $lock; /** @var ?string contents before modification if the lock file exists */ private $lockBackup; /** @var bool */ private $dependencyResolutionCompleted = false; /** * @return void */ protected function configure() { $this ->setName('require') ->setDescription('Adds required packages to your composer.json and installs them.') ->setDefinition(array( new InputArgument('packages', InputArgument::IS_ARRAY | InputArgument::OPTIONAL, 'Optional package name can also include a version constraint, e.g. foo/bar or foo/bar:1.0.0 or foo/bar=1.0.0 or "foo/bar 1.0.0"'), new InputOption('dev', null, InputOption::VALUE_NONE, 'Add requirement to require-dev.'), new InputOption('dry-run', null, InputOption::VALUE_NONE, 'Outputs the operations but will not execute anything (implicitly enables --verbose).'), new InputOption('prefer-source', null, InputOption::VALUE_NONE, 'Forces installation from package sources when possible, including VCS information.'), new InputOption('prefer-dist', null, InputOption::VALUE_NONE, 'Forces installation from package dist (default behavior).'), new InputOption('prefer-install', null, InputOption::VALUE_REQUIRED, 'Forces installation from package dist|source|auto (auto chooses source for dev versions, dist for the rest).'), new InputOption('fixed', null, InputOption::VALUE_NONE, 'Write fixed version to the composer.json.'), new InputOption('no-suggest', null, InputOption::VALUE_NONE, 'DEPRECATED: This flag does not exist anymore.'), new InputOption('no-progress', null, InputOption::VALUE_NONE, 'Do not output download progress.'), new InputOption('no-update', null, InputOption::VALUE_NONE, 'Disables the automatic update of the dependencies (implies --no-install).'), new InputOption('no-install', null, InputOption::VALUE_NONE, 'Skip the install step after updating the composer.lock file.'), new InputOption('update-no-dev', null, InputOption::VALUE_NONE, 'Run the dependency update with the --no-dev option.'), new InputOption('update-with-dependencies', 'w', InputOption::VALUE_NONE, 'Allows inherited dependencies to be updated, except those that are root requirements.'), new InputOption('update-with-all-dependencies', 'W', InputOption::VALUE_NONE, 'Allows all inherited dependencies to be updated, including those that are root requirements.'), new InputOption('with-dependencies', null, InputOption::VALUE_NONE, 'Alias for --update-with-dependencies'), new InputOption('with-all-dependencies', null, InputOption::VALUE_NONE, 'Alias for --update-with-all-dependencies'), new InputOption('ignore-platform-req', null, InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY, 'Ignore a specific platform requirement (php & ext- packages).'), new InputOption('ignore-platform-reqs', null, InputOption::VALUE_NONE, 'Ignore all platform requirements (php & ext- packages).'), new InputOption('prefer-stable', null, InputOption::VALUE_NONE, 'Prefer stable versions of dependencies.'), new InputOption('prefer-lowest', null, InputOption::VALUE_NONE, 'Prefer lowest versions of dependencies.'), new InputOption('sort-packages', null, InputOption::VALUE_NONE, 'Sorts packages when adding/updating a new dependency'), new InputOption('optimize-autoloader', 'o', InputOption::VALUE_NONE, 'Optimize autoloader during autoloader dump'), new InputOption('classmap-authoritative', 'a', InputOption::VALUE_NONE, 'Autoload classes from the classmap only. Implicitly enables `--optimize-autoloader`.'), new InputOption('apcu-autoloader', null, InputOption::VALUE_NONE, 'Use APCu to cache found/not-found classes.'), new InputOption('apcu-autoloader-prefix', null, InputOption::VALUE_REQUIRED, 'Use a custom prefix for the APCu autoloader cache. Implicitly enables --apcu-autoloader'), )) ->setHelp( <<file = Factory::getComposerFile(); $io = $this->getIO(); if ($input->getOption('no-suggest')) { $io->writeError('You are using the deprecated option "--no-suggest". It has no effect and will break in Composer 3.'); } $this->newlyCreated = !file_exists($this->file); if ($this->newlyCreated && !file_put_contents($this->file, "{\n}\n")) { $io->writeError(''.$this->file.' could not be created.'); return 1; } if (!Filesystem::isReadable($this->file)) { $io->writeError(''.$this->file.' is not readable.'); return 1; } if (filesize($this->file) === 0) { file_put_contents($this->file, "{\n}\n"); } $this->json = new JsonFile($this->file); $this->lock = Factory::getLockFile($this->file); $this->composerBackup = file_get_contents($this->json->getPath()); $this->lockBackup = file_exists($this->lock) ? file_get_contents($this->lock) : null; // check for writability by writing to the file as is_writable can not be trusted on network-mounts // see https://github.com/composer/composer/issues/8231 and https://bugs.php.net/bug.php?id=68926 if (!is_writable($this->file) && !Silencer::call('file_put_contents', $this->file, $this->composerBackup)) { $io->writeError(''.$this->file.' is not writable.'); return 1; } if ($input->getOption('fixed') === true) { $config = $this->json->read(); $packageType = empty($config['type']) ? 'library' : $config['type']; /** * @see https://github.com/composer/composer/pull/8313#issuecomment-532637955 */ if ($packageType !== 'project') { $io->writeError('"--fixed" option is allowed for "project" package types only to prevent possible misuses.'); if (empty($config['type'])) { $io->writeError('If your package is not library, you should explicitly specify "type" parameter in composer.json.'); } return 1; } } $composer = $this->getComposer(true, $input->getOption('no-plugins')); $repos = $composer->getRepositoryManager()->getRepositories(); $platformOverrides = $composer->getConfig()->get('platform') ?: array(); // initialize $this->repos as it is used by the parent InitCommand $this->repos = new CompositeRepository(array_merge( array($platformRepo = new PlatformRepository(array(), $platformOverrides)), $repos )); if ($composer->getPackage()->getPreferStable()) { $preferredStability = 'stable'; } else { $preferredStability = $composer->getPackage()->getMinimumStability(); } try { $requirements = $this->determineRequirements( $input, $output, $input->getArgument('packages'), $platformRepo, $preferredStability, !$input->getOption('no-update'), $input->getOption('fixed') ); } catch (\Exception $e) { if ($this->newlyCreated) { $this->revertComposerFile(false); throw new \RuntimeException('No composer.json present in the current directory ('.$this->file.'), this may be the cause of the following exception.', 0, $e); } throw $e; } $requireKey = $input->getOption('dev') ? 'require-dev' : 'require'; $removeKey = $input->getOption('dev') ? 'require' : 'require-dev'; $requirements = $this->formatRequirements($requirements); // validate requirements format $versionParser = new VersionParser(); foreach ($requirements as $package => $constraint) { if (strtolower($package) === $composer->getPackage()->getName()) { $io->writeError(sprintf('Root package \'%s\' cannot require itself in its composer.json', $package)); return 1; } $versionParser->parseConstraints($constraint); } $inconsistentRequireKeys = $this->getInconsistentRequireKeys($requirements, $requireKey); if (count($inconsistentRequireKeys) > 0) { foreach ($inconsistentRequireKeys as $package) { $io->warning(sprintf( '%s is currently present in the %s key and you ran the command %s the --dev flag, which would move it to the %s key.', $package, $removeKey, $input->getOption('dev') ? 'with' : 'without', $requireKey )); } if ($io->isInteractive()) { if (!$io->askConfirmation(sprintf('Do you want to move %s? [no]? ', count($inconsistentRequireKeys) > 1 ? 'these requirements' : 'this requirement'), false)) { if (!$io->askConfirmation(sprintf('Do you want to re-run the command %s --dev? [yes]? ', $input->getOption('dev') ? 'without' : 'with'), true)) { return 0; } list($requireKey, $removeKey) = array($removeKey, $requireKey); } } } $sortPackages = $input->getOption('sort-packages') || $composer->getConfig()->get('sort-packages'); $this->firstRequire = $this->newlyCreated; if (!$this->firstRequire) { $composerDefinition = $this->json->read(); if (empty($composerDefinition['require']) && empty($composerDefinition['require-dev'])) { $this->firstRequire = true; } } if (!$input->getOption('dry-run') && !$this->updateFileCleanly($this->json, $requirements, $requireKey, $removeKey, $sortPackages)) { $composerDefinition = $this->json->read(); foreach ($requirements as $package => $version) { $composerDefinition[$requireKey][$package] = $version; unset($composerDefinition[$removeKey][$package]); if (isset($composerDefinition[$removeKey]) && count($composerDefinition[$removeKey]) === 0) { unset($composerDefinition[$removeKey]); } } $this->json->write($composerDefinition); } $io->writeError(''.$this->file.' has been '.($this->newlyCreated ? 'created' : 'updated').''); if ($input->getOption('no-update')) { return 0; } $composer->getPluginManager()->deactivateInstalledPlugins(); try { return $this->doUpdate($input, $output, $io, $requirements, $requireKey, $removeKey); } catch (\Exception $e) { if (!$this->dependencyResolutionCompleted) { $this->revertComposerFile(false); } throw $e; } } /** * @param array $newRequirements * @param string $requireKey * @return string[] */ private function getInconsistentRequireKeys(array $newRequirements, $requireKey) { $requireKeys = $this->getPackagesByRequireKey(); $inconsistentRequirements = array(); foreach ($requireKeys as $package => $packageRequireKey) { if (!isset($newRequirements[$package])) { continue; } if ($requireKey !== $packageRequireKey) { $inconsistentRequirements[] = $package; } } return $inconsistentRequirements; } /** * @return array */ private function getPackagesByRequireKey() { $composerDefinition = $this->json->read(); $require = array(); $requireDev = array(); if (isset($composerDefinition['require'])) { $require = $composerDefinition['require']; } if (isset($composerDefinition['require-dev'])) { $requireDev = $composerDefinition['require-dev']; } return array_merge( array_fill_keys(array_keys($require), 'require'), array_fill_keys(array_keys($requireDev), 'require-dev') ); } /** * @private * @return void */ public function markSolverComplete() { $this->dependencyResolutionCompleted = true; } /** * @param array $requirements * @param string $requireKey * @param string $removeKey * @return int * @throws \Exception */ private function doUpdate(InputInterface $input, OutputInterface $output, IOInterface $io, array $requirements, $requireKey, $removeKey) { // Update packages $this->resetComposer(); $composer = $this->getComposer(true, $input->getOption('no-plugins'), $input->getOption('no-scripts')); $this->dependencyResolutionCompleted = false; $composer->getEventDispatcher()->addListener(InstallerEvents::PRE_OPERATIONS_EXEC, array($this, 'markSolverComplete'), 10000); if ($input->getOption('dry-run')) { $rootPackage = $composer->getPackage(); $links = array( 'require' => $rootPackage->getRequires(), 'require-dev' => $rootPackage->getDevRequires(), ); $loader = new ArrayLoader(); $newLinks = $loader->parseLinks($rootPackage->getName(), $rootPackage->getPrettyVersion(), BasePackage::$supportedLinkTypes[$requireKey]['method'], $requirements); $links[$requireKey] = array_merge($links[$requireKey], $newLinks); foreach ($requirements as $package => $constraint) { unset($links[$removeKey][$package]); } $rootPackage->setRequires($links['require']); $rootPackage->setDevRequires($links['require-dev']); } $updateDevMode = !$input->getOption('update-no-dev'); $optimize = $input->getOption('optimize-autoloader') || $composer->getConfig()->get('optimize-autoloader'); $authoritative = $input->getOption('classmap-authoritative') || $composer->getConfig()->get('classmap-authoritative'); $apcuPrefix = $input->getOption('apcu-autoloader-prefix'); $apcu = $apcuPrefix !== null || $input->getOption('apcu-autoloader') || $composer->getConfig()->get('apcu-autoloader'); $updateAllowTransitiveDependencies = Request::UPDATE_ONLY_LISTED; $flags = ''; if ($input->getOption('update-with-all-dependencies') || $input->getOption('with-all-dependencies')) { $updateAllowTransitiveDependencies = Request::UPDATE_LISTED_WITH_TRANSITIVE_DEPS; $flags .= ' --with-all-dependencies'; } elseif ($input->getOption('update-with-dependencies') || $input->getOption('with-dependencies')) { $updateAllowTransitiveDependencies = Request::UPDATE_LISTED_WITH_TRANSITIVE_DEPS_NO_ROOT_REQUIRE; $flags .= ' --with-dependencies'; } $io->writeError('Running composer update '.implode(' ', array_keys($requirements)).$flags.''); $commandEvent = new CommandEvent(PluginEvents::COMMAND, 'require', $input, $output); $composer->getEventDispatcher()->dispatch($commandEvent->getName(), $commandEvent); $composer->getInstallationManager()->setOutputProgress(!$input->getOption('no-progress')); $install = Installer::create($io, $composer); $ignorePlatformReqs = $input->getOption('ignore-platform-reqs') ?: ($input->getOption('ignore-platform-req') ?: false); list($preferSource, $preferDist) = $this->getPreferredInstallOptions($composer->getConfig(), $input); $install ->setDryRun($input->getOption('dry-run')) ->setVerbose($input->getOption('verbose')) ->setPreferSource($preferSource) ->setPreferDist($preferDist) ->setDevMode($updateDevMode) ->setOptimizeAutoloader($optimize) ->setClassMapAuthoritative($authoritative) ->setApcuAutoloader($apcu, $apcuPrefix) ->setUpdate(true) ->setInstall(!$input->getOption('no-install')) ->setUpdateAllowTransitiveDependencies($updateAllowTransitiveDependencies) ->setPlatformRequirementFilter(PlatformRequirementFilterFactory::fromBoolOrList($ignorePlatformReqs)) ->setPreferStable($input->getOption('prefer-stable')) ->setPreferLowest($input->getOption('prefer-lowest')) ; // if no lock is present, or the file is brand new, we do not do a // partial update as this is not supported by the Installer if (!$this->firstRequire && $composer->getLocker()->isLocked()) { $install->setUpdateAllowList(array_keys($requirements)); } $status = $install->run(); if ($status !== 0) { if ($status === Installer::ERROR_DEPENDENCY_RESOLUTION_FAILED) { foreach ($this->normalizeRequirements($input->getArgument('packages')) as $req) { if (!isset($req['version'])) { $io->writeError('You can also try re-running composer require with an explicit version constraint, e.g. "composer require '.$req['name'].':*" to figure out if any version is installable, or "composer require '.$req['name'].':^2.1" if you know which you need.'); break; } } } $this->revertComposerFile(false); } return $status; } /** * @param array $new * @param string $requireKey * @param string $removeKey * @param bool $sortPackages * @return bool */ private function updateFileCleanly(JsonFile $json, array $new, $requireKey, $removeKey, $sortPackages) { $contents = file_get_contents($json->getPath()); $manipulator = new JsonManipulator($contents); foreach ($new as $package => $constraint) { if (!$manipulator->addLink($requireKey, $package, $constraint, $sortPackages)) { return false; } if (!$manipulator->removeSubNode($removeKey, $package)) { return false; } } $manipulator->removeMainKeyIfEmpty($removeKey); file_put_contents($json->getPath(), $manipulator->getContents()); return true; } protected function interact(InputInterface $input, OutputInterface $output) { return; } /** * @param bool $hardExit * @return void */ public function revertComposerFile($hardExit = true) { $io = $this->getIO(); if ($this->newlyCreated) { $io->writeError("\n".'Installation failed, deleting '.$this->file.'.'); unlink($this->json->getPath()); if (file_exists($this->lock)) { unlink($this->lock); } } else { $msg = ' to its '; if ($this->lockBackup) { $msg = ' and '.$this->lock.' to their '; } $io->writeError("\n".'Installation failed, reverting '.$this->file.$msg.'original content.'); file_put_contents($this->json->getPath(), $this->composerBackup); if ($this->lockBackup) { file_put_contents($this->lock, $this->lockBackup); } } if ($hardExit) { exit(1); } } } * Jordi Boggiano * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Composer\Command; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputOption; /** * @author Niels Keurentjes */ class DependsCommand extends BaseDependencyCommand { /** * Configure command metadata. * * @return void */ protected function configure() { $this ->setName('depends') ->setAliases(array('why')) ->setDescription('Shows which packages cause the given package to be installed.') ->setDefinition(array( new InputArgument(self::ARGUMENT_PACKAGE, InputArgument::REQUIRED, 'Package to inspect'), new InputOption(self::OPTION_RECURSIVE, 'r', InputOption::VALUE_NONE, 'Recursively resolves up to the root package'), new InputOption(self::OPTION_TREE, 't', InputOption::VALUE_NONE, 'Prints the results as a nested tree'), )) ->setHelp( <<php composer.phar depends composer/composer Read more at https://getcomposer.org/doc/03-cli.md#depends-why- EOT ) ; } /** * Execute the function. * * @return int */ protected function execute(InputInterface $input, OutputInterface $output) { return parent::doExecute($input, $output); } } * Jordi Boggiano * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Composer\Command; use Composer\Config; use Composer\Factory; use Composer\Filter\PlatformRequirementFilter\IgnoreAllPlatformRequirementFilter; use Composer\Filter\PlatformRequirementFilter\PlatformRequirementFilterFactory; use Composer\Filter\PlatformRequirementFilter\PlatformRequirementFilterInterface; use Composer\Installer; use Composer\Installer\ProjectInstaller; use Composer\Installer\SuggestedPackagesReporter; use Composer\IO\IOInterface; use Composer\Package\BasePackage; use Composer\DependencyResolver\Operation\InstallOperation; use Composer\Package\Version\VersionSelector; use Composer\Package\AliasPackage; use Composer\Pcre\Preg; use Composer\Repository\RepositoryFactory; use Composer\Repository\CompositeRepository; use Composer\Repository\PlatformRepository; use Composer\Repository\InstalledArrayRepository; use Composer\Repository\RepositorySet; use Composer\Script\ScriptEvents; use Composer\Util\Silencer; 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\Finder\Finder; use Composer\Json\JsonFile; use Composer\Config\JsonConfigSource; use Composer\Util\Filesystem; use Composer\Util\Platform; use Composer\Util\ProcessExecutor; use Composer\Package\Version\VersionParser; /** * Install a package as new project into new directory. * * @author Benjamin Eberlei * @author Jordi Boggiano * @author Tobias Munk * @author Nils Adermann */ class CreateProjectCommand extends BaseCommand { /** * @var SuggestedPackagesReporter */ protected $suggestedPackagesReporter; /** * @return void */ protected function configure() { $this ->setName('create-project') ->setDescription('Creates new project from a package into given directory.') ->setDefinition(array( new InputArgument('package', InputArgument::OPTIONAL, 'Package name to be installed'), new InputArgument('directory', InputArgument::OPTIONAL, 'Directory where the files should be created'), new InputArgument('version', InputArgument::OPTIONAL, 'Version, will default to latest'), new InputOption('stability', 's', InputOption::VALUE_REQUIRED, 'Minimum-stability allowed (unless a version is specified).'), new InputOption('prefer-source', null, InputOption::VALUE_NONE, 'Forces installation from package sources when possible, including VCS information.'), new InputOption('prefer-dist', null, InputOption::VALUE_NONE, 'Forces installation from package dist (default behavior).'), new InputOption('prefer-install', null, InputOption::VALUE_REQUIRED, 'Forces installation from package dist|source|auto (auto chooses source for dev versions, dist for the rest).'), new InputOption('repository', null, InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY, 'Add custom repositories to look the package up, either by URL or using JSON arrays'), new InputOption('repository-url', null, InputOption::VALUE_REQUIRED, 'DEPRECATED: Use --repository instead.'), new InputOption('add-repository', null, InputOption::VALUE_NONE, 'Add the custom repository in the composer.json. If a lock file is present it will be deleted and an update will be run instead of install.'), new InputOption('dev', null, InputOption::VALUE_NONE, 'Enables installation of require-dev packages (enabled by default, only present for BC).'), new InputOption('no-dev', null, InputOption::VALUE_NONE, 'Disables installation of require-dev packages.'), new InputOption('no-custom-installers', null, InputOption::VALUE_NONE, 'DEPRECATED: Use no-plugins instead.'), new InputOption('no-scripts', null, InputOption::VALUE_NONE, 'Whether to prevent execution of all defined scripts in the root package.'), new InputOption('no-progress', null, InputOption::VALUE_NONE, 'Do not output download progress.'), new InputOption('no-secure-http', null, InputOption::VALUE_NONE, 'Disable the secure-http config option temporarily while installing the root package. Use at your own risk. Using this flag is a bad idea.'), new InputOption('keep-vcs', null, InputOption::VALUE_NONE, 'Whether to prevent deleting the vcs folder.'), new InputOption('remove-vcs', null, InputOption::VALUE_NONE, 'Whether to force deletion of the vcs folder without prompting.'), new InputOption('no-install', null, InputOption::VALUE_NONE, 'Whether to skip installation of the package dependencies.'), new InputOption('ignore-platform-req', null, InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY, 'Ignore a specific platform requirement (php & ext- packages).'), new InputOption('ignore-platform-reqs', null, InputOption::VALUE_NONE, 'Ignore all platform requirements (php & ext- packages).'), new InputOption('ask', null, InputOption::VALUE_NONE, 'Whether to ask for project directory.'), )) ->setHelp( <<create-project command creates a new project from a given package into a new directory. If executed without params and in a directory with a composer.json file it installs the packages for the current project. You can use this command to bootstrap new projects or setup a clean version-controlled installation for developers of your project. php composer.phar create-project vendor/project target-directory [version] You can also specify the version with the package name using = or : as separator. php composer.phar create-project vendor/project:version target-directory To install unstable packages, either specify the version you want, or use the --stability=dev (where dev can be one of RC, beta, alpha or dev). To setup a developer workable version you should create the project using the source controlled code by appending the '--prefer-source' flag. To install a package from another repository than the default one you can pass the '--repository=https://myrepository.org' flag. Read more at https://getcomposer.org/doc/03-cli.md#create-project EOT ) ; } /** * @return int */ protected function execute(InputInterface $input, OutputInterface $output) { $config = Factory::createConfig(); $io = $this->getIO(); list($preferSource, $preferDist) = $this->getPreferredInstallOptions($config, $input, true); if ($input->getOption('dev')) { $io->writeError('You are using the deprecated option "dev". Dev packages are installed by default now.'); } if ($input->getOption('no-custom-installers')) { $io->writeError('You are using the deprecated option "no-custom-installers". Use "no-plugins" instead.'); $input->setOption('no-plugins', true); } if ($input->isInteractive() && $input->getOption('ask')) { $parts = explode("/", strtolower($input->getArgument('package')), 2); $input->setArgument('directory', $io->ask('New project directory ['.array_pop($parts).']: ')); } $ignorePlatformReqs = $input->getOption('ignore-platform-reqs') ?: ($input->getOption('ignore-platform-req') ?: false); return $this->installProject( $io, $config, $input, $input->getArgument('package'), $input->getArgument('directory'), $input->getArgument('version'), $input->getOption('stability'), $preferSource, $preferDist, !$input->getOption('no-dev'), $input->getOption('repository') ?: $input->getOption('repository-url'), $input->getOption('no-plugins'), $input->getOption('no-scripts'), $input->getOption('no-progress'), $input->getOption('no-install'), PlatformRequirementFilterFactory::fromBoolOrList($ignorePlatformReqs), !$input->getOption('no-secure-http'), $input->getOption('add-repository') ); } /** * @param string|null $packageName * @param string|null $directory * @param string|null $packageVersion * @param string $stability * @param bool $preferSource * @param bool $preferDist * @param bool $installDevPackages * @param string|array|null $repositories * @param bool $disablePlugins * @param bool $disableScripts * @param bool $noProgress * @param bool $noInstall * @param bool $secureHttp * @param bool $addRepository * * @return int * @throws \Exception */ public function installProject(IOInterface $io, Config $config, InputInterface $input, $packageName = null, $directory = null, $packageVersion = null, $stability = 'stable', $preferSource = false, $preferDist = false, $installDevPackages = false, $repositories = null, $disablePlugins = false, $disableScripts = false, $noProgress = false, $noInstall = false, PlatformRequirementFilterInterface $platformRequirementFilter = null, $secureHttp = true, $addRepository = false) { $oldCwd = getcwd(); if ($repositories !== null && !is_array($repositories)) { $repositories = (array) $repositories; } $platformRequirementFilter = $platformRequirementFilter ?: PlatformRequirementFilterFactory::ignoreNothing(); // we need to manually load the configuration to pass the auth credentials to the io interface! $io->loadConfiguration($config); $this->suggestedPackagesReporter = new SuggestedPackagesReporter($io); if ($packageName !== null) { $installedFromVcs = $this->installRootPackage($io, $config, $packageName, $platformRequirementFilter, $directory, $packageVersion, $stability, $preferSource, $preferDist, $installDevPackages, $repositories, $disablePlugins, $disableScripts, $noProgress, $secureHttp); } else { $installedFromVcs = false; } if ($repositories !== null && $addRepository && is_file('composer.lock')) { unlink('composer.lock'); } $composer = Factory::create($io, null, $disablePlugins, $disableScripts); // add the repository to the composer.json and use it for the install run later if ($repositories !== null && $addRepository) { foreach ($repositories as $index => $repo) { $repoConfig = RepositoryFactory::configFromString($io, $composer->getConfig(), $repo, true); $composerJsonRepositoriesConfig = $composer->getConfig()->getRepositories(); $name = RepositoryFactory::generateRepositoryName($index, $repoConfig, $composerJsonRepositoriesConfig); $configSource = new JsonConfigSource(new JsonFile('composer.json')); if ( (isset($repoConfig['packagist']) && $repoConfig === array('packagist' => false)) || (isset($repoConfig['packagist.org']) && $repoConfig === array('packagist.org' => false)) ) { $configSource->addRepository('packagist.org', false); } else { $configSource->addRepository($name, $repoConfig, false); } $composer = Factory::create($io, null, $disablePlugins); } } $process = new ProcessExecutor($io); $fs = new Filesystem($process); // dispatch event $composer->getEventDispatcher()->dispatchScript(ScriptEvents::POST_ROOT_PACKAGE_INSTALL, $installDevPackages); // use the new config including the newly installed project $config = $composer->getConfig(); list($preferSource, $preferDist) = $this->getPreferredInstallOptions($config, $input); // install dependencies of the created project if ($noInstall === false) { $composer->getInstallationManager()->setOutputProgress(!$noProgress); $installer = Installer::create($io, $composer); $installer->setPreferSource($preferSource) ->setPreferDist($preferDist) ->setDevMode($installDevPackages) ->setPlatformRequirementFilter($platformRequirementFilter) ->setSuggestedPackagesReporter($this->suggestedPackagesReporter) ->setOptimizeAutoloader($config->get('optimize-autoloader')) ->setClassMapAuthoritative($config->get('classmap-authoritative')) ->setApcuAutoloader($config->get('apcu-autoloader')); if (!$composer->getLocker()->isLocked()) { $installer->setUpdate(true); } if ($disablePlugins) { $installer->disablePlugins(); } $status = $installer->run(); if (0 !== $status) { return $status; } } $hasVcs = $installedFromVcs; if ( !$input->getOption('keep-vcs') && $installedFromVcs && ( $input->getOption('remove-vcs') || !$io->isInteractive() || $io->askConfirmation('Do you want to remove the existing VCS (.git, .svn..) history? [Y,n]? ') ) ) { $finder = new Finder(); $finder->depth(0)->directories()->in(getcwd())->ignoreVCS(false)->ignoreDotFiles(false); foreach (array('.svn', '_svn', 'CVS', '_darcs', '.arch-params', '.monotone', '.bzr', '.git', '.hg', '.fslckout', '_FOSSIL_') as $vcsName) { $finder->name($vcsName); } try { $dirs = iterator_to_array($finder); unset($finder); foreach ($dirs as $dir) { if (!$fs->removeDirectory($dir)) { throw new \RuntimeException('Could not remove '.$dir); } } } catch (\Exception $e) { $io->writeError('An error occurred while removing the VCS metadata: '.$e->getMessage().''); } $hasVcs = false; } // rewriting self.version dependencies with explicit version numbers if the package's vcs metadata is gone if (!$hasVcs) { $package = $composer->getPackage(); $configSource = new JsonConfigSource(new JsonFile('composer.json')); foreach (BasePackage::$supportedLinkTypes as $type => $meta) { foreach ($package->{'get'.$meta['method']}() as $link) { if ($link->getPrettyConstraint() === 'self.version') { $configSource->addLink($type, $link->getTarget(), $package->getPrettyVersion()); } } } } // dispatch event $composer->getEventDispatcher()->dispatchScript(ScriptEvents::POST_CREATE_PROJECT_CMD, $installDevPackages); chdir($oldCwd); $vendorComposerDir = $config->get('vendor-dir').'/composer'; if (is_dir($vendorComposerDir) && $fs->isDirEmpty($vendorComposerDir)) { Silencer::call('rmdir', $vendorComposerDir); $vendorDir = $config->get('vendor-dir'); if (is_dir($vendorDir) && $fs->isDirEmpty($vendorDir)) { Silencer::call('rmdir', $vendorDir); } } return 0; } /** * @param string $packageName * @param string|null $directory * @param string|null $packageVersion * @param string|null $stability * @param bool $preferSource * @param bool $preferDist * @param bool $installDevPackages * @param array|null $repositories * @param bool $disablePlugins * @param bool $disableScripts * @param bool $noProgress * @param bool $secureHttp * * @return bool * @throws \Exception */ protected function installRootPackage(IOInterface $io, Config $config, $packageName, PlatformRequirementFilterInterface $platformRequirementFilter, $directory = null, $packageVersion = null, $stability = 'stable', $preferSource = false, $preferDist = false, $installDevPackages = false, array $repositories = null, $disablePlugins = false, $disableScripts = false, $noProgress = false, $secureHttp = true) { if (!$secureHttp) { $config->merge(array('config' => array('secure-http' => false)), Config::SOURCE_COMMAND); } $parser = new VersionParser(); $requirements = $parser->parseNameVersionPairs(array($packageName)); $name = strtolower($requirements[0]['name']); if (!$packageVersion && isset($requirements[0]['version'])) { $packageVersion = $requirements[0]['version']; } // if no directory was specified, use the 2nd part of the package name if (null === $directory) { $parts = explode("/", $name, 2); $directory = getcwd() . DIRECTORY_SEPARATOR . array_pop($parts); } $process = new ProcessExecutor($io); $fs = new Filesystem($process); if (!$fs->isAbsolutePath($directory)) { $directory = getcwd() . DIRECTORY_SEPARATOR . $directory; } $io->writeError('Creating a "' . $packageName . '" project at "' . $fs->findShortestPath(getcwd(), $directory, true) . '"'); if (file_exists($directory)) { if (!is_dir($directory)) { throw new \InvalidArgumentException('Cannot create project directory at "'.$directory.'", it exists as a file.'); } if (!$fs->isDirEmpty($directory)) { throw new \InvalidArgumentException('Project directory "'.$directory.'" is not empty.'); } } if (null === $stability) { if (null === $packageVersion) { $stability = 'stable'; } elseif (Preg::isMatch('{^[^,\s]*?@('.implode('|', array_keys(BasePackage::$stabilities)).')$}i', $packageVersion, $match)) { $stability = $match[1]; } else { $stability = VersionParser::parseStability($packageVersion); } } $stability = VersionParser::normalizeStability($stability); if (!isset(BasePackage::$stabilities[$stability])) { throw new \InvalidArgumentException('Invalid stability provided ('.$stability.'), must be one of: '.implode(', ', array_keys(BasePackage::$stabilities))); } $composer = Factory::create($io, $config->all(), $disablePlugins); $config = $composer->getConfig(); $rm = $composer->getRepositoryManager(); $repositorySet = new RepositorySet($stability); if (null === $repositories) { $repositorySet->addRepository(new CompositeRepository(RepositoryFactory::defaultRepos($io, $config, $rm))); } else { foreach ($repositories as $repo) { $repoConfig = RepositoryFactory::configFromString($io, $config, $repo, true); if ( (isset($repoConfig['packagist']) && $repoConfig === array('packagist' => false)) || (isset($repoConfig['packagist.org']) && $repoConfig === array('packagist.org' => false)) ) { continue; } $repositorySet->addRepository(RepositoryFactory::createRepo($io, $config, $repoConfig, $rm)); } } $platformOverrides = $config->get('platform') ?: array(); $platformRepo = new PlatformRepository(array(), $platformOverrides); // find the latest version if there are multiple $versionSelector = new VersionSelector($repositorySet, $platformRepo); $package = $versionSelector->findBestCandidate($name, $packageVersion, $stability, $platformRequirementFilter); if (!$package) { $errorMessage = "Could not find package $name with " . ($packageVersion ? "version $packageVersion" : "stability $stability"); if (!($platformRequirementFilter instanceof IgnoreAllPlatformRequirementFilter) && $versionSelector->findBestCandidate($name, $packageVersion, $stability, PlatformRequirementFilterFactory::ignoreAll())) { throw new \InvalidArgumentException($errorMessage .' in a version installable using your PHP version, PHP extensions and Composer version.'); } throw new \InvalidArgumentException($errorMessage .'.'); } // handler Ctrl+C for unix-like systems if (function_exists('pcntl_async_signals') && function_exists('pcntl_signal')) { @mkdir($directory, 0777, true); if ($realDir = realpath($directory)) { pcntl_async_signals(true); pcntl_signal(SIGINT, function () use ($realDir) { $fs = new Filesystem(); $fs->removeDirectory($realDir); exit(130); }); } } // handler Ctrl+C for Windows on PHP 7.4+ if (function_exists('sapi_windows_set_ctrl_handler') && PHP_SAPI === 'cli') { @mkdir($directory, 0777, true); if ($realDir = realpath($directory)) { sapi_windows_set_ctrl_handler(function () use ($realDir) { $fs = new Filesystem(); $fs->removeDirectory($realDir); exit(130); }); } } // avoid displaying 9999999-dev as version if default-branch was selected if ($package instanceof AliasPackage && $package->getPrettyVersion() === VersionParser::DEFAULT_BRANCH_ALIAS) { $package = $package->getAliasOf(); } $io->writeError('Installing ' . $package->getName() . ' (' . $package->getFullPrettyVersion(false) . ')'); if ($disablePlugins) { $io->writeError('Plugins have been disabled.'); } if ($package instanceof AliasPackage) { $package = $package->getAliasOf(); } $dm = $composer->getDownloadManager(); $dm->setPreferSource($preferSource) ->setPreferDist($preferDist); $projectInstaller = new ProjectInstaller($directory, $dm, $fs); $im = $composer->getInstallationManager(); $im->setOutputProgress(!$noProgress); $im->addInstaller($projectInstaller); $im->execute(new InstalledArrayRepository(), array(new InstallOperation($package))); $im->notifyInstalls($io); // collect suggestions $this->suggestedPackagesReporter->addSuggestionsFromPackage($package); $installedFromVcs = 'source' === $package->getInstallationSource(); $io->writeError('Created project in ' . $directory . ''); chdir($directory); Platform::putEnv('COMPOSER_ROOT_VERSION', $package->getPrettyVersion()); return $installedFromVcs; } } * Jordi Boggiano * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Composer\Command; use Composer\Filter\PlatformRequirementFilter\PlatformRequirementFilterFactory; use Composer\Plugin\CommandEvent; use Composer\Plugin\PluginEvents; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; /** * @author Jordi Boggiano */ class DumpAutoloadCommand extends BaseCommand { /** * @return void */ protected function configure() { $this ->setName('dump-autoload') ->setAliases(array('dumpautoload')) ->setDescription('Dumps the autoloader.') ->setDefinition(array( new InputOption('optimize', 'o', InputOption::VALUE_NONE, 'Optimizes PSR0 and PSR4 packages to be loaded with classmaps too, good for production.'), new InputOption('classmap-authoritative', 'a', InputOption::VALUE_NONE, 'Autoload classes from the classmap only. Implicitly enables `--optimize`.'), new InputOption('apcu', null, InputOption::VALUE_NONE, 'Use APCu to cache found/not-found classes.'), new InputOption('apcu-prefix', null, InputOption::VALUE_REQUIRED, 'Use a custom prefix for the APCu autoloader cache. Implicitly enables --apcu'), new InputOption('dev', null, InputOption::VALUE_NONE, 'Enables autoload-dev rules. Composer will by default infer this automatically according to the last install or update --no-dev state.'), new InputOption('no-dev', null, InputOption::VALUE_NONE, 'Disables autoload-dev rules. Composer will by default infer this automatically according to the last install or update --no-dev state.'), new InputOption('ignore-platform-req', null, InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY, 'Ignore a specific platform requirement (php & ext- packages).'), new InputOption('ignore-platform-reqs', null, InputOption::VALUE_NONE, 'Ignore all platform requirements (php & ext- packages).'), )) ->setHelp( <<php composer.phar dump-autoload Read more at https://getcomposer.org/doc/03-cli.md#dump-autoload-dumpautoload- EOT ) ; } /** * @return int */ protected function execute(InputInterface $input, OutputInterface $output) { $composer = $this->getComposer(); $commandEvent = new CommandEvent(PluginEvents::COMMAND, 'dump-autoload', $input, $output); $composer->getEventDispatcher()->dispatch($commandEvent->getName(), $commandEvent); $installationManager = $composer->getInstallationManager(); $localRepo = $composer->getRepositoryManager()->getLocalRepository(); $package = $composer->getPackage(); $config = $composer->getConfig(); $optimize = $input->getOption('optimize') || $config->get('optimize-autoloader'); $authoritative = $input->getOption('classmap-authoritative') || $config->get('classmap-authoritative'); $apcuPrefix = $input->getOption('apcu-prefix'); $apcu = $apcuPrefix !== null || $input->getOption('apcu') || $config->get('apcu-autoloader'); if ($authoritative) { $this->getIO()->write('Generating optimized autoload files (authoritative)'); } elseif ($optimize) { $this->getIO()->write('Generating optimized autoload files'); } else { $this->getIO()->write('Generating autoload files'); } $ignorePlatformReqs = $input->getOption('ignore-platform-reqs') ?: ($input->getOption('ignore-platform-req') ?: false); $generator = $composer->getAutoloadGenerator(); if ($input->getOption('no-dev')) { $generator->setDevMode(false); } if ($input->getOption('dev')) { if ($input->getOption('no-dev')) { throw new \InvalidArgumentException('You can not use both --no-dev and --dev as they conflict with each other.'); } $generator->setDevMode(true); } $generator->setClassMapAuthoritative($authoritative); $generator->setRunScripts(true); $generator->setApcu($apcu, $apcuPrefix); $generator->setPlatformRequirementFilter(PlatformRequirementFilterFactory::fromBoolOrList($ignorePlatformReqs)); $numberOfClasses = $generator->dump($config, $localRepo, $package, $installationManager, 'composer', $optimize); if ($authoritative) { $this->getIO()->write('Generated optimized autoload files (authoritative) containing '. $numberOfClasses .' classes'); } elseif ($optimize) { $this->getIO()->write('Generated optimized autoload files containing '. $numberOfClasses .' classes'); } else { $this->getIO()->write('Generated autoload files'); } return 0; } } * Jordi Boggiano * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Composer\Command; use Composer\DependencyResolver\Operation\InstallOperation; use Composer\DependencyResolver\Operation\UninstallOperation; use Composer\DependencyResolver\Transaction; use Composer\Filter\PlatformRequirementFilter\PlatformRequirementFilterFactory; use Composer\Package\AliasPackage; use Composer\Package\BasePackage; use Composer\Pcre\Preg; use Composer\Plugin\CommandEvent; use Composer\Plugin\PluginEvents; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Output\OutputInterface; /** * @author Jordi Boggiano */ class ReinstallCommand extends BaseCommand { /** * @return void */ protected function configure() { $this ->setName('reinstall') ->setDescription('Uninstalls and reinstalls the given package names') ->setDefinition(array( new InputOption('prefer-source', null, InputOption::VALUE_NONE, 'Forces installation from package sources when possible, including VCS information.'), new InputOption('prefer-dist', null, InputOption::VALUE_NONE, 'Forces installation from package dist (default behavior).'), new InputOption('prefer-install', null, InputOption::VALUE_REQUIRED, 'Forces installation from package dist|source|auto (auto chooses source for dev versions, dist for the rest).'), new InputOption('no-autoloader', null, InputOption::VALUE_NONE, 'Skips autoloader generation'), new InputOption('no-progress', null, InputOption::VALUE_NONE, 'Do not output download progress.'), new InputOption('optimize-autoloader', 'o', InputOption::VALUE_NONE, 'Optimize autoloader during autoloader dump'), new InputOption('classmap-authoritative', 'a', InputOption::VALUE_NONE, 'Autoload classes from the classmap only. Implicitly enables `--optimize-autoloader`.'), new InputOption('apcu-autoloader', null, InputOption::VALUE_NONE, 'Use APCu to cache found/not-found classes.'), new InputOption('apcu-autoloader-prefix', null, InputOption::VALUE_REQUIRED, 'Use a custom prefix for the APCu autoloader cache. Implicitly enables --apcu-autoloader'), new InputOption('ignore-platform-req', null, InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY, 'Ignore a specific platform requirement (php & ext- packages).'), new InputOption('ignore-platform-reqs', null, InputOption::VALUE_NONE, 'Ignore all platform requirements (php & ext- packages).'), new InputArgument('packages', InputArgument::IS_ARRAY | InputArgument::REQUIRED, 'List of package names to reinstall, can include a wildcard (*) to match any substring.'), )) ->setHelp( <<reinstall command looks up installed packages by name, uninstalls them and reinstalls them. This lets you do a clean install of a package if you messed with its files, or if you wish to change the installation type using --prefer-install. php composer.phar reinstall acme/foo "acme/bar-*" Read more at https://getcomposer.org/doc/03-cli.md#reinstall EOT ) ; } protected function execute(InputInterface $input, OutputInterface $output) { $io = $this->getIO(); $composer = $this->getComposer(true, $input->getOption('no-plugins'), $input->getOption('no-scripts')); $localRepo = $composer->getRepositoryManager()->getLocalRepository(); $packagesToReinstall = array(); $packageNamesToReinstall = array(); foreach ($input->getArgument('packages') as $pattern) { $patternRegexp = BasePackage::packageNameToRegexp($pattern); $matched = false; foreach ($localRepo->getCanonicalPackages() as $package) { if (Preg::isMatch($patternRegexp, $package->getName())) { $matched = true; $packagesToReinstall[] = $package; $packageNamesToReinstall[] = $package->getName(); } } if (!$matched) { $io->writeError('Pattern "' . $pattern . '" does not match any currently installed packages.'); } } if (!$packagesToReinstall) { $io->writeError('Found no packages to reinstall, aborting.'); return 1; } $uninstallOperations = array(); foreach ($packagesToReinstall as $package) { $uninstallOperations[] = new UninstallOperation($package); } // make sure we have a list of install operations ordered by dependency/plugins $presentPackages = $localRepo->getPackages(); $resultPackages = $presentPackages; foreach ($presentPackages as $index => $package) { if (in_array($package->getName(), $packageNamesToReinstall, true)) { unset($presentPackages[$index]); } } $transaction = new Transaction($presentPackages, $resultPackages); $installOperations = $transaction->getOperations(); // reverse-sort the uninstalls based on the install order $installOrder = array(); foreach ($installOperations as $index => $op) { if ($op instanceof InstallOperation && !$op->getPackage() instanceof AliasPackage) { $installOrder[$op->getPackage()->getName()] = $index; } } usort($uninstallOperations, function ($a, $b) use ($installOrder) { return $installOrder[$b->getPackage()->getName()] - $installOrder[$a->getPackage()->getName()]; }); $commandEvent = new CommandEvent(PluginEvents::COMMAND, 'reinstall', $input, $output); $composer->getEventDispatcher()->dispatch($commandEvent->getName(), $commandEvent); $config = $composer->getConfig(); list($preferSource, $preferDist) = $this->getPreferredInstallOptions($config, $input); $installationManager = $composer->getInstallationManager(); $downloadManager = $composer->getDownloadManager(); $package = $composer->getPackage(); $ignorePlatformReqs = $input->getOption('ignore-platform-reqs') ?: ($input->getOption('ignore-platform-req') ?: false); $installationManager->setOutputProgress(!$input->getOption('no-progress')); if ($input->getOption('no-plugins')) { $installationManager->disablePlugins(); } $downloadManager->setPreferSource($preferSource); $downloadManager->setPreferDist($preferDist); $installationManager->execute($localRepo, $uninstallOperations, true); $installationManager->execute($localRepo, $installOperations, true); if (!$input->getOption('no-autoloader')) { $optimize = $input->getOption('optimize-autoloader') || $config->get('optimize-autoloader'); $authoritative = $input->getOption('classmap-authoritative') || $config->get('classmap-authoritative'); $apcuPrefix = $input->getOption('apcu-autoloader-prefix'); $apcu = $apcuPrefix !== null || $input->getOption('apcu-autoloader') || $config->get('apcu-autoloader'); $generator = $composer->getAutoloadGenerator(); $generator->setClassMapAuthoritative($authoritative); $generator->setApcu($apcu, $apcuPrefix); $generator->setPlatformRequirementFilter(PlatformRequirementFilterFactory::fromBoolOrList($ignorePlatformReqs)); $generator->dump($config, $localRepo, $package, $installationManager, 'composer', $optimize); } return 0; } } * Jordi Boggiano * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Composer\Command; use Composer\Cache; use Composer\Factory; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; /** * @author David Neilsen */ class ClearCacheCommand extends BaseCommand { /** * @return void */ protected function configure() { $this ->setName('clear-cache') ->setAliases(array('clearcache', 'cc')) ->setDescription('Clears composer\'s internal package cache.') ->setHelp( <<clear-cache deletes all cached packages from composer's cache directory. Read more at https://getcomposer.org/doc/03-cli.md#clear-cache-clearcache-cc EOT ) ; } /** * @return int */ protected function execute(InputInterface $input, OutputInterface $output) { $config = Factory::createConfig(); $io = $this->getIO(); $cachePaths = array( 'cache-vcs-dir' => $config->get('cache-vcs-dir'), 'cache-repo-dir' => $config->get('cache-repo-dir'), 'cache-files-dir' => $config->get('cache-files-dir'), 'cache-dir' => $config->get('cache-dir'), ); foreach ($cachePaths as $key => $cachePath) { $cachePath = realpath($cachePath); if (!$cachePath) { $io->writeError("Cache directory does not exist ($key): $cachePath"); continue; } $cache = new Cache($io, $cachePath); $cache->setReadOnly($config->get('cache-read-only')); if (!$cache->isEnabled()) { $io->writeError("Cache is not enabled ($key): $cachePath"); continue; } $io->writeError("Clearing cache ($key): $cachePath"); $cache->clear(); } $io->writeError('All caches cleared.'); return 0; } } * Jordi Boggiano * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Composer\Command; use Composer\Config\JsonConfigSource; use Composer\DependencyResolver\Request; use Composer\Filter\PlatformRequirementFilter\PlatformRequirementFilterFactory; use Composer\Installer; use Composer\Pcre\Preg; use Composer\Plugin\CommandEvent; use Composer\Plugin\PluginEvents; use Composer\Json\JsonFile; use Composer\Factory; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Output\OutputInterface; use Composer\Package\BasePackage; /** * @author Pierre du Plessis * @author Jordi Boggiano */ class RemoveCommand extends BaseCommand { /** * @return void */ protected function configure() { $this ->setName('remove') ->setDescription('Removes a package from the require or require-dev.') ->setDefinition(array( new InputArgument('packages', InputArgument::IS_ARRAY | InputArgument::REQUIRED, 'Packages that should be removed.'), new InputOption('dev', null, InputOption::VALUE_NONE, 'Removes a package from the require-dev section.'), new InputOption('dry-run', null, InputOption::VALUE_NONE, 'Outputs the operations but will not execute anything (implicitly enables --verbose).'), new InputOption('no-progress', null, InputOption::VALUE_NONE, 'Do not output download progress.'), new InputOption('no-update', null, InputOption::VALUE_NONE, 'Disables the automatic update of the dependencies (implies --no-install).'), new InputOption('no-install', null, InputOption::VALUE_NONE, 'Skip the install step after updating the composer.lock file.'), new InputOption('update-no-dev', null, InputOption::VALUE_NONE, 'Run the dependency update with the --no-dev option.'), new InputOption('update-with-dependencies', 'w', InputOption::VALUE_NONE, 'Allows inherited dependencies to be updated with explicit dependencies. (Deprecrated, is now default behavior)'), new InputOption('update-with-all-dependencies', 'W', InputOption::VALUE_NONE, 'Allows all inherited dependencies to be updated, including those that are root requirements.'), new InputOption('with-all-dependencies', null, InputOption::VALUE_NONE, 'Alias for --update-with-all-dependencies'), new InputOption('no-update-with-dependencies', null, InputOption::VALUE_NONE, 'Does not allow inherited dependencies to be updated with explicit dependencies.'), new InputOption('unused', null, InputOption::VALUE_NONE, 'Remove all packages which are locked but not required by any other package.'), new InputOption('ignore-platform-req', null, InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY, 'Ignore a specific platform requirement (php & ext- packages).'), new InputOption('ignore-platform-reqs', null, InputOption::VALUE_NONE, 'Ignore all platform requirements (php & ext- packages).'), new InputOption('optimize-autoloader', 'o', InputOption::VALUE_NONE, 'Optimize autoloader during autoloader dump'), new InputOption('classmap-authoritative', 'a', InputOption::VALUE_NONE, 'Autoload classes from the classmap only. Implicitly enables `--optimize-autoloader`.'), new InputOption('apcu-autoloader', null, InputOption::VALUE_NONE, 'Use APCu to cache found/not-found classes.'), new InputOption('apcu-autoloader-prefix', null, InputOption::VALUE_REQUIRED, 'Use a custom prefix for the APCu autoloader cache. Implicitly enables --apcu-autoloader'), )) ->setHelp( <<remove command removes a package from the current list of installed packages php composer.phar remove Read more at https://getcomposer.org/doc/03-cli.md#remove EOT ) ; } /** * @return void */ protected function interact(InputInterface $input, OutputInterface $output) { if ($input->getOption('unused')) { $composer = $this->getComposer(); $locker = $composer->getLocker(); if (!$locker->isLocked()) { throw new \UnexpectedValueException('A valid composer.lock file is required to run this command with --unused'); } $lockedPackages = $locker->getLockedRepository()->getPackages(); $required = array(); foreach (array_merge($composer->getPackage()->getRequires(), $composer->getPackage()->getDevRequires()) as $link) { $required[$link->getTarget()] = true; } do { $found = false; foreach ($lockedPackages as $index => $package) { foreach ($package->getNames() as $name) { if (isset($required[$name])) { foreach ($package->getRequires() as $link) { $required[$link->getTarget()] = true; } $found = true; unset($lockedPackages[$index]); break; } } } } while ($found); $unused = array(); foreach ($lockedPackages as $package) { $unused[] = $package->getName(); } $input->setArgument('packages', array_merge($input->getArgument('packages'), $unused)); if (!$input->getArgument('packages')) { $this->getIO()->writeError('No unused packages to remove'); $this->setCode(function () { return 0; }); } } } /** * @return int * @throws \Seld\JsonLint\ParsingException */ protected function execute(InputInterface $input, OutputInterface $output) { $packages = $input->getArgument('packages'); $packages = array_map('strtolower', $packages); $file = Factory::getComposerFile(); $jsonFile = new JsonFile($file); $composer = $jsonFile->read(); $composerBackup = file_get_contents($jsonFile->getPath()); $json = new JsonConfigSource($jsonFile); $type = $input->getOption('dev') ? 'require-dev' : 'require'; $altType = !$input->getOption('dev') ? 'require-dev' : 'require'; $io = $this->getIO(); if ($input->getOption('update-with-dependencies')) { $io->writeError('You are using the deprecated option "update-with-dependencies". This is now default behaviour. The --no-update-with-dependencies option can be used to remove a package without its dependencies.'); } // make sure name checks are done case insensitively foreach (array('require', 'require-dev') as $linkType) { if (isset($composer[$linkType])) { foreach ($composer[$linkType] as $name => $version) { $composer[$linkType][strtolower($name)] = $name; } } } $dryRun = $input->getOption('dry-run'); $toRemove = array(); foreach ($packages as $package) { if (isset($composer[$type][$package])) { if ($dryRun) { $toRemove[$type][] = $composer[$type][$package]; } else { $json->removeLink($type, $composer[$type][$package]); } } elseif (isset($composer[$altType][$package])) { $io->writeError('' . $composer[$altType][$package] . ' could not be found in ' . $type . ' but it is present in ' . $altType . ''); if ($io->isInteractive()) { if ($io->askConfirmation('Do you want to remove it from ' . $altType . ' [yes]? ')) { if ($dryRun) { $toRemove[$altType][] = $composer[$altType][$package]; } else { $json->removeLink($altType, $composer[$altType][$package]); } } } } elseif (isset($composer[$type]) && $matches = Preg::grep(BasePackage::packageNameToRegexp($package), array_keys($composer[$type]))) { foreach ($matches as $matchedPackage) { if ($dryRun) { $toRemove[$type][] = $matchedPackage; } else { $json->removeLink($type, $matchedPackage); } } } elseif (isset($composer[$altType]) && $matches = Preg::grep(BasePackage::packageNameToRegexp($package), array_keys($composer[$altType]))) { foreach ($matches as $matchedPackage) { $io->writeError('' . $matchedPackage . ' could not be found in ' . $type . ' but it is present in ' . $altType . ''); if ($io->isInteractive()) { if ($io->askConfirmation('Do you want to remove it from ' . $altType . ' [yes]? ')) { if ($dryRun) { $toRemove[$altType][] = $matchedPackage; } else { $json->removeLink($altType, $matchedPackage); } } } } } else { $io->writeError(''.$package.' is not required in your composer.json and has not been removed'); } } $io->writeError(''.$file.' has been updated'); if ($input->getOption('no-update')) { return 0; } if ($composer = $this->getComposer(false)) { $composer->getPluginManager()->deactivateInstalledPlugins(); } // Update packages $this->resetComposer(); $composer = $this->getComposer(true, $input->getOption('no-plugins'), $input->getOption('no-scripts')); if ($dryRun) { $rootPackage = $composer->getPackage(); $links = array( 'require' => $rootPackage->getRequires(), 'require-dev' => $rootPackage->getDevRequires(), ); foreach ($toRemove as $type => $names) { foreach ($names as $name) { unset($links[$type][$name]); } } $rootPackage->setRequires($links['require']); $rootPackage->setDevRequires($links['require-dev']); } $commandEvent = new CommandEvent(PluginEvents::COMMAND, 'remove', $input, $output); $composer->getEventDispatcher()->dispatch($commandEvent->getName(), $commandEvent); $composer->getInstallationManager()->setOutputProgress(!$input->getOption('no-progress')); $install = Installer::create($io, $composer); $updateDevMode = !$input->getOption('update-no-dev'); $optimize = $input->getOption('optimize-autoloader') || $composer->getConfig()->get('optimize-autoloader'); $authoritative = $input->getOption('classmap-authoritative') || $composer->getConfig()->get('classmap-authoritative'); $apcuPrefix = $input->getOption('apcu-autoloader-prefix'); $apcu = $apcuPrefix !== null || $input->getOption('apcu-autoloader') || $composer->getConfig()->get('apcu-autoloader'); $updateAllowTransitiveDependencies = Request::UPDATE_LISTED_WITH_TRANSITIVE_DEPS_NO_ROOT_REQUIRE; $flags = ''; if ($input->getOption('update-with-all-dependencies') || $input->getOption('with-all-dependencies')) { $updateAllowTransitiveDependencies = Request::UPDATE_LISTED_WITH_TRANSITIVE_DEPS; $flags .= ' --with-all-dependencies'; } elseif ($input->getOption('no-update-with-dependencies')) { $updateAllowTransitiveDependencies = Request::UPDATE_ONLY_LISTED; $flags .= ' --with-dependencies'; } $io->writeError('Running composer update '.implode(' ', $packages).$flags.''); $ignorePlatformReqs = $input->getOption('ignore-platform-reqs') ?: ($input->getOption('ignore-platform-req') ?: false); $install ->setVerbose($input->getOption('verbose')) ->setDevMode($updateDevMode) ->setOptimizeAutoloader($optimize) ->setClassMapAuthoritative($authoritative) ->setApcuAutoloader($apcu, $apcuPrefix) ->setUpdate(true) ->setInstall(!$input->getOption('no-install')) ->setUpdateAllowTransitiveDependencies($updateAllowTransitiveDependencies) ->setPlatformRequirementFilter(PlatformRequirementFilterFactory::fromBoolOrList($ignorePlatformReqs)) ->setDryRun($dryRun) ; // if no lock is present, we do not do a partial update as // this is not supported by the Installer if ($composer->getLocker()->isLocked()) { $install->setUpdateAllowList($packages); } $status = $install->run(); if ($status !== 0) { $io->writeError("\n".'Removal failed, reverting '.$file.' to its original content.'); file_put_contents($jsonFile->getPath(), $composerBackup); } if (!$dryRun) { foreach ($packages as $package) { if ($composer->getRepositoryManager()->getLocalRepository()->findPackages($package)) { $io->writeError('Removal failed, '.$package.' is still present, it may be required by another package. See `composer why '.$package.'`.'); return 2; } } } return $status; } } * Jordi Boggiano * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Composer\Command; use Composer\Filter\PlatformRequirementFilter\PlatformRequirementFilterFactory; use Composer\Installer; use Composer\Plugin\CommandEvent; use Composer\Plugin\PluginEvents; use Composer\Util\HttpDownloader; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Output\OutputInterface; /** * @author Jordi Boggiano * @author Ryan Weaver * @author Konstantin Kudryashov * @author Nils Adermann */ class InstallCommand extends BaseCommand { /** * @return void */ protected function configure() { $this ->setName('install') ->setAliases(array('i')) ->setDescription('Installs the project dependencies from the composer.lock file if present, or falls back on the composer.json.') ->setDefinition(array( new InputOption('prefer-source', null, InputOption::VALUE_NONE, 'Forces installation from package sources when possible, including VCS information.'), new InputOption('prefer-dist', null, InputOption::VALUE_NONE, 'Forces installation from package dist (default behavior).'), new InputOption('prefer-install', null, InputOption::VALUE_REQUIRED, 'Forces installation from package dist|source|auto (auto chooses source for dev versions, dist for the rest).'), new InputOption('dry-run', null, InputOption::VALUE_NONE, 'Outputs the operations but will not execute anything (implicitly enables --verbose).'), new InputOption('dev', null, InputOption::VALUE_NONE, 'DEPRECATED: Enables installation of require-dev packages (enabled by default, only present for BC).'), new InputOption('no-suggest', null, InputOption::VALUE_NONE, 'DEPRECATED: This flag does not exist anymore.'), new InputOption('no-dev', null, InputOption::VALUE_NONE, 'Disables installation of require-dev packages.'), new InputOption('no-autoloader', null, InputOption::VALUE_NONE, 'Skips autoloader generation'), new InputOption('no-progress', null, InputOption::VALUE_NONE, 'Do not output download progress.'), new InputOption('no-install', null, InputOption::VALUE_NONE, 'Do not use, only defined here to catch misuse of the install command.'), new InputOption('verbose', 'v|vv|vvv', InputOption::VALUE_NONE, 'Shows more details including new commits pulled in when updating packages.'), new InputOption('optimize-autoloader', 'o', InputOption::VALUE_NONE, 'Optimize autoloader during autoloader dump'), new InputOption('classmap-authoritative', 'a', InputOption::VALUE_NONE, 'Autoload classes from the classmap only. Implicitly enables `--optimize-autoloader`.'), new InputOption('apcu-autoloader', null, InputOption::VALUE_NONE, 'Use APCu to cache found/not-found classes.'), new InputOption('apcu-autoloader-prefix', null, InputOption::VALUE_REQUIRED, 'Use a custom prefix for the APCu autoloader cache. Implicitly enables --apcu-autoloader'), new InputOption('ignore-platform-req', null, InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY, 'Ignore a specific platform requirement (php & ext- packages).'), new InputOption('ignore-platform-reqs', null, InputOption::VALUE_NONE, 'Ignore all platform requirements (php & ext- packages).'), new InputArgument('packages', InputArgument::IS_ARRAY | InputArgument::OPTIONAL, 'Should not be provided, use composer require instead to add a given package to composer.json.'), )) ->setHelp( <<install command reads the composer.lock file from the current directory, processes it, and downloads and installs all the libraries and dependencies outlined in that file. If the file does not exist it will look for composer.json and do the same. php composer.phar install Read more at https://getcomposer.org/doc/03-cli.md#install-i EOT ) ; } protected function execute(InputInterface $input, OutputInterface $output) { $io = $this->getIO(); if ($input->getOption('dev')) { $io->writeError('You are using the deprecated option "--dev". It has no effect and will break in Composer 3.'); } if ($input->getOption('no-suggest')) { $io->writeError('You are using the deprecated option "--no-suggest". It has no effect and will break in Composer 3.'); } if ($args = $input->getArgument('packages')) { $io->writeError('Invalid argument '.implode(' ', $args).'. Use "composer require '.implode(' ', $args).'" instead to add packages to your composer.json.'); return 1; } if ($input->getOption('no-install')) { $io->writeError('Invalid option "--no-install". Use "composer update --no-install" instead if you are trying to update the composer.lock file.'); return 1; } $composer = $this->getComposer(true, $input->getOption('no-plugins'), $input->getOption('no-scripts')); if ((!$composer->getLocker() || !$composer->getLocker()->isLocked()) && !HttpDownloader::isCurlEnabled()) { $io->writeError('Composer is operating significantly slower than normal because you do not have the PHP curl extension enabled.'); } $commandEvent = new CommandEvent(PluginEvents::COMMAND, 'install', $input, $output); $composer->getEventDispatcher()->dispatch($commandEvent->getName(), $commandEvent); $install = Installer::create($io, $composer); $config = $composer->getConfig(); list($preferSource, $preferDist) = $this->getPreferredInstallOptions($config, $input); $optimize = $input->getOption('optimize-autoloader') || $config->get('optimize-autoloader'); $authoritative = $input->getOption('classmap-authoritative') || $config->get('classmap-authoritative'); $apcuPrefix = $input->getOption('apcu-autoloader-prefix'); $apcu = $apcuPrefix !== null || $input->getOption('apcu-autoloader') || $config->get('apcu-autoloader'); $ignorePlatformReqs = $input->getOption('ignore-platform-reqs') ?: ($input->getOption('ignore-platform-req') ?: false); $composer->getInstallationManager()->setOutputProgress(!$input->getOption('no-progress')); $install ->setDryRun($input->getOption('dry-run')) ->setVerbose($input->getOption('verbose')) ->setPreferSource($preferSource) ->setPreferDist($preferDist) ->setDevMode(!$input->getOption('no-dev')) ->setDumpAutoloader(!$input->getOption('no-autoloader')) ->setOptimizeAutoloader($optimize) ->setClassMapAuthoritative($authoritative) ->setApcuAutoloader($apcu, $apcuPrefix) ->setPlatformRequirementFilter(PlatformRequirementFilterFactory::fromBoolOrList($ignorePlatformReqs)) ; if ($input->getOption('no-plugins')) { $install->disablePlugins(); } return $install->run(); } } * Jordi Boggiano * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Composer\Command; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\ArrayInput; use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; /** * @author Jordi Boggiano */ class OutdatedCommand extends ShowCommand { /** * @return void */ protected function configure() { $this ->setName('outdated') ->setDescription('Shows a list of installed packages that have updates available, including their latest version.') ->setDefinition(array( new InputArgument('package', InputArgument::OPTIONAL, 'Package to inspect. Or a name including a wildcard (*) to filter lists of packages instead.'), new InputOption('outdated', 'o', InputOption::VALUE_NONE, 'Show only packages that are outdated (this is the default, but present here for compat with `show`'), new InputOption('all', 'a', InputOption::VALUE_NONE, 'Show all installed packages with their latest versions'), new InputOption('locked', null, InputOption::VALUE_NONE, 'Shows updates for packages from the lock file, regardless of what is currently in vendor dir'), new InputOption('direct', 'D', InputOption::VALUE_NONE, 'Shows only packages that are directly required by the root package'), new InputOption('strict', null, InputOption::VALUE_NONE, 'Return a non-zero exit code when there are outdated packages'), new InputOption('minor-only', 'm', InputOption::VALUE_NONE, 'Show only packages that have minor SemVer-compatible updates. Use with the --outdated option.'), new InputOption('format', 'f', InputOption::VALUE_REQUIRED, 'Format of the output: text or json', 'text'), new InputOption('ignore', null, InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY, 'Ignore specified package(s). Use it with the --outdated option if you don\'t want to be informed about new versions of some packages.'), new InputOption('no-dev', null, InputOption::VALUE_NONE, 'Disables search in require-dev packages.'), new InputOption('ignore-platform-req', null, InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY, 'Ignore a specific platform requirement (php & ext- packages). Use with the --outdated option'), new InputOption('ignore-platform-reqs', null, InputOption::VALUE_NONE, 'Ignore all platform requirements (php & ext- packages). Use with the --outdated option'), )) ->setHelp( <<green (=): Dependency is in the latest version and is up to date. - yellow (~): Dependency has a new version available that includes backwards compatibility breaks according to semver, so upgrade when you can but it may involve work. - red (!): Dependency has a new version that is semver-compatible and you should upgrade it. Read more at https://getcomposer.org/doc/03-cli.md#outdated EOT ) ; } protected function execute(InputInterface $input, OutputInterface $output) { $args = array( 'command' => 'show', '--latest' => true, ); if (!$input->getOption('all')) { $args['--outdated'] = true; } if ($input->getOption('direct')) { $args['--direct'] = true; } if ($input->getArgument('package')) { $args['package'] = $input->getArgument('package'); } if ($input->getOption('strict')) { $args['--strict'] = true; } if ($input->getOption('minor-only')) { $args['--minor-only'] = true; } if ($input->getOption('locked')) { $args['--locked'] = true; } if ($input->getOption('no-dev')) { $args['--no-dev'] = true; } if ($input->getOption('ignore-platform-req')) { $args['--ignore-platform-req'] = $input->getOption('ignore-platform-req'); } if ($input->getOption('ignore-platform-reqs')) { $args['--ignore-platform-reqs'] = true; } $args['--format'] = $input->getOption('format'); $args['--ignore'] = $input->getOption('ignore'); $input = new ArrayInput($args); return $this->getApplication()->run($input, $output); } /** * @inheritDoc */ public function isProxyCommand() { return true; } } * Jordi Boggiano * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Composer\Command; use Composer\Composer; use Composer\Factory; use Composer\Config; use Composer\Downloader\TransportException; use Composer\Pcre\Preg; use Composer\Repository\PlatformRepository; use Composer\Plugin\CommandEvent; use Composer\Plugin\PluginEvents; use Composer\Util\ConfigValidator; use Composer\Util\IniHelper; use Composer\Util\ProcessExecutor; use Composer\Util\HttpDownloader; use Composer\Util\StreamContextFactory; use Composer\Util\Platform; use Composer\SelfUpdate\Keys; use Composer\SelfUpdate\Versions; use Composer\IO\NullIO; use Composer\Package\CompletePackageInterface; use Composer\XdebugHandler\XdebugHandler; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Process\ExecutableFinder; /** * @author Jordi Boggiano */ class DiagnoseCommand extends BaseCommand { /** @var HttpDownloader */ protected $httpDownloader; /** @var ProcessExecutor */ protected $process; /** @var int */ protected $exitCode = 0; /** * @return void */ protected function configure() { $this ->setName('diagnose') ->setDescription('Diagnoses the system to identify common errors.') ->setHelp( <<diagnose command checks common errors to help debugging problems. The process exit code will be 1 in case of warnings and 2 for errors. Read more at https://getcomposer.org/doc/03-cli.md#diagnose EOT ) ; } /** * @return int */ protected function execute(InputInterface $input, OutputInterface $output) { $composer = $this->getComposer(false); $io = $this->getIO(); if ($composer) { $commandEvent = new CommandEvent(PluginEvents::COMMAND, 'diagnose', $input, $output); $composer->getEventDispatcher()->dispatch($commandEvent->getName(), $commandEvent); $io->write('Checking composer.json: ', false); $this->outputResult($this->checkComposerSchema()); } if ($composer) { $config = $composer->getConfig(); } else { $config = Factory::createConfig(); } $config->merge(array('config' => array('secure-http' => false)), Config::SOURCE_COMMAND); $config->prohibitUrlByConfig('http://repo.packagist.org', new NullIO); $this->httpDownloader = Factory::createHttpDownloader($io, $config); $this->process = new ProcessExecutor($io); $io->write('Checking platform settings: ', false); $this->outputResult($this->checkPlatform()); $io->write('Checking git settings: ', false); $this->outputResult($this->checkGit()); $io->write('Checking http connectivity to packagist: ', false); $this->outputResult($this->checkHttp('http', $config)); $io->write('Checking https connectivity to packagist: ', false); $this->outputResult($this->checkHttp('https', $config)); $opts = stream_context_get_options(StreamContextFactory::getContext('http://example.org')); if (!empty($opts['http']['proxy'])) { $io->write('Checking HTTP proxy: ', false); $this->outputResult($this->checkHttpProxy()); } if ($oauth = $config->get('github-oauth')) { foreach ($oauth as $domain => $token) { $io->write('Checking '.$domain.' oauth access: ', false); $this->outputResult($this->checkGithubOauth($domain, $token)); } } else { $io->write('Checking github.com rate limit: ', false); try { $rate = $this->getGithubRateLimit('github.com'); if (!is_array($rate)) { $this->outputResult($rate); } elseif (10 > $rate['remaining']) { $io->write('WARNING'); $io->write(sprintf( 'Github has a rate limit on their API. ' . 'You currently have %u ' . 'out of %u requests left.' . PHP_EOL . 'See https://developer.github.com/v3/#rate-limiting and also' . PHP_EOL . ' https://getcomposer.org/doc/articles/troubleshooting.md#api-rate-limit-and-oauth-tokens', $rate['remaining'], $rate['limit'] )); } else { $this->outputResult(true); } } catch (\Exception $e) { if ($e instanceof TransportException && $e->getCode() === 401) { $this->outputResult('The oauth token for github.com seems invalid, run "composer config --global --unset github-oauth.github.com" to remove it'); } else { $this->outputResult($e); } } } $io->write('Checking disk free space: ', false); $this->outputResult($this->checkDiskSpace($config)); if (strpos(__FILE__, 'phar:') === 0) { $io->write('Checking pubkeys: ', false); $this->outputResult($this->checkPubKeys($config)); $io->write('Checking composer version: ', false); $this->outputResult($this->checkVersion($config)); } $io->write(sprintf('Composer version: %s', Composer::getVersion())); $platformOverrides = $config->get('platform') ?: array(); $platformRepo = new PlatformRepository(array(), $platformOverrides); $phpPkg = $platformRepo->findPackage('php', '*'); $phpVersion = $phpPkg->getPrettyVersion(); if ($phpPkg instanceof CompletePackageInterface && false !== strpos($phpPkg->getDescription(), 'overridden')) { $phpVersion .= ' - ' . $phpPkg->getDescription(); } $io->write(sprintf('PHP version: %s', $phpVersion)); if (defined('PHP_BINARY')) { $io->write(sprintf('PHP binary path: %s', PHP_BINARY)); } $io->write('OpenSSL version: ' . (defined('OPENSSL_VERSION_TEXT') ? ''.OPENSSL_VERSION_TEXT.'' : 'missing')); $io->write('cURL version: ' . $this->getCurlVersion()); $finder = new ExecutableFinder; $hasSystemUnzip = (bool) $finder->find('unzip'); $bin7zip = ''; if ($hasSystem7zip = (bool) $finder->find('7z', null, array('C:\Program Files\7-Zip'))) { $bin7zip = '7z'; } if (!Platform::isWindows() && !$hasSystem7zip && $hasSystem7zip = (bool) $finder->find('7zz')) { $bin7zip = '7zz'; } $io->write( 'zip: ' . (extension_loaded('zip') ? 'extension present' : 'extension not loaded') . ', ' . ($hasSystemUnzip ? 'unzip present' : 'unzip not available') . ', ' . ($hasSystem7zip ? '7-Zip present ('.$bin7zip.')' : '7-Zip not available') . (($hasSystem7zip || $hasSystemUnzip) && !function_exists('proc_open') ? ', proc_open is disabled or not present, unzip/7-z will not be usable' : '') ); return $this->exitCode; } /** * @return string|true */ private function checkComposerSchema() { $validator = new ConfigValidator($this->getIO()); list($errors, , $warnings) = $validator->validate(Factory::getComposerFile()); if ($errors || $warnings) { $messages = array( 'error' => $errors, 'warning' => $warnings, ); $output = ''; foreach ($messages as $style => $msgs) { foreach ($msgs as $msg) { $output .= '<' . $style . '>' . $msg . '' . PHP_EOL; } } return rtrim($output); } return true; } /** * @return string|true */ private function checkGit() { if (!function_exists('proc_open')) { return 'proc_open is not available, git cannot be used'; } $this->process->execute('git config color.ui', $output); if (strtolower(trim($output)) === 'always') { return 'Your git color.ui setting is set to always, this is known to create issues. Use "git config --global color.ui true" to set it correctly.'; } return true; } /** * @param string $proto * * @return string|string[]|true */ private function checkHttp($proto, Config $config) { $result = $this->checkConnectivity(); if ($result !== true) { return $result; } $result = array(); if ($proto === 'https' && $config->get('disable-tls') === true) { $tlsWarning = 'Composer is configured to disable SSL/TLS protection. This will leave remote HTTPS requests vulnerable to Man-In-The-Middle attacks.'; } try { $this->httpDownloader->get($proto . '://repo.packagist.org/packages.json'); } catch (TransportException $e) { if ($hints = HttpDownloader::getExceptionHints($e)) { foreach ($hints as $hint) { $result[] = $hint; } } $result[] = '[' . get_class($e) . '] ' . $e->getMessage() . ''; } if (isset($tlsWarning)) { $result[] = $tlsWarning; } if (count($result) > 0) { return $result; } return true; } /** * @return string|true|\Exception */ private function checkHttpProxy() { $result = $this->checkConnectivity(); if ($result !== true) { return $result; } $protocol = extension_loaded('openssl') ? 'https' : 'http'; try { $json = $this->httpDownloader->get($protocol . '://repo.packagist.org/packages.json')->decodeJson(); $hash = reset($json['provider-includes']); $hash = $hash['sha256']; $path = str_replace('%hash%', $hash, key($json['provider-includes'])); $provider = $this->httpDownloader->get($protocol . '://repo.packagist.org/'.$path)->getBody(); if (hash('sha256', $provider) !== $hash) { return 'It seems that your proxy is modifying http traffic on the fly'; } } catch (\Exception $e) { return $e; } return true; } /** * @param string $domain * @param string $token * * @return string|true|\Exception */ private function checkGithubOauth($domain, $token) { $result = $this->checkConnectivity(); if ($result !== true) { return $result; } $this->getIO()->setAuthentication($domain, $token, 'x-oauth-basic'); try { $url = $domain === 'github.com' ? 'https://api.'.$domain.'/' : 'https://'.$domain.'/api/v3/'; $this->httpDownloader->get($url, array( 'retry-auth-failure' => false, )); return true; } catch (\Exception $e) { if ($e instanceof TransportException && $e->getCode() === 401) { return 'The oauth token for '.$domain.' seems invalid, run "composer config --global --unset github-oauth.'.$domain.'" to remove it'; } return $e; } } /** * @param string $domain * @param string $token * @throws TransportException * @return mixed|string */ private function getGithubRateLimit($domain, $token = null) { $result = $this->checkConnectivity(); if ($result !== true) { return $result; } if ($token) { $this->getIO()->setAuthentication($domain, $token, 'x-oauth-basic'); } $url = $domain === 'github.com' ? 'https://api.'.$domain.'/rate_limit' : 'https://'.$domain.'/api/rate_limit'; $data = $this->httpDownloader->get($url, array('retry-auth-failure' => false))->decodeJson(); return $data['resources']['core']; } /** * @return string|true */ private function checkDiskSpace(Config $config) { $minSpaceFree = 1024 * 1024; if ((($df = @disk_free_space($dir = $config->get('home'))) !== false && $df < $minSpaceFree) || (($df = @disk_free_space($dir = $config->get('vendor-dir'))) !== false && $df < $minSpaceFree) ) { return 'The disk hosting '.$dir.' is full'; } return true; } /** * @return string[]|true */ private function checkPubKeys(Config $config) { $home = $config->get('home'); $errors = array(); $io = $this->getIO(); if (file_exists($home.'/keys.tags.pub') && file_exists($home.'/keys.dev.pub')) { $io->write(''); } if (file_exists($home.'/keys.tags.pub')) { $io->write('Tags Public Key Fingerprint: ' . Keys::fingerprint($home.'/keys.tags.pub')); } else { $errors[] = 'Missing pubkey for tags verification'; } if (file_exists($home.'/keys.dev.pub')) { $io->write('Dev Public Key Fingerprint: ' . Keys::fingerprint($home.'/keys.dev.pub')); } else { $errors[] = 'Missing pubkey for dev verification'; } if ($errors) { $errors[] = 'Run composer self-update --update-keys to set them up'; } return $errors ?: true; } /** * @return string|\Exception|true */ private function checkVersion(Config $config) { $result = $this->checkConnectivity(); if ($result !== true) { return $result; } $versionsUtil = new Versions($config, $this->httpDownloader); try { $latest = $versionsUtil->getLatest(); } catch (\Exception $e) { return $e; } if (Composer::VERSION !== $latest['version'] && Composer::VERSION !== '@package_version@') { return 'You are not running the latest '.$versionsUtil->getChannel().' version, run `composer self-update` to update ('.Composer::VERSION.' => '.$latest['version'].')'; } return true; } /** * @return string */ private function getCurlVersion() { if (extension_loaded('curl')) { if (!HttpDownloader::isCurlEnabled()) { return 'disabled via disable_functions, using php streams fallback, which reduces performance'; } $version = curl_version(); return ''.$version['version'].' '. 'libz '.(!empty($version['libz_version']) ? $version['libz_version'] : 'missing').' '. 'ssl '.(isset($version['ssl_version']) ? $version['ssl_version'] : 'missing').''; } return 'missing, using php streams fallback, which reduces performance'; } /** * @param bool|string|string[]|\Exception $result * * @return void */ private function outputResult($result) { $io = $this->getIO(); if (true === $result) { $io->write('OK'); return; } $hadError = false; $hadWarning = false; if ($result instanceof \Exception) { $result = '['.get_class($result).'] '.$result->getMessage().''; } if (!$result) { // falsey results should be considered as an error, even if there is nothing to output $hadError = true; } else { if (!is_array($result)) { $result = array($result); } foreach ($result as $message) { if (false !== strpos($message, '')) { $hadError = true; } elseif (false !== strpos($message, '')) { $hadWarning = true; } } } if ($hadError) { $io->write('FAIL'); $this->exitCode = max($this->exitCode, 2); } elseif ($hadWarning) { $io->write('WARNING'); $this->exitCode = max($this->exitCode, 1); } if ($result) { foreach ($result as $message) { $io->write($message); } } } /** * @return string|true */ private function checkPlatform() { $output = ''; $out = function ($msg, $style) use (&$output) { $output .= '<'.$style.'>'.$msg.''.PHP_EOL; }; // code below taken from getcomposer.org/installer, any changes should be made there and replicated here $errors = array(); $warnings = array(); $displayIniMessage = false; $iniMessage = PHP_EOL.PHP_EOL.IniHelper::getMessage(); $iniMessage .= PHP_EOL.'If you can not modify the ini file, you can also run `php -d option=value` to modify ini values on the fly. You can use -d multiple times.'; if (!function_exists('json_decode')) { $errors['json'] = true; } if (!extension_loaded('Phar')) { $errors['phar'] = true; } if (!extension_loaded('filter')) { $errors['filter'] = true; } if (!extension_loaded('hash')) { $errors['hash'] = true; } if (!extension_loaded('iconv') && !extension_loaded('mbstring')) { $errors['iconv_mbstring'] = true; } if (!filter_var(ini_get('allow_url_fopen'), FILTER_VALIDATE_BOOLEAN)) { $errors['allow_url_fopen'] = true; } if (extension_loaded('ionCube Loader') && ioncube_loader_iversion() < 40009) { $errors['ioncube'] = ioncube_loader_version(); } if (PHP_VERSION_ID < 50302) { $errors['php'] = PHP_VERSION; } if (!isset($errors['php']) && PHP_VERSION_ID < 50304) { $warnings['php'] = PHP_VERSION; } if (!extension_loaded('openssl')) { $errors['openssl'] = true; } if (extension_loaded('openssl') && OPENSSL_VERSION_NUMBER < 0x1000100f) { $warnings['openssl_version'] = true; } if (!defined('HHVM_VERSION') && !extension_loaded('apcu') && filter_var(ini_get('apc.enable_cli'), FILTER_VALIDATE_BOOLEAN)) { $warnings['apc_cli'] = true; } if (!extension_loaded('zlib')) { $warnings['zlib'] = true; } ob_start(); phpinfo(INFO_GENERAL); $phpinfo = ob_get_clean(); if (Preg::isMatch('{Configure Command(?: *
|$)}m', $phpinfo, $match)) { $configure = $match[1]; if (false !== strpos($configure, '--enable-sigchild')) { $warnings['sigchild'] = true; } if (false !== strpos($configure, '--with-curlwrappers')) { $warnings['curlwrappers'] = true; } } if (filter_var(ini_get('xdebug.profiler_enabled'), FILTER_VALIDATE_BOOLEAN)) { $warnings['xdebug_profile'] = true; } elseif (XdebugHandler::isXdebugActive()) { $warnings['xdebug_loaded'] = true; } if (defined('PHP_WINDOWS_VERSION_BUILD') && (version_compare(PHP_VERSION, '7.2.23', '<') || (version_compare(PHP_VERSION, '7.3.0', '>=') && version_compare(PHP_VERSION, '7.3.10', '<')))) { $warnings['onedrive'] = PHP_VERSION; } if (!empty($errors)) { foreach ($errors as $error => $current) { switch ($error) { case 'json': $text = PHP_EOL."The json extension is missing.".PHP_EOL; $text .= "Install it or recompile php without --disable-json"; break; case 'phar': $text = PHP_EOL."The phar extension is missing.".PHP_EOL; $text .= "Install it or recompile php without --disable-phar"; break; case 'filter': $text = PHP_EOL."The filter extension is missing.".PHP_EOL; $text .= "Install it or recompile php without --disable-filter"; break; case 'hash': $text = PHP_EOL."The hash extension is missing.".PHP_EOL; $text .= "Install it or recompile php without --disable-hash"; break; case 'iconv_mbstring': $text = PHP_EOL."The iconv OR mbstring extension is required and both are missing.".PHP_EOL; $text .= "Install either of them or recompile php without --disable-iconv"; break; case 'php': $text = PHP_EOL."Your PHP ({$current}) is too old, you must upgrade to PHP 5.3.2 or higher."; break; case 'allow_url_fopen': $text = PHP_EOL."The allow_url_fopen setting is incorrect.".PHP_EOL; $text .= "Add the following to the end of your `php.ini`:".PHP_EOL; $text .= " allow_url_fopen = On"; $displayIniMessage = true; break; case 'ioncube': $text = PHP_EOL."Your ionCube Loader extension ($current) is incompatible with Phar files.".PHP_EOL; $text .= "Upgrade to ionCube 4.0.9 or higher or remove this line (path may be different) from your `php.ini` to disable it:".PHP_EOL; $text .= " zend_extension = /usr/lib/php5/20090626+lfs/ioncube_loader_lin_5.3.so"; $displayIniMessage = true; break; case 'openssl': $text = PHP_EOL."The openssl extension is missing, which means that secure HTTPS transfers are impossible.".PHP_EOL; $text .= "If possible you should enable it or recompile php with --with-openssl"; break; default: throw new \InvalidArgumentException(sprintf("DiagnoseCommand: Unknown error type \"%s\". Please report at https://github.com/composer/composer/issues/new.", $error)); } $out($text, 'error'); } $output .= PHP_EOL; } if (!empty($warnings)) { foreach ($warnings as $warning => $current) { switch ($warning) { case 'apc_cli': $text = "The apc.enable_cli setting is incorrect.".PHP_EOL; $text .= "Add the following to the end of your `php.ini`:".PHP_EOL; $text .= " apc.enable_cli = Off"; $displayIniMessage = true; break; case 'zlib': $text = 'The zlib extension is not loaded, this can slow down Composer a lot.'.PHP_EOL; $text .= 'If possible, enable it or recompile php with --with-zlib'.PHP_EOL; $displayIniMessage = true; break; case 'sigchild': $text = "PHP was compiled with --enable-sigchild which can cause issues on some platforms.".PHP_EOL; $text .= "Recompile it without this flag if possible, see also:".PHP_EOL; $text .= " https://bugs.php.net/bug.php?id=22999"; break; case 'curlwrappers': $text = "PHP was compiled with --with-curlwrappers which will cause issues with HTTP authentication and GitHub.".PHP_EOL; $text .= " Recompile it without this flag if possible"; break; case 'php': $text = "Your PHP ({$current}) is quite old, upgrading to PHP 5.3.4 or higher is recommended.".PHP_EOL; $text .= " Composer works with 5.3.2+ for most people, but there might be edge case issues."; break; case 'openssl_version': // Attempt to parse version number out, fallback to whole string value. $opensslVersion = strstr(trim(strstr(OPENSSL_VERSION_TEXT, ' ')), ' ', true); $opensslVersion = $opensslVersion ?: OPENSSL_VERSION_TEXT; $text = "The OpenSSL library ({$opensslVersion}) used by PHP does not support TLSv1.2 or TLSv1.1.".PHP_EOL; $text .= "If possible you should upgrade OpenSSL to version 1.0.1 or above."; break; case 'xdebug_loaded': $text = "The xdebug extension is loaded, this can slow down Composer a little.".PHP_EOL; $text .= " Disabling it when using Composer is recommended."; break; case 'xdebug_profile': $text = "The xdebug.profiler_enabled setting is enabled, this can slow down Composer a lot.".PHP_EOL; $text .= "Add the following to the end of your `php.ini` to disable it:".PHP_EOL; $text .= " xdebug.profiler_enabled = 0"; $displayIniMessage = true; break; case 'onedrive': $text = "The Windows OneDrive folder is not supported on PHP versions below 7.2.23 and 7.3.10.".PHP_EOL; $text .= "Upgrade your PHP ({$current}) to use this location with Composer.".PHP_EOL; break; default: throw new \InvalidArgumentException(sprintf("DiagnoseCommand: Unknown warning type \"%s\". Please report at https://github.com/composer/composer/issues/new.", $warning)); } $out($text, 'comment'); } } if ($displayIniMessage) { $out($iniMessage, 'comment'); } return !$warnings && !$errors ? true : $output; } /** * Check if allow_url_fopen is ON * * @return string|true */ private function checkConnectivity() { if (!ini_get('allow_url_fopen')) { return 'Skipped because allow_url_fopen is missing.'; } return true; } } * Jordi Boggiano * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Composer\Command; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputOption; /** * @author Niels Keurentjes */ class ProhibitsCommand extends BaseDependencyCommand { /** * Configure command metadata. * * @return void */ protected function configure() { $this ->setName('prohibits') ->setAliases(array('why-not')) ->setDescription('Shows which packages prevent the given package from being installed.') ->setDefinition(array( new InputArgument(self::ARGUMENT_PACKAGE, InputArgument::REQUIRED, 'Package to inspect'), new InputArgument(self::ARGUMENT_CONSTRAINT, InputArgument::REQUIRED, 'Version constraint, which version you expected to be installed'), new InputOption(self::OPTION_RECURSIVE, 'r', InputOption::VALUE_NONE, 'Recursively resolves up to the root package'), new InputOption(self::OPTION_TREE, 't', InputOption::VALUE_NONE, 'Prints the results as a nested tree'), )) ->setHelp( <<php composer.phar prohibits composer/composer Read more at https://getcomposer.org/doc/03-cli.md#prohibits-why-not- EOT ) ; } /** * Execute the function. * * @return int */ protected function execute(InputInterface $input, OutputInterface $output) { return parent::doExecute($input, $output, true); } } * Jordi Boggiano * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Composer\Command; use Composer\Factory; use Composer\Json\JsonFile; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; use Composer\Repository\CompositeRepository; use Composer\Repository\PlatformRepository; use Composer\Repository\RepositoryInterface; use Composer\Plugin\CommandEvent; use Composer\Plugin\PluginEvents; /** * @author Robert Schönthal */ class SearchCommand extends BaseCommand { /** * @return void */ protected function configure() { $this ->setName('search') ->setDescription('Searches for packages.') ->setDefinition(array( new InputOption('only-name', 'N', InputOption::VALUE_NONE, 'Search only in package names'), new InputOption('only-vendor', 'O', InputOption::VALUE_NONE, 'Search only for vendor / organization names, returns only "vendor" as result'), new InputOption('type', 't', InputOption::VALUE_REQUIRED, 'Search for a specific package type'), new InputOption('format', 'f', InputOption::VALUE_REQUIRED, 'Format of the output: text or json', 'text'), new InputArgument('tokens', InputArgument::IS_ARRAY | InputArgument::REQUIRED, 'tokens to search for'), )) ->setHelp( <<php composer.phar search symfony composer Read more at https://getcomposer.org/doc/03-cli.md#search EOT ) ; } protected function execute(InputInterface $input, OutputInterface $output) { // init repos $platformRepo = new PlatformRepository; $io = $this->getIO(); $format = $input->getOption('format'); if (!in_array($format, array('text', 'json'))) { $io->writeError(sprintf('Unsupported format "%s". See help for supported formats.', $format)); return 1; } if (!($composer = $this->getComposer(false))) { $composer = Factory::create($this->getIO(), array(), $input->hasParameterOption('--no-plugins')); } $localRepo = $composer->getRepositoryManager()->getLocalRepository(); $installedRepo = new CompositeRepository(array($localRepo, $platformRepo)); $repos = new CompositeRepository(array_merge(array($installedRepo), $composer->getRepositoryManager()->getRepositories())); $commandEvent = new CommandEvent(PluginEvents::COMMAND, 'search', $input, $output); $composer->getEventDispatcher()->dispatch($commandEvent->getName(), $commandEvent); $mode = RepositoryInterface::SEARCH_FULLTEXT; if ($input->getOption('only-name') === true) { if ($input->getOption('only-vendor') === true) { throw new \InvalidArgumentException('--only-name and --only-vendor cannot be used together'); } $mode = RepositoryInterface::SEARCH_NAME; } elseif ($input->getOption('only-vendor') === true) { $mode = RepositoryInterface::SEARCH_VENDOR; } $type = $input->getOption('type'); $query = implode(' ', $input->getArgument('tokens')); if ($mode !== RepositoryInterface::SEARCH_FULLTEXT) { $query = preg_quote($query); } $results = $repos->search($query, $mode, $type); if ($results && $format === 'text') { $width = $this->getTerminalWidth(); $nameLength = 0; foreach ($results as $result) { $nameLength = max(strlen($result['name']), $nameLength); } $nameLength += 1; foreach ($results as $result) { $description = isset($result['description']) ? $result['description'] : ''; $warning = !empty($result['abandoned']) ? '! Abandoned ! ' : ''; $remaining = $width - $nameLength - strlen($warning) - 2; if (strlen($description) > $remaining) { $description = substr($description, 0, $remaining - 3) . '...'; } $io->write(str_pad($result['name'], $nameLength, ' ') . $warning . $description); } } elseif ($format === 'json') { $io->write(JsonFile::encode($results)); } return 0; } } * Jordi Boggiano * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Composer\Command; use Composer\Pcre\Preg; use Composer\Util\Filesystem; use Composer\Util\Platform; use Composer\Util\Silencer; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; use Composer\Config; use Composer\Config\JsonConfigSource; use Composer\Factory; use Composer\IO\IOInterface; use Composer\Json\JsonFile; use Composer\Semver\VersionParser; use Composer\Package\BasePackage; /** * @author Joshua Estes * @author Jordi Boggiano */ class ConfigCommand extends BaseCommand { /** * @var Config */ protected $config; /** * @var JsonFile */ protected $configFile; /** * @var JsonConfigSource */ protected $configSource; /** * @var JsonFile */ protected $authConfigFile; /** * @var JsonConfigSource */ protected $authConfigSource; /** * @return void */ protected function configure() { $this ->setName('config') ->setDescription('Sets config options.') ->setDefinition(array( new InputOption('global', 'g', InputOption::VALUE_NONE, 'Apply command to the global config file'), new InputOption('editor', 'e', InputOption::VALUE_NONE, 'Open editor'), new InputOption('auth', 'a', InputOption::VALUE_NONE, 'Affect auth config file (only used for --editor)'), new InputOption('unset', null, InputOption::VALUE_NONE, 'Unset the given setting-key'), new InputOption('list', 'l', InputOption::VALUE_NONE, 'List configuration settings'), new InputOption('file', 'f', InputOption::VALUE_REQUIRED, 'If you want to choose a different composer.json or config.json'), new InputOption('absolute', null, InputOption::VALUE_NONE, 'Returns absolute paths when fetching *-dir config values instead of relative'), new InputOption('json', 'j', InputOption::VALUE_NONE, 'JSON decode the setting value, to be used with extra.* keys'), new InputOption('merge', 'm', InputOption::VALUE_NONE, 'Merge the setting value with the current value, to be used with extra.* keys in combination with --json'), new InputOption('append', null, InputOption::VALUE_NONE, 'When adding a repository, append it (lowest priority) to the existing ones instead of prepending it (highest priority)'), new InputOption('source', null, InputOption::VALUE_NONE, 'Display where the config value is loaded from'), new InputArgument('setting-key', null, 'Setting key'), new InputArgument('setting-value', InputArgument::IS_ARRAY, 'Setting value'), )) ->setHelp( <<%command.full_name% bin-dir bin/ To read a config setting: %command.full_name% bin-dir Outputs: bin To edit the global config.json file: %command.full_name% --global To add a repository: %command.full_name% repositories.foo vcs https://bar.com To remove a repository (repo is a short alias for repositories): %command.full_name% --unset repo.foo To disable packagist: %command.full_name% repo.packagist false You can alter repositories in the global config.json file by passing in the --global option. To add or edit suggested packages you can use: %command.full_name% suggest.package reason for the suggestion To add or edit extra properties you can use: %command.full_name% extra.property value Or to add a complex value you can use json with: %command.full_name% extra.property --json '{"foo":true, "bar": []}' To edit the file in an external editor: %command.full_name% --editor To choose your editor you can set the "EDITOR" env variable. To get a list of configuration values in the file: %command.full_name% --list You can always pass more than one option. As an example, if you want to edit the global config.json file. %command.full_name% --editor --global Read more at https://getcomposer.org/doc/03-cli.md#config EOT ) ; } /** * @return void * @throws \Exception */ protected function initialize(InputInterface $input, OutputInterface $output) { parent::initialize($input, $output); if ($input->getOption('global') && null !== $input->getOption('file')) { throw new \RuntimeException('--file and --global can not be combined'); } $io = $this->getIO(); $this->config = Factory::createConfig($io); // Get the local composer.json, global config.json, or if the user // passed in a file to use $configFile = $input->getOption('global') ? ($this->config->get('home') . '/config.json') : ($input->getOption('file') ?: Factory::getComposerFile()); // Create global composer.json if this was invoked using `composer global config` if ( ($configFile === 'composer.json' || $configFile === './composer.json') && !file_exists($configFile) && realpath(getcwd()) === realpath($this->config->get('home')) ) { file_put_contents($configFile, "{\n}\n"); } $this->configFile = new JsonFile($configFile, null, $io); $this->configSource = new JsonConfigSource($this->configFile); $authConfigFile = $input->getOption('global') ? ($this->config->get('home') . '/auth.json') : dirname(realpath($configFile)) . '/auth.json'; $this->authConfigFile = new JsonFile($authConfigFile, null, $io); $this->authConfigSource = new JsonConfigSource($this->authConfigFile, true); // Initialize the global file if it's not there, ignoring any warnings or notices if ($input->getOption('global') && !$this->configFile->exists()) { touch($this->configFile->getPath()); $this->configFile->write(array('config' => new \ArrayObject)); Silencer::call('chmod', $this->configFile->getPath(), 0600); } if ($input->getOption('global') && !$this->authConfigFile->exists()) { touch($this->authConfigFile->getPath()); $this->authConfigFile->write(array('bitbucket-oauth' => new \ArrayObject, 'github-oauth' => new \ArrayObject, 'gitlab-oauth' => new \ArrayObject, 'gitlab-token' => new \ArrayObject, 'http-basic' => new \ArrayObject, 'bearer' => new \ArrayObject)); Silencer::call('chmod', $this->authConfigFile->getPath(), 0600); } if (!$this->configFile->exists()) { throw new \RuntimeException(sprintf('File "%s" cannot be found in the current directory', $configFile)); } } /** * @return int * @throws \Seld\JsonLint\ParsingException */ protected function execute(InputInterface $input, OutputInterface $output) { // Open file in editor if (true === $input->getOption('editor')) { $editor = escapeshellcmd(Platform::getEnv('EDITOR')); if (!$editor) { if (Platform::isWindows()) { $editor = 'notepad'; } else { foreach (array('editor', 'vim', 'vi', 'nano', 'pico', 'ed') as $candidate) { if (exec('which '.$candidate)) { $editor = $candidate; break; } } } } $file = $input->getOption('auth') ? $this->authConfigFile->getPath() : $this->configFile->getPath(); system($editor . ' ' . $file . (Platform::isWindows() ? '' : ' > `tty`')); return 0; } if (false === $input->getOption('global')) { $this->config->merge($this->configFile->read(), $this->configFile->getPath()); $this->config->merge(array('config' => $this->authConfigFile->exists() ? $this->authConfigFile->read() : array()), $this->authConfigFile->getPath()); } // List the configuration of the file settings if (true === $input->getOption('list')) { $this->listConfiguration($this->config->all(), $this->config->raw(), $output, null, (bool) $input->getOption('source')); return 0; } $settingKey = $input->getArgument('setting-key'); if (!is_string($settingKey)) { return 0; } // If the user enters in a config variable, parse it and save to file if (array() !== $input->getArgument('setting-value') && $input->getOption('unset')) { throw new \RuntimeException('You can not combine a setting value with --unset'); } // show the value if no value is provided if (array() === $input->getArgument('setting-value') && !$input->getOption('unset')) { $properties = array('name', 'type', 'description', 'homepage', 'version', 'minimum-stability', 'prefer-stable', 'keywords', 'license', 'extra'); $rawData = $this->configFile->read(); $data = $this->config->all(); if (Preg::isMatch('/^repos?(?:itories)?(?:\.(.+))?/', $settingKey, $matches)) { if (!isset($matches[1]) || $matches[1] === '') { $value = isset($data['repositories']) ? $data['repositories'] : array(); } else { if (!isset($data['repositories'][$matches[1]])) { throw new \InvalidArgumentException('There is no '.$matches[1].' repository defined'); } $value = $data['repositories'][$matches[1]]; } } elseif (strpos($settingKey, '.')) { $bits = explode('.', $settingKey); if ($bits[0] === 'extra') { $data = $rawData; } else { $data = $data['config']; } $match = false; foreach ($bits as $bit) { $key = isset($key) ? $key.'.'.$bit : $bit; $match = false; if (isset($data[$key])) { $match = true; $data = $data[$key]; unset($key); } } if (!$match) { throw new \RuntimeException($settingKey.' is not defined.'); } $value = $data; } elseif (isset($data['config'][$settingKey])) { $value = $this->config->get($settingKey, $input->getOption('absolute') ? 0 : Config::RELATIVE_PATHS); } elseif (isset($rawData[$settingKey]) && in_array($settingKey, $properties, true)) { $value = $rawData[$settingKey]; } else { throw new \RuntimeException($settingKey.' is not defined'); } if (is_array($value)) { $value = json_encode($value); } $sourceOfConfigValue = ''; if ($input->getOption('source')) { $sourceOfConfigValue = ' (' . $this->config->getSourceOfValue($settingKey) . ')'; } $this->getIO()->write($value . $sourceOfConfigValue, true, IOInterface::QUIET); return 0; } $values = $input->getArgument('setting-value'); // what the user is trying to add/change $booleanValidator = function ($val) { return in_array($val, array('true', 'false', '1', '0'), true); }; $booleanNormalizer = function ($val) { return $val !== 'false' && (bool) $val; }; // handle config values $uniqueConfigValues = array( 'process-timeout' => array('is_numeric', 'intval'), 'use-include-path' => array($booleanValidator, $booleanNormalizer), 'use-github-api' => array($booleanValidator, $booleanNormalizer), 'preferred-install' => array( function ($val) { return in_array($val, array('auto', 'source', 'dist'), true); }, function ($val) { return $val; }, ), 'gitlab-protocol' => array( function ($val) { return in_array($val, array('git', 'http', 'https'), true); }, function ($val) { return $val; }, ), 'store-auths' => array( function ($val) { return in_array($val, array('true', 'false', 'prompt'), true); }, function ($val) { if ('prompt' === $val) { return 'prompt'; } return $val !== 'false' && (bool) $val; }, ), 'notify-on-install' => array($booleanValidator, $booleanNormalizer), 'vendor-dir' => array('is_string', function ($val) { return $val; }), 'bin-dir' => array('is_string', function ($val) { return $val; }), 'archive-dir' => array('is_string', function ($val) { return $val; }), 'archive-format' => array('is_string', function ($val) { return $val; }), 'data-dir' => array('is_string', function ($val) { return $val; }), 'cache-dir' => array('is_string', function ($val) { return $val; }), 'cache-files-dir' => array('is_string', function ($val) { return $val; }), 'cache-repo-dir' => array('is_string', function ($val) { return $val; }), 'cache-vcs-dir' => array('is_string', function ($val) { return $val; }), 'cache-ttl' => array('is_numeric', 'intval'), 'cache-files-ttl' => array('is_numeric', 'intval'), 'cache-files-maxsize' => array( function ($val) { return Preg::isMatch('/^\s*([0-9.]+)\s*(?:([kmg])(?:i?b)?)?\s*$/i', $val); }, function ($val) { return $val; }, ), 'bin-compat' => array( function ($val) { return in_array($val, array('auto', 'full', 'symlink')); }, function ($val) { return $val; }, ), 'discard-changes' => array( function ($val) { return in_array($val, array('stash', 'true', 'false', '1', '0'), true); }, function ($val) { if ('stash' === $val) { return 'stash'; } return $val !== 'false' && (bool) $val; }, ), 'autoloader-suffix' => array('is_string', function ($val) { return $val === 'null' ? null : $val; }), 'sort-packages' => array($booleanValidator, $booleanNormalizer), 'optimize-autoloader' => array($booleanValidator, $booleanNormalizer), 'classmap-authoritative' => array($booleanValidator, $booleanNormalizer), 'apcu-autoloader' => array($booleanValidator, $booleanNormalizer), 'prepend-autoloader' => array($booleanValidator, $booleanNormalizer), 'disable-tls' => array($booleanValidator, $booleanNormalizer), 'secure-http' => array($booleanValidator, $booleanNormalizer), 'cafile' => array( function ($val) { return file_exists($val) && Filesystem::isReadable($val); }, function ($val) { return $val === 'null' ? null : $val; }, ), 'capath' => array( function ($val) { return is_dir($val) && Filesystem::isReadable($val); }, function ($val) { return $val === 'null' ? null : $val; }, ), 'github-expose-hostname' => array($booleanValidator, $booleanNormalizer), 'htaccess-protect' => array($booleanValidator, $booleanNormalizer), 'lock' => array($booleanValidator, $booleanNormalizer), 'allow-plugins' => array($booleanValidator, $booleanNormalizer), 'platform-check' => array( function ($val) { return in_array($val, array('php-only', 'true', 'false', '1', '0'), true); }, function ($val) { if ('php-only' === $val) { return 'php-only'; } return $val !== 'false' && (bool)$val; }, ), 'use-parent-dir' => array( function ($val) { return in_array($val, array('true', 'false', 'prompt'), true); }, function ($val) { if ('prompt' === $val) { return 'prompt'; } return $val !== 'false' && (bool) $val; }, ), ); $multiConfigValues = array( 'github-protocols' => array( function ($vals) { if (!is_array($vals)) { return 'array expected'; } foreach ($vals as $val) { if (!in_array($val, array('git', 'https', 'ssh'))) { return 'valid protocols include: git, https, ssh'; } } return true; }, function ($vals) { return $vals; }, ), 'github-domains' => array( function ($vals) { if (!is_array($vals)) { return 'array expected'; } return true; }, function ($vals) { return $vals; }, ), 'gitlab-domains' => array( function ($vals) { if (!is_array($vals)) { return 'array expected'; } return true; }, function ($vals) { return $vals; }, ), ); if ($input->getOption('unset') && (isset($uniqueConfigValues[$settingKey]) || isset($multiConfigValues[$settingKey]))) { if ($settingKey === 'disable-tls' && $this->config->get('disable-tls')) { $this->getIO()->writeError('You are now running Composer with SSL/TLS protection enabled.'); } $this->configSource->removeConfigSetting($settingKey); return 0; } if (isset($uniqueConfigValues[$settingKey])) { $this->handleSingleValue($settingKey, $uniqueConfigValues[$settingKey], $values, 'addConfigSetting'); return 0; } if (isset($multiConfigValues[$settingKey])) { $this->handleMultiValue($settingKey, $multiConfigValues[$settingKey], $values, 'addConfigSetting'); return 0; } // handle preferred-install per-package config if (Preg::isMatch('/^preferred-install\.(.+)/', $settingKey, $matches)) { if ($input->getOption('unset')) { $this->configSource->removeConfigSetting($settingKey); return 0; } list($validator) = $uniqueConfigValues['preferred-install']; if (!$validator($values[0])) { throw new \RuntimeException('Invalid value for '.$settingKey.'. Should be one of: auto, source, or dist'); } $this->configSource->addConfigSetting($settingKey, $values[0]); return 0; } // handle allow-plugins config setting elements true or false to add/remove if (Preg::isMatch('{^allow-plugins\.([a-zA-Z0-9/*-]+)}', $settingKey, $matches)) { if ($input->getOption('unset')) { $this->configSource->removeConfigSetting($settingKey); return 0; } if (true !== $booleanValidator($values[0])) { throw new \RuntimeException(sprintf( '"%s" is an invalid value', $values[0] )); } $normalizedValue = $booleanNormalizer($values[0]); $this->configSource->addConfigSetting($settingKey, $normalizedValue); return 0; } // handle properties $uniqueProps = array( 'name' => array('is_string', function ($val) { return $val; }), 'type' => array('is_string', function ($val) { return $val; }), 'description' => array('is_string', function ($val) { return $val; }), 'homepage' => array('is_string', function ($val) { return $val; }), 'version' => array('is_string', function ($val) { return $val; }), 'minimum-stability' => array( function ($val) { return isset(BasePackage::$stabilities[VersionParser::normalizeStability($val)]); }, function ($val) { return VersionParser::normalizeStability($val); }, ), 'prefer-stable' => array($booleanValidator, $booleanNormalizer), ); $multiProps = array( 'keywords' => array( function ($vals) { if (!is_array($vals)) { return 'array expected'; } return true; }, function ($vals) { return $vals; }, ), 'license' => array( function ($vals) { if (!is_array($vals)) { return 'array expected'; } return true; }, function ($vals) { return $vals; }, ), ); if ($input->getOption('global') && (isset($uniqueProps[$settingKey]) || isset($multiProps[$settingKey]) || strpos($settingKey, 'extra.') === 0)) { throw new \InvalidArgumentException('The ' . $settingKey . ' property can not be set in the global config.json file. Use `composer global config` to apply changes to the global composer.json'); } if ($input->getOption('unset') && (isset($uniqueProps[$settingKey]) || isset($multiProps[$settingKey]))) { $this->configSource->removeProperty($settingKey); return 0; } if (isset($uniqueProps[$settingKey])) { $this->handleSingleValue($settingKey, $uniqueProps[$settingKey], $values, 'addProperty'); return 0; } if (isset($multiProps[$settingKey])) { $this->handleMultiValue($settingKey, $multiProps[$settingKey], $values, 'addProperty'); return 0; } // handle repositories if (Preg::isMatch('/^repos?(?:itories)?\.(.+)/', $settingKey, $matches)) { if ($input->getOption('unset')) { $this->configSource->removeRepository($matches[1]); return 0; } if (2 === count($values)) { $this->configSource->addRepository($matches[1], array( 'type' => $values[0], 'url' => $values[1], ), $input->getOption('append')); return 0; } if (1 === count($values)) { $value = strtolower($values[0]); if (true === $booleanValidator($value)) { if (false === $booleanNormalizer($value)) { $this->configSource->addRepository($matches[1], false, $input->getOption('append')); return 0; } } else { $value = JsonFile::parseJson($values[0]); $this->configSource->addRepository($matches[1], $value, $input->getOption('append')); return 0; } } throw new \RuntimeException('You must pass the type and a url. Example: php composer.phar config repositories.foo vcs https://bar.com'); } // handle extra if (Preg::isMatch('/^extra\.(.+)/', $settingKey, $matches)) { if ($input->getOption('unset')) { $this->configSource->removeProperty($settingKey); return 0; } $value = $values[0]; if ($input->getOption('json')) { $value = JsonFile::parseJson($value); if ($input->getOption('merge')) { $currentValue = $this->configFile->read(); $bits = explode('.', $settingKey); foreach ($bits as $bit) { $currentValue = isset($currentValue[$bit]) ? $currentValue[$bit] : null; } if (is_array($currentValue)) { $value = array_merge($currentValue, $value); } } } $this->configSource->addProperty($settingKey, $value); return 0; } // handle suggest if (Preg::isMatch('/^suggest\.(.+)/', $settingKey, $matches)) { if ($input->getOption('unset')) { $this->configSource->removeProperty($settingKey); return 0; } $this->configSource->addProperty($settingKey, implode(' ', $values)); return 0; } // handle unsetting extra/suggest if (in_array($settingKey, array('suggest', 'extra'), true) && $input->getOption('unset')) { $this->configSource->removeProperty($settingKey); return 0; } // handle platform if (Preg::isMatch('/^platform\.(.+)/', $settingKey, $matches)) { if ($input->getOption('unset')) { $this->configSource->removeConfigSetting($settingKey); return 0; } $this->configSource->addConfigSetting($settingKey, $values[0] === 'false' ? false : $values[0]); return 0; } // handle unsetting platform if ($settingKey === 'platform' && $input->getOption('unset')) { $this->configSource->removeConfigSetting($settingKey); return 0; } // handle auth if (Preg::isMatch('/^(bitbucket-oauth|github-oauth|gitlab-oauth|gitlab-token|http-basic|bearer)\.(.+)/', $settingKey, $matches)) { if ($input->getOption('unset')) { $this->authConfigSource->removeConfigSetting($matches[1].'.'.$matches[2]); $this->configSource->removeConfigSetting($matches[1].'.'.$matches[2]); return 0; } if ($matches[1] === 'bitbucket-oauth') { if (2 !== count($values)) { throw new \RuntimeException('Expected two arguments (consumer-key, consumer-secret), got '.count($values)); } $this->configSource->removeConfigSetting($matches[1].'.'.$matches[2]); $this->authConfigSource->addConfigSetting($matches[1].'.'.$matches[2], array('consumer-key' => $values[0], 'consumer-secret' => $values[1])); } elseif ($matches[1] === 'gitlab-token' && 2 === count($values)) { $this->configSource->removeConfigSetting($matches[1].'.'.$matches[2]); $this->authConfigSource->addConfigSetting($matches[1].'.'.$matches[2], array('username' => $values[0], 'token' => $values[1])); } elseif (in_array($matches[1], array('github-oauth', 'gitlab-oauth', 'gitlab-token', 'bearer'), true)) { if (1 !== count($values)) { throw new \RuntimeException('Too many arguments, expected only one token'); } $this->configSource->removeConfigSetting($matches[1].'.'.$matches[2]); $this->authConfigSource->addConfigSetting($matches[1].'.'.$matches[2], $values[0]); } elseif ($matches[1] === 'http-basic') { if (2 !== count($values)) { throw new \RuntimeException('Expected two arguments (username, password), got '.count($values)); } $this->configSource->removeConfigSetting($matches[1].'.'.$matches[2]); $this->authConfigSource->addConfigSetting($matches[1].'.'.$matches[2], array('username' => $values[0], 'password' => $values[1])); } return 0; } // handle script if (Preg::isMatch('/^scripts\.(.+)/', $settingKey, $matches)) { if ($input->getOption('unset')) { $this->configSource->removeProperty($settingKey); return 0; } $this->configSource->addProperty($settingKey, count($values) > 1 ? $values : $values[0]); return 0; } throw new \InvalidArgumentException('Setting '.$settingKey.' does not exist or is not supported by this command'); } /** * @param string $key * @param array{callable, callable} $callbacks Validator and normalizer callbacks * @param array $values * @param string $method * * @return void */ protected function handleSingleValue($key, array $callbacks, array $values, $method) { list($validator, $normalizer) = $callbacks; if (1 !== count($values)) { throw new \RuntimeException('You can only pass one value. Example: php composer.phar config process-timeout 300'); } if (true !== $validation = $validator($values[0])) { throw new \RuntimeException(sprintf( '"%s" is an invalid value'.($validation ? ' ('.$validation.')' : ''), $values[0] )); } $normalizedValue = $normalizer($values[0]); if ($key === 'disable-tls') { if (!$normalizedValue && $this->config->get('disable-tls')) { $this->getIO()->writeError('You are now running Composer with SSL/TLS protection enabled.'); } elseif ($normalizedValue && !$this->config->get('disable-tls')) { $this->getIO()->writeError('You are now running Composer with SSL/TLS protection disabled.'); } } call_user_func(array($this->configSource, $method), $key, $normalizedValue); } /** * @param string $key * @param array{callable, callable} $callbacks Validator and normalizer callbacks * @param array $values * @param string $method * * @return void */ protected function handleMultiValue($key, array $callbacks, array $values, $method) { list($validator, $normalizer) = $callbacks; if (true !== $validation = $validator($values)) { throw new \RuntimeException(sprintf( '%s is an invalid value'.($validation ? ' ('.$validation.')' : ''), json_encode($values) )); } call_user_func(array($this->configSource, $method), $key, $normalizer($values)); } /** * Display the contents of the file in a pretty formatted way * * @param array $contents * @param array $rawContents * @param string|null $k * @param bool $showSource * * @return void */ protected function listConfiguration(array $contents, array $rawContents, OutputInterface $output, $k = null, $showSource = false) { $origK = $k; $io = $this->getIO(); foreach ($contents as $key => $value) { if ($k === null && !in_array($key, array('config', 'repositories'))) { continue; } $rawVal = isset($rawContents[$key]) ? $rawContents[$key] : null; if (is_array($value) && (!is_numeric(key($value)) || ($key === 'repositories' && null === $k))) { $k .= Preg::replace('{^config\.}', '', $key . '.'); $this->listConfiguration($value, $rawVal, $output, $k, $showSource); $k = $origK; continue; } if (is_array($value)) { $value = array_map(function ($val) { return is_array($val) ? json_encode($val) : $val; }, $value); $value = '['.implode(', ', $value).']'; } if (is_bool($value)) { $value = var_export($value, true); } $source = ''; if ($showSource) { $source = ' (' . $this->config->getSourceOfValue($k . $key) . ')'; } if (is_string($rawVal) && $rawVal != $value) { $io->write('[' . $k . $key . '] ' . $rawVal . ' (' . $value . ')' . $source, true, IOInterface::QUIET); } else { $io->write('[' . $k . $key . '] ' . $value . '' . $source, true, IOInterface::QUIET); } } } } * Jordi Boggiano * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Composer\Command; use Composer\Composer; use Composer\Config; use Composer\Console\Application; use Composer\Factory; use Composer\IO\IOInterface; use Composer\IO\NullIO; use Composer\Plugin\PreCommandRunEvent; use Composer\Package\Version\VersionParser; use Composer\Plugin\PluginEvents; use Composer\Util\Platform; use Symfony\Component\Console\Helper\Table; use Symfony\Component\Console\Helper\TableSeparator; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Terminal; /** * Base class for Composer commands * * @method Application getApplication() * * @author Ryan Weaver * @author Konstantin Kudryashov */ abstract class BaseCommand extends Command { /** * @var Composer|null */ private $composer; /** * @var IOInterface */ private $io; /** * @param bool $required * @param bool|null $disablePlugins * @param bool|null $disableScripts * @throws \RuntimeException * @return Composer|null */ public function getComposer($required = true, $disablePlugins = null, $disableScripts = null) { if (null === $this->composer) { $application = $this->getApplication(); if ($application instanceof Application) { /* @var $application Application */ $this->composer = $application->getComposer($required, $disablePlugins, $disableScripts); /** @phpstan-ignore-next-line */ } elseif ($required) { throw new \RuntimeException( 'Could not create a Composer\Composer instance, you must inject '. 'one if this command is not used with a Composer\Console\Application instance' ); } } return $this->composer; } /** * @return void */ public function setComposer(Composer $composer) { $this->composer = $composer; } /** * Removes the cached composer instance * * @return void */ public function resetComposer() { $this->composer = null; $this->getApplication()->resetComposer(); } /** * Whether or not this command is meant to call another command. * * This is mainly needed to avoid duplicated warnings messages. * * @return bool */ public function isProxyCommand() { return false; } /** * @return IOInterface */ public function getIO() { if (null === $this->io) { $application = $this->getApplication(); if ($application instanceof Application) { $this->io = $application->getIO(); /** @phpstan-ignore-next-line */ } else { $this->io = new NullIO(); } } return $this->io; } /** * @return void */ public function setIO(IOInterface $io) { $this->io = $io; } /** * @inheritDoc * * @return void */ protected function initialize(InputInterface $input, OutputInterface $output) { // initialize a plugin-enabled Composer instance, either local or global $disablePlugins = $input->hasParameterOption('--no-plugins'); $disableScripts = $input->hasParameterOption('--no-scripts'); if ($this instanceof SelfUpdateCommand) { $disablePlugins = true; $disableScripts = true; } $composer = $this->getComposer(false, $disablePlugins, $disableScripts); if (null === $composer) { $composer = Factory::createGlobal($this->getIO(), $disablePlugins, $disableScripts); } if ($composer) { $preCommandRunEvent = new PreCommandRunEvent(PluginEvents::PRE_COMMAND_RUN, $input, $this->getName()); $composer->getEventDispatcher()->dispatch($preCommandRunEvent->getName(), $preCommandRunEvent); } if (true === $input->hasParameterOption(array('--no-ansi')) && $input->hasOption('no-progress')) { $input->setOption('no-progress', true); } if (true == $input->hasOption('no-dev')) { if (!$input->getOption('no-dev') && true == Platform::getEnv('COMPOSER_NO_DEV')) { $input->setOption('no-dev', true); } } parent::initialize($input, $output); } /** * Returns preferSource and preferDist values based on the configuration. * * @param bool $keepVcsRequiresPreferSource * * @return bool[] An array composed of the preferSource and preferDist values */ protected function getPreferredInstallOptions(Config $config, InputInterface $input, $keepVcsRequiresPreferSource = false) { $preferSource = false; $preferDist = false; switch ($config->get('preferred-install')) { case 'source': $preferSource = true; break; case 'dist': $preferDist = true; break; case 'auto': default: // noop break; } if ($input->hasOption('prefer-install') && $input->getOption('prefer-install')) { if ($input->getOption('prefer-source')) { throw new \InvalidArgumentException('--prefer-source can not be used together with --prefer-install'); } if ($input->getOption('prefer-dist')) { throw new \InvalidArgumentException('--prefer-dist can not be used together with --prefer-install'); } switch ($input->getOption('prefer-install')) { case 'dist': $input->setOption('prefer-dist', true); break; case 'source': $input->setOption('prefer-source', true); break; case 'auto': $preferDist = false; $preferSource = false; break; default: throw new \UnexpectedValueException('--prefer-install accepts one of "dist", "source" or "auto", got '.$input->getOption('prefer-install')); } } if ($input->getOption('prefer-source') || $input->getOption('prefer-dist') || ($keepVcsRequiresPreferSource && $input->hasOption('keep-vcs') && $input->getOption('keep-vcs'))) { $preferSource = $input->getOption('prefer-source') || ($keepVcsRequiresPreferSource && $input->hasOption('keep-vcs') && $input->getOption('keep-vcs')); $preferDist = (bool) $input->getOption('prefer-dist'); } return array($preferSource, $preferDist); } /** * @param array $requirements * * @return array */ protected function formatRequirements(array $requirements) { $requires = array(); $requirements = $this->normalizeRequirements($requirements); foreach ($requirements as $requirement) { if (!isset($requirement['version'])) { throw new \UnexpectedValueException('Option '.$requirement['name'] .' is missing a version constraint, use e.g. '.$requirement['name'].':^1.0'); } $requires[$requirement['name']] = $requirement['version']; } return $requires; } /** * @param array $requirements * * @return list */ protected function normalizeRequirements(array $requirements) { $parser = new VersionParser(); return $parser->parseNameVersionPairs($requirements); } /** * @param array $table * * @return void */ protected function renderTable(array $table, OutputInterface $output) { $renderer = new Table($output); $renderer->setStyle('compact'); $rendererStyle = $renderer->getStyle(); if (method_exists($rendererStyle, 'setVerticalBorderChars')) { $rendererStyle->setVerticalBorderChars(''); } else { // TODO remove in composer 2.2 // @phpstan-ignore-next-line $rendererStyle->setVerticalBorderChar(''); } $rendererStyle->setCellRowContentFormat('%s '); $renderer->setRows($table)->render(); } /** * @return int */ protected function getTerminalWidth() { if (class_exists('Symfony\Component\Console\Terminal')) { $terminal = new Terminal(); $width = $terminal->getWidth(); } else { // For versions of Symfony console before 3.2 // TODO remove in composer 2.2 // @phpstan-ignore-next-line list($width) = $this->getApplication()->getTerminalDimensions(); } if (null === $width) { // In case the width is not detected, we're probably running the command // outside of a real terminal, use space without a limit $width = PHP_INT_MAX; } if (Platform::isWindows()) { $width--; } else { $width = max(80, $width); } return $width; } } * Jordi Boggiano * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Composer\Command; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Console\Input\InputArgument; /** * @author Davey Shafik */ class ExecCommand extends BaseCommand { /** * @return void */ protected function configure() { $this ->setName('exec') ->setDescription('Executes a vendored binary/script.') ->setDefinition(array( new InputOption('list', 'l', InputOption::VALUE_NONE), new InputArgument('binary', InputArgument::OPTIONAL, 'The binary to run, e.g. phpunit'), new InputArgument( 'args', InputArgument::IS_ARRAY | InputArgument::OPTIONAL, 'Arguments to pass to the binary. Use -- to separate from composer arguments' ), )) ->setHelp( <<getComposer(); $binDir = $composer->getConfig()->get('bin-dir'); if ($input->getOption('list') || !$input->getArgument('binary')) { $bins = glob($binDir . '/*'); $bins = array_merge($bins, array_map(function ($e) { return "$e (local)"; }, $composer->getPackage()->getBinaries())); if (!$bins) { throw new \RuntimeException("No binaries found in composer.json or in bin-dir ($binDir)"); } $this->getIO()->write( <<Available binaries: EOT ); foreach ($bins as $bin) { // skip .bat copies if (isset($previousBin) && $bin === $previousBin.'.bat') { continue; } $previousBin = $bin; $bin = basename($bin); $this->getIO()->write( <<- $bin EOT ); } return 0; } $binary = $input->getArgument('binary'); $dispatcher = $composer->getEventDispatcher(); $dispatcher->addListener('__exec_command', $binary); // If the CWD was modified, we restore it to what it was initially, as it was // most likely modified by the global command, and we want exec to run in the local working directory // not the global one if (getcwd() !== $this->getApplication()->getInitialWorkingDirectory()) { try { chdir($this->getApplication()->getInitialWorkingDirectory()); } catch (\Exception $e) { throw new \RuntimeException('Could not switch back to working directory "'.$this->getApplication()->getInitialWorkingDirectory().'"', 0, $e); } } return $dispatcher->dispatchScript('__exec_command', true, $input->getArgument('args')); } } * Jordi Boggiano * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Composer\Command; use Composer\Package\CompletePackageInterface; use Composer\Repository\RepositoryInterface; use Composer\Repository\RootPackageRepository; use Composer\Repository\RepositoryFactory; use Composer\Util\Platform; use Composer\Util\ProcessExecutor; use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; /** * @author Robert Schönthal */ class HomeCommand extends BaseCommand { /** * @inheritDoc * * @return void */ protected function configure() { $this ->setName('browse') ->setAliases(array('home')) ->setDescription('Opens the package\'s repository URL or homepage in your browser.') ->setDefinition(array( new InputArgument('packages', InputArgument::IS_ARRAY, 'Package(s) to browse to.'), new InputOption('homepage', 'H', InputOption::VALUE_NONE, 'Open the homepage instead of the repository URL.'), new InputOption('show', 's', InputOption::VALUE_NONE, 'Only show the homepage or repository URL.'), )) ->setHelp( <<initializeRepos(); $io = $this->getIO(); $return = 0; $packages = $input->getArgument('packages'); if (!$packages) { $io->writeError('No package specified, opening homepage for the root package'); $packages = array($this->getComposer()->getPackage()->getName()); } foreach ($packages as $packageName) { $handled = false; $packageExists = false; foreach ($repos as $repo) { foreach ($repo->findPackages($packageName) as $package) { $packageExists = true; if ($package instanceof CompletePackageInterface && $this->handlePackage($package, $input->getOption('homepage'), $input->getOption('show'))) { $handled = true; break 2; } } } if (!$packageExists) { $return = 1; $io->writeError('Package '.$packageName.' not found'); } if (!$handled) { $return = 1; $io->writeError(''.($input->getOption('homepage') ? 'Invalid or missing homepage' : 'Invalid or missing repository URL').' for '.$packageName.''); } } return $return; } /** * @param bool $showHomepage * @param bool $showOnly * @return bool */ private function handlePackage(CompletePackageInterface $package, $showHomepage, $showOnly) { $support = $package->getSupport(); $url = isset($support['source']) ? $support['source'] : $package->getSourceUrl(); if (!$url || $showHomepage) { $url = $package->getHomepage(); } if (!$url || !filter_var($url, FILTER_VALIDATE_URL)) { return false; } if ($showOnly) { $this->getIO()->write(sprintf('%s', $url)); } else { $this->openBrowser($url); } return true; } /** * opens a url in your system default browser * * @param string $url * @return void */ private function openBrowser($url) { $url = ProcessExecutor::escape($url); $process = new ProcessExecutor($this->getIO()); if (Platform::isWindows()) { $process->execute('start "web" explorer ' . $url, $output); return; } $linux = $process->execute('which xdg-open', $output); $osx = $process->execute('which open', $output); if (0 === $linux) { $process->execute('xdg-open ' . $url, $output); } elseif (0 === $osx) { $process->execute('open ' . $url, $output); } else { $this->getIO()->writeError('No suitable browser opening command found, open yourself: ' . $url); } } /** * Initializes repositories * * Returns an array of repos in order they should be checked in * * @return RepositoryInterface[] */ private function initializeRepos() { $composer = $this->getComposer(false); if ($composer) { return array_merge( array(new RootPackageRepository($composer->getPackage())), // root package array($composer->getRepositoryManager()->getLocalRepository()), // installed packages $composer->getRepositoryManager()->getRepositories() // remotes ); } return RepositoryFactory::defaultRepos($this->getIO()); } } * Jordi Boggiano * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Composer\Command; use Composer\Composer; use Composer\DependencyResolver\Request; use Composer\Filter\PlatformRequirementFilter\PlatformRequirementFilterFactory; use Composer\Installer; use Composer\IO\IOInterface; use Composer\Package\Loader\RootPackageLoader; use Composer\Pcre\Preg; use Composer\Plugin\CommandEvent; use Composer\Plugin\PluginEvents; use Composer\Package\Version\VersionParser; use Composer\Util\HttpDownloader; use Composer\Semver\Constraint\MultiConstraint; use Composer\Package\Link; use Symfony\Component\Console\Helper\Table; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Console\Question\Question; /** * @author Jordi Boggiano * @author Nils Adermann */ class UpdateCommand extends BaseCommand { /** * @return void */ protected function configure() { $this ->setName('update') ->setAliases(array('u', 'upgrade')) ->setDescription('Upgrades your dependencies to the latest version according to composer.json, and updates the composer.lock file.') ->setDefinition(array( new InputArgument('packages', InputArgument::IS_ARRAY | InputArgument::OPTIONAL, 'Packages that should be updated, if not provided all packages are.'), new InputOption('with', null, InputOption::VALUE_IS_ARRAY | InputOption::VALUE_REQUIRED, 'Temporary version constraint to add, e.g. foo/bar:1.0.0 or foo/bar=1.0.0'), new InputOption('prefer-source', null, InputOption::VALUE_NONE, 'Forces installation from package sources when possible, including VCS information.'), new InputOption('prefer-dist', null, InputOption::VALUE_NONE, 'Forces installation from package dist (default behavior).'), new InputOption('prefer-install', null, InputOption::VALUE_REQUIRED, 'Forces installation from package dist|source|auto (auto chooses source for dev versions, dist for the rest).'), new InputOption('dry-run', null, InputOption::VALUE_NONE, 'Outputs the operations but will not execute anything (implicitly enables --verbose).'), new InputOption('dev', null, InputOption::VALUE_NONE, 'DEPRECATED: Enables installation of require-dev packages (enabled by default, only present for BC).'), new InputOption('no-dev', null, InputOption::VALUE_NONE, 'Disables installation of require-dev packages.'), new InputOption('lock', null, InputOption::VALUE_NONE, 'Overwrites the lock file hash to suppress warning about the lock file being out of date without updating package versions. Package metadata like mirrors and URLs are updated if they changed.'), new InputOption('no-install', null, InputOption::VALUE_NONE, 'Skip the install step after updating the composer.lock file.'), new InputOption('no-autoloader', null, InputOption::VALUE_NONE, 'Skips autoloader generation'), new InputOption('no-suggest', null, InputOption::VALUE_NONE, 'DEPRECATED: This flag does not exist anymore.'), new InputOption('no-progress', null, InputOption::VALUE_NONE, 'Do not output download progress.'), new InputOption('with-dependencies', 'w', InputOption::VALUE_NONE, 'Update also dependencies of packages in the argument list, except those which are root requirements.'), new InputOption('with-all-dependencies', 'W', InputOption::VALUE_NONE, 'Update also dependencies of packages in the argument list, including those which are root requirements.'), new InputOption('verbose', 'v|vv|vvv', InputOption::VALUE_NONE, 'Shows more details including new commits pulled in when updating packages.'), new InputOption('optimize-autoloader', 'o', InputOption::VALUE_NONE, 'Optimize autoloader during autoloader dump.'), new InputOption('classmap-authoritative', 'a', InputOption::VALUE_NONE, 'Autoload classes from the classmap only. Implicitly enables `--optimize-autoloader`.'), new InputOption('apcu-autoloader', null, InputOption::VALUE_NONE, 'Use APCu to cache found/not-found classes.'), new InputOption('apcu-autoloader-prefix', null, InputOption::VALUE_REQUIRED, 'Use a custom prefix for the APCu autoloader cache. Implicitly enables --apcu-autoloader'), new InputOption('ignore-platform-req', null, InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY, 'Ignore a specific platform requirement (php & ext- packages).'), new InputOption('ignore-platform-reqs', null, InputOption::VALUE_NONE, 'Ignore all platform requirements (php & ext- packages).'), new InputOption('prefer-stable', null, InputOption::VALUE_NONE, 'Prefer stable versions of dependencies.'), new InputOption('prefer-lowest', null, InputOption::VALUE_NONE, 'Prefer lowest versions of dependencies.'), new InputOption('interactive', 'i', InputOption::VALUE_NONE, 'Interactive interface with autocompletion to select the packages to update.'), new InputOption('root-reqs', null, InputOption::VALUE_NONE, 'Restricts the update to your first degree dependencies.'), )) ->setHelp( <<update command reads the composer.json file from the current directory, processes it, and updates, removes or installs all the dependencies. php composer.phar update To limit the update operation to a few packages, you can list the package(s) you want to update as such: php composer.phar update vendor/package1 foo/mypackage [...] You may also use an asterisk (*) pattern to limit the update operation to package(s) from a specific vendor: php composer.phar update vendor/package1 foo/* [...] To run an update with more restrictive constraints you can use: php composer.phar update --with vendor/package:1.0.* To run a partial update with more restrictive constraints you can use the shorthand: php composer.phar update vendor/package:1.0.* To select packages names interactively with auto-completion use -i. Read more at https://getcomposer.org/doc/03-cli.md#update-u EOT ) ; } /** * @return int * @throws \Exception */ protected function execute(InputInterface $input, OutputInterface $output) { $io = $this->getIO(); if ($input->getOption('dev')) { $io->writeError('You are using the deprecated option "--dev". It has no effect and will break in Composer 3.'); } if ($input->getOption('no-suggest')) { $io->writeError('You are using the deprecated option "--no-suggest". It has no effect and will break in Composer 3.'); } $composer = $this->getComposer(true, $input->getOption('no-plugins'), $input->getOption('no-scripts')); if (!HttpDownloader::isCurlEnabled()) { $io->writeError('Composer is operating significantly slower than normal because you do not have the PHP curl extension enabled.'); } $packages = $input->getArgument('packages'); $reqs = $this->formatRequirements($input->getOption('with')); // extract --with shorthands from the allowlist if ($packages) { $allowlistPackagesWithRequirements = array_filter($packages, function ($pkg) { return Preg::isMatch('{\S+[ =:]\S+}', $pkg); }); foreach ($this->formatRequirements($allowlistPackagesWithRequirements) as $package => $constraint) { $reqs[$package] = $constraint; } // replace the foo/bar:req by foo/bar in the allowlist foreach ($allowlistPackagesWithRequirements as $package) { $packageName = Preg::replace('{^([^ =:]+)[ =:].*$}', '$1', $package); $index = array_search($package, $packages); $packages[$index] = $packageName; } } $rootPackage = $composer->getPackage(); $rootRequires = $rootPackage->getRequires(); $rootDevRequires = $rootPackage->getDevRequires(); foreach ($reqs as $package => $constraint) { if (isset($rootRequires[$package])) { $rootRequires[$package] = $this->appendConstraintToLink($rootRequires[$package], $constraint); } elseif (isset($rootDevRequires[$package])) { $rootDevRequires[$package] = $this->appendConstraintToLink($rootDevRequires[$package], $constraint); } else { throw new \UnexpectedValueException('Only root package requirements can receive temporary constraints and '.$package.' is not one'); } } $rootPackage->setRequires($rootRequires); $rootPackage->setDevRequires($rootDevRequires); $rootPackage->setReferences(RootPackageLoader::extractReferences($reqs, $rootPackage->getReferences())); $rootPackage->setStabilityFlags(RootPackageLoader::extractStabilityFlags($reqs, $rootPackage->getMinimumStability(), $rootPackage->getStabilityFlags())); if ($input->getOption('interactive')) { $packages = $this->getPackagesInteractively($io, $input, $output, $composer, $packages); } if ($input->getOption('root-reqs')) { $requires = array_keys($rootRequires); if (!$input->getOption('no-dev')) { $requires = array_merge($requires, array_keys($rootDevRequires)); } if (!empty($packages)) { $packages = array_intersect($packages, $requires); } else { $packages = $requires; } } // the arguments lock/nothing/mirrors are not package names but trigger a mirror update instead // they are further mutually exclusive with listing actual package names $filteredPackages = array_filter($packages, function ($package) { return !in_array($package, array('lock', 'nothing', 'mirrors'), true); }); $updateMirrors = $input->getOption('lock') || count($filteredPackages) != count($packages); $packages = $filteredPackages; if ($updateMirrors && !empty($packages)) { $io->writeError('You cannot simultaneously update only a selection of packages and regenerate the lock file metadata.'); return -1; } $commandEvent = new CommandEvent(PluginEvents::COMMAND, 'update', $input, $output); $composer->getEventDispatcher()->dispatch($commandEvent->getName(), $commandEvent); $composer->getInstallationManager()->setOutputProgress(!$input->getOption('no-progress')); $install = Installer::create($io, $composer); $config = $composer->getConfig(); list($preferSource, $preferDist) = $this->getPreferredInstallOptions($config, $input); $optimize = $input->getOption('optimize-autoloader') || $config->get('optimize-autoloader'); $authoritative = $input->getOption('classmap-authoritative') || $config->get('classmap-authoritative'); $apcuPrefix = $input->getOption('apcu-autoloader-prefix'); $apcu = $apcuPrefix !== null || $input->getOption('apcu-autoloader') || $config->get('apcu-autoloader'); $updateAllowTransitiveDependencies = Request::UPDATE_ONLY_LISTED; if ($input->getOption('with-all-dependencies')) { $updateAllowTransitiveDependencies = Request::UPDATE_LISTED_WITH_TRANSITIVE_DEPS; } elseif ($input->getOption('with-dependencies')) { $updateAllowTransitiveDependencies = Request::UPDATE_LISTED_WITH_TRANSITIVE_DEPS_NO_ROOT_REQUIRE; } $ignorePlatformReqs = $input->getOption('ignore-platform-reqs') ?: ($input->getOption('ignore-platform-req') ?: false); $install ->setDryRun($input->getOption('dry-run')) ->setVerbose($input->getOption('verbose')) ->setPreferSource($preferSource) ->setPreferDist($preferDist) ->setDevMode(!$input->getOption('no-dev')) ->setDumpAutoloader(!$input->getOption('no-autoloader')) ->setOptimizeAutoloader($optimize) ->setClassMapAuthoritative($authoritative) ->setApcuAutoloader($apcu, $apcuPrefix) ->setUpdate(true) ->setInstall(!$input->getOption('no-install')) ->setUpdateMirrors($updateMirrors) ->setUpdateAllowList($packages) ->setUpdateAllowTransitiveDependencies($updateAllowTransitiveDependencies) ->setPlatformRequirementFilter(PlatformRequirementFilterFactory::fromBoolOrList($ignorePlatformReqs)) ->setPreferStable($input->getOption('prefer-stable')) ->setPreferLowest($input->getOption('prefer-lowest')) ; if ($input->getOption('no-plugins')) { $install->disablePlugins(); } return $install->run(); } /** * @param array $packages * @return array */ private function getPackagesInteractively(IOInterface $io, InputInterface $input, OutputInterface $output, Composer $composer, array $packages) { if (!$input->isInteractive()) { throw new \InvalidArgumentException('--interactive cannot be used in non-interactive terminals.'); } $requires = array_merge( $composer->getPackage()->getRequires(), $composer->getPackage()->getDevRequires() ); $autocompleterValues = array(); foreach ($requires as $require) { $target = $require->getTarget(); $autocompleterValues[strtolower($target)] = $target; } $installedPackages = $composer->getRepositoryManager()->getLocalRepository()->getPackages(); foreach ($installedPackages as $package) { $autocompleterValues[$package->getName()] = $package->getPrettyName(); } $helper = $this->getHelper('question'); $question = new Question('Enter package name: ', null); $io->writeError('Press enter without value to end submission'); do { $autocompleterValues = array_diff($autocompleterValues, $packages); $question->setAutocompleterValues($autocompleterValues); $addedPackage = $helper->ask($input, $output, $question); if (!is_string($addedPackage) || empty($addedPackage)) { break; } $addedPackage = strtolower($addedPackage); if (!in_array($addedPackage, $packages)) { $packages[] = $addedPackage; } } while (true); $packages = array_filter($packages); if (!$packages) { throw new \InvalidArgumentException('You must enter minimum one package.'); } $table = new Table($output); $table->setHeaders(array('Selected packages')); foreach ($packages as $package) { $table->addRow(array($package)); } $table->render(); if ($io->askConfirmation(sprintf( 'Would you like to continue and update the above package%s [yes]? ', 1 === count($packages) ? '' : 's' ))) { return $packages; } throw new \RuntimeException('Installation aborted.'); } /** * @param string $constraint * @return Link */ private function appendConstraintToLink(Link $link, $constraint) { $parser = new VersionParser; $oldPrettyString = $link->getConstraint()->getPrettyString(); $newConstraint = MultiConstraint::create(array($link->getConstraint(), $parser->parseConstraints($constraint))); $newConstraint->setPrettyString($oldPrettyString.', '.$constraint); return new Link( $link->getSource(), $link->getTarget(), $newConstraint, /** @phpstan-ignore-next-line */ $link->getDescription(), $link->getPrettyConstraint() . ', ' . $constraint ); } } * Jordi Boggiano * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Composer\Command; use Composer\Composer; use Composer\DependencyResolver\DefaultPolicy; use Composer\Filter\PlatformRequirementFilter\PlatformRequirementFilterFactory; use Composer\Json\JsonFile; use Composer\Package\BasePackage; use Composer\Package\CompletePackageInterface; use Composer\Package\Link; use Composer\Package\AliasPackage; use Composer\Package\Package; use Composer\Package\PackageInterface; use Composer\Package\Version\VersionParser; use Composer\Package\Version\VersionSelector; use Composer\Pcre\Preg; use Composer\Plugin\CommandEvent; use Composer\Plugin\PluginEvents; use Composer\Repository\InstalledArrayRepository; use Composer\Repository\ComposerRepository; use Composer\Repository\CompositeRepository; use Composer\Repository\FilterRepository; use Composer\Repository\PlatformRepository; use Composer\Repository\RepositoryFactory; use Composer\Repository\InstalledRepository; use Composer\Repository\RepositoryInterface; use Composer\Repository\RepositorySet; use Composer\Repository\RootPackageRepository; use Composer\Semver\Constraint\ConstraintInterface; use Composer\Semver\Semver; use Composer\Spdx\SpdxLicenses; use Symfony\Component\Console\Formatter\OutputFormatterStyle; use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; /** * @author Robert Schönthal * @author Jordi Boggiano * @author Jérémy Romey * @author Mihai Plasoianu */ class ShowCommand extends BaseCommand { /** @var VersionParser */ protected $versionParser; /** @var string[] */ protected $colors; /** @var ?RepositorySet */ private $repositorySet; /** * @return void */ protected function configure() { $this ->setName('show') ->setAliases(array('info')) ->setDescription('Shows information about packages.') ->setDefinition(array( new InputArgument('package', InputArgument::OPTIONAL, 'Package to inspect. Or a name including a wildcard (*) to filter lists of packages instead.'), new InputArgument('version', InputArgument::OPTIONAL, 'Version or version constraint to inspect'), new InputOption('all', null, InputOption::VALUE_NONE, 'List all packages'), new InputOption('locked', null, InputOption::VALUE_NONE, 'List all locked packages'), new InputOption('installed', 'i', InputOption::VALUE_NONE, 'List installed packages only (enabled by default, only present for BC).'), new InputOption('platform', 'p', InputOption::VALUE_NONE, 'List platform packages only'), new InputOption('available', 'a', InputOption::VALUE_NONE, 'List available packages only'), new InputOption('self', 's', InputOption::VALUE_NONE, 'Show the root package information'), new InputOption('name-only', 'N', InputOption::VALUE_NONE, 'List package names only'), new InputOption('path', 'P', InputOption::VALUE_NONE, 'Show package paths'), new InputOption('tree', 't', InputOption::VALUE_NONE, 'List the dependencies as a tree'), new InputOption('latest', 'l', InputOption::VALUE_NONE, 'Show the latest version'), new InputOption('outdated', 'o', InputOption::VALUE_NONE, 'Show the latest version but only for packages that are outdated'), new InputOption('ignore', null, InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY, 'Ignore specified package(s). Use it with the --outdated option if you don\'t want to be informed about new versions of some packages.'), new InputOption('minor-only', 'm', InputOption::VALUE_NONE, 'Show only packages that have minor SemVer-compatible updates. Use with the --outdated option.'), new InputOption('direct', 'D', InputOption::VALUE_NONE, 'Shows only packages that are directly required by the root package'), new InputOption('strict', null, InputOption::VALUE_NONE, 'Return a non-zero exit code when there are outdated packages'), new InputOption('format', 'f', InputOption::VALUE_REQUIRED, 'Format of the output: text or json', 'text'), new InputOption('no-dev', null, InputOption::VALUE_NONE, 'Disables search in require-dev packages.'), new InputOption('ignore-platform-req', null, InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY, 'Ignore a specific platform requirement (php & ext- packages). Use with the --outdated option'), new InputOption('ignore-platform-reqs', null, InputOption::VALUE_NONE, 'Ignore all platform requirements (php & ext- packages). Use with the --outdated option'), )) ->setHelp( <<versionParser = new VersionParser; if ($input->getOption('tree')) { $this->initStyles($output); } $composer = $this->getComposer(false); $io = $this->getIO(); if ($input->getOption('installed')) { $io->writeError('You are using the deprecated option "installed". Only installed packages are shown by default now. The --all option can be used to show all packages.'); } if ($input->getOption('outdated')) { $input->setOption('latest', true); } elseif ($input->getOption('ignore')) { $io->writeError('You are using the option "ignore" for action other than "outdated", it will be ignored.'); } if ($input->getOption('direct') && ($input->getOption('all') || $input->getOption('available') || $input->getOption('platform'))) { $io->writeError('The --direct (-D) option is not usable in combination with --all, --platform (-p) or --available (-a)'); return 1; } if ($input->getOption('tree') && ($input->getOption('all') || $input->getOption('available'))) { $io->writeError('The --tree (-t) option is not usable in combination with --all or --available (-a)'); return 1; } if ($input->getOption('tree') && $input->getOption('latest')) { $io->writeError('The --tree (-t) option is not usable in combination with --latest (-l)'); return 1; } if ($input->getOption('tree') && $input->getOption('path')) { $io->writeError('The --tree (-t) option is not usable in combination with --path (-P)'); return 1; } $format = $input->getOption('format'); if (!in_array($format, array('text', 'json'))) { $io->writeError(sprintf('Unsupported format "%s". See help for supported formats.', $format)); return 1; } $ignorePlatformReqs = $input->getOption('ignore-platform-reqs') ?: ($input->getOption('ignore-platform-req') ?: false); // init repos $platformOverrides = array(); if ($composer) { $platformOverrides = $composer->getConfig()->get('platform') ?: array(); } $platformRepo = new PlatformRepository(array(), $platformOverrides); $lockedRepo = null; if ($input->getOption('self')) { $package = $this->getComposer()->getPackage(); if ($input->getOption('name-only')) { $io->write($package->getName()); return 0; } $repos = $installedRepo = new InstalledRepository(array(new RootPackageRepository($package))); } elseif ($input->getOption('platform')) { $repos = $installedRepo = new InstalledRepository(array($platformRepo)); } elseif ($input->getOption('available')) { $installedRepo = new InstalledRepository(array($platformRepo)); if ($composer) { $repos = new CompositeRepository($composer->getRepositoryManager()->getRepositories()); $installedRepo->addRepository($composer->getRepositoryManager()->getLocalRepository()); } else { $defaultRepos = RepositoryFactory::defaultRepos($io); $repos = new CompositeRepository($defaultRepos); $io->writeError('No composer.json found in the current directory, showing available packages from ' . implode(', ', array_keys($defaultRepos))); } } elseif ($input->getOption('all') && $composer) { $localRepo = $composer->getRepositoryManager()->getLocalRepository(); $locker = $composer->getLocker(); if ($locker->isLocked()) { $lockedRepo = $locker->getLockedRepository(true); $installedRepo = new InstalledRepository(array($lockedRepo, $localRepo, $platformRepo)); } else { $installedRepo = new InstalledRepository(array($localRepo, $platformRepo)); } $repos = new CompositeRepository(array_merge(array(new FilterRepository($installedRepo, array('canonical' => false))), $composer->getRepositoryManager()->getRepositories())); } elseif ($input->getOption('all')) { $defaultRepos = RepositoryFactory::defaultRepos($io); $io->writeError('No composer.json found in the current directory, showing available packages from ' . implode(', ', array_keys($defaultRepos))); $installedRepo = new InstalledRepository(array($platformRepo)); $repos = new CompositeRepository(array_merge(array($installedRepo), $defaultRepos)); } elseif ($input->getOption('locked')) { if (!$composer || !$composer->getLocker()->isLocked()) { throw new \UnexpectedValueException('A valid composer.json and composer.lock files is required to run this command with --locked'); } $locker = $composer->getLocker(); $lockedRepo = $locker->getLockedRepository(!$input->getOption('no-dev')); $repos = $installedRepo = new InstalledRepository(array($lockedRepo)); } else { // --installed / default case if (!$composer) { $composer = $this->getComposer(); } $rootPkg = $composer->getPackage(); $repos = $installedRepo = new InstalledRepository(array($composer->getRepositoryManager()->getLocalRepository())); if ($input->getOption('no-dev')) { $packages = $this->filterRequiredPackages($installedRepo, $rootPkg); $repos = $installedRepo = new InstalledRepository(array(new InstalledArrayRepository(array_map(function ($pkg) { return clone $pkg; }, $packages)))); } if (!$installedRepo->getPackages() && ($rootPkg->getRequires() || $rootPkg->getDevRequires())) { $io->writeError('No dependencies installed. Try running composer install or update.'); } } if ($composer) { $commandEvent = new CommandEvent(PluginEvents::COMMAND, 'show', $input, $output); $composer->getEventDispatcher()->dispatch($commandEvent->getName(), $commandEvent); } if ($input->getOption('latest') && null === $composer) { $io->writeError('No composer.json found in the current directory, disabling "latest" option'); $input->setOption('latest', false); } $packageFilter = $input->getArgument('package'); // show single package or single version if (($packageFilter && false === strpos($packageFilter, '*')) || !empty($package)) { if (empty($package)) { list($package, $versions) = $this->getPackage($installedRepo, $repos, $input->getArgument('package'), $input->getArgument('version')); if (empty($package)) { $options = $input->getOptions(); if (!isset($options['working-dir']) || !file_exists('composer.json')) { if (PlatformRepository::isPlatformPackage($input->getArgument('package')) && !$input->getOption('platform')) { throw new \InvalidArgumentException('Package ' . $packageFilter . ' not found, try using --platform (-p) to show platform packages.'); } throw new \InvalidArgumentException('Package ' . $packageFilter . ' not found'); } $io->writeError('Package ' . $packageFilter . ' not found in ' . $options['working-dir'] . '/composer.json'); return 1; } } else { $versions = array($package->getPrettyVersion() => $package->getVersion()); } $exitCode = 0; if ($input->getOption('tree')) { $arrayTree = $this->generatePackageTree($package, $installedRepo, $repos); if ('json' === $format) { $io->write(JsonFile::encode(array('installed' => array($arrayTree)))); } else { $this->displayPackageTree(array($arrayTree)); } } else { $latestPackage = null; if ($input->getOption('latest')) { $latestPackage = $this->findLatestPackage($package, $composer, $platformRepo, $input->getOption('minor-only'), $ignorePlatformReqs); } if ( $input->getOption('outdated') && $input->getOption('strict') && $latestPackage && $latestPackage->getFullPrettyVersion() !== $package->getFullPrettyVersion() && (!$latestPackage instanceof CompletePackageInterface || !$latestPackage->isAbandoned()) ) { $exitCode = 1; } if ($input->getOption('path')) { $io->write($package->getName(), false); $io->write(' ' . strtok(realpath($composer->getInstallationManager()->getInstallPath($package)), "\r\n")); return $exitCode; } if ('json' === $format) { $this->printPackageInfoAsJson($package, $versions, $installedRepo, $latestPackage ?: null); } else { $this->printPackageInfo($package, $versions, $installedRepo, $latestPackage ?: null); } } return $exitCode; } // show tree view if requested if ($input->getOption('tree')) { $rootRequires = $this->getRootRequires(); $packages = $installedRepo->getPackages(); usort($packages, function (BasePackage $a, BasePackage $b) { return strcmp((string) $a, (string) $b); }); $arrayTree = array(); foreach ($packages as $package) { if (in_array($package->getName(), $rootRequires, true)) { $arrayTree[] = $this->generatePackageTree($package, $installedRepo, $repos); } } if ('json' === $format) { $io->write(JsonFile::encode(array('installed' => $arrayTree))); } else { $this->displayPackageTree($arrayTree); } return 0; } // list packages $packages = array(); $packageFilterRegex = null; if (null !== $packageFilter) { $packageFilterRegex = '{^'.str_replace('\\*', '.*?', preg_quote($packageFilter)).'$}i'; } $packageListFilter = array(); if ($input->getOption('direct')) { $packageListFilter = $this->getRootRequires(); } if ($input->getOption('path') && null === $composer) { $io->writeError('No composer.json found in the current directory, disabling "path" option'); $input->setOption('path', false); } foreach ($repos->getRepositories() as $repo) { if ($repo === $platformRepo) { $type = 'platform'; } elseif ($lockedRepo !== null && $repo === $lockedRepo) { $type = 'locked'; } elseif ($repo === $installedRepo || in_array($repo, $installedRepo->getRepositories(), true)) { $type = 'installed'; } else { $type = 'available'; } if ($repo instanceof ComposerRepository) { foreach ($repo->getPackageNames($packageFilter) as $name) { $packages[$type][$name] = $name; } } else { foreach ($repo->getPackages() as $package) { if (!isset($packages[$type][$package->getName()]) || !is_object($packages[$type][$package->getName()]) || version_compare($packages[$type][$package->getName()]->getVersion(), $package->getVersion(), '<') ) { while ($package instanceof AliasPackage) { $package = $package->getAliasOf(); } if (!$packageFilterRegex || Preg::isMatch($packageFilterRegex, $package->getName())) { if (!$packageListFilter || in_array($package->getName(), $packageListFilter, true)) { $packages[$type][$package->getName()] = $package; } } } } if ($repo === $platformRepo) { foreach ($platformRepo->getDisabledPackages() as $name => $package) { $packages[$type][$name] = $package; } } } } $showAllTypes = $input->getOption('all'); $showLatest = $input->getOption('latest'); $showMinorOnly = $input->getOption('minor-only'); $ignoredPackages = array_map('strtolower', $input->getOption('ignore')); $indent = $showAllTypes ? ' ' : ''; /** @var PackageInterface[] $latestPackages */ $latestPackages = array(); $exitCode = 0; $viewData = array(); $viewMetaData = array(); foreach (array('platform' => true, 'locked' => true, 'available' => false, 'installed' => true) as $type => $showVersion) { if (isset($packages[$type])) { ksort($packages[$type]); $nameLength = $versionLength = $latestLength = 0; if ($showLatest && $showVersion) { foreach ($packages[$type] as $package) { if (is_object($package)) { $latestPackage = $this->findLatestPackage($package, $composer, $platformRepo, $showMinorOnly, $ignorePlatformReqs); if ($latestPackage === false) { continue; } $latestPackages[$package->getPrettyName()] = $latestPackage; } } } $writePath = !$input->getOption('name-only') && $input->getOption('path'); $writeVersion = !$input->getOption('name-only') && !$input->getOption('path') && $showVersion; $writeLatest = $writeVersion && $showLatest; $writeDescription = !$input->getOption('name-only') && !$input->getOption('path'); $hasOutdatedPackages = false; $viewData[$type] = array(); foreach ($packages[$type] as $package) { $packageViewData = array(); if (is_object($package)) { $latestPackage = null; if ($showLatest && isset($latestPackages[$package->getPrettyName()])) { $latestPackage = $latestPackages[$package->getPrettyName()]; } // Determine if Composer is checking outdated dependencies and if current package should trigger non-default exit code $packageIsUpToDate = $latestPackage && $latestPackage->getFullPrettyVersion() === $package->getFullPrettyVersion() && (!$latestPackage instanceof CompletePackageInterface || !$latestPackage->isAbandoned()); $packageIsIgnored = \in_array($package->getPrettyName(), $ignoredPackages, true); if ($input->getOption('outdated') && ($packageIsUpToDate || $packageIsIgnored)) { continue; } if ($input->getOption('outdated') || $input->getOption('strict')) { $hasOutdatedPackages = true; } $packageViewData['name'] = $package->getPrettyName(); $nameLength = max($nameLength, strlen($package->getPrettyName())); if ($writeVersion) { $packageViewData['version'] = $package->getFullPrettyVersion(); $versionLength = max($versionLength, strlen($package->getFullPrettyVersion())); } if ($writeLatest && $latestPackage) { $packageViewData['latest'] = $latestPackage->getFullPrettyVersion(); $packageViewData['latest-status'] = $this->getUpdateStatus($latestPackage, $package); $latestLength = max($latestLength, strlen($latestPackage->getFullPrettyVersion())); } if ($writeDescription && $package instanceof CompletePackageInterface) { $packageViewData['description'] = $package->getDescription(); } if ($writePath) { $packageViewData['path'] = strtok(realpath($composer->getInstallationManager()->getInstallPath($package)), "\r\n"); } if ($latestPackage instanceof CompletePackageInterface && $latestPackage->isAbandoned()) { $replacement = is_string($latestPackage->getReplacementPackage()) ? 'Use ' . $latestPackage->getReplacementPackage() . ' instead' : 'No replacement was suggested'; $packageWarning = sprintf( 'Package %s is abandoned, you should avoid using it. %s.', $package->getPrettyName(), $replacement ); $packageViewData['warning'] = $packageWarning; } } else { $packageViewData['name'] = $package; $nameLength = max($nameLength, strlen($package)); } $viewData[$type][] = $packageViewData; } $viewMetaData[$type] = array( 'nameLength' => $nameLength, 'versionLength' => $versionLength, 'latestLength' => $latestLength, ); if ($input->getOption('strict') && $hasOutdatedPackages) { $exitCode = 1; break; } } } if ('json' === $format) { $io->write(JsonFile::encode($viewData)); } else { if ($input->getOption('latest') && array_filter($viewData)) { if (!$io->isDecorated()) { $io->writeError('Legend:'); $io->writeError('! patch or minor release available - update recommended'); $io->writeError('~ major release available - update possible'); if (!$input->getOption('outdated')) { $io->writeError('= up to date version'); } } else { $io->writeError('Color legend:'); $io->writeError('- patch or minor release available - update recommended'); $io->writeError('- major release available - update possible'); if (!$input->getOption('outdated')) { $io->writeError('- up to date version'); } } } $width = $this->getTerminalWidth(); foreach ($viewData as $type => $packages) { $nameLength = $viewMetaData[$type]['nameLength']; $versionLength = $viewMetaData[$type]['versionLength']; $latestLength = $viewMetaData[$type]['latestLength']; $writeVersion = $nameLength + $versionLength + 3 <= $width; $writeLatest = $nameLength + $versionLength + $latestLength + 3 <= $width; $writeDescription = $nameLength + $versionLength + $latestLength + 24 <= $width; if ($writeLatest && !$io->isDecorated()) { $latestLength += 2; } if ($showAllTypes) { if ('available' === $type) { $io->write('' . $type . ':'); } else { $io->write('' . $type . ':'); } } foreach ($packages as $package) { $io->write($indent . str_pad($package['name'], $nameLength, ' '), false); if (isset($package['version']) && $writeVersion) { $io->write(' ' . str_pad($package['version'], $versionLength, ' '), false); } if (isset($package['latest']) && $writeLatest) { $latestVersion = $package['latest']; $updateStatus = $package['latest-status']; $style = $this->updateStatusToVersionStyle($updateStatus); if (!$io->isDecorated()) { $latestVersion = str_replace(array('up-to-date', 'semver-safe-update', 'update-possible'), array('=', '!', '~'), $updateStatus) . ' ' . $latestVersion; } $io->write(' <' . $style . '>' . str_pad($latestVersion, $latestLength, ' ') . '', false); } if (isset($package['description']) && $writeDescription) { $description = strtok($package['description'], "\r\n"); $remaining = $width - $nameLength - $versionLength - 4; if ($writeLatest) { $remaining -= $latestLength; } if (strlen($description) > $remaining) { $description = substr($description, 0, $remaining - 3) . '...'; } $io->write(' ' . $description, false); } if (isset($package['path'])) { $io->write(' ' . $package['path'], false); } $io->write(''); if (isset($package['warning'])) { $io->write('' . $package['warning'] . ''); } } if ($showAllTypes) { $io->write(''); } } } return $exitCode; } /** * @return string[] */ protected function getRootRequires() { $rootPackage = $this->getComposer()->getPackage(); return array_map( 'strtolower', array_keys(array_merge($rootPackage->getRequires(), $rootPackage->getDevRequires())) ); } /** * @return array|string|string[] */ protected function getVersionStyle(PackageInterface $latestPackage, PackageInterface $package) { return $this->updateStatusToVersionStyle($this->getUpdateStatus($latestPackage, $package)); } /** * finds a package by name and version if provided * * @param string $name * @param ConstraintInterface|string $version * @throws \InvalidArgumentException * @return array{CompletePackageInterface|null, array} */ protected function getPackage(InstalledRepository $installedRepo, RepositoryInterface $repos, $name, $version = null) { $name = strtolower($name); $constraint = is_string($version) ? $this->versionParser->parseConstraints($version) : $version; $policy = new DefaultPolicy(); $repositorySet = new RepositorySet('dev'); $repositorySet->allowInstalledRepositories(); $repositorySet->addRepository($repos); $matchedPackage = null; $versions = array(); if (PlatformRepository::isPlatformPackage($name)) { $pool = $repositorySet->createPoolWithAllPackages(); } else { $pool = $repositorySet->createPoolForPackage($name); } $matches = $pool->whatProvides($name, $constraint); foreach ($matches as $index => $package) { // avoid showing the 9999999-dev alias if the default branch has no branch-alias set if ($package instanceof AliasPackage && $package->getVersion() === VersionParser::DEFAULT_BRANCH_ALIAS) { $package = $package->getAliasOf(); } // select an exact match if it is in the installed repo and no specific version was required if (null === $version && $installedRepo->hasPackage($package)) { $matchedPackage = $package; } $versions[$package->getPrettyVersion()] = $package->getVersion(); $matches[$index] = $package->getId(); } // select preferred package according to policy rules if (!$matchedPackage && $matches && $preferred = $policy->selectPreferredPackages($pool, $matches)) { $matchedPackage = $pool->literalToPackage($preferred[0]); } return array($matchedPackage, $versions); } /** * Prints package info. * * @param array $versions * @param PackageInterface|null $latestPackage * * @return void */ protected function printPackageInfo(CompletePackageInterface $package, array $versions, InstalledRepository $installedRepo, PackageInterface $latestPackage = null) { $io = $this->getIO(); $this->printMeta($package, $versions, $installedRepo, $latestPackage ?: null); $this->printLinks($package, Link::TYPE_REQUIRE); $this->printLinks($package, Link::TYPE_DEV_REQUIRE, 'requires (dev)'); if ($package->getSuggests()) { $io->write("\nsuggests"); foreach ($package->getSuggests() as $suggested => $reason) { $io->write($suggested . ' ' . $reason . ''); } } $this->printLinks($package, Link::TYPE_PROVIDE); $this->printLinks($package, Link::TYPE_CONFLICT); $this->printLinks($package, Link::TYPE_REPLACE); } /** * Prints package metadata. * * @param array $versions * @param PackageInterface|null $latestPackage * * @return void */ protected function printMeta(CompletePackageInterface $package, array $versions, InstalledRepository $installedRepo, PackageInterface $latestPackage = null) { $io = $this->getIO(); $io->write('name : ' . $package->getPrettyName()); $io->write('descrip. : ' . $package->getDescription()); $io->write('keywords : ' . implode(', ', $package->getKeywords() ?: array())); $this->printVersions($package, $versions, $installedRepo); if ($latestPackage) { $style = $this->getVersionStyle($latestPackage, $package); $io->write('latest : <'.$style.'>' . $latestPackage->getPrettyVersion() . ''); } else { $latestPackage = $package; } $io->write('type : ' . $package->getType()); $this->printLicenses($package); $io->write('homepage : ' . $package->getHomepage()); $io->write('source : ' . sprintf('[%s] %s %s', $package->getSourceType(), $package->getSourceUrl(), $package->getSourceReference())); $io->write('dist : ' . sprintf('[%s] %s %s', $package->getDistType(), $package->getDistUrl(), $package->getDistReference())); if ($installedRepo->hasPackage($package)) { $io->write('path : ' . sprintf('%s', realpath($this->getComposer()->getInstallationManager()->getInstallPath($package)))); } $io->write('names : ' . implode(', ', $package->getNames())); if ($latestPackage instanceof CompletePackageInterface && $latestPackage->isAbandoned()) { $replacement = ($latestPackage->getReplacementPackage() !== null) ? ' The author suggests using the ' . $latestPackage->getReplacementPackage(). ' package instead.' : null; $io->writeError( sprintf('Attention: This package is abandoned and no longer maintained.%s', $replacement) ); } if ($package->getSupport()) { $io->write("\nsupport"); foreach ($package->getSupport() as $type => $value) { $io->write('' . $type . ' : '.$value); } } if ($package->getAutoload()) { $io->write("\nautoload"); $autoloadConfig = $package->getAutoload(); foreach ($autoloadConfig as $type => $autoloads) { $io->write('' . $type . ''); if ($type === 'psr-0' || $type === 'psr-4') { foreach ($autoloads as $name => $path) { $io->write(($name ?: '*') . ' => ' . (is_array($path) ? implode(', ', $path) : ($path ?: '.'))); } } elseif ($type === 'classmap') { $io->write(implode(', ', $autoloadConfig[$type])); } } if ($package->getIncludePaths()) { $io->write('include-path'); $io->write(implode(', ', $package->getIncludePaths())); } } } /** * Prints all available versions of this package and highlights the installed one if any. * * @param array $versions * * @return void */ protected function printVersions(CompletePackageInterface $package, array $versions, InstalledRepository $installedRepo) { $versions = array_keys($versions); $versions = Semver::rsort($versions); // highlight installed version if ($installedPackages = $installedRepo->findPackages($package->getName())) { foreach ($installedPackages as $installedPackage) { $installedVersion = $installedPackage->getPrettyVersion(); $key = array_search($installedVersion, $versions); if (false !== $key) { $versions[$key] = '* ' . $installedVersion . ''; } } } $versions = implode(', ', $versions); $this->getIO()->write('versions : ' . $versions); } /** * print link objects * * @param string $linkType * @param string $title * * @return void */ protected function printLinks(CompletePackageInterface $package, $linkType, $title = null) { $title = $title ?: $linkType; $io = $this->getIO(); if ($links = $package->{'get'.ucfirst($linkType)}()) { $io->write("\n" . $title . ""); foreach ($links as $link) { $io->write($link->getTarget() . ' ' . $link->getPrettyConstraint() . ''); } } } /** * Prints the licenses of a package with metadata * * @return void */ protected function printLicenses(CompletePackageInterface $package) { $spdxLicenses = new SpdxLicenses(); $licenses = $package->getLicense(); $io = $this->getIO(); foreach ($licenses as $licenseId) { $license = $spdxLicenses->getLicenseByIdentifier($licenseId); // keys: 0 fullname, 1 osi, 2 url if (!$license) { $out = $licenseId; } else { // is license OSI approved? if ($license[1] === true) { $out = sprintf('%s (%s) (OSI approved) %s', $license[0], $licenseId, $license[2]); } else { $out = sprintf('%s (%s) %s', $license[0], $licenseId, $license[2]); } } $io->write('license : ' . $out); } } /** * Prints package info in JSON format. * * @param array $versions * * @return void */ protected function printPackageInfoAsJson(CompletePackageInterface $package, array $versions, InstalledRepository $installedRepo, PackageInterface $latestPackage = null) { $json = array( 'name' => $package->getPrettyName(), 'description' => $package->getDescription(), 'keywords' => $package->getKeywords() ?: array(), 'type' => $package->getType(), 'homepage' => $package->getHomepage(), 'names' => $package->getNames(), ); $json = $this->appendVersions($json, $versions); $json = $this->appendLicenses($json, $package); if ($latestPackage) { $json['latest'] = $latestPackage->getPrettyVersion(); } else { $latestPackage = $package; } if ($package->getSourceType()) { $json['source'] = array( 'type' => $package->getSourceType(), 'url' => $package->getSourceUrl(), 'reference' => $package->getSourceReference(), ); } if ($package->getDistType()) { $json['dist'] = array( 'type' => $package->getDistType(), 'url' => $package->getDistUrl(), 'reference' => $package->getDistReference(), ); } if ($installedRepo->hasPackage($package)) { $json['path'] = realpath($this->getComposer()->getInstallationManager()->getInstallPath($package)); if ($json['path'] === false) { unset($json['path']); } } if ($latestPackage instanceof CompletePackageInterface && $latestPackage->isAbandoned()) { $json['replacement'] = $latestPackage->getReplacementPackage(); } if ($package->getSuggests()) { $json['suggests'] = $package->getSuggests(); } if ($package->getSupport()) { $json['support'] = $package->getSupport(); } $json = $this->appendAutoload($json, $package); if ($package->getIncludePaths()) { $json['include_path'] = $package->getIncludePaths(); } $json = $this->appendLinks($json, $package); $this->getIO()->write(JsonFile::encode($json)); } /** * @param array $json * @param array $versions * @return array */ private function appendVersions($json, array $versions) { uasort($versions, 'version_compare'); $versions = array_keys(array_reverse($versions)); $json['versions'] = $versions; return $json; } /** * @param array $json * @return array */ private function appendLicenses($json, CompletePackageInterface $package) { if ($licenses = $package->getLicense()) { $spdxLicenses = new SpdxLicenses(); $json['licenses'] = array_map(function ($licenseId) use ($spdxLicenses) { $license = $spdxLicenses->getLicenseByIdentifier($licenseId); // keys: 0 fullname, 1 osi, 2 url if (!$license) { return $licenseId; } return array( 'name' => $license[0], 'osi' => $licenseId, 'url' => $license[2], ); }, $licenses); } return $json; } /** * @param array $json * @return array */ private function appendAutoload($json, CompletePackageInterface $package) { if ($package->getAutoload()) { $autoload = array(); foreach ($package->getAutoload() as $type => $autoloads) { if ($type === 'psr-0' || $type === 'psr-4') { $psr = array(); foreach ($autoloads as $name => $path) { if (!$path) { $path = '.'; } $psr[$name ?: '*'] = $path; } $autoload[$type] = $psr; } elseif ($type === 'classmap') { $autoload['classmap'] = $autoloads; } } $json['autoload'] = $autoload; } return $json; } /** * @param array $json * @return array */ private function appendLinks($json, CompletePackageInterface $package) { foreach (Link::$TYPES as $linkType) { $json = $this->appendLink($json, $package, $linkType); } return $json; } /** * @param array $json * @param string $linkType * @return array */ private function appendLink($json, CompletePackageInterface $package, $linkType) { $links = $package->{'get' . ucfirst($linkType)}(); if ($links) { $json[$linkType] = array(); foreach ($links as $link) { $json[$linkType][$link->getTarget()] = $link->getPrettyConstraint(); } } return $json; } /** * Init styles for tree * * @return void */ protected function initStyles(OutputInterface $output) { $this->colors = array( 'green', 'yellow', 'cyan', 'magenta', 'blue', ); foreach ($this->colors as $color) { $style = new OutputFormatterStyle($color); $output->getFormatter()->setStyle($color, $style); } } /** * Display the tree * * @param array> $arrayTree * @return void */ protected function displayPackageTree(array $arrayTree) { $io = $this->getIO(); foreach ($arrayTree as $package) { $io->write(sprintf('%s', $package['name']), false); $io->write(' ' . $package['version'], false); $io->write(' ' . strtok($package['description'], "\r\n")); if (isset($package['requires'])) { $requires = $package['requires']; $treeBar = '├'; $j = 0; $total = count($requires); foreach ($requires as $require) { $requireName = $require['name']; $j++; if ($j === $total) { $treeBar = '└'; } $level = 1; $color = $this->colors[$level]; $info = sprintf( '%s──<%s>%s %s', $treeBar, $color, $requireName, $color, $require['version'] ); $this->writeTreeLine($info); $treeBar = str_replace('└', ' ', $treeBar); $packagesInTree = array($package['name'], $requireName); $this->displayTree($require, $packagesInTree, $treeBar, $level + 1); } } } } /** * Generate the package tree * * @return array>|string|null> */ protected function generatePackageTree( PackageInterface $package, InstalledRepository $installedRepo, RepositoryInterface $remoteRepos ) { $requires = $package->getRequires(); ksort($requires); $children = array(); foreach ($requires as $requireName => $require) { $packagesInTree = array($package->getName(), $requireName); $treeChildDesc = array( 'name' => $requireName, 'version' => $require->getPrettyConstraint(), ); $deepChildren = $this->addTree($requireName, $require, $installedRepo, $remoteRepos, $packagesInTree); if ($deepChildren) { $treeChildDesc['requires'] = $deepChildren; } $children[] = $treeChildDesc; } $tree = array( 'name' => $package->getPrettyName(), 'version' => $package->getPrettyVersion(), 'description' => $package instanceof CompletePackageInterface ? $package->getDescription() : '', ); if ($children) { $tree['requires'] = $children; } return $tree; } /** * Display a package tree * * @param array>|string|null>|string $package * @param array $packagesInTree * @param string $previousTreeBar * @param int $level * * @return void */ protected function displayTree( $package, array $packagesInTree, $previousTreeBar = '├', $level = 1 ) { $previousTreeBar = str_replace('├', '│', $previousTreeBar); if (is_array($package) && isset($package['requires'])) { $requires = $package['requires']; $treeBar = $previousTreeBar . ' ├'; $i = 0; $total = count($requires); foreach ($requires as $require) { $currentTree = $packagesInTree; $i++; if ($i === $total) { $treeBar = $previousTreeBar . ' └'; } $colorIdent = $level % count($this->colors); $color = $this->colors[$colorIdent]; $circularWarn = in_array( $require['name'], $currentTree, true ) ? '(circular dependency aborted here)' : ''; $info = rtrim(sprintf( '%s──<%s>%s %s %s', $treeBar, $color, $require['name'], $color, $require['version'], $circularWarn )); $this->writeTreeLine($info); $treeBar = str_replace('└', ' ', $treeBar); $currentTree[] = $require['name']; $this->displayTree($require, $currentTree, $treeBar, $level + 1); } } } /** * Display a package tree * * @param string $name * @param string[] $packagesInTree * @return array>|string>> */ protected function addTree( $name, Link $link, InstalledRepository $installedRepo, RepositoryInterface $remoteRepos, array $packagesInTree ) { $children = array(); list($package) = $this->getPackage( $installedRepo, $remoteRepos, $name, $link->getPrettyConstraint() === 'self.version' ? $link->getConstraint() : $link->getPrettyConstraint() ); if (is_object($package)) { $requires = $package->getRequires(); ksort($requires); foreach ($requires as $requireName => $require) { $currentTree = $packagesInTree; $treeChildDesc = array( 'name' => $requireName, 'version' => $require->getPrettyConstraint(), ); if (!in_array($requireName, $currentTree, true)) { $currentTree[] = $requireName; $deepChildren = $this->addTree($requireName, $require, $installedRepo, $remoteRepos, $currentTree); if ($deepChildren) { $treeChildDesc['requires'] = $deepChildren; } } $children[] = $treeChildDesc; } } return $children; } /** * @param string $updateStatus * @return string */ private function updateStatusToVersionStyle($updateStatus) { // 'up-to-date' is printed green // 'semver-safe-update' is printed red // 'update-possible' is printed yellow return str_replace(array('up-to-date', 'semver-safe-update', 'update-possible'), array('info', 'highlight', 'comment'), $updateStatus); } /** * @return string */ private function getUpdateStatus(PackageInterface $latestPackage, PackageInterface $package) { if ($latestPackage->getFullPrettyVersion() === $package->getFullPrettyVersion()) { return 'up-to-date'; } $constraint = $package->getVersion(); if (0 !== strpos($constraint, 'dev-')) { $constraint = '^'.$constraint; } if ($latestPackage->getVersion() && Semver::satisfies($latestPackage->getVersion(), $constraint)) { // it needs an immediate semver-compliant upgrade return 'semver-safe-update'; } // it needs an upgrade but has potential BC breaks so is not urgent return 'update-possible'; } /** * @param string $line * * @return void */ private function writeTreeLine($line) { $io = $this->getIO(); if (!$io->isDecorated()) { $line = str_replace(array('└', '├', '──', '│'), array('`-', '|-', '-', '|'), $line); } $io->write($line); } /** * Given a package, this finds the latest package matching it * * @param bool $minorOnly * @param bool|string $ignorePlatformReqs * * @return PackageInterface|false */ private function findLatestPackage(PackageInterface $package, Composer $composer, PlatformRepository $platformRepo, $minorOnly = false, $ignorePlatformReqs = false) { // find the latest version allowed in this repo set $name = $package->getName(); $versionSelector = new VersionSelector($this->getRepositorySet($composer), $platformRepo); $stability = $composer->getPackage()->getMinimumStability(); $flags = $composer->getPackage()->getStabilityFlags(); if (isset($flags[$name])) { $stability = array_search($flags[$name], BasePackage::$stabilities, true); } $bestStability = $stability; if ($composer->getPackage()->getPreferStable()) { $bestStability = $package->getStability(); } $targetVersion = null; if (0 === strpos($package->getVersion(), 'dev-')) { $targetVersion = $package->getVersion(); } if ($targetVersion === null && $minorOnly) { $targetVersion = '^' . $package->getVersion(); } $candidate = $versionSelector->findBestCandidate($name, $targetVersion, $bestStability, PlatformRequirementFilterFactory::fromBoolOrList($ignorePlatformReqs)); while ($candidate instanceof AliasPackage) { $candidate = $candidate->getAliasOf(); } return $candidate; } /** * @return RepositorySet */ private function getRepositorySet(Composer $composer) { if (!$this->repositorySet) { $this->repositorySet = new RepositorySet($composer->getPackage()->getMinimumStability(), $composer->getPackage()->getStabilityFlags()); $this->repositorySet->addRepository(new CompositeRepository($composer->getRepositoryManager()->getRepositories())); } return $this->repositorySet; } /** * Find package requires and child requires * * @param array $bucket * @return array */ private function filterRequiredPackages(RepositoryInterface $repo, PackageInterface $package, $bucket = array()) { $requires = $package->getRequires(); foreach ($repo->getPackages() as $candidate) { foreach ($candidate->getNames() as $name) { if (isset($requires[$name])) { if (!in_array($candidate, $bucket, true)) { $bucket[] = $candidate; $bucket = $this->filterRequiredPackages($repo, $candidate, $bucket); } break; } } } return $bucket; } } * Jordi Boggiano * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Composer\Command; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; use Composer\Downloader\ChangeReportInterface; use Composer\Downloader\DvcsDownloaderInterface; use Composer\Downloader\VcsCapableDownloaderInterface; use Composer\Package\Dumper\ArrayDumper; use Composer\Package\Version\VersionGuesser; use Composer\Package\Version\VersionParser; use Composer\Plugin\CommandEvent; use Composer\Plugin\PluginEvents; use Composer\Script\ScriptEvents; use Composer\Util\ProcessExecutor; /** * @author Tiago Ribeiro * @author Rui Marinho */ class StatusCommand extends BaseCommand { const EXIT_CODE_ERRORS = 1; const EXIT_CODE_UNPUSHED_CHANGES = 2; const EXIT_CODE_VERSION_CHANGES = 4; /** * @return void * @throws \Symfony\Component\Console\Exception\InvalidArgumentException */ protected function configure() { $this ->setName('status') ->setDescription('Shows a list of locally modified packages.') ->setDefinition(array( new InputOption('verbose', 'v|vv|vvv', InputOption::VALUE_NONE, 'Show modified files for each directory that contains changes.'), )) ->setHelp( <<getComposer(); $commandEvent = new CommandEvent(PluginEvents::COMMAND, 'status', $input, $output); $composer->getEventDispatcher()->dispatch($commandEvent->getName(), $commandEvent); // Dispatch pre-status-command $composer->getEventDispatcher()->dispatchScript(ScriptEvents::PRE_STATUS_CMD, true); $exitCode = $this->doExecute($input); // Dispatch post-status-command $composer->getEventDispatcher()->dispatchScript(ScriptEvents::POST_STATUS_CMD, true); return $exitCode; } /** * @return int */ private function doExecute(InputInterface $input) { // init repos $composer = $this->getComposer(); $installedRepo = $composer->getRepositoryManager()->getLocalRepository(); $dm = $composer->getDownloadManager(); $im = $composer->getInstallationManager(); $errors = array(); $io = $this->getIO(); $unpushedChanges = array(); $vcsVersionChanges = array(); $parser = new VersionParser; $guesser = new VersionGuesser($composer->getConfig(), new ProcessExecutor($io), $parser); $dumper = new ArrayDumper; // list packages foreach ($installedRepo->getCanonicalPackages() as $package) { $downloader = $dm->getDownloaderForPackage($package); $targetDir = $im->getInstallPath($package); if ($downloader instanceof ChangeReportInterface) { if (is_link($targetDir)) { $errors[$targetDir] = $targetDir . ' is a symbolic link.'; } if ($changes = $downloader->getLocalChanges($package, $targetDir)) { $errors[$targetDir] = $changes; } } if ($downloader instanceof VcsCapableDownloaderInterface) { if ($downloader->getVcsReference($package, $targetDir)) { switch ($package->getInstallationSource()) { case 'source': $previousRef = $package->getSourceReference(); break; case 'dist': $previousRef = $package->getDistReference(); break; default: $previousRef = null; } $currentVersion = $guesser->guessVersion($dumper->dump($package), $targetDir); if ($previousRef && $currentVersion && $currentVersion['commit'] !== $previousRef) { $vcsVersionChanges[$targetDir] = array( 'previous' => array( 'version' => $package->getPrettyVersion(), 'ref' => $previousRef, ), 'current' => array( 'version' => $currentVersion['pretty_version'], 'ref' => $currentVersion['commit'], ), ); } } } if ($downloader instanceof DvcsDownloaderInterface) { if ($unpushed = $downloader->getUnpushedChanges($package, $targetDir)) { $unpushedChanges[$targetDir] = $unpushed; } } } // output errors/warnings if (!$errors && !$unpushedChanges && !$vcsVersionChanges) { $io->writeError('No local changes'); return 0; } if ($errors) { $io->writeError('You have changes in the following dependencies:'); foreach ($errors as $path => $changes) { if ($input->getOption('verbose')) { $indentedChanges = implode("\n", array_map(function ($line) { return ' ' . ltrim($line); }, explode("\n", $changes))); $io->write(''.$path.':'); $io->write($indentedChanges); } else { $io->write($path); } } } if ($unpushedChanges) { $io->writeError('You have unpushed changes on the current branch in the following dependencies:'); foreach ($unpushedChanges as $path => $changes) { if ($input->getOption('verbose')) { $indentedChanges = implode("\n", array_map(function ($line) { return ' ' . ltrim($line); }, explode("\n", $changes))); $io->write(''.$path.':'); $io->write($indentedChanges); } else { $io->write($path); } } } if ($vcsVersionChanges) { $io->writeError('You have version variations in the following dependencies:'); foreach ($vcsVersionChanges as $path => $changes) { if ($input->getOption('verbose')) { // If we don't can't find a version, use the ref instead. $currentVersion = $changes['current']['version'] ?: $changes['current']['ref']; $previousVersion = $changes['previous']['version'] ?: $changes['previous']['ref']; if ($io->isVeryVerbose()) { // Output the ref regardless of whether or not it's being used as the version $currentVersion .= sprintf(' (%s)', $changes['current']['ref']); $previousVersion .= sprintf(' (%s)', $changes['previous']['ref']); } $io->write(''.$path.':'); $io->write(sprintf(' From %s to %s', $previousVersion, $currentVersion)); } else { $io->write($path); } } } if (($errors || $unpushedChanges || $vcsVersionChanges) && !$input->getOption('verbose')) { $io->writeError('Use --verbose (-v) to see a list of files'); } return ($errors ? self::EXIT_CODE_ERRORS : 0) + ($unpushedChanges ? self::EXIT_CODE_UNPUSHED_CHANGES : 0) + ($vcsVersionChanges ? self::EXIT_CODE_VERSION_CHANGES : 0); } } * Jordi Boggiano * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Composer\Command; use Composer\Json\JsonFile; use Composer\Package\CompletePackageInterface; use Composer\Plugin\CommandEvent; use Composer\Plugin\PluginEvents; use Composer\Package\PackageInterface; use Composer\Repository\RepositoryInterface; use Symfony\Component\Console\Helper\Table; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Console\Style\SymfonyStyle; /** * @author Benoît Merlet */ class LicensesCommand extends BaseCommand { /** * @return void */ protected function configure() { $this ->setName('licenses') ->setDescription('Shows information about licenses of dependencies.') ->setDefinition(array( new InputOption('format', 'f', InputOption::VALUE_REQUIRED, 'Format of the output: text, json or summary', 'text'), new InputOption('no-dev', null, InputOption::VALUE_NONE, 'Disables search in require-dev packages.'), )) ->setHelp( <<getComposer(); $commandEvent = new CommandEvent(PluginEvents::COMMAND, 'licenses', $input, $output); $composer->getEventDispatcher()->dispatch($commandEvent->getName(), $commandEvent); $root = $composer->getPackage(); $repo = $composer->getRepositoryManager()->getLocalRepository(); if ($input->getOption('no-dev')) { $packages = $this->filterRequiredPackages($repo, $root); } else { $packages = $this->appendPackages($repo->getPackages(), array()); } ksort($packages); $io = $this->getIO(); switch ($format = $input->getOption('format')) { case 'text': $io->write('Name: '.$root->getPrettyName().''); $io->write('Version: '.$root->getFullPrettyVersion().''); $io->write('Licenses: '.(implode(', ', $root->getLicense()) ?: 'none').''); $io->write('Dependencies:'); $io->write(''); $table = new Table($output); $table->setStyle('compact'); $tableStyle = $table->getStyle(); if (method_exists($tableStyle, 'setVerticalBorderChars')) { $tableStyle->setVerticalBorderChars(''); } else { // TODO remove in composer 2.2 // @phpstan-ignore-next-line $tableStyle->setVerticalBorderChar(''); } $tableStyle->setCellRowContentFormat('%s '); $table->setHeaders(array('Name', 'Version', 'License')); foreach ($packages as $package) { $table->addRow(array( $package->getPrettyName(), $package->getFullPrettyVersion(), implode(', ', $package instanceof CompletePackageInterface ? $package->getLicense() : array()) ?: 'none', )); } $table->render(); break; case 'json': $dependencies = array(); foreach ($packages as $package) { $dependencies[$package->getPrettyName()] = array( 'version' => $package->getFullPrettyVersion(), 'license' => $package instanceof CompletePackageInterface ? $package->getLicense() : array(), ); } $io->write(JsonFile::encode(array( 'name' => $root->getPrettyName(), 'version' => $root->getFullPrettyVersion(), 'license' => $root->getLicense(), 'dependencies' => $dependencies, ))); break; case 'summary': $usedLicenses = array(); foreach ($packages as $package) { $license = $package instanceof CompletePackageInterface ? $package->getLicense() : array(); $licenseName = $license[0]; if (!isset($usedLicenses[$licenseName])) { $usedLicenses[$licenseName] = 0; } $usedLicenses[$licenseName]++; } // Sort licenses so that the most used license will appear first arsort($usedLicenses, SORT_NUMERIC); $rows = array(); foreach ($usedLicenses as $usedLicense => $numberOfDependencies) { $rows[] = array($usedLicense, $numberOfDependencies); } $symfonyIo = new SymfonyStyle($input, $output); $symfonyIo->table( array('License', 'Number of dependencies'), $rows ); break; default: throw new \RuntimeException(sprintf('Unsupported format "%s". See help for supported formats.', $format)); } return 0; } /** * Find package requires and child requires * * @param array $bucket * @return array */ private function filterRequiredPackages(RepositoryInterface $repo, PackageInterface $package, $bucket = array()) { $requires = array_keys($package->getRequires()); $packageListNames = array_keys($bucket); $packages = array_filter( $repo->getPackages(), function ($package) use ($requires, $packageListNames) { return in_array($package->getName(), $requires) && !in_array($package->getName(), $packageListNames); } ); $bucket = $this->appendPackages($packages, $bucket); foreach ($packages as $package) { $bucket = $this->filterRequiredPackages($repo, $package, $bucket); } return $bucket; } /** * Adds packages to the package list * * @param PackageInterface[] $packages the list of packages to add * @param array $bucket the list to add packages to * @return array */ public function appendPackages(array $packages, array $bucket) { foreach ($packages as $package) { $bucket[$package->getName()] = $package; } return $bucket; } } * Jordi Boggiano * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Composer\Command; use Composer\Json\JsonFile; use Composer\Package\AliasPackage; use Composer\Package\BasePackage; use Composer\Package\CompletePackageInterface; use Composer\Pcre\Preg; use Composer\Repository\CompositeRepository; use Composer\Semver\Constraint\MatchAllConstraint; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; /** * @author Nicolas Grekas * @author Jordi Boggiano */ class FundCommand extends BaseCommand { /** * @return void */ protected function configure() { $this->setName('fund') ->setDescription('Discover how to help fund the maintenance of your dependencies.') ->setDefinition(array( new InputOption('format', 'f', InputOption::VALUE_REQUIRED, 'Format of the output: text or json', 'text'), )) ; } /** * @return int */ protected function execute(InputInterface $input, OutputInterface $output) { $composer = $this->getComposer(); $repo = $composer->getRepositoryManager()->getLocalRepository(); $remoteRepos = new CompositeRepository($composer->getRepositoryManager()->getRepositories()); $fundings = array(); $packagesToLoad = array(); foreach ($repo->getPackages() as $package) { if ($package instanceof AliasPackage) { continue; } $packagesToLoad[$package->getName()] = new MatchAllConstraint(); } // load all packages dev versions in parallel $result = $remoteRepos->loadPackages($packagesToLoad, array('dev' => BasePackage::STABILITY_DEV), array()); // collect funding data from default branches foreach ($result['packages'] as $package) { if ( !$package instanceof AliasPackage && $package instanceof CompletePackageInterface && $package->isDefaultBranch() && $package->getFunding() && isset($packagesToLoad[$package->getName()]) ) { $fundings = $this->insertFundingData($fundings, $package); unset($packagesToLoad[$package->getName()]); } } // collect funding from installed packages if none was found in the default branch above foreach ($repo->getPackages() as $package) { if ($package instanceof AliasPackage || !isset($packagesToLoad[$package->getName()])) { continue; } if ($package instanceof CompletePackageInterface && $package->getFunding()) { $fundings = $this->insertFundingData($fundings, $package); } } ksort($fundings); $io = $this->getIO(); $format = $input->getOption('format'); if (!in_array($format, array('text', 'json'))) { $io->writeError(sprintf('Unsupported format "%s". See help for supported formats.', $format)); return 1; } if ($fundings && $format === 'text') { $prev = null; $io->write('The following packages were found in your dependencies which publish funding information:'); foreach ($fundings as $vendor => $links) { $io->write(''); $io->write(sprintf("%s", $vendor)); foreach ($links as $url => $packages) { $line = sprintf(' %s', implode(', ', $packages)); if ($prev !== $line) { $io->write($line); $prev = $line; } $io->write(sprintf(' %s', $url)); } } $io->write(""); $io->write("Please consider following these links and sponsoring the work of package authors!"); $io->write("Thank you!"); } elseif ($format === 'json') { $io->write(JsonFile::encode($fundings)); } else { $io->write("No funding links were found in your package dependencies. This doesn't mean they don't need your support!"); } return 0; } /** * @param mixed[] $fundings * @return mixed[] */ private function insertFundingData(array $fundings, CompletePackageInterface $package) { foreach ($package->getFunding() as $fundingOption) { list($vendor, $packageName) = explode('/', $package->getPrettyName()); // ignore malformed funding entries if (empty($fundingOption['url'])) { continue; } $url = $fundingOption['url']; if (!empty($fundingOption['type']) && $fundingOption['type'] === 'github' && Preg::isMatch('{^https://github.com/([^/]+)$}', $url, $match)) { $url = 'https://github.com/sponsors/'.$match[1]; } $fundings[$vendor][$url][] = $packageName; } return $fundings; } } * Jordi Boggiano * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Composer\Command; use Composer\Package\Link; use Composer\Semver\Constraint\Constraint; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; use Composer\Repository\PlatformRepository; use Composer\Repository\RootPackageRepository; use Composer\Repository\InstalledRepository; class CheckPlatformReqsCommand extends BaseCommand { /** * @return void */ protected function configure() { $this->setName('check-platform-reqs') ->setDescription('Check that platform requirements are satisfied.') ->setDefinition(array( new InputOption('no-dev', null, InputOption::VALUE_NONE, 'Disables checking of require-dev packages requirements.'), new InputOption('lock', null, InputOption::VALUE_NONE, 'Checks requirements only from the lock file, not from installed packages.'), )) ->setHelp( <<php composer.phar check-platform-reqs EOT ); } /** * @return int */ protected function execute(InputInterface $input, OutputInterface $output) { $composer = $this->getComposer(); $requires = array(); $removePackages = array(); if ($input->getOption('lock')) { $this->getIO()->writeError('Checking '.($input->getOption('no-dev') ? 'non-dev ' : '').'platform requirements using the lock file'); $installedRepo = $composer->getLocker()->getLockedRepository(!$input->getOption('no-dev')); } else { $installedRepo = $composer->getRepositoryManager()->getLocalRepository(); // fallback to lockfile if installed repo is empty if (!$installedRepo->getPackages()) { $this->getIO()->writeError('No vendor dir present, checking '.($input->getOption('no-dev') ? 'non-dev ' : '').'platform requirements from the lock file'); $installedRepo = $composer->getLocker()->getLockedRepository(!$input->getOption('no-dev')); } else { if ($input->getOption('no-dev')) { $removePackages = $installedRepo->getDevPackageNames(); } $this->getIO()->writeError('Checking '.($input->getOption('no-dev') ? 'non-dev ' : '').'platform requirements for packages in the vendor dir'); } } if (!$input->getOption('no-dev')) { $requires += $composer->getPackage()->getDevRequires(); } foreach ($requires as $require => $link) { $requires[$require] = array($link); } $installedRepo = new InstalledRepository(array($installedRepo, new RootPackageRepository($composer->getPackage()))); foreach ($installedRepo->getPackages() as $package) { if (in_array($package->getName(), $removePackages, true)) { continue; } foreach ($package->getRequires() as $require => $link) { $requires[$require][] = $link; } } ksort($requires); $installedRepo->addRepository(new PlatformRepository(array(), array())); $results = array(); $exitCode = 0; /** * @var Link[] $links */ foreach ($requires as $require => $links) { if (PlatformRepository::isPlatformPackage($require)) { $candidates = $installedRepo->findPackagesWithReplacersAndProviders($require); if ($candidates) { $reqResults = array(); foreach ($candidates as $candidate) { $candidateConstraint = null; if ($candidate->getName() === $require) { $candidateConstraint = new Constraint('=', $candidate->getVersion()); $candidateConstraint->setPrettyString($candidate->getPrettyVersion()); } else { foreach (array_merge($candidate->getProvides(), $candidate->getReplaces()) as $link) { if ($link->getTarget() === $require) { $candidateConstraint = $link->getConstraint(); break; } } } // safety check for phpstan, but it should not be possible to get a candidate out of findPackagesWithReplacersAndProviders without a constraint matching $require if (!$candidateConstraint) { continue; } foreach ($links as $link) { if (!$link->getConstraint()->matches($candidateConstraint)) { $reqResults[] = array( $candidate->getName() === $require ? $candidate->getPrettyName() : $require, $candidateConstraint->getPrettyString(), $link, 'failed'.($candidate->getName() === $require ? '' : ' provided by '.$candidate->getPrettyName().''), ); // skip to next candidate continue 2; } } $results[] = array( $candidate->getName() === $require ? $candidate->getPrettyName() : $require, $candidateConstraint->getPrettyString(), null, 'success'.($candidate->getName() === $require ? '' : ' provided by '.$candidate->getPrettyName().''), ); // candidate matched, skip to next requirement continue 2; } // show the first error from every failed candidate $results = array_merge($results, $reqResults); $exitCode = max($exitCode, 1); continue; } $results[] = array( $require, 'n/a', $links[0], 'missing', ); $exitCode = max($exitCode, 2); } } $this->printTable($output, $results); return $exitCode; } /** * @param mixed[] $results * * @return void */ protected function printTable(OutputInterface $output, $results) { $rows = array(); foreach ($results as $result) { /** * @var Link|null $link */ list($platformPackage, $version, $link, $status) = $result; $rows[] = array( $platformPackage, $version, $link ? sprintf('%s %s %s (%s)', $link->getSource(), $link->getDescription(), $link->getTarget(), $link->getPrettyConstraint()) : '', $status, ); } $this->renderTable($rows, $output); } } * Jordi Boggiano * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Composer\Command; use Composer\Factory; use Composer\Pcre\Preg; use Composer\Util\Filesystem; use Composer\Util\Platform; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\StringInput; use Symfony\Component\Console\Output\OutputInterface; /** * @author Jordi Boggiano */ class GlobalCommand extends BaseCommand { /** * @return void */ protected function configure() { $this ->setName('global') ->setDescription('Allows running commands in the global composer dir ($COMPOSER_HOME).') ->setDefinition(array( new InputArgument('command-name', InputArgument::REQUIRED, ''), new InputArgument('args', InputArgument::IS_ARRAY | InputArgument::OPTIONAL, ''), )) ->setHelp( <<\AppData\Roaming\Composer on Windows and /home//.composer on unix systems. If your system uses freedesktop.org standards, then it will first check XDG_CONFIG_HOME or default to /home//.config/composer Note: This path may vary depending on customizations to bin-dir in composer.json or the environmental variable COMPOSER_BIN_DIR. Read more at https://getcomposer.org/doc/03-cli.md#global EOT ) ; } /** * @return int|void * @throws \Symfony\Component\Console\Exception\ExceptionInterface */ public function run(InputInterface $input, OutputInterface $output) { if (!method_exists($input, '__toString')) { throw new \LogicException('Expected an Input instance that is stringable, got '.get_class($input)); } // extract real command name $tokens = Preg::split('{\s+}', $input->__toString()); $args = array(); foreach ($tokens as $token) { if ($token && $token[0] !== '-') { $args[] = $token; if (count($args) >= 2) { break; } } } // show help for this command if no command was found if (count($args) < 2) { return parent::run($input, $output); } // The COMPOSER env var should not apply to the global execution scope if (Platform::getEnv('COMPOSER')) { Platform::clearEnv('COMPOSER'); } // change to global dir $config = Factory::createConfig(); $home = $config->get('home'); if (!is_dir($home)) { $fs = new Filesystem(); $fs->ensureDirectoryExists($home); if (!is_dir($home)) { throw new \RuntimeException('Could not create home directory'); } } try { chdir($home); } catch (\Exception $e) { throw new \RuntimeException('Could not switch to home directory "'.$home.'"', 0, $e); } $this->getIO()->writeError('Changed current directory to '.$home.''); // create new input without "global" command prefix $input = new StringInput(Preg::replace('{\bg(?:l(?:o(?:b(?:a(?:l)?)?)?)?)?\b}', '', $input->__toString(), 1)); $this->getApplication()->resetComposer(); return $this->getApplication()->run($input, $output); } /** * @inheritDoc */ public function isProxyCommand() { return true; } } * Jordi Boggiano * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Composer\Command; use Composer\Factory; use Composer\IO\IOInterface; use Composer\Config; use Composer\Composer; use Composer\Package\BasePackage; use Composer\Package\CompletePackageInterface; use Composer\Repository\CompositeRepository; use Composer\Repository\RepositoryFactory; use Composer\Script\ScriptEvents; use Composer\Plugin\CommandEvent; use Composer\Plugin\PluginEvents; use Composer\Util\Filesystem; use Composer\Util\Loop; use Composer\Util\ProcessExecutor; use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; /** * Creates an archive of a package for distribution. * * @author Nils Adermann */ class ArchiveCommand extends BaseCommand { /** * @return void */ protected function configure() { $this ->setName('archive') ->setDescription('Creates an archive of this composer package.') ->setDefinition(array( new InputArgument('package', InputArgument::OPTIONAL, 'The package to archive instead of the current project'), new InputArgument('version', InputArgument::OPTIONAL, 'A version constraint to find the package to archive'), new InputOption('format', 'f', InputOption::VALUE_REQUIRED, 'Format of the resulting archive: tar, tar.gz, tar.bz2 or zip (default tar)'), new InputOption('dir', null, InputOption::VALUE_REQUIRED, 'Write the archive to this directory'), new InputOption('file', null, InputOption::VALUE_REQUIRED, 'Write the archive with the given file name.' .' Note that the format will be appended.'), new InputOption('ignore-filters', null, InputOption::VALUE_NONE, 'Ignore filters when saving package'), )) ->setHelp( <<archive command creates an archive of the specified format containing the files and directories of the Composer project or the specified package in the specified version and writes it to the specified directory. php composer.phar archive [--format=zip] [--dir=/foo] [package [version]] Read more at https://getcomposer.org/doc/03-cli.md#archive EOT ) ; } protected function execute(InputInterface $input, OutputInterface $output) { $composer = $this->getComposer(false); $config = null; if ($composer) { $config = $composer->getConfig(); $commandEvent = new CommandEvent(PluginEvents::COMMAND, 'archive', $input, $output); $eventDispatcher = $composer->getEventDispatcher(); $eventDispatcher->dispatch($commandEvent->getName(), $commandEvent); $eventDispatcher->dispatchScript(ScriptEvents::PRE_ARCHIVE_CMD); } if (!$config) { $config = Factory::createConfig(); } if (null === $input->getOption('format')) { $input->setOption('format', $config->get('archive-format')); } if (null === $input->getOption('dir')) { $input->setOption('dir', $config->get('archive-dir')); } $returnCode = $this->archive( $this->getIO(), $config, $input->getArgument('package'), $input->getArgument('version'), $input->getOption('format'), $input->getOption('dir'), $input->getOption('file'), $input->getOption('ignore-filters'), $composer ); if (0 === $returnCode && $composer) { $composer->getEventDispatcher()->dispatchScript(ScriptEvents::POST_ARCHIVE_CMD); } return $returnCode; } /** * @param string|null $packageName * @param string|null $version * @param string $format * @param string $dest * @param string|null $fileName * @param bool $ignoreFilters * * @return int * @throws \Exception */ protected function archive(IOInterface $io, Config $config, $packageName = null, $version = null, $format = 'tar', $dest = '.', $fileName = null, $ignoreFilters = false, Composer $composer = null) { if ($composer) { $archiveManager = $composer->getArchiveManager(); } else { $factory = new Factory; $process = new ProcessExecutor(); $httpDownloader = Factory::createHttpDownloader($io, $config); $downloadManager = $factory->createDownloadManager($io, $config, $httpDownloader, $process); $archiveManager = $factory->createArchiveManager($config, $downloadManager, new Loop($httpDownloader, $process)); } if ($packageName) { $package = $this->selectPackage($io, $packageName, $version); if (!$package) { return 1; } } else { $package = $this->getComposer()->getPackage(); } $io->writeError('Creating the archive into "'.$dest.'".'); $packagePath = $archiveManager->archive($package, $format, $dest, $fileName, $ignoreFilters); $fs = new Filesystem; $shortPath = $fs->findShortestPath(getcwd(), $packagePath, true); $io->writeError('Created: ', false); $io->write(strlen($shortPath) < strlen($packagePath) ? $shortPath : $packagePath); return 0; } /** * @param string $packageName * @param string|null $version * * @return (BasePackage&CompletePackageInterface)|false */ protected function selectPackage(IOInterface $io, $packageName, $version = null) { $io->writeError('Searching for the specified package.'); if ($composer = $this->getComposer(false)) { $localRepo = $composer->getRepositoryManager()->getLocalRepository(); $repo = new CompositeRepository(array_merge(array($localRepo), $composer->getRepositoryManager()->getRepositories())); } else { $defaultRepos = RepositoryFactory::defaultRepos($this->getIO()); $io->writeError('No composer.json found in the current directory, searching packages from ' . implode(', ', array_keys($defaultRepos))); $repo = new CompositeRepository($defaultRepos); } $packages = $repo->findPackages($packageName, $version); if (count($packages) > 1) { $package = reset($packages); $io->writeError('Found multiple matches, selected '.$package->getPrettyString().'.'); $io->writeError('Alternatives were '.implode(', ', array_map(function ($p) { return $p->getPrettyString(); }, $packages)).'.'); $io->writeError('Please use a more specific constraint to pick a different package.'); } elseif ($packages) { $package = reset($packages); $io->writeError('Found an exact match '.$package->getPrettyString().'.'); } else { $io->writeError('Could not find a package matching '.$packageName.'.'); return false; } if (!$package instanceof CompletePackageInterface) { throw new \LogicException('Expected a CompletePackageInterface instance but found '.get_class($package)); } return $package; } } * Jordi Boggiano * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Composer\Command; use Composer\Composer; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; /** * @author Jordi Boggiano */ class AboutCommand extends BaseCommand { /** * @return void */ protected function configure() { $this ->setName('about') ->setDescription('Shows a short information about Composer.') ->setHelp( <<php composer.phar about EOT ) ; } protected function execute(InputInterface $input, OutputInterface $output) { $composerVersion = Composer::getVersion(); $this->getIO()->write( <<Composer - Dependency Manager for PHP - version $composerVersion Composer is a dependency manager tracking local dependencies of your projects and libraries. See https://getcomposer.org/ for more information. EOT ); return 0; } } * Jordi Boggiano * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Composer\Command; use Composer\Composer; use Composer\Factory; use Composer\Config; use Composer\Pcre\Preg; use Composer\Util\Filesystem; use Composer\Util\Platform; use Composer\SelfUpdate\Keys; use Composer\SelfUpdate\Versions; use Composer\IO\IOInterface; use Composer\Downloader\FilesystemException; use Composer\Downloader\TransportException; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Finder\Finder; /** * @author Igor Wiedler * @author Kevin Ran * @author Jordi Boggiano */ class SelfUpdateCommand extends BaseCommand { const HOMEPAGE = 'getcomposer.org'; const OLD_INSTALL_EXT = '-old.phar'; /** * @return void */ protected function configure() { $this ->setName('self-update') ->setAliases(array('selfupdate')) ->setDescription('Updates composer.phar to the latest version.') ->setDefinition(array( new InputOption('rollback', 'r', InputOption::VALUE_NONE, 'Revert to an older installation of composer'), new InputOption('clean-backups', null, InputOption::VALUE_NONE, 'Delete old backups during an update. This makes the current version of composer the only backup available after the update'), new InputArgument('version', InputArgument::OPTIONAL, 'The version to update to'), new InputOption('no-progress', null, InputOption::VALUE_NONE, 'Do not output download progress.'), new InputOption('update-keys', null, InputOption::VALUE_NONE, 'Prompt user for a key update'), new InputOption('stable', null, InputOption::VALUE_NONE, 'Force an update to the stable channel'), new InputOption('preview', null, InputOption::VALUE_NONE, 'Force an update to the preview channel'), new InputOption('snapshot', null, InputOption::VALUE_NONE, 'Force an update to the snapshot channel'), new InputOption('1', null, InputOption::VALUE_NONE, 'Force an update to the stable channel, but only use 1.x versions'), new InputOption('2', null, InputOption::VALUE_NONE, 'Force an update to the stable channel, but only use 2.x versions'), new InputOption('set-channel-only', null, InputOption::VALUE_NONE, 'Only store the channel as the default one and then exit'), )) ->setHelp( <<self-update command checks getcomposer.org for newer versions of composer and if found, installs the latest. php composer.phar self-update Read more at https://getcomposer.org/doc/03-cli.md#self-update-selfupdate- EOT ) ; } /** * @return int * @throws FilesystemException */ protected function execute(InputInterface $input, OutputInterface $output) { // trigger autoloading of a few classes which may be needed when verifying/swapping the phar file // to ensure we do not try to load them from the new phar, see https://github.com/composer/composer/issues/10252 class_exists('Composer\Util\Platform'); class_exists('Composer\Downloader\FilesystemException'); $config = Factory::createConfig(); if ($config->get('disable-tls') === true) { $baseUrl = 'http://' . self::HOMEPAGE; } else { $baseUrl = 'https://' . self::HOMEPAGE; } $io = $this->getIO(); $httpDownloader = Factory::createHttpDownloader($io, $config); $versionsUtil = new Versions($config, $httpDownloader); // switch channel if requested $requestedChannel = null; foreach (Versions::$channels as $channel) { if ($input->getOption($channel)) { $requestedChannel = $channel; $versionsUtil->setChannel($channel); break; } } if ($input->getOption('set-channel-only')) { return 0; } $cacheDir = $config->get('cache-dir'); $rollbackDir = $config->get('data-dir'); $home = $config->get('home'); $localFilename = realpath($_SERVER['argv'][0]) ?: $_SERVER['argv'][0]; if ($input->getOption('update-keys')) { $this->fetchKeys($io, $config); return 0; } // ensure composer.phar location is accessible if (!file_exists($localFilename)) { throw new FilesystemException('Composer update failed: the "'.$localFilename.'" is not accessible'); } // check if current dir is writable and if not try the cache dir from settings $tmpDir = is_writable(dirname($localFilename)) ? dirname($localFilename) : $cacheDir; // check for permissions in local filesystem before start connection process if (!is_writable($tmpDir)) { throw new FilesystemException('Composer update failed: the "'.$tmpDir.'" directory used to download the temp file could not be written'); } // check if composer is running as the same user that owns the directory root, only if POSIX is defined and callable if (function_exists('posix_getpwuid') && function_exists('posix_geteuid')) { $composeUser = posix_getpwuid(posix_geteuid()); $homeOwner = posix_getpwuid(fileowner($home)); if (isset($composeUser['name'], $homeOwner['name']) && $composeUser['name'] !== $homeOwner['name']) { $io->writeError('You are running Composer as "'.$composeUser['name'].'", while "'.$home.'" is owned by "'.$homeOwner['name'].'"'); } } if ($input->getOption('rollback')) { return $this->rollback($output, $rollbackDir, $localFilename); } $latest = $versionsUtil->getLatest(); $latestStable = $versionsUtil->getLatest('stable'); try { $latestPreview = $versionsUtil->getLatest('preview'); } catch (\UnexpectedValueException $e) { $latestPreview = $latestStable; } $latestVersion = $latest['version']; $updateVersion = $input->getArgument('version') ?: $latestVersion; $currentMajorVersion = Preg::replace('{^(\d+).*}', '$1', Composer::getVersion()); $updateMajorVersion = Preg::replace('{^(\d+).*}', '$1', $updateVersion); $previewMajorVersion = Preg::replace('{^(\d+).*}', '$1', $latestPreview['version']); if ($versionsUtil->getChannel() === 'stable' && !$input->getArgument('version')) { // if requesting stable channel and no specific version, avoid automatically upgrading to the next major // simply output a warning that the next major stable is available and let users upgrade to it manually if ($currentMajorVersion < $updateMajorVersion) { $skippedVersion = $updateVersion; $versionsUtil->setChannel($currentMajorVersion); $latest = $versionsUtil->getLatest(); $latestStable = $versionsUtil->getLatest('stable'); $latestVersion = $latest['version']; $updateVersion = $latestVersion; $io->writeError('A new stable major version of Composer is available ('.$skippedVersion.'), run "composer self-update --'.$updateMajorVersion.'" to update to it. See also https://getcomposer.org/'.$updateMajorVersion.''); } elseif ($currentMajorVersion < $previewMajorVersion) { // promote next major version if available in preview $io->writeError('A preview release of the next major version of Composer is available ('.$latestPreview['version'].'), run "composer self-update --preview" to give it a try. See also https://github.com/composer/composer/releases for changelogs.'); } } if ($requestedChannel && is_numeric($requestedChannel) && strpos($latestStable['version'], $requestedChannel) !== 0) { $io->writeError('Warning: You forced the install of '.$latestVersion.' via --'.$requestedChannel.', but '.$latestStable['version'].' is the latest stable version. Updating to it via composer self-update --stable is recommended.'); } if (Preg::isMatch('{^[0-9a-f]{40}$}', $updateVersion) && $updateVersion !== $latestVersion) { $io->writeError('You can not update to a specific SHA-1 as those phars are not available for download'); return 1; } $channelString = $versionsUtil->getChannel(); if (is_numeric($channelString)) { $channelString .= '.x'; } if (Composer::VERSION === $updateVersion) { $io->writeError( sprintf( 'You are already using the latest available Composer version %s (%s channel).', $updateVersion, $channelString ) ); // remove all backups except for the most recent, if any if ($input->getOption('clean-backups')) { $this->cleanBackups($rollbackDir, $this->getLastBackupVersion($rollbackDir)); } return 0; } $tempFilename = $tmpDir . '/' . basename($localFilename, '.phar').'-temp'.rand(0, 10000000).'.phar'; $backupFile = sprintf( '%s/%s-%s%s', $rollbackDir, strtr(Composer::RELEASE_DATE, ' :', '_-'), Preg::replace('{^([0-9a-f]{7})[0-9a-f]{33}$}', '$1', Composer::VERSION), self::OLD_INSTALL_EXT ); $updatingToTag = !Preg::isMatch('{^[0-9a-f]{40}$}', $updateVersion); $io->write(sprintf("Upgrading to version %s (%s channel).", $updateVersion, $channelString)); $remoteFilename = $baseUrl . ($updatingToTag ? "/download/{$updateVersion}/composer.phar" : '/composer.phar'); try { $signature = $httpDownloader->get($remoteFilename.'.sig')->getBody(); } catch (TransportException $e) { if ($e->getStatusCode() === 404) { throw new \InvalidArgumentException('Version "'.$updateVersion.'" could not be found.', 0, $e); } throw $e; } $io->writeError(' ', false); $httpDownloader->copy($remoteFilename, $tempFilename); $io->writeError(''); if (!file_exists($tempFilename) || !$signature) { $io->writeError('The download of the new composer version failed for an unexpected reason'); return 1; } // verify phar signature if (!extension_loaded('openssl') && $config->get('disable-tls')) { $io->writeError('Skipping phar signature verification as you have disabled OpenSSL via config.disable-tls'); } else { if (!extension_loaded('openssl')) { throw new \RuntimeException('The openssl extension is required for phar signatures to be verified but it is not available. ' . 'If you can not enable the openssl extension, you can disable this error, at your own risk, by setting the \'disable-tls\' option to true.'); } $sigFile = 'file://'.$home.'/' . ($updatingToTag ? 'keys.tags.pub' : 'keys.dev.pub'); if (!file_exists($sigFile)) { file_put_contents( $home.'/keys.dev.pub', <<getOption('clean-backups')) { $this->cleanBackups($rollbackDir); } if (!$this->setLocalPhar($localFilename, $tempFilename, $backupFile)) { @unlink($tempFilename); return 1; } if (file_exists($backupFile)) { $io->writeError(sprintf( 'Use composer self-update --rollback to return to version %s', Composer::VERSION )); } else { $io->writeError('A backup of the current version could not be written to '.$backupFile.', no rollback possible'); } return 0; } /** * @return void * @throws \Exception */ protected function fetchKeys(IOInterface $io, Config $config) { if (!$io->isInteractive()) { throw new \RuntimeException('Public keys can not be fetched in non-interactive mode, please run Composer interactively'); } $io->write('Open https://composer.github.io/pubkeys.html to find the latest keys'); $validator = function ($value) { if (!Preg::isMatch('{^-----BEGIN PUBLIC KEY-----$}', trim($value))) { throw new \UnexpectedValueException('Invalid input'); } return trim($value)."\n"; }; $devKey = ''; while (!Preg::isMatch('{(-----BEGIN PUBLIC KEY-----.+?-----END PUBLIC KEY-----)}s', $devKey, $match)) { $devKey = $io->askAndValidate('Enter Dev / Snapshot Public Key (including lines with -----): ', $validator); while ($line = $io->ask('')) { $devKey .= trim($line)."\n"; if (trim($line) === '-----END PUBLIC KEY-----') { break; } } } file_put_contents($keyPath = $config->get('home').'/keys.dev.pub', $match[0]); $io->write('Stored key with fingerprint: ' . Keys::fingerprint($keyPath)); $tagsKey = ''; while (!Preg::isMatch('{(-----BEGIN PUBLIC KEY-----.+?-----END PUBLIC KEY-----)}s', $tagsKey, $match)) { $tagsKey = $io->askAndValidate('Enter Tags Public Key (including lines with -----): ', $validator); while ($line = $io->ask('')) { $tagsKey .= trim($line)."\n"; if (trim($line) === '-----END PUBLIC KEY-----') { break; } } } file_put_contents($keyPath = $config->get('home').'/keys.tags.pub', $match[0]); $io->write('Stored key with fingerprint: ' . Keys::fingerprint($keyPath)); $io->write('Public keys stored in '.$config->get('home')); } /** * @param string $rollbackDir * @param string $localFilename * @return int * @throws FilesystemException */ protected function rollback(OutputInterface $output, $rollbackDir, $localFilename) { $rollbackVersion = $this->getLastBackupVersion($rollbackDir); if (!$rollbackVersion) { throw new \UnexpectedValueException('Composer rollback failed: no installation to roll back to in "'.$rollbackDir.'"'); } $oldFile = $rollbackDir . '/' . $rollbackVersion . self::OLD_INSTALL_EXT; if (!is_file($oldFile)) { throw new FilesystemException('Composer rollback failed: "'.$oldFile.'" could not be found'); } if (!Filesystem::isReadable($oldFile)) { throw new FilesystemException('Composer rollback failed: "'.$oldFile.'" could not be read'); } $io = $this->getIO(); $io->writeError(sprintf("Rolling back to version %s.", $rollbackVersion)); if (!$this->setLocalPhar($localFilename, $oldFile)) { return 1; } return 0; } /** * Checks if the downloaded/rollback phar is valid then moves it * * @param string $localFilename The composer.phar location * @param string $newFilename The downloaded or backup phar * @param string $backupTarget The filename to use for the backup * @throws FilesystemException If the file cannot be moved * @return bool Whether the phar is valid and has been moved */ protected function setLocalPhar($localFilename, $newFilename, $backupTarget = null) { $io = $this->getIO(); @chmod($newFilename, fileperms($localFilename)); // check phar validity if (!$this->validatePhar($newFilename, $error)) { $io->writeError('The '.($backupTarget ? 'update' : 'backup').' file is corrupted ('.$error.')'); if ($backupTarget) { $io->writeError('Please re-run the self-update command to try again.'); } return false; } // copy current file into backups dir if ($backupTarget) { @copy($localFilename, $backupTarget); } try { if (Platform::isWindows()) { // use copy to apply permissions from the destination directory // as rename uses source permissions and may block other users copy($newFilename, $localFilename); @unlink($newFilename); } else { rename($newFilename, $localFilename); } return true; } catch (\Exception $e) { // see if we can run this operation as an Admin on Windows if (!is_writable(dirname($localFilename)) && $io->isInteractive() && $this->isWindowsNonAdminUser()) { return $this->tryAsWindowsAdmin($localFilename, $newFilename); } @unlink($newFilename); $action = 'Composer '.($backupTarget ? 'update' : 'rollback'); throw new FilesystemException($action.' failed: "'.$localFilename.'" could not be written.'.PHP_EOL.$e->getMessage()); } } /** * @param string $rollbackDir * @param string|null $except * * @return void */ protected function cleanBackups($rollbackDir, $except = null) { $finder = $this->getOldInstallationFinder($rollbackDir); $io = $this->getIO(); $fs = new Filesystem; foreach ($finder as $file) { if ($except && $file->getBasename(self::OLD_INSTALL_EXT) === $except) { continue; } $file = (string) $file; $io->writeError('Removing: '.$file.''); $fs->remove($file); } } /** * @param string $rollbackDir * @return string|false */ protected function getLastBackupVersion($rollbackDir) { $finder = $this->getOldInstallationFinder($rollbackDir); $finder->sortByName(); $files = iterator_to_array($finder); if (count($files)) { return basename(end($files), self::OLD_INSTALL_EXT); } return false; } /** * @param string $rollbackDir * @return Finder */ protected function getOldInstallationFinder($rollbackDir) { return Finder::create() ->depth(0) ->files() ->name('*' . self::OLD_INSTALL_EXT) ->in($rollbackDir); } /** * Validates the downloaded/backup phar file * * @param string $pharFile The downloaded or backup phar * @param null|string $error Set by method on failure * * Code taken from getcomposer.org/installer. Any changes should be made * there and replicated here * * @throws \Exception * @return bool If the operation succeeded */ protected function validatePhar($pharFile, &$error) { if (ini_get('phar.readonly')) { return true; } try { // Test the phar validity $phar = new \Phar($pharFile); // Free the variable to unlock the file unset($phar); $result = true; } catch (\Exception $e) { if (!$e instanceof \UnexpectedValueException && !$e instanceof \PharException) { throw $e; } $error = $e->getMessage(); $result = false; } return $result; } /** * Returns true if this is a non-admin Windows user account * * @return bool */ protected function isWindowsNonAdminUser() { if (!Platform::isWindows()) { return false; } // fltmc.exe manages filter drivers and errors without admin privileges exec('fltmc.exe filters', $output, $exitCode); return $exitCode !== 0; } /** * Invokes a UAC prompt to update composer.phar as an admin * * Uses a .vbs script to elevate and run the cmd.exe copy command. * * @param string $localFilename The composer.phar location * @param string $newFilename The downloaded or backup phar * @return bool Whether composer.phar has been updated */ protected function tryAsWindowsAdmin($localFilename, $newFilename) { $io = $this->getIO(); $io->writeError('Unable to write "'.$localFilename.'". Access is denied.'); $helpMessage = 'Please run the self-update command as an Administrator.'; $question = 'Complete this operation with Administrator privileges [Y,n]? '; if (!$io->askConfirmation($question, false)) { $io->writeError('Operation cancelled. '.$helpMessage.''); return false; } $tmpFile = tempnam(sys_get_temp_dir(), ''); $script = $tmpFile.'.vbs'; rename($tmpFile, $script); $checksum = hash_file('sha256', $newFilename); // cmd's internal copy is fussy about backslashes $source = str_replace('/', '\\', $newFilename); $destination = str_replace('/', '\\', $localFilename); $vbs = <<writeError('Operation succeeded.'); @unlink($newFilename); } else { $io->writeError('Operation failed.'.$helpMessage.''); } return $result; } } * Jordi Boggiano * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Composer\Command; use Composer\Package\Link; use Composer\Package\PackageInterface; use Composer\Package\CompletePackageInterface; use Composer\Package\RootPackage; use Composer\Repository\InstalledArrayRepository; use Composer\Repository\CompositeRepository; use Composer\Repository\RootPackageRepository; use Composer\Repository\InstalledRepository; use Composer\Repository\PlatformRepository; use Composer\Repository\RepositoryFactory; use Composer\Plugin\CommandEvent; use Composer\Plugin\PluginEvents; use Symfony\Component\Console\Formatter\OutputFormatterStyle; use Composer\Package\Version\VersionParser; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; /** * Base implementation for commands mapping dependency relationships. * * @author Niels Keurentjes */ class BaseDependencyCommand extends BaseCommand { const ARGUMENT_PACKAGE = 'package'; const ARGUMENT_CONSTRAINT = 'version'; const OPTION_RECURSIVE = 'recursive'; const OPTION_TREE = 'tree'; /** @var ?string[] */ protected $colors; /** * Execute the command. * * @param bool $inverted Whether to invert matching process (why-not vs why behaviour) * @return int Exit code of the operation. */ protected function doExecute(InputInterface $input, OutputInterface $output, $inverted = false) { // Emit command event on startup $composer = $this->getComposer(); $commandEvent = new CommandEvent(PluginEvents::COMMAND, $this->getName(), $input, $output); $composer->getEventDispatcher()->dispatch($commandEvent->getName(), $commandEvent); $platformOverrides = $composer->getConfig()->get('platform') ?: array(); $installedRepo = new InstalledRepository(array( new RootPackageRepository($composer->getPackage()), $composer->getRepositoryManager()->getLocalRepository(), new PlatformRepository(array(), $platformOverrides), )); // Parse package name and constraint list($needle, $textConstraint) = array_pad( explode(':', $input->getArgument(self::ARGUMENT_PACKAGE)), 2, $input->hasArgument(self::ARGUMENT_CONSTRAINT) ? $input->getArgument(self::ARGUMENT_CONSTRAINT) : '*' ); // Find packages that are or provide the requested package first $packages = $installedRepo->findPackagesWithReplacersAndProviders($needle); if (empty($packages)) { throw new \InvalidArgumentException(sprintf('Could not find package "%s" in your project', $needle)); } // If the version we ask for is not installed then we need to locate it in remote repos and add it. // This is needed for why-not to resolve conflicts from an uninstalled version against installed packages. if (!$installedRepo->findPackage($needle, $textConstraint)) { $defaultRepos = new CompositeRepository(RepositoryFactory::defaultRepos($this->getIO())); if ($match = $defaultRepos->findPackage($needle, $textConstraint)) { $installedRepo->addRepository(new InstalledArrayRepository(array(clone $match))); } } // Include replaced packages for inverted lookups as they are then the actual starting point to consider $needles = array($needle); if ($inverted) { foreach ($packages as $package) { $needles = array_merge($needles, array_map(function (Link $link) { return $link->getTarget(); }, $package->getReplaces())); } } // Parse constraint if one was supplied if ('*' !== $textConstraint) { $versionParser = new VersionParser(); $constraint = $versionParser->parseConstraints($textConstraint); } else { $constraint = null; } // Parse rendering options $renderTree = $input->getOption(self::OPTION_TREE); $recursive = $renderTree || $input->getOption(self::OPTION_RECURSIVE); // Resolve dependencies $results = $installedRepo->getDependents($needles, $constraint, $inverted, $recursive); if (empty($results)) { $extra = (null !== $constraint) ? sprintf(' in versions %smatching %s', $inverted ? 'not ' : '', $textConstraint) : ''; $this->getIO()->writeError(sprintf( 'There is no installed package depending on "%s"%s', $needle, $extra )); } elseif ($renderTree) { $this->initStyles($output); $root = $packages[0]; $this->getIO()->write(sprintf('%s %s %s', $root->getPrettyName(), $root->getPrettyVersion(), $root instanceof CompletePackageInterface ? $root->getDescription() : '')); $this->printTree($results); } else { $this->printTable($output, $results); } return 0; } /** * Assembles and prints a bottom-up table of the dependencies. * * @param array{PackageInterface, Link, mixed}[] $results * * @return void */ protected function printTable(OutputInterface $output, $results) { $table = array(); $doubles = array(); do { $queue = array(); $rows = array(); foreach ($results as $result) { /** * @var PackageInterface $package * @var Link $link */ list($package, $link, $children) = $result; $unique = (string) $link; if (isset($doubles[$unique])) { continue; } $doubles[$unique] = true; $version = $package->getPrettyVersion() === RootPackage::DEFAULT_PRETTY_VERSION ? '-' : $package->getPrettyVersion(); $rows[] = array($package->getPrettyName(), $version, $link->getDescription(), sprintf('%s (%s)', $link->getTarget(), $link->getPrettyConstraint())); if ($children) { $queue = array_merge($queue, $children); } } $results = $queue; $table = array_merge($rows, $table); } while (!empty($results)); $this->renderTable($table, $output); } /** * Init styles for tree * * @return void */ protected function initStyles(OutputInterface $output) { $this->colors = array( 'green', 'yellow', 'cyan', 'magenta', 'blue', ); foreach ($this->colors as $color) { $style = new OutputFormatterStyle($color); $output->getFormatter()->setStyle($color, $style); } } /** * Recursively prints a tree of the selected results. * * @param array{PackageInterface, Link, mixed[]|bool}[] $results Results to be printed at this level. * @param string $prefix Prefix of the current tree level. * @param int $level Current level of recursion. * * @return void */ protected function printTree($results, $prefix = '', $level = 1) { $count = count($results); $idx = 0; foreach ($results as $result) { list($package, $link, $children) = $result; $color = $this->colors[$level % count($this->colors)]; $prevColor = $this->colors[($level - 1) % count($this->colors)]; $isLast = (++$idx == $count); $versionText = $package->getPrettyVersion() === RootPackage::DEFAULT_PRETTY_VERSION ? '' : $package->getPrettyVersion(); $packageText = rtrim(sprintf('<%s>%s %s', $color, $package->getPrettyName(), $versionText)); $linkText = sprintf('%s <%s>%s %s', $link->getDescription(), $prevColor, $link->getTarget(), $link->getPrettyConstraint()); $circularWarn = $children === false ? '(circular dependency aborted here)' : ''; $this->writeTreeLine(rtrim(sprintf("%s%s%s (%s) %s", $prefix, $isLast ? '└──' : '├──', $packageText, $linkText, $circularWarn))); if ($children) { $this->printTree($children, $prefix . ($isLast ? ' ' : '│ '), $level + 1); } } } /** * @param string $line * * @return void */ private function writeTreeLine($line) { $io = $this->getIO(); if (!$io->isDecorated()) { $line = str_replace(array('└', '├', '──', '│'), array('`-', '|-', '-', '|'), $line); } $io->write($line); } } * Jordi Boggiano * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Composer\Command; use Composer\Repository\PlatformRepository; use Composer\Repository\RootPackageRepository; use Composer\Repository\InstalledRepository; use Composer\Installer\SuggestedPackagesReporter; use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; class SuggestsCommand extends BaseCommand { /** * @return void */ protected function configure() { $this ->setName('suggests') ->setDescription('Shows package suggestions.') ->setDefinition(array( new InputOption('by-package', null, InputOption::VALUE_NONE, 'Groups output by suggesting package (default)'), new InputOption('by-suggestion', null, InputOption::VALUE_NONE, 'Groups output by suggested package'), new InputOption('all', 'a', InputOption::VALUE_NONE, 'Show suggestions from all dependencies, including transitive ones'), new InputOption('list', null, InputOption::VALUE_NONE, 'Show only list of suggested package names'), new InputOption('no-dev', null, InputOption::VALUE_NONE, 'Exclude suggestions from require-dev packages'), new InputArgument('packages', InputArgument::IS_ARRAY | InputArgument::OPTIONAL, 'Packages that you want to list suggestions from.'), )) ->setHelp( <<%command.name% command shows a sorted list of suggested packages. Read more at https://getcomposer.org/doc/03-cli.md#suggests EOT ) ; } /** * @inheritDoc */ protected function execute(InputInterface $input, OutputInterface $output) { $composer = $this->getComposer(); $installedRepos = array( new RootPackageRepository(clone $composer->getPackage()), ); $locker = $composer->getLocker(); if ($locker->isLocked()) { $installedRepos[] = new PlatformRepository(array(), $locker->getPlatformOverrides()); $installedRepos[] = $locker->getLockedRepository(!$input->getOption('no-dev')); } else { $installedRepos[] = new PlatformRepository(array(), $composer->getConfig()->get('platform') ?: array()); $installedRepos[] = $composer->getRepositoryManager()->getLocalRepository(); } $installedRepo = new InstalledRepository($installedRepos); $reporter = new SuggestedPackagesReporter($this->getIO()); $filter = $input->getArgument('packages'); $packages = $installedRepo->getPackages(); $packages[] = $composer->getPackage(); foreach ($packages as $package) { if (!empty($filter) && !in_array($package->getName(), $filter)) { continue; } $reporter->addSuggestionsFromPackage($package); } // Determine output mode, default is by-package $mode = SuggestedPackagesReporter::MODE_BY_PACKAGE; // if by-suggestion is given we override the default if ($input->getOption('by-suggestion')) { $mode = SuggestedPackagesReporter::MODE_BY_SUGGESTION; } // unless by-package is also present then we enable both if ($input->getOption('by-package')) { $mode |= SuggestedPackagesReporter::MODE_BY_PACKAGE; } // list is exclusive and overrides everything else if ($input->getOption('list')) { $mode = SuggestedPackagesReporter::MODE_LIST; } $reporter->output($mode, $installedRepo, empty($filter) && !$input->getOption('all') ? $composer->getPackage() : null); return 0; } } * Jordi Boggiano * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Composer\Command; use Composer\Factory; use Composer\IO\IOInterface; use Composer\Package\Loader\ValidatingArrayLoader; use Composer\Plugin\CommandEvent; use Composer\Plugin\PluginEvents; use Composer\Repository\InstalledRepository; use Composer\Repository\PlatformRepository; use Composer\Util\ConfigValidator; use Composer\Util\Filesystem; use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; /** * ValidateCommand * * @author Robert Schönthal * @author Jordi Boggiano */ class ValidateCommand extends BaseCommand { /** * configure * @return void */ protected function configure() { $this ->setName('validate') ->setDescription('Validates a composer.json and composer.lock.') ->setDefinition(array( new InputOption('no-check-all', null, InputOption::VALUE_NONE, 'Do not validate requires for overly strict/loose constraints'), new InputOption('no-check-lock', null, InputOption::VALUE_NONE, 'Do not check if lock file is up to date'), new InputOption('no-check-publish', null, InputOption::VALUE_NONE, 'Do not check for publish errors'), new InputOption('no-check-version', null, InputOption::VALUE_NONE, 'Do not report a warning if the version field is present'), new InputOption('with-dependencies', 'A', InputOption::VALUE_NONE, 'Also validate the composer.json of all installed dependencies'), new InputOption('strict', null, InputOption::VALUE_NONE, 'Return a non-zero exit code for warnings as well as errors'), new InputArgument('file', InputArgument::OPTIONAL, 'path to composer.json file'), )) ->setHelp( <<getArgument('file') ?: Factory::getComposerFile(); $io = $this->getIO(); if (!file_exists($file)) { $io->writeError('' . $file . ' not found.'); return 3; } if (!Filesystem::isReadable($file)) { $io->writeError('' . $file . ' is not readable.'); return 3; } $validator = new ConfigValidator($io); $checkAll = $input->getOption('no-check-all') ? 0 : ValidatingArrayLoader::CHECK_ALL; $checkPublish = !$input->getOption('no-check-publish'); $checkLock = !$input->getOption('no-check-lock'); $checkVersion = $input->getOption('no-check-version') ? 0 : ConfigValidator::CHECK_VERSION; $isStrict = $input->getOption('strict'); list($errors, $publishErrors, $warnings) = $validator->validate($file, $checkAll, $checkVersion); $lockErrors = array(); $composer = Factory::create($io, $file, $input->hasParameterOption('--no-plugins')); $locker = $composer->getLocker(); if ($locker->isLocked() && !$locker->isFresh()) { $lockErrors[] = '- The lock file is not up to date with the latest changes in composer.json, it is recommended that you run `composer update` or `composer update `.'; } if ($locker->isLocked()) { $missingRequirements = false; $sets = array( array('repo' => $locker->getLockedRepository(false), 'method' => 'getRequires', 'description' => 'Required'), array('repo' => $locker->getLockedRepository(true), 'method' => 'getDevRequires', 'description' => 'Required (in require-dev)'), ); foreach ($sets as $set) { $installedRepo = new InstalledRepository(array($set['repo'])); foreach (call_user_func(array($composer->getPackage(), $set['method'])) as $link) { if (PlatformRepository::isPlatformPackage($link->getTarget())) { continue; } if (!$installedRepo->findPackagesWithReplacersAndProviders($link->getTarget(), $link->getConstraint())) { if ($results = $installedRepo->findPackagesWithReplacersAndProviders($link->getTarget())) { $provider = reset($results); $lockErrors[] = '- ' . $set['description'].' package "' . $link->getTarget() . '" is in the lock file as "'.$provider->getPrettyVersion().'" but that does not satisfy your constraint "'.$link->getPrettyConstraint().'".'; } else { $lockErrors[] = '- ' . $set['description'].' package "' . $link->getTarget() . '" is not present in the lock file.'; } $missingRequirements = true; } } } if ($missingRequirements) { $lockErrors[] = 'This usually happens when composer files are incorrectly merged or the composer.json file is manually edited.'; $lockErrors[] = 'Read more about correctly resolving merge conflicts https://getcomposer.org/doc/articles/resolving-merge-conflicts.md'; $lockErrors[] = 'and prefer using the "require" command over editing the composer.json file directly https://getcomposer.org/doc/03-cli.md#require'; } } $this->outputResult($io, $file, $errors, $warnings, $checkPublish, $publishErrors, $checkLock, $lockErrors, true); // $errors include publish and lock errors when exists $exitCode = $errors ? 2 : ($isStrict && $warnings ? 1 : 0); if ($input->getOption('with-dependencies')) { $localRepo = $composer->getRepositoryManager()->getLocalRepository(); foreach ($localRepo->getPackages() as $package) { $path = $composer->getInstallationManager()->getInstallPath($package); $file = $path . '/composer.json'; if (is_dir($path) && file_exists($file)) { list($errors, $publishErrors, $warnings) = $validator->validate($file, $checkAll, $checkVersion); $this->outputResult($io, $package->getPrettyName(), $errors, $warnings, $checkPublish, $publishErrors); // $errors include publish errors when exists $depCode = $errors ? 2 : ($isStrict && $warnings ? 1 : 0); $exitCode = max($depCode, $exitCode); } } } $commandEvent = new CommandEvent(PluginEvents::COMMAND, 'validate', $input, $output); $eventCode = $composer->getEventDispatcher()->dispatch($commandEvent->getName(), $commandEvent); return max($eventCode, $exitCode); } /** * @param string $name * @param string[] $errors * @param string[] $warnings * @param bool $checkPublish * @param string[] $publishErrors * @param bool $checkLock * @param string[] $lockErrors * @param bool $printSchemaUrl * * @return void */ private function outputResult(IOInterface $io, $name, &$errors, &$warnings, $checkPublish = false, $publishErrors = array(), $checkLock = false, $lockErrors = array(), $printSchemaUrl = false) { $doPrintSchemaUrl = false; if ($errors) { $io->writeError('' . $name . ' is invalid, the following errors/warnings were found:'); } elseif ($publishErrors) { $io->writeError('' . $name . ' is valid for simple usage with Composer but has'); $io->writeError('strict errors that make it unable to be published as a package'); $doPrintSchemaUrl = $printSchemaUrl; } elseif ($warnings) { $io->writeError('' . $name . ' is valid, but with a few warnings'); $doPrintSchemaUrl = $printSchemaUrl; } elseif ($lockErrors) { $io->write('' . $name . ' is valid but your composer.lock has some '.($checkLock ? 'errors' : 'warnings').''); } else { $io->write('' . $name . ' is valid'); } if ($doPrintSchemaUrl) { $io->writeError('See https://getcomposer.org/doc/04-schema.md for details on the schema'); } if ($errors) { $errors = array_map(function ($err) { return '- ' . $err; }, $errors); array_unshift($errors, '# General errors'); } if ($warnings) { $warnings = array_map(function ($err) { return '- ' . $err; }, $warnings); array_unshift($warnings, '# General warnings'); } // Avoid setting the exit code to 1 in case --strict and --no-check-publish/--no-check-lock are combined $extraWarnings = array(); // If checking publish errors, display them as errors, otherwise just show them as warnings if ($publishErrors) { $publishErrors = array_map(function ($err) { return '- ' . $err; }, $publishErrors); if ($checkPublish) { array_unshift($publishErrors, '# Publish errors'); $errors = array_merge($errors, $publishErrors); } else { array_unshift($publishErrors, '# Publish warnings'); $extraWarnings = array_merge($extraWarnings, $publishErrors); } } // If checking lock errors, display them as errors, otherwise just show them as warnings if ($lockErrors) { if ($checkLock) { array_unshift($lockErrors, '# Lock file errors'); $errors = array_merge($errors, $lockErrors); } else { array_unshift($lockErrors, '# Lock file warnings'); $extraWarnings = array_merge($extraWarnings, $lockErrors); } } $messages = array( 'error' => $errors, 'warning' => array_merge($warnings, $extraWarnings), ); foreach ($messages as $style => $msgs) { foreach ($msgs as $msg) { if (strpos($msg, '#') === 0) { $io->writeError('<' . $style . '>' . $msg . ''); } else { $io->writeError($msg); } } } } } * Jordi Boggiano * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Composer; use Composer\Config\JsonConfigSource; use Composer\Json\JsonFile; use Composer\IO\IOInterface; use Composer\Package\Archiver; use Composer\Package\Version\VersionGuesser; use Composer\Package\RootPackageInterface; use Composer\Repository\RepositoryManager; use Composer\Repository\RepositoryFactory; use Composer\Util\Filesystem; use Composer\Util\Platform; use Composer\Util\ProcessExecutor; use Composer\Util\HttpDownloader; use Composer\Util\Loop; use Composer\Util\Silencer; use Composer\Plugin\PluginEvents; use Composer\EventDispatcher\Event; use Seld\JsonLint\DuplicateKeyException; use Symfony\Component\Console\Formatter\OutputFormatter; use Symfony\Component\Console\Formatter\OutputFormatterStyle; use Symfony\Component\Console\Output\ConsoleOutput; use Composer\EventDispatcher\EventDispatcher; use Composer\Autoload\AutoloadGenerator; use Composer\Package\Version\VersionParser; use Composer\Downloader\TransportException; use Composer\Json\JsonValidationException; use Composer\Repository\InstalledRepositoryInterface; use Seld\JsonLint\JsonParser; /** * Creates a configured instance of composer. * * @author Ryan Weaver * @author Jordi Boggiano * @author Igor Wiedler * @author Nils Adermann */ class Factory { /** * @throws \RuntimeException * @return string */ protected static function getHomeDir() { $home = Platform::getEnv('COMPOSER_HOME'); if ($home) { return $home; } if (Platform::isWindows()) { if (!Platform::getEnv('APPDATA')) { throw new \RuntimeException('The APPDATA or COMPOSER_HOME environment variable must be set for composer to run correctly'); } return rtrim(strtr(Platform::getEnv('APPDATA'), '\\', '/'), '/') . '/Composer'; } $userDir = self::getUserDir(); $dirs = array(); if (self::useXdg()) { // XDG Base Directory Specifications $xdgConfig = Platform::getEnv('XDG_CONFIG_HOME'); if (!$xdgConfig) { $xdgConfig = $userDir . '/.config'; } $dirs[] = $xdgConfig . '/composer'; } $dirs[] = $userDir . '/.composer'; // select first dir which exists of: $XDG_CONFIG_HOME/composer or ~/.composer foreach ($dirs as $dir) { if (Silencer::call('is_dir', $dir)) { return $dir; } } // if none exists, we default to first defined one (XDG one if system uses it, or ~/.composer otherwise) return $dirs[0]; } /** * @param string $home * @return string */ protected static function getCacheDir($home) { $cacheDir = Platform::getEnv('COMPOSER_CACHE_DIR'); if ($cacheDir) { return $cacheDir; } $homeEnv = Platform::getEnv('COMPOSER_HOME'); if ($homeEnv) { return $homeEnv . '/cache'; } if (Platform::isWindows()) { if ($cacheDir = Platform::getEnv('LOCALAPPDATA')) { $cacheDir .= '/Composer'; } else { $cacheDir = $home . '/cache'; } return rtrim(strtr($cacheDir, '\\', '/'), '/'); } $userDir = self::getUserDir(); if (PHP_OS === 'Darwin') { // Migrate existing cache dir in old location if present if (is_dir($home . '/cache') && !is_dir($userDir . '/Library/Caches/composer')) { Silencer::call('rename', $home . '/cache', $userDir . '/Library/Caches/composer'); } return $userDir . '/Library/Caches/composer'; } if ($home === $userDir . '/.composer' && is_dir($home . '/cache')) { return $home . '/cache'; } if (self::useXdg()) { $xdgCache = Platform::getEnv('XDG_CACHE_HOME') ?: $userDir . '/.cache'; return $xdgCache . '/composer'; } return $home . '/cache'; } /** * @param string $home * @return string */ protected static function getDataDir($home) { $homeEnv = Platform::getEnv('COMPOSER_HOME'); if ($homeEnv) { return $homeEnv; } if (Platform::isWindows()) { return strtr($home, '\\', '/'); } $userDir = self::getUserDir(); if ($home !== $userDir . '/.composer' && self::useXdg()) { $xdgData = Platform::getEnv('XDG_DATA_HOME') ?: $userDir . '/.local/share'; return $xdgData . '/composer'; } return $home; } /** * @param string|null $cwd * * @return Config */ public static function createConfig(IOInterface $io = null, $cwd = null) { $cwd = $cwd ?: (string) getcwd(); $config = new Config(true, $cwd); // determine and add main dirs to the config $home = self::getHomeDir(); $config->merge(array('config' => array( 'home' => $home, 'cache-dir' => self::getCacheDir($home), 'data-dir' => self::getDataDir($home), )), Config::SOURCE_DEFAULT); // load global config $file = new JsonFile($config->get('home').'/config.json'); if ($file->exists()) { if ($io && $io->isDebug()) { $io->writeError('Loading config file ' . $file->getPath()); } $config->merge($file->read(), $file->getPath()); } $config->setConfigSource(new JsonConfigSource($file)); $htaccessProtect = (bool) $config->get('htaccess-protect'); if ($htaccessProtect) { // Protect directory against web access. Since HOME could be // the www-data's user home and be web-accessible it is a // potential security risk $dirs = array($config->get('home'), $config->get('cache-dir'), $config->get('data-dir')); foreach ($dirs as $dir) { if (!file_exists($dir . '/.htaccess')) { if (!is_dir($dir)) { Silencer::call('mkdir', $dir, 0777, true); } Silencer::call('file_put_contents', $dir . '/.htaccess', 'Deny from all'); } } } // load global auth file $file = new JsonFile($config->get('home').'/auth.json'); if ($file->exists()) { if ($io && $io->isDebug()) { $io->writeError('Loading config file ' . $file->getPath()); } $config->merge(array('config' => $file->read()), $file->getPath()); } $config->setAuthConfigSource(new JsonConfigSource($file, true)); // load COMPOSER_AUTH environment variable if set if ($composerAuthEnv = Platform::getEnv('COMPOSER_AUTH')) { $authData = json_decode($composerAuthEnv, true); if (null === $authData) { if ($io) { $io->writeError('COMPOSER_AUTH environment variable is malformed, should be a valid JSON object'); } } else { if ($io && $io->isDebug()) { $io->writeError('Loading auth config from COMPOSER_AUTH'); } $config->merge(array('config' => $authData), 'COMPOSER_AUTH'); } } return $config; } /** * @return string */ public static function getComposerFile() { return trim(Platform::getEnv('COMPOSER')) ?: './composer.json'; } /** * @param string $composerFile * * @return string */ public static function getLockFile($composerFile) { return "json" === pathinfo($composerFile, PATHINFO_EXTENSION) ? substr($composerFile, 0, -4).'lock' : $composerFile . '.lock'; } /** * @return array{highlight: OutputFormatterStyle, warning: OutputFormatterStyle} */ public static function createAdditionalStyles() { return array( 'highlight' => new OutputFormatterStyle('red'), 'warning' => new OutputFormatterStyle('black', 'yellow'), ); } /** * Creates a ConsoleOutput instance * * @return ConsoleOutput */ public static function createOutput() { $styles = self::createAdditionalStyles(); $formatter = new OutputFormatter(false, $styles); return new ConsoleOutput(ConsoleOutput::VERBOSITY_NORMAL, null, $formatter); } /** * Creates a Composer instance * * @param IOInterface $io IO instance * @param array|string|null $localConfig either a configuration array or a filename to read from, if null it will * read from the default filename * @param bool $disablePlugins Whether plugins should not be loaded * @param bool $disableScripts Whether scripts should not be run * @param string|null $cwd * @param bool $fullLoad Whether to initialize everything or only main project stuff (used when loading the global composer) * @throws \InvalidArgumentException * @throws \UnexpectedValueException * @return Composer */ public function createComposer(IOInterface $io, $localConfig = null, $disablePlugins = false, $cwd = null, $fullLoad = true, $disableScripts = false) { $cwd = $cwd ?: (string) getcwd(); // load Composer configuration if (null === $localConfig) { $localConfig = static::getComposerFile(); } $localConfigSource = Config::SOURCE_UNKNOWN; if (is_string($localConfig)) { $composerFile = $localConfig; $file = new JsonFile($localConfig, null, $io); if (!$file->exists()) { if ($localConfig === './composer.json' || $localConfig === 'composer.json') { $message = 'Composer could not find a composer.json file in '.$cwd; } else { $message = 'Composer could not find the config file: '.$localConfig; } $instructions = $fullLoad ? 'To initialize a project, please create a composer.json file. See https://getcomposer.org/basic-usage' : ''; throw new \InvalidArgumentException($message.PHP_EOL.$instructions); } try { $file->validateSchema(JsonFile::LAX_SCHEMA); } catch (JsonValidationException $e) { $errors = ' - ' . implode(PHP_EOL . ' - ', $e->getErrors()); $message = $e->getMessage() . ':' . PHP_EOL . $errors; throw new JsonValidationException($message); } $jsonParser = new JsonParser; try { $jsonParser->parse(file_get_contents($localConfig), JsonParser::DETECT_KEY_CONFLICTS); } catch (DuplicateKeyException $e) { $details = $e->getDetails(); $io->writeError('Key '.$details['key'].' is a duplicate in '.$localConfig.' at line '.$details['line'].''); } $localConfig = $file->read(); $localConfigSource = $file->getPath(); } // Load config and override with local config/auth config $config = static::createConfig($io, $cwd); $config->merge($localConfig, $localConfigSource); if (isset($composerFile)) { $io->writeError('Loading config file ' . $composerFile .' ('.realpath($composerFile).')', true, IOInterface::DEBUG); $config->setConfigSource(new JsonConfigSource(new JsonFile(realpath($composerFile), null, $io))); $localAuthFile = new JsonFile(dirname(realpath($composerFile)) . '/auth.json', null, $io); if ($localAuthFile->exists()) { $io->writeError('Loading config file ' . $localAuthFile->getPath(), true, IOInterface::DEBUG); $config->merge(array('config' => $localAuthFile->read()), $localAuthFile->getPath()); $config->setAuthConfigSource(new JsonConfigSource($localAuthFile, true)); } } $vendorDir = $config->get('vendor-dir'); // initialize composer $composer = new Composer(); $composer->setConfig($config); if ($fullLoad) { // load auth configs into the IO instance $io->loadConfiguration($config); // load existing Composer\InstalledVersions instance if available if (!class_exists('Composer\InstalledVersions', false) && file_exists($installedVersionsPath = $config->get('vendor-dir').'/composer/InstalledVersions.php')) { include $installedVersionsPath; } } $httpDownloader = self::createHttpDownloader($io, $config); $process = new ProcessExecutor($io); $loop = new Loop($httpDownloader, $process); $composer->setLoop($loop); // initialize event dispatcher $dispatcher = new EventDispatcher($composer, $io, $process); $dispatcher->setRunScripts(!$disableScripts); $composer->setEventDispatcher($dispatcher); // initialize repository manager $rm = RepositoryFactory::manager($io, $config, $httpDownloader, $dispatcher, $process); $composer->setRepositoryManager($rm); // force-set the version of the global package if not defined as // guessing it adds no value and only takes time if (!$fullLoad && !isset($localConfig['version'])) { $localConfig['version'] = '1.0.0'; } // load package $parser = new VersionParser; $guesser = new VersionGuesser($config, $process, $parser); $loader = $this->loadRootPackage($rm, $config, $parser, $guesser, $io); $package = $loader->load($localConfig, 'Composer\Package\RootPackage', $cwd); $composer->setPackage($package); // load local repository $this->addLocalRepository($io, $rm, $vendorDir, $package, $process); // initialize installation manager $im = $this->createInstallationManager($loop, $io, $dispatcher); $composer->setInstallationManager($im); if ($fullLoad) { // initialize download manager $dm = $this->createDownloadManager($io, $config, $httpDownloader, $process, $dispatcher); $composer->setDownloadManager($dm); // initialize autoload generator $generator = new AutoloadGenerator($dispatcher, $io); $composer->setAutoloadGenerator($generator); // initialize archive manager $am = $this->createArchiveManager($config, $dm, $loop); $composer->setArchiveManager($am); } // add installers to the manager (must happen after download manager is created since they read it out of $composer) $this->createDefaultInstallers($im, $composer, $io, $process); if ($fullLoad) { $globalComposer = null; if (realpath($config->get('home')) !== $cwd) { $globalComposer = $this->createGlobalComposer($io, $config, $disablePlugins, $disableScripts); } $pm = $this->createPluginManager($io, $composer, $globalComposer, $disablePlugins); $composer->setPluginManager($pm); $pm->loadInstalledPlugins(); } // init locker if possible if ($fullLoad && isset($composerFile)) { $lockFile = self::getLockFile($composerFile); $locker = new Package\Locker($io, new JsonFile($lockFile, null, $io), $im, file_get_contents($composerFile), $process); $composer->setLocker($locker); } if ($fullLoad) { $initEvent = new Event(PluginEvents::INIT); $composer->getEventDispatcher()->dispatch($initEvent->getName(), $initEvent); // once everything is initialized we can // purge packages from local repos if they have been deleted on the filesystem $this->purgePackages($rm->getLocalRepository(), $im); } return $composer; } /** * @param IOInterface $io IO instance * @param bool $disablePlugins Whether plugins should not be loaded * @param bool $disableScripts Whether scripts should not be executed * @return Composer|null */ public static function createGlobal(IOInterface $io, $disablePlugins = false, $disableScripts = false) { $factory = new static(); return $factory->createGlobalComposer($io, static::createConfig($io), $disablePlugins, $disableScripts, true); } /** * @param Repository\RepositoryManager $rm * @param string $vendorDir * * @return void */ protected function addLocalRepository(IOInterface $io, RepositoryManager $rm, $vendorDir, RootPackageInterface $rootPackage, ProcessExecutor $process = null) { $fs = null; if ($process) { $fs = new Filesystem($process); } $rm->setLocalRepository(new Repository\InstalledFilesystemRepository(new JsonFile($vendorDir.'/composer/installed.json', null, $io), true, $rootPackage, $fs)); } /** * @param bool $disablePlugins * @param bool $disableScripts * @param bool $fullLoad * * @return Composer|null */ protected function createGlobalComposer(IOInterface $io, Config $config, $disablePlugins, $disableScripts, $fullLoad = false) { $composer = null; try { $composer = $this->createComposer($io, $config->get('home') . '/composer.json', $disablePlugins, $config->get('home'), $fullLoad, $disableScripts); } catch (\Exception $e) { $io->writeError('Failed to initialize global composer: '.$e->getMessage(), true, IOInterface::DEBUG); } return $composer; } /** * @param IO\IOInterface $io * @param Config $config * @param EventDispatcher $eventDispatcher * @return Downloader\DownloadManager */ public function createDownloadManager(IOInterface $io, Config $config, HttpDownloader $httpDownloader, ProcessExecutor $process, EventDispatcher $eventDispatcher = null) { $cache = null; if ($config->get('cache-files-ttl') > 0) { $cache = new Cache($io, $config->get('cache-files-dir'), 'a-z0-9_./'); $cache->setReadOnly($config->get('cache-read-only')); } $fs = new Filesystem($process); $dm = new Downloader\DownloadManager($io, false, $fs); switch ($preferred = $config->get('preferred-install')) { case 'dist': $dm->setPreferDist(true); break; case 'source': $dm->setPreferSource(true); break; case 'auto': default: // noop break; } if (is_array($preferred)) { $dm->setPreferences($preferred); } $dm->setDownloader('git', new Downloader\GitDownloader($io, $config, $process, $fs)); $dm->setDownloader('svn', new Downloader\SvnDownloader($io, $config, $process, $fs)); $dm->setDownloader('fossil', new Downloader\FossilDownloader($io, $config, $process, $fs)); $dm->setDownloader('hg', new Downloader\HgDownloader($io, $config, $process, $fs)); $dm->setDownloader('perforce', new Downloader\PerforceDownloader($io, $config, $process, $fs)); $dm->setDownloader('zip', new Downloader\ZipDownloader($io, $config, $httpDownloader, $eventDispatcher, $cache, $fs, $process)); $dm->setDownloader('rar', new Downloader\RarDownloader($io, $config, $httpDownloader, $eventDispatcher, $cache, $fs, $process)); $dm->setDownloader('tar', new Downloader\TarDownloader($io, $config, $httpDownloader, $eventDispatcher, $cache, $fs, $process)); $dm->setDownloader('gzip', new Downloader\GzipDownloader($io, $config, $httpDownloader, $eventDispatcher, $cache, $fs, $process)); $dm->setDownloader('xz', new Downloader\XzDownloader($io, $config, $httpDownloader, $eventDispatcher, $cache, $fs, $process)); $dm->setDownloader('phar', new Downloader\PharDownloader($io, $config, $httpDownloader, $eventDispatcher, $cache, $fs, $process)); $dm->setDownloader('file', new Downloader\FileDownloader($io, $config, $httpDownloader, $eventDispatcher, $cache, $fs, $process)); $dm->setDownloader('path', new Downloader\PathDownloader($io, $config, $httpDownloader, $eventDispatcher, $cache, $fs, $process)); return $dm; } /** * @param Config $config The configuration * @param Downloader\DownloadManager $dm Manager use to download sources * @return Archiver\ArchiveManager */ public function createArchiveManager(Config $config, Downloader\DownloadManager $dm, Loop $loop) { $am = new Archiver\ArchiveManager($dm, $loop); $am->addArchiver(new Archiver\ZipArchiver); $am->addArchiver(new Archiver\PharArchiver); return $am; } /** * @param IOInterface $io * @param Composer $composer * @param Composer $globalComposer * @param bool $disablePlugins * @return Plugin\PluginManager */ protected function createPluginManager(IOInterface $io, Composer $composer, Composer $globalComposer = null, $disablePlugins = false) { return new Plugin\PluginManager($io, $composer, $globalComposer, $disablePlugins); } /** * @return Installer\InstallationManager */ public function createInstallationManager(Loop $loop, IOInterface $io, EventDispatcher $eventDispatcher = null) { return new Installer\InstallationManager($loop, $io, $eventDispatcher); } /** * @return void */ protected function createDefaultInstallers(Installer\InstallationManager $im, Composer $composer, IOInterface $io, ProcessExecutor $process = null) { $fs = new Filesystem($process); $binaryInstaller = new Installer\BinaryInstaller($io, rtrim($composer->getConfig()->get('bin-dir'), '/'), $composer->getConfig()->get('bin-compat'), $fs, rtrim($composer->getConfig()->get('vendor-dir'), '/')); $im->addInstaller(new Installer\LibraryInstaller($io, $composer, null, $fs, $binaryInstaller)); $im->addInstaller(new Installer\PluginInstaller($io, $composer, $fs, $binaryInstaller)); $im->addInstaller(new Installer\MetapackageInstaller($io)); } /** * @param InstalledRepositoryInterface $repo repository to purge packages from * @param Installer\InstallationManager $im manager to check whether packages are still installed * * @return void */ protected function purgePackages(InstalledRepositoryInterface $repo, Installer\InstallationManager $im) { foreach ($repo->getPackages() as $package) { if (!$im->isPackageInstalled($repo, $package)) { $repo->removePackage($package); } } } /** * @return Package\Loader\RootPackageLoader */ protected function loadRootPackage(RepositoryManager $rm, Config $config, VersionParser $parser, VersionGuesser $guesser, IOInterface $io) { return new Package\Loader\RootPackageLoader($rm, $config, $parser, $guesser, $io); } /** * @param IOInterface $io IO instance * @param mixed $config either a configuration array or a filename to read from, if null it will read from * the default filename * @param bool $disablePlugins Whether plugins should not be loaded * @param bool $disableScripts Whether scripts should not be run * @return Composer */ public static function create(IOInterface $io, $config = null, $disablePlugins = false, $disableScripts = false) { $factory = new static(); return $factory->createComposer($io, $config, $disablePlugins, null, true, $disableScripts); } /** * If you are calling this in a plugin, you probably should instead use $composer->getLoop()->getHttpDownloader() * * @param IOInterface $io IO instance * @param Config $config Config instance * @param mixed[] $options Array of options passed directly to HttpDownloader constructor * @return HttpDownloader */ public static function createHttpDownloader(IOInterface $io, Config $config, $options = array()) { static $warned = false; $disableTls = false; // allow running the config command if disable-tls is in the arg list, even if openssl is missing, to allow disabling it via the config command if (isset($_SERVER['argv']) && in_array('disable-tls', $_SERVER['argv']) && (in_array('conf', $_SERVER['argv']) || in_array('config', $_SERVER['argv']))) { $warned = true; $disableTls = !extension_loaded('openssl'); } elseif ($config->get('disable-tls') === true) { if (!$warned) { $io->writeError('You are running Composer with SSL/TLS protection disabled.'); } $warned = true; $disableTls = true; } elseif (!extension_loaded('openssl')) { throw new Exception\NoSslException('The openssl extension is required for SSL/TLS protection but is not available. ' . 'If you can not enable the openssl extension, you can disable this error, at your own risk, by setting the \'disable-tls\' option to true.'); } $httpDownloaderOptions = array(); if ($disableTls === false) { if ($config->get('cafile')) { $httpDownloaderOptions['ssl']['cafile'] = $config->get('cafile'); } if ($config->get('capath')) { $httpDownloaderOptions['ssl']['capath'] = $config->get('capath'); } $httpDownloaderOptions = array_replace_recursive($httpDownloaderOptions, $options); } try { $httpDownloader = new HttpDownloader($io, $config, $httpDownloaderOptions, $disableTls); } catch (TransportException $e) { if (false !== strpos($e->getMessage(), 'cafile')) { $io->write('Unable to locate a valid CA certificate file. You must set a valid \'cafile\' option.'); $io->write('A valid CA certificate file is required for SSL/TLS protection.'); if (PHP_VERSION_ID < 50600) { $io->write('It is recommended you upgrade to PHP 5.6+ which can detect your system CA file automatically.'); } $io->write('You can disable this error, at your own risk, by setting the \'disable-tls\' option to true.'); } throw $e; } return $httpDownloader; } /** * @return bool */ private static function useXdg() { foreach (array_keys($_SERVER) as $key) { if (strpos($key, 'XDG_') === 0) { return true; } } if (Silencer::call('is_dir', '/etc/xdg')) { return true; } return false; } /** * @throws \RuntimeException * @return string */ private static function getUserDir() { $home = Platform::getEnv('HOME'); if (!$home) { throw new \RuntimeException('The HOME or COMPOSER_HOME environment variable must be set for composer to run correctly'); } return rtrim(strtr($home, '\\', '/'), '/'); } } * Jordi Boggiano * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Composer; use Composer\Autoload\AutoloadGenerator; use Composer\Console\GithubActionError; use Composer\DependencyResolver\DefaultPolicy; use Composer\DependencyResolver\LocalRepoTransaction; use Composer\DependencyResolver\LockTransaction; use Composer\DependencyResolver\Operation\UpdateOperation; use Composer\DependencyResolver\Operation\InstallOperation; use Composer\DependencyResolver\Operation\UninstallOperation; use Composer\DependencyResolver\PoolOptimizer; use Composer\DependencyResolver\Pool; use Composer\DependencyResolver\Request; use Composer\DependencyResolver\Solver; use Composer\DependencyResolver\SolverProblemsException; use Composer\DependencyResolver\PolicyInterface; use Composer\Downloader\DownloadManager; use Composer\EventDispatcher\EventDispatcher; use Composer\Filter\PlatformRequirementFilter\IgnoreListPlatformRequirementFilter; use Composer\Filter\PlatformRequirementFilter\PlatformRequirementFilterFactory; use Composer\Filter\PlatformRequirementFilter\PlatformRequirementFilterInterface; use Composer\Installer\InstallationManager; use Composer\Installer\InstallerEvents; use Composer\Installer\SuggestedPackagesReporter; use Composer\IO\IOInterface; use Composer\Package\AliasPackage; use Composer\Package\RootAliasPackage; use Composer\Package\BasePackage; use Composer\Package\CompletePackage; use Composer\Package\CompletePackageInterface; use Composer\Package\Link; use Composer\Package\Loader\ArrayLoader; use Composer\Package\Dumper\ArrayDumper; use Composer\Package\Version\VersionParser; use Composer\Package\Package; use Composer\Repository\ArrayRepository; use Composer\Repository\RepositorySet; use Composer\Repository\CompositeRepository; use Composer\Semver\Constraint\Constraint; use Composer\Package\Locker; use Composer\Package\RootPackageInterface; use Composer\Repository\InstalledArrayRepository; use Composer\Repository\InstalledRepositoryInterface; use Composer\Repository\InstalledRepository; use Composer\Repository\RootPackageRepository; use Composer\Repository\PlatformRepository; use Composer\Repository\RepositoryInterface; use Composer\Repository\RepositoryManager; use Composer\Repository\LockArrayRepository; use Composer\Script\ScriptEvents; use Composer\Util\Platform; /** * @author Jordi Boggiano * @author Beau Simensen * @author Konstantin Kudryashov * @author Nils Adermann */ class Installer { const ERROR_NONE = 0; // no error/success state const ERROR_GENERIC_FAILURE = 1; const ERROR_NO_LOCK_FILE_FOR_PARTIAL_UPDATE = 3; const ERROR_LOCK_FILE_INVALID = 4; // used/declared in SolverProblemsException, carried over here for completeness const ERROR_DEPENDENCY_RESOLUTION_FAILED = 2; /** * @var IOInterface */ protected $io; /** * @var Config */ protected $config; /** * @var RootPackageInterface&BasePackage */ protected $package; // TODO can we get rid of the below and just use the package itself? /** * @var RootPackageInterface&BasePackage */ protected $fixedRootPackage; /** * @var DownloadManager */ protected $downloadManager; /** * @var RepositoryManager */ protected $repositoryManager; /** * @var Locker */ protected $locker; /** * @var InstallationManager */ protected $installationManager; /** * @var EventDispatcher */ protected $eventDispatcher; /** * @var AutoloadGenerator */ protected $autoloadGenerator; /** @var bool */ protected $preferSource = false; /** @var bool */ protected $preferDist = false; /** @var bool */ protected $optimizeAutoloader = false; /** @var bool */ protected $classMapAuthoritative = false; /** @var bool */ protected $apcuAutoloader = false; /** @var string|null */ protected $apcuAutoloaderPrefix = null; /** @var bool */ protected $devMode = false; /** @var bool */ protected $dryRun = false; /** @var bool */ protected $verbose = false; /** @var bool */ protected $update = false; /** @var bool */ protected $install = true; /** @var bool */ protected $dumpAutoloader = true; /** @var bool */ protected $runScripts = true; /** @var bool */ protected $preferStable = false; /** @var bool */ protected $preferLowest = false; /** @var bool */ protected $writeLock; /** @var bool */ protected $executeOperations = true; /** @var bool */ protected $updateMirrors = false; /** * Array of package names/globs flagged for update * * @var string[]|null */ protected $updateAllowList = null; /** @var Request::UPDATE_* */ protected $updateAllowTransitiveDependencies = Request::UPDATE_ONLY_LISTED; /** * @var SuggestedPackagesReporter */ protected $suggestedPackagesReporter; /** * @var PlatformRequirementFilterInterface */ protected $platformRequirementFilter; /** * @var ?RepositoryInterface */ protected $additionalFixedRepository; /** * Constructor * * @param IOInterface $io * @param Config $config * @param RootPackageInterface&BasePackage $package * @param DownloadManager $downloadManager * @param RepositoryManager $repositoryManager * @param Locker $locker * @param InstallationManager $installationManager * @param EventDispatcher $eventDispatcher * @param AutoloadGenerator $autoloadGenerator */ public function __construct(IOInterface $io, Config $config, RootPackageInterface $package, DownloadManager $downloadManager, RepositoryManager $repositoryManager, Locker $locker, InstallationManager $installationManager, EventDispatcher $eventDispatcher, AutoloadGenerator $autoloadGenerator) { $this->io = $io; $this->config = $config; $this->package = $package; $this->downloadManager = $downloadManager; $this->repositoryManager = $repositoryManager; $this->locker = $locker; $this->installationManager = $installationManager; $this->eventDispatcher = $eventDispatcher; $this->autoloadGenerator = $autoloadGenerator; $this->suggestedPackagesReporter = new SuggestedPackagesReporter($this->io); $this->platformRequirementFilter = PlatformRequirementFilterFactory::ignoreNothing(); $this->writeLock = $config->get('lock'); } /** * Run installation (or update) * * @throws \Exception * @return int 0 on success or a positive error code on failure * @phpstan-return self::ERROR_* */ public function run() { // Disable GC to save CPU cycles, as the dependency solver can create hundreds of thousands // of PHP objects, the GC can spend quite some time walking the tree of references looking // for stuff to collect while there is nothing to collect. This slows things down dramatically // and turning it off results in much better performance. Do not try this at home however. gc_collect_cycles(); gc_disable(); if ($this->updateAllowList && $this->updateMirrors) { throw new \RuntimeException("The installer options updateMirrors and updateAllowList are mutually exclusive."); } $isFreshInstall = $this->repositoryManager->getLocalRepository()->isFresh(); // Force update if there is no lock file present if (!$this->update && !$this->locker->isLocked()) { $this->io->writeError('No composer.lock file present. Updating dependencies to latest instead of installing from lock file. See https://getcomposer.org/install for more information.'); $this->update = true; } if ($this->dryRun) { $this->verbose = true; $this->runScripts = false; $this->executeOperations = false; $this->writeLock = false; $this->dumpAutoloader = false; $this->mockLocalRepositories($this->repositoryManager); } if ($this->update && !$this->install) { $this->dumpAutoloader = false; } if ($this->runScripts) { Platform::putEnv('COMPOSER_DEV_MODE', $this->devMode ? '1' : '0'); // dispatch pre event // should we treat this more strictly as running an update and then running an install, triggering events multiple times? $eventName = $this->update ? ScriptEvents::PRE_UPDATE_CMD : ScriptEvents::PRE_INSTALL_CMD; $this->eventDispatcher->dispatchScript($eventName, $this->devMode); } $this->downloadManager->setPreferSource($this->preferSource); $this->downloadManager->setPreferDist($this->preferDist); $localRepo = $this->repositoryManager->getLocalRepository(); try { if ($this->update) { $res = $this->doUpdate($localRepo, $this->install); } else { $res = $this->doInstall($localRepo); } if ($res !== 0) { return $res; } } catch (\Exception $e) { if ($this->executeOperations && $this->install && $this->config->get('notify-on-install')) { $this->installationManager->notifyInstalls($this->io); } throw $e; } if ($this->executeOperations && $this->install && $this->config->get('notify-on-install')) { $this->installationManager->notifyInstalls($this->io); } if ($this->update) { $installedRepo = new InstalledRepository(array( $this->locker->getLockedRepository($this->devMode), $this->createPlatformRepo(false), new RootPackageRepository(clone $this->package), )); if ($isFreshInstall) { $this->suggestedPackagesReporter->addSuggestionsFromPackage($this->package); } $this->suggestedPackagesReporter->outputMinimalistic($installedRepo); } // Find abandoned packages and warn user $lockedRepository = $this->locker->getLockedRepository(true); foreach ($lockedRepository->getPackages() as $package) { if (!$package instanceof CompletePackage || !$package->isAbandoned()) { continue; } $replacement = is_string($package->getReplacementPackage()) ? 'Use ' . $package->getReplacementPackage() . ' instead' : 'No replacement was suggested'; $this->io->writeError( sprintf( "Package %s is abandoned, you should avoid using it. %s.", $package->getPrettyName(), $replacement ) ); } if ($this->dumpAutoloader) { // write autoloader if ($this->optimizeAutoloader) { $this->io->writeError('Generating optimized autoload files'); } else { $this->io->writeError('Generating autoload files'); } $this->autoloadGenerator->setClassMapAuthoritative($this->classMapAuthoritative); $this->autoloadGenerator->setApcu($this->apcuAutoloader, $this->apcuAutoloaderPrefix); $this->autoloadGenerator->setRunScripts($this->runScripts); $this->autoloadGenerator->setPlatformRequirementFilter($this->platformRequirementFilter); $this->autoloadGenerator->dump($this->config, $localRepo, $this->package, $this->installationManager, 'composer', $this->optimizeAutoloader); } if ($this->install && $this->executeOperations) { // force binaries re-generation in case they are missing foreach ($localRepo->getPackages() as $package) { $this->installationManager->ensureBinariesPresence($package); } } $fundingCount = 0; foreach ($localRepo->getPackages() as $package) { if ($package instanceof CompletePackageInterface && !$package instanceof AliasPackage && $package->getFunding()) { $fundingCount++; } } if ($fundingCount > 0) { $this->io->writeError(array( sprintf( "%d package%s you are using %s looking for funding.", $fundingCount, 1 === $fundingCount ? '' : 's', 1 === $fundingCount ? 'is' : 'are' ), 'Use the `composer fund` command to find out more!', )); } if ($this->runScripts) { // dispatch post event $eventName = $this->update ? ScriptEvents::POST_UPDATE_CMD : ScriptEvents::POST_INSTALL_CMD; $this->eventDispatcher->dispatchScript($eventName, $this->devMode); } // re-enable GC except on HHVM which triggers a warning here if (!defined('HHVM_VERSION')) { gc_enable(); } return 0; } /** * @param bool $doInstall * * @return int * @phpstan-return self::ERROR_* */ protected function doUpdate(InstalledRepositoryInterface $localRepo, $doInstall) { $platformRepo = $this->createPlatformRepo(true); $aliases = $this->getRootAliases(true); $lockedRepository = null; try { if ($this->locker->isLocked()) { $lockedRepository = $this->locker->getLockedRepository(true); } } catch (\Seld\JsonLint\ParsingException $e) { if ($this->updateAllowList || $this->updateMirrors) { // in case we are doing a partial update or updating mirrors, the lock file is needed so we error throw $e; } // otherwise, ignoring parse errors as the lock file will be regenerated from scratch when // doing a full update } if (($this->updateAllowList || $this->updateMirrors) && !$lockedRepository) { $this->io->writeError('Cannot update ' . ($this->updateMirrors ? 'lock file information' : 'only a partial set of packages') . ' without a lock file present. Run `composer update` to generate a lock file.', true, IOInterface::QUIET); return self::ERROR_NO_LOCK_FILE_FOR_PARTIAL_UPDATE; } $this->io->writeError('Loading composer repositories with package information'); // creating repository set $policy = $this->createPolicy(true); $repositorySet = $this->createRepositorySet(true, $platformRepo, $aliases); $repositories = $this->repositoryManager->getRepositories(); foreach ($repositories as $repository) { $repositorySet->addRepository($repository); } if ($lockedRepository) { $repositorySet->addRepository($lockedRepository); } $request = $this->createRequest($this->fixedRootPackage, $platformRepo, $lockedRepository); $this->requirePackagesForUpdate($request, $lockedRepository, true); // pass the allow list into the request, so the pool builder can apply it if ($this->updateAllowList) { $request->setUpdateAllowList($this->updateAllowList, $this->updateAllowTransitiveDependencies); } $pool = $repositorySet->createPool($request, $this->io, $this->eventDispatcher, $this->createPoolOptimizer($policy)); $this->io->writeError('Updating dependencies'); // solve dependencies $solver = new Solver($policy, $pool, $this->io); try { $lockTransaction = $solver->solve($request, $this->platformRequirementFilter); $ruleSetSize = $solver->getRuleSetSize(); $solver = null; } catch (SolverProblemsException $e) { $err = 'Your requirements could not be resolved to an installable set of packages.'; $prettyProblem = $e->getPrettyString($repositorySet, $request, $pool, $this->io->isVerbose()); $this->io->writeError(''. $err .'', true, IOInterface::QUIET); $this->io->writeError($prettyProblem); if (!$this->devMode) { $this->io->writeError('Running update with --no-dev does not mean require-dev is ignored, it just means the packages will not be installed. If dev requirements are blocking the update you have to resolve those problems.', true, IOInterface::QUIET); } $ghe = new GithubActionError($this->io); $ghe->emit($err."\n".$prettyProblem); return max(self::ERROR_GENERIC_FAILURE, $e->getCode()); } $this->io->writeError("Analyzed ".count($pool)." packages to resolve dependencies", true, IOInterface::VERBOSE); $this->io->writeError("Analyzed ".$ruleSetSize." rules to resolve dependencies", true, IOInterface::VERBOSE); $pool = null; if (!$lockTransaction->getOperations()) { $this->io->writeError('Nothing to modify in lock file'); } $exitCode = $this->extractDevPackages($lockTransaction, $platformRepo, $aliases, $policy, $lockedRepository); if ($exitCode !== 0) { return $exitCode; } // write lock $platformReqs = $this->extractPlatformRequirements($this->package->getRequires()); $platformDevReqs = $this->extractPlatformRequirements($this->package->getDevRequires()); $installsUpdates = $uninstalls = array(); if ($lockTransaction->getOperations()) { $installNames = $updateNames = $uninstallNames = array(); foreach ($lockTransaction->getOperations() as $operation) { if ($operation instanceof InstallOperation) { $installsUpdates[] = $operation; $installNames[] = $operation->getPackage()->getPrettyName().':'.$operation->getPackage()->getFullPrettyVersion(); } elseif ($operation instanceof UpdateOperation) { // when mirrors/metadata from a package gets updated we do not want to list it as an // update in the output as it is only an internal lock file metadata update if ($this->updateMirrors && $operation->getInitialPackage()->getName() == $operation->getTargetPackage()->getName() && $operation->getInitialPackage()->getVersion() == $operation->getTargetPackage()->getVersion() ) { continue; } $installsUpdates[] = $operation; $updateNames[] = $operation->getTargetPackage()->getPrettyName().':'.$operation->getTargetPackage()->getFullPrettyVersion(); } elseif ($operation instanceof UninstallOperation) { $uninstalls[] = $operation; $uninstallNames[] = $operation->getPackage()->getPrettyName(); } } $this->io->writeError(sprintf( "Lock file operations: %d install%s, %d update%s, %d removal%s", count($installNames), 1 === count($installNames) ? '' : 's', count($updateNames), 1 === count($updateNames) ? '' : 's', count($uninstalls), 1 === count($uninstalls) ? '' : 's' )); if ($installNames) { $this->io->writeError("Installs: ".implode(', ', $installNames), true, IOInterface::VERBOSE); } if ($updateNames) { $this->io->writeError("Updates: ".implode(', ', $updateNames), true, IOInterface::VERBOSE); } if ($uninstalls) { $this->io->writeError("Removals: ".implode(', ', $uninstallNames), true, IOInterface::VERBOSE); } } $sortByName = function ($a, $b) { if ($a instanceof UpdateOperation) { $a = $a->getTargetPackage()->getName(); } else { $a = $a->getPackage()->getName(); } if ($b instanceof UpdateOperation) { $b = $b->getTargetPackage()->getName(); } else { $b = $b->getPackage()->getName(); } return strcmp($a, $b); }; usort($uninstalls, $sortByName); usort($installsUpdates, $sortByName); foreach (array_merge($uninstalls, $installsUpdates) as $operation) { // collect suggestions if ($operation instanceof InstallOperation) { $this->suggestedPackagesReporter->addSuggestionsFromPackage($operation->getPackage()); } // output op, but alias op only in debug verbosity if (false === strpos($operation->getOperationType(), 'Alias') || $this->io->isDebug()) { $this->io->writeError(' - ' . $operation->show(true)); } } $updatedLock = $this->locker->setLockData( $lockTransaction->getNewLockPackages(false, $this->updateMirrors), $lockTransaction->getNewLockPackages(true, $this->updateMirrors), $platformReqs, $platformDevReqs, $lockTransaction->getAliases($aliases), $this->package->getMinimumStability(), $this->package->getStabilityFlags(), $this->preferStable || $this->package->getPreferStable(), $this->preferLowest, $this->config->get('platform') ?: array(), $this->writeLock && $this->executeOperations ); if ($updatedLock && $this->writeLock && $this->executeOperations) { $this->io->writeError('Writing lock file'); } // see https://github.com/composer/composer/issues/2764 if ($this->executeOperations && count($lockTransaction->getOperations()) > 0) { $vendorDir = $this->config->get('vendor-dir'); if (is_dir($vendorDir)) { // suppress errors as this fails sometimes on OSX for no apparent reason // see https://github.com/composer/composer/issues/4070#issuecomment-129792748 @touch($vendorDir); } } if ($doInstall) { // TODO ensure lock is used from locker as-is, since it may not have been written to disk in case of executeOperations == false return $this->doInstall($localRepo, true); } return 0; } /** * Run the solver a second time on top of the existing update result with only the current result set in the pool * and see what packages would get removed if we only had the non-dev packages in the solver request * * @param array> $aliases * * @return int * * @phpstan-param list $aliases * @phpstan-return self::ERROR_* */ protected function extractDevPackages(LockTransaction $lockTransaction, PlatformRepository $platformRepo, array $aliases, PolicyInterface $policy, LockArrayRepository $lockedRepository = null) { if (!$this->package->getDevRequires()) { return 0; } $resultRepo = new ArrayRepository(array()); $loader = new ArrayLoader(null, true); $dumper = new ArrayDumper(); foreach ($lockTransaction->getNewLockPackages(false) as $pkg) { $resultRepo->addPackage($loader->load($dumper->dump($pkg))); } $repositorySet = $this->createRepositorySet(true, $platformRepo, $aliases); $repositorySet->addRepository($resultRepo); $request = $this->createRequest($this->fixedRootPackage, $platformRepo); $this->requirePackagesForUpdate($request, $lockedRepository, false); $pool = $repositorySet->createPoolWithAllPackages(); $solver = new Solver($policy, $pool, $this->io); try { $nonDevLockTransaction = $solver->solve($request, $this->platformRequirementFilter); $solver = null; } catch (SolverProblemsException $e) { $err = 'Unable to find a compatible set of packages based on your non-dev requirements alone.'; $prettyProblem = $e->getPrettyString($repositorySet, $request, $pool, $this->io->isVerbose(), true); $this->io->writeError(''. $err .'', true, IOInterface::QUIET); $this->io->writeError('Your requirements can be resolved successfully when require-dev packages are present.'); $this->io->writeError('You may need to move packages from require-dev or some of their dependencies to require.'); $this->io->writeError($prettyProblem); $ghe = new GithubActionError($this->io); $ghe->emit($err."\n".$prettyProblem); return $e->getCode(); } $lockTransaction->setNonDevPackages($nonDevLockTransaction); return 0; } /** * @param InstalledRepositoryInterface $localRepo * @param bool $alreadySolved Whether the function is called as part of an update command or independently * @return int exit code * @phpstan-return self::ERROR_* */ protected function doInstall(InstalledRepositoryInterface $localRepo, $alreadySolved = false) { $this->io->writeError('Installing dependencies from lock file'.($this->devMode ? ' (including require-dev)' : '').''); $lockedRepository = $this->locker->getLockedRepository($this->devMode); // verify that the lock file works with the current platform repository // we can skip this part if we're doing this as the second step after an update if (!$alreadySolved) { $this->io->writeError('Verifying lock file contents can be installed on current platform.'); $platformRepo = $this->createPlatformRepo(false); // creating repository set $policy = $this->createPolicy(false); // use aliases from lock file only, so empty root aliases here $repositorySet = $this->createRepositorySet(false, $platformRepo, array(), $lockedRepository); $repositorySet->addRepository($lockedRepository); // creating requirements request $request = $this->createRequest($this->fixedRootPackage, $platformRepo, $lockedRepository); if (!$this->locker->isFresh()) { $this->io->writeError('Warning: The lock file is not up to date with the latest changes in composer.json. You may be getting outdated dependencies. It is recommended that you run `composer update` or `composer update `.', true, IOInterface::QUIET); } foreach ($lockedRepository->getPackages() as $package) { $request->fixLockedPackage($package); } foreach ($this->locker->getPlatformRequirements($this->devMode) as $link) { $request->requireName($link->getTarget(), $link->getConstraint()); } $pool = $repositorySet->createPool($request, $this->io, $this->eventDispatcher); // solve dependencies $solver = new Solver($policy, $pool, $this->io); try { $lockTransaction = $solver->solve($request, $this->platformRequirementFilter); $solver = null; // installing the locked packages on this platform resulted in lock modifying operations, there wasn't a conflict, but the lock file as-is seems to not work on this system if (0 !== count($lockTransaction->getOperations())) { $this->io->writeError('Your lock file cannot be installed on this system without changes. Please run composer update.', true, IOInterface::QUIET); return self::ERROR_LOCK_FILE_INVALID; } } catch (SolverProblemsException $e) { $err = 'Your lock file does not contain a compatible set of packages. Please run composer update.'; $prettyProblem = $e->getPrettyString($repositorySet, $request, $pool, $this->io->isVerbose()); $this->io->writeError(''. $err .'', true, IOInterface::QUIET); $this->io->writeError($prettyProblem); $ghe = new GithubActionError($this->io); $ghe->emit($err."\n".$prettyProblem); return max(self::ERROR_GENERIC_FAILURE, $e->getCode()); } } // TODO in how far do we need to do anything here to ensure dev packages being updated to latest in lock without version change are treated correctly? $localRepoTransaction = new LocalRepoTransaction($lockedRepository, $localRepo); $this->eventDispatcher->dispatchInstallerEvent(InstallerEvents::PRE_OPERATIONS_EXEC, $this->devMode, $this->executeOperations, $localRepoTransaction); if (!$localRepoTransaction->getOperations()) { $this->io->writeError('Nothing to install, update or remove'); } if ($localRepoTransaction->getOperations()) { $installs = $updates = $uninstalls = array(); foreach ($localRepoTransaction->getOperations() as $operation) { if ($operation instanceof InstallOperation) { $installs[] = $operation->getPackage()->getPrettyName().':'.$operation->getPackage()->getFullPrettyVersion(); } elseif ($operation instanceof UpdateOperation) { $updates[] = $operation->getTargetPackage()->getPrettyName().':'.$operation->getTargetPackage()->getFullPrettyVersion(); } elseif ($operation instanceof UninstallOperation) { $uninstalls[] = $operation->getPackage()->getPrettyName(); } } $this->io->writeError(sprintf( "Package operations: %d install%s, %d update%s, %d removal%s", count($installs), 1 === count($installs) ? '' : 's', count($updates), 1 === count($updates) ? '' : 's', count($uninstalls), 1 === count($uninstalls) ? '' : 's' )); if ($installs) { $this->io->writeError("Installs: ".implode(', ', $installs), true, IOInterface::VERBOSE); } if ($updates) { $this->io->writeError("Updates: ".implode(', ', $updates), true, IOInterface::VERBOSE); } if ($uninstalls) { $this->io->writeError("Removals: ".implode(', ', $uninstalls), true, IOInterface::VERBOSE); } } if ($this->executeOperations) { $localRepo->setDevPackageNames($this->locker->getDevPackageNames()); $this->installationManager->execute($localRepo, $localRepoTransaction->getOperations(), $this->devMode, $this->runScripts); } else { foreach ($localRepoTransaction->getOperations() as $operation) { // output op, but alias op only in debug verbosity if (false === strpos($operation->getOperationType(), 'Alias') || $this->io->isDebug()) { $this->io->writeError(' - ' . $operation->show(false)); } } } return 0; } /** * @param bool $forUpdate * * @return PlatformRepository */ protected function createPlatformRepo($forUpdate) { if ($forUpdate) { $platformOverrides = $this->config->get('platform') ?: array(); } else { $platformOverrides = $this->locker->getPlatformOverrides(); } return new PlatformRepository(array(), $platformOverrides); } /** * @param bool $forUpdate * @param array> $rootAliases * @param RepositoryInterface|null $lockedRepository * * @return RepositorySet * * @phpstan-param list $rootAliases */ private function createRepositorySet($forUpdate, PlatformRepository $platformRepo, array $rootAliases = array(), $lockedRepository = null) { if ($forUpdate) { $minimumStability = $this->package->getMinimumStability(); $stabilityFlags = $this->package->getStabilityFlags(); $requires = array_merge($this->package->getRequires(), $this->package->getDevRequires()); } else { $minimumStability = $this->locker->getMinimumStability(); $stabilityFlags = $this->locker->getStabilityFlags(); $requires = array(); foreach ($lockedRepository->getPackages() as $package) { $constraint = new Constraint('=', $package->getVersion()); $constraint->setPrettyString($package->getPrettyVersion()); $requires[$package->getName()] = $constraint; } } $rootRequires = array(); foreach ($requires as $req => $constraint) { if ($constraint instanceof Link) { $constraint = $constraint->getConstraint(); } // skip platform requirements from the root package to avoid filtering out existing platform packages if ($this->platformRequirementFilter->isIgnored($req)) { continue; } elseif ($this->platformRequirementFilter instanceof IgnoreListPlatformRequirementFilter) { $constraint = $this->platformRequirementFilter->filterConstraint($req, $constraint); } $rootRequires[$req] = $constraint; } $this->fixedRootPackage = clone $this->package; $this->fixedRootPackage->setRequires(array()); $this->fixedRootPackage->setDevRequires(array()); $stabilityFlags[$this->package->getName()] = BasePackage::$stabilities[VersionParser::parseStability($this->package->getVersion())]; $repositorySet = new RepositorySet($minimumStability, $stabilityFlags, $rootAliases, $this->package->getReferences(), $rootRequires); $repositorySet->addRepository(new RootPackageRepository($this->fixedRootPackage)); $repositorySet->addRepository($platformRepo); if ($this->additionalFixedRepository) { // allow using installed repos if needed to avoid warnings about installed repositories being used in the RepositorySet // see https://github.com/composer/composer/pull/9574 $additionalFixedRepositories = $this->additionalFixedRepository; if ($additionalFixedRepositories instanceof CompositeRepository) { $additionalFixedRepositories = $additionalFixedRepositories->getRepositories(); } else { $additionalFixedRepositories = array($additionalFixedRepositories); } foreach ($additionalFixedRepositories as $additionalFixedRepository) { if ($additionalFixedRepository instanceof InstalledRepository || $additionalFixedRepository instanceof InstalledRepositoryInterface) { $repositorySet->allowInstalledRepositories(); break; } } $repositorySet->addRepository($this->additionalFixedRepository); } return $repositorySet; } /** * @param bool $forUpdate * * @return DefaultPolicy */ private function createPolicy($forUpdate) { $preferStable = null; $preferLowest = null; if (!$forUpdate) { $preferStable = $this->locker->getPreferStable(); $preferLowest = $this->locker->getPreferLowest(); } // old lock file without prefer stable/lowest will return null // so in this case we use the composer.json info if (null === $preferStable) { $preferStable = $this->preferStable || $this->package->getPreferStable(); } if (null === $preferLowest) { $preferLowest = $this->preferLowest; } return new DefaultPolicy($preferStable, $preferLowest); } /** * @param RootPackageInterface&BasePackage $rootPackage * @return Request */ private function createRequest(RootPackageInterface $rootPackage, PlatformRepository $platformRepo, LockArrayRepository $lockedRepository = null) { $request = new Request($lockedRepository); $request->fixPackage($rootPackage); if ($rootPackage instanceof RootAliasPackage) { $request->fixPackage($rootPackage->getAliasOf()); } $fixedPackages = $platformRepo->getPackages(); if ($this->additionalFixedRepository) { $fixedPackages = array_merge($fixedPackages, $this->additionalFixedRepository->getPackages()); } // fix the version of all platform packages + additionally installed packages // to prevent the solver trying to remove or update those // TODO why not replaces? $provided = $rootPackage->getProvides(); foreach ($fixedPackages as $package) { // skip platform packages that are provided by the root package if ($package->getRepository() !== $platformRepo || !isset($provided[$package->getName()]) || !$provided[$package->getName()]->getConstraint()->matches(new Constraint('=', $package->getVersion())) ) { $request->fixPackage($package); } } return $request; } /** * @param LockArrayRepository|null $lockedRepository * @param bool $includeDevRequires * * @return void */ private function requirePackagesForUpdate(Request $request, LockArrayRepository $lockedRepository = null, $includeDevRequires = true) { // if we're updating mirrors we want to keep exactly the same versions installed which are in the lock file, but we want current remote metadata if ($this->updateMirrors) { $excludedPackages = array(); if (!$includeDevRequires) { $excludedPackages = array_flip($this->locker->getDevPackageNames()); } foreach ($lockedRepository->getPackages() as $lockedPackage) { // exclude alias packages here as for root aliases, both alias and aliased are // present in the lock repo and we only want to require the aliased version if (!$lockedPackage instanceof AliasPackage && !isset($excludedPackages[$lockedPackage->getName()])) { $request->requireName($lockedPackage->getName(), new Constraint('==', $lockedPackage->getVersion())); } } } else { $links = $this->package->getRequires(); if ($includeDevRequires) { $links = array_merge($links, $this->package->getDevRequires()); } foreach ($links as $link) { $request->requireName($link->getTarget(), $link->getConstraint()); } } } /** * @param bool $forUpdate * * @return array> * * @phpstan-return list */ private function getRootAliases($forUpdate) { if ($forUpdate) { $aliases = $this->package->getAliases(); } else { $aliases = $this->locker->getAliases(); } return $aliases; } /** * @param Link[] $links * * @return array */ private function extractPlatformRequirements(array $links) { $platformReqs = array(); foreach ($links as $link) { if (PlatformRepository::isPlatformPackage($link->getTarget())) { $platformReqs[$link->getTarget()] = $link->getPrettyConstraint(); } } return $platformReqs; } /** * Replace local repositories with InstalledArrayRepository instances * * This is to prevent any accidental modification of the existing repos on disk * * @return void */ private function mockLocalRepositories(RepositoryManager $rm) { $packages = array(); foreach ($rm->getLocalRepository()->getPackages() as $package) { $packages[(string) $package] = clone $package; } foreach ($packages as $key => $package) { if ($package instanceof AliasPackage) { $alias = (string) $package->getAliasOf(); $className = get_class($package); $packages[$key] = new $className($packages[$alias], $package->getVersion(), $package->getPrettyVersion()); } } $rm->setLocalRepository( new InstalledArrayRepository($packages) ); } /** * @return PoolOptimizer|null */ private function createPoolOptimizer(PolicyInterface $policy) { // Not the best architectural decision here, would need to be able // to configure from the outside of Installer but this is only // a debugging tool and should never be required in any other use case if ('0' === Platform::getEnv('COMPOSER_POOL_OPTIMIZER')) { $this->io->write('Pool Optimizer was disabled for debugging purposes.', true, IOInterface::DEBUG); return null; } return new PoolOptimizer($policy); } /** * Create Installer * * @param IOInterface $io * @param Composer $composer * @return Installer */ public static function create(IOInterface $io, Composer $composer) { return new static( $io, $composer->getConfig(), $composer->getPackage(), $composer->getDownloadManager(), $composer->getRepositoryManager(), $composer->getLocker(), $composer->getInstallationManager(), $composer->getEventDispatcher(), $composer->getAutoloadGenerator() ); } /** * @param RepositoryInterface $additionalFixedRepository * @return $this */ public function setAdditionalFixedRepository(RepositoryInterface $additionalFixedRepository) { $this->additionalFixedRepository = $additionalFixedRepository; return $this; } /** * Whether to run in drymode or not * * @param bool $dryRun * @return Installer */ public function setDryRun($dryRun = true) { $this->dryRun = (bool) $dryRun; return $this; } /** * Checks, if this is a dry run (simulation mode). * * @return bool */ public function isDryRun() { return $this->dryRun; } /** * prefer source installation * * @param bool $preferSource * @return Installer */ public function setPreferSource($preferSource = true) { $this->preferSource = (bool) $preferSource; return $this; } /** * prefer dist installation * * @param bool $preferDist * @return Installer */ public function setPreferDist($preferDist = true) { $this->preferDist = (bool) $preferDist; return $this; } /** * Whether or not generated autoloader are optimized * * @param bool $optimizeAutoloader * @return Installer */ public function setOptimizeAutoloader($optimizeAutoloader) { $this->optimizeAutoloader = (bool) $optimizeAutoloader; if (!$this->optimizeAutoloader) { // Force classMapAuthoritative off when not optimizing the // autoloader $this->setClassMapAuthoritative(false); } return $this; } /** * Whether or not generated autoloader considers the class map * authoritative. * * @param bool $classMapAuthoritative * @return Installer */ public function setClassMapAuthoritative($classMapAuthoritative) { $this->classMapAuthoritative = (bool) $classMapAuthoritative; if ($this->classMapAuthoritative) { // Force optimizeAutoloader when classmap is authoritative $this->setOptimizeAutoloader(true); } return $this; } /** * Whether or not generated autoloader considers APCu caching. * * @param bool $apcuAutoloader * @param string|null $apcuAutoloaderPrefix * @return Installer */ public function setApcuAutoloader($apcuAutoloader, $apcuAutoloaderPrefix = null) { $this->apcuAutoloader = $apcuAutoloader; $this->apcuAutoloaderPrefix = $apcuAutoloaderPrefix; return $this; } /** * update packages * * @param bool $update * @return Installer */ public function setUpdate($update) { $this->update = (bool) $update; return $this; } /** * Allows disabling the install step after an update * * @param bool $install * @return Installer */ public function setInstall($install) { $this->install = (bool) $install; return $this; } /** * enables dev packages * * @param bool $devMode * @return Installer */ public function setDevMode($devMode = true) { $this->devMode = (bool) $devMode; return $this; } /** * set whether to run autoloader or not * * This is disabled implicitly when enabling dryRun * * @param bool $dumpAutoloader * @return Installer */ public function setDumpAutoloader($dumpAutoloader = true) { $this->dumpAutoloader = (bool) $dumpAutoloader; return $this; } /** * set whether to run scripts or not * * This is disabled implicitly when enabling dryRun * * @param bool $runScripts * @return Installer * @deprecated Use setRunScripts(false) on the EventDispatcher instance being injected instead */ public function setRunScripts($runScripts = true) { $this->runScripts = (bool) $runScripts; return $this; } /** * set the config instance * * @param Config $config * @return Installer */ public function setConfig(Config $config) { $this->config = $config; return $this; } /** * run in verbose mode * * @param bool $verbose * @return Installer */ public function setVerbose($verbose = true) { $this->verbose = (bool) $verbose; return $this; } /** * Checks, if running in verbose mode. * * @return bool */ public function isVerbose() { return $this->verbose; } /** * set ignore Platform Package requirements * * If this is set to true, all platform requirements are ignored * If this is set to false, no platform requirements are ignored * If this is set to string[], those packages will be ignored * * @param bool|string[] $ignorePlatformReqs * * @return Installer * * @deprecated use setPlatformRequirementFilter instead */ public function setIgnorePlatformRequirements($ignorePlatformReqs) { trigger_error('Installer::setIgnorePlatformRequirements is deprecated since Composer 2.2, use setPlatformRequirementFilter instead.', E_USER_DEPRECATED); return $this->setPlatformRequirementFilter(PlatformRequirementFilterFactory::fromBoolOrList($ignorePlatformReqs)); } /** * @param PlatformRequirementFilterInterface $platformRequirementFilter * @return Installer */ public function setPlatformRequirementFilter(PlatformRequirementFilterInterface $platformRequirementFilter) { $this->platformRequirementFilter = $platformRequirementFilter; return $this; } /** * Update the lock file to the exact same versions and references but use current remote metadata like URLs and mirror info * * @param bool $updateMirrors * @return Installer */ public function setUpdateMirrors($updateMirrors) { $this->updateMirrors = $updateMirrors; return $this; } /** * restrict the update operation to a few packages, all other packages * that are already installed will be kept at their current version * * @param string[] $packages * * @return Installer */ public function setUpdateAllowList(array $packages) { $this->updateAllowList = array_flip(array_map('strtolower', $packages)); return $this; } /** * Should dependencies of packages marked for update be updated? * * Depending on the chosen constant this will either only update the directly named packages, all transitive * dependencies which are not root requirement or all transitive dependencies including root requirements * * @param int $updateAllowTransitiveDependencies One of the UPDATE_ constants on the Request class * @return Installer */ public function setUpdateAllowTransitiveDependencies($updateAllowTransitiveDependencies) { if (!in_array($updateAllowTransitiveDependencies, array(Request::UPDATE_ONLY_LISTED, Request::UPDATE_LISTED_WITH_TRANSITIVE_DEPS_NO_ROOT_REQUIRE, Request::UPDATE_LISTED_WITH_TRANSITIVE_DEPS), true)) { throw new \RuntimeException("Invalid value for updateAllowTransitiveDependencies supplied"); } $this->updateAllowTransitiveDependencies = $updateAllowTransitiveDependencies; return $this; } /** * Should packages be preferred in a stable version when updating? * * @param bool $preferStable * @return Installer */ public function setPreferStable($preferStable = true) { $this->preferStable = (bool) $preferStable; return $this; } /** * Should packages be preferred in a lowest version when updating? * * @param bool $preferLowest * @return Installer */ public function setPreferLowest($preferLowest = true) { $this->preferLowest = (bool) $preferLowest; return $this; } /** * Should the lock file be updated when updating? * * This is disabled implicitly when enabling dryRun * * @param bool $writeLock * @return Installer */ public function setWriteLock($writeLock = true) { $this->writeLock = (bool) $writeLock; return $this; } /** * Should the operations (package install, update and removal) be executed on disk? * * This is disabled implicitly when enabling dryRun * * @param bool $executeOperations * @return Installer */ public function setExecuteOperations($executeOperations = true) { $this->executeOperations = (bool) $executeOperations; return $this; } /** * Disables plugins. * * Call this if you want to ensure that third-party code never gets * executed. The default is to automatically install, and execute * custom third-party installers. * * @return Installer */ public function disablePlugins() { $this->installationManager->disablePlugins(); return $this; } /** * @param SuggestedPackagesReporter $suggestedPackagesReporter * @return Installer */ public function setSuggestedPackagesReporter(SuggestedPackagesReporter $suggestedPackagesReporter) { $this->suggestedPackagesReporter = $suggestedPackagesReporter; return $this; } } * Jordi Boggiano * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Composer\IO; use Composer\Question\StrictConfirmationQuestion; use Symfony\Component\Console\Helper\HelperSet; use Symfony\Component\Console\Helper\ProgressBar; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\ConsoleOutputInterface; use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Console\Question\ChoiceQuestion; use Symfony\Component\Console\Question\Question; /** * The Input/Output helper. * * @author François Pluchino * @author Jordi Boggiano */ class ConsoleIO extends BaseIO { /** @var InputInterface */ protected $input; /** @var OutputInterface */ protected $output; /** @var HelperSet */ protected $helperSet; /** @var string */ protected $lastMessage = ''; /** @var string */ protected $lastMessageErr = ''; /** @var float */ private $startTime; /** @var array */ private $verbosityMap; /** * Constructor. * * @param InputInterface $input The input instance * @param OutputInterface $output The output instance * @param HelperSet $helperSet The helperSet instance */ public function __construct(InputInterface $input, OutputInterface $output, HelperSet $helperSet) { $this->input = $input; $this->output = $output; $this->helperSet = $helperSet; $this->verbosityMap = array( self::QUIET => OutputInterface::VERBOSITY_QUIET, self::NORMAL => OutputInterface::VERBOSITY_NORMAL, self::VERBOSE => OutputInterface::VERBOSITY_VERBOSE, self::VERY_VERBOSE => OutputInterface::VERBOSITY_VERY_VERBOSE, self::DEBUG => OutputInterface::VERBOSITY_DEBUG, ); } /** * @param float $startTime * * @return void */ public function enableDebugging($startTime) { $this->startTime = $startTime; } /** * @inheritDoc */ public function isInteractive() { return $this->input->isInteractive(); } /** * @inheritDoc */ public function isDecorated() { return $this->output->isDecorated(); } /** * @inheritDoc */ public function isVerbose() { return $this->output->getVerbosity() >= OutputInterface::VERBOSITY_VERBOSE; } /** * @inheritDoc */ public function isVeryVerbose() { return $this->output->getVerbosity() >= OutputInterface::VERBOSITY_VERY_VERBOSE; } /** * @inheritDoc */ public function isDebug() { return $this->output->getVerbosity() >= OutputInterface::VERBOSITY_DEBUG; } /** * @inheritDoc */ public function write($messages, $newline = true, $verbosity = self::NORMAL) { $this->doWrite($messages, $newline, false, $verbosity); } /** * @inheritDoc */ public function writeError($messages, $newline = true, $verbosity = self::NORMAL) { $this->doWrite($messages, $newline, true, $verbosity); } /** * @inheritDoc */ public function writeRaw($messages, $newline = true, $verbosity = self::NORMAL) { $this->doWrite($messages, $newline, false, $verbosity, true); } /** * @inheritDoc */ public function writeErrorRaw($messages, $newline = true, $verbosity = self::NORMAL) { $this->doWrite($messages, $newline, true, $verbosity, true); } /** * @param string[]|string $messages * @param bool $newline * @param bool $stderr * @param int $verbosity * @param bool $raw * * @return void */ private function doWrite($messages, $newline, $stderr, $verbosity, $raw = false) { $sfVerbosity = $this->verbosityMap[$verbosity]; if ($sfVerbosity > $this->output->getVerbosity()) { return; } if ($raw) { if ($sfVerbosity === OutputInterface::OUTPUT_NORMAL) { $sfVerbosity = OutputInterface::OUTPUT_RAW; } else { $sfVerbosity |= OutputInterface::OUTPUT_RAW; } } if (null !== $this->startTime) { $memoryUsage = memory_get_usage() / 1024 / 1024; $timeSpent = microtime(true) - $this->startTime; $messages = array_map(function ($message) use ($memoryUsage, $timeSpent) { return sprintf('[%.1fMiB/%.2fs] %s', $memoryUsage, $timeSpent, $message); }, (array) $messages); } if (true === $stderr && $this->output instanceof ConsoleOutputInterface) { $this->output->getErrorOutput()->write($messages, $newline, $sfVerbosity); $this->lastMessageErr = implode($newline ? "\n" : '', (array) $messages); return; } $this->output->write($messages, $newline, $sfVerbosity); $this->lastMessage = implode($newline ? "\n" : '', (array) $messages); } /** * @inheritDoc */ public function overwrite($messages, $newline = true, $size = null, $verbosity = self::NORMAL) { $this->doOverwrite($messages, $newline, $size, false, $verbosity); } /** * @inheritDoc */ public function overwriteError($messages, $newline = true, $size = null, $verbosity = self::NORMAL) { $this->doOverwrite($messages, $newline, $size, true, $verbosity); } /** * @param string[]|string $messages * @param bool $newline * @param int|null $size * @param bool $stderr * @param int $verbosity * * @return void */ private function doOverwrite($messages, $newline, $size, $stderr, $verbosity) { // messages can be an array, let's convert it to string anyway $messages = implode($newline ? "\n" : '', (array) $messages); // since overwrite is supposed to overwrite last message... if (!isset($size)) { // removing possible formatting of lastMessage with strip_tags $size = strlen(strip_tags($stderr ? $this->lastMessageErr : $this->lastMessage)); } // ...let's fill its length with backspaces $this->doWrite(str_repeat("\x08", $size), false, $stderr, $verbosity); // write the new message $this->doWrite($messages, false, $stderr, $verbosity); // In cmd.exe on Win8.1 (possibly 10?), the line can not be cleared, so we need to // track the length of previous output and fill it with spaces to make sure the line is cleared. // See https://github.com/composer/composer/pull/5836 for more details $fill = $size - strlen(strip_tags($messages)); if ($fill > 0) { // whitespace whatever has left $this->doWrite(str_repeat(' ', $fill), false, $stderr, $verbosity); // move the cursor back $this->doWrite(str_repeat("\x08", $fill), false, $stderr, $verbosity); } if ($newline) { $this->doWrite('', true, $stderr, $verbosity); } if ($stderr) { $this->lastMessageErr = $messages; } else { $this->lastMessage = $messages; } } /** * @param int $max * @return ProgressBar */ public function getProgressBar($max = 0) { return new ProgressBar($this->getErrorOutput(), $max); } /** * @inheritDoc */ public function ask($question, $default = null) { /** @var \Symfony\Component\Console\Helper\QuestionHelper $helper */ $helper = $this->helperSet->get('question'); $question = new Question($question, $default); return $helper->ask($this->input, $this->getErrorOutput(), $question); } /** * @inheritDoc */ public function askConfirmation($question, $default = true) { /** @var \Symfony\Component\Console\Helper\QuestionHelper $helper */ $helper = $this->helperSet->get('question'); $question = new StrictConfirmationQuestion($question, $default); return $helper->ask($this->input, $this->getErrorOutput(), $question); } /** * @inheritDoc */ public function askAndValidate($question, $validator, $attempts = null, $default = null) { /** @var \Symfony\Component\Console\Helper\QuestionHelper $helper */ $helper = $this->helperSet->get('question'); $question = new Question($question, $default); $question->setValidator($validator); $question->setMaxAttempts($attempts); return $helper->ask($this->input, $this->getErrorOutput(), $question); } /** * @inheritDoc */ public function askAndHideAnswer($question) { /** @var \Symfony\Component\Console\Helper\QuestionHelper $helper */ $helper = $this->helperSet->get('question'); $question = new Question($question); $question->setHidden(true); return $helper->ask($this->input, $this->getErrorOutput(), $question); } /** * @inheritDoc */ public function select($question, $choices, $default, $attempts = false, $errorMessage = 'Value "%s" is invalid', $multiselect = false) { /** @var \Symfony\Component\Console\Helper\QuestionHelper $helper */ $helper = $this->helperSet->get('question'); $question = new ChoiceQuestion($question, $choices, $default); $question->setMaxAttempts($attempts ?: null); // IOInterface requires false, and Question requires null or int $question->setErrorMessage($errorMessage); $question->setMultiselect($multiselect); $result = $helper->ask($this->input, $this->getErrorOutput(), $question); if (!is_array($result)) { return (string) array_search($result, $choices, true); } $results = array(); foreach ($choices as $index => $choice) { if (in_array($choice, $result, true)) { $results[] = (string) $index; } } return $results; } /** * @return OutputInterface */ private function getErrorOutput() { if ($this->output instanceof ConsoleOutputInterface) { return $this->output->getErrorOutput(); } return $this->output; } } * Jordi Boggiano * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Composer\IO; /** * IOInterface that is not interactive and never writes the output * * @author Christophe Coevoet */ class NullIO extends BaseIO { /** * @inheritDoc */ public function isInteractive() { return false; } /** * @inheritDoc */ public function isVerbose() { return false; } /** * @inheritDoc */ public function isVeryVerbose() { return false; } /** * @inheritDoc */ public function isDebug() { return false; } /** * @inheritDoc */ public function isDecorated() { return false; } /** * @inheritDoc */ public function write($messages, $newline = true, $verbosity = self::NORMAL) { } /** * @inheritDoc */ public function writeError($messages, $newline = true, $verbosity = self::NORMAL) { } /** * @inheritDoc */ public function overwrite($messages, $newline = true, $size = 80, $verbosity = self::NORMAL) { } /** * @inheritDoc */ public function overwriteError($messages, $newline = true, $size = 80, $verbosity = self::NORMAL) { } /** * @inheritDoc */ public function ask($question, $default = null) { return $default; } /** * @inheritDoc */ public function askConfirmation($question, $default = true) { return $default; } /** * @inheritDoc */ public function askAndValidate($question, $validator, $attempts = null, $default = null) { return $default; } /** * @inheritDoc */ public function askAndHideAnswer($question) { return null; } /** * @inheritDoc */ public function select($question, $choices, $default, $attempts = false, $errorMessage = 'Value "%s" is invalid', $multiselect = false) { return $default; } } * Jordi Boggiano * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Composer\IO; use Composer\Config; use Composer\Pcre\Preg; use Composer\Util\ProcessExecutor; use Psr\Log\LogLevel; abstract class BaseIO implements IOInterface { /** @var array */ protected $authentications = array(); /** * @inheritDoc */ public function getAuthentications() { return $this->authentications; } /** * @return void */ public function resetAuthentications() { $this->authentications = array(); } /** * @inheritDoc */ public function hasAuthentication($repositoryName) { return isset($this->authentications[$repositoryName]); } /** * @inheritDoc */ public function getAuthentication($repositoryName) { if (isset($this->authentications[$repositoryName])) { return $this->authentications[$repositoryName]; } return array('username' => null, 'password' => null); } /** * @inheritDoc */ public function setAuthentication($repositoryName, $username, $password = null) { $this->authentications[$repositoryName] = array('username' => $username, 'password' => $password); } /** * @inheritDoc */ public function writeRaw($messages, $newline = true, $verbosity = self::NORMAL) { $this->write($messages, $newline, $verbosity); } /** * @inheritDoc */ public function writeErrorRaw($messages, $newline = true, $verbosity = self::NORMAL) { $this->writeError($messages, $newline, $verbosity); } /** * Check for overwrite and set the authentication information for the repository. * * @param string $repositoryName The unique name of repository * @param string $username The username * @param string $password The password * * @return void */ protected function checkAndSetAuthentication($repositoryName, $username, $password = null) { if ($this->hasAuthentication($repositoryName)) { $auth = $this->getAuthentication($repositoryName); if ($auth['username'] === $username && $auth['password'] === $password) { return; } $this->writeError( sprintf( "Warning: You should avoid overwriting already defined auth settings for %s.", $repositoryName ) ); } $this->setAuthentication($repositoryName, $username, $password); } /** * @inheritDoc */ public function loadConfiguration(Config $config) { $bitbucketOauth = $config->get('bitbucket-oauth') ?: array(); $githubOauth = $config->get('github-oauth') ?: array(); $gitlabOauth = $config->get('gitlab-oauth') ?: array(); $gitlabToken = $config->get('gitlab-token') ?: array(); $httpBasic = $config->get('http-basic') ?: array(); $bearerToken = $config->get('bearer') ?: array(); // reload oauth tokens from config if available foreach ($bitbucketOauth as $domain => $cred) { $this->checkAndSetAuthentication($domain, $cred['consumer-key'], $cred['consumer-secret']); } foreach ($githubOauth as $domain => $token) { // allowed chars for GH tokens are from https://github.blog/changelog/2021-03-04-authentication-token-format-updates/ // plus dots which were at some point used for GH app integration tokens if (!Preg::isMatch('{^[.A-Za-z0-9_]+$}', $token)) { throw new \UnexpectedValueException('Your github oauth token for '.$domain.' contains invalid characters: "'.$token.'"'); } $this->checkAndSetAuthentication($domain, $token, 'x-oauth-basic'); } foreach ($gitlabOauth as $domain => $token) { $this->checkAndSetAuthentication($domain, $token, 'oauth2'); } foreach ($gitlabToken as $domain => $token) { $username = is_array($token) && array_key_exists("username", $token) ? $token["username"] : $token; $password = is_array($token) && array_key_exists("token", $token) ? $token["token"] : 'private-token'; $this->checkAndSetAuthentication($domain, $username, $password); } // reload http basic credentials from config if available foreach ($httpBasic as $domain => $cred) { $this->checkAndSetAuthentication($domain, $cred['username'], $cred['password']); } foreach ($bearerToken as $domain => $token) { $this->checkAndSetAuthentication($domain, $token, 'bearer'); } // setup process timeout ProcessExecutor::setTimeout((int) $config->get('process-timeout')); } /** * @return void */ public function emergency($message, array $context = array()) { $this->log(LogLevel::EMERGENCY, $message, $context); } /** * @return void */ public function alert($message, array $context = array()) { $this->log(LogLevel::ALERT, $message, $context); } /** * @return void */ public function critical($message, array $context = array()) { $this->log(LogLevel::CRITICAL, $message, $context); } /** * @return void */ public function error($message, array $context = array()) { $this->log(LogLevel::ERROR, $message, $context); } /** * @return void */ public function warning($message, array $context = array()) { $this->log(LogLevel::WARNING, $message, $context); } /** * @return void */ public function notice($message, array $context = array()) { $this->log(LogLevel::NOTICE, $message, $context); } /** * @return void */ public function info($message, array $context = array()) { $this->log(LogLevel::INFO, $message, $context); } /** * @return void */ public function debug($message, array $context = array()) { $this->log(LogLevel::DEBUG, $message, $context); } /** * @return void */ public function log($level, $message, array $context = array()) { if (in_array($level, array(LogLevel::EMERGENCY, LogLevel::ALERT, LogLevel::CRITICAL, LogLevel::ERROR))) { $this->writeError(''.$message.''); } elseif ($level === LogLevel::WARNING) { $this->writeError(''.$message.''); } elseif ($level === LogLevel::NOTICE) { $this->writeError(''.$message.'', true, self::VERBOSE); } elseif ($level === LogLevel::INFO) { $this->writeError(''.$message.'', true, self::VERY_VERBOSE); } else { $this->writeError($message, true, self::DEBUG); } } } * Jordi Boggiano * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Composer\IO; use Composer\Config; use Psr\Log\LoggerInterface; /** * The Input/Output helper interface. * * @author François Pluchino */ interface IOInterface extends LoggerInterface { const QUIET = 1; const NORMAL = 2; const VERBOSE = 4; const VERY_VERBOSE = 8; const DEBUG = 16; /** * Is this input means interactive? * * @return bool */ public function isInteractive(); /** * Is this output verbose? * * @return bool */ public function isVerbose(); /** * Is the output very verbose? * * @return bool */ public function isVeryVerbose(); /** * Is the output in debug verbosity? * * @return bool */ public function isDebug(); /** * Is this output decorated? * * @return bool */ public function isDecorated(); /** * Writes a message to the output. * * @param string|string[] $messages The message as an array of lines or a single string * @param bool $newline Whether to add a newline or not * @param int $verbosity Verbosity level from the VERBOSITY_* constants * * @return void */ public function write($messages, $newline = true, $verbosity = self::NORMAL); /** * Writes a message to the error output. * * @param string|string[] $messages The message as an array of lines or a single string * @param bool $newline Whether to add a newline or not * @param int $verbosity Verbosity level from the VERBOSITY_* constants * * @return void */ public function writeError($messages, $newline = true, $verbosity = self::NORMAL); /** * Writes a message to the output, without formatting it. * * @param string|string[] $messages The message as an array of lines or a single string * @param bool $newline Whether to add a newline or not * @param int $verbosity Verbosity level from the VERBOSITY_* constants * * @return void */ public function writeRaw($messages, $newline = true, $verbosity = self::NORMAL); /** * Writes a message to the error output, without formatting it. * * @param string|string[] $messages The message as an array of lines or a single string * @param bool $newline Whether to add a newline or not * @param int $verbosity Verbosity level from the VERBOSITY_* constants * * @return void */ public function writeErrorRaw($messages, $newline = true, $verbosity = self::NORMAL); /** * Overwrites a previous message to the output. * * @param string|string[] $messages The message as an array of lines or a single string * @param bool $newline Whether to add a newline or not * @param int $size The size of line * @param int $verbosity Verbosity level from the VERBOSITY_* constants * * @return void */ public function overwrite($messages, $newline = true, $size = null, $verbosity = self::NORMAL); /** * Overwrites a previous message to the error output. * * @param string|string[] $messages The message as an array of lines or a single string * @param bool $newline Whether to add a newline or not * @param int $size The size of line * @param int $verbosity Verbosity level from the VERBOSITY_* constants * * @return void */ public function overwriteError($messages, $newline = true, $size = null, $verbosity = self::NORMAL); /** * Asks a question to the user. * * @param string $question The question to ask * @param string $default The default answer if none is given by the user * * @throws \RuntimeException If there is no data to read in the input stream * @return string|null The user answer */ public function ask($question, $default = null); /** * Asks a confirmation to the user. * * The question will be asked until the user answers by nothing, yes, or no. * * @param string $question The question to ask * @param bool $default The default answer if the user enters nothing * * @return bool true if the user has confirmed, false otherwise */ public function askConfirmation($question, $default = true); /** * Asks for a value and validates the response. * * The validator receives the data to validate. It must return the * validated data when the data is valid and throw an exception * otherwise. * * @param string $question The question to ask * @param callable $validator A PHP callback * @param null|int $attempts Max number of times to ask before giving up (default of null means infinite) * @param mixed $default The default answer if none is given by the user * * @throws \Exception When any of the validators return an error * @return mixed */ public function askAndValidate($question, $validator, $attempts = null, $default = null); /** * Asks a question to the user and hide the answer. * * @param string $question The question to ask * * @return string|null The answer */ public function askAndHideAnswer($question); /** * Asks the user to select a value. * * @param string $question The question to ask * @param string[] $choices List of choices to pick from * @param bool|string $default The default answer if the user enters nothing * @param bool|int $attempts Max number of times to ask before giving up (false by default, which means infinite) * @param string $errorMessage Message which will be shown if invalid value from choice list would be picked * @param bool $multiselect Select more than one value separated by comma * * @throws \InvalidArgumentException * @return int|string|string[]|bool The selected value or values (the key of the choices array) */ public function select($question, $choices, $default, $attempts = false, $errorMessage = 'Value "%s" is invalid', $multiselect = false); /** * Get all authentication information entered. * * @return array The map of authentication data */ public function getAuthentications(); /** * Verify if the repository has a authentication information. * * @param string $repositoryName The unique name of repository * * @return bool */ public function hasAuthentication($repositoryName); /** * Get the username and password of repository. * * @param string $repositoryName The unique name of repository * * @return array{username: string|null, password: string|null} */ public function getAuthentication($repositoryName); /** * Set the authentication information for the repository. * * @param string $repositoryName The unique name of repository * @param string $username The username * @param ?string $password The password * * @return void */ public function setAuthentication($repositoryName, $username, $password = null); /** * Loads authentications from a config instance * * @param Config $config * * @return void */ public function loadConfiguration(Config $config); } * Jordi Boggiano * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Composer\IO; use Composer\Pcre\Preg; use Symfony\Component\Console\Helper\QuestionHelper; use Symfony\Component\Console\Output\StreamOutput; use Symfony\Component\Console\Formatter\OutputFormatterInterface; use Symfony\Component\Console\Input\StreamableInputInterface; use Symfony\Component\Console\Input\StringInput; use Symfony\Component\Console\Helper\HelperSet; /** * @author Jordi Boggiano */ class BufferIO extends ConsoleIO { /** @var StringInput */ protected $input; /** @var StreamOutput */ protected $output; /** * @param string $input * @param int $verbosity * @param OutputFormatterInterface|null $formatter */ public function __construct($input = '', $verbosity = StreamOutput::VERBOSITY_NORMAL, OutputFormatterInterface $formatter = null) { $input = new StringInput($input); $input->setInteractive(false); $output = new StreamOutput(fopen('php://memory', 'rw'), $verbosity, $formatter ? $formatter->isDecorated() : false, $formatter); parent::__construct($input, $output, new HelperSet(array( new QuestionHelper(), ))); } /** * @return string output */ public function getOutput() { fseek($this->output->getStream(), 0); $output = stream_get_contents($this->output->getStream()); $output = Preg::replaceCallback("{(?<=^|\n|\x08)(.+?)(\x08+)}", function ($matches) { $pre = strip_tags($matches[1]); if (strlen($pre) === strlen($matches[2])) { return ''; } // TODO reverse parse the string, skipping span tags and \033\[([0-9;]+)m(.*?)\033\[0m style blobs return rtrim($matches[1])."\n"; }, $output); return $output; } /** * @param string[] $inputs * * @see createStream * * @return void */ public function setUserInputs(array $inputs) { if (!$this->input instanceof StreamableInputInterface) { throw new \RuntimeException('Setting the user inputs requires at least the version 3.2 of the symfony/console component.'); } $this->input->setStream($this->createStream($inputs)); $this->input->setInteractive(true); } /** * @param string[] $inputs * * @return false|resource stream */ private function createStream(array $inputs) { $stream = fopen('php://memory', 'r+'); foreach ($inputs as $input) { fwrite($stream, $input.PHP_EOL); } rewind($stream); return $stream; } } * Jordi Boggiano * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Composer\Package; use Composer\Repository\RepositoryInterface; /** * Defines the essential information a package has that is used during solving/installation * * @author Jordi Boggiano */ interface PackageInterface { const DISPLAY_SOURCE_REF_IF_DEV = 0; const DISPLAY_SOURCE_REF = 1; const DISPLAY_DIST_REF = 2; /** * Returns the package's name without version info, thus not a unique identifier * * @return string package name */ public function getName(); /** * Returns the package's pretty (i.e. with proper case) name * * @return string package name */ public function getPrettyName(); /** * Returns a set of names that could refer to this package * * No version or release type information should be included in any of the * names. Provided or replaced package names need to be returned as well. * * @param bool $provides Whether provided names should be included * * @return string[] An array of strings referring to this package */ public function getNames($provides = true); /** * Allows the solver to set an id for this package to refer to it. * * @param int $id * * @return void */ public function setId($id); /** * Retrieves the package's id set through setId * * @return int The previously set package id */ public function getId(); /** * Returns whether the package is a development virtual package or a concrete one * * @return bool */ public function isDev(); /** * Returns the package type, e.g. library * * @return string The package type */ public function getType(); /** * Returns the package targetDir property * * @return ?string The package targetDir */ public function getTargetDir(); /** * Returns the package extra data * * @return mixed[] The package extra data */ public function getExtra(); /** * Sets source from which this package was installed (source/dist). * * @param string $type source/dist * @phpstan-param 'source'|'dist'|null $type * * @return void */ public function setInstallationSource($type); /** * Returns source from which this package was installed (source/dist). * * @return ?string source/dist * @phpstan-return 'source'|'dist'|null */ public function getInstallationSource(); /** * Returns the repository type of this package, e.g. git, svn * * @return ?string The repository type */ public function getSourceType(); /** * Returns the repository url of this package, e.g. git://github.com/naderman/composer.git * * @return ?string The repository url */ public function getSourceUrl(); /** * Returns the repository urls of this package including mirrors, e.g. git://github.com/naderman/composer.git * * @return string[] */ public function getSourceUrls(); /** * Returns the repository reference of this package, e.g. master, 1.0.0 or a commit hash for git * * @return ?string The repository reference */ public function getSourceReference(); /** * Returns the source mirrors of this package * * @return ?array */ public function getSourceMirrors(); /** * @param ?array $mirrors * @return void */ public function setSourceMirrors($mirrors); /** * Returns the type of the distribution archive of this version, e.g. zip, tarball * * @return ?string The repository type */ public function getDistType(); /** * Returns the url of the distribution archive of this version * * @return ?string */ public function getDistUrl(); /** * Returns the urls of the distribution archive of this version, including mirrors * * @return string[] */ public function getDistUrls(); /** * Returns the reference of the distribution archive of this version, e.g. master, 1.0.0 or a commit hash for git * * @return ?string */ public function getDistReference(); /** * Returns the sha1 checksum for the distribution archive of this version * * @return ?string */ public function getDistSha1Checksum(); /** * Returns the dist mirrors of this package * * @return ?array */ public function getDistMirrors(); /** * @param ?array $mirrors * @return void */ public function setDistMirrors($mirrors); /** * Returns the version of this package * * @return string version */ public function getVersion(); /** * Returns the pretty (i.e. non-normalized) version string of this package * * @return string version */ public function getPrettyVersion(); /** * Returns the pretty version string plus a git or hg commit hash of this package * * @see getPrettyVersion * * @param bool $truncate If the source reference is a sha1 hash, truncate it * @param int $displayMode One of the DISPLAY_ constants on this interface determining display of references * @return string version * * @phpstan-param self::DISPLAY_SOURCE_REF_IF_DEV|self::DISPLAY_SOURCE_REF|self::DISPLAY_DIST_REF $displayMode */ public function getFullPrettyVersion($truncate = true, $displayMode = self::DISPLAY_SOURCE_REF_IF_DEV); /** * Returns the release date of the package * * @return ?\DateTime */ public function getReleaseDate(); /** * Returns the stability of this package: one of (dev, alpha, beta, RC, stable) * * @return string * * @phpstan-return 'stable'|'RC'|'beta'|'alpha'|'dev' */ public function getStability(); /** * Returns a set of links to packages which need to be installed before * this package can be installed * * @return array A map of package links defining required packages, indexed by the require package's name */ public function getRequires(); /** * Returns a set of links to packages which must not be installed at the * same time as this package * * @return Link[] An array of package links defining conflicting packages */ public function getConflicts(); /** * Returns a set of links to virtual packages that are provided through * this package * * @return Link[] An array of package links defining provided packages */ public function getProvides(); /** * Returns a set of links to packages which can alternatively be * satisfied by installing this package * * @return Link[] An array of package links defining replaced packages */ public function getReplaces(); /** * Returns a set of links to packages which are required to develop * this package. These are installed if in dev mode. * * @return array A map of package links defining packages required for development, indexed by the require package's name */ public function getDevRequires(); /** * Returns a set of package names and reasons why they are useful in * combination with this package. * * @return array An array of package suggestions with descriptions * @phpstan-return array */ public function getSuggests(); /** * Returns an associative array of autoloading rules * * {"": {""}} * * Type is either "psr-4", "psr-0", "classmap" or "files". Namespaces are mapped to * directories for autoloading using the type specified. * * @return array Mapping of autoloading rules * @phpstan-return array{psr-0?: array, psr-4?: array, classmap?: list, files?: list} */ public function getAutoload(); /** * Returns an associative array of dev autoloading rules * * {"": {""}} * * Type is either "psr-4", "psr-0", "classmap" or "files". Namespaces are mapped to * directories for autoloading using the type specified. * * @return array Mapping of dev autoloading rules * @phpstan-return array{psr-0?: array, psr-4?: array, classmap?: list, files?: list} */ public function getDevAutoload(); /** * Returns a list of directories which should get added to PHP's * include path. * * @return string[] */ public function getIncludePaths(); /** * Stores a reference to the repository that owns the package * * @param RepositoryInterface $repository * * @return void */ public function setRepository(RepositoryInterface $repository); /** * Returns a reference to the repository that owns the package * * @return ?RepositoryInterface */ public function getRepository(); /** * Returns the package binaries * * @return string[] */ public function getBinaries(); /** * Returns package unique name, constructed from name and version. * * @return string */ public function getUniqueName(); /** * Returns the package notification url * * @return ?string */ public function getNotificationUrl(); /** * Converts the package into a readable and unique string * * @return string */ public function __toString(); /** * Converts the package into a pretty readable string * * @return string */ public function getPrettyString(); /** * @return bool */ public function isDefaultBranch(); /** * Returns a list of options to download package dist files * * @return mixed[] */ public function getTransportOptions(); /** * Configures the list of options to download package dist files * * @param mixed[] $options * * @return void */ public function setTransportOptions(array $options); /** * @param string $reference * * @return void */ public function setSourceReference($reference); /** * @param string $url * * @return void */ public function setDistUrl($url); /** * @param string $type * * @return void */ public function setDistType($type); /** * @param string $reference * * @return void */ public function setDistReference($reference); /** * Set dist and source references and update dist URL for ones that contain a reference * * @param string $reference * * @return void */ public function setSourceDistReferences($reference); } * Jordi Boggiano * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Composer\Package; /** * Package containing additional metadata that is not used by the solver * * @author Nils Adermann */ class CompletePackage extends Package implements CompletePackageInterface { /** @var mixed[] */ protected $repositories = array(); /** @var string[] */ protected $license = array(); /** @var string[] */ protected $keywords = array(); /** @var array */ protected $authors = array(); /** @var ?string */ protected $description = null; /** @var ?string */ protected $homepage = null; /** @var array Map of script name to array of handlers */ protected $scripts = array(); /** @var array{issues?: string, forum?: string, wiki?: string, source?: string, email?: string, irc?: string, docs?: string, rss?: string, chat?: string} */ protected $support = array(); /** @var array */ protected $funding = array(); /** @var bool|string */ protected $abandoned = false; /** @var ?string */ protected $archiveName = null; /** @var string[] */ protected $archiveExcludes = array(); /** * @inheritDoc */ public function setScripts(array $scripts) { $this->scripts = $scripts; } /** * @inheritDoc */ public function getScripts() { return $this->scripts; } /** * @inheritDoc */ public function setRepositories(array $repositories) { $this->repositories = $repositories; } /** * @inheritDoc */ public function getRepositories() { return $this->repositories; } /** * @inheritDoc */ public function setLicense(array $license) { $this->license = $license; } /** * @inheritDoc */ public function getLicense() { return $this->license; } /** * @inheritDoc */ public function setKeywords(array $keywords) { $this->keywords = $keywords; } /** * @inheritDoc */ public function getKeywords() { return $this->keywords; } /** * @inheritDoc */ public function setAuthors(array $authors) { $this->authors = $authors; } /** * @inheritDoc */ public function getAuthors() { return $this->authors; } /** * @inheritDoc */ public function setDescription($description) { $this->description = $description; } /** * @inheritDoc */ public function getDescription() { return $this->description; } /** * @inheritDoc */ public function setHomepage($homepage) { $this->homepage = $homepage; } /** * @inheritDoc */ public function getHomepage() { return $this->homepage; } /** * @inheritDoc */ public function setSupport(array $support) { $this->support = $support; } /** * @inheritDoc */ public function getSupport() { return $this->support; } /** * @inheritDoc */ public function setFunding(array $funding) { $this->funding = $funding; } /** * @inheritDoc */ public function getFunding() { return $this->funding; } /** * @inheritDoc */ public function isAbandoned() { return (bool) $this->abandoned; } /** * @inheritDoc */ public function setAbandoned($abandoned) { $this->abandoned = $abandoned; } /** * @inheritDoc */ public function getReplacementPackage() { return \is_string($this->abandoned) ? $this->abandoned : null; } /** * @inheritDoc */ public function setArchiveName($name) { $this->archiveName = $name; } /** * @inheritDoc */ public function getArchiveName() { return $this->archiveName; } /** * @inheritDoc */ public function setArchiveExcludes(array $excludes) { $this->archiveExcludes = $excludes; } /** * @inheritDoc */ public function getArchiveExcludes() { return $this->archiveExcludes; } } * Jordi Boggiano * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Composer\Package\Loader; use Composer\Package\CompletePackageInterface; use Composer\Package\CompletePackage; use Composer\Package\CompleteAliasPackage; use Composer\Package\RootAliasPackage; use Composer\Package\RootPackage; /** * Defines a loader that takes an array to create package instances * * @author Jordi Boggiano */ interface LoaderInterface { /** * Converts a package from an array to a real instance * * @template PackageClass of CompletePackageInterface * * @param mixed[] $config package data * @param string $class FQCN to be instantiated * * @return CompletePackage|CompleteAliasPackage|RootPackage|RootAliasPackage * * @phpstan-param class-string $class */ public function load(array $config, $class = 'Composer\Package\CompletePackage'); } * Jordi Boggiano * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Composer\Package\Loader; /** * @author Jordi Boggiano */ class InvalidPackageException extends \Exception { /** @var string[] */ private $errors; /** @var string[] */ private $warnings; /** @var mixed[] package config */ private $data; /** * @param string[] $errors * @param string[] $warnings * @param mixed[] $data */ public function __construct(array $errors, array $warnings, array $data) { $this->errors = $errors; $this->warnings = $warnings; $this->data = $data; parent::__construct("Invalid package information: \n".implode("\n", array_merge($errors, $warnings))); } /** * @return mixed[] */ public function getData() { return $this->data; } /** * @return string[] */ public function getErrors() { return $this->errors; } /** * @return string[] */ public function getWarnings() { return $this->warnings; } } * Jordi Boggiano * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Composer\Package\Loader; use Composer\Json\JsonFile; use Composer\Package\CompletePackage; use Composer\Package\CompleteAliasPackage; /** * @author Konstantin Kudryashiv */ class JsonLoader { /** @var LoaderInterface */ private $loader; public function __construct(LoaderInterface $loader) { $this->loader = $loader; } /** * @param string|JsonFile $json A filename, json string or JsonFile instance to load the package from * @return CompletePackage|CompleteAliasPackage */ public function load($json) { if ($json instanceof JsonFile) { $config = $json->read(); } elseif (file_exists($json)) { $config = JsonFile::parseJson(file_get_contents($json), $json); } elseif (is_string($json)) { $config = JsonFile::parseJson($json); } else { throw new \InvalidArgumentException(sprintf( "JsonLoader: Unknown \$json parameter %s. Please report at https://github.com/composer/composer/issues/new.", gettype($json) )); } return $this->loader->load($config); } } * Jordi Boggiano * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Composer\Package\Loader; use Composer\Package\BasePackage; use Composer\Pcre\Preg; use Composer\Semver\Constraint\Constraint; use Composer\Package\Version\VersionParser; use Composer\Repository\PlatformRepository; use Composer\Spdx\SpdxLicenses; /** * @author Jordi Boggiano */ class ValidatingArrayLoader implements LoaderInterface { const CHECK_ALL = 3; const CHECK_UNBOUND_CONSTRAINTS = 1; const CHECK_STRICT_CONSTRAINTS = 2; /** @var LoaderInterface */ private $loader; /** @var VersionParser */ private $versionParser; /** @var string[] */ private $errors; /** @var string[] */ private $warnings; /** @var mixed[] */ private $config; /** @var int One or more of self::CHECK_* constants */ private $flags; /** * @param true $strictName * @param int $flags */ public function __construct(LoaderInterface $loader, $strictName = true, VersionParser $parser = null, $flags = 0) { $this->loader = $loader; $this->versionParser = $parser ?: new VersionParser(); $this->flags = $flags; if ($strictName !== true) { // @phpstan-ignore-line trigger_error('$strictName must be set to true in ValidatingArrayLoader\'s constructor as of 2.2, and it will be removed in 3.0', E_USER_DEPRECATED); } } /** * @inheritDoc */ public function load(array $config, $class = 'Composer\Package\CompletePackage') { $this->errors = array(); $this->warnings = array(); $this->config = $config; $this->validateString('name', true); if ($err = self::hasPackageNamingError($config['name'])) { $this->errors[] = 'name : '.$err; } if (!empty($this->config['version'])) { if (!is_scalar($this->config['version'])) { $this->validateString('version'); } else { if (!is_string($this->config['version'])) { $this->config['version'] = (string) $this->config['version']; } try { $this->versionParser->normalize($this->config['version']); } catch (\Exception $e) { $this->errors[] = 'version : invalid value ('.$this->config['version'].'): '.$e->getMessage(); unset($this->config['version']); } } } if (!empty($this->config['config']['platform'])) { foreach ((array) $this->config['config']['platform'] as $key => $platform) { if (false === $platform) { continue; } if (!is_string($platform)) { $this->errors[] = 'config.platform.' . $key . ' : invalid value ('.gettype($platform).' '.var_export($platform, true).'): expected string or false'; continue; } try { $this->versionParser->normalize($platform); } catch (\Exception $e) { $this->errors[] = 'config.platform.' . $key . ' : invalid value ('.$platform.'): '.$e->getMessage(); } } } $this->validateRegex('type', '[A-Za-z0-9-]+'); $this->validateString('target-dir'); $this->validateArray('extra'); if (isset($this->config['bin'])) { if (is_string($this->config['bin'])) { $this->validateString('bin'); } else { $this->validateFlatArray('bin'); } } $this->validateArray('scripts'); // TODO validate event names & listener syntax $this->validateString('description'); $this->validateUrl('homepage'); $this->validateFlatArray('keywords', '[\p{N}\p{L} ._-]+'); $releaseDate = null; $this->validateString('time'); if (!empty($this->config['time'])) { try { $releaseDate = new \DateTime($this->config['time'], new \DateTimeZone('UTC')); } catch (\Exception $e) { $this->errors[] = 'time : invalid value ('.$this->config['time'].'): '.$e->getMessage(); unset($this->config['time']); } } // check for license validity on newly updated branches if (isset($this->config['license']) && (!$releaseDate || $releaseDate->getTimestamp() >= strtotime('-8days'))) { if (is_array($this->config['license']) || is_string($this->config['license'])) { $licenses = (array) $this->config['license']; $licenseValidator = new SpdxLicenses(); foreach ($licenses as $license) { // replace proprietary by MIT for validation purposes since it's not a valid SPDX identifier, but is accepted by composer if ('proprietary' === $license) { continue; } $licenseToValidate = str_replace('proprietary', 'MIT', $license); if (!$licenseValidator->validate($licenseToValidate)) { if ($licenseValidator->validate(trim($licenseToValidate))) { $this->warnings[] = sprintf( 'License %s must not contain extra spaces, make sure to trim it.', json_encode($license) ); } else { $this->warnings[] = sprintf( 'License %s is not a valid SPDX license identifier, see https://spdx.org/licenses/ if you use an open license.' . PHP_EOL . 'If the software is closed-source, you may use "proprietary" as license.', json_encode($license) ); } } } } } if ($this->validateArray('authors') && !empty($this->config['authors'])) { foreach ($this->config['authors'] as $key => $author) { if (!is_array($author)) { $this->errors[] = 'authors.'.$key.' : should be an array, '.gettype($author).' given'; unset($this->config['authors'][$key]); continue; } foreach (array('homepage', 'email', 'name', 'role') as $authorData) { if (isset($author[$authorData]) && !is_string($author[$authorData])) { $this->errors[] = 'authors.'.$key.'.'.$authorData.' : invalid value, must be a string'; unset($this->config['authors'][$key][$authorData]); } } if (isset($author['homepage']) && !$this->filterUrl($author['homepage'])) { $this->warnings[] = 'authors.'.$key.'.homepage : invalid value ('.$author['homepage'].'), must be an http/https URL'; unset($this->config['authors'][$key]['homepage']); } if (isset($author['email']) && !filter_var($author['email'], FILTER_VALIDATE_EMAIL)) { $this->warnings[] = 'authors.'.$key.'.email : invalid value ('.$author['email'].'), must be a valid email address'; unset($this->config['authors'][$key]['email']); } if (empty($this->config['authors'][$key])) { unset($this->config['authors'][$key]); } } if (empty($this->config['authors'])) { unset($this->config['authors']); } } if ($this->validateArray('support') && !empty($this->config['support'])) { foreach (array('issues', 'forum', 'wiki', 'source', 'email', 'irc', 'docs', 'rss', 'chat') as $key) { if (isset($this->config['support'][$key]) && !is_string($this->config['support'][$key])) { $this->errors[] = 'support.'.$key.' : invalid value, must be a string'; unset($this->config['support'][$key]); } } if (isset($this->config['support']['email']) && !filter_var($this->config['support']['email'], FILTER_VALIDATE_EMAIL)) { $this->warnings[] = 'support.email : invalid value ('.$this->config['support']['email'].'), must be a valid email address'; unset($this->config['support']['email']); } if (isset($this->config['support']['irc']) && !$this->filterUrl($this->config['support']['irc'], array('irc', 'ircs'))) { $this->warnings[] = 'support.irc : invalid value ('.$this->config['support']['irc'].'), must be a irc:/// or ircs:// URL'; unset($this->config['support']['irc']); } foreach (array('issues', 'forum', 'wiki', 'source', 'docs', 'chat') as $key) { if (isset($this->config['support'][$key]) && !$this->filterUrl($this->config['support'][$key])) { $this->warnings[] = 'support.'.$key.' : invalid value ('.$this->config['support'][$key].'), must be an http/https URL'; unset($this->config['support'][$key]); } } if (empty($this->config['support'])) { unset($this->config['support']); } } if ($this->validateArray('funding') && !empty($this->config['funding'])) { foreach ($this->config['funding'] as $key => $fundingOption) { if (!is_array($fundingOption)) { $this->errors[] = 'funding.'.$key.' : should be an array, '.gettype($fundingOption).' given'; unset($this->config['funding'][$key]); continue; } foreach (array('type', 'url') as $fundingData) { if (isset($fundingOption[$fundingData]) && !is_string($fundingOption[$fundingData])) { $this->errors[] = 'funding.'.$key.'.'.$fundingData.' : invalid value, must be a string'; unset($this->config['funding'][$key][$fundingData]); } } if (isset($fundingOption['url']) && !$this->filterUrl($fundingOption['url'])) { $this->warnings[] = 'funding.'.$key.'.url : invalid value ('.$fundingOption['url'].'), must be an http/https URL'; unset($this->config['funding'][$key]['url']); } if (empty($this->config['funding'][$key])) { unset($this->config['funding'][$key]); } } if (empty($this->config['funding'])) { unset($this->config['funding']); } } $unboundConstraint = new Constraint('=', '10000000-dev'); $stableConstraint = new Constraint('=', '1.0.0'); foreach (array_keys(BasePackage::$supportedLinkTypes) as $linkType) { if ($this->validateArray($linkType) && isset($this->config[$linkType])) { foreach ($this->config[$linkType] as $package => $constraint) { if (0 === strcasecmp($package, $this->config['name'])) { $this->errors[] = $linkType.'.'.$package.' : a package cannot set a '.$linkType.' on itself'; unset($this->config[$linkType][$package]); continue; } if ($err = self::hasPackageNamingError($package, true)) { $this->errors[] = $linkType.'.'.$err; } elseif (!Preg::isMatch('{^[A-Za-z0-9_./-]+$}', $package)) { $this->warnings[] = $linkType.'.'.$package.' : invalid key, package names must be strings containing only [A-Za-z0-9_./-]'; } if (!is_string($constraint)) { $this->errors[] = $linkType.'.'.$package.' : invalid value, must be a string containing a version constraint'; unset($this->config[$linkType][$package]); } elseif ('self.version' !== $constraint) { try { $linkConstraint = $this->versionParser->parseConstraints($constraint); } catch (\Exception $e) { $this->errors[] = $linkType.'.'.$package.' : invalid version constraint ('.$e->getMessage().')'; unset($this->config[$linkType][$package]); continue; } // check requires for unbound constraints on non-platform packages if ( ($this->flags & self::CHECK_UNBOUND_CONSTRAINTS) && 'require' === $linkType && $linkConstraint->matches($unboundConstraint) && !PlatformRepository::isPlatformPackage($package) ) { $this->warnings[] = $linkType.'.'.$package.' : unbound version constraints ('.$constraint.') should be avoided'; } elseif ( // check requires for exact constraints ($this->flags & self::CHECK_STRICT_CONSTRAINTS) && 'require' === $linkType && strpos($linkConstraint, '=') === 0 && $stableConstraint->versionCompare($stableConstraint, $linkConstraint, '<=') ) { $this->warnings[] = $linkType.'.'.$package.' : exact version constraints ('.$constraint.') should be avoided if the package follows semantic versioning'; } } if ($linkType === 'conflict' && isset($this->config['replace']) && $keys = array_intersect_key($this->config['replace'], $this->config['conflict'])) { $this->errors[] = $linkType.'.'.$package.' : you cannot conflict with a package that is also replaced, as replace already creates an implicit conflict rule'; unset($this->config[$linkType][$package]); } } } } if ($this->validateArray('suggest') && !empty($this->config['suggest'])) { foreach ($this->config['suggest'] as $package => $description) { if (!is_string($description)) { $this->errors[] = 'suggest.'.$package.' : invalid value, must be a string describing why the package is suggested'; unset($this->config['suggest'][$package]); } } } if ($this->validateString('minimum-stability') && !empty($this->config['minimum-stability'])) { if (!isset(BasePackage::$stabilities[strtolower($this->config['minimum-stability'])]) && $this->config['minimum-stability'] !== 'RC') { $this->errors[] = 'minimum-stability : invalid value ('.$this->config['minimum-stability'].'), must be one of '.implode(', ', array_keys(BasePackage::$stabilities)); unset($this->config['minimum-stability']); } } if ($this->validateArray('autoload') && !empty($this->config['autoload'])) { $types = array('psr-0', 'psr-4', 'classmap', 'files', 'exclude-from-classmap'); foreach ($this->config['autoload'] as $type => $typeConfig) { if (!in_array($type, $types)) { $this->errors[] = 'autoload : invalid value ('.$type.'), must be one of '.implode(', ', $types); unset($this->config['autoload'][$type]); } if ($type === 'psr-4') { foreach ($typeConfig as $namespace => $dirs) { if ($namespace !== '' && '\\' !== substr($namespace, -1)) { $this->errors[] = 'autoload.psr-4 : invalid value ('.$namespace.'), namespaces must end with a namespace separator, should be '.$namespace.'\\\\'; } } } } } if (!empty($this->config['autoload']['psr-4']) && !empty($this->config['target-dir'])) { $this->errors[] = 'target-dir : this can not be used together with the autoload.psr-4 setting, remove target-dir to upgrade to psr-4'; // Unset the psr-4 setting, since unsetting target-dir might // interfere with other settings. unset($this->config['autoload']['psr-4']); } foreach (array('source', 'dist') as $srcType) { if ($this->validateArray($srcType) && !empty($this->config[$srcType])) { if (!isset($this->config[$srcType]['type'])) { $this->errors[] = $srcType . '.type : must be present'; } if (!isset($this->config[$srcType]['url'])) { $this->errors[] = $srcType . '.url : must be present'; } if ($srcType === 'source' && !isset($this->config[$srcType]['reference'])) { $this->errors[] = $srcType . '.reference : must be present'; } if (!is_string($this->config[$srcType]['type'])) { $this->errors[] = $srcType . '.type : should be a string, '.gettype($this->config[$srcType]['type']).' given'; } if (!is_string($this->config[$srcType]['url'])) { $this->errors[] = $srcType . '.url : should be a string, '.gettype($this->config[$srcType]['url']).' given'; } if (isset($this->config[$srcType]['reference']) && !is_string($this->config[$srcType]['reference']) && !is_int($this->config[$srcType]['reference'])) { $this->errors[] = $srcType . '.reference : should be a string or int, '.gettype($this->config[$srcType]['reference']).' given'; } if (isset($this->config[$srcType]['reference']) && Preg::isMatch('{^\s*-}', (string) $this->config[$srcType]['reference'])) { $this->errors[] = $srcType . '.reference : must not start with a "-", "'.$this->config[$srcType]['reference'].'" given'; } if (Preg::isMatch('{^\s*-}', $this->config[$srcType]['url'])) { $this->errors[] = $srcType . '.url : must not start with a "-", "'.$this->config[$srcType]['url'].'" given'; } } } // TODO validate repositories // TODO validate package repositories' packages using this recursively $this->validateFlatArray('include-path'); $this->validateArray('transport-options'); // branch alias validation if (isset($this->config['extra']['branch-alias'])) { if (!is_array($this->config['extra']['branch-alias'])) { $this->errors[] = 'extra.branch-alias : must be an array of versions => aliases'; } else { foreach ($this->config['extra']['branch-alias'] as $sourceBranch => $targetBranch) { if (!is_string($targetBranch)) { $this->warnings[] = 'extra.branch-alias.'.$sourceBranch.' : the target branch ('.json_encode($targetBranch).') must be a string, "'.gettype($targetBranch).'" received.'; unset($this->config['extra']['branch-alias'][$sourceBranch]); continue; } // ensure it is an alias to a -dev package if ('-dev' !== substr($targetBranch, -4)) { $this->warnings[] = 'extra.branch-alias.'.$sourceBranch.' : the target branch ('.$targetBranch.') must end in -dev'; unset($this->config['extra']['branch-alias'][$sourceBranch]); continue; } // normalize without -dev and ensure it's a numeric branch that is parseable $validatedTargetBranch = $this->versionParser->normalizeBranch(substr($targetBranch, 0, -4)); if ('-dev' !== substr($validatedTargetBranch, -4)) { $this->warnings[] = 'extra.branch-alias.'.$sourceBranch.' : the target branch ('.$targetBranch.') must be a parseable number like 2.0-dev'; unset($this->config['extra']['branch-alias'][$sourceBranch]); continue; } // If using numeric aliases ensure the alias is a valid subversion if (($sourcePrefix = $this->versionParser->parseNumericAliasPrefix($sourceBranch)) && ($targetPrefix = $this->versionParser->parseNumericAliasPrefix($targetBranch)) && (stripos($targetPrefix, $sourcePrefix) !== 0) ) { $this->warnings[] = 'extra.branch-alias.'.$sourceBranch.' : the target branch ('.$targetBranch.') is not a valid numeric alias for this version'; unset($this->config['extra']['branch-alias'][$sourceBranch]); } } } } if ($this->errors) { throw new InvalidPackageException($this->errors, $this->warnings, $config); } $package = $this->loader->load($this->config, $class); $this->config = array(); return $package; } /** * @return string[] */ public function getWarnings() { return $this->warnings; } /** * @return string[] */ public function getErrors() { return $this->errors; } /** * @param string $name * @param bool $isLink * * @return string|null */ public static function hasPackageNamingError($name, $isLink = false) { if (PlatformRepository::isPlatformPackage($name)) { return null; } if (!Preg::isMatch('{^[a-z0-9](?:[_.-]?[a-z0-9]+)*/[a-z0-9](?:(?:[_.]?|-{0,2})[a-z0-9]+)*$}iD', $name)) { return $name.' is invalid, it should have a vendor name, a forward slash, and a package name. The vendor and package name can be words separated by -, . or _. The complete name should match "^[a-z0-9]([_.-]?[a-z0-9]+)*/[a-z0-9](([_.]?|-{0,2})[a-z0-9]+)*$".'; } $reservedNames = array('nul', 'con', 'prn', 'aux', 'com1', 'com2', 'com3', 'com4', 'com5', 'com6', 'com7', 'com8', 'com9', 'lpt1', 'lpt2', 'lpt3', 'lpt4', 'lpt5', 'lpt6', 'lpt7', 'lpt8', 'lpt9'); $bits = explode('/', strtolower($name)); if (in_array($bits[0], $reservedNames, true) || in_array($bits[1], $reservedNames, true)) { return $name.' is reserved, package and vendor names can not match any of: '.implode(', ', $reservedNames).'.'; } if (Preg::isMatch('{\.json$}', $name)) { return $name.' is invalid, package names can not end in .json, consider renaming it or perhaps using a -json suffix instead.'; } if (Preg::isMatch('{[A-Z]}', $name)) { if ($isLink) { return $name.' is invalid, it should not contain uppercase characters. Please use '.strtolower($name).' instead.'; } $suggestName = Preg::replace('{(?:([a-z])([A-Z])|([A-Z])([A-Z][a-z]))}', '\\1\\3-\\2\\4', $name); $suggestName = strtolower($suggestName); return $name.' is invalid, it should not contain uppercase characters. We suggest using '.$suggestName.' instead.'; } return null; } /** * @param string $property * @param string $regex * @param bool $mandatory * * @return bool * * @phpstan-param non-empty-string $property * @phpstan-param non-empty-string $regex */ private function validateRegex($property, $regex, $mandatory = false) { if (!$this->validateString($property, $mandatory)) { return false; } if (!Preg::isMatch('{^'.$regex.'$}u', $this->config[$property])) { $message = $property.' : invalid value ('.$this->config[$property].'), must match '.$regex; if ($mandatory) { $this->errors[] = $message; } else { $this->warnings[] = $message; } unset($this->config[$property]); return false; } return true; } /** * @param string $property * @param bool $mandatory * * @return bool * * @phpstan-param non-empty-string $property */ private function validateString($property, $mandatory = false) { if (isset($this->config[$property]) && !is_string($this->config[$property])) { $this->errors[] = $property.' : should be a string, '.gettype($this->config[$property]).' given'; unset($this->config[$property]); return false; } if (!isset($this->config[$property]) || trim($this->config[$property]) === '') { if ($mandatory) { $this->errors[] = $property.' : must be present'; } unset($this->config[$property]); return false; } return true; } /** * @param string $property * @param bool $mandatory * * @return bool * * @phpstan-param non-empty-string $property */ private function validateArray($property, $mandatory = false) { if (isset($this->config[$property]) && !is_array($this->config[$property])) { $this->errors[] = $property.' : should be an array, '.gettype($this->config[$property]).' given'; unset($this->config[$property]); return false; } if (!isset($this->config[$property]) || !count($this->config[$property])) { if ($mandatory) { $this->errors[] = $property.' : must be present and contain at least one element'; } unset($this->config[$property]); return false; } return true; } /** * @param string $property * @param string|null $regex * @param bool $mandatory * * @return bool * * @phpstan-param non-empty-string $property * @phpstan-param non-empty-string|null $regex */ private function validateFlatArray($property, $regex = null, $mandatory = false) { if (!$this->validateArray($property, $mandatory)) { return false; } $pass = true; foreach ($this->config[$property] as $key => $value) { if (!is_string($value) && !is_numeric($value)) { $this->errors[] = $property.'.'.$key.' : must be a string or int, '.gettype($value).' given'; unset($this->config[$property][$key]); $pass = false; continue; } if ($regex && !Preg::isMatch('{^'.$regex.'$}u', $value)) { $this->warnings[] = $property.'.'.$key.' : invalid value ('.$value.'), must match '.$regex; unset($this->config[$property][$key]); $pass = false; } } return $pass; } /** * @param string $property * @param bool $mandatory * * @return bool * * @phpstan-param non-empty-string $property */ private function validateUrl($property, $mandatory = false) { if (!$this->validateString($property, $mandatory)) { return false; } if (!$this->filterUrl($this->config[$property])) { $this->warnings[] = $property.' : invalid value ('.$this->config[$property].'), must be an http/https URL'; unset($this->config[$property]); return false; } return true; } /** * @param mixed $value * @param string[] $schemes * * @return bool */ private function filterUrl($value, array $schemes = array('http', 'https')) { if ($value === '') { return true; } $bits = parse_url($value); if (empty($bits['scheme']) || empty($bits['host'])) { return false; } if (!in_array($bits['scheme'], $schemes, true)) { return false; } return true; } } * Jordi Boggiano * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Composer\Package\Loader; use Composer\Package\BasePackage; use Composer\Package\CompleteAliasPackage; use Composer\Package\CompletePackage; use Composer\Package\RootPackage; use Composer\Package\PackageInterface; use Composer\Package\CompletePackageInterface; use Composer\Package\Link; use Composer\Package\RootAliasPackage; use Composer\Package\Version\VersionParser; use Composer\Pcre\Preg; /** * @author Konstantin Kudryashiv * @author Jordi Boggiano */ class ArrayLoader implements LoaderInterface { /** @var VersionParser */ protected $versionParser; /** @var bool */ protected $loadOptions; /** * @param bool $loadOptions */ public function __construct(VersionParser $parser = null, $loadOptions = false) { if (!$parser) { $parser = new VersionParser; } $this->versionParser = $parser; $this->loadOptions = $loadOptions; } /** * @inheritDoc */ public function load(array $config, $class = 'Composer\Package\CompletePackage') { if ($class !== 'Composer\Package\CompletePackage' && $class !== 'Composer\Package\RootPackage') { trigger_error('The $class arg is deprecated, please reach out to Composer maintainers ASAP if you still need this.', E_USER_DEPRECATED); } $package = $this->createObject($config, $class); foreach (BasePackage::$supportedLinkTypes as $type => $opts) { if (isset($config[$type])) { $method = 'set'.ucfirst($opts['method']); $package->{$method}( $this->parseLinks( $package->getName(), $package->getPrettyVersion(), $opts['method'], $config[$type] ) ); } } $package = $this->configureObject($package, $config); return $package; } /** * @param list> $versions * * @return list */ public function loadPackages(array $versions) { $packages = array(); $linkCache = array(); foreach ($versions as $version) { $package = $this->createObject($version, 'Composer\Package\CompletePackage'); $this->configureCachedLinks($linkCache, $package, $version); $package = $this->configureObject($package, $version); $packages[] = $package; } return $packages; } /** * @template PackageClass of CompletePackageInterface * * @param mixed[] $config package data * @param string $class FQCN to be instantiated * * @return CompletePackage|RootPackage * * @phpstan-param class-string $class */ private function createObject(array $config, $class) { if (!isset($config['name'])) { throw new \UnexpectedValueException('Unknown package has no name defined ('.json_encode($config).').'); } if (!isset($config['version']) || !is_scalar($config['version'])) { throw new \UnexpectedValueException('Package '.$config['name'].' has no version defined.'); } if (!is_string($config['version'])) { $config['version'] = (string) $config['version']; } // handle already normalized versions if (isset($config['version_normalized']) && is_string($config['version_normalized'])) { $version = $config['version_normalized']; // handling of existing repos which need to remain composer v1 compatible, in case the version_normalized contained VersionParser::DEFAULT_BRANCH_ALIAS, we renormalize it if ($version === VersionParser::DEFAULT_BRANCH_ALIAS) { $version = $this->versionParser->normalize($config['version']); } } else { $version = $this->versionParser->normalize($config['version']); } return new $class($config['name'], $version, $config['version']); } /** * @param CompletePackage $package * @param mixed[] $config package data * * @return RootPackage|RootAliasPackage|CompletePackage|CompleteAliasPackage */ private function configureObject(PackageInterface $package, array $config) { if (!$package instanceof CompletePackage) { throw new \LogicException('ArrayLoader expects instances of the Composer\Package\CompletePackage class to function correctly'); } $package->setType(isset($config['type']) ? strtolower($config['type']) : 'library'); if (isset($config['target-dir'])) { $package->setTargetDir($config['target-dir']); } if (isset($config['extra']) && \is_array($config['extra'])) { $package->setExtra($config['extra']); } if (isset($config['bin'])) { if (!\is_array($config['bin'])) { $config['bin'] = array($config['bin']); } foreach ($config['bin'] as $key => $bin) { $config['bin'][$key] = ltrim($bin, '/'); } $package->setBinaries($config['bin']); } if (isset($config['installation-source'])) { $package->setInstallationSource($config['installation-source']); } if (isset($config['default-branch']) && $config['default-branch'] === true) { $package->setIsDefaultBranch(true); } if (isset($config['source'])) { if (!isset($config['source']['type'], $config['source']['url'], $config['source']['reference'])) { throw new \UnexpectedValueException(sprintf( "Package %s's source key should be specified as {\"type\": ..., \"url\": ..., \"reference\": ...},\n%s given.", $config['name'], json_encode($config['source']) )); } $package->setSourceType($config['source']['type']); $package->setSourceUrl($config['source']['url']); $package->setSourceReference(isset($config['source']['reference']) ? $config['source']['reference'] : null); if (isset($config['source']['mirrors'])) { $package->setSourceMirrors($config['source']['mirrors']); } } if (isset($config['dist'])) { if (!isset($config['dist']['type'], $config['dist']['url'])) { throw new \UnexpectedValueException(sprintf( "Package %s's dist key should be specified as ". "{\"type\": ..., \"url\": ..., \"reference\": ..., \"shasum\": ...},\n%s given.", $config['name'], json_encode($config['dist']) )); } $package->setDistType($config['dist']['type']); $package->setDistUrl($config['dist']['url']); $package->setDistReference(isset($config['dist']['reference']) ? $config['dist']['reference'] : null); $package->setDistSha1Checksum(isset($config['dist']['shasum']) ? $config['dist']['shasum'] : null); if (isset($config['dist']['mirrors'])) { $package->setDistMirrors($config['dist']['mirrors']); } } if (isset($config['suggest']) && \is_array($config['suggest'])) { foreach ($config['suggest'] as $target => $reason) { if ('self.version' === trim($reason)) { $config['suggest'][$target] = $package->getPrettyVersion(); } } $package->setSuggests($config['suggest']); } if (isset($config['autoload'])) { $package->setAutoload($config['autoload']); } if (isset($config['autoload-dev'])) { $package->setDevAutoload($config['autoload-dev']); } if (isset($config['include-path'])) { $package->setIncludePaths($config['include-path']); } if (!empty($config['time'])) { $time = Preg::isMatch('/^\d++$/D', $config['time']) ? '@'.$config['time'] : $config['time']; try { $date = new \DateTime($time, new \DateTimeZone('UTC')); $package->setReleaseDate($date); } catch (\Exception $e) { } } if (!empty($config['notification-url'])) { $package->setNotificationUrl($config['notification-url']); } if ($package instanceof CompletePackageInterface) { if (!empty($config['archive']['name'])) { $package->setArchiveName($config['archive']['name']); } if (!empty($config['archive']['exclude'])) { $package->setArchiveExcludes($config['archive']['exclude']); } if (isset($config['scripts']) && \is_array($config['scripts'])) { foreach ($config['scripts'] as $event => $listeners) { $config['scripts'][$event] = (array) $listeners; } foreach (array('composer', 'php', 'putenv') as $reserved) { if (isset($config['scripts'][$reserved])) { trigger_error('The `'.$reserved.'` script name is reserved for internal use, please avoid defining it', E_USER_DEPRECATED); } } $package->setScripts($config['scripts']); } if (!empty($config['description']) && \is_string($config['description'])) { $package->setDescription($config['description']); } if (!empty($config['homepage']) && \is_string($config['homepage'])) { $package->setHomepage($config['homepage']); } if (!empty($config['keywords']) && \is_array($config['keywords'])) { $package->setKeywords($config['keywords']); } if (!empty($config['license'])) { $package->setLicense(\is_array($config['license']) ? $config['license'] : array($config['license'])); } if (!empty($config['authors']) && \is_array($config['authors'])) { $package->setAuthors($config['authors']); } if (isset($config['support'])) { $package->setSupport($config['support']); } if (!empty($config['funding']) && \is_array($config['funding'])) { $package->setFunding($config['funding']); } if (isset($config['abandoned'])) { $package->setAbandoned($config['abandoned']); } } if ($this->loadOptions && isset($config['transport-options'])) { $package->setTransportOptions($config['transport-options']); } if ($aliasNormalized = $this->getBranchAlias($config)) { $prettyAlias = Preg::replace('{(\.9{7})+}', '.x', $aliasNormalized); if ($package instanceof RootPackage) { return new RootAliasPackage($package, $aliasNormalized, $prettyAlias); } return new CompleteAliasPackage($package, $aliasNormalized, $prettyAlias); } return $package; } /** * @param array>>> $linkCache * @param PackageInterface $package * @param mixed[] $config * * @return void */ private function configureCachedLinks(&$linkCache, $package, array $config) { $name = $package->getName(); $prettyVersion = $package->getPrettyVersion(); foreach (BasePackage::$supportedLinkTypes as $type => $opts) { if (isset($config[$type])) { $method = 'set'.ucfirst($opts['method']); $links = array(); foreach ($config[$type] as $prettyTarget => $constraint) { $target = strtolower($prettyTarget); // recursive links are not supported if ($target === $name) { continue; } if ($constraint === 'self.version') { $links[$target] = $this->createLink($name, $prettyVersion, $opts['method'], $target, $constraint); } else { if (!isset($linkCache[$name][$type][$target][$constraint])) { $linkCache[$name][$type][$target][$constraint] = array($target, $this->createLink($name, $prettyVersion, $opts['method'], $target, $constraint)); } list($target, $link) = $linkCache[$name][$type][$target][$constraint]; $links[$target] = $link; } } $package->{$method}($links); } } } /** * @param string $source source package name * @param string $sourceVersion source package version (pretty version ideally) * @param string $description link description (e.g. requires, replaces, ..) * @param array $links array of package name => constraint mappings * * @return Link[] * * @phpstan-param Link::TYPE_* $description */ public function parseLinks($source, $sourceVersion, $description, $links) { $res = array(); foreach ($links as $target => $constraint) { $target = strtolower($target); $res[$target] = $this->createLink($source, $sourceVersion, $description, $target, $constraint); } return $res; } /** * @param string $source source package name * @param string $sourceVersion source package version (pretty version ideally) * @param Link::TYPE_* $description link description (e.g. requires, replaces, ..) * @param string $target target package name * @param string $prettyConstraint constraint string * @return Link */ private function createLink($source, $sourceVersion, $description, $target, $prettyConstraint) { if (!\is_string($prettyConstraint)) { throw new \UnexpectedValueException('Link constraint in '.$source.' '.$description.' > '.$target.' should be a string, got '.\gettype($prettyConstraint) . ' (' . var_export($prettyConstraint, true) . ')'); } if ('self.version' === $prettyConstraint) { $parsedConstraint = $this->versionParser->parseConstraints($sourceVersion); } else { $parsedConstraint = $this->versionParser->parseConstraints($prettyConstraint); } return new Link($source, $target, $parsedConstraint, $description, $prettyConstraint); } /** * Retrieves a branch alias (dev-master => 1.0.x-dev for example) if it exists * * @param mixed[] $config the entire package config * * @return string|null normalized version of the branch alias or null if there is none */ public function getBranchAlias(array $config) { if (strpos($config['version'], 'dev-') !== 0 && '-dev' !== substr($config['version'], -4)) { return null; } if (isset($config['extra']['branch-alias']) && \is_array($config['extra']['branch-alias'])) { foreach ($config['extra']['branch-alias'] as $sourceBranch => $targetBranch) { // ensure it is an alias to a -dev package if ('-dev' !== substr($targetBranch, -4)) { continue; } // normalize without -dev and ensure it's a numeric branch that is parseable if ($targetBranch === VersionParser::DEFAULT_BRANCH_ALIAS) { $validatedTargetBranch = VersionParser::DEFAULT_BRANCH_ALIAS; } else { $validatedTargetBranch = $this->versionParser->normalizeBranch(substr($targetBranch, 0, -4)); } if ('-dev' !== substr($validatedTargetBranch, -4)) { continue; } // ensure that it is the current branch aliasing itself if (strtolower($config['version']) !== strtolower($sourceBranch)) { continue; } // If using numeric aliases ensure the alias is a valid subversion if (($sourcePrefix = $this->versionParser->parseNumericAliasPrefix($sourceBranch)) && ($targetPrefix = $this->versionParser->parseNumericAliasPrefix($targetBranch)) && (stripos($targetPrefix, $sourcePrefix) !== 0) ) { continue; } return $validatedTargetBranch; } } if ( isset($config['default-branch']) && $config['default-branch'] === true && false === $this->versionParser->parseNumericAliasPrefix($config['version']) ) { return VersionParser::DEFAULT_BRANCH_ALIAS; } return null; } } * Jordi Boggiano * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Composer\Package\Loader; use Composer\Package\BasePackage; use Composer\Config; use Composer\IO\IOInterface; use Composer\Package\RootAliasPackage; use Composer\Pcre\Preg; use Composer\Repository\RepositoryFactory; use Composer\Package\Version\VersionGuesser; use Composer\Package\Version\VersionParser; use Composer\Package\RootPackage; use Composer\Repository\RepositoryManager; use Composer\Util\Platform; use Composer\Util\ProcessExecutor; /** * ArrayLoader built for the sole purpose of loading the root package * * Sets additional defaults and loads repositories * * @author Jordi Boggiano */ class RootPackageLoader extends ArrayLoader { /** * @var RepositoryManager */ private $manager; /** * @var Config */ private $config; /** * @var VersionGuesser */ private $versionGuesser; public function __construct(RepositoryManager $manager, Config $config, VersionParser $parser = null, VersionGuesser $versionGuesser = null, IOInterface $io = null) { parent::__construct($parser); $this->manager = $manager; $this->config = $config; $this->versionGuesser = $versionGuesser ?: new VersionGuesser($config, new ProcessExecutor($io), $this->versionParser); } /** * @inheritDoc * * @template PackageClass of RootPackage * * @param string|null $cwd * * @return RootPackage|RootAliasPackage * * @phpstan-param class-string $class */ public function load(array $config, $class = 'Composer\Package\RootPackage', $cwd = null) { if ($class !== 'Composer\Package\RootPackage') { trigger_error('The $class arg is deprecated, please reach out to Composer maintainers ASAP if you still need this.', E_USER_DEPRECATED); } if (!isset($config['name'])) { $config['name'] = '__root__'; } elseif ($err = ValidatingArrayLoader::hasPackageNamingError($config['name'])) { throw new \RuntimeException('Your package name '.$err); } $autoVersioned = false; if (!isset($config['version'])) { $commit = null; // override with env var if available if (Platform::getEnv('COMPOSER_ROOT_VERSION')) { $config['version'] = Platform::getEnv('COMPOSER_ROOT_VERSION'); } else { $versionData = $this->versionGuesser->guessVersion($config, $cwd ?: getcwd()); if ($versionData) { $config['version'] = $versionData['pretty_version']; $config['version_normalized'] = $versionData['version']; $commit = $versionData['commit']; } } if (!isset($config['version'])) { $config['version'] = '1.0.0'; $autoVersioned = true; } if ($commit) { $config['source'] = array( 'type' => '', 'url' => '', 'reference' => $commit, ); $config['dist'] = array( 'type' => '', 'url' => '', 'reference' => $commit, ); } } /** @var RootPackage|RootAliasPackage $package */ $package = parent::load($config, $class); if ($package instanceof RootAliasPackage) { $realPackage = $package->getAliasOf(); } else { $realPackage = $package; } if (!$realPackage instanceof RootPackage) { throw new \LogicException('Expecting a Composer\Package\RootPackage at this point'); } if ($autoVersioned) { $realPackage->replaceVersion($realPackage->getVersion(), RootPackage::DEFAULT_PRETTY_VERSION); } if (isset($config['minimum-stability'])) { $realPackage->setMinimumStability(VersionParser::normalizeStability($config['minimum-stability'])); } $aliases = array(); $stabilityFlags = array(); $references = array(); foreach (array('require', 'require-dev') as $linkType) { if (isset($config[$linkType])) { $linkInfo = BasePackage::$supportedLinkTypes[$linkType]; $method = 'get'.ucfirst($linkInfo['method']); $links = array(); foreach ($realPackage->$method() as $link) { $links[$link->getTarget()] = $link->getConstraint()->getPrettyString(); } $aliases = $this->extractAliases($links, $aliases); $stabilityFlags = self::extractStabilityFlags($links, $realPackage->getMinimumStability(), $stabilityFlags); $references = self::extractReferences($links, $references); if (isset($links[$config['name']])) { throw new \RuntimeException(sprintf('Root package \'%s\' cannot require itself in its composer.json' . PHP_EOL . 'Did you accidentally name your root package after an external package?', $config['name'])); } } } foreach (array_keys(BasePackage::$supportedLinkTypes) as $linkType) { if (isset($config[$linkType])) { foreach ($config[$linkType] as $linkName => $constraint) { if ($err = ValidatingArrayLoader::hasPackageNamingError($linkName, true)) { throw new \RuntimeException($linkType.'.'.$err); } } } } $realPackage->setAliases($aliases); $realPackage->setStabilityFlags($stabilityFlags); $realPackage->setReferences($references); if (isset($config['prefer-stable'])) { $realPackage->setPreferStable((bool) $config['prefer-stable']); } if (isset($config['config'])) { $realPackage->setConfig($config['config']); } $repos = RepositoryFactory::defaultRepos(null, $this->config, $this->manager); foreach ($repos as $repo) { $this->manager->addRepository($repo); } $realPackage->setRepositories($this->config->getRepositories()); return $package; } /** * @param array $requires * @param list $aliases * * @return list */ private function extractAliases(array $requires, array $aliases) { foreach ($requires as $reqName => $reqVersion) { if (Preg::isMatch('{^([^,\s#]+)(?:#[^ ]+)? +as +([^,\s]+)$}', $reqVersion, $match)) { $aliases[] = array( 'package' => strtolower($reqName), 'version' => $this->versionParser->normalize($match[1], $reqVersion), 'alias' => $match[2], 'alias_normalized' => $this->versionParser->normalize($match[2], $reqVersion), ); } elseif (strpos($reqVersion, ' as ') !== false) { throw new \UnexpectedValueException('Invalid alias definition in "'.$reqName.'": "'.$reqVersion.'". Aliases should be in the form "exact-version as other-exact-version".'); } } return $aliases; } /** * @internal * * @param array $requires * @param string $minimumStability * @param array $stabilityFlags * * @return array * * @phpstan-param array $stabilityFlags * @phpstan-return array */ public static function extractStabilityFlags(array $requires, $minimumStability, array $stabilityFlags) { $stabilities = BasePackage::$stabilities; /** @var int $minimumStability */ $minimumStability = $stabilities[$minimumStability]; foreach ($requires as $reqName => $reqVersion) { $constraints = array(); // extract all sub-constraints in case it is an OR/AND multi-constraint $orSplit = Preg::split('{\s*\|\|?\s*}', trim($reqVersion)); foreach ($orSplit as $orConstraint) { $andSplit = Preg::split('{(?< ,]) *(? $stability) { continue; } $stabilityFlags[$name] = $stability; $match = true; } } if ($match) { continue; } foreach ($constraints as $constraint) { // infer flags for requirements that have an explicit -dev or -beta version specified but only // for those that are more unstable than the minimumStability or existing flags $reqVersion = Preg::replace('{^([^,\s@]+) as .+$}', '$1', $constraint); if (Preg::isMatch('{^[^,\s@]+$}', $reqVersion) && 'stable' !== ($stabilityName = VersionParser::parseStability($reqVersion))) { $name = strtolower($reqName); $stability = $stabilities[$stabilityName]; if ((isset($stabilityFlags[$name]) && $stabilityFlags[$name] > $stability) || ($minimumStability > $stability)) { continue; } $stabilityFlags[$name] = $stability; } } } return $stabilityFlags; } /** * @internal * * @param array $requires * @param array $references * * @return array */ public static function extractReferences(array $requires, array $references) { foreach ($requires as $reqName => $reqVersion) { $reqVersion = Preg::replace('{^([^,\s@]+) as .+$}', '$1', $reqVersion); if (Preg::isMatch('{^[^,\s@]+?#([a-f0-9]+)$}', $reqVersion, $match) && 'dev' === VersionParser::parseStability($reqVersion)) { $name = strtolower($reqName); $references[$name] = $match[1]; } } return $references; } } * Jordi Boggiano * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Composer\Package; use Composer\Semver\Constraint\Constraint; use Composer\Package\Version\VersionParser; /** * @author Jordi Boggiano */ class AliasPackage extends BasePackage { /** @var string */ protected $version; /** @var string */ protected $prettyVersion; /** @var bool */ protected $dev; /** @var bool */ protected $rootPackageAlias = false; /** * @var string * @phpstan-var 'stable'|'RC'|'beta'|'alpha'|'dev' */ protected $stability; /** @var bool */ protected $hasSelfVersionRequires = false; /** @var BasePackage */ protected $aliasOf; /** @var Link[] */ protected $requires; /** @var Link[] */ protected $devRequires; /** @var Link[] */ protected $conflicts; /** @var Link[] */ protected $provides; /** @var Link[] */ protected $replaces; /** * All descendants' constructors should call this parent constructor * * @param BasePackage $aliasOf The package this package is an alias of * @param string $version The version the alias must report * @param string $prettyVersion The alias's non-normalized version */ public function __construct(BasePackage $aliasOf, $version, $prettyVersion) { parent::__construct($aliasOf->getName()); $this->version = $version; $this->prettyVersion = $prettyVersion; $this->aliasOf = $aliasOf; $this->stability = VersionParser::parseStability($version); $this->dev = $this->stability === 'dev'; foreach (Link::$TYPES as $type) { $links = $aliasOf->{'get' . ucfirst($type)}(); $this->$type = $this->replaceSelfVersionDependencies($links, $type); } } /** * @return BasePackage */ public function getAliasOf() { return $this->aliasOf; } /** * @inheritDoc */ public function getVersion() { return $this->version; } /** * @inheritDoc */ public function getStability() { return $this->stability; } /** * @inheritDoc */ public function getPrettyVersion() { return $this->prettyVersion; } /** * @inheritDoc */ public function isDev() { return $this->dev; } /** * @inheritDoc */ public function getRequires() { return $this->requires; } /** * @inheritDoc * @return array */ public function getConflicts() { return $this->conflicts; } /** * @inheritDoc * @return array */ public function getProvides() { return $this->provides; } /** * @inheritDoc * @return array */ public function getReplaces() { return $this->replaces; } /** * @inheritDoc */ public function getDevRequires() { return $this->devRequires; } /** * Stores whether this is an alias created by an aliasing in the requirements of the root package or not * * Use by the policy for sorting manually aliased packages first, see #576 * * @param bool $value * * @return mixed */ public function setRootPackageAlias($value) { return $this->rootPackageAlias = $value; } /** * @see setRootPackageAlias * @return bool */ public function isRootPackageAlias() { return $this->rootPackageAlias; } /** * @param Link[] $links * @param Link::TYPE_* $linkType * * @return Link[] */ protected function replaceSelfVersionDependencies(array $links, $linkType) { // for self.version requirements, we use the original package's branch name instead, to avoid leaking the magic dev-master-alias to users $prettyVersion = $this->prettyVersion; if ($prettyVersion === VersionParser::DEFAULT_BRANCH_ALIAS) { $prettyVersion = $this->aliasOf->getPrettyVersion(); } if (\in_array($linkType, array(Link::TYPE_CONFLICT, Link::TYPE_PROVIDE, Link::TYPE_REPLACE), true)) { $newLinks = array(); foreach ($links as $link) { // link is self.version, but must be replacing also the replaced version if ('self.version' === $link->getPrettyConstraint()) { $newLinks[] = new Link($link->getSource(), $link->getTarget(), $constraint = new Constraint('=', $this->version), $linkType, $prettyVersion); $constraint->setPrettyString($prettyVersion); } } $links = array_merge($links, $newLinks); } else { foreach ($links as $index => $link) { if ('self.version' === $link->getPrettyConstraint()) { if ($linkType === Link::TYPE_REQUIRE) { $this->hasSelfVersionRequires = true; } $links[$index] = new Link($link->getSource(), $link->getTarget(), $constraint = new Constraint('=', $this->version), $linkType, $prettyVersion); $constraint->setPrettyString($prettyVersion); } } } return $links; } /** * @return bool */ public function hasSelfVersionRequires() { return $this->hasSelfVersionRequires; } public function __toString() { return parent::__toString().' ('.($this->rootPackageAlias ? 'root ' : ''). 'alias of '.$this->aliasOf->getVersion().')'; } /*************************************** * Wrappers around the aliased package * ***************************************/ public function getType() { return $this->aliasOf->getType(); } public function getTargetDir() { return $this->aliasOf->getTargetDir(); } public function getExtra() { return $this->aliasOf->getExtra(); } public function setInstallationSource($type) { $this->aliasOf->setInstallationSource($type); } public function getInstallationSource() { return $this->aliasOf->getInstallationSource(); } public function getSourceType() { return $this->aliasOf->getSourceType(); } public function getSourceUrl() { return $this->aliasOf->getSourceUrl(); } public function getSourceUrls() { return $this->aliasOf->getSourceUrls(); } public function getSourceReference() { return $this->aliasOf->getSourceReference(); } public function setSourceReference($reference) { $this->aliasOf->setSourceReference($reference); } public function setSourceMirrors($mirrors) { $this->aliasOf->setSourceMirrors($mirrors); } public function getSourceMirrors() { return $this->aliasOf->getSourceMirrors(); } public function getDistType() { return $this->aliasOf->getDistType(); } public function getDistUrl() { return $this->aliasOf->getDistUrl(); } public function getDistUrls() { return $this->aliasOf->getDistUrls(); } public function getDistReference() { return $this->aliasOf->getDistReference(); } public function setDistReference($reference) { $this->aliasOf->setDistReference($reference); } public function getDistSha1Checksum() { return $this->aliasOf->getDistSha1Checksum(); } public function setTransportOptions(array $options) { $this->aliasOf->setTransportOptions($options); } public function getTransportOptions() { return $this->aliasOf->getTransportOptions(); } public function setDistMirrors($mirrors) { $this->aliasOf->setDistMirrors($mirrors); } public function getDistMirrors() { return $this->aliasOf->getDistMirrors(); } public function getAutoload() { return $this->aliasOf->getAutoload(); } public function getDevAutoload() { return $this->aliasOf->getDevAutoload(); } public function getIncludePaths() { return $this->aliasOf->getIncludePaths(); } public function getReleaseDate() { return $this->aliasOf->getReleaseDate(); } public function getBinaries() { return $this->aliasOf->getBinaries(); } public function getSuggests() { return $this->aliasOf->getSuggests(); } public function getNotificationUrl() { return $this->aliasOf->getNotificationUrl(); } public function isDefaultBranch() { return $this->aliasOf->isDefaultBranch(); } public function setDistUrl($url) { $this->aliasOf->setDistUrl($url); } public function setDistType($type) { $this->aliasOf->setDistType($type); } public function setSourceDistReferences($reference) { $this->aliasOf->setSourceDistReferences($reference); } } * Jordi Boggiano * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Composer\Package; use Composer\Json\JsonFile; use Composer\Installer\InstallationManager; use Composer\Pcre\Preg; use Composer\Repository\LockArrayRepository; use Composer\Util\ProcessExecutor; use Composer\Package\Dumper\ArrayDumper; use Composer\Package\Loader\ArrayLoader; use Composer\Package\Version\VersionParser; use Composer\Plugin\PluginInterface; use Composer\Util\Git as GitUtil; use Composer\IO\IOInterface; use Seld\JsonLint\ParsingException; /** * Reads/writes project lockfile (composer.lock). * * @author Konstantin Kudryashiv * @author Jordi Boggiano */ class Locker { /** @var JsonFile */ private $lockFile; /** @var InstallationManager */ private $installationManager; /** @var string */ private $hash; /** @var string */ private $contentHash; /** @var ArrayLoader */ private $loader; /** @var ArrayDumper */ private $dumper; /** @var ProcessExecutor */ private $process; /** @var mixed[]|null */ private $lockDataCache = null; /** @var bool */ private $virtualFileWritten = false; /** * Initializes packages locker. * * @param IOInterface $io * @param JsonFile $lockFile lockfile loader * @param InstallationManager $installationManager installation manager instance * @param string $composerFileContents The contents of the composer file */ public function __construct(IOInterface $io, JsonFile $lockFile, InstallationManager $installationManager, $composerFileContents, ProcessExecutor $process = null) { $this->lockFile = $lockFile; $this->installationManager = $installationManager; $this->hash = md5($composerFileContents); $this->contentHash = self::getContentHash($composerFileContents); $this->loader = new ArrayLoader(null, true); $this->dumper = new ArrayDumper(); $this->process = $process ?: new ProcessExecutor($io); } /** * Returns the md5 hash of the sorted content of the composer file. * * @param string $composerFileContents The contents of the composer file. * * @return string */ public static function getContentHash($composerFileContents) { $content = json_decode($composerFileContents, true); $relevantKeys = array( 'name', 'version', 'require', 'require-dev', 'conflict', 'replace', 'provide', 'minimum-stability', 'prefer-stable', 'repositories', 'extra', ); $relevantContent = array(); foreach (array_intersect($relevantKeys, array_keys($content)) as $key) { $relevantContent[$key] = $content[$key]; } if (isset($content['config']['platform'])) { $relevantContent['config']['platform'] = $content['config']['platform']; } ksort($relevantContent); return md5(json_encode($relevantContent)); } /** * Checks whether locker has been locked (lockfile found). * * @return bool */ public function isLocked() { if (!$this->virtualFileWritten && !$this->lockFile->exists()) { return false; } $data = $this->getLockData(); return isset($data['packages']); } /** * Checks whether the lock file is still up to date with the current hash * * @return bool */ public function isFresh() { $lock = $this->lockFile->read(); if (!empty($lock['content-hash'])) { // There is a content hash key, use that instead of the file hash return $this->contentHash === $lock['content-hash']; } // BC support for old lock files without content-hash if (!empty($lock['hash'])) { return $this->hash === $lock['hash']; } // should not be reached unless the lock file is corrupted, so assume it's out of date return false; } /** * Searches and returns an array of locked packages, retrieved from registered repositories. * * @param bool $withDevReqs true to retrieve the locked dev packages * @throws \RuntimeException * @return \Composer\Repository\LockArrayRepository */ public function getLockedRepository($withDevReqs = false) { $lockData = $this->getLockData(); $packages = new LockArrayRepository(); $lockedPackages = $lockData['packages']; if ($withDevReqs) { if (isset($lockData['packages-dev'])) { $lockedPackages = array_merge($lockedPackages, $lockData['packages-dev']); } else { throw new \RuntimeException('The lock file does not contain require-dev information, run install with the --no-dev option or delete it and run composer update to generate a new lock file.'); } } if (empty($lockedPackages)) { return $packages; } if (isset($lockedPackages[0]['name'])) { $packageByName = array(); foreach ($lockedPackages as $info) { $package = $this->loader->load($info); $packages->addPackage($package); $packageByName[$package->getName()] = $package; if ($package instanceof AliasPackage) { $packageByName[$package->getAliasOf()->getName()] = $package->getAliasOf(); } } if (isset($lockData['aliases'])) { foreach ($lockData['aliases'] as $alias) { if (isset($packageByName[$alias['package']])) { $aliasPkg = new CompleteAliasPackage($packageByName[$alias['package']], $alias['alias_normalized'], $alias['alias']); $aliasPkg->setRootPackageAlias(true); $packages->addPackage($aliasPkg); } } } return $packages; } throw new \RuntimeException('Your composer.lock is invalid. Run "composer update" to generate a new one.'); } /** * @return string[] Names of dependencies installed through require-dev */ public function getDevPackageNames() { $names = array(); $lockData = $this->getLockData(); if (isset($lockData['packages-dev'])) { foreach ($lockData['packages-dev'] as $package) { $names[] = strtolower($package['name']); } } return $names; } /** * Returns the platform requirements stored in the lock file * * @param bool $withDevReqs if true, the platform requirements from the require-dev block are also returned * @return \Composer\Package\Link[] */ public function getPlatformRequirements($withDevReqs = false) { $lockData = $this->getLockData(); $requirements = array(); if (!empty($lockData['platform'])) { $requirements = $this->loader->parseLinks( '__root__', '1.0.0', Link::TYPE_REQUIRE, isset($lockData['platform']) ? $lockData['platform'] : array() ); } if ($withDevReqs && !empty($lockData['platform-dev'])) { $devRequirements = $this->loader->parseLinks( '__root__', '1.0.0', Link::TYPE_REQUIRE, isset($lockData['platform-dev']) ? $lockData['platform-dev'] : array() ); $requirements = array_merge($requirements, $devRequirements); } return $requirements; } /** * @return string */ public function getMinimumStability() { $lockData = $this->getLockData(); return isset($lockData['minimum-stability']) ? $lockData['minimum-stability'] : 'stable'; } /** * @return array */ public function getStabilityFlags() { $lockData = $this->getLockData(); return isset($lockData['stability-flags']) ? $lockData['stability-flags'] : array(); } /** * @return bool|null */ public function getPreferStable() { $lockData = $this->getLockData(); // return null if not set to allow caller logic to choose the // right behavior since old lock files have no prefer-stable return isset($lockData['prefer-stable']) ? $lockData['prefer-stable'] : null; } /** * @return bool|null */ public function getPreferLowest() { $lockData = $this->getLockData(); // return null if not set to allow caller logic to choose the // right behavior since old lock files have no prefer-lowest return isset($lockData['prefer-lowest']) ? $lockData['prefer-lowest'] : null; } /** * @return array */ public function getPlatformOverrides() { $lockData = $this->getLockData(); return isset($lockData['platform-overrides']) ? $lockData['platform-overrides'] : array(); } /** * @return string[][] * * @phpstan-return list */ public function getAliases() { $lockData = $this->getLockData(); return isset($lockData['aliases']) ? $lockData['aliases'] : array(); } /** * @return array */ public function getLockData() { if (null !== $this->lockDataCache) { return $this->lockDataCache; } if (!$this->lockFile->exists()) { throw new \LogicException('No lockfile found. Unable to read locked packages'); } return $this->lockDataCache = $this->lockFile->read(); } /** * Locks provided data into lockfile. * * @param PackageInterface[] $packages array of packages * @param PackageInterface[]|null $devPackages array of dev packages or null if installed without --dev * @param array $platformReqs array of package name => constraint for required platform packages * @param array $platformDevReqs array of package name => constraint for dev-required platform packages * @param string[][] $aliases array of aliases * @param string $minimumStability * @param array $stabilityFlags * @param bool $preferStable * @param bool $preferLowest * @param array $platformOverrides * @param bool $write Whether to actually write data to disk, useful in tests and for --dry-run * * @return bool * * @phpstan-param list $aliases */ public function setLockData(array $packages, $devPackages, array $platformReqs, $platformDevReqs, array $aliases, $minimumStability, array $stabilityFlags, $preferStable, $preferLowest, array $platformOverrides, $write = true) { // keep old default branch names normalized to DEFAULT_BRANCH_ALIAS for BC as that is how Composer 1 outputs the lock file // when loading the lock file the version is anyway ignored in Composer 2, so it has no adverse effect $aliases = array_map(function ($alias) { if (in_array($alias['version'], array('dev-master', 'dev-trunk', 'dev-default'), true)) { $alias['version'] = VersionParser::DEFAULT_BRANCH_ALIAS; } return $alias; }, $aliases); $lock = array( '_readme' => array('This file locks the dependencies of your project to a known state', 'Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies', 'This file is @gener'.'ated automatically', ), 'content-hash' => $this->contentHash, 'packages' => null, 'packages-dev' => null, 'aliases' => $aliases, 'minimum-stability' => $minimumStability, 'stability-flags' => $stabilityFlags, 'prefer-stable' => $preferStable, 'prefer-lowest' => $preferLowest, ); $lock['packages'] = $this->lockPackages($packages); if (null !== $devPackages) { $lock['packages-dev'] = $this->lockPackages($devPackages); } $lock['platform'] = $platformReqs; $lock['platform-dev'] = $platformDevReqs; if ($platformOverrides) { $lock['platform-overrides'] = $platformOverrides; } $lock['plugin-api-version'] = PluginInterface::PLUGIN_API_VERSION; try { $isLocked = $this->isLocked(); } catch (ParsingException $e) { $isLocked = false; } if (!$isLocked || $lock !== $this->getLockData()) { if ($write) { $this->lockFile->write($lock); $this->lockDataCache = null; $this->virtualFileWritten = false; } else { $this->virtualFileWritten = true; $this->lockDataCache = JsonFile::parseJson(JsonFile::encode($lock, 448 & JsonFile::JSON_PRETTY_PRINT)); } return true; } return false; } /** * @param PackageInterface[] $packages * * @return mixed[][] * * @phpstan-return list> */ private function lockPackages(array $packages) { $locked = array(); foreach ($packages as $package) { if ($package instanceof AliasPackage) { continue; } $name = $package->getPrettyName(); $version = $package->getPrettyVersion(); if (!$name || !$version) { throw new \LogicException(sprintf( 'Package "%s" has no version or name and can not be locked', $package )); } $spec = $this->dumper->dump($package); unset($spec['version_normalized']); // always move time to the end of the package definition $time = isset($spec['time']) ? $spec['time'] : null; unset($spec['time']); if ($package->isDev() && $package->getInstallationSource() === 'source') { // use the exact commit time of the current reference if it's a dev package $time = $this->getPackageTime($package) ?: $time; } if (null !== $time) { $spec['time'] = $time; } unset($spec['installation-source']); $locked[] = $spec; } usort($locked, function ($a, $b) { $comparison = strcmp($a['name'], $b['name']); if (0 !== $comparison) { return $comparison; } // If it is the same package, compare the versions to make the order deterministic return strcmp($a['version'], $b['version']); }); return $locked; } /** * Returns the packages's datetime for its source reference. * * @param PackageInterface $package The package to scan. * @return string|null The formatted datetime or null if none was found. */ private function getPackageTime(PackageInterface $package) { if (!function_exists('proc_open')) { return null; } $path = realpath($this->installationManager->getInstallPath($package)); $sourceType = $package->getSourceType(); $datetime = null; if ($path && in_array($sourceType, array('git', 'hg'))) { $sourceRef = $package->getSourceReference() ?: $package->getDistReference(); switch ($sourceType) { case 'git': GitUtil::cleanEnv(); if (0 === $this->process->execute('git log -n1 --pretty=%ct '.ProcessExecutor::escape($sourceRef).GitUtil::getNoShowSignatureFlag($this->process), $output, $path) && Preg::isMatch('{^\s*\d+\s*$}', $output)) { $datetime = new \DateTime('@'.trim($output), new \DateTimeZone('UTC')); } break; case 'hg': if (0 === $this->process->execute('hg log --template "{date|hgdate}" -r '.ProcessExecutor::escape($sourceRef), $output, $path) && Preg::isMatch('{^\s*(\d+)\s*}', $output, $match)) { $datetime = new \DateTime('@'.$match[1], new \DateTimeZone('UTC')); } break; } } return $datetime ? $datetime->format(DATE_RFC3339) : null; } } * Jordi Boggiano * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Composer\Package; use Composer\Package\Version\VersionParser; use Composer\Pcre\Preg; use Composer\Util\ComposerMirror; /** * Core package definitions that are needed to resolve dependencies and install packages * * @author Nils Adermann */ class Package extends BasePackage { /** @var string */ protected $type; /** @var ?string */ protected $targetDir; /** @var 'source'|'dist'|null */ protected $installationSource; /** @var ?string */ protected $sourceType; /** @var ?string */ protected $sourceUrl; /** @var ?string */ protected $sourceReference; /** @var ?array */ protected $sourceMirrors; /** @var ?string */ protected $distType; /** @var ?string */ protected $distUrl; /** @var ?string */ protected $distReference; /** @var ?string */ protected $distSha1Checksum; /** @var ?array */ protected $distMirrors; /** @var string */ protected $version; /** @var string */ protected $prettyVersion; /** @var ?\DateTime */ protected $releaseDate; /** @var mixed[] */ protected $extra = array(); /** @var string[] */ protected $binaries = array(); /** @var bool */ protected $dev; /** * @var string * @phpstan-var 'stable'|'RC'|'beta'|'alpha'|'dev' */ protected $stability; /** @var ?string */ protected $notificationUrl; /** @var array */ protected $requires = array(); /** @var array */ protected $conflicts = array(); /** @var array */ protected $provides = array(); /** @var array */ protected $replaces = array(); /** @var array */ protected $devRequires = array(); /** @var array */ protected $suggests = array(); /** @var array{psr-0?: array, psr-4?: array, classmap?: list, files?: list} */ protected $autoload = array(); /** @var array{psr-0?: array, psr-4?: array, classmap?: list, files?: list} */ protected $devAutoload = array(); /** @var string[] */ protected $includePaths = array(); /** @var bool */ protected $isDefaultBranch = false; /** @var mixed[] */ protected $transportOptions = array(); /** * Creates a new in memory package. * * @param string $name The package's name * @param string $version The package's version * @param string $prettyVersion The package's non-normalized version */ public function __construct($name, $version, $prettyVersion) { parent::__construct($name); $this->version = $version; $this->prettyVersion = $prettyVersion; $this->stability = VersionParser::parseStability($version); $this->dev = $this->stability === 'dev'; } /** * @inheritDoc */ public function isDev() { return $this->dev; } /** * @param string $type * * @return void */ public function setType($type) { $this->type = $type; } /** * @inheritDoc */ public function getType() { return $this->type ?: 'library'; } /** * @inheritDoc */ public function getStability() { return $this->stability; } /** * @param string $targetDir * * @return void */ public function setTargetDir($targetDir) { $this->targetDir = $targetDir; } /** * @inheritDoc */ public function getTargetDir() { if (null === $this->targetDir) { return null; } return ltrim(Preg::replace('{ (?:^|[\\\\/]+) \.\.? (?:[\\\\/]+|$) (?:\.\.? (?:[\\\\/]+|$) )*}x', '/', $this->targetDir), '/'); } /** * @param mixed[] $extra * * @return void */ public function setExtra(array $extra) { $this->extra = $extra; } /** * @inheritDoc */ public function getExtra() { return $this->extra; } /** * @param string[] $binaries * * @return void */ public function setBinaries(array $binaries) { $this->binaries = $binaries; } /** * @inheritDoc */ public function getBinaries() { return $this->binaries; } /** * @inheritDoc * * @return void */ public function setInstallationSource($type) { $this->installationSource = $type; } /** * @inheritDoc */ public function getInstallationSource() { return $this->installationSource; } /** * @param string $type * * @return void */ public function setSourceType($type) { $this->sourceType = $type; } /** * @inheritDoc */ public function getSourceType() { return $this->sourceType; } /** * @param string $url * * @return void */ public function setSourceUrl($url) { $this->sourceUrl = $url; } /** * @inheritDoc */ public function getSourceUrl() { return $this->sourceUrl; } /** * @param string $reference * * @return void */ public function setSourceReference($reference) { $this->sourceReference = $reference; } /** * @inheritDoc */ public function getSourceReference() { return $this->sourceReference; } /** * @inheritDoc * * @return void */ public function setSourceMirrors($mirrors) { $this->sourceMirrors = $mirrors; } /** * @inheritDoc */ public function getSourceMirrors() { return $this->sourceMirrors; } /** * @inheritDoc */ public function getSourceUrls() { return $this->getUrls($this->sourceUrl, $this->sourceMirrors, $this->sourceReference, $this->sourceType, 'source'); } /** * @param string $type * * @return void */ public function setDistType($type) { $this->distType = $type; } /** * @inheritDoc */ public function getDistType() { return $this->distType; } /** * @param string $url * * @return void */ public function setDistUrl($url) { $this->distUrl = $url; } /** * @inheritDoc */ public function getDistUrl() { return $this->distUrl; } /** * @param string $reference * * @return void */ public function setDistReference($reference) { $this->distReference = $reference; } /** * @inheritDoc */ public function getDistReference() { return $this->distReference; } /** * @param string $sha1checksum * * @return void */ public function setDistSha1Checksum($sha1checksum) { $this->distSha1Checksum = $sha1checksum; } /** * @inheritDoc */ public function getDistSha1Checksum() { return $this->distSha1Checksum; } /** * @inheritDoc * * @return void */ public function setDistMirrors($mirrors) { $this->distMirrors = $mirrors; } /** * @inheritDoc */ public function getDistMirrors() { return $this->distMirrors; } /** * @inheritDoc */ public function getDistUrls() { return $this->getUrls($this->distUrl, $this->distMirrors, $this->distReference, $this->distType, 'dist'); } /** * @inheritDoc */ public function getTransportOptions() { return $this->transportOptions; } /** * @inheritDoc */ public function setTransportOptions(array $options) { $this->transportOptions = $options; } /** * @inheritDoc */ public function getVersion() { return $this->version; } /** * @inheritDoc */ public function getPrettyVersion() { return $this->prettyVersion; } /** * Set the releaseDate * * @param \DateTime $releaseDate * * @return void */ public function setReleaseDate(\DateTime $releaseDate) { $this->releaseDate = $releaseDate; } /** * @inheritDoc */ public function getReleaseDate() { return $this->releaseDate; } /** * Set the required packages * * @param array $requires A set of package links * * @return void */ public function setRequires(array $requires) { if (isset($requires[0])) { // @phpstan-ignore-line $requires = $this->convertLinksToMap($requires, 'setRequires'); } $this->requires = $requires; } /** * @inheritDoc */ public function getRequires() { return $this->requires; } /** * Set the conflicting packages * * @param array $conflicts A set of package links * * @return void */ public function setConflicts(array $conflicts) { if (isset($conflicts[0])) { // @phpstan-ignore-line $conflicts = $this->convertLinksToMap($conflicts, 'setConflicts'); } $this->conflicts = $conflicts; } /** * @inheritDoc * @return array */ public function getConflicts() { return $this->conflicts; } /** * Set the provided virtual packages * * @param array $provides A set of package links * * @return void */ public function setProvides(array $provides) { if (isset($provides[0])) { // @phpstan-ignore-line $provides = $this->convertLinksToMap($provides, 'setProvides'); } $this->provides = $provides; } /** * @inheritDoc * @return array */ public function getProvides() { return $this->provides; } /** * Set the packages this one replaces * * @param array $replaces A set of package links * * @return void */ public function setReplaces(array $replaces) { if (isset($replaces[0])) { // @phpstan-ignore-line $replaces = $this->convertLinksToMap($replaces, 'setReplaces'); } $this->replaces = $replaces; } /** * @inheritDoc * @return array */ public function getReplaces() { return $this->replaces; } /** * Set the recommended packages * * @param array $devRequires A set of package links * * @return void */ public function setDevRequires(array $devRequires) { if (isset($devRequires[0])) { // @phpstan-ignore-line $devRequires = $this->convertLinksToMap($devRequires, 'setDevRequires'); } $this->devRequires = $devRequires; } /** * @inheritDoc */ public function getDevRequires() { return $this->devRequires; } /** * Set the suggested packages * * @param array $suggests A set of package names/comments * * @return void */ public function setSuggests(array $suggests) { $this->suggests = $suggests; } /** * @inheritDoc */ public function getSuggests() { return $this->suggests; } /** * Set the autoload mapping * * @param array $autoload Mapping of autoloading rules * * @return void * * @phpstan-param array{psr-0?: array, psr-4?: array, classmap?: list, files?: list} $autoload */ public function setAutoload(array $autoload) { $this->autoload = $autoload; } /** * @inheritDoc */ public function getAutoload() { return $this->autoload; } /** * Set the dev autoload mapping * * @param array $devAutoload Mapping of dev autoloading rules * * @return void * * @phpstan-param array{psr-0?: array, psr-4?: array, classmap?: list, files?: list} $devAutoload */ public function setDevAutoload(array $devAutoload) { $this->devAutoload = $devAutoload; } /** * @inheritDoc */ public function getDevAutoload() { return $this->devAutoload; } /** * Sets the list of paths added to PHP's include path. * * @param string[] $includePaths List of directories. * * @return void */ public function setIncludePaths(array $includePaths) { $this->includePaths = $includePaths; } /** * @inheritDoc */ public function getIncludePaths() { return $this->includePaths; } /** * Sets the notification URL * * @param string $notificationUrl * * @return void */ public function setNotificationUrl($notificationUrl) { $this->notificationUrl = $notificationUrl; } /** * @inheritDoc */ public function getNotificationUrl() { return $this->notificationUrl; } /** * @param bool $defaultBranch * * @return void */ public function setIsDefaultBranch($defaultBranch) { $this->isDefaultBranch = $defaultBranch; } /** * @inheritDoc */ public function isDefaultBranch() { return $this->isDefaultBranch; } /** * @inheritDoc */ public function setSourceDistReferences($reference) { $this->setSourceReference($reference); // only bitbucket, github and gitlab have auto generated dist URLs that easily allow replacing the reference in the dist URL // TODO generalize this a bit for self-managed/on-prem versions? Some kind of replace token in dist urls which allow this? if ( $this->getDistUrl() !== null && Preg::isMatch('{^https?://(?:(?:www\.)?bitbucket\.org|(api\.)?github\.com|(?:www\.)?gitlab\.com)/}i', $this->getDistUrl()) ) { $this->setDistReference($reference); $this->setDistUrl(Preg::replace('{(?<=/|sha=)[a-f0-9]{40}(?=/|$)}i', $reference, $this->getDistUrl())); } elseif ($this->getDistReference()) { // update the dist reference if there was one, but if none was provided ignore it $this->setDistReference($reference); } } /** * Replaces current version and pretty version with passed values. * It also sets stability. * * @param string $version The package's normalized version * @param string $prettyVersion The package's non-normalized version * * @return void */ public function replaceVersion($version, $prettyVersion) { $this->version = $version; $this->prettyVersion = $prettyVersion; $this->stability = VersionParser::parseStability($version); $this->dev = $this->stability === 'dev'; } /** * @param string|null $url * @param mixed[]|null $mirrors * @param string|null $ref * @param string|null $type * @param string $urlType * * @return string[] * * @phpstan-param list|null $mirrors */ protected function getUrls($url, $mirrors, $ref, $type, $urlType) { if (!$url) { return array(); } if ($urlType === 'dist' && false !== strpos($url, '%')) { $url = ComposerMirror::processUrl($url, $this->name, $this->version, $ref, $type, $this->prettyVersion); } $urls = array($url); if ($mirrors) { foreach ($mirrors as $mirror) { if ($urlType === 'dist') { $mirrorUrl = ComposerMirror::processUrl($mirror['url'], $this->name, $this->version, $ref, $type, $this->prettyVersion); } elseif ($urlType === 'source' && $type === 'git') { $mirrorUrl = ComposerMirror::processGitUrl($mirror['url'], $this->name, $url, $type); } elseif ($urlType === 'source' && $type === 'hg') { $mirrorUrl = ComposerMirror::processHgUrl($mirror['url'], $this->name, $url, $type); } else { continue; } if (!\in_array($mirrorUrl, $urls)) { $func = $mirror['preferred'] ? 'array_unshift' : 'array_push'; $func($urls, $mirrorUrl); } } } return $urls; } /** * @param array $links * @param string $source * @return array */ private function convertLinksToMap(array $links, $source) { trigger_error('Package::'.$source.' must be called with a map of lowercased package name => Link object, got a indexed array, this is deprecated and you should fix your usage.'); $newLinks = array(); foreach ($links as $link) { $newLinks[$link->getTarget()] = $link; } return $newLinks; } } * Jordi Boggiano * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Composer\Package; /** * @author Jordi Boggiano */ class CompleteAliasPackage extends AliasPackage implements CompletePackageInterface { /** @var CompletePackage */ protected $aliasOf; /** * All descendants' constructors should call this parent constructor * * @param CompletePackage $aliasOf The package this package is an alias of * @param string $version The version the alias must report * @param string $prettyVersion The alias's non-normalized version */ public function __construct(CompletePackage $aliasOf, $version, $prettyVersion) { parent::__construct($aliasOf, $version, $prettyVersion); } /** * @return CompletePackage */ public function getAliasOf() { return $this->aliasOf; } public function getScripts() { return $this->aliasOf->getScripts(); } public function setScripts(array $scripts) { $this->aliasOf->setScripts($scripts); } public function getRepositories() { return $this->aliasOf->getRepositories(); } public function setRepositories(array $repositories) { $this->aliasOf->setRepositories($repositories); } public function getLicense() { return $this->aliasOf->getLicense(); } public function setLicense(array $license) { $this->aliasOf->setLicense($license); } public function getKeywords() { return $this->aliasOf->getKeywords(); } public function setKeywords(array $keywords) { $this->aliasOf->setKeywords($keywords); } public function getDescription() { return $this->aliasOf->getDescription(); } public function setDescription($description) { $this->aliasOf->setDescription($description); } public function getHomepage() { return $this->aliasOf->getHomepage(); } public function setHomepage($homepage) { $this->aliasOf->setHomepage($homepage); } public function getAuthors() { return $this->aliasOf->getAuthors(); } public function setAuthors(array $authors) { $this->aliasOf->setAuthors($authors); } public function getSupport() { return $this->aliasOf->getSupport(); } public function setSupport(array $support) { $this->aliasOf->setSupport($support); } public function getFunding() { return $this->aliasOf->getFunding(); } public function setFunding(array $funding) { $this->aliasOf->setFunding($funding); } public function isAbandoned() { return $this->aliasOf->isAbandoned(); } public function getReplacementPackage() { return $this->aliasOf->getReplacementPackage(); } public function setAbandoned($abandoned) { $this->aliasOf->setAbandoned($abandoned); } public function getArchiveName() { return $this->aliasOf->getArchiveName(); } public function setArchiveName($name) { $this->aliasOf->setArchiveName($name); } public function getArchiveExcludes() { return $this->aliasOf->getArchiveExcludes(); } public function setArchiveExcludes(array $excludes) { $this->aliasOf->setArchiveExcludes($excludes); } } * Jordi Boggiano * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Composer\Package\Comparer; /** * class Comparer * * @author Hector Prats */ class Comparer { /** @var string Source directory */ private $source; /** @var string Target directory */ private $update; /** @var array{changed?: string[], removed?: string[], added?: string[]} */ private $changed; /** * @param string $source * * @return void */ public function setSource($source) { $this->source = $source; } /** * @param string $update * * @return void */ public function setUpdate($update) { $this->update = $update; } /** * @param bool $toString * @param bool $explicated * * @return array{changed?: string[], removed?: string[], added?: string[]}|string|false false if no change, string only if $toString is true */ public function getChanged($toString = false, $explicated = false) { $changed = $this->changed; if (!count($changed)) { return false; } if ($explicated) { foreach ($changed as $sectionKey => $itemSection) { foreach ($itemSection as $itemKey => $item) { $changed[$sectionKey][$itemKey] = $item.' ('.$sectionKey.')'; } } } if ($toString) { $strings = array(); foreach ($changed as $sectionKey => $itemSection) { foreach ($itemSection as $itemKey => $item) { $strings[] = $item."\r\n"; } } $changed = implode("\r\n", $strings); } return $changed; } /** * @return void */ public function doCompare() { $source = array(); $destination = array(); $this->changed = array(); $currentDirectory = getcwd(); chdir($this->source); $source = $this->doTree('.', $source); if (!is_array($source)) { return; } chdir($currentDirectory); chdir($this->update); $destination = $this->doTree('.', $destination); if (!is_array($destination)) { exit; } chdir($currentDirectory); foreach ($source as $dir => $value) { foreach ($value as $file => $hash) { if (isset($destination[$dir][$file])) { if ($hash !== $destination[$dir][$file]) { $this->changed['changed'][] = $dir.'/'.$file; } } else { $this->changed['removed'][] = $dir.'/'.$file; } } } foreach ($destination as $dir => $value) { foreach ($value as $file => $hash) { if (!isset($source[$dir][$file])) { $this->changed['added'][] = $dir.'/'.$file; } } } } /** * @param string $dir * @param mixed $array * * @return array>|false */ private function doTree($dir, &$array) { if ($dh = opendir($dir)) { while ($file = readdir($dh)) { if ($file !== '.' && $file !== '..') { if (is_link($dir.'/'.$file)) { $array[$dir][$file] = readlink($dir.'/'.$file); } elseif (is_dir($dir.'/'.$file)) { if (!count($array)) { $array[0] = 'Temp'; } if (!$this->doTree($dir.'/'.$file, $array)) { return false; } } elseif (is_file($dir.'/'.$file) && filesize($dir.'/'.$file)) { $array[$dir][$file] = md5_file($dir.'/'.$file); } } } if (count($array) > 1 && isset($array['0'])) { unset($array['0']); } return $array; } return false; } } * Jordi Boggiano * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Composer\Package; /** * The root package represents the project's composer.json and contains additional metadata * * @author Jordi Boggiano */ class RootPackage extends CompletePackage implements RootPackageInterface { const DEFAULT_PRETTY_VERSION = '1.0.0+no-version-set'; /** @var string */ protected $minimumStability = 'stable'; /** @var bool */ protected $preferStable = false; /** @var array Map of package name to stability constant */ protected $stabilityFlags = array(); /** @var mixed[] */ protected $config = array(); /** @var array Map of package name to reference/commit hash */ protected $references = array(); /** @var array */ protected $aliases = array(); /** * {@inerhitDoc} */ public function setMinimumStability($minimumStability) { $this->minimumStability = $minimumStability; } /** * @inheritDoc */ public function getMinimumStability() { return $this->minimumStability; } /** * @inheritDoc */ public function setStabilityFlags(array $stabilityFlags) { $this->stabilityFlags = $stabilityFlags; } /** * @inheritDoc */ public function getStabilityFlags() { return $this->stabilityFlags; } /** * {@inerhitDoc} */ public function setPreferStable($preferStable) { $this->preferStable = $preferStable; } /** * @inheritDoc */ public function getPreferStable() { return $this->preferStable; } /** * {@inerhitDoc} */ public function setConfig(array $config) { $this->config = $config; } /** * @inheritDoc */ public function getConfig() { return $this->config; } /** * {@inerhitDoc} */ public function setReferences(array $references) { $this->references = $references; } /** * @inheritDoc */ public function getReferences() { return $this->references; } /** * {@inerhitDoc} */ public function setAliases(array $aliases) { $this->aliases = $aliases; } /** * @inheritDoc */ public function getAliases() { return $this->aliases; } } * Jordi Boggiano * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Composer\Package; /** * @author Jordi Boggiano */ class RootAliasPackage extends CompleteAliasPackage implements RootPackageInterface { /** @var RootPackage */ protected $aliasOf; /** * All descendants' constructors should call this parent constructor * * @param RootPackage $aliasOf The package this package is an alias of * @param string $version The version the alias must report * @param string $prettyVersion The alias's non-normalized version */ public function __construct(RootPackage $aliasOf, $version, $prettyVersion) { parent::__construct($aliasOf, $version, $prettyVersion); } /** * @return RootPackage */ public function getAliasOf() { return $this->aliasOf; } /** * @inheritDoc */ public function getAliases() { return $this->aliasOf->getAliases(); } /** * @inheritDoc */ public function getMinimumStability() { return $this->aliasOf->getMinimumStability(); } /** * @inheritDoc */ public function getStabilityFlags() { return $this->aliasOf->getStabilityFlags(); } /** * @inheritDoc */ public function getReferences() { return $this->aliasOf->getReferences(); } /** * @inheritDoc */ public function getPreferStable() { return $this->aliasOf->getPreferStable(); } /** * @inheritDoc */ public function getConfig() { return $this->aliasOf->getConfig(); } /** * @inheritDoc */ public function setRequires(array $require) { $this->requires = $this->replaceSelfVersionDependencies($require, Link::TYPE_REQUIRE); $this->aliasOf->setRequires($require); } /** * @inheritDoc */ public function setDevRequires(array $devRequire) { $this->devRequires = $this->replaceSelfVersionDependencies($devRequire, Link::TYPE_DEV_REQUIRE); $this->aliasOf->setDevRequires($devRequire); } /** * @inheritDoc */ public function setConflicts(array $conflicts) { $this->conflicts = $this->replaceSelfVersionDependencies($conflicts, Link::TYPE_CONFLICT); $this->aliasOf->setConflicts($conflicts); } /** * @inheritDoc */ public function setProvides(array $provides) { $this->provides = $this->replaceSelfVersionDependencies($provides, Link::TYPE_PROVIDE); $this->aliasOf->setProvides($provides); } /** * @inheritDoc */ public function setReplaces(array $replaces) { $this->replaces = $this->replaceSelfVersionDependencies($replaces, Link::TYPE_REPLACE); $this->aliasOf->setReplaces($replaces); } /** * @inheritDoc */ public function setAutoload(array $autoload) { $this->aliasOf->setAutoload($autoload); } /** * @inheritDoc */ public function setDevAutoload(array $devAutoload) { $this->aliasOf->setDevAutoload($devAutoload); } /** * @inheritDoc */ public function setStabilityFlags(array $stabilityFlags) { $this->aliasOf->setStabilityFlags($stabilityFlags); } /** * @inheritDoc */ public function setMinimumStability($minimumStability) { $this->aliasOf->setMinimumStability($minimumStability); } /** * @inheritDoc */ public function setPreferStable($preferStable) { $this->aliasOf->setPreferStable($preferStable); } /** * @inheritDoc */ public function setConfig(array $config) { $this->aliasOf->setConfig($config); } /** * @inheritDoc */ public function setReferences(array $references) { $this->aliasOf->setReferences($references); } /** * @inheritDoc */ public function setAliases(array $aliases) { $this->aliasOf->setAliases($aliases); } /** * @inheritDoc */ public function setSuggests(array $suggests) { $this->aliasOf->setSuggests($suggests); } /** * @inheritDoc */ public function setExtra(array $extra) { $this->aliasOf->setExtra($extra); } public function __clone() { parent::__clone(); $this->aliasOf = clone $this->aliasOf; } } * Jordi Boggiano * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Composer\Package; /** * Defines additional fields that are only needed for the root package * * @author Jordi Boggiano */ interface RootPackageInterface extends CompletePackageInterface { /** * Returns a set of package names and their aliases * * @return array */ public function getAliases(); /** * Returns the minimum stability of the package * * @return string */ public function getMinimumStability(); /** * Returns the stability flags to apply to dependencies * * array('foo/bar' => 'dev') * * @return array */ public function getStabilityFlags(); /** * Returns a set of package names and source references that must be enforced on them * * array('foo/bar' => 'abcd1234') * * @return array */ public function getReferences(); /** * Returns true if the root package prefers picking stable packages over unstable ones * * @return bool */ public function getPreferStable(); /** * Returns the root package's configuration * * @return mixed[] */ public function getConfig(); /** * Set the required packages * * @param Link[] $requires A set of package links * * @return void */ public function setRequires(array $requires); /** * Set the recommended packages * * @param Link[] $devRequires A set of package links * * @return void */ public function setDevRequires(array $devRequires); /** * Set the conflicting packages * * @param Link[] $conflicts A set of package links * * @return void */ public function setConflicts(array $conflicts); /** * Set the provided virtual packages * * @param Link[] $provides A set of package links * * @return void */ public function setProvides(array $provides); /** * Set the packages this one replaces * * @param Link[] $replaces A set of package links * * @return void */ public function setReplaces(array $replaces); /** * Set the repositories * * @param mixed[] $repositories * * @return void */ public function setRepositories(array $repositories); /** * Set the autoload mapping * * @param array{psr-0?: array, psr-4?: array, classmap?: list, files?: list} $autoload Mapping of autoloading rules * * @return void */ public function setAutoload(array $autoload); /** * Set the dev autoload mapping * * @param array{psr-0?: array, psr-4?: array, classmap?: list, files?: list} $devAutoload Mapping of dev autoloading rules * * @return void */ public function setDevAutoload(array $devAutoload); /** * Set the stabilityFlags * * @param array $stabilityFlags * * @return void */ public function setStabilityFlags(array $stabilityFlags); /** * Set the minimumStability * * @param string $minimumStability * * @return void */ public function setMinimumStability($minimumStability); /** * Set the preferStable * * @param bool $preferStable * * @return void */ public function setPreferStable($preferStable); /** * Set the config * * @param mixed[] $config * * @return void */ public function setConfig(array $config); /** * Set the references * * @param array $references * * @return void */ public function setReferences(array $references); /** * Set the aliases * * @param array $aliases * * @return void */ public function setAliases(array $aliases); /** * Set the suggested packages * * @param array $suggests A set of package names/comments * * @return void */ public function setSuggests(array $suggests); /** * @param mixed[] $extra * * @return void */ public function setExtra(array $extra); } * Jordi Boggiano * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Composer\Package\Archiver; /** * @author Till Klampaeckel * @author Nils Adermann * @author Matthieu Moquet */ class PharArchiver implements ArchiverInterface { /** @var array */ protected static $formats = array( 'zip' => \Phar::ZIP, 'tar' => \Phar::TAR, 'tar.gz' => \Phar::TAR, 'tar.bz2' => \Phar::TAR, ); /** @var array */ protected static $compressFormats = array( 'tar.gz' => \Phar::GZ, 'tar.bz2' => \Phar::BZ2, ); /** * @inheritDoc */ public function archive($sources, $target, $format, array $excludes = array(), $ignoreFilters = false) { $sources = realpath($sources); // Phar would otherwise load the file which we don't want if (file_exists($target)) { unlink($target); } try { $filename = substr($target, 0, strrpos($target, $format) - 1); // Check if compress format if (isset(static::$compressFormats[$format])) { // Current compress format supported base on tar $target = $filename . '.tar'; } $phar = new \PharData( $target, \FilesystemIterator::KEY_AS_PATHNAME | \FilesystemIterator::CURRENT_AS_FILEINFO, '', static::$formats[$format] ); $files = new ArchivableFilesFinder($sources, $excludes, $ignoreFilters); $filesOnly = new ArchivableFilesFilter($files); $phar->buildFromIterator($filesOnly, $sources); $filesOnly->addEmptyDir($phar, $sources); if (isset(static::$compressFormats[$format])) { // Check can be compressed? if (!$phar->canCompress(static::$compressFormats[$format])) { throw new \RuntimeException(sprintf('Can not compress to %s format', $format)); } // Delete old tar unlink($target); // Compress the new tar $phar->compress(static::$compressFormats[$format]); // Make the correct filename $target = $filename . '.' . $format; } return $target; } catch (\UnexpectedValueException $e) { $message = sprintf( "Could not create archive '%s' from '%s': %s", $target, $sources, $e->getMessage() ); throw new \RuntimeException($message, $e->getCode(), $e); } } /** * @inheritDoc */ public function supports($format, $sourceType) { return isset(static::$formats[$format]); } } * Jordi Boggiano * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Composer\Package\Archiver; use Composer\Pcre\Preg; use Composer\Util\Filesystem; use FilesystemIterator; use Symfony\Component\Finder\Finder; use Symfony\Component\Finder\SplFileInfo; /** * A Symfony Finder wrapper which locates files that should go into archives * * Handles .gitignore, .gitattributes and .hgignore files as well as composer's * own exclude rules from composer.json * * @author Nils Adermann */ class ArchivableFilesFinder extends \FilterIterator { /** * @var Finder */ protected $finder; /** * Initializes the internal Symfony Finder with appropriate filters * * @param string $sources Path to source files to be archived * @param string[] $excludes Composer's own exclude rules from composer.json * @param bool $ignoreFilters Ignore filters when looking for files */ public function __construct($sources, array $excludes, $ignoreFilters = false) { $fs = new Filesystem(); $sources = $fs->normalizePath(realpath($sources)); if ($ignoreFilters) { $filters = array(); } else { $filters = array( new GitExcludeFilter($sources), new ComposerExcludeFilter($sources, $excludes), ); } $this->finder = new Finder(); $filter = function (\SplFileInfo $file) use ($sources, $filters, $fs) { if ($file->isLink() && strpos($file->getRealPath(), $sources) !== 0) { return false; } $relativePath = Preg::replace( '#^'.preg_quote($sources, '#').'#', '', $fs->normalizePath($file->getRealPath()) ); $exclude = false; foreach ($filters as $filter) { $exclude = $filter->filter($relativePath, $exclude); } return !$exclude; }; if (method_exists($filter, 'bindTo')) { $filter = $filter->bindTo(null); } $this->finder ->in($sources) ->filter($filter) ->ignoreVCS(true) ->ignoreDotFiles(false) ->sortByName(); parent::__construct($this->finder->getIterator()); } #[\ReturnTypeWillChange] public function accept() { /** @var SplFileInfo $current */ $current = $this->getInnerIterator()->current(); if (!$current->isDir()) { return true; } $iterator = new FilesystemIterator($current, FilesystemIterator::SKIP_DOTS); return !$iterator->valid(); } } * Jordi Boggiano * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Composer\Package\Archiver; use Composer\Pcre\Preg; use Symfony\Component\Finder; /** * @author Nils Adermann */ abstract class BaseExcludeFilter { /** * @var string */ protected $sourcePath; /** * @var array array of [$pattern, $negate, $stripLeadingSlash] arrays */ protected $excludePatterns; /** * @param string $sourcePath Directory containing sources to be filtered */ public function __construct($sourcePath) { $this->sourcePath = $sourcePath; $this->excludePatterns = array(); } /** * Checks the given path against all exclude patterns in this filter * * Negated patterns overwrite exclude decisions of previous filters. * * @param string $relativePath The file's path relative to the sourcePath * @param bool $exclude Whether a previous filter wants to exclude this file * * @return bool Whether the file should be excluded */ public function filter($relativePath, $exclude) { foreach ($this->excludePatterns as $patternData) { list($pattern, $negate, $stripLeadingSlash) = $patternData; if ($stripLeadingSlash) { $path = substr($relativePath, 1); } else { $path = $relativePath; } try { if (Preg::isMatch($pattern, $path)) { $exclude = !$negate; } } catch (\RuntimeException $e) { // suppressed } } return $exclude; } /** * Processes a file containing exclude rules of different formats per line * * @param string[] $lines A set of lines to be parsed * @param callable $lineParser The parser to be used on each line * * @return array Exclude patterns to be used in filter() */ protected function parseLines(array $lines, $lineParser) { return array_filter( array_map( function ($line) use ($lineParser) { $line = trim($line); if (!$line || 0 === strpos($line, '#')) { return null; } return call_user_func($lineParser, $line); }, $lines ), function ($pattern) { return $pattern !== null; } ); } /** * Generates a set of exclude patterns for filter() from gitignore rules * * @param string[] $rules A list of exclude rules in gitignore syntax * * @return array Exclude patterns */ protected function generatePatterns($rules) { $patterns = array(); foreach ($rules as $rule) { $patterns[] = $this->generatePattern($rule); } return $patterns; } /** * Generates an exclude pattern for filter() from a gitignore rule * * @param string $rule An exclude rule in gitignore syntax * * @return array{0: string, 1: bool, 2: bool} An exclude pattern */ protected function generatePattern($rule) { $negate = false; $pattern = ''; if ($rule !== '' && $rule[0] === '!') { $negate = true; $rule = ltrim($rule, '!'); } $firstSlashPosition = strpos($rule, '/'); if (0 === $firstSlashPosition) { $pattern = '^/'; } elseif (false === $firstSlashPosition || strlen($rule) - 1 === $firstSlashPosition) { $pattern = '/'; } $rule = trim($rule, '/'); // remove delimiters as well as caret (^) and dollar sign ($) from the regex $rule = substr(Finder\Glob::toRegex($rule), 2, -2); return array('{'.$pattern.$rule.'(?=$|/)}', $negate, false); } } * Jordi Boggiano * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Composer\Package\Archiver; use ZipArchive; use Composer\Util\Filesystem; /** * @author Jan Prieser */ class ZipArchiver implements ArchiverInterface { /** @var array */ protected static $formats = array( 'zip' => true, ); /** * @inheritDoc */ public function archive($sources, $target, $format, array $excludes = array(), $ignoreFilters = false) { $fs = new Filesystem(); $sources = $fs->normalizePath($sources); $zip = new ZipArchive(); $res = $zip->open($target, ZipArchive::CREATE); if ($res === true) { $files = new ArchivableFilesFinder($sources, $excludes, $ignoreFilters); foreach ($files as $file) { /** @var \SplFileInfo $file */ $filepath = strtr($file->getPath()."/".$file->getFilename(), '\\', '/'); $localname = $filepath; if (strpos($localname, $sources . '/') === 0) { $localname = substr($localname, strlen($sources . '/')); } if ($file->isDir()) { $zip->addEmptyDir($localname); } else { $zip->addFile($filepath, $localname); } /** * ZipArchive::setExternalAttributesName is available from >= PHP 5.6 * setExternalAttributesName() is only available with libzip 0.11.2 or above */ if (PHP_VERSION_ID >= 50600 && method_exists($zip, 'setExternalAttributesName')) { $perms = fileperms($filepath); /** * Ensure to preserve the permission umasks for the filepath in the archive. */ $zip->setExternalAttributesName($localname, ZipArchive::OPSYS_UNIX, $perms << 16); } } if ($zip->close()) { return $target; } } $message = sprintf( "Could not create archive '%s' from '%s': %s", $target, $sources, $zip->getStatusString() ); throw new \RuntimeException($message); } /** * @inheritDoc */ public function supports($format, $sourceType) { return isset(static::$formats[$format]) && $this->compressionAvailable(); } /** * @return bool */ private function compressionAvailable() { return class_exists('ZipArchive'); } } * Jordi Boggiano * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Composer\Package\Archiver; /** * @author Till Klampaeckel * @author Matthieu Moquet * @author Nils Adermann */ interface ArchiverInterface { /** * Create an archive from the sources. * * @param string $sources The sources directory * @param string $target The target file * @param string $format The format used for archive * @param string[] $excludes A list of patterns for files to exclude * @param bool $ignoreFilters Whether to ignore filters when looking for files * * @return string The path to the written archive file */ public function archive($sources, $target, $format, array $excludes = array(), $ignoreFilters = false); /** * Format supported by the archiver. * * @param string $format The archive format * @param string $sourceType The source type (git, svn, hg, etc.) * * @return bool true if the format is supported by the archiver */ public function supports($format, $sourceType); } * Jordi Boggiano * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Composer\Package\Archiver; use Composer\Downloader\DownloadManager; use Composer\Package\RootPackageInterface; use Composer\Pcre\Preg; use Composer\Util\Filesystem; use Composer\Util\Loop; use Composer\Util\SyncHelper; use Composer\Json\JsonFile; use Composer\Package\CompletePackageInterface; /** * @author Matthieu Moquet * @author Till Klampaeckel */ class ArchiveManager { /** @var DownloadManager */ protected $downloadManager; /** @var Loop */ protected $loop; /** * @var ArchiverInterface[] */ protected $archivers = array(); /** * @var bool */ protected $overwriteFiles = true; /** * @param DownloadManager $downloadManager A manager used to download package sources */ public function __construct(DownloadManager $downloadManager, Loop $loop) { $this->downloadManager = $downloadManager; $this->loop = $loop; } /** * @param ArchiverInterface $archiver * * @return void */ public function addArchiver(ArchiverInterface $archiver) { $this->archivers[] = $archiver; } /** * Set whether existing archives should be overwritten * * @param bool $overwriteFiles New setting * * @return $this */ public function setOverwriteFiles($overwriteFiles) { $this->overwriteFiles = $overwriteFiles; return $this; } /** * Generate a distinct filename for a particular version of a package. * * @param CompletePackageInterface $package The package to get a name for * * @return string A filename without an extension */ public function getPackageFilename(CompletePackageInterface $package) { if ($package->getArchiveName()) { $baseName = $package->getArchiveName(); } else { $baseName = Preg::replace('#[^a-z0-9-_]#i', '-', $package->getName()); } $nameParts = array($baseName); if (null !== $package->getDistReference() && Preg::isMatch('{^[a-f0-9]{40}$}', $package->getDistReference())) { array_push($nameParts, $package->getDistReference(), $package->getDistType()); } else { array_push($nameParts, $package->getPrettyVersion(), $package->getDistReference()); } if ($package->getSourceReference()) { $nameParts[] = substr(sha1($package->getSourceReference()), 0, 6); } $name = implode('-', array_filter($nameParts, function ($p) { return !empty($p); })); return str_replace('/', '-', $name); } /** * Create an archive of the specified package. * * @param CompletePackageInterface $package The package to archive * @param string $format The format of the archive (zip, tar, ...) * @param string $targetDir The directory where to build the archive * @param string|null $fileName The relative file name to use for the archive, or null to generate * the package name. Note that the format will be appended to this name * @param bool $ignoreFilters Ignore filters when looking for files in the package * @throws \InvalidArgumentException * @throws \RuntimeException * @return string The path of the created archive */ public function archive(CompletePackageInterface $package, $format, $targetDir, $fileName = null, $ignoreFilters = false) { if (empty($format)) { throw new \InvalidArgumentException('Format must be specified'); } // Search for the most appropriate archiver $usableArchiver = null; foreach ($this->archivers as $archiver) { if ($archiver->supports($format, $package->getSourceType())) { $usableArchiver = $archiver; break; } } // Checks the format/source type are supported before downloading the package if (null === $usableArchiver) { throw new \RuntimeException(sprintf('No archiver found to support %s format', $format)); } $filesystem = new Filesystem(); if ($package instanceof RootPackageInterface) { $sourcePath = realpath('.'); } else { // Directory used to download the sources $sourcePath = sys_get_temp_dir().'/composer_archive'.uniqid(); $filesystem->ensureDirectoryExists($sourcePath); try { // Download sources $promise = $this->downloadManager->download($package, $sourcePath); SyncHelper::await($this->loop, $promise); $promise = $this->downloadManager->install($package, $sourcePath); SyncHelper::await($this->loop, $promise); } catch (\Exception $e) { $filesystem->removeDirectory($sourcePath); throw $e; } // Check exclude from downloaded composer.json if (file_exists($composerJsonPath = $sourcePath.'/composer.json')) { $jsonFile = new JsonFile($composerJsonPath); $jsonData = $jsonFile->read(); if (!empty($jsonData['archive']['name'])) { $package->setArchiveName($jsonData['archive']['name']); } if (!empty($jsonData['archive']['exclude'])) { $package->setArchiveExcludes($jsonData['archive']['exclude']); } } } if (null === $fileName) { $packageName = $this->getPackageFilename($package); } else { $packageName = $fileName; } // Archive filename $filesystem->ensureDirectoryExists($targetDir); $target = realpath($targetDir).'/'.$packageName.'.'.$format; $filesystem->ensureDirectoryExists(dirname($target)); if (!$this->overwriteFiles && file_exists($target)) { return $target; } // Create the archive $tempTarget = sys_get_temp_dir().'/composer_archive'.uniqid().'.'.$format; $filesystem->ensureDirectoryExists(dirname($tempTarget)); $archivePath = $usableArchiver->archive($sourcePath, $tempTarget, $format, $package->getArchiveExcludes(), $ignoreFilters); $filesystem->rename($archivePath, $target); // cleanup temporary download if (!$package instanceof RootPackageInterface) { $filesystem->removeDirectory($sourcePath); } $filesystem->remove($tempTarget); return $target; } } * Jordi Boggiano * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Composer\Package\Archiver; /** * An exclude filter which processes composer's own exclude rules * * @author Nils Adermann */ class ComposerExcludeFilter extends BaseExcludeFilter { /** * @param string $sourcePath Directory containing sources to be filtered * @param string[] $excludeRules An array of exclude rules from composer.json */ public function __construct($sourcePath, array $excludeRules) { parent::__construct($sourcePath); $this->excludePatterns = $this->generatePatterns($excludeRules); } } * Jordi Boggiano * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Composer\Package\Archiver; use FilterIterator; use PharData; class ArchivableFilesFilter extends FilterIterator { /** @var string[] */ private $dirs = array(); /** * @return bool true if the current element is acceptable, otherwise false. */ #[\ReturnTypeWillChange] public function accept() { $file = $this->getInnerIterator()->current(); if ($file->isDir()) { $this->dirs[] = (string) $file; return false; } return true; } /** * @param string $sources * * @return void */ public function addEmptyDir(PharData $phar, $sources) { foreach ($this->dirs as $filepath) { $localname = str_replace($sources . "/", '', $filepath); $phar->addEmptyDir($localname); } } } * Jordi Boggiano * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Composer\Package\Archiver; use Composer\Pcre\Preg; /** * An exclude filter that processes gitattributes * * It respects export-ignore git attributes * * @author Nils Adermann */ class GitExcludeFilter extends BaseExcludeFilter { /** * Parses .gitattributes if it exists * * @param string $sourcePath */ public function __construct($sourcePath) { parent::__construct($sourcePath); if (file_exists($sourcePath.'/.gitattributes')) { $this->excludePatterns = array_merge( $this->excludePatterns, $this->parseLines( file($sourcePath.'/.gitattributes'), array($this, 'parseGitAttributesLine') ) ); } } /** * Callback parser which finds export-ignore rules in git attribute lines * * @param string $line A line from .gitattributes * * @return array{0: string, 1: bool, 2: bool}|null An exclude pattern for filter() */ public function parseGitAttributesLine($line) { $parts = Preg::split('#\s+#', $line); if (count($parts) == 2 && $parts[1] === 'export-ignore') { return $this->generatePattern($parts[0]); } if (count($parts) == 2 && $parts[1] === '-export-ignore') { return $this->generatePattern('!'.$parts[0]); } return null; } } * Jordi Boggiano * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Composer\Package; /** * Defines package metadata that is not necessarily needed for solving and installing packages * * @author Nils Adermann */ interface CompletePackageInterface extends PackageInterface { /** * Returns the scripts of this package * * @return array Map of script name to array of handlers */ public function getScripts(); /** * @param array $scripts * @return void */ public function setScripts(array $scripts); /** * Returns an array of repositories * * @return mixed[] Repositories */ public function getRepositories(); /** * Set the repositories * * @param mixed[] $repositories * @return void */ public function setRepositories(array $repositories); /** * Returns the package license, e.g. MIT, BSD, GPL * * @return string[] The package licenses */ public function getLicense(); /** * Set the license * * @param string[] $license * @return void */ public function setLicense(array $license); /** * Returns an array of keywords relating to the package * * @return string[] */ public function getKeywords(); /** * Set the keywords * * @param string[] $keywords * @return void */ public function setKeywords(array $keywords); /** * Returns the package description * * @return ?string */ public function getDescription(); /** * Set the description * * @param string $description * @return void */ public function setDescription($description); /** * Returns the package homepage * * @return ?string */ public function getHomepage(); /** * Set the homepage * * @param string $homepage * @return void */ public function setHomepage($homepage); /** * Returns an array of authors of the package * * Each item can contain name/homepage/email keys * * @return array */ public function getAuthors(); /** * Set the authors * * @param array $authors * @return void */ public function setAuthors(array $authors); /** * Returns the support information * * @return array{issues?: string, forum?: string, wiki?: string, source?: string, email?: string, irc?: string, docs?: string, rss?: string, chat?: string} */ public function getSupport(); /** * Set the support information * * @param array{issues?: string, forum?: string, wiki?: string, source?: string, email?: string, irc?: string, docs?: string, rss?: string, chat?: string} $support * @return void */ public function setSupport(array $support); /** * Returns an array of funding options for the package * * Each item will contain type and url keys * * @return array */ public function getFunding(); /** * Set the funding * * @param array $funding * @return void */ public function setFunding(array $funding); /** * Returns if the package is abandoned or not * * @return bool */ public function isAbandoned(); /** * If the package is abandoned and has a suggested replacement, this method returns it * * @return string|null */ public function getReplacementPackage(); /** * @param bool|string $abandoned * @return void */ public function setAbandoned($abandoned); /** * Returns default base filename for archive * * @return ?string */ public function getArchiveName(); /** * Sets default base filename for archive * * @param string $name * @return void */ public function setArchiveName($name); /** * Returns a list of patterns to exclude from package archives * * @return string[] */ public function getArchiveExcludes(); /** * Sets a list of patterns to be excluded from archives * * @param string[] $excludes * @return void */ public function setArchiveExcludes(array $excludes); } * Jordi Boggiano * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Composer\Package; use Composer\Semver\Constraint\ConstraintInterface; /** * Represents a link between two packages, represented by their names * * @author Nils Adermann */ class Link { const TYPE_REQUIRE = 'requires'; const TYPE_DEV_REQUIRE = 'devRequires'; const TYPE_PROVIDE = 'provides'; const TYPE_CONFLICT = 'conflicts'; const TYPE_REPLACE = 'replaces'; /** * Special type * @internal */ const TYPE_DOES_NOT_REQUIRE = 'does not require'; /** * TODO should be marked private once 5.3 is dropped * @private */ const TYPE_UNKNOWN = 'relates to'; /** * Will be converted into a constant once the min PHP version allows this * * @internal * @var string[] * @phpstan-var array */ public static $TYPES = array( self::TYPE_REQUIRE, self::TYPE_DEV_REQUIRE, self::TYPE_PROVIDE, self::TYPE_CONFLICT, self::TYPE_REPLACE, ); /** * @var string */ protected $source; /** * @var string */ protected $target; /** * @var ConstraintInterface */ protected $constraint; /** * @var string * @phpstan-var string $description */ protected $description; /** * @var ?string */ protected $prettyConstraint; /** * Creates a new package link. * * @param string $source * @param string $target * @param ConstraintInterface $constraint Constraint applying to the target of this link * @param self::TYPE_* $description Used to create a descriptive string representation * @param string|null $prettyConstraint */ public function __construct( $source, $target, ConstraintInterface $constraint, $description = self::TYPE_UNKNOWN, $prettyConstraint = null ) { $this->source = strtolower($source); $this->target = strtolower($target); $this->constraint = $constraint; $this->description = self::TYPE_DEV_REQUIRE === $description ? 'requires (for development)' : $description; $this->prettyConstraint = $prettyConstraint; } /** * @return string */ public function getDescription() { return $this->description; } /** * @return string */ public function getSource() { return $this->source; } /** * @return string */ public function getTarget() { return $this->target; } /** * @return ConstraintInterface */ public function getConstraint() { return $this->constraint; } /** * @throws \UnexpectedValueException If no pretty constraint was provided * @return string */ public function getPrettyConstraint() { if (null === $this->prettyConstraint) { throw new \UnexpectedValueException(sprintf('Link %s has been misconfigured and had no prettyConstraint given.', $this)); } return $this->prettyConstraint; } /** * @return string */ public function __toString() { return $this->source.' '.$this->description.' '.$this->target.' ('.$this->constraint.')'; } /** * @param PackageInterface $sourcePackage * @return string */ public function getPrettyString(PackageInterface $sourcePackage) { return $sourcePackage->getPrettyString().' '.$this->description.' '.$this->target.' '.$this->constraint->getPrettyString(); } } * Jordi Boggiano * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Composer\Package\Version; use Composer\Pcre\Preg; use Composer\Repository\PlatformRepository; use Composer\Semver\VersionParser as SemverVersionParser; use Composer\Semver\Semver; use Composer\Semver\Constraint\ConstraintInterface; class VersionParser extends SemverVersionParser { const DEFAULT_BRANCH_ALIAS = '9999999-dev'; /** @var array Constraint parsing cache */ private static $constraints = array(); /** * @inheritDoc */ public function parseConstraints($constraints) { if (!isset(self::$constraints[$constraints])) { self::$constraints[$constraints] = parent::parseConstraints($constraints); } return self::$constraints[$constraints]; } /** * Parses an array of strings representing package/version pairs. * * The parsing results in an array of arrays, each of which * contain a 'name' key with value and optionally a 'version' key with value. * * @param string[] $pairs a set of package/version pairs separated by ":", "=" or " " * * @return list */ public function parseNameVersionPairs(array $pairs) { $pairs = array_values($pairs); $result = array(); for ($i = 0, $count = count($pairs); $i < $count; $i++) { $pair = Preg::replace('{^([^=: ]+)[=: ](.*)$}', '$1 $2', trim($pairs[$i])); if (false === strpos($pair, ' ') && isset($pairs[$i + 1]) && false === strpos($pairs[$i + 1], '/') && !Preg::isMatch('{(?<=[a-z0-9_/-])\*|\*(?=[a-z0-9_/-])}i', $pairs[$i + 1]) && !PlatformRepository::isPlatformPackage($pairs[$i + 1])) { $pair .= ' '.$pairs[$i + 1]; $i++; } if (strpos($pair, ' ')) { list($name, $version) = explode(' ', $pair, 2); $result[] = array('name' => $name, 'version' => $version); } else { $result[] = array('name' => $pair); } } return $result; } /** * @param string $normalizedFrom * @param string $normalizedTo * * @return bool */ public static function isUpgrade($normalizedFrom, $normalizedTo) { if ($normalizedFrom === $normalizedTo) { return true; } if (in_array($normalizedFrom, array('dev-master', 'dev-trunk', 'dev-default'), true)) { $normalizedFrom = VersionParser::DEFAULT_BRANCH_ALIAS; } if (in_array($normalizedTo, array('dev-master', 'dev-trunk', 'dev-default'), true)) { $normalizedTo = VersionParser::DEFAULT_BRANCH_ALIAS; } if (strpos($normalizedFrom, 'dev-') === 0 || strpos($normalizedTo, 'dev-') === 0) { return true; } $sorted = Semver::sort(array($normalizedTo, $normalizedFrom)); return $sorted[0] === $normalizedFrom; } } * Jordi Boggiano * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Composer\Package\Version; use Composer\Config; use Composer\Pcre\Preg; use Composer\Repository\Vcs\HgDriver; use Composer\IO\NullIO; use Composer\Semver\VersionParser as SemverVersionParser; use Composer\Util\Git as GitUtil; use Composer\Util\HttpDownloader; use Composer\Util\ProcessExecutor; use Composer\Util\Svn as SvnUtil; /** * Try to guess the current version number based on different VCS configuration. * * @author Jordi Boggiano * @author Samuel Roze * * @phpstan-type Version array{version: string, commit: string|null, pretty_version: string|null, feature_version?: string|null, feature_pretty_version?: string|null} */ class VersionGuesser { /** * @var Config */ private $config; /** * @var ProcessExecutor */ private $process; /** * @var SemverVersionParser */ private $versionParser; /** * @param Config $config * @param ProcessExecutor $process * @param SemverVersionParser $versionParser */ public function __construct(Config $config, ProcessExecutor $process, SemverVersionParser $versionParser) { $this->config = $config; $this->process = $process; $this->versionParser = $versionParser; } /** * @param array $packageConfig * @param string $path Path to guess into * * @return array|null * @phpstan-return Version|null */ public function guessVersion(array $packageConfig, $path) { if (!function_exists('proc_open')) { return null; } $versionData = $this->guessGitVersion($packageConfig, $path); if (null !== $versionData && null !== $versionData['version']) { return $this->postprocess($versionData); } $versionData = $this->guessHgVersion($packageConfig, $path); if (null !== $versionData && null !== $versionData['version']) { return $this->postprocess($versionData); } $versionData = $this->guessFossilVersion($path); if (null !== $versionData && null !== $versionData['version']) { return $this->postprocess($versionData); } $versionData = $this->guessSvnVersion($packageConfig, $path); if (null !== $versionData && null !== $versionData['version']) { return $this->postprocess($versionData); } return null; } /** * @param array $versionData * * @phpstan-param Version $versionData * * @return array * @phpstan-return Version */ private function postprocess(array $versionData) { if (!empty($versionData['feature_version']) && $versionData['feature_version'] === $versionData['version'] && $versionData['feature_pretty_version'] === $versionData['pretty_version']) { unset($versionData['feature_version'], $versionData['feature_pretty_version']); } if ('-dev' === substr($versionData['version'], -4) && Preg::isMatch('{\.9{7}}', $versionData['version'])) { $versionData['pretty_version'] = Preg::replace('{(\.9{7})+}', '.x', $versionData['version']); } if (!empty($versionData['feature_version']) && '-dev' === substr($versionData['feature_version'], -4) && Preg::isMatch('{\.9{7}}', $versionData['feature_version'])) { $versionData['feature_pretty_version'] = Preg::replace('{(\.9{7})+}', '.x', $versionData['feature_version']); } return $versionData; } /** * @param array $packageConfig * @param string $path * * @return array{version: string|null, commit: string|null, pretty_version: string|null, feature_version?: string|null, feature_pretty_version?: string|null} */ private function guessGitVersion(array $packageConfig, $path) { GitUtil::cleanEnv(); $commit = null; $version = null; $prettyVersion = null; $featureVersion = null; $featurePrettyVersion = null; $isDetached = false; // try to fetch current version from git branch if (0 === $this->process->execute('git branch -a --no-color --no-abbrev -v', $output, $path)) { $branches = array(); $isFeatureBranch = false; // find current branch and collect all branch names foreach ($this->process->splitLines($output) as $branch) { if ($branch && Preg::isMatch('{^(?:\* ) *(\(no branch\)|\(detached from \S+\)|\(HEAD detached at \S+\)|\S+) *([a-f0-9]+) .*$}', $branch, $match)) { if ( $match[1] === '(no branch)' || strpos($match[1], '(detached ') === 0 || strpos($match[1], '(HEAD detached at') === 0 ) { $version = 'dev-' . $match[2]; $prettyVersion = $version; $isFeatureBranch = true; $isDetached = true; } else { $version = $this->versionParser->normalizeBranch($match[1]); $prettyVersion = 'dev-' . $match[1]; $isFeatureBranch = $this->isFeatureBranch($packageConfig, $match[1]); } if ($match[2]) { $commit = $match[2]; } } if ($branch && !Preg::isMatch('{^ *.+/HEAD }', $branch)) { if (Preg::isMatch('{^(?:\* )? *((?:remotes/(?:origin|upstream)/)?[^\s/]+) *([a-f0-9]+) .*$}', $branch, $match)) { $branches[] = $match[1]; } } } if ($isFeatureBranch) { $featureVersion = $version; $featurePrettyVersion = $prettyVersion; // try to find the best (nearest) version branch to assume this feature's version $result = $this->guessFeatureVersion($packageConfig, $version, $branches, 'git rev-list %candidate%..%branch%', $path); $version = $result['version']; $prettyVersion = $result['pretty_version']; } } if (!$version || $isDetached) { $result = $this->versionFromGitTags($path); if ($result) { $version = $result['version']; $prettyVersion = $result['pretty_version']; $featureVersion = null; $featurePrettyVersion = null; } } if (!$commit) { $command = 'git log --pretty="%H" -n1 HEAD'.GitUtil::getNoShowSignatureFlag($this->process); if (0 === $this->process->execute($command, $output, $path)) { $commit = trim($output) ?: null; } } if ($featureVersion) { return array('version' => $version, 'commit' => $commit, 'pretty_version' => $prettyVersion, 'feature_version' => $featureVersion, 'feature_pretty_version' => $featurePrettyVersion); } return array('version' => $version, 'commit' => $commit, 'pretty_version' => $prettyVersion); } /** * @param string $path * * @return array{version: string, pretty_version: string}|null */ private function versionFromGitTags($path) { // try to fetch current version from git tags if (0 === $this->process->execute('git describe --exact-match --tags', $output, $path)) { try { $version = $this->versionParser->normalize(trim($output)); return array('version' => $version, 'pretty_version' => trim($output)); } catch (\Exception $e) { } } return null; } /** * @param array $packageConfig * @param string $path * * @return array{version: string|null, commit: ''|null, pretty_version: string|null, feature_version?: string|null, feature_pretty_version?: string|null}|null */ private function guessHgVersion(array $packageConfig, $path) { // try to fetch current version from hg branch if (0 === $this->process->execute('hg branch', $output, $path)) { $branch = trim($output); $version = $this->versionParser->normalizeBranch($branch); $isFeatureBranch = 0 === strpos($version, 'dev-'); if (VersionParser::DEFAULT_BRANCH_ALIAS === $version) { return array('version' => $version, 'commit' => null, 'pretty_version' => 'dev-'.$branch); } if (!$isFeatureBranch) { return array('version' => $version, 'commit' => null, 'pretty_version' => $version); } // re-use the HgDriver to fetch branches (this properly includes bookmarks) $io = new NullIO(); $driver = new HgDriver(array('url' => $path), $io, $this->config, new HttpDownloader($io, $this->config), $this->process); $branches = array_map('strval', array_keys($driver->getBranches())); // try to find the best (nearest) version branch to assume this feature's version $result = $this->guessFeatureVersion($packageConfig, $version, $branches, 'hg log -r "not ancestors(\'%candidate%\') and ancestors(\'%branch%\')" --template "{node}\\n"', $path); $result['commit'] = ''; $result['feature_version'] = $version; $result['feature_pretty_version'] = $version; return $result; } return null; } /** * @param array $packageConfig * @param string|null $version * @param string[] $branches * @param string $scmCmdline * @param string $path * * @phpstan-param non-empty-string $scmCmdline * * @return array{version: string|null, pretty_version: string|null} */ private function guessFeatureVersion(array $packageConfig, $version, array $branches, $scmCmdline, $path) { $prettyVersion = $version; // ignore feature branches if they have no branch-alias or self.version is used // and find the branch they came from to use as a version instead if (!isset($packageConfig['extra']['branch-alias'][$version]) || strpos(json_encode($packageConfig), '"self.version"') ) { $branch = Preg::replace('{^dev-}', '', $version); $length = PHP_INT_MAX; // return directly, if branch is configured to be non-feature branch if (!$this->isFeatureBranch($packageConfig, $branch)) { return array('version' => $version, 'pretty_version' => $prettyVersion); } // sort local branches first then remote ones // and sort numeric branches below named ones, to make sure if the branch has the same distance from main and 1.10 and 1.9 for example, main is picked // and sort using natural sort so that 1.10 will appear before 1.9 usort($branches, function ($a, $b) { $aRemote = 0 === strpos($a, 'remotes/'); $bRemote = 0 === strpos($b, 'remotes/'); if ($aRemote !== $bRemote) { return $aRemote ? 1 : -1; } return strnatcasecmp($b, $a); }); foreach ($branches as $candidate) { $candidateVersion = Preg::replace('{^remotes/\S+/}', '', $candidate); // do not compare against itself or other feature branches if ($candidate === $branch || $this->isFeatureBranch($packageConfig, $candidateVersion)) { continue; } $cmdLine = str_replace(array('%candidate%', '%branch%'), array($candidate, $branch), $scmCmdline); if (0 !== $this->process->execute($cmdLine, $output, $path)) { continue; } if (strlen($output) < $length) { $length = strlen($output); $version = $this->versionParser->normalizeBranch($candidateVersion); $prettyVersion = 'dev-' . $candidateVersion; if ($length === 0) { break; } } } } return array('version' => $version, 'pretty_version' => $prettyVersion); } /** * @param array $packageConfig * @param string|null $branchName * * @return bool */ private function isFeatureBranch(array $packageConfig, $branchName) { $nonFeatureBranches = ''; if (!empty($packageConfig['non-feature-branches'])) { $nonFeatureBranches = implode('|', $packageConfig['non-feature-branches']); } return !Preg::isMatch('{^(' . $nonFeatureBranches . '|master|main|latest|next|current|support|tip|trunk|default|develop|\d+\..+)$}', $branchName, $match); } /** * @param string $path * * @return array{version: string|null, commit: '', pretty_version: string|null} */ private function guessFossilVersion($path) { $version = null; $prettyVersion = null; // try to fetch current version from fossil if (0 === $this->process->execute('fossil branch list', $output, $path)) { $branch = trim($output); $version = $this->versionParser->normalizeBranch($branch); $prettyVersion = 'dev-' . $branch; } // try to fetch current version from fossil tags if (0 === $this->process->execute('fossil tag list', $output, $path)) { try { $version = $this->versionParser->normalize(trim($output)); $prettyVersion = trim($output); } catch (\Exception $e) { } } return array('version' => $version, 'commit' => '', 'pretty_version' => $prettyVersion); } /** * @param array $packageConfig * @param string $path * * @return array{version: string, commit: '', pretty_version: string}|null */ private function guessSvnVersion(array $packageConfig, $path) { SvnUtil::cleanEnv(); // try to fetch current version from svn if (0 === $this->process->execute('svn info --xml', $output, $path)) { $trunkPath = isset($packageConfig['trunk-path']) ? preg_quote($packageConfig['trunk-path'], '#') : 'trunk'; $branchesPath = isset($packageConfig['branches-path']) ? preg_quote($packageConfig['branches-path'], '#') : 'branches'; $tagsPath = isset($packageConfig['tags-path']) ? preg_quote($packageConfig['tags-path'], '#') : 'tags'; $urlPattern = '#.*/(' . $trunkPath . '|(' . $branchesPath . '|' . $tagsPath . ')/(.*))#'; if (Preg::isMatch($urlPattern, $output, $matches)) { if (isset($matches[2]) && ($branchesPath === $matches[2] || $tagsPath === $matches[2])) { // we are in a branches path $version = $this->versionParser->normalizeBranch($matches[3]); $prettyVersion = 'dev-' . $matches[3]; return array('version' => $version, 'commit' => '', 'pretty_version' => $prettyVersion); } $prettyVersion = trim($matches[1]); if ($prettyVersion === 'trunk') { $version = 'dev-trunk'; } else { $version = $this->versionParser->normalize($prettyVersion); } return array('version' => $version, 'commit' => '', 'pretty_version' => $prettyVersion); } } return null; } } * Jordi Boggiano * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Composer\Package\Version; use Composer\Filter\PlatformRequirementFilter\IgnoreAllPlatformRequirementFilter; use Composer\Filter\PlatformRequirementFilter\PlatformRequirementFilterFactory; use Composer\Filter\PlatformRequirementFilter\PlatformRequirementFilterInterface; use Composer\Package\BasePackage; use Composer\Package\AliasPackage; use Composer\Package\PackageInterface; use Composer\Composer; use Composer\Package\Loader\ArrayLoader; use Composer\Package\Dumper\ArrayDumper; use Composer\Pcre\Preg; use Composer\Repository\RepositorySet; use Composer\Repository\PlatformRepository; use Composer\Semver\Constraint\Constraint; use Composer\Semver\Constraint\ConstraintInterface; /** * Selects the best possible version for a package * * @author Ryan Weaver * @author Jordi Boggiano */ class VersionSelector { /** @var RepositorySet */ private $repositorySet; /** @var array */ private $platformConstraints = array(); /** @var VersionParser */ private $parser; /** * @param PlatformRepository $platformRepo If passed in, the versions found will be filtered against their requirements to eliminate any not matching the current platform packages */ public function __construct(RepositorySet $repositorySet, PlatformRepository $platformRepo = null) { $this->repositorySet = $repositorySet; if ($platformRepo) { foreach ($platformRepo->getPackages() as $package) { $this->platformConstraints[$package->getName()][] = new Constraint('==', $package->getVersion()); } } } /** * Given a package name and optional version, returns the latest PackageInterface * that matches. * * @param string $packageName * @param string $targetPackageVersion * @param string $preferredStability * @param PlatformRequirementFilterInterface|bool|string[] $platformRequirementFilter * @param int $repoSetFlags* * @return PackageInterface|false */ public function findBestCandidate($packageName, $targetPackageVersion = null, $preferredStability = 'stable', $platformRequirementFilter = null, $repoSetFlags = 0) { if (!isset(BasePackage::$stabilities[$preferredStability])) { // If you get this, maybe you are still relying on the Composer 1.x signature where the 3rd arg was the php version throw new \UnexpectedValueException('Expected a valid stability name as 3rd argument, got '.$preferredStability); } if (null === $platformRequirementFilter) { $platformRequirementFilter = PlatformRequirementFilterFactory::ignoreNothing(); } elseif (!($platformRequirementFilter instanceof PlatformRequirementFilterInterface)) { trigger_error('VersionSelector::findBestCandidate with ignored platform reqs as bool|array is deprecated since Composer 2.2, use an instance of PlatformRequirementFilterInterface instead.', E_USER_DEPRECATED); $platformRequirementFilter = PlatformRequirementFilterFactory::fromBoolOrList($platformRequirementFilter); } $constraint = $targetPackageVersion ? $this->getParser()->parseConstraints($targetPackageVersion) : null; $candidates = $this->repositorySet->findPackages(strtolower($packageName), $constraint, $repoSetFlags); if ($this->platformConstraints && !($platformRequirementFilter instanceof IgnoreAllPlatformRequirementFilter)) { $platformConstraints = $this->platformConstraints; $candidates = array_filter($candidates, function ($pkg) use ($platformConstraints, $platformRequirementFilter) { $reqs = $pkg->getRequires(); foreach ($reqs as $name => $link) { if (!$platformRequirementFilter->isIgnored($name)) { if (isset($platformConstraints[$name])) { foreach ($platformConstraints[$name] as $constraint) { if ($link->getConstraint()->matches($constraint)) { continue 2; } } return false; } elseif (PlatformRepository::isPlatformPackage($name)) { // Package requires a platform package that is unknown on current platform. // It means that current platform cannot validate this constraint and so package is not installable. return false; } } } return true; }); } if (!$candidates) { return false; } // select highest version if we have many $package = reset($candidates); $minPriority = BasePackage::$stabilities[$preferredStability]; foreach ($candidates as $candidate) { $candidatePriority = $candidate->getStabilityPriority(); $currentPriority = $package->getStabilityPriority(); // candidate is less stable than our preferred stability, // and current package is more stable than candidate, skip it if ($minPriority < $candidatePriority && $currentPriority < $candidatePriority) { continue; } // candidate is less stable than our preferred stability, // and current package is less stable than candidate, select candidate if ($minPriority < $candidatePriority && $candidatePriority < $currentPriority) { $package = $candidate; continue; } // candidate is more stable than our preferred stability, // and current package is less stable than preferred stability, select candidate if ($minPriority >= $candidatePriority && $minPriority < $currentPriority) { $package = $candidate; continue; } // select highest version of the two if (version_compare($package->getVersion(), $candidate->getVersion(), '<')) { $package = $candidate; } } // if we end up with 9999999-dev as selected package, make sure we use the original version instead of the alias if ($package instanceof AliasPackage && $package->getVersion() === VersionParser::DEFAULT_BRANCH_ALIAS) { $package = $package->getAliasOf(); } return $package; } /** * Given a concrete version, this returns a ^ constraint (when possible) * that should be used, for example, in composer.json. * * For example: * * 1.2.1 -> ^1.2 * * 1.2 -> ^1.2 * * v3.2.1 -> ^3.2 * * 2.0-beta.1 -> ^2.0@beta * * dev-master -> ^2.1@dev (dev version with alias) * * dev-master -> dev-master (dev versions are untouched) * * @param PackageInterface $package * @return string */ public function findRecommendedRequireVersion(PackageInterface $package) { // Extensions which are versioned in sync with PHP should rather be required as "*" to simplify // the requires and have only one required version to change when bumping the php requirement if (0 === strpos($package->getName(), 'ext-')) { $phpVersion = PHP_MAJOR_VERSION . '.' . PHP_MINOR_VERSION . '.' . PHP_RELEASE_VERSION; $extVersion = implode('.', array_slice(explode('.', $package->getVersion()), 0, 3)); if ($phpVersion === $extVersion) { return '*'; } } $version = $package->getVersion(); if (!$package->isDev()) { return $this->transformVersion($version, $package->getPrettyVersion(), $package->getStability()); } $loader = new ArrayLoader($this->getParser()); $dumper = new ArrayDumper(); $extra = $loader->getBranchAlias($dumper->dump($package)); if ($extra && $extra !== VersionParser::DEFAULT_BRANCH_ALIAS) { $extra = Preg::replace('{^(\d+\.\d+\.\d+)(\.9999999)-dev$}', '$1.0', $extra, -1, $count); if ($count) { $extra = str_replace('.9999999', '.0', $extra); return $this->transformVersion($extra, $extra, 'dev'); } } return $package->getPrettyVersion(); } /** * @param string $version * @param string $prettyVersion * @param string $stability * * @return string */ private function transformVersion($version, $prettyVersion, $stability) { // attempt to transform 2.1.1 to 2.1 // this allows you to upgrade through minor versions $semanticVersionParts = explode('.', $version); // check to see if we have a semver-looking version if (count($semanticVersionParts) == 4 && Preg::isMatch('{^0\D?}', $semanticVersionParts[3])) { // remove the last parts (i.e. the patch version number and any extra) if ($semanticVersionParts[0] === '0') { unset($semanticVersionParts[3]); } else { unset($semanticVersionParts[2], $semanticVersionParts[3]); } $version = implode('.', $semanticVersionParts); } else { return $prettyVersion; } // append stability flag if not default if ($stability != 'stable') { $version .= '@'.$stability; } // 2.1 -> ^2.1 return '^' . $version; } /** * @return VersionParser */ private function getParser() { if ($this->parser === null) { $this->parser = new VersionParser(); } return $this->parser; } } * Jordi Boggiano * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Composer\Package\Version; use Composer\Package\BasePackage; /** * @author Jordi Boggiano */ class StabilityFilter { /** * Checks if any of the provided package names in the given stability match the configured acceptable stability and flags * * @param int[] $acceptableStabilities array of stability => BasePackage::STABILITY_* value * @phpstan-param array $acceptableStabilities * @param int[] $stabilityFlags an array of package name => BasePackage::STABILITY_* value * @phpstan-param array $stabilityFlags * @param string[] $names The package name(s) to check for stability flags * @param string $stability one of 'stable', 'RC', 'beta', 'alpha' or 'dev' * @return bool true if any package name is acceptable */ public static function isPackageAcceptable(array $acceptableStabilities, array $stabilityFlags, array $names, $stability) { foreach ($names as $name) { // allow if package matches the package-specific stability flag if (isset($stabilityFlags[$name])) { if (BasePackage::$stabilities[$stability] <= $stabilityFlags[$name]) { return true; } } elseif (isset($acceptableStabilities[$stability])) { // allow if package matches the global stability requirement and has no exception return true; } } return false; } } * Jordi Boggiano * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Composer\Package\Dumper; use Composer\Package\BasePackage; use Composer\Package\PackageInterface; use Composer\Package\CompletePackageInterface; use Composer\Package\RootPackageInterface; /** * @author Konstantin Kudryashiv * @author Jordi Boggiano */ class ArrayDumper { /** * @return array */ public function dump(PackageInterface $package) { $keys = array( 'binaries' => 'bin', 'type', 'extra', 'installationSource' => 'installation-source', 'autoload', 'devAutoload' => 'autoload-dev', 'notificationUrl' => 'notification-url', 'includePaths' => 'include-path', ); $data = array(); $data['name'] = $package->getPrettyName(); $data['version'] = $package->getPrettyVersion(); $data['version_normalized'] = $package->getVersion(); if ($package->getTargetDir()) { $data['target-dir'] = $package->getTargetDir(); } if ($package->getSourceType()) { $data['source']['type'] = $package->getSourceType(); $data['source']['url'] = $package->getSourceUrl(); if (null !== ($value = $package->getSourceReference())) { $data['source']['reference'] = $value; } if ($mirrors = $package->getSourceMirrors()) { $data['source']['mirrors'] = $mirrors; } } if ($package->getDistType()) { $data['dist']['type'] = $package->getDistType(); $data['dist']['url'] = $package->getDistUrl(); if (null !== ($value = $package->getDistReference())) { $data['dist']['reference'] = $value; } if (null !== ($value = $package->getDistSha1Checksum())) { $data['dist']['shasum'] = $value; } if ($mirrors = $package->getDistMirrors()) { $data['dist']['mirrors'] = $mirrors; } } foreach (BasePackage::$supportedLinkTypes as $type => $opts) { if ($links = $package->{'get'.ucfirst($opts['method'])}()) { foreach ($links as $link) { $data[$type][$link->getTarget()] = $link->getPrettyConstraint(); } ksort($data[$type]); } } if ($packages = $package->getSuggests()) { ksort($packages); $data['suggest'] = $packages; } if ($package->getReleaseDate()) { $data['time'] = $package->getReleaseDate()->format(DATE_RFC3339); } if ($package->isDefaultBranch()) { $data['default-branch'] = true; } $data = $this->dumpValues($package, $keys, $data); if ($package instanceof CompletePackageInterface) { if ($package->getArchiveName()) { $data['archive']['name'] = $package->getArchiveName(); } if ($package->getArchiveExcludes()) { $data['archive']['exclude'] = $package->getArchiveExcludes(); } $keys = array( 'scripts', 'license', 'authors', 'description', 'homepage', 'keywords', 'repositories', 'support', 'funding', ); $data = $this->dumpValues($package, $keys, $data); if (isset($data['keywords']) && \is_array($data['keywords'])) { sort($data['keywords']); } if ($package->isAbandoned()) { $data['abandoned'] = $package->getReplacementPackage() ?: true; } } if ($package instanceof RootPackageInterface) { $minimumStability = $package->getMinimumStability(); if ($minimumStability) { $data['minimum-stability'] = $minimumStability; } } if (\count($package->getTransportOptions()) > 0) { $data['transport-options'] = $package->getTransportOptions(); } return $data; } /** * @param array $keys * @param array $data * * @return array */ private function dumpValues(PackageInterface $package, array $keys, array $data) { foreach ($keys as $method => $key) { if (is_numeric($method)) { $method = $key; } $getter = 'get'.ucfirst($method); $value = $package->$getter(); if (null !== $value && !(\is_array($value) && 0 === \count($value))) { $data[$key] = $value; } } return $data; } } * Jordi Boggiano * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Composer\Package; use Composer\Repository\RepositoryInterface; use Composer\Repository\PlatformRepository; /** * Base class for packages providing name storage and default match implementation * * @author Nils Adermann */ abstract class BasePackage implements PackageInterface { /** * @phpstan-var array * @internal */ public static $supportedLinkTypes = array( 'require' => array('description' => 'requires', 'method' => Link::TYPE_REQUIRE), 'conflict' => array('description' => 'conflicts', 'method' => Link::TYPE_CONFLICT), 'provide' => array('description' => 'provides', 'method' => Link::TYPE_PROVIDE), 'replace' => array('description' => 'replaces', 'method' => Link::TYPE_REPLACE), 'require-dev' => array('description' => 'requires (for development)', 'method' => Link::TYPE_DEV_REQUIRE), ); const STABILITY_STABLE = 0; const STABILITY_RC = 5; const STABILITY_BETA = 10; const STABILITY_ALPHA = 15; const STABILITY_DEV = 20; /** @var array */ public static $stabilities = array( 'stable' => self::STABILITY_STABLE, 'RC' => self::STABILITY_RC, 'beta' => self::STABILITY_BETA, 'alpha' => self::STABILITY_ALPHA, 'dev' => self::STABILITY_DEV, ); /** * READ-ONLY: The package id, public for fast access in dependency solver * @var int * @internal * @readonly */ public $id; /** @var string */ protected $name; /** @var string */ protected $prettyName; /** @var ?RepositoryInterface */ protected $repository = null; /** * All descendants' constructors should call this parent constructor * * @param string $name The package's name */ public function __construct($name) { $this->prettyName = $name; $this->name = strtolower($name); $this->id = -1; } /** * @inheritDoc */ public function getName() { return $this->name; } /** * @inheritDoc */ public function getPrettyName() { return $this->prettyName; } /** * @inheritDoc */ public function getNames($provides = true) { $names = array( $this->getName() => true, ); if ($provides) { foreach ($this->getProvides() as $link) { $names[$link->getTarget()] = true; } } foreach ($this->getReplaces() as $link) { $names[$link->getTarget()] = true; } return array_keys($names); } /** * @inheritDoc */ public function setId($id) { $this->id = $id; } /** * @inheritDoc */ public function getId() { return $this->id; } /** * @inheritDoc */ public function setRepository(RepositoryInterface $repository) { if ($this->repository && $repository !== $this->repository) { throw new \LogicException('A package can only be added to one repository'); } $this->repository = $repository; } /** * @inheritDoc */ public function getRepository() { return $this->repository; } /** * checks if this package is a platform package * * @return bool */ public function isPlatform() { return $this->getRepository() instanceof PlatformRepository; } /** * Returns package unique name, constructed from name, version and release type. * * @return string */ public function getUniqueName() { return $this->getName().'-'.$this->getVersion(); } /** * @return bool */ public function equals(PackageInterface $package) { $self = $this; if ($this instanceof AliasPackage) { $self = $this->getAliasOf(); } if ($package instanceof AliasPackage) { $package = $package->getAliasOf(); } return $package === $self; } /** * Converts the package into a readable and unique string * * @return string */ public function __toString() { return $this->getUniqueName(); } public function getPrettyString() { return $this->getPrettyName().' '.$this->getPrettyVersion(); } /** * @inheritDoc */ public function getFullPrettyVersion($truncate = true, $displayMode = PackageInterface::DISPLAY_SOURCE_REF_IF_DEV) { if ($displayMode === PackageInterface::DISPLAY_SOURCE_REF_IF_DEV && (!$this->isDev() || !\in_array($this->getSourceType(), array('hg', 'git'))) ) { return $this->getPrettyVersion(); } switch ($displayMode) { case PackageInterface::DISPLAY_SOURCE_REF_IF_DEV: case PackageInterface::DISPLAY_SOURCE_REF: $reference = $this->getSourceReference(); break; case PackageInterface::DISPLAY_DIST_REF: $reference = $this->getDistReference(); break; default: throw new \UnexpectedValueException('Display mode '.$displayMode.' is not supported'); } if (null === $reference) { return $this->getPrettyVersion(); } // if source reference is a sha1 hash -- truncate if ($truncate && \strlen($reference) === 40 && $this->getSourceType() !== 'svn') { return $this->getPrettyVersion() . ' ' . substr($reference, 0, 7); } return $this->getPrettyVersion() . ' ' . $reference; } /** * @return int * * @phpstan-return self::STABILITY_* */ public function getStabilityPriority() { return self::$stabilities[$this->getStability()]; } public function __clone() { $this->repository = null; $this->id = -1; } /** * Build a regexp from a package name, expanding * globs as required * * @param string $allowPattern * @param string $wrap Wrap the cleaned string by the given string * @return string */ public static function packageNameToRegexp($allowPattern, $wrap = '{^%s$}i') { $cleanedAllowPattern = str_replace('\\*', '.*', preg_quote($allowPattern)); return sprintf($wrap, $cleanedAllowPattern); } /** * Build a regexp from package names, expanding * globs as required * * @param string[] $packageNames * @param string $wrap * @return string */ public static function packageNamesToRegexp(array $packageNames, $wrap = '{^(?:%s)$}iD') { $packageNames = array_map( function ($packageName) { return BasePackage::packageNameToRegexp($packageName, '%s'); }, $packageNames ); return sprintf($wrap, implode('|', $packageNames)); } } * Jordi Boggiano * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Composer; use Composer\Package\RootPackageInterface; use Composer\Package\Locker; use Composer\Pcre\Preg; use Composer\Util\Loop; use Composer\Repository\RepositoryManager; use Composer\Installer\InstallationManager; use Composer\Plugin\PluginManager; use Composer\Downloader\DownloadManager; use Composer\EventDispatcher\EventDispatcher; use Composer\Autoload\AutoloadGenerator; use Composer\Package\Archiver\ArchiveManager; /** * @author Jordi Boggiano * @author Konstantin Kudryashiv * @author Nils Adermann */ class Composer { /* * Examples of the following constants in the various configurations they can be in * * releases (phar): * const VERSION = '1.8.2'; * const BRANCH_ALIAS_VERSION = ''; * const RELEASE_DATE = '2019-01-29 15:00:53'; * const SOURCE_VERSION = ''; * * snapshot builds (phar): * const VERSION = 'd3873a05650e168251067d9648845c220c50e2d7'; * const BRANCH_ALIAS_VERSION = '1.9-dev'; * const RELEASE_DATE = '2019-02-20 07:43:56'; * const SOURCE_VERSION = ''; * * source (git clone): * const VERSION = '@package_version@'; * const BRANCH_ALIAS_VERSION = '@package_branch_alias_version@'; * const RELEASE_DATE = '@release_date@'; * const SOURCE_VERSION = '1.8-dev+source'; */ const VERSION = '2.2.5'; const BRANCH_ALIAS_VERSION = ''; const RELEASE_DATE = '2022-01-21 17:25:52'; const SOURCE_VERSION = ''; /** * Version number of the internal composer-runtime-api package * * This is used to version features available to projects at runtime * like the platform-check file, the Composer\InstalledVersions class * and possibly others in the future. * * @var string */ const RUNTIME_API_VERSION = '2.2.2'; /** * @return string */ public static function getVersion() { // no replacement done, this must be a source checkout if (self::VERSION === '@package_version'.'@') { return self::SOURCE_VERSION; } // we have a branch alias and version is a commit id, this must be a snapshot build if (self::BRANCH_ALIAS_VERSION !== '' && Preg::isMatch('{^[a-f0-9]{40}$}', self::VERSION)) { return self::BRANCH_ALIAS_VERSION.'+'.self::VERSION; } return self::VERSION; } /** * @var RootPackageInterface */ private $package; /** * @var ?Locker */ private $locker = null; /** * @var Loop */ private $loop; /** * @var Repository\RepositoryManager */ private $repositoryManager; /** * @var Downloader\DownloadManager */ private $downloadManager; /** * @var Installer\InstallationManager */ private $installationManager; /** * @var Plugin\PluginManager */ private $pluginManager; /** * @var Config */ private $config; /** * @var EventDispatcher */ private $eventDispatcher; /** * @var Autoload\AutoloadGenerator */ private $autoloadGenerator; /** * @var ArchiveManager */ private $archiveManager; /** * @return void */ public function setPackage(RootPackageInterface $package) { $this->package = $package; } /** * @return RootPackageInterface */ public function getPackage() { return $this->package; } /** * @return void */ public function setConfig(Config $config) { $this->config = $config; } /** * @return Config */ public function getConfig() { return $this->config; } /** * @return void */ public function setLocker(Locker $locker) { $this->locker = $locker; } /** * @return ?Locker */ public function getLocker() { return $this->locker; } /** * @return void */ public function setLoop(Loop $loop) { $this->loop = $loop; } /** * @return Loop */ public function getLoop() { return $this->loop; } /** * @return void */ public function setRepositoryManager(RepositoryManager $manager) { $this->repositoryManager = $manager; } /** * @return RepositoryManager */ public function getRepositoryManager() { return $this->repositoryManager; } /** * @return void */ public function setDownloadManager(DownloadManager $manager) { $this->downloadManager = $manager; } /** * @return DownloadManager */ public function getDownloadManager() { return $this->downloadManager; } /** * @return void */ public function setArchiveManager(ArchiveManager $manager) { $this->archiveManager = $manager; } /** * @return ArchiveManager */ public function getArchiveManager() { return $this->archiveManager; } /** * @return void */ public function setInstallationManager(InstallationManager $manager) { $this->installationManager = $manager; } /** * @return InstallationManager */ public function getInstallationManager() { return $this->installationManager; } /** * @return void */ public function setPluginManager(PluginManager $manager) { $this->pluginManager = $manager; } /** * @return PluginManager */ public function getPluginManager() { return $this->pluginManager; } /** * @return void */ public function setEventDispatcher(EventDispatcher $eventDispatcher) { $this->eventDispatcher = $eventDispatcher; } /** * @return EventDispatcher */ public function getEventDispatcher() { return $this->eventDispatcher; } /** * @return void */ public function setAutoloadGenerator(AutoloadGenerator $autoloadGenerator) { $this->autoloadGenerator = $autoloadGenerator; } /** * @return AutoloadGenerator */ public function getAutoloadGenerator() { return $this->autoloadGenerator; } } * Jordi Boggiano * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Composer\Json; use Exception; /** * @author Jordi Boggiano */ class JsonValidationException extends Exception { /** * @var string[] */ protected $errors; /** * @param string $message * @param string[] $errors */ public function __construct($message, $errors = array(), Exception $previous = null) { $this->errors = $errors; parent::__construct((string) $message, 0, $previous); } /** * @return string[] */ public function getErrors() { return $this->errors; } } * Jordi Boggiano * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Composer\Json; use Composer\Pcre\Preg; use Composer\Repository\PlatformRepository; /** * @author Jordi Boggiano */ class JsonManipulator { /** @var string */ private static $DEFINES = '(?(DEFINE) (? -? (?= [1-9]|0(?!\d) ) \d++ (\.\d++)? ([eE] [+-]?+ \d++)? ) (? true | false | null ) (? " ([^"\\\\]*+ | \\\\ ["\\\\bfnrt\/] | \\\\ u [0-9A-Fa-f]{4} )* " ) (? \[ (?: (?&json) \s*+ (?: , (?&json) \s*+ )*+ )?+ \s*+ \] ) (? \s*+ (?&string) \s*+ : (?&json) \s*+ ) (? \{ (?: (?&pair) (?: , (?&pair) )*+ )?+ \s*+ \} ) (? \s*+ (?: (?&number) | (?&boolean) | (?&string) | (?&array) | (?&object) ) ) )'; /** @var string */ private $contents; /** @var string */ private $newline; /** @var string */ private $indent; /** * @param string $contents */ public function __construct($contents) { $contents = trim($contents); if ($contents === '') { $contents = '{}'; } if (!Preg::isMatch('#^\{(.*)\}$#s', $contents)) { throw new \InvalidArgumentException('The json file must be an object ({})'); } $this->newline = false !== strpos($contents, "\r\n") ? "\r\n" : "\n"; $this->contents = $contents === '{}' ? '{' . $this->newline . '}' : $contents; $this->detectIndenting(); } /** * @return string */ public function getContents() { return $this->contents . $this->newline; } /** * @param string $type * @param string $package * @param string $constraint * @param bool $sortPackages * @return bool */ public function addLink($type, $package, $constraint, $sortPackages = false) { $decoded = JsonFile::parseJson($this->contents); // no link of that type yet if (!isset($decoded[$type])) { return $this->addMainKey($type, array($package => $constraint)); } $regex = '{'.self::$DEFINES.'^(?P\s*\{\s*(?:(?&string)\s*:\s*(?&json)\s*,\s*)*?)'. '(?P'.preg_quote(JsonFile::encode($type)).'\s*:\s*)(?P(?&json))(?P.*)}sx'; if (!Preg::isMatch($regex, $this->contents, $matches)) { return false; } $links = $matches['value']; // try to find existing link $packageRegex = str_replace('/', '\\\\?/', preg_quote($package)); $regex = '{'.self::$DEFINES.'"(?P'.$packageRegex.')"(\s*:\s*)(?&string)}ix'; if (Preg::isMatch($regex, $links, $packageMatches)) { // update existing link $existingPackage = $packageMatches['package']; $packageRegex = str_replace('/', '\\\\?/', preg_quote($existingPackage)); $links = Preg::replaceCallback('{'.self::$DEFINES.'"'.$packageRegex.'"(?P\s*:\s*)(?&string)}ix', function ($m) use ($existingPackage, $constraint) { return JsonFile::encode(str_replace('\\/', '/', $existingPackage)) . $m['separator'] . '"' . $constraint . '"'; }, $links); } else { if (Preg::isMatch('#^\s*\{\s*\S+.*?(\s*\}\s*)$#s', $links, $match)) { // link missing but non empty links $links = Preg::replace( '{'.preg_quote($match[1]).'$}', // addcslashes is used to double up backslashes/$ since preg_replace resolves them as back references otherwise, see #1588 addcslashes(',' . $this->newline . $this->indent . $this->indent . JsonFile::encode($package).': '.JsonFile::encode($constraint) . $match[1], '\\$'), $links ); } else { // links empty $links = '{' . $this->newline . $this->indent . $this->indent . JsonFile::encode($package).': '.JsonFile::encode($constraint) . $this->newline . $this->indent . '}'; } } if (true === $sortPackages) { $requirements = json_decode($links, true); $this->sortPackages($requirements); $links = $this->format($requirements); } $this->contents = $matches['start'] . $matches['property'] . $links . $matches['end']; return true; } /** * Sorts packages by importance (platform packages first, then PHP dependencies) and alphabetically. * * @link https://getcomposer.org/doc/02-libraries.md#platform-packages * * @param array $packages * @return void */ private function sortPackages(array &$packages = array()) { $prefix = function ($requirement) { if (PlatformRepository::isPlatformPackage($requirement)) { return Preg::replace( array( '/^php/', '/^hhvm/', '/^ext/', '/^lib/', '/^\D/', ), array( '0-$0', '1-$0', '2-$0', '3-$0', '4-$0', ), $requirement ); } return '5-'.$requirement; }; uksort($packages, function ($a, $b) use ($prefix) { return strnatcmp($prefix($a), $prefix($b)); }); } /** * @param string $name * @param array $config * @param bool $append * @return bool */ public function addRepository($name, $config, $append = true) { return $this->addSubNode('repositories', $name, $config, $append); } /** * @param string $name * @return bool */ public function removeRepository($name) { return $this->removeSubNode('repositories', $name); } /** * @param string $name * @param mixed $value * @return bool */ public function addConfigSetting($name, $value) { return $this->addSubNode('config', $name, $value); } /** * @param string $name * @return bool */ public function removeConfigSetting($name) { return $this->removeSubNode('config', $name); } /** * @param string $name * @param mixed $value * @return bool */ public function addProperty($name, $value) { if (strpos($name, 'suggest.') === 0) { return $this->addSubNode('suggest', substr($name, 8), $value); } if (strpos($name, 'extra.') === 0) { return $this->addSubNode('extra', substr($name, 6), $value); } if (strpos($name, 'scripts.') === 0) { return $this->addSubNode('scripts', substr($name, 8), $value); } return $this->addMainKey($name, $value); } /** * @param string $name * @return bool */ public function removeProperty($name) { if (strpos($name, 'suggest.') === 0) { return $this->removeSubNode('suggest', substr($name, 8)); } if (strpos($name, 'extra.') === 0) { return $this->removeSubNode('extra', substr($name, 6)); } if (strpos($name, 'scripts.') === 0) { return $this->removeSubNode('scripts', substr($name, 8)); } return $this->removeMainKey($name); } /** * @param string $mainNode * @param string $name * @param mixed $value * @param bool $append * @return bool */ public function addSubNode($mainNode, $name, $value, $append = true) { $decoded = JsonFile::parseJson($this->contents); $subName = null; if (in_array($mainNode, array('config', 'extra', 'scripts')) && false !== strpos($name, '.')) { list($name, $subName) = explode('.', $name, 2); } // no main node yet if (!isset($decoded[$mainNode])) { if ($subName !== null) { $this->addMainKey($mainNode, array($name => array($subName => $value))); } else { $this->addMainKey($mainNode, array($name => $value)); } return true; } // main node content not match-able $nodeRegex = '{'.self::$DEFINES.'^(?P \s* \{ \s* (?: (?&string) \s* : (?&json) \s* , \s* )*?'. preg_quote(JsonFile::encode($mainNode)).'\s*:\s*)(?P(?&object))(?P.*)}sx'; try { if (!Preg::isMatch($nodeRegex, $this->contents, $match)) { return false; } } catch (\RuntimeException $e) { if ($e->getCode() === PREG_BACKTRACK_LIMIT_ERROR) { return false; } throw $e; } $children = $match['content']; // invalid match due to un-regexable content, abort if (!@json_decode($children)) { return false; } $that = $this; // child exists $childRegex = '{'.self::$DEFINES.'(?P"'.preg_quote($name).'"\s*:\s*)(?P(?&json))(?P,?)}x'; if (Preg::isMatch($childRegex, $children, $matches)) { $children = Preg::replaceCallback($childRegex, function ($matches) use ($subName, $value, $that) { if ($subName !== null) { $curVal = json_decode($matches['content'], true); if (!is_array($curVal)) { $curVal = array(); } $curVal[$subName] = $value; $value = $curVal; } return $matches['start'] . $that->format($value, 1) . $matches['end']; }, $children); } else { Preg::match('#^{ (?P\s*?) (?P\S+.*?)? (?P\s*) }$#sx', $children, $match); $whitespace = ''; if (!empty($match['trailingspace'])) { $whitespace = $match['trailingspace']; } if (!empty($match['content'])) { if ($subName !== null) { $value = array($subName => $value); } // child missing but non empty children if ($append) { $children = Preg::replace( '#'.$whitespace.'}$#', addcslashes(',' . $this->newline . $this->indent . $this->indent . JsonFile::encode($name).': '.$this->format($value, 1) . $whitespace . '}', '\\$'), $children ); } else { $whitespace = ''; if (!empty($match['leadingspace'])) { $whitespace = $match['leadingspace']; } $children = Preg::replace( '#^{'.$whitespace.'#', addcslashes('{' . $whitespace . JsonFile::encode($name).': '.$this->format($value, 1) . ',' . $this->newline . $this->indent . $this->indent, '\\$'), $children ); } } else { if ($subName !== null) { $value = array($subName => $value); } // children present but empty $children = '{' . $this->newline . $this->indent . $this->indent . JsonFile::encode($name).': '.$this->format($value, 1) . $whitespace . '}'; } } $this->contents = Preg::replaceCallback($nodeRegex, function ($m) use ($children) { return $m['start'] . $children . $m['end']; }, $this->contents); return true; } /** * @param string $mainNode * @param string $name * @return bool */ public function removeSubNode($mainNode, $name) { $decoded = JsonFile::parseJson($this->contents); // no node or empty node if (empty($decoded[$mainNode])) { return true; } // no node content match-able $nodeRegex = '{'.self::$DEFINES.'^(?P \s* \{ \s* (?: (?&string) \s* : (?&json) \s* , \s* )*?'. preg_quote(JsonFile::encode($mainNode)).'\s*:\s*)(?P(?&object))(?P.*)}sx'; try { if (!Preg::isMatch($nodeRegex, $this->contents, $match)) { return false; } } catch (\RuntimeException $e) { if ($e->getCode() === PREG_BACKTRACK_LIMIT_ERROR) { return false; } throw $e; } $children = $match['content']; // invalid match due to un-regexable content, abort if (!@json_decode($children, true)) { return false; } $subName = null; if (in_array($mainNode, array('config', 'extra', 'scripts')) && false !== strpos($name, '.')) { list($name, $subName) = explode('.', $name, 2); } // no node to remove if (!isset($decoded[$mainNode][$name]) || ($subName && !isset($decoded[$mainNode][$name][$subName]))) { return true; } // try and find a match for the subkey $keyRegex = str_replace('/', '\\\\?/', preg_quote($name)); if (Preg::isMatch('{"'.$keyRegex.'"\s*:}i', $children)) { // find best match for the value of "name" if (Preg::isMatchAll('{'.self::$DEFINES.'"'.$keyRegex.'"\s*:\s*(?:(?&json))}x', $children, $matches)) { $bestMatch = ''; foreach ($matches[0] as $match) { if (strlen($bestMatch) < strlen($match)) { $bestMatch = $match; } } $childrenClean = Preg::replace('{,\s*'.preg_quote($bestMatch).'}i', '', $children, -1, $count); if (1 !== $count) { $childrenClean = Preg::replace('{'.preg_quote($bestMatch).'\s*,?\s*}i', '', $childrenClean, -1, $count); if (1 !== $count) { return false; } } } } else { $childrenClean = $children; } if (!isset($childrenClean)) { throw new \InvalidArgumentException("JsonManipulator: \$childrenClean is not defined. Please report at https://github.com/composer/composer/issues/new."); } // no child data left, $name was the only key in Preg::match('#^{ \s*? (?P\S+.*?)? (?P\s*) }$#sx', $childrenClean, $match); if (empty($match['content'])) { $newline = $this->newline; $indent = $this->indent; $this->contents = Preg::replaceCallback($nodeRegex, function ($matches) use ($indent, $newline) { return $matches['start'] . '{' . $newline . $indent . '}' . $matches['end']; }, $this->contents); // we have a subname, so we restore the rest of $name if ($subName !== null) { $curVal = json_decode($children, true); unset($curVal[$name][$subName]); $this->addSubNode($mainNode, $name, $curVal[$name]); } return true; } $that = $this; $this->contents = Preg::replaceCallback($nodeRegex, function ($matches) use ($that, $name, $subName, $childrenClean) { if ($subName !== null) { $curVal = json_decode($matches['content'], true); unset($curVal[$name][$subName]); $childrenClean = $that->format($curVal); } return $matches['start'] . $childrenClean . $matches['end']; }, $this->contents); return true; } /** * @param string $key * @param mixed $content * @return bool */ public function addMainKey($key, $content) { $decoded = JsonFile::parseJson($this->contents); $content = $this->format($content); // key exists already $regex = '{'.self::$DEFINES.'^(?P\s*\{\s*(?:(?&string)\s*:\s*(?&json)\s*,\s*)*?)'. '(?P'.preg_quote(JsonFile::encode($key)).'\s*:\s*(?&json))(?P.*)}sx'; if (isset($decoded[$key]) && Preg::isMatch($regex, $this->contents, $matches)) { // invalid match due to un-regexable content, abort if (!@json_decode('{'.$matches['key'].'}')) { return false; } $this->contents = $matches['start'] . JsonFile::encode($key).': '.$content . $matches['end']; return true; } // append at the end of the file and keep whitespace if (Preg::isMatch('#[^{\s](\s*)\}$#', $this->contents, $match)) { $this->contents = Preg::replace( '#'.$match[1].'\}$#', addcslashes(',' . $this->newline . $this->indent . JsonFile::encode($key). ': '. $content . $this->newline . '}', '\\$'), $this->contents ); return true; } // append at the end of the file $this->contents = Preg::replace( '#\}$#', addcslashes($this->indent . JsonFile::encode($key). ': '.$content . $this->newline . '}', '\\$'), $this->contents ); return true; } /** * @param string $key * @return bool */ public function removeMainKey($key) { $decoded = JsonFile::parseJson($this->contents); if (!array_key_exists($key, $decoded)) { return true; } // key exists already $regex = '{'.self::$DEFINES.'^(?P\s*\{\s*(?:(?&string)\s*:\s*(?&json)\s*,\s*)*?)'. '(?P'.preg_quote(JsonFile::encode($key)).'\s*:\s*(?&json))\s*,?\s*(?P.*)}sx'; if (Preg::isMatch($regex, $this->contents, $matches)) { // invalid match due to un-regexable content, abort if (!@json_decode('{'.$matches['removal'].'}')) { return false; } // check that we are not leaving a dangling comma on the previous line if the last line was removed if (Preg::isMatch('#,\s*$#', $matches['start']) && Preg::isMatch('#^\}$#', $matches['end'])) { $matches['start'] = rtrim(Preg::replace('#,(\s*)$#', '$1', $matches['start']), $this->indent); } $this->contents = $matches['start'] . $matches['end']; if (Preg::isMatch('#^\{\s*\}\s*$#', $this->contents)) { $this->contents = "{\n}"; } return true; } return false; } /** * @param string $key * @return bool */ public function removeMainKeyIfEmpty($key) { $decoded = JsonFile::parseJson($this->contents); if (!array_key_exists($key, $decoded)) { return true; } if (is_array($decoded[$key]) && count($decoded[$key]) === 0) { return $this->removeMainKey($key); } return true; } /** * @param mixed $data * @param int $depth * @return string */ public function format($data, $depth = 0) { if (is_array($data)) { reset($data); if (is_numeric(key($data))) { foreach ($data as $key => $val) { $data[$key] = $this->format($val, $depth + 1); } return '['.implode(', ', $data).']'; } $out = '{' . $this->newline; $elems = array(); foreach ($data as $key => $val) { $elems[] = str_repeat($this->indent, $depth + 2) . JsonFile::encode($key). ': '.$this->format($val, $depth + 1); } return $out . implode(','.$this->newline, $elems) . $this->newline . str_repeat($this->indent, $depth + 1) . '}'; } return JsonFile::encode($data); } /** * @return void */ protected function detectIndenting() { if (Preg::isMatch('{^([ \t]+)"}m', $this->contents, $match)) { $this->indent = $match[1]; } else { $this->indent = ' '; } } } * Jordi Boggiano * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Composer\Json; use Composer\Pcre\Preg; /** * Formats json strings used for php < 5.4 because the json_encode doesn't * supports the flags JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE * in these versions * * @author Konstantin Kudryashiv * @author Jordi Boggiano */ class JsonFormatter { /** * This code is based on the function found at: * http://recursive-design.com/blog/2008/03/11/format-json-with-php/ * * Originally licensed under MIT by Dave Perrett * * * @param string $json * @param bool $unescapeUnicode Un escape unicode * @param bool $unescapeSlashes Un escape slashes * @return string */ public static function format($json, $unescapeUnicode, $unescapeSlashes) { $result = ''; $pos = 0; $strLen = strlen($json); $indentStr = ' '; $newLine = "\n"; $outOfQuotes = true; $buffer = ''; $noescape = true; for ($i = 0; $i < $strLen; $i++) { // Grab the next character in the string $char = substr($json, $i, 1); // Are we inside a quoted string? if ('"' === $char && $noescape) { $outOfQuotes = !$outOfQuotes; } if (!$outOfQuotes) { $buffer .= $char; $noescape = '\\' === $char ? !$noescape : true; continue; } if ('' !== $buffer) { if ($unescapeSlashes) { $buffer = str_replace('\\/', '/', $buffer); } if ($unescapeUnicode && function_exists('mb_convert_encoding')) { // https://stackoverflow.com/questions/2934563/how-to-decode-unicode-escape-sequences-like-u00ed-to-proper-utf-8-encoded-cha $buffer = Preg::replaceCallback('/(\\\\+)u([0-9a-f]{4})/i', function ($match) { $l = strlen($match[1]); if ($l % 2) { $code = hexdec($match[2]); // 0xD800..0xDFFF denotes UTF-16 surrogate pair which won't be unescaped // see https://github.com/composer/composer/issues/7510 if (0xD800 <= $code && 0xDFFF >= $code) { return $match[0]; } return str_repeat('\\', $l - 1) . mb_convert_encoding( pack('H*', $match[2]), 'UTF-8', 'UCS-2BE' ); } return $match[0]; }, $buffer); } $result .= $buffer.$char; $buffer = ''; continue; } if (':' === $char) { // Add a space after the : character $char .= ' '; } elseif ('}' === $char || ']' === $char) { $pos--; $prevChar = substr($json, $i - 1, 1); if ('{' !== $prevChar && '[' !== $prevChar) { // If this character is the end of an element, // output a new line and indent the next line $result .= $newLine; for ($j = 0; $j < $pos; $j++) { $result .= $indentStr; } } else { // Collapse empty {} and [] $result = rtrim($result); } } $result .= $char; // If the last character was the beginning of an element, // output a new line and indent the next line if (',' === $char || '{' === $char || '[' === $char) { $result .= $newLine; if ('{' === $char || '[' === $char) { $pos++; } for ($j = 0; $j < $pos; $j++) { $result .= $indentStr; } } } return $result; } } * Jordi Boggiano * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Composer\Json; use Composer\Pcre\Preg; use JsonSchema\Validator; use Seld\JsonLint\JsonParser; use Seld\JsonLint\ParsingException; use Composer\Util\HttpDownloader; use Composer\IO\IOInterface; use Composer\Downloader\TransportException; /** * Reads/writes json files. * * @author Konstantin Kudryashiv * @author Jordi Boggiano */ class JsonFile { const LAX_SCHEMA = 1; const STRICT_SCHEMA = 2; const JSON_UNESCAPED_SLASHES = 64; const JSON_PRETTY_PRINT = 128; const JSON_UNESCAPED_UNICODE = 256; const COMPOSER_SCHEMA_PATH = '/../../../res/composer-schema.json'; /** @var string */ private $path; /** @var ?HttpDownloader */ private $httpDownloader; /** @var ?IOInterface */ private $io; /** * Initializes json file reader/parser. * * @param string $path path to a lockfile * @param ?HttpDownloader $httpDownloader required for loading http/https json files * @param ?IOInterface $io * @throws \InvalidArgumentException */ public function __construct($path, HttpDownloader $httpDownloader = null, IOInterface $io = null) { $this->path = $path; if (null === $httpDownloader && Preg::isMatch('{^https?://}i', $path)) { throw new \InvalidArgumentException('http urls require a HttpDownloader instance to be passed'); } $this->httpDownloader = $httpDownloader; $this->io = $io; } /** * @return string */ public function getPath() { return $this->path; } /** * Checks whether json file exists. * * @return bool */ public function exists() { return is_file($this->path); } /** * Reads json file. * * @throws ParsingException * @throws \RuntimeException * @return mixed */ public function read() { try { if ($this->httpDownloader) { $json = $this->httpDownloader->get($this->path)->getBody(); } else { if ($this->io && $this->io->isDebug()) { $realpathInfo = ''; $realpath = realpath($this->path); if (false !== $realpath && $realpath !== $this->path) { $realpathInfo = ' (' . $realpath . ')'; } $this->io->writeError('Reading ' . $this->path . $realpathInfo); } $json = file_get_contents($this->path); } } catch (TransportException $e) { throw new \RuntimeException($e->getMessage(), 0, $e); } catch (\Exception $e) { throw new \RuntimeException('Could not read '.$this->path."\n\n".$e->getMessage()); } return static::parseJson($json, $this->path); } /** * Writes json file. * * @param mixed[] $hash writes hash into json file * @param int $options json_encode options (defaults to JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE) * @throws \UnexpectedValueException|\Exception * @return void */ public function write(array $hash, $options = 448) { if ($this->path === 'php://memory') { file_put_contents($this->path, static::encode($hash, $options)); return; } $dir = dirname($this->path); if (!is_dir($dir)) { if (file_exists($dir)) { throw new \UnexpectedValueException( realpath($dir).' exists and is not a directory.' ); } if (!@mkdir($dir, 0777, true)) { throw new \UnexpectedValueException( $dir.' does not exist and could not be created.' ); } } $retries = 3; while ($retries--) { try { $this->filePutContentsIfModified($this->path, static::encode($hash, $options). ($options & self::JSON_PRETTY_PRINT ? "\n" : '')); break; } catch (\Exception $e) { if ($retries > 0) { usleep(500000); continue; } throw $e; } } } /** * Modify file properties only if content modified * * @param string $path * @param string $content * @return int|false */ private function filePutContentsIfModified($path, $content) { $currentContent = @file_get_contents($path); if (!$currentContent || ($currentContent != $content)) { return file_put_contents($path, $content); } return 0; } /** * Validates the schema of the current json file according to composer-schema.json rules * * @param int $schema a JsonFile::*_SCHEMA constant * @param string|null $schemaFile a path to the schema file * @throws JsonValidationException * @throws ParsingException * @return bool true on success */ public function validateSchema($schema = self::STRICT_SCHEMA, $schemaFile = null) { $content = file_get_contents($this->path); $data = json_decode($content); if (null === $data && 'null' !== $content) { self::validateSyntax($content, $this->path); } $isComposerSchemaFile = false; if (null === $schemaFile) { $isComposerSchemaFile = true; $schemaFile = __DIR__ . self::COMPOSER_SCHEMA_PATH; } // Prepend with file:// only when not using a special schema already (e.g. in the phar) if (false === strpos($schemaFile, '://')) { $schemaFile = 'file://' . $schemaFile; } $schemaData = (object) array('$ref' => $schemaFile); if ($schema === self::LAX_SCHEMA) { $schemaData->additionalProperties = true; $schemaData->required = array(); } elseif ($schema === self::STRICT_SCHEMA && $isComposerSchemaFile) { $schemaData->additionalProperties = false; $schemaData->required = array('name', 'description'); } $validator = new Validator(); $validator->check($data, $schemaData); if (!$validator->isValid()) { $errors = array(); foreach ((array) $validator->getErrors() as $error) { $errors[] = ($error['property'] ? $error['property'].' : ' : '').$error['message']; } throw new JsonValidationException('"'.$this->path.'" does not match the expected JSON schema', $errors); } return true; } /** * Encodes an array into (optionally pretty-printed) JSON * * @param mixed $data Data to encode into a formatted JSON string * @param int $options json_encode options (defaults to JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE) * @return string Encoded json */ public static function encode($data, $options = 448) { if (PHP_VERSION_ID >= 50400) { $json = json_encode($data, $options); if (false === $json) { self::throwEncodeError(json_last_error()); } // compact brackets to follow recent php versions if (PHP_VERSION_ID < 50428 || (PHP_VERSION_ID >= 50500 && PHP_VERSION_ID < 50512) || (defined('JSON_C_VERSION') && version_compare(phpversion('json'), '1.3.6', '<'))) { $json = Preg::replace('/\[\s+\]/', '[]', $json); $json = Preg::replace('/\{\s+\}/', '{}', $json); } return $json; } $json = json_encode($data); if (false === $json) { self::throwEncodeError(json_last_error()); } $prettyPrint = (bool) ($options & self::JSON_PRETTY_PRINT); $unescapeUnicode = (bool) ($options & self::JSON_UNESCAPED_UNICODE); $unescapeSlashes = (bool) ($options & self::JSON_UNESCAPED_SLASHES); if (!$prettyPrint && !$unescapeUnicode && !$unescapeSlashes) { return $json; } return JsonFormatter::format($json, $unescapeUnicode, $unescapeSlashes); } /** * Throws an exception according to a given code with a customized message * * @param int $code return code of json_last_error function * @throws \RuntimeException * @return void */ private static function throwEncodeError($code) { switch ($code) { case JSON_ERROR_DEPTH: $msg = 'Maximum stack depth exceeded'; break; case JSON_ERROR_STATE_MISMATCH: $msg = 'Underflow or the modes mismatch'; break; case JSON_ERROR_CTRL_CHAR: $msg = 'Unexpected control character found'; break; case JSON_ERROR_UTF8: $msg = 'Malformed UTF-8 characters, possibly incorrectly encoded'; break; default: $msg = 'Unknown error'; } throw new \RuntimeException('JSON encoding failed: '.$msg); } /** * Parses json string and returns hash. * * @param ?string $json json string * @param string $file the json file * * @throws ParsingException * @return mixed */ public static function parseJson($json, $file = null) { if (null === $json) { return null; } $data = json_decode($json, true); if (null === $data && JSON_ERROR_NONE !== json_last_error()) { self::validateSyntax($json, $file); } return $data; } /** * Validates the syntax of a JSON string * * @param string $json * @param string $file * @throws \UnexpectedValueException * @throws ParsingException * @return bool true on success */ protected static function validateSyntax($json, $file = null) { $parser = new JsonParser(); $result = $parser->lint($json); if (null === $result) { if (defined('JSON_ERROR_UTF8') && JSON_ERROR_UTF8 === json_last_error()) { throw new \UnexpectedValueException('"'.$file.'" is not UTF-8, could not parse as JSON'); } return true; } throw new ParsingException('"'.$file.'" does not contain valid JSON'."\n".$result->getMessage(), $result->getDetails()); } } * Jordi Boggiano * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Composer\Platform; use Composer\Util\Platform; use Composer\Util\ProcessExecutor; use Symfony\Component\Process\ExecutableFinder; class HhvmDetector { /** @var string|false|null */ private static $hhvmVersion = null; /** @var ?ExecutableFinder */ private $executableFinder; /** @var ?ProcessExecutor */ private $processExecutor; public function __construct(ExecutableFinder $executableFinder = null, ProcessExecutor $processExecutor = null) { $this->executableFinder = $executableFinder; $this->processExecutor = $processExecutor; } /** * @return void */ public function reset() { self::$hhvmVersion = null; } /** * @return string|null */ public function getVersion() { if (null !== self::$hhvmVersion) { return self::$hhvmVersion ?: null; } self::$hhvmVersion = defined('HHVM_VERSION') ? HHVM_VERSION : null; if (self::$hhvmVersion === null && !Platform::isWindows()) { self::$hhvmVersion = false; $this->executableFinder = $this->executableFinder ?: new ExecutableFinder(); $hhvmPath = $this->executableFinder->find('hhvm'); if ($hhvmPath !== null) { $this->processExecutor = $this->processExecutor ?: new ProcessExecutor(); $exitCode = $this->processExecutor->execute( ProcessExecutor::escape($hhvmPath). ' --php -d hhvm.jit=0 -r "echo HHVM_VERSION;" 2>/dev/null', self::$hhvmVersion ); if ($exitCode !== 0) { self::$hhvmVersion = false; } } } return self::$hhvmVersion ?: null; } } * Jordi Boggiano * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Composer\Platform; use Composer\Pcre\Preg; /** * @author Lars Strojny */ class Version { /** * @param string $opensslVersion * @param bool $isFips * @return string|null */ public static function parseOpenssl($opensslVersion, &$isFips) { $isFips = false; if (!Preg::isMatch('/^(?[0-9.]+)(?[a-z]{0,2})?(?(?:-?(?:dev|pre|alpha|beta|rc|fips)[\d]*)*)?(?-\w+)?$/', $opensslVersion, $matches)) { return null; } $isFips = strpos($matches['suffix'], 'fips') !== false; $suffix = strtr('-'.ltrim($matches['suffix'], '-'), array('-fips' => '', '-pre' => '-alpha')); $patch = self::convertAlphaVersionToIntVersion($matches['patch']); return rtrim($matches['version'].'.'.$patch.$suffix, '-'); } /** * @param string $libjpegVersion * @return string|null */ public static function parseLibjpeg($libjpegVersion) { if (!Preg::isMatch('/^(?\d+)(?[a-z]*)$/', $libjpegVersion, $matches)) { return null; } return $matches['major'].'.'.self::convertAlphaVersionToIntVersion($matches['minor']); } /** * @param string $zoneinfoVersion * @return string|null */ public static function parseZoneinfoVersion($zoneinfoVersion) { if (!Preg::isMatch('/^(?\d{4})(?[a-z]*)$/', $zoneinfoVersion, $matches)) { return null; } return $matches['year'].'.'.self::convertAlphaVersionToIntVersion($matches['revision']); } /** * "" => 0, "a" => 1, "zg" => 33 * * @param string $alpha * @return int */ private static function convertAlphaVersionToIntVersion($alpha) { return strlen($alpha) * (-ord('a') + 1) + array_sum(array_map('ord', str_split($alpha))); } /** * @param int $versionId * @return string */ public static function convertLibxpmVersionId($versionId) { return self::convertVersionId($versionId, 100); } /** * @param int $versionId * @return string */ public static function convertOpenldapVersionId($versionId) { return self::convertVersionId($versionId, 100); } /** * @param int $versionId * @param int $base * * @return string */ private static function convertVersionId($versionId, $base) { return sprintf( '%d.%d.%d', $versionId / ($base * $base), (int) ($versionId / $base) % $base, $versionId % $base ); } } * Jordi Boggiano * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Composer\Platform; class Runtime { /** * @param string $constant * @param class-string $class * * @return bool */ public function hasConstant($constant, $class = null) { return defined(ltrim($class.'::'.$constant, ':')); } /** * @param string $constant * @param class-string $class * * @return mixed */ public function getConstant($constant, $class = null) { return constant(ltrim($class.'::'.$constant, ':')); } /** * @param string $fn * * @return bool */ public function hasFunction($fn) { return function_exists($fn); } /** * @param callable $callable * @param mixed[] $arguments * * @return mixed */ public function invoke($callable, array $arguments = array()) { return call_user_func_array($callable, $arguments); } /** * @param class-string $class * * @return bool */ public function hasClass($class) { return class_exists($class, false); } /** * @param class-string $class * @param mixed[] $arguments * * @return object * @throws \ReflectionException */ public function construct($class, array $arguments = array()) { if (empty($arguments)) { return new $class; } $refl = new \ReflectionClass($class); return $refl->newInstanceArgs($arguments); } /** @return string[] */ public function getExtensions() { return get_loaded_extensions(); } /** * @param string $extension * * @return string */ public function getExtensionVersion($extension) { return phpversion($extension); } /** * @param string $extension * * @return string * @throws \ReflectionException */ public function getExtensionInfo($extension) { $reflector = new \ReflectionExtension($extension); ob_start(); $reflector->info(); return ob_get_clean(); } } * Jordi Boggiano * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ /** * @param string $file * @return ?\Composer\Autoload\ClassLoader */ function includeIfExists($file) { return file_exists($file) ? include $file : null; } if ((!$loader = includeIfExists(__DIR__.'/../vendor/autoload.php')) && (!$loader = includeIfExists(__DIR__.'/../../../autoload.php'))) { echo 'You must set up the project dependencies using `composer install`'.PHP_EOL. 'See https://getcomposer.org/download/ for instructions on installing Composer'.PHP_EOL; exit(1); } return $loader; = 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\ComposerStaticInit04114110af87f5624b3580eba43b8770::getInitializer($loader)); } else { $map = require __DIR__ . '/autoload_namespaces.php'; foreach ($map as $namespace => $path) { $loader->set($namespace, $path); } $map = require __DIR__ . '/autoload_psr4.php'; foreach ($map as $namespace => $path) { $loader->setPsr4($namespace, $path); } $classMap = require __DIR__ . '/autoload_classmap.php'; if ($classMap) { $loader->addClassMap($classMap); } } $loader->register(true); if ($useStaticLoader) { $includeFiles = Composer\Autoload\ComposerStaticInit04114110af87f5624b3580eba43b8770::$files; } else { $includeFiles = require __DIR__ . '/autoload_files.php'; } foreach ($includeFiles as $fileIdentifier => $file) { composerRequire04114110af87f5624b3580eba43b8770($fileIdentifier, $file); } return $loader; } } /** * @param string $fileIdentifier * @param string $file * @return void */ function composerRequire04114110af87f5624b3580eba43b8770($fileIdentifier, $file) { if (empty($GLOBALS['__composer_autoload_files'][$fileIdentifier])) { $GLOBALS['__composer_autoload_files'][$fileIdentifier] = true; require $file; } } $vendorDir . '/composer/InstalledVersions.php', 'WP_CLI' => $vendorDir . '/wp-cli/wp-cli/php/class-wp-cli.php', 'WP_CLI_Command' => $vendorDir . '/wp-cli/wp-cli/php/class-wp-cli-command.php', ); * * For the full copyright and license information, please view * the LICENSE file that was distributed with this source code. */ namespace Composer\Spdx; class SpdxLicenses { /** @var string */ const LICENSES_FILE = 'spdx-licenses.json'; /** @var string */ const EXCEPTIONS_FILE = 'spdx-exceptions.json'; /** * Contains all the licenses. * * The array is indexed by license identifiers, which contain * a numerically indexed array with license details. * * [ lowercased license identifier => * [ 0 => identifier (string), 1 => full name (string), 2 => osi certified (bool), 3 => deprecated (bool) ] * , ... * ] * * @var array */ private $licenses; /** * @var string */ private $licensesExpression; /** * Contains all the license exceptions. * * The array is indexed by license exception identifiers, which contain * a numerically indexed array with license exception details. * * [ lowercased exception identifier => * [ 0 => exception identifier (string), 1 => full name (string) ] * , ... * ] * * @var array */ private $exceptions; /** * @var string */ private $exceptionsExpression; public function __construct() { $this->loadLicenses(); $this->loadExceptions(); } /** * Returns license metadata by license identifier. * * This function adds a link to the full license text to the license metadata. * The array returned is in the form of: * * [ 0 => full name (string), 1 => osi certified, 2 => link to license text (string), 3 => deprecation status (bool) ] * * @param string $identifier * * @return array{0: string, 1: bool, 2: string, 3: bool}|null */ public function getLicenseByIdentifier($identifier) { $key = strtolower($identifier); if (!isset($this->licenses[$key])) { return null; } list($identifier, $name, $isOsiApproved, $isDeprecatedLicenseId) = $this->licenses[$key]; return array( $name, $isOsiApproved, 'https://spdx.org/licenses/' . $identifier . '.html#licenseText', $isDeprecatedLicenseId, ); } /** * Returns all licenses information, keyed by the lowercased license identifier. * * @return array{0: string, 1: string, 2: bool, 3: bool}[] Each item is [ 0 => identifier (string), 1 => full name (string), 2 => osi certified (bool), 3 => deprecated (bool) ] */ public function getLicenses() { return $this->licenses; } /** * Returns license exception metadata by license exception identifier. * * This function adds a link to the full license exception text to the license exception metadata. * The array returned is in the form of: * * [ 0 => full name (string), 1 => link to license text (string) ] * * @param string $identifier * * @return array{0: string, 1: string}|null */ public function getExceptionByIdentifier($identifier) { $key = strtolower($identifier); if (!isset($this->exceptions[$key])) { return null; } list($identifier, $name) = $this->exceptions[$key]; return array( $name, 'https://spdx.org/licenses/' . $identifier . '.html#licenseExceptionText', ); } /** * Returns the short identifier of a license (or license exception) by full name. * * @param string $name * * @return string|null */ public function getIdentifierByName($name) { foreach ($this->licenses as $licenseData) { if ($licenseData[1] === $name) { return $licenseData[0]; } } foreach ($this->exceptions as $licenseData) { if ($licenseData[1] === $name) { return $licenseData[0]; } } return null; } /** * Returns the OSI Approved status for a license by identifier. * * @param string $identifier * * @return bool */ public function isOsiApprovedByIdentifier($identifier) { return $this->licenses[strtolower($identifier)][2]; } /** * Returns the deprecation status for a license by identifier. * * @param string $identifier * * @return bool */ public function isDeprecatedByIdentifier($identifier) { return $this->licenses[strtolower($identifier)][3]; } /** * @param string[]|string $license * * @throws \InvalidArgumentException * * @return bool */ public function validate($license) { if (is_array($license)) { $count = count($license); if ($count !== count(array_filter($license, 'is_string'))) { throw new \InvalidArgumentException('Array of strings expected.'); } $license = $count > 1 ? '(' . implode(' OR ', $license) . ')' : (string) reset($license); } if (!is_string($license)) { throw new \InvalidArgumentException(sprintf( 'Array or String expected, %s given.', gettype($license) )); } return $this->isValidLicenseString($license); } /** * @return string */ public static function getResourcesDir() { return dirname(__DIR__) . '/res'; } /** * @return void */ private function loadLicenses() { if (null !== $this->licenses) { return; } $json = file_get_contents(self::getResourcesDir() . '/' . self::LICENSES_FILE); if (false === $json) { throw new \RuntimeException('Missing license file in ' . self::getResourcesDir() . '/' . self::LICENSES_FILE); } $this->licenses = array(); foreach (json_decode($json, true) as $identifier => $license) { $this->licenses[strtolower($identifier)] = array($identifier, $license[0], $license[1], $license[2]); } } /** * @return void */ private function loadExceptions() { if (null !== $this->exceptions) { return; } $json = file_get_contents(self::getResourcesDir() . '/' . self::EXCEPTIONS_FILE); if (false === $json) { throw new \RuntimeException('Missing exceptions file in ' . self::getResourcesDir() . '/' . self::EXCEPTIONS_FILE); } $this->exceptions = array(); foreach (json_decode($json, true) as $identifier => $exception) { $this->exceptions[strtolower($identifier)] = array($identifier, $exception[0]); } } /** * @return string */ private function getLicensesExpression() { if (null === $this->licensesExpression) { $licenses = array_map('preg_quote', array_keys($this->licenses)); rsort($licenses); $licenses = implode('|', $licenses); $this->licensesExpression = $licenses; } return $this->licensesExpression; } /** * @return string */ private function getExceptionsExpression() { if (null === $this->exceptionsExpression) { $exceptions = array_map('preg_quote', array_keys($this->exceptions)); rsort($exceptions); $exceptions = implode('|', $exceptions); $this->exceptionsExpression = $exceptions; } return $this->exceptionsExpression; } /** * @param string $license * * @throws \RuntimeException * * @return bool */ private function isValidLicenseString($license) { if (isset($this->licenses[strtolower($license)])) { return true; } $licenses = $this->getLicensesExpression(); $exceptions = $this->getExceptionsExpression(); $regex = <<[\pL\pN.-]{1,}) # license-id: taken from list (?${licenses}) # license-exception-id: taken from list (?${exceptions}) # license-ref: [DocumentRef-1*(idstring):]LicenseRef-1*(idstring) (?(?:DocumentRef-(?&idstring):)?LicenseRef-(?&idstring)) # simple-expresssion: license-id / license-id+ / license-ref (?(?&licenseid)\+? | (?&licenseid) | (?&licenseref)) # compound-expression: 1*( # simple-expression / # simple-expression WITH license-exception-id / # compound-expression AND compound-expression / # compound-expression OR compound-expression # ) / ( compound-expression ) ) (? (?&simple_expression) ( \s+ WITH \s+ (?&licenseexceptionid))? | \( \s* (?&compound_expression) \s* \) ) (? (?&compound_head) (?: \s+ (?:AND|OR) \s+ (?&compound_expression))? ) # license-expression: 1*1(simple-expression / compound-expression) (?(?&compound_expression) | (?&simple_expression)) ) # end of define ^(NONE | NOASSERTION | (?&license_expression))$ }xi REGEX; $match = preg_match($regex, $license); if (0 === $match) { return false; } if (false === $match) { throw new \RuntimeException('Regex failed to compile/run.'); } return true; } } * * For the full copyright and license information, please view * the LICENSE file that was distributed with this source code. */ namespace Composer\CaBundle; use Psr\Log\LoggerInterface; use Symfony\Component\Process\PhpProcess; /** * @author Chris Smith * @author Jordi Boggiano */ class CaBundle { /** @var string|null */ private static $caPath; /** @var array */ private static $caFileValidity = array(); /** @var bool|null */ private static $useOpensslParse; /** * Returns the system CA bundle path, or a path to the bundled one * * This method was adapted from Sslurp. * https://github.com/EvanDotPro/Sslurp * * (c) Evan Coury * * For the full copyright and license information, please see below: * * Copyright (c) 2013, Evan Coury * All rights reserved. * * Redistribution and use in source and binary forms, with or without modification, * are permitted provided that the following conditions are met: * * * Redistributions of source code must retain the above copyright notice, * this list of conditions and the following disclaimer. * * * Redistributions in binary form must reproduce the above copyright notice, * this list of conditions and the following disclaimer in the documentation * and/or other materials provided with the distribution. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. * * @param LoggerInterface $logger optional logger for information about which CA files were loaded * @return string path to a CA bundle file or directory */ public static function getSystemCaRootBundlePath(LoggerInterface $logger = null) { if (self::$caPath !== null) { return self::$caPath; } $caBundlePaths = array(); // If SSL_CERT_FILE env variable points to a valid certificate/bundle, use that. // This mimics how OpenSSL uses the SSL_CERT_FILE env variable. $caBundlePaths[] = self::getEnvVariable('SSL_CERT_FILE'); // If SSL_CERT_DIR env variable points to a valid certificate/bundle, use that. // This mimics how OpenSSL uses the SSL_CERT_FILE env variable. $caBundlePaths[] = self::getEnvVariable('SSL_CERT_DIR'); $caBundlePaths[] = ini_get('openssl.cafile'); $caBundlePaths[] = ini_get('openssl.capath'); $otherLocations = array( '/etc/pki/tls/certs/ca-bundle.crt', // Fedora, RHEL, CentOS (ca-certificates package) '/etc/ssl/certs/ca-certificates.crt', // Debian, Ubuntu, Gentoo, Arch Linux (ca-certificates package) '/etc/ssl/ca-bundle.pem', // SUSE, openSUSE (ca-certificates package) '/usr/local/share/certs/ca-root-nss.crt', // FreeBSD (ca_root_nss_package) '/usr/ssl/certs/ca-bundle.crt', // Cygwin '/opt/local/share/curl/curl-ca-bundle.crt', // OS X macports, curl-ca-bundle package '/usr/local/share/curl/curl-ca-bundle.crt', // Default cURL CA bunde path (without --with-ca-bundle option) '/usr/share/ssl/certs/ca-bundle.crt', // Really old RedHat? '/etc/ssl/cert.pem', // OpenBSD '/usr/local/etc/ssl/cert.pem', // FreeBSD 10.x '/usr/local/etc/openssl/cert.pem', // OS X homebrew, openssl package '/usr/local/etc/openssl@1.1/cert.pem', // OS X homebrew, openssl@1.1 package ); foreach($otherLocations as $location) { $otherLocations[] = dirname($location); } $caBundlePaths = array_merge($caBundlePaths, $otherLocations); foreach ($caBundlePaths as $caBundle) { if ($caBundle && self::caFileUsable($caBundle, $logger)) { return self::$caPath = $caBundle; } if ($caBundle && self::caDirUsable($caBundle, $logger)) { return self::$caPath = $caBundle; } } return self::$caPath = static::getBundledCaBundlePath(); // Bundled CA file, last resort } /** * Returns the path to the bundled CA file * * In case you don't want to trust the user or the system, you can use this directly * * @return string path to a CA bundle file */ public static function getBundledCaBundlePath() { $caBundleFile = __DIR__.'/../res/cacert.pem'; // cURL does not understand 'phar://' paths // see https://github.com/composer/ca-bundle/issues/10 if (0 === strpos($caBundleFile, 'phar://')) { $tempCaBundleFile = tempnam(sys_get_temp_dir(), 'openssl-ca-bundle-'); if (false === $tempCaBundleFile) { throw new \RuntimeException('Could not create a temporary file to store the bundled CA file'); } file_put_contents( $tempCaBundleFile, file_get_contents($caBundleFile) ); register_shutdown_function(function() use ($tempCaBundleFile) { @unlink($tempCaBundleFile); }); $caBundleFile = $tempCaBundleFile; } return $caBundleFile; } /** * Validates a CA file using opensl_x509_parse only if it is safe to use * * @param string $filename * @param LoggerInterface $logger optional logger for information about which CA files were loaded * * @return bool */ public static function validateCaFile($filename, LoggerInterface $logger = null) { static $warned = false; if (isset(self::$caFileValidity[$filename])) { return self::$caFileValidity[$filename]; } $contents = file_get_contents($filename); // assume the CA is valid if php is vulnerable to // https://www.sektioneins.de/advisories/advisory-012013-php-openssl_x509_parse-memory-corruption-vulnerability.html if (!static::isOpensslParseSafe()) { if (!$warned && $logger) { $logger->warning(sprintf( 'Your version of PHP, %s, is affected by CVE-2013-6420 and cannot safely perform certificate validation, we strongly suggest you upgrade.', PHP_VERSION )); $warned = true; } $isValid = !empty($contents); } elseif (is_string($contents) && strlen($contents) > 0) { $contents = preg_replace("/^(\\-+(?:BEGIN|END))\\s+TRUSTED\\s+(CERTIFICATE\\-+)\$/m", '$1 $2', $contents); if (null === $contents) { // regex extraction failed $isValid = false; } else { $isValid = (bool) openssl_x509_parse($contents); } } else { $isValid = false; } if ($logger) { $logger->debug('Checked CA file '.realpath($filename).': '.($isValid ? 'valid' : 'invalid')); } return self::$caFileValidity[$filename] = $isValid; } /** * Test if it is safe to use the PHP function openssl_x509_parse(). * * This checks if OpenSSL extensions is vulnerable to remote code execution * via the exploit documented as CVE-2013-6420. * * @return bool */ public static function isOpensslParseSafe() { if (null !== self::$useOpensslParse) { return self::$useOpensslParse; } if (PHP_VERSION_ID >= 50600) { return self::$useOpensslParse = true; } // Vulnerable: // PHP 5.3.0 - PHP 5.3.27 // PHP 5.4.0 - PHP 5.4.22 // PHP 5.5.0 - PHP 5.5.6 if ( (PHP_VERSION_ID < 50400 && PHP_VERSION_ID >= 50328) || (PHP_VERSION_ID < 50500 && PHP_VERSION_ID >= 50423) || PHP_VERSION_ID >= 50507 ) { // This version of PHP has the fix for CVE-2013-6420 applied. return self::$useOpensslParse = true; } if (defined('PHP_WINDOWS_VERSION_BUILD')) { // Windows is probably insecure in this case. return self::$useOpensslParse = false; } $compareDistroVersionPrefix = function ($prefix, $fixedVersion) { $regex = '{^'.preg_quote($prefix).'([0-9]+)$}'; if (preg_match($regex, PHP_VERSION, $m)) { return ((int) $m[1]) >= $fixedVersion; } return false; }; // Hard coded list of PHP distributions with the fix backported. if ( $compareDistroVersionPrefix('5.3.3-7+squeeze', 18) // Debian 6 (Squeeze) || $compareDistroVersionPrefix('5.4.4-14+deb7u', 7) // Debian 7 (Wheezy) || $compareDistroVersionPrefix('5.3.10-1ubuntu3.', 9) // Ubuntu 12.04 (Precise) ) { return self::$useOpensslParse = true; } // Symfony Process component is missing so we assume it is unsafe at this point if (!class_exists('Symfony\Component\Process\PhpProcess')) { return self::$useOpensslParse = false; } // This is where things get crazy, because distros backport security // fixes the chances are on NIX systems the fix has been applied but // it's not possible to verify that from the PHP version. // // To verify exec a new PHP process and run the issue testcase with // known safe input that replicates the bug. // Based on testcase in https://github.com/php/php-src/commit/c1224573c773b6845e83505f717fbf820fc18415 // changes in https://github.com/php/php-src/commit/76a7fd893b7d6101300cc656058704a73254d593 $cert = 'LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUVwRENDQTR5Z0F3SUJBZ0lKQUp6dThyNnU2ZUJjTUEwR0NTcUdTSWIzRFFFQkJRVUFNSUhETVFzd0NRWUQKVlFRR0V3SkVSVEVjTUJvR0ExVUVDQXdUVG05eVpISm9aV2x1TFZkbGMzUm1ZV3hsYmpFUU1BNEdBMVVFQnd3SApTOE9Ed3Jac2JqRVVNQklHQTFVRUNnd0xVMlZyZEdsdmJrVnBibk14SHpBZEJnTlZCQXNNRmsxaGJHbGphVzkxCmN5QkRaWEowSUZObFkzUnBiMjR4SVRBZkJnTlZCQU1NR0cxaGJHbGphVzkxY3k1elpXdDBhVzl1WldsdWN5NWsKWlRFcU1DZ0dDU3FHU0liM0RRRUpBUlliYzNSbFptRnVMbVZ6YzJWeVFITmxhM1JwYjI1bGFXNXpMbVJsTUhVWQpaREU1TnpBd01UQXhNREF3TURBd1dnQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBCkFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUEKQUFBQUFBQVhEVEUwTVRFeU9ERXhNemt6TlZvd2djTXhDekFKQmdOVkJBWVRBa1JGTVJ3d0dnWURWUVFJREJOTwpiM0prY21obGFXNHRWMlZ6ZEdaaGJHVnVNUkF3RGdZRFZRUUhEQWRMdzRQQ3RteHVNUlF3RWdZRFZRUUtEQXRUClpXdDBhVzl1UldsdWN6RWZNQjBHQTFVRUN3d1dUV0ZzYVdOcGIzVnpJRU5sY25RZ1UyVmpkR2x2YmpFaE1COEcKQTFVRUF3d1liV0ZzYVdOcGIzVnpMbk5sYTNScGIyNWxhVzV6TG1SbE1Tb3dLQVlKS29aSWh2Y05BUWtCRmh0egpkR1ZtWVc0dVpYTnpaWEpBYzJWcmRHbHZibVZwYm5NdVpHVXdnZ0VpTUEwR0NTcUdTSWIzRFFFQkFRVUFBNElCCkR3QXdnZ0VLQW9JQkFRRERBZjNobDdKWTBYY0ZuaXlFSnBTU0RxbjBPcUJyNlFQNjV1c0pQUnQvOFBhRG9xQnUKd0VZVC9OYSs2ZnNnUGpDMHVLOURaZ1dnMnRIV1dvYW5TYmxBTW96NVBINlorUzRTSFJaN2UyZERJalBqZGhqaAowbUxnMlVNTzV5cDBWNzk3R2dzOWxOdDZKUmZIODFNTjJvYlhXczROdHp0TE11RDZlZ3FwcjhkRGJyMzRhT3M4CnBrZHVpNVVhd1Raa3N5NXBMUEhxNWNNaEZHbTA2djY1Q0xvMFYyUGQ5K0tBb2tQclBjTjVLTEtlYno3bUxwazYKU01lRVhPS1A0aWRFcXh5UTdPN2ZCdUhNZWRzUWh1K3ByWTNzaTNCVXlLZlF0UDVDWm5YMmJwMHdLSHhYMTJEWAoxbmZGSXQ5RGJHdkhUY3lPdU4rblpMUEJtM3ZXeG50eUlJdlZBZ01CQUFHalFqQkFNQWtHQTFVZEV3UUNNQUF3CkVRWUpZSVpJQVliNFFnRUJCQVFEQWdlQU1Bc0dBMVVkRHdRRUF3SUZvREFUQmdOVkhTVUVEREFLQmdnckJnRUYKQlFjREFqQU5CZ2txaGtpRzl3MEJBUVVGQUFPQ0FRRUFHMGZaWVlDVGJkajFYWWMrMVNub2FQUit2SThDOENhRAo4KzBVWWhkbnlVNGdnYTBCQWNEclk5ZTk0ZUVBdTZacXljRjZGakxxWFhkQWJvcHBXb2NyNlQ2R0QxeDMzQ2tsClZBcnpHL0t4UW9oR0QySmVxa2hJTWxEb214SE83a2EzOStPYThpMnZXTFZ5alU4QVp2V01BcnVIYTRFRU55RzcKbFcyQWFnYUZLRkNyOVRuWFRmcmR4R1ZFYnY3S1ZRNmJkaGc1cDVTanBXSDErTXEwM3VSM1pYUEJZZHlWODMxOQpvMGxWajFLRkkyRENML2xpV2lzSlJvb2YrMWNSMzVDdGQwd1lCY3BCNlRac2xNY09QbDc2ZHdLd0pnZUpvMlFnClpzZm1jMnZDMS9xT2xOdU5xLzBUenprVkd2OEVUVDNDZ2FVK1VYZTRYT1Z2a2NjZWJKbjJkZz09Ci0tLS0tRU5EIENFUlRJRklDQVRFLS0tLS0K'; $script = <<<'EOT' error_reporting(-1); $info = openssl_x509_parse(base64_decode('%s')); var_dump(PHP_VERSION, $info['issuer']['emailAddress'], $info['validFrom_time_t']); EOT; $script = '<'."?php\n".sprintf($script, $cert); try { $process = new PhpProcess($script); $process->mustRun(); } catch (\Exception $e) { // In the case of any exceptions just accept it is not possible to // determine the safety of openssl_x509_parse and bail out. return self::$useOpensslParse = false; } $output = preg_split('{\r?\n}', trim($process->getOutput())); $errorOutput = trim($process->getErrorOutput()); if ( is_array($output) && count($output) === 3 && $output[0] === sprintf('string(%d) "%s"', strlen(PHP_VERSION), PHP_VERSION) && $output[1] === 'string(27) "stefan.esser@sektioneins.de"' && $output[2] === 'int(-1)' && preg_match('{openssl_x509_parse\(\): illegal (?:ASN1 data type for|length in) timestamp in - on line \d+}', $errorOutput) ) { // This PHP has the fix backported probably by a distro security team. return self::$useOpensslParse = true; } return self::$useOpensslParse = false; } /** * Resets the static caches * @return void */ public static function reset() { self::$caFileValidity = array(); self::$caPath = null; self::$useOpensslParse = null; } /** * @param string $name * @return string|false */ private static function getEnvVariable($name) { if (isset($_SERVER[$name])) { return (string) $_SERVER[$name]; } if (PHP_SAPI === 'cli' && ($value = getenv($name)) !== false && $value !== null) { return (string) $value; } return false; } /** * @param string|false $certFile * @param LoggerInterface|null $logger * @return bool */ private static function caFileUsable($certFile, LoggerInterface $logger = null) { return $certFile && static::isFile($certFile, $logger) && static::isReadable($certFile, $logger) && static::validateCaFile($certFile, $logger); } /** * @param string|false $certDir * @param LoggerInterface|null $logger * @return bool */ private static function caDirUsable($certDir, LoggerInterface $logger = null) { return $certDir && static::isDir($certDir, $logger) && static::isReadable($certDir, $logger) && static::glob($certDir . '/*', $logger); } /** * @param string $certFile * @param LoggerInterface|null $logger * @return bool */ private static function isFile($certFile, LoggerInterface $logger = null) { $isFile = @is_file($certFile); if (!$isFile && $logger) { $logger->debug(sprintf('Checked CA file %s does not exist or it is not a file.', $certFile)); } return $isFile; } /** * @param string $certDir * @param LoggerInterface|null $logger * @return bool */ private static function isDir($certDir, LoggerInterface $logger = null) { $isDir = @is_dir($certDir); if (!$isDir && $logger) { $logger->debug(sprintf('Checked directory %s does not exist or it is not a directory.', $certDir)); } return $isDir; } /** * @param string $certFileOrDir * @param LoggerInterface|null $logger * @return bool */ private static function isReadable($certFileOrDir, LoggerInterface $logger = null) { $isReadable = @is_readable($certFileOrDir); if (!$isReadable && $logger) { $logger->debug(sprintf('Checked file or directory %s is not readable.', $certFileOrDir)); } return $isReadable; } /** * @param string $pattern * @param LoggerInterface|null $logger * @return bool */ private static function glob($pattern, LoggerInterface $logger = null) { $certs = glob($pattern); if ($certs === false) { if ($logger) { $logger->debug(sprintf("An error occurred while trying to find certificates for pattern: %s", $pattern)); } return false; } if (count($certs) === 0) { if ($logger) { $logger->debug(sprintf("No CA files found for pattern: %s", $pattern)); } return false; } return true; } } array( 'pretty_version' => 'dev-release-2-6-0', 'version' => 'dev-release-2-6-0', 'type' => 'library', 'install_path' => __DIR__ . '/../../', 'aliases' => array(), 'reference' => '19712f1a5e20612a75512d2f418cde455adc5125', 'name' => 'wp-cli/wp-cli-bundle', 'dev' => false, ), 'versions' => array( 'composer/ca-bundle' => array( 'pretty_version' => '1.3.1', 'version' => '1.3.1.0', 'type' => 'library', 'install_path' => __DIR__ . '/./ca-bundle', 'aliases' => array(), 'reference' => '4c679186f2aca4ab6a0f1b0b9cf9252decb44d0b', 'dev_requirement' => false, ), 'composer/composer' => array( 'pretty_version' => '2.2.5', 'version' => '2.2.5.0', 'type' => 'library', 'install_path' => __DIR__ . '/./composer', 'aliases' => array(), 'reference' => '22c41ef275c7bb64fa28fb2c0871a39666832cb9', 'dev_requirement' => false, ), 'composer/metadata-minifier' => array( 'pretty_version' => '1.0.0', 'version' => '1.0.0.0', 'type' => 'library', 'install_path' => __DIR__ . '/./metadata-minifier', 'aliases' => array(), 'reference' => 'c549d23829536f0d0e984aaabbf02af91f443207', 'dev_requirement' => false, ), 'composer/pcre' => array( 'pretty_version' => '1.0.1', 'version' => '1.0.1.0', 'type' => 'library', 'install_path' => __DIR__ . '/./pcre', 'aliases' => array(), 'reference' => '67a32d7d6f9f560b726ab25a061b38ff3a80c560', 'dev_requirement' => false, ), 'composer/semver' => array( 'pretty_version' => '3.2.7', 'version' => '3.2.7.0', 'type' => 'library', 'install_path' => __DIR__ . '/./semver', 'aliases' => array(), 'reference' => 'deac27056b57e46faf136fae7b449eeaa71661ee', 'dev_requirement' => false, ), 'composer/spdx-licenses' => array( 'pretty_version' => '1.5.6', 'version' => '1.5.6.0', 'type' => 'library', 'install_path' => __DIR__ . '/./spdx-licenses', 'aliases' => array(), 'reference' => 'a30d487169d799745ca7280bc90fdfa693536901', 'dev_requirement' => false, ), 'composer/xdebug-handler' => array( 'pretty_version' => '2.0.4', 'version' => '2.0.4.0', 'type' => 'library', 'install_path' => __DIR__ . '/./xdebug-handler', 'aliases' => array(), 'reference' => '0c1a3925ec58a4ec98e992b9c7d171e9e184be0a', 'dev_requirement' => false, ), 'gettext/gettext' => array( 'pretty_version' => 'v4.8.6', 'version' => '4.8.6.0', 'type' => 'library', 'install_path' => __DIR__ . '/../gettext/gettext', 'aliases' => array(), 'reference' => 'bbeb8f4d3077663739aecb4551b22e720c0e9efe', 'dev_requirement' => false, ), 'gettext/languages' => array( 'pretty_version' => '2.9.0', 'version' => '2.9.0.0', 'type' => 'library', 'install_path' => __DIR__ . '/../gettext/languages', 'aliases' => array(), 'reference' => 'ed56dd2c7f4024cc953ed180d25f02f2640e3ffa', 'dev_requirement' => false, ), 'justinrainbow/json-schema' => array( 'pretty_version' => '5.2.11', 'version' => '5.2.11.0', 'type' => 'library', 'install_path' => __DIR__ . '/../justinrainbow/json-schema', 'aliases' => array(), 'reference' => '2ab6744b7296ded80f8cc4f9509abbff393399aa', 'dev_requirement' => false, ), 'mck89/peast' => array( 'pretty_version' => 'v1.13.11', 'version' => '1.13.11.0', 'type' => 'library', 'install_path' => __DIR__ . '/../mck89/peast', 'aliases' => array(), 'reference' => '78c57966f3da5f223636ea0417d71ac6ff61e47f', 'dev_requirement' => false, ), 'mustache/mustache' => array( 'pretty_version' => 'v2.14.1', 'version' => '2.14.1.0', 'type' => 'library', 'install_path' => __DIR__ . '/../mustache/mustache', 'aliases' => array(), 'reference' => '579ffa5c96e1d292c060b3dd62811ff01ad8c24e', 'dev_requirement' => false, ), 'nb/oxymel' => array( 'pretty_version' => 'v0.1.0', 'version' => '0.1.0.0', 'type' => 'library', 'install_path' => __DIR__ . '/../nb/oxymel', 'aliases' => array(), 'reference' => 'cbe626ef55d5c4cc9b5e6e3904b395861ea76e3c', 'dev_requirement' => false, ), 'psr/log' => array( 'pretty_version' => '1.1.4', 'version' => '1.1.4.0', 'type' => 'library', 'install_path' => __DIR__ . '/../psr/log', 'aliases' => array(), 'reference' => 'd49695b909c3b7628b6289db5479a1c204601f11', 'dev_requirement' => false, ), 'psr/log-implementation' => array( 'dev_requirement' => false, 'provided' => array( 0 => '1.0', ), ), 'react/promise' => array( 'pretty_version' => 'v2.8.0', 'version' => '2.8.0.0', 'type' => 'library', 'install_path' => __DIR__ . '/../react/promise', 'aliases' => array(), 'reference' => 'f3cff96a19736714524ca0dd1d4130de73dbbbc4', 'dev_requirement' => false, ), 'rmccue/requests' => array( 'pretty_version' => 'v1.8.1', 'version' => '1.8.1.0', 'type' => 'library', 'install_path' => __DIR__ . '/../rmccue/requests', 'aliases' => array(), 'reference' => '82e6936366eac3af4d836c18b9d8c31028fe4cd5', 'dev_requirement' => false, ), 'seld/jsonlint' => array( 'pretty_version' => '1.8.3', 'version' => '1.8.3.0', 'type' => 'library', 'install_path' => __DIR__ . '/../seld/jsonlint', 'aliases' => array(), 'reference' => '9ad6ce79c342fbd44df10ea95511a1b24dee5b57', 'dev_requirement' => false, ), 'seld/phar-utils' => array( 'pretty_version' => '1.2.0', 'version' => '1.2.0.0', 'type' => 'library', 'install_path' => __DIR__ . '/../seld/phar-utils', 'aliases' => array(), 'reference' => '9f3452c93ff423469c0d56450431562ca423dcee', 'dev_requirement' => false, ), 'symfony/console' => array( 'pretty_version' => 'v3.4.47', 'version' => '3.4.47.0', 'type' => 'library', 'install_path' => __DIR__ . '/../symfony/console', 'aliases' => array(), 'reference' => 'a10b1da6fc93080c180bba7219b5ff5b7518fe81', 'dev_requirement' => false, ), 'symfony/debug' => array( 'pretty_version' => 'v3.4.47', 'version' => '3.4.47.0', 'type' => 'library', 'install_path' => __DIR__ . '/../symfony/debug', 'aliases' => array(), 'reference' => 'ab42889de57fdfcfcc0759ab102e2fd4ea72dcae', 'dev_requirement' => false, ), 'symfony/filesystem' => array( 'pretty_version' => 'v3.4.47', 'version' => '3.4.47.0', 'type' => 'library', 'install_path' => __DIR__ . '/../symfony/filesystem', 'aliases' => array(), 'reference' => 'e58d7841cddfed6e846829040dca2cca0ebbbbb3', 'dev_requirement' => false, ), 'symfony/finder' => array( 'pretty_version' => 'v3.4.47', 'version' => '3.4.47.0', 'type' => 'library', 'install_path' => __DIR__ . '/../symfony/finder', 'aliases' => array(), 'reference' => 'b6b6ad3db3edb1b4b1c1896b1975fb684994de6e', 'dev_requirement' => false, ), 'symfony/polyfill-ctype' => array( 'pretty_version' => 'v1.19.0', 'version' => '1.19.0.0', 'type' => 'library', 'install_path' => __DIR__ . '/../symfony/polyfill-ctype', 'aliases' => array(), 'reference' => 'aed596913b70fae57be53d86faa2e9ef85a2297b', 'dev_requirement' => false, ), 'symfony/polyfill-mbstring' => array( 'pretty_version' => 'v1.19.0', 'version' => '1.19.0.0', 'type' => 'library', 'install_path' => __DIR__ . '/../symfony/polyfill-mbstring', 'aliases' => array(), 'reference' => 'b5f7b932ee6fa802fc792eabd77c4c88084517ce', 'dev_requirement' => false, ), 'symfony/process' => array( 'pretty_version' => 'v3.4.47', 'version' => '3.4.47.0', 'type' => 'library', 'install_path' => __DIR__ . '/../symfony/process', 'aliases' => array(), 'reference' => 'b8648cf1d5af12a44a51d07ef9bf980921f15fca', 'dev_requirement' => false, ), 'wp-cli/cache-command' => array( 'pretty_version' => 'v2.0.9', 'version' => '2.0.9.0', 'type' => 'wp-cli-package', 'install_path' => __DIR__ . '/../wp-cli/cache-command', 'aliases' => array(), 'reference' => '05378440d8c6d4d2a1a5e5cbc1ba92a5e4bf1c40', 'dev_requirement' => false, ), 'wp-cli/checksum-command' => array( 'pretty_version' => 'v2.1.2', 'version' => '2.1.2.0', 'type' => 'wp-cli-package', 'install_path' => __DIR__ . '/../wp-cli/checksum-command', 'aliases' => array(), 'reference' => 'ec59a24af2ca97b770a4709b0a1c241eeb4b4cff', 'dev_requirement' => false, ), 'wp-cli/config-command' => array( 'pretty_version' => 'v2.1.3', 'version' => '2.1.3.0', 'type' => 'wp-cli-package', 'install_path' => __DIR__ . '/../wp-cli/config-command', 'aliases' => array(), 'reference' => 'cdabbc47dae464a93b10361b9a18e84cf4e72fe2', 'dev_requirement' => false, ), 'wp-cli/core-command' => array( 'pretty_version' => 'v2.1.1', 'version' => '2.1.1.0', 'type' => 'wp-cli-package', 'install_path' => __DIR__ . '/../wp-cli/core-command', 'aliases' => array(), 'reference' => 'a75d052ea000b4f0ec14106c110836b376e95a4f', 'dev_requirement' => false, ), 'wp-cli/cron-command' => array( 'pretty_version' => 'v2.1.0', 'version' => '2.1.0.0', 'type' => 'wp-cli-package', 'install_path' => __DIR__ . '/../wp-cli/cron-command', 'aliases' => array(), 'reference' => 'bb9fd9645e9a5276d024a59affeda3e6aa8530be', 'dev_requirement' => false, ), 'wp-cli/db-command' => array( 'pretty_version' => 'v2.0.20', 'version' => '2.0.20.0', 'type' => 'wp-cli-package', 'install_path' => __DIR__ . '/../wp-cli/db-command', 'aliases' => array(), 'reference' => '709f58c30d178afcdecaf56068ca9f5e985ed6b9', 'dev_requirement' => false, ), 'wp-cli/embed-command' => array( 'pretty_version' => 'v2.0.11', 'version' => '2.0.11.0', 'type' => 'wp-cli-package', 'install_path' => __DIR__ . '/../wp-cli/embed-command', 'aliases' => array(), 'reference' => '00a901a66aecb4da94a8dace610eb1135fc82386', 'dev_requirement' => false, ), 'wp-cli/entity-command' => array( 'pretty_version' => 'v2.2.1', 'version' => '2.2.1.0', 'type' => 'wp-cli-package', 'install_path' => __DIR__ . '/../wp-cli/entity-command', 'aliases' => array(), 'reference' => 'd7d08b05c67651abde5d570851e46498a164cb34', 'dev_requirement' => false, ), 'wp-cli/eval-command' => array( 'pretty_version' => 'v2.1.2', 'version' => '2.1.2.0', 'type' => 'wp-cli-package', 'install_path' => __DIR__ . '/../wp-cli/eval-command', 'aliases' => array(), 'reference' => '5213040ec2167b2748f2689ff6fe24b92a064a90', 'dev_requirement' => false, ), 'wp-cli/export-command' => array( 'pretty_version' => 'v2.0.11', 'version' => '2.0.11.0', 'type' => 'wp-cli-package', 'install_path' => __DIR__ . '/../wp-cli/export-command', 'aliases' => array(), 'reference' => '8dd137e0c739a59bb3d3de684a219dbb34473e11', 'dev_requirement' => false, ), 'wp-cli/extension-command' => array( 'pretty_version' => 'v2.1.4', 'version' => '2.1.4.0', 'type' => 'wp-cli-package', 'install_path' => __DIR__ . '/../wp-cli/extension-command', 'aliases' => array(), 'reference' => '6401d7ea51084fac40010c2fb305be640675f385', 'dev_requirement' => false, ), 'wp-cli/i18n-command' => array( 'pretty_version' => 'v2.2.13', 'version' => '2.2.13.0', 'type' => 'wp-cli-package', 'install_path' => __DIR__ . '/../wp-cli/i18n-command', 'aliases' => array(), 'reference' => '77ece9e2c914bb56ea72b9ee9f00556dece07b3f', 'dev_requirement' => false, ), 'wp-cli/import-command' => array( 'pretty_version' => 'v2.0.8', 'version' => '2.0.8.0', 'type' => 'wp-cli-package', 'install_path' => __DIR__ . '/../wp-cli/import-command', 'aliases' => array(), 'reference' => 'a092e3abcca843f1fabf2e9b706a912ae075355f', 'dev_requirement' => false, ), 'wp-cli/language-command' => array( 'pretty_version' => 'v2.0.12', 'version' => '2.0.12.0', 'type' => 'wp-cli-package', 'install_path' => __DIR__ . '/../wp-cli/language-command', 'aliases' => array(), 'reference' => 'bbdba69179fc8df597928587111500c8ade40a38', 'dev_requirement' => false, ), 'wp-cli/maintenance-mode-command' => array( 'pretty_version' => 'v2.0.8', 'version' => '2.0.8.0', 'type' => 'wp-cli-package', 'install_path' => __DIR__ . '/../wp-cli/maintenance-mode-command', 'aliases' => array(), 'reference' => 'e65505c973ea9349257a4f33ac9edc78db0b189a', 'dev_requirement' => false, ), 'wp-cli/media-command' => array( 'pretty_version' => 'v2.0.12', 'version' => '2.0.12.0', 'type' => 'wp-cli-package', 'install_path' => __DIR__ . '/../wp-cli/media-command', 'aliases' => array(), 'reference' => '7cbf32c7fa68631619f85a9bea0fb80fca119ba6', 'dev_requirement' => false, ), 'wp-cli/mustangostang-spyc' => array( 'pretty_version' => '0.6.3', 'version' => '0.6.3.0', 'type' => 'library', 'install_path' => __DIR__ . '/../wp-cli/mustangostang-spyc', 'aliases' => array(), 'reference' => '6aa0b4da69ce9e9a2c8402dab8d43cf32c581cc7', 'dev_requirement' => false, ), 'wp-cli/package-command' => array( 'pretty_version' => 'v2.2.2', 'version' => '2.2.2.0', 'type' => 'wp-cli-package', 'install_path' => __DIR__ . '/../wp-cli/package-command', 'aliases' => array(), 'reference' => '36afdee21d022e6270867aa0cbfef6f453041814', 'dev_requirement' => false, ), 'wp-cli/php-cli-tools' => array( 'pretty_version' => 'v0.11.13', 'version' => '0.11.13.0', 'type' => 'library', 'install_path' => __DIR__ . '/../wp-cli/php-cli-tools', 'aliases' => array(), 'reference' => 'a2866855ac1abc53005c102e901553ad5772dc04', 'dev_requirement' => false, ), 'wp-cli/rewrite-command' => array( 'pretty_version' => 'v2.0.10', 'version' => '2.0.10.0', 'type' => 'wp-cli-package', 'install_path' => __DIR__ . '/../wp-cli/rewrite-command', 'aliases' => array(), 'reference' => '562a0a5a0d51be000de87d7a8a870de13383ecd6', 'dev_requirement' => false, ), 'wp-cli/role-command' => array( 'pretty_version' => 'v2.0.9', 'version' => '2.0.9.0', 'type' => 'wp-cli-package', 'install_path' => __DIR__ . '/../wp-cli/role-command', 'aliases' => array(), 'reference' => '9abd93952565935084160bc3be49dfb2483bb0b6', 'dev_requirement' => false, ), 'wp-cli/scaffold-command' => array( 'pretty_version' => 'v2.0.16', 'version' => '2.0.16.0', 'type' => 'wp-cli-package', 'install_path' => __DIR__ . '/../wp-cli/scaffold-command', 'aliases' => array(), 'reference' => '6d92fb363b8ed7473af7f12cf342aaf9d2c96e81', 'dev_requirement' => false, ), 'wp-cli/search-replace-command' => array( 'pretty_version' => 'v2.0.16', 'version' => '2.0.16.0', 'type' => 'wp-cli-package', 'install_path' => __DIR__ . '/../wp-cli/search-replace-command', 'aliases' => array(), 'reference' => 'dbf21560fd91710b2900f5631448657d28f2b380', 'dev_requirement' => false, ), 'wp-cli/server-command' => array( 'pretty_version' => 'v2.0.10', 'version' => '2.0.10.0', 'type' => 'wp-cli-package', 'install_path' => __DIR__ . '/../wp-cli/server-command', 'aliases' => array(), 'reference' => '50c81f45f1cf09bc0a52e3582b3e56d27ca3c33c', 'dev_requirement' => false, ), 'wp-cli/shell-command' => array( 'pretty_version' => 'v2.0.11', 'version' => '2.0.11.0', 'type' => 'wp-cli-package', 'install_path' => __DIR__ . '/../wp-cli/shell-command', 'aliases' => array(), 'reference' => '28a7de3134c9f059900d8fa4aea1d7d618481454', 'dev_requirement' => false, ), 'wp-cli/super-admin-command' => array( 'pretty_version' => 'v2.0.10', 'version' => '2.0.10.0', 'type' => 'wp-cli-package', 'install_path' => __DIR__ . '/../wp-cli/super-admin-command', 'aliases' => array(), 'reference' => 'e6707f3acfff089d19c5c55bba0fd66cd7d6c2fa', 'dev_requirement' => false, ), 'wp-cli/widget-command' => array( 'pretty_version' => 'v2.1.7', 'version' => '2.1.7.0', 'type' => 'wp-cli-package', 'install_path' => __DIR__ . '/../wp-cli/widget-command', 'aliases' => array(), 'reference' => '6aedab77f1cd2a0f511b62110c244c32b84dd429', 'dev_requirement' => false, ), 'wp-cli/wp-cli' => array( 'pretty_version' => 'v2.6.0', 'version' => '2.6.0.0', 'type' => 'library', 'install_path' => __DIR__ . '/../wp-cli/wp-cli', 'aliases' => array(), 'reference' => 'dee13c2baf6bf972484a63f8b8dab48f7220f095', 'dev_requirement' => false, ), 'wp-cli/wp-cli-bundle' => array( 'pretty_version' => 'dev-release-2-6-0', 'version' => 'dev-release-2-6-0', 'type' => 'library', 'install_path' => __DIR__ . '/../../', 'aliases' => array(), 'reference' => '19712f1a5e20612a75512d2f418cde455adc5125', 'dev_requirement' => false, ), 'wp-cli/wp-config-transformer' => array( 'pretty_version' => 'v1.3.0', 'version' => '1.3.0.0', 'type' => 'library', 'install_path' => __DIR__ . '/../wp-cli/wp-config-transformer', 'aliases' => array(), 'reference' => '2e90eefc6b8f5166f53aa5414fd8f1a572164ef1', 'dev_requirement' => false, ), ), ); * * For the full copyright and license information, please view * the LICENSE file that was distributed with this source code. */ namespace Composer\XdebugHandler; use Psr\Log\LoggerInterface; use Psr\Log\LogLevel; /** * @author John Stevenson * @internal */ class Status { const ENV_RESTART = 'XDEBUG_HANDLER_RESTART'; const CHECK = 'Check'; const ERROR = 'Error'; const INFO = 'Info'; const NORESTART = 'NoRestart'; const RESTART = 'Restart'; const RESTARTING = 'Restarting'; const RESTARTED = 'Restarted'; /** @var bool */ private $debug; /** @var string */ private $envAllowXdebug; /** @var string|null */ private $loaded; /** @var LoggerInterface|null */ private $logger; /** @var bool */ private $modeOff; /** @var float */ private $time; /** * Constructor * * @param string $envAllowXdebug Prefixed _ALLOW_XDEBUG name * @param bool $debug Whether debug output is required */ public function __construct($envAllowXdebug, $debug) { $start = getenv(self::ENV_RESTART); Process::setEnv(self::ENV_RESTART); $this->time = is_numeric($start) ? round((microtime(true) - $start) * 1000) : 0; $this->envAllowXdebug = $envAllowXdebug; $this->debug = $debug && defined('STDERR'); $this->modeOff = false; } /** * @param LoggerInterface $logger * * @return void */ public function setLogger(LoggerInterface $logger) { $this->logger = $logger; } /** * Calls a handler method to report a message * * @param string $op The handler constant * @param null|string $data Data required by the handler * * @return void * @throws \InvalidArgumentException If $op is not known */ public function report($op, $data) { if ($this->logger !== null || $this->debug) { $callable = array($this, 'report'.$op); if (!is_callable($callable)) { throw new \InvalidArgumentException('Unknown op handler: '.$op); } $params = $data !== null ? $data : array(); call_user_func_array($callable, array($params)); } } /** * Outputs a status message * * @param string $text * @param string $level * * @return void */ private function output($text, $level = null) { if ($this->logger !== null) { $this->logger->log($level !== null ? $level: LogLevel::DEBUG, $text); } if ($this->debug) { fwrite(STDERR, sprintf('xdebug-handler[%d] %s', getmypid(), $text.PHP_EOL)); } } /** * @param string $loaded * * @return void */ private function reportCheck($loaded) { list($version, $mode) = explode('|', $loaded); if ($version !== '') { $this->loaded = '('.$version.')'.($mode !== '' ? ' mode='.$mode : ''); } $this->modeOff = $mode === 'off'; $this->output('Checking '.$this->envAllowXdebug); } /** * @param string $error * * @return void */ private function reportError($error) { $this->output(sprintf('No restart (%s)', $error), LogLevel::WARNING); } /** * @param string $info * * @return void */ private function reportInfo($info) { $this->output($info); } /** * @return void */ private function reportNoRestart() { $this->output($this->getLoadedMessage()); if ($this->loaded !== null) { $text = sprintf('No restart (%s)', $this->getEnvAllow()); if (!((bool) getenv($this->envAllowXdebug))) { $text .= ' Allowed by '.($this->modeOff ? 'mode' : 'application'); } $this->output($text); } } /** * @return void */ private function reportRestart() { $this->output($this->getLoadedMessage()); Process::setEnv(self::ENV_RESTART, (string) microtime(true)); } /** * @return void */ private function reportRestarted() { $loaded = $this->getLoadedMessage(); $text = sprintf('Restarted (%d ms). %s', $this->time, $loaded); $level = $this->loaded !== null ? LogLevel::WARNING : null; $this->output($text, $level); } /** * @param string $command * * @return void */ private function reportRestarting($command) { $text = sprintf('Process restarting (%s)', $this->getEnvAllow()); $this->output($text); $text = 'Running '.$command; $this->output($text); } /** * Returns the _ALLOW_XDEBUG environment variable as name=value * * @return string */ private function getEnvAllow() { return $this->envAllowXdebug.'='.getenv($this->envAllowXdebug); } /** * Returns the Xdebug status and version * * @return string */ private function getLoadedMessage() { $loaded = $this->loaded !== null ? sprintf('loaded %s', $this->loaded) : 'not loaded'; return 'The Xdebug extension is '.$loaded; } } * * For the full copyright and license information, please view * the LICENSE file that was distributed with this source code. */ namespace Composer\XdebugHandler; /** * @author John Stevenson * * @phpstan-type restartData array{tmpIni: string, scannedInis: bool, scanDir: false|string, phprc: false|string, inis: string[], skipped: string} */ class PhpConfig { /** * Use the original PHP configuration * * @return string[] Empty array of PHP cli options */ public function useOriginal() { $this->getDataAndReset(); return array(); } /** * Use standard restart settings * * @return string[] PHP cli options */ public function useStandard() { $data = $this->getDataAndReset(); if ($data !== null) { return array('-n', '-c', $data['tmpIni']); } return array(); } /** * Use environment variables to persist settings * * @return string[] Empty array of PHP cli options */ public function usePersistent() { $data = $this->getDataAndReset(); if ($data !== null) { $this->updateEnv('PHPRC', $data['tmpIni']); $this->updateEnv('PHP_INI_SCAN_DIR', ''); } return array(); } /** * Returns restart data if available and resets the environment * * @return array|null * @phpstan-return restartData|null */ private function getDataAndReset() { $data = XdebugHandler::getRestartSettings(); if ($data !== null) { $this->updateEnv('PHPRC', $data['phprc']); $this->updateEnv('PHP_INI_SCAN_DIR', $data['scanDir']); } return $data; } /** * Updates a restart settings value in the environment * * @param string $name * @param string|false $value * * @return void */ private function updateEnv($name, $value) { Process::setEnv($name, false !== $value ? $value : null); } } * * For the full copyright and license information, please view * the LICENSE file that was distributed with this source code. */ namespace Composer\XdebugHandler; use Composer\Pcre\Preg; use Psr\Log\LoggerInterface; /** * @author John Stevenson * * @phpstan-import-type restartData from PhpConfig */ class XdebugHandler { const SUFFIX_ALLOW = '_ALLOW_XDEBUG'; const SUFFIX_INIS = '_ORIGINAL_INIS'; const RESTART_ID = 'internal'; const RESTART_SETTINGS = 'XDEBUG_HANDLER_SETTINGS'; const DEBUG = 'XDEBUG_HANDLER_DEBUG'; /** @var string|null */ protected $tmpIni; /** @var bool */ private static $inRestart; /** @var string */ private static $name; /** @var string|null */ private static $skipped; /** @var bool */ private static $xdebugActive; /** @var string|null */ private static $xdebugMode; /** @var string|null */ private static $xdebugVersion; /** @var bool */ private $cli; /** @var string|null */ private $debug; /** @var string */ private $envAllowXdebug; /** @var string */ private $envOriginalInis; /** @var bool */ private $persistent; /** @var string|null */ private $script; /** @var Status */ private $statusWriter; /** * Constructor * * The $envPrefix is used to create distinct environment variables. It is * uppercased and prepended to the default base values. For example 'myapp' * would result in MYAPP_ALLOW_XDEBUG and MYAPP_ORIGINAL_INIS. * * @param string $envPrefix Value used in environment variables * @throws \RuntimeException If the parameter is invalid */ public function __construct($envPrefix) { if (!is_string($envPrefix) || $envPrefix === '') { throw new \RuntimeException('Invalid constructor parameter'); } self::$name = strtoupper($envPrefix); $this->envAllowXdebug = self::$name.self::SUFFIX_ALLOW; $this->envOriginalInis = self::$name.self::SUFFIX_INIS; self::setXdebugDetails(); self::$inRestart = false; if ($this->cli = PHP_SAPI === 'cli') { $this->debug = (string) getenv(self::DEBUG); } $this->statusWriter = new Status($this->envAllowXdebug, (bool) $this->debug); } /** * Activates status message output to a PSR3 logger * * @param LoggerInterface $logger * * @return $this */ public function setLogger(LoggerInterface $logger) { $this->statusWriter->setLogger($logger); return $this; } /** * Sets the main script location if it cannot be called from argv * * @param string $script * * @return $this */ public function setMainScript($script) { $this->script = $script; return $this; } /** * Persist the settings to keep Xdebug out of sub-processes * * @return $this */ public function setPersistent() { $this->persistent = true; return $this; } /** * Checks if Xdebug is loaded and the process needs to be restarted * * This behaviour can be disabled by setting the MYAPP_ALLOW_XDEBUG * environment variable to 1. This variable is used internally so that * the restarted process is created only once. * * @return void */ public function check() { $this->notify(Status::CHECK, self::$xdebugVersion.'|'.self::$xdebugMode); $envArgs = explode('|', (string) getenv($this->envAllowXdebug)); if (!((bool) $envArgs[0]) && $this->requiresRestart(self::$xdebugActive)) { // Restart required $this->notify(Status::RESTART); if ($this->prepareRestart()) { $command = $this->getCommand(); $this->restart($command); } return; } if (self::RESTART_ID === $envArgs[0] && count($envArgs) === 5) { // Restarted, so unset environment variable and use saved values $this->notify(Status::RESTARTED); Process::setEnv($this->envAllowXdebug); self::$inRestart = true; if (self::$xdebugVersion === null) { // Skipped version is only set if Xdebug is not loaded self::$skipped = $envArgs[1]; } $this->tryEnableSignals(); // Put restart settings in the environment $this->setEnvRestartSettings($envArgs); return; } $this->notify(Status::NORESTART); $settings = self::getRestartSettings(); if ($settings !== null) { // Called with existing settings, so sync our settings $this->syncSettings($settings); } } /** * Returns an array of php.ini locations with at least one entry * * The equivalent of calling php_ini_loaded_file then php_ini_scanned_files. * The loaded ini location is the first entry and may be empty. * * @return string[] */ public static function getAllIniFiles() { if (self::$name !== null) { $env = getenv(self::$name.self::SUFFIX_INIS); if (false !== $env) { return explode(PATH_SEPARATOR, $env); } } $paths = array((string) php_ini_loaded_file()); $scanned = php_ini_scanned_files(); if ($scanned !== false) { $paths = array_merge($paths, array_map('trim', explode(',', $scanned))); } return $paths; } /** * Returns an array of restart settings or null * * Settings will be available if the current process was restarted, or * called with the settings from an existing restart. * * @return array|null * @phpstan-return restartData|null */ public static function getRestartSettings() { $envArgs = explode('|', (string) getenv(self::RESTART_SETTINGS)); if (count($envArgs) !== 6 || (!self::$inRestart && php_ini_loaded_file() !== $envArgs[0])) { return null; } return array( 'tmpIni' => $envArgs[0], 'scannedInis' => (bool) $envArgs[1], 'scanDir' => '*' === $envArgs[2] ? false : $envArgs[2], 'phprc' => '*' === $envArgs[3] ? false : $envArgs[3], 'inis' => explode(PATH_SEPARATOR, $envArgs[4]), 'skipped' => $envArgs[5], ); } /** * Returns the Xdebug version that triggered a successful restart * * @return string */ public static function getSkippedVersion() { return (string) self::$skipped; } /** * Returns whether Xdebug is loaded and active * * true: if Xdebug is loaded and is running in an active mode. * false: if Xdebug is not loaded, or it is running with xdebug.mode=off. * * @return bool */ public static function isXdebugActive() { self::setXdebugDetails(); return self::$xdebugActive; } /** * Allows an extending class to decide if there should be a restart * * The default is to restart if Xdebug is loaded and its mode is not "off". * Do not typehint for 1.x compatibility. * * @param bool $default The default behaviour * * @return bool Whether the process should restart */ protected function requiresRestart($default) { return $default; } /** * Allows an extending class to access the tmpIni * * Do not typehint for 1.x compatibility * * @param string[] $command * * @return void */ protected function restart($command) { $this->doRestart($command); } /** * Executes the restarted command then deletes the tmp ini * * @param string[] $command * * @return void * @phpstan-return never */ private function doRestart(array $command) { $this->tryEnableSignals(); $this->notify(Status::RESTARTING, implode(' ', $command)); if (PHP_VERSION_ID >= 70400) { $cmd = $command; } else { $cmd = Process::escapeShellCommand($command); if (defined('PHP_WINDOWS_VERSION_BUILD')) { // Outer quotes required on cmd string below PHP 8 $cmd = '"'.$cmd.'"'; } } $process = proc_open($cmd, array(), $pipes); if (is_resource($process)) { $exitCode = proc_close($process); } if (!isset($exitCode)) { // Unlikely that php or the default shell cannot be invoked $this->notify(Status::ERROR, 'Unable to restart process'); $exitCode = -1; } else { $this->notify(Status::INFO, 'Restarted process exited '.$exitCode); } if ($this->debug === '2') { $this->notify(Status::INFO, 'Temp ini saved: '.$this->tmpIni); } else { @unlink((string) $this->tmpIni); } exit($exitCode); } /** * Returns true if everything was written for the restart * * If any of the following fails (however unlikely) we must return false to * stop potential recursion: * - tmp ini file creation * - environment variable creation * * @return bool */ private function prepareRestart() { $error = null; $iniFiles = self::getAllIniFiles(); $scannedInis = count($iniFiles) > 1; $tmpDir = sys_get_temp_dir(); if (!$this->cli) { $error = 'Unsupported SAPI: '.PHP_SAPI; } elseif (!defined('PHP_BINARY')) { $error = 'PHP version is too old: '.PHP_VERSION; } elseif (!$this->checkConfiguration($info)) { $error = $info; } elseif (!$this->checkScanDirConfig()) { $error = 'PHP version does not report scanned inis: '.PHP_VERSION; } elseif (!$this->checkMainScript()) { $error = 'Unable to access main script: '.$this->script; } elseif (!$this->writeTmpIni($iniFiles, $tmpDir, $error)) { $error = $error !== null ? $error : 'Unable to create temp ini file at: '.$tmpDir; } elseif (!$this->setEnvironment($scannedInis, $iniFiles)) { $error = 'Unable to set environment variables'; } if ($error !== null) { $this->notify(Status::ERROR, $error); } return $error === null; } /** * Returns true if the tmp ini file was written * * @param string[] $iniFiles All ini files used in the current process * @param string $tmpDir The system temporary directory * @param null|string $error Set by method if ini file cannot be read * * @return bool */ private function writeTmpIni(array $iniFiles, $tmpDir, &$error) { if (($tmpfile = @tempnam($tmpDir, '')) === false) { return false; } $this->tmpIni = $tmpfile; // $iniFiles has at least one item and it may be empty if ($iniFiles[0] === '') { array_shift($iniFiles); } $content = ''; $sectionRegex = '/^\s*\[(?:PATH|HOST)\s*=/mi'; $xdebugRegex = '/^\s*(zend_extension\s*=.*xdebug.*)$/mi'; foreach ($iniFiles as $file) { // Check for inaccessible ini files if (($data = @file_get_contents($file)) === false) { $error = 'Unable to read ini: '.$file; return false; } // Check and remove directives after HOST and PATH sections if (Preg::isMatchWithOffsets($sectionRegex, $data, $matches, PREG_OFFSET_CAPTURE)) { $data = substr($data, 0, $matches[0][1]); } $content .= Preg::replace($xdebugRegex, ';$1', $data).PHP_EOL; } // Merge loaded settings into our ini content, if it is valid $config = parse_ini_string($content); $loaded = ini_get_all(null, false); if (false === $config || false === $loaded) { $error = 'Unable to parse ini data'; return false; } $content .= $this->mergeLoadedConfig($loaded, $config); // Work-around for https://bugs.php.net/bug.php?id=75932 $content .= 'opcache.enable_cli=0'.PHP_EOL; return (bool) @file_put_contents($this->tmpIni, $content); } /** * Returns the command line arguments for the restart * * @return string[] */ private function getCommand() { $php = array(PHP_BINARY); $args = array_slice($_SERVER['argv'], 1); if (!$this->persistent) { // Use command-line options array_push($php, '-n', '-c', $this->tmpIni); } return array_merge($php, array($this->script), $args); } /** * Returns true if the restart environment variables were set * * No need to update $_SERVER since this is set in the restarted process. * * @param bool $scannedInis Whether there were scanned ini files * @param string[] $iniFiles All ini files used in the current process * * @return bool */ private function setEnvironment($scannedInis, array $iniFiles) { $scanDir = getenv('PHP_INI_SCAN_DIR'); $phprc = getenv('PHPRC'); // Make original inis available to restarted process if (!putenv($this->envOriginalInis.'='.implode(PATH_SEPARATOR, $iniFiles))) { return false; } if ($this->persistent) { // Use the environment to persist the settings if (!putenv('PHP_INI_SCAN_DIR=') || !putenv('PHPRC='.$this->tmpIni)) { return false; } } // Flag restarted process and save values for it to use $envArgs = array( self::RESTART_ID, self::$xdebugVersion, (int) $scannedInis, false === $scanDir ? '*' : $scanDir, false === $phprc ? '*' : $phprc, ); return putenv($this->envAllowXdebug.'='.implode('|', $envArgs)); } /** * Logs status messages * * @param string $op Status handler constant * @param null|string $data Optional data * * @return void */ private function notify($op, $data = null) { $this->statusWriter->report($op, $data); } /** * Returns default, changed and command-line ini settings * * @param mixed[] $loadedConfig All current ini settings * @param mixed[] $iniConfig Settings from user ini files * * @return string */ private function mergeLoadedConfig(array $loadedConfig, array $iniConfig) { $content = ''; foreach ($loadedConfig as $name => $value) { // Value will either be null, string or array (HHVM only) if (!is_string($value) || strpos($name, 'xdebug') === 0 || $name === 'apc.mmap_file_mask') { continue; } if (!isset($iniConfig[$name]) || $iniConfig[$name] !== $value) { // Double-quote escape each value $content .= $name.'="'.addcslashes($value, '\\"').'"'.PHP_EOL; } } return $content; } /** * Returns true if the script name can be used * * @return bool */ private function checkMainScript() { if (null !== $this->script) { // Allow an application to set -- for standard input return file_exists($this->script) || '--' === $this->script; } if (file_exists($this->script = $_SERVER['argv'][0])) { return true; } // Use a backtrace to resolve Phar and chdir issues. $trace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS); $main = end($trace); if ($main !== false && isset($main['file'])) { return file_exists($this->script = $main['file']); } return false; } /** * Adds restart settings to the environment * * @param string[] $envArgs * * @return void */ private function setEnvRestartSettings($envArgs) { $settings = array( php_ini_loaded_file(), $envArgs[2], $envArgs[3], $envArgs[4], getenv($this->envOriginalInis), self::$skipped, ); Process::setEnv(self::RESTART_SETTINGS, implode('|', $settings)); } /** * Syncs settings and the environment if called with existing settings * * @param array $settings * @phpstan-param restartData $settings * * @return void */ private function syncSettings(array $settings) { if (false === getenv($this->envOriginalInis)) { // Called by another app, so make original inis available Process::setEnv($this->envOriginalInis, implode(PATH_SEPARATOR, $settings['inis'])); } self::$skipped = $settings['skipped']; $this->notify(Status::INFO, 'Process called with existing restart settings'); } /** * Returns true if there are scanned inis and PHP is able to report them * * php_ini_scanned_files will fail when PHP_CONFIG_FILE_SCAN_DIR is empty. * Fixed in 7.1.13 and 7.2.1 * * @return bool */ private function checkScanDirConfig() { if (PHP_VERSION_ID >= 70113 && PHP_VERSION_ID !== 70200) { return true; } return ((string) getenv('PHP_INI_SCAN_DIR') === '') || PHP_CONFIG_FILE_SCAN_DIR !== ''; } /** * Returns true if there are no known configuration issues * * @param string $info Set by method * @return bool */ private function checkConfiguration(&$info) { if (!function_exists('proc_open')) { $info = 'proc_open function is disabled'; return false; } if (extension_loaded('uopz') && !((bool) ini_get('uopz.disable'))) { // uopz works at opcode level and disables exit calls if (function_exists('uopz_allow_exit')) { @uopz_allow_exit(true); } else { $info = 'uopz extension is not compatible'; return false; } } // Check UNC paths when using cmd.exe if (defined('PHP_WINDOWS_VERSION_BUILD') && PHP_VERSION_ID < 70400) { $workingDir = getcwd(); if ($workingDir === false) { $info = 'unable to determine working directory'; return false; } if (0 === strpos($workingDir, '\\\\')) { $info = 'cmd.exe does not support UNC paths: '.$workingDir; return false; } } return true; } /** * Enables async signals and control interrupts in the restarted process * * Available on Unix PHP 7.1+ with the pcntl extension and Windows PHP 7.4+. * * @return void */ private function tryEnableSignals() { if (function_exists('pcntl_async_signals') && function_exists('pcntl_signal')) { pcntl_async_signals(true); $message = 'Async signals enabled'; if (!self::$inRestart) { // Restarting, so ignore SIGINT in parent pcntl_signal(SIGINT, SIG_IGN); } elseif (is_int(pcntl_signal_get_handler(SIGINT))) { // Restarted, no handler set so force default action pcntl_signal(SIGINT, SIG_DFL); } } if (!self::$inRestart && function_exists('sapi_windows_set_ctrl_handler')) { // Restarting, so set a handler to ignore CTRL events in the parent. // This ensures that CTRL+C events will be available in the child // process without having to enable them there, which is unreliable. sapi_windows_set_ctrl_handler(function ($evt) {}); } } /** * Sets static properties $xdebugActive, $xdebugVersion and $xdebugMode * * @return void */ private static function setXdebugDetails() { if (self::$xdebugActive !== null) { return; } self::$xdebugActive = false; if (!extension_loaded('xdebug')) { return; } $version = phpversion('xdebug'); self::$xdebugVersion = $version !== false ? $version : 'unknown'; if (version_compare(self::$xdebugVersion, '3.1', '>=')) { $modes = xdebug_info('mode'); self::$xdebugMode = count($modes) === 0 ? 'off' : implode(',', $modes); self::$xdebugActive = self::$xdebugMode !== 'off'; return; } // See if xdebug.mode is supported in this version $iniMode = ini_get('xdebug.mode'); if ($iniMode === false) { return; } // Environment value wins but cannot be empty $envMode = (string) getenv('XDEBUG_MODE'); if ($envMode !== '') { self::$xdebugMode = $envMode; } else { self::$xdebugMode = $iniMode !== '' ? $iniMode : 'off'; } // An empty comma-separated list is treated as mode 'off' if (Preg::isMatch('/^,+$/', str_replace(' ', '', self::$xdebugMode))) { self::$xdebugMode = 'off'; } self::$xdebugActive = self::$xdebugMode !== 'off'; } } * * For the full copyright and license information, please view * the LICENSE file that was distributed with this source code. */ namespace Composer\XdebugHandler; use Composer\Pcre\Preg; /** * Process utility functions * * @author John Stevenson */ class Process { /** * Escapes a string to be used as a shell argument. * * From https://github.com/johnstevenson/winbox-args * MIT Licensed (c) John Stevenson * * @param string $arg The argument to be escaped * @param bool $meta Additionally escape cmd.exe meta characters * @param bool $module The argument is the module to invoke * * @return string The escaped argument */ public static function escape($arg, $meta = true, $module = false) { if (!defined('PHP_WINDOWS_VERSION_BUILD')) { return "'".str_replace("'", "'\\''", $arg)."'"; } $quote = strpbrk($arg, " \t") !== false || $arg === ''; $arg = Preg::replace('/(\\\\*)"/', '$1$1\\"', $arg, -1, $dquotes); if ($meta) { $meta = $dquotes || Preg::isMatch('/%[^%]+%/', $arg); if (!$meta) { $quote = $quote || strpbrk($arg, '^&|<>()') !== false; } elseif ($module && !$dquotes && $quote) { $meta = false; } } if ($quote) { $arg = '"'.(Preg::replace('/(\\\\*)$/', '$1$1', $arg)).'"'; } if ($meta) { $arg = Preg::replace('/(["^&|<>()%])/', '^$1', $arg); } return $arg; } /** * Escapes an array of arguments that make up a shell command * * @param string[] $args Argument list, with the module name first * * @return string The escaped command line */ public static function escapeShellCommand(array $args) { $command = ''; $module = array_shift($args); if ($module !== null) { $command = self::escape($module, true, true); foreach ($args as $arg) { $command .= ' '.self::escape($arg); } } return $command; } /** * Makes putenv environment changes available in $_SERVER and $_ENV * * @param string $name * @param string|null $value A null value unsets the variable * * @return bool Whether the environment variable was set */ public static function setEnv($name, $value = null) { $unset = null === $value; if (!putenv($unset ? $name : $name.'='.$value)) { return false; } if ($unset) { unset($_SERVER[$name]); } else { $_SERVER[$name] = $value; } // Update $_ENV if it is being used if (false !== stripos((string) ini_get('variables_order'), 'E')) { if ($unset) { unset($_ENV[$name]); } else { $_ENV[$name] = $value; } } return true; } } __DIR__ . '/..' . '/symfony/polyfill-ctype/bootstrap.php', '0e6d7bf4a5811bfa5cf40c5ccd6fae6a' => __DIR__ . '/..' . '/symfony/polyfill-mbstring/bootstrap.php', 'ad155f8f1cf0d418fe49e248db8c661b' => __DIR__ . '/..' . '/react/promise/src/functions_include.php', 'be01b9b16925dcb22165c40b46681ac6' => __DIR__ . '/..' . '/wp-cli/php-cli-tools/lib/cli/cli.php', '3937806105cc8e221b8fa8db5b70d2f2' => __DIR__ . '/..' . '/wp-cli/mustangostang-spyc/includes/functions.php', 'ba366f96f4fddbdef61ad7a862b44f61' => __DIR__ . '/..' . '/wp-cli/package-command/package-command.php', '8a0ad02df6a5087f2c380f8fd52db273' => __DIR__ . '/..' . '/wp-cli/cache-command/cache-command.php', 'f3f0199a3ecd9f501d0a3b361bd2f61c' => __DIR__ . '/..' . '/wp-cli/entity-command/entity-command.php', '3f201033d5aceb2293314273be88f7c6' => __DIR__ . '/..' . '/wp-cli/extension-command/extension-command.php', 'c65f753375faee349b7adc48c2ee7cc2' => __DIR__ . '/..' . '/wp-cli/db-command/db-command.php', '5e099d3cac677dd2bec1003ea7707745' => __DIR__ . '/..' . '/wp-cli/media-command/media-command.php', 'd979c11fe80ba96ae3037b43429fe546' => __DIR__ . '/..' . '/wp-cli/scaffold-command/scaffold-command.php', '5c6ec5cff8f9d625772c8ed147f6b894' => __DIR__ . '/..' . '/wp-cli/export-command/export-command.php', 'ac949ce40a981819ba132473518a9a31' => __DIR__ . '/..' . '/wp-cli/wp-config-transformer/src/WPConfigTransformer.php', 'b66d29757fcb2fb7a9608d068e3716b0' => __DIR__ . '/..' . '/wp-cli/checksum-command/checksum-command.php', '30cbb6e4122dc988e494c6b9c0438233' => __DIR__ . '/..' . '/wp-cli/import-command/import-command.php', '9f04dd0aa5d67ec75a75c88c345a079e' => __DIR__ . '/..' . '/wp-cli/server-command/server-command.php', '8519779bbb65eeb842af2f629ce7b6f8' => __DIR__ . '/..' . '/wp-cli/super-admin-command/super-admin-command.php', '5deaf6ce9c8bbdfb65104c7e938d5875' => __DIR__ . '/..' . '/wp-cli/config-command/config-command.php', '68c39b88215b6cf7a0da164166670ef9' => __DIR__ . '/..' . '/wp-cli/core-command/core-command.php', '7654e00bf0e632580764400bd8293a9c' => __DIR__ . '/..' . '/wp-cli/cron-command/cron-command.php', '021d3a13471556f0b57038d679f7f8ea' => __DIR__ . '/..' . '/wp-cli/embed-command/embed-command.php', 'f958dca3f412fd7975da1700912a9321' => __DIR__ . '/..' . '/wp-cli/eval-command/eval-command.php', 'ffb465a494c3101218c4417180c2c9a2' => __DIR__ . '/..' . '/wp-cli/i18n-command/i18n-command.php', '1c88c1eff05217a8cac80c64c9ac2080' => __DIR__ . '/..' . '/wp-cli/maintenance-mode-command/maintenance-mode-command.php', 'ace0d205db7f4135ec32132a0076d555' => __DIR__ . '/..' . '/wp-cli/language-command/language-command.php', 'f399c1c8d0c787d5c94c09884cdd9762' => __DIR__ . '/..' . '/wp-cli/rewrite-command/rewrite-command.php', '080fadd667195d055c5a23386f270261' => __DIR__ . '/..' . '/wp-cli/role-command/role-command.php', '8ecb13f8bbc22b1b34d12b14ec01077a' => __DIR__ . '/..' . '/wp-cli/search-replace-command/search-replace-command.php', '129d58fa8151374aceb8571bcaa97504' => __DIR__ . '/..' . '/wp-cli/shell-command/shell-command.php', '1f05372afcc7d0c51a305cef1d56dd01' => __DIR__ . '/..' . '/wp-cli/widget-command/widget-command.php', ); public static $prefixLengthsPsr4 = array ( 'W' => array ( 'WP_CLI\\MaintenanceMode\\' => 23, 'WP_CLI\\I18n\\' => 12, 'WP_CLI\\Embeds\\' => 14, 'WP_CLI\\' => 7, ), 'S' => array ( 'Symfony\\Polyfill\\Mbstring\\' => 26, 'Symfony\\Polyfill\\Ctype\\' => 23, 'Symfony\\Component\\Process\\' => 26, 'Symfony\\Component\\Finder\\' => 25, 'Symfony\\Component\\Filesystem\\' => 29, 'Symfony\\Component\\Debug\\' => 24, 'Symfony\\Component\\Console\\' => 26, 'Seld\\PharUtils\\' => 15, 'Seld\\JsonLint\\' => 14, ), 'R' => array ( 'React\\Promise\\' => 14, ), 'P' => array ( 'Psr\\Log\\' => 8, 'Peast\\test\\' => 11, 'Peast\\' => 6, ), 'M' => array ( 'Mustangostang\\' => 14, ), 'J' => array ( 'JsonSchema\\' => 11, ), 'G' => array ( 'Gettext\\Languages\\' => 18, 'Gettext\\' => 8, ), 'C' => array ( 'Composer\\XdebugHandler\\' => 23, 'Composer\\Spdx\\' => 14, 'Composer\\Semver\\' => 16, 'Composer\\Pcre\\' => 14, 'Composer\\MetadataMinifier\\' => 26, 'Composer\\CaBundle\\' => 18, 'Composer\\' => 9, ), ); public static $prefixDirsPsr4 = array ( 'WP_CLI\\MaintenanceMode\\' => array ( 0 => __DIR__ . '/..' . '/wp-cli/maintenance-mode-command/src', ), 'WP_CLI\\I18n\\' => array ( 0 => __DIR__ . '/..' . '/wp-cli/i18n-command/src', ), 'WP_CLI\\Embeds\\' => array ( 0 => __DIR__ . '/..' . '/wp-cli/embed-command/src', ), 'WP_CLI\\' => array ( 0 => __DIR__ . '/..' . '/wp-cli/entity-command/src/WP_CLI', 1 => __DIR__ . '/..' . '/wp-cli/shell-command/src/WP_CLI', ), 'Symfony\\Polyfill\\Mbstring\\' => array ( 0 => __DIR__ . '/..' . '/symfony/polyfill-mbstring', ), 'Symfony\\Polyfill\\Ctype\\' => array ( 0 => __DIR__ . '/..' . '/symfony/polyfill-ctype', ), 'Symfony\\Component\\Process\\' => array ( 0 => __DIR__ . '/..' . '/symfony/process', ), 'Symfony\\Component\\Finder\\' => array ( 0 => __DIR__ . '/..' . '/symfony/finder', ), 'Symfony\\Component\\Filesystem\\' => array ( 0 => __DIR__ . '/..' . '/symfony/filesystem', ), 'Symfony\\Component\\Debug\\' => array ( ), 'Symfony\\Component\\Console\\' => array ( 0 => __DIR__ . '/..' . '/symfony/console', ), 'Seld\\PharUtils\\' => array ( 0 => __DIR__ . '/..' . '/seld/phar-utils/src', ), 'Seld\\JsonLint\\' => array ( 0 => __DIR__ . '/..' . '/seld/jsonlint/src/Seld/JsonLint', ), 'React\\Promise\\' => array ( 0 => __DIR__ . '/..' . '/react/promise/src', ), 'Psr\\Log\\' => array ( 0 => __DIR__ . '/..' . '/psr/log/Psr/Log', ), 'Peast\\test\\' => array ( 0 => __DIR__ . '/..' . '/mck89/peast/test/Peast', ), 'Peast\\' => array ( 0 => __DIR__ . '/..' . '/mck89/peast/lib/Peast', ), 'Mustangostang\\' => array ( 0 => __DIR__ . '/..' . '/wp-cli/mustangostang-spyc/src', ), 'JsonSchema\\' => array ( 0 => __DIR__ . '/..' . '/justinrainbow/json-schema/src/JsonSchema', ), 'Gettext\\Languages\\' => array ( 0 => __DIR__ . '/..' . '/gettext/languages/src', ), 'Gettext\\' => array ( 0 => __DIR__ . '/..' . '/gettext/gettext/src', ), 'Composer\\XdebugHandler\\' => array ( 0 => __DIR__ . '/..' . '/composer/xdebug-handler/src', ), 'Composer\\Spdx\\' => array ( ), 'Composer\\Semver\\' => array ( 0 => __DIR__ . '/..' . '/composer/semver/src', ), 'Composer\\Pcre\\' => array ( 0 => __DIR__ . '/..' . '/composer/pcre/src', ), 'Composer\\MetadataMinifier\\' => array ( 0 => __DIR__ . '/..' . '/composer/metadata-minifier/src', ), 'Composer\\CaBundle\\' => array ( 0 => __DIR__ . '/..' . '/composer/ca-bundle/src', ), 'Composer\\' => array ( 0 => __DIR__ . '/..' . '/composer/composer/src/Composer', ), ); public static $fallbackDirsPsr4 = array ( 0 => __DIR__ . '/..' . '/wp-cli/cache-command/src', 1 => __DIR__ . '/..' . '/wp-cli/config-command/src', 2 => __DIR__ . '/..' . '/wp-cli/core-command/src', 3 => __DIR__ . '/..' . '/wp-cli/checksum-command/src', 4 => __DIR__ . '/..' . '/wp-cli/cron-command/src', 5 => __DIR__ . '/..' . '/wp-cli/db-command/src', 6 => __DIR__ . '/..' . '/wp-cli/eval-command/src', 7 => __DIR__ . '/..' . '/wp-cli/export-command/src', 8 => __DIR__ . '/..' . '/wp-cli/entity-command/src', 9 => __DIR__ . '/..' . '/wp-cli/extension-command/src', 10 => __DIR__ . '/..' . '/wp-cli/import-command/src', 11 => __DIR__ . '/..' . '/wp-cli/language-command/src', 12 => __DIR__ . '/..' . '/wp-cli/media-command/src', 13 => __DIR__ . '/..' . '/wp-cli/package-command/src', 14 => __DIR__ . '/..' . '/wp-cli/rewrite-command/src', 15 => __DIR__ . '/..' . '/wp-cli/role-command/src', 16 => __DIR__ . '/..' . '/wp-cli/search-replace-command/src', 17 => __DIR__ . '/..' . '/wp-cli/server-command/src', 18 => __DIR__ . '/..' . '/wp-cli/scaffold-command/src', 19 => __DIR__ . '/..' . '/wp-cli/shell-command/src', 20 => __DIR__ . '/..' . '/wp-cli/super-admin-command/src', 21 => __DIR__ . '/..' . '/wp-cli/widget-command/src', ); public static $prefixesPsr0 = array ( 'c' => array ( 'cli' => array ( 0 => __DIR__ . '/..' . '/wp-cli/php-cli-tools/lib', ), ), 'W' => array ( 'WP_CLI\\' => array ( 0 => __DIR__ . '/..' . '/wp-cli/wp-cli/php', ), ), 'R' => array ( 'Requests' => array ( 0 => __DIR__ . '/..' . '/rmccue/requests/library', ), ), 'O' => array ( 'Oxymel' => array ( 0 => __DIR__ . '/..' . '/nb/oxymel', ), ), 'M' => array ( 'Mustache' => array ( 0 => __DIR__ . '/..' . '/mustache/mustache/src', ), ), ); public static $classMap = array ( 'Composer\\InstalledVersions' => __DIR__ . '/..' . '/composer/InstalledVersions.php', 'WP_CLI' => __DIR__ . '/..' . '/wp-cli/wp-cli/php/class-wp-cli.php', 'WP_CLI_Command' => __DIR__ . '/..' . '/wp-cli/wp-cli/php/class-wp-cli-command.php', ); public static function getInitializer(ClassLoader $loader) { return \Closure::bind(function () use ($loader) { $loader->prefixLengthsPsr4 = ComposerStaticInit04114110af87f5624b3580eba43b8770::$prefixLengthsPsr4; $loader->prefixDirsPsr4 = ComposerStaticInit04114110af87f5624b3580eba43b8770::$prefixDirsPsr4; $loader->fallbackDirsPsr4 = ComposerStaticInit04114110af87f5624b3580eba43b8770::$fallbackDirsPsr4; $loader->prefixesPsr0 = ComposerStaticInit04114110af87f5624b3580eba43b8770::$prefixesPsr0; $loader->classMap = ComposerStaticInit04114110af87f5624b3580eba43b8770::$classMap; }, null, ClassLoader::class); } } * * For the full copyright and license information, please view * the LICENSE file that was distributed with this source code. */ namespace Composer\Pcre; class PcreException extends \RuntimeException { /** * @param string $function * @param string|string[] $pattern * @return self */ public static function fromFunction($function, $pattern) { $code = preg_last_error(); if (is_array($pattern)) { $pattern = implode(', ', $pattern); } return new PcreException($function.'(): failed executing "'.$pattern.'": '.self::pcreLastErrorMessage($code), $code); } /** * @param int $code * @return string */ private static function pcreLastErrorMessage($code) { if (PHP_VERSION_ID >= 80000) { return preg_last_error_msg(); } // older php versions did not set the code properly in all cases if (PHP_VERSION_ID < 70201 && $code === 0) { return 'UNDEFINED_ERROR'; } $constants = get_defined_constants(true); if (!isset($constants['pcre'])) { return 'UNDEFINED_ERROR'; } foreach ($constants['pcre'] as $const => $val) { if ($val === $code && substr($const, -6) === '_ERROR') { return $const; } } return 'UNDEFINED_ERROR'; } } * * For the full copyright and license information, please view * the LICENSE file that was distributed with this source code. */ namespace Composer\Pcre; final class MatchAllWithOffsetsResult { /** * An array of match group => list of matches, every match being a pair of string matched + offset in bytes (or -1 if no match) * * @readonly * @var array> * @phpstan-var array}>> */ public $matches; /** * @readonly * @var 0|positive-int */ public $count; /** * @readonly * @var bool */ public $matched; /** * @param 0|positive-int $count * @param array> $matches * @phpstan-param array}>> $matches */ public function __construct($count, array $matches) { $this->matches = $matches; $this->matched = (bool) $count; $this->count = $count; } } * * For the full copyright and license information, please view * the LICENSE file that was distributed with this source code. */ namespace Composer\Pcre; class Regex { /** * @param non-empty-string $pattern * @param string $subject * @param int $offset * @return bool */ public static function isMatch($pattern, $subject, $offset = 0) { return (bool) Preg::match($pattern, $subject, $matches, 0, $offset); } /** * @param non-empty-string $pattern * @param string $subject * @param int $flags PREG_UNMATCHED_AS_NULL, only available on PHP 7.2+ * @param int $offset * @return MatchResult */ public static function match($pattern, $subject, $flags = 0, $offset = 0) { if (($flags & PREG_OFFSET_CAPTURE) !== 0) { throw new \InvalidArgumentException('PREG_OFFSET_CAPTURE is not supported as it changes the return type, use matchWithOffsets() instead'); } $count = Preg::match($pattern, $subject, $matches, $flags, $offset); return new MatchResult($count, $matches); } /** * Runs preg_match with PREG_OFFSET_CAPTURE * * @param non-empty-string $pattern * @param string $subject * @param int $flags PREG_UNMATCHED_AS_NULL, only available on PHP 7.2+ * @param int $offset * @return MatchWithOffsetsResult */ public static function matchWithOffsets($pattern, $subject, $flags = 0, $offset = 0) { $count = Preg::matchWithOffsets($pattern, $subject, $matches, $flags, $offset); return new MatchWithOffsetsResult($count, $matches); } /** * @param non-empty-string $pattern * @param string $subject * @param int $flags PREG_UNMATCHED_AS_NULL, only available on PHP 7.2+ * @param int $offset * @return MatchAllResult */ public static function matchAll($pattern, $subject, $flags = 0, $offset = 0) { if (($flags & PREG_OFFSET_CAPTURE) !== 0) { throw new \InvalidArgumentException('PREG_OFFSET_CAPTURE is not supported as it changes the return type, use matchAllWithOffsets() instead'); } if (($flags & PREG_SET_ORDER) !== 0) { throw new \InvalidArgumentException('PREG_SET_ORDER is not supported as it changes the return type'); } $count = Preg::matchAll($pattern, $subject, $matches, $flags, $offset); return new MatchAllResult($count, $matches); } /** * Runs preg_match_all with PREG_OFFSET_CAPTURE * * @param non-empty-string $pattern * @param string $subject * @param int $flags PREG_UNMATCHED_AS_NULL, only available on PHP 7.2+ * @param int $offset * @return MatchAllWithOffsetsResult */ public static function matchAllWithOffsets($pattern, $subject, $flags = 0, $offset = 0) { $count = Preg::matchAllWithOffsets($pattern, $subject, $matches, $flags, $offset); return new MatchAllWithOffsetsResult($count, $matches); } /** * @param string|string[] $pattern * @param string|string[] $replacement * @param string $subject * @param int $limit * @return ReplaceResult */ public static function replace($pattern, $replacement, $subject, $limit = -1) { $result = Preg::replace($pattern, $replacement, $subject, $limit, $count); return new ReplaceResult($count, $result); } /** * @param string|string[] $pattern * @param callable $replacement * @param string $subject * @param int $limit * @param int $flags PREG_OFFSET_CAPTURE or PREG_UNMATCHED_AS_NULL, only available on PHP 7.4+ * @return ReplaceResult */ public static function replaceCallback($pattern, $replacement, $subject, $limit = -1, $flags = 0) { $result = Preg::replaceCallback($pattern, $replacement, $subject, $limit, $count, $flags); return new ReplaceResult($count, $result); } /** * Available from PHP 7.0 * * @param array $pattern * @param string $subject * @param int $limit * @param int $flags PREG_OFFSET_CAPTURE or PREG_UNMATCHED_AS_NULL, only available on PHP 7.4+ * @return ReplaceResult */ public static function replaceCallbackArray($pattern, $subject, $limit = -1, $flags = 0) { $result = Preg::replaceCallbackArray($pattern, $subject, $limit, $count, $flags); return new ReplaceResult($count, $result); } } * * For the full copyright and license information, please view * the LICENSE file that was distributed with this source code. */ namespace Composer\Pcre; class Preg { const ARRAY_MSG = '$subject as an array is not supported. You can use \'foreach\' instead.'; /** * @param non-empty-string $pattern * @param string $subject * @param array $matches Set by method * @param int $flags PREG_UNMATCHED_AS_NULL, only available on PHP 7.2+ * @param int $offset * @return 0|1 */ public static function match($pattern, $subject, &$matches = null, $flags = 0, $offset = 0) { if (($flags & PREG_OFFSET_CAPTURE) !== 0) { throw new \InvalidArgumentException('PREG_OFFSET_CAPTURE is not supported as it changes the type of $matches, use matchWithOffsets() instead'); } $result = preg_match($pattern, $subject, $matches, $flags, $offset); if ($result === false) { throw PcreException::fromFunction('preg_match', $pattern); } return $result; } /** * Runs preg_match with PREG_OFFSET_CAPTURE * * @param non-empty-string $pattern * @param string $subject * @param array $matches Set by method * @param int $flags PREG_UNMATCHED_AS_NULL, only available on PHP 7.2+ * @param int $offset * @return 0|1 * * @phpstan-param array}> $matches */ public static function matchWithOffsets($pattern, $subject, &$matches, $flags = 0, $offset = 0) { $result = preg_match($pattern, $subject, $matches, $flags | PREG_OFFSET_CAPTURE, $offset); if ($result === false) { throw PcreException::fromFunction('preg_match', $pattern); } return $result; } /** * @param non-empty-string $pattern * @param string $subject * @param array> $matches Set by method * @param int $flags PREG_UNMATCHED_AS_NULL, only available on PHP 7.2+ * @param int $offset * @return 0|positive-int */ public static function matchAll($pattern, $subject, &$matches = null, $flags = 0, $offset = 0) { if (($flags & PREG_OFFSET_CAPTURE) !== 0) { throw new \InvalidArgumentException('PREG_OFFSET_CAPTURE is not supported as it changes the type of $matches, use matchAllWithOffsets() instead'); } if (($flags & PREG_SET_ORDER) !== 0) { throw new \InvalidArgumentException('PREG_SET_ORDER is not supported as it changes the type of $matches'); } $result = preg_match_all($pattern, $subject, $matches, $flags, $offset); if ($result === false || $result === null) { throw PcreException::fromFunction('preg_match_all', $pattern); } return $result; } /** * Runs preg_match_all with PREG_OFFSET_CAPTURE * * @param non-empty-string $pattern * @param string $subject * @param array> $matches Set by method * @param int $flags PREG_UNMATCHED_AS_NULL, only available on PHP 7.2+ * @param int $offset * @return 0|positive-int * * @phpstan-param array}>> $matches */ public static function matchAllWithOffsets($pattern, $subject, &$matches, $flags = 0, $offset = 0) { $result = preg_match_all($pattern, $subject, $matches, $flags | PREG_OFFSET_CAPTURE, $offset); if ($result === false || $result === null) { throw PcreException::fromFunction('preg_match_all', $pattern); } return $result; } /** * @param string|string[] $pattern * @param string|string[] $replacement * @param string $subject * @param int $limit * @param int $count Set by method * @return string */ public static function replace($pattern, $replacement, $subject, $limit = -1, &$count = null) { if (is_array($subject)) { // @phpstan-ignore-line throw new \InvalidArgumentException(static::ARRAY_MSG); } $result = preg_replace($pattern, $replacement, $subject, $limit, $count); if ($result === null) { throw PcreException::fromFunction('preg_replace', $pattern); } return $result; } /** * @param string|string[] $pattern * @param callable $replacement * @param string $subject * @param int $limit * @param int $count Set by method * @param int $flags PREG_OFFSET_CAPTURE or PREG_UNMATCHED_AS_NULL, only available on PHP 7.4+ * @return string */ public static function replaceCallback($pattern, $replacement, $subject, $limit = -1, &$count = null, $flags = 0) { if (is_array($subject)) { // @phpstan-ignore-line throw new \InvalidArgumentException(static::ARRAY_MSG); } if (PHP_VERSION_ID >= 70400) { $result = preg_replace_callback($pattern, $replacement, $subject, $limit, $count, $flags); } else { $result = preg_replace_callback($pattern, $replacement, $subject, $limit, $count); } if ($result === null) { throw PcreException::fromFunction('preg_replace_callback', $pattern); } return $result; } /** * Available from PHP 7.0 * * @param array $pattern * @param string $subject * @param int $limit * @param int $count Set by method * @param int $flags PREG_OFFSET_CAPTURE or PREG_UNMATCHED_AS_NULL, only available on PHP 7.4+ * @return string */ public static function replaceCallbackArray(array $pattern, $subject, $limit = -1, &$count = null, $flags = 0) { if (is_array($subject)) { // @phpstan-ignore-line throw new \InvalidArgumentException(static::ARRAY_MSG); } if (PHP_VERSION_ID >= 70400) { $result = preg_replace_callback_array($pattern, $subject, $limit, $count, $flags); } else { $result = preg_replace_callback_array($pattern, $subject, $limit, $count); } if ($result === null) { $pattern = array_keys($pattern); throw PcreException::fromFunction('preg_replace_callback_array', $pattern); } return $result; } /** * @param string $pattern * @param string $subject * @param int $limit * @param int $flags PREG_SPLIT_NO_EMPTY or PREG_SPLIT_DELIM_CAPTURE * @return list */ public static function split($pattern, $subject, $limit = -1, $flags = 0) { if (($flags & PREG_SPLIT_OFFSET_CAPTURE) !== 0) { throw new \InvalidArgumentException('PREG_SPLIT_OFFSET_CAPTURE is not supported as it changes the type of $matches, use splitWithOffsets() instead'); } $result = preg_split($pattern, $subject, $limit, $flags); if ($result === false) { throw PcreException::fromFunction('preg_split', $pattern); } return $result; } /** * @param string $pattern * @param string $subject * @param int $limit * @param int $flags PREG_SPLIT_NO_EMPTY or PREG_SPLIT_DELIM_CAPTURE * @return list * @phpstan-return list}> */ public static function splitWithOffsets($pattern, $subject, $limit = -1, $flags = 0) { $result = preg_split($pattern, $subject, $limit, $flags | PREG_SPLIT_OFFSET_CAPTURE); if ($result === false) { throw PcreException::fromFunction('preg_split', $pattern); } return $result; } /** * @template T of string|\Stringable * @param string $pattern * @param array $array * @param int $flags PREG_GREP_INVERT * @return array */ public static function grep($pattern, array $array, $flags = 0) { $result = preg_grep($pattern, $array, $flags); if ($result === false) { throw PcreException::fromFunction('preg_grep', $pattern); } return $result; } /** * @param non-empty-string $pattern * @param string $subject * @param array $matches Set by method * @param int $flags PREG_UNMATCHED_AS_NULL, only available on PHP 7.2+ * @param int $offset * @return bool */ public static function isMatch($pattern, $subject, &$matches = null, $flags = 0, $offset = 0) { return (bool) static::match($pattern, $subject, $matches, $flags, $offset); } /** * @param non-empty-string $pattern * @param string $subject * @param array> $matches Set by method * @param int $flags PREG_UNMATCHED_AS_NULL, only available on PHP 7.2+ * @param int $offset * @return bool */ public static function isMatchAll($pattern, $subject, &$matches = null, $flags = 0, $offset = 0) { return (bool) static::matchAll($pattern, $subject, $matches, $flags, $offset); } /** * Runs preg_match with PREG_OFFSET_CAPTURE * * @param non-empty-string $pattern * @param string $subject * @param array $matches Set by method * @param int $flags PREG_UNMATCHED_AS_NULL, only available on PHP 7.2+ * @param int $offset * @return bool * * @phpstan-param array}> $matches */ public static function isMatchWithOffsets($pattern, $subject, &$matches, $flags = 0, $offset = 0) { return (bool) static::matchWithOffsets($pattern, $subject, $matches, $flags, $offset); } /** * Runs preg_match_all with PREG_OFFSET_CAPTURE * * @param non-empty-string $pattern * @param string $subject * @param array> $matches Set by method * @param int $flags PREG_UNMATCHED_AS_NULL, only available on PHP 7.2+ * @param int $offset * @return bool * * @phpstan-param array}>> $matches */ public static function isMatchAllWithOffsets($pattern, $subject, &$matches, $flags = 0, $offset = 0) { return (bool) static::matchAllWithOffsets($pattern, $subject, $matches, $flags, $offset); } } * * For the full copyright and license information, please view * the LICENSE file that was distributed with this source code. */ namespace Composer\Pcre; final class MatchResult { /** * An array of match group => string matched * * @readonly * @var array */ public $matches; /** * @readonly * @var bool */ public $matched; /** * @param 0|positive-int $count * @param array $matches */ public function __construct($count, array $matches) { $this->matches = $matches; $this->matched = (bool) $count; } } * * For the full copyright and license information, please view * the LICENSE file that was distributed with this source code. */ namespace Composer\Pcre; final class MatchAllResult { /** * An array of match group => list of matched strings * * @readonly * @var array> */ public $matches; /** * @readonly * @var 0|positive-int */ public $count; /** * @readonly * @var bool */ public $matched; /** * @param 0|positive-int $count * @param array> $matches */ public function __construct($count, array $matches) { $this->matches = $matches; $this->matched = (bool) $count; $this->count = $count; } } * * For the full copyright and license information, please view * the LICENSE file that was distributed with this source code. */ namespace Composer\Pcre; final class ReplaceResult { /** * @readonly * @var string */ public $result; /** * @readonly * @var 0|positive-int */ public $count; /** * @readonly * @var bool */ public $matched; /** * @param 0|positive-int $count * @param string $result */ public function __construct($count, $result) { $this->count = $count; $this->matched = (bool) $count; $this->result = $result; } } * * For the full copyright and license information, please view * the LICENSE file that was distributed with this source code. */ namespace Composer\Pcre; final class MatchWithOffsetsResult { /** * An array of match group => pair of string matched + offset in bytes (or -1 if no match) * * @readonly * @var array * @phpstan-var array}> */ public $matches; /** * @readonly * @var bool */ public $matched; /** * @param 0|positive-int $count * @param array $matches * @phpstan-param array}> $matches */ public function __construct($count, array $matches) { $this->matches = $matches; $this->matched = (bool) $count; } } array($vendorDir . '/wp-cli/php-cli-tools/lib'), 'WP_CLI\\' => array($vendorDir . '/wp-cli/wp-cli/php'), 'Requests' => array($vendorDir . '/rmccue/requests/library'), 'Oxymel' => array($vendorDir . '/nb/oxymel'), 'Mustache' => array($vendorDir . '/mustache/mustache/src'), ); * Jordi Boggiano * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Composer; use Composer\Autoload\ClassLoader; use Composer\Semver\VersionParser; /** * This class is copied in every Composer installed project and available to all * * See also https://getcomposer.org/doc/07-runtime.md#installed-versions * * To require its presence, you can require `composer-runtime-api ^2.0` */ class InstalledVersions { /** * @var mixed[]|null * @psalm-var array{root: array{name: string, version: string, reference: string, pretty_version: string, aliases: string[], dev: bool, install_path: string, type: string}, versions: array}|array{}|null */ private static $installed; /** * @var bool|null */ private static $canGetVendors; /** * @var array[] * @psalm-var array}> */ private static $installedByVendor = array(); /** * Returns a list of all package names which are present, either by being installed, replaced or provided * * @return string[] * @psalm-return list */ public static function getInstalledPackages() { $packages = array(); foreach (self::getInstalled() as $installed) { $packages[] = array_keys($installed['versions']); } if (1 === \count($packages)) { return $packages[0]; } return array_keys(array_flip(\call_user_func_array('array_merge', $packages))); } /** * Returns a list of all package names with a specific type e.g. 'library' * * @param string $type * @return string[] * @psalm-return list */ public static function getInstalledPackagesByType($type) { $packagesByType = array(); foreach (self::getInstalled() as $installed) { foreach ($installed['versions'] as $name => $package) { if (isset($package['type']) && $package['type'] === $type) { $packagesByType[] = $name; } } } return $packagesByType; } /** * Checks whether the given package is installed * * This also returns true if the package name is provided or replaced by another package * * @param string $packageName * @param bool $includeDevRequirements * @return bool */ public static function isInstalled($packageName, $includeDevRequirements = true) { foreach (self::getInstalled() as $installed) { if (isset($installed['versions'][$packageName])) { return $includeDevRequirements || empty($installed['versions'][$packageName]['dev_requirement']); } } return false; } /** * Checks whether the given package satisfies a version constraint * * e.g. If you want to know whether version 2.3+ of package foo/bar is installed, you would call: * * Composer\InstalledVersions::satisfies(new VersionParser, 'foo/bar', '^2.3') * * @param VersionParser $parser Install composer/semver to have access to this class and functionality * @param string $packageName * @param string|null $constraint A version constraint to check for, if you pass one you have to make sure composer/semver is required by your package * @return bool */ public static function satisfies(VersionParser $parser, $packageName, $constraint) { $constraint = $parser->parseConstraints($constraint); $provided = $parser->parseConstraints(self::getVersionRanges($packageName)); return $provided->matches($constraint); } /** * Returns a version constraint representing all the range(s) which are installed for a given package * * It is easier to use this via isInstalled() with the $constraint argument if you need to check * whether a given version of a package is installed, and not just whether it exists * * @param string $packageName * @return string Version constraint usable with composer/semver */ public static function getVersionRanges($packageName) { foreach (self::getInstalled() as $installed) { if (!isset($installed['versions'][$packageName])) { continue; } $ranges = array(); if (isset($installed['versions'][$packageName]['pretty_version'])) { $ranges[] = $installed['versions'][$packageName]['pretty_version']; } if (array_key_exists('aliases', $installed['versions'][$packageName])) { $ranges = array_merge($ranges, $installed['versions'][$packageName]['aliases']); } if (array_key_exists('replaced', $installed['versions'][$packageName])) { $ranges = array_merge($ranges, $installed['versions'][$packageName]['replaced']); } if (array_key_exists('provided', $installed['versions'][$packageName])) { $ranges = array_merge($ranges, $installed['versions'][$packageName]['provided']); } return implode(' || ', $ranges); } throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed'); } /** * @param string $packageName * @return string|null If the package is being replaced or provided but is not really installed, null will be returned as version, use satisfies or getVersionRanges if you need to know if a given version is present */ public static function getVersion($packageName) { foreach (self::getInstalled() as $installed) { if (!isset($installed['versions'][$packageName])) { continue; } if (!isset($installed['versions'][$packageName]['version'])) { return null; } return $installed['versions'][$packageName]['version']; } throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed'); } /** * @param string $packageName * @return string|null If the package is being replaced or provided but is not really installed, null will be returned as version, use satisfies or getVersionRanges if you need to know if a given version is present */ public static function getPrettyVersion($packageName) { foreach (self::getInstalled() as $installed) { if (!isset($installed['versions'][$packageName])) { continue; } if (!isset($installed['versions'][$packageName]['pretty_version'])) { return null; } return $installed['versions'][$packageName]['pretty_version']; } throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed'); } /** * @param string $packageName * @return string|null If the package is being replaced or provided but is not really installed, null will be returned as reference */ public static function getReference($packageName) { foreach (self::getInstalled() as $installed) { if (!isset($installed['versions'][$packageName])) { continue; } if (!isset($installed['versions'][$packageName]['reference'])) { return null; } return $installed['versions'][$packageName]['reference']; } throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed'); } /** * @param string $packageName * @return string|null If the package is being replaced or provided but is not really installed, null will be returned as install path. Packages of type metapackages also have a null install path. */ public static function getInstallPath($packageName) { foreach (self::getInstalled() as $installed) { if (!isset($installed['versions'][$packageName])) { continue; } return isset($installed['versions'][$packageName]['install_path']) ? $installed['versions'][$packageName]['install_path'] : null; } throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed'); } /** * @return array * @psalm-return array{name: string, version: string, reference: string, pretty_version: string, aliases: string[], dev: bool, install_path: string, type: string} */ public static function getRootPackage() { $installed = self::getInstalled(); return $installed[0]['root']; } /** * Returns the raw installed.php data for custom implementations * * @deprecated Use getAllRawData() instead which returns all datasets for all autoloaders present in the process. getRawData only returns the first dataset loaded, which may not be what you expect. * @return array[] * @psalm-return array{root: array{name: string, version: string, reference: string, pretty_version: string, aliases: string[], dev: bool, install_path: string, type: string}, versions: array} */ public static function getRawData() { @trigger_error('getRawData only returns the first dataset loaded, which may not be what you expect. Use getAllRawData() instead which returns all datasets for all autoloaders present in the process.', E_USER_DEPRECATED); if (null === self::$installed) { // only require the installed.php file if this file is loaded from its dumped location, // and not from its source location in the composer/composer package, see https://github.com/composer/composer/issues/9937 if (substr(__DIR__, -8, 1) !== 'C') { self::$installed = include __DIR__ . '/installed.php'; } else { self::$installed = array(); } } return self::$installed; } /** * Returns the raw data of all installed.php which are currently loaded for custom implementations * * @return array[] * @psalm-return list}> */ public static function getAllRawData() { return self::getInstalled(); } /** * Lets you reload the static array from another file * * This is only useful for complex integrations in which a project needs to use * this class but then also needs to execute another project's autoloader in process, * and wants to ensure both projects have access to their version of installed.php. * * A typical case would be PHPUnit, where it would need to make sure it reads all * the data it needs from this class, then call reload() with * `require $CWD/vendor/composer/installed.php` (or similar) as input to make sure * the project in which it runs can then also use this class safely, without * interference between PHPUnit's dependencies and the project's dependencies. * * @param array[] $data A vendor/composer/installed.php data set * @return void * * @psalm-param array{root: array{name: string, version: string, reference: string, pretty_version: string, aliases: string[], dev: bool, install_path: string, type: string}, versions: array} $data */ public static function reload($data) { self::$installed = $data; self::$installedByVendor = array(); } /** * @return array[] * @psalm-return list}> */ private static function getInstalled() { if (null === self::$canGetVendors) { self::$canGetVendors = method_exists('Composer\Autoload\ClassLoader', 'getRegisteredLoaders'); } $installed = array(); if (self::$canGetVendors) { foreach (ClassLoader::getRegisteredLoaders() as $vendorDir => $loader) { if (isset(self::$installedByVendor[$vendorDir])) { $installed[] = self::$installedByVendor[$vendorDir]; } elseif (is_file($vendorDir.'/composer/installed.php')) { $installed[] = self::$installedByVendor[$vendorDir] = require $vendorDir.'/composer/installed.php'; if (null === self::$installed && strtr($vendorDir.'/composer', '\\', '/') === strtr(__DIR__, '\\', '/')) { self::$installed = $installed[count($installed) - 1]; } } } } if (null === self::$installed) { // only require the installed.php file if this file is loaded from its dumped location, // and not from its source location in the composer/composer package, see https://github.com/composer/composer/issues/9937 if (substr(__DIR__, -8, 1) !== 'C') { self::$installed = require __DIR__ . '/installed.php'; } else { self::$installed = array(); } } $installed[] = self::$installed; return $installed; } } $vendorDir . '/symfony/polyfill-ctype/bootstrap.php', '0e6d7bf4a5811bfa5cf40c5ccd6fae6a' => $vendorDir . '/symfony/polyfill-mbstring/bootstrap.php', 'ad155f8f1cf0d418fe49e248db8c661b' => $vendorDir . '/react/promise/src/functions_include.php', 'be01b9b16925dcb22165c40b46681ac6' => $vendorDir . '/wp-cli/php-cli-tools/lib/cli/cli.php', '3937806105cc8e221b8fa8db5b70d2f2' => $vendorDir . '/wp-cli/mustangostang-spyc/includes/functions.php', 'ba366f96f4fddbdef61ad7a862b44f61' => $vendorDir . '/wp-cli/package-command/package-command.php', '8a0ad02df6a5087f2c380f8fd52db273' => $vendorDir . '/wp-cli/cache-command/cache-command.php', 'f3f0199a3ecd9f501d0a3b361bd2f61c' => $vendorDir . '/wp-cli/entity-command/entity-command.php', '3f201033d5aceb2293314273be88f7c6' => $vendorDir . '/wp-cli/extension-command/extension-command.php', 'c65f753375faee349b7adc48c2ee7cc2' => $vendorDir . '/wp-cli/db-command/db-command.php', '5e099d3cac677dd2bec1003ea7707745' => $vendorDir . '/wp-cli/media-command/media-command.php', 'd979c11fe80ba96ae3037b43429fe546' => $vendorDir . '/wp-cli/scaffold-command/scaffold-command.php', '5c6ec5cff8f9d625772c8ed147f6b894' => $vendorDir . '/wp-cli/export-command/export-command.php', 'ac949ce40a981819ba132473518a9a31' => $vendorDir . '/wp-cli/wp-config-transformer/src/WPConfigTransformer.php', 'b66d29757fcb2fb7a9608d068e3716b0' => $vendorDir . '/wp-cli/checksum-command/checksum-command.php', '30cbb6e4122dc988e494c6b9c0438233' => $vendorDir . '/wp-cli/import-command/import-command.php', '9f04dd0aa5d67ec75a75c88c345a079e' => $vendorDir . '/wp-cli/server-command/server-command.php', '8519779bbb65eeb842af2f629ce7b6f8' => $vendorDir . '/wp-cli/super-admin-command/super-admin-command.php', '5deaf6ce9c8bbdfb65104c7e938d5875' => $vendorDir . '/wp-cli/config-command/config-command.php', '68c39b88215b6cf7a0da164166670ef9' => $vendorDir . '/wp-cli/core-command/core-command.php', '7654e00bf0e632580764400bd8293a9c' => $vendorDir . '/wp-cli/cron-command/cron-command.php', '021d3a13471556f0b57038d679f7f8ea' => $vendorDir . '/wp-cli/embed-command/embed-command.php', 'f958dca3f412fd7975da1700912a9321' => $vendorDir . '/wp-cli/eval-command/eval-command.php', 'ffb465a494c3101218c4417180c2c9a2' => $vendorDir . '/wp-cli/i18n-command/i18n-command.php', '1c88c1eff05217a8cac80c64c9ac2080' => $vendorDir . '/wp-cli/maintenance-mode-command/maintenance-mode-command.php', 'ace0d205db7f4135ec32132a0076d555' => $vendorDir . '/wp-cli/language-command/language-command.php', 'f399c1c8d0c787d5c94c09884cdd9762' => $vendorDir . '/wp-cli/rewrite-command/rewrite-command.php', '080fadd667195d055c5a23386f270261' => $vendorDir . '/wp-cli/role-command/role-command.php', '8ecb13f8bbc22b1b34d12b14ec01077a' => $vendorDir . '/wp-cli/search-replace-command/search-replace-command.php', '129d58fa8151374aceb8571bcaa97504' => $vendorDir . '/wp-cli/shell-command/shell-command.php', '1f05372afcc7d0c51a305cef1d56dd01' => $vendorDir . '/wp-cli/widget-command/widget-command.php', ); * * For the full copyright and license information, please view * the LICENSE file that was distributed with this source code. */ namespace Composer\Semver; use Composer\Semver\Constraint\ConstraintInterface; use Composer\Semver\Constraint\MatchAllConstraint; use Composer\Semver\Constraint\MultiConstraint; use Composer\Semver\Constraint\Constraint; /** * Version parser. * * @author Jordi Boggiano */ class VersionParser { /** * Regex to match pre-release data (sort of). * * Due to backwards compatibility: * - Instead of enforcing hyphen, an underscore, dot or nothing at all are also accepted. * - Only stabilities as recognized by Composer are allowed to precede a numerical identifier. * - Numerical-only pre-release identifiers are not supported, see tests. * * |--------------| * [major].[minor].[patch] -[pre-release] +[build-metadata] * * @var string */ private static $modifierRegex = '[._-]?(?:(stable|beta|b|RC|alpha|a|patch|pl|p)((?:[.-]?\d+)*+)?)?([.-]?dev)?'; /** @var string */ private static $stabilitiesRegex = 'stable|RC|beta|alpha|dev'; /** * Returns the stability of a version. * * @param string $version * * @return string * @phpstan-return 'stable'|'RC'|'beta'|'alpha'|'dev' */ public static function parseStability($version) { $version = (string) preg_replace('{#.+$}', '', $version); if (strpos($version, 'dev-') === 0 || '-dev' === substr($version, -4)) { return 'dev'; } preg_match('{' . self::$modifierRegex . '(?:\+.*)?$}i', strtolower($version), $match); if (!empty($match[3])) { return 'dev'; } if (!empty($match[1])) { if ('beta' === $match[1] || 'b' === $match[1]) { return 'beta'; } if ('alpha' === $match[1] || 'a' === $match[1]) { return 'alpha'; } if ('rc' === $match[1]) { return 'RC'; } } return 'stable'; } /** * @param string $stability * * @return string */ public static function normalizeStability($stability) { $stability = strtolower($stability); return $stability === 'rc' ? 'RC' : $stability; } /** * Normalizes a version string to be able to perform comparisons on it. * * @param string $version * @param string $fullVersion optional complete version string to give more context * * @throws \UnexpectedValueException * * @return string */ public function normalize($version, $fullVersion = null) { $version = trim($version); $origVersion = $version; if (null === $fullVersion) { $fullVersion = $version; } // strip off aliasing if (preg_match('{^([^,\s]++) ++as ++([^,\s]++)$}', $version, $match)) { $version = $match[1]; } // strip off stability flag if (preg_match('{@(?:' . self::$stabilitiesRegex . ')$}i', $version, $match)) { $version = substr($version, 0, strlen($version) - strlen($match[0])); } // normalize master/trunk/default branches to dev-name for BC with 1.x as these used to be valid constraints if (\in_array($version, array('master', 'trunk', 'default'), true)) { $version = 'dev-' . $version; } // if requirement is branch-like, use full name if (stripos($version, 'dev-') === 0) { return 'dev-' . substr($version, 4); } // strip off build metadata if (preg_match('{^([^,\s+]++)\+[^\s]++$}', $version, $match)) { $version = $match[1]; } // match classical versioning if (preg_match('{^v?(\d{1,5})(\.\d++)?(\.\d++)?(\.\d++)?' . self::$modifierRegex . '$}i', $version, $matches)) { $version = $matches[1] . (!empty($matches[2]) ? $matches[2] : '.0') . (!empty($matches[3]) ? $matches[3] : '.0') . (!empty($matches[4]) ? $matches[4] : '.0'); $index = 5; // match date(time) based versioning } elseif (preg_match('{^v?(\d{4}(?:[.:-]?\d{2}){1,6}(?:[.:-]?\d{1,3})?)' . self::$modifierRegex . '$}i', $version, $matches)) { $version = preg_replace('{\D}', '.', $matches[1]); $index = 2; } // add version modifiers if a version was matched if (isset($index)) { if (!empty($matches[$index])) { if ('stable' === $matches[$index]) { return $version; } $version .= '-' . $this->expandStability($matches[$index]) . (isset($matches[$index + 1]) && '' !== $matches[$index + 1] ? ltrim($matches[$index + 1], '.-') : ''); } if (!empty($matches[$index + 2])) { $version .= '-dev'; } return $version; } // match dev branches if (preg_match('{(.*?)[.-]?dev$}i', $version, $match)) { try { $normalized = $this->normalizeBranch($match[1]); // a branch ending with -dev is only valid if it is numeric // if it gets prefixed with dev- it means the branch name should // have had a dev- prefix already when passed to normalize if (strpos($normalized, 'dev-') === false) { return $normalized; } } catch (\Exception $e) { } } $extraMessage = ''; if (preg_match('{ +as +' . preg_quote($version) . '(?:@(?:'.self::$stabilitiesRegex.'))?$}', $fullVersion)) { $extraMessage = ' in "' . $fullVersion . '", the alias must be an exact version'; } elseif (preg_match('{^' . preg_quote($version) . '(?:@(?:'.self::$stabilitiesRegex.'))? +as +}', $fullVersion)) { $extraMessage = ' in "' . $fullVersion . '", the alias source must be an exact version, if it is a branch name you should prefix it with dev-'; } throw new \UnexpectedValueException('Invalid version string "' . $origVersion . '"' . $extraMessage); } /** * Extract numeric prefix from alias, if it is in numeric format, suitable for version comparison. * * @param string $branch Branch name (e.g. 2.1.x-dev) * * @return string|false Numeric prefix if present (e.g. 2.1.) or false */ public function parseNumericAliasPrefix($branch) { if (preg_match('{^(?P(\d++\\.)*\d++)(?:\.x)?-dev$}i', $branch, $matches)) { return $matches['version'] . '.'; } return false; } /** * Normalizes a branch name to be able to perform comparisons on it. * * @param string $name * * @return string */ public function normalizeBranch($name) { $name = trim($name); if (preg_match('{^v?(\d++)(\.(?:\d++|[xX*]))?(\.(?:\d++|[xX*]))?(\.(?:\d++|[xX*]))?$}i', $name, $matches)) { $version = ''; for ($i = 1; $i < 5; ++$i) { $version .= isset($matches[$i]) ? str_replace(array('*', 'X'), 'x', $matches[$i]) : '.x'; } return str_replace('x', '9999999', $version) . '-dev'; } return 'dev-' . $name; } /** * Normalizes a default branch name (i.e. master on git) to 9999999-dev. * * @param string $name * * @return string */ public function normalizeDefaultBranch($name) { if ($name === 'dev-master' || $name === 'dev-default' || $name === 'dev-trunk') { return '9999999-dev'; } return $name; } /** * Parses a constraint string into MultiConstraint and/or Constraint objects. * * @param string $constraints * * @return ConstraintInterface */ public function parseConstraints($constraints) { $prettyConstraint = $constraints; $orConstraints = preg_split('{\s*\|\|?\s*}', trim($constraints)); if (false === $orConstraints) { throw new \RuntimeException('Failed to preg_split string: '.$constraints); } $orGroups = array(); foreach ($orConstraints as $constraints) { $andConstraints = preg_split('{(?< ,]) *(? 1) { $constraintObjects = array(); foreach ($andConstraints as $constraint) { foreach ($this->parseConstraint($constraint) as $parsedConstraint) { $constraintObjects[] = $parsedConstraint; } } } else { $constraintObjects = $this->parseConstraint($andConstraints[0]); } if (1 === \count($constraintObjects)) { $constraint = $constraintObjects[0]; } else { $constraint = new MultiConstraint($constraintObjects); } $orGroups[] = $constraint; } $constraint = MultiConstraint::create($orGroups, false); $constraint->setPrettyString($prettyConstraint); return $constraint; } /** * @param string $constraint * * @throws \UnexpectedValueException * * @return array * * @phpstan-return non-empty-array */ private function parseConstraint($constraint) { // strip off aliasing if (preg_match('{^([^,\s]++) ++as ++([^,\s]++)$}', $constraint, $match)) { $constraint = $match[1]; } // strip @stability flags, and keep it for later use if (preg_match('{^([^,\s]*?)@(' . self::$stabilitiesRegex . ')$}i', $constraint, $match)) { $constraint = '' !== $match[1] ? $match[1] : '*'; if ($match[2] !== 'stable') { $stabilityModifier = $match[2]; } } // get rid of #refs as those are used by composer only if (preg_match('{^(dev-[^,\s@]+?|[^,\s@]+?\.x-dev)#.+$}i', $constraint, $match)) { $constraint = $match[1]; } if (preg_match('{^(v)?[xX*](\.[xX*])*$}i', $constraint, $match)) { if (!empty($match[1]) || !empty($match[2])) { return array(new Constraint('>=', '0.0.0.0-dev')); } return array(new MatchAllConstraint()); } $versionRegex = 'v?(\d++)(?:\.(\d++))?(?:\.(\d++))?(?:\.(\d++))?(?:' . self::$modifierRegex . '|\.([xX*][.-]?dev))(?:\+[^\s]+)?'; // Tilde Range // // Like wildcard constraints, unsuffixed tilde constraints say that they must be greater than the previous // version, to ensure that unstable instances of the current version are allowed. However, if a stability // suffix is added to the constraint, then a >= match on the current version is used instead. if (preg_match('{^~>?' . $versionRegex . '$}i', $constraint, $matches)) { if (strpos($constraint, '~>') === 0) { throw new \UnexpectedValueException( 'Could not parse version constraint ' . $constraint . ': ' . 'Invalid operator "~>", you probably meant to use the "~" operator' ); } // Work out which position in the version we are operating at if (isset($matches[4]) && '' !== $matches[4] && null !== $matches[4]) { $position = 4; } elseif (isset($matches[3]) && '' !== $matches[3] && null !== $matches[3]) { $position = 3; } elseif (isset($matches[2]) && '' !== $matches[2] && null !== $matches[2]) { $position = 2; } else { $position = 1; } // when matching 2.x-dev or 3.0.x-dev we have to shift the second or third number, despite no second/third number matching above if (!empty($matches[8])) { $position++; } // Calculate the stability suffix $stabilitySuffix = ''; if (empty($matches[5]) && empty($matches[7]) && empty($matches[8])) { $stabilitySuffix .= '-dev'; } $lowVersion = $this->normalize(substr($constraint . $stabilitySuffix, 1)); $lowerBound = new Constraint('>=', $lowVersion); // For upper bound, we increment the position of one more significance, // but highPosition = 0 would be illegal $highPosition = max(1, $position - 1); $highVersion = $this->manipulateVersionString($matches, $highPosition, 1) . '-dev'; $upperBound = new Constraint('<', $highVersion); return array( $lowerBound, $upperBound, ); } // Caret Range // // Allows changes that do not modify the left-most non-zero digit in the [major, minor, patch] tuple. // In other words, this allows patch and minor updates for versions 1.0.0 and above, patch updates for // versions 0.X >=0.1.0, and no updates for versions 0.0.X if (preg_match('{^\^' . $versionRegex . '($)}i', $constraint, $matches)) { // Work out which position in the version we are operating at if ('0' !== $matches[1] || '' === $matches[2] || null === $matches[2]) { $position = 1; } elseif ('0' !== $matches[2] || '' === $matches[3] || null === $matches[3]) { $position = 2; } else { $position = 3; } // Calculate the stability suffix $stabilitySuffix = ''; if (empty($matches[5]) && empty($matches[7]) && empty($matches[8])) { $stabilitySuffix .= '-dev'; } $lowVersion = $this->normalize(substr($constraint . $stabilitySuffix, 1)); $lowerBound = new Constraint('>=', $lowVersion); // For upper bound, we increment the position of one more significance, // but highPosition = 0 would be illegal $highVersion = $this->manipulateVersionString($matches, $position, 1) . '-dev'; $upperBound = new Constraint('<', $highVersion); return array( $lowerBound, $upperBound, ); } // X Range // // Any of X, x, or * may be used to "stand in" for one of the numeric values in the [major, minor, patch] tuple. // A partial version range is treated as an X-Range, so the special character is in fact optional. if (preg_match('{^v?(\d++)(?:\.(\d++))?(?:\.(\d++))?(?:\.[xX*])++$}', $constraint, $matches)) { if (isset($matches[3]) && '' !== $matches[3] && null !== $matches[3]) { $position = 3; } elseif (isset($matches[2]) && '' !== $matches[2] && null !== $matches[2]) { $position = 2; } else { $position = 1; } $lowVersion = $this->manipulateVersionString($matches, $position) . '-dev'; $highVersion = $this->manipulateVersionString($matches, $position, 1) . '-dev'; if ($lowVersion === '0.0.0.0-dev') { return array(new Constraint('<', $highVersion)); } return array( new Constraint('>=', $lowVersion), new Constraint('<', $highVersion), ); } // Hyphen Range // // Specifies an inclusive set. If a partial version is provided as the first version in the inclusive range, // then the missing pieces are replaced with zeroes. If a partial version is provided as the second version in // the inclusive range, then all versions that start with the supplied parts of the tuple are accepted, but // nothing that would be greater than the provided tuple parts. if (preg_match('{^(?P' . $versionRegex . ') +- +(?P' . $versionRegex . ')($)}i', $constraint, $matches)) { // Calculate the stability suffix $lowStabilitySuffix = ''; if (empty($matches[6]) && empty($matches[8]) && empty($matches[9])) { $lowStabilitySuffix = '-dev'; } $lowVersion = $this->normalize($matches['from']); $lowerBound = new Constraint('>=', $lowVersion . $lowStabilitySuffix); $empty = function ($x) { return ($x === 0 || $x === '0') ? false : empty($x); }; if ((!$empty($matches[12]) && !$empty($matches[13])) || !empty($matches[15]) || !empty($matches[17]) || !empty($matches[18])) { $highVersion = $this->normalize($matches['to']); $upperBound = new Constraint('<=', $highVersion); } else { $highMatch = array('', $matches[11], $matches[12], $matches[13], $matches[14]); // validate to version $this->normalize($matches['to']); $highVersion = $this->manipulateVersionString($highMatch, $empty($matches[12]) ? 1 : 2, 1) . '-dev'; $upperBound = new Constraint('<', $highVersion); } return array( $lowerBound, $upperBound, ); } // Basic Comparators if (preg_match('{^(<>|!=|>=?|<=?|==?)?\s*(.*)}', $constraint, $matches)) { try { try { $version = $this->normalize($matches[2]); } catch (\UnexpectedValueException $e) { // recover from an invalid constraint like foobar-dev which should be dev-foobar // except if the constraint uses a known operator, in which case it must be a parse error if (substr($matches[2], -4) === '-dev' && preg_match('{^[0-9a-zA-Z-./]+$}', $matches[2])) { $version = $this->normalize('dev-'.substr($matches[2], 0, -4)); } else { throw $e; } } $op = $matches[1] ?: '='; if ($op !== '==' && $op !== '=' && !empty($stabilityModifier) && self::parseStability($version) === 'stable') { $version .= '-' . $stabilityModifier; } elseif ('<' === $op || '>=' === $op) { if (!preg_match('/-' . self::$modifierRegex . '$/', strtolower($matches[2]))) { if (strpos($matches[2], 'dev-') !== 0) { $version .= '-dev'; } } } return array(new Constraint($matches[1] ?: '=', $version)); } catch (\Exception $e) { } } $message = 'Could not parse version constraint ' . $constraint; if (isset($e)) { $message .= ': ' . $e->getMessage(); } throw new \UnexpectedValueException($message); } /** * Increment, decrement, or simply pad a version number. * * Support function for {@link parseConstraint()} * * @param array $matches Array with version parts in array indexes 1,2,3,4 * @param int $position 1,2,3,4 - which segment of the version to increment/decrement * @param int $increment * @param string $pad The string to pad version parts after $position * * @return string|null The new version * * @phpstan-param string[] $matches */ private function manipulateVersionString(array $matches, $position, $increment = 0, $pad = '0') { for ($i = 4; $i > 0; --$i) { if ($i > $position) { $matches[$i] = $pad; } elseif ($i === $position && $increment) { $matches[$i] += $increment; // If $matches[$i] was 0, carry the decrement if ($matches[$i] < 0) { $matches[$i] = $pad; --$position; // Return null on a carry overflow if ($i === 1) { return null; } } } } return $matches[1] . '.' . $matches[2] . '.' . $matches[3] . '.' . $matches[4]; } /** * Expand shorthand stability string to long version. * * @param string $stability * * @return string */ private function expandStability($stability) { $stability = strtolower($stability); switch ($stability) { case 'a': return 'alpha'; case 'b': return 'beta'; case 'p': case 'pl': return 'patch'; case 'rc': return 'RC'; default: return $stability; } } } * * For the full copyright and license information, please view * the LICENSE file that was distributed with this source code. */ namespace Composer\Semver; use Composer\Semver\Constraint\Constraint; class Interval { /** @var Constraint */ private $start; /** @var Constraint */ private $end; public function __construct(Constraint $start, Constraint $end) { $this->start = $start; $this->end = $end; } /** * @return Constraint */ public function getStart() { return $this->start; } /** * @return Constraint */ public function getEnd() { return $this->end; } /** * @return Constraint */ public static function fromZero() { static $zero; if (null === $zero) { $zero = new Constraint('>=', '0.0.0.0-dev'); } return $zero; } /** * @return Constraint */ public static function untilPositiveInfinity() { static $positiveInfinity; if (null === $positiveInfinity) { $positiveInfinity = new Constraint('<', PHP_INT_MAX.'.0.0.0'); } return $positiveInfinity; } /** * @return self */ public static function any() { return new self(self::fromZero(), self::untilPositiveInfinity()); } /** * @return array{'names': string[], 'exclude': bool} */ public static function anyDev() { // any == exclude nothing return array('names' => array(), 'exclude' => true); } /** * @return array{'names': string[], 'exclude': bool} */ public static function noDev() { // nothing == no names included return array('names' => array(), 'exclude' => false); } } * * For the full copyright and license information, please view * the LICENSE file that was distributed with this source code. */ namespace Composer\Semver\Constraint; /** * Defines a constraint. */ class Constraint implements ConstraintInterface { /* operator integer values */ const OP_EQ = 0; const OP_LT = 1; const OP_LE = 2; const OP_GT = 3; const OP_GE = 4; const OP_NE = 5; /* operator string values */ const STR_OP_EQ = '=='; const STR_OP_EQ_ALT = '='; const STR_OP_LT = '<'; const STR_OP_LE = '<='; const STR_OP_GT = '>'; const STR_OP_GE = '>='; const STR_OP_NE = '!='; const STR_OP_NE_ALT = '<>'; /** * Operator to integer translation table. * * @var array * @phpstan-var array */ private static $transOpStr = array( '=' => self::OP_EQ, '==' => self::OP_EQ, '<' => self::OP_LT, '<=' => self::OP_LE, '>' => self::OP_GT, '>=' => self::OP_GE, '<>' => self::OP_NE, '!=' => self::OP_NE, ); /** * Integer to operator translation table. * * @var array * @phpstan-var array */ private static $transOpInt = array( self::OP_EQ => '==', self::OP_LT => '<', self::OP_LE => '<=', self::OP_GT => '>', self::OP_GE => '>=', self::OP_NE => '!=', ); /** * @var int * @phpstan-var self::OP_* */ protected $operator; /** @var string */ protected $version; /** @var string|null */ protected $prettyString; /** @var Bound */ protected $lowerBound; /** @var Bound */ protected $upperBound; /** * Sets operator and version to compare with. * * @param string $operator * @param string $version * * @throws \InvalidArgumentException if invalid operator is given. * * @phpstan-param self::STR_OP_* $operator */ public function __construct($operator, $version) { if (!isset(self::$transOpStr[$operator])) { throw new \InvalidArgumentException(sprintf( 'Invalid operator "%s" given, expected one of: %s', $operator, implode(', ', self::getSupportedOperators()) )); } $this->operator = self::$transOpStr[$operator]; $this->version = $version; } /** * @return string */ public function getVersion() { return $this->version; } /** * @return string * * @phpstan-return self::STR_OP_* */ public function getOperator() { return self::$transOpInt[$this->operator]; } /** * @param ConstraintInterface $provider * * @return bool */ public function matches(ConstraintInterface $provider) { if ($provider instanceof self) { return $this->matchSpecific($provider); } // turn matching around to find a match return $provider->matches($this); } /** * {@inheritDoc} */ public function setPrettyString($prettyString) { $this->prettyString = $prettyString; } /** * {@inheritDoc} */ public function getPrettyString() { if ($this->prettyString) { return $this->prettyString; } return $this->__toString(); } /** * Get all supported comparison operators. * * @return array * * @phpstan-return list */ public static function getSupportedOperators() { return array_keys(self::$transOpStr); } /** * @param string $operator * @return int * * @phpstan-param self::STR_OP_* $operator * @phpstan-return self::OP_* */ public static function getOperatorConstant($operator) { return self::$transOpStr[$operator]; } /** * @param string $a * @param string $b * @param string $operator * @param bool $compareBranches * * @throws \InvalidArgumentException if invalid operator is given. * * @return bool * * @phpstan-param self::STR_OP_* $operator */ public function versionCompare($a, $b, $operator, $compareBranches = false) { if (!isset(self::$transOpStr[$operator])) { throw new \InvalidArgumentException(sprintf( 'Invalid operator "%s" given, expected one of: %s', $operator, implode(', ', self::getSupportedOperators()) )); } $aIsBranch = strpos($a, 'dev-') === 0; $bIsBranch = strpos($b, 'dev-') === 0; if ($operator === '!=' && ($aIsBranch || $bIsBranch)) { return $a !== $b; } if ($aIsBranch && $bIsBranch) { return $operator === '==' && $a === $b; } // when branches are not comparable, we make sure dev branches never match anything if (!$compareBranches && ($aIsBranch || $bIsBranch)) { return false; } return \version_compare($a, $b, $operator); } /** * {@inheritDoc} */ public function compile($otherOperator) { if (strpos($this->version, 'dev-') === 0) { if (self::OP_EQ === $this->operator) { if (self::OP_EQ === $otherOperator) { return sprintf('$b && $v === %s', \var_export($this->version, true)); } if (self::OP_NE === $otherOperator) { return sprintf('!$b || $v !== %s', \var_export($this->version, true)); } return 'false'; } if (self::OP_NE === $this->operator) { if (self::OP_EQ === $otherOperator) { return sprintf('!$b || $v !== %s', \var_export($this->version, true)); } if (self::OP_NE === $otherOperator) { return 'true'; } return '!$b'; } return 'false'; } if (self::OP_EQ === $this->operator) { if (self::OP_EQ === $otherOperator) { return sprintf('\version_compare($v, %s, \'==\')', \var_export($this->version, true)); } if (self::OP_NE === $otherOperator) { return sprintf('$b || \version_compare($v, %s, \'!=\')', \var_export($this->version, true)); } return sprintf('!$b && \version_compare(%s, $v, \'%s\')', \var_export($this->version, true), self::$transOpInt[$otherOperator]); } if (self::OP_NE === $this->operator) { if (self::OP_EQ === $otherOperator) { return sprintf('$b || (!$b && \version_compare($v, %s, \'!=\'))', \var_export($this->version, true)); } if (self::OP_NE === $otherOperator) { return 'true'; } return '!$b'; } if (self::OP_LT === $this->operator || self::OP_LE === $this->operator) { if (self::OP_LT === $otherOperator || self::OP_LE === $otherOperator) { return '!$b'; } } else { // $this->operator must be self::OP_GT || self::OP_GE here if (self::OP_GT === $otherOperator || self::OP_GE === $otherOperator) { return '!$b'; } } if (self::OP_NE === $otherOperator) { return 'true'; } $codeComparison = sprintf('\version_compare($v, %s, \'%s\')', \var_export($this->version, true), self::$transOpInt[$this->operator]); if ($this->operator === self::OP_LE) { if ($otherOperator === self::OP_GT) { return sprintf('!$b && \version_compare($v, %s, \'!=\') && ', \var_export($this->version, true)) . $codeComparison; } } elseif ($this->operator === self::OP_GE) { if ($otherOperator === self::OP_LT) { return sprintf('!$b && \version_compare($v, %s, \'!=\') && ', \var_export($this->version, true)) . $codeComparison; } } return sprintf('!$b && %s', $codeComparison); } /** * @param Constraint $provider * @param bool $compareBranches * * @return bool */ public function matchSpecific(Constraint $provider, $compareBranches = false) { $noEqualOp = str_replace('=', '', self::$transOpInt[$this->operator]); $providerNoEqualOp = str_replace('=', '', self::$transOpInt[$provider->operator]); $isEqualOp = self::OP_EQ === $this->operator; $isNonEqualOp = self::OP_NE === $this->operator; $isProviderEqualOp = self::OP_EQ === $provider->operator; $isProviderNonEqualOp = self::OP_NE === $provider->operator; // '!=' operator is match when other operator is not '==' operator or version is not match // these kinds of comparisons always have a solution if ($isNonEqualOp || $isProviderNonEqualOp) { if ($isNonEqualOp && !$isProviderNonEqualOp && !$isProviderEqualOp && strpos($provider->version, 'dev-') === 0) { return false; } if ($isProviderNonEqualOp && !$isNonEqualOp && !$isEqualOp && strpos($this->version, 'dev-') === 0) { return false; } if (!$isEqualOp && !$isProviderEqualOp) { return true; } return $this->versionCompare($provider->version, $this->version, '!=', $compareBranches); } // an example for the condition is <= 2.0 & < 1.0 // these kinds of comparisons always have a solution if ($this->operator !== self::OP_EQ && $noEqualOp === $providerNoEqualOp) { return !(strpos($this->version, 'dev-') === 0 || strpos($provider->version, 'dev-') === 0); } $version1 = $isEqualOp ? $this->version : $provider->version; $version2 = $isEqualOp ? $provider->version : $this->version; $operator = $isEqualOp ? $provider->operator : $this->operator; if ($this->versionCompare($version1, $version2, self::$transOpInt[$operator], $compareBranches)) { // special case, e.g. require >= 1.0 and provide < 1.0 // 1.0 >= 1.0 but 1.0 is outside of the provided interval return !(self::$transOpInt[$provider->operator] === $providerNoEqualOp && self::$transOpInt[$this->operator] !== $noEqualOp && \version_compare($provider->version, $this->version, '==')); } return false; } /** * @return string */ public function __toString() { return self::$transOpInt[$this->operator] . ' ' . $this->version; } /** * {@inheritDoc} */ public function getLowerBound() { $this->extractBounds(); return $this->lowerBound; } /** * {@inheritDoc} */ public function getUpperBound() { $this->extractBounds(); return $this->upperBound; } /** * @return void */ private function extractBounds() { if (null !== $this->lowerBound) { return; } // Branches if (strpos($this->version, 'dev-') === 0) { $this->lowerBound = Bound::zero(); $this->upperBound = Bound::positiveInfinity(); return; } switch ($this->operator) { case self::OP_EQ: $this->lowerBound = new Bound($this->version, true); $this->upperBound = new Bound($this->version, true); break; case self::OP_LT: $this->lowerBound = Bound::zero(); $this->upperBound = new Bound($this->version, false); break; case self::OP_LE: $this->lowerBound = Bound::zero(); $this->upperBound = new Bound($this->version, true); break; case self::OP_GT: $this->lowerBound = new Bound($this->version, false); $this->upperBound = Bound::positiveInfinity(); break; case self::OP_GE: $this->lowerBound = new Bound($this->version, true); $this->upperBound = Bound::positiveInfinity(); break; case self::OP_NE: $this->lowerBound = Bound::zero(); $this->upperBound = Bound::positiveInfinity(); break; } } } * * For the full copyright and license information, please view * the LICENSE file that was distributed with this source code. */ namespace Composer\Semver\Constraint; /** * Defines a conjunctive or disjunctive set of constraints. */ class MultiConstraint implements ConstraintInterface { /** * @var ConstraintInterface[] * @phpstan-var non-empty-array */ protected $constraints; /** @var string|null */ protected $prettyString; /** @var string|null */ protected $string; /** @var bool */ protected $conjunctive; /** @var Bound|null */ protected $lowerBound; /** @var Bound|null */ protected $upperBound; /** * @param ConstraintInterface[] $constraints A set of constraints * @param bool $conjunctive Whether the constraints should be treated as conjunctive or disjunctive * * @throws \InvalidArgumentException If less than 2 constraints are passed */ public function __construct(array $constraints, $conjunctive = true) { if (\count($constraints) < 2) { throw new \InvalidArgumentException( 'Must provide at least two constraints for a MultiConstraint. Use '. 'the regular Constraint class for one constraint only or MatchAllConstraint for none. You may use '. 'MultiConstraint::create() which optimizes and handles those cases automatically.' ); } $this->constraints = $constraints; $this->conjunctive = $conjunctive; } /** * @return ConstraintInterface[] */ public function getConstraints() { return $this->constraints; } /** * @return bool */ public function isConjunctive() { return $this->conjunctive; } /** * @return bool */ public function isDisjunctive() { return !$this->conjunctive; } /** * {@inheritDoc} */ public function compile($otherOperator) { $parts = array(); foreach ($this->constraints as $constraint) { $code = $constraint->compile($otherOperator); if ($code === 'true') { if (!$this->conjunctive) { return 'true'; } } elseif ($code === 'false') { if ($this->conjunctive) { return 'false'; } } else { $parts[] = '('.$code.')'; } } if (!$parts) { return $this->conjunctive ? 'true' : 'false'; } return $this->conjunctive ? implode('&&', $parts) : implode('||', $parts); } /** * @param ConstraintInterface $provider * * @return bool */ public function matches(ConstraintInterface $provider) { if (false === $this->conjunctive) { foreach ($this->constraints as $constraint) { if ($provider->matches($constraint)) { return true; } } return false; } // when matching a conjunctive and a disjunctive multi constraint we have to iterate over the disjunctive one // otherwise we'd return true if different parts of the disjunctive constraint match the conjunctive one // which would lead to incorrect results, e.g. [>1 and <2] would match [<1 or >2] although they do not intersect if ($provider instanceof MultiConstraint && $provider->isDisjunctive()) { return $provider->matches($this); } foreach ($this->constraints as $constraint) { if (!$provider->matches($constraint)) { return false; } } return true; } /** * {@inheritDoc} */ public function setPrettyString($prettyString) { $this->prettyString = $prettyString; } /** * {@inheritDoc} */ public function getPrettyString() { if ($this->prettyString) { return $this->prettyString; } return (string) $this; } /** * {@inheritDoc} */ public function __toString() { if ($this->string !== null) { return $this->string; } $constraints = array(); foreach ($this->constraints as $constraint) { $constraints[] = (string) $constraint; } return $this->string = '[' . implode($this->conjunctive ? ' ' : ' || ', $constraints) . ']'; } /** * {@inheritDoc} */ public function getLowerBound() { $this->extractBounds(); if (null === $this->lowerBound) { throw new \LogicException('extractBounds should have populated the lowerBound property'); } return $this->lowerBound; } /** * {@inheritDoc} */ public function getUpperBound() { $this->extractBounds(); if (null === $this->upperBound) { throw new \LogicException('extractBounds should have populated the upperBound property'); } return $this->upperBound; } /** * Tries to optimize the constraints as much as possible, meaning * reducing/collapsing congruent constraints etc. * Does not necessarily return a MultiConstraint instance if * things can be reduced to a simple constraint * * @param ConstraintInterface[] $constraints A set of constraints * @param bool $conjunctive Whether the constraints should be treated as conjunctive or disjunctive * * @return ConstraintInterface */ public static function create(array $constraints, $conjunctive = true) { if (0 === \count($constraints)) { return new MatchAllConstraint(); } if (1 === \count($constraints)) { return $constraints[0]; } $optimized = self::optimizeConstraints($constraints, $conjunctive); if ($optimized !== null) { list($constraints, $conjunctive) = $optimized; if (\count($constraints) === 1) { return $constraints[0]; } } return new self($constraints, $conjunctive); } /** * @param ConstraintInterface[] $constraints * @param bool $conjunctive * @return ?array * * @phpstan-return array{0: list, 1: bool}|null */ private static function optimizeConstraints(array $constraints, $conjunctive) { // parse the two OR groups and if they are contiguous we collapse // them into one constraint // [>= 1 < 2] || [>= 2 < 3] || [>= 3 < 4] => [>= 1 < 4] if (!$conjunctive) { $left = $constraints[0]; $mergedConstraints = array(); $optimized = false; for ($i = 1, $l = \count($constraints); $i < $l; $i++) { $right = $constraints[$i]; if ( $left instanceof self && $left->conjunctive && $right instanceof self && $right->conjunctive && \count($left->constraints) === 2 && \count($right->constraints) === 2 && ($left0 = (string) $left->constraints[0]) && $left0[0] === '>' && $left0[1] === '=' && ($left1 = (string) $left->constraints[1]) && $left1[0] === '<' && ($right0 = (string) $right->constraints[0]) && $right0[0] === '>' && $right0[1] === '=' && ($right1 = (string) $right->constraints[1]) && $right1[0] === '<' && substr($left1, 2) === substr($right0, 3) ) { $optimized = true; $left = new MultiConstraint( array( $left->constraints[0], $right->constraints[1], ), true); } else { $mergedConstraints[] = $left; $left = $right; } } if ($optimized) { $mergedConstraints[] = $left; return array($mergedConstraints, false); } } // TODO: Here's the place to put more optimizations return null; } /** * @return void */ private function extractBounds() { if (null !== $this->lowerBound) { return; } foreach ($this->constraints as $constraint) { if (null === $this->lowerBound || null === $this->upperBound) { $this->lowerBound = $constraint->getLowerBound(); $this->upperBound = $constraint->getUpperBound(); continue; } if ($constraint->getLowerBound()->compareTo($this->lowerBound, $this->isConjunctive() ? '>' : '<')) { $this->lowerBound = $constraint->getLowerBound(); } if ($constraint->getUpperBound()->compareTo($this->upperBound, $this->isConjunctive() ? '<' : '>')) { $this->upperBound = $constraint->getUpperBound(); } } } } * * For the full copyright and license information, please view * the LICENSE file that was distributed with this source code. */ namespace Composer\Semver\Constraint; /** * DO NOT IMPLEMENT this interface. It is only meant for usage as a type hint * in libraries relying on composer/semver but creating your own constraint class * that implements this interface is not a supported use case and will cause the * composer/semver components to return unexpected results. */ interface ConstraintInterface { /** * Checks whether the given constraint intersects in any way with this constraint * * @param ConstraintInterface $provider * * @return bool */ public function matches(ConstraintInterface $provider); /** * Provides a compiled version of the constraint for the given operator * The compiled version must be a PHP expression. * Executor of compile version must provide 2 variables: * - $v = the string version to compare with * - $b = whether or not the version is a non-comparable branch (starts with "dev-") * * @see Constraint::OP_* for the list of available operators. * @example return '!$b && version_compare($v, '1.0', '>')'; * * @param int $otherOperator one Constraint::OP_* * * @return string * * @phpstan-param Constraint::OP_* $otherOperator */ public function compile($otherOperator); /** * @return Bound */ public function getUpperBound(); /** * @return Bound */ public function getLowerBound(); /** * @return string */ public function getPrettyString(); /** * @param string|null $prettyString * * @return void */ public function setPrettyString($prettyString); /** * @return string */ public function __toString(); } * * For the full copyright and license information, please view * the LICENSE file that was distributed with this source code. */ namespace Composer\Semver\Constraint; class Bound { /** * @var string */ private $version; /** * @var bool */ private $isInclusive; /** * @param string $version * @param bool $isInclusive */ public function __construct($version, $isInclusive) { $this->version = $version; $this->isInclusive = $isInclusive; } /** * @return string */ public function getVersion() { return $this->version; } /** * @return bool */ public function isInclusive() { return $this->isInclusive; } /** * @return bool */ public function isZero() { return $this->getVersion() === '0.0.0.0-dev' && $this->isInclusive(); } /** * @return bool */ public function isPositiveInfinity() { return $this->getVersion() === PHP_INT_MAX.'.0.0.0' && !$this->isInclusive(); } /** * Compares a bound to another with a given operator. * * @param Bound $other * @param string $operator * * @return bool */ public function compareTo(Bound $other, $operator) { if (!\in_array($operator, array('<', '>'), true)) { throw new \InvalidArgumentException('Does not support any other operator other than > or <.'); } // If they are the same it doesn't matter if ($this == $other) { return false; } $compareResult = version_compare($this->getVersion(), $other->getVersion()); // Not the same version means we don't need to check if the bounds are inclusive or not if (0 !== $compareResult) { return (('>' === $operator) ? 1 : -1) === $compareResult; } // Question we're answering here is "am I higher than $other?" return '>' === $operator ? $other->isInclusive() : !$other->isInclusive(); } public function __toString() { return sprintf( '%s [%s]', $this->getVersion(), $this->isInclusive() ? 'inclusive' : 'exclusive' ); } /** * @return self */ public static function zero() { return new Bound('0.0.0.0-dev', true); } /** * @return self */ public static function positiveInfinity() { return new Bound(PHP_INT_MAX.'.0.0.0', false); } } * * For the full copyright and license information, please view * the LICENSE file that was distributed with this source code. */ namespace Composer\Semver\Constraint; /** * Blackhole of constraints, nothing escapes it */ class MatchNoneConstraint implements ConstraintInterface { /** @var string|null */ protected $prettyString; /** * @param ConstraintInterface $provider * * @return bool */ public function matches(ConstraintInterface $provider) { return false; } /** * {@inheritDoc} */ public function compile($otherOperator) { return 'false'; } /** * {@inheritDoc} */ public function setPrettyString($prettyString) { $this->prettyString = $prettyString; } /** * {@inheritDoc} */ public function getPrettyString() { if ($this->prettyString) { return $this->prettyString; } return (string) $this; } /** * {@inheritDoc} */ public function __toString() { return '[]'; } /** * {@inheritDoc} */ public function getUpperBound() { return new Bound('0.0.0.0-dev', false); } /** * {@inheritDoc} */ public function getLowerBound() { return new Bound('0.0.0.0-dev', false); } } * * For the full copyright and license information, please view * the LICENSE file that was distributed with this source code. */ namespace Composer\Semver\Constraint; /** * Defines the absence of a constraint. * * This constraint matches everything. */ class MatchAllConstraint implements ConstraintInterface { /** @var string|null */ protected $prettyString; /** * @param ConstraintInterface $provider * * @return bool */ public function matches(ConstraintInterface $provider) { return true; } /** * {@inheritDoc} */ public function compile($otherOperator) { return 'true'; } /** * {@inheritDoc} */ public function setPrettyString($prettyString) { $this->prettyString = $prettyString; } /** * {@inheritDoc} */ public function getPrettyString() { if ($this->prettyString) { return $this->prettyString; } return (string) $this; } /** * {@inheritDoc} */ public function __toString() { return '*'; } /** * {@inheritDoc} */ public function getUpperBound() { return Bound::positiveInfinity(); } /** * {@inheritDoc} */ public function getLowerBound() { return Bound::zero(); } } * * For the full copyright and license information, please view * the LICENSE file that was distributed with this source code. */ namespace Composer\Semver; use Composer\Semver\Constraint\Constraint; use Composer\Semver\Constraint\ConstraintInterface; use Composer\Semver\Constraint\MatchAllConstraint; use Composer\Semver\Constraint\MatchNoneConstraint; use Composer\Semver\Constraint\MultiConstraint; /** * Helper class generating intervals from constraints * * This contains utilities for: * * - compacting an existing constraint which can be used to combine several into one * by creating a MultiConstraint out of the many constraints you have. * * - checking whether one subset is a subset of another. * * Note: You should call clear to free memoization memory usage when you are done using this class */ class Intervals { /** * @phpstan-var array */ private static $intervalsCache = array(); /** * @phpstan-var array */ private static $opSortOrder = array( '>=' => -3, '<' => -2, '>' => 2, '<=' => 3, ); /** * Clears the memoization cache once you are done * * @return void */ public static function clear() { self::$intervalsCache = array(); } /** * Checks whether $candidate is a subset of $constraint * * @return bool */ public static function isSubsetOf(ConstraintInterface $candidate, ConstraintInterface $constraint) { if ($constraint instanceof MatchAllConstraint) { return true; } if ($candidate instanceof MatchNoneConstraint || $constraint instanceof MatchNoneConstraint) { return false; } $intersectionIntervals = self::get(new MultiConstraint(array($candidate, $constraint), true)); $candidateIntervals = self::get($candidate); if (\count($intersectionIntervals['numeric']) !== \count($candidateIntervals['numeric'])) { return false; } foreach ($intersectionIntervals['numeric'] as $index => $interval) { if (!isset($candidateIntervals['numeric'][$index])) { return false; } if ((string) $candidateIntervals['numeric'][$index]->getStart() !== (string) $interval->getStart()) { return false; } if ((string) $candidateIntervals['numeric'][$index]->getEnd() !== (string) $interval->getEnd()) { return false; } } if ($intersectionIntervals['branches']['exclude'] !== $candidateIntervals['branches']['exclude']) { return false; } if (\count($intersectionIntervals['branches']['names']) !== \count($candidateIntervals['branches']['names'])) { return false; } foreach ($intersectionIntervals['branches']['names'] as $index => $name) { if ($name !== $candidateIntervals['branches']['names'][$index]) { return false; } } return true; } /** * Checks whether $a and $b have any intersection, equivalent to $a->matches($b) * * @return bool */ public static function haveIntersections(ConstraintInterface $a, ConstraintInterface $b) { if ($a instanceof MatchAllConstraint || $b instanceof MatchAllConstraint) { return true; } if ($a instanceof MatchNoneConstraint || $b instanceof MatchNoneConstraint) { return false; } $intersectionIntervals = self::generateIntervals(new MultiConstraint(array($a, $b), true), true); return \count($intersectionIntervals['numeric']) > 0 || $intersectionIntervals['branches']['exclude'] || \count($intersectionIntervals['branches']['names']) > 0; } /** * Attempts to optimize a MultiConstraint * * When merging MultiConstraints together they can get very large, this will * compact it by looking at the real intervals covered by all the constraints * and then creates a new constraint containing only the smallest amount of rules * to match the same intervals. * * @return ConstraintInterface */ public static function compactConstraint(ConstraintInterface $constraint) { if (!$constraint instanceof MultiConstraint) { return $constraint; } $intervals = self::generateIntervals($constraint); $constraints = array(); $hasNumericMatchAll = false; if (\count($intervals['numeric']) === 1 && (string) $intervals['numeric'][0]->getStart() === (string) Interval::fromZero() && (string) $intervals['numeric'][0]->getEnd() === (string) Interval::untilPositiveInfinity()) { $constraints[] = $intervals['numeric'][0]->getStart(); $hasNumericMatchAll = true; } else { $unEqualConstraints = array(); for ($i = 0, $count = \count($intervals['numeric']); $i < $count; $i++) { $interval = $intervals['numeric'][$i]; // if current interval ends with < N and next interval begins with > N we can swap this out for != N // but this needs to happen as a conjunctive expression together with the start of the current interval // and end of next interval, so [>=M, N, [>=M, !=N, getEnd()->getOperator() === '<' && $i+1 < $count) { $nextInterval = $intervals['numeric'][$i+1]; if ($interval->getEnd()->getVersion() === $nextInterval->getStart()->getVersion() && $nextInterval->getStart()->getOperator() === '>') { // only add a start if we didn't already do so, can be skipped if we're looking at second // interval in [>=M, N, P, =M, !=N] already and we only want to add !=P right now if (\count($unEqualConstraints) === 0 && (string) $interval->getStart() !== (string) Interval::fromZero()) { $unEqualConstraints[] = $interval->getStart(); } $unEqualConstraints[] = new Constraint('!=', $interval->getEnd()->getVersion()); continue; } } if (\count($unEqualConstraints) > 0) { // this is where the end of the following interval of a != constraint is added as explained above if ((string) $interval->getEnd() !== (string) Interval::untilPositiveInfinity()) { $unEqualConstraints[] = $interval->getEnd(); } // count is 1 if entire constraint is just one != expression if (\count($unEqualConstraints) > 1) { $constraints[] = new MultiConstraint($unEqualConstraints, true); } else { $constraints[] = $unEqualConstraints[0]; } $unEqualConstraints = array(); continue; } // convert back >= x - <= x intervals to == x if ($interval->getStart()->getVersion() === $interval->getEnd()->getVersion() && $interval->getStart()->getOperator() === '>=' && $interval->getEnd()->getOperator() === '<=') { $constraints[] = new Constraint('==', $interval->getStart()->getVersion()); continue; } if ((string) $interval->getStart() === (string) Interval::fromZero()) { $constraints[] = $interval->getEnd(); } elseif ((string) $interval->getEnd() === (string) Interval::untilPositiveInfinity()) { $constraints[] = $interval->getStart(); } else { $constraints[] = new MultiConstraint(array($interval->getStart(), $interval->getEnd()), true); } } } $devConstraints = array(); if (0 === \count($intervals['branches']['names'])) { if ($intervals['branches']['exclude']) { if ($hasNumericMatchAll) { return new MatchAllConstraint; } // otherwise constraint should contain a != operator and already cover this } } else { foreach ($intervals['branches']['names'] as $branchName) { if ($intervals['branches']['exclude']) { $devConstraints[] = new Constraint('!=', $branchName); } else { $devConstraints[] = new Constraint('==', $branchName); } } // excluded branches, e.g. != dev-foo are conjunctive with the interval, so // > 2.0 != dev-foo must return a conjunctive constraint if ($intervals['branches']['exclude']) { if (\count($constraints) > 1) { return new MultiConstraint(array_merge( array(new MultiConstraint($constraints, false)), $devConstraints ), true); } if (\count($constraints) === 1 && (string)$constraints[0] === (string)Interval::fromZero()) { if (\count($devConstraints) > 1) { return new MultiConstraint($devConstraints, true); } return $devConstraints[0]; } return new MultiConstraint(array_merge($constraints, $devConstraints), true); } // otherwise devConstraints contains a list of == operators for branches which are disjunctive with the // rest of the constraint $constraints = array_merge($constraints, $devConstraints); } if (\count($constraints) > 1) { return new MultiConstraint($constraints, false); } if (\count($constraints) === 1) { return $constraints[0]; } return new MatchNoneConstraint; } /** * Creates an array of numeric intervals and branch constraints representing a given constraint * * if the returned numeric array is empty it means the constraint matches nothing in the numeric range (0 - +inf) * if the returned branches array is empty it means no dev-* versions are matched * if a constraint matches all possible dev-* versions, branches will contain Interval::anyDev() * * @return array * @phpstan-return array{'numeric': Interval[], 'branches': array{'names': string[], 'exclude': bool}} */ public static function get(ConstraintInterface $constraint) { $key = (string) $constraint; if (!isset(self::$intervalsCache[$key])) { self::$intervalsCache[$key] = self::generateIntervals($constraint); } return self::$intervalsCache[$key]; } /** * @param bool $stopOnFirstValidInterval * * @phpstan-return array{'numeric': Interval[], 'branches': array{'names': string[], 'exclude': bool}} */ private static function generateIntervals(ConstraintInterface $constraint, $stopOnFirstValidInterval = false) { if ($constraint instanceof MatchAllConstraint) { return array('numeric' => array(new Interval(Interval::fromZero(), Interval::untilPositiveInfinity())), 'branches' => Interval::anyDev()); } if ($constraint instanceof MatchNoneConstraint) { return array('numeric' => array(), 'branches' => array('names' => array(), 'exclude' => false)); } if ($constraint instanceof Constraint) { return self::generateSingleConstraintIntervals($constraint); } if (!$constraint instanceof MultiConstraint) { throw new \UnexpectedValueException('The constraint passed in should be an MatchAllConstraint, Constraint or MultiConstraint instance, got '.\get_class($constraint).'.'); } $constraints = $constraint->getConstraints(); $numericGroups = array(); $constraintBranches = array(); foreach ($constraints as $c) { $res = self::get($c); $numericGroups[] = $res['numeric']; $constraintBranches[] = $res['branches']; } if ($constraint->isDisjunctive()) { $branches = Interval::noDev(); foreach ($constraintBranches as $b) { if ($b['exclude']) { if ($branches['exclude']) { // disjunctive constraint, so only exclude what's excluded in all constraints // !=a,!=b || !=b,!=c => !=b $branches['names'] = array_intersect($branches['names'], $b['names']); } else { // disjunctive constraint so exclude all names which are not explicitly included in the alternative // (==b || ==c) || !=a,!=b => !=a $branches['exclude'] = true; $branches['names'] = array_diff($b['names'], $branches['names']); } } else { if ($branches['exclude']) { // disjunctive constraint so exclude all names which are not explicitly included in the alternative // !=a,!=b || (==b || ==c) => !=a $branches['names'] = array_diff($branches['names'], $b['names']); } else { // disjunctive constraint, so just add all the other branches // (==a || ==b) || ==c => ==a || ==b || ==c $branches['names'] = array_merge($branches['names'], $b['names']); } } } } else { $branches = Interval::anyDev(); foreach ($constraintBranches as $b) { if ($b['exclude']) { if ($branches['exclude']) { // conjunctive, so just add all branch names to be excluded // !=a && !=b => !=a,!=b $branches['names'] = array_merge($branches['names'], $b['names']); } else { // conjunctive, so only keep included names which are not excluded // (==a||==c) && !=a,!=b => ==c $branches['names'] = array_diff($branches['names'], $b['names']); } } else { if ($branches['exclude']) { // conjunctive, so only keep included names which are not excluded // !=a,!=b && (==a||==c) => ==c $branches['names'] = array_diff($b['names'], $branches['names']); $branches['exclude'] = false; } else { // conjunctive, so only keep names that are included in both // (==a||==b) && (==a||==c) => ==a $branches['names'] = array_intersect($branches['names'], $b['names']); } } } } $branches['names'] = array_unique($branches['names']); if (\count($numericGroups) === 1) { return array('numeric' => $numericGroups[0], 'branches' => $branches); } $borders = array(); foreach ($numericGroups as $group) { foreach ($group as $interval) { $borders[] = array('version' => $interval->getStart()->getVersion(), 'operator' => $interval->getStart()->getOperator(), 'side' => 'start'); $borders[] = array('version' => $interval->getEnd()->getVersion(), 'operator' => $interval->getEnd()->getOperator(), 'side' => 'end'); } } $opSortOrder = self::$opSortOrder; usort($borders, function ($a, $b) use ($opSortOrder) { $order = version_compare($a['version'], $b['version']); if ($order === 0) { return $opSortOrder[$a['operator']] - $opSortOrder[$b['operator']]; } return $order; }); $activeIntervals = 0; $intervals = array(); $index = 0; $activationThreshold = $constraint->isConjunctive() ? \count($numericGroups) : 1; $start = null; foreach ($borders as $border) { if ($border['side'] === 'start') { $activeIntervals++; } else { $activeIntervals--; } if (!$start && $activeIntervals >= $activationThreshold) { $start = new Constraint($border['operator'], $border['version']); } elseif ($start && $activeIntervals < $activationThreshold) { // filter out invalid intervals like > x - <= x, or >= x - < x if ( version_compare($start->getVersion(), $border['version'], '=') && ( ($start->getOperator() === '>' && $border['operator'] === '<=') || ($start->getOperator() === '>=' && $border['operator'] === '<') ) ) { unset($intervals[$index]); } else { $intervals[$index] = new Interval($start, new Constraint($border['operator'], $border['version'])); $index++; if ($stopOnFirstValidInterval) { break; } } $start = null; } } return array('numeric' => $intervals, 'branches' => $branches); } /** * @phpstan-return array{'numeric': Interval[], 'branches': array{'names': string[], 'exclude': bool}} */ private static function generateSingleConstraintIntervals(Constraint $constraint) { $op = $constraint->getOperator(); // handle branch constraints first if (strpos($constraint->getVersion(), 'dev-') === 0) { $intervals = array(); $branches = array('names' => array(), 'exclude' => false); // != dev-foo means any numeric version may match, we treat >/< like != they are not really defined for branches if ($op === '!=') { $intervals[] = new Interval(Interval::fromZero(), Interval::untilPositiveInfinity()); $branches = array('names' => array($constraint->getVersion()), 'exclude' => true); } elseif ($op === '==') { $branches['names'][] = $constraint->getVersion(); } return array( 'numeric' => $intervals, 'branches' => $branches, ); } if ($op[0] === '>') { // > & >= return array('numeric' => array(new Interval($constraint, Interval::untilPositiveInfinity())), 'branches' => Interval::noDev()); } if ($op[0] === '<') { // < & <= return array('numeric' => array(new Interval(Interval::fromZero(), $constraint)), 'branches' => Interval::noDev()); } if ($op === '!=') { // convert !=x to intervals of 0 - x - +inf + dev* return array('numeric' => array( new Interval(Interval::fromZero(), new Constraint('<', $constraint->getVersion())), new Interval(new Constraint('>', $constraint->getVersion()), Interval::untilPositiveInfinity()), ), 'branches' => Interval::anyDev()); } // convert ==x to an interval of >=x - <=x return array('numeric' => array( new Interval(new Constraint('>=', $constraint->getVersion()), new Constraint('<=', $constraint->getVersion())), ), 'branches' => Interval::noDev()); } } * * For the full copyright and license information, please view * the LICENSE file that was distributed with this source code. */ namespace Composer\Semver; use Composer\Semver\Constraint\Constraint; class Comparator { /** * Evaluates the expression: $version1 > $version2. * * @param string $version1 * @param string $version2 * * @return bool */ public static function greaterThan($version1, $version2) { return self::compare($version1, '>', $version2); } /** * Evaluates the expression: $version1 >= $version2. * * @param string $version1 * @param string $version2 * * @return bool */ public static function greaterThanOrEqualTo($version1, $version2) { return self::compare($version1, '>=', $version2); } /** * Evaluates the expression: $version1 < $version2. * * @param string $version1 * @param string $version2 * * @return bool */ public static function lessThan($version1, $version2) { return self::compare($version1, '<', $version2); } /** * Evaluates the expression: $version1 <= $version2. * * @param string $version1 * @param string $version2 * * @return bool */ public static function lessThanOrEqualTo($version1, $version2) { return self::compare($version1, '<=', $version2); } /** * Evaluates the expression: $version1 == $version2. * * @param string $version1 * @param string $version2 * * @return bool */ public static function equalTo($version1, $version2) { return self::compare($version1, '==', $version2); } /** * Evaluates the expression: $version1 != $version2. * * @param string $version1 * @param string $version2 * * @return bool */ public static function notEqualTo($version1, $version2) { return self::compare($version1, '!=', $version2); } /** * Evaluates the expression: $version1 $operator $version2. * * @param string $version1 * @param string $operator * @param string $version2 * * @return bool * * @phpstan-param Constraint::STR_OP_* $operator */ public static function compare($version1, $operator, $version2) { $constraint = new Constraint($operator, $version2); return $constraint->matchSpecific(new Constraint('==', $version1), true); } } * * For the full copyright and license information, please view * the LICENSE file that was distributed with this source code. */ namespace Composer\Semver; use Composer\Semver\Constraint\Constraint; use Composer\Semver\Constraint\ConstraintInterface; /** * Helper class to evaluate constraint by compiling and reusing the code to evaluate */ class CompilingMatcher { /** * @var array * @phpstan-var array */ private static $compiledCheckerCache = array(); /** @var bool */ private static $enabled; /** * @phpstan-var array */ private static $transOpInt = array( Constraint::OP_EQ => Constraint::STR_OP_EQ, Constraint::OP_LT => Constraint::STR_OP_LT, Constraint::OP_LE => Constraint::STR_OP_LE, Constraint::OP_GT => Constraint::STR_OP_GT, Constraint::OP_GE => Constraint::STR_OP_GE, Constraint::OP_NE => Constraint::STR_OP_NE, ); /** * Evaluates the expression: $constraint match $operator $version * * @param ConstraintInterface $constraint * @param int $operator * @phpstan-param Constraint::OP_* $operator * @param string $version * * @return mixed */ public static function match(ConstraintInterface $constraint, $operator, $version) { if (self::$enabled === null) { self::$enabled = !\in_array('eval', explode(',', (string) ini_get('disable_functions')), true); } if (!self::$enabled) { return $constraint->matches(new Constraint(self::$transOpInt[$operator], $version)); } $cacheKey = $operator.$constraint; if (!isset(self::$compiledCheckerCache[$cacheKey])) { $code = $constraint->compile($operator); self::$compiledCheckerCache[$cacheKey] = $function = eval('return function($v, $b){return '.$code.';};'); } else { $function = self::$compiledCheckerCache[$cacheKey]; } return $function($version, strpos($version, 'dev-') === 0); } } * * For the full copyright and license information, please view * the LICENSE file that was distributed with this source code. */ namespace Composer\Semver; use Composer\Semver\Constraint\Constraint; class Semver { const SORT_ASC = 1; const SORT_DESC = -1; /** @var VersionParser */ private static $versionParser; /** * Determine if given version satisfies given constraints. * * @param string $version * @param string $constraints * * @return bool */ public static function satisfies($version, $constraints) { if (null === self::$versionParser) { self::$versionParser = new VersionParser(); } $versionParser = self::$versionParser; $provider = new Constraint('==', $versionParser->normalize($version)); $parsedConstraints = $versionParser->parseConstraints($constraints); return $parsedConstraints->matches($provider); } /** * Return all versions that satisfy given constraints. * * @param string[] $versions * @param string $constraints * * @return string[] */ public static function satisfiedBy(array $versions, $constraints) { $versions = array_filter($versions, function ($version) use ($constraints) { return Semver::satisfies($version, $constraints); }); return array_values($versions); } /** * Sort given array of versions. * * @param string[] $versions * * @return string[] */ public static function sort(array $versions) { return self::usort($versions, self::SORT_ASC); } /** * Sort given array of versions in reverse. * * @param string[] $versions * * @return string[] */ public static function rsort(array $versions) { return self::usort($versions, self::SORT_DESC); } /** * @param string[] $versions * @param int $direction * * @return string[] */ private static function usort(array $versions, $direction) { if (null === self::$versionParser) { self::$versionParser = new VersionParser(); } $versionParser = self::$versionParser; $normalized = array(); // Normalize outside of usort() scope for minor performance increase. // Creates an array of arrays: [[normalized, key], ...] foreach ($versions as $key => $version) { $normalizedVersion = $versionParser->normalize($version); $normalizedVersion = $versionParser->normalizeDefaultBranch($normalizedVersion); $normalized[] = array($normalizedVersion, $key); } usort($normalized, function (array $left, array $right) use ($direction) { if ($left[0] === $right[0]) { return 0; } if (Comparator::lessThan($left[0], $right[0])) { return -$direction; } return $direction; }); // Recreate input array, using the original indexes which are now in sorted order. $sorted = array(); foreach ($normalized as $item) { $sorted[] = $versions[$item[1]]; } return $sorted; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Composer\MetadataMinifier; class MetadataMinifier { /** * Expands an array of minified versions back to their original format * * @param array[] $versions A list of minified version arrays * @return array[] A list of version arrays */ public static function expand(array $versions) { $expanded = array(); $expandedVersion = null; foreach ($versions as $versionData) { if (!$expandedVersion) { $expandedVersion = $versionData; $expanded[] = $expandedVersion; continue; } // add any changes from the previous version to the expanded one foreach ($versionData as $key => $val) { if ($val === '__unset') { unset($expandedVersion[$key]); } else { $expandedVersion[$key] = $val; } } $expanded[] = $expandedVersion; } return $expanded; } /** * Minifies an array of versions into a set of version diffs * * @param array[] $versions A list of version arrays * @return array[] A list of versions minified with each array only containing the differences to the previous one */ public static function minify(array $versions) { $minifiedVersions = array(); $lastKnownVersionData = null; foreach ($versions as $version) { if (!$lastKnownVersionData) { $lastKnownVersionData = $version; $minifiedVersions[] = $version; continue; } $minifiedVersion = array(); // add any changes from the previous version foreach ($version as $key => $val) { if (!isset($lastKnownVersionData[$key]) || $lastKnownVersionData[$key] !== $val) { $minifiedVersion[$key] = $val; $lastKnownVersionData[$key] = $val; } } // store any deletions from the previous version for keys missing in current one foreach ($lastKnownVersionData as $key => $val) { if (!isset($version[$key])) { $minifiedVersion[$key] = "__unset"; unset($lastKnownVersionData[$key]); } } $minifiedVersions[] = $minifiedVersion; } return $minifiedVersions; } } array($vendorDir . '/wp-cli/maintenance-mode-command/src'), 'WP_CLI\\I18n\\' => array($vendorDir . '/wp-cli/i18n-command/src'), 'WP_CLI\\Embeds\\' => array($vendorDir . '/wp-cli/embed-command/src'), 'WP_CLI\\' => array($vendorDir . '/wp-cli/entity-command/src/WP_CLI', $vendorDir . '/wp-cli/shell-command/src/WP_CLI'), 'Symfony\\Polyfill\\Mbstring\\' => array($vendorDir . '/symfony/polyfill-mbstring'), 'Symfony\\Polyfill\\Ctype\\' => array($vendorDir . '/symfony/polyfill-ctype'), 'Symfony\\Component\\Process\\' => array($vendorDir . '/symfony/process'), 'Symfony\\Component\\Finder\\' => array($vendorDir . '/symfony/finder'), 'Symfony\\Component\\Filesystem\\' => array($vendorDir . '/symfony/filesystem'), 'Symfony\\Component\\Console\\' => array($vendorDir . '/symfony/console'), 'Seld\\PharUtils\\' => array($vendorDir . '/seld/phar-utils/src'), 'Seld\\JsonLint\\' => array($vendorDir . '/seld/jsonlint/src/Seld/JsonLint'), 'React\\Promise\\' => array($vendorDir . '/react/promise/src'), 'Psr\\Log\\' => array($vendorDir . '/psr/log/Psr/Log'), 'Peast\\test\\' => array($vendorDir . '/mck89/peast/test/Peast'), 'Peast\\' => array($vendorDir . '/mck89/peast/lib/Peast'), 'Mustangostang\\' => array($vendorDir . '/wp-cli/mustangostang-spyc/src'), 'JsonSchema\\' => array($vendorDir . '/justinrainbow/json-schema/src/JsonSchema'), 'Gettext\\Languages\\' => array($vendorDir . '/gettext/languages/src'), 'Gettext\\' => array($vendorDir . '/gettext/gettext/src'), 'Composer\\XdebugHandler\\' => array($vendorDir . '/composer/xdebug-handler/src'), 'Composer\\Semver\\' => array($vendorDir . '/composer/semver/src'), 'Composer\\Pcre\\' => array($vendorDir . '/composer/pcre/src'), 'Composer\\MetadataMinifier\\' => array($vendorDir . '/composer/metadata-minifier/src'), 'Composer\\CaBundle\\' => array($vendorDir . '/composer/ca-bundle/src'), 'Composer\\' => array($vendorDir . '/composer/composer/src/Composer'), '' => array($vendorDir . '/wp-cli/cache-command/src', $vendorDir . '/wp-cli/config-command/src', $vendorDir . '/wp-cli/core-command/src', $vendorDir . '/wp-cli/checksum-command/src', $vendorDir . '/wp-cli/cron-command/src', $vendorDir . '/wp-cli/db-command/src', $vendorDir . '/wp-cli/eval-command/src', $vendorDir . '/wp-cli/export-command/src', $vendorDir . '/wp-cli/entity-command/src', $vendorDir . '/wp-cli/extension-command/src', $vendorDir . '/wp-cli/import-command/src', $vendorDir . '/wp-cli/language-command/src', $vendorDir . '/wp-cli/media-command/src', $vendorDir . '/wp-cli/package-command/src', $vendorDir . '/wp-cli/rewrite-command/src', $vendorDir . '/wp-cli/role-command/src', $vendorDir . '/wp-cli/search-replace-command/src', $vendorDir . '/wp-cli/server-command/src', $vendorDir . '/wp-cli/scaffold-command/src', $vendorDir . '/wp-cli/shell-command/src', $vendorDir . '/wp-cli/super-admin-command/src', $vendorDir . '/wp-cli/widget-command/src'), ); = 50600)) { $issues[] = 'Your Composer dependencies require a PHP version ">= 5.6.0". You are running ' . PHP_VERSION . '.'; } if ($issues) { if (!headers_sent()) { header('HTTP/1.1 500 Internal Server Error'); } if (!ini_get('display_errors')) { if (PHP_SAPI === 'cli' || PHP_SAPI === 'phpdbg') { fwrite(STDERR, 'Composer detected issues in your platform:' . PHP_EOL.PHP_EOL . implode(PHP_EOL, $issues) . PHP_EOL.PHP_EOL); } elseif (!headers_sent()) { echo 'Composer detected issues in your platform:' . PHP_EOL.PHP_EOL . str_replace('You are running '.PHP_VERSION.'.', '', implode(PHP_EOL, $issues)) . PHP_EOL.PHP_EOL; } } trigger_error( 'Composer detected issues in your platform: ' . implode(' ', $issues), E_USER_ERROR ); } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Polyfill\Ctype; /** * Ctype implementation through regex. * * @internal * * @author Gert de Pagter */ final class Ctype { /** * Returns TRUE if every character in text is either a letter or a digit, FALSE otherwise. * * @see https://php.net/ctype-alnum * * @param string|int $text * * @return bool */ public static function ctype_alnum($text) { $text = self::convert_int_to_char_for_ctype($text); return \is_string($text) && '' !== $text && !preg_match('/[^A-Za-z0-9]/', $text); } /** * Returns TRUE if every character in text is a letter, FALSE otherwise. * * @see https://php.net/ctype-alpha * * @param string|int $text * * @return bool */ public static function ctype_alpha($text) { $text = self::convert_int_to_char_for_ctype($text); return \is_string($text) && '' !== $text && !preg_match('/[^A-Za-z]/', $text); } /** * Returns TRUE if every character in text is a control character from the current locale, FALSE otherwise. * * @see https://php.net/ctype-cntrl * * @param string|int $text * * @return bool */ public static function ctype_cntrl($text) { $text = self::convert_int_to_char_for_ctype($text); return \is_string($text) && '' !== $text && !preg_match('/[^\x00-\x1f\x7f]/', $text); } /** * Returns TRUE if every character in the string text is a decimal digit, FALSE otherwise. * * @see https://php.net/ctype-digit * * @param string|int $text * * @return bool */ public static function ctype_digit($text) { $text = self::convert_int_to_char_for_ctype($text); return \is_string($text) && '' !== $text && !preg_match('/[^0-9]/', $text); } /** * Returns TRUE if every character in text is printable and actually creates visible output (no white space), FALSE otherwise. * * @see https://php.net/ctype-graph * * @param string|int $text * * @return bool */ public static function ctype_graph($text) { $text = self::convert_int_to_char_for_ctype($text); return \is_string($text) && '' !== $text && !preg_match('/[^!-~]/', $text); } /** * Returns TRUE if every character in text is a lowercase letter. * * @see https://php.net/ctype-lower * * @param string|int $text * * @return bool */ public static function ctype_lower($text) { $text = self::convert_int_to_char_for_ctype($text); return \is_string($text) && '' !== $text && !preg_match('/[^a-z]/', $text); } /** * Returns TRUE if every character in text will actually create output (including blanks). Returns FALSE if text contains control characters or characters that do not have any output or control function at all. * * @see https://php.net/ctype-print * * @param string|int $text * * @return bool */ public static function ctype_print($text) { $text = self::convert_int_to_char_for_ctype($text); return \is_string($text) && '' !== $text && !preg_match('/[^ -~]/', $text); } /** * Returns TRUE if every character in text is printable, but neither letter, digit or blank, FALSE otherwise. * * @see https://php.net/ctype-punct * * @param string|int $text * * @return bool */ public static function ctype_punct($text) { $text = self::convert_int_to_char_for_ctype($text); return \is_string($text) && '' !== $text && !preg_match('/[^!-\/\:-@\[-`\{-~]/', $text); } /** * Returns TRUE if every character in text creates some sort of white space, FALSE otherwise. Besides the blank character this also includes tab, vertical tab, line feed, carriage return and form feed characters. * * @see https://php.net/ctype-space * * @param string|int $text * * @return bool */ public static function ctype_space($text) { $text = self::convert_int_to_char_for_ctype($text); return \is_string($text) && '' !== $text && !preg_match('/[^\s]/', $text); } /** * Returns TRUE if every character in text is an uppercase letter. * * @see https://php.net/ctype-upper * * @param string|int $text * * @return bool */ public static function ctype_upper($text) { $text = self::convert_int_to_char_for_ctype($text); return \is_string($text) && '' !== $text && !preg_match('/[^A-Z]/', $text); } /** * Returns TRUE if every character in text is a hexadecimal 'digit', that is a decimal digit or a character from [A-Fa-f] , FALSE otherwise. * * @see https://php.net/ctype-xdigit * * @param string|int $text * * @return bool */ public static function ctype_xdigit($text) { $text = self::convert_int_to_char_for_ctype($text); return \is_string($text) && '' !== $text && !preg_match('/[^A-Fa-f0-9]/', $text); } /** * Converts integers to their char versions according to normal ctype behaviour, if needed. * * If an integer between -128 and 255 inclusive is provided, * it is interpreted as the ASCII value of a single character * (negative values have 256 added in order to allow characters in the Extended ASCII range). * Any other integer is interpreted as a string containing the decimal digits of the integer. * * @param string|int $int * * @return mixed */ private static function convert_int_to_char_for_ctype($int) { if (!\is_int($int)) { return $int; } if ($int < -128 || $int > 255) { return (string) $int; } if ($int < 0) { $int += 256; } return \chr($int); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ use Symfony\Polyfill\Ctype as p; if (!function_exists('ctype_alnum')) { function ctype_alnum($input) { return p\Ctype::ctype_alnum($input); } } if (!function_exists('ctype_alpha')) { function ctype_alpha($input) { return p\Ctype::ctype_alpha($input); } } if (!function_exists('ctype_cntrl')) { function ctype_cntrl($input) { return p\Ctype::ctype_cntrl($input); } } if (!function_exists('ctype_digit')) { function ctype_digit($input) { return p\Ctype::ctype_digit($input); } } if (!function_exists('ctype_graph')) { function ctype_graph($input) { return p\Ctype::ctype_graph($input); } } if (!function_exists('ctype_lower')) { function ctype_lower($input) { return p\Ctype::ctype_lower($input); } } if (!function_exists('ctype_print')) { function ctype_print($input) { return p\Ctype::ctype_print($input); } } if (!function_exists('ctype_punct')) { function ctype_punct($input) { return p\Ctype::ctype_punct($input); } } if (!function_exists('ctype_space')) { function ctype_space($input) { return p\Ctype::ctype_space($input); } } if (!function_exists('ctype_upper')) { function ctype_upper($input) { return p\Ctype::ctype_upper($input); } } if (!function_exists('ctype_xdigit')) { function ctype_xdigit($input) { return p\Ctype::ctype_xdigit($input); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Filesystem; use Symfony\Component\Filesystem\Exception\IOException; use Symfony\Component\Lock\Store\FlockStore; use Symfony\Component\Lock\Store\SemaphoreStore; @trigger_error(sprintf('The %s class is deprecated since Symfony 3.4 and will be removed in 4.0. Use %s or %s instead.', LockHandler::class, SemaphoreStore::class, FlockStore::class), \E_USER_DEPRECATED); /** * LockHandler class provides a simple abstraction to lock anything by means of * a file lock. * * A locked file is created based on the lock name when calling lock(). Other * lock handlers will not be able to lock the same name until it is released * (explicitly by calling release() or implicitly when the instance holding the * lock is destroyed). * * @author Grégoire Pineau * @author Romain Neutron * @author Nicolas Grekas * * @deprecated since version 3.4, to be removed in 4.0. Use Symfony\Component\Lock\Store\SemaphoreStore or Symfony\Component\Lock\Store\FlockStore instead. */ class LockHandler { private $file; private $handle; /** * @param string $name The lock name * @param string|null $lockPath The directory to store the lock. Default values will use temporary directory * * @throws IOException If the lock directory could not be created or is not writable */ public function __construct($name, $lockPath = null) { $lockPath = $lockPath ?: sys_get_temp_dir(); if (!is_dir($lockPath)) { $fs = new Filesystem(); $fs->mkdir($lockPath); } if (!is_writable($lockPath)) { throw new IOException(sprintf('The directory "%s" is not writable.', $lockPath), 0, null, $lockPath); } $this->file = sprintf('%s/sf.%s.%s.lock', $lockPath, preg_replace('/[^a-z0-9\._-]+/i', '-', $name), hash('sha256', $name)); } /** * Lock the resource. * * @param bool $blocking Wait until the lock is released * * @return bool Returns true if the lock was acquired, false otherwise * * @throws IOException If the lock file could not be created or opened */ public function lock($blocking = false) { if ($this->handle) { return true; } $error = null; // Silence error reporting set_error_handler(function ($errno, $msg) use (&$error) { $error = $msg; }); if (!$this->handle = fopen($this->file, 'r+') ?: fopen($this->file, 'r')) { if ($this->handle = fopen($this->file, 'x')) { chmod($this->file, 0666); } elseif (!$this->handle = fopen($this->file, 'r+') ?: fopen($this->file, 'r')) { usleep(100); // Give some time for chmod() to complete $this->handle = fopen($this->file, 'r+') ?: fopen($this->file, 'r'); } } restore_error_handler(); if (!$this->handle) { throw new IOException($error, 0, null, $this->file); } // On Windows, even if PHP doc says the contrary, LOCK_NB works, see // https://bugs.php.net/54129 if (!flock($this->handle, \LOCK_EX | ($blocking ? 0 : \LOCK_NB))) { fclose($this->handle); $this->handle = null; return false; } return true; } /** * Release the resource. */ public function release() { if ($this->handle) { flock($this->handle, \LOCK_UN | \LOCK_NB); fclose($this->handle); $this->handle = null; } } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Filesystem\Exception; /** * IOException interface for file and input/output stream related exceptions thrown by the component. * * @author Christian Gärtner */ interface IOExceptionInterface extends ExceptionInterface { /** * Returns the associated path for the exception. * * @return string|null The path */ public function getPath(); } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Filesystem\Exception; /** * Exception class thrown when a filesystem operation failure happens. * * @author Romain Neutron * @author Christian Gärtner * @author Fabien Potencier */ class IOException extends \RuntimeException implements IOExceptionInterface { private $path; public function __construct($message, $code = 0, \Exception $previous = null, $path = null) { $this->path = $path; parent::__construct($message, $code, $previous); } /** * {@inheritdoc} */ public function getPath() { return $this->path; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Filesystem\Exception; /** * Exception interface for all exceptions thrown by the component. * * @author Romain Neutron */ interface ExceptionInterface { } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Filesystem\Exception; /** * Exception class thrown when a file couldn't be found. * * @author Fabien Potencier * @author Christian Gärtner */ class FileNotFoundException extends IOException { public function __construct($message = null, $code = 0, \Exception $previous = null, $path = null) { if (null === $message) { if (null === $path) { $message = 'File could not be found.'; } else { $message = sprintf('File "%s" could not be found.', $path); } } parent::__construct($message, $code, $previous, $path); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Filesystem; use Symfony\Component\Filesystem\Exception\FileNotFoundException; use Symfony\Component\Filesystem\Exception\IOException; /** * Provides basic utility to manipulate the file system. * * @author Fabien Potencier */ class Filesystem { private static $lastError; /** * Copies a file. * * If the target file is older than the origin file, it's always overwritten. * If the target file is newer, it is overwritten only when the * $overwriteNewerFiles option is set to true. * * @param string $originFile The original filename * @param string $targetFile The target filename * @param bool $overwriteNewerFiles If true, target files newer than origin files are overwritten * * @throws FileNotFoundException When originFile doesn't exist * @throws IOException When copy fails */ public function copy($originFile, $targetFile, $overwriteNewerFiles = false) { $originIsLocal = stream_is_local($originFile) || 0 === stripos($originFile, 'file://'); if ($originIsLocal && !is_file($originFile)) { throw new FileNotFoundException(sprintf('Failed to copy "%s" because file does not exist.', $originFile), 0, null, $originFile); } $this->mkdir(\dirname($targetFile)); $doCopy = true; if (!$overwriteNewerFiles && null === parse_url($originFile, \PHP_URL_HOST) && is_file($targetFile)) { $doCopy = filemtime($originFile) > filemtime($targetFile); } if ($doCopy) { // https://bugs.php.net/64634 if (false === $source = @fopen($originFile, 'r')) { throw new IOException(sprintf('Failed to copy "%s" to "%s" because source file could not be opened for reading.', $originFile, $targetFile), 0, null, $originFile); } // Stream context created to allow files overwrite when using FTP stream wrapper - disabled by default if (false === $target = @fopen($targetFile, 'w', null, stream_context_create(['ftp' => ['overwrite' => true]]))) { throw new IOException(sprintf('Failed to copy "%s" to "%s" because target file could not be opened for writing.', $originFile, $targetFile), 0, null, $originFile); } $bytesCopied = stream_copy_to_stream($source, $target); fclose($source); fclose($target); unset($source, $target); if (!is_file($targetFile)) { throw new IOException(sprintf('Failed to copy "%s" to "%s".', $originFile, $targetFile), 0, null, $originFile); } if ($originIsLocal) { // Like `cp`, preserve executable permission bits @chmod($targetFile, fileperms($targetFile) | (fileperms($originFile) & 0111)); if ($bytesCopied !== $bytesOrigin = filesize($originFile)) { throw new IOException(sprintf('Failed to copy the whole content of "%s" to "%s" (%g of %g bytes copied).', $originFile, $targetFile, $bytesCopied, $bytesOrigin), 0, null, $originFile); } } } } /** * Creates a directory recursively. * * @param string|iterable $dirs The directory path * @param int $mode The directory mode * * @throws IOException On any directory creation failure */ public function mkdir($dirs, $mode = 0777) { foreach ($this->toIterable($dirs) as $dir) { if (is_dir($dir)) { continue; } if (!self::box('mkdir', $dir, $mode, true)) { if (!is_dir($dir)) { // The directory was not created by a concurrent process. Let's throw an exception with a developer friendly error message if we have one if (self::$lastError) { throw new IOException(sprintf('Failed to create "%s": ', $dir).self::$lastError, 0, null, $dir); } throw new IOException(sprintf('Failed to create "%s".', $dir), 0, null, $dir); } } } } /** * Checks the existence of files or directories. * * @param string|iterable $files A filename, an array of files, or a \Traversable instance to check * * @return bool true if the file exists, false otherwise */ public function exists($files) { $maxPathLength = \PHP_MAXPATHLEN - 2; foreach ($this->toIterable($files) as $file) { if (\strlen($file) > $maxPathLength) { throw new IOException(sprintf('Could not check if file exist because path length exceeds %d characters.', $maxPathLength), 0, null, $file); } if (!file_exists($file)) { return false; } } return true; } /** * Sets access and modification time of file. * * @param string|iterable $files A filename, an array of files, or a \Traversable instance to create * @param int|null $time The touch time as a Unix timestamp, if not supplied the current system time is used * @param int|null $atime The access time as a Unix timestamp, if not supplied the current system time is used * * @throws IOException When touch fails */ public function touch($files, $time = null, $atime = null) { foreach ($this->toIterable($files) as $file) { $touch = $time ? @touch($file, $time, $atime) : @touch($file); if (true !== $touch) { throw new IOException(sprintf('Failed to touch "%s".', $file), 0, null, $file); } } } /** * Removes files or directories. * * @param string|iterable $files A filename, an array of files, or a \Traversable instance to remove * * @throws IOException When removal fails */ public function remove($files) { if ($files instanceof \Traversable) { $files = iterator_to_array($files, false); } elseif (!\is_array($files)) { $files = [$files]; } $files = array_reverse($files); foreach ($files as $file) { if (is_link($file)) { // See https://bugs.php.net/52176 if (!(self::box('unlink', $file) || '\\' !== \DIRECTORY_SEPARATOR || self::box('rmdir', $file)) && file_exists($file)) { throw new IOException(sprintf('Failed to remove symlink "%s": ', $file).self::$lastError); } } elseif (is_dir($file)) { $this->remove(new \FilesystemIterator($file, \FilesystemIterator::CURRENT_AS_PATHNAME | \FilesystemIterator::SKIP_DOTS)); if (!self::box('rmdir', $file) && file_exists($file)) { throw new IOException(sprintf('Failed to remove directory "%s": ', $file).self::$lastError); } } elseif (!self::box('unlink', $file) && (false !== strpos(self::$lastError, 'Permission denied') || file_exists($file))) { throw new IOException(sprintf('Failed to remove file "%s": ', $file).self::$lastError); } } } /** * Change mode for an array of files or directories. * * @param string|iterable $files A filename, an array of files, or a \Traversable instance to change mode * @param int $mode The new mode (octal) * @param int $umask The mode mask (octal) * @param bool $recursive Whether change the mod recursively or not * * @throws IOException When the change fails */ public function chmod($files, $mode, $umask = 0000, $recursive = false) { foreach ($this->toIterable($files) as $file) { if ((\PHP_VERSION_ID < 80000 || \is_int($mode)) && true !== @chmod($file, $mode & ~$umask)) { throw new IOException(sprintf('Failed to chmod file "%s".', $file), 0, null, $file); } if ($recursive && is_dir($file) && !is_link($file)) { $this->chmod(new \FilesystemIterator($file), $mode, $umask, true); } } } /** * Change the owner of an array of files or directories. * * @param string|iterable $files A filename, an array of files, or a \Traversable instance to change owner * @param string|int $user A user name or number * @param bool $recursive Whether change the owner recursively or not * * @throws IOException When the change fails */ public function chown($files, $user, $recursive = false) { foreach ($this->toIterable($files) as $file) { if ($recursive && is_dir($file) && !is_link($file)) { $this->chown(new \FilesystemIterator($file), $user, true); } if (is_link($file) && \function_exists('lchown')) { if (true !== @lchown($file, $user)) { throw new IOException(sprintf('Failed to chown file "%s".', $file), 0, null, $file); } } else { if (true !== @chown($file, $user)) { throw new IOException(sprintf('Failed to chown file "%s".', $file), 0, null, $file); } } } } /** * Change the group of an array of files or directories. * * @param string|iterable $files A filename, an array of files, or a \Traversable instance to change group * @param string|int $group A group name or number * @param bool $recursive Whether change the group recursively or not * * @throws IOException When the change fails */ public function chgrp($files, $group, $recursive = false) { foreach ($this->toIterable($files) as $file) { if ($recursive && is_dir($file) && !is_link($file)) { $this->chgrp(new \FilesystemIterator($file), $group, true); } if (is_link($file) && \function_exists('lchgrp')) { if (true !== @lchgrp($file, $group) || (\defined('HHVM_VERSION') && !posix_getgrnam($group))) { throw new IOException(sprintf('Failed to chgrp file "%s".', $file), 0, null, $file); } } else { if (true !== @chgrp($file, $group)) { throw new IOException(sprintf('Failed to chgrp file "%s".', $file), 0, null, $file); } } } } /** * Renames a file or a directory. * * @param string $origin The origin filename or directory * @param string $target The new filename or directory * @param bool $overwrite Whether to overwrite the target if it already exists * * @throws IOException When target file or directory already exists * @throws IOException When origin cannot be renamed */ public function rename($origin, $target, $overwrite = false) { // we check that target does not exist if (!$overwrite && $this->isReadable($target)) { throw new IOException(sprintf('Cannot rename because the target "%s" already exists.', $target), 0, null, $target); } if (true !== @rename($origin, $target)) { if (is_dir($origin)) { // See https://bugs.php.net/54097 & https://php.net/rename#113943 $this->mirror($origin, $target, null, ['override' => $overwrite, 'delete' => $overwrite]); $this->remove($origin); return; } throw new IOException(sprintf('Cannot rename "%s" to "%s".', $origin, $target), 0, null, $target); } } /** * Tells whether a file exists and is readable. * * @param string $filename Path to the file * * @return bool * * @throws IOException When windows path is longer than 258 characters */ private function isReadable($filename) { $maxPathLength = \PHP_MAXPATHLEN - 2; if (\strlen($filename) > $maxPathLength) { throw new IOException(sprintf('Could not check if file is readable because path length exceeds %d characters.', $maxPathLength), 0, null, $filename); } return is_readable($filename); } /** * Creates a symbolic link or copy a directory. * * @param string $originDir The origin directory path * @param string $targetDir The symbolic link name * @param bool $copyOnWindows Whether to copy files if on Windows * * @throws IOException When symlink fails */ public function symlink($originDir, $targetDir, $copyOnWindows = false) { if ('\\' === \DIRECTORY_SEPARATOR) { $originDir = strtr($originDir, '/', '\\'); $targetDir = strtr($targetDir, '/', '\\'); if ($copyOnWindows) { $this->mirror($originDir, $targetDir); return; } } $this->mkdir(\dirname($targetDir)); if (is_link($targetDir)) { if (readlink($targetDir) === $originDir) { return; } $this->remove($targetDir); } if (!self::box('symlink', $originDir, $targetDir)) { $this->linkException($originDir, $targetDir, 'symbolic'); } } /** * Creates a hard link, or several hard links to a file. * * @param string $originFile The original file * @param string|string[] $targetFiles The target file(s) * * @throws FileNotFoundException When original file is missing or not a file * @throws IOException When link fails, including if link already exists */ public function hardlink($originFile, $targetFiles) { if (!$this->exists($originFile)) { throw new FileNotFoundException(null, 0, null, $originFile); } if (!is_file($originFile)) { throw new FileNotFoundException(sprintf('Origin file "%s" is not a file.', $originFile)); } foreach ($this->toIterable($targetFiles) as $targetFile) { if (is_file($targetFile)) { if (fileinode($originFile) === fileinode($targetFile)) { continue; } $this->remove($targetFile); } if (!self::box('link', $originFile, $targetFile)) { $this->linkException($originFile, $targetFile, 'hard'); } } } /** * @param string $origin * @param string $target * @param string $linkType Name of the link type, typically 'symbolic' or 'hard' */ private function linkException($origin, $target, $linkType) { if (self::$lastError) { if ('\\' === \DIRECTORY_SEPARATOR && false !== strpos(self::$lastError, 'error code(1314)')) { throw new IOException(sprintf('Unable to create "%s" link due to error code 1314: \'A required privilege is not held by the client\'. Do you have the required Administrator-rights?', $linkType), 0, null, $target); } } throw new IOException(sprintf('Failed to create "%s" link from "%s" to "%s".', $linkType, $origin, $target), 0, null, $target); } /** * Resolves links in paths. * * With $canonicalize = false (default) * - if $path does not exist or is not a link, returns null * - if $path is a link, returns the next direct target of the link without considering the existence of the target * * With $canonicalize = true * - if $path does not exist, returns null * - if $path exists, returns its absolute fully resolved final version * * @param string $path A filesystem path * @param bool $canonicalize Whether or not to return a canonicalized path * * @return string|null */ public function readlink($path, $canonicalize = false) { if (!$canonicalize && !is_link($path)) { return null; } if ($canonicalize) { if (!$this->exists($path)) { return null; } if ('\\' === \DIRECTORY_SEPARATOR) { $path = readlink($path); } return realpath($path); } if ('\\' === \DIRECTORY_SEPARATOR) { return realpath($path); } return readlink($path); } /** * Given an existing path, convert it to a path relative to a given starting path. * * @param string $endPath Absolute path of target * @param string $startPath Absolute path where traversal begins * * @return string Path of target relative to starting path */ public function makePathRelative($endPath, $startPath) { if (!$this->isAbsolutePath($endPath) || !$this->isAbsolutePath($startPath)) { @trigger_error(sprintf('Support for passing relative paths to %s() is deprecated since Symfony 3.4 and will be removed in 4.0.', __METHOD__), \E_USER_DEPRECATED); } // Normalize separators on Windows if ('\\' === \DIRECTORY_SEPARATOR) { $endPath = str_replace('\\', '/', $endPath); $startPath = str_replace('\\', '/', $startPath); } $splitDriveLetter = function ($path) { return (\strlen($path) > 2 && ':' === $path[1] && '/' === $path[2] && ctype_alpha($path[0])) ? [substr($path, 2), strtoupper($path[0])] : [$path, null]; }; $splitPath = function ($path, $absolute) { $result = []; foreach (explode('/', trim($path, '/')) as $segment) { if ('..' === $segment && ($absolute || \count($result))) { array_pop($result); } elseif ('.' !== $segment && '' !== $segment) { $result[] = $segment; } } return $result; }; list($endPath, $endDriveLetter) = $splitDriveLetter($endPath); list($startPath, $startDriveLetter) = $splitDriveLetter($startPath); $startPathArr = $splitPath($startPath, static::isAbsolutePath($startPath)); $endPathArr = $splitPath($endPath, static::isAbsolutePath($endPath)); if ($endDriveLetter && $startDriveLetter && $endDriveLetter != $startDriveLetter) { // End path is on another drive, so no relative path exists return $endDriveLetter.':/'.($endPathArr ? implode('/', $endPathArr).'/' : ''); } // Find for which directory the common path stops $index = 0; while (isset($startPathArr[$index]) && isset($endPathArr[$index]) && $startPathArr[$index] === $endPathArr[$index]) { ++$index; } // Determine how deep the start path is relative to the common path (ie, "web/bundles" = 2 levels) if (1 === \count($startPathArr) && '' === $startPathArr[0]) { $depth = 0; } else { $depth = \count($startPathArr) - $index; } // Repeated "../" for each level need to reach the common path $traverser = str_repeat('../', $depth); $endPathRemainder = implode('/', \array_slice($endPathArr, $index)); // Construct $endPath from traversing to the common path, then to the remaining $endPath $relativePath = $traverser.('' !== $endPathRemainder ? $endPathRemainder.'/' : ''); return '' === $relativePath ? './' : $relativePath; } /** * Mirrors a directory to another. * * Copies files and directories from the origin directory into the target directory. By default: * * - existing files in the target directory will be overwritten, except if they are newer (see the `override` option) * - files in the target directory that do not exist in the source directory will not be deleted (see the `delete` option) * * @param string $originDir The origin directory * @param string $targetDir The target directory * @param \Traversable|null $iterator Iterator that filters which files and directories to copy, if null a recursive iterator is created * @param array $options An array of boolean options * Valid options are: * - $options['override'] If true, target files newer than origin files are overwritten (see copy(), defaults to false) * - $options['copy_on_windows'] Whether to copy files instead of links on Windows (see symlink(), defaults to false) * - $options['delete'] Whether to delete files that are not in the source directory (defaults to false) * * @throws IOException When file type is unknown */ public function mirror($originDir, $targetDir, \Traversable $iterator = null, $options = []) { $targetDir = rtrim($targetDir, '/\\'); $originDir = rtrim($originDir, '/\\'); $originDirLen = \strlen($originDir); // Iterate in destination folder to remove obsolete entries if ($this->exists($targetDir) && isset($options['delete']) && $options['delete']) { $deleteIterator = $iterator; if (null === $deleteIterator) { $flags = \FilesystemIterator::SKIP_DOTS; $deleteIterator = new \RecursiveIteratorIterator(new \RecursiveDirectoryIterator($targetDir, $flags), \RecursiveIteratorIterator::CHILD_FIRST); } $targetDirLen = \strlen($targetDir); foreach ($deleteIterator as $file) { $origin = $originDir.substr($file->getPathname(), $targetDirLen); if (!$this->exists($origin)) { $this->remove($file); } } } $copyOnWindows = false; if (isset($options['copy_on_windows'])) { $copyOnWindows = $options['copy_on_windows']; } if (null === $iterator) { $flags = $copyOnWindows ? \FilesystemIterator::SKIP_DOTS | \FilesystemIterator::FOLLOW_SYMLINKS : \FilesystemIterator::SKIP_DOTS; $iterator = new \RecursiveIteratorIterator(new \RecursiveDirectoryIterator($originDir, $flags), \RecursiveIteratorIterator::SELF_FIRST); } if ($this->exists($originDir)) { $this->mkdir($targetDir); } foreach ($iterator as $file) { $target = $targetDir.substr($file->getPathname(), $originDirLen); if ($copyOnWindows) { if (is_file($file)) { $this->copy($file, $target, isset($options['override']) ? $options['override'] : false); } elseif (is_dir($file)) { $this->mkdir($target); } else { throw new IOException(sprintf('Unable to guess "%s" file type.', $file), 0, null, $file); } } else { if (is_link($file)) { $this->symlink($file->getLinkTarget(), $target); } elseif (is_dir($file)) { $this->mkdir($target); } elseif (is_file($file)) { $this->copy($file, $target, isset($options['override']) ? $options['override'] : false); } else { throw new IOException(sprintf('Unable to guess "%s" file type.', $file), 0, null, $file); } } } } /** * Returns whether the file path is an absolute path. * * @param string $file A file path * * @return bool */ public function isAbsolutePath($file) { return '' !== (string) $file && (strspn($file, '/\\', 0, 1) || (\strlen($file) > 3 && ctype_alpha($file[0]) && ':' === $file[1] && strspn($file, '/\\', 2, 1) ) || null !== parse_url($file, \PHP_URL_SCHEME) ); } /** * Creates a temporary file with support for custom stream wrappers. * * @param string $dir The directory where the temporary filename will be created * @param string $prefix The prefix of the generated temporary filename * Note: Windows uses only the first three characters of prefix * * @return string The new temporary filename (with path), or throw an exception on failure */ public function tempnam($dir, $prefix) { list($scheme, $hierarchy) = $this->getSchemeAndHierarchy($dir); // If no scheme or scheme is "file" or "gs" (Google Cloud) create temp file in local filesystem if (null === $scheme || 'file' === $scheme || 'gs' === $scheme) { $tmpFile = @tempnam($hierarchy, $prefix); // If tempnam failed or no scheme return the filename otherwise prepend the scheme if (false !== $tmpFile) { if (null !== $scheme && 'gs' !== $scheme) { return $scheme.'://'.$tmpFile; } return $tmpFile; } throw new IOException('A temporary file could not be created.'); } // Loop until we create a valid temp file or have reached 10 attempts for ($i = 0; $i < 10; ++$i) { // Create a unique filename $tmpFile = $dir.'/'.$prefix.uniqid(mt_rand(), true); // Use fopen instead of file_exists as some streams do not support stat // Use mode 'x+' to atomically check existence and create to avoid a TOCTOU vulnerability $handle = @fopen($tmpFile, 'x+'); // If unsuccessful restart the loop if (false === $handle) { continue; } // Close the file if it was successfully opened @fclose($handle); return $tmpFile; } throw new IOException('A temporary file could not be created.'); } /** * Atomically dumps content into a file. * * @param string $filename The file to be written to * @param string $content The data to write into the file * * @throws IOException if the file cannot be written to */ public function dumpFile($filename, $content) { $dir = \dirname($filename); if (!is_dir($dir)) { $this->mkdir($dir); } if (!is_writable($dir)) { throw new IOException(sprintf('Unable to write to the "%s" directory.', $dir), 0, null, $dir); } // Will create a temp file with 0600 access rights // when the filesystem supports chmod. $tmpFile = $this->tempnam($dir, basename($filename)); if (false === @file_put_contents($tmpFile, $content)) { throw new IOException(sprintf('Failed to write file "%s".', $filename), 0, null, $filename); } @chmod($tmpFile, file_exists($filename) ? fileperms($filename) : 0666 & ~umask()); $this->rename($tmpFile, $filename, true); } /** * Appends content to an existing file. * * @param string $filename The file to which to append content * @param string $content The content to append * * @throws IOException If the file is not writable */ public function appendToFile($filename, $content) { $dir = \dirname($filename); if (!is_dir($dir)) { $this->mkdir($dir); } if (!is_writable($dir)) { throw new IOException(sprintf('Unable to write to the "%s" directory.', $dir), 0, null, $dir); } if (false === @file_put_contents($filename, $content, \FILE_APPEND)) { throw new IOException(sprintf('Failed to write file "%s".', $filename), 0, null, $filename); } } /** * @param mixed $files * * @return array|\Traversable */ private function toIterable($files) { return \is_array($files) || $files instanceof \Traversable ? $files : [$files]; } /** * Gets a 2-tuple of scheme (may be null) and hierarchical part of a filename (e.g. file:///tmp -> [file, tmp]). * * @param string $filename The filename to be parsed * * @return array The filename scheme and hierarchical part */ private function getSchemeAndHierarchy($filename) { $components = explode('://', $filename, 2); return 2 === \count($components) ? [$components[0], $components[1]] : [null, $components[0]]; } /** * @param callable $func * * @return mixed */ private static function box($func) { self::$lastError = null; set_error_handler(__CLASS__.'::handleError'); try { $result = \call_user_func_array($func, \array_slice(\func_get_args(), 1)); restore_error_handler(); return $result; } catch (\Throwable $e) { } catch (\Exception $e) { } restore_error_handler(); throw $e; } /** * @internal */ public static function handleError($type, $msg) { self::$lastError = $msg; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Console\DependencyInjection; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\CommandLoader\ContainerCommandLoader; use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface; use Symfony\Component\DependencyInjection\Compiler\ServiceLocatorTagPass; use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\Exception\InvalidArgumentException; use Symfony\Component\DependencyInjection\TypedReference; /** * Registers console commands. * * @author Grégoire Pineau */ class AddConsoleCommandPass implements CompilerPassInterface { private $commandLoaderServiceId; private $commandTag; public function __construct($commandLoaderServiceId = 'console.command_loader', $commandTag = 'console.command') { $this->commandLoaderServiceId = $commandLoaderServiceId; $this->commandTag = $commandTag; } public function process(ContainerBuilder $container) { $commandServices = $container->findTaggedServiceIds($this->commandTag, true); $lazyCommandMap = []; $lazyCommandRefs = []; $serviceIds = []; $lazyServiceIds = []; foreach ($commandServices as $id => $tags) { $definition = $container->getDefinition($id); $class = $container->getParameterBag()->resolveValue($definition->getClass()); $commandId = 'console.command.'.strtolower(str_replace('\\', '_', $class)); if (isset($tags[0]['command'])) { $commandName = $tags[0]['command']; } else { if (!$r = $container->getReflectionClass($class)) { throw new InvalidArgumentException(sprintf('Class "%s" used for service "%s" cannot be found.', $class, $id)); } if (!$r->isSubclassOf(Command::class)) { throw new InvalidArgumentException(sprintf('The service "%s" tagged "%s" must be a subclass of "%s".', $id, $this->commandTag, Command::class)); } $commandName = $class::getDefaultName(); } if (null === $commandName) { if (isset($serviceIds[$commandId]) || $container->hasAlias($commandId)) { $commandId = $commandId.'_'.$id; } if (!$definition->isPublic() || $definition->isPrivate()) { $container->setAlias($commandId, $id)->setPublic(true); $id = $commandId; } $serviceIds[$commandId] = $id; continue; } $serviceIds[$commandId] = $id; $lazyServiceIds[$id] = true; unset($tags[0]); $lazyCommandMap[$commandName] = $id; $lazyCommandRefs[$id] = new TypedReference($id, $class); $aliases = []; foreach ($tags as $tag) { if (isset($tag['command'])) { $aliases[] = $tag['command']; $lazyCommandMap[$tag['command']] = $id; } } $definition->addMethodCall('setName', [$commandName]); if ($aliases) { $definition->addMethodCall('setAliases', [$aliases]); } } $container ->register($this->commandLoaderServiceId, ContainerCommandLoader::class) ->setPublic(true) ->setArguments([ServiceLocatorTagPass::register($container, $lazyCommandRefs), $lazyCommandMap]); $container->setParameter('console.command.ids', $serviceIds); $container->setParameter('console.lazy_command.ids', $lazyServiceIds); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Console\Input; use Symfony\Component\Console\Exception\InvalidArgumentException; use Symfony\Component\Console\Exception\LogicException; /** * A InputDefinition represents a set of valid command line arguments and options. * * Usage: * * $definition = new InputDefinition([ * new InputArgument('name', InputArgument::REQUIRED), * new InputOption('foo', 'f', InputOption::VALUE_REQUIRED), * ]); * * @author Fabien Potencier */ class InputDefinition { private $arguments; private $requiredCount; private $hasAnArrayArgument = false; private $hasOptional; private $options; private $shortcuts; /** * @param array $definition An array of InputArgument and InputOption instance */ public function __construct(array $definition = []) { $this->setDefinition($definition); } /** * Sets the definition of the input. */ public function setDefinition(array $definition) { $arguments = []; $options = []; foreach ($definition as $item) { if ($item instanceof InputOption) { $options[] = $item; } else { $arguments[] = $item; } } $this->setArguments($arguments); $this->setOptions($options); } /** * Sets the InputArgument objects. * * @param InputArgument[] $arguments An array of InputArgument objects */ public function setArguments($arguments = []) { $this->arguments = []; $this->requiredCount = 0; $this->hasOptional = false; $this->hasAnArrayArgument = false; $this->addArguments($arguments); } /** * Adds an array of InputArgument objects. * * @param InputArgument[] $arguments An array of InputArgument objects */ public function addArguments($arguments = []) { if (null !== $arguments) { foreach ($arguments as $argument) { $this->addArgument($argument); } } } /** * @throws LogicException When incorrect argument is given */ public function addArgument(InputArgument $argument) { if (isset($this->arguments[$argument->getName()])) { throw new LogicException(sprintf('An argument with name "%s" already exists.', $argument->getName())); } if ($this->hasAnArrayArgument) { throw new LogicException('Cannot add an argument after an array argument.'); } if ($argument->isRequired() && $this->hasOptional) { throw new LogicException('Cannot add a required argument after an optional one.'); } if ($argument->isArray()) { $this->hasAnArrayArgument = true; } if ($argument->isRequired()) { ++$this->requiredCount; } else { $this->hasOptional = true; } $this->arguments[$argument->getName()] = $argument; } /** * Returns an InputArgument by name or by position. * * @param string|int $name The InputArgument name or position * * @return InputArgument An InputArgument object * * @throws InvalidArgumentException When argument given doesn't exist */ public function getArgument($name) { if (!$this->hasArgument($name)) { throw new InvalidArgumentException(sprintf('The "%s" argument does not exist.', $name)); } $arguments = \is_int($name) ? array_values($this->arguments) : $this->arguments; return $arguments[$name]; } /** * Returns true if an InputArgument object exists by name or position. * * @param string|int $name The InputArgument name or position * * @return bool true if the InputArgument object exists, false otherwise */ public function hasArgument($name) { $arguments = \is_int($name) ? array_values($this->arguments) : $this->arguments; return isset($arguments[$name]); } /** * Gets the array of InputArgument objects. * * @return InputArgument[] An array of InputArgument objects */ public function getArguments() { return $this->arguments; } /** * Returns the number of InputArguments. * * @return int The number of InputArguments */ public function getArgumentCount() { return $this->hasAnArrayArgument ? \PHP_INT_MAX : \count($this->arguments); } /** * Returns the number of required InputArguments. * * @return int The number of required InputArguments */ public function getArgumentRequiredCount() { return $this->requiredCount; } /** * Gets the default values. * * @return array An array of default values */ public function getArgumentDefaults() { $values = []; foreach ($this->arguments as $argument) { $values[$argument->getName()] = $argument->getDefault(); } return $values; } /** * Sets the InputOption objects. * * @param InputOption[] $options An array of InputOption objects */ public function setOptions($options = []) { $this->options = []; $this->shortcuts = []; $this->addOptions($options); } /** * Adds an array of InputOption objects. * * @param InputOption[] $options An array of InputOption objects */ public function addOptions($options = []) { foreach ($options as $option) { $this->addOption($option); } } /** * @throws LogicException When option given already exist */ public function addOption(InputOption $option) { if (isset($this->options[$option->getName()]) && !$option->equals($this->options[$option->getName()])) { throw new LogicException(sprintf('An option named "%s" already exists.', $option->getName())); } if ($option->getShortcut()) { foreach (explode('|', $option->getShortcut()) as $shortcut) { if (isset($this->shortcuts[$shortcut]) && !$option->equals($this->options[$this->shortcuts[$shortcut]])) { throw new LogicException(sprintf('An option with shortcut "%s" already exists.', $shortcut)); } } } $this->options[$option->getName()] = $option; if ($option->getShortcut()) { foreach (explode('|', $option->getShortcut()) as $shortcut) { $this->shortcuts[$shortcut] = $option->getName(); } } } /** * Returns an InputOption by name. * * @param string $name The InputOption name * * @return InputOption A InputOption object * * @throws InvalidArgumentException When option given doesn't exist */ public function getOption($name) { if (!$this->hasOption($name)) { throw new InvalidArgumentException(sprintf('The "--%s" option does not exist.', $name)); } return $this->options[$name]; } /** * Returns true if an InputOption object exists by name. * * This method can't be used to check if the user included the option when * executing the command (use getOption() instead). * * @param string $name The InputOption name * * @return bool true if the InputOption object exists, false otherwise */ public function hasOption($name) { return isset($this->options[$name]); } /** * Gets the array of InputOption objects. * * @return InputOption[] An array of InputOption objects */ public function getOptions() { return $this->options; } /** * Returns true if an InputOption object exists by shortcut. * * @param string $name The InputOption shortcut * * @return bool true if the InputOption object exists, false otherwise */ public function hasShortcut($name) { return isset($this->shortcuts[$name]); } /** * Gets an InputOption by shortcut. * * @param string $shortcut The Shortcut name * * @return InputOption An InputOption object */ public function getOptionForShortcut($shortcut) { return $this->getOption($this->shortcutToName($shortcut)); } /** * Gets an array of default values. * * @return array An array of all default values */ public function getOptionDefaults() { $values = []; foreach ($this->options as $option) { $values[$option->getName()] = $option->getDefault(); } return $values; } /** * Returns the InputOption name given a shortcut. * * @param string $shortcut The shortcut * * @return string The InputOption name * * @throws InvalidArgumentException When option given does not exist * * @internal */ public function shortcutToName($shortcut) { if (!isset($this->shortcuts[$shortcut])) { throw new InvalidArgumentException(sprintf('The "-%s" option does not exist.', $shortcut)); } return $this->shortcuts[$shortcut]; } /** * Gets the synopsis. * * @param bool $short Whether to return the short version (with options folded) or not * * @return string The synopsis */ public function getSynopsis($short = false) { $elements = []; if ($short && $this->getOptions()) { $elements[] = '[options]'; } elseif (!$short) { foreach ($this->getOptions() as $option) { $value = ''; if ($option->acceptValue()) { $value = sprintf( ' %s%s%s', $option->isValueOptional() ? '[' : '', strtoupper($option->getName()), $option->isValueOptional() ? ']' : '' ); } $shortcut = $option->getShortcut() ? sprintf('-%s|', $option->getShortcut()) : ''; $elements[] = sprintf('[%s--%s%s]', $shortcut, $option->getName(), $value); } } if (\count($elements) && $this->getArguments()) { $elements[] = '[--]'; } foreach ($this->getArguments() as $argument) { $element = '<'.$argument->getName().'>'; if (!$argument->isRequired()) { $element = '['.$element.']'; } elseif ($argument->isArray()) { $element .= ' ('.$element.')'; } if ($argument->isArray()) { $element .= '...'; } $elements[] = $element; } return implode(' ', $elements); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Console\Input; use Symfony\Component\Console\Exception\InvalidArgumentException; /** * StringInput represents an input provided as a string. * * Usage: * * $input = new StringInput('foo --bar="foobar"'); * * @author Fabien Potencier */ class StringInput extends ArgvInput { const REGEX_STRING = '([^\s]+?)(?:\s|(?setTokens($this->tokenize($input)); } /** * Tokenizes a string. * * @param string $input The input to tokenize * * @return array An array of tokens * * @throws InvalidArgumentException When unable to parse input (should never happen) */ private function tokenize($input) { $tokens = []; $length = \strlen($input); $cursor = 0; while ($cursor < $length) { if (preg_match('/\s+/A', $input, $match, null, $cursor)) { } elseif (preg_match('/([^="\'\s]+?)(=?)('.self::REGEX_QUOTED_STRING.'+)/A', $input, $match, null, $cursor)) { $tokens[] = $match[1].$match[2].stripcslashes(str_replace(['"\'', '\'"', '\'\'', '""'], '', substr($match[3], 1, \strlen($match[3]) - 2))); } elseif (preg_match('/'.self::REGEX_QUOTED_STRING.'/A', $input, $match, null, $cursor)) { $tokens[] = stripcslashes(substr($match[0], 1, \strlen($match[0]) - 2)); } elseif (preg_match('/'.self::REGEX_STRING.'/A', $input, $match, null, $cursor)) { $tokens[] = stripcslashes($match[1]); } else { // should never happen throw new InvalidArgumentException(sprintf('Unable to parse input near "... %s ...".', substr($input, $cursor, 10))); } $cursor += \strlen($match[0]); } return $tokens; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Console\Input; use Symfony\Component\Console\Exception\InvalidArgumentException; use Symfony\Component\Console\Exception\LogicException; /** * Represents a command line argument. * * @author Fabien Potencier */ class InputArgument { const REQUIRED = 1; const OPTIONAL = 2; const IS_ARRAY = 4; private $name; private $mode; private $default; private $description; /** * @param string $name The argument name * @param int|null $mode The argument mode: self::REQUIRED or self::OPTIONAL * @param string $description A description text * @param string|string[]|null $default The default value (for self::OPTIONAL mode only) * * @throws InvalidArgumentException When argument mode is not valid */ public function __construct($name, $mode = null, $description = '', $default = null) { if (null === $mode) { $mode = self::OPTIONAL; } elseif (!\is_int($mode) || $mode > 7 || $mode < 1) { throw new InvalidArgumentException(sprintf('Argument mode "%s" is not valid.', $mode)); } $this->name = $name; $this->mode = $mode; $this->description = $description; $this->setDefault($default); } /** * Returns the argument name. * * @return string The argument name */ public function getName() { return $this->name; } /** * Returns true if the argument is required. * * @return bool true if parameter mode is self::REQUIRED, false otherwise */ public function isRequired() { return self::REQUIRED === (self::REQUIRED & $this->mode); } /** * Returns true if the argument can take multiple values. * * @return bool true if mode is self::IS_ARRAY, false otherwise */ public function isArray() { return self::IS_ARRAY === (self::IS_ARRAY & $this->mode); } /** * Sets the default value. * * @param string|string[]|null $default The default value * * @throws LogicException When incorrect default value is given */ public function setDefault($default = null) { if (self::REQUIRED === $this->mode && null !== $default) { throw new LogicException('Cannot set a default value except for InputArgument::OPTIONAL mode.'); } if ($this->isArray()) { if (null === $default) { $default = []; } elseif (!\is_array($default)) { throw new LogicException('A default value for an array argument must be an array.'); } } $this->default = $default; } /** * Returns the default value. * * @return string|string[]|null The default value */ public function getDefault() { return $this->default; } /** * Returns the description text. * * @return string The description text */ public function getDescription() { return $this->description; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Console\Input; use Symfony\Component\Console\Exception\InvalidArgumentException; use Symfony\Component\Console\Exception\RuntimeException; /** * Input is the base class for all concrete Input classes. * * Three concrete classes are provided by default: * * * `ArgvInput`: The input comes from the CLI arguments (argv) * * `StringInput`: The input is provided as a string * * `ArrayInput`: The input is provided as an array * * @author Fabien Potencier */ abstract class Input implements InputInterface, StreamableInputInterface { protected $definition; protected $stream; protected $options = []; protected $arguments = []; protected $interactive = true; public function __construct(InputDefinition $definition = null) { if (null === $definition) { $this->definition = new InputDefinition(); } else { $this->bind($definition); $this->validate(); } } /** * {@inheritdoc} */ public function bind(InputDefinition $definition) { $this->arguments = []; $this->options = []; $this->definition = $definition; $this->parse(); } /** * Processes command line arguments. */ abstract protected function parse(); /** * {@inheritdoc} */ public function validate() { $definition = $this->definition; $givenArguments = $this->arguments; $missingArguments = array_filter(array_keys($definition->getArguments()), function ($argument) use ($definition, $givenArguments) { return !\array_key_exists($argument, $givenArguments) && $definition->getArgument($argument)->isRequired(); }); if (\count($missingArguments) > 0) { throw new RuntimeException(sprintf('Not enough arguments (missing: "%s").', implode(', ', $missingArguments))); } } /** * {@inheritdoc} */ public function isInteractive() { return $this->interactive; } /** * {@inheritdoc} */ public function setInteractive($interactive) { $this->interactive = (bool) $interactive; } /** * {@inheritdoc} */ public function getArguments() { return array_merge($this->definition->getArgumentDefaults(), $this->arguments); } /** * {@inheritdoc} */ public function getArgument($name) { if (!$this->definition->hasArgument($name)) { throw new InvalidArgumentException(sprintf('The "%s" argument does not exist.', $name)); } return isset($this->arguments[$name]) ? $this->arguments[$name] : $this->definition->getArgument($name)->getDefault(); } /** * {@inheritdoc} */ public function setArgument($name, $value) { if (!$this->definition->hasArgument($name)) { throw new InvalidArgumentException(sprintf('The "%s" argument does not exist.', $name)); } $this->arguments[$name] = $value; } /** * {@inheritdoc} */ public function hasArgument($name) { return $this->definition->hasArgument($name); } /** * {@inheritdoc} */ public function getOptions() { return array_merge($this->definition->getOptionDefaults(), $this->options); } /** * {@inheritdoc} */ public function getOption($name) { if (!$this->definition->hasOption($name)) { throw new InvalidArgumentException(sprintf('The "%s" option does not exist.', $name)); } return \array_key_exists($name, $this->options) ? $this->options[$name] : $this->definition->getOption($name)->getDefault(); } /** * {@inheritdoc} */ public function setOption($name, $value) { if (!$this->definition->hasOption($name)) { throw new InvalidArgumentException(sprintf('The "%s" option does not exist.', $name)); } $this->options[$name] = $value; } /** * {@inheritdoc} */ public function hasOption($name) { return $this->definition->hasOption($name); } /** * Escapes a token through escapeshellarg if it contains unsafe chars. * * @param string $token * * @return string */ public function escapeToken($token) { return preg_match('{^[\w-]+$}', $token) ? $token : escapeshellarg($token); } /** * {@inheritdoc} */ public function setStream($stream) { $this->stream = $stream; } /** * {@inheritdoc} */ public function getStream() { return $this->stream; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Console\Input; /** * StreamableInputInterface is the interface implemented by all input classes * that have an input stream. * * @author Robin Chalas */ interface StreamableInputInterface extends InputInterface { /** * Sets the input stream to read from when interacting with the user. * * This is mainly useful for testing purpose. * * @param resource $stream The input stream */ public function setStream($stream); /** * Returns the input stream. * * @return resource|null */ public function getStream(); } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Console\Input; use Symfony\Component\Console\Exception\InvalidArgumentException; use Symfony\Component\Console\Exception\RuntimeException; /** * InputInterface is the interface implemented by all input classes. * * @author Fabien Potencier */ interface InputInterface { /** * Returns the first argument from the raw parameters (not parsed). * * @return string|null The value of the first argument or null otherwise */ public function getFirstArgument(); /** * Returns true if the raw parameters (not parsed) contain a value. * * This method is to be used to introspect the input parameters * before they have been validated. It must be used carefully. * Does not necessarily return the correct result for short options * when multiple flags are combined in the same option. * * @param string|array $values The values to look for in the raw parameters (can be an array) * @param bool $onlyParams Only check real parameters, skip those following an end of options (--) signal * * @return bool true if the value is contained in the raw parameters */ public function hasParameterOption($values, $onlyParams = false); /** * Returns the value of a raw option (not parsed). * * This method is to be used to introspect the input parameters * before they have been validated. It must be used carefully. * Does not necessarily return the correct result for short options * when multiple flags are combined in the same option. * * @param string|array $values The value(s) to look for in the raw parameters (can be an array) * @param mixed $default The default value to return if no result is found * @param bool $onlyParams Only check real parameters, skip those following an end of options (--) signal * * @return mixed The option value */ public function getParameterOption($values, $default = false, $onlyParams = false); /** * Binds the current Input instance with the given arguments and options. * * @throws RuntimeException */ public function bind(InputDefinition $definition); /** * Validates the input. * * @throws RuntimeException When not enough arguments are given */ public function validate(); /** * Returns all the given arguments merged with the default values. * * @return array */ public function getArguments(); /** * Returns the argument value for a given argument name. * * @param string $name The argument name * * @return string|string[]|null The argument value * * @throws InvalidArgumentException When argument given doesn't exist */ public function getArgument($name); /** * Sets an argument value by name. * * @param string $name The argument name * @param string|string[]|null $value The argument value * * @throws InvalidArgumentException When argument given doesn't exist */ public function setArgument($name, $value); /** * Returns true if an InputArgument object exists by name or position. * * @param string|int $name The InputArgument name or position * * @return bool true if the InputArgument object exists, false otherwise */ public function hasArgument($name); /** * Returns all the given options merged with the default values. * * @return array */ public function getOptions(); /** * Returns the option value for a given option name. * * @param string $name The option name * * @return string|string[]|bool|null The option value * * @throws InvalidArgumentException When option given doesn't exist */ public function getOption($name); /** * Sets an option value by name. * * @param string $name The option name * @param string|string[]|bool|null $value The option value * * @throws InvalidArgumentException When option given doesn't exist */ public function setOption($name, $value); /** * Returns true if an InputOption object exists by name. * * @param string $name The InputOption name * * @return bool true if the InputOption object exists, false otherwise */ public function hasOption($name); /** * Is this input means interactive? * * @return bool */ public function isInteractive(); /** * Sets the input interactivity. * * @param bool $interactive If the input should be interactive */ public function setInteractive($interactive); } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Console\Input; /** * InputAwareInterface should be implemented by classes that depends on the * Console Input. * * @author Wouter J */ interface InputAwareInterface { /** * Sets the Console Input. */ public function setInput(InputInterface $input); } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Console\Input; use Symfony\Component\Console\Exception\InvalidArgumentException; use Symfony\Component\Console\Exception\InvalidOptionException; /** * ArrayInput represents an input provided as an array. * * Usage: * * $input = new ArrayInput(['command' => 'foo:bar', 'foo' => 'bar', '--bar' => 'foobar']); * * @author Fabien Potencier */ class ArrayInput extends Input { private $parameters; public function __construct(array $parameters, InputDefinition $definition = null) { $this->parameters = $parameters; parent::__construct($definition); } /** * {@inheritdoc} */ public function getFirstArgument() { foreach ($this->parameters as $param => $value) { if ($param && \is_string($param) && '-' === $param[0]) { continue; } return $value; } return null; } /** * {@inheritdoc} */ public function hasParameterOption($values, $onlyParams = false) { $values = (array) $values; foreach ($this->parameters as $k => $v) { if (!\is_int($k)) { $v = $k; } if ($onlyParams && '--' === $v) { return false; } if (\in_array($v, $values)) { return true; } } return false; } /** * {@inheritdoc} */ public function getParameterOption($values, $default = false, $onlyParams = false) { $values = (array) $values; foreach ($this->parameters as $k => $v) { if ($onlyParams && ('--' === $k || (\is_int($k) && '--' === $v))) { return $default; } if (\is_int($k)) { if (\in_array($v, $values)) { return true; } } elseif (\in_array($k, $values)) { return $v; } } return $default; } /** * Returns a stringified representation of the args passed to the command. * * @return string */ public function __toString() { $params = []; foreach ($this->parameters as $param => $val) { if ($param && \is_string($param) && '-' === $param[0]) { if (\is_array($val)) { foreach ($val as $v) { $params[] = $param.('' != $v ? '='.$this->escapeToken($v) : ''); } } else { $params[] = $param.('' != $val ? '='.$this->escapeToken($val) : ''); } } else { $params[] = \is_array($val) ? implode(' ', array_map([$this, 'escapeToken'], $val)) : $this->escapeToken($val); } } return implode(' ', $params); } /** * {@inheritdoc} */ protected function parse() { foreach ($this->parameters as $key => $value) { if ('--' === $key) { return; } if (0 === strpos($key, '--')) { $this->addLongOption(substr($key, 2), $value); } elseif (0 === strpos($key, '-')) { $this->addShortOption(substr($key, 1), $value); } else { $this->addArgument($key, $value); } } } /** * Adds a short option value. * * @param string $shortcut The short option key * @param mixed $value The value for the option * * @throws InvalidOptionException When option given doesn't exist */ private function addShortOption($shortcut, $value) { if (!$this->definition->hasShortcut($shortcut)) { throw new InvalidOptionException(sprintf('The "-%s" option does not exist.', $shortcut)); } $this->addLongOption($this->definition->getOptionForShortcut($shortcut)->getName(), $value); } /** * Adds a long option value. * * @param string $name The long option key * @param mixed $value The value for the option * * @throws InvalidOptionException When option given doesn't exist * @throws InvalidOptionException When a required value is missing */ private function addLongOption($name, $value) { if (!$this->definition->hasOption($name)) { throw new InvalidOptionException(sprintf('The "--%s" option does not exist.', $name)); } $option = $this->definition->getOption($name); if (null === $value) { if ($option->isValueRequired()) { throw new InvalidOptionException(sprintf('The "--%s" option requires a value.', $name)); } if (!$option->isValueOptional()) { $value = true; } } $this->options[$name] = $value; } /** * Adds an argument value. * * @param string $name The argument name * @param mixed $value The value for the argument * * @throws InvalidArgumentException When argument given doesn't exist */ private function addArgument($name, $value) { if (!$this->definition->hasArgument($name)) { throw new InvalidArgumentException(sprintf('The "%s" argument does not exist.', $name)); } $this->arguments[$name] = $value; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Console\Input; use Symfony\Component\Console\Exception\RuntimeException; /** * ArgvInput represents an input coming from the CLI arguments. * * Usage: * * $input = new ArgvInput(); * * By default, the `$_SERVER['argv']` array is used for the input values. * * This can be overridden by explicitly passing the input values in the constructor: * * $input = new ArgvInput($_SERVER['argv']); * * If you pass it yourself, don't forget that the first element of the array * is the name of the running application. * * When passing an argument to the constructor, be sure that it respects * the same rules as the argv one. It's almost always better to use the * `StringInput` when you want to provide your own input. * * @author Fabien Potencier * * @see http://www.gnu.org/software/libc/manual/html_node/Argument-Syntax.html * @see http://www.opengroup.org/onlinepubs/009695399/basedefs/xbd_chap12.html#tag_12_02 */ class ArgvInput extends Input { private $tokens; private $parsed; /** * @param array|null $argv An array of parameters from the CLI (in the argv format) * @param InputDefinition|null $definition A InputDefinition instance */ public function __construct(array $argv = null, InputDefinition $definition = null) { $argv = null !== $argv ? $argv : (isset($_SERVER['argv']) ? $_SERVER['argv'] : []); // strip the application name array_shift($argv); $this->tokens = $argv; parent::__construct($definition); } protected function setTokens(array $tokens) { $this->tokens = $tokens; } /** * {@inheritdoc} */ protected function parse() { $parseOptions = true; $this->parsed = $this->tokens; while (null !== $token = array_shift($this->parsed)) { if ($parseOptions && '' == $token) { $this->parseArgument($token); } elseif ($parseOptions && '--' == $token) { $parseOptions = false; } elseif ($parseOptions && 0 === strpos($token, '--')) { $this->parseLongOption($token); } elseif ($parseOptions && '-' === $token[0] && '-' !== $token) { $this->parseShortOption($token); } else { $this->parseArgument($token); } } } /** * Parses a short option. * * @param string $token The current token */ private function parseShortOption($token) { $name = substr($token, 1); if (\strlen($name) > 1) { if ($this->definition->hasShortcut($name[0]) && $this->definition->getOptionForShortcut($name[0])->acceptValue()) { // an option with a value (with no space) $this->addShortOption($name[0], substr($name, 1)); } else { $this->parseShortOptionSet($name); } } else { $this->addShortOption($name, null); } } /** * Parses a short option set. * * @param string $name The current token * * @throws RuntimeException When option given doesn't exist */ private function parseShortOptionSet($name) { $len = \strlen($name); for ($i = 0; $i < $len; ++$i) { if (!$this->definition->hasShortcut($name[$i])) { $encoding = mb_detect_encoding($name, null, true); throw new RuntimeException(sprintf('The "-%s" option does not exist.', false === $encoding ? $name[$i] : mb_substr($name, $i, 1, $encoding))); } $option = $this->definition->getOptionForShortcut($name[$i]); if ($option->acceptValue()) { $this->addLongOption($option->getName(), $i === $len - 1 ? null : substr($name, $i + 1)); break; } else { $this->addLongOption($option->getName(), null); } } } /** * Parses a long option. * * @param string $token The current token */ private function parseLongOption($token) { $name = substr($token, 2); if (false !== $pos = strpos($name, '=')) { if (0 === \strlen($value = substr($name, $pos + 1))) { // if no value after "=" then substr() returns "" since php7 only, false before // see https://php.net/migration70.incompatible.php#119151 if (\PHP_VERSION_ID < 70000 && false === $value) { $value = ''; } array_unshift($this->parsed, $value); } $this->addLongOption(substr($name, 0, $pos), $value); } else { $this->addLongOption($name, null); } } /** * Parses an argument. * * @param string $token The current token * * @throws RuntimeException When too many arguments are given */ private function parseArgument($token) { $c = \count($this->arguments); // if input is expecting another argument, add it if ($this->definition->hasArgument($c)) { $arg = $this->definition->getArgument($c); $this->arguments[$arg->getName()] = $arg->isArray() ? [$token] : $token; // if last argument isArray(), append token to last argument } elseif ($this->definition->hasArgument($c - 1) && $this->definition->getArgument($c - 1)->isArray()) { $arg = $this->definition->getArgument($c - 1); $this->arguments[$arg->getName()][] = $token; // unexpected argument } else { $all = $this->definition->getArguments(); if (\count($all)) { throw new RuntimeException(sprintf('Too many arguments, expected arguments "%s".', implode('" "', array_keys($all)))); } throw new RuntimeException(sprintf('No arguments expected, got "%s".', $token)); } } /** * Adds a short option value. * * @param string $shortcut The short option key * @param mixed $value The value for the option * * @throws RuntimeException When option given doesn't exist */ private function addShortOption($shortcut, $value) { if (!$this->definition->hasShortcut($shortcut)) { throw new RuntimeException(sprintf('The "-%s" option does not exist.', $shortcut)); } $this->addLongOption($this->definition->getOptionForShortcut($shortcut)->getName(), $value); } /** * Adds a long option value. * * @param string $name The long option key * @param mixed $value The value for the option * * @throws RuntimeException When option given doesn't exist */ private function addLongOption($name, $value) { if (!$this->definition->hasOption($name)) { throw new RuntimeException(sprintf('The "--%s" option does not exist.', $name)); } $option = $this->definition->getOption($name); if (null !== $value && !$option->acceptValue()) { throw new RuntimeException(sprintf('The "--%s" option does not accept a value.', $name)); } if (\in_array($value, ['', null], true) && $option->acceptValue() && \count($this->parsed)) { // if option accepts an optional or mandatory argument // let's see if there is one provided $next = array_shift($this->parsed); if ((isset($next[0]) && '-' !== $next[0]) || \in_array($next, ['', null], true)) { $value = $next; } else { array_unshift($this->parsed, $next); } } if (null === $value) { if ($option->isValueRequired()) { throw new RuntimeException(sprintf('The "--%s" option requires a value.', $name)); } if (!$option->isArray() && !$option->isValueOptional()) { $value = true; } } if ($option->isArray()) { $this->options[$name][] = $value; } else { $this->options[$name] = $value; } } /** * {@inheritdoc} */ public function getFirstArgument() { $isOption = false; foreach ($this->tokens as $i => $token) { if ($token && '-' === $token[0]) { if (false !== strpos($token, '=') || !isset($this->tokens[$i + 1])) { continue; } // If it's a long option, consider that everything after "--" is the option name. // Otherwise, use the last char (if it's a short option set, only the last one can take a value with space separator) $name = '-' === $token[1] ? substr($token, 2) : substr($token, -1); if (!isset($this->options[$name]) && !$this->definition->hasShortcut($name)) { // noop } elseif ((isset($this->options[$name]) || isset($this->options[$name = $this->definition->shortcutToName($name)])) && $this->tokens[$i + 1] === $this->options[$name]) { $isOption = true; } continue; } if ($isOption) { $isOption = false; continue; } return $token; } return null; } /** * {@inheritdoc} */ public function hasParameterOption($values, $onlyParams = false) { $values = (array) $values; foreach ($this->tokens as $token) { if ($onlyParams && '--' === $token) { return false; } foreach ($values as $value) { // Options with values: // For long options, test for '--option=' at beginning // For short options, test for '-o' at beginning $leading = 0 === strpos($value, '--') ? $value.'=' : $value; if ($token === $value || '' !== $leading && 0 === strpos($token, $leading)) { return true; } } } return false; } /** * {@inheritdoc} */ public function getParameterOption($values, $default = false, $onlyParams = false) { $values = (array) $values; $tokens = $this->tokens; while (0 < \count($tokens)) { $token = array_shift($tokens); if ($onlyParams && '--' === $token) { return $default; } foreach ($values as $value) { if ($token === $value) { return array_shift($tokens); } // Options with values: // For long options, test for '--option=' at beginning // For short options, test for '-o' at beginning $leading = 0 === strpos($value, '--') ? $value.'=' : $value; if ('' !== $leading && 0 === strpos($token, $leading)) { return substr($token, \strlen($leading)); } } } return $default; } /** * Returns a stringified representation of the args passed to the command. * * @return string */ public function __toString() { $tokens = array_map(function ($token) { if (preg_match('{^(-[^=]+=)(.+)}', $token, $match)) { return $match[1].$this->escapeToken($match[2]); } if ($token && '-' !== $token[0]) { return $this->escapeToken($token); } return $token; }, $this->tokens); return implode(' ', $tokens); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Console\Input; use Symfony\Component\Console\Exception\InvalidArgumentException; use Symfony\Component\Console\Exception\LogicException; /** * Represents a command line option. * * @author Fabien Potencier */ class InputOption { const VALUE_NONE = 1; const VALUE_REQUIRED = 2; const VALUE_OPTIONAL = 4; const VALUE_IS_ARRAY = 8; private $name; private $shortcut; private $mode; private $default; private $description; /** * @param string $name The option name * @param string|array|null $shortcut The shortcuts, can be null, a string of shortcuts delimited by | or an array of shortcuts * @param int|null $mode The option mode: One of the VALUE_* constants * @param string $description A description text * @param string|string[]|int|bool|null $default The default value (must be null for self::VALUE_NONE) * * @throws InvalidArgumentException If option mode is invalid or incompatible */ public function __construct($name, $shortcut = null, $mode = null, $description = '', $default = null) { if (0 === strpos($name, '--')) { $name = substr($name, 2); } if (empty($name)) { throw new InvalidArgumentException('An option name cannot be empty.'); } if (empty($shortcut)) { $shortcut = null; } if (null !== $shortcut) { if (\is_array($shortcut)) { $shortcut = implode('|', $shortcut); } $shortcuts = preg_split('{(\|)-?}', ltrim($shortcut, '-')); $shortcuts = array_filter($shortcuts); $shortcut = implode('|', $shortcuts); if (empty($shortcut)) { throw new InvalidArgumentException('An option shortcut cannot be empty.'); } } if (null === $mode) { $mode = self::VALUE_NONE; } elseif (!\is_int($mode) || $mode > 15 || $mode < 1) { throw new InvalidArgumentException(sprintf('Option mode "%s" is not valid.', $mode)); } $this->name = $name; $this->shortcut = $shortcut; $this->mode = $mode; $this->description = $description; if ($this->isArray() && !$this->acceptValue()) { throw new InvalidArgumentException('Impossible to have an option mode VALUE_IS_ARRAY if the option does not accept a value.'); } $this->setDefault($default); } /** * Returns the option shortcut. * * @return string|null The shortcut */ public function getShortcut() { return $this->shortcut; } /** * Returns the option name. * * @return string The name */ public function getName() { return $this->name; } /** * Returns true if the option accepts a value. * * @return bool true if value mode is not self::VALUE_NONE, false otherwise */ public function acceptValue() { return $this->isValueRequired() || $this->isValueOptional(); } /** * Returns true if the option requires a value. * * @return bool true if value mode is self::VALUE_REQUIRED, false otherwise */ public function isValueRequired() { return self::VALUE_REQUIRED === (self::VALUE_REQUIRED & $this->mode); } /** * Returns true if the option takes an optional value. * * @return bool true if value mode is self::VALUE_OPTIONAL, false otherwise */ public function isValueOptional() { return self::VALUE_OPTIONAL === (self::VALUE_OPTIONAL & $this->mode); } /** * Returns true if the option can take multiple values. * * @return bool true if mode is self::VALUE_IS_ARRAY, false otherwise */ public function isArray() { return self::VALUE_IS_ARRAY === (self::VALUE_IS_ARRAY & $this->mode); } /** * Sets the default value. * * @param string|string[]|int|bool|null $default The default value * * @throws LogicException When incorrect default value is given */ public function setDefault($default = null) { if (self::VALUE_NONE === (self::VALUE_NONE & $this->mode) && null !== $default) { throw new LogicException('Cannot set a default value when using InputOption::VALUE_NONE mode.'); } if ($this->isArray()) { if (null === $default) { $default = []; } elseif (!\is_array($default)) { throw new LogicException('A default value for an array option must be an array.'); } } $this->default = $this->acceptValue() ? $default : false; } /** * Returns the default value. * * @return string|string[]|int|bool|null The default value */ public function getDefault() { return $this->default; } /** * Returns the description text. * * @return string The description text */ public function getDescription() { return $this->description; } /** * Checks whether the given option equals this one. * * @return bool */ public function equals(self $option) { return $option->getName() === $this->getName() && $option->getShortcut() === $this->getShortcut() && $option->getDefault() === $this->getDefault() && $option->isArray() === $this->isArray() && $option->isValueRequired() === $this->isValueRequired() && $option->isValueOptional() === $this->isValueOptional() ; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Console\Question; use Symfony\Component\Console\Exception\InvalidArgumentException; use Symfony\Component\Console\Exception\LogicException; /** * Represents a Question. * * @author Fabien Potencier */ class Question { private $question; private $attempts; private $hidden = false; private $hiddenFallback = true; private $autocompleterValues; private $validator; private $default; private $normalizer; /** * @param string $question The question to ask to the user * @param mixed $default The default answer to return if the user enters nothing */ public function __construct($question, $default = null) { $this->question = $question; $this->default = $default; } /** * Returns the question. * * @return string */ public function getQuestion() { return $this->question; } /** * Returns the default answer. * * @return mixed */ public function getDefault() { return $this->default; } /** * Returns whether the user response must be hidden. * * @return bool */ public function isHidden() { return $this->hidden; } /** * Sets whether the user response must be hidden or not. * * @param bool $hidden * * @return $this * * @throws LogicException In case the autocompleter is also used */ public function setHidden($hidden) { if ($this->autocompleterValues) { throw new LogicException('A hidden question cannot use the autocompleter.'); } $this->hidden = (bool) $hidden; return $this; } /** * In case the response can not be hidden, whether to fallback on non-hidden question or not. * * @return bool */ public function isHiddenFallback() { return $this->hiddenFallback; } /** * Sets whether to fallback on non-hidden question if the response can not be hidden. * * @param bool $fallback * * @return $this */ public function setHiddenFallback($fallback) { $this->hiddenFallback = (bool) $fallback; return $this; } /** * Gets values for the autocompleter. * * @return iterable|null */ public function getAutocompleterValues() { return $this->autocompleterValues; } /** * Sets values for the autocompleter. * * @param iterable|null $values * * @return $this * * @throws InvalidArgumentException * @throws LogicException */ public function setAutocompleterValues($values) { if (\is_array($values)) { $values = $this->isAssoc($values) ? array_merge(array_keys($values), array_values($values)) : array_values($values); } if (null !== $values && !\is_array($values) && !$values instanceof \Traversable) { throw new InvalidArgumentException('Autocompleter values can be either an array, `null` or a `Traversable` object.'); } if ($this->hidden) { throw new LogicException('A hidden question cannot use the autocompleter.'); } $this->autocompleterValues = $values; return $this; } /** * Sets a validator for the question. * * @return $this */ public function setValidator(callable $validator = null) { $this->validator = $validator; return $this; } /** * Gets the validator for the question. * * @return callable|null */ public function getValidator() { return $this->validator; } /** * Sets the maximum number of attempts. * * Null means an unlimited number of attempts. * * @param int|null $attempts * * @return $this * * @throws InvalidArgumentException in case the number of attempts is invalid */ public function setMaxAttempts($attempts) { if (null !== $attempts) { $attempts = (int) $attempts; if ($attempts < 1) { throw new InvalidArgumentException('Maximum number of attempts must be a positive value.'); } } $this->attempts = $attempts; return $this; } /** * Gets the maximum number of attempts. * * Null means an unlimited number of attempts. * * @return int|null */ public function getMaxAttempts() { return $this->attempts; } /** * Sets a normalizer for the response. * * The normalizer can be a callable (a string), a closure or a class implementing __invoke. * * @return $this */ public function setNormalizer(callable $normalizer) { $this->normalizer = $normalizer; return $this; } /** * Gets the normalizer for the response. * * The normalizer can ba a callable (a string), a closure or a class implementing __invoke. * * @return callable|null */ public function getNormalizer() { return $this->normalizer; } protected function isAssoc($array) { return (bool) \count(array_filter(array_keys($array), 'is_string')); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Console\Question; use Symfony\Component\Console\Exception\InvalidArgumentException; /** * Represents a choice question. * * @author Fabien Potencier */ class ChoiceQuestion extends Question { private $choices; private $multiselect = false; private $prompt = ' > '; private $errorMessage = 'Value "%s" is invalid'; /** * @param string $question The question to ask to the user * @param array $choices The list of available choices * @param mixed $default The default answer to return */ public function __construct($question, array $choices, $default = null) { if (!$choices) { throw new \LogicException('Choice question must have at least 1 choice available.'); } parent::__construct($question, $default); $this->choices = $choices; $this->setValidator($this->getDefaultValidator()); $this->setAutocompleterValues($choices); } /** * Returns available choices. * * @return array */ public function getChoices() { return $this->choices; } /** * Sets multiselect option. * * When multiselect is set to true, multiple choices can be answered. * * @param bool $multiselect * * @return $this */ public function setMultiselect($multiselect) { $this->multiselect = $multiselect; $this->setValidator($this->getDefaultValidator()); return $this; } /** * Returns whether the choices are multiselect. * * @return bool */ public function isMultiselect() { return $this->multiselect; } /** * Gets the prompt for choices. * * @return string */ public function getPrompt() { return $this->prompt; } /** * Sets the prompt for choices. * * @param string $prompt * * @return $this */ public function setPrompt($prompt) { $this->prompt = $prompt; return $this; } /** * Sets the error message for invalid values. * * The error message has a string placeholder (%s) for the invalid value. * * @param string $errorMessage * * @return $this */ public function setErrorMessage($errorMessage) { $this->errorMessage = $errorMessage; $this->setValidator($this->getDefaultValidator()); return $this; } /** * Returns the default answer validator. * * @return callable */ private function getDefaultValidator() { $choices = $this->choices; $errorMessage = $this->errorMessage; $multiselect = $this->multiselect; $isAssoc = $this->isAssoc($choices); return function ($selected) use ($choices, $errorMessage, $multiselect, $isAssoc) { if ($multiselect) { // Check for a separated comma values if (!preg_match('/^[^,]+(?:,[^,]+)*$/', $selected, $matches)) { throw new InvalidArgumentException(sprintf($errorMessage, $selected)); } $selectedChoices = array_map('trim', explode(',', $selected)); } else { $selectedChoices = [trim($selected)]; } $multiselectChoices = []; foreach ($selectedChoices as $value) { $results = []; foreach ($choices as $key => $choice) { if ($choice === $value) { $results[] = $key; } } if (\count($results) > 1) { throw new InvalidArgumentException(sprintf('The provided answer is ambiguous. Value should be one of "%s".', implode('" or "', $results))); } $result = array_search($value, $choices); if (!$isAssoc) { if (false !== $result) { $result = $choices[$result]; } elseif (isset($choices[$value])) { $result = $choices[$value]; } } elseif (false === $result && isset($choices[$value])) { $result = $value; } if (false === $result) { throw new InvalidArgumentException(sprintf($errorMessage, $value)); } $multiselectChoices[] = (string) $result; } if ($multiselect) { return $multiselectChoices; } return current($multiselectChoices); }; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Console\Question; /** * Represents a yes/no question. * * @author Fabien Potencier */ class ConfirmationQuestion extends Question { private $trueAnswerRegex; /** * @param string $question The question to ask to the user * @param bool $default The default answer to return, true or false * @param string $trueAnswerRegex A regex to match the "yes" answer */ public function __construct($question, $default = true, $trueAnswerRegex = '/^y/i') { parent::__construct($question, (bool) $default); $this->trueAnswerRegex = $trueAnswerRegex; $this->setNormalizer($this->getDefaultNormalizer()); } /** * Returns the default answer normalizer. * * @return callable */ private function getDefaultNormalizer() { $default = $this->getDefault(); $regex = $this->trueAnswerRegex; return function ($answer) use ($default, $regex) { if (\is_bool($answer)) { return $answer; } $answerIsTrue = (bool) preg_match($regex, $answer); if (false === $default) { return $answer && $answerIsTrue; } return '' === $answer || $answerIsTrue; }; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Console; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Command\HelpCommand; use Symfony\Component\Console\Command\ListCommand; use Symfony\Component\Console\CommandLoader\CommandLoaderInterface; use Symfony\Component\Console\Event\ConsoleCommandEvent; use Symfony\Component\Console\Event\ConsoleErrorEvent; use Symfony\Component\Console\Event\ConsoleExceptionEvent; use Symfony\Component\Console\Event\ConsoleTerminateEvent; use Symfony\Component\Console\Exception\CommandNotFoundException; use Symfony\Component\Console\Exception\ExceptionInterface; use Symfony\Component\Console\Exception\LogicException; use Symfony\Component\Console\Formatter\OutputFormatter; use Symfony\Component\Console\Helper\DebugFormatterHelper; use Symfony\Component\Console\Helper\FormatterHelper; use Symfony\Component\Console\Helper\Helper; use Symfony\Component\Console\Helper\HelperSet; use Symfony\Component\Console\Helper\ProcessHelper; use Symfony\Component\Console\Helper\QuestionHelper; use Symfony\Component\Console\Input\ArgvInput; use Symfony\Component\Console\Input\ArrayInput; use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputAwareInterface; use Symfony\Component\Console\Input\InputDefinition; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Input\StreamableInputInterface; use Symfony\Component\Console\Output\ConsoleOutput; use Symfony\Component\Console\Output\ConsoleOutputInterface; use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Debug\ErrorHandler; use Symfony\Component\Debug\Exception\FatalThrowableError; use Symfony\Component\EventDispatcher\EventDispatcherInterface; /** * An Application is the container for a collection of commands. * * It is the main entry point of a Console application. * * This class is optimized for a standard CLI environment. * * Usage: * * $app = new Application('myapp', '1.0 (stable)'); * $app->add(new SimpleCommand()); * $app->run(); * * @author Fabien Potencier */ class Application { private $commands = []; private $wantHelps = false; private $runningCommand; private $name; private $version; private $commandLoader; private $catchExceptions = true; private $autoExit = true; private $definition; private $helperSet; private $dispatcher; private $terminal; private $defaultCommand; private $singleCommand = false; private $initialized; /** * @param string $name The name of the application * @param string $version The version of the application */ public function __construct($name = 'UNKNOWN', $version = 'UNKNOWN') { $this->name = $name; $this->version = $version; $this->terminal = new Terminal(); $this->defaultCommand = 'list'; } public function setDispatcher(EventDispatcherInterface $dispatcher) { $this->dispatcher = $dispatcher; } public function setCommandLoader(CommandLoaderInterface $commandLoader) { $this->commandLoader = $commandLoader; } /** * Runs the current application. * * @return int 0 if everything went fine, or an error code * * @throws \Exception When running fails. Bypass this when {@link setCatchExceptions()}. */ public function run(InputInterface $input = null, OutputInterface $output = null) { putenv('LINES='.$this->terminal->getHeight()); putenv('COLUMNS='.$this->terminal->getWidth()); if (null === $input) { $input = new ArgvInput(); } if (null === $output) { $output = new ConsoleOutput(); } $renderException = function ($e) use ($output) { if (!$e instanceof \Exception) { $e = class_exists(FatalThrowableError::class) ? new FatalThrowableError($e) : new \ErrorException($e->getMessage(), $e->getCode(), \E_ERROR, $e->getFile(), $e->getLine()); } if ($output instanceof ConsoleOutputInterface) { $this->renderException($e, $output->getErrorOutput()); } else { $this->renderException($e, $output); } }; if ($phpHandler = set_exception_handler($renderException)) { restore_exception_handler(); if (!\is_array($phpHandler) || !$phpHandler[0] instanceof ErrorHandler) { $debugHandler = true; } elseif ($debugHandler = $phpHandler[0]->setExceptionHandler($renderException)) { $phpHandler[0]->setExceptionHandler($debugHandler); } } if (null !== $this->dispatcher && $this->dispatcher->hasListeners(ConsoleEvents::EXCEPTION)) { @trigger_error(sprintf('The "ConsoleEvents::EXCEPTION" event is deprecated since Symfony 3.3 and will be removed in 4.0. Listen to the "ConsoleEvents::ERROR" event instead.'), \E_USER_DEPRECATED); } $this->configureIO($input, $output); try { $exitCode = $this->doRun($input, $output); } catch (\Exception $e) { if (!$this->catchExceptions) { throw $e; } $renderException($e); $exitCode = $e->getCode(); if (is_numeric($exitCode)) { $exitCode = (int) $exitCode; if (0 === $exitCode) { $exitCode = 1; } } else { $exitCode = 1; } } finally { // if the exception handler changed, keep it // otherwise, unregister $renderException if (!$phpHandler) { if (set_exception_handler($renderException) === $renderException) { restore_exception_handler(); } restore_exception_handler(); } elseif (!$debugHandler) { $finalHandler = $phpHandler[0]->setExceptionHandler(null); if ($finalHandler !== $renderException) { $phpHandler[0]->setExceptionHandler($finalHandler); } } } if ($this->autoExit) { if ($exitCode > 255) { $exitCode = 255; } exit($exitCode); } return $exitCode; } /** * Runs the current application. * * @return int 0 if everything went fine, or an error code */ public function doRun(InputInterface $input, OutputInterface $output) { if (true === $input->hasParameterOption(['--version', '-V'], true)) { $output->writeln($this->getLongVersion()); return 0; } try { // Makes ArgvInput::getFirstArgument() able to distinguish an option from an argument. $input->bind($this->getDefinition()); } catch (ExceptionInterface $e) { // Errors must be ignored, full binding/validation happens later when the command is known. } $name = $this->getCommandName($input); if (true === $input->hasParameterOption(['--help', '-h'], true)) { if (!$name) { $name = 'help'; $input = new ArrayInput(['command_name' => $this->defaultCommand]); } else { $this->wantHelps = true; } } if (!$name) { $name = $this->defaultCommand; $definition = $this->getDefinition(); $definition->setArguments(array_merge( $definition->getArguments(), [ 'command' => new InputArgument('command', InputArgument::OPTIONAL, $definition->getArgument('command')->getDescription(), $name), ] )); } try { $e = $this->runningCommand = null; // the command name MUST be the first element of the input $command = $this->find($name); } catch (\Exception $e) { } catch (\Throwable $e) { } if (null !== $e) { if (null !== $this->dispatcher) { $event = new ConsoleErrorEvent($input, $output, $e); $this->dispatcher->dispatch(ConsoleEvents::ERROR, $event); $e = $event->getError(); if (0 === $event->getExitCode()) { return 0; } } throw $e; } $this->runningCommand = $command; $exitCode = $this->doRunCommand($command, $input, $output); $this->runningCommand = null; return $exitCode; } public function setHelperSet(HelperSet $helperSet) { $this->helperSet = $helperSet; } /** * Get the helper set associated with the command. * * @return HelperSet The HelperSet instance associated with this command */ public function getHelperSet() { if (!$this->helperSet) { $this->helperSet = $this->getDefaultHelperSet(); } return $this->helperSet; } public function setDefinition(InputDefinition $definition) { $this->definition = $definition; } /** * Gets the InputDefinition related to this Application. * * @return InputDefinition The InputDefinition instance */ public function getDefinition() { if (!$this->definition) { $this->definition = $this->getDefaultInputDefinition(); } if ($this->singleCommand) { $inputDefinition = $this->definition; $inputDefinition->setArguments(); return $inputDefinition; } return $this->definition; } /** * Gets the help message. * * @return string A help message */ public function getHelp() { return $this->getLongVersion(); } /** * Gets whether to catch exceptions or not during commands execution. * * @return bool Whether to catch exceptions or not during commands execution */ public function areExceptionsCaught() { return $this->catchExceptions; } /** * Sets whether to catch exceptions or not during commands execution. * * @param bool $boolean Whether to catch exceptions or not during commands execution */ public function setCatchExceptions($boolean) { $this->catchExceptions = (bool) $boolean; } /** * Gets whether to automatically exit after a command execution or not. * * @return bool Whether to automatically exit after a command execution or not */ public function isAutoExitEnabled() { return $this->autoExit; } /** * Sets whether to automatically exit after a command execution or not. * * @param bool $boolean Whether to automatically exit after a command execution or not */ public function setAutoExit($boolean) { $this->autoExit = (bool) $boolean; } /** * Gets the name of the application. * * @return string The application name */ public function getName() { return $this->name; } /** * Sets the application name. * * @param string $name The application name */ public function setName($name) { $this->name = $name; } /** * Gets the application version. * * @return string The application version */ public function getVersion() { return $this->version; } /** * Sets the application version. * * @param string $version The application version */ public function setVersion($version) { $this->version = $version; } /** * Returns the long version of the application. * * @return string The long application version */ public function getLongVersion() { if ('UNKNOWN' !== $this->getName()) { if ('UNKNOWN' !== $this->getVersion()) { return sprintf('%s %s', $this->getName(), $this->getVersion()); } return $this->getName(); } return 'Console Tool'; } /** * Registers a new command. * * @param string $name The command name * * @return Command The newly created command */ public function register($name) { return $this->add(new Command($name)); } /** * Adds an array of command objects. * * If a Command is not enabled it will not be added. * * @param Command[] $commands An array of commands */ public function addCommands(array $commands) { foreach ($commands as $command) { $this->add($command); } } /** * Adds a command object. * * If a command with the same name already exists, it will be overridden. * If the command is not enabled it will not be added. * * @return Command|null The registered command if enabled or null */ public function add(Command $command) { $this->init(); $command->setApplication($this); if (!$command->isEnabled()) { $command->setApplication(null); return null; } // Will throw if the command is not correctly initialized. $command->getDefinition(); if (!$command->getName()) { throw new LogicException(sprintf('The command defined in "%s" cannot have an empty name.', \get_class($command))); } $this->commands[$command->getName()] = $command; foreach ($command->getAliases() as $alias) { $this->commands[$alias] = $command; } return $command; } /** * Returns a registered command by name or alias. * * @param string $name The command name or alias * * @return Command A Command object * * @throws CommandNotFoundException When given command name does not exist */ public function get($name) { $this->init(); if (!$this->has($name)) { throw new CommandNotFoundException(sprintf('The command "%s" does not exist.', $name)); } // When the command has a different name than the one used at the command loader level if (!isset($this->commands[$name])) { throw new CommandNotFoundException(sprintf('The "%s" command cannot be found because it is registered under multiple names. Make sure you don\'t set a different name via constructor or "setName()".', $name)); } $command = $this->commands[$name]; if ($this->wantHelps) { $this->wantHelps = false; $helpCommand = $this->get('help'); $helpCommand->setCommand($command); return $helpCommand; } return $command; } /** * Returns true if the command exists, false otherwise. * * @param string $name The command name or alias * * @return bool true if the command exists, false otherwise */ public function has($name) { $this->init(); return isset($this->commands[$name]) || ($this->commandLoader && $this->commandLoader->has($name) && $this->add($this->commandLoader->get($name))); } /** * Returns an array of all unique namespaces used by currently registered commands. * * It does not return the global namespace which always exists. * * @return string[] An array of namespaces */ public function getNamespaces() { $namespaces = []; foreach ($this->all() as $command) { if ($command->isHidden()) { continue; } $namespaces = array_merge($namespaces, $this->extractAllNamespaces($command->getName())); foreach ($command->getAliases() as $alias) { $namespaces = array_merge($namespaces, $this->extractAllNamespaces($alias)); } } return array_values(array_unique(array_filter($namespaces))); } /** * Finds a registered namespace by a name or an abbreviation. * * @param string $namespace A namespace or abbreviation to search for * * @return string A registered namespace * * @throws CommandNotFoundException When namespace is incorrect or ambiguous */ public function findNamespace($namespace) { $allNamespaces = $this->getNamespaces(); $expr = preg_replace_callback('{([^:]+|)}', function ($matches) { return preg_quote($matches[1]).'[^:]*'; }, $namespace); $namespaces = preg_grep('{^'.$expr.'}', $allNamespaces); if (empty($namespaces)) { $message = sprintf('There are no commands defined in the "%s" namespace.', $namespace); if ($alternatives = $this->findAlternatives($namespace, $allNamespaces)) { if (1 == \count($alternatives)) { $message .= "\n\nDid you mean this?\n "; } else { $message .= "\n\nDid you mean one of these?\n "; } $message .= implode("\n ", $alternatives); } throw new CommandNotFoundException($message, $alternatives); } $exact = \in_array($namespace, $namespaces, true); if (\count($namespaces) > 1 && !$exact) { throw new CommandNotFoundException(sprintf("The namespace \"%s\" is ambiguous.\nDid you mean one of these?\n%s.", $namespace, $this->getAbbreviationSuggestions(array_values($namespaces))), array_values($namespaces)); } return $exact ? $namespace : reset($namespaces); } /** * Finds a command by name or alias. * * Contrary to get, this command tries to find the best * match if you give it an abbreviation of a name or alias. * * @param string $name A command name or a command alias * * @return Command A Command instance * * @throws CommandNotFoundException When command name is incorrect or ambiguous */ public function find($name) { $this->init(); $aliases = []; foreach ($this->commands as $command) { foreach ($command->getAliases() as $alias) { if (!$this->has($alias)) { $this->commands[$alias] = $command; } } } if ($this->has($name)) { return $this->get($name); } $allCommands = $this->commandLoader ? array_merge($this->commandLoader->getNames(), array_keys($this->commands)) : array_keys($this->commands); $expr = preg_replace_callback('{([^:]+|)}', function ($matches) { return preg_quote($matches[1]).'[^:]*'; }, $name); $commands = preg_grep('{^'.$expr.'}', $allCommands); if (empty($commands)) { $commands = preg_grep('{^'.$expr.'}i', $allCommands); } // if no commands matched or we just matched namespaces if (empty($commands) || \count(preg_grep('{^'.$expr.'$}i', $commands)) < 1) { if (false !== $pos = strrpos($name, ':')) { // check if a namespace exists and contains commands $this->findNamespace(substr($name, 0, $pos)); } $message = sprintf('Command "%s" is not defined.', $name); if ($alternatives = $this->findAlternatives($name, $allCommands)) { // remove hidden commands $alternatives = array_filter($alternatives, function ($name) { return !$this->get($name)->isHidden(); }); if (1 == \count($alternatives)) { $message .= "\n\nDid you mean this?\n "; } else { $message .= "\n\nDid you mean one of these?\n "; } $message .= implode("\n ", $alternatives); } throw new CommandNotFoundException($message, array_values($alternatives)); } // filter out aliases for commands which are already on the list if (\count($commands) > 1) { $commandList = $this->commandLoader ? array_merge(array_flip($this->commandLoader->getNames()), $this->commands) : $this->commands; $commands = array_unique(array_filter($commands, function ($nameOrAlias) use (&$commandList, $commands, &$aliases) { if (!$commandList[$nameOrAlias] instanceof Command) { $commandList[$nameOrAlias] = $this->commandLoader->get($nameOrAlias); } $commandName = $commandList[$nameOrAlias]->getName(); $aliases[$nameOrAlias] = $commandName; return $commandName === $nameOrAlias || !\in_array($commandName, $commands); })); } $exact = \in_array($name, $commands, true) || isset($aliases[$name]); if (\count($commands) > 1 && !$exact) { $usableWidth = $this->terminal->getWidth() - 10; $abbrevs = array_values($commands); $maxLen = 0; foreach ($abbrevs as $abbrev) { $maxLen = max(Helper::strlen($abbrev), $maxLen); } $abbrevs = array_map(function ($cmd) use ($commandList, $usableWidth, $maxLen) { if ($commandList[$cmd]->isHidden()) { return false; } $abbrev = str_pad($cmd, $maxLen, ' ').' '.$commandList[$cmd]->getDescription(); return Helper::strlen($abbrev) > $usableWidth ? Helper::substr($abbrev, 0, $usableWidth - 3).'...' : $abbrev; }, array_values($commands)); $suggestions = $this->getAbbreviationSuggestions(array_filter($abbrevs)); throw new CommandNotFoundException(sprintf("Command \"%s\" is ambiguous.\nDid you mean one of these?\n%s.", $name, $suggestions), array_values($commands)); } return $this->get($exact ? $name : reset($commands)); } /** * Gets the commands (registered in the given namespace if provided). * * The array keys are the full names and the values the command instances. * * @param string $namespace A namespace name * * @return Command[] An array of Command instances */ public function all($namespace = null) { $this->init(); if (null === $namespace) { if (!$this->commandLoader) { return $this->commands; } $commands = $this->commands; foreach ($this->commandLoader->getNames() as $name) { if (!isset($commands[$name]) && $this->has($name)) { $commands[$name] = $this->get($name); } } return $commands; } $commands = []; foreach ($this->commands as $name => $command) { if ($namespace === $this->extractNamespace($name, substr_count($namespace, ':') + 1)) { $commands[$name] = $command; } } if ($this->commandLoader) { foreach ($this->commandLoader->getNames() as $name) { if (!isset($commands[$name]) && $namespace === $this->extractNamespace($name, substr_count($namespace, ':') + 1) && $this->has($name)) { $commands[$name] = $this->get($name); } } } return $commands; } /** * Returns an array of possible abbreviations given a set of names. * * @param array $names An array of names * * @return array An array of abbreviations */ public static function getAbbreviations($names) { $abbrevs = []; foreach ($names as $name) { for ($len = \strlen($name); $len > 0; --$len) { $abbrev = substr($name, 0, $len); $abbrevs[$abbrev][] = $name; } } return $abbrevs; } /** * Renders a caught exception. */ public function renderException(\Exception $e, OutputInterface $output) { $output->writeln('', OutputInterface::VERBOSITY_QUIET); $this->doRenderException($e, $output); if (null !== $this->runningCommand) { $output->writeln(sprintf('%s', sprintf($this->runningCommand->getSynopsis(), $this->getName())), OutputInterface::VERBOSITY_QUIET); $output->writeln('', OutputInterface::VERBOSITY_QUIET); } } protected function doRenderException(\Exception $e, OutputInterface $output) { do { $message = trim($e->getMessage()); if ('' === $message || OutputInterface::VERBOSITY_VERBOSE <= $output->getVerbosity()) { $title = sprintf(' [%s%s] ', \get_class($e), 0 !== ($code = $e->getCode()) ? ' ('.$code.')' : ''); $len = Helper::strlen($title); } else { $len = 0; } $width = $this->terminal->getWidth() ? $this->terminal->getWidth() - 1 : \PHP_INT_MAX; // HHVM only accepts 32 bits integer in str_split, even when PHP_INT_MAX is a 64 bit integer: https://github.com/facebook/hhvm/issues/1327 if (\defined('HHVM_VERSION') && $width > 1 << 31) { $width = 1 << 31; } $lines = []; foreach ('' !== $message ? preg_split('/\r?\n/', $message) : [] as $line) { foreach ($this->splitStringByWidth($line, $width - 4) as $line) { // pre-format lines to get the right string length $lineLength = Helper::strlen($line) + 4; $lines[] = [$line, $lineLength]; $len = max($lineLength, $len); } } $messages = []; if (!$e instanceof ExceptionInterface || OutputInterface::VERBOSITY_VERBOSE <= $output->getVerbosity()) { $messages[] = sprintf('%s', OutputFormatter::escape(sprintf('In %s line %s:', basename($e->getFile()) ?: 'n/a', $e->getLine() ?: 'n/a'))); } $messages[] = $emptyLine = sprintf('%s', str_repeat(' ', $len)); if ('' === $message || OutputInterface::VERBOSITY_VERBOSE <= $output->getVerbosity()) { $messages[] = sprintf('%s%s', $title, str_repeat(' ', max(0, $len - Helper::strlen($title)))); } foreach ($lines as $line) { $messages[] = sprintf(' %s %s', OutputFormatter::escape($line[0]), str_repeat(' ', $len - $line[1])); } $messages[] = $emptyLine; $messages[] = ''; $output->writeln($messages, OutputInterface::VERBOSITY_QUIET); if (OutputInterface::VERBOSITY_VERBOSE <= $output->getVerbosity()) { $output->writeln('Exception trace:', OutputInterface::VERBOSITY_QUIET); // exception related properties $trace = $e->getTrace(); array_unshift($trace, [ 'function' => '', 'file' => $e->getFile() ?: 'n/a', 'line' => $e->getLine() ?: 'n/a', 'args' => [], ]); for ($i = 0, $count = \count($trace); $i < $count; ++$i) { $class = isset($trace[$i]['class']) ? $trace[$i]['class'] : ''; $type = isset($trace[$i]['type']) ? $trace[$i]['type'] : ''; $function = isset($trace[$i]['function']) ? $trace[$i]['function'] : ''; $file = isset($trace[$i]['file']) ? $trace[$i]['file'] : 'n/a'; $line = isset($trace[$i]['line']) ? $trace[$i]['line'] : 'n/a'; $output->writeln(sprintf(' %s%s at %s:%s', $class, $function ? $type.$function.'()' : '', $file, $line), OutputInterface::VERBOSITY_QUIET); } $output->writeln('', OutputInterface::VERBOSITY_QUIET); } } while ($e = $e->getPrevious()); } /** * Tries to figure out the terminal width in which this application runs. * * @return int|null * * @deprecated since version 3.2, to be removed in 4.0. Create a Terminal instance instead. */ protected function getTerminalWidth() { @trigger_error(sprintf('The "%s()" method is deprecated as of 3.2 and will be removed in 4.0. Create a Terminal instance instead.', __METHOD__), \E_USER_DEPRECATED); return $this->terminal->getWidth(); } /** * Tries to figure out the terminal height in which this application runs. * * @return int|null * * @deprecated since version 3.2, to be removed in 4.0. Create a Terminal instance instead. */ protected function getTerminalHeight() { @trigger_error(sprintf('The "%s()" method is deprecated as of 3.2 and will be removed in 4.0. Create a Terminal instance instead.', __METHOD__), \E_USER_DEPRECATED); return $this->terminal->getHeight(); } /** * Tries to figure out the terminal dimensions based on the current environment. * * @return array Array containing width and height * * @deprecated since version 3.2, to be removed in 4.0. Create a Terminal instance instead. */ public function getTerminalDimensions() { @trigger_error(sprintf('The "%s()" method is deprecated as of 3.2 and will be removed in 4.0. Create a Terminal instance instead.', __METHOD__), \E_USER_DEPRECATED); return [$this->terminal->getWidth(), $this->terminal->getHeight()]; } /** * Sets terminal dimensions. * * Can be useful to force terminal dimensions for functional tests. * * @param int $width The width * @param int $height The height * * @return $this * * @deprecated since version 3.2, to be removed in 4.0. Set the COLUMNS and LINES env vars instead. */ public function setTerminalDimensions($width, $height) { @trigger_error(sprintf('The "%s()" method is deprecated as of 3.2 and will be removed in 4.0. Set the COLUMNS and LINES env vars instead.', __METHOD__), \E_USER_DEPRECATED); putenv('COLUMNS='.$width); putenv('LINES='.$height); return $this; } /** * Configures the input and output instances based on the user arguments and options. */ protected function configureIO(InputInterface $input, OutputInterface $output) { if (true === $input->hasParameterOption(['--ansi'], true)) { $output->setDecorated(true); } elseif (true === $input->hasParameterOption(['--no-ansi'], true)) { $output->setDecorated(false); } if (true === $input->hasParameterOption(['--no-interaction', '-n'], true)) { $input->setInteractive(false); } elseif (\function_exists('posix_isatty')) { $inputStream = null; if ($input instanceof StreamableInputInterface) { $inputStream = $input->getStream(); } // This check ensures that calling QuestionHelper::setInputStream() works // To be removed in 4.0 (in the same time as QuestionHelper::setInputStream) if (!$inputStream && $this->getHelperSet()->has('question')) { $inputStream = $this->getHelperSet()->get('question')->getInputStream(false); } if (!@posix_isatty($inputStream) && false === getenv('SHELL_INTERACTIVE')) { $input->setInteractive(false); } } switch ($shellVerbosity = (int) getenv('SHELL_VERBOSITY')) { case -1: $output->setVerbosity(OutputInterface::VERBOSITY_QUIET); break; case 1: $output->setVerbosity(OutputInterface::VERBOSITY_VERBOSE); break; case 2: $output->setVerbosity(OutputInterface::VERBOSITY_VERY_VERBOSE); break; case 3: $output->setVerbosity(OutputInterface::VERBOSITY_DEBUG); break; default: $shellVerbosity = 0; break; } if (true === $input->hasParameterOption(['--quiet', '-q'], true)) { $output->setVerbosity(OutputInterface::VERBOSITY_QUIET); $shellVerbosity = -1; } else { if ($input->hasParameterOption('-vvv', true) || $input->hasParameterOption('--verbose=3', true) || 3 === $input->getParameterOption('--verbose', false, true)) { $output->setVerbosity(OutputInterface::VERBOSITY_DEBUG); $shellVerbosity = 3; } elseif ($input->hasParameterOption('-vv', true) || $input->hasParameterOption('--verbose=2', true) || 2 === $input->getParameterOption('--verbose', false, true)) { $output->setVerbosity(OutputInterface::VERBOSITY_VERY_VERBOSE); $shellVerbosity = 2; } elseif ($input->hasParameterOption('-v', true) || $input->hasParameterOption('--verbose=1', true) || $input->hasParameterOption('--verbose', true) || $input->getParameterOption('--verbose', false, true)) { $output->setVerbosity(OutputInterface::VERBOSITY_VERBOSE); $shellVerbosity = 1; } } if (-1 === $shellVerbosity) { $input->setInteractive(false); } putenv('SHELL_VERBOSITY='.$shellVerbosity); $_ENV['SHELL_VERBOSITY'] = $shellVerbosity; $_SERVER['SHELL_VERBOSITY'] = $shellVerbosity; } /** * Runs the current command. * * If an event dispatcher has been attached to the application, * events are also dispatched during the life-cycle of the command. * * @return int 0 if everything went fine, or an error code */ protected function doRunCommand(Command $command, InputInterface $input, OutputInterface $output) { foreach ($command->getHelperSet() as $helper) { if ($helper instanceof InputAwareInterface) { $helper->setInput($input); } } if (null === $this->dispatcher) { return $command->run($input, $output); } // bind before the console.command event, so the listeners have access to input options/arguments try { $command->mergeApplicationDefinition(); $input->bind($command->getDefinition()); } catch (ExceptionInterface $e) { // ignore invalid options/arguments for now, to allow the event listeners to customize the InputDefinition } $event = new ConsoleCommandEvent($command, $input, $output); $e = null; try { $this->dispatcher->dispatch(ConsoleEvents::COMMAND, $event); if ($event->commandShouldRun()) { $exitCode = $command->run($input, $output); } else { $exitCode = ConsoleCommandEvent::RETURN_CODE_DISABLED; } } catch (\Exception $e) { } catch (\Throwable $e) { } if (null !== $e) { if ($this->dispatcher->hasListeners(ConsoleEvents::EXCEPTION)) { $x = $e instanceof \Exception ? $e : new FatalThrowableError($e); $event = new ConsoleExceptionEvent($command, $input, $output, $x, $x->getCode()); $this->dispatcher->dispatch(ConsoleEvents::EXCEPTION, $event); if ($x !== $event->getException()) { $e = $event->getException(); } } $event = new ConsoleErrorEvent($input, $output, $e, $command); $this->dispatcher->dispatch(ConsoleEvents::ERROR, $event); $e = $event->getError(); if (0 === $exitCode = $event->getExitCode()) { $e = null; } } $event = new ConsoleTerminateEvent($command, $input, $output, $exitCode); $this->dispatcher->dispatch(ConsoleEvents::TERMINATE, $event); if (null !== $e) { throw $e; } return $event->getExitCode(); } /** * Gets the name of the command based on input. * * @return string|null */ protected function getCommandName(InputInterface $input) { return $this->singleCommand ? $this->defaultCommand : $input->getFirstArgument(); } /** * Gets the default input definition. * * @return InputDefinition An InputDefinition instance */ protected function getDefaultInputDefinition() { return new InputDefinition([ new InputArgument('command', InputArgument::REQUIRED, 'The command to execute'), new InputOption('--help', '-h', InputOption::VALUE_NONE, 'Display this help message'), new InputOption('--quiet', '-q', InputOption::VALUE_NONE, 'Do not output any message'), new InputOption('--verbose', '-v|vv|vvv', InputOption::VALUE_NONE, 'Increase the verbosity of messages: 1 for normal output, 2 for more verbose output and 3 for debug'), new InputOption('--version', '-V', InputOption::VALUE_NONE, 'Display this application version'), new InputOption('--ansi', '', InputOption::VALUE_NONE, 'Force ANSI output'), new InputOption('--no-ansi', '', InputOption::VALUE_NONE, 'Disable ANSI output'), new InputOption('--no-interaction', '-n', InputOption::VALUE_NONE, 'Do not ask any interactive question'), ]); } /** * Gets the default commands that should always be available. * * @return Command[] An array of default Command instances */ protected function getDefaultCommands() { return [new HelpCommand(), new ListCommand()]; } /** * Gets the default helper set with the helpers that should always be available. * * @return HelperSet A HelperSet instance */ protected function getDefaultHelperSet() { return new HelperSet([ new FormatterHelper(), new DebugFormatterHelper(), new ProcessHelper(), new QuestionHelper(), ]); } /** * Returns abbreviated suggestions in string format. * * @param array $abbrevs Abbreviated suggestions to convert * * @return string A formatted string of abbreviated suggestions */ private function getAbbreviationSuggestions($abbrevs) { return ' '.implode("\n ", $abbrevs); } /** * Returns the namespace part of the command name. * * This method is not part of public API and should not be used directly. * * @param string $name The full name of the command * @param string $limit The maximum number of parts of the namespace * * @return string The namespace of the command */ public function extractNamespace($name, $limit = null) { $parts = explode(':', $name, -1); return implode(':', null === $limit ? $parts : \array_slice($parts, 0, $limit)); } /** * Finds alternative of $name among $collection, * if nothing is found in $collection, try in $abbrevs. * * @param string $name The string * @param iterable $collection The collection * * @return string[] A sorted array of similar string */ private function findAlternatives($name, $collection) { $threshold = 1e3; $alternatives = []; $collectionParts = []; foreach ($collection as $item) { $collectionParts[$item] = explode(':', $item); } foreach (explode(':', $name) as $i => $subname) { foreach ($collectionParts as $collectionName => $parts) { $exists = isset($alternatives[$collectionName]); if (!isset($parts[$i]) && $exists) { $alternatives[$collectionName] += $threshold; continue; } elseif (!isset($parts[$i])) { continue; } $lev = levenshtein($subname, $parts[$i]); if ($lev <= \strlen($subname) / 3 || '' !== $subname && false !== strpos($parts[$i], $subname)) { $alternatives[$collectionName] = $exists ? $alternatives[$collectionName] + $lev : $lev; } elseif ($exists) { $alternatives[$collectionName] += $threshold; } } } foreach ($collection as $item) { $lev = levenshtein($name, $item); if ($lev <= \strlen($name) / 3 || false !== strpos($item, $name)) { $alternatives[$item] = isset($alternatives[$item]) ? $alternatives[$item] - $lev : $lev; } } $alternatives = array_filter($alternatives, function ($lev) use ($threshold) { return $lev < 2 * $threshold; }); ksort($alternatives, \SORT_NATURAL | \SORT_FLAG_CASE); return array_keys($alternatives); } /** * Sets the default Command name. * * @param string $commandName The Command name * @param bool $isSingleCommand Set to true if there is only one command in this application * * @return self */ public function setDefaultCommand($commandName, $isSingleCommand = false) { $this->defaultCommand = $commandName; if ($isSingleCommand) { // Ensure the command exist $this->find($commandName); $this->singleCommand = true; } return $this; } /** * @internal */ public function isSingleCommand() { return $this->singleCommand; } private function splitStringByWidth($string, $width) { // str_split is not suitable for multi-byte characters, we should use preg_split to get char array properly. // additionally, array_slice() is not enough as some character has doubled width. // we need a function to split string not by character count but by string width if (false === $encoding = mb_detect_encoding($string, null, true)) { return str_split($string, $width); } $utf8String = mb_convert_encoding($string, 'utf8', $encoding); $lines = []; $line = ''; foreach (preg_split('//u', $utf8String) as $char) { // test if $char could be appended to current line if (mb_strwidth($line.$char, 'utf8') <= $width) { $line .= $char; continue; } // if not, push current line to array and make new line $lines[] = str_pad($line, $width); $line = $char; } $lines[] = \count($lines) ? str_pad($line, $width) : $line; mb_convert_variables($encoding, 'utf8', $lines); return $lines; } /** * Returns all namespaces of the command name. * * @param string $name The full name of the command * * @return string[] The namespaces of the command */ private function extractAllNamespaces($name) { // -1 as third argument is needed to skip the command short name when exploding $parts = explode(':', $name, -1); $namespaces = []; foreach ($parts as $part) { if (\count($namespaces)) { $namespaces[] = end($namespaces).':'.$part; } else { $namespaces[] = $part; } } return $namespaces; } private function init() { if ($this->initialized) { return; } $this->initialized = true; foreach ($this->getDefaultCommands() as $command) { $this->add($command); } } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Console\Tester; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\ArrayInput; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Console\Output\StreamOutput; /** * Eases the testing of console commands. * * @author Fabien Potencier * @author Robin Chalas */ class CommandTester { private $command; private $input; private $output; private $inputs = []; private $statusCode; public function __construct(Command $command) { $this->command = $command; } /** * Executes the command. * * Available execution options: * * * interactive: Sets the input interactive flag * * decorated: Sets the output decorated flag * * verbosity: Sets the output verbosity flag * * @param array $input An array of command arguments and options * @param array $options An array of execution options * * @return int The command exit code */ public function execute(array $input, array $options = []) { // set the command name automatically if the application requires // this argument and no command name was passed if (!isset($input['command']) && (null !== $application = $this->command->getApplication()) && $application->getDefinition()->hasArgument('command') ) { $input = array_merge(['command' => $this->command->getName()], $input); } $this->input = new ArrayInput($input); // Use an in-memory input stream even if no inputs are set so that QuestionHelper::ask() does not rely on the blocking STDIN. $this->input->setStream(self::createStream($this->inputs)); if (isset($options['interactive'])) { $this->input->setInteractive($options['interactive']); } $this->output = new StreamOutput(fopen('php://memory', 'w', false)); $this->output->setDecorated(isset($options['decorated']) ? $options['decorated'] : false); if (isset($options['verbosity'])) { $this->output->setVerbosity($options['verbosity']); } return $this->statusCode = $this->command->run($this->input, $this->output); } /** * Gets the display returned by the last execution of the command. * * @param bool $normalize Whether to normalize end of lines to \n or not * * @return string The display */ public function getDisplay($normalize = false) { if (null === $this->output) { throw new \RuntimeException('Output not initialized, did you execute the command before requesting the display?'); } rewind($this->output->getStream()); $display = stream_get_contents($this->output->getStream()); if ($normalize) { $display = str_replace(\PHP_EOL, "\n", $display); } return $display; } /** * Gets the input instance used by the last execution of the command. * * @return InputInterface The current input instance */ public function getInput() { return $this->input; } /** * Gets the output instance used by the last execution of the command. * * @return OutputInterface The current output instance */ public function getOutput() { return $this->output; } /** * Gets the status code returned by the last execution of the application. * * @return int The status code */ public function getStatusCode() { return $this->statusCode; } /** * Sets the user inputs. * * @param array $inputs An array of strings representing each input * passed to the command input stream * * @return CommandTester */ public function setInputs(array $inputs) { $this->inputs = $inputs; return $this; } private static function createStream(array $inputs) { $stream = fopen('php://memory', 'r+', false); foreach ($inputs as $input) { fwrite($stream, $input.\PHP_EOL); } rewind($stream); return $stream; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Console\Tester; use Symfony\Component\Console\Application; use Symfony\Component\Console\Input\ArrayInput; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\ConsoleOutput; use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Console\Output\StreamOutput; /** * Eases the testing of console applications. * * When testing an application, don't forget to disable the auto exit flag: * * $application = new Application(); * $application->setAutoExit(false); * * @author Fabien Potencier */ class ApplicationTester { private $application; private $input; private $statusCode; /** * @var OutputInterface */ private $output; private $captureStreamsIndependently = false; public function __construct(Application $application) { $this->application = $application; } /** * Executes the application. * * Available options: * * * interactive: Sets the input interactive flag * * decorated: Sets the output decorated flag * * verbosity: Sets the output verbosity flag * * capture_stderr_separately: Make output of stdOut and stdErr separately available * * @param array $input An array of arguments and options * @param array $options An array of options * * @return int The command exit code */ public function run(array $input, $options = []) { $this->input = new ArrayInput($input); if (isset($options['interactive'])) { $this->input->setInteractive($options['interactive']); } $this->captureStreamsIndependently = \array_key_exists('capture_stderr_separately', $options) && $options['capture_stderr_separately']; if (!$this->captureStreamsIndependently) { $this->output = new StreamOutput(fopen('php://memory', 'w', false)); if (isset($options['decorated'])) { $this->output->setDecorated($options['decorated']); } if (isset($options['verbosity'])) { $this->output->setVerbosity($options['verbosity']); } } else { $this->output = new ConsoleOutput( isset($options['verbosity']) ? $options['verbosity'] : ConsoleOutput::VERBOSITY_NORMAL, isset($options['decorated']) ? $options['decorated'] : null ); $errorOutput = new StreamOutput(fopen('php://memory', 'w', false)); $errorOutput->setFormatter($this->output->getFormatter()); $errorOutput->setVerbosity($this->output->getVerbosity()); $errorOutput->setDecorated($this->output->isDecorated()); $reflectedOutput = new \ReflectionObject($this->output); $strErrProperty = $reflectedOutput->getProperty('stderr'); $strErrProperty->setAccessible(true); $strErrProperty->setValue($this->output, $errorOutput); $reflectedParent = $reflectedOutput->getParentClass(); $streamProperty = $reflectedParent->getProperty('stream'); $streamProperty->setAccessible(true); $streamProperty->setValue($this->output, fopen('php://memory', 'w', false)); } return $this->statusCode = $this->application->run($this->input, $this->output); } /** * Gets the display returned by the last execution of the application. * * @param bool $normalize Whether to normalize end of lines to \n or not * * @return string The display */ public function getDisplay($normalize = false) { rewind($this->output->getStream()); $display = stream_get_contents($this->output->getStream()); if ($normalize) { $display = str_replace(\PHP_EOL, "\n", $display); } return $display; } /** * Gets the output written to STDERR by the application. * * @param bool $normalize Whether to normalize end of lines to \n or not * * @return string */ public function getErrorOutput($normalize = false) { if (!$this->captureStreamsIndependently) { throw new \LogicException('The error output is not available when the tester is run without "capture_stderr_separately" option set.'); } rewind($this->output->getErrorOutput()->getStream()); $display = stream_get_contents($this->output->getErrorOutput()->getStream()); if ($normalize) { $display = str_replace(\PHP_EOL, "\n", $display); } return $display; } /** * Gets the input instance used by the last execution of the application. * * @return InputInterface The current input instance */ public function getInput() { return $this->input; } /** * Gets the output instance used by the last execution of the application. * * @return OutputInterface The current output instance */ public function getOutput() { return $this->output; } /** * Gets the status code returned by the last execution of the application. * * @return int The status code */ public function getStatusCode() { return $this->statusCode; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Console\Exception; /** * @author Jérôme Tamarelle */ class InvalidArgumentException extends \InvalidArgumentException implements ExceptionInterface { } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Console\Exception; /** * Represents an incorrect command name typed in the console. * * @author Jérôme Tamarelle */ class CommandNotFoundException extends \InvalidArgumentException implements ExceptionInterface { private $alternatives; /** * @param string $message Exception message to throw * @param array $alternatives List of similar defined names * @param int $code Exception code * @param \Exception $previous Previous exception used for the exception chaining */ public function __construct($message, array $alternatives = [], $code = 0, \Exception $previous = null) { parent::__construct($message, $code, $previous); $this->alternatives = $alternatives; } /** * @return array A list of similar defined names */ public function getAlternatives() { return $this->alternatives; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Console\Exception; /** * @author Jérôme Tamarelle */ class LogicException extends \LogicException implements ExceptionInterface { } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Console\Exception; /** * ExceptionInterface. * * @author Jérôme Tamarelle */ interface ExceptionInterface { } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Console\Exception; /** * Represents an incorrect option name typed in the console. * * @author Jérôme Tamarelle */ class InvalidOptionException extends \InvalidArgumentException implements ExceptionInterface { } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Console\Exception; /** * @author Jérôme Tamarelle */ class RuntimeException extends \RuntimeException implements ExceptionInterface { } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Console\Descriptor; use Symfony\Component\Console\Application; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Helper\Helper; use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputDefinition; use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; /** * Markdown descriptor. * * @author Jean-François Simon * * @internal */ class MarkdownDescriptor extends Descriptor { /** * {@inheritdoc} */ public function describe(OutputInterface $output, $object, array $options = []) { $decorated = $output->isDecorated(); $output->setDecorated(false); parent::describe($output, $object, $options); $output->setDecorated($decorated); } /** * {@inheritdoc} */ protected function write($content, $decorated = true) { parent::write($content, $decorated); } /** * {@inheritdoc} */ protected function describeInputArgument(InputArgument $argument, array $options = []) { $this->write( '#### `'.($argument->getName() ?: '')."`\n\n" .($argument->getDescription() ? preg_replace('/\s*[\r\n]\s*/', "\n", $argument->getDescription())."\n\n" : '') .'* Is required: '.($argument->isRequired() ? 'yes' : 'no')."\n" .'* Is array: '.($argument->isArray() ? 'yes' : 'no')."\n" .'* Default: `'.str_replace("\n", '', var_export($argument->getDefault(), true)).'`' ); } /** * {@inheritdoc} */ protected function describeInputOption(InputOption $option, array $options = []) { $name = '--'.$option->getName(); if ($option->getShortcut()) { $name .= '|-'.str_replace('|', '|-', $option->getShortcut()).''; } $this->write( '#### `'.$name.'`'."\n\n" .($option->getDescription() ? preg_replace('/\s*[\r\n]\s*/', "\n", $option->getDescription())."\n\n" : '') .'* Accept value: '.($option->acceptValue() ? 'yes' : 'no')."\n" .'* Is value required: '.($option->isValueRequired() ? 'yes' : 'no')."\n" .'* Is multiple: '.($option->isArray() ? 'yes' : 'no')."\n" .'* Default: `'.str_replace("\n", '', var_export($option->getDefault(), true)).'`' ); } /** * {@inheritdoc} */ protected function describeInputDefinition(InputDefinition $definition, array $options = []) { if ($showArguments = \count($definition->getArguments()) > 0) { $this->write('### Arguments'); foreach ($definition->getArguments() as $argument) { $this->write("\n\n"); $this->write($this->describeInputArgument($argument)); } } if (\count($definition->getOptions()) > 0) { if ($showArguments) { $this->write("\n\n"); } $this->write('### Options'); foreach ($definition->getOptions() as $option) { $this->write("\n\n"); $this->write($this->describeInputOption($option)); } } } /** * {@inheritdoc} */ protected function describeCommand(Command $command, array $options = []) { $command->getSynopsis(); $command->mergeApplicationDefinition(false); $this->write( '`'.$command->getName()."`\n" .str_repeat('-', Helper::strlen($command->getName()) + 2)."\n\n" .($command->getDescription() ? $command->getDescription()."\n\n" : '') .'### Usage'."\n\n" .array_reduce(array_merge([$command->getSynopsis()], $command->getAliases(), $command->getUsages()), function ($carry, $usage) { return $carry.'* `'.$usage.'`'."\n"; }) ); if ($help = $command->getProcessedHelp()) { $this->write("\n"); $this->write($help); } if ($command->getNativeDefinition()) { $this->write("\n\n"); $this->describeInputDefinition($command->getNativeDefinition()); } } /** * {@inheritdoc} */ protected function describeApplication(Application $application, array $options = []) { $describedNamespace = isset($options['namespace']) ? $options['namespace'] : null; $description = new ApplicationDescription($application, $describedNamespace); $title = $this->getApplicationTitle($application); $this->write($title."\n".str_repeat('=', Helper::strlen($title))); foreach ($description->getNamespaces() as $namespace) { if (ApplicationDescription::GLOBAL_NAMESPACE !== $namespace['id']) { $this->write("\n\n"); $this->write('**'.$namespace['id'].':**'); } $this->write("\n\n"); $this->write(implode("\n", array_map(function ($commandName) use ($description) { return sprintf('* [`%s`](#%s)', $commandName, str_replace(':', '', $description->getCommand($commandName)->getName())); }, $namespace['commands']))); } foreach ($description->getCommands() as $command) { $this->write("\n\n"); $this->write($this->describeCommand($command)); } } private function getApplicationTitle(Application $application) { if ('UNKNOWN' !== $application->getName()) { if ('UNKNOWN' !== $application->getVersion()) { return sprintf('%s %s', $application->getName(), $application->getVersion()); } return $application->getName(); } return 'Console Tool'; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Console\Descriptor; use Symfony\Component\Console\Application; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputDefinition; use Symfony\Component\Console\Input\InputOption; /** * XML descriptor. * * @author Jean-François Simon * * @internal */ class XmlDescriptor extends Descriptor { /** * @return \DOMDocument */ public function getInputDefinitionDocument(InputDefinition $definition) { $dom = new \DOMDocument('1.0', 'UTF-8'); $dom->appendChild($definitionXML = $dom->createElement('definition')); $definitionXML->appendChild($argumentsXML = $dom->createElement('arguments')); foreach ($definition->getArguments() as $argument) { $this->appendDocument($argumentsXML, $this->getInputArgumentDocument($argument)); } $definitionXML->appendChild($optionsXML = $dom->createElement('options')); foreach ($definition->getOptions() as $option) { $this->appendDocument($optionsXML, $this->getInputOptionDocument($option)); } return $dom; } /** * @return \DOMDocument */ public function getCommandDocument(Command $command) { $dom = new \DOMDocument('1.0', 'UTF-8'); $dom->appendChild($commandXML = $dom->createElement('command')); $command->getSynopsis(); $command->mergeApplicationDefinition(false); $commandXML->setAttribute('id', $command->getName()); $commandXML->setAttribute('name', $command->getName()); $commandXML->setAttribute('hidden', $command->isHidden() ? 1 : 0); $commandXML->appendChild($usagesXML = $dom->createElement('usages')); foreach (array_merge([$command->getSynopsis()], $command->getAliases(), $command->getUsages()) as $usage) { $usagesXML->appendChild($dom->createElement('usage', $usage)); } $commandXML->appendChild($descriptionXML = $dom->createElement('description')); $descriptionXML->appendChild($dom->createTextNode(str_replace("\n", "\n ", $command->getDescription()))); $commandXML->appendChild($helpXML = $dom->createElement('help')); $helpXML->appendChild($dom->createTextNode(str_replace("\n", "\n ", $command->getProcessedHelp()))); $definitionXML = $this->getInputDefinitionDocument($command->getNativeDefinition()); $this->appendDocument($commandXML, $definitionXML->getElementsByTagName('definition')->item(0)); return $dom; } /** * @param string|null $namespace * * @return \DOMDocument */ public function getApplicationDocument(Application $application, $namespace = null) { $dom = new \DOMDocument('1.0', 'UTF-8'); $dom->appendChild($rootXml = $dom->createElement('symfony')); if ('UNKNOWN' !== $application->getName()) { $rootXml->setAttribute('name', $application->getName()); if ('UNKNOWN' !== $application->getVersion()) { $rootXml->setAttribute('version', $application->getVersion()); } } $rootXml->appendChild($commandsXML = $dom->createElement('commands')); $description = new ApplicationDescription($application, $namespace, true); if ($namespace) { $commandsXML->setAttribute('namespace', $namespace); } foreach ($description->getCommands() as $command) { $this->appendDocument($commandsXML, $this->getCommandDocument($command)); } if (!$namespace) { $rootXml->appendChild($namespacesXML = $dom->createElement('namespaces')); foreach ($description->getNamespaces() as $namespaceDescription) { $namespacesXML->appendChild($namespaceArrayXML = $dom->createElement('namespace')); $namespaceArrayXML->setAttribute('id', $namespaceDescription['id']); foreach ($namespaceDescription['commands'] as $name) { $namespaceArrayXML->appendChild($commandXML = $dom->createElement('command')); $commandXML->appendChild($dom->createTextNode($name)); } } } return $dom; } /** * {@inheritdoc} */ protected function describeInputArgument(InputArgument $argument, array $options = []) { $this->writeDocument($this->getInputArgumentDocument($argument)); } /** * {@inheritdoc} */ protected function describeInputOption(InputOption $option, array $options = []) { $this->writeDocument($this->getInputOptionDocument($option)); } /** * {@inheritdoc} */ protected function describeInputDefinition(InputDefinition $definition, array $options = []) { $this->writeDocument($this->getInputDefinitionDocument($definition)); } /** * {@inheritdoc} */ protected function describeCommand(Command $command, array $options = []) { $this->writeDocument($this->getCommandDocument($command)); } /** * {@inheritdoc} */ protected function describeApplication(Application $application, array $options = []) { $this->writeDocument($this->getApplicationDocument($application, isset($options['namespace']) ? $options['namespace'] : null)); } /** * Appends document children to parent node. */ private function appendDocument(\DOMNode $parentNode, \DOMNode $importedParent) { foreach ($importedParent->childNodes as $childNode) { $parentNode->appendChild($parentNode->ownerDocument->importNode($childNode, true)); } } /** * Writes DOM document. */ private function writeDocument(\DOMDocument $dom) { $dom->formatOutput = true; $this->write($dom->saveXML()); } /** * @return \DOMDocument */ private function getInputArgumentDocument(InputArgument $argument) { $dom = new \DOMDocument('1.0', 'UTF-8'); $dom->appendChild($objectXML = $dom->createElement('argument')); $objectXML->setAttribute('name', $argument->getName()); $objectXML->setAttribute('is_required', $argument->isRequired() ? 1 : 0); $objectXML->setAttribute('is_array', $argument->isArray() ? 1 : 0); $objectXML->appendChild($descriptionXML = $dom->createElement('description')); $descriptionXML->appendChild($dom->createTextNode($argument->getDescription())); $objectXML->appendChild($defaultsXML = $dom->createElement('defaults')); $defaults = \is_array($argument->getDefault()) ? $argument->getDefault() : (\is_bool($argument->getDefault()) ? [var_export($argument->getDefault(), true)] : ($argument->getDefault() ? [$argument->getDefault()] : [])); foreach ($defaults as $default) { $defaultsXML->appendChild($defaultXML = $dom->createElement('default')); $defaultXML->appendChild($dom->createTextNode($default)); } return $dom; } /** * @return \DOMDocument */ private function getInputOptionDocument(InputOption $option) { $dom = new \DOMDocument('1.0', 'UTF-8'); $dom->appendChild($objectXML = $dom->createElement('option')); $objectXML->setAttribute('name', '--'.$option->getName()); $pos = strpos($option->getShortcut(), '|'); if (false !== $pos) { $objectXML->setAttribute('shortcut', '-'.substr($option->getShortcut(), 0, $pos)); $objectXML->setAttribute('shortcuts', '-'.str_replace('|', '|-', $option->getShortcut())); } else { $objectXML->setAttribute('shortcut', $option->getShortcut() ? '-'.$option->getShortcut() : ''); } $objectXML->setAttribute('accept_value', $option->acceptValue() ? 1 : 0); $objectXML->setAttribute('is_value_required', $option->isValueRequired() ? 1 : 0); $objectXML->setAttribute('is_multiple', $option->isArray() ? 1 : 0); $objectXML->appendChild($descriptionXML = $dom->createElement('description')); $descriptionXML->appendChild($dom->createTextNode($option->getDescription())); if ($option->acceptValue()) { $defaults = \is_array($option->getDefault()) ? $option->getDefault() : (\is_bool($option->getDefault()) ? [var_export($option->getDefault(), true)] : ($option->getDefault() ? [$option->getDefault()] : [])); $objectXML->appendChild($defaultsXML = $dom->createElement('defaults')); if (!empty($defaults)) { foreach ($defaults as $default) { $defaultsXML->appendChild($defaultXML = $dom->createElement('default')); $defaultXML->appendChild($dom->createTextNode($default)); } } } return $dom; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Console\Descriptor; use Symfony\Component\Console\Output\OutputInterface; /** * Descriptor interface. * * @author Jean-François Simon */ interface DescriptorInterface { /** * Describes an object if supported. * * @param object $object */ public function describe(OutputInterface $output, $object, array $options = []); } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Console\Descriptor; use Symfony\Component\Console\Application; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputDefinition; use Symfony\Component\Console\Input\InputOption; /** * JSON descriptor. * * @author Jean-François Simon * * @internal */ class JsonDescriptor extends Descriptor { /** * {@inheritdoc} */ protected function describeInputArgument(InputArgument $argument, array $options = []) { $this->writeData($this->getInputArgumentData($argument), $options); } /** * {@inheritdoc} */ protected function describeInputOption(InputOption $option, array $options = []) { $this->writeData($this->getInputOptionData($option), $options); } /** * {@inheritdoc} */ protected function describeInputDefinition(InputDefinition $definition, array $options = []) { $this->writeData($this->getInputDefinitionData($definition), $options); } /** * {@inheritdoc} */ protected function describeCommand(Command $command, array $options = []) { $this->writeData($this->getCommandData($command), $options); } /** * {@inheritdoc} */ protected function describeApplication(Application $application, array $options = []) { $describedNamespace = isset($options['namespace']) ? $options['namespace'] : null; $description = new ApplicationDescription($application, $describedNamespace, true); $commands = []; foreach ($description->getCommands() as $command) { $commands[] = $this->getCommandData($command); } $data = []; if ('UNKNOWN' !== $application->getName()) { $data['application']['name'] = $application->getName(); if ('UNKNOWN' !== $application->getVersion()) { $data['application']['version'] = $application->getVersion(); } } $data['commands'] = $commands; if ($describedNamespace) { $data['namespace'] = $describedNamespace; } else { $data['namespaces'] = array_values($description->getNamespaces()); } $this->writeData($data, $options); } /** * Writes data as json. */ private function writeData(array $data, array $options) { $flags = isset($options['json_encoding']) ? $options['json_encoding'] : 0; $this->write(json_encode($data, $flags)); } /** * @return array */ private function getInputArgumentData(InputArgument $argument) { return [ 'name' => $argument->getName(), 'is_required' => $argument->isRequired(), 'is_array' => $argument->isArray(), 'description' => preg_replace('/\s*[\r\n]\s*/', ' ', $argument->getDescription()), 'default' => \INF === $argument->getDefault() ? 'INF' : $argument->getDefault(), ]; } /** * @return array */ private function getInputOptionData(InputOption $option) { return [ 'name' => '--'.$option->getName(), 'shortcut' => $option->getShortcut() ? '-'.str_replace('|', '|-', $option->getShortcut()) : '', 'accept_value' => $option->acceptValue(), 'is_value_required' => $option->isValueRequired(), 'is_multiple' => $option->isArray(), 'description' => preg_replace('/\s*[\r\n]\s*/', ' ', $option->getDescription()), 'default' => \INF === $option->getDefault() ? 'INF' : $option->getDefault(), ]; } /** * @return array */ private function getInputDefinitionData(InputDefinition $definition) { $inputArguments = []; foreach ($definition->getArguments() as $name => $argument) { $inputArguments[$name] = $this->getInputArgumentData($argument); } $inputOptions = []; foreach ($definition->getOptions() as $name => $option) { $inputOptions[$name] = $this->getInputOptionData($option); } return ['arguments' => $inputArguments, 'options' => $inputOptions]; } /** * @return array */ private function getCommandData(Command $command) { $command->getSynopsis(); $command->mergeApplicationDefinition(false); return [ 'name' => $command->getName(), 'usage' => array_merge([$command->getSynopsis()], $command->getUsages(), $command->getAliases()), 'description' => $command->getDescription(), 'help' => $command->getProcessedHelp(), 'definition' => $this->getInputDefinitionData($command->getNativeDefinition()), 'hidden' => $command->isHidden(), ]; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Console\Descriptor; use Symfony\Component\Console\Application; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Exception\CommandNotFoundException; /** * @author Jean-François Simon * * @internal */ class ApplicationDescription { const GLOBAL_NAMESPACE = '_global'; private $application; private $namespace; private $showHidden; /** * @var array */ private $namespaces; /** * @var Command[] */ private $commands; /** * @var Command[] */ private $aliases; /** * @param string|null $namespace * @param bool $showHidden */ public function __construct(Application $application, $namespace = null, $showHidden = false) { $this->application = $application; $this->namespace = $namespace; $this->showHidden = $showHidden; } /** * @return array */ public function getNamespaces() { if (null === $this->namespaces) { $this->inspectApplication(); } return $this->namespaces; } /** * @return Command[] */ public function getCommands() { if (null === $this->commands) { $this->inspectApplication(); } return $this->commands; } /** * @param string $name * * @return Command * * @throws CommandNotFoundException */ public function getCommand($name) { if (!isset($this->commands[$name]) && !isset($this->aliases[$name])) { throw new CommandNotFoundException(sprintf('Command "%s" does not exist.', $name)); } return isset($this->commands[$name]) ? $this->commands[$name] : $this->aliases[$name]; } private function inspectApplication() { $this->commands = []; $this->namespaces = []; $all = $this->application->all($this->namespace ? $this->application->findNamespace($this->namespace) : null); foreach ($this->sortCommands($all) as $namespace => $commands) { $names = []; /** @var Command $command */ foreach ($commands as $name => $command) { if (!$command->getName() || (!$this->showHidden && $command->isHidden())) { continue; } if ($command->getName() === $name) { $this->commands[$name] = $command; } else { $this->aliases[$name] = $command; } $names[] = $name; } $this->namespaces[$namespace] = ['id' => $namespace, 'commands' => $names]; } } /** * @return array */ private function sortCommands(array $commands) { $namespacedCommands = []; $globalCommands = []; $sortedCommands = []; foreach ($commands as $name => $command) { $key = $this->application->extractNamespace($name, 1); if (\in_array($key, ['', self::GLOBAL_NAMESPACE], true)) { $globalCommands[$name] = $command; } else { $namespacedCommands[$key][$name] = $command; } } if ($globalCommands) { ksort($globalCommands); $sortedCommands[self::GLOBAL_NAMESPACE] = $globalCommands; } if ($namespacedCommands) { ksort($namespacedCommands); foreach ($namespacedCommands as $key => $commandsSet) { ksort($commandsSet); $sortedCommands[$key] = $commandsSet; } } return $sortedCommands; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Console\Descriptor; use Symfony\Component\Console\Application; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Exception\InvalidArgumentException; use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputDefinition; use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; /** * @author Jean-François Simon * * @internal */ abstract class Descriptor implements DescriptorInterface { /** * @var OutputInterface */ protected $output; /** * {@inheritdoc} */ public function describe(OutputInterface $output, $object, array $options = []) { $this->output = $output; switch (true) { case $object instanceof InputArgument: $this->describeInputArgument($object, $options); break; case $object instanceof InputOption: $this->describeInputOption($object, $options); break; case $object instanceof InputDefinition: $this->describeInputDefinition($object, $options); break; case $object instanceof Command: $this->describeCommand($object, $options); break; case $object instanceof Application: $this->describeApplication($object, $options); break; default: throw new InvalidArgumentException(sprintf('Object of type "%s" is not describable.', \get_class($object))); } } /** * Writes content to output. * * @param string $content * @param bool $decorated */ protected function write($content, $decorated = false) { $this->output->write($content, false, $decorated ? OutputInterface::OUTPUT_NORMAL : OutputInterface::OUTPUT_RAW); } /** * Describes an InputArgument instance. * * @return string|mixed */ abstract protected function describeInputArgument(InputArgument $argument, array $options = []); /** * Describes an InputOption instance. * * @return string|mixed */ abstract protected function describeInputOption(InputOption $option, array $options = []); /** * Describes an InputDefinition instance. * * @return string|mixed */ abstract protected function describeInputDefinition(InputDefinition $definition, array $options = []); /** * Describes a Command instance. * * @return string|mixed */ abstract protected function describeCommand(Command $command, array $options = []); /** * Describes an Application instance. * * @return string|mixed */ abstract protected function describeApplication(Application $application, array $options = []); } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Console\Descriptor; use Symfony\Component\Console\Application; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Formatter\OutputFormatter; use Symfony\Component\Console\Helper\Helper; use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputDefinition; use Symfony\Component\Console\Input\InputOption; /** * Text descriptor. * * @author Jean-François Simon * * @internal */ class TextDescriptor extends Descriptor { /** * {@inheritdoc} */ protected function describeInputArgument(InputArgument $argument, array $options = []) { if (null !== $argument->getDefault() && (!\is_array($argument->getDefault()) || \count($argument->getDefault()))) { $default = sprintf(' [default: %s]', $this->formatDefaultValue($argument->getDefault())); } else { $default = ''; } $totalWidth = isset($options['total_width']) ? $options['total_width'] : Helper::strlen($argument->getName()); $spacingWidth = $totalWidth - \strlen($argument->getName()); $this->writeText(sprintf(' %s %s%s%s', $argument->getName(), str_repeat(' ', $spacingWidth), // + 4 = 2 spaces before , 2 spaces after preg_replace('/\s*[\r\n]\s*/', "\n".str_repeat(' ', $totalWidth + 4), $argument->getDescription()), $default ), $options); } /** * {@inheritdoc} */ protected function describeInputOption(InputOption $option, array $options = []) { if ($option->acceptValue() && null !== $option->getDefault() && (!\is_array($option->getDefault()) || \count($option->getDefault()))) { $default = sprintf(' [default: %s]', $this->formatDefaultValue($option->getDefault())); } else { $default = ''; } $value = ''; if ($option->acceptValue()) { $value = '='.strtoupper($option->getName()); if ($option->isValueOptional()) { $value = '['.$value.']'; } } $totalWidth = isset($options['total_width']) ? $options['total_width'] : $this->calculateTotalWidthForOptions([$option]); $synopsis = sprintf('%s%s', $option->getShortcut() ? sprintf('-%s, ', $option->getShortcut()) : ' ', sprintf('--%s%s', $option->getName(), $value) ); $spacingWidth = $totalWidth - Helper::strlen($synopsis); $this->writeText(sprintf(' %s %s%s%s%s', $synopsis, str_repeat(' ', $spacingWidth), // + 4 = 2 spaces before , 2 spaces after preg_replace('/\s*[\r\n]\s*/', "\n".str_repeat(' ', $totalWidth + 4), $option->getDescription()), $default, $option->isArray() ? ' (multiple values allowed)' : '' ), $options); } /** * {@inheritdoc} */ protected function describeInputDefinition(InputDefinition $definition, array $options = []) { $totalWidth = $this->calculateTotalWidthForOptions($definition->getOptions()); foreach ($definition->getArguments() as $argument) { $totalWidth = max($totalWidth, Helper::strlen($argument->getName())); } if ($definition->getArguments()) { $this->writeText('Arguments:', $options); $this->writeText("\n"); foreach ($definition->getArguments() as $argument) { $this->describeInputArgument($argument, array_merge($options, ['total_width' => $totalWidth])); $this->writeText("\n"); } } if ($definition->getArguments() && $definition->getOptions()) { $this->writeText("\n"); } if ($definition->getOptions()) { $laterOptions = []; $this->writeText('Options:', $options); foreach ($definition->getOptions() as $option) { if (\strlen($option->getShortcut()) > 1) { $laterOptions[] = $option; continue; } $this->writeText("\n"); $this->describeInputOption($option, array_merge($options, ['total_width' => $totalWidth])); } foreach ($laterOptions as $option) { $this->writeText("\n"); $this->describeInputOption($option, array_merge($options, ['total_width' => $totalWidth])); } } } /** * {@inheritdoc} */ protected function describeCommand(Command $command, array $options = []) { $command->getSynopsis(true); $command->getSynopsis(false); $command->mergeApplicationDefinition(false); $this->writeText('Usage:', $options); foreach (array_merge([$command->getSynopsis(true)], $command->getAliases(), $command->getUsages()) as $usage) { $this->writeText("\n"); $this->writeText(' '.OutputFormatter::escape($usage), $options); } $this->writeText("\n"); $definition = $command->getNativeDefinition(); if ($definition->getOptions() || $definition->getArguments()) { $this->writeText("\n"); $this->describeInputDefinition($definition, $options); $this->writeText("\n"); } if ($help = $command->getProcessedHelp()) { $this->writeText("\n"); $this->writeText('Help:', $options); $this->writeText("\n"); $this->writeText(' '.str_replace("\n", "\n ", $help), $options); $this->writeText("\n"); } } /** * {@inheritdoc} */ protected function describeApplication(Application $application, array $options = []) { $describedNamespace = isset($options['namespace']) ? $options['namespace'] : null; $description = new ApplicationDescription($application, $describedNamespace); if (isset($options['raw_text']) && $options['raw_text']) { $width = $this->getColumnWidth($description->getCommands()); foreach ($description->getCommands() as $command) { $this->writeText(sprintf("%-{$width}s %s", $command->getName(), $command->getDescription()), $options); $this->writeText("\n"); } } else { if ('' != $help = $application->getHelp()) { $this->writeText("$help\n\n", $options); } $this->writeText("Usage:\n", $options); $this->writeText(" command [options] [arguments]\n\n", $options); $this->describeInputDefinition(new InputDefinition($application->getDefinition()->getOptions()), $options); $this->writeText("\n"); $this->writeText("\n"); $commands = $description->getCommands(); $namespaces = $description->getNamespaces(); if ($describedNamespace && $namespaces) { // make sure all alias commands are included when describing a specific namespace $describedNamespaceInfo = reset($namespaces); foreach ($describedNamespaceInfo['commands'] as $name) { $commands[$name] = $description->getCommand($name); } } // calculate max. width based on available commands per namespace $width = $this->getColumnWidth(\call_user_func_array('array_merge', array_map(function ($namespace) use ($commands) { return array_intersect($namespace['commands'], array_keys($commands)); }, array_values($namespaces)))); if ($describedNamespace) { $this->writeText(sprintf('Available commands for the "%s" namespace:', $describedNamespace), $options); } else { $this->writeText('Available commands:', $options); } foreach ($namespaces as $namespace) { $namespace['commands'] = array_filter($namespace['commands'], function ($name) use ($commands) { return isset($commands[$name]); }); if (!$namespace['commands']) { continue; } if (!$describedNamespace && ApplicationDescription::GLOBAL_NAMESPACE !== $namespace['id']) { $this->writeText("\n"); $this->writeText(' '.$namespace['id'].'', $options); } foreach ($namespace['commands'] as $name) { $this->writeText("\n"); $spacingWidth = $width - Helper::strlen($name); $command = $commands[$name]; $commandAliases = $name === $command->getName() ? $this->getCommandAliasesText($command) : ''; $this->writeText(sprintf(' %s%s%s', $name, str_repeat(' ', $spacingWidth), $commandAliases.$command->getDescription()), $options); } } $this->writeText("\n"); } } /** * {@inheritdoc} */ private function writeText($content, array $options = []) { $this->write( isset($options['raw_text']) && $options['raw_text'] ? strip_tags($content) : $content, isset($options['raw_output']) ? !$options['raw_output'] : true ); } /** * Formats command aliases to show them in the command description. * * @return string */ private function getCommandAliasesText(Command $command) { $text = ''; $aliases = $command->getAliases(); if ($aliases) { $text = '['.implode('|', $aliases).'] '; } return $text; } /** * Formats input option/argument default value. * * @param mixed $default * * @return string */ private function formatDefaultValue($default) { if (\INF === $default) { return 'INF'; } if (\is_string($default)) { $default = OutputFormatter::escape($default); } elseif (\is_array($default)) { foreach ($default as $key => $value) { if (\is_string($value)) { $default[$key] = OutputFormatter::escape($value); } } } return str_replace('\\\\', '\\', json_encode($default, \JSON_UNESCAPED_SLASHES | \JSON_UNESCAPED_UNICODE)); } /** * @param (Command|string)[] $commands * * @return int */ private function getColumnWidth(array $commands) { $widths = []; foreach ($commands as $command) { if ($command instanceof Command) { $widths[] = Helper::strlen($command->getName()); foreach ($command->getAliases() as $alias) { $widths[] = Helper::strlen($alias); } } else { $widths[] = Helper::strlen($command); } } return $widths ? max($widths) + 2 : 0; } /** * @param InputOption[] $options * * @return int */ private function calculateTotalWidthForOptions(array $options) { $totalWidth = 0; foreach ($options as $option) { // "-" + shortcut + ", --" + name $nameLength = 1 + max(Helper::strlen($option->getShortcut()), 1) + 4 + Helper::strlen($option->getName()); if ($option->acceptValue()) { $valueLength = 1 + Helper::strlen($option->getName()); // = + value $valueLength += $option->isValueOptional() ? 2 : 0; // [ + ] $nameLength += $valueLength; } $totalWidth = max($totalWidth, $nameLength); } return $totalWidth; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Console\EventListener; use Psr\Log\LoggerInterface; use Symfony\Component\Console\ConsoleEvents; use Symfony\Component\Console\Event\ConsoleErrorEvent; use Symfony\Component\Console\Event\ConsoleEvent; use Symfony\Component\Console\Event\ConsoleTerminateEvent; use Symfony\Component\EventDispatcher\EventSubscriberInterface; /** * @author James Halsall * @author Robin Chalas */ class ErrorListener implements EventSubscriberInterface { private $logger; public function __construct(LoggerInterface $logger = null) { $this->logger = $logger; } public function onConsoleError(ConsoleErrorEvent $event) { if (null === $this->logger) { return; } $error = $event->getError(); if (!$inputString = $this->getInputString($event)) { $this->logger->error('An error occurred while using the console. Message: "{message}"', ['exception' => $error, 'message' => $error->getMessage()]); return; } $this->logger->error('Error thrown while running command "{command}". Message: "{message}"', ['exception' => $error, 'command' => $inputString, 'message' => $error->getMessage()]); } public function onConsoleTerminate(ConsoleTerminateEvent $event) { if (null === $this->logger) { return; } $exitCode = $event->getExitCode(); if (0 === $exitCode) { return; } if (!$inputString = $this->getInputString($event)) { $this->logger->debug('The console exited with code "{code}"', ['code' => $exitCode]); return; } $this->logger->debug('Command "{command}" exited with code "{code}"', ['command' => $inputString, 'code' => $exitCode]); } public static function getSubscribedEvents() { return [ ConsoleEvents::ERROR => ['onConsoleError', -128], ConsoleEvents::TERMINATE => ['onConsoleTerminate', -128], ]; } private static function getInputString(ConsoleEvent $event) { $commandName = $event->getCommand() ? $event->getCommand()->getName() : null; $input = $event->getInput(); if (method_exists($input, '__toString')) { if ($commandName) { return str_replace(["'$commandName'", "\"$commandName\""], $commandName, (string) $input); } return (string) $input; } return $commandName; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Console\Helper; /** * Helps outputting debug information when running an external program from a command. * * An external program can be a Process, an HTTP request, or anything else. * * @author Fabien Potencier */ class DebugFormatterHelper extends Helper { private $colors = ['black', 'red', 'green', 'yellow', 'blue', 'magenta', 'cyan', 'white', 'default']; private $started = []; private $count = -1; /** * Starts a debug formatting session. * * @param string $id The id of the formatting session * @param string $message The message to display * @param string $prefix The prefix to use * * @return string */ public function start($id, $message, $prefix = 'RUN') { $this->started[$id] = ['border' => ++$this->count % \count($this->colors)]; return sprintf("%s %s %s\n", $this->getBorder($id), $prefix, $message); } /** * Adds progress to a formatting session. * * @param string $id The id of the formatting session * @param string $buffer The message to display * @param bool $error Whether to consider the buffer as error * @param string $prefix The prefix for output * @param string $errorPrefix The prefix for error output * * @return string */ public function progress($id, $buffer, $error = false, $prefix = 'OUT', $errorPrefix = 'ERR') { $message = ''; if ($error) { if (isset($this->started[$id]['out'])) { $message .= "\n"; unset($this->started[$id]['out']); } if (!isset($this->started[$id]['err'])) { $message .= sprintf('%s %s ', $this->getBorder($id), $errorPrefix); $this->started[$id]['err'] = true; } $message .= str_replace("\n", sprintf("\n%s %s ", $this->getBorder($id), $errorPrefix), $buffer); } else { if (isset($this->started[$id]['err'])) { $message .= "\n"; unset($this->started[$id]['err']); } if (!isset($this->started[$id]['out'])) { $message .= sprintf('%s %s ', $this->getBorder($id), $prefix); $this->started[$id]['out'] = true; } $message .= str_replace("\n", sprintf("\n%s %s ", $this->getBorder($id), $prefix), $buffer); } return $message; } /** * Stops a formatting session. * * @param string $id The id of the formatting session * @param string $message The message to display * @param bool $successful Whether to consider the result as success * @param string $prefix The prefix for the end output * * @return string */ public function stop($id, $message, $successful, $prefix = 'RES') { $trailingEOL = isset($this->started[$id]['out']) || isset($this->started[$id]['err']) ? "\n" : ''; if ($successful) { return sprintf("%s%s %s %s\n", $trailingEOL, $this->getBorder($id), $prefix, $message); } $message = sprintf("%s%s %s %s\n", $trailingEOL, $this->getBorder($id), $prefix, $message); unset($this->started[$id]['out'], $this->started[$id]['err']); return $message; } /** * @param string $id The id of the formatting session * * @return string */ private function getBorder($id) { return sprintf(' ', $this->colors[$this->started[$id]['border']]); } /** * {@inheritdoc} */ public function getName() { return 'debug_formatter'; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Console\Helper; use Symfony\Component\Console\Formatter\OutputFormatterInterface; /** * Helper is the base class for all helper classes. * * @author Fabien Potencier */ abstract class Helper implements HelperInterface { protected $helperSet = null; /** * {@inheritdoc} */ public function setHelperSet(HelperSet $helperSet = null) { $this->helperSet = $helperSet; } /** * {@inheritdoc} */ public function getHelperSet() { return $this->helperSet; } /** * Returns the length of a string, using mb_strwidth if it is available. * * @param string $string The string to check its length * * @return int The length of the string */ public static function strlen($string) { if (false === $encoding = mb_detect_encoding($string, null, true)) { return \strlen($string); } return mb_strwidth($string, $encoding); } /** * Returns the subset of a string, using mb_substr if it is available. * * @param string $string String to subset * @param int $from Start offset * @param int|null $length Length to read * * @return string The string subset */ public static function substr($string, $from, $length = null) { if (false === $encoding = mb_detect_encoding($string, null, true)) { return substr($string, $from, $length); } return mb_substr($string, $from, $length, $encoding); } public static function formatTime($secs) { static $timeFormats = [ [0, '< 1 sec'], [1, '1 sec'], [2, 'secs', 1], [60, '1 min'], [120, 'mins', 60], [3600, '1 hr'], [7200, 'hrs', 3600], [86400, '1 day'], [172800, 'days', 86400], ]; foreach ($timeFormats as $index => $format) { if ($secs >= $format[0]) { if ((isset($timeFormats[$index + 1]) && $secs < $timeFormats[$index + 1][0]) || $index == \count($timeFormats) - 1 ) { if (2 == \count($format)) { return $format[1]; } return floor($secs / $format[2]).' '.$format[1]; } } } } public static function formatMemory($memory) { if ($memory >= 1024 * 1024 * 1024) { return sprintf('%.1f GiB', $memory / 1024 / 1024 / 1024); } if ($memory >= 1024 * 1024) { return sprintf('%.1f MiB', $memory / 1024 / 1024); } if ($memory >= 1024) { return sprintf('%d KiB', $memory / 1024); } return sprintf('%d B', $memory); } public static function strlenWithoutDecoration(OutputFormatterInterface $formatter, $string) { return self::strlen(self::removeDecoration($formatter, $string)); } public static function removeDecoration(OutputFormatterInterface $formatter, $string) { $isDecorated = $formatter->isDecorated(); $formatter->setDecorated(false); // remove <...> formatting $string = $formatter->format($string); // remove already formatted characters $string = preg_replace("/\033\[[^m]*m/", '', $string); $formatter->setDecorated($isDecorated); return $string; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Console\Helper; use Symfony\Component\Console\Exception\LogicException; use Symfony\Component\Console\Output\ConsoleOutputInterface; use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Console\Terminal; /** * The ProgressBar provides helpers to display progress output. * * @author Fabien Potencier * @author Chris Jones */ final class ProgressBar { private $barWidth = 28; private $barChar; private $emptyBarChar = '-'; private $progressChar = '>'; private $format; private $internalFormat; private $redrawFreq = 1; private $output; private $step = 0; private $max; private $startTime; private $stepWidth; private $percent = 0.0; private $formatLineCount; private $messages = []; private $overwrite = true; private $terminal; private $firstRun = true; private static $formatters; private static $formats; /** * @param OutputInterface $output An OutputInterface instance * @param int $max Maximum steps (0 if unknown) */ public function __construct(OutputInterface $output, $max = 0) { if ($output instanceof ConsoleOutputInterface) { $output = $output->getErrorOutput(); } $this->output = $output; $this->setMaxSteps($max); $this->terminal = new Terminal(); if (!$this->output->isDecorated()) { // disable overwrite when output does not support ANSI codes. $this->overwrite = false; // set a reasonable redraw frequency so output isn't flooded $this->setRedrawFrequency($max / 10); } $this->startTime = time(); } /** * Sets a placeholder formatter for a given name. * * This method also allow you to override an existing placeholder. * * @param string $name The placeholder name (including the delimiter char like %) * @param callable $callable A PHP callable */ public static function setPlaceholderFormatterDefinition($name, callable $callable) { if (!self::$formatters) { self::$formatters = self::initPlaceholderFormatters(); } self::$formatters[$name] = $callable; } /** * Gets the placeholder formatter for a given name. * * @param string $name The placeholder name (including the delimiter char like %) * * @return callable|null A PHP callable */ public static function getPlaceholderFormatterDefinition($name) { if (!self::$formatters) { self::$formatters = self::initPlaceholderFormatters(); } return isset(self::$formatters[$name]) ? self::$formatters[$name] : null; } /** * Sets a format for a given name. * * This method also allow you to override an existing format. * * @param string $name The format name * @param string $format A format string */ public static function setFormatDefinition($name, $format) { if (!self::$formats) { self::$formats = self::initFormats(); } self::$formats[$name] = $format; } /** * Gets the format for a given name. * * @param string $name The format name * * @return string|null A format string */ public static function getFormatDefinition($name) { if (!self::$formats) { self::$formats = self::initFormats(); } return isset(self::$formats[$name]) ? self::$formats[$name] : null; } /** * Associates a text with a named placeholder. * * The text is displayed when the progress bar is rendered but only * when the corresponding placeholder is part of the custom format line * (by wrapping the name with %). * * @param string $message The text to associate with the placeholder * @param string $name The name of the placeholder */ public function setMessage($message, $name = 'message') { $this->messages[$name] = $message; } public function getMessage($name = 'message') { return $this->messages[$name]; } /** * Gets the progress bar start time. * * @return int The progress bar start time */ public function getStartTime() { return $this->startTime; } /** * Gets the progress bar maximal steps. * * @return int The progress bar max steps */ public function getMaxSteps() { return $this->max; } /** * Gets the current step position. * * @return int The progress bar step */ public function getProgress() { return $this->step; } /** * Gets the progress bar step width. * * @return int The progress bar step width */ private function getStepWidth() { return $this->stepWidth; } /** * Gets the current progress bar percent. * * @return float The current progress bar percent */ public function getProgressPercent() { return $this->percent; } /** * Sets the progress bar width. * * @param int $size The progress bar size */ public function setBarWidth($size) { $this->barWidth = max(1, (int) $size); } /** * Gets the progress bar width. * * @return int The progress bar size */ public function getBarWidth() { return $this->barWidth; } /** * Sets the bar character. * * @param string $char A character */ public function setBarCharacter($char) { $this->barChar = $char; } /** * Gets the bar character. * * @return string A character */ public function getBarCharacter() { if (null === $this->barChar) { return $this->max ? '=' : $this->emptyBarChar; } return $this->barChar; } /** * Sets the empty bar character. * * @param string $char A character */ public function setEmptyBarCharacter($char) { $this->emptyBarChar = $char; } /** * Gets the empty bar character. * * @return string A character */ public function getEmptyBarCharacter() { return $this->emptyBarChar; } /** * Sets the progress bar character. * * @param string $char A character */ public function setProgressCharacter($char) { $this->progressChar = $char; } /** * Gets the progress bar character. * * @return string A character */ public function getProgressCharacter() { return $this->progressChar; } /** * Sets the progress bar format. * * @param string $format The format */ public function setFormat($format) { $this->format = null; $this->internalFormat = $format; } /** * Sets the redraw frequency. * * @param int|float $freq The frequency in steps */ public function setRedrawFrequency($freq) { $this->redrawFreq = max((int) $freq, 1); } /** * Starts the progress output. * * @param int|null $max Number of steps to complete the bar (0 if indeterminate), null to leave unchanged */ public function start($max = null) { $this->startTime = time(); $this->step = 0; $this->percent = 0.0; if (null !== $max) { $this->setMaxSteps($max); } $this->display(); } /** * Advances the progress output X steps. * * @param int $step Number of steps to advance */ public function advance($step = 1) { $this->setProgress($this->step + $step); } /** * Sets whether to overwrite the progressbar, false for new line. * * @param bool $overwrite */ public function setOverwrite($overwrite) { $this->overwrite = (bool) $overwrite; } /** * Sets the current progress. * * @param int $step The current progress */ public function setProgress($step) { $step = (int) $step; if ($this->max && $step > $this->max) { $this->max = $step; } elseif ($step < 0) { $step = 0; } $prevPeriod = (int) ($this->step / $this->redrawFreq); $currPeriod = (int) ($step / $this->redrawFreq); $this->step = $step; $this->percent = $this->max ? (float) $this->step / $this->max : 0; if ($prevPeriod !== $currPeriod || $this->max === $step) { $this->display(); } } /** * Finishes the progress output. */ public function finish() { if (!$this->max) { $this->max = $this->step; } if ($this->step === $this->max && !$this->overwrite) { // prevent double 100% output return; } $this->setProgress($this->max); } /** * Outputs the current progress string. */ public function display() { if (OutputInterface::VERBOSITY_QUIET === $this->output->getVerbosity()) { return; } if (null === $this->format) { $this->setRealFormat($this->internalFormat ?: $this->determineBestFormat()); } $this->overwrite($this->buildLine()); } /** * Removes the progress bar from the current line. * * This is useful if you wish to write some output * while a progress bar is running. * Call display() to show the progress bar again. */ public function clear() { if (!$this->overwrite) { return; } if (null === $this->format) { $this->setRealFormat($this->internalFormat ?: $this->determineBestFormat()); } $this->overwrite(''); } /** * Sets the progress bar format. * * @param string $format The format */ private function setRealFormat($format) { // try to use the _nomax variant if available if (!$this->max && null !== self::getFormatDefinition($format.'_nomax')) { $this->format = self::getFormatDefinition($format.'_nomax'); } elseif (null !== self::getFormatDefinition($format)) { $this->format = self::getFormatDefinition($format); } else { $this->format = $format; } $this->formatLineCount = substr_count($this->format, "\n"); } /** * Sets the progress bar maximal steps. * * @param int $max The progress bar max steps */ private function setMaxSteps($max) { $this->max = max(0, (int) $max); $this->stepWidth = $this->max ? Helper::strlen($this->max) : 4; } /** * Overwrites a previous message to the output. * * @param string $message The message */ private function overwrite($message) { if ($this->overwrite) { if (!$this->firstRun) { // Erase previous lines if ($this->formatLineCount > 0) { $message = str_repeat("\x1B[1A\x1B[2K", $this->formatLineCount).$message; } // Move the cursor to the beginning of the line and erase the line $message = "\x0D\x1B[2K$message"; } } elseif ($this->step > 0) { $message = \PHP_EOL.$message; } $this->firstRun = false; $this->output->write($message); } private function determineBestFormat() { switch ($this->output->getVerbosity()) { // OutputInterface::VERBOSITY_QUIET: display is disabled anyway case OutputInterface::VERBOSITY_VERBOSE: return $this->max ? 'verbose' : 'verbose_nomax'; case OutputInterface::VERBOSITY_VERY_VERBOSE: return $this->max ? 'very_verbose' : 'very_verbose_nomax'; case OutputInterface::VERBOSITY_DEBUG: return $this->max ? 'debug' : 'debug_nomax'; default: return $this->max ? 'normal' : 'normal_nomax'; } } private static function initPlaceholderFormatters() { return [ 'bar' => function (self $bar, OutputInterface $output) { $completeBars = floor($bar->getMaxSteps() > 0 ? $bar->getProgressPercent() * $bar->getBarWidth() : $bar->getProgress() % $bar->getBarWidth()); $display = str_repeat($bar->getBarCharacter(), $completeBars); if ($completeBars < $bar->getBarWidth()) { $emptyBars = $bar->getBarWidth() - $completeBars - Helper::strlenWithoutDecoration($output->getFormatter(), $bar->getProgressCharacter()); $display .= $bar->getProgressCharacter().str_repeat($bar->getEmptyBarCharacter(), $emptyBars); } return $display; }, 'elapsed' => function (self $bar) { return Helper::formatTime(time() - $bar->getStartTime()); }, 'remaining' => function (self $bar) { if (!$bar->getMaxSteps()) { throw new LogicException('Unable to display the remaining time if the maximum number of steps is not set.'); } if (!$bar->getProgress()) { $remaining = 0; } else { $remaining = round((time() - $bar->getStartTime()) / $bar->getProgress() * ($bar->getMaxSteps() - $bar->getProgress())); } return Helper::formatTime($remaining); }, 'estimated' => function (self $bar) { if (!$bar->getMaxSteps()) { throw new LogicException('Unable to display the estimated time if the maximum number of steps is not set.'); } if (!$bar->getProgress()) { $estimated = 0; } else { $estimated = round((time() - $bar->getStartTime()) / $bar->getProgress() * $bar->getMaxSteps()); } return Helper::formatTime($estimated); }, 'memory' => function (self $bar) { return Helper::formatMemory(memory_get_usage(true)); }, 'current' => function (self $bar) { return str_pad($bar->getProgress(), $bar->getStepWidth(), ' ', \STR_PAD_LEFT); }, 'max' => function (self $bar) { return $bar->getMaxSteps(); }, 'percent' => function (self $bar) { return floor($bar->getProgressPercent() * 100); }, ]; } private static function initFormats() { return [ 'normal' => ' %current%/%max% [%bar%] %percent:3s%%', 'normal_nomax' => ' %current% [%bar%]', 'verbose' => ' %current%/%max% [%bar%] %percent:3s%% %elapsed:6s%', 'verbose_nomax' => ' %current% [%bar%] %elapsed:6s%', 'very_verbose' => ' %current%/%max% [%bar%] %percent:3s%% %elapsed:6s%/%estimated:-6s%', 'very_verbose_nomax' => ' %current% [%bar%] %elapsed:6s%', 'debug' => ' %current%/%max% [%bar%] %percent:3s%% %elapsed:6s%/%estimated:-6s% %memory:6s%', 'debug_nomax' => ' %current% [%bar%] %elapsed:6s% %memory:6s%', ]; } /** * @return string */ private function buildLine() { $regex = "{%([a-z\-_]+)(?:\:([^%]+))?%}i"; $callback = function ($matches) { if ($formatter = $this::getPlaceholderFormatterDefinition($matches[1])) { $text = \call_user_func($formatter, $this, $this->output); } elseif (isset($this->messages[$matches[1]])) { $text = $this->messages[$matches[1]]; } else { return $matches[0]; } if (isset($matches[2])) { $text = sprintf('%'.$matches[2], $text); } return $text; }; $line = preg_replace_callback($regex, $callback, $this->format); // gets string length for each sub line with multiline format $linesLength = array_map(function ($subLine) { return Helper::strlenWithoutDecoration($this->output->getFormatter(), rtrim($subLine, "\r")); }, explode("\n", $line)); $linesWidth = max($linesLength); $terminalWidth = $this->terminal->getWidth(); if ($linesWidth <= $terminalWidth) { return $line; } $this->setBarWidth($this->barWidth - $linesWidth + $terminalWidth); return preg_replace_callback($regex, $callback, $this->format); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Console\Helper; /** * HelperInterface is the interface all helpers must implement. * * @author Fabien Potencier */ interface HelperInterface { /** * Sets the helper set associated with this helper. */ public function setHelperSet(HelperSet $helperSet = null); /** * Gets the helper set associated with this helper. * * @return HelperSet A HelperSet instance */ public function getHelperSet(); /** * Returns the canonical name of this helper. * * @return string The canonical name */ public function getName(); } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Console\Helper; use Symfony\Component\Console\Exception\InvalidArgumentException; use Symfony\Component\Console\Exception\LogicException; /** * Defines the styles for a Table. * * @author Fabien Potencier * @author Саша Стаменковић */ class TableStyle { private $paddingChar = ' '; private $horizontalBorderChar = '-'; private $verticalBorderChar = '|'; private $crossingChar = '+'; private $cellHeaderFormat = '%s'; private $cellRowFormat = '%s'; private $cellRowContentFormat = ' %s '; private $borderFormat = '%s'; private $padType = \STR_PAD_RIGHT; /** * Sets padding character, used for cell padding. * * @param string $paddingChar * * @return $this */ public function setPaddingChar($paddingChar) { if (!$paddingChar) { throw new LogicException('The padding char must not be empty.'); } $this->paddingChar = $paddingChar; return $this; } /** * Gets padding character, used for cell padding. * * @return string */ public function getPaddingChar() { return $this->paddingChar; } /** * Sets horizontal border character. * * @param string $horizontalBorderChar * * @return $this */ public function setHorizontalBorderChar($horizontalBorderChar) { $this->horizontalBorderChar = $horizontalBorderChar; return $this; } /** * Gets horizontal border character. * * @return string */ public function getHorizontalBorderChar() { return $this->horizontalBorderChar; } /** * Sets vertical border character. * * @param string $verticalBorderChar * * @return $this */ public function setVerticalBorderChar($verticalBorderChar) { $this->verticalBorderChar = $verticalBorderChar; return $this; } /** * Gets vertical border character. * * @return string */ public function getVerticalBorderChar() { return $this->verticalBorderChar; } /** * Sets crossing character. * * @param string $crossingChar * * @return $this */ public function setCrossingChar($crossingChar) { $this->crossingChar = $crossingChar; return $this; } /** * Gets crossing character. * * @return string */ public function getCrossingChar() { return $this->crossingChar; } /** * Sets header cell format. * * @param string $cellHeaderFormat * * @return $this */ public function setCellHeaderFormat($cellHeaderFormat) { $this->cellHeaderFormat = $cellHeaderFormat; return $this; } /** * Gets header cell format. * * @return string */ public function getCellHeaderFormat() { return $this->cellHeaderFormat; } /** * Sets row cell format. * * @param string $cellRowFormat * * @return $this */ public function setCellRowFormat($cellRowFormat) { $this->cellRowFormat = $cellRowFormat; return $this; } /** * Gets row cell format. * * @return string */ public function getCellRowFormat() { return $this->cellRowFormat; } /** * Sets row cell content format. * * @param string $cellRowContentFormat * * @return $this */ public function setCellRowContentFormat($cellRowContentFormat) { $this->cellRowContentFormat = $cellRowContentFormat; return $this; } /** * Gets row cell content format. * * @return string */ public function getCellRowContentFormat() { return $this->cellRowContentFormat; } /** * Sets table border format. * * @param string $borderFormat * * @return $this */ public function setBorderFormat($borderFormat) { $this->borderFormat = $borderFormat; return $this; } /** * Gets table border format. * * @return string */ public function getBorderFormat() { return $this->borderFormat; } /** * Sets cell padding type. * * @param int $padType STR_PAD_* * * @return $this */ public function setPadType($padType) { if (!\in_array($padType, [\STR_PAD_LEFT, \STR_PAD_RIGHT, \STR_PAD_BOTH], true)) { throw new InvalidArgumentException('Invalid padding type. Expected one of (STR_PAD_LEFT, STR_PAD_RIGHT, STR_PAD_BOTH).'); } $this->padType = $padType; return $this; } /** * Gets cell padding type. * * @return int */ public function getPadType() { return $this->padType; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Console\Helper; use Symfony\Component\Console\Exception\InvalidArgumentException; use Symfony\Component\Console\Exception\RuntimeException; use Symfony\Component\Console\Formatter\OutputFormatter; use Symfony\Component\Console\Formatter\OutputFormatterStyle; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\StreamableInputInterface; use Symfony\Component\Console\Output\ConsoleOutputInterface; use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Console\Question\ChoiceQuestion; use Symfony\Component\Console\Question\Question; use Symfony\Component\Console\Terminal; /** * The QuestionHelper class provides helpers to interact with the user. * * @author Fabien Potencier */ class QuestionHelper extends Helper { private $inputStream; private static $shell; private static $stty = true; /** * Asks a question to the user. * * @return mixed The user answer * * @throws RuntimeException If there is no data to read in the input stream */ public function ask(InputInterface $input, OutputInterface $output, Question $question) { if ($output instanceof ConsoleOutputInterface) { $output = $output->getErrorOutput(); } if (!$input->isInteractive()) { $default = $question->getDefault(); if (null === $default) { return $default; } if ($validator = $question->getValidator()) { return \call_user_func($question->getValidator(), $default); } elseif ($question instanceof ChoiceQuestion) { $choices = $question->getChoices(); if (!$question->isMultiselect()) { return isset($choices[$default]) ? $choices[$default] : $default; } $default = explode(',', $default); foreach ($default as $k => $v) { $v = trim($v); $default[$k] = isset($choices[$v]) ? $choices[$v] : $v; } } return $default; } if ($input instanceof StreamableInputInterface && $stream = $input->getStream()) { $this->inputStream = $stream; } if (!$question->getValidator()) { return $this->doAsk($output, $question); } $interviewer = function () use ($output, $question) { return $this->doAsk($output, $question); }; return $this->validateAttempts($interviewer, $output, $question); } /** * Sets the input stream to read from when interacting with the user. * * This is mainly useful for testing purpose. * * @deprecated since version 3.2, to be removed in 4.0. Use * StreamableInputInterface::setStream() instead. * * @param resource $stream The input stream * * @throws InvalidArgumentException In case the stream is not a resource */ public function setInputStream($stream) { @trigger_error(sprintf('The %s() method is deprecated since Symfony 3.2 and will be removed in 4.0. Use %s::setStream() instead.', __METHOD__, StreamableInputInterface::class), \E_USER_DEPRECATED); if (!\is_resource($stream)) { throw new InvalidArgumentException('Input stream must be a valid resource.'); } $this->inputStream = $stream; } /** * Returns the helper's input stream. * * @deprecated since version 3.2, to be removed in 4.0. Use * StreamableInputInterface::getStream() instead. * * @return resource */ public function getInputStream() { if (0 === \func_num_args() || func_get_arg(0)) { @trigger_error(sprintf('The %s() method is deprecated since Symfony 3.2 and will be removed in 4.0. Use %s::getStream() instead.', __METHOD__, StreamableInputInterface::class), \E_USER_DEPRECATED); } return $this->inputStream; } /** * {@inheritdoc} */ public function getName() { return 'question'; } /** * Prevents usage of stty. */ public static function disableStty() { self::$stty = false; } /** * Asks the question to the user. * * @return bool|mixed|string|null * * @throws RuntimeException In case the fallback is deactivated and the response cannot be hidden */ private function doAsk(OutputInterface $output, Question $question) { $this->writePrompt($output, $question); $inputStream = $this->inputStream ?: \STDIN; $autocomplete = $question->getAutocompleterValues(); if (\function_exists('sapi_windows_cp_set')) { // Codepage used by cmd.exe on Windows to allow special characters (éàüñ). @sapi_windows_cp_set(1252); } if (null === $autocomplete || !self::$stty || !Terminal::hasSttyAvailable()) { $ret = false; if ($question->isHidden()) { try { $ret = trim($this->getHiddenResponse($output, $inputStream)); } catch (RuntimeException $e) { if (!$question->isHiddenFallback()) { throw $e; } } } if (false === $ret) { $ret = fgets($inputStream, 4096); if (false === $ret) { throw new RuntimeException('Aborted.'); } $ret = trim($ret); } } else { $ret = trim($this->autocomplete($output, $question, $inputStream, \is_array($autocomplete) ? $autocomplete : iterator_to_array($autocomplete, false))); } $ret = \strlen($ret) > 0 ? $ret : $question->getDefault(); if ($normalizer = $question->getNormalizer()) { return $normalizer($ret); } return $ret; } /** * Outputs the question prompt. */ protected function writePrompt(OutputInterface $output, Question $question) { $message = $question->getQuestion(); if ($question instanceof ChoiceQuestion) { $output->writeln(array_merge([ $question->getQuestion(), ], $this->formatChoiceQuestionChoices($question, 'info'))); $message = $question->getPrompt(); } $output->write($message); } /** * @param string $tag * * @return string[] */ protected function formatChoiceQuestionChoices(ChoiceQuestion $question, $tag) { $messages = []; $maxWidth = max(array_map('self::strlen', array_keys($choices = $question->getChoices()))); foreach ($choices as $key => $value) { $padding = str_repeat(' ', $maxWidth - self::strlen($key)); $messages[] = sprintf(" [<$tag>%s$padding] %s", $key, $value); } return $messages; } /** * Outputs an error message. */ protected function writeError(OutputInterface $output, \Exception $error) { if (null !== $this->getHelperSet() && $this->getHelperSet()->has('formatter')) { $message = $this->getHelperSet()->get('formatter')->formatBlock($error->getMessage(), 'error'); } else { $message = ''.$error->getMessage().''; } $output->writeln($message); } /** * Autocompletes a question. * * @param resource $inputStream * * @return string */ private function autocomplete(OutputInterface $output, Question $question, $inputStream, array $autocomplete) { $fullChoice = ''; $ret = ''; $i = 0; $ofs = -1; $matches = $autocomplete; $numMatches = \count($matches); $sttyMode = shell_exec('stty -g'); // Disable icanon (so we can fread each keypress) and echo (we'll do echoing here instead) shell_exec('stty -icanon -echo'); // Add highlighted text style $output->getFormatter()->setStyle('hl', new OutputFormatterStyle('black', 'white')); // Read a keypress while (!feof($inputStream)) { $c = fread($inputStream, 1); // as opposed to fgets(), fread() returns an empty string when the stream content is empty, not false. if (false === $c || ('' === $ret && '' === $c && null === $question->getDefault())) { shell_exec(sprintf('stty %s', $sttyMode)); throw new RuntimeException('Aborted.'); } elseif ("\177" === $c) { // Backspace Character if (0 === $numMatches && 0 !== $i) { --$i; $fullChoice = self::substr($fullChoice, 0, $i); // Move cursor backwards $output->write("\033[1D"); } if (0 === $i) { $ofs = -1; $matches = $autocomplete; $numMatches = \count($matches); } else { $numMatches = 0; } // Pop the last character off the end of our string $ret = self::substr($ret, 0, $i); } elseif ("\033" === $c) { // Did we read an escape sequence? $c .= fread($inputStream, 2); // A = Up Arrow. B = Down Arrow if (isset($c[2]) && ('A' === $c[2] || 'B' === $c[2])) { if ('A' === $c[2] && -1 === $ofs) { $ofs = 0; } if (0 === $numMatches) { continue; } $ofs += ('A' === $c[2]) ? -1 : 1; $ofs = ($numMatches + $ofs) % $numMatches; } } elseif (\ord($c) < 32) { if ("\t" === $c || "\n" === $c) { if ($numMatches > 0 && -1 !== $ofs) { $ret = $matches[$ofs]; // Echo out remaining chars for current match $remainingCharacters = substr($ret, \strlen(trim($this->mostRecentlyEnteredValue($fullChoice)))); $output->write($remainingCharacters); $fullChoice .= $remainingCharacters; $i = self::strlen($fullChoice); } if ("\n" === $c) { $output->write($c); break; } $numMatches = 0; } continue; } else { if ("\x80" <= $c) { $c .= fread($inputStream, ["\xC0" => 1, "\xD0" => 1, "\xE0" => 2, "\xF0" => 3][$c & "\xF0"]); } $output->write($c); $ret .= $c; $fullChoice .= $c; ++$i; $tempRet = $ret; if ($question instanceof ChoiceQuestion && $question->isMultiselect()) { $tempRet = $this->mostRecentlyEnteredValue($fullChoice); } $numMatches = 0; $ofs = 0; foreach ($autocomplete as $value) { // If typed characters match the beginning chunk of value (e.g. [AcmeDe]moBundle) if (0 === strpos($value, $tempRet)) { $matches[$numMatches++] = $value; } } } // Erase characters from cursor to end of line $output->write("\033[K"); if ($numMatches > 0 && -1 !== $ofs) { // Save cursor position $output->write("\0337"); // Write highlighted text, complete the partially entered response $charactersEntered = \strlen(trim($this->mostRecentlyEnteredValue($fullChoice))); $output->write(''.OutputFormatter::escapeTrailingBackslash(substr($matches[$ofs], $charactersEntered)).''); // Restore cursor position $output->write("\0338"); } } // Reset stty so it behaves normally again shell_exec(sprintf('stty %s', $sttyMode)); return $fullChoice; } private function mostRecentlyEnteredValue($entered) { // Determine the most recent value that the user entered if (false === strpos($entered, ',')) { return $entered; } $choices = explode(',', $entered); if (\strlen($lastChoice = trim($choices[\count($choices) - 1])) > 0) { return $lastChoice; } return $entered; } /** * Gets a hidden response from user. * * @param OutputInterface $output An Output instance * @param resource $inputStream The handler resource * * @return string The answer * * @throws RuntimeException In case the fallback is deactivated and the response cannot be hidden */ private function getHiddenResponse(OutputInterface $output, $inputStream) { if ('\\' === \DIRECTORY_SEPARATOR) { $exe = __DIR__.'/../Resources/bin/hiddeninput.exe'; // handle code running from a phar if ('phar:' === substr(__FILE__, 0, 5)) { $tmpExe = sys_get_temp_dir().'/hiddeninput.exe'; copy($exe, $tmpExe); $exe = $tmpExe; } $value = rtrim(shell_exec($exe)); $output->writeln(''); if (isset($tmpExe)) { unlink($tmpExe); } return $value; } if (self::$stty && Terminal::hasSttyAvailable()) { $sttyMode = shell_exec('stty -g'); shell_exec('stty -echo'); $value = fgets($inputStream, 4096); shell_exec(sprintf('stty %s', $sttyMode)); if (false === $value) { throw new RuntimeException('Aborted.'); } $value = trim($value); $output->writeln(''); return $value; } if (false !== $shell = $this->getShell()) { $readCmd = 'csh' === $shell ? 'set mypassword = $<' : 'read -r mypassword'; $command = sprintf("/usr/bin/env %s -c 'stty -echo; %s; stty echo; echo \$mypassword'", $shell, $readCmd); $value = rtrim(shell_exec($command)); $output->writeln(''); return $value; } throw new RuntimeException('Unable to hide the response.'); } /** * Validates an attempt. * * @param callable $interviewer A callable that will ask for a question and return the result * @param OutputInterface $output An Output instance * @param Question $question A Question instance * * @return mixed The validated response * * @throws \Exception In case the max number of attempts has been reached and no valid response has been given */ private function validateAttempts(callable $interviewer, OutputInterface $output, Question $question) { $error = null; $attempts = $question->getMaxAttempts(); while (null === $attempts || $attempts--) { if (null !== $error) { $this->writeError($output, $error); } try { return \call_user_func($question->getValidator(), $interviewer()); } catch (RuntimeException $e) { throw $e; } catch (\Exception $error) { } } throw $error; } /** * Returns a valid unix shell. * * @return string|bool The valid shell name, false in case no valid shell is found */ private function getShell() { if (null !== self::$shell) { return self::$shell; } self::$shell = false; if (file_exists('/usr/bin/env')) { // handle other OSs with bash/zsh/ksh/csh if available to hide the answer $test = "/usr/bin/env %s -c 'echo OK' 2> /dev/null"; foreach (['bash', 'zsh', 'ksh', 'csh'] as $sh) { if ('OK' === rtrim(shell_exec(sprintf($test, $sh)))) { self::$shell = $sh; break; } } } return self::$shell; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Console\Helper; use Symfony\Component\Console\Exception\InvalidArgumentException; use Symfony\Component\Console\Output\OutputInterface; /** * Provides helpers to display a table. * * @author Fabien Potencier * @author Саша Стаменковић * @author Abdellatif Ait boudad * @author Max Grigorian */ class Table { /** * Table headers. */ private $headers = []; /** * Table rows. */ private $rows = []; /** * Column widths cache. */ private $effectiveColumnWidths = []; /** * Number of columns cache. * * @var int */ private $numberOfColumns; /** * @var OutputInterface */ private $output; /** * @var TableStyle */ private $style; /** * @var array */ private $columnStyles = []; /** * User set column widths. * * @var array */ private $columnWidths = []; private static $styles; public function __construct(OutputInterface $output) { $this->output = $output; if (!self::$styles) { self::$styles = self::initStyles(); } $this->setStyle('default'); } /** * Sets a style definition. * * @param string $name The style name * @param TableStyle $style A TableStyle instance */ public static function setStyleDefinition($name, TableStyle $style) { if (!self::$styles) { self::$styles = self::initStyles(); } self::$styles[$name] = $style; } /** * Gets a style definition by name. * * @param string $name The style name * * @return TableStyle */ public static function getStyleDefinition($name) { if (!self::$styles) { self::$styles = self::initStyles(); } if (isset(self::$styles[$name])) { return self::$styles[$name]; } throw new InvalidArgumentException(sprintf('Style "%s" is not defined.', $name)); } /** * Sets table style. * * @param TableStyle|string $name The style name or a TableStyle instance * * @return $this */ public function setStyle($name) { $this->style = $this->resolveStyle($name); return $this; } /** * Gets the current table style. * * @return TableStyle */ public function getStyle() { return $this->style; } /** * Sets table column style. * * @param int $columnIndex Column index * @param TableStyle|string $name The style name or a TableStyle instance * * @return $this */ public function setColumnStyle($columnIndex, $name) { $columnIndex = (int) $columnIndex; $this->columnStyles[$columnIndex] = $this->resolveStyle($name); return $this; } /** * Gets the current style for a column. * * If style was not set, it returns the global table style. * * @param int $columnIndex Column index * * @return TableStyle */ public function getColumnStyle($columnIndex) { if (isset($this->columnStyles[$columnIndex])) { return $this->columnStyles[$columnIndex]; } return $this->getStyle(); } /** * Sets the minimum width of a column. * * @param int $columnIndex Column index * @param int $width Minimum column width in characters * * @return $this */ public function setColumnWidth($columnIndex, $width) { $this->columnWidths[(int) $columnIndex] = (int) $width; return $this; } /** * Sets the minimum width of all columns. * * @return $this */ public function setColumnWidths(array $widths) { $this->columnWidths = []; foreach ($widths as $index => $width) { $this->setColumnWidth($index, $width); } return $this; } public function setHeaders(array $headers) { $headers = array_values($headers); if (!empty($headers) && !\is_array($headers[0])) { $headers = [$headers]; } $this->headers = $headers; return $this; } public function setRows(array $rows) { $this->rows = []; return $this->addRows($rows); } public function addRows(array $rows) { foreach ($rows as $row) { $this->addRow($row); } return $this; } public function addRow($row) { if ($row instanceof TableSeparator) { $this->rows[] = $row; return $this; } if (!\is_array($row)) { throw new InvalidArgumentException('A row must be an array or a TableSeparator instance.'); } $this->rows[] = array_values($row); return $this; } public function setRow($column, array $row) { $this->rows[$column] = $row; return $this; } /** * Renders table to output. * * Example: * * +---------------+-----------------------+------------------+ * | ISBN | Title | Author | * +---------------+-----------------------+------------------+ * | 99921-58-10-7 | Divine Comedy | Dante Alighieri | * | 9971-5-0210-0 | A Tale of Two Cities | Charles Dickens | * | 960-425-059-0 | The Lord of the Rings | J. R. R. Tolkien | * +---------------+-----------------------+------------------+ */ public function render() { $this->calculateNumberOfColumns(); $rows = $this->buildTableRows($this->rows); $headers = $this->buildTableRows($this->headers); $this->calculateColumnsWidth(array_merge($headers, $rows)); $this->renderRowSeparator(); if (!empty($headers)) { foreach ($headers as $header) { $this->renderRow($header, $this->style->getCellHeaderFormat()); $this->renderRowSeparator(); } } foreach ($rows as $row) { if ($row instanceof TableSeparator) { $this->renderRowSeparator(); } else { $this->renderRow($row, $this->style->getCellRowFormat()); } } if (!empty($rows)) { $this->renderRowSeparator(); } $this->cleanup(); } /** * Renders horizontal header separator. * * Example: * * +-----+-----------+-------+ */ private function renderRowSeparator() { if (0 === $count = $this->numberOfColumns) { return; } if (!$this->style->getHorizontalBorderChar() && !$this->style->getCrossingChar()) { return; } $markup = $this->style->getCrossingChar(); for ($column = 0; $column < $count; ++$column) { $markup .= str_repeat($this->style->getHorizontalBorderChar(), $this->effectiveColumnWidths[$column]).$this->style->getCrossingChar(); } $this->output->writeln(sprintf($this->style->getBorderFormat(), $markup)); } /** * Renders vertical column separator. */ private function renderColumnSeparator() { return sprintf($this->style->getBorderFormat(), $this->style->getVerticalBorderChar()); } /** * Renders table row. * * Example: * * | 9971-5-0210-0 | A Tale of Two Cities | Charles Dickens | * * @param string $cellFormat */ private function renderRow(array $row, $cellFormat) { if (empty($row)) { return; } $rowContent = $this->renderColumnSeparator(); foreach ($this->getRowColumns($row) as $column) { $rowContent .= $this->renderCell($row, $column, $cellFormat); $rowContent .= $this->renderColumnSeparator(); } $this->output->writeln($rowContent); } /** * Renders table cell with padding. * * @param int $column * @param string $cellFormat */ private function renderCell(array $row, $column, $cellFormat) { $cell = isset($row[$column]) ? $row[$column] : ''; $width = $this->effectiveColumnWidths[$column]; if ($cell instanceof TableCell && $cell->getColspan() > 1) { // add the width of the following columns(numbers of colspan). foreach (range($column + 1, $column + $cell->getColspan() - 1) as $nextColumn) { $width += $this->getColumnSeparatorWidth() + $this->effectiveColumnWidths[$nextColumn]; } } // str_pad won't work properly with multi-byte strings, we need to fix the padding if (false !== $encoding = mb_detect_encoding($cell, null, true)) { $width += \strlen($cell) - mb_strwidth($cell, $encoding); } $style = $this->getColumnStyle($column); if ($cell instanceof TableSeparator) { return sprintf($style->getBorderFormat(), str_repeat($style->getHorizontalBorderChar(), $width)); } $width += Helper::strlen($cell) - Helper::strlenWithoutDecoration($this->output->getFormatter(), $cell); $content = sprintf($style->getCellRowContentFormat(), $cell); return sprintf($cellFormat, str_pad($content, $width, $style->getPaddingChar(), $style->getPadType())); } /** * Calculate number of columns for this table. */ private function calculateNumberOfColumns() { if (null !== $this->numberOfColumns) { return; } $columns = [0]; foreach (array_merge($this->headers, $this->rows) as $row) { if ($row instanceof TableSeparator) { continue; } $columns[] = $this->getNumberOfColumns($row); } $this->numberOfColumns = max($columns); } private function buildTableRows($rows) { $unmergedRows = []; for ($rowKey = 0; $rowKey < \count($rows); ++$rowKey) { $rows = $this->fillNextRows($rows, $rowKey); // Remove any new line breaks and replace it with a new line foreach ($rows[$rowKey] as $column => $cell) { if (!strstr($cell, "\n")) { continue; } $lines = explode("\n", str_replace("\n", "\n", $cell)); foreach ($lines as $lineKey => $line) { if ($cell instanceof TableCell) { $line = new TableCell($line, ['colspan' => $cell->getColspan()]); } if (0 === $lineKey) { $rows[$rowKey][$column] = $line; } else { $unmergedRows[$rowKey][$lineKey][$column] = $line; } } } } $tableRows = []; foreach ($rows as $rowKey => $row) { $tableRows[] = $this->fillCells($row); if (isset($unmergedRows[$rowKey])) { $tableRows = array_merge($tableRows, $unmergedRows[$rowKey]); } } return $tableRows; } /** * fill rows that contains rowspan > 1. * * @param int $line * * @return array * * @throws InvalidArgumentException */ private function fillNextRows(array $rows, $line) { $unmergedRows = []; foreach ($rows[$line] as $column => $cell) { if (null !== $cell && !$cell instanceof TableCell && !is_scalar($cell) && !(\is_object($cell) && method_exists($cell, '__toString'))) { throw new InvalidArgumentException(sprintf('A cell must be a TableCell, a scalar or an object implementing "__toString()", "%s" given.', \gettype($cell))); } if ($cell instanceof TableCell && $cell->getRowspan() > 1) { $nbLines = $cell->getRowspan() - 1; $lines = [$cell]; if (strstr($cell, "\n")) { $lines = explode("\n", str_replace("\n", "\n", $cell)); $nbLines = \count($lines) > $nbLines ? substr_count($cell, "\n") : $nbLines; $rows[$line][$column] = new TableCell($lines[0], ['colspan' => $cell->getColspan()]); unset($lines[0]); } // create a two dimensional array (rowspan x colspan) $unmergedRows = array_replace_recursive(array_fill($line + 1, $nbLines, []), $unmergedRows); foreach ($unmergedRows as $unmergedRowKey => $unmergedRow) { $value = isset($lines[$unmergedRowKey - $line]) ? $lines[$unmergedRowKey - $line] : ''; $unmergedRows[$unmergedRowKey][$column] = new TableCell($value, ['colspan' => $cell->getColspan()]); if ($nbLines === $unmergedRowKey - $line) { break; } } } } foreach ($unmergedRows as $unmergedRowKey => $unmergedRow) { // we need to know if $unmergedRow will be merged or inserted into $rows if (isset($rows[$unmergedRowKey]) && \is_array($rows[$unmergedRowKey]) && ($this->getNumberOfColumns($rows[$unmergedRowKey]) + $this->getNumberOfColumns($unmergedRows[$unmergedRowKey]) <= $this->numberOfColumns)) { foreach ($unmergedRow as $cellKey => $cell) { // insert cell into row at cellKey position array_splice($rows[$unmergedRowKey], $cellKey, 0, [$cell]); } } else { $row = $this->copyRow($rows, $unmergedRowKey - 1); foreach ($unmergedRow as $column => $cell) { if (!empty($cell)) { $row[$column] = $unmergedRow[$column]; } } array_splice($rows, $unmergedRowKey, 0, [$row]); } } return $rows; } /** * fill cells for a row that contains colspan > 1. * * @return array */ private function fillCells($row) { $newRow = []; foreach ($row as $column => $cell) { $newRow[] = $cell; if ($cell instanceof TableCell && $cell->getColspan() > 1) { foreach (range($column + 1, $column + $cell->getColspan() - 1) as $position) { // insert empty value at column position $newRow[] = ''; } } } return $newRow ?: $row; } /** * @param int $line * * @return array */ private function copyRow(array $rows, $line) { $row = $rows[$line]; foreach ($row as $cellKey => $cellValue) { $row[$cellKey] = ''; if ($cellValue instanceof TableCell) { $row[$cellKey] = new TableCell('', ['colspan' => $cellValue->getColspan()]); } } return $row; } /** * Gets number of columns by row. * * @return int */ private function getNumberOfColumns(array $row) { $columns = \count($row); foreach ($row as $column) { $columns += $column instanceof TableCell ? ($column->getColspan() - 1) : 0; } return $columns; } /** * Gets list of columns for the given row. * * @return array */ private function getRowColumns(array $row) { $columns = range(0, $this->numberOfColumns - 1); foreach ($row as $cellKey => $cell) { if ($cell instanceof TableCell && $cell->getColspan() > 1) { // exclude grouped columns. $columns = array_diff($columns, range($cellKey + 1, $cellKey + $cell->getColspan() - 1)); } } return $columns; } /** * Calculates columns widths. */ private function calculateColumnsWidth(array $rows) { for ($column = 0; $column < $this->numberOfColumns; ++$column) { $lengths = []; foreach ($rows as $row) { if ($row instanceof TableSeparator) { continue; } foreach ($row as $i => $cell) { if ($cell instanceof TableCell) { $textContent = Helper::removeDecoration($this->output->getFormatter(), $cell); $textLength = Helper::strlen($textContent); if ($textLength > 0) { $contentColumns = str_split($textContent, ceil($textLength / $cell->getColspan())); foreach ($contentColumns as $position => $content) { $row[$i + $position] = $content; } } } } $lengths[] = $this->getCellWidth($row, $column); } $this->effectiveColumnWidths[$column] = max($lengths) + Helper::strlen($this->style->getCellRowContentFormat()) - 2; } } /** * Gets column width. * * @return int */ private function getColumnSeparatorWidth() { return Helper::strlen(sprintf($this->style->getBorderFormat(), $this->style->getVerticalBorderChar())); } /** * Gets cell width. * * @param int $column * * @return int */ private function getCellWidth(array $row, $column) { $cellWidth = 0; if (isset($row[$column])) { $cell = $row[$column]; $cellWidth = Helper::strlenWithoutDecoration($this->output->getFormatter(), $cell); } $columnWidth = isset($this->columnWidths[$column]) ? $this->columnWidths[$column] : 0; return max($cellWidth, $columnWidth); } /** * Called after rendering to cleanup cache data. */ private function cleanup() { $this->effectiveColumnWidths = []; $this->numberOfColumns = null; } private static function initStyles() { $borderless = new TableStyle(); $borderless ->setHorizontalBorderChar('=') ->setVerticalBorderChar(' ') ->setCrossingChar(' ') ; $compact = new TableStyle(); $compact ->setHorizontalBorderChar('') ->setVerticalBorderChar(' ') ->setCrossingChar('') ->setCellRowContentFormat('%s') ; $styleGuide = new TableStyle(); $styleGuide ->setHorizontalBorderChar('-') ->setVerticalBorderChar(' ') ->setCrossingChar(' ') ->setCellHeaderFormat('%s') ; return [ 'default' => new TableStyle(), 'borderless' => $borderless, 'compact' => $compact, 'symfony-style-guide' => $styleGuide, ]; } private function resolveStyle($name) { if ($name instanceof TableStyle) { return $name; } if (isset(self::$styles[$name])) { return self::$styles[$name]; } throw new InvalidArgumentException(sprintf('Style "%s" is not defined.', $name)); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Console\Helper; use Symfony\Component\Console\Input\InputAwareInterface; use Symfony\Component\Console\Input\InputInterface; /** * An implementation of InputAwareInterface for Helpers. * * @author Wouter J */ abstract class InputAwareHelper extends Helper implements InputAwareInterface { protected $input; /** * {@inheritdoc} */ public function setInput(InputInterface $input) { $this->input = $input; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Console\Helper; /** * Marks a row as being a separator. * * @author Fabien Potencier */ class TableSeparator extends TableCell { public function __construct(array $options = []) { parent::__construct('', $options); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Console\Helper; use Symfony\Component\Console\Exception\InvalidArgumentException; use Symfony\Component\Console\Exception\LogicException; use Symfony\Component\Console\Output\OutputInterface; /** * @author Kevin Bond */ class ProgressIndicator { private $output; private $startTime; private $format; private $message; private $indicatorValues; private $indicatorCurrent; private $indicatorChangeInterval; private $indicatorUpdateTime; private $started = false; private static $formatters; private static $formats; /** * @param string|null $format Indicator format * @param int $indicatorChangeInterval Change interval in milliseconds * @param array|null $indicatorValues Animated indicator characters */ public function __construct(OutputInterface $output, $format = null, $indicatorChangeInterval = 100, $indicatorValues = null) { $this->output = $output; if (null === $format) { $format = $this->determineBestFormat(); } if (null === $indicatorValues) { $indicatorValues = ['-', '\\', '|', '/']; } $indicatorValues = array_values($indicatorValues); if (2 > \count($indicatorValues)) { throw new InvalidArgumentException('Must have at least 2 indicator value characters.'); } $this->format = self::getFormatDefinition($format); $this->indicatorChangeInterval = $indicatorChangeInterval; $this->indicatorValues = $indicatorValues; $this->startTime = time(); } /** * Sets the current indicator message. * * @param string|null $message */ public function setMessage($message) { $this->message = $message; $this->display(); } /** * Starts the indicator output. * * @param $message */ public function start($message) { if ($this->started) { throw new LogicException('Progress indicator already started.'); } $this->message = $message; $this->started = true; $this->startTime = time(); $this->indicatorUpdateTime = $this->getCurrentTimeInMilliseconds() + $this->indicatorChangeInterval; $this->indicatorCurrent = 0; $this->display(); } /** * Advances the indicator. */ public function advance() { if (!$this->started) { throw new LogicException('Progress indicator has not yet been started.'); } if (!$this->output->isDecorated()) { return; } $currentTime = $this->getCurrentTimeInMilliseconds(); if ($currentTime < $this->indicatorUpdateTime) { return; } $this->indicatorUpdateTime = $currentTime + $this->indicatorChangeInterval; ++$this->indicatorCurrent; $this->display(); } /** * Finish the indicator with message. * * @param $message */ public function finish($message) { if (!$this->started) { throw new LogicException('Progress indicator has not yet been started.'); } $this->message = $message; $this->display(); $this->output->writeln(''); $this->started = false; } /** * Gets the format for a given name. * * @param string $name The format name * * @return string|null A format string */ public static function getFormatDefinition($name) { if (!self::$formats) { self::$formats = self::initFormats(); } return isset(self::$formats[$name]) ? self::$formats[$name] : null; } /** * Sets a placeholder formatter for a given name. * * This method also allow you to override an existing placeholder. * * @param string $name The placeholder name (including the delimiter char like %) * @param callable $callable A PHP callable */ public static function setPlaceholderFormatterDefinition($name, $callable) { if (!self::$formatters) { self::$formatters = self::initPlaceholderFormatters(); } self::$formatters[$name] = $callable; } /** * Gets the placeholder formatter for a given name. * * @param string $name The placeholder name (including the delimiter char like %) * * @return callable|null A PHP callable */ public static function getPlaceholderFormatterDefinition($name) { if (!self::$formatters) { self::$formatters = self::initPlaceholderFormatters(); } return isset(self::$formatters[$name]) ? self::$formatters[$name] : null; } private function display() { if (OutputInterface::VERBOSITY_QUIET === $this->output->getVerbosity()) { return; } $self = $this; $this->overwrite(preg_replace_callback("{%([a-z\-_]+)(?:\:([^%]+))?%}i", function ($matches) use ($self) { if ($formatter = $self::getPlaceholderFormatterDefinition($matches[1])) { return \call_user_func($formatter, $self); } return $matches[0]; }, $this->format)); } private function determineBestFormat() { switch ($this->output->getVerbosity()) { // OutputInterface::VERBOSITY_QUIET: display is disabled anyway case OutputInterface::VERBOSITY_VERBOSE: return $this->output->isDecorated() ? 'verbose' : 'verbose_no_ansi'; case OutputInterface::VERBOSITY_VERY_VERBOSE: case OutputInterface::VERBOSITY_DEBUG: return $this->output->isDecorated() ? 'very_verbose' : 'very_verbose_no_ansi'; default: return $this->output->isDecorated() ? 'normal' : 'normal_no_ansi'; } } /** * Overwrites a previous message to the output. * * @param string $message The message */ private function overwrite($message) { if ($this->output->isDecorated()) { $this->output->write("\x0D\x1B[2K"); $this->output->write($message); } else { $this->output->writeln($message); } } private function getCurrentTimeInMilliseconds() { return round(microtime(true) * 1000); } private static function initPlaceholderFormatters() { return [ 'indicator' => function (self $indicator) { return $indicator->indicatorValues[$indicator->indicatorCurrent % \count($indicator->indicatorValues)]; }, 'message' => function (self $indicator) { return $indicator->message; }, 'elapsed' => function (self $indicator) { return Helper::formatTime(time() - $indicator->startTime); }, 'memory' => function () { return Helper::formatMemory(memory_get_usage(true)); }, ]; } private static function initFormats() { return [ 'normal' => ' %indicator% %message%', 'normal_no_ansi' => ' %message%', 'verbose' => ' %indicator% %message% (%elapsed:6s%)', 'verbose_no_ansi' => ' %message% (%elapsed:6s%)', 'very_verbose' => ' %indicator% %message% (%elapsed:6s%, %memory:6s%)', 'very_verbose_no_ansi' => ' %message% (%elapsed:6s%, %memory:6s%)', ]; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Console\Helper; use Symfony\Component\Console\Exception\InvalidArgumentException; /** * @author Abdellatif Ait boudad */ class TableCell { private $value; private $options = [ 'rowspan' => 1, 'colspan' => 1, ]; /** * @param string $value */ public function __construct($value = '', array $options = []) { if (is_numeric($value) && !\is_string($value)) { $value = (string) $value; } $this->value = $value; // check option names if ($diff = array_diff(array_keys($options), array_keys($this->options))) { throw new InvalidArgumentException(sprintf('The TableCell does not support the following options: \'%s\'.', implode('\', \'', $diff))); } $this->options = array_merge($this->options, $options); } /** * Returns the cell value. * * @return string */ public function __toString() { return $this->value; } /** * Gets number of colspan. * * @return int */ public function getColspan() { return (int) $this->options['colspan']; } /** * Gets number of rowspan. * * @return int */ public function getRowspan() { return (int) $this->options['rowspan']; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Console\Helper; use Symfony\Component\Console\Formatter\OutputFormatter; /** * The Formatter class provides helpers to format messages. * * @author Fabien Potencier */ class FormatterHelper extends Helper { /** * Formats a message within a section. * * @param string $section The section name * @param string $message The message * @param string $style The style to apply to the section * * @return string The format section */ public function formatSection($section, $message, $style = 'info') { return sprintf('<%s>[%s] %s', $style, $section, $style, $message); } /** * Formats a message as a block of text. * * @param string|array $messages The message to write in the block * @param string $style The style to apply to the whole block * @param bool $large Whether to return a large block * * @return string The formatter message */ public function formatBlock($messages, $style, $large = false) { if (!\is_array($messages)) { $messages = [$messages]; } $len = 0; $lines = []; foreach ($messages as $message) { $message = OutputFormatter::escape($message); $lines[] = sprintf($large ? ' %s ' : ' %s ', $message); $len = max(self::strlen($message) + ($large ? 4 : 2), $len); } $messages = $large ? [str_repeat(' ', $len)] : []; for ($i = 0; isset($lines[$i]); ++$i) { $messages[] = $lines[$i].str_repeat(' ', $len - self::strlen($lines[$i])); } if ($large) { $messages[] = str_repeat(' ', $len); } for ($i = 0; isset($messages[$i]); ++$i) { $messages[$i] = sprintf('<%s>%s', $style, $messages[$i], $style); } return implode("\n", $messages); } /** * Truncates a message to the given length. * * @param string $message * @param int $length * @param string $suffix * * @return string */ public function truncate($message, $length, $suffix = '...') { $computedLength = $length - self::strlen($suffix); if ($computedLength > self::strlen($message)) { return $message; } return self::substr($message, 0, $length).$suffix; } /** * {@inheritdoc} */ public function getName() { return 'formatter'; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Console\Helper; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Exception\InvalidArgumentException; /** * HelperSet represents a set of helpers to be used with a command. * * @author Fabien Potencier */ class HelperSet implements \IteratorAggregate { /** * @var Helper[] */ private $helpers = []; private $command; /** * @param Helper[] $helpers An array of helper */ public function __construct(array $helpers = []) { foreach ($helpers as $alias => $helper) { $this->set($helper, \is_int($alias) ? null : $alias); } } /** * Sets a helper. * * @param HelperInterface $helper The helper instance * @param string $alias An alias */ public function set(HelperInterface $helper, $alias = null) { $this->helpers[$helper->getName()] = $helper; if (null !== $alias) { $this->helpers[$alias] = $helper; } $helper->setHelperSet($this); } /** * Returns true if the helper if defined. * * @param string $name The helper name * * @return bool true if the helper is defined, false otherwise */ public function has($name) { return isset($this->helpers[$name]); } /** * Gets a helper value. * * @param string $name The helper name * * @return HelperInterface The helper instance * * @throws InvalidArgumentException if the helper is not defined */ public function get($name) { if (!$this->has($name)) { throw new InvalidArgumentException(sprintf('The helper "%s" is not defined.', $name)); } return $this->helpers[$name]; } public function setCommand(Command $command = null) { $this->command = $command; } /** * Gets the command associated with this helper set. * * @return Command A Command instance */ public function getCommand() { return $this->command; } /** * @return Helper[] */ public function getIterator() { return new \ArrayIterator($this->helpers); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Console\Helper; use Symfony\Component\Console\Descriptor\DescriptorInterface; use Symfony\Component\Console\Descriptor\JsonDescriptor; use Symfony\Component\Console\Descriptor\MarkdownDescriptor; use Symfony\Component\Console\Descriptor\TextDescriptor; use Symfony\Component\Console\Descriptor\XmlDescriptor; use Symfony\Component\Console\Exception\InvalidArgumentException; use Symfony\Component\Console\Output\OutputInterface; /** * This class adds helper method to describe objects in various formats. * * @author Jean-François Simon */ class DescriptorHelper extends Helper { /** * @var DescriptorInterface[] */ private $descriptors = []; public function __construct() { $this ->register('txt', new TextDescriptor()) ->register('xml', new XmlDescriptor()) ->register('json', new JsonDescriptor()) ->register('md', new MarkdownDescriptor()) ; } /** * Describes an object if supported. * * Available options are: * * format: string, the output format name * * raw_text: boolean, sets output type as raw * * @param object $object * * @throws InvalidArgumentException when the given format is not supported */ public function describe(OutputInterface $output, $object, array $options = []) { $options = array_merge([ 'raw_text' => false, 'format' => 'txt', ], $options); if (!isset($this->descriptors[$options['format']])) { throw new InvalidArgumentException(sprintf('Unsupported format "%s".', $options['format'])); } $descriptor = $this->descriptors[$options['format']]; $descriptor->describe($output, $object, $options); } /** * Registers a descriptor. * * @param string $format * * @return $this */ public function register($format, DescriptorInterface $descriptor) { $this->descriptors[$format] = $descriptor; return $this; } /** * {@inheritdoc} */ public function getName() { return 'descriptor'; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Console\Helper; use Symfony\Component\Console\Exception\LogicException; use Symfony\Component\Console\Formatter\OutputFormatter; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Console\Question\ChoiceQuestion; use Symfony\Component\Console\Question\ConfirmationQuestion; use Symfony\Component\Console\Question\Question; use Symfony\Component\Console\Style\SymfonyStyle; /** * Symfony Style Guide compliant question helper. * * @author Kevin Bond */ class SymfonyQuestionHelper extends QuestionHelper { /** * {@inheritdoc} * * To be removed in 4.0 */ public function ask(InputInterface $input, OutputInterface $output, Question $question) { $validator = $question->getValidator(); $question->setValidator(function ($value) use ($validator) { if (null !== $validator) { $value = $validator($value); } else { // make required if (!\is_array($value) && !\is_bool($value) && 0 === \strlen($value)) { @trigger_error('The default question validator is deprecated since Symfony 3.3 and will not be used anymore in version 4.0. Set a custom question validator if needed.', \E_USER_DEPRECATED); throw new LogicException('A value is required.'); } } return $value; }); return parent::ask($input, $output, $question); } /** * {@inheritdoc} */ protected function writePrompt(OutputInterface $output, Question $question) { $text = OutputFormatter::escapeTrailingBackslash($question->getQuestion()); $default = $question->getDefault(); switch (true) { case null === $default: $text = sprintf(' %s:', $text); break; case $question instanceof ConfirmationQuestion: $text = sprintf(' %s (yes/no) [%s]:', $text, $default ? 'yes' : 'no'); break; case $question instanceof ChoiceQuestion && $question->isMultiselect(): $choices = $question->getChoices(); $default = explode(',', $default); foreach ($default as $key => $value) { $default[$key] = $choices[trim($value)]; } $text = sprintf(' %s [%s]:', $text, OutputFormatter::escape(implode(', ', $default))); break; case $question instanceof ChoiceQuestion: $choices = $question->getChoices(); $text = sprintf(' %s [%s]:', $text, OutputFormatter::escape(isset($choices[$default]) ? $choices[$default] : $default)); break; default: $text = sprintf(' %s [%s]:', $text, OutputFormatter::escape($default)); } $output->writeln($text); $prompt = ' > '; if ($question instanceof ChoiceQuestion) { $output->writeln($this->formatChoiceQuestionChoices($question, 'comment')); $prompt = $question->getPrompt(); } $output->write($prompt); } /** * {@inheritdoc} */ protected function writeError(OutputInterface $output, \Exception $error) { if ($output instanceof SymfonyStyle) { $output->newLine(); $output->error($error->getMessage()); return; } parent::writeError($output, $error); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Console\Helper; use Symfony\Component\Console\Output\ConsoleOutputInterface; use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Process\Exception\ProcessFailedException; use Symfony\Component\Process\Process; /** * The ProcessHelper class provides helpers to run external processes. * * @author Fabien Potencier */ class ProcessHelper extends Helper { /** * Runs an external process. * * @param OutputInterface $output An OutputInterface instance * @param string|array|Process $cmd An instance of Process or an array of arguments to escape and run or a command to run * @param string|null $error An error message that must be displayed if something went wrong * @param callable|null $callback A PHP callback to run whenever there is some * output available on STDOUT or STDERR * @param int $verbosity The threshold for verbosity * * @return Process The process that ran */ public function run(OutputInterface $output, $cmd, $error = null, callable $callback = null, $verbosity = OutputInterface::VERBOSITY_VERY_VERBOSE) { if (!class_exists(Process::class)) { throw new \LogicException('The ProcessHelper cannot be run as the Process component is not installed. Try running "compose require symfony/process".'); } if ($output instanceof ConsoleOutputInterface) { $output = $output->getErrorOutput(); } $formatter = $this->getHelperSet()->get('debug_formatter'); if ($cmd instanceof Process) { $process = $cmd; } else { $process = new Process($cmd); } if ($verbosity <= $output->getVerbosity()) { $output->write($formatter->start(spl_object_hash($process), $this->escapeString($process->getCommandLine()))); } if ($output->isDebug()) { $callback = $this->wrapCallback($output, $process, $callback); } $process->run($callback); if ($verbosity <= $output->getVerbosity()) { $message = $process->isSuccessful() ? 'Command ran successfully' : sprintf('%s Command did not run successfully', $process->getExitCode()); $output->write($formatter->stop(spl_object_hash($process), $message, $process->isSuccessful())); } if (!$process->isSuccessful() && null !== $error) { $output->writeln(sprintf('%s', $this->escapeString($error))); } return $process; } /** * Runs the process. * * This is identical to run() except that an exception is thrown if the process * exits with a non-zero exit code. * * @param OutputInterface $output An OutputInterface instance * @param string|Process $cmd An instance of Process or a command to run * @param string|null $error An error message that must be displayed if something went wrong * @param callable|null $callback A PHP callback to run whenever there is some * output available on STDOUT or STDERR * * @return Process The process that ran * * @throws ProcessFailedException * * @see run() */ public function mustRun(OutputInterface $output, $cmd, $error = null, callable $callback = null) { $process = $this->run($output, $cmd, $error, $callback); if (!$process->isSuccessful()) { throw new ProcessFailedException($process); } return $process; } /** * Wraps a Process callback to add debugging output. * * @param OutputInterface $output An OutputInterface interface * @param Process $process The Process * @param callable|null $callback A PHP callable * * @return callable */ public function wrapCallback(OutputInterface $output, Process $process, callable $callback = null) { if ($output instanceof ConsoleOutputInterface) { $output = $output->getErrorOutput(); } $formatter = $this->getHelperSet()->get('debug_formatter'); return function ($type, $buffer) use ($output, $process, $callback, $formatter) { $output->write($formatter->progress(spl_object_hash($process), $this->escapeString($buffer), Process::ERR === $type)); if (null !== $callback) { \call_user_func($callback, $type, $buffer); } }; } private function escapeString($str) { return str_replace('<', '\\<', $str); } /** * {@inheritdoc} */ public function getName() { return 'process'; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Console\CommandLoader; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Exception\CommandNotFoundException; /** * @author Robin Chalas */ interface CommandLoaderInterface { /** * Loads a command. * * @param string $name * * @return Command * * @throws CommandNotFoundException */ public function get($name); /** * Checks if a command exists. * * @param string $name * * @return bool */ public function has($name); /** * @return string[] All registered command names */ public function getNames(); } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Console\CommandLoader; use Psr\Container\ContainerInterface; use Symfony\Component\Console\Exception\CommandNotFoundException; /** * Loads commands from a PSR-11 container. * * @author Robin Chalas */ class ContainerCommandLoader implements CommandLoaderInterface { private $container; private $commandMap; /** * @param ContainerInterface $container A container from which to load command services * @param array $commandMap An array with command names as keys and service ids as values */ public function __construct(ContainerInterface $container, array $commandMap) { $this->container = $container; $this->commandMap = $commandMap; } /** * {@inheritdoc} */ public function get($name) { if (!$this->has($name)) { throw new CommandNotFoundException(sprintf('Command "%s" does not exist.', $name)); } return $this->container->get($this->commandMap[$name]); } /** * {@inheritdoc} */ public function has($name) { return isset($this->commandMap[$name]) && $this->container->has($this->commandMap[$name]); } /** * {@inheritdoc} */ public function getNames() { return array_keys($this->commandMap); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Console\CommandLoader; use Symfony\Component\Console\Exception\CommandNotFoundException; /** * A simple command loader using factories to instantiate commands lazily. * * @author Maxime Steinhausser */ class FactoryCommandLoader implements CommandLoaderInterface { private $factories; /** * @param callable[] $factories Indexed by command names */ public function __construct(array $factories) { $this->factories = $factories; } /** * {@inheritdoc} */ public function has($name) { return isset($this->factories[$name]); } /** * {@inheritdoc} */ public function get($name) { if (!isset($this->factories[$name])) { throw new CommandNotFoundException(sprintf('Command "%s" does not exist.', $name)); } $factory = $this->factories[$name]; return $factory(); } /** * {@inheritdoc} */ public function getNames() { return array_keys($this->factories); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Console\Logger; use Psr\Log\AbstractLogger; use Psr\Log\InvalidArgumentException; use Psr\Log\LogLevel; use Symfony\Component\Console\Output\ConsoleOutputInterface; use Symfony\Component\Console\Output\OutputInterface; /** * PSR-3 compliant console logger. * * @author Kévin Dunglas * * @see https://www.php-fig.org/psr/psr-3/ */ class ConsoleLogger extends AbstractLogger { const INFO = 'info'; const ERROR = 'error'; private $output; private $verbosityLevelMap = [ LogLevel::EMERGENCY => OutputInterface::VERBOSITY_NORMAL, LogLevel::ALERT => OutputInterface::VERBOSITY_NORMAL, LogLevel::CRITICAL => OutputInterface::VERBOSITY_NORMAL, LogLevel::ERROR => OutputInterface::VERBOSITY_NORMAL, LogLevel::WARNING => OutputInterface::VERBOSITY_NORMAL, LogLevel::NOTICE => OutputInterface::VERBOSITY_VERBOSE, LogLevel::INFO => OutputInterface::VERBOSITY_VERY_VERBOSE, LogLevel::DEBUG => OutputInterface::VERBOSITY_DEBUG, ]; private $formatLevelMap = [ LogLevel::EMERGENCY => self::ERROR, LogLevel::ALERT => self::ERROR, LogLevel::CRITICAL => self::ERROR, LogLevel::ERROR => self::ERROR, LogLevel::WARNING => self::INFO, LogLevel::NOTICE => self::INFO, LogLevel::INFO => self::INFO, LogLevel::DEBUG => self::INFO, ]; private $errored = false; public function __construct(OutputInterface $output, array $verbosityLevelMap = [], array $formatLevelMap = []) { $this->output = $output; $this->verbosityLevelMap = $verbosityLevelMap + $this->verbosityLevelMap; $this->formatLevelMap = $formatLevelMap + $this->formatLevelMap; } /** * {@inheritdoc} */ public function log($level, $message, array $context = []) { if (!isset($this->verbosityLevelMap[$level])) { throw new InvalidArgumentException(sprintf('The log level "%s" does not exist.', $level)); } $output = $this->output; // Write to the error output if necessary and available if (self::ERROR === $this->formatLevelMap[$level]) { if ($this->output instanceof ConsoleOutputInterface) { $output = $output->getErrorOutput(); } $this->errored = true; } // the if condition check isn't necessary -- it's the same one that $output will do internally anyway. // We only do it for efficiency here as the message formatting is relatively expensive. if ($output->getVerbosity() >= $this->verbosityLevelMap[$level]) { $output->writeln(sprintf('<%1$s>[%2$s] %3$s', $this->formatLevelMap[$level], $level, $this->interpolate($message, $context)), $this->verbosityLevelMap[$level]); } } /** * Returns true when any messages have been logged at error levels. * * @return bool */ public function hasErrored() { return $this->errored; } /** * Interpolates context values into the message placeholders. * * @author PHP Framework Interoperability Group * * @param string $message * * @return string */ private function interpolate($message, array $context) { if (false === strpos($message, '{')) { return $message; } $replacements = []; foreach ($context as $key => $val) { if (null === $val || is_scalar($val) || (\is_object($val) && method_exists($val, '__toString'))) { $replacements["{{$key}}"] = $val; } elseif ($val instanceof \DateTimeInterface) { $replacements["{{$key}}"] = $val->format(\DateTime::RFC3339); } elseif (\is_object($val)) { $replacements["{{$key}}"] = '[object '.\get_class($val).']'; } else { $replacements["{{$key}}"] = '['.\gettype($val).']'; } } return strtr($message, $replacements); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Console\Output; use Symfony\Component\Console\Formatter\OutputFormatterInterface; /** * ConsoleOutput is the default class for all CLI output. It uses STDOUT and STDERR. * * This class is a convenient wrapper around `StreamOutput` for both STDOUT and STDERR. * * $output = new ConsoleOutput(); * * This is equivalent to: * * $output = new StreamOutput(fopen('php://stdout', 'w')); * $stdErr = new StreamOutput(fopen('php://stderr', 'w')); * * @author Fabien Potencier */ class ConsoleOutput extends StreamOutput implements ConsoleOutputInterface { private $stderr; /** * @param int $verbosity The verbosity level (one of the VERBOSITY constants in OutputInterface) * @param bool|null $decorated Whether to decorate messages (null for auto-guessing) * @param OutputFormatterInterface|null $formatter Output formatter instance (null to use default OutputFormatter) */ public function __construct($verbosity = self::VERBOSITY_NORMAL, $decorated = null, OutputFormatterInterface $formatter = null) { parent::__construct($this->openOutputStream(), $verbosity, $decorated, $formatter); $actualDecorated = $this->isDecorated(); $this->stderr = new StreamOutput($this->openErrorStream(), $verbosity, $decorated, $this->getFormatter()); if (null === $decorated) { $this->setDecorated($actualDecorated && $this->stderr->isDecorated()); } } /** * {@inheritdoc} */ public function setDecorated($decorated) { parent::setDecorated($decorated); $this->stderr->setDecorated($decorated); } /** * {@inheritdoc} */ public function setFormatter(OutputFormatterInterface $formatter) { parent::setFormatter($formatter); $this->stderr->setFormatter($formatter); } /** * {@inheritdoc} */ public function setVerbosity($level) { parent::setVerbosity($level); $this->stderr->setVerbosity($level); } /** * {@inheritdoc} */ public function getErrorOutput() { return $this->stderr; } /** * {@inheritdoc} */ public function setErrorOutput(OutputInterface $error) { $this->stderr = $error; } /** * Returns true if current environment supports writing console output to * STDOUT. * * @return bool */ protected function hasStdoutSupport() { return false === $this->isRunningOS400(); } /** * Returns true if current environment supports writing console output to * STDERR. * * @return bool */ protected function hasStderrSupport() { return false === $this->isRunningOS400(); } /** * Checks if current executing environment is IBM iSeries (OS400), which * doesn't properly convert character-encodings between ASCII to EBCDIC. * * @return bool */ private function isRunningOS400() { $checks = [ \function_exists('php_uname') ? php_uname('s') : '', getenv('OSTYPE'), \PHP_OS, ]; return false !== stripos(implode(';', $checks), 'OS400'); } /** * @return resource */ private function openOutputStream() { if (!$this->hasStdoutSupport()) { return fopen('php://output', 'w'); } return @fopen('php://stdout', 'w') ?: fopen('php://output', 'w'); } /** * @return resource */ private function openErrorStream() { return fopen($this->hasStderrSupport() ? 'php://stderr' : 'php://output', 'w'); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Console\Output; use Symfony\Component\Console\Formatter\OutputFormatterInterface; /** * OutputInterface is the interface implemented by all Output classes. * * @author Fabien Potencier */ interface OutputInterface { const VERBOSITY_QUIET = 16; const VERBOSITY_NORMAL = 32; const VERBOSITY_VERBOSE = 64; const VERBOSITY_VERY_VERBOSE = 128; const VERBOSITY_DEBUG = 256; const OUTPUT_NORMAL = 1; const OUTPUT_RAW = 2; const OUTPUT_PLAIN = 4; /** * Writes a message to the output. * * @param string|array $messages The message as an array of strings or a single string * @param bool $newline Whether to add a newline * @param int $options A bitmask of options (one of the OUTPUT or VERBOSITY constants), 0 is considered the same as self::OUTPUT_NORMAL | self::VERBOSITY_NORMAL */ public function write($messages, $newline = false, $options = 0); /** * Writes a message to the output and adds a newline at the end. * * @param string|array $messages The message as an array of strings or a single string * @param int $options A bitmask of options (one of the OUTPUT or VERBOSITY constants), 0 is considered the same as self::OUTPUT_NORMAL | self::VERBOSITY_NORMAL */ public function writeln($messages, $options = 0); /** * Sets the verbosity of the output. * * @param int $level The level of verbosity (one of the VERBOSITY constants) */ public function setVerbosity($level); /** * Gets the current verbosity of the output. * * @return int The current level of verbosity (one of the VERBOSITY constants) */ public function getVerbosity(); /** * Returns whether verbosity is quiet (-q). * * @return bool true if verbosity is set to VERBOSITY_QUIET, false otherwise */ public function isQuiet(); /** * Returns whether verbosity is verbose (-v). * * @return bool true if verbosity is set to VERBOSITY_VERBOSE, false otherwise */ public function isVerbose(); /** * Returns whether verbosity is very verbose (-vv). * * @return bool true if verbosity is set to VERBOSITY_VERY_VERBOSE, false otherwise */ public function isVeryVerbose(); /** * Returns whether verbosity is debug (-vvv). * * @return bool true if verbosity is set to VERBOSITY_DEBUG, false otherwise */ public function isDebug(); /** * Sets the decorated flag. * * @param bool $decorated Whether to decorate the messages */ public function setDecorated($decorated); /** * Gets the decorated flag. * * @return bool true if the output will decorate messages, false otherwise */ public function isDecorated(); public function setFormatter(OutputFormatterInterface $formatter); /** * Returns current output formatter instance. * * @return OutputFormatterInterface */ public function getFormatter(); } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Console\Output; /** * @author Jean-François Simon */ class BufferedOutput extends Output { private $buffer = ''; /** * Empties buffer and returns its content. * * @return string */ public function fetch() { $content = $this->buffer; $this->buffer = ''; return $content; } /** * {@inheritdoc} */ protected function doWrite($message, $newline) { $this->buffer .= $message; if ($newline) { $this->buffer .= \PHP_EOL; } } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Console\Output; use Symfony\Component\Console\Formatter\OutputFormatter; use Symfony\Component\Console\Formatter\OutputFormatterInterface; /** * Base class for output classes. * * There are five levels of verbosity: * * * normal: no option passed (normal output) * * verbose: -v (more output) * * very verbose: -vv (highly extended output) * * debug: -vvv (all debug output) * * quiet: -q (no output) * * @author Fabien Potencier */ abstract class Output implements OutputInterface { private $verbosity; private $formatter; /** * @param int $verbosity The verbosity level (one of the VERBOSITY constants in OutputInterface) * @param bool $decorated Whether to decorate messages * @param OutputFormatterInterface|null $formatter Output formatter instance (null to use default OutputFormatter) */ public function __construct($verbosity = self::VERBOSITY_NORMAL, $decorated = false, OutputFormatterInterface $formatter = null) { $this->verbosity = null === $verbosity ? self::VERBOSITY_NORMAL : $verbosity; $this->formatter = $formatter ?: new OutputFormatter(); $this->formatter->setDecorated($decorated); } /** * {@inheritdoc} */ public function setFormatter(OutputFormatterInterface $formatter) { $this->formatter = $formatter; } /** * {@inheritdoc} */ public function getFormatter() { return $this->formatter; } /** * {@inheritdoc} */ public function setDecorated($decorated) { $this->formatter->setDecorated($decorated); } /** * {@inheritdoc} */ public function isDecorated() { return $this->formatter->isDecorated(); } /** * {@inheritdoc} */ public function setVerbosity($level) { $this->verbosity = (int) $level; } /** * {@inheritdoc} */ public function getVerbosity() { return $this->verbosity; } /** * {@inheritdoc} */ public function isQuiet() { return self::VERBOSITY_QUIET === $this->verbosity; } /** * {@inheritdoc} */ public function isVerbose() { return self::VERBOSITY_VERBOSE <= $this->verbosity; } /** * {@inheritdoc} */ public function isVeryVerbose() { return self::VERBOSITY_VERY_VERBOSE <= $this->verbosity; } /** * {@inheritdoc} */ public function isDebug() { return self::VERBOSITY_DEBUG <= $this->verbosity; } /** * {@inheritdoc} */ public function writeln($messages, $options = self::OUTPUT_NORMAL) { $this->write($messages, true, $options); } /** * {@inheritdoc} */ public function write($messages, $newline = false, $options = self::OUTPUT_NORMAL) { $messages = (array) $messages; $types = self::OUTPUT_NORMAL | self::OUTPUT_RAW | self::OUTPUT_PLAIN; $type = $types & $options ?: self::OUTPUT_NORMAL; $verbosities = self::VERBOSITY_QUIET | self::VERBOSITY_NORMAL | self::VERBOSITY_VERBOSE | self::VERBOSITY_VERY_VERBOSE | self::VERBOSITY_DEBUG; $verbosity = $verbosities & $options ?: self::VERBOSITY_NORMAL; if ($verbosity > $this->getVerbosity()) { return; } foreach ($messages as $message) { switch ($type) { case OutputInterface::OUTPUT_NORMAL: $message = $this->formatter->format($message); break; case OutputInterface::OUTPUT_RAW: break; case OutputInterface::OUTPUT_PLAIN: $message = strip_tags($this->formatter->format($message)); break; } $this->doWrite($message, $newline); } } /** * Writes a message to the output. * * @param string $message A message to write to the output * @param bool $newline Whether to add a newline or not */ abstract protected function doWrite($message, $newline); } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Console\Output; /** * ConsoleOutputInterface is the interface implemented by ConsoleOutput class. * This adds information about stderr output stream. * * @author Dariusz Górecki */ interface ConsoleOutputInterface extends OutputInterface { /** * Gets the OutputInterface for errors. * * @return OutputInterface */ public function getErrorOutput(); public function setErrorOutput(OutputInterface $error); } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Console\Output; use Symfony\Component\Console\Exception\InvalidArgumentException; use Symfony\Component\Console\Formatter\OutputFormatterInterface; /** * StreamOutput writes the output to a given stream. * * Usage: * * $output = new StreamOutput(fopen('php://stdout', 'w')); * * As `StreamOutput` can use any stream, you can also use a file: * * $output = new StreamOutput(fopen('/path/to/output.log', 'a', false)); * * @author Fabien Potencier */ class StreamOutput extends Output { private $stream; /** * @param resource $stream A stream resource * @param int $verbosity The verbosity level (one of the VERBOSITY constants in OutputInterface) * @param bool|null $decorated Whether to decorate messages (null for auto-guessing) * @param OutputFormatterInterface|null $formatter Output formatter instance (null to use default OutputFormatter) * * @throws InvalidArgumentException When first argument is not a real stream */ public function __construct($stream, $verbosity = self::VERBOSITY_NORMAL, $decorated = null, OutputFormatterInterface $formatter = null) { if (!\is_resource($stream) || 'stream' !== get_resource_type($stream)) { throw new InvalidArgumentException('The StreamOutput class needs a stream as its first argument.'); } $this->stream = $stream; if (null === $decorated) { $decorated = $this->hasColorSupport(); } parent::__construct($verbosity, $decorated, $formatter); } /** * Gets the stream attached to this StreamOutput instance. * * @return resource A stream resource */ public function getStream() { return $this->stream; } /** * {@inheritdoc} */ protected function doWrite($message, $newline) { if ($newline) { $message .= \PHP_EOL; } @fwrite($this->stream, $message); fflush($this->stream); } /** * Returns true if the stream supports colorization. * * Colorization is disabled if not supported by the stream: * * This is tricky on Windows, because Cygwin, Msys2 etc emulate pseudo * terminals via named pipes, so we can only check the environment. * * Reference: Composer\XdebugHandler\Process::supportsColor * https://github.com/composer/xdebug-handler * * @return bool true if the stream supports colorization, false otherwise */ protected function hasColorSupport() { if ('Hyper' === getenv('TERM_PROGRAM')) { return true; } if (\DIRECTORY_SEPARATOR === '\\') { return (\function_exists('sapi_windows_vt100_support') && @sapi_windows_vt100_support($this->stream)) || false !== getenv('ANSICON') || 'ON' === getenv('ConEmuANSI') || 'xterm' === getenv('TERM'); } if (\function_exists('stream_isatty')) { return @stream_isatty($this->stream); } if (\function_exists('posix_isatty')) { return @posix_isatty($this->stream); } $stat = @fstat($this->stream); // Check if formatted mode is S_IFCHR return $stat ? 0020000 === ($stat['mode'] & 0170000) : false; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Console\Output; use Symfony\Component\Console\Formatter\OutputFormatter; use Symfony\Component\Console\Formatter\OutputFormatterInterface; /** * NullOutput suppresses all output. * * $output = new NullOutput(); * * @author Fabien Potencier * @author Tobias Schultze */ class NullOutput implements OutputInterface { /** * {@inheritdoc} */ public function setFormatter(OutputFormatterInterface $formatter) { // do nothing } /** * {@inheritdoc} */ public function getFormatter() { // to comply with the interface we must return a OutputFormatterInterface return new OutputFormatter(); } /** * {@inheritdoc} */ public function setDecorated($decorated) { // do nothing } /** * {@inheritdoc} */ public function isDecorated() { return false; } /** * {@inheritdoc} */ public function setVerbosity($level) { // do nothing } /** * {@inheritdoc} */ public function getVerbosity() { return self::VERBOSITY_QUIET; } /** * {@inheritdoc} */ public function isQuiet() { return true; } /** * {@inheritdoc} */ public function isVerbose() { return false; } /** * {@inheritdoc} */ public function isVeryVerbose() { return false; } /** * {@inheritdoc} */ public function isDebug() { return false; } /** * {@inheritdoc} */ public function writeln($messages, $options = self::OUTPUT_NORMAL) { // do nothing } /** * {@inheritdoc} */ public function write($messages, $newline = false, $options = self::OUTPUT_NORMAL) { // do nothing } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Console; class Terminal { private static $width; private static $height; private static $stty; /** * Gets the terminal width. * * @return int */ public function getWidth() { $width = getenv('COLUMNS'); if (false !== $width) { return (int) trim($width); } if (null === self::$width) { self::initDimensions(); } return self::$width ?: 80; } /** * Gets the terminal height. * * @return int */ public function getHeight() { $height = getenv('LINES'); if (false !== $height) { return (int) trim($height); } if (null === self::$height) { self::initDimensions(); } return self::$height ?: 50; } /** * @internal * * @return bool */ public static function hasSttyAvailable() { if (null !== self::$stty) { return self::$stty; } // skip check if exec function is disabled if (!\function_exists('exec')) { return false; } exec('stty 2>&1', $output, $exitcode); return self::$stty = 0 === $exitcode; } private static function initDimensions() { if ('\\' === \DIRECTORY_SEPARATOR) { if (preg_match('/^(\d+)x(\d+)(?: \((\d+)x(\d+)\))?$/', trim(getenv('ANSICON')), $matches)) { // extract [w, H] from "wxh (WxH)" // or [w, h] from "wxh" self::$width = (int) $matches[1]; self::$height = isset($matches[4]) ? (int) $matches[4] : (int) $matches[2]; } elseif (!self::hasVt100Support() && self::hasSttyAvailable()) { // only use stty on Windows if the terminal does not support vt100 (e.g. Windows 7 + git-bash) // testing for stty in a Windows 10 vt100-enabled console will implicitly disable vt100 support on STDOUT self::initDimensionsUsingStty(); } elseif (null !== $dimensions = self::getConsoleMode()) { // extract [w, h] from "wxh" self::$width = (int) $dimensions[0]; self::$height = (int) $dimensions[1]; } } else { self::initDimensionsUsingStty(); } } /** * Returns whether STDOUT has vt100 support (some Windows 10+ configurations). */ private static function hasVt100Support() { return \function_exists('sapi_windows_vt100_support') && sapi_windows_vt100_support(fopen('php://stdout', 'w')); } /** * Initializes dimensions using the output of an stty columns line. */ private static function initDimensionsUsingStty() { if ($sttyString = self::getSttyColumns()) { if (preg_match('/rows.(\d+);.columns.(\d+);/i', $sttyString, $matches)) { // extract [w, h] from "rows h; columns w;" self::$width = (int) $matches[2]; self::$height = (int) $matches[1]; } elseif (preg_match('/;.(\d+).rows;.(\d+).columns/i', $sttyString, $matches)) { // extract [w, h] from "; h rows; w columns" self::$width = (int) $matches[2]; self::$height = (int) $matches[1]; } } } /** * Runs and parses mode CON if it's available, suppressing any error output. * * @return int[]|null An array composed of the width and the height or null if it could not be parsed */ private static function getConsoleMode() { $info = self::readFromProcess('mode CON'); if (null === $info || !preg_match('/--------+\r?\n.+?(\d+)\r?\n.+?(\d+)\r?\n/', $info, $matches)) { return null; } return [(int) $matches[2], (int) $matches[1]]; } /** * Runs and parses stty -a if it's available, suppressing any error output. * * @return string|null */ private static function getSttyColumns() { return self::readFromProcess('stty -a | grep columns'); } /** * @param string $command * * @return string|null */ private static function readFromProcess($command) { if (!\function_exists('proc_open')) { return null; } $descriptorspec = [ 1 => ['pipe', 'w'], 2 => ['pipe', 'w'], ]; $process = proc_open($command, $descriptorspec, $pipes, null, null, ['suppress_errors' => true]); if (!\is_resource($process)) { return null; } $info = stream_get_contents($pipes[1]); fclose($pipes[1]); fclose($pipes[2]); proc_close($process); return $info; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Console\Command; use Symfony\Component\Console\Helper\DescriptorHelper; use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; /** * HelpCommand displays the help for a given command. * * @author Fabien Potencier */ class HelpCommand extends Command { private $command; /** * {@inheritdoc} */ protected function configure() { $this->ignoreValidationErrors(); $this ->setName('help') ->setDefinition([ new InputArgument('command_name', InputArgument::OPTIONAL, 'The command name', 'help'), new InputOption('format', null, InputOption::VALUE_REQUIRED, 'The output format (txt, xml, json, or md)', 'txt'), new InputOption('raw', null, InputOption::VALUE_NONE, 'To output raw command help'), ]) ->setDescription('Displays help for a command') ->setHelp(<<<'EOF' The %command.name% command displays help for a given command: php %command.full_name% list You can also output the help in other formats by using the --format option: php %command.full_name% --format=xml list To display the list of available commands, please use the list command. EOF ) ; } public function setCommand(Command $command) { $this->command = $command; } /** * {@inheritdoc} */ protected function execute(InputInterface $input, OutputInterface $output) { if (null === $this->command) { $this->command = $this->getApplication()->find($input->getArgument('command_name')); } $helper = new DescriptorHelper(); $helper->describe($output, $this->command, [ 'format' => $input->getOption('format'), 'raw_text' => $input->getOption('raw'), ]); $this->command = null; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Console\Command; use Symfony\Component\Console\Application; use Symfony\Component\Console\Exception\ExceptionInterface; use Symfony\Component\Console\Exception\InvalidArgumentException; use Symfony\Component\Console\Exception\LogicException; use Symfony\Component\Console\Helper\HelperSet; use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputDefinition; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; /** * Base class for all commands. * * @author Fabien Potencier */ class Command { /** * @var string|null The default command name */ protected static $defaultName; private $application; private $name; private $processTitle; private $aliases = []; private $definition; private $hidden = false; private $help = ''; private $description = ''; private $ignoreValidationErrors = false; private $applicationDefinitionMerged = false; private $applicationDefinitionMergedWithArgs = false; private $code; private $synopsis = []; private $usages = []; private $helperSet; /** * @return string|null The default command name or null when no default name is set */ public static function getDefaultName() { $class = static::class; $r = new \ReflectionProperty($class, 'defaultName'); return $class === $r->class ? static::$defaultName : null; } /** * @param string|null $name The name of the command; passing null means it must be set in configure() * * @throws LogicException When the command name is empty */ public function __construct($name = null) { $this->definition = new InputDefinition(); if (null !== $name || null !== $name = static::getDefaultName()) { $this->setName($name); } $this->configure(); } /** * Ignores validation errors. * * This is mainly useful for the help command. */ public function ignoreValidationErrors() { $this->ignoreValidationErrors = true; } public function setApplication(Application $application = null) { $this->application = $application; if ($application) { $this->setHelperSet($application->getHelperSet()); } else { $this->helperSet = null; } } public function setHelperSet(HelperSet $helperSet) { $this->helperSet = $helperSet; } /** * Gets the helper set. * * @return HelperSet|null A HelperSet instance */ public function getHelperSet() { return $this->helperSet; } /** * Gets the application instance for this command. * * @return Application|null An Application instance */ public function getApplication() { return $this->application; } /** * Checks whether the command is enabled or not in the current environment. * * Override this to check for x or y and return false if the command can not * run properly under the current conditions. * * @return bool */ public function isEnabled() { return true; } /** * Configures the current command. */ protected function configure() { } /** * Executes the current command. * * This method is not abstract because you can use this class * as a concrete class. In this case, instead of defining the * execute() method, you set the code to execute by passing * a Closure to the setCode() method. * * @return int|null null or 0 if everything went fine, or an error code * * @throws LogicException When this abstract method is not implemented * * @see setCode() */ protected function execute(InputInterface $input, OutputInterface $output) { throw new LogicException('You must override the execute() method in the concrete command class.'); } /** * Interacts with the user. * * This method is executed before the InputDefinition is validated. * This means that this is the only place where the command can * interactively ask for values of missing required arguments. */ protected function interact(InputInterface $input, OutputInterface $output) { } /** * Initializes the command after the input has been bound and before the input * is validated. * * This is mainly useful when a lot of commands extends one main command * where some things need to be initialized based on the input arguments and options. * * @see InputInterface::bind() * @see InputInterface::validate() */ protected function initialize(InputInterface $input, OutputInterface $output) { } /** * Runs the command. * * The code to execute is either defined directly with the * setCode() method or by overriding the execute() method * in a sub-class. * * @return int The command exit code * * @throws \Exception When binding input fails. Bypass this by calling {@link ignoreValidationErrors()}. * * @see setCode() * @see execute() */ public function run(InputInterface $input, OutputInterface $output) { // force the creation of the synopsis before the merge with the app definition $this->getSynopsis(true); $this->getSynopsis(false); // add the application arguments and options $this->mergeApplicationDefinition(); // bind the input against the command specific arguments/options try { $input->bind($this->definition); } catch (ExceptionInterface $e) { if (!$this->ignoreValidationErrors) { throw $e; } } $this->initialize($input, $output); if (null !== $this->processTitle) { if (\function_exists('cli_set_process_title')) { if (!@cli_set_process_title($this->processTitle)) { if ('Darwin' === \PHP_OS) { $output->writeln('Running "cli_set_process_title" as an unprivileged user is not supported on MacOS.', OutputInterface::VERBOSITY_VERY_VERBOSE); } else { cli_set_process_title($this->processTitle); } } } elseif (\function_exists('setproctitle')) { setproctitle($this->processTitle); } elseif (OutputInterface::VERBOSITY_VERY_VERBOSE === $output->getVerbosity()) { $output->writeln('Install the proctitle PECL to be able to change the process title.'); } } if ($input->isInteractive()) { $this->interact($input, $output); } // The command name argument is often omitted when a command is executed directly with its run() method. // It would fail the validation if we didn't make sure the command argument is present, // since it's required by the application. if ($input->hasArgument('command') && null === $input->getArgument('command')) { $input->setArgument('command', $this->getName()); } $input->validate(); if ($this->code) { $statusCode = \call_user_func($this->code, $input, $output); } else { $statusCode = $this->execute($input, $output); } return is_numeric($statusCode) ? (int) $statusCode : 0; } /** * Sets the code to execute when running this command. * * If this method is used, it overrides the code defined * in the execute() method. * * @param callable $code A callable(InputInterface $input, OutputInterface $output) * * @return $this * * @throws InvalidArgumentException * * @see execute() */ public function setCode(callable $code) { if ($code instanceof \Closure) { $r = new \ReflectionFunction($code); if (null === $r->getClosureThis()) { if (\PHP_VERSION_ID < 70000) { // Bug in PHP5: https://bugs.php.net/64761 // This means that we cannot bind static closures and therefore we must // ignore any errors here. There is no way to test if the closure is // bindable. $code = @\Closure::bind($code, $this); } else { $code = \Closure::bind($code, $this); } } } $this->code = $code; return $this; } /** * Merges the application definition with the command definition. * * This method is not part of public API and should not be used directly. * * @param bool $mergeArgs Whether to merge or not the Application definition arguments to Command definition arguments */ public function mergeApplicationDefinition($mergeArgs = true) { if (null === $this->application || (true === $this->applicationDefinitionMerged && ($this->applicationDefinitionMergedWithArgs || !$mergeArgs))) { return; } $this->definition->addOptions($this->application->getDefinition()->getOptions()); $this->applicationDefinitionMerged = true; if ($mergeArgs) { $currentArguments = $this->definition->getArguments(); $this->definition->setArguments($this->application->getDefinition()->getArguments()); $this->definition->addArguments($currentArguments); $this->applicationDefinitionMergedWithArgs = true; } } /** * Sets an array of argument and option instances. * * @param array|InputDefinition $definition An array of argument and option instances or a definition instance * * @return $this */ public function setDefinition($definition) { if ($definition instanceof InputDefinition) { $this->definition = $definition; } else { $this->definition->setDefinition($definition); } $this->applicationDefinitionMerged = false; return $this; } /** * Gets the InputDefinition attached to this Command. * * @return InputDefinition An InputDefinition instance */ public function getDefinition() { if (null === $this->definition) { throw new LogicException(sprintf('Command class "%s" is not correctly initialized. You probably forgot to call the parent constructor.', static::class)); } return $this->definition; } /** * Gets the InputDefinition to be used to create representations of this Command. * * Can be overridden to provide the original command representation when it would otherwise * be changed by merging with the application InputDefinition. * * This method is not part of public API and should not be used directly. * * @return InputDefinition An InputDefinition instance */ public function getNativeDefinition() { return $this->getDefinition(); } /** * Adds an argument. * * @param string $name The argument name * @param int|null $mode The argument mode: InputArgument::REQUIRED or InputArgument::OPTIONAL * @param string $description A description text * @param string|string[]|null $default The default value (for InputArgument::OPTIONAL mode only) * * @throws InvalidArgumentException When argument mode is not valid * * @return $this */ public function addArgument($name, $mode = null, $description = '', $default = null) { $this->definition->addArgument(new InputArgument($name, $mode, $description, $default)); return $this; } /** * Adds an option. * * @param string $name The option name * @param string|array|null $shortcut The shortcuts, can be null, a string of shortcuts delimited by | or an array of shortcuts * @param int|null $mode The option mode: One of the InputOption::VALUE_* constants * @param string $description A description text * @param string|string[]|int|bool|null $default The default value (must be null for InputOption::VALUE_NONE) * * @throws InvalidArgumentException If option mode is invalid or incompatible * * @return $this */ public function addOption($name, $shortcut = null, $mode = null, $description = '', $default = null) { $this->definition->addOption(new InputOption($name, $shortcut, $mode, $description, $default)); return $this; } /** * Sets the name of the command. * * This method can set both the namespace and the name if * you separate them by a colon (:) * * $command->setName('foo:bar'); * * @param string $name The command name * * @return $this * * @throws InvalidArgumentException When the name is invalid */ public function setName($name) { $this->validateName($name); $this->name = $name; return $this; } /** * Sets the process title of the command. * * This feature should be used only when creating a long process command, * like a daemon. * * @param string $title The process title * * @return $this */ public function setProcessTitle($title) { $this->processTitle = $title; return $this; } /** * Returns the command name. * * @return string|null */ public function getName() { return $this->name; } /** * @param bool $hidden Whether or not the command should be hidden from the list of commands * * @return Command The current instance */ public function setHidden($hidden) { $this->hidden = (bool) $hidden; return $this; } /** * @return bool whether the command should be publicly shown or not */ public function isHidden() { return $this->hidden; } /** * Sets the description for the command. * * @param string $description The description for the command * * @return $this */ public function setDescription($description) { $this->description = $description; return $this; } /** * Returns the description for the command. * * @return string The description for the command */ public function getDescription() { return $this->description; } /** * Sets the help for the command. * * @param string $help The help for the command * * @return $this */ public function setHelp($help) { $this->help = $help; return $this; } /** * Returns the help for the command. * * @return string The help for the command */ public function getHelp() { return $this->help; } /** * Returns the processed help for the command replacing the %command.name% and * %command.full_name% patterns with the real values dynamically. * * @return string The processed help for the command */ public function getProcessedHelp() { $name = $this->name; $isSingleCommand = $this->application && $this->application->isSingleCommand(); $placeholders = [ '%command.name%', '%command.full_name%', ]; $replacements = [ $name, $isSingleCommand ? $_SERVER['PHP_SELF'] : $_SERVER['PHP_SELF'].' '.$name, ]; return str_replace($placeholders, $replacements, $this->getHelp() ?: $this->getDescription()); } /** * Sets the aliases for the command. * * @param string[] $aliases An array of aliases for the command * * @return $this * * @throws InvalidArgumentException When an alias is invalid */ public function setAliases($aliases) { if (!\is_array($aliases) && !$aliases instanceof \Traversable) { throw new InvalidArgumentException('$aliases must be an array or an instance of \Traversable.'); } foreach ($aliases as $alias) { $this->validateName($alias); } $this->aliases = $aliases; return $this; } /** * Returns the aliases for the command. * * @return array An array of aliases for the command */ public function getAliases() { return $this->aliases; } /** * Returns the synopsis for the command. * * @param bool $short Whether to show the short version of the synopsis (with options folded) or not * * @return string The synopsis */ public function getSynopsis($short = false) { $key = $short ? 'short' : 'long'; if (!isset($this->synopsis[$key])) { $this->synopsis[$key] = trim(sprintf('%s %s', $this->name, $this->definition->getSynopsis($short))); } return $this->synopsis[$key]; } /** * Add a command usage example. * * @param string $usage The usage, it'll be prefixed with the command name * * @return $this */ public function addUsage($usage) { if (0 !== strpos($usage, $this->name)) { $usage = sprintf('%s %s', $this->name, $usage); } $this->usages[] = $usage; return $this; } /** * Returns alternative usages of the command. * * @return array */ public function getUsages() { return $this->usages; } /** * Gets a helper instance by name. * * @param string $name The helper name * * @return mixed The helper value * * @throws LogicException if no HelperSet is defined * @throws InvalidArgumentException if the helper is not defined */ public function getHelper($name) { if (null === $this->helperSet) { throw new LogicException(sprintf('Cannot retrieve helper "%s" because there is no HelperSet defined. Did you forget to add your command to the application or to set the application on the command using the setApplication() method? You can also set the HelperSet directly using the setHelperSet() method.', $name)); } return $this->helperSet->get($name); } /** * Validates a command name. * * It must be non-empty and parts can optionally be separated by ":". * * @param string $name * * @throws InvalidArgumentException When the name is invalid */ private function validateName($name) { if (!preg_match('/^[^\:]++(\:[^\:]++)*$/', $name)) { throw new InvalidArgumentException(sprintf('Command name "%s" is invalid.', $name)); } } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Console\Command; use Symfony\Component\Console\Exception\LogicException; use Symfony\Component\Console\Exception\RuntimeException; use Symfony\Component\Lock\Factory; use Symfony\Component\Lock\Lock; use Symfony\Component\Lock\Store\FlockStore; use Symfony\Component\Lock\Store\SemaphoreStore; /** * Basic lock feature for commands. * * @author Geoffrey Brier */ trait LockableTrait { /** @var Lock */ private $lock; /** * Locks a command. * * @return bool */ private function lock($name = null, $blocking = false) { if (!class_exists(SemaphoreStore::class)) { throw new RuntimeException('To enable the locking feature you must install the symfony/lock component.'); } if (null !== $this->lock) { throw new LogicException('A lock is already in place.'); } if (SemaphoreStore::isSupported($blocking)) { $store = new SemaphoreStore(); } else { $store = new FlockStore(); } $this->lock = (new Factory($store))->createLock($name ?: $this->getName()); if (!$this->lock->acquire($blocking)) { $this->lock = null; return false; } return true; } /** * Releases the command lock if there is one. */ private function release() { if ($this->lock) { $this->lock->release(); $this->lock = null; } } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Console\Command; use Symfony\Component\Console\Helper\DescriptorHelper; use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputDefinition; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; /** * ListCommand displays the list of all available commands for the application. * * @author Fabien Potencier */ class ListCommand extends Command { /** * {@inheritdoc} */ protected function configure() { $this ->setName('list') ->setDefinition($this->createDefinition()) ->setDescription('Lists commands') ->setHelp(<<<'EOF' The %command.name% command lists all commands: php %command.full_name% You can also display the commands for a specific namespace: php %command.full_name% test You can also output the information in other formats by using the --format option: php %command.full_name% --format=xml It's also possible to get raw list of commands (useful for embedding command runner): php %command.full_name% --raw EOF ) ; } /** * {@inheritdoc} */ public function getNativeDefinition() { return $this->createDefinition(); } /** * {@inheritdoc} */ protected function execute(InputInterface $input, OutputInterface $output) { $helper = new DescriptorHelper(); $helper->describe($output, $this->getApplication(), [ 'format' => $input->getOption('format'), 'raw_text' => $input->getOption('raw'), 'namespace' => $input->getArgument('namespace'), ]); } /** * {@inheritdoc} */ private function createDefinition() { return new InputDefinition([ new InputArgument('namespace', InputArgument::OPTIONAL, 'The namespace name'), new InputOption('raw', null, InputOption::VALUE_NONE, 'To output raw command list'), new InputOption('format', null, InputOption::VALUE_REQUIRED, 'The output format (txt, xml, json, or md)', 'txt'), ]); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Console\Style; /** * Output style helpers. * * @author Kevin Bond */ interface StyleInterface { /** * Formats a command title. * * @param string $message */ public function title($message); /** * Formats a section title. * * @param string $message */ public function section($message); /** * Formats a list. */ public function listing(array $elements); /** * Formats informational text. * * @param string|array $message */ public function text($message); /** * Formats a success result bar. * * @param string|array $message */ public function success($message); /** * Formats an error result bar. * * @param string|array $message */ public function error($message); /** * Formats an warning result bar. * * @param string|array $message */ public function warning($message); /** * Formats a note admonition. * * @param string|array $message */ public function note($message); /** * Formats a caution admonition. * * @param string|array $message */ public function caution($message); /** * Formats a table. */ public function table(array $headers, array $rows); /** * Asks a question. * * @param string $question * @param string|null $default * @param callable|null $validator * * @return mixed */ public function ask($question, $default = null, $validator = null); /** * Asks a question with the user input hidden. * * @param string $question * @param callable|null $validator * * @return mixed */ public function askHidden($question, $validator = null); /** * Asks for confirmation. * * @param string $question * @param bool $default * * @return bool */ public function confirm($question, $default = true); /** * Asks a choice question. * * @param string $question * @param string|int|null $default * * @return mixed */ public function choice($question, array $choices, $default = null); /** * Add newline(s). * * @param int $count The number of newlines */ public function newLine($count = 1); /** * Starts the progress output. * * @param int $max Maximum steps (0 if unknown) */ public function progressStart($max = 0); /** * Advances the progress output X steps. * * @param int $step Number of steps to advance */ public function progressAdvance($step = 1); /** * Finishes the progress output. */ public function progressFinish(); } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Console\Style; use Symfony\Component\Console\Exception\RuntimeException; use Symfony\Component\Console\Formatter\OutputFormatter; use Symfony\Component\Console\Helper\Helper; use Symfony\Component\Console\Helper\ProgressBar; use Symfony\Component\Console\Helper\SymfonyQuestionHelper; use Symfony\Component\Console\Helper\Table; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\BufferedOutput; use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Console\Question\ChoiceQuestion; use Symfony\Component\Console\Question\ConfirmationQuestion; use Symfony\Component\Console\Question\Question; use Symfony\Component\Console\Terminal; /** * Output decorator helpers for the Symfony Style Guide. * * @author Kevin Bond */ class SymfonyStyle extends OutputStyle { const MAX_LINE_LENGTH = 120; private $input; private $questionHelper; private $progressBar; private $lineLength; private $bufferedOutput; public function __construct(InputInterface $input, OutputInterface $output) { $this->input = $input; $this->bufferedOutput = new BufferedOutput($output->getVerbosity(), false, clone $output->getFormatter()); // Windows cmd wraps lines as soon as the terminal width is reached, whether there are following chars or not. $width = (new Terminal())->getWidth() ?: self::MAX_LINE_LENGTH; $this->lineLength = min($width - (int) (\DIRECTORY_SEPARATOR === '\\'), self::MAX_LINE_LENGTH); parent::__construct($output); } /** * Formats a message as a block of text. * * @param string|array $messages The message to write in the block * @param string|null $type The block type (added in [] on first line) * @param string|null $style The style to apply to the whole block * @param string $prefix The prefix for the block * @param bool $padding Whether to add vertical padding * @param bool $escape Whether to escape the message */ public function block($messages, $type = null, $style = null, $prefix = ' ', $padding = false, $escape = true) { $messages = \is_array($messages) ? array_values($messages) : [$messages]; $this->autoPrependBlock(); $this->writeln($this->createBlock($messages, $type, $style, $prefix, $padding, $escape)); $this->newLine(); } /** * {@inheritdoc} */ public function title($message) { $this->autoPrependBlock(); $this->writeln([ sprintf('%s', OutputFormatter::escapeTrailingBackslash($message)), sprintf('%s', str_repeat('=', Helper::strlenWithoutDecoration($this->getFormatter(), $message))), ]); $this->newLine(); } /** * {@inheritdoc} */ public function section($message) { $this->autoPrependBlock(); $this->writeln([ sprintf('%s', OutputFormatter::escapeTrailingBackslash($message)), sprintf('%s', str_repeat('-', Helper::strlenWithoutDecoration($this->getFormatter(), $message))), ]); $this->newLine(); } /** * {@inheritdoc} */ public function listing(array $elements) { $this->autoPrependText(); $elements = array_map(function ($element) { return sprintf(' * %s', $element); }, $elements); $this->writeln($elements); $this->newLine(); } /** * {@inheritdoc} */ public function text($message) { $this->autoPrependText(); $messages = \is_array($message) ? array_values($message) : [$message]; foreach ($messages as $message) { $this->writeln(sprintf(' %s', $message)); } } /** * Formats a command comment. * * @param string|array $message */ public function comment($message) { $this->block($message, null, null, ' // ', false, false); } /** * {@inheritdoc} */ public function success($message) { $this->block($message, 'OK', 'fg=black;bg=green', ' ', true); } /** * {@inheritdoc} */ public function error($message) { $this->block($message, 'ERROR', 'fg=white;bg=red', ' ', true); } /** * {@inheritdoc} */ public function warning($message) { $this->block($message, 'WARNING', 'fg=white;bg=red', ' ', true); } /** * {@inheritdoc} */ public function note($message) { $this->block($message, 'NOTE', 'fg=yellow', ' ! '); } /** * {@inheritdoc} */ public function caution($message) { $this->block($message, 'CAUTION', 'fg=white;bg=red', ' ! ', true); } /** * {@inheritdoc} */ public function table(array $headers, array $rows) { $style = clone Table::getStyleDefinition('symfony-style-guide'); $style->setCellHeaderFormat('%s'); $table = new Table($this); $table->setHeaders($headers); $table->setRows($rows); $table->setStyle($style); $table->render(); $this->newLine(); } /** * {@inheritdoc} */ public function ask($question, $default = null, $validator = null) { $question = new Question($question, $default); $question->setValidator($validator); return $this->askQuestion($question); } /** * {@inheritdoc} */ public function askHidden($question, $validator = null) { $question = new Question($question); $question->setHidden(true); $question->setValidator($validator); return $this->askQuestion($question); } /** * {@inheritdoc} */ public function confirm($question, $default = true) { return $this->askQuestion(new ConfirmationQuestion($question, $default)); } /** * {@inheritdoc} */ public function choice($question, array $choices, $default = null) { if (null !== $default) { $values = array_flip($choices); $default = isset($values[$default]) ? $values[$default] : $default; } return $this->askQuestion(new ChoiceQuestion($question, $choices, $default)); } /** * {@inheritdoc} */ public function progressStart($max = 0) { $this->progressBar = $this->createProgressBar($max); $this->progressBar->start(); } /** * {@inheritdoc} */ public function progressAdvance($step = 1) { $this->getProgressBar()->advance($step); } /** * {@inheritdoc} */ public function progressFinish() { $this->getProgressBar()->finish(); $this->newLine(2); $this->progressBar = null; } /** * {@inheritdoc} */ public function createProgressBar($max = 0) { $progressBar = parent::createProgressBar($max); if ('\\' !== \DIRECTORY_SEPARATOR || 'Hyper' === getenv('TERM_PROGRAM')) { $progressBar->setEmptyBarCharacter('░'); // light shade character \u2591 $progressBar->setProgressCharacter(''); $progressBar->setBarCharacter('▓'); // dark shade character \u2593 } return $progressBar; } /** * @return mixed */ public function askQuestion(Question $question) { if ($this->input->isInteractive()) { $this->autoPrependBlock(); } if (!$this->questionHelper) { $this->questionHelper = new SymfonyQuestionHelper(); } $answer = $this->questionHelper->ask($this->input, $this, $question); if ($this->input->isInteractive()) { $this->newLine(); $this->bufferedOutput->write("\n"); } return $answer; } /** * {@inheritdoc} */ public function writeln($messages, $type = self::OUTPUT_NORMAL) { parent::writeln($messages, $type); $this->bufferedOutput->writeln($this->reduceBuffer($messages), $type); } /** * {@inheritdoc} */ public function write($messages, $newline = false, $type = self::OUTPUT_NORMAL) { parent::write($messages, $newline, $type); $this->bufferedOutput->write($this->reduceBuffer($messages), $newline, $type); } /** * {@inheritdoc} */ public function newLine($count = 1) { parent::newLine($count); $this->bufferedOutput->write(str_repeat("\n", $count)); } /** * Returns a new instance which makes use of stderr if available. * * @return self */ public function getErrorStyle() { return new self($this->input, $this->getErrorOutput()); } /** * @return ProgressBar */ private function getProgressBar() { if (!$this->progressBar) { throw new RuntimeException('The ProgressBar is not started.'); } return $this->progressBar; } private function autoPrependBlock() { $chars = substr(str_replace(\PHP_EOL, "\n", $this->bufferedOutput->fetch()), -2); if (!isset($chars[0])) { $this->newLine(); //empty history, so we should start with a new line. return; } //Prepend new line for each non LF chars (This means no blank line was output before) $this->newLine(2 - substr_count($chars, "\n")); } private function autoPrependText() { $fetched = $this->bufferedOutput->fetch(); //Prepend new line if last char isn't EOL: if ("\n" !== substr($fetched, -1)) { $this->newLine(); } } private function reduceBuffer($messages) { // We need to know if the two last chars are PHP_EOL // Preserve the last 4 chars inserted (PHP_EOL on windows is two chars) in the history buffer return array_map(function ($value) { return substr($value, -4); }, array_merge([$this->bufferedOutput->fetch()], (array) $messages)); } private function createBlock($messages, $type = null, $style = null, $prefix = ' ', $padding = false, $escape = false) { $indentLength = 0; $prefixLength = Helper::strlenWithoutDecoration($this->getFormatter(), $prefix); $lines = []; if (null !== $type) { $type = sprintf('[%s] ', $type); $indentLength = \strlen($type); $lineIndentation = str_repeat(' ', $indentLength); } // wrap and add newlines for each element foreach ($messages as $key => $message) { if ($escape) { $message = OutputFormatter::escape($message); } $lines = array_merge($lines, explode(\PHP_EOL, wordwrap($message, $this->lineLength - $prefixLength - $indentLength, \PHP_EOL, true))); if (\count($messages) > 1 && $key < \count($messages) - 1) { $lines[] = ''; } } $firstLineIndex = 0; if ($padding && $this->isDecorated()) { $firstLineIndex = 1; array_unshift($lines, ''); $lines[] = ''; } foreach ($lines as $i => &$line) { if (null !== $type) { $line = $firstLineIndex === $i ? $type.$line : $lineIndentation.$line; } $line = $prefix.$line; $line .= str_repeat(' ', $this->lineLength - Helper::strlenWithoutDecoration($this->getFormatter(), $line)); if ($style) { $line = sprintf('<%s>%s', $style, $line); } } return $lines; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Console\Style; use Symfony\Component\Console\Formatter\OutputFormatterInterface; use Symfony\Component\Console\Helper\ProgressBar; use Symfony\Component\Console\Output\ConsoleOutputInterface; use Symfony\Component\Console\Output\OutputInterface; /** * Decorates output to add console style guide helpers. * * @author Kevin Bond */ abstract class OutputStyle implements OutputInterface, StyleInterface { private $output; public function __construct(OutputInterface $output) { $this->output = $output; } /** * {@inheritdoc} */ public function newLine($count = 1) { $this->output->write(str_repeat(\PHP_EOL, $count)); } /** * @param int $max * * @return ProgressBar */ public function createProgressBar($max = 0) { return new ProgressBar($this->output, $max); } /** * {@inheritdoc} */ public function write($messages, $newline = false, $type = self::OUTPUT_NORMAL) { $this->output->write($messages, $newline, $type); } /** * {@inheritdoc} */ public function writeln($messages, $type = self::OUTPUT_NORMAL) { $this->output->writeln($messages, $type); } /** * {@inheritdoc} */ public function setVerbosity($level) { $this->output->setVerbosity($level); } /** * {@inheritdoc} */ public function getVerbosity() { return $this->output->getVerbosity(); } /** * {@inheritdoc} */ public function setDecorated($decorated) { $this->output->setDecorated($decorated); } /** * {@inheritdoc} */ public function isDecorated() { return $this->output->isDecorated(); } /** * {@inheritdoc} */ public function setFormatter(OutputFormatterInterface $formatter) { $this->output->setFormatter($formatter); } /** * {@inheritdoc} */ public function getFormatter() { return $this->output->getFormatter(); } /** * {@inheritdoc} */ public function isQuiet() { return $this->output->isQuiet(); } /** * {@inheritdoc} */ public function isVerbose() { return $this->output->isVerbose(); } /** * {@inheritdoc} */ public function isVeryVerbose() { return $this->output->isVeryVerbose(); } /** * {@inheritdoc} */ public function isDebug() { return $this->output->isDebug(); } protected function getErrorOutput() { if (!$this->output instanceof ConsoleOutputInterface) { return $this->output; } return $this->output->getErrorOutput(); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Console\Formatter; use Symfony\Component\Console\Exception\InvalidArgumentException; /** * Formatter style class for defining styles. * * @author Konstantin Kudryashov */ class OutputFormatterStyle implements OutputFormatterStyleInterface { private static $availableForegroundColors = [ 'black' => ['set' => 30, 'unset' => 39], 'red' => ['set' => 31, 'unset' => 39], 'green' => ['set' => 32, 'unset' => 39], 'yellow' => ['set' => 33, 'unset' => 39], 'blue' => ['set' => 34, 'unset' => 39], 'magenta' => ['set' => 35, 'unset' => 39], 'cyan' => ['set' => 36, 'unset' => 39], 'white' => ['set' => 37, 'unset' => 39], 'default' => ['set' => 39, 'unset' => 39], ]; private static $availableBackgroundColors = [ 'black' => ['set' => 40, 'unset' => 49], 'red' => ['set' => 41, 'unset' => 49], 'green' => ['set' => 42, 'unset' => 49], 'yellow' => ['set' => 43, 'unset' => 49], 'blue' => ['set' => 44, 'unset' => 49], 'magenta' => ['set' => 45, 'unset' => 49], 'cyan' => ['set' => 46, 'unset' => 49], 'white' => ['set' => 47, 'unset' => 49], 'default' => ['set' => 49, 'unset' => 49], ]; private static $availableOptions = [ 'bold' => ['set' => 1, 'unset' => 22], 'underscore' => ['set' => 4, 'unset' => 24], 'blink' => ['set' => 5, 'unset' => 25], 'reverse' => ['set' => 7, 'unset' => 27], 'conceal' => ['set' => 8, 'unset' => 28], ]; private $foreground; private $background; private $options = []; /** * Initializes output formatter style. * * @param string|null $foreground The style foreground color name * @param string|null $background The style background color name * @param array $options The style options */ public function __construct($foreground = null, $background = null, array $options = []) { if (null !== $foreground) { $this->setForeground($foreground); } if (null !== $background) { $this->setBackground($background); } if (\count($options)) { $this->setOptions($options); } } /** * {@inheritdoc} */ public function setForeground($color = null) { if (null === $color) { $this->foreground = null; return; } if (!isset(static::$availableForegroundColors[$color])) { throw new InvalidArgumentException(sprintf('Invalid foreground color specified: "%s". Expected one of (%s).', $color, implode(', ', array_keys(static::$availableForegroundColors)))); } $this->foreground = static::$availableForegroundColors[$color]; } /** * {@inheritdoc} */ public function setBackground($color = null) { if (null === $color) { $this->background = null; return; } if (!isset(static::$availableBackgroundColors[$color])) { throw new InvalidArgumentException(sprintf('Invalid background color specified: "%s". Expected one of (%s).', $color, implode(', ', array_keys(static::$availableBackgroundColors)))); } $this->background = static::$availableBackgroundColors[$color]; } /** * {@inheritdoc} */ public function setOption($option) { if (!isset(static::$availableOptions[$option])) { throw new InvalidArgumentException(sprintf('Invalid option specified: "%s". Expected one of (%s).', $option, implode(', ', array_keys(static::$availableOptions)))); } if (!\in_array(static::$availableOptions[$option], $this->options)) { $this->options[] = static::$availableOptions[$option]; } } /** * {@inheritdoc} */ public function unsetOption($option) { if (!isset(static::$availableOptions[$option])) { throw new InvalidArgumentException(sprintf('Invalid option specified: "%s". Expected one of (%s).', $option, implode(', ', array_keys(static::$availableOptions)))); } $pos = array_search(static::$availableOptions[$option], $this->options); if (false !== $pos) { unset($this->options[$pos]); } } /** * {@inheritdoc} */ public function setOptions(array $options) { $this->options = []; foreach ($options as $option) { $this->setOption($option); } } /** * {@inheritdoc} */ public function apply($text) { $setCodes = []; $unsetCodes = []; if (null !== $this->foreground) { $setCodes[] = $this->foreground['set']; $unsetCodes[] = $this->foreground['unset']; } if (null !== $this->background) { $setCodes[] = $this->background['set']; $unsetCodes[] = $this->background['unset']; } if (\count($this->options)) { foreach ($this->options as $option) { $setCodes[] = $option['set']; $unsetCodes[] = $option['unset']; } } if (0 === \count($setCodes)) { return $text; } return sprintf("\033[%sm%s\033[%sm", implode(';', $setCodes), $text, implode(';', $unsetCodes)); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Console\Formatter; /** * Formatter style interface for defining styles. * * @author Konstantin Kudryashov */ interface OutputFormatterStyleInterface { /** * Sets style foreground color. * * @param string|null $color The color name */ public function setForeground($color = null); /** * Sets style background color. * * @param string $color The color name */ public function setBackground($color = null); /** * Sets some specific style option. * * @param string $option The option name */ public function setOption($option); /** * Unsets some specific style option. * * @param string $option The option name */ public function unsetOption($option); /** * Sets multiple style options at once. */ public function setOptions(array $options); /** * Applies the style to a given text. * * @param string $text The text to style * * @return string */ public function apply($text); } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Console\Formatter; use Symfony\Component\Console\Exception\InvalidArgumentException; /** * Formatter class for console output. * * @author Konstantin Kudryashov */ class OutputFormatter implements OutputFormatterInterface { private $decorated; private $styles = []; private $styleStack; /** * Escapes "<" special char in given text. * * @param string $text Text to escape * * @return string Escaped text */ public static function escape($text) { $text = preg_replace('/([^\\\\]?) FormatterStyle" instances */ public function __construct($decorated = false, array $styles = []) { $this->decorated = (bool) $decorated; $this->setStyle('error', new OutputFormatterStyle('white', 'red')); $this->setStyle('info', new OutputFormatterStyle('green')); $this->setStyle('comment', new OutputFormatterStyle('yellow')); $this->setStyle('question', new OutputFormatterStyle('black', 'cyan')); foreach ($styles as $name => $style) { $this->setStyle($name, $style); } $this->styleStack = new OutputFormatterStyleStack(); } /** * {@inheritdoc} */ public function setDecorated($decorated) { $this->decorated = (bool) $decorated; } /** * {@inheritdoc} */ public function isDecorated() { return $this->decorated; } /** * {@inheritdoc} */ public function setStyle($name, OutputFormatterStyleInterface $style) { $this->styles[strtolower($name)] = $style; } /** * {@inheritdoc} */ public function hasStyle($name) { return isset($this->styles[strtolower($name)]); } /** * {@inheritdoc} */ public function getStyle($name) { if (!$this->hasStyle($name)) { throw new InvalidArgumentException(sprintf('Undefined style: "%s".', $name)); } return $this->styles[strtolower($name)]; } /** * {@inheritdoc} */ public function format($message) { $message = (string) $message; $offset = 0; $output = ''; $tagRegex = '[a-z][a-z0-9,_=;-]*+'; preg_match_all("#<(($tagRegex) | /($tagRegex)?)>#ix", $message, $matches, \PREG_OFFSET_CAPTURE); foreach ($matches[0] as $i => $match) { $pos = $match[1]; $text = $match[0]; if (0 != $pos && '\\' == $message[$pos - 1]) { continue; } // add the text up to the next tag $output .= $this->applyCurrentStyle(substr($message, $offset, $pos - $offset)); $offset = $pos + \strlen($text); // opening tag? if ($open = '/' != $text[1]) { $tag = $matches[1][$i][0]; } else { $tag = isset($matches[3][$i][0]) ? $matches[3][$i][0] : ''; } if (!$open && !$tag) { // $this->styleStack->pop(); } elseif (false === $style = $this->createStyleFromString($tag)) { $output .= $this->applyCurrentStyle($text); } elseif ($open) { $this->styleStack->push($style); } else { $this->styleStack->pop($style); } } $output .= $this->applyCurrentStyle(substr($message, $offset)); if (false !== strpos($output, "\0")) { return strtr($output, ["\0" => '\\', '\\<' => '<']); } return str_replace('\\<', '<', $output); } /** * @return OutputFormatterStyleStack */ public function getStyleStack() { return $this->styleStack; } /** * Tries to create new style instance from string. * * @param string $string * * @return OutputFormatterStyle|false false if string is not format string */ private function createStyleFromString($string) { if (isset($this->styles[$string])) { return $this->styles[$string]; } if (!preg_match_all('/([^=]+)=([^;]+)(;|$)/', $string, $matches, \PREG_SET_ORDER)) { return false; } $style = new OutputFormatterStyle(); foreach ($matches as $match) { array_shift($match); $match[0] = strtolower($match[0]); if ('fg' == $match[0]) { $style->setForeground(strtolower($match[1])); } elseif ('bg' == $match[0]) { $style->setBackground(strtolower($match[1])); } elseif ('options' === $match[0]) { preg_match_all('([^,;]+)', strtolower($match[1]), $options); $options = array_shift($options); foreach ($options as $option) { try { $style->setOption($option); } catch (\InvalidArgumentException $e) { @trigger_error(sprintf('Unknown style options are deprecated since Symfony 3.2 and will be removed in 4.0. Exception "%s".', $e->getMessage()), \E_USER_DEPRECATED); return false; } } } else { return false; } } return $style; } /** * Applies current style from stack to text, if must be applied. * * @param string $text Input text * * @return string Styled text */ private function applyCurrentStyle($text) { return $this->isDecorated() && \strlen($text) > 0 ? $this->styleStack->getCurrent()->apply($text) : $text; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Console\Formatter; use Symfony\Component\Console\Exception\InvalidArgumentException; /** * @author Jean-François Simon */ class OutputFormatterStyleStack { /** * @var OutputFormatterStyleInterface[] */ private $styles; private $emptyStyle; public function __construct(OutputFormatterStyleInterface $emptyStyle = null) { $this->emptyStyle = $emptyStyle ?: new OutputFormatterStyle(); $this->reset(); } /** * Resets stack (ie. empty internal arrays). */ public function reset() { $this->styles = []; } /** * Pushes a style in the stack. */ public function push(OutputFormatterStyleInterface $style) { $this->styles[] = $style; } /** * Pops a style from the stack. * * @return OutputFormatterStyleInterface * * @throws InvalidArgumentException When style tags incorrectly nested */ public function pop(OutputFormatterStyleInterface $style = null) { if (empty($this->styles)) { return $this->emptyStyle; } if (null === $style) { return array_pop($this->styles); } foreach (array_reverse($this->styles, true) as $index => $stackedStyle) { if ($style->apply('') === $stackedStyle->apply('')) { $this->styles = \array_slice($this->styles, 0, $index); return $stackedStyle; } } throw new InvalidArgumentException('Incorrectly nested style tag found.'); } /** * Computes current style with stacks top codes. * * @return OutputFormatterStyle */ public function getCurrent() { if (empty($this->styles)) { return $this->emptyStyle; } return $this->styles[\count($this->styles) - 1]; } /** * @return $this */ public function setEmptyStyle(OutputFormatterStyleInterface $emptyStyle) { $this->emptyStyle = $emptyStyle; return $this; } /** * @return OutputFormatterStyleInterface */ public function getEmptyStyle() { return $this->emptyStyle; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Console\Formatter; /** * Formatter interface for console output. * * @author Konstantin Kudryashov */ interface OutputFormatterInterface { /** * Sets the decorated flag. * * @param bool $decorated Whether to decorate the messages or not */ public function setDecorated($decorated); /** * Gets the decorated flag. * * @return bool true if the output will decorate messages, false otherwise */ public function isDecorated(); /** * Sets a new style. * * @param string $name The style name * @param OutputFormatterStyleInterface $style The style instance */ public function setStyle($name, OutputFormatterStyleInterface $style); /** * Checks if output formatter has style with specified name. * * @param string $name * * @return bool */ public function hasStyle($name); /** * Gets style options from style with specified name. * * @param string $name * * @return OutputFormatterStyleInterface * * @throws \InvalidArgumentException When style isn't defined */ public function getStyle($name); /** * Formats a message according to the given styles. * * @param string $message The message to style * * @return string The styled message */ public function format($message); } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Console\Event; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; /** * Allows to manipulate the exit code of a command after its execution. * * @author Francesco Levorato */ class ConsoleTerminateEvent extends ConsoleEvent { /** * The exit code of the command. * * @var int */ private $exitCode; public function __construct(Command $command, InputInterface $input, OutputInterface $output, $exitCode) { parent::__construct($command, $input, $output); $this->setExitCode($exitCode); } /** * Sets the exit code. * * @param int $exitCode The command exit code */ public function setExitCode($exitCode) { $this->exitCode = (int) $exitCode; } /** * Gets the exit code. * * @return int The command exit code */ public function getExitCode() { return $this->exitCode; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Console\Event; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\EventDispatcher\Event; /** * Allows to inspect input and output of a command. * * @author Francesco Levorato */ class ConsoleEvent extends Event { protected $command; private $input; private $output; public function __construct(Command $command = null, InputInterface $input, OutputInterface $output) { $this->command = $command; $this->input = $input; $this->output = $output; } /** * Gets the command that is executed. * * @return Command|null A Command instance */ public function getCommand() { return $this->command; } /** * Gets the input instance. * * @return InputInterface An InputInterface instance */ public function getInput() { return $this->input; } /** * Gets the output instance. * * @return OutputInterface An OutputInterface instance */ public function getOutput() { return $this->output; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Console\Event; @trigger_error(sprintf('The "%s" class is deprecated since Symfony 3.3 and will be removed in 4.0. Use the ConsoleErrorEvent instead.', ConsoleExceptionEvent::class), \E_USER_DEPRECATED); use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; /** * Allows to handle exception thrown in a command. * * @author Fabien Potencier * * @deprecated since version 3.3, to be removed in 4.0. Use ConsoleErrorEvent instead. */ class ConsoleExceptionEvent extends ConsoleEvent { private $exception; private $exitCode; public function __construct(Command $command, InputInterface $input, OutputInterface $output, \Exception $exception, $exitCode) { parent::__construct($command, $input, $output); $this->setException($exception); $this->exitCode = (int) $exitCode; } /** * Returns the thrown exception. * * @return \Exception The thrown exception */ public function getException() { return $this->exception; } /** * Replaces the thrown exception. * * This exception will be thrown if no response is set in the event. * * @param \Exception $exception The thrown exception */ public function setException(\Exception $exception) { $this->exception = $exception; } /** * Gets the exit code. * * @return int The command exit code */ public function getExitCode() { return $this->exitCode; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Console\Event; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Exception\InvalidArgumentException; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; /** * Allows to handle throwables thrown while running a command. * * @author Wouter de Jong */ final class ConsoleErrorEvent extends ConsoleEvent { private $error; private $exitCode; public function __construct(InputInterface $input, OutputInterface $output, $error, Command $command = null) { parent::__construct($command, $input, $output); $this->setError($error); } /** * Returns the thrown error/exception. * * @return \Throwable */ public function getError() { return $this->error; } /** * Replaces the thrown error/exception. * * @param \Throwable $error */ public function setError($error) { if (!$error instanceof \Throwable && !$error instanceof \Exception) { throw new InvalidArgumentException(sprintf('The error passed to ConsoleErrorEvent must be an instance of \Throwable or \Exception, "%s" was passed instead.', \is_object($error) ? \get_class($error) : \gettype($error))); } $this->error = $error; } /** * Sets the exit code. * * @param int $exitCode The command exit code */ public function setExitCode($exitCode) { $this->exitCode = (int) $exitCode; $r = new \ReflectionProperty($this->error, 'code'); $r->setAccessible(true); $r->setValue($this->error, $this->exitCode); } /** * Gets the exit code. * * @return int The command exit code */ public function getExitCode() { return null !== $this->exitCode ? $this->exitCode : (\is_int($this->error->getCode()) && 0 !== $this->error->getCode() ? $this->error->getCode() : 1); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Console\Event; /** * Allows to do things before the command is executed, like skipping the command or changing the input. * * @author Fabien Potencier */ class ConsoleCommandEvent extends ConsoleEvent { /** * The return code for skipped commands, this will also be passed into the terminate event. */ const RETURN_CODE_DISABLED = 113; /** * Indicates if the command should be run or skipped. */ private $commandShouldRun = true; /** * Disables the command, so it won't be run. * * @return bool */ public function disableCommand() { return $this->commandShouldRun = false; } /** * Enables the command. * * @return bool */ public function enableCommand() { return $this->commandShouldRun = true; } /** * Returns true if the command is runnable, false otherwise. * * @return bool */ public function commandShouldRun() { return $this->commandShouldRun; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Console; /** * Contains all events dispatched by an Application. * * @author Francesco Levorato */ final class ConsoleEvents { /** * The COMMAND event allows you to attach listeners before any command is * executed by the console. It also allows you to modify the command, input and output * before they are handled to the command. * * @Event("Symfony\Component\Console\Event\ConsoleCommandEvent") */ const COMMAND = 'console.command'; /** * The TERMINATE event allows you to attach listeners after a command is * executed by the console. * * @Event("Symfony\Component\Console\Event\ConsoleTerminateEvent") */ const TERMINATE = 'console.terminate'; /** * The EXCEPTION event occurs when an uncaught exception appears * while executing Command#run(). * * This event allows you to deal with the exception or * to modify the thrown exception. * * @Event("Symfony\Component\Console\Event\ConsoleExceptionEvent") * * @deprecated The console.exception event is deprecated since version 3.3 and will be removed in 4.0. Use the console.error event instead. */ const EXCEPTION = 'console.exception'; /** * The ERROR event occurs when an uncaught exception or error appears. * * This event allows you to deal with the exception/error or * to modify the thrown exception. * * @Event("Symfony\Component\Console\Event\ConsoleErrorEvent") */ const ERROR = 'console.error'; } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Finder; /** * Extends \SplFileInfo to support relative paths. * * @author Fabien Potencier */ class SplFileInfo extends \SplFileInfo { private $relativePath; private $relativePathname; /** * @param string $file The file name * @param string $relativePath The relative path * @param string $relativePathname The relative path name */ public function __construct($file, $relativePath, $relativePathname) { parent::__construct($file); $this->relativePath = $relativePath; $this->relativePathname = $relativePathname; } /** * Returns the relative path. * * This path does not contain the file name. * * @return string the relative path */ public function getRelativePath() { return $this->relativePath; } /** * Returns the relative path name. * * This path contains the file name. * * @return string the relative path name */ public function getRelativePathname() { return $this->relativePathname; } /** * Returns the contents of the file. * * @return string the contents of the file * * @throws \RuntimeException */ public function getContents() { set_error_handler(function ($type, $msg) use (&$error) { $error = $msg; }); $content = file_get_contents($this->getPathname()); restore_error_handler(); if (false === $content) { throw new \RuntimeException($error); } return $content; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Finder\Exception; /** * @author Jean-François Simon * * @deprecated since 3.3, to be removed in 4.0. */ interface ExceptionInterface { /** * @return \Symfony\Component\Finder\Adapter\AdapterInterface */ public function getAdapter(); } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Finder\Exception; /** * @author Jean-François Simon */ class AccessDeniedException extends \UnexpectedValueException { } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Finder\Comparator; /** * DateCompare compiles date comparisons. * * @author Fabien Potencier */ class DateComparator extends Comparator { /** * @param string $test A comparison string * * @throws \InvalidArgumentException If the test is not understood */ public function __construct($test) { if (!preg_match('#^\s*(==|!=|[<>]=?|after|since|before|until)?\s*(.+?)\s*$#i', $test, $matches)) { throw new \InvalidArgumentException(sprintf('Don\'t understand "%s" as a date test.', $test)); } try { $date = new \DateTime($matches[2]); $target = $date->format('U'); } catch (\Exception $e) { throw new \InvalidArgumentException(sprintf('"%s" is not a valid date.', $matches[2])); } $operator = isset($matches[1]) ? $matches[1] : '=='; if ('since' === $operator || 'after' === $operator) { $operator = '>'; } if ('until' === $operator || 'before' === $operator) { $operator = '<'; } $this->setOperator($operator); $this->setTarget($target); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Finder\Comparator; /** * Comparator. * * @author Fabien Potencier */ class Comparator { private $target; private $operator = '=='; /** * Gets the target value. * * @return string The target value */ public function getTarget() { return $this->target; } /** * Sets the target value. * * @param string $target The target value */ public function setTarget($target) { $this->target = $target; } /** * Gets the comparison operator. * * @return string The operator */ public function getOperator() { return $this->operator; } /** * Sets the comparison operator. * * @param string $operator A valid operator * * @throws \InvalidArgumentException */ public function setOperator($operator) { if (!$operator) { $operator = '=='; } if (!\in_array($operator, ['>', '<', '>=', '<=', '==', '!='])) { throw new \InvalidArgumentException(sprintf('Invalid operator "%s".', $operator)); } $this->operator = $operator; } /** * Tests against the target. * * @param mixed $test A test value * * @return bool */ public function test($test) { switch ($this->operator) { case '>': return $test > $this->target; case '>=': return $test >= $this->target; case '<': return $test < $this->target; case '<=': return $test <= $this->target; case '!=': return $test != $this->target; } return $test == $this->target; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Finder\Comparator; /** * NumberComparator compiles a simple comparison to an anonymous * subroutine, which you can call with a value to be tested again. * * Now this would be very pointless, if NumberCompare didn't understand * magnitudes. * * The target value may use magnitudes of kilobytes (k, ki), * megabytes (m, mi), or gigabytes (g, gi). Those suffixed * with an i use the appropriate 2**n version in accordance with the * IEC standard: http://physics.nist.gov/cuu/Units/binary.html * * Based on the Perl Number::Compare module. * * @author Fabien Potencier PHP port * @author Richard Clamp Perl version * @copyright 2004-2005 Fabien Potencier * @copyright 2002 Richard Clamp * * @see http://physics.nist.gov/cuu/Units/binary.html */ class NumberComparator extends Comparator { /** * @param string|int $test A comparison string or an integer * * @throws \InvalidArgumentException If the test is not understood */ public function __construct($test) { if (!preg_match('#^\s*(==|!=|[<>]=?)?\s*([0-9\.]+)\s*([kmg]i?)?\s*$#i', $test, $matches)) { throw new \InvalidArgumentException(sprintf('Don\'t understand "%s" as a number test.', $test)); } $target = $matches[2]; if (!is_numeric($target)) { throw new \InvalidArgumentException(sprintf('Invalid number "%s".', $target)); } if (isset($matches[3])) { // magnitude switch (strtolower($matches[3])) { case 'k': $target *= 1000; break; case 'ki': $target *= 1024; break; case 'm': $target *= 1000000; break; case 'mi': $target *= 1024 * 1024; break; case 'g': $target *= 1000000000; break; case 'gi': $target *= 1024 * 1024 * 1024; break; } } $this->setTarget($target); $this->setOperator(isset($matches[1]) ? $matches[1] : '=='); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Finder\Iterator; use Symfony\Component\Finder\Comparator\DateComparator; /** * DateRangeFilterIterator filters out files that are not in the given date range (last modified dates). * * @author Fabien Potencier */ class DateRangeFilterIterator extends FilterIterator { private $comparators = []; /** * @param \Iterator $iterator The Iterator to filter * @param DateComparator[] $comparators An array of DateComparator instances */ public function __construct(\Iterator $iterator, array $comparators) { $this->comparators = $comparators; parent::__construct($iterator); } /** * Filters the iterator values. * * @return bool true if the value should be kept, false otherwise */ public function accept() { $fileinfo = $this->current(); if (!file_exists($fileinfo->getPathname())) { return false; } $filedate = $fileinfo->getMTime(); foreach ($this->comparators as $compare) { if (!$compare->test($filedate)) { return false; } } return true; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Finder\Iterator; use Symfony\Component\Finder\Exception\AccessDeniedException; use Symfony\Component\Finder\SplFileInfo; /** * Extends the \RecursiveDirectoryIterator to support relative paths. * * @author Victor Berchet */ class RecursiveDirectoryIterator extends \RecursiveDirectoryIterator { /** * @var bool */ private $ignoreUnreadableDirs; /** * @var bool */ private $rewindable; // these 3 properties take part of the performance optimization to avoid redoing the same work in all iterations private $rootPath; private $subPath; private $directorySeparator = '/'; /** * @param string $path * @param int $flags * @param bool $ignoreUnreadableDirs * * @throws \RuntimeException */ public function __construct($path, $flags, $ignoreUnreadableDirs = false) { if ($flags & (self::CURRENT_AS_PATHNAME | self::CURRENT_AS_SELF)) { throw new \RuntimeException('This iterator only support returning current as fileinfo.'); } parent::__construct($path, $flags); $this->ignoreUnreadableDirs = $ignoreUnreadableDirs; $this->rootPath = $path; if ('/' !== \DIRECTORY_SEPARATOR && !($flags & self::UNIX_PATHS)) { $this->directorySeparator = \DIRECTORY_SEPARATOR; } } /** * Return an instance of SplFileInfo with support for relative paths. * * @return SplFileInfo File information */ public function current() { // the logic here avoids redoing the same work in all iterations if (null === $subPathname = $this->subPath) { $subPathname = $this->subPath = (string) $this->getSubPath(); } if ('' !== $subPathname) { $subPathname .= $this->directorySeparator; } $subPathname .= $this->getFilename(); if ('/' !== $basePath = $this->rootPath) { $basePath .= $this->directorySeparator; } return new SplFileInfo($basePath.$subPathname, $this->subPath, $subPathname); } /** * @return \RecursiveIterator * * @throws AccessDeniedException */ public function getChildren() { try { $children = parent::getChildren(); if ($children instanceof self) { // parent method will call the constructor with default arguments, so unreadable dirs won't be ignored anymore $children->ignoreUnreadableDirs = $this->ignoreUnreadableDirs; // performance optimization to avoid redoing the same work in all children $children->rewindable = &$this->rewindable; $children->rootPath = $this->rootPath; } return $children; } catch (\UnexpectedValueException $e) { if ($this->ignoreUnreadableDirs) { // If directory is unreadable and finder is set to ignore it, a fake empty content is returned. return new \RecursiveArrayIterator([]); } else { throw new AccessDeniedException($e->getMessage(), $e->getCode(), $e); } } } /** * Do nothing for non rewindable stream. */ public function rewind() { if (false === $this->isRewindable()) { return; } // @see https://bugs.php.net/68557 if (\PHP_VERSION_ID < 50523 || \PHP_VERSION_ID >= 50600 && \PHP_VERSION_ID < 50607) { parent::next(); } parent::rewind(); } /** * Checks if the stream is rewindable. * * @return bool true when the stream is rewindable, false otherwise */ public function isRewindable() { if (null !== $this->rewindable) { return $this->rewindable; } // workaround for an HHVM bug, should be removed when https://github.com/facebook/hhvm/issues/7281 is fixed if ('' === $this->getPath()) { return $this->rewindable = false; } if (false !== $stream = @opendir($this->getPath())) { $infos = stream_get_meta_data($stream); closedir($stream); if ($infos['seekable']) { return $this->rewindable = true; } } return $this->rewindable = false; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Finder\Iterator; use Symfony\Component\Finder\Glob; /** * FilenameFilterIterator filters files by patterns (a regexp, a glob, or a string). * * @author Fabien Potencier */ class FilenameFilterIterator extends MultiplePcreFilterIterator { /** * Filters the iterator values. * * @return bool true if the value should be kept, false otherwise */ public function accept() { return $this->isAccepted($this->current()->getFilename()); } /** * Converts glob to regexp. * * PCRE patterns are left unchanged. * Glob strings are transformed with Glob::toRegex(). * * @param string $str Pattern: glob or regexp * * @return string regexp corresponding to a given glob or regexp */ protected function toRegex($str) { return $this->isRegex($str) ? $str : Glob::toRegex($str); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Finder\Iterator; use Symfony\Component\Finder\Comparator\NumberComparator; /** * SizeRangeFilterIterator filters out files that are not in the given size range. * * @author Fabien Potencier */ class SizeRangeFilterIterator extends FilterIterator { private $comparators = []; /** * @param \Iterator $iterator The Iterator to filter * @param NumberComparator[] $comparators An array of NumberComparator instances */ public function __construct(\Iterator $iterator, array $comparators) { $this->comparators = $comparators; parent::__construct($iterator); } /** * Filters the iterator values. * * @return bool true if the value should be kept, false otherwise */ public function accept() { $fileinfo = $this->current(); if (!$fileinfo->isFile()) { return true; } $filesize = $fileinfo->getSize(); foreach ($this->comparators as $compare) { if (!$compare->test($filesize)) { return false; } } return true; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Finder\Iterator; /** * CustomFilterIterator filters files by applying anonymous functions. * * The anonymous function receives a \SplFileInfo and must return false * to remove files. * * @author Fabien Potencier */ class CustomFilterIterator extends FilterIterator { private $filters = []; /** * @param \Iterator $iterator The Iterator to filter * @param callable[] $filters An array of PHP callbacks * * @throws \InvalidArgumentException */ public function __construct(\Iterator $iterator, array $filters) { foreach ($filters as $filter) { if (!\is_callable($filter)) { throw new \InvalidArgumentException('Invalid PHP callback.'); } } $this->filters = $filters; parent::__construct($iterator); } /** * Filters the iterator values. * * @return bool true if the value should be kept, false otherwise */ public function accept() { $fileinfo = $this->current(); foreach ($this->filters as $filter) { if (false === \call_user_func($filter, $fileinfo)) { return false; } } return true; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Finder\Iterator; /** * SortableIterator applies a sort on a given Iterator. * * @author Fabien Potencier */ class SortableIterator implements \IteratorAggregate { const SORT_BY_NAME = 1; const SORT_BY_TYPE = 2; const SORT_BY_ACCESSED_TIME = 3; const SORT_BY_CHANGED_TIME = 4; const SORT_BY_MODIFIED_TIME = 5; private $iterator; private $sort; /** * @param \Traversable $iterator The Iterator to filter * @param int|callable $sort The sort type (SORT_BY_NAME, SORT_BY_TYPE, or a PHP callback) * * @throws \InvalidArgumentException */ public function __construct(\Traversable $iterator, $sort) { $this->iterator = $iterator; if (self::SORT_BY_NAME === $sort) { $this->sort = static function ($a, $b) { return strcmp($a->getRealPath() ?: $a->getPathname(), $b->getRealPath() ?: $b->getPathname()); }; } elseif (self::SORT_BY_TYPE === $sort) { $this->sort = static function ($a, $b) { if ($a->isDir() && $b->isFile()) { return -1; } elseif ($a->isFile() && $b->isDir()) { return 1; } return strcmp($a->getRealPath() ?: $a->getPathname(), $b->getRealPath() ?: $b->getPathname()); }; } elseif (self::SORT_BY_ACCESSED_TIME === $sort) { $this->sort = static function ($a, $b) { return $a->getATime() - $b->getATime(); }; } elseif (self::SORT_BY_CHANGED_TIME === $sort) { $this->sort = static function ($a, $b) { return $a->getCTime() - $b->getCTime(); }; } elseif (self::SORT_BY_MODIFIED_TIME === $sort) { $this->sort = static function ($a, $b) { return $a->getMTime() - $b->getMTime(); }; } elseif (\is_callable($sort)) { $this->sort = $sort; } else { throw new \InvalidArgumentException('The SortableIterator takes a PHP callable or a valid built-in sort algorithm as an argument.'); } } public function getIterator() { $array = iterator_to_array($this->iterator, true); uasort($array, $this->sort); return new \ArrayIterator($array); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Finder\Iterator; /** * FilecontentFilterIterator filters files by their contents using patterns (regexps or strings). * * @author Fabien Potencier * @author Włodzimierz Gajda */ class FilecontentFilterIterator extends MultiplePcreFilterIterator { /** * Filters the iterator values. * * @return bool true if the value should be kept, false otherwise */ public function accept() { if (!$this->matchRegexps && !$this->noMatchRegexps) { return true; } $fileinfo = $this->current(); if ($fileinfo->isDir() || !$fileinfo->isReadable()) { return false; } $content = $fileinfo->getContents(); if (!$content) { return false; } return $this->isAccepted($content); } /** * Converts string to regexp if necessary. * * @param string $str Pattern: string or regexp * * @return string regexp corresponding to a given string or regexp */ protected function toRegex($str) { return $this->isRegex($str) ? $str : '/'.preg_quote($str, '/').'/'; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Finder\Iterator; /** * This iterator just overrides the rewind method in order to correct a PHP bug, * which existed before version 5.5.23/5.6.7. * * @see https://bugs.php.net/68557 * * @author Alex Bogomazov * * @deprecated since 3.4, to be removed in 4.0. */ abstract class FilterIterator extends \FilterIterator { /** * This is a workaround for the problem with \FilterIterator leaving inner \FilesystemIterator in wrong state after * rewind in some cases. * * @see FilterIterator::rewind() */ public function rewind() { if (\PHP_VERSION_ID > 50607 || (\PHP_VERSION_ID > 50523 && \PHP_VERSION_ID < 50600)) { parent::rewind(); return; } $iterator = $this; while ($iterator instanceof \OuterIterator) { $innerIterator = $iterator->getInnerIterator(); if ($innerIterator instanceof RecursiveDirectoryIterator) { // this condition is necessary for iterators to work properly with non-local filesystems like ftp if ($innerIterator->isRewindable()) { $innerIterator->next(); $innerIterator->rewind(); } } elseif ($innerIterator instanceof \FilesystemIterator) { $innerIterator->next(); $innerIterator->rewind(); } $iterator = $innerIterator; } parent::rewind(); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Finder\Iterator; /** * ExcludeDirectoryFilterIterator filters out directories. * * @author Fabien Potencier */ class ExcludeDirectoryFilterIterator extends FilterIterator implements \RecursiveIterator { private $iterator; private $isRecursive; private $excludedDirs = []; private $excludedPattern; /** * @param \Iterator $iterator The Iterator to filter * @param string[] $directories An array of directories to exclude */ public function __construct(\Iterator $iterator, array $directories) { $this->iterator = $iterator; $this->isRecursive = $iterator instanceof \RecursiveIterator; $patterns = []; foreach ($directories as $directory) { $directory = rtrim($directory, '/'); if (!$this->isRecursive || false !== strpos($directory, '/')) { $patterns[] = preg_quote($directory, '#'); } else { $this->excludedDirs[$directory] = true; } } if ($patterns) { $this->excludedPattern = '#(?:^|/)(?:'.implode('|', $patterns).')(?:/|$)#'; } parent::__construct($iterator); } /** * Filters the iterator values. * * @return bool True if the value should be kept, false otherwise */ public function accept() { if ($this->isRecursive && isset($this->excludedDirs[$this->getFilename()]) && $this->isDir()) { return false; } if ($this->excludedPattern) { $path = $this->isDir() ? $this->current()->getRelativePathname() : $this->current()->getRelativePath(); $path = str_replace('\\', '/', $path); return !preg_match($this->excludedPattern, $path); } return true; } public function hasChildren() { return $this->isRecursive && $this->iterator->hasChildren(); } public function getChildren() { $children = new self($this->iterator->getChildren(), []); $children->excludedDirs = $this->excludedDirs; $children->excludedPattern = $this->excludedPattern; return $children; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Finder\Iterator; /** * MultiplePcreFilterIterator filters files using patterns (regexps, globs or strings). * * @author Fabien Potencier */ abstract class MultiplePcreFilterIterator extends FilterIterator { protected $matchRegexps = []; protected $noMatchRegexps = []; /** * @param \Iterator $iterator The Iterator to filter * @param array $matchPatterns An array of patterns that need to match * @param array $noMatchPatterns An array of patterns that need to not match */ public function __construct(\Iterator $iterator, array $matchPatterns, array $noMatchPatterns) { foreach ($matchPatterns as $pattern) { $this->matchRegexps[] = $this->toRegex($pattern); } foreach ($noMatchPatterns as $pattern) { $this->noMatchRegexps[] = $this->toRegex($pattern); } parent::__construct($iterator); } /** * Checks whether the string is accepted by the regex filters. * * If there is no regexps defined in the class, this method will accept the string. * Such case can be handled by child classes before calling the method if they want to * apply a different behavior. * * @param string $string The string to be matched against filters * * @return bool */ protected function isAccepted($string) { // should at least not match one rule to exclude foreach ($this->noMatchRegexps as $regex) { if (preg_match($regex, $string)) { return false; } } // should at least match one rule if ($this->matchRegexps) { foreach ($this->matchRegexps as $regex) { if (preg_match($regex, $string)) { return true; } } return false; } // If there is no match rules, the file is accepted return true; } /** * Checks whether the string is a regex. * * @param string $str * * @return bool Whether the given string is a regex */ protected function isRegex($str) { if (preg_match('/^(.{3,}?)[imsxuADU]*$/', $str, $m)) { $start = substr($m[1], 0, 1); $end = substr($m[1], -1); if ($start === $end) { return !preg_match('/[*?[:alnum:] \\\\]/', $start); } foreach ([['{', '}'], ['(', ')'], ['[', ']'], ['<', '>']] as $delimiters) { if ($start === $delimiters[0] && $end === $delimiters[1]) { return true; } } } return false; } /** * Converts string into regexp. * * @param string $str Pattern * * @return string regexp corresponding to a given string */ abstract protected function toRegex($str); } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Finder\Iterator; /** * PathFilterIterator filters files by path patterns (e.g. some/special/dir). * * @author Fabien Potencier * @author Włodzimierz Gajda */ class PathFilterIterator extends MultiplePcreFilterIterator { /** * Filters the iterator values. * * @return bool true if the value should be kept, false otherwise */ public function accept() { $filename = $this->current()->getRelativePathname(); if ('\\' === \DIRECTORY_SEPARATOR) { $filename = str_replace('\\', '/', $filename); } return $this->isAccepted($filename); } /** * Converts strings to regexp. * * PCRE patterns are left unchanged. * * Default conversion: * 'lorem/ipsum/dolor' ==> 'lorem\/ipsum\/dolor/' * * Use only / as directory separator (on Windows also). * * @param string $str Pattern: regexp or dirname * * @return string regexp corresponding to a given string or regexp */ protected function toRegex($str) { return $this->isRegex($str) ? $str : '/'.preg_quote($str, '/').'/'; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Finder\Iterator; /** * DepthRangeFilterIterator limits the directory depth. * * @author Fabien Potencier */ class DepthRangeFilterIterator extends FilterIterator { private $minDepth = 0; /** * @param \RecursiveIteratorIterator $iterator The Iterator to filter * @param int $minDepth The min depth * @param int $maxDepth The max depth */ public function __construct(\RecursiveIteratorIterator $iterator, $minDepth = 0, $maxDepth = \PHP_INT_MAX) { $this->minDepth = $minDepth; $iterator->setMaxDepth(\PHP_INT_MAX === $maxDepth ? -1 : $maxDepth); parent::__construct($iterator); } /** * Filters the iterator values. * * @return bool true if the value should be kept, false otherwise */ public function accept() { return $this->getInnerIterator()->getDepth() >= $this->minDepth; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Finder\Iterator; /** * FileTypeFilterIterator only keeps files, directories, or both. * * @author Fabien Potencier */ class FileTypeFilterIterator extends FilterIterator { const ONLY_FILES = 1; const ONLY_DIRECTORIES = 2; private $mode; /** * @param \Iterator $iterator The Iterator to filter * @param int $mode The mode (self::ONLY_FILES or self::ONLY_DIRECTORIES) */ public function __construct(\Iterator $iterator, $mode) { $this->mode = $mode; parent::__construct($iterator); } /** * Filters the iterator values. * * @return bool true if the value should be kept, false otherwise */ public function accept() { $fileinfo = $this->current(); if (self::ONLY_DIRECTORIES === (self::ONLY_DIRECTORIES & $this->mode) && $fileinfo->isFile()) { return false; } elseif (self::ONLY_FILES === (self::ONLY_FILES & $this->mode) && $fileinfo->isDir()) { return false; } return true; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Finder; use Symfony\Component\Finder\Comparator\DateComparator; use Symfony\Component\Finder\Comparator\NumberComparator; use Symfony\Component\Finder\Iterator\CustomFilterIterator; use Symfony\Component\Finder\Iterator\DateRangeFilterIterator; use Symfony\Component\Finder\Iterator\DepthRangeFilterIterator; use Symfony\Component\Finder\Iterator\ExcludeDirectoryFilterIterator; use Symfony\Component\Finder\Iterator\FilecontentFilterIterator; use Symfony\Component\Finder\Iterator\FilenameFilterIterator; use Symfony\Component\Finder\Iterator\SizeRangeFilterIterator; use Symfony\Component\Finder\Iterator\SortableIterator; /** * Finder allows to build rules to find files and directories. * * It is a thin wrapper around several specialized iterator classes. * * All rules may be invoked several times. * * All methods return the current Finder object to allow chaining: * * $finder = Finder::create()->files()->name('*.php')->in(__DIR__); * * @author Fabien Potencier */ class Finder implements \IteratorAggregate, \Countable { const IGNORE_VCS_FILES = 1; const IGNORE_DOT_FILES = 2; private $mode = 0; private $names = []; private $notNames = []; private $exclude = []; private $filters = []; private $depths = []; private $sizes = []; private $followLinks = false; private $sort = false; private $ignore = 0; private $dirs = []; private $dates = []; private $iterators = []; private $contains = []; private $notContains = []; private $paths = []; private $notPaths = []; private $ignoreUnreadableDirs = false; private static $vcsPatterns = ['.svn', '_svn', 'CVS', '_darcs', '.arch-params', '.monotone', '.bzr', '.git', '.hg']; public function __construct() { $this->ignore = static::IGNORE_VCS_FILES | static::IGNORE_DOT_FILES; } /** * Creates a new Finder. * * @return static */ public static function create() { return new static(); } /** * Restricts the matching to directories only. * * @return $this */ public function directories() { $this->mode = Iterator\FileTypeFilterIterator::ONLY_DIRECTORIES; return $this; } /** * Restricts the matching to files only. * * @return $this */ public function files() { $this->mode = Iterator\FileTypeFilterIterator::ONLY_FILES; return $this; } /** * Adds tests for the directory depth. * * Usage: * * $finder->depth('> 1') // the Finder will start matching at level 1. * $finder->depth('< 3') // the Finder will descend at most 3 levels of directories below the starting point. * * @param string|int $level The depth level expression * * @return $this * * @see DepthRangeFilterIterator * @see NumberComparator */ public function depth($level) { $this->depths[] = new Comparator\NumberComparator($level); return $this; } /** * Adds tests for file dates (last modified). * * The date must be something that strtotime() is able to parse: * * $finder->date('since yesterday'); * $finder->date('until 2 days ago'); * $finder->date('> now - 2 hours'); * $finder->date('>= 2005-10-15'); * * @param string $date A date range string * * @return $this * * @see strtotime * @see DateRangeFilterIterator * @see DateComparator */ public function date($date) { $this->dates[] = new Comparator\DateComparator($date); return $this; } /** * Adds rules that files must match. * * You can use patterns (delimited with / sign), globs or simple strings. * * $finder->name('*.php') * $finder->name('/\.php$/') // same as above * $finder->name('test.php') * * @param string $pattern A pattern (a regexp, a glob, or a string) * * @return $this * * @see FilenameFilterIterator */ public function name($pattern) { $this->names[] = $pattern; return $this; } /** * Adds rules that files must not match. * * @param string $pattern A pattern (a regexp, a glob, or a string) * * @return $this * * @see FilenameFilterIterator */ public function notName($pattern) { $this->notNames[] = $pattern; return $this; } /** * Adds tests that file contents must match. * * Strings or PCRE patterns can be used: * * $finder->contains('Lorem ipsum') * $finder->contains('/Lorem ipsum/i') * * @param string $pattern A pattern (string or regexp) * * @return $this * * @see FilecontentFilterIterator */ public function contains($pattern) { $this->contains[] = $pattern; return $this; } /** * Adds tests that file contents must not match. * * Strings or PCRE patterns can be used: * * $finder->notContains('Lorem ipsum') * $finder->notContains('/Lorem ipsum/i') * * @param string $pattern A pattern (string or regexp) * * @return $this * * @see FilecontentFilterIterator */ public function notContains($pattern) { $this->notContains[] = $pattern; return $this; } /** * Adds rules that filenames must match. * * You can use patterns (delimited with / sign) or simple strings. * * $finder->path('some/special/dir') * $finder->path('/some\/special\/dir/') // same as above * * Use only / as dirname separator. * * @param string $pattern A pattern (a regexp or a string) * * @return $this * * @see FilenameFilterIterator */ public function path($pattern) { $this->paths[] = $pattern; return $this; } /** * Adds rules that filenames must not match. * * You can use patterns (delimited with / sign) or simple strings. * * $finder->notPath('some/special/dir') * $finder->notPath('/some\/special\/dir/') // same as above * * Use only / as dirname separator. * * @param string $pattern A pattern (a regexp or a string) * * @return $this * * @see FilenameFilterIterator */ public function notPath($pattern) { $this->notPaths[] = $pattern; return $this; } /** * Adds tests for file sizes. * * $finder->size('> 10K'); * $finder->size('<= 1Ki'); * $finder->size(4); * * @param string|int $size A size range string or an integer * * @return $this * * @see SizeRangeFilterIterator * @see NumberComparator */ public function size($size) { $this->sizes[] = new Comparator\NumberComparator($size); return $this; } /** * Excludes directories. * * Directories passed as argument must be relative to the ones defined with the `in()` method. For example: * * $finder->in(__DIR__)->exclude('ruby'); * * @param string|array $dirs A directory path or an array of directories * * @return $this * * @see ExcludeDirectoryFilterIterator */ public function exclude($dirs) { $this->exclude = array_merge($this->exclude, (array) $dirs); return $this; } /** * Excludes "hidden" directories and files (starting with a dot). * * This option is enabled by default. * * @param bool $ignoreDotFiles Whether to exclude "hidden" files or not * * @return $this * * @see ExcludeDirectoryFilterIterator */ public function ignoreDotFiles($ignoreDotFiles) { if ($ignoreDotFiles) { $this->ignore |= static::IGNORE_DOT_FILES; } else { $this->ignore &= ~static::IGNORE_DOT_FILES; } return $this; } /** * Forces the finder to ignore version control directories. * * This option is enabled by default. * * @param bool $ignoreVCS Whether to exclude VCS files or not * * @return $this * * @see ExcludeDirectoryFilterIterator */ public function ignoreVCS($ignoreVCS) { if ($ignoreVCS) { $this->ignore |= static::IGNORE_VCS_FILES; } else { $this->ignore &= ~static::IGNORE_VCS_FILES; } return $this; } /** * Adds VCS patterns. * * @see ignoreVCS() * * @param string|string[] $pattern VCS patterns to ignore */ public static function addVCSPattern($pattern) { foreach ((array) $pattern as $p) { self::$vcsPatterns[] = $p; } self::$vcsPatterns = array_unique(self::$vcsPatterns); } /** * Sorts files and directories by an anonymous function. * * The anonymous function receives two \SplFileInfo instances to compare. * * This can be slow as all the matching files and directories must be retrieved for comparison. * * @return $this * * @see SortableIterator */ public function sort(\Closure $closure) { $this->sort = $closure; return $this; } /** * Sorts files and directories by name. * * This can be slow as all the matching files and directories must be retrieved for comparison. * * @return $this * * @see SortableIterator */ public function sortByName() { $this->sort = Iterator\SortableIterator::SORT_BY_NAME; return $this; } /** * Sorts files and directories by type (directories before files), then by name. * * This can be slow as all the matching files and directories must be retrieved for comparison. * * @return $this * * @see SortableIterator */ public function sortByType() { $this->sort = Iterator\SortableIterator::SORT_BY_TYPE; return $this; } /** * Sorts files and directories by the last accessed time. * * This is the time that the file was last accessed, read or written to. * * This can be slow as all the matching files and directories must be retrieved for comparison. * * @return $this * * @see SortableIterator */ public function sortByAccessedTime() { $this->sort = Iterator\SortableIterator::SORT_BY_ACCESSED_TIME; return $this; } /** * Sorts files and directories by the last inode changed time. * * This is the time that the inode information was last modified (permissions, owner, group or other metadata). * * On Windows, since inode is not available, changed time is actually the file creation time. * * This can be slow as all the matching files and directories must be retrieved for comparison. * * @return $this * * @see SortableIterator */ public function sortByChangedTime() { $this->sort = Iterator\SortableIterator::SORT_BY_CHANGED_TIME; return $this; } /** * Sorts files and directories by the last modified time. * * This is the last time the actual contents of the file were last modified. * * This can be slow as all the matching files and directories must be retrieved for comparison. * * @return $this * * @see SortableIterator */ public function sortByModifiedTime() { $this->sort = Iterator\SortableIterator::SORT_BY_MODIFIED_TIME; return $this; } /** * Filters the iterator with an anonymous function. * * The anonymous function receives a \SplFileInfo and must return false * to remove files. * * @return $this * * @see CustomFilterIterator */ public function filter(\Closure $closure) { $this->filters[] = $closure; return $this; } /** * Forces the following of symlinks. * * @return $this */ public function followLinks() { $this->followLinks = true; return $this; } /** * Tells finder to ignore unreadable directories. * * By default, scanning unreadable directories content throws an AccessDeniedException. * * @param bool $ignore * * @return $this */ public function ignoreUnreadableDirs($ignore = true) { $this->ignoreUnreadableDirs = (bool) $ignore; return $this; } /** * Searches files and directories which match defined rules. * * @param string|string[] $dirs A directory path or an array of directories * * @return $this * * @throws \InvalidArgumentException if one of the directories does not exist */ public function in($dirs) { $resolvedDirs = []; foreach ((array) $dirs as $dir) { if (is_dir($dir)) { $resolvedDirs[] = $this->normalizeDir($dir); } elseif ($glob = glob($dir, (\defined('GLOB_BRACE') ? \GLOB_BRACE : 0) | \GLOB_ONLYDIR | \GLOB_NOSORT)) { sort($glob); $resolvedDirs = array_merge($resolvedDirs, array_map([$this, 'normalizeDir'], $glob)); } else { throw new \InvalidArgumentException(sprintf('The "%s" directory does not exist.', $dir)); } } $this->dirs = array_merge($this->dirs, $resolvedDirs); return $this; } /** * Returns an Iterator for the current Finder configuration. * * This method implements the IteratorAggregate interface. * * @return \Iterator|SplFileInfo[] An iterator * * @throws \LogicException if the in() method has not been called */ public function getIterator() { if (0 === \count($this->dirs) && 0 === \count($this->iterators)) { throw new \LogicException('You must call one of in() or append() methods before iterating over a Finder.'); } if (1 === \count($this->dirs) && 0 === \count($this->iterators)) { return $this->searchInDirectory($this->dirs[0]); } $iterator = new \AppendIterator(); foreach ($this->dirs as $dir) { $iterator->append($this->searchInDirectory($dir)); } foreach ($this->iterators as $it) { $iterator->append($it); } return $iterator; } /** * Appends an existing set of files/directories to the finder. * * The set can be another Finder, an Iterator, an IteratorAggregate, or even a plain array. * * @param iterable $iterator * * @return $this * * @throws \InvalidArgumentException when the given argument is not iterable */ public function append($iterator) { if ($iterator instanceof \IteratorAggregate) { $this->iterators[] = $iterator->getIterator(); } elseif ($iterator instanceof \Iterator) { $this->iterators[] = $iterator; } elseif ($iterator instanceof \Traversable || \is_array($iterator)) { $it = new \ArrayIterator(); foreach ($iterator as $file) { $it->append($file instanceof \SplFileInfo ? $file : new \SplFileInfo($file)); } $this->iterators[] = $it; } else { throw new \InvalidArgumentException('Finder::append() method wrong argument type.'); } return $this; } /** * Check if any results were found. * * @return bool */ public function hasResults() { foreach ($this->getIterator() as $_) { return true; } return false; } /** * Counts all the results collected by the iterators. * * @return int */ public function count() { return iterator_count($this->getIterator()); } /** * @param string $dir * * @return \Iterator */ private function searchInDirectory($dir) { $exclude = $this->exclude; $notPaths = $this->notPaths; if (static::IGNORE_VCS_FILES === (static::IGNORE_VCS_FILES & $this->ignore)) { $exclude = array_merge($exclude, self::$vcsPatterns); } if (static::IGNORE_DOT_FILES === (static::IGNORE_DOT_FILES & $this->ignore)) { $notPaths[] = '#(^|/)\..+(/|$)#'; } $minDepth = 0; $maxDepth = \PHP_INT_MAX; foreach ($this->depths as $comparator) { switch ($comparator->getOperator()) { case '>': $minDepth = $comparator->getTarget() + 1; break; case '>=': $minDepth = $comparator->getTarget(); break; case '<': $maxDepth = $comparator->getTarget() - 1; break; case '<=': $maxDepth = $comparator->getTarget(); break; default: $minDepth = $maxDepth = $comparator->getTarget(); } } $flags = \RecursiveDirectoryIterator::SKIP_DOTS; if ($this->followLinks) { $flags |= \RecursiveDirectoryIterator::FOLLOW_SYMLINKS; } $iterator = new Iterator\RecursiveDirectoryIterator($dir, $flags, $this->ignoreUnreadableDirs); if ($exclude) { $iterator = new Iterator\ExcludeDirectoryFilterIterator($iterator, $exclude); } $iterator = new \RecursiveIteratorIterator($iterator, \RecursiveIteratorIterator::SELF_FIRST); if ($minDepth > 0 || $maxDepth < \PHP_INT_MAX) { $iterator = new Iterator\DepthRangeFilterIterator($iterator, $minDepth, $maxDepth); } if ($this->mode) { $iterator = new Iterator\FileTypeFilterIterator($iterator, $this->mode); } if ($this->names || $this->notNames) { $iterator = new Iterator\FilenameFilterIterator($iterator, $this->names, $this->notNames); } if ($this->contains || $this->notContains) { $iterator = new Iterator\FilecontentFilterIterator($iterator, $this->contains, $this->notContains); } if ($this->sizes) { $iterator = new Iterator\SizeRangeFilterIterator($iterator, $this->sizes); } if ($this->dates) { $iterator = new Iterator\DateRangeFilterIterator($iterator, $this->dates); } if ($this->filters) { $iterator = new Iterator\CustomFilterIterator($iterator, $this->filters); } if ($this->paths || $notPaths) { $iterator = new Iterator\PathFilterIterator($iterator, $this->paths, $notPaths); } if ($this->sort) { $iteratorAggregate = new Iterator\SortableIterator($iterator, $this->sort); $iterator = $iteratorAggregate->getIterator(); } return $iterator; } /** * Normalizes given directory names by removing trailing slashes. * * Excluding: (s)ftp:// or ssh2.(s)ftp:// wrapper * * @param string $dir * * @return string */ private function normalizeDir($dir) { if ('/' === $dir) { return $dir; } $dir = rtrim($dir, '/'.\DIRECTORY_SEPARATOR); if (preg_match('#^(ssh2\.)?s?ftp://#', $dir)) { $dir .= '/'; } return $dir; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Finder; /** * Glob matches globbing patterns against text. * * if match_glob("foo.*", "foo.bar") echo "matched\n"; * * // prints foo.bar and foo.baz * $regex = glob_to_regex("foo.*"); * for (['foo.bar', 'foo.baz', 'foo', 'bar'] as $t) * { * if (/$regex/) echo "matched: $car\n"; * } * * Glob implements glob(3) style matching that can be used to match * against text, rather than fetching names from a filesystem. * * Based on the Perl Text::Glob module. * * @author Fabien Potencier PHP port * @author Richard Clamp Perl version * @copyright 2004-2005 Fabien Potencier * @copyright 2002 Richard Clamp */ class Glob { /** * Returns a regexp which is the equivalent of the glob pattern. * * @param string $glob The glob pattern * @param bool $strictLeadingDot * @param bool $strictWildcardSlash * @param string $delimiter Optional delimiter * * @return string regex The regexp */ public static function toRegex($glob, $strictLeadingDot = true, $strictWildcardSlash = true, $delimiter = '#') { $firstByte = true; $escaping = false; $inCurlies = 0; $regex = ''; $sizeGlob = \strlen($glob); for ($i = 0; $i < $sizeGlob; ++$i) { $car = $glob[$i]; if ($firstByte && $strictLeadingDot && '.' !== $car) { $regex .= '(?=[^\.])'; } $firstByte = '/' === $car; if ($firstByte && $strictWildcardSlash && isset($glob[$i + 2]) && '**' === $glob[$i + 1].$glob[$i + 2] && (!isset($glob[$i + 3]) || '/' === $glob[$i + 3])) { $car = '[^/]++/'; if (!isset($glob[$i + 3])) { $car .= '?'; } if ($strictLeadingDot) { $car = '(?=[^\.])'.$car; } $car = '/(?:'.$car.')*'; $i += 2 + isset($glob[$i + 3]); if ('/' === $delimiter) { $car = str_replace('/', '\\/', $car); } } if ($delimiter === $car || '.' === $car || '(' === $car || ')' === $car || '|' === $car || '+' === $car || '^' === $car || '$' === $car) { $regex .= "\\$car"; } elseif ('*' === $car) { $regex .= $escaping ? '\\*' : ($strictWildcardSlash ? '[^/]*' : '.*'); } elseif ('?' === $car) { $regex .= $escaping ? '\\?' : ($strictWildcardSlash ? '[^/]' : '.'); } elseif ('{' === $car) { $regex .= $escaping ? '\\{' : '('; if (!$escaping) { ++$inCurlies; } } elseif ('}' === $car && $inCurlies) { $regex .= $escaping ? '}' : ')'; if (!$escaping) { --$inCurlies; } } elseif (',' === $car && $inCurlies) { $regex .= $escaping ? ',' : '|'; } elseif ('\\' === $car) { if ($escaping) { $regex .= '\\\\'; $escaping = false; } else { $escaping = true; } continue; } else { $regex .= $car; } $escaping = false; } return $delimiter.'^'.$regex.'$'.$delimiter; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ use Symfony\Polyfill\Mbstring as p; if (!function_exists('mb_convert_variables')) { /** * Convert character code in variable(s) */ function mb_convert_variables($to_encoding, $from_encoding, &$var, &...$vars) { $vars = [&$var, ...$vars]; $ok = true; array_walk_recursive($vars, function (&$v) use (&$ok, $to_encoding, $from_encoding) { if (false === $v = p\Mbstring::mb_convert_encoding($v, $to_encoding, $from_encoding)) { $ok = false; } }); return $ok ? $from_encoding : false; } } 'A', 'b' => 'B', 'c' => 'C', 'd' => 'D', 'e' => 'E', 'f' => 'F', 'g' => 'G', 'h' => 'H', 'i' => 'I', 'j' => 'J', 'k' => 'K', 'l' => 'L', 'm' => 'M', 'n' => 'N', 'o' => 'O', 'p' => 'P', 'q' => 'Q', 'r' => 'R', 's' => 'S', 't' => 'T', 'u' => 'U', 'v' => 'V', 'w' => 'W', 'x' => 'X', 'y' => 'Y', 'z' => 'Z', 'µ' => 'Μ', 'à' => 'À', 'á' => 'Á', 'â' => 'Â', 'ã' => 'Ã', 'ä' => 'Ä', 'å' => 'Å', 'æ' => 'Æ', 'ç' => 'Ç', 'è' => 'È', 'é' => 'É', 'ê' => 'Ê', 'ë' => 'Ë', 'ì' => 'Ì', 'í' => 'Í', 'î' => 'Î', 'ï' => 'Ï', 'ð' => 'Ð', 'ñ' => 'Ñ', 'ò' => 'Ò', 'ó' => 'Ó', 'ô' => 'Ô', 'õ' => 'Õ', 'ö' => 'Ö', 'ø' => 'Ø', 'ù' => 'Ù', 'ú' => 'Ú', 'û' => 'Û', 'ü' => 'Ü', 'ý' => 'Ý', 'þ' => 'Þ', 'ÿ' => 'Ÿ', 'ā' => 'Ā', 'ă' => 'Ă', 'ą' => 'Ą', 'ć' => 'Ć', 'ĉ' => 'Ĉ', 'ċ' => 'Ċ', 'č' => 'Č', 'ď' => 'Ď', 'đ' => 'Đ', 'ē' => 'Ē', 'ĕ' => 'Ĕ', 'ė' => 'Ė', 'ę' => 'Ę', 'ě' => 'Ě', 'ĝ' => 'Ĝ', 'ğ' => 'Ğ', 'ġ' => 'Ġ', 'ģ' => 'Ģ', 'ĥ' => 'Ĥ', 'ħ' => 'Ħ', 'ĩ' => 'Ĩ', 'ī' => 'Ī', 'ĭ' => 'Ĭ', 'į' => 'Į', 'ı' => 'I', 'ij' => 'IJ', 'ĵ' => 'Ĵ', 'ķ' => 'Ķ', 'ĺ' => 'Ĺ', 'ļ' => 'Ļ', 'ľ' => 'Ľ', 'ŀ' => 'Ŀ', 'ł' => 'Ł', 'ń' => 'Ń', 'ņ' => 'Ņ', 'ň' => 'Ň', 'ŋ' => 'Ŋ', 'ō' => 'Ō', 'ŏ' => 'Ŏ', 'ő' => 'Ő', 'œ' => 'Œ', 'ŕ' => 'Ŕ', 'ŗ' => 'Ŗ', 'ř' => 'Ř', 'ś' => 'Ś', 'ŝ' => 'Ŝ', 'ş' => 'Ş', 'š' => 'Š', 'ţ' => 'Ţ', 'ť' => 'Ť', 'ŧ' => 'Ŧ', 'ũ' => 'Ũ', 'ū' => 'Ū', 'ŭ' => 'Ŭ', 'ů' => 'Ů', 'ű' => 'Ű', 'ų' => 'Ų', 'ŵ' => 'Ŵ', 'ŷ' => 'Ŷ', 'ź' => 'Ź', 'ż' => 'Ż', 'ž' => 'Ž', 'ſ' => 'S', 'ƀ' => 'Ƀ', 'ƃ' => 'Ƃ', 'ƅ' => 'Ƅ', 'ƈ' => 'Ƈ', 'ƌ' => 'Ƌ', 'ƒ' => 'Ƒ', 'ƕ' => 'Ƕ', 'ƙ' => 'Ƙ', 'ƚ' => 'Ƚ', 'ƞ' => 'Ƞ', 'ơ' => 'Ơ', 'ƣ' => 'Ƣ', 'ƥ' => 'Ƥ', 'ƨ' => 'Ƨ', 'ƭ' => 'Ƭ', 'ư' => 'Ư', 'ƴ' => 'Ƴ', 'ƶ' => 'Ƶ', 'ƹ' => 'Ƹ', 'ƽ' => 'Ƽ', 'ƿ' => 'Ƿ', 'Dž' => 'DŽ', 'dž' => 'DŽ', 'Lj' => 'LJ', 'lj' => 'LJ', 'Nj' => 'NJ', 'nj' => 'NJ', 'ǎ' => 'Ǎ', 'ǐ' => 'Ǐ', 'ǒ' => 'Ǒ', 'ǔ' => 'Ǔ', 'ǖ' => 'Ǖ', 'ǘ' => 'Ǘ', 'ǚ' => 'Ǚ', 'ǜ' => 'Ǜ', 'ǝ' => 'Ǝ', 'ǟ' => 'Ǟ', 'ǡ' => 'Ǡ', 'ǣ' => 'Ǣ', 'ǥ' => 'Ǥ', 'ǧ' => 'Ǧ', 'ǩ' => 'Ǩ', 'ǫ' => 'Ǫ', 'ǭ' => 'Ǭ', 'ǯ' => 'Ǯ', 'Dz' => 'DZ', 'dz' => 'DZ', 'ǵ' => 'Ǵ', 'ǹ' => 'Ǹ', 'ǻ' => 'Ǻ', 'ǽ' => 'Ǽ', 'ǿ' => 'Ǿ', 'ȁ' => 'Ȁ', 'ȃ' => 'Ȃ', 'ȅ' => 'Ȅ', 'ȇ' => 'Ȇ', 'ȉ' => 'Ȉ', 'ȋ' => 'Ȋ', 'ȍ' => 'Ȍ', 'ȏ' => 'Ȏ', 'ȑ' => 'Ȑ', 'ȓ' => 'Ȓ', 'ȕ' => 'Ȕ', 'ȗ' => 'Ȗ', 'ș' => 'Ș', 'ț' => 'Ț', 'ȝ' => 'Ȝ', 'ȟ' => 'Ȟ', 'ȣ' => 'Ȣ', 'ȥ' => 'Ȥ', 'ȧ' => 'Ȧ', 'ȩ' => 'Ȩ', 'ȫ' => 'Ȫ', 'ȭ' => 'Ȭ', 'ȯ' => 'Ȯ', 'ȱ' => 'Ȱ', 'ȳ' => 'Ȳ', 'ȼ' => 'Ȼ', 'ȿ' => 'Ȿ', 'ɀ' => 'Ɀ', 'ɂ' => 'Ɂ', 'ɇ' => 'Ɇ', 'ɉ' => 'Ɉ', 'ɋ' => 'Ɋ', 'ɍ' => 'Ɍ', 'ɏ' => 'Ɏ', 'ɐ' => 'Ɐ', 'ɑ' => 'Ɑ', 'ɒ' => 'Ɒ', 'ɓ' => 'Ɓ', 'ɔ' => 'Ɔ', 'ɖ' => 'Ɖ', 'ɗ' => 'Ɗ', 'ə' => 'Ə', 'ɛ' => 'Ɛ', 'ɜ' => 'Ɜ', 'ɠ' => 'Ɠ', 'ɡ' => 'Ɡ', 'ɣ' => 'Ɣ', 'ɥ' => 'Ɥ', 'ɦ' => 'Ɦ', 'ɨ' => 'Ɨ', 'ɩ' => 'Ɩ', 'ɪ' => 'Ɪ', 'ɫ' => 'Ɫ', 'ɬ' => 'Ɬ', 'ɯ' => 'Ɯ', 'ɱ' => 'Ɱ', 'ɲ' => 'Ɲ', 'ɵ' => 'Ɵ', 'ɽ' => 'Ɽ', 'ʀ' => 'Ʀ', 'ʂ' => 'Ʂ', 'ʃ' => 'Ʃ', 'ʇ' => 'Ʇ', 'ʈ' => 'Ʈ', 'ʉ' => 'Ʉ', 'ʊ' => 'Ʊ', 'ʋ' => 'Ʋ', 'ʌ' => 'Ʌ', 'ʒ' => 'Ʒ', 'ʝ' => 'Ʝ', 'ʞ' => 'Ʞ', 'ͅ' => 'Ι', 'ͱ' => 'Ͱ', 'ͳ' => 'Ͳ', 'ͷ' => 'Ͷ', 'ͻ' => 'Ͻ', 'ͼ' => 'Ͼ', 'ͽ' => 'Ͽ', 'ά' => 'Ά', 'έ' => 'Έ', 'ή' => 'Ή', 'ί' => 'Ί', 'α' => 'Α', 'β' => 'Β', 'γ' => 'Γ', 'δ' => 'Δ', 'ε' => 'Ε', 'ζ' => 'Ζ', 'η' => 'Η', 'θ' => 'Θ', 'ι' => 'Ι', 'κ' => 'Κ', 'λ' => 'Λ', 'μ' => 'Μ', 'ν' => 'Ν', 'ξ' => 'Ξ', 'ο' => 'Ο', 'π' => 'Π', 'ρ' => 'Ρ', 'ς' => 'Σ', 'σ' => 'Σ', 'τ' => 'Τ', 'υ' => 'Υ', 'φ' => 'Φ', 'χ' => 'Χ', 'ψ' => 'Ψ', 'ω' => 'Ω', 'ϊ' => 'Ϊ', 'ϋ' => 'Ϋ', 'ό' => 'Ό', 'ύ' => 'Ύ', 'ώ' => 'Ώ', 'ϐ' => 'Β', 'ϑ' => 'Θ', 'ϕ' => 'Φ', 'ϖ' => 'Π', 'ϗ' => 'Ϗ', 'ϙ' => 'Ϙ', 'ϛ' => 'Ϛ', 'ϝ' => 'Ϝ', 'ϟ' => 'Ϟ', 'ϡ' => 'Ϡ', 'ϣ' => 'Ϣ', 'ϥ' => 'Ϥ', 'ϧ' => 'Ϧ', 'ϩ' => 'Ϩ', 'ϫ' => 'Ϫ', 'ϭ' => 'Ϭ', 'ϯ' => 'Ϯ', 'ϰ' => 'Κ', 'ϱ' => 'Ρ', 'ϲ' => 'Ϲ', 'ϳ' => 'Ϳ', 'ϵ' => 'Ε', 'ϸ' => 'Ϸ', 'ϻ' => 'Ϻ', 'а' => 'А', 'б' => 'Б', 'в' => 'В', 'г' => 'Г', 'д' => 'Д', 'е' => 'Е', 'ж' => 'Ж', 'з' => 'З', 'и' => 'И', 'й' => 'Й', 'к' => 'К', 'л' => 'Л', 'м' => 'М', 'н' => 'Н', 'о' => 'О', 'п' => 'П', 'р' => 'Р', 'с' => 'С', 'т' => 'Т', 'у' => 'У', 'ф' => 'Ф', 'х' => 'Х', 'ц' => 'Ц', 'ч' => 'Ч', 'ш' => 'Ш', 'щ' => 'Щ', 'ъ' => 'Ъ', 'ы' => 'Ы', 'ь' => 'Ь', 'э' => 'Э', 'ю' => 'Ю', 'я' => 'Я', 'ѐ' => 'Ѐ', 'ё' => 'Ё', 'ђ' => 'Ђ', 'ѓ' => 'Ѓ', 'є' => 'Є', 'ѕ' => 'Ѕ', 'і' => 'І', 'ї' => 'Ї', 'ј' => 'Ј', 'љ' => 'Љ', 'њ' => 'Њ', 'ћ' => 'Ћ', 'ќ' => 'Ќ', 'ѝ' => 'Ѝ', 'ў' => 'Ў', 'џ' => 'Џ', 'ѡ' => 'Ѡ', 'ѣ' => 'Ѣ', 'ѥ' => 'Ѥ', 'ѧ' => 'Ѧ', 'ѩ' => 'Ѩ', 'ѫ' => 'Ѫ', 'ѭ' => 'Ѭ', 'ѯ' => 'Ѯ', 'ѱ' => 'Ѱ', 'ѳ' => 'Ѳ', 'ѵ' => 'Ѵ', 'ѷ' => 'Ѷ', 'ѹ' => 'Ѹ', 'ѻ' => 'Ѻ', 'ѽ' => 'Ѽ', 'ѿ' => 'Ѿ', 'ҁ' => 'Ҁ', 'ҋ' => 'Ҋ', 'ҍ' => 'Ҍ', 'ҏ' => 'Ҏ', 'ґ' => 'Ґ', 'ғ' => 'Ғ', 'ҕ' => 'Ҕ', 'җ' => 'Җ', 'ҙ' => 'Ҙ', 'қ' => 'Қ', 'ҝ' => 'Ҝ', 'ҟ' => 'Ҟ', 'ҡ' => 'Ҡ', 'ң' => 'Ң', 'ҥ' => 'Ҥ', 'ҧ' => 'Ҧ', 'ҩ' => 'Ҩ', 'ҫ' => 'Ҫ', 'ҭ' => 'Ҭ', 'ү' => 'Ү', 'ұ' => 'Ұ', 'ҳ' => 'Ҳ', 'ҵ' => 'Ҵ', 'ҷ' => 'Ҷ', 'ҹ' => 'Ҹ', 'һ' => 'Һ', 'ҽ' => 'Ҽ', 'ҿ' => 'Ҿ', 'ӂ' => 'Ӂ', 'ӄ' => 'Ӄ', 'ӆ' => 'Ӆ', 'ӈ' => 'Ӈ', 'ӊ' => 'Ӊ', 'ӌ' => 'Ӌ', 'ӎ' => 'Ӎ', 'ӏ' => 'Ӏ', 'ӑ' => 'Ӑ', 'ӓ' => 'Ӓ', 'ӕ' => 'Ӕ', 'ӗ' => 'Ӗ', 'ә' => 'Ә', 'ӛ' => 'Ӛ', 'ӝ' => 'Ӝ', 'ӟ' => 'Ӟ', 'ӡ' => 'Ӡ', 'ӣ' => 'Ӣ', 'ӥ' => 'Ӥ', 'ӧ' => 'Ӧ', 'ө' => 'Ө', 'ӫ' => 'Ӫ', 'ӭ' => 'Ӭ', 'ӯ' => 'Ӯ', 'ӱ' => 'Ӱ', 'ӳ' => 'Ӳ', 'ӵ' => 'Ӵ', 'ӷ' => 'Ӷ', 'ӹ' => 'Ӹ', 'ӻ' => 'Ӻ', 'ӽ' => 'Ӽ', 'ӿ' => 'Ӿ', 'ԁ' => 'Ԁ', 'ԃ' => 'Ԃ', 'ԅ' => 'Ԅ', 'ԇ' => 'Ԇ', 'ԉ' => 'Ԉ', 'ԋ' => 'Ԋ', 'ԍ' => 'Ԍ', 'ԏ' => 'Ԏ', 'ԑ' => 'Ԑ', 'ԓ' => 'Ԓ', 'ԕ' => 'Ԕ', 'ԗ' => 'Ԗ', 'ԙ' => 'Ԙ', 'ԛ' => 'Ԛ', 'ԝ' => 'Ԝ', 'ԟ' => 'Ԟ', 'ԡ' => 'Ԡ', 'ԣ' => 'Ԣ', 'ԥ' => 'Ԥ', 'ԧ' => 'Ԧ', 'ԩ' => 'Ԩ', 'ԫ' => 'Ԫ', 'ԭ' => 'Ԭ', 'ԯ' => 'Ԯ', 'ա' => 'Ա', 'բ' => 'Բ', 'գ' => 'Գ', 'դ' => 'Դ', 'ե' => 'Ե', 'զ' => 'Զ', 'է' => 'Է', 'ը' => 'Ը', 'թ' => 'Թ', 'ժ' => 'Ժ', 'ի' => 'Ի', 'լ' => 'Լ', 'խ' => 'Խ', 'ծ' => 'Ծ', 'կ' => 'Կ', 'հ' => 'Հ', 'ձ' => 'Ձ', 'ղ' => 'Ղ', 'ճ' => 'Ճ', 'մ' => 'Մ', 'յ' => 'Յ', 'ն' => 'Ն', 'շ' => 'Շ', 'ո' => 'Ո', 'չ' => 'Չ', 'պ' => 'Պ', 'ջ' => 'Ջ', 'ռ' => 'Ռ', 'ս' => 'Ս', 'վ' => 'Վ', 'տ' => 'Տ', 'ր' => 'Ր', 'ց' => 'Ց', 'ւ' => 'Ւ', 'փ' => 'Փ', 'ք' => 'Ք', 'օ' => 'Օ', 'ֆ' => 'Ֆ', 'ა' => 'Ა', 'ბ' => 'Ბ', 'გ' => 'Გ', 'დ' => 'Დ', 'ე' => 'Ე', 'ვ' => 'Ვ', 'ზ' => 'Ზ', 'თ' => 'Თ', 'ი' => 'Ი', 'კ' => 'Კ', 'ლ' => 'Ლ', 'მ' => 'Მ', 'ნ' => 'Ნ', 'ო' => 'Ო', 'პ' => 'Პ', 'ჟ' => 'Ჟ', 'რ' => 'Რ', 'ს' => 'Ს', 'ტ' => 'Ტ', 'უ' => 'Უ', 'ფ' => 'Ფ', 'ქ' => 'Ქ', 'ღ' => 'Ღ', 'ყ' => 'Ყ', 'შ' => 'Შ', 'ჩ' => 'Ჩ', 'ც' => 'Ც', 'ძ' => 'Ძ', 'წ' => 'Წ', 'ჭ' => 'Ჭ', 'ხ' => 'Ხ', 'ჯ' => 'Ჯ', 'ჰ' => 'Ჰ', 'ჱ' => 'Ჱ', 'ჲ' => 'Ჲ', 'ჳ' => 'Ჳ', 'ჴ' => 'Ჴ', 'ჵ' => 'Ჵ', 'ჶ' => 'Ჶ', 'ჷ' => 'Ჷ', 'ჸ' => 'Ჸ', 'ჹ' => 'Ჹ', 'ჺ' => 'Ჺ', 'ჽ' => 'Ჽ', 'ჾ' => 'Ჾ', 'ჿ' => 'Ჿ', 'ᏸ' => 'Ᏸ', 'ᏹ' => 'Ᏹ', 'ᏺ' => 'Ᏺ', 'ᏻ' => 'Ᏻ', 'ᏼ' => 'Ᏼ', 'ᏽ' => 'Ᏽ', 'ᲀ' => 'В', 'ᲁ' => 'Д', 'ᲂ' => 'О', 'ᲃ' => 'С', 'ᲄ' => 'Т', 'ᲅ' => 'Т', 'ᲆ' => 'Ъ', 'ᲇ' => 'Ѣ', 'ᲈ' => 'Ꙋ', 'ᵹ' => 'Ᵹ', 'ᵽ' => 'Ᵽ', 'ᶎ' => 'Ᶎ', 'ḁ' => 'Ḁ', 'ḃ' => 'Ḃ', 'ḅ' => 'Ḅ', 'ḇ' => 'Ḇ', 'ḉ' => 'Ḉ', 'ḋ' => 'Ḋ', 'ḍ' => 'Ḍ', 'ḏ' => 'Ḏ', 'ḑ' => 'Ḑ', 'ḓ' => 'Ḓ', 'ḕ' => 'Ḕ', 'ḗ' => 'Ḗ', 'ḙ' => 'Ḙ', 'ḛ' => 'Ḛ', 'ḝ' => 'Ḝ', 'ḟ' => 'Ḟ', 'ḡ' => 'Ḡ', 'ḣ' => 'Ḣ', 'ḥ' => 'Ḥ', 'ḧ' => 'Ḧ', 'ḩ' => 'Ḩ', 'ḫ' => 'Ḫ', 'ḭ' => 'Ḭ', 'ḯ' => 'Ḯ', 'ḱ' => 'Ḱ', 'ḳ' => 'Ḳ', 'ḵ' => 'Ḵ', 'ḷ' => 'Ḷ', 'ḹ' => 'Ḹ', 'ḻ' => 'Ḻ', 'ḽ' => 'Ḽ', 'ḿ' => 'Ḿ', 'ṁ' => 'Ṁ', 'ṃ' => 'Ṃ', 'ṅ' => 'Ṅ', 'ṇ' => 'Ṇ', 'ṉ' => 'Ṉ', 'ṋ' => 'Ṋ', 'ṍ' => 'Ṍ', 'ṏ' => 'Ṏ', 'ṑ' => 'Ṑ', 'ṓ' => 'Ṓ', 'ṕ' => 'Ṕ', 'ṗ' => 'Ṗ', 'ṙ' => 'Ṙ', 'ṛ' => 'Ṛ', 'ṝ' => 'Ṝ', 'ṟ' => 'Ṟ', 'ṡ' => 'Ṡ', 'ṣ' => 'Ṣ', 'ṥ' => 'Ṥ', 'ṧ' => 'Ṧ', 'ṩ' => 'Ṩ', 'ṫ' => 'Ṫ', 'ṭ' => 'Ṭ', 'ṯ' => 'Ṯ', 'ṱ' => 'Ṱ', 'ṳ' => 'Ṳ', 'ṵ' => 'Ṵ', 'ṷ' => 'Ṷ', 'ṹ' => 'Ṹ', 'ṻ' => 'Ṻ', 'ṽ' => 'Ṽ', 'ṿ' => 'Ṿ', 'ẁ' => 'Ẁ', 'ẃ' => 'Ẃ', 'ẅ' => 'Ẅ', 'ẇ' => 'Ẇ', 'ẉ' => 'Ẉ', 'ẋ' => 'Ẋ', 'ẍ' => 'Ẍ', 'ẏ' => 'Ẏ', 'ẑ' => 'Ẑ', 'ẓ' => 'Ẓ', 'ẕ' => 'Ẕ', 'ẛ' => 'Ṡ', 'ạ' => 'Ạ', 'ả' => 'Ả', 'ấ' => 'Ấ', 'ầ' => 'Ầ', 'ẩ' => 'Ẩ', 'ẫ' => 'Ẫ', 'ậ' => 'Ậ', 'ắ' => 'Ắ', 'ằ' => 'Ằ', 'ẳ' => 'Ẳ', 'ẵ' => 'Ẵ', 'ặ' => 'Ặ', 'ẹ' => 'Ẹ', 'ẻ' => 'Ẻ', 'ẽ' => 'Ẽ', 'ế' => 'Ế', 'ề' => 'Ề', 'ể' => 'Ể', 'ễ' => 'Ễ', 'ệ' => 'Ệ', 'ỉ' => 'Ỉ', 'ị' => 'Ị', 'ọ' => 'Ọ', 'ỏ' => 'Ỏ', 'ố' => 'Ố', 'ồ' => 'Ồ', 'ổ' => 'Ổ', 'ỗ' => 'Ỗ', 'ộ' => 'Ộ', 'ớ' => 'Ớ', 'ờ' => 'Ờ', 'ở' => 'Ở', 'ỡ' => 'Ỡ', 'ợ' => 'Ợ', 'ụ' => 'Ụ', 'ủ' => 'Ủ', 'ứ' => 'Ứ', 'ừ' => 'Ừ', 'ử' => 'Ử', 'ữ' => 'Ữ', 'ự' => 'Ự', 'ỳ' => 'Ỳ', 'ỵ' => 'Ỵ', 'ỷ' => 'Ỷ', 'ỹ' => 'Ỹ', 'ỻ' => 'Ỻ', 'ỽ' => 'Ỽ', 'ỿ' => 'Ỿ', 'ἀ' => 'Ἀ', 'ἁ' => 'Ἁ', 'ἂ' => 'Ἂ', 'ἃ' => 'Ἃ', 'ἄ' => 'Ἄ', 'ἅ' => 'Ἅ', 'ἆ' => 'Ἆ', 'ἇ' => 'Ἇ', 'ἐ' => 'Ἐ', 'ἑ' => 'Ἑ', 'ἒ' => 'Ἒ', 'ἓ' => 'Ἓ', 'ἔ' => 'Ἔ', 'ἕ' => 'Ἕ', 'ἠ' => 'Ἠ', 'ἡ' => 'Ἡ', 'ἢ' => 'Ἢ', 'ἣ' => 'Ἣ', 'ἤ' => 'Ἤ', 'ἥ' => 'Ἥ', 'ἦ' => 'Ἦ', 'ἧ' => 'Ἧ', 'ἰ' => 'Ἰ', 'ἱ' => 'Ἱ', 'ἲ' => 'Ἲ', 'ἳ' => 'Ἳ', 'ἴ' => 'Ἴ', 'ἵ' => 'Ἵ', 'ἶ' => 'Ἶ', 'ἷ' => 'Ἷ', 'ὀ' => 'Ὀ', 'ὁ' => 'Ὁ', 'ὂ' => 'Ὂ', 'ὃ' => 'Ὃ', 'ὄ' => 'Ὄ', 'ὅ' => 'Ὅ', 'ὑ' => 'Ὑ', 'ὓ' => 'Ὓ', 'ὕ' => 'Ὕ', 'ὗ' => 'Ὗ', 'ὠ' => 'Ὠ', 'ὡ' => 'Ὡ', 'ὢ' => 'Ὢ', 'ὣ' => 'Ὣ', 'ὤ' => 'Ὤ', 'ὥ' => 'Ὥ', 'ὦ' => 'Ὦ', 'ὧ' => 'Ὧ', 'ὰ' => 'Ὰ', 'ά' => 'Ά', 'ὲ' => 'Ὲ', 'έ' => 'Έ', 'ὴ' => 'Ὴ', 'ή' => 'Ή', 'ὶ' => 'Ὶ', 'ί' => 'Ί', 'ὸ' => 'Ὸ', 'ό' => 'Ό', 'ὺ' => 'Ὺ', 'ύ' => 'Ύ', 'ὼ' => 'Ὼ', 'ώ' => 'Ώ', 'ᾀ' => 'ᾈ', 'ᾁ' => 'ᾉ', 'ᾂ' => 'ᾊ', 'ᾃ' => 'ᾋ', 'ᾄ' => 'ᾌ', 'ᾅ' => 'ᾍ', 'ᾆ' => 'ᾎ', 'ᾇ' => 'ᾏ', 'ᾐ' => 'ᾘ', 'ᾑ' => 'ᾙ', 'ᾒ' => 'ᾚ', 'ᾓ' => 'ᾛ', 'ᾔ' => 'ᾜ', 'ᾕ' => 'ᾝ', 'ᾖ' => 'ᾞ', 'ᾗ' => 'ᾟ', 'ᾠ' => 'ᾨ', 'ᾡ' => 'ᾩ', 'ᾢ' => 'ᾪ', 'ᾣ' => 'ᾫ', 'ᾤ' => 'ᾬ', 'ᾥ' => 'ᾭ', 'ᾦ' => 'ᾮ', 'ᾧ' => 'ᾯ', 'ᾰ' => 'Ᾰ', 'ᾱ' => 'Ᾱ', 'ᾳ' => 'ᾼ', 'ι' => 'Ι', 'ῃ' => 'ῌ', 'ῐ' => 'Ῐ', 'ῑ' => 'Ῑ', 'ῠ' => 'Ῠ', 'ῡ' => 'Ῡ', 'ῥ' => 'Ῥ', 'ῳ' => 'ῼ', 'ⅎ' => 'Ⅎ', 'ⅰ' => 'Ⅰ', 'ⅱ' => 'Ⅱ', 'ⅲ' => 'Ⅲ', 'ⅳ' => 'Ⅳ', 'ⅴ' => 'Ⅴ', 'ⅵ' => 'Ⅵ', 'ⅶ' => 'Ⅶ', 'ⅷ' => 'Ⅷ', 'ⅸ' => 'Ⅸ', 'ⅹ' => 'Ⅹ', 'ⅺ' => 'Ⅺ', 'ⅻ' => 'Ⅻ', 'ⅼ' => 'Ⅼ', 'ⅽ' => 'Ⅽ', 'ⅾ' => 'Ⅾ', 'ⅿ' => 'Ⅿ', 'ↄ' => 'Ↄ', 'ⓐ' => 'Ⓐ', 'ⓑ' => 'Ⓑ', 'ⓒ' => 'Ⓒ', 'ⓓ' => 'Ⓓ', 'ⓔ' => 'Ⓔ', 'ⓕ' => 'Ⓕ', 'ⓖ' => 'Ⓖ', 'ⓗ' => 'Ⓗ', 'ⓘ' => 'Ⓘ', 'ⓙ' => 'Ⓙ', 'ⓚ' => 'Ⓚ', 'ⓛ' => 'Ⓛ', 'ⓜ' => 'Ⓜ', 'ⓝ' => 'Ⓝ', 'ⓞ' => 'Ⓞ', 'ⓟ' => 'Ⓟ', 'ⓠ' => 'Ⓠ', 'ⓡ' => 'Ⓡ', 'ⓢ' => 'Ⓢ', 'ⓣ' => 'Ⓣ', 'ⓤ' => 'Ⓤ', 'ⓥ' => 'Ⓥ', 'ⓦ' => 'Ⓦ', 'ⓧ' => 'Ⓧ', 'ⓨ' => 'Ⓨ', 'ⓩ' => 'Ⓩ', 'ⰰ' => 'Ⰰ', 'ⰱ' => 'Ⰱ', 'ⰲ' => 'Ⰲ', 'ⰳ' => 'Ⰳ', 'ⰴ' => 'Ⰴ', 'ⰵ' => 'Ⰵ', 'ⰶ' => 'Ⰶ', 'ⰷ' => 'Ⰷ', 'ⰸ' => 'Ⰸ', 'ⰹ' => 'Ⰹ', 'ⰺ' => 'Ⰺ', 'ⰻ' => 'Ⰻ', 'ⰼ' => 'Ⰼ', 'ⰽ' => 'Ⰽ', 'ⰾ' => 'Ⰾ', 'ⰿ' => 'Ⰿ', 'ⱀ' => 'Ⱀ', 'ⱁ' => 'Ⱁ', 'ⱂ' => 'Ⱂ', 'ⱃ' => 'Ⱃ', 'ⱄ' => 'Ⱄ', 'ⱅ' => 'Ⱅ', 'ⱆ' => 'Ⱆ', 'ⱇ' => 'Ⱇ', 'ⱈ' => 'Ⱈ', 'ⱉ' => 'Ⱉ', 'ⱊ' => 'Ⱊ', 'ⱋ' => 'Ⱋ', 'ⱌ' => 'Ⱌ', 'ⱍ' => 'Ⱍ', 'ⱎ' => 'Ⱎ', 'ⱏ' => 'Ⱏ', 'ⱐ' => 'Ⱐ', 'ⱑ' => 'Ⱑ', 'ⱒ' => 'Ⱒ', 'ⱓ' => 'Ⱓ', 'ⱔ' => 'Ⱔ', 'ⱕ' => 'Ⱕ', 'ⱖ' => 'Ⱖ', 'ⱗ' => 'Ⱗ', 'ⱘ' => 'Ⱘ', 'ⱙ' => 'Ⱙ', 'ⱚ' => 'Ⱚ', 'ⱛ' => 'Ⱛ', 'ⱜ' => 'Ⱜ', 'ⱝ' => 'Ⱝ', 'ⱞ' => 'Ⱞ', 'ⱡ' => 'Ⱡ', 'ⱥ' => 'Ⱥ', 'ⱦ' => 'Ⱦ', 'ⱨ' => 'Ⱨ', 'ⱪ' => 'Ⱪ', 'ⱬ' => 'Ⱬ', 'ⱳ' => 'Ⱳ', 'ⱶ' => 'Ⱶ', 'ⲁ' => 'Ⲁ', 'ⲃ' => 'Ⲃ', 'ⲅ' => 'Ⲅ', 'ⲇ' => 'Ⲇ', 'ⲉ' => 'Ⲉ', 'ⲋ' => 'Ⲋ', 'ⲍ' => 'Ⲍ', 'ⲏ' => 'Ⲏ', 'ⲑ' => 'Ⲑ', 'ⲓ' => 'Ⲓ', 'ⲕ' => 'Ⲕ', 'ⲗ' => 'Ⲗ', 'ⲙ' => 'Ⲙ', 'ⲛ' => 'Ⲛ', 'ⲝ' => 'Ⲝ', 'ⲟ' => 'Ⲟ', 'ⲡ' => 'Ⲡ', 'ⲣ' => 'Ⲣ', 'ⲥ' => 'Ⲥ', 'ⲧ' => 'Ⲧ', 'ⲩ' => 'Ⲩ', 'ⲫ' => 'Ⲫ', 'ⲭ' => 'Ⲭ', 'ⲯ' => 'Ⲯ', 'ⲱ' => 'Ⲱ', 'ⲳ' => 'Ⲳ', 'ⲵ' => 'Ⲵ', 'ⲷ' => 'Ⲷ', 'ⲹ' => 'Ⲹ', 'ⲻ' => 'Ⲻ', 'ⲽ' => 'Ⲽ', 'ⲿ' => 'Ⲿ', 'ⳁ' => 'Ⳁ', 'ⳃ' => 'Ⳃ', 'ⳅ' => 'Ⳅ', 'ⳇ' => 'Ⳇ', 'ⳉ' => 'Ⳉ', 'ⳋ' => 'Ⳋ', 'ⳍ' => 'Ⳍ', 'ⳏ' => 'Ⳏ', 'ⳑ' => 'Ⳑ', 'ⳓ' => 'Ⳓ', 'ⳕ' => 'Ⳕ', 'ⳗ' => 'Ⳗ', 'ⳙ' => 'Ⳙ', 'ⳛ' => 'Ⳛ', 'ⳝ' => 'Ⳝ', 'ⳟ' => 'Ⳟ', 'ⳡ' => 'Ⳡ', 'ⳣ' => 'Ⳣ', 'ⳬ' => 'Ⳬ', 'ⳮ' => 'Ⳮ', 'ⳳ' => 'Ⳳ', 'ⴀ' => 'Ⴀ', 'ⴁ' => 'Ⴁ', 'ⴂ' => 'Ⴂ', 'ⴃ' => 'Ⴃ', 'ⴄ' => 'Ⴄ', 'ⴅ' => 'Ⴅ', 'ⴆ' => 'Ⴆ', 'ⴇ' => 'Ⴇ', 'ⴈ' => 'Ⴈ', 'ⴉ' => 'Ⴉ', 'ⴊ' => 'Ⴊ', 'ⴋ' => 'Ⴋ', 'ⴌ' => 'Ⴌ', 'ⴍ' => 'Ⴍ', 'ⴎ' => 'Ⴎ', 'ⴏ' => 'Ⴏ', 'ⴐ' => 'Ⴐ', 'ⴑ' => 'Ⴑ', 'ⴒ' => 'Ⴒ', 'ⴓ' => 'Ⴓ', 'ⴔ' => 'Ⴔ', 'ⴕ' => 'Ⴕ', 'ⴖ' => 'Ⴖ', 'ⴗ' => 'Ⴗ', 'ⴘ' => 'Ⴘ', 'ⴙ' => 'Ⴙ', 'ⴚ' => 'Ⴚ', 'ⴛ' => 'Ⴛ', 'ⴜ' => 'Ⴜ', 'ⴝ' => 'Ⴝ', 'ⴞ' => 'Ⴞ', 'ⴟ' => 'Ⴟ', 'ⴠ' => 'Ⴠ', 'ⴡ' => 'Ⴡ', 'ⴢ' => 'Ⴢ', 'ⴣ' => 'Ⴣ', 'ⴤ' => 'Ⴤ', 'ⴥ' => 'Ⴥ', 'ⴧ' => 'Ⴧ', 'ⴭ' => 'Ⴭ', 'ꙁ' => 'Ꙁ', 'ꙃ' => 'Ꙃ', 'ꙅ' => 'Ꙅ', 'ꙇ' => 'Ꙇ', 'ꙉ' => 'Ꙉ', 'ꙋ' => 'Ꙋ', 'ꙍ' => 'Ꙍ', 'ꙏ' => 'Ꙏ', 'ꙑ' => 'Ꙑ', 'ꙓ' => 'Ꙓ', 'ꙕ' => 'Ꙕ', 'ꙗ' => 'Ꙗ', 'ꙙ' => 'Ꙙ', 'ꙛ' => 'Ꙛ', 'ꙝ' => 'Ꙝ', 'ꙟ' => 'Ꙟ', 'ꙡ' => 'Ꙡ', 'ꙣ' => 'Ꙣ', 'ꙥ' => 'Ꙥ', 'ꙧ' => 'Ꙧ', 'ꙩ' => 'Ꙩ', 'ꙫ' => 'Ꙫ', 'ꙭ' => 'Ꙭ', 'ꚁ' => 'Ꚁ', 'ꚃ' => 'Ꚃ', 'ꚅ' => 'Ꚅ', 'ꚇ' => 'Ꚇ', 'ꚉ' => 'Ꚉ', 'ꚋ' => 'Ꚋ', 'ꚍ' => 'Ꚍ', 'ꚏ' => 'Ꚏ', 'ꚑ' => 'Ꚑ', 'ꚓ' => 'Ꚓ', 'ꚕ' => 'Ꚕ', 'ꚗ' => 'Ꚗ', 'ꚙ' => 'Ꚙ', 'ꚛ' => 'Ꚛ', 'ꜣ' => 'Ꜣ', 'ꜥ' => 'Ꜥ', 'ꜧ' => 'Ꜧ', 'ꜩ' => 'Ꜩ', 'ꜫ' => 'Ꜫ', 'ꜭ' => 'Ꜭ', 'ꜯ' => 'Ꜯ', 'ꜳ' => 'Ꜳ', 'ꜵ' => 'Ꜵ', 'ꜷ' => 'Ꜷ', 'ꜹ' => 'Ꜹ', 'ꜻ' => 'Ꜻ', 'ꜽ' => 'Ꜽ', 'ꜿ' => 'Ꜿ', 'ꝁ' => 'Ꝁ', 'ꝃ' => 'Ꝃ', 'ꝅ' => 'Ꝅ', 'ꝇ' => 'Ꝇ', 'ꝉ' => 'Ꝉ', 'ꝋ' => 'Ꝋ', 'ꝍ' => 'Ꝍ', 'ꝏ' => 'Ꝏ', 'ꝑ' => 'Ꝑ', 'ꝓ' => 'Ꝓ', 'ꝕ' => 'Ꝕ', 'ꝗ' => 'Ꝗ', 'ꝙ' => 'Ꝙ', 'ꝛ' => 'Ꝛ', 'ꝝ' => 'Ꝝ', 'ꝟ' => 'Ꝟ', 'ꝡ' => 'Ꝡ', 'ꝣ' => 'Ꝣ', 'ꝥ' => 'Ꝥ', 'ꝧ' => 'Ꝧ', 'ꝩ' => 'Ꝩ', 'ꝫ' => 'Ꝫ', 'ꝭ' => 'Ꝭ', 'ꝯ' => 'Ꝯ', 'ꝺ' => 'Ꝺ', 'ꝼ' => 'Ꝼ', 'ꝿ' => 'Ꝿ', 'ꞁ' => 'Ꞁ', 'ꞃ' => 'Ꞃ', 'ꞅ' => 'Ꞅ', 'ꞇ' => 'Ꞇ', 'ꞌ' => 'Ꞌ', 'ꞑ' => 'Ꞑ', 'ꞓ' => 'Ꞓ', 'ꞔ' => 'Ꞔ', 'ꞗ' => 'Ꞗ', 'ꞙ' => 'Ꞙ', 'ꞛ' => 'Ꞛ', 'ꞝ' => 'Ꞝ', 'ꞟ' => 'Ꞟ', 'ꞡ' => 'Ꞡ', 'ꞣ' => 'Ꞣ', 'ꞥ' => 'Ꞥ', 'ꞧ' => 'Ꞧ', 'ꞩ' => 'Ꞩ', 'ꞵ' => 'Ꞵ', 'ꞷ' => 'Ꞷ', 'ꞹ' => 'Ꞹ', 'ꞻ' => 'Ꞻ', 'ꞽ' => 'Ꞽ', 'ꞿ' => 'Ꞿ', 'ꟃ' => 'Ꟃ', 'ꟈ' => 'Ꟈ', 'ꟊ' => 'Ꟊ', 'ꟶ' => 'Ꟶ', 'ꭓ' => 'Ꭓ', 'ꭰ' => 'Ꭰ', 'ꭱ' => 'Ꭱ', 'ꭲ' => 'Ꭲ', 'ꭳ' => 'Ꭳ', 'ꭴ' => 'Ꭴ', 'ꭵ' => 'Ꭵ', 'ꭶ' => 'Ꭶ', 'ꭷ' => 'Ꭷ', 'ꭸ' => 'Ꭸ', 'ꭹ' => 'Ꭹ', 'ꭺ' => 'Ꭺ', 'ꭻ' => 'Ꭻ', 'ꭼ' => 'Ꭼ', 'ꭽ' => 'Ꭽ', 'ꭾ' => 'Ꭾ', 'ꭿ' => 'Ꭿ', 'ꮀ' => 'Ꮀ', 'ꮁ' => 'Ꮁ', 'ꮂ' => 'Ꮂ', 'ꮃ' => 'Ꮃ', 'ꮄ' => 'Ꮄ', 'ꮅ' => 'Ꮅ', 'ꮆ' => 'Ꮆ', 'ꮇ' => 'Ꮇ', 'ꮈ' => 'Ꮈ', 'ꮉ' => 'Ꮉ', 'ꮊ' => 'Ꮊ', 'ꮋ' => 'Ꮋ', 'ꮌ' => 'Ꮌ', 'ꮍ' => 'Ꮍ', 'ꮎ' => 'Ꮎ', 'ꮏ' => 'Ꮏ', 'ꮐ' => 'Ꮐ', 'ꮑ' => 'Ꮑ', 'ꮒ' => 'Ꮒ', 'ꮓ' => 'Ꮓ', 'ꮔ' => 'Ꮔ', 'ꮕ' => 'Ꮕ', 'ꮖ' => 'Ꮖ', 'ꮗ' => 'Ꮗ', 'ꮘ' => 'Ꮘ', 'ꮙ' => 'Ꮙ', 'ꮚ' => 'Ꮚ', 'ꮛ' => 'Ꮛ', 'ꮜ' => 'Ꮜ', 'ꮝ' => 'Ꮝ', 'ꮞ' => 'Ꮞ', 'ꮟ' => 'Ꮟ', 'ꮠ' => 'Ꮠ', 'ꮡ' => 'Ꮡ', 'ꮢ' => 'Ꮢ', 'ꮣ' => 'Ꮣ', 'ꮤ' => 'Ꮤ', 'ꮥ' => 'Ꮥ', 'ꮦ' => 'Ꮦ', 'ꮧ' => 'Ꮧ', 'ꮨ' => 'Ꮨ', 'ꮩ' => 'Ꮩ', 'ꮪ' => 'Ꮪ', 'ꮫ' => 'Ꮫ', 'ꮬ' => 'Ꮬ', 'ꮭ' => 'Ꮭ', 'ꮮ' => 'Ꮮ', 'ꮯ' => 'Ꮯ', 'ꮰ' => 'Ꮰ', 'ꮱ' => 'Ꮱ', 'ꮲ' => 'Ꮲ', 'ꮳ' => 'Ꮳ', 'ꮴ' => 'Ꮴ', 'ꮵ' => 'Ꮵ', 'ꮶ' => 'Ꮶ', 'ꮷ' => 'Ꮷ', 'ꮸ' => 'Ꮸ', 'ꮹ' => 'Ꮹ', 'ꮺ' => 'Ꮺ', 'ꮻ' => 'Ꮻ', 'ꮼ' => 'Ꮼ', 'ꮽ' => 'Ꮽ', 'ꮾ' => 'Ꮾ', 'ꮿ' => 'Ꮿ', 'a' => 'A', 'b' => 'B', 'c' => 'C', 'd' => 'D', 'e' => 'E', 'f' => 'F', 'g' => 'G', 'h' => 'H', 'i' => 'I', 'j' => 'J', 'k' => 'K', 'l' => 'L', 'm' => 'M', 'n' => 'N', 'o' => 'O', 'p' => 'P', 'q' => 'Q', 'r' => 'R', 's' => 'S', 't' => 'T', 'u' => 'U', 'v' => 'V', 'w' => 'W', 'x' => 'X', 'y' => 'Y', 'z' => 'Z', '𐐨' => '𐐀', '𐐩' => '𐐁', '𐐪' => '𐐂', '𐐫' => '𐐃', '𐐬' => '𐐄', '𐐭' => '𐐅', '𐐮' => '𐐆', '𐐯' => '𐐇', '𐐰' => '𐐈', '𐐱' => '𐐉', '𐐲' => '𐐊', '𐐳' => '𐐋', '𐐴' => '𐐌', '𐐵' => '𐐍', '𐐶' => '𐐎', '𐐷' => '𐐏', '𐐸' => '𐐐', '𐐹' => '𐐑', '𐐺' => '𐐒', '𐐻' => '𐐓', '𐐼' => '𐐔', '𐐽' => '𐐕', '𐐾' => '𐐖', '𐐿' => '𐐗', '𐑀' => '𐐘', '𐑁' => '𐐙', '𐑂' => '𐐚', '𐑃' => '𐐛', '𐑄' => '𐐜', '𐑅' => '𐐝', '𐑆' => '𐐞', '𐑇' => '𐐟', '𐑈' => '𐐠', '𐑉' => '𐐡', '𐑊' => '𐐢', '𐑋' => '𐐣', '𐑌' => '𐐤', '𐑍' => '𐐥', '𐑎' => '𐐦', '𐑏' => '𐐧', '𐓘' => '𐒰', '𐓙' => '𐒱', '𐓚' => '𐒲', '𐓛' => '𐒳', '𐓜' => '𐒴', '𐓝' => '𐒵', '𐓞' => '𐒶', '𐓟' => '𐒷', '𐓠' => '𐒸', '𐓡' => '𐒹', '𐓢' => '𐒺', '𐓣' => '𐒻', '𐓤' => '𐒼', '𐓥' => '𐒽', '𐓦' => '𐒾', '𐓧' => '𐒿', '𐓨' => '𐓀', '𐓩' => '𐓁', '𐓪' => '𐓂', '𐓫' => '𐓃', '𐓬' => '𐓄', '𐓭' => '𐓅', '𐓮' => '𐓆', '𐓯' => '𐓇', '𐓰' => '𐓈', '𐓱' => '𐓉', '𐓲' => '𐓊', '𐓳' => '𐓋', '𐓴' => '𐓌', '𐓵' => '𐓍', '𐓶' => '𐓎', '𐓷' => '𐓏', '𐓸' => '𐓐', '𐓹' => '𐓑', '𐓺' => '𐓒', '𐓻' => '𐓓', '𐳀' => '𐲀', '𐳁' => '𐲁', '𐳂' => '𐲂', '𐳃' => '𐲃', '𐳄' => '𐲄', '𐳅' => '𐲅', '𐳆' => '𐲆', '𐳇' => '𐲇', '𐳈' => '𐲈', '𐳉' => '𐲉', '𐳊' => '𐲊', '𐳋' => '𐲋', '𐳌' => '𐲌', '𐳍' => '𐲍', '𐳎' => '𐲎', '𐳏' => '𐲏', '𐳐' => '𐲐', '𐳑' => '𐲑', '𐳒' => '𐲒', '𐳓' => '𐲓', '𐳔' => '𐲔', '𐳕' => '𐲕', '𐳖' => '𐲖', '𐳗' => '𐲗', '𐳘' => '𐲘', '𐳙' => '𐲙', '𐳚' => '𐲚', '𐳛' => '𐲛', '𐳜' => '𐲜', '𐳝' => '𐲝', '𐳞' => '𐲞', '𐳟' => '𐲟', '𐳠' => '𐲠', '𐳡' => '𐲡', '𐳢' => '𐲢', '𐳣' => '𐲣', '𐳤' => '𐲤', '𐳥' => '𐲥', '𐳦' => '𐲦', '𐳧' => '𐲧', '𐳨' => '𐲨', '𐳩' => '𐲩', '𐳪' => '𐲪', '𐳫' => '𐲫', '𐳬' => '𐲬', '𐳭' => '𐲭', '𐳮' => '𐲮', '𐳯' => '𐲯', '𐳰' => '𐲰', '𐳱' => '𐲱', '𐳲' => '𐲲', '𑣀' => '𑢠', '𑣁' => '𑢡', '𑣂' => '𑢢', '𑣃' => '𑢣', '𑣄' => '𑢤', '𑣅' => '𑢥', '𑣆' => '𑢦', '𑣇' => '𑢧', '𑣈' => '𑢨', '𑣉' => '𑢩', '𑣊' => '𑢪', '𑣋' => '𑢫', '𑣌' => '𑢬', '𑣍' => '𑢭', '𑣎' => '𑢮', '𑣏' => '𑢯', '𑣐' => '𑢰', '𑣑' => '𑢱', '𑣒' => '𑢲', '𑣓' => '𑢳', '𑣔' => '𑢴', '𑣕' => '𑢵', '𑣖' => '𑢶', '𑣗' => '𑢷', '𑣘' => '𑢸', '𑣙' => '𑢹', '𑣚' => '𑢺', '𑣛' => '𑢻', '𑣜' => '𑢼', '𑣝' => '𑢽', '𑣞' => '𑢾', '𑣟' => '𑢿', '𖹠' => '𖹀', '𖹡' => '𖹁', '𖹢' => '𖹂', '𖹣' => '𖹃', '𖹤' => '𖹄', '𖹥' => '𖹅', '𖹦' => '𖹆', '𖹧' => '𖹇', '𖹨' => '𖹈', '𖹩' => '𖹉', '𖹪' => '𖹊', '𖹫' => '𖹋', '𖹬' => '𖹌', '𖹭' => '𖹍', '𖹮' => '𖹎', '𖹯' => '𖹏', '𖹰' => '𖹐', '𖹱' => '𖹑', '𖹲' => '𖹒', '𖹳' => '𖹓', '𖹴' => '𖹔', '𖹵' => '𖹕', '𖹶' => '𖹖', '𖹷' => '𖹗', '𖹸' => '𖹘', '𖹹' => '𖹙', '𖹺' => '𖹚', '𖹻' => '𖹛', '𖹼' => '𖹜', '𖹽' => '𖹝', '𖹾' => '𖹞', '𖹿' => '𖹟', '𞤢' => '𞤀', '𞤣' => '𞤁', '𞤤' => '𞤂', '𞤥' => '𞤃', '𞤦' => '𞤄', '𞤧' => '𞤅', '𞤨' => '𞤆', '𞤩' => '𞤇', '𞤪' => '𞤈', '𞤫' => '𞤉', '𞤬' => '𞤊', '𞤭' => '𞤋', '𞤮' => '𞤌', '𞤯' => '𞤍', '𞤰' => '𞤎', '𞤱' => '𞤏', '𞤲' => '𞤐', '𞤳' => '𞤑', '𞤴' => '𞤒', '𞤵' => '𞤓', '𞤶' => '𞤔', '𞤷' => '𞤕', '𞤸' => '𞤖', '𞤹' => '𞤗', '𞤺' => '𞤘', '𞤻' => '𞤙', '𞤼' => '𞤚', '𞤽' => '𞤛', '𞤾' => '𞤜', '𞤿' => '𞤝', '𞥀' => '𞤞', '𞥁' => '𞤟', '𞥂' => '𞤠', '𞥃' => '𞤡', ); 'a', 'B' => 'b', 'C' => 'c', 'D' => 'd', 'E' => 'e', 'F' => 'f', 'G' => 'g', 'H' => 'h', 'I' => 'i', 'J' => 'j', 'K' => 'k', 'L' => 'l', 'M' => 'm', 'N' => 'n', 'O' => 'o', 'P' => 'p', 'Q' => 'q', 'R' => 'r', 'S' => 's', 'T' => 't', 'U' => 'u', 'V' => 'v', 'W' => 'w', 'X' => 'x', 'Y' => 'y', 'Z' => 'z', 'À' => 'à', 'Á' => 'á', 'Â' => 'â', 'Ã' => 'ã', 'Ä' => 'ä', 'Å' => 'å', 'Æ' => 'æ', 'Ç' => 'ç', 'È' => 'è', 'É' => 'é', 'Ê' => 'ê', 'Ë' => 'ë', 'Ì' => 'ì', 'Í' => 'í', 'Î' => 'î', 'Ï' => 'ï', 'Ð' => 'ð', 'Ñ' => 'ñ', 'Ò' => 'ò', 'Ó' => 'ó', 'Ô' => 'ô', 'Õ' => 'õ', 'Ö' => 'ö', 'Ø' => 'ø', 'Ù' => 'ù', 'Ú' => 'ú', 'Û' => 'û', 'Ü' => 'ü', 'Ý' => 'ý', 'Þ' => 'þ', 'Ā' => 'ā', 'Ă' => 'ă', 'Ą' => 'ą', 'Ć' => 'ć', 'Ĉ' => 'ĉ', 'Ċ' => 'ċ', 'Č' => 'č', 'Ď' => 'ď', 'Đ' => 'đ', 'Ē' => 'ē', 'Ĕ' => 'ĕ', 'Ė' => 'ė', 'Ę' => 'ę', 'Ě' => 'ě', 'Ĝ' => 'ĝ', 'Ğ' => 'ğ', 'Ġ' => 'ġ', 'Ģ' => 'ģ', 'Ĥ' => 'ĥ', 'Ħ' => 'ħ', 'Ĩ' => 'ĩ', 'Ī' => 'ī', 'Ĭ' => 'ĭ', 'Į' => 'į', 'İ' => 'i', 'IJ' => 'ij', 'Ĵ' => 'ĵ', 'Ķ' => 'ķ', 'Ĺ' => 'ĺ', 'Ļ' => 'ļ', 'Ľ' => 'ľ', 'Ŀ' => 'ŀ', 'Ł' => 'ł', 'Ń' => 'ń', 'Ņ' => 'ņ', 'Ň' => 'ň', 'Ŋ' => 'ŋ', 'Ō' => 'ō', 'Ŏ' => 'ŏ', 'Ő' => 'ő', 'Œ' => 'œ', 'Ŕ' => 'ŕ', 'Ŗ' => 'ŗ', 'Ř' => 'ř', 'Ś' => 'ś', 'Ŝ' => 'ŝ', 'Ş' => 'ş', 'Š' => 'š', 'Ţ' => 'ţ', 'Ť' => 'ť', 'Ŧ' => 'ŧ', 'Ũ' => 'ũ', 'Ū' => 'ū', 'Ŭ' => 'ŭ', 'Ů' => 'ů', 'Ű' => 'ű', 'Ų' => 'ų', 'Ŵ' => 'ŵ', 'Ŷ' => 'ŷ', 'Ÿ' => 'ÿ', 'Ź' => 'ź', 'Ż' => 'ż', 'Ž' => 'ž', 'Ɓ' => 'ɓ', 'Ƃ' => 'ƃ', 'Ƅ' => 'ƅ', 'Ɔ' => 'ɔ', 'Ƈ' => 'ƈ', 'Ɖ' => 'ɖ', 'Ɗ' => 'ɗ', 'Ƌ' => 'ƌ', 'Ǝ' => 'ǝ', 'Ə' => 'ə', 'Ɛ' => 'ɛ', 'Ƒ' => 'ƒ', 'Ɠ' => 'ɠ', 'Ɣ' => 'ɣ', 'Ɩ' => 'ɩ', 'Ɨ' => 'ɨ', 'Ƙ' => 'ƙ', 'Ɯ' => 'ɯ', 'Ɲ' => 'ɲ', 'Ɵ' => 'ɵ', 'Ơ' => 'ơ', 'Ƣ' => 'ƣ', 'Ƥ' => 'ƥ', 'Ʀ' => 'ʀ', 'Ƨ' => 'ƨ', 'Ʃ' => 'ʃ', 'Ƭ' => 'ƭ', 'Ʈ' => 'ʈ', 'Ư' => 'ư', 'Ʊ' => 'ʊ', 'Ʋ' => 'ʋ', 'Ƴ' => 'ƴ', 'Ƶ' => 'ƶ', 'Ʒ' => 'ʒ', 'Ƹ' => 'ƹ', 'Ƽ' => 'ƽ', 'DŽ' => 'dž', 'Dž' => 'dž', 'LJ' => 'lj', 'Lj' => 'lj', 'NJ' => 'nj', 'Nj' => 'nj', 'Ǎ' => 'ǎ', 'Ǐ' => 'ǐ', 'Ǒ' => 'ǒ', 'Ǔ' => 'ǔ', 'Ǖ' => 'ǖ', 'Ǘ' => 'ǘ', 'Ǚ' => 'ǚ', 'Ǜ' => 'ǜ', 'Ǟ' => 'ǟ', 'Ǡ' => 'ǡ', 'Ǣ' => 'ǣ', 'Ǥ' => 'ǥ', 'Ǧ' => 'ǧ', 'Ǩ' => 'ǩ', 'Ǫ' => 'ǫ', 'Ǭ' => 'ǭ', 'Ǯ' => 'ǯ', 'DZ' => 'dz', 'Dz' => 'dz', 'Ǵ' => 'ǵ', 'Ƕ' => 'ƕ', 'Ƿ' => 'ƿ', 'Ǹ' => 'ǹ', 'Ǻ' => 'ǻ', 'Ǽ' => 'ǽ', 'Ǿ' => 'ǿ', 'Ȁ' => 'ȁ', 'Ȃ' => 'ȃ', 'Ȅ' => 'ȅ', 'Ȇ' => 'ȇ', 'Ȉ' => 'ȉ', 'Ȋ' => 'ȋ', 'Ȍ' => 'ȍ', 'Ȏ' => 'ȏ', 'Ȑ' => 'ȑ', 'Ȓ' => 'ȓ', 'Ȕ' => 'ȕ', 'Ȗ' => 'ȗ', 'Ș' => 'ș', 'Ț' => 'ț', 'Ȝ' => 'ȝ', 'Ȟ' => 'ȟ', 'Ƞ' => 'ƞ', 'Ȣ' => 'ȣ', 'Ȥ' => 'ȥ', 'Ȧ' => 'ȧ', 'Ȩ' => 'ȩ', 'Ȫ' => 'ȫ', 'Ȭ' => 'ȭ', 'Ȯ' => 'ȯ', 'Ȱ' => 'ȱ', 'Ȳ' => 'ȳ', 'Ⱥ' => 'ⱥ', 'Ȼ' => 'ȼ', 'Ƚ' => 'ƚ', 'Ⱦ' => 'ⱦ', 'Ɂ' => 'ɂ', 'Ƀ' => 'ƀ', 'Ʉ' => 'ʉ', 'Ʌ' => 'ʌ', 'Ɇ' => 'ɇ', 'Ɉ' => 'ɉ', 'Ɋ' => 'ɋ', 'Ɍ' => 'ɍ', 'Ɏ' => 'ɏ', 'Ͱ' => 'ͱ', 'Ͳ' => 'ͳ', 'Ͷ' => 'ͷ', 'Ϳ' => 'ϳ', 'Ά' => 'ά', 'Έ' => 'έ', 'Ή' => 'ή', 'Ί' => 'ί', 'Ό' => 'ό', 'Ύ' => 'ύ', 'Ώ' => 'ώ', 'Α' => 'α', 'Β' => 'β', 'Γ' => 'γ', 'Δ' => 'δ', 'Ε' => 'ε', 'Ζ' => 'ζ', 'Η' => 'η', 'Θ' => 'θ', 'Ι' => 'ι', 'Κ' => 'κ', 'Λ' => 'λ', 'Μ' => 'μ', 'Ν' => 'ν', 'Ξ' => 'ξ', 'Ο' => 'ο', 'Π' => 'π', 'Ρ' => 'ρ', 'Σ' => 'σ', 'Τ' => 'τ', 'Υ' => 'υ', 'Φ' => 'φ', 'Χ' => 'χ', 'Ψ' => 'ψ', 'Ω' => 'ω', 'Ϊ' => 'ϊ', 'Ϋ' => 'ϋ', 'Ϗ' => 'ϗ', 'Ϙ' => 'ϙ', 'Ϛ' => 'ϛ', 'Ϝ' => 'ϝ', 'Ϟ' => 'ϟ', 'Ϡ' => 'ϡ', 'Ϣ' => 'ϣ', 'Ϥ' => 'ϥ', 'Ϧ' => 'ϧ', 'Ϩ' => 'ϩ', 'Ϫ' => 'ϫ', 'Ϭ' => 'ϭ', 'Ϯ' => 'ϯ', 'ϴ' => 'θ', 'Ϸ' => 'ϸ', 'Ϲ' => 'ϲ', 'Ϻ' => 'ϻ', 'Ͻ' => 'ͻ', 'Ͼ' => 'ͼ', 'Ͽ' => 'ͽ', 'Ѐ' => 'ѐ', 'Ё' => 'ё', 'Ђ' => 'ђ', 'Ѓ' => 'ѓ', 'Є' => 'є', 'Ѕ' => 'ѕ', 'І' => 'і', 'Ї' => 'ї', 'Ј' => 'ј', 'Љ' => 'љ', 'Њ' => 'њ', 'Ћ' => 'ћ', 'Ќ' => 'ќ', 'Ѝ' => 'ѝ', 'Ў' => 'ў', 'Џ' => 'џ', 'А' => 'а', 'Б' => 'б', 'В' => 'в', 'Г' => 'г', 'Д' => 'д', 'Е' => 'е', 'Ж' => 'ж', 'З' => 'з', 'И' => 'и', 'Й' => 'й', 'К' => 'к', 'Л' => 'л', 'М' => 'м', 'Н' => 'н', 'О' => 'о', 'П' => 'п', 'Р' => 'р', 'С' => 'с', 'Т' => 'т', 'У' => 'у', 'Ф' => 'ф', 'Х' => 'х', 'Ц' => 'ц', 'Ч' => 'ч', 'Ш' => 'ш', 'Щ' => 'щ', 'Ъ' => 'ъ', 'Ы' => 'ы', 'Ь' => 'ь', 'Э' => 'э', 'Ю' => 'ю', 'Я' => 'я', 'Ѡ' => 'ѡ', 'Ѣ' => 'ѣ', 'Ѥ' => 'ѥ', 'Ѧ' => 'ѧ', 'Ѩ' => 'ѩ', 'Ѫ' => 'ѫ', 'Ѭ' => 'ѭ', 'Ѯ' => 'ѯ', 'Ѱ' => 'ѱ', 'Ѳ' => 'ѳ', 'Ѵ' => 'ѵ', 'Ѷ' => 'ѷ', 'Ѹ' => 'ѹ', 'Ѻ' => 'ѻ', 'Ѽ' => 'ѽ', 'Ѿ' => 'ѿ', 'Ҁ' => 'ҁ', 'Ҋ' => 'ҋ', 'Ҍ' => 'ҍ', 'Ҏ' => 'ҏ', 'Ґ' => 'ґ', 'Ғ' => 'ғ', 'Ҕ' => 'ҕ', 'Җ' => 'җ', 'Ҙ' => 'ҙ', 'Қ' => 'қ', 'Ҝ' => 'ҝ', 'Ҟ' => 'ҟ', 'Ҡ' => 'ҡ', 'Ң' => 'ң', 'Ҥ' => 'ҥ', 'Ҧ' => 'ҧ', 'Ҩ' => 'ҩ', 'Ҫ' => 'ҫ', 'Ҭ' => 'ҭ', 'Ү' => 'ү', 'Ұ' => 'ұ', 'Ҳ' => 'ҳ', 'Ҵ' => 'ҵ', 'Ҷ' => 'ҷ', 'Ҹ' => 'ҹ', 'Һ' => 'һ', 'Ҽ' => 'ҽ', 'Ҿ' => 'ҿ', 'Ӏ' => 'ӏ', 'Ӂ' => 'ӂ', 'Ӄ' => 'ӄ', 'Ӆ' => 'ӆ', 'Ӈ' => 'ӈ', 'Ӊ' => 'ӊ', 'Ӌ' => 'ӌ', 'Ӎ' => 'ӎ', 'Ӑ' => 'ӑ', 'Ӓ' => 'ӓ', 'Ӕ' => 'ӕ', 'Ӗ' => 'ӗ', 'Ә' => 'ә', 'Ӛ' => 'ӛ', 'Ӝ' => 'ӝ', 'Ӟ' => 'ӟ', 'Ӡ' => 'ӡ', 'Ӣ' => 'ӣ', 'Ӥ' => 'ӥ', 'Ӧ' => 'ӧ', 'Ө' => 'ө', 'Ӫ' => 'ӫ', 'Ӭ' => 'ӭ', 'Ӯ' => 'ӯ', 'Ӱ' => 'ӱ', 'Ӳ' => 'ӳ', 'Ӵ' => 'ӵ', 'Ӷ' => 'ӷ', 'Ӹ' => 'ӹ', 'Ӻ' => 'ӻ', 'Ӽ' => 'ӽ', 'Ӿ' => 'ӿ', 'Ԁ' => 'ԁ', 'Ԃ' => 'ԃ', 'Ԅ' => 'ԅ', 'Ԇ' => 'ԇ', 'Ԉ' => 'ԉ', 'Ԋ' => 'ԋ', 'Ԍ' => 'ԍ', 'Ԏ' => 'ԏ', 'Ԑ' => 'ԑ', 'Ԓ' => 'ԓ', 'Ԕ' => 'ԕ', 'Ԗ' => 'ԗ', 'Ԙ' => 'ԙ', 'Ԛ' => 'ԛ', 'Ԝ' => 'ԝ', 'Ԟ' => 'ԟ', 'Ԡ' => 'ԡ', 'Ԣ' => 'ԣ', 'Ԥ' => 'ԥ', 'Ԧ' => 'ԧ', 'Ԩ' => 'ԩ', 'Ԫ' => 'ԫ', 'Ԭ' => 'ԭ', 'Ԯ' => 'ԯ', 'Ա' => 'ա', 'Բ' => 'բ', 'Գ' => 'գ', 'Դ' => 'դ', 'Ե' => 'ե', 'Զ' => 'զ', 'Է' => 'է', 'Ը' => 'ը', 'Թ' => 'թ', 'Ժ' => 'ժ', 'Ի' => 'ի', 'Լ' => 'լ', 'Խ' => 'խ', 'Ծ' => 'ծ', 'Կ' => 'կ', 'Հ' => 'հ', 'Ձ' => 'ձ', 'Ղ' => 'ղ', 'Ճ' => 'ճ', 'Մ' => 'մ', 'Յ' => 'յ', 'Ն' => 'ն', 'Շ' => 'շ', 'Ո' => 'ո', 'Չ' => 'չ', 'Պ' => 'պ', 'Ջ' => 'ջ', 'Ռ' => 'ռ', 'Ս' => 'ս', 'Վ' => 'վ', 'Տ' => 'տ', 'Ր' => 'ր', 'Ց' => 'ց', 'Ւ' => 'ւ', 'Փ' => 'փ', 'Ք' => 'ք', 'Օ' => 'օ', 'Ֆ' => 'ֆ', 'Ⴀ' => 'ⴀ', 'Ⴁ' => 'ⴁ', 'Ⴂ' => 'ⴂ', 'Ⴃ' => 'ⴃ', 'Ⴄ' => 'ⴄ', 'Ⴅ' => 'ⴅ', 'Ⴆ' => 'ⴆ', 'Ⴇ' => 'ⴇ', 'Ⴈ' => 'ⴈ', 'Ⴉ' => 'ⴉ', 'Ⴊ' => 'ⴊ', 'Ⴋ' => 'ⴋ', 'Ⴌ' => 'ⴌ', 'Ⴍ' => 'ⴍ', 'Ⴎ' => 'ⴎ', 'Ⴏ' => 'ⴏ', 'Ⴐ' => 'ⴐ', 'Ⴑ' => 'ⴑ', 'Ⴒ' => 'ⴒ', 'Ⴓ' => 'ⴓ', 'Ⴔ' => 'ⴔ', 'Ⴕ' => 'ⴕ', 'Ⴖ' => 'ⴖ', 'Ⴗ' => 'ⴗ', 'Ⴘ' => 'ⴘ', 'Ⴙ' => 'ⴙ', 'Ⴚ' => 'ⴚ', 'Ⴛ' => 'ⴛ', 'Ⴜ' => 'ⴜ', 'Ⴝ' => 'ⴝ', 'Ⴞ' => 'ⴞ', 'Ⴟ' => 'ⴟ', 'Ⴠ' => 'ⴠ', 'Ⴡ' => 'ⴡ', 'Ⴢ' => 'ⴢ', 'Ⴣ' => 'ⴣ', 'Ⴤ' => 'ⴤ', 'Ⴥ' => 'ⴥ', 'Ⴧ' => 'ⴧ', 'Ⴭ' => 'ⴭ', 'Ꭰ' => 'ꭰ', 'Ꭱ' => 'ꭱ', 'Ꭲ' => 'ꭲ', 'Ꭳ' => 'ꭳ', 'Ꭴ' => 'ꭴ', 'Ꭵ' => 'ꭵ', 'Ꭶ' => 'ꭶ', 'Ꭷ' => 'ꭷ', 'Ꭸ' => 'ꭸ', 'Ꭹ' => 'ꭹ', 'Ꭺ' => 'ꭺ', 'Ꭻ' => 'ꭻ', 'Ꭼ' => 'ꭼ', 'Ꭽ' => 'ꭽ', 'Ꭾ' => 'ꭾ', 'Ꭿ' => 'ꭿ', 'Ꮀ' => 'ꮀ', 'Ꮁ' => 'ꮁ', 'Ꮂ' => 'ꮂ', 'Ꮃ' => 'ꮃ', 'Ꮄ' => 'ꮄ', 'Ꮅ' => 'ꮅ', 'Ꮆ' => 'ꮆ', 'Ꮇ' => 'ꮇ', 'Ꮈ' => 'ꮈ', 'Ꮉ' => 'ꮉ', 'Ꮊ' => 'ꮊ', 'Ꮋ' => 'ꮋ', 'Ꮌ' => 'ꮌ', 'Ꮍ' => 'ꮍ', 'Ꮎ' => 'ꮎ', 'Ꮏ' => 'ꮏ', 'Ꮐ' => 'ꮐ', 'Ꮑ' => 'ꮑ', 'Ꮒ' => 'ꮒ', 'Ꮓ' => 'ꮓ', 'Ꮔ' => 'ꮔ', 'Ꮕ' => 'ꮕ', 'Ꮖ' => 'ꮖ', 'Ꮗ' => 'ꮗ', 'Ꮘ' => 'ꮘ', 'Ꮙ' => 'ꮙ', 'Ꮚ' => 'ꮚ', 'Ꮛ' => 'ꮛ', 'Ꮜ' => 'ꮜ', 'Ꮝ' => 'ꮝ', 'Ꮞ' => 'ꮞ', 'Ꮟ' => 'ꮟ', 'Ꮠ' => 'ꮠ', 'Ꮡ' => 'ꮡ', 'Ꮢ' => 'ꮢ', 'Ꮣ' => 'ꮣ', 'Ꮤ' => 'ꮤ', 'Ꮥ' => 'ꮥ', 'Ꮦ' => 'ꮦ', 'Ꮧ' => 'ꮧ', 'Ꮨ' => 'ꮨ', 'Ꮩ' => 'ꮩ', 'Ꮪ' => 'ꮪ', 'Ꮫ' => 'ꮫ', 'Ꮬ' => 'ꮬ', 'Ꮭ' => 'ꮭ', 'Ꮮ' => 'ꮮ', 'Ꮯ' => 'ꮯ', 'Ꮰ' => 'ꮰ', 'Ꮱ' => 'ꮱ', 'Ꮲ' => 'ꮲ', 'Ꮳ' => 'ꮳ', 'Ꮴ' => 'ꮴ', 'Ꮵ' => 'ꮵ', 'Ꮶ' => 'ꮶ', 'Ꮷ' => 'ꮷ', 'Ꮸ' => 'ꮸ', 'Ꮹ' => 'ꮹ', 'Ꮺ' => 'ꮺ', 'Ꮻ' => 'ꮻ', 'Ꮼ' => 'ꮼ', 'Ꮽ' => 'ꮽ', 'Ꮾ' => 'ꮾ', 'Ꮿ' => 'ꮿ', 'Ᏸ' => 'ᏸ', 'Ᏹ' => 'ᏹ', 'Ᏺ' => 'ᏺ', 'Ᏻ' => 'ᏻ', 'Ᏼ' => 'ᏼ', 'Ᏽ' => 'ᏽ', 'Ა' => 'ა', 'Ბ' => 'ბ', 'Გ' => 'გ', 'Დ' => 'დ', 'Ე' => 'ე', 'Ვ' => 'ვ', 'Ზ' => 'ზ', 'Თ' => 'თ', 'Ი' => 'ი', 'Კ' => 'კ', 'Ლ' => 'ლ', 'Მ' => 'მ', 'Ნ' => 'ნ', 'Ო' => 'ო', 'Პ' => 'პ', 'Ჟ' => 'ჟ', 'Რ' => 'რ', 'Ს' => 'ს', 'Ტ' => 'ტ', 'Უ' => 'უ', 'Ფ' => 'ფ', 'Ქ' => 'ქ', 'Ღ' => 'ღ', 'Ყ' => 'ყ', 'Შ' => 'შ', 'Ჩ' => 'ჩ', 'Ც' => 'ც', 'Ძ' => 'ძ', 'Წ' => 'წ', 'Ჭ' => 'ჭ', 'Ხ' => 'ხ', 'Ჯ' => 'ჯ', 'Ჰ' => 'ჰ', 'Ჱ' => 'ჱ', 'Ჲ' => 'ჲ', 'Ჳ' => 'ჳ', 'Ჴ' => 'ჴ', 'Ჵ' => 'ჵ', 'Ჶ' => 'ჶ', 'Ჷ' => 'ჷ', 'Ჸ' => 'ჸ', 'Ჹ' => 'ჹ', 'Ჺ' => 'ჺ', 'Ჽ' => 'ჽ', 'Ჾ' => 'ჾ', 'Ჿ' => 'ჿ', 'Ḁ' => 'ḁ', 'Ḃ' => 'ḃ', 'Ḅ' => 'ḅ', 'Ḇ' => 'ḇ', 'Ḉ' => 'ḉ', 'Ḋ' => 'ḋ', 'Ḍ' => 'ḍ', 'Ḏ' => 'ḏ', 'Ḑ' => 'ḑ', 'Ḓ' => 'ḓ', 'Ḕ' => 'ḕ', 'Ḗ' => 'ḗ', 'Ḙ' => 'ḙ', 'Ḛ' => 'ḛ', 'Ḝ' => 'ḝ', 'Ḟ' => 'ḟ', 'Ḡ' => 'ḡ', 'Ḣ' => 'ḣ', 'Ḥ' => 'ḥ', 'Ḧ' => 'ḧ', 'Ḩ' => 'ḩ', 'Ḫ' => 'ḫ', 'Ḭ' => 'ḭ', 'Ḯ' => 'ḯ', 'Ḱ' => 'ḱ', 'Ḳ' => 'ḳ', 'Ḵ' => 'ḵ', 'Ḷ' => 'ḷ', 'Ḹ' => 'ḹ', 'Ḻ' => 'ḻ', 'Ḽ' => 'ḽ', 'Ḿ' => 'ḿ', 'Ṁ' => 'ṁ', 'Ṃ' => 'ṃ', 'Ṅ' => 'ṅ', 'Ṇ' => 'ṇ', 'Ṉ' => 'ṉ', 'Ṋ' => 'ṋ', 'Ṍ' => 'ṍ', 'Ṏ' => 'ṏ', 'Ṑ' => 'ṑ', 'Ṓ' => 'ṓ', 'Ṕ' => 'ṕ', 'Ṗ' => 'ṗ', 'Ṙ' => 'ṙ', 'Ṛ' => 'ṛ', 'Ṝ' => 'ṝ', 'Ṟ' => 'ṟ', 'Ṡ' => 'ṡ', 'Ṣ' => 'ṣ', 'Ṥ' => 'ṥ', 'Ṧ' => 'ṧ', 'Ṩ' => 'ṩ', 'Ṫ' => 'ṫ', 'Ṭ' => 'ṭ', 'Ṯ' => 'ṯ', 'Ṱ' => 'ṱ', 'Ṳ' => 'ṳ', 'Ṵ' => 'ṵ', 'Ṷ' => 'ṷ', 'Ṹ' => 'ṹ', 'Ṻ' => 'ṻ', 'Ṽ' => 'ṽ', 'Ṿ' => 'ṿ', 'Ẁ' => 'ẁ', 'Ẃ' => 'ẃ', 'Ẅ' => 'ẅ', 'Ẇ' => 'ẇ', 'Ẉ' => 'ẉ', 'Ẋ' => 'ẋ', 'Ẍ' => 'ẍ', 'Ẏ' => 'ẏ', 'Ẑ' => 'ẑ', 'Ẓ' => 'ẓ', 'Ẕ' => 'ẕ', 'ẞ' => 'ß', 'Ạ' => 'ạ', 'Ả' => 'ả', 'Ấ' => 'ấ', 'Ầ' => 'ầ', 'Ẩ' => 'ẩ', 'Ẫ' => 'ẫ', 'Ậ' => 'ậ', 'Ắ' => 'ắ', 'Ằ' => 'ằ', 'Ẳ' => 'ẳ', 'Ẵ' => 'ẵ', 'Ặ' => 'ặ', 'Ẹ' => 'ẹ', 'Ẻ' => 'ẻ', 'Ẽ' => 'ẽ', 'Ế' => 'ế', 'Ề' => 'ề', 'Ể' => 'ể', 'Ễ' => 'ễ', 'Ệ' => 'ệ', 'Ỉ' => 'ỉ', 'Ị' => 'ị', 'Ọ' => 'ọ', 'Ỏ' => 'ỏ', 'Ố' => 'ố', 'Ồ' => 'ồ', 'Ổ' => 'ổ', 'Ỗ' => 'ỗ', 'Ộ' => 'ộ', 'Ớ' => 'ớ', 'Ờ' => 'ờ', 'Ở' => 'ở', 'Ỡ' => 'ỡ', 'Ợ' => 'ợ', 'Ụ' => 'ụ', 'Ủ' => 'ủ', 'Ứ' => 'ứ', 'Ừ' => 'ừ', 'Ử' => 'ử', 'Ữ' => 'ữ', 'Ự' => 'ự', 'Ỳ' => 'ỳ', 'Ỵ' => 'ỵ', 'Ỷ' => 'ỷ', 'Ỹ' => 'ỹ', 'Ỻ' => 'ỻ', 'Ỽ' => 'ỽ', 'Ỿ' => 'ỿ', 'Ἀ' => 'ἀ', 'Ἁ' => 'ἁ', 'Ἂ' => 'ἂ', 'Ἃ' => 'ἃ', 'Ἄ' => 'ἄ', 'Ἅ' => 'ἅ', 'Ἆ' => 'ἆ', 'Ἇ' => 'ἇ', 'Ἐ' => 'ἐ', 'Ἑ' => 'ἑ', 'Ἒ' => 'ἒ', 'Ἓ' => 'ἓ', 'Ἔ' => 'ἔ', 'Ἕ' => 'ἕ', 'Ἠ' => 'ἠ', 'Ἡ' => 'ἡ', 'Ἢ' => 'ἢ', 'Ἣ' => 'ἣ', 'Ἤ' => 'ἤ', 'Ἥ' => 'ἥ', 'Ἦ' => 'ἦ', 'Ἧ' => 'ἧ', 'Ἰ' => 'ἰ', 'Ἱ' => 'ἱ', 'Ἲ' => 'ἲ', 'Ἳ' => 'ἳ', 'Ἴ' => 'ἴ', 'Ἵ' => 'ἵ', 'Ἶ' => 'ἶ', 'Ἷ' => 'ἷ', 'Ὀ' => 'ὀ', 'Ὁ' => 'ὁ', 'Ὂ' => 'ὂ', 'Ὃ' => 'ὃ', 'Ὄ' => 'ὄ', 'Ὅ' => 'ὅ', 'Ὑ' => 'ὑ', 'Ὓ' => 'ὓ', 'Ὕ' => 'ὕ', 'Ὗ' => 'ὗ', 'Ὠ' => 'ὠ', 'Ὡ' => 'ὡ', 'Ὢ' => 'ὢ', 'Ὣ' => 'ὣ', 'Ὤ' => 'ὤ', 'Ὥ' => 'ὥ', 'Ὦ' => 'ὦ', 'Ὧ' => 'ὧ', 'ᾈ' => 'ᾀ', 'ᾉ' => 'ᾁ', 'ᾊ' => 'ᾂ', 'ᾋ' => 'ᾃ', 'ᾌ' => 'ᾄ', 'ᾍ' => 'ᾅ', 'ᾎ' => 'ᾆ', 'ᾏ' => 'ᾇ', 'ᾘ' => 'ᾐ', 'ᾙ' => 'ᾑ', 'ᾚ' => 'ᾒ', 'ᾛ' => 'ᾓ', 'ᾜ' => 'ᾔ', 'ᾝ' => 'ᾕ', 'ᾞ' => 'ᾖ', 'ᾟ' => 'ᾗ', 'ᾨ' => 'ᾠ', 'ᾩ' => 'ᾡ', 'ᾪ' => 'ᾢ', 'ᾫ' => 'ᾣ', 'ᾬ' => 'ᾤ', 'ᾭ' => 'ᾥ', 'ᾮ' => 'ᾦ', 'ᾯ' => 'ᾧ', 'Ᾰ' => 'ᾰ', 'Ᾱ' => 'ᾱ', 'Ὰ' => 'ὰ', 'Ά' => 'ά', 'ᾼ' => 'ᾳ', 'Ὲ' => 'ὲ', 'Έ' => 'έ', 'Ὴ' => 'ὴ', 'Ή' => 'ή', 'ῌ' => 'ῃ', 'Ῐ' => 'ῐ', 'Ῑ' => 'ῑ', 'Ὶ' => 'ὶ', 'Ί' => 'ί', 'Ῠ' => 'ῠ', 'Ῡ' => 'ῡ', 'Ὺ' => 'ὺ', 'Ύ' => 'ύ', 'Ῥ' => 'ῥ', 'Ὸ' => 'ὸ', 'Ό' => 'ό', 'Ὼ' => 'ὼ', 'Ώ' => 'ώ', 'ῼ' => 'ῳ', 'Ω' => 'ω', 'K' => 'k', 'Å' => 'å', 'Ⅎ' => 'ⅎ', 'Ⅰ' => 'ⅰ', 'Ⅱ' => 'ⅱ', 'Ⅲ' => 'ⅲ', 'Ⅳ' => 'ⅳ', 'Ⅴ' => 'ⅴ', 'Ⅵ' => 'ⅵ', 'Ⅶ' => 'ⅶ', 'Ⅷ' => 'ⅷ', 'Ⅸ' => 'ⅸ', 'Ⅹ' => 'ⅹ', 'Ⅺ' => 'ⅺ', 'Ⅻ' => 'ⅻ', 'Ⅼ' => 'ⅼ', 'Ⅽ' => 'ⅽ', 'Ⅾ' => 'ⅾ', 'Ⅿ' => 'ⅿ', 'Ↄ' => 'ↄ', 'Ⓐ' => 'ⓐ', 'Ⓑ' => 'ⓑ', 'Ⓒ' => 'ⓒ', 'Ⓓ' => 'ⓓ', 'Ⓔ' => 'ⓔ', 'Ⓕ' => 'ⓕ', 'Ⓖ' => 'ⓖ', 'Ⓗ' => 'ⓗ', 'Ⓘ' => 'ⓘ', 'Ⓙ' => 'ⓙ', 'Ⓚ' => 'ⓚ', 'Ⓛ' => 'ⓛ', 'Ⓜ' => 'ⓜ', 'Ⓝ' => 'ⓝ', 'Ⓞ' => 'ⓞ', 'Ⓟ' => 'ⓟ', 'Ⓠ' => 'ⓠ', 'Ⓡ' => 'ⓡ', 'Ⓢ' => 'ⓢ', 'Ⓣ' => 'ⓣ', 'Ⓤ' => 'ⓤ', 'Ⓥ' => 'ⓥ', 'Ⓦ' => 'ⓦ', 'Ⓧ' => 'ⓧ', 'Ⓨ' => 'ⓨ', 'Ⓩ' => 'ⓩ', 'Ⰰ' => 'ⰰ', 'Ⰱ' => 'ⰱ', 'Ⰲ' => 'ⰲ', 'Ⰳ' => 'ⰳ', 'Ⰴ' => 'ⰴ', 'Ⰵ' => 'ⰵ', 'Ⰶ' => 'ⰶ', 'Ⰷ' => 'ⰷ', 'Ⰸ' => 'ⰸ', 'Ⰹ' => 'ⰹ', 'Ⰺ' => 'ⰺ', 'Ⰻ' => 'ⰻ', 'Ⰼ' => 'ⰼ', 'Ⰽ' => 'ⰽ', 'Ⰾ' => 'ⰾ', 'Ⰿ' => 'ⰿ', 'Ⱀ' => 'ⱀ', 'Ⱁ' => 'ⱁ', 'Ⱂ' => 'ⱂ', 'Ⱃ' => 'ⱃ', 'Ⱄ' => 'ⱄ', 'Ⱅ' => 'ⱅ', 'Ⱆ' => 'ⱆ', 'Ⱇ' => 'ⱇ', 'Ⱈ' => 'ⱈ', 'Ⱉ' => 'ⱉ', 'Ⱊ' => 'ⱊ', 'Ⱋ' => 'ⱋ', 'Ⱌ' => 'ⱌ', 'Ⱍ' => 'ⱍ', 'Ⱎ' => 'ⱎ', 'Ⱏ' => 'ⱏ', 'Ⱐ' => 'ⱐ', 'Ⱑ' => 'ⱑ', 'Ⱒ' => 'ⱒ', 'Ⱓ' => 'ⱓ', 'Ⱔ' => 'ⱔ', 'Ⱕ' => 'ⱕ', 'Ⱖ' => 'ⱖ', 'Ⱗ' => 'ⱗ', 'Ⱘ' => 'ⱘ', 'Ⱙ' => 'ⱙ', 'Ⱚ' => 'ⱚ', 'Ⱛ' => 'ⱛ', 'Ⱜ' => 'ⱜ', 'Ⱝ' => 'ⱝ', 'Ⱞ' => 'ⱞ', 'Ⱡ' => 'ⱡ', 'Ɫ' => 'ɫ', 'Ᵽ' => 'ᵽ', 'Ɽ' => 'ɽ', 'Ⱨ' => 'ⱨ', 'Ⱪ' => 'ⱪ', 'Ⱬ' => 'ⱬ', 'Ɑ' => 'ɑ', 'Ɱ' => 'ɱ', 'Ɐ' => 'ɐ', 'Ɒ' => 'ɒ', 'Ⱳ' => 'ⱳ', 'Ⱶ' => 'ⱶ', 'Ȿ' => 'ȿ', 'Ɀ' => 'ɀ', 'Ⲁ' => 'ⲁ', 'Ⲃ' => 'ⲃ', 'Ⲅ' => 'ⲅ', 'Ⲇ' => 'ⲇ', 'Ⲉ' => 'ⲉ', 'Ⲋ' => 'ⲋ', 'Ⲍ' => 'ⲍ', 'Ⲏ' => 'ⲏ', 'Ⲑ' => 'ⲑ', 'Ⲓ' => 'ⲓ', 'Ⲕ' => 'ⲕ', 'Ⲗ' => 'ⲗ', 'Ⲙ' => 'ⲙ', 'Ⲛ' => 'ⲛ', 'Ⲝ' => 'ⲝ', 'Ⲟ' => 'ⲟ', 'Ⲡ' => 'ⲡ', 'Ⲣ' => 'ⲣ', 'Ⲥ' => 'ⲥ', 'Ⲧ' => 'ⲧ', 'Ⲩ' => 'ⲩ', 'Ⲫ' => 'ⲫ', 'Ⲭ' => 'ⲭ', 'Ⲯ' => 'ⲯ', 'Ⲱ' => 'ⲱ', 'Ⲳ' => 'ⲳ', 'Ⲵ' => 'ⲵ', 'Ⲷ' => 'ⲷ', 'Ⲹ' => 'ⲹ', 'Ⲻ' => 'ⲻ', 'Ⲽ' => 'ⲽ', 'Ⲿ' => 'ⲿ', 'Ⳁ' => 'ⳁ', 'Ⳃ' => 'ⳃ', 'Ⳅ' => 'ⳅ', 'Ⳇ' => 'ⳇ', 'Ⳉ' => 'ⳉ', 'Ⳋ' => 'ⳋ', 'Ⳍ' => 'ⳍ', 'Ⳏ' => 'ⳏ', 'Ⳑ' => 'ⳑ', 'Ⳓ' => 'ⳓ', 'Ⳕ' => 'ⳕ', 'Ⳗ' => 'ⳗ', 'Ⳙ' => 'ⳙ', 'Ⳛ' => 'ⳛ', 'Ⳝ' => 'ⳝ', 'Ⳟ' => 'ⳟ', 'Ⳡ' => 'ⳡ', 'Ⳣ' => 'ⳣ', 'Ⳬ' => 'ⳬ', 'Ⳮ' => 'ⳮ', 'Ⳳ' => 'ⳳ', 'Ꙁ' => 'ꙁ', 'Ꙃ' => 'ꙃ', 'Ꙅ' => 'ꙅ', 'Ꙇ' => 'ꙇ', 'Ꙉ' => 'ꙉ', 'Ꙋ' => 'ꙋ', 'Ꙍ' => 'ꙍ', 'Ꙏ' => 'ꙏ', 'Ꙑ' => 'ꙑ', 'Ꙓ' => 'ꙓ', 'Ꙕ' => 'ꙕ', 'Ꙗ' => 'ꙗ', 'Ꙙ' => 'ꙙ', 'Ꙛ' => 'ꙛ', 'Ꙝ' => 'ꙝ', 'Ꙟ' => 'ꙟ', 'Ꙡ' => 'ꙡ', 'Ꙣ' => 'ꙣ', 'Ꙥ' => 'ꙥ', 'Ꙧ' => 'ꙧ', 'Ꙩ' => 'ꙩ', 'Ꙫ' => 'ꙫ', 'Ꙭ' => 'ꙭ', 'Ꚁ' => 'ꚁ', 'Ꚃ' => 'ꚃ', 'Ꚅ' => 'ꚅ', 'Ꚇ' => 'ꚇ', 'Ꚉ' => 'ꚉ', 'Ꚋ' => 'ꚋ', 'Ꚍ' => 'ꚍ', 'Ꚏ' => 'ꚏ', 'Ꚑ' => 'ꚑ', 'Ꚓ' => 'ꚓ', 'Ꚕ' => 'ꚕ', 'Ꚗ' => 'ꚗ', 'Ꚙ' => 'ꚙ', 'Ꚛ' => 'ꚛ', 'Ꜣ' => 'ꜣ', 'Ꜥ' => 'ꜥ', 'Ꜧ' => 'ꜧ', 'Ꜩ' => 'ꜩ', 'Ꜫ' => 'ꜫ', 'Ꜭ' => 'ꜭ', 'Ꜯ' => 'ꜯ', 'Ꜳ' => 'ꜳ', 'Ꜵ' => 'ꜵ', 'Ꜷ' => 'ꜷ', 'Ꜹ' => 'ꜹ', 'Ꜻ' => 'ꜻ', 'Ꜽ' => 'ꜽ', 'Ꜿ' => 'ꜿ', 'Ꝁ' => 'ꝁ', 'Ꝃ' => 'ꝃ', 'Ꝅ' => 'ꝅ', 'Ꝇ' => 'ꝇ', 'Ꝉ' => 'ꝉ', 'Ꝋ' => 'ꝋ', 'Ꝍ' => 'ꝍ', 'Ꝏ' => 'ꝏ', 'Ꝑ' => 'ꝑ', 'Ꝓ' => 'ꝓ', 'Ꝕ' => 'ꝕ', 'Ꝗ' => 'ꝗ', 'Ꝙ' => 'ꝙ', 'Ꝛ' => 'ꝛ', 'Ꝝ' => 'ꝝ', 'Ꝟ' => 'ꝟ', 'Ꝡ' => 'ꝡ', 'Ꝣ' => 'ꝣ', 'Ꝥ' => 'ꝥ', 'Ꝧ' => 'ꝧ', 'Ꝩ' => 'ꝩ', 'Ꝫ' => 'ꝫ', 'Ꝭ' => 'ꝭ', 'Ꝯ' => 'ꝯ', 'Ꝺ' => 'ꝺ', 'Ꝼ' => 'ꝼ', 'Ᵹ' => 'ᵹ', 'Ꝿ' => 'ꝿ', 'Ꞁ' => 'ꞁ', 'Ꞃ' => 'ꞃ', 'Ꞅ' => 'ꞅ', 'Ꞇ' => 'ꞇ', 'Ꞌ' => 'ꞌ', 'Ɥ' => 'ɥ', 'Ꞑ' => 'ꞑ', 'Ꞓ' => 'ꞓ', 'Ꞗ' => 'ꞗ', 'Ꞙ' => 'ꞙ', 'Ꞛ' => 'ꞛ', 'Ꞝ' => 'ꞝ', 'Ꞟ' => 'ꞟ', 'Ꞡ' => 'ꞡ', 'Ꞣ' => 'ꞣ', 'Ꞥ' => 'ꞥ', 'Ꞧ' => 'ꞧ', 'Ꞩ' => 'ꞩ', 'Ɦ' => 'ɦ', 'Ɜ' => 'ɜ', 'Ɡ' => 'ɡ', 'Ɬ' => 'ɬ', 'Ɪ' => 'ɪ', 'Ʞ' => 'ʞ', 'Ʇ' => 'ʇ', 'Ʝ' => 'ʝ', 'Ꭓ' => 'ꭓ', 'Ꞵ' => 'ꞵ', 'Ꞷ' => 'ꞷ', 'Ꞹ' => 'ꞹ', 'Ꞻ' => 'ꞻ', 'Ꞽ' => 'ꞽ', 'Ꞿ' => 'ꞿ', 'Ꟃ' => 'ꟃ', 'Ꞔ' => 'ꞔ', 'Ʂ' => 'ʂ', 'Ᶎ' => 'ᶎ', 'Ꟈ' => 'ꟈ', 'Ꟊ' => 'ꟊ', 'Ꟶ' => 'ꟶ', 'A' => 'a', 'B' => 'b', 'C' => 'c', 'D' => 'd', 'E' => 'e', 'F' => 'f', 'G' => 'g', 'H' => 'h', 'I' => 'i', 'J' => 'j', 'K' => 'k', 'L' => 'l', 'M' => 'm', 'N' => 'n', 'O' => 'o', 'P' => 'p', 'Q' => 'q', 'R' => 'r', 'S' => 's', 'T' => 't', 'U' => 'u', 'V' => 'v', 'W' => 'w', 'X' => 'x', 'Y' => 'y', 'Z' => 'z', '𐐀' => '𐐨', '𐐁' => '𐐩', '𐐂' => '𐐪', '𐐃' => '𐐫', '𐐄' => '𐐬', '𐐅' => '𐐭', '𐐆' => '𐐮', '𐐇' => '𐐯', '𐐈' => '𐐰', '𐐉' => '𐐱', '𐐊' => '𐐲', '𐐋' => '𐐳', '𐐌' => '𐐴', '𐐍' => '𐐵', '𐐎' => '𐐶', '𐐏' => '𐐷', '𐐐' => '𐐸', '𐐑' => '𐐹', '𐐒' => '𐐺', '𐐓' => '𐐻', '𐐔' => '𐐼', '𐐕' => '𐐽', '𐐖' => '𐐾', '𐐗' => '𐐿', '𐐘' => '𐑀', '𐐙' => '𐑁', '𐐚' => '𐑂', '𐐛' => '𐑃', '𐐜' => '𐑄', '𐐝' => '𐑅', '𐐞' => '𐑆', '𐐟' => '𐑇', '𐐠' => '𐑈', '𐐡' => '𐑉', '𐐢' => '𐑊', '𐐣' => '𐑋', '𐐤' => '𐑌', '𐐥' => '𐑍', '𐐦' => '𐑎', '𐐧' => '𐑏', '𐒰' => '𐓘', '𐒱' => '𐓙', '𐒲' => '𐓚', '𐒳' => '𐓛', '𐒴' => '𐓜', '𐒵' => '𐓝', '𐒶' => '𐓞', '𐒷' => '𐓟', '𐒸' => '𐓠', '𐒹' => '𐓡', '𐒺' => '𐓢', '𐒻' => '𐓣', '𐒼' => '𐓤', '𐒽' => '𐓥', '𐒾' => '𐓦', '𐒿' => '𐓧', '𐓀' => '𐓨', '𐓁' => '𐓩', '𐓂' => '𐓪', '𐓃' => '𐓫', '𐓄' => '𐓬', '𐓅' => '𐓭', '𐓆' => '𐓮', '𐓇' => '𐓯', '𐓈' => '𐓰', '𐓉' => '𐓱', '𐓊' => '𐓲', '𐓋' => '𐓳', '𐓌' => '𐓴', '𐓍' => '𐓵', '𐓎' => '𐓶', '𐓏' => '𐓷', '𐓐' => '𐓸', '𐓑' => '𐓹', '𐓒' => '𐓺', '𐓓' => '𐓻', '𐲀' => '𐳀', '𐲁' => '𐳁', '𐲂' => '𐳂', '𐲃' => '𐳃', '𐲄' => '𐳄', '𐲅' => '𐳅', '𐲆' => '𐳆', '𐲇' => '𐳇', '𐲈' => '𐳈', '𐲉' => '𐳉', '𐲊' => '𐳊', '𐲋' => '𐳋', '𐲌' => '𐳌', '𐲍' => '𐳍', '𐲎' => '𐳎', '𐲏' => '𐳏', '𐲐' => '𐳐', '𐲑' => '𐳑', '𐲒' => '𐳒', '𐲓' => '𐳓', '𐲔' => '𐳔', '𐲕' => '𐳕', '𐲖' => '𐳖', '𐲗' => '𐳗', '𐲘' => '𐳘', '𐲙' => '𐳙', '𐲚' => '𐳚', '𐲛' => '𐳛', '𐲜' => '𐳜', '𐲝' => '𐳝', '𐲞' => '𐳞', '𐲟' => '𐳟', '𐲠' => '𐳠', '𐲡' => '𐳡', '𐲢' => '𐳢', '𐲣' => '𐳣', '𐲤' => '𐳤', '𐲥' => '𐳥', '𐲦' => '𐳦', '𐲧' => '𐳧', '𐲨' => '𐳨', '𐲩' => '𐳩', '𐲪' => '𐳪', '𐲫' => '𐳫', '𐲬' => '𐳬', '𐲭' => '𐳭', '𐲮' => '𐳮', '𐲯' => '𐳯', '𐲰' => '𐳰', '𐲱' => '𐳱', '𐲲' => '𐳲', '𑢠' => '𑣀', '𑢡' => '𑣁', '𑢢' => '𑣂', '𑢣' => '𑣃', '𑢤' => '𑣄', '𑢥' => '𑣅', '𑢦' => '𑣆', '𑢧' => '𑣇', '𑢨' => '𑣈', '𑢩' => '𑣉', '𑢪' => '𑣊', '𑢫' => '𑣋', '𑢬' => '𑣌', '𑢭' => '𑣍', '𑢮' => '𑣎', '𑢯' => '𑣏', '𑢰' => '𑣐', '𑢱' => '𑣑', '𑢲' => '𑣒', '𑢳' => '𑣓', '𑢴' => '𑣔', '𑢵' => '𑣕', '𑢶' => '𑣖', '𑢷' => '𑣗', '𑢸' => '𑣘', '𑢹' => '𑣙', '𑢺' => '𑣚', '𑢻' => '𑣛', '𑢼' => '𑣜', '𑢽' => '𑣝', '𑢾' => '𑣞', '𑢿' => '𑣟', '𖹀' => '𖹠', '𖹁' => '𖹡', '𖹂' => '𖹢', '𖹃' => '𖹣', '𖹄' => '𖹤', '𖹅' => '𖹥', '𖹆' => '𖹦', '𖹇' => '𖹧', '𖹈' => '𖹨', '𖹉' => '𖹩', '𖹊' => '𖹪', '𖹋' => '𖹫', '𖹌' => '𖹬', '𖹍' => '𖹭', '𖹎' => '𖹮', '𖹏' => '𖹯', '𖹐' => '𖹰', '𖹑' => '𖹱', '𖹒' => '𖹲', '𖹓' => '𖹳', '𖹔' => '𖹴', '𖹕' => '𖹵', '𖹖' => '𖹶', '𖹗' => '𖹷', '𖹘' => '𖹸', '𖹙' => '𖹹', '𖹚' => '𖹺', '𖹛' => '𖹻', '𖹜' => '𖹼', '𖹝' => '𖹽', '𖹞' => '𖹾', '𖹟' => '𖹿', '𞤀' => '𞤢', '𞤁' => '𞤣', '𞤂' => '𞤤', '𞤃' => '𞤥', '𞤄' => '𞤦', '𞤅' => '𞤧', '𞤆' => '𞤨', '𞤇' => '𞤩', '𞤈' => '𞤪', '𞤉' => '𞤫', '𞤊' => '𞤬', '𞤋' => '𞤭', '𞤌' => '𞤮', '𞤍' => '𞤯', '𞤎' => '𞤰', '𞤏' => '𞤱', '𞤐' => '𞤲', '𞤑' => '𞤳', '𞤒' => '𞤴', '𞤓' => '𞤵', '𞤔' => '𞤶', '𞤕' => '𞤷', '𞤖' => '𞤸', '𞤗' => '𞤹', '𞤘' => '𞤺', '𞤙' => '𞤻', '𞤚' => '𞤼', '𞤛' => '𞤽', '𞤜' => '𞤾', '𞤝' => '𞤿', '𞤞' => '𞥀', '𞤟' => '𞥁', '𞤠' => '𞥂', '𞤡' => '𞥃', ); * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ use Symfony\Polyfill\Mbstring as p; if (!function_exists('mb_convert_encoding')) { function mb_convert_encoding($string, $to_encoding, $from_encoding = null) { return p\Mbstring::mb_convert_encoding($string, $to_encoding, $from_encoding); } } if (!function_exists('mb_decode_mimeheader')) { function mb_decode_mimeheader($string) { return p\Mbstring::mb_decode_mimeheader($string); } } if (!function_exists('mb_encode_mimeheader')) { function mb_encode_mimeheader($string, $charset = null, $transfer_encoding = null, $newline = null, $indent = null) { return p\Mbstring::mb_encode_mimeheader($string, $charset, $transfer_encoding, $newline, $indent); } } if (!function_exists('mb_decode_numericentity')) { function mb_decode_numericentity($string, $map, $encoding = null) { return p\Mbstring::mb_decode_numericentity($string, $map, $encoding); } } if (!function_exists('mb_encode_numericentity')) { function mb_encode_numericentity($string, $map, $encoding = null, $hex = false) { return p\Mbstring::mb_encode_numericentity($string, $map, $encoding, $hex); } } if (!function_exists('mb_convert_case')) { function mb_convert_case($string, $mode, $encoding = null) { return p\Mbstring::mb_convert_case($string, $mode, $encoding); } } if (!function_exists('mb_internal_encoding')) { function mb_internal_encoding($encoding = null) { return p\Mbstring::mb_internal_encoding($encoding); } } if (!function_exists('mb_language')) { function mb_language($language = null) { return p\Mbstring::mb_language($language); } } if (!function_exists('mb_list_encodings')) { function mb_list_encodings() { return p\Mbstring::mb_list_encodings(); } } if (!function_exists('mb_encoding_aliases')) { function mb_encoding_aliases($encoding) { return p\Mbstring::mb_encoding_aliases($encoding); } } if (!function_exists('mb_check_encoding')) { function mb_check_encoding($value = null, $encoding = null) { return p\Mbstring::mb_check_encoding($value, $encoding); } } if (!function_exists('mb_detect_encoding')) { function mb_detect_encoding($string, $encodings = null, $strict = false) { return p\Mbstring::mb_detect_encoding($string, $encodings, $strict); } } if (!function_exists('mb_detect_order')) { function mb_detect_order($encoding = null) { return p\Mbstring::mb_detect_order($encoding); } } if (!function_exists('mb_parse_str')) { function mb_parse_str($string, &$result = array()) { parse_str($string, $result); } } if (!function_exists('mb_strlen')) { function mb_strlen($string, $encoding = null) { return p\Mbstring::mb_strlen($string, $encoding); } } if (!function_exists('mb_strpos')) { function mb_strpos($haystack, $needle, $offset = 0, $encoding = null) { return p\Mbstring::mb_strpos($haystack, $needle, $offset, $encoding); } } if (!function_exists('mb_strtolower')) { function mb_strtolower($string, $encoding = null) { return p\Mbstring::mb_strtolower($string, $encoding); } } if (!function_exists('mb_strtoupper')) { function mb_strtoupper($string, $encoding = null) { return p\Mbstring::mb_strtoupper($string, $encoding); } } if (!function_exists('mb_substitute_character')) { function mb_substitute_character($substitute_character = null) { return p\Mbstring::mb_substitute_character($substitute_character); } } if (!function_exists('mb_substr')) { function mb_substr($string, $start, $length = 2147483647, $encoding = null) { return p\Mbstring::mb_substr($string, $start, $length, $encoding); } } if (!function_exists('mb_stripos')) { function mb_stripos($haystack, $needle, $offset = 0, $encoding = null) { return p\Mbstring::mb_stripos($haystack, $needle, $offset, $encoding); } } if (!function_exists('mb_stristr')) { function mb_stristr($haystack, $needle, $before_needle = false, $encoding = null) { return p\Mbstring::mb_stristr($haystack, $needle, $before_needle, $encoding); } } if (!function_exists('mb_strrchr')) { function mb_strrchr($haystack, $needle, $before_needle = false, $encoding = null) { return p\Mbstring::mb_strrchr($haystack, $needle, $before_needle, $encoding); } } if (!function_exists('mb_strrichr')) { function mb_strrichr($haystack, $needle, $before_needle = false, $encoding = null) { return p\Mbstring::mb_strrichr($haystack, $needle, $before_needle, $encoding); } } if (!function_exists('mb_strripos')) { function mb_strripos($haystack, $needle, $offset = 0, $encoding = null) { return p\Mbstring::mb_strripos($haystack, $needle, $offset, $encoding); } } if (!function_exists('mb_strrpos')) { function mb_strrpos($haystack, $needle, $offset = 0, $encoding = null) { return p\Mbstring::mb_strrpos($haystack, $needle, $offset, $encoding); } } if (!function_exists('mb_strstr')) { function mb_strstr($haystack, $needle, $before_needle = false, $encoding = null) { return p\Mbstring::mb_strstr($haystack, $needle, $before_needle, $encoding); } } if (!function_exists('mb_get_info')) { function mb_get_info($type = 'all') { return p\Mbstring::mb_get_info($type); } } if (!function_exists('mb_http_output')) { function mb_http_output($encoding = null) { return p\Mbstring::mb_http_output($encoding); } } if (!function_exists('mb_strwidth')) { function mb_strwidth($string, $encoding = null) { return p\Mbstring::mb_strwidth($string, $encoding); } } if (!function_exists('mb_substr_count')) { function mb_substr_count($haystack, $needle, $encoding = null) { return p\Mbstring::mb_substr_count($haystack, $needle, $encoding); } } if (!function_exists('mb_output_handler')) { function mb_output_handler($string, $status) { return p\Mbstring::mb_output_handler($string, $status); } } if (!function_exists('mb_http_input')) { function mb_http_input($type = '') { return p\Mbstring::mb_http_input($type); } } if (PHP_VERSION_ID >= 80000) { require_once __DIR__.'/Resources/mb_convert_variables.php8'; } elseif (!function_exists('mb_convert_variables')) { function mb_convert_variables($toEncoding, $fromEncoding, &$a = null, &$b = null, &$c = null, &$d = null, &$e = null, &$f = null) { return p\Mbstring::mb_convert_variables($toEncoding, $fromEncoding, $a, $b, $c, $d, $e, $f); } } if (!function_exists('mb_ord')) { function mb_ord($string, $encoding = null) { return p\Mbstring::mb_ord($string, $encoding); } } if (!function_exists('mb_chr')) { function mb_chr($codepoint, $encoding = null) { return p\Mbstring::mb_chr($codepoint, $encoding); } } if (!function_exists('mb_scrub')) { function mb_scrub($string, $encoding = null) { $encoding = null === $encoding ? mb_internal_encoding() : $encoding; return mb_convert_encoding($string, $encoding, $encoding); } } if (!function_exists('mb_str_split')) { function mb_str_split($string, $length = 1, $encoding = null) { return p\Mbstring::mb_str_split($string, $length, $encoding); } } if (extension_loaded('mbstring')) { return; } if (!defined('MB_CASE_UPPER')) { define('MB_CASE_UPPER', 0); } if (!defined('MB_CASE_LOWER')) { define('MB_CASE_LOWER', 1); } if (!defined('MB_CASE_TITLE')) { define('MB_CASE_TITLE', 2); } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Polyfill\Mbstring; /** * Partial mbstring implementation in PHP, iconv based, UTF-8 centric. * * Implemented: * - mb_chr - Returns a specific character from its Unicode code point * - mb_convert_encoding - Convert character encoding * - mb_convert_variables - Convert character code in variable(s) * - mb_decode_mimeheader - Decode string in MIME header field * - mb_encode_mimeheader - Encode string for MIME header XXX NATIVE IMPLEMENTATION IS REALLY BUGGED * - mb_decode_numericentity - Decode HTML numeric string reference to character * - mb_encode_numericentity - Encode character to HTML numeric string reference * - mb_convert_case - Perform case folding on a string * - mb_detect_encoding - Detect character encoding * - mb_get_info - Get internal settings of mbstring * - mb_http_input - Detect HTTP input character encoding * - mb_http_output - Set/Get HTTP output character encoding * - mb_internal_encoding - Set/Get internal character encoding * - mb_list_encodings - Returns an array of all supported encodings * - mb_ord - Returns the Unicode code point of a character * - mb_output_handler - Callback function converts character encoding in output buffer * - mb_scrub - Replaces ill-formed byte sequences with substitute characters * - mb_strlen - Get string length * - mb_strpos - Find position of first occurrence of string in a string * - mb_strrpos - Find position of last occurrence of a string in a string * - mb_str_split - Convert a string to an array * - mb_strtolower - Make a string lowercase * - mb_strtoupper - Make a string uppercase * - mb_substitute_character - Set/Get substitution character * - mb_substr - Get part of string * - mb_stripos - Finds position of first occurrence of a string within another, case insensitive * - mb_stristr - Finds first occurrence of a string within another, case insensitive * - mb_strrchr - Finds the last occurrence of a character in a string within another * - mb_strrichr - Finds the last occurrence of a character in a string within another, case insensitive * - mb_strripos - Finds position of last occurrence of a string within another, case insensitive * - mb_strstr - Finds first occurrence of a string within another * - mb_strwidth - Return width of string * - mb_substr_count - Count the number of substring occurrences * * Not implemented: * - mb_convert_kana - Convert "kana" one from another ("zen-kaku", "han-kaku" and more) * - mb_ereg_* - Regular expression with multibyte support * - mb_parse_str - Parse GET/POST/COOKIE data and set global variable * - mb_preferred_mime_name - Get MIME charset string * - mb_regex_encoding - Returns current encoding for multibyte regex as string * - mb_regex_set_options - Set/Get the default options for mbregex functions * - mb_send_mail - Send encoded mail * - mb_split - Split multibyte string using regular expression * - mb_strcut - Get part of string * - mb_strimwidth - Get truncated string with specified width * * @author Nicolas Grekas * * @internal */ final class Mbstring { const MB_CASE_FOLD = PHP_INT_MAX; private static $encodingList = array('ASCII', 'UTF-8'); private static $language = 'neutral'; private static $internalEncoding = 'UTF-8'; private static $caseFold = array( array('µ', 'ſ', "\xCD\x85", 'ς', "\xCF\x90", "\xCF\x91", "\xCF\x95", "\xCF\x96", "\xCF\xB0", "\xCF\xB1", "\xCF\xB5", "\xE1\xBA\x9B", "\xE1\xBE\xBE"), array('μ', 's', 'ι', 'σ', 'β', 'θ', 'φ', 'π', 'κ', 'ρ', 'ε', "\xE1\xB9\xA1", 'ι'), ); public static function mb_convert_encoding($s, $toEncoding, $fromEncoding = null) { if (\is_array($fromEncoding) || false !== strpos($fromEncoding, ',')) { $fromEncoding = self::mb_detect_encoding($s, $fromEncoding); } else { $fromEncoding = self::getEncoding($fromEncoding); } $toEncoding = self::getEncoding($toEncoding); if ('BASE64' === $fromEncoding) { $s = base64_decode($s); $fromEncoding = $toEncoding; } if ('BASE64' === $toEncoding) { return base64_encode($s); } if ('HTML-ENTITIES' === $toEncoding || 'HTML' === $toEncoding) { if ('HTML-ENTITIES' === $fromEncoding || 'HTML' === $fromEncoding) { $fromEncoding = 'Windows-1252'; } if ('UTF-8' !== $fromEncoding) { $s = iconv($fromEncoding, 'UTF-8//IGNORE', $s); } return preg_replace_callback('/[\x80-\xFF]+/', array(__CLASS__, 'html_encoding_callback'), $s); } if ('HTML-ENTITIES' === $fromEncoding) { $s = html_entity_decode($s, ENT_COMPAT, 'UTF-8'); $fromEncoding = 'UTF-8'; } return iconv($fromEncoding, $toEncoding.'//IGNORE', $s); } public static function mb_convert_variables($toEncoding, $fromEncoding, &$a = null, &$b = null, &$c = null, &$d = null, &$e = null, &$f = null) { $vars = array(&$a, &$b, &$c, &$d, &$e, &$f); $ok = true; array_walk_recursive($vars, function (&$v) use (&$ok, $toEncoding, $fromEncoding) { if (false === $v = Mbstring::mb_convert_encoding($v, $toEncoding, $fromEncoding)) { $ok = false; } }); return $ok ? $fromEncoding : false; } public static function mb_decode_mimeheader($s) { return iconv_mime_decode($s, 2, self::$internalEncoding); } public static function mb_encode_mimeheader($s, $charset = null, $transferEncoding = null, $linefeed = null, $indent = null) { trigger_error('mb_encode_mimeheader() is bugged. Please use iconv_mime_encode() instead', E_USER_WARNING); } public static function mb_decode_numericentity($s, $convmap, $encoding = null) { if (null !== $s && !\is_scalar($s) && !(\is_object($s) && \method_exists($s, '__toString'))) { trigger_error('mb_decode_numericentity() expects parameter 1 to be string, '.\gettype($s).' given', E_USER_WARNING); return null; } if (!\is_array($convmap) || !$convmap) { return false; } if (null !== $encoding && !\is_scalar($encoding)) { trigger_error('mb_decode_numericentity() expects parameter 3 to be string, '.\gettype($s).' given', E_USER_WARNING); return ''; // Instead of null (cf. mb_encode_numericentity). } $s = (string) $s; if ('' === $s) { return ''; } $encoding = self::getEncoding($encoding); if ('UTF-8' === $encoding) { $encoding = null; if (!preg_match('//u', $s)) { $s = @iconv('UTF-8', 'UTF-8//IGNORE', $s); } } else { $s = iconv($encoding, 'UTF-8//IGNORE', $s); } $cnt = floor(\count($convmap) / 4) * 4; for ($i = 0; $i < $cnt; $i += 4) { // collector_decode_htmlnumericentity ignores $convmap[$i + 3] $convmap[$i] += $convmap[$i + 2]; $convmap[$i + 1] += $convmap[$i + 2]; } $s = preg_replace_callback('/&#(?:0*([0-9]+)|x0*([0-9a-fA-F]+))(?!&);?/', function (array $m) use ($cnt, $convmap) { $c = isset($m[2]) ? (int) hexdec($m[2]) : $m[1]; for ($i = 0; $i < $cnt; $i += 4) { if ($c >= $convmap[$i] && $c <= $convmap[$i + 1]) { return Mbstring::mb_chr($c - $convmap[$i + 2]); } } return $m[0]; }, $s); if (null === $encoding) { return $s; } return iconv('UTF-8', $encoding.'//IGNORE', $s); } public static function mb_encode_numericentity($s, $convmap, $encoding = null, $is_hex = false) { if (null !== $s && !\is_scalar($s) && !(\is_object($s) && \method_exists($s, '__toString'))) { trigger_error('mb_encode_numericentity() expects parameter 1 to be string, '.\gettype($s).' given', E_USER_WARNING); return null; } if (!\is_array($convmap) || !$convmap) { return false; } if (null !== $encoding && !\is_scalar($encoding)) { trigger_error('mb_encode_numericentity() expects parameter 3 to be string, '.\gettype($s).' given', E_USER_WARNING); return null; // Instead of '' (cf. mb_decode_numericentity). } if (null !== $is_hex && !\is_scalar($is_hex)) { trigger_error('mb_encode_numericentity() expects parameter 4 to be boolean, '.\gettype($s).' given', E_USER_WARNING); return null; } $s = (string) $s; if ('' === $s) { return ''; } $encoding = self::getEncoding($encoding); if ('UTF-8' === $encoding) { $encoding = null; if (!preg_match('//u', $s)) { $s = @iconv('UTF-8', 'UTF-8//IGNORE', $s); } } else { $s = iconv($encoding, 'UTF-8//IGNORE', $s); } static $ulenMask = array("\xC0" => 2, "\xD0" => 2, "\xE0" => 3, "\xF0" => 4); $cnt = floor(\count($convmap) / 4) * 4; $i = 0; $len = \strlen($s); $result = ''; while ($i < $len) { $ulen = $s[$i] < "\x80" ? 1 : $ulenMask[$s[$i] & "\xF0"]; $uchr = substr($s, $i, $ulen); $i += $ulen; $c = self::mb_ord($uchr); for ($j = 0; $j < $cnt; $j += 4) { if ($c >= $convmap[$j] && $c <= $convmap[$j + 1]) { $cOffset = ($c + $convmap[$j + 2]) & $convmap[$j + 3]; $result .= $is_hex ? sprintf('&#x%X;', $cOffset) : '&#'.$cOffset.';'; continue 2; } } $result .= $uchr; } if (null === $encoding) { return $result; } return iconv('UTF-8', $encoding.'//IGNORE', $result); } public static function mb_convert_case($s, $mode, $encoding = null) { $s = (string) $s; if ('' === $s) { return ''; } $encoding = self::getEncoding($encoding); if ('UTF-8' === $encoding) { $encoding = null; if (!preg_match('//u', $s)) { $s = @iconv('UTF-8', 'UTF-8//IGNORE', $s); } } else { $s = iconv($encoding, 'UTF-8//IGNORE', $s); } if (MB_CASE_TITLE == $mode) { static $titleRegexp = null; if (null === $titleRegexp) { $titleRegexp = self::getData('titleCaseRegexp'); } $s = preg_replace_callback($titleRegexp, array(__CLASS__, 'title_case'), $s); } else { if (MB_CASE_UPPER == $mode) { static $upper = null; if (null === $upper) { $upper = self::getData('upperCase'); } $map = $upper; } else { if (self::MB_CASE_FOLD === $mode) { $s = str_replace(self::$caseFold[0], self::$caseFold[1], $s); } static $lower = null; if (null === $lower) { $lower = self::getData('lowerCase'); } $map = $lower; } static $ulenMask = array("\xC0" => 2, "\xD0" => 2, "\xE0" => 3, "\xF0" => 4); $i = 0; $len = \strlen($s); while ($i < $len) { $ulen = $s[$i] < "\x80" ? 1 : $ulenMask[$s[$i] & "\xF0"]; $uchr = substr($s, $i, $ulen); $i += $ulen; if (isset($map[$uchr])) { $uchr = $map[$uchr]; $nlen = \strlen($uchr); if ($nlen == $ulen) { $nlen = $i; do { $s[--$nlen] = $uchr[--$ulen]; } while ($ulen); } else { $s = substr_replace($s, $uchr, $i - $ulen, $ulen); $len += $nlen - $ulen; $i += $nlen - $ulen; } } } } if (null === $encoding) { return $s; } return iconv('UTF-8', $encoding.'//IGNORE', $s); } public static function mb_internal_encoding($encoding = null) { if (null === $encoding) { return self::$internalEncoding; } $encoding = self::getEncoding($encoding); if ('UTF-8' === $encoding || false !== @iconv($encoding, $encoding, ' ')) { self::$internalEncoding = $encoding; return true; } return false; } public static function mb_language($lang = null) { if (null === $lang) { return self::$language; } switch ($lang = strtolower($lang)) { case 'uni': case 'neutral': self::$language = $lang; return true; } return false; } public static function mb_list_encodings() { return array('UTF-8'); } public static function mb_encoding_aliases($encoding) { switch (strtoupper($encoding)) { case 'UTF8': case 'UTF-8': return array('utf8'); } return false; } public static function mb_check_encoding($var = null, $encoding = null) { if (null === $encoding) { if (null === $var) { return false; } $encoding = self::$internalEncoding; } return self::mb_detect_encoding($var, array($encoding)) || false !== @iconv($encoding, $encoding, $var); } public static function mb_detect_encoding($str, $encodingList = null, $strict = false) { if (null === $encodingList) { $encodingList = self::$encodingList; } else { if (!\is_array($encodingList)) { $encodingList = array_map('trim', explode(',', $encodingList)); } $encodingList = array_map('strtoupper', $encodingList); } foreach ($encodingList as $enc) { switch ($enc) { case 'ASCII': if (!preg_match('/[\x80-\xFF]/', $str)) { return $enc; } break; case 'UTF8': case 'UTF-8': if (preg_match('//u', $str)) { return 'UTF-8'; } break; default: if (0 === strncmp($enc, 'ISO-8859-', 9)) { return $enc; } } } return false; } public static function mb_detect_order($encodingList = null) { if (null === $encodingList) { return self::$encodingList; } if (!\is_array($encodingList)) { $encodingList = array_map('trim', explode(',', $encodingList)); } $encodingList = array_map('strtoupper', $encodingList); foreach ($encodingList as $enc) { switch ($enc) { default: if (strncmp($enc, 'ISO-8859-', 9)) { return false; } // no break case 'ASCII': case 'UTF8': case 'UTF-8': } } self::$encodingList = $encodingList; return true; } public static function mb_strlen($s, $encoding = null) { $encoding = self::getEncoding($encoding); if ('CP850' === $encoding || 'ASCII' === $encoding) { return \strlen($s); } return @iconv_strlen($s, $encoding); } public static function mb_strpos($haystack, $needle, $offset = 0, $encoding = null) { $encoding = self::getEncoding($encoding); if ('CP850' === $encoding || 'ASCII' === $encoding) { return strpos($haystack, $needle, $offset); } $needle = (string) $needle; if ('' === $needle) { trigger_error(__METHOD__.': Empty delimiter', E_USER_WARNING); return false; } return iconv_strpos($haystack, $needle, $offset, $encoding); } public static function mb_strrpos($haystack, $needle, $offset = 0, $encoding = null) { $encoding = self::getEncoding($encoding); if ('CP850' === $encoding || 'ASCII' === $encoding) { return strrpos($haystack, $needle, $offset); } if ($offset != (int) $offset) { $offset = 0; } elseif ($offset = (int) $offset) { if ($offset < 0) { if (0 > $offset += self::mb_strlen($needle)) { $haystack = self::mb_substr($haystack, 0, $offset, $encoding); } $offset = 0; } else { $haystack = self::mb_substr($haystack, $offset, 2147483647, $encoding); } } $pos = iconv_strrpos($haystack, $needle, $encoding); return false !== $pos ? $offset + $pos : false; } public static function mb_str_split($string, $split_length = 1, $encoding = null) { if (null !== $string && !\is_scalar($string) && !(\is_object($string) && \method_exists($string, '__toString'))) { trigger_error('mb_str_split() expects parameter 1 to be string, '.\gettype($string).' given', E_USER_WARNING); return null; } if (1 > $split_length = (int) $split_length) { trigger_error('The length of each segment must be greater than zero', E_USER_WARNING); return false; } if (null === $encoding) { $encoding = mb_internal_encoding(); } if ('UTF-8' === $encoding = self::getEncoding($encoding)) { $rx = '/('; while (65535 < $split_length) { $rx .= '.{65535}'; $split_length -= 65535; } $rx .= '.{'.$split_length.'})/us'; return preg_split($rx, $string, null, PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_NO_EMPTY); } $result = array(); $length = mb_strlen($string, $encoding); for ($i = 0; $i < $length; $i += $split_length) { $result[] = mb_substr($string, $i, $split_length, $encoding); } return $result; } public static function mb_strtolower($s, $encoding = null) { return self::mb_convert_case($s, MB_CASE_LOWER, $encoding); } public static function mb_strtoupper($s, $encoding = null) { return self::mb_convert_case($s, MB_CASE_UPPER, $encoding); } public static function mb_substitute_character($c = null) { if (0 === strcasecmp($c, 'none')) { return true; } return null !== $c ? false : 'none'; } public static function mb_substr($s, $start, $length = null, $encoding = null) { $encoding = self::getEncoding($encoding); if ('CP850' === $encoding || 'ASCII' === $encoding) { return (string) substr($s, $start, null === $length ? 2147483647 : $length); } if ($start < 0) { $start = iconv_strlen($s, $encoding) + $start; if ($start < 0) { $start = 0; } } if (null === $length) { $length = 2147483647; } elseif ($length < 0) { $length = iconv_strlen($s, $encoding) + $length - $start; if ($length < 0) { return ''; } } return (string) iconv_substr($s, $start, $length, $encoding); } public static function mb_stripos($haystack, $needle, $offset = 0, $encoding = null) { $haystack = self::mb_convert_case($haystack, self::MB_CASE_FOLD, $encoding); $needle = self::mb_convert_case($needle, self::MB_CASE_FOLD, $encoding); return self::mb_strpos($haystack, $needle, $offset, $encoding); } public static function mb_stristr($haystack, $needle, $part = false, $encoding = null) { $pos = self::mb_stripos($haystack, $needle, 0, $encoding); return self::getSubpart($pos, $part, $haystack, $encoding); } public static function mb_strrchr($haystack, $needle, $part = false, $encoding = null) { $encoding = self::getEncoding($encoding); if ('CP850' === $encoding || 'ASCII' === $encoding) { $pos = strrpos($haystack, $needle); } else { $needle = self::mb_substr($needle, 0, 1, $encoding); $pos = iconv_strrpos($haystack, $needle, $encoding); } return self::getSubpart($pos, $part, $haystack, $encoding); } public static function mb_strrichr($haystack, $needle, $part = false, $encoding = null) { $needle = self::mb_substr($needle, 0, 1, $encoding); $pos = self::mb_strripos($haystack, $needle, $encoding); return self::getSubpart($pos, $part, $haystack, $encoding); } public static function mb_strripos($haystack, $needle, $offset = 0, $encoding = null) { $haystack = self::mb_convert_case($haystack, self::MB_CASE_FOLD, $encoding); $needle = self::mb_convert_case($needle, self::MB_CASE_FOLD, $encoding); return self::mb_strrpos($haystack, $needle, $offset, $encoding); } public static function mb_strstr($haystack, $needle, $part = false, $encoding = null) { $pos = strpos($haystack, $needle); if (false === $pos) { return false; } if ($part) { return substr($haystack, 0, $pos); } return substr($haystack, $pos); } public static function mb_get_info($type = 'all') { $info = array( 'internal_encoding' => self::$internalEncoding, 'http_output' => 'pass', 'http_output_conv_mimetypes' => '^(text/|application/xhtml\+xml)', 'func_overload' => 0, 'func_overload_list' => 'no overload', 'mail_charset' => 'UTF-8', 'mail_header_encoding' => 'BASE64', 'mail_body_encoding' => 'BASE64', 'illegal_chars' => 0, 'encoding_translation' => 'Off', 'language' => self::$language, 'detect_order' => self::$encodingList, 'substitute_character' => 'none', 'strict_detection' => 'Off', ); if ('all' === $type) { return $info; } if (isset($info[$type])) { return $info[$type]; } return false; } public static function mb_http_input($type = '') { return false; } public static function mb_http_output($encoding = null) { return null !== $encoding ? 'pass' === $encoding : 'pass'; } public static function mb_strwidth($s, $encoding = null) { $encoding = self::getEncoding($encoding); if ('UTF-8' !== $encoding) { $s = iconv($encoding, 'UTF-8//IGNORE', $s); } $s = preg_replace('/[\x{1100}-\x{115F}\x{2329}\x{232A}\x{2E80}-\x{303E}\x{3040}-\x{A4CF}\x{AC00}-\x{D7A3}\x{F900}-\x{FAFF}\x{FE10}-\x{FE19}\x{FE30}-\x{FE6F}\x{FF00}-\x{FF60}\x{FFE0}-\x{FFE6}\x{20000}-\x{2FFFD}\x{30000}-\x{3FFFD}]/u', '', $s, -1, $wide); return ($wide << 1) + iconv_strlen($s, 'UTF-8'); } public static function mb_substr_count($haystack, $needle, $encoding = null) { return substr_count($haystack, $needle); } public static function mb_output_handler($contents, $status) { return $contents; } public static function mb_chr($code, $encoding = null) { if (0x80 > $code %= 0x200000) { $s = \chr($code); } elseif (0x800 > $code) { $s = \chr(0xC0 | $code >> 6).\chr(0x80 | $code & 0x3F); } elseif (0x10000 > $code) { $s = \chr(0xE0 | $code >> 12).\chr(0x80 | $code >> 6 & 0x3F).\chr(0x80 | $code & 0x3F); } else { $s = \chr(0xF0 | $code >> 18).\chr(0x80 | $code >> 12 & 0x3F).\chr(0x80 | $code >> 6 & 0x3F).\chr(0x80 | $code & 0x3F); } if ('UTF-8' !== $encoding = self::getEncoding($encoding)) { $s = mb_convert_encoding($s, $encoding, 'UTF-8'); } return $s; } public static function mb_ord($s, $encoding = null) { if ('UTF-8' !== $encoding = self::getEncoding($encoding)) { $s = mb_convert_encoding($s, 'UTF-8', $encoding); } if (1 === \strlen($s)) { return \ord($s); } $code = ($s = unpack('C*', substr($s, 0, 4))) ? $s[1] : 0; if (0xF0 <= $code) { return (($code - 0xF0) << 18) + (($s[2] - 0x80) << 12) + (($s[3] - 0x80) << 6) + $s[4] - 0x80; } if (0xE0 <= $code) { return (($code - 0xE0) << 12) + (($s[2] - 0x80) << 6) + $s[3] - 0x80; } if (0xC0 <= $code) { return (($code - 0xC0) << 6) + $s[2] - 0x80; } return $code; } private static function getSubpart($pos, $part, $haystack, $encoding) { if (false === $pos) { return false; } if ($part) { return self::mb_substr($haystack, 0, $pos, $encoding); } return self::mb_substr($haystack, $pos, null, $encoding); } private static function html_encoding_callback(array $m) { $i = 1; $entities = ''; $m = unpack('C*', htmlentities($m[0], ENT_COMPAT, 'UTF-8')); while (isset($m[$i])) { if (0x80 > $m[$i]) { $entities .= \chr($m[$i++]); continue; } if (0xF0 <= $m[$i]) { $c = (($m[$i++] - 0xF0) << 18) + (($m[$i++] - 0x80) << 12) + (($m[$i++] - 0x80) << 6) + $m[$i++] - 0x80; } elseif (0xE0 <= $m[$i]) { $c = (($m[$i++] - 0xE0) << 12) + (($m[$i++] - 0x80) << 6) + $m[$i++] - 0x80; } else { $c = (($m[$i++] - 0xC0) << 6) + $m[$i++] - 0x80; } $entities .= '&#'.$c.';'; } return $entities; } private static function title_case(array $s) { return self::mb_convert_case($s[1], MB_CASE_UPPER, 'UTF-8').self::mb_convert_case($s[2], MB_CASE_LOWER, 'UTF-8'); } private static function getData($file) { if (file_exists($file = __DIR__.'/Resources/unidata/'.$file.'.php')) { return require $file; } return false; } private static function getEncoding($encoding) { if (null === $encoding) { return self::$internalEncoding; } if ('UTF-8' === $encoding) { return 'UTF-8'; } $encoding = strtoupper($encoding); if ('8BIT' === $encoding || 'BINARY' === $encoding) { return 'CP850'; } if ('UTF8' === $encoding) { return 'UTF-8'; } return $encoding; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Process; use Symfony\Component\Process\Exception\RuntimeException; /** * PhpProcess runs a PHP script in an independent process. * * $p = new PhpProcess(''); * $p->run(); * print $p->getOutput()."\n"; * * @author Fabien Potencier */ class PhpProcess extends Process { /** * @param string $script The PHP script to run (as a string) * @param string|null $cwd The working directory or null to use the working dir of the current PHP process * @param array|null $env The environment variables or null to use the same environment as the current PHP process * @param int $timeout The timeout in seconds * @param array $options An array of options for proc_open */ public function __construct($script, $cwd = null, array $env = null, $timeout = 60, array $options = null) { $executableFinder = new PhpExecutableFinder(); if (false === $php = $executableFinder->find(false)) { $php = null; } else { $php = array_merge([$php], $executableFinder->findArguments()); } if ('phpdbg' === \PHP_SAPI) { $file = tempnam(sys_get_temp_dir(), 'dbg'); file_put_contents($file, $script); register_shutdown_function('unlink', $file); $php[] = $file; $script = null; } if (null !== $options) { @trigger_error(sprintf('The $options parameter of the %s constructor is deprecated since Symfony 3.3 and will be removed in 4.0.', __CLASS__), \E_USER_DEPRECATED); } parent::__construct($php, $cwd, $env, $script, $timeout, $options); } /** * Sets the path to the PHP binary to use. */ public function setPhpBinary($php) { $this->setCommandLine($php); } /** * {@inheritdoc} */ public function start(callable $callback = null/*, array $env = []*/) { if (null === $this->getCommandLine()) { throw new RuntimeException('Unable to find the PHP executable.'); } $env = 1 < \func_num_args() ? func_get_arg(1) : null; parent::start($callback, $env); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Process; @trigger_error(sprintf('The %s class is deprecated since Symfony 3.4 and will be removed in 4.0. Use the Process class instead.', ProcessBuilder::class), \E_USER_DEPRECATED); use Symfony\Component\Process\Exception\InvalidArgumentException; use Symfony\Component\Process\Exception\LogicException; /** * @author Kris Wallsmith * * @deprecated since version 3.4, to be removed in 4.0. Use the Process class instead. */ class ProcessBuilder { private $arguments; private $cwd; private $env = []; private $input; private $timeout = 60; private $options; private $inheritEnv = true; private $prefix = []; private $outputDisabled = false; /** * @param string[] $arguments An array of arguments */ public function __construct(array $arguments = []) { $this->arguments = $arguments; } /** * Creates a process builder instance. * * @param string[] $arguments An array of arguments * * @return static */ public static function create(array $arguments = []) { return new static($arguments); } /** * Adds an unescaped argument to the command string. * * @param string $argument A command argument * * @return $this */ public function add($argument) { $this->arguments[] = $argument; return $this; } /** * Adds a prefix to the command string. * * The prefix is preserved when resetting arguments. * * @param string|array $prefix A command prefix or an array of command prefixes * * @return $this */ public function setPrefix($prefix) { $this->prefix = \is_array($prefix) ? $prefix : [$prefix]; return $this; } /** * Sets the arguments of the process. * * Arguments must not be escaped. * Previous arguments are removed. * * @param string[] $arguments * * @return $this */ public function setArguments(array $arguments) { $this->arguments = $arguments; return $this; } /** * Sets the working directory. * * @param string|null $cwd The working directory * * @return $this */ public function setWorkingDirectory($cwd) { $this->cwd = $cwd; return $this; } /** * Sets whether environment variables will be inherited or not. * * @param bool $inheritEnv * * @return $this */ public function inheritEnvironmentVariables($inheritEnv = true) { $this->inheritEnv = $inheritEnv; return $this; } /** * Sets an environment variable. * * Setting a variable overrides its previous value. Use `null` to unset a * defined environment variable. * * @param string $name The variable name * @param string|null $value The variable value * * @return $this */ public function setEnv($name, $value) { $this->env[$name] = $value; return $this; } /** * Adds a set of environment variables. * * Already existing environment variables with the same name will be * overridden by the new values passed to this method. Pass `null` to unset * a variable. * * @param array $variables The variables * * @return $this */ public function addEnvironmentVariables(array $variables) { $this->env = array_replace($this->env, $variables); return $this; } /** * Sets the input of the process. * * @param resource|string|int|float|bool|\Traversable|null $input The input content * * @return $this * * @throws InvalidArgumentException In case the argument is invalid */ public function setInput($input) { $this->input = ProcessUtils::validateInput(__METHOD__, $input); return $this; } /** * Sets the process timeout. * * To disable the timeout, set this value to null. * * @param float|null $timeout * * @return $this * * @throws InvalidArgumentException */ public function setTimeout($timeout) { if (null === $timeout) { $this->timeout = null; return $this; } $timeout = (float) $timeout; if ($timeout < 0) { throw new InvalidArgumentException('The timeout value must be a valid positive integer or float number.'); } $this->timeout = $timeout; return $this; } /** * Adds a proc_open option. * * @param string $name The option name * @param string $value The option value * * @return $this */ public function setOption($name, $value) { $this->options[$name] = $value; return $this; } /** * Disables fetching output and error output from the underlying process. * * @return $this */ public function disableOutput() { $this->outputDisabled = true; return $this; } /** * Enables fetching output and error output from the underlying process. * * @return $this */ public function enableOutput() { $this->outputDisabled = false; return $this; } /** * Creates a Process instance and returns it. * * @return Process * * @throws LogicException In case no arguments have been provided */ public function getProcess() { if (0 === \count($this->prefix) && 0 === \count($this->arguments)) { throw new LogicException('You must add() command arguments before calling getProcess().'); } $arguments = array_merge($this->prefix, $this->arguments); $process = new Process($arguments, $this->cwd, $this->env, $this->input, $this->timeout, $this->options); // to preserve the BC with symfony <3.3, we convert the array structure // to a string structure to avoid the prefixing with the exec command $process->setCommandLine($process->getCommandLine()); if ($this->inheritEnv) { $process->inheritEnvironmentVariables(); } if ($this->outputDisabled) { $process->disableOutput(); } return $process; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Process; /** * An executable finder specifically designed for the PHP executable. * * @author Fabien Potencier * @author Johannes M. Schmitt */ class PhpExecutableFinder { private $executableFinder; public function __construct() { $this->executableFinder = new ExecutableFinder(); } /** * Finds The PHP executable. * * @param bool $includeArgs Whether or not include command arguments * * @return string|false The PHP executable path or false if it cannot be found */ public function find($includeArgs = true) { $args = $this->findArguments(); $args = $includeArgs && $args ? ' '.implode(' ', $args) : ''; // HHVM support if (\defined('HHVM_VERSION')) { return (getenv('PHP_BINARY') ?: \PHP_BINARY).$args; } // PHP_BINARY return the current sapi executable if (\PHP_BINARY && \in_array(\PHP_SAPI, ['cli', 'cli-server', 'phpdbg'], true)) { return \PHP_BINARY.$args; } if ($php = getenv('PHP_PATH')) { if (!@is_executable($php)) { return false; } return $php; } if ($php = getenv('PHP_PEAR_PHP_BIN')) { if (@is_executable($php)) { return $php; } } if (@is_executable($php = \PHP_BINDIR.('\\' === \DIRECTORY_SEPARATOR ? '\\php.exe' : '/php'))) { return $php; } $dirs = [\PHP_BINDIR]; if ('\\' === \DIRECTORY_SEPARATOR) { $dirs[] = 'C:\xampp\php\\'; } return $this->executableFinder->find('php', false, $dirs); } /** * Finds the PHP executable arguments. * * @return array The PHP executable arguments */ public function findArguments() { $arguments = []; if (\defined('HHVM_VERSION')) { $arguments[] = '--php'; } elseif ('phpdbg' === \PHP_SAPI) { $arguments[] = '-qrr'; } return $arguments; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Process\Exception; /** * InvalidArgumentException for the Process Component. * * @author Romain Neutron */ class InvalidArgumentException extends \InvalidArgumentException implements ExceptionInterface { } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Process\Exception; /** * LogicException for the Process Component. * * @author Romain Neutron */ class LogicException extends \LogicException implements ExceptionInterface { } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Process\Exception; use Symfony\Component\Process\Process; /** * Exception for failed processes. * * @author Johannes M. Schmitt */ class ProcessFailedException extends RuntimeException { private $process; public function __construct(Process $process) { if ($process->isSuccessful()) { throw new InvalidArgumentException('Expected a failed process, but the given process was successful.'); } $error = sprintf('The command "%s" failed.'."\n\nExit Code: %s(%s)\n\nWorking directory: %s", $process->getCommandLine(), $process->getExitCode(), $process->getExitCodeText(), $process->getWorkingDirectory() ); if (!$process->isOutputDisabled()) { $error .= sprintf("\n\nOutput:\n================\n%s\n\nError Output:\n================\n%s", $process->getOutput(), $process->getErrorOutput() ); } parent::__construct($error); $this->process = $process; } public function getProcess() { return $this->process; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Process\Exception; /** * Marker Interface for the Process Component. * * @author Johannes M. Schmitt */ interface ExceptionInterface { } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Process\Exception; use Symfony\Component\Process\Process; /** * Exception that is thrown when a process times out. * * @author Johannes M. Schmitt */ class ProcessTimedOutException extends RuntimeException { const TYPE_GENERAL = 1; const TYPE_IDLE = 2; private $process; private $timeoutType; public function __construct(Process $process, $timeoutType) { $this->process = $process; $this->timeoutType = $timeoutType; parent::__construct(sprintf( 'The process "%s" exceeded the timeout of %s seconds.', $process->getCommandLine(), $this->getExceededTimeout() )); } public function getProcess() { return $this->process; } public function isGeneralTimeout() { return self::TYPE_GENERAL === $this->timeoutType; } public function isIdleTimeout() { return self::TYPE_IDLE === $this->timeoutType; } public function getExceededTimeout() { switch ($this->timeoutType) { case self::TYPE_GENERAL: return $this->process->getTimeout(); case self::TYPE_IDLE: return $this->process->getIdleTimeout(); default: throw new \LogicException(sprintf('Unknown timeout type "%d".', $this->timeoutType)); } } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Process\Exception; /** * RuntimeException for the Process Component. * * @author Johannes M. Schmitt */ class RuntimeException extends \RuntimeException implements ExceptionInterface { } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Process\Pipes; /** * PipesInterface manages descriptors and pipes for the use of proc_open. * * @author Romain Neutron * * @internal */ interface PipesInterface { const CHUNK_SIZE = 16384; /** * Returns an array of descriptors for the use of proc_open. * * @return array */ public function getDescriptors(); /** * Returns an array of filenames indexed by their related stream in case these pipes use temporary files. * * @return string[] */ public function getFiles(); /** * Reads data in file handles and pipes. * * @param bool $blocking Whether to use blocking calls or not * @param bool $close Whether to close pipes if they've reached EOF * * @return string[] An array of read data indexed by their fd */ public function readAndWrite($blocking, $close = false); /** * Returns if the current state has open file handles or pipes. * * @return bool */ public function areOpen(); /** * Returns if pipes are able to read output. * * @return bool */ public function haveReadSupport(); /** * Closes file handles and pipes. */ public function close(); } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Process\Pipes; use Symfony\Component\Process\Process; /** * UnixPipes implementation uses unix pipes as handles. * * @author Romain Neutron * * @internal */ class UnixPipes extends AbstractPipes { private $ttyMode; private $ptyMode; private $haveReadSupport; public function __construct($ttyMode, $ptyMode, $input, $haveReadSupport) { $this->ttyMode = (bool) $ttyMode; $this->ptyMode = (bool) $ptyMode; $this->haveReadSupport = (bool) $haveReadSupport; parent::__construct($input); } public function __destruct() { $this->close(); } /** * {@inheritdoc} */ public function getDescriptors() { if (!$this->haveReadSupport) { $nullstream = fopen('/dev/null', 'c'); return [ ['pipe', 'r'], $nullstream, $nullstream, ]; } if ($this->ttyMode) { return [ ['file', '/dev/tty', 'r'], ['file', '/dev/tty', 'w'], ['file', '/dev/tty', 'w'], ]; } if ($this->ptyMode && Process::isPtySupported()) { return [ ['pty'], ['pty'], ['pty'], ]; } return [ ['pipe', 'r'], ['pipe', 'w'], // stdout ['pipe', 'w'], // stderr ]; } /** * {@inheritdoc} */ public function getFiles() { return []; } /** * {@inheritdoc} */ public function readAndWrite($blocking, $close = false) { $this->unblock(); $w = $this->write(); $read = $e = []; $r = $this->pipes; unset($r[0]); // let's have a look if something changed in streams set_error_handler([$this, 'handleError']); if (($r || $w) && false === stream_select($r, $w, $e, 0, $blocking ? Process::TIMEOUT_PRECISION * 1E6 : 0)) { restore_error_handler(); // if a system call has been interrupted, forget about it, let's try again // otherwise, an error occurred, let's reset pipes if (!$this->hasSystemCallBeenInterrupted()) { $this->pipes = []; } return $read; } restore_error_handler(); foreach ($r as $pipe) { // prior PHP 5.4 the array passed to stream_select is modified and // lose key association, we have to find back the key $read[$type = array_search($pipe, $this->pipes, true)] = ''; do { $data = @fread($pipe, self::CHUNK_SIZE); $read[$type] .= $data; } while (isset($data[0]) && ($close || isset($data[self::CHUNK_SIZE - 1]))); if (!isset($read[$type][0])) { unset($read[$type]); } if ($close && feof($pipe)) { fclose($pipe); unset($this->pipes[$type]); } } return $read; } /** * {@inheritdoc} */ public function haveReadSupport() { return $this->haveReadSupport; } /** * {@inheritdoc} */ public function areOpen() { return (bool) $this->pipes; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Process\Pipes; use Symfony\Component\Process\Exception\RuntimeException; use Symfony\Component\Process\Process; /** * WindowsPipes implementation uses temporary files as handles. * * @see https://bugs.php.net/51800 * @see https://bugs.php.net/65650 * * @author Romain Neutron * * @internal */ class WindowsPipes extends AbstractPipes { private $files = []; private $fileHandles = []; private $lockHandles = []; private $readBytes = [ Process::STDOUT => 0, Process::STDERR => 0, ]; private $haveReadSupport; public function __construct($input, $haveReadSupport) { $this->haveReadSupport = (bool) $haveReadSupport; if ($this->haveReadSupport) { // Fix for PHP bug #51800: reading from STDOUT pipe hangs forever on Windows if the output is too big. // Workaround for this problem is to use temporary files instead of pipes on Windows platform. // // @see https://bugs.php.net/51800 $pipes = [ Process::STDOUT => Process::OUT, Process::STDERR => Process::ERR, ]; $tmpDir = sys_get_temp_dir(); $lastError = 'unknown reason'; set_error_handler(function ($type, $msg) use (&$lastError) { $lastError = $msg; }); for ($i = 0;; ++$i) { foreach ($pipes as $pipe => $name) { $file = sprintf('%s\\sf_proc_%02X.%s', $tmpDir, $i, $name); if (!$h = fopen($file.'.lock', 'w')) { if (file_exists($file.'.lock')) { continue 2; } restore_error_handler(); throw new RuntimeException('A temporary file could not be opened to write the process output: '.$lastError); } if (!flock($h, \LOCK_EX | \LOCK_NB)) { continue 2; } if (isset($this->lockHandles[$pipe])) { flock($this->lockHandles[$pipe], \LOCK_UN); fclose($this->lockHandles[$pipe]); } $this->lockHandles[$pipe] = $h; if (!fclose(fopen($file, 'w')) || !$h = fopen($file, 'r')) { flock($this->lockHandles[$pipe], \LOCK_UN); fclose($this->lockHandles[$pipe]); unset($this->lockHandles[$pipe]); continue 2; } $this->fileHandles[$pipe] = $h; $this->files[$pipe] = $file; } break; } restore_error_handler(); } parent::__construct($input); } public function __destruct() { $this->close(); } /** * {@inheritdoc} */ public function getDescriptors() { if (!$this->haveReadSupport) { $nullstream = fopen('NUL', 'c'); return [ ['pipe', 'r'], $nullstream, $nullstream, ]; } // We're not using pipe on Windows platform as it hangs (https://bugs.php.net/51800) // We're not using file handles as it can produce corrupted output https://bugs.php.net/65650 // So we redirect output within the commandline and pass the nul device to the process return [ ['pipe', 'r'], ['file', 'NUL', 'w'], ['file', 'NUL', 'w'], ]; } /** * {@inheritdoc} */ public function getFiles() { return $this->files; } /** * {@inheritdoc} */ public function readAndWrite($blocking, $close = false) { $this->unblock(); $w = $this->write(); $read = $r = $e = []; if ($blocking) { if ($w) { @stream_select($r, $w, $e, 0, Process::TIMEOUT_PRECISION * 1E6); } elseif ($this->fileHandles) { usleep(Process::TIMEOUT_PRECISION * 1E6); } } foreach ($this->fileHandles as $type => $fileHandle) { $data = stream_get_contents($fileHandle, -1, $this->readBytes[$type]); if (isset($data[0])) { $this->readBytes[$type] += \strlen($data); $read[$type] = $data; } if ($close) { ftruncate($fileHandle, 0); fclose($fileHandle); flock($this->lockHandles[$type], \LOCK_UN); fclose($this->lockHandles[$type]); unset($this->fileHandles[$type], $this->lockHandles[$type]); } } return $read; } /** * {@inheritdoc} */ public function haveReadSupport() { return $this->haveReadSupport; } /** * {@inheritdoc} */ public function areOpen() { return $this->pipes && $this->fileHandles; } /** * {@inheritdoc} */ public function close() { parent::close(); foreach ($this->fileHandles as $type => $handle) { ftruncate($handle, 0); fclose($handle); flock($this->lockHandles[$type], \LOCK_UN); fclose($this->lockHandles[$type]); } $this->fileHandles = $this->lockHandles = []; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Process\Pipes; use Symfony\Component\Process\Exception\InvalidArgumentException; /** * @author Romain Neutron * * @internal */ abstract class AbstractPipes implements PipesInterface { public $pipes = []; private $inputBuffer = ''; private $input; private $blocked = true; private $lastError; /** * @param resource|string|int|float|bool|\Iterator|null $input */ public function __construct($input) { if (\is_resource($input) || $input instanceof \Iterator) { $this->input = $input; } elseif (\is_string($input)) { $this->inputBuffer = $input; } else { $this->inputBuffer = (string) $input; } } /** * {@inheritdoc} */ public function close() { foreach ($this->pipes as $pipe) { fclose($pipe); } $this->pipes = []; } /** * Returns true if a system call has been interrupted. * * @return bool */ protected function hasSystemCallBeenInterrupted() { $lastError = $this->lastError; $this->lastError = null; // stream_select returns false when the `select` system call is interrupted by an incoming signal return null !== $lastError && false !== stripos($lastError, 'interrupted system call'); } /** * Unblocks streams. */ protected function unblock() { if (!$this->blocked) { return; } foreach ($this->pipes as $pipe) { stream_set_blocking($pipe, 0); } if (\is_resource($this->input)) { stream_set_blocking($this->input, 0); } $this->blocked = false; } /** * Writes input to stdin. * * @return array|null * * @throws InvalidArgumentException When an input iterator yields a non supported value */ protected function write() { if (!isset($this->pipes[0])) { return null; } $input = $this->input; if ($input instanceof \Iterator) { if (!$input->valid()) { $input = null; } elseif (\is_resource($input = $input->current())) { stream_set_blocking($input, 0); } elseif (!isset($this->inputBuffer[0])) { if (!\is_string($input)) { if (!is_scalar($input)) { throw new InvalidArgumentException(sprintf('"%s" yielded a value of type "%s", but only scalars and stream resources are supported.', \get_class($this->input), \gettype($input))); } $input = (string) $input; } $this->inputBuffer = $input; $this->input->next(); $input = null; } else { $input = null; } } $r = $e = []; $w = [$this->pipes[0]]; // let's have a look if something changed in streams if (false === @stream_select($r, $w, $e, 0, 0)) { return null; } foreach ($w as $stdin) { if (isset($this->inputBuffer[0])) { $written = fwrite($stdin, $this->inputBuffer); $this->inputBuffer = substr($this->inputBuffer, $written); if (isset($this->inputBuffer[0])) { return [$this->pipes[0]]; } } if ($input) { for (;;) { $data = fread($input, self::CHUNK_SIZE); if (!isset($data[0])) { break; } $written = fwrite($stdin, $data); $data = substr($data, $written); if (isset($data[0])) { $this->inputBuffer = $data; return [$this->pipes[0]]; } } if (feof($input)) { if ($this->input instanceof \Iterator) { $this->input->next(); } else { $this->input = null; } } } } // no input to read on resource, buffer is empty if (!isset($this->inputBuffer[0]) && !($this->input instanceof \Iterator ? $this->input->valid() : $this->input)) { $this->input = null; fclose($this->pipes[0]); unset($this->pipes[0]); } elseif (!$w) { return [$this->pipes[0]]; } return null; } /** * @internal */ public function handleError($type, $msg) { $this->lastError = $msg; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Process; use Symfony\Component\Process\Exception\RuntimeException; /** * Provides a way to continuously write to the input of a Process until the InputStream is closed. * * @author Nicolas Grekas */ class InputStream implements \IteratorAggregate { /** @var callable|null */ private $onEmpty = null; private $input = []; private $open = true; /** * Sets a callback that is called when the write buffer becomes empty. */ public function onEmpty(callable $onEmpty = null) { $this->onEmpty = $onEmpty; } /** * Appends an input to the write buffer. * * @param resource|string|int|float|bool|\Traversable|null $input The input to append as scalar, * stream resource or \Traversable */ public function write($input) { if (null === $input) { return; } if ($this->isClosed()) { throw new RuntimeException(sprintf('"%s" is closed.', static::class)); } $this->input[] = ProcessUtils::validateInput(__METHOD__, $input); } /** * Closes the write buffer. */ public function close() { $this->open = false; } /** * Tells whether the write buffer is closed or not. */ public function isClosed() { return !$this->open; } public function getIterator() { $this->open = true; while ($this->open || $this->input) { if (!$this->input) { yield ''; continue; } $current = array_shift($this->input); if ($current instanceof \Iterator) { foreach ($current as $cur) { yield $cur; } } else { yield $current; } if (!$this->input && $this->open && null !== $onEmpty = $this->onEmpty) { $this->write($onEmpty($this)); } } } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Process; /** * Generic executable finder. * * @author Fabien Potencier * @author Johannes M. Schmitt */ class ExecutableFinder { private $suffixes = ['.exe', '.bat', '.cmd', '.com']; /** * Replaces default suffixes of executable. */ public function setSuffixes(array $suffixes) { $this->suffixes = $suffixes; } /** * Adds new possible suffix to check for executable. * * @param string $suffix */ public function addSuffix($suffix) { $this->suffixes[] = $suffix; } /** * Finds an executable by name. * * @param string $name The executable name (without the extension) * @param string|null $default The default to return if no executable is found * @param array $extraDirs Additional dirs to check into * * @return string|null The executable path or default value */ public function find($name, $default = null, array $extraDirs = []) { if (ini_get('open_basedir')) { $searchPath = array_merge(explode(\PATH_SEPARATOR, ini_get('open_basedir')), $extraDirs); $dirs = []; foreach ($searchPath as $path) { // Silencing against https://bugs.php.net/69240 if (@is_dir($path)) { $dirs[] = $path; } else { if (basename($path) == $name && @is_executable($path)) { return $path; } } } } else { $dirs = array_merge( explode(\PATH_SEPARATOR, getenv('PATH') ?: getenv('Path')), $extraDirs ); } $suffixes = ['']; if ('\\' === \DIRECTORY_SEPARATOR) { $pathExt = getenv('PATHEXT'); $suffixes = array_merge($pathExt ? explode(\PATH_SEPARATOR, $pathExt) : $this->suffixes, $suffixes); } foreach ($suffixes as $suffix) { foreach ($dirs as $dir) { if (@is_file($file = $dir.\DIRECTORY_SEPARATOR.$name.$suffix) && ('\\' === \DIRECTORY_SEPARATOR || @is_executable($file))) { return $file; } } } return $default; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Process; use Symfony\Component\Process\Exception\InvalidArgumentException; /** * ProcessUtils is a bunch of utility methods. * * This class contains static methods only and is not meant to be instantiated. * * @author Martin Hasoň */ class ProcessUtils { /** * This class should not be instantiated. */ private function __construct() { } /** * Escapes a string to be used as a shell argument. * * @param string $argument The argument that will be escaped * * @return string The escaped argument * * @deprecated since version 3.3, to be removed in 4.0. Use a command line array or give env vars to the `Process::start/run()` method instead. */ public static function escapeArgument($argument) { @trigger_error('The '.__METHOD__.'() method is deprecated since Symfony 3.3 and will be removed in 4.0. Use a command line array or give env vars to the Process::start/run() method instead.', \E_USER_DEPRECATED); //Fix for PHP bug #43784 escapeshellarg removes % from given string //Fix for PHP bug #49446 escapeshellarg doesn't work on Windows //@see https://bugs.php.net/43784 //@see https://bugs.php.net/49446 if ('\\' === \DIRECTORY_SEPARATOR) { if ('' === $argument) { return escapeshellarg($argument); } $escapedArgument = ''; $quote = false; foreach (preg_split('/(")/', $argument, -1, \PREG_SPLIT_NO_EMPTY | \PREG_SPLIT_DELIM_CAPTURE) as $part) { if ('"' === $part) { $escapedArgument .= '\\"'; } elseif (self::isSurroundedBy($part, '%')) { // Avoid environment variable expansion $escapedArgument .= '^%"'.substr($part, 1, -1).'"^%'; } else { // escape trailing backslash if ('\\' === substr($part, -1)) { $part .= '\\'; } $quote = true; $escapedArgument .= $part; } } if ($quote) { $escapedArgument = '"'.$escapedArgument.'"'; } return $escapedArgument; } return "'".str_replace("'", "'\\''", $argument)."'"; } /** * Validates and normalizes a Process input. * * @param string $caller The name of method call that validates the input * @param mixed $input The input to validate * * @return mixed The validated input * * @throws InvalidArgumentException In case the input is not valid */ public static function validateInput($caller, $input) { if (null !== $input) { if (\is_resource($input)) { return $input; } if (\is_string($input)) { return $input; } if (is_scalar($input)) { return (string) $input; } if ($input instanceof Process) { return $input->getIterator($input::ITER_SKIP_ERR); } if ($input instanceof \Iterator) { return $input; } if ($input instanceof \Traversable) { return new \IteratorIterator($input); } throw new InvalidArgumentException(sprintf('"%s" only accepts strings, Traversable objects or stream resources.', $caller)); } return $input; } private static function isSurroundedBy($arg, $char) { return 2 < \strlen($arg) && $char === $arg[0] && $char === $arg[\strlen($arg) - 1]; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Process; use Symfony\Component\Process\Exception\InvalidArgumentException; use Symfony\Component\Process\Exception\LogicException; use Symfony\Component\Process\Exception\ProcessFailedException; use Symfony\Component\Process\Exception\ProcessTimedOutException; use Symfony\Component\Process\Exception\RuntimeException; use Symfony\Component\Process\Pipes\PipesInterface; use Symfony\Component\Process\Pipes\UnixPipes; use Symfony\Component\Process\Pipes\WindowsPipes; /** * Process is a thin wrapper around proc_* functions to easily * start independent PHP processes. * * @author Fabien Potencier * @author Romain Neutron */ class Process implements \IteratorAggregate { const ERR = 'err'; const OUT = 'out'; const STATUS_READY = 'ready'; const STATUS_STARTED = 'started'; const STATUS_TERMINATED = 'terminated'; const STDIN = 0; const STDOUT = 1; const STDERR = 2; // Timeout Precision in seconds. const TIMEOUT_PRECISION = 0.2; const ITER_NON_BLOCKING = 1; // By default, iterating over outputs is a blocking call, use this flag to make it non-blocking const ITER_KEEP_OUTPUT = 2; // By default, outputs are cleared while iterating, use this flag to keep them in memory const ITER_SKIP_OUT = 4; // Use this flag to skip STDOUT while iterating const ITER_SKIP_ERR = 8; // Use this flag to skip STDERR while iterating private $callback; private $hasCallback = false; private $commandline; private $cwd; private $env; private $input; private $starttime; private $lastOutputTime; private $timeout; private $idleTimeout; private $options = ['suppress_errors' => true]; private $exitcode; private $fallbackStatus = []; private $processInformation; private $outputDisabled = false; private $stdout; private $stderr; private $enhanceWindowsCompatibility = true; private $enhanceSigchildCompatibility; private $process; private $status = self::STATUS_READY; private $incrementalOutputOffset = 0; private $incrementalErrorOutputOffset = 0; private $tty = false; private $pty; private $inheritEnv = false; private $useFileHandles = false; /** @var PipesInterface */ private $processPipes; private $latestSignal; private static $sigchild; /** * Exit codes translation table. * * User-defined errors must use exit codes in the 64-113 range. */ public static $exitCodes = [ 0 => 'OK', 1 => 'General error', 2 => 'Misuse of shell builtins', 126 => 'Invoked command cannot execute', 127 => 'Command not found', 128 => 'Invalid exit argument', // signals 129 => 'Hangup', 130 => 'Interrupt', 131 => 'Quit and dump core', 132 => 'Illegal instruction', 133 => 'Trace/breakpoint trap', 134 => 'Process aborted', 135 => 'Bus error: "access to undefined portion of memory object"', 136 => 'Floating point exception: "erroneous arithmetic operation"', 137 => 'Kill (terminate immediately)', 138 => 'User-defined 1', 139 => 'Segmentation violation', 140 => 'User-defined 2', 141 => 'Write to pipe with no one reading', 142 => 'Signal raised by alarm', 143 => 'Termination (request to terminate)', // 144 - not defined 145 => 'Child process terminated, stopped (or continued*)', 146 => 'Continue if stopped', 147 => 'Stop executing temporarily', 148 => 'Terminal stop signal', 149 => 'Background process attempting to read from tty ("in")', 150 => 'Background process attempting to write to tty ("out")', 151 => 'Urgent data available on socket', 152 => 'CPU time limit exceeded', 153 => 'File size limit exceeded', 154 => 'Signal raised by timer counting virtual time: "virtual timer expired"', 155 => 'Profiling timer expired', // 156 - not defined 157 => 'Pollable event', // 158 - not defined 159 => 'Bad syscall', ]; /** * @param string|array $commandline The command line to run * @param string|null $cwd The working directory or null to use the working dir of the current PHP process * @param array|null $env The environment variables or null to use the same environment as the current PHP process * @param mixed|null $input The input as stream resource, scalar or \Traversable, or null for no input * @param int|float|null $timeout The timeout in seconds or null to disable * @param array $options An array of options for proc_open * * @throws RuntimeException When proc_open is not installed */ public function __construct($commandline, $cwd = null, array $env = null, $input = null, $timeout = 60, array $options = null) { if (!\function_exists('proc_open')) { throw new RuntimeException('The Process class relies on proc_open, which is not available on your PHP installation.'); } $this->commandline = $commandline; $this->cwd = $cwd; // on Windows, if the cwd changed via chdir(), proc_open defaults to the dir where PHP was started // on Gnu/Linux, PHP builds with --enable-maintainer-zts are also affected // @see : https://bugs.php.net/51800 // @see : https://bugs.php.net/50524 if (null === $this->cwd && (\defined('ZEND_THREAD_SAFE') || '\\' === \DIRECTORY_SEPARATOR)) { $this->cwd = getcwd(); } if (null !== $env) { $this->setEnv($env); } $this->setInput($input); $this->setTimeout($timeout); $this->useFileHandles = '\\' === \DIRECTORY_SEPARATOR; $this->pty = false; $this->enhanceSigchildCompatibility = '\\' !== \DIRECTORY_SEPARATOR && $this->isSigchildEnabled(); if (null !== $options) { @trigger_error(sprintf('The $options parameter of the %s constructor is deprecated since Symfony 3.3 and will be removed in 4.0.', __CLASS__), \E_USER_DEPRECATED); $this->options = array_replace($this->options, $options); } } public function __destruct() { $this->stop(0); } public function __clone() { $this->resetProcessData(); } /** * Runs the process. * * The callback receives the type of output (out or err) and * some bytes from the output in real-time. It allows to have feedback * from the independent process during execution. * * The STDOUT and STDERR are also available after the process is finished * via the getOutput() and getErrorOutput() methods. * * @param callable|null $callback A PHP callback to run whenever there is some * output available on STDOUT or STDERR * * @return int The exit status code * * @throws RuntimeException When process can't be launched * @throws RuntimeException When process stopped after receiving signal * @throws LogicException In case a callback is provided and output has been disabled * * @final since version 3.3 */ public function run($callback = null/*, array $env = []*/) { $env = 1 < \func_num_args() ? func_get_arg(1) : null; $this->start($callback, $env); return $this->wait(); } /** * Runs the process. * * This is identical to run() except that an exception is thrown if the process * exits with a non-zero exit code. * * @return $this * * @throws RuntimeException if PHP was compiled with --enable-sigchild and the enhanced sigchild compatibility mode is not enabled * @throws ProcessFailedException if the process didn't terminate successfully * * @final since version 3.3 */ public function mustRun(callable $callback = null/*, array $env = []*/) { if (!$this->enhanceSigchildCompatibility && $this->isSigchildEnabled()) { throw new RuntimeException('This PHP has been compiled with --enable-sigchild. You must use setEnhanceSigchildCompatibility() to use this method.'); } $env = 1 < \func_num_args() ? func_get_arg(1) : null; if (0 !== $this->run($callback, $env)) { throw new ProcessFailedException($this); } return $this; } /** * Starts the process and returns after writing the input to STDIN. * * This method blocks until all STDIN data is sent to the process then it * returns while the process runs in the background. * * The termination of the process can be awaited with wait(). * * The callback receives the type of output (out or err) and some bytes from * the output in real-time while writing the standard input to the process. * It allows to have feedback from the independent process during execution. * * @param callable|null $callback A PHP callback to run whenever there is some * output available on STDOUT or STDERR * * @throws RuntimeException When process can't be launched * @throws RuntimeException When process is already running * @throws LogicException In case a callback is provided and output has been disabled */ public function start(callable $callback = null/*, array $env = [*/) { if ($this->isRunning()) { throw new RuntimeException('Process is already running.'); } if (2 <= \func_num_args()) { $env = func_get_arg(1); } else { if (__CLASS__ !== static::class) { $r = new \ReflectionMethod($this, __FUNCTION__); if (__CLASS__ !== $r->getDeclaringClass()->getName() && (2 > $r->getNumberOfParameters() || 'env' !== $r->getParameters()[1]->name)) { @trigger_error(sprintf('The %s::start() method expects a second "$env" argument since Symfony 3.3. It will be made mandatory in 4.0.', static::class), \E_USER_DEPRECATED); } } $env = null; } $this->resetProcessData(); $this->starttime = $this->lastOutputTime = microtime(true); $this->callback = $this->buildCallback($callback); $this->hasCallback = null !== $callback; $descriptors = $this->getDescriptors(); $inheritEnv = $this->inheritEnv; if (\is_array($commandline = $this->commandline)) { $commandline = implode(' ', array_map([$this, 'escapeArgument'], $commandline)); if ('\\' !== \DIRECTORY_SEPARATOR) { // exec is mandatory to deal with sending a signal to the process $commandline = 'exec '.$commandline; } } if (null === $env) { $env = $this->env; } else { if ($this->env) { $env += $this->env; } $inheritEnv = true; } if (null !== $env && $inheritEnv) { $env += $this->getDefaultEnv(); } elseif (null !== $env) { @trigger_error('Not inheriting environment variables is deprecated since Symfony 3.3 and will always happen in 4.0. Set "Process::inheritEnvironmentVariables()" to true instead.', \E_USER_DEPRECATED); } else { $env = $this->getDefaultEnv(); } if ('\\' === \DIRECTORY_SEPARATOR && $this->enhanceWindowsCompatibility) { $this->options['bypass_shell'] = true; $commandline = $this->prepareWindowsCommandLine($commandline, $env); } elseif (!$this->useFileHandles && $this->enhanceSigchildCompatibility && $this->isSigchildEnabled()) { // last exit code is output on the fourth pipe and caught to work around --enable-sigchild $descriptors[3] = ['pipe', 'w']; // See https://unix.stackexchange.com/questions/71205/background-process-pipe-input $commandline = '{ ('.$commandline.') <&3 3<&- 3>/dev/null & } 3<&0;'; $commandline .= 'pid=$!; echo $pid >&3; wait $pid; code=$?; echo $code >&3; exit $code'; // Workaround for the bug, when PTS functionality is enabled. // @see : https://bugs.php.net/69442 $ptsWorkaround = fopen(__FILE__, 'r'); } if (\defined('HHVM_VERSION')) { $envPairs = $env; } else { $envPairs = []; foreach ($env as $k => $v) { if (false !== $v) { $envPairs[] = $k.'='.$v; } } } if (!is_dir($this->cwd)) { @trigger_error('The provided cwd does not exist. Command is currently ran against getcwd(). This behavior is deprecated since Symfony 3.4 and will be removed in 4.0.', \E_USER_DEPRECATED); } $this->process = @proc_open($commandline, $descriptors, $this->processPipes->pipes, $this->cwd, $envPairs, $this->options); if (!\is_resource($this->process)) { throw new RuntimeException('Unable to launch a new process.'); } $this->status = self::STATUS_STARTED; if (isset($descriptors[3])) { $this->fallbackStatus['pid'] = (int) fgets($this->processPipes->pipes[3]); } if ($this->tty) { return; } $this->updateStatus(false); $this->checkTimeout(); } /** * Restarts the process. * * Be warned that the process is cloned before being started. * * @param callable|null $callback A PHP callback to run whenever there is some * output available on STDOUT or STDERR * * @return static * * @throws RuntimeException When process can't be launched * @throws RuntimeException When process is already running * * @see start() * * @final since version 3.3 */ public function restart(callable $callback = null/*, array $env = []*/) { if ($this->isRunning()) { throw new RuntimeException('Process is already running.'); } $env = 1 < \func_num_args() ? func_get_arg(1) : null; $process = clone $this; $process->start($callback, $env); return $process; } /** * Waits for the process to terminate. * * The callback receives the type of output (out or err) and some bytes * from the output in real-time while writing the standard input to the process. * It allows to have feedback from the independent process during execution. * * @param callable|null $callback A valid PHP callback * * @return int The exitcode of the process * * @throws RuntimeException When process timed out * @throws RuntimeException When process stopped after receiving signal * @throws LogicException When process is not yet started */ public function wait(callable $callback = null) { $this->requireProcessIsStarted(__FUNCTION__); $this->updateStatus(false); if (null !== $callback) { if (!$this->processPipes->haveReadSupport()) { $this->stop(0); throw new \LogicException('Pass the callback to the Process::start method or enableOutput to use a callback with Process::wait.'); } $this->callback = $this->buildCallback($callback); } do { $this->checkTimeout(); $running = '\\' === \DIRECTORY_SEPARATOR ? $this->isRunning() : $this->processPipes->areOpen(); $this->readPipes($running, '\\' !== \DIRECTORY_SEPARATOR || !$running); } while ($running); while ($this->isRunning()) { $this->checkTimeout(); usleep(1000); } if ($this->processInformation['signaled'] && $this->processInformation['termsig'] !== $this->latestSignal) { throw new RuntimeException(sprintf('The process has been signaled with signal "%s".', $this->processInformation['termsig'])); } return $this->exitcode; } /** * Returns the Pid (process identifier), if applicable. * * @return int|null The process id if running, null otherwise */ public function getPid() { return $this->isRunning() ? $this->processInformation['pid'] : null; } /** * Sends a POSIX signal to the process. * * @param int $signal A valid POSIX signal (see https://php.net/pcntl.constants) * * @return $this * * @throws LogicException In case the process is not running * @throws RuntimeException In case --enable-sigchild is activated and the process can't be killed * @throws RuntimeException In case of failure */ public function signal($signal) { $this->doSignal($signal, true); return $this; } /** * Disables fetching output and error output from the underlying process. * * @return $this * * @throws RuntimeException In case the process is already running * @throws LogicException if an idle timeout is set */ public function disableOutput() { if ($this->isRunning()) { throw new RuntimeException('Disabling output while the process is running is not possible.'); } if (null !== $this->idleTimeout) { throw new LogicException('Output can not be disabled while an idle timeout is set.'); } $this->outputDisabled = true; return $this; } /** * Enables fetching output and error output from the underlying process. * * @return $this * * @throws RuntimeException In case the process is already running */ public function enableOutput() { if ($this->isRunning()) { throw new RuntimeException('Enabling output while the process is running is not possible.'); } $this->outputDisabled = false; return $this; } /** * Returns true in case the output is disabled, false otherwise. * * @return bool */ public function isOutputDisabled() { return $this->outputDisabled; } /** * Returns the current output of the process (STDOUT). * * @return string The process output * * @throws LogicException in case the output has been disabled * @throws LogicException In case the process is not started */ public function getOutput() { $this->readPipesForOutput(__FUNCTION__); if (false === $ret = stream_get_contents($this->stdout, -1, 0)) { return ''; } return $ret; } /** * Returns the output incrementally. * * In comparison with the getOutput method which always return the whole * output, this one returns the new output since the last call. * * @return string The process output since the last call * * @throws LogicException in case the output has been disabled * @throws LogicException In case the process is not started */ public function getIncrementalOutput() { $this->readPipesForOutput(__FUNCTION__); $latest = stream_get_contents($this->stdout, -1, $this->incrementalOutputOffset); $this->incrementalOutputOffset = ftell($this->stdout); if (false === $latest) { return ''; } return $latest; } /** * Returns an iterator to the output of the process, with the output type as keys (Process::OUT/ERR). * * @param int $flags A bit field of Process::ITER_* flags * * @throws LogicException in case the output has been disabled * @throws LogicException In case the process is not started * * @return \Generator */ public function getIterator($flags = 0) { $this->readPipesForOutput(__FUNCTION__, false); $clearOutput = !(self::ITER_KEEP_OUTPUT & $flags); $blocking = !(self::ITER_NON_BLOCKING & $flags); $yieldOut = !(self::ITER_SKIP_OUT & $flags); $yieldErr = !(self::ITER_SKIP_ERR & $flags); while (null !== $this->callback || ($yieldOut && !feof($this->stdout)) || ($yieldErr && !feof($this->stderr))) { if ($yieldOut) { $out = stream_get_contents($this->stdout, -1, $this->incrementalOutputOffset); if (isset($out[0])) { if ($clearOutput) { $this->clearOutput(); } else { $this->incrementalOutputOffset = ftell($this->stdout); } yield self::OUT => $out; } } if ($yieldErr) { $err = stream_get_contents($this->stderr, -1, $this->incrementalErrorOutputOffset); if (isset($err[0])) { if ($clearOutput) { $this->clearErrorOutput(); } else { $this->incrementalErrorOutputOffset = ftell($this->stderr); } yield self::ERR => $err; } } if (!$blocking && !isset($out[0]) && !isset($err[0])) { yield self::OUT => ''; } $this->checkTimeout(); $this->readPipesForOutput(__FUNCTION__, $blocking); } } /** * Clears the process output. * * @return $this */ public function clearOutput() { ftruncate($this->stdout, 0); fseek($this->stdout, 0); $this->incrementalOutputOffset = 0; return $this; } /** * Returns the current error output of the process (STDERR). * * @return string The process error output * * @throws LogicException in case the output has been disabled * @throws LogicException In case the process is not started */ public function getErrorOutput() { $this->readPipesForOutput(__FUNCTION__); if (false === $ret = stream_get_contents($this->stderr, -1, 0)) { return ''; } return $ret; } /** * Returns the errorOutput incrementally. * * In comparison with the getErrorOutput method which always return the * whole error output, this one returns the new error output since the last * call. * * @return string The process error output since the last call * * @throws LogicException in case the output has been disabled * @throws LogicException In case the process is not started */ public function getIncrementalErrorOutput() { $this->readPipesForOutput(__FUNCTION__); $latest = stream_get_contents($this->stderr, -1, $this->incrementalErrorOutputOffset); $this->incrementalErrorOutputOffset = ftell($this->stderr); if (false === $latest) { return ''; } return $latest; } /** * Clears the process output. * * @return $this */ public function clearErrorOutput() { ftruncate($this->stderr, 0); fseek($this->stderr, 0); $this->incrementalErrorOutputOffset = 0; return $this; } /** * Returns the exit code returned by the process. * * @return int|null The exit status code, null if the Process is not terminated * * @throws RuntimeException In case --enable-sigchild is activated and the sigchild compatibility mode is disabled */ public function getExitCode() { if (!$this->enhanceSigchildCompatibility && $this->isSigchildEnabled()) { throw new RuntimeException('This PHP has been compiled with --enable-sigchild. You must use setEnhanceSigchildCompatibility() to use this method.'); } $this->updateStatus(false); return $this->exitcode; } /** * Returns a string representation for the exit code returned by the process. * * This method relies on the Unix exit code status standardization * and might not be relevant for other operating systems. * * @return string|null A string representation for the exit status code, null if the Process is not terminated * * @see http://tldp.org/LDP/abs/html/exitcodes.html * @see http://en.wikipedia.org/wiki/Unix_signal */ public function getExitCodeText() { if (null === $exitcode = $this->getExitCode()) { return null; } return isset(self::$exitCodes[$exitcode]) ? self::$exitCodes[$exitcode] : 'Unknown error'; } /** * Checks if the process ended successfully. * * @return bool true if the process ended successfully, false otherwise */ public function isSuccessful() { return 0 === $this->getExitCode(); } /** * Returns true if the child process has been terminated by an uncaught signal. * * It always returns false on Windows. * * @return bool * * @throws RuntimeException In case --enable-sigchild is activated * @throws LogicException In case the process is not terminated */ public function hasBeenSignaled() { $this->requireProcessIsTerminated(__FUNCTION__); if (!$this->enhanceSigchildCompatibility && $this->isSigchildEnabled()) { throw new RuntimeException('This PHP has been compiled with --enable-sigchild. Term signal can not be retrieved.'); } return $this->processInformation['signaled']; } /** * Returns the number of the signal that caused the child process to terminate its execution. * * It is only meaningful if hasBeenSignaled() returns true. * * @return int * * @throws RuntimeException In case --enable-sigchild is activated * @throws LogicException In case the process is not terminated */ public function getTermSignal() { $this->requireProcessIsTerminated(__FUNCTION__); if ($this->isSigchildEnabled() && (!$this->enhanceSigchildCompatibility || -1 === $this->processInformation['termsig'])) { throw new RuntimeException('This PHP has been compiled with --enable-sigchild. Term signal can not be retrieved.'); } return $this->processInformation['termsig']; } /** * Returns true if the child process has been stopped by a signal. * * It always returns false on Windows. * * @return bool * * @throws LogicException In case the process is not terminated */ public function hasBeenStopped() { $this->requireProcessIsTerminated(__FUNCTION__); return $this->processInformation['stopped']; } /** * Returns the number of the signal that caused the child process to stop its execution. * * It is only meaningful if hasBeenStopped() returns true. * * @return int * * @throws LogicException In case the process is not terminated */ public function getStopSignal() { $this->requireProcessIsTerminated(__FUNCTION__); return $this->processInformation['stopsig']; } /** * Checks if the process is currently running. * * @return bool true if the process is currently running, false otherwise */ public function isRunning() { if (self::STATUS_STARTED !== $this->status) { return false; } $this->updateStatus(false); return $this->processInformation['running']; } /** * Checks if the process has been started with no regard to the current state. * * @return bool true if status is ready, false otherwise */ public function isStarted() { return self::STATUS_READY != $this->status; } /** * Checks if the process is terminated. * * @return bool true if process is terminated, false otherwise */ public function isTerminated() { $this->updateStatus(false); return self::STATUS_TERMINATED == $this->status; } /** * Gets the process status. * * The status is one of: ready, started, terminated. * * @return string The current process status */ public function getStatus() { $this->updateStatus(false); return $this->status; } /** * Stops the process. * * @param int|float $timeout The timeout in seconds * @param int $signal A POSIX signal to send in case the process has not stop at timeout, default is SIGKILL (9) * * @return int|null The exit-code of the process or null if it's not running */ public function stop($timeout = 10, $signal = null) { $timeoutMicro = microtime(true) + $timeout; if ($this->isRunning()) { // given `SIGTERM` may not be defined and that `proc_terminate` uses the constant value and not the constant itself, we use the same here $this->doSignal(15, false); do { usleep(1000); } while ($this->isRunning() && microtime(true) < $timeoutMicro); if ($this->isRunning()) { // Avoid exception here: process is supposed to be running, but it might have stopped just // after this line. In any case, let's silently discard the error, we cannot do anything. $this->doSignal($signal ?: 9, false); } } if ($this->isRunning()) { if (isset($this->fallbackStatus['pid'])) { unset($this->fallbackStatus['pid']); return $this->stop(0, $signal); } $this->close(); } return $this->exitcode; } /** * Adds a line to the STDOUT stream. * * @internal * * @param string $line The line to append */ public function addOutput($line) { $this->lastOutputTime = microtime(true); fseek($this->stdout, 0, \SEEK_END); fwrite($this->stdout, $line); fseek($this->stdout, $this->incrementalOutputOffset); } /** * Adds a line to the STDERR stream. * * @internal * * @param string $line The line to append */ public function addErrorOutput($line) { $this->lastOutputTime = microtime(true); fseek($this->stderr, 0, \SEEK_END); fwrite($this->stderr, $line); fseek($this->stderr, $this->incrementalErrorOutputOffset); } /** * Gets the command line to be executed. * * @return string The command to execute */ public function getCommandLine() { return \is_array($this->commandline) ? implode(' ', array_map([$this, 'escapeArgument'], $this->commandline)) : $this->commandline; } /** * Sets the command line to be executed. * * @param string|array $commandline The command to execute * * @return $this */ public function setCommandLine($commandline) { $this->commandline = $commandline; return $this; } /** * Gets the process timeout (max. runtime). * * @return float|null The timeout in seconds or null if it's disabled */ public function getTimeout() { return $this->timeout; } /** * Gets the process idle timeout (max. time since last output). * * @return float|null The timeout in seconds or null if it's disabled */ public function getIdleTimeout() { return $this->idleTimeout; } /** * Sets the process timeout (max. runtime) in seconds. * * To disable the timeout, set this value to null. * * @param int|float|null $timeout The timeout in seconds * * @return $this * * @throws InvalidArgumentException if the timeout is negative */ public function setTimeout($timeout) { $this->timeout = $this->validateTimeout($timeout); return $this; } /** * Sets the process idle timeout (max. time since last output). * * To disable the timeout, set this value to null. * * @param int|float|null $timeout The timeout in seconds * * @return $this * * @throws LogicException if the output is disabled * @throws InvalidArgumentException if the timeout is negative */ public function setIdleTimeout($timeout) { if (null !== $timeout && $this->outputDisabled) { throw new LogicException('Idle timeout can not be set while the output is disabled.'); } $this->idleTimeout = $this->validateTimeout($timeout); return $this; } /** * Enables or disables the TTY mode. * * @param bool $tty True to enabled and false to disable * * @return $this * * @throws RuntimeException In case the TTY mode is not supported */ public function setTty($tty) { if ('\\' === \DIRECTORY_SEPARATOR && $tty) { throw new RuntimeException('TTY mode is not supported on Windows platform.'); } if ($tty) { static $isTtySupported; if (null === $isTtySupported) { $isTtySupported = (bool) @proc_open('echo 1 >/dev/null', [['file', '/dev/tty', 'r'], ['file', '/dev/tty', 'w'], ['file', '/dev/tty', 'w']], $pipes); } if (!$isTtySupported) { throw new RuntimeException('TTY mode requires /dev/tty to be read/writable.'); } } $this->tty = (bool) $tty; return $this; } /** * Checks if the TTY mode is enabled. * * @return bool true if the TTY mode is enabled, false otherwise */ public function isTty() { return $this->tty; } /** * Sets PTY mode. * * @param bool $bool * * @return $this */ public function setPty($bool) { $this->pty = (bool) $bool; return $this; } /** * Returns PTY state. * * @return bool */ public function isPty() { return $this->pty; } /** * Gets the working directory. * * @return string|null The current working directory or null on failure */ public function getWorkingDirectory() { if (null === $this->cwd) { // getcwd() will return false if any one of the parent directories does not have // the readable or search mode set, even if the current directory does return getcwd() ?: null; } return $this->cwd; } /** * Sets the current working directory. * * @param string $cwd The new working directory * * @return $this */ public function setWorkingDirectory($cwd) { $this->cwd = $cwd; return $this; } /** * Gets the environment variables. * * @return array The current environment variables */ public function getEnv() { return $this->env; } /** * Sets the environment variables. * * Each environment variable value should be a string. * If it is an array, the variable is ignored. * If it is false or null, it will be removed when * env vars are otherwise inherited. * * That happens in PHP when 'argv' is registered into * the $_ENV array for instance. * * @param array $env The new environment variables * * @return $this */ public function setEnv(array $env) { // Process can not handle env values that are arrays $env = array_filter($env, function ($value) { return !\is_array($value); }); $this->env = $env; return $this; } /** * Gets the Process input. * * @return resource|string|\Iterator|null The Process input */ public function getInput() { return $this->input; } /** * Sets the input. * * This content will be passed to the underlying process standard input. * * @param string|int|float|bool|resource|\Traversable|null $input The content * * @return $this * * @throws LogicException In case the process is running */ public function setInput($input) { if ($this->isRunning()) { throw new LogicException('Input can not be set while the process is running.'); } $this->input = ProcessUtils::validateInput(__METHOD__, $input); return $this; } /** * Gets the options for proc_open. * * @return array The current options * * @deprecated since version 3.3, to be removed in 4.0. */ public function getOptions() { @trigger_error(sprintf('The %s() method is deprecated since Symfony 3.3 and will be removed in 4.0.', __METHOD__), \E_USER_DEPRECATED); return $this->options; } /** * Sets the options for proc_open. * * @param array $options The new options * * @return $this * * @deprecated since version 3.3, to be removed in 4.0. */ public function setOptions(array $options) { @trigger_error(sprintf('The %s() method is deprecated since Symfony 3.3 and will be removed in 4.0.', __METHOD__), \E_USER_DEPRECATED); $this->options = $options; return $this; } /** * Gets whether or not Windows compatibility is enabled. * * This is true by default. * * @return bool * * @deprecated since version 3.3, to be removed in 4.0. Enhanced Windows compatibility will always be enabled. */ public function getEnhanceWindowsCompatibility() { @trigger_error(sprintf('The %s() method is deprecated since Symfony 3.3 and will be removed in 4.0. Enhanced Windows compatibility will always be enabled.', __METHOD__), \E_USER_DEPRECATED); return $this->enhanceWindowsCompatibility; } /** * Sets whether or not Windows compatibility is enabled. * * @param bool $enhance * * @return $this * * @deprecated since version 3.3, to be removed in 4.0. Enhanced Windows compatibility will always be enabled. */ public function setEnhanceWindowsCompatibility($enhance) { @trigger_error(sprintf('The %s() method is deprecated since Symfony 3.3 and will be removed in 4.0. Enhanced Windows compatibility will always be enabled.', __METHOD__), \E_USER_DEPRECATED); $this->enhanceWindowsCompatibility = (bool) $enhance; return $this; } /** * Returns whether sigchild compatibility mode is activated or not. * * @return bool * * @deprecated since version 3.3, to be removed in 4.0. Sigchild compatibility will always be enabled. */ public function getEnhanceSigchildCompatibility() { @trigger_error(sprintf('The %s() method is deprecated since Symfony 3.3 and will be removed in 4.0. Sigchild compatibility will always be enabled.', __METHOD__), \E_USER_DEPRECATED); return $this->enhanceSigchildCompatibility; } /** * Activates sigchild compatibility mode. * * Sigchild compatibility mode is required to get the exit code and * determine the success of a process when PHP has been compiled with * the --enable-sigchild option * * @param bool $enhance * * @return $this * * @deprecated since version 3.3, to be removed in 4.0. */ public function setEnhanceSigchildCompatibility($enhance) { @trigger_error(sprintf('The %s() method is deprecated since Symfony 3.3 and will be removed in 4.0. Sigchild compatibility will always be enabled.', __METHOD__), \E_USER_DEPRECATED); $this->enhanceSigchildCompatibility = (bool) $enhance; return $this; } /** * Sets whether environment variables will be inherited or not. * * @param bool $inheritEnv * * @return $this */ public function inheritEnvironmentVariables($inheritEnv = true) { if (!$inheritEnv) { @trigger_error('Not inheriting environment variables is deprecated since Symfony 3.3 and will always happen in 4.0. Set "Process::inheritEnvironmentVariables()" to true instead.', \E_USER_DEPRECATED); } $this->inheritEnv = (bool) $inheritEnv; return $this; } /** * Returns whether environment variables will be inherited or not. * * @return bool * * @deprecated since version 3.3, to be removed in 4.0. Environment variables will always be inherited. */ public function areEnvironmentVariablesInherited() { @trigger_error(sprintf('The %s() method is deprecated since Symfony 3.3 and will be removed in 4.0. Environment variables will always be inherited.', __METHOD__), \E_USER_DEPRECATED); return $this->inheritEnv; } /** * Performs a check between the timeout definition and the time the process started. * * In case you run a background process (with the start method), you should * trigger this method regularly to ensure the process timeout * * @throws ProcessTimedOutException In case the timeout was reached */ public function checkTimeout() { if (self::STATUS_STARTED !== $this->status) { return; } if (null !== $this->timeout && $this->timeout < microtime(true) - $this->starttime) { $this->stop(0); throw new ProcessTimedOutException($this, ProcessTimedOutException::TYPE_GENERAL); } if (null !== $this->idleTimeout && $this->idleTimeout < microtime(true) - $this->lastOutputTime) { $this->stop(0); throw new ProcessTimedOutException($this, ProcessTimedOutException::TYPE_IDLE); } } /** * Returns whether PTY is supported on the current operating system. * * @return bool */ public static function isPtySupported() { static $result; if (null !== $result) { return $result; } if ('\\' === \DIRECTORY_SEPARATOR) { return $result = false; } return $result = (bool) @proc_open('echo 1 >/dev/null', [['pty'], ['pty'], ['pty']], $pipes); } /** * Creates the descriptors needed by the proc_open. * * @return array */ private function getDescriptors() { if ($this->input instanceof \Iterator) { $this->input->rewind(); } if ('\\' === \DIRECTORY_SEPARATOR) { $this->processPipes = new WindowsPipes($this->input, !$this->outputDisabled || $this->hasCallback); } else { $this->processPipes = new UnixPipes($this->isTty(), $this->isPty(), $this->input, !$this->outputDisabled || $this->hasCallback); } return $this->processPipes->getDescriptors(); } /** * Builds up the callback used by wait(). * * The callbacks adds all occurred output to the specific buffer and calls * the user callback (if present) with the received output. * * @param callable|null $callback The user defined PHP callback * * @return \Closure A PHP closure */ protected function buildCallback(callable $callback = null) { if ($this->outputDisabled) { return function ($type, $data) use ($callback) { if (null !== $callback) { \call_user_func($callback, $type, $data); } }; } $out = self::OUT; return function ($type, $data) use ($callback, $out) { if ($out == $type) { $this->addOutput($data); } else { $this->addErrorOutput($data); } if (null !== $callback) { \call_user_func($callback, $type, $data); } }; } /** * Updates the status of the process, reads pipes. * * @param bool $blocking Whether to use a blocking read call */ protected function updateStatus($blocking) { if (self::STATUS_STARTED !== $this->status) { return; } $this->processInformation = proc_get_status($this->process); $running = $this->processInformation['running']; $this->readPipes($running && $blocking, '\\' !== \DIRECTORY_SEPARATOR || !$running); if ($this->fallbackStatus && $this->enhanceSigchildCompatibility && $this->isSigchildEnabled()) { $this->processInformation = $this->fallbackStatus + $this->processInformation; } if (!$running) { $this->close(); } } /** * Returns whether PHP has been compiled with the '--enable-sigchild' option or not. * * @return bool */ protected function isSigchildEnabled() { if (null !== self::$sigchild) { return self::$sigchild; } if (!\function_exists('phpinfo') || \defined('HHVM_VERSION')) { return self::$sigchild = false; } ob_start(); phpinfo(\INFO_GENERAL); return self::$sigchild = false !== strpos(ob_get_clean(), '--enable-sigchild'); } /** * Reads pipes for the freshest output. * * @param string $caller The name of the method that needs fresh outputs * @param bool $blocking Whether to use blocking calls or not * * @throws LogicException in case output has been disabled or process is not started */ private function readPipesForOutput($caller, $blocking = false) { if ($this->outputDisabled) { throw new LogicException('Output has been disabled.'); } $this->requireProcessIsStarted($caller); $this->updateStatus($blocking); } /** * Validates and returns the filtered timeout. * * @param int|float|null $timeout * * @return float|null * * @throws InvalidArgumentException if the given timeout is a negative number */ private function validateTimeout($timeout) { $timeout = (float) $timeout; if (0.0 === $timeout) { $timeout = null; } elseif ($timeout < 0) { throw new InvalidArgumentException('The timeout value must be a valid positive integer or float number.'); } return $timeout; } /** * Reads pipes, executes callback. * * @param bool $blocking Whether to use blocking calls or not * @param bool $close Whether to close file handles or not */ private function readPipes($blocking, $close) { $result = $this->processPipes->readAndWrite($blocking, $close); $callback = $this->callback; foreach ($result as $type => $data) { if (3 !== $type) { $callback(self::STDOUT === $type ? self::OUT : self::ERR, $data); } elseif (!isset($this->fallbackStatus['signaled'])) { $this->fallbackStatus['exitcode'] = (int) $data; } } } /** * Closes process resource, closes file handles, sets the exitcode. * * @return int The exitcode */ private function close() { $this->processPipes->close(); if (\is_resource($this->process)) { proc_close($this->process); } $this->exitcode = $this->processInformation['exitcode']; $this->status = self::STATUS_TERMINATED; if (-1 === $this->exitcode) { if ($this->processInformation['signaled'] && 0 < $this->processInformation['termsig']) { // if process has been signaled, no exitcode but a valid termsig, apply Unix convention $this->exitcode = 128 + $this->processInformation['termsig']; } elseif ($this->enhanceSigchildCompatibility && $this->isSigchildEnabled()) { $this->processInformation['signaled'] = true; $this->processInformation['termsig'] = -1; } } // Free memory from self-reference callback created by buildCallback // Doing so in other contexts like __destruct or by garbage collector is ineffective // Now pipes are closed, so the callback is no longer necessary $this->callback = null; return $this->exitcode; } /** * Resets data related to the latest run of the process. */ private function resetProcessData() { $this->starttime = null; $this->callback = null; $this->exitcode = null; $this->fallbackStatus = []; $this->processInformation = null; $this->stdout = fopen('php://temp/maxmemory:'.(1024 * 1024), 'w+b'); $this->stderr = fopen('php://temp/maxmemory:'.(1024 * 1024), 'w+b'); $this->process = null; $this->latestSignal = null; $this->status = self::STATUS_READY; $this->incrementalOutputOffset = 0; $this->incrementalErrorOutputOffset = 0; } /** * Sends a POSIX signal to the process. * * @param int $signal A valid POSIX signal (see https://php.net/pcntl.constants) * @param bool $throwException Whether to throw exception in case signal failed * * @return bool True if the signal was sent successfully, false otherwise * * @throws LogicException In case the process is not running * @throws RuntimeException In case --enable-sigchild is activated and the process can't be killed * @throws RuntimeException In case of failure */ private function doSignal($signal, $throwException) { if (null === $pid = $this->getPid()) { if ($throwException) { throw new LogicException('Can not send signal on a non running process.'); } return false; } if ('\\' === \DIRECTORY_SEPARATOR) { exec(sprintf('taskkill /F /T /PID %d 2>&1', $pid), $output, $exitCode); if ($exitCode && $this->isRunning()) { if ($throwException) { throw new RuntimeException(sprintf('Unable to kill the process (%s).', implode(' ', $output))); } return false; } } else { if (!$this->enhanceSigchildCompatibility || !$this->isSigchildEnabled()) { $ok = @proc_terminate($this->process, $signal); } elseif (\function_exists('posix_kill')) { $ok = @posix_kill($pid, $signal); } elseif ($ok = proc_open(sprintf('kill -%d %d', $signal, $pid), [2 => ['pipe', 'w']], $pipes)) { $ok = false === fgets($pipes[2]); } if (!$ok) { if ($throwException) { throw new RuntimeException(sprintf('Error while sending signal `%s`.', $signal)); } return false; } } $this->latestSignal = (int) $signal; $this->fallbackStatus['signaled'] = true; $this->fallbackStatus['exitcode'] = -1; $this->fallbackStatus['termsig'] = $this->latestSignal; return true; } private function prepareWindowsCommandLine($cmd, array &$env) { $uid = uniqid('', true); $varCount = 0; $varCache = []; $cmd = preg_replace_callback( '/"(?:( [^"%!^]*+ (?: (?: !LF! | "(?:\^[%!^])?+" ) [^"%!^]*+ )++ ) | [^"]*+ )"/x', function ($m) use (&$env, &$varCache, &$varCount, $uid) { if (!isset($m[1])) { return $m[0]; } if (isset($varCache[$m[0]])) { return $varCache[$m[0]]; } if (false !== strpos($value = $m[1], "\0")) { $value = str_replace("\0", '?', $value); } if (false === strpbrk($value, "\"%!\n")) { return '"'.$value.'"'; } $value = str_replace(['!LF!', '"^!"', '"^%"', '"^^"', '""'], ["\n", '!', '%', '^', '"'], $value); $value = '"'.preg_replace('/(\\\\*)"/', '$1$1\\"', $value).'"'; $var = $uid.++$varCount; $env[$var] = $value; return $varCache[$m[0]] = '!'.$var.'!'; }, $cmd ); $cmd = 'cmd /V:ON /E:ON /D /C ('.str_replace("\n", ' ', $cmd).')'; foreach ($this->processPipes->getFiles() as $offset => $filename) { $cmd .= ' '.$offset.'>"'.$filename.'"'; } return $cmd; } /** * Ensures the process is running or terminated, throws a LogicException if the process has a not started. * * @param string $functionName The function name that was called * * @throws LogicException if the process has not run */ private function requireProcessIsStarted($functionName) { if (!$this->isStarted()) { throw new LogicException(sprintf('Process must be started before calling "%s()".', $functionName)); } } /** * Ensures the process is terminated, throws a LogicException if the process has a status different than `terminated`. * * @param string $functionName The function name that was called * * @throws LogicException if the process is not yet terminated */ private function requireProcessIsTerminated($functionName) { if (!$this->isTerminated()) { throw new LogicException(sprintf('Process must be terminated before calling "%s()".', $functionName)); } } /** * Escapes a string to be used as a shell argument. * * @param string $argument The argument that will be escaped * * @return string The escaped argument */ private function escapeArgument($argument) { if ('\\' !== \DIRECTORY_SEPARATOR) { return "'".str_replace("'", "'\\''", $argument)."'"; } if ('' === $argument = (string) $argument) { return '""'; } if (false !== strpos($argument, "\0")) { $argument = str_replace("\0", '?', $argument); } if (!preg_match('/[\/()%!^"<>&|\s]/', $argument)) { return $argument; } $argument = preg_replace('/(\\\\+)$/', '$1$1', $argument); return '"'.str_replace(['"', '^', '%', '!', "\n"], ['""', '"^^"', '"^%"', '"^!"', '!LF!'], $argument).'"'; } private function getDefaultEnv() { $env = []; foreach ($_SERVER as $k => $v) { if (\is_string($v) && false !== $v = getenv($k)) { $env[$k] = $v; } } foreach ($_ENV as $k => $v) { if (\is_string($v)) { $env[$k] = $v; } } return $env; } } reason = $reason; } public function then(callable $onFulfilled = null, callable $onRejected = null, callable $onProgress = null) { if (null === $onRejected) { return $this; } try { return resolve($onRejected($this->reason)); } catch (\Throwable $exception) { return new RejectedPromise($exception); } catch (\Exception $exception) { return new RejectedPromise($exception); } } public function done(callable $onFulfilled = null, callable $onRejected = null, callable $onProgress = null) { if (null === $onRejected) { throw UnhandledRejectionException::resolve($this->reason); } $result = $onRejected($this->reason); if ($result instanceof self) { throw UnhandledRejectionException::resolve($result->reason); } if ($result instanceof ExtendedPromiseInterface) { $result->done(); } } public function otherwise(callable $onRejected) { if (!_checkTypehint($onRejected, $this->reason)) { return $this; } return $this->then(null, $onRejected); } public function always(callable $onFulfilledOrRejected) { return $this->then(null, function ($reason) use ($onFulfilledOrRejected) { return resolve($onFulfilledOrRejected())->then(function () use ($reason) { return new RejectedPromise($reason); }); }); } public function progress(callable $onProgress) { return $this; } public function cancel() { } } canceller = $canceller; } public function promise() { if (null === $this->promise) { $this->promise = new Promise(function ($resolve, $reject, $notify) { $this->resolveCallback = $resolve; $this->rejectCallback = $reject; $this->notifyCallback = $notify; }, $this->canceller); $this->canceller = null; } return $this->promise; } public function resolve($value = null) { $this->promise(); \call_user_func($this->resolveCallback, $value); } public function reject($reason = null) { $this->promise(); \call_user_func($this->rejectCallback, $reason); } /** * @deprecated 2.6.0 Progress support is deprecated and should not be used anymore. * @param mixed $update */ public function notify($update = null) { $this->promise(); \call_user_func($this->notifyCallback, $update); } /** * @deprecated 2.2.0 * @see Deferred::notify() */ public function progress($update = null) { $this->notify($update); } } then($resolve, $reject, $notify); }, $canceller); } return new FulfilledPromise($promiseOrValue); } /** * Creates a rejected promise for the supplied `$promiseOrValue`. * * If `$promiseOrValue` is a value, it will be the rejection value of the * returned promise. * * If `$promiseOrValue` is a promise, its completion value will be the rejected * value of the returned promise. * * This can be useful in situations where you need to reject a promise without * throwing an exception. For example, it allows you to propagate a rejection with * the value of another promise. * * @param mixed $promiseOrValue * @return PromiseInterface */ function reject($promiseOrValue = null) { if ($promiseOrValue instanceof PromiseInterface) { return resolve($promiseOrValue)->then(function ($value) { return new RejectedPromise($value); }); } return new RejectedPromise($promiseOrValue); } /** * Returns a promise that will resolve only once all the items in * `$promisesOrValues` have resolved. The resolution value of the returned promise * will be an array containing the resolution values of each of the items in * `$promisesOrValues`. * * @param array $promisesOrValues * @return PromiseInterface */ function all($promisesOrValues) { return map($promisesOrValues, function ($val) { return $val; }); } /** * Initiates a competitive race that allows one winner. Returns a promise which is * resolved in the same way the first settled promise resolves. * * The returned promise will become **infinitely pending** if `$promisesOrValues` * contains 0 items. * * @param array $promisesOrValues * @return PromiseInterface */ function race($promisesOrValues) { $cancellationQueue = new CancellationQueue(); $cancellationQueue->enqueue($promisesOrValues); return new Promise(function ($resolve, $reject, $notify) use ($promisesOrValues, $cancellationQueue) { resolve($promisesOrValues) ->done(function ($array) use ($cancellationQueue, $resolve, $reject, $notify) { if (!is_array($array) || !$array) { $resolve(); return; } foreach ($array as $promiseOrValue) { $cancellationQueue->enqueue($promiseOrValue); resolve($promiseOrValue) ->done($resolve, $reject, $notify); } }, $reject, $notify); }, $cancellationQueue); } /** * Returns a promise that will resolve when any one of the items in * `$promisesOrValues` resolves. The resolution value of the returned promise * will be the resolution value of the triggering item. * * The returned promise will only reject if *all* items in `$promisesOrValues` are * rejected. The rejection value will be an array of all rejection reasons. * * The returned promise will also reject with a `React\Promise\Exception\LengthException` * if `$promisesOrValues` contains 0 items. * * @param array $promisesOrValues * @return PromiseInterface */ function any($promisesOrValues) { return some($promisesOrValues, 1) ->then(function ($val) { return \array_shift($val); }); } /** * Returns a promise that will resolve when `$howMany` of the supplied items in * `$promisesOrValues` resolve. The resolution value of the returned promise * will be an array of length `$howMany` containing the resolution values of the * triggering items. * * The returned promise will reject if it becomes impossible for `$howMany` items * to resolve (that is, when `(count($promisesOrValues) - $howMany) + 1` items * reject). The rejection value will be an array of * `(count($promisesOrValues) - $howMany) + 1` rejection reasons. * * The returned promise will also reject with a `React\Promise\Exception\LengthException` * if `$promisesOrValues` contains less items than `$howMany`. * * @param array $promisesOrValues * @param int $howMany * @return PromiseInterface */ function some($promisesOrValues, $howMany) { $cancellationQueue = new CancellationQueue(); $cancellationQueue->enqueue($promisesOrValues); return new Promise(function ($resolve, $reject, $notify) use ($promisesOrValues, $howMany, $cancellationQueue) { resolve($promisesOrValues) ->done(function ($array) use ($howMany, $cancellationQueue, $resolve, $reject, $notify) { if (!\is_array($array) || $howMany < 1) { $resolve([]); return; } $len = \count($array); if ($len < $howMany) { throw new Exception\LengthException( \sprintf( 'Input array must contain at least %d item%s but contains only %s item%s.', $howMany, 1 === $howMany ? '' : 's', $len, 1 === $len ? '' : 's' ) ); } $toResolve = $howMany; $toReject = ($len - $toResolve) + 1; $values = []; $reasons = []; foreach ($array as $i => $promiseOrValue) { $fulfiller = function ($val) use ($i, &$values, &$toResolve, $toReject, $resolve) { if ($toResolve < 1 || $toReject < 1) { return; } $values[$i] = $val; if (0 === --$toResolve) { $resolve($values); } }; $rejecter = function ($reason) use ($i, &$reasons, &$toReject, $toResolve, $reject) { if ($toResolve < 1 || $toReject < 1) { return; } $reasons[$i] = $reason; if (0 === --$toReject) { $reject($reasons); } }; $cancellationQueue->enqueue($promiseOrValue); resolve($promiseOrValue) ->done($fulfiller, $rejecter, $notify); } }, $reject, $notify); }, $cancellationQueue); } /** * Traditional map function, similar to `array_map()`, but allows input to contain * promises and/or values, and `$mapFunc` may return either a value or a promise. * * The map function receives each item as argument, where item is a fully resolved * value of a promise or value in `$promisesOrValues`. * * @param array $promisesOrValues * @param callable $mapFunc * @return PromiseInterface */ function map($promisesOrValues, callable $mapFunc) { $cancellationQueue = new CancellationQueue(); $cancellationQueue->enqueue($promisesOrValues); return new Promise(function ($resolve, $reject, $notify) use ($promisesOrValues, $mapFunc, $cancellationQueue) { resolve($promisesOrValues) ->done(function ($array) use ($mapFunc, $cancellationQueue, $resolve, $reject, $notify) { if (!\is_array($array) || !$array) { $resolve([]); return; } $toResolve = \count($array); $values = []; foreach ($array as $i => $promiseOrValue) { $cancellationQueue->enqueue($promiseOrValue); $values[$i] = null; resolve($promiseOrValue) ->then($mapFunc) ->done( function ($mapped) use ($i, &$values, &$toResolve, $resolve) { $values[$i] = $mapped; if (0 === --$toResolve) { $resolve($values); } }, $reject, $notify ); } }, $reject, $notify); }, $cancellationQueue); } /** * Traditional reduce function, similar to `array_reduce()`, but input may contain * promises and/or values, and `$reduceFunc` may return either a value or a * promise, *and* `$initialValue` may be a promise or a value for the starting * value. * * @param array $promisesOrValues * @param callable $reduceFunc * @param mixed $initialValue * @return PromiseInterface */ function reduce($promisesOrValues, callable $reduceFunc, $initialValue = null) { $cancellationQueue = new CancellationQueue(); $cancellationQueue->enqueue($promisesOrValues); return new Promise(function ($resolve, $reject, $notify) use ($promisesOrValues, $reduceFunc, $initialValue, $cancellationQueue) { resolve($promisesOrValues) ->done(function ($array) use ($reduceFunc, $initialValue, $cancellationQueue, $resolve, $reject, $notify) { if (!\is_array($array)) { $array = []; } $total = \count($array); $i = 0; // Wrap the supplied $reduceFunc with one that handles promises and then // delegates to the supplied. $wrappedReduceFunc = function ($current, $val) use ($reduceFunc, $cancellationQueue, $total, &$i) { $cancellationQueue->enqueue($val); return $current ->then(function ($c) use ($reduceFunc, $total, &$i, $val) { return resolve($val) ->then(function ($value) use ($reduceFunc, $total, &$i, $c) { return $reduceFunc($c, $value, $i++, $total); }); }); }; $cancellationQueue->enqueue($initialValue); \array_reduce($array, $wrappedReduceFunc, resolve($initialValue)) ->done($resolve, $reject, $notify); }, $reject, $notify); }, $cancellationQueue); } /** * @internal */ function _checkTypehint(callable $callback, $object) { if (!\is_object($object)) { return true; } if (\is_array($callback)) { $callbackReflection = new \ReflectionMethod($callback[0], $callback[1]); } elseif (\is_object($callback) && !$callback instanceof \Closure) { $callbackReflection = new \ReflectionMethod($callback, '__invoke'); } else { $callbackReflection = new \ReflectionFunction($callback); } $parameters = $callbackReflection->getParameters(); if (!isset($parameters[0])) { return true; } $expectedException = $parameters[0]; if (!$expectedException->getClass()) { return true; } return $expectedException->getClass()->isInstance($object); } canceller = $canceller; // Explicitly overwrite arguments with null values before invoking // resolver function. This ensure that these arguments do not show up // in the stack trace in PHP 7+ only. $cb = $resolver; $resolver = $canceller = null; $this->call($cb); } public function then(callable $onFulfilled = null, callable $onRejected = null, callable $onProgress = null) { if (null !== $this->result) { return $this->result->then($onFulfilled, $onRejected, $onProgress); } if (null === $this->canceller) { return new static($this->resolver($onFulfilled, $onRejected, $onProgress)); } // This promise has a canceller, so we create a new child promise which // has a canceller that invokes the parent canceller if all other // followers are also cancelled. We keep a reference to this promise // instance for the static canceller function and clear this to avoid // keeping a cyclic reference between parent and follower. $parent = $this; ++$parent->requiredCancelRequests; return new static( $this->resolver($onFulfilled, $onRejected, $onProgress), static function () use (&$parent) { if (++$parent->cancelRequests >= $parent->requiredCancelRequests) { $parent->cancel(); } $parent = null; } ); } public function done(callable $onFulfilled = null, callable $onRejected = null, callable $onProgress = null) { if (null !== $this->result) { return $this->result->done($onFulfilled, $onRejected, $onProgress); } $this->handlers[] = static function (ExtendedPromiseInterface $promise) use ($onFulfilled, $onRejected) { $promise ->done($onFulfilled, $onRejected); }; if ($onProgress) { $this->progressHandlers[] = $onProgress; } } public function otherwise(callable $onRejected) { return $this->then(null, static function ($reason) use ($onRejected) { if (!_checkTypehint($onRejected, $reason)) { return new RejectedPromise($reason); } return $onRejected($reason); }); } public function always(callable $onFulfilledOrRejected) { return $this->then(static function ($value) use ($onFulfilledOrRejected) { return resolve($onFulfilledOrRejected())->then(function () use ($value) { return $value; }); }, static function ($reason) use ($onFulfilledOrRejected) { return resolve($onFulfilledOrRejected())->then(function () use ($reason) { return new RejectedPromise($reason); }); }); } public function progress(callable $onProgress) { return $this->then(null, null, $onProgress); } public function cancel() { if (null === $this->canceller || null !== $this->result) { return; } $canceller = $this->canceller; $this->canceller = null; $this->call($canceller); } private function resolver(callable $onFulfilled = null, callable $onRejected = null, callable $onProgress = null) { return function ($resolve, $reject, $notify) use ($onFulfilled, $onRejected, $onProgress) { if ($onProgress) { $progressHandler = static function ($update) use ($notify, $onProgress) { try { $notify($onProgress($update)); } catch (\Throwable $e) { $notify($e); } catch (\Exception $e) { $notify($e); } }; } else { $progressHandler = $notify; } $this->handlers[] = static function (ExtendedPromiseInterface $promise) use ($onFulfilled, $onRejected, $resolve, $reject, $progressHandler) { $promise ->then($onFulfilled, $onRejected) ->done($resolve, $reject, $progressHandler); }; $this->progressHandlers[] = $progressHandler; }; } private function reject($reason = null) { if (null !== $this->result) { return; } $this->settle(reject($reason)); } private function settle(ExtendedPromiseInterface $promise) { $promise = $this->unwrap($promise); if ($promise === $this) { $promise = new RejectedPromise( new \LogicException('Cannot resolve a promise with itself.') ); } $handlers = $this->handlers; $this->progressHandlers = $this->handlers = []; $this->result = $promise; $this->canceller = null; foreach ($handlers as $handler) { $handler($promise); } } private function unwrap($promise) { $promise = $this->extract($promise); while ($promise instanceof self && null !== $promise->result) { $promise = $this->extract($promise->result); } return $promise; } private function extract($promise) { if ($promise instanceof LazyPromise) { $promise = $promise->promise(); } return $promise; } private function call(callable $cb) { // Explicitly overwrite argument with null value. This ensure that this // argument does not show up in the stack trace in PHP 7+ only. $callback = $cb; $cb = null; // Use reflection to inspect number of arguments expected by this callback. // We did some careful benchmarking here: Using reflection to avoid unneeded // function arguments is actually faster than blindly passing them. // Also, this helps avoiding unnecessary function arguments in the call stack // if the callback creates an Exception (creating garbage cycles). if (\is_array($callback)) { $ref = new \ReflectionMethod($callback[0], $callback[1]); } elseif (\is_object($callback) && !$callback instanceof \Closure) { $ref = new \ReflectionMethod($callback, '__invoke'); } else { $ref = new \ReflectionFunction($callback); } $args = $ref->getNumberOfParameters(); try { if ($args === 0) { $callback(); } else { // Keep references to this promise instance for the static resolve/reject functions. // By using static callbacks that are not bound to this instance // and passing the target promise instance by reference, we can // still execute its resolving logic and still clear this // reference when settling the promise. This helps avoiding // garbage cycles if any callback creates an Exception. // These assumptions are covered by the test suite, so if you ever feel like // refactoring this, go ahead, any alternative suggestions are welcome! $target =& $this; $progressHandlers =& $this->progressHandlers; $callback( static function ($value = null) use (&$target) { if ($target !== null) { $target->settle(resolve($value)); $target = null; } }, static function ($reason = null) use (&$target) { if ($target !== null) { $target->reject($reason); $target = null; } }, static function ($update = null) use (&$progressHandlers) { foreach ($progressHandlers as $handler) { $handler($update); } } ); } } catch (\Throwable $e) { $target = null; $this->reject($e); } catch (\Exception $e) { $target = null; $this->reject($e); } } } then(null, $onRejected); * ``` * * Additionally, you can type hint the `$reason` argument of `$onRejected` to catch * only specific errors. * * @param callable $onRejected * @return ExtendedPromiseInterface */ public function otherwise(callable $onRejected); /** * Allows you to execute "cleanup" type tasks in a promise chain. * * It arranges for `$onFulfilledOrRejected` to be called, with no arguments, * when the promise is either fulfilled or rejected. * * * If `$promise` fulfills, and `$onFulfilledOrRejected` returns successfully, * `$newPromise` will fulfill with the same value as `$promise`. * * If `$promise` fulfills, and `$onFulfilledOrRejected` throws or returns a * rejected promise, `$newPromise` will reject with the thrown exception or * rejected promise's reason. * * If `$promise` rejects, and `$onFulfilledOrRejected` returns successfully, * `$newPromise` will reject with the same reason as `$promise`. * * If `$promise` rejects, and `$onFulfilledOrRejected` throws or returns a * rejected promise, `$newPromise` will reject with the thrown exception or * rejected promise's reason. * * `always()` behaves similarly to the synchronous finally statement. When combined * with `otherwise()`, `always()` allows you to write code that is similar to the familiar * synchronous catch/finally pair. * * Consider the following synchronous code: * * ```php * try { * return doSomething(); * } catch(\Exception $e) { * return handleError($e); * } finally { * cleanup(); * } * ``` * * Similar asynchronous code (with `doSomething()` that returns a promise) can be * written: * * ```php * return doSomething() * ->otherwise('handleError') * ->always('cleanup'); * ``` * * @param callable $onFulfilledOrRejected * @return ExtendedPromiseInterface */ public function always(callable $onFulfilledOrRejected); /** * Registers a handler for progress updates from promise. It is a shortcut for: * * ```php * $promise->then(null, null, $onProgress); * ``` * * @param callable $onProgress * @return ExtendedPromiseInterface * @deprecated 2.6.0 Progress support is deprecated and should not be used anymore. */ public function progress(callable $onProgress); } factory = $factory; } public function then(callable $onFulfilled = null, callable $onRejected = null, callable $onProgress = null) { return $this->promise()->then($onFulfilled, $onRejected, $onProgress); } public function done(callable $onFulfilled = null, callable $onRejected = null, callable $onProgress = null) { return $this->promise()->done($onFulfilled, $onRejected, $onProgress); } public function otherwise(callable $onRejected) { return $this->promise()->otherwise($onRejected); } public function always(callable $onFulfilledOrRejected) { return $this->promise()->always($onFulfilledOrRejected); } public function progress(callable $onProgress) { return $this->promise()->progress($onProgress); } public function cancel() { return $this->promise()->cancel(); } /** * @internal * @see Promise::settle() */ public function promise() { if (null === $this->promise) { try { $this->promise = resolve(\call_user_func($this->factory)); } catch (\Throwable $exception) { $this->promise = new RejectedPromise($exception); } catch (\Exception $exception) { $this->promise = new RejectedPromise($exception); } } return $this->promise; } } reason = $reason; $message = \sprintf('Unhandled Rejection: %s', \json_encode($reason)); parent::__construct($message, 0); } public function getReason() { return $this->reason; } } value = $value; } public function then(callable $onFulfilled = null, callable $onRejected = null, callable $onProgress = null) { if (null === $onFulfilled) { return $this; } try { return resolve($onFulfilled($this->value)); } catch (\Throwable $exception) { return new RejectedPromise($exception); } catch (\Exception $exception) { return new RejectedPromise($exception); } } public function done(callable $onFulfilled = null, callable $onRejected = null, callable $onProgress = null) { if (null === $onFulfilled) { return; } $result = $onFulfilled($this->value); if ($result instanceof ExtendedPromiseInterface) { $result->done(); } } public function otherwise(callable $onRejected) { return $this; } public function always(callable $onFulfilledOrRejected) { return $this->then(function ($value) use ($onFulfilledOrRejected) { return resolve($onFulfilledOrRejected())->then(function () use ($value) { return $value; }); }); } public function progress(callable $onProgress) { return $this; } public function cancel() { } } started) { return; } $this->started = true; $this->drain(); } public function enqueue($cancellable) { if (!\is_object($cancellable) || !\method_exists($cancellable, 'then') || !\method_exists($cancellable, 'cancel')) { return; } $length = \array_push($this->queue, $cancellable); if ($this->started && 1 === $length) { $this->drain(); } } private function drain() { for ($i = key($this->queue); isset($this->queue[$i]); $i++) { $cancellable = $this->queue[$i]; $exception = null; try { $cancellable->cancel(); } catch (\Throwable $exception) { } catch (\Exception $exception) { } unset($this->queue[$i]); if ($exception) { throw $exception; } } $this->queue = []; } } * : The name, slug, or term ID for the menu. * * [--fields=] * : Limit the output to specific object fields. * * [--format=] * : Render output in a particular format. * --- * default: table * options: * - table * - csv * - json * - count * - ids * - yaml * --- * * ## AVAILABLE FIELDS * * These fields will be displayed by default for each menu item: * * * db_id * * type * * title * * link * * position * * These fields are optionally available: * * * menu_item_parent * * object_id * * object * * type * * type_label * * target * * attr_title * * description * * classes * * xfn * * ## EXAMPLES * * $ wp menu item list main-menu * +-------+-----------+-------------+---------------------------------+----------+ * | db_id | type | title | link | position | * +-------+-----------+-------------+---------------------------------+----------+ * | 5 | custom | Home | http://example.com | 1 | * | 6 | post_type | Sample Page | http://example.com/sample-page/ | 2 | * +-------+-----------+-------------+---------------------------------+----------+ * * @subcommand list */ public function list_( $args, $assoc_args ) { $items = wp_get_nav_menu_items( $args[0] ); if ( false === $items || is_wp_error( $items ) ) { WP_CLI::error( 'Invalid menu.' ); } // Correct position inconsistency and // protected `url` param in WP-CLI $items = array_map( function( $item ) { $item->position = $item->menu_order; $item->link = $item->url; return $item; }, $items ); if ( ! empty( $assoc_args['format'] ) && 'ids' === $assoc_args['format'] ) { $items = array_map( function( $item ) { return $item->db_id; }, $items ); } $formatter = $this->get_formatter( $assoc_args ); $formatter->display_items( $items ); } /** * Adds a post as a menu item. * * ## OPTIONS * * * : The name, slug, or term ID for the menu. * * * : Post ID to add to the menu. * * [--title=] * : Set a custom title for the menu item. * * [--link=<link>] * : Set a custom url for the menu item. * * [--description=<description>] * : Set a custom description for the menu item. * * [--attr-title=<attr-title>] * : Set a custom title attribute for the menu item. * * [--target=<target>] * : Set a custom link target for the menu item. * * [--classes=<classes>] * : Set a custom link classes for the menu item. * * [--position=<position>] * : Specify the position of this menu item. * * [--parent-id=<parent-id>] * : Make this menu item a child of another menu item. * * [--porcelain] * : Output just the new menu item id. * * ## EXAMPLES * * $ wp menu item add-post sidebar-menu 33 --title="Custom Test Post" * Success: Menu item added. * * @subcommand add-post */ public function add_post( $args, $assoc_args ) { $assoc_args['object-id'] = $args[1]; unset( $args[1] ); $post = get_post( $assoc_args['object-id'] ); if ( ! $post ) { WP_CLI::error( 'Invalid post.' ); } $assoc_args['object'] = $post->post_type; $this->add_or_update_item( 'add', 'post_type', $args, $assoc_args ); } /** * Adds a taxonomy term as a menu item. * * ## OPTIONS * * <menu> * : The name, slug, or term ID for the menu. * * <taxonomy> * : Taxonomy of the term to be added. * * <term-id> * : Term ID of the term to be added. * * [--title=<title>] * : Set a custom title for the menu item. * * [--link=<link>] * : Set a custom url for the menu item. * * [--description=<description>] * : Set a custom description for the menu item. * * [--attr-title=<attr-title>] * : Set a custom title attribute for the menu item. * * [--target=<target>] * : Set a custom link target for the menu item. * * [--classes=<classes>] * : Set a custom link classes for the menu item. * * [--position=<position>] * : Specify the position of this menu item. * * [--parent-id=<parent-id>] * : Make this menu item a child of another menu item. * * [--porcelain] * : Output just the new menu item id. * * ## EXAMPLES * * $ wp menu item add-term sidebar-menu post_tag 24 * Success: Menu item added. * * @subcommand add-term */ public function add_term( $args, $assoc_args ) { $assoc_args['object'] = $args[1]; unset( $args[1] ); $assoc_args['object-id'] = $args[2]; unset( $args[2] ); if ( ! get_term_by( 'id', $assoc_args['object-id'], $assoc_args['object'] ) ) { WP_CLI::error( 'Invalid term.' ); } $this->add_or_update_item( 'add', 'taxonomy', $args, $assoc_args ); } /** * Adds a custom menu item. * * ## OPTIONS * * <menu> * : The name, slug, or term ID for the menu. * * <title> * : Title for the link. * * <link> * : Target URL for the link. * * [--description=<description>] * : Set a custom description for the menu item. * * [--attr-title=<attr-title>] * : Set a custom title attribute for the menu item. * * [--target=<target>] * : Set a custom link target for the menu item. * * [--classes=<classes>] * : Set a custom link classes for the menu item. * * [--position=<position>] * : Specify the position of this menu item. * * [--parent-id=<parent-id>] * : Make this menu item a child of another menu item. * * [--porcelain] * : Output just the new menu item id. * * ## EXAMPLES * * $ wp menu item add-custom sidebar-menu Apple http://apple.com * Success: Menu item added. * * @subcommand add-custom */ public function add_custom( $args, $assoc_args ) { $assoc_args['title'] = $args[1]; unset( $args[1] ); $assoc_args['link'] = $args[2]; unset( $args[2] ); $this->add_or_update_item( 'add', 'custom', $args, $assoc_args ); } /** * Updates a menu item. * * ## OPTIONS * * <db-id> * : Database ID for the menu item. * * [--title=<title>] * : Set a custom title for the menu item. * * [--link=<link>] * : Set a custom url for the menu item. * * [--description=<description>] * : Set a custom description for the menu item. * * [--attr-title=<attr-title>] * : Set a custom title attribute for the menu item. * * [--target=<target>] * : Set a custom link target for the menu item. * * [--classes=<classes>] * : Set a custom link classes for the menu item. * * [--position=<position>] * : Specify the position of this menu item. * * [--parent-id=<parent-id>] * : Make this menu item a child of another menu item. * * ## EXAMPLES * * $ wp menu item update 45 --title=WordPress --link='http://wordpress.org' --target=_blank --position=2 * Success: Menu item updated. * * @subcommand update */ public function update( $args, $assoc_args ) { // Shuffle the position of these. $args[1] = $args[0]; $terms = get_the_terms( $args[1], 'nav_menu' ); if ( $terms && ! is_wp_error( $terms ) ) { $args[0] = (int) $terms[0]->term_id; } else { $args[0] = 0; } $type = get_post_meta( $args[1], '_menu_item_type', true ); $this->add_or_update_item( 'update', $type, $args, $assoc_args ); } /** * Deletes one or more items from a menu. * * ## OPTIONS * * <db-id>... * : Database ID for the menu item(s). * * ## EXAMPLES * * $ wp menu item delete 45 * Success: 1 menu item deleted. * * @subcommand delete */ public function delete( $args, $assoc_args ) { global $wpdb; $count = 0; $errors = 0; foreach ( $args as $arg ) { $post = get_post( $arg ); $menu_term = get_the_terms( $arg, 'nav_menu' ); $parent_menu_id = (int) get_post_meta( $arg, '_menu_item_menu_item_parent', true ); $result = wp_delete_post( $arg, true ); if ( ! $result ) { WP_CLI::warning( "Couldn't delete menu item {$arg}." ); $errors++; } else { if ( is_array( $menu_term ) && ! empty( $menu_term ) && $post ) { $this->reorder_menu_items( $menu_term[0]->term_id, $post->menu_order, -1, 0 ); } if ( $parent_menu_id ) { $children = $wpdb->get_results( $wpdb->prepare( "SELECT post_id FROM $wpdb->postmeta WHERE meta_key='_menu_item_menu_item_parent' AND meta_value=%s", (int) $arg ) ); if ( $children ) { $children_query = $wpdb->prepare( "UPDATE $wpdb->postmeta SET meta_value = %d WHERE meta_key = '_menu_item_menu_item_parent' AND meta_value=%s", $parent_menu_id, (int) $arg ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- $children_query is already prepared above. $wpdb->query( $children_query ); foreach ( $children as $child ) { clean_post_cache( $child ); } } } } // phpcs:ignore WordPress.PHP.StrictComparisons.LooseComparison -- Will increase count for non existent menu. if ( false != $result ) { $count++; } } Utils\report_batch_operation_results( 'menu item', 'delete', count( $args ), $count, $errors ); } /** * Worker method to create new items or update existing ones. */ private function add_or_update_item( $method, $type, $args, $assoc_args ) { $menu = $args[0]; $menu_item_db_id = Utils\get_flag_value( $args, 1, 0 ); $menu = wp_get_nav_menu_object( $menu ); if ( ! $menu || is_wp_error( $menu ) ) { WP_CLI::error( 'Invalid menu.' ); } // `url` is protected in WP-CLI, so we use `link` instead $assoc_args['url'] = Utils\get_flag_value( $assoc_args, 'link' ); // Need to persist the menu item data. See https://core.trac.wordpress.org/ticket/28138 if ( 'update' === $method ) { $menu_item_obj = get_post( $menu_item_db_id ); $menu_item_obj = wp_setup_nav_menu_item( $menu_item_obj ); // Correct the menu position if this was the first item. See https://core.trac.wordpress.org/ticket/28140 $position = ( 0 === $menu_item_obj->menu_order ) ? 1 : $menu_item_obj->menu_order; $default_args = [ 'position' => $position, 'title' => $menu_item_obj->title, 'url' => $menu_item_obj->url, 'description' => $menu_item_obj->description, 'object' => $menu_item_obj->object, 'object-id' => $menu_item_obj->object_id, 'parent-id' => $menu_item_obj->menu_item_parent, 'attr-title' => $menu_item_obj->attr_title, 'target' => $menu_item_obj->target, 'classes' => implode( ' ', $menu_item_obj->classes ), // stored in the database as array 'xfn' => $menu_item_obj->xfn, 'status' => $menu_item_obj->post_status, ]; } else { $default_args = [ 'position' => 0, 'title' => '', 'url' => '', 'description' => '', 'object' => '', 'object-id' => 0, 'parent-id' => 0, 'attr-title' => '', 'target' => '', 'classes' => '', 'xfn' => '', // Core oddly defaults to 'draft' for create, // and 'publish' for update // Easiest to always work with publish 'status' => 'publish', ]; } $menu_item_args = []; foreach ( $default_args as $key => $default_value ) { // wp_update_nav_menu_item() has a weird argument prefix $new_key = 'menu-item-' . $key; $menu_item_args[ $new_key ] = Utils\get_flag_value( $assoc_args, $key, $default_value ); } $menu_item_args['menu-item-type'] = $type; $result = wp_update_nav_menu_item( $menu->term_id, $menu_item_db_id, $menu_item_args ); if ( is_wp_error( $result ) ) { WP_CLI::error( $result->get_error_message() ); } elseif ( ! $result ) { if ( 'add' === $method ) { WP_CLI::error( "Couldn't add menu item." ); } elseif ( 'update' === $method ) { WP_CLI::error( "Couldn't update menu item." ); } } else { if ( ( 'add' === $method ) && $menu_item_args['menu-item-position'] ) { $this->reorder_menu_items( $menu->term_id, $menu_item_args['menu-item-position'], +1, $result ); } /** * Set the menu * * wp_update_nav_menu_item() *should* take care of this, but * depends on wp_insert_post()'s "tax_input" argument, which * is ignored if the user can't edit the taxonomy * * @see https://core.trac.wordpress.org/ticket/27113 */ if ( ! is_object_in_term( $result, 'nav_menu', (int) $menu->term_id ) ) { wp_set_object_terms( $result, [ (int) $menu->term_id ], 'nav_menu' ); } if ( 'add' === $method && ! empty( $assoc_args['porcelain'] ) ) { WP_CLI::line( $result ); } else { if ( 'add' === $method ) { WP_CLI::success( 'Menu item added.' ); } elseif ( 'update' === $method ) { WP_CLI::success( 'Menu item updated.' ); } } } } /** * Move block of items in one nav_menu up or down by incrementing/decrementing their menu_order field. * Expects the menu items to have proper menu_orders (i.e. doesn't fix errors from previous incorrect operations). * * @param int $menu_id ID of the nav_menu * @param int $min_position minimal menu_order to touch * @param int $increment how much to change menu_order: +1 to move down, -1 to move up * @param int $ignore_item_id menu item that should be ignored by the change (e.g. newly created menu item) * @return int number of rows affected */ private function reorder_menu_items( $menu_id, $min_position, $increment, $ignore_item_id = 0 ) { global $wpdb; return $wpdb->query( $wpdb->prepare( "UPDATE $wpdb->posts SET `menu_order`=`menu_order`+(%d) WHERE `menu_order`>=%d AND ID IN (SELECT object_id FROM $wpdb->term_relationships WHERE term_taxonomy_id=%d) AND ID<>%d", (int) $increment, (int) $min_position, (int) $menu_id, (int) $ignore_item_id ) ); } protected function get_formatter( &$assoc_args ) { return new Formatter( $assoc_args, $this->obj_fields ); } } <?php use WP_CLI\Dispatcher\CommandNamespace; /** * Perform network-wide operations. * * ## EXAMPLES * * # Get a list of super-admins * $ wp network meta get 1 site_admins * array ( * 0 => 'supervisor', * ) */ class Network_Namespace extends CommandNamespace { } <?php namespace WP_CLI; use WP_CLI; use WP_CLI_Command; use WP_CLI\Utils; use WP_Error; /** * Base class for WP-CLI commands that deal with database objects. * * @package wp-cli */ abstract class CommandWithDBObject extends WP_CLI_Command { /** * @var string $object_type WordPress' expected name for the object. */ protected $obj_type; /** * @var string $obj_id_key Key representing object's PK field in db. */ protected $obj_id_key = 'ID'; /** * @var array $obj_fields Default fields to display for each object. */ protected $obj_fields; /** * Create a given database object. * Exits with status. * * @param array $args Arguments passed to command. Generally unused. * @param array $assoc_args Parameters passed to command to be passed to callback. * @param string $callback Function used to create object. */ protected function _create( $args, $assoc_args, $callback ) { unset( $assoc_args[ $this->obj_id_key ] ); $obj_id = $callback( $assoc_args ); if ( is_wp_error( $obj_id ) ) { WP_CLI::error( $obj_id ); } if ( Utils\get_flag_value( $assoc_args, 'porcelain' ) ) { WP_CLI::line( $obj_id ); } else { WP_CLI::success( "Created {$this->obj_type} {$obj_id}." ); } } /** * Update a given database object. * Exits with status. * * @param array $args Collection of one or more object ids to update. * @param array $assoc_args Fields => values to update on each object. * @param string $callback Function used to update object. */ protected function _update( $args, $assoc_args, $callback ) { $status = 0; if ( empty( $assoc_args ) ) { WP_CLI::error( 'Need some fields to update.' ); } if ( Utils\get_flag_value( $assoc_args, 'defer-term-counting' ) ) { wp_defer_term_counting( true ); } foreach ( $args as $obj_id ) { $params = array_merge( $assoc_args, [ $this->obj_id_key => $obj_id ] ); $status = $this->success_or_failure( $this->wp_error_to_resp( $callback( $params ), "Updated {$this->obj_type} {$obj_id}." ) ); } if ( Utils\get_flag_value( $assoc_args, 'defer-term-counting' ) ) { wp_defer_term_counting( false ); } exit( $status ); } /** * Transforms arguments with '__' from CSV into expected arrays * * @param array $assoc_args * @return array */ protected static function process_csv_arguments_to_arrays( $assoc_args ) { foreach ( $assoc_args as $k => $v ) { if ( false !== strpos( $k, '__' ) ) { $assoc_args[ $k ] = explode( ',', $v ); } } return $assoc_args; } /** * Delete a given database object. * Exits with status. * * @param array $args Collection of one or more object ids to delete. * @param array $assoc_args Any arguments needed for the callback function. * @param callable $callback Function used to delete object. */ protected function _delete( $args, $assoc_args, $callback ) { $status = 0; if ( Utils\get_flag_value( $assoc_args, 'defer-term-counting' ) ) { wp_defer_term_counting( true ); } foreach ( $args as $obj_id ) { $result = $callback( $obj_id, $assoc_args ); $status = $this->success_or_failure( $result ); } if ( Utils\get_flag_value( $assoc_args, 'defer-term-counting' ) ) { wp_defer_term_counting( false ); } exit( $status ); } /** * Format callback response to consistent format. * * @param WP_Error|true $response Response from CRUD callback. * @param string $success_msg * @return array */ protected function wp_error_to_resp( $response, $success_msg ) { return is_wp_error( $response ) ? [ 'error', $response->get_error_message() ] : [ 'success', $success_msg ]; } /** * Display success or warning based on response; return proper exit code. * * @param array $response Formatted from a CRUD callback. * @return int $status */ protected function success_or_failure( $response ) { list( $type, $msg ) = $response; if ( 'success' === $type ) { WP_CLI::success( $msg ); $status = 0; } else { WP_CLI::warning( $msg ); $status = 1; } return $status; } /** * Get Formatter object based on supplied parameters. * * @param array $assoc_args Parameters passed to command. Determines formatting. * @return Formatter */ protected function get_formatter( &$assoc_args ) { if ( ! empty( $assoc_args['fields'] ) ) { if ( is_string( $assoc_args['fields'] ) ) { $fields = explode( ',', $assoc_args['fields'] ); } else { $fields = $assoc_args['fields']; } } else { $fields = $this->obj_fields; } return new Formatter( $assoc_args, $fields, $this->obj_type ); } /** * Given a callback, display the URL for one or more objects. * * @param array $args One or more object references. * @param string $callback Function to get URL for the object. */ protected function _url( $args, $callback ) { foreach ( $args as $obj_id ) { $object = $this->fetcher->get_check( $obj_id ); WP_CLI::line( $callback( $object->{$this->obj_id_key} ) ); } } } <?php namespace WP_CLI\Entity; use OutOfBoundsException; class NonExistentKeyException extends OutOfBoundsException { /** @var RecursiveDataStructureTraverser */ protected $traverser; /** * @param RecursiveDataStructureTraverser $traverser */ public function set_traverser( $traverser ) { $this->traverser = $traverser; } /** * @return RecursiveDataStructureTraverser */ public function get_traverser() { return $this->traverser; } } <?php namespace WP_CLI\Entity; use UnexpectedValueException; class RecursiveDataStructureTraverser { /** * @var mixed The data to traverse set by reference. */ protected $data; /** * @var null|string The key the data belongs to in the parent's data. */ protected $key; /** * @var null|static The parent instance of the traverser. */ protected $parent; /** * RecursiveDataStructureTraverser constructor. * * @param mixed $data The data to read/manipulate by reference. * @param string|int $key The key/property the data belongs to. * @param static $parent */ public function __construct( &$data, $key = null, $parent = null ) { $this->data =& $data; $this->key = $key; $this->parent = $parent; } /** * Get the nested value at the given key path. * * @param string|int|array $key_path * * @return static */ public function get( $key_path ) { return $this->traverse_to( (array) $key_path )->value(); } /** * Get the current data. * * @return mixed */ public function value() { return $this->data; } /** * Update a nested value at the given key path. * * @param string|int|array $key_path * @param mixed $value */ public function update( $key_path, $value ) { $this->traverse_to( (array) $key_path )->set_value( $value ); } /** * Update the current data with the given value. * * This will mutate the variable which was passed into the constructor * as the data is set and traversed by reference. * * @param mixed $value */ public function set_value( $value ) { $this->data = $value; } /** * Unset the value at the given key path. * * @param $key_path */ public function delete( $key_path ) { $this->traverse_to( (array) $key_path )->unset_on_parent(); } /** * Define a nested value while creating keys if they do not exist. * * @param array $key_path * @param mixed $value */ public function insert( $key_path, $value ) { try { $this->update( $key_path, $value ); } catch ( NonExistentKeyException $exception ) { $exception->get_traverser()->create_key(); $this->insert( $key_path, $value ); } } /** * Delete the key on the parent's data that references this data. */ public function unset_on_parent() { $this->parent->delete_by_key( $this->key ); } /** * Delete the given key from the data. * * @param $key */ public function delete_by_key( $key ) { if ( is_array( $this->data ) ) { unset( $this->data[ $key ] ); } else { unset( $this->data->$key ); } } /** * Get an instance of the traverser for the given hierarchical key. * * @param array $key_path Hierarchical key path within the current data to traverse to. * * @throws NonExistentKeyException * * @return static */ public function traverse_to( array $key_path ) { $current = array_shift( $key_path ); if ( null === $current ) { return $this; } if ( ! $this->exists( $current ) ) { $exception = new NonExistentKeyException( "No data exists for key \"{$current}\"" ); $exception->set_traverser( new static( $this->data, $current, $this->parent ) ); throw $exception; } foreach ( $this->data as $key => &$key_data ) { if ( $key === $current ) { $traverser = new static( $key_data, $key, $this ); return $traverser->traverse_to( $key_path ); } } } /** * Create the key on the current data. * * @throws UnexpectedValueException */ protected function create_key() { if ( is_array( $this->data ) ) { $this->data[ $this->key ] = null; } elseif ( is_object( $this->data ) ) { $this->data->{$this->key} = null; } else { $type = gettype( $this->data ); throw new UnexpectedValueException( "Cannot create key \"{$this->key}\" on data type {$type}" ); } } /** * Check if the given key exists on the current data. * * @param string $key * * @return bool */ public function exists( $key ) { return ( is_array( $this->data ) && array_key_exists( $key, $this->data ) ) || ( is_object( $this->data ) && property_exists( $this->data, $key ) ); } } <?php namespace WP_CLI\Entity; class Utils { /** * Check whether any input is passed to STDIN. * * @return bool */ public static function has_stdin() { $handle = fopen( 'php://stdin', 'r' ); $read = array( $handle ); $write = null; $except = null; $streams = stream_select( $read, $write, $except, 0 ); fclose( $handle ); return 1 === $streams; } } <?php namespace WP_CLI; use WP_CLI; use WP_CLI_Command; use WP_CLI\Utils; /** * Base class for WP-CLI commands that deal with terms * * @package wp-cli */ abstract class CommandWithTerms extends WP_CLI_Command { /** * @var string $object_type WordPress' expected name for the object. */ protected $obj_type; /** * @var string $object_id WordPress' object id. */ protected $obj_id; /** * @var array $obj_fields Default fields to display for each object. */ protected $obj_fields = [ 'term_id', 'name', 'slug', 'taxonomy', ]; /** * List all terms associated with an object. * * <id> * : ID for the object. * * <taxonomy>... * : One or more taxonomies to list. * * [--field=<field>] * : Prints the value of a single field for each term. * * [--fields=<fields>] * : Limit the output to specific row fields. * * [--format=<format>] * : Render output in a particular format. * --- * default: table * options: * - table * - csv * - json * - yaml * - count * - ids * --- * * ## AVAILABLE FIELDS * * These fields will be displayed by default for each term: * * * term_id * * name * * slug * * taxonomy * * These fields are optionally available: * * * term_taxonomy_id * * description * * term_group * * parent * * count * * @subcommand list */ public function list_( $args, $assoc_args ) { $defaults = [ 'format' => 'table', ]; $assoc_args = array_merge( $defaults, $assoc_args ); $object_id = array_shift( $args ); $taxonomy_names = $args; $taxonomy_args = []; $this->set_obj_id( $object_id ); foreach ( $taxonomy_names as $taxonomy ) { $this->taxonomy_exists( $taxonomy ); } if ( 'ids' === $assoc_args['format'] ) { $taxonomy_args['fields'] = 'ids'; } $items = wp_get_object_terms( $object_id, $taxonomy_names, $taxonomy_args ); $formatter = $this->get_formatter( $assoc_args ); $formatter->display_items( $items ); } /** * Remove a term from an object. * * ## OPTIONS * * <id> * : The ID of the object. * * <taxonomy> * : The name of the term's taxonomy. * * [<term>...] * : The name of the term or terms to be removed from the object. * * [--by=<field>] * : Explicitly handle the term value as a slug or id. * --- * options: * - slug * - id * --- * * [--all] * : Remove all terms from the object. */ public function remove( $args, $assoc_args ) { $object_id = array_shift( $args ); $taxonomy = array_shift( $args ); $terms = $args; $this->set_obj_id( $object_id ); $this->taxonomy_exists( $taxonomy ); $field = Utils\get_flag_value( $assoc_args, 'by' ); if ( $field ) { $terms = $this->prepare_terms( $field, $terms, $taxonomy ); } if ( Utils\get_flag_value( $assoc_args, 'all' ) ) { // No need to specify terms while removing all terms. if ( $terms ) { WP_CLI::error( 'No need to specify terms while removing all terms.' ); } // Remove all set categories from post. $result = wp_delete_object_term_relationships( $object_id, $taxonomy ); $message = 'Removed all terms.'; if ( 'category' === $taxonomy ) { // Set default category to post. $default_category = (int) get_option( 'default_category' ); $default_category = ( ! empty( $default_category ) ) ? $default_category : 1; $default_category = wp_set_object_terms( $object_id, [ $default_category ], $taxonomy, true ); if ( is_wp_error( $default_category ) ) { WP_CLI::error( 'Failed to set default term.' ); } $message = 'Removed all terms and set default term.'; } if ( is_wp_error( $result ) ) { WP_CLI::error( 'Failed to remove all terms.' ); } WP_CLI::success( $message ); return; } // Abort if no terms are specified. if ( ! $terms ) { WP_CLI::error( 'Please specify one or more terms, or use --all.' ); } // Remove term from post. $result = wp_remove_object_terms( $object_id, $terms, $taxonomy ); $label = count( $terms ) > 1 ? 'terms' : 'term'; if ( is_wp_error( $result ) ) { WP_CLI::error( "Failed to remove {$label}." ); } WP_CLI::success( "Removed {$label}." ); } /** * Add a term to an object. * * Append the term to the existing set of terms on the object. * * <id> * : The ID of the object. * * <taxonomy> * : The name of the taxonomy type to be added. * * <term>... * : The slug of the term or terms to be added. * * [--by=<field>] * : Explicitly handle the term value as a slug or id. * --- * options: * - slug * - id * --- */ public function add( $args, $assoc_args ) { $object_id = array_shift( $args ); $taxonomy = array_shift( $args ); $terms = $args; $this->set_obj_id( $object_id ); $this->taxonomy_exists( $taxonomy ); $field = Utils\get_flag_value( $assoc_args, 'by' ); if ( $field ) { $terms = $this->prepare_terms( $field, $terms, $taxonomy ); } $result = wp_set_object_terms( $object_id, $terms, $taxonomy, true ); $label = count( $terms ) > 1 ? 'terms' : 'term'; if ( ! is_wp_error( $result ) ) { WP_CLI::success( "Added {$label}." ); } else { WP_CLI::error( "Failed to add {$label}." ); } } /** * Set object terms. * * Replaces existing terms on the object. * * <id> * : The ID of the object. * * <taxonomy> * : The name of the taxonomy type to be updated. * * <term>... * : The slug of the term or terms to be updated. * * [--by=<field>] * : Explicitly handle the term value as a slug or id. * --- * options: * - slug * - id * --- */ public function set( $args, $assoc_args ) { $object_id = array_shift( $args ); $taxonomy = array_shift( $args ); $terms = $args; $this->set_obj_id( $object_id ); $this->taxonomy_exists( $taxonomy ); $field = Utils\get_flag_value( $assoc_args, 'by' ); if ( $field ) { $terms = $this->prepare_terms( $field, $terms, $taxonomy ); } $result = wp_set_object_terms( $object_id, $terms, $taxonomy, false ); $label = count( $terms ) > 1 ? 'terms' : 'term'; if ( ! is_wp_error( $result ) ) { WP_CLI::success( "Set {$label}." ); } else { WP_CLI::error( "Failed to set {$label}." ); } } /** * Check if taxonomy exists * * @param $taxonomy */ protected function taxonomy_exists( $taxonomy ) { $taxonomy_names = get_object_taxonomies( $this->get_object_type() ); if ( ! in_array( $taxonomy, $taxonomy_names, true ) ) { WP_CLI::error( "Invalid taxonomy {$taxonomy}." ); } } /** * Prepare terms if `--by=<field>` flag is used * * @param array $terms * @param string $field * @param string $taxonomy */ protected function prepare_terms( $field, $terms, $taxonomy ) { if ( 'id' === $field ) { $new_terms = []; foreach ( $terms as $term_id ) { $term = get_term_by( 'term_id', $term_id, $taxonomy ); if ( $term ) { $new_terms[] = $term->slug; } } $terms = $new_terms; } return $terms; } /** * Set obj_id Class variable * * @param string $obj_id */ protected function set_obj_id( $obj_id ) { $this->obj_id = $obj_id; } /** * Get obj_id Class variable * * @return string */ protected function get_obj_id() { return $this->obj_id; } /** * Get obj_type Class variable * * @return string $obj_type */ protected function get_object_type() { return $this->obj_type; } /** * Get Formatter object based on supplied parameters. * * @param array $assoc_args Parameters passed to command. Determines formatting. * * @return WP_CLI\Formatter */ protected function get_formatter( &$assoc_args ) { return new Formatter( $assoc_args, $this->obj_fields, $this->obj_type ); } } <?php namespace WP_CLI; use Exception; use WP_CLI; use WP_CLI_Command; use WP_CLI\Entity\RecursiveDataStructureTraverser; use WP_CLI\Entity\Utils as EntityUtils; /** * Base class for WP-CLI commands that deal with metadata * * @package wp-cli */ abstract class CommandWithMeta extends WP_CLI_Command { protected $meta_type; /** * List all metadata associated with an object. * * ## OPTIONS * * <id> * : ID for the object. * * [--keys=<keys>] * : Limit output to metadata of specific keys. * * [--fields=<fields>] * : Limit the output to specific row fields. Defaults to id,meta_key,meta_value. * * [--format=<format>] * : Render output in a particular format. * --- * default: table * options: * - table * - csv * - json * - yaml * - count * --- * * [--orderby=<fields>] * : Set orderby which field. * --- * default: id * options: * - id * - meta_key * - meta_value * --- * * [--order=<order>] * : Set ascending or descending order. * --- * default: asc * options: * - asc * - desc * --- * * [--unserialize] * : Unserialize meta_value output. * * @subcommand list */ public function list_( $args, $assoc_args ) { list( $object_id ) = $args; $keys = ! empty( $assoc_args['keys'] ) ? explode( ',', $assoc_args['keys'] ) : []; $object_id = $this->check_object_id( $object_id ); $metadata = $this->get_metadata( $object_id ); if ( ! $metadata ) { $metadata = []; } $items = []; foreach ( $metadata as $key => $values ) { // Skip if not requested if ( ! empty( $keys ) && ! in_array( $key, $keys, true ) ) { continue; } foreach ( $values as $item_value ) { if ( Utils\get_flag_value( $assoc_args, 'unserialize' ) ) { $item_value = maybe_unserialize( $item_value ); } $items[] = (object) [ "{$this->meta_type}_id" => $object_id, 'meta_key' => $key, 'meta_value' => $item_value, ]; } } $order = Utils\get_flag_value( $assoc_args, 'order' ); $orderby = Utils\get_flag_value( $assoc_args, 'orderby' ); if ( 'id' !== $orderby ) { usort( $items, function ( $a, $b ) use ( $orderby, $order ) { // Sort array. return 'asc' === $order ? $a->$orderby > $b->$orderby : $a->$orderby < $b->$orderby; } ); } elseif ( 'id' === $orderby && 'desc' === $order ) { // Sort by default descending. krsort( $items ); } if ( ! empty( $assoc_args['fields'] ) ) { $fields = explode( ',', $assoc_args['fields'] ); } else { $fields = $this->get_fields(); } $formatter = new Formatter( $assoc_args, $fields, $this->meta_type ); $formatter->display_items( $items ); } /** * Get meta field value. * * ## OPTIONS * * <id> * : The ID of the object. * * <key> * : The name of the meta field to get. * * [--format=<format>] * : Get value in a particular format. * --- * default: var_export * options: * - var_export * - json * - yaml * --- */ public function get( $args, $assoc_args ) { list( $object_id, $meta_key ) = $args; $object_id = $this->check_object_id( $object_id ); $value = $this->get_metadata( $object_id, $meta_key, true ); if ( '' === $value ) { die( 1 ); } WP_CLI::print_value( $value, $assoc_args ); } /** * Delete a meta field. * * ## OPTIONS * * <id> * : The ID of the object. * * [<key>] * : The name of the meta field to delete. * * [<value>] * : The value to delete. If omitted, all rows with key will deleted. * * [--all] * : Delete all meta for the object. */ public function delete( $args, $assoc_args ) { list( $object_id ) = $args; $meta_key = ! empty( $args[1] ) ? $args[1] : ''; $meta_value = ! empty( $args[2] ) ? $args[2] : ''; if ( empty( $meta_key ) && ! Utils\get_flag_value( $assoc_args, 'all' ) ) { WP_CLI::error( 'Please specify a meta key, or use the --all flag.' ); } $object_id = $this->check_object_id( $object_id ); if ( Utils\get_flag_value( $assoc_args, 'all' ) ) { $errors = false; foreach ( $this->get_metadata( $object_id ) as $meta_key => $values ) { $success = $this->delete_metadata( $object_id, $meta_key ); if ( $success ) { WP_CLI::log( "Deleted '{$meta_key}' custom field." ); } else { WP_CLI::warning( "Failed to delete '{$meta_key}' custom field." ); $errors = true; } } if ( $errors ) { WP_CLI::error( 'Failed to delete all custom fields.' ); } else { WP_CLI::success( 'Deleted all custom fields.' ); } } else { $success = $this->delete_metadata( $object_id, $meta_key, $meta_value ); if ( $success ) { WP_CLI::success( 'Deleted custom field.' ); } else { WP_CLI::error( 'Failed to delete custom field.' ); } } } /** * Add a meta field. * * ## OPTIONS * * <id> * : The ID of the object. * * <key> * : The name of the meta field to create. * * [<value>] * : The value of the meta field. If omitted, the value is read from STDIN. * * [--format=<format>] * : The serialization format for the value. * --- * default: plaintext * options: * - plaintext * - json * --- */ public function add( $args, $assoc_args ) { list( $object_id, $meta_key ) = $args; $meta_value = WP_CLI::get_value_from_arg_or_stdin( $args, 2 ); $meta_value = WP_CLI::read_value( $meta_value, $assoc_args ); $object_id = $this->check_object_id( $object_id ); $meta_value = wp_slash( $meta_value ); $success = $this->add_metadata( $object_id, $meta_key, $meta_value ); if ( $success ) { WP_CLI::success( 'Added custom field.' ); } else { WP_CLI::error( 'Failed to add custom field.' ); } } /** * Update a meta field. * * ## OPTIONS * * <id> * : The ID of the object. * * <key> * : The name of the meta field to update. * * [<value>] * : The new value. If omitted, the value is read from STDIN. * * [--format=<format>] * : The serialization format for the value. * --- * default: plaintext * options: * - plaintext * - json * --- * * @alias set */ public function update( $args, $assoc_args ) { list( $object_id, $meta_key ) = $args; $meta_value = WP_CLI::get_value_from_arg_or_stdin( $args, 2 ); $meta_value = WP_CLI::read_value( $meta_value, $assoc_args ); $object_id = $this->check_object_id( $object_id ); $meta_value = sanitize_meta( $meta_key, $meta_value, $this->meta_type ); $old_value = sanitize_meta( $meta_key, $this->get_metadata( $object_id, $meta_key, true ), $this->meta_type ); if ( $meta_value === $old_value ) { WP_CLI::success( "Value passed for custom field '{$meta_key}' is unchanged." ); } else { $meta_value = wp_slash( $meta_value ); $success = $this->update_metadata( $object_id, $meta_key, $meta_value ); if ( $success ) { WP_CLI::success( "Updated custom field '{$meta_key}'." ); } else { WP_CLI::error( "Failed to update custom field '{$meta_key}'." ); } } } /** * Get a nested value from a meta field. * * ## OPTIONS * * <id> * : The ID of the object. * * <key> * : The name of the meta field to get. * * <key-path>... * : The name(s) of the keys within the value to locate the value to pluck. * * [--format=<format>] * : The output format of the value. * --- * default: plaintext * options: * - plaintext * - json * - yaml */ public function pluck( $args, $assoc_args ) { list( $object_id, $meta_key ) = $args; $object_id = $this->check_object_id( $object_id ); $key_path = array_map( function ( $key ) { if ( is_numeric( $key ) && ( (string) intval( $key ) === $key ) ) { return (int) $key; } return $key; }, array_slice( $args, 2 ) ); $value = $this->get_metadata( $object_id, $meta_key, true ); $traverser = new RecursiveDataStructureTraverser( $value ); try { $value = $traverser->get( $key_path ); } catch ( Exception $exception ) { die( 1 ); } WP_CLI::print_value( $value, $assoc_args ); } /** * Update a nested value for a meta field. * * ## OPTIONS * * <action> * : Patch action to perform. * --- * options: * - insert * - update * - delete * --- * * <id> * : The ID of the object. * * <key> * : The name of the meta field to update. * * <key-path>... * : The name(s) of the keys within the value to locate the value to patch. * * [<value>] * : The new value. If omitted, the value is read from STDIN. * * [--format=<format>] * : The serialization format for the value. * --- * default: plaintext * options: * - plaintext * - json * --- */ public function patch( $args, $assoc_args ) { list( $action, $object_id, $meta_key ) = $args; $object_id = $this->check_object_id( $object_id ); $key_path = array_map( function( $key ) { if ( is_numeric( $key ) && ( (string) intval( $key ) === $key ) ) { return (int) $key; } return $key; }, array_slice( $args, 3 ) ); if ( 'delete' === $action ) { $patch_value = null; } else { $stdin_value = EntityUtils::has_stdin() ? trim( WP_CLI::get_value_from_arg_or_stdin( $args, -1 ) ) : null; $patch_value = ! empty( $stdin_value ) ? WP_CLI::read_value( $stdin_value, $assoc_args ) : WP_CLI::read_value( array_pop( $key_path ), $assoc_args ); } /* Need to make a copy of $current_meta_value here as it is modified by reference */ $current_meta_value = sanitize_meta( $meta_key, $this->get_metadata( $object_id, $meta_key, true ), $this->meta_type ); $old_meta_value = $current_meta_value; if ( is_object( $current_meta_value ) ) { $old_meta_value = clone $current_meta_value; } $traverser = new RecursiveDataStructureTraverser( $current_meta_value ); try { $traverser->$action( $key_path, $patch_value ); } catch ( Exception $exception ) { WP_CLI::error( $exception->getMessage() ); } $patched_meta_value = sanitize_meta( $meta_key, $traverser->value(), $this->meta_type ); if ( $patched_meta_value === $old_meta_value ) { WP_CLI::success( "Value passed for custom field '{$meta_key}' is unchanged." ); } else { $slashed = wp_slash( $patched_meta_value ); $success = $this->update_metadata( $object_id, $meta_key, $slashed ); if ( $success ) { WP_CLI::success( "Updated custom field '{$meta_key}'." ); } else { WP_CLI::error( "Failed to update custom field '{$meta_key}'." ); } } } /** * Wrapper method for add_metadata that can be overridden in sub classes. * * @param int $object_id ID of the object the metadata is for. * @param string $meta_key Metadata key to use. * @param mixed $meta_value Metadata value. Must be serializable if * non-scalar. * @param bool $unique Optional, default is false. Whether the * specified metadata key should be unique for the * object. If true, and the object already has a * value for the specified metadata key, no change * will be made. * * @return int|false The meta ID on success, false on failure. */ protected function add_metadata( $object_id, $meta_key, $meta_value, $unique = false ) { return add_metadata( $this->meta_type, $object_id, $meta_key, $meta_value, $unique ); } /** * Wrapper method for update_metadata that can be overridden in sub classes. * * @param int $object_id ID of the object the metadata is for. * @param string $meta_key Metadata key to use. * @param mixed $meta_value Metadata value. Must be serializable if * non-scalar. * @param mixed $prev_value Optional. If specified, only update existing * metadata entries with the specified value. * Otherwise, update all entries. * * @return int|bool Meta ID if the key didn't exist, true on successful * update, false on failure. */ protected function update_metadata( $object_id, $meta_key, $meta_value, $prev_value = '' ) { return update_metadata( $this->meta_type, $object_id, $meta_key, $meta_value, $prev_value ); } /** * Wrapper method for get_metadata that can be overridden in sub classes. * * @param int $object_id ID of the object the metadata is for. * @param string $meta_key Optional. Metadata key. If not specified, * retrieve all metadata for the specified object. * @param bool $single Optional, default is false. If true, return only * the first value of the specified meta_key. This * parameter has no effect if meta_key is not * specified. * * @return mixed Single metadata value, or array of values. */ protected function get_metadata( $object_id, $meta_key = '', $single = false ) { return get_metadata( $this->meta_type, $object_id, $meta_key, $single ); } /** * Wrapper method for delete_metadata that can be overridden in sub classes. * * @param int $object_id ID of the object metadata is for * @param string $meta_key Metadata key * @param mixed $meta_value Optional. Metadata value. Must be serializable * if non-scalar. If specified, only delete * metadata entries with this value. Otherwise, * delete all entries with the specified meta_key. * Pass `null, `false`, or an empty string to skip * this check. For backward compatibility, it is * not possible to pass an empty string to delete * those entries with an empty string for a value. * * @return bool True on successful delete, false on failure. */ protected function delete_metadata( $object_id, $meta_key, $meta_value = '' ) { return delete_metadata( $this->meta_type, $object_id, $meta_key, $meta_value, false ); } /** * Get the fields for this object's meta * * @return array */ private function get_fields() { return [ "{$this->meta_type}_id", 'meta_key', 'meta_value', ]; } /** * Check that the object ID exists * * @param int */ protected function check_object_id( $object_id ) { // Needs to be set in subclass return $object_id; } } <?php use WP_CLI\CommandWithMeta; use WP_CLI\Fetchers\Post as PostFetcher; /** * Adds, updates, deletes, and lists post custom fields. * * ## EXAMPLES * * # Set post meta * $ wp post meta set 123 _wp_page_template about.php * Success: Updated custom field '_wp_page_template'. * * # Get post meta * $ wp post meta get 123 _wp_page_template * about.php * * # Update post meta * $ wp post meta update 123 _wp_page_template contact.php * Success: Updated custom field '_wp_page_template'. * * # Delete post meta * $ wp post meta delete 123 _wp_page_template * Success: Deleted custom field. */ class Post_Meta_Command extends CommandWithMeta { protected $meta_type = 'post'; /** * Check that the post ID exists * * @param int */ protected function check_object_id( $object_id ) { $fetcher = new PostFetcher(); $post = $fetcher->get_check( $object_id ); return $post->ID; } /** * Wrapper method for add_metadata that can be overridden in sub classes. * * @param int $object_id ID of the object the metadata is for. * @param string $meta_key Metadata key to use. * @param mixed $meta_value Metadata value. Must be serializable if * non-scalar. * @param bool $unique Optional, default is false. Whether the * specified metadata key should be unique for the * object. If true, and the object already has a * value for the specified metadata key, no change * will be made. * * @return int|false The meta ID on success, false on failure. */ protected function add_metadata( $object_id, $meta_key, $meta_value, $unique = false ) { return add_post_meta( $object_id, $meta_key, $meta_value, $unique ); } /** * Wrapper method for update_metadata that can be overridden in sub classes. * * @param int $object_id ID of the object the metadata is for. * @param string $meta_key Metadata key to use. * @param mixed $meta_value Metadata value. Must be serializable if * non-scalar. * @param mixed $prev_value Optional. If specified, only update existing * metadata entries with the specified value. * Otherwise, update all entries. * * @return int|bool Meta ID if the key didn't exist, true on successful * update, false on failure. */ protected function update_metadata( $object_id, $meta_key, $meta_value, $prev_value = '' ) { return update_post_meta( $object_id, $meta_key, $meta_value, $prev_value ); } /** * Wrapper method for get_metadata that can be overridden in sub classes. * * @param int $object_id ID of the object the metadata is for. * @param string $meta_key Optional. Metadata key. If not specified, * retrieve all metadata for the specified object. * @param bool $single Optional, default is false. If true, return only * the first value of the specified meta_key. This * parameter has no effect if meta_key is not * specified. * * @return mixed Single metadata value, or array of values. */ protected function get_metadata( $object_id, $meta_key = '', $single = false ) { return get_post_meta( $object_id, $meta_key, $single ); } /** * Wrapper method for delete_metadata that can be overridden in sub classes. * * @param int $object_id ID of the object metadata is for * @param string $meta_key Metadata key * @param mixed $meta_value Optional. Metadata value. Must be serializable * if non-scalar. If specified, only delete * metadata entries with this value. Otherwise, * delete all entries with the specified meta_key. * Pass `null, `false`, or an empty string to skip * this check. For backward compatibility, it is * not possible to pass an empty string to delete * those entries with an empty string for a value. * * @return bool True on successful delete, false on failure. */ protected function delete_metadata( $object_id, $meta_key, $meta_value = '' ) { return delete_post_meta( $object_id, $meta_key, $meta_value ); } } <?php use WP_CLI\CommandWithDBObject; use WP_CLI\Entity\Utils as EntityUtils; use WP_CLI\Fetchers\Site as SiteFetcher; use WP_CLI\Fetchers\User as UserFetcher; use WP_CLI\Formatter; use WP_CLI\Iterators\CSV as CsvIterator; use WP_CLI\Utils; /** * Manages users, along with their roles, capabilities, and meta. * * See references for [Roles and Capabilities](https://codex.wordpress.org/Roles_and_Capabilities) and [WP User class](https://codex.wordpress.org/Class_Reference/WP_User). * * ## EXAMPLES * * # List user IDs * $ wp user list --field=ID * 1 * * # Create a new user. * $ wp user create bob bob@example.com --role=author * Success: Created user 3. * Password: k9**&I4vNH(& * * # Update an existing user. * $ wp user update 123 --display_name=Mary --user_pass=marypass * Success: Updated user 123. * * # Delete user 123 and reassign posts to user 567 * $ wp user delete 123 --reassign=567 * Success: Removed user 123 from http://example.com * * @package wp-cli */ class User_Command extends CommandWithDBObject { protected $obj_type = 'user'; protected $obj_fields = [ 'ID', 'user_login', 'display_name', 'user_email', 'user_registered', 'roles', ]; private $cap_fields = [ 'name', ]; public function __construct() { $this->fetcher = new UserFetcher(); $this->sitefetcher = new SiteFetcher(); } /** * Lists users. * * Display WordPress users based on all arguments supported by * [WP_User_Query()](https://developer.wordpress.org/reference/classes/wp_user_query/prepare_query/). * * ## OPTIONS * * [--role=<role>] * : Only display users with a certain role. * * [--<field>=<value>] * : Control output by one or more arguments of WP_User_Query(). * * [--network] * : List all users in the network for multisite. * * [--field=<field>] * : Prints the value of a single field for each user. * * [--fields=<fields>] * : Limit the output to specific object fields. * * [--format=<format>] * : Render output in a particular format. * --- * default: table * options: * - table * - csv * - ids * - json * - count * - yaml * --- * * ## AVAILABLE FIELDS * * These fields will be displayed by default for each user: * * * ID * * user_login * * display_name * * user_email * * user_registered * * roles * * These fields are optionally available: * * * user_pass * * user_nicename * * user_url * * user_activation_key * * user_status * * spam * * deleted * * caps * * cap_key * * allcaps * * filter * * url * * ## EXAMPLES * * # List user IDs * $ wp user list --field=ID * 1 * * # List users with administrator role * $ wp user list --role=administrator --format=csv * ID,user_login,display_name,user_email,user_registered,roles * 1,supervisor,supervisor,supervisor@gmail.com,"2016-06-03 04:37:00",administrator * * # List users with only given fields * $ wp user list --fields=display_name,user_email --format=json * [{"display_name":"supervisor","user_email":"supervisor@gmail.com"}] * * # List users ordered by the 'last_activity' meta value. * $ wp user list --meta_key=last_activity --orderby=meta_value_num * * @subcommand list */ public function list_( $args, $assoc_args ) { if ( Utils\get_flag_value( $assoc_args, 'network' ) ) { if ( ! is_multisite() ) { WP_CLI::error( 'This is not a multisite installation.' ); } $assoc_args['blog_id'] = 0; if ( isset( $assoc_args['fields'] ) ) { $fields = explode( ',', $assoc_args['fields'] ); $assoc_args['fields'] = array_diff( $fields, [ 'roles' ] ); } else { $assoc_args['fields'] = array_diff( $this->obj_fields, [ 'roles' ] ); } } $formatter = $this->get_formatter( $assoc_args ); if ( in_array( $formatter->format, [ 'ids', 'count' ], true ) ) { $assoc_args['fields'] = 'ids'; } else { $assoc_args['fields'] = 'all_with_meta'; } $assoc_args['count_total'] = false; $assoc_args = self::process_csv_arguments_to_arrays( $assoc_args ); $users = get_users( $assoc_args ); if ( 'ids' === $formatter->format ) { echo implode( ' ', $users ); } elseif ( 'count' === $formatter->format ) { $formatter->display_items( $users ); } else { $iterator = Utils\iterator_map( $users, function ( $user ) { if ( ! is_object( $user ) ) { return $user; } $user->roles = implode( ',', $user->roles ); $user->url = get_author_posts_url( $user->ID, $user->user_nicename ); return $user; } ); $formatter->display_items( $iterator ); } } /** * Gets details about a user. * * ## OPTIONS * * <user> * : User ID, user email, or user login. * * [--field=<field>] * : Instead of returning the whole user, returns the value of a single field. * * [--fields=<fields>] * : Get a specific subset of the user's fields. * * [--format=<format>] * : Render output in a particular format. * --- * default: table * options: * - table * - csv * - json * - yaml * --- * * ## EXAMPLES * * # Get user * $ wp user get 12 --field=login * supervisor * * # Get user and export to JSON file * $ wp user get bob --format=json > bob.json */ public function get( $args, $assoc_args ) { $user = $this->fetcher->get_check( $args[0] ); $user_data = $user->to_array(); $user_data['roles'] = implode( ', ', $user->roles ); $formatter = $this->get_formatter( $assoc_args ); $formatter->display_item( $user_data ); } /** * Deletes one or more users from the current site. * * On multisite, `wp user delete` only removes the user from the current * site. Include `--network` to also remove the user from the database, but * make sure to reassign their posts prior to deleting the user. * * ## OPTIONS * * <user>... * : The user login, user email, or user ID of the user(s) to delete. * * [--network] * : On multisite, delete the user from the entire network. * * [--reassign=<user-id>] * : User ID to reassign the posts to. * * [--yes] * : Answer yes to any confirmation prompts. * * ## EXAMPLES * * # Delete user 123 and reassign posts to user 567 * $ wp user delete 123 --reassign=567 * Success: Removed user 123 from http://example.com * * # Delete all contributors and reassign their posts to user 2 * $ wp user delete $(wp user list --role=contributor --field=ID) --reassign=2 * Success: Removed user 813 from http://example.com * Success: Removed user 578 from http://example.com */ public function delete( $args, $assoc_args ) { $network = Utils\get_flag_value( $assoc_args, 'network' ) && is_multisite(); $reassign = Utils\get_flag_value( $assoc_args, 'reassign' ); if ( $network && $reassign ) { WP_CLI::error( 'Reassigning content to a different user is not supported on multisite.' ); } if ( ! $reassign ) { WP_CLI::confirm( '--reassign parameter not passed. All associated posts will be deleted. Proceed?', $assoc_args ); } $users = $this->fetcher->get_many( $args ); parent::_delete( $users, $assoc_args, function ( $user ) use ( $network, $reassign ) { $user_id = $user->ID; if ( $network ) { $result = wpmu_delete_user( $user_id ); $message = "Deleted user {$user_id}."; } else { $result = wp_delete_user( $user_id, $reassign ); $message = "Removed user {$user_id} from " . home_url() . '.'; } if ( ! $result ) { $message = "Failed deleting user {$user_id}."; if ( is_multisite() && is_super_admin( $user_id ) ) { $message .= ' The user is a super admin.'; } return [ 'error', $message ]; } return [ 'success', $message ]; } ); } /** * Creates a new user. * * ## OPTIONS * * <user-login> * : The login of the user to create. * * <user-email> * : The email address of the user to create. * * [--role=<role>] * : The role of the user to create. Default: default role. Possible values * include 'administrator', 'editor', 'author', 'contributor', 'subscriber'. * * [--user_pass=<password>] * : The user password. Default: randomly generated. * * [--user_registered=<yyyy-mm-dd-hh-ii-ss>] * : The date the user registered. Default: current date. * * [--display_name=<name>] * : The display name. * * [--user_nicename=<nice_name>] * : A string that contains a URL-friendly name for the user. The default is the user's username. * * [--user_url=<url>] * : A string containing the user's URL for the user's web site. * * [--nickname=<nickname>] * : The user's nickname, defaults to the user's username. * * [--first_name=<first_name>] * : The user's first name. * * [--last_name=<last_name>] * : The user's last name. * * [--description=<description>] * : A string containing content about the user. * * [--rich_editing=<rich_editing>] * : A string for whether to enable the rich editor or not. False if not empty. * * [--send-email] * : Send an email to the user with their new account details. * * [--porcelain] * : Output just the new user id. * * ## EXAMPLES * * # Create user * $ wp user create bob bob@example.com --role=author * Success: Created user 3. * Password: k9**&I4vNH(& * * # Create user without showing password upon success * $ wp user create ann ann@example.com --porcelain * 4 */ public function create( $args, $assoc_args ) { $user = new stdClass(); list( $user->user_login, $user->user_email ) = $args; $assoc_args = wp_slash( $assoc_args ); if ( username_exists( $user->user_login ) ) { WP_CLI::error( "The '{$user->user_login}' username is already registered." ); } if ( ! is_email( $user->user_email ) ) { WP_CLI::error( "The '{$user->user_email}' email address is invalid." ); } $user->user_registered = Utils\get_flag_value( $assoc_args, 'user_registered', date_format( date_create(), '%F %T' ) ); $user->display_name = Utils\get_flag_value( $assoc_args, 'display_name', false ); $user->first_name = Utils\get_flag_value( $assoc_args, 'first_name', false ); $user->last_name = Utils\get_flag_value( $assoc_args, 'last_name', false ); $user->description = Utils\get_flag_value( $assoc_args, 'description', false ); if ( isset( $assoc_args['user_pass'] ) ) { $user->user_pass = $assoc_args['user_pass']; } else { $user->user_pass = wp_generate_password( 24 ); $generated_pass = true; } $user->role = Utils\get_flag_value( $assoc_args, 'role', get_option( 'default_role' ) ); self::validate_role( $user->role ); if ( ! Utils\get_flag_value( $assoc_args, 'send-email' ) ) { add_filter( 'send_password_change_email', '__return_false' ); add_filter( 'send_email_change_email', '__return_false' ); } if ( is_multisite() ) { $result = wpmu_validate_user_signup( $user->user_login, $user->user_email ); if ( is_wp_error( $result['errors'] ) && ! empty( $result['errors']->errors ) ) { WP_CLI::error( $result['errors'] ); } $user_id = wpmu_create_user( $user->user_login, $user->user_pass, $user->user_email ); if ( ! $user_id ) { WP_CLI::error( 'Unknown error creating new user.' ); } $user->ID = $user_id; $user_id = wp_update_user( $user ); if ( is_wp_error( $user_id ) ) { WP_CLI::error( $user_id ); } } else { $user_id = wp_insert_user( $user ); } if ( ! $user_id || is_wp_error( $user_id ) ) { if ( ! $user_id ) { $user_id = 'Unknown error creating new user.'; } WP_CLI::error( $user_id ); } else { if ( false === $user->role ) { delete_user_option( $user_id, 'capabilities' ); delete_user_option( $user_id, 'user_level' ); } } if ( Utils\get_flag_value( $assoc_args, 'send-email' ) ) { self::wp_new_user_notification( $user_id, $user->user_pass ); } if ( Utils\get_flag_value( $assoc_args, 'porcelain' ) ) { WP_CLI::line( $user_id ); } else { WP_CLI::success( "Created user {$user_id}." ); if ( isset( $generated_pass ) ) { WP_CLI::line( "Password: {$user->user_pass}" ); } } } /** * Updates an existing user. * * ## OPTIONS * * <user>... * : The user login, user email or user ID of the user(s) to update. * * [--user_pass=<password>] * : A string that contains the plain text password for the user. * * [--user_nicename=<nice_name>] * : A string that contains a URL-friendly name for the user. The default is the user's username. * * [--user_url=<url>] * : A string containing the user's URL for the user's web site. * * [--user_email=<email>] * : A string containing the user's email address. * * [--display_name=<display_name>] * : A string that will be shown on the site. Defaults to user's username. * * [--nickname=<nickname>] * : The user's nickname, defaults to the user's username. * * [--first_name=<first_name>] * : The user's first name. * * [--last_name=<last_name>] * : The user's last name. * * [--description=<description>] * : A string containing content about the user. * * [--rich_editing=<rich_editing>] * : A string for whether to enable the rich editor or not. False if not empty. * * [--user_registered=<yyyy-mm-dd-hh-ii-ss>] * : The date the user registered. * * [--role=<role>] * : A string used to set the user's role. * * --<field>=<value> * : One or more fields to update. For accepted fields, see wp_update_user(). * * [--skip-email] * : Don't send an email notification to the user. * * ## EXAMPLES * * # Update user * $ wp user update 123 --display_name=Mary --user_pass=marypass * Success: Updated user 123. */ public function update( $args, $assoc_args ) { if ( isset( $assoc_args['user_login'] ) ) { WP_CLI::warning( "User logins can't be changed." ); unset( $assoc_args['user_login'] ); } $user_ids = []; foreach ( $this->fetcher->get_many( $args ) as $user ) { $user_ids[] = $user->ID; } $skip_email = Utils\get_flag_value( $assoc_args, 'skip-email' ); if ( $skip_email ) { add_filter( 'send_email_change_email', '__return_false' ); add_filter( 'send_password_change_email', '__return_false' ); } $assoc_args = wp_slash( $assoc_args ); parent::_update( $user_ids, $assoc_args, 'wp_update_user' ); if ( $skip_email ) { remove_filter( 'send_email_change_email', '__return_false' ); remove_filter( 'send_password_change_email', '__return_false' ); } } /** * Generates some users. * * Creates a specified number of new users with dummy data. * * ## OPTIONS * * [--count=<number>] * : How many users to generate? * --- * default: 100 * --- * * [--role=<role>] * : The role of the generated users. Default: default role from WP * * [--format=<format>] * : Render output in a particular format. * --- * default: progress * options: * - progress * - ids * --- * * ## EXAMPLES * * # Add meta to every generated users. * $ wp user generate --format=ids --count=3 | xargs -d ' ' -I % wp user meta add % foo bar * Success: Added custom field. * Success: Added custom field. * Success: Added custom field. */ public function generate( $args, $assoc_args ) { global $blog_id; $defaults = [ 'count' => 100, 'role' => get_option( 'default_role' ), ]; $assoc_args = array_merge( $defaults, $assoc_args ); $role = $assoc_args['role']; if ( ! empty( $role ) ) { self::validate_role( $role ); } $user_count = count_users(); $total = $user_count['total_users']; $limit = $assoc_args['count'] + $total; $format = Utils\get_flag_value( $assoc_args, 'format', 'progress' ); $notify = false; if ( 'progress' === $format ) { $notify = Utils\make_progress_bar( 'Generating users', $assoc_args['count'] ); } for ( $index = $total; $index < $limit; $index++ ) { $login = "user_{$blog_id}_{$index}"; $name = "User {$index}"; $user_id = wp_insert_user( [ 'user_login' => $login, 'user_pass' => $login, 'nickname' => $name, 'display_name' => $name, 'role' => $role, ] ); if ( false === $role ) { delete_user_option( $user_id, 'capabilities' ); delete_user_option( $user_id, 'user_level' ); } if ( 'progress' === $format ) { $notify->tick(); } elseif ( 'ids' === $format ) { echo $user_id; if ( $index < $limit - 1 ) { echo ' '; } } } if ( 'progress' === $format ) { $notify->finish(); } } /** * Sets the user role. * * ## OPTIONS * * <user> * : User ID, user email, or user login. * * [<role>] * : Make the user have the specified role. If not passed, the default role is * used. * * ## EXAMPLES * * $ wp user set-role 12 author * Success: Added johndoe (12) to http://example.com as author. * * @subcommand set-role */ public function set_role( $args, $assoc_args ) { $user = $this->fetcher->get_check( $args[0] ); $role = Utils\get_flag_value( $args, 1, get_option( 'default_role' ) ); self::validate_role( $role ); // Multisite if ( function_exists( 'add_user_to_blog' ) ) { add_user_to_blog( get_current_blog_id(), $user->ID, $role ); } else { $user->set_role( $role ); } WP_CLI::success( "Added {$user->user_login} ({$user->ID}) to " . site_url() . " as {$role}." ); } /** * Adds a role for a user. * * ## OPTIONS * * <user> * : User ID, user email, or user login. * * <role> * : Add the specified role to the user. * * ## EXAMPLES * * $ wp user add-role 12 author * Success: Added 'author' role for johndoe (12). * * @subcommand add-role */ public function add_role( $args, $assoc_args ) { $user = $this->fetcher->get_check( $args[0] ); $role = $args[1]; self::validate_role( $role ); $user->add_role( $role ); WP_CLI::success( "Added '{$role}' role for {$user->user_login} ({$user->ID})." ); } /** * Removes a user's role. * * ## OPTIONS * * <user> * : User ID, user email, or user login. * * [<role>] * : A specific role to remove. * * ## EXAMPLES * * $ wp user remove-role 12 author * Success: Removed 'author' role for johndoe (12). * * @subcommand remove-role */ public function remove_role( $args, $assoc_args ) { $user = $this->fetcher->get_check( $args[0] ); if ( isset( $args[1] ) ) { $role = $args[1]; self::validate_role( $role ); $user->remove_role( $role ); WP_CLI::success( "Removed '{$role}' role for {$user->user_login} ({$user->ID})." ); } else { // Multisite if ( function_exists( 'remove_user_from_blog' ) ) { remove_user_from_blog( $user->ID, get_current_blog_id() ); } else { $user->remove_all_caps(); } WP_CLI::success( "Removed {$user->user_login} ({$user->ID}) from " . site_url() . '.' ); } } /** * Adds a capability to a user. * * ## OPTIONS * * <user> * : User ID, user email, or user login. * * <cap> * : The capability to add. * * ## EXAMPLES * * # Add a capability for a user * $ wp user add-cap john create_premium_item * Success: Added 'create_premium_item' capability for john (16). * * # Add a capability for a user * $ wp user add-cap 15 edit_product * Success: Added 'edit_product' capability for johndoe (15). * * @subcommand add-cap */ public function add_cap( $args, $assoc_args ) { $user = $this->fetcher->get_check( $args[0] ); if ( $user ) { $cap = $args[1]; $user->add_cap( $cap ); WP_CLI::success( "Added '{$cap}' capability for {$user->user_login} ({$user->ID})." ); } } /** * Removes a user's capability. * * ## OPTIONS * * <user> * : User ID, user email, or user login. * * <cap> * : The capability to be removed. * * ## EXAMPLES * * $ wp user remove-cap 11 publish_newsletters * Success: Removed 'publish_newsletters' cap for supervisor (11). * * $ wp user remove-cap 11 publish_posts * Error: The 'publish_posts' cap for supervisor (11) is inherited from a role. * * $ wp user remove-cap 11 nonexistent_cap * Error: No such 'nonexistent_cap' cap for supervisor (11). * * @subcommand remove-cap */ public function remove_cap( $args, $assoc_args ) { $user = $this->fetcher->get_check( $args[0] ); if ( $user ) { $cap = $args[1]; if ( ! isset( $user->caps[ $cap ] ) ) { if ( isset( $user->allcaps[ $cap ] ) ) { WP_CLI::error( "The '{$cap}' cap for {$user->user_login} ({$user->ID}) is inherited from a role." ); } WP_CLI::error( "No such '{$cap}' cap for {$user->user_login} ({$user->ID})." ); } $user->remove_cap( $cap ); WP_CLI::success( "Removed '{$cap}' cap for {$user->user_login} ({$user->ID})." ); } } /** * Lists all capabilities for a user. * * ## OPTIONS * * <user> * : User ID, user email, or login. * * [--format=<format>] * : Render output in a particular format. * --- * default: list * options: * - list * - table * - csv * - json * - count * - yaml * --- * * ## EXAMPLES * * $ wp user list-caps 21 * edit_product * create_premium_item * * @subcommand list-caps */ public function list_caps( $args, $assoc_args ) { $user = $this->fetcher->get_check( $args[0] ); if ( $user ) { $user->get_role_caps(); $user_caps_list = $user->allcaps; $active_user_cap_list = []; foreach ( $user_caps_list as $cap => $active ) { if ( $active ) { $active_user_cap_list[] = $cap; } } if ( 'list' === $assoc_args['format'] ) { foreach ( $active_user_cap_list as $cap ) { WP_CLI::line( $cap ); } } else { $output_caps = []; foreach ( $active_user_cap_list as $cap ) { $output_cap = new stdClass(); $output_cap->name = $cap; $output_caps[] = $output_cap; } $formatter = new Formatter( $assoc_args, $this->cap_fields ); $formatter->display_items( $output_caps ); } } } /** * Imports users from a CSV file. * * If the user already exists (matching the email address or login), then * the user is updated unless the `--skip-update` flag is used. * * ## OPTIONS * * <file> * : The local or remote CSV file of users to import. If '-', then reads from STDIN. * * [--send-email] * : Send an email to new users with their account details. * * [--skip-update] * : Don't update users that already exist. * * ## EXAMPLES * * # Import users from local CSV file * $ wp user import-csv /path/to/users.csv * Success: bobjones created * Success: newuser1 created * Success: existinguser created * * # Import users from remote CSV file * $ wp user import-csv http://example.com/users.csv * * Sample users.csv file: * * user_login,user_email,display_name,role * bobjones,bobjones@example.com,Bob Jones,contributor * newuser1,newuser1@example.com,New User,author * existinguser,existinguser@example.com,Existing User,administrator * * @subcommand import-csv */ public function import_csv( $args, $assoc_args ) { $blog_users = get_users(); $filename = $args[0]; if ( 0 === stripos( $filename, 'http://' ) || 0 === stripos( $filename, 'https://' ) ) { $response = wp_remote_head( $filename ); $response_code = (string) wp_remote_retrieve_response_code( $response ); if ( in_array( (int) $response_code[0], [ 4, 5 ], true ) ) { WP_CLI::error( "Couldn't access remote CSV file (HTTP {$response_code} response)." ); } } elseif ( '-' === $filename ) { if ( ! EntityUtils::has_stdin() ) { WP_CLI::error( 'Unable to read content from STDIN.' ); } } elseif ( ! file_exists( $filename ) ) { WP_CLI::error( "Missing file: {$filename}" ); } // Don't send core's emails during the creation / update process add_filter( 'send_password_change_email', '__return_false' ); add_filter( 'send_email_change_email', '__return_false' ); if ( '-' === $filename ) { $file_object = new NoRewindIterator( new SplFileObject( 'php://stdin' ) ); $file_object->setFlags( SplFileObject::READ_CSV ); $csv_data = []; $indexes = []; foreach ( $file_object as $line ) { if ( empty( $line[0] ) ) { continue; } if ( empty( $indexes ) ) { $indexes = $line; continue; } foreach ( $indexes as $n => $key ) { $data[ $key ] = $line[ $n ]; } $csv_data[] = $data; } } else { $csv_data = new CsvIterator( $filename ); } foreach ( $csv_data as $new_user ) { $defaults = [ 'role' => get_option( 'default_role' ), 'user_pass' => wp_generate_password(), 'user_registered' => date_format( date_create(), '%F %T' ), 'display_name' => false, ]; $new_user = array_merge( $defaults, $new_user ); $secondary_roles = []; if ( ! empty( $new_user['roles'] ) ) { $roles = array_map( 'trim', explode( ',', $new_user['roles'] ) ); $invalid_role = false; foreach ( $roles as $role ) { if ( null === get_role( $role ) ) { WP_CLI::warning( "{$new_user['user_login']} has an invalid role." ); $invalid_role = true; break; } } if ( $invalid_role ) { continue; } $new_user['role'] = array_shift( $roles ); $secondary_roles = $roles; } elseif ( 'none' === $new_user['role'] ) { $new_user['role'] = false; } elseif ( null === get_role( $new_user['role'] ) ) { WP_CLI::warning( "{$new_user['user_login']} has an invalid role." ); continue; } // User already exists and we just need to add them to the site if they aren't already there $existing_user = get_user_by( 'email', $new_user['user_email'] ); if ( ! $existing_user ) { $existing_user = get_user_by( 'login', $new_user['user_login'] ); } if ( $existing_user && Utils\get_flag_value( $assoc_args, 'skip-update' ) ) { WP_CLI::log( "{$existing_user->user_login} exists and has been skipped." ); continue; } if ( $existing_user ) { $new_user['ID'] = $existing_user->ID; $user_id = wp_update_user( $new_user ); if ( ! in_array( $existing_user->user_login, wp_list_pluck( $blog_users, 'user_login' ), true ) && is_multisite() && $new_user['role'] ) { add_user_to_blog( get_current_blog_id(), $existing_user->ID, $new_user['role'] ); WP_CLI::log( "{$existing_user->user_login} added as {$new_user['role']}." ); } // Create the user } else { unset( $new_user['ID'] ); // Unset else it will just return the ID if ( is_multisite() ) { $result = wpmu_validate_user_signup( $new_user['user_login'], $new_user['user_email'] ); if ( is_wp_error( $result['errors'] ) && ! empty( $result['errors']->errors ) ) { WP_CLI::warning( $result['errors'] ); continue; } $user_id = wpmu_create_user( $new_user['user_login'], $new_user['user_pass'], $new_user['user_email'] ); if ( ! $user_id ) { WP_CLI::warning( 'Unknown error creating new user.' ); continue; } $new_user['ID'] = $user_id; $user_id = wp_update_user( $new_user ); if ( is_wp_error( $user_id ) ) { WP_CLI::warning( $user_id ); continue; } } else { $user_id = wp_insert_user( $new_user ); } if ( Utils\get_flag_value( $assoc_args, 'send-email' ) ) { self::wp_new_user_notification( $user_id, $new_user['user_pass'] ); } } if ( is_wp_error( $user_id ) ) { WP_CLI::warning( $user_id ); continue; } if ( false === $new_user['role'] ) { delete_user_option( $user_id, 'capabilities' ); delete_user_option( $user_id, 'user_level' ); } $user = get_user_by( 'id', $user_id ); foreach ( $secondary_roles as $secondary_role ) { $user->add_role( $secondary_role ); } if ( ! empty( $existing_user ) ) { WP_CLI::success( $new_user['user_login'] . ' updated.' ); } else { WP_CLI::success( $new_user['user_login'] . ' created.' ); } } } /** * Resets the password for one or more users. * * ## OPTIONS * * <user>... * : one or more user logins or IDs. * * [--skip-email] * : Don't send an email notification to the affected user(s). * * ## EXAMPLES * * # Reset the password for two users and send them the change email. * $ wp user reset-password admin editor * Reset password for admin. * Reset password for editor. * Success: Passwords reset for 2 users. * * @subcommand reset-password */ public function reset_password( $args, $assoc_args ) { $skip_email = Utils\get_flag_value( $assoc_args, 'skip-email' ); if ( $skip_email ) { add_filter( 'send_password_change_email', '__return_false' ); } $fetcher = new UserFetcher(); $users = $fetcher->get_many( $args ); foreach ( $users as $user ) { wp_update_user( [ 'ID' => $user->ID, 'user_pass' => wp_generate_password(), ] ); WP_CLI::log( "Reset password for {$user->user_login}." ); } if ( $skip_email ) { remove_filter( 'send_password_change_email', '__return_false' ); } $reset_user_count = count( $users ); if ( 1 === $reset_user_count ) { WP_CLI::success( "Password reset for {$reset_user_count} user." ); } elseif ( $reset_user_count > 1 ) { WP_CLI::success( "Passwords reset for {$reset_user_count} users." ); } else { WP_CLI::error( 'No user found to reset password.' ); } } /** * Checks whether the role is valid * * @param string */ private static function validate_role( $role ) { if ( ! empty( $role ) && null === get_role( $role ) ) { WP_CLI::error( "Role doesn't exist: {$role}" ); } } /** * Accommodates three different behaviors for wp_new_user_notification() * - 4.3.1 and above: expect second argument to be deprecated * - 4.3: Second argument was repurposed as $notify * - Below 4.3: Send the password in the notification * * @param string $user_id * @param string $password */ public static function wp_new_user_notification( $user_id, $password ) { if ( Utils\wp_version_compare( '4.3.1', '>=' ) ) { wp_new_user_notification( $user_id, null, 'both' ); } elseif ( Utils\wp_version_compare( '4.3', '>=' ) ) { // phpcs:ignore WordPress.WP.DeprecatedParameters.Wp_new_user_notificationParam2Found -- Only called in valid conditions. wp_new_user_notification( $user_id, 'both' ); } else { // phpcs:ignore WordPress.WP.DeprecatedParameters.Wp_new_user_notificationParam2Found -- Only called in valid conditions. wp_new_user_notification( $user_id, $password ); } } /** * Marks one or more users as spam. * * ## OPTIONS * * <id>... * : One or more IDs of users to mark as spam. * * ## EXAMPLES * * $ wp user spam 123 * User 123 marked as spam. * Success: Spamed 1 of 1 users. */ public function spam( $args ) { $this->update_msuser_status( $args, 'spam', '1' ); } /** * Removes one or more users from spam. * * ## OPTIONS * * <id>... * : One or more IDs of users to remove from spam. * * ## EXAMPLES * * $ wp user unspam 123 * User 123 removed from spam. * Success: Unspamed 1 of 1 users. */ public function unspam( $args ) { $this->update_msuser_status( $args, 'spam', '0' ); } /** * Common command for updating user data. */ private function update_msuser_status( $user_ids, $pref, $value ) { // If site is not multisite, then stop execution. if ( ! is_multisite() ) { WP_CLI::error( 'This is not a multisite installation.' ); } if ( 'spam' === $pref ) { $action = (int) $value ? 'marked as spam' : 'removed from spam'; $verb = (int) $value ? 'spam' : 'unspam'; } $successes = 0; $errors = 0; $users = $this->fetcher->get_many( $user_ids ); if ( count( $users ) < count( $user_ids ) ) { $errors = count( $user_ids ) - count( $users ); } foreach ( $user_ids as $user_id ) { $user = get_userdata( $user_id ); // If no user found, then show warning. if ( empty( $user ) ) { WP_CLI::warning( "User {$user_id} doesn't exist." ); continue; } // Super admin should not be marked as spam. if ( is_super_admin( $user->ID ) ) { WP_CLI::warning( "User cannot be modified. The user {$user->ID} is a network administrator." ); continue; } // Skip if user is already marked as spam and show warning. if ( $value === $user->spam ) { WP_CLI::warning( "User {$user_id} already {$action}." ); continue; } // Make that user's blog as spam too. $blogs = (array) get_blogs_of_user( $user_id, true ); foreach ( $blogs as $details ) { $site = $this->sitefetcher->get_check( $details->site_id ); // Main blog shouldn't a spam ! if ( $details->userblog_id !== $site->blog_id ) { update_blog_status( $details->userblog_id, $pref, $value ); } } if ( Utils\wp_version_compare( '5.3', '<' ) ) { // phpcs:ignore WordPress.WP.DeprecatedFunctions.update_user_statusFound -- Fallback for older versions. update_user_status( $user_id, $pref, $value ); } else { wp_update_user( [ 'ID' => $user_id, $pref => $value, ] ); } WP_CLI::log( "User {$user_id} {$action}." ); $successes++; } Utils\report_batch_operation_results( 'user', $verb, count( $user_ids ), $successes, $errors ); } /** * Checks if a user's password is valid or not. * * ## OPTIONS * * <user> * : The user login, user email or user ID of the user to check credentials for. * * <user_pass> * : A string that contains the plain text password for the user. * * [--escape-chars] * : Escape password with `wp_slash()` to mimic the same behavior as `wp-login.php`. * * ## EXAMPLES * * # Check whether given credentials are valid; exit status 0 if valid, otherwise 1 * $ wp user check-password admin adminpass * $ echo $? * 1 * * # Bash script for checking whether given credentials are valid or not * if ! $(wp user check-password admin adminpass); then * notify-send "Invalid Credentials"; * fi * * @subcommand check-password */ public function check_password( $args, $assoc_args ) { $escape_chars = Utils\get_flag_value( $assoc_args, 'escape-chars', false ); if ( ! $escape_chars && wp_slash( wp_unslash( $args[1] ) ) !== $args[1] ) { WP_CLI::warning( 'Password contains characters that need to be escaped. Please escape them manually or use the `--escape-chars` option.' ); } $user = $this->fetcher->get_check( $args[0] ); $user_pass = $escape_chars ? wp_slash( $args[1] ) : $args[1]; if ( wp_check_password( $user_pass, $user->data->user_pass, $user->ID ) ) { WP_CLI::halt( 0 ); } else { WP_CLI::halt( 1 ); } } } <?php use WP_CLI\CommandWithDBObject; use WP_CLI\Entity\Utils as EntityUtils; use WP_CLI\Fetchers\Post as PostFetcher; use WP_CLI\Fetchers\User as UserFetcher; use WP_CLI\Utils; /** * Manages posts, content, and meta. * * ## EXAMPLES * * # Create a new post. * $ wp post create --post_type=post --post_title='A sample post' * Success: Created post 123. * * # Update an existing post. * $ wp post update 123 --post_status=draft * Success: Updated post 123. * * # Delete an existing post. * $ wp post delete 123 * Success: Trashed post 123. * * @package wp-cli */ class Post_Command extends CommandWithDBObject { protected $obj_type = 'post'; protected $obj_fields = [ 'ID', 'post_title', 'post_name', 'post_date', 'post_status', ]; public function __construct() { $this->fetcher = new PostFetcher(); } /** * Creates a new post. * * ## OPTIONS * * [--post_author=<post_author>] * : The ID of the user who added the post. Default is the current user ID. * * [--post_date=<post_date>] * : The date of the post. Default is the current time. * * [--post_date_gmt=<post_date_gmt>] * : The date of the post in the GMT timezone. Default is the value of $post_date. * * [--post_content=<post_content>] * : The post content. Default empty. * * [--post_content_filtered=<post_content_filtered>] * : The filtered post content. Default empty. * * [--post_title=<post_title>] * : The post title. Default empty. * * [--post_excerpt=<post_excerpt>] * : The post excerpt. Default empty. * * [--post_status=<post_status>] * : The post status. Default 'draft'. * * [--post_type=<post_type>] * : The post type. Default 'post'. * * [--comment_status=<comment_status>] * : Whether the post can accept comments. Accepts 'open' or 'closed'. Default is the value of 'default_comment_status' option. * * [--ping_status=<ping_status>] * : Whether the post can accept pings. Accepts 'open' or 'closed'. Default is the value of 'default_ping_status' option. * * [--post_password=<post_password>] * : The password to access the post. Default empty. * * [--post_name=<post_name>] * : The post name. Default is the sanitized post title when creating a new post. * * [--from-post=<post_id>] * : Post id of a post to be duplicated. * * [--to_ping=<to_ping>] * : Space or carriage return-separated list of URLs to ping. Default empty. * * [--pinged=<pinged>] * : Space or carriage return-separated list of URLs that have been pinged. Default empty. * * [--post_modified=<post_modified>] * : The date when the post was last modified. Default is the current time. * * [--post_modified_gmt=<post_modified_gmt>] * : The date when the post was last modified in the GMT timezone. Default is the current time. * * [--post_parent=<post_parent>] * : Set this for the post it belongs to, if any. Default 0. * * [--menu_order=<menu_order>] * : The order the post should be displayed in. Default 0. * * [--post_mime_type=<post_mime_type>] * : The mime type of the post. Default empty. * * [--guid=<guid>] * : Global Unique ID for referencing the post. Default empty. * * [--post_category=<post_category>] * : Array of category names, slugs, or IDs. Defaults to value of the 'default_category' option. * * [--tags_input=<tags_input>] * : Array of tag names, slugs, or IDs. Default empty. * * [--tax_input=<tax_input>] * : Array of taxonomy terms keyed by their taxonomy name. Default empty. * * [--meta_input=<meta_input>] * : Array in JSON format of post meta values keyed by their post meta key. Default empty. * * [<file>] * : Read post content from <file>. If this value is present, the * `--post_content` argument will be ignored. * * Passing `-` as the filename will cause post content to * be read from STDIN. * * [--<field>=<value>] * : Associative args for the new post. See wp_insert_post(). * * [--edit] * : Immediately open system's editor to write or edit post content. * * If content is read from a file, from STDIN, or from the `--post_content` * argument, that text will be loaded into the editor. * * [--porcelain] * : Output just the new post id. * * * ## EXAMPLES * * # Create post and schedule for future * $ wp post create --post_type=page --post_title='A future post' --post_status=future --post_date='2020-12-01 07:00:00' * Success: Created post 1921. * * # Create post with content from given file * $ wp post create ./post-content.txt --post_category=201,345 --post_title='Post from file' * Success: Created post 1922. * * # Create a post with multiple meta values. * $ wp post create --post_title='A post' --post_content='Just a small post.' --meta_input='{"key1":"value1","key2":"value2"}' * Success: Created post 1923. * * # Create a duplicate post from existing posts. * $ wp post create --from-post=123 --post_title='Different Title' * Success: Created post 2350. */ public function create( $args, $assoc_args ) { if ( ! empty( $args[0] ) ) { $assoc_args['post_content'] = $this->read_from_file_or_stdin( $args[0] ); } if ( Utils\get_flag_value( $assoc_args, 'edit' ) ) { $input = Utils\get_flag_value( $assoc_args, 'post_content', '' ); $output = $this->_edit( $input, 'WP-CLI: New Post' ); if ( $output ) { $assoc_args['post_content'] = $output; } else { $assoc_args['post_content'] = $input; } } if ( isset( $assoc_args['post_category'] ) ) { $assoc_args['post_category'] = $this->get_category_ids( $assoc_args['post_category'] ); } if ( isset( $assoc_args['meta_input'] ) && Utils\wp_version_compare( '4.4', '<' ) ) { WP_CLI::warning( "The 'meta_input' field was only introduced in WordPress 4.4 so will have no effect." ); } $array_arguments = [ 'meta_input' ]; $assoc_args = Utils\parse_shell_arrays( $assoc_args, $array_arguments ); if ( isset( $assoc_args['from-post'] ) ) { $post = $this->fetcher->get_check( $assoc_args['from-post'] ); $post_arr = get_object_vars( $post ); $post_id = $post_arr['ID']; unset( $post_arr['post_date'] ); unset( $post_arr['post_date_gmt'] ); unset( $post_arr['guid'] ); unset( $post_arr['ID'] ); if ( empty( $assoc_args['meta_input'] ) ) { $assoc_args['meta_input'] = $this->get_metadata( $post_id ); } if ( empty( $assoc_args['post_category'] ) ) { $post_arr['post_category'] = $this->get_category( $post_id ); } if ( empty( $assoc_args['tags_input'] ) ) { $post_arr['tags_input'] = $this->get_tags( $post_id ); } $assoc_args = array_merge( $post_arr, $assoc_args ); } $assoc_args = wp_slash( $assoc_args ); parent::_create( $args, $assoc_args, function ( $params ) { return wp_insert_post( $params, true ); } ); } /** * Updates one or more existing posts. * * ## OPTIONS * * <id>... * : One or more IDs of posts to update. * * [--post_author=<post_author>] * : The ID of the user who added the post. Default is the current user ID. * * [--post_date=<post_date>] * : The date of the post. Default is the current time. * * [--post_date_gmt=<post_date_gmt>] * : The date of the post in the GMT timezone. Default is the value of $post_date. * * [--post_content=<post_content>] * : The post content. Default empty. * * [--post_content_filtered=<post_content_filtered>] * : The filtered post content. Default empty. * * [--post_title=<post_title>] * : The post title. Default empty. * * [--post_excerpt=<post_excerpt>] * : The post excerpt. Default empty. * * [--post_status=<post_status>] * : The post status. Default 'draft'. * * [--post_type=<post_type>] * : The post type. Default 'post'. * * [--comment_status=<comment_status>] * : Whether the post can accept comments. Accepts 'open' or 'closed'. Default is the value of 'default_comment_status' option. * * [--ping_status=<ping_status>] * : Whether the post can accept pings. Accepts 'open' or 'closed'. Default is the value of 'default_ping_status' option. * * [--post_password=<post_password>] * : The password to access the post. Default empty. * * [--post_name=<post_name>] * : The post name. Default is the sanitized post title when creating a new post. * * [--to_ping=<to_ping>] * : Space or carriage return-separated list of URLs to ping. Default empty. * * [--pinged=<pinged>] * : Space or carriage return-separated list of URLs that have been pinged. Default empty. * * [--post_modified=<post_modified>] * : The date when the post was last modified. Default is the current time. * * [--post_modified_gmt=<post_modified_gmt>] * : The date when the post was last modified in the GMT timezone. Default is the current time. * * [--post_parent=<post_parent>] * : Set this for the post it belongs to, if any. Default 0. * * [--menu_order=<menu_order>] * : The order the post should be displayed in. Default 0. * * [--post_mime_type=<post_mime_type>] * : The mime type of the post. Default empty. * * [--guid=<guid>] * : Global Unique ID for referencing the post. Default empty. * * [--post_category=<post_category>] * : Array of category names, slugs, or IDs. Defaults to value of the 'default_category' option. * * [--tags_input=<tags_input>] * : Array of tag names, slugs, or IDs. Default empty. * * [--tax_input=<tax_input>] * : Array of taxonomy terms keyed by their taxonomy name. Default empty. * * [--meta_input=<meta_input>] * : Array in JSON format of post meta values keyed by their post meta key. Default empty. * * [<file>] * : Read post content from <file>. If this value is present, the * `--post_content` argument will be ignored. * * Passing `-` as the filename will cause post content to * be read from STDIN. * * --<field>=<value> * : One or more fields to update. See wp_insert_post(). * * [--defer-term-counting] * : Recalculate term count in batch, for a performance boost. * * ## EXAMPLES * * $ wp post update 123 --post_name=something --post_status=draft * Success: Updated post 123. * * # Update a post with multiple meta values. * $ wp post update 123 --meta_input='{"key1":"value1","key2":"value2"}' * Success: Updated post 123. * * # Update multiple posts at once. * $ wp post update 123 456 --post_author=789 * Success: Updated post 123. * Success: Updated post 456. * * # Update all posts of a given post type at once. * $ wp post update $(wp post list --post_type=page --format=ids) --post_author=123 * Success: Updated post 123. * Success: Updated post 456. */ public function update( $args, $assoc_args ) { foreach ( $args as $key => $arg ) { if ( is_numeric( $arg ) ) { continue; } $assoc_args['post_content'] = $this->read_from_file_or_stdin( $arg ); unset( $args[ $key ] ); break; } if ( isset( $assoc_args['post_category'] ) ) { $assoc_args['post_category'] = $this->get_category_ids( $assoc_args['post_category'] ); } if ( isset( $assoc_args['meta_input'] ) && Utils\wp_version_compare( '4.4', '<' ) ) { WP_CLI::warning( "The 'meta_input' field was only introduced in WordPress 4.4 so will have no effect." ); } $array_arguments = [ 'meta_input' ]; $assoc_args = Utils\parse_shell_arrays( $assoc_args, $array_arguments ); $assoc_args = wp_slash( $assoc_args ); parent::_update( $args, $assoc_args, function ( $params ) { return wp_update_post( $params, true ); } ); } /** * Launches system editor to edit post content. * * ## OPTIONS * * <id> * : The ID of the post to edit. * * ## EXAMPLES * * # Launch system editor to edit post * $ wp post edit 123 */ public function edit( $args, $assoc_args ) { $post = $this->fetcher->get_check( $args[0] ); $result = $this->_edit( $post->post_content, "WP-CLI post {$post->ID}" ); if ( false === $result ) { WP_CLI::warning( 'No change made to post content.', 'Aborted' ); } else { $this->update( $args, [ 'post_content' => $result ] ); } } // phpcs:ignore PSR2.Methods.MethodDeclaration.Underscore -- Whitelisting to provide backward compatibility to classes possibly extending this class. protected function _edit( $content, $title ) { // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- Calling native WordPress hook. $content = apply_filters( 'the_editor_content', $content ); $output = Utils\launch_editor_for_input( $content, $title ); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- Calling native WordPress hook. return ( is_string( $output ) ) ? apply_filters( 'content_save_pre', $output ) : $output; } /** * Gets details about a post. * * ## OPTIONS * * <id> * : The ID of the post to get. * * [--field=<field>] * : Instead of returning the whole post, returns the value of a single field. * * [--fields=<fields>] * : Limit the output to specific fields. Defaults to all fields. * * [--format=<format>] * : Render output in a particular format. * --- * default: table * options: * - table * - csv * - json * - yaml * --- * * ## EXAMPLES * * # Save the post content to a file * $ wp post get 123 --field=content > file.txt */ public function get( $args, $assoc_args ) { $post = $this->fetcher->get_check( $args[0] ); $post_arr = get_object_vars( $post ); unset( $post_arr['filter'] ); if ( empty( $assoc_args['fields'] ) ) { $assoc_args['fields'] = array_keys( $post_arr ); } $formatter = $this->get_formatter( $assoc_args ); $formatter->display_item( $post_arr ); } /** * Deletes an existing post. * * ## OPTIONS * * <id>... * : One or more IDs of posts to delete. * * [--force] * : Skip the trash bin. * * [--defer-term-counting] * : Recalculate term count in batch, for a performance boost. * * ## EXAMPLES * * # Delete post skipping trash * $ wp post delete 123 --force * Success: Deleted post 123. * * # Delete all pages * $ wp post delete $(wp post list --post_type='page' --format=ids) * Success: Trashed post 1164. * Success: Trashed post 1186. * * # Delete all posts in the trash * $ wp post delete $(wp post list --post_status=trash --format=ids) * Success: Deleted post 1268. * Success: Deleted post 1294. */ public function delete( $args, $assoc_args ) { $defaults = [ 'force' => false ]; $assoc_args = array_merge( $defaults, $assoc_args ); parent::_delete( $args, $assoc_args, [ $this, 'delete_callback' ] ); } /** * Callback used to delete a post. * * @param $post_id * @param $assoc_args * @return array */ protected function delete_callback( $post_id, $assoc_args ) { $status = get_post_status( $post_id ); $post_type = get_post_type( $post_id ); if ( ! $assoc_args['force'] && ( 'post' !== $post_type && 'page' !== $post_type ) ) { return [ 'error', "Posts of type '{$post_type}' do not support being sent to trash.\n" . 'Please use the --force flag to skip trash and delete them permanently.', ]; } if ( ! wp_delete_post( $post_id, $assoc_args['force'] ) ) { return [ 'error', "Failed deleting post {$post_id}." ]; } $action = $assoc_args['force'] || 'trash' === $status || 'revision' === $post_type ? 'Deleted' : 'Trashed'; return [ 'success', "{$action} post {$post_id}." ]; } /** * Gets a list of posts. * * Display posts based on all arguments supported by [WP_Query()](https://developer.wordpress.org/reference/classes/wp_query/). * Only shows post types marked as post by default. * * ## OPTIONS * * [--<field>=<value>] * : One or more args to pass to WP_Query. * * [--field=<field>] * : Prints the value of a single field for each post. * * [--fields=<fields>] * : Limit the output to specific object fields. * * [--format=<format>] * : Render output in a particular format. * --- * default: table * options: * - table * - csv * - ids * - json * - count * - yaml * --- * * ## AVAILABLE FIELDS * * These fields will be displayed by default for each post: * * * ID * * post_title * * post_name * * post_date * * post_status * * These fields are optionally available: * * * post_author * * post_date_gmt * * post_content * * post_excerpt * * comment_status * * ping_status * * post_password * * to_ping * * pinged * * post_modified * * post_modified_gmt * * post_content_filtered * * post_parent * * guid * * menu_order * * post_type * * post_mime_type * * comment_count * * filter * * url * * ## EXAMPLES * * # List post * $ wp post list --field=ID * 568 * 829 * 1329 * 1695 * * # List posts in JSON * $ wp post list --post_type=post --posts_per_page=5 --format=json * [{"ID":1,"post_title":"Hello world!","post_name":"hello-world","post_date":"2015-06-20 09:00:10","post_status":"publish"},{"ID":1178,"post_title":"Markup: HTML Tags and Formatting","post_name":"markup-html-tags-and-formatting","post_date":"2013-01-11 20:22:19","post_status":"draft"}] * * # List all pages * $ wp post list --post_type=page --fields=post_title,post_status * +-------------+-------------+ * | post_title | post_status | * +-------------+-------------+ * | Sample Page | publish | * +-------------+-------------+ * * # List ids of all pages and posts * $ wp post list --post_type=page,post --format=ids * 15 25 34 37 198 * * # List given posts * $ wp post list --post__in=1,3 * +----+--------------+-------------+---------------------+-------------+ * | ID | post_title | post_name | post_date | post_status | * +----+--------------+-------------+---------------------+-------------+ * | 3 | Lorem Ipsum | lorem-ipsum | 2016-06-01 14:34:36 | publish | * | 1 | Hello world! | hello-world | 2016-06-01 14:31:12 | publish | * +----+--------------+-------------+---------------------+-------------+ * * # List given post by a specific author * $ wp post list --author=2 * +----+-------------------+-------------------+---------------------+-------------+ * | ID | post_title | post_name | post_date | post_status | * +----+-------------------+-------------------+---------------------+-------------+ * | 14 | New documentation | new-documentation | 2021-06-18 21:05:11 | publish | * +----+-------------------+-------------------+---------------------+-------------+ * * @subcommand list */ public function list_( $args, $assoc_args ) { $formatter = $this->get_formatter( $assoc_args ); $defaults = [ 'posts_per_page' => -1, 'post_status' => 'any', ]; $query_args = array_merge( $defaults, $assoc_args ); $query_args = self::process_csv_arguments_to_arrays( $query_args ); if ( isset( $query_args['post_type'] ) && 'any' !== $query_args['post_type'] ) { $query_args['post_type'] = explode( ',', $query_args['post_type'] ); } if ( 'ids' === $formatter->format ) { $query_args['fields'] = 'ids'; $query = new WP_Query( $query_args ); echo implode( ' ', $query->posts ); } elseif ( 'count' === $formatter->format ) { $query_args['fields'] = 'ids'; $query = new WP_Query( $query_args ); $formatter->display_items( $query->posts ); } else { $query = new WP_Query( $query_args ); $posts = array_map( function( $post ) { $post->url = get_permalink( $post->ID ); return $post; }, $query->posts ); $formatter->display_items( $posts ); } } /** * Generates some posts. * * Creates a specified number of new posts with dummy data. * * ## OPTIONS * * [--count=<number>] * : How many posts to generate? * --- * default: 100 * --- * * [--post_type=<type>] * : The type of the generated posts. * --- * default: post * --- * * [--post_status=<status>] * : The status of the generated posts. * --- * default: publish * --- * * [--post_title=<post_title>] * : The post title. * --- * default: * --- * * [--post_author=<login>] * : The author of the generated posts. * --- * default: * --- * * [--post_date=<yyyy-mm-dd-hh-ii-ss>] * : The date of the generated posts. Default: current date * * [--post_date_gmt=<yyyy-mm-dd-hh-ii-ss>] * : The GMT date of the generated posts. Default: value of post_date (or current date if it's not set) * * [--post_content] * : If set, the command reads the post_content from STDIN. * * [--max_depth=<number>] * : For hierarchical post types, generate child posts down to a certain depth. * --- * default: 1 * --- * * [--format=<format>] * : Render output in a particular format. * --- * default: progress * options: * - progress * - ids * --- * * ## EXAMPLES * * # Generate posts. * $ wp post generate --count=10 --post_type=page --post_date=1999-01-04 * Generating posts 100% [================================================] 0:01 / 0:04 * * # Generate posts with fetched content. * $ curl -N http://loripsum.net/api/5 | wp post generate --post_content --count=10 * % Total % Received % Xferd Average Speed Time Time Time Current * Dload Upload Total Spent Left Speed * 100 2509 100 2509 0 0 616 0 0:00:04 0:00:04 --:--:-- 616 * Generating posts 100% [================================================] 0:01 / 0:04 * * # Add meta to every generated posts. * $ wp post generate --format=ids | xargs -d ' ' -I % wp post meta add % foo bar * Success: Added custom field. * Success: Added custom field. * Success: Added custom field. */ public function generate( $args, $assoc_args ) { global $wpdb; $defaults = [ 'count' => 100, 'max_depth' => 1, 'post_type' => 'post', 'post_status' => 'publish', 'post_author' => false, 'post_date' => false, 'post_date_gmt' => false, 'post_content' => '', 'post_title' => '', ]; $post_data = array_merge( $defaults, $assoc_args ); $call_time = current_time( 'mysql' ); if ( false === $post_data['post_date_gmt'] ) { $post_data['post_date_gmt'] = $post_data['post_date'] ?: $call_time; } if ( false === $post_data['post_date'] ) { $post_data['post_date'] = $post_data['post_date_gmt'] ?: $call_time; } if ( ! post_type_exists( $post_data['post_type'] ) ) { WP_CLI::error( "'{$post_data['post_type']}' is not a registered post type." ); } if ( $post_data['post_author'] ) { $user_fetcher = new UserFetcher(); $post_data['post_author'] = $user_fetcher->get_check( $post_data['post_author'] )->ID; } if ( Utils\get_flag_value( $assoc_args, 'post_content' ) ) { if ( ! EntityUtils::has_stdin() ) { WP_CLI::error( 'The parameter `post_content` reads from STDIN.' ); } $post_data['post_content'] = file_get_contents( 'php://stdin' ); } // Get the total number of posts. $total = $wpdb->get_var( $wpdb->prepare( "SELECT COUNT(*) FROM $wpdb->posts WHERE post_type = %s", $post_data['post_type'] ) ); $label = ! empty( $post_data['post_title'] ) ? $post_data['post_title'] : get_post_type_object( $post_data['post_type'] )->labels->singular_name; $hierarchical = get_post_type_object( $post_data['post_type'] )->hierarchical; $limit = $post_data['count'] + $total; $format = Utils\get_flag_value( $assoc_args, 'format', 'progress' ); $notify = false; if ( 'progress' === $format ) { $notify = Utils\make_progress_bar( 'Generating posts', $post_data['count'] ); } $previous_post_id = 0; $current_depth = 1; $current_parent = 0; for ( $index = $total; $index < $limit; $index++ ) { if ( $hierarchical ) { if ( $this->maybe_make_child() && $current_depth < $post_data['max_depth'] ) { $current_parent = $previous_post_id; $current_depth++; } elseif ( $this->maybe_reset_depth() ) { $current_depth = 1; $current_parent = 0; } } $args = [ 'post_type' => $post_data['post_type'], 'post_title' => ( ! empty( $post_data['post_title'] ) && $index === $total ) ? $label : "{$label} {$index}", 'post_status' => $post_data['post_status'], 'post_author' => $post_data['post_author'], 'post_parent' => $current_parent, 'post_name' => ! empty( $post_data['post_title'] ) ? sanitize_title( $post_data['post_title'] . ( $index === $total ? '' : "-{$index}" ) ) : "post-{$index}", 'post_date' => $post_data['post_date'], 'post_date_gmt' => $post_data['post_date_gmt'], 'post_content' => $post_data['post_content'], ]; $post_id = wp_insert_post( $args, true ); if ( is_wp_error( $post_id ) ) { WP_CLI::warning( $post_id ); } else { $previous_post_id = $post_id; if ( 'ids' === $format ) { echo $post_id; if ( $index < $limit - 1 ) { echo ' '; } } } if ( 'progress' === $format ) { $notify->tick(); } } if ( 'progress' === $format ) { $notify->finish(); } } private function maybe_make_child() { // 50% chance of making child post. return ( wp_rand( 1, 2 ) === 1 ); } private function maybe_reset_depth() { // 10% chance of reseting to root depth, return ( wp_rand( 1, 10 ) === 7 ); } /** * Read post content from file or STDIN * * @param string $arg Supplied argument * @return string */ private function read_from_file_or_stdin( $arg ) { if ( '-' !== $arg ) { $readfile = $arg; if ( ! file_exists( $readfile ) || ! is_file( $readfile ) ) { WP_CLI::error( "Unable to read content from '{$readfile}'." ); } } else { $readfile = 'php://stdin'; } return file_get_contents( $readfile ); } /** * Resolves post_category arg into an array of category ids. * * @param string $arg Supplied argument. * @return array */ private function get_category_ids( $arg ) { $categories = explode( ',', $arg ); $category_ids = []; foreach ( $categories as $post_category ) { if ( trim( $post_category ) ) { if ( is_numeric( $post_category ) && (int) $post_category ) { $category_id = category_exists( (int) $post_category ); } else { $category_id = category_exists( $post_category ); } if ( ! $category_id ) { WP_CLI::error( "No such post category '{$post_category}'." ); } $category_ids[] = $category_id; } } // If no category ids found, return exploded array for compat with previous WP-CLI versions. return $category_ids ?: $categories; } /** * Get post metadata. * * @param $post_id ID of the post. * * @return array */ private function get_metadata( $post_id ) { $metadata = get_metadata( 'post', $post_id ); $items = []; foreach ( $metadata as $key => $values ) { foreach ( $values as $item_value ) { $item_value = maybe_unserialize( $item_value ); $items[ $key ] = $item_value; } } return $items; } /** * Get Categories of a post. * * @param $post_id ID of the post. * * @return array */ private function get_category( $post_id ) { $category_data = get_the_category( $post_id ); $category_arr = []; foreach ( $category_data as $cat ) { array_push( $category_arr, $cat->term_id ); } return $category_arr; } /** * Get Tags of a post. * * @param $post_id ID of the post. * * @return array */ private function get_tags( $post_id ) { $tag_data = get_the_tags( $post_id ); $tag_arr = []; if ( $tag_data ) { foreach ( $tag_data as $tag ) { array_push( $tag_arr, $tag->slug ); } } return $tag_arr; } /** * Verifies whether a post exists. * * Displays a success message if the post does exist. * * ## OPTIONS * * <id> * : The ID of the post to check. * * ## EXAMPLES * * # The post exists. * $ wp post exists 1337 * Success: Post with ID 1337 exists. * $ echo $? * 0 * * # The post does not exist. * $ wp post exists 10000 * $ echo $? * 1 */ public function exists( $args ) { if ( $this->fetcher->get( $args[0] ) ) { WP_CLI::success( "Post with ID {$args[0]} exists." ); } else { WP_CLI::halt( 1 ); } } } <?php use WP_CLI\CommandWithMeta; use WP_CLI\Fetchers\Comment as CommentFetcher; /** * Adds, updates, deletes, and lists comment custom fields. * * ## EXAMPLES * * # Set comment meta * $ wp comment meta set 123 description "Mary is a WordPress developer." * Success: Updated custom field 'description'. * * # Get comment meta * $ wp comment meta get 123 description * Mary is a WordPress developer. * * # Update comment meta * $ wp comment meta update 123 description "Mary is an awesome WordPress developer." * Success: Updated custom field 'description'. * * # Delete comment meta * $ wp comment meta delete 123 description * Success: Deleted custom field. */ class Comment_Meta_Command extends CommandWithMeta { protected $meta_type = 'comment'; /** * Wrapper method for add_metadata that can be overridden in sub classes. * * @param int $object_id ID of the object the metadata is for. * @param string $meta_key Metadata key to use. * @param mixed $meta_value Metadata value. Must be serializable if * non-scalar. * @param bool $unique Optional, default is false. Whether the * specified metadata key should be unique for the * object. If true, and the object already has a * value for the specified metadata key, no change * will be made. * * @return int|false The meta ID on success, false on failure. */ protected function add_metadata( $object_id, $meta_key, $meta_value, $unique = false ) { return add_comment_meta( $object_id, $meta_key, $meta_value, $unique ); } /** * Wrapper method for update_metadata that can be overridden in sub classes. * * @param int $object_id ID of the object the metadata is for. * @param string $meta_key Metadata key to use. * @param mixed $meta_value Metadata value. Must be serializable if * non-scalar. * @param mixed $prev_value Optional. If specified, only update existing * metadata entries with the specified value. * Otherwise, update all entries. * * @return int|bool Meta ID if the key didn't exist, true on successful * update, false on failure. */ protected function update_metadata( $object_id, $meta_key, $meta_value, $prev_value = '' ) { return update_comment_meta( $object_id, $meta_key, $meta_value, $prev_value ); } /** * Wrapper method for get_metadata that can be overridden in sub classes. * * @param int $object_id ID of the object the metadata is for. * @param string $meta_key Optional. Metadata key. If not specified, * retrieve all metadata for the specified object. * @param bool $single Optional, default is false. If true, return only * the first value of the specified meta_key. This * parameter has no effect if meta_key is not * specified. * * @return mixed Single metadata value, or array of values. */ protected function get_metadata( $object_id, $meta_key = '', $single = false ) { return get_comment_meta( $object_id, $meta_key, $single ); } /** * Wrapper method for delete_metadata that can be overridden in sub classes. * * @param int $object_id ID of the object metadata is for * @param string $meta_key Metadata key * @param mixed $meta_value Optional. Metadata value. Must be serializable * if non-scalar. If specified, only delete * metadata entries with this value. Otherwise, * delete all entries with the specified meta_key. * Pass `null, `false`, or an empty string to skip * this check. For backward compatibility, it is * not possible to pass an empty string to delete * those entries with an empty string for a value. * * @return bool True on successful delete, false on failure. */ protected function delete_metadata( $object_id, $meta_key, $meta_value = '' ) { return delete_comment_meta( $object_id, $meta_key, $meta_value ); } /** * Check that the comment ID exists * * @param int */ protected function check_object_id( $object_id ) { $fetcher = new CommentFetcher(); $comment = $fetcher->get_check( $object_id ); return $comment->comment_ID; } } <?php use WP_CLI\CommandWithDBObject; use WP_CLI\Fetchers\Comment as CommentFetcher; use WP_CLI\Utils; /** * Creates, updates, deletes, and moderates comments. * * ## EXAMPLES * * # Create a new comment. * $ wp comment create --comment_post_ID=15 --comment_content="hello blog" --comment_author="wp-cli" * Success: Created comment 932. * * # Update an existing comment. * $ wp comment update 123 --comment_author='That Guy' * Success: Updated comment 123. * * # Delete an existing comment. * $ wp comment delete 1337 --force * Success: Deleted comment 1337. * * # Delete all spam comments. * $ wp comment delete $(wp comment list --status=spam --format=ids) * Success: Deleted comment 264. * Success: Deleted comment 262. * * @package wp-cli */ class Comment_Command extends CommandWithDBObject { protected $obj_type = 'comment'; protected $obj_id_key = 'comment_ID'; protected $obj_fields = [ 'comment_ID', 'comment_post_ID', 'comment_date', 'comment_approved', 'comment_author', 'comment_author_email', ]; public function __construct() { $this->fetcher = new CommentFetcher(); } /** * Creates a new comment. * * ## OPTIONS * * [--<field>=<value>] * : Associative args for the new comment. See wp_insert_comment(). * * [--porcelain] * : Output just the new comment id. * * ## EXAMPLES * * # Create comment. * $ wp comment create --comment_post_ID=15 --comment_content="hello blog" --comment_author="wp-cli" * Success: Created comment 932. */ public function create( $args, $assoc_args ) { $assoc_args = wp_slash( $assoc_args ); parent::_create( $args, $assoc_args, function ( $params ) { if ( isset( $params['comment_post_ID'] ) ) { $post_id = $params['comment_post_ID']; $post = get_post( $post_id ); if ( ! $post ) { return new WP_Error( 'no_post', "Can't find post {$post_id}." ); } } else { // Make sure it's set for older WP versions else get undefined PHP notice. $params['comment_post_ID'] = 0; } // We use wp_insert_comment() instead of wp_new_comment() to stay at a low level and // avoid wp_die() formatted messages or notifications $comment_id = wp_insert_comment( $params ); if ( ! $comment_id ) { return new WP_Error( 'db_error', 'Could not create comment.' ); } return $comment_id; } ); } /** * Updates one or more comments. * * ## OPTIONS * * <id>... * : One or more IDs of comments to update. * * --<field>=<value> * : One or more fields to update. See wp_update_comment(). * * ## EXAMPLES * * # Update comment. * $ wp comment update 123 --comment_author='That Guy' * Success: Updated comment 123. */ public function update( $args, $assoc_args ) { $assoc_args = wp_slash( $assoc_args ); parent::_update( $args, $assoc_args, function ( $params ) { if ( ! wp_update_comment( $params ) ) { return new WP_Error( 'Could not update comment.' ); } return true; } ); } /** * Generates some number of new dummy comments. * * Creates a specified number of new comments with dummy data. * * ## OPTIONS * * [--count=<number>] * : How many comments to generate? * --- * default: 100 * --- * * [--post_id=<post-id>] * : Assign comments to a specific post. * * [--format=<format>] * : Render output in a particular format. * --- * default: progress * options: * - progress * - ids * --- * * ## EXAMPLES * * # Generate comments for the given post. * $ wp comment generate --format=ids --count=3 --post_id=123 * 138 139 140 * * # Add meta to every generated comment. * $ wp comment generate --format=ids --count=3 | xargs -d ' ' -I % wp comment meta add % foo bar * Success: Added custom field. * Success: Added custom field. * Success: Added custom field. */ public function generate( $args, $assoc_args ) { $defaults = [ 'count' => 100, 'post_id' => 0, ]; $assoc_args = array_merge( $defaults, $assoc_args ); $format = Utils\get_flag_value( $assoc_args, 'format', 'progress' ); $notify = false; if ( 'progress' === $format ) { $notify = Utils\make_progress_bar( 'Generating comments', $assoc_args['count'] ); } $comment_count = wp_count_comments(); $total = (int) $comment_count->total_comments; $limit = $total + $assoc_args['count']; for ( $index = $total; $index < $limit; $index++ ) { $comment_id = wp_insert_comment( [ 'comment_content' => "Comment {$index}", 'comment_post_ID' => $assoc_args['post_id'], ] ); if ( 'progress' === $format ) { $notify->tick(); } elseif ( 'ids' === $format ) { echo $comment_id; if ( $index < $limit - 1 ) { echo ' '; } } } if ( 'progress' === $format ) { $notify->finish(); } } /** * Gets the data of a single comment. * * ## OPTIONS * * <id> * : The comment to get. * * [--field=<field>] * : Instead of returning the whole comment, returns the value of a single field. * * [--fields=<fields>] * : Limit the output to specific fields. Defaults to all fields. * * [--format=<format>] * : Render output in a particular format. * --- * default: table * options: * - table * - csv * - json * - yaml * --- * * ## EXAMPLES * * # Get comment. * $ wp comment get 21 --field=content * Thanks for all the comments, everyone! */ public function get( $args, $assoc_args ) { $comment_id = (int) $args[0]; $comment = get_comment( $comment_id ); if ( empty( $comment ) ) { WP_CLI::error( 'Invalid comment ID.' ); } if ( empty( $assoc_args['fields'] ) ) { $comment_array = get_object_vars( $comment ); $assoc_args['fields'] = array_keys( $comment_array ); } $formatter = $this->get_formatter( $assoc_args ); $formatter->display_item( $comment ); } /** * Gets a list of comments. * * Display comments based on all arguments supported by * [WP_Comment_Query()](https://developer.wordpress.org/reference/classes/WP_Comment_Query/__construct/). * * ## OPTIONS * * [--<field>=<value>] * : One or more args to pass to WP_Comment_Query. * * [--field=<field>] * : Prints the value of a single field for each comment. * * [--fields=<fields>] * : Limit the output to specific object fields. * * [--format=<format>] * : Render output in a particular format. * --- * default: table * options: * - table * - ids * - csv * - json * - count * - yaml * --- * * ## AVAILABLE FIELDS * * These fields will be displayed by default for each comment: * * * comment_ID * * comment_post_ID * * comment_date * * comment_approved * * comment_author * * comment_author_email * * These fields are optionally available: * * * comment_author_url * * comment_author_IP * * comment_date_gmt * * comment_content * * comment_karma * * comment_agent * * comment_type * * comment_parent * * user_id * * url * * ## EXAMPLES * * # List comment IDs. * $ wp comment list --field=ID * 22 * 23 * 24 * * # List comments of a post. * $ wp comment list --post_id=1 --fields=ID,comment_date,comment_author * +------------+---------------------+----------------+ * | comment_ID | comment_date | comment_author | * +------------+---------------------+----------------+ * | 1 | 2015-06-20 09:00:10 | Mr WordPress | * +------------+---------------------+----------------+ * * # List approved comments. * $ wp comment list --number=3 --status=approve --fields=ID,comment_date,comment_author * +------------+---------------------+----------------+ * | comment_ID | comment_date | comment_author | * +------------+---------------------+----------------+ * | 1 | 2015-06-20 09:00:10 | Mr WordPress | * | 30 | 2013-03-14 12:35:07 | John Doe | * | 29 | 2013-03-14 11:56:08 | Jane Doe | * +------------+---------------------+----------------+ * * @subcommand list */ public function list_( $args, $assoc_args ) { $formatter = $this->get_formatter( $assoc_args ); if ( 'ids' === $formatter->format ) { $assoc_args['fields'] = 'comment_ID'; } $assoc_args = self::process_csv_arguments_to_arrays( $assoc_args ); if ( 'count' === $formatter->format ) { $assoc_args['count'] = true; } if ( ! empty( $assoc_args['comment__in'] ) && ! empty( $assoc_args['orderby'] ) && 'comment__in' === $assoc_args['orderby'] && Utils\wp_version_compare( '4.4', '<' ) ) { $comments = []; foreach ( $assoc_args['comment__in'] as $comment_id ) { $comment = get_comment( $comment_id ); if ( $comment ) { $comments[] = $comment; } else { WP_CLI::warning( "Invalid comment {$comment_id}." ); } } } else { $query = new WP_Comment_Query(); $comments = $query->query( $assoc_args ); } if ( 'count' === $formatter->format ) { echo $comments; } else { if ( 'ids' === $formatter->format ) { $comments = wp_list_pluck( $comments, 'comment_ID' ); } elseif ( is_array( $comments ) ) { $comments = array_map( function( $comment ) { $comment->url = get_comment_link( $comment->comment_ID ); return $comment; }, $comments ); } $formatter->display_items( $comments ); } } /** * Deletes a comment. * * ## OPTIONS * * <id>... * : One or more IDs of comments to delete. * * [--force] * : Skip the trash bin. * * ## EXAMPLES * * # Delete comment. * $ wp comment delete 1337 --force * Success: Deleted comment 1337. * * # Delete multiple comments. * $ wp comment delete 1337 2341 --force * Success: Deleted comment 1337. * Success: Deleted comment 2341. */ public function delete( $args, $assoc_args ) { parent::_delete( $args, $assoc_args, function ( $comment_id, $assoc_args ) { $force = Utils\get_flag_value( $assoc_args, 'force' ); $status = wp_get_comment_status( $comment_id ); $result = wp_delete_comment( $comment_id, $force ); if ( ! $result ) { return [ 'error', "Failed deleting comment {$comment_id}." ]; } $verb = ( $force || 'trash' === $status ) ? 'Deleted' : 'Trashed'; return [ 'success', "{$verb} comment {$comment_id}." ]; } ); } private function call( $args, $status, $success, $failure ) { $comment_id = absint( $args ); $func = "wp_{$status}_comment"; if ( ! $func( $comment_id ) ) { WP_CLI::error( sprintf( $failure, "comment {$comment_id}" ) ); } WP_CLI::success( sprintf( $success, "comment {$comment_id}" ) ); } private function set_status( $args, $status, $success ) { $comment = $this->fetcher->get_check( $args ); $result = wp_set_comment_status( $comment->comment_ID, $status, true ); if ( is_wp_error( $result ) ) { WP_CLI::error( $result ); } WP_CLI::success( "{$success} comment {$comment->comment_ID}." ); } /** * Warns if `$_SERVER['SERVER_NAME']` not set as used in email from-address sent to post author in `wp_notify_postauthor()`. */ private function check_server_name() { if ( empty( $_SERVER['SERVER_NAME'] ) ) { WP_CLI::warning( 'Site url not set - defaulting to \'example.com\'. Any notification emails sent to post author may appear to come from \'example.com\'.' ); $_SERVER['SERVER_NAME'] = 'example.com'; } } /** * Trashes a comment. * * ## OPTIONS * * <id>... * : The IDs of the comments to trash. * * ## EXAMPLES * * # Trash comment. * $ wp comment trash 1337 * Success: Trashed comment 1337. */ public function trash( $args, $assoc_args ) { foreach ( $args as $id ) { $this->call( $id, __FUNCTION__, 'Trashed %s.', 'Failed trashing %s.' ); } } /** * Untrashes a comment. * * ## OPTIONS * * <id>... * : The IDs of the comments to untrash. * * ## EXAMPLES * * # Untrash comment. * $ wp comment untrash 1337 * Success: Untrashed comment 1337. */ public function untrash( $args, $assoc_args ) { $this->check_server_name(); foreach ( $args as $id ) { $this->call( $id, __FUNCTION__, 'Untrashed %s.', 'Failed untrashing %s.' ); } } /** * Marks a comment as spam. * * ## OPTIONS * * <id>... * : The IDs of the comments to mark as spam. * * ## EXAMPLES * * # Spam comment. * $ wp comment spam 1337 * Success: Marked as spam comment 1337. */ public function spam( $args, $assoc_args ) { foreach ( $args as $id ) { $this->call( $id, __FUNCTION__, 'Marked %s as spam.', 'Failed marking %s as spam.' ); } } /** * Unmarks a comment as spam. * * ## OPTIONS * * <id>... * : The IDs of the comments to unmark as spam. * * ## EXAMPLES * * # Unspam comment. * $ wp comment unspam 1337 * Success: Unspammed comment 1337. */ public function unspam( $args, $assoc_args ) { $this->check_server_name(); foreach ( $args as $id ) { $this->call( $id, __FUNCTION__, 'Unspammed %s.', 'Failed unspamming %s.' ); } } /** * Approves a comment. * * ## OPTIONS * * <id>... * : The IDs of the comments to approve. * * ## EXAMPLES * * # Approve comment. * $ wp comment approve 1337 * Success: Approved comment 1337. */ public function approve( $args, $assoc_args ) { $this->check_server_name(); foreach ( $args as $id ) { $this->set_status( $id, 'approve', 'Approved' ); } } /** * Unapproves a comment. * * ## OPTIONS * * <id>... * : The IDs of the comments to unapprove. * * ## EXAMPLES * * # Unapprove comment. * $ wp comment unapprove 1337 * Success: Unapproved comment 1337. */ public function unapprove( $args, $assoc_args ) { $this->check_server_name(); foreach ( $args as $id ) { $this->set_status( $id, 'hold', 'Unapproved' ); } } /** * Counts comments, on whole blog or on a given post. * * ## OPTIONS * * [<post-id>] * : The ID of the post to count comments in. * * ## EXAMPLES * * # Count comments on whole blog. * $ wp comment count * approved: 33 * spam: 3 * trash: 1 * post-trashed: 0 * all: 34 * moderated: 1 * total_comments: 37 * * # Count comments in a post. * $ wp comment count 42 * approved: 19 * spam: 0 * trash: 0 * post-trashed: 0 * all: 19 * moderated: 0 * total_comments: 19 */ public function count( $args, $assoc_args ) { $post_id = Utils\get_flag_value( $args, 0, 0 ); $count = wp_count_comments( $post_id ); // Move total_comments to the end of the object $total = $count->total_comments; unset( $count->total_comments ); $count->total_comments = $total; foreach ( $count as $status => $count ) { WP_CLI::line( str_pad( "$status:", 17 ) . $count ); } } /** * Recalculates the comment_count value for one or more posts. * * ## OPTIONS * * <id>... * : IDs for one or more posts to update. * * ## EXAMPLES * * # Recount comment for the post. * $ wp comment recount 123 * Updated post 123 comment count to 67. */ public function recount( $args ) { foreach ( $args as $id ) { wp_update_comment_count( $id ); $post = get_post( $id ); if ( $post ) { WP_CLI::log( "Updated post {$post->ID} comment count to {$post->comment_count}." ); } else { WP_CLI::warning( "Post {$post->ID} doesn't exist." ); } } } /** * Gets the status of a comment. * * ## OPTIONS * * <id> * : The ID of the comment to check. * * ## EXAMPLES * * # Get status of comment. * $ wp comment status 1337 * approved */ public function status( $args, $assoc_args ) { list( $comment_id ) = $args; $status = wp_get_comment_status( $comment_id ); if ( false === $status ) { WP_CLI::error( "Could not check status of comment {$comment_id}." ); } else { WP_CLI::line( $status ); } } /** * Verifies whether a comment exists. * * Displays a success message if the comment does exist. * * ## OPTIONS * * <id> * : The ID of the comment to check. * * ## EXAMPLES * * # Check whether comment exists. * $ wp comment exists 1337 * Success: Comment with ID 1337 exists. */ public function exists( $args ) { if ( $this->fetcher->get( $args[0] ) ) { WP_CLI::success( "Comment with ID {$args[0]} exists." ); } } } <?php use WP_CLI\CommandWithDBObject; use WP_CLI\Fetchers\Site as SiteFetcher; use WP_CLI\Iterators\Query as QueryIterator; use WP_CLI\Iterators\Table as TableIterator; use WP_CLI\Utils; use WP_CLI\Formatter; /** * Creates, deletes, empties, moderates, and lists one or more sites on a multisite installation. * * ## EXAMPLES * * # Create site * $ wp site create --slug=example * Success: Site 3 created: www.example.com/example/ * * # Output a simple list of site URLs * $ wp site list --field=url * http://www.example.com/ * http://www.example.com/subdir/ * * # Delete site * $ wp site delete 123 * Are you sure you want to delete the 'http://www.example.com/example' site? [y/n] y * Success: The site at 'http://www.example.com/example' was deleted. * * @package wp-cli */ class Site_Command extends CommandWithDBObject { protected $obj_type = 'site'; protected $obj_id_key = 'blog_id'; public function __construct() { $this->fetcher = new SiteFetcher(); } /** * Delete comments. */ private function empty_comments() { global $wpdb; // Empty comments and comment cache $comment_ids = $wpdb->get_col( "SELECT comment_ID FROM $wpdb->comments" ); foreach ( $comment_ids as $comment_id ) { wp_cache_delete( $comment_id, 'comment' ); wp_cache_delete( $comment_id, 'comment_meta' ); } $wpdb->query( "TRUNCATE $wpdb->comments" ); $wpdb->query( "TRUNCATE $wpdb->commentmeta" ); } /** * Delete all posts. */ private function empty_posts() { global $wpdb; // Empty posts and post cache $posts_query = "SELECT ID FROM $wpdb->posts"; $posts = new QueryIterator( $posts_query, 10000 ); $taxonomies = get_taxonomies(); while ( $posts->valid() ) { $post_id = $posts->current()->ID; wp_cache_delete( $post_id, 'posts' ); wp_cache_delete( $post_id, 'post_meta' ); foreach ( $taxonomies as $taxonomy ) { wp_cache_delete( $post_id, "{$taxonomy}_relationships" ); } wp_cache_delete( $wpdb->blogid . '-' . $post_id, 'global-posts' ); $posts->next(); } $wpdb->query( "TRUNCATE $wpdb->posts" ); $wpdb->query( "TRUNCATE $wpdb->postmeta" ); } /** * Delete terms, taxonomies, and tax relationships. */ private function empty_taxonomies() { global $wpdb; // Empty taxonomies and terms $terms = $wpdb->get_results( "SELECT term_id, taxonomy FROM $wpdb->term_taxonomy" ); $taxonomies = []; foreach ( (array) $terms as $term ) { $taxonomies[] = $term->taxonomy; wp_cache_delete( $term->term_id, $term->taxonomy ); } $taxonomies = array_unique( $taxonomies ); $cleaned = []; foreach ( $taxonomies as $taxonomy ) { if ( isset( $cleaned[ $taxonomy ] ) ) { continue; } $cleaned[ $taxonomy ] = true; wp_cache_delete( 'all_ids', $taxonomy ); wp_cache_delete( 'get', $taxonomy ); delete_option( "{$taxonomy}_children" ); } $wpdb->query( "TRUNCATE $wpdb->terms" ); $wpdb->query( "TRUNCATE $wpdb->term_taxonomy" ); $wpdb->query( "TRUNCATE $wpdb->term_relationships" ); if ( ! empty( $wpdb->termmeta ) ) { $wpdb->query( "TRUNCATE $wpdb->termmeta" ); } } /** * Delete all links, link_category terms, and related cache. */ private function empty_links() { global $wpdb; // Remove links and related cached data. $links_query = "SELECT link_id FROM {$wpdb->links}"; $links = new QueryIterator( $links_query, 10000 ); // Remove bookmarks cache group. wp_cache_delete( 'get_bookmarks', 'bookmark' ); while ( $links->valid() ) { $link_id = $links->current()->link_id; // Remove cache for the link. wp_delete_object_term_relationships( $link_id, 'link_category' ); wp_cache_delete( $link_id, 'bookmark' ); clean_object_term_cache( $link_id, 'link' ); $links->next(); } // Empty the table once link related cache and term is removed. $wpdb->query( "TRUNCATE {$wpdb->links}" ); } /** * Insert default terms. */ private function insert_default_terms() { global $wpdb; // Default category $cat_name = __( 'Uncategorized' ); /* translators: Default category slug */ $cat_slug = sanitize_title( _x( 'Uncategorized', 'Default category slug' ) ); if ( global_terms_enabled() ) { $cat_id = $wpdb->get_var( $wpdb->prepare( "SELECT cat_ID FROM {$wpdb->sitecategories} WHERE category_nicename = %s", $cat_slug ) ); if ( null === $cat_id ) { $wpdb->insert( $wpdb->sitecategories, [ 'cat_ID' => 0, 'cat_name' => $cat_name, 'category_nicename' => $cat_slug, 'last_updated' => current_time( 'mysql', true ), ] ); $cat_id = $wpdb->insert_id; } update_option( 'default_category', $cat_id ); } else { $cat_id = 1; } $wpdb->insert( $wpdb->terms, [ 'term_id' => $cat_id, 'name' => $cat_name, 'slug' => $cat_slug, 'term_group' => 0, ] ); $wpdb->insert( $wpdb->term_taxonomy, [ 'term_id' => $cat_id, 'taxonomy' => 'category', 'description' => '', 'parent' => 0, 'count' => 0, ] ); } /** * Reset option values to default. */ private function reset_options() { // Reset Privacy Policy value to prevent error. update_option( 'wp_page_for_privacy_policy', 0 ); // Reset sticky posts option. update_option( 'sticky_posts', [] ); } /** * Empties a site of its content (posts, comments, terms, and meta). * * Truncates posts, comments, and terms tables to empty a site of its * content. Doesn't affect site configuration (options) or users. * * If running a persistent object cache, make sure to flush the cache * after emptying the site, as the cache values will be invalid otherwise. * * To also empty custom database tables, you'll need to hook into command * execution: * * ``` * WP_CLI::add_hook( 'after_invoke:site empty', function(){ * global $wpdb; * foreach( array( 'p2p', 'p2pmeta' ) as $table ) { * $table = $wpdb->$table; * $wpdb->query( "TRUNCATE $table" ); * } * }); * ``` * * ## OPTIONS * * [--uploads] * : Also delete *all* files in the site's uploads directory. * * [--yes] * : Proceed to empty the site without a confirmation prompt. * * ## EXAMPLES * * $ wp site empty * Are you sure you want to empty the site at http://www.example.com of all posts, links, comments, and terms? [y/n] y * Success: The site at 'http://www.example.com' was emptied. * * @subcommand empty */ public function empty_( $args, $assoc_args ) { $upload_message = ''; if ( Utils\get_flag_value( $assoc_args, 'uploads' ) ) { $upload_message = ', and delete its uploads directory'; } WP_CLI::confirm( "Are you sure you want to empty the site at '" . site_url() . "' of all posts, links, comments, and terms" . $upload_message . '?', $assoc_args ); $this->empty_posts(); $this->empty_links(); $this->empty_comments(); $this->empty_taxonomies(); $this->insert_default_terms(); $this->reset_options(); if ( ! empty( $upload_message ) ) { $upload_dir = wp_upload_dir(); $files = new RecursiveIteratorIterator( new RecursiveDirectoryIterator( $upload_dir['basedir'], RecursiveDirectoryIterator::SKIP_DOTS ), RecursiveIteratorIterator::CHILD_FIRST ); $files_to_unlink = []; $directories_to_delete = []; $is_main_site = is_main_site(); foreach ( $files as $fileinfo ) { $realpath = $fileinfo->getRealPath(); // Don't clobber subsites when operating on the main site if ( $is_main_site && false !== stripos( $realpath, '/sites/' ) ) { continue; } if ( $fileinfo->isDir() ) { $directories_to_delete[] = $realpath; } else { $files_to_unlink[] = $realpath; } } foreach ( $files_to_unlink as $file ) { unlink( $file ); } foreach ( $directories_to_delete as $directory ) { // Directory could be main sites directory '/sites' which may be non-empty. @rmdir( $directory ); } // May be non-empty if '/sites' still around. @rmdir( $upload_dir['basedir'] ); } WP_CLI::success( "The site at '" . site_url() . "' was emptied." ); } /** * Deletes a site in a multisite installation. * * ## OPTIONS * * [<site-id>] * : The id of the site to delete. If not provided, you must set the --slug parameter. * * [--slug=<slug>] * : Path of the blog to be deleted. Subdomain on subdomain installs, directory on subdirectory installs. * * [--yes] * : Answer yes to the confirmation message. * * [--keep-tables] * : Delete the blog from the list, but don't drop it's tables. * * ## EXAMPLES * * $ wp site delete 123 * Are you sure you want to delete the http://www.example.com/example site? [y/n] y * Success: The site at 'http://www.example.com/example' was deleted. */ public function delete( $args, $assoc_args ) { if ( ! is_multisite() ) { WP_CLI::error( 'This is not a multisite installation.' ); } if ( isset( $assoc_args['slug'] ) ) { $blog = get_blog_details( trim( $assoc_args['slug'], '/' ) ); } else { if ( empty( $args ) ) { WP_CLI::error( 'Need to specify a blog id.' ); } $blog_id = $args[0]; if ( is_main_site( $blog_id ) ) { WP_CLI::error( 'You cannot delete the root site.' ); } $blog = get_blog_details( $blog_id ); } if ( ! $blog ) { WP_CLI::error( 'Site not found.' ); } $site_url = trailingslashit( $blog->siteurl ); WP_CLI::confirm( "Are you sure you want to delete the '{$site_url}' site?", $assoc_args ); wpmu_delete_blog( $blog->blog_id, ! Utils\get_flag_value( $assoc_args, 'keep-tables' ) ); WP_CLI::success( "The site at '{$site_url}' was deleted." ); } /** * Creates a site in a multisite installation. * * ## OPTIONS * * --slug=<slug> * : Path for the new site. Subdomain on subdomain installs, directory on subdirectory installs. * * [--title=<title>] * : Title of the new site. Default: prettified slug. * * [--email=<email>] * : Email for admin user. User will be created if none exists. Assignment to super admin if not included. * * [--network_id=<network-id>] * : Network to associate new site with. Defaults to current network (typically 1). * * [--private] * : If set, the new site will be non-public (not indexed) * * [--porcelain] * : If set, only the site id will be output on success. * * ## EXAMPLES * * $ wp site create --slug=example * Success: Site 3 created: http://www.example.com/example/ */ public function create( $args, $assoc_args ) { if ( ! is_multisite() ) { WP_CLI::error( 'This is not a multisite installation.' ); } global $wpdb, $current_site; $base = $assoc_args['slug']; $title = Utils\get_flag_value( $assoc_args, 'title', ucfirst( $base ) ); $email = empty( $assoc_args['email'] ) ? '' : $assoc_args['email']; // Network if ( ! empty( $assoc_args['network_id'] ) ) { $network = $this->get_network( $assoc_args['network_id'] ); if ( false === $network ) { WP_CLI::error( "Network with id {$assoc_args['network_id']} does not exist." ); } } else { $network = $current_site; } $public = ! Utils\get_flag_value( $assoc_args, 'private' ); // Sanitize if ( preg_match( '|^([a-zA-Z0-9-])+$|', $base ) ) { $base = strtolower( $base ); } // If not a subdomain install, make sure the domain isn't a reserved word if ( ! is_subdomain_install() ) { // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- Calling WordPress native hook. $subdirectory_reserved_names = apply_filters( 'subdirectory_reserved_names', [ 'page', 'comments', 'blog', 'files', 'feed' ] ); if ( in_array( $base, $subdirectory_reserved_names, true ) ) { WP_CLI::error( 'The following words are reserved and cannot be used as blog names: ' . implode( ', ', $subdirectory_reserved_names ) ); } } // Check for valid email, if not, use the first super admin found // Probably a more efficient way to do this so we dont query for the // User twice if super admin $email = sanitize_email( $email ); if ( empty( $email ) || ! is_email( $email ) ) { $super_admins = get_super_admins(); $email = ''; if ( ! empty( $super_admins ) && is_array( $super_admins ) ) { // Just get the first one $super_login = $super_admins[0]; $super_user = get_user_by( 'login', $super_login ); if ( $super_user ) { $email = $super_user->user_email; } } } if ( is_subdomain_install() ) { $newdomain = $base . '.' . preg_replace( '|^www\.|', '', $current_site->domain ); $path = $current_site->path; } else { $newdomain = $current_site->domain; $path = $current_site->path . $base . '/'; } $user_id = email_exists( $email ); if ( ! $user_id ) { // Create a new user with a random password $password = wp_generate_password( 12, false ); $user_id = wpmu_create_user( $base, $password, $email ); if ( false === $user_id ) { WP_CLI::error( "Can't create user." ); } else { User_Command::wp_new_user_notification( $user_id, $password ); } } $wpdb->hide_errors(); $title = wp_slash( $title ); $id = wpmu_create_blog( $newdomain, $path, $title, $user_id, [ 'public' => $public ], $network->id ); $wpdb->show_errors(); if ( ! is_wp_error( $id ) ) { if ( ! is_super_admin( $user_id ) && ! get_user_option( 'primary_blog', $user_id ) ) { update_user_option( $user_id, 'primary_blog', $id, true ); } } else { WP_CLI::error( $id->get_error_message() ); } if ( Utils\get_flag_value( $assoc_args, 'porcelain' ) ) { WP_CLI::line( $id ); } else { $site_url = trailingslashit( get_site_url( $id ) ); WP_CLI::success( "Site {$id} created: {$site_url}" ); } } /** * Gets network data for a given id. * * @param int $network_id * @return bool|array False if no network found with given id, array otherwise */ private function get_network( $network_id ) { global $wpdb; // Load network data $networks = $wpdb->get_results( $wpdb->prepare( "SELECT * FROM $wpdb->site WHERE id = %d", $network_id ) ); if ( ! empty( $networks ) ) { // Only care about domain and path which are set here return $networks[0]; } return false; } /** * Lists all sites in a multisite installation. * * ## OPTIONS * * [--network=<id>] * : The network to which the sites belong. * * [--<field>=<value>] * : Filter by one or more fields (see "Available Fields" section). However, * 'url' isn't an available filter, because it's created from domain + path. * * [--site__in=<value>] * : Only list the sites with these blog_id values (comma-separated). * * [--field=<field>] * : Prints the value of a single field for each site. * * [--fields=<fields>] * : Comma-separated list of fields to show. * * [--format=<format>] * : Render output in a particular format. * --- * default: table * options: * - table * - csv * - count * - ids * - json * - yaml * --- * * ## AVAILABLE FIELDS * * These fields will be displayed by default for each site: * * * blog_id * * url * * last_updated * * registered * * These fields are optionally available: * * * site_id * * domain * * path * * public * * archived * * mature * * spam * * deleted * * lang_id * * ## EXAMPLES * * # Output a simple list of site URLs * $ wp site list --field=url * http://www.example.com/ * http://www.example.com/subdir/ * * @subcommand list */ public function list_( $args, $assoc_args ) { if ( ! is_multisite() ) { WP_CLI::error( 'This is not a multisite installation.' ); } global $wpdb; if ( isset( $assoc_args['fields'] ) ) { $assoc_args['fields'] = preg_split( '/,[ \t]*/', $assoc_args['fields'] ); } $defaults = [ 'format' => 'table', 'fields' => [ 'blog_id', 'url', 'last_updated', 'registered' ], ]; $assoc_args = array_merge( $defaults, $assoc_args ); $where = []; $append = ''; $site_cols = [ 'blog_id', 'last_updated', 'registered', 'site_id', 'domain', 'path', 'public', 'archived', 'mature', 'spam', 'deleted', 'lang_id' ]; foreach ( $site_cols as $col ) { if ( isset( $assoc_args[ $col ] ) ) { $where[ $col ] = $assoc_args[ $col ]; } } if ( isset( $assoc_args['site__in'] ) ) { $where['blog_id'] = explode( ',', $assoc_args['site__in'] ); $append = 'ORDER BY FIELD( blog_id, ' . implode( ',', array_map( 'intval', $where['blog_id'] ) ) . ' )'; } if ( isset( $assoc_args['network'] ) ) { $where['site_id'] = $assoc_args['network']; } $iterator_args = [ 'table' => $wpdb->blogs, 'where' => $where, 'append' => $append, ]; $iterator = new TableIterator( $iterator_args ); $iterator = Utils\iterator_map( $iterator, function( $blog ) { $blog->url = trailingslashit( get_home_url( $blog->blog_id ) ); return $blog; } ); if ( ! empty( $assoc_args['format'] ) && 'ids' === $assoc_args['format'] ) { $sites = iterator_to_array( $iterator ); $ids = wp_list_pluck( $sites, 'blog_id' ); $formatter = new Formatter( $assoc_args, null, 'site' ); $formatter->display_items( $ids ); } else { $formatter = new Formatter( $assoc_args, null, 'site' ); $formatter->display_items( $iterator ); } } /** * Archives one or more sites. * * ## OPTIONS * * <id>... * : One or more IDs of sites to archive. * * ## EXAMPLES * * $ wp site archive 123 * Success: Site 123 archived. */ public function archive( $args ) { $this->update_site_status( $args, 'archived', 1 ); } /** * Unarchives one or more sites. * * ## OPTIONS * * <id>... * : One or more IDs of sites to unarchive. * * ## EXAMPLES * * $ wp site unarchive 123 * Success: Site 123 unarchived. */ public function unarchive( $args ) { $this->update_site_status( $args, 'archived', 0 ); } /** * Activates one or more sites. * * ## OPTIONS * * <id>... * : One or more IDs of sites to activate. * * ## EXAMPLES * * $ wp site activate 123 * Success: Site 123 activated. */ public function activate( $args ) { $this->update_site_status( $args, 'deleted', 0 ); } /** * Deactivates one or more sites. * * ## OPTIONS * * <id>... * : One or more IDs of sites to deactivate. * * ## EXAMPLES * * $ wp site deactivate 123 * Success: Site 123 deactivated. */ public function deactivate( $args ) { $this->update_site_status( $args, 'deleted', 1 ); } /** * Marks one or more sites as spam. * * ## OPTIONS * * <id>... * : One or more IDs of sites to be marked as spam. * * ## EXAMPLES * * $ wp site spam 123 * Success: Site 123 marked as spam. */ public function spam( $args ) { $this->update_site_status( $args, 'spam', 1 ); } /** * Removes one or more sites from spam. * * ## OPTIONS * * <id>... * : One or more IDs of sites to remove from spam. * * ## EXAMPLES * * $ wp site unspam 123 * Success: Site 123 removed from spam. * * @subcommand unspam */ public function unspam( $args ) { $this->update_site_status( $args, 'spam', 0 ); } /** * Sets one or more sites as mature. * * ## OPTIONS * * <id>... * : One or more IDs of sites to set as mature. * * ## EXAMPLES * * $ wp site mature 123 * Success: Site 123 marked as mature. */ public function mature( $args ) { $this->update_site_status( $args, 'mature', 1 ); } /** * Sets one or more sites as immature. * * ## OPTIONS * * <id>... * : One or more IDs of sites to set as unmature. * * ## EXAMPLES * * $ wp site general 123 * Success: Site 123 marked as unmature. */ public function unmature( $args ) { $this->update_site_status( $args, 'mature', 0 ); } /** * Sets one or more sites as public. * * ## OPTIONS * * <id>... * : One or more IDs of sites to set as public. * * ## EXAMPLES * * $ wp site public 123 * Success: Site 123 marked as public. * * @subcommand public */ public function set_public( $args ) { $this->update_site_status( $args, 'public', 1 ); } /** * Sets one or more sites as private. * * ## OPTIONS * * <id>... * : One or more IDs of sites to set as private. * * ## EXAMPLES * * $ wp site private 123 * Success: Site 123 marked as private. * * @subcommand private */ public function set_private( $args ) { $this->update_site_status( $args, 'public', 0 ); } private function update_site_status( $ids, $pref, $value ) { $value = (int) $value; switch ( $pref ) { case 'archived': $action = $value ? 'archived' : 'unarchived'; break; case 'deleted': $action = $value ? 'deactivated' : 'activated'; break; case 'mature': $action = $value ? 'marked as mature' : 'marked as unmature'; break; case 'public': $action = $value ? 'marked as public' : 'marked as private'; break; case 'spam': $action = $value ? 'marked as spam' : 'removed from spam'; break; } foreach ( $ids as $site_id ) { $site = $this->fetcher->get_check( $site_id ); if ( is_main_site( $site->blog_id ) ) { WP_CLI::warning( 'You are not allowed to change the main site.' ); continue; } $old_value = (int) get_blog_status( $site->blog_id, $pref ); if ( $value === $old_value ) { WP_CLI::warning( "Site {$site->blog_id} already {$action}." ); continue; } update_blog_status( $site->blog_id, $pref, $value ); WP_CLI::success( "Site {$site->blog_id} {$action}." ); } } } <?php use WP_CLI\ExitException; use WP_CLI\Fetchers\User as UserFetcher; use WP_CLI\Formatter; use WP_CLI\Utils; /** * Creates, updates, deletes, lists and retrieves application passwords. * * ## EXAMPLES * * # List user application passwords and only show app name and password hash * $ wp user application-passwords list 123 --fields=name,password * +--------+------------------------------------+ * | name | password | * +--------+------------------------------------+ * | myapp | $P$BVGeou1CUot114YohIemgpwxQCzb8O/ | * +--------+------------------------------------+ * * # Get a specific application password and only show app name and created timestamp * $ wp user application-passwords get 123 6633824d-c1d7-4f79-9dd5-4586f734d69e --fields=name,created * +--------+------------+ * | name | created | * +--------+------------+ * | myapp | 1638395611 | * +--------+------------+ * * # Create user application password * $ wp user application-passwords create 123 myapp * Success: Created application password. * Password: ZG1bxdxdzjTwhsY8vK8l1C65 * * # Only print the password without any chrome * $ wp user application-passwords create 123 myapp --porcelain * ZG1bxdxdzjTwhsY8vK8l1C65 * * # Update an existing application password * $ wp user application-passwords update 123 6633824d-c1d7-4f79-9dd5-4586f734d69e --name=newappname * Success: Updated application password. * * # Check if an application password for a given application exists * $ wp user application-passwords exists 123 myapp * $ echo $? * 1 * * # Bash script for checking whether an application password exists and creating one if not * if ! wp user application-password exists 123 myapp; then * PASSWORD=$(wp user application-password create 123 myapp --porcelain) * fi */ final class User_Application_Password_Command { /** * List of application password fields. * * @var array<string> */ const APPLICATION_PASSWORD_FIELDS = [ 'uuid', 'app_id', 'name', 'password', 'created', 'last_used', 'last_ip', ]; /** * Lists all application passwords associated with a user. * * ## OPTIONS * * <user> * : The user login, user email, or user ID of the user to get application passwords for. * * [--<field>=<value>] * : Filter the list by a specific field. * * [--field=<field>] * : Prints the value of a single field for each application password. * * [--fields=<fields>] * : Limit the output to specific fields. * * [--format=<format>] * : Render output in a particular format. * --- * default: table * options: * - table * - csv * - json * - count * - yaml * --- * * [--orderby=<fields>] * : Set orderby which field. * --- * default: created * options: * - uuid * - app_id * - name * - password * - created * - last_used * - last_ip * --- * * [--order=<order>] * : Set ascending or descending order. * --- * default: desc * options: * - asc * - desc * --- * * ## EXAMPLES * * # List user application passwords and only show app name and password hash * $ wp user application-passwords list 123 --fields=name,password * +--------+------------------------------------+ * | name | password | * +--------+------------------------------------+ * | myapp | $P$BVGeou1CUot114YohIemgpwxQCzb8O/ | * +--------+------------------------------------+ * * @subcommand list * * @param array $args Indexed array of positional arguments. * @param array $assoc_args Associative array of associative arguments. * @throws ExitException If the user could not be found/parsed. * @throws ExitException If the application passwords could not be retrieved. */ public function list_( $args, $assoc_args ) { $args = $this->replace_login_with_user_id( $args ); list( $user_id ) = $args; $application_passwords = WP_Application_Passwords::get_user_application_passwords( $user_id ); if ( $application_passwords instanceof WP_Error ) { WP_CLI::error( $application_passwords ); } if ( empty( $application_passwords ) ) { $application_passwords = []; } $order = Utils\get_flag_value( $assoc_args, 'order' ); $orderby = Utils\get_flag_value( $assoc_args, 'orderby' ); usort( $application_passwords, static function ( $a, $b ) use ( $orderby, $order ) { // Sort array. return 'asc' === $order ? $a[ $orderby ] > $b[ $orderby ] : $a[ $orderby ] < $b[ $orderby ]; } ); $fields = self::APPLICATION_PASSWORD_FIELDS; // Avoid confusion regarding the dash/underscore usage. foreach ( [ 'app-id', 'last-used', 'last-ip' ] as $flag ) { if ( array_key_exists( $flag, $assoc_args ) ) { $underscored_flag = str_replace( '-', '_', $flag ); $assoc_args[ $underscored_flag ] = $assoc_args[ $flag ]; unset( $assoc_args[ $flag ] ); } } foreach ( $fields as $field ) { if ( ! array_key_exists( $field, $assoc_args ) ) { continue; } $value = Utils\get_flag_value( $assoc_args, $field ); $application_passwords = array_filter( $application_passwords, static function ( $application_password ) use ( $field, $value ) { return $application_password[ $field ] === $value; } ); } if ( ! empty( $assoc_args['fields'] ) ) { $fields = explode( ',', $assoc_args['fields'] ); } $formatter = new Formatter( $assoc_args, $fields ); $formatter->display_items( $application_passwords ); } /** * Gets a specific application password. * * ## OPTIONS * * <user> * : The user login, user email, or user ID of the user to get the application password for. * * <uuid> * : The universally unique ID of the application password. * * [--field=<field>] * : Prints the value of a single field for the application password. * * [--fields=<fields>] * : Limit the output to specific fields. * * [--format=<format>] * : Render output in a particular format. * --- * default: table * options: * - table * - csv * - json * - yaml * --- * * ## EXAMPLES * * # Get a specific application password and only show app name and created timestamp * $ wp user application-passwords get 123 6633824d-c1d7-4f79-9dd5-4586f734d69e --fields=name,created * +--------+------------+ * | name | created | * +--------+------------+ * | myapp | 1638395611 | * +--------+------------+ * * @param array $args Indexed array of positional arguments. * @param array $assoc_args Associative array of associative arguments. * @throws ExitException If the application passwords could not be retrieved. */ public function get( $args, $assoc_args ) { $args = $this->replace_login_with_user_id( $args ); list( $user_id, $uuid ) = $args; $application_password = WP_Application_Passwords::get_user_application_password( $user_id, $uuid ); if ( $application_password instanceof WP_Error ) { WP_CLI::error( $application_password ); } if ( null === $application_password ) { WP_CLI::error( 'No application password found for this user ID and UUID.' ); } $fields = self::APPLICATION_PASSWORD_FIELDS; if ( ! empty( $assoc_args['fields'] ) ) { $fields = explode( ',', $assoc_args['fields'] ); } $formatter = new Formatter( $assoc_args, $fields ); $formatter->display_items( [ $application_password ] ); } /** * Creates a new application password. * * ## OPTIONS * * <user> * : The user login, user email, or user ID of the user to create a new application password for. * * <app-name> * : Unique name of the application to create an application password for. * * [--app-id=<app-id>] * : Application ID to attribute to the application password. * * [--porcelain] * : Output just the new password. * * ## EXAMPLES * * # Create user application password * $ wp user application-passwords create 123 myapp * Success: Created application password. * Password: ZG1bxdxdzjTwhsY8vK8l1C65 * * # Only print the password without any chrome * $ wp user application-passwords create 123 myapp --porcelain * ZG1bxdxdzjTwhsY8vK8l1C65 * * # Create user application with a custom application ID for internal tracking * $ wp user application-passwords create 123 myapp --app-id=42 --porcelain * ZG1bxdxdzjTwhsY8vK8l1C65 * * @param array $args Indexed array of positional arguments. * @param array $assoc_args Associative array of associative arguments. * @throws ExitException If the application password could not be created. */ public function create( $args, $assoc_args ) { $args = $this->replace_login_with_user_id( $args ); list( $user_id, $app_name ) = $args; $app_id = Utils\get_flag_value( $assoc_args, 'app-id', '' ); $arguments = [ 'name' => $app_name, 'app_id' => $app_id, ]; $result = WP_Application_Passwords::create_new_application_password( $user_id, $arguments ); if ( $result instanceof WP_Error ) { WP_CLI::error( $result ); } if ( Utils\get_flag_value( $assoc_args, 'porcelain', false ) ) { WP_CLI::line( $result[0] ); WP_CLI::halt( 0 ); } WP_CLI::success( 'Created application password.' ); WP_CLI::line( "Password: {$result[0]}" ); } /** * Updates an existing application password. * * ## OPTIONS * * <user> * : The user login, user email, or user ID of the user to update the application password for. * * <uuid> * : The universally unique ID of the application password. * * [--<field>=<value>] * : Update the <field> with a new <value>. Currently supported fields: name. * * ## EXAMPLES * * # Update an existing application password * $ wp user application-passwords update 123 6633824d-c1d7-4f79-9dd5-4586f734d69e --name=newappname * Success: Updated application password. * * @param array $args Indexed array of positional arguments. * @param array $assoc_args Associative array of associative arguments. * @throws ExitException If the application password could not be created. */ public function update( $args, $assoc_args ) { $args = $this->replace_login_with_user_id( $args ); list( $user_id, $uuid ) = $args; $result = WP_Application_Passwords::update_application_password( $user_id, $uuid, $assoc_args ); if ( $result instanceof WP_Error ) { WP_CLI::error( $result ); } WP_CLI::success( 'Updated application password.' ); } /** * Record usage of an application password. * * ## OPTIONS * * <user> * : The user login, user email, or user ID of the user to update the application password for. * * <uuid> * : The universally unique ID of the application password. * * ## EXAMPLES * * # Record usage of an application password * $ wp user application-passwords record-usage 123 6633824d-c1d7-4f79-9dd5-4586f734d69e * Success: Recorded application password usage. * * @subcommand record-usage * * @param array $args Indexed array of positional arguments. * @param array $assoc_args Associative array of associative arguments. * @throws ExitException If the application password could not be created. */ public function record_usage( $args, $assoc_args ) { $args = $this->replace_login_with_user_id( $args ); list( $user_id, $uuid ) = $args; $result = WP_Application_Passwords::record_application_password_usage( $user_id, $uuid ); if ( $result instanceof WP_Error ) { WP_CLI::error( $result ); } WP_CLI::success( 'Recorded application password usage.' ); } /** * Delete an existing application password. * * ## OPTIONS * * <user> * : The user login, user email, or user ID of the user to delete the application password for. * * [<uuid>...] * : Comma-separated list of UUIDs of the application passwords to delete. * * [--all] * : Delete all of the user's application password. * * ## EXAMPLES * * # Record usage of an application password * $ wp user application-passwords record-usage 123 6633824d-c1d7-4f79-9dd5-4586f734d69e * Success: Recorded application password usage. * * @param array $args Indexed array of positional arguments. * @param array $assoc_args Associative array of associative arguments. * @throws ExitException If the application password could not be created. */ public function delete( $args, $assoc_args ) { $args = $this->replace_login_with_user_id( $args ); $user_id = array_shift( $args ); $all = Utils\get_flag_value( $assoc_args, 'all', false ); $count = count( $args ); if ( ( 0 < $count && true === $all ) || ( 0 === $count && true !== $all ) ) { WP_CLI::error( 'You need to specify either one or more UUIDS or provide the --all flag' ); } if ( true === $all ) { $result = WP_Application_Passwords::delete_all_application_passwords( $user_id ); if ( $result instanceof WP_Error ) { WP_CLI::error( $result ); } WP_CLI::success( 'Deleted all application passwords.' ); WP_CLI::halt( 0 ); } $errors = 0; foreach ( $args as $uuid ) { $result = WP_Application_Passwords::delete_application_password( $user_id, $uuid ); if ( $result instanceof WP_Error ) { WP_CLI::warning( "Failed to delete UUID {$uuid}: " . $result->get_error_message() ); $errors++; } } WP_CLI::success( sprintf( 'Deleted %d of %d application %s.', $count - $errors, $count, Utils\pluralize( 'password', $count ) ) ); WP_CLI::halt( 0 === $errors ? 0 : 1 ); } /** * Checks whether an application password for a given application exists. * * ## OPTIONS * * <user> * : The user login, user email, or user ID of the user to check the existence of an application password for. * * <app-name> * : Name of the application to check the existence of an application password for. * * ## EXAMPLES * * # Check if an application password for a given application exists * $ wp user application-passwords exists 123 myapp * $ echo $? * 1 * * # Bash script for checking whether an application password exists and creating one if not * if ! wp user application-password exists 123 myapp; then * PASSWORD=$(wp user application-password create 123 myapp --porcelain) * fi * * @param array $args Indexed array of positional arguments. * @param array $assoc_args Associative array of associative arguments. * @throws ExitException If the application password could not be created. */ public function exists( $args, $assoc_args ) { $args = $this->replace_login_with_user_id( $args ); list( $user_id, $app_name ) = $args; $result = $this->application_name_exists_for_user( $user_id, $app_name ); if ( $result instanceof WP_Error ) { WP_CLI::error( $result ); } WP_CLI::halt( $result ? 0 : 1 ); } /** * Replaces user_login value with user ID. * * @param array $args Associative array of arguments. * @return array Associative array of arguments with the user login replaced with an ID. * @throws ExitException If the user is not found. */ private function replace_login_with_user_id( $args ) { $user = ( new UserFetcher() )->get_check( $args[0] ); $args[0] = $user->ID; return $args; } /** * Checks if application name exists for the given user. * * This is a polyfill for WP_Application_Passwords::get_user_application_passwords(), which was only added for * WordPress 5.7+, but we're already supporting application passwords for WordPress 5.6+. * * @param int $user_id User ID to check the application passwords for. * @param string $app_name Application name to look for. * @return bool */ private function application_name_exists_for_user( $user_id, $app_name ) { if ( Utils\wp_version_compare( '5.7', '<' ) ) { $passwords = WP_Application_Passwords::get_user_application_passwords( $user_id ); foreach ( $passwords as $password ) { if ( strtolower( $password['name'] ) === strtolower( $name ) ) { return true; } } return false; } return WP_Application_Passwords::application_name_exists_for_user( $user_id, $app_name ); } } <?php use WP_CLI\CommandWithTerms; use WP_CLI\Fetchers\Post as PostFetcher; /** * Adds, updates, removes, and lists post terms. * * ## EXAMPLES * * # Set post terms * $ wp post term set 123 test category * Success: Set terms. */ class Post_Term_Command extends CommandWithTerms { protected $obj_type = 'post'; public function __construct() { $this->fetcher = new PostFetcher(); } protected function get_object_type() { $post = $this->fetcher->get_check( $this->get_obj_id() ); return $post->post_type; } } <?php use WP_CLI\Fetchers\User as UserFetcher; use WP_CLI\Formatter; use WP_CLI\Utils; /** * Destroys and lists a user's sessions. * * ## EXAMPLES * * # List a user's sessions. * $ wp user session list admin@example.com --format=csv * login_time,expiration_time,ip,ua * "2016-01-01 12:34:56","2016-02-01 12:34:56",127.0.0.1,"Mozilla/5.0..." * * # Destroy the most recent session of the given user. * $ wp user session destroy admin * Success: Destroyed session. 3 sessions remaining. * * @package wp-cli */ class User_Session_Command extends WP_CLI_Command { private $fields = [ 'token', 'login_time', 'expiration_time', 'ip', 'ua', ]; public function __construct() { $this->fetcher = new UserFetcher(); } /** * Destroy a session for the given user. * * ## OPTIONS * * <user> * : User ID, user email, or user login. * * [<token>] * : The token of the session to destroy. Defaults to the most recently created session. * * [--all] * : Destroy all of the user's sessions. * * ## EXAMPLES * * # Destroy the most recent session of the given user. * $ wp user session destroy admin * Success: Destroyed session. 3 sessions remaining. * * # Destroy a specific session of the given user. * $ wp user session destroy admin e073ad8540a9c2... * Success: Destroyed session. 2 sessions remaining. * * # Destroy all the sessions of the given user. * $ wp user session destroy admin --all * Success: Destroyed all sessions. * * # Destroy all sessions for all users. * $ wp user list --field=ID | xargs -n 1 wp user session destroy --all * Success: Destroyed all sessions. * Success: Destroyed all sessions. */ public function destroy( $args, $assoc_args ) { $user = $this->fetcher->get_check( $args[0] ); $token = Utils\get_flag_value( $args, 1, null ); $all = Utils\get_flag_value( $assoc_args, 'all', false ); $manager = WP_Session_Tokens::get_instance( $user->ID ); if ( $token && $all ) { WP_CLI::error( 'The --all flag cannot be specified along with a session token.' ); } if ( $all ) { $manager->destroy_all(); WP_CLI::success( 'Destroyed all sessions.' ); return; } $sessions = $this->get_all_sessions( $manager ); if ( ! $token ) { if ( empty( $sessions ) ) { WP_CLI::success( 'No sessions to destroy.' ); } $last = end( $sessions ); $token = $last['token']; } if ( ! isset( $sessions[ $token ] ) ) { WP_CLI::error( 'Session not found.' ); } $this->destroy_session( $manager, $token ); $remaining = count( $manager->get_all() ); WP_CLI::success( "Destroyed session. {$remaining} remaining." ); } /** * List sessions for the given user. * * Note: The `token` field does not return the actual token, but a hash of * it. The real token is not persisted and can only be found in the * corresponding cookies on the client side. * * ## OPTIONS * * <user> * : User ID, user email, or user login. * * [--fields=<fields>] * : Limit the output to specific fields. * * [--format=<format>] * : Render output in a particular format. * --- * default: table * options: * - table * - csv * - json * - yaml * - count * - ids * --- * * ## AVAILABLE FIELDS * * These fields will be displayed by default for each session: * * * token * * login_time * * expiration_time * * ip * * ua * * These fields are optionally available: * * * expiration * * login * * ## EXAMPLES * * # List a user's sessions. * $ wp user session list admin@example.com --format=csv * login_time,expiration_time,ip,ua * "2016-01-01 12:34:56","2016-02-01 12:34:56",127.0.0.1,"Mozilla/5.0..." * * @subcommand list */ public function list_( $args, $assoc_args ) { $user = $this->fetcher->get_check( $args[0] ); $formatter = $this->get_formatter( $assoc_args ); $manager = WP_Session_Tokens::get_instance( $user->ID ); $sessions = $this->get_all_sessions( $manager ); if ( 'ids' === $formatter->format ) { echo implode( ' ', array_keys( $sessions ) ); } else { $formatter->display_items( $sessions ); } } protected function get_all_sessions( WP_Session_Tokens $manager ) { // Make the private session data accessible to WP-CLI $get_sessions = new ReflectionMethod( $manager, 'get_sessions' ); $get_sessions->setAccessible( true ); $sessions = $get_sessions->invoke( $manager ); array_walk( $sessions, function( &$session, $token ) { $session['token'] = $token; $session['login_time'] = date( 'Y-m-d H:i:s', $session['login'] ); // phpcs:ignore WordPress.DateTime.RestrictedFunctions.date_date $session['expiration_time'] = date( 'Y-m-d H:i:s', $session['expiration'] ); // phpcs:ignore WordPress.DateTime.RestrictedFunctions.date_date } ); return $sessions; } protected function destroy_session( WP_Session_Tokens $manager, $token ) { $update_session = new ReflectionMethod( $manager, 'update_session' ); $update_session->setAccessible( true ); return $update_session->invoke( $manager, $token, null ); } private function get_formatter( &$assoc_args ) { return new Formatter( $assoc_args, $this->fields ); } } <?php use WP_CLI\Entity\RecursiveDataStructureTraverser; use WP_CLI\Formatter; use WP_CLI\Utils; use WP_CLI\Entity\Utils as EntityUtils; /** * Adds, updates, deletes, and lists site options in a multisite installation. * * ## EXAMPLES * * # Get site registration * $ wp site option get registration * none * * # Add site option * $ wp site option add my_option foobar * Success: Added 'my_option' site option. * * # Update site option * $ wp site option update my_option '{"foo": "bar"}' --format=json * Success: Updated 'my_option' site option. * * # Delete site option * $ wp site option delete my_option * Success: Deleted 'my_option' site option. */ class Site_Option_Command extends WP_CLI_Command { /** * Gets a site option. * * ## OPTIONS * * <key> * : Key for the site option. * * [--format=<format>] * : Get value in a particular format. * --- * default: var_export * options: * - var_export * - json * - yaml * --- * * ## EXAMPLES * * # Get site upload filetypes * $ wp site option get upload_filetypes * jpg jpeg png gif mov avi mpg */ public function get( $args, $assoc_args ) { list( $key ) = $args; $value = get_site_option( $key ); if ( false === $value ) { WP_CLI::halt( 1 ); } WP_CLI::print_value( $value, $assoc_args ); } /** * Adds a site option. * * ## OPTIONS * * <key> * : The name of the site option to add. * * [<value>] * : The value of the site option to add. If ommited, the value is read from STDIN. * * [--format=<format>] * : The serialization format for the value. * --- * default: plaintext * options: * - plaintext * - json * --- * * ## EXAMPLES * * # Create a site option by reading a JSON file * $ wp site option add my_option --format=json < config.json * Success: Added 'my_option' site option. */ public function add( $args, $assoc_args ) { $key = $args[0]; $value = WP_CLI::get_value_from_arg_or_stdin( $args, 1 ); $value = WP_CLI::read_value( $value, $assoc_args ); if ( ! add_site_option( $key, $value ) ) { WP_CLI::error( "Could not add site option '{$key}'. Does it already exist?" ); } else { WP_CLI::success( "Added '{$key}' site option." ); } } /** * Lists site options. * * ## OPTIONS * * [--search=<pattern>] * : Use wildcards ( * and ? ) to match option name. * * [--site_id=<id>] * : Limit options to those of a particular site id. * * [--field=<field>] * : Prints the value of a single field. * * [--fields=<fields>] * : Limit the output to specific object fields. * * [--format=<format>] * : The serialization format for the value. total_bytes displays the total size of matching options in bytes. * --- * default: table * options: * - table * - json * - csv * - count * - yaml * - total_bytes * --- * * ## AVAILABLE FIELDS * * This field will be displayed by default for each matching option: * * * meta_key * * meta_value * * These fields are optionally available: * * * meta_id * * site_id * * size_bytes * * ## EXAMPLES * * # List all site options begining with "i2f_" * $ wp site option list --search="i2f_*" * +-------------+--------------+ * | meta_key | meta_value | * +-------------+--------------+ * | i2f_version | 0.1.0 | * +-------------+--------------+ * * @subcommand list */ public function list_( $args, $assoc_args ) { global $wpdb; $pattern = '%'; $fields = [ 'meta_key', 'meta_value' ]; $size_query = ',LENGTH(meta_value) AS `size_bytes`'; if ( isset( $assoc_args['search'] ) ) { $pattern = self::esc_like( $assoc_args['search'] ); // substitute wildcards $pattern = str_replace( '*', '%', $pattern ); $pattern = str_replace( '?', '_', $pattern ); } if ( isset( $assoc_args['fields'] ) ) { $fields = explode( ',', $assoc_args['fields'] ); } if ( Utils\get_flag_value( $assoc_args, 'format' ) === 'total_bytes' ) { $fields = [ 'size_bytes' ]; $size_query = ',SUM(LENGTH(meta_value)) AS `size_bytes`'; } $query = $wpdb->prepare( 'SELECT `meta_id`, `site_id`, `meta_key`,`meta_value`' . $size_query // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- Hard-coded partial query without user input. . " FROM `$wpdb->sitemeta` WHERE `meta_key` LIKE %s", $pattern ); $site_id = Utils\get_flag_value( $assoc_args, 'site_id' ); if ( $site_id ) { $query .= $wpdb->prepare( ' AND site_id=%d', $site_id ); } // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- $query is already prepared above. $results = $wpdb->get_results( $query ); if ( Utils\get_flag_value( $assoc_args, 'format' ) === 'total_bytes' ) { WP_CLI::line( $results[0]->size_bytes ); } else { $formatter = new Formatter( $assoc_args, $fields ); $formatter->display_items( $results ); } } /** * Updates a site option. * * ## OPTIONS * * <key> * : The name of the site option to update. * * [<value>] * : The new value. If ommited, the value is read from STDIN. * * [--format=<format>] * : The serialization format for the value. * --- * default: plaintext * options: * - plaintext * - json * --- * * ## EXAMPLES * * # Update a site option by reading from a file * $ wp site option update my_option < value.txt * Success: Updated 'my_option' site option. * * @alias set */ public function update( $args, $assoc_args ) { $key = $args[0]; $value = WP_CLI::get_value_from_arg_or_stdin( $args, 1 ); $value = WP_CLI::read_value( $value, $assoc_args ); $value = sanitize_option( $key, $value ); $old_value = sanitize_option( $key, get_site_option( $key ) ); if ( $value === $old_value ) { WP_CLI::success( "Value passed for '{$key}' site option is unchanged." ); } else { if ( update_site_option( $key, $value ) ) { WP_CLI::success( "Updated '{$key}' site option." ); } else { WP_CLI::error( "Could not update site option '{$key}'." ); } } } /** * Deletes a site option. * * ## OPTIONS * * <key> * : Key for the site option. * * ## EXAMPLES * * $ wp site option delete my_option * Success: Deleted 'my_option' site option. */ public function delete( $args ) { list( $key ) = $args; if ( ! delete_site_option( $key ) ) { WP_CLI::error( "Could not delete '{$key}' site option. Does it exist?" ); } else { WP_CLI::success( "Deleted '{$key}' site option." ); } } /** * Gets a nested value from an option. * * ## OPTIONS * * <key> * : The option name. * * <key-path>... * : The name(s) of the keys within the value to locate the value to pluck. * * [--format=<format>] * : The output format of the value. * --- * default: plaintext * options: * - plaintext * - json * - yaml */ public function pluck( $args, $assoc_args ) { list( $key ) = $args; $value = get_site_option( $key ); if ( false === $value ) { WP_CLI::halt( 1 ); } $key_path = array_map( function( $key ) { if ( is_numeric( $key ) && ( (string) intval( $key ) === $key ) ) { return (int) $key; } return $key; }, array_slice( $args, 1 ) ); $traverser = new RecursiveDataStructureTraverser( $value ); try { $value = $traverser->get( $key_path ); } catch ( Exception $exception ) { die( 1 ); } WP_CLI::print_value( $value, $assoc_args ); } /** * Updates a nested value in an option. * * ## OPTIONS * * <action> * : Patch action to perform. * --- * options: * - insert * - update * - delete * --- * * <key> * : The option name. * * <key-path>... * : The name(s) of the keys within the value to locate the value to patch. * * [<value>] * : The new value. If omitted, the value is read from STDIN. * * [--format=<format>] * : The serialization format for the value. * --- * default: plaintext * options: * - plaintext * - json * --- */ public function patch( $args, $assoc_args ) { list( $action, $key ) = $args; $key_path = array_map( function( $key ) { if ( is_numeric( $key ) && ( (string) intval( $key ) === $key ) ) { return (int) $key; } return $key; }, array_slice( $args, 2 ) ); if ( 'delete' === $action ) { $patch_value = null; } else { $stdin_value = EntityUtils::has_stdin() ? trim( WP_CLI::get_value_from_arg_or_stdin( $args, -1 ) ) : null; $patch_value = ! empty( $stdin_value ) ? WP_CLI::read_value( $stdin_value, $assoc_args ) : WP_CLI::read_value( array_pop( $key_path ), $assoc_args ); } /* Need to make a copy of $current_value here as it is modified by reference */ $old_value = sanitize_option( $key, get_site_option( $key ) ); $current_value = $old_value; if ( is_object( $current_value ) ) { $old_value = clone $current_value; } $traverser = new RecursiveDataStructureTraverser( $current_value ); try { $traverser->$action( $key_path, $patch_value ); } catch ( Exception $exception ) { WP_CLI::error( $exception->getMessage() ); } $patched_value = sanitize_option( $key, $traverser->value() ); if ( $patched_value === $old_value ) { WP_CLI::success( "Value passed for '{$key}' site option is unchanged." ); } else { if ( update_site_option( $key, $patched_value ) ) { WP_CLI::success( "Updated '{$key}' site option." ); } else { WP_CLI::error( "Could not update site option '{$key}'." ); } } } private static function esc_like( $old ) { global $wpdb; // Remove notices in 4.0 and support backwards compatibility if ( method_exists( $wpdb, 'esc_like' ) ) { // 4.0 $old = $wpdb->esc_like( $old ); } else { // phpcs:ignore WordPress.WP.DeprecatedFunctions.like_escapeFound -- called in WordPress 3.9 or less. $old = like_escape( esc_sql( $old ) ); } return $old; } } <?php use WP_CLI\CommandWithMeta; use WP_CLI\Fetchers\User as UserFetcher; /** * Adds, updates, deletes, and lists user custom fields. * * ## EXAMPLES * * # Add user meta * $ wp user meta add 123 bio "Mary is an WordPress developer." * Success: Added custom field. * * # List user meta * $ wp user meta list 123 --keys=nickname,description,wp_capabilities * +---------+-----------------+--------------------------------+ * | user_id | meta_key | meta_value | * +---------+-----------------+--------------------------------+ * | 123 | nickname | supervisor | * | 123 | description | Mary is a WordPress developer. | * | 123 | wp_capabilities | {"administrator":true} | * +---------+-----------------+--------------------------------+ * * # Update user meta * $ wp user meta update 123 bio "Mary is an awesome WordPress developer." * Success: Updated custom field 'bio'. * * # Delete user meta * $ wp user meta delete 123 bio * Success: Deleted custom field. */ class User_Meta_Command extends CommandWithMeta { protected $meta_type = 'user'; public function __construct() { $this->fetcher = new UserFetcher(); } /** * Lists all metadata associated with a user. * * ## OPTIONS * * <user> * : The user login, user email, or user ID of the user to get metadata for. * * [--keys=<keys>] * : Limit output to metadata of specific keys. * * [--fields=<fields>] * : Limit the output to specific row fields. Defaults to id,meta_key,meta_value. * * [--format=<format>] * : Render output in a particular format. * --- * default: table * options: * - table * - csv * - json * - count * - yaml * --- * * [--orderby=<fields>] * : Set orderby which field. * --- * default: id * options: * - id * - meta_key * - meta_value * --- * * [--order=<order>] * : Set ascending or descending order. * --- * default: asc * options: * - asc * - desc * --- * * [--unserialize] * : Unserialize meta_value output. * * ## EXAMPLES * * # List user meta * $ wp user meta list 123 --keys=nickname,description,wp_capabilities * +---------+-----------------+--------------------------------+ * | user_id | meta_key | meta_value | * +---------+-----------------+--------------------------------+ * | 123 | nickname | supervisor | * | 123 | description | Mary is a WordPress developer. | * | 123 | wp_capabilities | {"administrator":true} | * +---------+-----------------+--------------------------------+ * * @subcommand list */ public function list_( $args, $assoc_args ) { $args = $this->replace_login_with_user_id( $args ); parent::list_( $args, $assoc_args ); } /** * Gets meta field value. * * ## OPTIONS * * <user> * : The user login, user email, or user ID of the user to get metadata for. * * <key> * : The metadata key. * * [--format=<format>] * : Render output in a particular format. * --- * default: table * options: * - table * - csv * - json * - yaml * --- * * ## EXAMPLES * * # Get user meta * $ wp user meta get 123 bio * Mary is an WordPress developer. */ public function get( $args, $assoc_args ) { $args = $this->replace_login_with_user_id( $args ); parent::get( $args, $assoc_args ); } /** * Deletes a meta field. * * ## OPTIONS * * <user> * : The user login, user email, or user ID of the user to delete metadata from. * * <key> * : The metadata key. * * [<value>] * : The value to delete. If omitted, all rows with key will deleted. * * ## EXAMPLES * * # Delete user meta * $ wp user meta delete 123 bio * Success: Deleted custom field. */ public function delete( $args, $assoc_args ) { $args = $this->replace_login_with_user_id( $args ); parent::delete( $args, $assoc_args ); } /** * Adds a meta field. * * ## OPTIONS * * <user> * : The user login, user email, or user ID of the user to add metadata for. * * <key> * : The metadata key. * * <value> * : The new metadata value. * * [--format=<format>] * : The serialization format for the value. * --- * default: plaintext * options: * - plaintext * - json * --- * * ## EXAMPLES * * # Add user meta * $ wp user meta add 123 bio "Mary is an WordPress developer." * Success: Added custom field. */ public function add( $args, $assoc_args ) { $args = $this->replace_login_with_user_id( $args ); parent::add( $args, $assoc_args ); } /** * Updates a meta field. * * ## OPTIONS * * <user> * : The user login, user email, or user ID of the user to update metadata for. * * <key> * : The metadata key. * * <value> * : The new metadata value. * * [--format=<format>] * : The serialization format for the value. * --- * default: plaintext * options: * - plaintext * - json * --- * * ## EXAMPLES * * # Update user meta * $ wp user meta update 123 bio "Mary is an awesome WordPress developer." * Success: Updated custom field 'bio'. * * @alias set */ public function update( $args, $assoc_args ) { $args = $this->replace_login_with_user_id( $args ); parent::update( $args, $assoc_args ); } /** * Wrapper method for add_metadata that can be overridden in sub classes. * * @param int $object_id ID of the object the metadata is for. * @param string $meta_key Metadata key to use. * @param mixed $meta_value Metadata value. Must be serializable if * non-scalar. * @param bool $unique Optional, default is false. Whether the * specified metadata key should be unique for the * object. If true, and the object already has a * value for the specified metadata key, no change * will be made. * * @return int|false The meta ID on success, false on failure. */ protected function add_metadata( $object_id, $meta_key, $meta_value, $unique = false ) { return add_user_meta( $object_id, $meta_key, $meta_value, $unique ); } /** * Wrapper method for update_metadata that can be overridden in sub classes. * * @param int $object_id ID of the object the metadata is for. * @param string $meta_key Metadata key to use. * @param mixed $meta_value Metadata value. Must be serializable if * non-scalar. * @param mixed $prev_value Optional. If specified, only update existing * metadata entries with the specified value. * Otherwise, update all entries. * * @return int|bool Meta ID if the key didn't exist, true on successful * update, false on failure. */ protected function update_metadata( $object_id, $meta_key, $meta_value, $prev_value = '' ) { return update_user_meta( $object_id, $meta_key, $meta_value, $prev_value ); } /** * Wrapper method for get_metadata that can be overridden in sub classes. * * @param int $object_id ID of the object the metadata is for. * @param string $meta_key Optional. Metadata key. If not specified, * retrieve all metadata for the specified object. * @param bool $single Optional, default is false. If true, return only * the first value of the specified meta_key. This * parameter has no effect if meta_key is not * specified. * * @return mixed Single metadata value, or array of values. */ protected function get_metadata( $object_id, $meta_key = '', $single = false ) { return get_user_meta( $object_id, $meta_key, $single ); } /** * Wrapper method for delete_metadata that can be overridden in sub classes. * * @param int $object_id ID of the object metadata is for * @param string $meta_key Metadata key * @param mixed $meta_value Optional. Metadata value. Must be serializable * if non-scalar. If specified, only delete * metadata entries with this value. Otherwise, * delete all entries with the specified meta_key. * Pass `null, `false`, or an empty string to skip * this check. For backward compatibility, it is * not possible to pass an empty string to delete * those entries with an empty string for a value. * * @return bool True on successful delete, false on failure. */ protected function delete_metadata( $object_id, $meta_key, $meta_value = '' ) { return delete_user_meta( $object_id, $meta_key, $meta_value ); } /** * Replaces user_login value with user ID * user meta is a special case that also supports user_login * * @param array * @return array */ private function replace_login_with_user_id( $args ) { $user = $this->fetcher->get_check( $args[0] ); $args[0] = $user->ID; return $args; } } <?php use WP_CLI\Formatter; use WP_CLI\Utils; /** * Lists, creates, assigns, and deletes the active theme's navigation menus. * * See the [Navigation Menus](https://developer.wordpress.org/themes/functionality/navigation-menus/) reference in the Theme Handbook. * * ## EXAMPLES * * # Create a new menu * $ wp menu create "My Menu" * Success: Created menu 200. * * # List existing menus * $ wp menu list * +---------+----------+----------+-----------+-------+ * | term_id | name | slug | locations | count | * +---------+----------+----------+-----------+-------+ * | 200 | My Menu | my-menu | | 0 | * | 177 | Top Menu | top-menu | primary | 7 | * +---------+----------+----------+-----------+-------+ * * # Create a new menu link item * $ wp menu item add-custom my-menu Apple http://apple.com --porcelain * 1922 * * # Assign the 'my-menu' menu to the 'primary' location * $ wp menu location assign my-menu primary * Success: Assigned location to menu. * * @package wp-cli */ class Menu_Command extends WP_CLI_Command { protected $obj_type = 'nav_menu'; protected $obj_fields = [ 'term_id', 'name', 'slug', 'locations', 'count', ]; /** * Creates a new menu. * * ## OPTIONS * * <menu-name> * : A descriptive name for the menu. * * [--porcelain] * : Output just the new menu id. * * ## EXAMPLES * * $ wp menu create "My Menu" * Success: Created menu 200. */ public function create( $args, $assoc_args ) { $menu_id = wp_create_nav_menu( $args[0] ); if ( is_wp_error( $menu_id ) ) { WP_CLI::error( $menu_id->get_error_message() ); } else { if ( Utils\get_flag_value( $assoc_args, 'porcelain' ) ) { WP_CLI::line( $menu_id ); } else { WP_CLI::success( "Created menu {$menu_id}." ); } } } /** * Deletes one or more menus. * * ## OPTIONS * * <menu>... * : The name, slug, or term ID for the menu(s). * * ## EXAMPLES * * $ wp menu delete "My Menu" * Success: 1 menu deleted. */ public function delete( $args, $assoc_args ) { $count = 0; $errors = 0; foreach ( $args as $arg ) { $ret = wp_delete_nav_menu( $arg ); if ( ! $ret || is_wp_error( $ret ) ) { WP_CLI::warning( "Couldn't delete menu '{$arg}'." ); $errors++; } else { WP_CLI::log( "Deleted menu '{$arg}'." ); $count++; } } Utils\report_batch_operation_results( 'menu', 'delete', count( $args ), $count, $errors ); } /** * Gets a list of menus. * * ## OPTIONS * * [--fields=<fields>] * : Limit the output to specific object fields. * * [--format=<format>] * : Render output in a particular format. * --- * default: table * options: * - table * - csv * - json * - count * - ids * - yaml * --- * * ## AVAILABLE FIELDS * * These fields will be displayed by default for each menu: * * * term_id * * name * * slug * * count * * These fields are optionally available: * * * term_group * * term_taxonomy_id * * taxonomy * * description * * parent * * locations * * ## EXAMPLES * * $ wp menu list * +---------+----------+----------+-----------+-------+ * | term_id | name | slug | locations | count | * +---------+----------+----------+-----------+-------+ * | 200 | My Menu | my-menu | | 0 | * | 177 | Top Menu | top-menu | primary | 7 | * +---------+----------+----------+-----------+-------+ * * @subcommand list */ public function list_( $args, $assoc_args ) { $menus = wp_get_nav_menus(); $menu_locations = get_nav_menu_locations(); foreach ( $menus as &$menu ) { $menu->locations = []; foreach ( $menu_locations as $location => $term_id ) { if ( $term_id === $menu->term_id ) { $menu->locations[] = $location; } } // Normalize the data for some output formats. if ( ! isset( $assoc_args['format'] ) || in_array( $assoc_args['format'], [ 'csv', 'table' ], true ) ) { $menu->locations = implode( ',', $menu->locations ); } } $formatter = $this->get_formatter( $assoc_args ); if ( 'ids' === $formatter->format ) { $ids = array_map( function( $o ) { return $o->term_id; }, $menus ); $formatter->display_items( $ids ); } else { $formatter->display_items( $menus ); } } protected function get_formatter( &$assoc_args ) { return new Formatter( $assoc_args, $this->obj_fields, $this->obj_type ); } } <?php use WP_CLI\CommandWithMeta; /** * Adds, updates, deletes, and lists term custom fields. * * ## EXAMPLES * * # Set term meta * $ wp term meta set 123 bio "Mary is a WordPress developer." * Success: Updated custom field 'bio'. * * # Get term meta * $ wp term meta get 123 bio * Mary is a WordPress developer. * * # Update term meta * $ wp term meta update 123 bio "Mary is an awesome WordPress developer." * Success: Updated custom field 'bio'. * * # Delete term meta * $ wp term meta delete 123 bio * Success: Deleted custom field. */ class Term_Meta_Command extends CommandWithMeta { protected $meta_type = 'term'; /** * Check that the term ID exists * * @param int */ protected function check_object_id( $object_id ) { $term = get_term( $object_id ); if ( ! $term ) { WP_CLI::error( "Could not find the term with ID {$object_id}." ); } return $term->term_id; } /** * Wrapper method for add_metadata that can be overridden in sub classes. * * @param int $object_id ID of the object the metadata is for. * @param string $meta_key Metadata key to use. * @param mixed $meta_value Metadata value. Must be serializable if * non-scalar. * @param bool $unique Optional, default is false. Whether the * specified metadata key should be unique for the * object. If true, and the object already has a * value for the specified metadata key, no change * will be made. * * @return int|false The meta ID on success, false on failure. */ protected function add_metadata( $object_id, $meta_key, $meta_value, $unique = false ) { return add_term_meta( $object_id, $meta_key, $meta_value, $unique ); } /** * Wrapper method for update_metadata that can be overridden in sub classes. * * @param int $object_id ID of the object the metadata is for. * @param string $meta_key Metadata key to use. * @param mixed $meta_value Metadata value. Must be serializable if * non-scalar. * @param mixed $prev_value Optional. If specified, only update existing * metadata entries with the specified value. * Otherwise, update all entries. * * @return int|bool Meta ID if the key didn't exist, true on successful * update, false on failure. */ protected function update_metadata( $object_id, $meta_key, $meta_value, $prev_value = '' ) { return update_term_meta( $object_id, $meta_key, $meta_value, $prev_value ); } /** * Wrapper method for get_metadata that can be overridden in sub classes. * * @param int $object_id ID of the object the metadata is for. * @param string $meta_key Optional. Metadata key. If not specified, * retrieve all metadata for the specified object. * @param bool $single Optional, default is false. If true, return only * the first value of the specified meta_key. This * parameter has no effect if meta_key is not * specified. * * @return mixed Single metadata value, or array of values. */ protected function get_metadata( $object_id, $meta_key = '', $single = false ) { return get_term_meta( $object_id, $meta_key, $single ); } /** * Wrapper method for delete_metadata that can be overridden in sub classes. * * @param int $object_id ID of the object metadata is for * @param string $meta_key Metadata key * @param mixed $meta_value Optional. Metadata value. Must be serializable * if non-scalar. If specified, only delete * metadata entries with this value. Otherwise, * delete all entries with the specified meta_key. * Pass `null, `false`, or an empty string to skip * this check. For backward compatibility, it is * not possible to pass an empty string to delete * those entries with an empty string for a value. * * @return bool True on successful delete, false on failure. */ protected function delete_metadata( $object_id, $meta_key, $meta_value = '' ) { return delete_term_meta( $object_id, $meta_key, $meta_value ); } } <?php use WP_CLI\Formatter; use WP_CLI\Utils; /** * Assigns, removes, and lists a menu's locations. * * ## EXAMPLES * * # List available menu locations * $ wp menu location list * +----------+-------------------+ * | location | description | * +----------+-------------------+ * | primary | Primary Menu | * | social | Social Links Menu | * +----------+-------------------+ * * # Assign the 'primary-menu' menu to the 'primary' location * $ wp menu location assign primary-menu primary * Success: Assigned location to menu. * * # Remove the 'primary-menu' menu from the 'primary' location * $ wp menu location remove primary-menu primary * Success: Removed location from menu. */ class Menu_Location_Command extends WP_CLI_Command { /** * Lists locations for the current theme. * * ## OPTIONS * * [--format=<format>] * : Render output in a particular format. * --- * default: table * options: * - table * - csv * - json * - count * - yaml * - ids * --- * * ## AVAILABLE FIELDS * * These fields will be displayed by default for each location: * * * name * * description * * ## EXAMPLES * * $ wp menu location list * +----------+-------------------+ * | location | description | * +----------+-------------------+ * | primary | Primary Menu | * | social | Social Links Menu | * +----------+-------------------+ * * @subcommand list */ public function list_( $args, $assoc_args ) { $locations = get_registered_nav_menus(); $location_objs = []; foreach ( $locations as $location => $description ) { $location_obj = new stdClass(); $location_obj->location = $location; $location_obj->description = $description; $location_objs[] = $location_obj; } $formatter = new Formatter( $assoc_args, [ 'location', 'description' ] ); if ( 'ids' === $formatter->format ) { $ids = array_map( function( $o ) { return $o->location; }, $location_objs ); $formatter->display_items( $ids ); } else { $formatter->display_items( $location_objs ); } } /** * Assigns a location to a menu. * * ## OPTIONS * * <menu> * : The name, slug, or term ID for the menu. * * <location> * : Location's slug. * * ## EXAMPLES * * $ wp menu location assign primary-menu primary * Success: Assigned location primary to menu primary-menu. * * @subcommand assign */ public function assign( $args, $assoc_args ) { list( $menu, $location ) = $args; $menu_obj = wp_get_nav_menu_object( $menu ); if ( ! $menu_obj ) { WP_CLI::error( "Invalid menu {$menu}." ); } $locations = get_registered_nav_menus(); if ( ! array_key_exists( $location, $locations ) ) { WP_CLI::error( "Invalid location {$location}." ); } $locations = get_nav_menu_locations(); $locations[ $location ] = $menu_obj->term_id; set_theme_mod( 'nav_menu_locations', $locations ); WP_CLI::success( "Assigned location {$location} to menu {$menu}." ); } /** * Removes a location from a menu. * * ## OPTIONS * * <menu> * : The name, slug, or term ID for the menu. * * <location> * : Location's slug. * * ## EXAMPLES * * $ wp menu location remove primary-menu primary * Success: Removed location from menu. * * @subcommand remove */ public function remove( $args, $assoc_args ) { list( $menu, $location ) = $args; $menu = wp_get_nav_menu_object( $menu ); if ( ! $menu || is_wp_error( $menu ) ) { WP_CLI::error( 'Invalid menu.' ); } $locations = get_nav_menu_locations(); if ( Utils\get_flag_value( $locations, $location ) !== $menu->term_id ) { WP_CLI::error( "Menu isn't assigned to location." ); } $locations[ $location ] = 0; set_theme_mod( 'nav_menu_locations', $locations ); WP_CLI::success( 'Removed location from menu.' ); } } <?php use WP_CLI\Formatter; use WP_CLI\Utils; /** * Manages taxonomy terms and term meta, with create, delete, and list commands. * * See reference for [taxonomies and their terms](https://codex.wordpress.org/Taxonomies). * * ## EXAMPLES * * # Create a new term. * $ wp term create category Apple --description="A type of fruit" * Success: Created category 199. * * # Get details about a term. * $ wp term get category 199 --format=json --fields=term_id,name,slug,count * {"term_id":199,"name":"Apple","slug":"apple","count":1} * * # Update an existing term. * $ wp term update category 15 --name=Apple * Success: Term updated. * * # Get the term's URL. * $ wp term list post_tag --include=123 --field=url * http://example.com/tag/tips-and-tricks * * # Delete post category * $ wp term delete category 15 * Success: Deleted category 15. * * # Recount posts assigned to each categories and tags * $ wp term recount category post_tag * Success: Updated category term count * Success: Updated post_tag term count * * @package wp-cli */ class Term_Command extends WP_CLI_Command { private $fields = [ 'term_id', 'term_taxonomy_id', 'name', 'slug', 'description', 'parent', 'count', ]; /** * Lists terms in a taxonomy. * * ## OPTIONS * * <taxonomy>... * : List terms of one or more taxonomies * * [--<field>=<value>] * : Filter by one or more fields (see get_terms() $args parameter for a list of fields). * * [--field=<field>] * : Prints the value of a single field for each term. * * [--fields=<fields>] * : Limit the output to specific object fields. * * [--format=<format>] * : Render output in a particular format. * --- * default: table * options: * - table * - csv * - ids * - json * - count * - yaml * --- * * ## AVAILABLE FIELDS * * These fields will be displayed by default for each term: * * * term_id * * term_taxonomy_id * * name * * slug * * description * * parent * * count * * These fields are optionally available: * * * url * * ## EXAMPLES * * # List post categories * $ wp term list category --format=csv * term_id,term_taxonomy_id,name,slug,description,parent,count * 2,2,aciform,aciform,,0,1 * 3,3,antiquarianism,antiquarianism,,0,1 * 4,4,arrangement,arrangement,,0,1 * 5,5,asmodeus,asmodeus,,0,1 * * # List post tags * $ wp term list post_tag --fields=name,slug * +-----------+-------------+ * | name | slug | * +-----------+-------------+ * | 8BIT | 8bit | * | alignment | alignment-2 | * | Articles | articles | * | aside | aside | * +-----------+-------------+ * * @subcommand list */ public function list_( $args, $assoc_args ) { foreach ( $args as $taxonomy ) { if ( ! taxonomy_exists( $taxonomy ) ) { WP_CLI::error( "Taxonomy {$taxonomy} doesn't exist." ); } } $formatter = $this->get_formatter( $assoc_args ); $defaults = [ 'hide_empty' => false, ]; $assoc_args = array_merge( $defaults, $assoc_args ); if ( ! empty( $assoc_args['term_id'] ) ) { $term = get_term_by( 'id', $assoc_args['term_id'], $args[0] ); $terms = [ $term ]; } elseif ( ! empty( $assoc_args['include'] ) && ! empty( $assoc_args['orderby'] ) && 'include' === $assoc_args['orderby'] && Utils\wp_version_compare( '4.7', '<' ) ) { $terms = []; $term_ids = explode( ',', $assoc_args['include'] ); foreach ( $term_ids as $term_id ) { $term = get_term_by( 'id', $term_id, $args[0] ); if ( $term && ! is_wp_error( $term ) ) { $terms[] = $term; } else { WP_CLI::warning( "Invalid term {$term_id}." ); } } } else { $terms = get_terms( $args, $assoc_args ); } $terms = array_map( function( $term ) { $term->count = (int) $term->count; $term->parent = (int) $term->parent; $term->url = get_term_link( $term ); return $term; }, $terms ); if ( 'ids' === $formatter->format ) { $terms = wp_list_pluck( $terms, 'term_id' ); echo implode( ' ', $terms ); } else { $formatter->display_items( $terms ); } } /** * Creates a new term. * * ## OPTIONS * * <taxonomy> * : Taxonomy for the new term. * * <term> * : A name for the new term. * * [--slug=<slug>] * : A unique slug for the new term. Defaults to sanitized version of name. * * [--description=<description>] * : A description for the new term. * * [--parent=<term-id>] * : A parent for the new term. * * [--porcelain] * : Output just the new term id. * * ## EXAMPLES * * # Create a new category "Apple" with a description. * $ wp term create category Apple --description="A type of fruit" * Success: Created category 199. */ public function create( $args, $assoc_args ) { list( $taxonomy, $term ) = $args; $defaults = [ 'slug' => sanitize_title( $term ), 'description' => '', 'parent' => 0, ]; $assoc_args = wp_parse_args( $assoc_args, $defaults ); $porcelain = Utils\get_flag_value( $assoc_args, 'porcelain' ); unset( $assoc_args['porcelain'] ); // Compatibility for < WP 4.0 if ( $assoc_args['parent'] > 0 && ! term_exists( (int) $assoc_args['parent'] ) ) { WP_CLI::error( 'Parent term does not exist.' ); } $assoc_args = wp_slash( $assoc_args ); $term = wp_slash( $term ); $result = wp_insert_term( $term, $taxonomy, $assoc_args ); if ( is_wp_error( $result ) ) { WP_CLI::error( $result->get_error_message() ); } else { if ( $porcelain ) { WP_CLI::line( $result['term_id'] ); } else { WP_CLI::success( "Created {$taxonomy} {$result['term_id']}." ); } } } /** * Gets details about a term. * * ## OPTIONS * * <taxonomy> * : Taxonomy of the term to get * * <term> * : ID or slug of the term to get * * [--by=<field>] * : Explicitly handle the term value as a slug or id. * --- * default: id * options: * - slug * - id * --- * * [--field=<field>] * : Instead of returning the whole term, returns the value of a single field. * * [--fields=<fields>] * : Limit the output to specific fields. Defaults to all fields. * * [--format=<format>] * : Render output in a particular format. * --- * default: table * options: * - table * - csv * - json * - yaml * --- * * ## EXAMPLES * * # Get details about a category with id 199. * $ wp term get category 199 --format=json * {"term_id":199,"name":"Apple","slug":"apple","term_group":0,"term_taxonomy_id":199,"taxonomy":"category","description":"A type of fruit","parent":0,"count":0,"filter":"raw"} * * # Get details about a category with slug apple. * $ wp term get category apple --by=slug --format=json * {"term_id":199,"name":"Apple","slug":"apple","term_group":0,"term_taxonomy_id":199,"taxonomy":"category","description":"A type of fruit","parent":0,"count":0,"filter":"raw"} */ public function get( $args, $assoc_args ) { list( $taxonomy, $term ) = $args; $term = get_term_by( Utils\get_flag_value( $assoc_args, 'by' ), $term, $taxonomy ); if ( ! $term ) { WP_CLI::error( "Term doesn't exist." ); } if ( empty( $assoc_args['fields'] ) ) { $term_array = get_object_vars( $term ); $assoc_args['fields'] = array_keys( $term_array ); } $term->count = (int) $term->count; $term->parent = (int) $term->parent; $formatter = $this->get_formatter( $assoc_args ); $formatter->display_item( $term ); } /** * Updates an existing term. * * ## OPTIONS * * <taxonomy> * : Taxonomy of the term to update. * * <term> * : ID or slug for the term to update. * * [--by=<field>] * : Explicitly handle the term value as a slug or id. * --- * default: id * options: * - slug * - id * --- * * [--name=<name>] * : A new name for the term. * * [--slug=<slug>] * : A new slug for the term. * * [--description=<description>] * : A new description for the term. * * [--parent=<term-id>] * : A new parent for the term. * * ## EXAMPLES * * # Change category with id 15 to use the name "Apple" * $ wp term update category 15 --name=Apple * Success: Term updated. * * # Change category with slug apple to use the name "Apple" * $ wp term update category apple --by=slug --name=Apple * Success: Term updated. */ public function update( $args, $assoc_args ) { list( $taxonomy, $term ) = $args; $defaults = [ 'name' => null, 'slug' => null, 'description' => null, 'parent' => null, ]; $assoc_args = wp_parse_args( $assoc_args, $defaults ); foreach ( $assoc_args as $key => $value ) { if ( null === $value ) { unset( $assoc_args[ $key ] ); } } $assoc_args = wp_slash( $assoc_args ); $term = get_term_by( Utils\get_flag_value( $assoc_args, 'by' ), $term, $taxonomy ); if ( ! $term ) { WP_CLI::error( "Term doesn't exist." ); } // Update term. $result = wp_update_term( $term->term_id, $taxonomy, $assoc_args ); if ( is_wp_error( $result ) ) { WP_CLI::error( $result->get_error_message() ); } else { WP_CLI::success( 'Term updated.' ); } } /** * Deletes an existing term. * * Errors if the term doesn't exist, or there was a problem in deleting it. * * ## OPTIONS * * <taxonomy> * : Taxonomy of the term to delete. * * <term>... * : One or more IDs or slugs of terms to delete. * * [--by=<field>] * : Explicitly handle the term value as a slug or id. * --- * default: id * options: * - slug * - id * --- * * ## EXAMPLES * * # Delete post category by id * $ wp term delete category 15 * Deleted category 15. * Success: Deleted 1 of 1 terms. * * # Delete post category by slug * $ wp term delete category apple --by=slug * Deleted category 15. * Success: Deleted 1 of 1 terms. * * # Delete all post tags * $ wp term list post_tag --field=term_id | xargs wp term delete post_tag * Deleted post_tag 159. * Deleted post_tag 160. * Deleted post_tag 161. * Success: Deleted 3 of 3 terms. */ public function delete( $args, $assoc_args ) { $taxonomy = array_shift( $args ); $successes = 0; $errors = 0; foreach ( $args as $term_id ) { $term = $term_id; // Get term by specified argument otherwise get term by id. $field = Utils\get_flag_value( $assoc_args, 'by' ); if ( 'slug' === $field ) { // Get term by slug. $term = get_term_by( $field, $term, $taxonomy ); // If term not found, then show error message and skip the iteration. if ( ! $term ) { WP_CLI::warning( "{$taxonomy} {$term_id} doesn't exist." ); continue; } // Get the term id; $term_id = $term->term_id; } $result = wp_delete_term( $term_id, $taxonomy ); if ( is_wp_error( $result ) ) { WP_CLI::warning( $result ); $errors++; } elseif ( $result ) { WP_CLI::log( "Deleted {$taxonomy} {$term_id}." ); $successes++; } else { WP_CLI::warning( "{$taxonomy} {$term_id} doesn't exist." ); } } Utils\report_batch_operation_results( 'term', 'delete', count( $args ), $successes, $errors ); } /** * Generates some terms. * * Creates a specified number of new terms with dummy data. * * ## OPTIONS * * <taxonomy> * : The taxonomy for the generated terms. * * [--count=<number>] * : How many terms to generate? * --- * default: 100 * --- * * [--max_depth=<number>] * : Generate child terms down to a certain depth. * --- * default: 1 * --- * * [--format=<format>] * : Render output in a particular format. * --- * default: progress * options: * - progress * - ids * --- * * ## EXAMPLES * * # Generate post categories. * $ wp term generate category --count=10 * Generating terms 100% [=========] 0:02 / 0:02 * * # Add meta to every generated term. * $ wp term generate category --format=ids --count=3 | xargs -d ' ' -I % wp term meta add % foo bar * Success: Added custom field. * Success: Added custom field. * Success: Added custom field. */ public function generate( $args, $assoc_args ) { global $wpdb; list ( $taxonomy ) = $args; $defaults = [ 'count' => 100, 'max_depth' => 1, ]; $final_args = array_merge( $defaults, $assoc_args ); $count = $final_args['count']; $max_depth = $final_args['max_depth']; if ( ! taxonomy_exists( $taxonomy ) ) { WP_CLI::error( "'{$taxonomy}' is not a registered taxonomy." ); } $label = get_taxonomy( $taxonomy )->labels->singular_name; $slug = sanitize_title_with_dashes( $label ); $hierarchical = get_taxonomy( $taxonomy )->hierarchical; $format = Utils\get_flag_value( $assoc_args, 'format', 'progress' ); $notify = false; if ( 'progress' === $format ) { $notify = Utils\make_progress_bar( 'Generating terms', $count ); } $previous_term_id = 0; $current_parent = 0; $current_depth = 1; $max_id = (int) $wpdb->get_var( "SELECT term_taxonomy_id FROM {$wpdb->term_taxonomy} ORDER BY term_taxonomy_id DESC LIMIT 1" ); $suspend_cache_invalidation = wp_suspend_cache_invalidation( true ); $created = []; for ( $index = $max_id + 1; $index <= $max_id + $count; $index++ ) { if ( $hierarchical ) { if ( $previous_term_id && $this->maybe_make_child() && $current_depth < $max_depth ) { $current_parent = $previous_term_id; $current_depth++; } elseif ( $this->maybe_reset_depth() ) { $current_parent = 0; $current_depth = 1; } } $args = [ 'parent' => $current_parent, 'slug' => $slug . "-{$index}", ]; $name = "{$label} {$index}"; $term = wp_insert_term( $name, $taxonomy, $args ); if ( is_wp_error( $term ) ) { WP_CLI::warning( $term ); } else { $created[] = $term['term_id']; $previous_term_id = $term['term_id']; if ( 'ids' === $format ) { echo $term['term_id']; if ( $index < $max_id + $count ) { echo ' '; } } } if ( 'progress' === $format ) { $notify->tick(); } } wp_suspend_cache_invalidation( $suspend_cache_invalidation ); clean_term_cache( $created, $taxonomy ); if ( 'progress' === $format ) { $notify->finish(); } } /** * Recalculates number of posts assigned to each term. * * In instances where manual updates are made to the terms assigned to * posts in the database, the number of posts associated with a term * can become out-of-sync with the actual number of posts. * * This command runs wp_update_term_count() on the taxonomy's terms * to bring the count back to the correct value. * * ## OPTIONS * * <taxonomy>... * : One or more taxonomies to recalculate. * * ## EXAMPLES * * # Recount posts assigned to each categories and tags * $ wp term recount category post_tag * Success: Updated category term count. * Success: Updated post_tag term count. * * # Recount all listed taxonomies * $ wp taxonomy list --field=name | xargs wp term recount * Success: Updated category term count. * Success: Updated post_tag term count. * Success: Updated nav_menu term count. * Success: Updated link_category term count. * Success: Updated post_format term count. */ public function recount( $args ) { foreach ( $args as $taxonomy ) { if ( ! taxonomy_exists( $taxonomy ) ) { WP_CLI::warning( "Taxonomy {$taxonomy} does not exist." ); } else { $terms = get_terms( $taxonomy, [ 'hide_empty' => false ] ); $term_taxonomy_ids = wp_list_pluck( $terms, 'term_taxonomy_id' ); wp_update_term_count( $term_taxonomy_ids, $taxonomy ); WP_CLI::success( "Updated {$taxonomy} term count." ); } } } /** * Migrate a term of a taxonomy to another taxonomy. * * ## OPTIONS * * <term> * : Slug or ID of the term to migrate. * * [--by=<field>] * : Explicitly handle the term value as a slug or id. * --- * default: id * options: * - slug * - id * --- * * [--from=<taxonomy>] * : Taxonomy slug of the term to migrate. * * [--to=<taxonomy>] * : Taxonomy slug to migrate to. * * ## EXAMPLES * * # Migrate a category's term (video) to tag taxonomy. * $ wp term migrate 9190 --from=category --to=post_tag * Term '9190' migrated! * Old instance of term '9190' removed from its original taxonomy. * Success: Migrated the term '9190' from taxonomy 'category' to taxonomy 'post_tag' for 1 posts */ public function migrate( $args, $assoc_args ) { $term_reference = $args[0]; $original_taxonomy = Utils\get_flag_value( $assoc_args, 'from' ); $destination_taxonomy = Utils\get_flag_value( $assoc_args, 'to' ); $term = get_term_by( Utils\get_flag_value( $assoc_args, 'by' ), $term_reference, $original_taxonomy ); if ( ! $term ) { WP_CLI::error( "Taxonomy term '{$term_reference}' for taxonomy '{$original_taxonomy}' doesn't exist." ); } $original_taxonomy = get_taxonomy( $original_taxonomy ); $id = wp_insert_term( $term->name, $destination_taxonomy, [ 'slug' => $term->slug, 'parent' => 0, 'description' => $term->description, ] ); if ( is_wp_error( $id ) ) { WP_CLI::error( $id->get_error_message() ); } $post_ids = get_objects_in_term( $term->term_id, $original_taxonomy->name ); foreach ( $post_ids as $post_id ) { $type = get_post_type( $post_id ); if ( in_array( $type, $original_taxonomy->object_type, true ) ) { $term_taxonomy_id = wp_set_object_terms( $post_id, $id['term_id'], $destination_taxonomy, true ); if ( is_wp_error( $term_taxonomy_id ) ) { WP_CLI::error( "Failed to assign the term '{$term->slug}' to the post {$post_id}. Reason: " . $term_taxonomy_id->get_error_message() ); } WP_CLI::log( "Term '{$term->slug}' assigned to post {$post_id}." ); } clean_post_cache( $post_id ); } clean_term_cache( $term->term_id ); WP_CLI::log( "Term '{$term->slug}' migrated." ); $del = wp_delete_term( $term->term_id, $original_taxonomy->name ); if ( is_wp_error( $del ) ) { WP_CLI::error( "Failed to delete the term '{$term->slug}'. Reason: " . $del->get_error_message() ); } WP_CLI::log( "Old instance of term '{$term->slug}' removed from its original taxonomy." ); $post_count = count( $post_ids ); $post_plural = Utils\pluralize( 'post', $post_count ); WP_CLI::success( "Migrated the term '{$term->slug}' from taxonomy '{$original_taxonomy->name}' to taxonomy '{$destination_taxonomy}' for {$post_count} {$post_plural}." ); } private function maybe_make_child() { // 50% chance of making child term. return ( wp_rand( 1, 2 ) === 1 ); } private function maybe_reset_depth() { // 10% chance of reseting to root depth. return ( wp_rand( 1, 10 ) === 7 ); } private function get_formatter( &$assoc_args ) { return new Formatter( $assoc_args, $this->fields, 'term' ); } } <?php use WP_CLI\Entity\RecursiveDataStructureTraverser; use WP_CLI\Formatter; use WP_CLI\Utils; use WP_CLI\Entity\Utils as EntityUtils; /** * Retrieves and sets site options, including plugin and WordPress settings. * * See the [Plugin Settings API](https://developer.wordpress.org/plugins/settings/settings-api/) and the [Theme Options](https://developer.wordpress.org/themes/customize-api/) for more information on adding customized options. * * ## EXAMPLES * * # Get site URL. * $ wp option get siteurl * http://example.com * * # Add option. * $ wp option add my_option foobar * Success: Added 'my_option' option. * * # Update option. * $ wp option update my_option '{"foo": "bar"}' --format=json * Success: Updated 'my_option' option. * * # Delete option. * $ wp option delete my_option * Success: Deleted 'my_option' option. * * @package wp-cli */ class Option_Command extends WP_CLI_Command { /** * Gets the value for an option. * * ## OPTIONS * * <key> * : Key for the option. * * [--format=<format>] * : Get value in a particular format. * --- * default: var_export * options: * - var_export * - json * - yaml * --- * * ## EXAMPLES * * # Get option. * $ wp option get home * http://example.com * * # Get blog description. * $ wp option get blogdescription * A random blog description * * # Get blog name * $ wp option get blogname * A random blog name * * # Get admin email. * $ wp option get admin_email * someone@example.com * * # Get option in JSON format. * $ wp option get active_plugins --format=json * {"0":"dynamically-dynamic-sidebar\/dynamically-dynamic-sidebar.php","1":"monster-widget\/monster-widget.php","2":"show-current-template\/show-current-template.php","3":"theme-check\/theme-check.php","5":"wordpress-importer\/wordpress-importer.php"} */ public function get( $args, $assoc_args ) { list( $key ) = $args; $value = get_option( $key ); if ( false === $value ) { WP_CLI::error( "Could not get '{$key}' option. Does it exist?" ); } WP_CLI::print_value( $value, $assoc_args ); } /** * Adds a new option value. * * Errors if the option already exists. * * ## OPTIONS * * <key> * : The name of the option to add. * * [<value>] * : The value of the option to add. If ommited, the value is read from STDIN. * * [--format=<format>] * : The serialization format for the value. * --- * default: plaintext * options: * - plaintext * - json * --- * * [--autoload=<autoload>] * : Should this option be automatically loaded. * --- * options: * - 'yes' * - 'no' * --- * * ## EXAMPLES * * # Create an option by reading a JSON file. * $ wp option add my_option --format=json < config.json * Success: Added 'my_option' option. */ public function add( $args, $assoc_args ) { $key = $args[0]; $value = WP_CLI::get_value_from_arg_or_stdin( $args, 1 ); $value = WP_CLI::read_value( $value, $assoc_args ); if ( Utils\get_flag_value( $assoc_args, 'autoload' ) === 'no' ) { $autoload = 'no'; } else { $autoload = 'yes'; } if ( ! add_option( $key, $value, '', $autoload ) ) { WP_CLI::error( "Could not add option '{$key}'. Does it already exist?" ); } else { WP_CLI::success( "Added '{$key}' option." ); } } /** * Lists options and their values. * * ## OPTIONS * * [--search=<pattern>] * : Use wildcards ( * and ? ) to match option name. * * [--exclude=<pattern>] * : Pattern to exclude. Use wildcards ( * and ? ) to match option name. * * [--autoload=<value>] * : Match only autoload options when value is on, and only not-autoload option when off. * * [--transients] * : List only transients. Use `--no-transients` to ignore all transients. * * [--unserialize] * : Unserialize option values in output. * * [--field=<field>] * : Prints the value of a single field. * * [--fields=<fields>] * : Limit the output to specific object fields. * * [--format=<format>] * : The serialization format for the value. total_bytes displays the total size of matching options in bytes. * --- * default: table * options: * - table * - json * - csv * - count * - yaml * - total_bytes * --- * * [--orderby=<fields>] * : Set orderby which field. * --- * default: option_id * options: * - option_id * - option_name * - option_value * --- * * [--order=<order>] * : Set ascending or descending order. * --- * default: asc * options: * - asc * - desc * --- * * ## AVAILABLE FIELDS * * This field will be displayed by default for each matching option: * * * option_name * * option_value * * These fields are optionally available: * * * autoload * * size_bytes * * ## EXAMPLES * * # Get the total size of all autoload options. * $ wp option list --autoload=on --format=total_bytes * 33198 * * # Find biggest transients. * $ wp option list --search="*_transient_*" --fields=option_name,size_bytes | sort -n -k 2 | tail * option_name size_bytes * _site_transient_timeout_theme_roots 10 * _site_transient_theme_roots 76 * _site_transient_update_themes 181 * _site_transient_update_core 808 * _site_transient_update_plugins 6645 * * # List all options beginning with "i2f_". * $ wp option list --search="i2f_*" * +-------------+--------------+ * | option_name | option_value | * +-------------+--------------+ * | i2f_version | 0.1.0 | * +-------------+--------------+ * * # Delete all options beginning with "theme_mods_". * $ wp option list --search="theme_mods_*" --field=option_name | xargs -I % wp option delete % * Success: Deleted 'theme_mods_twentysixteen' option. * Success: Deleted 'theme_mods_twentyfifteen' option. * Success: Deleted 'theme_mods_twentyfourteen' option. * * @subcommand list */ public function list_( $args, $assoc_args ) { global $wpdb; $pattern = '%'; $exclude = ''; $fields = array( 'option_name', 'option_value' ); $size_query = ',LENGTH(option_value) AS `size_bytes`'; $autoload_query = ''; if ( isset( $assoc_args['search'] ) ) { $pattern = self::esc_like( $assoc_args['search'] ); // substitute wildcards $pattern = str_replace( '*', '%', $pattern ); $pattern = str_replace( '?', '_', $pattern ); } if ( isset( $assoc_args['exclude'] ) ) { $exclude = self::esc_like( $assoc_args['exclude'] ); $exclude = str_replace( '*', '%', $exclude ); $exclude = str_replace( '?', '_', $exclude ); } if ( isset( $assoc_args['fields'] ) ) { $fields = explode( ',', $assoc_args['fields'] ); } if ( Utils\get_flag_value( $assoc_args, 'format' ) === 'total_bytes' ) { $fields = array( 'size_bytes' ); $size_query = ',SUM(LENGTH(option_value)) AS `size_bytes`'; } if ( isset( $assoc_args['autoload'] ) ) { if ( 'on' === $assoc_args['autoload'] ) { $autoload_query = " AND autoload='yes'"; } elseif ( 'off' === $assoc_args['autoload'] ) { $autoload_query = " AND autoload='no'"; } else { WP_CLI::error( "Value of '--autoload' should be on or off." ); } } // By default we don't want to display transients. $show_transients = Utils\get_flag_value( $assoc_args, 'transients', false ); if ( $show_transients ) { $transients_query = " AND option_name LIKE '\_transient\_%' OR option_name LIKE '\_site\_transient\_%'"; } else { $transients_query = " AND option_name NOT LIKE '\_transient\_%' AND option_name NOT LIKE '\_site\_transient\_%'"; } $where = ''; if ( $pattern ) { $where .= $wpdb->prepare( 'WHERE `option_name` LIKE %s', $pattern ); } if ( $exclude ) { $where .= $wpdb->prepare( ' AND `option_name` NOT LIKE %s', $exclude ); } $where .= $autoload_query . $transients_query; // phpcs:disable WordPress.DB.PreparedSQL -- Hardcoded query parts without user input. $results = $wpdb->get_results( 'SELECT `option_name`,`option_value`,`autoload`' . $size_query . " FROM `$wpdb->options` {$where}" ); // phpcs:enable $orderby = Utils\get_flag_value( $assoc_args, 'orderby' ); $order = Utils\get_flag_value( $assoc_args, 'order' ); // Sort result. if ( 'option_id' !== $orderby ) { usort( $results, function ( $a, $b ) use ( $orderby, $order ) { // Sort array. return 'asc' === $order ? $a->$orderby > $b->$orderby : $a->$orderby < $b->$orderby; } ); } elseif ( 'option_id' === $orderby && 'desc' === $order ) { // Sort by default descending. krsort( $results ); } if ( true === Utils\get_flag_value( $assoc_args, 'unserialize', null ) ) { foreach ( $results as $k => &$v ) { if ( ! empty( $v->option_value ) ) { $v->option_value = maybe_unserialize( $v->option_value ); } } } if ( Utils\get_flag_value( $assoc_args, 'format' ) === 'total_bytes' ) { WP_CLI::line( $results[0]->size_bytes ); } else { $formatter = new Formatter( $assoc_args, $fields ); $formatter->display_items( $results ); } } /** * Updates an option value. * * ## OPTIONS * * <key> * : The name of the option to update. * * [<value>] * : The new value. If ommited, the value is read from STDIN. * * [--autoload=<autoload>] * : Requires WP 4.2. Should this option be automatically loaded. * --- * options: * - 'yes' * - 'no' * --- * * [--format=<format>] * : The serialization format for the value. * --- * default: plaintext * options: * - plaintext * - json * --- * * ## EXAMPLES * * # Update an option by reading from a file. * $ wp option update my_option < value.txt * Success: Updated 'my_option' option. * * # Update one option on multiple sites using xargs. * $ wp site list --field=url | xargs -n1 -I {} sh -c 'wp --url={} option update my_option my_value' * Success: Updated 'my_option' option. * Success: Updated 'my_option' option. * * # Update site blog name. * $ wp option update blogname "Random blog name" * Success: Updated 'blogname' option. * * # Update site blog description. * $ wp option update blogdescription "Some random blog description" * Success: Updated 'blogdescription' option. * * # Update admin email address. * $ wp option update admin_email someone@example.com * Success: Updated 'admin_email' option. * * # Set the default role. * $ wp option update default_role author * Success: Updated 'default_role' option. * * # Set the timezone string. * $ wp option update timezone_string "America/New_York" * Success: Updated 'timezone_string' option. * * @alias set */ public function update( $args, $assoc_args ) { $key = $args[0]; $value = WP_CLI::get_value_from_arg_or_stdin( $args, 1 ); $value = WP_CLI::read_value( $value, $assoc_args ); $autoload = Utils\get_flag_value( $assoc_args, 'autoload' ); if ( ! in_array( $autoload, [ 'yes', 'no' ], true ) ) { $autoload = null; } $value = sanitize_option( $key, $value ); // Sanitization WordPress normally performs when getting an option if ( in_array( $key, [ 'siteurl', 'home', 'category_base', 'tag_base' ], true ) ) { $value = untrailingslashit( $value ); } $old_value = sanitize_option( $key, get_option( $key ) ); if ( $value === $old_value && null === $autoload ) { WP_CLI::success( "Value passed for '{$key}' option is unchanged." ); } else { if ( update_option( $key, $value, $autoload ) ) { WP_CLI::success( "Updated '{$key}' option." ); } else { WP_CLI::error( "Could not update option '{$key}'." ); } } } /** * Deletes an option. * * ## OPTIONS * * <key>... * : Key for the option. * * ## EXAMPLES * * # Delete an option. * $ wp option delete my_option * Success: Deleted 'my_option' option. * * # Delete multiple options. * $ wp option delete option_one option_two option_three * Success: Deleted 'option_one' option. * Success: Deleted 'option_two' option. * Warning: Could not delete 'option_three' option. Does it exist? */ public function delete( $args ) { foreach ( $args as $arg ) { if ( ! delete_option( $arg ) ) { WP_CLI::warning( "Could not delete '{$arg}' option. Does it exist?" ); } else { WP_CLI::success( "Deleted '{$arg}' option." ); } } } /** * Gets a nested value from an option. * * ## OPTIONS * * <key> * : The option name. * * <key-path>... * : The name(s) of the keys within the value to locate the value to pluck. * * [--format=<format>] * : The output format of the value. * --- * default: plaintext * options: * - plaintext * - json * - yaml * --- */ public function pluck( $args, $assoc_args ) { list( $key ) = $args; $value = get_option( $key ); if ( false === $value ) { WP_CLI::halt( 1 ); } $key_path = array_map( function( $key ) { if ( is_numeric( $key ) && ( (string) intval( $key ) === $key ) ) { return (int) $key; } return $key; }, array_slice( $args, 1 ) ); $traverser = new RecursiveDataStructureTraverser( $value ); try { $value = $traverser->get( $key_path ); } catch ( Exception $exception ) { die( 1 ); } WP_CLI::print_value( $value, $assoc_args ); } /** * Updates a nested value in an option. * * ## OPTIONS * * <action> * : Patch action to perform. * --- * options: * - insert * - update * - delete * --- * * <key> * : The option name. * * <key-path>... * : The name(s) of the keys within the value to locate the value to patch. * * [<value>] * : The new value. If omitted, the value is read from STDIN. * * [--format=<format>] * : The serialization format for the value. * --- * default: plaintext * options: * - plaintext * - json * --- */ public function patch( $args, $assoc_args ) { list( $action, $key ) = $args; $key_path = array_map( function( $key ) { if ( is_numeric( $key ) && ( (string) intval( $key ) === $key ) ) { return (int) $key; } return $key; }, array_slice( $args, 2 ) ); if ( 'delete' === $action ) { $patch_value = null; } else { $stdin_value = EntityUtils::has_stdin() ? trim( WP_CLI::get_value_from_arg_or_stdin( $args, -1 ) ) : null; $patch_value = ! empty( $stdin_value ) ? WP_CLI::read_value( $stdin_value, $assoc_args ) : WP_CLI::read_value( array_pop( $key_path ), $assoc_args ); } /* Need to make a copy of $current_value here as it is modified by reference */ $old_value = sanitize_option( $key, get_option( $key ) ); $current_value = $old_value; if ( is_object( $current_value ) ) { $old_value = clone $current_value; } $traverser = new RecursiveDataStructureTraverser( $current_value ); try { $traverser->$action( $key_path, $patch_value ); } catch ( Exception $exception ) { WP_CLI::error( $exception->getMessage() ); } $patched_value = sanitize_option( $key, $traverser->value() ); if ( $patched_value === $old_value ) { WP_CLI::success( "Value passed for '{$key}' option is unchanged." ); } else { if ( update_option( $key, $patched_value ) ) { WP_CLI::success( "Updated '{$key}' option." ); } else { WP_CLI::error( "Could not update option '{$key}'." ); } } } private static function esc_like( $old ) { global $wpdb; // Remove notices in 4.0 and support backwards compatibility if ( method_exists( $wpdb, 'esc_like' ) ) { // 4.0 $old = $wpdb->esc_like( $old ); } else { // phpcs:ignore WordPress.WP.DeprecatedFunctions.like_escapeFound -- called in WordPress 3.9 or less. $old = like_escape( esc_sql( $old ) ); } return $old; } } <?php use WP_CLI\CommandWithMeta; /** * Gets, adds, updates, deletes, and lists network custom fields. * * ## EXAMPLES * * # Get a list of super-admins * $ wp network meta get 1 site_admins * array ( * 0 => 'supervisor', * ) */ class Network_Meta_Command extends CommandWithMeta { protected $meta_type = 'site'; } <?php use WP_CLI\Formatter; use WP_CLI\Utils; /** * Retrieves information about registered taxonomies. * * See references for [built-in taxonomies](https://developer.wordpress.org/themes/basics/categories-tags-custom-taxonomies/) and [custom taxonomies](https://developer.wordpress.org/plugins/taxonomies/working-with-custom-taxonomies/). * * ## EXAMPLES * * # List all taxonomies with 'post' object type. * $ wp taxonomy list --object_type=post --fields=name,public * +-------------+--------+ * | name | public | * +-------------+--------+ * | category | 1 | * | post_tag | 1 | * | post_format | 1 | * +-------------+--------+ * * # Get capabilities of 'post_tag' taxonomy. * $ wp taxonomy get post_tag --field=cap * {"manage_terms":"manage_categories","edit_terms":"manage_categories","delete_terms":"manage_categories","assign_terms":"edit_posts"} * * @package wp-cli */ class Taxonomy_Command extends WP_CLI_Command { private $fields = array( 'name', 'label', 'description', 'object_type', 'show_tagcloud', 'hierarchical', 'public', ); public function __construct() { if ( Utils\wp_version_compare( 3.7, '<' ) ) { // remove description for wp <= 3.7 $this->fields = array_values( array_diff( $this->fields, array( 'description' ) ) ); } parent::__construct(); } /** * Gets the term counts for each supplied taxonomy. * * @param array $taxonomies Taxonomies to fetch counts for. * @return array Associative array of term counts keyed by taxonomy. */ protected function get_counts( $taxonomies ) { global $wpdb; if ( count( $taxonomies ) <= 0 ) { return []; } $query = $wpdb->prepare( "SELECT `taxonomy`, COUNT(*) AS `count` FROM $wpdb->term_taxonomy WHERE `taxonomy` IN (" . implode( ',', array_fill( 0, count( $taxonomies ), '%s' ) ) . ') GROUP BY `taxonomy`', $taxonomies ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- $query is already prepared above. $counts = $wpdb->get_results( $query ); // Make sure there's a count for every item. $counts = array_merge( array_fill_keys( $taxonomies, 0 ), wp_list_pluck( $counts, 'count', 'taxonomy' ) ); return $counts; } /** * Lists registered taxonomies. * * ## OPTIONS * * [--<field>=<value>] * : Filter by one or more fields (see get_taxonomies() first parameter for a list of available fields). * * [--field=<field>] * : Prints the value of a single field for each taxonomy. * * [--fields=<fields>] * : Limit the output to specific taxonomy fields. * * [--format=<format>] * : Render output in a particular format. * --- * default: table * options: * - table * - csv * - json * - count * - yaml * --- * * ## AVAILABLE FIELDS * * These fields will be displayed by default for each term: * * * name * * label * * description * * object_type * * show_tagcloud * * hierarchical * * public * * These fields are optionally available: * * * count * * ## EXAMPLES * * # List all taxonomies. * $ wp taxonomy list --format=csv * name,label,description,object_type,show_tagcloud,hierarchical,public * category,Categories,,post,1,1,1 * post_tag,Tags,,post,1,,1 * nav_menu,"Navigation Menus",,nav_menu_item,,, * link_category,"Link Categories",,link,1,, * post_format,Format,,post,,,1 * * # List all taxonomies with 'post' object type. * $ wp taxonomy list --object_type=post --fields=name,public * +-------------+--------+ * | name | public | * +-------------+--------+ * | category | 1 | * | post_tag | 1 | * | post_format | 1 | * +-------------+--------+ * * @subcommand list */ public function list_( $args, $assoc_args ) { $formatter = $this->get_formatter( $assoc_args ); // Check if it's strict mode or not. $strict = Utils\get_flag_value( $assoc_args, 'strict', false ); unset( $assoc_args['strict'] ); if ( isset( $assoc_args['object_type'] ) ) { $assoc_args['object_type'] = array( $assoc_args['object_type'] ); $taxonomy_object = $assoc_args['object_type']; } else { $taxonomy_object = get_post_types(); } $fields = $formatter->fields; $taxonomies = ( isset( $taxonomy_object ) && ! $strict ) ? get_object_taxonomies( $taxonomy_object, 'objects' ) : get_taxonomies( $assoc_args, 'objects' ); $counts = []; if ( count( $taxonomies ) > 0 && in_array( 'count', $fields, true ) ) { $counts = $this->get_counts( wp_list_pluck( $taxonomies, 'name' ) ); } $taxonomies = array_map( function( $taxonomy ) use ( $counts ) { $taxonomy->object_type = implode( ', ', $taxonomy->object_type ); $taxonomy->count = isset( $counts[ $taxonomy->name ] ) ? $counts[ $taxonomy->name ] : 0; return $taxonomy; }, $taxonomies ); $formatter->display_items( $taxonomies ); } /** * Gets details about a registered taxonomy. * * ## OPTIONS * * <taxonomy> * : Taxonomy slug. * * [--field=<field>] * : Instead of returning the whole taxonomy, returns the value of a single field. * * [--fields=<fields>] * : Limit the output to specific fields. Defaults to all fields. * * [--format=<format>] * : Render output in a particular format. * --- * default: table * options: * - table * - csv * - json * - yaml * --- * * ## AVAILABLE FIELDS * * These fields will be displayed by default for the specified taxonomy: * * * name * * label * * description * * object_type * * show_tagcloud * * hierarchical * * public * * labels * * cap * * These fields are optionally available: * * * count * * ## EXAMPLES * * # Get details of `category` taxonomy. * $ wp taxonomy get category --fields=name,label,object_type * +-------------+------------+ * | Field | Value | * +-------------+------------+ * | name | category | * | label | Categories | * | object_type | ["post"] | * +-------------+------------+ * * # Get capabilities of 'post_tag' taxonomy. * $ wp taxonomy get post_tag --field=cap * {"manage_terms":"manage_categories","edit_terms":"manage_categories","delete_terms":"manage_categories","assign_terms":"edit_posts"} */ public function get( $args, $assoc_args ) { $taxonomy = get_taxonomy( $args[0] ); if ( ! $taxonomy ) { WP_CLI::error( "Taxonomy {$args[0]} doesn't exist." ); } if ( empty( $assoc_args['fields'] ) ) { $default_fields = array_merge( $this->fields, array( 'labels', 'cap', ) ); $assoc_args['fields'] = $default_fields; } $formatter = $this->get_formatter( $assoc_args ); $fields = $formatter->fields; $count = 0; if ( in_array( 'count', $fields, true ) ) { $count = $this->get_counts( [ $taxonomy->name ] ); $count = $count[ $taxonomy->name ]; } $data = array( 'name' => $taxonomy->name, 'label' => $taxonomy->label, 'description' => $taxonomy->description, 'object_type' => $taxonomy->object_type, 'show_tagcloud' => $taxonomy->show_tagcloud, 'hierarchical' => $taxonomy->hierarchical, 'public' => $taxonomy->public, 'labels' => $taxonomy->labels, 'cap' => $taxonomy->cap, 'count' => $count, ); $formatter->display_item( $data ); } private function get_formatter( &$assoc_args ) { return new Formatter( $assoc_args, $this->fields, 'taxonomy' ); } } <?php use WP_CLI\CommandWithTerms; /** * Adds, updates, removes, and lists user terms. * * ## EXAMPLES * * # Set user terms * $ wp user term set 123 test category * Success: Set terms. */ class User_Term_Command extends CommandWithTerms { protected $obj_type = 'user'; } <?php use WP_CLI\CommandWithMeta; use WP_CLI\Fetchers\Site as SiteFetcher; /** * Adds, updates, deletes, and lists site custom fields. * * ## EXAMPLES * * # Set site meta * $ wp site meta set 123 bio "Mary is a WordPress developer." * Success: Updated custom field 'bio'. * * # Get site meta * $ wp site meta get 123 bio * Mary is a WordPress developer. * * # Update site meta * $ wp site meta update 123 bio "Mary is an awesome WordPress developer." * Success: Updated custom field 'bio'. * * # Delete site meta * $ wp site meta delete 123 bio * Success: Deleted custom field. */ class Site_Meta_Command extends CommandWithMeta { protected $meta_type = 'blog'; /** * Check that the site ID exists * * @param int */ protected function check_object_id( $object_id ) { $fetcher = new SiteFetcher(); $site = $fetcher->get_check( $object_id ); return $site->blog_id; } /** * Wrapper method for add_metadata that can be overridden in sub classes. * * @param int $object_id ID of the object the metadata is for. * @param string $meta_key Metadata key to use. * @param mixed $meta_value Metadata value. Must be serializable if * non-scalar. * @param bool $unique Optional, default is false. Whether the * specified metadata key should be unique for the * object. If true, and the object already has a * value for the specified metadata key, no change * will be made. * * @return int|false The meta ID on success, false on failure. */ protected function add_metadata( $object_id, $meta_key, $meta_value, $unique = false ) { return add_site_meta( $object_id, $meta_key, $meta_value, $unique ); } /** * Wrapper method for update_metadata that can be overridden in sub classes. * * @param int $object_id ID of the object the metadata is for. * @param string $meta_key Metadata key to use. * @param mixed $meta_value Metadata value. Must be serializable if * non-scalar. * @param mixed $prev_value Optional. If specified, only update existing * metadata entries with the specified value. * Otherwise, update all entries. * * @return int|bool Meta ID if the key didn't exist, true on successful * update, false on failure. */ protected function update_metadata( $object_id, $meta_key, $meta_value, $prev_value = '' ) { return update_site_meta( $object_id, $meta_key, $meta_value, $prev_value ); } /** * Wrapper method for get_metadata that can be overridden in sub classes. * * @param int $object_id ID of the object the metadata is for. * @param string $meta_key Optional. Metadata key. If not specified, * retrieve all metadata for the specified object. * @param bool $single Optional, default is false. If true, return only * the first value of the specified meta_key. This * parameter has no effect if meta_key is not * specified. * * @return mixed Single metadata value, or array of values. */ protected function get_metadata( $object_id, $meta_key = '', $single = false ) { return get_site_meta( $object_id, $meta_key, $single ); } /** * Wrapper method for delete_metadata that can be overridden in sub classes. * * @param int $object_id ID of the object metadata is for * @param string $meta_key Metadata key * @param mixed $meta_value Optional. Metadata value. Must be serializable * if non-scalar. If specified, only delete * metadata entries with this value. Otherwise, * delete all entries with the specified meta_key. * Pass `null, `false`, or an empty string to skip * this check. For backward compatibility, it is * not possible to pass an empty string to delete * those entries with an empty string for a value. * * @return bool True on successful delete, false on failure. */ protected function delete_metadata( $object_id, $meta_key, $meta_value = '' ) { return delete_site_meta( $object_id, $meta_key, $meta_value ); } } <?php use WP_CLI\Formatter; /** * Retrieves details on the site's registered post types. * * Get information on WordPress' built-in and the site's [custom post types](https://developer.wordpress.org/plugins/post-types/). * * ## EXAMPLES * * # Get details about a post type * $ wp post-type get page --fields=name,label,hierarchical --format=json * {"name":"page","label":"Pages","hierarchical":true} * * # List post types with 'post' capability type * $ wp post-type list --capability_type=post --fields=name,public * +---------------+--------+ * | name | public | * +---------------+--------+ * | post | 1 | * | attachment | 1 | * | revision | | * | nav_menu_item | | * +---------------+--------+ * * @package wp-cli */ class Post_Type_Command extends WP_CLI_Command { private $fields = array( 'name', 'label', 'description', 'hierarchical', 'public', 'capability_type', ); /** * Gets the post counts for each supplied post type. * * @param array $post_types Post types to fetch counts for. * @return array Associative array of post counts keyed by post type. */ protected function get_counts( $post_types ) { global $wpdb; if ( count( $post_types ) <= 0 ) { return []; } $query = $wpdb->prepare( "SELECT `post_type`, COUNT(*) AS `count` FROM $wpdb->posts WHERE `post_type` IN (" . implode( ',', array_fill( 0, count( $post_types ), '%s' ) ) . ') GROUP BY `post_type`', $post_types ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- $query is already prepared above. $counts = $wpdb->get_results( $query ); // Make sure there's a count for every item. $counts = array_merge( array_fill_keys( $post_types, 0 ), wp_list_pluck( $counts, 'count', 'post_type' ) ); return $counts; } /** * Lists registered post types. * * ## OPTIONS * * [--<field>=<value>] * : Filter by one or more fields (see get_post_types() first parameter for a list of available fields). * * [--field=<field>] * : Prints the value of a single field for each post type. * * [--fields=<fields>] * : Limit the output to specific post type fields. * * [--format=<format>] * : Render output in a particular format. * --- * default: table * options: * - table * - csv * - json * - count * - yaml * --- * * ## AVAILABLE FIELDS * * These fields will be displayed by default for each post type: * * * name * * label * * description * * hierarchical * * public * * capability_type * * These fields are optionally available: * * * count * * ## EXAMPLES * * # List registered post types * $ wp post-type list --format=csv * name,label,description,hierarchical,public,capability_type * post,Posts,,,1,post * page,Pages,,1,1,page * attachment,Media,,,1,post * revision,Revisions,,,,post * nav_menu_item,"Navigation Menu Items",,,,post * * # List post types with 'post' capability type * $ wp post-type list --capability_type=post --fields=name,public * +---------------+--------+ * | name | public | * +---------------+--------+ * | post | 1 | * | attachment | 1 | * | revision | | * | nav_menu_item | | * +---------------+--------+ * * @subcommand list */ public function list_( $args, $assoc_args ) { $formatter = $this->get_formatter( $assoc_args ); $fields = $formatter->fields; $types = get_post_types( $assoc_args, 'objects' ); $counts = []; if ( count( $types ) > 0 && in_array( 'count', $fields, true ) ) { $counts = $this->get_counts( wp_list_pluck( $types, 'name' ) ); } $types = array_map( function( $type ) use ( $counts ) { $type->count = isset( $counts[ $type->name ] ) ? $counts[ $type->name ] : 0; return $type; }, $types ); $formatter->display_items( $types ); } /** * Gets details about a registered post type. * * ## OPTIONS * * <post-type> * : Post type slug * * [--field=<field>] * : Instead of returning the whole taxonomy, returns the value of a single field. * * [--fields=<fields>] * : Limit the output to specific fields. Defaults to all fields. * * [--format=<format>] * : Render output in a particular format. * --- * default: table * options: * - table * - csv * - json * - yaml * --- * * ## AVAILABLE FIELDS * * These fields will be displayed by default for the specified post type: * * * name * * label * * description * * hierarchical * * public * * capability_type * * labels * * cap * * supports * * These fields are optionally available: * * * count * * ## EXAMPLES * * # Get details about the 'page' post type. * $ wp post-type get page --fields=name,label,hierarchical --format=json * {"name":"page","label":"Pages","hierarchical":true} */ public function get( $args, $assoc_args ) { $post_type = get_post_type_object( $args[0] ); if ( ! $post_type ) { WP_CLI::error( "Post type {$args[0]} doesn't exist." ); } if ( empty( $assoc_args['fields'] ) ) { $default_fields = array_merge( $this->fields, array( 'labels', 'cap', 'supports', ) ); $assoc_args['fields'] = $default_fields; } $formatter = $this->get_formatter( $assoc_args ); $fields = $formatter->fields; $count = 0; if ( in_array( 'count', $fields, true ) ) { $count = $this->get_counts( [ $post_type->name ] ); $count = $count[ $post_type->name ]; } $data = array( 'name' => $post_type->name, 'label' => $post_type->label, 'description' => $post_type->description, 'hierarchical' => $post_type->hierarchical, 'public' => $post_type->public, 'capability_type' => $post_type->capability_type, 'labels' => $post_type->labels, 'cap' => $post_type->cap, 'supports' => get_all_post_type_supports( $post_type->name ), 'count' => $count, ); $formatter->display_item( $data ); } private function get_formatter( &$assoc_args ) { return new Formatter( $assoc_args, $this->fields, 'post-type' ); } } <?php use WP_CLI\Utils; if ( ! class_exists( 'WP_CLI' ) ) { return; } $wpcli_entity_autoloader = dirname( __FILE__ ) . '/vendor/autoload.php'; if ( file_exists( $wpcli_entity_autoloader ) ) { require_once $wpcli_entity_autoloader; } WP_CLI::add_command( 'comment', 'Comment_Command' ); WP_CLI::add_command( 'comment meta', 'Comment_Meta_Command' ); WP_CLI::add_command( 'menu', 'Menu_Command' ); WP_CLI::add_command( 'menu item', 'Menu_Item_Command' ); WP_CLI::add_command( 'menu location', 'Menu_Location_Command' ); WP_CLI::add_command( 'network meta', 'Network_Meta_Command', array( 'before_invoke' => function () { if ( ! is_multisite() ) { WP_CLI::error( 'This is not a multisite installation.' ); } }, ) ); WP_CLI::add_command( 'option', 'Option_Command' ); WP_CLI::add_command( 'post', 'Post_Command' ); WP_CLI::add_command( 'post meta', 'Post_Meta_Command' ); WP_CLI::add_command( 'post term', 'Post_Term_Command' ); WP_CLI::add_command( 'post-type', 'Post_Type_Command' ); WP_CLI::add_command( 'site', 'Site_Command' ); WP_CLI::add_command( 'site meta', 'Site_Meta_Command', array( 'before_invoke' => function() { if ( ! is_multisite() ) { WP_CLI::error( 'This is not a multisite installation.' ); } if ( ! function_exists( 'is_site_meta_supported' ) || ! is_site_meta_supported() ) { WP_CLI::error( sprintf( 'The %s table is not installed. Please run the network database upgrade.', $GLOBALS['wpdb']->blogmeta ) ); } }, ) ); WP_CLI::add_command( 'site option', 'Site_Option_Command', array( 'before_invoke' => function() { if ( ! is_multisite() ) { WP_CLI::error( 'This is not a multisite installation.' ); } }, ) ); WP_CLI::add_command( 'taxonomy', 'Taxonomy_Command' ); WP_CLI::add_command( 'term', 'Term_Command' ); WP_CLI::add_command( 'term meta', 'Term_Meta_Command', array( 'before_invoke' => function() { if ( Utils\wp_version_compare( '4.4', '<' ) ) { WP_CLI::error( 'Requires WordPress 4.4 or greater.' ); } }, ) ); WP_CLI::add_command( 'user', 'User_Command' ); WP_CLI::add_command( 'user application-password', 'User_Application_Password_Command', array( 'before_invoke' => function() { if ( Utils\wp_version_compare( '5.6', '<' ) ) { WP_CLI::error( 'Requires WordPress 5.6 or greater.' ); } }, ) ); WP_CLI::add_command( 'user meta', 'User_Meta_Command' ); WP_CLI::add_command( 'user session', 'User_Session_Command', array( 'before_invoke' => function() { if ( Utils\wp_version_compare( '4.0', '<' ) ) { WP_CLI::error( 'Requires WordPress 4.0 or greater.' ); } }, ) ); WP_CLI::add_command( 'user term', 'User_Term_Command' ); if ( class_exists( 'WP_CLI\Dispatcher\CommandNamespace' ) ) { WP_CLI::add_command( 'network', 'Network_Namespace' ); } <?php namespace WP_CLI\I18n; use Exception; use Gettext\Extractors\PhpCode; use Gettext\Translations; use WP_CLI; final class PhpCodeExtractor extends PhpCode { use IterableCodeExtractor; public static $options = [ 'extractComments' => [ 'translators', 'Translators' ], 'constants' => [], 'functions' => [ '__' => 'text_domain', 'esc_attr__' => 'text_domain', 'esc_html__' => 'text_domain', 'esc_xml__' => 'text_domain', '_e' => 'text_domain', 'esc_attr_e' => 'text_domain', 'esc_html_e' => 'text_domain', 'esc_xml_e' => 'text_domain', '_x' => 'text_context_domain', '_ex' => 'text_context_domain', 'esc_attr_x' => 'text_context_domain', 'esc_html_x' => 'text_context_domain', 'esc_xml_x' => 'text_context_domain', '_n' => 'single_plural_number_domain', '_nx' => 'single_plural_number_context_domain', '_n_noop' => 'single_plural_domain', '_nx_noop' => 'single_plural_context_domain', // Compat. '_' => 'gettext', // Same as 'text_domain'. // Deprecated. '_c' => 'text_domain', '_nc' => 'single_plural_number_domain', '__ngettext' => 'single_plural_number_domain', '__ngettext_noop' => 'single_plural_domain', ], ]; protected static $functionsScannerClass = 'WP_CLI\I18n\PhpFunctionsScanner'; /** * {@inheritdoc} */ public static function fromString( $string, Translations $translations, array $options = [] ) { WP_CLI::debug( "Parsing file {$options['file']}", 'make-pot' ); try { static::fromStringMultiple( $string, [ $translations ], $options ); } catch ( Exception $exception ) { WP_CLI::debug( sprintf( 'Could not parse file %1$s: %2$s', $options['file'], $exception->getMessage() ), 'make-pot' ); } } } <?php namespace WP_CLI\I18n; use Gettext\Extractors\Extractor; use Gettext\Extractors\ExtractorInterface; use Gettext\Translations; use WP_CLI; final class BlockExtractor extends Extractor implements ExtractorInterface { use IterableCodeExtractor; /** * @inheritdoc */ public static function fromString( $string, Translations $translations, array $options = [] ) { $file = $options['file']; WP_CLI::debug( "Parsing file {$file}", 'make-pot' ); $file_data = json_decode( $string, true ); if ( null === $file_data ) { WP_CLI::debug( sprintf( 'Could not parse file %1$s: error code %2$s', $file, json_last_error() ), 'make-pot' ); return; } $domain = isset( $file_data['textdomain'] ) ? $file_data['textdomain'] : null; // Always allow missing domain or when --ignore-domain is used, but skip if domains don't match. if ( null !== $translations->getDomain() && null !== $domain && $domain !== $translations->getDomain() ) { return; } $add_reference = ! empty( $options['addReferences'] ); foreach ( $file_data as $key => $original ) { switch ( $key ) { case 'title': case 'description': $translation = $translations->insert( sprintf( 'block %s', $key ), $original ); if ( $add_reference ) { $translation->addReference( $file ); } break; case 'keywords': if ( ! is_array( $original ) ) { continue 2; } foreach ( $original as $msg ) { $translation = $translations->insert( 'block keyword', $msg ); if ( $add_reference ) { $translation->addReference( $file ); } } break; case 'styles': if ( ! is_array( $original ) ) { continue 2; } foreach ( $original as $msg ) { $translation = $translations->insert( 'block style label', $msg['label'] ); if ( $add_reference ) { $translation->addReference( $file ); } } } } } } <?php namespace WP_CLI\I18n; use Gettext\Extractors\JsCode; use Gettext\Translations; use Peast\Syntax\Exception as PeastException; use WP_CLI; final class MapCodeExtractor extends JsCode { use IterableCodeExtractor; public static $options = [ 'extractComments' => [ 'translators', 'Translators' ], 'constants' => [], 'functions' => [ '__' => 'text_domain', '_x' => 'text_context_domain', '_n' => 'single_plural_number_domain', '_nx' => 'single_plural_number_context_domain', ], ]; /** * {@inheritdoc} */ public static function fromString( $string, Translations $translations, array $options = [] ) { if ( ! array_key_exists( 'file', $options ) || substr( $options['file'], -7 ) !== '.js.map' ) { return; } $options['file'] = substr( $options['file'], 0, -7 ) . '.js'; try { $options += static::$options; $map_object = json_decode( $string ); if ( ! isset( $map_object->sourcesContent ) || ! is_array( $map_object->sourcesContent ) ) { return; } $string = implode( "\n", $map_object->sourcesContent ); WP_CLI::debug( "Parsing file {$options['file']}", 'make-pot' ); $functions = new JsFunctionsScanner( $string ); $functions->enableCommentsExtraction( $options['extractComments'] ); $functions->saveGettextFunctions( $translations, $options ); } catch ( PeastException $e ) { WP_CLI::debug( sprintf( 'Could not parse file %1$s.map: %2$s (line %3$d, column %4$d in the concatenated sourcesContent)', $options['file'], $e->getMessage(), $e->getPosition()->getLine(), $e->getPosition()->getColumn() ), 'make-pot' ); } } } <?php namespace WP_CLI\I18n; use Gettext\Translation; use Gettext\Translations; use RecursiveCallbackFilterIterator; use RecursiveDirectoryIterator; use RecursiveIteratorIterator; use SplFileInfo; use WP_CLI; use WP_CLI\Utils; trait IterableCodeExtractor { private static $dir = ''; /** * Extract the translations from a file. * * @param array|string $file_or_files A path of a file or files * @param Translations $translations The translations instance to append the new translations. * @param array $options { * Optional. An array of options passed down to static::fromString() * * @type bool $wpExtractTemplates Extract 'Template Name' headers in theme files. Default 'false'. * @type array $restrictFileNames Skip all files which are not included in this array. * } * @return null */ public static function fromFile( $file_or_files, Translations $translations, array $options = [] ) { foreach ( static::getFiles( $file_or_files ) as $file ) { if ( ! empty( $options['restrictFileNames'] ) ) { $basename = Utils\basename( $file ); if ( ! in_array( $basename, $options['restrictFileNames'], true ) ) { continue; } } // Make sure a relative file path is added as a comment. $options['file'] = ltrim( str_replace( static::$dir, '', Utils\normalize_path( $file ) ), '/' ); $string = file_get_contents( $file ); if ( ! $string ) { WP_CLI::debug( sprintf( 'Could not load file %1s', $file ), 'make-pot' ); continue; } if ( ! empty( $options['wpExtractTemplates'] ) ) { $headers = MakePotCommand::get_file_data_from_string( $string, [ 'Template Name' => 'Template Name' ] ); if ( ! empty( $headers['Template Name'] ) ) { $translation = new Translation( '', $headers['Template Name'] ); $translation->addExtractedComment( 'Template Name of the theme' ); $translations[] = $translation; } } static::fromString( $string, $translations, $options ); } } /** * Extract the translations from a file. * * @param string $dir Root path to start the recursive traversal in. * @param Translations $translations The translations instance to append the new translations. * @param array $options { * Optional. An array of options passed down to static::fromString() * * @type bool $wpExtractTemplates Extract 'Template Name' headers in theme files. Default 'false'. * @type array $exclude A list of path to exclude. Default []. * @type array $extensions A list of extensions to process. Default []. * } * @return null */ public static function fromDirectory( $dir, Translations $translations, array $options = [] ) { $dir = Utils\normalize_path( $dir ); static::$dir = $dir; $include = isset( $options['include'] ) ? $options['include'] : []; $exclude = isset( $options['exclude'] ) ? $options['exclude'] : []; $files = static::getFilesFromDirectory( $dir, $include, $exclude, $options['extensions'] ); if ( ! empty( $files ) ) { static::fromFile( $files, $translations, $options ); } static::$dir = ''; } /** * Determines whether a file is valid based on given matchers. * * @param SplFileInfo $file File or directory. * @param array $matchers List of files and directories to match. * @return int How strongly the file is matched. */ protected static function calculateMatchScore( SplFileInfo $file, array $matchers = [] ) { if ( empty( $matchers ) ) { return 0; } if ( in_array( $file->getBasename(), $matchers, true ) ) { return 10; } // Check for more complex paths, e.g. /some/sub/folder. $root_relative_path = str_replace( static::$dir, '', $file->getPathname() ); foreach ( $matchers as $path_or_file ) { $pattern = preg_quote( str_replace( '*', '__wildcard__', $path_or_file ), '/' ); $pattern = '(^|/)' . str_replace( '__wildcard__', '(.+)', $pattern ); // Base score is the amount of nested directories, discounting wildcards. $base_score = count( array_filter( explode( '/', $path_or_file ), static function ( $component ) { return '*' !== $component; } ) ); if ( 0 === $base_score ) { // If the matcher is simply * it gets a score above the implicit score but below 1. $base_score = 0.2; } // If the matcher contains no wildcards and matches the end of the path. if ( false === strpos( $path_or_file, '*' ) && false !== mb_ereg( $pattern . '$', $root_relative_path ) ) { return $base_score * 10; } // If the matcher matches the end of the path or a full directory contained. if ( false !== mb_ereg( $pattern . '(/|$)', $root_relative_path ) ) { return $base_score; } } return 0; } /** * Determines whether or not a directory has children that may be matched. * * @param SplFileInfo $dir Directory. * @param array $matchers List of files and directories to match. * @return bool Whether or not there are any matchers for children of this directory. */ protected static function containsMatchingChildren( SplFileInfo $dir, array $matchers = [] ) { if ( empty( $matchers ) ) { return false; } /** @var string $root_relative_path */ $root_relative_path = str_replace( static::$dir, '', $dir->getPathname() ); foreach ( $matchers as $path_or_file ) { // If the matcher contains no wildcards and the path matches the start of the matcher. if ( '' !== $root_relative_path && false === strpos( $path_or_file, '*' ) && 0 === strpos( $path_or_file . '/', $root_relative_path ) ) { return true; } $base = current( explode( '*', $path_or_file ) ); // If start of the path matches the start of the matcher until the first wildcard. // Or the start of the matcher until the first wildcard matches the start of the path. if ( ( '' !== $root_relative_path && 0 === strpos( $base, $root_relative_path ) ) || ( '' !== $base && 0 === strpos( $root_relative_path, $base ) ) ) { return true; } } return false; } /** * Recursively gets all PHP files within a directory. * * @param string $dir A path of a directory. * @param array $include List of files and directories to include. * @param array $exclude List of files and directories to skip. * @param array $extensions List of filename extensions to process. * * @return array File list. */ public static function getFilesFromDirectory( $dir, array $include = [], array $exclude = [], $extensions = [] ) { $filtered_files = []; $files = new RecursiveIteratorIterator( new RecursiveCallbackFilterIterator( new RecursiveDirectoryIterator( $dir, RecursiveDirectoryIterator::SKIP_DOTS | RecursiveDirectoryIterator::UNIX_PATHS | RecursiveDirectoryIterator::FOLLOW_SYMLINKS ), static function ( $file, $key, $iterator ) use ( $include, $exclude, $extensions ) { /** @var RecursiveCallbackFilterIterator $iterator */ /** @var SplFileInfo $file */ // Normalize include and exclude paths. $include = array_map( 'static::trim_leading_slash', $include ); $exclude = array_map( 'static::trim_leading_slash', $exclude ); // If no $include is passed everything gets the weakest possible matching score. $inclusion_score = empty( $include ) ? 0.1 : static::calculateMatchScore( $file, $include ); $exclusion_score = static::calculateMatchScore( $file, $exclude ); // Always include directories that aren't excluded. if ( 0 === $exclusion_score && $iterator->hasChildren() ) { return true; } if ( ( 0 === $inclusion_score || $exclusion_score > $inclusion_score ) && $iterator->hasChildren() ) { // Always include directories that may have matching children even if they are excluded. return static::containsMatchingChildren( $file, $include ); } if ( ! $file->isFile() || ! in_array( $file->getExtension(), $extensions, true ) ) { return false; } return $inclusion_score > $exclusion_score; } ), RecursiveIteratorIterator::CHILD_FIRST ); foreach ( $files as $file ) { /** @var SplFileInfo $file */ if ( ! $file->isFile() || ! in_array( $file->getExtension(), $extensions, true ) ) { continue; } $filtered_files[] = Utils\normalize_path( $file->getPathname() ); } sort( $filtered_files, SORT_NATURAL | SORT_FLAG_CASE ); return $filtered_files; } /** * Trim leading slash from a path. * * @param string $path Path to trim. * @return string Trimmed path. */ private static function trim_leading_slash( $path ) { return ltrim( $path, '/' ); } } <?php namespace WP_CLI\I18n; use Gettext\Utils\JsFunctionsScanner as GettextJsFunctionsScanner; use Gettext\Utils\ParsedComment; use Peast\Peast; use Peast\Syntax\Node; use Peast\Traverser; final class JsFunctionsScanner extends GettextJsFunctionsScanner { /** * If not false, comments will be extracted. * * @var string|false|array */ private $extract_comments = false; /** * Holds a list of source code comments already added to a string. * * Prevents associating the same comment to multiple strings. * * @var Node\Comment[] $comments_cache */ private $comments_cache = []; /** * Enable extracting comments that start with a tag (if $tag is empty all the comments will be extracted). * * @param mixed $tag */ public function enableCommentsExtraction( $tag = '' ) { $this->extract_comments = $tag; } /** * Disable comments extraction. */ public function disableCommentsExtraction() { $this->extract_comments = false; } /** * {@inheritdoc} */ public function saveGettextFunctions( $translations, array $options ) { // Ignore multiple translations for now. // @todo Add proper support for multiple translations. if ( is_array( $translations ) ) { $translations = $translations[0]; } $peast_options = [ 'sourceType' => Peast::SOURCE_TYPE_MODULE, 'comments' => false !== $this->extract_comments, 'jsx' => true, ]; $ast = Peast::latest( $this->code, $peast_options )->parse(); $traverser = new Traverser(); $all_comments = []; /** * Traverse through JS code to find and extract gettext functions. * * Make sure translator comments in front of variable declarations * and inside nested call expressions are available when parsing the function call. */ $traverser->addFunction( function ( $node ) use ( &$translations, $options, &$all_comments ) { $functions = $options['functions']; $file = $options['file']; $add_reference = ! empty( $options['addReferences'] ); /** @var Node\Node $node */ foreach ( $node->getLeadingComments() as $comment ) { $all_comments[] = $comment; } /** @var Node\CallExpression $node */ if ( 'CallExpression' !== $node->getType() ) { return; } $callee = $this->resolveExpressionCallee( $node ); if ( ! $callee || ! isset( $functions[ $callee['name'] ] ) ) { return; } /** @var Node\CallExpression $node */ foreach ( $node->getArguments() as $argument ) { // Support nested function calls. $argument->setLeadingComments( $argument->getLeadingComments() + $node->getLeadingComments() ); } foreach ( $callee['comments'] as $comment ) { $all_comments[] = $comment; } $domain = null; $original = null; $context = null; $plural = null; $args = []; /** @var Node\Node $argument */ foreach ( $node->getArguments() as $argument ) { foreach ( $argument->getLeadingComments() as $comment ) { $all_comments[] = $comment; } if ( 'Identifier' === $argument->getType() || 'Expression' === substr( $argument->getType(), -strlen( 'Expression' ) ) ) { $args[] = ''; // The value doesn't matter as it's unused. continue; } if ( 'Literal' === $argument->getType() ) { /** @var Node\Literal $argument */ $args[] = $argument->getValue(); continue; } if ( 'TemplateLiteral' === $argument->getType() && 0 === count( $argument->getExpressions() ) ) { /** @var Node\TemplateLiteral $argument */ /** @var Node\TemplateElement[] $parts */ // Since there are no expressions within the TemplateLiteral, there is only one TemplateElement. $parts = $argument->getParts(); $args[] = $parts[0]->getValue(); continue; } // If we reach this, an unsupported argument type has been encountered. // Do not try to parse this function call at all. return; } switch ( $functions[ $callee['name'] ] ) { case 'text_domain': case 'gettext': list( $original, $domain ) = array_pad( $args, 2, null ); break; case 'text_context_domain': list( $original, $context, $domain ) = array_pad( $args, 3, null ); break; case 'single_plural_number_domain': list( $original, $plural, $number, $domain ) = array_pad( $args, 4, null ); break; case 'single_plural_number_context_domain': list( $original, $plural, $number, $context, $domain ) = array_pad( $args, 5, null ); break; } if ( '' === (string) $original ) { return; } if ( $domain !== $translations->getDomain() && null !== $translations->getDomain() ) { return; } if ( isset( $options['line'] ) ) { $line = $options['line']; } else { $line = $node->getLocation()->getStart()->getLine(); } $translation = $translations->insert( $context, $original, $plural ); if ( $add_reference ) { $translation->addReference( $file, $line ); } /** @var Node\Comment $comment */ foreach ( $all_comments as $comment ) { // Comments should be before the translation. if ( ! $this->commentPrecedesNode( $comment, $node ) ) { continue; } if ( in_array( $comment, $this->comments_cache, true ) ) { continue; } $parsed_comment = ParsedComment::create( $comment->getRawText(), $comment->getLocation()->getStart()->getLine() ); $prefixes = array_filter( (array) $this->extract_comments ); if ( $parsed_comment->checkPrefixes( $prefixes ) ) { $translation->addExtractedComment( $parsed_comment->getComment() ); $this->comments_cache[] = $comment; } } if ( isset( $parsed_comment ) ) { $all_comments = []; } } ); /** * Traverse through JS code contained within eval() to find and extract gettext functions. */ $scanner = $this; $traverser->addFunction( function ( $node ) use ( &$translations, $options, $scanner ) { /** @var Node\CallExpression $node */ if ( 'CallExpression' !== $node->getType() ) { return; } $callee = $this->resolveExpressionCallee( $node ); if ( ! $callee || 'eval' !== $callee['name'] ) { return; } $eval_contents = ''; /** @var Node\Node $argument */ foreach ( $node->getArguments() as $argument ) { if ( 'Literal' === $argument->getType() ) { /** @var Node\Literal $argument */ $eval_contents = $argument->getValue(); break; } } if ( ! $eval_contents ) { return; } // Override the line location to be that of the eval(). $options['line'] = $node->getLocation()->getStart()->getLine(); $class = get_class( $scanner ); $evals = new $class( $eval_contents ); $evals->enableCommentsExtraction( $options['extractComments'] ); $evals->saveGettextFunctions( $translations, $options ); } ); $traverser->traverse( $ast ); } /** * Resolve the callee of a call expression using known formats. * * @param Node\CallExpression $node The call expression whose callee to resolve. * * @return array|bool Array containing the name and comments of the identifier if resolved. False if not. */ private function resolveExpressionCallee( Node\CallExpression $node ) { $callee = $node->getCallee(); // If the callee is a simple identifier it can simply be returned. // For example: __( "translation" ). if ( 'Identifier' === $callee->getType() ) { return [ 'name' => $callee->getName(), 'comments' => $callee->getLeadingComments(), ]; } // If the callee is a member expression resolve it to the property. // For example: wp.i18n.__( "translation" ) or u.__( "translation" ). if ( 'MemberExpression' === $callee->getType() && 'Identifier' === $callee->getProperty()->getType() ) { // Make sure to unpack wp.i18n which is a nested MemberExpression. $comments = 'MemberExpression' === $callee->getObject()->getType() ? $callee->getObject()->getObject()->getLeadingComments() : $callee->getObject()->getLeadingComments(); return [ 'name' => $callee->getProperty()->getName(), 'comments' => $comments, ]; } // If the callee is a call expression as created by Webpack resolve it. // For example: Object(u.__)( "translation" ). if ( 'CallExpression' === $callee->getType() && 'Identifier' === $callee->getCallee()->getType() && 'Object' === $callee->getCallee()->getName() && [] !== $callee->getArguments() && 'MemberExpression' === $callee->getArguments()[0]->getType() ) { $property = $callee->getArguments()[0]->getProperty(); // Matches minified webpack statements: Object(u.__)( "translation" ). if ( 'Identifier' === $property->getType() ) { return [ 'name' => $property->getName(), 'comments' => $callee->getCallee()->getLeadingComments(), ]; } // Matches unminified webpack statements: // Object(_wordpress_i18n__WEBPACK_IMPORTED_MODULE_7__["__"])( "translation" ); if ( 'Literal' === $property->getType() ) { $name = $property->getValue(); // Matches mangled webpack statement: // Object(_wordpress_i18n__WEBPACK_IMPORTED_MODULE_7__[/* __ */ "a"])( "translation" ); $leading_property_comments = $property->getLeadingComments(); if ( count( $leading_property_comments ) === 1 && $leading_property_comments[0]->getKind() === 'multiline' ) { $name = trim( $leading_property_comments[0]->getText() ); } return [ 'name' => $name, 'comments' => $callee->getCallee()->getLeadingComments(), ]; } } // If the callee is an indirect function call as created by babel, resolve it. // For example: `(0, u.__)( "translation" )`. if ( 'ParenthesizedExpression' === $callee->getType() && 'SequenceExpression' === $callee->getExpression()->getType() && 2 === count( $callee->getExpression()->getExpressions() ) && 'Literal' === $callee->getExpression()->getExpressions()[0]->getType() && [] !== $node->getArguments() ) { // Matches any general indirect function call: `(0, __)( "translation" )`. if ( 'Identifier' === $callee->getExpression()->getExpressions()[1]->getType() ) { return [ 'name' => $callee->getExpression()->getExpressions()[1]->getName(), 'comments' => $callee->getLeadingComments(), ]; } // Matches indirect function calls used by babel for module imports: `(0, _i18n.__)( "translation" )`. if ( 'MemberExpression' === $callee->getExpression()->getExpressions()[1]->getType() ) { $property = $callee->getExpression()->getExpressions()[1]->getProperty(); if ( 'Identifier' === $property->getType() ) { return [ 'name' => $property->getName(), 'comments' => $callee->getLeadingComments(), ]; } } } // Unknown format. return false; } /** * Returns wether or not a comment precedes a node. * The comment must be before the node and on the same line or the one before. * * @param Node\Comment $comment The comment. * @param Node\Node $node The node. * * @return bool Whether or not the comment precedes the node. */ private function commentPrecedesNode( Node\Comment $comment, Node\Node $node ) { // Comments should be on the same or an earlier line than the translation. if ( $node->getLocation()->getStart()->getLine() - $comment->getLocation()->getEnd()->getLine() > 1 ) { return false; } // Comments on the same line should be before the translation. if ( $node->getLocation()->getStart()->getLine() === $comment->getLocation()->getEnd()->getLine() && $node->getLocation()->getStart()->getColumn() < $comment->getLocation()->getStart()->getColumn() ) { return false; } return true; } } <?php namespace WP_CLI\I18n; use Exception; use Gettext\Extractors\JsCode; use Gettext\Translations; use Peast\Syntax\Exception as PeastException; use WP_CLI; final class JsCodeExtractor extends JsCode { use IterableCodeExtractor; public static $options = [ 'extractComments' => [ 'translators', 'Translators' ], 'constants' => [], 'functions' => [ '__' => 'text_domain', '_x' => 'text_context_domain', '_n' => 'single_plural_number_domain', '_nx' => 'single_plural_number_context_domain', ], ]; protected static $functionsScannerClass = 'WP_CLI\I18n\JsFunctionsScanner'; /** * @inheritdoc */ public static function fromString( $string, Translations $translations, array $options = [] ) { WP_CLI::debug( "Parsing file {$options['file']}", 'make-pot' ); try { static::fromStringMultiple( $string, [ $translations ], $options ); } catch ( PeastException $exception ) { WP_CLI::debug( sprintf( 'Could not parse file %1$s: %2$s (line %3$d, column %4$d)', $options['file'], $exception->getMessage(), $exception->getPosition()->getLine(), $exception->getPosition()->getColumn() ), 'make-pot' ); } catch ( Exception $exception ) { WP_CLI::debug( sprintf( 'Could not parse file %1$s: %2$s', $options['file'], $exception->getMessage() ), 'make-pot' ); } } /** * @inheritDoc */ public static function fromStringMultiple( $string, array $translations, array $options = [] ) { $options += static::$options; /** @var JsFunctionsScanner $functions */ $functions = new static::$functionsScannerClass( $string ); $functions->enableCommentsExtraction( $options['extractComments'] ); $functions->saveGettextFunctions( $translations, $options ); } } <?php namespace WP_CLI\I18n; use WP_CLI\Dispatcher\CommandNamespace as BaseCommandNamespace; /** * Provides internationalization tools for WordPress projects. * * ## EXAMPLES * * # Create a POT file for the WordPress plugin/theme in the current directory * $ wp i18n make-pot . languages/my-plugin.pot * * @when before_wp_load */ class CommandNamespace extends BaseCommandNamespace { } <?php namespace WP_CLI\I18n; use Gettext\Utils\PhpFunctionsScanner as GettextPhpFunctionsScanner; class PhpFunctionsScanner extends GettextPhpFunctionsScanner { /** * {@inheritdoc} */ public function saveGettextFunctions( $translations, array $options ) { // Ignore multiple translations for now. // @todo Add proper support for multiple translations. if ( is_array( $translations ) ) { $translations = $translations[0]; } $functions = $options['functions']; $file = $options['file']; $add_reference = ! empty( $options['addReferences'] ); foreach ( $this->getFunctions( $options['constants'] ) as $function ) { list( $name, $line, $args ) = $function; if ( ! isset( $functions[ $name ] ) ) { continue; } $original = null; $domain = null; $context = null; $plural = null; switch ( $functions[ $name ] ) { case 'text_domain': case 'gettext': list( $original, $domain ) = array_pad( $args, 2, null ); break; case 'text_context_domain': list( $original, $context, $domain ) = array_pad( $args, 3, null ); break; case 'single_plural_number_domain': list( $original, $plural, $number, $domain ) = array_pad( $args, 4, null ); break; case 'single_plural_number_context_domain': list( $original, $plural, $number, $context, $domain ) = array_pad( $args, 5, null ); break; case 'single_plural_domain': list( $original, $plural, $domain ) = array_pad( $args, 3, null ); break; case 'single_plural_context_domain': list( $original, $plural, $context, $domain ) = array_pad( $args, 4, null ); break; default: // Should never happen. \WP_CLI::error( sprintf( "Internal error: unknown function map '%s' for '%s'.", $functions[ $name ], $name ) ); } if ( '' === (string) $original ) { continue; } if ( $domain !== $translations->getDomain() && null !== $translations->getDomain() ) { continue; } $translation = $translations->insert( $context, $original, $plural ); if ( $add_reference ) { $translation = $translation->addReference( $file, $line ); } if ( isset( $function[3] ) ) { foreach ( $function[3] as $extracted_comment ) { $translation = $translation->addExtractedComment( $extracted_comment ); } } } } } <?php namespace WP_CLI\I18n; use Gettext\Extractors\Extractor; use Gettext\Extractors\ExtractorInterface; use Gettext\Translations; use WP_CLI; use WP_CLI\Utils; final class ThemeJsonExtractor extends Extractor implements ExtractorInterface { use IterableCodeExtractor; /** * Source URL from which to download the latest theme-i18n.json file. * * @var string */ const THEME_JSON_SOURCE = 'https://develop.svn.wordpress.org/trunk/src/wp-includes/theme-i18n.json'; /** * @inheritdoc */ public static function fromString( $string, Translations $translations, array $options = [] ) { $file = $options['file']; WP_CLI::debug( "Parsing file {$file}", 'make-pot' ); $theme_json = json_decode( $string, true ); if ( null === $theme_json ) { WP_CLI::debug( sprintf( 'Could not parse file %1$s: error code %2$s', $file, json_last_error() ), 'make-pot' ); return; } $fields = self::get_fields_to_translate(); foreach ( $fields as $field ) { $path = $field['path']; $key = $field['key']; $context = $field['context']; /* * We need to process the paths that include '*' separately. * One example of such a path would be: * [ 'settings', 'blocks', '*', 'color', 'palette' ] */ $nodes_to_iterate = array_keys( $path, '*', true ); if ( ! empty( $nodes_to_iterate ) ) { /* * At the moment, we only need to support one '*' in the path, so take it directly. * - base will be [ 'settings', 'blocks' ] * - data will be [ 'color', 'palette' ] */ $base_path = array_slice( $path, 0, $nodes_to_iterate[0] ); $data_path = array_slice( $path, $nodes_to_iterate[0] + 1 ); $base_tree = self::array_get( $theme_json, $base_path, [] ); foreach ( $base_tree as $node_data ) { $array_to_translate = self::array_get( $node_data, $data_path ); if ( null === $array_to_translate ) { continue; } foreach ( $array_to_translate as $item_to_translate ) { if ( empty( $item_to_translate[ $key ] ) ) { continue; } $translation = $translations->insert( $context, $item_to_translate[ $key ] ); $translation->addReference( $file ); } } } else { $array_to_translate = self::array_get( $theme_json, $path ); if ( null === $array_to_translate ) { continue; } foreach ( $array_to_translate as $item_to_translate ) { if ( empty( $item_to_translate[ $key ] ) ) { continue; } $translation = $translations->insert( $context, $item_to_translate[ $key ] ); $translation->addReference( $file ); } } } } /** * Given a remote URL, fetches it remotely and returns its content. * * Returns an empty string in case of error. * * @param string $url URL of the file to fetch. * * @return string Contents of the file. */ private static function remote_get( $url ) { if ( ! $url ) { return ''; } $headers = [ 'Content-type: application/json' ]; $options = [ 'halt_on_error' => false ]; $response = Utils\http_request( 'GET', $url, null, $headers, $options ); if ( ! $response->success || 200 > (int) $response->status_code || 300 <= $response->status_code ) { WP_CLI::debug( "Failed to download from URL {$url}", 'make-pot' ); return ''; } return trim( $response->body ); } /** * Returns a data structure to help setting up translations for theme.json data. * * [ * [ * 'path' => [ 'settings', 'color', 'palette' ], * 'key' => 'key-that-stores-the-string-to-translate', * 'context' => 'translation-context', * ], * [ * 'path' => 'etc', * 'key' => 'etc', * 'context' => 'etc', * ], * ] * * Ported from the core class `WP_Theme_JSON_Resolver`. * * @return array An array of theme.json fields that are translatable and the keys that are translatable. */ private static function get_fields_to_translate() { $json = self::remote_get( self::THEME_JSON_SOURCE ); if ( empty( $json ) ) { WP_CLI::debug( 'Remote file could not be accessed, will use local file as fallback', 'make-pot' ); $json = file_get_contents( __DIR__ . '/../assets/theme-i18n.json' ); } $file_structure = json_decode( $json, true ); if ( JSON_ERROR_NONE !== json_last_error() ) { WP_CLI::debug( 'Error when decoding theme-i18n.json file', 'make-pot' ); return []; } if ( ! is_array( $file_structure ) ) { return []; } return self::extract_paths_to_translate( $file_structure ); } /** * Converts a tree as in theme-i18.json file into a linear array * containing metadata to translate a theme.json file. * * For example, given this input: * * { * "settings": { * "*": { * "typography": { * "fontSizes": [ { "name": "Font size name" } ], * "fontStyles": [ { "name": "Font size name" } ] * } * } * } * } * * will return this output: * * [ * 0 => [ * 'path' => [ 'settings', '*', 'typography', 'fontSizes' ], * 'key' => 'name', * 'context' => 'Font size name' * ], * 1 => [ * 'path' => [ 'settings', '*', 'typography', 'fontStyles' ], * 'key' => 'name', * 'context' => 'Font style name' * ] * ] * * Ported from the core class `WP_Theme_JSON_Resolver`. * * @param array $i18n_partial A tree that follows the format of theme-i18n.json. * @param array $current_path Keeps track of the path as we walk down the given tree. * @return array A linear array containing the paths to translate. */ private static function extract_paths_to_translate( $i18n_partial, $current_path = [] ) { $result = []; foreach ( $i18n_partial as $property => $partial_child ) { if ( is_numeric( $property ) ) { foreach ( $partial_child as $key => $context ) { $result[] = [ 'path' => $current_path, 'key' => $key, 'context' => $context, ]; } return $result; } $result = array_merge( $result, self::extract_paths_to_translate( $partial_child, array_merge( $current_path, [ $property ] ) ) ); } return $result; } /** * Accesses an array in depth based on a path of keys. * * It is the PHP equivalent of JavaScript's `lodash.get()` and mirroring it may help other components * retain some symmetry between client and server implementations. * * Example usage: * * $array = [ * 'a' => [ * 'b' => [ * 'c' => 1, * ], * ], * ]; * array_get( $array, [ 'a', 'b', 'c' ] ); * * @param array $array An array from which we want to retrieve some information. * @param array $path An array of keys describing the path with which to retrieve information. * @param mixed $default The return value if the path does not exist within the array, * or if `$array` or `$path` are not arrays. * @return mixed The value from the path specified. */ private static function array_get( $array, $path, $default = null ) { // Confirm $path is valid. if ( ! is_array( $path ) || 0 === count( $path ) ) { return $default; } foreach ( $path as $path_element ) { if ( ! is_array( $array ) || ( ! is_string( $path_element ) && ! is_integer( $path_element ) && null !== $path_element ) || ! array_key_exists( $path_element, $array ) ) { return $default; } $array = $array[ $path_element ]; } return $array; } } <?php namespace WP_CLI\I18n; use Gettext\Extractors\Po; use Gettext\Merge; use Gettext\Translation; use Gettext\Translations; use Gettext\Utils\ParsedComment; use WP_CLI; use WP_CLI_Command; use WP_CLI\Utils; use DirectoryIterator; use IteratorIterator; class MakePotCommand extends WP_CLI_Command { /** * @var string */ protected $source; /** * @var string */ protected $destination; /** * @var array */ protected $merge = []; /** * @var Translations[] */ protected $exceptions = []; /** * @var bool */ protected $subtract_and_merge; /** * @var array */ protected $include = []; /** * @var array */ protected $exclude = [ 'node_modules', '.git', '.svn', '.CVS', '.hg', 'vendor', 'Gruntfile.js', 'webpack.config.js', '*.min.js' ]; /** * @var string */ protected $slug; /** * @var array */ protected $main_file_data = []; /** * @var bool */ protected $skip_js = false; /** * @var bool */ protected $skip_php = false; /** * @var bool */ protected $skip_block_json = false; /** * @var bool */ protected $skip_theme_json = false; /** * @var bool */ protected $skip_audit = false; /** * @var bool */ protected $location = true; /** * @var array */ protected $headers = []; /** * @var string */ protected $domain; /** * @var string */ protected $package_name; /** * @var string */ protected $file_comment; /** * @var string */ protected $project_type = 'generic'; /** * These Regexes copied from http://php.net/manual/en/function.sprintf.php#93552 * and adjusted for better precision and updated specs. */ const SPRINTF_PLACEHOLDER_REGEX = '/(?: (?<!%) # Don\'t match a literal % (%%). ( % # Start of placeholder. (?:[0-9]+\$)? # Optional ordering of the placeholders. [+-]? # Optional sign specifier. (?: (?:0|\'.)? # Optional padding specifier - excluding the space. -? # Optional alignment specifier. [0-9]* # Optional width specifier. (?:\.(?:[ 0]|\'.)?[0-9]+)? # Optional precision specifier with optional padding character. | # Only recognize the space as padding in combination with a width specifier. (?:[ ])? # Optional space padding specifier. -? # Optional alignment specifier. [0-9]+ # Width specifier. (?:\.(?:[ 0]|\'.)?[0-9]+)? # Optional precision specifier with optional padding character. ) [bcdeEfFgGosuxX] # Type specifier. ) )/x'; /** * "Unordered" means there's no position specifier: '%s', not '%2$s'. */ const UNORDERED_SPRINTF_PLACEHOLDER_REGEX = '/(?: (?<!%) # Don\'t match a literal % (%%). % # Start of placeholder. [+-]? # Optional sign specifier. (?: (?:0|\'.)? # Optional padding specifier - excluding the space. -? # Optional alignment specifier. [0-9]* # Optional width specifier. (?:\.(?:[ 0]|\'.)?[0-9]+)? # Optional precision specifier with optional padding character. | # Only recognize the space as padding in combination with a width specifier. (?:[ ])? # Optional space padding specifier. -? # Optional alignment specifier. [0-9]+ # Width specifier. (?:\.(?:[ 0]|\'.)?[0-9]+)? # Optional precision specifier with optional padding character. ) [bcdeEfFgGosuxX] # Type specifier. )/x'; /** * Create a POT file for a WordPress project. * * Scans PHP and JavaScript files for translatable strings, as well as theme stylesheets and plugin files * if the source directory is detected as either a plugin or theme. * * ## OPTIONS * * <source> * : Directory to scan for string extraction. * * [<destination>] * : Name of the resulting POT file. * * [--slug=<slug>] * : Plugin or theme slug. Defaults to the source directory's basename. * * [--domain=<domain>] * : Text domain to look for in the source code, unless the `--ignore-domain` option is used. * By default, the "Text Domain" header of the plugin or theme is used. * If none is provided, it falls back to the project slug. * * [--ignore-domain] * : Ignore the text domain completely and extract strings with any text domain. * * [--merge[=<paths>]] * : Comma-separated list of POT files whose contents should be merged with the extracted strings. * If left empty, defaults to the destination POT file. POT file headers will be ignored. * * [--subtract=<paths>] * : Comma-separated list of POT files whose contents should act as some sort of denylist for string extraction. * Any string which is found on that denylist will not be extracted. * This can be useful when you want to create multiple POT files from the same source directory with slightly * different content and no duplicate strings between them. * * [--subtract-and-merge] * : Whether source code references and comments from the generated POT file should be instead added to the POT file * used for subtraction. Warning: this modifies the files passed to `--subtract`! * * [--include=<paths>] * : Comma-separated list of files and paths that should be used for string extraction. * If provided, only these files and folders will be taken into account for string extraction. * For example, `--include="src,my-file.php` will ignore anything besides `my-file.php` and files in the `src` * directory. Simple glob patterns can be used, i.e. `--include=foo-*.php` includes any PHP file with the `foo-` * prefix. Leading and trailing slashes are ignored, i.e. `/my/directory/` is the same as `my/directory`. * * [--exclude=<paths>] * : Comma-separated list of files and paths that should be skipped for string extraction. * For example, `--exclude=".github,myfile.php` would ignore any strings found within `myfile.php` or the `.github` * folder. Simple glob patterns can be used, i.e. `--exclude=foo-*.php` excludes any PHP file with the `foo-` * prefix. Leading and trailing slashes are ignored, i.e. `/my/directory/` is the same as `my/directory`. The * following files and folders are always excluded: node_modules, .git, .svn, .CVS, .hg, vendor, *.min.js. * * [--headers=<headers>] * : Array in JSON format of custom headers which will be added to the POT file. Defaults to empty array. * * [--location] * : Whether to write `#: filename:line` lines. * Defaults to true, use `--no-location` to skip the removal. * Note that disabling this option makes it harder for technically skilled translators to understand each message’s context. * * [--skip-js] * : Skips JavaScript string extraction. Useful when this is done in another build step, e.g. through Babel. * * [--skip-php] * : Skips PHP string extraction. * * [--skip-block-json] * : Skips string extraction from block.json files. * * [--skip-theme-json] * : Skips string extraction from theme.json files. * * [--skip-audit] * : Skips string audit where it tries to find possible mistakes in translatable strings. Useful when running in an * automated environment. * * [--file-comment=<file-comment>] * : String that should be added as a comment to the top of the resulting POT file. * By default, a copyright comment is added for WordPress plugins and themes in the following manner: * * ``` * Copyright (C) 2018 Example Plugin Author * This file is distributed under the same license as the Example Plugin package. * ``` * * If a plugin or theme specifies a license in their main plugin file or stylesheet, the comment looks like * this: * * ``` * Copyright (C) 2018 Example Plugin Author * This file is distributed under the GPLv2. * ``` * * [--package-name=<name>] * : Name to use for package name in the resulting POT file's `Project-Id-Version` header. * Overrides plugin or theme name, if applicable. * * ## EXAMPLES * * # Create a POT file for the WordPress plugin/theme in the current directory * $ wp i18n make-pot . languages/my-plugin.pot * * # Create a POT file for the continents and cities list in WordPress core. * $ wp i18n make-pot . continents-and-cities.pot --include="wp-admin/includes/continents-cities.php" * --ignore-domain * * @when before_wp_load * * @throws WP_CLI\ExitException */ public function __invoke( $args, $assoc_args ) { $this->handle_arguments( $args, $assoc_args ); $translations = $this->extract_strings(); if ( ! $translations ) { WP_CLI::warning( 'No strings found' ); } $translations_count = count( $translations ); if ( 1 === $translations_count ) { WP_CLI::debug( sprintf( 'Extracted %d string', $translations_count ), 'make-pot' ); } else { WP_CLI::debug( sprintf( 'Extracted %d strings', $translations_count ), 'make-pot' ); } if ( ! PotGenerator::toFile( $translations, $this->destination ) ) { WP_CLI::error( 'Could not generate a POT file!' ); } WP_CLI::success( 'POT file successfully generated!' ); } /** * Process arguments from command-line in a reusable way. * * @throws WP_CLI\ExitException * * @param array $args Command arguments. * @param array $assoc_args Associative arguments. */ public function handle_arguments( $args, $assoc_args ) { $array_arguments = array( 'headers' ); $assoc_args = Utils\parse_shell_arrays( $assoc_args, $array_arguments ); $this->source = realpath( $args[0] ); $this->slug = Utils\get_flag_value( $assoc_args, 'slug', Utils\basename( $this->source ) ); $this->skip_js = Utils\get_flag_value( $assoc_args, 'skip-js', $this->skip_js ); $this->skip_php = Utils\get_flag_value( $assoc_args, 'skip-php', $this->skip_php ); $this->skip_block_json = Utils\get_flag_value( $assoc_args, 'skip-block-json', $this->skip_block_json ); $this->skip_theme_json = Utils\get_flag_value( $assoc_args, 'skip-theme-json', $this->skip_theme_json ); $this->skip_audit = Utils\get_flag_value( $assoc_args, 'skip-audit', $this->skip_audit ); $this->headers = Utils\get_flag_value( $assoc_args, 'headers', $this->headers ); $this->file_comment = Utils\get_flag_value( $assoc_args, 'file-comment' ); $this->package_name = Utils\get_flag_value( $assoc_args, 'package-name' ); $this->location = Utils\get_flag_value( $assoc_args, 'location', true ); $ignore_domain = Utils\get_flag_value( $assoc_args, 'ignore-domain', false ); if ( ! $this->source || ! is_dir( $this->source ) ) { WP_CLI::error( 'Not a valid source directory!' ); } $this->main_file_data = $this->get_main_file_data(); if ( $ignore_domain ) { WP_CLI::debug( 'Extracting all strings regardless of text domain', 'make-pot' ); } if ( ! $ignore_domain ) { $this->domain = $this->slug; if ( ! empty( $this->main_file_data['Text Domain'] ) ) { $this->domain = $this->main_file_data['Text Domain']; } $this->domain = Utils\get_flag_value( $assoc_args, 'domain', $this->domain ); WP_CLI::debug( sprintf( 'Extracting all strings with text domain "%s"', $this->domain ), 'make-pot' ); } // Determine destination. $this->destination = "{$this->source}/{$this->slug}.pot"; if ( ! empty( $this->main_file_data['Domain Path'] ) ) { // Domain Path inside source folder. $this->destination = sprintf( '%s/%s/%s.pot', $this->source, $this->unslashit( $this->main_file_data['Domain Path'] ), $this->slug ); } if ( isset( $args[1] ) ) { $this->destination = $args[1]; } WP_CLI::debug( sprintf( 'Destination: %s', $this->destination ), 'make-pot' ); // Two is_dir() checks in case of a race condition. if ( ! is_dir( dirname( $this->destination ) ) && ! mkdir( dirname( $this->destination ), 0777, true ) && ! is_dir( dirname( $this->destination ) ) ) { WP_CLI::error( 'Could not create destination directory!' ); } if ( isset( $assoc_args['merge'] ) ) { if ( true === $assoc_args['merge'] ) { $this->merge = [ $this->destination ]; } elseif ( ! empty( $assoc_args['merge'] ) ) { $this->merge = explode( ',', $assoc_args['merge'] ); } $this->merge = array_filter( $this->merge, function ( $file ) { if ( ! file_exists( $file ) ) { WP_CLI::warning( sprintf( 'Invalid file provided to --merge: %s', $file ) ); return false; } return true; } ); if ( ! empty( $this->merge ) ) { WP_CLI::debug( sprintf( 'Merging with existing POT %s: %s', WP_CLI\Utils\pluralize( 'file', count( $this->merge ) ), implode( ',', $this->merge ) ), 'make-pot' ); } } if ( isset( $assoc_args['subtract'] ) ) { $this->subtract_and_merge = Utils\get_flag_value( $assoc_args, 'subtract-and-merge', false ); $files = explode( ',', $assoc_args['subtract'] ); foreach ( $files as $file ) { if ( ! file_exists( $file ) ) { WP_CLI::warning( sprintf( 'Invalid file provided to --subtract: %s', $file ) ); continue; } WP_CLI::debug( sprintf( 'Ignoring any string already existing in: %s', $file ), 'make-pot' ); $this->exceptions[ $file ] = new Translations(); Po::fromFile( $file, $this->exceptions[ $file ] ); } } if ( isset( $assoc_args['include'] ) ) { $this->include = array_filter( explode( ',', $assoc_args['include'] ) ); $this->include = array_map( [ $this, 'unslashit' ], $this->include ); $this->include = array_unique( $this->include ); WP_CLI::debug( sprintf( 'Only including the following files: %s', implode( ',', $this->include ) ), 'make-pot' ); } if ( isset( $assoc_args['exclude'] ) ) { $this->exclude = array_filter( array_merge( $this->exclude, explode( ',', $assoc_args['exclude'] ) ) ); $this->exclude = array_map( [ $this, 'unslashit' ], $this->exclude ); $this->exclude = array_unique( $this->exclude ); } WP_CLI::debug( sprintf( 'Excluding the following files: %s', implode( ',', $this->exclude ) ), 'make-pot' ); } /** * Removes leading and trailing slashes of a string. * * @param string $string What to add and remove slashes from. * @return string String without leading and trailing slashes. */ protected function unslashit( $string ) { return ltrim( rtrim( trim( $string ), '/\\' ), '/\\' ); } /** * Retrieves the main file data of the plugin or theme. * * @return array */ protected function get_main_file_data() { $files = new IteratorIterator( new DirectoryIterator( $this->source ) ); /** @var DirectoryIterator $file */ foreach ( $files as $file ) { // wp-content/themes/my-theme/style.css if ( $file->isFile() && 'style' === $file->getBasename( '.css' ) && $file->isReadable() ) { $theme_data = static::get_file_data( $file->getRealPath(), array_combine( $this->get_file_headers( 'theme' ), $this->get_file_headers( 'theme' ) ) ); // Stop when it contains a valid Theme Name header. if ( ! empty( $theme_data['Theme Name'] ) ) { WP_CLI::log( 'Theme stylesheet detected.' ); WP_CLI::debug( sprintf( 'Theme stylesheet: %s', $file->getRealPath() ), 'make-pot' ); $this->project_type = 'theme'; return $theme_data; } } // wp-content/themes/my-themes/theme-a/style.css if ( $file->isDir() && ! $file->isDot() && is_readable( $file->getRealPath() . '/style.css' ) ) { $theme_data = static::get_file_data( $file->getRealPath() . '/style.css', array_combine( $this->get_file_headers( 'theme' ), $this->get_file_headers( 'theme' ) ) ); // Stop when it contains a valid Theme Name header. if ( ! empty( $theme_data['Theme Name'] ) ) { WP_CLI::log( 'Theme stylesheet detected.' ); WP_CLI::debug( sprintf( 'Theme stylesheet: %s', $file->getRealPath() . '/style.css' ), 'make-pot' ); $this->project_type = 'theme'; return $theme_data; } } // wp-content/plugins/my-plugin/my-plugin.php if ( $file->isFile() && $file->isReadable() && 'php' === $file->getExtension() ) { $plugin_data = static::get_file_data( $file->getRealPath(), array_combine( $this->get_file_headers( 'plugin' ), $this->get_file_headers( 'plugin' ) ) ); // Stop when we find a file with a valid Plugin Name header. if ( ! empty( $plugin_data['Plugin Name'] ) ) { WP_CLI::log( 'Plugin file detected.' ); WP_CLI::debug( sprintf( 'Plugin file: %s', $file->getRealPath() ), 'make-pot' ); $this->project_type = 'plugin'; return $plugin_data; } } } WP_CLI::debug( 'No valid theme stylesheet or plugin file found, treating as a regular project.', 'make-pot' ); return []; } /** * Returns the file headers for themes and plugins. * * @param string $type Source type, either theme or plugin. * * @return array List of file headers. */ protected function get_file_headers( $type ) { switch ( $type ) { case 'plugin': return [ 'Plugin Name', 'Plugin URI', 'Description', 'Author', 'Author URI', 'Version', 'License', 'Domain Path', 'Text Domain', ]; case 'theme': return [ 'Theme Name', 'Theme URI', 'Description', 'Author', 'Author URI', 'Version', 'License', 'Domain Path', 'Text Domain', ]; default: return []; } } /** * Creates a POT file and stores it on disk. * * @throws WP_CLI\ExitException * * @return Translations A Translation set. */ protected function extract_strings() { $translations = new Translations(); // Add existing strings first but don't keep headers. if ( ! empty( $this->merge ) ) { $existing_translations = new Translations(); Po::fromFile( $this->merge, $existing_translations ); $translations->mergeWith( $existing_translations, Merge::ADD | Merge::REMOVE ); } PotGenerator::setCommentBeforeHeaders( $this->get_file_comment() ); $this->set_default_headers( $translations ); // POT files have no Language header. $translations->deleteHeader( Translations::HEADER_LANGUAGE ); // Only relevant for PO files, not POT files. $translations->setHeader( 'PO-Revision-Date', 'YEAR-MO-DA HO:MI+ZONE' ); if ( $this->domain ) { $translations->setDomain( $this->domain ); } unset( $this->main_file_data['Version'], $this->main_file_data['License'], $this->main_file_data['Domain Path'], $this->main_file_data['Text Domain'] ); // Set entries from main file data. foreach ( $this->main_file_data as $header => $data ) { if ( empty( $data ) ) { continue; } $translation = new Translation( '', $data ); if ( isset( $this->main_file_data['Theme Name'] ) ) { $translation->addExtractedComment( sprintf( '%s of the theme', $header ) ); } else { $translation->addExtractedComment( sprintf( '%s of the plugin', $header ) ); } $translations[] = $translation; } try { if ( ! $this->skip_php ) { $options = [ // Extract 'Template Name' headers in theme files. 'wpExtractTemplates' => isset( $this->main_file_data['Theme Name'] ), 'include' => $this->include, 'exclude' => $this->exclude, 'extensions' => [ 'php' ], 'addReferences' => $this->location, ]; PhpCodeExtractor::fromDirectory( $this->source, $translations, $options ); } if ( ! $this->skip_js ) { JsCodeExtractor::fromDirectory( $this->source, $translations, [ 'include' => $this->include, 'exclude' => $this->exclude, 'extensions' => [ 'js', 'jsx' ], 'addReferences' => $this->location, ] ); MapCodeExtractor::fromDirectory( $this->source, $translations, [ 'include' => $this->include, 'exclude' => $this->exclude, 'extensions' => [ 'map' ], 'addReferences' => $this->location, ] ); } if ( ! $this->skip_block_json ) { BlockExtractor::fromDirectory( $this->source, $translations, [ // Only look for block.json files, nothing else. 'restrictFileNames' => [ 'block.json' ], 'include' => $this->include, 'exclude' => $this->exclude, 'extensions' => [ 'json' ], 'addReferences' => $this->location, ] ); } if ( ! $this->skip_theme_json ) { ThemeJsonExtractor::fromDirectory( $this->source, $translations, [ // Only look for theme.json files, nothing else. 'restrictFileNames' => [ 'theme.json' ], 'include' => $this->include, 'exclude' => $this->exclude, 'extensions' => [ 'json' ], ] ); } } catch ( \Exception $e ) { WP_CLI::error( $e->getMessage() ); } foreach ( $this->exceptions as $file => $exception_translations ) { /** @var Translation $exception_translation */ foreach ( $exception_translations as $exception_translation ) { if ( ! $translations->find( $exception_translation ) ) { continue; } if ( $this->subtract_and_merge ) { $translation = $translations[ $exception_translation->getId() ]; $exception_translation->mergeWith( $translation ); } unset( $translations[ $exception_translation->getId() ] ); } if ( $this->subtract_and_merge ) { PotGenerator::toFile( $exception_translations, $file ); } } if ( ! $this->skip_audit ) { $this->audit_strings( $translations ); } return $translations; } /** * Audits strings. * * Goes through all extracted strings to find possible mistakes. * * @param Translations $translations Translations object. */ protected function audit_strings( $translations ) { foreach ( $translations as $translation ) { /** @var Translation $translation */ $references = $translation->getReferences(); // File headers don't have any file references. $location = $translation->hasReferences() ? '(' . implode( ':', array_shift( $references ) ) . ')' : ''; // Check 1: Flag strings with placeholders that should have translator comments. if ( ! $translation->hasExtractedComments() && preg_match( self::SPRINTF_PLACEHOLDER_REGEX, $translation->getOriginal(), $placeholders ) >= 1 ) { $message = sprintf( 'The string "%1$s" contains placeholders but has no "translators:" comment to clarify their meaning. %2$s', $translation->getOriginal(), $location ); WP_CLI::warning( $message ); } // Check 2: Flag strings with different translator comments. if ( $translation->hasExtractedComments() ) { $comments = $translation->getExtractedComments(); // Remove plugin header information from comments. $comments = array_filter( $comments, function ( $comment ) { /** @var ParsedComment|string $comment */ /** @var string $file_header */ foreach ( $this->get_file_headers( $this->project_type ) as $file_header ) { if ( 0 === strpos( ( $comment instanceof ParsedComment ? $comment->getComment() : $comment ), $file_header ) ) { return null; } } return $comment; } ); $unique_comments = array(); // Remove duplicate comments. $comments = array_filter( $comments, function ( $comment ) use ( &$unique_comments ) { /** @var ParsedComment|string $comment */ if ( in_array( ( $comment instanceof ParsedComment ? $comment->getComment() : $comment ), $unique_comments, true ) ) { return null; } $unique_comments[] = ( $comment instanceof ParsedComment ? $comment->getComment() : $comment ); return $comment; } ); $comments_count = count( $comments ); if ( $comments_count > 1 ) { $message = sprintf( 'The string "%1$s" has %2$d different translator comments. %3$s', $translation->getOriginal(), $comments_count, $location ); WP_CLI::warning( $message ); } } $non_placeholder_content = trim( preg_replace( '`^([\'"])(.*)\1$`Ds', '$2', $translation->getOriginal() ) ); $non_placeholder_content = preg_replace( self::SPRINTF_PLACEHOLDER_REGEX, '', $non_placeholder_content ); // Check 3: Flag empty strings without any translatable content. if ( '' === $non_placeholder_content ) { $message = sprintf( 'Found string without translatable content. %s', $location ); WP_CLI::warning( $message ); } // Check 4: Flag strings with multiple unordered placeholders (%s %s %s vs. %1$s %2$s %3$s). $unordered_matches_count = preg_match_all( self::UNORDERED_SPRINTF_PLACEHOLDER_REGEX, $translation->getOriginal(), $unordered_matches ); $unordered_matches = $unordered_matches[0]; if ( $unordered_matches_count >= 2 ) { $message = sprintf( 'Multiple placeholders should be ordered. %s', $location ); WP_CLI::warning( $message ); } if ( $translation->hasPlural() ) { preg_match_all( self::SPRINTF_PLACEHOLDER_REGEX, $translation->getOriginal(), $single_placeholders ); $single_placeholders = $single_placeholders[0]; preg_match_all( self::SPRINTF_PLACEHOLDER_REGEX, $translation->getPlural(), $plural_placeholders ); $plural_placeholders = $plural_placeholders[0]; // see https://developer.wordpress.org/plugins/internationalization/how-to-internationalize-your-plugin/#plurals if ( count( $single_placeholders ) < count( $plural_placeholders ) ) { // Check 5: Flag things like _n( 'One comment', '%s Comments' ) $message = sprintf( 'Missing singular placeholder, needed for some languages. See https://developer.wordpress.org/plugins/internationalization/how-to-internationalize-your-plugin/#plurals %s', $location ); WP_CLI::warning( $message ); } else { // Reordering is fine, but mismatched placeholders is probably wrong. sort( $single_placeholders ); sort( $plural_placeholders ); // Check 6: Flag things like _n( '%s Comment (%d)', '%s Comments (%s)' ) if ( $single_placeholders !== $plural_placeholders ) { $message = sprintf( 'Mismatched placeholders for singular and plural string. %s', $location ); WP_CLI::warning( $message ); } } } } } /** * Returns the copyright comment for the given package. * * @return string File comment. */ protected function get_file_comment() { if ( '' === $this->file_comment ) { return ''; } if ( isset( $this->file_comment ) ) { return implode( "\n", explode( '\n', $this->file_comment ) ); } if ( isset( $this->main_file_data['Theme Name'] ) ) { if ( isset( $this->main_file_data['License'] ) ) { return sprintf( "Copyright (C) %1\$s %2\$s\nThis file is distributed under the %3\$s.", date( 'Y' ), // phpcs:ignore WordPress.DateTime.RestrictedFunctions.date_date $this->main_file_data['Author'], $this->main_file_data['License'] ); } return sprintf( "Copyright (C) %1\$s %2\$s\nThis file is distributed under the same license as the %3\$s theme.", date( 'Y' ), // phpcs:ignore WordPress.DateTime.RestrictedFunctions.date_date $this->main_file_data['Author'], $this->main_file_data['Theme Name'] ); } if ( isset( $this->main_file_data['Plugin Name'] ) ) { if ( isset( $this->main_file_data['License'] ) && ! empty( $this->main_file_data['License'] ) ) { return sprintf( "Copyright (C) %1\$s %2\$s\nThis file is distributed under the %3\$s.", date( 'Y' ), // phpcs:ignore WordPress.DateTime.RestrictedFunctions.date_date $this->main_file_data['Author'], $this->main_file_data['License'] ); } return sprintf( "Copyright (C) %1\$s %2\$s\nThis file is distributed under the same license as the %3\$s plugin.", date( 'Y' ), // phpcs:ignore WordPress.DateTime.RestrictedFunctions.date_date $this->main_file_data['Author'], $this->main_file_data['Plugin Name'] ); } return ''; } /** * Sets default POT file headers for the project. * * @param Translations $translations Translations object. */ protected function set_default_headers( $translations ) { $name = null; $version = $this->get_wp_version(); $bugs_address = null; if ( ! $version && isset( $this->main_file_data['Version'] ) ) { $version = $this->main_file_data['Version']; } if ( isset( $this->main_file_data['Theme Name'] ) ) { $name = $this->main_file_data['Theme Name']; $bugs_address = sprintf( 'https://wordpress.org/support/theme/%s', $this->slug ); } elseif ( isset( $this->main_file_data['Plugin Name'] ) ) { $name = $this->main_file_data['Plugin Name']; $bugs_address = sprintf( 'https://wordpress.org/support/plugin/%s', $this->slug ); } if ( null !== $this->package_name ) { $name = $this->package_name; } if ( null !== $name ) { $translations->setHeader( 'Project-Id-Version', $name . ( $version ? ' ' . $version : '' ) ); } if ( null !== $bugs_address ) { $translations->setHeader( 'Report-Msgid-Bugs-To', $bugs_address ); } $translations->setHeader( 'Last-Translator', 'FULL NAME <EMAIL@ADDRESS>' ); $translations->setHeader( 'Language-Team', 'LANGUAGE <LL@li.org>' ); $translations->setHeader( 'X-Generator', 'WP-CLI ' . WP_CLI_VERSION ); foreach ( $this->headers as $key => $value ) { $translations->setHeader( $key, $value ); } } /** * Extracts the WordPress version number from wp-includes/version.php. * * @return string|false Version number on success, false otherwise. */ protected function get_wp_version() { $version_php = $this->source . '/wp-includes/version.php'; if ( ! file_exists( $version_php ) || ! is_readable( $version_php ) ) { return false; } return preg_match( '/\$wp_version\s*=\s*\'(.*?)\';/', file_get_contents( $version_php ), $matches ) ? $matches[1] : false; } /** * Retrieves metadata from a file. * * Searches for metadata in the first 8kiB of a file, such as a plugin or theme. * Each piece of metadata must be on its own line. Fields can not span multiple * lines, the value will get cut at the end of the first line. * * If the file data is not within that first 8kiB, then the author should correct * their plugin file and move the data headers to the top. * * @see get_file_data() * * @param string $file Path to the file. * @param array $headers List of headers, in the format array('HeaderKey' => 'Header Name'). * * @return array Array of file headers in `HeaderKey => Header Value` format. */ protected static function get_file_data( $file, $headers ) { // We don't need to write to the file, so just open for reading. $fp = fopen( $file, 'rb' ); // Pull only the first 8kiB of the file in. $file_data = fread( $fp, 8192 ); // PHP will close file handle, but we are good citizens. fclose( $fp ); // Make sure we catch CR-only line endings. $file_data = str_replace( "\r", "\n", $file_data ); return static::get_file_data_from_string( $file_data, $headers ); } /** * Retrieves metadata from a string. * * @param string $string String to look for metadata in. * @param array $headers List of headers. * * @return array Array of file headers in `HeaderKey => Header Value` format. */ public static function get_file_data_from_string( $string, $headers ) { foreach ( $headers as $field => $regex ) { if ( preg_match( '/^[ \t\/*#@]*' . preg_quote( $regex, '/' ) . ':(.*)$/mi', $string, $match ) && $match[1] ) { $headers[ $field ] = static::_cleanup_header_comment( $match[1] ); } else { $headers[ $field ] = ''; } } return $headers; } /** * Strip close comment and close php tags from file headers used by WP. * * @see _cleanup_header_comment() * * @param string $str Header comment to clean up. * * @return string */ protected static function _cleanup_header_comment( $str ) { // phpcs:ignore PSR2.Methods.MethodDeclaration.Underscore -- Not changing because third-party commands might use/extend. return trim( preg_replace( '/\s*(?:\*\/|\?>).*/', '', $str ) ); } } <?php namespace WP_CLI\I18n; use Gettext\Generators\Po as PoGenerator; use Gettext\Translations; use Gettext\Utils\ParsedComment; /** * POT file generator. * * The only difference to the existing PO file generator is that this * adds some comments at the very beginning of the file. */ class PotGenerator extends PoGenerator { protected static $comments_before_headers = []; /** * Text to include as a comment before the start of the PO contents * * Doesn't need to include # in the beginning of lines, these are added automatically. * * @param string $comment File comment. */ public static function setCommentBeforeHeaders( $comment ) { $comments = explode( "\n", $comment ); foreach ( $comments as $line ) { if ( '' !== trim( $line ) ) { static::$comments_before_headers[] = '# ' . $line; } } } /** * {@parentDoc}. */ public static function toString( Translations $translations, array $options = [] ) { $lines = static::$comments_before_headers; $lines[] = 'msgid ""'; $lines[] = 'msgstr ""'; $plural_form = $translations->getPluralForms(); $plural_size = is_array( $plural_form ) ? ( $plural_form[0] - 1 ) : 1; foreach ( $translations->getHeaders() as $name => $value ) { $lines[] = sprintf( '"%s: %s\\n"', $name, $value ); } $lines[] = ''; foreach ( $translations as $translation ) { /** @var \Gettext\Translation $translation */ if ( $translation->hasComments() ) { foreach ( $translation->getComments() as $comment ) { $lines[] = '# ' . $comment; } } if ( $translation->hasExtractedComments() ) { $unique_comments = array(); /** @var ParsedComment|string $comment */ foreach ( $translation->getExtractedComments() as $comment ) { $comment = ( $comment instanceof ParsedComment ? $comment->getComment() : $comment ); if ( ! in_array( $comment, $unique_comments, true ) ) { $lines[] = '#. ' . $comment; $unique_comments[] = $comment; } } } foreach ( $translation->getReferences() as $reference ) { $lines[] = '#: ' . $reference[0] . ( null !== $reference[1] ? ':' . $reference[1] : '' ); } if ( $translation->hasFlags() ) { $lines[] = '#, ' . implode( ',', $translation->getFlags() ); } $prefix = $translation->isDisabled() ? '#~ ' : ''; if ( $translation->hasContext() ) { $lines[] = $prefix . 'msgctxt ' . self::convertString( $translation->getContext() ); } self::addLines( $lines, $prefix . 'msgid', $translation->getOriginal() ); if ( $translation->hasPlural() ) { self::addLines( $lines, $prefix . 'msgid_plural', $translation->getPlural() ); for ( $i = 0; $i <= $plural_size; $i ++ ) { self::addLines( $lines, $prefix . 'msgstr[' . $i . ']', '' ); } } else { self::addLines( $lines, $prefix . 'msgstr', $translation->getTranslation() ); } $lines[] = ''; } return implode( "\n", $lines ); } /** * Escapes and adds double quotes to a string. * * @param string $string Multiline string. * * @return string[] */ protected static function multilineQuote( $string ) { $lines = explode( "\n", $string ); $last = count( $lines ) - 1; foreach ( $lines as $k => $line ) { if ( $k === $last ) { $lines[ $k ] = self::convertString( $line ); } else { $lines[ $k ] = self::convertString( $line . "\n" ); } } return $lines; } /** * Add one or more lines depending whether the string is multiline or not. * * @param array &$lines Array lines should be added to. * @param string $name Name of the line, e.g. msgstr or msgid_plural. * @param string $value The line to add. */ protected static function addLines( array &$lines, $name, $value ) { $newlines = self::multilineQuote( $value ); if ( count( $newlines ) === 1 ) { $lines[] = $name . ' ' . $newlines[0]; } else { $lines[] = $name . ' ""'; foreach ( $newlines as $line ) { $lines[] = $line; } } } } <?php namespace WP_CLI\I18n; use Gettext\Extractors\Po as PoExtractor; use Gettext\Generators\Po as PoGenerator; use Gettext\Translation; use Gettext\Translations; use WP_CLI; use WP_CLI_Command; use WP_CLI\Utils; use DirectoryIterator; use IteratorIterator; use SplFileInfo; class MakeJsonCommand extends WP_CLI_Command { /** * Options passed to json_encode(). * * @var int JSON options. */ protected $json_options = 0; /** * Extract JavaScript strings from PO files and add them to individual JSON files. * * For JavaScript internationalization purposes, WordPress requires translations to be split up into * one Jed-formatted JSON file per JavaScript source file. * * See https://make.wordpress.org/core/2018/11/09/new-javascript-i18n-support-in-wordpress/ to learn more * about WordPress JavaScript internationalization. * * ## OPTIONS * * <source> * : Path to an existing PO file or a directory containing multiple PO files. * * [<destination>] * : Path to the destination directory for the resulting JSON files. Defaults to the source directory. * * [--purge] * : Whether to purge the strings that were extracted from the original source file. Defaults to true, use `--no-purge` to skip the removal. * * [--update-mo-files] * : Whether MO files should be updated as well after updating PO files. * Only has an effect when used in combination with `--purge`. * * [--pretty-print] * : Pretty-print resulting JSON files. * * [--use-map=<paths_or_maps>] * : Whether to use a mapping file for the strings, as a JSON value, array to specify multiple. * Each element can either be a string (file path) or object (map). * * ## EXAMPLES * * # Create JSON files for all PO files in the languages directory * $ wp i18n make-json languages * * # Create JSON files for my-plugin-de_DE.po and leave the PO file untouched. * $ wp i18n make-json my-plugin-de_DE.po /tmp --no-purge * * # Create JSON files with mapping * $ wp i18n make-json languages --use-map=build/map.json * * # Create JSON files with multiple mappings * $ wp i18n make-json languages '--use-map=["build/map.json","build/map2.json"]' * * # Create JSON files with object mapping * $ wp i18n make-json languages '--use-map={"source/index.js":"build/index.js"}' * * @when before_wp_load * * @throws WP_CLI\ExitException */ public function __invoke( $args, $assoc_args ) { $assoc_args = Utils\parse_shell_arrays( $assoc_args, array( 'use-map' ) ); $purge = Utils\get_flag_value( $assoc_args, 'purge', true ); $update_mo_files = Utils\get_flag_value( $assoc_args, 'update-mo-files', true ); $map_paths = Utils\get_flag_value( $assoc_args, 'use-map', false ); if ( Utils\get_flag_value( $assoc_args, 'pretty-print', false ) ) { $this->json_options |= JSON_PRETTY_PRINT; } $source = realpath( $args[0] ); if ( ! $source || ( ! is_file( $source ) && ! is_dir( $source ) ) ) { WP_CLI::error( 'Source file or directory does not exist!' ); } $destination = is_file( $source ) ? dirname( $source ) : $source; if ( isset( $args[1] ) ) { $destination = $args[1]; } $map = $this->build_map( $map_paths ); if ( is_array( $map ) && empty( $map ) ) { WP_CLI::error( 'No valid keys found. No file was created.' ); } // Two is_dir() checks in case of a race condition. if ( ! is_dir( $destination ) && ! mkdir( $destination, 0777, true ) && ! is_dir( $destination ) ) { WP_CLI::error( 'Could not create destination directory!' ); } $result_count = 0; if ( is_file( $source ) ) { $files = [ new SplFileInfo( $source ) ]; } else { $files = new IteratorIterator( new DirectoryIterator( $source ) ); } /** @var DirectoryIterator $file */ foreach ( $files as $file ) { if ( $file->isFile() && $file->isReadable() && 'po' === $file->getExtension() ) { $result = $this->make_json( $file->getRealPath(), $destination, $map ); $result_count += count( $result ); if ( $purge ) { $removed = $this->remove_js_strings_from_po_file( $file->getRealPath() ); if ( ! $removed ) { WP_CLI::warning( sprintf( 'Could not update file %s', basename( $source ) ) ); continue; } if ( $update_mo_files ) { $file_basename = basename( $file->getFilename(), '.po' ); $destination_file = "{$destination}/{$file_basename}.mo"; $translations = Translations::fromPoFile( $file->getPathname() ); if ( ! $translations->toMoFile( $destination_file ) ) { WP_CLI::warning( "Could not create file {$destination_file}" ); continue; } } } } } WP_CLI::success( sprintf( 'Created %d %s.', $result_count, Utils\pluralize( 'file', $result_count ) ) ); } /** * Collect maps from paths, normalize and merge * * @param string|array|bool $paths_or_maps argument. False to do nothing. * @return array|null Mapping array. Null if no maps specified. */ protected function build_map( $paths_or_maps ) { if ( false === $paths_or_maps ) { return null; } $map = []; // not an array: single value could also be object (associative array) if ( ! is_array( $paths_or_maps ) || empty( array_filter( array_keys( $paths_or_maps ), 'is_int' ) ) ) { $paths_or_maps = [ $paths_or_maps ]; } $paths = array_filter( $paths_or_maps, 'is_string' ); WP_CLI::debug( sprintf( 'Using %d map files: %s', count( $paths ), implode( ', ', $paths ) ), 'make-json' ); $maps = array_filter( $paths_or_maps, 'is_array' ); WP_CLI::debug( sprintf( 'Using %d inline map objects', count( $maps ) ), 'make-json' ); WP_CLI::debug( sprintf( 'Dropping %d invalid values from map argument', count( $paths_or_maps ) - count( $paths ) - count( $maps ) ), 'make-json' ); $to_transform = array_map( function ( $value, $index ) { return [ $value, sprintf( 'inline object %d', $index ) ]; }, $maps, array_keys( $maps ) ); foreach ( $paths as $path ) { if ( ! file_exists( $path ) || is_dir( $path ) ) { WP_CLI::warning( sprintf( 'Map file %s does not exist', $path ) ); continue; } $json = json_decode( file_get_contents( $path ), true ); if ( ! is_array( $json ) ) { WP_CLI::warning( sprintf( 'Map file %s invalid', $path ) ); continue; } $to_transform[] = [ $json, $path ]; } foreach ( $to_transform as $transform ) { list( $json, $file ) = $transform; $key_num = count( $json ); // normalize contents to string[] $json = array_map( function ( $value ) { if ( is_array( $value ) ) { $value = array_values( array_filter( $value, 'is_string' ) ); if ( ! empty( $value ) ) { return $value; } } if ( is_string( $value ) ) { return [ $value ]; } return null; }, $json ); WP_CLI::debug( sprintf( 'Dropped %d keys from %s', count( $json ) - $key_num, $file ), 'make-json' ); $map = array_merge_recursive( $map, $json ); } return $map; } /** * Splits a single PO file into multiple JSON files. * * @param string $source_file Path to the source file. * @param string $destination Path to the destination directory. * @param array|null $map Source to build file mapping. * @return array List of created JSON files. */ protected function make_json( $source_file, $destination, $map ) { /** @var Translations[] $mapping */ $mapping = []; $translations = new Translations(); $result = []; PoExtractor::fromFile( $source_file, $translations ); $base_file_name = basename( $source_file, '.po' ); foreach ( $translations as $index => $translation ) { /** @var Translation $translation */ // Find all unique sources this translation originates from. $sources = array_map( function ( $reference ) { $file = $reference[0]; if ( substr( $file, - 7 ) === '.min.js' ) { return substr( $file, 0, - 7 ) . '.js'; } if ( substr( $file, - 3 ) === '.js' ) { return $file; } return null; }, $this->reference_map( $translation->getReferences(), $map ) ); $sources = array_unique( array_filter( $sources ) ); foreach ( $sources as $source ) { if ( ! isset( $mapping[ $source ] ) ) { $mapping[ $source ] = new Translations(); // phpcs:ignore Squiz.PHP.CommentedOutCode.Found -- Provide code that is meant to be used once the bug is fixed. // See https://core.trac.wordpress.org/ticket/45441 // $mapping[ $source ]->setDomain( $translations->getDomain() ); $mapping[ $source ]->setHeader( 'Language', $translations->getLanguage() ); $mapping[ $source ]->setHeader( 'PO-Revision-Date', $translations->getHeader( 'PO-Revision-Date' ) ); $plural_forms = $translations->getPluralForms(); if ( $plural_forms ) { list( $count, $rule ) = $plural_forms; $mapping[ $source ]->setPluralForms( $count, $rule ); } } $mapping[ $source ][] = $translation; } } $result += $this->build_json_files( $mapping, $base_file_name, $destination ); return $result; } /** * Takes the references and applies map, if given * * @param array $references translation references * @param array|null $map file mapping * @return array mapped references */ protected function reference_map( $references, $map ) { if ( is_null( $map ) ) { return $references; } // translate using map $temp = array_map( function ( $reference ) use ( &$map ) { $file = $reference[0]; if ( array_key_exists( $file, $map ) ) { return $map[ $file ]; } return null; }, $references ); // this is now an array of arrays of sources, translate to array of sources $references = []; foreach ( $temp as $sources ) { if ( is_null( $sources ) ) { continue; } $references = array_merge( $references, $sources ); } // and wrap to array $references = array_map( function ( $value ) { return [ $value ]; }, $references ); return $references; } /** * Builds a mapping of JS file names to translation entries. * * Exports translations for each JS file to a separate translation file. * * @param array $mapping A mapping of files to translation entries. * @param string $base_file_name Base file name for JSON files. * @param string $destination Path to the destination directory. * * @return array List of created JSON files. */ protected function build_json_files( $mapping, $base_file_name, $destination ) { $result = []; foreach ( $mapping as $file => $translations ) { /** @var Translations $translations */ $hash = md5( $file ); $destination_file = "${destination}/{$base_file_name}-{$hash}.json"; $success = JedGenerator::toFile( $translations, $destination_file, [ 'json' => $this->json_options, 'source' => $file, ] ); if ( ! $success ) { WP_CLI::warning( sprintf( 'Could not create file %s', basename( $destination_file, '.json' ) ) ); continue; } $result[] = $destination_file; } return $result; } /** * Removes strings from PO file that only occur in JavaScript file. * * @param string $source_file Path to the PO file. * @return bool True on success, false otherwise. */ protected function remove_js_strings_from_po_file( $source_file ) { /** @var Translations[] $mapping */ $translations = new Translations(); PoExtractor::fromFile( $source_file, $translations ); foreach ( $translations->getArrayCopy() as $translation ) { /** @var Translation $translation */ if ( ! $translation->hasReferences() ) { continue; } foreach ( $translation->getReferences() as $reference ) { $file = $reference[0]; if ( substr( $file, - 3 ) !== '.js' ) { continue 2; } } unset( $translations[ $translation->getId() ] ); } return PoGenerator::toFile( $translations, $source_file ); } } <?php namespace WP_CLI\I18n; use Gettext\Generators\Jed; use Gettext\Translation; use Gettext\Translations; /** * Jed file generator. * * Adds some more meta data to JED translation files than the default generator. */ class JedGenerator extends Jed { /** * {@parentDoc}. */ public static function toString( Translations $translations, array $options = [] ) { $options += static::$options; $domain = $translations->getDomain() ?: 'messages'; $messages = static::buildMessages( $translations ); $configuration = [ '' => [ 'domain' => $domain, 'lang' => $translations->getLanguage() ?: 'en', 'plural-forms' => $translations->getHeader( 'Plural-Forms' ) ?: 'nplurals=2; plural=(n != 1);', ], ]; $data = [ 'translation-revision-date' => $translations->getHeader( 'PO-Revision-Date' ), 'generator' => 'WP-CLI/' . WP_CLI_VERSION, 'source' => $options['source'], 'domain' => $domain, 'locale_data' => [ $domain => $configuration + $messages, ], ]; return json_encode( $data, $options['json'] ); } /** * Generates an array with all translations. * * @param Translations $translations * * @return array */ public static function buildMessages( Translations $translations ) { $plural_forms = $translations->getPluralForms(); $number_of_plurals = is_array( $plural_forms ) ? ( $plural_forms[0] - 1 ) : null; $messages = []; $context_glue = chr( 4 ); foreach ( $translations as $translation ) { /** @var Translation $translation */ if ( $translation->isDisabled() ) { continue; } $key = $translation->getOriginal(); if ( $translation->hasContext() ) { $key = $translation->getContext() . $context_glue . $key; } if ( $translation->hasPluralTranslations( true ) ) { $message = $translation->getPluralTranslations( $number_of_plurals ); array_unshift( $message, $translation->getTranslation() ); } else { $message = [ $translation->getTranslation() ]; } $messages[ $key ] = $message; } return $messages; } } <?php namespace WP_CLI\I18n; use DirectoryIterator; use Gettext\Translations; use IteratorIterator; use SplFileInfo; use WP_CLI; use WP_CLI\Utils; use WP_CLI_Command; class MakeMoCommand extends WP_CLI_Command { /** * Create MO files from PO files. * * ## OPTIONS * * <source> * : Path to an existing PO file or a directory containing multiple PO files. * * [<destination>] * : Path to the destination directory for the resulting MO files. Defaults to the source directory. * * @when before_wp_load * * @throws WP_CLI\ExitException */ public function __invoke( $args, $assoc_args ) { $source = realpath( $args[0] ); if ( ! $source || ( ! is_file( $source ) && ! is_dir( $source ) ) ) { WP_CLI::error( 'Source file or directory does not exist!' ); } $destination = is_file( $source ) ? dirname( $source ) : $source; if ( isset( $args[1] ) ) { $destination = $args[1]; } // Two is_dir() checks in case of a race condition. if ( ! is_dir( $destination ) && ! mkdir( $destination, 0777, true ) && ! is_dir( $destination ) ) { WP_CLI::error( 'Could not create destination directory!' ); } if ( is_file( $source ) ) { $files = [ new SplFileInfo( $source ) ]; } else { $files = new IteratorIterator( new DirectoryIterator( $source ) ); } $result_count = 0; /** @var DirectoryIterator $file */ foreach ( $files as $file ) { if ( 'po' !== $file->getExtension() ) { continue; } if ( ! $file->isFile() || ! $file->isReadable() ) { WP_CLI::warning( sprintf( 'Could not read file %s', $file->getFilename() ) ); continue; } $file_basename = basename( $file->getFilename(), '.po' ); $destination_file = "{$destination}/{$file_basename}.mo"; $translations = Translations::fromPoFile( $file->getPathname() ); if ( ! $translations->toMoFile( $destination_file ) ) { WP_CLI::warning( sprintf( 'Could not create file %s', $destination_file ) ); continue; } $result_count++; } WP_CLI::success( sprintf( 'Created %d %s.', $result_count, Utils\pluralize( 'file', $result_count ) ) ); } } <?php if ( ! class_exists( 'WP_CLI' ) ) { return; } $wpcli_i18n_autoloader = __DIR__ . '/vendor/autoload.php'; if ( file_exists( $wpcli_i18n_autoloader ) ) { require_once $wpcli_i18n_autoloader; } if ( class_exists( 'WP_CLI\Dispatcher\CommandNamespace' ) ) { WP_CLI::add_command( 'i18n', '\WP_CLI\I18n\CommandNamespace' ); } WP_CLI::add_command( 'i18n make-pot', '\WP_CLI\I18n\MakePotCommand', array( 'before_invoke' => static function() { if ( ! function_exists( 'mb_ereg' ) ) { WP_CLI::error( 'The mbstring extension is required for string extraction to work reliably.' ); } }, ) ); WP_CLI::add_command( 'i18n make-json', '\WP_CLI\I18n\MakeJsonCommand' ); WP_CLI::add_command( 'i18n make-mo', '\WP_CLI\I18n\MakeMoCommand' ); <?php use WP_CLI\ExitException; use WP_CLI\Utils; use WP_CLI\WpOrgApi; /** * Generates and reads the wp-config.php file. */ class Config_Command extends WP_CLI_Command { /** * List of characters that are valid for a key name. * * @var string */ const VALID_KEY_CHARACTERS = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!@#$%^&*()-_ []{}<>~`+=,.;:/?|'; /** * List of default constants that are generated by WordPress Core. * * @string */ const DEFAULT_SALT_CONSTANTS = [ 'AUTH_KEY', 'SECURE_AUTH_KEY', 'LOGGED_IN_KEY', 'NONCE_KEY', 'AUTH_SALT', 'SECURE_AUTH_SALT', 'LOGGED_IN_SALT', 'NONCE_SALT', ]; /** * Retrieve the initial locale from the WordPress version file. * * @return string Initial locale if present, or an empty string if not. */ private static function get_initial_locale() { global $wp_local_package; include ABSPATH . '/wp-includes/version.php'; if ( ! empty( $wp_local_package ) ) { return $wp_local_package; } return ''; } /** * Generates a wp-config.php file. * * Creates a new wp-config.php with database constants, and verifies that * the database constants are correct. * * ## OPTIONS * * --dbname=<dbname> * : Set the database name. * * --dbuser=<dbuser> * : Set the database user. * * [--dbpass=<dbpass>] * : Set the database user password. * * [--dbhost=<dbhost>] * : Set the database host. * --- * default: localhost * --- * * [--dbprefix=<dbprefix>] * : Set the database table prefix. * --- * default: wp_ * --- * * [--dbcharset=<dbcharset>] * : Set the database charset. * --- * default: utf8 * --- * * [--dbcollate=<dbcollate>] * : Set the database collation. * --- * default: * --- * * [--locale=<locale>] * : Set the WPLANG constant. Defaults to $wp_local_package variable. * * [--extra-php] * : If set, the command copies additional PHP code into wp-config.php from STDIN. * * [--skip-salts] * : If set, keys and salts won't be generated, but should instead be passed via `--extra-php`. * * [--skip-check] * : If set, the database connection is not checked. * * [--force] * : Overwrites existing files, if present. * * [--config-file=<path>] * : Specify the file path to the config file to be created. Defaults to the root of the * WordPress installation and the filename "wp-config.php". * * [--insecure] * : Retry API download without certificate validation if TLS handshake fails. Note: This makes the request vulnerable to a MITM attack. * * ## EXAMPLES * * # Standard wp-config.php file * $ wp config create --dbname=testing --dbuser=wp --dbpass=securepswd --locale=ro_RO * Success: Generated 'wp-config.php' file. * * # Enable WP_DEBUG and WP_DEBUG_LOG * $ wp config create --dbname=testing --dbuser=wp --dbpass=securepswd --extra-php <<PHP * define( 'WP_DEBUG', true ); * define( 'WP_DEBUG_LOG', true ); * PHP * Success: Generated 'wp-config.php' file. * * # Avoid disclosing password to bash history by reading from password.txt * # Using --prompt=dbpass will prompt for the 'dbpass' argument * $ wp config create --dbname=testing --dbuser=wp --prompt=dbpass < password.txt * Success: Generated 'wp-config.php' file. */ public function create( $_, $assoc_args ) { if ( ! Utils\get_flag_value( $assoc_args, 'force' ) ) { if ( isset( $assoc_args['config-file'] ) && file_exists( $assoc_args['config-file'] ) ) { $this->config_file_already_exist_error( basename( $assoc_args['config-file'] ) ); } elseif ( ! isset( $assoc_args['config-file'] ) && Utils\locate_wp_config() ) { $this->config_file_already_exist_error( 'wp-config.php' ); } } $defaults = [ 'dbhost' => 'localhost', 'dbpass' => '', 'dbprefix' => 'wp_', 'dbcharset' => 'utf8', 'dbcollate' => '', 'locale' => self::get_initial_locale(), 'config-file' => rtrim( ABSPATH, '/\\' ) . '/wp-config.php', ]; $assoc_args = array_merge( $defaults, $assoc_args ); if ( preg_match( '|[^a-z0-9_]|i', $assoc_args['dbprefix'] ) ) { WP_CLI::error( '--dbprefix can only contain numbers, letters, and underscores.' ); } $mysql_db_connection_args = [ 'execute' => ';', 'host' => $assoc_args['dbhost'], 'user' => $assoc_args['dbuser'], 'pass' => $assoc_args['dbpass'], ]; // Check DB connection. if ( ! Utils\get_flag_value( $assoc_args, 'skip-check' ) ) { Utils\run_mysql_command( '/usr/bin/env mysql --no-defaults', $mysql_db_connection_args ); } if ( ! Utils\get_flag_value( $assoc_args, 'skip-salts' ) ) { try { $assoc_args['keys-and-salts'] = true; $assoc_args['auth-key'] = self::unique_key(); $assoc_args['secure-auth-key'] = self::unique_key(); $assoc_args['logged-in-key'] = self::unique_key(); $assoc_args['nonce-key'] = self::unique_key(); $assoc_args['auth-salt'] = self::unique_key(); $assoc_args['secure-auth-salt'] = self::unique_key(); $assoc_args['logged-in-salt'] = self::unique_key(); $assoc_args['nonce-salt'] = self::unique_key(); $assoc_args['wp-cache-key-salt'] = self::unique_key(); } catch ( Exception $e ) { $assoc_args['keys-and-salts'] = false; $assoc_args['keys-and-salts-alt'] = self::fetch_remote_salts( (bool) Utils\get_flag_value( $assoc_args, 'insecure', false ) ); } } if ( Utils\wp_version_compare( '4.0', '<' ) ) { $assoc_args['add-wplang'] = true; } else { $assoc_args['add-wplang'] = false; } foreach ( $assoc_args as $key => $value ) { $assoc_args[ $key ] = $this->escape_config_value( $key, $value ); } // 'extra-php' from STDIN is retrieved after escaping to avoid breaking // the PHP code. if ( Utils\get_flag_value( $assoc_args, 'extra-php' ) === true ) { $assoc_args['extra-php'] = file_get_contents( 'php://stdin' ); } $command_root = Utils\phar_safe_path( dirname( __DIR__ ) ); $out = Utils\mustache_render( "{$command_root}/templates/wp-config.mustache", $assoc_args ); $wp_config_file_name = basename( $assoc_args['config-file'] ); $bytes_written = file_put_contents( $assoc_args['config-file'], $out ); if ( ! $bytes_written ) { WP_CLI::error( "Could not create new '{$wp_config_file_name}' file." ); } else { WP_CLI::success( "Generated '{$wp_config_file_name}' file." ); } } /** * Gives error when wp-config already exist and try to create it. * * @param string $wp_config_file_name Config file name. * @return void */ private function config_file_already_exist_error( $wp_config_file_name ) { WP_CLI::error( "The '{$wp_config_file_name}' file already exists." ); } /** * Launches system editor to edit the wp-config.php file. * * ## OPTIONS * * [--config-file=<path>] * : Specify the file path to the config file to be edited. Defaults to the root of the * WordPress installation and the filename "wp-config.php". * * ## EXAMPLES * * # Launch system editor to edit wp-config.php file * $ wp config edit * * # Edit wp-config.php file in a specific editor * $ EDITOR=vim wp config edit * * @when before_wp_load */ public function edit( $_, $assoc_args ) { $path = $this->get_config_path( $assoc_args ); $wp_config_file_name = basename( $path ); $contents = file_get_contents( $path ); $r = Utils\launch_editor_for_input( $contents, $wp_config_file_name, 'php' ); if ( false === $r ) { WP_CLI::warning( "No changes made to {$wp_config_file_name}, aborted." ); } else { file_put_contents( $path, $r ); } } /** * Gets the path to wp-config.php file. * * ## EXAMPLES * * # Get wp-config.php file path * $ wp config path * /home/person/htdocs/project/wp-config.php * * @when before_wp_load */ public function path() { WP_CLI::line( $this->get_config_path( array() ) ); } /** * Lists variables, constants, and file includes defined in wp-config.php file. * * ## OPTIONS * * [<filter>...] * : Name or partial name to filter the list by. * * [--fields=<fields>] * : Limit the output to specific fields. Defaults to all fields. * * [--format=<format>] * : Render output in a particular format. * Dotenv is limited to non-object values. * --- * default: table * options: * - table * - csv * - json * - yaml * - dotenv * --- * * [--strict] * : Enforce strict matching when a filter is provided. * * [--config-file=<path>] * : Specify the file path to the config file to be read. Defaults to the root of the * WordPress installation and the filename "wp-config.php". * * ## EXAMPLES * * # List constants and variables defined in wp-config.php file. * $ wp config list * +------------------+------------------------------------------------------------------+----------+ * | key | value | type | * +------------------+------------------------------------------------------------------+----------+ * | table_prefix | wp_ | variable | * | DB_NAME | wp_cli_test | constant | * | DB_USER | root | constant | * | DB_PASSWORD | root | constant | * | AUTH_KEY | r6+@shP1yO&$)1gdu.hl[/j;7Zrvmt~o;#WxSsa0mlQOi24j2cR,7i+QM/#7S:o^ | constant | * | SECURE_AUTH_KEY | iO-z!_m--YH$Tx2tf/&V,YW*13Z_HiRLqi)d?$o-tMdY+82pK$`T.NYW~iTLW;xp | constant | * +------------------+------------------------------------------------------------------+----------+ * * # List only database user and password from wp-config.php file. * $ wp config list DB_USER DB_PASSWORD --strict * +------------------+-------+----------+ * | key | value | type | * +------------------+-------+----------+ * | DB_USER | root | constant | * | DB_PASSWORD | root | constant | * +------------------+-------+----------+ * * # List all salts from wp-config.php file. * $ wp config list _SALT * +------------------+------------------------------------------------------------------+----------+ * | key | value | type | * +------------------+------------------------------------------------------------------+----------+ * | AUTH_SALT | n:]Xditk+_7>Qi=>BmtZHiH-6/Ecrvl(V5ceeGP:{>?;BT^=[B3-0>,~F5z$(+Q$ | constant | * | SECURE_AUTH_SALT | ?Z/p|XhDw3w}?c.z%|+BAr|(Iv*H%%U+Du&kKR y?cJOYyRVRBeB[2zF-`(>+LCC | constant | * | LOGGED_IN_SALT | +$@(1{b~Z~s}Cs>8Y]6[m6~TnoCDpE>O%e75u}&6kUH!>q:7uM4lxbB6[1pa_X,q | constant | * | NONCE_SALT | _x+F li|QL?0OSQns1_JZ{|Ix3Jleox-71km/gifnyz8kmo=w-;@AE8W,(fP<N}2 | constant | * +------------------+------------------------------------------------------------------+----------+ * * @when before_wp_load * @subcommand list */ public function list_( $args, $assoc_args ) { $path = $this->get_config_path( $assoc_args ); $wp_config_file_name = basename( $path ); $strict = Utils\get_flag_value( $assoc_args, 'strict' ); if ( $strict && empty( $args ) ) { WP_CLI::error( 'The --strict option can only be used in combination with a filter.' ); } $default_fields = [ 'name', 'value', 'type', ]; $defaults = [ 'fields' => implode( ',', $default_fields ), 'format' => 'table', ]; $assoc_args = array_merge( $defaults, $assoc_args ); $values = self::get_wp_config_vars( $path ); if ( ! empty( $args ) ) { $values = $this->filter_values( $values, $args, $strict ); } if ( empty( $values ) ) { WP_CLI::error( "No matching entries found in '{$wp_config_file_name}'." ); } if ( 'dotenv' === $assoc_args['format'] ) { return array_walk( $values, array( $this, 'print_dotenv' ) ); } Utils\format_items( $assoc_args['format'], $values, $assoc_args['fields'] ); } /** * Gets the value of a specific constant or variable defined in wp-config.php file. * * ## OPTIONS * * <name> * : Name of the wp-config.php constant or variable. * * [--type=<type>] * : Type of config value to retrieve. Defaults to 'all'. * --- * default: all * options: * - constant * - variable * - all * --- * * [--format=<format>] * : Get value in a particular format. * Dotenv is limited to non-object values. * --- * default: var_export * options: * - var_export * - json * - yaml * - dotenv * --- * * [--config-file=<path>] * : Specify the file path to the config file to be read. Defaults to the root of the * WordPress installation and the filename "wp-config.php". * * ## EXAMPLES * * # Get the table_prefix as defined in wp-config.php file. * $ wp config get table_prefix * wp_ * * @when before_wp_load */ public function get( $args, $assoc_args ) { $path = $this->get_config_path( $assoc_args ); $wp_config_file_name = basename( $path ); list( $name ) = $args; $type = Utils\get_flag_value( $assoc_args, 'type' ); $value = $this->return_value( $name, $type, self::get_wp_config_vars( $path ), $wp_config_file_name ); WP_CLI::print_value( $value, $assoc_args ); } /** * Get the array of wp-config.php constants and variables. * * @param string $wp_config_path Config file path * * @return array */ private static function get_wp_config_vars( $wp_config_path = '' ) { $wp_cli_original_defined_constants = get_defined_constants(); $wp_cli_original_defined_vars = get_defined_vars(); $wp_cli_original_includes = get_included_files(); // phpcs:ignore Squiz.PHP.Eval.Discouraged -- Don't have another way. eval( WP_CLI::get_runner()->get_wp_config_code( $wp_config_path ) ); $wp_config_vars = self::get_wp_config_diff( get_defined_vars(), $wp_cli_original_defined_vars, 'variable', [ 'wp_cli_original_defined_vars' ] ); $wp_config_constants = self::get_wp_config_diff( get_defined_constants(), $wp_cli_original_defined_constants, 'constant' ); foreach ( $wp_config_vars as $name => $value ) { if ( 'wp_cli_original_includes' === $value['name'] ) { $name_backup = $name; break; } } unset( $wp_config_vars[ $name_backup ] ); $wp_config_vars = array_values( $wp_config_vars ); $wp_config_includes = array_diff( get_included_files(), $wp_cli_original_includes ); $wp_config_includes_array = []; foreach ( $wp_config_includes as $name => $value ) { $wp_config_includes_array[] = [ 'name' => basename( $value ), 'value' => $value, 'type' => 'includes', ]; } return array_merge( $wp_config_vars, $wp_config_constants, $wp_config_includes_array ); } /** * Sets the value of a specific constant or variable defined in wp-config.php file. * * ## OPTIONS * * <name> * : Name of the wp-config.php constant or variable. * * <value> * : Value to set the wp-config.php constant or variable to. * * [--add] * : Add the value if it doesn't exist yet. * This is the default behavior, override with --no-add. * * [--raw] * : Place the value into the wp-config.php file as is, instead of as a quoted string. * * [--anchor=<anchor>] * : Anchor string where additions of new values are anchored around. * Defaults to "/* That's all, stop editing!". * The special case "EOF" string uses the end of the file as the anchor. * * [--placement=<placement>] * : Where to place the new values in relation to the anchor string. * --- * default: 'before' * options: * - before * - after * --- * * [--separator=<separator>] * : Separator string to put between an added value and its anchor string. * The following escape sequences will be recognized and properly interpreted: '\n' => newline, '\r' => carriage return, '\t' => tab. * Defaults to a single EOL ("\n" on *nix and "\r\n" on Windows). * * [--type=<type>] * : Type of the config value to set. Defaults to 'all'. * --- * default: all * options: * - constant * - variable * - all * --- * * [--config-file=<path>] * : Specify the file path to the config file to be modified. Defaults to the root of the * WordPress installation and the filename "wp-config.php". * * ## EXAMPLES * * # Set the WP_DEBUG constant to true. * $ wp config set WP_DEBUG true --raw * * @when before_wp_load */ public function set( $args, $assoc_args ) { $path = $this->get_config_path( $assoc_args ); $wp_config_file_name = basename( $path ); list( $name, $value ) = $args; $type = Utils\get_flag_value( $assoc_args, 'type' ); $options = []; $option_flags = [ 'raw' => false, 'add' => true, 'anchor' => null, 'placement' => null, 'separator' => null, ]; foreach ( $option_flags as $option => $default ) { $option_value = Utils\get_flag_value( $assoc_args, $option, $default ); if ( null !== $option_value ) { $options[ $option ] = $option_value; if ( 'separator' === $option ) { $options['separator'] = $this->parse_separator( $options['separator'] ); } } } $adding = false; try { $config_transformer = new WPConfigTransformer( $path ); switch ( $type ) { case 'all': $has_constant = $config_transformer->exists( 'constant', $name ); $has_variable = $config_transformer->exists( 'variable', $name ); if ( $has_constant && $has_variable ) { WP_CLI::error( "Found both a constant and a variable '{$name}' in the '{$wp_config_file_name}' file. Use --type=<type> to disambiguate." ); } if ( ! $has_constant && ! $has_variable ) { if ( ! $options['add'] ) { $message = "The constant or variable '{$name}' is not defined in the '{$wp_config_file_name}' file."; WP_CLI::error( $message ); } $type = 'constant'; $adding = true; } else { $type = $has_constant ? 'constant' : 'variable'; } break; case 'constant': case 'variable': if ( ! $config_transformer->exists( $type, $name ) ) { if ( ! $options['add'] ) { WP_CLI::error( "The {$type} '{$name}' is not defined in the '{$wp_config_file_name}' file." ); } $adding = true; } } $config_transformer->update( $type, $name, $value, $options ); } catch ( Exception $exception ) { WP_CLI::error( "Could not process the '{$wp_config_file_name}' transformation.\nReason: {$exception->getMessage()}" ); } $raw = $options['raw'] ? 'raw ' : ''; if ( $adding ) { $message = "Added the {$type} '{$name}' to the '{$wp_config_file_name}' file with the {$raw}value '{$value}'."; } else { $message = "Updated the {$type} '{$name}' in the '{$wp_config_file_name}' file with the {$raw}value '{$value}'."; } WP_CLI::success( $message ); } /** * Deletes a specific constant or variable from the wp-config.php file. * * ## OPTIONS * * <name> * : Name of the wp-config.php constant or variable. * * [--type=<type>] * : Type of the config value to delete. Defaults to 'all'. * --- * default: all * options: * - constant * - variable * - all * --- * * [--config-file=<path>] * : Specify the file path to the config file to be modified. Defaults to the root of the * WordPress installation and the filename "wp-config.php". * * ## EXAMPLES * * # Delete the COOKIE_DOMAIN constant from the wp-config.php file. * $ wp config delete COOKIE_DOMAIN * * @when before_wp_load */ public function delete( $args, $assoc_args ) { $path = $this->get_config_path( $assoc_args ); $wp_config_file_name = basename( $path ); list( $name ) = $args; $type = Utils\get_flag_value( $assoc_args, 'type' ); try { $config_transformer = new WPConfigTransformer( $path ); switch ( $type ) { case 'all': $has_constant = $config_transformer->exists( 'constant', $name ); $has_variable = $config_transformer->exists( 'variable', $name ); if ( $has_constant && $has_variable ) { WP_CLI::error( "Found both a constant and a variable '{$name}' in the '{$wp_config_file_name}' file. Use --type=<type> to disambiguate." ); } if ( ! $has_constant && ! $has_variable ) { WP_CLI::error( "The constant or variable '{$name}' is not defined in the '{$wp_config_file_name}' file." ); } else { $type = $has_constant ? 'constant' : 'variable'; } break; case 'constant': case 'variable': if ( ! $config_transformer->exists( $type, $name ) ) { WP_CLI::error( "The {$type} '{$name}' is not defined in the '{$wp_config_file_name}' file." ); } } $config_transformer->remove( $type, $name ); } catch ( Exception $exception ) { WP_CLI::error( "Could not process the '{$wp_config_file_name}' transformation.\nReason: {$exception->getMessage()}" ); } WP_CLI::success( "Deleted the {$type} '{$name}' from the '{$wp_config_file_name}' file." ); } /** * Checks whether a specific constant or variable exists in the wp-config.php file. * * ## OPTIONS * * <name> * : Name of the wp-config.php constant or variable. * * [--type=<type>] * : Type of the config value to set. Defaults to 'all'. * --- * default: all * options: * - constant * - variable * - all * --- * * [--config-file=<path>] * : Specify the file path to the config file to be checked. Defaults to the root of the * WordPress installation and the filename "wp-config.php". * * ## EXAMPLES * * # Check whether the DB_PASSWORD constant exists in the wp-config.php file. * $ wp config has DB_PASSWORD * * @when before_wp_load */ public function has( $args, $assoc_args ) { $path = $this->get_config_path( $assoc_args ); $wp_config_file_name = basename( $path ); list( $name ) = $args; $type = Utils\get_flag_value( $assoc_args, 'type' ); try { $config_transformer = new WPConfigTransformer( $path ); switch ( $type ) { case 'all': $has_constant = $config_transformer->exists( 'constant', $name ); $has_variable = $config_transformer->exists( 'variable', $name ); if ( $has_constant && $has_variable ) { WP_CLI::error( "Found both a constant and a variable '{$name}' in the '{$wp_config_file_name}' file. Use --type=<type> to disambiguate." ); } if ( ! $has_constant && ! $has_variable ) { WP_CLI::halt( 1 ); } else { WP_CLI::halt( 0 ); } break; case 'constant': case 'variable': if ( ! $config_transformer->exists( $type, $name ) ) { WP_CLI::halt( 1 ); } WP_CLI::halt( 0 ); } } catch ( Exception $exception ) { WP_CLI::error( "Could not process the '{$wp_config_file_name}' transformation.\nReason: {$exception->getMessage()}" ); } } /** * Refreshes the salts defined in the wp-config.php file. * * ## OPTIONS * * [<keys>...] * : One ore more keys to shuffle. If none are provided, this falls back to the default WordPress Core salt keys. * * [--force] * : If an unknown key is requested to be shuffled, add it instead of throwing a warning. * * [--config-file=<path>] * : Specify the file path to the config file to be modified. Defaults to the root of the * WordPress installation and the filename "wp-config.php". * * [--insecure] * : Retry API download without certificate validation if TLS handshake fails. Note: This makes the request vulnerable to a MITM attack. * * ## EXAMPLES * * # Get new salts for your wp-config.php file * $ wp config shuffle-salts * Success: Shuffled the salt keys. * * # Add a cache key salt to the wp-config.php file * $ wp config shuffle-salts WP_CACHE_KEY_SALT --force * * @subcommand shuffle-salts * @when before_wp_load */ public function shuffle_salts( $args, $assoc_args ) { $keys = $args; $force = Utils\get_flag_value( $assoc_args, 'force', false ); if ( empty( $keys ) ) { $keys = self::DEFAULT_SALT_CONSTANTS; } $secret_keys = []; try { foreach ( $keys as $key ) { if ( ! $force && ! in_array( $key, self::DEFAULT_SALT_CONSTANTS, true ) ) { WP_CLI::warning( "Could not shuffle the unknown key '{$key}'." ); continue; } $secret_keys[ $key ] = trim( self::unique_key() ); } } catch ( Exception $ex ) { foreach ( $keys as $key ) { if ( ! in_array( $key, self::DEFAULT_SALT_CONSTANTS, true ) ) { if ( $force ) { WP_CLI::warning( "Could not add the key '{$key}' because 'random_int()' is not supported." ); } else { WP_CLI::warning( "Could not shuffle the unknown key '{$key}'." ); } } } $remote_salts = self::fetch_remote_salts( (bool) Utils\get_flag_value( $assoc_args, 'insecure', false ) ); $remote_salts = explode( "\n", $remote_salts ); foreach ( $remote_salts as $k => $salt ) { if ( ! empty( $salt ) ) { $key = self::DEFAULT_SALT_CONSTANTS[ $k ]; if ( in_array( $key, $keys, true ) ) { $secret_keys[ $key ] = trim( substr( $salt, 28, 64 ) ); } } } } $path = $this->get_config_path( $assoc_args ); try { $config_transformer = new WPConfigTransformer( $path ); foreach ( $secret_keys as $key => $value ) { $config_transformer->update( 'constant', $key, (string) $value ); } } catch ( Exception $exception ) { $wp_config_file_name = basename( $path ); WP_CLI::error( "Could not process the '{$wp_config_file_name}' transformation.\nReason: {$exception->getMessage()}" ); } WP_CLI::success( 'Shuffled the salt keys.' ); } /** * Filters wp-config.php file configurations. * * @param array $list * @param array $previous_list * @param string $type * @param array $exclude_list * @return array */ private static function get_wp_config_diff( $list, $previous_list, $type, $exclude_list = [] ) { $result = []; foreach ( $list as $name => $val ) { if ( array_key_exists( $name, $previous_list ) || in_array( $name, $exclude_list, true ) ) { continue; } $out = []; $out['name'] = $name; $out['value'] = $val; $out['type'] = $type; $result[] = $out; } return $result; } /** * Read the salts from the WordPress.org API. * * @param bool $insecure Optional. Whether to retry without certificate validation on TLS handshake failure. * @return string String with a set of PHP define() statements to define the salts. * @throws ExitException If the remote request failed. */ private static function fetch_remote_salts( $insecure = false ) { try { $salts = (string) ( new WpOrgApi( [ 'insecure' => $insecure ] ) )->get_salts(); } catch ( Exception $exception ) { WP_CLI::error( $exception ); } // Adapt whitespace to adhere to WPCS. $salts = preg_replace( '/define\(\'(.*?)\'\);/', 'define( \'$1\' );', $salts ); return $salts; } /** * Prints the value of a constant or variable defined in the wp-config.php file. * * If the constant or variable is not defined in the wp-config file then an error will be returned. * * @param string $name * @param string $type * @param string $type * @param string $wp_config_file_name Config file name * @return string The value of the requested constant or variable as defined in the wp-config.php file; if the * requested constant or variable is not defined then the function will print an error and exit. */ private function return_value( $name, $type, $values, $wp_config_file_name ) { $results = []; foreach ( $values as $value ) { if ( $name === $value['name'] && ( 'all' === $type || $type === $value['type'] ) ) { $results[] = $value; } } if ( count( $results ) > 1 ) { WP_CLI::error( "Found both a constant and a variable '{$name}' in the '{$wp_config_file_name}' file. Use --type=<type> to disambiguate." ); } if ( ! empty( $results ) ) { return $results[0]['value']; } $type = 'all' === $type ? 'constant or variable' : $type; $names = array_column( $values, 'name' ); $candidate = Utils\get_suggestion( $name, $names ); if ( ! empty( $candidate ) && $candidate !== $name ) { WP_CLI::error( "The {$type} '{$name}' is not defined in the '{$wp_config_file_name}' file.\nDid you mean '{$candidate}'?" ); } WP_CLI::error( "The {$type} '{$name}' is not defined in the '{$wp_config_file_name}' file." ); } /** * Generates a unique key/salt for the wp-config file. * * @throws Exception * * @return string */ private static function unique_key() { if ( ! function_exists( 'random_int' ) ) { throw new Exception( "'random_int' does not exist" ); } $chars = self::VALID_KEY_CHARACTERS; $key = ''; for ( $i = 0; $i < 64; $i++ ) { // phpcs:ignore PHPCompatibility.FunctionUse.NewFunctions.random_intFound -- Will be called only if function exists. $key .= substr( $chars, random_int( 0, strlen( $chars ) - 1 ), 1 ); } return $key; } /** * Filters the values based on a provider filter key. * * @param array $values * @param array $filters * @param bool $strict * * @return array */ private function filter_values( $values, $filters, $strict ) { $result = []; foreach ( $values as $value ) { foreach ( $filters as $filter ) { if ( $strict && $filter !== $value['name'] ) { continue; } if ( false === strpos( $value['name'], $filter ) ) { continue; } $result[] = $value; } } return $result; } /** * Gets the path to the wp-config.php file or gives a helpful error if none found. * * @param array $assoc_args associative arguments given while calling wp config subcommand * @return string Path to wp-config.php file. */ private function get_config_path( $assoc_args ) { if ( isset( $assoc_args['config-file'] ) ) { $path = $assoc_args['config-file']; if ( ! file_exists( $path ) ) { $this->config_file_not_found_error( basename( $assoc_args['config-file'] ) ); } } else { $path = Utils\locate_wp_config(); if ( ! $path ) { $this->config_file_not_found_error( 'wp-config.php' ); } } return $path; } /** * Gives error the wp-config file not found * * @param string $wp_config_file_name Config file name. * @return void */ private function config_file_not_found_error( $wp_config_file_name ) { WP_CLI::error( "'{$wp_config_file_name}' not found.\nEither create one manually or use `wp config create`." ); } /** * Parses the separator argument, to allow for special character handling. * * Does the following transformations: * - '\n' => "\n" (newline) * - '\r' => "\r" (carriage return) * - '\t' => "\t" (tab) * * @param string $separator Separator string to parse. * * @return mixed Parsed separator string. */ private function parse_separator( $separator ) { $separator = str_replace( [ '\n', '\r', '\t' ], [ "\n", "\r", "\t" ], $separator ); return $separator; } /** * Writes a provided variable's key and value to stdout, in dotenv format. * * @param array $value */ private function print_dotenv( array $value ) { if ( ! isset( $value['name'] ) || ! isset( $value['type'] ) || 'constant' !== $value['type'] ) { return; } $name = strtoupper( $value['name'] ); $variable_value = isset( $value['value'] ) ? $value['value'] : ''; $variable_value = str_replace( "'", "\'", $variable_value ); if ( ! is_numeric( $variable_value ) ) { $variable_value = "'{$variable_value}'"; } WP_CLI::line( "{$name}={$variable_value}" ); } /** * Escape a config value so it can be safely used within single quotes. * * @param string $key Key into the arguments array. * @param mixed $value Value to escape. * @return mixed Escaped value. */ private function escape_config_value( $key, $value ) { // Skip 'extra-php', it mustn't be escaped. if ( 'extra-php' === $key ) { return $value; } // Skip 'keys-and-salts-alt' and assume they are safe. if ( 'keys-and-salts-alt' === $key && ! empty( $value ) ) { return $value; } if ( is_string( $value ) ) { return addslashes( $value ); } return $value; } } <?php if ( ! class_exists( 'WP_CLI' ) ) { return; } $wpcli_config_autoloader = dirname( __FILE__ ) . '/vendor/autoload.php'; if ( file_exists( $wpcli_config_autoloader ) ) { require_once $wpcli_config_autoloader; } WP_CLI::add_command( 'config', 'Config_Command' ); <?php use WP_CLI\Utils; use WP_CLI\Formatter; /** * Lists registered sidebars. * * A [sidebar](https://developer.wordpress.org/themes/functionality/sidebars/) is any widgetized area of your theme. * * ## EXAMPLES * * # List sidebars * $ wp sidebar list --fields=name,id --format=csv * name,id * "Widget Area",sidebar-1 * "Inactive Widgets",wp_inactive_widgets */ class Sidebar_Command extends WP_CLI_Command { private $fields = [ 'name', 'id', 'description', ]; /** * Lists registered sidebars. * * ## OPTIONS * * [--fields=<fields>] * : Limit the output to specific object fields. * * [--format=<format>] * : Render output in a particular format. * --- * default: table * options: * - table * - csv * - json * - ids * - count * - yaml * --- * * ## AVAILABLE FIELDS * * These fields will be displayed by default for each sidebar: * * * name * * id * * description * * These fields are optionally available: * * * class * * before_widget * * after_widget * * before_title * * after_title * * ## EXAMPLES * * $ wp sidebar list --fields=name,id --format=csv * name,id * "Widget Area",sidebar-1 * "Inactive Widgets",wp_inactive_widgets * * @subcommand list */ public function list_( $args, $assoc_args ) { global $wp_registered_sidebars; Utils\wp_register_unused_sidebar(); if ( ! empty( $assoc_args['format'] ) && 'ids' === $assoc_args['format'] ) { $sidebars = wp_list_pluck( $wp_registered_sidebars, 'id' ); } else { $sidebars = $wp_registered_sidebars; } $formatter = new Formatter( $assoc_args, $this->fields ); $formatter->display_items( $sidebars ); } } <?php use WP_CLI\Utils; use WP_CLI\Formatter; /** * Manages widgets, including adding and moving them within sidebars. * * A [widget](https://developer.wordpress.org/themes/functionality/widgets/) adds content and features to a widget area (also called a [sidebar](https://developer.wordpress.org/themes/functionality/sidebars/)). * * ## EXAMPLES * * # List widgets on a given sidebar * $ wp widget list sidebar-1 * +----------+------------+----------+----------------------+ * | name | id | position | options | * +----------+------------+----------+----------------------+ * | meta | meta-6 | 1 | {"title":"Meta"} | * | calendar | calendar-2 | 2 | {"title":"Calendar"} | * +----------+------------+----------+----------------------+ * * # Add a calendar widget to the second position on the sidebar * $ wp widget add calendar sidebar-1 2 * Success: Added widget to sidebar. * * # Update option(s) associated with a given widget * $ wp widget update calendar-1 --title="Calendar" * Success: Widget updated. * * # Delete one or more widgets entirely * $ wp widget delete calendar-2 archive-1 * Success: 2 widgets removed from sidebar. */ class Widget_Command extends WP_CLI_Command { private $fields = [ 'name', 'id', 'position', 'options', ]; /** * Lists widgets associated with a sidebar. * * ## OPTIONS * * <sidebar-id> * : ID for the corresponding sidebar. * * [--fields=<fields>] * : Limit the output to specific object fields. * * [--format=<format>] * : Render output in a particular format. * --- * default: table * options: * - table * - csv * - ids * - json * - count * - yaml * --- * * ## AVAILABLE FIELDS * * These fields will be displayed by default for each widget: * * * name * * id * * position * * options * * There are no optionally available fields. * * ## EXAMPLES * * $ wp widget list sidebar-1 --fields=name,id --format=csv * name,id * meta,meta-5 * search,search-3 * * @subcommand list */ public function list_( $args, $assoc_args ) { list( $sidebar_id ) = $args; $this->validate_sidebar( $sidebar_id ); $output_widgets = $this->get_sidebar_widgets( $sidebar_id ); if ( ! empty( $assoc_args['format'] ) && 'ids' === $assoc_args['format'] ) { $output_widgets = wp_list_pluck( $output_widgets, 'id' ); } $formatter = new Formatter( $assoc_args, $this->fields ); $formatter->display_items( $output_widgets ); } /** * Adds a widget to a sidebar. * * Creates a new widget entry in the database, and associates it with the * sidebar. * * ## OPTIONS * * <name> * : Widget name. * * <sidebar-id> * : ID for the corresponding sidebar. * * [<position>] * : Widget's current position within the sidebar. Defaults to last * * [--<field>=<value>] * : Widget option to add, with its new value * * ## EXAMPLES * * # Add a new calendar widget to sidebar-1 with title "Calendar" * $ wp widget add calendar sidebar-1 2 --title="Calendar" * Success: Added widget to sidebar. * * @subcommand add */ public function add( $args, $assoc_args ) { list( $name, $sidebar_id ) = $args; $this->validate_sidebar( $sidebar_id ); $position = count( $args ) > 2 ? $args[2] - 1 : count( $this->get_sidebar_widgets( $sidebar_id ) ); $widget = $this->get_widget_obj( $name ); if ( false === $widget ) { WP_CLI::error( 'Invalid widget type.' ); } /* * Adding a widget is as easy as: * 1. Creating a new widget option * 2. Adding the widget to the sidebar * 3. Positioning appropriately */ $widget_options = $this->get_widget_options( $name ); $option_keys = $widget_options; if ( ! isset( $widget_options['_multiwidget'] ) ) { $widget_options['_multiwidget'] = 1; } unset( $option_keys['_multiwidget'] ); $option_keys = array_keys( $option_keys ); $last_key = array_pop( $option_keys ); $option_index = $last_key + 1; $widget_options[ $option_index ] = $this->sanitize_widget_options( $name, $assoc_args, array() ); $this->update_widget_options( $name, $widget_options ); $widget_id = $name . '-' . $option_index; $this->move_sidebar_widget( $widget_id, null, $sidebar_id, null, $position ); WP_CLI::success( 'Added widget to sidebar.' ); } /** * Updates options for an existing widget. * * ## OPTIONS * * <widget-id> * : Unique ID for the widget * * [--<field>=<value>] * : Field to update, with its new value * * ## EXAMPLES * * # Change calendar-1 widget title to "Our Calendar" * $ wp widget update calendar-1 --title="Our Calendar" * Success: Widget updated. * * @subcommand update */ public function update( $args, $assoc_args ) { list( $widget_id ) = $args; if ( ! $this->validate_sidebar_widget( $widget_id ) ) { WP_CLI::error( "Widget doesn't exist." ); } if ( empty( $assoc_args ) ) { WP_CLI::error( 'No options specified to update.' ); } list( $name, $option_index ) = $this->get_widget_data( $widget_id ); $widget_options = $this->get_widget_options( $name ); $clean_options = $this->sanitize_widget_options( $name, $assoc_args, $widget_options[ $option_index ] ); $widget_options[ $option_index ] = array_merge( (array) $widget_options[ $option_index ], $clean_options ); $this->update_widget_options( $name, $widget_options ); WP_CLI::success( 'Widget updated.' ); } /** * Moves the position of a widget. * * Changes the order of a widget in its existing sidebar, or moves it to a * new sidebar. * * ## OPTIONS * * <widget-id> * : Unique ID for the widget * * [--position=<position>] * : Assign the widget to a new position. * * [--sidebar-id=<sidebar-id>] * : Assign the widget to a new sidebar * * ## EXAMPLES * * # Change position of widget * $ wp widget move recent-comments-2 --position=2 * Success: Widget moved. * * # Move widget to Inactive Widgets * $ wp widget move recent-comments-2 --sidebar-id=wp_inactive_widgets * Success: Widget moved. * * @subcommand move */ public function move( $args, $assoc_args ) { list( $widget_id ) = $args; if ( ! $this->validate_sidebar_widget( $widget_id ) ) { WP_CLI::error( "Widget doesn't exist." ); } if ( empty( $assoc_args['position'] ) && empty( $assoc_args['sidebar-id'] ) ) { WP_CLI::error( 'A new position or new sidebar must be specified.' ); } list( $name, $option_index, $current_sidebar_id, $current_sidebar_index ) = $this->get_widget_data( $widget_id ); $new_sidebar_id = ! empty( $assoc_args['sidebar-id'] ) ? $assoc_args['sidebar-id'] : $current_sidebar_id; $this->validate_sidebar( $new_sidebar_id ); $new_sidebar_index = ! empty( $assoc_args['position'] ) ? $assoc_args['position'] - 1 : $current_sidebar_index; // Moving between sidebars adds to the top. if ( $new_sidebar_id !== $current_sidebar_id && $new_sidebar_index === $current_sidebar_index ) { // Human-readable positions are different than numerically indexed array. $new_sidebar_index = 0; } $this->move_sidebar_widget( $widget_id, $current_sidebar_id, $new_sidebar_id, $current_sidebar_index, $new_sidebar_index ); WP_CLI::success( 'Widget moved.' ); } /** * Deactivates one or more widgets from an active sidebar. * * Moves widgets to Inactive Widgets. * * ## OPTIONS * * <widget-id>... * : Unique ID for the widget(s) * * ## EXAMPLES * * # Deactivate the recent-comments-2 widget. * $ wp widget deactivate recent-comments-2 * Success: 1 widget deactivated. * * @subcommand deactivate */ public function deactivate( $args, $assoc_args ) { $count = 0; $errors = 0; foreach ( $args as $widget_id ) { if ( ! $this->validate_sidebar_widget( $widget_id ) ) { WP_CLI::warning( "Widget '{$widget_id}' doesn't exist." ); $errors++; continue; } list( $name, $option_index, $sidebar_id, $sidebar_index ) = $this->get_widget_data( $widget_id ); if ( 'wp_inactive_widgets' === $sidebar_id ) { WP_CLI::warning( sprintf( "'%s' is already deactivated.", $widget_id ) ); continue; } $this->move_sidebar_widget( $widget_id, $sidebar_id, 'wp_inactive_widgets', $sidebar_index, count( $this->get_sidebar_widgets( 'wp_inactive_widgets' ) ) ); $count++; } Utils\report_batch_operation_results( 'widget', 'deactivate', count( $args ), $count, $errors ); } /** * Deletes one or more widgets from a sidebar. * * ## OPTIONS * * <widget-id>... * : Unique ID for the widget(s) * * ## EXAMPLES * * # Delete the recent-comments-2 widget from its sidebar. * $ wp widget delete recent-comments-2 * Success: Deleted 1 of 1 widgets. * * @subcommand delete */ public function delete( $args, $assoc_args ) { $count = 0; $errors = 0; foreach ( $args as $widget_id ) { if ( ! $this->validate_sidebar_widget( $widget_id ) ) { WP_CLI::warning( "Widget '{$widget_id}' doesn't exist." ); $errors++; continue; } // Remove the widget's settings. list( $name, $option_index, $sidebar_id, $sidebar_index ) = $this->get_widget_data( $widget_id ); $widget_options = $this->get_widget_options( $name ); unset( $widget_options[ $option_index ] ); $this->update_widget_options( $name, $widget_options ); // Remove the widget from the sidebar. $all_widgets = $this->wp_get_sidebars_widgets(); unset( $all_widgets[ $sidebar_id ][ $sidebar_index ] ); $all_widgets[ $sidebar_id ] = array_values( $all_widgets[ $sidebar_id ] ); update_option( 'sidebars_widgets', $all_widgets ); $count++; } Utils\report_batch_operation_results( 'widget', 'delete', count( $args ), $count, $errors ); } /** * Resets sidebar. * * Removes all widgets from the sidebar and places them in Inactive Widgets. * * ## OPTIONS * * [<sidebar-id>...] * : One or more sidebars to reset. * * [--all] * : If set, all sidebars will be reset. * * ## EXAMPLES * * # Reset a sidebar * $ wp widget reset sidebar-1 * Success: Sidebar 'sidebar-1' reset. * * # Reset multiple sidebars * $ wp widget reset sidebar-1 sidebar-2 * Success: Sidebar 'sidebar-1' reset. * Success: Sidebar 'sidebar-2' reset. * * # Reset all sidebars * $ wp widget reset --all * Success: Sidebar 'sidebar-1' reset. * Success: Sidebar 'sidebar-2' reset. * Success: Sidebar 'sidebar-3' reset. */ public function reset( $args, $assoc_args ) { global $wp_registered_sidebars; $all = Utils\get_flag_value( $assoc_args, 'all', false ); // Bail if no arguments and no all flag. if ( ! $all && empty( $args ) ) { WP_CLI::error( 'Please specify one or more sidebars, or use --all.' ); } // Fetch all sidebars if all flag is set. if ( $all ) { $args = array_keys( $wp_registered_sidebars ); } // Sidebar ID wp_inactive_widgets is reserved by WP core for inactive widgets. if ( isset( $args['wp_inactive_widgets'] ) ) { unset( $args['wp_inactive_widgets'] ); } // Check if no registered sidebar. if ( empty( $args ) ) { WP_CLI::error( 'No sidebar registered.' ); } $count = 0; $errors = 0; foreach ( $args as $sidebar_id ) { if ( ! array_key_exists( $sidebar_id, $wp_registered_sidebars ) ) { WP_CLI::warning( sprintf( 'Invalid sidebar: %s', $sidebar_id ) ); $errors++; continue; } $widgets = $this->get_sidebar_widgets( $sidebar_id ); if ( empty( $widgets ) ) { WP_CLI::warning( sprintf( "Sidebar '%s' is already empty.", $sidebar_id ) ); } else { foreach ( $widgets as $widget ) { $widget_id = $widget->id; list( $name, $option_index, $new_sidebar_id, $sidebar_index ) = $this->get_widget_data( $widget_id ); $this->move_sidebar_widget( $widget_id, $new_sidebar_id, 'wp_inactive_widgets', $sidebar_index, count( $this->get_sidebar_widgets( 'wp_inactive_widgets' ) ) ); } WP_CLI::log( sprintf( "Sidebar '%s' reset.", $sidebar_id ) ); $count++; } } Utils\report_batch_operation_results( 'sidebar', 'reset', count( $args ), $count, $errors ); } /** * Checks whether a sidebar is a valid sidebar * * @param string $sidebar_id */ private function validate_sidebar( $sidebar_id ) { global $wp_registered_sidebars; Utils\wp_register_unused_sidebar(); if ( ! array_key_exists( $sidebar_id, $wp_registered_sidebars ) ) { WP_CLI::error( 'Invalid sidebar.' ); } } /** * Checks whether the specified widget is on the sidebar * * @param string $widget_id */ private function validate_sidebar_widget( $widget_id ) { $sidebars_widgets = $this->wp_get_sidebars_widgets(); $widget_exists = false; foreach ( $sidebars_widgets as $sidebar_id => $widgets ) { if ( in_array( $widget_id, $widgets, true ) ) { $widget_exists = true; break; } } return $widget_exists; } /** * Gets the widgets (and their associated data) for a given sidebar * * @param string $sidebar_id * @return array */ private function get_sidebar_widgets( $sidebar_id ) { $all_widgets = $this->wp_get_sidebars_widgets(); if ( empty( $all_widgets[ $sidebar_id ] ) ) { return array(); } $prepared_widgets = array(); foreach ( $all_widgets[ $sidebar_id ] as $key => $widget_id ) { $prepared_widget = new stdClass(); $parts = explode( '-', $widget_id ); $option_index = array_pop( $parts ); $widget_name = implode( '-', $parts ); $prepared_widget->name = $widget_name; $prepared_widget->id = $widget_id; $prepared_widget->position = $key + 1; $widget_options = get_option( 'widget_' . $widget_name ); $prepared_widget->options = $widget_options[ $option_index ]; $prepared_widgets[] = $prepared_widget; } return $prepared_widgets; } /** * Re-implementation of wp_get_sidebars_widgets() * because the original has a nasty global component */ private function wp_get_sidebars_widgets() { $sidebars_widgets = get_option( 'sidebars_widgets', array() ); if ( is_array( $sidebars_widgets ) && isset( $sidebars_widgets['array_version'] ) ) { unset( $sidebars_widgets['array_version'] ); } return $sidebars_widgets; } /** * Gets the widget's name, option index, sidebar, and sidebar index from its ID * * @param string $widget_id * @return array */ private function get_widget_data( $widget_id ) { $parts = explode( '-', $widget_id ); $option_index = array_pop( $parts ); $name = implode( '-', $parts ); $sidebar_id = false; $sidebar_index = false; $all_widgets = $this->wp_get_sidebars_widgets(); foreach ( $all_widgets as $s_id => &$widgets ) { $key = array_search( $widget_id, $widgets, true ); if ( false !== $key ) { $sidebar_id = $s_id; $sidebar_index = $key; break; } } return array( $name, $option_index, $sidebar_id, $sidebar_index ); } /** * Gets the options for a given widget * * @param string $name * @return array */ private function get_widget_options( $name ) { return get_option( 'widget_' . $name, array() ); } /** * Updates the options for a given widget * * @param string $name * @param mixed */ private function update_widget_options( $name, $value ) { update_option( 'widget_' . $name, $value ); } /** * Repositions a widget within a sidebar or move to another sidebar. * * @param string $widget_id * @param string|null $current_sidebar_id * @param string $new_sidebar_id * @param int|null $current_index * @param int $new_index */ private function move_sidebar_widget( $widget_id, $current_sidebar_id, $new_sidebar_id, $current_index, $new_index ) { $all_widgets = $this->wp_get_sidebars_widgets(); $needs_placement = true; // Existing widget if ( $current_sidebar_id && ! is_null( $current_index ) ) { $widgets = $all_widgets[ $current_sidebar_id ]; if ( $current_sidebar_id !== $new_sidebar_id ) { unset( $widgets[ $current_index ] ); } else { $part = array_splice( $widgets, $current_index, 1 ); array_splice( $widgets, $new_index, 0, $part ); $needs_placement = false; } $all_widgets[ $current_sidebar_id ] = array_values( $widgets ); } if ( $needs_placement ) { $widgets = ! empty( $all_widgets[ $new_sidebar_id ] ) ? $all_widgets[ $new_sidebar_id ] : array(); $before = array_slice( $widgets, 0, $new_index, true ); $after = array_slice( $widgets, $new_index, count( $widgets ), true ); $widgets = array_merge( $before, array( $widget_id ), $after ); $all_widgets[ $new_sidebar_id ] = array_values( $widgets ); } update_option( 'sidebars_widgets', $all_widgets ); } /** * Gets a widget's instantiated object based on its name * * @param string $id_base Name of the widget * @return WP_Widget|false */ private function get_widget_obj( $id_base ) { global $wp_widget_factory; $widget = wp_filter_object_list( $wp_widget_factory->widgets, array( 'id_base' => $id_base ) ); if ( empty( $widget ) ) { return false; } return array_pop( $widget ); } /** * Cleans up a widget's options based on its update callback * * @param string $id_base Name of the widget * @param mixed $dirty_options * @param mixed $old_options * @return mixed */ private function sanitize_widget_options( $id_base, $dirty_options, $old_options ) { $widget = $this->get_widget_obj( $id_base ); if ( empty( $widget ) ) { return array(); } // No easy way to determine expected array keys for $dirty_options // because Widget API dependent on the form fields // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged -- Whitelisting due to above reason. return @$widget->update( $dirty_options, $old_options ); } } <?php if ( ! class_exists( 'WP_CLI' ) ) { return; } $wpcli_widget_autoloader = dirname( __FILE__ ) . '/vendor/autoload.php'; if ( file_exists( $wpcli_widget_autoloader ) ) { require_once $wpcli_widget_autoloader; } WP_CLI::add_command( 'widget', 'Widget_Command' ); WP_CLI::add_command( 'sidebar', 'Sidebar_Command' ); <?php namespace WP_CLI\MaintenanceMode; use WP_CLI; use WP_CLI_Command; use WP_Upgrader; use WP_Filesystem_Base; /** * Activates, deactivates or checks the status of the maintenance mode of a site. * * ## EXAMPLES * * # Activate Maintenance mode. * $ wp maintenance-mode activate * Enabling Maintenance mode... * Success: Activated Maintenance mode. * * # Deactivate Maintenance mode. * $ wp maintenance-mode deactivate * Disabling Maintenance mode... * Success: Deactivated Maintenance mode. * * # Display Maintenance mode status. * $ wp maintenance-mode status * Maintenance mode is active. * * # Get Maintenance mode status for scripting purpose. * $ wp maintenance-mode is-active * $ echo $? * 1 * * @when after_wp_load * @package wp-cli */ class MaintenanceModeCommand extends WP_CLI_Command { /** * Instance of WP_Upgrader. * * @var WP_Upgrader */ private $upgrader; /** * Instantiate a MaintenanceModeCommand object. */ public function __construct() { if ( ! class_exists( 'WP_Upgrader' ) ) { require_once ABSPATH . 'wp-admin/includes/class-wp-upgrader.php'; } $this->upgrader = new WP_Upgrader( new WP_CLI\UpgraderSkin() ); $this->upgrader->init(); } /** * Activates maintenance mode. * * [--force] * : Force maintenance mode activation operation. * * ## EXAMPLES * * $ wp maintenance-mode activate * Enabling Maintenance mode... * Success: Activated Maintenance mode. */ public function activate( $_, $assoc_args ) { if ( $this->get_maintenance_mode_status() && ! WP_CLI\Utils\get_flag_value( $assoc_args, 'force' ) ) { WP_CLI::error( 'Maintenance mode already activated.' ); } $this->upgrader->maintenance_mode( true ); WP_CLI::success( 'Activated Maintenance mode.' ); } /** * Deactivates maintenance mode. * * ## EXAMPLES * * $ wp maintenance-mode deactivate * Disabling Maintenance mode... * Success: Deactivated Maintenance mode. */ public function deactivate() { if ( ! $this->get_maintenance_mode_status() ) { WP_CLI::error( 'Maintenance mode already deactivated.' ); } $this->upgrader->maintenance_mode( false ); WP_CLI::success( 'Deactivated Maintenance mode.' ); } /** * Displays maintenance mode status. * * ## EXAMPLES * * $ wp maintenance-mode status * Maintenance mode is active. */ public function status() { $status = $this->get_maintenance_mode_status() ? 'active' : 'not active'; WP_CLI::line( "Maintenance mode is {$status}." ); } /** * Detects maintenance mode status. * * ## EXAMPLES * * $ wp maintenance-mode is-active * $ echo $? * 1 * * @subcommand is-active */ public function is_active() { WP_CLI::halt( $this->get_maintenance_mode_status() ? 0 : 1 ); } /** * Returns status of maintenance mode. * * @return bool */ private function get_maintenance_mode_status() { $wp_filesystem = $this->init_wp_filesystem(); $maintenance_file = trailingslashit( $wp_filesystem->abspath() ) . '.maintenance'; return $wp_filesystem->exists( $maintenance_file ); } /** * Initializes WP_Filesystem. * * @return WP_Filesystem_Base */ protected function init_wp_filesystem() { global $wp_filesystem; WP_Filesystem(); return $wp_filesystem; } } <?php if ( ! class_exists( 'WP_CLI' ) ) { return; } $wpcli_maintenance_mode_autoloader = __DIR__ . '/vendor/autoload.php'; if ( file_exists( $wpcli_maintenance_mode_autoloader ) ) { require_once $wpcli_maintenance_mode_autoloader; } WP_CLI::add_command( 'maintenance-mode', '\WP_CLI\MaintenanceMode\MaintenanceModeCommand' ); <?php namespace WP_CLI\Shell; use WP_CLI; class REPL { private $prompt; private $history_file; public function __construct( $prompt ) { $this->prompt = $prompt; $this->set_history_file(); } public function start() { while ( true ) { $line = $this->prompt(); if ( '' === $line ) { continue; } $line = rtrim( $line, ';' ) . ';'; if ( self::starts_with( self::non_expressions(), $line ) ) { ob_start(); // phpcs:ignore Squiz.PHP.Eval.Discouraged -- This is meant to be a REPL, no way to avoid eval. eval( $line ); $out = ob_get_clean(); if ( 0 < strlen( $out ) ) { $out = rtrim( $out, "\n" ) . "\n"; } fwrite( STDOUT, $out ); } else { if ( ! self::starts_with( 'return', $line ) ) { $line = 'return ' . $line; } // Write directly to STDOUT, to sidestep any output buffers created by plugins ob_start(); // phpcs:ignore Squiz.PHP.Eval.Discouraged -- This is meant to be a REPL, no way to avoid eval. $evl = eval( $line ); $out = ob_get_clean(); if ( 0 < strlen( $out ) ) { echo rtrim( $out, "\n" ) . "\n"; } echo '=> '; var_dump( $evl ); fwrite( STDOUT, ob_get_clean() ); } } } private static function non_expressions() { return implode( '|', array( 'echo', 'global', 'unset', 'function', 'while', 'for', 'foreach', 'if', 'switch', 'include', 'include\_once', 'require', 'require\_once', ) ); } private function prompt() { $full_line = false; $done = false; do { $prompt = ( ! $done && false !== $full_line ) ? '--> ' : $this->prompt; $fp = popen( self::create_prompt_cmd( $prompt, $this->history_file ), 'r' ); $line = fgets( $fp ); pclose( $fp ); if ( ! $line ) { break; } $line = rtrim( $line, "\n" ); if ( $line && '\\' === $line[ strlen( $line ) - 1 ] ) { $line = substr( $line, 0, -1 ); } else { $done = true; } $full_line .= $line; } while ( ! $done ); if ( false === $full_line ) { return 'exit'; } return $full_line; } private static function create_prompt_cmd( $prompt, $history_path ) { $prompt = escapeshellarg( $prompt ); $history_path = escapeshellarg( $history_path ); if ( getenv( 'WP_CLI_CUSTOM_SHELL' ) ) { $shell_binary = getenv( 'WP_CLI_CUSTOM_SHELL' ); } else { $shell_binary = '/bin/bash'; } if ( ! is_file( $shell_binary ) || ! is_readable( $shell_binary ) ) { WP_CLI::error( "The shell binary '{$shell_binary}' is not valid. You can override the shell to be used through the WP_CLI_CUSTOM_SHELL environment variable." ); } $shell_binary = escapeshellarg( $shell_binary ); $cmd = 'set -f; ' . "history -r {$history_path}; " . 'LINE=""; ' . "read -re -p {$prompt} LINE; " . '[ $? -eq 0 ] || exit; ' . 'history -s "$LINE"; ' . "history -w {$history_path}; " . 'echo $LINE; '; return "{$shell_binary} -c " . escapeshellarg( $cmd ); } private function set_history_file() { $data = getcwd() . get_current_user(); $this->history_file = \WP_CLI\Utils\get_temp_dir() . 'wp-cli-history-' . md5( $data ); } private static function starts_with( $tokens, $line ) { return preg_match( "/^($tokens)[\(\s]+/", $line ); } } <?php use WP_CLI\Utils; class Shell_Command extends WP_CLI_Command { /** * Opens an interactive PHP console for running and testing PHP code. * * `wp shell` allows you to evaluate PHP statements and expressions * interactively, from within a WordPress environment. Type a bit of code, * hit enter, and see the code execute right before you. Because WordPress * is loaded, you have access to all the functions, classes and globals * that you can use within a WordPress plugin, for example. * * ## OPTIONS * * [--basic] * : Force the use of WP-CLI's built-in PHP REPL, even if the Boris or * PsySH PHP REPLs are available. * * ## EXAMPLES * * # Call get_bloginfo() to get the name of the site. * $ wp shell * wp> get_bloginfo( 'name' ); * => string(6) "WP-CLI" */ public function __invoke( $_, $assoc_args ) { $implementations = array( 'Psy\\Shell', 'Boris\\Boris', 'WP_CLI\\Shell\\REPL', ); if ( Utils\get_flag_value( $assoc_args, 'basic' ) ) { $class = 'WP_CLI\\Shell\\REPL'; } else { foreach ( $implementations as $candidate ) { if ( class_exists( $candidate ) ) { $class = $candidate; break; } } } if ( 'Psy\\Shell' === $class ) { $shell = new Psy\Shell(); $shell->run(); } else { $repl = new $class( 'wp> ' ); $repl->start(); } } } <?php if ( ! class_exists( 'WP_CLI' ) ) { return; } $wpcli_shell_autoloader = dirname( __FILE__ ) . '/vendor/autoload.php'; if ( file_exists( $wpcli_shell_autoloader ) ) { require_once $wpcli_shell_autoloader; } WP_CLI::add_command( 'shell', 'Shell_Command' ); #!/usr/bin/env sh # Special case for *AMP installers, since they normally don't set themselves # as the default cli php out of the box. for amp_php in $(cat $(dirname $0)/amp-paths.txt); do if [ -x $amp_php ]; then echo $amp_php exit fi done which php || which php-cli if [ $? -eq 0 ]; then exit fi echo "no PHP binary found" >&2 exit 1 <?php /** * List all contributors to this release. * * Usage: wp --require=utils/contrib-list.php contrib-list * * If you run into GitHub API rate limit issues, set a GITHUB_TOKEN * environment variable. */ use WP_CLI\Utils; class Contrib_List_Command { /** * List all contributors to this release. * * Run within the main WP-CLI project repository. * * ## OPTIONS * * [--format=<format>] * : Render output in a specific format. * --- * default: markdown * options: * - markdown * - html * --- * * @when before_wp_load */ public function __invoke( $_, $assoc_args ) { $contributors = array(); $contributor_count = 0; $pull_request_count = 0; // Get the contributors to the current open large project milestones foreach ( array( 'wp-cli/wp-cli', 'wp-cli/handbook', 'wp-cli/wp-cli.github.com' ) as $repo ) { $milestones = self::get_project_milestones( $repo ); // Cheap way to get the latest milestone $milestone = array_shift( $milestones ); WP_CLI::log( 'Current open ' . $repo . ' milestone: ' . $milestone->title ); $pull_requests = self::get_project_milestone_pull_requests( $repo, $milestone->number ); $repo_contributors = self::parse_contributors_from_pull_requests( $pull_requests ); WP_CLI::log( ' - Contributors: ' . count( $repo_contributors ) ); WP_CLI::log( ' - Pull requests: ' . count( $pull_requests ) ); $pull_request_count += count( $pull_requests ); $contributors = array_merge( $contributors, $repo_contributors ); } // Identify all command dependencies and their contributors $milestones = self::get_project_milestones( 'wp-cli/wp-cli', array( 'state' => 'closed' ) ); // Cheap way to get the latest closed milestone $milestone = array_shift( $milestones ); $composer_lock_url = sprintf( 'https://raw.githubusercontent.com/wp-cli/wp-cli/v%s/composer.lock', $milestone->title ); WP_CLI::log( 'Fetching ' . $composer_lock_url ); $response = Utils\http_request( 'GET', $composer_lock_url ); if ( 200 !== $response->status_code ) { WP_CLI::error( sprintf( 'Could not fetch composer.json (HTTP code %d)', $response->status_code ) ); } $composer_json = json_decode( $response->body, true ); foreach ( $composer_json['packages'] as $package ) { $package_name = $package['name']; $version_constraint = str_replace( 'v', '', $package['version'] ); if ( ! preg_match( '#^wp-cli/.+-command$#', $package_name ) ) { continue; } // Closed milestones denote a tagged release $milestones = self::get_project_milestones( $package_name, array( 'state' => 'closed' ) ); $milestone_ids = array(); $milestone_titles = array(); foreach ( $milestones as $milestone ) { if ( ! version_compare( $milestone->title, $version_constraint, '>' ) ) { continue; } $milestone_ids[] = $milestone->number; $milestone_titles[] = $milestone->title; } // No shipped releases for this milestone. if ( empty( $milestone_ids ) ) { continue; } WP_CLI::log( 'Closed ' . $package_name . ' milestone(s): ' . implode( ', ', $milestone_titles ) ); foreach ( $milestone_ids as $milestone_id ) { $pull_requests = self::get_project_milestone_pull_requests( $package_name, $milestone_id ); $repo_contributors = self::parse_contributors_from_pull_requests( $pull_requests ); WP_CLI::log( ' - Contributors: ' . count( $repo_contributors ) ); WP_CLI::log( ' - Pull requests: ' . count( $pull_requests ) ); $pull_request_count += count( $pull_requests ); $contributors = array_merge( $contributors, $repo_contributors ); } } WP_CLI::log( 'Total contributors: ' . count( $contributors ) ); WP_CLI::log( 'Total pull requests: ' . $pull_request_count ); // Sort and render the contributor list asort( $contributors, SORT_NATURAL | SORT_FLAG_CASE ); if ( in_array( $assoc_args['format'], array( 'markdown', 'html' ), true ) ) { $contrib_list = ''; foreach ( $contributors as $url => $login ) { if ( 'markdown' === $assoc_args['format'] ) { $contrib_list .= '[' . $login . '](' . $url . '), '; } elseif ( 'html' === $assoc_args['format'] ) { $contrib_list .= '<a href="' . $url . '">' . $login . '</a>, '; } } $contrib_list = rtrim( $contrib_list, ', ' ); WP_CLI::log( $contrib_list ); } } /** * Get the milestones for a given project * * @param string $project * @return array */ private static function get_project_milestones( $project, $args = array() ) { $request_url = sprintf( 'https://api.github.com/repos/%s/milestones', $project ); list( $body, $headers ) = self::make_github_api_request( $request_url, $args ); return $body; } /** * Get the pull requests assigned to a milestone of a given project * * @param string $project * @param integer $milestone_id * @return array */ private static function get_project_milestone_pull_requests( $project, $milestone_id ) { $request_url = sprintf( 'https://api.github.com/repos/%s/issues', $project ); $args = array( 'milestone' => $milestone_id, 'state' => 'all', ); $pull_requests = array(); do { list( $body, $headers ) = self::make_github_api_request( $request_url, $args ); foreach ( $body as $issue ) { if ( ! empty( $issue->pull_request ) ) { $pull_requests[] = $issue; } } $args = array(); $request_url = false; // Set $request_url to 'rel="next" if present' if ( ! empty( $headers['Link'] ) ) { $bits = trim( explode( ',', $headers['Link'] ) ); foreach ( $bits as $bit ) { if ( false !== stripos( $bit, 'rel="next"' ) ) { $hrefandrel = explode( '; ', $bit ); $request_url = trim( $hrefandrel[0], '<>' ); break; } } } } while ( $request_url ); return $pull_requests; } /** * Parse the contributors from pull request objects * * @param array $pull_requests * @return array */ private static function parse_contributors_from_pull_requests( $pull_requests ) { $contributors = array(); foreach ( $pull_requests as $pull_request ) { if ( ! empty( $pull_request->user ) ) { $contributors[ $pull_request->user->html_url ] = $pull_request->user->login; } } return $contributors; } /** * Make a request to the GitHub API * * @param string $url * @param string $args * @return array */ private static function make_github_api_request( $url, $args = array() ) { $headers = array( 'Accept' => 'application/vnd.github.v3+json', 'User-Agent' => 'WP-CLI', ); $token = getenv( 'GITHUB_TOKEN' ); if ( false !== $token ) { $headers['Authorization'] = 'token ' . $token; } $response = Utils\http_request( 'GET', $url, $args, $headers ); if ( 200 !== $response->status_code ) { WP_CLI::error( sprintf( 'GitHub API returned: %s (HTTP code %d)', $response->body, $response->status_code ) ); } return array( json_decode( $response->body ), $response->headers ); } } WP_CLI::add_command( 'contrib-list', 'Contrib_List_Command' ); <?php $file = $argv[1]; if ( ! file_exists( $file ) ) { echo 'File does not exist.'; exit( 1 ); } $contents = file_get_contents( $file ); $composer = json_decode( $contents ); if ( empty( $composer ) || ! is_object( $composer ) ) { echo 'Invalid composer.json for package.'; exit( 1 ); } if ( empty( $composer->autoload->files ) ) { echo 'composer.json must specify valid "autoload" => "files"'; exit( 1 ); } echo implode( PHP_EOL, $composer->autoload->files ); exit( 0 ); <?php namespace WP_CLI; use ArrayObject; use Exception; class SearchReplacer { private $from; private $to; private $recurse_objects; private $regex; private $regex_flags; private $regex_delimiter; private $regex_limit; private $logging; private $log_data; private $max_recursion; /** * @param string $from String we're looking to replace. * @param string $to What we want it to be replaced with. * @param bool $recurse_objects Should objects be recursively replaced? * @param bool $regex Whether `$from` is a regular expression. * @param string $regex_flags Flags for regular expression. * @param string $regex_delimiter Delimiter for regular expression. * @param bool $logging Whether logging. * @param integer $regex_limit The maximum possible replacements for each pattern in each subject string. */ public function __construct( $from, $to, $recurse_objects = false, $regex = false, $regex_flags = '', $regex_delimiter = '/', $logging = false, $regex_limit = -1 ) { $this->from = $from; $this->to = $to; $this->recurse_objects = $recurse_objects; $this->regex = $regex; $this->regex_flags = $regex_flags; $this->regex_delimiter = $regex_delimiter; $this->regex_limit = $regex_limit; $this->logging = $logging; $this->clear_log_data(); // Get the XDebug nesting level. Will be zero (no limit) if no value is set $this->max_recursion = intval( ini_get( 'xdebug.max_nesting_level' ) ); } /** * Take a serialised array and unserialise it replacing elements as needed and * unserialising any subordinate arrays and performing the replace on those too. * Ignores any serialized objects unless $recurse_objects is set to true. * * @param array|string $data The data to operate on. * @param bool $serialised Does the value of $data need to be unserialized? * * @return array The original array with all elements replaced as needed. */ public function run( $data, $serialised = false ) { return $this->run_recursively( $data, $serialised ); } /** * @param int $recursion_level Current recursion depth within the original data. * @param array $visited_data Data that has been seen in previous recursion iterations. */ private function run_recursively( $data, $serialised, $recursion_level = 0, $visited_data = array() ) { // some unseriliased data cannot be re-serialised eg. SimpleXMLElements try { if ( $this->recurse_objects ) { // If we've reached the maximum recursion level, short circuit if ( 0 !== $this->max_recursion && $recursion_level >= $this->max_recursion ) { return $data; } if ( is_array( $data ) || is_object( $data ) ) { // If we've seen this exact object or array before, short circuit if ( in_array( $data, $visited_data, true ) ) { return $data; // Avoid infinite loops when there's a cycle } // Add this data to the list of $visited_data[] = $data; } } $unserialized = is_string( $data ) ? @unserialize( $data ) : false; if ( false !== $unserialized ) { $data = $this->run_recursively( $unserialized, true, $recursion_level + 1 ); } elseif ( is_array( $data ) ) { $keys = array_keys( $data ); foreach ( $keys as $key ) { $data[ $key ] = $this->run_recursively( $data[ $key ], false, $recursion_level + 1, $visited_data ); } } elseif ( $this->recurse_objects && ( is_object( $data ) || $data instanceof \__PHP_Incomplete_Class ) ) { if ( $data instanceof \__PHP_Incomplete_Class ) { $array = new ArrayObject( $data ); \WP_CLI::warning( sprintf( 'Skipping an uninitialized class "%s", replacements might not be complete.', $array['__PHP_Incomplete_Class_Name'] ) ); } else { foreach ( $data as $key => $value ) { $data->$key = $this->run_recursively( $value, false, $recursion_level + 1, $visited_data ); } } } elseif ( is_string( $data ) ) { if ( $this->logging ) { $old_data = $data; } if ( $this->regex ) { $search_regex = $this->regex_delimiter; $search_regex .= $this->from; $search_regex .= $this->regex_delimiter; $search_regex .= $this->regex_flags; $result = preg_replace( $search_regex, $this->to, $data, $this->regex_limit ); if ( null === $result || PREG_NO_ERROR !== preg_last_error() ) { \WP_CLI::warning( sprintf( 'The provided regular expression threw a PCRE error - %s', $this->preg_error_message( $result ) ) ); } $data = $result; } else { $data = str_replace( $this->from, $this->to, $data ); } if ( $this->logging && $old_data !== $data ) { $this->log_data[] = $old_data; } } if ( $serialised ) { return serialize( $data ); } } catch ( Exception $error ) { // phpcs:ignore Generic.CodeAnalysis.EmptyStatement.DetectedCatch -- Deliberally empty. } return $data; } /** * Gets existing data saved for this run when logging. * @return array Array of data strings, prior to replacements. */ public function get_log_data() { return $this->log_data; } /** * Clears data stored for logging. */ public function clear_log_data() { $this->log_data = array(); } /** * Get the PCRE error constant name from an error value. * * @param integer $error Error code. * @return string Error constant name. */ private function preg_error_message( $error ) { static $error_names = null; if ( null === $error_names ) { $definitions = get_defined_constants( true ); $pcre_constants = array_key_exists( 'pcre', $definitions ) ? $definitions['pcre'] : array(); $error_names = array_flip( $pcre_constants ); } return isset( $error_names[ $error ] ) ? $error_names[ $error ] : '<unknown error>'; } } <?php use cli\Colors; use cli\Table; use WP_CLI\Iterators; use WP_CLI\SearchReplacer; use WP_CLI\Utils; use function cli\safe_substr; class Search_Replace_Command extends WP_CLI_Command { private $dry_run; private $export_handle = false; private $export_insert_size; private $recurse_objects; private $regex; private $regex_flags; private $regex_delimiter; private $regex_limit = -1; private $skip_tables; private $skip_columns; private $include_columns; private $format; private $report; private $verbose; private $report_changed_only; private $log_handle = null; private $log_before_context = 40; private $log_after_context = 40; private $log_prefixes = array( '< ', '> ' ); private $log_colors; private $log_encoding; /** * Searches/replaces strings in the database. * * Searches through all rows in a selection of tables and replaces * appearances of the first string with the second string. * * By default, the command uses tables registered to the `$wpdb` object. On * multisite, this will just be the tables for the current site unless * `--network` is specified. * * Search/replace intelligently handles PHP serialized data, and does not * change primary key values. * * ## OPTIONS * * <old> * : A string to search for within the database. * * <new> * : Replace instances of the first string with this new string. * * [<table>...] * : List of database tables to restrict the replacement to. Wildcards are * supported, e.g. `'wp_*options'` or `'wp_post*'`. * * [--dry-run] * : Run the entire search/replace operation and show report, but don't save * changes to the database. * * [--network] * : Search/replace through all the tables registered to $wpdb in a * multisite install. * * [--all-tables-with-prefix] * : Enable replacement on any tables that match the table prefix even if * not registered on $wpdb. * * [--all-tables] * : Enable replacement on ALL tables in the database, regardless of the * prefix, and even if not registered on $wpdb. Overrides --network * and --all-tables-with-prefix. * * [--export[=<file>]] * : Write transformed data as SQL file instead of saving replacements to * the database. If <file> is not supplied, will output to STDOUT. * * [--export_insert_size=<rows>] * : Define number of rows in single INSERT statement when doing SQL export. * You might want to change this depending on your database configuration * (e.g. if you need to do fewer queries). Default: 50 * * [--skip-tables=<tables>] * : Do not perform the replacement on specific tables. Use commas to * specify multiple tables. Wildcards are supported, e.g. `'wp_*options'` or `'wp_post*'`. * * [--skip-columns=<columns>] * : Do not perform the replacement on specific columns. Use commas to * specify multiple columns. * * [--include-columns=<columns>] * : Perform the replacement on specific columns. Use commas to * specify multiple columns. * * [--precise] * : Force the use of PHP (instead of SQL) which is more thorough, * but slower. * * [--recurse-objects] * : Enable recursing into objects to replace strings. Defaults to true; * pass --no-recurse-objects to disable. * * [--verbose] * : Prints rows to the console as they're updated. * * [--regex] * : Runs the search using a regular expression (without delimiters). * Warning: search-replace will take about 15-20x longer when using --regex. * * [--regex-flags=<regex-flags>] * : Pass PCRE modifiers to regex search-replace (e.g. 'i' for case-insensitivity). * * [--regex-delimiter=<regex-delimiter>] * : The delimiter to use for the regex. It must be escaped if it appears in the search string. The default value is the result of `chr(1)`. * * [--regex-limit=<regex-limit>] * : The maximum possible replacements for the regex per row (or per unserialized data bit per row). Defaults to -1 (no limit). * * [--format=<format>] * : Render output in a particular format. * --- * default: table * options: * - table * - count * --- * * [--report] * : Produce report. Defaults to true. * * [--report-changed-only] * : Report changed fields only. Defaults to false, unless logging, when it defaults to true. * * [--log[=<file>]] * : Log the items changed. If <file> is not supplied or is "-", will output to STDOUT. * Warning: causes a significant slow down, similar or worse to enabling --precise or --regex. * * [--before_context=<num>] * : For logging, number of characters to display before the old match and the new replacement. Default 40. Ignored if not logging. * * [--after_context=<num>] * : For logging, number of characters to display after the old match and the new replacement. Default 40. Ignored if not logging. * * ## EXAMPLES * * # Search and replace but skip one column * $ wp search-replace 'http://example.test' 'http://example.com' --skip-columns=guid * * # Run search/replace operation but dont save in database * $ wp search-replace 'foo' 'bar' wp_posts wp_postmeta wp_terms --dry-run * * # Run case-insensitive regex search/replace operation (slow) * $ wp search-replace '\[foo id="([0-9]+)"' '[bar id="\1"' --regex --regex-flags='i' * * # Turn your production multisite database into a local dev database * $ wp search-replace --url=example.com example.com example.test 'wp_*options' wp_blogs * * # Search/replace to a SQL file without transforming the database * $ wp search-replace foo bar --export=database.sql * * # Bash script: Search/replace production to development url (multisite compatible) * #!/bin/bash * if $(wp --url=http://example.com core is-installed --network); then * wp search-replace --url=http://example.com 'http://example.com' 'http://example.test' --recurse-objects --network --skip-columns=guid --skip-tables=wp_users * else * wp search-replace 'http://example.com' 'http://example.test' --recurse-objects --skip-columns=guid --skip-tables=wp_users * fi */ public function __invoke( $args, $assoc_args ) { global $wpdb; $old = array_shift( $args ); $new = array_shift( $args ); $total = 0; $report = array(); $this->dry_run = Utils\get_flag_value( $assoc_args, 'dry-run' ); $php_only = Utils\get_flag_value( $assoc_args, 'precise' ); $this->recurse_objects = Utils\get_flag_value( $assoc_args, 'recurse-objects', true ); $this->verbose = Utils\get_flag_value( $assoc_args, 'verbose' ); $this->format = Utils\get_flag_value( $assoc_args, 'format' ); $this->regex = Utils\get_flag_value( $assoc_args, 'regex', false ); if ( null !== $this->regex ) { $default_regex_delimiter = false; $this->regex_flags = Utils\get_flag_value( $assoc_args, 'regex-flags', false ); $this->regex_delimiter = Utils\get_flag_value( $assoc_args, 'regex-delimiter', '' ); if ( '' === $this->regex_delimiter ) { $this->regex_delimiter = chr( 1 ); $default_regex_delimiter = true; } } $regex_limit = Utils\get_flag_value( $assoc_args, 'regex-limit' ); if ( null !== $regex_limit ) { if ( ! preg_match( '/^(?:[0-9]+|-1)$/', $regex_limit ) || 0 === (int) $regex_limit ) { WP_CLI::error( '`--regex-limit` expects a non-zero positive integer or -1.' ); } $this->regex_limit = (int) $regex_limit; } if ( ! empty( $this->regex ) ) { if ( '' === $this->regex_delimiter ) { $this->regex_delimiter = chr( 1 ); } $search_regex = $this->regex_delimiter; $search_regex .= $old; $search_regex .= $this->regex_delimiter; $search_regex .= $this->regex_flags; // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged -- Preventing a warning when testing the regex. if ( false === @preg_match( $search_regex, '' ) ) { $error = error_get_last(); $preg_error_message = ( ! empty( $error ) && array_key_exists( 'message', $error ) ) ? "\n{$error['message']}." : ''; if ( $default_regex_delimiter ) { $flags_msg = $this->regex_flags ? "flags '$this->regex_flags'" : 'no flags'; $msg = "The regex pattern '$old' with default delimiter 'chr(1)' and {$flags_msg} fails."; } else { $msg = "The regex '$search_regex' fails."; } WP_CLI::error( $msg . $preg_error_message ); } } $this->skip_columns = explode( ',', Utils\get_flag_value( $assoc_args, 'skip-columns' ) ); $this->skip_tables = explode( ',', Utils\get_flag_value( $assoc_args, 'skip-tables' ) ); $this->include_columns = array_filter( explode( ',', Utils\get_flag_value( $assoc_args, 'include-columns' ) ) ); if ( $old === $new && ! $this->regex ) { WP_CLI::warning( "Replacement value '{$old}' is identical to search value '{$new}'. Skipping operation." ); exit; } $export = Utils\get_flag_value( $assoc_args, 'export' ); if ( null !== $export ) { if ( $this->dry_run ) { WP_CLI::error( 'You cannot supply --dry-run and --export at the same time.' ); } if ( true === $export ) { $this->export_handle = STDOUT; $this->verbose = false; } else { $this->export_handle = @fopen( $assoc_args['export'], 'w' ); if ( false === $this->export_handle ) { $error = error_get_last(); WP_CLI::error( sprintf( 'Unable to open export file "%s" for writing: %s.', $assoc_args['export'], $error['message'] ) ); } } $export_insert_size = Utils\get_flag_value( $assoc_args, 'export_insert_size', 50 ); // phpcs:ignore WordPress.PHP.StrictComparisons.LooseComparison -- See the code, this is deliberate. if ( (int) $export_insert_size == $export_insert_size && $export_insert_size > 0 ) { $this->export_insert_size = $export_insert_size; } $php_only = true; } $log = Utils\get_flag_value( $assoc_args, 'log' ); if ( null !== $log ) { if ( true === $log || '-' === $log ) { $this->log_handle = STDOUT; } else { $this->log_handle = @fopen( $assoc_args['log'], 'w' ); if ( false === $this->log_handle ) { $error = error_get_last(); WP_CLI::error( sprintf( 'Unable to open log file "%s" for writing: %s.', $assoc_args['log'], $error['message'] ) ); } } if ( $this->log_handle ) { $before_context = Utils\get_flag_value( $assoc_args, 'before_context' ); if ( null !== $before_context && preg_match( '/^[0-9]+$/', $before_context ) ) { $this->log_before_context = (int) $before_context; } $after_context = Utils\get_flag_value( $assoc_args, 'after_context' ); if ( null !== $after_context && preg_match( '/^[0-9]+$/', $after_context ) ) { $this->log_after_context = (int) $after_context; } $log_prefixes = getenv( 'WP_CLI_SEARCH_REPLACE_LOG_PREFIXES' ); if ( false !== $log_prefixes && preg_match( '/^([^,]*),([^,]*)$/', $log_prefixes, $matches ) ) { $this->log_prefixes = array( $matches[1], $matches[2] ); } if ( STDOUT === $this->log_handle ) { $default_log_colors = array( 'log_table_column_id' => '%B', 'log_old' => '%R', 'log_new' => '%G', ); } else { $default_log_colors = array( 'log_table_column_id' => '', 'log_old' => '', 'log_new' => '', ); } $log_colors = getenv( 'WP_CLI_SEARCH_REPLACE_LOG_COLORS' ); if ( false !== $log_colors && preg_match( '/^([^,]*),([^,]*),([^,]*)$/', $log_colors, $matches ) ) { $default_log_colors = array( 'log_table_column_id' => $matches[1], 'log_old' => $matches[2], 'log_new' => $matches[3], ); } $this->log_colors = $this->get_colors( $assoc_args, $default_log_colors ); $this->log_encoding = 0 === strpos( $wpdb->charset, 'utf8' ) ? 'UTF-8' : false; } } $this->report = Utils\get_flag_value( $assoc_args, 'report', true ); // Defaults to true if logging, else defaults to false. $this->report_changed_only = Utils\get_flag_value( $assoc_args, 'report-changed-only', null !== $this->log_handle ); if ( $this->regex_flags ) { $php_only = true; } // never mess with hashed passwords $this->skip_columns[] = 'user_pass'; // Get table names based on leftover $args or supplied $assoc_args $tables = Utils\wp_get_table_names( $args, $assoc_args ); foreach ( $tables as $table ) { foreach ( $this->skip_tables as $skip_table ) { if ( fnmatch( $skip_table, $table ) ) { continue 2; } } $table_sql = self::esc_sql_ident( $table ); if ( $this->export_handle ) { fwrite( $this->export_handle, "\nDROP TABLE IF EXISTS $table_sql;\n" ); // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- escaped through self::esc_sql_ident $row = $wpdb->get_row( "SHOW CREATE TABLE $table_sql", ARRAY_N ); fwrite( $this->export_handle, $row[1] . ";\n" ); list( $table_report, $total_rows ) = $this->php_export_table( $table, $old, $new ); if ( $this->report ) { $report = array_merge( $report, $table_report ); } $total += $total_rows; // Don't perform replacements on the actual database continue; } list( $primary_keys, $columns, $all_columns ) = self::get_columns( $table ); // since we'll be updating one row at a time, // we need a primary key to identify the row if ( empty( $primary_keys ) ) { // wasn't updated, so skip to the next table if ( $this->report_changed_only ) { continue; } if ( $this->report ) { $report[] = array( $table, '', 'skipped', '' ); } else { WP_CLI::warning( $all_columns ? "No primary keys for table '$table'." : "No such table '$table'." ); } continue; } foreach ( $columns as $col ) { if ( ! empty( $this->include_columns ) && ! in_array( $col, $this->include_columns, true ) ) { continue; } if ( in_array( $col, $this->skip_columns, true ) ) { continue; } if ( $this->verbose && 'count' !== $this->format ) { $this->start_time = microtime( true ); WP_CLI::log( sprintf( 'Checking: %s.%s', $table, $col ) ); } if ( ! $php_only && ! $this->regex ) { $col_sql = self::esc_sql_ident( $col ); $wpdb->last_error = ''; // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- escaped through self::esc_sql_ident $serial_row = $wpdb->get_row( "SELECT * FROM $table_sql WHERE $col_sql REGEXP '^[aiO]:[1-9]' LIMIT 1" ); // When the regex triggers an error, we should fall back to PHP if ( false !== strpos( $wpdb->last_error, 'ERROR 1139' ) ) { $serial_row = true; } } if ( $php_only || $this->regex || null !== $serial_row ) { $type = 'PHP'; $count = $this->php_handle_col( $col, $primary_keys, $table, $old, $new ); } else { $type = 'SQL'; $count = $this->sql_handle_col( $col, $primary_keys, $table, $old, $new ); } if ( $this->report && ( $count || ! $this->report_changed_only ) ) { $report[] = array( $table, $col, $count, $type ); } $total += $count; } } if ( $this->export_handle && STDOUT !== $this->export_handle ) { fclose( $this->export_handle ); } // Only informational output after this point if ( WP_CLI::get_config( 'quiet' ) || STDOUT === $this->export_handle ) { return; } if ( 'count' === $this->format ) { WP_CLI::line( $total ); return; } if ( $this->report && ! empty( $report ) ) { $table = new Table(); $table->setHeaders( array( 'Table', 'Column', 'Replacements', 'Type' ) ); $table->setRows( $report ); $table->display(); } if ( ! $this->dry_run ) { if ( ! empty( $assoc_args['export'] ) ) { $success_message = 1 === $total ? "Made 1 replacement and exported to {$assoc_args['export']}." : "Made {$total} replacements and exported to {$assoc_args['export']}."; } else { $success_message = 1 === $total ? 'Made 1 replacement.' : "Made $total replacements."; if ( $total && 'Default' !== Utils\wp_get_cache_type() ) { $success_message .= ' Please remember to flush your persistent object cache with `wp cache flush`.'; } } WP_CLI::success( $success_message ); } else { $success_message = ( 1 === $total ) ? '%d replacement to be made.' : '%d replacements to be made.'; WP_CLI::success( sprintf( $success_message, $total ) ); } } private function php_export_table( $table, $old, $new ) { list( $primary_keys, $columns, $all_columns ) = self::get_columns( $table ); $chunk_size = getenv( 'BEHAT_RUN' ) ? 10 : 1000; $args = array( 'table' => $table, 'fields' => $all_columns, 'chunk_size' => $chunk_size, ); $replacer = new SearchReplacer( $old, $new, $this->recurse_objects, $this->regex, $this->regex_flags, $this->regex_delimiter, false, $this->regex_limit ); $col_counts = array_fill_keys( $all_columns, 0 ); if ( $this->verbose && 'table' === $this->format ) { $this->start_time = microtime( true ); WP_CLI::log( sprintf( 'Checking: %s', $table ) ); } $rows = array(); foreach ( new Iterators\Table( $args ) as $i => $row ) { $row_fields = array(); foreach ( $all_columns as $col ) { $value = $row->$col; if ( $value && ! in_array( $col, $primary_keys, true ) && ! in_array( $col, $this->skip_columns, true ) ) { $new_value = $replacer->run( $value ); if ( $new_value !== $value ) { $col_counts[ $col ]++; $value = $new_value; } } $row_fields[ $col ] = $value; } $rows[] = $row_fields; } $this->write_sql_row_fields( $table, $rows ); $table_report = array(); $total_rows = 0; $total_cols = 0; foreach ( $col_counts as $col => $col_count ) { if ( $this->report && ( $col_count || ! $this->report_changed_only ) ) { $table_report[] = array( $table, $col, $col_count, 'PHP' ); } if ( $col_count ) { $total_cols++; $total_rows += $col_count; } } if ( $this->verbose && 'table' === $this->format ) { $time = round( microtime( true ) - $this->start_time, 3 ); WP_CLI::log( sprintf( '%d columns and %d total rows affected using PHP (in %ss).', $total_cols, $total_rows, $time ) ); } return array( $table_report, $total_rows ); } private function sql_handle_col( $col, $primary_keys, $table, $old, $new ) { global $wpdb; $table_sql = self::esc_sql_ident( $table ); $col_sql = self::esc_sql_ident( $col ); if ( $this->dry_run ) { if ( $this->log_handle ) { $count = $this->log_sql_diff( $col, $primary_keys, $table, $old, $new ); } else { // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- escaped through self::esc_sql_ident $count = $wpdb->get_var( $wpdb->prepare( "SELECT COUNT($col_sql) FROM $table_sql WHERE $col_sql LIKE BINARY %s;", '%' . self::esc_like( $old ) . '%' ) ); } } else { if ( $this->log_handle ) { $this->log_sql_diff( $col, $primary_keys, $table, $old, $new ); } // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- escaped through self::esc_sql_ident $count = $wpdb->query( $wpdb->prepare( "UPDATE $table_sql SET $col_sql = REPLACE($col_sql, %s, %s);", $old, $new ) ); } if ( $this->verbose && 'table' === $this->format ) { $time = round( microtime( true ) - $this->start_time, 3 ); WP_CLI::log( sprintf( '%d rows affected using SQL (in %ss).', $count, $time ) ); } return $count; } private function php_handle_col( $col, $primary_keys, $table, $old, $new ) { global $wpdb; $count = 0; $replacer = new SearchReplacer( $old, $new, $this->recurse_objects, $this->regex, $this->regex_flags, $this->regex_delimiter, null !== $this->log_handle, $this->regex_limit ); $table_sql = self::esc_sql_ident( $table ); $col_sql = self::esc_sql_ident( $col ); $where = $this->regex ? '' : " WHERE $col_sql" . $wpdb->prepare( ' LIKE BINARY %s', '%' . self::esc_like( $old ) . '%' ); $escaped_primary_keys = self::esc_sql_ident( $primary_keys ); $primary_keys_sql = implode( ',', $escaped_primary_keys ); $order_by_keys = array_map( static function ( $key ) { return "{$key} ASC"; }, $escaped_primary_keys ); $order_by_sql = 'ORDER BY ' . implode( ',', $order_by_keys ); $limit = 1000; $offset = 0; // Updates have to be deferred to after the chunking is completed, as // the offset will otherwise not work correctly. $updates = []; // 2 errors: // - WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- escaped through self::esc_sql_ident // - WordPress.CodeAnalysis.AssignmentInCondition -- no reason to do copy-paste for a single valid assignment in while // phpcs:ignore while ( $rows = $wpdb->get_results( "SELECT {$primary_keys_sql} FROM {$table_sql} {$where} {$order_by_sql} LIMIT {$limit} OFFSET {$offset}" ) ) { foreach ( $rows as $keys ) { $where_sql = ''; foreach ( (array) $keys as $k => $v ) { if ( '' !== $where_sql ) { $where_sql .= ' AND '; } $where_sql .= self::esc_sql_ident( $k ) . ' = ' . self::esc_sql_value( $v ); } // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- escaped through self::esc_sql_ident $col_value = $wpdb->get_var( "SELECT {$col_sql} FROM {$table_sql} WHERE {$where_sql}" ); if ( '' === $col_value ) { continue; } $value = $replacer->run( $col_value ); if ( $value === $col_value ) { continue; } if ( $this->log_handle ) { $this->log_php_diff( $col, $keys, $table, $old, $new, $replacer->get_log_data() ); $replacer->clear_log_data(); } $count++; if ( ! $this->dry_run ) { $update_where = array(); foreach ( (array) $keys as $k => $v ) { $update_where[ $k ] = $v; } $updates[] = [ $table, array( $col => $value ), $update_where ]; } } $offset += $limit; } foreach ( $updates as $update ) { $wpdb->update( ...$update ); } if ( $this->verbose && 'table' === $this->format ) { $time = round( microtime( true ) - $this->start_time, 3 ); WP_CLI::log( sprintf( '%d rows affected using PHP (in %ss).', $count, $time ) ); } return $count; } private function write_sql_row_fields( $table, $rows ) { global $wpdb; if ( empty( $rows ) ) { return; } $table_sql = self::esc_sql_ident( $table ); $insert = "INSERT INTO $table_sql ("; $insert .= join( ', ', self::esc_sql_ident( array_keys( $rows[0] ) ) ); $insert .= ') VALUES '; $insert .= "\n"; $sql = $insert; $values = array(); $index = 1; $count = count( $rows ); $export_insert_size = $this->export_insert_size; foreach ( $rows as $row_fields ) { $subs = array(); foreach ( $row_fields as $field_value ) { if ( null === $field_value ) { $subs[] = 'NULL'; } else { $subs[] = '%s'; $values[] = $field_value; } } $sql .= '(' . join( ', ', $subs ) . ')'; // Add new insert statement if needed. Before this we close the previous with semicolon and write statement to sql-file. // "Statement break" is needed: // 1. When the loop is running every nth time (where n is insert statement size, $export_index_size). Remainder is zero also on first round, so it have to be excluded. // $index % $export_insert_size == 0 && $index > 0 // 2. Or when the loop is running last time // $index == $count if ( ( 0 === $index % $export_insert_size && $index > 0 ) || $index === $count ) { $sql .= ";\n"; if ( method_exists( $wpdb, 'remove_placeholder_escape' ) ) { // since 4.8.3 // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- verified inputs above $sql = $wpdb->remove_placeholder_escape( $wpdb->prepare( $sql, array_values( $values ) ) ); } else { // 4.8.2 or less // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- verified inputs above $sql = $wpdb->prepare( $sql, array_values( $values ) ); } fwrite( $this->export_handle, $sql ); // If there is still rows to loop, reset $sql and $values variables. if ( $count > $index ) { $sql = $insert; $values = array(); } } else { // Otherwise just add comma and new line $sql .= ",\n"; } $index++; } } private static function get_columns( $table ) { global $wpdb; $table_sql = self::esc_sql_ident( $table ); $primary_keys = array(); $text_columns = array(); $all_columns = array(); $suppress_errors = $wpdb->suppress_errors(); // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- escaped through self::esc_sql_ident $results = $wpdb->get_results( "DESCRIBE $table_sql" ); if ( ! empty( $results ) ) { foreach ( $results as $col ) { if ( 'PRI' === $col->Key ) { $primary_keys[] = $col->Field; } if ( self::is_text_col( $col->Type ) ) { $text_columns[] = $col->Field; } $all_columns[] = $col->Field; } } $wpdb->suppress_errors( $suppress_errors ); return array( $primary_keys, $text_columns, $all_columns ); } private static function is_text_col( $type ) { foreach ( array( 'text', 'varchar' ) as $token ) { if ( false !== strpos( $type, $token ) ) { return true; } } return false; } private static function esc_like( $old ) { global $wpdb; // Remove notices in 4.0 and support backwards compatibility if ( method_exists( $wpdb, 'esc_like' ) ) { // 4.0 $old = $wpdb->esc_like( $old ); } else { // phpcs:ignore WordPress.WP.DeprecatedFunctions.like_escapeFound -- BC-layer for WP 3.9 or less. $old = like_escape( esc_sql( $old ) ); // Note: this double escaping is actually necessary, even though `esc_like()` will be used in a `prepare()`. } return $old; } /** * Escapes (backticks) MySQL identifiers (aka schema object names) - i.e. column names, table names, and database/index/alias/view etc names. * See https://dev.mysql.com/doc/refman/5.5/en/identifiers.html * * @param string|array $idents A single identifier or an array of identifiers. * @return string|array An escaped string if given a string, or an array of escaped strings if given an array of strings. */ private static function esc_sql_ident( $idents ) { $backtick = static function ( $v ) { // Escape any backticks in the identifier by doubling. return '`' . str_replace( '`', '``', $v ) . '`'; }; if ( is_string( $idents ) ) { return $backtick( $idents ); } return array_map( $backtick, $idents ); } /** * Puts MySQL string values in single quotes, to avoid them being interpreted as column names. * * @param string|array $values A single value or an array of values. * @return string|array A quoted string if given a string, or an array of quoted strings if given an array of strings. */ private static function esc_sql_value( $values ) { $quote = static function ( $v ) { // Don't quote integer values to avoid MySQL's implicit type conversion. if ( preg_match( '/^[+-]?[0-9]{1,20}$/', $v ) ) { // MySQL BIGINT UNSIGNED max 18446744073709551615 (20 digits). return esc_sql( $v ); } // Put any string values between single quotes. return "'" . esc_sql( $v ) . "'"; }; if ( is_array( $values ) ) { return array_map( $quote, $values ); } return $quote( $values ); } /** * Gets the color codes from the options if any, and returns the passed in array colorized with 2 elements per entry, a color code (or '') and a reset (or ''). * * @param array $assoc_args The associative argument array passed to the command. * @param array $colors Array of default percent color code strings keyed by the color contexts. * @return array Array containing 2-element arrays keyed to the input $colors array. */ private function get_colors( $assoc_args, $colors ) { $color_reset = WP_CLI::colorize( '%n' ); $color_code_callback = static function ( $v ) { return substr( $v, 1 ); }; $color_codes = array_keys( Colors::getColors() ); $color_codes = array_map( $color_code_callback, $color_codes ); $color_codes = implode( '', $color_codes ); $color_codes_regex = '/^(?:%[' . $color_codes . '])*$/'; foreach ( array_keys( $colors ) as $color_col ) { $col_color_flag = Utils\get_flag_value( $assoc_args, $color_col . '_color' ); if ( null !== $col_color_flag ) { if ( ! preg_match( $color_codes_regex, $col_color_flag, $matches ) ) { WP_CLI::warning( "Unrecognized percent color code '$col_color_flag' for '{$color_col}_color'." ); } else { $colors[ $color_col ] = $matches[0]; } } $colors[ $color_col ] = $colors[ $color_col ] ? array( WP_CLI::colorize( $colors[ $color_col ] ), $color_reset ) : array( '', '' ); } return $colors; } /* * Logs the difference between old match and new replacement for SQL replacement. * * @param string $col Column being processed. * @param array $primary_keys Primary keys for table. * @param string $table Table being processed. * @param string $old Old value to match. * @param string $new New value to replace the old value with. * @return int Count of changed rows. */ private function log_sql_diff( $col, $primary_keys, $table, $old, $new ) { global $wpdb; if ( $primary_keys ) { $esc_primary_keys = implode( ', ', self::esc_sql_ident( $primary_keys ) ); $primary_keys_sql = count( $primary_keys ) > 1 ? "CONCAT_WS(',', {$esc_primary_keys}), " : "{$esc_primary_keys}, "; } else { $primary_keys_sql = ''; } $table_sql = self::esc_sql_ident( $table ); $col_sql = self::esc_sql_ident( $col ); // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- escaped through self::esc_sql_ident $results = $wpdb->get_results( $wpdb->prepare( "SELECT {$primary_keys_sql}{$col_sql} FROM {$table_sql} WHERE {$col_sql} LIKE BINARY %s", '%' . self::esc_like( $old ) . '%' ), ARRAY_N ); if ( empty( $results ) ) { return 0; } $search_regex = '/' . preg_quote( $old, '/' ) . '/'; foreach ( $results as $result ) { list( $keys, $data ) = $primary_keys ? array( $result[0], $result[1] ) : array( null, $result[0] ); if ( preg_match_all( $search_regex, $data, $matches, PREG_OFFSET_CAPTURE ) ) { list( $old_bits, $new_bits ) = $this->log_bits( $search_regex, $data, $matches, $new ); $this->log_write( $col, $keys, $table, $old_bits, $new_bits ); } } return count( $results ); } /* * Logs the difference between old matches and new replacements at the end of a PHP (regex) replacement of a database row. * * @param string $col Column being processed. * @param array $keys Associative array (or object) of primary key names and their values for the row being processed. * @param string $table Table being processed. * @param string $old Old value to match. * @param string $new New value to replace the old value with. * @param array $log_data Array of data strings before replacements. */ private function log_php_diff( $col, $keys, $table, $old, $new, $log_data ) { if ( $this->regex ) { $search_regex = $this->regex_delimiter . $old . $this->regex_delimiter . $this->regex_flags; } else { $search_regex = '/' . preg_quote( $old, '/' ) . '/'; } $old_bits = array(); $new_bits = array(); foreach ( $log_data as $data ) { if ( preg_match_all( $search_regex, $data, $matches, PREG_OFFSET_CAPTURE ) ) { $bits = $this->log_bits( $search_regex, $data, $matches, $new ); $old_bits = array_merge( $old_bits, $bits[0] ); $new_bits = array_merge( $new_bits, $bits[1] ); } } if ( $old_bits ) { $this->log_write( $col, $keys, $table, $old_bits, $new_bits ); } } /** * Returns the arrays of old matches and new replacements based on the passed-in matches, with context. * * @param string $search_regex The search regular expression. * @param string $old_data Existing data being processed. * @param array $old_matches Old matches array returned by `preg_match_all()`. * @param string $new New value to replace the old value with. * @return array Two element array containing the array of old match log strings and the array of new replacement log strings with before/after contexts. */ private function log_bits( $search_regex, $old_data, $old_matches, $new ) { $encoding = $this->log_encoding; if ( ! $encoding && ( $this->log_before_context || $this->log_after_context ) && function_exists( 'mb_detect_encoding' ) ) { $encoding = mb_detect_encoding( $old_data, null, true /*strict*/ ); } // Generate a new data matches analog of the old data matches by simulating a `preg_replace()`. $is_regex = $this->regex; $i = 0; $diff = 0; $new_matches = array(); $new_data = preg_replace_callback( $search_regex, static function ( $matches ) use ( $old_matches, $new, $is_regex, &$new_matches, &$i, &$diff ) { if ( $is_regex ) { // Sub in any back references, "$1", "\2" etc, in the replacement string. $new = preg_replace_callback( '/(?<!\\\\)(?:\\\\\\\\)*((?:\\\\|\\$)[0-9]{1,2}|\\${[0-9]{1,2}\\})/', static function ( $m ) use ( $matches ) { $idx = (int) str_replace( array( '\\', '$', '{', '}' ), '', $m[0] ); return isset( $matches[ $idx ] ) ? $matches[ $idx ] : ''; }, $new ); $new = str_replace( '\\\\', '\\', $new ); // Unescape any backslashed backslashes. } $new_matches[0][ $i ][0] = $new; $new_matches[0][ $i ][1] = $old_matches[0][ $i ][1] + $diff; $diff += strlen( $new ) - strlen( $old_matches[0][ $i ][0] ); $i++; return $new; }, $old_data, $this->regex_limit, $match_cnt ); $old_bits = array(); $new_bits = array(); $append_next = false; $last_old_offset = 0; $last_new_offset = 0; for ( $i = 0; $i < $match_cnt; $i++ ) { $old_match = $old_matches[0][ $i ][0]; $old_offset = $old_matches[0][ $i ][1]; $new_match = $new_matches[0][ $i ][0]; $new_offset = $new_matches[0][ $i ][1]; $old_log = $this->log_colors['log_old'][0] . $old_match . $this->log_colors['log_old'][1]; $new_log = $this->log_colors['log_new'][0] . $new_match . $this->log_colors['log_new'][1]; $old_before = ''; $old_after = ''; $new_before = ''; $new_after = ''; $after_shortened = false; // Offsets are in bytes, so need to use `strlen()` and `substr()` before using `safe_substr()`. if ( $this->log_before_context && $old_offset && ! $append_next ) { $old_before = safe_substr( substr( $old_data, $last_old_offset, $old_offset - $last_old_offset ), -$this->log_before_context, null /*length*/, false /*is_width*/, $encoding ); $new_before = safe_substr( substr( $new_data, $last_new_offset, $new_offset - $last_new_offset ), -$this->log_before_context, null /*length*/, false /*is_width*/, $encoding ); } if ( $this->log_after_context ) { $old_end_offset = $old_offset + strlen( $old_match ); $new_end_offset = $new_offset + strlen( $new_match ); $old_after = safe_substr( substr( $old_data, $old_end_offset ), 0, $this->log_after_context, false /*is_width*/, $encoding ); $new_after = safe_substr( substr( $new_data, $new_end_offset ), 0, $this->log_after_context, false /*is_width*/, $encoding ); // To lessen context duplication in output, shorten the after context if it overlaps with the next match. if ( $i + 1 < $match_cnt && $old_end_offset + strlen( $old_after ) > $old_matches[0][ $i + 1 ][1] ) { $old_after = substr( $old_after, 0, $old_matches[0][ $i + 1 ][1] - $old_end_offset ); $new_after = substr( $new_after, 0, $new_matches[0][ $i + 1 ][1] - $new_end_offset ); $after_shortened = true; // On the next iteration, will append with no before context. } } if ( $append_next ) { $cnt = count( $old_bits ); $old_bits[ $cnt - 1 ] .= $old_log . $old_after; $new_bits[ $cnt - 1 ] .= $new_log . $new_after; } else { $old_bits[] = $old_before . $old_log . $old_after; $new_bits[] = $new_before . $new_log . $new_after; } $append_next = $after_shortened; $last_old_offset = $old_offset; $last_new_offset = $new_offset; } return array( $old_bits, $new_bits ); } /* * Outputs the log strings. * * @param string $col Column being processed. * @param array $keys Associative array (or object) of primary key names and their values for the row being processed. * @param string $table Table being processed. * @param array $old_bits Array of old match log strings. * @param array $new_bits Array of new replacement log strings. */ private function log_write( $col, $keys, $table, $old_bits, $new_bits ) { $id_log = $keys ? ( ':' . implode( ',', (array) $keys ) ) : ''; $table_column_id_log = $this->log_colors['log_table_column_id'][0] . $table . '.' . $col . $id_log . $this->log_colors['log_table_column_id'][1]; $old_log = str_replace( array( "\r\n", "\n" ), ' ', implode( ' [...] ', $old_bits ) ); $new_log = str_replace( array( "\r\n", "\n" ), ' ', implode( ' [...] ', $new_bits ) ); if ( $this->log_prefixes[0] ) { $old_log = $this->log_colors['log_old'][0] . $this->log_prefixes[0] . $this->log_colors['log_old'][1] . $old_log; } if ( $this->log_prefixes[1] ) { $new_log = $this->log_colors['log_new'][0] . $this->log_prefixes[1] . $this->log_colors['log_new'][1] . $new_log; } fwrite( $this->log_handle, "{$table_column_id_log}\n{$old_log}\n{$new_log}\n" ); } } <?php if ( ! class_exists( 'WP_CLI' ) ) { return; } $wpcli_search_replace_autoloader = dirname( __FILE__ ) . '/vendor/autoload.php'; if ( file_exists( $wpcli_search_replace_autoloader ) ) { require_once $wpcli_search_replace_autoloader; } WP_CLI::add_command( 'search-replace', 'Search_Replace_Command' ); <?php /* * WP_CLI: Copied and adapted from vendor/composer/composer/src/Composer/Json/JsonManipulator.php. Changes marked `WP_CLI`. * * This file is part of Composer. * * (c) Nils Adermann <naderman@naderman.de> * Jordi Boggiano <j.boggiano@seld.be> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace WP_CLI; // WP_CLI use Composer\Json\JsonFile; // WP_CLI use Composer\Repository\PlatformRepository; /** * @author Jordi Boggiano <j.boggiano@seld.be> */ class JsonManipulator { private static $DEFINES = '(?(DEFINE) (?<number> -? (?= [1-9]|0(?!\d) ) \d+ (\.\d+)? ([eE] [+-]? \d+)? ) (?<boolean> true | false | null ) (?<string> " ([^"\\\\]* | \\\\ ["\\\\bfnrt\/] | \\\\ u [0-9a-f]{4} )* " ) (?<array> \[ (?: (?&json) \s* (?: , (?&json) \s* )* )? \s* \] ) (?<pair> \s* (?&string) \s* : (?&json) \s* ) (?<object> \{ (?: (?&pair) (?: , (?&pair) )* )? \s* \} ) (?<json> \s* (?: (?&number) | (?&boolean) | (?&string) | (?&array) | (?&object) ) ) )'; private $contents; private $newline; private $indent; public function __construct($contents) { $contents = trim($contents); if ($contents === '') { $contents = '{}'; } if (!$this->pregMatch('#^\{(.*)\}$#s', $contents)) { throw new \InvalidArgumentException('The json file must be an object ({})'); } $this->newline = false !== strpos($contents, "\r\n") ? "\r\n" : "\n"; $this->contents = $contents === '{}' ? '{' . $this->newline . '}' : $contents; $this->detectIndenting(); } public function getContents() { return $this->contents . $this->newline; } public function addLink($type, $package, $constraint, $sortPackages = false, $caseInsensitive = false) // WP_CLI: caseInsensitive. { $decoded = JsonFile::parseJson($this->contents); // no link of that type yet if (!isset($decoded[$type])) { return $this->addMainKey($type, array($package => $constraint)); } $regex = '{'.self::$DEFINES.'^(?P<start>\s*\{\s*(?:(?&string)\s*:\s*(?&json)\s*,\s*)*?)'. '(?P<property>'.preg_quote(JsonFile::encode($type)).'\s*:\s*)(?P<value>(?&json))(?P<end>.*)}sx'; if (!$this->pregMatch($regex, $this->contents, $matches)) { return false; } // WP_CLI: begin caseInsensitive. if ($caseInsensitive) { // Just zap any existing packages first in a case insensitive manner. $this->removeSubNode($type, $package, $caseInsensitive); return $this->addLink($type, $package, $constraint, $sortPackages); } // WP_CLI: end caseInsensitive. $links = $matches['value']; // try to find existing link $packageRegex = str_replace('/', '\\\\?/', preg_quote($package)); $regex = '{'.self::$DEFINES.'"(?P<package>'.$packageRegex.')"(\s*:\s*)(?&string)}ix'; if ($this->pregMatch($regex, $links, $packageMatches)) { // update existing link $existingPackage = $packageMatches['package']; $packageRegex = str_replace('/', '\\\\?/', preg_quote($existingPackage)); $links = preg_replace_callback('{'.self::$DEFINES.'"'.$packageRegex.'"(?P<separator>\s*:\s*)(?&string)}ix', function ($m) use ($existingPackage, $constraint) { return JsonFile::encode(str_replace('\\/', '/', $existingPackage)) . $m['separator'] . '"' . $constraint . '"'; }, $links); } else { if ($this->pregMatch('#^\s*\{\s*\S+.*?(\s*\}\s*)$#s', $links, $match)) { // link missing but non empty links $links = preg_replace( '{'.preg_quote($match[1]).'$}', // addcslashes is used to double up backslashes/$ since preg_replace resolves them as back references otherwise, see #1588 addcslashes(',' . $this->newline . $this->indent . $this->indent . JsonFile::encode($package).': '.JsonFile::encode($constraint) . $match[1], '\\$'), $links ); } else { // links empty $links = '{' . $this->newline . $this->indent . $this->indent . JsonFile::encode($package).': '.JsonFile::encode($constraint) . $this->newline . $this->indent . '}'; } } if (true === $sortPackages) { $requirements = json_decode($links, true); $this->sortPackages($requirements); $links = $this->format($requirements); } $this->contents = $matches['start'] . $matches['property'] . $links . $matches['end']; return true; } /** * Sorts packages by importance (platform packages first, then PHP dependencies) and alphabetically. * * @link https://getcomposer.org/doc/02-libraries.md#platform-packages * * @param array $packages */ private function sortPackages(array &$packages = array()) { $prefix = function ($requirement) { if (preg_match(PlatformRepository::PLATFORM_PACKAGE_REGEX, $requirement)) { return preg_replace( array( '/^php/', '/^hhvm/', '/^ext/', '/^lib/', '/^\D/', ), array( '0-$0', '1-$0', '2-$0', '3-$0', '4-$0', ), $requirement ); } return '5-'.$requirement; }; uksort($packages, function ($a, $b) use ($prefix) { return strnatcmp($prefix($a), $prefix($b)); }); } public function addRepository($name, $config) { return $this->addSubNode('repositories', $name, $config); } public function removeRepository($name) { return $this->removeSubNode('repositories', $name); } public function addConfigSetting($name, $value) { return $this->addSubNode('config', $name, $value); } public function removeConfigSetting($name) { return $this->removeSubNode('config', $name); } public function addProperty($name, $value) { if (substr($name, 0, 6) === 'extra.') { return $this->addSubNode('extra', substr($name, 6), $value); } return $this->addMainKey($name, $value); } public function removeProperty($name) { if (substr($name, 0, 6) === 'extra.') { return $this->removeSubNode('extra', substr($name, 6)); } return $this->removeMainKey($name); } public function addSubNode($mainNode, $name, $value, $caseInsensitive = false) // WP_CLI: caseInsensitive. { $decoded = JsonFile::parseJson($this->contents); $subName = null; if (in_array($mainNode, array('config', 'extra')) && false !== strpos($name, '.')) { list($name, $subName) = explode('.', $name, 2); } // no main node yet if (!isset($decoded[$mainNode])) { if ($subName !== null) { $this->addMainKey($mainNode, array($name => array($subName => $value))); } else { $this->addMainKey($mainNode, array($name => $value)); } return true; } // main node content not match-able $nodeRegex = '{'.self::$DEFINES.'^(?P<start> \s* \{ \s* (?: (?&string) \s* : (?&json) \s* , \s* )*?'. preg_quote(JsonFile::encode($mainNode)).'\s*:\s*)(?P<content>(?&object))(?P<end>.*)}sx'; try { if (!$this->pregMatch($nodeRegex, $this->contents, $match)) { return false; } } catch (\RuntimeException $e) { if ($e->getCode() === PREG_BACKTRACK_LIMIT_ERROR) { return false; } throw $e; } // WP_CLI: begin caseInsensitive. if ($caseInsensitive) { // Just zap any existing names first in a case insensitive manner. $this->removeSubNode($mainNode, $name, $caseInsensitive); return $this->addSubNode($mainNode, $name, $value); } // WP_CLI: end caseInsensitive. $children = $match['content']; // invalid match due to un-regexable content, abort if (!@json_decode($children)) { return false; } $that = $this; // child exists $childRegex = '{'.self::$DEFINES.'(?P<start>"'.preg_quote($name).'"\s*:\s*)(?P<content>(?&json))(?P<end>,?)}x'; if ($this->pregMatch($childRegex, $children, $matches)) { $children = preg_replace_callback($childRegex, function ($matches) use ($name, $subName, $value, $that) { if ($subName !== null) { $curVal = json_decode($matches['content'], true); if (!is_array($curVal)) { $curVal = array(); } $curVal[$subName] = $value; $value = $curVal; } return $matches['start'] . $that->format($value, 1) . $matches['end']; }, $children); } else { $this->pregMatch('#^{ \s*? (?P<content>\S+.*?)? (?P<trailingspace>\s*) }$#sx', $children, $match); $whitespace = ''; if (!empty($match['trailingspace'])) { $whitespace = $match['trailingspace']; } if (!empty($match['content'])) { if ($subName !== null) { $value = array($subName => $value); } // child missing but non empty children $children = preg_replace( '#'.$whitespace.'}$#', addcslashes(',' . $this->newline . $this->indent . $this->indent . JsonFile::encode($name).': '.$this->format($value, 1) . $whitespace . '}', '\\$'), $children ); } else { if ($subName !== null) { $value = array($subName => $value); } // children present but empty $children = '{' . $this->newline . $this->indent . $this->indent . JsonFile::encode($name).': '.$this->format($value, 1) . $whitespace . '}'; } } $this->contents = preg_replace_callback($nodeRegex, function ($m) use ($children) { return $m['start'] . $children . $m['end']; }, $this->contents); return true; } public function removeSubNode($mainNode, $name, $caseInsensitive = false) // WP_CLI: caseInsensitive. { $decoded = JsonFile::parseJson($this->contents); // no node or empty node if (empty($decoded[$mainNode])) { return true; } // WP_CLI: begin caseInsensitive. if ( $caseInsensitive ) { // This is more or less a copy of the code at the start of `addLink()` above. $regex = '{'.self::$DEFINES.'^(?P<start>\s*\{\s*(?:(?&string)\s*:\s*(?&json)\s*,\s*)*?)'. '(?P<property>'.preg_quote(JsonFile::encode($mainNode)).'\s*:\s*)(?P<value>(?&json))(?P<end>.*)}sx'; if (!$this->pregMatch($regex, $this->contents, $matches)) { return true; } $value = $matches['value']; // Renamed from `$links` in `addLink()` case above. // try to find existing values $nameRegex = str_replace('/', '\\\\?/', preg_quote($name)); // Renamed from `$packageRegex` in `addLink()` case above. $regex = '{'.self::$DEFINES.'"(?P<name>'.$nameRegex.')"(\s*:\s*)(?&json)}ix'; // Need `(?&json)` PCRE subroutine here, as opposed to `(?&string)` in `addLink()` case. if (preg_match_all($regex, $value, $nameMatches)) { // Just zap them all individually in a case sensitive manner. foreach ( $nameMatches['name'] as $nameMatch ) { $this->removeSubNode($mainNode, $nameMatch); } } return true; } // WP_CLI: end caseInsensitive. // no node content match-able $nodeRegex = '{'.self::$DEFINES.'^(?P<start> \s* \{ \s* (?: (?&string) \s* : (?&json) \s* , \s* )*?'. preg_quote(JsonFile::encode($mainNode)).'\s*:\s*)(?P<content>(?&object))(?P<end>.*)}sx'; try { if (!$this->pregMatch($nodeRegex, $this->contents, $match)) { return false; } } catch (\RuntimeException $e) { if ($e->getCode() === PREG_BACKTRACK_LIMIT_ERROR) { return false; } throw $e; } $children = $match['content']; // invalid match due to un-regexable content, abort if (!@json_decode($children, true)) { return false; } $subName = null; if (in_array($mainNode, array('config', 'extra')) && false !== strpos($name, '.')) { list($name, $subName) = explode('.', $name, 2); } // no node to remove if (!isset($decoded[$mainNode][$name]) || ($subName && !isset($decoded[$mainNode][$name][$subName]))) { return true; } // try and find a match for the subkey if ($this->pregMatch('{"'.preg_quote($name).'"\s*:}i', $children)) { // find best match for the value of "name" if (preg_match_all('{'.self::$DEFINES.'"'.preg_quote($name).'"\s*:\s*(?:(?&json))}x', $children, $matches)) { $bestMatch = ''; foreach ($matches[0] as $match) { if (strlen($bestMatch) < strlen($match)) { $bestMatch = $match; } } $childrenClean = preg_replace('{,\s*'.preg_quote($bestMatch).'}', '', $children, -1, $count); // WP_CLI: As the preg_match_all() above is case-sensitive, so should this be. if (1 !== $count) { $childrenClean = preg_replace('{'.preg_quote($bestMatch).'\s*,?\s*}', '', $childrenClean, -1, $count); // WP_CLI: ditto. if (1 !== $count) { return false; } } } } else { $childrenClean = $children; } // no child data left, $name was the only key in $this->pregMatch('#^{ \s*? (?P<content>\S+.*?)? (?P<trailingspace>\s*) }$#sx', $childrenClean, $match); if (empty($match['content'])) { $newline = $this->newline; $indent = $this->indent; $this->contents = preg_replace_callback($nodeRegex, function ($matches) use ($indent, $newline) { return $matches['start'] . '{' . $newline . $indent . '}' . $matches['end']; }, $this->contents); // we have a subname, so we restore the rest of $name if ($subName !== null) { $curVal = json_decode($children, true); unset($curVal[$name][$subName]); $this->addSubNode($mainNode, $name, $curVal[$name]); } return true; } $that = $this; $this->contents = preg_replace_callback($nodeRegex, function ($matches) use ($that, $name, $subName, $childrenClean) { if ($subName !== null) { $curVal = json_decode($matches['content'], true); unset($curVal[$name][$subName]); $childrenClean = $that->format($curVal, 0); } return $matches['start'] . $childrenClean . $matches['end']; }, $this->contents); return true; } public function addMainKey($key, $content) { $decoded = JsonFile::parseJson($this->contents); $content = $this->format($content); // key exists already $regex = '{'.self::$DEFINES.'^(?P<start>\s*\{\s*(?:(?&string)\s*:\s*(?&json)\s*,\s*)*?)'. '(?P<key>'.preg_quote(JsonFile::encode($key)).'\s*:\s*(?&json))(?P<end>.*)}sx'; if (isset($decoded[$key]) && $this->pregMatch($regex, $this->contents, $matches)) { // invalid match due to un-regexable content, abort if (!@json_decode('{'.$matches['key'].'}')) { return false; } $this->contents = $matches['start'] . JsonFile::encode($key).': '.$content . $matches['end']; return true; } // append at the end of the file and keep whitespace if ($this->pregMatch('#[^{\s](\s*)\}$#', $this->contents, $match)) { $this->contents = preg_replace( '#'.$match[1].'\}$#', addcslashes(',' . $this->newline . $this->indent . JsonFile::encode($key). ': '. $content . $this->newline . '}', '\\$'), $this->contents ); return true; } // append at the end of the file $this->contents = preg_replace( '#\}$#', addcslashes($this->indent . JsonFile::encode($key). ': '.$content . $this->newline . '}', '\\$'), $this->contents ); return true; } public function removeMainKey($key) { $decoded = JsonFile::parseJson($this->contents); if (!isset($decoded[$key])) { return true; } // key exists already $regex = '{'.self::$DEFINES.'^(?P<start>\s*\{\s*(?:(?&string)\s*:\s*(?&json)\s*,\s*)*?)'. '(?P<removal>'.preg_quote(JsonFile::encode($key)).'\s*:\s*(?&json))\s*,?\s*(?P<end>.*)}sx'; if ($this->pregMatch($regex, $this->contents, $matches)) { // invalid match due to un-regexable content, abort if (!@json_decode('{'.$matches['removal'].'}')) { return false; } // check that we are not leaving a dangling comma on the previous line if the last line was removed if (preg_match('#,\s*$#', $matches['start']) && preg_match('#^\}$#', $matches['end'])) { $matches['start'] = rtrim(preg_replace('#,(\s*)$#', '$1', $matches['start']), $this->indent); } $this->contents = $matches['start'] . $matches['end']; if (preg_match('#^\{\s*\}\s*$#', $this->contents)) { $this->contents = "{\n}"; } return true; } return false; } public function format($data, $depth = 0) { if (is_array($data)) { reset($data); if (is_numeric(key($data))) { foreach ($data as $key => $val) { $data[$key] = $this->format($val, $depth + 1); } return '['.implode(', ', $data).']'; } $out = '{' . $this->newline; $elems = array(); foreach ($data as $key => $val) { $elems[] = str_repeat($this->indent, $depth + 2) . JsonFile::encode($key). ': '.$this->format($val, $depth + 1); } return $out . implode(','.$this->newline, $elems) . $this->newline . str_repeat($this->indent, $depth + 1) . '}'; } return JsonFile::encode($data); } protected function detectIndenting() { if ($this->pregMatch('{^([ \t]+)"}m', $this->contents, $match)) { $this->indent = $match[1]; } else { $this->indent = ' '; } } protected function pregMatch($re, $str, &$matches = array()) { $count = preg_match($re, $str, $matches); if ($count === false) { switch (preg_last_error()) { case PREG_NO_ERROR: throw new \RuntimeException('Failed to execute regex: PREG_NO_ERROR', PREG_NO_ERROR); case PREG_INTERNAL_ERROR: throw new \RuntimeException('Failed to execute regex: PREG_INTERNAL_ERROR', PREG_INTERNAL_ERROR); case PREG_BACKTRACK_LIMIT_ERROR: throw new \RuntimeException('Failed to execute regex: PREG_BACKTRACK_LIMIT_ERROR', PREG_BACKTRACK_LIMIT_ERROR); case PREG_RECURSION_LIMIT_ERROR: throw new \RuntimeException('Failed to execute regex: PREG_RECURSION_LIMIT_ERROR', PREG_RECURSION_LIMIT_ERROR); case PREG_BAD_UTF8_ERROR: throw new \RuntimeException('Failed to execute regex: PREG_BAD_UTF8_ERROR', PREG_BAD_UTF8_ERROR); case PREG_BAD_UTF8_OFFSET_ERROR: throw new \RuntimeException('Failed to execute regex: PREG_BAD_UTF8_OFFSET_ERROR', PREG_BAD_UTF8_OFFSET_ERROR); case 6: // PREG_JIT_STACKLIMIT_ERROR if (PHP_VERSION_ID > 70000) { throw new \RuntimeException('Failed to execute regex: PREG_JIT_STACKLIMIT_ERROR', 6); } // fallthrough default: throw new \RuntimeException('Failed to execute regex: Unknown error'); } } return $count; } } <?php use Composer\Composer; use Composer\Config; use Composer\Config\JsonConfigSource; use Composer\DependencyResolver\Pool; use Composer\DependencyResolver\Request; use Composer\EventDispatcher\Event; use Composer\Factory; use Composer\IO\NullIO; use Composer\Installer; use Composer\Json\JsonFile; use Composer\Package; use Composer\Package\BasePackage; use Composer\Package\PackageInterface; use Composer\Package\Version\VersionParser; use Composer\Package\Version\VersionSelector; use Composer\Repository; use Composer\Repository\CompositeRepository; use Composer\Repository\ComposerRepository; use Composer\Repository\RepositoryManager; use Composer\Util\Filesystem; use Composer\Util\HttpDownloader; use WP_CLI\ComposerIO; use WP_CLI\Extractor; use WP_CLI\Utils; use WP_CLI\JsonManipulator; use WP_CLI\PackageManagerEventSubscriber; /** * Lists, installs, and removes WP-CLI packages. * * WP-CLI packages are community-maintained projects built on WP-CLI. They can * contain WP-CLI commands, but they can also just extend WP-CLI in some way. * * Learn how to create your own command from the * [Commands Cookbook](https://make.wordpress.org/cli/handbook/commands-cookbook/) * * ## EXAMPLES * * # List installed packages * $ wp package list * +-----------------------+------------------------------------------+---------+----------+ * | name | description | authors | version | * +-----------------------+------------------------------------------+---------+----------+ * | wp-cli/server-command | Start a development server for WordPress | | dev-main | * +-----------------------+------------------------------------------+---------+----------+ * * # Install the latest development version of the package * $ wp package install wp-cli/server-command * Installing package wp-cli/server-command (dev-main) * Updating /home/person/.wp-cli/packages/composer.json to require the package... * Using Composer to install the package... * --- * Loading composer repositories with package information * Updating dependencies * Resolving dependencies through SAT * Dependency resolution completed in 0.005 seconds * Analyzed 732 packages to resolve dependencies * Analyzed 1034 rules to resolve dependencies * - Installing package * Writing lock file * Generating autoload files * --- * Success: Package installed. * * # Uninstall package * $ wp package uninstall wp-cli/server-command * Removing require statement from /home/person/.wp-cli/packages/composer.json * Deleting package directory /home/person/.wp-cli/packages/vendor/wp-cli/server-command * Regenerating Composer autoload. * Success: Uninstalled package. * * @package WP-CLI * * @when before_wp_load */ class Package_Command extends WP_CLI_Command { const PACKAGE_INDEX_URL = 'https://wp-cli.org/package-index/'; const SSL_CERTIFICATE = '/rmccue/requests/library/Requests/Transport/cacert.pem'; const DEFAULT_DEV_BRANCH_CONSTRAINTS = 'dev-main || dev-master || dev-trunk'; private $version_selector = false; /** * Default author data used while creating default WP-CLI packages composer.json. * * @var array */ private $author_data = [ 'name' => 'WP-CLI', 'email' => 'noreply@wpcli.org', ]; /** * Default repository data used while creating default WP-CLI packages composer.json. * @var array */ private $composer_type_package = [ 'type' => 'composer', 'url' => self::PACKAGE_INDEX_URL, ]; /** * Browses WP-CLI packages available for installation. * * Lists packages available for installation from the [Package Index](http://wp-cli.org/package-index/). * Although the package index will remain in place for backward compatibility reasons, it has been * deprecated and will not be updated further. Please refer to https://github.com/wp-cli/ideas/issues/51 * to read about its potential replacement. * * ## OPTIONS * * [--fields=<fields>] * : Limit the output to specific fields. Defaults to all fields. * * [--format=<format>] * : Render output in a particular format. * --- * default: table * options: * - table * - csv * - ids * - json * - yaml * --- * * ## AVAILABLE FIELDS * * These fields will be displayed by default for each package: * * * name * * description * * authors * * version * * There are no optionally available fields. * * ## EXAMPLES * * $ wp package browse --format=yaml * --- * 10up/mu-migration: * name: 10up/mu-migration * description: A set of WP-CLI commands to support the migration of single WordPress instances to multisite * authors: Nícholas André * version: dev-main, dev-develop * aaemnnosttv/wp-cli-dotenv-command: * name: aaemnnosttv/wp-cli-dotenv-command * description: Dotenv commands for WP-CLI * authors: Evan Mattson * version: v0.1, v0.1-beta.1, v0.2, dev-main, dev-dev, dev-develop, dev-tests/behat * aaemnnosttv/wp-cli-http-command: * name: aaemnnosttv/wp-cli-http-command * description: WP-CLI command for using the WordPress HTTP API * authors: Evan Mattson * version: dev-main */ public function browse( $_, $assoc_args ) { $this->set_composer_auth_env_var(); if ( empty( $assoc_args['format'] ) || 'table' === $assoc_args['format'] ) { WP_CLI::line( WP_CLI::colorize( '%CAlthough the package index will remain in place for backward compatibility reasons, it has been deprecated and will not be updated further. Please refer to https://github.com/wp-cli/ideas/issues/51 to read about its potential replacement.%n' ) ); } $this->show_packages( 'browse', $this->get_community_packages(), $assoc_args ); } /** * Installs a WP-CLI package. * * Packages are required to be a valid Composer package, and can be * specified as: * * * Package name from WP-CLI's package index. * * Git URL accessible by the current shell user. * * Path to a directory on the local machine. * * Local or remote .zip file. * * Packages are installed to `~/.wp-cli/packages/` by default. Use the * `WP_CLI_PACKAGES_DIR` environment variable to provide a custom path. * * When installing a local directory, WP-CLI simply registers a * reference to the directory. If you move or delete the directory, WP-CLI's * reference breaks. * * When installing a .zip file, WP-CLI extracts the package to * `~/.wp-cli/packages/local/<package-name>`. * * ## OPTIONS * * <name|git|path|zip> * : Name, git URL, directory path, or .zip file for the package to install. * Names can optionally include a version constraint * (e.g. wp-cli/server-command:@stable). * * [--insecure] * : Retry downloads without certificate validation if TLS handshake fails. Note: This makes the request vulnerable to a MITM attack. * * ## EXAMPLES * * # Install a package hosted at a git URL. * $ wp package install runcommand/hook * * # Install the latest stable version. * $ wp package install wp-cli/server-command:@stable * * # Install a package hosted at a GitLab.com URL. * $ wp package install https://gitlab.com/foo/wp-cli-bar-command.git * * # Install a package in a .zip file. * $ wp package install google-sitemap-generator-cli.zip */ public function install( $args, $assoc_args ) { list( $package_name ) = $args; $insecure = (bool) Utils\get_flag_value( $assoc_args, 'insecure', false ); $this->set_composer_auth_env_var(); $git_package = false; $dir_package = false; $version = ''; if ( $this->is_git_repository( $package_name ) ) { if ( '' === $version ) { $version = "dev-{$this->get_github_default_branch( $package_name, $insecure )}"; } $git_package = $package_name; $matches = []; if ( preg_match( '#([^:\/]+\/[^\/]+)\.git#', $package_name, $matches ) ) { $package_name = $this->check_git_package_name( $matches[1], $package_name, $version, $insecure ); } else { WP_CLI::error( "Couldn't parse package name from expected path '<name>/<package>'." ); } } elseif ( ( false !== strpos( $package_name, '://' ) && false !== stripos( $package_name, '.zip' ) ) || ( pathinfo( $package_name, PATHINFO_EXTENSION ) === 'zip' && is_file( $package_name ) ) ) { // Download the remote ZIP file to a temp directory $temp = false; if ( false !== strpos( $package_name, '://' ) ) { $temp = Utils\get_temp_dir() . uniqid( 'wp-cli-package_', true /*more_entropy*/ ) . '.zip'; $options = [ 'timeout' => 600, 'filename' => $temp, 'insecure' => $insecure, ]; $response = Utils\http_request( 'GET', $package_name, null, [], $options ); if ( 20 !== (int) substr( $response->status_code, 0, 2 ) ) { @unlink( $temp ); // @codingStandardsIgnoreLine WP_CLI::error( sprintf( "Couldn't download package from '%s' (HTTP code %d).", $package_name, $response->status_code ) ); } $package_name = $temp; } $dir_package = Utils\get_temp_dir() . uniqid( 'wp-cli-package_', true /*more_entropy*/ ); try { // Extract the package to get the package name Extractor::extract( $package_name, $dir_package ); if ( $temp ) { unlink( $temp ); $temp = false; } list( $package_name, $version ) = self::get_package_name_and_version_from_dir_package( $dir_package ); // Move to a location based on the package name $local_dir = rtrim( WP_CLI::get_runner()->get_packages_dir_path(), '/' ) . '/local/'; $actual_dir_package = $local_dir . str_replace( '/', '-', $package_name ); Extractor::copy_overwrite_files( $dir_package, $actual_dir_package ); Extractor::rmdir( $dir_package ); // Behold, the extracted package $dir_package = $actual_dir_package; } catch ( Exception $e ) { if ( $temp ) { unlink( $temp ); } if ( file_exists( $dir_package ) ) { try { Extractor::rmdir( $dir_package ); } catch ( Exception $rmdir_e ) { WP_CLI::warning( $rmdir_e->getMessage() ); } } WP_CLI::error( $e->getMessage() ); } } elseif ( is_dir( $package_name ) && file_exists( $package_name . '/composer.json' ) ) { $dir_package = $package_name; if ( ! Utils\is_path_absolute( $dir_package ) ) { $dir_package = getcwd() . DIRECTORY_SEPARATOR . $dir_package; } list( $package_name, $version ) = self::get_package_name_and_version_from_dir_package( $dir_package ); } else { if ( false !== strpos( $package_name, ':' ) ) { list( $package_name, $version ) = explode( ':', $package_name ); } $package = $this->get_package_by_shortened_identifier( $package_name ); if ( ! $package ) { WP_CLI::error( sprintf( "Invalid package: shortened identifier '%s' not found.", $package_name ) ); } if ( is_string( $package ) ) { if ( $this->is_git_repository( $package ) ) { $git_package = $package; if ( '' === $version ) { $version = "dev-{$this->get_github_default_branch( $package_name, $insecure )}"; } if ( '@stable' === $version ) { $tag = $this->get_github_latest_release_tag( $package_name, $insecure ); $version = $this->guess_version_constraint_from_tag( $tag ); } $package_name = $this->check_github_package_name( $package_name, $version, $insecure ); } } elseif ( $package_name !== $package->getPrettyName() ) { // BC support for specifying lowercase names for mixed-case package index packages - don't bother warning. $package_name = $package->getPrettyName(); } } if ( $this->is_composer_v2() ) { $package_name = function_exists( 'mb_strtolower' ) ? mb_strtolower( $package_name ) : strtolower( $package_name ); } if ( '' === $version ) { $version = self::DEFAULT_DEV_BRANCH_CONSTRAINTS; } WP_CLI::log( sprintf( 'Installing package %s (%s)', $package_name, $version ) ); // Read the WP-CLI packages composer.json and do some initial error checking. list( $json_path, $composer_backup, $composer_backup_decoded ) = $this->get_composer_json_path_backup_decoded(); // Revert on shutdown if `$revert` is true (set to false on success). $revert = true; $this->register_revert_shutdown_function( $json_path, $composer_backup, $revert ); // Add the 'require' to composer.json WP_CLI::log( sprintf( 'Updating %s to require the package...', $json_path ) ); $json_manipulator = new JsonManipulator( $composer_backup ); $json_manipulator->addMainKey( 'name', 'wp-cli/wp-cli' ); $json_manipulator->addMainKey( 'version', self::get_wp_cli_version_composer() ); $json_manipulator->addLink( 'require', $package_name, $version, false /*sortPackages*/, true /*caseInsensitive*/ ); $json_manipulator->addConfigSetting( 'secure-http', true ); $package_args = []; if ( $git_package ) { WP_CLI::log( sprintf( 'Registering %s as a VCS repository...', $git_package ) ); $package_args = [ 'type' => 'vcs', 'url' => $git_package, ]; $json_manipulator->addSubNode( 'repositories', $package_name, $package_args, true /*caseInsensitive*/ ); } elseif ( $dir_package ) { WP_CLI::log( sprintf( 'Registering %s as a path repository...', $dir_package ) ); $package_args = [ 'type' => 'path', 'url' => $dir_package, ]; $json_manipulator->addSubNode( 'repositories', $package_name, $package_args, true /*caseInsensitive*/ ); } // If the composer file does not contain the current package index repository, refresh the repository definition. if ( empty( $composer_backup_decoded['repositories']['wp-cli']['url'] ) || self::PACKAGE_INDEX_URL !== $composer_backup_decoded['repositories']['wp-cli']['url'] ) { WP_CLI::log( 'Updating package index repository url...' ); $package_args = $this->composer_type_package; $json_manipulator->addRepository( 'wp-cli', $package_args ); } file_put_contents( $json_path, $json_manipulator->getContents() ); $composer = $this->get_composer(); // Set up the EventSubscriber $event_subscriber = new PackageManagerEventSubscriber(); $composer->getEventDispatcher()->addSubscriber( $event_subscriber ); // Set up the installer $install = Installer::create( new ComposerIO(), $composer ); $install->setUpdate( true ); // Installer class will only override composer.lock with this flag $install->setPreferSource( true ); // Use VCS when VCS for easier contributions. // Try running the installer, but revert composer.json if failed WP_CLI::log( 'Using Composer to install the package...' ); WP_CLI::log( '---' ); $res = false; try { $res = $install->run(); } catch ( Exception $e ) { WP_CLI::warning( $e->getMessage() ); } // TODO: The --insecure flag should cause another Composer run with verify disabled. WP_CLI::log( '---' ); if ( 0 === $res ) { $revert = false; WP_CLI::success( 'Package installed.' ); } else { $res_msg = $res ? " (Composer return code {$res})" : ''; // $res may be null apparently. WP_CLI::debug( "composer.json content:\n" . file_get_contents( $json_path ), 'packages' ); WP_CLI::error( "Package installation failed{$res_msg}." ); } } /** * Lists installed WP-CLI packages. * * ## OPTIONS * * [--fields=<fields>] * : Limit the output to specific fields. Defaults to all fields. * * [--format=<format>] * : Render output in a particular format. * --- * default: table * options: * - table * - csv * - ids * - json * - yaml * --- * * ## AVAILABLE FIELDS * * These fields will be displayed by default for each package: * * * name * * authors * * version * * update * * update_version * * These fields are optionally available: * * * description * * ## EXAMPLES * * $ wp package list * +-----------------------+------------------------------------------+---------+----------+ * | name | description | authors | version | * +-----------------------+------------------------------------------+---------+----------+ * | wp-cli/server-command | Start a development server for WordPress | | dev-main | * +-----------------------+------------------------------------------+---------+----------+ * * @subcommand list */ public function list_( $args, $assoc_args ) { $this->set_composer_auth_env_var(); $this->show_packages( 'list', $this->get_installed_packages(), $assoc_args ); } /** * Gets the path to an installed WP-CLI package, or the package directory. * * If you want to contribute to a package, this is a great way to jump to it. * * ## OPTIONS * * [<name>] * : Name of the package to get the directory for. * * ## EXAMPLES * * # Get package path * $ wp package path * /home/person/.wp-cli/packages/ * * # Change directory to package path * $ cd $(wp package path) && pwd * /home/vagrant/.wp-cli/packages */ public function path( $args ) { $packages_dir = WP_CLI::get_runner()->get_packages_dir_path(); if ( ! empty( $args ) ) { $packages_dir .= 'vendor/' . $args[0]; if ( ! is_dir( $packages_dir ) ) { WP_CLI::error( 'Invalid package name.' ); } } WP_CLI::line( $packages_dir ); } /** * Updates all installed WP-CLI packages to their latest version. * * ## EXAMPLES * * $ wp package update * Using Composer to update packages... * --- * Loading composer repositories with package information * Updating dependencies * Resolving dependencies through SAT * Dependency resolution completed in 0.074 seconds * Analyzed 1062 packages to resolve dependencies * Analyzed 22383 rules to resolve dependencies * Writing lock file * Generating autoload files * --- * Success: Packages updated. */ public function update() { $this->set_composer_auth_env_var(); $composer = $this->get_composer(); // Set up the EventSubscriber $event_subscriber = new PackageManagerEventSubscriber(); $composer->getEventDispatcher()->addSubscriber( $event_subscriber ); // Set up the installer $install = Installer::create( new ComposerIO(), $composer ); $install->setUpdate( true ); // Installer class will only override composer.lock with this flag $install->setPreferSource( true ); // Use VCS when VCS for easier contributions. WP_CLI::log( 'Using Composer to update packages...' ); WP_CLI::log( '---' ); $res = false; try { $res = $install->run(); } catch ( Exception $e ) { WP_CLI::warning( $e->getMessage() ); } WP_CLI::log( '---' ); // TODO: The --insecure (to be added here) flag should cause another Composer run with verify disabled. if ( 0 === $res ) { WP_CLI::success( 'Packages updated.' ); } else { $res_msg = $res ? " (Composer return code {$res})" : ''; // $res may be null apparently. WP_CLI::error( "Failed to update packages{$res_msg}." ); } } /** * Uninstalls a WP-CLI package. * * ## OPTIONS * * <name> * : Name of the package to uninstall. * * [--insecure] * : Retry downloads without certificate validation if TLS handshake fails. Note: This makes the request vulnerable to a MITM attack. * * ## EXAMPLES * * $ wp package uninstall wp-cli/server-command * Removing require statement from /home/person/.wp-cli/packages/composer.json * Deleting package directory /home/person/.wp-cli/packages/vendor/wp-cli/server-command * Regenerating Composer autoload. * Success: Uninstalled package. */ public function uninstall( $args, $assoc_args ) { list( $package_name ) = $args; $insecure = (bool) Utils\get_flag_value( $assoc_args, 'insecure', false ); $this->set_composer_auth_env_var(); $package = $this->get_installed_package_by_name( $package_name ); if ( false === $package ) { $package_name = $this->get_package_by_shortened_identifier( $package_name ); if ( false === $package_name ) { WP_CLI::error( 'Package not installed.' ); } $version = "dev-{$this->get_github_default_branch( $package_name, $insecure )}"; $matches = []; if ( preg_match( '#^(?:https?://github\.com/|git@github\.com:)(?<repo_name>.*?).git$#', $package_name, $matches ) ) { $package_name = $this->check_git_package_name( $matches['repo_name'], $package_name, $version, $insecure ); } } else { $package_name = $package->getPrettyName(); // Make sure package name is what's in composer.json. } // Read the WP-CLI packages composer.json and do some initial error checking. list( $json_path, $composer_backup, $composer_backup_decoded ) = $this->get_composer_json_path_backup_decoded(); // Revert on shutdown if `$revert` is true (set to false on success). $revert = true; $this->register_revert_shutdown_function( $json_path, $composer_backup, $revert ); // Remove the 'require' from composer.json. WP_CLI::log( sprintf( 'Removing require statement for package \'%s\' from %s', $package_name, $json_path ) ); $manipulator = new JsonManipulator( $composer_backup ); $manipulator->removeSubNode( 'require', $package_name, true /*caseInsensitive*/ ); // Remove the 'repository' details from composer.json. WP_CLI::log( sprintf( 'Removing repository details from %s', $json_path ) ); $manipulator->removeSubNode( 'repositories', $package_name, true /*caseInsensitive*/ ); file_put_contents( $json_path, $manipulator->getContents() ); $composer = $this->get_composer(); // Set up the installer. $install = Installer::create( new NullIO(), $composer ); $install->setUpdate( true ); // Installer class will only override composer.lock with this flag $install->setPreferSource( true ); // Use VCS when VCS for easier contributions. WP_CLI::log( 'Removing package directories and regenerating autoloader...' ); $res = false; try { $res = $install->run(); } catch ( Exception $e ) { WP_CLI::warning( $e->getMessage() ); } if ( 0 === $res ) { $revert = false; WP_CLI::success( 'Uninstalled package.' ); } else { $res_msg = $res ? " (Composer return code {$res})" : ''; // $res may be null apparently. WP_CLI::error( "Package removal failed{$res_msg}." ); } } /** * Checks whether a package is a WP-CLI community package based * on membership in our package index. * * @param object $package A package object * @return bool */ private function is_community_package( $package ) { return $this->package_index()->hasPackage( $package ); } /** * Gets a Composer instance. */ private function get_composer() { $this->avoid_composer_ca_bundle(); try { $composer_path = $this->get_composer_json_path(); // Composer's auto-load generating code makes some assumptions about where // the 'vendor-dir' is, and where Composer is running from. // Best to just pretend we're installing a package from ~/.wp-cli or similar chdir( pathinfo( $composer_path, PATHINFO_DIRNAME ) ); // Prevent DateTime error/warning when no timezone set. // Note: The package is loaded before WordPress load, For environments that don't have set time in php.ini. // phpcs:ignore WordPress.DateTime.RestrictedFunctions.timezone_change_date_default_timezone_set,WordPress.PHP.NoSilencedErrors.Discouraged date_default_timezone_set( @date_default_timezone_get() ); $composer = Factory::create( new NullIO(), $composer_path ); } catch ( Exception $e ) { WP_CLI::error( sprintf( 'Failed to get composer instance: %s', $e->getMessage() ) ); } return $composer; } /** * Gets all of the community packages. * * @return array */ private function get_community_packages() { static $community_packages; if ( null === $community_packages ) { $this->avoid_composer_ca_bundle(); try { $community_packages = $this->package_index()->getPackages(); } catch ( Exception $e ) { WP_CLI::error( $e->getMessage() ); } } return $community_packages; } /** * Gets the package index instance * * We need to construct the instance manually, because there's no way to select * a particular instance using $composer->getRepositoryManager() * * @return ComposerRepository */ private function package_index() { static $package_index; if ( ! $package_index ) { $config_args = [ 'config' => [ 'secure-http' => true, 'home' => dirname( $this->get_composer_json_path() ), ], ]; $config = new Config(); $config->merge( $config_args ); $config->setConfigSource( new JsonConfigSource( $this->get_composer_json() ) ); $io = new NullIO(); try { if ( $this->is_composer_v2() ) { $http_downloader = new HttpDownloader( $io, $config ); $package_index = new ComposerRepository( [ 'url' => self::PACKAGE_INDEX_URL ], $io, $config, $http_downloader ); } else { $package_index = new ComposerRepository( [ 'url' => self::PACKAGE_INDEX_URL ], $io, $config ); } } catch ( Exception $e ) { WP_CLI::error( $e->getMessage() ); } } return $package_index; } /** * Displays a set of packages * * @param string $context * @param array * @param array */ private function show_packages( $context, $packages, $assoc_args ) { if ( 'list' === $context ) { $default_fields = [ 'name', 'authors', 'version', 'update', 'update_version', ]; } elseif ( 'browse' === $context ) { $default_fields = [ 'name', 'description', 'authors', 'version', ]; } $defaults = [ 'fields' => implode( ',', $default_fields ), 'format' => 'table', ]; $assoc_args = array_merge( $defaults, $assoc_args ); $composer = $this->get_composer(); $list = []; foreach ( $packages as $package ) { $name = $package->getPrettyName(); if ( isset( $list[ $name ] ) ) { $list[ $name ]['version'][] = $package->getPrettyVersion(); } else { $package_output = []; $package_output['name'] = $package->getPrettyName(); $package_output['description'] = $package->getDescription(); $package_output['authors'] = implode( ', ', array_column( (array) $package->getAuthors(), 'name' ) ); $package_output['version'] = [ $package->getPrettyVersion() ]; $update = 'none'; $update_version = ''; if ( 'list' === $context ) { try { $latest = $this->find_latest_package( $package, $composer, null ); if ( $latest && $latest->getFullPrettyVersion() !== $package->getFullPrettyVersion() ) { $update = 'available'; $update_version = $latest->getPrettyVersion(); } } catch ( Exception $e ) { WP_CLI::warning( $e->getMessage() ); $update = 'error'; $update_version = $update; } } $package_output['update'] = $update; $package_output['update_version'] = $update_version; $package_output['pretty_name'] = $package->getPrettyName(); // Deprecated but kept for BC with package-command 1.0.8. $list[ $package_output['name'] ] = $package_output; } } $list = array_map( function( $package ) { $package['version'] = implode( ', ', $package['version'] ); return $package; }, $list ); ksort( $list ); if ( 'ids' === $assoc_args['format'] ) { $list = array_keys( $list ); } Utils\format_items( $assoc_args['format'], $list, $assoc_args['fields'] ); } /** * Gets a package by its shortened identifier. * * A shortened identifier has the form `<vendor>/<package>`. * * This method first checks the deprecated package index, for BC reasons, * and then falls back to the corresponding GitHub URL. * * @param string $package_name Name of the package to get. * @param bool $insecure Optional. Whether to insecurely retry downloads that failed TLS handshake. Defaults * to false. */ private function get_package_by_shortened_identifier( $package_name, $insecure = false ) { // Check the package index first, so we don't break existing behavior. $lc_package_name = strtolower( $package_name ); // For BC check. foreach ( $this->get_community_packages() as $package ) { if ( $package_name === $package->getPrettyName() ) { return $package; } // For BC allow getting by lowercase name. if ( $lc_package_name === $package->getName() ) { return $package; } } $options = [ 'insecure' => $insecure ]; // Check if the package exists on Packagist. $url = "https://repo.packagist.org/p/{$package_name}.json"; $response = Utils\http_request( 'GET', $url, null, [], $options ); if ( 20 === (int) substr( $response->status_code, 0, 2 ) ) { return $package_name; } // Fall back to GitHub URL if we had no match yet. $url = "https://github.com/{$package_name}.git"; $github_token = getenv( 'GITHUB_TOKEN' ); // Use GITHUB_TOKEN if available to avoid authorization failures or rate-limiting. $headers = $github_token ? [ 'Authorization' => 'token ' . $github_token ] : []; $response = Utils\http_request( 'GET', $url, null /*data*/, $headers, $options ); if ( 20 === (int) substr( $response->status_code, 0, 2 ) ) { return $url; } return false; } /** * Gets the installed community packages. */ private function get_installed_packages() { $composer = $this->get_composer(); $repo = $composer->getRepositoryManager()->getLocalRepository(); $existing = json_decode( file_get_contents( $this->get_composer_json_path() ), true ); $installed_package_keys = ! empty( $existing['require'] ) ? array_keys( $existing['require'] ) : []; if ( empty( $installed_package_keys ) ) { return []; } // For use by legacy incorrect name check. $lc_installed_package_keys = array_map( 'strtolower', $installed_package_keys ); $installed_packages = []; foreach ( $repo->getCanonicalPackages() as $package ) { $idx = array_search( $package->getName(), $lc_installed_package_keys, true ); // Use pretty name as it's case sensitive and what's in composer.json (or at least should be). if ( in_array( $package->getPrettyName(), $installed_package_keys, true ) ) { $installed_packages[] = $package; } elseif ( false !== $idx ) { // Legacy incorrect name check. if ( ! $this->is_composer_v2() ) { WP_CLI::warning( sprintf( "Found package '%s' misnamed '%s' in '%s'.", $package->getPrettyName(), $installed_package_keys[ $idx ], $this->get_composer_json_path() ) ); } $installed_packages[] = $package; } } return $installed_packages; } /** * Gets an installed package by its name. */ private function get_installed_package_by_name( $package_name ) { foreach ( $this->get_installed_packages() as $package ) { if ( $package_name === $package->getPrettyName() ) { return $package; } // Also check non-pretty (lowercase) name in case of legacy incorrect name. if ( $package_name === $package->getName() ) { return $package; } } return false; } /** * Checks if the package name provided is already installed. */ private function is_package_installed( $package_name ) { if ( $this->get_installed_package_by_name( $package_name ) ) { return true; } else { return false; } } /** * Gets the name of the package from the composer.json in a directory path * * @param string $dir_package * @return array Two-element array containing package name and version. */ private static function get_package_name_and_version_from_dir_package( $dir_package ) { $composer_file = $dir_package . '/composer.json'; if ( ! file_exists( $composer_file ) ) { WP_CLI::error( sprintf( "Invalid package: composer.json file '%s' not found.", $composer_file ) ); } $composer_data = json_decode( file_get_contents( $composer_file ), true ); if ( null === $composer_data ) { WP_CLI::error( sprintf( "Invalid package: failed to parse composer.json file '%s' as json.", $composer_file ) ); } if ( empty( $composer_data['name'] ) ) { WP_CLI::error( sprintf( "Invalid package: no name in composer.json file '%s'.", $composer_file ) ); } $package_name = $composer_data['name']; $version = self::DEFAULT_DEV_BRANCH_CONSTRAINTS; if ( ! empty( $composer_data['version'] ) ) { $version = $composer_data['version']; } return [ $package_name, $version ]; } /** * Gets the WP-CLI packages composer.json object. */ private function get_composer_json() { return new JsonFile( $this->get_composer_json_path() ); } /** * Gets the absolute path to the WP-CLI packages composer.json. */ private function get_composer_json_path() { static $composer_path; if ( null === $composer_path || getenv( 'WP_CLI_TEST_PACKAGE_GET_COMPOSER_JSON_PATH' ) ) { if ( getenv( 'WP_CLI_PACKAGES_DIR' ) ) { $composer_path = Utils\trailingslashit( getenv( 'WP_CLI_PACKAGES_DIR' ) ) . 'composer.json'; } else { $composer_path = Utils\trailingslashit( Utils\get_home_dir() ) . '.wp-cli/packages/composer.json'; } // `composer.json` and its directory might need to be created if ( ! file_exists( $composer_path ) ) { $composer_path = $this->create_default_composer_json( $composer_path ); } else { $composer_path = realpath( $composer_path ); if ( false === $composer_path ) { $error = error_get_last(); WP_CLI::error( sprintf( "Composer path '%s' for packages/composer.json not found: %s", $composer_path, $error['message'] ) ); } } } return $composer_path; } /** * Gets the WP-CLI version for composer.json */ private static function get_wp_cli_version_composer() { preg_match( '#^[0-9\.]+(-(alpha|beta)[^-]{0,})?#', WP_CLI_VERSION, $matches ); $version = isset( $matches[0] ) ? $matches[0] : ''; return $version; } /** * Creates a default WP-CLI packages composer.json. * * @param string $composer_path Where the composer.json should be created * @return string Returns the absolute path of the newly created default WP-CLI packages composer.json. */ private function create_default_composer_json( $composer_path ) { $composer_dir = pathinfo( $composer_path, PATHINFO_DIRNAME ); if ( ! is_dir( $composer_dir ) ) { if ( ! @mkdir( $composer_dir, 0777, true ) ) { // @codingStandardsIgnoreLine $error = error_get_last(); WP_CLI::error( sprintf( "Composer directory '%s' for packages couldn't be created: %s", $composer_dir, $error['message'] ) ); } } $composer_dir = realpath( $composer_dir ); if ( false === $composer_dir ) { $error = error_get_last(); WP_CLI::error( sprintf( "Composer directory '%s' for packages not found: %s", $composer_dir, $error['message'] ) ); } $composer_path = Utils\trailingslashit( $composer_dir ) . Utils\basename( $composer_path ); $json_file = new JsonFile( $composer_path ); $repositories = (object) [ 'wp-cli' => (object) $this->composer_type_package, ]; $options = [ 'name' => 'wp-cli/wp-cli', 'description' => 'Installed community packages used by WP-CLI', 'version' => self::get_wp_cli_version_composer(), 'authors' => [ (object) $this->author_data ], 'homepage' => self::PACKAGE_INDEX_URL, 'require' => new stdClass(), 'require-dev' => new stdClass(), 'minimum-stability' => 'dev', 'prefer-stable' => true, 'license' => 'MIT', 'repositories' => $repositories, ]; try { $json_file->write( $options ); } catch ( Exception $e ) { WP_CLI::error( $e->getMessage() ); } return $composer_path; } /** * Given a package, this finds the latest package matching it * * @param PackageInterface $package * @param Composer $composer * @param string $phpVersion * @param bool $minorOnly * * @return PackageInterface|null */ private function find_latest_package( PackageInterface $package, Composer $composer, $php_version, $minor_only = false ) { // Find the latest version allowed in this pool/repository set. $name = $package->getPrettyName(); $version_selector = $this->get_version_selector( $composer ); $stability = $composer->getPackage()->getMinimumStability(); $flags = $composer->getPackage()->getStabilityFlags(); if ( isset( $flags[ $name ] ) ) { $stability = array_search( $flags[ $name ], BasePackage::$stabilities, true ); } $best_stability = $stability; if ( $composer->getPackage()->getPreferStable() ) { $best_stability = $package->getStability(); } $target_version = null; if ( 0 === strpos( $package->getVersion(), 'dev-' ) ) { $target_version = $package->getVersion(); } if ( null === $target_version && $minor_only ) { $target_version = '^' . $package->getVersion(); } if ( $this->is_composer_v2() ) { return $version_selector->findBestCandidate( $name, $target_version, $best_stability ); } return $version_selector->findBestCandidate( $name, $target_version, $php_version, $best_stability ); } private function get_version_selector( Composer $composer ) { if ( ! $this->version_selector ) { if ( $this->is_composer_v2() ) { $repository_set = new Repository\RepositorySet( $composer->getPackage()->getMinimumStability(), $composer->getPackage()->getStabilityFlags() ); $repository_set->addRepository( new CompositeRepository( $composer->getRepositoryManager()->getRepositories() ) ); $this->version_selector = new VersionSelector( $repository_set ); } else { $pool = new Pool( $composer->getPackage()->getMinimumStability(), $composer->getPackage()->getStabilityFlags() ); $pool->addRepository( new CompositeRepository( $composer->getRepositoryManager()->getRepositories() ) ); $this->version_selector = new VersionSelector( $pool ); } } return $this->version_selector; } /** * Checks whether a given package is a git repository. * * @param string $package Package name to check. * * @return bool Whether the package is a git repository. */ private function is_git_repository( $package ) { return '.git' === strtolower( substr( $package, -4, 4 ) ); } /** * Checks that `$package_name` matches the name in composer.json at Github.com, and return corrected value if not. * * @param string $package_name Package name to check. * @param string $version Optional. Package version. Defaults to empty string. * @param bool $insecure Optional. Whether to insecurely retry downloads that failed TLS handshake. Defaults * to false. */ private function check_github_package_name( $package_name, $version = '', $insecure = false ) { $github_token = getenv( 'GITHUB_TOKEN' ); // Use GITHUB_TOKEN if available to avoid authorization failures or rate-limiting. $headers = $github_token ? [ 'Authorization' => 'token ' . $github_token ] : []; $options = [ 'insecure' => $insecure ]; // Generate raw git URL of composer.json file. $raw_content_url = "https://raw.githubusercontent.com/{$package_name}/{$this->get_raw_git_version( $version )}/composer.json"; $response = Utils\http_request( 'GET', $raw_content_url, null /*data*/, $headers, $options ); if ( 20 !== (int) substr( $response->status_code, 0, 2 ) ) { // Could not get composer.json. Possibly private so warn and return best guess from input (always xxx/xxx). WP_CLI::warning( sprintf( "Couldn't download composer.json file from '%s' (HTTP code %d). Presuming package name is '%s'.", $raw_content_url, $response->status_code, $package_name ) ); return $package_name; } // Convert composer.json JSON to Array. $composer_content_as_array = json_decode( $response->body, true ); if ( null === $composer_content_as_array ) { WP_CLI::error( sprintf( "Failed to parse '%s' as json.", $raw_content_url ) ); } if ( empty( $composer_content_as_array['name'] ) ) { WP_CLI::error( sprintf( "Invalid package: no name in composer.json file '%s'.", $raw_content_url ) ); } // Package name in composer.json that is hosted on GitHub. $package_name_on_repo = $composer_content_as_array['name']; // If package name and repository name are not identical, then fix it. if ( $package_name !== $package_name_on_repo ) { WP_CLI::warning( sprintf( "Package name mismatch...Updating from git name '%s' to composer.json name '%s'.", $package_name, $package_name_on_repo ) ); $package_name = $package_name_on_repo; } return $package_name; } /** * Checks that `$package_name` matches the name in composer.json at the corresponding upstream repository, and return corrected value if not. * * @param string $package_name Package name to check. * @param string $url URL to fetch the package from. * @param string $version Optional. Package version. Defaults to empty string. * @param bool $insecure Optional. Whether to insecurely retry downloads that failed TLS handshake. Defaults * to false. */ private function check_git_package_name( $package_name, $url = '', $version = '', $insecure = false ) { if ( $url && ( strpos( $url, '://gitlab.com/' ) !== false ) || ( strpos( $url, 'git@gitlab.com:' ) !== false ) ) { return $this->check_gitlab_package_name( $package_name, $version, $insecure ); } return $this->check_github_package_name( $package_name, $version, $insecure ); } /** * Checks that `$package_name` matches the name in composer.json at GitLab.com, and return corrected value if not. * * @param string $package_name Package name to check. * @param string $version Optional. Package version. Defaults to empty string. * @param bool $insecure Optional. Whether to insecurely retry downloads that failed TLS handshake. Defaults * to false. */ private function check_gitlab_package_name( $package_name, $version = '', $insecure = false ) { // Generate raw git URL of composer.json file. $raw_content_public_url = 'https://gitlab.com/' . $package_name . '/-/raw/' . $this->get_raw_git_version( $version ) . '/composer.json'; $raw_content_private_url = 'https://gitlab.com/api/v4/projects/' . rawurlencode( $package_name ) . '/repository/files/composer.json/raw?ref=' . $this->get_raw_git_version( $version ); $options = [ 'insecure' => $insecure ]; $response = Utils\http_request( 'GET', $raw_content_public_url, null /*data*/, [], $options ); if ( $response->status_code < 200 || $response->status_code >= 300 ) { // Could not get composer.json. Possibly private so warn and return best guess from input (always xxx/xxx). WP_CLI::warning( sprintf( "Couldn't download composer.json file from '%s' (HTTP code %d). Presuming package name is '%s'.", $raw_content_public_url, $response->status_code, $package_name ) ); return $package_name; } if ( strpos( $response->headers['content-type'], 'text/html' ) === 0 ) { $gitlab_token = getenv( 'GITLAB_TOKEN' ); // Use GITLAB_TOKEN if available to avoid authorization failures or rate-limiting. $headers = $gitlab_token ? [ 'PRIVATE-TOKEN' => $gitlab_token ] : []; $response = Utils\http_request( 'GET', $raw_content_private_url, null /*data*/, $headers, $options ); if ( $response->status_code < 200 || $response->status_code >= 300 ) { // Could not get composer.json. Possibly private so warn and return best guess from input (always xxx/xxx). WP_CLI::warning( sprintf( "Couldn't download composer.json file from '%s' (HTTP code %d). Presuming package name is '%s'.", $raw_content_private_url, $response->status_code, $package_name ) ); return $package_name; } } // Convert composer.json JSON to Array. $composer_content_as_array = json_decode( $response->body, true ); if ( null === $composer_content_as_array ) { WP_CLI::error( sprintf( "Failed to parse '%s' as json.", $response->url ) ); } if ( empty( $composer_content_as_array['name'] ) ) { WP_CLI::error( sprintf( "Invalid package: no name in composer.json file '%s'.", $response->url ) ); } // Package name in composer.json that is hosted on Gitlab. $package_name_on_repo = $composer_content_as_array['name']; // If package name and repository name are not identical, then fix it. if ( $package_name !== $package_name_on_repo ) { WP_CLI::warning( sprintf( "Package name mismatch...Updating from git name '%s' to composer.json name '%s'.", $package_name, $package_name_on_repo ) ); $package_name = $package_name_on_repo; } return $package_name; } /** * Get the version to use for raw GitHub request. Very basic. * * @string $version Package version. * @string Version to use for GitHub request. */ private function get_raw_git_version( $version ) { if ( '' === $version ) { return 'master'; } // If Composer hash given then just use whatever's after it. $hash_pos = strpos( $version, '#' ); if ( false !== $hash_pos ) { return substr( $version, $hash_pos + 1 ); } // Strip any Composer 'dev-' prefix. if ( 0 === strncmp( $version, 'dev-', 4 ) ) { $version = substr( $version, 4 ); } // Ignore/strip any relative suffixes. return str_replace( [ '^', '~' ], '', $version ); } /** * Gets the release tag for the latest stable release of a GitHub repository. * * @param string $package_name Name of the repository. * * @return string Release tag. */ private function get_github_latest_release_tag( $package_name, $insecure ) { $url = "https://api.github.com/repos/{$package_name}/releases/latest"; $options = [ 'insecure' => $insecure ]; $response = Utils\http_request( 'GET', $url, null, [], $options ); if ( 20 !== (int) substr( $response->status_code, 0, 2 ) ) { WP_CLI::warning( 'Could not guess stable version from GitHub repository, falling back to master branch' ); return 'master'; } $package_data = json_decode( $response->body ); if ( JSON_ERROR_NONE !== json_last_error() ) { WP_CLI::warning( 'Could not guess stable version from GitHub repository, falling back to master branch' ); return 'master'; } $tag = $package_data->tag_name; WP_CLI::debug( "Extracted latest stable release tag: {$tag}", 'packages' ); return $tag; } /** * Guesses the version constraint from a release tag. * * @param string $tag Release tag to guess the version constraint from. * * @return string Version constraint. */ private function guess_version_constraint_from_tag( $tag ) { $matches = []; if ( 1 !== preg_match( '/(?:version|v)\s*((?:[0-9]+\.?)+)(?:-.*)/i', $tag, $matches ) ) { return $tag; } $constraint = "^{$matches[1]}"; WP_CLI::debug( "Guessing version constraint to use: {$constraint}", 'packages' ); return $constraint; } /** * Sets `COMPOSER_AUTH` environment variable (which Composer merges into the config setup in `Composer\Factory::createConfig()`) depending on available environment variables. * Avoids authorization failures when accessing various sites. */ private function set_composer_auth_env_var() { $changed = false; $composer_auth = getenv( 'COMPOSER_AUTH' ); if ( false !== $composer_auth ) { $composer_auth = json_decode( $composer_auth, true /*assoc*/ ); } if ( empty( $composer_auth ) || ! is_array( $composer_auth ) ) { $composer_auth = []; } $github_token = getenv( 'GITHUB_TOKEN' ); if ( ! isset( $composer_auth['github-oauth'] ) && is_string( $github_token ) ) { $composer_auth['github-oauth'] = [ 'github.com' => $github_token ]; $changed = true; } if ( $changed ) { putenv( 'COMPOSER_AUTH=' . json_encode( $composer_auth ) ); } } /** * Avoid using default Composer CA bundle if in phar as we don't include it. * See https://github.com/composer/ca-bundle/blob/1.1.0/src/CaBundle.php#L64 */ private function avoid_composer_ca_bundle() { if ( Utils\inside_phar() && ! getenv( 'SSL_CERT_FILE' ) && ! getenv( 'SSL_CERT_DIR' ) && ! ini_get( 'openssl.cafile' ) && ! ini_get( 'openssl.capath' ) ) { $certificate = Utils\extract_from_phar( WP_CLI_VENDOR_DIR . self::SSL_CERTIFICATE ); putenv( "SSL_CERT_FILE={$certificate}" ); } } /** * Reads the WP-CLI packages composer.json, checking validity and returning array containing its path, contents, and decoded contents. * * @return array Indexed array containing the path, the contents, and the decoded contents of the WP-CLI packages composer.json. */ private function get_composer_json_path_backup_decoded() { $composer_json_obj = $this->get_composer_json(); $json_path = $composer_json_obj->getPath(); $composer_backup = file_get_contents( $json_path ); if ( false === $composer_backup ) { $error = error_get_last(); WP_CLI::error( sprintf( "Failed to read '%s': %s", $json_path, $error['message'] ) ); } try { $composer_backup_decoded = $composer_json_obj->read(); } catch ( Exception $e ) { WP_CLI::error( sprintf( "Failed to parse '%s' as json: %s", $json_path, $e->getMessage() ) ); } return [ $json_path, $composer_backup, $composer_backup_decoded ]; } /** * Registers a PHP shutdown function to revert composer.json unless * referenced `$revert` flag is false. * * @param string $json_path Path to composer.json. * @param string $composer_backup Original contents of composer.json. * @param bool &$revert Flags whether to revert or not. */ private function register_revert_shutdown_function( $json_path, $composer_backup, &$revert ) { // Allocate all needed memory beforehand as much as possible. $revert_msg = "Reverted composer.json.\n"; $revert_fail_msg = "Failed to revert composer.json.\n"; $memory_msg = "WP-CLI ran out of memory. Please see https://bit.ly/wpclimem for further help.\n"; $memory_string = 'Allowed memory size of'; $error_array = [ 'type' => 42, 'message' => 'Some random dummy string to take up memory', 'file' => 'Another random string, which would be a filename this time', 'line' => 314, ]; register_shutdown_function( static function () use ( $json_path, $composer_backup, &$revert, $revert_msg, $revert_fail_msg, $memory_msg, $memory_string, $error_array ) { if ( $revert ) { if ( false !== file_put_contents( $json_path, $composer_backup ) ) { fwrite( STDERR, $revert_msg ); } else { fwrite( STDERR, $revert_fail_msg ); } } $error_array = error_get_last(); if ( is_array( $error_array ) && false !== strpos( $error_array['message'], $memory_string ) ) { fwrite( STDERR, $memory_msg ); } } ); } /** * Check whether we are dealing with Composer version 2.0.0+. * * @return bool */ private function is_composer_v2() { return version_compare( Composer::getVersion(), '2.0.0', '>=' ); } /** * Try to retrieve default branch via GitHub API. * * @param string $package_name GitHub package name to retrieve the default branch from. * @param bool $insecure Optional. Whether to insecurely retry downloads that failed TLS handshake. Defaults * to false. * @return string Default branch, or 'master' if it could not be retrieved. */ private function get_github_default_branch( $package_name, $insecure = false ) { $github_token = getenv( 'GITHUB_TOKEN' ); // Use GITHUB_TOKEN if available to avoid authorization failures or rate-limiting. $headers = $github_token ? [ 'Authorization' => 'token ' . $github_token ] : []; $options = [ 'insecure' => $insecure ]; $matches = []; if ( preg_match( '#^(?:https?://github\.com/|git@github\.com:)(?<repo_name>.*?).git$#', $package_name, $matches ) ) { $package_name = $matches['repo_name']; } $github_api_repo_url = "https://api.github.com/repos/{$package_name}"; $response = Utils\http_request( 'GET', $github_api_repo_url, null /*data*/, $headers, $options ); if ( 20 !== (int) substr( $response->status_code, 0, 2 ) ) { WP_CLI::warning( sprintf( "Couldn't fetch default branch for package '%s' (HTTP code %d). Presuming default branch is 'master'.", $package_name, $response->status_code ) ); return 'master'; } $package_data = json_decode( $response->body ); if ( JSON_ERROR_NONE !== json_last_error() ) { WP_CLI::warning( "Couldn't fetch default branch for package '%s' (failed to decode JSON response). Presuming default branch is 'master'." ); return 'master'; } $default_branch = $package_data->default_branch; if ( ! is_string( $default_branch ) || empty( $default_branch ) ) { WP_CLI::warning( sprintf( "Couldn't fetch default branch for package '%s'. Presuming default branch is 'master'.", $package_name ) ); return 'master'; } WP_CLI::debug( "Detected package default branch: {$default_branch}", 'packages' ); return $default_branch; } } <?php if ( ! class_exists( 'WP_CLI' ) ) { return; } $wpcli_package_autoloader = dirname( __FILE__ ) . '/vendor/autoload.php'; if ( file_exists( $wpcli_package_autoloader ) && ! class_exists( 'Package_Command' ) ) { require_once $wpcli_package_autoloader; } WP_CLI::add_command( 'package', 'Package_Command' ); <?php /** * An example application using php-cli-tools and Buzz */ require_once __DIR__ . '/vendor/autoload.php'; define('BUZZ_PATH', realpath('../Buzz')); define('SCRIPT_NAME', array_shift($argv)); require_once BUZZ_PATH . '/lib/Buzz/ClassLoader.php'; Buzz\ClassLoader::register(); class HttpConsole { protected $_host; protected $_prompt; public function __construct($host) { $this->_host = 'http://' . $host; $this->_prompt = '%K' . $this->_host . '%n/%K>%n '; } public function handleRequest($type, $path) { $request = new Buzz\Message\Request($type, $path, $this->_host); $response = new Buzz\Message\Response; $client = new Buzz\Client\FileGetContents(); $client->send($request, $response); // Display headers foreach ($response->getHeaders() as $i => $header) { if ($i == 0) { \cli\line('%G{:header}%n', compact('header')); continue; } list($key, $value) = explode(': ', $header, 2); \cli\line('%W{:key}%n: {:value}', compact('key', 'value')); } \cli\line("\n"); print $response->getContent() . "\n"; switch ($type) { } } public function run() { while (true) { $cmd = \cli\prompt($this->_prompt, false, null); if (preg_match('/^(HEAD|GET|POST|PUT|DELETE) (\S+)$/', $cmd, $matches)) { $this->handleRequest($matches[1], $matches[2]); continue; } if ($cmd == '\q') { break; } } } } try { $console = new HttpConsole(array_shift($argv) ?: '127.0.0.1:80'); $console->run(); } catch (\Exception $e) { \cli\err("\n\n%R" . $e->getMessage() . "%n\n"); } ?> <?php error_reporting(-1); require_once __DIR__ . '/vendor/autoload.php'; $args = new cli\Arguments(array( 'flags' => array( 'verbose' => array( 'description' => 'Turn on verbose mode', 'aliases' => array('v') ), 'c' => array( 'description' => 'A counter to test stackable', 'stackable' => true ) ), 'options' => array( 'user' => array( 'description' => 'Username for authentication', 'aliases' => array('u') ) ), 'strict' => true )); try { $args->parse(); } catch (cli\InvalidArguments $e) { echo $e->getMessage() . "\n\n"; } print_r($args->getArguments()); <?php /** * PHP Command Line Tools * * This source file is subject to the MIT license that is bundled * with this package in the file LICENSE. * * @author James Logsdon <dwarf@girsbrain.org> * @copyright 2010 James Logsdom (http://girsbrain.org) * @license http://www.opensource.org/licenses/mit-license.php The MIT License */ namespace cli; /** * A more complex type of Notifier, `Progress` Notifiers always have a maxim * value and generally show some form of percent complete or estimated time * to completion along with the standard Notifier displays. * * @see cli\Notify */ abstract class Progress extends \cli\Notify { protected $_total = 0; /** * Instantiates a Progress Notifier. * * @param string $msg The text to display next to the Notifier. * @param int $total The total number of ticks we will be performing. * @param int $interval The interval in milliseconds between updates. * @see cli\Progress::setTotal() */ public function __construct($msg, $total, $interval = 100) { parent::__construct($msg, $interval); $this->setTotal($total); } /** * Set the max increments for this progress notifier. * * @param int $total The total number of times this indicator should be `tick`ed. * @throws \InvalidArgumentException Thrown if the `$total` is less than 0. */ public function setTotal($total) { $this->_total = (int)$total; if ($this->_total < 0) { throw new \InvalidArgumentException('Maximum value out of range, must be positive.'); } } /** * Reset the progress state so the same instance can be used in multiple loops. */ public function reset($total = null) { parent::reset(); if ($total) { $this->setTotal($total); } } /** * Behaves in a similar manner to `cli\Notify::current()`, but the output * is padded to match the length of `cli\Progress::total()`. * * @return string The formatted and padded tick count. * @see cli\Progress::total() */ public function current() { $size = strlen($this->total()); return str_pad(parent::current(), $size); } /** * Returns the formatted total expected ticks. * * @return string The formatted total ticks. */ public function total() { return number_format($this->_total); } /** * Calculates the estimated total time for the tick count to reach the * total ticks given. * * @return int The estimated total number of seconds for all ticks to be * completed. This is not the estimated time left, but total. * @see cli\Notify::speed() * @see cli\Notify::elapsed() */ public function estimated() { $speed = $this->speed(); if (!$speed || !$this->elapsed()) { return 0; } $estimated = round($this->_total / $speed); return $estimated; } /** * Forces the current tick count to the total ticks given at instatiation * time before passing on to `cli\Notify::finish()`. */ public function finish() { $this->_current = $this->_total; parent::finish(); } /** * Increments are tick counter by the given amount. If no amount is provided, * the ticker is incremented by 1. * * @param int $increment The amount to increment by. */ public function increment($increment = 1) { $this->_current = min($this->_total, $this->_current + $increment); } /** * Calculate the percentage completed. * * @return float The percent completed. */ public function percent() { if ($this->_total == 0) { return 1; } return ($this->_current / $this->_total); } } <?php /** * PHP Command Line Tools * * This source file is subject to the MIT license that is bundled * with this package in the file LICENSE. * * @author James Logsdon <dwarf@girsbrain.org> * @copyright 2010 James Logsdom (http://girsbrain.org) * @license http://www.opensource.org/licenses/mit-license.php The MIT License */ namespace cli; /** * The `Shell` class is a utility class for shell related tasks such as * information on width. */ class Shell { /** * Returns the number of columns the current shell has for display. * * @return int The number of columns. * @todo Test on more systems. */ static public function columns() { static $columns; if ( getenv( 'PHP_CLI_TOOLS_TEST_SHELL_COLUMNS_RESET' ) ) { $columns = null; } if ( null === $columns ) { if ( function_exists( 'exec' ) ) { if ( self::is_windows() ) { // Cater for shells such as Cygwin and Git bash where `mode CON` returns an incorrect value for columns. if ( ( $shell = getenv( 'SHELL' ) ) && preg_match( '/(?:bash|zsh)(?:\.exe)?$/', $shell ) && getenv( 'TERM' ) ) { $columns = (int) exec( 'tput cols' ); } if ( ! $columns ) { $return_var = -1; $output = array(); exec( 'mode CON', $output, $return_var ); if ( 0 === $return_var && $output ) { // Look for second line ending in ": <number>" (searching for "Columns:" will fail on non-English locales). if ( preg_match( '/:\s*[0-9]+\n[^:]+:\s*([0-9]+)\n/', implode( "\n", $output ), $matches ) ) { $columns = (int) $matches[1]; } } } } else { if ( ! ( $columns = (int) getenv( 'COLUMNS' ) ) ) { $size = exec( '/usr/bin/env stty size 2>/dev/null' ); if ( '' !== $size && preg_match( '/[0-9]+ ([0-9]+)/', $size, $matches ) ) { $columns = (int) $matches[1]; } if ( ! $columns ) { if ( getenv( 'TERM' ) ) { $columns = (int) exec( '/usr/bin/env tput cols 2>/dev/null' ); } } } } } if ( ! $columns ) { $columns = 80; // default width of cmd window on Windows OS } } return $columns; } /** * Checks whether the output of the current script is a TTY or a pipe / redirect * * Returns true if STDOUT output is being redirected to a pipe or a file; false is * output is being sent directly to the terminal. * * If an env variable SHELL_PIPE exists, returned result depends it's * value. Strings like 1, 0, yes, no, that validate to booleans are accepted. * * To enable ASCII formatting even when shell is piped, use the * ENV variable SHELL_PIPE=0 * * @return bool */ static public function isPiped() { $shellPipe = getenv('SHELL_PIPE'); if ($shellPipe !== false) { return filter_var($shellPipe, FILTER_VALIDATE_BOOLEAN); } else { if ( function_exists('stream_isatty') ) { return !stream_isatty(STDOUT); } else { return (function_exists('posix_isatty') && !posix_isatty(STDOUT)); } } } /** * Uses `stty` to hide input/output completely. * @param boolean $hidden Will hide/show the next data. Defaults to true. */ static public function hide($hidden = true) { system( 'stty ' . ( $hidden? '-echo' : 'echo' ) ); } /** * Is this shell in Windows? * * @return bool */ static private function is_windows() { return strtoupper(substr(PHP_OS, 0, 3)) === 'WIN'; } } ?> <?php /** * PHP Command Line Tools * * This source file is subject to the MIT license that is bundled * with this package in the file LICENSE. * * @author James Logsdon <dwarf@girsbrain.org> * @copyright 2010 James Logsdom (http://girsbrain.org) * @license http://www.opensource.org/licenses/mit-license.php The MIT License */ namespace cli\progress; use cli; use cli\Notify; use cli\Progress; use cli\Shell; use cli\Streams; /** * Displays a progress bar spanning the entire shell. * * Basic format: * * ^MSG PER% [======================= ] 00:00 / 00:00$ */ class Bar extends Progress { protected $_bars = '=>'; protected $_formatMessage = '{:msg} {:percent}% ['; protected $_formatTiming = '] {:elapsed} / {:estimated}'; protected $_format = '{:msg}{:bar}{:timing}'; /** * Prints the progress bar to the screen with percent complete, elapsed time * and estimated total time. * * @param boolean $finish `true` if this was called from * `cli\Notify::finish()`, `false` otherwise. * @see cli\out() * @see cli\Notify::formatTime() * @see cli\Notify::elapsed() * @see cli\Progress::estimated(); * @see cli\Progress::percent() * @see cli\Shell::columns() */ public function display($finish = false) { $_percent = $this->percent(); $percent = str_pad(floor($_percent * 100), 3); $msg = $this->_message; $msg = Streams::render($this->_formatMessage, compact('msg', 'percent')); $estimated = $this->formatTime($this->estimated()); $elapsed = str_pad($this->formatTime($this->elapsed()), strlen($estimated)); $timing = Streams::render($this->_formatTiming, compact('elapsed', 'estimated')); $size = Shell::columns(); $size -= strlen($msg . $timing); if ( $size < 0 ) { $size = 0; } $bar = str_repeat($this->_bars[0], floor($_percent * $size)) . $this->_bars[1]; // substr is needed to trim off the bar cap at 100% $bar = substr(str_pad($bar, $size, ' '), 0, $size); Streams::out($this->_format, compact('msg', 'bar', 'timing')); } /** * This method augments the base definition from cli\Notify to optionally * allow passing a new message. * * @param int $increment The amount to increment by. * @param string $msg The text to display next to the Notifier. (optional) * @see cli\Notify::tick() */ public function tick($increment = 1, $msg = null) { if ($msg) { $this->_message = $msg; } Notify::tick($increment); } } <?php /** * PHP Command Line Tools * * This source file is subject to the MIT license that is bundled * with this package in the file LICENSE. * * @author James Logsdon <dwarf@girsbrain.org> * @copyright 2010 James Logsdom (http://girsbrain.org) * @license http://www.opensource.org/licenses/mit-license.php The MIT License */ namespace cli\notify; use cli\Notify; use cli\Streams; /** * The `Spinner` Notifier displays an ASCII spinner. */ class Spinner extends Notify { protected $_chars = '-\|/'; protected $_format = '{:msg} {:char} ({:elapsed}, {:speed}/s)'; protected $_iteration = 0; /** * Prints the current spinner position to `STDOUT` with the time elapsed * and tick speed. * * @param boolean $finish `true` if this was called from * `cli\Notify::finish()`, `false` otherwise. * @see cli\out_padded() * @see cli\Notify::formatTime() * @see cli\Notify::speed() */ public function display($finish = false) { $msg = $this->_message; $idx = $this->_iteration++ % strlen($this->_chars); $char = $this->_chars[$idx]; $speed = number_format(round($this->speed())); $elapsed = $this->formatTime($this->elapsed()); Streams::out_padded($this->_format, compact('msg', 'char', 'elapsed', 'speed')); } } <?php /** * PHP Command Line Tools * * This source file is subject to the MIT license that is bundled * with this package in the file LICENSE. * * @author James Logsdon <dwarf@girsbrain.org> * @copyright 2010 James Logsdom (http://girsbrain.org) * @license http://www.opensource.org/licenses/mit-license.php The MIT License */ namespace cli\notify; use cli\Notify; use cli\Streams; /** * A Notifer that displays a string of periods. */ class Dots extends Notify { protected $_dots; protected $_format = '{:msg}{:dots} ({:elapsed}, {:speed}/s)'; protected $_iteration; /** * Instatiates a Notification object. * * @param string $msg The text to display next to the Notifier. * @param int $dots The number of dots to iterate through. * @param int $interval The interval in milliseconds between updates. * @throws \InvalidArgumentException */ public function __construct($msg, $dots = 3, $interval = 100) { parent::__construct($msg, $interval); $this->_dots = (int)$dots; if ($this->_dots <= 0) { throw new \InvalidArgumentException('Dot count out of range, must be positive.'); } } /** * Prints the correct number of dots to `STDOUT` with the time elapsed and * tick speed. * * @param boolean $finish `true` if this was called from * `cli\Notify::finish()`, `false` otherwise. * @see cli\out_padded() * @see cli\Notify::formatTime() * @see cli\Notify::speed() */ public function display($finish = false) { $repeat = $this->_dots; if (!$finish) { $repeat = $this->_iteration++ % $repeat; } $msg = $this->_message; $dots = str_pad(str_repeat('.', $repeat), $this->_dots); $speed = number_format(round($this->speed())); $elapsed = $this->formatTime($this->elapsed()); Streams::out_padded($this->_format, compact('msg', 'dots', 'speed', 'elapsed')); } } <?php /** * PHP Command Line Tools * * This source file is subject to the MIT license that is bundled * with this package in the file LICENSE. * * @author James Logsdon <dwarf@girsbrain.org> * @copyright 2010 James Logsdom (http://girsbrain.org) * @license http://www.opensource.org/licenses/mit-license.php The MIT License */ namespace cli; /** * Change the color of text. * * Reference: http://graphcomp.com/info/specs/ansi_col.html#colors */ class Colors { static protected $_colors = array( 'color' => array( 'black' => 30, 'red' => 31, 'green' => 32, 'yellow' => 33, 'blue' => 34, 'magenta' => 35, 'cyan' => 36, 'white' => 37 ), 'style' => array( 'bright' => 1, 'dim' => 2, 'underline' => 4, 'blink' => 5, 'reverse' => 7, 'hidden' => 8 ), 'background' => array( 'black' => 40, 'red' => 41, 'green' => 42, 'yellow' => 43, 'blue' => 44, 'magenta' => 45, 'cyan' => 46, 'white' => 47 ) ); static protected $_enabled = null; static protected $_string_cache = array(); static public function enable($force = true) { self::$_enabled = $force === true ? true : null; } static public function disable($force = true) { self::$_enabled = $force === true ? false : null; } /** * Check if we should colorize output based on local flags and shell type. * * Only check the shell type if `Colors::$_enabled` is null and `$colored` is null. */ static public function shouldColorize($colored = null) { return self::$_enabled === true || (self::$_enabled !== false && ($colored === true || ($colored !== false && Streams::isTty()))); } /** * Set the color. * * @param string $color The name of the color or style to set. * @return string */ static public function color($color) { if (!is_array($color)) { $color = compact('color'); } $color += array('color' => null, 'style' => null, 'background' => null); if ($color['color'] == 'reset') { return "\033[0m"; } $colors = array(); foreach (array('color', 'style', 'background') as $type) { $code = $color[$type]; if (isset(self::$_colors[$type][$code])) { $colors[] = self::$_colors[$type][$code]; } } if (empty($colors)) { $colors[] = 0; } return "\033[" . join(';', $colors) . "m"; } /** * Colorize a string using helpful string formatters. If the `Streams::$out` points to a TTY coloring will be enabled, * otherwise disabled. You can control this check with the `$colored` parameter. * * @param string $string * @param boolean $colored Force enable or disable the colorized output. If left as `null` the TTY will control coloring. * @return string */ static public function colorize($string, $colored = null) { $passed = $string; if (!self::shouldColorize($colored)) { $return = self::decolorize( $passed, 2 /*keep_encodings*/ ); self::cacheString($passed, $return); return $return; } $md5 = md5($passed); if (isset(self::$_string_cache[$md5]['colorized'])) { return self::$_string_cache[$md5]['colorized']; } $string = str_replace('%%', '%¾', $string); foreach (self::getColors() as $key => $value) { $string = str_replace($key, self::color($value), $string); } $string = str_replace('%¾', '%', $string); self::cacheString($passed, $string); return $string; } /** * Remove color information from a string. * * @param string $string A string with color information. * @param int $keep Optional. If the 1 bit is set, color tokens (eg "%n") won't be stripped. If the 2 bit is set, color encodings (ANSI escapes) won't be stripped. Default 0. * @return string A string with color information removed. */ static public function decolorize( $string, $keep = 0 ) { if ( ! ( $keep & 1 ) ) { // Get rid of color tokens if they exist $string = str_replace('%%', '%¾', $string); $string = str_replace(array_keys(self::getColors()), '', $string); $string = str_replace('%¾', '%', $string); } if ( ! ( $keep & 2 ) ) { // Remove color encoding if it exists foreach (self::getColors() as $key => $value) { $string = str_replace(self::color($value), '', $string); } } return $string; } /** * Cache the original, colorized, and decolorized versions of a string. * * @param string $passed The original string before colorization. * @param string $colorized The string after running through self::colorize. * @param string $deprecated Optional. Not used. Default null. */ static public function cacheString( $passed, $colorized, $deprecated = null ) { self::$_string_cache[md5($passed)] = array( 'passed' => $passed, 'colorized' => $colorized, 'decolorized' => self::decolorize($passed), // Not very useful but keep for BC. ); } /** * Return the length of the string without color codes. * * @param string $string the string to measure * @return int */ static public function length($string) { return safe_strlen( self::decolorize( $string ) ); } /** * Return the width (length in characters) of the string without color codes if enabled. * * @param string $string The string to measure. * @param bool $pre_colorized Optional. Set if the string is pre-colorized. Default false. * @param string|bool $encoding Optional. The encoding of the string. Default false. * @return int */ static public function width( $string, $pre_colorized = false, $encoding = false ) { return strwidth( $pre_colorized || self::shouldColorize() ? self::decolorize( $string, $pre_colorized ? 1 /*keep_tokens*/ : 0 ) : $string, $encoding ); } /** * Pad the string to a certain display length. * * @param string $string The string to pad. * @param int $length The display length. * @param bool $pre_colorized Optional. Set if the string is pre-colorized. Default false. * @param string|bool $encoding Optional. The encoding of the string. Default false. * @param int $pad_type Optional. Can be STR_PAD_RIGHT, STR_PAD_LEFT, or STR_PAD_BOTH. If pad_type is not specified it is assumed to be STR_PAD_RIGHT. * @return string */ static public function pad( $string, $length, $pre_colorized = false, $encoding = false, $pad_type = STR_PAD_RIGHT ) { $real_length = self::width( $string, $pre_colorized, $encoding ); $diff = strlen( $string ) - $real_length; $length += $diff; return str_pad( $string, $length, ' ', $pad_type ); } /** * Get the color mapping array. * * @return array Array of color tokens mapped to colors and styles. */ static public function getColors() { return array( '%y' => array('color' => 'yellow'), '%g' => array('color' => 'green'), '%b' => array('color' => 'blue'), '%r' => array('color' => 'red'), '%p' => array('color' => 'magenta'), '%m' => array('color' => 'magenta'), '%c' => array('color' => 'cyan'), '%w' => array('color' => 'white'), '%k' => array('color' => 'black'), '%n' => array('color' => 'reset'), '%Y' => array('color' => 'yellow', 'style' => 'bright'), '%G' => array('color' => 'green', 'style' => 'bright'), '%B' => array('color' => 'blue', 'style' => 'bright'), '%R' => array('color' => 'red', 'style' => 'bright'), '%P' => array('color' => 'magenta', 'style' => 'bright'), '%M' => array('color' => 'magenta', 'style' => 'bright'), '%C' => array('color' => 'cyan', 'style' => 'bright'), '%W' => array('color' => 'white', 'style' => 'bright'), '%K' => array('color' => 'black', 'style' => 'bright'), '%N' => array('color' => 'reset', 'style' => 'bright'), '%3' => array('background' => 'yellow'), '%2' => array('background' => 'green'), '%4' => array('background' => 'blue'), '%1' => array('background' => 'red'), '%5' => array('background' => 'magenta'), '%6' => array('background' => 'cyan'), '%7' => array('background' => 'white'), '%0' => array('background' => 'black'), '%F' => array('style' => 'blink'), '%U' => array('style' => 'underline'), '%8' => array('style' => 'reverse'), '%9' => array('style' => 'bright'), '%_' => array('style' => 'bright') ); } /** * Get the cached string values. * * @return array The cached string values. */ static public function getStringCache() { return self::$_string_cache; } /** * Clear the string cache. */ static public function clearStringCache() { self::$_string_cache = array(); } } <?php /** * PHP Command Line Tools * * This source file is subject to the MIT license that is bundled * with this package in the file LICENSE. * * @author James Logsdon <dwarf@girsbrain.org> * @copyright 2010 James Logsdom (http://girsbrain.org) * @license http://www.opensource.org/licenses/mit-license.php The MIT License */ namespace cli; use cli\arguments\Argument; use cli\arguments\HelpScreen; use cli\arguments\InvalidArguments; use cli\arguments\Lexer; /** * Parses command line arguments. */ class Arguments implements \ArrayAccess { protected $_flags = array(); protected $_options = array(); protected $_strict = false; protected $_input = array(); protected $_invalid = array(); protected $_parsed; protected $_lexer; /** * Initializes the argument parser. If you wish to change the default behaviour * you may pass an array of options as the first argument. Valid options are * `'help'` and `'strict'`, each a boolean. * * `'help'` is `true` by default, `'strict'` is false by default. * * @param array $options An array of options for this parser. */ public function __construct($options = array()) { $options += array( 'strict' => false, 'input' => array_slice($_SERVER['argv'], 1) ); $this->_input = $options['input']; $this->setStrict($options['strict']); if (isset($options['flags'])) { $this->addFlags($options['flags']); } if (isset($options['options'])) { $this->addOptions($options['options']); } } /** * Get the list of arguments found by the defined definitions. * * @return array */ public function getArguments() { if (!isset($this->_parsed)) { $this->parse(); } return $this->_parsed; } public function getHelpScreen() { return new HelpScreen($this); } /** * Encodes the parsed arguments as JSON. * * @return string */ public function asJSON() { return json_encode($this->_parsed); } /** * Returns true if a given argument was parsed. * * @param mixed $offset An Argument object or the name of the argument. * @return bool */ public function offsetExists($offset) { if ($offset instanceOf Argument) { $offset = $offset->key; } return array_key_exists($offset, $this->_parsed); } /** * Get the parsed argument's value. * * @param mixed $offset An Argument object or the name of the argument. * @return mixed */ public function offsetGet($offset) { if ($offset instanceOf Argument) { $offset = $offset->key; } if (isset($this->_parsed[$offset])) { return $this->_parsed[$offset]; } } /** * Sets the value of a parsed argument. * * @param mixed $offset An Argument object or the name of the argument. * @param mixed $value The value to set */ public function offsetSet($offset, $value) { if ($offset instanceOf Argument) { $offset = $offset->key; } $this->_parsed[$offset] = $value; } /** * Unset a parsed argument. * * @param mixed $offset An Argument object or the name of the argument. */ public function offsetUnset($offset) { if ($offset instanceOf Argument) { $offset = $offset->key; } unset($this->_parsed[$offset]); } /** * Adds a flag (boolean argument) to the argument list. * * @param mixed $flag A string representing the flag, or an array of strings. * @param array $settings An array of settings for this flag. * @setting string description A description to be shown in --help. * @setting bool default The default value for this flag. * @setting bool stackable Whether the flag is repeatable to increase the value. * @setting array aliases Other ways to trigger this flag. * @return $this */ public function addFlag($flag, $settings = array()) { if (is_string($settings)) { $settings = array('description' => $settings); } if (is_array($flag)) { $settings['aliases'] = $flag; $flag = array_shift($settings['aliases']); } if (isset($this->_flags[$flag])) { $this->_warn('flag already exists: ' . $flag); return $this; } $settings += array( 'default' => false, 'stackable' => false, 'description' => null, 'aliases' => array() ); $this->_flags[$flag] = $settings; return $this; } /** * Add multiple flags at once. The input array should be keyed with the * primary flag character, and the values should be the settings array * used by {addFlag}. * * @param array $flags An array of flags to add * @return $this */ public function addFlags($flags) { foreach ($flags as $flag => $settings) { if (is_numeric($flag)) { $this->_warn('No flag character given'); continue; } $this->addFlag($flag, $settings); } return $this; } /** * Adds an option (string argument) to the argument list. * * @param mixed $option A string representing the option, or an array of strings. * @param array $settings An array of settings for this option. * @setting string description A description to be shown in --help. * @setting bool default The default value for this option. * @setting array aliases Other ways to trigger this option. * @return $this */ public function addOption($option, $settings = array()) { if (is_string($settings)) { $settings = array('description' => $settings); } if (is_array($option)) { $settings['aliases'] = $option; $option = array_shift($settings['aliases']); } if (isset($this->_options[$option])) { $this->_warn('option already exists: ' . $option); return $this; } $settings += array( 'default' => null, 'description' => null, 'aliases' => array() ); $this->_options[$option] = $settings; return $this; } /** * Add multiple options at once. The input array should be keyed with the * primary option string, and the values should be the settings array * used by {addOption}. * * @param array $options An array of options to add * @return $this */ public function addOptions($options) { foreach ($options as $option => $settings) { if (is_numeric($option)) { $this->_warn('No option string given'); continue; } $this->addOption($option, $settings); } return $this; } /** * Enable or disable strict mode. If strict mode is active any invalid * arguments found by the parser will throw `cli\arguments\InvalidArguments`. * * Even if strict is disabled, invalid arguments are logged and can be * retrieved with `cli\Arguments::getInvalidArguments()`. * * @param bool $strict True to enable, false to disable. * @return $this */ public function setStrict($strict) { $this->_strict = (bool)$strict; return $this; } /** * Get the list of invalid arguments the parser found. * * @return array */ public function getInvalidArguments() { return $this->_invalid; } /** * Get a flag by primary matcher or any defined aliases. * * @param mixed $flag Either a string representing the flag or an * cli\arguments\Argument object. * @return array */ public function getFlag($flag) { if ($flag instanceOf Argument) { $obj = $flag; $flag = $flag->value; } if (isset($this->_flags[$flag])) { return $this->_flags[$flag]; } foreach ($this->_flags as $master => $settings) { if (in_array($flag, (array)$settings['aliases'])) { if (isset($obj)) { $obj->key = $master; } $cache[$flag] =& $settings; return $settings; } } } public function getFlags() { return $this->_flags; } public function hasFlags() { return !empty($this->_flags); } /** * Returns true if the given argument is defined as a flag. * * @param mixed $argument Either a string representing the flag or an * cli\arguments\Argument object. * @return bool */ public function isFlag($argument) { return (null !== $this->getFlag($argument)); } /** * Returns true if the given flag is stackable. * * @param mixed $flag Either a string representing the flag or an * cli\arguments\Argument object. * @return bool */ public function isStackable($flag) { $settings = $this->getFlag($flag); return isset($settings) && (true === $settings['stackable']); } /** * Get an option by primary matcher or any defined aliases. * * @param mixed $option Either a string representing the option or an * cli\arguments\Argument object. * @return array */ public function getOption($option) { if ($option instanceOf Argument) { $obj = $option; $option = $option->value; } if (isset($this->_options[$option])) { return $this->_options[$option]; } foreach ($this->_options as $master => $settings) { if (in_array($option, (array)$settings['aliases'])) { if (isset($obj)) { $obj->key = $master; } return $settings; } } } public function getOptions() { return $this->_options; } public function hasOptions() { return !empty($this->_options); } /** * Returns true if the given argument is defined as an option. * * @param mixed $argument Either a string representing the option or an * cli\arguments\Argument object. * @return bool */ public function isOption($argument) { return (null != $this->getOption($argument)); } /** * Parses the argument list with the given options. The returned argument list * will use either the first long name given or the first name in the list * if a long name is not given. * * @return array * @throws arguments\InvalidArguments */ public function parse() { $this->_invalid = array(); $this->_parsed = array(); $this->_lexer = new Lexer($this->_input); $this->_applyDefaults(); foreach ($this->_lexer as $argument) { if ($this->_parseFlag($argument)) { continue; } if ($this->_parseOption($argument)) { continue; } array_push($this->_invalid, $argument->raw); } if ($this->_strict && !empty($this->_invalid)) { throw new InvalidArguments($this->_invalid); } } /** * This applies the default values, if any, of all of the * flags and options, so that if there is a default value * it will be available. */ private function _applyDefaults() { foreach($this->_flags as $flag => $settings) { $this[$flag] = $settings['default']; } foreach($this->_options as $option => $settings) { // If the default is 0 we should still let it be set. if (!empty($settings['default']) || $settings['default'] === 0) { $this[$option] = $settings['default']; } } } private function _warn($message) { trigger_error('[' . __CLASS__ .'] ' . $message, E_USER_WARNING); } private function _parseFlag($argument) { if (!$this->isFlag($argument)) { return false; } if ($this->isStackable($argument)) { if (!isset($this[$argument])) { $this[$argument->key] = 0; } $this[$argument->key] += 1; } else { $this[$argument->key] = true; } return true; } private function _parseOption($option) { if (!$this->isOption($option)) { return false; } // Peak ahead to make sure we get a value. if ($this->_lexer->end() || !$this->_lexer->peek->isValue) { $optionSettings = $this->getOption($option->key); if (empty($optionSettings['default']) && $optionSettings !== 0) { // Oops! Got no value and no default , throw a warning and continue. $this->_warn('no value given for ' . $option->raw); $this[$option->key] = null; } else { // No value and we have a default, so we set to the default $this[$option->key] = $optionSettings['default']; } return true; } // Store as array and join to string after looping for values $values = array(); // Loop until we find a flag in peak-ahead foreach ($this->_lexer as $value) { array_push($values, $value->raw); if (!$this->_lexer->end() && !$this->_lexer->peek->isValue) { break; } } $this[$option->key] = join(' ', $values); return true; } } <?php /** * PHP Command Line Tools * * This source file is subject to the MIT license that is bundled * with this package in the file LICENSE. * * @author James Logsdon <dwarf@girsbrain.org> * @copyright 2010 James Logsdom (http://girsbrain.org) * @license http://www.opensource.org/licenses/mit-license.php The MIT License */ namespace cli; use cli\Shell; use cli\Streams; use cli\table\Ascii; use cli\table\Renderer; use cli\table\Tabular; /** * The `Table` class is used to display data in a tabular format. */ class Table { protected $_renderer; protected $_headers = array(); protected $_footers = array(); protected $_width = array(); protected $_rows = array(); /** * Initializes the `Table` class. * * There are 3 ways to instantiate this class: * * 1. Pass an array of strings as the first parameter for the column headers * and a 2-dimensional array as the second parameter for the data rows. * 2. Pass an array of hash tables (string indexes instead of numerical) * where each hash table is a row and the indexes of the *first* hash * table are used as the header values. * 3. Pass nothing and use `setHeaders()` and `addRow()` or `setRows()`. * * @param array $headers Headers used in this table. Optional. * @param array $rows The rows of data for this table. Optional. * @param array $footers Footers used in this table. Optional. */ public function __construct(array $headers = null, array $rows = null, array $footers = null) { if (!empty($headers)) { // If all the rows is given in $headers we use the keys from the // first row for the header values if ($rows === null) { $rows = $headers; $keys = array_keys(array_shift($headers)); $headers = array(); foreach ($keys as $header) { $headers[$header] = $header; } } $this->setHeaders($headers); $this->setRows($rows); } if (!empty($footers)) { $this->setFooters($footers); } if (Shell::isPiped()) { $this->setRenderer(new Tabular()); } else { $this->setRenderer(new Ascii()); } } public function resetTable() { $this->_headers = array(); $this->_width = array(); $this->_rows = array(); $this->_footers = array(); return $this; } /** * Sets the renderer used by this table. * * @param table\Renderer $renderer The renderer to use for output. * @see table\Renderer * @see table\Ascii * @see table\Tabular */ public function setRenderer(Renderer $renderer) { $this->_renderer = $renderer; } /** * Loops through the row and sets the maximum width for each column. * * @param array $row The table row. * @return array $row */ protected function checkRow(array $row) { foreach ($row as $column => $str) { $width = Colors::width( $str, $this->isAsciiPreColorized( $column ) ); if (!isset($this->_width[$column]) || $width > $this->_width[$column]) { $this->_width[$column] = $width; } } return $row; } /** * Output the table to `STDOUT` using `cli\line()`. * * If STDOUT is a pipe or redirected to a file, should output simple * tab-separated text. Otherwise, renders table with ASCII table borders * * @uses cli\Shell::isPiped() Determine what format to output * * @see cli\Table::renderRow() */ public function display() { foreach( $this->getDisplayLines() as $line ) { Streams::line( $line ); } } /** * Get the table lines to output. * * @see cli\Table::display() * @see cli\Table::renderRow() * * @return array */ public function getDisplayLines() { $this->_renderer->setWidths($this->_width, $fallback = true); $border = $this->_renderer->border(); $out = array(); if (isset($border)) { $out[] = $border; } $out[] = $this->_renderer->row($this->_headers); if (isset($border)) { $out[] = $border; } foreach ($this->_rows as $row) { $row = $this->_renderer->row($row); $row = explode( PHP_EOL, $row ); $out = array_merge( $out, $row ); } if (isset($border)) { $out[] = $border; } if ($this->_footers) { $out[] = $this->_renderer->row($this->_footers); if (isset($border)) { $out[] = $border; } } return $out; } /** * Sort the table by a column. Must be called before `cli\Table::display()`. * * @param int $column The index of the column to sort by. */ public function sort($column) { if (!isset($this->_headers[$column])) { trigger_error('No column with index ' . $column, E_USER_NOTICE); return; } usort($this->_rows, function($a, $b) use ($column) { return strcmp($a[$column], $b[$column]); }); } /** * Set the headers of the table. * * @param array $headers An array of strings containing column header names. */ public function setHeaders(array $headers) { $this->_headers = $this->checkRow($headers); } /** * Set the footers of the table. * * @param array $footers An array of strings containing column footers names. */ public function setFooters(array $footers) { $this->_footers = $this->checkRow($footers); } /** * Add a row to the table. * * @param array $row The row data. * @see cli\Table::checkRow() */ public function addRow(array $row) { $this->_rows[] = $this->checkRow($row); } /** * Clears all previous rows and adds the given rows. * * @param array $rows A 2-dimensional array of row data. * @see cli\Table::addRow() */ public function setRows(array $rows) { $this->_rows = array(); foreach ($rows as $row) { $this->addRow($row); } } public function countRows() { return count($this->_rows); } /** * Set whether items in an Ascii table are pre-colorized. * * @param bool|array $precolorized A boolean to set all columns in the table as pre-colorized, or an array of booleans keyed by column index (number) to set individual columns as pre-colorized. * @see cli\Ascii::setPreColorized() */ public function setAsciiPreColorized( $pre_colorized ) { if ( $this->_renderer instanceof Ascii ) { $this->_renderer->setPreColorized( $pre_colorized ); } } /** * Is a column in an Ascii table pre-colorized? * * @param int $column Column index to check. * @return bool True if whole Ascii table is marked as pre-colorized, or if the individual column is pre-colorized; else false. * @see cli\Ascii::isPreColorized() */ private function isAsciiPreColorized( $column ) { if ( $this->_renderer instanceof Ascii ) { return $this->_renderer->isPreColorized( $column ); } return false; } } <?php namespace cli; class Streams { protected static $out = STDOUT; protected static $in = STDIN; protected static $err = STDERR; static function _call( $func, $args ) { $method = __CLASS__ . '::' . $func; return call_user_func_array( $method, $args ); } static public function isTty() { if ( function_exists('stream_isatty') ) { return !stream_isatty(static::$out); } else { return (function_exists('posix_isatty') && !posix_isatty(static::$out)); } } /** * Handles rendering strings. If extra scalar arguments are given after the `$msg` * the string will be rendered with `sprintf`. If the second argument is an `array` * then each key in the array will be the placeholder name. Placeholders are of the * format {:key}. * * @param string $msg The message to render. * @param mixed ... Either scalar arguments or a single array argument. * @return string The rendered string. */ public static function render( $msg ) { $args = func_get_args(); // No string replacement is needed if( count( $args ) == 1 || ( is_string( $args[1] ) && '' === $args[1] ) ) { return Colors::shouldColorize() ? Colors::colorize( $msg ) : $msg; } // If the first argument is not an array just pass to sprintf if( !is_array( $args[1] ) ) { // Colorize the message first so sprintf doesn't bitch at us if ( Colors::shouldColorize() ) { $args[0] = Colors::colorize( $args[0] ); } // Escape percent characters for sprintf $args[0] = preg_replace('/(%([^\w]|$))/', "%$1", $args[0]); return call_user_func_array( 'sprintf', $args ); } // Here we do named replacement so formatting strings are more understandable foreach( $args[1] as $key => $value ) { $msg = str_replace( '{:' . $key . '}', $value, $msg ); } return Colors::shouldColorize() ? Colors::colorize( $msg ) : $msg; } /** * Shortcut for printing to `STDOUT`. The message and parameters are passed * through `sprintf` before output. * * @param string $msg The message to output in `printf` format. * @param mixed ... Either scalar arguments or a single array argument. * @return void * @see \cli\render() */ public static function out( $msg ) { fwrite( static::$out, self::_call( 'render', func_get_args() ) ); } /** * Pads `$msg` to the width of the shell before passing to `cli\out`. * * @param string $msg The message to pad and pass on. * @param mixed ... Either scalar arguments or a single array argument. * @return void * @see cli\out() */ public static function out_padded( $msg ) { $msg = self::_call( 'render', func_get_args() ); self::out( str_pad( $msg, \cli\Shell::columns() ) ); } /** * Prints a message to `STDOUT` with a newline appended. See `\cli\out` for * more documentation. * * @see cli\out() */ public static function line( $msg = '' ) { // func_get_args is empty if no args are passed even with the default above. $args = array_merge( func_get_args(), array( '' ) ); $args[0] .= "\n"; self::_call( 'out', $args ); } /** * Shortcut for printing to `STDERR`. The message and parameters are passed * through `sprintf` before output. * * @param string $msg The message to output in `printf` format. With no string, * a newline is printed. * @param mixed ... Either scalar arguments or a single array argument. * @return void */ public static function err( $msg = '' ) { // func_get_args is empty if no args are passed even with the default above. $args = array_merge( func_get_args(), array( '' ) ); $args[0] .= "\n"; fwrite( static::$err, self::_call( 'render', $args ) ); } /** * Takes input from `STDIN` in the given format. If an end of transmission * character is sent (^D), an exception is thrown. * * @param string $format A valid input format. See `fscanf` for documentation. * If none is given, all input up to the first newline * is accepted. * @param boolean $hide If true will hide what the user types in. * @return string The input with whitespace trimmed. * @throws \Exception Thrown if ctrl-D (EOT) is sent as input. */ public static function input( $format = null, $hide = false ) { if ( $hide ) Shell::hide(); if( $format ) { fscanf( static::$in, $format . "\n", $line ); } else { $line = fgets( static::$in ); } if ( $hide ) { Shell::hide( false ); echo "\n"; } if( $line === false ) { throw new \Exception( 'Caught ^D during input' ); } return trim( $line ); } /** * Displays an input prompt. If no default value is provided the prompt will * continue displaying until input is received. * * @param string $question The question to ask the user. * @param bool|string $default A default value if the user provides no input. * @param string $marker A string to append to the question and default value * on display. * @param boolean $hide Optionally hides what the user types in. * @return string The users input. * @see cli\input() */ public static function prompt( $question, $default = null, $marker = ': ', $hide = false ) { if( $default && strpos( $question, '[' ) === false ) { $question .= ' [' . $default . ']'; } while( true ) { self::out( $question . $marker ); $line = self::input( null, $hide ); if ( trim( $line ) !== '' ) return $line; if( $default !== false ) return $default; } } /** * Presents a user with a multiple choice question, useful for 'yes/no' type * questions (which this public static function defaults too). * * @param string $question The question to ask the user. * @param string $choice A string of characters allowed as a response. Case is ignored. * @param string $default The default choice. NULL if a default is not allowed. * @return string The users choice. * @see cli\prompt() */ public static function choose( $question, $choice = 'yn', $default = 'n' ) { if( !is_string( $choice ) ) { $choice = join( '', $choice ); } // Make every choice character lowercase except the default $choice = str_ireplace( $default, strtoupper( $default ), strtolower( $choice ) ); // Seperate each choice with a forward-slash $choices = trim( join( '/', preg_split( '//', $choice ) ), '/' ); while( true ) { $line = self::prompt( sprintf( '%s? [%s]', $question, $choices ), $default, '' ); if( stripos( $choice, $line ) !== false ) { return strtolower( $line ); } if( !empty( $default ) ) { return strtolower( $default ); } } } /** * Displays an array of strings as a menu where a user can enter a number to * choose an option. The array must be a single dimension with either strings * or objects with a `__toString()` method. * * @param array $items The list of items the user can choose from. * @param string $default The index of the default item. * @param string $title The message displayed to the user when prompted. * @return string The index of the chosen item. * @see cli\line() * @see cli\input() * @see cli\err() */ public static function menu( $items, $default = null, $title = 'Choose an item' ) { $map = array_values( $items ); if( $default && strpos( $title, '[' ) === false && isset( $items[$default] ) ) { $title .= ' [' . $items[$default] . ']'; } foreach( $map as $idx => $item ) { self::line( ' %d. %s', $idx + 1, (string)$item ); } self::line(); while( true ) { fwrite( static::$out, sprintf( '%s: ', $title ) ); $line = self::input(); if( is_numeric( $line ) ) { $line--; if( isset( $map[$line] ) ) { return array_search( $map[$line], $items ); } if( $line < 0 || $line >= count( $map ) ) { self::err( 'Invalid menu selection: out of range' ); } } else if( isset( $default ) ) { return $default; } } } /** * Sets one of the streams (input, output, or error) to a `stream` type resource. * * Valid $whichStream values are: * - 'in' (default: STDIN) * - 'out' (default: STDOUT) * - 'err' (default: STDERR) * * Any custom streams will be closed for you on shutdown, so please don't close stream * resources used with this method. * * @param string $whichStream The stream property to update * @param resource $stream The new stream resource to use * @return void * @throws \Exception Thrown if $stream is not a resource of the 'stream' type. */ public static function setStream( $whichStream, $stream ) { if( !is_resource( $stream ) || get_resource_type( $stream ) !== 'stream' ) { throw new \Exception( 'Invalid resource type!' ); } if( property_exists( __CLASS__, $whichStream ) ) { static::${$whichStream} = $stream; } register_shutdown_function( function() use ($stream) { fclose( $stream ); } ); } } <?php /** * PHP Command Line Tools * * This source file is subject to the MIT license that is bundled * with this package in the file LICENSE. * * @author James Logsdon <dwarf@girsbrain.org> * @copyright 2010 James Logsdom (http://girsbrain.org) * @license http://www.opensource.org/licenses/mit-license.php The MIT License */ namespace cli\arguments; /** * Thrown when undefined arguments are detected in strict mode. */ class InvalidArguments extends \InvalidArgumentException { protected $arguments; /** * @param array $arguments A list of arguments that do not fit the profile. */ public function __construct(array $arguments) { $this->arguments = $arguments; $this->message = $this->_generateMessage(); } /** * Get the arguments that caused the exception. * * @return array */ public function getArguments() { return $this->arguments; } private function _generateMessage() { return 'unknown argument' . (count($this->arguments) > 1 ? 's' : '') . ': ' . join(', ', $this->arguments); } } <?php /** * PHP Command Line Tools * * This source file is subject to the MIT license that is bundled * with this package in the file LICENSE. * * @author James Logsdon <dwarf@girsbrain.org> * @copyright 2010 James Logsdom (http://girsbrain.org) * @license http://www.opensource.org/licenses/mit-license.php The MIT License */ namespace cli\arguments; use cli\Memoize; /** * Represents an Argument or a value and provides several helpers related to parsing an argument list. */ class Argument extends Memoize { /** * The canonical name of this argument, used for aliasing. * * @param string */ public $key; private $_argument; private $_raw; /** * @param string $argument The raw argument, leading dashes included. */ public function __construct($argument) { $this->_raw = $argument; $this->key =& $this->_argument; if ($this->isLong) { $this->_argument = substr($this->_raw, 2); } else if ($this->isShort) { $this->_argument = substr($this->_raw, 1); } else { $this->_argument = $this->_raw; } } /** * Returns the raw input as a string. * * @return string */ public function __toString() { return (string)$this->_raw; } /** * Returns the formatted argument string. * * @return string */ public function value() { return $this->_argument; } /** * Returns the raw input. * * @return mixed */ public function raw() { return $this->_raw; } /** * Returns true if the string matches the pattern for long arguments. * * @return bool */ public function isLong() { return (0 == strncmp($this->_raw, '--', 2)); } /** * Returns true if the string matches the pattern for short arguments. * * @return bool */ public function isShort() { return !$this->isLong && (0 == strncmp($this->_raw, '-', 1)); } /** * Returns true if the string matches the pattern for arguments. * * @return bool */ public function isArgument() { return $this->isShort() || $this->isLong(); } /** * Returns true if the string matches the pattern for values. * * @return bool */ public function isValue() { return !$this->isArgument; } /** * Returns true if the argument is short but contains several characters. Each * character is considered a separate argument. * * @return bool */ public function canExplode() { return $this->isShort && strlen($this->_argument) > 1; } /** * Returns all but the first character of the argument, removing them from the * objects representation at the same time. * * @return array */ public function exploded() { $exploded = array(); for ($i = strlen($this->_argument); $i > 0; $i--) { array_push($exploded, $this->_argument[$i - 1]); } $this->_argument = array_pop($exploded); $this->_raw = '-' . $this->_argument; return $exploded; } } <?php /** * PHP Command Line Tools * * This source file is subject to the MIT license that is bundled * with this package in the file LICENSE. * * @author James Logsdon <dwarf@girsbrain.org> * @copyright 2010 James Logsdom (http://girsbrain.org) * @license http://www.opensource.org/licenses/mit-license.php The MIT License */ namespace cli\arguments; use cli\Memoize; class Lexer extends Memoize implements \Iterator { private $_items = array(); private $_index = 0; private $_length = 0; private $_first = true; /** * @param array $items A list of strings to process as tokens. */ public function __construct(array $items) { $this->_items = $items; $this->_length = count($items); } /** * The current token. * * @return string */ public function current() { return $this->_item; } /** * Peek ahead to the next token without moving the cursor. * * @return Argument */ public function peek() { return new Argument($this->_items[0]); } /** * Move the cursor forward 1 element if it is valid. */ public function next() { if ($this->valid()) { $this->_shift(); } } /** * Return the current position of the cursor. * * @return int */ public function key() { return $this->_index; } /** * Move forward 1 element and, if the method hasn't been called before, reset * the cursor's position to 0. */ public function rewind() { $this->_shift(); if ($this->_first) { $this->_index = 0; $this->_first = false; } } /** * Returns true if the cursor has not reached the end of the list. * * @return bool */ public function valid() { return ($this->_index < $this->_length); } /** * Push an element to the front of the stack. * * @param mixed $item The value to set */ public function unshift($item) { array_unshift($this->_items, $item); $this->_length += 1; } /** * Returns true if the cursor is at the end of the list. * * @return bool */ public function end() { return ($this->_index + 1) == $this->_length; } private function _shift() { $this->_item = new Argument(array_shift($this->_items)); $this->_index += 1; $this->_explode(); $this->_unmemo('peek'); } private function _explode() { if (!$this->_item->canExplode) { return false; } foreach ($this->_item->exploded as $piece) { $this->unshift('-' . $piece); } } } <?php /** * PHP Command Line Tools * * This source file is subject to the MIT license that is bundled * with this package in the file LICENSE. * * @author James Logsdon <dwarf@girsbrain.org> * @copyright 2010 James Logsdom (http://girsbrain.org) * @license http://www.opensource.org/licenses/mit-license.php The MIT License */ namespace cli\arguments; use cli\Arguments; /** * Arguments help screen renderer */ class HelpScreen { protected $_flags = array(); protected $_maxFlag = 0; protected $_options = array(); protected $_maxOption = 0; public function __construct(Arguments $arguments) { $this->setArguments($arguments); } public function __toString() { return $this->render(); } public function setArguments(Arguments $arguments) { $this->consumeArgumentFlags($arguments); $this->consumeArgumentOptions($arguments); } public function consumeArgumentFlags(Arguments $arguments) { $data = $this->_consume($arguments->getFlags()); $this->_flags = $data[0]; $this->_flagMax = $data[1]; } public function consumeArgumentOptions(Arguments $arguments) { $data = $this->_consume($arguments->getOptions()); $this->_options = $data[0]; $this->_optionMax = $data[1]; } public function render() { $help = array(); array_push($help, $this->_renderFlags()); array_push($help, $this->_renderOptions()); return join("\n\n", $help); } private function _renderFlags() { if (empty($this->_flags)) { return null; } return "Flags\n" . $this->_renderScreen($this->_flags, $this->_flagMax); } private function _renderOptions() { if (empty($this->_options)) { return null; } return "Options\n" . $this->_renderScreen($this->_options, $this->_optionMax); } private function _renderScreen($options, $max) { $help = array(); foreach ($options as $option => $settings) { $formatted = ' ' . str_pad($option, $max); $dlen = 80 - 4 - $max; $description = str_split($settings['description'], $dlen); $formatted.= ' ' . array_shift($description); if ($settings['default']) { $formatted .= ' [default: ' . $settings['default'] . ']'; } $pad = str_repeat(' ', $max + 3); while ($desc = array_shift($description)) { $formatted .= "\n${pad}${desc}"; } array_push($help, $formatted); } return join("\n", $help); } private function _consume($options) { $max = 0; $out = array(); foreach ($options as $option => $settings) { $names = array('--' . $option); foreach ($settings['aliases'] as $alias) { array_push($names, '-' . $alias); } $names = join(', ', $names); $max = max(strlen($names), $max); $out[$names] = $settings; } return array($out, $max); } } <?php /** * PHP Command Line Tools * * This source file is subject to the MIT license that is bundled * with this package in the file LICENSE. * * @author James Logsdon <dwarf@girsbrain.org> * @copyright 2010 James Logsdom (http://girsbrain.org) * @license http://www.opensource.org/licenses/mit-license.php The MIT License */ namespace cli; abstract class Memoize { protected $_memoCache = array(); public function __get($name) { if (isset($this->_memoCache[$name])) { return $this->_memoCache[$name]; } // Hide probable private methods if (0 == strncmp($name, '_', 1)) { return ($this->_memoCache[$name] = null); } if (!method_exists($this, $name)) { return ($this->_memoCache[$name] = null); } $method = array($this, $name); ($this->_memoCache[$name] = call_user_func($method)); return $this->_memoCache[$name]; } protected function _unmemo($name) { if ($name === true) { $this->_memoCache = array(); } else { unset($this->_memoCache[$name]); } } } <?php /** * PHP Command Line Tools * * This source file is subject to the MIT license that is bundled * with this package in the file LICENSE. * * @author Ryan Sullivan <rsullivan@connectstudios.com> * @copyright 2010 James Logsdom (http://girsbrain.org) * @license http://www.opensource.org/licenses/mit-license.php The MIT License */ namespace cli\tree; /** * Tree renderers are used to change how a tree is displayed. */ abstract class Renderer { /** * @param array $tree * @return string|null */ abstract public function render(array $tree); } <?php /** * PHP Command Line Tools * * This source file is subject to the MIT license that is bundled * with this package in the file LICENSE. * * @author Ryan Sullivan <rsullivan@connectstudios.com> * @copyright 2010 James Logsdom (http://girsbrain.org) * @license http://www.opensource.org/licenses/mit-license.php The MIT License */ namespace cli\tree; /** * The ASCII renderer renders trees with ASCII lines. */ class Ascii extends Renderer { /** * @param array $tree * @return string */ public function render(array $tree) { $output = ''; $treeIterator = new \RecursiveTreeIterator( new \RecursiveArrayIterator($tree), \RecursiveTreeIterator::SELF_FIRST ); foreach ($treeIterator as $val) { $output .= $val . "\n"; } return $output; } } <?php /** * PHP Command Line Tools * * This source file is subject to the MIT license that is bundled * with this package in the file LICENSE. * * @author Ryan Sullivan <rsullivan@connectstudios.com> * @copyright 2010 James Logsdom (http://girsbrain.org) * @license http://www.opensource.org/licenses/mit-license.php The MIT License */ namespace cli\tree; /** * The ASCII renderer renders trees with ASCII lines. */ class Markdown extends Renderer { /** * How many spaces to indent by * @var int */ protected $_padding = 2; /** * @param int $padding Optional. Default 2. */ function __construct($padding = null) { if ($padding) { $this->_padding = $padding; } } /** * Renders the tree * * @param array $tree * @param int $level Optional * @return string */ public function render(array $tree, $level = 0) { $output = ''; foreach ($tree as $label => $next) { if (is_string($next)) { $label = $next; } // Output the label $output .= sprintf("%s- %s\n", str_repeat(' ', $level * $this->_padding), $label); // Next level if (is_array($next)) { $output .= $this->render($next, $level + 1); } } return $output; } } <?php /** * PHP Command Line Tools * * This source file is subject to the MIT license that is bundled * with this package in the file LICENSE. * * @author James Logsdon <dwarf@girsbrain.org> * @copyright 2010 James Logsdom (http://girsbrain.org) * @license http://www.opensource.org/licenses/mit-license.php The MIT License */ namespace cli; /** * Handles rendering strings. If extra scalar arguments are given after the `$msg` * the string will be rendered with `sprintf`. If the second argument is an `array` * then each key in the array will be the placeholder name. Placeholders are of the * format {:key}. * * @param string $msg The message to render. * @param mixed ... Either scalar arguments or a single array argument. * @return string The rendered string. */ function render( $msg ) { return Streams::_call( 'render', func_get_args() ); } /** * Shortcut for printing to `STDOUT`. The message and parameters are passed * through `sprintf` before output. * * @param string $msg The message to output in `printf` format. * @param mixed ... Either scalar arguments or a single array argument. * @return void * @see \cli\render() */ function out( $msg ) { Streams::_call( 'out', func_get_args() ); } /** * Pads `$msg` to the width of the shell before passing to `cli\out`. * * @param string $msg The message to pad and pass on. * @param mixed ... Either scalar arguments or a single array argument. * @return void * @see cli\out() */ function out_padded( $msg ) { Streams::_call( 'out_padded', func_get_args() ); } /** * Prints a message to `STDOUT` with a newline appended. See `\cli\out` for * more documentation. * * @see cli\out() */ function line( $msg = '' ) { Streams::_call( 'line', func_get_args() ); } /** * Shortcut for printing to `STDERR`. The message and parameters are passed * through `sprintf` before output. * * @param string $msg The message to output in `printf` format. With no string, * a newline is printed. * @param mixed ... Either scalar arguments or a single array argument. * @return void */ function err( $msg = '' ) { Streams::_call( 'err', func_get_args() ); } /** * Takes input from `STDIN` in the given format. If an end of transmission * character is sent (^D), an exception is thrown. * * @param string $format A valid input format. See `fscanf` for documentation. * If none is given, all input up to the first newline * is accepted. * @return string The input with whitespace trimmed. * @throws \Exception Thrown if ctrl-D (EOT) is sent as input. */ function input( $format = null ) { return Streams::input( $format ); } /** * Displays an input prompt. If no default value is provided the prompt will * continue displaying until input is received. * * @param string $question The question to ask the user. * @param string $default A default value if the user provides no input. * @param string $marker A string to append to the question and default value on display. * @param boolean $hide If the user input should be hidden * @return string The users input. * @see cli\input() */ function prompt( $question, $default = false, $marker = ': ', $hide = false ) { return Streams::prompt( $question, $default, $marker, $hide ); } /** * Presents a user with a multiple choice question, useful for 'yes/no' type * questions (which this function defaults too). * * @param string $question The question to ask the user. * @param string $choice * @param string|null $default The default choice. NULL if a default is not allowed. * @internal param string $valid A string of characters allowed as a response. Case * is ignored. * @return string The users choice. * @see cli\prompt() */ function choose( $question, $choice = 'yn', $default = 'n' ) { return Streams::choose( $question, $choice, $default ); } /** * Does the same as {@see choose()}, but always asks yes/no and returns a boolean * * @param string $question The question to ask the user. * @param bool|null $default The default choice, in a boolean format. * @return bool */ function confirm( $question, $default = false ) { if ( is_bool( $default ) ) { $default = $default? 'y' : 'n'; } $result = choose( $question, 'yn', $default ); return $result == 'y'; } /** * Displays an array of strings as a menu where a user can enter a number to * choose an option. The array must be a single dimension with either strings * or objects with a `__toString()` method. * * @param array $items The list of items the user can choose from. * @param string $default The index of the default item. * @param string $title The message displayed to the user when prompted. * @return string The index of the chosen item. * @see cli\line() * @see cli\input() * @see cli\err() */ function menu( $items, $default = null, $title = 'Choose an item' ) { return Streams::menu( $items, $default, $title ); } /** * Attempts an encoding-safe way of getting string length. If intl extension or PCRE with '\X' or mb_string extension aren't * available, falls back to basic strlen. * * @param string $str The string to check. * @param string|bool $encoding Optional. The encoding of the string. Default false. * @return int Numeric value that represents the string's length */ function safe_strlen( $str, $encoding = false ) { // Allow for selective testings - "1" bit set tests grapheme_strlen(), "2" preg_match_all( '/\X/u' ), "4" mb_strlen(), "other" strlen(). $test_safe_strlen = getenv( 'PHP_CLI_TOOLS_TEST_SAFE_STRLEN' ); // Assume UTF-8 if no encoding given - `grapheme_strlen()` will return null if given non-UTF-8 string. if ( ( ! $encoding || 'UTF-8' === $encoding ) && can_use_icu() && null !== ( $length = grapheme_strlen( $str ) ) ) { if ( ! $test_safe_strlen || ( $test_safe_strlen & 1 ) ) { return $length; } } // Assume UTF-8 if no encoding given - `preg_match_all()` will return false if given non-UTF-8 string. if ( ( ! $encoding || 'UTF-8' === $encoding ) && can_use_pcre_x() && false !== ( $length = preg_match_all( '/\X/u', $str, $dummy /*needed for PHP 5.3*/ ) ) ) { if ( ! $test_safe_strlen || ( $test_safe_strlen & 2 ) ) { return $length; } } // Legacy encodings and old PHPs will reach here. if ( function_exists( 'mb_strlen' ) && ( $encoding || function_exists( 'mb_detect_encoding' ) ) ) { if ( ! $encoding ) { $encoding = mb_detect_encoding( $str, null, true /*strict*/ ); } $length = $encoding ? mb_strlen( $str, $encoding ) : mb_strlen( $str ); // mbstring funcs can fail if given `$encoding` arg that evals to false. if ( 'UTF-8' === $encoding ) { // Subtract combining characters. $length -= preg_match_all( get_unicode_regexs( 'm' ), $str, $dummy /*needed for PHP 5.3*/ ); } if ( ! $test_safe_strlen || ( $test_safe_strlen & 4 ) ) { return $length; } } return strlen( $str ); } /** * Attempts an encoding-safe way of getting a substring. If intl extension or PCRE with '\X' or mb_string extension aren't * available, falls back to substr(). * * @param string $str The input string. * @param int $start The starting position of the substring. * @param int|bool|null $length Optional, unless $is_width is set. Maximum length of the substring. Default false. Negative not supported. * @param int|bool $is_width Optional. If set and encoding is UTF-8, $length (which must be specified) is interpreted as spacing width. Default false. * @param string|bool $encoding Optional. The encoding of the string. Default false. * @return bool|string False if given unsupported args, otherwise substring of string specified by start and length parameters */ function safe_substr( $str, $start, $length = false, $is_width = false, $encoding = false ) { // Negative $length or $is_width and $length not specified not supported. if ( $length < 0 || ( $is_width && ( null === $length || false === $length ) ) ) { return false; } // Need this for normalization below and other uses. $safe_strlen = safe_strlen( $str, $encoding ); // Normalize `$length` when not specified - PHP 5.3 substr takes false as full length, PHP > 5.3 takes null. if ( null === $length || false === $length ) { $length = $safe_strlen; } // Normalize `$start` - various methods treat this differently. if ( $start > $safe_strlen ) { return ''; } if ( $start < 0 && -$start > $safe_strlen ) { $start = 0; } // Allow for selective testings - "1" bit set tests grapheme_substr(), "2" preg_split( '/\X/' ), "4" mb_substr(), "8" substr(). $test_safe_substr = getenv( 'PHP_CLI_TOOLS_TEST_SAFE_SUBSTR' ); // Assume UTF-8 if no encoding given - `grapheme_substr()` will return false (not null like `grapheme_strlen()`) if given non-UTF-8 string. if ( ( ! $encoding || 'UTF-8' === $encoding ) && can_use_icu() && false !== ( $try = grapheme_substr( $str, $start, $length ) ) ) { if ( ! $test_safe_substr || ( $test_safe_substr & 1 ) ) { return $is_width ? _safe_substr_eaw( $try, $length ) : $try; } } // Assume UTF-8 if no encoding given - `preg_split()` returns a one element array if given non-UTF-8 string (PHP bug) so need to check `preg_last_error()`. if ( ( ! $encoding || 'UTF-8' === $encoding ) && can_use_pcre_x() ) { if ( false !== ( $try = preg_split( '/(\X)/u', $str, $safe_strlen + 1, PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_NO_EMPTY ) ) && ! preg_last_error() ) { $try = implode( '', array_slice( $try, $start, $length ) ); if ( ! $test_safe_substr || ( $test_safe_substr & 2 ) ) { return $is_width ? _safe_substr_eaw( $try, $length ) : $try; } } } // Legacy encodings and old PHPs will reach here. if ( function_exists( 'mb_substr' ) && ( $encoding || function_exists( 'mb_detect_encoding' ) ) ) { if ( ! $encoding ) { $encoding = mb_detect_encoding( $str, null, true /*strict*/ ); } // Bug: not adjusting for combining chars. $try = $encoding ? mb_substr( $str, $start, $length, $encoding ) : mb_substr( $str, $start, $length ); // mbstring funcs can fail if given `$encoding` arg that evals to false. if ( 'UTF-8' === $encoding && $is_width ) { $try = _safe_substr_eaw( $try, $length ); } if ( ! $test_safe_substr || ( $test_safe_substr & 4 ) ) { return $try; } } return substr( $str, $start, $length ); } /** * Internal function used by `safe_substr()` to adjust for East Asian double-width chars. * * @return string */ function _safe_substr_eaw( $str, $length ) { // Set the East Asian Width regex. $eaw_regex = get_unicode_regexs( 'eaw' ); // If there's any East Asian double-width chars... if ( preg_match( $eaw_regex, $str ) ) { // Note that if the length ends in the middle of a double-width char, the char is excluded, not included. // See if it's all EAW. if ( function_exists( 'mb_substr' ) && preg_match_all( $eaw_regex, $str, $dummy /*needed for PHP 5.3*/ ) === $length ) { // Just halve the length so (rounded down to a minimum of 1). $str = mb_substr( $str, 0, max( (int) ( $length / 2 ), 1 ), 'UTF-8' ); } else { // Explode string into an array of UTF-8 chars. Based on core `_mb_substr()` in "wp-includes/compat.php". $chars = preg_split( '/([\x00-\x7f\xc2-\xf4][^\x00-\x7f\xc2-\xf4]*)/', $str, $length + 1, PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_NO_EMPTY ); $cnt = min( count( $chars ), $length ); $width = $length; for ( $length = 0; $length < $cnt && $width > 0; $length++ ) { $width -= preg_match( $eaw_regex, $chars[ $length ] ) ? 2 : 1; } // Round down to a minimum of 1. if ( $width < 0 && $length > 1 ) { $length--; } return join( '', array_slice( $chars, 0, $length ) ); } } return $str; } /** * An encoding-safe way of padding string length for display * * @param string $string The string to pad. * @param int $length The length to pad it to. * @param string|bool $encoding Optional. The encoding of the string. Default false. * @return string */ function safe_str_pad( $string, $length, $encoding = false ) { $real_length = strwidth( $string, $encoding ); $diff = strlen( $string ) - $real_length; $length += $diff; return str_pad( $string, $length ); } /** * Get width of string, ie length in characters, taking into account multi-byte and mark characters for UTF-8, and multi-byte for non-UTF-8. * * @param string $string The string to check. * @param string|bool $encoding Optional. The encoding of the string. Default false. * @return int The string's width. */ function strwidth( $string, $encoding = false ) { // Set the East Asian Width and Mark regexs. list( $eaw_regex, $m_regex ) = get_unicode_regexs(); // Allow for selective testings - "1" bit set tests grapheme_strlen(), "2" preg_match_all( '/\X/u' ), "4" mb_strwidth(), "other" safe_strlen(). $test_strwidth = getenv( 'PHP_CLI_TOOLS_TEST_STRWIDTH' ); // Assume UTF-8 if no encoding given - `grapheme_strlen()` will return null if given non-UTF-8 string. if ( ( ! $encoding || 'UTF-8' === $encoding ) && can_use_icu() && null !== ( $width = grapheme_strlen( $string ) ) ) { if ( ! $test_strwidth || ( $test_strwidth & 1 ) ) { return $width + preg_match_all( $eaw_regex, $string, $dummy /*needed for PHP 5.3*/ ); } } // Assume UTF-8 if no encoding given - `preg_match_all()` will return false if given non-UTF-8 string. if ( ( ! $encoding || 'UTF-8' === $encoding ) && can_use_pcre_x() && false !== ( $width = preg_match_all( '/\X/u', $string, $dummy /*needed for PHP 5.3*/ ) ) ) { if ( ! $test_strwidth || ( $test_strwidth & 2 ) ) { return $width + preg_match_all( $eaw_regex, $string, $dummy /*needed for PHP 5.3*/ ); } } // Legacy encodings and old PHPs will reach here. if ( function_exists( 'mb_strwidth' ) && ( $encoding || function_exists( 'mb_detect_encoding' ) ) ) { if ( ! $encoding ) { $encoding = mb_detect_encoding( $string, null, true /*strict*/ ); } $width = $encoding ? mb_strwidth( $string, $encoding ) : mb_strwidth( $string ); // mbstring funcs can fail if given `$encoding` arg that evals to false. if ( 'UTF-8' === $encoding ) { // Subtract combining characters. $width -= preg_match_all( $m_regex, $string, $dummy /*needed for PHP 5.3*/ ); } if ( ! $test_strwidth || ( $test_strwidth & 4 ) ) { return $width; } } return safe_strlen( $string, $encoding ); } /** * Returns whether ICU is modern enough not to flake out. * * @return bool */ function can_use_icu() { static $can_use_icu = null; if ( null === $can_use_icu ) { // Choosing ICU 54, Unicode 7.0. $can_use_icu = defined( 'INTL_ICU_VERSION' ) && version_compare( INTL_ICU_VERSION, '54.1', '>=' ) && function_exists( 'grapheme_strlen' ) && function_exists( 'grapheme_substr' ); } return $can_use_icu; } /** * Returns whether PCRE Unicode extended grapheme cluster '\X' is available for use. * * @return bool */ function can_use_pcre_x() { static $can_use_pcre_x = null; if ( null === $can_use_pcre_x ) { // '\X' introduced (as Unicde extended grapheme cluster) in PCRE 8.32 - see https://vcs.pcre.org/pcre/code/tags/pcre-8.32/ChangeLog?view=markup line 53. // Older versions of PCRE were bundled with PHP <= 5.3.23 & <= 5.4.13. $pcre_version = substr( PCRE_VERSION, 0, strspn( PCRE_VERSION, '0123456789.' ) ); // Remove any trailing date stuff. $can_use_pcre_x = version_compare( $pcre_version, '8.32', '>=' ) && false !== @preg_match( '/\X/u', '' ); } return $can_use_pcre_x; } /** * Get the regexs generated from Unicode data. * * @param string $idx Optional. Return a specific regex only. Default null. * @return array|string Returns keyed array if not given $idx or $idx doesn't exist, otherwise the specific regex string. */ function get_unicode_regexs( $idx = null ) { static $eaw_regex; // East Asian Width regex. Characters that count as 2 characters as they're "wide" or "fullwidth". See http://www.unicode.org/reports/tr11/tr11-19.html static $m_regex; // Mark characters regex (Unicode property "M") - mark combining "Mc", mark enclosing "Me" and mark non-spacing "Mn" chars that should be ignored for spacing purposes. if ( null === $eaw_regex ) { // Load both regexs generated from Unicode data. require __DIR__ . '/unicode/regex.php'; } if ( null !== $idx ) { if ( 'eaw' === $idx ) { return $eaw_regex; } if ( 'm' === $idx ) { return $m_regex; } } return array( $eaw_regex, $m_regex, ); } <?php /** * PHP Command Line Tools * * This source file is subject to the MIT license that is bundled * with this package in the file LICENSE. * * @author James Logsdon <dwarf@girsbrain.org> * @copyright 2010 James Logsdom (http://girsbrain.org) * @license http://www.opensource.org/licenses/mit-license.php The MIT License */ namespace cli\table; /** * Table renderers are used to change how a table is displayed. */ abstract class Renderer { protected $_widths = array(); public function __construct(array $widths = array()) { $this->setWidths($widths); } /** * Set the widths of each column in the table. * * @param array $widths The widths of the columns. * @param bool $fallback Whether to use these values as fallback only. */ public function setWidths(array $widths, $fallback = false) { if ($fallback) { foreach ( $this->_widths as $index => $value ) { $widths[$index] = $value; } } $this->_widths = $widths; } /** * Render a border for the top and bottom and separating the headers from the * table rows. * * @return string The table border. */ public function border() { return null; } /** * Renders a row for output. * * @param array $row The table row. * @return string The formatted table row. */ abstract public function row(array $row); } <?php /** * PHP Command Line Tools * * This source file is subject to the MIT license that is bundled * with this package in the file LICENSE. * * @author James Logsdon <dwarf@girsbrain.org> * @copyright 2010 James Logsdom (http://girsbrain.org) * @license http://www.opensource.org/licenses/mit-license.php The MIT License */ namespace cli\table; /** * The tabular renderer is used for displaying data in a tabular format. */ class Tabular extends Renderer { /** * Renders a row for output. * * @param array $row The table row. * @return string The formatted table row. */ public function row(array $row) { return implode("\t", array_values($row)); } } <?php /** * PHP Command Line Tools * * This source file is subject to the MIT license that is bundled * with this package in the file LICENSE. * * @author James Logsdon <dwarf@girsbrain.org> * @copyright 2010 James Logsdom (http://girsbrain.org) * @license http://www.opensource.org/licenses/mit-license.php The MIT License */ namespace cli\table; use cli\Colors; use cli\Shell; /** * The ASCII renderer renders tables with ASCII borders. */ class Ascii extends Renderer { protected $_characters = array( 'corner' => '+', 'line' => '-', 'border' => '|', 'padding' => ' ', ); protected $_border = null; protected $_constraintWidth = null; protected $_pre_colorized = false; /** * Set the widths of each column in the table. * * @param array $widths The widths of the columns. * @param bool $fallback Whether to use these values as fallback only. */ public function setWidths(array $widths, $fallback = false) { if ($fallback) { foreach ( $this->_widths as $index => $value ) { $widths[$index] = $value; } } $this->_widths = $widths; if ( is_null( $this->_constraintWidth ) ) { $this->_constraintWidth = (int) Shell::columns(); } $col_count = count( $widths ); $col_borders_count = $col_count ? ( ( $col_count - 1 ) * strlen( $this->_characters['border'] ) ) : 0; $table_borders_count = strlen( $this->_characters['border'] ) * 2; $col_padding_count = $col_count * strlen( $this->_characters['padding'] ) * 2; $max_width = $this->_constraintWidth - $col_borders_count - $table_borders_count - $col_padding_count; if ( $widths && $max_width && array_sum( $widths ) > $max_width ) { $avg = floor( $max_width / count( $widths ) ); $resize_widths = array(); $extra_width = 0; foreach( $widths as $width ) { if ( $width > $avg ) { $resize_widths[] = $width; } else { $extra_width = $extra_width + ( $avg - $width ); } } if ( ! empty( $resize_widths ) && $extra_width ) { $avg_extra_width = floor( $extra_width / count( $resize_widths ) ); foreach( $widths as &$width ) { if ( in_array( $width, $resize_widths ) ) { $width = $avg + $avg_extra_width; array_shift( $resize_widths ); // Last item gets the cake if ( empty( $resize_widths ) ) { $width = 0; // Zero it so not in sum. $width = $max_width - array_sum( $widths ); } } } } } $this->_widths = $widths; } /** * Set the constraint width for the table * * @param int $constraintWidth */ public function setConstraintWidth( $constraintWidth ) { $this->_constraintWidth = $constraintWidth; } /** * Set the characters used for rendering the Ascii table. * * The keys `corner`, `line` and `border` are used in rendering. * * @param $characters array Characters used in rendering. */ public function setCharacters(array $characters) { $this->_characters = array_merge($this->_characters, $characters); } /** * Render a border for the top and bottom and separating the headers from the * table rows. * * @return string The table border. */ public function border() { if (!isset($this->_border)) { $this->_border = $this->_characters['corner']; foreach ($this->_widths as $width) { $this->_border .= str_repeat($this->_characters['line'], $width + 2); $this->_border .= $this->_characters['corner']; } } return $this->_border; } /** * Renders a row for output. * * @param array $row The table row. * @return string The formatted table row. */ public function row( array $row ) { $extra_row_count = 0; if ( count( $row ) > 0 ) { $extra_rows = array_fill( 0, count( $row ), array() ); foreach( $row as $col => $value ) { $value = str_replace( array( "\r\n", "\n" ), ' ', $value ); $col_width = $this->_widths[ $col ]; $encoding = function_exists( 'mb_detect_encoding' ) ? mb_detect_encoding( $value, null, true /*strict*/ ) : false; $original_val_width = Colors::width( $value, self::isPreColorized( $col ), $encoding ); if ( $col_width && $original_val_width > $col_width ) { $row[ $col ] = \cli\safe_substr( $value, 0, $col_width, true /*is_width*/, $encoding ); $value = \cli\safe_substr( $value, \cli\safe_strlen( $row[ $col ], $encoding ), null /*length*/, false /*is_width*/, $encoding ); $i = 0; do { $extra_value = \cli\safe_substr( $value, 0, $col_width, true /*is_width*/, $encoding ); $val_width = Colors::width( $extra_value, self::isPreColorized( $col ), $encoding ); if ( $val_width ) { $extra_rows[ $col ][] = $extra_value; $value = \cli\safe_substr( $value, \cli\safe_strlen( $extra_value, $encoding ), null /*length*/, false /*is_width*/, $encoding ); $i++; if ( $i > $extra_row_count ) { $extra_row_count = $i; } } } while( $value ); } } } $row = array_map(array($this, 'padColumn'), $row, array_keys($row)); array_unshift($row, ''); // First border array_push($row, ''); // Last border $ret = join($this->_characters['border'], $row); if ( $extra_row_count ) { foreach( $extra_rows as $col => $col_values ) { while( count( $col_values ) < $extra_row_count ) { $col_values[] = ''; } } do { $row_values = array(); $has_more = false; foreach( $extra_rows as $col => &$col_values ) { $row_values[ $col ] = array_shift( $col_values ); if ( count( $col_values ) ) { $has_more = true; } } $row_values = array_map(array($this, 'padColumn'), $row_values, array_keys($row_values)); array_unshift($row_values, ''); // First border array_push($row_values, ''); // Last border $ret .= PHP_EOL . join($this->_characters['border'], $row_values); } while( $has_more ); } return $ret; } private function padColumn($content, $column) { return $this->_characters['padding'] . Colors::pad( $content, $this->_widths[ $column ], $this->isPreColorized( $column ) ) . $this->_characters['padding']; } /** * Set whether items are pre-colorized. * * @param bool|array $colorized A boolean to set all columns in the table as pre-colorized, or an array of booleans keyed by column index (number) to set individual columns as pre-colorized. */ public function setPreColorized( $pre_colorized ) { $this->_pre_colorized = $pre_colorized; } /** * Is a column pre-colorized? * * @param int $column Column index to check. * @return bool True if whole table is marked as pre-colorized, or if the individual column is pre-colorized; else false. */ public function isPreColorized( $column ) { if ( is_bool( $this->_pre_colorized ) ) { return $this->_pre_colorized; } if ( is_array( $this->_pre_colorized ) && isset( $this->_pre_colorized[ $column ] ) ) { return $this->_pre_colorized[ $column ]; } return false; } } <?php /** * PHP Command Line Tools * * This source file is subject to the MIT license that is bundled * with this package in the file LICENSE. * * @author Ryan Sullivan <rsullivan@connectstudios.com> * @copyright 2010 James Logsdom (http://girsbrain.org) * @license http://www.opensource.org/licenses/mit-license.php The MIT License */ namespace cli; /** * The `Tree` class is used to display data in a tree-like format. */ class Tree { protected $_renderer; protected $_data = array(); /** * Sets the renderer used by this tree. * * @param tree\Renderer $renderer The renderer to use for output. * @see tree\Renderer * @see tree\Ascii * @see tree\Markdown */ public function setRenderer(tree\Renderer $renderer) { $this->_renderer = $renderer; } /** * Set the data. * Format: * [ * 'Label' => [ * 'Thing' => ['Thing'], * ], * 'Thing', * ] * @param array $data */ public function setData(array $data) { $this->_data = $data; } /** * Render the tree and return it as a string. * * @return string|null */ public function render() { return $this->_renderer->render($this->_data); } /** * Display the rendered tree */ public function display() { echo $this->render(); } } <?php /** * PHP Command Line Tools * * This source file is subject to the MIT license that is bundled * with this package in the file LICENSE. * * @author James Logsdon <dwarf@girsbrain.org> * @copyright 2010 James Logsdom (http://girsbrain.org) * @license http://www.opensource.org/licenses/mit-license.php The MIT License */ namespace cli; use cli\Streams; /** * The `Notify` class is the basis of all feedback classes, such as Indicators * and Progress meters. The default behaviour is to refresh output after 100ms * have passed. This is done to preventing the screen from flickering and keep * slowdowns from output to a minimum. * * The most basic form of Notifier has no maxim, and simply displays a series * of characters to indicate progress is being made. */ abstract class Notify { protected $_current = 0; protected $_first = true; protected $_interval; protected $_message; protected $_start; protected $_timer; /** * Instatiates a Notification object. * * @param string $msg The text to display next to the Notifier. * @param int $interval The interval in milliseconds between updates. */ public function __construct($msg, $interval = 100) { $this->_message = $msg; $this->_interval = (int)$interval; } /** * This method should be used to print out the Notifier. This method is * called from `cli\Notify::tick()` after `cli\Notify::$_interval` has passed. * * @abstract * @param boolean $finish * @see cli\Notify::tick() */ abstract public function display($finish = false); /** * Reset the notifier state so the same instance can be used in multiple loops. */ public function reset() { $this->_current = 0; $this->_first = true; $this->_start = null; $this->_timer = null; } /** * Returns the formatted tick count. * * @return string The formatted tick count. */ public function current() { return number_format($this->_current); } /** * Calculates the time elapsed since the Notifier was first ticked. * * @return int The elapsed time in seconds. */ public function elapsed() { if (!$this->_start) { return 0; } $elapsed = time() - $this->_start; return $elapsed; } /** * Calculates the speed (number of ticks per second) at which the Notifier * is being updated. * * @return int The number of ticks performed in 1 second. */ public function speed() { static $tick, $iteration = 0, $speed = 0; if (!$this->_start) { return 0; } else if (!$tick) { $tick = $this->_start; } $now = microtime(true); $span = $now - $tick; if ($span > 1) { $iteration++; $tick = $now; $speed = ($this->_current / $iteration) / $span; } return $speed; } /** * Takes a time span given in seconds and formats it for display. The * returned string will be in MM:SS form. * * @param int $time The time span in seconds to format. * @return string The formatted time span. */ public function formatTime($time) { return floor($time / 60) . ':' . str_pad($time % 60, 2, 0, STR_PAD_LEFT); } /** * Finish our Notification display. Should be called after the Notifier is * no longer needed. * * @see cli\Notify::display() */ public function finish() { Streams::out("\r"); $this->display(true); Streams::line(); } /** * Increments are tick counter by the given amount. If no amount is provided, * the ticker is incremented by 1. * * @param int $increment The amount to increment by. */ public function increment($increment = 1) { $this->_current += $increment; } /** * Determines whether the display should be updated or not according to * our interval setting. * * @return boolean `true` if the display should be updated, `false` otherwise. */ public function shouldUpdate() { $now = microtime(true) * 1000; if (empty($this->_timer)) { $this->_start = (int)(($this->_timer = $now) / 1000); return true; } if (($now - $this->_timer) > $this->_interval) { $this->_timer = $now; return true; } return false; } /** * This method is the meat of all Notifiers. First we increment the ticker * and then update the display if enough time has passed since our last tick. * * @param int $increment The amount to increment by. * @see cli\Notify::increment() * @see cli\Notify::shouldUpdate() * @see cli\Notify::display() */ public function tick($increment = 1) { $this->increment($increment); if ($this->shouldUpdate()) { Streams::out("\r"); $this->display(); } } } <?php // Generated by "gen_east_asian_width.php" from "http://www.unicode.org/Public/10.0.0/ucd/EastAsianWidth.txt". $eaw_regex = '/\xe1(?:\x84[\x80-\xbf]|\x85[\x80-\x9f])|\xe2(?:\x8c[\x9a\x9b\xa9\xaa]|\x8f[\xa9-\xac\xb0\xb3]|\x97[\xbd\xbe]|\x98[\x94\x95]|\x99[\x88-\x93\xbf]|\x9a[\x93\xa1\xaa\xab\xbd\xbe]|\x9b[\x84\x85\x8e\x94\xaa\xb2\xb3\xb5\xba\xbd]|\x9c[\x85\x8a\x8b\xa8]|\x9d[\x8c\x8e\x93-\x95\x97]|\x9e[\x95-\x97\xb0\xbf]|\xac[\x9b\x9c]|\xad[\x90\x95]|\xba[\x80-\x99\x9b-\xbf]|\xbb[\x80-\xb3]|[\xbc-\xbe][\x80-\xbf]|\xbf[\x80-\x95\xb0-\xbb])|\xe3(?:\x80[\x80-\xbe]|\x81[\x81-\xbf]|\x82[\x80-\x96\x99-\xbf]|\x83[\x80-\xbf]|\x84[\x85-\xae\xb1-\xbf]|\x85[\x80-\xbf]|\x86[\x80-\x8e\x90-\xba]|\x87[\x80-\xa3\xb0-\xbf]|\x88[\x80-\x9e\xa0-\xbf]|\x89[\x80-\x87\x90-\xbf]|\x8a[\x80-\xbf]|\x8b[\x80-\xbe]|[\x8c-\xbf][\x80-\xbf])|\xe4(?:[\x80-\xb6][\x80-\xbf]|[\xb8-\xbf][\x80-\xbf])|[\xe5-\xe9][\x80-\xbf][\x80-\xbf]|\xea(?:[\x80-\x91][\x80-\xbf]|\x92[\x80-\x8c\x90-\xbf]|\x93[\x80-\x86]|\xa5[\xa0-\xbc]|[\xb0-\xbf][\x80-\xbf])|[\xeb\xec][\x80-\xbf][\x80-\xbf]|\xed(?:[\x80-\x9d][\x80-\xbf]|\x9e[\x80-\xa3])|\xef(?:[\xa4-\xab][\x80-\xbf]|\xb8[\x90-\x99\xb0-\xbf]|\xb9[\x80-\x92\x94-\xa6\xa8-\xab]|\xbc[\x81-\xbf]|\xbd[\x80-\xa0]|\xbf[\xa0-\xa6])|\xf0(?:\x96\xbf[\xa0\xa1]|\x97[\x80-\xbf][\x80-\xbf]|\x98(?:[\x80-\x9e][\x80-\xbf]|\x9f[\x80-\xac]|[\xa0-\xaa][\x80-\xbf]|\xab[\x80-\xb2])|\x9b(?:[\x80-\x83][\x80-\xbf]|\x84[\x80-\x9e]|\x85[\xb0-\xbf]|[\x86-\x8a][\x80-\xbf]|\x8b[\x80-\xbb])|\x9f(?:\x80\x84|\x83\x8f|\x86[\x8e\x91-\x9a]|\x88[\x80-\x82\x90-\xbb]|\x89[\x80-\x88\x90\x91\xa0-\xa5]|\x8c[\x80-\xa0\xad-\xb5\xb7-\xbf]|\x8d[\x80-\xbc\xbe\xbf]|\x8e[\x80-\x93\xa0-\xbf]|\x8f[\x80-\x8a\x8f-\x93\xa0-\xb0\xb4\xb8-\xbf]|\x90[\x80-\xbe]|\x91[\x80\x82-\xbf]|\x92[\x80-\xbf]|\x93[\x80-\xbc\xbf]|\x94[\x80-\xbd]|\x95[\x8b-\x8e\x90-\xa7\xba]|\x96[\x95\x96\xa4]|\x97[\xbb-\xbf]|\x98[\x80-\xbf]|\x99[\x80-\x8f]|\x9a[\x80-\xbf]|\x9b[\x80-\x85\x8c\x90-\x92\xab\xac\xb4-\xb8]|\xa4[\x90-\xbe]|\xa5[\x80-\x8c\x90-\xab]|\xa6[\x80-\x97]|\xa7[\x80\x90-\xa6])|[\xa0-\xae][\x80-\xbf][\x80-\xbf]|\xaf(?:[\x80-\xbe][\x80-\xbf]|\xbf[\x80-\xbd])|[\xb0-\xbe][\x80-\xbf][\x80-\xbf]|\xbf(?:[\x80-\xbe][\x80-\xbf]|\xbf[\x80-\xbd]))/'; // 181738 code points. // Generated by "gen_cat_regex_alts.php" from "http://www.unicode.org/Public/10.0.0/ucd/UnicodeData.txt". $m_regex = '/\xcc[\x80-\xbf]|\xcd[\x80-\xaf]|\xd2[\x83-\x89]|\xd6[\x91-\xbd\xbf]|\xd7[\x81\x82\x84\x85\x87]|\xd8[\x90-\x9a]|\xd9[\x8b-\x9f\xb0]|\xdb[\x96-\x9c\x9f-\xa4\xa7\xa8\xaa-\xad]|\xdc[\x91\xb0-\xbf]|\xdd[\x80-\x8a]|\xde[\xa6-\xb0]|\xdf[\xab-\xb3]|\xe0(?:\xa0[\x96-\x99\x9b-\xa3\xa5-\xa7\xa9-\xad]|\xa1[\x99-\x9b]|\xa3[\x94-\xa1\xa3-\xbf]|\xa4[\x80-\x83\xba-\xbc\xbe\xbf]|\xa5[\x80-\x8f\x91-\x97\xa2\xa3]|\xa6[\x81-\x83\xbc\xbe\xbf]|\xa7[\x80-\x84\x87\x88\x8b-\x8d\x97\xa2\xa3]|\xa8[\x81-\x83\xbc\xbe\xbf]|\xa9[\x80-\x82\x87\x88\x8b-\x8d\x91\xb0\xb1\xb5]|\xaa[\x81-\x83\xbc\xbe\xbf]|\xab[\x80-\x85\x87-\x89\x8b-\x8d\xa2\xa3\xba-\xbf]|\xac[\x81-\x83\xbc\xbe\xbf]|\xad[\x80-\x84\x87\x88\x8b-\x8d\x96\x97\xa2\xa3]|\xae[\x82\xbe\xbf]|\xaf[\x80-\x82\x86-\x88\x8a-\x8d\x97]|\xb0[\x80-\x83\xbe\xbf]|\xb1[\x80-\x84\x86-\x88\x8a-\x8d\x95\x96\xa2\xa3]|\xb2[\x81-\x83\xbc\xbe\xbf]|\xb3[\x80-\x84\x86-\x88\x8a-\x8d\x95\x96\xa2\xa3]|\xb4[\x80-\x83\xbb\xbc\xbe\xbf]|\xb5[\x80-\x84\x86-\x88\x8a-\x8d\x97\xa2\xa3]|\xb6[\x82\x83]|\xb7[\x8a\x8f-\x94\x96\x98-\x9f\xb2\xb3]|\xb8[\xb1\xb4-\xba]|\xb9[\x87-\x8e]|\xba[\xb1\xb4-\xb9\xbb\xbc]|\xbb[\x88-\x8d]|\xbc[\x98\x99\xb5\xb7\xb9\xbe\xbf]|\xbd[\xb1-\xbf]|\xbe[\x80-\x84\x86\x87\x8d-\x97\x99-\xbc]|\xbf\x86)|\xe1(?:\x80[\xab-\xbe]|\x81[\x96-\x99\x9e-\xa0\xa2-\xa4\xa7-\xad\xb1-\xb4]|\x82[\x82-\x8d\x8f\x9a-\x9d]|\x8d[\x9d-\x9f]|\x9c[\x92-\x94\xb2-\xb4]|\x9d[\x92\x93\xb2\xb3]|\x9e[\xb4-\xbf]|\x9f[\x80-\x93\x9d]|\xa0[\x8b-\x8d]|\xa2[\x85\x86\xa9]|\xa4[\xa0-\xab\xb0-\xbb]|\xa8[\x97-\x9b]|\xa9[\x95-\x9e\xa0-\xbc\xbf]|\xaa[\xb0-\xbe]|\xac[\x80-\x84\xb4-\xbf]|\xad[\x80-\x84\xab-\xb3]|\xae[\x80-\x82\xa1-\xad]|\xaf[\xa6-\xb3]|\xb0[\xa4-\xb7]|\xb3[\x90-\x92\x94-\xa8\xad\xb2-\xb4\xb7-\xb9]|\xb7[\x80-\xb9\xbb-\xbf])|\xe2(?:\x83[\x90-\xb0]|\xb3[\xaf-\xb1]|\xb5\xbf|\xb7[\xa0-\xbf])|\xe3(?:\x80[\xaa-\xaf]|\x82[\x99\x9a])|\xea(?:\x99[\xaf-\xb2\xb4-\xbd]|\x9a[\x9e\x9f]|\x9b[\xb0\xb1]|\xa0[\x82\x86\x8b\xa3-\xa7]|\xa2[\x80\x81\xb4-\xbf]|\xa3[\x80-\x85\xa0-\xb1]|\xa4[\xa6-\xad]|\xa5[\x87-\x93]|\xa6[\x80-\x83\xb3-\xbf]|\xa7[\x80\xa5]|\xa8[\xa9-\xb6]|\xa9[\x83\x8c\x8d\xbb-\xbd]|\xaa[\xb0\xb2-\xb4\xb7\xb8\xbe\xbf]|\xab[\x81\xab-\xaf\xb5\xb6]|\xaf[\xa3-\xaa\xac\xad])|\xef(?:\xac\x9e|\xb8[\x80-\x8f\xa0-\xaf])|\xf0(?:\x90(?:\x87\xbd|\x8b\xa0|\x8d[\xb6-\xba]|\xa8[\x81-\x83\x85\x86\x8c-\x8f\xb8-\xba\xbf]|\xab[\xa5\xa6])|\x91(?:\x80[\x80-\x82\xb8-\xbf]|\x81[\x80-\x86\xbf]|\x82[\x80-\x82\xb0-\xba]|\x84[\x80-\x82\xa7-\xb4]|\x85\xb3|\x86[\x80-\x82\xb3-\xbf]|\x87[\x80\x8a-\x8c]|\x88[\xac-\xb7\xbe]|\x8b[\x9f-\xaa]|\x8c[\x80-\x83\xbc\xbe\xbf]|\x8d[\x80-\x84\x87\x88\x8b-\x8d\x97\xa2\xa3\xa6-\xac\xb0-\xb4]|\x90[\xb5-\xbf]|\x91[\x80-\x86]|\x92[\xb0-\xbf]|\x93[\x80-\x83]|\x96[\xaf-\xb5\xb8-\xbf]|\x97[\x80\x9c\x9d]|\x98[\xb0-\xbf]|\x99\x80|\x9a[\xab-\xb7]|\x9c[\x9d-\xab]|\xa8[\x81-\x8a\xb3-\xb9\xbb-\xbe]|\xa9[\x87\x91-\x9b]|\xaa[\x8a-\x99]|\xb0[\xaf-\xb6\xb8-\xbf]|\xb2[\x92-\xa7\xa9-\xb6]|\xb4[\xb1-\xb6\xba\xbc\xbd\xbf]|\xb5[\x80-\x85\x87])|\x96(?:\xab[\xb0-\xb4]|\xac[\xb0-\xb6]|\xbd[\x91-\xbe]|\xbe[\x8f-\x92])|\x9b\xb2[\x9d\x9e]|\x9d(?:\x85[\xa5-\xa9\xad-\xb2\xbb-\xbf]|\x86[\x80-\x82\x85-\x8b\xaa-\xad]|\x89[\x82-\x84]|\xa8[\x80-\xb6\xbb-\xbf]|\xa9[\x80-\xac\xb5]|\xaa[\x84\x9b-\x9f\xa1-\xaf])|\x9e(?:\x80[\x80-\x86\x88-\x98\x9b-\xa1\xa3\xa4\xa6-\xaa]|\xa3[\x90-\x96]|\xa5[\x84-\x8a]))|\xf3\xa0(?:[\x84-\x86][\x80-\xbf]|\x87[\x80-\xaf])/'; // 2177 code points. <?php namespace WP_CLI\Embeds; use WP_CLI; use WP_CLI\Utils; use WP_CLI_Command; class Fetch_Command extends WP_CLI_Command { /** * Attempts to convert a URL into embed HTML. * * In non-raw mode, starts by checking the URL against the regex of the registered embed handlers. * If none of the regex matches and it's enabled, then the URL will be given to the WP_oEmbed class. * * In raw mode, checks the providers directly and returns the data. * * ## OPTIONS * * <url> * : URL to retrieve oEmbed data for. * * [--width=<width>] * : Width of the embed in pixels. * * [--height=<height>] * : Height of the embed in pixels. * * [--post-id=<id>] * : Cache oEmbed response for a given post. * * [--discover] * : Enable oEmbed discovery. Defaults to true. * * [--skip-cache] * : Ignore already cached oEmbed responses. Has no effect if using the 'raw' option, which doesn't use the cache. * * [--skip-sanitization] * : Remove the filter that WordPress from 4.4 onwards uses to sanitize oEmbed responses. Has no effect if using the 'raw' option, which by-passes sanitization. * * [--do-shortcode] * : If the URL is handled by a registered embed handler and returns a shortcode, do shortcode and return result. Has no effect if using the 'raw' option, which by-passes handlers. * * [--limit-response-size=<size>] * : Limit the size of the resulting HTML when using discovery. Default 150 KB (the standard WordPress limit). Not compatible with 'no-discover'. * * [--raw] * : Return the raw oEmbed response instead of the resulting HTML. Ignores the cache and does not sanitize responses or use registered embed handlers. * * [--raw-format=<json|xml>] * : Render raw oEmbed data in a particular format. Defaults to json. Can only be specified in conjunction with the 'raw' option. * --- * options: * - json * - xml * --- * * ## EXAMPLES * * # Get embed HTML for a given URL. * $ wp embed fetch https://www.youtube.com/watch?v=dQw4w9WgXcQ * <iframe width="525" height="295" src="https://www.youtube.com/embed/dQw4w9WgXcQ?feature=oembed" ... * * # Get raw oEmbed data for a given URL. * $ wp embed fetch https://www.youtube.com/watch?v=dQw4w9WgXcQ --raw * {"author_url":"https:\/\/www.youtube.com\/user\/RickAstleyVEVO","width":525,"version":"1.0", ... */ public function __invoke( $args, $assoc_args ) { /** @var \WP_Embed $wp_embed */ global $wp_embed; $url = $args[0]; $raw = Utils\get_flag_value( $assoc_args, 'raw' ); $raw_format = Utils\get_flag_value( $assoc_args, 'raw-format' ); $post_id = Utils\get_flag_value( $assoc_args, 'post-id' ); $discover = Utils\get_flag_value( $assoc_args, 'discover' ); $response_size_limit = Utils\get_flag_value( $assoc_args, 'limit-response-size' ); $width = Utils\get_flag_value( $assoc_args, 'width' ); $height = Utils\get_flag_value( $assoc_args, 'height' ); // The `$key_suffix` used for caching is part based on serializing the attributes array without normalizing it first so need to try to replicate that. $oembed_args = array(); if ( null !== $width ) { $oembed_args['width'] = $width; // Keep as string as if from a shortcode attribute. } if ( null !== $height ) { $oembed_args['height'] = $height; // Keep as string as if from a shortcode attribute. } if ( null !== $discover ) { $oembed_args['discover'] = $discover ? '1' : '0'; // Make it a string as if from a shortcode attribute. } $discover = null === $discover ? true : (bool) $discover; // Will use to set `$oembed_args['discover']` when not calling `WP_Embed::shortcode()`. if ( ! $discover && null !== $response_size_limit ) { WP_CLI::error( "The 'limit-response-size' option can only be used with discovery." ); } if ( ! $raw && null !== $raw_format ) { WP_CLI::error( "The 'raw-format' option can only be used with the 'raw' option." ); } if ( $response_size_limit ) { if ( Utils\wp_version_compare( '4.0', '<' ) ) { WP_CLI::warning( "The 'limit-response-size' option only works for WordPress 4.0 onwards." ); // Fall through anyway... } add_filter( 'oembed_remote_get_args', function ( $args ) use ( $response_size_limit ) { $args['limit_response_size'] = $response_size_limit; return $args; }, PHP_INT_MAX ); } // If raw, query providers directly, by-passing cache. if ( $raw ) { $oembed = new oEmbed(); // Needs to be here to make sure `_wp_oembed_get_object()` defined (in "wp-includes/class-oembed.php" for WP < 4.7). $oembed_args['discover'] = $discover; // Make 'oembed_dataparse' filter a no-op so get raw unsanitized data. remove_all_filters( 'oembed_dataparse' ); // Save a few cycles. add_filter( 'oembed_dataparse', function ( $return, $data, $url ) { return $data; }, PHP_INT_MAX, 3 ); // Allow `wp_filter_pre_oembed_result()` to provide local URLs (WP >= 4.5.3). // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- Using WP Core hook. $data = apply_filters( 'pre_oembed_result', null, $url, $oembed_args ); if ( null === $data ) { $provider = $oembed->get_provider( $url, $oembed_args ); if ( ! $provider ) { if ( ! $discover ) { WP_CLI::error( 'No oEmbed provider found for given URL. Maybe try discovery?' ); } else { WP_CLI::error( 'No oEmbed provider found for given URL.' ); } } $data = $oembed->fetch( $provider, $url, $oembed_args ); } if ( false === $data ) { WP_CLI::error( 'There was an error fetching the oEmbed data.' ); } if ( 'xml' === $raw_format ) { if ( ! class_exists( 'SimpleXMLElement' ) ) { WP_CLI::error( "The PHP extension 'SimpleXMLElement' is not available but is required for XML-formatted output." ); } WP_CLI::log( $this->oembed_create_xml( (array) $data ) ); } else { WP_CLI::log( json_encode( $data ) ); } return; } if ( $post_id ) { // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited -- the request is asking for a post id directly so need to override the global. $GLOBALS['post'] = get_post( $post_id ); if ( null === $GLOBALS['post'] ) { WP_CLI::warning( sprintf( "Post id '%s' not found.", $post_id ) ); } } if ( Utils\get_flag_value( $assoc_args, 'skip-sanitization' ) ) { remove_filter( 'oembed_dataparse', 'wp_filter_oembed_result', 10, 3 ); } if ( Utils\get_flag_value( $assoc_args, 'skip-cache' ) ) { $wp_embed->usecache = false; // In order to skip caching, also need `$cached_recently` to be false in `WP_Embed::shortcode()`, so set TTL to zero. add_filter( 'oembed_ttl', '__return_zero', PHP_INT_MAX ); } else { $wp_embed->usecache = true; } // `WP_Embed::shortcode()` sets the 'discover' attribute based on 'embed_oembed_discover' filter, no matter what's passed to it. add_filter( 'embed_oembed_discover', $discover ? '__return_true' : '__return_false', PHP_INT_MAX ); // For WP < 4.9, `WP_Embed::shortcode()` won't check providers if no post_id supplied, so set `maybe_make_link()` to return false so can check and do it ourselves. // Also set if WP < 4.4 and don't have 'unfiltered_html' privileges on post. $check_providers = Utils\wp_version_compare( '4.9', '<' ) && ( ! $post_id || ( Utils\wp_version_compare( '4.4', '<' ) && ! author_can( $post_id, 'unfiltered_html' ) ) ); if ( $check_providers ) { add_filter( 'embed_maybe_make_link', '__return_false', PHP_INT_MAX ); } $html = $wp_embed->shortcode( $oembed_args, $url ); if ( false === $html && $check_providers ) { // Check providers. $oembed_args['discover'] = $discover; $html = wp_oembed_get( $url, $oembed_args ); // `wp_oembed_get()` returns zero-length string instead of false on failure due to `_strip_newlines()` 'oembed_dataparse' filter so make sure false. if ( '' === $html ) { $html = false; } } if ( false !== $html && '[' === substr( $html, 0, 1 ) && Utils\get_flag_value( $assoc_args, 'do-shortcode' ) ) { $html = do_shortcode( $html, true ); } if ( false === $html ) { WP_CLI::error( 'There was an error fetching the oEmbed data.' ); } WP_CLI::log( $html ); } /** * Creates an XML string from a given array. * * Same as `\_oembed_create_xml()` in "wp-includes\embed.php" introduced in WP 4.4.0. Polyfilled as marked private (and also to cater for older WP versions). * * @see _oembed_create_xml() * * @param array $data The original oEmbed response data. * @param \SimpleXMLElement $node Optional. XML node to append the result to recursively. * @return string|false XML string on success, false on error. */ protected function oembed_create_xml( $data, $node = null ) { if ( ! is_array( $data ) || empty( $data ) ) { return false; } if ( null === $node ) { $node = new \SimpleXMLElement( '<oembed></oembed>' ); } foreach ( $data as $key => $value ) { if ( is_numeric( $key ) ) { $key = 'oembed'; } if ( is_array( $value ) ) { $item = $node->addChild( $key ); $this->oembed_create_xml( $value, $item ); } else { $node->addChild( $key, esc_html( $value ) ); } } return $node->asXML(); } } <?php namespace WP_CLI\Embeds; use WP_CLI; use WP_CLI\Process; use WP_CLI\Utils; use WP_CLI\Formatter; use WP_CLI_Command; /** * Finds, triggers, and deletes oEmbed caches. */ class Cache_Command extends WP_CLI_Command { /** * Deletes all oEmbed caches for a given post. * * oEmbed caches for a post are stored in the post's metadata. * * ## OPTIONS * * <post_id> * : ID of the post to clear the cache for. * * ## EXAMPLES * * # Clear cache for a post * $ wp embed cache clear 123 * Success: Cleared oEmbed cache. */ public function clear( $args, $assoc_args ) { /** @var \WP_Embed $wp_embed */ global $wp_embed; $post_id = $args[0]; $post_metas = get_post_custom_keys( $post_id ); if ( $post_metas ) { $post_metas = array_filter( $post_metas, function ( $v ) { return '_oembed_' === substr( $v, 0, 8 ); } ); } if ( empty( $post_metas ) ) { WP_CLI::error( 'No cache to clear!' ); } $wp_embed->delete_oembed_caches( $post_id ); WP_CLI::success( 'Cleared oEmbed cache.' ); } /** * Finds an oEmbed cache post ID for a given URL. * * Starting with WordPress 4.9, embeds that aren't associated with a specific post will be cached in * a new oembed_cache post type. There can be more than one such entry for a url depending on attributes and context. * * Not to be confused with oEmbed caches for a given post which are stored in the post's metadata. * * ## OPTIONS * * <url> * : URL to retrieve oEmbed data for. * * [--width=<width>] * : Width of the embed in pixels. Part of cache key so must match. Defaults to `content_width` if set else 500px, so is theme and context dependent. * * [--height=<height>] * : Height of the embed in pixels. Part of cache key so must match. Defaults to 1.5 * default width (`content_width` or 500px), to a maximum of 1000px. * * [--discover] * : Whether to search with the discover attribute set or not. Part of cache key so must match. If not given, will search with attribute: unset, '1', '0', returning first. * * ## EXAMPLES * * # Find cache post ID for a given URL. * $ wp embed cache find https://www.youtube.com/watch?v=dQw4w9WgXcQ --width=500 * 123 */ public function find( $args, $assoc_args ) { if ( Utils\wp_version_compare( '4.9', '<' ) ) { WP_CLI::error( 'Requires WordPress 4.9 or greater.' ); } /** @var \WP_Embed $wp_embed */ global $wp_embed; $url = $args[0]; $width = Utils\get_flag_value( $assoc_args, 'width' ); $height = Utils\get_flag_value( $assoc_args, 'height' ); $discover = Utils\get_flag_value( $assoc_args, 'discover' ); // The `$key_suffix` used for caching is part based on serializing the attributes array without normalizing it first so need to try to replicate that. $oembed_args = array(); if ( null !== $width ) { $oembed_args['width'] = $width; // Keep as string as if from a shortcode attribute. } if ( null !== $height ) { $oembed_args['height'] = $height; // Keep as string as if from a shortcode attribute. } if ( null !== $discover ) { $discovers = array( ( $discover ) ? '1' : '0' ); } else { $discovers = array( null, '1', '0' ); } $attr = wp_parse_args( $oembed_args, wp_embed_defaults( $url ) ); foreach ( $discovers as $discover ) { if ( null !== $discover ) { $attr['discover'] = $discover; } // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.serialize_serialize -- needed to mimic WP Core behavior. See: \WP_Embed::shortcode. $key_suffix = md5( $url . serialize( $attr ) ); $cached_post_id = $wp_embed->find_oembed_post_id( $key_suffix ); if ( $cached_post_id ) { WP_CLI::line( $cached_post_id ); return; } } WP_CLI::error( 'No cache post ID found!' ); } /** * Triggers the caching of all oEmbed results for a given post. * * oEmbed caches for a post are stored in the post's metadata. * * ## OPTIONS * * <post_id> * : ID of the post to do the caching for. * * ## EXAMPLES * * # Triggers cache for a post * $ wp embed cache trigger 456 * Success: Caching triggered! */ public function trigger( $args, $assoc_args ) { /** @var \WP_Embed $wp_embed */ global $wp_embed; $post_id = $args[0]; $post = get_post( $post_id ); $post_types = get_post_types( array( 'show_ui' => true ) ); if ( empty( $post->ID ) ) { WP_CLI::warning( sprintf( "Post id '%s' not found.", $post_id ) ); return; } /** This filter is documented in wp-includes/class-wp-embed.php */ // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- Using WP Core hook. if ( ! in_array( $post->post_type, apply_filters( 'embed_cache_oembed_types', $post_types ), true ) ) { WP_CLI::warning( sprintf( "Cannot cache oEmbed results for '%s' post type", $post->post_type ) ); return; } $wp_embed->cache_oembed( $post_id ); WP_CLI::success( 'Caching triggered!' ); } } <?php namespace WP_CLI\Embeds; use WP_CLI\Dispatcher\CommandNamespace; /** * Inspects oEmbed providers, clears embed cache, and more. * * @package wp-cli */ class Embeds_Namespace extends CommandNamespace { } <?php namespace WP_CLI\Embeds; use WP_CLI; use WP_CLI\Formatter; use WP_CLI_Command; /** * Retrieves embed handlers. */ class Handler_Command extends WP_CLI_Command { protected $default_fields = array( 'id', 'regex', ); /** * Lists all available embed handlers. * * ## OPTIONS * * [--field=<field>] * : Display the value of a single field * * [--fields=<fields>] * : Limit the output to specific fields. * * [--format=<format>] * : Render output in a particular format. * --- * default: table * options: * - table * - csv * - json * --- * * ## AVAILABLE FIELDS * * These fields will be displayed by default for each handler: * * * id * * regex * * These fields are optionally available: * * * callback * * priority * * ## EXAMPLES * * # List id,regex,priority fields of available handlers. * $ wp embed handler list --fields=priority,id * +----------+-------------------+ * | priority | id | * +----------+-------------------+ * | 10 | youtube_embed_url | * | 9999 | audio | * | 9999 | video | * * @subcommand list */ public function list_handlers( $args, $assoc_args ) { /** @var \WP_Embed $wp_embed */ global $wp_embed; $all_handlers = array(); ksort( $wp_embed->handlers ); foreach ( $wp_embed->handlers as $priority => $handlers ) { foreach ( $handlers as $id => $handler ) { $all_handlers[] = array( 'id' => $id, 'regex' => $handler['regex'], 'callback' => $handler['callback'], 'priority' => $priority, ); } } $formatter = $this->get_formatter( $assoc_args ); $formatter->display_items( $all_handlers ); } /** * Get Formatter object based on supplied parameters. * * @param array $assoc_args Parameters passed to command. Determines formatting. * @return \WP_CLI\Formatter */ protected function get_formatter( &$assoc_args ) { return new Formatter( $assoc_args, $this->default_fields ); } } <?php namespace WP_CLI\Embeds; use WP_CLI; use WP_CLI\Formatter; use WP_CLI\Utils; use WP_CLI_Command; /** * Retrieves oEmbed providers. */ class Provider_Command extends WP_CLI_Command { protected $default_fields = array( 'format', 'endpoint', ); /** * Lists all available oEmbed providers. * * ## OPTIONS * * [--field=<field>] * : Display the value of a single field * * [--fields=<fields>] * : Limit the output to specific fields. * * [--format=<format>] * : Render output in a particular format. * --- * default: table * options: * - table * - csv * - json * --- * * [--force-regex] * : Turn the asterisk-type provider URLs into regexes. * * ## AVAILABLE FIELDS * * These fields will be displayed by default for each provider: * * * format * * endpoint * * This field is optionally available: * * * regex * * ## EXAMPLES * * # List format,endpoint fields of available providers. * $ wp embed provider list --fields=format,endpoint * +------------------------------+-----------------------------------------+ * | format | endpoint | * +------------------------------+-----------------------------------------+ * | #https?://youtu\.be/.*#i | https://www.youtube.com/oembed | * | #https?://flic\.kr/.*#i | https://www.flickr.com/services/oembed/ | * | #https?://wordpress\.tv/.*#i | https://wordpress.tv/oembed/ | * * @subcommand list */ public function list_providers( $args, $assoc_args ) { $oembed = new oEmbed(); $force_regex = Utils\get_flag_value( $assoc_args, 'force-regex' ); $providers = array(); foreach ( (array) $oembed->providers as $matchmask => $data ) { list( $providerurl, $regex ) = $data; // Turn the asterisk-type provider URLs into regex if ( $force_regex && ! $regex ) { $matchmask = '#' . str_replace( '___wildcard___', '(.+)', preg_quote( str_replace( '*', '___wildcard___', $matchmask ), '#' ) ) . '#i'; $matchmask = preg_replace( '|^#http\\\://|', '#https?\://', $matchmask ); } $providers[] = array( 'format' => $matchmask, 'endpoint' => $providerurl, 'regex' => $regex ? '1' : '0', ); } $formatter = $this->get_formatter( $assoc_args ); $formatter->display_items( $providers ); } /** * Gets the matching provider for a given URL. * * ## OPTIONS * * <url> * : URL to retrieve provider for. * * [--discover] * : Whether to use oEmbed discovery or not. Defaults to true. * * [--limit-response-size=<size>] * : Limit the size of the resulting HTML when using discovery. Default 150 KB (the standard WordPress limit). Not compatible with 'no-discover'. * * [--link-type=<json|xml>] * : Whether to accept only a certain link type when using discovery. Defaults to any (json or xml), preferring json. Not compatible with 'no-discover'. * --- * options: * - json * - xml * --- * * ## EXAMPLES * * # Get the matching provider for the URL. * $ wp embed provider match https://www.youtube.com/watch?v=dQw4w9WgXcQ * https://www.youtube.com/oembed * * @subcommand match */ public function match_provider( $args, $assoc_args ) { $oembed = new oEmbed(); $url = $args[0]; $discover = \WP_CLI\Utils\get_flag_value( $assoc_args, 'discover', true ); $response_size_limit = \WP_CLI\Utils\get_flag_value( $assoc_args, 'limit-response-size' ); $link_type = \WP_CLI\Utils\get_flag_value( $assoc_args, 'link-type' ); if ( ! $discover && ( null !== $response_size_limit || null !== $link_type ) ) { if ( null !== $response_size_limit && null !== $link_type ) { $msg = "The 'limit-response-size' and 'link-type' options can only be used with discovery."; } elseif ( null !== $response_size_limit ) { $msg = "The 'limit-response-size' option can only be used with discovery."; } else { $msg = "The 'link-type' option can only be used with discovery."; } WP_CLI::error( $msg ); } if ( $response_size_limit ) { if ( Utils\wp_version_compare( '4.0', '<' ) ) { WP_CLI::warning( "The 'limit-response-size' option only works for WordPress 4.0 onwards." ); // Fall through anyway... } add_filter( 'oembed_remote_get_args', function ( $args ) use ( $response_size_limit ) { $args['limit_response_size'] = $response_size_limit; return $args; } ); } if ( $link_type ) { // Filter discovery response. add_filter( 'oembed_linktypes', function ( $linktypes ) use ( $link_type ) { foreach ( $linktypes as $mime_type => $linktype_format ) { if ( $link_type !== $linktype_format ) { unset( $linktypes[ $mime_type ] ); } } return $linktypes; } ); } $oembed_args = array( 'discover' => $discover, ); $provider = $oembed->get_provider( $url, $oembed_args ); if ( ! $provider ) { if ( ! $discover ) { WP_CLI::error( 'No oEmbed provider found for given URL. Maybe try discovery?' ); } else { WP_CLI::error( 'No oEmbed provider found for given URL.' ); } } WP_CLI::line( $provider ); } /** * Get Formatter object based on supplied parameters. * * @param array $assoc_args Parameters passed to command. Determines formatting. * @return \WP_CLI\Formatter */ protected function get_formatter( &$assoc_args ) { return new Formatter( $assoc_args, $this->default_fields ); } } <?php namespace WP_CLI\Embeds; if ( ! class_exists( '\WP_oEmbed' ) ) { require_once ABSPATH . WPINC . '/class-oembed.php'; } /** * Polyfill for older WP versions. */ class oEmbed extends \WP_oEmbed { /** * Takes a URL and returns the corresponding oEmbed provider's URL, if there is one. * * @since 4.0.0 * * @see WP_oEmbed::discover() * * @param string $url The URL to the content. * @param string|array $args Optional provider arguments. * @return false|string False on failure, otherwise the oEmbed provider URL. */ public function get_provider( $url, $args = '' ) { if ( method_exists( 'WP_oEmbed', 'get_provider' ) ) { return parent::get_provider( $url, $args ); } $args = wp_parse_args( $args ); $provider = false; if ( ! isset( $args['discover'] ) ) { $args['discover'] = true; } foreach ( $this->providers as $matchmask => $data ) { list( $providerurl, $regex ) = $data; // Turn the asterisk-type provider URLs into regex if ( ! $regex ) { $matchmask = '#' . str_replace( '___wildcard___', '(.+)', preg_quote( str_replace( '*', '___wildcard___', $matchmask ), '#' ) ) . '#i'; $matchmask = preg_replace( '|^#http\\\://|', '#https?\://', $matchmask ); } if ( preg_match( $matchmask, $url ) ) { $provider = str_replace( '{format}', 'json', $providerurl ); // JSON is easier to deal with than XML break; } } if ( ! $provider && $args['discover'] ) { $provider = $this->discover( $url ); } return $provider; } } <?php if ( ! class_exists( 'WP_CLI' ) ) { return; } $wpcli_embed_autoloader = __DIR__ . '/vendor/autoload.php'; if ( file_exists( $wpcli_embed_autoloader ) ) { require_once $wpcli_embed_autoloader; } if ( class_exists( 'WP_CLI\Dispatcher\CommandNamespace' ) ) { WP_CLI::add_command( 'embed', '\WP_CLI\Embeds\Embeds_Namespace' ); } WP_CLI::add_command( 'embed fetch', '\WP_CLI\Embeds\Fetch_Command' ); WP_CLI::add_command( 'embed provider', '\WP_CLI\Embeds\Provider_Command' ); WP_CLI::add_command( 'embed handler', '\WP_CLI\Embeds\Handler_Command' ); WP_CLI::add_command( 'embed cache', '\WP_CLI\Embeds\Cache_Command' ); <?php if ( ! class_exists( 'WP_CLI' ) ) { return; } $wpcli_core_autoloader = dirname( __FILE__ ) . '/vendor/autoload.php'; if ( file_exists( $wpcli_core_autoloader ) ) { require_once $wpcli_core_autoloader; } WP_CLI::add_command( 'core', 'Core_Command' ); <?php namespace WP_CLI\Core; /** * A Core Upgrader class that leaves packages intact by default. * * @package wp-cli */ class NonDestructiveCoreUpgrader extends CoreUpgrader { // phpcs:ignore Generic.CodeAnalysis.UselessOverridingMethod.Found -- Provide default value. public function unpack_package( $package, $delete_package = false ) { return parent::unpack_package( $package, $delete_package ); } } <?php namespace WP_CLI\Core; use Exception; use Requests_Response; use WP_CLI; use WP_CLI\Utils; use WP_Error; use Core_Upgrader as DefaultCoreUpgrader; use WP_Filesystem_Base; /** * A Core Upgrader class that caches the download, and uses cached if available. * * @package wp-cli */ class CoreUpgrader extends DefaultCoreUpgrader { /** * Whether to retry without certificate validation on TLS handshake failure. * * @var bool */ private $insecure; /** * CoreUpgrader constructor. * * @param WP_Upgrader_Skin|null $skin */ public function __construct( $skin = null, $insecure = false ) { $this->insecure = $insecure; parent::__construct( $skin ); } /** * Caches the download, and uses cached if available. * * @access public * * @param string $package The URI of the package. If this is the full path to an * existing local file, it will be returned untouched. * @param bool $check_signatures Whether to validate file signatures. Default false. * @param array $hook_extra Extra arguments to pass to the filter hooks. Default empty array. * @return string|WP_Error The full path to the downloaded package file, or a WP_Error object. */ public function download_package( $package, $check_signatures = false, $hook_extra = [] ) { /** * Filter whether to return the package. * * @since 3.7.0 * * @param bool $reply Whether to bail without returning the package. Default is false. * @param string $package The package file name. * @param object $this The WP_Upgrader instance. */ $reply = apply_filters( // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- Override existing hook from Core. 'upgrader_pre_download', false, $package, $this, $hook_extra ); if ( false !== $reply ) { return $reply; } // Check if package is a local or remote file. Bail if it's local. if ( ! preg_match( '!^(http|https|ftp)://!i', $package ) && file_exists( $package ) ) { return $package; } if ( empty( $package ) ) { return new WP_Error( 'no_package', $this->strings['no_package'] ); } $filename = pathinfo( $package, PATHINFO_FILENAME ); $extension = pathinfo( $package, PATHINFO_EXTENSION ); $temp = Utils\get_temp_dir() . uniqid( 'wp_' ) . ".{$extension}"; register_shutdown_function( function () use ( $temp ) { if ( file_exists( $temp ) ) { unlink( $temp ); } } ); $cache = WP_CLI::get_cache(); $update = $GLOBALS['wpcli_core_update_obj']; $cache_key = "core/{$filename}-{$update->locale}.{$extension}"; $cache_file = $cache->has( $cache_key ); if ( $cache_file && false === stripos( $package, 'https://wordpress.org/nightly-builds/' ) && false === stripos( $package, 'http://wordpress.org/nightly-builds/' ) ) { WP_CLI::log( "Using cached file '{$cache_file}'..." ); copy( $cache_file, $temp ); return $temp; } /* * Download to a temporary file because piping from cURL to tar is flaky * on MinGW (and probably in other environments too). */ $headers = [ 'Accept' => 'application/json' ]; $options = [ 'timeout' => 600, // 10 minutes ought to be enough for everybody. 'filename' => $temp, 'halt_on_error' => false, 'insecure' => $this->insecure, ]; $this->skin->feedback( 'downloading_package', $package ); /** @var Requests_Response|null $req */ try { $response = Utils\http_request( 'GET', $package, null, $headers, $options ); } catch ( Exception $e ) { return new WP_Error( 'download_failed', $e->getMessage() ); } if ( ! is_null( $response ) && 200 !== (int) $response->status_code ) { return new WP_Error( 'download_failed', $this->strings['download_failed'] ); } if ( false === stripos( $package, 'https://wordpress.org/nightly-builds/' ) ) { $cache->import( $cache_key, $temp ); } return $temp; } /** * Upgrade WordPress core. * * @access public * * @global WP_Filesystem_Base $wp_filesystem Subclass * @global callable $_wp_filesystem_direct_method * * @param object $current Response object for whether WordPress is current. * @param array $args { * Optional. Arguments for upgrading WordPress core. Default empty array. * * @type bool $pre_check_md5 Whether to check the file checksums before * attempting the upgrade. Default true. * @type bool $attempt_rollback Whether to attempt to rollback the chances if * there is a problem. Default false. * @type bool $do_rollback Whether to perform this "upgrade" as a rollback. * Default false. * } * @return null|false|WP_Error False or WP_Error on failure, null on success. */ public function upgrade( $current, $args = [] ) { set_error_handler( [ __CLASS__, 'error_handler' ], E_USER_WARNING | E_USER_NOTICE ); $result = parent::upgrade( $current, $args ); restore_error_handler(); return $result; } /** * Error handler to ignore failures on accessing SSL "https://api.wordpress.org/core/checksums/1.0/" in `get_core_checksums()` which seem to occur intermittently. */ public static function error_handler( $errno, $errstr, $errfile, $errline, $errcontext = null ) { // If ignoring E_USER_WARNING | E_USER_NOTICE, default. if ( ! ( error_reporting() & $errno ) ) { return false; } // If not in "wp-admin/includes/update.php", default. $update_php = 'wp-admin/includes/update.php'; if ( 0 !== substr_compare( $errfile, $update_php, -strlen( $update_php ) ) ) { return false; } // Else assume it's in `get_core_checksums()` and just ignore it. return true; } } <?php use Composer\Semver\Comparator; use WP_CLI\Extractor; use WP_CLI\Iterators\Table as TableIterator; use WP_CLI\Utils; use WP_CLI\Formatter; use WP_CLI\WpOrgApi; /** * Downloads, installs, updates, and manages a WordPress installation. * * ## EXAMPLES * * # Download WordPress core * $ wp core download --locale=nl_NL * Downloading WordPress 4.5.2 (nl_NL)... * md5 hash verified: c5366d05b521831dd0b29dfc386e56a5 * Success: WordPress downloaded. * * # Install WordPress * $ wp core install --url=example.com --title=Example --admin_user=supervisor --admin_password=strongpassword --admin_email=info@example.com * Success: WordPress installed successfully. * * # Display the WordPress version * $ wp core version * 4.5.2 * * @package wp-cli */ class Core_Command extends WP_CLI_Command { /** * Checks for WordPress updates via Version Check API. * * Lists the most recent versions when there are updates available, * or success message when up to date. * * ## OPTIONS * * [--minor] * : Compare only the first two parts of the version number. * * [--major] * : Compare only the first part of the version number. * * [--field=<field>] * : Prints the value of a single field for each update. * * [--fields=<fields>] * : Limit the output to specific object fields. Defaults to version,update_type,package_url. * * [--format=<format>] * : Render output in a particular format. * --- * default: table * options: * - table * - csv * - count * - json * - yaml * --- * * ## EXAMPLES * * $ wp core check-update * +---------+-------------+-------------------------------------------------------------+ * | version | update_type | package_url | * +---------+-------------+-------------------------------------------------------------+ * | 4.5.2 | major | https://downloads.wordpress.org/release/wordpress-4.5.2.zip | * +---------+-------------+-------------------------------------------------------------+ * * @subcommand check-update */ public function check_update( $_, $assoc_args ) { $updates = $this->get_updates( $assoc_args ); if ( $updates ) { $updates = array_reverse( $updates ); $formatter = new Formatter( $assoc_args, [ 'version', 'update_type', 'package_url' ] ); $formatter->display_items( $updates ); } elseif ( empty( $assoc_args['format'] ) || 'table' === $assoc_args['format'] ) { WP_CLI::success( 'WordPress is at the latest version.' ); } } /** * Downloads core WordPress files. * * Downloads and extracts WordPress core files to the specified path. Uses * current directory when no path is specified. Downloaded build is verified * to have the correct md5 and then cached to the local filesytem. * Subsequent uses of command will use the local cache if it still exists. * * ## OPTIONS * * [<download-url>] * : Download directly from a provided URL instead of fetching the URL from the wordpress.org servers. * * [--path=<path>] * : Specify the path in which to install WordPress. Defaults to current * directory. * * [--locale=<locale>] * : Select which language you want to download. * * [--version=<version>] * : Select which version you want to download. Accepts a version number, 'latest' or 'nightly'. * * [--skip-content] * : Download WP without the default themes and plugins. * * [--force] * : Overwrites existing files, if present. * * [--insecure] * : Retry download without certificate validation if TLS handshake fails. Note: This makes the request vulnerable to a MITM attack. * * ## EXAMPLES * * $ wp core download --locale=nl_NL * Downloading WordPress 4.5.2 (nl_NL)... * md5 hash verified: c5366d05b521831dd0b29dfc386e56a5 * Success: WordPress downloaded. * * @when before_wp_load */ public function download( $args, $assoc_args ) { $download_dir = ! empty( $assoc_args['path'] ) ? ( rtrim( $assoc_args['path'], '/\\' ) . '/' ) : ABSPATH; $wordpress_present = is_readable( $download_dir . 'wp-load.php' ); if ( $wordpress_present && ! Utils\get_flag_value( $assoc_args, 'force' ) ) { WP_CLI::error( 'WordPress files seem to already be present here.' ); } if ( ! is_dir( $download_dir ) ) { if ( ! is_writable( dirname( $download_dir ) ) ) { WP_CLI::error( "Insufficient permission to create directory '{$download_dir}'." ); } WP_CLI::log( "Creating directory '{$download_dir}'." ); if ( ! @mkdir( $download_dir, 0777, true /*recursive*/ ) ) { $error = error_get_last(); WP_CLI::error( "Failed to create directory '{$download_dir}': {$error['message']}." ); } } if ( ! is_writable( $download_dir ) ) { WP_CLI::error( "'{$download_dir}' is not writable by current user." ); } $locale = (string) Utils\get_flag_value( $assoc_args, 'locale', 'en_US' ); $skip_content = (bool) Utils\get_flag_value( $assoc_args, 'skip-content', false ); $insecure = (bool) Utils\get_flag_value( $assoc_args, 'insecure', false ); $download_url = array_shift( $args ); $from_url = ! empty( $download_url ); if ( $from_url ) { $version = null; if ( isset( $assoc_args['version'] ) ) { WP_CLI::error( 'Version option is not available for URL downloads.' ); } if ( $skip_content || 'en_US' !== $locale ) { WP_CLI::error( 'Skip content and locale options are not available for URL downloads.' ); } } elseif ( isset( $assoc_args['version'] ) && 'latest' !== $assoc_args['version'] ) { $version = $assoc_args['version']; if ( in_array( strtolower( $version ), [ 'trunk', 'nightly' ], true ) ) { $version = 'nightly'; } // Nightly builds and skip content are only available in .zip format. $extension = ( ( 'nightly' === $version ) || $skip_content ) ? 'zip' : 'tar.gz'; $download_url = $this->get_download_url( $version, $locale, $extension ); } else { try { $offer = ( new WpOrgApi( [ 'insecure' => $insecure ] ) ) ->get_core_download_offer( $locale ); } catch ( Exception $exception ) { WP_CLI::error( $exception ); } if ( ! $offer ) { WP_CLI::error( "The requested locale ({$locale}) was not found." ); } $version = $offer['current']; $download_url = $offer['download']; if ( ! $skip_content ) { $download_url = str_replace( '.zip', '.tar.gz', $download_url ); } } if ( 'nightly' === $version && 'en_US' !== $locale ) { WP_CLI::error( 'Nightly builds are only available for the en_US locale.' ); } $from_version = ''; if ( file_exists( $download_dir . 'wp-includes/version.php' ) ) { $wp_details = self::get_wp_details( $download_dir ); $from_version = $wp_details['wp_version']; } if ( $from_url ) { WP_CLI::log( "Downloading from {$download_url} ..." ); } else { WP_CLI::log( "Downloading WordPress {$version} ({$locale})..." ); } $path_parts = pathinfo( $download_url ); $extension = 'tar.gz'; if ( 'zip' === $path_parts['extension'] ) { $extension = 'zip'; if ( ! class_exists( 'ZipArchive' ) ) { WP_CLI::error( 'Extracting a zip file requires ZipArchive.' ); } } if ( $skip_content && 'zip' !== $extension ) { WP_CLI::error( 'Skip content is only available for ZIP files.' ); } $cache = WP_CLI::get_cache(); if ( $from_url ) { $cache_file = null; } else { $cache_key = "core/wordpress-{$version}-{$locale}.{$extension}"; $cache_file = $cache->has( $cache_key ); } $bad_cache = false; if ( $cache_file ) { WP_CLI::log( "Using cached file '{$cache_file}'..." ); $skip_content_cache_file = $skip_content ? self::strip_content_dir( $cache_file ) : null; try { Extractor::extract( $skip_content_cache_file ?: $cache_file, $download_dir ); } catch ( Exception $exception ) { WP_CLI::warning( 'Extraction failed, downloading a new copy...' ); $bad_cache = true; } } if ( ! $cache_file || $bad_cache ) { // We need to use a temporary file because piping from cURL to tar is flaky // on MinGW (and probably in other environments too). $temp = Utils\get_temp_dir() . uniqid( 'wp_' ) . ".{$extension}"; register_shutdown_function( function () use ( $temp ) { if ( file_exists( $temp ) ) { unlink( $temp ); } } ); $headers = [ 'Accept' => 'application/json' ]; $options = [ 'timeout' => 600, // 10 minutes ought to be enough for everybody 'filename' => $temp, 'insecure' => $insecure, ]; $response = Utils\http_request( 'GET', $download_url, null, $headers, $options ); if ( 404 === (int) $response->status_code ) { WP_CLI::error( 'Release not found. Double-check locale or version.' ); } elseif ( 20 !== (int) substr( $response->status_code, 0, 2 ) ) { WP_CLI::error( "Couldn't access download URL (HTTP code {$response->status_code})." ); } if ( 'nightly' !== $version ) { unset( $options['filename'] ); $md5_response = Utils\http_request( 'GET', $download_url . '.md5', null, [], $options ); if ( $md5_response->status_code >= 200 && $md5_response->status_code < 300 ) { $md5_file = md5_file( $temp ); if ( $md5_file === $md5_response->body ) { WP_CLI::log( 'md5 hash verified: ' . $md5_file ); } else { WP_CLI::error( "md5 hash for download ({$md5_file}) is different than the release hash ({$md5_response->body})." ); } } else { WP_CLI::warning( "Couldn't access md5 hash for release ({$download_url}.md5, HTTP code {$md5_response->status_code})." ); } } else { WP_CLI::warning( 'md5 hash checks are not available for nightly downloads.' ); } $skip_content_temp = $skip_content ? self::strip_content_dir( $temp ) : null; try { Extractor::extract( $skip_content_temp ?: $temp, $download_dir ); } catch ( Exception $exception ) { WP_CLI::error( "Couldn't extract WordPress archive. {$exception->getMessage()}" ); } // Do not use the cache for nightly builds or for downloaded URLs // (the URL could be something like "latest.zip" or "nightly.zip"). if ( ! $from_url && 'nightly' !== $version ) { $cache->import( $cache_key, $temp ); } } if ( $wordpress_present ) { $this->cleanup_extra_files( $from_version, $version, $locale, $insecure ); } WP_CLI::success( 'WordPress downloaded.' ); } /** * Checks if WordPress is installed. * * Determines whether WordPress is installed by checking if the standard * database tables are installed. Doesn't produce output; uses exit codes * to communicate whether WordPress is installed. * * [--network] * : Check if this is a multisite installation. * * ## EXAMPLES * * # Check whether WordPress is installed; exit status 0 if installed, otherwise 1 * $ wp core is-installed * $ echo $? * 1 * * # Bash script for checking whether WordPress is installed or not * if ! wp core is-installed; then * wp core install * fi * * @subcommand is-installed */ public function is_installed( $args, $assoc_args ) { if ( is_blog_installed() && ( ! Utils\get_flag_value( $assoc_args, 'network' ) || is_multisite() ) ) { WP_CLI::halt( 0 ); } WP_CLI::halt( 1 ); } /** * Runs the standard WordPress installation process. * * Creates the WordPress tables in the database using the URL, title, and * default admin user details provided. Performs the famous 5 minute install * in seconds or less. * * Note: if you've installed WordPress in a subdirectory, then you'll need * to `wp option update siteurl` after `wp core install`. For instance, if * WordPress is installed in the `/wp` directory and your domain is example.com, * then you'll need to run `wp option update siteurl http://example.com/wp` for * your WordPress installation to function properly. * * Note: When using custom user tables (e.g. `CUSTOM_USER_TABLE`), the admin * email and password are ignored if the user_login already exists. If the * user_login doesn't exist, a new user will be created. * * ## OPTIONS * * --url=<url> * : The address of the new site. * * --title=<site-title> * : The title of the new site. * * --admin_user=<username> * : The name of the admin user. * * [--admin_password=<password>] * : The password for the admin user. Defaults to randomly generated string. * * --admin_email=<email> * : The email address for the admin user. * * [--skip-email] * : Don't send an email notification to the new admin user. * * ## EXAMPLES * * # Install WordPress in 5 seconds * $ wp core install --url=example.com --title=Example --admin_user=supervisor --admin_password=strongpassword --admin_email=info@example.com * Success: WordPress installed successfully. * * # Install WordPress without disclosing admin_password to bash history * $ wp core install --url=example.com --title=Example --admin_user=supervisor --admin_email=info@example.com --prompt=admin_password < admin_password.txt */ public function install( $args, $assoc_args ) { if ( $this->do_install( $assoc_args ) ) { WP_CLI::success( 'WordPress installed successfully.' ); } else { WP_CLI::log( 'WordPress is already installed.' ); } } /** * Transforms an existing single-site installation into a multisite installation. * * Creates the multisite database tables, and adds the multisite constants * to wp-config.php. * * For those using WordPress with Apache, remember to update the `.htaccess` * file with the appropriate multisite rewrite rules. * * [Review the multisite documentation](https://codex.wordpress.org/Create_A_Network) * for more details about how multisite works. * * ## OPTIONS * * [--title=<network-title>] * : The title of the new network. * * [--base=<url-path>] * : Base path after the domain name that each site url will start with. * --- * default: / * --- * * [--subdomains] * : If passed, the network will use subdomains, instead of subdirectories. Doesn't work with 'localhost'. * * ## EXAMPLES * * $ wp core multisite-convert * Set up multisite database tables. * Added multisite constants to wp-config.php. * Success: Network installed. Don't forget to set up rewrite rules. * * @subcommand multisite-convert * @alias install-network */ public function multisite_convert( $args, $assoc_args ) { if ( is_multisite() ) { WP_CLI::error( 'This already is a multisite installation.' ); } $assoc_args = self::set_multisite_defaults( $assoc_args ); if ( ! isset( $assoc_args['title'] ) ) { // translators: placeholder is blog name $assoc_args['title'] = sprintf( _x( '%s Sites', 'Default network name' ), get_option( 'blogname' ) ); } if ( $this->multisite_convert_( $assoc_args ) ) { WP_CLI::success( "Network installed. Don't forget to set up rewrite rules (and a .htaccess file, if using Apache)." ); } } /** * Installs WordPress multisite from scratch. * * Creates the WordPress tables in the database using the URL, title, and * default admin user details provided. Then, creates the multisite tables * in the database and adds multisite constants to the wp-config.php. * * For those using WordPress with Apache, remember to update the `.htaccess` * file with the appropriate multisite rewrite rules. * * ## OPTIONS * * [--url=<url>] * : The address of the new site. * * [--base=<url-path>] * : Base path after the domain name that each site url in the network will start with. * --- * default: / * --- * * [--subdomains] * : If passed, the network will use subdomains, instead of subdirectories. Doesn't work with 'localhost'. * * --title=<site-title> * : The title of the new site. * * --admin_user=<username> * : The name of the admin user. * --- * default: admin * --- * * [--admin_password=<password>] * : The password for the admin user. Defaults to randomly generated string. * * --admin_email=<email> * : The email address for the admin user. * * [--skip-email] * : Don't send an email notification to the new admin user. * * [--skip-config] * : Don't add multisite constants to wp-config.php. * * ## EXAMPLES * * $ wp core multisite-install --title="Welcome to the WordPress" \ * > --admin_user="admin" --admin_password="password" \ * > --admin_email="user@example.com" * Single site database tables already present. * Set up multisite database tables. * Added multisite constants to wp-config.php. * Success: Network installed. Don't forget to set up rewrite rules. * * @subcommand multisite-install */ public function multisite_install( $args, $assoc_args ) { if ( $this->do_install( $assoc_args ) ) { WP_CLI::log( 'Created single site database tables.' ); } else { WP_CLI::log( 'Single site database tables already present.' ); } $assoc_args = self::set_multisite_defaults( $assoc_args ); // translators: placeholder is user supplied title $assoc_args['title'] = sprintf( _x( '%s Sites', 'Default network name' ), $assoc_args['title'] ); // Overwrite runtime args, to avoid mismatches. $consts_to_args = [ 'SUBDOMAIN_INSTALL' => 'subdomains', 'PATH_CURRENT_SITE' => 'base', 'SITE_ID_CURRENT_SITE' => 'site_id', 'BLOG_ID_CURRENT_SITE' => 'blog_id', ]; foreach ( $consts_to_args as $const => $arg ) { if ( defined( $const ) ) { $assoc_args[ $arg ] = constant( $const ); } } if ( ! $this->multisite_convert_( $assoc_args ) ) { return; } // Do the steps that were skipped by populate_network(), // which checks is_multisite(). if ( is_multisite() ) { $site_user = get_user_by( 'email', $assoc_args['admin_email'] ); self::add_site_admins( $site_user ); $domain = self::get_clean_basedomain(); self::create_initial_blog( $assoc_args['site_id'], $assoc_args['blog_id'], $domain, $assoc_args['base'], $assoc_args['subdomains'], $site_user ); } WP_CLI::success( "Network installed. Don't forget to set up rewrite rules (and a .htaccess file, if using Apache)." ); } private static function set_multisite_defaults( $assoc_args ) { $defaults = [ 'subdomains' => false, 'base' => '/', 'site_id' => 1, 'blog_id' => 1, ]; return array_merge( $defaults, $assoc_args ); } private function do_install( $assoc_args ) { if ( is_blog_installed() ) { return false; } if ( true === Utils\get_flag_value( $assoc_args, 'skip-email' ) ) { if ( ! function_exists( 'wp_new_blog_notification' ) ) { function wp_new_blog_notification() { // Silence is golden } } // WP 4.9.0 - skip "Notice of Admin Email Change" email as well (https://core.trac.wordpress.org/ticket/39117). add_filter( 'send_site_admin_email_change_email', '__return_false' ); } require_once ABSPATH . 'wp-admin/includes/upgrade.php'; $defaults = [ 'title' => '', 'admin_user' => '', 'admin_email' => '', 'admin_password' => '', ]; $args = wp_parse_args( $assoc_args, $defaults ); // Support prompting for the `--url=<url>`, // which is normally a runtime argument if ( isset( $assoc_args['url'] ) ) { WP_CLI::set_url( $assoc_args['url'] ); } $public = true; $password = empty( $args['admin_password'] ) ? wp_generate_password( 18 ) : $args['admin_password']; if ( ! is_email( $args['admin_email'] ) ) { WP_CLI::error( "The '{$args['admin_email']}' email address is invalid." ); } $result = wp_install( $args['title'], $args['admin_user'], $args['admin_email'], $public, '', $password ); if ( is_wp_error( $result ) ) { $reason = WP_CLI::error_to_string( $result ); WP_CLI::error( "Installation failed ({$reason})." ); } if ( ! empty( $GLOBALS['wpdb']->last_error ) ) { WP_CLI::error( 'Installation produced database errors, and may have partially or completely failed.' ); } if ( empty( $args['admin_password'] ) ) { WP_CLI::log( "Admin password: {$result['password']}" ); } // Confirm the uploads directory exists $upload_dir = wp_upload_dir(); if ( ! empty( $upload_dir['error'] ) ) { WP_CLI::warning( $upload_dir['error'] ); } return true; } private function multisite_convert_( $assoc_args ) { global $wpdb; require_once ABSPATH . 'wp-admin/includes/upgrade.php'; $domain = self::get_clean_basedomain(); if ( 'localhost' === $domain && ! empty( $assoc_args['subdomains'] ) ) { WP_CLI::error( "Multisite with subdomains cannot be configured when domain is 'localhost'." ); } // need to register the multisite tables manually for some reason foreach ( $wpdb->tables( 'ms_global' ) as $table => $prefixed_table ) { $wpdb->$table = $prefixed_table; } install_network(); $result = populate_network( $assoc_args['site_id'], $domain, get_option( 'admin_email' ), $assoc_args['title'], $assoc_args['base'], $assoc_args['subdomains'] ); $site_id = $wpdb->get_var( "SELECT id FROM $wpdb->site" ); $site_id = ( null === $site_id ) ? 1 : (int) $site_id; if ( true === $result ) { WP_CLI::log( 'Set up multisite database tables.' ); } elseif ( is_wp_error( $result ) ) { switch ( $result->get_error_code() ) { case 'siteid_exists': WP_CLI::log( $result->get_error_message() ); return false; case 'no_wildcard_dns': WP_CLI::warning( __( 'Wildcard DNS may not be configured correctly.' ) ); break; default: WP_CLI::error( $result ); } } // delete_site_option() cleans the alloptions cache to prevent dupe option delete_site_option( 'upload_space_check_disabled' ); update_site_option( 'upload_space_check_disabled', 1 ); if ( ! is_multisite() ) { $subdomain_export = Utils\get_flag_value( $assoc_args, 'subdomains' ) ? 'true' : 'false'; $ms_config = <<<EOT define( 'WP_ALLOW_MULTISITE', true ); define( 'MULTISITE', true ); define( 'SUBDOMAIN_INSTALL', {$subdomain_export} ); \$base = '{$assoc_args['base']}'; define( 'DOMAIN_CURRENT_SITE', '{$domain}' ); define( 'PATH_CURRENT_SITE', '{$assoc_args['base']}' ); define( 'SITE_ID_CURRENT_SITE', {$site_id} ); define( 'BLOG_ID_CURRENT_SITE', 1 ); EOT; $wp_config_path = Utils\locate_wp_config(); if ( true === Utils\get_flag_value( $assoc_args, 'skip-config' ) ) { WP_CLI::log( "Addition of multisite constants to 'wp-config.php' skipped. You need to add them manually:\n{$ms_config}" ); } elseif ( is_writable( $wp_config_path ) && self::modify_wp_config( $ms_config ) ) { WP_CLI::log( "Added multisite constants to 'wp-config.php'." ); } else { WP_CLI::warning( "Multisite constants could not be written to 'wp-config.php'. You may need to add them manually:\n{$ms_config}" ); } } else { /* Multisite constants are defined, therefore we already have an empty site_admins site meta. * * Code based on parts of delete_network_option. */ $rows = $wpdb->get_results( "SELECT meta_id, site_id FROM {$wpdb->sitemeta} WHERE meta_key = 'site_admins' AND meta_value = ''" ); foreach ( $rows as $row ) { wp_cache_delete( "{$row->site_id}:site_admins", 'site-options' ); $wpdb->delete( $wpdb->sitemeta, [ 'meta_id' => $row->meta_id ] ); } } return true; } // copied from populate_network() private static function create_initial_blog( $network_id, $blog_id, $domain, $path, $subdomain_install, $site_user ) { global $wpdb, $current_site, $wp_rewrite; // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited -- This is meant to replace Core functionality. $current_site = new stdClass(); $current_site->domain = $domain; $current_site->path = $path; $current_site->site_name = ucfirst( $domain ); $blog_data = [ 'site_id' => $network_id, 'domain' => $domain, 'path' => $path, 'registered' => current_time( 'mysql' ), ]; $wpdb->insert( $wpdb->blogs, $blog_data ); $current_site->blog_id = $wpdb->insert_id; $blog_id = $wpdb->insert_id; update_user_meta( $site_user->ID, 'source_domain', $domain ); update_user_meta( $site_user->ID, 'primary_blog', $blog_id ); if ( $subdomain_install ) { $wp_rewrite->set_permalink_structure( '/%year%/%monthnum%/%day%/%postname%/' ); } else { $wp_rewrite->set_permalink_structure( '/blog/%year%/%monthnum%/%day%/%postname%/' ); } flush_rewrite_rules(); } // copied from populate_network() private static function add_site_admins( $site_user ) { $site_admins = [ $site_user->user_login ]; $users = get_users( [ 'fields' => [ 'ID', 'user_login' ] ] ); if ( $users ) { foreach ( $users as $user ) { if ( is_super_admin( $user->ID ) && ! in_array( $user->user_login, $site_admins, true ) ) { $site_admins[] = $user->user_login; } } } update_site_option( 'site_admins', $site_admins ); } private static function modify_wp_config( $content ) { $wp_config_path = Utils\locate_wp_config(); $token = "/* That's all, stop editing!"; $config_contents = file_get_contents( $wp_config_path ); if ( false === strpos( $config_contents, $token ) ) { return false; } list( $before, $after ) = explode( $token, $config_contents ); $content = trim( $content ); file_put_contents( $wp_config_path, "{$before}\n\n{$content}\n\n{$token}{$after}" ); return true; } private static function get_clean_basedomain() { $domain = preg_replace( '|https?://|', '', get_option( 'siteurl' ) ); $slash = strpos( $domain, '/' ); if ( false !== $slash ) { $domain = substr( $domain, 0, $slash ); } return $domain; } /** * Displays the WordPress version. * * ## OPTIONS * * [--extra] * : Show extended version information. * * ## EXAMPLES * * # Display the WordPress version * $ wp core version * 4.5.2 * * # Display WordPress version along with other information * $ wp core version --extra * WordPress version: 4.5.2 * Database revision: 36686 * TinyMCE version: 4.310 (4310-20160418) * Package language: en_US * * @when before_wp_load */ public function version( $args = [], $assoc_args = [] ) { $details = self::get_wp_details(); if ( ! Utils\get_flag_value( $assoc_args, 'extra' ) ) { WP_CLI::line( $details['wp_version'] ); return; } $match = []; $found_version = preg_match( '/(\d)(\d+)-/', $details['tinymce_version'], $match ); $human_readable_tiny_mce = $found_version ? "{$match[1]}.{$match[2]}" : ''; echo Utils\mustache_render( self::get_template_path( 'versions.mustache' ), [ 'wp-version' => $details['wp_version'], 'db-version' => $details['wp_db_version'], 'local-package' => empty( $details['wp_local_package'] ) ? 'en_US' : $details['wp_local_package'], 'mce-version' => $human_readable_tiny_mce ? "{$human_readable_tiny_mce} ({$details['tinymce_version']})" : $details['tinymce_version'], ] ); } /** * Gets version information from `wp-includes/version.php`. * * @return array { * @type string $wp_version The WordPress version. * @type int $wp_db_version The WordPress DB revision. * @type string $tinymce_version The TinyMCE version. * @type string $wp_local_package The TinyMCE version. * } */ private static function get_wp_details( $abspath = ABSPATH ) { $versions_path = $abspath . 'wp-includes/version.php'; if ( ! is_readable( $versions_path ) ) { WP_CLI::error( "This does not seem to be a WordPress installation.\n" . 'Pass --path=`path/to/wordpress` or run `wp core download`.' ); } $version_content = file_get_contents( $versions_path, null, null, 6, 2048 ); $vars = [ 'wp_version', 'wp_db_version', 'tinymce_version', 'wp_local_package' ]; $result = []; foreach ( $vars as $var_name ) { $result[ $var_name ] = self::find_var( $var_name, $version_content ); } return $result; } /** * Gets the template path based on installation type. */ private static function get_template_path( $template ) { $command_root = Utils\phar_safe_path( dirname( __DIR__ ) ); $template_path = "{$command_root}/templates/{$template}"; if ( ! file_exists( $template_path ) ) { WP_CLI::error( "Couldn't find {$template}" ); } return $template_path; } /** * Searches for the value assigned to variable `$var_name` in PHP code `$code`. * * This is equivalent to matching the `\$VAR_NAME = ([^;]+)` regular expression and returning * the first match either as a `string` or as an `integer` (depending if it's surrounded by * quotes or not). * * @param string $var_name Variable name to search for. * @param string $code PHP code to search in. * * @return int|string|null */ private static function find_var( $var_name, $code ) { $start = strpos( $code, '$' . $var_name . ' = ' ); if ( ! $start ) { return null; } $start = $start + strlen( $var_name ) + 3; $end = strpos( $code, ';', $start ); $value = substr( $code, $start, $end - $start ); return trim( $value, " '" ); } /** * Security copy of the core function with Requests - Gets the checksums for the given version of WordPress. * * @param string $version Version string to query. * @param string $locale Locale to query. * @param bool $insecure Whether to retry without certificate validation on TLS handshake failure. * @return string|array String message on failure. An array of checksums on success. */ private static function get_core_checksums( $version, $locale, $insecure ) { $query = http_build_query( compact( 'version', 'locale' ), null, '&' ); $url = "https://api.wordpress.org/core/checksums/1.0/?{$query}"; $headers = [ 'Accept' => 'application/json' ]; $options = [ 'timeout' => 30, 'insecure' => $insecure, ]; $response = Utils\http_request( 'GET', $url, null, $headers, $options ); if ( ! $response->success || 200 !== (int) $response->status_code ) { return "Checksum request '{$url}' failed (HTTP {$response->status_code})."; } $body = trim( $response->body ); $body = json_decode( $body, true ); if ( ! is_array( $body ) || ! isset( $body['checksums'] ) || ! is_array( $body['checksums'] ) ) { return "Checksums not available for WordPress {$version}/{$locale}."; } return $body['checksums']; } /** * Updates WordPress to a newer version. * * Defaults to updating WordPress to the latest version. * * If you see "Error: Another update is currently in progress.", you may * need to run `wp option delete core_updater.lock` after verifying another * update isn't actually running. * * ## OPTIONS * * [<zip>] * : Path to zip file to use, instead of downloading from wordpress.org. * * [--minor] * : Only perform updates for minor releases (e.g. update from WP 4.3 to 4.3.3 instead of 4.4.2). * * [--version=<version>] * : Update to a specific version, instead of to the latest version. Alternatively accepts 'nightly'. * * [--force] * : Update even when installed WP version is greater than the requested version. * * [--locale=<locale>] * : Select which language you want to download. * * [--insecure] * : Retry download without certificate validation if TLS handshake fails. Note: This makes the request vulnerable to a MITM attack. * * ## EXAMPLES * * # Update WordPress * $ wp core update * Updating to version 4.5.2 (en_US)... * Downloading update from https://downloads.wordpress.org/release/wordpress-4.5.2-no-content.zip... * Unpacking the update... * Cleaning up files... * No files found that need cleaning up * Success: WordPress updated successfully. * * # Update WordPress to latest version of 3.8 release * $ wp core update --version=3.8 ../latest.zip * Updating to version 3.8 ()... * Unpacking the update... * Cleaning up files... * File removed: wp-admin/js/tags-box.js * ... * File removed: wp-admin/js/updates.min. * 377 files cleaned up * Success: WordPress updated successfully. * * # Update WordPress to 3.1 forcefully * $ wp core update --version=3.1 --force * Updating to version 3.1 (en_US)... * Downloading update from https://wordpress.org/wordpress-3.1.zip... * Unpacking the update... * Warning: Checksums not available for WordPress 3.1/en_US. Please cleanup files manually. * Success: WordPress updated successfully. * * @alias upgrade */ public function update( $args, $assoc_args ) { global $wp_version; $update = null; $upgrader = 'WP_CLI\\Core\\CoreUpgrader'; if ( 'trunk' === Utils\get_flag_value( $assoc_args, 'version' ) ) { $assoc_args['version'] = 'nightly'; } if ( ! empty( $args[0] ) ) { // ZIP path or URL is given $upgrader = 'WP_CLI\\Core\\NonDestructiveCoreUpgrader'; $version = Utils\get_flag_value( $assoc_args, 'version' ); $update = (object) [ 'response' => 'upgrade', 'current' => $version, 'download' => $args[0], 'packages' => (object) [ 'partial' => null, 'new_bundled' => null, 'no_content' => null, 'full' => $args[0], ], 'version' => $version, 'locale' => null, ]; } elseif ( empty( $assoc_args['version'] ) ) { // Update to next release wp_version_check(); $from_api = get_site_transient( 'update_core' ); if ( Utils\get_flag_value( $assoc_args, 'minor' ) ) { foreach ( $from_api->updates as $offer ) { $sem_ver = Utils\get_named_sem_ver( $offer->version, $wp_version ); if ( ! $sem_ver || 'patch' !== $sem_ver ) { continue; } $update = $offer; break; } if ( empty( $update ) ) { WP_CLI::success( 'WordPress is at the latest minor release.' ); return; } } else { if ( ! empty( $from_api->updates ) ) { list( $update ) = $from_api->updates; } } } elseif ( Utils\wp_version_compare( $assoc_args['version'], '<' ) || 'nightly' === $assoc_args['version'] || Utils\get_flag_value( $assoc_args, 'force' ) ) { // Specific version is given $version = $assoc_args['version']; $locale = Utils\get_flag_value( $assoc_args, 'locale', get_locale() ); $new_package = $this->get_download_url( $version, $locale ); $update = (object) [ 'response' => 'upgrade', 'current' => $assoc_args['version'], 'download' => $new_package, 'packages' => (object) [ 'partial' => null, 'new_bundled' => null, 'no_content' => null, 'full' => $new_package, ], 'version' => $version, 'locale' => $locale, ]; } if ( ! empty( $update ) && ( $update->version !== $wp_version || Utils\get_flag_value( $assoc_args, 'force' ) ) ) { require_once ABSPATH . 'wp-admin/includes/upgrade.php'; if ( $update->version ) { WP_CLI::log( "Updating to version {$update->version} ({$update->locale})..." ); } else { WP_CLI::log( 'Starting update...' ); } $from_version = $wp_version; $insecure = (bool) Utils\get_flag_value( $assoc_args, 'insecure', false ); $GLOBALS['wpcli_core_update_obj'] = $update; $result = Utils\get_upgrader( $upgrader, $insecure )->upgrade( $update ); unset( $GLOBALS['wpcli_core_update_obj'] ); if ( is_wp_error( $result ) ) { $message = WP_CLI::error_to_string( $result ); if ( 'up_to_date' !== $result->get_error_code() ) { WP_CLI::error( $message ); } else { WP_CLI::success( $message ); } } else { $to_version = ''; if ( file_exists( ABSPATH . 'wp-includes/version.php' ) ) { $wp_details = self::get_wp_details(); $to_version = $wp_details['wp_version']; } $locale = (string) Utils\get_flag_value( $assoc_args, 'locale', get_locale() ); $this->cleanup_extra_files( $from_version, $to_version, $locale, $insecure ); WP_CLI::success( 'WordPress updated successfully.' ); } } else { WP_CLI::success( 'WordPress is up to date.' ); } } /** * Runs the WordPress database update procedure. * * [--network] * : Update databases for all sites on a network * * [--dry-run] * : Compare database versions without performing the update. * * ## EXAMPLES * * # Update the WordPress database * $ wp core update-db * Success: WordPress database upgraded successfully from db version 36686 to 35700. * * # Update databases for all sites on a network * $ wp core update-db --network * WordPress database upgraded successfully from db version 35700 to 29630 on example.com/ * Success: WordPress database upgraded on 123/123 sites * * @subcommand update-db */ public function update_db( $args, $assoc_args ) { global $wpdb, $wp_db_version, $wp_current_db_version; $network = Utils\get_flag_value( $assoc_args, 'network' ); if ( $network && ! is_multisite() ) { WP_CLI::error( 'This is not a multisite installation.' ); } $dry_run = Utils\get_flag_value( $assoc_args, 'dry-run' ); if ( $dry_run ) { WP_CLI::log( 'Performing a dry run, with no database modification.' ); } if ( $network ) { $iterator_args = [ 'table' => $wpdb->blogs, 'where' => [ 'spam' => 0, 'deleted' => 0, 'archived' => 0, ], ]; $it = new TableIterator( $iterator_args ); $success = 0; $total = 0; $site_ids = []; foreach ( $it as $blog ) { $total++; $site_ids[] = $blog->site_id; $url = $blog->domain . $blog->path; $cmd = "--url={$url} core update-db"; if ( $dry_run ) { $cmd .= ' --dry-run'; } $process = WP_CLI::runcommand( $cmd, [ 'return' => 'all', 'exit_error' => false, ] ); if ( 0 === (int) $process->return_code ) { // See if we can parse the stdout if ( preg_match( '#Success: (.+)#', $process->stdout, $matches ) ) { $message = rtrim( $matches[1], '.' ); $message = "{$message} on {$url}"; } else { $message = "Database upgraded successfully on {$url}"; } WP_CLI::log( $message ); $success++; } else { WP_CLI::warning( "Database failed to upgrade on {$url}" ); } } if ( ! $dry_run && $total && $success === $total ) { foreach ( array_unique( $site_ids ) as $site_id ) { update_metadata( 'site', $site_id, 'wpmu_upgrade_site', $wp_db_version ); } } WP_CLI::success( "WordPress database upgraded on {$success}/{$total} sites." ); } else { require_once ABSPATH . 'wp-admin/includes/upgrade.php'; // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited -- Replacing WP Core behavior is the goal here. $wp_current_db_version = (int) __get_option( 'db_version' ); if ( $wp_db_version !== $wp_current_db_version ) { if ( $dry_run ) { WP_CLI::success( "WordPress database will be upgraded from db version {$wp_current_db_version} to {$wp_db_version}." ); } else { // WP upgrade isn't too fussy about generating MySQL warnings such as "Duplicate key name" during an upgrade so suppress. $wpdb->suppress_errors(); // WP upgrade expects `$_SERVER['HTTP_HOST']` to be set in `wp_guess_url()`, otherwise get PHP notice. if ( ! isset( $_SERVER['HTTP_HOST'] ) ) { $_SERVER['HTTP_HOST'] = 'http://example.com'; } wp_upgrade(); WP_CLI::success( "WordPress database upgraded successfully from db version {$wp_current_db_version} to {$wp_db_version}." ); } } else { WP_CLI::success( "WordPress database already at latest db version {$wp_db_version}." ); } } } /** * Gets download url based on version, locale and desired file type. * * @param $version * @param string $locale * @param string $file_type * @return string */ private function get_download_url( $version, $locale = 'en_US', $file_type = 'zip' ) { if ( 'nightly' === $version ) { if ( 'zip' === $file_type ) { return 'https://wordpress.org/nightly-builds/wordpress-latest.zip'; } else { WP_CLI::error( 'Nightly builds are only available in .zip format.' ); } } $locale_subdomain = 'en_US' === $locale ? '' : substr( $locale, 0, 2 ) . '.'; $locale_suffix = 'en_US' === $locale ? '' : "-{$locale}"; return "https://{$locale_subdomain}wordpress.org/wordpress-{$version}{$locale_suffix}.{$file_type}"; } /** * Returns update information. */ private function get_updates( $assoc_args ) { wp_version_check(); $from_api = get_site_transient( 'update_core' ); if ( ! $from_api ) { return []; } $compare_version = str_replace( '-src', '', $GLOBALS['wp_version'] ); $updates = [ 'major' => false, 'minor' => false, ]; foreach ( $from_api->updates as $offer ) { $update_type = Utils\get_named_sem_ver( $offer->version, $compare_version ); if ( ! $update_type ) { continue; } // WordPress follow its own versioning which is roughly equivalent to semver if ( 'minor' === $update_type ) { $update_type = 'major'; } elseif ( 'patch' === $update_type ) { $update_type = 'minor'; } if ( ! empty( $updates[ $update_type ] ) && ! Comparator::greaterThan( $offer->version, $updates[ $update_type ]['version'] ) ) { continue; } $updates[ $update_type ] = [ 'version' => $offer->version, 'update_type' => $update_type, 'package_url' => ! empty( $offer->packages->partial ) ? $offer->packages->partial : $offer->packages->full, ]; } foreach ( $updates as $type => $value ) { if ( empty( $value ) ) { unset( $updates[ $type ] ); } } foreach ( [ 'major', 'minor' ] as $type ) { if ( true === Utils\get_flag_value( $assoc_args, $type ) ) { return ! empty( $updates[ $type ] ) ? [ $updates[ $type ] ] : false; } } return array_values( $updates ); } /** * Clean up extra files. * * @param string $version_from Starting version that the installation was updated from. * @param string $version_to Target version that the installation is updated to. * @param string $locale Locale of the installation. * @param bool $insecure Whether to retry without certificate validation on TLS handshake failure. */ private function cleanup_extra_files( $version_from, $version_to, $locale, $insecure ) { if ( ! $version_from || ! $version_to ) { WP_CLI::warning( 'Failed to find WordPress version. Please cleanup files manually.' ); return; } $old_checksums = self::get_core_checksums( $version_from, $locale ?: 'en_US', $insecure ); if ( ! is_array( $old_checksums ) ) { WP_CLI::warning( "{$old_checksums} Please cleanup files manually." ); return; } $new_checksums = self::get_core_checksums( $version_to, $locale ?: 'en_US', $insecure ); if ( ! is_array( $new_checksums ) ) { WP_CLI::warning( "{$new_checksums} Please cleanup files manually." ); return; } // Compare the files from the old version and the new version in a case-insensitive manner, // to prevent files being incorrectly deleted on systems with case-insensitive filesystems // when core changes the case of filenames. // The main logic for this was taken from the Joomla project and adapted for WP. // See: https://github.com/joomla/joomla-cms/blob/bb5368c7ef9c20270e6e9fcc4b364cd0849082a5/administrator/components/com_admin/script.php#L8158 $old_filepaths = array_keys( $old_checksums ); $new_filepaths = array_keys( $new_checksums ); $new_filepaths = array_combine( array_map( 'strtolower', $new_filepaths ), $new_filepaths ); $old_filepaths_to_check = array_diff( $old_filepaths, $new_filepaths ); foreach ( $old_filepaths_to_check as $old_filepath_to_check ) { $old_realpath = realpath( ABSPATH . $old_filepath_to_check ); // On Unix without incorrectly cased file. if ( false === $old_realpath ) { continue; } $lowercase_old_filepath_to_check = strtolower( $old_filepath_to_check ); if ( ! array_key_exists( $lowercase_old_filepath_to_check, $new_filepaths ) ) { $files_to_remove[] = $old_filepath_to_check; continue; } // We are now left with only the files that are similar from old to new except for their case. $old_basename = basename( $old_realpath ); $new_filepath = $new_filepaths[ $lowercase_old_filepath_to_check ]; $expected_basename = basename( $new_filepath ); $new_realpath = realpath( ABSPATH . $new_filepath ); $new_basename = basename( $new_realpath ); // On Windows or Unix with only the incorrectly cased file. if ( $new_basename !== $expected_basename ) { WP_CLI::debug( "Renaming file '{$old_filepath_to_check}' => '{$new_filepath}'", 'core' ); rename( ABSPATH . $old_filepath_to_check, ABSPATH . $old_filepath_to_check . '.tmp' ); rename( ABSPATH . $old_filepath_to_check . '.tmp', ABSPATH . $new_filepath ); continue; } // There might still be an incorrectly cased file on other OS than Windows. if ( basename( $old_filepath_to_check ) === $old_basename ) { // Check if case-insensitive file system, eg on OSX. if ( fileinode( $old_realpath ) === fileinode( $new_realpath ) ) { // Check deeper because even realpath or glob might not return the actual case. if ( ! in_array( $expected_basename, scandir( dirname( $new_realpath ) ), true ) ) { WP_CLI::debug( "Renaming file '{$old_filepath_to_check}' => '{$new_filepath}'", 'core' ); rename( ABSPATH . $old_filepath_to_check, ABSPATH . $old_filepath_to_check . '.tmp' ); rename( ABSPATH . $old_filepath_to_check . '.tmp', ABSPATH . $new_filepath ); } } else { // On Unix with both files: Delete the incorrectly cased file. $files_to_remove[] = $old_filepath_to_check; } } } if ( ! empty( $files_to_remove ) ) { WP_CLI::log( 'Cleaning up files...' ); $count = 0; foreach ( $files_to_remove as $file ) { // wp-content should be considered user data if ( 0 === stripos( $file, 'wp-content' ) ) { continue; } if ( file_exists( ABSPATH . $file ) ) { unlink( ABSPATH . $file ); WP_CLI::log( "File removed: {$file}" ); $count++; } } if ( $count ) { WP_CLI::log( number_format( $count ) . ' files cleaned up.' ); } else { WP_CLI::log( 'No files found that need cleaning up.' ); } } } private static function strip_content_dir( $zip_file ) { $new_zip_file = Utils\get_temp_dir() . uniqid( 'wp_' ) . '.zip'; register_shutdown_function( function () use ( $new_zip_file ) { if ( file_exists( $new_zip_file ) ) { unlink( $new_zip_file ); } } ); // Duplicate file to avoid modifying the original, which could be cache. if ( ! copy( $zip_file, $new_zip_file ) ) { WP_CLI::error( 'Failed to copy ZIP file.' ); } $zip = new ZipArchive(); $res = $zip->open( $new_zip_file ); if ( true === $res ) { // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase for ( $i = 0; $i < $zip->numFiles; $i++ ) { $info = $zip->statIndex( $i ); if ( false !== stripos( $info['name'], 'wp-content/' ) ) { $zip->deleteIndex( $i ); } } $zip->close(); return $new_zip_file; } else { WP_CLI::error( 'ZipArchive failed to open ZIP file.' ); } } } <?php /** * Adds, removes, fetches, and flushes the WP Object Cache object. * * By default, the WP Object Cache exists in PHP memory for the length of the * request (and is emptied at the end). Use a persistent object cache drop-in * to persist the object cache between requests. * * [Read the codex article](https://codex.wordpress.org/Class_Reference/WP_Object_Cache) * for more detail. * * ## EXAMPLES * * # Set cache. * $ wp cache set my_key my_value my_group 300 * Success: Set object 'my_key' in group 'my_group'. * * # Get cache. * $ wp cache get my_key my_group * my_value * * @package wp-cli */ class Cache_Command extends WP_CLI_Command { /** * Adds a value to the object cache. * * Errors if a value already exists for the key, which means the value can't * be added. * * ## OPTIONS * * <key> * : Cache key. * * <value> * : Value to add to the key. * * [<group>] * : Method for grouping data within the cache which allows the same key to be used across groups. * --- * default: default * --- * * [<expiration>] * : Define how long to keep the value, in seconds. `0` means as long as possible. * --- * default: 0 * --- * * ## EXAMPLES * * # Add cache. * $ wp cache add my_key my_group my_value 300 * Success: Added object 'my_key' in group 'my_value'. */ public function add( $args, $assoc_args ) { list( $key, $value, $group, $expiration ) = $args; if ( ! wp_cache_add( $key, $value, $group, $expiration ) ) { WP_CLI::error( "Could not add object '$key' in group '$group'. Does it already exist?" ); } WP_CLI::success( "Added object '$key' in group '$group'." ); } /** * Decrements a value in the object cache. * * Errors if the value can't be decremented. * * ## OPTIONS * * <key> * : Cache key. * * [<offset>] * : The amount by which to decrement the item's value. * --- * default: 1 * --- * * [<group>] * : Method for grouping data within the cache which allows the same key to be used across groups. * --- * default: default * --- * * ## EXAMPLES * * # Decrease cache value. * $ wp cache decr my_key 2 my_group * 48 */ public function decr( $args, $assoc_args ) { list( $key, $offset, $group ) = $args; $value = wp_cache_decr( $key, $offset, $group ); if ( false === $value ) { WP_CLI::error( 'The value was not decremented.' ); } WP_CLI::print_value( $value, $assoc_args ); } /** * Removes a value from the object cache. * * Errors if the value can't be deleted. * * ## OPTIONS * * <key> * : Cache key. * * [<group>] * : Method for grouping data within the cache which allows the same key to be used across groups. * --- * default: default * --- * * ## EXAMPLES * * # Delete cache. * $ wp cache delete my_key my_group * Success: Object deleted. */ public function delete( $args, $assoc_args ) { list( $key, $group ) = $args; $result = wp_cache_delete( $key, $group ); if ( false === $result ) { WP_CLI::error( 'The object was not deleted.' ); } WP_CLI::success( 'Object deleted.' ); } /** * Flushes the object cache. * * For WordPress multisite instances using a persistent object cache, * flushing the object cache will typically flush the cache for all sites. * Beware of the performance impact when flushing the object cache in * production. * * Errors if the object cache can't be flushed. * * ## EXAMPLES * * # Flush cache. * $ wp cache flush * Success: The cache was flushed. */ public function flush( $args, $assoc_args ) { $value = wp_cache_flush(); if ( false === $value ) { WP_CLI::error( 'The object cache could not be flushed.' ); } WP_CLI::success( 'The cache was flushed.' ); } /** * Gets a value from the object cache. * * Errors if the value doesn't exist. * * ## OPTIONS * * <key> * : Cache key. * * [<group>] * : Method for grouping data within the cache which allows the same key to be used across groups. * --- * default: default * --- * * ## EXAMPLES * * # Get cache. * $ wp cache get my_key my_group * my_value */ public function get( $args, $assoc_args ) { list( $key, $group ) = $args; $value = wp_cache_get( $key, $group ); if ( false === $value ) { WP_CLI::error( "Object with key '$key' and group '$group' not found." ); } WP_CLI::print_value( $value, $assoc_args ); } /** * Increments a value in the object cache. * * Errors if the value can't be incremented. * * ## OPTIONS * * <key> * : Cache key. * * [<offset>] * : The amount by which to increment the item's value. * --- * default: 1 * --- * * [<group>] * : Method for grouping data within the cache which allows the same key to be used across groups. * --- * default: default * --- * * ## EXAMPLES * * # Increase cache value. * $ wp cache incr my_key 2 my_group * 50 */ public function incr( $args, $assoc_args ) { list( $key, $offset, $group ) = $args; $value = wp_cache_incr( $key, $offset, $group ); if ( false === $value ) { WP_CLI::error( 'The value was not incremented.' ); } WP_CLI::print_value( $value, $assoc_args ); } /** * Replaces a value in the object cache, if the value already exists. * * Errors if the value can't be replaced. * * ## OPTIONS * * <key> * : Cache key. * * <value> * : Value to replace. * * [<group>] * : Method for grouping data within the cache which allows the same key to be used across groups. * --- * default: default * --- * * [<expiration>] * : Define how long to keep the value, in seconds. `0` means as long as possible. * --- * default: 0 * --- * * ## EXAMPLES * * # Replace cache. * $ wp cache replace my_key new_value my_group * Success: Replaced object 'my_key' in group 'my_group'. */ public function replace( $args, $assoc_args ) { list( $key, $value, $group, $expiration ) = $args; $result = wp_cache_replace( $key, $value, $group, $expiration ); if ( false === $result ) { WP_CLI::error( "Could not replace object '$key' in group '$group'. Does it not exist?" ); } WP_CLI::success( "Replaced object '$key' in group '$group'." ); } /** * Sets a value to the object cache, regardless of whether it already exists. * * Errors if the value can't be set. * * ## OPTIONS * * <key> * : Cache key. * * <value> * : Value to set on the key. * * [<group>] * : Method for grouping data within the cache which allows the same key to be used across groups. * --- * default: default * --- * * [<expiration>] * : Define how long to keep the value, in seconds. `0` means as long as possible. * --- * default: 0 * --- * * ## EXAMPLES * * # Set cache. * $ wp cache set my_key my_value my_group 300 * Success: Set object 'my_key' in group 'my_group'. */ public function set( $args, $assoc_args ) { list( $key, $value, $group, $expiration ) = $args; $result = wp_cache_set( $key, $value, $group, $expiration ); if ( false === $result ) { WP_CLI::error( "Could not add object '$key' in group '$group'." ); } WP_CLI::success( "Set object '$key' in group '$group'." ); } /** * Attempts to determine which object cache is being used. * * Note that the guesses made by this function are based on the * WP_Object_Cache classes that define the 3rd party object cache extension. * Changes to those classes could render problems with this function's * ability to determine which object cache is being used. * * ## EXAMPLES * * # Check cache type. * $ wp cache type * Default */ public function type( $args, $assoc_args ) { $message = WP_CLI\Utils\wp_get_cache_type(); WP_CLI::line( $message ); } } <?php use WP_CLI\Utils; /** * Adds, gets, and deletes entries in the WordPress Transient Cache. * * By default, the transient cache uses the WordPress database to persist values * between requests. On a single site installation, values are stored in the * `wp_options` table. On a multisite installation, values are stored in the * `wp_options` or the `wp_sitemeta` table, depending on use of the `--network` * flag. * * When a persistent object cache drop-in is installed (e.g. Redis or Memcached), * the transient cache skips the database and simply wraps the WP Object Cache. * * ## EXAMPLES * * # Set transient. * $ wp transient set sample_key "test data" 3600 * Success: Transient added. * * # Get transient. * $ wp transient get sample_key * test data * * # Delete transient. * $ wp transient delete sample_key * Success: Transient deleted. * * # Delete expired transients. * $ wp transient delete --expired * Success: 12 expired transients deleted from the database. * * # Delete all transients. * $ wp transient delete --all * Success: 14 transients deleted from the database. */ class Transient_Command extends WP_CLI_Command { /** * Gets a transient value. * * For a more complete explanation of the transient cache, including the * network|site cache, please see docs for `wp transient`. * * ## OPTIONS * * <key> * : Key for the transient. * * [--format=<format>] * : Render output in a particular format. * --- * default: table * options: * - table * - csv * - json * - yaml * --- * * [--network] * : Get the value of a network|site transient. On single site, this is * is a specially-named cache key. On multisite, this is a global cache * (instead of local to the site). * * ## EXAMPLES * * $ wp transient get sample_key * test data * * $ wp transient get random_key * Warning: Transient with key "random_key" is not set. */ public function get( $args, $assoc_args ) { list( $key ) = $args; $func = Utils\get_flag_value( $assoc_args, 'network' ) ? 'get_site_transient' : 'get_transient'; $value = $func( $key ); if ( false === $value ) { WP_CLI::warning( 'Transient with key "' . $key . '" is not set.' ); exit; } WP_CLI::print_value( $value, $assoc_args ); } /** * Sets a transient value. * * `<expiration>` is the time until expiration, in seconds. * * For a more complete explanation of the transient cache, including the * network|site cache, please see docs for `wp transient`. * * ## OPTIONS * * <key> * : Key for the transient. * * <value> * : Value to be set for the transient. * * [<expiration>] * : Time until expiration, in seconds. * * [--network] * : Set the value of a network|site transient. On single site, this is * is a specially-named cache key. On multisite, this is a global cache * (instead of local to the site). * * ## EXAMPLES * * $ wp transient set sample_key "test data" 3600 * Success: Transient added. */ public function set( $args, $assoc_args ) { list( $key, $value ) = $args; $expiration = Utils\get_flag_value( $args, 2, 0 ); $func = Utils\get_flag_value( $assoc_args, 'network' ) ? 'set_site_transient' : 'set_transient'; if ( $func( $key, $value, $expiration ) ) { WP_CLI::success( 'Transient added.' ); } else { WP_CLI::error( 'Transient could not be set.' ); } } /** * Deletes a transient value. * * For a more complete explanation of the transient cache, including the * network|site cache, please see docs for `wp transient`. * * ## OPTIONS * * [<key>] * : Key for the transient. * * [--network] * : Delete the value of a network|site transient. On single site, this is * is a specially-named cache key. On multisite, this is a global cache * (instead of local to the site). * * [--all] * : Delete all transients. * * [--expired] * : Delete all expired transients. * * ## EXAMPLES * * # Delete transient. * $ wp transient delete sample_key * Success: Transient deleted. * * # Delete expired transients. * $ wp transient delete --expired * Success: 12 expired transients deleted from the database. * * # Delete expired site transients. * $ wp transient delete --expired --network * Success: 1 expired transient deleted from the database. * * # Delete all transients. * $ wp transient delete --all * Success: 14 transients deleted from the database. * * # Delete all site transients. * $ wp transient delete --all --network * Success: 2 transients deleted from the database. * * # Delete all transients in a multsite. * $ wp transient delete --all --network && wp site list --field=url | xargs -n1 -I % wp --url=% transient delete --all */ public function delete( $args, $assoc_args ) { $key = ( ! empty( $args ) ) ? $args[0] : null; $all = Utils\get_flag_value( $assoc_args, 'all' ); $expired = Utils\get_flag_value( $assoc_args, 'expired' ); $network = Utils\get_flag_value( $assoc_args, 'network' ); if ( true === $all ) { $this->delete_all( $network ); return; } elseif ( true === $expired ) { $this->delete_expired( $network ); return; } if ( ! $key ) { WP_CLI::error( 'Please specify transient key, or use --all or --expired.' ); } $func = $network ? 'delete_site_transient' : 'delete_transient'; if ( $func( $key ) ) { WP_CLI::success( 'Transient deleted.' ); } else { $func = Utils\get_flag_value( $assoc_args, 'network' ) ? 'get_site_transient' : 'get_transient'; if ( $func( $key ) ) { WP_CLI::error( 'Transient was not deleted even though the transient appears to exist.' ); } else { WP_CLI::warning( 'Transient was not deleted; however, the transient does not appear to exist.' ); } } } /** * Determines the type of transients implementation. * * Indicates whether the transients API is using an object cache or the * database. * * For a more complete explanation of the transient cache, including the * network|site cache, please see docs for `wp transient`. * * ## EXAMPLES * * $ wp transient type * Transients are saved to the database. */ public function type() { if ( wp_using_ext_object_cache() ) { $message = 'Transients are saved to the object cache.'; } else { $message = 'Transients are saved to the database.'; } WP_CLI::line( $message ); } /** * Lists transients and their values. * * ## OPTIONS * * [--search=<pattern>] * : Use wildcards ( * and ? ) to match transient name. * * [--exclude=<pattern>] * : Pattern to exclude. Use wildcards ( * and ? ) to match transient name. * * [--network] * : Get the values of network|site transients. On single site, this is * a specially-named cache key. On multisite, this is a global cache * (instead of local to the site). * * [--unserialize] * : Unserialize transient values in output. * * [--human-readable] * : Human-readable output for expirations. * * [--fields=<fields>] * : Limit the output to specific object fields. * * [--format=<format>] * : The serialization format for the value. * --- * default: table * options: * - table * - json * - csv * - count * - yaml * --- * * ## AVAILABLE FIELDS * * This field will be displayed by default for each matching option: * * * name * * value * * expiration * * ## EXAMPLES * * # List all transients * $ wp transient list * +------+-------+---------------+ * | name | value | expiration | * +------+-------+---------------+ * | foo | bar | 39 mins | * | foo2 | bar2 | no expiration | * | foo3 | bar2 | expired | * | foo4 | bar4 | 4 hours | * +------+-------+---------------+ * * @subcommand list */ public function list_( $args, $assoc_args ) { global $wpdb; $network = Utils\get_flag_value( $assoc_args, 'network', false ); $unserialize = Utils\get_flag_value( $assoc_args, 'unserialize', false ); $human_readable = Utils\get_flag_value( $assoc_args, 'human-readable', false ); $fields = array( 'name', 'value', 'expiration' ); if ( isset( $assoc_args['fields'] ) ) { $fields = explode( ',', $assoc_args['fields'] ); } $pattern = '%'; $exclude = ''; if ( isset( $assoc_args['search'] ) ) { $pattern = Utils\esc_like( $assoc_args['search'] ); // Substitute wildcards. $pattern = str_replace( array( '*', '?' ), array( '%', '_' ), $pattern ); } if ( isset( $assoc_args['exclude'] ) ) { $exclude = Utils\esc_like( $assoc_args['exclude'] ); // Substitute wildcards. $exclude = str_replace( array( '*', '?' ), array( '%', '_' ), $exclude ); } if ( $network ) { if ( is_multisite() ) { $where = $wpdb->prepare( 'WHERE `meta_key` LIKE %s', Utils\esc_like( '_site_transient_' ) . $pattern ); $where .= $wpdb->prepare( ' AND meta_key NOT LIKE %s', Utils\esc_like( '_site_transient_timeout_' ) . '%' ); if ( $exclude ) { $where .= $wpdb->prepare( ' AND meta_key NOT LIKE %s', Utils\esc_like( '_site_transient_' ) . $exclude ); } $query = "SELECT `meta_key` as `name`, `meta_value` as `value` FROM {$wpdb->sitemeta} {$where}"; } else { $where = $wpdb->prepare( 'WHERE `option_name` LIKE %s', Utils\esc_like( '_site_transient_' ) . $pattern ); $where .= $wpdb->prepare( ' AND option_name NOT LIKE %s', Utils\esc_like( '_site_transient_timeout_' ) . '%' ); if ( $exclude ) { $where .= $wpdb->prepare( ' AND option_name NOT LIKE %s', Utils\esc_like( '_site_transient_' ) . $exclude ); } $query = "SELECT `option_name` as `name`, `option_value` as `value` FROM {$wpdb->options} {$where}"; } } else { $where = $wpdb->prepare( 'WHERE `option_name` LIKE %s', Utils\esc_like( '_transient_' ) . $pattern ); $where .= $wpdb->prepare( ' AND option_name NOT LIKE %s', Utils\esc_like( '_transient_timeout_' ) . '%' ); if ( $exclude ) { $where .= $wpdb->prepare( ' AND option_name NOT LIKE %s', Utils\esc_like( '_transient_' ) . $exclude ); } $query = "SELECT `option_name` as `name`, `option_value` as `value` FROM {$wpdb->options} {$where}"; } // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- Prepared properly above. $results = $wpdb->get_results( $query ); foreach ( $results as $result ) { $result->name = str_replace( array( '_site_transient_', '_transient_' ), '', $result->name ); $result->expiration = $this->get_transient_expiration( $result->name, $network, $human_readable ); if ( $unserialize ) { $result->value = maybe_unserialize( $result->value ); } } $formatter = new \WP_CLI\Formatter( $assoc_args, $fields ); $formatter->display_items( $results ); } /** * Retrieves the expiration time. * * @param string $name Transient name. * @param bool $is_site_transient Optional. Whether this is a site transient. Default false. * @param bool $human_readable Optional. Whether to return the difference between now and the * expiration time in a human-readable format. Default false. * @return string Expiration time string. */ private function get_transient_expiration( $name, $is_site_transient = false, $human_readable = false ) { if ( $is_site_transient ) { if ( is_multisite() ) { $expiration = (int) get_site_option( '_site_transient_timeout_' . $name ); } else { $expiration = (int) get_option( '_site_transient_timeout_' . $name ); } } else { $expiration = (int) get_option( '_transient_timeout_' . $name ); } if ( 0 === $expiration ) { return $human_readable ? 'never expires' : 'false'; } if ( ! $human_readable ) { return $expiration; } $now = time(); if ( $now > $expiration ) { return 'expired'; } return human_time_diff( $now, $expiration ); } /** * Deletes all expired transients. * * Only deletes the expired transients from the database. * * @param bool $network Whether to delete transients or network|site transients. */ private function delete_expired( $network ) { global $wpdb; $count = 0; if ( ! $network ) { $count += $wpdb->query( $wpdb->prepare( "DELETE a, b FROM {$wpdb->options} a, {$wpdb->options} b WHERE a.option_name LIKE %s AND a.option_name NOT LIKE %s AND b.option_name = CONCAT( '_transient_timeout_', SUBSTRING( a.option_name, 12 ) ) AND b.option_value < %d", Utils\esc_like( '_transient_' ) . '%', Utils\esc_like( '_transient_timeout_' ) . '%', time() ) ); } else { if ( ! is_multisite() ) { // Non-Multisite stores site transients in the options table. $count += $wpdb->query( $wpdb->prepare( "DELETE a, b FROM {$wpdb->options} a, {$wpdb->options} b WHERE a.option_name LIKE %s AND a.option_name NOT LIKE %s AND b.option_name = CONCAT( '_site_transient_timeout_', SUBSTRING( a.option_name, 17 ) ) AND b.option_value < %d", Utils\esc_like( '_site_transient_' ) . '%', Utils\esc_like( '_site_transient_timeout_' ) . '%', time() ) ); } else { // Multisite stores site transients in the sitemeta table. $count += $wpdb->query( $wpdb->prepare( "DELETE a, b FROM {$wpdb->sitemeta} a, {$wpdb->sitemeta} b WHERE a.meta_key LIKE %s AND a.meta_key NOT LIKE %s AND b.meta_key = CONCAT( '_site_transient_timeout_', SUBSTRING( a.meta_key, 17 ) ) AND b.meta_value < %d", Utils\esc_like( '_site_transient_' ) . '%', Utils\esc_like( '_site_transient_timeout_' ) . '%', time() ) ); } } // The above queries delete the transient and the transient timeout // thus each transient is counted twice. $count = $count / 2; if ( $count > 0 ) { WP_CLI::success( sprintf( '%d expired %s deleted from the database.', $count, Utils\pluralize( 'transient', $count ) ) ); } else { WP_CLI::success( 'No expired transients found.' ); } if ( wp_using_ext_object_cache() ) { WP_CLI::warning( 'Transients are stored in an external object cache, and this command only deletes those stored in the database. You must flush the cache to delete all transients.' ); } } /** * Deletes all transients. * * Only deletes the transients from the database. * * @param bool $network Whether to delete transients or network|site transients. */ private function delete_all( $network ) { global $wpdb; // To ensure proper count values we first delete all transients with a timeout // and then the remaining transients without a timeout. $count = 0; if ( ! $network ) { $deleted = $wpdb->query( $wpdb->prepare( "DELETE a, b FROM {$wpdb->options} a, {$wpdb->options} b WHERE a.option_name LIKE %s AND a.option_name NOT LIKE %s AND b.option_name = CONCAT( '_transient_timeout_', SUBSTRING( a.option_name, 12 ) )", Utils\esc_like( '_transient_' ) . '%', Utils\esc_like( '_transient_timeout_' ) . '%' ) ); $count += $deleted / 2; // Ignore affected rows for timeouts. $count += $wpdb->query( $wpdb->prepare( "DELETE FROM $wpdb->options WHERE option_name LIKE %s", Utils\esc_like( '_transient_' ) . '%' ) ); } else { if ( ! is_multisite() ) { // Non-Multisite stores site transients in the options table. $deleted = $wpdb->query( $wpdb->prepare( "DELETE a, b FROM {$wpdb->options} a, {$wpdb->options} b WHERE a.option_name LIKE %s AND a.option_name NOT LIKE %s AND b.option_name = CONCAT( '_site_transient_timeout_', SUBSTRING( a.option_name, 17 ) )", Utils\esc_like( '_site_transient_' ) . '%', Utils\esc_like( '_site_transient_timeout_' ) . '%' ) ); $count += $deleted / 2; // Ignore affected rows for timeouts. $count += $wpdb->query( $wpdb->prepare( "DELETE FROM $wpdb->options WHERE option_name LIKE %s", Utils\esc_like( '_site_transient_' ) . '%' ) ); } else { // Multisite stores site transients in the sitemeta table. $deleted = $wpdb->query( $wpdb->prepare( "DELETE a, b FROM {$wpdb->sitemeta} a, {$wpdb->sitemeta} b WHERE a.meta_key LIKE %s AND a.meta_key NOT LIKE %s AND b.meta_key = CONCAT( '_site_transient_timeout_', SUBSTRING( a.meta_key, 17 ) )", Utils\esc_like( '_site_transient_' ) . '%', Utils\esc_like( '_site_transient_timeout_' ) . '%' ) ); $count += $deleted / 2; // Ignore affected rows for timeouts. $count += $wpdb->query( $wpdb->prepare( "DELETE FROM $wpdb->sitemeta WHERE meta_key LIKE %s", Utils\esc_like( '_site_transient_' ) . '%' ) ); } } if ( $count > 0 ) { WP_CLI::success( sprintf( '%d %s deleted from the database.', $count, Utils\pluralize( 'transient', $count ) ) ); } else { WP_CLI::success( 'No transients found.' ); } if ( wp_using_ext_object_cache() ) { WP_CLI::warning( 'Transients are stored in an external object cache, and this command only deletes those stored in the database. You must flush the cache to delete all transients.' ); } } } <?php if ( ! class_exists( 'WP_CLI' ) ) { return; } $wpcli_cache_autoloader = dirname( __FILE__ ) . '/vendor/autoload.php'; if ( file_exists( $wpcli_cache_autoloader ) ) { require_once $wpcli_cache_autoloader; } WP_CLI::add_command( 'cache', 'Cache_Command' ); WP_CLI::add_command( 'transient', 'Transient_Command' ); <?php use WP_CLI\Utils; /** * Imports files as attachments, regenerates thumbnails, or lists registered image sizes. * * ## EXAMPLES * * # Re-generate all thumbnails, without confirmation. * $ wp media regenerate --yes * Found 3 images to regenerate. * 1/3 Regenerated thumbnails for "Sydney Harbor Bridge" (ID 760). * 2/3 Regenerated thumbnails for "Boardwalk" (ID 757). * 3/3 Regenerated thumbnails for "Sunburst Over River" (ID 756). * Success: Regenerated 3 of 3 images. * * # Import a local image and set it to be the featured image for a post. * $ wp media import ~/Downloads/image.png --post_id=123 --title="A downloaded picture" --featured_image * Success: Imported file '/home/person/Downloads/image.png' as attachment ID 1753 and attached to post 123 as featured image. * * # List all registered image sizes * $ wp media image-size * +---------------------------+-------+--------+-------+ * | name | width | height | crop | * +---------------------------+-------+--------+-------+ * | full | | | N/A | * | twentyfourteen-full-width | 1038 | 576 | hard | * | large | 1024 | 1024 | soft | * | medium_large | 768 | 0 | soft | * | medium | 300 | 300 | soft | * | thumbnail | 150 | 150 | hard | * +---------------------------+-------+--------+-------+ * * @package wp-cli */ class Media_Command extends WP_CLI_Command { /** * Clear the WP object cache after this many regenerations/imports. * * @var integer */ const WP_CLEAR_OBJECT_CACHE_INTERVAL = 500; /** * Regenerates thumbnails for one or more attachments. * * ## OPTIONS * * [<attachment-id>...] * : One or more IDs of the attachments to regenerate. * * [--image_size=<image_size>] * : Name of the image size to regenerate. Only thumbnails of this image size will be regenerated, thumbnails of other image sizes will not. * * [--skip-delete] * : Skip deletion of the original thumbnails. If your thumbnails are linked from sources outside your control, it's likely best to leave them around. Defaults to false. * * [--only-missing] * : Only generate thumbnails for images missing image sizes. * * [--yes] * : Answer yes to the confirmation message. Confirmation only shows when no IDs passed as arguments. * * ## EXAMPLES * * # Regenerate thumbnails for given attachment IDs. * $ wp media regenerate 123 124 125 * Found 3 images to regenerate. * 1/3 Regenerated thumbnails for "Vertical Image" (ID 123). * 2/3 Regenerated thumbnails for "Horizontal Image" (ID 124). * 3/3 Regenerated thumbnails for "Beautiful Picture" (ID 125). * Success: Regenerated 3 of 3 images. * * # Regenerate all thumbnails, without confirmation. * $ wp media regenerate --yes * Found 3 images to regenerate. * 1/3 Regenerated thumbnails for "Sydney Harbor Bridge" (ID 760). * 2/3 Regenerated thumbnails for "Boardwalk" (ID 757). * 3/3 Regenerated thumbnails for "Sunburst Over River" (ID 756). * Success: Regenerated 3 of 3 images. * * # Re-generate all thumbnails that have IDs between 1000 and 2000. * $ seq 1000 2000 | xargs wp media regenerate * Found 4 images to regenerate. * 1/4 Regenerated thumbnails for "Vertical Featured Image" (ID 1027). * 2/4 Regenerated thumbnails for "Horizontal Featured Image" (ID 1022). * 3/4 Regenerated thumbnails for "Unicorn Wallpaper" (ID 1045). * 4/4 Regenerated thumbnails for "I Am Worth Loving Wallpaper" (ID 1023). * Success: Regenerated 4 of 4 images. * * # Re-generate only the thumbnails of "large" image size for all images. * $ wp media regenerate --image_size=large * Do you really want to regenerate the "large" image size for all images? [y/n] y * Found 3 images to regenerate. * 1/3 Regenerated "large" thumbnail for "Sydney Harbor Bridge" (ID 760). * 2/3 No "large" thumbnail regeneration needed for "Boardwalk" (ID 757). * 3/3 Regenerated "large" thumbnail for "Sunburst Over River" (ID 756). * Success: Regenerated 3 of 3 images. */ public function regenerate( $args, $assoc_args = array() ) { $assoc_args = wp_parse_args( $assoc_args, [ 'image_size' => '' ] ); $image_size = $assoc_args['image_size']; if ( $image_size && ! in_array( $image_size, get_intermediate_image_sizes(), true ) ) { WP_CLI::error( sprintf( 'Unknown image size "%s".', $image_size ) ); } if ( empty( $args ) ) { if ( $image_size ) { WP_CLI::confirm( sprintf( 'Do you really want to regenerate the "%s" image size for all images?', $image_size ), $assoc_args ); } else { WP_CLI::confirm( 'Do you really want to regenerate all images?', $assoc_args ); } } $skip_delete = Utils\get_flag_value( $assoc_args, 'skip-delete' ); $only_missing = Utils\get_flag_value( $assoc_args, 'only-missing' ); if ( $only_missing ) { $skip_delete = true; } $additional_mime_types = array(); if ( Utils\wp_version_compare( '4.7', '>=' ) ) { $additional_mime_types[] = 'application/pdf'; } $images = $this->get_images( $args, $additional_mime_types ); $count = $images->post_count; if ( ! $count ) { WP_CLI::warning( 'No images found.' ); return; } WP_CLI::log( sprintf( 'Found %1$d %2$s to regenerate.', $count, _n( 'image', 'images', $count ) ) ); if ( $image_size ) { $image_size_filters = $this->add_image_size_filters( $image_size ); } $number = 0; $successes = 0; $errors = 0; $skips = 0; foreach ( $images->posts as $post ) { $number++; if ( 0 === $number % self::WP_CLEAR_OBJECT_CACHE_INTERVAL ) { Utils\wp_clear_object_cache(); } $this->process_regeneration( $post->ID, $skip_delete, $only_missing, $image_size, $number . '/' . $count, $successes, $errors, $skips ); } if ( $image_size ) { $this->remove_image_size_filters( $image_size_filters ); } Utils\report_batch_operation_results( 'image', 'regenerate', $count, $successes, $errors, $skips ); } /** * Creates attachments from local files or URLs. * * ## OPTIONS * * <file>... * : Path to file or files to be imported. Supports the glob(3) capabilities of the current shell. * If file is recognized as a URL (for example, with a scheme of http or ftp), the file will be * downloaded to a temp file before being sideloaded. * * [--post_id=<post_id>] * : ID of the post to attach the imported files to. * * [--title=<title>] * : Attachment title (post title field). * * [--caption=<caption>] * : Caption for attachent (post excerpt field). * * [--alt=<alt_text>] * : Alt text for image (saved as post meta). * * [--desc=<description>] * : "Description" field (post content) of attachment post. * * [--skip-copy] * : If set, media files (local only) are imported to the library but not moved on disk. * File names will not be run through wp_unique_filename() with this set. * * [--preserve-filetime] * : Use the file modified time as the post published & modified dates. * Remote files will always use the current time. * * [--featured_image] * : If set, set the imported image as the Featured Image of the post its attached to. * * [--porcelain] * : Output just the new attachment ID. * * ## EXAMPLES * * # Import all jpgs in the current user's "Pictures" directory, not attached to any post. * $ wp media import ~/Pictures/**\/*.jpg * Imported file '/home/person/Pictures/landscape-photo.jpg' as attachment ID 1751. * Imported file '/home/person/Pictures/fashion-icon.jpg' as attachment ID 1752. * Success: Imported 2 of 2 items. * * # Import a local image and set it to be the post thumbnail for a post. * $ wp media import ~/Downloads/image.png --post_id=123 --title="A downloaded picture" --featured_image * Imported file '/home/person/Downloads/image.png' as attachment ID 1753 and attached to post 123 as featured image. * Success: Imported 1 of 1 images. * * # Import a local image, but set it as the featured image for all posts. * # 1. Import the image and get its attachment ID. * # 2. Assign the attachment ID as the featured image for all posts. * $ ATTACHMENT_ID="$(wp media import ~/Downloads/image.png --porcelain)" * $ wp post list --post_type=post --format=ids | xargs -d ' ' -I % wp post meta add % _thumbnail_id $ATTACHMENT_ID * Success: Added custom field. * Success: Added custom field. * * # Import an image from the web. * $ wp media import http://s.wordpress.org/style/images/wp-header-logo.png --title='The WordPress logo' --alt="Semantic personal publishing" * Imported file 'http://s.wordpress.org/style/images/wp-header-logo.png' as attachment ID 1755. * Success: Imported 1 of 1 images. * * # Get the URL for an attachment after import. * $ wp media import http://s.wordpress.org/style/images/wp-header-logo.png --porcelain | xargs -I {} wp post list --post__in={} --field=url --post_type=attachment * http://wordpress-develop.dev/wp-header-logo/ */ public function import( $args, $assoc_args = array() ) { $assoc_args = wp_parse_args( $assoc_args, array( 'title' => '', 'caption' => '', 'alt' => '', 'desc' => '', ) ); // Assume the most generic term $noun = 'item'; // Current site's timezone offset. $gmt_offset = get_option( 'gmt_offset' ); // Use the noun `image` when sure the media file is an image if ( Utils\get_flag_value( $assoc_args, 'featured_image' ) || $assoc_args['alt'] ) { $noun = 'image'; } if ( isset( $assoc_args['post_id'] ) ) { if ( ! get_post( $assoc_args['post_id'] ) ) { WP_CLI::warning( 'Invalid --post_id' ); $assoc_args['post_id'] = false; } } else { $assoc_args['post_id'] = false; } $number = 0; $successes = 0; $errors = 0; foreach ( $args as $file ) { $number++; if ( 0 === $number % self::WP_CLEAR_OBJECT_CACHE_INTERVAL ) { Utils\wp_clear_object_cache(); } // phpcs:ignore WordPress.WP.AlternativeFunctions.parse_url_parse_url -- parse_url will only be used in absence of wp_parse_url. $is_file_remote = function_exists( 'wp_parse_url' ) ? wp_parse_url( $file, PHP_URL_HOST ) : parse_url( $file, PHP_URL_HOST ); $orig_filename = $file; $file_time = ''; if ( empty( $is_file_remote ) ) { if ( ! file_exists( $file ) ) { WP_CLI::warning( "Unable to import file '$file'. Reason: File doesn't exist." ); $errors++; continue; } if ( Utils\get_flag_value( $assoc_args, 'skip-copy' ) ) { $tempfile = $file; } else { $tempfile = $this->make_copy( $file ); } $name = Utils\basename( $file ); if ( Utils\get_flag_value( $assoc_args, 'preserve-filetime' ) ) { $file_time = @filemtime( $file ); } } else { $tempfile = download_url( $file ); if ( is_wp_error( $tempfile ) ) { WP_CLI::warning( sprintf( "Unable to import file '%s'. Reason: %s", $file, implode( ', ', $tempfile->get_error_messages() ) ) ); $errors++; continue; } $name = strtok( Utils\basename( $file ), '?' ); } $file_array = array( 'tmp_name' => $tempfile, 'name' => $name, ); $post_array = array( 'post_title' => $assoc_args['title'], 'post_excerpt' => $assoc_args['caption'], 'post_content' => $assoc_args['desc'], ); if ( ! empty( $file_time ) ) { $post_array['post_date'] = gmdate( 'Y-m-d H:i:s', $file_time + ( $gmt_offset * HOUR_IN_SECONDS ) ); $post_array['post_date_gmt'] = gmdate( 'Y-m-d H:i:s', $file_time ); $post_array['post_modified'] = gmdate( 'Y-m-d H:i:s', $file_time + ( $gmt_offset * HOUR_IN_SECONDS ) ); $post_array['post_modified_gmt'] = gmdate( 'Y-m-d H:i:s', $file_time ); } $post_array = wp_slash( $post_array ); // use image exif/iptc data for title and caption defaults if possible if ( empty( $post_array['post_title'] ) || empty( $post_array['post_excerpt'] ) ) { // @codingStandardsIgnoreStart $image_meta = @wp_read_image_metadata( $tempfile ); // @codingStandardsIgnoreEnd if ( ! empty( $image_meta ) ) { if ( empty( $post_array['post_title'] ) && trim( $image_meta['title'] ) && ! is_numeric( sanitize_title( $image_meta['title'] ) ) ) { $post_array['post_title'] = $image_meta['title']; } if ( empty( $post_array['post_excerpt'] ) && trim( $image_meta['caption'] ) ) { $post_array['post_excerpt'] = $image_meta['caption']; } } } if ( empty( $post_array['post_title'] ) ) { $post_array['post_title'] = preg_replace( '/\.[^.]+$/', '', Utils\basename( $file ) ); } if ( Utils\get_flag_value( $assoc_args, 'skip-copy' ) ) { $wp_filetype = wp_check_filetype( $file, null ); $post_array['post_mime_type'] = $wp_filetype['type']; $post_array['post_status'] = 'inherit'; $success = wp_insert_attachment( $post_array, $file, $assoc_args['post_id'] ); if ( is_wp_error( $success ) ) { WP_CLI::warning( sprintf( "Unable to insert file '%s'. Reason: %s", $orig_filename, implode( ', ', $success->get_error_messages() ) ) ); $errors++; continue; } wp_update_attachment_metadata( $success, wp_generate_attachment_metadata( $success, $file ) ); } else { // Deletes the temporary file. $success = media_handle_sideload( $file_array, $assoc_args['post_id'], $assoc_args['title'], $post_array ); if ( is_wp_error( $success ) ) { WP_CLI::warning( sprintf( "Unable to import file '%s'. Reason: %s", $orig_filename, implode( ', ', $success->get_error_messages() ) ) ); $errors++; continue; } } // Set alt text if ( $assoc_args['alt'] ) { update_post_meta( $success, '_wp_attachment_image_alt', wp_slash( $assoc_args['alt'] ) ); } // Set as featured image, if --post_id and --featured_image are set if ( $assoc_args['post_id'] && Utils\get_flag_value( $assoc_args, 'featured_image' ) ) { update_post_meta( $assoc_args['post_id'], '_thumbnail_id', $success ); } $attachment_success_text = ''; if ( $assoc_args['post_id'] ) { $attachment_success_text = " and attached to post {$assoc_args['post_id']}"; if ( Utils\get_flag_value( $assoc_args, 'featured_image' ) ) { $attachment_success_text .= ' as featured image'; } } if ( Utils\get_flag_value( $assoc_args, 'porcelain' ) ) { WP_CLI::line( $success ); } else { WP_CLI::log( sprintf( "Imported file '%s' as attachment ID %d%s.", $orig_filename, $success, $attachment_success_text ) ); } $successes++; } // Report the result of the operation if ( ! Utils\get_flag_value( $assoc_args, 'porcelain' ) ) { Utils\report_batch_operation_results( $noun, 'import', count( $args ), $successes, $errors ); } elseif ( $errors ) { WP_CLI::halt( 1 ); } } /** * Lists image sizes registered with WordPress. * * ## OPTIONS * * [--fields=<fields>] * : Limit the output to specific fields. Defaults to all fields. * * [--format=<format>] * : Render output in a specific format * --- * default: table * options: * - table * - json * - csv * - yaml * - count * --- * * ## AVAILABLE FIELDS * * These fields will be displayed by default for each image size: * * name * * width * * height * * crop * * ratio * * ## EXAMPLES * * # List all registered image sizes * $ wp media image-size * +---------------------------+-------+--------+-------+-------+ * | name | width | height | crop | ratio | * +---------------------------+-------+--------+-------+-------+ * | full | | | N/A | N/A | * | twentyfourteen-full-width | 1038 | 576 | hard | 173:96| * | large | 1024 | 1024 | soft | N/A | * | medium_large | 768 | 0 | soft | N/A | * | medium | 300 | 300 | soft | N/A | * | thumbnail | 150 | 150 | hard | 1:1 | * +---------------------------+-------+--------+-------+-------+ * * @subcommand image-size */ public function image_size( $args, $assoc_args ) { $assoc_args = array_merge( array( 'fields' => 'name,width,height,crop,ratio', ), $assoc_args ); $sizes = $this->get_registered_image_sizes(); usort( $sizes, function( $a, $b ) { if ( $a['width'] === $b['width'] ) { return 0; } return ( $a['width'] < $b['width'] ) ? 1 : -1; } ); array_unshift( $sizes, array( 'name' => 'full', 'width' => '', 'height' => '', 'crop' => 'N/A', 'ratio' => 'N/A', ) ); WP_CLI\Utils\format_items( $assoc_args['format'], $sizes, explode( ',', $assoc_args['fields'] ) ); } private function get_ratio( $width, $height ) { if ( 0 === $height ) { return "0:{$width}"; } if ( 0 === $width ) { return "{$height}:0"; } $gcd = $this->gcd( $width, $height ); $width_ratio = $width / $gcd; $height_ratio = $height / $gcd; return "{$width_ratio}:{$height_ratio}"; } private function gcd( $num1, $num2 ) { while ( 0 !== $num2 ) { $t = $num1 % $num2; $num1 = $num2; $num2 = $t; } return $num1; } // wp_tempnam() inexplicably forces a .tmp extension, which spoils MIME type detection private function make_copy( $path ) { $dir = get_temp_dir(); $filename = Utils\basename( $path ); if ( empty( $filename ) ) { $filename = time(); } $filename = $dir . wp_unique_filename( $dir, $filename ); if ( ! copy( $path, $filename ) ) { WP_CLI::error( "Could not create temporary file for $path." ); } return $filename; } private function process_regeneration( $id, $skip_delete, $only_missing, $image_size, $progress, &$successes, &$errors, &$skips ) { $title = get_the_title( $id ); if ( '' === $title ) { // If audio or video cover art then the id is the sub attachment id, which has no title. if ( metadata_exists( 'post', $id, '_cover_hash' ) ) { // Unfortunately the only way to get the attachment title would be to do a non-indexed query against the meta value of `_thumbnail_id`. So don't. $att_desc = sprintf( 'cover attachment (ID %d)', $id ); } else { $att_desc = sprintf( '"(no title)" (ID %d)', $id ); } } else { $att_desc = sprintf( '"%1$s" (ID %2$d)', $title, $id ); } $thumbnail_desc = $image_size ? sprintf( '"%s" thumbnail', $image_size ) : 'thumbnail'; $fullsizepath = $this->get_attached_file( $id ); if ( false === $fullsizepath || ! file_exists( $fullsizepath ) ) { WP_CLI::warning( "Can't find $att_desc." ); $errors++; return; } $is_pdf = 'application/pdf' === get_post_mime_type( $id ); $needs_regeneration = $this->needs_regeneration( $id, $fullsizepath, $is_pdf, $image_size, $skip_delete, $skip_it ); if ( $skip_it ) { WP_CLI::log( "$progress Skipped $thumbnail_desc regeneration for $att_desc." ); $skips++; return; } if ( $only_missing && ! $needs_regeneration ) { WP_CLI::log( "$progress No $thumbnail_desc regeneration needed for $att_desc." ); $successes++; return; } $metadata = wp_generate_attachment_metadata( $id, $fullsizepath ); if ( is_wp_error( $metadata ) ) { WP_CLI::warning( sprintf( '%s (ID %d)', $metadata->get_error_message(), $id ) ); WP_CLI::log( "$progress Couldn't regenerate thumbnails for $att_desc." ); $errors++; return; } // Note it's possible for no metadata to be generated for PDFs if restricted to a specific image size. if ( empty( $metadata ) && ! ( $is_pdf && $image_size ) ) { WP_CLI::warning( sprintf( 'No metadata. (ID %d)', $id ) ); WP_CLI::log( "$progress Couldn't regenerate thumbnails for $att_desc." ); $errors++; return; } if ( $image_size ) { if ( $this->update_attachment_metadata_for_image_size( $id, $metadata, $image_size ) ) { WP_CLI::log( "$progress Regenerated $thumbnail_desc for $att_desc." ); } else { WP_CLI::log( "$progress No $thumbnail_desc regeneration needed for $att_desc." ); } } else { wp_update_attachment_metadata( $id, $metadata ); WP_CLI::log( "$progress Regenerated thumbnails for $att_desc." ); } $successes++; } private function remove_old_images( $metadata, $fullsizepath, $image_size ) { if ( empty( $metadata['sizes'] ) ) { return; } if ( $image_size ) { if ( empty( $metadata['sizes'][ $image_size ] ) ) { return; } $metadata['sizes'] = array( $image_size => $metadata['sizes'][ $image_size ] ); } $dir_path = dirname( $fullsizepath ) . '/'; foreach ( $metadata['sizes'] as $size_info ) { $intermediate_path = $dir_path . $size_info['file']; if ( $intermediate_path === $fullsizepath ) { continue; } if ( file_exists( $intermediate_path ) ) { unlink( $intermediate_path ); } } } private function needs_regeneration( $att_id, $fullsizepath, $is_pdf, $image_size, $skip_delete, &$skip_it ) { // Assume not skipping. $skip_it = false; // Note: zero-length string returned if no metadata, for instance if PDF or non-standard image (eg an SVG). $metadata = wp_get_attachment_metadata( $att_id ); $image_sizes = $this->get_intermediate_image_sizes_for_attachment( $fullsizepath, $is_pdf, $metadata, $att_id ); // First check if no applicable editor currently available (non-destructive - ie old thumbnails not removed). if ( is_wp_error( $image_sizes ) && 'image_no_editor' === $image_sizes->get_error_code() ) { // Warn unless PDF or non-standard image. if ( ! $is_pdf && is_array( $metadata ) && ! empty( $metadata['sizes'] ) ) { WP_CLI::warning( sprintf( '%s (ID %d)', $image_sizes->get_error_message(), $att_id ) ); } $skip_it = true; return false; } // If uploaded when applicable image editor such as Imagick unavailable, the metadata or sizes metadata may not exist. if ( ! is_array( $metadata ) ) { $metadata = array(); } // If set `$metadata['sizes']` should be array but explicitly check as following code depends on it. if ( ! isset( $metadata['sizes'] ) || ! is_array( $metadata['sizes'] ) ) { $metadata['sizes'] = array(); } // Remove any old thumbnails (so now destructive). if ( ! $skip_delete ) { $this->remove_old_images( $metadata, $fullsizepath, $image_size ); } // Check for any other error (such as load error) apart from no editor available. if ( is_wp_error( $image_sizes ) ) { // Warn but assume it may be possible to regenerate and allow processing to continue and possibly fail. WP_CLI::warning( sprintf( '%s (ID %d)', $image_sizes->get_error_message(), $att_id ) ); return true; } // Have sizes - check whether there're new ones or they've changed. Note that an attachment can have no sizes if it's on or below the thumbnail threshold. if ( $image_size ) { if ( empty( $image_sizes[ $image_size ] ) ) { return false; } if ( empty( $metadata['sizes'][ $image_size ] ) ) { return true; } $metadata['sizes'] = array( $image_size => $metadata['sizes'][ $image_size ] ); } if ( $this->image_sizes_differ( $image_sizes, $metadata['sizes'] ) ) { return true; } $dir_path = dirname( $fullsizepath ) . '/'; // Check that the thumbnail files exist. foreach ( $metadata['sizes'] as $size_info ) { $intermediate_path = $dir_path . $size_info['file']; if ( $intermediate_path === $fullsizepath ) { continue; } if ( ! file_exists( $intermediate_path ) ) { return true; } } return false; } // Whether there's new image sizes or the width/height of existing image sizes have changed. private function image_sizes_differ( $image_sizes, $meta_sizes ) { // Check if have new image size(s). if ( array_diff( array_keys( $image_sizes ), array_keys( $meta_sizes ) ) ) { return true; } // Check if image sizes have changed. foreach ( $image_sizes as $name => $image_size ) { if ( $image_size['width'] !== $meta_sizes[ $name ]['width'] || $image_size['height'] !== $meta_sizes[ $name ]['height'] ) { return true; } } return false; } // Like WP's get_intermediate_image_sizes(), but removes sizes that won't be generated for a particular attachment due to its being on or below their thresholds, // and returns associative array with size name => width/height entries, resolved to crop values if applicable. private function get_intermediate_image_sizes_for_attachment( $fullsizepath, $is_pdf, $metadata, $att_id ) { // Need to get width, height of attachment for image_resize_dimensions(). $editor = wp_get_image_editor( $fullsizepath ); if ( is_wp_error( $editor ) ) { return $editor; } $result = $editor->load(); if ( is_wp_error( $result ) ) { unset( $editor ); return $result; } list( $width, $height ) = array_values( $editor->get_size() ); unset( $editor ); $sizes = array(); foreach ( $this->get_intermediate_sizes( $is_pdf, $metadata, $att_id ) as $name => $size ) { // Need to check destination and original width or height differ before calling image_resize_dimensions(), otherwise it will return non-false. $dims = image_resize_dimensions( $width, $height, $size['width'], $size['height'], $size['crop'] ); if ( ( $width !== $size['width'] || $height !== $size['height'] ) && $dims ) { list( $dst_x, $dst_y, $src_x, $src_y, $dst_w, $dst_h, $src_w, $src_h ) = $dims; $sizes[ $name ] = array( 'width' => $dst_w, 'height' => $dst_h, ); } } return $sizes; } // Like WP's get_intermediate_image_sizes(), but returns associative array with name => size info entries (and caters for PDFs also). private function get_intermediate_sizes( $is_pdf, $metadata, $att_id ) { if ( $is_pdf ) { // Copied from wp_generate_attachment_metadata() in "wp-admin/includes/image.php". $fallback_sizes = array( 'thumbnail', 'medium', 'large', ); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- Calling native WordPress hook. $intermediate_image_sizes = apply_filters( 'fallback_intermediate_image_sizes', $fallback_sizes, $metadata ); } else { $intermediate_image_sizes = get_intermediate_image_sizes(); } // Adapted from wp_generate_attachment_metadata() in "wp-admin/includes/image.php". if ( function_exists( 'wp_get_additional_image_sizes' ) ) { $_wp_additional_image_sizes = wp_get_additional_image_sizes(); } else { // For WP < 4.7.0. global $_wp_additional_image_sizes; if ( ! $_wp_additional_image_sizes ) { // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited -- Used as a fallback for WordPress version less than 4.7.0 as function wp_get_additional_image_sizes didn't exist then. $_wp_additional_image_sizes = array(); } } $sizes = array(); foreach ( $intermediate_image_sizes as $s ) { if ( isset( $_wp_additional_image_sizes[ $s ]['width'] ) ) { $sizes[ $s ]['width'] = (int) $_wp_additional_image_sizes[ $s ]['width']; } else { $sizes[ $s ]['width'] = (int) get_option( "{$s}_size_w" ); } if ( isset( $_wp_additional_image_sizes[ $s ]['height'] ) ) { $sizes[ $s ]['height'] = (int) $_wp_additional_image_sizes[ $s ]['height']; } else { $sizes[ $s ]['height'] = (int) get_option( "{$s}_size_h" ); } if ( isset( $_wp_additional_image_sizes[ $s ]['crop'] ) ) { $sizes[ $s ]['crop'] = (bool) $_wp_additional_image_sizes[ $s ]['crop']; } else { // Force PDF thumbnails to be soft crops. if ( $is_pdf && 'thumbnail' === $s ) { $sizes[ $s ]['crop'] = false; } else { $sizes[ $s ]['crop'] = (bool) get_option( "{$s}_crop" ); } } } // Check here that not PDF (as filter not applied in core if is) and `$metadata` is array (as may not be and filter only applied in core when is). if ( ! $is_pdf && is_array( $metadata ) ) { if ( Utils\wp_version_compare( '5.3', '>=' ) ) { // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- Calling native WordPress hook. $sizes = apply_filters( 'intermediate_image_sizes_advanced', $sizes, $metadata, $att_id ); } else { // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- Calling native WordPress hook. $sizes = apply_filters( 'intermediate_image_sizes_advanced', $sizes, $metadata ); } } return $sizes; } // Add filters to only process a particular intermediate image size in wp_generate_attachment_metadata(). private function add_image_size_filters( $image_size ) { $image_size_filters = array(); // For images. $image_size_filters['intermediate_image_sizes_advanced'] = function ( $sizes ) use ( $image_size ) { // $sizes is associative array of name => size info entries. if ( isset( $sizes[ $image_size ] ) ) { return array( $image_size => $sizes[ $image_size ] ); } return array(); }; // For PDF previews. $image_size_filters['fallback_intermediate_image_sizes'] = function ( $fallback_sizes ) use ( $image_size ) { // $fallback_sizes is indexed array of size names. if ( in_array( $image_size, $fallback_sizes, true ) ) { return array( $image_size ); } return array(); }; foreach ( $image_size_filters as $name => $filter ) { add_filter( $name, $filter, PHP_INT_MAX ); } return $image_size_filters; } // Remove above intermediate image size filters. private function remove_image_size_filters( $image_size_filters ) { foreach ( $image_size_filters as $name => $filter ) { remove_filter( $name, $filter, PHP_INT_MAX ); } } // Update attachment sizes metadata just for a particular intermediate image size. private function update_attachment_metadata_for_image_size( $id, $new_metadata, $image_size ) { $metadata = wp_get_attachment_metadata( $id ); if ( ! is_array( $metadata ) ) { return false; } // If have metadata for image_size. if ( ! empty( $new_metadata['sizes'][ $image_size ] ) ) { $metadata['sizes'][ $image_size ] = $new_metadata['sizes'][ $image_size ]; wp_update_attachment_metadata( $id, $metadata ); return true; } // Else remove unused metadata if any. if ( ! empty( $metadata['sizes'][ $image_size ] ) ) { unset( $metadata['sizes'][ $image_size ] ); wp_update_attachment_metadata( $id, $metadata ); // Treat removing unused metadata as no change. } return false; } /** * Get images from the installation. * * @param array $args The query arguments to use. Optional. * @param array $additional_mime_types The additional mime types to search for. Optional. * * @return WP_Query The query result. */ private function get_images( $args = array(), $additional_mime_types = array() ) { $mime_types = array_merge( array( 'image' ), $additional_mime_types ); $query_args = array( 'post_type' => 'attachment', 'post__in' => $args, 'post_mime_type' => $mime_types, 'post_status' => 'any', 'posts_per_page' => -1, ); return new WP_Query( $query_args ); } /** * Get the metadata for the passed intermediate image size. * * @param string $size The image size to get the metadata for. * * @return array The image size metadata. */ private function get_intermediate_size_metadata( $size ) { $width = intval( get_option( "{$size}_size_w" ) ); $height = intval( get_option( "{$size}_size_h" ) ); $crop = get_option( "{$size}_crop" ); return array( 'name' => $size, 'width' => $width, 'height' => $height, 'crop' => false !== $crop ? 'hard' : 'soft', 'ratio' => false !== $crop ? $this->get_ratio( $width, $height ) : 'N/A', ); } /** * Get all the registered image sizes along with their dimensions. * * @global array $_wp_additional_image_sizes The additional image sizes to parse. * * @link https://wordpress.stackexchange.com/a/251602 Original solution. * * @return array $image_sizes The image sizes */ private function get_registered_image_sizes() { global $_wp_additional_image_sizes; $image_sizes = array(); $default_image_sizes = get_intermediate_image_sizes(); foreach ( $default_image_sizes as $size ) { $image_sizes[] = $this->get_intermediate_size_metadata( $size ); } if ( is_array( $_wp_additional_image_sizes ) ) { foreach ( $_wp_additional_image_sizes as $size => $size_args ) { $crop = filter_var( $size_args['crop'], FILTER_VALIDATE_BOOLEAN ); $image_sizes[] = array( 'name' => $size, 'width' => $size_args['width'], 'height' => $size_args['height'], 'crop' => empty( $crop ) || is_array( $size_args['crop'] ) ? 'soft' : 'hard', 'ratio' => empty( $crop ) || is_array( $size_args['crop'] ) ? 'N/A' : $this->get_ratio( $size_args['width'], $size_args['height'] ), ); } } return $image_sizes; } /** * Fix image orientation for one or more attachments. * * ## OPTIONS * * [<attachment-id>...] * : One or more IDs of the attachments to regenerate. * * [--dry-run] * : Check images needing orientation without performing the operation. * * ## EXAMPLES * * # Fix orientation for all images. * $ wp media fix-orientation * 1/3 Fixing orientation for "Landscape_4" (ID 62). * 2/3 Fixing orientation for "Landscape_3" (ID 61). * 3/3 Fixing orientation for "Landscape_2" (ID 60). * Success: Fixed 3 of 3 images. * * # Fix orientation dry run. * $ wp media fix-orientation 63 -dry run * 1/1 "Portrait_6" (ID 63) will be affected. * Success: 1 of 1 image will be affected. * * # Fix orientation for specific images. * $ wp media fix-orientation 63 * 1/1 Fixing orientation for "Portrait_6" (ID 63). * Success: Fixed 1 of 1 images. * * @subcommand fix-orientation */ public function fix_orientation( $args, $assoc_args ) { // EXIF is required to read image metadata for orientation. if ( ! extension_loaded( 'exif' ) ) { WP_CLI::error( "'EXIF' extension is not loaded, it is required for this operation." ); } elseif ( ! function_exists( 'exif_read_data' ) ) { WP_CLI::error( "Function 'exif_read_data' does not exist, it is required for this operation." ); } $images = $this->get_images( $args ); $count = $images->post_count; $dry_run = Utils\get_flag_value( $assoc_args, 'dry-run' ); if ( ! $count ) { WP_CLI::error( 'No images found.' ); } $number = 0; $successes = 0; $errors = 0; foreach ( $images->posts as $post ) { $number++; if ( 0 === $number % self::WP_CLEAR_OBJECT_CACHE_INTERVAL ) { Utils\wp_clear_object_cache(); } $this->process_orientation_fix( $post->ID, "{$number}/{$count}", $successes, $errors, $dry_run ); } if ( Utils\get_flag_value( $assoc_args, 'dry-run' ) ) { WP_CLI::success( sprintf( '%s of %s %s will be affected.', $successes, $count, Utils\pluralize( 'image', $count ) ) ); } else { Utils\report_batch_operation_results( 'image', 'fix', $count, $successes, $errors ); } } /** * Perform orientation fix on attachments. * * @param int $id Attachment Id. * @param string $progress Current progress string. * @param int $successes Count of success in current operation. * @param int $errors Count of errors in current operation. * @param bool $dry_run Is this a dry run? */ private function process_orientation_fix( $id, $progress, &$successes, &$errors, $dry_run ) { $title = get_the_title( $id ); if ( '' === $title ) { // If audio or video cover art then the id is the sub attachment id, which has no title. if ( metadata_exists( 'post', $id, '_cover_hash' ) ) { // Unfortunately the only way to get the attachment title would be to do a non-indexed query against the meta value of `_thumbnail_id`. So don't. $att_desc = sprintf( 'cover attachment (ID %d)', $id ); } else { $att_desc = sprintf( '"(no title)" (ID %d)', $id ); } } else { $att_desc = sprintf( '"%1$s" (ID %2$d)', $title, $id ); } $full_size_path = $this->get_attached_file( $id ); if ( false === $full_size_path || ! file_exists( $full_size_path ) ) { WP_CLI::warning( "Can't find {$att_desc}." ); $errors++; return; } // Get current metadata of the attachment. $metadata = wp_generate_attachment_metadata( $id, $full_size_path ); $image_meta = ! empty( $metadata['image_meta'] ) ? $metadata['image_meta'] : []; if ( isset( $image_meta['orientation'] ) && absint( $image_meta['orientation'] ) > 1 ) { if ( ! $dry_run ) { WP_CLI::log( "{$progress} Fixing orientation for {$att_desc}." ); if ( false !== $this->flip_rotate_image( $id, $metadata, $image_meta, $full_size_path ) ) { $successes++; } else { $errors++; WP_CLI::log( "Couldn't fix orientation for {$att_desc}." ); } } else { WP_CLI::log( "{$progress} {$att_desc} will be affected." ); $successes++; } } else { WP_CLI::log( "{$progress} No orientation fix required for {$att_desc}." ); } } /** * Perform image rotate operations on the image. * * @param int $id Attachment Id. * @param array $metadata Attachment Metadata. * @param array $image_meta `image_meta` information for the attachment. * @param string $full_size_path Path to original image. * * @return bool Whether the image rotation operation succeeded. */ private function flip_rotate_image( $id, $metadata, $image_meta, $full_size_path ) { $editor = wp_get_image_editor( $full_size_path ); if ( ! is_wp_error( $editor ) ) { $operations = $this->calculate_transformation( (int) $image_meta['orientation'] ); // Rotate image if required. if ( true === $operations['rotate'] ) { $editor->rotate( $operations['degree'] ); } // Flip image if required. if ( false !== $operations['flip'] ) { $editor->flip( $operations['flip'][0], $operations['flip'][1] ); } // Save the image and generate metadata. $editor->save( $full_size_path ); $metadata = wp_generate_attachment_metadata( $id, $full_size_path ); $image_meta = empty( $metadata['image_meta'] ) ? [] : $metadata['image_meta']; // Update attachment metadata with newly generated data. wp_update_attachment_metadata( $id, $metadata ); if ( isset( $image_meta['orientation'] ) && absint( $image_meta['orientation'] ) === 0 ) { return true; } } return false; } /** * Return array of operations to be done for provided orientation value. * * @param int $orientation EXIF orientation value. * * @return array */ private function calculate_transformation( $orientation ) { $rotate = false; $flip = false; $degree = 0; switch ( $orientation ) { case 2: $flip = [ false, true ]; // $flip image along given axis [ horizontal, vertical ] break; case 3: $flip = [ true, true ]; break; case 4: $flip = [ true, false ]; break; case 5: $degree = -90; $rotate = true; $flip = [ false, true ]; break; case 6: $degree = -90; $rotate = true; break; case 7: $degree = 90; $rotate = true; $flip = [ false, true ]; break; case 8: $degree = 90; $rotate = true; break; default: $degree = 0; $rotate = true; break; } return [ 'flip' => $flip, 'degree' => $degree, 'rotate' => $rotate, ]; } /** * Add compatibility indirection to get_attached_file(). * * In WordPress 5.3, behavior changed to account for automatic resizing of * big image files. * * @see https://core.trac.wordpress.org/ticket/47873 * * @param int $attachment_id ID of the attachment to get the filepath for. * @return string|false Filepath of the attachment, or false if not found. */ private function get_attached_file( $attachment_id ) { if ( function_exists( 'wp_get_original_image_path' ) ) { $filepath = wp_get_original_image_path( $attachment_id ); if ( false !== $filepath ) { return $filepath; } } return get_attached_file( $attachment_id ); } } <?php if ( ! class_exists( 'WP_CLI' ) ) { return; } $wpcli_media_autoloader = dirname( __FILE__ ) . '/vendor/autoload.php'; if ( file_exists( $wpcli_media_autoloader ) ) { require_once $wpcli_media_autoloader; } /** * Check for Imagick and GD extensions availability. * * @throws \WP_CLI\ExitException */ $wpcli_media_assert_image_editor_support = function () { if ( ! wp_image_editor_supports() ) { WP_CLI::error( 'No support for generating images found. ' . 'Please install the Imagick or GD PHP extensions.' ); } }; WP_CLI::add_command( 'media', 'Media_Command', [ 'before_invoke' => $wpcli_media_assert_image_editor_support ] ); <?php namespace WP_CLI\Fetchers; /** * Fetch a WordPress plugin based on one of its attributes. * * This is a special version of the plugin fetcher. It doesn't use the * `all_plugins` filter, so that plugins cannot hide themselves from the * checks. * */ class UnfilteredPlugin extends Base { /** * @var string $msg Error message to use when invalid data is provided */ protected $msg = "The '%s' plugin could not be found."; /** * Get a plugin object by name. * * @param string $name * * @return object|false */ public function get( $name ) { foreach ( get_plugins() as $file => $_ ) { if ( "{$name}.php" === $file || ( $name && $file === $name ) || ( dirname( $file ) === $name && '.' !== $name ) ) { return (object) compact( 'name', 'file' ); } } return false; } } <?php use WP_CLI\Utils; /** * Base command that all checksum commands rely on. * * @package wp-cli */ class Checksum_Base_Command extends WP_CLI_Command { /** * Normalizes directory separators to slashes. * * @param string $path Path to convert. * * @return string Path with all backslashes replaced by slashes. */ public static function normalize_directory_separators( $path ) { return str_replace( '\\', '/', $path ); } /** * Read a remote file and return its contents. * * @param string $url URL of the remote file to read. * * @return mixed */ protected static function _read( $url ) { // phpcs:ignore PSR2.Methods.MethodDeclaration.Underscore -- Could be used in classes extending this class. $headers = array( 'Accept' => 'application/json' ); $response = Utils\http_request( 'GET', $url, null, $headers, array( 'timeout' => 30 ) ); if ( 200 === $response->status_code ) { return $response->body; } WP_CLI::error( "Couldn't fetch response from {$url} (HTTP code {$response->status_code})." ); } /** * Recursively get the list of files for a given path. * * @param string $path Root path to start the recursive traversal in. * * @return array<string> */ protected function get_files( $path ) { $filtered_files = array(); try { $files = new RecursiveIteratorIterator( new RecursiveCallbackFilterIterator( new RecursiveDirectoryIterator( $path, RecursiveDirectoryIterator::SKIP_DOTS ), function ( $current, $key, $iterator ) use ( $path ) { return $this->filter_file( self::normalize_directory_separators( substr( $current->getPathname(), strlen( $path ) ) ) ); } ), RecursiveIteratorIterator::CHILD_FIRST ); foreach ( $files as $file_info ) { if ( $file_info->isFile() ) { $filtered_files[] = self::normalize_directory_separators( substr( $file_info->getPathname(), strlen( $path ) ) ); } } } catch ( Exception $e ) { WP_CLI::error( $e->getMessage() ); } return $filtered_files; } /** * Whether to include the file in the verification or not. * * Can be overridden in subclasses. * * @param string $filepath Path to a file. * * @return bool */ protected function filter_file( $filepath ) { return true; } } <?php use WP_CLI\Utils; use WP_CLI\WpOrgApi; /** * Verifies core file integrity by comparing to published checksums. * * @package wp-cli */ class Checksum_Core_Command extends Checksum_Base_Command { /** * Verifies WordPress files against WordPress.org's checksums. * * Downloads md5 checksums for the current version from WordPress.org, and * compares those checksums against the currently installed files. * * For security, avoids loading WordPress when verifying checksums. * * If you experience issues verifying from this command, ensure you are * passing the relevant `--locale` and `--version` arguments according to * the values from the `Dashboard->Updates` menu in the admin area of the * site. * * ## OPTIONS * * [--version=<version>] * : Verify checksums against a specific version of WordPress. * * [--locale=<locale>] * : Verify checksums against a specific locale of WordPress. * * [--insecure] * : Retry downloads without certificate validation if TLS handshake fails. Note: This makes the request vulnerable to a MITM attack. * * ## EXAMPLES * * # Verify checksums * $ wp core verify-checksums * Success: WordPress installation verifies against checksums. * * # Verify checksums for given WordPress version * $ wp core verify-checksums --version=4.0 * Success: WordPress installation verifies against checksums. * * # Verify checksums for given locale * $ wp core verify-checksums --locale=en_US * Success: WordPress installation verifies against checksums. * * # Verify checksums for given locale * $ wp core verify-checksums --locale=ja * Warning: File doesn't verify against checksum: wp-includes/version.php * Warning: File doesn't verify against checksum: readme.html * Warning: File doesn't verify against checksum: wp-config-sample.php * Error: WordPress installation doesn't verify against checksums. * * @when before_wp_load */ public function __invoke( $args, $assoc_args ) { $wp_version = ''; $locale = ''; if ( ! empty( $assoc_args['version'] ) ) { $wp_version = $assoc_args['version']; } if ( ! empty( $assoc_args['locale'] ) ) { $locale = $assoc_args['locale']; } if ( empty( $wp_version ) ) { $details = self::get_wp_details(); $wp_version = $details['wp_version']; if ( empty( $locale ) ) { $locale = $details['wp_local_package']; } } $insecure = (bool) Utils\get_flag_value( $assoc_args, 'insecure', false ); $wp_org_api = new WpOrgApi( [ 'insecure' => $insecure ] ); try { $checksums = $wp_org_api->get_core_checksums( $wp_version, empty( $locale ) ? 'en_US' : $locale ); } catch ( Exception $exception ) { WP_CLI::error( $exception ); } if ( ! is_array( $checksums ) ) { WP_CLI::error( "Couldn't get checksums from WordPress.org." ); } $has_errors = false; foreach ( $checksums as $file => $checksum ) { // Skip files which get updated if ( 'wp-content' === substr( $file, 0, 10 ) ) { continue; } if ( ! file_exists( ABSPATH . $file ) ) { WP_CLI::warning( "File doesn't exist: {$file}" ); $has_errors = true; continue; } $md5_file = md5_file( ABSPATH . $file ); if ( $md5_file !== $checksum ) { WP_CLI::warning( "File doesn't verify against checksum: {$file}" ); $has_errors = true; } } $core_checksums_files = array_filter( array_keys( $checksums ), [ $this, 'filter_file' ] ); $core_files = $this->get_files( ABSPATH ); $additional_files = array_diff( $core_files, $core_checksums_files ); if ( ! empty( $additional_files ) ) { foreach ( $additional_files as $additional_file ) { WP_CLI::warning( "File should not exist: {$additional_file}" ); } } if ( ! $has_errors ) { WP_CLI::success( 'WordPress installation verifies against checksums.' ); } else { WP_CLI::error( "WordPress installation doesn't verify against checksums." ); } } /** * Whether to include the file in the verification or not. * * @param string $filepath Path to a file. * * @return bool */ protected function filter_file( $filepath ) { return ( 0 === strpos( $filepath, 'wp-admin/' ) || 0 === strpos( $filepath, 'wp-includes/' ) || 1 === preg_match( '/^wp-(?!config\.php)([^\/]*)$/', $filepath ) ); } /** * Gets version information from `wp-includes/version.php`. * * @return array { * @type string $wp_version The WordPress version. * @type int $wp_db_version The WordPress DB revision. * @type string $tinymce_version The TinyMCE version. * @type string $wp_local_package The TinyMCE version. * } */ private static function get_wp_details() { $versions_path = ABSPATH . 'wp-includes/version.php'; if ( ! is_readable( $versions_path ) ) { WP_CLI::error( "This does not seem to be a WordPress install.\n" . 'Pass --path=`path/to/wordpress` or run `wp core download`.' ); } $version_content = file_get_contents( $versions_path, null, null, 6, 2048 ); $vars = [ 'wp_version', 'wp_db_version', 'tinymce_version', 'wp_local_package' ]; $result = []; foreach ( $vars as $var_name ) { $result[ $var_name ] = self::find_var( $var_name, $version_content ); } return $result; } /** * Searches for the value assigned to variable `$var_name` in PHP code `$code`. * * This is equivalent to matching the `\$VAR_NAME = ([^;]+)` regular expression and returning * the first match either as a `string` or as an `integer` (depending if it's surrounded by * quotes or not). * * @param string $var_name Variable name to search for. * @param string $code PHP code to search in. * * @return int|string|null */ private static function find_var( $var_name, $code ) { $start = strpos( $code, '$' . $var_name . ' = ' ); if ( ! $start ) { return null; } $start = $start + strlen( $var_name ) + 3; $end = strpos( $code, ';', $start ); $value = substr( $code, $start, $end - $start ); return trim( $value, "'" ); } } <?php use WP_CLI\Fetchers; use WP_CLI\Formatter; use WP_CLI\Utils; use WP_CLI\WpOrgApi; /** * Verifies plugin file integrity by comparing to published checksums. * * @package wp-cli */ class Checksum_Plugin_Command extends Checksum_Base_Command { /** * URL template that points to the API endpoint to use. * * @var string */ private $url_template = 'https://downloads.wordpress.org/plugin-checksums/{slug}/{version}.json'; /** * Cached plugin data for all installed plugins. * * @var array|null */ private $plugins_data; /** * Array of detected errors. * * @var array */ private $errors = array(); /** * Verifies plugin files against WordPress.org's checksums. * * ## OPTIONS * * [<plugin>...] * : One or more plugins to verify. * * [--all] * : If set, all plugins will be verified. * * [--strict] * : If set, even "soft changes" like readme.txt changes will trigger * checksum errors. * * [--format=<format>] * : Render output in a specific format. * --- * default: table * options: * - table * - json * - csv * - yaml * - count * --- * * [--insecure] * : Retry downloads without certificate validation if TLS handshake fails. Note: This makes the request vulnerable to a MITM attack. * * ## EXAMPLES * * # Verify the checksums of all installed plugins * $ wp plugin verify-checksums --all * Success: Verified 8 of 8 plugins. * * # Verify the checksums of a single plugin, Akismet in this case * $ wp plugin verify-checksums akismet * Success: Verified 1 of 1 plugins. */ public function __invoke( $args, $assoc_args ) { $fetcher = new Fetchers\UnfilteredPlugin(); $all = (bool) Utils\get_flag_value( $assoc_args, 'all', false ); $strict = (bool) Utils\get_flag_value( $assoc_args, 'strict', false ); $insecure = (bool) Utils\get_flag_value( $assoc_args, 'insecure', false ); $plugins = $fetcher->get_many( $all ? $this->get_all_plugin_names() : $args ); if ( empty( $plugins ) && ! $all ) { WP_CLI::error( 'You need to specify either one or more plugin slugs to check or use the --all flag to check all plugins.' ); } $skips = 0; foreach ( $plugins as $plugin ) { $version = $this->get_plugin_version( $plugin->file ); if ( false === $version ) { WP_CLI::warning( "Could not retrieve the version for plugin {$plugin->name}, skipping." ); $skips++; continue; } $wp_org_api = new WpOrgApi( [ 'insecure' => $insecure ] ); try { $checksums = $wp_org_api->get_plugin_checksums( $plugin->name, $version ); } catch ( Exception $exception ) { $checksums = false; } if ( false === $checksums ) { WP_CLI::warning( "Could not retrieve the checksums for version {$version} of plugin {$plugin->name}, skipping." ); $skips++; continue; } $files = $this->get_plugin_files( $plugin->file ); foreach ( $checksums as $file => $checksum_array ) { if ( ! in_array( $file, $files, true ) ) { $this->add_error( $plugin->name, $file, 'File is missing' ); } } foreach ( $files as $file ) { if ( ! array_key_exists( $file, $checksums ) ) { $this->add_error( $plugin->name, $file, 'File was added' ); continue; } if ( ! $strict && $this->is_soft_change_file( $file ) ) { continue; } $result = $this->check_file_checksum( dirname( $plugin->file ) . '/' . $file, $checksums[ $file ] ); if ( true !== $result ) { $this->add_error( $plugin->name, $file, is_string( $result ) ? $result : 'Checksum does not match' ); } } } if ( ! empty( $this->errors ) ) { $formatter = new Formatter( $assoc_args, array( 'plugin_name', 'file', 'message' ) ); $formatter->display_items( $this->errors ); } $total = count( $plugins ); $failures = count( array_unique( array_column( $this->errors, 'plugin_name' ) ) ); $successes = $total - $failures - $skips; Utils\report_batch_operation_results( 'plugin', 'verify', $total, $successes, $failures, $skips ); } /** * Adds a new error to the array of detected errors. * * @param string $plugin_name Name of the plugin that had the error. * @param string $file Relative path to the file that had the error. * @param string $message Message explaining the error. */ private function add_error( $plugin_name, $file, $message ) { $error['plugin_name'] = $plugin_name; $error['file'] = $file; $error['message'] = $message; $this->errors[] = $error; } /** * Gets the currently installed version for a given plugin. * * @param string $path Relative path to plugin file to get the version for. * * @return string|false Installed version of the plugin, or false if not * found. */ private function get_plugin_version( $path ) { if ( ! isset( $this->plugins_data ) ) { $this->plugins_data = get_plugins(); } if ( ! array_key_exists( $path, $this->plugins_data ) ) { return false; } return $this->plugins_data[ $path ]['Version']; } /** * Gets the names of all installed plugins. * * @return array<string> Names of all installed plugins. */ private function get_all_plugin_names() { $names = array(); foreach ( get_plugins() as $file => $details ) { $names[] = Utils\get_plugin_name( $file ); } return $names; } /** * Gets the list of files that are part of the given plugin. * * @param string $path Relative path to the main plugin file. * * @return array<string> Array of files with their relative paths. */ private function get_plugin_files( $path ) { $folder = dirname( $this->get_absolute_path( $path ) ); // Return single file plugins immediately, to avoid iterating over the // entire plugins folder. if ( WP_PLUGIN_DIR === $folder ) { return (array) $path; } return $this->get_files( trailingslashit( $folder ) ); } /** * Checks the integrity of a single plugin file by comparing it to the * officially provided checksum. * * @param string $path Relative path to the plugin file to check the * integrity of. * @param array $checksums Array of provided checksums to compare against. * * @return true|string */ private function check_file_checksum( $path, $checksums ) { if ( $this->supports_sha256() && array_key_exists( 'sha256', $checksums ) ) { $sha256 = $this->get_sha256( $this->get_absolute_path( $path ) ); return in_array( $sha256, (array) $checksums['sha256'], true ); } if ( ! array_key_exists( 'md5', $checksums ) ) { return 'No matching checksum algorithm found'; } $md5 = $this->get_md5( $this->get_absolute_path( $path ) ); return in_array( $md5, (array) $checksums['md5'], true ); } /** * Checks whether the current environment supports 256-bit SHA-2. * * Should be supported for PHP 5+, but we might find edge cases depending on * host. * * @return bool */ private function supports_sha256() { return true; } /** * Gets the 256-bit SHA-2 of a given file. * * @param string $filepath Absolute path to the file to calculate the SHA-2 * for. * * @return string */ private function get_sha256( $filepath ) { return hash_file( 'sha256', $filepath ); } /** * Gets the MD5 of a given file. * * @param string $filepath Absolute path to the file to calculate the MD5 * for. * * @return string */ private function get_md5( $filepath ) { return hash_file( 'md5', $filepath ); } /** * Gets the absolute path to a relative plugin file. * * @param string $path Relative path to get the absolute path for. * * @return string */ private function get_absolute_path( $path ) { return WP_PLUGIN_DIR . '/' . $path; } /** * Returns a list of files that only trigger checksum errors in strict mode. * * @return array<string> Array of file names. */ private function get_soft_change_files() { static $files = array( 'readme.txt', 'readme.md', ); return $files; } /** * Checks whether a given file will only trigger checksum errors in strict * mode. * * @param string $file File to check. * * @return bool Whether the file only triggers checksum errors in strict * mode. */ private function is_soft_change_file( $file ) { return in_array( strtolower( $file ), $this->get_soft_change_files(), true ); } } <?php use WP_CLI\Dispatcher\CommandNamespace; /** * Manages plugins, including installs, activations, and updates. */ final class Plugin_Command_Namespace extends CommandNamespace { } <?php use WP_CLI\Dispatcher\CommandNamespace; /** * Downloads, installs, updates, and manages a WordPress installation. */ final class Core_Command_Namespace extends CommandNamespace { } <?php if ( ! class_exists( 'WP_CLI' ) ) { return; } $wpcli_checksum_autoloader = dirname( __FILE__ ) . '/vendor/autoload.php'; if ( file_exists( $wpcli_checksum_autoloader ) ) { require_once $wpcli_checksum_autoloader; } WP_CLI::add_command( 'core', 'Core_Command_Namespace' ); WP_CLI::add_command( 'core verify-checksums', 'Checksum_Core_Command' ); WP_CLI::add_command( 'plugin', 'Plugin_Command_Namespace' ); WP_CLI::add_command( 'plugin verify-checksums', 'Checksum_Plugin_Command' ); <?php namespace Mustangostang; /** * Spyc -- A Simple PHP YAML Class * @version 0.6.2 * @author Vlad Andersen <vlad.andersen@gmail.com> * @author Chris Wanstrath <chris@ozmm.org> * @link https://github.com/mustangostang/spyc/ * @copyright Copyright 2005-2006 Chris Wanstrath, 2006-2011 Vlad Andersen * @license http://www.opensource.org/licenses/mit-license.php MIT License * @package Spyc */ /** * The Simple PHP YAML Class. * * This class can be used to read a YAML file and convert its contents * into a PHP array. It currently supports a very limited subsection of * the YAML spec. * * Usage: * <code> * $Spyc = new Spyc; * $array = $Spyc->load($file); * </code> * or: * <code> * $array = Spyc::YAMLLoad($file); * </code> * or: * <code> * $array = spyc_load_file($file); * </code> * @package Spyc */ class Spyc { // SETTINGS const REMPTY = "\0\0\0\0\0"; /** * Setting this to true will force YAMLDump to enclose any string value in * quotes. False by default. * * @var bool */ public $setting_dump_force_quotes = false; /** * Setting this to true will forse YAMLLoad to use syck_load function when * possible. False by default. * @var bool */ public $setting_use_syck_is_possible = false; /**#@+ * @access private * @var mixed */ private $_dumpIndent; private $_dumpWordWrap; private $_containsGroupAnchor = false; private $_containsGroupAlias = false; private $path; private $result; private $LiteralPlaceHolder = '___YAML_Literal_Block___'; private $SavedGroups = array(); private $indent; /** * Path modifier that should be applied after adding current element. * @var array */ private $delayedPath = array(); /**#@+ * @access public * @var mixed */ public $_nodeId; /** * Load a valid YAML string to Spyc. * @param string $input * @return array */ public function load ($input) { return $this->_loadString($input); } /** * Load a valid YAML file to Spyc. * @param string $file * @return array */ public function loadFile ($file) { return $this->_load($file); } /** * Load YAML into a PHP array statically * * The load method, when supplied with a YAML stream (string or file), * will do its best to convert YAML in a file into a PHP array. Pretty * simple. * Usage: * <code> * $array = Spyc::YAMLLoad('lucky.yaml'); * print_r($array); * </code> * @access public * @return array * @param string $input Path of YAML file or string containing YAML */ public static function YAMLLoad($input) { $Spyc = new Spyc; return $Spyc->_load($input); } /** * Load a string of YAML into a PHP array statically * * The load method, when supplied with a YAML string, will do its best * to convert YAML in a string into a PHP array. Pretty simple. * * Note: use this function if you don't want files from the file system * loaded and processed as YAML. This is of interest to people concerned * about security whose input is from a string. * * Usage: * <code> * $array = Spyc::YAMLLoadString("---\n0: hello world\n"); * print_r($array); * </code> * @access public * @return array * @param string $input String containing YAML */ public static function YAMLLoadString($input) { $Spyc = new Spyc; return $Spyc->_loadString($input); } /** * Dump YAML from PHP array statically * * The dump method, when supplied with an array, will do its best * to convert the array into friendly YAML. Pretty simple. Feel free to * save the returned string as nothing.yaml and pass it around. * * Oh, and you can decide how big the indent is and what the wordwrap * for folding is. Pretty cool -- just pass in 'false' for either if * you want to use the default. * * Indent's default is 2 spaces, wordwrap's default is 40 characters. And * you can turn off wordwrap by passing in 0. * * @access public * @return string * @param array|\stdClass $array PHP array * @param int $indent Pass in false to use the default, which is 2 * @param int $wordwrap Pass in 0 for no wordwrap, false for default (40) * @param bool $no_opening_dashes Do not start YAML file with "---\n" */ public static function YAMLDump($array, $indent = false, $wordwrap = false, $no_opening_dashes = false) { $spyc = new Spyc; return $spyc->dump($array, $indent, $wordwrap, $no_opening_dashes); } /** * Dump PHP array to YAML * * The dump method, when supplied with an array, will do its best * to convert the array into friendly YAML. Pretty simple. Feel free to * save the returned string as tasteful.yaml and pass it around. * * Oh, and you can decide how big the indent is and what the wordwrap * for folding is. Pretty cool -- just pass in 'false' for either if * you want to use the default. * * Indent's default is 2 spaces, wordwrap's default is 40 characters. And * you can turn off wordwrap by passing in 0. * * @access public * @return string * @param array $array PHP array * @param int $indent Pass in false to use the default, which is 2 * @param int $wordwrap Pass in 0 for no wordwrap, false for default (40) */ public function dump($array,$indent = false,$wordwrap = false, $no_opening_dashes = false) { // Dumps to some very clean YAML. We'll have to add some more features // and options soon. And better support for folding. // New features and options. if ($indent === false or !is_numeric($indent)) { $this->_dumpIndent = 2; } else { $this->_dumpIndent = $indent; } if ($wordwrap === false or !is_numeric($wordwrap)) { $this->_dumpWordWrap = 40; } else { $this->_dumpWordWrap = $wordwrap; } // New YAML document $string = ""; if (!$no_opening_dashes) $string = "---\n"; // Start at the base of the array and move through it. if ($array) { $array = (array)$array; $previous_key = -1; foreach ($array as $key => $value) { if (!isset($first_key)) $first_key = $key; $string .= $this->_yamlize($key,$value,0,$previous_key, $first_key, $array); $previous_key = $key; } } return $string; } /** * Attempts to convert a key / value array item to YAML * @access private * @return string * @param $key The name of the key * @param $value The value of the item * @param $indent The indent of the current node */ private function _yamlize($key,$value,$indent, $previous_key = -1, $first_key = 0, $source_array = null) { if(is_object($value)) $value = (array)$value; if (is_array($value)) { if (empty ($value)) return $this->_dumpNode($key, array(), $indent, $previous_key, $first_key, $source_array); // It has children. What to do? // Make it the right kind of item $string = $this->_dumpNode($key, self::REMPTY, $indent, $previous_key, $first_key, $source_array); // Add the indent $indent += $this->_dumpIndent; // Yamlize the array $string .= $this->_yamlizeArray($value,$indent); } elseif (!is_array($value)) { // It doesn't have children. Yip. $string = $this->_dumpNode($key, $value, $indent, $previous_key, $first_key, $source_array); } return $string; } /** * Attempts to convert an array to YAML * @access private * @return string * @param $array The array you want to convert * @param $indent The indent of the current level */ private function _yamlizeArray($array,$indent) { if (is_array($array)) { $string = ''; $previous_key = -1; foreach ($array as $key => $value) { if (!isset($first_key)) $first_key = $key; $string .= $this->_yamlize($key, $value, $indent, $previous_key, $first_key, $array); $previous_key = $key; } return $string; } else { return false; } } /** * Returns YAML from a key and a value * @access private * @return string * @param $key The name of the key * @param $value The value of the item * @param $indent The indent of the current node */ private function _dumpNode($key, $value, $indent, $previous_key = -1, $first_key = 0, $source_array = null) { // do some folding here, for blocks if (is_string ($value) && ((strpos($value,"\n") !== false || strpos($value,": ") !== false || strpos($value,"- ") !== false || strpos($value,"*") !== false || strpos($value,"#") !== false || strpos($value,"<") !== false || strpos($value,">") !== false || strpos ($value, '%') !== false || strpos ($value, ' ') !== false || strpos($value,"[") !== false || strpos($value,"]") !== false || strpos($value,"{") !== false || strpos($value,"}") !== false) || strpos($value,"&") !== false || strpos($value, "'") !== false || strpos($value, "!") === 0 || substr ($value, -1, 1) == ':') ) { $value = $this->_doLiteralBlock($value,$indent); } else { $value = $this->_doFolding($value,$indent); } if ($value === array()) $value = '[ ]'; if ($value === "") $value = '""'; if (self::isTranslationWord($value)) { $value = $this->_doLiteralBlock($value, $indent); } if (trim ($value) != $value) $value = $this->_doLiteralBlock($value,$indent); if (is_bool($value)) { $value = $value ? "true" : "false"; } if ($value === null) $value = 'null'; if ($value === "'" . self::REMPTY . "'") $value = null; $spaces = str_repeat(' ',$indent); //if (is_int($key) && $key - 1 == $previous_key && $first_key===0) { if (is_array ($source_array) && array_keys($source_array) === range(0, count($source_array) - 1)) { // It's a sequence $string = $spaces.'- '.$value."\n"; } else { // if ($first_key===0) throw new \Exception('Keys are all screwy. The first one was zero, now it\'s "'. $key .'"'); // It's mapped if (strpos($key, ":") !== false || strpos($key, "#") !== false) { $key = '"' . $key . '"'; } $string = rtrim ($spaces.$key.': '.$value)."\n"; } return $string; } /** * Creates a literal block for dumping * @access private * @return string * @param $value * @param $indent int The value of the indent */ private function _doLiteralBlock($value,$indent) { if ($value === "\n") return '\n'; if (strpos($value, "\n") === false && strpos($value, "'") === false) { return sprintf ("'%s'", $value); } if (strpos($value, "\n") === false && strpos($value, '"') === false) { return sprintf ('"%s"', $value); } $exploded = explode("\n",$value); $newValue = '|'; if (isset($exploded[0]) && ($exploded[0] == "|" || $exploded[0] == "|-" || $exploded[0] == ">")) { $newValue = $exploded[0]; unset($exploded[0]); } $indent += $this->_dumpIndent; $spaces = str_repeat(' ',$indent); foreach ($exploded as $line) { $line = trim($line); if (strpos($line, '"') === 0 && strrpos($line, '"') == (strlen($line)-1) || strpos($line, "'") === 0 && strrpos($line, "'") == (strlen($line)-1)) { $line = substr($line, 1, -1); } $newValue .= "\n" . $spaces . ($line); } return $newValue; } /** * Folds a string of text, if necessary * @access private * @return string * @param $value The string you wish to fold */ private function _doFolding($value,$indent) { // Don't do anything if wordwrap is set to 0 if ($this->_dumpWordWrap !== 0 && is_string ($value) && strlen($value) > $this->_dumpWordWrap) { $indent += $this->_dumpIndent; $indent = str_repeat(' ',$indent); $wrapped = wordwrap($value,$this->_dumpWordWrap,"\n$indent"); $value = ">\n".$indent.$wrapped; } else { if ($this->setting_dump_force_quotes && is_string ($value) && $value !== self::REMPTY) $value = '"' . $value . '"'; if (is_numeric($value) && is_string($value)) $value = '"' . $value . '"'; } return $value; } private function isTrueWord($value) { $words = self::getTranslations(array('true', 'on', 'yes', 'y')); return in_array($value, $words, true); } private function isFalseWord($value) { $words = self::getTranslations(array('false', 'off', 'no', 'n')); return in_array($value, $words, true); } private function isNullWord($value) { $words = self::getTranslations(array('null', '~')); return in_array($value, $words, true); } private function isTranslationWord($value) { return ( self::isTrueWord($value) || self::isFalseWord($value) || self::isNullWord($value) ); } /** * Coerce a string into a native type * Reference: http://yaml.org/type/bool.html * TODO: Use only words from the YAML spec. * @access private * @param $value The value to coerce */ private function coerceValue(&$value) { if (self::isTrueWord($value)) { $value = true; } else if (self::isFalseWord($value)) { $value = false; } else if (self::isNullWord($value)) { $value = null; } } /** * Given a set of words, perform the appropriate translations on them to * match the YAML 1.1 specification for type coercing. * @param $words The words to translate * @access private */ private static function getTranslations(array $words) { $result = array(); foreach ($words as $i) { $result = array_merge($result, array(ucfirst($i), strtoupper($i), strtolower($i))); } return $result; } // LOADING FUNCTIONS private function _load($input) { $Source = $this->loadFromSource($input); return $this->loadWithSource($Source); } private function _loadString($input) { $Source = $this->loadFromString($input); return $this->loadWithSource($Source); } private function loadWithSource($Source) { if (empty ($Source)) return array(); if ($this->setting_use_syck_is_possible && function_exists ('syck_load')) { $array = syck_load (implode ("\n", $Source)); return is_array($array) ? $array : array(); } $this->path = array(); $this->result = array(); $cnt = count($Source); for ($i = 0; $i < $cnt; $i++) { $line = $Source[$i]; $this->indent = strlen($line) - strlen(ltrim($line)); $tempPath = $this->getParentPathByIndent($this->indent); $line = self::stripIndent($line, $this->indent); if (self::isComment($line)) continue; if (self::isEmpty($line)) continue; $this->path = $tempPath; $literalBlockStyle = self::startsLiteralBlock($line); if ($literalBlockStyle) { $line = rtrim ($line, $literalBlockStyle . " \n"); $literalBlock = ''; $line .= ' '.$this->LiteralPlaceHolder; $literal_block_indent = strlen($Source[$i+1]) - strlen(ltrim($Source[$i+1])); while (++$i < $cnt && $this->literalBlockContinues($Source[$i], $this->indent)) { $literalBlock = $this->addLiteralLine($literalBlock, $Source[$i], $literalBlockStyle, $literal_block_indent); } $i--; } // Strip out comments if (strpos ($line, '#')) { $line = preg_replace('/\s*#([^"\']+)$/','',$line); } while (++$i < $cnt && self::greedilyNeedNextLine($line)) { $line = rtrim ($line, " \n\t\r") . ' ' . ltrim ($Source[$i], " \t"); } $i--; $lineArray = $this->_parseLine($line); if ($literalBlockStyle) $lineArray = $this->revertLiteralPlaceHolder ($lineArray, $literalBlock); $this->addArray($lineArray, $this->indent); foreach ($this->delayedPath as $indent => $delayedPath) $this->path[$indent] = $delayedPath; $this->delayedPath = array(); } return $this->result; } private function loadFromSource ($input) { if (!empty($input) && strpos($input, "\n") === false && file_exists($input)) $input = file_get_contents($input); return $this->loadFromString($input); } private function loadFromString ($input) { $lines = explode("\n",$input); foreach ($lines as $k => $_) { $lines[$k] = rtrim ($_, "\r"); } return $lines; } /** * Parses YAML code and returns an array for a node * @access private * @return array * @param string $line A line from the YAML file */ private function _parseLine($line) { if (!$line) return array(); $line = trim($line); if (!$line) return array(); $array = array(); $group = $this->nodeContainsGroup($line); if ($group) { $this->addGroup($line, $group); $line = $this->stripGroup ($line, $group); } if ($this->startsMappedSequence($line)) return $this->returnMappedSequence($line); if ($this->startsMappedValue($line)) return $this->returnMappedValue($line); if ($this->isArrayElement($line)) return $this->returnArrayElement($line); if ($this->isPlainArray($line)) return $this->returnPlainArray($line); return $this->returnKeyValuePair($line); } /** * Finds the type of the passed value, returns the value as the new type. * @access private * @param string $value * @return mixed */ private function _toType($value) { if ($value === '') return ""; $first_character = $value[0]; $last_character = substr($value, -1, 1); $is_quoted = false; do { if (!$value) break; if ($first_character != '"' && $first_character != "'") break; if ($last_character != '"' && $last_character != "'") break; $is_quoted = true; } while (0); if ($is_quoted) { $value = str_replace('\n', "\n", $value); if ($first_character == "'") return strtr(substr ($value, 1, -1), array ('\'\'' => '\'', '\\\''=> '\'')); return strtr(substr ($value, 1, -1), array ('\\"' => '"', '\\\''=> '\'')); } if (strpos($value, ' #') !== false && !$is_quoted) $value = preg_replace('/\s+#(.+)$/','',$value); if ($first_character == '[' && $last_character == ']') { // Take out strings sequences and mappings $innerValue = trim(substr ($value, 1, -1)); if ($innerValue === '') return array(); $explode = $this->_inlineEscape($innerValue); // Propagate value array $value = array(); foreach ($explode as $v) { $value[] = $this->_toType($v); } return $value; } if (strpos($value,': ')!==false && $first_character != '{') { $array = explode(': ',$value); $key = trim($array[0]); array_shift($array); $value = trim(implode(': ',$array)); $value = $this->_toType($value); return array($key => $value); } if ($first_character == '{' && $last_character == '}') { $innerValue = trim(substr ($value, 1, -1)); if ($innerValue === '') return array(); // Inline Mapping // Take out strings sequences and mappings $explode = $this->_inlineEscape($innerValue); // Propagate value array $array = array(); foreach ($explode as $v) { $SubArr = $this->_toType($v); if (empty($SubArr)) continue; if (is_array ($SubArr)) { $array[key($SubArr)] = $SubArr[key($SubArr)]; continue; } $array[] = $SubArr; } return $array; } if ($value == 'null' || $value == 'NULL' || $value == 'Null' || $value == '' || $value == '~') { return null; } if ( is_numeric($value) && preg_match ('/^(-|)[1-9]+[0-9]*$/', $value) ){ $intvalue = (int)$value; if ($intvalue != PHP_INT_MAX && $intvalue != ~PHP_INT_MAX) $value = $intvalue; return $value; } if ( is_string($value) && preg_match('/^0[xX][0-9a-fA-F]+$/', $value)) { // Hexadecimal value. return hexdec($value); } $this->coerceValue($value); if (is_numeric($value)) { if ($value === '0') return 0; if (rtrim ($value, 0) === $value) $value = (float)$value; return $value; } return $value; } /** * Used in inlines to check for more inlines or quoted strings * @access private * @return array */ private function _inlineEscape($inline) { // There's gotta be a cleaner way to do this... // While pure sequences seem to be nesting just fine, // pure mappings and mappings with sequences inside can't go very // deep. This needs to be fixed. $seqs = array(); $maps = array(); $saved_strings = array(); $saved_empties = array(); // Check for empty strings $regex = '/("")|(\'\')/'; if (preg_match_all($regex,$inline,$strings)) { $saved_empties = $strings[0]; $inline = preg_replace($regex,'YAMLEmpty',$inline); } unset($regex); // Check for strings $regex = '/(?:(")|(?:\'))((?(1)[^"]+|[^\']+))(?(1)"|\')/'; if (preg_match_all($regex,$inline,$strings)) { $saved_strings = $strings[0]; $inline = preg_replace($regex,'YAMLString',$inline); } unset($regex); // echo $inline; $i = 0; do { // Check for sequences while (preg_match('/\[([^{}\[\]]+)\]/U',$inline,$matchseqs)) { $seqs[] = $matchseqs[0]; $inline = preg_replace('/\[([^{}\[\]]+)\]/U', ('YAMLSeq' . (count($seqs) - 1) . 's'), $inline, 1); } // Check for mappings while (preg_match('/{([^\[\]{}]+)}/U',$inline,$matchmaps)) { $maps[] = $matchmaps[0]; $inline = preg_replace('/{([^\[\]{}]+)}/U', ('YAMLMap' . (count($maps) - 1) . 's'), $inline, 1); } if ($i++ >= 10) break; } while (strpos ($inline, '[') !== false || strpos ($inline, '{') !== false); $explode = explode(',',$inline); $explode = array_map('trim', $explode); $stringi = 0; $i = 0; while (1) { // Re-add the sequences if (!empty($seqs)) { foreach ($explode as $key => $value) { if (strpos($value,'YAMLSeq') !== false) { foreach ($seqs as $seqk => $seq) { $explode[$key] = str_replace(('YAMLSeq'.$seqk.'s'),$seq,$value); $value = $explode[$key]; } } } } // Re-add the mappings if (!empty($maps)) { foreach ($explode as $key => $value) { if (strpos($value,'YAMLMap') !== false) { foreach ($maps as $mapk => $map) { $explode[$key] = str_replace(('YAMLMap'.$mapk.'s'), $map, $value); $value = $explode[$key]; } } } } // Re-add the strings if (!empty($saved_strings)) { foreach ($explode as $key => $value) { while (strpos($value,'YAMLString') !== false) { $explode[$key] = preg_replace('/YAMLString/',$saved_strings[$stringi],$value, 1); unset($saved_strings[$stringi]); ++$stringi; $value = $explode[$key]; } } } // Re-add the empties if (!empty($saved_empties)) { foreach ($explode as $key => $value) { while (strpos($value,'YAMLEmpty') !== false) { $explode[$key] = preg_replace('/YAMLEmpty/', '', $value, 1); $value = $explode[$key]; } } } $finished = true; foreach ($explode as $key => $value) { if (strpos($value,'YAMLSeq') !== false) { $finished = false; break; } if (strpos($value,'YAMLMap') !== false) { $finished = false; break; } if (strpos($value,'YAMLString') !== false) { $finished = false; break; } if (strpos($value,'YAMLEmpty') !== false) { $finished = false; break; } } if ($finished) break; $i++; if ($i > 10) break; // Prevent infinite loops. } return $explode; } private function literalBlockContinues ($line, $lineIndent) { if (!trim($line)) return true; if (strlen($line) - strlen(ltrim($line)) > $lineIndent) return true; return false; } private function referenceContentsByAlias ($alias) { do { if (!isset($this->SavedGroups[$alias])) { echo "Bad group name: $alias."; break; } $groupPath = $this->SavedGroups[$alias]; $value = $this->result; foreach ($groupPath as $k) { $value = $value[$k]; } } while (false); return $value; } private function addArrayInline ($array, $indent) { $CommonGroupPath = $this->path; if (empty ($array)) return false; foreach ($array as $k => $_) { $this->addArray(array($k => $_), $indent); $this->path = $CommonGroupPath; } return true; } private function addArray ($incoming_data, $incoming_indent) { // print_r ($incoming_data); if (count ($incoming_data) > 1) return $this->addArrayInline ($incoming_data, $incoming_indent); $key = key ($incoming_data); $value = isset($incoming_data[$key]) ? $incoming_data[$key] : null; if ($key === '__!YAMLZero') $key = '0'; if ($incoming_indent == 0 && !$this->_containsGroupAlias && !$this->_containsGroupAnchor) { // Shortcut for root-level values. if ($key || $key === '' || $key === '0') { $this->result[$key] = $value; } else { $this->result[] = $value; end ($this->result); $key = key ($this->result); } $this->path[$incoming_indent] = $key; return; } $history = array(); // Unfolding inner array tree. $history[] = $_arr = $this->result; foreach ($this->path as $k) { $history[] = $_arr = $_arr[$k]; } if ($this->_containsGroupAlias) { $value = $this->referenceContentsByAlias($this->_containsGroupAlias); $this->_containsGroupAlias = false; } // Adding string or numeric key to the innermost level or $this->arr. if (is_string($key) && $key == '<<') { if (!is_array ($_arr)) { $_arr = array (); } $_arr = array_merge ($_arr, $value); } else if ($key || $key === '' || $key === '0') { if (!is_array ($_arr)) $_arr = array ($key=>$value); else $_arr[$key] = $value; } else { if (!is_array ($_arr)) { $_arr = array ($value); $key = 0; } else { $_arr[] = $value; end ($_arr); $key = key ($_arr); } } $reverse_path = array_reverse($this->path); $reverse_history = array_reverse ($history); $reverse_history[0] = $_arr; $cnt = count($reverse_history) - 1; for ($i = 0; $i < $cnt; $i++) { $reverse_history[$i+1][$reverse_path[$i]] = $reverse_history[$i]; } $this->result = $reverse_history[$cnt]; $this->path[$incoming_indent] = $key; if ($this->_containsGroupAnchor) { $this->SavedGroups[$this->_containsGroupAnchor] = $this->path; if (is_array ($value)) { $k = key ($value); if (!is_int ($k)) { $this->SavedGroups[$this->_containsGroupAnchor][$incoming_indent + 2] = $k; } } $this->_containsGroupAnchor = false; } } private static function startsLiteralBlock ($line) { $lastChar = substr (trim($line), -1); if ($lastChar != '>' && $lastChar != '|') return false; if ($lastChar == '|') return $lastChar; // HTML tags should not be counted as literal blocks. if (preg_match ('#<.*?>$#', $line)) return false; return $lastChar; } private static function greedilyNeedNextLine($line) { $line = trim ($line); if (!strlen($line)) return false; if (substr ($line, -1, 1) == ']') return false; if ($line[0] == '[') return true; if (preg_match ('#^[^:]+?:\s*\[#', $line)) return true; return false; } private function addLiteralLine ($literalBlock, $line, $literalBlockStyle, $indent = -1) { $line = self::stripIndent($line, $indent); if ($literalBlockStyle !== '|') { $line = self::stripIndent($line); } $line = rtrim ($line, "\r\n\t ") . "\n"; if ($literalBlockStyle == '|') { return $literalBlock . $line; } if (strlen($line) == 0) return rtrim($literalBlock, ' ') . "\n"; if ($line == "\n" && $literalBlockStyle == '>') { return rtrim ($literalBlock, " \t") . "\n"; } if ($line != "\n") $line = trim ($line, "\r\n ") . " "; return $literalBlock . $line; } function revertLiteralPlaceHolder ($lineArray, $literalBlock) { foreach ($lineArray as $k => $_) { if (is_array($_)) $lineArray[$k] = $this->revertLiteralPlaceHolder ($_, $literalBlock); else if (substr($_, -1 * strlen ($this->LiteralPlaceHolder)) == $this->LiteralPlaceHolder) $lineArray[$k] = rtrim ($literalBlock, " \r\n"); } return $lineArray; } private static function stripIndent ($line, $indent = -1) { if ($indent == -1) $indent = strlen($line) - strlen(ltrim($line)); return substr ($line, $indent); } private function getParentPathByIndent ($indent) { if ($indent == 0) return array(); $linePath = $this->path; do { end($linePath); $lastIndentInParentPath = key($linePath); if ($indent <= $lastIndentInParentPath) array_pop ($linePath); } while ($indent <= $lastIndentInParentPath); return $linePath; } private function clearBiggerPathValues ($indent) { if ($indent == 0) $this->path = array(); if (empty ($this->path)) return true; foreach ($this->path as $k => $_) { if ($k > $indent) unset ($this->path[$k]); } return true; } private static function isComment ($line) { if (!$line) return false; if ($line[0] == '#') return true; if (trim($line, " \r\n\t") == '---') return true; return false; } private static function isEmpty ($line) { return (trim ($line) === ''); } private function isArrayElement ($line) { if (!$line || !is_scalar($line)) return false; if (substr($line, 0, 2) != '- ') return false; if (strlen ($line) > 3) if (substr($line,0,3) == '---') return false; return true; } private function isHashElement ($line) { return strpos($line, ':'); } private function isLiteral ($line) { if ($this->isArrayElement($line)) return false; if ($this->isHashElement($line)) return false; return true; } private static function unquote ($value) { if (!$value) return $value; if (!is_string($value)) return $value; if ($value[0] == '\'') return trim ($value, '\''); if ($value[0] == '"') return trim ($value, '"'); return $value; } private function startsMappedSequence ($line) { return (substr($line, 0, 2) == '- ' && substr ($line, -1, 1) == ':'); } private function returnMappedSequence ($line) { $array = array(); $key = self::unquote(trim(substr($line,1,-1))); $array[$key] = array(); $this->delayedPath = array(strpos ($line, $key) + $this->indent => $key); return array($array); } private function checkKeysInValue($value) { if (strchr('[{"\'', $value[0]) === false) { if (strchr($value, ': ') !== false) { throw new \Exception('Too many keys: '.$value); } } } private function returnMappedValue ($line) { $this->checkKeysInValue($line); $array = array(); $key = self::unquote (trim(substr($line,0,-1))); $array[$key] = ''; return $array; } private function startsMappedValue ($line) { return (substr ($line, -1, 1) == ':'); } private function isPlainArray ($line) { return ($line[0] == '[' && substr ($line, -1, 1) == ']'); } private function returnPlainArray ($line) { return $this->_toType($line); } private function returnKeyValuePair ($line) { $array = array(); $key = ''; if (strpos ($line, ': ')) { // It's a key/value pair most likely // If the key is in double quotes pull it out if (($line[0] == '"' || $line[0] == "'") && preg_match('/^(["\'](.*)["\'](\s)*:)/',$line,$matches)) { $value = trim(str_replace($matches[1],'',$line)); $key = $matches[2]; } else { // Do some guesswork as to the key and the value $explode = explode(': ', $line); $key = trim(array_shift($explode)); $value = trim(implode(': ', $explode)); $this->checkKeysInValue($value); } // Set the type of the value. Int, string, etc $value = $this->_toType($value); if ($key === '0') $key = '__!YAMLZero'; $array[$key] = $value; } else { $array = array ($line); } return $array; } private function returnArrayElement ($line) { if (strlen($line) <= 1) return array(array()); // Weird %) $array = array(); $value = trim(substr($line,1)); $value = $this->_toType($value); if ($this->isArrayElement($value)) { $value = $this->returnArrayElement($value); } $array[] = $value; return $array; } private function nodeContainsGroup ($line) { $symbolsForReference = 'A-z0-9_\-'; if (strpos($line, '&') === false && strpos($line, '*') === false) return false; // Please die fast ;-) if ($line[0] == '&' && preg_match('/^(&['.$symbolsForReference.']+)/', $line, $matches)) return $matches[1]; if ($line[0] == '*' && preg_match('/^(\*['.$symbolsForReference.']+)/', $line, $matches)) return $matches[1]; if (preg_match('/(&['.$symbolsForReference.']+)$/', $line, $matches)) return $matches[1]; if (preg_match('/(\*['.$symbolsForReference.']+$)/', $line, $matches)) return $matches[1]; if (preg_match ('#^\s*<<\s*:\s*(\*[^\s]+).*$#', $line, $matches)) return $matches[1]; return false; } private function addGroup ($line, $group) { if ($group[0] == '&') $this->_containsGroupAnchor = substr ($group, 1); if ($group[0] == '*') $this->_containsGroupAlias = substr ($group, 1); //print_r ($this->path); } private function stripGroup ($line, $group) { $line = trim(str_replace($group, '', $line)); return $line; } } <?php /** * Spyc -- A Simple PHP YAML Class * @version 0.6.2 * @author Vlad Andersen <vlad.andersen@gmail.com> * @author Chris Wanstrath <chris@ozmm.org> * @link https://github.com/mustangostang/spyc/ * @copyright Copyright 2005-2006 Chris Wanstrath, 2006-2011 Vlad Andersen * @license http://www.opensource.org/licenses/mit-license.php MIT License * @package Spyc */ if (!class_exists('Mustangostang\Spyc')) { require_once dirname(__FILE__) . '/src/Spyc.php'; } class_alias('Mustangostang\Spyc', 'Spyc'); require_once dirname(__FILE__) . '/includes/functions.php'; // Enable use of Spyc from command line // The syntax is the following: php Spyc.php spyc.yaml do { if (PHP_SAPI != 'cli') break; if (empty ($_SERVER['argc']) || $_SERVER['argc'] < 2) break; if (empty ($_SERVER['PHP_SELF']) || FALSE === strpos ($_SERVER['PHP_SELF'], 'Spyc.php') ) break; $file = $argv[1]; echo json_encode (spyc_load_file ($file)); } while (0); <?php /** * Spyc -- A Simple PHP YAML Class * @version 0.6.2 * @author Vlad Andersen <vlad.andersen@gmail.com> * @author Chris Wanstrath <chris@ozmm.org> * @link https://github.com/mustangostang/spyc/ * @copyright Copyright 2005-2006 Chris Wanstrath, 2006-2011 Vlad Andersen * @license http://www.opensource.org/licenses/mit-license.php MIT License * @package Spyc */ if (!function_exists('spyc_load')) { /** * Parses YAML to array. * @param string $string YAML string. * @return array */ function spyc_load ($string) { return Spyc::YAMLLoadString($string); } } if (!function_exists('spyc_load_file')) { /** * Parses YAML to array. * @param string $file Path to YAML file. * @return array */ function spyc_load_file ($file) { return Spyc::YAMLLoad($file); } } if (!function_exists('spyc_dump')) { /** * Dumps array to YAML. * @param array $data Array. * @return string */ function spyc_dump ($data) { return Spyc::YAMLDump($data, false, false, true); } } <?php /** * Spyc -- A Simple PHP YAML Class * @version 0.4.5 * @author Vlad Andersen <vlad.andersen@gmail.com> * @author Chris Wanstrath <chris@ozmm.org> * @link http://code.google.com/p/spyc/ * @copyright Copyright 2005-2006 Chris Wanstrath, 2006-2009 Vlad Andersen * @license http://www.opensource.org/licenses/mit-license.php MIT License * @package Spyc */ if (!function_exists('spyc_load')) { /** * Parses YAML to array. * @param string $string YAML string. * @return array */ function spyc_load ($string) { return Spyc::YAMLLoadString($string); } } if (!function_exists('spyc_load_file')) { /** * Parses YAML to array. * @param string $file Path to YAML file. * @return array */ function spyc_load_file ($file) { return Spyc::YAMLLoad($file); } } /** * The Simple PHP YAML Class. * * This class can be used to read a YAML file and convert its contents * into a PHP array. It currently supports a very limited subsection of * the YAML spec. * * Usage: * <code> * $Spyc = new Spyc; * $array = $Spyc->load($file); * </code> * or: * <code> * $array = Spyc::YAMLLoad($file); * </code> * or: * <code> * $array = spyc_load_file($file); * </code> * @package Spyc */ class Spyc { // SETTINGS /** * Setting this to true will force YAMLDump to enclose any string value in * quotes. False by default. * * @var bool */ var $setting_dump_force_quotes = false; /** * Setting this to true will forse YAMLLoad to use syck_load function when * possible. False by default. * @var bool */ var $setting_use_syck_is_possible = false; /**#@+ * @access private * @var mixed */ var $_dumpIndent; var $_dumpWordWrap; var $_containsGroupAnchor = false; var $_containsGroupAlias = false; var $path; var $result; var $LiteralPlaceHolder = '___YAML_Literal_Block___'; var $SavedGroups = array(); var $indent; /** * Path modifier that should be applied after adding current element. * @var array */ var $delayedPath = array(); /**#@+ * @access public * @var mixed */ var $_nodeId; /** * Load a valid YAML string to Spyc. * @param string $input * @return array */ function load ($input) { return $this->__loadString($input); } /** * Load a valid YAML file to Spyc. * @param string $file * @return array */ function loadFile ($file) { return $this->__load($file); } /** * Load YAML into a PHP array statically * * The load method, when supplied with a YAML stream (string or file), * will do its best to convert YAML in a file into a PHP array. Pretty * simple. * Usage: * <code> * $array = Spyc::YAMLLoad('lucky.yaml'); * print_r($array); * </code> * @access public * @return array * @param string $input Path of YAML file or string containing YAML */ function YAMLLoad($input) { $Spyc = new Spyc; return $Spyc->__load($input); } /** * Load a string of YAML into a PHP array statically * * The load method, when supplied with a YAML string, will do its best * to convert YAML in a string into a PHP array. Pretty simple. * * Note: use this function if you don't want files from the file system * loaded and processed as YAML. This is of interest to people concerned * about security whose input is from a string. * * Usage: * <code> * $array = Spyc::YAMLLoadString("---\n0: hello world\n"); * print_r($array); * </code> * @access public * @return array * @param string $input String containing YAML */ function YAMLLoadString($input) { $Spyc = new Spyc; return $Spyc->__loadString($input); } /** * Dump YAML from PHP array statically * * The dump method, when supplied with an array, will do its best * to convert the array into friendly YAML. Pretty simple. Feel free to * save the returned string as nothing.yaml and pass it around. * * Oh, and you can decide how big the indent is and what the wordwrap * for folding is. Pretty cool -- just pass in 'false' for either if * you want to use the default. * * Indent's default is 2 spaces, wordwrap's default is 40 characters. And * you can turn off wordwrap by passing in 0. * * @access public * @return string * @param array $array PHP array * @param int $indent Pass in false to use the default, which is 2 * @param int $wordwrap Pass in 0 for no wordwrap, false for default (40) */ function YAMLDump($array,$indent = false,$wordwrap = false) { $spyc = new Spyc; return $spyc->dump($array,$indent,$wordwrap); } /** * Dump PHP array to YAML * * The dump method, when supplied with an array, will do its best * to convert the array into friendly YAML. Pretty simple. Feel free to * save the returned string as tasteful.yaml and pass it around. * * Oh, and you can decide how big the indent is and what the wordwrap * for folding is. Pretty cool -- just pass in 'false' for either if * you want to use the default. * * Indent's default is 2 spaces, wordwrap's default is 40 characters. And * you can turn off wordwrap by passing in 0. * * @access public * @return string * @param array $array PHP array * @param int $indent Pass in false to use the default, which is 2 * @param int $wordwrap Pass in 0 for no wordwrap, false for default (40) */ function dump($array,$indent = false,$wordwrap = false) { // Dumps to some very clean YAML. We'll have to add some more features // and options soon. And better support for folding. // New features and options. if ($indent === false or !is_numeric($indent)) { $this->_dumpIndent = 2; } else { $this->_dumpIndent = $indent; } if ($wordwrap === false or !is_numeric($wordwrap)) { $this->_dumpWordWrap = 40; } else { $this->_dumpWordWrap = $wordwrap; } // New YAML document $string = "---\n"; // Start at the base of the array and move through it. if ($array) { $array = (array)$array; $first_key = key($array); $previous_key = -1; foreach ($array as $key => $value) { $string .= $this->_yamlize($key,$value,0,$previous_key, $first_key); $previous_key = $key; } } return $string; } /** * Attempts to convert a key / value array item to YAML * @access private * @return string * @param $key The name of the key * @param $value The value of the item * @param $indent The indent of the current node */ function _yamlize($key,$value,$indent, $previous_key = -1, $first_key = 0) { if (is_array($value)) { if (empty ($value)) return $this->_dumpNode($key, array(), $indent, $previous_key, $first_key); // It has children. What to do? // Make it the right kind of item $string = $this->_dumpNode($key, NULL, $indent, $previous_key, $first_key); // Add the indent $indent += $this->_dumpIndent; // Yamlize the array $string .= $this->_yamlizeArray($value,$indent); } elseif (!is_array($value)) { // It doesn't have children. Yip. $string = $this->_dumpNode($key, $value, $indent, $previous_key, $first_key); } return $string; } /** * Attempts to convert an array to YAML * @access private * @return string * @param $array The array you want to convert * @param $indent The indent of the current level */ function _yamlizeArray($array,$indent) { if (is_array($array)) { $string = ''; $previous_key = -1; $first_key = key($array); foreach ($array as $key => $value) { $string .= $this->_yamlize($key, $value, $indent, $previous_key, $first_key); $previous_key = $key; } return $string; } else { return false; } } /** * Returns YAML from a key and a value * @access private * @return string * @param $key The name of the key * @param $value The value of the item * @param $indent The indent of the current node */ function _dumpNode($key, $value, $indent, $previous_key = -1, $first_key = 0) { // do some folding here, for blocks if (is_string ($value) && ((strpos($value,"\n") !== false || strpos($value,": ") !== false || strpos($value,"- ") !== false || strpos($value,"*") !== false || strpos($value,"#") !== false || strpos($value,"<") !== false || strpos($value,">") !== false || strpos($value,"[") !== false || strpos($value,"]") !== false || strpos($value,"{") !== false || strpos($value,"}") !== false) || substr ($value, -1, 1) == ':')) { $value = $this->_doLiteralBlock($value,$indent); } else { $value = $this->_doFolding($value,$indent); if (is_bool($value)) { $value = ($value) ? "true" : "false"; } } if ($value === array()) $value = '[ ]'; $spaces = str_repeat(' ',$indent); if (is_int($key) && $key - 1 == $previous_key && $first_key===0) { // It's a sequence $string = $spaces.'- '.$value."\n"; } else { if ($first_key===0) throw new Exception('Keys are all screwy. The first one was zero, now it\'s "'. $key .'"'); // It's mapped if (strpos($key, ":") !== false) { $key = '"' . $key . '"'; } $string = $spaces.$key.': '.$value."\n"; } return $string; } /** * Creates a literal block for dumping * @access private * @return string * @param $value * @param $indent int The value of the indent */ function _doLiteralBlock($value,$indent) { if (strpos($value, "\n") === false && strpos($value, "'") === false) { return sprintf ("'%s'", $value); } if (strpos($value, "\n") === false && strpos($value, '"') === false) { return sprintf ('"%s"', $value); } $exploded = explode("\n",$value); $newValue = '|'; $indent += $this->_dumpIndent; $spaces = str_repeat(' ',$indent); foreach ($exploded as $line) { $newValue .= "\n" . $spaces . trim($line); } return $newValue; } /** * Folds a string of text, if necessary * @access private * @return string * @param $value The string you wish to fold */ function _doFolding($value,$indent) { // Don't do anything if wordwrap is set to 0 if ($this->_dumpWordWrap !== 0 && is_string ($value) && strlen($value) > $this->_dumpWordWrap) { $indent += $this->_dumpIndent; $indent = str_repeat(' ',$indent); $wrapped = wordwrap($value,$this->_dumpWordWrap,"\n$indent"); $value = ">\n".$indent.$wrapped; } else { if ($this->setting_dump_force_quotes && is_string ($value)) $value = '"' . $value . '"'; } return $value; } // LOADING FUNCTIONS function __load($input) { $Source = $this->loadFromSource($input); return $this->loadWithSource($Source); } function __loadString($input) { $Source = $this->loadFromString($input); return $this->loadWithSource($Source); } function loadWithSource($Source) { if (empty ($Source)) return array(); if ($this->setting_use_syck_is_possible && function_exists ('syck_load')) { $array = syck_load (implode ('', $Source)); return is_array($array) ? $array : array(); } $this->path = array(); $this->result = array(); $cnt = count($Source); for ($i = 0; $i < $cnt; $i++) { $line = $Source[$i]; $this->indent = strlen($line) - strlen(ltrim($line)); $tempPath = $this->getParentPathByIndent($this->indent); $line = $this->stripIndent($line, $this->indent); if ($this->isComment($line)) continue; if ($this->isEmpty($line)) continue; $this->path = $tempPath; $literalBlockStyle = $this->startsLiteralBlock($line); if ($literalBlockStyle) { $line = rtrim ($line, $literalBlockStyle . " \n"); $literalBlock = ''; $line .= $this->LiteralPlaceHolder; while (++$i < $cnt && $this->literalBlockContinues($Source[$i], $this->indent)) { $literalBlock = $this->addLiteralLine($literalBlock, $Source[$i], $literalBlockStyle); } $i--; } while (++$i < $cnt && $this->greedilyNeedNextLine($line)) { $line = rtrim ($line, " \n\t\r") . ' ' . ltrim ($Source[$i], " \t"); } $i--; if (strpos ($line, '#')) { if (strpos ($line, '"') === false && strpos ($line, "'") === false) $line = preg_replace('/\s+#(.+)$/','',$line); } $lineArray = $this->_parseLine($line); if ($literalBlockStyle) $lineArray = $this->revertLiteralPlaceHolder ($lineArray, $literalBlock); $this->addArray($lineArray, $this->indent); foreach ($this->delayedPath as $indent => $delayedPath) $this->path[$indent] = $delayedPath; $this->delayedPath = array(); } return $this->result; } function loadFromSource ($input) { if (!empty($input) && strpos($input, "\n") === false && file_exists($input)) return file($input); return $this->loadFromString($input); } function loadFromString ($input) { $lines = explode("\n",$input); foreach ($lines as $k => $_) { $lines[$k] = rtrim ($_, "\r"); } return $lines; } /** * Parses YAML code and returns an array for a node * @access private * @return array * @param string $line A line from the YAML file */ function _parseLine($line) { if (!$line) return array(); $line = trim($line); if (!$line) return array(); $array = array(); $group = $this->nodeContainsGroup($line); if ($group) { $this->addGroup($line, $group); $line = $this->stripGroup ($line, $group); } if ($this->startsMappedSequence($line)) return $this->returnMappedSequence($line); if ($this->startsMappedValue($line)) return $this->returnMappedValue($line); if ($this->isArrayElement($line)) return $this->returnArrayElement($line); if ($this->isPlainArray($line)) return $this->returnPlainArray($line); return $this->returnKeyValuePair($line); } /** * Finds the type of the passed value, returns the value as the new type. * @access private * @param string $value * @return mixed */ function _toType($value) { if ($value === '') return null; $first_character = $value[0]; $last_character = substr($value, -1, 1); $is_quoted = false; do { if (!$value) break; if ($first_character != '"' && $first_character != "'") break; if ($last_character != '"' && $last_character != "'") break; $is_quoted = true; } while (0); if ($is_quoted) return strtr(substr ($value, 1, -1), array ('\\"' => '"', '\'\'' => '\'', '\\\'' => '\'')); if (strpos($value, ' #') !== false) $value = preg_replace('/\s+#(.+)$/','',$value); if ($first_character == '[' && $last_character == ']') { // Take out strings sequences and mappings $innerValue = trim(substr ($value, 1, -1)); if ($innerValue === '') return array(); $explode = $this->_inlineEscape($innerValue); // Propagate value array $value = array(); foreach ($explode as $v) { $value[] = $this->_toType($v); } return $value; } if (strpos($value,': ')!==false && $first_character != '{') { $array = explode(': ',$value); $key = trim($array[0]); array_shift($array); $value = trim(implode(': ',$array)); $value = $this->_toType($value); return array($key => $value); } if ($first_character == '{' && $last_character == '}') { $innerValue = trim(substr ($value, 1, -1)); if ($innerValue === '') return array(); // Inline Mapping // Take out strings sequences and mappings $explode = $this->_inlineEscape($innerValue); // Propagate value array $array = array(); foreach ($explode as $v) { $SubArr = $this->_toType($v); if (empty($SubArr)) continue; if (is_array ($SubArr)) { $array[key($SubArr)] = $SubArr[key($SubArr)]; continue; } $array[] = $SubArr; } return $array; } if ($value == 'null' || $value == 'NULL' || $value == 'Null' || $value == '' || $value == '~') { return null; } if (intval($first_character) > 0 && preg_match ('/^[1-9]+[0-9]*$/', $value)) { $intvalue = (int)$value; if ($intvalue != PHP_INT_MAX) $value = $intvalue; return $value; } if (in_array($value, array('true', 'on', '+', 'yes', 'y', 'True', 'TRUE', 'On', 'ON', 'YES', 'Yes', 'Y'))) { return true; } if (in_array(strtolower($value), array('false', 'off', '-', 'no', 'n'))) { return false; } if (is_numeric($value)) { if ($value === '0') return 0; if (trim ($value, 0) === $value) $value = (float)$value; return $value; } return $value; } /** * Used in inlines to check for more inlines or quoted strings * @access private * @return array */ function _inlineEscape($inline) { // There's gotta be a cleaner way to do this... // While pure sequences seem to be nesting just fine, // pure mappings and mappings with sequences inside can't go very // deep. This needs to be fixed. $seqs = array(); $maps = array(); $saved_strings = array(); // Check for strings $regex = '/(?:(")|(?:\'))((?(1)[^"]+|[^\']+))(?(1)"|\')/'; if (preg_match_all($regex,$inline,$strings)) { $saved_strings = $strings[0]; $inline = preg_replace($regex,'YAMLString',$inline); } unset($regex); $i = 0; do { // Check for sequences while (preg_match('/\[([^{}\[\]]+)\]/U',$inline,$matchseqs)) { $seqs[] = $matchseqs[0]; $inline = preg_replace('/\[([^{}\[\]]+)\]/U', ('YAMLSeq' . (count($seqs) - 1) . 's'), $inline, 1); } // Check for mappings while (preg_match('/{([^\[\]{}]+)}/U',$inline,$matchmaps)) { $maps[] = $matchmaps[0]; $inline = preg_replace('/{([^\[\]{}]+)}/U', ('YAMLMap' . (count($maps) - 1) . 's'), $inline, 1); } if ($i++ >= 10) break; } while (strpos ($inline, '[') !== false || strpos ($inline, '{') !== false); $explode = explode(', ',$inline); $stringi = 0; $i = 0; while (1) { // Re-add the sequences if (!empty($seqs)) { foreach ($explode as $key => $value) { if (strpos($value,'YAMLSeq') !== false) { foreach ($seqs as $seqk => $seq) { $explode[$key] = str_replace(('YAMLSeq'.$seqk.'s'),$seq,$value); $value = $explode[$key]; } } } } // Re-add the mappings if (!empty($maps)) { foreach ($explode as $key => $value) { if (strpos($value,'YAMLMap') !== false) { foreach ($maps as $mapk => $map) { $explode[$key] = str_replace(('YAMLMap'.$mapk.'s'), $map, $value); $value = $explode[$key]; } } } } // Re-add the strings if (!empty($saved_strings)) { foreach ($explode as $key => $value) { while (strpos($value,'YAMLString') !== false) { $explode[$key] = preg_replace('/YAMLString/',$saved_strings[$stringi],$value, 1); unset($saved_strings[$stringi]); ++$stringi; $value = $explode[$key]; } } } $finished = true; foreach ($explode as $key => $value) { if (strpos($value,'YAMLSeq') !== false) { $finished = false; break; } if (strpos($value,'YAMLMap') !== false) { $finished = false; break; } if (strpos($value,'YAMLString') !== false) { $finished = false; break; } } if ($finished) break; $i++; if ($i > 10) break; // Prevent infinite loops. } return $explode; } function literalBlockContinues ($line, $lineIndent) { if (!trim($line)) return true; if (strlen($line) - strlen(ltrim($line)) > $lineIndent) return true; return false; } function referenceContentsByAlias ($alias) { do { if (!isset($this->SavedGroups[$alias])) { echo "Bad group name: $alias."; break; } $groupPath = $this->SavedGroups[$alias]; $value = $this->result; foreach ($groupPath as $k) { $value = $value[$k]; } } while (false); return $value; } function addArrayInline ($array, $indent) { $CommonGroupPath = $this->path; if (empty ($array)) return false; foreach ($array as $k => $_) { $this->addArray(array($k => $_), $indent); $this->path = $CommonGroupPath; } return true; } function addArray ($incoming_data, $incoming_indent) { // print_r ($incoming_data); if (count ($incoming_data) > 1) return $this->addArrayInline ($incoming_data, $incoming_indent); $key = key ($incoming_data); $value = isset($incoming_data[$key]) ? $incoming_data[$key] : null; if ($key === '__!YAMLZero') $key = '0'; if ($incoming_indent == 0 && !$this->_containsGroupAlias && !$this->_containsGroupAnchor) { // Shortcut for root-level values. if ($key || $key === '' || $key === '0') { $this->result[$key] = $value; } else { $this->result[] = $value; end ($this->result); $key = key ($this->result); } $this->path[$incoming_indent] = $key; return; } $history = array(); // Unfolding inner array tree. $history[] = $_arr = $this->result; foreach ($this->path as $k) { $history[] = $_arr = $_arr[$k]; } if ($this->_containsGroupAlias) { $value = $this->referenceContentsByAlias($this->_containsGroupAlias); $this->_containsGroupAlias = false; } // Adding string or numeric key to the innermost level or $this->arr. if (is_string($key) && $key == '<<') { if (!is_array ($_arr)) { $_arr = array (); } $_arr = array_merge ($_arr, $value); } else if ($key || $key === '' || $key === '0') { $_arr[$key] = $value; } else { if (!is_array ($_arr)) { $_arr = array ($value); $key = 0; } else { $_arr[] = $value; end ($_arr); $key = key ($_arr); } } $reverse_path = array_reverse($this->path); $reverse_history = array_reverse ($history); $reverse_history[0] = $_arr; $cnt = count($reverse_history) - 1; for ($i = 0; $i < $cnt; $i++) { $reverse_history[$i+1][$reverse_path[$i]] = $reverse_history[$i]; } $this->result = $reverse_history[$cnt]; $this->path[$incoming_indent] = $key; if ($this->_containsGroupAnchor) { $this->SavedGroups[$this->_containsGroupAnchor] = $this->path; if (is_array ($value)) { $k = key ($value); if (!is_int ($k)) { $this->SavedGroups[$this->_containsGroupAnchor][$incoming_indent + 2] = $k; } } $this->_containsGroupAnchor = false; } } function startsLiteralBlock ($line) { $lastChar = substr (trim($line), -1); if ($lastChar != '>' && $lastChar != '|') return false; if ($lastChar == '|') return $lastChar; // HTML tags should not be counted as literal blocks. if (preg_match ('#<.*?>$#', $line)) return false; return $lastChar; } function greedilyNeedNextLine($line) { $line = trim ($line); if (!strlen($line)) return false; if (substr ($line, -1, 1) == ']') return false; if ($line[0] == '[') return true; if (preg_match ('#^[^:]+?:\s*\[#', $line)) return true; return false; } function addLiteralLine ($literalBlock, $line, $literalBlockStyle) { $line = $this->stripIndent($line); $line = rtrim ($line, "\r\n\t ") . "\n"; if ($literalBlockStyle == '|') { return $literalBlock . $line; } if (strlen($line) == 0) return rtrim($literalBlock, ' ') . "\n"; if ($line == "\n" && $literalBlockStyle == '>') { return rtrim ($literalBlock, " \t") . "\n"; } if ($line != "\n") $line = trim ($line, "\r\n ") . " "; return $literalBlock . $line; } function revertLiteralPlaceHolder ($lineArray, $literalBlock) { foreach ($lineArray as $k => $_) { if (is_array($_)) $lineArray[$k] = $this->revertLiteralPlaceHolder ($_, $literalBlock); else if (substr($_, -1 * strlen ($this->LiteralPlaceHolder)) == $this->LiteralPlaceHolder) $lineArray[$k] = rtrim ($literalBlock, " \r\n"); } return $lineArray; } function stripIndent ($line, $indent = -1) { if ($indent == -1) $indent = strlen($line) - strlen(ltrim($line)); return substr ($line, $indent); } function getParentPathByIndent ($indent) { if ($indent == 0) return array(); $linePath = $this->path; do { end($linePath); $lastIndentInParentPath = key($linePath); if ($indent <= $lastIndentInParentPath) array_pop ($linePath); } while ($indent <= $lastIndentInParentPath); return $linePath; } function clearBiggerPathValues ($indent) { if ($indent == 0) $this->path = array(); if (empty ($this->path)) return true; foreach ($this->path as $k => $_) { if ($k > $indent) unset ($this->path[$k]); } return true; } function isComment ($line) { if (!$line) return false; if ($line[0] == '#') return true; if (trim($line, " \r\n\t") == '---') return true; return false; } function isEmpty ($line) { return (trim ($line) === ''); } function isArrayElement ($line) { if (!$line) return false; if ($line[0] != '-') return false; if (strlen ($line) > 3) if (substr($line,0,3) == '---') return false; return true; } function isHashElement ($line) { return strpos($line, ':'); } function isLiteral ($line) { if ($this->isArrayElement($line)) return false; if ($this->isHashElement($line)) return false; return true; } function unquote ($value) { if (!$value) return $value; if (!is_string($value)) return $value; if ($value[0] == '\'') return trim ($value, '\''); if ($value[0] == '"') return trim ($value, '"'); return $value; } function startsMappedSequence ($line) { return ($line[0] == '-' && substr ($line, -1, 1) == ':'); } function returnMappedSequence ($line) { $array = array(); $key = $this->unquote(trim(substr($line,1,-1))); $array[$key] = array(); $this->delayedPath = array(strpos ($line, $key) + $this->indent => $key); return array($array); } function returnMappedValue ($line) { $array = array(); $key = $this->unquote (trim(substr($line,0,-1))); $array[$key] = ''; return $array; } function startsMappedValue ($line) { return (substr ($line, -1, 1) == ':'); } function isPlainArray ($line) { return ($line[0] == '[' && substr ($line, -1, 1) == ']'); } function returnPlainArray ($line) { return $this->_toType($line); } function returnKeyValuePair ($line) { $array = array(); $key = ''; if (strpos ($line, ':')) { // It's a key/value pair most likely // If the key is in double quotes pull it out if (($line[0] == '"' || $line[0] == "'") && preg_match('/^(["\'](.*)["\'](\s)*:)/',$line,$matches)) { $value = trim(str_replace($matches[1],'',$line)); $key = $matches[2]; } else { // Do some guesswork as to the key and the value $explode = explode(':',$line); $key = trim($explode[0]); array_shift($explode); $value = trim(implode(':',$explode)); } // Set the type of the value. Int, string, etc $value = $this->_toType($value); if ($key === '0') $key = '__!YAMLZero'; $array[$key] = $value; } else { $array = array ($line); } return $array; } function returnArrayElement ($line) { if (strlen($line) <= 1) return array(array()); // Weird %) $array = array(); $value = trim(substr($line,1)); $value = $this->_toType($value); $array[] = $value; return $array; } function nodeContainsGroup ($line) { $symbolsForReference = 'A-z0-9_\-'; if (strpos($line, '&') === false && strpos($line, '*') === false) return false; // Please die fast ;-) if ($line[0] == '&' && preg_match('/^(&['.$symbolsForReference.']+)/', $line, $matches)) return $matches[1]; if ($line[0] == '*' && preg_match('/^(\*['.$symbolsForReference.']+)/', $line, $matches)) return $matches[1]; if (preg_match('/(&['.$symbolsForReference.']+)$/', $line, $matches)) return $matches[1]; if (preg_match('/(\*['.$symbolsForReference.']+$)/', $line, $matches)) return $matches[1]; if (preg_match ('#^\s*<<\s*:\s*(\*[^\s]+).*$#', $line, $matches)) return $matches[1]; return false; } function addGroup ($line, $group) { if ($group[0] == '&') $this->_containsGroupAnchor = substr ($group, 1); if ($group[0] == '*') $this->_containsGroupAlias = substr ($group, 1); //print_r ($this->path); } function stripGroup ($line, $group) { $line = trim(str_replace($group, '', $line)); return $line; } } // Enable use of Spyc from command line // The syntax is the following: php spyc.php spyc.yaml define ('SPYC_FROM_COMMAND_LINE', false); do { if (!SPYC_FROM_COMMAND_LINE) break; if (empty ($_SERVER['argc']) || $_SERVER['argc'] < 2) break; if (empty ($_SERVER['PHP_SELF']) || $_SERVER['PHP_SELF'] != 'spyc.php') break; $file = $argv[1]; printf ("Spyc loading file: %s\n", $file); print_r (spyc_load_file ($file)); } while (0);<?php php5to4 ("../spyc.php", 'spyc-latest.php4'); function php5to4 ($src, $dest) { $code = file_get_contents ($src); $code = preg_replace ('#(public|private|protected)\s+\$#i', 'var \$', $code); $code = preg_replace ('#(public|private|protected)\s+static\s+\$#i', 'var \$', $code); $code = preg_replace ('#(public|private|protected)\s+function#i', 'function', $code); $code = preg_replace ('#(public|private|protected)\s+static\s+function#i', 'function', $code); $code = preg_replace ('#throw new Exception\\(([^)]*)\\)#i', 'trigger_error($1,E_USER_ERROR)', $code); $code = str_replace ('self::', '$this->', $code); $f = fopen ($dest, 'w'); fwrite($f, $code); fclose ($f); print "Written to $dest.\n"; }<?php # # S P Y C # a simple php yaml class # v0.3 # # author: [chris wanstrath, chris@ozmm.org] # websites: [http://www.yaml.org, http://spyc.sourceforge.net/] # license: [MIT License, http://www.opensource.org/licenses/mit-license.php] # copyright: (c) 2005-2006 Chris Wanstrath # # We're gonna load a file into memory and see if we get what we expect. # If not, we're gonna complain. # # Pretty lo-fi. Let's see if we can't get some unit testing going in the next, # I dunno, 20 months? Alright. Go team. # error_reporting(E_ALL); include('spyc.php4'); $yaml = Spyc::YAMLLoad('../spyc.yaml'); // print_r ($yaml); # Added in .2 if ($yaml[1040] != "Ooo, a numeric key!") die('Key: 1040 failed'); # Test mappings / types if ($yaml['String'] != "Anyone's name, really.") die('Key: String failed'); if ($yaml['Int'] !== 13) die('Key: Int failed'); if ($yaml['True'] !== true) die('Key: True failed'); if ($yaml['False'] !== false) die('Key: False failed'); if ($yaml['Zero'] !== 0) die('Key: Zero failed'); if (isset($yaml['Null'])) die('Key: Null failed'); if ($yaml['Float'] !== 5.34) die('Key: Float failed'); # Test sequences if ($yaml[0] != "PHP Class") die('Sequence 0 failed'); if ($yaml[1] != "Basic YAML Loader") die('Sequence 1 failed'); if ($yaml[2] != "Very Basic YAML Dumper") die('Sequence 2 failed'); # A sequence of a sequence if ($yaml[3] != array("YAML is so easy to learn.", "Your config files will never be the same.")) die('Sequence 3 failed'); # Sequence of mappings if ($yaml[4] != array("cpu" => "1.5ghz", "ram" => "1 gig", "os" => "os x 10.4.1")) die('Sequence 4 failed'); # Mapped sequence if ($yaml['domains'] != array("yaml.org", "php.net")) die("Key: 'domains' failed"); # A sequence like this. if ($yaml[5] != array("program" => "Adium", "platform" => "OS X", "type" => "Chat Client")) die('Sequence 5 failed'); # A folded block as a mapped value if ($yaml['no time'] != "There isn't any time for your tricks!\nDo you understand?") die("Key: 'no time' failed"); # A literal block as a mapped value if ($yaml['some time'] != "There is nothing but time\nfor your tricks.") die("Key: 'some time' failed"); # Crazy combinations if ($yaml['databases'] != array( array("name" => "spartan", "notes" => array( "Needs to be backed up", "Needs to be normalized" ), "type" => "mysql" ))) die("Key: 'databases' failed"); # You can be a bit tricky if ($yaml["if: you'd"] != "like") die("Key: 'if: you\'d' failed"); # Inline sequences if ($yaml[6] != array("One", "Two", "Three", "Four")) die("Sequence 6 failed"); # Nested Inline Sequences if ($yaml[7] != array("One", array("Two", "And", "Three"), "Four", "Five")) die("Sequence 7 failed"); # Nested Nested Inline Sequences if ($yaml[8] != array( "This", array("Is", "Getting", array("Ridiculous", "Guys")), "Seriously", array("Show", "Mercy"))) die("Sequence 8 failed"); # Inline mappings if ($yaml[9] != array("name" => "chris", "age" => "young", "brand" => "lucky strike")) die("Sequence 9 failed"); # Nested inline mappings if ($yaml[10] != array("name" => "mark", "age" => "older than chris", "brand" => array("marlboro", "lucky strike"))) die("Sequence 10 failed"); # References -- they're shaky, but functional if ($yaml['dynamic languages'] != array('Perl', 'Python', 'PHP', 'Ruby')) die("Key: 'dynamic languages' failed"); if ($yaml['compiled languages'] != array('C/C++', 'Java')) die("Key: 'compiled languages' failed"); if ($yaml['all languages'] != array( array('Perl', 'Python', 'PHP', 'Ruby'), array('C/C++', 'Java') )) die("Key: 'all languages' failed"); # Added in .2.2: Escaped quotes if ($yaml[11] != "you know, this shouldn't work. but it does.") die("Sequence 11 failed."); if ($yaml[12] != "that's my value.") die("Sequence 12 failed."); if ($yaml[13] != "again, that's my value.") die("Sequence 13 failed."); if ($yaml[14] != "here's to \"quotes\", boss.") die("Sequence 14 failed."); if ($yaml[15] != array( 'name' => "Foo, Bar's", 'age' => 20)) die("Sequence 15 failed."); if ($yaml[16] != array( 0 => "a", 1 => array (0 => 1, 1 => 2), 2 => "b")) die("Sequence 16 failed."); if ($yaml['endloop'] != "Does this line in the end indeed make Spyc go to an infinite loop?") die("[endloop] failed."); print "spyc.yaml parsed correctly\n"; ?><?php /** * Transforms a wp-config.php file. */ class WPConfigTransformer { /** * Append to end of file */ const ANCHOR_EOF = 'EOF'; /** * Path to the wp-config.php file. * * @var string */ protected $wp_config_path; /** * Original source of the wp-config.php file. * * @var string */ protected $wp_config_src; /** * Array of parsed configs. * * @var array */ protected $wp_configs = array(); /** * Instantiates the class with a valid wp-config.php. * * @throws Exception If the wp-config.php file is missing. * @throws Exception If the wp-config.php file is not writable. * * @param string $wp_config_path Path to a wp-config.php file. */ public function __construct( $wp_config_path ) { $basename = basename( $wp_config_path ); if ( ! file_exists( $wp_config_path ) ) { throw new Exception( "{$basename} does not exist." ); } if ( ! is_writable( $wp_config_path ) ) { throw new Exception( "{$basename} is not writable." ); } $this->wp_config_path = $wp_config_path; } /** * Checks if a config exists in the wp-config.php file. * * @throws Exception If the wp-config.php file is empty. * @throws Exception If the requested config type is invalid. * * @param string $type Config type (constant or variable). * @param string $name Config name. * * @return bool */ public function exists( $type, $name ) { $wp_config_src = file_get_contents( $this->wp_config_path ); if ( ! trim( $wp_config_src ) ) { throw new Exception( 'Config file is empty.' ); } // Normalize the newline to prevent an issue coming from OSX. $this->wp_config_src = str_replace( array( "\n\r", "\r" ), "\n", $wp_config_src ); $this->wp_configs = $this->parse_wp_config( $this->wp_config_src ); if ( ! isset( $this->wp_configs[ $type ] ) ) { throw new Exception( "Config type '{$type}' does not exist." ); } return isset( $this->wp_configs[ $type ][ $name ] ); } /** * Get the value of a config in the wp-config.php file. * * @throws Exception If the wp-config.php file is empty. * @throws Exception If the requested config type is invalid. * * @param string $type Config type (constant or variable). * @param string $name Config name. * * @return array */ public function get_value( $type, $name ) { $wp_config_src = file_get_contents( $this->wp_config_path ); if ( ! trim( $wp_config_src ) ) { throw new Exception( 'Config file is empty.' ); } $this->wp_config_src = $wp_config_src; $this->wp_configs = $this->parse_wp_config( $this->wp_config_src ); if ( ! isset( $this->wp_configs[ $type ] ) ) { throw new Exception( "Config type '{$type}' does not exist." ); } return $this->wp_configs[ $type ][ $name ]['value']; } /** * Adds a config to the wp-config.php file. * * @throws Exception If the config value provided is not a string. * @throws Exception If the config placement anchor could not be located. * * @param string $type Config type (constant or variable). * @param string $name Config name. * @param string $value Config value. * @param array $options (optional) Array of special behavior options. * * @return bool */ public function add( $type, $name, $value, array $options = array() ) { if ( ! is_string( $value ) ) { throw new Exception( 'Config value must be a string.' ); } if ( $this->exists( $type, $name ) ) { return false; } $defaults = array( 'raw' => false, // Display value in raw format without quotes. 'anchor' => "/* That's all, stop editing!", // Config placement anchor string. 'separator' => PHP_EOL, // Separator between config definition and anchor string. 'placement' => 'before', // Config placement direction (insert before or after). ); list( $raw, $anchor, $separator, $placement ) = array_values( array_merge( $defaults, $options ) ); $raw = (bool) $raw; $anchor = (string) $anchor; $separator = (string) $separator; $placement = (string) $placement; if ( self::ANCHOR_EOF === $anchor ) { $contents = $this->wp_config_src . $this->normalize( $type, $name, $this->format_value( $value, $raw ) ); } else { if ( false === strpos( $this->wp_config_src, $anchor ) ) { throw new Exception( 'Unable to locate placement anchor.' ); } $new_src = $this->normalize( $type, $name, $this->format_value( $value, $raw ) ); $new_src = ( 'after' === $placement ) ? $anchor . $separator . $new_src : $new_src . $separator . $anchor; $contents = str_replace( $anchor, $new_src, $this->wp_config_src ); } return $this->save( $contents ); } /** * Updates an existing config in the wp-config.php file. * * @throws Exception If the config value provided is not a string. * * @param string $type Config type (constant or variable). * @param string $name Config name. * @param string $value Config value. * @param array $options (optional) Array of special behavior options. * * @return bool */ public function update( $type, $name, $value, array $options = array() ) { if ( ! is_string( $value ) ) { throw new Exception( 'Config value must be a string.' ); } $defaults = array( 'add' => true, // Add the config if missing. 'raw' => false, // Display value in raw format without quotes. 'normalize' => false, // Normalize config output using WP Coding Standards. ); list( $add, $raw, $normalize ) = array_values( array_merge( $defaults, $options ) ); $add = (bool) $add; $raw = (bool) $raw; $normalize = (bool) $normalize; if ( ! $this->exists( $type, $name ) ) { return ( $add ) ? $this->add( $type, $name, $value, $options ) : false; } $old_src = $this->wp_configs[ $type ][ $name ]['src']; $old_value = $this->wp_configs[ $type ][ $name ]['value']; $new_value = $this->format_value( $value, $raw ); if ( $normalize ) { $new_src = $this->normalize( $type, $name, $new_value ); } else { $new_parts = $this->wp_configs[ $type ][ $name ]['parts']; $new_parts[1] = str_replace( $old_value, $new_value, $new_parts[1] ); // Only edit the value part. $new_src = implode( '', $new_parts ); } $contents = preg_replace( sprintf( '/(?<=^|;|<\?php\s|<\?\s)(\s*?)%s/m', preg_quote( trim( $old_src ), '/' ) ), '$1' . str_replace( '$', '\$', trim( $new_src ) ), $this->wp_config_src ); return $this->save( $contents ); } /** * Removes a config from the wp-config.php file. * * @param string $type Config type (constant or variable). * @param string $name Config name. * * @return bool */ public function remove( $type, $name ) { if ( ! $this->exists( $type, $name ) ) { return false; } $pattern = sprintf( '/(?<=^|;|<\?php\s|<\?\s)%s\s*(\S|$)/m', preg_quote( $this->wp_configs[ $type ][ $name ]['src'], '/' ) ); $contents = preg_replace( $pattern, '$1', $this->wp_config_src ); return $this->save( $contents ); } /** * Applies formatting to a config value. * * @throws Exception When a raw value is requested for an empty string. * * @param string $value Config value. * @param bool $raw Display value in raw format without quotes. * * @return mixed */ protected function format_value( $value, $raw ) { if ( $raw && '' === trim( $value ) ) { throw new Exception( 'Raw value for empty string not supported.' ); } return ( $raw ) ? $value : var_export( $value, true ); } /** * Normalizes the source output for a name/value pair. * * @throws Exception If the requested config type does not support normalization. * * @param string $type Config type (constant or variable). * @param string $name Config name. * @param mixed $value Config value. * * @return string */ protected function normalize( $type, $name, $value ) { if ( 'constant' === $type ) { $placeholder = "define( '%s', %s );"; } elseif ( 'variable' === $type ) { $placeholder = '$%s = %s;'; } else { throw new Exception( "Unable to normalize config type '{$type}'." ); } return sprintf( $placeholder, $name, $value ); } /** * Parses the source of a wp-config.php file. * * @param string $src Config file source. * * @return array */ protected function parse_wp_config( $src ) { $configs = array(); $configs['constant'] = array(); $configs['variable'] = array(); // Strip comments. foreach ( token_get_all( $src ) as $token ) { if ( in_array( $token[0], array( T_COMMENT, T_DOC_COMMENT ), true ) ) { $src = str_replace( $token[1], '', $src ); } } preg_match_all( '/(?<=^|;|<\?php\s|<\?\s)(\h*define\s*\(\s*[\'"](\w*?)[\'"]\s*)(,\s*(\'\'|""|\'.*?[^\\\\]\'|".*?[^\\\\]"|.*?)\s*)((?:,\s*(?:true|false)\s*)?\)\s*;)/ims', $src, $constants ); preg_match_all( '/(?<=^|;|<\?php\s|<\?\s)(\h*\$(\w+)\s*=)(\s*(\'\'|""|\'.*?[^\\\\]\'|".*?[^\\\\]"|.*?)\s*;)/ims', $src, $variables ); if ( ! empty( $constants[0] ) && ! empty( $constants[1] ) && ! empty( $constants[2] ) && ! empty( $constants[3] ) && ! empty( $constants[4] ) && ! empty( $constants[5] ) ) { foreach ( $constants[2] as $index => $name ) { $configs['constant'][ $name ] = array( 'src' => $constants[0][ $index ], 'value' => $constants[4][ $index ], 'parts' => array( $constants[1][ $index ], $constants[3][ $index ], $constants[5][ $index ], ), ); } } if ( ! empty( $variables[0] ) && ! empty( $variables[1] ) && ! empty( $variables[2] ) && ! empty( $variables[3] ) && ! empty( $variables[4] ) ) { // Remove duplicate(s), last definition wins. $variables[2] = array_reverse( array_unique( array_reverse( $variables[2], true ) ), true ); foreach ( $variables[2] as $index => $name ) { $configs['variable'][ $name ] = array( 'src' => $variables[0][ $index ], 'value' => $variables[4][ $index ], 'parts' => array( $variables[1][ $index ], $variables[3][ $index ], ), ); } } return $configs; } /** * Saves new contents to the wp-config.php file. * * @throws Exception If the config file content provided is empty. * @throws Exception If there is a failure when saving the wp-config.php file. * * @param string $contents New config contents. * * @return bool */ protected function save( $contents ) { if ( ! trim( $contents ) ) { throw new Exception( 'Cannot save the config file with empty contents.' ); } if ( $contents === $this->wp_config_src ) { return false; } $result = file_put_contents( $this->wp_config_path, $contents, LOCK_EX ); if ( false === $result ) { throw new Exception( 'Failed to update the config file.' ); } return true; } } <?php use WP_CLI\Utils; /** * Schedules, runs, and deletes WP-Cron events. * * ## EXAMPLES * * # Schedule a new cron event * $ wp cron event schedule cron_test * Success: Scheduled event with hook 'cron_test' for 2016-05-31 10:19:16 GMT. * * # Run all cron events due right now * $ wp cron event run --due-now * Success: Executed a total of 2 cron events. * * # Delete all scheduled cron events for the given hook * $ wp cron event delete cron_test * Success: Deleted 2 instances of the cron event 'cron_test'. * * # List scheduled cron events in JSON * $ wp cron event list --fields=hook,next_run --format=json * [{"hook":"wp_version_check","next_run":"2016-05-31 10:15:13"},{"hook":"wp_update_plugins","next_run":"2016-05-31 10:15:13"},{"hook":"wp_update_themes","next_run":"2016-05-31 10:15:14"}] * * @package wp-cli */ class Cron_Event_Command extends WP_CLI_Command { private $fields = array( 'hook', 'next_run_gmt', 'next_run_relative', 'recurrence', ); private static $time_format = 'Y-m-d H:i:s'; /** * Lists scheduled cron events. * * ## OPTIONS * * [--fields=<fields>] * : Limit the output to specific object fields. * * [--<field>=<value>] * : Filter by one or more fields. * * [--field=<field>] * : Prints the value of a single field for each event. * * [--format=<format>] * : Render output in a particular format. * --- * default: table * options: * - table * - csv * - ids * - json * - count * - yaml * --- * * ## AVAILABLE FIELDS * * These fields will be displayed by default for each cron event: * * hook * * next_run_gmt * * next_run_relative * * recurrence * * These fields are optionally available: * * time * * sig * * args * * schedule * * interval * * next_run * * ## EXAMPLES * * # List scheduled cron events * $ wp cron event list * +-------------------+---------------------+---------------------+------------+ * | hook | next_run_gmt | next_run_relative | recurrence | * +-------------------+---------------------+---------------------+------------+ * | wp_version_check | 2016-05-31 22:15:13 | 11 hours 57 minutes | 12 hours | * | wp_update_plugins | 2016-05-31 22:15:13 | 11 hours 57 minutes | 12 hours | * | wp_update_themes | 2016-05-31 22:15:14 | 11 hours 57 minutes | 12 hours | * +-------------------+---------------------+---------------------+------------+ * * # List scheduled cron events in JSON * $ wp cron event list --fields=hook,next_run --format=json * [{"hook":"wp_version_check","next_run":"2016-05-31 10:15:13"},{"hook":"wp_update_plugins","next_run":"2016-05-31 10:15:13"},{"hook":"wp_update_themes","next_run":"2016-05-31 10:15:14"}] * * @subcommand list */ public function list_( $args, $assoc_args ) { $formatter = $this->get_formatter( $assoc_args ); $events = self::get_cron_events(); if ( is_wp_error( $events ) ) { $events = array(); } foreach ( $events as $key => $event ) { foreach ( $this->fields as $field ) { if ( ! empty( $assoc_args[ $field ] ) && $event->{$field} !== $assoc_args[ $field ] ) { unset( $events[ $key ] ); break; } } } if ( 'ids' === $formatter->format ) { echo implode( ' ', wp_list_pluck( $events, 'hook' ) ); } else { $formatter->display_items( $events ); } } /** * Schedules a new cron event. * * ## OPTIONS * * <hook> * : The hook name. * * [<next-run>] * : A Unix timestamp or an English textual datetime description compatible with `strtotime()`. Defaults to now. * * [<recurrence>] * : How often the event should recur. See `wp cron schedule list` for available schedule names. Defaults to no recurrence. * * [--<field>=<value>] * : Arguments to pass to the hook for the event. <field> should be a numeric key, not a string. * * ## EXAMPLES * * # Schedule a new cron event * $ wp cron event schedule cron_test * Success: Scheduled event with hook 'cron_test' for 2016-05-31 10:19:16 GMT. * * # Schedule new cron event with hourly recurrence * $ wp cron event schedule cron_test now hourly * Success: Scheduled event with hook 'cron_test' for 2016-05-31 10:20:32 GMT. * * # Schedule new cron event and pass arguments * $ wp cron event schedule cron_test '+1 hour' --0=first-argument --1=second-argument * Success: Scheduled event with hook 'cron_test' for 2016-05-31 11:21:35 GMT. */ public function schedule( $args, $assoc_args ) { if ( count( $assoc_args ) && count( array_filter( array_keys( $assoc_args ), 'is_string' ) ) ) { WP_CLI::warning( 'Numeric keys should be used for the hook arguments.' ); } $hook = $args[0]; $next_run = Utils\get_flag_value( $args, 1, 'now' ); $recurrence = Utils\get_flag_value( $args, 2, false ); if ( empty( $next_run ) ) { $timestamp = time(); } elseif ( is_numeric( $next_run ) ) { $timestamp = absint( $next_run ); } else { $timestamp = strtotime( $next_run ); } if ( ! $timestamp ) { WP_CLI::error( sprintf( "'%s' is not a valid datetime.", $next_run ) ); } if ( ! empty( $recurrence ) ) { $schedules = wp_get_schedules(); if ( ! isset( $schedules[ $recurrence ] ) ) { WP_CLI::error( sprintf( "'%s' is not a valid schedule name for recurrence.", $recurrence ) ); } $event = wp_schedule_event( $timestamp, $recurrence, $hook, $assoc_args ); } else { $event = wp_schedule_single_event( $timestamp, $hook, $assoc_args ); } if ( false !== $event ) { WP_CLI::success( sprintf( "Scheduled event with hook '%s' for %s GMT.", $hook, date( self::$time_format, $timestamp ) ) ); //phpcs:ignore WordPress.DateTime.RestrictedFunctions.date_date } else { WP_CLI::error( 'Event not scheduled.' ); } } /** * Runs the next scheduled cron event for the given hook. * * ## OPTIONS * * [<hook>...] * : One or more hooks to run. * * [--due-now] * : Run all hooks due right now. * * [--all] * : Run all hooks. * * ## EXAMPLES * * # Run all cron events due right now * $ wp cron event run --due-now * Success: Executed a total of 2 cron events. */ public function run( $args, $assoc_args ) { if ( empty( $args ) && ! Utils\get_flag_value( $assoc_args, 'due-now' ) && ! Utils\get_flag_value( $assoc_args, 'all' ) ) { WP_CLI::error( 'Please specify one or more cron events, or use --due-now/--all.' ); } $events = self::get_cron_events(); if ( is_wp_error( $events ) ) { WP_CLI::error( $events ); } $hooks = wp_list_pluck( $events, 'hook' ); foreach ( $args as $hook ) { if ( ! in_array( $hook, $hooks, true ) ) { WP_CLI::error( sprintf( "Invalid cron event '%s'", $hook ) ); } } if ( Utils\get_flag_value( $assoc_args, 'due-now' ) ) { $due_events = array(); foreach ( $events as $event ) { if ( ! empty( $args ) && ! in_array( $event->hook, $args, true ) ) { continue; } if ( time() >= $event->time ) { $due_events[] = $event; } } $events = $due_events; } elseif ( ! Utils\get_flag_value( $assoc_args, 'all' ) ) { $due_events = array(); foreach ( $events as $event ) { if ( in_array( $event->hook, $args, true ) ) { $due_events[] = $event; } } $events = $due_events; } $executed = 0; foreach ( $events as $event ) { $start = microtime( true ); $result = self::run_event( $event ); $total = round( microtime( true ) - $start, 3 ); $executed++; WP_CLI::log( sprintf( "Executed the cron event '%s' in %ss.", $event->hook, $total ) ); } $message = ( 1 === $executed ) ? 'Executed a total of %d cron event.' : 'Executed a total of %d cron events.'; WP_CLI::success( sprintf( $message, $executed ) ); } /** * Unschedules all cron events for a given hook. * * ## OPTIONS * * <hook> * : Name of the hook for which all events should be unscheduled. * * ## EXAMPLES * * # Unschedule a cron event on given hook. * $ wp cron event unschedule cron_test * Success: Unscheduled 2 events with hook 'cron_test'. */ public function unschedule( $args, $assoc_args ) { list( $hook ) = $args; if ( Utils\wp_version_compare( '4.9.0', '<' ) ) { WP_CLI::error( 'Unscheduling events is only supported from WordPress 4.9.0 onwards.' ); } $unscheduled = wp_unschedule_hook( $hook ); if ( empty( $unscheduled ) ) { $message = 'Failed to unschedule events for hook \'%1\$s.'; // If 0 event found on hook. if ( 0 === $unscheduled ) { $message = "No events found for hook '%1\$s'."; } WP_CLI::error( sprintf( $message, $hook ) ); } else { WP_CLI::success( sprintf( 'Unscheduled %1$d %2$s for hook \'%3$s\'.', $unscheduled, Utils\pluralize( 'event', $unscheduled ), $hook ) ); } } /** * Executes an event immediately. * * @param stdClass $event The event * @return bool Whether the event was successfully executed or not. */ protected static function run_event( stdClass $event ) { if ( ! defined( 'DOING_CRON' ) ) { // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedConstantFound -- Using native WordPress constant. define( 'DOING_CRON', true ); } if ( false !== $event->schedule ) { $new_args = array( $event->time, $event->schedule, $event->hook, $event->args ); call_user_func_array( 'wp_reschedule_event', $new_args ); } wp_unschedule_event( $event->time, $event->hook, $event->args ); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.DynamicHooknameFound -- Can't prefix dynamic hooks here, calling registered hooks. do_action_ref_array( $event->hook, $event->args ); return true; } /** * Deletes all scheduled cron events for the given hook. * * ## OPTIONS * * <hook> * : The hook name. * * ## EXAMPLES * * # Delete all scheduled cron events for the given hook * $ wp cron event delete cron_test * Success: Deleted 2 instances of the cron event 'cron_test'. */ public function delete( $args, $assoc_args ) { $hook = $args[0]; $events = self::get_cron_events(); if ( is_wp_error( $events ) ) { WP_CLI::error( $events ); } $deleted = 0; foreach ( $events as $event ) { if ( $event->hook === $hook ) { $result = self::delete_event( $event ); if ( $result ) { $deleted++; } else { WP_CLI::warning( sprintf( "Failed to the delete the cron event '%s'.", $hook ) ); } } } if ( $deleted ) { $message = ( 1 === $deleted ) ? "Deleted the cron event '%2\$s'." : "Deleted %1\$d instances of the cron event '%2\$s'."; WP_CLI::success( sprintf( $message, $deleted, $hook ) ); } else { WP_CLI::error( sprintf( "Invalid cron event '%s'.", $hook ) ); } } /** * Deletes a cron event. * * @param stdClass $event The event * @return bool Whether the event was successfully deleted or not. */ protected static function delete_event( stdClass $event ) { $crons = _get_cron_array(); if ( ! isset( $crons[ $event->time ][ $event->hook ][ $event->sig ] ) ) { return false; } wp_unschedule_event( $event->time, $event->hook, $event->args ); return true; } /** * Callback function to format a cron event. * * @param stdClass $event The event. * @return stdClass The formatted event object. */ protected static function format_event( stdClass $event ) { $event->next_run = get_date_from_gmt( date( 'Y-m-d H:i:s', $event->time ), self::$time_format ); //phpcs:ignore WordPress.DateTime.RestrictedFunctions.date_date $event->next_run_gmt = date( self::$time_format, $event->time ); //phpcs:ignore WordPress.DateTime.RestrictedFunctions.date_date $event->next_run_relative = self::interval( $event->time - time() ); $event->recurrence = ( $event->schedule ) ? self::interval( $event->interval ) : 'Non-repeating'; return $event; } /** * Fetches an array of scheduled cron events. * * @return array|WP_Error An array of event objects, or a WP_Error object if there are no events scheduled. */ protected static function get_cron_events() { $crons = _get_cron_array(); $events = array(); if ( empty( $crons ) ) { return new WP_Error( 'no_events', 'You currently have no scheduled cron events.' ); } foreach ( $crons as $time => $hooks ) { foreach ( $hooks as $hook => $hook_events ) { foreach ( $hook_events as $sig => $data ) { $events[] = (object) array( 'hook' => $hook, 'time' => $time, 'sig' => $sig, 'args' => $data['args'], 'schedule' => $data['schedule'], 'interval' => Utils\get_flag_value( $data, 'interval' ), ); } } } $events = array_map( 'Cron_Event_Command::format_event', $events ); return $events; } /** * Converts a time interval into human-readable format. * * Similar to WordPress' built-in `human_time_diff()` but returns two time period chunks instead of just one. * * @param int $since An interval of time in seconds * @return string The interval in human readable format */ private static function interval( $since ) { if ( $since <= 0 ) { return 'now'; } $since = absint( $since ); // Array of time period chunks. $chunks = array( array( 60 * 60 * 24 * 365, 'year' ), array( 60 * 60 * 24 * 30, 'month' ), array( 60 * 60 * 24 * 7, 'week' ), array( 60 * 60 * 24, 'day' ), array( 60 * 60, 'hour' ), array( 60, 'minute' ), array( 1, 'second' ), ); // we only want to output two chunks of time here, eg: // x years, xx months // x days, xx hours // so there's only two bits of calculation below: // step one: the first chunk for ( $i = 0, $j = count( $chunks ); $i < $j; $i++ ) { $seconds = $chunks[ $i ][0]; $name = $chunks[ $i ][1]; // Finding the biggest chunk (if the chunk fits, break). $count = floor( $since / $seconds ); if ( floatval( 0 ) !== $count ) { break; } } // Set output var. $output = sprintf( '%d %s', $count, Utils\pluralize( $name, absint( $count ) ) ); // Step two: the second chunk. if ( $i + 1 < $j ) { $seconds2 = $chunks[ $i + 1 ][0]; $name2 = $chunks[ $i + 1 ][1]; $count2 = floor( ( $since - ( $seconds * $count ) ) / $seconds2 ); if ( floatval( 0 ) !== $count2 ) { // Add to output var. $output .= ' ' . sprintf( '%d %s', $count2, Utils\pluralize( $name2, absint( $count2 ) ) ); } } return $output; } private function get_formatter( &$assoc_args ) { return new \WP_CLI\Formatter( $assoc_args, $this->fields, 'event' ); } } <?php /** * Gets WP-Cron schedules. * * ## EXAMPLES * * # List available cron schedules * $ wp cron schedule list * +------------+-------------+----------+ * | name | display | interval | * +------------+-------------+----------+ * | hourly | Once Hourly | 3600 | * | twicedaily | Twice Daily | 43200 | * | daily | Once Daily | 86400 | * +------------+-------------+----------+ */ class Cron_Schedule_Command extends WP_CLI_Command { private $fields = array( 'name', 'display', 'interval', ); /** * List available cron schedules. * * ## OPTIONS * * [--fields=<fields>] * : Limit the output to specific object fields. * * [--field=<field>] * : Prints the value of a single field for each schedule. * * [--format=<format>] * : Render output in a particular format. * --- * default: table * options: * - table * - csv * - ids * - json * - yaml * --- * * ## AVAILABLE FIELDS * * These fields will be displayed by default for each cron schedule: * * * name * * display * * interval * * There are no additional fields. * * ## EXAMPLES * * # List available cron schedules * $ wp cron schedule list * +------------+-------------+----------+ * | name | display | interval | * +------------+-------------+----------+ * | hourly | Once Hourly | 3600 | * | twicedaily | Twice Daily | 43200 | * | daily | Once Daily | 86400 | * +------------+-------------+----------+ * * # List id of available cron schedule * $ wp cron schedule list --fields=name --format=ids * hourly twicedaily daily * * @subcommand list */ public function list_( $args, $assoc_args ) { $formatter = $this->get_formatter( $assoc_args ); $schedules = self::get_schedules(); if ( 'ids' === $formatter->format ) { echo implode( ' ', wp_list_pluck( $schedules, 'name' ) ); } else { $formatter->display_items( $schedules ); } } /** * Callback function to format a cron schedule. * * @param array $schedule The schedule. * @param string $name The schedule name. * @return array The formatted schedule. */ protected static function format_schedule( array $schedule, $name ) { $schedule['name'] = $name; return $schedule; } /** * Return a list of the cron schedules sorted according to interval. * * @return array The array of cron schedules. Each schedule is itself an array. */ protected static function get_schedules() { $schedules = wp_get_schedules(); if ( ! empty( $schedules ) ) { uasort( $schedules, 'Cron_Schedule_Command::sort' ); $schedules = array_map( 'Cron_Schedule_Command::format_schedule', $schedules, array_keys( $schedules ) ); } return $schedules; } /** * Callback function to sort the cron schedule array by interval. * */ protected static function sort( array $a, array $b ) { return $a['interval'] - $b['interval']; } private function get_formatter( &$assoc_args ) { return new \WP_CLI\Formatter( $assoc_args, $this->fields, 'schedule' ); } } <?php /** * Tests, runs, and deletes WP-Cron events; manages WP-Cron schedules. * * ## EXAMPLES * * # Test WP Cron spawning system * $ wp cron test * Success: WP-Cron spawning is working as expected. */ class Cron_Command extends WP_CLI_Command { /** * Tests the WP Cron spawning system and reports back its status. * * This command tests the spawning system by performing the following steps: * * * Checks to see if the `DISABLE_WP_CRON` constant is set; errors if true * because WP-Cron is disabled. * * Checks to see if the `ALTERNATE_WP_CRON` constant is set; warns if true. * * Attempts to spawn WP-Cron over HTTP; warns if non 200 response code is * returned. * * ## EXAMPLES * * # Cron test runs successfully. * $ wp cron test * Success: WP-Cron spawning is working as expected. */ public function test() { if ( defined( 'DISABLE_WP_CRON' ) && DISABLE_WP_CRON ) { WP_CLI::error( 'The DISABLE_WP_CRON constant is set to true. WP-Cron spawning is disabled.' ); } if ( defined( 'ALTERNATE_WP_CRON' ) && ALTERNATE_WP_CRON ) { WP_CLI::warning( 'The ALTERNATE_WP_CRON constant is set to true. WP-Cron spawning is not asynchronous.' ); } $spawn = self::get_cron_spawn(); if ( is_wp_error( $spawn ) ) { WP_CLI::error( sprintf( 'WP-Cron spawn failed with error: %s', $spawn->get_error_message() ) ); } $code = wp_remote_retrieve_response_code( $spawn ); $message = wp_remote_retrieve_response_message( $spawn ); if ( 200 === $code ) { WP_CLI::success( 'WP-Cron spawning is working as expected.' ); } else { WP_CLI::error( sprintf( 'WP-Cron spawn returned HTTP status code: %1$s %2$s', $code, $message ) ); } } /** * Spawns a request to `wp-cron.php` and return the response. * * This function is designed to mimic the functionality in `spawn_cron()` * with the addition of returning the result of the `wp_remote_post()` * request. * * @return WP_Error|array The response or WP_Error on failure. */ protected static function get_cron_spawn() { $sslverify = \WP_CLI\Utils\wp_version_compare( 4.0, '<' ); $doing_wp_cron = sprintf( '%.22F', microtime( true ) ); $cron_request_array = array( 'url' => site_url( 'wp-cron.php?doing_wp_cron=' . $doing_wp_cron ), 'key' => $doing_wp_cron, 'args' => array( 'timeout' => 3, 'blocking' => true, // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- Calling native WordPress hook. 'sslverify' => apply_filters( 'https_local_ssl_verify', $sslverify ), ), ); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- Calling native WordPress hook. $cron_request = apply_filters( 'cron_request', $cron_request_array ); # Enforce a blocking request in case something that's hooked onto the 'cron_request' filter sets it to false $cron_request['args']['blocking'] = true; $result = wp_remote_post( $cron_request['url'], $cron_request['args'] ); return $result; } } <?php if ( ! class_exists( 'WP_CLI' ) ) { return; } $wpcli_cron_autoloader = dirname( __FILE__ ) . '/vendor/autoload.php'; if ( file_exists( $wpcli_cron_autoloader ) ) { require_once $wpcli_cron_autoloader; } WP_CLI::add_command( 'cron', 'Cron_Command' ); WP_CLI::add_command( 'cron event', 'Cron_Event_Command' ); WP_CLI::add_command( 'cron schedule', 'Cron_Schedule_Command' ); <?php use WP_CLI\Utils; /** * Lists or flushes the site's rewrite rules, updates the permalink structure. * * See the WordPress [Rewrite API](https://codex.wordpress.org/Rewrite_API) and * [WP Rewrite](https://codex.wordpress.org/Class_Reference/WP_Rewrite) class reference. * * ## EXAMPLES * * # Flush rewrite rules * $ wp rewrite flush * Success: Rewrite rules flushed. * * # Update permalink structure * $ wp rewrite structure '/%year%/%monthnum%/%postname%' * Success: Rewrite structure set. * * # List rewrite rules * $ wp rewrite list --format=csv * match,query,source * ^wp-json/?$,index.php?rest_route=/,other * ^wp-json/(.*)?,index.php?rest_route=/$matches[1],other * category/(.+?)/feed/(feed|rdf|rss|rss2|atom)/?$,index.php?category_name=$matches[1]&feed=$matches[2],category * category/(.+?)/(feed|rdf|rss|rss2|atom)/?$,index.php?category_name=$matches[1]&feed=$matches[2],category * category/(.+?)/embed/?$,index.php?category_name=$matches[1]&embed=true,category * * @package wp-cli */ class Rewrite_Command extends WP_CLI_Command { /** * Flushes rewrite rules. * * Resets WordPress' rewrite rules based on registered post types, etc. * * To regenerate a .htaccess file with WP-CLI, you'll need to add the mod_rewrite module * to your wp-cli.yml or config.yml. For example: * * ``` * apache_modules: * - mod_rewrite * ``` * * ## OPTIONS * * [--hard] * : Perform a hard flush - update `.htaccess` rules as well as rewrite rules in database. Works only on single site installs. * * ## EXAMPLES * * $ wp rewrite flush * Success: Rewrite rules flushed. */ public function flush( $args, $assoc_args ) { // make sure we detect mod_rewrite if configured in apache_modules in config self::apache_modules(); if ( Utils\get_flag_value( $assoc_args, 'hard' ) && ! in_array( 'mod_rewrite', (array) WP_CLI::get_config( 'apache_modules' ), true ) ) { WP_CLI::warning( 'Regenerating a .htaccess file requires special configuration. See usage docs.' ); } if ( Utils\get_flag_value( $assoc_args, 'hard' ) && is_multisite() ) { WP_CLI::warning( "WordPress can't generate .htaccess file for a multisite install." ); } self::check_skip_plugins_themes(); flush_rewrite_rules( Utils\get_flag_value( $assoc_args, 'hard' ) ); if ( ! get_option( 'rewrite_rules' ) ) { WP_CLI::warning( "Rewrite rules are empty, possibly because of a missing permalink_structure option. Use 'wp rewrite list' to verify, or 'wp rewrite structure' to update permalink_structure." ); } else { WP_CLI::success( 'Rewrite rules flushed.' ); } } /** * Updates the permalink structure. * * Sets the post permalink structure to the specified pattern. * * To regenerate a .htaccess file with WP-CLI, you'll need to add * the mod_rewrite module to your [WP-CLI config](https://make.wordpress.org/cli/handbook/config/#config-files). * For example: * * ``` * apache_modules: * - mod_rewrite * ``` * * ## OPTIONS * * <permastruct> * : The new permalink structure to apply. * * [--category-base=<base>] * : Set the base for category permalinks, i.e. '/category/'. * * [--tag-base=<base>] * : Set the base for tag permalinks, i.e. '/tag/'. * * [--hard] * : Perform a hard flush - update `.htaccess` rules as well as rewrite rules in database. * * ## EXAMPLES * * $ wp rewrite structure '/%year%/%monthnum%/%postname%/' * Success: Rewrite structure set. */ public function structure( $args, $assoc_args ) { global $wp_rewrite; // copypasta from /wp-admin/options-permalink.php $blog_prefix = ''; $prefix = $blog_prefix; if ( is_multisite() && ! is_subdomain_install() && is_main_site() ) { $blog_prefix = '/blog'; } $permalink_structure = ( 'default' === $args[0] ) ? '' : $args[0]; if ( ! empty( $permalink_structure ) ) { $permalink_structure = preg_replace( '#/+#', '/', '/' . str_replace( '#', '', $permalink_structure ) ); if ( $prefix && $blog_prefix ) { $permalink_structure = $prefix . preg_replace( '#^/?index\.php#', '', $permalink_structure ); } else { $permalink_structure = $blog_prefix . $permalink_structure; } } $wp_rewrite->set_permalink_structure( $permalink_structure ); // Update category or tag bases if ( isset( $assoc_args['category-base'] ) ) { $category_base = $assoc_args['category-base']; if ( ! empty( $category_base ) ) { $category_base = $blog_prefix . preg_replace( '#/+#', '/', '/' . str_replace( '#', '', $category_base ) ); } $wp_rewrite->set_category_base( $category_base ); } if ( isset( $assoc_args['tag-base'] ) ) { $tag_base = $assoc_args['tag-base']; if ( ! empty( $tag_base ) ) { $tag_base = $blog_prefix . preg_replace( '#/+#', '/', '/' . str_replace( '#', '', $tag_base ) ); } $wp_rewrite->set_tag_base( $tag_base ); } // make sure we detect mod_rewrite if configured in apache_modules in config self::apache_modules(); WP_CLI::success( 'Rewrite structure set.' ); // Launch a new process to flush rewrites because core expects flush // to happen after rewrites are set $new_assoc_args = []; $cmd = 'rewrite flush'; if ( Utils\get_flag_value( $assoc_args, 'hard' ) ) { $cmd .= ' --hard'; $new_assoc_args['hard'] = true; if ( ! in_array( 'mod_rewrite', (array) WP_CLI::get_config( 'apache_modules' ), true ) ) { WP_CLI::warning( 'Regenerating a .htaccess file requires special configuration. See usage docs.' ); } } $process_run = WP_CLI::runcommand( $cmd ); if ( ! empty( $process_run->stderr ) ) { // Strip "Warning: " WP_CLI::warning( substr( $process_run->stderr, 9 ) ); } } /** * Gets a list of the current rewrite rules. * * ## OPTIONS * * [--match=<url>] * : Show rewrite rules matching a particular URL. * * [--source=<source>] * : Show rewrite rules from a particular source. * * [--fields=<fields>] * : Limit the output to specific fields. Defaults to match,query,source. * * [--format=<format>] * : Render output in a particular format. * --- * default: table * options: * - table * - csv * - json * - count * - yaml * --- * * ## EXAMPLES * * $ wp rewrite list --format=csv * match,query,source * ^wp-json/?$,index.php?rest_route=/,other * ^wp-json/(.*)?,index.php?rest_route=/$matches[1],other * category/(.+?)/feed/(feed|rdf|rss|rss2|atom)/?$,index.php?category_name=$matches[1]&feed=$matches[2],category * category/(.+?)/(feed|rdf|rss|rss2|atom)/?$,index.php?category_name=$matches[1]&feed=$matches[2],category * category/(.+?)/embed/?$,index.php?category_name=$matches[1]&embed=true,category * * @subcommand list */ public function list_( $args, $assoc_args ) { global $wp_rewrite; $rules = get_option( 'rewrite_rules' ); if ( ! $rules ) { $rules = []; WP_CLI::warning( 'No rewrite rules.' ); } self::check_skip_plugins_themes(); $defaults = [ 'source' => '', 'match' => '', 'format' => 'table', 'fields' => 'match,query,source', ]; $assoc_args = array_merge( $defaults, $assoc_args ); $rewrite_rules_by_source = []; $rewrite_rules_by_source['post'] = $wp_rewrite->generate_rewrite_rules( $wp_rewrite->permalink_structure, EP_PERMALINK ); $rewrite_rules_by_source['date'] = $wp_rewrite->generate_rewrite_rules( $wp_rewrite->get_date_permastruct(), EP_DATE ); $rewrite_rules_by_source['root'] = $wp_rewrite->generate_rewrite_rules( $wp_rewrite->root . '/', EP_ROOT ); $rewrite_rules_by_source['comments'] = $wp_rewrite->generate_rewrite_rules( $wp_rewrite->root . $wp_rewrite->comments_base, EP_COMMENTS, true, true, true, false ); $rewrite_rules_by_source['search'] = $wp_rewrite->generate_rewrite_rules( $wp_rewrite->get_search_permastruct(), EP_SEARCH ); $rewrite_rules_by_source['author'] = $wp_rewrite->generate_rewrite_rules( $wp_rewrite->get_author_permastruct(), EP_AUTHORS ); $rewrite_rules_by_source['page'] = $wp_rewrite->page_rewrite_rules(); // Extra permastructs including tags, categories, etc. foreach ( $wp_rewrite->extra_permastructs as $permastructname => $permastruct ) { if ( is_array( $permastruct ) ) { $rewrite_rules_by_source[ $permastructname ] = $wp_rewrite->generate_rewrite_rules( $permastruct['struct'], $permastruct['ep_mask'], $permastruct['paged'], $permastruct['feed'], $permastruct['forcomments'], $permastruct['walk_dirs'], $permastruct['endpoints'] ); } else { $rewrite_rules_by_source[ $permastructname ] = $wp_rewrite->generate_rewrite_rules( $permastruct, EP_NONE ); } } // Apply the filters used in core just in case foreach ( $rewrite_rules_by_source as $source => $source_rules ) { // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.DynamicHooknameFound -- Can't prefix dynamic hooks here, calling hooks for custom permastructs. $rewrite_rules_by_source[ $source ] = apply_filters( $source . '_rewrite_rules', $source_rules ); if ( 'post_tag' === $source ) { if ( Utils\wp_version_compare( '3.1.0', '>=' ) ) { // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- Calling native WordPress hook. $rewrite_rules_by_source[ $source ] = apply_filters( 'post_tag_rewrite_rules', $source_rules ); } else { // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- Calling native WordPress hook. $rewrite_rules_by_source[ $source ] = apply_filters( 'tag_rewrite_rules', $source_rules ); } } } $rule_list = []; foreach ( $rules as $match => $query ) { if ( ! empty( $assoc_args['match'] ) && ! preg_match( "!^$match!", trim( $assoc_args['match'], '/' ) ) ) { continue; } $source = 'other'; foreach ( $rewrite_rules_by_source as $rules_source => $source_rules ) { if ( array_key_exists( $match, $source_rules ) ) { $source = $rules_source; } } if ( ! empty( $assoc_args['source'] ) && $source !== $assoc_args['source'] ) { continue; } $rule_list[] = compact( 'match', 'query', 'source' ); } Utils\format_items( $assoc_args['format'], $rule_list, explode( ',', $assoc_args['fields'] ) ); } /** * Exposes apache modules if present in config * * Implementation Notes: This function exposes a global function * apache_get_modules and also sets the $is_apache global variable. * * This is so that flush_rewrite_rules will actually write out the * .htaccess file for apache WordPress installations. There is a check * to see: * * 1. if the $is_apache variable is set. * 2. if the mod_rewrite module is returned from the apache_get_modules * function. * * To get this to work with wp-cli you'll need to add the mod_rewrite module * to your config.yml. For example * * ``` * apache_modules: * - mod_rewrite * ``` * * If this isn't done then the .htaccess rewrite rules won't be flushed out * to disk. */ private static function apache_modules() { $mods = WP_CLI::get_config( 'apache_modules' ); if ( ! empty( $mods ) && ! function_exists( 'apache_get_modules' ) ) { global $is_apache; // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited $is_apache = true; // needed for get_home_path() and .htaccess location $_SERVER['SCRIPT_FILENAME'] = ABSPATH; function apache_get_modules() { return WP_CLI::get_config( 'apache_modules' ); } } } /** * Displays a warning if --skip-plugins or --skip-themes are in use. * * Skipping the loading of plugins or themes can mean some rewrite rules * are unregistered, which may cause erroneous behavior. */ private static function check_skip_plugins_themes() { $skipped = []; if ( WP_CLI::get_config( 'skip-plugins' ) ) { $skipped[] = 'plugins'; } if ( WP_CLI::get_config( 'skip-themes' ) ) { $skipped[] = 'themes'; } if ( empty( $skipped ) ) { return; } $skipped = implode( ' and ', $skipped ); WP_CLI::warning( sprintf( "Some rewrite rules may be missing because %s weren't loaded.", $skipped ) ); } } WP_CLI::add_command( 'rewrite', 'Rewrite_Command' ); <?php if ( ! class_exists( 'WP_CLI' ) ) { return; } $wpcli_rewrite_autoloader = dirname( __FILE__ ) . '/vendor/autoload.php'; if ( file_exists( $wpcli_rewrite_autoloader ) ) { require_once $wpcli_rewrite_autoloader; } WP_CLI::add_command( 'rewrite', 'Rewrite_Command' ); <?php if ( ! class_exists( 'WP_CLI' ) ) { return; } $wpcli_eval_autoloader = dirname( __FILE__ ) . '/vendor/autoload.php'; if ( file_exists( $wpcli_eval_autoloader ) ) { require_once $wpcli_eval_autoloader; } WP_CLI::add_command( 'eval', 'Eval_Command' ); WP_CLI::add_command( 'eval-file', 'EvalFile_Command' ); <?php use WP_CLI\Utils; class EvalFile_Command extends WP_CLI_Command { /** * Regular expression pattern to match the shell shebang. * * @var string */ const SHEBANG_PATTERN = '/^(#!.*)$/m'; /** * Loads and executes a PHP file. * * Note: because code is executed within a method, global variables need * to be explicitly globalized. * * ## OPTIONS * * <file> * : The path to the PHP file to execute. Use '-' to run code from STDIN. * * [<arg>...] * : One or more arguments to pass to the file. They are placed in the $args variable. * * [--skip-wordpress] * : Load and execute file without loading WordPress. * * @when before_wp_load * * ## EXAMPLES * * wp eval-file my-code.php value1 value2 */ public function __invoke( $args, $assoc_args ) { $file = array_shift( $args ); if ( '-' !== $file && ! file_exists( $file ) ) { WP_CLI::error( "'$file' does not exist." ); } if ( null === Utils\get_flag_value( $assoc_args, 'skip-wordpress' ) ) { WP_CLI::get_runner()->load_wordpress(); } self::execute_eval( $file, $args ); } /** * Evaluate a provided file. * * @param string $file Filepath to execute, or - for STDIN. * @param mixed $args Array of positional arguments to pass to the file. */ private static function execute_eval( $file, $args ) { if ( '-' === $file ) { eval( '?>' . file_get_contents( 'php://stdin' ) ); } else { $file_contents = file_get_contents( $file ); // Adjust for __FILE__ and __DIR__ magic constants. $file_contents = Utils\replace_path_consts( $file_contents, $file ); // Check for and remove she-bang. if ( 0 === strncmp( $file_contents, '#!', 2 ) ) { $file_contents = preg_replace( static::SHEBANG_PATTERN, '', $file_contents ); } eval( '?>' . $file_contents ); } } } <?php use WP_CLI\Utils; class Eval_Command extends WP_CLI_Command { /** * Executes arbitrary PHP code. * * Note: because code is executed within a method, global variables need * to be explicitly globalized. * * ## OPTIONS * * <php-code> * : The code to execute, as a string. * * [--skip-wordpress] * : Execute code without loading WordPress. * * ## EXAMPLES * * # Display WordPress content directory. * $ wp eval 'echo WP_CONTENT_DIR;' * /var/www/wordpress/wp-content * * # Generate a random number. * $ wp eval 'echo rand();' --skip-wordpress * 479620423 * * @when before_wp_load */ public function __invoke( $args, $assoc_args ) { if ( null === Utils\get_flag_value( $assoc_args, 'skip-wordpress' ) ) { WP_CLI::get_runner()->load_wordpress(); } eval( $args[0] ); } } <?php if ( ! class_exists( 'WP_CLI' ) ) { return; } $wpcli_scaffold_autoloader = dirname( __FILE__ ) . '/vendor/autoload.php'; if ( file_exists( $wpcli_scaffold_autoloader ) ) { require_once $wpcli_scaffold_autoloader; } WP_CLI::add_command( 'scaffold', 'Scaffold_Command' ); <?php use WP_CLI\Utils; use WP_CLI\Inflector; /** * Generates code for post types, taxonomies, plugins, child themes, etc. * * ## EXAMPLES * * # Generate a new plugin with unit tests * $ wp scaffold plugin sample-plugin * Success: Created plugin files. * Success: Created test files. * * # Generate theme based on _s * $ wp scaffold _s sample-theme --theme_name="Sample Theme" --author="John Doe" * Success: Created theme 'Sample Theme'. * * # Generate code for post type registration in given theme * $ wp scaffold post-type movie --label=Movie --theme=simple-life * Success: Created /var/www/example.com/public_html/wp-content/themes/simple-life/post-types/movie.php * * @package wp-cli */ class Scaffold_Command extends WP_CLI_Command { /** * Generates PHP code for registering a custom post type. * * ## OPTIONS * * <slug> * : The internal name of the post type. * * [--label=<label>] * : The text used to translate the update messages. * * [--textdomain=<textdomain>] * : The textdomain to use for the labels. * * [--dashicon=<dashicon>] * : The dashicon to use in the menu. * * [--theme] * : Create a file in the active theme directory, instead of sending to * STDOUT. Specify a theme with `--theme=<theme>` to have the file placed in that theme. * * [--plugin=<plugin>] * : Create a file in the given plugin's directory, instead of sending to STDOUT. * * [--raw] * : Just generate the `register_post_type()` call and nothing else. * * [--force] * : Overwrite files that already exist. * * ## EXAMPLES * * # Generate a 'movie' post type for the 'simple-life' theme * $ wp scaffold post-type movie --label=Movie --theme=simple-life * Success: Created '/var/www/example.com/public_html/wp-content/themes/simple-life/post-types/movie.php'. * * @subcommand post-type * * @alias cpt */ public function post_type( $args, $assoc_args ) { if ( strlen( $args[0] ) > 20 ) { WP_CLI::error( 'Post type slugs cannot exceed 20 characters in length.' ); } $defaults = [ 'textdomain' => '', 'dashicon' => 'admin-post', ]; $templates = [ 'post_type.mustache', 'post_type_extended.mustache', ]; $this->scaffold( $args[0], $assoc_args, $defaults, '/post-types/', $templates ); } /** * Generates PHP code for registering a custom taxonomy. * * ## OPTIONS * * <slug> * : The internal name of the taxonomy. * * [--post_types=<post-types>] * : Post types to register for use with the taxonomy. * * [--label=<label>] * : The text used to translate the update messages. * * [--textdomain=<textdomain>] * : The textdomain to use for the labels. * * [--theme] * : Create a file in the active theme directory, instead of sending to * STDOUT. Specify a theme with `--theme=<theme>` to have the file placed in that theme. * * [--plugin=<plugin>] * : Create a file in the given plugin's directory, instead of sending to STDOUT. * * [--raw] * : Just generate the `register_taxonomy()` call and nothing else. * * [--force] * : Overwrite files that already exist. * * ## EXAMPLES * * # Generate PHP code for registering a custom taxonomy and save in a file * $ wp scaffold taxonomy venue --post_types=event,presentation > taxonomy.php * * @subcommand taxonomy * * @alias tax */ public function taxonomy( $args, $assoc_args ) { $defaults = [ 'textdomain' => '', 'post_types' => "'post'", ]; if ( isset( $assoc_args['post_types'] ) ) { $assoc_args['post_types'] = $this->quote_comma_list_elements( $assoc_args['post_types'] ); } $templates = [ 'taxonomy.mustache', 'taxonomy_extended.mustache', ]; $this->scaffold( $args[0], $assoc_args, $defaults, '/taxonomies/', $templates ); } private function scaffold( $slug, $assoc_args, $defaults, $subdir, $templates ) { $wp_filesystem = $this->init_wp_filesystem(); $control_defaults = [ 'label' => preg_replace( '/_|-/', ' ', strtolower( $slug ) ), 'theme' => false, 'plugin' => false, 'raw' => false, ]; $control_args = $this->extract_args( $assoc_args, $control_defaults ); $vars = $this->extract_args( $assoc_args, $defaults ); $dashicon = $this->extract_dashicon( $assoc_args ); if ( $dashicon ) { $vars['dashicon'] = $dashicon; } $vars['slug'] = $slug; $vars['textdomain'] = $this->get_textdomain( $vars['textdomain'], $control_args ); $vars['label'] = $control_args['label']; $vars['label_ucfirst'] = ucfirst( $vars['label'] ); $vars['label_plural'] = $this->pluralize( $vars['label'] ); $vars['label_plural_ucfirst'] = ucfirst( $vars['label_plural'] ); $machine_name = $this->generate_machine_name( $slug ); $machine_name_plural = $this->pluralize( $slug ); list( $raw_template, $extended_template ) = $templates; $raw_output = self::mustache_render( $raw_template, $vars ); if ( ! $control_args['raw'] ) { $vars['machine_name'] = $machine_name; $vars['output'] = $raw_output; $final_output = self::mustache_render( $extended_template, $vars ); } else { $final_output = $raw_output; } $path = $this->get_output_path( $control_args, $subdir ); if ( is_string( $path ) && ! empty( $path ) ) { $filename = "{$path}{$slug}.php"; $force = Utils\get_flag_value( $assoc_args, 'force' ); $files_written = $this->create_files( [ $filename => $final_output ], $force ); $skip_message = "Skipped creating '{$filename}'."; $success_message = "Created '{$filename}'."; $this->log_whether_files_written( $files_written, $skip_message, $success_message ); } else { // STDOUT echo $final_output; } } /** * Generates PHP, JS and CSS code for registering a Gutenberg block for a plugin or theme. * * Blocks are the fundamental element of the Gutenberg editor. They are the primary way in which plugins and themes can register their own functionality and extend the capabilities of the editor. * * Visit the [Gutenberg handbook](https://wordpress.org/gutenberg/handbook/block-api/) to learn more about Block API. * * When you scaffold a block you must use either the theme or plugin option. The latter is recommended. * * ## OPTIONS * * <slug> * : The internal name of the block. * * [--title=<title>] * : The display title for your block. * * [--dashicon=<dashicon>] * : The dashicon to make it easier to identify your block. * * [--category=<category>] * : The category name to help users browse and discover your block. * --- * default: widgets * options: * - common * - embed * - formatting * - layout * - widgets * --- * * [--theme] * : Create files in the active theme directory. Specify a theme with `--theme=<theme>` to have the file placed in that theme. * * [--plugin=<plugin>] * : Create files in the given plugin's directory. * * [--force] * : Overwrite files that already exist. * * ## EXAMPLES * * # Generate a 'movie' block for the 'movies' plugin * $ wp scaffold block movie --title="Movie block" --plugin=movies * Success: Created block 'Movie block'. * * # Generate a 'movie' block for the 'simple-life' theme * $ wp scaffold block movie --title="Movie block" --theme=simple-life * Success: Created block 'Movie block'. * * # Create a new plugin and add two blocks * # Create plugin called books * $ wp scaffold plugin books * # Add a block called book to plugin books * $ wp scaffold block book --title="Book" --plugin=books * # Add a second block to plugin called books. * $ wp scaffold block books --title="Book List" --plugin=books * * @subcommand block */ public function block( $args, $assoc_args ) { $slug = $args[0]; if ( ! preg_match( '/^[a-z][a-z0-9\-]*$/', $slug ) ) { WP_CLI::error( 'Invalid block slug specified. Block slugs can contain only lowercase alphanumeric characters or dashes, and start with a letter.' ); } $defaults = [ 'title' => str_replace( '-', ' ', $slug ), 'category' => 'widgets', ]; $data = $this->extract_args( $assoc_args, $defaults ); $data['slug'] = $slug; $data['title_ucfirst'] = ucfirst( $data['title'] ); $data['title_ucfirst_js'] = esc_js( $data['title_ucfirst'] ); $dashicon = $this->extract_dashicon( $assoc_args ); if ( $dashicon ) { $data['dashicon'] = $dashicon; } $control_defaults = [ 'force' => false, 'plugin' => false, 'theme' => false, ]; $control_args = $this->extract_args( $assoc_args, $control_defaults ); if ( isset( $control_args['plugin'] ) ) { if ( ! preg_match( '/^[A-Za-z0-9\-]*$/', $control_args['plugin'] ) ) { WP_CLI::error( 'Invalid plugin name specified. The block editor can only register blocks for plugins that have nothing but lowercase alphanumeric characters or dashes in their slug.' ); } } $data['namespace'] = $control_args['plugin'] ? $control_args['plugin'] : $this->get_theme_name( $control_args['theme'] ); $data['machine_name'] = $this->generate_machine_name( $slug ); $data['plugin'] = $control_args['plugin'] ? true : false; $data['theme'] = ! $data['plugin']; $block_dir = $this->get_output_path( $control_args, '/blocks' ); if ( ! $block_dir ) { WP_CLI::error( 'No plugin or theme selected.' ); } $files_to_create = [ "{$block_dir}/{$slug}.php" => self::mustache_render( 'block-php.mustache', $data ), "{$block_dir}/{$slug}/index.js" => self::mustache_render( 'block-index-js.mustache', $data ), "{$block_dir}/{$slug}/editor.css" => self::mustache_render( 'block-editor-css.mustache', $data ), "{$block_dir}/{$slug}/style.css" => self::mustache_render( 'block-style-css.mustache', $data ), ]; $files_written = $this->create_files( $files_to_create, $control_args['force'] ); $skip_message = 'All block files were skipped.'; $success_message = "Created block '{$data['title_ucfirst']}'."; $this->log_whether_files_written( $files_written, $skip_message, $success_message ); } /** * Generates starter code for a theme based on _s. * * See the [Underscores website](https://underscores.me/) for more details. * * ## OPTIONS * * <slug> * : The slug for the new theme, used for prefixing functions. * * [--activate] * : Activate the newly downloaded theme. * * [--enable-network] * : Enable the newly downloaded theme for the entire network. * * [--theme_name=<title>] * : What to put in the 'Theme Name:' header in 'style.css'. * * [--author=<full-name>] * : What to put in the 'Author:' header in 'style.css'. * * [--author_uri=<uri>] * : What to put in the 'Author URI:' header in 'style.css'. * * [--sassify] * : Include stylesheets as SASS. * * [--woocommerce] * : Include WooCommerce boilerplate files. * * [--force] * : Overwrite files that already exist. * * ## EXAMPLES * * # Generate a theme with name "Sample Theme" and author "John Doe" * $ wp scaffold _s sample-theme --theme_name="Sample Theme" --author="John Doe" * Success: Created theme 'Sample Theme'. * * @alias _s */ public function underscores( $args, $assoc_args ) { $theme_slug = $args[0]; $theme_path = WP_CONTENT_DIR . '/themes'; $url = 'https://underscores.me'; $timeout = 30; if ( ! preg_match( '/^[a-z_]\w+$/i', str_replace( '-', '_', $theme_slug ) ) ) { WP_CLI::error( 'Invalid theme slug specified. Theme slugs can only contain letters, numbers, underscores and hyphens, and can only start with a letter or underscore.' ); } $defaults = [ 'theme_name' => ucfirst( $theme_slug ), 'author' => 'Me', 'author_uri' => '', ]; $data = wp_parse_args( $assoc_args, $defaults ); $_s_theme_path = "$theme_path/$data[theme_name]"; $error_msg = $this->check_target_directory( 'theme', $_s_theme_path ); if ( ! empty( $error_msg ) ) { WP_CLI::error( "Invalid theme slug specified. {$error_msg}" ); } $force = Utils\get_flag_value( $assoc_args, 'force' ); $should_write_file = $this->prompt_if_files_will_be_overwritten( $_s_theme_path, $force ); if ( ! $should_write_file ) { WP_CLI::log( 'No files created' ); die; } $theme_description = "Custom theme: {$data['theme_name']}, developed by {$data['author']}"; $body = []; $body['underscoresme_name'] = $data['theme_name']; $body['underscoresme_slug'] = $theme_slug; $body['underscoresme_author'] = $data['author']; $body['underscoresme_author_uri'] = $data['author_uri']; $body['underscoresme_description'] = $theme_description; $body['underscoresme_generate_submit'] = 'Generate'; $body['underscoresme_generate'] = '1'; if ( Utils\get_flag_value( $assoc_args, 'sassify' ) ) { $body['underscoresme_sass'] = 1; } if ( Utils\get_flag_value( $assoc_args, 'woocommerce' ) ) { $body['underscoresme_woocommerce'] = 1; } $tmpfname = wp_tempnam( $url ); $post_args = [ 'timeout' => $timeout, 'body' => $body, 'stream' => true, 'filename' => $tmpfname, ]; $response = wp_remote_post( $url, $post_args ); // Workaround to get scaffolding to work within Travis CI. // See https://github.com/wp-cli/scaffold-command/issues/181 if ( is_wp_error( $response ) && false !== strpos( $response->get_error_message(), 'gnutls_handshake() failed' ) ) { // Certificate problem, falling back to unsecured request instead. $alt_url = str_replace( 'https://', 'http://', $url ); WP_CLI::warning( "Secured request to {$url} failed, using {$alt_url} as a fallback." ); $response = wp_remote_post( $alt_url, $post_args ); } if ( is_wp_error( $response ) ) { WP_CLI::error( $response ); } $response_code = wp_remote_retrieve_response_code( $response ); if ( 200 !== (int) $response_code ) { WP_CLI::error( "Couldn't create theme (received {$response_code} response)." ); } $this->maybe_create_themes_dir(); $this->init_wp_filesystem(); $unzip_result = unzip_file( $tmpfname, $theme_path ); unlink( $tmpfname ); if ( true === $unzip_result ) { $files_to_create = [ "{$theme_path}/{$theme_slug}/.editorconfig" => file_get_contents( self::get_template_path( '.editorconfig' ) ), ]; $this->create_files( $files_to_create, false ); WP_CLI::success( "Created theme '{$data['theme_name']}'." ); } else { WP_CLI::error( "Could not decompress your theme files ('{$tmpfname}') at '{$theme_path}': {$unzip_result->get_error_message()}" ); } if ( Utils\get_flag_value( $assoc_args, 'activate' ) ) { WP_CLI::run_command( [ 'theme', 'activate', $theme_slug ] ); } elseif ( Utils\get_flag_value( $assoc_args, 'enable-network' ) ) { WP_CLI::run_command( [ 'theme', 'enable', $theme_slug ], [ 'network' => true ] ); } } /** * Generates child theme based on an existing theme. * * Creates a child theme folder with `functions.php` and `style.css` files. * * ## OPTIONS * * <slug> * : The slug for the new child theme. * * --parent_theme=<slug> * : What to put in the 'Template:' header in 'style.css'. * * [--theme_name=<title>] * : What to put in the 'Theme Name:' header in 'style.css'. * * [--author=<full-name>] * : What to put in the 'Author:' header in 'style.css'. * * [--author_uri=<uri>] * : What to put in the 'Author URI:' header in 'style.css'. * * [--theme_uri=<uri>] * : What to put in the 'Theme URI:' header in 'style.css'. * * [--activate] * : Activate the newly created child theme. * * [--enable-network] * : Enable the newly created child theme for the entire network. * * [--force] * : Overwrite files that already exist. * * ## EXAMPLES * * # Generate a 'sample-theme' child theme based on TwentySixteen * $ wp scaffold child-theme sample-theme --parent_theme=twentysixteen * Success: Created '/var/www/example.com/public_html/wp-content/themes/sample-theme'. * * @subcommand child-theme */ public function child_theme( $args, $assoc_args ) { $theme_slug = $args[0]; if ( in_array( $theme_slug, [ '.', '..' ], true ) ) { WP_CLI::error( "Invalid theme slug specified. The slug cannot be '.' or '..'." ); } $defaults = [ 'theme_name' => ucfirst( $theme_slug ), 'author' => 'Me', 'author_uri' => '', 'theme_uri' => '', ]; $data = wp_parse_args( $assoc_args, $defaults ); $data['slug'] = $theme_slug; $data['parent_theme_function_safe'] = str_replace( [ ' ', '-' ], '_', $data['parent_theme'] ); $data['description'] = ucfirst( $data['parent_theme'] ) . ' child theme.'; $theme_dir = WP_CONTENT_DIR . "/themes/{$theme_slug}"; $error_msg = $this->check_target_directory( 'theme', $theme_dir ); if ( ! empty( $error_msg ) ) { WP_CLI::error( "Invalid theme slug specified. {$error_msg}" ); } $theme_style_path = "{$theme_dir}/style.css"; $theme_functions_path = "{$theme_dir}/functions.php"; $this->maybe_create_themes_dir(); $files_to_create = [ $theme_style_path => self::mustache_render( 'child_theme.mustache', $data ), $theme_functions_path => self::mustache_render( 'child_theme_functions.mustache', $data ), "{$theme_dir}/.editorconfig" => file_get_contents( self::get_template_path( '.editorconfig' ) ), ]; $force = Utils\get_flag_value( $assoc_args, 'force' ); $files_written = $this->create_files( $files_to_create, $force ); $skip_message = 'All theme files were skipped.'; $success_message = "Created '{$theme_dir}'."; $this->log_whether_files_written( $files_written, $skip_message, $success_message ); if ( Utils\get_flag_value( $assoc_args, 'activate' ) ) { WP_CLI::run_command( [ 'theme', 'activate', $theme_slug ] ); } elseif ( Utils\get_flag_value( $assoc_args, 'enable-network' ) ) { WP_CLI::run_command( [ 'theme', 'enable', $theme_slug ], [ 'network' => true ] ); } } private function get_output_path( $assoc_args, $subdir ) { if ( $assoc_args['theme'] ) { $theme = $assoc_args['theme']; if ( is_string( $theme ) ) { $path = get_theme_root( $theme ) . "/{$theme}"; } else { $path = get_stylesheet_directory(); } if ( ! is_dir( $path ) ) { WP_CLI::error( "Can't find '{$theme}' theme." ); } } elseif ( $assoc_args['plugin'] ) { $plugin = $assoc_args['plugin']; $path = WP_PLUGIN_DIR . "/{$plugin}"; if ( ! is_dir( $path ) ) { WP_CLI::error( "Can't find '{$plugin}' plugin." ); } } else { return false; } $path .= $subdir; return $path; } /** * Generates starter code for a plugin. * * The following files are always generated: * * * `plugin-slug.php` is the main PHP plugin file. * * `readme.txt` is the readme file for the plugin. * * `package.json` needed by NPM holds various metadata relevant to the project. Packages: `grunt`, `grunt-wp-i18n` and `grunt-wp-readme-to-markdown`. Scripts: `start`, `readme`, `i18n`. * * `Gruntfile.js` is the JS file containing Grunt tasks. Tasks: `i18n` containing `addtextdomain` and `makepot`, `readme` containing `wp_readme_to_markdown`. * * `.editorconfig` is the configuration file for Editor. * * `.gitignore` tells which files (or patterns) git should ignore. * * `.distignore` tells which files and folders should be ignored in distribution. * * The following files are also included unless the `--skip-tests` is used: * * * `phpunit.xml.dist` is the configuration file for PHPUnit. * * `.travis.yml` is the configuration file for Travis CI. Use `--ci=<provider>` to select a different service. * * `bin/install-wp-tests.sh` configures the WordPress test suite and a test database. * * `tests/bootstrap.php` is the file that makes the current plugin active when running the test suite. * * `tests/test-sample.php` is a sample file containing test cases. * * `.phpcs.xml.dist` is a collection of PHP_CodeSniffer rules. * * ## OPTIONS * * <slug> * : The internal name of the plugin. * * [--dir=<dirname>] * : Put the new plugin in some arbitrary directory path. Plugin directory will be path plus supplied slug. * * [--plugin_name=<title>] * : What to put in the 'Plugin Name:' header. * * [--plugin_description=<description>] * : What to put in the 'Description:' header. * * [--plugin_author=<author>] * : What to put in the 'Author:' header. * * [--plugin_author_uri=<url>] * : What to put in the 'Author URI:' header. * * [--plugin_uri=<url>] * : What to put in the 'Plugin URI:' header. * * [--skip-tests] * : Don't generate files for unit testing. * * [--ci=<provider>] * : Choose a configuration file for a continuous integration provider. * --- * default: travis * options: * - travis * - circle * - gitlab * --- * * [--activate] * : Activate the newly generated plugin. * * [--activate-network] * : Network activate the newly generated plugin. * * [--force] * : Overwrite files that already exist. * * ## EXAMPLES * * $ wp scaffold plugin sample-plugin * Success: Created plugin files. * Success: Created test files. */ public function plugin( $args, $assoc_args ) { $plugin_slug = $args[0]; $plugin_name = ucwords( str_replace( '-', ' ', $plugin_slug ) ); $plugin_package = str_replace( ' ', '_', $plugin_name ); if ( in_array( $plugin_slug, [ '.', '..' ], true ) ) { WP_CLI::error( "Invalid plugin slug specified. The slug cannot be '.' or '..'." ); } $defaults = [ 'plugin_slug' => $plugin_slug, 'plugin_name' => $plugin_name, 'plugin_package' => $plugin_package, 'plugin_description' => 'PLUGIN DESCRIPTION HERE', 'plugin_author' => 'YOUR NAME HERE', 'plugin_author_uri' => 'YOUR SITE HERE', 'plugin_uri' => 'PLUGIN SITE HERE', 'plugin_tested_up_to' => get_bloginfo( 'version' ), ]; $data = wp_parse_args( $assoc_args, $defaults ); $data['textdomain'] = $plugin_slug; if ( ! empty( $assoc_args['dir'] ) ) { if ( ! is_dir( $assoc_args['dir'] ) ) { WP_CLI::error( "Cannot create plugin in directory that doesn't exist." ); } $plugin_dir = "{$assoc_args['dir']}/{$plugin_slug}"; } else { $plugin_dir = WP_PLUGIN_DIR . "/{$plugin_slug}"; $this->maybe_create_plugins_dir(); $error_msg = $this->check_target_directory( 'plugin', $plugin_dir ); if ( ! empty( $error_msg ) ) { WP_CLI::error( "Invalid plugin slug specified. {$error_msg}" ); } } $plugin_path = "{$plugin_dir}/{$plugin_slug}.php"; $plugin_readme_path = "{$plugin_dir}/readme.txt"; $files_to_create = [ $plugin_path => self::mustache_render( 'plugin.mustache', $data ), $plugin_readme_path => self::mustache_render( 'plugin-readme.mustache', $data ), "{$plugin_dir}/package.json" => self::mustache_render( 'plugin-packages.mustache', $data ), "{$plugin_dir}/Gruntfile.js" => self::mustache_render( 'plugin-gruntfile.mustache', $data ), "{$plugin_dir}/.gitignore" => self::mustache_render( 'plugin-gitignore.mustache', $data ), "{$plugin_dir}/.distignore" => self::mustache_render( 'plugin-distignore.mustache', $data ), "{$plugin_dir}/.editorconfig" => file_get_contents( self::get_template_path( '.editorconfig' ) ), ]; $force = Utils\get_flag_value( $assoc_args, 'force' ); $files_written = $this->create_files( $files_to_create, $force ); $skip_message = 'All plugin files were skipped.'; $success_message = 'Created plugin files.'; $this->log_whether_files_written( $files_written, $skip_message, $success_message ); if ( ! Utils\get_flag_value( $assoc_args, 'skip-tests' ) ) { $command_args = [ 'dir' => $plugin_dir, 'ci' => empty( $assoc_args['ci'] ) ? '' : $assoc_args['ci'], 'force' => $force, ]; WP_CLI::run_command( [ 'scaffold', 'plugin-tests', $plugin_slug ], $command_args ); } if ( Utils\get_flag_value( $assoc_args, 'activate' ) ) { WP_CLI::run_command( [ 'plugin', 'activate', $plugin_slug ] ); } elseif ( Utils\get_flag_value( $assoc_args, 'activate-network' ) ) { WP_CLI::run_command( [ 'plugin', 'activate', $plugin_slug ], [ 'network' => true ] ); } } /** * Generates files needed for running PHPUnit tests in a plugin. * * The following files are generated by default: * * * `phpunit.xml.dist` is the configuration file for PHPUnit. * * `.travis.yml` is the configuration file for Travis CI. Use `--ci=<provider>` to select a different service. * * `bin/install-wp-tests.sh` configures the WordPress test suite and a test database. * * `tests/bootstrap.php` is the file that makes the current plugin active when running the test suite. * * `tests/test-sample.php` is a sample file containing the actual tests. * * `.phpcs.xml.dist` is a collection of PHP_CodeSniffer rules. * * Learn more from the [plugin unit tests documentation](https://make.wordpress.org/cli/handbook/plugin-unit-tests/). * * ## ENVIRONMENT * * The `tests/bootstrap.php` file looks for the WP_TESTS_DIR environment * variable. * * ## OPTIONS * * [<plugin>] * : The name of the plugin to generate test files for. * * [--dir=<dirname>] * : Generate test files for a non-standard plugin path. If no plugin slug is specified, the directory name is used. * * [--ci=<provider>] * : Choose a configuration file for a continuous integration provider. * --- * default: travis * options: * - travis * - circle * - gitlab * - bitbucket * --- * * [--force] * : Overwrite files that already exist. * * ## EXAMPLES * * # Generate unit test files for plugin 'sample-plugin'. * $ wp scaffold plugin-tests sample-plugin * Success: Created test files. * * @subcommand plugin-tests */ public function plugin_tests( $args, $assoc_args ) { $this->scaffold_plugin_theme_tests( $args, $assoc_args, 'plugin' ); } /** * Generates files needed for running PHPUnit tests in a theme. * * The following files are generated by default: * * * `phpunit.xml.dist` is the configuration file for PHPUnit. * * `.travis.yml` is the configuration file for Travis CI. Use `--ci=<provider>` to select a different service. * * `bin/install-wp-tests.sh` configures the WordPress test suite and a test database. * * `tests/bootstrap.php` is the file that makes the current theme active when running the test suite. * * `tests/test-sample.php` is a sample file containing the actual tests. * * `.phpcs.xml.dist` is a collection of PHP_CodeSniffer rules. * * Learn more from the [plugin unit tests documentation](https://make.wordpress.org/cli/handbook/plugin-unit-tests/). * * ## ENVIRONMENT * * The `tests/bootstrap.php` file looks for the WP_TESTS_DIR environment * variable. * * ## OPTIONS * * [<theme>] * : The name of the theme to generate test files for. * * [--dir=<dirname>] * : Generate test files for a non-standard theme path. If no theme slug is specified, the directory name is used. * * [--ci=<provider>] * : Choose a configuration file for a continuous integration provider. * --- * default: travis * options: * - travis * - circle * - gitlab * - bitbucket * --- * * [--force] * : Overwrite files that already exist. * * ## EXAMPLES * * # Generate unit test files for theme 'twentysixteenchild'. * $ wp scaffold theme-tests twentysixteenchild * Success: Created test files. * * @subcommand theme-tests */ public function theme_tests( $args, $assoc_args ) { $this->scaffold_plugin_theme_tests( $args, $assoc_args, 'theme' ); } private function scaffold_plugin_theme_tests( $args, $assoc_args, $type ) { $wp_filesystem = $this->init_wp_filesystem(); if ( ! empty( $args[0] ) ) { $slug = $args[0]; if ( in_array( $slug, [ '.', '..' ], true ) ) { WP_CLI::error( "Invalid {$type} slug specified. The slug cannot be '.' or '..'." ); } if ( 'theme' === $type ) { $theme = wp_get_theme( $slug ); if ( $theme->exists() ) { $target_dir = $theme->get_stylesheet_directory(); } else { WP_CLI::error( "Invalid {$type} slug specified. The theme '{$slug}' does not exist." ); } } else { $target_dir = WP_PLUGIN_DIR . "/{$slug}"; } if ( empty( $assoc_args['dir'] ) && ! is_dir( $target_dir ) ) { WP_CLI::error( "Invalid {$type} slug specified. No such target directory '{$target_dir}'." ); } $error_msg = $this->check_target_directory( $type, $target_dir ); if ( ! empty( $error_msg ) ) { WP_CLI::error( "Invalid {$type} slug specified. {$error_msg}" ); } } if ( ! empty( $assoc_args['dir'] ) ) { $target_dir = $assoc_args['dir']; if ( ! is_dir( $target_dir ) ) { WP_CLI::error( "Invalid {$type} directory specified. No such directory '{$target_dir}'." ); } if ( empty( $slug ) ) { $slug = Utils\basename( $target_dir ); } } if ( empty( $slug ) || empty( $target_dir ) ) { WP_CLI::error( "Invalid {$type} specified." ); } $name = ucwords( str_replace( '-', ' ', $slug ) ); $package = str_replace( ' ', '_', $name ); $tests_dir = "{$target_dir}/tests"; $bin_dir = "{$target_dir}/bin"; $wp_filesystem->mkdir( $tests_dir ); $wp_filesystem->mkdir( $bin_dir ); $wp_versions_to_test = []; // Parse plugin readme.txt if ( file_exists( "{$target_dir}/readme.txt" ) ) { $readme_content = file_get_contents( "{$target_dir}/readme.txt" ); preg_match( '/Requires at least\:(.*)\n/m', $readme_content, $matches ); if ( isset( $matches[1] ) && $matches[1] ) { $wp_versions_to_test[] = trim( $matches[1] ); } } $wp_versions_to_test[] = 'latest'; $wp_versions_to_test[] = 'trunk'; $template_data = [ "{$type}_slug" => $slug, "{$type}_package" => $package, ]; $force = Utils\get_flag_value( $assoc_args, 'force' ); $files_to_create = [ "{$tests_dir}/bootstrap.php" => self::mustache_render( "{$type}-bootstrap.mustache", $template_data ), "{$tests_dir}/test-sample.php" => self::mustache_render( "{$type}-test-sample.mustache", $template_data ), ]; if ( 'travis' === $assoc_args['ci'] ) { $files_to_create[ "{$target_dir}/.travis.yml" ] = self::mustache_render( 'plugin-travis.mustache', compact( 'wp_versions_to_test' ) ); } elseif ( 'circle' === $assoc_args['ci'] ) { $files_to_create[ "{$target_dir}/.circleci/config.yml" ] = self::mustache_render( 'plugin-circle.mustache', compact( 'wp_versions_to_test' ) ); } elseif ( 'gitlab' === $assoc_args['ci'] ) { $files_to_create[ "{$target_dir}/.gitlab-ci.yml" ] = self::mustache_render( 'plugin-gitlab.mustache' ); } elseif ( 'bitbucket' === $assoc_args['ci'] ) { $files_to_create[ "{$target_dir}/bitbucket-pipelines.yml" ] = self::mustache_render( 'plugin-bitbucket.mustache' ); } $files_written = $this->create_files( $files_to_create, $force ); $to_copy = [ 'install-wp-tests.sh' => $bin_dir, 'phpunit.xml.dist' => $target_dir, '.phpcs.xml.dist' => $target_dir, ]; foreach ( $to_copy as $file => $dir ) { $file_name = "{$dir}/{$file}"; $force = Utils\get_flag_value( $assoc_args, 'force' ); $should_write_file = $this->prompt_if_files_will_be_overwritten( $file_name, $force ); if ( ! $should_write_file ) { continue; } $files_written[] = $file_name; $wp_filesystem->copy( self::get_template_path( $file ), $file_name, true ); if ( 'install-wp-tests.sh' === $file ) { if ( ! $wp_filesystem->chmod( "{$dir}/{$file}", 0755 ) ) { WP_CLI::warning( "Couldn't mark 'install-wp-tests.sh' as executable." ); } } } $skip_message = 'All test files were skipped.'; $success_message = 'Created test files.'; $this->log_whether_files_written( $files_written, $skip_message, $success_message ); } /** * Checks that the `$target_dir` is a child directory of the WP themes or plugins directory, depending on `$type`. * * @param string $type "theme" or "plugin" * @param string $target_dir The theme/plugin directory to check. * * @return null|string Returns null on success, error message on error. */ private function check_target_directory( $type, $target_dir ) { $parent_dir = dirname( self::canonicalize_path( str_replace( '\\', '/', $target_dir ) ) ); if ( 'theme' === $type && str_replace( '\\', '/', WP_CONTENT_DIR . '/themes' ) !== $parent_dir ) { return sprintf( 'The target directory \'%1$s\' is not in \'%2$s\'.', $target_dir, WP_CONTENT_DIR . '/themes' ); } if ( 'plugin' === $type && str_replace( '\\', '/', WP_PLUGIN_DIR ) !== $parent_dir ) { return sprintf( 'The target directory \'%1$s\' is not in \'%2$s\'.', $target_dir, WP_PLUGIN_DIR ); } // Success. return null; } protected function create_files( $files_and_contents, $force ) { $wp_filesystem = $this->init_wp_filesystem(); $wrote_files = []; foreach ( $files_and_contents as $filename => $contents ) { $should_write_file = $this->prompt_if_files_will_be_overwritten( $filename, $force ); if ( ! $should_write_file ) { continue; } $wp_filesystem->mkdir( dirname( $filename ) ); if ( ! $wp_filesystem->put_contents( $filename, $contents ) ) { WP_CLI::error( "Error creating file: {$filename}" ); } elseif ( $should_write_file ) { $wrote_files[] = $filename; } } return $wrote_files; } protected function prompt_if_files_will_be_overwritten( $filename, $force ) { $should_write_file = true; if ( ! file_exists( $filename ) ) { return true; } WP_CLI::warning( 'File already exists.' ); WP_CLI::log( $filename ); if ( ! $force ) { do { $answer = cli\prompt( 'Skip this file, or replace it with scaffolding?', $default = false, $marker = '[s/r]: ' ); } while ( ! in_array( $answer, [ 's', 'r' ], true ) ); $should_write_file = 'r' === $answer; } $outcome = $should_write_file ? 'Replacing' : 'Skipping'; WP_CLI::log( $outcome . PHP_EOL ); return $should_write_file; } protected function log_whether_files_written( $files_written, $skip_message, $success_message ) { if ( empty( $files_written ) ) { WP_CLI::log( $skip_message ); } else { WP_CLI::success( $success_message ); } } /** * Extracts dashicon name when provided or return null otherwise. * * @param array $assoc_args * @return string|null */ private function extract_dashicon( $assoc_args ) { $dashicon = Utils\get_flag_value( $assoc_args, 'dashicon' ); if ( ! $dashicon ) { return null; } return preg_replace( '/dashicon(-|s-)/', '', $dashicon ); } /** * If you're writing your files to your theme directory your textdomain also needs to be the same as your theme. * Same goes for when plugin is being used. */ private function get_textdomain( $textdomain, $args ) { if ( strlen( $textdomain ) ) { return $textdomain; } if ( $args['theme'] ) { return $this->get_theme_name( $args['theme'] ); } if ( $args['plugin'] && true !== $args['plugin'] ) { return $args['plugin']; } return 'YOUR-TEXTDOMAIN'; } /** * Generates the machine name for function declarations. * * @param string $slug Slug name to convert. * @return string */ private function generate_machine_name( $slug ) { return str_replace( '-', '_', $slug ); } /** * Pluralizes a noun. * * @see Inflector::pluralize() * @param string $word Word to be pluralized. * @return string */ private function pluralize( $word ) { return Inflector::pluralize( $word ); } protected function extract_args( $assoc_args, $defaults ) { $out = []; foreach ( $defaults as $key => $value ) { $out[ $key ] = Utils\get_flag_value( $assoc_args, $key, $value ); } return $out; } protected function quote_comma_list_elements( $comma_list ) { return "'" . implode( "', '", explode( ',', $comma_list ) ) . "'"; } /** * Creates the themes directory if it doesn't already exist. */ protected function maybe_create_themes_dir() { $themes_dir = WP_CONTENT_DIR . '/themes'; if ( ! is_dir( $themes_dir ) ) { wp_mkdir_p( $themes_dir ); } } /** * Creates the plugins directory if it doesn't already exist. */ protected function maybe_create_plugins_dir() { if ( ! is_dir( WP_PLUGIN_DIR ) ) { wp_mkdir_p( WP_PLUGIN_DIR ); } } /** * Initializes WP_Filesystem. */ protected function init_wp_filesystem() { global $wp_filesystem; WP_Filesystem(); return $wp_filesystem; } /** * Localizes the template path. */ private static function mustache_render( $template, $data = [] ) { return Utils\mustache_render( dirname( dirname( __FILE__ ) ) . "/templates/{$template}", $data ); } /** * Gets the template path based on installation type. */ private static function get_template_path( $template ) { $command_root = Utils\phar_safe_path( dirname( __DIR__ ) ); $template_path = "{$command_root}/templates/{$template}"; if ( ! file_exists( $template_path ) ) { WP_CLI::error( "Couldn't find {$template}" ); } return $template_path; } /* * Returns the canonicalized path, with dot and double dot segments resolved. * * Copied from Symfony\Component\DomCrawler\AbstractUriElement::canonicalizePath(). * Implements RFC 3986, section 5.2.4. * * @param string $path The path to make canonical. * * @return string The canonicalized path. */ private static function canonicalize_path( $path ) { if ( '' === $path || '/' === $path ) { return $path; } if ( '.' === substr( $path, -1 ) ) { $path .= '/'; } $output = []; foreach ( explode( '/', $path ) as $segment ) { if ( '..' === $segment ) { array_pop( $output ); } elseif ( '.' !== $segment ) { $output[] = $segment; } } return implode( '/', $output ); } /** * Gets an active theme's name when true provided or the same name otherwise. * * @param string|bool $theme Theme name or true. * @return string */ private function get_theme_name( $theme ) { if ( true === $theme ) { $theme = wp_get_theme()->template; } return strtolower( $theme ); } } <?php /** * Functions to register client-side assets (scripts and stylesheets) for the * Gutenberg block. * * @package {{namespace}} */ /** * Registers all block assets so that they can be enqueued through Gutenberg in * the corresponding context. * * @see https://wordpress.org/gutenberg/handbook/designers-developers/developers/tutorials/block-tutorial/applying-styles-with-stylesheets/ */ function {{machine_name}}_block_init() { // Skip block registration if Gutenberg is not enabled/merged. if ( ! function_exists( 'register_block_type' ) ) { return; } {{#plugin}} $dir = dirname( __FILE__ ); {{/plugin}} {{#theme}} $dir = get_stylesheet_directory() . '/blocks'; {{/theme}} $index_js = '{{slug}}/index.js'; wp_register_script( '{{slug}}-block-editor', {{#plugin}} plugins_url( $index_js, __FILE__ ), {{/plugin}} {{#theme}} get_stylesheet_directory_uri() . "/blocks/{$index_js}", {{/theme}} [ 'wp-blocks', 'wp-i18n', 'wp-element', ], filemtime( "{$dir}/{$index_js}" ) ); $editor_css = '{{slug}}/editor.css'; wp_register_style( '{{slug}}-block-editor', {{#plugin}} plugins_url( $editor_css, __FILE__ ), {{/plugin}} {{#theme}} get_stylesheet_directory_uri() . "/blocks/{$editor_css}", {{/theme}} [], filemtime( "{$dir}/{$editor_css}" ) ); $style_css = '{{slug}}/style.css'; wp_register_style( '{{slug}}-block', {{#plugin}} plugins_url( $style_css, __FILE__ ), {{/plugin}} {{#theme}} get_stylesheet_directory_uri() . "/blocks/{$style_css}", {{/theme}} [], filemtime( "{$dir}/{$style_css}" ) ); register_block_type( '{{namespace}}/{{slug}}', [ 'editor_script' => '{{slug}}-block-editor', 'editor_style' => '{{slug}}-block-editor', 'style' => '{{slug}}-block', ] ); } add_action( 'init', '{{machine_name}}_block_init' ); <?php if ( ! class_exists( 'WP_CLI' ) ) { return; } $wpcli_super_admin_autoloader = dirname( __FILE__ ) . '/vendor/autoload.php'; if ( file_exists( $wpcli_super_admin_autoloader ) ) { require_once $wpcli_super_admin_autoloader; } WP_CLI::add_command( 'super-admin', 'Super_Admin_Command', array( 'before_invoke' => function () { if ( ! is_multisite() ) { WP_CLI::error( 'This is not a multisite installation.' ); } }, ) ); <?php use WP_CLI\Fetchers\User as UserFetcher; /** * Lists, adds, or removes super admin users on a multisite installation. * * ## EXAMPLES * * # List user with super-admin capabilities * $ wp super-admin list * supervisor * administrator * * # Grant super-admin privileges to the user. * $ wp super-admin add superadmin2 * Success: Granted super-admin capabilities. * * # Revoke super-admin privileges to the user. * $ wp super-admin remove superadmin2 * Success: Revoked super-admin capabilities. * * @package wp-cli */ class Super_Admin_Command extends WP_CLI_Command { private $fields = [ 'user_login', ]; public function __construct() { $this->fetcher = new UserFetcher(); } /** * Lists users with super admin capabilities. * * ## OPTIONS * * [--format=<format>] * : Render output in a particular format. * --- * default: list * options: * - list * - table * - csv * - json * - count * - yaml * --- * * ## EXAMPLES * * # List user with super-admin capabilities * $ wp super-admin list * supervisor * administrator * * @subcommand list */ public function list_subcommand( $_, $assoc_args ) { $super_admins = self::get_admins(); if ( 'list' === $assoc_args['format'] ) { foreach ( $super_admins as $user_login ) { WP_CLI::line( $user_login ); } } else { $output_users = []; foreach ( $super_admins as $user_login ) { $output_user = new stdClass(); $output_user->user_login = $user_login; $output_users[] = $output_user; } $formatter = new \WP_CLI\Formatter( $assoc_args, $this->fields ); $formatter->display_items( $output_users ); } } /** * Grants super admin privileges to one or more users. * * ## OPTIONS * * <user>... * : One or more user IDs, user emails, or user logins. * * ## EXAMPLES * * $ wp super-admin add superadmin2 * Success: Granted super-admin capabilities. */ public function add( $args, $_ ) { $successes = 0; $errors = 0; $users = $this->fetcher->get_many( $args ); if ( count( $users ) !== count( $args ) ) { $errors = count( $args ) - count( $users ); } $super_admins = self::get_admins(); $num_super_admins = count( $super_admins ); $new_super_admins = []; foreach ( $users as $user ) { do_action( 'grant_super_admin', (int) $user->ID ); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound if ( in_array( $user->user_login, $super_admins, true ) ) { WP_CLI::warning( "User '{$user->user_login}' already has super-admin capabilities." ); continue; } $new_super_admins[] = $user->ID; $super_admins[] = $user->user_login; $successes++; } if ( count( $super_admins ) === $num_super_admins ) { if ( $errors ) { $user_count = count( $args ); WP_CLI::error( "Couldn't grant super-admin capabilities to {$errors} of {$user_count} users." ); } else { WP_CLI::success( 'Super admins remain unchanged.' ); } } else { if ( update_site_option( 'site_admins', $super_admins ) ) { if ( $errors ) { $user_count = count( $args ); WP_CLI::error( "Only granted super-admin capabilities to {$successes} of {$user_count} users." ); } else { $message = $successes > 1 ? 'users' : 'user'; WP_CLI::success( "Granted super-admin capabilities to {$successes} {$message}." ); } } else { WP_CLI::error( 'Site options update failed.' ); } } foreach ( $new_super_admins as $user_id ) { do_action( 'granted_super_admin', (int) $user_id ); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound } } /** * Removes super admin privileges from one or more users. * * ## OPTIONS * * <user>... * : One or more user IDs, user emails, or user logins. * * ## EXAMPLES * * $ wp super-admin remove superadmin2 * Success: Revoked super-admin capabilities. */ public function remove( $args, $_ ) { $users = $this->fetcher->get_many( $args ); $user_logins = $users ? array_values( array_unique( wp_list_pluck( $users, 'user_login' ) ) ) : []; $user_logins_count = count( $user_logins ); $user_ids = []; foreach ( $users as $user ) { $user_ids[ $user->user_login ] = $user->ID; do_action( 'revoke_super_admin', (int) $user->ID ); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound } $super_admins = self::get_admins(); if ( ! $super_admins ) { WP_CLI::error( 'No super admins to revoke super-admin privileges from.' ); } if ( $user_logins_count < count( $args ) ) { $flipped_user_logins = array_flip( $user_logins ); // Fetcher has already warned so don't bother here, but continue with any args that are possible login names to cater for invalid users in the site options meta. $user_logins = array_merge( $user_logins, array_unique( array_filter( $args, static function ( $v ) use ( $flipped_user_logins ) { // Exclude numeric and email-like logins (login names can be email-like but ignore this given the circumstances). return ! isset( $flipped_user_logins[ $v ] ) && ! is_numeric( $v ) && ! is_email( $v ); } ) ) ); $user_logins_count = count( $user_logins ); } if ( ! $user_logins ) { WP_CLI::error( 'No valid user logins given to revoke super-admin privileges from.' ); } $update_super_admins = array_diff( $super_admins, $user_logins ); if ( $update_super_admins === $super_admins ) { WP_CLI::error( $user_logins_count > 1 ? 'None of the given users is a super admin.' : 'The given user is not a super admin.' ); } update_site_option( 'site_admins', $update_super_admins ); $successes = count( $super_admins ) - count( $update_super_admins ); if ( $successes === $user_logins_count ) { $message = $user_logins_count > 1 ? 'users' : 'user'; $msg = "Revoked super-admin capabilities from {$user_logins_count} {$message}."; } else { $msg = "Revoked super-admin capabilities from {$successes} of {$user_logins_count} users."; } if ( ! $update_super_admins ) { $msg .= ' There are no remaining super admins.'; } WP_CLI::success( $msg ); $removed_logins = array_intersect( $user_logins, $super_admins ); foreach ( $removed_logins as $user_login ) { $user_id = null; if ( array_key_exists( $user_login, $user_ids ) ) { $user_id = $user_ids[ $user_login ]; } else { $user = get_user_by( 'login', $user_login ); if ( $user instanceof WP_User ) { $user_id = $user->ID; } } if ( null !== $user_id ) { do_action( 'revoked_super_admin', (int) $user_ids[ $user_login ] ); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound } } } private static function get_admins() { // We don't use get_super_admins() because we don't want to mess with the global return (array) get_site_option( 'site_admins', [ 'admin' ] ); } } <?php namespace WP_CLI; use WP_CLI; trait ParsePluginNameInput { /** * If have optional args ([<plugin>...]) and an all option, then check have something to do. * * @param array $args Passed-in arguments. * @param bool $all All flag. * @param string $verb Optional. Verb to use. Defaults to 'install'. * @return array Same as $args if not all, otherwise all slugs. * @throws ExitException If neither plugin name nor --all were provided. */ protected function check_optional_args_and_all( $args, $all, $verb = 'install' ) { if ( $all ) { $args = array_map( '\WP_CLI\Utils\get_plugin_name', array_keys( $this->get_all_plugins() ) ); } if ( empty( $args ) ) { if ( ! $all ) { WP_CLI::error( 'Please specify one or more plugins, or use --all.' ); } $past_tense_verb = Utils\past_tense_verb( $verb ); WP_CLI::success( "No plugins {$past_tense_verb}." ); // Don't error if --all given for BC. } return $args; } /** * Gets all available plugins. * * Uses the same filter core uses in plugins.php to determine which plugins * should be available to manage through the WP_Plugins_List_Table class. * * @return array */ private function get_all_plugins() { // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- Calling native WordPress hook. return apply_filters( 'all_plugins', get_plugins() ); } } <?php namespace WP_CLI; /** * A plugin upgrader class that clears the destination directory. */ class DestructivePluginUpgrader extends \Plugin_Upgrader { public function install_package( $args = array() ) { parent::upgrade_strings(); // Needed for the 'remove_old' string. $args['clear_destination'] = true; $args['abort_if_destination_exists'] = false; return parent::install_package( $args ); } } <?php namespace WP_CLI; /** * A theme upgrader class that clears the destination directory. */ class DestructiveThemeUpgrader extends \Theme_Upgrader { public function install_package( $args = array() ) { parent::upgrade_strings(); // Needed for the 'remove_old' string. $args['clear_destination'] = true; $args['abort_if_destination_exists'] = false; return parent::install_package( $args ); } } <?php namespace WP_CLI\Fetchers; /** * Fetch a WordPress plugin based on one of its attributes. */ class Plugin extends Base { /** * @var string $msg Error message to use when invalid data is provided */ protected $msg = "The '%s' plugin could not be found."; /** * Get a plugin object by name * * @param string $name * @return object|false */ public function get( $name ) { // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- Calling native WordPress hook. foreach ( apply_filters( 'all_plugins', get_plugins() ) as $file => $_ ) { if ( "$name.php" === $file || ( $name && $file === $name ) || ( dirname( $file ) === $name && '.' !== $name ) ) { return (object) compact( 'name', 'file' ); } } return false; } } <?php namespace WP_CLI\Fetchers; use WP_CLI\Utils; /** * Fetch a WordPress theme based on one of its attributes. */ class Theme extends Base { /** * @var string $msg Error message to use when invalid data is provided */ protected $msg = "The '%s' theme could not be found."; /** * Get a theme object by name * * @param string $name * @return object|false */ public function get( $name ) { // Workaround to equalize folder naming conventions across Win/Mac/Linux. // Returns false if theme stylesheet doesn't exactly match existing themes. $existing_themes = wp_get_themes( array( 'errors' => null ) ); $existing_stylesheets = array_keys( $existing_themes ); if ( ! in_array( $name, $existing_stylesheets, true ) ) { $inexact_match = $this->find_inexact_match( $name, $existing_themes ); if ( false !== $inexact_match ) { $this->msg .= sprintf( " Did you mean '%s'?", $inexact_match ); } return false; } $theme = $existing_themes[ $name ]; return $theme; } /** * Find and return the key in $existing_themes that matches $name with * a case insensitive string comparison. * * @param string $name Name of theme received by command. * @param array $existing_themes Key/value pair of existing themes, key is * a case sensitive name. * @return string|boolean Case sensitive name if match found, otherwise false. */ private function find_inexact_match( $name, $existing_themes ) { $target = strtolower( $name ); $themes = array_map( 'strtolower', array_keys( $existing_themes ) ); if ( in_array( $target, $themes, true ) ) { return $target; } $suggestion = Utils\get_suggestion( $target, $themes ); if ( '' !== $suggestion ) { return $suggestion; } return false; } } <?php namespace WP_CLI; use WP_CLI; trait ParseThemeNameInput { /** * If have optional args ([<theme>...]) and an all option, then check have something to do. * * @param array $args Passed-in arguments. * @param bool $all All flag. * @param string $verb Optional. Verb to use. Defaults to 'install'. * @return array Same as $args if not all, otherwise all slugs. * @throws ExitException If neither plugin name nor --all were provided. */ protected function check_optional_args_and_all( $args, $all, $verb = 'install' ) { if ( $all ) { $args = array_map( '\WP_CLI\Utils\get_theme_name', array_keys( $this->get_all_themes() ) ); } if ( empty( $args ) ) { if ( ! $all ) { WP_CLI::error( 'Please specify one or more themes, or use --all.' ); } $past_tense_verb = Utils\past_tense_verb( $verb ); WP_CLI::success( "No themes {$past_tense_verb}." ); // Don't error if --all given for BC. } return $args; } /** * Gets all available themes. * * Uses the same filter core uses in themes.php to determine which themes * should be available to manage through the WP_Themes_List_Table class. * * @return array */ private function get_all_themes() { $items = array(); $theme_version_info = array(); if ( is_multisite() ) { $site_enabled = get_option( 'allowedthemes' ); if ( empty( $site_enabled ) ) { $site_enabled = array(); } $network_enabled = get_site_option( 'allowedthemes' ); if ( empty( $network_enabled ) ) { $network_enabled = array(); } } $all_update_info = $this->get_update_info(); $checked_themes = isset( $all_update_info->checked ) ? $all_update_info->checked : array(); if ( ! empty( $checked_themes ) ) { foreach ( $checked_themes as $slug => $version ) { $theme_version_info[ $slug ] = $this->is_theme_version_valid( $slug, $version ); } } foreach ( wp_get_themes() as $key => $theme ) { $file = $theme->get_stylesheet_directory(); $update_info = ( isset( $all_update_info->response[ $theme->get_stylesheet() ] ) && null !== $all_update_info->response[ $theme->get_stylesheet() ] ) ? (array) $all_update_info->response[ $theme->get_stylesheet() ] : null; $items[ $file ] = [ 'name' => $key, 'status' => $this->get_status( $theme ), 'update' => (bool) $update_info, 'update_version' => isset( $update_info['new_version'] ) ? $update_info['new_version'] : null, 'update_package' => isset( $update_info['package'] ) ? $update_info['package'] : null, 'version' => $theme->get( 'Version' ), 'update_id' => $theme->get_stylesheet(), 'title' => $theme->get( 'Name' ), 'description' => wordwrap( $theme->get( 'Description' ) ), 'author' => $theme->get( 'Author' ), ]; // Compare version and update information in theme list. if ( isset( $theme_version_info[ $key ] ) && false === $theme_version_info[ $key ] ) { $items[ $file ]['update'] = 'version higher than expected'; } if ( is_multisite() ) { if ( ! empty( $site_enabled[ $key ] ) && ! empty( $network_enabled[ $key ] ) ) { $items[ $file ]['enabled'] = 'network,site'; } elseif ( ! empty( $network_enabled[ $key ] ) ) { $items[ $file ]['enabled'] = 'network'; } elseif ( ! empty( $site_enabled[ $key ] ) ) { $items[ $file ]['enabled'] = 'site'; } else { $items[ $file ]['enabled'] = 'no'; } } } return $items; } /** * Check if current version of the theme is higher than the one available at WP.org. * * @param string $slug Theme slug. * @param string $version Theme current version. * * @return bool|string */ protected function is_theme_version_valid( $slug, $version ) { // Get Theme Info. $theme_info = themes_api( 'theme_information', array( 'slug' => $slug ) ); // Return empty string for themes not on WP.org. if ( is_wp_error( $theme_info ) ) { return ''; } // Compare theme version info. return ! version_compare( $version, $theme_info->version, '>' ); } /** * Get the status for a given theme. * * @param string $theme Theme to get the status for. * * @return string Status of the theme. */ protected function get_status( $theme ) { if ( $this->is_active_theme( $theme ) ) { return 'active'; } if ( $theme->get_stylesheet_directory() === get_template_directory() ) { return 'parent'; } return 'inactive'; } /** * Check whether a given theme is the active theme. * * @param string $theme Theme to check. * * @return bool Whether the provided theme is the active theme. */ protected function is_active_theme( $theme ) { return $theme->get_stylesheet_directory() === get_stylesheet_directory(); } /** * Get the available update info. * * @return mixed Available update info. */ protected function get_update_info() { return get_site_transient( 'update_themes' ); } } <?php namespace WP_CLI; use Composer\Semver\Comparator; use Exception; use WP_CLI; use WP_CLI\Fetchers; use WP_CLI\Loggers; use WP_CLI\Utils; use WP_Error; abstract class CommandWithUpgrade extends \WP_CLI_Command { protected $item_type; protected $obj_fields; protected $upgrade_refresh; protected $upgrade_transient; protected $chained_command = false; // Invalid version message. const INVALID_VERSION_MESSAGE = 'version higher than expected'; public function __construct() { // Do not automatically check translations updates after updating plugins/themes. add_action( 'upgrader_process_complete', function() { remove_action( 'upgrader_process_complete', [ 'Language_Pack_Upgrader', 'async_upgrade' ], 20 ); }, 1 ); add_filter( 'http_request_timeout', function () { return 1 * MINUTE_IN_SECONDS; }, 999 ); $this->fetcher = new Fetchers\Plugin(); } abstract protected function get_upgrader_class( $force ); abstract protected function get_item_list(); /** * @param array List of update candidates * @param array List of item names * @return array List of update candidates */ abstract protected function filter_item_list( $items, $args ); abstract protected function get_all_items(); abstract protected function get_status( $file ); abstract protected function status_single( $args ); abstract protected function install_from_repo( $slug, $assoc_args ); public function status( $args ) { // Force WordPress to check for updates. call_user_func( $this->upgrade_refresh ); if ( empty( $args ) ) { $this->status_all(); } else { $this->status_single( $args ); } } private function status_all() { $items = $this->get_all_items(); $n = count( $items ); WP_CLI::log( sprintf( '%d installed %s:', $n, Utils\pluralize( $this->item_type, absint( $n ) ) ) ); $padding = $this->get_padding( $items ); foreach ( $items as $file => $details ) { if ( $details['update'] ) { $line = ' %yU%n'; } else { $line = ' '; } $line .= $this->format_status( $details['status'], 'short' ); $line .= ' ' . str_pad( $details['name'], $padding ) . '%n'; if ( ! empty( $details['version'] ) ) { $line .= ' ' . $details['version']; } WP_CLI::line( WP_CLI::colorize( $line ) ); } WP_CLI::line(); $this->show_legend( $items ); } private function get_padding( $items ) { $max_len = 0; foreach ( $items as $details ) { $len = strlen( $details['name'] ); if ( $len > $max_len ) { $max_len = $len; } } return $max_len; } private function show_legend( $items ) { $statuses = array_unique( wp_list_pluck( $items, 'status' ) ); $legend_line = array(); foreach ( $statuses as $status ) { $legend_line[] = sprintf( '%s%s = %s%%n', $this->get_color( $status ), $this->map['short'][ $status ], $this->map['long'][ $status ] ); } if ( in_array( true, wp_list_pluck( $items, 'update' ), true ) ) { $legend_line[] = '%yU = Update Available%n'; } WP_CLI::line( 'Legend: ' . WP_CLI::colorize( implode( ', ', $legend_line ) ) ); } public function install( $args, $assoc_args ) { $successes = 0; $errors = 0; foreach ( $args as $slug ) { if ( empty( $slug ) ) { WP_CLI::warning( 'Ignoring ambiguous empty slug value.' ); continue; } $result = false; $is_remote = false !== strpos( $slug, '://' ); // Check if a URL to a remote or local zip has been specified. if ( $is_remote || ( pathinfo( $slug, PATHINFO_EXTENSION ) === 'zip' && is_file( $slug ) ) ) { // Install from local or remote zip file. $file_upgrader = $this->get_upgrader( $assoc_args ); $filter = false; // If a GitHub URL, do some guessing as to the correct plugin/theme directory. if ( $is_remote && 'github.com' === $this->parse_url_host_component( $slug, PHP_URL_HOST ) // Don't attempt to rename ZIPs uploaded to the releases page or coming from a raw source. && ! preg_match( '#github\.com/[^/]+/[^/]+/(?:releases/download|raw)/#', $slug ) ) { $filter = function( $source, $remote_source, $upgrader ) use ( $slug ) { $slug_dir = Utils\basename( $this->parse_url_host_component( $slug, PHP_URL_PATH ), '.zip' ); // Don't use the zip name if archive attached to release, as name likely to contain version tag/branch. if ( preg_match( '#github\.com/[^/]+/([^/]+)/archive/#', $slug, $matches ) ) { // Note this will be wrong if the project name isn't the same as the plugin/theme slug name. $slug_dir = $matches[1]; } $source_dir = Utils\basename( $source ); // `$source` is trailing-slashed path to the unzipped archive directory, so basename returns the unslashed directory. if ( $source_dir === $slug_dir ) { return $source; } $new_path = substr_replace( $source, $slug_dir, strrpos( $source, $source_dir ), strlen( $source_dir ) ); if ( $GLOBALS['wp_filesystem']->move( $source, $new_path ) ) { WP_CLI::log( sprintf( "Renamed Github-based project from '%s' to '%s'.", $source_dir, $slug_dir ) ); return $new_path; } return new WP_Error( 'wpcli_install_github', "Couldn't move Github-based project to appropriate directory." ); }; add_filter( 'upgrader_source_selection', $filter, 10, 3 ); } if ( $file_upgrader->install( $slug ) ) { $slug = $file_upgrader->result['destination_name']; $result = true; if ( $filter ) { remove_filter( 'upgrader_source_selection', $filter, 10 ); } $successes++; } else { $errors++; } } else { // Assume a plugin/theme slug from the WordPress.org repository has been specified. $result = $this->install_from_repo( $slug, $assoc_args ); if ( is_null( $result ) ) { $errors++; } elseif ( is_wp_error( $result ) ) { $key = $result->get_error_code(); if ( in_array( $key, [ 'plugins_api_failed', 'themes_api_failed' ], true ) && ! empty( $result->error_data[ $key ] ) && in_array( $result->error_data[ $key ], [ 'N;', 'b:0;' ], true ) ) { WP_CLI::warning( "Couldn't find '$slug' in the WordPress.org {$this->item_type} directory." ); $errors++; } else { WP_CLI::warning( "$slug: " . $result->get_error_message() ); if ( 'already_installed' !== $key ) { $errors++; } } } else { $successes++; } } // Check extension is available or not. $extension = $this->fetcher->get_many( array( $slug ) ); // If installation goes well $result will be true. $allow_activation = $result; // Allow installation for installed extension. if ( is_wp_error( $result ) && 'already_installed' === $result->get_error_code() ) { $allow_activation = true; } if ( true === $allow_activation && count( $extension ) > 0 ) { $this->chained_command = true; if ( Utils\get_flag_value( $assoc_args, 'activate-network' ) ) { WP_CLI::log( "Network-activating '$slug'..." ); $this->activate( array( $slug ), array( 'network' => true ) ); } if ( Utils\get_flag_value( $assoc_args, 'activate' ) ) { WP_CLI::log( "Activating '$slug'..." ); $this->activate( array( $slug ) ); } $this->chained_command = false; } } Utils\report_batch_operation_results( $this->item_type, 'install', count( $args ), $successes, $errors ); } /** * Prepare an API response for downloading a particular version of an item. * * @param object $response wordpress.org API response * @param string $version The desired version of the package */ protected static function alter_api_response( $response, $version ) { if ( $response->version === $version ) { return; } // WordPress.org forces https, but still sometimes returns http // See https://twitter.com/nacin/status/512362694205140992 $response->download_link = str_replace( 'http://', 'https://', $response->download_link ); list( $link ) = explode( $response->slug, $response->download_link ); if ( false !== strpos( $response->download_link, '/theme/' ) ) { $download_type = 'theme'; } elseif ( false !== strpos( $response->download_link, '/plugin/' ) ) { $download_type = 'plugin'; } else { $download_type = 'plugin/theme'; } if ( 'dev' === $version ) { $response->download_link = $link . $response->slug . '.zip'; $response->version = 'Development Version'; } else { $response->download_link = $link . $response->slug . '.' . $version . '.zip'; $response->version = $version; // Check if the requested version exists. $response = wp_remote_head( $response->download_link ); $response_code = wp_remote_retrieve_response_code( $response ); if ( 200 !== (int) $response_code ) { if ( is_wp_error( $response ) ) { $error_msg = $response->get_error_message(); } else { $error_msg = sprintf( 'HTTP code %d', $response_code ); } WP_CLI::error( sprintf( "Can't find the requested %s's version %s in the WordPress.org %s repository (%s).", $download_type, $version, $download_type, $error_msg ) ); } } } protected function get_upgrader( $assoc_args ) { $force = (bool) Utils\get_flag_value( $assoc_args, 'force', false ); $insecure = (bool) Utils\get_flag_value( $assoc_args, 'insecure', false ); $upgrader_class = $this->get_upgrader_class( $force ); return Utils\get_upgrader( $upgrader_class, $insecure ); } protected function update_many( $args, $assoc_args ) { call_user_func( $this->upgrade_refresh ); if ( ! empty( $assoc_args['format'] ) && in_array( $assoc_args['format'], [ 'json', 'csv' ], true ) ) { $logger = new Loggers\Quiet( WP_CLI::get_runner()->in_color() ); WP_CLI::set_logger( $logger ); } if ( ! Utils\get_flag_value( $assoc_args, 'all' ) && empty( $args ) ) { WP_CLI::error( "Please specify one or more {$this->item_type}s, or use --all." ); } if ( Utils\get_flag_value( $assoc_args, 'minor' ) && Utils\get_flag_value( $assoc_args, 'patch' ) ) { WP_CLI::error( '--minor and --patch cannot be used together.' ); } $items = $this->get_item_list(); $errors = 0; if ( ! Utils\get_flag_value( $assoc_args, 'all' ) ) { $items = $this->filter_item_list( $items, $args ); $errors = count( $args ) - count( $items ); } $items_to_update = wp_list_filter( $items, [ 'update' => true ] ); $minor = (bool) Utils\get_flag_value( $assoc_args, 'minor', false ); $patch = (bool) Utils\get_flag_value( $assoc_args, 'patch', false ); if ( 'plugin' === $this->item_type && ( $minor || $patch ) ) { $type = $minor ? 'minor' : 'patch'; $insecure = (bool) Utils\get_flag_value( $assoc_args, 'insecure', false ); $items_to_update = self::get_minor_or_patch_updates( $items_to_update, $type, $insecure ); } $exclude = Utils\get_flag_value( $assoc_args, 'exclude' ); if ( isset( $exclude ) ) { $exclude_items = explode( ',', trim( $assoc_args['exclude'], ',' ) ); unset( $assoc_args['exclude'] ); foreach ( $exclude_items as $item ) { if ( 'plugin' === $this->item_type ) { $plugin = $this->fetcher->get( $item ); unset( $items_to_update[ $plugin->file ] ); } elseif ( 'theme' === $this->item_type ) { $theme_root = get_theme_root() . '/' . $item; unset( $items_to_update[ $theme_root ] ); } } } // Check for items to update and remove extensions that have version higher than expected. foreach ( $items_to_update as $item_key => $item_info ) { if ( static::INVALID_VERSION_MESSAGE === $item_info['update'] ) { WP_CLI::warning( "{$item_info['name']}: " . static::INVALID_VERSION_MESSAGE . '.' ); $errors++; unset( $items_to_update[ $item_key ] ); } } if ( Utils\get_flag_value( $assoc_args, 'dry-run' ) ) { if ( empty( $items_to_update ) ) { WP_CLI::log( "No {$this->item_type} updates available." ); if ( null !== $exclude ) { WP_CLI::log( "Skipped updates for: $exclude" ); } return; } if ( ! empty( $assoc_args['format'] ) && in_array( $assoc_args['format'], [ 'json', 'csv' ], true ) ) { Utils\format_items( $assoc_args['format'], $items_to_update, [ 'name', 'status', 'version', 'update_version' ] ); } elseif ( ! empty( $assoc_args['format'] ) && 'summary' === $assoc_args['format'] ) { WP_CLI::log( "Available {$this->item_type} updates:" ); foreach ( $items_to_update as $item_to_update => $info ) { WP_CLI::log( "{$info['title']} update from version {$info['version']} to version {$info['update_version']}" ); } } else { WP_CLI::log( "Available {$this->item_type} updates:" ); Utils\format_items( 'table', $items_to_update, [ 'name', 'status', 'version', 'update_version' ] ); } if ( null !== $exclude ) { WP_CLI::log( "Skipped updates for: $exclude" ); } return; } $result = array(); // Only attempt to update if there is something to update. if ( ! empty( $items_to_update ) ) { $cache_manager = WP_CLI::get_http_cache_manager(); foreach ( $items_to_update as $item ) { $cache_manager->whitelist_package( $item['update_package'], $this->item_type, $item['name'], $item['update_version'] ); } $upgrader = $this->get_upgrader( $assoc_args ); // Ensure the upgrader uses the download offer present in each item. $transient_filter = function( $transient ) use ( $items_to_update ) { foreach ( $items_to_update as $name => $item_data ) { if ( isset( $transient->response[ $name ] ) ) { $transient->response[ $name ]->new_version = $item_data['update_version']; $transient->response[ $name ]->package = $item_data['update_package']; } } return $transient; }; add_filter( 'site_transient_' . $this->upgrade_transient, $transient_filter, 999 ); $result = $upgrader->bulk_upgrade( wp_list_pluck( $items_to_update, 'update_id' ) ); remove_filter( 'site_transient_' . $this->upgrade_transient, $transient_filter, 999 ); } // Let the user know the results. $num_to_update = count( $items_to_update ); $num_updated = count( array_filter( $result, static function ( $result ) { return $result && ! is_wp_error( $result ); } ) ); if ( $num_to_update > 0 ) { if ( ! empty( $assoc_args['format'] ) && 'summary' === $assoc_args['format'] ) { foreach ( $items_to_update as $item_to_update => $info ) { $message = null !== $result[ $info['update_id'] ] ? 'updated successfully' : 'did not update'; WP_CLI::log( "{$info['title']} {$message} from version {$info['version']} to version {$info['update_version']}" ); } } else { $status = array(); foreach ( $items_to_update as $item_to_update => $info ) { $status[ $item_to_update ] = [ 'name' => $info['name'], 'old_version' => $info['version'], 'new_version' => $info['update_version'], 'status' => ( null !== $result[ $info['update_id'] ] && ! is_wp_error( $result[ $info['update_id'] ] ) ) ? 'Updated' : 'Error', ]; if ( null === $result[ $info['update_id'] ] || is_wp_error( $result[ $info['update_id'] ] ) ) { $errors++; } } $format = 'table'; if ( ! empty( $assoc_args['format'] ) && in_array( $assoc_args['format'], [ 'json', 'csv' ], true ) ) { $format = $assoc_args['format']; } Utils\format_items( $format, $status, [ 'name', 'old_version', 'new_version', 'status' ] ); } } $total_updated = Utils\get_flag_value( $assoc_args, 'all' ) ? $num_to_update : count( $args ); Utils\report_batch_operation_results( $this->item_type, 'update', $total_updated, $num_updated, $errors ); if ( null !== $exclude ) { WP_CLI::log( "Skipped updates for: $exclude" ); } } // phpcs:ignore PSR2.Methods.MethodDeclaration.Underscore -- Whitelisting to provide backward compatibility to classes possibly extending this class. protected function _list( $_, $assoc_args ) { // Force WordPress to check for updates if `--skip-update-check` is not passed. if ( false === (bool) Utils\get_flag_value( $assoc_args, 'skip-update-check', false ) ) { call_user_func( $this->upgrade_refresh ); } $all_items = $this->get_all_items(); if ( ! is_array( $all_items ) ) { WP_CLI::error( "No {$this->item_type}s found." ); } foreach ( $all_items as $key => &$item ) { if ( empty( $item['version'] ) ) { $item['version'] = ''; } foreach ( $item as $field => &$value ) { if ( true === $value ) { $value = 'available'; } elseif ( false === $value ) { $value = 'none'; } } foreach ( $this->obj_fields as $field ) { if ( ! array_key_exists( $field, $assoc_args ) ) { continue; } // This can be either a value to filter by or a comma-separated list of values. // Also, it is not forbidden for a value to contain a comma (in which case we can filter only by one). $field_filter = $assoc_args[ $field ]; if ( $item[ $field ] !== $field_filter && ! in_array( $item[ $field ], array_map( 'trim', explode( ',', $field_filter ) ), true ) ) { unset( $all_items[ $key ] ); } } } $formatter = $this->get_formatter( $assoc_args ); $formatter->display_items( $all_items ); } /** * Check whether an item has an update available or not. * * @param string $slug The plugin/theme slug * * @return bool */ protected function has_update( $slug ) { $update_list = get_site_transient( $this->upgrade_transient ); return isset( $update_list->response[ $slug ] ); } /** * Get the available update info * * @return mixed */ protected function get_update_info() { return get_site_transient( $this->upgrade_transient ); } private $map = [ 'short' => [ 'inactive' => 'I', 'active' => 'A', 'active-network' => 'N', 'must-use' => 'M', 'parent' => 'P', 'dropin' => 'D', ], 'long' => [ 'inactive' => 'Inactive', 'active' => 'Active', 'active-network' => 'Network Active', 'must-use' => 'Must Use', 'parent' => 'Parent', 'dropin' => 'Drop-In', ], ]; protected function format_status( $status, $format ) { return $this->get_color( $status ) . $this->map[ $format ][ $status ]; } private function get_color( $status ) { static $colors = [ 'inactive' => '', 'active' => '%g', 'active-network' => '%g', 'must-use' => '%c', 'parent' => '%p', 'dropin' => '%B', ]; return $colors[ $status ]; } /** * Get the minor or patch version for plugins with available updates * * @param array $items Plugins with updates. * @param string $type Either 'minor' or 'patch'. * @param bool $insecure Whether to retry without certificate validation on TLS handshake failure. * @return array */ private function get_minor_or_patch_updates( $items, $type, $insecure ) { $wp_org_api = new WpOrgApi( [ 'insecure' => $insecure ] ); foreach ( $items as $i => $item ) { try { $data = $wp_org_api->get_plugin_info( $item['name'] ); } catch ( Exception $exception ) { unset( $items[ $i ] ); continue; } // No minor or patch versions to access. if ( empty( $data['versions'] ) ) { unset( $items[ $i ] ); continue; } $update_version = false; $update_package = false; foreach ( $data['versions'] as $version => $download_link ) { $update_type = Utils\get_named_sem_ver( $version, $item['version'] ); // Compared version must be older. if ( ! $update_type ) { continue; } // Only permit 'patch' for 'patch'. if ( 'patch' === $type && 'patch' !== $update_type ) { continue; } // Permit 'minor' or 'patch' for 'minor' phpcs:ignore Squiz.PHP.CommentedOutCode.Found -- False positive. if ( 'minor' === $type && ! in_array( $update_type, array( 'minor', 'patch' ), true ) ) { continue; } if ( $update_version && ! Comparator::greaterThan( $version, $update_version ) ) { continue; } $update_version = $version; $update_package = $download_link; } // If there's not a matching version, bail on updates. if ( ! $update_version ) { unset( $items[ $i ] ); continue; } $items[ $i ]['update_version'] = $update_version; $items[ $i ]['update_package'] = $update_package; } return $items; } /** * Search wordpress.org repo. * * @param array $args A arguments array containing the search term in the first element. * @param array $assoc_args Data passed in from command. */ // phpcs:ignore PSR2.Methods.MethodDeclaration.Underscore -- Whitelisting to provide backward compatibility to classes possibly extending this class. protected function _search( $args, $assoc_args ) { $term = $args[0]; $defaults = [ 'per-page' => 10, 'page' => 1, 'fields' => implode( ',', [ 'name', 'slug', 'rating' ] ), ]; $assoc_args = array_merge( $defaults, $assoc_args ); $fields = array(); foreach ( explode( ',', $assoc_args['fields'] ) as $field ) { $fields[ $field ] = true; } $format = ! empty( $assoc_args['format'] ) ? $assoc_args['format'] : 'table'; $formatter = $this->get_formatter( $assoc_args ); $api_args = [ 'per_page' => (int) $assoc_args['per-page'], 'page' => (int) $assoc_args['page'], 'search' => $term, 'fields' => $fields, ]; if ( 'plugin' === $this->item_type ) { $api = plugins_api( 'query_plugins', $api_args ); } else { $api = themes_api( 'query_themes', $api_args ); } if ( is_wp_error( $api ) ) { WP_CLI::error( $api->get_error_message() . __( ' Try again' ) ); } $plural = $this->item_type . 's'; if ( ! isset( $api->$plural ) ) { WP_CLI::error( __( 'API error. Try Again.' ) ); } $items = $api->$plural; // Add `url` for plugin or theme on wordpress.org. foreach ( $items as $index => $item_object ) { if ( $item_object instanceof \stdClass ) { $item_object->url = "https://wordpress.org/{$plural}/{$item_object->slug}/"; } } if ( 'table' === $format ) { $count = Utils\get_flag_value( $api->info, 'results', 'unknown' ); WP_CLI::success( sprintf( 'Showing %s of %s %s.', count( $items ), $count, $plural ) ); } $formatter->display_items( $items ); } protected function get_formatter( &$assoc_args ) { return new \WP_CLI\Formatter( $assoc_args, $this->obj_fields, $this->item_type ); } /** * Error handler to ignore failures on accessing SSL "https://api.wordpress.org/themes/update-check/1.1/" in `wp_update_themes()` * and "https://api.wordpress.org/plugins/update-check/1.1/" in `wp_update_plugins()` which seem to occur intermittently. */ public static function error_handler( $errno, $errstr, $errfile, $errline, $errcontext = null ) { // If ignoring E_USER_WARNING | E_USER_NOTICE, default. if ( ! ( error_reporting() & $errno ) ) { return false; } // If not in "wp-includes/update.php", default. $update_php = 'wp-includes/update.php'; if ( 0 !== substr_compare( $errfile, $update_php, -strlen( $update_php ) ) ) { return false; } // Else assume it's in `wp_update_themes()` or `wp_update_plugins()` and just ignore it. return true; } /** * Retrieves PHP_URL_HOST component from URL. * * @param int $component The component to retrieve. * * @return string */ private function parse_url_host_component( $url, $component ) { // phpcs:ignore WordPress.WP.AlternativeFunctions.parse_url_parse_url -- parse_url will only be used in absence of wp_parse_url. return function_exists( 'wp_parse_url' ) ? wp_parse_url( $url, $component ) : parse_url( $url, $component ); } } <?php /** * Sets, gets, and removes theme mods. * * ## EXAMPLES * * # Set the 'background_color' theme mod to '000000'. * $ wp theme mod set background_color 000000 * Success: Theme mod background_color set to 000000 * * # Get single theme mod in JSON format. * $ wp theme mod get background_color --format=json * [{"key":"background_color","value":"dd3333"}] * * # Remove all theme mods. * $ wp theme mod remove --all * Success: Theme mods removed. */ class Theme_Mod_Command extends WP_CLI_Command { private $fields = [ 'key', 'value' ]; /** * Gets one or more theme mods. * * ## OPTIONS * * [<mod>...] * : One or more mods to get. * * [--field=<field>] * : Returns the value of a single field. * * [--all] * : List all theme mods * * [--format=<format>] * : Render output in a particular format. * --- * default: table * options: * - table * - json * - csv * - yaml * --- * * ## EXAMPLES * * # Get all theme mods. * $ wp theme mod get --all * +------------------+---------+ * | key | value | * +------------------+---------+ * | background_color | dd3333 | * | link_color | #dd9933 | * | main_text_color | #8224e3 | * +------------------+---------+ * * # Get single theme mod in JSON format. * $ wp theme mod get background_color --format=json * [{"key":"background_color","value":"dd3333"}] * * # Get value of a single theme mod. * $ wp theme mod get background_color --field=value * dd3333 * * # Get multiple theme mods. * $ wp theme mod get background_color header_textcolor * +------------------+--------+ * | key | value | * +------------------+--------+ * | background_color | dd3333 | * | header_textcolor | | * +------------------+--------+ */ public function get( $args = array(), $assoc_args = array() ) { if ( ! \WP_CLI\Utils\get_flag_value( $assoc_args, 'all' ) && empty( $args ) ) { WP_CLI::error( 'You must specify at least one mod or use --all.' ); } if ( \WP_CLI\Utils\get_flag_value( $assoc_args, 'all' ) ) { $args = array(); } $list = array(); $mods = get_theme_mods(); if ( ! is_array( $mods ) ) { // If no mods are set (perhaps new theme), make sure foreach still works. $mods = array(); } foreach ( $mods as $k => $v ) { // If mods were given, skip the others. if ( ! empty( $args ) && ! in_array( $k, $args, true ) ) { continue; } if ( is_array( $v ) ) { $list[] = [ 'key' => $k, 'value' => '=>', ]; foreach ( $v as $_k => $_v ) { $list[] = [ 'key' => " $_k", 'value' => $_v, ]; } } else { $list[] = [ 'key' => $k, 'value' => $v, ]; } } // For unset mods, show blank value. foreach ( $args as $mod ) { if ( ! isset( $mods[ $mod ] ) ) { $list[] = [ 'key' => $mod, 'value' => '', ]; } } $formatter = new \WP_CLI\Formatter( $assoc_args, $this->fields, 'thememods' ); $formatter->display_items( $list ); } /** * Gets a list of theme mods. * * ## OPTIONS * * [--field=<field>] * : Returns the value of a single field. * * [--format=<format>] * : Render output in a particular format. * --- * default: table * options: * - table * - json * - csv * - yaml * --- * * ## EXAMPLES * * # Gets a list of theme mods. * $ wp theme mod list * +------------------+---------+ * | key | value | * +------------------+---------+ * | background_color | dd3333 | * | link_color | #dd9933 | * | main_text_color | #8224e3 | * +------------------+---------+ * * @subcommand list */ public function list_( $args = array(), $assoc_args = array() ) { $assoc_args['all'] = 1; $this->get( $args, $assoc_args ); } /** * Removes one or more theme mods. * * ## OPTIONS * * [<mod>...] * : One or more mods to remove. * * [--all] * : Remove all theme mods. * * ## EXAMPLES * * # Remove all theme mods. * $ wp theme mod remove --all * Success: Theme mods removed. * * # Remove single theme mod. * $ wp theme mod remove background_color * Success: 1 mod removed. * * # Remove multiple theme mods. * $ wp theme mod remove background_color header_textcolor * Success: 2 mods removed. */ public function remove( $args = array(), $assoc_args = array() ) { if ( ! \WP_CLI\Utils\get_flag_value( $assoc_args, 'all' ) && empty( $args ) ) { WP_CLI::error( 'You must specify at least one mod or use --all.' ); } if ( \WP_CLI\Utils\get_flag_value( $assoc_args, 'all' ) ) { remove_theme_mods(); WP_CLI::success( 'Theme mods removed.' ); return; } foreach ( $args as $mod ) { remove_theme_mod( $mod ); } $count = count( $args ); $success_message = ( 1 === $count ) ? '%d mod removed.' : '%d mods removed.'; WP_CLI::success( sprintf( $success_message, $count ) ); } /** * Sets the value of a theme mod. * * ## OPTIONS * * <mod> * : The name of the theme mod to set or update. * * <value> * : The new value. * * ## EXAMPLES * * # Set theme mod * $ wp theme mod set background_color 000000 * Success: Theme mod background_color set to 000000 */ public function set( $args = array(), $assoc_args = array() ) { list( $mod, $value ) = $args; set_theme_mod( $mod, $value ); if ( get_theme_mod( $mod ) === $value ) { WP_CLI::success( "Theme mod {$mod} set to {$value}." ); } else { WP_CLI::success( "Could not update theme mod {$mod}." ); } } } <?php use WP_CLI\ParsePluginNameInput; use WP_CLI\Utils; /** * Manages plugins, including installs, activations, and updates. * * See the WordPress [Plugin Handbook](https://developer.wordpress.org/plugins/) developer resource for more information on plugins. * * ## EXAMPLES * * # Activate plugin * $ wp plugin activate hello * Plugin 'hello' activated. * Success: Activated 1 of 1 plugins. * * # Deactivate plugin * $ wp plugin deactivate hello * Plugin 'hello' deactivated. * Success: Deactivated 1 of 1 plugins. * * # Delete plugin * $ wp plugin delete hello * Deleted 'hello' plugin. * Success: Deleted 1 of 1 plugins. * * # Install the latest version from wordpress.org and activate * $ wp plugin install bbpress --activate * Installing bbPress (2.5.9) * Downloading install package from https://downloads.wordpress.org/plugin/bbpress.2.5.9.zip... * Using cached file '/home/vagrant/.wp-cli/cache/plugin/bbpress-2.5.9.zip'... * Unpacking the package... * Installing the plugin... * Plugin installed successfully. * Activating 'bbpress'... * Plugin 'bbpress' activated. * Success: Installed 1 of 1 plugins. * * @package wp-cli */ class Plugin_Command extends \WP_CLI\CommandWithUpgrade { use ParsePluginNameInput; protected $item_type = 'plugin'; protected $upgrade_refresh = 'wp_update_plugins'; protected $upgrade_transient = 'update_plugins'; protected $obj_fields = array( 'name', 'status', 'update', 'version', ); /** * Plugin fetcher instance. * * @var \WP_CLI\Fetchers\Plugin */ protected $fetcher; public function __construct() { require_once ABSPATH . 'wp-admin/includes/plugin.php'; require_once ABSPATH . 'wp-admin/includes/plugin-install.php'; parent::__construct(); $this->fetcher = new WP_CLI\Fetchers\Plugin(); } protected function get_upgrader_class( $force ) { return $force ? '\\WP_CLI\\DestructivePluginUpgrader' : 'Plugin_Upgrader'; } /** * Reveals the status of one or all plugins. * * ## OPTIONS * * [<plugin>] * : A particular plugin to show the status for. * * ## EXAMPLES * * # Displays status of all plugins * $ wp plugin status * 5 installed plugins: * I akismet 3.1.11 * I easy-digital-downloads 2.5.16 * A theme-check 20160523.1 * I wen-logo-slider 2.0.3 * M ns-pack 1.0.0 * Legend: I = Inactive, A = Active, M = Must Use * * # Displays status of a plugin * $ wp plugin status theme-check * Plugin theme-check details: * Name: Theme Check * Status: Active * Version: 20160523.1 * Author: Otto42, pross * Description: A simple and easy way to test your theme for all the latest WordPress standards and practices. A great theme development tool! */ public function status( $args ) { parent::status( $args ); } /** * Searches the WordPress.org plugin directory. * * Displays plugins in the WordPress.org plugin directory matching a given * search query. * * ## OPTIONS * * <search> * : The string to search for. * * [--page=<page>] * : Optional page to display. * --- * default: 1 * --- * * [--per-page=<per-page>] * : Optional number of results to display. * --- * default: 10 * --- * * [--field=<field>] * : Prints the value of a single field for each plugin. * * [--fields=<fields>] * : Ask for specific fields from the API. Defaults to name,slug,author_profile,rating. Acceptable values: * * **name**: Plugin Name * **slug**: Plugin Slug * **version**: Current Version Number * **author**: Plugin Author * **author_profile**: Plugin Author Profile * **contributors**: Plugin Contributors * **requires**: Plugin Minimum Requirements * **tested**: Plugin Tested Up To * **compatibility**: Plugin Compatible With * **rating**: Plugin Rating in Percent and Total Number * **ratings**: Plugin Ratings for each star (1-5) * **num_ratings**: Number of Plugin Ratings * **homepage**: Plugin Author's Homepage * **description**: Plugin's Description * **short_description**: Plugin's Short Description * **sections**: Plugin Readme Sections: description, installation, FAQ, screenshots, other notes, and changelog * **downloaded**: Plugin Download Count * **last_updated**: Plugin's Last Update * **added**: Plugin's Date Added to wordpress.org Repository * **tags**: Plugin's Tags * **versions**: Plugin's Available Versions with D/L Link * **donate_link**: Plugin's Donation Link * **banners**: Plugin's Banner Image Link * **icons**: Plugin's Icon Image Link * **active_installs**: Plugin's Number of Active Installs * **contributors**: Plugin's List of Contributors * **url**: Plugin's URL on wordpress.org * * [--format=<format>] * : Render output in a particular format. * --- * default: table * options: * - table * - csv * - count * - json * - yaml * --- * * ## EXAMPLES * * $ wp plugin search dsgnwrks --per-page=20 --format=json * Success: Showing 3 of 3 plugins. * [{"name":"DsgnWrks Instagram Importer Debug","slug":"dsgnwrks-instagram-importer-debug","rating":0},{"name":"DsgnWrks Instagram Importer","slug":"dsgnwrks-instagram-importer","rating":84},{"name":"DsgnWrks Twitter Importer","slug":"dsgnwrks-twitter-importer","rating":80}] * * $ wp plugin search dsgnwrks --fields=name,version,slug,rating,num_ratings * Success: Showing 3 of 3 plugins. * +-----------------------------------+---------+-----------------------------------+--------+-------------+ * | name | version | slug | rating | num_ratings | * +-----------------------------------+---------+-----------------------------------+--------+-------------+ * | DsgnWrks Instagram Importer Debug | 0.1.6 | dsgnwrks-instagram-importer-debug | 0 | 0 | * | DsgnWrks Instagram Importer | 1.3.7 | dsgnwrks-instagram-importer | 84 | 23 | * | DsgnWrks Twitter Importer | 1.1.1 | dsgnwrks-twitter-importer | 80 | 1 | * +-----------------------------------+---------+-----------------------------------+--------+-------------+ */ public function search( $args, $assoc_args ) { parent::_search( $args, $assoc_args ); } protected function status_single( $args ) { $plugin = $this->fetcher->get_check( $args[0] ); $file = $plugin->file; $details = $this->get_details( $file ); $status = $this->format_status( $this->get_status( $file ), 'long' ); $version = $details['Version']; if ( $this->has_update( $file ) ) { $version .= ' (%gUpdate available%n)'; } echo WP_CLI::colorize( Utils\mustache_render( self::get_template_path( 'plugin-status.mustache' ), [ 'slug' => Utils\get_plugin_name( $file ), 'status' => $status, 'version' => $version, 'name' => $details['Name'], 'author' => $details['Author'], 'description' => $details['Description'], ] ) ); } protected function get_all_items() { $items = $this->get_item_list(); foreach ( get_mu_plugins() as $file => $mu_plugin ) { $mu_version = ''; if ( ! empty( $mu_plugin['Version'] ) ) { $mu_version = $mu_plugin['Version']; } $items[ $file ] = array( 'name' => Utils\get_plugin_name( $file ), 'status' => 'must-use', 'update' => false, 'update_version' => null, 'update_package' => null, 'version' => $mu_version, 'update_id' => '', 'title' => '', 'description' => '', 'file' => $file, ); } $raw_items = get_dropins(); $raw_data = _get_dropins(); foreach ( $raw_items as $name => $item_data ) { $description = ! empty( $raw_data[ $name ][0] ) ? $raw_data[ $name ][0] : ''; $items[ $name ] = [ 'name' => $name, 'title' => $item_data['Title'], 'description' => $description, 'status' => 'dropin', 'update' => false, 'update_version' => null, 'update_package' => null, 'update_id' => '', 'file' => $name, ]; } return $items; } /** * Activates one or more plugins. * * ## OPTIONS * * [<plugin>...] * : One or more plugins to activate. * * [--all] * : If set, all plugins will be activated. * * [--network] * : If set, the plugin will be activated for the entire multisite network. * * ## EXAMPLES * * # Activate plugin * $ wp plugin activate hello * Plugin 'hello' activated. * Success: Activated 1 of 1 plugins. * * # Activate plugin in entire multisite network * $ wp plugin activate hello --network * Plugin 'hello' network activated. * Success: Network activated 1 of 1 plugins. */ public function activate( $args, $assoc_args = array() ) { $network_wide = Utils\get_flag_value( $assoc_args, 'network' ); $all = Utils\get_flag_value( $assoc_args, 'all', false ); $args = $this->check_optional_args_and_all( $args, $all ); if ( ! $args ) { return; } $successes = 0; $errors = 0; $plugins = $this->fetcher->get_many( $args ); if ( count( $plugins ) < count( $args ) ) { $errors = count( $args ) - count( $plugins ); } foreach ( $plugins as $plugin ) { $status = $this->get_status( $plugin->file ); if ( $all && in_array( $status, [ 'active', 'active-network' ], true ) ) { continue; } // Network-active is the highest level of activation status. if ( 'active-network' === $status ) { WP_CLI::warning( "Plugin '{$plugin->name}' is already network active." ); continue; } // Don't reactivate active plugins, but do let them become network-active. if ( ! $network_wide && 'active' === $status ) { WP_CLI::warning( "Plugin '{$plugin->name}' is already active." ); continue; } // Plugins need to be deactivated before being network activated. if ( $network_wide && 'active' === $status ) { deactivate_plugins( $plugin->file, false, false ); } $result = activate_plugin( $plugin->file, '', $network_wide ); if ( is_wp_error( $result ) ) { $message = $result->get_error_message(); $message = preg_replace( '/<a\s[^>]+>.*<\/a>/im', '', $message ); $message = wp_strip_all_tags( $message ); $message = str_replace( 'Error: ', '', $message ); WP_CLI::warning( "Failed to activate plugin. {$message}" ); } else { $this->active_output( $plugin->name, $plugin->file, $network_wide, 'activate' ); $successes++; } } if ( ! $this->chained_command ) { $verb = $network_wide ? 'network activate' : 'activate'; Utils\report_batch_operation_results( 'plugin', $verb, count( $args ), $successes, $errors ); } } /** * Deactivates one or more plugins. * * ## OPTIONS * * [<plugin>...] * : One or more plugins to deactivate. * * [--uninstall] * : Uninstall the plugin after deactivation. * * [--all] * : If set, all plugins will be deactivated. * * [--network] * : If set, the plugin will be deactivated for the entire multisite network. * * ## EXAMPLES * * # Deactivate plugin * $ wp plugin deactivate hello * Plugin 'hello' deactivated. * Success: Deactivated 1 of 1 plugins. */ public function deactivate( $args, $assoc_args = array() ) { $network_wide = Utils\get_flag_value( $assoc_args, 'network' ); $disable_all = Utils\get_flag_value( $assoc_args, 'all' ); $args = $this->check_optional_args_and_all( $args, $disable_all ); if ( ! $args ) { return; } $successes = 0; $errors = 0; $plugins = $this->fetcher->get_many( $args ); if ( count( $plugins ) < count( $args ) ) { $errors = count( $args ) - count( $plugins ); } foreach ( $plugins as $plugin ) { $status = $this->get_status( $plugin->file ); if ( $disable_all && ! in_array( $status, [ 'active', 'active-network' ], true ) ) { continue; } // Network active plugins must be explicitly deactivated. if ( ! $network_wide && 'active-network' === $status ) { WP_CLI::warning( "Plugin '{$plugin->name}' is network active and must be deactivated with --network flag." ); $errors++; continue; } if ( ! in_array( $status, [ 'active', 'active-network' ], true ) ) { WP_CLI::warning( "Plugin '{$plugin->name}' isn't active." ); continue; } deactivate_plugins( $plugin->file, false, $network_wide ); if ( ! is_network_admin() ) { update_option( 'recently_activated', array( $plugin->file => time() ) + (array) get_option( 'recently_activated' ) ); } else { update_site_option( 'recently_activated', array( $plugin->file => time() ) + (array) get_site_option( 'recently_activated' ) ); } $this->active_output( $plugin->name, $plugin->file, $network_wide, 'deactivate' ); $successes++; if ( Utils\get_flag_value( $assoc_args, 'uninstall' ) ) { WP_CLI::log( "Uninstalling '{$plugin->name}'..." ); $this->chained_command = true; $this->uninstall( array( $plugin->name ) ); $this->chained_command = false; } } if ( ! $this->chained_command ) { $verb = $network_wide ? 'network deactivate' : 'deactivate'; Utils\report_batch_operation_results( 'plugin', $verb, count( $args ), $successes, $errors ); } } /** * Toggles a plugin's activation state. * * If the plugin is active, then it will be deactivated. If the plugin is * inactive, then it will be activated. * * ## OPTIONS * * <plugin>... * : One or more plugins to toggle. * * [--network] * : If set, the plugin will be toggled for the entire multisite network. * * ## EXAMPLES * * # Akismet is currently activated * $ wp plugin toggle akismet * Plugin 'akismet' deactivated. * Success: Toggled 1 of 1 plugins. * * # Akismet is currently deactivated * $ wp plugin toggle akismet * Plugin 'akismet' activated. * Success: Toggled 1 of 1 plugins. */ public function toggle( $args, $assoc_args = array() ) { $network_wide = Utils\get_flag_value( $assoc_args, 'network' ); $successes = 0; $errors = 0; $plugins = $this->fetcher->get_many( $args ); if ( count( $plugins ) < count( $args ) ) { $errors = count( $args ) - count( $plugins ); } $this->chained_command = true; foreach ( $plugins as $plugin ) { if ( $this->check_active( $plugin->file, $network_wide ) ) { $this->deactivate( array( $plugin->name ), $assoc_args ); } else { $this->activate( array( $plugin->name ), $assoc_args ); } $successes++; } $this->chained_command = false; Utils\report_batch_operation_results( 'plugin', 'toggle', count( $args ), $successes, $errors ); } /** * Gets the path to a plugin or to the plugin directory. * * ## OPTIONS * * [<plugin>] * : The plugin to get the path to. If not set, will return the path to the * plugins directory. * * [--dir] * : If set, get the path to the closest parent directory, instead of the * plugin file. * * ## EXAMPLES * * $ cd $(wp plugin path) && pwd * /var/www/wordpress/wp-content/plugins */ public function path( $args, $assoc_args ) { $path = untrailingslashit( WP_PLUGIN_DIR ); if ( ! empty( $args ) ) { $plugin = $this->fetcher->get_check( $args[0] ); $path .= '/' . $plugin->file; if ( Utils\get_flag_value( $assoc_args, 'dir' ) ) { $path = dirname( $path ); } } WP_CLI::line( $path ); } protected function install_from_repo( $slug, $assoc_args ) { $api = plugins_api( 'plugin_information', array( 'slug' => $slug ) ); if ( is_wp_error( $api ) ) { return $api; } if ( isset( $assoc_args['version'] ) ) { self::alter_api_response( $api, $assoc_args['version'] ); } $status = install_plugin_install_status( $api ); if ( ! Utils\get_flag_value( $assoc_args, 'force' ) && 'install' !== $status['status'] ) { // We know this will fail, so avoid a needless download of the package. return new WP_Error( 'already_installed', 'Plugin already installed.' ); } WP_CLI::log( sprintf( 'Installing %s (%s)', html_entity_decode( $api->name, ENT_QUOTES ), $api->version ) ); if ( Utils\get_flag_value( $assoc_args, 'version' ) !== 'dev' ) { WP_CLI::get_http_cache_manager()->whitelist_package( $api->download_link, $this->item_type, $api->slug, $api->version ); } // Ignore failures on accessing SSL "https://api.wordpress.org/plugins/update-check/1.1/" in `wp_update_plugins()` which seem to occur intermittently. set_error_handler( array( __CLASS__, 'error_handler' ), E_USER_WARNING | E_USER_NOTICE ); $result = $this->get_upgrader( $assoc_args )->install( $api->download_link ); restore_error_handler(); return $result; } /** * Updates one or more plugins. * * ## OPTIONS * * [<plugin>...] * : One or more plugins to update. * * [--all] * : If set, all plugins that have updates will be updated. * * [--exclude=<name>] * : Comma separated list of plugin names that should be excluded from updating. * * [--minor] * : Only perform updates for minor releases (e.g. from 1.3 to 1.4 instead of 2.0) * * [--patch] * : Only perform updates for patch releases (e.g. from 1.3 to 1.3.3 instead of 1.4) * * [--format=<format>] * : Render output in a particular format. * --- * default: table * options: * - table * - csv * - json * - summary * --- * * [--version=<version>] * : If set, the plugin will be updated to the specified version. * * [--dry-run] * : Preview which plugins would be updated. * * [--insecure] * : Retry downloads without certificate validation if TLS handshake fails. Note: This makes the request vulnerable to a MITM attack. * * ## EXAMPLES * * $ wp plugin update bbpress --version=dev * Installing bbPress (Development Version) * Downloading install package from https://downloads.wordpress.org/plugin/bbpress.zip... * Unpacking the package... * Installing the plugin... * Removing the old version of the plugin... * Plugin updated successfully. * Success: Updated 1 of 2 plugins. * * $ wp plugin update --all * Enabling Maintenance mode... * Downloading update from https://downloads.wordpress.org/plugin/akismet.3.1.11.zip... * Unpacking the update... * Installing the latest version... * Removing the old version of the plugin... * Plugin updated successfully. * Downloading update from https://downloads.wordpress.org/plugin/nginx-champuru.3.2.0.zip... * Unpacking the update... * Installing the latest version... * Removing the old version of the plugin... * Plugin updated successfully. * Disabling Maintenance mode... * +------------------------+-------------+-------------+---------+ * | name | old_version | new_version | status | * +------------------------+-------------+-------------+---------+ * | akismet | 3.1.3 | 3.1.11 | Updated | * | nginx-cache-controller | 3.1.1 | 3.2.0 | Updated | * +------------------------+-------------+-------------+---------+ * Success: Updated 2 of 2 plugins. * * $ wp plugin update --all --exclude=akismet * Enabling Maintenance mode... * Downloading update from https://downloads.wordpress.org/plugin/nginx-champuru.3.2.0.zip... * Unpacking the update... * Installing the latest version... * Removing the old version of the plugin... * Plugin updated successfully. * Disabling Maintenance mode... * +------------------------+-------------+-------------+---------+ * | name | old_version | new_version | status | * +------------------------+-------------+-------------+---------+ * | nginx-cache-controller | 3.1.1 | 3.2.0 | Updated | * +------------------------+-------------+-------------+---------+ * * @alias upgrade */ public function update( $args, $assoc_args ) { $all = Utils\get_flag_value( $assoc_args, 'all', false ); $args = $this->check_optional_args_and_all( $args, $all ); if ( ! $args ) { return; } if ( isset( $assoc_args['version'] ) ) { foreach ( $this->fetcher->get_many( $args ) as $plugin ) { $assoc_args['force'] = 1; $this->install( array( $plugin->name ), $assoc_args ); } } else { parent::update_many( $args, $assoc_args ); } } protected function get_item_list() { $items = []; $duplicate_names = []; foreach ( $this->get_all_plugins() as $file => $details ) { $all_update_info = $this->get_update_info(); $update_info = ( isset( $all_update_info->response[ $file ] ) && null !== $all_update_info->response[ $file ] ) ? (array) $all_update_info->response[ $file ] : null; $name = Utils\get_plugin_name( $file ); if ( ! isset( $duplicate_names[ $name ] ) ) { $duplicate_names[ $name ] = array(); } $duplicate_names[ $name ][] = $file; $items[ $file ] = [ 'name' => $name, 'status' => $this->get_status( $file ), 'update' => (bool) $update_info, 'update_version' => isset( $update_info ) && isset( $update_info['new_version'] ) ? $update_info['new_version'] : null, 'update_package' => isset( $update_info ) && isset( $update_info['package'] ) ? $update_info['package'] : null, 'version' => $details['Version'], 'update_id' => $file, 'title' => $details['Name'], 'description' => wordwrap( $details['Description'] ), 'file' => $file, ]; if ( null === $update_info ) { // Get info for all plugins that don't have an update. $plugin_update_info = isset( $all_update_info->no_update[ $file ] ) ? $all_update_info->no_update[ $file ] : null; // Compare version and update information in plugin list. if ( null !== $plugin_update_info && version_compare( $details['Version'], $plugin_update_info->new_version, '>' ) ) { $items[ $file ]['update'] = static::INVALID_VERSION_MESSAGE; } } } foreach ( $duplicate_names as $name => $files ) { if ( count( $files ) <= 1 ) { continue; } foreach ( $files as $file ) { $items[ $file ]['name'] = str_replace( '.' . pathinfo( $file, PATHINFO_EXTENSION ), '', $file ); } } return $items; } protected function filter_item_list( $items, $args ) { $basenames = wp_list_pluck( $this->fetcher->get_many( $args ), 'file' ); return Utils\pick_fields( $items, $basenames ); } /** * Installs one or more plugins. * * ## OPTIONS * * <plugin|zip|url>... * : One or more plugins to install. Accepts a plugin slug, the path to a local zip file, or a URL to a remote zip file. * * [--version=<version>] * : If set, get that particular version from wordpress.org, instead of the * stable version. * * [--force] * : If set, the command will overwrite any installed version of the plugin, without prompting * for confirmation. * * [--activate] * : If set, the plugin will be activated immediately after install. * * [--activate-network] * : If set, the plugin will be network activated immediately after install * * [--insecure] * : Retry downloads without certificate validation if TLS handshake fails. Note: This makes the request vulnerable to a MITM attack. * * ## EXAMPLES * * # Install the latest version from wordpress.org and activate * $ wp plugin install bbpress --activate * Installing bbPress (2.5.9) * Downloading install package from https://downloads.wordpress.org/plugin/bbpress.2.5.9.zip... * Using cached file '/home/vagrant/.wp-cli/cache/plugin/bbpress-2.5.9.zip'... * Unpacking the package... * Installing the plugin... * Plugin installed successfully. * Activating 'bbpress'... * Plugin 'bbpress' activated. * Success: Installed 1 of 1 plugins. * * # Install the development version from wordpress.org * $ wp plugin install bbpress --version=dev * Installing bbPress (Development Version) * Downloading install package from https://downloads.wordpress.org/plugin/bbpress.zip... * Unpacking the package... * Installing the plugin... * Plugin installed successfully. * Success: Installed 1 of 1 plugins. * * # Install from a local zip file * $ wp plugin install ../my-plugin.zip * Unpacking the package... * Installing the plugin... * Plugin installed successfully. * Success: Installed 1 of 1 plugins. * * # Install from a remote zip file * $ wp plugin install http://s3.amazonaws.com/bucketname/my-plugin.zip?AWSAccessKeyId=123&Expires=456&Signature=abcdef * Downloading install package from http://s3.amazonaws.com/bucketname/my-plugin.zip?AWSAccessKeyId=123&Expires=456&Signature=abcdef * Unpacking the package... * Installing the plugin... * Plugin installed successfully. * Success: Installed 1 of 1 plugins. * * # Update from a remote zip file * $ wp plugin install https://github.com/envato/wp-envato-market/archive/master.zip --force * Downloading install package from https://github.com/envato/wp-envato-market/archive/master.zip * Unpacking the package... * Installing the plugin... * Renamed Github-based project from 'wp-envato-market-master' to 'wp-envato-market'. * Plugin updated successfully * Success: Installed 1 of 1 plugins. * * # Forcefully re-install all installed plugins * $ wp plugin install $(wp plugin list --field=name) --force * Installing Akismet (3.1.11) * Downloading install package from https://downloads.wordpress.org/plugin/akismet.3.1.11.zip... * Unpacking the package... * Installing the plugin... * Removing the old version of the plugin... * Plugin updated successfully * Success: Installed 1 of 1 plugins. */ public function install( $args, $assoc_args ) { if ( ! is_dir( WP_PLUGIN_DIR ) ) { wp_mkdir_p( WP_PLUGIN_DIR ); } parent::install( $args, $assoc_args ); } /** * Gets details about an installed plugin. * * ## OPTIONS * * <plugin> * : The plugin to get. * * [--field=<field>] * : Instead of returning the whole plugin, returns the value of a single field. * * [--fields=<fields>] * : Limit the output to specific fields. Defaults to all fields. * * [--format=<format>] * : Render output in a particular format. * --- * default: table * options: * - table * - csv * - json * - yaml * --- * * ## EXAMPLES * * $ wp plugin get bbpress --format=json * {"name":"bbpress","title":"bbPress","author":"The bbPress Contributors","version":"2.6-alpha","description":"bbPress is forum software with a twist from the creators of WordPress.","status":"active"} */ public function get( $args, $assoc_args ) { $plugin = $this->fetcher->get_check( $args[0] ); $file = $plugin->file; $plugin_data = get_plugin_data( WP_PLUGIN_DIR . '/' . $file, false, false ); $plugin_obj = (object) [ 'name' => Utils\get_plugin_name( $file ), 'title' => $plugin_data['Name'], 'author' => $plugin_data['Author'], 'version' => $plugin_data['Version'], 'description' => wordwrap( $plugin_data['Description'] ), 'status' => $this->get_status( $file ), ]; if ( empty( $assoc_args['fields'] ) ) { $plugin_array = get_object_vars( $plugin_obj ); $assoc_args['fields'] = array_keys( $plugin_array ); } $formatter = $this->get_formatter( $assoc_args ); $formatter->display_item( $plugin_obj ); } /** * Uninstalls one or more plugins. * * ## OPTIONS * * [<plugin>...] * : One or more plugins to uninstall. * * [--deactivate] * : Deactivate the plugin before uninstalling. Default behavior is to warn and skip if the plugin is active. * * [--skip-delete] * : If set, the plugin files will not be deleted. Only the uninstall procedure * will be run. * * [--all] * : If set, all plugins will be uninstalled. * * ## EXAMPLES * * $ wp plugin uninstall hello * Uninstalled and deleted 'hello' plugin. * Success: Uninstalled 1 of 1 plugins. */ public function uninstall( $args, $assoc_args = array() ) { $all = Utils\get_flag_value( $assoc_args, 'all', false ); // Check if plugin names or --all is passed. $args = $this->check_optional_args_and_all( $args, $all, 'uninstall' ); if ( ! $args ) { return; } $successes = 0; $errors = 0; $plugins = $this->fetcher->get_many( $args ); if ( count( $plugins ) < count( $args ) ) { $errors = count( $args ) - count( $plugins ); } foreach ( $plugins as $plugin ) { if ( is_plugin_active( $plugin->file ) && ! WP_CLI\Utils\get_flag_value( $assoc_args, 'deactivate' ) ) { WP_CLI::warning( "The '{$plugin->name}' plugin is active." ); $errors++; continue; } if ( Utils\get_flag_value( $assoc_args, 'deactivate' ) ) { WP_CLI::log( "Deactivating '{$plugin->name}'..." ); $this->chained_command = true; $this->deactivate( array( $plugin->name ) ); $this->chained_command = false; } uninstall_plugin( $plugin->file ); if ( ! Utils\get_flag_value( $assoc_args, 'skip-delete' ) && $this->delete_plugin( $plugin ) ) { WP_CLI::log( "Uninstalled and deleted '$plugin->name' plugin." ); } else { WP_CLI::log( "Ran uninstall procedure for '$plugin->name' plugin without deleting." ); } $successes++; } if ( ! $this->chained_command ) { Utils\report_batch_operation_results( 'plugin', 'uninstall', count( $args ), $successes, $errors ); } } /** * Checks if a given plugin is installed. * * Returns exit code 0 when installed, 1 when uninstalled. * * ## OPTIONS * * <plugin> * : The plugin to check. * * ## EXAMPLES * * # Check whether plugin is installed; exit status 0 if installed, otherwise 1 * $ wp plugin is-installed hello * $ echo $? * 1 * * @subcommand is-installed */ public function is_installed( $args, $assoc_args = array() ) { if ( $this->fetcher->get( $args[0] ) ) { WP_CLI::halt( 0 ); } else { WP_CLI::halt( 1 ); } } /** * Checks if a given plugin is active. * * Returns exit code 0 when active, 1 when not active. * * ## OPTIONS * * <plugin> * : The plugin to check. * * [--network] * : If set, check if plugin is network-activated. * * ## EXAMPLES * * # Check whether plugin is Active; exit status 0 if active, otherwise 1 * $ wp plugin is-active hello * $ echo $? * 1 * * @subcommand is-active */ public function is_active( $args, $assoc_args = array() ) { $network_wide = Utils\get_flag_value( $assoc_args, 'network' ); $plugin = $this->fetcher->get( $args[0] ); if ( ! $plugin ) { WP_CLI::halt( 1 ); } $this->check_active( $plugin->file, $network_wide ) ? WP_CLI::halt( 0 ) : WP_CLI::halt( 1 ); } /** * Deletes plugin files without deactivating or uninstalling. * * ## OPTIONS * * [<plugin>...] * : One or more plugins to delete. * * [--all] * : If set, all plugins will be deleted. * * ## EXAMPLES * * # Delete plugin * $ wp plugin delete hello * Deleted 'hello' plugin. * Success: Deleted 1 of 1 plugins. * * # Delete inactive plugins * $ wp plugin delete $(wp plugin list --status=inactive --field=name) * Deleted 'tinymce-templates' plugin. * Success: Deleted 1 of 1 plugins. */ public function delete( $args, $assoc_args = array() ) { $all = Utils\get_flag_value( $assoc_args, 'all', false ); // Check if plugin names or --all is passed. $args = $this->check_optional_args_and_all( $args, $all, 'delete' ); if ( ! $args ) { return; } $successes = 0; $errors = 0; foreach ( $this->fetcher->get_many( $args ) as $plugin ) { if ( $this->delete_plugin( $plugin ) ) { WP_CLI::log( "Deleted '{$plugin->name}' plugin." ); $successes++; } else { $errors++; } } if ( ! $this->chained_command ) { Utils\report_batch_operation_results( 'plugin', 'delete', count( $args ), $successes, $errors ); } } /** * Gets a list of plugins. * * Displays a list of the plugins installed on the site with activation * status, whether or not there's an update available, etc. * * Use `--status=dropin` to list installed dropins (e.g. `object-cache.php`). * * ## OPTIONS * * [--<field>=<value>] * : Filter results based on the value of a field. * * [--field=<field>] * : Prints the value of a single field for each plugin. * * [--fields=<fields>] * : Limit the output to specific object fields. * * [--format=<format>] * : Render output in a particular format. * --- * default: table * options: * - table * - csv * - count * - json * - yaml * --- * * [--status=<status>] * : Filter the output by plugin status. * --- * options: * - active * - active-network * - dropin * - inactive * - must-use * --- * * [--skip-update-check] * : If set, the plugin update check will be skipped. * * ## AVAILABLE FIELDS * * These fields will be displayed by default for each plugin: * * * name * * status * * update * * version * * These fields are optionally available: * * * update_version * * update_package * * update_id * * title * * description * * file * * ## EXAMPLES * * # List active plugins on the site. * $ wp plugin list --status=active --format=json * [{"name":"dynamic-hostname","status":"active","update":"none","version":"0.4.2"},{"name":"tinymce-templates","status":"active","update":"none","version":"4.4.3"},{"name":"wp-multibyte-patch","status":"active","update":"none","version":"2.4"},{"name":"wp-total-hacks","status":"active","update":"none","version":"2.0.1"}] * * # List plugins on each site in a network. * $ wp site list --field=url | xargs -I % wp plugin list --url=% * +---------+----------------+--------+---------+ * | name | status | update | version | * +---------+----------------+--------+---------+ * | akismet | active-network | none | 3.1.11 | * | hello | inactive | none | 1.6 | * +---------+----------------+--------+---------+ * +---------+----------------+--------+---------+ * | name | status | update | version | * +---------+----------------+--------+---------+ * | akismet | active-network | none | 3.1.11 | * | hello | inactive | none | 1.6 | * +---------+----------------+--------+---------+ * * @subcommand list */ public function list_( $_, $assoc_args ) { parent::_list( $_, $assoc_args ); } /* PRIVATES */ private function check_active( $file, $network_wide ) { $required = $network_wide ? 'active-network' : 'active'; return $required === $this->get_status( $file ); } private function active_output( $name, $file, $network_wide, $action ) { $network_wide = $network_wide || ( is_multisite() && is_network_only_plugin( $file ) ); $check = $this->check_active( $file, $network_wide ); if ( ( 'activate' === $action ) ? $check : ! $check ) { if ( $network_wide ) { WP_CLI::log( "Plugin '{$name}' network {$action}d." ); } else { WP_CLI::log( "Plugin '{$name}' {$action}d." ); } } else { WP_CLI::warning( "Could not {$action} the '{$name}' plugin." ); } } protected function get_status( $file ) { if ( is_plugin_active_for_network( $file ) ) { return 'active-network'; } if ( is_plugin_active( $file ) ) { return 'active'; } return 'inactive'; } /** * Gets the template path based on installation type. */ private static function get_template_path( $template ) { $command_root = Utils\phar_safe_path( dirname( __DIR__ ) ); $template_path = "{$command_root}/templates/{$template}"; if ( ! file_exists( $template_path ) ) { WP_CLI::error( "Couldn't find {$template}" ); } return $template_path; } /** * Gets the details of a plugin. * * @param object * @return array */ private function get_details( $file ) { $plugin_folder = get_plugins( '/' . plugin_basename( dirname( $file ) ) ); $plugin_file = Utils\basename( $file ); return $plugin_folder[ $plugin_file ]; } private function delete_plugin( $plugin ) { $plugin_dir = dirname( $plugin->file ); if ( '.' === $plugin_dir ) { $plugin_dir = $plugin->file; } $path = path_join( WP_PLUGIN_DIR, $plugin_dir ); if ( Utils\is_windows() ) { // Handles plugins that are not in own folders // e.g. Hello Dolly -> plugins/hello.php if ( is_file( $path ) ) { $command = 'del /f /q '; } else { $command = 'rd /s /q '; } $path = str_replace( '/', '\\', $path ); } else { $command = 'rm -rf '; } return ! WP_CLI::launch( $command . escapeshellarg( $path ) ); } } <?php use WP_CLI\Fetchers\Plugin; use WP_CLI\Formatter; use WP_CLI\ParsePluginNameInput; use WP_CLI\Utils; /** * Manages plugin auto-updates. * * ## EXAMPLES * * # Enable the auto-updates for a plugin * $ wp plugin auto-updates enable hello * Plugin auto-updates for 'hello' enabled. * Success: Enabled 1 of 1 plugin auto-updates. * * # Disable the auto-updates for a plugin * $ wp plugin auto-updates disable hello * Plugin auto-updates for 'hello' disabled. * Success: Disabled 1 of 1 plugin auto-updates. * * # Get the status of plugin auto-updates * $ wp plugin auto-updates status hello * Auto-updates for plugin 'hello' are disabled. * * @package wp-cli */ class Plugin_AutoUpdates_Command { use ParsePluginNameInput; /** * Site option that stores the status of plugin auto-updates. * * @var string */ const SITE_OPTION = 'auto_update_plugins'; /** * Plugin fetcher instance. * * @var Plugin */ protected $fetcher; /** * Plugin_AutoUpdates_Command constructor. */ public function __construct() { $this->fetcher = new Plugin(); } /** * Enables the auto-updates for a plugin. * * ## OPTIONS * * [<plugin>...] * : One or more plugins to enable auto-updates for. * * [--all] * : If set, auto-updates will be enabled for all plugins. * * [--disabled-only] * : If set, filters list of plugins to only include the ones that have * auto-updates disabled. * * ## EXAMPLES * * # Enable the auto-updates for a plugin * $ wp plugin auto-updates enable hello * Plugin auto-updates for 'hello' enabled. * Success: Enabled 1 of 1 plugin auto-updates. */ public function enable( $args, $assoc_args ) { $all = Utils\get_flag_value( $assoc_args, 'all', false ); $disabled_only = Utils\get_flag_value( $assoc_args, 'disabled-only', false ); $args = $this->check_optional_args_and_all( $args, $all ); if ( ! $args ) { return; } $plugins = $this->fetcher->get_many( $args ); $auto_updates = get_site_option( static::SITE_OPTION ); if ( false === $auto_updates ) { $auto_updates = []; } $count = 0; $successes = 0; foreach ( $plugins as $plugin ) { $enabled = in_array( $plugin->file, $auto_updates, true ); if ( $disabled_only && $enabled ) { continue; } $count++; if ( $enabled ) { WP_CLI::warning( "Auto-updates already enabled for plugin {$plugin->name}." ); } else { $auto_updates[] = $plugin->file; $successes++; } } if ( 0 === $count ) { WP_CLI::error( 'No plugins provided to enable auto-updates for.' ); } update_site_option( static::SITE_OPTION, $auto_updates ); Utils\report_batch_operation_results( 'plugin auto-update', 'enable', $count, $successes, $count - $successes ); } /** * Disables the auto-updates for a plugin. * * ## OPTIONS * * [<plugin>...] * : One or more plugins to disable auto-updates for. * * [--all] * : If set, auto-updates will be disabled for all plugins. * * [--enabled-only] * : If set, filters list of plugins to only include the ones that have * auto-updates enabled. * * ## EXAMPLES * * # Disable the auto-updates for a plugin * $ wp plugin auto-updates disable hello * Plugin auto-updates for 'hello' disabled. * Success: Disabled 1 of 1 plugin auto-updates. */ public function disable( $args, $assoc_args ) { $all = Utils\get_flag_value( $assoc_args, 'all', false ); $enabled_only = Utils\get_flag_value( $assoc_args, 'enabled-only', false ); $args = $this->check_optional_args_and_all( $args, $all ); if ( ! $args ) { return; } $plugins = $this->fetcher->get_many( $args ); $auto_updates = get_site_option( static::SITE_OPTION ); if ( false === $auto_updates ) { $auto_updates = []; } $count = 0; $successes = 0; foreach ( $plugins as $plugin ) { $enabled = in_array( $plugin->file, $auto_updates, true ); if ( $enabled_only && ! $enabled ) { continue; } $count++; if ( ! $enabled ) { WP_CLI::warning( "Auto-updates already disabled for plugin {$plugin->name}." ); } else { $auto_updates = array_diff( $auto_updates, [ $plugin->file ] ); $successes++; } } if ( 0 === $count ) { WP_CLI::error( 'No plugins provided to disable auto-updates for.' ); } if ( count( $auto_updates ) > 0 ) { update_site_option( static::SITE_OPTION, $auto_updates ); } else { delete_site_option( static::SITE_OPTION ); } Utils\report_batch_operation_results( 'plugin auto-update', 'disable', $count, $successes, $count - $successes ); } /** * Shows the status of auto-updates for a plugin. * * ## OPTIONS * * [<plugin>...] * : One or more plugins to show the status of the auto-updates of. * * [--all] * : If set, the status of auto-updates for all plugins will be shown. * * [--enabled-only] * : If set, filters list of plugins to only include the ones that have * auto-updates enabled. * * [--disabled-only] * : If set, filters list of plugins to only include the ones that have * auto-updates disabled. * * [--field=<field>] * : Only show the provided field. * * ## EXAMPLES * * # Get the status of plugin auto-updates * $ wp plugin auto-updates status hello * +-------+----------+ * | name | status | * +-------+----------+ * | hello | disabled | * +-------+----------+ * * # Get the list of plugins that have auto-updates enabled * $ wp plugin auto-updates status --all --enabled-only --field=name * akismet * duplicate-post */ public function status( $args, $assoc_args ) { $all = Utils\get_flag_value( $assoc_args, 'all', false ); $enabled_only = Utils\get_flag_value( $assoc_args, 'enabled-only', false ); $disabled_only = Utils\get_flag_value( $assoc_args, 'disabled-only', false ); if ( $enabled_only && $disabled_only ) { WP_CLI::error( '--enabled-only and --disabled-only are mutually exclusive and ' . 'cannot be used at the same time.' ); } $args = $this->check_optional_args_and_all( $args, $all ); if ( ! $args ) { return; } $plugins = $this->fetcher->get_many( $args ); $auto_updates = get_site_option( static::SITE_OPTION ); if ( false === $auto_updates ) { $auto_updates = []; } $results = []; foreach ( $plugins as $plugin ) { $enabled = in_array( $plugin->file, $auto_updates, true ); if ( $enabled_only && ! $enabled ) { continue; } if ( $disabled_only && $enabled ) { continue; } $results[] = [ 'name' => $plugin->name, 'file' => $plugin->file, 'status' => $enabled ? 'enabled' : 'disabled', ]; } $formatter = new Formatter( $assoc_args, [ 'name', 'status' ], 'plugin' ); $formatter->display_items( $results ); } } <?php use WP_CLI\CommandWithUpgrade; use WP_CLI\ParseThemeNameInput; use WP_CLI\Utils; /** * Manages themes, including installs, activations, and updates. * * See the WordPress [Theme Handbook](https://developer.wordpress.org/themes/) developer resource for more information on themes. * * ## EXAMPLES * * # Install the latest version of a theme from wordpress.org and activate * $ wp theme install twentysixteen --activate * Installing Twenty Sixteen (1.2) * Downloading install package from http://downloads.wordpress.org/theme/twentysixteen.1.2.zip... * Unpacking the package... * Installing the theme... * Theme installed successfully. * Activating 'twentysixteen'... * Success: Switched to 'Twenty Sixteen' theme. * * # Get details of an installed theme * $ wp theme get twentysixteen --fields=name,title,version * +---------+----------------+ * | Field | Value | * +---------+----------------+ * | name | Twenty Sixteen | * | title | Twenty Sixteen | * | version | 1.2 | * +---------+----------------+ * * # Get status of theme * $ wp theme status twentysixteen * Theme twentysixteen details: * Name: Twenty Sixteen * Status: Active * Version: 1.2 * Author: the WordPress team * * @package wp-cli */ class Theme_Command extends CommandWithUpgrade { use ParseThemeNameInput; protected $item_type = 'theme'; protected $upgrade_refresh = 'wp_update_themes'; protected $upgrade_transient = 'update_themes'; protected $obj_fields = [ 'name', 'status', 'update', 'version' ]; public function __construct() { if ( is_multisite() ) { $this->obj_fields[] = 'enabled'; } parent::__construct(); $this->fetcher = new WP_CLI\Fetchers\Theme(); } protected function get_upgrader_class( $force ) { return $force ? '\\WP_CLI\\DestructiveThemeUpgrader' : 'Theme_Upgrader'; } /** * Reveals the status of one or all themes. * * ## OPTIONS * * [<theme>] * : A particular theme to show the status for. * * ## EXAMPLES * * $ wp theme status twentysixteen * Theme twentysixteen details: * Name: Twenty Sixteen * Status: Inactive * Version: 1.2 * Author: the WordPress team */ public function status( $args ) { if ( isset( $args[0] ) ) { $theme = $this->fetcher->get_check( $args[0] ); $errors = $theme->errors(); if ( is_wp_error( $errors ) ) { $message = $errors->get_error_message(); WP_CLI::error( $message ); } } parent::status( $args ); } /** * Searches the WordPress.org theme directory. * * Displays themes in the WordPress.org theme directory matching a given * search query. * * ## OPTIONS * * <search> * : The string to search for. * * [--page=<page>] * : Optional page to display. * --- * default: 1 * --- * * [--per-page=<per-page>] * : Optional number of results to display. Defaults to 10. * * [--field=<field>] * : Prints the value of a single field for each theme. * * [--fields=<fields>] * : Ask for specific fields from the API. Defaults to name,slug,author,rating. Acceptable values: * * **name**: Theme Name * **slug**: Theme Slug * **version**: Current Version Number * **author**: Theme Author * **preview_url**: Theme Preview URL * **screenshot_url**: Theme Screenshot URL * **rating**: Theme Rating * **num_ratings**: Number of Theme Ratings * **homepage**: Theme Author's Homepage * **description**: Theme Description * **url**: Theme's URL on wordpress.org * * [--format=<format>] * : Render output in a particular format. * --- * default: table * options: * - table * - csv * - json * - count * - yaml * --- * * ## EXAMPLES * * $ wp theme search photo --per-page=6 * Success: Showing 6 of 203 themes. * +----------------------+----------------------+--------+ * | name | slug | rating | * +----------------------+----------------------+--------+ * | Photos | photos | 100 | * | Infinite Photography | infinite-photography | 100 | * | PhotoBook | photobook | 100 | * | BG Photo Frame | bg-photo-frame | 0 | * | fPhotography | fphotography | 0 | * | Photo Perfect | photo-perfect | 98 | * +----------------------+----------------------+--------+ */ public function search( $args, $assoc_args ) { parent::_search( $args, $assoc_args ); } protected function status_single( $args ) { $theme = $this->fetcher->get_check( $args[0] ); $status = $this->format_status( $this->get_status( $theme ), 'long' ); $version = $theme->get( 'Version' ); if ( $this->has_update( $theme->get_stylesheet() ) ) { $version .= ' (%gUpdate available%n)'; } echo WP_CLI::colorize( Utils\mustache_render( self::get_template_path( 'theme-status.mustache' ), [ 'slug' => $theme->get_stylesheet(), 'status' => $status, 'version' => $version, 'name' => $theme->get( 'Name' ), 'author' => $theme->get( 'Author' ), ] ) ); } protected function get_all_items() { return $this->get_item_list(); } /** * Activates a theme. * * ## OPTIONS * * <theme> * : The theme to activate. * * ## EXAMPLES * * $ wp theme activate twentysixteen * Success: Switched to 'Twenty Sixteen' theme. */ public function activate( $args = array() ) { $theme = $this->fetcher->get_check( $args[0] ); $errors = $theme->errors(); if ( is_wp_error( $errors ) ) { $message = $errors->get_error_message(); WP_CLI::error( $message ); } $name = $theme->get( 'Name' ); if ( 'active' === $this->get_status( $theme ) ) { WP_CLI::warning( "The '$name' theme is already active." ); return; } if ( $theme->get_stylesheet() !== $theme->get_template() && ! $this->fetcher->get( $theme->get_template() ) ) { WP_CLI::error( "The '{$theme->get_stylesheet()}' theme cannot be activated without its parent, '{$theme->get_template()}'." ); } switch_theme( $theme->get_template(), $theme->get_stylesheet() ); if ( $this->is_active_theme( $theme ) ) { WP_CLI::success( "Switched to '$name' theme." ); } else { WP_CLI::error( "Could not switch to '$name' theme." ); } } /** * Enables a theme on a WordPress multisite install. * * Permits theme to be activated from the dashboard of a site on a WordPress * multisite install. * * ## OPTIONS * * <theme> * : The theme to enable. * * [--network] * : If set, the theme is enabled for the entire network * * [--activate] * : If set, the theme is activated for the current site. Note that * the "network" flag has no influence on this. * * ## EXAMPLES * * # Enable theme * $ wp theme enable twentysixteen * Success: Enabled the 'Twenty Sixteen' theme. * * # Network enable theme * $ wp theme enable twentysixteen --network * Success: Network enabled the 'Twenty Sixteen' theme. * * # Network enable and activate theme for current site * $ wp theme enable twentysixteen --activate * Success: Enabled the 'Twenty Sixteen' theme. * Success: Switched to 'Twenty Sixteen' theme. */ public function enable( $args, $assoc_args ) { if ( ! is_multisite() ) { WP_CLI::error( 'This is not a multisite installation.' ); } $theme = $this->fetcher->get_check( $args[0] ); $name = $theme->get( 'Name' ); # If the --network flag is set, we'll be calling the (get|update)_site_option functions $_site = ! empty( $assoc_args['network'] ) ? '_site' : ''; # Add the current theme to the allowed themes option or site option $allowed_themes = call_user_func( "get{$_site}_option", 'allowedthemes' ); if ( empty( $allowed_themes ) ) { $allowed_themes = array(); } $allowed_themes[ $theme->get_stylesheet() ] = true; call_user_func( "update{$_site}_option", 'allowedthemes', $allowed_themes ); if ( ! empty( $assoc_args['network'] ) ) { WP_CLI::success( "Network enabled the '$name' theme." ); } else { WP_CLI::success( "Enabled the '$name' theme." ); } # If the --activate flag is set, activate the theme for the current site if ( ! empty( $assoc_args['activate'] ) ) { $this->activate( $args ); } } /** * Disables a theme on a WordPress multisite install. * * Removes ability for a theme to be activated from the dashboard of a site * on a WordPress multisite install. * * ## OPTIONS * * <theme> * : The theme to disable. * * [--network] * : If set, the theme is disabled on the network level. Note that * individual sites may still have this theme enabled if it was * enabled for them independently. * * ## EXAMPLES * * # Disable theme * $ wp theme disable twentysixteen * Success: Disabled the 'Twenty Sixteen' theme. * * # Disable theme in network level * $ wp theme disable twentysixteen --network * Success: Network disabled the 'Twenty Sixteen' theme. */ public function disable( $args, $assoc_args ) { if ( ! is_multisite() ) { WP_CLI::error( 'This is not a multisite installation.' ); } $theme = $this->fetcher->get_check( $args[0] ); $name = $theme->get( 'Name' ); # If the --network flag is set, we'll be calling the (get|update)_site_option functions $_site = ! empty( $assoc_args['network'] ) ? '_site' : ''; # Add the current theme to the allowed themes option or site option $allowed_themes = call_user_func( "get{$_site}_option", 'allowedthemes' ); if ( ! empty( $allowed_themes[ $theme->get_stylesheet() ] ) ) { unset( $allowed_themes[ $theme->get_stylesheet() ] ); } call_user_func( "update{$_site}_option", 'allowedthemes', $allowed_themes ); if ( ! empty( $assoc_args['network'] ) ) { WP_CLI::success( "Network disabled the '$name' theme." ); } else { WP_CLI::success( "Disabled the '$name' theme." ); } } /** * Gets the path to a theme or to the theme directory. * * ## OPTIONS * * [<theme>] * : The theme to get the path to. Path includes "style.css" file. * If not set, will return the path to the themes directory. * * [--dir] * : If set, get the path to the closest parent directory, instead of the * theme's "style.css" file. * * ## EXAMPLES * * # Get theme path * $ wp theme path * /var/www/example.com/public_html/wp-content/themes * * # Change directory to theme path * $ cd $(wp theme path) */ public function path( $args, $assoc_args ) { if ( empty( $args ) ) { $path = WP_CONTENT_DIR . '/themes'; } else { $theme = $this->fetcher->get_check( $args[0] ); $path = $theme->get_stylesheet_directory(); if ( ! Utils\get_flag_value( $assoc_args, 'dir' ) ) { $path .= '/style.css'; } } WP_CLI::line( $path ); } protected function install_from_repo( $slug, $assoc_args ) { $api = themes_api( 'theme_information', array( 'slug' => $slug ) ); if ( is_wp_error( $api ) ) { return $api; } if ( isset( $assoc_args['version'] ) ) { self::alter_api_response( $api, $assoc_args['version'] ); } if ( ! Utils\get_flag_value( $assoc_args, 'force' ) ) { $theme = wp_get_theme( $slug ); if ( $theme->exists() ) { // We know this will fail, so avoid a needless download of the package. return new WP_Error( 'already_installed', 'Theme already installed.' ); } // Clear cache so WP_Theme doesn't create a "missing theme" object. $cache_hash = md5( $theme->theme_root . '/' . $theme->stylesheet ); foreach ( [ 'theme', 'screenshot', 'headers', 'page_templates' ] as $key ) { wp_cache_delete( $key . '-' . $cache_hash, 'themes' ); } } WP_CLI::log( sprintf( 'Installing %s (%s)', html_entity_decode( $api->name, ENT_QUOTES ), $api->version ) ); if ( Utils\get_flag_value( $assoc_args, 'version' ) !== 'dev' ) { WP_CLI::get_http_cache_manager()->whitelist_package( $api->download_link, $this->item_type, $api->slug, $api->version ); } // Ignore failures on accessing SSL "https://api.wordpress.org/themes/update-check/1.1/" in `wp_update_themes()` which seem to occur intermittently. set_error_handler( array( __CLASS__, 'error_handler' ), E_USER_WARNING | E_USER_NOTICE ); $result = $this->get_upgrader( $assoc_args )->install( $api->download_link ); restore_error_handler(); return $result; } protected function get_item_list() { return $this->get_all_themes(); } protected function filter_item_list( $items, $args ) { $theme_files = array(); foreach ( $args as $arg ) { $theme_files[] = $this->fetcher->get_check( $arg )->get_stylesheet_directory(); } return Utils\pick_fields( $items, $theme_files ); } /** * Installs one or more themes. * * ## OPTIONS * * <theme|zip|url>... * : One or more themes to install. Accepts a theme slug, the path to a local zip file, or a URL to a remote zip file. * * [--version=<version>] * : If set, get that particular version from wordpress.org, instead of the * stable version. * * [--force] * : If set, the command will overwrite any installed version of the theme, without prompting * for confirmation. * * [--activate] * : If set, the theme will be activated immediately after install. * * [--insecure] * : Retry downloads without certificate validation if TLS handshake fails. Note: This makes the request vulnerable to a MITM attack. * * ## EXAMPLES * * # Install the latest version from wordpress.org and activate * $ wp theme install twentysixteen --activate * Installing Twenty Sixteen (1.2) * Downloading install package from http://downloads.wordpress.org/theme/twentysixteen.1.2.zip... * Unpacking the package... * Installing the theme... * Theme installed successfully. * Activating 'twentysixteen'... * Success: Switched to 'Twenty Sixteen' theme. * * # Install from a local zip file * $ wp theme install ../my-theme.zip * * # Install from a remote zip file * $ wp theme install http://s3.amazonaws.com/bucketname/my-theme.zip?AWSAccessKeyId=123&Expires=456&Signature=abcdef */ public function install( $args, $assoc_args ) { $theme_root = get_theme_root(); if ( $theme_root && ! is_dir( $theme_root ) ) { wp_mkdir_p( $theme_root ); register_theme_directory( $theme_root ); } parent::install( $args, $assoc_args ); } /** * Gets details about a theme. * * ## OPTIONS * * <theme> * : The theme to get. * * [--field=<field>] * : Instead of returning the whole theme, returns the value of a single field. * * [--fields=<fields>] * : Limit the output to specific fields. Defaults to all fields. * * [--format=<format>] * : Render output in a particular format. * --- * default: table * options: * - table * - csv * - json * - yaml * --- * * ## EXAMPLES * * $ wp theme get twentysixteen --fields=name,title,version * +---------+----------------+ * | Field | Value | * +---------+----------------+ * | name | Twenty Sixteen | * | title | Twenty Sixteen | * | version | 1.2 | * +---------+----------------+ */ public function get( $args, $assoc_args ) { $theme = $this->fetcher->get_check( $args[0] ); $errors = $theme->errors(); if ( is_wp_error( $errors ) ) { $message = $errors->get_error_message(); WP_CLI::error( $message ); } // WP_Theme object employs magic getter, unfortunately. $theme_vars = [ 'name', 'title', 'version', 'status', 'parent_theme', 'template_dir', 'stylesheet_dir', 'template', 'stylesheet', 'screenshot', 'description', 'author', 'tags', 'theme_root', 'theme_root_uri', ]; $theme_obj = new stdClass(); foreach ( $theme_vars as $var ) { $theme_obj->$var = $theme->$var; } $theme_obj->status = $this->get_status( $theme ); $theme_obj->description = wordwrap( $theme_obj->description ); if ( empty( $assoc_args['fields'] ) ) { $assoc_args['fields'] = $theme_vars; } $formatter = $this->get_formatter( $assoc_args ); $formatter->display_item( $theme_obj ); } /** * Updates one or more themes. * * ## OPTIONS * * [<theme>...] * : One or more themes to update. * * [--all] * : If set, all themes that have updates will be updated. * * [--exclude=<theme-names>] * : Comma separated list of theme names that should be excluded from updating. * * [--format=<format>] * : Render output in a particular format. * --- * default: table * options: * - table * - csv * - json * - summary * --- * * [--version=<version>] * : If set, the theme will be updated to the specified version. * * [--dry-run] * : Preview which themes would be updated. * * [--insecure] * : Retry downloads without certificate validation if TLS handshake fails. Note: This makes the request vulnerable to a MITM attack. * * ## EXAMPLES * * # Update multiple themes * $ wp theme update twentyfifteen twentysixteen * Downloading update from https://downloads.wordpress.org/theme/twentyfifteen.1.5.zip... * Unpacking the update... * Installing the latest version... * Removing the old version of the theme... * Theme updated successfully. * Downloading update from https://downloads.wordpress.org/theme/twentysixteen.1.2.zip... * Unpacking the update... * Installing the latest version... * Removing the old version of the theme... * Theme updated successfully. * +---------------+-------------+-------------+---------+ * | name | old_version | new_version | status | * +---------------+-------------+-------------+---------+ * | twentyfifteen | 1.4 | 1.5 | Updated | * | twentysixteen | 1.1 | 1.2 | Updated | * +---------------+-------------+-------------+---------+ * Success: Updated 2 of 2 themes. * * # Exclude themes updates when bulk updating the themes * $ wp theme update --all --exclude=twentyfifteen * Downloading update from https://downloads.wordpress.org/theme/astra.1.0.5.1.zip... * Unpacking the update... * Installing the latest version... * Removing the old version of the theme... * Theme updated successfully. * Downloading update from https://downloads.wordpress.org/theme/twentyseventeen.1.2.zip... * Unpacking the update... * Installing the latest version... * Removing the old version of the theme... * Theme updated successfully. * +-----------------+----------+---------+----------------+ * | name | status | version | update_version | * +-----------------+----------+---------+----------------+ * | astra | inactive | 1.0.1 | 1.0.5.1 | * | twentyseventeen | inactive | 1.1 | 1.2 | * +-----------------+----------+---------+----------------+ * Success: Updated 2 of 2 themes. * * # Update all themes * $ wp theme update --all * * @alias upgrade */ public function update( $args, $assoc_args ) { $all = Utils\get_flag_value( $assoc_args, 'all', false ); $args = $this->check_optional_args_and_all( $args, $all ); if ( ! $args ) { return; } if ( isset( $assoc_args['version'] ) && isset( $assoc_args['dry-run'] ) ) { WP_CLI::error( '--dry-run cannot be used together with --version.' ); } if ( isset( $assoc_args['version'] ) ) { foreach ( $this->fetcher->get_many( $args ) as $theme ) { $r = delete_theme( $theme->stylesheet ); if ( is_wp_error( $r ) ) { WP_CLI::warning( $r ); } else { $assoc_args['force'] = true; $this->install( array( $theme->stylesheet ), $assoc_args ); } } } else { parent::update_many( $args, $assoc_args ); } } /** * Checks if a given theme is installed. * * Returns exit code 0 when installed, 1 when uninstalled. * * ## OPTIONS * * <theme> * : The theme to check. * * ## EXAMPLES * * # Check whether theme is installed; exit status 0 if installed, otherwise 1 * $ wp theme is-installed hello * $ echo $? * 1 * * @subcommand is-installed */ public function is_installed( $args, $assoc_args = array() ) { $theme = wp_get_theme( $args[0] ); if ( $theme->exists() ) { WP_CLI::halt( 0 ); } else { WP_CLI::halt( 1 ); } } /** * Checks if a given theme is active. * * Returns exit code 0 when active, 1 when not active. * * ## OPTIONS * * <theme> * : The plugin to check. * * ## EXAMPLES * * # Check whether theme is Active; exit status 0 if active, otherwise 1 * $ wp theme is-active twentyfifteen * $ echo $? * 1 * * @subcommand is-active */ public function is_active( $args, $assoc_args = array() ) { $theme = wp_get_theme( $args[0] ); if ( ! $theme->exists() ) { WP_CLI::halt( 1 ); } $this->is_active_theme( $theme ) ? WP_CLI::halt( 0 ) : WP_CLI::halt( 1 ); } /** * Deletes one or more themes. * * Removes the theme or themes from the filesystem. * * ## OPTIONS * * [<theme>...] * : One or more themes to delete. * * [--all] * : If set, all themes will be deleted except active theme. * * [--force] * : To delete active theme use this. * * ## EXAMPLES * * $ wp theme delete twentytwelve * Deleted 'twentytwelve' theme. * Success: Deleted 1 of 1 themes. * * @alias uninstall */ public function delete( $args, $assoc_args ) { $all = Utils\get_flag_value( $assoc_args, 'all', false ); $args = $this->check_optional_args_and_all( $args, $all, 'delete' ); if ( ! $args ) { return; } $force = Utils\get_flag_value( $assoc_args, 'force', false ); $successes = 0; $errors = 0; foreach ( $this->fetcher->get_many( $args ) as $theme ) { $theme_slug = $theme->get_stylesheet(); if ( $this->is_active_theme( $theme ) && ! $force ) { if ( ! $all ) { WP_CLI::warning( "Can't delete the currently active theme: $theme_slug" ); $errors++; } continue; } $r = delete_theme( $theme_slug ); if ( is_wp_error( $r ) ) { WP_CLI::warning( $r ); $errors++; } else { WP_CLI::log( "Deleted '$theme_slug' theme." ); $successes++; } } if ( ! $this->chained_command ) { Utils\report_batch_operation_results( 'theme', 'delete', count( $args ), $successes, $errors ); } } /** * Gets a list of themes. * * ## OPTIONS * * [--<field>=<value>] * : Filter results based on the value of a field. * * [--field=<field>] * : Prints the value of a single field for each theme. * * [--fields=<fields>] * : Limit the output to specific object fields. * * [--format=<format>] * : Render output in a particular format. * --- * default: table * options: * - table * - csv * - json * - count * - yaml * --- * * [--status=<status>] * : Filter the output by theme status. * --- * options: * - active * - parent * - inactive * --- * * [--skip-update-check] * : If set, the theme update check will be skipped. * * ## AVAILABLE FIELDS * * These fields will be displayed by default for each theme: * * * name * * status * * update * * version * * These fields are optionally available: * * * update_version * * update_package * * update_id * * title * * description * * ## EXAMPLES * * # List themes * $ wp theme list --status=inactive --format=csv * name,status,update,version * twentyfourteen,inactive,none,1.7 * twentysixteen,inactive,available,1.1 * * @subcommand list */ public function list_( $_, $assoc_args ) { parent::_list( $_, $assoc_args ); } /** * Gets the template path based on installation type. */ private static function get_template_path( $template ) { $command_root = Utils\phar_safe_path( dirname( __DIR__ ) ); $template_path = "{$command_root}/templates/{$template}"; if ( ! file_exists( $template_path ) ) { WP_CLI::error( "Couldn't find {$template}" ); } return $template_path; } } <?php use WP_CLI\Fetchers\Theme; use WP_CLI\Formatter; use WP_CLI\ParseThemeNameInput; use WP_CLI\Utils; /** * Manages theme auto-updates. * * ## EXAMPLES * * # Enable the auto-updates for a theme * $ wp theme auto-updates enable twentysixteen * Theme auto-updates for 'twentysixteen' enabled. * Success: Enabled 1 of 1 theme auto-updates. * * # Disable the auto-updates for a theme * $ wp theme auto-updates disable twentysixteen * Theme auto-updates for 'twentysixteen' disabled. * Success: Disabled 1 of 1 theme auto-updates. * * # Get the status of theme auto-updates * $ wp theme auto-updates status twentysixteen * Auto-updates for theme 'twentysixteen' are disabled. * * @package wp-cli */ class Theme_AutoUpdates_Command { use ParseThemeNameInput; /** * Site option that stores the status of theme auto-updates. * * @var string */ const SITE_OPTION = 'auto_update_themes'; /** * Theme fetcher instance. * * @var Theme */ protected $fetcher; /** * Theme_AutoUpdates_Command constructor. */ public function __construct() { $this->fetcher = new Theme(); } /** * Enables the auto-updates for a theme. * * ## OPTIONS * * [<theme>...] * : One or more themes to enable auto-updates for. * * [--all] * : If set, auto-updates will be enabled for all themes. * * [--disabled-only] * : If set, filters list of themes to only include the ones that have * auto-updates disabled. * * ## EXAMPLES * * # Enable the auto-updates for a theme * $ wp theme auto-updates enable twentysixteen * Theme auto-updates for 'twentysixteen' enabled. * Success: Enabled 1 of 1 theme auto-updates. */ public function enable( $args, $assoc_args ) { $all = Utils\get_flag_value( $assoc_args, 'all', false ); $disabled_only = Utils\get_flag_value( $assoc_args, 'disabled-only', false ); $args = $this->check_optional_args_and_all( $args, $all ); if ( ! $args ) { return; } $themes = $this->fetcher->get_many( $args ); $auto_updates = get_site_option( static::SITE_OPTION ); if ( false === $auto_updates ) { $auto_updates = []; } $count = 0; $successes = 0; foreach ( $themes as $theme ) { $enabled = in_array( $theme->stylesheet, $auto_updates, true ); if ( $disabled_only && $enabled ) { continue; } $count++; if ( $enabled ) { WP_CLI::warning( "Auto-updates already enabled for theme {$theme->stylesheet}." ); } else { $auto_updates[] = $theme->stylesheet; $successes++; } } if ( 0 === $count ) { WP_CLI::error( 'No themes provided to enable auto-updates for.' ); } update_site_option( static::SITE_OPTION, $auto_updates ); Utils\report_batch_operation_results( 'theme auto-update', 'enable', $count, $successes, $count - $successes ); } /** * Disables the auto-updates for a theme. * * ## OPTIONS * * [<theme>...] * : One or more themes to disable auto-updates for. * * [--all] * : If set, auto-updates will be disabled for all themes. * * [--enabled-only] * : If set, filters list of themes to only include the ones that have * auto-updates enabled. * * ## EXAMPLES * * # Disable the auto-updates for a theme * $ wp theme auto-updates disable twentysixteen * Theme auto-updates for 'twentysixteen' disabled. * Success: Disabled 1 of 1 theme auto-updates. */ public function disable( $args, $assoc_args ) { $all = Utils\get_flag_value( $assoc_args, 'all', false ); $enabled_only = Utils\get_flag_value( $assoc_args, 'enabled-only', false ); $args = $this->check_optional_args_and_all( $args, $all ); if ( ! $args ) { return; } $themes = $this->fetcher->get_many( $args ); $auto_updates = get_site_option( static::SITE_OPTION ); if ( false === $auto_updates ) { $auto_updates = []; } $count = 0; $successes = 0; foreach ( $themes as $theme ) { $enabled = in_array( $theme->stylesheet, $auto_updates, true ); if ( $enabled_only && ! $enabled ) { continue; } $count++; if ( ! $enabled ) { WP_CLI::warning( "Auto-updates already disabled for theme {$theme->stylesheet}." ); } else { $auto_updates = array_diff( $auto_updates, [ $theme->stylesheet ] ); $successes++; } } if ( 0 === $count ) { WP_CLI::error( 'No themes provided to disable auto-updates for.' ); } if ( count( $auto_updates ) > 0 ) { update_site_option( static::SITE_OPTION, $auto_updates ); } else { delete_site_option( static::SITE_OPTION ); } Utils\report_batch_operation_results( 'theme auto-update', 'disable', $count, $successes, $count - $successes ); } /** * Shows the status of auto-updates for a theme. * * ## OPTIONS * * [<theme>...] * : One or more themes to show the status of the auto-updates of. * * [--all] * : If set, the status of auto-updates for all themes will be shown. * * [--enabled-only] * : If set, filters list of themes to only include the ones that have * auto-updates enabled. * * [--disabled-only] * : If set, filters list of themes to only include the ones that have * auto-updates disabled. * * [--field=<field>] * : Only show the provided field. * * ## EXAMPLES * * # Get the status of theme auto-updates * $ wp theme auto-updates status twentysixteen * +---------------+----------+ * | name | status | * +---------------+----------+ * | twentysixteen | disabled | * +---------------+----------+ * * # Get the list of themes that have auto-updates enabled * $ wp theme auto-updates status --all --enabled-only --field=name * twentysixteen * twentyseventeen */ public function status( $args, $assoc_args ) { $all = Utils\get_flag_value( $assoc_args, 'all', false ); $enabled_only = Utils\get_flag_value( $assoc_args, 'enabled-only', false ); $disabled_only = Utils\get_flag_value( $assoc_args, 'disabled-only', false ); if ( $enabled_only && $disabled_only ) { WP_CLI::error( '--enabled-only and --disabled-only are mutually exclusive and ' . 'cannot be used at the same time.' ); } $args = $this->check_optional_args_and_all( $args, $all ); if ( ! $args ) { return; } $themes = $this->fetcher->get_many( $args ); $auto_updates = get_site_option( static::SITE_OPTION ); if ( false === $auto_updates ) { $auto_updates = []; } $results = []; foreach ( $themes as $theme ) { $enabled = in_array( $theme->stylesheet, $auto_updates, true ); if ( $enabled_only && ! $enabled ) { continue; } if ( $disabled_only && $enabled ) { continue; } $results[] = [ 'name' => $theme->stylesheet, 'name ' => $theme->stylesheet, 'status' => $enabled ? 'enabled' : 'disabled', ]; } $formatter = new Formatter( $assoc_args, [ 'name', 'status' ], 'theme' ); $formatter->display_items( $results ); } } <?php if ( ! class_exists( 'WP_CLI' ) ) { return; } $wpcli_extension_autoloader = dirname( __FILE__ ) . '/vendor/autoload.php'; if ( file_exists( $wpcli_extension_autoloader ) ) { require_once $wpcli_extension_autoloader; } $wpcli_extension_requires_wp_5_5 = [ 'before_invoke' => static function () { if ( WP_CLI\Utils\wp_version_compare( '5.5', '<' ) ) { WP_CLI::error( 'Requires WordPress 5.5 or greater.' ); } }, ]; WP_CLI::add_command( 'plugin', 'Plugin_Command' ); WP_CLI::add_command( 'plugin auto-updates', 'Plugin_AutoUpdates_Command', $wpcli_extension_requires_wp_5_5 ); WP_CLI::add_command( 'theme', 'Theme_Command' ); WP_CLI::add_command( 'theme auto-updates', 'Theme_AutoUpdates_Command', $wpcli_extension_requires_wp_5_5 ); WP_CLI::add_command( 'theme mod', 'Theme_Mod_Command' ); <?php class WP_Map_Iterator extends IteratorIterator { public function __construct( $iterator, $callback ) { $this->callback = $callback; parent::__construct( $iterator ); } public function current() { $original_current = parent::current(); return call_user_func( $this->callback, $original_current ); } } <?php use WP_CLI\Utils; // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedConstantFound -- Changing breaks Phar compat. define( 'WP_CLI_EXPORT_COMMAND_NO_SPLIT', '-1' ); /** * Exports WordPress content to a WXR file. * * ## EXAMPLES * * # Export posts published by the user between given start and end date * $ wp export --dir=/tmp/ --user=admin --post_type=post --start_date=2011-01-01 --end_date=2011-12-31 * Starting export process... * Writing to file /tmp/staging.wordpress.2016-05-24.000.xml * Success: All done with export. * * @package wp-cli */ class Export_Command extends WP_CLI_Command { /** * Initialize the array of arguments that will be eventually be passed to export_wp. * * @var array */ public $export_args = []; private $stdout; private $max_file_size; private $wxr_path; /** * Exports WordPress content to a WXR file. * * Generates one or more WXR files containing authors, terms, posts, * comments, and attachments. WXR files do not include site configuration * (options) or the attachment files themselves. * * ## OPTIONS * * [--dir=<dirname>] * : Full path to directory where WXR export files should be stored. Defaults * to current working directory. * * [--stdout] * : Output the whole XML using standard output (incompatible with --dir=) * * [--skip_comments] * : Don't include comments in the WXR export file. * * [--max_file_size=<MB>] * : A single export file should have this many megabytes. -1 for unlimited. * --- * default: 15 * --- * * ## FILTERS * * [--start_date=<date>] * : Export only posts published after this date, in format YYYY-MM-DD. * * [--end_date=<date>] * : Export only posts published before this date, in format YYYY-MM-DD. * * [--post_type=<post-type>] * : Export only posts with this post_type. Separate multiple post types with a * comma. * --- * default: any * --- * * [--post_type__not_in=<post-type>] * : Export all post types except those identified. Separate multiple post types * with a comma. Defaults to none. * * [--post__in=<pid>] * : Export all posts specified as a comma- or space-separated list of IDs. * Post's attachments won't be exported unless --with_attachments is specified. * * [--with_attachments] * : Force including attachments in case --post__in has been specified. * * [--start_id=<pid>] * : Export only posts with IDs greater than or equal to this post ID. * * [--max_num_posts=<num>] * : Export no more than <num> posts (excluding attachments). * * [--author=<author>] * : Export only posts by this author. Can be either user login or user ID. * * [--category=<name|id>] * : Export only posts in this category. * * [--post_status=<status>] * : Export only posts with this status. * * [--filename_format=<format>] * : Use a custom format for export filenames. Defaults to '{site}.wordpress.{date}.{n}.xml'. * * ## EXAMPLES * * # Export posts published by the user between given start and end date * $ wp export --dir=/tmp/ --user=admin --post_type=post --start_date=2011-01-01 --end_date=2011-12-31 * Starting export process... * Writing to file /tmp/staging.wordpress.2016-05-24.000.xml * Success: All done with export. * * # Export posts by IDs * $ wp export --dir=/tmp/ --post__in=123,124,125 * Starting export process... * Writing to file /tmp/staging.wordpress.2016-05-24.000.xml * Success: All done with export. * * # Export a random subset of content * $ wp export --post__in="$(wp post list --post_type=post --orderby=rand --posts_per_page=8 --format=ids)" * Starting export process... * Writing to file /var/www/example.com/public_html/staging.wordpress.2016-05-24.000.xml * Success: All done with export. */ public function __invoke( $_, $assoc_args ) { $defaults = [ 'dir' => null, 'stdout' => false, 'start_date' => null, 'end_date' => null, 'post_type' => null, 'post_type__not_in' => null, 'max_num_posts' => null, 'author' => null, 'category' => null, 'post_status' => null, 'post__in' => null, 'with_attachments' => true, // or FALSE if user requested some post__in 'start_id' => null, 'skip_comments' => null, 'max_file_size' => 15, 'filename_format' => '{site}.wordpress.{date}.{n}.xml', ]; if ( ! empty( $assoc_args['stdout'] ) && ( ! empty( $assoc_args['dir'] ) || ! empty( $assoc_args['filename_format'] ) ) ) { WP_CLI::error( '--stdout and --dir cannot be used together.' ); } if ( ! empty( $assoc_args['post__in'] ) && empty( $assoc_args['with_attachments'] ) ) { $defaults['with_attachments'] = false; } $assoc_args = wp_parse_args( $assoc_args, $defaults ); $this->validate_args( $assoc_args ); $this->export_args['with_attachments'] = Utils\get_flag_value( $assoc_args, 'with_attachments', $defaults['with_attachments'] ); if ( ! function_exists( 'wp_export' ) ) { self::load_export_api(); } if ( ! $this->stdout ) { WP_CLI::log( 'Starting export process...' ); } add_action( 'wp_export_new_file', static function ( $file_path ) { WP_CLI::log( sprintf( 'Writing to file %s', $file_path ) ); Utils\wp_clear_object_cache(); } ); try { if ( $this->stdout ) { wp_export( [ 'filters' => $this->export_args, 'writer' => 'WP_Export_File_Writer', 'writer_args' => 'php://output', ] ); } else { wp_export( [ 'filters' => $this->export_args, 'writer' => 'WP_Export_Split_Files_Writer', 'writer_args' => [ 'max_file_size' => $this->max_file_size, 'destination_directory' => $this->wxr_path, 'filename_template' => self::get_filename_template( $assoc_args['filename_format'] ), ], ] ); } } catch ( Exception $e ) { WP_CLI::error( $e->getMessage() ); } if ( ! $this->stdout ) { WP_CLI::success( 'All done with export.' ); } } private static function get_filename_template( $filename_format ) { $sitename = sanitize_key( get_bloginfo( 'name' ) ); if ( empty( $sitename ) ) { $sitename = 'site'; } return str_replace( [ '{site}', '{date}', '{n}' ], [ $sitename, date( 'Y-m-d' ), '%03d' ], $filename_format ); // phpcs:ignore WordPress.DateTime.RestrictedFunctions.date_date } public static function load_export_api() { require dirname( __DIR__ ) . '/functions.php'; } private function validate_args( $args ) { $has_errors = false; foreach ( $args as $key => $value ) { if ( is_callable( [ $this, 'check_' . $key ] ) ) { $result = call_user_func( [ $this, 'check_' . $key ], $value ); if ( false === $result ) { $has_errors = true; } } } if ( $args['stdout'] ) { $this->wxr_path = null; $this->stdout = true; } if ( $has_errors ) { WP_CLI::halt( 1 ); } } private function check_dir( $path ) { if ( empty( $path ) ) { $path = getcwd(); } elseif ( ! is_dir( $path ) ) { WP_CLI::error( sprintf( "The directory '%s' does not exist.", $path ) ); } elseif ( ! is_writable( $path ) ) { WP_CLI::error( sprintf( "The directory '%s' is not writable.", $path ) ); } $this->wxr_path = trailingslashit( $path ); return true; } private function check_start_date( $date ) { if ( null === $date ) { return true; } $time = strtotime( $date ); if ( ! empty( $date ) && ! $time ) { WP_CLI::warning( sprintf( 'The start_date %s is invalid.', $date ) ); return false; } $this->export_args['start_date'] = date( 'Y-m-d', $time ); // phpcs:ignore WordPress.DateTime.RestrictedFunctions.date_date return true; } private function check_end_date( $date ) { if ( null === $date ) { return true; } $time = strtotime( $date ); if ( ! empty( $date ) && ! $time ) { WP_CLI::warning( sprintf( 'The end_date %s is invalid.', $date ) ); return false; } $this->export_args['end_date'] = date( 'Y-m-d', $time ); // phpcs:ignore WordPress.DateTime.RestrictedFunctions.date_date return true; } private function check_post_type( $post_type ) { if ( null === $post_type || 'any' === $post_type ) { return true; } $post_type = array_unique( array_filter( explode( ',', $post_type ) ) ); $post_types = get_post_types(); foreach ( $post_type as $type ) { if ( ! in_array( $type, $post_types, true ) ) { WP_CLI::warning( sprintf( 'The post type %s does not exist. Choose "any" or any of these existing post types instead: %s', $type, implode( ', ', $post_types ) ) ); return false; } } $this->export_args['post_type'] = $post_type; return true; } private function check_post_type__not_in( $post_type ) { if ( null === $post_type ) { return true; } $post_type = array_unique( array_filter( explode( ',', $post_type ) ) ); $post_types = get_post_types(); foreach ( $post_type as $type ) { if ( ! in_array( $type, $post_types, true ) ) { WP_CLI::warning( sprintf( 'The post type %s does not exist. Use any of these existing post types instead: %s', $type, implode( ', ', $post_types ) ) ); return false; } } $this->export_args['post_type'] = array_diff( $post_types, $post_type ); return true; } private function check_post__in( $post__in ) { if ( null === $post__in ) { return true; } $separator = false !== stripos( $post__in, ' ' ) ? ' ' : ','; $post__in = array_filter( array_unique( array_map( 'intval', explode( $separator, $post__in ) ) ) ); if ( empty( $post__in ) ) { WP_CLI::warning( 'post__in should be comma-separated post IDs.' ); return false; } // New exporter uses a different argument. $this->export_args['post_ids'] = $post__in; return true; } private function check_start_id( $start_id ) { if ( null === $start_id ) { return true; } $start_id = (int) $start_id; // Post IDs must be greater than 0. if ( 0 >= $start_id ) { WP_CLI::warning( "Invalid start ID: {$start_id}" ); return false; } $this->export_args['start_id'] = $start_id; return true; } private function check_author( $author ) { if ( null === $author ) { return true; } // phpcs:ignore WordPress.WP.DeprecatedFunctions.get_users_of_blogFound -- Fallback. $authors = function_exists( 'get_users' ) ? get_users() : get_users_of_blog(); if ( empty( $authors ) || is_wp_error( $authors ) ) { WP_CLI::warning( 'Could not find any authors in this blog.' ); return false; } $hit = false; foreach ( $authors as $user ) { if ( $hit ) { break; } if ( (int) $author === $user->ID || $author === $user->user_login ) { $hit = $user->ID; } } if ( false === $hit ) { $authors_nice = []; foreach ( $authors as $_author ) { $authors_nice[] = sprintf( '%s (%s)', $_author->user_login, $_author->display_name ); } WP_CLI::warning( sprintf( 'Could not find a matching author for %s. The following authors exist: %s', $author, implode( ', ', $authors_nice ) ) ); return false; } $this->export_args['author'] = $hit; return true; } private function check_max_num_posts( $num ) { if ( null !== $num && ( ! is_numeric( $num ) || $num <= 0 ) ) { WP_CLI::warning( 'max_num_posts should be a positive integer.' ); return false; } $this->export_args['max_num_posts'] = (int) $num; return true; } private function check_category( $category ) { if ( null === $category ) { return true; } if ( is_numeric( $category ) ) { $category = (int) $category; } $term = category_exists( $category ); if ( empty( $term ) || is_wp_error( $term ) ) { WP_CLI::warning( sprintf( 'Could not find a category matching %s.', $category ) ); return false; } $this->export_args['category'] = $category; return true; } private function check_post_status( $status ) { if ( null === $status ) { return true; } $stati = get_post_statuses(); if ( empty( $stati ) || is_wp_error( $stati ) ) { WP_CLI::warning( 'Could not find any post stati.' ); return false; } if ( ! isset( $stati[ $status ] ) ) { WP_CLI::warning( sprintf( 'Could not find a post_status matching %s. Here is a list of available stati: %s', $status, implode( ', ', array_keys( $stati ) ) ) ); return false; } $this->export_args['status'] = $status; return true; } private function check_skip_comments( $skip ) { if ( null === $skip ) { return true; } if ( 0 !== (int) $skip && 1 !== (int) $skip ) { WP_CLI::warning( 'skip_comments needs to be 0 (no) or 1 (yes).' ); return false; } $this->export_args['skip_comments'] = $skip; return true; } private function check_max_file_size( $size ) { if ( ! is_numeric( $size ) ) { WP_CLI::warning( 'max_file_size should be numeric.' ); return false; } $this->max_file_size = $size; return true; } } <?php use WP_CLI\Utils; /** * Represents a set of posts and other site data to be exported. * * An immutable object, which gathers all data needed for the export. */ class WP_Export_Query { const QUERY_CHUNK = 100; private static $defaults = [ 'post_ids' => null, 'post_type' => null, 'status' => null, 'author' => null, 'start_date' => null, 'end_date' => null, 'start_id' => null, 'max_num_posts' => null, 'category' => null, ]; private $post_ids; private $filters; private $wheres = []; private $joins = []; private $author; private $category; public $missing_parents = false; public function __construct( $filters = [] ) { $this->filters = wp_parse_args( $filters, self::$defaults ); $user = $this->find_user_from_any_object( $this->filters['author'] ); if ( $user && ! is_wp_error( $user ) ) { $this->author = $user; } $this->post_ids = $this->calculate_post_ids(); } public function post_ids() { return $this->post_ids; } public function charset() { return get_bloginfo( 'charset' ); } public function site_metadata() { $metadata = [ 'name' => $this->bloginfo_rss( 'name' ), 'url' => $this->bloginfo_rss( 'url' ), 'language' => $this->bloginfo_rss( 'language' ), 'description' => $this->bloginfo_rss( 'description' ), 'pubDate' => date( 'D, d M Y H:i:s +0000' ), // phpcs:ignore WordPress.DateTime.RestrictedFunctions.date_date 'site_url' => is_multisite() ? network_home_url() : $this->bloginfo_rss( 'url' ), 'blog_url' => $this->bloginfo_rss( 'url' ), ]; return $metadata; } public function wp_generator_tag() { // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- Calling native WordPress hook. return apply_filters( 'the_generator', get_the_generator( 'export' ), 'export' ); } public function authors() { global $wpdb; // If we're filtering by a specific author, we only need to include that // author's user object, and no other users. if ( is_object( $this->author ) && property_exists( $this->author, 'ID' ) ) { return [ $this->author ]; } $authors = []; $author_ids = (array) $wpdb->get_col( "SELECT DISTINCT post_author FROM $wpdb->posts WHERE post_status != 'auto-draft'" ); foreach ( $author_ids as $author_id ) { $authors[] = get_userdata( $author_id ); } $authors = array_filter( $authors ); return $authors; } public function categories() { if ( $this->category ) { return [ $this->category ]; } if ( $this->filters['post_type'] ) { return []; } $categories = (array) get_categories( [ 'get' => 'all' ] ); $this->check_for_orphaned_terms( $categories ); $categories = self::topologically_sort_terms( $categories ); return $categories; } public function tags() { if ( $this->filters['post_type'] ) { return []; } $tags = (array) get_tags( [ 'get' => 'all' ] ); $this->check_for_orphaned_terms( $tags ); return $tags; } public function custom_taxonomies_terms() { if ( $this->filters['post_type'] ) { return []; } $custom_taxonomies = get_taxonomies( [ '_builtin' => false ] ); $custom_terms = (array) get_terms( $custom_taxonomies, [ 'get' => 'all' ] ); $this->check_for_orphaned_terms( $custom_terms ); $custom_terms = self::topologically_sort_terms( $custom_terms ); return $custom_terms; } public function nav_menu_terms() { $nav_menus = wp_get_nav_menus(); foreach ( $nav_menus as $term ) { $term->description = ''; } return $nav_menus; } public function exportify_post( $post ) { $GLOBALS['wp_query']->in_the_loop = true; $previous_global_post = Utils\get_flag_value( $GLOBALS, 'post' ); // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited -- Temporary override. $GLOBALS['post'] = $post; setup_postdata( $post ); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- Calling native WordPress hook. $post->post_content = apply_filters( 'the_content_export', $post->post_content ); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- Calling native WordPress hook. $post->post_excerpt = apply_filters( 'the_excerpt_export', $post->post_excerpt ); $post->is_sticky = is_sticky( $post->ID ) ? 1 : 0; $post->terms = self::get_terms_for_post( $post ); $post->meta = self::get_meta_for_post( $post ); $post->comments = $this->get_comments_for_post( $post ); // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited -- Revert back to original. $GLOBALS['post'] = $previous_global_post; return $post; } public function posts() { $posts_iterator = new WP_Post_IDs_Iterator( $this->post_ids, self::QUERY_CHUNK ); return new WP_Map_Iterator( $posts_iterator, [ $this, 'exportify_post' ] ); } private function calculate_post_ids() { global $wpdb; if ( is_array( $this->filters['post_ids'] ) ) { if ( $this->filters['with_attachments'] ) { $attachment_post_ids = $this->include_attachment_ids( $this->filters['post_ids'] ); $this->filters['post_ids'] = array_merge( $this->filters['post_ids'], $attachment_post_ids ); } return $this->filters['post_ids']; } $this->post_type_where(); $this->status_where(); $this->author_where(); $this->start_date_where(); $this->end_date_where(); $this->start_id_where(); $this->category_where(); $where = implode( ' AND ', array_filter( $this->wheres ) ); if ( $where ) { $where = "WHERE $where"; } $join = implode( ' ', array_filter( $this->joins ) ); // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Individual where clauses run through $wpdb->prepare(). $post_ids = $wpdb->get_col( "SELECT ID FROM {$wpdb->posts} AS p {$join} {$where} {$this->max_num_posts()}" ); if ( $this->filters['post_type'] ) { $post_ids = array_merge( $post_ids, $this->include_attachment_ids( $post_ids ) ); } return $post_ids; } private function post_type_where() { $post_types_filters = [ 'can_export' => true ]; if ( $this->filters['post_type'] ) { $post_types = $this->filters['post_type']; // Flatten single post types if ( is_array( $post_types ) && 1 === count( $post_types ) ) { $post_types = array_shift( $post_types ); } $post_types_filters = array_merge( $post_types_filters, [ 'name' => $post_types ] ); } // Multiple post types if ( isset( $post_types_filters['name'] ) && is_array( $post_types_filters['name'] ) ) { $post_types = []; foreach ( $post_types_filters['name'] as $post_type ) { if ( post_type_exists( $post_type ) ) { $post_types[] = $post_type; } } } else { $post_types = get_post_types( $post_types_filters ); } if ( ! $post_types ) { $this->wheres[] = 'p.post_type IS NULL'; return; } if ( false === $this->filters['with_attachments'] && ( ! $this->filters['post_type'] || ! in_array( 'attachment', $this->filters['post_type'], true ) ) ) { unset( $post_types['attachment'] ); } $this->wheres[] = _wp_export_build_IN_condition( 'p.post_type', $post_types ); } private function status_where() { global $wpdb; if ( ! $this->filters['status'] ) { $this->wheres[] = "p.post_status != 'auto-draft'"; return; } $this->wheres[] = $wpdb->prepare( 'p.post_status = %s', $this->filters['status'] ); } private function author_where() { global $wpdb; if ( is_object( $this->author ) && property_exists( $this->author, 'ID' ) ) { $this->wheres[] = $wpdb->prepare( 'p.post_author = %d', $this->author->ID ); } } private function start_date_where() { global $wpdb; $timestamp = strtotime( $this->filters['start_date'] ); if ( ! $timestamp ) { return; } $this->wheres[] = $wpdb->prepare( 'p.post_date >= %s', date( 'Y-m-d 00:00:00', $timestamp ) ); // phpcs:ignore WordPress.DateTime.RestrictedFunctions.date_date } private function end_date_where() { global $wpdb; if ( preg_match( '/^\d{4}-\d{2}$/', $this->filters['end_date'] ) ) { $timestamp = $this->get_timestamp_for_the_last_day_of_a_month( $this->filters['end_date'] ); } else { $timestamp = strtotime( $this->filters['end_date'] ); } if ( ! $timestamp ) { return; } $this->wheres[] = $wpdb->prepare( 'p.post_date <= %s', date( 'Y-m-d 23:59:59', $timestamp ) ); // phpcs:ignore WordPress.DateTime.RestrictedFunctions.date_date } private function start_id_where() { global $wpdb; $start_id = absint( $this->filters['start_id'] ); if ( 0 === $start_id ) { return; } $this->wheres[] = $wpdb->prepare( 'p.ID >= %d', $start_id ); } private function get_timestamp_for_the_last_day_of_a_month( $yyyy_mm ) { return strtotime( "$yyyy_mm +1month -1day" ); } private function category_where() { global $wpdb; if ( 'post' !== $this->filters['post_type'] && ! in_array( 'post', (array) $this->filters['post_type'], true ) ) { return; } $category = $this->find_category_from_any_object( $this->filters['category'] ); if ( ! $category ) { return; } $this->category = $category; $this->joins[] = "INNER JOIN {$wpdb->term_relationships} AS tr ON (p.ID = tr.object_id)"; $this->wheres[] = $wpdb->prepare( 'tr.term_taxonomy_id = %d', $category->term_taxonomy_id ); } private function max_num_posts() { if ( $this->filters['max_num_posts'] > 0 ) { return "LIMIT {$this->filters['max_num_posts']}"; } else { return ''; } } private function include_attachment_ids( $post_ids ) { global $wpdb; if ( ! $post_ids ) { return []; } $attachment_ids = []; // phpcs:ignore WordPress.CodeAnalysis.AssignmentInCondition.FoundInWhileCondition -- Assigment is part of the break condition. while ( $batch_of_post_ids = array_splice( $post_ids, 0, self::QUERY_CHUNK ) ) { $post_parent_condition = _wp_export_build_IN_condition( 'post_parent', $batch_of_post_ids ); // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Escaped in wpcli_export_build_in_condition() function. $attachment_ids = array_merge( $attachment_ids, (array) $wpdb->get_col( "SELECT ID FROM {$wpdb->posts} WHERE post_type = 'attachment' AND {$post_parent_condition}" ) ); } return array_map( 'intval', $attachment_ids ); } private function bloginfo_rss( $section ) { // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- Calling native WordPress hook. return apply_filters( 'bloginfo_rss', get_bloginfo_rss( $section ), $section ); } private function find_user_from_any_object( $user ) { if ( is_numeric( $user ) ) { return get_user_by( 'id', $user ); } elseif ( is_string( $user ) ) { return get_user_by( 'login', $user ); } elseif ( isset( $user->ID ) ) { return get_user_by( 'id', $user->ID ); } return false; } private function find_category_from_any_object( $category ) { if ( is_numeric( $category ) ) { return get_term( $category, 'category' ); } elseif ( is_string( $category ) ) { $term = term_exists( $category, 'category' ); return isset( $term['term_id'] ) ? get_term( $term['term_id'], 'category' ) : false; } elseif ( isset( $category->term_id ) ) { return get_term( $category->term_id, 'category' ); } return false; } private static function topologically_sort_terms( $terms ) { $sorted = []; // phpcs:ignore WordPress.CodeAnalysis.AssignmentInCondition.FoundInWhileCondition -- assignment is used as break condition. while ( $term = array_shift( $terms ) ) { if ( 0 === (int) $term->parent || isset( $sorted[ $term->parent ] ) ) { $sorted[ $term->term_id ] = $term; } else { $terms[] = $term; } } return $sorted; } private function check_for_orphaned_terms( $terms ) { $term_ids = []; $have_parent = []; foreach ( $terms as $term ) { $term_ids[ $term->term_id ] = true; if ( 0 !== (int) $term->parent ) { $have_parent[] = $term; } } foreach ( $have_parent as $has_parent ) { if ( ! isset( $term_ids[ $has_parent->parent ] ) ) { $this->missing_parents = $has_parent; throw new WP_Export_Term_Exception( "Term is missing a parent: {$has_parent->slug} ({$has_parent->term_taxonomy_id})" ); } } } private static function get_terms_for_post( $post ) { $taxonomies = get_object_taxonomies( $post->post_type ); if ( empty( $taxonomies ) ) { return []; } return wp_get_object_terms( $post->ID, $taxonomies ) ?: []; } private static function get_meta_for_post( $post ) { global $wpdb; $meta_for_export = []; $meta_from_db = $wpdb->get_results( $wpdb->prepare( "SELECT * FROM $wpdb->postmeta WHERE post_id = %d", $post->ID ) ); foreach ( $meta_from_db as $meta ) { if ( '_edit_lock' === $meta->meta_key ) { continue; } // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- Calling native WordPress hook. if ( apply_filters( 'wxr_export_skip_postmeta', false, $meta->meta_key, $meta ) ) { continue; } $meta_for_export[] = $meta; } return $meta_for_export; } private function get_comments_for_post( $post ) { global $wpdb; if ( isset( $this->filters['skip_comments'] ) ) { return []; } $comments = $wpdb->get_results( $wpdb->prepare( "SELECT * FROM $wpdb->comments WHERE comment_post_ID = %d AND comment_approved <> 'spam'", $post->ID ) ); foreach ( $comments as $comment ) { $comment->meta = $wpdb->get_results( $wpdb->prepare( "SELECT * FROM $wpdb->commentmeta WHERE comment_id = %d", $comment->comment_ID ) ) ?: []; } return $comments; } } <?php class WP_Export_Returner extends WP_Export_Base_Writer { private $result = ''; public function export() { $this->private = ''; try { parent::export(); } catch ( WP_Export_Exception $e ) { // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- Possibly used by third party extension. $message = apply_filters( 'export_error_message', $e->getMessage() ); return new WP_Error( 'wp-export-error', $message ); } catch ( WP_Export_Term_Exception $e ) { // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- Possibly used by third party extensions. do_action( 'export_term_orphaned', $this->formatter->export->missing_parents ); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- Possibly used by third party extension. $message = apply_filters( 'export_term_error_message', $e->getMessage() ); return new WP_Error( 'wp-export-error', $message ); } return $this->result; } protected function write( $xml ) { $this->result .= $xml; } } <?php class WP_Export_Term_Exception extends RuntimeException { } <?php class WP_Export_Split_Files_Writer extends WP_Export_Base_Writer { private $max_file_size; private $destination_directory; private $filename_template; private $before_posts_xml; private $after_posts_xml; private $f; private $next_file_number = 0; private $current_file_size = 0; public function __construct( $formatter, $writer_args = [] ) { parent::__construct( $formatter ); if ( ! defined( 'MB_IN_BYTES' ) ) { // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedConstantFound -- WordPress native constants. define( 'MB_IN_BYTES', 1024 * 1024 ); } //TODO: check if args are not missing if ( is_null( $writer_args['max_file_size'] ) ) { $this->max_file_size = 15 * MB_IN_BYTES; } elseif ( WP_CLI_EXPORT_COMMAND_NO_SPLIT === (string) $writer_args['max_file_size'] ) { $this->max_file_size = WP_CLI_EXPORT_COMMAND_NO_SPLIT; } else { $this->max_file_size = $writer_args['max_file_size'] * MB_IN_BYTES; } $this->destination_directory = $writer_args['destination_directory']; $this->filename_template = $writer_args['filename_template']; $this->before_posts_xml = $this->formatter->before_posts(); $this->after_posts_xml = $this->formatter->after_posts(); } public function export() { $this->start_new_file(); foreach ( $this->formatter->posts() as $post_xml ) { if ( WP_CLI_EXPORT_COMMAND_NO_SPLIT !== $this->max_file_size && $this->current_file_size + strlen( $post_xml ) > $this->max_file_size ) { $this->start_new_file(); } $this->write( $post_xml ); } $this->close_current_file(); } protected function write( $xml ) { $res = fwrite( $this->f, $xml ); if ( false === $res ) { throw new WP_Export_Exception( 'WP Export: error writing to export file.' ); } $this->current_file_size += strlen( $xml ); } private function start_new_file() { if ( $this->f ) { $this->close_current_file(); } $file_path = $this->next_file_path(); $this->f = fopen( $file_path, 'w' ); if ( ! $this->f ) { throw new WP_Export_Exception( "WP Export: error opening {$file_path} for writing." ); } // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- Possibly used by third party extension. do_action( 'wp_export_new_file', $file_path ); $this->current_file_size = 0; $this->write( $this->before_posts_xml ); } private function close_current_file() { if ( ! $this->f ) { return; } $this->write( $this->after_posts_xml ); fclose( $this->f ); } private function next_file_name() { $next_file_name = sprintf( $this->filename_template, $this->next_file_number ); $this->next_file_number++; return $next_file_name; } private function next_file_path() { return untrailingslashit( $this->destination_directory ) . DIRECTORY_SEPARATOR . $this->next_file_name(); } } <?php class WP_Export_File_Writer extends WP_Export_Base_Writer { private $f; private $file_name; public function __construct( $formatter, $file_name ) { parent::__construct( $formatter ); $this->file_name = $file_name; } public function export() { $this->f = fopen( $this->file_name, 'w' ); if ( ! $this->f ) { throw new WP_Export_Exception( "WP Export: error opening {$this->file_name} for writing." ); } try { parent::export(); } catch ( WP_Export_Exception $e ) { fclose( $this->f ); throw $e; } catch ( WP_Export_Term_Exception $e ) { fclose( $this->f ); throw $e; } fclose( $this->f ); } protected function write( $xml ) { $res = fwrite( $this->f, $xml ); if ( false === $res ) { throw new WP_Export_Exception( 'WP Export: error writing to export file.' ); } } } <?php class WP_Export_Oxymel extends Oxymel { public function optional( $tag_name, $contents ) { if ( $contents ) { $this->$tag_name( $contents ); } return $this; } public function optional_cdata( $tag_name, $contents ) { if ( $contents ) { $this->$tag_name->contains->cdata( $contents )->end; } return $this; } public function cdata( $text ) { if ( is_string( $text ) && ! seems_utf8( $text ) ) { $text = utf8_encode( $text ); } return parent::cdata( $text ); } } <?php abstract class WP_Export_Base_Writer { protected $formatter; public function __construct( $formatter ) { $this->formatter = $formatter; } public function export() { $this->write( $this->formatter->before_posts() ); foreach ( $this->formatter->posts() as $post_in_wxr ) { $this->write( $post_in_wxr ); } $this->write( $this->formatter->after_posts() ); } abstract protected function write( $xml ); } <?php /** * Version number for the export format. * * Bump this when something changes that might affect compatibility. * * @since 2.5.0 */ define( 'WXR_VERSION', '1.2' ); //phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedConstantFound -- WordPress constant. /** * Responsible for formatting the data in WP_Export_Query to WXR */ class WP_Export_WXR_Formatter { public function __construct( $export ) { $this->export = $export; $this->wxr_version = WXR_VERSION; } public function before_posts( $requested_sections = [] ) { $available_sections = [ 'header', 'site_metadata', 'authors', 'categories', 'tags', 'nav_menu_terms', 'custom_taxonomies_terms', 'rss2_head_action', ]; $before_posts_xml = ''; foreach ( $available_sections as $section ) { if ( ! $requested_sections || in_array( $section, $requested_sections, true ) ) { $before_posts_xml .= $this->$section(); } } return $before_posts_xml; } public function posts() { return new WP_Map_Iterator( $this->export->posts(), [ $this, 'post' ] ); } public function after_posts() { return $this->footer(); } public function header() { $oxymel = new Oxymel(); $wp_generator_tag = $this->export->wp_generator_tag(); $comment = <<<COMMENT This is a WordPress eXtended RSS file generated by WordPress as an export of your site. It contains information about your site's posts, pages, comments, categories, and other content. You may use this file to transfer that content from one site to another. This file is not intended to serve as a complete backup of your site. To import this information into a WordPress site follow these steps: 1. Log in to that site as an administrator. 2. Go to Tools: Import in the WordPress admin panel. 3. Install the "WordPress" importer from the list. 4. Activate & Run Importer. 5. Upload this file using the form provided on that page. 6. You will first be asked to map the authors in this export file to users on the site. For each author, you may choose to map to an existing user on the site or to create a new user. 7. WordPress will then import each of the posts, pages, comments, categories, etc. contained in this file into your site. COMMENT; return $oxymel ->xml ->comment( $comment ) ->raw( $wp_generator_tag ) ->open_rss( [ 'version' => '2.0', 'xmlns:excerpt' => "http://wordpress.org/export/{$this->wxr_version}/excerpt/", 'xmlns:content' => 'http://purl.org/rss/1.0/modules/content/', 'xmlns:wfw' => 'http://wellformedweb.org/CommentAPI/', 'xmlns:dc' => 'http://purl.org/dc/elements/1.1/', 'xmlns:wp' => "http://wordpress.org/export/{$this->wxr_version}/", ] ) ->open_channel ->to_string(); } public function site_metadata() { $oxymel = new Oxymel(); $metadata = $this->export->site_metadata(); return $oxymel ->title( $metadata['name'] ) ->link( $metadata['url'] ) ->description( $metadata['description'] ) ->pubDate( $metadata['pubDate'] ) ->language( $metadata['language'] ) ->tag( 'wp:wxr_version', $this->wxr_version ) ->tag( 'wp:base_site_url', $metadata['site_url'] ) ->tag( 'wp:base_blog_url', $metadata['blog_url'] ) ->to_string(); } public function authors() { $oxymel = new Oxymel(); $authors = $this->export->authors(); foreach ( $authors as $author ) { $oxymel ->tag( 'wp:author' )->contains ->tag( 'wp:author_id', $author->ID ) ->tag( 'wp:author_login', $author->user_login ) ->tag( 'wp:author_email', $author->user_email ) ->tag( 'wp:author_display_name' )->contains->cdata( $author->display_name )->end ->tag( 'wp:author_first_name' )->contains->cdata( $author->user_firstname )->end ->tag( 'wp:author_last_name' )->contains->cdata( $author->user_lastname )->end ->end; } return $oxymel->to_string(); } public function categories() { $oxymel = new WP_Export_Oxymel(); $categories = $this->export->categories(); foreach ( $categories as $category ) { $category->parent_slug = $category->parent ? $categories[ $category->parent ]->slug : ''; $oxymel->tag( 'wp:category' )->contains ->tag( 'wp:term_id', $category->term_id ) ->tag( 'wp:category_nicename', $category->slug ) ->tag( 'wp:category_parent', $category->parent_slug ) ->optional_cdata( 'wp:cat_name', $category->name ) ->optional_cdata( 'wp:category_description', $category->description ) ->end; } return $oxymel->to_string(); } public function tags() { $oxymel = new WP_Export_Oxymel(); $tags = $this->export->tags(); foreach ( $tags as $tag ) { $oxymel->tag( 'wp:tag' )->contains ->tag( 'wp:term_id', $tag->term_id ) ->tag( 'wp:tag_slug', $tag->slug ) ->optional_cdata( 'wp:tag_name', $tag->name ) ->optional_cdata( 'wp:tag_description', $tag->description ) ->end; } return $oxymel->to_string(); } public function nav_menu_terms() { return $this->terms( $this->export->nav_menu_terms() ); } public function custom_taxonomies_terms() { return $this->terms( $this->export->custom_taxonomies_terms() ); } public function rss2_head_action() { ob_start(); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- WordPress native hook. do_action( 'rss2_head' ); return ob_get_clean(); } public function post( $post ) { $oxymel = new WP_Export_Oxymel(); $GLOBALS['wp_query']->in_the_loop = true; // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited -- Intentional. $GLOBALS['post'] = $post; setup_postdata( $post ); $oxymel->item->contains ->tag( 'title' )->contains->cdata( apply_filters( 'the_title_export', $post->post_title ) )->end // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- WordPress native hook. ->link( esc_url( apply_filters( 'the_permalink_rss', get_permalink() ) ) ) // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- WordPress native hook. ->pubDate( mysql2date( 'D, d M Y H:i:s +0000', get_post_time( 'Y-m-d H:i:s', true ), false ) ) ->tag( 'dc:creator', get_the_author_meta( 'login' ) ) ->guid( esc_url( get_the_guid() ), [ 'isPermaLink' => 'false' ] ) ->description( '' ) ->tag( 'content:encoded' )->contains->cdata( $post->post_content )->end ->tag( 'excerpt:encoded' )->contains->cdata( $post->post_excerpt )->end ->tag( 'wp:post_id', $post->ID ) ->tag( 'wp:post_date', $post->post_date ) ->tag( 'wp:post_date_gmt', $post->post_date_gmt ) ->tag( 'wp:post_modified', $post->post_modified ) ->tag( 'wp:post_modified_gmt', $post->post_modified_gmt ) ->tag( 'wp:comment_status', $post->comment_status ) ->tag( 'wp:ping_status', $post->ping_status ) ->tag( 'wp:post_name', $post->post_name ) ->tag( 'wp:status', $post->post_status ) ->tag( 'wp:post_parent', $post->post_parent ) ->tag( 'wp:menu_order', $post->menu_order ) ->tag( 'wp:post_type', $post->post_type ) ->tag( 'wp:post_password', $post->post_password ) ->tag( 'wp:is_sticky', $post->is_sticky ) ->optional( 'wp:attachment_url', wp_get_attachment_url( $post->ID ) ); foreach ( $post->terms as $term ) { $oxymel ->category( [ 'domain' => $term->taxonomy, 'nicename' => $term->slug, ] )->contains->cdata( $term->name )->end; } foreach ( $post->meta as $meta ) { $oxymel ->tag( 'wp:postmeta' )->contains ->tag( 'wp:meta_key', $meta->meta_key ) ->tag( 'wp:meta_value' )->contains->cdata( $meta->meta_value )->end ->end; } foreach ( $post->comments as $comment ) { $oxymel ->tag( 'wp:comment' )->contains ->tag( 'wp:comment_id', $comment->comment_ID ) ->tag( 'wp:comment_author' )->contains->cdata( $comment->comment_author )->end ->tag( 'wp:comment_author_email', $comment->comment_author_email ) ->tag( 'wp:comment_author_url', esc_url( $comment->comment_author_url ) ) ->tag( 'wp:comment_author_IP', $comment->comment_author_IP ) ->tag( 'wp:comment_date', $comment->comment_date ) ->tag( 'wp:comment_date_gmt', $comment->comment_date_gmt ) ->tag( 'wp:comment_content' )->contains->cdata( $comment->comment_content )->end ->tag( 'wp:comment_approved', $comment->comment_approved ) ->tag( 'wp:comment_type', $comment->comment_type ) ->tag( 'wp:comment_parent', $comment->comment_parent ) ->tag( 'wp:comment_user_id', $comment->user_id ) ->oxymel( $this->comment_meta( $comment ) ) ->end; } $oxymel ->end; return $oxymel->to_string(); } public function footer() { $oxymel = new Oxymel(); return $oxymel->close_channel->close_rss->to_string(); } protected function terms( $terms ) { $oxymel = new WP_Export_Oxymel(); foreach ( $terms as $term ) { $term->parent_slug = $term->parent ? $terms[ $term->parent ]->slug : ''; $oxymel->tag( 'wp:term' )->contains ->tag( 'wp:term_id', $term->term_id ) ->tag( 'wp:term_taxonomy', $term->taxonomy ) ->tag( 'wp:term_slug', $term->slug ); if ( 'nav_menu' !== $term->taxonomy ) { $oxymel ->tag( 'wp:term_parent', $term->parent_slug ); } $oxymel ->optional_cdata( 'wp:term_name', $term->name ) ->optional_cdata( 'wp:term_description', $term->description ) ->end; } return $oxymel->to_string(); } protected function comment_meta( $comment ) { global $wpdb; $metas = $wpdb->get_results( $wpdb->prepare( "SELECT * FROM $wpdb->commentmeta WHERE comment_id = %d", $comment->comment_ID ) ); if ( ! $metas ) { return new Oxymel(); } $oxymel = new WP_Export_Oxymel(); foreach ( $metas as $meta ) { $oxymel->tag( 'wp:commentmeta' )->contains ->tag( 'wp:meta_key', $meta->meta_key ) ->tag( 'wp:meta_value' )->contains->cdata( $meta->meta_value )->end ->end; } return $oxymel; } } <?php class WP_Iterator_Exception extends Exception { } <?php class WP_Post_IDs_Iterator implements Iterator { private $limit = 100; private $post_ids; private $ids_left; private $results = array(); public function __construct( $post_ids, $limit = null ) { $this->db = $GLOBALS['wpdb']; $this->post_ids = $post_ids; $this->ids_left = $post_ids; if ( ! is_null( $limit ) ) { $this->limit = $limit; } } public function current() { return $this->results[ $this->index_in_results ]; } public function key() { return $this->global_index; } public function next() { $this->index_in_results++; $this->global_index++; } public function rewind() { $this->results = array(); $this->global_index = 0; $this->index_in_results = 0; $this->ids_left = $this->post_ids; } public function valid() { if ( isset( $this->results[ $this->index_in_results ] ) ) { return true; } if ( empty( $this->ids_left ) ) { return false; } $has_posts = $this->load_next_posts_from_db(); if ( ! $has_posts ) { return false; } $this->index_in_results = 0; return true; } private function load_next_posts_from_db() { $next_batch_post_ids = array_splice( $this->ids_left, 0, $this->limit ); $in_post_ids_sql = _wp_export_build_IN_condition( 'ID', $next_batch_post_ids ); $this->results = $this->db->get_results( "SELECT * FROM {$this->db->posts} WHERE {$in_post_ids_sql}" ); if ( ! $this->results ) { if ( $this->db->last_error ) { throw new WP_Iterator_Exception( "Database error: {$this->db->last_error}" ); } else { return false; } } return true; } } <?php class WP_Export_Exception extends RuntimeException { } <?php class WP_Export_XML_Over_HTTP extends WP_Export_Base_Writer { private $file_name; public function __construct( $formatter, $file_name ) { parent::__construct( $formatter ); $this->file_name = $file_name; } public function export() { try { $export = $this->get_export(); $this->send_headers(); echo $export; } catch ( WP_Export_Exception $e ) { // phpcs:disable WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- Possibly used by third party extension. $message = apply_filters( 'export_error_message', $e->getMessage() ); wp_die( $message, __( 'Export Error' ), [ 'back_link' => true ] ); } catch ( WP_Export_Term_Exception $e ) { do_action( 'export_term_orphaned', $this->formatter->export->missing_parents ); $message = apply_filters( 'export_term_error_message', $e->getMessage() ); // phpcs:enable wp_die( $message, __( 'Export Error' ), [ 'back_link' => true ] ); } } protected function write( $xml ) { $this->result .= $xml; } protected function get_export() { $this->result = ''; parent::export(); return $this->result; } protected function send_headers() { header( 'Content-Description: File Transfer' ); header( 'Content-Disposition: attachment; filename=' . $this->file_name ); header( 'Content-Type: text/xml; charset=' . get_option( 'blog_charset' ), true ); } } <?php // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedFunctionFound -- Renaming breaks Phar compat. function wp_export( $args = array() ) { $defaults = array( 'filters' => array(), 'format' => 'WP_Export_WXR_Formatter', 'writer' => 'WP_Export_Returner', 'writer_args' => null, ); $args = wp_parse_args( $args, $defaults ); $export_query = new WP_Export_Query( $args['filters'] ); $formatter = new $args['format']( $export_query ); $writer = new $args['writer']( $formatter, $args['writer_args'] ); try { return $writer->export(); } catch ( WP_Export_Exception $e ) { return new WP_Error( 'wp-export-error', $e->getMessage() ); } } // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedFunctionFound,WordPress.NamingConventions.ValidFunctionName.FunctionNameInvalid -- Renaming breaks Phar compat. function _wp_export_build_IN_condition( $column_name, $values, $format = '%s' ) { global $wpdb; if ( ! is_array( $values ) || empty( $values ) ) { return ''; } $formats = implode( ', ', array_fill( 0, count( $values ), $format ) ); // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared,WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare -- $column_name_sql escaped as ident, $formats hardcoded value. return $wpdb->prepare( "{$column_name} IN ({$formats})", $values ); } <?php if ( ! class_exists( 'WP_CLI' ) ) { return; } $wpcli_export_autoloader = dirname( __FILE__ ) . '/vendor/autoload.php'; if ( file_exists( $wpcli_export_autoloader ) ) { require_once $wpcli_export_autoloader; } WP_CLI::add_command( 'export', 'Export_Command' ); <?php use WP_CLI\Utils; class Server_Command extends WP_CLI_Command { /** * Launches PHP's built-in web server for a specific WordPress installation. * * Uses `php -S` to launch a web server serving the WordPress webroot. * <http://php.net/manual/en/features.commandline.webserver.php> * * Importantly, PHP's built-in web server doesn't support `.htaccess` files. * If this is a requirement, please use a more advanced web server. * * ## OPTIONS * * [--host=<host>] * : The hostname to bind the server to. * --- * default: localhost * --- * * [--port=<port>] * : The port number to bind the server to. * --- * default: 8080 * --- * * [--docroot=<path>] * : The path to use as the document root. If the path global parameter is * set, the default value is it. * * [--config=<file>] * : Configure the server with a specific .ini file. * * ## EXAMPLES * * # Make the instance available on any address (with port 8080) * $ wp server --host=0.0.0.0 * PHP 5.6.9 Development Server started at Tue May 24 01:27:11 2016 * Listening on http://0.0.0.0:8080 * Document root is / * Press Ctrl-C to quit. * * # Run on port 80 (for multisite) * $ wp server --host=localhost.localdomain --port=80 * PHP 5.6.9 Development Server started at Tue May 24 01:30:06 2016 * Listening on http://localhost1.localdomain1:80 * Document root is / * Press Ctrl-C to quit. * * # Configure the server with a specific .ini file * $ wp server --config=development.ini * PHP 7.0.9 Development Server started at Mon Aug 22 12:09:04 2016 * Listening on http://localhost:8080 * Document root is / * Press Ctrl-C to quit. * * @when before_wp_load */ public function __invoke( $_, $assoc_args ) { $defaults = array( 'host' => 'localhost', 'port' => 8080, 'docroot' => ! is_null( WP_CLI::get_runner()->config['path'] ) ? WP_CLI::get_runner()->config['path'] : false, 'config' => get_cfg_var( 'cfg_file_path' ), ); $assoc_args = array_merge( $defaults, $assoc_args ); $docroot = $assoc_args['docroot']; if ( ! $docroot ) { $config_path = WP_CLI::get_runner()->project_config_path; if ( ! $config_path ) { $docroot = ABSPATH; } else { $docroot = dirname( $config_path ); } } // Get the path to the router file $command_root = Utils\phar_safe_path( dirname( __DIR__ ) ); $router_path = $command_root . '/router.php'; if ( ! file_exists( $router_path ) ) { WP_CLI::error( "Couldn't find router.php" ); } $cmd = Utils\esc_cmd( '%s -S %s -t %s -c %s %s', WP_CLI::get_php_binary(), $assoc_args['host'] . ':' . $assoc_args['port'], $docroot, $assoc_args['config'], Utils\extract_from_phar( $router_path ) ); $descriptors = array( STDIN, STDOUT, STDERR ); // https://bugs.php.net/bug.php?id=60181 $options = array(); if ( Utils\is_windows() ) { $options['bypass_shell'] = true; } exit( proc_close( proc_open( $cmd, $descriptors, $pipes, null, null, $options ) ) ); } } <?php // Used by `wp server` to route requests. namespace WP_CLI\Router; /** * This is a copy of WordPress's add_filter() function. * * We duplicate it because WordPress is not loaded yet. */ function add_filter( $tag, $function_to_add, $priority = 10, $accepted_args = 1 ) { global $wp_filter, $merged_filters; $idx = _wp_filter_build_unique_id( $tag, $function_to_add, $priority ); // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited $wp_filter[ $tag ][ $priority ][ $idx ] = array( 'function' => $function_to_add, 'accepted_args' => $accepted_args, ); unset( $merged_filters[ $tag ] ); return true; } /** * This is a copy of WordPress's _wp_filter_build_unique_id() function. * * We duplicate it because WordPress is not loaded yet. */ function _wp_filter_build_unique_id( $tag, $function, $priority ) { global $wp_filter; static $filter_id_count = 0; if ( is_string( $function ) ) { return $function; } if ( is_object( $function ) ) { // Closures are currently implemented as objects $function = array( $function, '' ); } else { $function = (array) $function; } if ( is_object( $function[0] ) ) { // Object Class Calling if ( function_exists( 'spl_object_hash' ) ) { return spl_object_hash( $function[0] ) . $function[1]; } else { $obj_idx = get_class( $function[0] ) . $function[1]; if ( ! isset( $function[0]->wp_filter_id ) ) { if ( false === $priority ) { return false; } $obj_idx .= isset( $wp_filter[ $tag ][ $priority ] ) ? count( (array) $wp_filter[ $tag ][ $priority ] ) : $filter_id_count; $function[0]->wp_filter_id = $filter_id_count; ++$filter_id_count; } else { $obj_idx .= $function[0]->wp_filter_id; } return $obj_idx; } } elseif ( is_string( $function[0] ) ) { // Static Calling return $function[0] . '::' . $function[1]; } } function _get_full_host( $url ) { // phpcs:ignore WordPress.WP.AlternativeFunctions.parse_url_parse_url $parsed_url = parse_url( $url ); $host = $parsed_url['host']; if ( isset( $parsed_url['port'] ) && 80 !== $parsed_url['port'] ) { $host .= ':' . $parsed_url['port']; } return $host; } // We need to trick WordPress into using the URL set by `wp server`, especially on multisite. add_filter( 'option_home', function ( $url ) { $GLOBALS['wpcli_server_original_url'] = $url; return 'http://' . $_SERVER['HTTP_HOST']; }, 20 ); add_filter( 'option_siteurl', function ( $url ) { if ( ! isset( $GLOBALS['wpcli_server_original_url'] ) ) { get_option( 'home' ); // trigger the option_home filter } $home_url_host = _get_full_host( $GLOBALS['wpcli_server_original_url'] ); $site_url_host = _get_full_host( $url ); if ( $site_url_host === $home_url_host ) { $url = str_replace( $site_url_host, $_SERVER['HTTP_HOST'], $url ); } return $url; }, 20 ); $_SERVER['SERVER_ADDR'] = gethostbyname( $_SERVER['SERVER_NAME'] ); $wpcli_server_root = $_SERVER['DOCUMENT_ROOT']; // phpcs:ignore WordPress.WP.AlternativeFunctions.parse_url_parse_url $wpcli_server_path = '/' . ltrim( parse_url( urldecode( $_SERVER['REQUEST_URI'] ) )['path'], '/' ); if ( file_exists( $wpcli_server_root . $wpcli_server_path ) ) { if ( is_dir( $wpcli_server_root . $wpcli_server_path ) && substr( $wpcli_server_path, -1 ) !== '/' ) { header( "Location: $wpcli_server_path/" ); exit; } if ( strpos( $wpcli_server_path, '.php' ) !== false ) { chdir( dirname( $wpcli_server_root . $wpcli_server_path ) ); require_once $wpcli_server_root . $wpcli_server_path; } else { return false; } } else { chdir( $wpcli_server_root ); require_once 'index.php'; } <?php if ( ! class_exists( 'WP_CLI' ) ) { return; } $wpcli_server_autoloader = dirname( __FILE__ ) . '/vendor/autoload.php'; if ( file_exists( $wpcli_server_autoloader ) ) { require_once $wpcli_server_autoloader; } WP_CLI::add_command( 'server', 'Server_Command', array( 'before_invoke' => function() { $min_version = '5.4'; if ( version_compare( PHP_VERSION, $min_version, '<' ) ) { WP_CLI::error( "The `wp server` command requires PHP {$min_version} or newer." ); } }, ) ); <?php namespace WP_CLI; use WP_CLI; use WP_CLI_Command; /** * Base class for WP-CLI commands that deal with translations * * @package wp-cli */ abstract class CommandWithTranslation extends WP_CLI_Command { protected $obj_type; protected $obj_fields; /** * Callback to sort array by a 'language' key. */ protected function sort_translations_callback( $a, $b ) { return strnatcasecmp( $a['language'], $b['language'] ); } /** * Updates installed languages for the current object type. */ public function update( $args, $assoc_args ) { $updates = $this->get_translation_updates(); if ( empty( $updates ) ) { WP_CLI::success( 'Translations are up to date.' ); return; } if ( empty( $args ) ) { $args = array( null ); // Used for core. } $upgrader = 'WP_CLI\\LanguagePackUpgrader'; $results = array(); $num_to_update = 0; foreach ( $args as $slug ) { // Gets a list of all languages. $all_languages = $this->get_all_languages( $slug ); $updates_per_type = array(); // Formats the updates list. foreach ( $updates as $update ) { if ( null !== $slug && $update->slug !== $slug ) { continue; } $name = 'WordPress'; // Core. if ( 'plugin' === $update->type ) { $plugins = get_plugins( '/' . $update->slug ); $plugin_data = array_shift( $plugins ); $name = $plugin_data['Name']; } elseif ( 'theme' === $update->type ) { $theme_data = wp_get_theme( $update->slug ); $name = $theme_data['Name']; } // Gets the translation data. $translation = wp_list_filter( $all_languages, array( 'language' => $update->language ) ); $translation = (object) reset( $translation ); $update->Type = ucfirst( $update->type ); $update->Name = $name; $update->Version = $update->version; if ( isset( $translation->english_name ) ) { $update->Language = $translation->english_name; } if ( ! isset( $updates_per_type[ $update->type ] ) ) { $updates_per_type[ $update->type ] = array(); } $updates_per_type[ $update->type ][] = $update; } $obj_type = rtrim( $this->obj_type, 's' ); $available_updates = isset( $updates_per_type[ $obj_type ] ) ? $updates_per_type[ $obj_type ] : null; if ( ! is_array( $available_updates ) ) { continue; } $num_to_update += count( $available_updates ); if ( ! Utils\get_flag_value( $assoc_args, 'dry-run' ) ) { // Update translations. foreach ( $available_updates as $update ) { WP_CLI::line( "Updating '{$update->Language}' translation for {$update->Name} {$update->Version}..." ); $result = Utils\get_upgrader( $upgrader )->upgrade( $update ); $results[] = $result; } } } // Only preview which translations would be updated. if ( Utils\get_flag_value( $assoc_args, 'dry-run' ) ) { $update_count = count( $updates ); WP_CLI::line( sprintf( 'Found %d translation %s that would be processed:', $update_count, WP_CLI\Utils\pluralize( 'update', $update_count ) ) ); Utils\format_items( 'table', $updates, array( 'Type', 'Name', 'Version', 'Language' ) ); return; } $num_updated = count( array_filter( $results ) ); $line = sprintf( "Updated $num_updated/$num_to_update %s.", WP_CLI\Utils\pluralize( 'translation', $num_updated ) ); if ( $num_to_update === $num_updated ) { WP_CLI::success( $line ); } elseif ( $num_updated > 0 ) { WP_CLI::warning( $line ); } else { WP_CLI::error( $line ); } } /** * Get all updates available for all translations. * * @see wp_get_translation_updates() * * @return array */ protected function get_translation_updates() { $available = $this->get_installed_languages(); $func = function() use ( $available ) { return $available; }; switch ( $this->obj_type ) { case 'plugins': add_filter( 'plugins_update_check_locales', $func ); wp_clean_plugins_cache(); // Check for Plugin translation updates. wp_update_plugins(); remove_filter( 'plugins_update_check_locales', $func ); $transient = 'update_plugins'; break; case 'themes': add_filter( 'themes_update_check_locales', $func ); wp_clean_themes_cache(); // Check for Theme translation updates. wp_update_themes(); remove_filter( 'themes_update_check_locales', $func ); $transient = 'update_themes'; break; default: delete_site_transient( 'update_core' ); // Check for Core translation updates. wp_version_check(); $transient = 'update_core'; break; } $updates = array(); $transient = get_site_transient( $transient ); if ( empty( $transient->translations ) ) { return $updates; } foreach ( $transient->translations as $translation ) { $updates[] = (object) $translation; } return $updates; } /** * Download a language pack. * * @see wp_download_language_pack() * * @param string $download Language code to download. * @param string $slug Plugin or theme slug. Not used for core. * @return string|\WP_Error Returns the language code if successfully downloaded, or a WP_Error object on failure. */ protected function download_language_pack( $download, $slug = null ) { $translations = $this->get_all_languages( $slug ); $translation_to_load = null; foreach ( $translations as $translation ) { if ( $translation['language'] === $download ) { $translation_to_load = $translation; break; } } if ( ! $translation_to_load ) { return new \WP_Error( 'not_found', "Language '{$download}' not available." ); } $translation = (object) $translation; $translation->type = rtrim( $this->obj_type, 's' ); // Make sure caching in LanguagePackUpgrader works. if ( ! isset( $translation->slug ) ) { $translation->slug = $slug; } $upgrader = 'WP_CLI\\LanguagePackUpgrader'; $result = Utils\get_upgrader( $upgrader )->upgrade( $translation, array( 'clear_update_cache' => false ) ); if ( is_wp_error( $result ) ) { return $result; } if ( ! $result ) { return new \WP_Error( 'not_installed', "Could not install language '{$download}'." ); } return $translation->language; } /** * Return a list of installed languages. * * @param string $slug Optional. Plugin or theme slug. Defaults to 'default' for core. * * @return array */ protected function get_installed_languages( $slug = 'default' ) { $available = wp_get_installed_translations( $this->obj_type ); $available = ! empty( $available[ $slug ] ) ? array_keys( $available[ $slug ] ) : array(); $available[] = 'en_US'; return $available; } /** * Return a list of all languages. * * @param string $slug Optional. Plugin or theme slug. Not used for core. * * @return array */ protected function get_all_languages( $slug = null ) { require_once ABSPATH . '/wp-admin/includes/translation-install.php'; require ABSPATH . WPINC . '/version.php'; // Include an unmodified $wp_version $args = array( 'version' => $wp_version, ); if ( $slug ) { $args['slug'] = $slug; if ( 'plugins' === $this->obj_type ) { $plugins = get_plugins( '/' . $slug ); $plugin_data = array_shift( $plugins ); if ( isset( $plugin_data['Version'] ) ) { $args['version'] = $plugin_data['Version']; } } elseif ( 'themes' === $this->obj_type ) { $theme_data = wp_get_theme( $slug ); if ( isset( $theme_data['Version'] ) ) { $args['version'] = $theme_data['Version']; } } } $response = translations_api( $this->obj_type, $args ); if ( is_wp_error( $response ) ) { WP_CLI::error( $response ); } $translations = ! empty( $response['translations'] ) ? $response['translations'] : array(); $en_us = array( 'language' => 'en_US', 'english_name' => 'English (United States)', 'native_name' => 'English (United States)', 'updated' => '', ); $translations[] = $en_us; uasort( $translations, array( $this, 'sort_translations_callback' ) ); return $translations; } /** * Get Formatter object based on supplied parameters. * * @param array $assoc_args Parameters passed to command. Determines formatting. * @return Formatter */ protected function get_formatter( &$assoc_args ) { return new Formatter( $assoc_args, $this->obj_fields, $this->obj_type ); } } <?php namespace WP_CLI; use WP_CLI; /** * A Language Pack Upgrader class that caches the download, and uses cached if available * * @package wp-cli */ class LanguagePackUpgrader extends \Language_Pack_Upgrader { /** * Initialize the upgrade strings. * * Makes sure that the strings are always in English. */ public function upgrade_strings() { $switched_locale = function_exists( 'switch_to_locale' ) && switch_to_locale( 'en_US' ); parent::upgrade_strings(); if ( $switched_locale ) { restore_previous_locale(); } } /** * Initialize the generic strings. * * Makes sure that the strings are always in English. */ public function generic_strings() { $switched_locale = function_exists( 'switch_to_locale' ) && switch_to_locale( 'en_US' ); parent::generic_strings(); if ( $switched_locale ) { restore_previous_locale(); } } /** * Caches the download, and uses cached if available. * * @param string $package The URI of the package. If this is the full path to an * existing local file, it will be returned untouched. * @param bool $check_signatures Whether to validate file signatures. Default false. * @param array $hook_extra Extra arguments to pass to the filter hooks. Default empty array. * @return string|\WP_Error The full path to the downloaded package file, or a WP_Error object. */ public function download_package( $package, $check_signatures = false, $hook_extra = [] ) { /** * Filter whether to return the package. * * @since 3.7.0 * @since 5.5.0 Added the `$hook_extra` parameter. * * @param bool $reply Whether to bail without returning the package. Default is false. * @param string $package The package file name. * @param \WP_Upgrader $this The WP_Upgrader instance. * @param array $hook_extra Extra arguments passed to hooked filters. * * @phpcs:disable WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- Using WP native hook. */ $reply = apply_filters( 'upgrader_pre_download', false, $package, $this, $hook_extra ); // phpcs:enable if ( false !== $reply ) { return $reply; } // Check if package is a local or remote file. Bail if it's local. if ( ! preg_match( '!^(http|https|ftp)://!i', $package ) && file_exists( $package ) ) { return $package; } if ( empty( $package ) ) { return new \WP_Error( 'no_package', $this->strings['no_package'] ); } $language_update = $this->skin->language_update; $type = $language_update->type; $slug = empty( $language_update->slug ) ? 'default' : $language_update->slug; $updated = strtotime( $language_update->updated ); $version = $language_update->version; $language = $language_update->language; $ext = pathinfo( $package, PATHINFO_EXTENSION ); $temp = \WP_CLI\Utils\get_temp_dir() . uniqid( 'wp_' ) . '.' . $ext; $cache = WP_CLI::get_cache(); $cache_key = "translation/{$type}-{$slug}-{$version}-{$language}-{$updated}.{$ext}"; $cache_file = $cache->has( $cache_key ); if ( $cache_file ) { WP_CLI::log( "Using cached file '$cache_file'..." ); copy( $cache_file, $temp ); return $temp; } $this->skin->feedback( 'downloading_package', $package ); $temp = download_url( $package, 600 ); // 10 minutes ought to be enough for everybody. if ( is_wp_error( $temp ) ) { return $temp; } $cache->import( $cache_key, $temp ); return $temp; } } <?php /** * Installs, activates, and manages core language packs. * * ## EXAMPLES * * # Install the Dutch core language pack. * $ wp language core install nl_NL * Success: Language installed. * * # Activate the Dutch core language pack. * $ wp language core activate nl_NL * Success: Language activated. * * # Uninstall the Dutch core language pack. * $ wp language core uninstall nl_NL * Success: Language uninstalled. * * # List installed core language packages. * $ wp language core list --status=installed * +----------+--------------+-------------+-----------+-----------+---------------------+ * | language | english_name | native_name | status | update | updated | * +----------+--------------+-------------+-----------+-----------+---------------------+ * | nl_NL | Dutch | Nederlands | installed | available | 2016-05-13 08:12:50 | * +----------+--------------+-------------+-----------+-----------+---------------------+ */ class Core_Language_Command extends WP_CLI\CommandWithTranslation { protected $obj_type = 'core'; protected $obj_fields = array( 'language', 'english_name', 'native_name', 'status', 'update', 'updated', ); /** * Lists all available languages. * * ## OPTIONS * * [--field=<field>] * : Display the value of a single field * * [--<field>=<value>] * : Filter results by key=value pairs. * * [--fields=<fields>] * : Limit the output to specific fields. * * [--format=<format>] * : Render output in a particular format. * --- * default: table * options: * - table * - csv * - json * --- * * ## AVAILABLE FIELDS * * These fields will be displayed by default for each translation: * * * language * * english_name * * native_name * * status * * update * * updated * * ## EXAMPLES * * # List language,english_name,status fields of available languages. * $ wp language core list --fields=language,english_name,status * +----------------+-------------------------+-------------+ * | language | english_name | status | * +----------------+-------------------------+-------------+ * | ar | Arabic | uninstalled | * | ary | Moroccan Arabic | uninstalled | * | az | Azerbaijani | uninstalled | * * @subcommand list */ public function list_( $args, $assoc_args ) { $translations = $this->get_all_languages(); $available = $this->get_installed_languages(); $updates = $this->get_translation_updates(); $current_locale = get_locale(); $translations = array_map( function( $translation ) use ( $available, $current_locale, $updates ) { $translation['status'] = 'uninstalled'; if ( in_array( $translation['language'], $available, true ) ) { $translation['status'] = 'installed'; } if ( $current_locale === $translation['language'] ) { $translation['status'] = 'active'; } $update = wp_list_filter( $updates, array( 'language' => $translation['language'] ) ); if ( $update ) { $translation['update'] = 'available'; } else { $translation['update'] = 'none'; } return $translation; }, $translations ); foreach ( $translations as $key => $translation ) { foreach ( array_keys( $translation ) as $field ) { if ( isset( $assoc_args[ $field ] ) && $assoc_args[ $field ] !== $translation[ $field ] ) { unset( $translations[ $key ] ); } } } $formatter = $this->get_formatter( $assoc_args ); $formatter->display_items( $translations ); } /** * Checks if a given language is installed. * * Returns exit code 0 when installed, 1 when uninstalled. * * ## OPTIONS * * <language> * : The language code to check. * * ## EXAMPLES * * # Check whether the German language is installed; exit status 0 if installed, otherwise 1. * $ wp language core is-installed de_DE * $ echo $? * 1 * * @subcommand is-installed */ public function is_installed( $args, $assoc_args = array() ) { list( $language_code ) = $args; $available = $this->get_installed_languages(); if ( in_array( $language_code, $available, true ) ) { \WP_CLI::halt( 0 ); } else { \WP_CLI::halt( 1 ); } } /** * Installs a given language. * * Downloads the language pack from WordPress.org. * * ## OPTIONS * * <language>... * : Language code to install. * * [--activate] * : If set, the language will be activated immediately after install. * * ## EXAMPLES * * # Install the Japanese language. * $ wp language core install ja * Downloading translation from https://downloads.wordpress.org/translation/core/4.9.8/ja.zip... * Unpacking the update... * Installing the latest version... * Translation updated successfully. * Language 'ja' installed. * Success: Installed 1 of 1 languages. * * @subcommand install */ public function install( $args, $assoc_args ) { $language_codes = (array) $args; $count = count( $language_codes ); if ( $count > 1 && in_array( true, $assoc_args, true ) ) { WP_CLI::error( 'Only a single language can be active.' ); } $available = $this->get_installed_languages(); $successes = 0; $errors = 0; $skips = 0; foreach ( $language_codes as $language_code ) { if ( in_array( $language_code, $available, true ) ) { \WP_CLI::log( "Language '{$language_code}' already installed." ); $skips++; } else { $response = $this->download_language_pack( $language_code ); if ( is_wp_error( $response ) ) { \WP_CLI::warning( $response ); \WP_CLI::log( "Language '{$language_code}' not installed." ); // Skip if translation is not yet available. if ( 'not_found' === $response->get_error_code() ) { $skips++; } else { $errors++; } } else { \WP_CLI::log( "Language '{$language_code}' installed." ); $successes++; } } if ( WP_CLI\Utils\get_flag_value( $assoc_args, 'activate' ) ) { $this->activate_language( $language_code ); } } \WP_CLI\Utils\report_batch_operation_results( 'language', 'install', $count, $successes, $errors, $skips ); } /** * Uninstalls a given language. * * ## OPTIONS * * <language>... * : Language code to uninstall. * * ## EXAMPLES * * $ wp language core uninstall ja * Success: Language uninstalled. * * @subcommand uninstall * @throws WP_CLI\ExitException */ public function uninstall( $args, $assoc_args ) { global $wp_filesystem; $language_codes = (array) $args; $available = $this->get_installed_languages(); foreach ( $language_codes as $language_code ) { if ( ! in_array( $language_code, $available, true ) ) { WP_CLI::error( 'Language not installed.' ); } $dir = 'core' === $this->obj_type ? '' : "/$this->obj_type"; $files = scandir( WP_LANG_DIR . $dir ); if ( ! $files ) { WP_CLI::error( 'No files found in language directory.' ); } $current_locale = get_locale(); if ( $language_code === $current_locale ) { WP_CLI::warning( "The '{$language_code}' language is active." ); exit; } // As of WP 4.0, no API for deleting a language pack WP_Filesystem(); $deleted = false; foreach ( $files as $file ) { if ( '.' === $file[0] || is_dir( $file ) ) { continue; } $extension_length = strlen( $language_code ) + 4; $ending = substr( $file, -$extension_length ); if ( ! in_array( $file, array( $language_code . '.po', $language_code . '.mo' ), true ) && ! in_array( $ending, array( '-' . $language_code . '.po', '-' . $language_code . '.mo' ), true ) ) { continue; } /** @var WP_Filesystem_Base $wp_filesystem */ $deleted = $wp_filesystem->delete( WP_LANG_DIR . $dir . '/' . $file ); } if ( $deleted ) { WP_CLI::success( 'Language uninstalled.' ); } else { WP_CLI::error( "Couldn't uninstall language." ); } } } /** * Updates installed languages for core. * * ## OPTIONS * * [--dry-run] * : Preview which translations would be updated. * * ## EXAMPLES * * $ wp language core update * Updating 'Japanese' translation for WordPress 4.9.2... * Downloading translation from https://downloads.wordpress.org/translation/core/4.9.2/ja.zip... * Translation updated successfully. * Success: Updated 1/1 translation. * * @subcommand update */ public function update( $args, $assoc_args ) { // phpcs:ignore Generic.CodeAnalysis.UselessOverridingMethod.Found -- Overruling the documentation, so not useless ;-). parent::update( $args, $assoc_args ); } /** * Activates a given language. * * ## OPTIONS * * <language> * : Language code to activate. * * ## EXAMPLES * * $ wp language core activate ja * Success: Language activated. * * @subcommand activate * @throws WP_CLI\ExitException */ public function activate( $args, $assoc_args ) { \WP_CLI::warning( 'This command is deprecated. use wp site switch-language instead' ); list( $language_code ) = $args; $this->activate_language( $language_code ); } private function activate_language( $language_code ) { $available = $this->get_installed_languages(); if ( ! in_array( $language_code, $available, true ) ) { WP_CLI::error( 'Language not installed.' ); } if ( 'en_US' === $language_code ) { $language_code = ''; } if ( get_locale() === $language_code ) { WP_CLI::warning( "Language '{$language_code}' already active." ); return; } update_option( 'WPLANG', $language_code ); WP_CLI::success( 'Language activated.' ); } } <?php /** * Installs, activates, and manages plugin language packs. * * ## EXAMPLES * * # Install the Dutch plugin language pack. * $ wp language plugin install hello-dolly nl_NL * Success: Language installed. * * # Uninstall the Dutch plugin language pack. * $ wp language plugin uninstall hello-dolly nl_NL * Success: Language uninstalled. * * # List installed plugin language packages. * $ wp language plugin list --status=installed * +----------+--------------+-------------+-----------+-----------+---------------------+ * | language | english_name | native_name | status | update | updated | * +----------+--------------+-------------+-----------+-----------+---------------------+ * | nl_NL | Dutch | Nederlands | installed | available | 2016-05-13 08:12:50 | * +----------+--------------+-------------+-----------+-----------+---------------------+ */ class Plugin_Language_Command extends WP_CLI\CommandWithTranslation { protected $obj_type = 'plugins'; protected $obj_fields = array( 'plugin', 'language', 'english_name', 'native_name', 'status', 'update', 'updated', ); /** * Lists all available languages for one or more plugins. * * ## OPTIONS * * [<plugin>...] * : One or more plugins to list languages for. * * [--all] * : If set, available languages for all plugins will be listed. * * [--field=<field>] * : Display the value of a single field. * * [--<field>=<value>] * : Filter results by key=value pairs. * * [--fields=<fields>] * : Limit the output to specific fields. * * [--format=<format>] * : Render output in a particular format. * --- * default: table * options: * - table * - csv * - json * --- * * ## AVAILABLE FIELDS * * These fields will be displayed by default for each translation: * * * plugin * * language * * english_name * * native_name * * status * * update * * updated * * ## EXAMPLES * * # List language,english_name,status fields of available languages. * $ wp language plugin list --fields=language,english_name,status * +----------------+-------------------------+-------------+ * | language | english_name | status | * +----------------+-------------------------+-------------+ * | ar | Arabic | uninstalled | * | ary | Moroccan Arabic | uninstalled | * | az | Azerbaijani | uninstalled | * * @subcommand list */ public function list_( $args, $assoc_args ) { $all = \WP_CLI\Utils\get_flag_value( $assoc_args, 'all', false ); if ( ! $all && empty( $args ) ) { WP_CLI::error( 'Please specify one or more plugins, or use --all.' ); } if ( $all ) { $args = array_map( '\WP_CLI\Utils\get_plugin_name', array_keys( $this->get_all_plugins() ) ); if ( empty( $args ) ) { WP_CLI::success( 'No plugins installed.' ); return; } } $updates = $this->get_translation_updates(); $current_locale = get_locale(); $translations = array(); foreach ( $args as $plugin ) { $installed_translations = $this->get_installed_languages( $plugin ); $available_translations = $this->get_all_languages( $plugin ); foreach ( $available_translations as $translation ) { $translation['plugin'] = $plugin; $translation['status'] = in_array( $translation['language'], $installed_translations, true ) ? 'installed' : 'uninstalled'; if ( $current_locale === $translation['language'] ) { $translation['status'] = 'active'; } $filter_args = array( 'language' => $translation['language'], 'type' => 'plugin', 'slug' => $plugin, ); $update = wp_list_filter( $updates, $filter_args ); $translation['update'] = $update ? 'available' : 'none'; // Support features like --status=active. foreach ( array_keys( $translation ) as $field ) { if ( isset( $assoc_args[ $field ] ) && $assoc_args[ $field ] !== $translation[ $field ] ) { continue 2; } } $translations[] = $translation; } } $formatter = $this->get_formatter( $assoc_args ); $formatter->display_items( $translations ); } /** * Checks if a given language is installed. * * Returns exit code 0 when installed, 1 when uninstalled. * * ## OPTIONS * * <plugin> * : Plugin to check for. * * <language>... * : The language code to check. * * ## EXAMPLES * * # Check whether the German language is installed for Akismet; exit status 0 if installed, otherwise 1. * $ wp language plugin is-installed akismet de_DE * $ echo $? * 1 * * @subcommand is-installed */ public function is_installed( $args, $assoc_args = array() ) { $plugin = array_shift( $args ); $language_codes = (array) $args; $available = $this->get_installed_languages( $plugin ); foreach ( $language_codes as $language_code ) { if ( ! in_array( $language_code, $available, true ) ) { \WP_CLI::halt( 1 ); } } \WP_CLI::halt( 0 ); } /** * Installs a given language for a plugin. * * Downloads the language pack from WordPress.org. * * ## OPTIONS * * [<plugin>] * : Plugin to install language for. * * [--all] * : If set, languages for all plugins will be installed. * * <language>... * : Language code to install. * * [--format=<format>] * : Render output in a particular format. Used when installing languages for all plugins. * --- * default: table * options: * - table * - csv * - json * - summary * --- * * ## EXAMPLES * * # Install the Japanese language for Akismet. * $ wp language plugin install akismet ja * Downloading translation from https://downloads.wordpress.org/translation/plugin/akismet/4.0.3/ja.zip... * Unpacking the update... * Installing the latest version... * Translation updated successfully. * Language 'ja' installed. * Success: Installed 1 of 1 languages. * * @subcommand install */ public function install( $args, $assoc_args ) { $all = \WP_CLI\Utils\get_flag_value( $assoc_args, 'all', false ); if ( ! $all && count( $args ) < 2 ) { \WP_CLI::error( 'Please specify a plugin, or use --all.' ); } if ( $all ) { $this->install_many( $args, $assoc_args ); } else { $this->install_one( $args, $assoc_args ); } } /** * Installs translations for a plugin. * * @param array $args Runtime arguments. * @param array $assoc_args Runtime arguments. */ private function install_one( $args, $assoc_args ) { $plugin = array_shift( $args ); $language_codes = (array) $args; $count = count( $language_codes ); $available = $this->get_installed_languages( $plugin ); $successes = 0; $errors = 0; $skips = 0; foreach ( $language_codes as $language_code ) { if ( in_array( $language_code, $available, true ) ) { \WP_CLI::log( "Language '{$language_code}' already installed." ); $skips++; } else { $response = $this->download_language_pack( $language_code, $plugin ); if ( is_wp_error( $response ) ) { \WP_CLI::warning( $response ); \WP_CLI::log( "Language '{$language_code}' not installed." ); // Skip if translation is not yet available. if ( 'not_found' === $response->get_error_code() ) { $skips++; } else { $errors++; } } else { \WP_CLI::log( "Language '{$language_code}' installed." ); $successes++; } } } \WP_CLI\Utils\report_batch_operation_results( 'language', 'install', $count, $successes, $errors, $skips ); } /** * Installs translations for all installed plugins. * * @param array $args Runtime arguments. * @param array $assoc_args Runtime arguments. */ private function install_many( $args, $assoc_args ) { $language_codes = (array) $args; $plugins = $this->get_all_plugins(); if ( empty( $assoc_args['format'] ) ) { $assoc_args['format'] = 'table'; } if ( in_array( $assoc_args['format'], array( 'json', 'csv' ), true ) ) { $logger = new \WP_CLI\Loggers\Quiet(); \WP_CLI::set_logger( $logger ); } if ( empty( $plugins ) ) { \WP_CLI::success( 'No plugins installed.' ); return; } $count = count( $plugins ) * count( $language_codes ); $results = array(); $successes = 0; $errors = 0; $skips = 0; foreach ( $plugins as $plugin_path => $plugin_details ) { $plugin_name = \WP_CLI\Utils\get_plugin_name( $plugin_path ); $available = $this->get_installed_languages( $plugin_name ); foreach ( $language_codes as $language_code ) { $result = [ 'name' => $plugin_name, 'locale' => $language_code, ]; if ( in_array( $language_code, $available, true ) ) { \WP_CLI::log( "Language '{$language_code}' for '{$plugin_details['Name']}' already installed." ); $result['status'] = 'already installed'; $skips++; } else { $response = $this->download_language_pack( $language_code, $plugin_name ); if ( is_wp_error( $response ) ) { \WP_CLI::warning( $response ); \WP_CLI::log( "Language '{$language_code}' for '{$plugin_details['Name']}' not installed." ); if ( 'not_found' === $response->get_error_code() ) { $result['status'] = 'not available'; $skips++; } else { $result['status'] = 'not installed'; $errors++; } } else { \WP_CLI::log( "Language '{$language_code}' for '{$plugin_details['Name']}' installed." ); $result['status'] = 'installed'; $successes++; } } $results[] = (object) $result; } } if ( 'summary' !== $assoc_args['format'] ) { \WP_CLI\Utils\format_items( $assoc_args['format'], $results, array( 'name', 'locale', 'status' ) ); } \WP_CLI\Utils\report_batch_operation_results( 'language', 'install', $count, $successes, $errors, $skips ); } /** * Uninstalls a given language for a plugin. * * ## OPTIONS * * <plugin> * : Plugin to uninstall language for. * * <language>... * : Language code to uninstall. * * ## EXAMPLES * * $ wp language plugin uninstall hello-dolly ja * Success: Language uninstalled. * * @subcommand uninstall */ public function uninstall( $args, $assoc_args ) { /** @var WP_Filesystem_Base $wp_filesystem */ global $wp_filesystem; $plugin = array_shift( $args ); $language_codes = (array) $args; $current_locale = get_locale(); $dir = WP_LANG_DIR . "/$this->obj_type"; $files = scandir( $dir ); if ( ! $files ) { \WP_CLI::error( 'No files found in language directory.' ); } // As of WP 4.0, no API for deleting a language pack WP_Filesystem(); $available = $this->get_installed_languages( $plugin ); foreach ( $language_codes as $language_code ) { if ( ! in_array( $language_code, $available, true ) ) { \WP_CLI::error( 'Language not installed.' ); } if ( $language_code === $current_locale ) { \WP_CLI::warning( "The '{$language_code}' language is active." ); exit; } if ( $wp_filesystem->delete( "{$dir}/{$plugin}-{$language_code}.po" ) && $wp_filesystem->delete( "{$dir}/{$plugin}-{$language_code}.mo" ) ) { \WP_CLI::success( 'Language uninstalled.' ); } else { \WP_CLI::error( "Couldn't uninstall language." ); } } } /** * Updates installed languages for one or more plugins. * * ## OPTIONS * * [<plugin>...] * : One or more plugins to update languages for. * * [--all] * : If set, languages for all plugins will be updated. * * [--dry-run] * : Preview which translations would be updated. * * ## EXAMPLES * * $ wp language plugin update --all * Updating 'Japanese' translation for Akismet 3.1.11... * Downloading translation from https://downloads.wordpress.org/translation/plugin/akismet/3.1.11/ja.zip... * Translation updated successfully. * Success: Updated 1/1 translation. * * @subcommand update */ public function update( $args, $assoc_args ) { $all = \WP_CLI\Utils\get_flag_value( $assoc_args, 'all', false ); if ( ! $all && empty( $args ) ) { WP_CLI::error( 'Please specify one or more plugins, or use --all.' ); } if ( $all ) { $args = array_map( '\WP_CLI\Utils\get_plugin_name', array_keys( $this->get_all_plugins() ) ); if ( empty( $args ) ) { WP_CLI::success( 'No plugins installed.' ); return; } } parent::update( $args, $assoc_args ); } /** * Gets all available plugins. * * Uses the same filter core uses in plugins.php to determine which plugins * should be available to manage through the WP_Plugins_List_Table class. * * @return array */ private function get_all_plugins() { // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- Using WP native hook. return apply_filters( 'all_plugins', get_plugins() ); } } <?php class Site_Switch_Language_Command extends WP_CLI\CommandWithTranslation { protected $obj_type = 'core'; /** * Activates a given language. * * ## OPTIONS * * <language> * : Language code to activate. * * ## EXAMPLES * * $ wp site switch-language ja * Success: Language activated. * * @throws WP_CLI\ExitException */ public function __invoke( $args, $assoc_args ) { list( $language_code ) = $args; $available = $this->get_installed_languages(); if ( ! in_array( $language_code, $available, true ) ) { WP_CLI::error( 'Language not installed.' ); } if ( 'en_US' === $language_code ) { $language_code = ''; } if ( get_locale() === $language_code ) { WP_CLI::warning( "Language '{$language_code}' already active." ); return; } update_option( 'WPLANG', $language_code ); WP_CLI::success( 'Language activated.' ); } } <?php /** * Installs, activates, and manages theme language packs. * * ## EXAMPLES * * # Install the Dutch theme language pack. * $ wp language theme install twentyten nl_NL * Success: Language installed. * * # Uninstall the Dutch theme language pack. * $ wp language theme uninstall twentyten nl_NL * Success: Language uninstalled. * * # List installed theme language packages. * $ wp language theme list --status=installed * +----------+--------------+-------------+-----------+-----------+---------------------+ * | language | english_name | native_name | status | update | updated | * +----------+--------------+-------------+-----------+-----------+---------------------+ * | nl_NL | Dutch | Nederlands | installed | available | 2016-05-13 08:12:50 | * +----------+--------------+-------------+-----------+-----------+---------------------+ */ class Theme_Language_Command extends WP_CLI\CommandWithTranslation { protected $obj_type = 'themes'; protected $obj_fields = array( 'theme', 'language', 'english_name', 'native_name', 'status', 'update', 'updated', ); /** * Lists all available languages for one or more themes. * * ## OPTIONS * * [<theme>...] * : One or more themes to list languages for. * * [--all] * : If set, available languages for all themes will be listed. * * [--field=<field>] * : Display the value of a single field. * * [--<field>=<value>] * : Filter results by key=value pairs. * * [--fields=<fields>] * : Limit the output to specific fields. * * [--format=<format>] * : Render output in a particular format. * --- * default: table * options: * - table * - csv * - json * --- * * ## AVAILABLE FIELDS * * These fields will be displayed by default for each translation: * * * theme * * language * * english_name * * native_name * * status * * update * * updated * * ## EXAMPLES * * # List language,english_name,status fields of available languages. * $ wp language theme list --fields=language,english_name,status * +----------------+-------------------------+-------------+ * | language | english_name | status | * +----------------+-------------------------+-------------+ * | ar | Arabic | uninstalled | * | ary | Moroccan Arabic | uninstalled | * | az | Azerbaijani | uninstalled | * * @subcommand list */ public function list_( $args, $assoc_args ) { $all = \WP_CLI\Utils\get_flag_value( $assoc_args, 'all', false ); if ( ! $all && empty( $args ) ) { WP_CLI::error( 'Please specify one or more themes, or use --all.' ); } if ( $all ) { $args = array_map( function( $file ) { return \WP_CLI\Utils\get_theme_name( $file ); }, array_keys( wp_get_themes() ) ); if ( empty( $args ) ) { WP_CLI::success( 'No themes installed.' ); return; } } $updates = $this->get_translation_updates(); $current_locale = get_locale(); $translations = array(); foreach ( $args as $theme ) { $installed_translations = $this->get_installed_languages( $theme ); $available_translations = $this->get_all_languages( $theme ); foreach ( $available_translations as $translation ) { $translation['theme'] = $theme; $translation['status'] = in_array( $translation['language'], $installed_translations, true ) ? 'installed' : 'uninstalled'; if ( $current_locale === $translation['language'] ) { $translation['status'] = 'active'; } $filter_args = array( 'language' => $translation['language'], 'type' => 'theme', 'slug' => $theme, ); $update = wp_list_filter( $updates, $filter_args ); $translation['update'] = $update ? 'available' : 'none'; // Support features like --status=active. foreach ( array_keys( $translation ) as $field ) { if ( isset( $assoc_args[ $field ] ) && $assoc_args[ $field ] !== $translation[ $field ] ) { continue 2; } } $translations[] = $translation; } } $formatter = $this->get_formatter( $assoc_args ); $formatter->display_items( $translations ); } /** * Checks if a given language is installed. * * Returns exit code 0 when installed, 1 when uninstalled. * * ## OPTIONS * * <theme> * : Theme to check for. * * <language>... * : The language code to check. * * ## EXAMPLES * * # Check whether the German language is installed for Twenty Seventeen; exit status 0 if installed, otherwise 1. * $ wp language theme is-installed twentyseventeen de_DE * $ echo $? * 1 * * @subcommand is-installed */ public function is_installed( $args, $assoc_args = array() ) { $theme = array_shift( $args ); $language_codes = (array) $args; $available = $this->get_installed_languages( $theme ); foreach ( $language_codes as $language_code ) { if ( ! in_array( $language_code, $available, true ) ) { \WP_CLI::halt( 1 ); } } \WP_CLI::halt( 0 ); } /** * Installs a given language for a theme. * * Downloads the language pack from WordPress.org. * * ## OPTIONS * * [<theme>] * : Theme to install language for. * * [--all] * : If set, languages for all themes will be installed. * * <language>... * : Language code to install. * * [--format=<format>] * : Render output in a particular format. Used when installing languages for all themes. * --- * default: table * options: * - table * - csv * - json * - summary * --- * * ## EXAMPLES * * # Install the Japanese language for Twenty Seventeen. * $ wp language theme install twentyseventeen ja * Downloading translation from https://downloads.wordpress.org/translation/theme/twentyseventeen/1.3/ja.zip... * Unpacking the update... * Installing the latest version... * Translation updated successfully. * Language 'ja' installed. * Success: Installed 1 of 1 languages. * * @subcommand install */ public function install( $args, $assoc_args ) { $all = \WP_CLI\Utils\get_flag_value( $assoc_args, 'all', false ); if ( ! $all && count( $args ) < 2 ) { \WP_CLI::error( 'Please specify a theme, or use --all.' ); } if ( $all ) { $this->install_many( $args, $assoc_args ); } else { $this->install_one( $args, $assoc_args ); } } /** * Installs translations for a theme. * * @param array $args Runtime arguments. * @param array $assoc_args Runtime arguments. */ private function install_one( $args, $assoc_args ) { $theme = array_shift( $args ); $language_codes = (array) $args; $count = count( $language_codes ); $available = $this->get_installed_languages( $theme ); $successes = 0; $errors = 0; $skips = 0; foreach ( $language_codes as $language_code ) { if ( in_array( $language_code, $available, true ) ) { \WP_CLI::log( "Language '{$language_code}' already installed." ); $skips++; } else { $response = $this->download_language_pack( $language_code, $theme ); if ( is_wp_error( $response ) ) { \WP_CLI::warning( $response ); \WP_CLI::log( "Language '{$language_code}' not installed." ); // Skip if translation is not yet available. if ( 'not_found' === $response->get_error_code() ) { $skips++; } else { $errors++; } } else { \WP_CLI::log( "Language '{$language_code}' installed." ); $successes++; } } } \WP_CLI\Utils\report_batch_operation_results( 'language', 'install', $count, $successes, $errors, $skips ); } /** * Installs translations for all installed themes. * * @param array $args Runtime arguments. * @param array $assoc_args Runtime arguments. */ private function install_many( $args, $assoc_args ) { $language_codes = (array) $args; $themes = wp_get_themes(); if ( empty( $assoc_args['format'] ) ) { $assoc_args['format'] = 'table'; } if ( in_array( $assoc_args['format'], array( 'json', 'csv' ), true ) ) { $logger = new \WP_CLI\Loggers\Quiet(); \WP_CLI::set_logger( $logger ); } if ( empty( $themes ) ) { \WP_CLI::success( 'No themes installed.' ); return; } $count = count( $themes ) * count( $language_codes ); $results = array(); $successes = 0; $errors = 0; $skips = 0; foreach ( $themes as $theme_path => $theme_details ) { $theme_name = \WP_CLI\Utils\get_theme_name( $theme_path ); $available = $this->get_installed_languages( $theme_name ); foreach ( $language_codes as $language_code ) { $result = [ 'name' => $theme_name, 'locale' => $language_code, ]; if ( in_array( $language_code, $available, true ) ) { \WP_CLI::log( "Language '{$language_code}' for '{$theme_details['Name']}' already installed." ); $result['status'] = 'already installed'; $skips++; } else { $response = $this->download_language_pack( $language_code, $theme_name ); if ( is_wp_error( $response ) ) { \WP_CLI::warning( $response ); \WP_CLI::log( "Language '{$language_code}' for '{$theme_details['Name']}' not installed." ); if ( 'not_found' === $response->get_error_code() ) { $result['status'] = 'not available'; $skips++; } else { $result['status'] = 'not installed'; $errors++; } } else { \WP_CLI::log( "Language '{$language_code}' for '{$theme_details['Name']}' installed." ); $result['status'] = 'installed'; $successes++; } } $results[] = (object) $result; } } if ( 'summary' !== $assoc_args['format'] ) { \WP_CLI\Utils\format_items( $assoc_args['format'], $results, array( 'name', 'locale', 'status' ) ); } \WP_CLI\Utils\report_batch_operation_results( 'language', 'install', $count, $successes, $errors, $skips ); } /** * Uninstalls a given language for a theme. * * ## OPTIONS * * <theme> * : Theme to uninstall language for. * * <language>... * : Language code to uninstall. * * ## EXAMPLES * * $ wp language theme uninstall twentyten ja * Success: Language uninstalled. * * @subcommand uninstall */ public function uninstall( $args, $assoc_args ) { /** @var WP_Filesystem_Base $wp_filesystem */ global $wp_filesystem; $theme = array_shift( $args ); $language_codes = (array) $args; $current_locale = get_locale(); $dir = WP_LANG_DIR . "/$this->obj_type"; $files = scandir( $dir ); if ( ! $files ) { \WP_CLI::error( 'No files found in language directory.' ); } // As of WP 4.0, no API for deleting a language pack WP_Filesystem(); $available = $this->get_installed_languages( $theme ); foreach ( $language_codes as $language_code ) { if ( ! in_array( $language_code, $available, true ) ) { WP_CLI::error( 'Language not installed.' ); } if ( $language_code === $current_locale ) { WP_CLI::warning( "The '{$language_code}' language is active." ); exit; } if ( $wp_filesystem->delete( "{$dir}/{$theme}-{$language_code}.po" ) && $wp_filesystem->delete( "{$dir}/{$theme}-{$language_code}.mo" ) ) { WP_CLI::success( 'Language uninstalled.' ); } else { WP_CLI::error( "Couldn't uninstall language." ); } } } /** * Updates installed languages for one or more themes. * * ## OPTIONS * * [<theme>...] * : One or more themes to update languages for. * * [--all] * : If set, languages for all themes will be updated. * * [--dry-run] * : Preview which translations would be updated. * * ## EXAMPLES * * $ wp language theme update --all * Updating 'Japanese' translation for Twenty Fifteen 1.5... * Downloading translation from https://downloads.wordpress.org/translation/theme/twentyfifteen/1.5/ja.zip... * Translation updated successfully. * Success: Updated 1/1 translation. * * @subcommand update */ public function update( $args, $assoc_args ) { $all = \WP_CLI\Utils\get_flag_value( $assoc_args, 'all', false ); if ( ! $all && empty( $args ) ) { WP_CLI::error( 'Please specify one or more themes, or use --all.' ); } if ( $all ) { $args = array_map( '\WP_CLI\Utils\get_theme_name', array_keys( wp_get_themes() ) ); if ( empty( $args ) ) { WP_CLI::success( 'No themes installed.' ); return; } } parent::update( $args, $assoc_args ); } } <?php use WP_CLI\Dispatcher\CommandNamespace; /** * Installs, activates, and manages language packs. * * ## EXAMPLES * * # Install the Dutch core language pack. * $ wp language core install nl_NL * Success: Language installed. * * # Activate the Dutch core language pack. * $ wp language core activate nl_NL * Success: Language activated. * * # Install the Dutch language pack for Twenty Seventeen. * $ wp language theme install twentyseventeen nl_NL * Success: Language installed. * * # Install the Dutch language pack for Akismet. * $ wp language plugin install akismet nl_NL * Success: Language installed. */ class Language_Namespace extends CommandNamespace { } <?php if ( ! class_exists( 'WP_CLI' ) ) { return; } $wpcli_language_autoloader = dirname( __FILE__ ) . '/vendor/autoload.php'; if ( file_exists( $wpcli_language_autoloader ) ) { require_once $wpcli_language_autoloader; } $wpcli_language_check_requirements = function() { if ( \WP_CLI\Utils\wp_version_compare( '4.0', '<' ) ) { WP_CLI::error( 'Requires WordPress 4.0 or greater.' ); } }; WP_CLI::add_command( 'language core', 'Core_Language_Command', array( 'before_invoke' => $wpcli_language_check_requirements ) ); WP_CLI::add_command( 'language plugin', 'Plugin_Language_Command', array( 'before_invoke' => $wpcli_language_check_requirements ) ); WP_CLI::add_command( 'language theme', 'Theme_Language_Command', array( 'before_invoke' => $wpcli_language_check_requirements ) ); WP_CLI::add_hook( 'after_add_command:site', function () { WP_CLI::add_command( 'site switch-language', 'Site_Switch_Language_Command' ); } ); if ( class_exists( 'WP_CLI\Dispatcher\CommandNamespace' ) ) { WP_CLI::add_command( 'language', 'Language_Namespace' ); } <?php use WP_CLI\Formatter; /** * Adds, removes, and lists capabilities of a user role. * * See references for [Roles and Capabilities](https://codex.wordpress.org/Roles_and_Capabilities) and [WP User class](https://codex.wordpress.org/Class_Reference/WP_User). * * ## EXAMPLES * * # Add 'spectate' capability to 'author' role. * $ wp cap add 'author' 'spectate' * Success: Added 1 capability to 'author' role. * * # Add all caps from 'editor' role to 'author' role. * $ wp cap list 'editor' | xargs wp cap add 'author' * Success: Added 24 capabilities to 'author' role. * * # Remove all caps from 'editor' role that also appear in 'author' role. * $ wp cap list 'author' | xargs wp cap remove 'editor' * Success: Removed 34 capabilities from 'editor' role. */ class Capabilities_Command extends WP_CLI_Command { /** * List of available fields. * * @var array */ private $fields = [ 'name' ]; /** * Lists capabilities for a given role. * * ## OPTIONS * * <role> * : Key for the role. * * [--format=<format>] * : Render output in a particular format. * --- * default: list * options: * - list * - table * - csv * - json * - count * - yaml * --- * * [--show-grant] * : Display all capabilities defined for a role including grant. * --- * default: false * --- * * ## EXAMPLES * * # Display alphabetical list of Contributor capabilities. * $ wp cap list 'contributor' | sort * delete_posts * edit_posts * level_0 * level_1 * read * * @subcommand list */ public function list_( $args, $assoc_args ) { $role_obj = self::get_role( $args[0] ); $show_grant = ! empty( $assoc_args['show-grant'] ); if ( $show_grant ) { array_push( $this->fields, 'grant' ); $capabilities = $role_obj->capabilities; } else { $capabilities = array_filter( $role_obj->capabilities ); } $output_caps = array(); foreach ( $capabilities as $cap => $grant ) { $output_cap = new stdClass(); $output_cap->name = $cap; $output_cap->grant = $grant ? 'true' : 'false'; $output_caps[] = $output_cap; } if ( 'list' === $assoc_args['format'] ) { foreach ( $output_caps as $cap ) { if ( $show_grant ) { WP_CLI::line( implode( ',', array( $cap->name, $cap->grant ) ) ); } else { WP_CLI::line( $cap->name ); } } } else { $formatter = new Formatter( $assoc_args, $this->fields ); $formatter->display_items( $output_caps ); } } /** * Adds capabilities to a given role. * * ## OPTIONS * * <role> * : Key for the role. * * <cap>... * : One or more capabilities to add. * * [--grant] * : Adds the capability as an explicit boolean value, instead of implicitly defaulting to `true`. * --- * default: true * options: * - true * - false * --- * * ## EXAMPLES * * # Add 'spectate' capability to 'author' role. * $ wp cap add author spectate * Success: Added 1 capability to 'author' role. */ public function add( $args, $assoc_args ) { self::persistence_check(); $role = array_shift( $args ); $role_obj = self::get_role( $role ); $grant = ! isset( $assoc_args['grant'] ) || ! empty( $assoc_args['grant'] ); $count = 0; foreach ( $args as $cap ) { if ( true === $grant && $role_obj->has_cap( $cap ) ) { continue; } if ( false === $grant && isset( $role_obj->capabilities[ $cap ] ) && false === $role_obj->capabilities[ $cap ] ) { continue; } $role_obj->add_cap( $cap, $grant ); $count++; } $capability = WP_CLI\Utils\pluralize( 'capability', $count ); $grant_qualification = $grant ? '' : ' as false'; WP_CLI::success( "Added {$count} {$capability} to '{$role}' role{$grant_qualification}." ); } /** * Removes capabilities from a given role. * * ## OPTIONS * * <role> * : Key for the role. * * <cap>... * : One or more capabilities to remove. * * ## EXAMPLES * * # Remove 'spectate' capability from 'author' role. * $ wp cap remove author spectate * Success: Removed 1 capability from 'author' role. */ public function remove( $args ) { self::persistence_check(); $role = array_shift( $args ); $role_obj = self::get_role( $role ); $count = 0; foreach ( $args as $cap ) { if ( ! isset( $role_obj->capabilities[ $cap ] ) ) { continue; } $role_obj->remove_cap( $cap ); $count++; } $capability = WP_CLI\Utils\pluralize( 'capability', $count ); WP_CLI::success( "Removed {$count} {$capability} from '{$role}' role." ); } /** * Retrieve a specific role from the system. * * @param string $role Role to retrieve. * @return WP_Role Requested role. * @throws \WP_CLI\ExitException If the role could not be found. */ private static function get_role( $role ) { global $wp_roles; $role_obj = $wp_roles->get_role( $role ); if ( ! $role_obj ) { WP_CLI::error( "'{$role}' role not found." ); } return $role_obj; } /** * Assert that the roles are persisted to the database. * * @throws \WP_CLI\ExitException If the roles are not persisted to the * database. */ private static function persistence_check() { global $wp_roles; if ( ! $wp_roles->use_db ) { WP_CLI::error( 'Role definitions are not persistent.' ); } } } <?php use WP_CLI\Utils; use WP_CLI\Formatter; /** * Manages user roles, including creating new roles and resetting to defaults. * * See references for [Roles and Capabilities](https://codex.wordpress.org/Roles_and_Capabilities) and [WP User class](https://codex.wordpress.org/Class_Reference/WP_User). * * ## EXAMPLES * * # List roles. * $ wp role list --fields=role --format=csv * role * administrator * editor * author * contributor * subscriber * * # Check to see if a role exists. * $ wp role exists editor * Success: Role with ID 'editor' exists. * * # Create a new role. * $ wp role create approver Approver * Success: Role with key 'approver' created. * * # Delete an existing role. * $ wp role delete approver * Success: Role with key 'approver' deleted. * * # Reset existing roles to their default capabilities. * $ wp role reset administrator author contributor * Success: Reset 3/3 roles. * * @package wp-cli */ class Role_Command extends WP_CLI_Command { /** * List of available fields. * * @var array */ private $fields = [ 'name', 'role' ]; /** * Default roles as provided by WordPress Core. * * @var array */ private $roles = [ 'administrator', 'editor', 'author', 'contributor', 'subscriber' ]; /** * Lists all roles. * * ## OPTIONS * * [--fields=<fields>] * : Limit the output to specific object fields. * * [--field=<field>] * : Prints the value of a single field. * * [--format=<format>] * : Render output in a particular format. * --- * default: table * options: * - table * - csv * - json * - count * - yaml * --- * * ## AVAILABLE FIELDS * * These fields will be displayed by default for each role: * * * name * * role * * There are no optional fields. * * ## EXAMPLES * * # List roles. * $ wp role list --fields=role --format=csv * role * administrator * editor * author * contributor * subscriber * * @subcommand list */ public function list_( $args, $assoc_args ) { global $wp_roles; $output_roles = array(); foreach ( $wp_roles->roles as $key => $role ) { $output_role = new stdClass(); $output_role->name = $role['name']; $output_role->role = $key; $output_roles[] = $output_role; } $formatter = new Formatter( $assoc_args, $this->fields ); $formatter->display_items( $output_roles ); } /** * Checks if a role exists. * * Exits with return code 0 if the role exists, 1 if it does not. * * ## OPTIONS * * <role-key> * : The internal name of the role. * * ## EXAMPLES * * # Check if a role exists. * $ wp role exists editor * Success: Role with ID 'editor' exists. */ public function exists( $args ) { global $wp_roles; if ( ! in_array( $args[0], array_keys( $wp_roles->roles ), true ) ) { WP_CLI::error( "Role with ID '{$args[0]}' does not exist." ); } WP_CLI::success( "Role with ID '{$args[0]}' exists." ); } /** * Creates a new role. * * ## OPTIONS * * <role-key> * : The internal name of the role. * * <role-name> * : The publicly visible name of the role. * * [--clone=<role>] * : Clone capabilities from an existing role. * * ## EXAMPLES * * # Create role for Approver. * $ wp role create approver Approver * Success: Role with key 'approver' created. * * # Create role for Product Administrator. * $ wp role create productadmin "Product Administrator" * Success: Role with key 'productadmin' created. */ public function create( $args, $assoc_args ) { global $wp_roles; self::persistence_check(); $role_key = array_shift( $args ); $role_name = array_shift( $args ); if ( empty( $role_key ) || empty( $role_name ) ) { WP_CLI::error( "Can't create role, insufficient information provided." ); } $capabilities = false; if ( ! empty( $assoc_args['clone'] ) ) { $role_obj = $wp_roles->get_role( $assoc_args['clone'] ); if ( ! $role_obj ) { WP_CLI::error( "'{$assoc_args['clone']}' role not found." ); } $capabilities = array_keys( $role_obj->capabilities ); } if ( add_role( $role_key, $role_name ) ) { if ( ! empty( $capabilities ) ) { $role_obj = $wp_roles->get_role( $role_key ); foreach ( $capabilities as $cap ) { $role_obj->add_cap( $cap ); } WP_CLI::success( "Role with key '{$role_key}' created. Cloned capabilities from '{$assoc_args['clone']}'." ); } else { WP_CLI::success( "Role with key '{$role_key}' created." ); } } else { WP_CLI::error( "Role couldn't be created." ); } } /** * Deletes an existing role. * * ## OPTIONS * * <role-key> * : The internal name of the role. * * ## EXAMPLES * * # Delete approver role. * $ wp role delete approver * Success: Role with key 'approver' deleted. * * # Delete productadmin role. * wp role delete productadmin * Success: Role with key 'productadmin' deleted. */ public function delete( $args ) { global $wp_roles; self::persistence_check(); $role_key = array_shift( $args ); if ( empty( $role_key ) || ! isset( $wp_roles->roles[ $role_key ] ) ) { WP_CLI::error( 'Role key not provided, or is invalid.' ); } remove_role( $role_key ); // Note: remove_role() doesn't indicate success or otherwise, so we have to // check ourselves if ( ! isset( $wp_roles->roles[ $role_key ] ) ) { WP_CLI::success( "Role with key '{$role_key}' deleted." ); } else { WP_CLI::error( "Role with key '{$role_key}' could not be deleted." ); } } /** * Resets any default role to default capabilities. * * ## OPTIONS * * [<role-key>...] * : The internal name of one or more roles to reset. * * [--all] * : If set, all default roles will be reset. * * ## EXAMPLES * * # Reset role. * $ wp role reset administrator author contributor * Success: Reset 1/3 roles. * * # Reset all default roles. * $ wp role reset --all * Success: All default roles reset. */ public function reset( $args, $assoc_args ) { self::persistence_check(); if ( ! Utils\get_flag_value( $assoc_args, 'all' ) && empty( $args ) ) { WP_CLI::error( 'Role key not provided, or is invalid.' ); } if ( ! function_exists( 'populate_roles' ) ) { require_once ABSPATH . 'wp-admin/includes/schema.php'; } global $wp_roles; $all_roles = array_keys( $wp_roles->roles ); $preserve_args = $args; // Get our default roles. $default_roles = $this->roles; $preserve = $this->roles; $before = []; if ( Utils\get_flag_value( $assoc_args, 'all' ) ) { foreach ( $default_roles as $role ) { $before[ $role ] = get_role( $role ); remove_role( $role ); $args[] = $role; } populate_roles(); $not_affected_roles = array_diff( $all_roles, $default_roles ); if ( ! empty( $not_affected_roles ) ) { foreach ( $not_affected_roles as $not_affected_role ) { WP_CLI::log( "Custom role '{$not_affected_role}' not affected." ); } } } else { foreach ( $args as $k => $role_key ) { $key = array_search( $role_key, $default_roles, true ); if ( false !== $key ) { unset( $preserve[ $key ] ); $before[ $role_key ] = get_role( $role_key ); remove_role( $role_key ); } else { unset( $args[ $k ] ); } } $not_affected_roles = array_diff( $preserve_args, $default_roles ); if ( ! empty( $not_affected_roles ) ) { foreach ( $not_affected_roles as $not_affected_role ) { WP_CLI::log( "Custom role '{$not_affected_role}' not affected." ); } } // No roles were unset, bail. if ( count( $default_roles ) === count( $preserve ) ) { WP_CLI::error( 'Must specify a default role to reset.' ); } // For the roles we're not resetting. foreach ( $preserve as $k => $role ) { /* save roles * if get_role is null * save role name for re-removal */ $roleobj = get_role( $role ); $preserve[ $k ] = is_null( $roleobj ) ? $role : $roleobj; remove_role( $role ); } // Put back all default roles and capabilities. populate_roles(); // Restore the preserved roles. foreach ( $preserve as $k => $roleobj ) { // Re-remove after populating. if ( is_a( $roleobj, 'WP_Role' ) ) { remove_role( $roleobj->name ); add_role( $roleobj->name, ucwords( $roleobj->name ), $roleobj->capabilities ); } else { // When not an object, that means the role didn't exist before. remove_role( $roleobj ); } } } $num_reset = 0; $args = array_unique( $args ); $num_to_reset = count( $args ); foreach ( $args as $role_key ) { $after[ $role_key ] = get_role( $role_key ); // phpcs:ignore WordPress.PHP.StrictComparisons.LooseComparison -- Object instances won't be same, strict check will fail here. if ( $after[ $role_key ] != $before[ $role_key ] ) { ++$num_reset; $restored_cap = array_diff_key( $after[ $role_key ]->capabilities, $before[ $role_key ]->capabilities ); $removed_cap = array_diff_key( $before[ $role_key ]->capabilities, $after[ $role_key ]->capabilities ); $restored_cap_count = count( $restored_cap ); $removed_cap_count = count( $removed_cap ); $restored_text = ( 1 === $restored_cap_count ) ? '%d capability' : '%d capabilities'; $removed_text = ( 1 === $removed_cap_count ) ? '%d capability' : '%d capabilities'; $message = "Restored {$restored_text} to and removed {$removed_text} from '%s' role."; WP_CLI::log( sprintf( $message, $restored_cap_count, $removed_cap_count, $role_key ) ); } else { WP_CLI::log( "No changes necessary for '{$role_key}' role." ); } } if ( $num_reset ) { if ( 1 === count( $args ) ) { WP_CLI::success( 'Role reset.' ); } else { WP_CLI::success( "{$num_reset} of {$num_to_reset} roles reset." ); } } else { if ( 1 === count( $args ) ) { WP_CLI::success( 'Role didn\'t need resetting.' ); } else { WP_CLI::success( 'No roles needed resetting.' ); } } } /** * Assert that the roles are persisted to the database. * * @throws \WP_CLI\ExitException If the roles are not persisted to the * database. */ private static function persistence_check() { global $wp_roles; if ( ! $wp_roles->use_db ) { WP_CLI::error( 'Role definitions are not persistent.' ); } } } <?php if ( ! class_exists( 'WP_CLI' ) ) { return; } $wpcli_role_autoloader = dirname( __FILE__ ) . '/vendor/autoload.php'; if ( file_exists( $wpcli_role_autoloader ) ) { require_once $wpcli_role_autoloader; } WP_CLI::add_command( 'cap', 'Capabilities_Command' ); WP_CLI::add_command( 'role', 'Role_Command' ); <?php class Import_Command extends WP_CLI_Command { public $processed_posts = array(); /** * Imports content from a given WXR file. * * Provides a command line interface to the WordPress Importer plugin, for * performing data migrations. * * ## OPTIONS * * <file>... * : Path to one or more valid WXR files for importing. Directories are also accepted. * * --authors=<authors> * : How the author mapping should be handled. Options are 'create', 'mapping.csv', or 'skip'. The first will create any non-existent users from the WXR file. The second will read author mapping associations from a CSV, or create a CSV for editing if the file path doesn't exist. The CSV requires two columns, and a header row like "old_user_login,new_user_login". The last option will skip any author mapping. * * [--skip=<data-type>] * : Skip importing specific data. Supported options are: 'attachment' and 'image_resize' (skip time-consuming thumbnail generation). * * ## EXAMPLES * * # Import content from a WXR file * $ wp import example.wordpress.2016-06-21.xml --authors=create * Starting the import process... * Processing post #1 ("Hello world!") (post_type: post) * -- 1 of 1 * -- Tue, 21 Jun 2016 05:31:12 +0000 * -- Imported post as post_id #1 * Success: Finished importing from 'example.wordpress.2016-06-21.xml' file. */ public function __invoke( $args, $assoc_args ) { $defaults = array( 'authors' => null, 'skip' => array(), ); $assoc_args = wp_parse_args( $assoc_args, $defaults ); if ( ! is_array( $assoc_args['skip'] ) ) { $assoc_args['skip'] = explode( ',', $assoc_args['skip'] ); } $importer = $this->is_importer_available(); if ( is_wp_error( $importer ) ) { WP_CLI::error( $importer ); } $this->add_wxr_filters(); WP_CLI::log( 'Starting the import process...' ); $new_args = array(); foreach ( $args as $arg ) { if ( is_dir( $arg ) ) { $dir = WP_CLI\Utils\trailingslashit( $arg ); $files = glob( $dir . '*.wxr' ); if ( ! empty( $files ) ) { $new_args = array_merge( $new_args, $files ); } $files = glob( $dir . '*.xml' ); if ( ! empty( $files ) ) { $new_args = array_merge( $new_args, $files ); } } else { if ( file_exists( $arg ) ) { $new_args[] = $arg; } } } $args = $new_args; foreach ( $args as $file ) { if ( ! is_readable( $file ) ) { WP_CLI::warning( "Can't read '$file' file." ); } $ret = $this->import_wxr( $file, $assoc_args ); if ( is_wp_error( $ret ) ) { WP_CLI::error( $ret ); } else { WP_CLI::log( '' ); // WXR import ends with HTML, so make sure message is on next line WP_CLI::success( "Finished importing from '$file' file." ); } } } /** * Imports a WXR file. */ private function import_wxr( $file, $args ) { $wp_import = new WP_Import(); $wp_import->processed_posts = $this->processed_posts; $import_data = $wp_import->parse( $file ); if ( is_wp_error( $import_data ) ) { return $import_data; } // Prepare the data to be used in process_author_mapping(); $wp_import->get_authors_from_import( $import_data ); // We no longer need the original data, so unset to avoid using excess // memory. unset( $import_data ); $author_data = array(); foreach ( $wp_import->authors as $wxr_author ) { $author = new \stdClass(); // Always in the WXR $author->user_login = $wxr_author['author_login']; // Should be in the WXR; no guarantees if ( isset( $wxr_author['author_email'] ) ) { $author->user_email = $wxr_author['author_email']; } if ( isset( $wxr_author['author_display_name'] ) ) { $author->display_name = $wxr_author['author_display_name']; } if ( isset( $wxr_author['author_first_name'] ) ) { $author->first_name = $wxr_author['author_first_name']; } if ( isset( $wxr_author['author_last_name'] ) ) { $author->last_name = $wxr_author['author_last_name']; } $author_data[] = $author; } // Build the author mapping $author_mapping = $this->process_author_mapping( $args['authors'], $author_data ); if ( is_wp_error( $author_mapping ) ) { return $author_mapping; } $author_in = wp_list_pluck( $author_mapping, 'old_user_login' ); $author_out = wp_list_pluck( $author_mapping, 'new_user_login' ); unset( $author_mapping, $author_data ); // $user_select needs to be an array of user IDs $user_select = array(); $invalid_user_select = array(); foreach ( $author_out as $author_login ) { $user = get_user_by( 'login', $author_login ); if ( $user ) { $user_select[] = $user->ID; } else { $invalid_user_select[] = $author_login; } } if ( ! empty( $invalid_user_select ) ) { return new WP_Error( 'invalid-author-mapping', sprintf( 'These user_logins are invalid: %s', implode( ',', $invalid_user_select ) ) ); } unset( $author_out ); // Drive the import $wp_import->fetch_attachments = ! in_array( 'attachment', $args['skip'], true ); $_GET = array( 'import' => 'wordpress', 'step' => 2, ); $_POST = array( 'imported_authors' => $author_in, 'user_map' => $user_select, 'fetch_attachments' => $wp_import->fetch_attachments, ); if ( in_array( 'image_resize', $args['skip'], true ) ) { add_filter( 'intermediate_image_sizes_advanced', array( $this, 'filter_set_image_sizes' ) ); } $GLOBALS['wpcli_import_current_file'] = basename( $file ); $wp_import->import( $file ); $this->processed_posts += $wp_import->processed_posts; return true; } public function filter_set_image_sizes( $sizes ) { // Return null here to prevent the core image resizing logic from running. return null; } /** * Defines useful verbosity filters for the WXR importer. */ private function add_wxr_filters() { add_filter( 'wp_import_posts', function( $posts ) { global $wpcli_import_counts; $wpcli_import_counts['current_post'] = 0; $wpcli_import_counts['total_posts'] = count( $posts ); return $posts; }, 10 ); add_filter( 'wp_import_post_comments', function( $comments, $post_id, $post ) { global $wpcli_import_counts; $wpcli_import_counts['current_comment'] = 0; $wpcli_import_counts['total_comments'] = count( $comments ); return $comments; }, 10, 3 ); add_filter( 'wp_import_post_data_raw', function( $post ) { global $wpcli_import_counts, $wpcli_import_current_file; $wpcli_import_counts['current_post']++; WP_CLI::log( '' ); WP_CLI::log( '' ); WP_CLI::log( sprintf( 'Processing post #%d ("%s") (post_type: %s)', $post['post_id'], $post['post_title'], $post['post_type'] ) ); WP_CLI::log( sprintf( '-- %s of %s (in file %s)', number_format( $wpcli_import_counts['current_post'] ), number_format( $wpcli_import_counts['total_posts'] ), $wpcli_import_current_file ) ); WP_CLI::log( '-- ' . date( 'r' ) ); // phpcs:ignore WordPress.DateTime.RestrictedFunctions.date_date return $post; } ); add_action( 'wp_import_insert_post', function( $post_id, $original_post_id, $post, $postdata ) { global $wpcli_import_counts; if ( is_wp_error( $post_id ) ) { WP_CLI::warning( '-- Error importing post: ' . $post_id->get_error_code() ); } else { WP_CLI::log( "-- Imported post as post_id #{$post_id}" ); } if ( 0 === ( $wpcli_import_counts['current_post'] % 500 ) ) { WP_CLI\Utils\wp_clear_object_cache(); WP_CLI::log( '-- Cleared object cache.' ); } }, 10, 4 ); add_action( 'wp_import_insert_term', function( $t, $import_term, $post_id, $post ) { WP_CLI::log( "-- Created term \"{$import_term['name']}\"" ); }, 10, 4 ); add_action( 'wp_import_set_post_terms', function( $tt_ids, $term_ids, $taxonomy, $post_id, $post ) { WP_CLI::log( '-- Added terms (' . implode( ',', $term_ids ) . ") for taxonomy \"{$taxonomy}\"" ); }, 10, 5 ); add_action( 'wp_import_insert_comment', function( $comment_id, $comment, $comment_post_id, $post ) { global $wpcli_import_counts; $wpcli_import_counts['current_comment']++; WP_CLI::log( sprintf( '-- Added comment #%d (%s of %s)', $comment_id, number_format( $wpcli_import_counts['current_comment'] ), number_format( $wpcli_import_counts['total_comments'] ) ) ); }, 10, 4 ); add_action( 'import_post_meta', function( $post_id, $key, $value ) { WP_CLI::log( "-- Added post_meta $key" ); }, 10, 3 ); } /** * Determines whether the requested importer is available. */ private function is_importer_available() { require_once ABSPATH . 'wp-admin/includes/plugin.php'; if ( class_exists( 'WP_Import' ) ) { return true; } $plugins = get_plugins(); $wordpress_importer = 'wordpress-importer/wordpress-importer.php'; if ( array_key_exists( $wordpress_importer, $plugins ) ) { $error_msg = "WordPress Importer needs to be activated. Try 'wp plugin activate wordpress-importer'."; } else { $error_msg = "WordPress Importer needs to be installed. Try 'wp plugin install wordpress-importer --activate'."; } return new WP_Error( 'importer-missing', $error_msg ); } /** * Processes how the authors should be mapped * * @param string $authors_arg The `--author` argument originally passed to command * @param array $author_data An array of WP_User-esque author objects * @return array|WP_Error $author_mapping Author mapping array if successful, WP_Error if something bad happened */ private function process_author_mapping( $authors_arg, $author_data ) { // Provided an author mapping file (method checks validity) if ( file_exists( $authors_arg ) ) { return $this->read_author_mapping_file( $authors_arg ); } // Provided a file reference, but the file doesn't yet exist if ( false !== stripos( $authors_arg, '.csv' ) ) { return $this->create_author_mapping_file( $authors_arg, $author_data ); } switch ( $authors_arg ) { // Create authors if they don't yet exist; maybe match on email or user_login case 'create': return $this->create_authors_for_mapping( $author_data ); // Skip any sort of author mapping case 'skip': return array(); default: return new WP_Error( 'invalid-argument', "'authors' argument is invalid." ); } } /** * Reads an author mapping file. */ private function read_author_mapping_file( $file ) { $author_mapping = array(); foreach ( new \WP_CLI\Iterators\CSV( $file ) as $i => $author ) { if ( ! array_key_exists( 'old_user_login', $author ) || ! array_key_exists( 'new_user_login', $author ) ) { return new WP_Error( 'invalid-author-mapping', "Author mapping file isn't properly formatted." ); } $author_mapping[] = $author; } return $author_mapping; } /** * Creates an author mapping file, based on provided author data. * * @return WP_Error The file was just now created, so some action needs to be taken */ private function create_author_mapping_file( $file, $author_data ) { if ( touch( $file ) ) { $author_mapping = array(); foreach ( $author_data as $author ) { $author_mapping[] = array( 'old_user_login' => $author->user_login, 'new_user_login' => $this->suggest_user( $author->user_login, $author->user_email ), ); } $file_resource = fopen( $file, 'w' ); \WP_CLI\utils\write_csv( $file_resource, $author_mapping, array( 'old_user_login', 'new_user_login' ) ); return new WP_Error( 'author-mapping-error', sprintf( 'Please update author mapping file before continuing: %s', $file ) ); } else { return new WP_Error( 'author-mapping-error', "Couldn't create author mapping file." ); } } /** * Creates users if they don't exist, and build an author mapping file. */ private function create_authors_for_mapping( $author_data ) { $author_mapping = array(); foreach ( $author_data as $author ) { if ( isset( $author->user_email ) ) { $user = get_user_by( 'email', $author->user_email ); if ( $user instanceof WP_User ) { $author_mapping[] = array( 'old_user_login' => $author->user_login, 'new_user_login' => $user->user_login, ); continue; } } $user = get_user_by( 'login', $author->user_login ); if ( $user instanceof WP_User ) { $author_mapping[] = array( 'old_user_login' => $author->user_login, 'new_user_login' => $user->user_login, ); continue; } $user = array( 'user_login' => '', 'user_email' => '', 'user_pass' => wp_generate_password(), ); $user = array_merge( $user, (array) $author ); $user_id = wp_insert_user( $user ); if ( is_wp_error( $user_id ) ) { return $user_id; } $user = get_user_by( 'id', $user_id ); $author_mapping[] = array( 'old_user_login' => $author->user_login, 'new_user_login' => $user->user_login, ); } return $author_mapping; } /** * Suggests a blog user based on the levenshtein distance. */ private function suggest_user( $author_user_login, $author_user_email = '' ) { if ( ! isset( $this->blog_users ) ) { $this->blog_users = get_users(); } $shortest = -1; $shortestavg = array(); $threshold = floor( ( strlen( $author_user_login ) / 100 ) * 10 ); // 10 % of the strlen are valid $closest = ''; foreach ( $this->blog_users as $user ) { // Before we resort to an algorithm, let's try for an exact match if ( $author_user_email && $user->user_email === $author_user_email ) { return $user->user_login; } $levs = array(); $levs[] = levenshtein( $author_user_login, $user->display_name ); $levs[] = levenshtein( $author_user_login, $user->user_login ); $levs[] = levenshtein( $author_user_login, $user->user_email ); $email_parts = explode( '@', $user->user_email ); $email_login = array_shift( $email_parts ); $levs[] = levenshtein( $author_user_login, $email_login ); rsort( $levs ); $lev = array_pop( $levs ); if ( 0 === $lev ) { $closest = $user->user_login; $shortest = 0; break; } if ( ( $lev <= $shortest || $shortest < 0 ) && $lev <= $threshold ) { $closest = $user->user_login; $shortest = $lev; } $shortestavg[] = $lev; } // in case all usernames have a common pattern if ( $shortest > ( array_sum( $shortestavg ) / count( $shortestavg ) ) ) { return ''; } return $closest; } } <?php if ( ! class_exists( 'WP_CLI' ) ) { return; } $wpcli_import_autoloader = dirname( __FILE__ ) . '/vendor/autoload.php'; if ( file_exists( $wpcli_import_autoloader ) ) { require_once $wpcli_import_autoloader; } WP_CLI::add_command( 'import', 'Import_Command' ); <?php use WP_CLI\Formatter; use WP_CLI\Utils; /** * Performs basic database operations using credentials stored in wp-config.php. * * ## EXAMPLES * * # Create a new database. * $ wp db create * Success: Database created. * * # Drop an existing database. * $ wp db drop --yes * Success: Database dropped. * * # Reset the current database. * $ wp db reset --yes * Success: Database reset. * * # Execute a SQL query stored in a file. * $ wp db query < debug.sql * * @when after_wp_config_load */ class DB_Command extends WP_CLI_Command { /** * Legacy UTF-8 encoding for MySQL. * * @var string */ const ENCODING_UTF8 = 'utf8'; /** * Standards-compliant UTF-8 encoding for MySQL. * * @var string */ const ENCODING_UTF8MB4 = 'utf8mb4'; /** * A list of incompatible SQL modes. * * Copied over from WordPress Core code. * @see https://github.com/WordPress/wordpress-develop/blob/5.4.0/src/wp-includes/wp-db.php#L559-L572 * * @var string[] Array of SQL mode names that are incompatible with WordPress. */ protected $sql_incompatible_modes = [ 'NO_ZERO_DATE', 'ONLY_FULL_GROUP_BY', 'STRICT_TRANS_TABLES', 'STRICT_ALL_TABLES', 'TRADITIONAL', 'ANSI', ]; /** * Creates a new database. * * Runs `CREATE_DATABASE` SQL statement using `DB_HOST`, `DB_NAME`, * `DB_USER` and `DB_PASSWORD` database credentials specified in * wp-config.php. * * ## OPTIONS * * [--dbuser=<value>] * : Username to pass to mysql. Defaults to DB_USER. * * [--dbpass=<value>] * : Password to pass to mysql. Defaults to DB_PASSWORD. * * [--defaults] * : Loads the environment's MySQL option files. Default behavior is to skip loading them to avoid failures due to misconfiguration. * * ## EXAMPLES * * $ wp db create * Success: Database created. */ public function create( $_, $assoc_args ) { $this->run_query( self::get_create_query(), $assoc_args ); WP_CLI::success( 'Database created.' ); } /** * Deletes the existing database. * * Runs `DROP_DATABASE` SQL statement using `DB_HOST`, `DB_NAME`, * `DB_USER` and `DB_PASSWORD` database credentials specified in * wp-config.php. * * ## OPTIONS * * [--dbuser=<value>] * : Username to pass to mysql. Defaults to DB_USER. * * [--dbpass=<value>] * : Password to pass to mysql. Defaults to DB_PASSWORD. * * [--yes] * : Answer yes to the confirmation message. * * [--defaults] * : Loads the environment's MySQL option files. Default behavior is to skip loading them to avoid failures due to misconfiguration. * * ## EXAMPLES * * $ wp db drop --yes * Success: Database dropped. */ public function drop( $_, $assoc_args ) { WP_CLI::confirm( "Are you sure you want to drop the '" . DB_NAME . "' database?", $assoc_args ); $this->run_query( sprintf( 'DROP DATABASE `%s`', DB_NAME ), $assoc_args ); WP_CLI::success( 'Database dropped.' ); } /** * Removes all tables from the database. * * Runs `DROP_DATABASE` and `CREATE_DATABASE` SQL statements using * `DB_HOST`, `DB_NAME`, `DB_USER` and `DB_PASSWORD` database credentials * specified in wp-config.php. * * ## OPTIONS * * [--dbuser=<value>] * : Username to pass to mysql. Defaults to DB_USER. * * [--dbpass=<value>] * : Password to pass to mysql. Defaults to DB_PASSWORD. * * [--yes] * : Answer yes to the confirmation message. * * [--defaults] * : Loads the environment's MySQL option files. Default behavior is to skip loading them to avoid failures due to misconfiguration. * * ## EXAMPLES * * $ wp db reset --yes * Success: Database reset. */ public function reset( $_, $assoc_args ) { WP_CLI::confirm( "Are you sure you want to reset the '" . DB_NAME . "' database?", $assoc_args ); $this->run_query( sprintf( 'DROP DATABASE IF EXISTS `%s`', DB_NAME ), $assoc_args ); $this->run_query( self::get_create_query(), $assoc_args ); WP_CLI::success( 'Database reset.' ); } /** * Removes all tables with `$table_prefix` from the database. * * Runs `DROP_TABLE` for each table that has a `$table_prefix` as specified * in wp-config.php. * * ## OPTIONS * * [--dbuser=<value>] * : Username to pass to mysql. Defaults to DB_USER. * * [--dbpass=<value>] * : Password to pass to mysql. Defaults to DB_PASSWORD. * * [--yes] * : Answer yes to the confirmation message. * * [--defaults] * : Loads the environment's MySQL option files. Default behavior is to skip loading them to avoid failures due to misconfiguration. * * ## EXAMPLES * * # Delete all tables that match the current site prefix. * $ wp db clean --yes * Success: Tables dropped. * * @when after_wp_load */ public function clean( $_, $assoc_args ) { global $wpdb; WP_CLI::confirm( sprintf( "Are you sure you want to drop all the tables on '%s' that use the current site's database prefix ('%s')?", DB_NAME, $wpdb->get_blog_prefix() ), $assoc_args ); $tables = Utils\wp_get_table_names( [], [ 'all-tables-with-prefix' => true ] ); foreach ( $tables as $table ) { $this->run_query( sprintf( 'DROP TABLE IF EXISTS `%s`.`%s`', DB_NAME, $table ), $assoc_args ); } WP_CLI::success( 'Tables dropped.' ); } /** * Checks the current status of the database. * * Runs `mysqlcheck` utility with `--check` using `DB_HOST`, * `DB_NAME`, `DB_USER` and `DB_PASSWORD` database credentials * specified in wp-config.php. * * [See docs](http://dev.mysql.com/doc/refman/5.7/en/check-table.html) * for more details on the `CHECK TABLE` statement. * * ## OPTIONS * * [--dbuser=<value>] * : Username to pass to mysqlcheck. Defaults to DB_USER. * * [--dbpass=<value>] * : Password to pass to mysqlcheck. Defaults to DB_PASSWORD. * * [--<field>=<value>] * : Extra arguments to pass to mysqlcheck. [Refer to mysqlcheck docs](https://dev.mysql.com/doc/en/mysqlcheck.html). * * [--defaults] * : Loads the environment's MySQL option files. Default behavior is to skip loading them to avoid failures due to misconfiguration. * * ## EXAMPLES * * $ wp db check * Success: Database checked. */ public function check( $_, $assoc_args ) { $command = sprintf( '/usr/bin/env mysqlcheck%s %s', $this->get_defaults_flag_string( $assoc_args ), '%s' ); WP_CLI::debug( "Running shell command: {$command}", 'db' ); $assoc_args['check'] = true; self::run( Utils\esc_cmd( $command, DB_NAME ), $assoc_args ); WP_CLI::debug( 'Associative arguments: ' . json_encode( $assoc_args ), 'db' ); WP_CLI::success( 'Database checked.' ); } /** * Optimizes the database. * * Runs `mysqlcheck` utility with `--optimize=true` using `DB_HOST`, * `DB_NAME`, `DB_USER` and `DB_PASSWORD` database credentials * specified in wp-config.php. * * [See docs](http://dev.mysql.com/doc/refman/5.7/en/optimize-table.html) * for more details on the `OPTIMIZE TABLE` statement. * * ## OPTIONS * * [--dbuser=<value>] * : Username to pass to mysqlcheck. Defaults to DB_USER. * * [--dbpass=<value>] * : Password to pass to mysqlcheck. Defaults to DB_PASSWORD. * * [--<field>=<value>] * : Extra arguments to pass to mysqlcheck. [Refer to mysqlcheck docs](https://dev.mysql.com/doc/en/mysqlcheck.html). * * [--defaults] * : Loads the environment's MySQL option files. Default behavior is to skip loading them to avoid failures due to misconfiguration. * * ## EXAMPLES * * $ wp db optimize * Success: Database optimized. */ public function optimize( $_, $assoc_args ) { $command = sprintf( '/usr/bin/env mysqlcheck%s %s', $this->get_defaults_flag_string( $assoc_args ), '%s' ); WP_CLI::debug( "Running shell command: {$command}", 'db' ); $assoc_args['optimize'] = true; self::run( Utils\esc_cmd( $command, DB_NAME ), $assoc_args ); WP_CLI::debug( 'Associative arguments: ' . json_encode( $assoc_args ), 'db' ); WP_CLI::success( 'Database optimized.' ); } /** * Repairs the database. * * Runs `mysqlcheck` utility with `--repair=true` using `DB_HOST`, * `DB_NAME`, `DB_USER` and `DB_PASSWORD` database credentials * specified in wp-config.php. * * [See docs](http://dev.mysql.com/doc/refman/5.7/en/repair-table.html) for * more details on the `REPAIR TABLE` statement. * * ## OPTIONS * * [--dbuser=<value>] * : Username to pass to mysqlcheck. Defaults to DB_USER. * * [--dbpass=<value>] * : Password to pass to mysqlcheck. Defaults to DB_PASSWORD. * * [--<field>=<value>] * : Extra arguments to pass to mysqlcheck. [Refer to mysqlcheck docs](https://dev.mysql.com/doc/en/mysqlcheck.html). * * [--defaults] * : Loads the environment's MySQL option files. Default behavior is to skip loading them to avoid failures due to misconfiguration. * * ## EXAMPLES * * $ wp db repair * Success: Database repaired. */ public function repair( $_, $assoc_args ) { $command = sprintf( '/usr/bin/env mysqlcheck%s %s', $this->get_defaults_flag_string( $assoc_args ), '%s' ); WP_CLI::debug( "Running shell command: {$command}", 'db' ); $assoc_args['repair'] = true; self::run( Utils\esc_cmd( $command, DB_NAME ), $assoc_args ); WP_CLI::debug( 'Associative arguments: ' . json_encode( $assoc_args ), 'db' ); WP_CLI::success( 'Database repaired.' ); } /** * Opens a MySQL console using credentials from wp-config.php * * ## OPTIONS * * [--database=<database>] * : Use a specific database. Defaults to DB_NAME. * * [--default-character-set=<character-set>] * : Use a specific character set. Defaults to DB_CHARSET when defined. * * [--dbuser=<value>] * : Username to pass to mysql. Defaults to DB_USER. * * [--dbpass=<value>] * : Password to pass to mysql. Defaults to DB_PASSWORD. * * [--<field>=<value>] * : Extra arguments to pass to mysql. [Refer to mysql docs](https://dev.mysql.com/doc/en/mysql-command-options.html). * * [--defaults] * : Loads the environment's MySQL option files. Default behavior is to skip loading them to avoid failures due to misconfiguration. * * ## EXAMPLES * * # Open MySQL console * $ wp db cli * mysql> * * @alias connect */ public function cli( $_, $assoc_args ) { $command = sprintf( '/usr/bin/env mysql%s --no-auto-rehash', $this->get_defaults_flag_string( $assoc_args ) ); WP_CLI::debug( "Running shell command: {$command}", 'db' ); if ( ! isset( $assoc_args['database'] ) ) { $assoc_args['database'] = DB_NAME; } WP_CLI::debug( 'Associative arguments: ' . json_encode( $assoc_args ), 'db' ); self::run( $command, $assoc_args, null, true ); } /** * Executes a SQL query against the database. * * Executes an arbitrary SQL query using `DB_HOST`, `DB_NAME`, `DB_USER` * and `DB_PASSWORD` database credentials specified in wp-config.php. * * ## OPTIONS * * [<sql>] * : A SQL query. If not passed, will try to read from STDIN. * * [--dbuser=<value>] * : Username to pass to mysql. Defaults to DB_USER. * * [--dbpass=<value>] * : Password to pass to mysql. Defaults to DB_PASSWORD. * * [--<field>=<value>] * : Extra arguments to pass to mysql. [Refer to mysql docs](https://dev.mysql.com/doc/en/mysql-command-options.html). * * [--defaults] * : Loads the environment's MySQL option files. Default behavior is to skip loading them to avoid failures due to misconfiguration. * * ## EXAMPLES * * # Execute a query stored in a file * $ wp db query < debug.sql * * # Check all tables in the database * $ wp db query "CHECK TABLE $(wp db tables | paste -s -d, -);" * +---------------------------------------+-------+----------+----------+ * | Table | Op | Msg_type | Msg_text | * +---------------------------------------+-------+----------+----------+ * | wordpress_dbase.wp_users | check | status | OK | * | wordpress_dbase.wp_usermeta | check | status | OK | * | wordpress_dbase.wp_posts | check | status | OK | * | wordpress_dbase.wp_comments | check | status | OK | * | wordpress_dbase.wp_links | check | status | OK | * | wordpress_dbase.wp_options | check | status | OK | * | wordpress_dbase.wp_postmeta | check | status | OK | * | wordpress_dbase.wp_terms | check | status | OK | * | wordpress_dbase.wp_term_taxonomy | check | status | OK | * | wordpress_dbase.wp_term_relationships | check | status | OK | * | wordpress_dbase.wp_termmeta | check | status | OK | * | wordpress_dbase.wp_commentmeta | check | status | OK | * +---------------------------------------+-------+----------+----------+ * * # Pass extra arguments through to MySQL * $ wp db query 'SELECT * FROM wp_options WHERE option_name="home"' --skip-column-names * +---+------+------------------------------+-----+ * | 2 | home | http://wordpress-develop.dev | yes | * +---+------+------------------------------+-----+ */ public function query( $args, $assoc_args ) { $command = sprintf( '/usr/bin/env mysql%s --no-auto-rehash', $this->get_defaults_flag_string( $assoc_args ) ); WP_CLI::debug( "Running shell command: {$command}", 'db' ); $assoc_args['database'] = DB_NAME; // The query might come from STDIN. if ( ! empty( $args ) ) { $assoc_args['execute'] = $args[0]; } if ( isset( $assoc_args['execute'] ) ) { // Ensure that the SQL mode is compatible with WPDB. $assoc_args['execute'] = $this->get_sql_mode_query( $assoc_args ) . $assoc_args['execute']; } WP_CLI::debug( 'Associative arguments: ' . json_encode( $assoc_args ), 'db' ); self::run( $command, $assoc_args ); } /** * Exports the database to a file or to STDOUT. * * Runs `mysqldump` utility using `DB_HOST`, `DB_NAME`, `DB_USER` and * `DB_PASSWORD` database credentials specified in wp-config.php. Accepts any valid `mysqldump` flags. * * ## OPTIONS * * [<file>] * : The name of the SQL file to export. If '-', then outputs to STDOUT. If * omitted, it will be '{dbname}-{Y-m-d}-{random-hash}.sql'. * * [--dbuser=<value>] * : Username to pass to mysqldump. Defaults to DB_USER. * * [--dbpass=<value>] * : Password to pass to mysqldump. Defaults to DB_PASSWORD. * * [--<field>=<value>] * : Extra arguments to pass to mysqldump. [Refer to mysqldump docs](https://dev.mysql.com/doc/en/mysqldump.html#mysqldump-option-summary). * * [--tables=<tables>] * : The comma separated list of specific tables to export. Excluding this parameter will export all tables in the database. * * [--exclude_tables=<tables>] * : The comma separated list of specific tables that should be skipped from exporting. Excluding this parameter will export all tables in the database. * * [--include-tablespaces] * : Skips adding the default --no-tablespaces option to mysqldump. * * [--porcelain] * : Output filename for the exported database. * * [--defaults] * : Loads the environment's MySQL option files. Default behavior is to skip loading them to avoid failures due to misconfiguration. * * ## EXAMPLES * * # Export database with drop query included * $ wp db export --add-drop-table * Success: Exported to 'wordpress_dbase-db72bb5.sql'. * * # Export certain tables * $ wp db export --tables=wp_options,wp_users * Success: Exported to 'wordpress_dbase-db72bb5.sql'. * * # Export all tables matching a wildcard * $ wp db export --tables=$(wp db tables 'wp_user*' --format=csv) * Success: Exported to 'wordpress_dbase-db72bb5.sql'. * * # Export all tables matching prefix * $ wp db export --tables=$(wp db tables --all-tables-with-prefix --format=csv) * Success: Exported to 'wordpress_dbase-db72bb5.sql'. * * # Export certain posts without create table statements * $ wp db export --no-create-info=true --tables=wp_posts --where="ID in (100,101,102)" * Success: Exported to 'wordpress_dbase-db72bb5.sql'. * * # Export relating meta for certain posts without create table statements * $ wp db export --no-create-info=true --tables=wp_postmeta --where="post_id in (100,101,102)" * Success: Exported to 'wordpress_dbase-db72bb5.sql'. * * # Skip certain tables from the exported database * $ wp db export --exclude_tables=wp_options,wp_users * Success: Exported to 'wordpress_dbase-db72bb5.sql'. * * # Skip all tables matching a wildcard from the exported database * $ wp db export --exclude_tables=$(wp db tables 'wp_user*' --format=csv) * Success: Exported to 'wordpress_dbase-db72bb5.sql'. * * # Skip all tables matching prefix from the exported database * $ wp db export --exclude_tables=$(wp db tables --all-tables-with-prefix --format=csv) * Success: Exported to 'wordpress_dbase-db72bb5.sql'. * * # Export database to STDOUT. * $ wp db export - * -- MySQL dump 10.13 Distrib 5.7.19, for osx10.12 (x86_64) * -- * -- Host: localhost Database: wpdev * -- ------------------------------------------------------ * -- Server version 5.7.19 * ... * * @alias dump */ public function export( $args, $assoc_args ) { if ( ! empty( $args[0] ) ) { $result_file = $args[0]; } else { // phpcs:ignore WordPress.WP.AlternativeFunctions.rand_mt_rand -- WordPress is not loaded. $hash = substr( md5( mt_rand() ), 0, 7 ); $result_file = sprintf( '%s-%s-%s.sql', DB_NAME, date( 'Y-m-d' ), $hash ); // phpcs:ignore WordPress.DateTime.RestrictedFunctions.date_date } $stdout = ( '-' === $result_file ); $porcelain = Utils\get_flag_value( $assoc_args, 'porcelain' ); // Bail if both porcelain and STDOUT are set. if ( $stdout && $porcelain ) { WP_CLI::error( 'Porcelain is not allowed when output mode is STDOUT.' ); } if ( ! $stdout ) { $assoc_args['result-file'] = $result_file; } $mysqldump_binary = Utils\force_env_on_nix_systems( 'mysqldump' ); $support_column_statistics = exec( $mysqldump_binary . ' --help | grep "column-statistics"' ); /* * In case that `--default-character-set` is not given and `DB_CHARSET` is `utf8`, * we try to deduce what the actual character set for the posts table of the * current database is and use `utf8mb4` as a `default-character-set` if that * seems like the safer default, to ensure emojis are encoded correctly. */ if ( ! isset( $assoc_args['default-character-set'] ) && ( defined( 'DB_CHARSET' ) && self::ENCODING_UTF8 === constant( 'DB_CHARSET' ) ) && self::ENCODING_UTF8MB4 === $this->get_posts_table_charset( $assoc_args ) ) { WP_CLI::debug( 'Setting missing default character set to ' . self::ENCODING_UTF8MB4, 'db' ); $assoc_args['default-character-set'] = self::ENCODING_UTF8MB4; } $initial_command = sprintf( "{$mysqldump_binary}%s ", $this->get_defaults_flag_string( $assoc_args ) ); WP_CLI::debug( "Running initial shell command: {$initial_command}", 'db' ); $default_arguments = [ '%s' ]; if ( $support_column_statistics ) { $default_arguments[] = '--skip-column-statistics'; } if ( ! Utils\get_flag_value( $assoc_args, 'include-tablespaces', false ) ) { $default_arguments[] = '--no-tablespaces'; } $command = $initial_command . implode( ' ', $default_arguments ); $command_esc_args = [ DB_NAME ]; if ( isset( $assoc_args['tables'] ) ) { $tables = explode( ',', trim( $assoc_args['tables'], ',' ) ); unset( $assoc_args['tables'] ); $command .= ' --tables'; foreach ( $tables as $table ) { $command .= ' %s'; $command_esc_args[] = trim( $table ); } } $exclude_tables = Utils\get_flag_value( $assoc_args, 'exclude_tables' ); if ( isset( $exclude_tables ) ) { $tables = explode( ',', trim( $assoc_args['exclude_tables'], ',' ) ); unset( $assoc_args['exclude_tables'] ); foreach ( $tables as $table ) { $command .= ' --ignore-table'; $command .= ' %s'; $command_esc_args[] = trim( DB_NAME . '.' . $table ); } } $escaped_command = call_user_func_array( '\WP_CLI\Utils\esc_cmd', array_merge( [ $command ], $command_esc_args ) ); // Remove parameters not needed for SQL run. unset( $assoc_args['porcelain'] ); WP_CLI::debug( 'Associative arguments: ' . json_encode( $assoc_args ), 'db' ); self::run( $escaped_command, $assoc_args ); if ( $porcelain ) { WP_CLI::line( $result_file ); } elseif ( ! $stdout ) { WP_CLI::success( sprintf( "Exported to '%s'.", $result_file ) ); } } /** * Get the current character set of the posts table. * * @param array Associative array of associative arguments. * @return string Posts table character set. */ private function get_posts_table_charset( $assoc_args ) { $query = 'SELECT CCSA.character_set_name ' . 'FROM information_schema.`TABLES` T, ' . 'information_schema.`COLLATION_CHARACTER_SET_APPLICABILITY` CCSA ' . 'WHERE CCSA.collation_name = T.table_collation ' . 'AND T.table_schema = "' . DB_NAME . '" ' . 'AND T.table_name LIKE "%\_posts";'; list( $stdout, $stderr, $exit_code ) = self::run( sprintf( '/usr/bin/env mysql%s --no-auto-rehash --batch --skip-column-names', $this->get_defaults_flag_string( $assoc_args ) ), [ 'execute' => $query ], false ); if ( $exit_code ) { WP_CLI::warning( 'Failed to get current character set of the posts table.' . ( ! empty( $stderr ) ? " Reason: {$stderr}" : '' ) ); return self::ENCODING_UTF8MB4; } $stdout = trim( $stdout ); WP_CLI::debug( "Detected character set of the posts table: {$stdout}.", 'db' ); return $stdout; } /** * Imports a database from a file or from STDIN. * * Runs SQL queries using `DB_HOST`, `DB_NAME`, `DB_USER` and * `DB_PASSWORD` database credentials specified in wp-config.php. This * does not create database by itself and only performs whatever tasks are * defined in the SQL. * * ## OPTIONS * * [<file>] * : The name of the SQL file to import. If '-', then reads from STDIN. If omitted, it will look for '{dbname}.sql'. * * [--dbuser=<value>] * : Username to pass to mysql. Defaults to DB_USER. * * [--dbpass=<value>] * : Password to pass to mysql. Defaults to DB_PASSWORD. * * [--<field>=<value>] * : Extra arguments to pass to mysql. [Refer to mysql binary docs](https://dev.mysql.com/doc/refman/8.0/en/mysql-command-options.html). * * [--skip-optimization] * : When using an SQL file, do not include speed optimization such as disabling auto-commit and key checks. * * [--defaults] * : Loads the environment's MySQL option files. Default behavior is to skip loading them to avoid failures due to misconfiguration. * * ## EXAMPLES * * # Import MySQL from a file. * $ wp db import wordpress_dbase.sql * Success: Imported from 'wordpress_dbase.sql'. */ public function import( $args, $assoc_args ) { if ( ! empty( $args[0] ) ) { $result_file = $args[0]; } else { $result_file = sprintf( '%s.sql', DB_NAME ); } // Process options to MySQL. $mysql_args = array_merge( [ 'database' => DB_NAME ], self::get_dbuser_dbpass_args( $assoc_args ), self::get_mysql_args( $assoc_args ) ); if ( '-' !== $result_file ) { if ( ! is_readable( $result_file ) ) { WP_CLI::error( sprintf( 'Import file missing or not readable: %s', $result_file ) ); } $query = Utils\get_flag_value( $assoc_args, 'skip-optimization' ) ? 'SOURCE %s;' : 'SET autocommit = 0; SET unique_checks = 0; SET foreign_key_checks = 0; SOURCE %s; COMMIT;'; $query = $this->get_sql_mode_query( $assoc_args ) . $query; $mysql_args['execute'] = sprintf( $query, $result_file ); } else { $result_file = 'STDIN'; } $command = sprintf( '/usr/bin/env mysql%s --no-auto-rehash', $this->get_defaults_flag_string( $assoc_args ) ); WP_CLI::debug( "Running shell command: {$command}", 'db' ); WP_CLI::debug( 'Associative arguments: ' . json_encode( $assoc_args ), 'db' ); self::run( $command, $mysql_args ); WP_CLI::success( sprintf( "Imported from '%s'.", $result_file ) ); } /** * Lists the database tables. * * Defaults to all tables registered to the $wpdb database handler. * * ## OPTIONS * * [<table>...] * : List tables based on wildcard search, e.g. 'wp_*_options' or 'wp_post?'. * * [--scope=<scope>] * : Can be all, global, ms_global, blog, or old tables. Defaults to all. * * [--network] * : List all the tables in a multisite install. * * [--all-tables-with-prefix] * : List all tables that match the table prefix even if not registered on $wpdb. Overrides --network. * * [--all-tables] * : List all tables in the database, regardless of the prefix, and even if not registered on $wpdb. Overrides --all-tables-with-prefix. * * [--format=<format>] * : Render output in a particular format. * --- * default: list * options: * - list * - csv * --- * * ## EXAMPLES * * # List tables for a single site, without shared tables like 'wp_users' * $ wp db tables --scope=blog --url=sub.example.com * wp_3_posts * wp_3_comments * wp_3_options * wp_3_postmeta * wp_3_terms * wp_3_term_taxonomy * wp_3_term_relationships * wp_3_termmeta * wp_3_commentmeta * * # Export only tables for a single site * $ wp db export --tables=$(wp db tables --url=sub.example.com --format=csv) * Success: Exported to wordpress_dbase.sql * * @when after_wp_load */ public function tables( $args, $assoc_args ) { $format = Utils\get_flag_value( $assoc_args, 'format' ); unset( $assoc_args['format'] ); if ( empty( $args ) && empty( $assoc_args ) ) { $assoc_args['scope'] = 'all'; } $tables = Utils\wp_get_table_names( $args, $assoc_args ); if ( 'csv' === $format ) { WP_CLI::line( implode( ',', $tables ) ); } else { foreach ( $tables as $table ) { WP_CLI::line( $table ); } } } /** * Displays the database name and size. * * Display the database name and size for `DB_NAME` specified in wp-config.php. * The size defaults to a human-readable number. * * Available size formats include: * * b (bytes) * * kb (kilobytes) * * mb (megabytes) * * gb (gigabytes) * * tb (terabytes) * * B (ISO Byte setting, with no conversion) * * KB (ISO Kilobyte setting, with 1 KB = 1,000 B) * * KiB (ISO Kibibyte setting, with 1 KiB = 1,024 B) * * MB (ISO Megabyte setting, with 1 MB = 1,000 KB) * * MiB (ISO Mebibyte setting, with 1 MiB = 1,024 KiB) * * GB (ISO Gigabyte setting, with 1 GB = 1,000 MB) * * GiB (ISO Gibibyte setting, with 1 GiB = 1,024 MiB) * * TB (ISO Terabyte setting, with 1 TB = 1,000 GB) * * TiB (ISO Tebibyte setting, with 1 TiB = 1,024 GiB) * * ## OPTIONS * * [--size_format=<format>] * : Display the database size only, as a bare number. * --- * options: * - b * - kb * - mb * - gb * - tb * - B * - KB * - KiB * - MB * - MiB * - GB * - GiB * - TB * - TiB * --- * * [--tables] * : Display each table name and size instead of the database size. * * [--human-readable] * : Display database sizes in human readable formats. * * [--format=<format>] * : Render output in a particular format. * --- * options: * - table * - csv * - json * - yaml * --- * * [--scope=<scope>] * : Can be all, global, ms_global, blog, or old tables. Defaults to all. * * [--network] * : List all the tables in a multisite install. * * [--decimals=<decimals>] * : Number of digits after decimal point. Defaults to 0. * * [--all-tables-with-prefix] * : List all tables that match the table prefix even if not registered on $wpdb. Overrides --network. * * [--all-tables] * : List all tables in the database, regardless of the prefix, and even if not registered on $wpdb. Overrides --all-tables-with-prefix. * * ## EXAMPLES * * $ wp db size * +-------------------+------+ * | Name | Size | * +-------------------+------+ * | wordpress_default | 6 MB | * +-------------------+------+ * * $ wp db size --tables * +-----------------------+-------+ * | Name | Size | * +-----------------------+-------+ * | wp_users | 64 KB | * | wp_usermeta | 48 KB | * | wp_posts | 80 KB | * | wp_comments | 96 KB | * | wp_links | 32 KB | * | wp_options | 32 KB | * | wp_postmeta | 48 KB | * | wp_terms | 48 KB | * | wp_term_taxonomy | 48 KB | * | wp_term_relationships | 32 KB | * | wp_termmeta | 48 KB | * | wp_commentmeta | 48 KB | * +-----------------------+-------+ * * $ wp db size --size_format=b * 5865472 * * $ wp db size --size_format=kb * 5728 * * $ wp db size --size_format=mb * 6 * * @when after_wp_load */ public function size( $args, $assoc_args ) { global $wpdb; $format = Utils\get_flag_value( $assoc_args, 'format' ); $size_format = Utils\get_flag_value( $assoc_args, 'size_format' ); $human_readable = Utils\get_flag_value( $assoc_args, 'human-readable', false ); $tables = Utils\get_flag_value( $assoc_args, 'tables' ); $tables = ! empty( $tables ); $all_tables = Utils\get_flag_value( $assoc_args, 'all-tables' ); $all_tables_with_prefix = Utils\get_flag_value( $assoc_args, 'all-tables-with-prefix' ); if ( ! is_null( $size_format ) && $human_readable ) { WP_CLI::error( 'Cannot use --size_format and --human-readable arguments at the same time.' ); } unset( $assoc_args['format'] ); unset( $assoc_args['size_format'] ); unset( $assoc_args['human-readable'] ); unset( $assoc_args['tables'] ); if ( empty( $args ) && empty( $assoc_args ) ) { $assoc_args['scope'] = 'all'; } // Build rows for the formatter. $rows = []; $fields = [ 'Name', 'Size' ]; $default_unit = ( empty( $size_format ) && ! $human_readable ) ? ' B' : ''; if ( $tables || $all_tables || $all_tables_with_prefix ) { // Add all of the table sizes. foreach ( Utils\wp_get_table_names( $args, $assoc_args ) as $table_name ) { // Get the table size. $table_bytes = $wpdb->get_var( $wpdb->prepare( 'SELECT SUM(data_length + index_length) FROM information_schema.TABLES where table_schema = %s and Table_Name = %s GROUP BY Table_Name LIMIT 1', DB_NAME, $table_name ) ); // Add the table size to the list. $rows[] = [ 'Name' => $table_name, 'Size' => strtoupper( $table_bytes ) . $default_unit, ]; } } else { // Get the database size. $db_bytes = $wpdb->get_var( $wpdb->prepare( 'SELECT SUM(data_length + index_length) FROM information_schema.TABLES where table_schema = %s GROUP BY table_schema;', DB_NAME ) ); // Add the database size to the list. $rows[] = [ 'Name' => DB_NAME, 'Size' => strtoupper( $db_bytes ) . $default_unit, ]; } if ( ! empty( $size_format ) || $human_readable ) { foreach ( $rows as $index => $row ) { // phpcs:disable WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedConstantFound -- Backfilling WP native constants. if ( ! defined( 'KB_IN_BYTES' ) ) { define( 'KB_IN_BYTES', 1024 ); } if ( ! defined( 'MB_IN_BYTES' ) ) { define( 'MB_IN_BYTES', 1024 * KB_IN_BYTES ); } if ( ! defined( 'GB_IN_BYTES' ) ) { define( 'GB_IN_BYTES', 1024 * MB_IN_BYTES ); } if ( ! defined( 'TB_IN_BYTES' ) ) { define( 'TB_IN_BYTES', 1024 * GB_IN_BYTES ); } // phpcs:enable if ( $human_readable ) { $size_key = floor( log( $row['Size'] ) / log( 1000 ) ); $sizes = [ 'B', 'KB', 'MB', 'GB', 'TB' ]; $size_format = isset( $sizes[ $size_key ] ) ? $sizes[ $size_key ] : $sizes[0]; } // Display the database size as a number. switch ( $size_format ) { case 'TB': $divisor = pow( 1000, 4 ); break; case 'GB': $divisor = pow( 1000, 3 ); break; case 'MB': $divisor = pow( 1000, 2 ); break; case 'KB': $divisor = 1000; break; case 'tb': case 'TiB': $divisor = TB_IN_BYTES; break; case 'gb': case 'GiB': $divisor = GB_IN_BYTES; break; case 'mb': case 'MiB': $divisor = MB_IN_BYTES; break; case 'kb': case 'KiB': $divisor = KB_IN_BYTES; break; case 'b': case 'B': default: $divisor = 1; break; } $size_format_display = preg_replace( '/IB$/u', 'iB', strtoupper( $size_format ) ); $decimals = Utils\get_flag_value( $assoc_args, 'decimals', 0 ); $rows[ $index ]['Size'] = round( $row['Size'] / $divisor, $decimals ) . ' ' . $size_format_display; } } if ( ! empty( $size_format ) && ! $tables && ! $format && ! $human_readable && true !== $all_tables && true !== $all_tables_with_prefix ) { WP_CLI::line( str_replace( " {$size_format_display}", '', $rows[0]['Size'] ) ); } else { // Display the rows. $args = [ 'format' => $format, ]; $formatter = new Formatter( $args, $fields ); $formatter->display_items( $rows ); } } /** * Displays the database table prefix. * * Display the database table prefix, as defined by the database handler's interpretation of the current site. * * ## EXAMPLES * * $ wp db prefix * wp_ * * @when after_wp_load */ public function prefix() { global $wpdb; WP_CLI::log( $wpdb->prefix ); } /** * Finds a string in the database. * * Searches through all of the text columns in a selection of database tables for a given string, Outputs colorized references to the string. * * Defaults to searching through all tables registered to $wpdb. On multisite, this default is limited to the tables for the current site. * * ## OPTIONS * * <search> * : String to search for. The search is case-insensitive by default. * * [<tables>...] * : One or more tables to search through for the string. * * [--network] * : Search through all the tables registered to $wpdb in a multisite install. * * [--all-tables-with-prefix] * : Search through all tables that match the registered table prefix, even if not registered on $wpdb. On one hand, sometimes plugins use tables without registering them to $wpdb. On another hand, this could return tables you don't expect. Overrides --network. * * [--all-tables] * : Search through ALL tables in the database, regardless of the prefix, and even if not registered on $wpdb. Overrides --network and --all-tables-with-prefix. * * [--before_context=<num>] * : Number of characters to display before the match. * --- * default: 40 * --- * * [--after_context=<num>] * : Number of characters to display after the match. * --- * default: 40 * --- * * [--regex] * : Runs the search as a regular expression (without delimiters). The search becomes case-sensitive (i.e. no PCRE flags are added). Delimiters must be escaped if they occur in the expression. Because the search is run on individual columns, you can use the `^` and `$` tokens to mark the start and end of a match, respectively. * * [--regex-flags=<regex-flags>] * : Pass PCRE modifiers to the regex search (e.g. 'i' for case-insensitivity). * * [--regex-delimiter=<regex-delimiter>] * : The delimiter to use for the regex. It must be escaped if it appears in the search string. The default value is the result of `chr(1)`. * * [--table_column_once] * : Output the 'table:column' line once before all matching row lines in the table column rather than before each matching row. * * [--one_line] * : Place the 'table:column' output on the same line as the row id and match ('table:column:id:match'). Overrides --table_column_once. * * [--matches_only] * : Only output the string matches (including context). No 'table:column's or row ids are outputted. * * [--stats] * : Output stats on the number of matches found, time taken, tables/columns/rows searched, tables skipped. * * [--table_column_color=<color_code>] * : Percent color code to use for the 'table:column' output. For a list of available percent color codes, see below. Default '%G' (bright green). * * [--id_color=<color_code>] * : Percent color code to use for the row id output. For a list of available percent color codes, see below. Default '%Y' (bright yellow). * * [--match_color=<color_code>] * : Percent color code to use for the match (unless both before and after context are 0, when no color code is used). For a list of available percent color codes, see below. Default '%3%k' (black on a mustard background). * * The percent color codes available are: * * | Code | Color * | ---- | ----- * | %y | Yellow (dark) (mustard) * | %g | Green (dark) * | %b | Blue (dark) * | %r | Red (dark) * | %m | Magenta (dark) * | %c | Cyan (dark) * | %w | White (dark) (light gray) * | %k | Black * | %Y | Yellow (bright) * | %G | Green (bright) * | %B | Blue (bright) * | %R | Red (bright) * | %M | Magenta (bright) * | %C | Cyan (bright) * | %W | White * | %K | Black (bright) (dark gray) * | %3 | Yellow background (dark) (mustard) * | %2 | Green background (dark) * | %4 | Blue background (dark) * | %1 | Red background (dark) * | %5 | Magenta background (dark) * | %6 | Cyan background (dark) * | %7 | White background (dark) (light gray) * | %0 | Black background * | %8 | Reverse * | %U | Underline * | %F | Blink (unlikely to work) * * They can be concatenated. For instance, the default match color of black on a mustard (dark yellow) background `%3%k` can be made black on a bright yellow background with `%Y%0%8`. * * ## EXAMPLES * * # Search through the database for the 'wordpress-develop' string * $ wp db search wordpress-develop * wp_options:option_value * 1:http://wordpress-develop.dev * wp_options:option_value * 1:https://example.com/foo * ... * * # Search through a multisite database on the subsite 'foo' for the 'example.com' string * $ wp db search example.com --url=example.com/foo * wp_2_comments:comment_author_url * 1:https://example.com/ * wp_2_options:option_value * ... * * # Search through the database for the 'https?://' regular expression, printing stats. * $ wp db search 'https?://' --regex --stats * wp_comments:comment_author_url * 1:https://wordpress.org/ * ... * Success: Found 99146 matches in 10.752s (10.559s searching). Searched 12 tables, 53 columns, 1358907 rows. 1 table skipped: wp_term_relationships. * * # SQL search database table 'wp_options' where 'option_name' match 'foo' * wp db query 'SELECT * FROM wp_options WHERE option_name like "%foo%"' --skip-column-names * +----+--------------+--------------------------------+-----+ * | 98 | foo_options | a:1:{s:12:"_multiwidget";i:1;} | yes | * | 99 | foo_settings | a:0:{} | yes | * +----+--------------+--------------------------------+-----+ * * # SQL search and delete records from database table 'wp_options' where 'option_name' match 'foo' * wp db query "DELETE from wp_options where option_id in ($(wp db query "SELECT GROUP_CONCAT(option_id SEPARATOR ',') from wp_options where option_name like '%foo%';" --silent --skip-column-names))" * * @when after_wp_load */ public function search( $args, $assoc_args ) { global $wpdb; $start_run_time = microtime( true ); $search = array_shift( $args ); $before_context = Utils\get_flag_value( $assoc_args, 'before_context', 40 ); $before_context = '' === $before_context ? $before_context : (int) $before_context; $after_context = Utils\get_flag_value( $assoc_args, 'after_context', 40 ); $after_context = '' === $after_context ? $after_context : (int) $after_context; $regex = Utils\get_flag_value( $assoc_args, 'regex', false ); if ( false !== $regex ) { $regex_flags = Utils\get_flag_value( $assoc_args, 'regex-flags', false ); $default_regex_delimiter = false; $regex_delimiter = Utils\get_flag_value( $assoc_args, 'regex-delimiter', '' ); if ( '' === $regex_delimiter ) { $regex_delimiter = chr( 1 ); $default_regex_delimiter = true; } } $colors = self::get_colors( $assoc_args, [ 'table_column' => '%G', 'id' => '%Y', 'match' => $before_context || $after_context ? '%3%k' : '', ] ); $table_column_once = Utils\get_flag_value( $assoc_args, 'table_column_once', false ); $one_line = Utils\get_flag_value( $assoc_args, 'one_line', false ); $matches_only = Utils\get_flag_value( $assoc_args, 'matches_only', false ); $stats = Utils\get_flag_value( $assoc_args, 'stats', false ); $column_count = 0; $row_count = 0; $match_count = 0; $skipped = []; if ( $regex ) { // Note the user must escape the delimiter in the search. $search_regex = $regex_delimiter . $search . $regex_delimiter; if ( $regex_flags ) { $search_regex .= $regex_flags; } // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged -- Unsure why this is needed, leaving in for now. if ( false === @preg_match( $search_regex, '' ) ) { if ( $default_regex_delimiter ) { $flags_msg = $regex_flags ? "flags '$regex_flags'" : 'no flags'; $msg = "The regex pattern '$search' with default delimiter 'chr(1)' and {$flags_msg} fails."; } else { $msg = "The regex '$search_regex' fails."; } WP_CLI::error( $msg ); } } else { $search_regex = '#' . preg_quote( $search, '#' ) . '#i'; $esc_like_search = '%' . Utils\esc_like( $search ) . '%'; } $encoding = null; if ( 0 === strpos( $wpdb->charset, self::ENCODING_UTF8 ) ) { $encoding = 'UTF-8'; } $tables = Utils\wp_get_table_names( $args, $assoc_args ); $start_search_time = microtime( true ); foreach ( $tables as $table ) { list( $primary_keys, $text_columns, $all_columns ) = self::get_columns( $table ); if ( ! $all_columns ) { WP_CLI::error( "No such table '$table'." ); } if ( ! $text_columns ) { if ( $stats ) { $skipped[] = $table; } else { // Don't bother warning for term relationships (which is just 3 int columns). if ( ! preg_match( '/_term_relationships$/', $table ) ) { WP_CLI::warning( $primary_keys ? "No text columns for table '$table' - skipped." : "No primary key or text columns for table '$table' - skipped." ); } } continue; } $table_sql = self::esc_sql_ident( $table ); $column_count += count( $text_columns ); if ( ! $primary_keys ) { WP_CLI::warning( "No primary key for table '$table'. No row ids will be outputted." ); $primary_key = ''; $primary_key_sql = ''; } else { $primary_key = array_shift( $primary_keys ); $primary_key_sql = self::esc_sql_ident( $primary_key ) . ', '; } foreach ( $text_columns as $column ) { $column_sql = self::esc_sql_ident( $column ); if ( $regex ) { // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Escaped through esc_sql_ident/esc_like. $results = $wpdb->get_results( "SELECT {$primary_key_sql}{$column_sql} FROM {$table_sql}" ); } else { // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Escaped through esc_sql_ident/esc_like. $results = $wpdb->get_results( $wpdb->prepare( "SELECT {$primary_key_sql}{$column_sql} FROM {$table_sql} WHERE {$column_sql} LIKE %s;", $esc_like_search ) ); } if ( $results ) { $row_count += count( $results ); $table_column_val = $colors['table_column'][0] . "{$table}:{$column}" . $colors['table_column'][1]; $outputted_table_column_once = false; foreach ( $results as $result ) { $col_val = $result->$column; if ( preg_match_all( $search_regex, $col_val, $matches, PREG_OFFSET_CAPTURE ) ) { if ( ! $matches_only && ( ! $table_column_once || ! $outputted_table_column_once ) && ! $one_line ) { WP_CLI::log( $table_column_val ); $outputted_table_column_once = true; } $pk_val = $primary_key ? ( $colors['id'][0] . $result->$primary_key . $colors['id'][1] . ':' ) : ''; $bits = []; $col_encoding = $encoding; if ( ! $col_encoding && ( $before_context || $after_context ) && function_exists( 'mb_detect_encoding' ) ) { $col_encoding = mb_detect_encoding( $col_val, null, true /*strict*/ ); } $append_next = false; $last_offset = 0; $match_cnt = count( $matches[0] ); for ( $i = 0; $i < $match_cnt; $i++ ) { $match = $matches[0][ $i ][0]; $offset = $matches[0][ $i ][1]; $log = $colors['match'][0] . $match . $colors['match'][1]; $before = ''; $after = ''; $after_shortened = false; // Offsets are in bytes, so need to use `strlen()` and `substr()` before using `safe_substr()`. if ( $before_context && $offset && ! $append_next ) { $before = \cli\safe_substr( substr( $col_val, $last_offset, $offset - $last_offset ), -$before_context, null /*length*/, false /*is_width*/, $col_encoding ); } if ( $after_context ) { $end_offset = $offset + strlen( $match ); $after = \cli\safe_substr( substr( $col_val, $end_offset ), 0, $after_context, false /*is_width*/, $col_encoding ); // To lessen context duplication in output, shorten the after context if it overlaps with the next match. if ( $i + 1 < $match_cnt && $end_offset + strlen( $after ) > $matches[0][ $i + 1 ][1] ) { $after = substr( $after, 0, $matches[0][ $i + 1 ][1] - $end_offset ); $after_shortened = true; // On the next iteration, will append with no before context. } } if ( $append_next ) { $cnt = count( $bits ); $bits[ $cnt - 1 ] .= $log . $after; } else { $bits[] = $before . $log . $after; } $append_next = $after_shortened; $last_offset = $offset; } $match_count += $match_cnt; $col_val = implode( ' [...] ', $bits ); WP_CLI::log( $matches_only ? $col_val : ( $one_line ? "{$table_column_val}:{$pk_val}{$col_val}" : "{$pk_val}{$col_val}" ) ); } } } } } if ( $stats ) { $table_count = count( $tables ); $skipped_count = count( $skipped ); $match_str = 1 === $match_count ? 'match' : 'matches'; $table_str = 1 === $table_count ? 'table' : 'tables'; $column_str = 1 === $column_count ? 'column' : 'columns'; $row_str = 1 === $row_count ? 'row' : 'rows'; $skipped_str = 1 === $skipped_count ? 'table skipped' : 'tables skipped'; if ( 0 !== $skipped_count ) { $skipped_str .= ': ' . implode( ', ', $skipped ); } $end_time = microtime( true ); $run_time = $end_time - $start_run_time; $search_time = $end_time - $start_search_time; $stats_msg = sprintf( 'Found %d %s in %.3fs (%.3fs searching). Searched %d %s, %d %s, %d %s. %d %s.', $match_count, $match_str, $run_time, $search_time, $table_count, $table_str, $column_count, $column_str, $row_count, $row_str, $skipped_count, $skipped_str ); WP_CLI::success( $stats_msg ); } } /** * Displays information about a given table. * * ## OPTIONS * * <table> * : Name of the database table. * * [--format] * : Render output in a particular format. * --- * default: table * options: * - table * - csv * - json * - yaml * --- * * ## EXAMPLES * * $ wp db columns wp_posts * +-----------------------+---------------------+------+-----+---------------------+----------------+ * | Field | Type | Null | Key | Default | Extra | * +-----------------------+---------------------+------+-----+---------------------+----------------+ * | ID | bigint(20) unsigned | NO | PRI | | auto_increment | * | post_author | bigint(20) unsigned | NO | MUL | 0 | | * | post_date | datetime | NO | | 0000-00-00 00:00:00 | | * | post_date_gmt | datetime | NO | | 0000-00-00 00:00:00 | | * | post_content | longtext | NO | | | | * | post_title | text | NO | | | | * | post_excerpt | text | NO | | | | * | post_status | varchar(20) | NO | | publish | | * | comment_status | varchar(20) | NO | | open | | * | ping_status | varchar(20) | NO | | open | | * | post_password | varchar(255) | NO | | | | * | post_name | varchar(200) | NO | MUL | | | * | to_ping | text | NO | | | | * | pinged | text | NO | | | | * | post_modified | datetime | NO | | 0000-00-00 00:00:00 | | * | post_modified_gmt | datetime | NO | | 0000-00-00 00:00:00 | | * | post_content_filtered | longtext | NO | | | | * | post_parent | bigint(20) unsigned | NO | MUL | 0 | | * | guid | varchar(255) | NO | | | | * | menu_order | int(11) | NO | | 0 | | * | post_type | varchar(20) | NO | MUL | post | | * | post_mime_type | varchar(100) | NO | | | | * | comment_count | bigint(20) | NO | | 0 | | * +-----------------------+---------------------+------+-----+---------------------+----------------+ * * @when after_wp_load */ public function columns( $args, $assoc_args ) { global $wpdb; $format = Utils\get_flag_value( $assoc_args, 'format' ); Utils\wp_get_table_names( [ $args[0] ], [ 'all-tables' => true ] ); $columns = $wpdb->get_results( // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- Asserted to be a valid table name through wp_get_table_names. 'SHOW COLUMNS FROM ' . $args[0] ); $formatter_fields = [ 'Field', 'Type', 'Null', 'Key', 'Default', 'Extra' ]; $formatter_args = [ 'format' => $format, ]; $formatter = new Formatter( $formatter_args, $formatter_fields ); $formatter->display_items( $columns ); } private static function get_create_query() { $create_query = sprintf( 'CREATE DATABASE %s', self::esc_sql_ident( DB_NAME ) ); if ( defined( 'DB_CHARSET' ) && constant( 'DB_CHARSET' ) ) { $create_query .= sprintf( ' DEFAULT CHARSET %s', self::esc_sql_ident( DB_CHARSET ) ); } if ( defined( 'DB_COLLATE' ) && constant( 'DB_COLLATE' ) ) { $create_query .= sprintf( ' DEFAULT COLLATE %s', self::esc_sql_ident( DB_COLLATE ) ); } return $create_query; } /** * Run a single query via the 'mysql' binary. * * This includes the necessary setup to make sure the queries behave similar * to what WPDB produces. * * @param string $query Query to execute. * @param array $assoc_args Optional. Associative array of arguments. */ protected function run_query( $query, $assoc_args = [] ) { // Ensure that the SQL mode is compatible with WPDB. $query = $this->get_sql_mode_query( $assoc_args ) . $query; WP_CLI::debug( "Query: {$query}", 'db' ); $mysql_args = array_merge( self::get_dbuser_dbpass_args( $assoc_args ), self::get_mysql_args( $assoc_args ) ); self::run( sprintf( '/usr/bin/env mysql%s --no-auto-rehash', $this->get_defaults_flag_string( $assoc_args ) ), array_merge( [ 'execute' => $query ], $mysql_args ) ); } /** * Run a MySQL command and optionally return the output. * * @param string $cmd Command to run. * @param array $assoc_args Optional. Associative array of arguments to * use. * @param bool $send_to_shell Optional. Whether to send STDOUT and STDERR * immediately to the shell. Defaults to true. * @param bool $interactive Optional. Whether MySQL is meant to be * executed as an interactive process. Defaults * to false. * * @return array { * Associative array containing STDOUT and STDERR output. * * @type string $stdout Output that was sent to STDOUT. * @type string $stderr Output that was sent to STDERR. * @type int $exit_code Exit code of the process. * } */ private static function run( $cmd, $assoc_args = [], $send_to_shell = true, $interactive = false ) { $required = [ 'host' => DB_HOST, 'user' => DB_USER, 'pass' => DB_PASSWORD, ]; if ( ! isset( $assoc_args['default-character-set'] ) && defined( 'DB_CHARSET' ) && constant( 'DB_CHARSET' ) ) { $required['default-character-set'] = constant( 'DB_CHARSET' ); } // Using 'dbuser' as option name to workaround clash with WP-CLI's global WP 'user' parameter, with 'dbpass' also available for tidyness. if ( isset( $assoc_args['dbuser'] ) ) { $required['user'] = $assoc_args['dbuser']; unset( $assoc_args['dbuser'] ); } if ( isset( $assoc_args['dbpass'] ) ) { $required['pass'] = $assoc_args['dbpass']; unset( $assoc_args['dbpass'], $assoc_args['password'] ); } $final_args = array_merge( $required, $assoc_args ); // Adapt ordering of arguments. uksort( $final_args, static function ( $a, $b ) { switch ( $b ) { case 'force': return -1; default: return 1; } } ); return Utils\run_mysql_command( $cmd, $final_args, null, $send_to_shell, $interactive ); } /** * Helper to pluck 'dbuser' and 'dbpass' from associative args array. * * @param array $assoc_args Associative args array. * @return array Array with `dbuser' and 'dbpass' set if in passed-in associative args array. */ private static function get_dbuser_dbpass_args( $assoc_args ) { $mysql_args = []; $dbuser = Utils\get_flag_value( $assoc_args, 'dbuser' ); if ( null !== $dbuser ) { $mysql_args['dbuser'] = $dbuser; } $dbpass = Utils\get_flag_value( $assoc_args, 'dbpass' ); if ( null !== $dbpass ) { $mysql_args['dbpass'] = $dbpass; } return $mysql_args; } /** * Gets the column names of a db table differentiated into key columns and text columns and all columns. * * @param string $table The table name. * @return array A 3 element array consisting of an array of primary key column names, an array of text column names, and an array containing all column names. */ private static function get_columns( $table ) { global $wpdb; $table_sql = self::esc_sql_ident( $table ); $primary_keys = []; $text_columns = []; $all_columns = []; $suppress_errors = $wpdb->suppress_errors(); // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Escaped through esc_sql_ident/esc_like. $results = $wpdb->get_results( "DESCRIBE $table_sql" ); if ( $results ) { foreach ( $results as $col ) { // phpcs:disable WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase -- Property names come from database. if ( 'PRI' === $col->Key ) { $primary_keys[] = $col->Field; } if ( self::is_text_col( $col->Type ) ) { $text_columns[] = $col->Field; } $all_columns[] = $col->Field; // phpcs:enable } } $wpdb->suppress_errors( $suppress_errors ); return [ $primary_keys, $text_columns, $all_columns ]; } /** * Determines whether a column is considered text or not. * * @param string Column type. * @return bool True if text column, false otherwise. */ private static function is_text_col( $type ) { foreach ( [ 'text', 'varchar' ] as $token ) { if ( false !== strpos( $type, $token ) ) { return true; } } return false; } /** * Escapes (backticks) MySQL identifiers (aka schema object names) - i.e. column names, table names, and database/index/alias/view etc names. * See https://dev.mysql.com/doc/refman/5.5/en/identifiers.html * * @param string|array $idents A single identifier or an array of identifiers. * @return string|array An escaped string if given a string, or an array of escaped strings if given an array of strings. */ private static function esc_sql_ident( $idents ) { $backtick = static function ( $v ) { // Escape any backticks in the identifier by doubling. return '`' . str_replace( '`', '``', $v ) . '`'; }; if ( is_string( $idents ) ) { return $backtick( $idents ); } return array_map( $backtick, $idents ); } /** * Gets the color codes from the options if any, and returns the passed in array colorized with 2 elements per entry, a color code (or '') and a reset (or ''). * * @param array $assoc_args The associative argument array passed to the command. * @param array $colors Array of default percent color code strings keyed by the 3 color contexts 'table_column', 'id', 'match'. * @return array Array containing 3 2-element arrays. */ private function get_colors( $assoc_args, $colors ) { $color_reset = WP_CLI::colorize( '%n' ); $color_codes = implode( '', array_map( function ( $v ) { return substr( $v, 1 ); }, array_keys( \cli\Colors::getColors() ) ) ); $color_codes_regex = '/^(?:%[' . $color_codes . '])*$/'; foreach ( array_keys( $colors ) as $color_col ) { $col_color_flag = Utils\get_flag_value( $assoc_args, $color_col . '_color', false ); if ( false !== $col_color_flag ) { if ( ! preg_match( $color_codes_regex, $col_color_flag, $matches ) ) { WP_CLI::warning( "Unrecognized percent color code '$col_color_flag' for '{$color_col}_color'." ); } else { $colors[ $color_col ] = $matches[0]; } } $colors[ $color_col ] = $colors[ $color_col ] ? [ WP_CLI::colorize( $colors[ $color_col ] ), $color_reset ] : [ '', '' ]; } return $colors; } /** * Helper to pluck `mysql` options from associative args array. * * @param array $assoc_args Associative args array. * @return array Array with `mysql` options set if in passed-in associative args array. */ private static function get_mysql_args( $assoc_args ) { $allowed_mysql_options = [ 'auto-rehash', 'auto-vertical-output', 'batch', 'binary-as-hex', 'binary-mode', 'bind-address', 'character-sets-dir', 'column-names', 'column-type-info', 'comments', 'compress', 'connect-expired-password', 'connect_timeout', 'database', 'debug', 'debug-check', 'debug-info', 'default-auth', 'default-character-set', 'defaults-extra-file', 'defaults-file', 'defaults-group-suffix', 'delimiter', 'enable-cleartext-plugin', 'execute', 'force', 'get-server-public-key', 'help', 'histignore', 'host', 'html', 'ignore-spaces', 'init-command', 'line-numbers', 'local-infile', 'login-path', 'max_allowed_packet', 'max_join_size', 'named-commands', 'net_buffer_length', 'no-beep', 'one-database', 'pager', 'pipe', 'plugin-dir', 'port', 'print-defaults', 'protocol', 'quick', 'raw', 'reconnect', 'i-am-a-dummy', 'safe-updates', 'secure-auth', 'select_limit', 'server-public-key-path', 'shared-memory-base-name', 'show-warnings', 'sigint-ignore', 'silent', 'skip-auto-rehash', 'skip-column-names', 'skip-line-numbers', 'skip-named-commands', 'skip-pager', 'skip-reconnect', 'socket', 'ssl-ca', 'ssl-capath', 'ssl-cert', 'ssl-cipher', 'ssl-crl', 'ssl-crlpath', 'ssl-fips-mode', 'ssl-key', 'ssl-mode', 'syslog', 'table', 'tee', 'tls-version', 'unbuffered', 'verbose', 'version', 'vertical', 'wait', 'xml', ]; $mysql_args = []; foreach ( $assoc_args as $mysql_option_key => $mysql_option_value ) { // Check flags to make sure they only contain valid options. if ( in_array( $mysql_option_key, $allowed_mysql_options, true ) && ! empty( $mysql_option_value ) ) { $mysql_args[ $mysql_option_key ] = $mysql_option_value; } } return $mysql_args; } /** * Writes out the `--no-defaults` flag for MySQL commands unless the --defaults flag is specified for the WP_CLI command. * * @param array $assoc_args Associative args array. * @return string Either the '--no-defaults' flag for use in the command or an empty string. */ protected function get_defaults_flag_string( &$assoc_args ) { $flag_string = ' --no-defaults'; if ( array_key_exists( 'defaults', $assoc_args ) ) { if ( true === Utils\get_flag_value( $assoc_args, 'defaults' ) ) { $flag_string = ''; } unset( $assoc_args['defaults'] ); } return $flag_string; } /** * Get the query to change the current SQL mode, and ensure its WordPress compatibility. * * If no modes are passed, it will ensure the current MySQL server modes are * compatible. * * Copied and adapted from WordPress Core code. * * @see https://github.com/WordPress/wordpress-develop/blob/5.4.0/src/wp-includes/wp-db.php#L817-L880 * * @param array $assoc_args The associative argument array passed to the command. * @param array $modes Optional. A list of SQL modes to set. * @return string Query string to use for setting the SQL modes to a * compatible state. */ protected function get_sql_mode_query( $assoc_args, $modes = [] ) { if ( empty( $modes ) ) { $modes = $this->get_current_sql_modes( $assoc_args ); } $modes = array_change_key_case( $modes, CASE_UPPER ); $is_mode_adaptation_needed = false; foreach ( $modes as $i => $mode ) { if ( in_array( $mode, $this->sql_incompatible_modes, true ) ) { unset( $modes[ $i ] ); $is_mode_adaptation_needed = true; } } if ( ! $is_mode_adaptation_needed ) { WP_CLI::debug( sprintf( 'SQL modes look fine: %s', json_encode( $modes ) ) ); return ''; } WP_CLI::debug( sprintf( 'SQL mode adaptation is needed: %s => %s', json_encode( $this->get_current_sql_modes( $assoc_args ) ), json_encode( $modes ) ) ); $modes_str = implode( ',', $modes ); return "SET SESSION sql_mode='{$modes_str}';"; } /** * Get the list of current SQL modes. * * @param array $assoc_args The associative argument array passed to the command. * @return string[] Array of SQL modes. */ protected function get_current_sql_modes( $assoc_args ) { static $modes = null; // Make sure the provided arguments don't interfere with the expected // output here. $args = []; foreach ( [] as $arg ) { if ( isset( $assoc_args[ $arg ] ) ) { $args[ $arg ] = $assoc_args[ $arg ]; } } if ( null === $modes ) { $modes = []; list( $stdout, $stderr, $exit_code ) = self::run( sprintf( '/usr/bin/env mysql%s --no-auto-rehash --batch --skip-column-names', $this->get_defaults_flag_string( $assoc_args ) ), array_merge( $args, [ 'execute' => 'SELECT @@SESSION.sql_mode' ] ), false ); if ( $exit_code ) { WP_CLI::error( 'Failed to get current SQL modes.' . ( ! empty( $stderr ) ? " Reason: {$stderr}" : '' ), $exit_code ); } if ( ! empty( $stdout ) ) { $modes = array_filter( array_map( 'trim', preg_split( "/\r\n|\n|\r|,/", $stdout ) ) ); } if ( false === $modes ) { $modes = []; } } return $modes; } } <?php if ( ! class_exists( 'WP_CLI' ) ) { return; } $wpcli_db_autoloader = dirname( __FILE__ ) . '/vendor/autoload.php'; if ( file_exists( $wpcli_db_autoloader ) ) { require_once $wpcli_db_autoloader; } WP_CLI::add_command( 'db', 'DB_Command' ); <?php class Oxymel { private $xml; private $dom; private $current_element; private $last_inserted; private $go_deep_on_next_element = 0; private $go_up_on_next_element = 0; private $nesting_level = 0; private $contains_nesting_level = 0; private $indentation= ' '; public function __construct() { $this->xml = ''; $this->init_new_dom(); } public function to_string() { return $this->xml .= $this->indent( $this->xml_from_dom(), $this->nesting_level ); } public function __call( $name, $args ) { array_unshift( $args, $name ); return call_user_func_array( array( $this, 'tag' ), $args ); } public function __get( $name ) { return $this->$name(); } public function contains() { $this->contains_nesting_level++; $this->nesting_level++; if ( $this->go_deep_on_next_element ) { throw new OxymelException( 'contains cannot be used consecutively more than once' ); } $this->go_deep_on_next_element++; return $this; } public function end() { $this->contains_nesting_level--; $this->nesting_level--; if ( $this->contains_nesting_level < 0 ) { throw new OxymelException( 'end is used without a matching contains' ); } $this->go_up_on_next_element++; return $this; } public function tag( $name, $content_or_attributes = null, $attributes = array() ) { list( $content, $attributes ) = $this->get_content_and_attributes_from_tag_args( $content_or_attributes, $attributes ); $is_opening = 0 === strpos( $name, 'open_' ); $is_closing = 0 === strpos( $name, 'close_' ); $name = preg_replace("/^(open|close)_/", '', $name ); $element = $this->create_element( $name, $content, $attributes ); if ( !$is_opening && !$is_closing ) $this->add_element_to_dom( $element ); elseif ( $is_opening ) $this->add_opening_tag_from_element( $element ); elseif ( $is_closing ) $this->add_closing_tag_from_tag_name( $name ); return $this; } public function cdata( $text ) { $this->add_element_to_dom( $this->dom->createCDATASection( $text ) ); return $this; } public function text( $text ) { $this->add_element_to_dom( $this->dom->createTextNode( $text ) ); return $this; } public function comment( $text ) { $this->add_element_to_dom( $this->dom->createComment( $text ) ); return $this; } public function xml() { $this->add_element_to_dom( $this->dom->createProcessingInstruction( 'xml', 'version="1.0" encoding="UTF-8"' ) ); return $this; } public function oxymel( Oxymel $other ) { foreach( $other->dom->childNodes as $child ) { $child = $this->dom->importNode( $child, true ); $this->add_element_to_dom( $child ); } return $this; } public function raw( $raw_xml ) { if ( !$raw_xml ) { return $this; } $fragment = $this->dom->createDocumentFragment(); $fragment->appendXML($raw_xml); $this->add_element_to_dom( $fragment ); return $this; } private function add_element_to_dom( $element ) { $this->move_current_element_deep(); $this->move_current_element_up(); $this->last_inserted = $this->current_element->appendChild($element); } private function move_current_element_deep() { if ( $this->go_deep_on_next_element ) { if ( !$this->last_inserted ) { throw new OxymelException( 'contains has been used before adding any tags' ); } $this->current_element = $this->last_inserted; $this->go_deep_on_next_element--; } } private function move_current_element_up() { if ( $this->go_up_on_next_element ) { while ( $this->go_up_on_next_element ) { $this->current_element = $this->current_element->parentNode; $this->go_up_on_next_element--; } } } private function get_content_and_attributes_from_tag_args( $content_or_attributes, array $attributes ) { $content = null; if ( !$attributes ) { if ( is_array( $content_or_attributes ) ) $attributes = $content_or_attributes; else $content = $content_or_attributes; } else { $content = $content_or_attributes; } return array( $content, $attributes ); } private function init_new_dom() { unset( $this->dom, $this->current_element ); $this->dom = new DOMDocument(); $this->dom->formatOutput = true; $this->current_element = $this->dom; $this->last_inserted = null; } private function xml_from_dom() { if ( 0 !== $this->contains_nesting_level ) { throw new OxymelException( 'contains and end calls do not match' ); } $xml = ''; foreach( $this->dom->childNodes as $child ) { $xml .= $this->dom->saveXML( $child ) . "\n"; } return $xml; } private function create_element( $name, $content, $attributes ) { if ( !is_null( $content ) ) $element = $this->dom->createElement( $name, $content ); else $element = $this->dom->createElement( $name ); foreach( $attributes as $attribute_name => $attribute_value ) { $element->setAttribute( $attribute_name, $attribute_value ); } return $element; } private function add_opening_tag_from_element( $element ) { $this->xml .= $this->indent( $this->xml_from_dom(), $this->nesting_level ); $tag = $this->dom->saveXML($element); $this->xml .= $this->indent( str_replace( '/>', '>', $tag ) . "\n", $this->nesting_level ); $this->nesting_level++; $this->init_new_dom(); } private function add_closing_tag_from_tag_name( $name ) { $this->xml .= $this->xml_from_dom(); $this->nesting_level--; if ( $this->nesting_level < 0 ) { $this->xml = $this->indent( $this->xml, -$this->nesting_level ); $this->nesting_level = 0; } $this->xml .= $this->indent( "</$name>\n", $this->nesting_level ); $this->init_new_dom(); } private function indent( $string, $level ) { if ( !$level ) { return $string; } $lines = explode( "\n", $string ); foreach( $lines as &$line ) { if ( !trim( $line ) ) continue; $line = str_repeat( $this->indentation, $level ) . $line; } return implode( "\n", $lines ); } } class OxymelException extends Exception { } <?php require_once dirname( __FILE__ ) . '/Oxymel.php'; class OxymelTest extends PHPUnit_Framework_TestCase { function __construct() { $this->x = new Oxymel; } function test_xml_adds_procesing_instrution_with_version_and_encoding() { $this->a( '<?xml version="1.0" encoding="UTF-8"?>', $this->x->xml ); } function test_self_closing() { $this->a('<baba/>', $this->x->baba); } function test_self_closing_method() { $this->a('<baba/>', $this->x->baba()); } function test_attribute_no_content() { $this->a('<baba a="b" c="d"/>', $this->x->baba( array( 'a' => 'b', 'c' => 'd' ) ) ); } function test_content_no_attribute() { $this->a('<baba>content</baba>', $this->x->baba( 'content' ) ); } function test_escapiing_of_content() { $this->a('<baba><</baba>', $this->x->baba( '<' ) ); } function test_escaping_of_attributes() { $this->a('<baba a="<"/>', $this->x->baba( array( 'a' => '<' ) ) ); } function test_content_and_attributes() { $this->a('<baba a="b" c="d">content</baba>', $this->x->baba( 'content', array( 'a' => 'b', 'c' => 'd' ) ) ); } function test_content_and_attributes_error() { $this->setExpectedException( 'PHPUnit_Framework_Error' ); $this->a('<baba a="b" c="d">content</baba>', $this->x->baba( array( 'a' => 'b', 'c' => 'd' ), 'content' ) ); } function test_go_down() { $this->a('<baba> <dyado/> </baba>', $this->x->baba->contains->dyado->end ); } function test_go_down_and_up() { $this->a("<level0> <level1/> </level0> <level0/>", $this->x->level0->contains->level1->end->level0 ); } function test_cdata() { $this->a('<baba><![CDATA[content]]></baba>', $this->x->baba->contains->cdata('content')->end); } function test_nested_cdata() { $this->a( '<![CDATA[x]]]]><![CDATA[>]]>', $this->x->cdata( 'x]]>' ) ); } function test_raw() { $this->a('<baba> <dyado/> </baba>', $this->x->baba->contains->raw('<dyado></dyado>')->end); } function test_raw_doesnt_do_anything_with_empty_arg() { $this->a('<baba/>', $this->x->baba->raw( '' ) ); } function test_oxymel_ns() { $oxymel = new Oxymel; $oxymel->tag( 'wp:baba' ); $this->a( '<wp:baba/>', $this->x->oxymel( $oxymel ) ); } function x_test_only_up_error() { $this->a('', $this->x->end ); } function test_open_in_the_end() { $this->a("<baba/> <newtag>", $this->x->baba->open_newtag ); } function test_open_with_attributes() { $this->a("<baba/> <newtag a=\"b\">", $this->x->baba->open_newtag( array( 'a' => 'b' ) ) ); } function test_dom_open_dom() { $this->a("<baba/> <newtag> <baba/>", $this->x->baba->open_newtag->baba ); } function test_close_in_the_beginning() { $this->a("</oldtag> <baba/>", $this->x->close_oldtag->baba ); } function test_dom_close_dom() { $this->a(" <baba/> </oldtag> <baba/>", $this->x->baba->close_oldtag->baba ); } function test_baba() { $this->a("<a>baba<x/></a>", $this->x->a('baba')->contains->x->end ); } function test_comment() { $this->a('<!--baba-->', $this->x->comment('baba')); } function test_two_invocations() { $this->x->baba(); $this->x->dyado(); $this->a( "<baba/> <dyado/>", $this->x ); } function test_double_end() { $this->a( "<out> <mid> <in/> </mid> </out> <nextout/>", $this->x->out->contains->mid->contains->in->end->end->nextout ); } function test_end_without_contains() { $this->setExpectedException( 'OxymelException' ); $this->x->end->baba; } function test_leading_contains() { $this->setExpectedException( 'OxymelException' ); $this->a( '<baba/>', $this->x->contains->baba ); } function test_consecutive_contains_should_error() { $this->setExpectedException( 'OxymelException' ); $this->x->baba->contains->contains->dyado->end->wink; } function test_end_without_mathcing_contains_but_with_enough_parents() { $this->setExpectedException( 'OxymelException' ); $this->x->contains->baba->end->end->baba; } function test_contains_after_newly_initialized_dom_should_error() { $this->setExpectedException( 'OxymelException' ); $this->x->baba->open_rss->contains->baba->end; } function test_nested_open_tags_should_be_indented() { $this->a( "<baba> <baba>", $this->x->open_baba->open_baba ); } function test_normal_xml_between_two_opening_tags_should_be_indented() { $this->a( "<baba> <baba/> <baba>", $this->x->open_baba->baba->open_baba ); } function test_normal_xml_after_opening_tag_should_be_indented() { $this->a( "<baba> <baba/>", $this->x->open_baba->baba ); } function test_normal_xml_before_closing_tag_should_be_indented() { $this->a( " <baba/> </baba>", $this->x->baba->close_baba ); } function test_text_should_escape_the_content_inside() { $this->a( 'a & b', $this->x->text( 'a & b' ) ); } function test_dangling_contains_should_be_caught_in_the_end() { $this->setExpectedException( 'OxymelException' ); $this->x->contains->to_string(); } private function a($value, $x) { $this->assertEquals( $value . "\n", $x->to_string()); } } <?php namespace Psr\Log; class InvalidArgumentException extends \InvalidArgumentException { } <?php namespace Psr\Log; /** * Describes a logger-aware instance. */ interface LoggerAwareInterface { /** * Sets a logger instance on the object. * * @param LoggerInterface $logger * * @return void */ public function setLogger(LoggerInterface $logger); } <?php namespace Psr\Log; /** * Basic Implementation of LoggerAwareInterface. */ trait LoggerAwareTrait { /** * The logger instance. * * @var LoggerInterface|null */ protected $logger; /** * Sets a logger. * * @param LoggerInterface $logger */ public function setLogger(LoggerInterface $logger) { $this->logger = $logger; } } <?php namespace Psr\Log; /** * This is a simple Logger implementation that other Loggers can inherit from. * * It simply delegates all log-level-specific methods to the `log` method to * reduce boilerplate code that a simple Logger that does the same thing with * messages regardless of the error level has to implement. */ abstract class AbstractLogger implements LoggerInterface { /** * System is unusable. * * @param string $message * @param mixed[] $context * * @return void */ public function emergency($message, array $context = array()) { $this->log(LogLevel::EMERGENCY, $message, $context); } /** * Action must be taken immediately. * * Example: Entire website down, database unavailable, etc. This should * trigger the SMS alerts and wake you up. * * @param string $message * @param mixed[] $context * * @return void */ public function alert($message, array $context = array()) { $this->log(LogLevel::ALERT, $message, $context); } /** * Critical conditions. * * Example: Application component unavailable, unexpected exception. * * @param string $message * @param mixed[] $context * * @return void */ public function critical($message, array $context = array()) { $this->log(LogLevel::CRITICAL, $message, $context); } /** * Runtime errors that do not require immediate action but should typically * be logged and monitored. * * @param string $message * @param mixed[] $context * * @return void */ public function error($message, array $context = array()) { $this->log(LogLevel::ERROR, $message, $context); } /** * Exceptional occurrences that are not errors. * * Example: Use of deprecated APIs, poor use of an API, undesirable things * that are not necessarily wrong. * * @param string $message * @param mixed[] $context * * @return void */ public function warning($message, array $context = array()) { $this->log(LogLevel::WARNING, $message, $context); } /** * Normal but significant events. * * @param string $message * @param mixed[] $context * * @return void */ public function notice($message, array $context = array()) { $this->log(LogLevel::NOTICE, $message, $context); } /** * Interesting events. * * Example: User logs in, SQL logs. * * @param string $message * @param mixed[] $context * * @return void */ public function info($message, array $context = array()) { $this->log(LogLevel::INFO, $message, $context); } /** * Detailed debug information. * * @param string $message * @param mixed[] $context * * @return void */ public function debug($message, array $context = array()) { $this->log(LogLevel::DEBUG, $message, $context); } } <?php namespace Psr\Log; /** * Describes a logger instance. * * The message MUST be a string or object implementing __toString(). * * The message MAY contain placeholders in the form: {foo} where foo * will be replaced by the context data in key "foo". * * The context array can contain arbitrary data. The only assumption that * can be made by implementors is that if an Exception instance is given * to produce a stack trace, it MUST be in a key named "exception". * * See https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-3-logger-interface.md * for the full interface specification. */ interface LoggerInterface { /** * System is unusable. * * @param string $message * @param mixed[] $context * * @return void */ public function emergency($message, array $context = array()); /** * Action must be taken immediately. * * Example: Entire website down, database unavailable, etc. This should * trigger the SMS alerts and wake you up. * * @param string $message * @param mixed[] $context * * @return void */ public function alert($message, array $context = array()); /** * Critical conditions. * * Example: Application component unavailable, unexpected exception. * * @param string $message * @param mixed[] $context * * @return void */ public function critical($message, array $context = array()); /** * Runtime errors that do not require immediate action but should typically * be logged and monitored. * * @param string $message * @param mixed[] $context * * @return void */ public function error($message, array $context = array()); /** * Exceptional occurrences that are not errors. * * Example: Use of deprecated APIs, poor use of an API, undesirable things * that are not necessarily wrong. * * @param string $message * @param mixed[] $context * * @return void */ public function warning($message, array $context = array()); /** * Normal but significant events. * * @param string $message * @param mixed[] $context * * @return void */ public function notice($message, array $context = array()); /** * Interesting events. * * Example: User logs in, SQL logs. * * @param string $message * @param mixed[] $context * * @return void */ public function info($message, array $context = array()); /** * Detailed debug information. * * @param string $message * @param mixed[] $context * * @return void */ public function debug($message, array $context = array()); /** * Logs with an arbitrary level. * * @param mixed $level * @param string $message * @param mixed[] $context * * @return void * * @throws \Psr\Log\InvalidArgumentException */ public function log($level, $message, array $context = array()); } <?php namespace Psr\Log; /** * This Logger can be used to avoid conditional log calls. * * Logging should always be optional, and if no logger is provided to your * library creating a NullLogger instance to have something to throw logs at * is a good way to avoid littering your code with `if ($this->logger) { }` * blocks. */ class NullLogger extends AbstractLogger { /** * Logs with an arbitrary level. * * @param mixed $level * @param string $message * @param array $context * * @return void * * @throws \Psr\Log\InvalidArgumentException */ public function log($level, $message, array $context = array()) { // noop } } <?php namespace Psr\Log; /** * Describes log levels. */ class LogLevel { const EMERGENCY = 'emergency'; const ALERT = 'alert'; const CRITICAL = 'critical'; const ERROR = 'error'; const WARNING = 'warning'; const NOTICE = 'notice'; const INFO = 'info'; const DEBUG = 'debug'; } <?php namespace Psr\Log; /** * This is a simple Logger trait that classes unable to extend AbstractLogger * (because they extend another class, etc) can include. * * It simply delegates all log-level-specific methods to the `log` method to * reduce boilerplate code that a simple Logger that does the same thing with * messages regardless of the error level has to implement. */ trait LoggerTrait { /** * System is unusable. * * @param string $message * @param array $context * * @return void */ public function emergency($message, array $context = array()) { $this->log(LogLevel::EMERGENCY, $message, $context); } /** * Action must be taken immediately. * * Example: Entire website down, database unavailable, etc. This should * trigger the SMS alerts and wake you up. * * @param string $message * @param array $context * * @return void */ public function alert($message, array $context = array()) { $this->log(LogLevel::ALERT, $message, $context); } /** * Critical conditions. * * Example: Application component unavailable, unexpected exception. * * @param string $message * @param array $context * * @return void */ public function critical($message, array $context = array()) { $this->log(LogLevel::CRITICAL, $message, $context); } /** * Runtime errors that do not require immediate action but should typically * be logged and monitored. * * @param string $message * @param array $context * * @return void */ public function error($message, array $context = array()) { $this->log(LogLevel::ERROR, $message, $context); } /** * Exceptional occurrences that are not errors. * * Example: Use of deprecated APIs, poor use of an API, undesirable things * that are not necessarily wrong. * * @param string $message * @param array $context * * @return void */ public function warning($message, array $context = array()) { $this->log(LogLevel::WARNING, $message, $context); } /** * Normal but significant events. * * @param string $message * @param array $context * * @return void */ public function notice($message, array $context = array()) { $this->log(LogLevel::NOTICE, $message, $context); } /** * Interesting events. * * Example: User logs in, SQL logs. * * @param string $message * @param array $context * * @return void */ public function info($message, array $context = array()) { $this->log(LogLevel::INFO, $message, $context); } /** * Detailed debug information. * * @param string $message * @param array $context * * @return void */ public function debug($message, array $context = array()) { $this->log(LogLevel::DEBUG, $message, $context); } /** * Logs with an arbitrary level. * * @param mixed $level * @param string $message * @param array $context * * @return void * * @throws \Psr\Log\InvalidArgumentException */ abstract public function log($level, $message, array $context = array()); } <?php /* * This file is part of the JSON Lint package. * * (c) Jordi Boggiano <j.boggiano@seld.be> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Seld\JsonLint; use stdClass; /** * Parser class * * Example: * * $parser = new JsonParser(); * // returns null if it's valid json, or an error object * $parser->lint($json); * // returns parsed json, like json_decode does, but slower, throws exceptions on failure. * $parser->parse($json); * * Ported from https://github.com/zaach/jsonlint */ class JsonParser { const DETECT_KEY_CONFLICTS = 1; const ALLOW_DUPLICATE_KEYS = 2; const PARSE_TO_ASSOC = 4; private $lexer; private $flags; private $stack; private $vstack; // semantic value stack private $lstack; // location stack /** * @phpstan-var array<string, int> */ private $symbols = array( 'error' => 2, 'JSONString' => 3, 'STRING' => 4, 'JSONNumber' => 5, 'NUMBER' => 6, 'JSONNullLiteral' => 7, 'NULL' => 8, 'JSONBooleanLiteral' => 9, 'TRUE' => 10, 'FALSE' => 11, 'JSONText' => 12, 'JSONValue' => 13, 'EOF' => 14, 'JSONObject' => 15, 'JSONArray' => 16, '{' => 17, '}' => 18, 'JSONMemberList' => 19, 'JSONMember' => 20, ':' => 21, ',' => 22, '[' => 23, ']' => 24, 'JSONElementList' => 25, '$accept' => 0, '$end' => 1, ); /** * @phpstan-var array<int, string> */ private $terminals_ = array( 2 => "error", 4 => "STRING", 6 => "NUMBER", 8 => "NULL", 10 => "TRUE", 11 => "FALSE", 14 => "EOF", 17 => "{", 18 => "}", 21 => ":", 22 => ",", 23 => "[", 24 => "]", ); private $productions_ = array( 0, array(3, 1), array(5, 1), array(7, 1), array(9, 1), array(9, 1), array(12, 2), array(13, 1), array(13, 1), array(13, 1), array(13, 1), array(13, 1), array(13, 1), array(15, 2), array(15, 3), array(20, 3), array(19, 1), array(19, 3), array(16, 2), array(16, 3), array(25, 1), array(25, 3) ); private $table = array(array(3 => 5, 4 => array(1,12), 5 => 6, 6 => array(1,13), 7 => 3, 8 => array(1,9), 9 => 4, 10 => array(1,10), 11 => array(1,11), 12 => 1, 13 => 2, 15 => 7, 16 => 8, 17 => array(1,14), 23 => array(1,15)), array( 1 => array(3)), array( 14 => array(1,16)), array( 14 => array(2,7), 18 => array(2,7), 22 => array(2,7), 24 => array(2,7)), array( 14 => array(2,8), 18 => array(2,8), 22 => array(2,8), 24 => array(2,8)), array( 14 => array(2,9), 18 => array(2,9), 22 => array(2,9), 24 => array(2,9)), array( 14 => array(2,10), 18 => array(2,10), 22 => array(2,10), 24 => array(2,10)), array( 14 => array(2,11), 18 => array(2,11), 22 => array(2,11), 24 => array(2,11)), array( 14 => array(2,12), 18 => array(2,12), 22 => array(2,12), 24 => array(2,12)), array( 14 => array(2,3), 18 => array(2,3), 22 => array(2,3), 24 => array(2,3)), array( 14 => array(2,4), 18 => array(2,4), 22 => array(2,4), 24 => array(2,4)), array( 14 => array(2,5), 18 => array(2,5), 22 => array(2,5), 24 => array(2,5)), array( 14 => array(2,1), 18 => array(2,1), 21 => array(2,1), 22 => array(2,1), 24 => array(2,1)), array( 14 => array(2,2), 18 => array(2,2), 22 => array(2,2), 24 => array(2,2)), array( 3 => 20, 4 => array(1,12), 18 => array(1,17), 19 => 18, 20 => 19 ), array( 3 => 5, 4 => array(1,12), 5 => 6, 6 => array(1,13), 7 => 3, 8 => array(1,9), 9 => 4, 10 => array(1,10), 11 => array(1,11), 13 => 23, 15 => 7, 16 => 8, 17 => array(1,14), 23 => array(1,15), 24 => array(1,21), 25 => 22 ), array( 1 => array(2,6)), array( 14 => array(2,13), 18 => array(2,13), 22 => array(2,13), 24 => array(2,13)), array( 18 => array(1,24), 22 => array(1,25)), array( 18 => array(2,16), 22 => array(2,16)), array( 21 => array(1,26)), array( 14 => array(2,18), 18 => array(2,18), 22 => array(2,18), 24 => array(2,18)), array( 22 => array(1,28), 24 => array(1,27)), array( 22 => array(2,20), 24 => array(2,20)), array( 14 => array(2,14), 18 => array(2,14), 22 => array(2,14), 24 => array(2,14)), array( 3 => 20, 4 => array(1,12), 20 => 29 ), array( 3 => 5, 4 => array(1,12), 5 => 6, 6 => array(1,13), 7 => 3, 8 => array(1,9), 9 => 4, 10 => array(1,10), 11 => array(1,11), 13 => 30, 15 => 7, 16 => 8, 17 => array(1,14), 23 => array(1,15)), array( 14 => array(2,19), 18 => array(2,19), 22 => array(2,19), 24 => array(2,19)), array( 3 => 5, 4 => array(1,12), 5 => 6, 6 => array(1,13), 7 => 3, 8 => array(1,9), 9 => 4, 10 => array(1,10), 11 => array(1,11), 13 => 31, 15 => 7, 16 => 8, 17 => array(1,14), 23 => array(1,15)), array( 18 => array(2,17), 22 => array(2,17)), array( 18 => array(2,15), 22 => array(2,15)), array( 22 => array(2,21), 24 => array(2,21)), ); private $defaultActions = array( 16 => array(2, 6) ); /** * @param string $input JSON string * @param int $flags Bitmask of parse/lint options (see constants of this class) * @return null|ParsingException null if no error is found, a ParsingException containing all details otherwise */ public function lint($input, $flags = 0) { try { $this->parse($input, $flags); } catch (ParsingException $e) { return $e; } return null; } /** * @param string $input JSON string * @param int $flags Bitmask of parse/lint options (see constants of this class) * @return mixed * @throws ParsingException */ public function parse($input, $flags = 0) { $this->failOnBOM($input); $this->flags = $flags; $this->stack = array(0); $this->vstack = array(null); $this->lstack = array(); $yytext = ''; $yylineno = 0; $yyleng = 0; $recovering = 0; $TERROR = 2; $EOF = 1; $this->lexer = new Lexer(); $this->lexer->setInput($input); $yyloc = $this->lexer->yylloc; $this->lstack[] = $yyloc; $symbol = null; $preErrorSymbol = null; $state = null; $action = null; $a = null; $r = null; $yyval = new stdClass; $p = null; $len = null; $newState = null; $expected = null; $errStr = null; while (true) { // retrieve state number from top of stack $state = $this->stack[\count($this->stack)-1]; // use default actions if available if (isset($this->defaultActions[$state])) { $action = $this->defaultActions[$state]; } else { if ($symbol == null) { $symbol = $this->lex(); } // read action for current state and first input $action = isset($this->table[$state][$symbol]) ? $this->table[$state][$symbol] : false; } // handle parse error if (!$action || !$action[0]) { if (!$recovering) { // Report error $expected = array(); foreach ($this->table[$state] as $p => $ignore) { if (isset($this->terminals_[$p]) && $p > 2) { $expected[] = "'" . $this->terminals_[$p] . "'"; } } $message = null; if (\in_array("'STRING'", $expected) && \in_array(substr($this->lexer->match, 0, 1), array('"', "'"))) { $message = "Invalid string"; if ("'" === substr($this->lexer->match, 0, 1)) { $message .= ", it appears you used single quotes instead of double quotes"; } elseif (preg_match('{".+?(\\\\[^"bfnrt/\\\\u](...)?)}', $this->lexer->getFullUpcomingInput(), $match)) { $message .= ", it appears you have an unescaped backslash at: ".$match[1]; } elseif (preg_match('{"(?:[^"]+|\\\\")*$}m', $this->lexer->getFullUpcomingInput())) { $message .= ", it appears you forgot to terminate a string, or attempted to write a multiline string which is invalid"; } } $errStr = 'Parse error on line ' . ($yylineno+1) . ":\n"; $errStr .= $this->lexer->showPosition() . "\n"; if ($message) { $errStr .= $message; } else { $errStr .= (\count($expected) > 1) ? "Expected one of: " : "Expected: "; $errStr .= implode(', ', $expected); } if (',' === substr(trim($this->lexer->getPastInput()), -1)) { $errStr .= " - It appears you have an extra trailing comma"; } $this->parseError($errStr, array( 'text' => $this->lexer->match, 'token' => !empty($this->terminals_[$symbol]) ? $this->terminals_[$symbol] : $symbol, 'line' => $this->lexer->yylineno, 'loc' => $yyloc, 'expected' => $expected, )); } // just recovered from another error if ($recovering == 3) { if ($symbol == $EOF) { throw new ParsingException($errStr ?: 'Parsing halted.'); } // discard current lookahead and grab another $yyleng = $this->lexer->yyleng; $yytext = $this->lexer->yytext; $yylineno = $this->lexer->yylineno; $yyloc = $this->lexer->yylloc; $symbol = $this->lex(); } // try to recover from error while (true) { // check for error recovery rule in this state if (\array_key_exists($TERROR, $this->table[$state])) { break; } if ($state == 0) { throw new ParsingException($errStr ?: 'Parsing halted.'); } $this->popStack(1); $state = $this->stack[\count($this->stack)-1]; } $preErrorSymbol = $symbol; // save the lookahead token $symbol = $TERROR; // insert generic error symbol as new lookahead $state = $this->stack[\count($this->stack)-1]; $action = isset($this->table[$state][$TERROR]) ? $this->table[$state][$TERROR] : false; $recovering = 3; // allow 3 real symbols to be shifted before reporting a new error } // this shouldn't happen, unless resolve defaults are off if (\is_array($action[0]) && \count($action) > 1) { throw new ParsingException('Parse Error: multiple actions possible at state: ' . $state . ', token: ' . $symbol); } switch ($action[0]) { case 1: // shift $this->stack[] = $symbol; $this->vstack[] = $this->lexer->yytext; $this->lstack[] = $this->lexer->yylloc; $this->stack[] = $action[1]; // push state $symbol = null; if (!$preErrorSymbol) { // normal execution/no error $yyleng = $this->lexer->yyleng; $yytext = $this->lexer->yytext; $yylineno = $this->lexer->yylineno; $yyloc = $this->lexer->yylloc; if ($recovering > 0) { $recovering--; } } else { // error just occurred, resume old lookahead f/ before error $symbol = $preErrorSymbol; $preErrorSymbol = null; } break; case 2: // reduce $len = $this->productions_[$action[1]][1]; // perform semantic action $yyval->token = $this->vstack[\count($this->vstack) - $len]; // default to $$ = $1 // default location, uses first token for firsts, last for lasts $yyval->store = array( // _$ = store 'first_line' => $this->lstack[\count($this->lstack) - ($len ?: 1)]['first_line'], 'last_line' => $this->lstack[\count($this->lstack) - 1]['last_line'], 'first_column' => $this->lstack[\count($this->lstack) - ($len ?: 1)]['first_column'], 'last_column' => $this->lstack[\count($this->lstack) - 1]['last_column'], ); $r = $this->performAction($yyval, $yytext, $yyleng, $yylineno, $action[1], $this->vstack); if (!$r instanceof Undefined) { return $r; } if ($len) { $this->popStack($len); } $this->stack[] = $this->productions_[$action[1]][0]; // push nonterminal (reduce) $this->vstack[] = $yyval->token; $this->lstack[] = $yyval->store; $newState = $this->table[$this->stack[\count($this->stack)-2]][$this->stack[\count($this->stack)-1]]; $this->stack[] = $newState; break; case 3: // accept return true; } } } protected function parseError($str, $hash) { throw new ParsingException($str, $hash); } // $$ = $tokens // needs to be passed by ref? // $ = $token // _$ removed, useless? private function performAction(stdClass $yyval, $yytext, $yyleng, $yylineno, $yystate, &$tokens) { // $0 = $len $len = \count($tokens) - 1; switch ($yystate) { case 1: $yytext = preg_replace_callback('{(?:\\\\["bfnrt/\\\\]|\\\\u[a-fA-F0-9]{4})}', array($this, 'stringInterpolation'), $yytext); $yyval->token = $yytext; break; case 2: if (strpos($yytext, 'e') !== false || strpos($yytext, 'E') !== false) { $yyval->token = \floatval($yytext); } else { $yyval->token = strpos($yytext, '.') === false ? \intval($yytext) : \floatval($yytext); } break; case 3: $yyval->token = null; break; case 4: $yyval->token = true; break; case 5: $yyval->token = false; break; case 6: return $yyval->token = $tokens[$len-1]; case 13: if ($this->flags & self::PARSE_TO_ASSOC) { $yyval->token = array(); } else { $yyval->token = new stdClass; } break; case 14: $yyval->token = $tokens[$len-1]; break; case 15: $yyval->token = array($tokens[$len-2], $tokens[$len]); break; case 16: if (PHP_VERSION_ID < 70100) { $property = $tokens[$len][0] === '' ? '_empty_' : $tokens[$len][0]; } else { $property = $tokens[$len][0]; } if ($this->flags & self::PARSE_TO_ASSOC) { $yyval->token = array(); $yyval->token[$property] = $tokens[$len][1]; } else { $yyval->token = new stdClass; $yyval->token->$property = $tokens[$len][1]; } break; case 17: if ($this->flags & self::PARSE_TO_ASSOC) { $yyval->token =& $tokens[$len-2]; $key = $tokens[$len][0]; if (($this->flags & self::DETECT_KEY_CONFLICTS) && isset($tokens[$len-2][$key])) { $errStr = 'Parse error on line ' . ($yylineno+1) . ":\n"; $errStr .= $this->lexer->showPosition() . "\n"; $errStr .= "Duplicate key: ".$tokens[$len][0]; throw new DuplicateKeyException($errStr, $tokens[$len][0], array('line' => $yylineno+1)); } elseif (($this->flags & self::ALLOW_DUPLICATE_KEYS) && isset($tokens[$len-2][$key])) { $duplicateCount = 1; do { $duplicateKey = $key . '.' . $duplicateCount++; } while (isset($tokens[$len-2][$duplicateKey])); $key = $duplicateKey; } $tokens[$len-2][$key] = $tokens[$len][1]; } else { $yyval->token = $tokens[$len-2]; if (PHP_VERSION_ID < 70100) { $key = $tokens[$len][0] === '' ? '_empty_' : $tokens[$len][0]; } else { $key = $tokens[$len][0]; } if (($this->flags & self::DETECT_KEY_CONFLICTS) && isset($tokens[$len-2]->{$key})) { $errStr = 'Parse error on line ' . ($yylineno+1) . ":\n"; $errStr .= $this->lexer->showPosition() . "\n"; $errStr .= "Duplicate key: ".$tokens[$len][0]; throw new DuplicateKeyException($errStr, $tokens[$len][0], array('line' => $yylineno+1)); } elseif (($this->flags & self::ALLOW_DUPLICATE_KEYS) && isset($tokens[$len-2]->{$key})) { $duplicateCount = 1; do { $duplicateKey = $key . '.' . $duplicateCount++; } while (isset($tokens[$len-2]->$duplicateKey)); $key = $duplicateKey; } $tokens[$len-2]->$key = $tokens[$len][1]; } break; case 18: $yyval->token = array(); break; case 19: $yyval->token = $tokens[$len-1]; break; case 20: $yyval->token = array($tokens[$len]); break; case 21: $tokens[$len-2][] = $tokens[$len]; $yyval->token = $tokens[$len-2]; break; } return new Undefined(); } private function stringInterpolation($match) { switch ($match[0]) { case '\\\\': return '\\'; case '\"': return '"'; case '\b': return \chr(8); case '\f': return \chr(12); case '\n': return "\n"; case '\r': return "\r"; case '\t': return "\t"; case '\/': return "/"; default: return html_entity_decode('&#x'.ltrim(substr($match[0], 2), '0').';', ENT_QUOTES, 'UTF-8'); } } private function popStack($n) { $this->stack = \array_slice($this->stack, 0, - (2 * $n)); $this->vstack = \array_slice($this->vstack, 0, - $n); $this->lstack = \array_slice($this->lstack, 0, - $n); } private function lex() { $token = $this->lexer->lex() ?: 1; // $end = 1 // if token isn't its numeric value, convert if (!is_numeric($token)) { $token = isset($this->symbols[$token]) ? $this->symbols[$token] : $token; } return $token; } private function failOnBOM($input) { // UTF-8 ByteOrderMark sequence $bom = "\xEF\xBB\xBF"; if (substr($input, 0, 3) === $bom) { $this->parseError("BOM detected, make sure your input does not include a Unicode Byte-Order-Mark", array()); } } } <?php /* * This file is part of the JSON Lint package. * * (c) Jordi Boggiano <j.boggiano@seld.be> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Seld\JsonLint; /** * Lexer class * * Ported from https://github.com/zaach/jsonlint */ class Lexer { private $EOF = 1; /** * @phpstan-var array<int, string> */ private $rules = array( 0 => '/\G\s+/', 1 => '/\G-?([0-9]|[1-9][0-9]+)(\.[0-9]+)?([eE][+-]?[0-9]+)?\b/', 2 => '{\G"(?>\\\\["bfnrt/\\\\]|\\\\u[a-fA-F0-9]{4}|[^\0-\x1f\\\\"]++)*+"}', 3 => '/\G\{/', 4 => '/\G\}/', 5 => '/\G\[/', 6 => '/\G\]/', 7 => '/\G,/', 8 => '/\G:/', 9 => '/\Gtrue\b/', 10 => '/\Gfalse\b/', 11 => '/\Gnull\b/', 12 => '/\G$/', 13 => '/\G./', ); private $conditions = array( "INITIAL" => array( "rules" => array(0,1,2,3,4,5,6,7,8,9,10,11,12,13), "inclusive" => true, ), ); private $conditionStack; private $input; private $more; private $done; private $offset; public $match; public $yylineno; public $yyleng; public $yytext; public $yylloc; public function lex() { $r = $this->next(); if (!$r instanceof Undefined) { return $r; } return $this->lex(); } public function setInput($input) { $this->input = $input; $this->more = false; $this->done = false; $this->offset = 0; $this->yylineno = $this->yyleng = 0; $this->yytext = $this->match = ''; $this->conditionStack = array('INITIAL'); $this->yylloc = array('first_line' => 1, 'first_column' => 0, 'last_line' => 1, 'last_column' => 0); return $this; } public function showPosition() { $pre = str_replace("\n", '', $this->getPastInput()); $c = str_repeat('-', max(0, \strlen($pre) - 1)); // new Array(pre.length + 1).join("-"); return $pre . str_replace("\n", '', $this->getUpcomingInput()) . "\n" . $c . "^"; } public function getPastInput() { $pastLength = $this->offset - \strlen($this->match); return ($pastLength > 20 ? '...' : '') . substr($this->input, max(0, $pastLength - 20), min(20, $pastLength)); } public function getUpcomingInput() { $next = $this->match; if (\strlen($next) < 20) { $next .= substr($this->input, $this->offset, 20 - \strlen($next)); } return substr($next, 0, 20) . (\strlen($next) > 20 ? '...' : ''); } public function getFullUpcomingInput() { $next = $this->match; if (substr($next, 0, 1) === '"' && substr_count($next, '"') === 1) { $len = \strlen($this->input); $strEnd = min(strpos($this->input, '"', $this->offset + 1) ?: $len, strpos($this->input, "\n", $this->offset + 1) ?: $len); $next .= substr($this->input, $this->offset, $strEnd - $this->offset); } elseif (\strlen($next) < 20) { $next .= substr($this->input, $this->offset, 20 - \strlen($next)); } return $next; } protected function parseError($str, $hash) { throw new \Exception($str); } private function next() { if ($this->done) { return $this->EOF; } if ($this->offset === \strlen($this->input)) { $this->done = true; } $token = null; $match = null; $col = null; $lines = null; if (!$this->more) { $this->yytext = ''; $this->match = ''; } $rules = $this->getCurrentRules(); $rulesLen = \count($rules); for ($i=0; $i < $rulesLen; $i++) { if (preg_match($this->rules[$rules[$i]], $this->input, $match, 0, $this->offset)) { preg_match_all('/\n.*/', $match[0], $lines); $lines = $lines[0]; if ($lines) { $this->yylineno += \count($lines); } $this->yylloc = array( 'first_line' => $this->yylloc['last_line'], 'last_line' => $this->yylineno+1, 'first_column' => $this->yylloc['last_column'], 'last_column' => $lines ? \strlen($lines[\count($lines) - 1]) - 1 : $this->yylloc['last_column'] + \strlen($match[0]), ); $this->yytext .= $match[0]; $this->match .= $match[0]; $this->yyleng = \strlen($this->yytext); $this->more = false; $this->offset += \strlen($match[0]); $token = $this->performAction($rules[$i], $this->conditionStack[\count($this->conditionStack)-1]); if ($token) { return $token; } return new Undefined(); } } if ($this->offset === \strlen($this->input)) { return $this->EOF; } $this->parseError( 'Lexical error on line ' . ($this->yylineno+1) . ". Unrecognized text.\n" . $this->showPosition(), array( 'text' => "", 'token' => null, 'line' => $this->yylineno, ) ); } private function getCurrentRules() { return $this->conditions[$this->conditionStack[\count($this->conditionStack)-1]]['rules']; } private function performAction($avoiding_name_collisions, $YY_START) { switch ($avoiding_name_collisions) { case 0:/* skip whitespace */ break; case 1: return 6; case 2: $this->yytext = substr($this->yytext, 1, $this->yyleng-2); return 4; case 3: return 17; case 4: return 18; case 5: return 23; case 6: return 24; case 7: return 22; case 8: return 21; case 9: return 10; case 10: return 11; case 11: return 8; case 12: return 14; case 13: return 'INVALID'; } } } <?php /* * This file is part of the JSON Lint package. * * (c) Jordi Boggiano <j.boggiano@seld.be> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Seld\JsonLint; class Undefined { } <?php /* * This file is part of the JSON Lint package. * * (c) Jordi Boggiano <j.boggiano@seld.be> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Seld\JsonLint; class DuplicateKeyException extends ParsingException { /** * @param string $message * @param string $key * @phpstan-param array{text?: string, token?: string, line?: int, loc?: array{first_line: int, first_column: int, last_line: int, last_column: int}, expected?: string[]} $details */ public function __construct($message, $key, array $details = array()) { $details['key'] = $key; parent::__construct($message, $details); } public function getKey() { return $this->details['key']; } /** * @phpstan-return array{text?: string, token?: string, line?: int, loc?: array{first_line: int, first_column: int, last_line: int, last_column: int}, expected?: string[], key: string} */ public function getDetails() { return $this->details; } } <?php /* * This file is part of the JSON Lint package. * * (c) Jordi Boggiano <j.boggiano@seld.be> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Seld\JsonLint; class ParsingException extends \Exception { protected $details; /** * @param string $message * @phpstan-param array{text?: string, token?: string, line?: int, loc?: array{first_line: int, first_column: int, last_line: int, last_column: int}, expected?: string[]} $details */ public function __construct($message, $details = array()) { $this->details = $details; parent::__construct($message); } /** * @phpstan-return array{text?: string, token?: string, line?: int, loc?: array{first_line: int, first_column: int, last_line: int, last_column: int}, expected?: string[]} */ public function getDetails() { return $this->details; } } <?php /* * This file is part of PHAR Utils. * * (c) Jordi Boggiano <j.boggiano@seld.be> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Seld\PharUtils; class Timestamps { private $contents; /** * @param string $file path to the phar file to use */ public function __construct($file) { $this->contents = file_get_contents($file); } /** * Updates each file's unix timestamps in the PHAR * * The PHAR signature can then be produced in a reproducible manner. * * @param int|\DateTimeInterface|string $timestamp Date string or DateTime or unix timestamp to use */ public function updateTimestamps($timestamp = null) { if ($timestamp instanceof \DateTime || $timestamp instanceof \DateTimeInterface) { $timestamp = $timestamp->getTimestamp(); } elseif (is_string($timestamp)) { $timestamp = strtotime($timestamp); } elseif (!is_int($timestamp)) { $timestamp = strtotime('1984-12-24T00:00:00Z'); } // detect manifest offset / end of stub if (!preg_match('{__HALT_COMPILER\(\);(?: +\?>)?\r?\n}', $this->contents, $match, PREG_OFFSET_CAPTURE)) { throw new \RuntimeException('Could not detect the stub\'s end in the phar'); } // set starting position and skip past manifest length $pos = $match[0][1] + strlen($match[0][0]); $stubEnd = $pos + $this->readUint($pos, 4); $pos += 4; $numFiles = $this->readUint($pos, 4); $pos += 4; // skip API version (YOLO) $pos += 2; // skip PHAR flags $pos += 4; $aliasLength = $this->readUint($pos, 4); $pos += 4 + $aliasLength; $metadataLength = $this->readUint($pos, 4); $pos += 4 + $metadataLength; while ($pos < $stubEnd) { $filenameLength = $this->readUint($pos, 4); $pos += 4 + $filenameLength; // skip filesize $pos += 4; // update timestamp to a fixed value $this->contents = substr_replace($this->contents, pack('L', $timestamp), $pos, 4); // skip timestamp, compressed file size, crc32 checksum and file flags $pos += 4*4; $metadataLength = $this->readUint($pos, 4); $pos += 4 + $metadataLength; $numFiles--; } if ($numFiles !== 0) { throw new \LogicException('All files were not processed, something must have gone wrong'); } } /** * Saves the updated phar file, optionally with an updated signature. * * @param string $path * @param int $signatureAlgo One of Phar::MD5, Phar::SHA1, Phar::SHA256 or Phar::SHA512 * @return bool */ public function save($path, $signatureAlgo) { $pos = $this->determineSignatureBegin(); $algos = array( \Phar::MD5 => 'md5', \Phar::SHA1 => 'sha1', \Phar::SHA256 => 'sha256', \Phar::SHA512 => 'sha512', ); if (!isset($algos[$signatureAlgo])) { throw new \UnexpectedValueException('Invalid hash algorithm given: '.$signatureAlgo.' expected one of Phar::MD5, Phar::SHA1, Phar::SHA256 or Phar::SHA512'); } $algo = $algos[$signatureAlgo]; // re-sign phar // signature $signature = hash($algo, substr($this->contents, 0, $pos), true) // sig type . pack('L', $signatureAlgo) // ohai Greg & Marcus . 'GBMB'; $this->contents = substr($this->contents, 0, $pos) . $signature; return file_put_contents($path, $this->contents); } private function readUint($pos, $bytes) { $res = unpack('V', substr($this->contents, $pos, $bytes)); return $res[1]; } /** * Determine the beginning of the signature. * * @return int */ private function determineSignatureBegin() { // detect signature position if (!preg_match('{__HALT_COMPILER\(\);(?: +\?>)?\r?\n}', $this->contents, $match, PREG_OFFSET_CAPTURE)) { throw new \RuntimeException('Could not detect the stub\'s end in the phar'); } // set starting position and skip past manifest length $pos = $match[0][1] + strlen($match[0][0]); $manifestEnd = $pos + 4 + $this->readUint($pos, 4); $pos += 4; $numFiles = $this->readUint($pos, 4); $pos += 4; // skip API version (YOLO) $pos += 2; // skip PHAR flags $pos += 4; $aliasLength = $this->readUint($pos, 4); $pos += 4 + $aliasLength; $metadataLength = $this->readUint($pos, 4); $pos += 4 + $metadataLength; $compressedSizes = 0; while (($numFiles > 0) && ($pos < $manifestEnd - 24)) { $filenameLength = $this->readUint($pos, 4); $pos += 4 + $filenameLength; // skip filesize and timestamp $pos += 2*4; $compressedSizes += $this->readUint($pos, 4); // skip compressed file size, crc32 checksum and file flags $pos += 3*4; $metadataLength = $this->readUint($pos, 4); $pos += 4 + $metadataLength; $numFiles--; } if ($numFiles !== 0) { throw new \LogicException('All files were not processed, something must have gone wrong'); } return $manifestEnd + $compressedSizes; } } <?php /* * This file is part of PHAR Utils. * * (c) Jordi Boggiano <j.boggiano@seld.be> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Seld\PharUtils; class Linter { /** * Lints all php files inside a given phar with the current PHP version * * @param string $path Phar file path * @param list<string> $excludedPaths Paths which should be skipped by the linter */ public static function lint($path, array $excludedPaths = array()) { $php = defined('PHP_BINARY') ? PHP_BINARY : 'php'; if ($isWindows = defined('PHP_WINDOWS_VERSION_BUILD')) { $tmpFile = @tempnam(sys_get_temp_dir(), ''); if (!$tmpFile || !is_writable($tmpFile)) { throw new \RuntimeException('Unable to create temp file'); } $php = self::escapeWindowsPath($php); $tmpFile = self::escapeWindowsPath($tmpFile); // PHP 8 encloses the command in double-quotes if (PHP_VERSION_ID >= 80000) { $format = '%s -l %s'; } else { $format = '"%s -l %s"'; } $command = sprintf($format, $php, $tmpFile); } else { $command = "'".$php."' -l"; } $descriptorspec = array( 0 => array('pipe', 'r'), 1 => array('pipe', 'w'), 2 => array('pipe', 'w') ); // path to phar + phar:// + trailing slash $baseLen = strlen(realpath($path)) + 7 + 1; foreach (new \RecursiveIteratorIterator(new \Phar($path)) as $file) { if ($file->isDir()) { continue; } if (substr($file, -4) === '.php') { $filename = (string) $file; if (in_array(substr($filename, $baseLen), $excludedPaths, true)) { continue; } if ($isWindows) { file_put_contents($tmpFile, file_get_contents($filename)); } $process = proc_open($command, $descriptorspec, $pipes); if (is_resource($process)) { if (!$isWindows) { fwrite($pipes[0], file_get_contents($filename)); } fclose($pipes[0]); $stdout = stream_get_contents($pipes[1]); fclose($pipes[1]); $stderr = stream_get_contents($pipes[2]); fclose($pipes[2]); $exitCode = proc_close($process); if ($exitCode !== 0) { if ($isWindows) { $stderr = str_replace($tmpFile, $filename, $stderr); } throw new \UnexpectedValueException('Failed linting '.$file.': '.$stderr); } } else { throw new \RuntimeException('Could not start linter process'); } } } if ($isWindows) { @unlink($tmpFile); } } /** * Escapes a Windows file path * * @param string $path * @return string The escaped path */ private static function escapeWindowsPath($path) { // Quote if path contains spaces or brackets if (strpbrk($path, " ()") !== false) { $path = '"'.$path.'"'; } return $path; } } <?php /* * This file is part of the JsonSchema package. * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace JsonSchema\Entity; use JsonSchema\Exception\InvalidArgumentException; /** * @package JsonSchema\Entity * * @author Joost Nijhuis <jnijhuis81@gmail.com> */ class JsonPointer { /** @var string */ private $filename; /** @var string[] */ private $propertyPaths = array(); /** * @var bool Whether the value at this path was set from a schema default */ private $fromDefault = false; /** * @param string $value * * @throws InvalidArgumentException when $value is not a string */ public function __construct($value) { if (!is_string($value)) { throw new InvalidArgumentException('Ref value must be a string'); } $splitRef = explode('#', $value, 2); $this->filename = $splitRef[0]; if (array_key_exists(1, $splitRef)) { $this->propertyPaths = $this->decodePropertyPaths($splitRef[1]); } } /** * @param string $propertyPathString * * @return string[] */ private function decodePropertyPaths($propertyPathString) { $paths = array(); foreach (explode('/', trim($propertyPathString, '/')) as $path) { $path = $this->decodePath($path); if (is_string($path) && '' !== $path) { $paths[] = $path; } } return $paths; } /** * @return array */ private function encodePropertyPaths() { return array_map( array($this, 'encodePath'), $this->getPropertyPaths() ); } /** * @param string $path * * @return string */ private function decodePath($path) { return strtr($path, array('~1' => '/', '~0' => '~', '%25' => '%')); } /** * @param string $path * * @return string */ private function encodePath($path) { return strtr($path, array('/' => '~1', '~' => '~0', '%' => '%25')); } /** * @return string */ public function getFilename() { return $this->filename; } /** * @return string[] */ public function getPropertyPaths() { return $this->propertyPaths; } /** * @param array $propertyPaths * * @return JsonPointer */ public function withPropertyPaths(array $propertyPaths) { $new = clone $this; $new->propertyPaths = $propertyPaths; return $new; } /** * @return string */ public function getPropertyPathAsString() { return rtrim('#/' . implode('/', $this->encodePropertyPaths()), '/'); } /** * @return string */ public function __toString() { return $this->getFilename() . $this->getPropertyPathAsString(); } /** * Mark the value at this path as being set from a schema default */ public function setFromDefault() { $this->fromDefault = true; } /** * Check whether the value at this path was set from a schema default * * @return bool */ public function fromDefault() { return $this->fromDefault; } } <?php /* * This file is part of the JsonSchema package. * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace JsonSchema; /** * @package JsonSchema */ interface UriRetrieverInterface { /** * Retrieve a URI * * @param string $uri JSON Schema URI * @param null|string $baseUri * * @return object JSON Schema contents */ public function retrieve($uri, $baseUri = null); } <?php namespace JsonSchema; class Rfc3339 { const REGEX = '/^(\d{4}-\d{2}-\d{2}[T ]{1}\d{2}:\d{2}:\d{2})(\.\d+)?(Z|([+-]\d{2}):?(\d{2}))$/'; /** * Try creating a DateTime instance * * @param string $string * * @return \DateTime|null */ public static function createFromString($string) { if (!preg_match(self::REGEX, strtoupper($string), $matches)) { return null; } $dateAndTime = $matches[1]; $microseconds = $matches[2] ?: '.000000'; $timeZone = 'Z' !== $matches[3] ? $matches[4] . ':' . $matches[5] : '+00:00'; $dateFormat = strpos($dateAndTime, 'T') === false ? 'Y-m-d H:i:s.uP' : 'Y-m-d\TH:i:s.uP'; $dateTime = \DateTime::createFromFormat($dateFormat, $dateAndTime . $microseconds . $timeZone, new \DateTimeZone('UTC')); return $dateTime ?: null; } } <?php namespace JsonSchema; use JsonSchema\Constraints\BaseConstraint; use JsonSchema\Entity\JsonPointer; use JsonSchema\Exception\UnresolvableJsonPointerException; use JsonSchema\Uri\UriResolver; use JsonSchema\Uri\UriRetriever; class SchemaStorage implements SchemaStorageInterface { const INTERNAL_PROVIDED_SCHEMA_URI = 'internal://provided-schema/'; protected $uriRetriever; protected $uriResolver; protected $schemas = array(); public function __construct( UriRetrieverInterface $uriRetriever = null, UriResolverInterface $uriResolver = null ) { $this->uriRetriever = $uriRetriever ?: new UriRetriever(); $this->uriResolver = $uriResolver ?: new UriResolver(); } /** * @return UriRetrieverInterface */ public function getUriRetriever() { return $this->uriRetriever; } /** * @return UriResolverInterface */ public function getUriResolver() { return $this->uriResolver; } /** * {@inheritdoc} */ public function addSchema($id, $schema = null) { if (is_null($schema) && $id !== self::INTERNAL_PROVIDED_SCHEMA_URI) { // if the schema was user-provided to Validator and is still null, then assume this is // what the user intended, as there's no way for us to retrieve anything else. User-supplied // schemas do not have an associated URI when passed via Validator::validate(). $schema = $this->uriRetriever->retrieve($id); } // cast array schemas to object if (is_array($schema)) { $schema = BaseConstraint::arrayToObjectRecursive($schema); } // workaround for bug in draft-03 & draft-04 meta-schemas (id & $ref defined with incorrect format) // see https://github.com/json-schema-org/JSON-Schema-Test-Suite/issues/177#issuecomment-293051367 if (is_object($schema) && property_exists($schema, 'id')) { if ($schema->id == 'http://json-schema.org/draft-04/schema#') { $schema->properties->id->format = 'uri-reference'; } elseif ($schema->id == 'http://json-schema.org/draft-03/schema#') { $schema->properties->id->format = 'uri-reference'; $schema->properties->{'$ref'}->format = 'uri-reference'; } } // resolve references $this->expandRefs($schema, $id); $this->schemas[$id] = $schema; } /** * Recursively resolve all references against the provided base * * @param mixed $schema * @param string $base */ private function expandRefs(&$schema, $base = null) { if (!is_object($schema)) { if (is_array($schema)) { foreach ($schema as &$member) { $this->expandRefs($member, $base); } } return; } if (property_exists($schema, 'id') && is_string($schema->id) && $base != $schema->id) { $base = $this->uriResolver->resolve($schema->id, $base); } if (property_exists($schema, '$ref') && is_string($schema->{'$ref'})) { $refPointer = new JsonPointer($this->uriResolver->resolve($schema->{'$ref'}, $base)); $schema->{'$ref'} = (string) $refPointer; } foreach ($schema as &$member) { $this->expandRefs($member, $base); } } /** * {@inheritdoc} */ public function getSchema($id) { if (!array_key_exists($id, $this->schemas)) { $this->addSchema($id); } return $this->schemas[$id]; } /** * {@inheritdoc} */ public function resolveRef($ref) { $jsonPointer = new JsonPointer($ref); // resolve filename for pointer $fileName = $jsonPointer->getFilename(); if (!strlen($fileName)) { throw new UnresolvableJsonPointerException(sprintf( "Could not resolve fragment '%s': no file is defined", $jsonPointer->getPropertyPathAsString() )); } // get & process the schema $refSchema = $this->getSchema($fileName); foreach ($jsonPointer->getPropertyPaths() as $path) { if (is_object($refSchema) && property_exists($refSchema, $path)) { $refSchema = $this->resolveRefSchema($refSchema->{$path}); } elseif (is_array($refSchema) && array_key_exists($path, $refSchema)) { $refSchema = $this->resolveRefSchema($refSchema[$path]); } else { throw new UnresolvableJsonPointerException(sprintf( 'File: %s is found, but could not resolve fragment: %s', $jsonPointer->getFilename(), $jsonPointer->getPropertyPathAsString() )); } } return $refSchema; } /** * {@inheritdoc} */ public function resolveRefSchema($refSchema) { if (is_object($refSchema) && property_exists($refSchema, '$ref') && is_string($refSchema->{'$ref'})) { $newSchema = $this->resolveRef($refSchema->{'$ref'}); $refSchema = (object) (get_object_vars($refSchema) + get_object_vars($newSchema)); unset($refSchema->{'$ref'}); } return $refSchema; } } <?php /* * This file is part of the JsonSchema package. * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace JsonSchema\Exception; /** * Wrapper for the InvalidArgumentException */ class InvalidArgumentException extends \InvalidArgumentException implements ExceptionInterface { } <?php /* * This file is part of the JsonSchema package. * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace JsonSchema\Exception; /** * Wrapper for the InvalidSchemaMediaType */ class InvalidSchemaMediaTypeException extends RuntimeException { } <?php /* * This file is part of the JsonSchema package. * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace JsonSchema\Exception; /** * Wrapper for the ResourceNotFoundException */ class InvalidConfigException extends RuntimeException { } <?php /* * This file is part of the JsonSchema package. * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace JsonSchema\Exception; /** * Wrapper for the UriResolverException */ class UriResolverException extends RuntimeException { } <?php namespace JsonSchema\Exception; interface ExceptionInterface { } <?php /* * This file is part of the JsonSchema package. * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace JsonSchema\Exception; /** * @package JsonSchema\Exception * * @author Joost Nijhuis <jnijhuis81@gmail.com> */ class UnresolvableJsonPointerException extends InvalidArgumentException { } <?php /* * This file is part of the JsonSchema package. * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace JsonSchema\Exception; /** * Wrapper for the InvalidSchemaMediaType */ class InvalidSchemaException extends RuntimeException { } <?php /* * This file is part of the JsonSchema package. * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace JsonSchema\Exception; /** * Wrapper for the JsonDecodingException */ class JsonDecodingException extends RuntimeException { public function __construct($code = JSON_ERROR_NONE, \Exception $previous = null) { switch ($code) { case JSON_ERROR_DEPTH: $message = 'The maximum stack depth has been exceeded'; break; case JSON_ERROR_STATE_MISMATCH: $message = 'Invalid or malformed JSON'; break; case JSON_ERROR_CTRL_CHAR: $message = 'Control character error, possibly incorrectly encoded'; break; case JSON_ERROR_UTF8: $message = 'Malformed UTF-8 characters, possibly incorrectly encoded'; break; case JSON_ERROR_SYNTAX: $message = 'JSON syntax is malformed'; break; default: $message = 'Syntax error'; } parent::__construct($message, $code, $previous); } } <?php /* * This file is part of the JsonSchema package. * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace JsonSchema\Exception; class ValidationException extends RuntimeException { } <?php /* * This file is part of the JsonSchema package. * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace JsonSchema\Exception; /** * Wrapper for the RuntimeException */ class RuntimeException extends \RuntimeException implements ExceptionInterface { } <?php /* * This file is part of the JsonSchema package. * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace JsonSchema\Exception; /** * Wrapper for the ResourceNotFoundException */ class ResourceNotFoundException extends RuntimeException { } <?php /* * This file is part of the JsonSchema package. * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace JsonSchema\Exception; /** * Wrapper for the InvalidSourceUriException */ class InvalidSourceUriException extends InvalidArgumentException { } <?php /* * This file is part of the JsonSchema package. * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace JsonSchema; /** * @package JsonSchema */ interface UriResolverInterface { /** * Resolves a URI * * @param string $uri Absolute or relative * @param null|string $baseUri Optional base URI * * @return string Absolute URI */ public function resolve($uri, $baseUri = null); } <?php /* * This file is part of the JsonSchema package. * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace JsonSchema\Constraints; use JsonSchema\Entity\JsonPointer; /** * The Base Constraints, all Validators should extend this class * * @author Robert Schönthal <seroscho@googlemail.com> * @author Bruno Prieto Reis <bruno.p.reis@gmail.com> */ abstract class Constraint extends BaseConstraint implements ConstraintInterface { protected $inlineSchemaProperty = '$schema'; const CHECK_MODE_NONE = 0x00000000; const CHECK_MODE_NORMAL = 0x00000001; const CHECK_MODE_TYPE_CAST = 0x00000002; const CHECK_MODE_COERCE_TYPES = 0x00000004; const CHECK_MODE_APPLY_DEFAULTS = 0x00000008; const CHECK_MODE_EXCEPTIONS = 0x00000010; const CHECK_MODE_DISABLE_FORMAT = 0x00000020; const CHECK_MODE_ONLY_REQUIRED_DEFAULTS = 0x00000080; const CHECK_MODE_VALIDATE_SCHEMA = 0x00000100; /** * Bubble down the path * * @param JsonPointer|null $path Current path * @param mixed $i What to append to the path * * @return JsonPointer; */ protected function incrementPath(JsonPointer $path = null, $i) { $path = $path ?: new JsonPointer(''); if ($i === null || $i === '') { return $path; } $path = $path->withPropertyPaths( array_merge( $path->getPropertyPaths(), array($i) ) ); return $path; } /** * Validates an array * * @param mixed $value * @param mixed $schema * @param JsonPointer|null $path * @param mixed $i */ protected function checkArray(&$value, $schema = null, JsonPointer $path = null, $i = null) { $validator = $this->factory->createInstanceFor('collection'); $validator->check($value, $schema, $path, $i); $this->addErrors($validator->getErrors()); } /** * Validates an object * * @param mixed $value * @param mixed $schema * @param JsonPointer|null $path * @param mixed $properties * @param mixed $additionalProperties * @param mixed $patternProperties */ protected function checkObject(&$value, $schema = null, JsonPointer $path = null, $properties = null, $additionalProperties = null, $patternProperties = null, $appliedDefaults = array()) { $validator = $this->factory->createInstanceFor('object'); $validator->check($value, $schema, $path, $properties, $additionalProperties, $patternProperties, $appliedDefaults); $this->addErrors($validator->getErrors()); } /** * Validates the type of a property * * @param mixed $value * @param mixed $schema * @param JsonPointer|null $path * @param mixed $i */ protected function checkType(&$value, $schema = null, JsonPointer $path = null, $i = null) { $validator = $this->factory->createInstanceFor('type'); $validator->check($value, $schema, $path, $i); $this->addErrors($validator->getErrors()); } /** * Checks a undefined element * * @param mixed $value * @param mixed $schema * @param JsonPointer|null $path * @param mixed $i */ protected function checkUndefined(&$value, $schema = null, JsonPointer $path = null, $i = null, $fromDefault = false) { $validator = $this->factory->createInstanceFor('undefined'); $validator->check($value, $this->factory->getSchemaStorage()->resolveRefSchema($schema), $path, $i, $fromDefault); $this->addErrors($validator->getErrors()); } /** * Checks a string element * * @param mixed $value * @param mixed $schema * @param JsonPointer|null $path * @param mixed $i */ protected function checkString($value, $schema = null, JsonPointer $path = null, $i = null) { $validator = $this->factory->createInstanceFor('string'); $validator->check($value, $schema, $path, $i); $this->addErrors($validator->getErrors()); } /** * Checks a number element * * @param mixed $value * @param mixed $schema * @param JsonPointer $path * @param mixed $i */ protected function checkNumber($value, $schema = null, JsonPointer $path = null, $i = null) { $validator = $this->factory->createInstanceFor('number'); $validator->check($value, $schema, $path, $i); $this->addErrors($validator->getErrors()); } /** * Checks a enum element * * @param mixed $value * @param mixed $schema * @param JsonPointer|null $path * @param mixed $i */ protected function checkEnum($value, $schema = null, JsonPointer $path = null, $i = null) { $validator = $this->factory->createInstanceFor('enum'); $validator->check($value, $schema, $path, $i); $this->addErrors($validator->getErrors()); } /** * Checks format of an element * * @param mixed $value * @param mixed $schema * @param JsonPointer|null $path * @param mixed $i */ protected function checkFormat($value, $schema = null, JsonPointer $path = null, $i = null) { $validator = $this->factory->createInstanceFor('format'); $validator->check($value, $schema, $path, $i); $this->addErrors($validator->getErrors()); } /** * Get the type check based on the set check mode. * * @return TypeCheck\TypeCheckInterface */ protected function getTypeCheck() { return $this->factory->getTypeCheck(); } /** * @param JsonPointer $pointer * * @return string property path */ protected function convertJsonPointerIntoPropertyPath(JsonPointer $pointer) { $result = array_map( function ($path) { return sprintf(is_numeric($path) ? '[%d]' : '.%s', $path); }, $pointer->getPropertyPaths() ); return trim(implode('', $result), '.'); } } <?php /* * This file is part of the JsonSchema package. * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace JsonSchema\Constraints; use JsonSchema\Entity\JsonPointer; use JsonSchema\Exception\InvalidArgumentException; use JsonSchema\Exception\InvalidSchemaException; use JsonSchema\Exception\RuntimeException; use JsonSchema\Validator; /** * The SchemaConstraint Constraints, validates an element against a given schema * * @author Robert Schönthal <seroscho@googlemail.com> * @author Bruno Prieto Reis <bruno.p.reis@gmail.com> */ class SchemaConstraint extends Constraint { const DEFAULT_SCHEMA_SPEC = 'http://json-schema.org/draft-04/schema#'; /** * {@inheritdoc} */ public function check(&$element, $schema = null, JsonPointer $path = null, $i = null) { if ($schema !== null) { // passed schema $validationSchema = $schema; } elseif ($this->getTypeCheck()->propertyExists($element, $this->inlineSchemaProperty)) { // inline schema $validationSchema = $this->getTypeCheck()->propertyGet($element, $this->inlineSchemaProperty); } else { throw new InvalidArgumentException('no schema found to verify against'); } // cast array schemas to object if (is_array($validationSchema)) { $validationSchema = BaseConstraint::arrayToObjectRecursive($validationSchema); } // validate schema against whatever is defined in $validationSchema->$schema. If no // schema is defined, assume self::DEFAULT_SCHEMA_SPEC (currently draft-04). if ($this->factory->getConfig(self::CHECK_MODE_VALIDATE_SCHEMA)) { if (!$this->getTypeCheck()->isObject($validationSchema)) { throw new RuntimeException('Cannot validate the schema of a non-object'); } if ($this->getTypeCheck()->propertyExists($validationSchema, '$schema')) { $schemaSpec = $this->getTypeCheck()->propertyGet($validationSchema, '$schema'); } else { $schemaSpec = self::DEFAULT_SCHEMA_SPEC; } // get the spec schema $schemaStorage = $this->factory->getSchemaStorage(); if (!$this->getTypeCheck()->isObject($schemaSpec)) { $schemaSpec = $schemaStorage->getSchema($schemaSpec); } // save error count, config & subtract CHECK_MODE_VALIDATE_SCHEMA $initialErrorCount = $this->numErrors(); $initialConfig = $this->factory->getConfig(); $initialContext = $this->factory->getErrorContext(); $this->factory->removeConfig(self::CHECK_MODE_VALIDATE_SCHEMA | self::CHECK_MODE_APPLY_DEFAULTS); $this->factory->addConfig(self::CHECK_MODE_TYPE_CAST); $this->factory->setErrorContext(Validator::ERROR_SCHEMA_VALIDATION); // validate schema try { $this->check($validationSchema, $schemaSpec); } catch (\Exception $e) { if ($this->factory->getConfig(self::CHECK_MODE_EXCEPTIONS)) { throw new InvalidSchemaException('Schema did not pass validation', 0, $e); } } if ($this->numErrors() > $initialErrorCount) { $this->addError($path, 'Schema is not valid', 'schema'); } // restore the initial config $this->factory->setConfig($initialConfig); $this->factory->setErrorContext($initialContext); } // validate element against $validationSchema $this->checkUndefined($element, $validationSchema, $path, $i); } } <?php /* * This file is part of the JsonSchema package. * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace JsonSchema\Constraints; use JsonSchema\Entity\JsonPointer; /** * The CollectionConstraint Constraints, validates an array against a given schema * * @author Robert Schönthal <seroscho@googlemail.com> * @author Bruno Prieto Reis <bruno.p.reis@gmail.com> */ class CollectionConstraint extends Constraint { /** * {@inheritdoc} */ public function check(&$value, $schema = null, JsonPointer $path = null, $i = null) { // Verify minItems if (isset($schema->minItems) && count($value) < $schema->minItems) { $this->addError($path, 'There must be a minimum of ' . $schema->minItems . ' items in the array', 'minItems', array('minItems' => $schema->minItems)); } // Verify maxItems if (isset($schema->maxItems) && count($value) > $schema->maxItems) { $this->addError($path, 'There must be a maximum of ' . $schema->maxItems . ' items in the array', 'maxItems', array('maxItems' => $schema->maxItems)); } // Verify uniqueItems if (isset($schema->uniqueItems) && $schema->uniqueItems) { $unique = $value; if (is_array($value) && count($value)) { $unique = array_map(function ($e) { return var_export($e, true); }, $value); } if (count(array_unique($unique)) != count($value)) { $this->addError($path, 'There are no duplicates allowed in the array', 'uniqueItems'); } } // Verify items if (isset($schema->items)) { $this->validateItems($value, $schema, $path, $i); } } /** * Validates the items * * @param array $value * @param \stdClass $schema * @param JsonPointer|null $path * @param string $i */ protected function validateItems(&$value, $schema = null, JsonPointer $path = null, $i = null) { if (is_object($schema->items)) { // just one type definition for the whole array foreach ($value as $k => &$v) { $initErrors = $this->getErrors(); // First check if its defined in "items" $this->checkUndefined($v, $schema->items, $path, $k); // Recheck with "additionalItems" if the first test fails if (count($initErrors) < count($this->getErrors()) && (isset($schema->additionalItems) && $schema->additionalItems !== false)) { $secondErrors = $this->getErrors(); $this->checkUndefined($v, $schema->additionalItems, $path, $k); } // Reset errors if needed if (isset($secondErrors) && count($secondErrors) < count($this->getErrors())) { $this->errors = $secondErrors; } elseif (isset($secondErrors) && count($secondErrors) === count($this->getErrors())) { $this->errors = $initErrors; } } unset($v); /* remove dangling reference to prevent any future bugs * caused by accidentally using $v elsewhere */ } else { // Defined item type definitions foreach ($value as $k => &$v) { if (array_key_exists($k, $schema->items)) { $this->checkUndefined($v, $schema->items[$k], $path, $k); } else { // Additional items if (property_exists($schema, 'additionalItems')) { if ($schema->additionalItems !== false) { $this->checkUndefined($v, $schema->additionalItems, $path, $k); } else { $this->addError( $path, 'The item ' . $i . '[' . $k . '] is not defined and the definition does not allow additional items', 'additionalItems', array('additionalItems' => $schema->additionalItems)); } } else { // Should be valid against an empty schema $this->checkUndefined($v, new \stdClass(), $path, $k); } } } unset($v); /* remove dangling reference to prevent any future bugs * caused by accidentally using $v elsewhere */ // Treat when we have more schema definitions than values, not for empty arrays if (count($value) > 0) { for ($k = count($value); $k < count($schema->items); $k++) { $undefinedInstance = $this->factory->createInstanceFor('undefined'); $this->checkUndefined($undefinedInstance, $schema->items[$k], $path, $k); } } } } } <?php namespace JsonSchema\Constraints\TypeCheck; interface TypeCheckInterface { public static function isObject($value); public static function isArray($value); public static function propertyGet($value, $property); public static function propertySet(&$value, $property, $data); public static function propertyExists($value, $property); public static function propertyCount($value); } <?php namespace JsonSchema\Constraints\TypeCheck; class StrictTypeCheck implements TypeCheckInterface { public static function isObject($value) { return is_object($value); } public static function isArray($value) { return is_array($value); } public static function propertyGet($value, $property) { return $value->{$property}; } public static function propertySet(&$value, $property, $data) { $value->{$property} = $data; } public static function propertyExists($value, $property) { return property_exists($value, $property); } public static function propertyCount($value) { if (!is_object($value)) { return 0; } return count(get_object_vars($value)); } } <?php namespace JsonSchema\Constraints\TypeCheck; class LooseTypeCheck implements TypeCheckInterface { public static function isObject($value) { return is_object($value) || (is_array($value) && (count($value) == 0 || self::isAssociativeArray($value))); } public static function isArray($value) { return is_array($value) && (count($value) == 0 || !self::isAssociativeArray($value)); } public static function propertyGet($value, $property) { if (is_object($value)) { return $value->{$property}; } return $value[$property]; } public static function propertySet(&$value, $property, $data) { if (is_object($value)) { $value->{$property} = $data; } else { $value[$property] = $data; } } public static function propertyExists($value, $property) { if (is_object($value)) { return property_exists($value, $property); } return array_key_exists($property, $value); } public static function propertyCount($value) { if (is_object($value)) { return count(get_object_vars($value)); } return count($value); } /** * Check if the provided array is associative or not * * @param array $arr * * @return bool */ private static function isAssociativeArray($arr) { return array_keys($arr) !== range(0, count($arr) - 1); } } <?php /* * This file is part of the JsonSchema package. * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace JsonSchema\Constraints; use JsonSchema\Entity\JsonPointer; use JsonSchema\Rfc3339; /** * Validates against the "format" property * * @author Justin Rainbow <justin.rainbow@gmail.com> * * @see http://tools.ietf.org/html/draft-zyp-json-schema-03#section-5.23 */ class FormatConstraint extends Constraint { /** * {@inheritdoc} */ public function check(&$element, $schema = null, JsonPointer $path = null, $i = null) { if (!isset($schema->format) || $this->factory->getConfig(self::CHECK_MODE_DISABLE_FORMAT)) { return; } switch ($schema->format) { case 'date': if (!$date = $this->validateDateTime($element, 'Y-m-d')) { $this->addError($path, sprintf('Invalid date %s, expected format YYYY-MM-DD', json_encode($element)), 'format', array('format' => $schema->format)); } break; case 'time': if (!$this->validateDateTime($element, 'H:i:s')) { $this->addError($path, sprintf('Invalid time %s, expected format hh:mm:ss', json_encode($element)), 'format', array('format' => $schema->format)); } break; case 'date-time': if (null === Rfc3339::createFromString($element)) { $this->addError($path, sprintf('Invalid date-time %s, expected format YYYY-MM-DDThh:mm:ssZ or YYYY-MM-DDThh:mm:ss+hh:mm', json_encode($element)), 'format', array('format' => $schema->format)); } break; case 'utc-millisec': if (!$this->validateDateTime($element, 'U')) { $this->addError($path, sprintf('Invalid time %s, expected integer of milliseconds since Epoch', json_encode($element)), 'format', array('format' => $schema->format)); } break; case 'regex': if (!$this->validateRegex($element)) { $this->addError($path, 'Invalid regex format ' . $element, 'format', array('format' => $schema->format)); } break; case 'color': if (!$this->validateColor($element)) { $this->addError($path, 'Invalid color', 'format', array('format' => $schema->format)); } break; case 'style': if (!$this->validateStyle($element)) { $this->addError($path, 'Invalid style', 'format', array('format' => $schema->format)); } break; case 'phone': if (!$this->validatePhone($element)) { $this->addError($path, 'Invalid phone number', 'format', array('format' => $schema->format)); } break; case 'uri': if (null === filter_var($element, FILTER_VALIDATE_URL, FILTER_NULL_ON_FAILURE)) { $this->addError($path, 'Invalid URL format', 'format', array('format' => $schema->format)); } break; case 'uriref': case 'uri-reference': if (null === filter_var($element, FILTER_VALIDATE_URL, FILTER_NULL_ON_FAILURE)) { // FILTER_VALIDATE_URL does not conform to RFC-3986, and cannot handle relative URLs, but // the json-schema spec uses RFC-3986, so need a bit of hackery to properly validate them. // See https://tools.ietf.org/html/rfc3986#section-4.2 for additional information. if (substr($element, 0, 2) === '//') { // network-path reference $validURL = filter_var('scheme:' . $element, FILTER_VALIDATE_URL, FILTER_NULL_ON_FAILURE); } elseif (substr($element, 0, 1) === '/') { // absolute-path reference $validURL = filter_var('scheme://host' . $element, FILTER_VALIDATE_URL, FILTER_NULL_ON_FAILURE); } elseif (strlen($element)) { // relative-path reference $pathParts = explode('/', $element, 2); if (strpos($pathParts[0], ':') !== false) { $validURL = null; } else { $validURL = filter_var('scheme://host/' . $element, FILTER_VALIDATE_URL, FILTER_NULL_ON_FAILURE); } } else { $validURL = null; } if ($validURL === null) { $this->addError($path, 'Invalid URL format', 'format', array('format' => $schema->format)); } } break; case 'email': $filterFlags = FILTER_NULL_ON_FAILURE; if (defined('FILTER_FLAG_EMAIL_UNICODE')) { // Only available from PHP >= 7.1.0, so ignore it for coverage checks $filterFlags |= constant('FILTER_FLAG_EMAIL_UNICODE'); // @codeCoverageIgnore } if (null === filter_var($element, FILTER_VALIDATE_EMAIL, $filterFlags)) { $this->addError($path, 'Invalid email', 'format', array('format' => $schema->format)); } break; case 'ip-address': case 'ipv4': if (null === filter_var($element, FILTER_VALIDATE_IP, FILTER_NULL_ON_FAILURE | FILTER_FLAG_IPV4)) { $this->addError($path, 'Invalid IP address', 'format', array('format' => $schema->format)); } break; case 'ipv6': if (null === filter_var($element, FILTER_VALIDATE_IP, FILTER_NULL_ON_FAILURE | FILTER_FLAG_IPV6)) { $this->addError($path, 'Invalid IP address', 'format', array('format' => $schema->format)); } break; case 'host-name': case 'hostname': if (!$this->validateHostname($element)) { $this->addError($path, 'Invalid hostname', 'format', array('format' => $schema->format)); } break; default: // Empty as it should be: // The value of this keyword is called a format attribute. It MUST be a string. // A format attribute can generally only validate a given set of instance types. // If the type of the instance to validate is not in this set, validation for // this format attribute and instance SHOULD succeed. // http://json-schema.org/latest/json-schema-validation.html#anchor105 break; } } protected function validateDateTime($datetime, $format) { $dt = \DateTime::createFromFormat($format, $datetime); if (!$dt) { return false; } if ($datetime === $dt->format($format)) { return true; } // handles the case where a non-6 digit microsecond datetime is passed // which will fail the above string comparison because the passed // $datetime may be '2000-05-01T12:12:12.123Z' but format() will return // '2000-05-01T12:12:12.123000Z' if ((strpos('u', $format) !== -1) && (preg_match('/\.\d+Z$/', $datetime))) { return true; } return false; } protected function validateRegex($regex) { return false !== @preg_match('/' . $regex . '/u', ''); } protected function validateColor($color) { if (in_array(strtolower($color), array('aqua', 'black', 'blue', 'fuchsia', 'gray', 'green', 'lime', 'maroon', 'navy', 'olive', 'orange', 'purple', 'red', 'silver', 'teal', 'white', 'yellow'))) { return true; } return preg_match('/^#([a-f0-9]{3}|[a-f0-9]{6})$/i', $color); } protected function validateStyle($style) { $properties = explode(';', rtrim($style, ';')); $invalidEntries = preg_grep('/^\s*[-a-z]+\s*:\s*.+$/i', $properties, PREG_GREP_INVERT); return empty($invalidEntries); } protected function validatePhone($phone) { return preg_match('/^\+?(\(\d{3}\)|\d{3}) \d{3} \d{4}$/', $phone); } protected function validateHostname($host) { $hostnameRegex = '/^(([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\-]*[a-zA-Z0-9])\.)*([A-Za-z0-9]|[A-Za-z0-9][A-Za-z0-9\-]*[A-Za-z0-9])$/i'; return preg_match($hostnameRegex, $host); } } <?php /* * This file is part of the JsonSchema package. * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace JsonSchema\Constraints; use JsonSchema\Entity\JsonPointer; /** * The NumberConstraint Constraints, validates an number against a given schema * * @author Robert Schönthal <seroscho@googlemail.com> * @author Bruno Prieto Reis <bruno.p.reis@gmail.com> */ class NumberConstraint extends Constraint { /** * {@inheritdoc} */ public function check(&$element, $schema = null, JsonPointer $path = null, $i = null) { // Verify minimum if (isset($schema->exclusiveMinimum)) { if (isset($schema->minimum)) { if ($schema->exclusiveMinimum && $element <= $schema->minimum) { $this->addError($path, 'Must have a minimum value of ' . $schema->minimum, 'exclusiveMinimum', array('minimum' => $schema->minimum)); } elseif ($element < $schema->minimum) { $this->addError($path, 'Must have a minimum value of ' . $schema->minimum, 'minimum', array('minimum' => $schema->minimum)); } } else { $this->addError($path, 'Use of exclusiveMinimum requires presence of minimum', 'missingMinimum'); } } elseif (isset($schema->minimum) && $element < $schema->minimum) { $this->addError($path, 'Must have a minimum value of ' . $schema->minimum, 'minimum', array('minimum' => $schema->minimum)); } // Verify maximum if (isset($schema->exclusiveMaximum)) { if (isset($schema->maximum)) { if ($schema->exclusiveMaximum && $element >= $schema->maximum) { $this->addError($path, 'Must have a maximum value of ' . $schema->maximum, 'exclusiveMaximum', array('maximum' => $schema->maximum)); } elseif ($element > $schema->maximum) { $this->addError($path, 'Must have a maximum value of ' . $schema->maximum, 'maximum', array('maximum' => $schema->maximum)); } } else { $this->addError($path, 'Use of exclusiveMaximum requires presence of maximum', 'missingMaximum'); } } elseif (isset($schema->maximum) && $element > $schema->maximum) { $this->addError($path, 'Must have a maximum value of ' . $schema->maximum, 'maximum', array('maximum' => $schema->maximum)); } // Verify divisibleBy - Draft v3 if (isset($schema->divisibleBy) && $this->fmod($element, $schema->divisibleBy) != 0) { $this->addError($path, 'Is not divisible by ' . $schema->divisibleBy, 'divisibleBy', array('divisibleBy' => $schema->divisibleBy)); } // Verify multipleOf - Draft v4 if (isset($schema->multipleOf) && $this->fmod($element, $schema->multipleOf) != 0) { $this->addError($path, 'Must be a multiple of ' . $schema->multipleOf, 'multipleOf', array('multipleOf' => $schema->multipleOf)); } $this->checkFormat($element, $schema, $path, $i); } private function fmod($number1, $number2) { $modulus = ($number1 - round($number1 / $number2) * $number2); $precision = 0.0000000001; if (-$precision < $modulus && $modulus < $precision) { return 0.0; } return $modulus; } } <?php /* * This file is part of the JsonSchema package. * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace JsonSchema\Constraints; use JsonSchema\Entity\JsonPointer; /** * The Constraints Interface * * @author Robert Schönthal <seroscho@googlemail.com> */ interface ConstraintInterface { /** * returns all collected errors * * @return array */ public function getErrors(); /** * adds errors to this validator * * @param array $errors */ public function addErrors(array $errors); /** * adds an error * * @param JsonPointer|null $path * @param string $message * @param string $constraint the constraint/rule that is broken, e.g.: 'minLength' * @param array $more more array elements to add to the error */ public function addError(JsonPointer $path = null, $message, $constraint='', array $more = null); /** * checks if the validator has not raised errors * * @return bool */ public function isValid(); /** * invokes the validation of an element * * @abstract * * @param mixed $value * @param mixed $schema * @param JsonPointer|null $path * @param mixed $i * * @throws \JsonSchema\Exception\ExceptionInterface */ public function check(&$value, $schema = null, JsonPointer $path = null, $i = null); } <?php /* * This file is part of the JsonSchema package. * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace JsonSchema\Constraints; use JsonSchema\Entity\JsonPointer; /** * The StringConstraint Constraints, validates an string against a given schema * * @author Robert Schönthal <seroscho@googlemail.com> * @author Bruno Prieto Reis <bruno.p.reis@gmail.com> */ class StringConstraint extends Constraint { /** * {@inheritdoc} */ public function check(&$element, $schema = null, JsonPointer $path = null, $i = null) { // Verify maxLength if (isset($schema->maxLength) && $this->strlen($element) > $schema->maxLength) { $this->addError($path, 'Must be at most ' . $schema->maxLength . ' characters long', 'maxLength', array( 'maxLength' => $schema->maxLength, )); } //verify minLength if (isset($schema->minLength) && $this->strlen($element) < $schema->minLength) { $this->addError($path, 'Must be at least ' . $schema->minLength . ' characters long', 'minLength', array( 'minLength' => $schema->minLength, )); } // Verify a regex pattern if (isset($schema->pattern) && !preg_match('#' . str_replace('#', '\\#', $schema->pattern) . '#u', $element)) { $this->addError($path, 'Does not match the regex pattern ' . $schema->pattern, 'pattern', array( 'pattern' => $schema->pattern, )); } $this->checkFormat($element, $schema, $path, $i); } private function strlen($string) { if (extension_loaded('mbstring')) { return mb_strlen($string, mb_detect_encoding($string)); } // mbstring is present on all test platforms, so strlen() can be ignored for coverage return strlen($string); // @codeCoverageIgnore } } <?php /* * This file is part of the JsonSchema package. * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace JsonSchema\Constraints; use JsonSchema\Exception\InvalidArgumentException; use JsonSchema\SchemaStorage; use JsonSchema\SchemaStorageInterface; use JsonSchema\Uri\UriRetriever; use JsonSchema\UriRetrieverInterface; use JsonSchema\Validator; /** * Factory for centralize constraint initialization. */ class Factory { /** * @var SchemaStorage */ protected $schemaStorage; /** * @var UriRetriever */ protected $uriRetriever; /** * @var int */ private $checkMode = Constraint::CHECK_MODE_NORMAL; /** * @var TypeCheck\TypeCheckInterface[] */ private $typeCheck = array(); /** * @var int Validation context */ protected $errorContext = Validator::ERROR_DOCUMENT_VALIDATION; /** * @var array */ protected $constraintMap = array( 'array' => 'JsonSchema\Constraints\CollectionConstraint', 'collection' => 'JsonSchema\Constraints\CollectionConstraint', 'object' => 'JsonSchema\Constraints\ObjectConstraint', 'type' => 'JsonSchema\Constraints\TypeConstraint', 'undefined' => 'JsonSchema\Constraints\UndefinedConstraint', 'string' => 'JsonSchema\Constraints\StringConstraint', 'number' => 'JsonSchema\Constraints\NumberConstraint', 'enum' => 'JsonSchema\Constraints\EnumConstraint', 'format' => 'JsonSchema\Constraints\FormatConstraint', 'schema' => 'JsonSchema\Constraints\SchemaConstraint', 'validator' => 'JsonSchema\Validator' ); /** * @var array<ConstraintInterface> */ private $instanceCache = array(); /** * @param SchemaStorage $schemaStorage * @param UriRetrieverInterface $uriRetriever * @param int $checkMode */ public function __construct( SchemaStorageInterface $schemaStorage = null, UriRetrieverInterface $uriRetriever = null, $checkMode = Constraint::CHECK_MODE_NORMAL ) { // set provided config options $this->setConfig($checkMode); $this->uriRetriever = $uriRetriever ?: new UriRetriever(); $this->schemaStorage = $schemaStorage ?: new SchemaStorage($this->uriRetriever); } /** * Set config values * * @param int $checkMode Set checkMode options - does not preserve existing flags */ public function setConfig($checkMode = Constraint::CHECK_MODE_NORMAL) { $this->checkMode = $checkMode; } /** * Enable checkMode flags * * @param int $options */ public function addConfig($options) { $this->checkMode |= $options; } /** * Disable checkMode flags * * @param int $options */ public function removeConfig($options) { $this->checkMode &= ~$options; } /** * Get checkMode option * * @param int $options Options to get, if null then return entire bitmask * * @return int */ public function getConfig($options = null) { if ($options === null) { return $this->checkMode; } return $this->checkMode & $options; } /** * @return UriRetrieverInterface */ public function getUriRetriever() { return $this->uriRetriever; } public function getSchemaStorage() { return $this->schemaStorage; } public function getTypeCheck() { if (!isset($this->typeCheck[$this->checkMode])) { $this->typeCheck[$this->checkMode] = ($this->checkMode & Constraint::CHECK_MODE_TYPE_CAST) ? new TypeCheck\LooseTypeCheck() : new TypeCheck\StrictTypeCheck(); } return $this->typeCheck[$this->checkMode]; } /** * @param string $name * @param string $class * * @return Factory */ public function setConstraintClass($name, $class) { // Ensure class exists if (!class_exists($class)) { throw new InvalidArgumentException('Unknown constraint ' . $name); } // Ensure class is appropriate if (!in_array('JsonSchema\Constraints\ConstraintInterface', class_implements($class))) { throw new InvalidArgumentException('Invalid class ' . $name); } $this->constraintMap[$name] = $class; return $this; } /** * Create a constraint instance for the given constraint name. * * @param string $constraintName * * @throws InvalidArgumentException if is not possible create the constraint instance * * @return ConstraintInterface|ObjectConstraint */ public function createInstanceFor($constraintName) { if (!isset($this->constraintMap[$constraintName])) { throw new InvalidArgumentException('Unknown constraint ' . $constraintName); } if (!isset($this->instanceCache[$constraintName])) { $this->instanceCache[$constraintName] = new $this->constraintMap[$constraintName]($this); } return clone $this->instanceCache[$constraintName]; } /** * Get the error context * * @return string */ public function getErrorContext() { return $this->errorContext; } /** * Set the error context * * @param string $validationContext */ public function setErrorContext($errorContext) { $this->errorContext = $errorContext; } } <?php /* * This file is part of the JsonSchema package. * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace JsonSchema\Constraints; use JsonSchema\Entity\JsonPointer; /** * The ObjectConstraint Constraints, validates an object against a given schema * * @author Robert Schönthal <seroscho@googlemail.com> * @author Bruno Prieto Reis <bruno.p.reis@gmail.com> */ class ObjectConstraint extends Constraint { /** * @var array List of properties to which a default value has been applied */ protected $appliedDefaults = array(); /** * {@inheritdoc} */ public function check(&$element, $schema = null, JsonPointer $path = null, $properties = null, $additionalProp = null, $patternProperties = null, $appliedDefaults = array()) { if ($element instanceof UndefinedConstraint) { return; } $this->appliedDefaults = $appliedDefaults; $matches = array(); if ($patternProperties) { // validate the element pattern properties $matches = $this->validatePatternProperties($element, $path, $patternProperties); } if ($properties) { // validate the element properties $this->validateProperties($element, $properties, $path); } // validate additional element properties & constraints $this->validateElement($element, $matches, $schema, $path, $properties, $additionalProp); } public function validatePatternProperties($element, JsonPointer $path = null, $patternProperties) { $try = array('/', '#', '+', '~', '%'); $matches = array(); foreach ($patternProperties as $pregex => $schema) { $delimiter = '/'; // Choose delimiter. Necessary for patterns like ^/ , otherwise you get error foreach ($try as $delimiter) { if (strpos($pregex, $delimiter) === false) { // safe to use break; } } // Validate the pattern before using it to test for matches if (@preg_match($delimiter . $pregex . $delimiter . 'u', '') === false) { $this->addError($path, 'The pattern "' . $pregex . '" is invalid', 'pregex', array('pregex' => $pregex)); continue; } foreach ($element as $i => $value) { if (preg_match($delimiter . $pregex . $delimiter . 'u', $i)) { $matches[] = $i; $this->checkUndefined($value, $schema ?: new \stdClass(), $path, $i, in_array($i, $this->appliedDefaults)); } } } return $matches; } /** * Validates the element properties * * @param \StdClass $element Element to validate * @param array $matches Matches from patternProperties (if any) * @param \StdClass $schema ObjectConstraint definition * @param JsonPointer|null $path Current test path * @param \StdClass $properties Properties * @param mixed $additionalProp Additional properties */ public function validateElement($element, $matches, $schema = null, JsonPointer $path = null, $properties = null, $additionalProp = null) { $this->validateMinMaxConstraint($element, $schema, $path); foreach ($element as $i => $value) { $definition = $this->getProperty($properties, $i); // no additional properties allowed if (!in_array($i, $matches) && $additionalProp === false && $this->inlineSchemaProperty !== $i && !$definition) { $this->addError($path, 'The property ' . $i . ' is not defined and the definition does not allow additional properties', 'additionalProp'); } // additional properties defined if (!in_array($i, $matches) && $additionalProp && !$definition) { if ($additionalProp === true) { $this->checkUndefined($value, null, $path, $i, in_array($i, $this->appliedDefaults)); } else { $this->checkUndefined($value, $additionalProp, $path, $i, in_array($i, $this->appliedDefaults)); } } // property requires presence of another $require = $this->getProperty($definition, 'requires'); if ($require && !$this->getProperty($element, $require)) { $this->addError($path, 'The presence of the property ' . $i . ' requires that ' . $require . ' also be present', 'requires'); } $property = $this->getProperty($element, $i, $this->factory->createInstanceFor('undefined')); if (is_object($property)) { $this->validateMinMaxConstraint(!($property instanceof UndefinedConstraint) ? $property : $element, $definition, $path); } } } /** * Validates the definition properties * * @param \stdClass $element Element to validate * @param \stdClass $properties Property definitions * @param JsonPointer|null $path Path? */ public function validateProperties(&$element, $properties = null, JsonPointer $path = null) { $undefinedConstraint = $this->factory->createInstanceFor('undefined'); foreach ($properties as $i => $value) { $property = &$this->getProperty($element, $i, $undefinedConstraint); $definition = $this->getProperty($properties, $i); if (is_object($definition)) { // Undefined constraint will check for is_object() and quit if is not - so why pass it? $this->checkUndefined($property, $definition, $path, $i, in_array($i, $this->appliedDefaults)); } } } /** * retrieves a property from an object or array * * @param mixed $element Element to validate * @param string $property Property to retrieve * @param mixed $fallback Default value if property is not found * * @return mixed */ protected function &getProperty(&$element, $property, $fallback = null) { if (is_array($element) && (isset($element[$property]) || array_key_exists($property, $element)) /*$this->checkMode == self::CHECK_MODE_TYPE_CAST*/) { return $element[$property]; } elseif (is_object($element) && property_exists($element, $property)) { return $element->$property; } return $fallback; } /** * validating minimum and maximum property constraints (if present) against an element * * @param \stdClass $element Element to validate * @param \stdClass $objectDefinition ObjectConstraint definition * @param JsonPointer|null $path Path to test? */ protected function validateMinMaxConstraint($element, $objectDefinition, JsonPointer $path = null) { // Verify minimum number of properties if (isset($objectDefinition->minProperties) && !is_object($objectDefinition->minProperties)) { if ($this->getTypeCheck()->propertyCount($element) < $objectDefinition->minProperties) { $this->addError($path, 'Must contain a minimum of ' . $objectDefinition->minProperties . ' properties', 'minProperties', array('minProperties' => $objectDefinition->minProperties)); } } // Verify maximum number of properties if (isset($objectDefinition->maxProperties) && !is_object($objectDefinition->maxProperties)) { if ($this->getTypeCheck()->propertyCount($element) > $objectDefinition->maxProperties) { $this->addError($path, 'Must contain no more than ' . $objectDefinition->maxProperties . ' properties', 'maxProperties', array('maxProperties' => $objectDefinition->maxProperties)); } } } } <?php /* * This file is part of the JsonSchema package. * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace JsonSchema\Constraints; use JsonSchema\Constraints\TypeCheck\LooseTypeCheck; use JsonSchema\Entity\JsonPointer; use JsonSchema\Exception\ValidationException; use JsonSchema\Uri\UriResolver; /** * The UndefinedConstraint Constraints * * @author Robert Schönthal <seroscho@googlemail.com> * @author Bruno Prieto Reis <bruno.p.reis@gmail.com> */ class UndefinedConstraint extends Constraint { /** * @var array List of properties to which a default value has been applied */ protected $appliedDefaults = array(); /** * {@inheritdoc} */ public function check(&$value, $schema = null, JsonPointer $path = null, $i = null, $fromDefault = false) { if (is_null($schema) || !is_object($schema)) { return; } $path = $this->incrementPath($path ?: new JsonPointer(''), $i); if ($fromDefault) { $path->setFromDefault(); } // check special properties $this->validateCommonProperties($value, $schema, $path, $i); // check allOf, anyOf, and oneOf properties $this->validateOfProperties($value, $schema, $path, ''); // check known types $this->validateTypes($value, $schema, $path, $i); } /** * Validates the value against the types * * @param mixed $value * @param mixed $schema * @param JsonPointer $path * @param string $i */ public function validateTypes(&$value, $schema, JsonPointer $path, $i = null) { // check array if ($this->getTypeCheck()->isArray($value)) { $this->checkArray($value, $schema, $path, $i); } // check object if (LooseTypeCheck::isObject($value)) { // object processing should always be run on assoc arrays, // so use LooseTypeCheck here even if CHECK_MODE_TYPE_CAST // is not set (i.e. don't use $this->getTypeCheck() here). $this->checkObject( $value, $schema, $path, isset($schema->properties) ? $schema->properties : null, isset($schema->additionalProperties) ? $schema->additionalProperties : null, isset($schema->patternProperties) ? $schema->patternProperties : null, $this->appliedDefaults ); } // check string if (is_string($value)) { $this->checkString($value, $schema, $path, $i); } // check numeric if (is_numeric($value)) { $this->checkNumber($value, $schema, $path, $i); } // check enum if (isset($schema->enum)) { $this->checkEnum($value, $schema, $path, $i); } } /** * Validates common properties * * @param mixed $value * @param mixed $schema * @param JsonPointer $path * @param string $i */ protected function validateCommonProperties(&$value, $schema, JsonPointer $path, $i = '') { // if it extends another schema, it must pass that schema as well if (isset($schema->extends)) { if (is_string($schema->extends)) { $schema->extends = $this->validateUri($schema, $schema->extends); } if (is_array($schema->extends)) { foreach ($schema->extends as $extends) { $this->checkUndefined($value, $extends, $path, $i); } } else { $this->checkUndefined($value, $schema->extends, $path, $i); } } // Apply default values from schema if (!$path->fromDefault()) { $this->applyDefaultValues($value, $schema, $path); } // Verify required values if ($this->getTypeCheck()->isObject($value)) { if (!($value instanceof self) && isset($schema->required) && is_array($schema->required)) { // Draft 4 - Required is an array of strings - e.g. "required": ["foo", ...] foreach ($schema->required as $required) { if (!$this->getTypeCheck()->propertyExists($value, $required)) { $this->addError( $this->incrementPath($path ?: new JsonPointer(''), $required), 'The property ' . $required . ' is required', 'required' ); } } } elseif (isset($schema->required) && !is_array($schema->required)) { // Draft 3 - Required attribute - e.g. "foo": {"type": "string", "required": true} if ($schema->required && $value instanceof self) { $propertyPaths = $path->getPropertyPaths(); $propertyName = end($propertyPaths); $this->addError( $path, 'The property ' . $propertyName . ' is required', 'required' ); } } else { // if the value is both undefined and not required, skip remaining checks // in this method which assume an actual, defined instance when validating. if ($value instanceof self) { return; } } } // Verify type if (!($value instanceof self)) { $this->checkType($value, $schema, $path, $i); } // Verify disallowed items if (isset($schema->disallow)) { $initErrors = $this->getErrors(); $typeSchema = new \stdClass(); $typeSchema->type = $schema->disallow; $this->checkType($value, $typeSchema, $path); // if no new errors were raised it must be a disallowed value if (count($this->getErrors()) == count($initErrors)) { $this->addError($path, 'Disallowed value was matched', 'disallow'); } else { $this->errors = $initErrors; } } if (isset($schema->not)) { $initErrors = $this->getErrors(); $this->checkUndefined($value, $schema->not, $path, $i); // if no new errors were raised then the instance validated against the "not" schema if (count($this->getErrors()) == count($initErrors)) { $this->addError($path, 'Matched a schema which it should not', 'not'); } else { $this->errors = $initErrors; } } // Verify that dependencies are met if (isset($schema->dependencies) && $this->getTypeCheck()->isObject($value)) { $this->validateDependencies($value, $schema->dependencies, $path); } } /** * Check whether a default should be applied for this value * * @param mixed $schema * @param mixed $parentSchema * @param bool $requiredOnly * * @return bool */ private function shouldApplyDefaultValue($requiredOnly, $schema, $name = null, $parentSchema = null) { // required-only mode is off if (!$requiredOnly) { return true; } // draft-04 required is set if ( $name !== null && isset($parentSchema->required) && is_array($parentSchema->required) && in_array($name, $parentSchema->required) ) { return true; } // draft-03 required is set if (isset($schema->required) && !is_array($schema->required) && $schema->required) { return true; } // default case return false; } /** * Apply default values * * @param mixed $value * @param mixed $schema * @param JsonPointer $path */ protected function applyDefaultValues(&$value, $schema, $path) { // only apply defaults if feature is enabled if (!$this->factory->getConfig(self::CHECK_MODE_APPLY_DEFAULTS)) { return; } // apply defaults if appropriate $requiredOnly = $this->factory->getConfig(self::CHECK_MODE_ONLY_REQUIRED_DEFAULTS); if (isset($schema->properties) && LooseTypeCheck::isObject($value)) { // $value is an object or assoc array, and properties are defined - treat as an object foreach ($schema->properties as $currentProperty => $propertyDefinition) { $propertyDefinition = $this->factory->getSchemaStorage()->resolveRefSchema($propertyDefinition); if ( !LooseTypeCheck::propertyExists($value, $currentProperty) && property_exists($propertyDefinition, 'default') && $this->shouldApplyDefaultValue($requiredOnly, $propertyDefinition, $currentProperty, $schema) ) { // assign default value if (is_object($propertyDefinition->default)) { LooseTypeCheck::propertySet($value, $currentProperty, clone $propertyDefinition->default); } else { LooseTypeCheck::propertySet($value, $currentProperty, $propertyDefinition->default); } $this->appliedDefaults[] = $currentProperty; } } } elseif (isset($schema->items) && LooseTypeCheck::isArray($value)) { $items = array(); if (LooseTypeCheck::isArray($schema->items)) { $items = $schema->items; } elseif (isset($schema->minItems) && count($value) < $schema->minItems) { $items = array_fill(count($value), $schema->minItems - count($value), $schema->items); } // $value is an array, and items are defined - treat as plain array foreach ($items as $currentItem => $itemDefinition) { $itemDefinition = $this->factory->getSchemaStorage()->resolveRefSchema($itemDefinition); if ( !array_key_exists($currentItem, $value) && property_exists($itemDefinition, 'default') && $this->shouldApplyDefaultValue($requiredOnly, $itemDefinition)) { if (is_object($itemDefinition->default)) { $value[$currentItem] = clone $itemDefinition->default; } else { $value[$currentItem] = $itemDefinition->default; } } $path->setFromDefault(); } } elseif ( $value instanceof self && property_exists($schema, 'default') && $this->shouldApplyDefaultValue($requiredOnly, $schema)) { // $value is a leaf, not a container - apply the default directly $value = is_object($schema->default) ? clone $schema->default : $schema->default; $path->setFromDefault(); } } /** * Validate allOf, anyOf, and oneOf properties * * @param mixed $value * @param mixed $schema * @param JsonPointer $path * @param string $i */ protected function validateOfProperties(&$value, $schema, JsonPointer $path, $i = '') { // Verify type if ($value instanceof self) { return; } if (isset($schema->allOf)) { $isValid = true; foreach ($schema->allOf as $allOf) { $initErrors = $this->getErrors(); $this->checkUndefined($value, $allOf, $path, $i); $isValid = $isValid && (count($this->getErrors()) == count($initErrors)); } if (!$isValid) { $this->addError($path, 'Failed to match all schemas', 'allOf'); } } if (isset($schema->anyOf)) { $isValid = false; $startErrors = $this->getErrors(); $caughtException = null; foreach ($schema->anyOf as $anyOf) { $initErrors = $this->getErrors(); try { $this->checkUndefined($value, $anyOf, $path, $i); if ($isValid = (count($this->getErrors()) == count($initErrors))) { break; } } catch (ValidationException $e) { $isValid = false; } } if (!$isValid) { $this->addError($path, 'Failed to match at least one schema', 'anyOf'); } else { $this->errors = $startErrors; } } if (isset($schema->oneOf)) { $allErrors = array(); $matchedSchemas = 0; $startErrors = $this->getErrors(); foreach ($schema->oneOf as $oneOf) { try { $this->errors = array(); $this->checkUndefined($value, $oneOf, $path, $i); if (count($this->getErrors()) == 0) { $matchedSchemas++; } $allErrors = array_merge($allErrors, array_values($this->getErrors())); } catch (ValidationException $e) { // deliberately do nothing here - validation failed, but we want to check // other schema options in the OneOf field. } } if ($matchedSchemas !== 1) { $this->addErrors(array_merge($allErrors, $startErrors)); $this->addError($path, 'Failed to match exactly one schema', 'oneOf'); } else { $this->errors = $startErrors; } } } /** * Validate dependencies * * @param mixed $value * @param mixed $dependencies * @param JsonPointer $path * @param string $i */ protected function validateDependencies($value, $dependencies, JsonPointer $path, $i = '') { foreach ($dependencies as $key => $dependency) { if ($this->getTypeCheck()->propertyExists($value, $key)) { if (is_string($dependency)) { // Draft 3 string is allowed - e.g. "dependencies": {"bar": "foo"} if (!$this->getTypeCheck()->propertyExists($value, $dependency)) { $this->addError($path, "$key depends on $dependency and $dependency is missing", 'dependencies'); } } elseif (is_array($dependency)) { // Draft 4 must be an array - e.g. "dependencies": {"bar": ["foo"]} foreach ($dependency as $d) { if (!$this->getTypeCheck()->propertyExists($value, $d)) { $this->addError($path, "$key depends on $d and $d is missing", 'dependencies'); } } } elseif (is_object($dependency)) { // Schema - e.g. "dependencies": {"bar": {"properties": {"foo": {...}}}} $this->checkUndefined($value, $dependency, $path, $i); } } } } protected function validateUri($schema, $schemaUri = null) { $resolver = new UriResolver(); $retriever = $this->factory->getUriRetriever(); $jsonSchema = null; if ($resolver->isValid($schemaUri)) { $schemaId = property_exists($schema, 'id') ? $schema->id : null; $jsonSchema = $retriever->retrieve($schemaId, $schemaUri); } return $jsonSchema; } } <?php /* * This file is part of the JsonSchema package. * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace JsonSchema\Constraints; use JsonSchema\Entity\JsonPointer; use JsonSchema\Exception\InvalidArgumentException; use UnexpectedValueException as StandardUnexpectedValueException; /** * The TypeConstraint Constraints, validates an element against a given type * * @author Robert Schönthal <seroscho@googlemail.com> * @author Bruno Prieto Reis <bruno.p.reis@gmail.com> */ class TypeConstraint extends Constraint { /** * @var array|string[] type wordings for validation error messages */ public static $wording = array( 'integer' => 'an integer', 'number' => 'a number', 'boolean' => 'a boolean', 'object' => 'an object', 'array' => 'an array', 'string' => 'a string', 'null' => 'a null', 'any' => null, // validation of 'any' is always true so is not needed in message wording 0 => null, // validation of a false-y value is always true, so not needed as well ); /** * {@inheritdoc} */ public function check(&$value = null, $schema = null, JsonPointer $path = null, $i = null) { $type = isset($schema->type) ? $schema->type : null; $isValid = false; $wording = array(); if (is_array($type)) { $this->validateTypesArray($value, $type, $wording, $isValid, $path); } elseif (is_object($type)) { $this->checkUndefined($value, $type, $path); return; } else { $isValid = $this->validateType($value, $type); } if ($isValid === false) { if (!is_array($type)) { $this->validateTypeNameWording($type); $wording[] = self::$wording[$type]; } $this->addError($path, ucwords(gettype($value)) . ' value found, but ' . $this->implodeWith($wording, ', ', 'or') . ' is required', 'type'); } } /** * Validates the given $value against the array of types in $type. Sets the value * of $isValid to true, if at least one $type mateches the type of $value or the value * passed as $isValid is already true. * * @param mixed $value Value to validate * @param array $type TypeConstraints to check agains * @param array $validTypesWording An array of wordings of the valid types of the array $type * @param bool $isValid The current validation value * @param $path */ protected function validateTypesArray(&$value, array $type, &$validTypesWording, &$isValid, $path) { foreach ($type as $tp) { // $tp can be an object, if it's a schema instead of a simple type, validate it // with a new type constraint if (is_object($tp)) { if (!$isValid) { $validator = $this->factory->createInstanceFor('type'); $subSchema = new \stdClass(); $subSchema->type = $tp; $validator->check($value, $subSchema, $path, null); $error = $validator->getErrors(); $isValid = !(bool) $error; $validTypesWording[] = self::$wording['object']; } } else { $this->validateTypeNameWording($tp); $validTypesWording[] = self::$wording[$tp]; if (!$isValid) { $isValid = $this->validateType($value, $tp); } } } } /** * Implodes the given array like implode() with turned around parameters and with the * difference, that, if $listEnd isn't false, the last element delimiter is $listEnd instead of * $delimiter. * * @param array $elements The elements to implode * @param string $delimiter The delimiter to use * @param bool $listEnd The last delimiter to use (defaults to $delimiter) * * @return string */ protected function implodeWith(array $elements, $delimiter = ', ', $listEnd = false) { if ($listEnd === false || !isset($elements[1])) { return implode($delimiter, $elements); } $lastElement = array_slice($elements, -1); $firsElements = join($delimiter, array_slice($elements, 0, -1)); $implodedElements = array_merge(array($firsElements), $lastElement); return join(" $listEnd ", $implodedElements); } /** * Validates the given $type, if there's an associated self::$wording. If not, throws an * exception. * * @param string $type The type to validate * * @throws StandardUnexpectedValueException */ protected function validateTypeNameWording($type) { if (!isset(self::$wording[$type])) { throw new StandardUnexpectedValueException( sprintf( 'No wording for %s available, expected wordings are: [%s]', var_export($type, true), implode(', ', array_filter(self::$wording))) ); } } /** * Verifies that a given value is of a certain type * * @param mixed $value Value to validate * @param string $type TypeConstraint to check against * * @throws InvalidArgumentException * * @return bool */ protected function validateType(&$value, $type) { //mostly the case for inline schema if (!$type) { return true; } if ('any' === $type) { return true; } if ('object' === $type) { return $this->getTypeCheck()->isObject($value); } if ('array' === $type) { return $this->getTypeCheck()->isArray($value); } $coerce = $this->factory->getConfig(Constraint::CHECK_MODE_COERCE_TYPES); if ('integer' === $type) { if ($coerce) { $value = $this->toInteger($value); } return is_int($value); } if ('number' === $type) { if ($coerce) { $value = $this->toNumber($value); } return is_numeric($value) && !is_string($value); } if ('boolean' === $type) { if ($coerce) { $value = $this->toBoolean($value); } return is_bool($value); } if ('string' === $type) { return is_string($value); } if ('email' === $type) { return is_string($value); } if ('null' === $type) { return is_null($value); } throw new InvalidArgumentException((is_object($value) ? 'object' : $value) . ' is an invalid type for ' . $type); } /** * Converts a value to boolean. For example, "true" becomes true. * * @param $value The value to convert to boolean * * @return bool|mixed */ protected function toBoolean($value) { if ($value === 'true') { return true; } if ($value === 'false') { return false; } return $value; } /** * Converts a numeric string to a number. For example, "4" becomes 4. * * @param mixed $value the value to convert to a number * * @return int|float|mixed */ protected function toNumber($value) { if (is_numeric($value)) { return $value + 0; // cast to number } return $value; } protected function toInteger($value) { if (is_numeric($value) && (int) $value == $value) { return (int) $value; // cast to number } return $value; } } <?php /* * This file is part of the JsonSchema package. * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace JsonSchema\Constraints; use JsonSchema\Entity\JsonPointer; /** * The EnumConstraint Constraints, validates an element against a given set of possibilities * * @author Robert Schönthal <seroscho@googlemail.com> * @author Bruno Prieto Reis <bruno.p.reis@gmail.com> */ class EnumConstraint extends Constraint { /** * {@inheritdoc} */ public function check(&$element, $schema = null, JsonPointer $path = null, $i = null) { // Only validate enum if the attribute exists if ($element instanceof UndefinedConstraint && (!isset($schema->required) || !$schema->required)) { return; } $type = gettype($element); foreach ($schema->enum as $enum) { $enumType = gettype($enum); if ($this->factory->getConfig(self::CHECK_MODE_TYPE_CAST) && $type == 'array' && $enumType == 'object') { if ((object) $element == $enum) { return; } } if ($type === gettype($enum)) { if ($type == 'object') { if ($element == $enum) { return; } } elseif ($element === $enum) { return; } } } $this->addError($path, 'Does not have a value in the enumeration ' . json_encode($schema->enum), 'enum', array('enum' => $schema->enum)); } } <?php /* * This file is part of the JsonSchema package. * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace JsonSchema\Constraints; use JsonSchema\Entity\JsonPointer; use JsonSchema\Exception\InvalidArgumentException; use JsonSchema\Exception\ValidationException; use JsonSchema\Validator; /** * A more basic constraint definition - used for the public * interface to avoid exposing library internals. */ class BaseConstraint { /** * @var array Errors */ protected $errors = array(); /** * @var int All error types which have occurred */ protected $errorMask = Validator::ERROR_NONE; /** * @var Factory */ protected $factory; /** * @param Factory $factory */ public function __construct(Factory $factory = null) { $this->factory = $factory ?: new Factory(); } public function addError(JsonPointer $path = null, $message, $constraint = '', array $more = null) { $error = array( 'property' => $this->convertJsonPointerIntoPropertyPath($path ?: new JsonPointer('')), 'pointer' => ltrim(strval($path ?: new JsonPointer('')), '#'), 'message' => $message, 'constraint' => $constraint, 'context' => $this->factory->getErrorContext(), ); if ($this->factory->getConfig(Constraint::CHECK_MODE_EXCEPTIONS)) { throw new ValidationException(sprintf('Error validating %s: %s', $error['pointer'], $error['message'])); } if (is_array($more) && count($more) > 0) { $error += $more; } $this->errors[] = $error; $this->errorMask |= $error['context']; } public function addErrors(array $errors) { if ($errors) { $this->errors = array_merge($this->errors, $errors); $errorMask = &$this->errorMask; array_walk($errors, function ($error) use (&$errorMask) { if (isset($error['context'])) { $errorMask |= $error['context']; } }); } } public function getErrors($errorContext = Validator::ERROR_ALL) { if ($errorContext === Validator::ERROR_ALL) { return $this->errors; } return array_filter($this->errors, function ($error) use ($errorContext) { if ($errorContext & $error['context']) { return true; } }); } public function numErrors($errorContext = Validator::ERROR_ALL) { if ($errorContext === Validator::ERROR_ALL) { return count($this->errors); } return count($this->getErrors($errorContext)); } public function isValid() { return !$this->getErrors(); } /** * Clears any reported errors. Should be used between * multiple validation checks. */ public function reset() { $this->errors = array(); $this->errorMask = Validator::ERROR_NONE; } /** * Get the error mask * * @return int */ public function getErrorMask() { return $this->errorMask; } /** * Recursively cast an associative array to an object * * @param array $array * * @return object */ public static function arrayToObjectRecursive($array) { $json = json_encode($array); if (json_last_error() !== \JSON_ERROR_NONE) { $message = 'Unable to encode schema array as JSON'; if (function_exists('json_last_error_msg')) { $message .= ': ' . json_last_error_msg(); } throw new InvalidArgumentException($message); } return (object) json_decode($json); } } <?php /* * This file is part of the JsonSchema package. * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace JsonSchema\Iterator; /** * @package JsonSchema\Iterator * * @author Joost Nijhuis <jnijhuis81@gmail.com> */ class ObjectIterator implements \Iterator, \Countable { /** @var object */ private $object; /** @var int */ private $position = 0; /** @var array */ private $data = array(); /** @var bool */ private $initialized = false; /** * @param object $object */ public function __construct($object) { $this->object = $object; } /** * {@inheritdoc} */ public function current() { $this->initialize(); return $this->data[$this->position]; } /** * {@inheritdoc} */ public function next() { $this->initialize(); $this->position++; } /** * {@inheritdoc} */ public function key() { $this->initialize(); return $this->position; } /** * {@inheritdoc} */ public function valid() { $this->initialize(); return isset($this->data[$this->position]); } /** * {@inheritdoc} */ public function rewind() { $this->initialize(); $this->position = 0; } /** * {@inheritdoc} */ public function count() { $this->initialize(); return count($this->data); } /** * Initializer */ private function initialize() { if (!$this->initialized) { $this->data = $this->buildDataFromObject($this->object); $this->initialized = true; } } /** * @param object $object * * @return array */ private function buildDataFromObject($object) { $result = array(); $stack = new \SplStack(); $stack->push($object); while (!$stack->isEmpty()) { $current = $stack->pop(); if (is_object($current)) { array_push($result, $current); } foreach ($this->getDataFromItem($current) as $propertyName => $propertyValue) { if (is_object($propertyValue) || is_array($propertyValue)) { $stack->push($propertyValue); } } } return $result; } /** * @param object|array $item * * @return array */ private function getDataFromItem($item) { if (!is_object($item) && !is_array($item)) { return array(); } return is_object($item) ? get_object_vars($item) : $item; } } <?php /* * This file is part of the JsonSchema package. * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace JsonSchema\Uri; use JsonSchema\Exception\UriResolverException; use JsonSchema\UriResolverInterface; /** * Resolves JSON Schema URIs * * @author Sander Coolen <sander@jibber.nl> */ class UriResolver implements UriResolverInterface { /** * Parses a URI into five main components * * @param string $uri * * @return array */ public function parse($uri) { preg_match('|^(([^:/?#]+):)?(//([^/?#]*))?([^?#]*)(\?([^#]*))?(#(.*))?|', $uri, $match); $components = array(); if (5 < count($match)) { $components = array( 'scheme' => $match[2], 'authority' => $match[4], 'path' => $match[5] ); } if (7 < count($match)) { $components['query'] = $match[7]; } if (9 < count($match)) { $components['fragment'] = $match[9]; } return $components; } /** * Builds a URI based on n array with the main components * * @param array $components * * @return string */ public function generate(array $components) { $uri = $components['scheme'] . '://' . $components['authority'] . $components['path']; if (array_key_exists('query', $components) && strlen($components['query'])) { $uri .= '?' . $components['query']; } if (array_key_exists('fragment', $components)) { $uri .= '#' . $components['fragment']; } return $uri; } /** * {@inheritdoc} */ public function resolve($uri, $baseUri = null) { // treat non-uri base as local file path if ( !is_null($baseUri) && !filter_var($baseUri, \FILTER_VALIDATE_URL) && !preg_match('|^[^/]+://|u', $baseUri) ) { if (is_file($baseUri)) { $baseUri = 'file://' . realpath($baseUri); } elseif (is_dir($baseUri)) { $baseUri = 'file://' . realpath($baseUri) . '/'; } else { $baseUri = 'file://' . getcwd() . '/' . $baseUri; } } if ($uri == '') { return $baseUri; } $components = $this->parse($uri); $path = $components['path']; if (!empty($components['scheme'])) { return $uri; } $baseComponents = $this->parse($baseUri); $basePath = $baseComponents['path']; $baseComponents['path'] = self::combineRelativePathWithBasePath($path, $basePath); if (isset($components['fragment'])) { $baseComponents['fragment'] = $components['fragment']; } return $this->generate($baseComponents); } /** * Tries to glue a relative path onto an absolute one * * @param string $relativePath * @param string $basePath * * @throws UriResolverException * * @return string Merged path */ public static function combineRelativePathWithBasePath($relativePath, $basePath) { $relativePath = self::normalizePath($relativePath); if ($relativePath == '') { return $basePath; } if ($relativePath[0] == '/') { return $relativePath; } $basePathSegments = explode('/', $basePath); preg_match('|^/?(\.\./(?:\./)*)*|', $relativePath, $match); $numLevelUp = strlen($match[0]) /3 + 1; if ($numLevelUp >= count($basePathSegments)) { throw new UriResolverException(sprintf("Unable to resolve URI '%s' from base '%s'", $relativePath, $basePath)); } $basePathSegments = array_slice($basePathSegments, 0, -$numLevelUp); $path = preg_replace('|^/?(\.\./(\./)*)*|', '', $relativePath); return implode('/', $basePathSegments) . '/' . $path; } /** * Normalizes a URI path component by removing dot-slash and double slashes * * @param string $path * * @return string */ private static function normalizePath($path) { $path = preg_replace('|((?<!\.)\./)*|', '', $path); $path = preg_replace('|//|', '/', $path); return $path; } /** * @param string $uri * * @return bool */ public function isValid($uri) { $components = $this->parse($uri); return !empty($components); } } <?php /* * This file is part of the JsonSchema package. * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace JsonSchema\Uri; use JsonSchema\Exception\InvalidSchemaMediaTypeException; use JsonSchema\Exception\JsonDecodingException; use JsonSchema\Exception\ResourceNotFoundException; use JsonSchema\Uri\Retrievers\FileGetContents; use JsonSchema\Uri\Retrievers\UriRetrieverInterface; use JsonSchema\UriRetrieverInterface as BaseUriRetrieverInterface; use JsonSchema\Validator; /** * Retrieves JSON Schema URIs * * @author Tyler Akins <fidian@rumkin.com> */ class UriRetriever implements BaseUriRetrieverInterface { /** * @var array Map of URL translations */ protected $translationMap = array( // use local copies of the spec schemas '|^https?://json-schema.org/draft-(0[34])/schema#?|' => 'package://dist/schema/json-schema-draft-$1.json' ); /** * @var array A list of endpoints for media type check exclusion */ protected $allowedInvalidContentTypeEndpoints = array( 'http://json-schema.org/', 'https://json-schema.org/' ); /** * @var null|UriRetrieverInterface */ protected $uriRetriever = null; /** * @var array|object[] * * @see loadSchema */ private $schemaCache = array(); /** * Adds an endpoint to the media type validation exclusion list * * @param string $endpoint */ public function addInvalidContentTypeEndpoint($endpoint) { $this->allowedInvalidContentTypeEndpoints[] = $endpoint; } /** * Guarantee the correct media type was encountered * * @param UriRetrieverInterface $uriRetriever * @param string $uri * * @return bool|void */ public function confirmMediaType($uriRetriever, $uri) { $contentType = $uriRetriever->getContentType(); if (is_null($contentType)) { // Well, we didn't get an invalid one return; } if (in_array($contentType, array(Validator::SCHEMA_MEDIA_TYPE, 'application/json'))) { return; } foreach ($this->allowedInvalidContentTypeEndpoints as $endpoint) { if (strpos($uri, $endpoint) === 0) { return true; } } throw new InvalidSchemaMediaTypeException(sprintf('Media type %s expected', Validator::SCHEMA_MEDIA_TYPE)); } /** * Get a URI Retriever * * If none is specified, sets a default FileGetContents retriever and * returns that object. * * @return UriRetrieverInterface */ public function getUriRetriever() { if (is_null($this->uriRetriever)) { $this->setUriRetriever(new FileGetContents()); } return $this->uriRetriever; } /** * Resolve a schema based on pointer * * URIs can have a fragment at the end in the format of * #/path/to/object and we are to look up the 'path' property of * the first object then the 'to' and 'object' properties. * * @param object $jsonSchema JSON Schema contents * @param string $uri JSON Schema URI * * @throws ResourceNotFoundException * * @return object JSON Schema after walking down the fragment pieces */ public function resolvePointer($jsonSchema, $uri) { $resolver = new UriResolver(); $parsed = $resolver->parse($uri); if (empty($parsed['fragment'])) { return $jsonSchema; } $path = explode('/', $parsed['fragment']); while ($path) { $pathElement = array_shift($path); if (!empty($pathElement)) { $pathElement = str_replace('~1', '/', $pathElement); $pathElement = str_replace('~0', '~', $pathElement); if (!empty($jsonSchema->$pathElement)) { $jsonSchema = $jsonSchema->$pathElement; } else { throw new ResourceNotFoundException( 'Fragment "' . $parsed['fragment'] . '" not found' . ' in ' . $uri ); } if (!is_object($jsonSchema)) { throw new ResourceNotFoundException( 'Fragment part "' . $pathElement . '" is no object ' . ' in ' . $uri ); } } } return $jsonSchema; } /** * {@inheritdoc} */ public function retrieve($uri, $baseUri = null, $translate = true) { $resolver = new UriResolver(); $resolvedUri = $fetchUri = $resolver->resolve($uri, $baseUri); //fetch URL without #fragment $arParts = $resolver->parse($resolvedUri); if (isset($arParts['fragment'])) { unset($arParts['fragment']); $fetchUri = $resolver->generate($arParts); } // apply URI translations if ($translate) { $fetchUri = $this->translate($fetchUri); } $jsonSchema = $this->loadSchema($fetchUri); // Use the JSON pointer if specified $jsonSchema = $this->resolvePointer($jsonSchema, $resolvedUri); if ($jsonSchema instanceof \stdClass) { $jsonSchema->id = $resolvedUri; } return $jsonSchema; } /** * Fetch a schema from the given URI, json-decode it and return it. * Caches schema objects. * * @param string $fetchUri Absolute URI * * @return object JSON schema object */ protected function loadSchema($fetchUri) { if (isset($this->schemaCache[$fetchUri])) { return $this->schemaCache[$fetchUri]; } $uriRetriever = $this->getUriRetriever(); $contents = $this->uriRetriever->retrieve($fetchUri); $this->confirmMediaType($uriRetriever, $fetchUri); $jsonSchema = json_decode($contents); if (JSON_ERROR_NONE < $error = json_last_error()) { throw new JsonDecodingException($error); } $this->schemaCache[$fetchUri] = $jsonSchema; return $jsonSchema; } /** * Set the URI Retriever * * @param UriRetrieverInterface $uriRetriever * * @return $this for chaining */ public function setUriRetriever(UriRetrieverInterface $uriRetriever) { $this->uriRetriever = $uriRetriever; return $this; } /** * Parses a URI into five main components * * @param string $uri * * @return array */ public function parse($uri) { preg_match('|^(([^:/?#]+):)?(//([^/?#]*))?([^?#]*)(\?([^#]*))?(#(.*))?|', $uri, $match); $components = array(); if (5 < count($match)) { $components = array( 'scheme' => $match[2], 'authority' => $match[4], 'path' => $match[5] ); } if (7 < count($match)) { $components['query'] = $match[7]; } if (9 < count($match)) { $components['fragment'] = $match[9]; } return $components; } /** * Builds a URI based on n array with the main components * * @param array $components * * @return string */ public function generate(array $components) { $uri = $components['scheme'] . '://' . $components['authority'] . $components['path']; if (array_key_exists('query', $components)) { $uri .= $components['query']; } if (array_key_exists('fragment', $components)) { $uri .= $components['fragment']; } return $uri; } /** * Resolves a URI * * @param string $uri Absolute or relative * @param string $baseUri Optional base URI * * @return string */ public function resolve($uri, $baseUri = null) { $components = $this->parse($uri); $path = $components['path']; if ((array_key_exists('scheme', $components)) && ('http' === $components['scheme'])) { return $uri; } $baseComponents = $this->parse($baseUri); $basePath = $baseComponents['path']; $baseComponents['path'] = UriResolver::combineRelativePathWithBasePath($path, $basePath); return $this->generate($baseComponents); } /** * @param string $uri * * @return bool */ public function isValid($uri) { $components = $this->parse($uri); return !empty($components); } /** * Set a URL translation rule */ public function setTranslation($from, $to) { $this->translationMap[$from] = $to; } /** * Apply URI translation rules */ public function translate($uri) { foreach ($this->translationMap as $from => $to) { $uri = preg_replace($from, $to, $uri); } // translate references to local files within the json-schema package $uri = preg_replace('|^package://|', sprintf('file://%s/', realpath(__DIR__ . '/../../..')), $uri); return $uri; } } <?php /* * This file is part of the JsonSchema package. * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace JsonSchema\Uri\Retrievers; /** * Interface for URI retrievers * * @author Sander Coolen <sander@jibber.nl> */ interface UriRetrieverInterface { /** * Retrieve a schema from the specified URI * * @param string $uri URI that resolves to a JSON schema * * @throws \JsonSchema\Exception\ResourceNotFoundException * * @return mixed string|null */ public function retrieve($uri); /** * Get media content type * * @return string */ public function getContentType(); } <?php namespace JsonSchema\Uri\Retrievers; use JsonSchema\Validator; /** * URI retrieved based on a predefined array of schemas * * @example * * $retriever = new PredefinedArray(array( * 'http://acme.com/schemas/person#' => '{ ... }', * 'http://acme.com/schemas/address#' => '{ ... }', * )) * * $schema = $retriever->retrieve('http://acme.com/schemas/person#'); */ class PredefinedArray extends AbstractRetriever { /** * Contains schemas as URI => JSON * * @var array */ private $schemas; /** * Constructor * * @param array $schemas * @param string $contentType */ public function __construct(array $schemas, $contentType = Validator::SCHEMA_MEDIA_TYPE) { $this->schemas = $schemas; $this->contentType = $contentType; } /** * {@inheritdoc} * * @see \JsonSchema\Uri\Retrievers\UriRetrieverInterface::retrieve() */ public function retrieve($uri) { if (!array_key_exists($uri, $this->schemas)) { throw new \JsonSchema\Exception\ResourceNotFoundException(sprintf( 'The JSON schema "%s" was not found.', $uri )); } return $this->schemas[$uri]; } } <?php /* * This file is part of the JsonSchema package. * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace JsonSchema\Uri\Retrievers; use JsonSchema\Exception\ResourceNotFoundException; /** * Tries to retrieve JSON schemas from a URI using file_get_contents() * * @author Sander Coolen <sander@jibber.nl> */ class FileGetContents extends AbstractRetriever { protected $messageBody; /** * {@inheritdoc} * * @see \JsonSchema\Uri\Retrievers\UriRetrieverInterface::retrieve() */ public function retrieve($uri) { $errorMessage = null; set_error_handler(function ($errno, $errstr) use (&$errorMessage) { $errorMessage = $errstr; }); $response = file_get_contents($uri); restore_error_handler(); if ($errorMessage) { throw new ResourceNotFoundException($errorMessage); } if (false === $response) { throw new ResourceNotFoundException('JSON schema not found at ' . $uri); } if ($response == '' && substr($uri, 0, 7) == 'file://' && substr($uri, -1) == '/' ) { throw new ResourceNotFoundException('JSON schema not found at ' . $uri); } $this->messageBody = $response; if (!empty($http_response_header)) { // $http_response_header cannot be tested, because it's defined in the method's local scope // See http://php.net/manual/en/reserved.variables.httpresponseheader.php for more info. $this->fetchContentType($http_response_header); // @codeCoverageIgnore } else { // @codeCoverageIgnore // Could be a "file://" url or something else - fake up the response $this->contentType = null; } return $this->messageBody; } /** * @param array $headers HTTP Response Headers * * @return bool Whether the Content-Type header was found or not */ private function fetchContentType(array $headers) { foreach ($headers as $header) { if ($this->contentType = self::getContentTypeMatchInHeader($header)) { return true; } } return false; } /** * @param string $header * * @return string|null */ protected static function getContentTypeMatchInHeader($header) { if (0 < preg_match("/Content-Type:(\V*)/ims", $header, $match)) { return trim($match[1]); } return null; } } <?php /* * This file is part of the JsonSchema package. * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace JsonSchema\Uri\Retrievers; use JsonSchema\Exception\RuntimeException; use JsonSchema\Validator; /** * Tries to retrieve JSON schemas from a URI using cURL library * * @author Sander Coolen <sander@jibber.nl> */ class Curl extends AbstractRetriever { protected $messageBody; public function __construct() { if (!function_exists('curl_init')) { // Cannot test this, because curl_init is present on all test platforms plus mock throw new RuntimeException('cURL not installed'); // @codeCoverageIgnore } } /** * {@inheritdoc} * * @see \JsonSchema\Uri\Retrievers\UriRetrieverInterface::retrieve() */ public function retrieve($uri) { $ch = curl_init(); curl_setopt($ch, CURLOPT_URL, $uri); curl_setopt($ch, CURLOPT_HEADER, true); curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); curl_setopt($ch, CURLOPT_HTTPHEADER, array('Accept: ' . Validator::SCHEMA_MEDIA_TYPE)); $response = curl_exec($ch); if (false === $response) { throw new \JsonSchema\Exception\ResourceNotFoundException('JSON schema not found'); } $this->fetchMessageBody($response); $this->fetchContentType($response); curl_close($ch); return $this->messageBody; } /** * @param string $response cURL HTTP response */ private function fetchMessageBody($response) { preg_match("/(?:\r\n){2}(.*)$/ms", $response, $match); $this->messageBody = $match[1]; } /** * @param string $response cURL HTTP response * * @return bool Whether the Content-Type header was found or not */ protected function fetchContentType($response) { if (0 < preg_match("/Content-Type:(\V*)/ims", $response, $match)) { $this->contentType = trim($match[1]); return true; } return false; } } <?php /** * JsonSchema * * @filesource */ namespace JsonSchema\Uri\Retrievers; /** * AbstractRetriever implements the default shared behavior * that all descendant Retrievers should inherit * * @author Steven Garcia <webwhammy@gmail.com> */ abstract class AbstractRetriever implements UriRetrieverInterface { /** * Media content type * * @var string */ protected $contentType; /** * {@inheritdoc} * * @see \JsonSchema\Uri\Retrievers\UriRetrieverInterface::getContentType() */ public function getContentType() { return $this->contentType; } } <?php /* * This file is part of the JsonSchema package. * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace JsonSchema; use JsonSchema\Constraints\BaseConstraint; use JsonSchema\Constraints\Constraint; /** * A JsonSchema Constraint * * @author Robert Schönthal <seroscho@googlemail.com> * @author Bruno Prieto Reis <bruno.p.reis@gmail.com> * * @see README.md */ class Validator extends BaseConstraint { const SCHEMA_MEDIA_TYPE = 'application/schema+json'; const ERROR_NONE = 0x00000000; const ERROR_ALL = 0xFFFFFFFF; const ERROR_DOCUMENT_VALIDATION = 0x00000001; const ERROR_SCHEMA_VALIDATION = 0x00000002; /** * Validates the given data against the schema and returns an object containing the results * Both the php object and the schema are supposed to be a result of a json_decode call. * The validation works as defined by the schema proposal in http://json-schema.org. * * Note that the first argument is passed by reference, so you must pass in a variable. */ public function validate(&$value, $schema = null, $checkMode = null) { // make sure $schema is an object if (is_array($schema)) { $schema = self::arrayToObjectRecursive($schema); } // set checkMode $initialCheckMode = $this->factory->getConfig(); if ($checkMode !== null) { $this->factory->setConfig($checkMode); } // add provided schema to SchemaStorage with internal URI to allow internal $ref resolution if (is_object($schema) && property_exists($schema, 'id')) { $schemaURI = $schema->id; } else { $schemaURI = SchemaStorage::INTERNAL_PROVIDED_SCHEMA_URI; } $this->factory->getSchemaStorage()->addSchema($schemaURI, $schema); $validator = $this->factory->createInstanceFor('schema'); $validator->check( $value, $this->factory->getSchemaStorage()->getSchema($schemaURI) ); $this->factory->setConfig($initialCheckMode); $this->addErrors(array_unique($validator->getErrors(), SORT_REGULAR)); return $validator->getErrorMask(); } /** * Alias to validate(), to maintain backwards-compatibility with the previous API */ public function check($value, $schema) { return $this->validate($value, $schema); } /** * Alias to validate(), to maintain backwards-compatibility with the previous API */ public function coerce(&$value, $schema) { return $this->validate($value, $schema, Constraint::CHECK_MODE_COERCE_TYPES); } } <?php namespace JsonSchema; interface SchemaStorageInterface { /** * Adds schema with given identifier * * @param string $id * @param object $schema */ public function addSchema($id, $schema = null); /** * Returns schema for given identifier, or null if it does not exist * * @param string $id * * @return object */ public function getSchema($id); /** * Returns schema for given reference with all sub-references resolved * * @param string $ref * * @return object */ public function resolveRef($ref); /** * Returns schema referenced by '$ref' property * * @param mixed $refSchema * * @return object */ public function resolveRefSchema($refSchema); } <?php namespace Gettext; /** * Interface used by all translators. */ interface TranslatorInterface { /** * Register this translator as global, to use with the gettext functions __(), p__(), etc. * Returns the previous translator if exists. * * @return TranslatorInterface|null */ public function register(); /** * Noop, marks the string for translation but returns it unchanged. * * @param string $original * * @return string */ public function noop($original); /** * Gets a translation using the original string. * * @param string $original * * @return string */ public function gettext($original); /** * Gets a translation checking the plural form. * * @param string $original * @param string $plural * @param string $value * * @return string */ public function ngettext($original, $plural, $value); /** * Gets a translation checking the domain and the plural form. * * @param string $domain * @param string $original * @param string $plural * @param string $value * * @return string */ public function dngettext($domain, $original, $plural, $value); /** * Gets a translation checking the context and the plural form. * * @param string $context * @param string $original * @param string $plural * @param string $value * * @return string */ public function npgettext($context, $original, $plural, $value); /** * Gets a translation checking the context. * * @param string $context * @param string $original * * @return string */ public function pgettext($context, $original); /** * Gets a translation checking the domain. * * @param string $domain * @param string $original * * @return string */ public function dgettext($domain, $original); /** * Gets a translation checking the domain and context. * * @param string $domain * @param string $context * @param string $original * * @return string */ public function dpgettext($domain, $context, $original); /** * Gets a translation checking the domain, the context and the plural form. * * @param string $domain * @param string $context * @param string $original * @param string $plural * @param string $value */ public function dnpgettext($domain, $context, $original, $plural, $value); } <?php namespace Gettext; /** * Class to manage a translation string. */ class Translation { protected $id; protected $context; protected $original; protected $translation = ''; protected $plural; protected $pluralTranslation = []; protected $references = []; protected $comments = []; protected $extractedComments = []; protected $flags = []; protected $disabled = false; /** * Generates the id of a translation (context + glue + original). * * @param string $context * @param string $original * * @return string */ public static function generateId($context, $original) { return "{$context}\004{$original}"; } /** * Create a new instance of a Translation object. * * This is a factory method that will work even when Translation is extended. * * @param string $context The context of the translation * @param string $original The original string * @param string $plural The original plural string * @return static New Translation instance */ public static function create($context, $original, $plural = '') { return new static($context, $original, $plural); } /** * Construct. * * @param string $context The context of the translation * @param string $original The original string * @param string $plural The original plural string */ public function __construct($context, $original, $plural = '') { $this->context = (string) $context; $this->original = (string) $original; $this->setPlural($plural); } /** * Clones this translation. * * @param null|string $context Optional new context * @param null|string $original Optional new original * * @return Translation */ public function getClone($context = null, $original = null) { $new = clone $this; if ($context !== null) { $new->context = (string) $context; } if ($original !== null) { $new->original = (string) $original; } return $new; } /** * Sets the id of this translation. * @warning The use of this function to set a custom ID will prevent * Translations::find from matching this translation. * * @param string $id */ public function setId($id) { $this->id = $id; } /** * Returns the id of this translation. * * @return string */ public function getId() { if ($this->id === null) { return static::generateId($this->context, $this->original); } return $this->id; } /** * Checks whether the translation matches with the arguments. * * @param string $context * @param string $original * * @return bool */ public function is($context, $original = '') { return (($this->context === $context) && ($this->original === $original)) ? true : false; } /** * Enable or disable the translation * * @param bool $disabled * * @return self */ public function setDisabled($disabled) { $this->disabled = (bool) $disabled; return $this; } /** * Returns whether the translation is disabled * * @return bool */ public function isDisabled() { return $this->disabled; } /** * Gets the original string. * * @return string */ public function getOriginal() { return $this->original; } /** * Checks if the original string is empty or not. * * @return bool */ public function hasOriginal() { return ($this->original !== '') ? true : false; } /** * Sets the translation string. * * @param string $translation * * @return self */ public function setTranslation($translation) { $this->translation = (string) $translation; return $this; } /** * Gets the translation string. * * @return string */ public function getTranslation() { return $this->translation; } /** * Checks if the translation string is empty or not. * * @return bool */ public function hasTranslation() { return ($this->translation !== '') ? true : false; } /** * Sets the plural translation string. * * @param string $plural * * @return self */ public function setPlural($plural) { $this->plural = (string) $plural; return $this; } /** * Gets the plural translation string. * * @return string */ public function getPlural() { return $this->plural; } /** * Checks if the plural translation string is empty or not. * * @return bool */ public function hasPlural() { return ($this->plural !== '') ? true : false; } /** * Set a new plural translation. * * @param array $plural * * @return self */ public function setPluralTranslations(array $plural) { $this->pluralTranslation = $plural; return $this; } /** * Gets all plural translations. * * @param int $size * * @return array */ public function getPluralTranslations($size = null) { if ($size === null) { return $this->pluralTranslation; } $current = count($this->pluralTranslation); if ($size > $current) { return $this->pluralTranslation + array_fill(0, $size, ''); } if ($size < $current) { return array_slice($this->pluralTranslation, 0, $size); } return $this->pluralTranslation; } /** * Checks if there are any plural translation. * * @param bool $checkContent * * @return bool */ public function hasPluralTranslations($checkContent = false) { if ($checkContent) { return implode('', $this->pluralTranslation) !== ''; } return !empty($this->pluralTranslation); } /** * Removes all plural translations. * * @return self */ public function deletePluralTranslation() { $this->pluralTranslation = []; return $this; } /** * Gets the context of this translation. * * @return string */ public function getContext() { return $this->context; } /** * Checks if the context is empty or not. * * @return bool */ public function hasContext() { return (isset($this->context) && ($this->context !== '')) ? true : false; } /** * Adds a new reference for this translation. * * @param string $filename The file path where the translation has been found * @param null|int $line The line number where the translation has been found * * @return self */ public function addReference($filename, $line = null) { $key = "{$filename}:{$line}"; $this->references[$key] = [$filename, $line]; return $this; } /** * Checks if the translation has any reference. * * @return bool */ public function hasReferences() { return !empty($this->references); } /** * Return all references for this translation. * * @return array */ public function getReferences() { return array_values($this->references); } /** * Removes all references. * * @return self */ public function deleteReferences() { $this->references = []; return $this; } /** * Adds a new comment for this translation. * * @param string $comment * * @return self */ public function addComment($comment) { if (!in_array($comment, $this->comments, true)) { $this->comments[] = $comment; } return $this; } /** * Checks if the translation has any comment. * * @return bool */ public function hasComments() { return isset($this->comments[0]); } /** * Returns all comments for this translation. * * @return array */ public function getComments() { return $this->comments; } /** * Removes all comments. * * @return self */ public function deleteComments() { $this->comments = []; return $this; } /** * Adds a new extracted comment for this translation. * * @param string $comment * * @return self */ public function addExtractedComment($comment) { if (!in_array($comment, $this->extractedComments, true)) { $this->extractedComments[] = $comment; } return $this; } /** * Checks if the translation has any extracted comment. * * @return bool */ public function hasExtractedComments() { return isset($this->extractedComments[0]); } /** * Returns all extracted comments for this translation. * * @return array */ public function getExtractedComments() { return $this->extractedComments; } /** * Removes all extracted comments. * * @return self */ public function deleteExtractedComments() { $this->extractedComments = []; return $this; } /** * Adds a new flag for this translation. * * @param string $flag * * @return self */ public function addFlag($flag) { if (!in_array($flag, $this->flags, true)) { $this->flags[] = $flag; } return $this; } /** * Checks if the translation has any flag. * * @return bool */ public function hasFlags() { return isset($this->flags[0]); } /** * Returns all extracted flags for this translation. * * @return array */ public function getFlags() { return $this->flags; } /** * Removes all flags. * * @return self */ public function deleteFlags() { $this->flags = []; return $this; } /** * Merges this translation with other translation. * * @param Translation $translation The translation to merge with * @param int $options * * @return self */ public function mergeWith(Translation $translation, $options = Merge::DEFAULTS) { Merge::mergeTranslation($translation, $this, $options); Merge::mergeReferences($translation, $this, $options); Merge::mergeComments($translation, $this, $options); Merge::mergeExtractedComments($translation, $this, $options); Merge::mergeFlags($translation, $this, $options); return $this; } } <?php namespace Gettext\Utils; /** * Function parsed by PhpFunctionsScanner. */ class ParsedFunction { /** * The function name. * * @var string */ protected $name; /** * The line where the function starts. * * @var int */ protected $line; /** * The strings extracted from the function arguments. * * @var string[] */ protected $arguments; /** * The current index of the function (-1 if no arguments). * * @var int|null */ protected $argumentIndex; /** * Shall we stop adding string chunks to the current argument? * * @var bool */ protected $argumentStopped; /** * Extracted comments. * * @var ParsedComment[]|null */ protected $comments; /** * Initializes the instance. * * @param string $name The function name. * @param int $line The line where the function starts. */ public function __construct($name, $line) { $this->name = $name; $this->line = $line; $this->arguments = []; $this->argumentIndex = -1; $this->argumentStopped = false; $this->comments = null; } /** * Stop extracting strings from the current argument (because we found something that's not a string). */ public function stopArgument() { if ($this->argumentIndex === -1) { $this->argumentIndex = 0; } $this->argumentStopped = true; } /** * Go to the next argument because we a comma was found. */ public function nextArgument() { if ($this->argumentIndex === -1) { // This should neve occur, but let's stay safe - During test/development an Exception should be thrown. $this->argumentIndex = 1; } else { ++$this->argumentIndex; } $this->argumentStopped = false; } /** * Add a string to the current argument. * * @param string|null $chunk */ public function addArgumentChunk($chunk) { if ($this->argumentStopped === false) { if ($this->argumentIndex === -1) { $this->argumentIndex = 0; } if (isset($this->arguments[$this->argumentIndex])) { $this->arguments[$this->argumentIndex] .= $chunk; } else { $this->arguments[$this->argumentIndex] = $chunk; } } } /** * Add a comment associated to this function. * * @param ParsedComment $comment */ public function addComment($comment) { if ($this->comments === null) { $this->comments = []; } $this->comments[] = $comment; } /** * Return the line the function starts. * * @return int Line number. */ public function getLine() { return $this->line; } /** * A closing parenthesis was found: return the final data. * The array returned has the following values: * 0 => string The function name. * 1 => int The line where the function starts. * 2 => string[] the strings extracted from the function arguments. * 3 => string[] the comments associated to the function. * * @return array */ public function close() { $arguments = []; for ($i = 0; $i <= $this->argumentIndex; ++$i) { $arguments[$i] = isset($this->arguments[$i]) ? $this->arguments[$i] : null; } return [ $this->name, $this->line, $arguments, $this->comments, ]; } } <?php namespace Gettext\Utils; use Exception; use Gettext\Translations; abstract class FunctionsScanner { /** * Scan and returns the functions and the arguments. * * @param array $constants Constants used in the code to replace * * @return array */ abstract public function getFunctions(array $constants = []); /** * Search for specific functions and create translations. * * You can pass multiple translation with different domains and value found will be sorted respectively. * * @param Translations|Translations[] $translations Multiple domain translations instances where to save the values * @param array $options The extractor options * @throws Exception */ public function saveGettextFunctions($translations, array $options) { $translations = is_array($translations) ? $translations : [$translations]; /** @var Translations[] $translationByDomain [domain => translations, ..] */ $translationByDomain = array_reduce($translations, function ($carry, Translations $translations) { $carry[$translations->getDomain()] = $translations; return $carry; }, []); $functions = $options['functions']; $file = $options['file']; /** * List of source code comments already associated with a function. * * Prevents associating the same comment to multiple functions. * * @var ParsedComment[] $commentsCache */ $commentsCache = []; foreach ($this->getFunctions($options['constants']) as $function) { list($name, $line, $args) = $function; if (isset($options['lineOffset'])) { $line += $options['lineOffset']; } if (!isset($functions[$name])) { continue; } $deconstructed = $this->deconstructArgs($functions[$name], $args); if (!$deconstructed) { continue; } list($domain, $context, $original, $plural) = $deconstructed; if ((string)$original === '') { continue; } $isDefaultDomain = $domain === null; $domainTranslations = isset($translationByDomain[$domain]) ? $translationByDomain[$domain] : false; if (!empty($options['domainOnly']) && $isDefaultDomain) { // If we want to find translations for a specific domain, skip default domain messages continue; } if (!$domainTranslations) { continue; } $translation = $domainTranslations->insert($context, $original, $plural); $translation->addReference($file, $line); if (isset($function[3])) { /* @var ParsedComment $extractedComment */ foreach ($function[3] as $extractedComment) { if (in_array($extractedComment, $commentsCache, true)) { continue; } $translation->addExtractedComment($extractedComment->getComment()); $commentsCache[] = $extractedComment; } } } } /** * Deconstruct arguments to translation values * * @param $function * @param $args * @return array|null * @throws Exception */ protected function deconstructArgs($function, $args) { $domain = null; $context = null; $original = null; $plural = null; switch ($function) { case 'noop': case 'gettext': if (!isset($args[0])) { return null; } $original = $args[0]; break; case 'ngettext': if (!isset($args[1])) { return null; } list($original, $plural) = $args; break; case 'pgettext': if (!isset($args[1])) { return null; } list($context, $original) = $args; break; case 'dgettext': if (!isset($args[1])) { return null; } list($domain, $original) = $args; break; case 'dpgettext': if (!isset($args[2])) { return null; } list($domain, $context, $original) = $args; break; case 'npgettext': if (!isset($args[2])) { return null; } list($context, $original, $plural) = $args; break; case 'dnpgettext': if (!isset($args[3])) { return null; } list($domain, $context, $original, $plural) = $args; break; case 'dngettext': if (!isset($args[2])) { return null; } list($domain, $original, $plural) = $args; break; default: throw new Exception(sprintf('Not valid function %s', $function)); } return [$domain, $context, $original, $plural]; } } <?php namespace Gettext\Utils; class JsFunctionsScanner extends FunctionsScanner { protected $code; protected $status = []; /** * Constructor. * * @param string $code The php code to scan */ public function __construct($code) { // Normalize newline characters $this->code = str_replace(["\r\n", "\n\r", "\r"], "\n", $code); } /** * {@inheritdoc} */ public function getFunctions(array $constants = []) { $length = strlen($this->code); $line = 1; $buffer = ''; $functions = []; $bufferFunctions = []; $char = null; for ($pos = 0; $pos < $length; ++$pos) { $prev = $char; $char = $this->code[$pos]; $next = isset($this->code[$pos + 1]) ? $this->code[$pos + 1] : null; switch ($char) { case '\\': switch ($this->status()) { case 'simple-quote': if ($next !== "'") { break 2; } break; case 'double-quote': if ($next !== '"') { break 2; } break; case 'back-tick': if ($next !== '`') { break 2; } break; } $prev = $char; $char = $next; $pos++; $next = isset($this->code[$pos]) ? $this->code[$pos] : null; break; case "\n": ++$line; if ($this->status('line-comment')) { $this->upStatus(); } break; case '/': switch ($this->status()) { case 'simple-quote': case 'double-quote': case 'back-tick': case 'line-comment': break; case 'block-comment': if ($prev === '*') { $this->upStatus(); } break; default: if ($next === '/') { $this->downStatus('line-comment'); } elseif ($next === '*') { $this->downStatus('block-comment'); } break; } break; case "'": switch ($this->status()) { case 'simple-quote': $this->upStatus(); break; case 'line-comment': case 'block-comment': case 'double-quote': case 'back-tick': break; default: $this->downStatus('simple-quote'); break; } break; case '"': switch ($this->status()) { case 'double-quote': $this->upStatus(); break; case 'line-comment': case 'block-comment': case 'simple-quote': case 'back-tick': break; default: $this->downStatus('double-quote'); break; } break; case '`': switch ($this->status()) { case 'back-tick': $this->upStatus(); break; case 'line-comment': case 'block-comment': case 'simple-quote': case 'double-quote': break; default: $this->downStatus('back-tick'); break; } break; case '(': switch ($this->status()) { case 'simple-quote': case 'double-quote': case 'back-tick': case 'line-comment': case 'block-comment': break; default: if ($buffer && preg_match('/(\w+)$/', $buffer, $matches)) { $this->downStatus('function'); array_unshift($bufferFunctions, [$matches[1], $line, []]); $buffer = ''; continue 3; } break; } break; case ')': switch ($this->status()) { case 'function': if (($argument = static::prepareArgument($buffer))) { $bufferFunctions[0][2][] = $argument; } if (!empty($bufferFunctions)) { $functions[] = array_shift($bufferFunctions); } $this->upStatus(); $buffer = ''; continue 3; } break; case ',': switch ($this->status()) { case 'function': if (($argument = static::prepareArgument($buffer))) { $bufferFunctions[0][2][] = $argument; } $buffer = ''; continue 3; } break; case ' ': case '\t': switch ($this->status()) { case 'double-quote': case 'simple-quote': case 'back-tick': break; default: $buffer = ''; continue 3; } break; } switch ($this->status()) { case 'line-comment': case 'block-comment': break; default: $buffer .= $char; break; } } return $functions; } /** * Get the current context of the scan. * * @param null|string $match To check whether the current status is this value * * @return string|bool */ protected function status($match = null) { $status = isset($this->status[0]) ? $this->status[0] : null; if ($match !== null) { return $status === $match; } return $status; } /** * Add a new status to the stack. * * @param string $status */ protected function downStatus($status) { array_unshift($this->status, $status); } /** * Removes and return the current status. * * @return string|null */ protected function upStatus() { return array_shift($this->status); } /** * Prepares the arguments found in functions. * * @param string $argument * * @return string */ protected static function prepareArgument($argument) { if ($argument && in_array($argument[0], ['"', "'", '`'], true)) { return static::convertString(substr($argument, 1, -1)); } } /** * Decodes a string with an argument. * * @param string $value * * @return string */ protected static function convertString($value) { if (strpos($value, '\\') === false) { return $value; } return preg_replace_callback( '/\\\(n|r|t|v|e|f|"|\\\)/', function ($match) { switch ($match[1][0]) { case 'n': return "\n"; case 'r': return "\r"; case 't': return "\t"; case 'v': return "\v"; case 'e': return "\e"; case 'f': return "\f"; case '"': return '"'; case '\\': return '\\'; } }, $value ); } } <?php namespace Gettext\Utils; /** * Comment parsed by PhpFunctionsScanner. */ class ParsedComment { /** * The comment itself. * * @var string */ protected $comment; /** * The line where the comment starts. * * @var int */ protected $firstLine; /** * The line where the comment ends. * * @var int */ protected $lastLine; /** * Initializes the instance. * * @param string $comment The comment itself. * @param int $firstLine The line where the comment starts. * @param int $lastLine The line where the comment ends. */ public function __construct($comment, $firstLine, $lastLine) { $this->comment = $comment; $this->firstLine = $firstLine; $this->lastLine = $lastLine; } /** * Create new object from raw comment data. * * @param string $value The PHP comment string. * @param int $line The line where the comment starts. * * @return static The parsed comment. */ public static function create($value, $line) { $lastLine = $line + substr_count($value, "\n"); $lines = array_map(function ($line) { if ('' === trim($line)) { return null; } $line = ltrim($line, "#*/ \t"); $line = rtrim($line, "#*/ \t"); return trim($line); }, explode("\n", $value)); // Remove empty lines. $lines = array_filter($lines); $value = implode(' ', $lines); return new static($value, $line, $lastLine); } /** * Return the line where the comment starts. * * @return int Line number. */ public function getFirstLine() { return $this->firstLine; } /** * Return the line where the comment ends. * * @return int Line number. */ public function getLastLine() { return $this->lastLine; } /** * Return the actual comment string. * * @return string The comment. */ public function getComment() { return $this->comment; } /** * Whether this comment is related with a given function. * * @param ParsedFunction $function The function to check. * @return bool Whether the comment is related or not. */ public function isRelatedWith(ParsedFunction $function) { return $this->getLastLine() === $function->getLine() || $this->getLastLine() === $function->getLine() - 1; } /** * Whether the comment matches the required prefixes. * * @param array $prefixes An array of prefixes to check. * @return bool Whether the comment matches the prefixes or not. */ public function checkPrefixes(array $prefixes) { if ('' === $this->comment) { return false; } if (empty($prefixes)) { return true; } foreach ($prefixes as $prefix) { if (strpos($this->comment, $prefix) === 0) { return true; } } return false; } } <?php namespace Gettext\Utils; use Gettext\Translations; /** * Trait used by all generators that exports the translations to plain dictionary (original => singular-translation). */ trait DictionaryTrait { use HeadersGeneratorTrait; use HeadersExtractorTrait; /** * Returns a plain dictionary with the format [original => translation]. * * @param Translations $translations * @param bool $includeHeaders * * @return array */ protected static function toArray(Translations $translations, $includeHeaders) { $messages = []; if ($includeHeaders) { $messages[''] = static::generateHeaders($translations); } foreach ($translations as $translation) { if ($translation->isDisabled()) { continue; } $messages[$translation->getOriginal()] = $translation->getTranslation(); } return $messages; } /** * Extract the entries from a dictionary. * * @param array $messages * @param Translations $translations */ protected static function fromArray(array $messages, Translations $translations) { foreach ($messages as $original => $translation) { if ($original === '') { static::extractHeaders($translation, $translations); continue; } $translations->insert(null, $original)->setTranslation($translation); } } } <?php namespace Gettext\Utils; use Gettext\Extractors\PhpCode; class PhpFunctionsScanner extends FunctionsScanner { /** * PHP tokens of the code to be parsed. * * @var array */ protected $tokens; /** * If not false, comments will be extracted. * * @var string|false|array */ protected $extractComments = false; /** * Enable extracting comments that start with a tag (if $tag is empty all the comments will be extracted). * * @param mixed $tag */ public function enableCommentsExtraction($tag = '') { $this->extractComments = $tag; } /** * Disable comments extraction. */ public function disableCommentsExtraction() { $this->extractComments = false; } /** * Constructor. * * @param string $code The php code to scan */ public function __construct($code) { $this->tokens = array_values( array_filter( token_get_all($code), function ($token) { return !is_array($token) || $token[0] !== T_WHITESPACE; } ) ); } /** * {@inheritdoc} */ public function getFunctions(array $constants = []) { $count = count($this->tokens); /* @var ParsedFunction[] $bufferFunctions */ $bufferFunctions = []; /* @var ParsedComment[] $bufferComments */ $bufferComments = []; /* @var array $functions */ $functions = []; for ($k = 0; $k < $count; ++$k) { $value = $this->tokens[$k]; if (is_string($value)) { if (isset($bufferFunctions[0])) { switch ($value) { case ',': $bufferFunctions[0]->nextArgument(); break; case ')': $functions[] = array_shift($bufferFunctions)->close(); break; case '.': break; default: $bufferFunctions[0]->stopArgument(); break; } } continue; } switch ($value[0]) { case T_CONSTANT_ENCAPSED_STRING: //add an argument to the current function if (isset($bufferFunctions[0])) { $bufferFunctions[0]->addArgumentChunk(PhpCode::convertString($value[1])); } break; case T_STRING: if (isset($bufferFunctions[0])) { if (isset($constants[$value[1]])) { $bufferFunctions[0]->addArgumentChunk($constants[$value[1]]); break; } if (strtolower($value[1]) === 'null') { $bufferFunctions[0]->addArgumentChunk(null); break; } $bufferFunctions[0]->stopArgument(); } //new function found for ($j = $k + 1; $j < $count; ++$j) { $nextToken = $this->tokens[$j]; if (is_array($nextToken) && $nextToken[0] === T_COMMENT) { continue; } if ($nextToken === '(') { $newFunction = new ParsedFunction($value[1], $value[2]); // add comment that was on the line before. if (isset($bufferComments[0])) { $comment = $bufferComments[0]; if ($comment->isRelatedWith($newFunction)) { $newFunction->addComment($comment); } } array_unshift($bufferFunctions, $newFunction); $k = $j; } break; } break; case T_COMMENT: $comment = $this->parsePhpComment($value[1], $value[2]); if ($comment) { array_unshift($bufferComments, $comment); // The comment is inside the function call. if (isset($bufferFunctions[0])) { $bufferFunctions[0]->addComment($comment); } } break; default: if (isset($bufferFunctions[0])) { $bufferFunctions[0]->stopArgument(); } break; } } return $functions; } /** * Extract the actual text from a PHP comment. * * If set, only returns comments that match the prefix(es). * * @param string $value The PHP comment. * @param int $line Line number. * * @return null|ParsedComment Comment or null if comment extraction is disabled or if there is a prefix mismatch. */ protected function parsePhpComment($value, $line) { if ($this->extractComments === false) { return null; } //this returns a comment or null $comment = ParsedComment::create($value, $line); $prefixes = array_filter((array) $this->extractComments); if ($comment && $comment->checkPrefixes($prefixes)) { return $comment; } return null; } } <?php namespace Gettext\Utils; class StringReader { public $pos; public $str; public $strlen; /** * Constructor. * * @param string $str The string to read */ public function __construct($str) { $this->str = $str; $this->strlen = strlen($this->str); } /** * Read and returns a part of the string. * * @param int $bytes The number of bytes to read * * @return string */ public function read($bytes) { $data = substr($this->str, $this->pos, $bytes); $this->seekto($this->pos + $bytes); return $data; } /** * Move the cursor to a specific position. * * @param int $pos The amount of bytes to move * * @return int The new position */ public function seekto($pos) { $this->pos = ($this->strlen < $pos) ? $this->strlen : $pos; return $this->pos; } } <?php namespace Gettext\Utils; use Gettext\Translations; /** * Trait to provide the functionality of extracting headers. */ trait HeadersExtractorTrait { /** * Add the headers found to the translations instance. * * @param string $headers * @param Translations $translations * * @return array */ protected static function extractHeaders($headers, Translations $translations) { $headers = explode("\n", $headers); $currentHeader = null; foreach ($headers as $line) { $line = static::convertString($line); if ($line === '') { continue; } if (static::isHeaderDefinition($line)) { $header = array_map('trim', explode(':', $line, 2)); $currentHeader = $header[0]; $translations->setHeader($currentHeader, $header[1]); } else { $entry = $translations->getHeader($currentHeader); $translations->setHeader($currentHeader, $entry.$line); } } } /** * Checks if it is a header definition line. Useful for distguishing between header definitions * and possible continuations of a header entry. * * @param string $line Line to parse * * @return bool */ protected static function isHeaderDefinition($line) { return (bool) preg_match('/^[\w-]+:/', $line); } /** * Normalize a string. * * @param string $value * * @return string */ public static function convertString($value) { return $value; } } <?php namespace Gettext\Utils; use Gettext\Translations; /** * Trait used by all generators that exports the translations to multidimensional arrays * (context => [original => [translation, plural1, pluraln...]]). */ trait MultidimensionalArrayTrait { use HeadersGeneratorTrait; use HeadersExtractorTrait; /** * Returns a multidimensional array. * * @param Translations $translations * @param bool $includeHeaders * @param bool $forceArray * * @return array */ protected static function toArray(Translations $translations, $includeHeaders, $forceArray = false) { $pluralForm = $translations->getPluralForms(); $pluralSize = is_array($pluralForm) ? ($pluralForm[0] - 1) : null; $messages = []; if ($includeHeaders) { $messages[''] = [ '' => [static::generateHeaders($translations)], ]; } foreach ($translations as $translation) { if ($translation->isDisabled()) { continue; } $context = $translation->getContext(); $original = $translation->getOriginal(); if (!isset($messages[$context])) { $messages[$context] = []; } if ($translation->hasPluralTranslations(true)) { $messages[$context][$original] = $translation->getPluralTranslations($pluralSize); array_unshift($messages[$context][$original], $translation->getTranslation()); } elseif ($forceArray) { $messages[$context][$original] = [$translation->getTranslation()]; } else { $messages[$context][$original] = $translation->getTranslation(); } } return [ 'domain' => $translations->getDomain(), 'plural-forms' => $translations->getHeader('Plural-Forms'), 'messages' => $messages, ]; } /** * Extract the entries from a multidimensional array. * * @param array $messages * @param Translations $translations */ protected static function fromArray(array $messages, Translations $translations) { if (!empty($messages['domain'])) { $translations->setDomain($messages['domain']); } if (!empty($messages['plural-forms'])) { $translations->setHeader(Translations::HEADER_PLURAL, $messages['plural-forms']); } foreach ($messages['messages'] as $context => $contextTranslations) { foreach ($contextTranslations as $original => $value) { if ($context === '' && $original === '') { static::extractHeaders(is_array($value) ? array_shift($value) : $value, $translations); continue; } $translation = $translations->insert($context, $original); if (is_array($value)) { $translation->setTranslation(array_shift($value)); $translation->setPluralTranslations($value); } else { $translation->setTranslation($value); } } } } } <?php namespace Gettext\Utils; /* * Trait to provide the functionality of read/write csv. */ trait CsvTrait { protected static $csvEscapeChar; /** * Check whether support the escape_char argument to fgetcsv/fputcsv or not * * @return bool */ protected static function supportsCsvEscapeChar() { if (static::$csvEscapeChar === null) { static::$csvEscapeChar = version_compare(PHP_VERSION, '5.5.4') >= 0; } return static::$csvEscapeChar; } /** * @param resource $handle * @param array $options * * @return array */ protected static function fgetcsv($handle, $options) { if (static::supportsCsvEscapeChar()) { return fgetcsv($handle, 0, $options['delimiter'], $options['enclosure'], $options['escape_char']); } return fgetcsv($handle, 0, $options['delimiter'], $options['enclosure']); } /** * @param resource $handle * @param array $fields * @param array $options * * @return bool|int */ protected static function fputcsv($handle, $fields, $options) { if (static::supportsCsvEscapeChar()) { return fputcsv($handle, $fields, $options['delimiter'], $options['enclosure'], $options['escape_char']); } return fputcsv($handle, $fields, $options['delimiter'], $options['enclosure']); } } <?php namespace Gettext\Utils; use Gettext\Translations; /** * Trait to provide the functionality of extracting headers. */ trait HeadersGeneratorTrait { /** * Returns the headers as a string. * * @param Translations $translations * * @return string */ protected static function generateHeaders(Translations $translations) { $headers = ''; foreach ($translations->getHeaders() as $name => $value) { $headers .= sprintf("%s: %s\n", $name, $value); } return $headers; } } <?php namespace Gettext\Generators; use Gettext\Translations; use Gettext\Utils\MultidimensionalArrayTrait; class PhpArray extends Generator implements GeneratorInterface { use MultidimensionalArrayTrait; public static $options = [ 'includeHeaders' => true, ]; /** * {@inheritdoc} */ public static function toString(Translations $translations, array $options = []) { $array = static::generate($translations, $options); return '<?php return '.var_export($array, true).';'; } /** * Generates an array with the translations. * * @param Translations $translations * @param array $options * * @return array */ public static function generate(Translations $translations, array $options = []) { $options += static::$options; return static::toArray($translations, $options['includeHeaders'], true); } } <?php namespace Gettext\Generators; use Gettext\Translations; use Gettext\Utils\DictionaryTrait; use Gettext\Utils\CsvTrait; class CsvDictionary extends Generator implements GeneratorInterface { use DictionaryTrait; use CsvTrait; public static $options = [ 'includeHeaders' => false, 'delimiter' => ",", 'enclosure' => '"', 'escape_char' => "\\" ]; /** * {@parentDoc}. */ public static function toString(Translations $translations, array $options = []) { $options += static::$options; $handle = fopen('php://memory', 'w'); foreach (static::toArray($translations, $options['includeHeaders']) as $original => $translation) { static::fputcsv($handle, [$original, $translation], $options); } rewind($handle); $csv = stream_get_contents($handle); fclose($handle); return $csv; } } <?php namespace Gettext\Generators; use Gettext\Translations; use Gettext\Utils\DictionaryTrait; class JsonDictionary extends Generator implements GeneratorInterface { use DictionaryTrait; public static $options = [ 'json' => 0, 'includeHeaders' => false, ]; /** * {@parentDoc}. */ public static function toString(Translations $translations, array $options = []) { $options += static::$options; return json_encode(static::toArray($translations, $options['includeHeaders']), $options['json']); } } <?php namespace Gettext\Generators; use Gettext\Translations; class Jed extends Generator implements GeneratorInterface { public static $options = [ 'json' => 0, ]; /** * {@parentDoc}. */ public static function toString(Translations $translations, array $options = []) { $domain = $translations->getDomain() ?: 'messages'; $options += static::$options; return json_encode([ $domain => [ '' => [ 'domain' => $domain, 'lang' => $translations->getLanguage() ?: 'en', 'plural-forms' => $translations->getHeader('Plural-Forms') ?: 'nplurals=2; plural=(n != 1);', ], ] + static::buildMessages($translations), ], $options['json']); } /** * Generates an array with all translations. * * @param Translations $translations * * @return array */ protected static function buildMessages(Translations $translations) { $pluralForm = $translations->getPluralForms(); $pluralSize = is_array($pluralForm) ? ($pluralForm[0] - 1) : null; $messages = []; $context_glue = '\u0004'; foreach ($translations as $translation) { if ($translation->isDisabled()) { continue; } $key = ($translation->hasContext() ? $translation->getContext().$context_glue : '') .$translation->getOriginal(); if ($translation->hasPluralTranslations(true)) { $message = $translation->getPluralTranslations($pluralSize); array_unshift($message, $translation->getTranslation()); } else { $message = [$translation->getTranslation()]; } $messages[$key] = $message; } return $messages; } } <?php namespace Gettext\Generators; use Gettext\Translation; use Gettext\Translations; use DOMDocument; class Xliff extends Generator implements GeneratorInterface { const UNIT_ID_REGEXP = '/^XLIFF_UNIT_ID: (.*)$/'; /** * {@inheritdoc} */ public static function toString(Translations $translations, array $options = []) { $dom = new DOMDocument('1.0', 'utf-8'); $dom->formatOutput = true; $xliff = $dom->appendChild($dom->createElement('xliff')); $xliff->setAttribute('xmlns', 'urn:oasis:names:tc:xliff:document:2.0'); $xliff->setAttribute('version', '2.0'); $xliff->setAttribute('srcLang', $translations->getLanguage()); $xliff->setAttribute('trgLang', $translations->getLanguage()); $file = $xliff->appendChild($dom->createElement('file')); $file->setAttribute('id', $translations->getDomain().'.'.$translations->getLanguage()); //Save headers as notes $notes = $dom->createElement('notes'); foreach ($translations->getHeaders() as $name => $value) { $notes->appendChild(static::createTextNode($dom, 'note', $value))->setAttribute('id', $name); } if ($notes->hasChildNodes()) { $file->appendChild($notes); } foreach ($translations as $translation) { //Find an XLIFF unit ID, if one is available; otherwise generate $unitId = static::getUnitID($translation)?:md5($translation->getContext().$translation->getOriginal()); $unit = $dom->createElement('unit'); $unit->setAttribute('id', $unitId); //Save comments as notes $notes = $dom->createElement('notes'); $notes->appendChild(static::createTextNode($dom, 'note', $translation->getContext())) ->setAttribute('category', 'context'); foreach ($translation->getComments() as $comment) { //Skip XLIFF unit ID comments. if (preg_match(static::UNIT_ID_REGEXP, $comment)) { continue; } $notes->appendChild(static::createTextNode($dom, 'note', $comment)) ->setAttribute('category', 'comment'); } foreach ($translation->getExtractedComments() as $comment) { $notes->appendChild(static::createTextNode($dom, 'note', $comment)) ->setAttribute('category', 'extracted-comment'); } foreach ($translation->getFlags() as $flag) { $notes->appendChild(static::createTextNode($dom, 'note', $flag)) ->setAttribute('category', 'flag'); } foreach ($translation->getReferences() as $reference) { $notes->appendChild(static::createTextNode($dom, 'note', $reference[0].':'.$reference[1])) ->setAttribute('category', 'reference'); } $unit->appendChild($notes); $segment = $unit->appendChild($dom->createElement('segment')); $segment->appendChild(static::createTextNode($dom, 'source', $translation->getOriginal())); $segment->appendChild(static::createTextNode($dom, 'target', $translation->getTranslation())); foreach ($translation->getPluralTranslations() as $plural) { if ($plural !== '') { $segment->appendChild(static::createTextNode($dom, 'target', $plural)); } } $file->appendChild($unit); } return $dom->saveXML(); } protected static function createTextNode(DOMDocument $dom, $name, $string) { $node = $dom->createElement($name); $text = (preg_match('/[&<>]/', $string) === 1) ? $dom->createCDATASection($string) : $dom->createTextNode($string); $node->appendChild($text); return $node; } /** * Gets the translation's unit ID, if one is available. * * @param Translation $translation * * @return string|null */ public static function getUnitID(Translation $translation) { foreach ($translation->getComments() as $comment) { if (preg_match(static::UNIT_ID_REGEXP, $comment, $matches)) { return $matches[1]; } } return null; } } <?php namespace Gettext\Generators; use Gettext\Translations; interface GeneratorInterface { /** * Saves the translations in a file. * * @param Translations $translations * @param string $file * @param array $options * * @return bool */ public static function toFile(Translations $translations, $file, array $options = []); /** * Generates a string with the translations ready to save in a file. * * @param Translations $translations * @param array $options * * @return string */ public static function toString(Translations $translations, array $options = []); } <?php namespace Gettext\Generators; use Gettext\Translations; use Gettext\Utils\HeadersGeneratorTrait; class Mo extends Generator implements GeneratorInterface { use HeadersGeneratorTrait; public static $options = [ 'includeHeaders' => true, ]; /** * {@parentDoc}. */ public static function toString(Translations $translations, array $options = []) { $options += static::$options; $messages = []; if ($options['includeHeaders']) { $messages[''] = static::generateHeaders($translations); } foreach ($translations as $translation) { if (!$translation->hasTranslation() || $translation->isDisabled()) { continue; } if ($translation->hasContext()) { $originalString = $translation->getContext()."\x04".$translation->getOriginal(); } else { $originalString = $translation->getOriginal(); } $messages[$originalString] = $translation; } ksort($messages); $numEntries = count($messages); $originalsTable = ''; $translationsTable = ''; $originalsIndex = []; $translationsIndex = []; $pluralForm = $translations->getPluralForms(); $pluralSize = is_array($pluralForm) ? ($pluralForm[0] - 1) : null; foreach ($messages as $originalString => $translation) { if (is_string($translation)) { // Headers $translationString = $translation; } else { /* @var $translation \Gettext\Translation */ if ($translation->hasPlural() && $translation->hasPluralTranslations(true)) { $originalString .= "\x00".$translation->getPlural(); $translationString = $translation->getTranslation(); $translationString .= "\x00".implode("\x00", $translation->getPluralTranslations($pluralSize)); } else { $translationString = $translation->getTranslation(); } } $originalsIndex[] = [ 'relativeOffset' => strlen($originalsTable), 'length' => strlen($originalString) ]; $originalsTable .= $originalString."\x00"; $translationsIndex[] = [ 'relativeOffset' => strlen($translationsTable), 'length' => strlen($translationString) ]; $translationsTable .= $translationString."\x00"; } // Offset of table with the original strings index: right after the header (which is 7 words) $originalsIndexOffset = 7 * 4; // Size of table with the original strings index $originalsIndexSize = $numEntries * (4 + 4); // Offset of table with the translation strings index: right after the original strings index table $translationsIndexOffset = $originalsIndexOffset + $originalsIndexSize; // Size of table with the translation strings index $translationsIndexSize = $numEntries * (4 + 4); // Hashing table starts after the header and after the index table $originalsStringsOffset = $translationsIndexOffset + $translationsIndexSize; // Translations start after the keys $translationsStringsOffset = $originalsStringsOffset + strlen($originalsTable); // Let's generate the .mo file binary data $mo = ''; // Magic number $mo .= pack('L', 0x950412de); // File format revision $mo .= pack('L', 0); // Number of strings $mo .= pack('L', $numEntries); // Offset of table with original strings $mo .= pack('L', $originalsIndexOffset); // Offset of table with translation strings $mo .= pack('L', $translationsIndexOffset); // Size of hashing table: we don't use it. $mo .= pack('L', 0); // Offset of hashing table: it would start right after the translations index table $mo .= pack('L', $translationsIndexOffset + $translationsIndexSize); // Write the lengths & offsets of the original strings foreach ($originalsIndex as $info) { $mo .= pack('L', $info['length']); $mo .= pack('L', $originalsStringsOffset + $info['relativeOffset']); } // Write the lengths & offsets of the translated strings foreach ($translationsIndex as $info) { $mo .= pack('L', $info['length']); $mo .= pack('L', $translationsStringsOffset + $info['relativeOffset']); } // Write original strings $mo .= $originalsTable; // Write translation strings $mo .= $translationsTable; return $mo; } } <?php namespace Gettext\Generators; use Gettext\Translations; use Gettext\Utils\HeadersGeneratorTrait; use Gettext\Utils\CsvTrait; /** * Class to export translations to csv. */ class Csv extends Generator implements GeneratorInterface { use HeadersGeneratorTrait; use CsvTrait; public static $options = [ 'includeHeaders' => false, 'delimiter' => ",", 'enclosure' => '"', 'escape_char' => "\\" ]; /** * {@parentDoc}. */ public static function toString(Translations $translations, array $options = []) { $options += static::$options; $handle = fopen('php://memory', 'w'); if ($options['includeHeaders']) { static::fputcsv($handle, ['', '', static::generateHeaders($translations)], $options); } foreach ($translations as $translation) { if ($translation->isDisabled()) { continue; } $line = [$translation->getContext(), $translation->getOriginal(), $translation->getTranslation()]; if ($translation->hasPluralTranslations(true)) { $line = array_merge($line, $translation->getPluralTranslations()); } static::fputcsv($handle, $line, $options); } rewind($handle); $csv = stream_get_contents($handle); fclose($handle); return $csv; } } <?php namespace Gettext\Generators; use Gettext\Translations; use Gettext\Utils\MultidimensionalArrayTrait; class Json extends Generator implements GeneratorInterface { use MultidimensionalArrayTrait; public static $options = [ 'json' => 0, 'includeHeaders' => false, ]; /** * {@inheritdoc} */ public static function toString(Translations $translations, array $options = []) { $options += static::$options; return json_encode(static::toArray($translations, $options['includeHeaders'], true), $options['json']); } } <?php namespace Gettext\Generators; use Gettext\Translations; abstract class Generator implements GeneratorInterface { /** * {@inheritdoc} */ public static function toFile(Translations $translations, $file, array $options = []) { $content = static::toString($translations, $options); if (file_put_contents($file, $content) === false) { return false; } return true; } } <?php namespace Gettext\Generators; use Gettext\Translations; use Gettext\Utils\DictionaryTrait; use Symfony\Component\Yaml\Yaml as YamlDumper; class YamlDictionary extends Generator implements GeneratorInterface { use DictionaryTrait; public static $options = [ 'includeHeaders' => false, 'indent' => 2, 'inline' => 3, ]; /** * {@inheritdoc} */ public static function toString(Translations $translations, array $options = []) { $options += static::$options; return YamlDumper::dump( static::toArray($translations, $options['includeHeaders']), $options['inline'], $options['indent'] ); } } <?php namespace Gettext\Generators; use Gettext\Translations; use Gettext\Utils\MultidimensionalArrayTrait; use Symfony\Component\Yaml\Yaml as YamlDumper; class Yaml extends Generator implements GeneratorInterface { use MultidimensionalArrayTrait; public static $options = [ 'includeHeaders' => false, 'indent' => 2, 'inline' => 4, ]; /** * {@inheritdoc} */ public static function toString(Translations $translations, array $options = []) { $options += static::$options; return YamlDumper::dump( static::toArray($translations, $options['includeHeaders']), $options['inline'], $options['indent'] ); } } <?php namespace Gettext\Generators; use Gettext\Translations; class Po extends Generator implements GeneratorInterface { public static $options = [ 'noLocation' => false, ]; /** * {@parentDoc}. */ public static function toString(Translations $translations, array $options = []) { $options += static::$options; $pluralForm = $translations->getPluralForms(); $pluralSize = is_array($pluralForm) ? ($pluralForm[0] - 1) : null; $lines = ['msgid ""', 'msgstr ""']; foreach ($translations->getHeaders() as $name => $value) { $lines[] = sprintf('"%s: %s\\n"', $name, $value); } $lines[] = ''; //Translations foreach ($translations as $translation) { if ($translation->hasComments()) { foreach ($translation->getComments() as $comment) { $lines[] = '# '.$comment; } } if ($translation->hasExtractedComments()) { foreach ($translation->getExtractedComments() as $comment) { $lines[] = '#. '.$comment; } } if (!$options['noLocation'] && $translation->hasReferences()) { foreach ($translation->getReferences() as $reference) { $lines[] = '#: '.$reference[0].(!is_null($reference[1]) ? ':'.$reference[1] : null); } } if ($translation->hasFlags()) { $lines[] = '#, '.implode(',', $translation->getFlags()); } $prefix = $translation->isDisabled() ? '#~ ' : ''; if ($translation->hasContext()) { $lines[] = $prefix.'msgctxt '.static::convertString($translation->getContext()); } static::addLines($lines, $prefix.'msgid', $translation->getOriginal()); if ($translation->hasPlural()) { static::addLines($lines, $prefix.'msgid_plural', $translation->getPlural()); static::addLines($lines, $prefix.'msgstr[0]', $translation->getTranslation()); foreach ($translation->getPluralTranslations($pluralSize) as $k => $v) { static::addLines($lines, $prefix.'msgstr['.($k + 1).']', $v); } } else { static::addLines($lines, $prefix.'msgstr', $translation->getTranslation()); } $lines[] = ''; } return implode("\n", $lines); } /** * Escapes and adds double quotes to a string. * * @param string $string * * @return string */ protected static function multilineQuote($string) { $lines = explode("\n", $string); $last = count($lines) - 1; foreach ($lines as $k => $line) { if ($k === $last) { $lines[$k] = static::convertString($line); } else { $lines[$k] = static::convertString($line."\n"); } } return $lines; } /** * Add one or more lines depending whether the string is multiline or not. * * @param array &$lines * @param string $name * @param string $value */ protected static function addLines(array &$lines, $name, $value) { $newLines = static::multilineQuote($value); if (count($newLines) === 1) { $lines[] = $name.' '.$newLines[0]; } else { $lines[] = $name.' ""'; foreach ($newLines as $line) { $lines[] = $line; } } } /** * Convert a string to its PO representation. * * @param string $value * * @return string */ public static function convertString($value) { return '"'.strtr( $value, [ "\x00" => '', '\\' => '\\\\', "\t" => '\t', "\r" => '\r', "\n" => '\n', '"' => '\\"', ] ).'"'; } } <?php namespace Gettext; use Gettext\Languages\Language; use BadMethodCallException; use InvalidArgumentException; use ArrayObject; /** * Class to manage a collection of translations. * * @method static $this fromBladeFile(string $filename, array $options = []) * @method static $this fromBladeString(string $string, array $options = []) * @method $this addFromBladeFile(string $filename, array $options = []) * @method $this addFromBladeString(string $string, array $options = []) * @method static $this fromCsvFile(string $filename, array $options = []) * @method static $this fromCsvString(string $string, array $options = []) * @method $this addFromCsvFile(string $filename, array $options = []) * @method $this addFromCsvString(string $string, array $options = []) * @method bool toCsvFile(string $filename, array $options = []) * @method string toCsvString(array $options = []) * @method static $this fromCsvDictionaryFile(string $filename, array $options = []) * @method static $this fromCsvDictionaryString(string $string, array $options = []) * @method $this addFromCsvDictionaryFile(string $filename, array $options = []) * @method $this addFromCsvDictionaryString(string $string, array $options = []) * @method bool toCsvDictionaryFile(string $filename, array $options = []) * @method string toCsvDictionaryString(array $options = []) * @method static $this fromJedFile(string $filename, array $options = []) * @method static $this fromJedString(string $string, array $options = []) * @method $this addFromJedFile(string $filename, array $options = []) * @method $this addFromJedString(string $string, array $options = []) * @method bool toJedFile(string $filename, array $options = []) * @method string toJedString(array $options = []) * @method static $this fromJsCodeFile(string $filename, array $options = []) * @method static $this fromJsCodeString(string $string, array $options = []) * @method $this addFromJsCodeFile(string $filename, array $options = []) * @method $this addFromJsCodeString(string $string, array $options = []) * @method static $this fromJsonFile(string $filename, array $options = []) * @method static $this fromJsonString(string $string, array $options = []) * @method $this addFromJsonFile(string $filename, array $options = []) * @method $this addFromJsonString(string $string, array $options = []) * @method bool toJsonFile(string $filename, array $options = []) * @method string toJsonString(array $options = []) * @method static $this fromJsonDictionaryFile(string $filename, array $options = []) * @method static $this fromJsonDictionaryString(string $string, array $options = []) * @method $this addFromJsonDictionaryFile(string $filename, array $options = []) * @method $this addFromJsonDictionaryString(string $string, array $options = []) * @method bool toJsonDictionaryFile(string $filename, array $options = []) * @method string toJsonDictionaryString(array $options = []) * @method static $this fromMoFile(string $filename, array $options = []) * @method static $this fromMoString(string $string, array $options = []) * @method $this addFromMoFile(string $filename, array $options = []) * @method $this addFromMoString(string $string, array $options = []) * @method bool toMoFile(string $filename, array $options = []) * @method string toMoString(array $options = []) * @method static $this fromPhpArrayFile(string $filename, array $options = []) * @method static $this fromPhpArrayString(string $string, array $options = []) * @method $this addFromPhpArrayFile(string $filename, array $options = []) * @method $this addFromPhpArrayString(string $string, array $options = []) * @method bool toPhpArrayFile(string $filename, array $options = []) * @method string toPhpArrayString(array $options = []) * @method static $this fromPhpCodeFile(string $filename, array $options = []) * @method static $this fromPhpCodeString(string $string, array $options = []) * @method $this addFromPhpCodeFile(string $filename, array $options = []) * @method $this addFromPhpCodeString(string $string, array $options = []) * @method static $this fromPoFile(string $filename, array $options = []) * @method static $this fromPoString(string $string, array $options = []) * @method $this addFromPoFile(string $filename, array $options = []) * @method $this addFromPoString(string $string, array $options = []) * @method bool toPoFile(string $filename, array $options = []) * @method string toPoString(array $options = []) * @method static $this fromTwigFile(string $filename, array $options = []) * @method static $this fromTwigString(string $string, array $options = []) * @method $this addFromTwigFile(string $filename, array $options = []) * @method $this addFromTwigString(string $string, array $options = []) * @method static $this fromVueJsFile(string $filename, array $options = []) * @method static $this fromVueJsString(string $filename, array $options = []) * @method $this addFromVueJsFile(string $filename, array $options = []) * @method $this addFromVueJsString(string $filename, array $options = []) * @method static $this fromXliffFile(string $filename, array $options = []) * @method static $this fromXliffString(string $string, array $options = []) * @method $this addFromXliffFile(string $filename, array $options = []) * @method $this addFromXliffString(string $string, array $options = []) * @method bool toXliffFile(string $filename, array $options = []) * @method string toXliffString(array $options = []) * @method static $this fromYamlFile(string $filename, array $options = []) * @method static $this fromYamlString(string $string, array $options = []) * @method $this addFromYamlFile(string $filename, array $options = []) * @method $this addFromYamlString(string $string, array $options = []) * @method bool toYamlFile(string $filename, array $options = []) * @method string toYamlString(array $options = []) * @method static $this fromYamlDictionaryFile(string $filename, array $options = []) * @method static $this fromYamlDictionaryString(string $string, array $options = []) * @method $this addFromYamlDictionaryFile(string $filename, array $options = []) * @method $this addFromYamlDictionaryString(string $string, array $options = []) * @method bool toYamlDictionaryFile(string $filename, array $options = []) * @method string toYamlDictionaryString(array $options = []) */ class Translations extends ArrayObject { const HEADER_LANGUAGE = 'Language'; const HEADER_PLURAL = 'Plural-Forms'; const HEADER_DOMAIN = 'X-Domain'; public static $options = [ 'defaultHeaders' => [ 'Project-Id-Version' => '', 'Report-Msgid-Bugs-To' => '', 'Last-Translator' => '', 'Language-Team' => '', 'MIME-Version' => '1.0', 'Content-Type' => 'text/plain; charset=UTF-8', 'Content-Transfer-Encoding' => '8bit', ], 'headersSorting' => false, 'defaultDateHeaders' => [ 'POT-Creation-Date', 'PO-Revision-Date', ], ]; protected $headers; protected $translationClass; /** * @see ArrayObject::__construct() */ public function __construct( $input = [], $flags = 0, $iterator_class = 'ArrayIterator', $translationClass = 'Gettext\Translation' ) { $this->headers = static::$options['defaultHeaders']; foreach (static::$options['defaultDateHeaders'] as $header) { $this->headers[$header] = date('c'); } $this->headers[self::HEADER_LANGUAGE] = ''; $this->translationClass = $translationClass; parent::__construct($input, $flags, $iterator_class); } /** * Magic method to create new instances using extractors * For example: Translations::fromMoFile($filename, $options);. * * @return Translations */ public static function __callStatic($name, $arguments) { if (!preg_match('/^from(\w+)(File|String)$/i', $name, $matches)) { throw new BadMethodCallException("The method $name does not exists"); } return call_user_func_array([new static(), 'add'.ucfirst($name)], $arguments); } /** * Magic method to import/export the translations to a specific format * For example: $translations->toMoFile($filename, $options); * For example: $translations->addFromMoFile($filename, $options);. * * @return self|bool */ public function __call($name, $arguments) { if (!preg_match('/^(addFrom|to)(\w+)(File|String)$/i', $name, $matches)) { throw new BadMethodCallException("The method $name does not exists"); } if ($matches[1] === 'addFrom') { $extractor = 'Gettext\\Extractors\\'.$matches[2].'::from'.$matches[3]; $source = array_shift($arguments); $options = array_shift($arguments) ?: []; call_user_func($extractor, $source, $this, $options); return $this; } $generator = 'Gettext\\Generators\\'.$matches[2].'::to'.$matches[3]; array_unshift($arguments, $this); return call_user_func_array($generator, $arguments); } /** * Magic method to clone each translation on clone the translations object. */ public function __clone() { $array = []; foreach ($this as $key => $translation) { $array[$key] = clone $translation; } $this->exchangeArray($array); } /** * Control the new translations added. * * @param mixed $index * @param Translation $value * * @throws InvalidArgumentException If the value is not an instance of Gettext\Translation * * @return Translation */ public function offsetSet($index, $value) { if (!($value instanceof Translation)) { throw new InvalidArgumentException( 'Only instances of Gettext\\Translation must be added to a Gettext\\Translations' ); } $id = $value->getId(); if ($this->offsetExists($id)) { $this[$id]->mergeWith($value); return $this[$id]; } parent::offsetSet($id, $value); return $value; } /** * Set the plural definition. * * @param int $count * @param string $rule * * @return self */ public function setPluralForms($count, $rule) { if (preg_match('/[a-z]/i', str_replace('n', '', $rule))) { throw new \InvalidArgumentException('Invalid Plural form: ' . $rule); } $this->setHeader(self::HEADER_PLURAL, "nplurals={$count}; plural={$rule};"); return $this; } /** * Returns the parsed plural definition. * * @param null|array [count, rule] */ public function getPluralForms() { $header = $this->getHeader(self::HEADER_PLURAL); if (!empty($header) && preg_match('/^nplurals\s*=\s*(\d+)\s*;\s*plural\s*=\s*([^;]+)\s*;$/', $header, $matches) ) { return [intval($matches[1]), $matches[2]]; } } /** * Set a new header. * * @param string $name * @param string $value * * @return self */ public function setHeader($name, $value) { $name = trim($name); $this->headers[$name] = trim($value); return $this; } /** * Returns a header value. * * @param string $name * * @return null|string */ public function getHeader($name) { return isset($this->headers[$name]) ? $this->headers[$name] : null; } /** * Returns all header for this translations (in alphabetic order). * * @return array */ public function getHeaders() { if (static::$options['headersSorting']) { ksort($this->headers); } return $this->headers; } /** * Removes all headers. * * @return self */ public function deleteHeaders() { $this->headers = []; return $this; } /** * Removes one header. * * @param string $name * * @return self */ public function deleteHeader($name) { unset($this->headers[$name]); return $this; } /** * Returns the language value. * * @return string $language */ public function getLanguage() { return $this->getHeader(self::HEADER_LANGUAGE); } /** * Sets the language and the plural forms. * * @param string $language * * @throws InvalidArgumentException if the language hasn't been recognized * * @return self */ public function setLanguage($language) { $this->setHeader(self::HEADER_LANGUAGE, trim($language)); if (($info = Language::getById($language))) { return $this->setPluralForms(count($info->categories), $info->formula); } throw new InvalidArgumentException(sprintf('The language "%s" is not valid', $language)); } /** * Checks whether the language is empty or not. * * @return bool */ public function hasLanguage() { $language = $this->getLanguage(); return (is_string($language) && ($language !== '')) ? true : false; } /** * Set a new domain for this translations. * * @param string $domain * * @return self */ public function setDomain($domain) { $this->setHeader(self::HEADER_DOMAIN, trim($domain)); return $this; } /** * Returns the domain. * * @return string */ public function getDomain() { return $this->getHeader(self::HEADER_DOMAIN); } /** * Checks whether the domain is empty or not. * * @return bool */ public function hasDomain() { $domain = $this->getDomain(); return (is_string($domain) && ($domain !== '')) ? true : false; } /** * Search for a specific translation. * * @param string|Translation $context The context of the translation or a translation instance * @param string $original The original string * @warning Translations with custom identifiers (e.g. XLIFF unit IDs) cannot be found using this function. * * @return Translation|false */ public function find($context, $original = '') { if ($context instanceof Translation) { $id = $context->getId(); } else { $id = Translation::generateId($context, $original); } return $this->offsetExists($id) ? $this[$id] : false; } /** * Count all elements translated * * @return integer */ public function countTranslated() { $c = 0; foreach ($this as $v) { if ($v->hasTranslation()) { $c++; } } return $c; } /** * Creates and insert/merges a new translation. * * @param string $context The translation context * @param string $original The translation original string * @param string $plural The translation original plural string * * @return Translation The translation created */ public function insert($context, $original, $plural = '') { return $this->offsetSet(null, $this->createNewTranslation($context, $original, $plural)); } /** * Merges this translations with other translations. * * @param Translations $translations The translations instance to merge with * @param int $options * * @return self */ public function mergeWith(Translations $translations, $options = Merge::DEFAULTS) { Merge::mergeHeaders($translations, $this, $options); Merge::mergeTranslations($translations, $this, $options); return $this; } /** * Create a new instance of a Translation object. * * @param string $context The context of the translation * @param string $original The original string * @param string $plural The original plural string * @return Translation New Translation instance */ public function createNewTranslation($context, $original, $plural = '') { $class = $this->translationClass; return $class::create($context, $original, $plural); } } <?php namespace Gettext; use Gettext\Generators\PhpArray; class Translator extends BaseTranslator implements TranslatorInterface { protected $domain; protected $dictionary = []; protected $plurals = []; /** * Loads translation from a Translations instance, a file on an array. * * @param Translations|string|array $translations * * @return static */ public function loadTranslations($translations) { if (is_object($translations) && $translations instanceof Translations) { $translations = PhpArray::generate($translations, ['includeHeaders' => false]); } elseif (is_string($translations) && is_file($translations)) { $translations = include $translations; } elseif (!is_array($translations)) { throw new \InvalidArgumentException( 'Invalid Translator: only arrays, files or instance of Translations are allowed' ); } $this->addTranslations($translations); return $this; } /** * Set the default domain. * * @param string $domain * * @return static */ public function defaultDomain($domain) { $this->domain = $domain; return $this; } /** * @see TranslatorInterface * * {@inheritdoc} */ public function gettext($original) { return $this->dpgettext($this->domain, null, $original); } /** * @see TranslatorInterface * * {@inheritdoc} */ public function ngettext($original, $plural, $value) { return $this->dnpgettext($this->domain, null, $original, $plural, $value); } /** * @see TranslatorInterface * * {@inheritdoc} */ public function dngettext($domain, $original, $plural, $value) { return $this->dnpgettext($domain, null, $original, $plural, $value); } /** * @see TranslatorInterface * * {@inheritdoc} */ public function npgettext($context, $original, $plural, $value) { return $this->dnpgettext($this->domain, $context, $original, $plural, $value); } /** * @see TranslatorInterface * * {@inheritdoc} */ public function pgettext($context, $original) { return $this->dpgettext($this->domain, $context, $original); } /** * @see TranslatorInterface * * {@inheritdoc} */ public function dgettext($domain, $original) { return $this->dpgettext($domain, null, $original); } /** * @see TranslatorInterface * * {@inheritdoc} */ public function dpgettext($domain, $context, $original) { $translation = $this->getTranslation($domain, $context, $original); if (isset($translation[0]) && $translation[0] !== '') { return $translation[0]; } return $original; } /** * @see TranslatorInterface * * {@inheritdoc} */ public function dnpgettext($domain, $context, $original, $plural, $value) { $translation = $this->getTranslation($domain, $context, $original); $key = $this->getPluralIndex($domain, $value, $translation === false); if (isset($translation[$key]) && $translation[$key] !== '') { return $translation[$key]; } return ($key === 0) ? $original : $plural; } /** * Set new translations to the dictionary. * * @param array $translations */ protected function addTranslations(array $translations) { $domain = isset($translations['domain']) ? $translations['domain'] : ''; //Set the first domain loaded as default domain if ($this->domain === null) { $this->domain = $domain; } if (isset($this->dictionary[$domain])) { $this->dictionary[$domain] = array_replace_recursive($this->dictionary[$domain], $translations['messages']); return; } if (!empty($translations['plural-forms'])) { list($count, $code) = array_map('trim', explode(';', $translations['plural-forms'], 2)); // extract just the expression turn 'n' into a php variable '$n'. // Slap on a return keyword and semicolon at the end. $this->plurals[$domain] = [ 'count' => (int) str_replace('nplurals=', '', $count), 'code' => str_replace('plural=', 'return ', str_replace('n', '$n', $code)).';', ]; } $this->dictionary[$domain] = $translations['messages']; } /** * Search and returns a translation. * * @param string $domain * @param string $context * @param string $original * * @return string|false */ protected function getTranslation($domain, $context, $original) { return isset($this->dictionary[$domain][$context][$original]) ? $this->dictionary[$domain][$context][$original] : false; } /** * Executes the plural decision code given the number to decide which * plural version to take. * * @param string $domain * @param string $n * @param bool $fallback set to true to get fallback plural index * * @return int */ protected function getPluralIndex($domain, $n, $fallback) { //Not loaded domain or translation, use a fallback if (!isset($this->plurals[$domain]) || $fallback === true) { return $n == 1 ? 0 : 1; } if (!isset($this->plurals[$domain]['function'])) { $code = static::fixTerseIfs($this->plurals[$domain]['code']); $this->plurals[$domain]['function'] = eval("return function (\$n) { $code };"); } if ($this->plurals[$domain]['count'] <= 2) { return call_user_func($this->plurals[$domain]['function'], $n) ? 1 : 0; } return call_user_func($this->plurals[$domain]['function'], $n); } /** * This function will recursively wrap failure states in brackets if they contain a nested terse if. * * This because PHP can not handle nested terse if's unless they are wrapped in brackets. * * This code probably only works for the gettext plural decision codes. * * return ($n==1 ? 0 : $n%10>=2 && $n%10<=4 && ($n%100<10 || $n%100>=20) ? 1 : 2); * becomes * return ($n==1 ? 0 : ($n%10>=2 && $n%10<=4 && ($n%100<10 || $n%100>=20) ? 1 : 2)); * * @param string $code the terse if string * @param bool $inner If inner is true we wrap it in brackets * * @return string A formatted terse If that PHP can work with. */ private static function fixTerseIfs($code, $inner = false) { /* * (?P<expression>[^?]+) Capture everything up to ? as 'expression' * \? ? * (?P<success>[^:]+) Capture everything up to : as 'success' * : : * (?P<failure>[^;]+) Capture everything up to ; as 'failure' */ preg_match('/(?P<expression>[^?]+)\?(?P<success>[^:]+):(?P<failure>[^;]+)/', $code, $matches); // If no match was found then no terse if was present if (!isset($matches[0])) { return $code; } $expression = $matches['expression']; $success = $matches['success']; $failure = $matches['failure']; // Go look for another terse if in the failure state. $failure = static::fixTerseIfs($failure, true); $code = $expression.' ? '.$success.' : '.$failure; if ($inner) { return "($code)"; } // note the semicolon. We need that for executing the code. return "$code;"; } } <?php namespace Gettext; class GettextTranslator extends BaseTranslator implements TranslatorInterface { /** * Constructor. Detects the current language using the environment variables. * * @param string $language */ public function __construct($language = null) { if (!function_exists('gettext')) { throw new \RuntimeException('This class require the gettext extension for PHP'); } //detects the language environment respecting the priority order //http://php.net/manual/en/function.gettext.php#114062 if (empty($language)) { $language = getenv('LANGUAGE') ?: getenv('LC_ALL') ?: getenv('LC_MESSAGES') ?: getenv('LANG'); } if (!empty($language)) { $this->setLanguage($language); } } /** * Define the current locale. * * @param string $language * @param int|null $category * * @return self */ public function setLanguage($language, $category = null) { if ($category === null) { $category = defined('LC_MESSAGES') ? LC_MESSAGES : LC_ALL; } setlocale($category, $language); putenv('LANGUAGE='.$language); return $this; } /** * Loads a gettext domain. * * @param string $domain * @param string $path * @param bool $default * * @return self */ public function loadDomain($domain, $path = null, $default = true) { bindtextdomain($domain, $path); bind_textdomain_codeset($domain, 'UTF-8'); if ($default) { textdomain($domain); } return $this; } /** * @see TranslatorInterface * * {@inheritdoc} */ public function gettext($original) { return gettext($original); } /** * @see TranslatorInterface * * {@inheritdoc} */ public function ngettext($original, $plural, $value) { return ngettext($original, $plural, $value); } /** * @see TranslatorInterface * * {@inheritdoc} */ public function dngettext($domain, $original, $plural, $value) { return dngettext($domain, $original, $plural, $value); } /** * @see TranslatorInterface * * {@inheritdoc} */ public function npgettext($context, $original, $plural, $value) { $message = $context."\x04".$original; $translation = ngettext($message, $plural, $value); return ($translation === $message) ? $original : $translation; } /** * @see TranslatorInterface * * {@inheritdoc} */ public function pgettext($context, $original) { $message = $context."\x04".$original; $translation = gettext($message); return ($translation === $message) ? $original : $translation; } /** * @see TranslatorInterface * * {@inheritdoc} */ public function dgettext($domain, $original) { return dgettext($domain, $original); } /** * @see TranslatorInterface * * {@inheritdoc} */ public function dpgettext($domain, $context, $original) { $message = $context."\x04".$original; $translation = dgettext($domain, $message); return ($translation === $message) ? $original : $translation; } /** * @see TranslatorInterface * * {@inheritdoc} */ public function dnpgettext($domain, $context, $original, $plural, $value) { $message = $context."\x04".$original; $translation = dngettext($domain, $message, $plural, $value); return ($translation === $message) ? $original : $translation; } } <?php namespace Gettext; /** * Static class with merge contants. */ final class Merge { const ADD = 1; const REMOVE = 2; const HEADERS_ADD = 4; const HEADERS_REMOVE = 8; const HEADERS_OVERRIDE = 16; const LANGUAGE_OVERRIDE = 32; const DOMAIN_OVERRIDE = 64; const TRANSLATION_OVERRIDE = 128; const COMMENTS_OURS = 256; const COMMENTS_THEIRS = 512; const EXTRACTED_COMMENTS_OURS = 1024; const EXTRACTED_COMMENTS_THEIRS = 2048; const FLAGS_OURS = 4096; const FLAGS_THEIRS = 8192; const REFERENCES_OURS = 16384; const REFERENCES_THEIRS = 32768; const DEFAULTS = 5; //1 + 4 /** * Merge the flags of two translations. * * @param Translation $from * @param Translation $to * @param int $options */ public static function mergeFlags(Translation $from, Translation $to, $options = self::DEFAULTS) { if ($options & self::FLAGS_THEIRS) { $to->deleteFlags(); } if (!($options & self::FLAGS_OURS)) { foreach ($from->getFlags() as $flag) { $to->addFlag($flag); } } } /** * Merge the extracted comments of two translations. * * @param Translation $from * @param Translation $to * @param int $options */ public static function mergeExtractedComments(Translation $from, Translation $to, $options = self::DEFAULTS) { if ($options & self::EXTRACTED_COMMENTS_THEIRS) { $to->deleteExtractedComments(); } if (!($options & self::EXTRACTED_COMMENTS_OURS)) { foreach ($from->getExtractedComments() as $comment) { $to->addExtractedComment($comment); } } } /** * Merge the comments of two translations. * * @param Translation $from * @param Translation $to * @param int $options */ public static function mergeComments(Translation $from, Translation $to, $options = self::DEFAULTS) { if ($options & self::COMMENTS_THEIRS) { $to->deleteComments(); } if (!($options & self::COMMENTS_OURS)) { foreach ($from->getComments() as $comment) { $to->addComment($comment); } } } /** * Merge the references of two translations. * * @param Translation $from * @param Translation $to * @param int $options */ public static function mergeReferences(Translation $from, Translation $to, $options = self::DEFAULTS) { if ($options & self::REFERENCES_THEIRS) { $to->deleteReferences(); } if (!($options & self::REFERENCES_OURS)) { foreach ($from->getReferences() as $reference) { $to->addReference($reference[0], $reference[1]); } } } /** * Merge the translations of two translations. * * @param Translation $from * @param Translation $to * @param int $options */ public static function mergeTranslation(Translation $from, Translation $to, $options = self::DEFAULTS) { $override = (boolean) ($options & self::TRANSLATION_OVERRIDE); if (!$to->hasTranslation() || ($from->hasTranslation() && $override)) { $to->setTranslation($from->getTranslation()); } if (!$to->hasPlural() || ($from->hasPlural() && $override)) { $to->setPlural($from->getPlural()); } if (!$to->hasPluralTranslations() || ($from->hasPluralTranslations() && $override)) { $to->setPluralTranslations($from->getPluralTranslations()); } } /** * Merge the translations of two translations. * * @param Translations $from * @param Translations $to * @param int $options */ public static function mergeTranslations(Translations $from, Translations $to, $options = self::DEFAULTS) { if ($options & self::REMOVE) { $filtered = []; foreach ($to as $entry) { if ($from->find($entry)) { $filtered[$entry->getId()] = $entry; } } $to->exchangeArray($filtered); } foreach ($from as $entry) { if (($existing = $to->find($entry))) { $existing->mergeWith($entry, $options); } elseif ($options & self::ADD) { $to[] = $entry->getClone(); } } } /** * Merge the headers of two translations. * * @param Translations $from * @param Translations $to * @param int $options */ public static function mergeHeaders(Translations $from, Translations $to, $options = self::DEFAULTS) { if ($options & self::HEADERS_REMOVE) { foreach (array_keys($to->getHeaders()) as $name) { if ($from->getHeader($name) === null) { $to->deleteHeader($name); } } } foreach ($from->getHeaders() as $name => $value) { $current = $to->getHeader($name); if (empty($current)) { if ($options & self::HEADERS_ADD) { $to->setHeader($name, $value); } continue; } if (empty($value)) { continue; } switch ($name) { case Translations::HEADER_LANGUAGE: case Translations::HEADER_PLURAL: if ($options & self::LANGUAGE_OVERRIDE) { $to->setHeader($name, $value); } break; case Translations::HEADER_DOMAIN: if ($options & self::DOMAIN_OVERRIDE) { $to->setHeader($name, $value); } break; default: if ($options & self::HEADERS_OVERRIDE) { $to->setHeader($name, $value); } } } } } <?php use Gettext\BaseTranslator; /** * Returns the translation of a string. * * @param string $original * * @return string */ function __($original) { $text = BaseTranslator::$current->gettext($original); if (func_num_args() === 1) { return $text; } $args = array_slice(func_get_args(), 1); return is_array($args[0]) ? strtr($text, $args[0]) : vsprintf($text, $args); } /** * Noop, marks the string for translation but returns it unchanged. * * @param string $original * * @return string */ function noop__($original) { return $original; } /** * Returns the singular/plural translation of a string. * * @param string $original * @param string $plural * @param string $value * * @return string */ function n__($original, $plural, $value) { $text = BaseTranslator::$current->ngettext($original, $plural, $value); if (func_num_args() === 3) { return $text; } $args = array_slice(func_get_args(), 3); return is_array($args[0]) ? strtr($text, $args[0]) : vsprintf($text, $args); } /** * Returns the translation of a string in a specific context. * * @param string $context * @param string $original * * @return string */ function p__($context, $original) { $text = BaseTranslator::$current->pgettext($context, $original); if (func_num_args() === 2) { return $text; } $args = array_slice(func_get_args(), 2); return is_array($args[0]) ? strtr($text, $args[0]) : vsprintf($text, $args); } /** * Returns the translation of a string in a specific domain. * * @param string $domain * @param string $original * * @return string */ function d__($domain, $original) { $text = BaseTranslator::$current->dgettext($domain, $original); if (func_num_args() === 2) { return $text; } $args = array_slice(func_get_args(), 2); return is_array($args[0]) ? strtr($text, $args[0]) : vsprintf($text, $args); } /** * Returns the translation of a string in a specific domain and context. * * @param string $domain * @param string $context * @param string $original * * @return string */ function dp__($domain, $context, $original) { $text = BaseTranslator::$current->dpgettext($domain, $context, $original); if (func_num_args() === 3) { return $text; } $args = array_slice(func_get_args(), 3); return is_array($args[0]) ? strtr($text, $args[0]) : vsprintf($text, $args); } /** * Returns the singular/plural translation of a string in a specific domain. * * @param string $domain * @param string $original * @param string $plural * @param string $value * * @return string */ function dn__($domain, $original, $plural, $value) { $text = BaseTranslator::$current->dngettext($domain, $original, $plural, $value); if (func_num_args() === 4) { return $text; } $args = array_slice(func_get_args(), 4); return is_array($args[0]) ? strtr($text, $args[0]) : vsprintf($text, $args); } /** * Returns the singular/plural translation of a string in a specific context. * * @param string $context * @param string $original * @param string $plural * @param string $value * * @return string */ function np__($context, $original, $plural, $value) { $text = BaseTranslator::$current->npgettext($context, $original, $plural, $value); if (func_num_args() === 4) { return $text; } $args = array_slice(func_get_args(), 4); return is_array($args[0]) ? strtr($text, $args[0]) : vsprintf($text, $args); } /** * Returns the singular/plural translation of a string in a specific domain and context. * * @param string $domain * @param string $context * @param string $original * @param string $plural * @param string $value * * @return string */ function dnp__($domain, $context, $original, $plural, $value) { $text = BaseTranslator::$current->dnpgettext($domain, $context, $original, $plural, $value); if (func_num_args() === 5) { return $text; } $args = array_slice(func_get_args(), 5); return is_array($args[0]) ? strtr($text, $args[0]) : vsprintf($text, $args); } <?php namespace Gettext\Extractors; use BadMethodCallException; use Gettext\Translations; use Gettext\Utils\MultidimensionalArrayTrait; /** * Class to get gettext strings from php files returning arrays. */ class PhpArray extends Extractor implements ExtractorInterface { use MultidimensionalArrayTrait; /** * {@inheritdoc} */ public static function fromFile($file, Translations $translations, array $options = []) { foreach (static::getFiles($file) as $file) { static::fromArray(include($file), $translations); } } /** * {@inheritdoc} */ public static function fromString($string, Translations $translations, array $options = []) { throw new BadMethodCallException('PhpArray::fromString() cannot be called. Use PhpArray::fromFile()'); } } <?php namespace Gettext\Extractors; use Gettext\Translations; use Gettext\Utils\HeadersExtractorTrait; use Gettext\Utils\CsvTrait; /** * Class to get gettext strings from csv. */ class CsvDictionary extends Extractor implements ExtractorInterface { use HeadersExtractorTrait; use CsvTrait; public static $options = [ 'delimiter' => ",", 'enclosure' => '"', 'escape_char' => "\\" ]; /** * {@inheritdoc} */ public static function fromString($string, Translations $translations, array $options = []) { $options += static::$options; $handle = fopen('php://memory', 'w'); fputs($handle, $string); rewind($handle); while ($row = static::fgetcsv($handle, $options)) { list($original, $translation) = $row + ['', '']; if ($original === '') { static::extractHeaders($translation, $translations); continue; } $translations->insert(null, $original)->setTranslation($translation); } fclose($handle); } } <?php namespace Gettext\Extractors; use Gettext\Translations; use Twig_Loader_Array; use Twig_Environment; use Twig_Source; use Twig_Extensions_Extension_I18n; /** * Class to get gettext strings from twig files returning arrays. */ class Twig extends Extractor implements ExtractorInterface { public static $options = [ 'extractComments' => 'notes:', 'twig' => null, ]; /** * {@inheritdoc} */ public static function fromString($string, Translations $translations, array $options = []) { $options += static::$options; $twig = $options['twig'] ?: static::createTwig(); PhpCode::fromString($twig->compileSource(new Twig_Source($string, '')), $translations, $options); } /** * Returns a Twig instance. * * @return Twig_Environment */ protected static function createTwig() { $twig = new Twig_Environment(new Twig_Loader_Array(['' => ''])); $twig->addExtension(new Twig_Extensions_Extension_I18n()); return static::$options['twig'] = $twig; } } <?php namespace Gettext\Extractors; use Gettext\Translations; use Gettext\Utils\DictionaryTrait; /** * Class to get gettext strings from plain json. */ class JsonDictionary extends Extractor implements ExtractorInterface { use DictionaryTrait; /** * {@inheritdoc} */ public static function fromString($string, Translations $translations, array $options = []) { $messages = json_decode($string, true); if (is_array($messages)) { static::fromArray($messages, $translations); } } } <?php namespace Gettext\Extractors; use Gettext\Translations; interface ExtractorInterface { /** * Extract the translations from a file. * * @param array|string $file A path of a file or files * @param Translations $translations The translations instance to append the new translations. * @param array $options */ public static function fromFile($file, Translations $translations, array $options = []); /** * Parses a string and append the translations found in the Translations instance. * * @param string $string * @param Translations $translations * @param array $options */ public static function fromString($string, Translations $translations, array $options = []); } <?php /** @noinspection PhpComposerExtensionStubsInspection */ namespace Gettext\Extractors; use DOMAttr; use DOMDocument; use DOMElement; use DOMNode; use Exception; use Gettext\Translations; use Gettext\Utils\FunctionsScanner; /** * Class to get gettext strings from VueJS template files. */ class VueJs extends Extractor implements ExtractorInterface, ExtractorMultiInterface { public static $options = [ 'constants' => [], 'functions' => [ 'gettext' => 'gettext', '__' => 'gettext', 'ngettext' => 'ngettext', 'n__' => 'ngettext', 'pgettext' => 'pgettext', 'p__' => 'pgettext', 'dgettext' => 'dgettext', 'd__' => 'dgettext', 'dngettext' => 'dngettext', 'dn__' => 'dngettext', 'dpgettext' => 'dpgettext', 'dp__' => 'dpgettext', 'npgettext' => 'npgettext', 'np__' => 'npgettext', 'dnpgettext' => 'dnpgettext', 'dnp__' => 'dnpgettext', 'noop' => 'noop', 'noop__' => 'noop', ], ]; protected static $functionsScannerClass = 'Gettext\Utils\JsFunctionsScanner'; /** * @inheritDoc * @throws Exception */ public static function fromFileMultiple($file, array $translations, array $options = []) { foreach (static::getFiles($file) as $file) { $options['file'] = $file; static::fromStringMultiple(static::readFile($file), $translations, $options); } } /** * @inheritdoc * @throws Exception */ public static function fromString($string, Translations $translations, array $options = []) { static::fromStringMultiple($string, [$translations], $options); } /** * @inheritDoc * @throws Exception */ public static function fromStringMultiple($string, array $translations, array $options = []) { $options += static::$options; $options += [ // HTML attribute prefixes we parse as JS which could contain translations (are JS expressions) 'attributePrefixes' => [ ':', 'v-bind:', 'v-on:', 'v-text', ], // HTML Tags to parse 'tagNames' => [ 'translate', ], // HTML tags to parse when attribute exists 'tagAttributes' => [ 'v-translate', ], // Comments 'commentAttributes' => [ 'translate-comment', ], 'contextAttributes' => [ 'translate-context', ], // Attribute with plural content 'pluralAttributes' => [ 'translate-plural', ], ]; // Ok, this is the weirdest hack, but let me explain: // On Linux (Mac is fine), when converting HTML to DOM, new lines get trimmed after the first tag. // So if there are new lines between <template> and next element, they are lost // So we insert a "." which is a text node, and it will prevent that newlines are stripped between elements. // Same thing happens between template and script tag. $string = str_replace('<template>', '<template>.', $string); $string = str_replace('</template>', '</template>.', $string); // Normalize newlines $string = str_replace(["\r\n", "\n\r", "\r"], "\n", $string); // VueJS files are valid HTML files, we will operate with the DOM here $dom = static::convertHtmlToDom($string); $script = static::extractScriptTag($string); // Parse the script part as a regular JS code if ($script) { $scriptLineNumber = $dom->getElementsByTagName('script')->item(0)->getLineNo(); static::getScriptTranslationsFromString( $script, $translations, $options, $scriptLineNumber - 1 ); } // Template part is parsed separately, all variables will be extracted // and handled as a regular JS code $template = $dom->getElementsByTagName('template')->item(0); if ($template) { static::getTemplateTranslations( $template, $translations, $options, $template->getLineNo() - 1 ); } } /** * Extracts script tag contents using regex instead of DOM operations. * If we parse using DOM, some contents may change, for example, tags within strings will be stripped * * @param $string * @return bool|string */ protected static function extractScriptTag($string) { if (preg_match('#<\s*?script\b[^>]*>(.*?)</script\b[^>]*>#s', $string, $matches)) { return $matches[1]; } return ''; } /** * @param string $html * @return DOMDocument */ protected static function convertHtmlToDom($html) { $dom = new DOMDocument; libxml_use_internal_errors(true); // Prepend xml encoding so DOMDocument document handles UTF8 correctly. // Assuming that vue template files will not have any xml encoding tags, because duplicate tags may be ignored. $dom->loadHTML('<?xml encoding="utf-8"?>' . $html); libxml_clear_errors(); return $dom; } /** * Extract translations from script part * * @param string $scriptContents Only script tag contents, not the whole template * @param Translations|Translations[] $translations One or multiple domain Translation objects * @param array $options * @param int $lineOffset Number of lines the script is offset in the vue template file * @throws Exception */ protected static function getScriptTranslationsFromString( $scriptContents, $translations, array $options = [], $lineOffset = 0 ) { /** @var FunctionsScanner $functions */ $functions = new static::$functionsScannerClass($scriptContents); $options['lineOffset'] = $lineOffset; $functions->saveGettextFunctions($translations, $options); } /** * Parse template to extract all translations (element content and dynamic element attributes) * * @param DOMNode $dom * @param Translations|Translations[] $translations One or multiple domain Translation objects * @param array $options * @param int $lineOffset Line number where the template part starts in the vue file * @throws Exception */ protected static function getTemplateTranslations( DOMNode $dom, $translations, array $options, $lineOffset = 0 ) { // Build a JS string from all template attribute expressions $fakeAttributeJs = static::getTemplateAttributeFakeJs($options, $dom); // 1 line offset is necessary because parent template element was ignored when converting to DOM static::getScriptTranslationsFromString($fakeAttributeJs, $translations, $options, $lineOffset); // Build a JS string from template element content expressions $fakeTemplateJs = static::getTemplateFakeJs($dom); static::getScriptTranslationsFromString($fakeTemplateJs, $translations, $options, $lineOffset); static::getTagTranslations($options, $dom, $translations); } /** * @param array $options * @param DOMNode $dom * @param Translations|Translations[] $translations */ protected static function getTagTranslations(array $options, DOMNode $dom, $translations) { // Since tag scanning does not support domains, we always use the first translation given $translations = is_array($translations) ? reset($translations) : $translations; $children = $dom->childNodes; for ($i = 0; $i < $children->length; $i++) { $node = $children->item($i); if (!($node instanceof DOMElement)) { continue; } $translatable = false; if (in_array($node->tagName, $options['tagNames'], true)) { $translatable = true; } $attrList = $node->attributes; $context = null; $plural = ""; $comment = null; for ($j = 0; $j < $attrList->length; $j++) { /** @var DOMAttr $domAttr */ $domAttr = $attrList->item($j); // Check if this is a dynamic vue attribute if (in_array($domAttr->name, $options['tagAttributes'])) { $translatable = true; } if (in_array($domAttr->name, $options['contextAttributes'])) { $context = $domAttr->value; } if (in_array($domAttr->name, $options['pluralAttributes'])) { $plural = $domAttr->value; } if (in_array($domAttr->name, $options['commentAttributes'])) { $comment = $domAttr->value; } } if ($translatable) { $translation = $translations->insert($context, trim($node->textContent), $plural); $translation->addReference($options['file'], $node->getLineNo()); if ($comment) { $translation->addExtractedComment($comment); } } if ($node->hasChildNodes()) { static::getTagTranslations($options, $node, $translations); } } } /** * Extract JS expressions from element attribute bindings (excluding text within elements) * For example: <span :title="__('extract this')"> skip element content </span> * * @param array $options * @param DOMNode $dom * @return string JS code */ protected static function getTemplateAttributeFakeJs(array $options, DOMNode $dom) { $expressionsByLine = static::getVueAttributeExpressions($options['attributePrefixes'], $dom); if (empty($expressionsByLine)) { return ''; } $maxLines = max(array_keys($expressionsByLine)); $fakeJs = ''; for ($line = 1; $line <= $maxLines; $line++) { if (isset($expressionsByLine[$line])) { $fakeJs .= implode("; ", $expressionsByLine[$line]); } $fakeJs .= "\n"; } return $fakeJs; } /** * Loop DOM element recursively and parse out all dynamic vue attributes which are basically JS expressions * * @param array $attributePrefixes List of attribute prefixes we parse as JS (may contain translations) * @param DOMNode $dom * @param array $expressionByLine [lineNumber => [jsExpression, ..], ..] * @return array [lineNumber => [jsExpression, ..], ..] */ protected static function getVueAttributeExpressions( array $attributePrefixes, DOMNode $dom, array &$expressionByLine = [] ) { $children = $dom->childNodes; for ($i = 0; $i < $children->length; $i++) { $node = $children->item($i); if (!($node instanceof DOMElement)) { continue; } $attrList = $node->attributes; for ($j = 0; $j < $attrList->length; $j++) { /** @var DOMAttr $domAttr */ $domAttr = $attrList->item($j); // Check if this is a dynamic vue attribute if (static::isAttributeMatching($domAttr->name, $attributePrefixes)) { $line = $domAttr->getLineNo(); $expressionByLine += [$line => []]; $expressionByLine[$line][] = $domAttr->value; } } if ($node->hasChildNodes()) { $expressionByLine = static::getVueAttributeExpressions($attributePrefixes, $node, $expressionByLine); } } return $expressionByLine; } /** * Check if this attribute name should be parsed for translations * * @param string $attributeName * @param string[] $attributePrefixes * @return bool */ protected static function isAttributeMatching($attributeName, $attributePrefixes) { foreach ($attributePrefixes as $prefix) { if (strpos($attributeName, $prefix) === 0) { return true; } } return false; } /** * Extract JS expressions from within template elements (excluding attributes) * For example: <span :title="skip attributes"> {{__("extract element content")}} </span> * * @param DOMNode $dom * @return string JS code */ protected static function getTemplateFakeJs(DOMNode $dom) { $fakeJs = ''; $lines = explode("\n", $dom->textContent); // Build a fake JS file from template by extracting JS expressions within each template line foreach ($lines as $line) { $expressionMatched = static::parseOneTemplateLine($line); $fakeJs .= implode("; ", $expressionMatched) . "\n"; } return $fakeJs; } /** * Match JS expressions in a template line * * @param string $line * @return string[] */ protected static function parseOneTemplateLine($line) { $line = trim($line); if (!$line) { return []; } $regex = '#\{\{(.*?)\}\}#'; preg_match_all($regex, $line, $matches); $matched = array_map(function ($v) { return trim($v, '\'"{}'); }, $matches[1]); return $matched; } } <?php namespace Gettext\Extractors; use Gettext\Translations; interface ExtractorMultiInterface { /** * Parses a string and append the translations found in the Translations instance. * Allows scanning for multiple domains at a time (each Translation has to have a different domain) * * @param string $string * @param Translations[] $translations * @param array $options */ public static function fromStringMultiple($string, array $translations, array $options = []); /** * Parses a string and append the translations found in the Translations instance. * Allows scanning for multiple domains at a time (each Translation has to have a different domain) * * @param $file * @param Translations[] $translations * @param array $options */ public static function fromFileMultiple($file, array $translations, array $options = []); } <?php namespace Gettext\Extractors; use Gettext\Translations; /** * Class to get gettext strings from json files. */ class Jed extends Extractor implements ExtractorInterface { /** * {@inheritdoc} */ public static function fromString($string, Translations $translations, array $options = []) { static::extract(json_decode($string, true), $translations); } /** * Handle an array of translations and append to the Translations instance. * * @param array $content * @param Translations $translations */ public static function extract(array $content, Translations $translations) { $messages = current($content); $headers = isset($messages['']) ? $messages[''] : null; unset($messages['']); if (!empty($headers['domain'])) { $translations->setDomain($headers['domain']); } if (!empty($headers['lang'])) { $translations->setLanguage($headers['lang']); } if (!empty($headers['plural-forms'])) { $translations->setHeader(Translations::HEADER_PLURAL, $headers['plural-forms']); } $context_glue = '\u0004'; foreach ($messages as $key => $translation) { $key = explode($context_glue, $key); $context = isset($key[1]) ? array_shift($key) : ''; $translations->insert($context, array_shift($key)) ->setTranslation(array_shift($translation)) ->setPluralTranslations($translation); } } } <?php namespace Gettext\Extractors; use Gettext\Translations; use Gettext\Translation; use SimpleXMLElement; /** * Class to get gettext strings from xliff format. */ class Xliff extends Extractor implements ExtractorInterface { public static $options = [ 'unitid_as_id' => false ]; /** * {@inheritdoc} */ public static function fromString($string, Translations $translations, array $options = []) { $options += static::$options; $xml = new SimpleXMLElement($string, null, false); foreach ($xml->file as $file) { if (isset($file->notes)) { foreach ($file->notes->note as $note) { $translations->setHeader($note['id'], (string) $note); } } foreach ($file->unit as $unit) { foreach ($unit->segment as $segment) { $targets = []; foreach ($segment->target as $target) { $targets[] = (string) $target; } $translation = $translations->createNewTranslation(null, (string) $segment->source); if (isset($unit['id'])) { $unitId = (string) $unit['id']; $translation->addComment("XLIFF_UNIT_ID: $unitId"); if ($options['unitid_as_id']) { $translation->setId($unitId); } } $translation->setTranslation(array_shift($targets)); $translation->setPluralTranslations($targets); if (isset($unit->notes)) { foreach ($unit->notes->note as $note) { switch ($note['category']) { case 'context': $translation = $translation->getClone((string) $note); break; case 'extracted-comment': $translation->addExtractedComment((string) $note); break; case 'flag': $translation->addFlag((string) $note); break; case 'reference': $ref = explode(':', (string) $note, 2); $translation->addReference($ref[0], isset($ref[1]) ? $ref[1] : null); break; default: $translation->addComment((string) $note); break; } } } $translations[] = $translation; } } } } } <?php namespace Gettext\Extractors; use Exception; use Gettext\Translations; use Gettext\Utils\FunctionsScanner; /** * Class to get gettext strings from javascript files. */ class JsCode extends Extractor implements ExtractorInterface, ExtractorMultiInterface { public static $options = [ 'constants' => [], 'functions' => [ 'gettext' => 'gettext', '__' => 'gettext', 'ngettext' => 'ngettext', 'n__' => 'ngettext', 'pgettext' => 'pgettext', 'p__' => 'pgettext', 'dgettext' => 'dgettext', 'd__' => 'dgettext', 'dngettext' => 'dngettext', 'dn__' => 'dngettext', 'dpgettext' => 'dpgettext', 'dp__' => 'dpgettext', 'npgettext' => 'npgettext', 'np__' => 'npgettext', 'dnpgettext' => 'dnpgettext', 'dnp__' => 'dnpgettext', 'noop' => 'noop', 'noop__' => 'noop', ], ]; protected static $functionsScannerClass = 'Gettext\Utils\JsFunctionsScanner'; /** * @inheritdoc * @throws Exception */ public static function fromString($string, Translations $translations, array $options = []) { static::fromStringMultiple($string, [$translations], $options); } /** * @inheritDoc * @throws Exception */ public static function fromStringMultiple($string, array $translations, array $options = []) { $options += static::$options; /** @var FunctionsScanner $functions */ $functions = new static::$functionsScannerClass($string); $functions->saveGettextFunctions($translations, $options); } /** * @inheritDoc * @throws Exception */ public static function fromFileMultiple($file, array $translations, array $options = []) { foreach (static::getFiles($file) as $file) { $options['file'] = $file; static::fromStringMultiple(static::readFile($file), $translations, $options); } } } <?php namespace Gettext\Extractors; use Exception; use Gettext\Translations; use Gettext\Utils\StringReader; /** * Class to get gettext strings from .mo files. */ class Mo extends Extractor implements ExtractorInterface { const MAGIC1 = -1794895138; const MAGIC2 = -569244523; const MAGIC3 = 2500072158; protected static $stringReaderClass = 'Gettext\Utils\StringReader'; /** * {@inheritdoc} */ public static function fromString($string, Translations $translations, array $options = []) { /** @var StringReader $stream */ $stream = new static::$stringReaderClass($string); $magic = static::readInt($stream, 'V'); if (($magic === static::MAGIC1) || ($magic === static::MAGIC3)) { //to make sure it works for 64-bit platforms $byteOrder = 'V'; //low endian } elseif ($magic === (static::MAGIC2 & 0xFFFFFFFF)) { $byteOrder = 'N'; //big endian } else { throw new Exception('Not MO file'); } static::readInt($stream, $byteOrder); $total = static::readInt($stream, $byteOrder); //total string count $originals = static::readInt($stream, $byteOrder); //offset of original table $tran = static::readInt($stream, $byteOrder); //offset of translation table $stream->seekto($originals); $table_originals = static::readIntArray($stream, $byteOrder, $total * 2); $stream->seekto($tran); $table_translations = static::readIntArray($stream, $byteOrder, $total * 2); for ($i = 0; $i < $total; ++$i) { $next = $i * 2; $stream->seekto($table_originals[$next + 2]); $original = $stream->read($table_originals[$next + 1]); $stream->seekto($table_translations[$next + 2]); $translated = $stream->read($table_translations[$next + 1]); if ($original === '') { // Headers foreach (explode("\n", $translated) as $headerLine) { if ($headerLine === '') { continue; } $headerChunks = preg_split('/:\s*/', $headerLine, 2); $translations->setHeader($headerChunks[0], isset($headerChunks[1]) ? $headerChunks[1] : ''); } continue; } $chunks = explode("\x04", $original, 2); if (isset($chunks[1])) { $context = $chunks[0]; $original = $chunks[1]; } else { $context = ''; } $chunks = explode("\x00", $original, 2); if (isset($chunks[1])) { $original = $chunks[0]; $plural = $chunks[1]; } else { $plural = ''; } $translation = $translations->insert($context, $original, $plural); if ($translated === '') { continue; } if ($plural === '') { $translation->setTranslation($translated); continue; } $v = explode("\x00", $translated); $translation->setTranslation(array_shift($v)); $translation->setPluralTranslations($v); } } /** * @param StringReader $stream * @param string $byteOrder */ protected static function readInt(StringReader $stream, $byteOrder) { if (($read = $stream->read(4)) === false) { return false; } $read = unpack($byteOrder, $read); return array_shift($read); } /** * @param StringReader $stream * @param string $byteOrder * @param int $count */ protected static function readIntArray(StringReader $stream, $byteOrder, $count) { return unpack($byteOrder.$count, $stream->read(4 * $count)); } } <?php namespace Gettext\Extractors; use Gettext\Translations; use Gettext\Utils\HeadersExtractorTrait; use Gettext\Utils\CsvTrait; /** * Class to get gettext strings from csv. */ class Csv extends Extractor implements ExtractorInterface { use HeadersExtractorTrait; use CsvTrait; public static $options = [ 'delimiter' => ",", 'enclosure' => '"', 'escape_char' => "\\" ]; /** * {@inheritdoc} */ public static function fromString($string, Translations $translations, array $options = []) { $options += static::$options; $handle = fopen('php://memory', 'w'); fputs($handle, $string); rewind($handle); while ($row = static::fgetcsv($handle, $options)) { $context = array_shift($row); $original = array_shift($row); if ($context === '' && $original === '') { static::extractHeaders(array_shift($row), $translations); continue; } $translation = $translations->insert($context, $original); if (!empty($row)) { $translation->setTranslation(array_shift($row)); $translation->setPluralTranslations($row); } } fclose($handle); } } <?php namespace Gettext\Extractors; use Gettext\Translations; use Gettext\Utils\MultidimensionalArrayTrait; /** * Class to get gettext strings from json. */ class Json extends Extractor implements ExtractorInterface { use MultidimensionalArrayTrait; /** * {@inheritdoc} */ public static function fromString($string, Translations $translations, array $options = []) { $messages = json_decode($string, true); if (is_array($messages)) { static::fromArray($messages, $translations); } } } <?php namespace Gettext\Extractors; use Exception; use Gettext\Translations; use Gettext\Utils\FunctionsScanner; /** * Class to get gettext strings from php files returning arrays. */ class PhpCode extends Extractor implements ExtractorInterface, ExtractorMultiInterface { public static $options = [ // - false: to not extract comments // - empty string: to extract all comments // - non-empty string: to extract comments that start with that string // - array with strings to extract comments format. 'extractComments' => false, 'constants' => [], 'functions' => [ 'gettext' => 'gettext', '__' => 'gettext', 'ngettext' => 'ngettext', 'n__' => 'ngettext', 'pgettext' => 'pgettext', 'p__' => 'pgettext', 'dgettext' => 'dgettext', 'd__' => 'dgettext', 'dngettext' => 'dngettext', 'dn__' => 'dngettext', 'dpgettext' => 'dpgettext', 'dp__' => 'dpgettext', 'npgettext' => 'npgettext', 'np__' => 'npgettext', 'dnpgettext' => 'dnpgettext', 'dnp__' => 'dnpgettext', 'noop' => 'noop', 'noop__' => 'noop', ], ]; protected static $functionsScannerClass = 'Gettext\Utils\PhpFunctionsScanner'; /** * {@inheritdoc} * @throws Exception */ public static function fromString($string, Translations $translations, array $options = []) { static::fromStringMultiple($string, [$translations], $options); } /** * @inheritDoc * @throws Exception */ public static function fromStringMultiple($string, array $translations, array $options = []) { $options += static::$options; /** @var FunctionsScanner $functions */ $functions = new static::$functionsScannerClass($string); if ($options['extractComments'] !== false) { $functions->enableCommentsExtraction($options['extractComments']); } $functions->saveGettextFunctions($translations, $options); } /** * @inheritDoc */ public static function fromFileMultiple($file, array $translations, array $options = []) { foreach (static::getFiles($file) as $file) { $options['file'] = $file; static::fromStringMultiple(static::readFile($file), $translations, $options); } } /** * Decodes a T_CONSTANT_ENCAPSED_STRING string. * * @param string $value * * @return string */ public static function convertString($value) { if (strpos($value, '\\') === false) { return substr($value, 1, -1); } if ($value[0] === "'") { return strtr(substr($value, 1, -1), ['\\\\' => '\\', '\\\'' => '\'']); } $value = substr($value, 1, -1); return preg_replace_callback( '/\\\(n|r|t|v|e|f|\$|"|\\\|x[0-9A-Fa-f]{1,2}|u{[0-9a-f]{1,6}}|[0-7]{1,3})/', function ($match) { switch ($match[1][0]) { case 'n': return "\n"; case 'r': return "\r"; case 't': return "\t"; case 'v': return "\v"; case 'e': return "\e"; case 'f': return "\f"; case '$': return '$'; case '"': return '"'; case '\\': return '\\'; case 'x': return chr(hexdec(substr($match[1], 1))); case 'u': return static::unicodeChar(hexdec(substr($match[1], 1))); default: return chr(octdec($match[1])); } }, $value ); } /** * @param $dec * @return string|null * @see http://php.net/manual/en/function.chr.php#118804 */ protected static function unicodeChar($dec) { if ($dec < 0x80) { return chr($dec); } if ($dec < 0x0800) { return chr(0xC0 + ($dec >> 6)) . chr(0x80 + ($dec & 0x3f)); } if ($dec < 0x010000) { return chr(0xE0 + ($dec >> 12)) . chr(0x80 + (($dec >> 6) & 0x3f)) . chr(0x80 + ($dec & 0x3f)); } if ($dec < 0x200000) { return chr(0xF0 + ($dec >> 18)) . chr(0x80 + (($dec >> 12) & 0x3f)) . chr(0x80 + (($dec >> 6) & 0x3f)) . chr(0x80 + ($dec & 0x3f)); } return null; } } <?php namespace Gettext\Extractors; use Gettext\Translations; use Illuminate\Filesystem\Filesystem; use Illuminate\View\Compilers\BladeCompiler; /** * Class to get gettext strings from blade.php files returning arrays. */ class Blade extends Extractor implements ExtractorInterface { /** * {@inheritdoc} */ public static function fromString($string, Translations $translations, array $options = []) { if (empty($options['facade'])) { $cachePath = empty($options['cachePath']) ? sys_get_temp_dir() : $options['cachePath']; $bladeCompiler = new BladeCompiler(new Filesystem(), $cachePath); if (method_exists($bladeCompiler, 'withoutComponentTags')) { $bladeCompiler->withoutComponentTags(); } $string = $bladeCompiler->compileString($string); } else { $string = $options['facade']::compileString($string); } PhpCode::fromString($string, $translations, $options); } } <?php namespace Gettext\Extractors; use Gettext\Translations; use Gettext\Utils\DictionaryTrait; use Symfony\Component\Yaml\Yaml as YamlParser; /** * Class to get gettext strings from yaml. */ class YamlDictionary extends Extractor implements ExtractorInterface { use DictionaryTrait; /** * {@inheritdoc} */ public static function fromString($string, Translations $translations, array $options = []) { $messages = YamlParser::parse($string); if (is_array($messages)) { static::fromArray($messages, $translations); } } } <?php namespace Gettext\Extractors; use Exception; use InvalidArgumentException; use Gettext\Translations; abstract class Extractor implements ExtractorInterface { /** * {@inheritdoc} */ public static function fromFile($file, Translations $translations, array $options = []) { foreach (static::getFiles($file) as $file) { $options['file'] = $file; static::fromString(static::readFile($file), $translations, $options); } } /** * Checks and returns all files. * * @param string|array $file The file/s * * @return array The file paths */ protected static function getFiles($file) { if (empty($file)) { throw new InvalidArgumentException('There is not any file defined'); } if (is_string($file)) { if (!is_file($file)) { throw new InvalidArgumentException("'$file' is not a valid file"); } if (!is_readable($file)) { throw new InvalidArgumentException("'$file' is not a readable file"); } return [$file]; } if (is_array($file)) { $files = []; foreach ($file as $f) { $files = array_merge($files, static::getFiles($f)); } return $files; } throw new InvalidArgumentException('The first argument must be string or array'); } /** * Reads and returns the content of a file. * * @param string $file * * @return string */ protected static function readFile($file) { $length = filesize($file); if (!($fd = fopen($file, 'rb'))) { throw new Exception("Cannot read the file '$file', probably permissions"); } $content = $length ? fread($fd, $length) : ''; fclose($fd); return $content; } } <?php namespace Gettext\Extractors; use Gettext\Translations; use Gettext\Utils\MultidimensionalArrayTrait; use Symfony\Component\Yaml\Yaml as YamlParser; /** * Class to get gettext strings from yaml. */ class Yaml extends Extractor implements ExtractorInterface { use MultidimensionalArrayTrait; /** * {@inheritdoc} */ public static function fromString($string, Translations $translations, array $options = []) { $messages = YamlParser::parse($string); if (is_array($messages)) { static::fromArray($messages, $translations); } } } <?php namespace Gettext\Extractors; use Gettext\Translations; use Gettext\Translation; use Gettext\Utils\HeadersExtractorTrait; /** * Class to get gettext strings from php files returning arrays. */ class Po extends Extractor implements ExtractorInterface { use HeadersExtractorTrait; /** * Parses a .po file and append the translations found in the Translations instance. * * {@inheritdoc} */ public static function fromString($string, Translations $translations, array $options = []) { $lines = explode("\n", $string); $i = 0; $translation = $translations->createNewTranslation('', ''); for ($n = count($lines); $i < $n; ++$i) { $line = trim($lines[$i]); $line = static::fixMultiLines($line, $lines, $i); if ($line === '') { if ($translation->is('', '')) { static::extractHeaders($translation->getTranslation(), $translations); } elseif ($translation->hasOriginal()) { $translations[] = $translation; } $translation = $translations->createNewTranslation('', ''); continue; } $splitLine = preg_split('/\s+/', $line, 2); $key = $splitLine[0]; $data = isset($splitLine[1]) ? $splitLine[1] : ''; if ($key === '#~') { $translation->setDisabled(true); $splitLine = preg_split('/\s+/', $data, 2); $key = $splitLine[0]; $data = isset($splitLine[1]) ? $splitLine[1] : ''; } switch ($key) { case '#': $translation->addComment($data); $append = null; break; case '#.': $translation->addExtractedComment($data); $append = null; break; case '#,': foreach (array_map('trim', explode(',', trim($data))) as $value) { $translation->addFlag($value); } $append = null; break; case '#:': foreach (preg_split('/\s+/', trim($data)) as $value) { if (preg_match('/^(.+)(:(\d*))?$/U', $value, $matches)) { $translation->addReference($matches[1], isset($matches[3]) ? $matches[3] : null); } } $append = null; break; case 'msgctxt': $translation = $translation->getClone(static::convertString($data)); $append = 'Context'; break; case 'msgid': $translation = $translation->getClone(null, static::convertString($data)); $append = 'Original'; break; case 'msgid_plural': $translation->setPlural(static::convertString($data)); $append = 'Plural'; break; case 'msgstr': case 'msgstr[0]': $translation->setTranslation(static::convertString($data)); $append = 'Translation'; break; case 'msgstr[1]': $translation->setPluralTranslations([static::convertString($data)]); $append = 'PluralTranslation'; break; default: if (strpos($key, 'msgstr[') === 0) { $p = $translation->getPluralTranslations(); $p[] = static::convertString($data); $translation->setPluralTranslations($p); $append = 'PluralTranslation'; break; } if (isset($append)) { if ($append === 'Context') { $translation = $translation->getClone($translation->getContext() ."\n" .static::convertString($data)); break; } if ($append === 'Original') { $translation = $translation->getClone(null, $translation->getOriginal() ."\n" .static::convertString($data)); break; } if ($append === 'PluralTranslation') { $p = $translation->getPluralTranslations(); $p[] = array_pop($p)."\n".static::convertString($data); $translation->setPluralTranslations($p); break; } $getMethod = 'get'.$append; $setMethod = 'set'.$append; $translation->$setMethod($translation->$getMethod()."\n".static::convertString($data)); } break; } } if ($translation->hasOriginal() && !in_array($translation, iterator_to_array($translations))) { $translations[] = $translation; } } /** * Gets one string from multiline strings. * * @param string $line * @param array $lines * @param int &$i * * @return string */ protected static function fixMultiLines($line, array $lines, &$i) { for ($j = $i, $t = count($lines); $j < $t; ++$j) { if (substr($line, -1, 1) == '"' && isset($lines[$j + 1])) { $nextLine = trim($lines[$j + 1]); if (substr($nextLine, 0, 1) == '"') { $line = substr($line, 0, -1).substr($nextLine, 1); continue; } if (substr($nextLine, 0, 4) == '#~ "') { $line = substr($line, 0, -1).substr($nextLine, 4); continue; } } $i = $j; break; } return $line; } /** * Convert a string from its PO representation. * * @param string $value * * @return string */ public static function convertString($value) { if (!$value) { return ''; } if ($value[0] === '"') { $value = substr($value, 1, -1); } return strtr( $value, [ '\\\\' => '\\', '\\a' => "\x07", '\\b' => "\x08", '\\t' => "\t", '\\n' => "\n", '\\v' => "\x0b", '\\f' => "\x0c", '\\r' => "\r", '\\"' => '"', ] ); } } <?php namespace Gettext; abstract class BaseTranslator implements TranslatorInterface { /** @var TranslatorInterface */ public static $current; /** * @see TranslatorInterface */ public function noop($original) { return $original; } /** * @see TranslatorInterface */ public function register() { $previous = static::$current; static::$current = $this; static::includeFunctions(); return $previous; } /** * Include the gettext functions */ public static function includeFunctions() { include_once __DIR__.'/translator_functions.php'; } } <?php spl_autoload_register(function ($class) { if (strpos($class, 'Gettext\\') !== 0) { return; } $file = __DIR__.str_replace('\\', DIRECTORY_SEPARATOR, substr($class, strlen('Gettext'))).'.php'; if (is_file($file)) { require_once $file; } }); <?php namespace Gettext\Languages; use Exception; /** * Holds the CLDR data. */ class CldrData { /** * Super-special plural category: this should always be present for any language. * * @var string */ const OTHER_CATEGORY = 'other'; /** * The list of the plural categories, sorted from 'zero' to 'other'. * * @var string[] */ public static $categories = array('zero', 'one', 'two', 'few', 'many', self::OTHER_CATEGORY); /** * The loaded CLDR data. * * @var array */ private static $data; /** * @var array */ private static $plurals; /** * Returns a dictionary containing the language names. * The keys are the language identifiers. * The values are the language names in US English. * * @return string[] */ public static function getLanguageNames() { return self::getData('languages'); } /** * Return a dictionary containing the territory names (in US English). * The keys are the territory identifiers. * The values are the territory names in US English. * * @return string[] */ public static function getTerritoryNames() { return self::getData('territories'); } /** * Return a dictionary containing the script names (in US English). * The keys are the script identifiers. * The values are the script names in US English. * * @param bool $standAlone set to true to retrieve the stand-alone script names, false otherwise * * @return string[] */ public static function getScriptNames($standAlone) { return self::getData($standAlone ? 'standAloneScripts' : 'scripts'); } /** * A dictionary containing the plural rules. * The keys are the language identifiers. * The values are arrays whose keys are the CLDR category names and the values are the CLDR category definition. * * @example The English key-value pair is somethink like this: * <code><pre> * "en": { * "pluralRule-count-one": "i = 1 and v = 0 @integer 1", * "pluralRule-count-other": " @integer 0, 2~16, 100, 1000, 10000, 100000, 1000000, … @decimal 0.0~1.5, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, …" * } * </pre></code> * * @return array */ public static function getPlurals() { return self::getData('plurals'); } /** * Return a list of superseded language codes. * * @return array keys are the former language codes, values are the new language/locale codes */ public static function getSupersededLanguages() { return self::getData('supersededLanguages'); } /** * Retrieve the name of a language, as well as if a language code is deprecated in favor of another language code. * * @param string $id the language identifier * * @return array|null Returns an array with the keys 'id' (normalized), 'name', 'supersededBy' (optional), 'territory' (optional), 'script' (optional), 'baseLanguage' (optional), 'categories'. If $id is not valid returns null. */ public static function getLanguageInfo($id) { $result = null; $matches = array(); if (preg_match('/^([a-z]{2,3})(?:[_\-]([a-z]{4}))?(?:[_\-]([a-z]{2}|[0-9]{3}))?(?:$|-)/i', $id, $matches)) { $languageId = strtolower($matches[1]); $scriptId = (isset($matches[2]) && ($matches[2] !== '')) ? ucfirst(strtolower($matches[2])) : null; $territoryId = (isset($matches[3]) && ($matches[3] !== '')) ? strtoupper($matches[3]) : null; $normalizedId = $languageId; if (isset($scriptId)) { $normalizedId .= '_' . $scriptId; } if (isset($territoryId)) { $normalizedId .= '_' . $territoryId; } // Structure precedence: see Likely Subtags - http://www.unicode.org/reports/tr35/tr35-31/tr35.html#Likely_Subtags $variants = array(); $variantsWithScript = array(); $variantsWithTerritory = array(); if (isset($scriptId) && isset($territoryId)) { $variantsWithTerritory[] = $variantsWithScript[] = $variants[] = "{$languageId}_{$scriptId}_{$territoryId}"; } if (isset($scriptId)) { $variantsWithScript[] = $variants[] = "{$languageId}_{$scriptId}"; } if (isset($territoryId)) { $variantsWithTerritory[] = $variants[] = "{$languageId}_{$territoryId}"; } $variants[] = $languageId; $allGood = true; $scriptName = null; $scriptStandAloneName = null; if (isset($scriptId)) { $scriptNames = self::getScriptNames(false); if (isset($scriptNames[$scriptId])) { $scriptName = $scriptNames[$scriptId]; $scriptStandAloneNames = self::getScriptNames(true); $scriptStandAloneName = $scriptStandAloneNames[$scriptId]; } else { $allGood = false; } } $territoryName = null; if (isset($territoryId)) { $territoryNames = self::getTerritoryNames(); if (isset($territoryNames[$territoryId])) { if ($territoryId !== '001') { $territoryName = $territoryNames[$territoryId]; } } else { $allGood = false; } } $languageName = null; $languageNames = self::getLanguageNames(); foreach ($variants as $variant) { if (isset($languageNames[$variant])) { $languageName = $languageNames[$variant]; if (isset($scriptName) && (!in_array($variant, $variantsWithScript))) { $languageName = $scriptName . ' ' . $languageName; } if (isset($territoryName) && (!in_array($variant, $variantsWithTerritory))) { $languageName .= ' (' . $territoryNames[$territoryId] . ')'; } break; } } if (!isset($languageName)) { $allGood = false; } $baseLanguage = null; if (isset($scriptId) || isset($territoryId)) { if (isset($languageNames[$languageId]) && ($languageNames[$languageId] !== $languageName)) { $baseLanguage = $languageNames[$languageId]; } } $plural = null; $plurals = self::getPlurals(); foreach ($variants as $variant) { if (isset($plurals[$variant])) { $plural = $plurals[$variant]; break; } } if (!isset($plural)) { $allGood = false; } $supersededBy = null; $supersededBys = self::getSupersededLanguages(); foreach ($variants as $variant) { if (isset($supersededBys[$variant])) { $supersededBy = $supersededBys[$variant]; break; } } if ($allGood) { $result = array(); $result['id'] = $normalizedId; $result['name'] = $languageName; if (isset($supersededBy)) { $result['supersededBy'] = $supersededBy; } if (isset($scriptStandAloneName)) { $result['script'] = $scriptStandAloneName; } if (isset($territoryName)) { $result['territory'] = $territoryName; } if (isset($baseLanguage)) { $result['baseLanguage'] = $baseLanguage; } $result['categories'] = $plural; } } return $result; } /** * Returns the loaded CLDR data. * * @param string $key Can be 'languages', 'territories', 'plurals', 'supersededLanguages', 'scripts', 'standAloneScripts' * * @return array */ private static function getData($key) { if (!isset(self::$data)) { $fixKeys = function ($list, &$standAlone = null) { $result = array(); $standAlone = array(); $match = null; foreach ($list as $key => $value) { $variant = ''; if (preg_match('/^(.+)-alt-(short|variant|stand-alone|long|menu)$/', $key, $match)) { $key = $match[1]; $variant = $match[2]; } $key = str_replace('-', '_', $key); switch ($key) { case 'root': // Language: Root case 'und': // Language: Unknown Language case 'zxx': // Language: No linguistic content case 'ZZ': // Territory: Unknown Region case 'Zinh': // Script: Inherited case 'Zmth': // Script: Mathematical Notation case 'Zsym': // Script: Symbols case 'Zxxx': // Script: Unwritten case 'Zyyy': // Script: Common case 'Zzzz': // Script: Unknown Script break; default: switch ($variant) { case 'stand-alone': $standAlone[$key] = $value; break; case '': $result[$key] = $value; break; } break; } } return $result; }; $data = array(); $json = json_decode(file_get_contents(__DIR__ . '/cldr-data/main/en-US/languages.json'), true); $data['languages'] = $fixKeys($json['main']['en-US']['localeDisplayNames']['languages']); $json = json_decode(file_get_contents(__DIR__ . '/cldr-data/main/en-US/territories.json'), true); $data['territories'] = $fixKeys($json['main']['en-US']['localeDisplayNames']['territories']); $json = json_decode(file_get_contents(__DIR__ . '/cldr-data/supplemental/plurals.json'), true); $data['plurals'] = $fixKeys($json['supplemental']['plurals-type-cardinal']); $json = json_decode(file_get_contents(__DIR__ . '/cldr-data/main/en-US/scripts.json'), true); $data['scripts'] = $fixKeys($json['main']['en-US']['localeDisplayNames']['scripts'], $data['standAloneScripts']); $data['standAloneScripts'] = array_merge($data['scripts'], $data['standAloneScripts']); $data['scripts'] = array_merge($data['standAloneScripts'], $data['scripts']); $data['supersededLanguages'] = array(); // Remove the languages for which we don't have plurals $m = null; foreach (array_keys(array_diff_key($data['languages'], $data['plurals'])) as $missingPlural) { if (preg_match('/^([a-z]{2,3})_/', $missingPlural, $m)) { if (!isset($data['plurals'][$m[1]])) { unset($data['languages'][$missingPlural]); } } else { unset($data['languages'][$missingPlural]); } } // Fix the languages for which we have plurals $formerCodes = array( 'in' => 'id', // former Indonesian 'iw' => 'he', // former Hebrew 'ji' => 'yi', // former Yiddish 'jw' => 'jv', // former Javanese 'mo' => 'ro_MD', // former Moldavian ); $knownMissingLanguages = array( 'guw' => 'Gun', 'hnj' => 'Mong Njua', 'nah' => 'Nahuatl', 'smi' => 'Sami', ); foreach (array_keys(array_diff_key($data['plurals'], $data['languages'])) as $missingLanguage) { if (isset($formerCodes[$missingLanguage]) && isset($data['languages'][$formerCodes[$missingLanguage]])) { $data['languages'][$missingLanguage] = $data['languages'][$formerCodes[$missingLanguage]]; $data['supersededLanguages'][$missingLanguage] = $formerCodes[$missingLanguage]; } else { if (isset($knownMissingLanguages[$missingLanguage])) { $data['languages'][$missingLanguage] = $knownMissingLanguages[$missingLanguage]; } else { throw new Exception("We have the plural rule for the language '{$missingLanguage}' but we don't have its language name"); } } } ksort($data['languages'], SORT_STRING); ksort($data['territories'], SORT_STRING); ksort($data['plurals'], SORT_STRING); ksort($data['scripts'], SORT_STRING); ksort($data['standAloneScripts'], SORT_STRING); ksort($data['supersededLanguages'], SORT_STRING); self::$data = $data; } if (!isset(self::$data[$key])) { throw new Exception("Invalid CLDR data key: '{$key}'"); } return self::$data[$key]; } } <?php namespace Gettext\Languages; use Exception; /** * A helper class that handles a single category rules (eg 'zero', 'one', ...) and its formula and examples. */ class Category { /** * The category identifier (eg 'zero', 'one', ..., 'other'). * * @var string */ public $id; /** * The gettext formula that identifies this category (null if and only if the category is 'other'). * * @var string|null */ public $formula; /** * The CLDR representation of some exemplar numeric ranges that satisfy this category. * * @var string|null */ public $examples; /** * Initialize the instance and parse the formula. * * @param string $cldrCategoryId the CLDR category identifier (eg 'pluralRule-count-one') * @param string $cldrFormulaAndExamples the CLDR formula and examples (eg 'i = 1 and v = 0 @integer 1') * * @throws \Exception */ public function __construct($cldrCategoryId, $cldrFormulaAndExamples) { $matches = array(); if (!preg_match('/^pluralRule-count-(.+)$/', $cldrCategoryId, $matches)) { throw new Exception("Invalid CLDR category: '{$cldrCategoryId}'"); } if (!in_array($matches[1], CldrData::$categories)) { throw new Exception("Invalid CLDR category: '{$cldrCategoryId}'"); } $this->id = $matches[1]; $cldrFormulaAndExamplesNormalized = trim(preg_replace('/\s+/', ' ', $cldrFormulaAndExamples)); if (!preg_match('/^([^@]*)(?:@integer([^@]+))?(?:@decimal(?:[^@]+))?$/', $cldrFormulaAndExamplesNormalized, $matches)) { throw new Exception("Invalid CLDR category rule: {$cldrFormulaAndExamples}"); } $cldrFormula = trim($matches[1]); $s = isset($matches[2]) ? trim($matches[2]) : ''; $this->examples = ($s === '') ? null : $s; switch ($this->id) { case CldrData::OTHER_CATEGORY: if ($cldrFormula !== '') { throw new Exception("The '" . CldrData::OTHER_CATEGORY . "' category should not have any formula, but it has '{$cldrFormula}'"); } $this->formula = null; break; default: if ($cldrFormula === '') { throw new Exception("The '{$this->id}' category does not have a formula"); } $this->formula = FormulaConverter::convertFormula($cldrFormula); break; } } /** * Return a list of numbers corresponding to the $examples value. * * @throws \Exception throws an Exception if we weren't able to expand the examples * * @return int[] */ public function getExampleIntegers() { return self::expandExamples($this->examples); } /** * Expand a list of examples as defined by CLDR. * * @param string $examples A string like '1, 2, 5...7, …'. * * @throws \Exception throws an Exception if we weren't able to expand $examples * * @return int[] */ public static function expandExamples($examples) { $result = array(); $m = null; if (substr($examples, -strlen(', …')) === ', …') { $examples = substr($examples, 0, strlen($examples) - strlen(', …')); } foreach (explode(',', str_replace(' ', '', $examples)) as $range) { if (preg_match('/^(?<num>\d+)((c|e)(?<exp>\d+))?$/', $range, $m)) { $result[] = (int) (isset($m['exp']) ? ($m['num'] . str_repeat('0', (int) $m['exp'])) : $range); } elseif (preg_match('/^(\d+)~(\d+)$/', $range, $m)) { $from = (int) $m[1]; $to = (int) $m[2]; $delta = $to - $from; $step = (int) max(1, $delta / 100); for ($i = $from; $i < $to; $i += $step) { $result[] = $i; } $result[] = $to; } else { throw new Exception("Unhandled test range '{$range}' in '{$examples}'"); } } if (empty($result)) { throw new Exception("No test numbers from '{$examples}'"); } return $result; } } <?php namespace Gettext\Languages; use Exception; /** * A helper class to convert a CLDR formula to a gettext formula. */ class FormulaConverter { /** * Converts a formula from the CLDR representation to the gettext representation. * * @param string $cldrFormula the CLDR formula to convert * * @throws \Exception * * @return bool|string returns true if the gettext will always evaluate to true, false if gettext will always evaluate to false, return the gettext formula otherwise */ public static function convertFormula($cldrFormula) { if (strpbrk($cldrFormula, '()') !== false) { throw new Exception("Unable to convert the formula '{$cldrFormula}': parenthesis handling not implemented"); } $orSeparatedChunks = array(); foreach (explode(' or ', $cldrFormula) as $cldrFormulaChunk) { $gettextFormulaChunk = null; $andSeparatedChunks = array(); foreach (explode(' and ', $cldrFormulaChunk) as $cldrAtom) { $gettextAtom = self::convertAtom($cldrAtom); if ($gettextAtom === false) { // One atom joined by 'and' always evaluates to false => the whole 'and' group is always false $gettextFormulaChunk = false; break; } if ($gettextAtom !== true) { $andSeparatedChunks[] = $gettextAtom; } } if (!isset($gettextFormulaChunk)) { if (empty($andSeparatedChunks)) { // All the atoms joined by 'and' always evaluate to true => the whole 'and' group is always true $gettextFormulaChunk = true; } else { $gettextFormulaChunk = implode(' && ', $andSeparatedChunks); // Special cases simplification switch ($gettextFormulaChunk) { case 'n >= 0 && n <= 2 && n != 2': $gettextFormulaChunk = 'n == 0 || n == 1'; break; } } } if ($gettextFormulaChunk === true) { // One part of the formula joined with the others by 'or' always evaluates to true => the whole formula always evaluates to true return true; } if ($gettextFormulaChunk !== false) { $orSeparatedChunks[] = $gettextFormulaChunk; } } if (empty($orSeparatedChunks)) { // All the parts joined by 'or' always evaluate to false => the whole formula always evaluates to false return false; } return implode(' || ', $orSeparatedChunks); } /** * Converts an atomic part of the CLDR formula to its gettext representation. * * @param string $cldrAtom the CLDR formula atom to convert * * @throws \Exception * * @return bool|string returns true if the gettext will always evaluate to true, false if gettext will always evaluate to false, return the gettext formula otherwise */ private static function convertAtom($cldrAtom) { $m = null; $gettextAtom = $cldrAtom; $gettextAtom = str_replace(' = ', ' == ', $gettextAtom); $gettextAtom = str_replace('i', 'n', $gettextAtom); if (preg_match('/^n( % \d+)? (!=|==) \d+$/', $gettextAtom)) { return $gettextAtom; } if (preg_match('/^n( % \d+)? (!=|==) \d+(,\d+|\.\.\d+)+$/', $gettextAtom)) { return self::expandAtom($gettextAtom); } if (preg_match('/^(?:v|w)(?: % 10+)? == (\d+)(?:\.\.\d+)?$/', $gettextAtom, $m)) { // For gettext: v == 0, w == 0 return (int) $m[1] === 0 ? true : false; } if (preg_match('/^(?:v|w)(?: % 10+)? != (\d+)(?:\.\.\d+)?$/', $gettextAtom, $m)) { // For gettext: v == 0, w == 0 return (int) $m[1] === 0 ? false : true; } if (preg_match('/^(?:f|t|c|e)(?: % 10+)? == (\d+)(?:\.\.\d+)?$/', $gettextAtom, $m)) { // f == empty, t == empty, c == empty, e == empty return (int) $m[1] === 0 ? true : false; } if (preg_match('/^(?:f|t|c|e)(?: % 10+)? != (\d+)(?:\.\.\d+)?$/', $gettextAtom, $m)) { // f == empty, t == empty, c == empty, e == empty return (int) $m[1] === 0 ? false : true; } throw new Exception("Unable to convert the formula chunk '{$cldrAtom}' from CLDR to gettext"); } /** * Expands an atom containing a range (for instance: 'n == 1,3..5'). * * @param string $atom * * @throws \Exception * * @return string */ private static function expandAtom($atom) { $m = null; if (preg_match('/^(n(?: % \d+)?) (==|!=) (\d+(?:\.\.\d+|,\d+)+)$/', $atom, $m)) { $what = $m[1]; $op = $m[2]; $chunks = array(); foreach (explode(',', $m[3]) as $range) { $chunk = null; if ((!isset($chunk)) && preg_match('/^\d+$/', $range)) { $chunk = "{$what} {$op} {$range}"; } if ((!isset($chunk)) && preg_match('/^(\d+)\.\.(\d+)$/', $range, $m)) { $from = (int) $m[1]; $to = (int) $m[2]; if (($to - $from) === 1) { switch ($op) { case '==': $chunk = "({$what} == {$from} || {$what} == {$to})"; break; case '!=': $chunk = "{$what} != {$from} && {$what} == {$to}"; break; } } else { switch ($op) { case '==': $chunk = "{$what} >= {$from} && {$what} <= {$to}"; break; case '!=': if ($what === 'n' && $from <= 0) { $chunk = "{$what} > {$to}"; } else { $chunk = "({$what} < {$from} || {$what} > {$to})"; } break; } } } if (!isset($chunk)) { throw new Exception("Unhandled range '{$range}' in '{$atom}'"); } $chunks[] = $chunk; } if (count($chunks) === 1) { return $chunks[0]; } switch ($op) { case '==': return '(' . implode(' || ', $chunks) . ')'; break; case '!=': return implode(' && ', $chunks); } } throw new Exception("Unable to expand '{$atom}'"); } } <?php namespace Gettext\Languages; use Exception; /** * Main class to convert the plural rules of a language from CLDR to gettext. */ class Language { /** * The language ID. * * @var string */ public $id; /** * The language name. * * @var string */ public $name; /** * If this language is deprecated: the gettext code of the new language. * * @var string|null */ public $supersededBy; /** * The script name. * * @var string|null */ public $script; /** * The territory name. * * @var string|null */ public $territory; /** * The name of the base language. * * @var string|null */ public $baseLanguage; /** * The list of categories. * * @var \Gettext\Languages\Category[] */ public $categories; /** * The gettext formula to decide which category should be applied. * * @var string */ public $formula; /** * Initialize the instance and parse the language code. * * @param array $info The result of CldrData::getLanguageInfo() * * @throws \Exception throws an Exception if $fullId is not valid */ private function __construct($info) { $this->id = $info['id']; $this->name = $info['name']; $this->supersededBy = isset($info['supersededBy']) ? $info['supersededBy'] : null; $this->script = isset($info['script']) ? $info['script'] : null; $this->territory = isset($info['territory']) ? $info['territory'] : null; $this->baseLanguage = isset($info['baseLanguage']) ? $info['baseLanguage'] : null; // Let's build the category list $this->categories = array(); foreach ($info['categories'] as $cldrCategoryId => $cldrFormulaAndExamples) { $category = new Category($cldrCategoryId, $cldrFormulaAndExamples); foreach ($this->categories as $c) { if ($category->id === $c->id) { throw new Exception("The category '{$category->id}' is specified more than once"); } } $this->categories[] = $category; } if (empty($this->categories)) { throw new Exception("The language '{$info['id']}' does not have any plural category"); } // Let's sort the categories from 'zero' to 'other' usort($this->categories, function (Category $category1, Category $category2) { return array_search($category1->id, CldrData::$categories) - array_search($category2->id, CldrData::$categories); }); // The 'other' category should always be there if ($this->categories[count($this->categories) - 1]->id !== CldrData::OTHER_CATEGORY) { throw new Exception("The language '{$info['id']}' does not have the '" . CldrData::OTHER_CATEGORY . "' plural category"); } $this->checkAlwaysTrueCategories(); $this->checkAlwaysFalseCategories(); $this->checkAllCategoriesWithExamples(); $this->formula = $this->buildFormula(); } /** * Return a list of all languages available. * * @throws \Exception * * @return \Gettext\Languages\Language[] */ public static function getAll() { $result = array(); foreach (array_keys(CldrData::getLanguageNames()) as $cldrLanguageId) { $result[] = new self(CldrData::getLanguageInfo($cldrLanguageId)); } return $result; } /** * Return a Language instance given the language id. * * @param string $id * * @return \Gettext\Languages\Language|null */ public static function getById($id) { $result = null; $info = CldrData::getLanguageInfo($id); if (isset($info)) { $result = new self($info); } return $result; } /** * Returns a clone of this instance with all the strings to US-ASCII. * * @return \Gettext\Languages\Language */ public function getUSAsciiClone() { $clone = clone $this; self::asciifier($clone->name); self::asciifier($clone->formula); $clone->categories = array(); foreach ($this->categories as $category) { $categoryClone = clone $category; self::asciifier($categoryClone->examples); $clone->categories[] = $categoryClone; } return $clone; } /** * Build the formula starting from the currently defined categories. * * @param bool $withoutParenthesis TRUE to build a formula in standard gettext format, FALSE (default) to build a PHP-compatible formula * * @return string */ public function buildFormula($withoutParenthesis = false) { $numCategories = count($this->categories); switch ($numCategories) { case 1: // Just one category return '0'; case 2: return self::reduceFormula(self::reverseFormula($this->categories[0]->formula)); default: $formula = (string) ($numCategories - 1); for ($i = $numCategories - 2; $i >= 0; $i--) { $f = self::reduceFormula($this->categories[$i]->formula); if (!$withoutParenthesis && !preg_match('/^\([^()]+\)$/', $f)) { $f = "({$f})"; } $formula = "{$f} ? {$i} : {$formula}"; if (!$withoutParenthesis && $i > 0) { $formula = "({$formula})"; } } return $formula; } } /** * Let's look for categories that will always occur. * This because with decimals (CLDR) we may have more cases, with integers (gettext) we have just one case. * If we found that (single) category we reduce the categories to that one only. * * @throws \Exception */ private function checkAlwaysTrueCategories() { $alwaysTrueCategory = null; foreach ($this->categories as $category) { if ($category->formula === true) { if (!isset($category->examples)) { throw new Exception("The category '{$category->id}' should always occur, but it does not have examples (so for CLDR it will never occur for integers!)"); } $alwaysTrueCategory = $category; break; } } if (isset($alwaysTrueCategory)) { foreach ($this->categories as $category) { if (($category !== $alwaysTrueCategory) && isset($category->examples)) { throw new Exception("The category '{$category->id}' should never occur, but it has some examples (so for CLDR it will occur!)"); } } $alwaysTrueCategory->id = CldrData::OTHER_CATEGORY; $alwaysTrueCategory->formula = null; $this->categories = array($alwaysTrueCategory); } } /** * Let's look for categories that will never occur. * This because with decimals (CLDR) we may have more cases, with integers (gettext) we have some less cases. * If we found those categories we strip them out. * * @throws \Exception */ private function checkAlwaysFalseCategories() { $filtered = array(); foreach ($this->categories as $category) { if ($category->formula === false) { if (isset($category->examples)) { throw new Exception("The category '{$category->id}' should never occur, but it has examples (so for CLDR it may occur!)"); } } else { $filtered[] = $category; } } $this->categories = $filtered; } /** * Let's look for categories that don't have examples. * This because with decimals (CLDR) we may have more cases, with integers (gettext) we have some less cases. * If we found those categories, we check that they never occur and we strip them out. * * @throws \Exception */ private function checkAllCategoriesWithExamples() { $allCategoriesIds = array(); $goodCategories = array(); $badCategories = array(); $badCategoriesIds = array(); foreach ($this->categories as $category) { $allCategoriesIds[] = $category->id; if (isset($category->examples)) { $goodCategories[] = $category; } else { $badCategories[] = $category; $badCategoriesIds[] = $category->id; } } if (empty($badCategories)) { return; } $removeCategoriesWithoutExamples = false; switch (implode(',', $badCategoriesIds) . '@' . implode(',', $allCategoriesIds)) { case CldrData::OTHER_CATEGORY . '@one,few,many,' . CldrData::OTHER_CATEGORY: switch ($this->buildFormula()) { case '(n % 10 == 1 && n % 100 != 11) ? 0 : ((n % 10 >= 2 && n % 10 <= 4 && (n % 100 < 12 || n % 100 > 14)) ? 1 : ((n % 10 == 0 || n % 10 >= 5 && n % 10 <= 9 || n % 100 >= 11 && n % 100 <= 14) ? 2 : 3))': // Numbers ending with 0 => case 2 ('many') // Numbers ending with 1 but not with 11 => case 0 ('one') // Numbers ending with 11 => case 2 ('many') // Numbers ending with 2 but not with 12 => case 1 ('few') // Numbers ending with 12 => case 2 ('many') // Numbers ending with 3 but not with 13 => case 1 ('few') // Numbers ending with 13 => case 2 ('many') // Numbers ending with 4 but not with 14 => case 1 ('few') // Numbers ending with 14 => case 2 ('many') // Numbers ending with 5 => case 2 ('many') // Numbers ending with 6 => case 2 ('many') // Numbers ending with 7 => case 2 ('many') // Numbers ending with 8 => case 2 ('many') // Numbers ending with 9 => case 2 ('many') // => the 'other' case never occurs: use 'other' for 'many' $removeCategoriesWithoutExamples = true; break; case '(n == 1) ? 0 : ((n % 10 >= 2 && n % 10 <= 4 && (n % 100 < 12 || n % 100 > 14)) ? 1 : ((n != 1 && (n % 10 == 0 || n % 10 == 1) || n % 10 >= 5 && n % 10 <= 9 || n % 100 >= 12 && n % 100 <= 14) ? 2 : 3))': // Numbers ending with 0 => case 2 ('many') // Numbers ending with 1 but not number 1 => case 2 ('many') // Number 1 => case 0 ('one') // Numbers ending with 2 but not with 12 => case 1 ('few') // Numbers ending with 12 => case 2 ('many') // Numbers ending with 3 but not with 13 => case 1 ('few') // Numbers ending with 13 => case 2 ('many') // Numbers ending with 4 but not with 14 => case 1 ('few') // Numbers ending with 14 => case 2 ('many') // Numbers ending with 5 => case 2 ('many') // Numbers ending with 6 => case 2 ('many') // Numbers ending with 7 => case 2 ('many') // Numbers ending with 8 => case 2 ('many') // Numbers ending with 9 => case 2 ('many') // => the 'other' case never occurs: use 'other' for 'many' $removeCategoriesWithoutExamples = true; break; } } if (!$removeCategoriesWithoutExamples) { throw new Exception("Unhandled case of plural categories without examples '" . implode(', ', $badCategoriesIds) . "' out of '" . implode(', ', $allCategoriesIds) . "'"); } if ($badCategories[count($badCategories) - 1]->id === CldrData::OTHER_CATEGORY) { // We're removing the 'other' cagory: let's change the last good category to 'other' $lastGood = $goodCategories[count($goodCategories) - 1]; $lastGood->id = CldrData::OTHER_CATEGORY; $lastGood->formula = null; } $this->categories = $goodCategories; } /** * Reverse a formula. * * @param string $formula * * @throws \Exception * * @return string */ private static function reverseFormula($formula) { if (preg_match('/^n( % \d+)? == \d+(\.\.\d+|,\d+)*?$/', $formula)) { return str_replace(' == ', ' != ', $formula); } if (preg_match('/^n( % \d+)? != \d+(\.\.\d+|,\d+)*?$/', $formula)) { return str_replace(' != ', ' == ', $formula); } if (preg_match('/^\(?n == \d+ \|\| n == \d+\)?$/', $formula)) { return trim(str_replace(array(' == ', ' || '), array(' != ', ' && '), $formula), '()'); } $m = null; if (preg_match('/^(n(?: % \d+)?) == (\d+) && (n(?: % \d+)?) != (\d+)$/', $formula, $m)) { return "{$m[1]} != {$m[2]} || {$m[3]} == {$m[4]}"; } switch ($formula) { case '(n == 1 || n == 2 || n == 3) || n % 10 != 4 && n % 10 != 6 && n % 10 != 9': return 'n != 1 && n != 2 && n != 3 && (n % 10 == 4 || n % 10 == 6 || n % 10 == 9)'; case '(n == 0 || n == 1) || n >= 11 && n <= 99': return 'n >= 2 && (n < 11 || n > 99)'; } throw new Exception("Unable to reverse the formula '{$formula}'"); } /** * Reduce some excessively complex formulas. * * @param string $formula * * @return string */ private static function reduceFormula($formula) { $map = array( 'n != 0 && n != 1' => 'n > 1', '(n == 0 || n == 1) && n != 0' => 'n == 1', ); return isset($map[$formula]) ? $map[$formula] : $formula; } /** * Take one variable and, if it's a string, we transliterate it to US-ASCII. * * @param mixed $value the variable to work on * * @throws \Exception */ private static function asciifier(&$value) { if (is_string($value) && $value !== '') { // Avoid converting from 'Ÿ' to '"Y', let's prefer 'Y' $value = strtr($value, array( 'À' => 'A', 'Á' => 'A', 'Â' => 'A', 'Ã' => 'A', 'Ä' => 'A', 'Å' => 'A', 'È' => 'E', 'É' => 'E', 'Ê' => 'E', 'Ë' => 'E', 'Ì' => 'I', 'Í' => 'I', 'Î' => 'I', 'Ï' => 'I', 'Ñ' => 'N', 'Ò' => 'O', 'Ó' => 'O', 'Ô' => 'O', 'Õ' => 'O', 'Ö' => 'O', 'Ù' => 'U', 'Ú' => 'U', 'Û' => 'U', 'Ü' => 'U', 'Ÿ' => 'Y', 'Ý' => 'Y', 'à' => 'a', 'á' => 'a', 'â' => 'a', 'ã' => 'a', 'ä' => 'a', 'å' => 'a', 'è' => 'e', 'é' => 'e', 'ê' => 'e', 'ë' => 'e', 'ì' => 'i', 'í' => 'i', 'î' => 'i', 'ï' => 'i', 'ñ' => 'n', 'ò' => 'o', 'ó' => 'o', 'ô' => 'o', 'õ' => 'o', 'ö' => 'o', 'ù' => 'u', 'ú' => 'u', 'û' => 'u', 'ü' => 'u', 'ý' => 'y', 'ÿ' => 'y', '…' => '...', 'ʼ' => "'", '’' => "'", )); } } } <?php spl_autoload_register( function ($class) { if (strpos($class, 'Gettext\\Languages\\') !== 0) { return; } $file = __DIR__ . str_replace('\\', DIRECTORY_SEPARATOR, substr($class, strlen('Gettext\\Languages'))) . '.php'; if (is_file($file)) { require_once $file; } } ); <?php namespace Gettext\Languages\Exporter; class Xml extends Exporter { /** * {@inheritdoc} * * @see \Gettext\Languages\Exporter\Exporter::getDescription() */ public static function getDescription() { return 'Build an XML file - schema available at http://mlocati.github.io/cldr-to-gettext-plural-rules/GettextLanguages.xsd'; } /** * {@inheritdoc} * * @see \Gettext\Languages\Exporter\Exporter::toStringDo() */ protected static function toStringDo($languages) { $xml = new \DOMDocument('1.0', 'UTF-8'); $xml->loadXML('<languages xmlns="https://github.com/mlocati/cldr-to-gettext-plural-rules" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="https://github.com/mlocati/cldr-to-gettext-plural-rules http://mlocati.github.io/cldr-to-gettext-plural-rules/GettextLanguages.xsd" />'); $xLanguages = $xml->firstChild; foreach ($languages as $language) { $xLanguage = $xml->createElement('language'); $xLanguage->setAttribute('id', $language->id); $xLanguage->setAttribute('name', $language->name); if (isset($language->supersededBy)) { $xLanguage->setAttribute('supersededBy', $language->supersededBy); } if (isset($language->script)) { $xLanguage->setAttribute('script', $language->script); } if (isset($language->territory)) { $xLanguage->setAttribute('territory', $language->territory); } if (isset($language->baseLanguage)) { $xLanguage->setAttribute('baseLanguage', $language->baseLanguage); } $xLanguage->setAttribute('formula', $language->formula); foreach ($language->categories as $category) { $xCategory = $xml->createElement('category'); $xCategory->setAttribute('id', $category->id); $xCategory->setAttribute('examples', $category->examples); $xLanguage->appendChild($xCategory); } $xLanguages->appendChild($xLanguage); } $xml->formatOutput = true; return $xml->saveXML(); } } <?php namespace Gettext\Languages\Exporter; use Exception; class Prettyjson extends Json { /** * {@inheritdoc} * * @see \Gettext\Languages\Exporter\Exporter::getDescription() */ public static function getDescription() { return 'Build an uncompressed JSON-encoded file (PHP 5.4 or later is needed)'; } /** * {@inheritdoc} * * @see \Gettext\Languages\Exporter\Json::getEncodeOptions() */ protected static function getEncodeOptions() { if (!(defined('\JSON_PRETTY_PRINT') && defined('\JSON_UNESCAPED_SLASHES') && defined('\JSON_UNESCAPED_UNICODE'))) { throw new Exception('PHP 5.4 or later is required to export uncompressed JSON'); } return \JSON_PRETTY_PRINT | \JSON_UNESCAPED_SLASHES | \JSON_UNESCAPED_UNICODE; } } <?php namespace Gettext\Languages\Exporter; class Docs extends Html { /** * {@inheritdoc} * * @see \Gettext\Languages\Exporter\Exporter::isForPublicUse() */ public static function isForPublicUse() { return false; } /** * {@inheritdoc} * * @see \Gettext\Languages\Exporter\Exporter::getDescription() */ public static function getDescription() { return 'Build the page https://php-gettext.github.io/Languages/'; } /** * {@inheritdoc} * * @see \Gettext\Languages\Exporter\Exporter::toStringDo() */ protected static function toStringDo($languages) { $result = <<<'EOT' <!doctype html> <html lang="en"> <head> <meta charset="utf-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="author" content="Michele Locati"> <title>gettext plural rules - built from CLDR
EOT; $result .= static::buildTable($languages, true); $result .= <<<'EOT'
EOT; return $result; } } id . '\' => array('; $lines[] = ' \'name\' => \'' . addslashes($lc->name) . '\','; if (isset($lc->supersededBy)) { $lines[] = ' \'supersededBy\' => \'' . $lc->supersededBy . '\','; } if (isset($lc->script)) { $lines[] = ' \'script\' => \'' . addslashes($lc->script) . '\','; } if (isset($lc->territory)) { $lines[] = ' \'territory\' => \'' . addslashes($lc->territory) . '\','; } if (isset($lc->baseLanguage)) { $lines[] = ' \'baseLanguage\' => \'' . addslashes($lc->baseLanguage) . '\','; } $lines[] = ' \'formula\' => \'' . $lc->formula . '\','; $lines[] = ' \'plurals\' => ' . count($lc->categories) . ','; $catNames = array(); foreach ($lc->categories as $c) { $catNames[] = "'{$c->id}'"; } $lines[] = ' \'cases\' => array(' . implode(', ', $catNames) . '),'; $lines[] = ' \'examples\' => array('; foreach ($lc->categories as $c) { $lines[] = ' \'' . $c->id . '\' => \'' . $c->examples . '\','; } $lines[] = ' ),'; $lines[] = ' ),'; } $lines[] = ');'; $lines[] = ''; return implode("\n", $lines); } } name; if (isset($language->supersededBy)) { $item['supersededBy'] = $language->supersededBy; } if (isset($language->script)) { $item['script'] = $language->script; } if (isset($language->territory)) { $item['territory'] = $language->territory; } if (isset($language->baseLanguage)) { $item['baseLanguage'] = $language->baseLanguage; } $item['formula'] = $language->formula; $item['plurals'] = count($language->categories); $item['cases'] = array(); $item['examples'] = array(); foreach ($language->categories as $category) { $item['cases'][] = $category->id; $item['examples'][$category->id] = $category->examples; } $list[$language->id] = $item; } return json_encode($list, static::getEncodeOptions()); } } $class) { if (call_user_func(self::getExporterClassName($handle) . '::isForPublicUse') === true) { $result[$handle] = $class; } } } else { $result = self::$exporters; } return $result; } /** * Return the description of a specific exporter. * * @param string $exporterHandle the handle of the exporter * * @throws \Exception throws an Exception if $exporterHandle is not valid * * @return string */ final public static function getExporterDescription($exporterHandle) { $exporters = self::getExporters(); if (!isset($exporters[$exporterHandle])) { throw new Exception("Invalid exporter handle: '{$exporterHandle}'"); } return call_user_func(self::getExporterClassName($exporterHandle) . '::getDescription'); } /** * Returns the fully qualified class name of a exporter given its handle. * * @param string $exporterHandle the exporter class handle * * @return string */ final public static function getExporterClassName($exporterHandle) { return __NAMESPACE__ . '\\' . ucfirst(strtolower($exporterHandle)); } /** * Convert a list of Language instances to string. * * @param \Gettext\Languages\Language[] $languages the Language instances to convert * @param array|null $options * * @return string */ final public static function toString($languages, $options = null) { if (isset($options) && is_array($options)) { if (isset($options['us-ascii']) && $options['us-ascii']) { $asciiList = array(); foreach ($languages as $language) { $asciiList[] = $language->getUSAsciiClone(); } $languages = $asciiList; } } return static::toStringDo($languages); } /** * Save the Language instances to a file. * * @param \Gettext\Languages\Language[] $languages the Language instances to convert * @param array|null $options * * @throws \Exception */ final public static function toFile($languages, $filename, $options = null) { $data = self::toString($languages, $options); if (@file_put_contents($filename, $data) === false) { throw new Exception("Error writing data to '{$filename}'"); } } /** * Is this exporter for public use? * * @return bool */ public static function isForPublicUse() { return true; } /** * Return a short description of the exporter. * * @return string */ public static function getDescription() { throw new Exception(get_called_class() . ' does not implement the method ' . __FUNCTION__); } /** * Convert a list of Language instances to string. * * @param \Gettext\Languages\Language[] $languages the Language instances to convert * * @return string */ protected static function toStringDo($languages) { throw new Exception(get_called_class() . ' does not implement the method ' . __FUNCTION__); } } id . '\' => {'; $lines[] = ' \'name\' => \'' . addslashes($lc->name) . '\','; if (isset($lc->supersededBy)) { $lines[] = ' \'supersededBy\' => \'' . $lc->supersededBy . '\','; } if (isset($lc->script)) { $lines[] = ' \'script\' => \'' . addslashes($lc->script) . '\','; } if (isset($lc->territory)) { $lines[] = ' \'territory\' => \'' . addslashes($lc->territory) . '\','; } if (isset($lc->baseLanguage)) { $lines[] = ' \'baseLanguage\' => \'' . addslashes($lc->baseLanguage) . '\','; } $lines[] = ' \'formula\' => \'' . $lc->formula . '\','; $lines[] = ' \'plurals\' => ' . count($lc->categories) . ','; $catNames = array(); foreach ($lc->categories as $c) { $catNames[] = "'{$c->id}'"; } $lines[] = ' \'cases\' => [' . implode(', ', $catNames) . '],'; $lines[] = ' \'examples\' => {'; foreach ($lc->categories as $c) { $lines[] = ' \'' . $c->id . '\' => \'' . $c->examples . '\','; } $lines[] = ' },'; $lines[] = ' },'; } $lines[] = '}'; $lines[] = ''; return implode("\n", $lines); } } '; $lines[] = $prefix . '
'; $lines[] = $prefix . ' '; $lines[] = $prefix . ' '; $lines[] = $prefix . ' '; $lines[] = $prefix . ' '; $lines[] = $prefix . ' '; $lines[] = $prefix . ' '; $lines[] = $prefix . ' '; $lines[] = $prefix . ' '; $lines[] = $prefix . ' '; foreach ($languages as $lc) { $lines[] = $prefix . ' '; $lines[] = $prefix . ' '; $name = self::h($lc->name); if (isset($lc->supersededBy)) { $name .= '
Superseded by ' . $lc->supersededBy . ''; } $lines[] = $prefix . ' '; $lines[] = $prefix . ' '; $lines[] = $prefix . ' '; $cases = array(); foreach ($lc->categories as $c) { $cases[] = '
  • ' . $c->id . '' . self::h($c->examples) . '
  • '; } $lines[] = $prefix . ' '; $lines[] = $prefix . ' '; } $lines[] = $prefix . ' '; $lines[] = $prefix . '
    | *=> *)(.*?)(?:
    Language codeLanguage name# pluralsFormulaPlurals
    ' . $lc->id . '' . $name . '' . count($lc->categories) . '' . self::h($lc->formula) . '' . implode('', $cases) . '
    '; return implode("\n", $lines); } } id . '\n"'; $lines[] = '"Plural-Forms: nplurals=' . count($language->categories) . '; plural=' . $language->formula . '\n"'; $lines[] = ''; return implode("\n", $lines); } } * * For the full copyright and license information refer to the LICENSE file * distributed with this source code */ namespace Peast; /** * Nodes renderer class * * @author Marco Marchiò */ class Renderer { /** * Formatter to use for the rendering * * @var Formatter\Base */ protected $formatter; /** * Rendering options taken from the formatter * * @var object */ protected $renderOpts; /** * Node types that does not require semicolon insertion * * @var array */ protected $noSemicolon = array( "ClassDeclaration", "ExportDefaultDeclaration", "ForInStatement", "ForOfStatement", "ForStatement", "FunctionDeclaration", "IfStatement", "LabeledStatement", "StaticBlock", "SwitchStatement", "TryStatement", "WhileStatement", "WithStatement", "MethodDefinition" ); /** * Sets the formatter to use for the rendering * * @param Formatter\Base $formatter Formatter * * @return $this */ public function setFormatter(Formatter\Base $formatter) { $this->formatter = $formatter; $this->renderOpts = (object) array( "nl" => $this->formatter->getNewLine(), "ind" => $this->formatter->getIndentation(), "nlbc" => $this->formatter->getNewLineBeforeCurlyBracket(), "sao" => $this->formatter->getSpacesAroundOperator() ? " " : "", "sirb" => $this->formatter->getSpacesInsideRoundBrackets() ? " " : "", "awb" => $this->formatter->getAlwaysWrapBlocks() ); return $this; } /** * Returns the formatter to use for the rendering * * @return Formatter\Base */ public function getFormatter() { return $this->formatter; } /** * Renders the given node * * @param Syntax\Node\Node $node Node to render * * @return string * * @throws \Exception */ public function render(Syntax\Node\Node $node) { //Throw exception if no formatter has been specified if (!$this->formatter) { throw new \Exception("Formatter not set"); } //Reset indentation level $this->renderOpts->indLevel = 0; //Start rendering return $this->renderNode($node); } /** * Renders a node * * @param Syntax\Node\Node $node Node to render * * @return string */ protected function renderNode(Syntax\Node\Node $node) { $code = ""; $type = $node->getType(); switch ($type) { case "ArrayExpression": case "ArrayPattern": $code .= "[" . $this->joinNodes( $node->getElements(), "," . $this->renderOpts->sao ) . "]"; break; case "ArrowFunctionExpression": if ($node->getAsync()) { $code .= "async" . $this->renderOpts->sao; } $code .= "(" . $this->renderOpts->sirb . $this->joinNodes( $node->getParams(), "," . $this->renderOpts->sao ) . $this->renderOpts->sirb . ")" . $this->renderOpts->sao . "=>"; $body = $node->getBody(); if ($body->getType() !== "BlockStatement") { $code .= $this->renderOpts->sao . $this->renderNode($body); } else { $code .= $this->renderStatementBlock($body, true); } break; case "AwaitExpression": $code .= "await " . $this->renderNode($node->getArgument()); break; case "AssignmentExpression": case "AssignmentPattern": case "BinaryExpression": case "LogicalExpression": $operator = $type === "AssignmentPattern" ? "=" : $node->getOperator(); $code .= $this->renderNode($node->getLeft()); $codeRight = $this->renderNode($node->getRight()); if (preg_match("#^[a-z]+$#i", $operator)) { $code .= " " . $operator . " "; } else { //If there's no space around the operator, additional checks must //be performed to prevent errors when rendering unary and update //expressions inside binary expressions $checkSpace = !$this->renderOpts->sao && $type === "BinaryExpression"; //The space is mandatory if the left part ends with the same //character used as operator if ($checkSpace && $code && substr($code, -1) === $operator) { $code .= " "; } $code .= $this->renderOpts->sao . $operator . $this->renderOpts->sao; //The space is mandatory if the right part begins with the same //character used as operator if ($checkSpace && $codeRight && $codeRight[0] === $operator) { $code .= " "; } } $code .= $codeRight; break; case "BlockStatement": case "ClassBody": case "Program": $code .= $this->renderStatementBlock( $node->getBody(), false, false, true, false ); break; case "BreakStatement": case "ContinueStatement": $code .= $type === "BreakStatement" ? "break" : "continue"; if ($label = $node->getLabel()) { $code .= " " . $this->renderNode($label); } break; case "CallExpression": case "NewExpression": if ($type === "NewExpression") { $code .= "new "; $optional = false; } else { $optional = $node->getOptional(); } $code .= $this->renderNode($node->getCallee()) . ($optional ? "?." : "") . "(" . $this->renderOpts->sirb . $this->joinNodes( $node->getArguments(), "," . $this->renderOpts->sao ) . $this->renderOpts->sirb . ")"; break; case "CatchClause": $code .= "catch"; if ($params = $node->getParam()) { $code .= $this->renderOpts->sao . "(" . $this->renderOpts->sirb . $this->renderNode($params) . $this->renderOpts->sirb . ")"; } $code .= $this->renderStatementBlock($node->getBody(), true); break; case "ChainExpression": case "ExpressionStatement": $code .= $this->renderNode($node->getExpression()); break; case "ClassExpression": case "ClassDeclaration": $code .= "class"; if ($id = $node->getId()) { $code .= " " . $this->renderNode($id); } if ($superClass = $node->getSuperClass()) { $code .= " extends " . $this->renderNode($superClass); } $code .= $this->renderStatementBlock( $node->getBody(), true ); break; case "ConditionalExpression": $code .= $this->renderNode($node->getTest()) . $this->renderOpts->sao . "?" . $this->renderOpts->sao . $this->renderNode($node->getConsequent()) . $this->renderOpts->sao . ":" . $this->renderOpts->sao . $this->renderNode($node->getAlternate()); break; case "DebuggerStatement": $code .= "debugger"; break; case "DoWhileStatement": $code .= "do" . $this->renderStatementBlock( $node->getBody(), null, true ) . $this->renderOpts->sao . "while" . $this->renderOpts->sao . "(" . $this->renderOpts->sirb . $this->renderNode($node->getTest()) . $this->renderOpts->sirb . ")"; break; case "JSXEmptyExpression": case "EmptyStatement": break; case "ExportAllDeclaration": $code .= "export *"; $exported = $node->getExported(); if ($exported) { $code .= " as " . $this->renderNode($exported); } $code .= " from " . $this->renderNode($node->getSource()); break; case "ExportDefaultDeclaration": $declaration = $node->getDeclaration(); $code .= "export default " . $this->renderNode($declaration); if ($this->requiresSemicolon($declaration)) { $code .= ";"; } break; case "ExportNamedDeclaration": $code .= "export"; if ($dec = $node->getDeclaration()) { $code .= " " . $this->renderNode($dec); } else { $code .= $this->renderOpts->sao . "{" . $this->joinNodes( $node->getSpecifiers(), "," . $this->renderOpts->sao ) . "}"; if ($source = $node->getSource()) { $code .= $this->renderOpts->sao . "from " . $this->renderNode($source); } } break; case "ExportSpecifier": $local = $this->renderNode($node->getLocal()); $ref = $this->renderNode($node->getExported()); $code .= $local === $ref ? $local : $local . " as " . $ref; break; case "ForInStatement": case "ForOfStatement": //Force single line mode for substatements $this->renderOpts->forceSingleLine = true; $code .= "for" . ($type === "ForOfStatement" && $node->getAwait() ? " await" : "") . $this->renderOpts->sao . "(" . $this->renderOpts->sirb . $this->renderNode($node->getLeft()) . " " . ($type === "ForInStatement" ? "in" : "of") . " " . $this->renderNode($node->getRight()) . $this->renderOpts->sirb . ")" . $this->renderStatementBlock($node->getBody()); unset($this->renderOpts->forceSingleLine); break; case "ForStatement": //Force single line mode for substatements $this->renderOpts->forceSingleLine = true; $code .= "for" . $this->renderOpts->sao . "(" . $this->renderOpts->sirb; if ($init = $node->getInit()) { $code .= $this->renderNode($init); } $code .= ";" . $this->renderOpts->sao; if ($test = $node->getTest()) { $code .= $this->renderNode($test); } $code .= ";" . $this->renderOpts->sao; if ($update = $node->getUpdate()) { $code .= $this->renderNode($update); } $code .= $this->renderOpts->sirb . ")" . $this->renderStatementBlock($node->getBody()); unset($this->renderOpts->forceSingleLine); break; case "FunctionDeclaration": case "FunctionExpression": $id = $node->getId(); if ($node->getAsync()) { $code .= "async "; } $code .= "function"; if ($node->getGenerator()) { $code .= $this->renderOpts->sao . "*"; } elseif ($id) { $code .= " "; } if ($id) { if ($node->getGenerator()) { $code .= $this->renderOpts->sao; } $code .= $this->renderNode($id); } $code .= $this->renderOpts->sao . "(" . $this->renderOpts->sirb . $this->joinNodes( $node->getParams(), "," . $this->renderOpts->sao ) . $this->renderOpts->sirb . ")" . $this->renderStatementBlock($node->getBody(), true); break; case "ImportExpression": $code .= "import(" . $this->renderOpts->sirb . $this->renderNode($node->getSource()) . $this->renderOpts->sirb . ")"; break; case "JSXIdentifier": case "Identifier": $code .= $node->getRawName(); break; case "IfStatement": $code .= "if" . $this->renderOpts->sao . "(" . $this->renderOpts->sirb . $this->renderNode($node->getTest()) . $this->renderOpts->sirb . ")"; $consequent = $node->getConsequent(); $alternate = $node->getAlternate(); $forceBracketsConsequent = $forceBracketsAlternate = null; if (!$this->renderOpts->awb && $alternate) { $forceBracketsConsequent = $this->checkIfPartsBracketsRequirement( $consequent ); $forceBracketsAlternate = $this->checkIfPartsBracketsRequirement( $alternate ); } $code .= $this->renderStatementBlock( $consequent, $forceBracketsConsequent ); if ($alternate) { $code .= $this->renderOpts->sao . "else" . $this->renderStatementBlock( $alternate, $forceBracketsAlternate, true ); } break; case "ImportDeclaration": $code .= "import "; $specifiers = $node->getSpecifiers(); if (count($specifiers)) { $sep = "," . $this->renderOpts->sao; $groups = $parts = array(); foreach ($specifiers as $spec) { $specType = $spec->getType(); if (!isset($groups[$specType])) { $groups[$specType] = array(); } $groups[$specType][] = $spec; } if (isset($groups["ImportDefaultSpecifier"])) { foreach ($groups["ImportDefaultSpecifier"] as $s) { $parts[] = $this->renderNode($s); } } if (isset($groups["ImportNamespaceSpecifier"])) { foreach ($groups["ImportNamespaceSpecifier"] as $s) { $parts[] = $this->renderNode($s); } } if (isset($groups["ImportSpecifier"])) { $impSpec = array(); foreach ($groups["ImportSpecifier"] as $s) { $impSpec[] = $this->renderNode($s); } $parts[] = "{" . implode($sep, $impSpec) . "}"; } $code .= implode($sep, $parts) . " from "; } $code .= $this->renderNode($node->getSource()); break; case "ImportDefaultSpecifier": $code .= $this->renderNode($node->getLocal()); break; case "ImportNamespaceSpecifier": $code .= "* as " . $this->renderNode($node->getLocal()); break; case "ImportSpecifier": $local = $this->renderNode($node->getLocal()); $ref = $this->renderNode($node->getImported()); $code .= $local === $ref ? $local : $ref . " as " . $local; break; case "JSXAttribute": $code .= $this->renderNode($node->getName()); if ($value = $node->getValue()) { $code .= "=" . $this->renderNode($value); } break; case "JSXClosingElement": $code .= "renderNode($node->getName()) . ">"; break; case "JSXClosingFragment": $code .= ""; break; case "JSXElement": $code .= $this->renderNode($node->getOpeningElement()) . $this->joinNodes($node->getChildren(), ""); if ($closing = $node->getClosingElement()) { $code .= $this->renderNode($closing); } break; case "JSXExpressionContainer": $code .= "{" . $this->renderNode($node->getExpression()) . "}"; break; case "JSXFragment": $code .= $this->renderNode($node->getOpeningFragment()) . $this->joinNodes($node->getChildren(), "") . $this->renderNode($node->getClosingFragment()); break; case "JSXNamespacedName": $code .= $this->renderNode($node->getNamespace()) . ":" . $this->renderNode($node->getName()); break; case "JSXOpeningElement": $code .= "<" . $this->renderNode($node->getName()); $attributes = $node->getAttributes(); if (count($attributes)) { $code .= " " . $this->joinNodes($attributes, " "); } if ($node->getSelfClosing()) { $code .= "/"; } $code .= ">"; break; case "JSXOpeningFragment": $code .= "<>"; break; case "JSXSpreadAttribute": $code .= "{..." . $this->renderNode($node->getArgument()) . "}"; break; case "JSXSpreadChild": $code .= "{..." . $this->renderNode($node->getExpression()) . "}"; break; case "LabeledStatement": $body = $node->getBody(); $code .= $this->renderNode($node->getLabel()) . ":"; if ($body->getType() === "BlockStatement") { $code .= $this->renderStatementBlock($body, true); } else { $code .= $this->renderOpts->nl . $this->getIndentation() . $this->renderNode($body); } if ($this->requiresSemicolon($body)) { $code .= ";"; } break; case "JSXText": case "Literal": case "RegExpLiteral": $code .= $node->getRaw(); break; case "JSXMemberExpression": case "MemberExpression": $property = $node->getProperty(); $compiledProperty = $this->renderNode($property); $code .= $this->renderNode($node->getObject()); $optional = false; if ($type === "MemberExpression") { $optional = $node->getOptional(); } $propertyType = $property->getType(); if ($type === "MemberExpression" && ($node->getComputed() || ($propertyType !== "Identifier" && $propertyType !== "PrivateIdentifier"))) { $code .= ($optional ? "?." : "") . "[" . $compiledProperty . "]"; } else { $code .= ($optional ? "?." : ".") . $compiledProperty; } break; case "MetaProperty": $code .= $node->getMeta() . "." . $node->getProperty(); break; case "MethodDefinition": if ($node->getStatic()) { $code .= "static "; } $value = $node->getValue(); $key = $node->getKey(); $kind = $node->getKind(); if ($kind === $node::KIND_GET || $kind === $node::KIND_SET) { $code .= $kind . " "; } else { if ($value->getAsync()) { $code .= "async "; } if ($value->getGenerator()) { $code .= "*" . $this->renderOpts->sao; } } if ($node->getComputed()) { $code .= "[" . $this->renderNode($key) . "]"; } else { $code .= $this->renderNode($key); } $code .= $this->renderOpts->sao . preg_replace("/^[^(]+/", "", $this->renderNode($value)); break; case "ObjectExpression": $currentIndentation = $this->getIndentation(); $this->renderOpts->indLevel++; $indentation = $this->getIndentation(); //Handle single line mode if (isset($this->renderOpts->forceSingleLine)) { $start = $end = ""; $separator = "," . $this->renderOpts->sao; } else { $end = $this->renderOpts->nl . $currentIndentation; $start = $this->renderOpts->nl . $indentation; $separator = "," . $this->renderOpts->nl . $indentation; } $code .= "{"; $properties = $node->getProperties(); if (count($properties)) { $code .= $start . $this->joinNodes( $properties, $separator ) . $end; } $code .= "}"; $this->renderOpts->indLevel--; break; case "ObjectPattern": $code .= "{" . $this->joinNodes( $node->getProperties(), "," . $this->renderOpts->sao ) . "}"; break; case "ParenthesizedExpression": $code .= "(" . $this->renderOpts->sirb . $this->renderNode($node->getExpression()) . $this->renderOpts->sirb . ")"; break; case "PrivateIdentifier": $code .= "#" . $node->getName(); break; case "Property": $value = $node->getValue(); $key = $node->getKey(); $compiledKey = $this->renderNode($key); $compiledValue = $this->renderNode($value); $keyType = $key->getType(); $valueType = $value->getType(); if ($valueType === "AssignmentPattern" && $compiledKey === $this->renderNode($value->getLeft())) { $code .= $compiledValue; } else { $kind = $node->getKind(); $getterSetter = $kind === $node::KIND_GET || $kind === $node::KIND_SET; if ($getterSetter) { $code .= $kind . " "; } elseif ($value->getType() === "FunctionExpression" && $value->getGenerator()) { $code .= "*" . $this->renderOpts->sao; } if ($node->getComputed()) { $code .= "[" . $compiledKey . "]"; } else { $code .= $compiledKey; } if ($node->getMethod() || $getterSetter) { $code .= $this->renderOpts->sao . preg_replace("/^[^(]+/", "", $compiledValue); } elseif ($keyType !== "Identifier" || $valueType !== "Identifier" || $compiledKey !== $compiledValue ) { $code .= ($node->getShorthand() ? "=" : ":") . $this->renderOpts->sao . $compiledValue; } } break; case "PropertyDefinition": if ($node->getStatic()) { $code .= "static "; } $compiledKey = $this->renderNode($node->getKey()); if ($node->getComputed()) { $code .= "[" . $compiledKey . "]"; } else { $code .= $compiledKey; } if ($value = $node->getValue()) { $code .= $this->renderOpts->sao . "=" . $this->renderOpts->sao . $this->renderNode($value); } break; case "RestElement": case "SpreadElement": $code .= "..." . $this->renderNode($node->getArgument()); break; case "ReturnStatement": $code .= "return"; if ($argument = $node->getArgument()) { $code .= " " . $this->renderNode($argument); } break; case "SequenceExpression": $code .= $this->joinNodes( $node->getExpressions(), "," . $this->renderOpts->sao ); break; case "StaticBlock": $code .= "static"; $code .= $this->renderStatementBlock($node->getBody(), true); break; case "Super": $code .= "super"; break; case "SwitchCase": if ($test = $node->getTest()) { $code .= "case " . $this->renderNode($test); } else { $code .= "default"; } $code .= ":"; if (count($node->getConsequent())) { $code .= $this->renderOpts->nl . $this->renderStatementBlock( $node->getConsequent(), false ); } break; case "SwitchStatement": $code .= "switch" . $this->renderOpts->sao . "(" . $this->renderOpts->sirb . $this->renderNode($node->getDiscriminant()) . $this->renderOpts->sirb . ")" . $this->renderStatementBlock( $node->getCases(), true, false, false ); break; case "TaggedTemplateExpression": $code .= $this->renderNode($node->getTag()) . $this->renderNode($node->getQuasi()); break; case "TemplateElement": $code .= $node->getRawValue(); break; case "TemplateLiteral": $code .= "`"; foreach ($node->getParts() as $part) { if ($part->getType() === "TemplateElement") { $code .= $this->renderNode($part); } else { $code .= "$" . "{" . $this->renderNode($part) . "}"; } } $code .= "`"; break; case "ThisExpression": $code .= "this"; break; case "ThrowStatement": $code .= "throw " . $this->renderNode($node->getArgument()); break; case "TryStatement": $code .= "try" . $this->renderStatementBlock($node->getBlock(), true); if ($handler = $node->getHandler()) { $code .= $this->renderOpts->sao . $this->renderNode($handler); } if ($finalizer = $node->getFinalizer()) { $code .= $this->renderOpts->sao . "finally" . $this->renderStatementBlock($finalizer, true); } break; case "UnaryExpression": case "UpdateExpression": $prefix = $node->getPrefix(); if ($prefix) { $code .= $node->getOperator(); if (preg_match("#^[a-z]+$#i", $node->getOperator())) { $code .= " "; } } $code .= $this->renderNode($node->getArgument()); if (!$prefix) { $code .= $node->getOperator(); } break; case "VariableDeclaration": $this->renderOpts->indLevel++; $indentation = $this->getIndentation(); //Handle single line mode if (isset($this->renderOpts->forceSingleLine)) { $separator = "," . $this->renderOpts->sao; } else { $separator = "," . $this->renderOpts->nl . $indentation; } $code .= $node->getKind() . " " . $this->joinNodes( $node->getDeclarations(), $separator ); $this->renderOpts->indLevel--; break; case "VariableDeclarator": $code .= $this->renderNode($node->getId()); if ($init = $node->getInit()) { $code .= $this->renderOpts->sao . "=" . $this->renderOpts->sao . $this->renderNode($init); } break; case "WhileStatement": $code .= "while" . $this->renderOpts->sao . "(" . $this->renderOpts->sirb . $this->renderNode($node->getTest()) . $this->renderOpts->sirb . ")" . $this->renderStatementBlock($node->getBody()); break; case "WithStatement": $code .= "with" . $this->renderOpts->sao . "(" . $this->renderOpts->sirb . $this->renderNode($node->getObject()) . $this->renderOpts->sirb . ")" . $this->renderStatementBlock($node->getBody()); break; case "YieldExpression": $code .= "yield"; if ($node->getDelegate()) { $code .= " *"; } if ($argument = $node->getArgument()) { $code .= " " . $this->renderNode($argument); } break; } return $code; } /** * Renders a node as a block statement * * @param Syntax\Node\Node|array $node Node or array of * nodes to render * @param bool $forceBrackets Overrides brackets * inserting rules * @param bool $mandatorySeparator True if a starting * separator is * mandatory * @param bool $addSemicolons Semicolons are * inserted automatically * if this parameter is * not false * @param bool $incIndent If false indentation * level won't be * incremented * * @return string */ protected function renderStatementBlock( $node, $forceBrackets = null, $mandatorySeparator = false, $addSemicolons = true, $incIndent = true ) { $code = ""; //Special handling of BlockStatement and ClassBody nodes by rendering //their child nodes if (!is_array($node) && in_array($node->getType(), array("BlockStatement", "ClassBody"))) { $node = $node->getBody(); } //If $forceBrackets is not null use its value to override curly brackets //insertion rules if ($forceBrackets !== null) { $hasBrackets = $forceBrackets; } else { //Insert curly brackets if required by the formatter or if the //number of nodes to render is different from one $hasBrackets = $this->renderOpts->awb || (is_array($node) && count($node) !== 1); } $currentIndentation = $this->getIndentation(); //If $forceBrackets is not set to false then the node can be wrapped in //curly braces, so a separator defined by formatter must be inserted if ($forceBrackets !== false) { if ($this->renderOpts->nlbc) { $code .= $this->renderOpts->nl . $currentIndentation; } else { $code .= $this->renderOpts->sao; } } //Insert open curly bracket if required if ($hasBrackets) { $code .= "{" . $this->renderOpts->nl; } elseif ($mandatorySeparator) { //If bracket is not inserted but a separator is still required //a space is added $code .= " "; } //Increase indentation level if ($incIndent) { $this->renderOpts->indLevel++; } $subIndentation = $this->getIndentation(); //Render the node or the array of nodes $emptyBody = is_array($node) && !count($node); if (is_array($node)) { if (!$emptyBody) { $code .= $subIndentation . $this->joinNodes( $node, $this->renderOpts->nl . $subIndentation, $addSemicolons ); } } else { $code .= $subIndentation . $this->renderNode($node); if ($addSemicolons && $this->requiresSemicolon($node)) { $code .= ";"; } } //Reset the indentation level if ($incIndent) { $this->renderOpts->indLevel--; } //Insert closing curly bracket if required if ($hasBrackets) { //Add a new line if something was rendered if (!$emptyBody) { $code .= $this->renderOpts->nl; } $code .= $currentIndentation . "}"; } return $code; } /** * Joins an array of nodes with the given separator * * @param array $nodes Nodes * @param string $separator Separator * @param bool $addSemicolons True to add semicolons after each node * * @return string */ protected function joinNodes($nodes, $separator, $addSemicolons=false) { $parts = array(); foreach ($nodes as $node) { if (!$node) { $code = ""; } else { $code = $this->renderNode($node); if ($addSemicolons && $this->requiresSemicolon($node)) { $code .= ";"; } } $parts[] = $code; } return implode($separator, $parts); } /** * Checks if consequent and alternate nodes of an IfStatement require the * mandatory insertion of brackets around them to avoid the pattern where * the "else" code is rendered as part of an inner IfStatement. * * @param Syntax\Node\Node $node Node * * @return null|bool */ protected function checkIfPartsBracketsRequirement($node) { if ($node->getType() === "BlockStatement" && count($node->getBody()) > 1) { return null; } $forceBrackets = null; $optBracketNodes = array( "DoWhileStatement", "ForInStatement", "ForOfStatement", "ForStatement", "WhileStatement", "WithStatement" ); $checkFn = function ($n) use ($optBracketNodes, &$forceBrackets) { $type = $n->getType(); if ($type === "IfStatement") { if (!$n->getAlternate()) { $forceBrackets = true; } return Traverser::DONT_TRAVERSE_CHILD_NODES; } elseif ($type === "BlockStatement") { if (count($n->getBody()) !== 1) { return Traverser::DONT_TRAVERSE_CHILD_NODES; } } elseif ($type === "LabeledStatement") { if ($n->getBody()->getType() === "BlockStatement") { $forceBrackets = true; } return Traverser::DONT_TRAVERSE_CHILD_NODES; } elseif (!in_array($type, $optBracketNodes)) { return Traverser::DONT_TRAVERSE_CHILD_NODES; } }; $node->traverse($checkFn); return $forceBrackets; } /** * Check if the given node requires semicolons insertion * * @param Syntax\Node\Node $node Node * * @return bool */ protected function requiresSemicolon($node) { return !in_array($node->getType(), $this->noSemicolon); } /** * Returns the current indentation string * * @return string */ protected function getIndentation() { return str_repeat( $this->renderOpts->ind, $this->renderOpts->indLevel ); } } * * For the full copyright and license information refer to the LICENSE file * distributed with this source code */ namespace Peast; /** * Main class of Peast library. * Every function of this class takes two arguments: * - The source code to parse * - The options array that is an associative array of parser settings. * Available options are: * - "sourceType": one of the source type constants declared in this class. * This option tells the parser to parse the source in script or module * mode. If this option is not provided the parser will work in script * mode. * - "sourceEncoding": the encoding of the source. If not specified the * parser will assume UTF-8. * - "strictEncoding": if false the parser will handle invalid UTF8 * characters in the source code by replacing them with the character * defined in the "mbstring.substitute_character" ini setting, otherwise * it will throw an exception. * - "comments": if true it enables comments parsing. * - "jsx": if true it enables parsing of JSX syntax. * * @method static Syntax\Parser ES2015(string $source, array $options = array()) * Returns a parser instance with ES2015 features for the given source. See Peast * class documentation to understand the function arguments. * * @method static Syntax\Parser ES6(string $source, array $options = array()) * Returns a parser instance with ES2015 features for the given source. See Peast * class documentation to understand function arguments. * * @method static Syntax\Parser ES2016(string $source, array $options = array()) * Returns a parser instance with ES2016 features for the given source. See Peast * class documentation to understand function arguments. * * @method static Syntax\Parser ES7(string $source, array $options = array()) * Returns a parser instance with ES2016 features for the given source. See Peast * class documentation to understand function arguments. * * @method static Syntax\Parser ES2017(string $source, array $options = array()) * Returns a parser instance with ES2017 features for the given source. See Peast * class documentation to understand function arguments. * * @method static Syntax\Parser ES8(string $source, array $options = array()) * Returns a parser instance with ES2017 features for the given source. See Peast * class documentation to understand function arguments. * * @method static Syntax\Parser ES2018(string $source, array $options = array()) * Returns a parser instance with ES2018 features for the given source. See Peast * class documentation to understand function arguments. * * @method static Syntax\Parser ES9(string $source, array $options = array()) * Returns a parser instance with ES2018 features for the given source. See Peast * class documentation to understand function arguments. * * @method static Syntax\Parser ES2019(string $source, array $options = array()) * Returns a parser instance with ES2019 features for the given source. See Peast * class documentation to understand function arguments. * * @method static Syntax\Parser ES10(string $source, array $options = array()) * Returns a parser instance with ES2019 features for the given source. See Peast * class documentation to understand function arguments. * * @method static Syntax\Parser ES2020(string $source, array $options = array()) * Returns a parser instance with ES2020 features for the given source. See Peast * class documentation to understand function arguments. * * @method static Syntax\Parser ES11(string $source, array $options = array()) * Returns a parser instance with ES2020 features for the given source. See Peast * class documentation to understand function arguments. * * @method static Syntax\Parser ES2021(string $source, array $options = array()) * Returns a parser instance with ES2021 features for the given source. See Peast * class documentation to understand function arguments. * * @method static Syntax\Parser ES12(string $source, array $options = array()) * Returns a parser instance with ES2021 features for the given source. See Peast * class documentation to understand function arguments. * * @method static Syntax\Parser ES2022(string $source, array $options = array()) * Returns a parser instance with ES2022 features for the given source. See Peast * class documentation to understand function arguments. * * @method static Syntax\Parser ES13(string $source, array $options = array()) * Returns a parser instance with ES2022 features for the given source. See Peast * class documentation to understand function arguments. * * @method static Syntax\Parser latest(string $source, array $options = array()) * Returns an instance of the latest parser version for the given source. See * Peast class documentation to understand function arguments. * * @author Marco Marchiò */ class Peast { //Source type constants /** * This source type indicates that the source is a script and import * and export keywords are not parsed. */ const SOURCE_TYPE_SCRIPT = "script"; /** * This source type indicates that the source is a module, this enables * the parsing of import and export keywords. */ const SOURCE_TYPE_MODULE = "module"; /** * Valid versions and aliases * * @var array */ static protected $versions = array( "ES6" => "ES2015", "ES7" => "ES2016", "ES8" => "ES2017", "ES9" => "ES2018", "ES10" => "ES2019", "ES11" => "ES2020", "ES12" => "ES2021", "ES13" => "ES2022" ); /** * Magic method that exposes all the functions to access parser with * specific features * * @param string $version Parser version * @param array $args Parser arguments * * @return Syntax\Parser * * @throws \Exception */ public static function __callStatic($version, $args) { $source = $args[0]; $options = isset($args[1]) ? $args[1] : array(); if (!in_array($version, self::$versions)) { if ($version === "latest") { $version = end(self::$versions); } elseif (isset(self::$versions[$version])) { $version = self::$versions[$version]; } else { throw new \Exception("Invalid version $version"); } } $featuresClass = "\\Peast\\Syntax\\$version\\Features"; return new Syntax\Parser( $source, new $featuresClass, $options ); } } * * For the full copyright and license information refer to the LICENSE file * distributed with this source code */ namespace Peast; /** * Nodes query class * * @author Marco Marchiò */ class Query implements \IteratorAggregate, \Countable { /** * Current matches * * @var Selector\Matches */ protected $matches; /** * Options array * * @var array */ protected $options; /** * Class constructor. Available options are: * - encoding: selectors encoding. If not specified the * parser will assume UTF-8. * * @param Syntax\Node\Program $root Root node * @param array $options Options array */ public function __construct(Syntax\Node\Program $root, $options = array()) { $this->matches = new Selector\Matches(); $this->matches->addMatch($root); $this->options = $options; } /** * Finds nodes matching the given selector starting from the * current matched nodes, if any, or from the root * * @param string $selector Selector * * @return $this * * @throws Selector\Exception */ public function find($selector) { $parser = new Selector\Parser($selector, $this->options); $selector = $parser->parse(); $this->matches = $selector->exec($this->matches); return $this; } /** * Executes the given selector on the current nodes and filters * out the nodes which don't match * * @param string $selector Selector * * @return $this * * @throws Selector\Exception */ public function filter($selector) { $parser = new Selector\Parser($selector, $this->options); $selector = $parser->parse(true); $this->matches->filter(function ($node, $parent) use ($selector) { $newMatch = new Selector\Matches(); $newMatch->addMatch($node, $parent); return $selector->exec($newMatch)->getMatches(); }); return $this; } /** * Returns the number of matched nodes * * @return int */ #[\ReturnTypeWillChange] public function count() { return $this->matches->count(); } /** * Returns the node at the given index * * @param int $index Index * * @return array * * @throws \Exception */ public function get($index) { return $this->matches->get($index)[0]; } /** * Returns the nodes iterator * * @return \ArrayIterator */ #[\ReturnTypeWillChange] public function getIterator() { return new \ArrayIterator($this->matches->getNodes()); } } * * For the full copyright and license information refer to the LICENSE file * distributed with this source code */ namespace Peast\Selector\Node\Part; use Peast\Syntax\Node\Node; /** * Selector part base class * * @author Marco Marchiò * * @abstract */ abstract class Part { /** * Priority * * @var int */ protected $priority = 5; public function getPriority() { return $this->priority; } /** * Returns true if the selector part matches the given node, * false otherwise * * @param Node $node Node * @param Node $parent Parent node * * @return bool * * @abstract */ abstract public function check(Node $node, Node $parent = null); } * * For the full copyright and license information refer to the LICENSE file * distributed with this source code */ namespace Peast\Selector\Node\Part; use Peast\Syntax\Node\Node; use Peast\Syntax\Utils; /** * Selector part index pseudo class * * @author Marco Marchiò */ class PseudoIndex extends Pseudo { /** * Priority * * @var int */ protected $priority = 2; /** * Step * * @var int */ protected $step = 0; /** * Offset * * @var int */ protected $offset = 0; /** * Sets the step * * @param int $step Step * * @return $this */ public function setStep($step) { $this->step = $step; return $this; } /** * Sets the offset * * @param int $offset Offset * * @return $this */ public function setOffset($offset) { $this->offset = $offset; return $this; } /** * Returns true if the selector part matches the given node, * false otherwise * * @param Node $node Node * @param Node $parent Parent node * * @return bool */ public function check(Node $node, Node $parent = null) { $props = Utils::getExpandedNodeProperties($parent); $count = count($props); $reverse = $this->name === "nth-last-child"; if ($reverse) { $start = $count - 1 - ($this->offset - 1); $step = $this->step * -1; if ($step > 0) { $reverse = false; } } else { $start = $this->offset - 1; $step = $this->step; if ($step < 0) { $reverse = true; } } //Step 0 will cause an infinite loop, so it must be set to the //number of props so that it will execute only one iteration if (!$step) { $step = $reverse ? -$count : $count; } for ($i = $start; ($reverse && $i >= 0) || (!$reverse && $i < $count); $i += $step) { if (isset($props[$i]) && $props[$i] === $node) { return true; } } return false; } } * * For the full copyright and license information refer to the LICENSE file * distributed with this source code */ namespace Peast\Selector\Node\Part; use Peast\Syntax\Node\Node; use Peast\Syntax\Node\Pattern; use Peast\Syntax\Node\Statement; use Peast\Syntax\Node\Expression; use Peast\Syntax\Node\Declaration; use Peast\Syntax\Utils; /** * Selector part simple pseudo class * * @author Marco Marchiò */ class PseudoSimple extends Pseudo { /** * Priority * * @var int */ protected $priority = 3; /** * Returns true if the selector part matches the given node, * false otherwise * * @param Node $node Node * @param Node $parent Parent node * * @return bool */ public function check(Node $node, Node $parent = null) { switch ($this->name) { case "pattern": return $node instanceof Pattern; case "statement": return $node instanceof Statement; case "expression": return $node instanceof Expression; case "declaration": return $node instanceof Declaration; case "last-child": case "first-child": $first = $this->name === "first-child"; $props = Utils::getExpandedNodeProperties($parent); return count($props) > 0 && ( $first ? $props[0] === $node : array_pop($props) === $node ); } } } * * For the full copyright and license information refer to the LICENSE file * distributed with this source code */ namespace Peast\Selector\Node\Part; use Peast\Selector\Matches; use Peast\Selector\Node\Selector; use Peast\Syntax\Node\Node; /** * Selector part selector pseudo class * * @author Marco Marchiò */ class PseudoSelector extends Pseudo { /** * Priority * * @var int */ protected $priority = 1; /** * Selector * * @var Selector */ protected $selector; /** * Sets the selector * * @param Selector $selector Selector * * @return $this */ public function setSelector(Selector $selector) { $this->selector = $selector; return $this; } /** * Returns true if the selector part matches the given node, * false otherwise * * @param Node $node Node * @param Node $parent Parent node * * @return bool */ public function check(Node $node, Node $parent = null) { $match = new Matches(); $match->addMatch($node, $parent); $res = $this->selector->exec($match)->count(); return $this->name === "not" ? $res === 0 : $res !== 0; } } * * For the full copyright and license information refer to the LICENSE file * distributed with this source code */ namespace Peast\Selector\Node\Part; /** * Selector pseudo part base class * * @author Marco Marchiò * * @abstract */ abstract class Pseudo extends Part { /** * Selector name * * @var string */ protected $name; /** * Sets the name * * @param string $name Name * * @return $this */ public function setName($name) { $this->name = $name; return $this; } } * * For the full copyright and license information refer to the LICENSE file * distributed with this source code */ namespace Peast\Selector\Node\Part; use Peast\Syntax\Node\Node; use Peast\Syntax\Utils; /** * Selector part attribute class * * @author Marco Marchiò */ class Attribute extends Part { /** * Priority * * @var int */ protected $priority = 4; /** * Attribute names * * @var array */ protected $names = array(); /** * Attribute match operator * * @var array */ protected $operator = null; /** * Attribute value * * @var mixed */ protected $value = null; /** * Case insensitive flag * * @var bool */ protected $caseInsensitive = false; /** * Regex flag * * @var bool */ protected $regex = false; /** * Adds a name * * @param string $name Name * * @return $this */ public function addName($name) { $this->names[] = $name; return $this; } /** * Sets the operator * * @param string $operator Operator * * @return $this */ public function setOperator($operator) { $this->operator = $operator; return $this; } /** * Sets the value * * @param mixed $value Value * * @return $this */ public function setValue($value) { $this->value = $value; return $this; } /** * Sets the case insensitive flag * * @param bool $caseInsensitive Case insensitive flag * * @return $this */ public function setCaseInsensitive($caseInsensitive) { $this->caseInsensitive = $caseInsensitive; return $this; } /** * Sets the regex flag * * @param bool $regex Regex flag * * @return $this */ public function setRegex($regex) { $this->regex = $regex; return $this; } /** * Returns true if the selector part matches the given node, * false otherwise * * @param Node $node Node * @param Node $parent Parent node * * @return bool */ public function check(Node $node, Node $parent = null) { $attr = $node; foreach ($this->names as $name) { $attrFound = false; if ($attr instanceof Node) { $props = Utils::getNodeProperties($attr); foreach ($props as $prop) { if ($prop["name"] === $name) { $attrFound = true; $attr = $attr->{$prop["getter"]}(); break; } } } if (!$attrFound) { return false; } } $bothStrings = is_string($attr) && is_string($this->value); switch ($this->operator) { case "=": if ($bothStrings) { if ($this->regex) { return preg_match($this->value, $attr); } return $this->compareStr( $this->value, $attr, $this->caseInsensitive, true, true ); } if (is_int($attr) && is_float($this->value)) { return (float) $attr === $this->value; } return $attr === $this->value; case "<": if (is_float($this->value) && !is_float($attr) && !is_int($attr) && !is_string($attr)) { return false; } return $attr < $this->value; case ">": if (is_float($this->value) && !is_float($attr) && !is_int($attr) && !is_string($attr)) { return false; } return $attr > $this->value; case "<=": if (is_float($this->value) && !is_float($attr) && !is_int($attr) && !is_string($attr)) { return false; } return $attr <= $this->value; case ">=": if (is_float($this->value) && !is_float($attr) && !is_int($attr) && !is_string($attr)) { return false; } return $attr >= $this->value; case "^=": case "$=": case "*=": return $this->compareStr( $this->value, $attr, $this->caseInsensitive, $this->operator === "^=", $this->operator === "$=" ); default: return true; } } /** * Compares two strings * * @param string $v1 Search value * @param string $v2 Compare value * @param bool $caseInsensitive True if the search must be case insensitive * @param bool $matchStart True if the search must be executed from the * beginning of the string * @param bool $matchEnd True if the search must be executed from the * end of the string * * @return bool */ protected function compareStr($v1, $v2, $caseInsensitive, $matchStart, $matchEnd) { $regex = "#" . ($matchStart ? "^" : "") . preg_quote($v1) . ($matchEnd ? "$" : "") . "#u" . ($caseInsensitive ? "i" : ""); return (bool) preg_match($regex, $v2); } } * * For the full copyright and license information refer to the LICENSE file * distributed with this source code */ namespace Peast\Selector\Node\Part; use Peast\Syntax\Node\Node; /** * Selector part type class * * @author Marco Marchiò */ class Type extends Part { /** * Selector type * * @var string */ protected $type; /** * Sets the selector type * * @param string $type Type * * @return $this */ public function setType($type) { $this->type = $type; return $this; } /** * Returns true if the selector part matches the given node, * false otherwise * * @param Node $node Node * @param Node $parent Parent node * * @return bool */ public function check(Node $node, Node $parent = null) { return $node->getType() === $this->type; } } * * For the full copyright and license information refer to the LICENSE file * distributed with this source code */ namespace Peast\Selector\Node; use Peast\Selector\Matches; /** * Selector group class * * @author Marco Marchiò */ class Group { /** * Selector combinators * * @var Combinator[] */ protected $combinators = array(); /** * Adds a combinator * * @param Combinator $combinators Combinator * * @return $this */ public function addCombinator(Combinator $combinators) { $this->combinators[] = $combinators; return $this; } /** * Executes the current group on the given matches * * @param Matches $matches Matches */ public function exec(Matches $matches) { foreach ($this->combinators as $combinator) { $combinator->exec($matches); } } } * * For the full copyright and license information refer to the LICENSE file * distributed with this source code */ namespace Peast\Selector\Node; use Peast\Selector\Matches; /** * Selector class * * @author Marco Marchiò */ class Selector { /** * Selector groups * * @var Group[] */ protected $groups = array(); /** * Adds a new group * * @param Group $group Group * * @return $this */ public function addGroup(Group $group) { $this->groups[] = $group; return $this; } /** * Executes the current selector on the given matches * * @param Matches $matches Matches * * @return Matches */ public function exec(Matches $matches) { $retMatches = array(); foreach ($this->groups as $group) { $clonedMatches = $matches->createClone(); $group->exec($clonedMatches); $retMatches[] = $clonedMatches; } if (count($retMatches) > 1) { $retMatches[0]->merge(array_slice($retMatches, 1)); } return $retMatches[0]; } } * * For the full copyright and license information refer to the LICENSE file * distributed with this source code */ namespace Peast\Selector\Node; use Peast\Selector\Matches; use Peast\Syntax\Utils; use Peast\Traverser; /** * Selector combinator class * * @author Marco Marchiò */ class Combinator { /** * Operator * * @var string|null */ protected $operator; /** * Selector parts * * @var Part\Part[] */ protected $parts = array(); /** * Sets the operator * * @param string $operator Operator * @return $this */ public function setOperator($operator) { $this->operator = $operator; return $this; } /** * Adds a new selector part * * @param Part\Part $part Part * @return $this */ public function addPart(Part\Part $part) { $this->parts[] = $part; return $this; } /** * Executes the current group on the given matches * * @param Matches $matches Matches */ public function exec(Matches $matches) { $parts = $this->parts; //Sort the parts by priority to execute faster checks first usort($parts, function ($p1, $p2) { $pr1 = $p1->getPriority(); $pr2 = $p2->getPriority(); if ($pr1 === $pr2) { return 0; } elseif ($pr1 < $pr2) { return -1; } else { return 1; } }); $filter = function ($node, $parent) use ($parts) { foreach ($parts as $part) { if (!$part->check($node, $parent)) { return false; } } return true; }; switch ($this->operator) { case " ": case ">": $children = $this->operator === ">"; $matches->map(function ($curNode) use ($filter, $children) { $ret = array(); $curNode->traverse( function ($node, $parent) use ($filter, $children, &$ret) { if ($filter($node, $parent)) { $ret[] = array($node, $parent); } if ($children) { return Traverser::DONT_TRAVERSE_CHILD_NODES; } }, array( "skipStartingNode" => true, "passParentNode" => true ) ); return $ret; }); break; case "~": case "+": $adjacent = $this->operator === "+"; $matches->map(function ($node, $parent) use ($filter, $adjacent) { $ret = array(); $evaluate = false; $props = $parent ? Utils::getExpandedNodeProperties($parent) : array(); foreach ($props as $propNode) { if ($evaluate) { if ($propNode && $filter($propNode, $parent)) { $ret[] = array($propNode, $parent); } if ($adjacent) { break; } } elseif ($propNode === $node) { $evaluate = true; } } return $ret; }); break; default: $matches->filter($filter); break; } } } * * For the full copyright and license information refer to the LICENSE file * distributed with this source code */ namespace Peast\Selector; /** * Selector parser class * * @author Marco Marchiò */ class Parser { /** * Selector string * * @var string */ protected $selector; /** * Current parser index * * @var int */ protected $index = 0; /** * Selector length * * @var int */ protected $length; /** * Whitespaces * * @var array */ protected $whitespaces = array(" ", "\t", "\n", "\r", "\f"); /** * Combinators * * @var array */ protected $combinators = array(">", "+", "~"); /** * Attribute selector operator characters * * @var array */ protected $attrOperatorChars = array("=", "<", ">", "^", "$", "*"); /** * Attribute selector operators * * @var array */ protected $attrOperators = array("=", "<", ">", "<=", ">=", "^=", "$=", "*="); /** * Valid pseudo selectors. The value indicates the argument type: * - 0: no arguments * - 1: index formula (An+B syntax) * - 2: selector * @var array */ protected $validPseudo = array( "pattern" => 0, "statement" => 0, "expression" => 0, "declaration" => 0, "first-child" => 0, "last-child" => 0, "nth-child" => 1, "nth-last-child" => 1, "has" => 2, "is" => 2, "not" => 2 ); /** * Class constructor * * @param string $selector Selector string * @param array $options Options array. See Query class * documentation for available options */ public function __construct($selector, $options = array()) { $encoding = isset($options["encoding"]) ? $options["encoding"] : null; if ($encoding && !preg_match("/UTF-?8/i", $encoding)) { $selector = mb_convert_encoding($selector, "UTF-8", $encoding); } $this->selector = $selector; $this->length = strlen($selector); } /** * Starts the parsing and returns the parsed selector * * @param bool $filter True if the selector must be used for a filter * * @return Node\Selector * * @throws Exception */ public function parse($filter = false) { $selector = $this->parseSelector($filter); //Throw an exception if the end has not been reached if (($char = $this->getChar()) !== null) { throw new Exception("Invalid syntax '$char'"); } return $selector; } /** * Parses a selector * * @param bool $filter True if the selector must be used for a filter * * @return Node\Selector * * @throws Exception */ public function parseSelector($filter = false) { $selector = new Node\Selector; do { $first = true; $group = new Node\Group; while (true) { $combinator = $this->consumeCombinator(); if (!$first && !$combinator) { break; } $parts = $this->parseSelectorParts(); if (!count($parts)) { throw new Exception("Missing selector after combinator"); } $first = false; $selCombinator = new Node\Combinator; $selCombinator->setOperator( $combinator ?: ($filter ? null : " ") ); foreach ($parts as $part) { $selCombinator->addPart($part); } $group->addCombinator($selCombinator); } $selector->addGroup($group); $this->consumeWhitespaces(); } while ($this->consume(",")); return $selector; } /** * Parses a set of selector pats * * @return array * * @throws Exception */ protected function parseSelectorParts() { $parts = array(); while (true) { if ( ($part = $this->parseSelectorPartType()) || ($part = $this->parseSelectorPartAttribute()) || ($part = $this->parseSelectorPartPseudo()) ) { $parts[] = $part; } else { break; } } return $parts; } /** * Parses a type selector part * * @return Node\Part\Type|null */ protected function parseSelectorPartType() { $type = $this->consumeWord(); if ($type) { $part = new Node\Part\Type; $part->setType($type); return $part; } return null; } /** * Parses an attribute selector part * * @return Node\Part\Attribute|null * * @throws Exception */ protected function parseSelectorPartAttribute() { if (!$this->consume("[")) { return null; } $this->consumeWhitespaces(); $part = new Node\Part\Attribute; if (!($name = $this->consumeWord())) { throw new Exception("Missing attribute name"); } $part->addName($name); while ($this->consume(".")) { if (!($name = $this->consumeWord())) { throw new Exception("Missing attribute name after dot"); } $part->addName($name); } $this->consumeWhitespaces(); $operator = $this->consumeAny($this->attrOperatorChars); if ($operator) { if (!in_array($operator, $this->attrOperators)) { throw new Exception("Invalid attribute operator '$operator'"); } $part->setOperator($operator); $this->consumeWhitespaces(); if (!($value = $this->parseLiteral())) { throw new Exception("Missing attribute value"); } $part->setValue($value[0]); if ($value[1]) { if ($operator != "=") { throw new Exception( "Only '=' operator is valid for attribute regex match" ); } $part->setRegex(true); } $this->consumeWhitespaces(); if ($this->consume("i")) { if (!is_string($value[0]) || $value[1]) { throw new Exception( "Case insensitive flag can be used only for string values" ); } $part->setCaseInsensitive(true); $this->consumeWhitespaces(); } } if (!$this->consume("]")) { throw new Exception("Unterminated attribute selector"); } return $part; } /** * Parses a pseudo selector part * * @return Node\Part\Pseudo|null * * @throws Exception */ protected function parseSelectorPartPseudo() { if (!$this->consume(":")) { return null; } $name = $this->consumeWord("-"); if (!isset($this->validPseudo[$name])) { throw new Exception("Unsupported pseudo selector '$name'"); } $argsType = $this->validPseudo[$name]; $error = false; if ($argsType === 1) { $part = new Node\Part\PseudoIndex; if (!$this->consume("(")) { $error = true; } if (!$error) { $this->consumeWhitespaces(); if ($indices = $this->consumeRegex("-?\d*n(?:\+\d+)?|\d+")) { $indices = explode("n", $indices); if (count($indices) === 1) { $part->setOffset((int) $indices[0]); } else { switch ($indices[0]) { case "": $part->setStep(1); break; case "-": $part->setStep(-1); break; default: $part->setStep((int) $indices[0]); break; } if ($indices[1] !== "") { $part->setOffset((int) $indices[1]); } } } elseif ( ($word = $this->consumeWord()) && ($word === "even" || $word === "odd") ) { $part->setStep(2); if ($word === "odd") { $part->setOffset(1); } } else { $error = true; } $this->consumeWhitespaces(); if (!$error && !$this->consume(")")) { $error = true; } } } elseif ($argsType === 2) { $part = new Node\Part\PseudoSelector; if ( $this->consume("(") && ($selector = $this->parseSelector($name !== "has")) && $this->consume(")") ) { $part->setSelector($selector); } else { $error = true; } } else { $part = new Node\Part\PseudoSimple; } if ($error) { throw new Exception( "Invalid argument for pseudo selector '$name'" ); } $part->setName($name); return $part; } /** * Parses a literal value * * @return array|null * * @throws Exception */ protected function parseLiteral() { if ( ($literal = $this->parseLiteralBoolNull()) !== 0 || ($literal = $this->parseLiteralString()) !== null || ($literal = $this->parseLiteralNumber()) !== null ) { return array($literal, false); } elseif ($literal = $this->parseLiteralRegex()) { return array($literal, true); } return null; } /** * Parses a literal boolean or null value * * @return int|bool|null * * @throws Exception */ protected function parseLiteralBoolNull() { $word = $this->consumeWord(); if (!$word) { return 0; } elseif ($word === "true") { return true; } elseif ($word === "false") { return false; } elseif ($word === "null") { return null; } throw new Exception("Invalid attribute value '$word'"); } /** * Parses a literal string * * @return string|null * * @throws Exception */ protected function parseLiteralString() { if (!($quote = $this->consumeAny(array("'", '"'), true))) { return null; } if (($str = $this->consumeUntil($quote)) === null) { throw new Exception("Unterminated string in attribute value"); } return $str; } /** * Parses a literal number * * @return int|float|null */ protected function parseLiteralNumber() { if ( $this->getChar() === "0" && ($val = $this->consumeRegex("0[xX][a-fA-F]+|0[bB][01]+|0[oO][0-7]+")) ) { $form = strtolower($val[1]); $val = substr($val, 2); if ($form === "x") { return hexdec($val); } elseif ($form === "o") { return octdec($val); } return bindec($val); } $reg = "-?\d+(?:\.\d+)?(?:[eE][+-]?\d+)?|-?\.\d+(?:[eE][+-]?\d+)?"; if (!($val = $this->consumeRegex($reg))) { return null; } return (float) $val; } /** * Parses a literal regex * * @return string|null * * @throws Exception */ protected function parseLiteralRegex() { if (!($sep = $this->consume("/"))) { return null; } if (($reg = $this->consumeUntil($sep, false, true)) === null) { throw new Exception("Unterminated regex in attribute value"); } $modifiers = $this->consumeWord(); return $sep . $reg . ($modifiers ?: ""); } /** * Consumes the given regex * * @param string $regex Regex to consume * * @return mixed|null */ protected function consumeRegex($regex) { if ($this->getChar() === null) { return null; } if (!preg_match("#^($regex)#", substr($this->selector, $this->index), $matches)) { return null; } $this->index += strlen($matches[1]); return $matches[1]; } /** * Consumes all the characters until the given one is reached * * @param string $stop Stop character * @param bool $removeEscapes If false escape characters won't be removed * @param false $includeStop If true stop character will be returned * * @return string|null */ protected function consumeUntil($stop, $removeEscapes = true, $includeStop = false) { $buffer = ""; $escaped = false; while (($char = $this->getChar()) !== null) { $this->index += 1; if (!$escaped) { if ($char === "\\") { $escaped = true; if (!$removeEscapes) { $buffer .= $char; } continue; } elseif ($char === $stop) { if ($includeStop) { $buffer .= $char; } return $buffer; } } $buffer .= $char; $escaped = false; } return null; } /** * Consumes a word composed by characters a-z * * @param null|string $extraChar Extra character to match * * @return string */ protected function consumeWord($extraChar = null) { $buffer = ""; while ($char = $this->getChar()) { if ( ($char >= "a" && $char <= "z") || ($char >= "A" && $char <= "Z") || ($extraChar !== null && $char === $extraChar) ) { $buffer .= $char; $this->index += 1; } else { break; } } return $buffer; } /** * Consumes a combinator * * @return string|null */ protected function consumeCombinator() { //Initial ws can be trimmed if followed by another combinator $ws = $this->consumeWhitespaces(); if ($combinator = $this->consumeAny($this->combinators, true)) { $this->consumeWhitespaces(); } elseif ($ws) { //If there's no other combinators use the space $combinator = " "; } else { $combinator = null; } return $combinator; } /** * Consumes as much whitespaces as possible * * @return string */ protected function consumeWhitespaces() { return $this->consumeAny($this->whitespaces); } /** * Consumes the given characters * * @param array $chars Characters to consume * @param false $stopAtFirst If true only the first matching character * is consumed * * @return string */ protected function consumeAny($chars, $stopAtFirst = false) { $buffer = ""; while (($char = $this->getChar()) !== null) { if (in_array($char, $chars)) { $buffer .= $char; $this->index++; if ($stopAtFirst) { break; } } else { break; } } return $buffer; } /** * Consumes the current character if it is equal to the * given one * * @param string $char Character to compare * * @return string|null */ protected function consume($char) { if ($this->getChar() === $char) { $this->index++; return $char; } return null; } /** * Returns the current character or null if the end * have been reached * * @return string|null */ protected function getChar() { if ($this->index < $this->length) { return $this->selector[$this->index]; } return null; } } * * For the full copyright and license information refer to the LICENSE file * distributed with this source code */ namespace Peast\Selector; /** * Selector exception class. Syntax errors in selectors are thrown * using this exception class. * * @author Marco Marchiò * * @codeCoverageIgnore */ class Exception extends \Exception { } * * For the full copyright and license information refer to the LICENSE file * distributed with this source code */ namespace Peast\Selector; use Peast\Syntax\Node\Node; /** * Selector matches class * * @author Marco Marchiò */ class Matches { /** * Matches array * * @var array */ protected $matches; /** * Class constructor * * @param array $matches Matches */ public function __construct($matches = array()) { $this->matches = $matches; } /** * Adds a new match * * @param Node $node * @param Node|null $parent */ public function addMatch(Node $node, Node $parent = null) { $this->matches[] = array($node, $parent); } /** * Returns the matches * * @returns array */ public function getMatches() { return $this->matches; } /** * Returns the matched nodes * * @return array */ public function getNodes() { return array_map(function ($m) { return $m[0]; }, $this->matches); } /** * Filters the matches using the given function, if it returns * a false value the match will be removed. The function will * receive the node and its parent as arguments. * * @param callable $fn Filter function * @return $this */ public function filter(callable $fn) { $newMatches = array(); foreach ($this->matches as $match) { if ($fn($match[0], $match[1])) { $newMatches[] = $match; } } $this->matches = $newMatches; return $this; } /** * Replaces all the matches with the result of the given function. * The function will receive the node and its parent as arguments * and must return an array of matches * * @param callable $fn Map function * * @return $this */ public function map(callable $fn) { $newMatches = array(); foreach ($this->matches as $match) { $res = $fn($match[0], $match[1]); if ($res) { $newMatches = array_merge($newMatches, $res); } } $this->matches = $newMatches; return $this->unique(); } /** * Merges the current object with the other given Matches objects * * @param Matches[] $matchesArr Array of Matches to merge * * @return $this */ public function merge($matchesArr) { foreach ($matchesArr as $matches) { foreach ($matches->getMatches() as $match) { $this->addMatch($match[0], $match[1]); } } return $this->unique(); } /** * Remove all duplicated matches * * @return $this */ public function unique() { $newMatches = array(); $newNodes = array(); foreach ($this->matches as $match) { if (!in_array($match[0], $newNodes, true)) { $newMatches[] = $match; $newNodes[] = $match[0]; } } $this->matches = $newMatches; return $this; } /** * Returns a clone of the current object * * @return Matches */ public function createClone() { return new self($this->matches); } /** * Returns the number of matches * * @return int */ public function count() { return count($this->matches); } /** * Returns the match at the given index * * @param int $index Index * * @return array * * @throws \Exception */ public function get($index) { $index = (int) $index; if (!isset($this->matches[$index])) { throw new \Exception("Invalid index $index"); } return $this->matches[$index]; } } * * For the full copyright and license information refer to the LICENSE file * distributed with this source code */ namespace Peast; /** * Nodes traverser class * * @author Marco Marchiò */ class Traverser { /** * If a function return this value, the current node will be removed */ const REMOVE_NODE = 1; /** * If a function return this value, the current node's children won't be * traversed */ const DONT_TRAVERSE_CHILD_NODES = 2; /** * If a function return this value, the traverser will stop running */ const STOP_TRAVERSING = 4; /** * Array of functions to call on each node * * @var array */ protected $functions = array(); /** * Pass parent node flag * * @var bool */ protected $passParentNode = false; /** * Skip starting node flag * * @var bool */ protected $skipStartingNode = false; /** * Class constructor. Available options are: * - skipStartingNode: if true the starting node will be skipped * - passParentNode: if true the parent node of each node will be * passed as second argument when the functions are called. Note * that the parent node is calculated during traversing, so for * the starting node it will always be null. * * @param array $options Options array */ public function __construct($options = array()) { if (isset($options["passParentNode"])) { $this->passParentNode = (bool) $options["passParentNode"]; } if (isset($options["skipStartingNode"])) { $this->skipStartingNode = (bool) $options["skipStartingNode"]; } } /** * Adds a function that will be called for each node in the tree. The * function will receive the current node as argument. The action that will * be executed on the node by the traverser depends on the returned value * of the function: * - a node: it will replace the node with the returned one * - a numeric value that is a combination of the constants defined in this * class: it will execute the function related to each constant * - an array where the first element is a node and the second element is a * numeric value that is a combination of the constants defined in this * class: it will replace the node with the returned one and it will * execute the function related to each constant (REMOVE_NODE will be * ignored since it does not make any sense in this case) * - other: nothing * * @param callable $fn Function to add * * @return $this */ public function addFunction(callable $fn) { $this->functions[] = $fn; return $this; } /** * Starts the traversing * * @param Syntax\Node\Node $node Starting node * * @return Syntax\Node\Node */ public function traverse(Syntax\Node\Node $node) { if ($this->skipStartingNode) { $this->traverseChildren($node); } else { $this->execFunctions($node); } return $node; } /** * Executes all functions on the given node and, if required, starts * traversing its children. The returned value is an array where the first * value is the node or null if it has been removed and the second value is * a boolean indicating if the traverser must continue the traversing or not * * @param Syntax\Node\Node $node Node * @param Syntax\Node\Node|null $parent Parent node * * @return array */ protected function execFunctions($node, $parent = null) { $traverseChildren = true; $continueTraversing = true; foreach ($this->functions as $fn) { $ret = $this->passParentNode ? $fn($node, $parent) : $fn($node); if ($ret) { if (is_array($ret) && $ret[0] instanceof Syntax\Node\Node) { $node = $ret[0]; if (isset($ret[1]) && is_numeric($ret[1])) { if ($ret[1] & self::DONT_TRAVERSE_CHILD_NODES) { $traverseChildren = false; } if ($ret[1] & self::STOP_TRAVERSING) { $continueTraversing = false; } } } elseif ($ret instanceof Syntax\Node\Node) { $node = $ret; } elseif (is_numeric($ret)) { if ($ret & self::DONT_TRAVERSE_CHILD_NODES) { $traverseChildren = false; } if ($ret & self::STOP_TRAVERSING) { $continueTraversing = false; } if ($ret & self::REMOVE_NODE) { $node = null; $traverseChildren = false; break; } } } } if ($traverseChildren && $continueTraversing) { $continueTraversing = $this->traverseChildren($node); } return array($node, $continueTraversing); } /** * Traverses node children. It returns a boolean indicating if the * traversing must continue or not * * @param Syntax\Node\Node $node Node * * @return bool */ protected function traverseChildren(Syntax\Node\Node $node) { $continue = true; foreach (Syntax\Utils::getNodeProperties($node, true) as $prop) { $getter = $prop["getter"]; $setter = $prop["setter"]; $child = $node->$getter(); if (!$child) { continue; } elseif (is_array($child)) { $newChildren = array(); foreach ($child as $c) { if (!$c || !$continue) { $newChildren[] = $c; } else { list($c, $continue) = $this->execFunctions($c, $node); if ($c) { $newChildren[] = $c; } } } $node->$setter($newChildren); } else { list($child, $continue) = $this->execFunctions($child, $node); $node->$setter($child); } if (!$continue) { break; } } return $continue; } } * * For the full copyright and license information refer to the LICENSE file * distributed with this source code */ namespace Peast\Syntax\ES2021; /** * ES2021 features class * * @author Marco Marchiò * * @codeCoverageIgnore */ class Features extends \Peast\Syntax\ES2020\Features { /** * Logical assignment operators * * @var bool */ public $logicalAssignmentOperators = true; /** * Numeric literal separator * * @var bool */ public $numericLiteralSeparator = true; } * * For the full copyright and license information refer to the LICENSE file * distributed with this source code */ namespace Peast\Syntax; /** * Base class for scanners. * * @author Marco Marchiò */ class Scanner { use JSX\Scanner; /** * Scanner features * * @var Features */ protected $features; /** * Current column * * @var int */ protected $column = 0; /** * Current line * * @var int */ protected $line = 1; /** * Current index * * @var int */ protected $index = 0; /** * Source length * * @var int */ protected $length; /** * Source characters * * @var array */ protected $source; /** * Consumed position * * @var Position */ protected $position; /** * Current token * * @var Token */ protected $currentToken; /** * Next token * * @var Token */ protected $nextToken; /** * Strict mode flag * * @var bool */ protected $strictMode = false; /** * True to register tokens in the tokens array * * @var bool */ protected $registerTokens = false; /** * Module mode * * @var bool */ protected $isModule = false; /** * Comments handling * * @var bool */ protected $comments = false; /** * Internal JSX scan flag * * @var bool */ protected $jsx = false; /** * Registered tokens array * * @var array */ protected $tokens = array(); /** * Comments to tokens map * * @var array */ protected $commentsMap = array(); /** * Events emitter * * @var EventsEmitter */ protected $eventsEmitter; /** * Regex to match identifiers starts * * @var string */ protected $idStartRegex = "/[\p{Lu}\p{Ll}\p{Lt}\p{Lm}\p{Lo}\p{Nl}\x{1885}\x{1886}\x{2118}\x{212E}\x{309B}\x{309C}]/u"; /** * Regex to match identifiers parts * * @var string */ protected $idPartRegex = "/[\p{Lu}\p{Ll}\p{Lt}\p{Lm}\p{Lo}\p{Nl}\x{1885}\x{1886}\x{2118}\x{212E}\x{309B}\x{309C}\p{Mn}\p{Mc}\p{Nd}\p{Pc}\x{00B7}\x{0387}\x{1369}\x{136A}\x{136B}\x{136C}\x{136D}\x{136E}\x{136F}\x{1370}\x{1371}\x{19DA}\x{200C}\x{200D}]/u"; /** * Keywords array * * @var array */ protected $keywords = array( "break", "do", "in", "typeof", "case", "else", "instanceof", "var", "catch", "export", "new", "void", "class", "extends", "return", "while", "const", "finally", "super", "with", "continue", "for", "switch", "debugger", "function", "this", "default", "if", "throw", "delete", "import", "try", "enum", "await" ); /** * Array of words that are keywords only in strict mode * * @var array */ protected $strictModeKeywords = array( "implements", "interface", "package", "private", "protected", "public", "static", "let", "yield" ); /** * Punctuators array * * @var array */ protected $punctuators = array( ".", ";", ",", "<", ">", "<=", ">=", "==", "!=", "===", "!==", "+", "-", "*", "%", "++", "--", "<<", ">>", ">>>", "&", "|", "^", "!", "~", "&&", "||", "?", ":", "=", "+=", "-=", "*=", "%=", "<<=", ">>=", ">>>=", "&=", "|=", "^=", "=>", "...", "/", "/=", "**", "**=", "??", "?.", "&&=", "||=", "??=" ); /** * Punctuators LSM * * @var LSM */ protected $punctuatorsLSM; /** * Strings stops LSM * * @var LSM */ protected $stringsStopsLSM; /** * Brackets array * * @var array */ protected $brackets = array( "(" => "", "[" => "", "{" => "", ")" => "(", "]" => "[", "}" => "{" ); /** * Open brackets array * * @var array */ protected $openBrackets = array(); /** * Open templates array * * @var array */ protected $openTemplates = array(); /** * Whitespaces array * * @var array */ protected $whitespaces = array( " ", "\t", "\n", "\r", "\f", "\v", 0x00A0, 0xFEFF, 0x00A0, 0x1680, 0x2000, 0x2001, 0x2002, 0x2003, 0x2004, 0x2005, 0x2006, 0x2007, 0x2008, 0x2009, 0x200A, 0x202F, 0x205F, 0x3000, 0x2028, 0x2029 ); /** * Line terminators characters array * * @var array * * @static */ public static $lineTerminatorsChars = array("\n", "\r", 0x2028, 0x2029); /** * Line terminators sequences array * * @var array * * @static */ public static $lineTerminatorsSequences = array("\r\n"); /** * Regex to split texts using valid ES line terminators * * @var array */ protected $linesSplitter; /** * Concatenation of line terminators characters and line terminators * sequences * * @var array */ protected $lineTerminators; /** * Properties to copy when getting the scanner state * * @var array */ protected $stateProps = array("position", "index", "column", "line", "currentToken", "nextToken", "strictMode", "openBrackets", "openTemplates", "commentsMap"); /** * Decimal numbers * * @var array */ protected $numbers = array("0", "1", "2", "3", "4", "5", "6", "7", "8", "9"); /** * Hexadecimal numbers * * @var array */ protected $xnumbers = array("0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "a", "b", "c", "d", "e", "f", "A", "B", "C", "D", "E", "F"); /** * Octal numbers * * @var array */ protected $onumbers = array("0", "1", "2", "3", "4", "5", "6", "7"); /** * Binary numbers * * @var array */ protected $bnumbers = array("0", "1"); /** * Class constructor * * @param string $source Source code * @param Features $features Scanner features * @param array $options Parsing options */ function __construct( $source, Features $features, $options ) { $this->features = $features; $encoding = isset($options["sourceEncoding"]) ? $options["sourceEncoding"] : null; //Strip BOM characters from the source $this->stripBOM($source, $encoding); //Convert to UTF8 if needed if ($encoding && !preg_match("/UTF-?8/i", $encoding)) { $source = mb_convert_encoding($source, "UTF-8", $encoding); } //Instead of using mb_substr for each character, split the source //into an array of UTF8 characters for performance reasons $this->source = Utils::stringToUTF8Array( $source, !isset($options["strictEncoding"]) || $options["strictEncoding"] ); $this->length = count($this->source); //Convert character codes to UTF8 characters in whitespaces and line //terminators $this->lineTerminators = array_merge( self::$lineTerminatorsSequences, self::$lineTerminatorsChars ); foreach (array("whitespaces", "lineTerminators") as $key) { foreach ($this->$key as $i => $char) { if (is_int($char)) { $this->{$key}[$i] = Utils::unicodeToUtf8($char); } } } //Remove exponentiation operator if the feature //is not enabled if (!$this->features->exponentiationOperator) { Utils::removeArrayValue($this->punctuators, "**"); Utils::removeArrayValue($this->punctuators, "**="); } if (!$this->features->optionalChaining) { Utils::removeArrayValue($this->punctuators, "?."); } //Remove logical assignment operators if the feature //is not enabled if (!$this->features->logicalAssignmentOperators) { Utils::removeArrayValue($this->punctuators, "&&="); Utils::removeArrayValue($this->punctuators, "||="); Utils::removeArrayValue($this->punctuators, "??="); } //Create a LSM for punctuators array $this->punctuatorsLSM = new LSM($this->punctuators); //Create a LSM for strings stops $this->stringsStopsLSM = new LSM($this->lineTerminators, true); //Allow paragraph and line separators in strings if ($this->features->paragraphLineSepInStrings) { $this->stringsStopsLSM->remove(Utils::unicodeToUtf8(0x2028)); $this->stringsStopsLSM->remove(Utils::unicodeToUtf8(0x2029)); } //Remove await as keyword if async/await is enabled if ($this->features->asyncAwait) { Utils::removeArrayValue($this->keywords, "await"); } $this->linesSplitter = "/" . implode("|", $this->lineTerminators) . "/uS"; $this->position = new Position(0, 0, 0); } /** * Strips BOM characters from the source and detects source encoding if not * given by the user * * @param string $source Source * @param string $encoding User specified encoding */ public function stripBOM(&$source, &$encoding) { $boms = array( "\xEF" => array(array("\xBB", "\xBF"), "UTF-8"), "\xFE" => array(array("\xFF"), "UTF-16BE"), "\xFF" => array(array("\xFE"), "UTF-16LE"), ); if (!isset($source[0]) || !isset($boms[$source[0]])) { return; } $bom = $boms[$source[0]]; $l = count($bom[0]); for ($i = 0; $i < $l; $i++) { if (!isset($source[$i + 1]) || $source[$i + 1] !== $bom[0][$i]) { return; } } $source = substr($source, $l + 1); if (!$encoding) { $encoding = $bom[1]; } } /** * Enables or disables module scanning mode * * @param bool $enable True to enable module scanning mode, false to disable it * * @return $this */ public function enableModuleMode($enable = true) { $this->isModule = $enable; return $this; } /** * Enables or disables comments handling * * @param bool $enable True to enable comments handling, false to disable it * * @return $this */ public function enableComments($enable = true) { $this->comments = $enable; return $this; } /** * Enables or disables tokens registration in the token array * * @param bool $enable True to enable token registration, false to disable it * * @return $this */ public function enableTokenRegistration($enable = true) { $this->registerTokens = $enable; return $this; } /** * Return registered tokens * * @return array */ public function getTokens() { return $this->tokens; } /** * Returns the scanner's event emitter * * @return EventsEmitter */ public function getEventsEmitter() { if (!$this->eventsEmitter) { //The event emitter is created here so that it won't exist if not //necessary $this->eventsEmitter = new EventsEmitter; } return $this->eventsEmitter; } /** * Enables or disables strict mode * * @param bool $strictMode Strict mode state * * @return $this */ public function setStrictMode($strictMode) { $this->strictMode = $strictMode; return $this; } /** * Return strict mode state * * @return bool */ public function getStrictMode() { return $this->strictMode; } /** * Checks if the given token is a keyword in the current strict mode state * * @param Token $token Token to checks * * @return bool */ public function isStrictModeKeyword($token) { return $token->type === Token::TYPE_KEYWORD && (in_array($token->value, $this->keywords) || ( $this->strictMode && in_array($token->value, $this->strictModeKeywords))); } /** * Returns the current scanner state * * @return array */ public function getState() { //Consume current and next tokens so that they wont' be parsed again //if the state is restored. If the current token is a slash the next //token isn't parsed, this prevents some edge cases where a regexp //that contains something that can be interpreted as a comment causes //the content to be parsed as a real comment too $token = $this->currentToken ?: $this->getToken(); if ($token && $token->value !== "/") { $this->getNextToken(); } $state = array(); foreach ($this->stateProps as $prop) { $state[$prop] = $this->$prop; } if ($this->registerTokens) { $state["tokensNum"] = count($this->tokens); } //Emit the FreezeState event and pass the given state so that listeners //attached to this event can add data $this->eventsEmitter && $this->eventsEmitter->fire( "FreezeState", array(&$state) ); return $state; } /** * Sets the current scanner state * * @param array $state State * * @return $this */ public function setState($state) { if ($this->registerTokens) { //Check if tokens have been added if (isset($this->tokens[$state["tokensNum"]])) { //Remove all added tokens for ($i = count($this->tokens) - 1; $i >= $state["tokensNum"]; $i--) { array_pop($this->tokens); } } unset($state["tokensNum"]); } //Emit the ResetState event and pass the given state $this->eventsEmitter && $this->eventsEmitter->fire( "ResetState", array(&$state) ); foreach ($state as $key => $value) { $this->$key = $value; } return $this; } /** * Returns current scanner state * * @param bool $scanPosition By default this method returns the scanner * consumed position, if this parameter is true * the scanned position will be returned * * @return Position */ public function getPosition($scanPosition = false) { if ($scanPosition) { return new Position($this->line, $this->column, $this->index); } else { return $this->position; } } /** * Sets the current scan position at the given one * * @param Position $position Position at which the scan position will be set * * @return $this */ public function setScanPosition(Position $position = null) { $this->line = $position->getLine(); $this->column = $position->getColumn(); $this->index = $position->getIndex(); return $this; } /** * Return the character at the given index in the source code or null if the * end is reached. * * @param int $index Index, if not given it will use the current index * * @return string|null */ public function charAt($index = null) { if ($index === null) { $index = $this->index; } return $index < $this->length ? $this->source[$index] : null; } /** * Throws a syntax error * * @param string $message Error message * * @return void * * @throws Exception */ protected function error($message = null) { if (!$message) { $message = "Unexpected " . $this->charAt(); } throw new Exception($message, $this->getPosition(true)); } /** * Consumes the current token * * @return $this */ public function consumeToken() { //Move the scanner position to the end of the current position $this->position = $this->currentToken->location->end; //Before consume the token, consume comments associated with it if ($this->comments) { $this->consumeCommentsForCurrentToken(); } //Register the token if required if ($this->registerTokens) { $this->tokens[] = $this->currentToken; } //Emit the TokenConsumed event for the consumed token $this->eventsEmitter && $this->eventsEmitter->fire( "TokenConsumed", array($this->currentToken) ); $this->currentToken = $this->nextToken; $this->nextToken = null; return $this; } /** * Checks if the given string is matched, if so it consumes the token * * @param string $expected String to check * * @return Token|null */ public function consume($expected) { //Do not call getToken if there's already a pending token for //performance reasons $token = $this->currentToken ?: $this->getToken(); if ($token && $token->value === $expected) { $this->consumeToken(); return $token; } return null; } /** * Checks if one of the given strings is matched, if so it consumes the * token * * @param array $expected Strings to check * * @return Token|null */ public function consumeOneOf($expected) { //Do not call getToken if there's already a pending token for //performance reasons $token = $this->currentToken ?: $this->getToken(); if ($token && in_array($token->value, $expected)) { $this->consumeToken(); return $token; } return null; } /** * Checks that there are not line terminators following the current scan * position before next token * * @param bool $nextToken By default it checks the current token position * relative to the current position, if this * parameter is true the check will be done relative * to the next token * * @return bool */ public function noLineTerminators($nextToken = false) { if ($nextToken) { $nextToken = $this->getNextToken(); $refLine = !$nextToken ? null : $nextToken->location->end->getLine(); } else { $refLine = $this->getPosition()->getLine(); } $token = $this->currentToken ?: $this->getToken(); return $token && $token->location->start->getLine() === $refLine; } /** * Checks if one of the given strings follows the current scan position * * @param string|array $expected String or array of strings to check * @param bool $nextToken This parameter must be true if the first * parameter is an array so that it will * check also next tokens * * @return bool */ public function isBefore($expected, $nextToken = false) { $token = $this->currentToken ?: $this->getToken(); if (!$token) { return false; } elseif (in_array($token->value, $expected)) { return true; } elseif (!$nextToken) { return false; } if (!$this->getNextToken()) { return false; } foreach ($expected as $val) { if (!is_array($val) || $val[0] !== $token->value) { continue; } //If the second value in the array is true check that the current //token is not followed by line terminators, otherwise compare its //value to the next token if (($val[1] === true && $this->noLineTerminators(true)) || ($val[1] !== true && $val[1] === $this->nextToken->value)) { return true; } } return false; } /** * Returns the next token * * @return Token|null */ public function getNextToken() { if (!$this->nextToken) { $token = $this->currentToken ?: $this->getToken(); $this->currentToken = null; $this->nextToken = $this->getToken(true); $this->currentToken = $token; } return $this->nextToken; } /** * Returns the current token * * @param bool $skipEOFChecks True to skip end of file checks * even if the end is reached * * @return Token|null */ public function getToken($skipEOFChecks = false) { //The current token is returned until consumed if ($this->currentToken) { return $this->currentToken; } $comments = $this->skipWhitespacesAndComments(); //Emit the TokenCreated event for all the comments found if ($comments) { foreach ($comments as $comment) { $this->eventsEmitter && $this->eventsEmitter->fire( "TokenCreated", array($comment) ); } } //When the end of the source is reached if ($this->index >= $this->length) { //Check if there are open brackets if (!$skipEOFChecks) { foreach ($this->openBrackets as $bracket => $num) { if ($num) { $this->error("Unclosed $bracket"); } } //Check if there are open templates if (count($this->openTemplates)) { $this->error("Unterminated template"); } } //Register comments and consume them if ($this->comments && $comments) { $this->commentsForCurrentToken($comments); } //Emit the EndReached event when at the end of the source $this->eventsEmitter && $this->eventsEmitter->fire( "EndReached" ); return null; } $startPosition = $this->getPosition(true); $origException = null; try { //Try to match a token if ( ($this->jsx && ($token = $this->scanJSXIdentifier())) || ($token = $this->scanTemplate()) || ($token = $this->scanNumber()) || ($this->jsx && ($token = $this->scanJSXPunctuator())) || ($token = $this->scanPunctuator()) || ($token = $this->scanKeywordOrIdentifier()) || ($this->jsx && ($token = $this->scanJSXString())) || ($token = $this->scanString()) ) { //Set the token start and end positions $token->location->start = $startPosition; $token->location->end = $this->getPosition(true); $this->currentToken = $token; //Register comments if required if ($this->comments && $comments) { $this->commentsForCurrentToken($comments); } //Emit the TokenCreated event for the token just created $this->eventsEmitter && $this->eventsEmitter->fire( "TokenCreated", array($this->currentToken) ); return $this->currentToken; } } catch (Exception $e) { $origException = $e; } //If last token was "/" do not throw an error if the token has not be //recognized since it can be the first character in a regexp and it will //be consumed when the current token will be reconsumed as a regexp if ($this->isAfterSlash($startPosition)) { $this->setScanPosition($startPosition); return null; } //No valid token found. If there was a scan error, throw the same //exception again, otherwise throw a new error if ($origException) { throw $origException; } $this->error(); } /** * Executes the operations to handle the end of the source scanning * * @return $this */ public function consumeEnd() { //Consume final comments if ($this->comments) { $this->consumeCommentsForCurrentToken(); } //Emit the EndReached event when at the end of the source $this->eventsEmitter && $this->eventsEmitter->fire( "EndReached" ); return $this; } /** * Gets or sets comments for the current token. If the parameter is an * array it associates the given comments array to the current node, * otherwise comments for the current token are returned * * @param array $comments Comments array * * @return array */ protected function commentsForCurrentToken($comments = null) { $id = $this->currentToken ? spl_object_hash($this->currentToken) : ""; if ($comments !== null) { $this->commentsMap[$id] = $comments; } elseif (isset($this->commentsMap[$id])) { $comments = $this->commentsMap[$id]; unset($this->commentsMap[$id]); } return $comments; } /** * Consumes comment tokens associated with the current token * * @return $this */ protected function consumeCommentsForCurrentToken() { $comments = $this->commentsForCurrentToken(); if ($comments && ($this->registerTokens || $this->eventsEmitter)) { foreach ($comments as $comment) { //Register the token if required if ($this->registerTokens) { $this->tokens[] = $comment; } //Emit the TokenConsumed event for the comment $this->eventsEmitter && $this->eventsEmitter->fire( "TokenConsumed", array($comment) ); } } return $this; } /** * Checks if the consumed or the scanned position follow a slash. * * @param Position $position Additional position to check * * @return bool */ protected function isAfterSlash($position = null) { $consumedIndex = $this->getPosition()->getIndex(); $checkIndices = array($consumedIndex, $consumedIndex + 1); if ($position) { $checkIndices[] = $position->getIndex() - 1; } foreach ($checkIndices as $i) { if ($i >= 0 && $this->charAt($i) === "/") { return true; } } return false; } /** * Tries to reconsume the current token as a regexp if possible * * @return Token|null */ public function reconsumeCurrentTokenAsRegexp() { $token = $this->currentToken ?: $this->getToken(); $value = $token ? $token->value : null; //Check if the token starts with "/" if (!$value || $value[0] !== "/") { return null; } //Reset the scanner position to the token's start position $startPosition = $token->location->start; $this->setScanPosition($startPosition); $buffer = "/"; $this->index++; $this->column++; $inClass = false; while (true) { //In a characters class the delimiter "/" is allowed without escape, //so the characters class must be closed before closing the regexp $stops = $inClass ? array("]") : array("/", "["); $tempBuffer = $this->consumeUntil($stops); if ($tempBuffer === null) { if ($inClass) { $this->error( "Unterminated character class in regexp" ); } else { $this->error("Unterminated regexp"); } } $buffer .= $tempBuffer[0]; if ($tempBuffer[1] === "/") { break; } else { $inClass = $tempBuffer[1] === "["; } } //Flags while (($char = $this->charAt()) !== null) { $lower = strtolower($char); if ($lower >= "a" && $lower <= "z") { $buffer .= $char; $this->index++; $this->column++; } else { break; } } //If next token has already been parsed and it's a bracket exclude it //from the count of open brackets if ($this->nextToken) { $nextVal = $this->nextToken->value; if (isset($this->brackets[$nextVal]) && isset($this->openBrackets[$nextVal]) ) { if ($this->brackets[$nextVal]) { $this->openBrackets[$nextVal]++; } else { $this->openBrackets[$nextVal]--; } } $this->nextToken = null; } //If comments handling is enabled, get the comments associated with the //current token $comments = $this->comments ? $this->commentsForCurrentToken() : null; //Replace the current token with a regexp token $token = new Token(Token::TYPE_REGULAR_EXPRESSION, $buffer); $token->location->start = $startPosition; $token->location->end = $this->getPosition(true); $this->currentToken = $token; if ($comments) { //Attach the comments to the new current token $this->commentsForCurrentToken($comments); } return $this->currentToken; } /** * Skips whitespaces and comments from the current scan position. If * comments handling is enabled, the array of parsed comments * * @return array */ protected function skipWhitespacesAndComments() { $comments = []; $content = ""; $secStartIdx = $this->index; while (($char = $this->charAt()) !== null) { //Whitespace if (in_array($char, $this->whitespaces)) { $content .= $char; $this->index++; } elseif ($char === "/") { //Comment $nextChar = $this->charAt($this->index + 1); if ($nextChar === "/" || $nextChar === "*") { //If comments must be handled, empty the current content too //and get the comment start position if ($this->comments) { if ($content !== "") { $this->adjustColumnAndLine($content); $content = ""; } $start = $this->getPosition(true); } $inline = $nextChar === "/"; $this->index += 2; $content .= $char . $nextChar; while (true) { $char = $this->charAt(); if ($char === null) { if (!$inline) { //If the end of the source has been reached and //a multiline comment is still open, it's an //error $this->error("Unterminated comment"); } $isEnd = true; } else { $content .= $char; $this->index++; $isEnd = $inline ? //Inline comment in_array($char, $this->lineTerminators) : //Multiline comment $char === "*" && $this->charAt() === "/"; } if ($isEnd) { if (!$inline) { $content .= "/"; $this->index++; } if ($this->comments) { //For inline comments the closing line //terminator must be excluded from comment text if ($inline && $char !== null) { $this->index--; $content = substr($content, 0, -strlen($char)); } $this->adjustColumnAndLine($content); $token = new Token(Token::TYPE_COMMENT, $content); $token->location->start = $start; $token->location->end = $this->getPosition(true); $comments[] = $token; //For inline comments the new content contains //the closing line terminator since the char has //already been processed $content = ""; if ($inline && $char !== null) { $content = $char; $this->index++; } } break; } } } else { break; } } elseif (!$this->isModule && $char === "<" && $this->charAt($this->index + 1) === "!" && $this->charAt($this->index + 2) === "-" && $this->charAt($this->index + 3) === "-" ) { //If comments must be handled, empty the current content too //and get the comment start position if ($this->comments) { if ($content !== "") { $this->adjustColumnAndLine($content); $content = ""; } $start = $this->getPosition(true); } //Open html comment $this->index += 4; $content .= ""; while (true) { $char = $this->charAt(); if ($char === null) { $isEnd = true; } else { $content .= $char; $this->index++; $isEnd = in_array($char, $this->lineTerminators); } if ($isEnd) { if ($this->comments) { //Remove the closing line terminator from the //comment text if ($char !== null) { $this->index--; $content = substr($content, 0, -strlen($char)); } $this->adjustColumnAndLine($content); $token = new Token(Token::TYPE_COMMENT, $content); $token->location->start = $start; $token->location->end = $this->getPosition(true); $comments[] = $token; $content = ""; if ($char !== null) { $content = $char; $this->index++; } } break; } } } else { break; } } else { break; } } if ($content !== "") { $this->adjustColumnAndLine($content); } return $comments; } /** * String scanning method * * @param bool $handleEscape True to handle escaping * * @return Token|null */ protected function scanString($handleEscape = true) { $char = $this->charAt(); if ($char === "'" || $char === '"') { $this->index++; $this->column++; //Add the quote to the LSM and then remove it after consuming $this->stringsStopsLSM->add($char); $buffer = $this->consumeUntil($this->stringsStopsLSM, $handleEscape); $this->stringsStopsLSM->remove($char); if ($buffer === null || $buffer[1] !== $char) { $this->error("Unterminated string"); } return new Token(Token::TYPE_STRING_LITERAL, $char . $buffer[0]); } return null; } /** * Template scanning method * * @return Token|null */ protected function scanTemplate() { $char = $this->charAt(); //Get the current number of open curly brackets $openCurly = isset($this->openBrackets["{"]) ? $this->openBrackets["{"] : 0; //If the character is a curly bracket check and the number of open //curly brackets matches the last number in the open templates stack, //then the bracket closes the open template expression $endExpression = false; if ($char === "}") { $len = count($this->openTemplates); if ($len && $this->openTemplates[$len - 1] === $openCurly) { $endExpression = true; array_pop($this->openTemplates); } } if ($char === "`" || $endExpression) { $this->index++; $this->column++; $buffer = $char; while (true) { $tempBuffer = $this->consumeUntil(array("`", "$")); if (!$tempBuffer) { $this->error("Unterminated template"); } $buffer .= $tempBuffer[0]; if ($tempBuffer[1] !== "$" || $this->charAt() === "{") { //If "${" is found it's a new template expression, register //the current number of open curly brackets in the open //templates stack if ($tempBuffer[1] === "$") { $this->index++; $this->column++; $buffer .= "{"; $this->openTemplates[] = $openCurly; } break; } } return new Token(Token::TYPE_TEMPLATE, $buffer); } return null; } /** * Number scanning method * * @return Token|null */ protected function scanNumber() { //Numbers can start with a decimal number or with a dot (.5) $char = $this->charAt(); if (!(($char >= "0" && $char <= "9") || $char === ".")) { return null; } $buffer = ""; $allowedDecimals = true; //Parse the integer part if ($char !== ".") { //Consume all decimal numbers $buffer = $this->consumeNumbers(); $char = $this->charAt(); if ($this->features->bigInt && $char === "n") { $this->index++; $this->column++; return new Token(Token::TYPE_BIGINT_LITERAL, $buffer . $char); } $lower = $char !== null ? strtolower($char) : null; //Handle hexadecimal (0x), octal (0o) and binary (0b) forms if ($buffer === "0" && $lower !== null && isset($this->{$lower . "numbers"}) ) { $this->index++; $this->column++; $tempBuffer = $this->consumeNumbers($lower); if ($tempBuffer === null) { $this->error("Missing numbers after 0$char"); } $buffer .= $char . $tempBuffer; //Check that there are not numbers left if ($this->consumeNumbers() !== null) { $this->error(); } if ($this->features->bigInt && $this->charAt() === "n") { $this->index++; $this->column++; return new Token(Token::TYPE_BIGINT_LITERAL, $buffer . $char); } return new Token(Token::TYPE_NUMERIC_LITERAL, $buffer); } //Consume exponent part if present if ($tempBuffer = $this->consumeExponentPart()) { $buffer .= $tempBuffer; $allowedDecimals = false; } } //Parse the decimal part if ($allowedDecimals && $this->charAt() === ".") { //Consume the dot $this->index++; $this->column++; $buffer .= "."; //Consume all decimal numbers $tempBuffer = $this->consumeNumbers(); $buffer .= $tempBuffer; //If the buffer contains only the dot it should be parsed as //punctuator if ($buffer === ".") { $this->index--; $this->column--; return null; } //Consume exponent part if present if (($tempBuffer = $this->consumeExponentPart()) !== null) { $buffer .= $tempBuffer; } } return new Token(Token::TYPE_NUMERIC_LITERAL, $buffer); } /** * Consumes the maximum number of digits * * @param string $type Digits type (decimal, hexadecimal, etc...) * @param int $max Maximum number of digits to match * * @return string|null */ protected function consumeNumbers($type = "", $max = null) { $buffer = ""; $char = $this->charAt(); $count = 0; $extra = $this->features->numericLiteralSeparator ? "_" : ""; while ( in_array($char, $this->{$type . "numbers"}) || ($count && $char === $extra) ) { $buffer .= $char; $this->index++; $this->column++; $count ++; if ($count === $max) { break; } $char = $this->charAt(); } if ($count && substr($buffer, -1) === "_") { $this->error( "Numeric separators are not allowed at the end of a number" ); } return $count ? $buffer : null; } /** * Consumes the exponent part of a number * * @return string|null */ protected function consumeExponentPart() { $buffer = ""; $char = $this->charAt(); if ($char !== null && strtolower($char) === "e") { $this->index++; $this->column++; $buffer .= $char; $char = $this->charAt(); if ($char === "+" || $char === "-") { $this->index++; $this->column++; $buffer .= $char; } $tempBuffer = $this->consumeNumbers(); if ($tempBuffer === null) { $this->error("Missing exponent"); } $buffer .= $tempBuffer; } return $buffer; } /** * Punctuator scanning method * * @return Token|null */ protected function scanPunctuator() { $token = null; $char = $this->charAt(); //Check if the next char is a bracket if (isset($this->brackets[$char])) { //Check if it is a closing bracket if ($this->brackets[$char]) { $openBracket = $this->brackets[$char]; //Check if there is a corresponding open bracket if (!isset($this->openBrackets[$openBracket]) || !$this->openBrackets[$openBracket] ) { if (!$this->isAfterSlash($this->getPosition(true))) { $this->error(); } } else { $this->openBrackets[$openBracket]--; } } else { if (!isset($this->openBrackets[$char])) { $this->openBrackets[$char] = 0; } $this->openBrackets[$char]++; } $this->index++; $this->column++; $token = new Token(Token::TYPE_PUNCTUATOR, $char); } elseif ( //Try to match the longest punctuator $match = $this->punctuatorsLSM->match($this, $this->index, $char) ) { //Optional chaining punctuator cannot appear before a number, in this //case only the question mark must be consumed if ($match[1] === "?." && ($nextChar = $this->charAt($this->index + $match[0])) !== null && $nextChar >= "0" && $nextChar <= "9" ) { $match = array(1, "?"); } $this->index += $match[0]; $this->column += $match[0]; $token = new Token(Token::TYPE_PUNCTUATOR, $match[1]); } return $token; } /** * Keywords and identifiers scanning method * * @return Token|null */ protected function scanKeywordOrIdentifier() { //Check private identifier start character if ($private = $this->features->privateMethodsAndFields && $this->charAt() === "#") { $this->index++; $this->column++; } //Consume the maximum number of characters that are unicode escape //sequences or valid identifier starts (only the first character) or //parts $buffer = ""; $start = true; while (($char = $this->charAt()) !== null) { if ( ($char >= "a" && $char <= "z") || ($char >= "A" && $char <= "Z") || $char === "_" || $char === "$" || (!$start && $char >= "0" && $char <= "9") || $this->isIdentifierChar($char, $start) ) { $buffer .= $char; $this->index++; $this->column++; } elseif ($char === "\\" && ($seq = $this->consumeUnicodeEscapeSequence())) { //Verify that it's a valid character if (!$this->isIdentifierChar($seq[1], $start)) { break; } $buffer .= $seq[0]; } else { break; } $start = false; } //Identify token type if ($buffer === "") { //Unconsume the hash if nothing was found after that if ($private) { $this->index--; $this->column--; } return null; } elseif ($private) { $type = Token::TYPE_PRIVATE_IDENTIFIER; $buffer = "#" . $buffer; } elseif ($buffer === "null") { $type = Token::TYPE_NULL_LITERAL; } elseif ($buffer === "true" || $buffer === "false") { $type = Token::TYPE_BOOLEAN_LITERAL; } elseif (in_array($buffer, $this->keywords) || in_array($buffer, $this->strictModeKeywords) ) { $type = Token::TYPE_KEYWORD; } else { $type = Token::TYPE_IDENTIFIER; } return new Token($type, $buffer); } /** * Consumes an unicode escape sequence * * @return array|null */ protected function consumeUnicodeEscapeSequence() { if ($this->charAt() !== "\\" || $this->charAt($this->index + 1) !== "u" ) { return null; } $startIndex = $this->index; $startColumn = $this->column; $this->index += 2; $this->column += 2; $brackets = false; if ($this->charAt() === "{") { //\u{FFF} $brackets = true; $this->index++; $this->column++; $code = $this->consumeNumbers("x"); if ($code && $this->charAt() !== "}") { $code = null; } else { $this->index++; $this->column++; } } else { //\uFFFF $code = $this->consumeNumbers("x", 4); if ($code && strlen($code) !== 4) { $code = null; } } //Unconsume everything if the format is invalid if ($code === null) { $this->index = $startIndex; $this->column = $startColumn; return null; } //Return an array where the first element is the matched sequence //and the second one is the decoded character return array( $brackets ? "\\u{" . $code . "}" : "\\u" . $code, Utils::unicodeToUtf8(hexdec($code)) ); } /** * Checks if the given character is valid for an identifier * * @param string $char Character to check * @param bool $start If true it will check that the character is * valid to start an identifier * * @return bool */ protected function isIdentifierChar($char, $start = true) { return ($char >= "a" && $char <= "z") || ($char >= "A" && $char <= "Z") || $char === "_" || $char === "$" || (!$start && $char >= "0" && $char <= "9") || preg_match($start ? $this->idStartRegex : $this->idPartRegex, $char); } /** * Increases columns and lines count according to the given string * * @param string $buffer String to analyze * * @return void */ protected function adjustColumnAndLine($buffer) { $lines = preg_split($this->linesSplitter, $buffer); $linesCount = count($lines) - 1; $this->line += $linesCount; $columns = mb_strlen($lines[$linesCount], "UTF-8"); if ($linesCount) { $this->column = $columns; } else { $this->column += $columns; } } /** * Consumes characters until one of the given characters is found * * @param array|LSM $stops Characters to search * @param bool $handleEscape True to handle escaping * @param bool $collectStop True to include the stop character * * @return array|null */ protected function consumeUntil( $stops, $handleEscape = true, $collectStop = true ) { $isLSM = $stops instanceof LSM; $buffer = ""; $escaped = false; while (($char = $this->charAt()) !== null) { $incrIndex = 1; $isStop = false; if ($isLSM) { $m = $stops->match($this, $this->index, $char); if ($m) { $isStop = true; $incrIndex = $m[0]; $char = $m[1]; } } else { $isStop = in_array($char, $stops); } $validStop = $isStop && !$escaped; if (!$validStop || $collectStop) { $this->index += $incrIndex; $buffer .= $char; } if ($validStop) { if (!$collectStop && $buffer === "") { return null; } $this->adjustColumnAndLine($buffer); return array($buffer, $char); } elseif (!$escaped && $char === "\\" && $handleEscape) { $escaped = true; } else { $escaped = false; } } return null; } } * * For the full copyright and license information refer to the LICENSE file * distributed with this source code */ namespace Peast\Syntax\ES2022; /** * ES2022 features class * * @author Marco Marchiò * * @codeCoverageIgnore */ class Features extends \Peast\Syntax\ES2021\Features { /** * Private methods and fields * * @var bool */ public $privateMethodsAndFields = true; /** * Class fields * * @var bool */ public $classFields = true; /** * "in" operator for private fields * * @var bool */ public $classFieldsPrivateIn = true; /** * Top level await * * @var bool */ public $topLevelAwait = true; /** * Class static block * * @var bool */ public $classStaticBlock = true; /** * Arbitrary module namespace identifier names * * @var bool */ public $arbitraryModuleNSNames = true; } * * For the full copyright and license information refer to the LICENSE file * distributed with this source code */ namespace Peast\Syntax\ES2015; /** * ES2015 features class * * @author Marco Marchiò * * @codeCoverageIgnore */ class Features extends \Peast\Syntax\Features { } * * For the full copyright and license information refer to the LICENSE file * distributed with this source code */ namespace Peast\Syntax; /** * Base class for parsers. * * @author Marco Marchiò * * @abstract */ abstract class ParserAbstract { /** * Associated scanner * * @var Scanner */ protected $scanner; /** * Parser features * * @var Features */ protected $features; /** * Parser context * * @var \stdClass */ protected $context; /** * Source type * * @var string */ protected $sourceType; /** * Comments handling * * @var bool */ protected $comments; /** * JSX syntax handling * * @var bool */ protected $jsx; /** * Events emitter * * @var EventsEmitter */ protected $eventsEmitter; /** * Class constructor * * @param string $source Source code * @param Features $features Parser features * @param array $options Parsing options */ public function __construct( $source, Features $features, $options = array() ) { $this->features = $features; $this->sourceType = isset($options["sourceType"]) ? $options["sourceType"] : \Peast\Peast::SOURCE_TYPE_SCRIPT; //Create the scanner $this->scanner = new Scanner($source, $features, $options); //Enable module scanning if required if ($this->sourceType === \Peast\Peast::SOURCE_TYPE_MODULE) { $this->scanner->enableModuleMode(); } //Enable comments scanning $this->comments = isset($options["comments"]) && $options["comments"]; if ($this->comments) { $this->scanner->enableComments(); //Create the comments registry new CommentsRegistry($this); } // Enable jsx syntax if required $this->jsx = isset($options["jsx"]) && $options["jsx"]; $this->initContext(); $this->postInit(); } /** * Initializes parser context * * @return void */ abstract protected function initContext(); /** * Post initialize operations * * @return void */ abstract protected function postInit(); /** * Parses the source * * @return Node\Node * * @abstract */ abstract public function parse(); /** * Returns parsed tokens from the source code * * @return Token[] */ public function tokenize() { $this->scanner->enableTokenRegistration(); $this->parse(); return $this->scanner->getTokens(); } /** * Returns the scanner associated with the parser * * @return Scanner */ public function getScanner() { return $this->scanner; } /** * Returns the parser features class * * @return Features */ public function getFeatures() { return $this->features; } /** * Returns the parser's events emitter * * @return EventsEmitter */ public function getEventsEmitter() { if (!$this->eventsEmitter) { //The event emitter is created here so that it won't exist if not //necessary $this->eventsEmitter = new EventsEmitter; } return $this->eventsEmitter; } /** * Calls a method with an isolated parser context, applying the given flags, * but restoring their values after the execution. * * @param array|null $flags Key/value array of changes to apply to the * context flags. If it's null or the first * element of the array is null the context will * be reset before applying new values. * @param string $fn Method to call * @param array|null $args Method arguments * * @return mixed */ protected function isolateContext($flags, $fn, $args = null) { //Store the current context $oldContext = clone $this->context; //If flag argument is null reset the context if ($flags === null) { $this->initContext(); } else { //Apply new values to the flags foreach ($flags as $k => $v) { // If null reset the context if ($v === null) { $this->initContext(); } else { $this->context->$k = $v; } } } //Call the method with the given arguments $ret = $args ? call_user_func_array(array($this, $fn), $args) : $this->$fn(); //Restore previous context $this->context = $oldContext; return $ret; } /** * Creates a node * * @param string $nodeType Node's type * @param mixed $position Node's start position * * @return Node\Node * * @codeCoverageIgnore */ protected function createNode($nodeType, $position) { //Use the right class to get an instance of the node $nodeClass = "\\Peast\\Syntax\\Node\\" . $nodeType; $node = new $nodeClass; //Add the node start position if ($position instanceof Node\Node || $position instanceof Token) { $position = $position->location->start; } elseif (is_array($position)) { if (count($position)) { $position = $position[0]->location->start; } else { $position = $this->scanner->getPosition(); } } $node->location->start = $position; //Emit the NodeCreated event for the node $this->eventsEmitter && $this->eventsEmitter->fire( "NodeCreated", array($node) ); return $node; } /** * Completes a node by adding the end position * * @param Node\Node $node Node to complete * @param Position $position Node's end position * * @return mixed It actually returns a Node but mixed solves * a lot of PHPDoc problems * * @codeCoverageIgnore */ protected function completeNode(Node\Node $node, $position = null) { //Add the node end position $node->location->end = $position ?: $this->scanner->getPosition(); //Emit the NodeCompleted event for the node $this->eventsEmitter && $this->eventsEmitter->fire( "NodeCompleted", array($node) ); return $node; } /** * Throws a syntax error * * @param string $message Error message * @param Position $position Error position * * @return void * * @throws Exception */ protected function error($message = "", $position = null) { if (!$message) { $token = $this->scanner->getToken(); if ($token === null) { $message = "Unexpected end of input"; } else { $position = $token->location->start; $message = "Unexpected: " . $token->value; } } if (!$position) { $position = $this->scanner->getPosition(); } throw new Exception($message, $position); } /** * Asserts that a valid end of statement follows the current position * * @return boolean * * @throws Exception */ protected function assertEndOfStatement() { //The end of statement is reached when it is followed by line //terminators, end of source, "}" or ";". In the last case the token //must be consumed if (!$this->scanner->noLineTerminators()) { return true; } else { if ($this->scanner->consume(";")) { return true; } $token = $this->scanner->getToken(); if (!$token || $token->value === "}") { return true; } } $this->error(); } /** * Parses a character separated list of instructions or null if the * sequence is not valid * * @param callable $fn Parsing instruction function * @param string $char Separator * * @return array * * @throws Exception */ protected function charSeparatedListOf($fn, $char = ",") { $list = array(); $valid = true; while ($param = $this->$fn()) { $list[] = $param; $valid = true; if (!$this->scanner->consume($char)) { break; } else { $valid = false; } } if (!$valid) { $this->error(); } return $list; } } * * For the full copyright and license information refer to the LICENSE file * distributed with this source code */ namespace Peast\Syntax\JSX; use Peast\Syntax\Token; /** * JSX scanner trait * * @author Marco Marchiò */ trait Scanner { /** * Tries to reconsume the current token as a jsx text if possible * * @return Token|null */ public function reconsumeCurrentTokenAsJSXText() { //Current and next tokens must be reset and the open brackets count must be correct //like they were never scanned foreach (array($this->currentToken, $this->nextToken) as $token) { if ($token && isset($this->brackets[$token->value])) { if ($refBracket = $this->brackets[$token->value]) { $this->openBrackets[$refBracket]++; } else { $this->openBrackets[$token->value]--; } } } $this->nextToken = null; $this->currentToken = null; $startPosition = $this->getPosition(); $this->setScanPosition($startPosition); $result = $this->consumeUntil(array("{", "<"), false, false); if ($result) { $this->currentToken = new Token(Token::TYPE_JSX_TEXT, $result[0]); $this->currentToken->location->start = $startPosition; $this->currentToken->location->end = $this->getPosition(true); } return $this->currentToken; } /** * Reconsumes the current token in jsx mode * * @return Token|null */ public function reconsumeCurrentTokenInJSXMode() { $this->jsx = true; $this->nextToken = null; $this->currentToken = null; $startPosition = $this->getPosition(); $this->setScanPosition($startPosition); $token = $this->getToken(); $this->jsx = false; return $token; } /** * String scanning method in jsx mode * * @return Token|null */ public function scanJSXString() { return $this->scanString(false); } /** * String punctuator method in jsx mode * * @return Token|null */ public function scanJSXPunctuator() { //The ">" character in jsx mode must be emitted in its own token //without matching longer sequences like ">>" $char = $this->charAt(); if ($char === ">") { $this->index++; $this->column++; return new Token(Token::TYPE_PUNCTUATOR, $char); } return $this->scanPunctuator(); } /** * Identifier scanning method in jsx mode * * @return Token|null */ public function scanJSXIdentifier() { $buffer = ""; $char = $this->charAt(); if ($char !== null && $this->isIdentifierChar($char)) { do { $buffer .= $char; $this->index++; $this->column++; $char = $this->charAt(); } while ( $char !== null && ($this->isIdentifierChar($char, false) || $char === "-") ); } return $buffer === "" ? null : new Token(Token::TYPE_JSX_IDENTIFIER, $buffer); } } * * For the full copyright and license information refer to the LICENSE file * distributed with this source code */ namespace Peast\Syntax\JSX; use Peast\Syntax\Token; /** * JSX parser trait * * @author Marco Marchiò */ trait Parser { /** * Creates a JSX node * * @param string $nodeType Node's type * @param mixed $position Node's start position * * @return \Peast\Syntax\Node\Node */ protected function createJSXNode($nodeType, $position) { return $this->createNode("JSX\\$nodeType", $position); } /** * Parses a jsx fragment * * @return \Peast\Syntax\Node\JSX\JSXFragment|null */ protected function parseJSXFragment() { $startOpeningToken = $this->scanner->getToken(); if (!$startOpeningToken || $startOpeningToken->value !== "<") { return null; } $endOpeningToken = $this->scanner->getNextToken(); if (!$endOpeningToken || $endOpeningToken->value !== ">") { return null; } $this->scanner->consumeToken(); $this->scanner->consumeToken(); $children = $this->parseJSXChildren(); if (!($startClosingToken = $this->scanner->consume("<")) || !$this->scanner->consume("/") || !$this->scanner->reconsumeCurrentTokenInJSXMode() || $endOpeningToken->value !== ">") { $this->error(); } $this->scanner->consumeToken(); //Opening tag $openingNode = $this->createJSXNode( "JSXOpeningFragment", $startOpeningToken ); $this->completeNode( $openingNode, $endOpeningToken->location->end ); //Closing tag $closingNode = $this->createJSXNode( "JSXClosingFragment", $startClosingToken ); $this->completeNode($closingNode); //Fragment $node = $this->createJSXNode("JSXFragment", $startOpeningToken); $node->setOpeningFragment($openingNode); $node->setClosingFragment($closingNode); if ($children) { $node->setChildren($children); } return $this->completeNode($node); } /** * Parses a group of jsx children * * @return \Peast\Syntax\Node\Node[]|null */ protected function parseJSXChildren() { $children = array(); while ($child = $this->parseJSXChild()) { $children[] = $child; } return count($children) ? $children : null; } /** * Parses a jsx child * * @return \Peast\Syntax\Node\Node|null */ protected function parseJSXChild() { if ($node = $this->parseJSXText()) { return $node; } elseif ($node = $this->parseJSXFragment()) { return $node; } elseif($node = $this->parseJSXElement()) { return $node; } elseif ($startToken = $this->scanner->consume("{")) { $spread = $this->scanner->consume("..."); $exp = $this->parseAssignmentExpression(); $midPos = $this->scanner->getPosition(); if (($spread && !$exp) || !$this->scanner->consume("}")) { $this->error(); } $node = $this->createJSXNode( $spread ? "JSXSpreadChild" : "JSXExpressionContainer", $startToken ); if (!$exp) { $exp = $this->createJSXNode("JSXEmptyExpression", $midPos); $this->completeNode($exp, $midPos); } $node->setExpression($exp); return $this->completeNode($node); } return null; } /** * Parses a jsx text * * @return \Peast\Syntax\Node\JSX\JSXText|null */ protected function parseJSXText() { if (!($token = $this->scanner->reconsumeCurrentTokenAsJSXText())) { return null; } $this->scanner->consumeToken(); $node = $this->createJSXNode("JSXText", $token); $node->setRaw($token->value); return $this->completeNode($node, $token->location->end); } /** * Parses a jsx element * * @return \Peast\Syntax\Node\JSX\JSXElement|null */ protected function parseJSXElement() { $startOpeningToken = $this->scanner->getToken(); if (!$startOpeningToken || $startOpeningToken->value !== "<") { return null; } $nextToken = $this->scanner->getNextToken(); if ($nextToken && $nextToken->value === "/") { return null; } $this->scanner->consumeToken(); if (!($name = $this->parseJSXIdentifierOrMemberExpression())) { $this->error(); } $attributes = $this->parseJSXAttributes(); $selfClosing = $this->scanner->consume("/"); $endOpeningToken = $this->scanner->reconsumeCurrentTokenInJSXMode(); if (!$endOpeningToken || $endOpeningToken->value !== ">") { $this->error(); } $this->scanner->consumeToken(); if (!$selfClosing) { $children = $this->parseJSXChildren(); if ( ($startClosingToken = $this->scanner->consume("<")) && $this->scanner->consume("/") && ($closingName = $this->parseJSXIdentifierOrMemberExpression()) && ($endClosingToken = $this->scanner->reconsumeCurrentTokenInJSXMode()) && ($endClosingToken->value === ">") ) { $this->scanner->consumeToken(); if (!$this->isSameJSXElementName($name, $closingName)) { $this->error("Closing tag does not match opening tag"); } } else { $this->error(); } } //Opening tag $openingNode = $this->createJSXNode( "JSXOpeningElement", $startOpeningToken ); $openingNode->setName($name); $openingNode->setSelfClosing($selfClosing); if ($attributes) { $openingNode->setAttributes($attributes); } $this->completeNode( $openingNode, $endOpeningToken->location->end ); //Closing tag $closingNode = null; if (!$selfClosing) { $closingNode = $this->createJSXNode( "JSXClosingElement", $startClosingToken ); $closingNode->setName($closingName); $this->completeNode($closingNode); } //Element $node = $this->createJSXNode("JSXElement", $startOpeningToken); $node->setOpeningElement($openingNode); if ($closingNode) { $node->setClosingElement($closingNode); if ($children) { $node->setChildren($children); } } return $this->completeNode($node); } /** * Parses a jsx identifier, namespaced identifier or member expression * * @param bool $allowMember True to allow member expressions * * @return \Peast\Syntax\Node\Node|null */ protected function parseJSXIdentifierOrMemberExpression($allowMember = true) { $idToken = $this->scanner->reconsumeCurrentTokenInJSXMode(); if (!$idToken || $idToken->type !== Token::TYPE_JSX_IDENTIFIER) { return null; } $this->scanner->consumeToken(); $idNode = $this->createJSXNode("JSXIdentifier", $idToken); $idNode->setName($idToken->value); $idNode = $this->completeNode($idNode); //Namespaced identifier if ($this->scanner->consume(":")) { $idToken2 = $this->scanner->reconsumeCurrentTokenInJSXMode(); if (!$idToken2 || $idToken2->type !== Token::TYPE_JSX_IDENTIFIER) { $this->error(); } $this->scanner->consumeToken(); $idNode2 = $this->createJSXNode("JSXIdentifier", $idToken2); $idNode2->setName($idToken2->value); $idNode2 = $this->completeNode($idNode2); $node = $this->createJSXNode("JSXNamespacedName", $idToken); $node->setNamespace($idNode); $node->setName($idNode2); return $this->completeNode($node); } //Get following identifiers $nextIds = array(); if ($allowMember) { while ($this->scanner->consume(".")) { $nextId = $this->scanner->reconsumeCurrentTokenInJSXMode(); if (!$nextId || $nextId->type !== Token::TYPE_JSX_IDENTIFIER) { $this->error(); } $this->scanner->consumeToken(); $nextIds[] = $nextId; } } //Create the member expression if required $objectNode = $idNode; foreach ($nextIds as $nid) { $propEnd = $nid->location->end; $propNode = $this->createJSXNode("JSXIdentifier", $nid); $propNode->setName($nid->value); $propNode = $this->completeNode($propNode, $propEnd); $node = $this->createJSXNode("JSXMemberExpression", $objectNode); $node->setObject($objectNode); $node->setProperty($propNode); $objectNode = $this->completeNode($node, $propEnd); } return $objectNode; } /** * Parses a jsx attributes list * * @return \Peast\Syntax\Node\Node[]|null */ protected function parseJSXAttributes() { $attributes = array(); while ( ($attr = $this->parseJSXSpreadAttribute()) || ($attr = $this->parseJSXAttribute()) ) { $attributes[] = $attr; } return count($attributes) ? $attributes : null; } /** * Parses a jsx spread attribute * * @return \Peast\Syntax\Node\JSX\JSXSpreadAttribute|null */ protected function parseJSXSpreadAttribute() { if (!($openToken = $this->scanner->consume("{"))) { return null; } if ( $this->scanner->consume("...") && ($exp = $this->parseAssignmentExpression()) && $this->scanner->consume("}") ) { $node = $this->createJSXNode("JSXSpreadAttribute", $openToken); $node->setArgument($exp); return $this->completeNode($node); } $this->error(); } /** * Parses a jsx spread attribute * * @return \Peast\Syntax\Node\JSX\JSXSpreadAttribute|null */ protected function parseJSXAttribute() { if (!($name = $this->parseJSXIdentifierOrMemberExpression(false))) { return null; } $value = null; if ($this->scanner->consume("=")) { $strToken = $this->scanner->reconsumeCurrentTokenInJSXMode(); if ($strToken && $strToken->type === Token::TYPE_STRING_LITERAL) { $this->scanner->consumeToken(); $value = $this->createNode("StringLiteral", $strToken); $value->setRaw($strToken->value); $value = $this->completeNode($value); } elseif ($startExp = $this->scanner->consume("{")) { if ( ($exp = $this->parseAssignmentExpression()) && $this->scanner->consume("}") ) { $value = $this->createJSXNode( "JSXExpressionContainer", $startExp ); $value->setExpression($exp); $value = $this->completeNode($value); } else { $this->error(); } } elseif ( !($value = $this->parseJSXFragment()) && !($value = $this->parseJSXElement()) ) { $this->error(); } } $node = $this->createJSXNode("JSXAttribute", $name); $node->setName($name); if ($value) { $node->setValue($value); } return $this->completeNode($node); } /** * Checks that 2 tag names are equal * * @param \Peast\Syntax\Node\Node $n1 First name * @param \Peast\Syntax\Node\Node $n2 Second name * * @return bool */ protected function isSameJSXElementName($n1, $n2) { $type = $n1->getType(); if ($type !== $n2->getType()) { return false; } elseif ($type === "JSXNamespacedName") { return $this->isSameJSXElementName( $n1->getNamespace(), $n2->getNamespace() ) && $this->isSameJSXElementName( $n1->getName(), $n2->getName() ); } elseif ($type === "JSXMemberExpression") { return $this->isSameJSXElementName( $n1->getObject(), $n2->getObject() ) && $this->isSameJSXElementName( $n1->getProperty(), $n2->getProperty() ); } return $type === "JSXIdentifier" && $n1->getName() === $n2->getName(); } } * * For the full copyright and license information refer to the LICENSE file * distributed with this source code */ namespace Peast\Syntax; /** * Comments registry class. Internal class used to manage comments * * @author Marco Marchiò */ class CommentsRegistry { /** * Map of the indices where nodes start * * @var int */ protected $nodesStartMap = array(); /** * Map of the indices where nodes end * * @var int */ protected $nodesEndMap = array(); /** * Comments buffer * * @var array */ protected $buffer = null; /** * Last token index * * @var int */ protected $lastTokenIndex = null; /** * Comments registry * * @var array */ protected $registry = array(); /** * Class constructor * * @param Parser $parser Parser */ public function __construct(Parser $parser) { $parser->getEventsEmitter() ->addListener("NodeCompleted", array($this, "onNodeCompleted")) ->addListener("EndParsing", array($this, "onEndParsing")); $parser->getScanner()->getEventsEmitter() ->addListener("TokenConsumed", array($this, "onTokenConsumed")) ->addListener("EndReached", array($this, "onTokenConsumed")) ->addListener("FreezeState", array($this, "onScannerFreezeState")) ->addListener("ResetState", array($this, "onScannerResetState")); } /** * Listener called every time the scanner compose the array that represents * its current state * * @param array $state State * * @return void */ public function onScannerFreezeState(&$state) { //Register the current last token index $state["commentsLastTokenIndex"] = $this->lastTokenIndex; } /** * Listener called every time the scanner reset its state using the given * array * * @param array $state State * * @return void */ public function onScannerResetState(&$state) { //Reset the last token index and delete it from the state array $this->lastTokenIndex = $state["commentsLastTokenIndex"]; unset($state["commentsLastTokenIndex"]); } /** * Listener called every time a token is consumed and when the scanner * reaches the end of the source * * @param Token|null $token Consumed token or null if the end has * been reached * * @return void */ public function onTokenConsumed(Token $token = null) { //Check if it's a comment if ($token && $token->type === Token::TYPE_COMMENT) { //If there is not an open comments buffer, create it if (!$this->buffer) { $this->buffer = array( "prev" => $this->lastTokenIndex, "next" => null, "comments" => array() ); } //Add the comment token to the buffer $this->buffer["comments"][] = $token; } else { if ($token) { $loc = $token->location; //Store the token end position $this->lastTokenIndex = $loc->end->getIndex(); if ($this->buffer) { //Fill the "next" key on the comments buffer with the token //start position $this->buffer["next"] = $loc->start->getIndex(); } } //If there is an open comment buffer, close it and move it to the //registry if ($buffer = $this->buffer) { //Use the location as key to add the group of comments to the //registry, in this way if comments are reprocessed they won't //be duplicated $key = implode("-", array( $buffer["prev"] !== null ? $buffer["prev"] : "", $buffer["next"] !== null ? $buffer["next"] : "" )); $this->registry[$key] = $this->buffer; $this->buffer = null; } } } /** * Listener called every time a node is completed by the parser * * @param Node\Node $node Completed node * * @return void */ public function onNodeCompleted(Node\Node $node) { //Every time a node is completed, register its start and end indices //in the relative properties $loc = $node->location; foreach (array("Start", "End") as $pos) { $val = $loc->{"get$pos"}()->getIndex(); $map = &$this->{"nodes{$pos}Map"}; if (!isset($map[$val])) { $map[$val] = array(); } $map[$val][] = $node; } } /** * Listener called when parsing process ends * * @return void */ public function onEndParsing() { //Return if there are no comments to process if ($this->registry) { //Make sure nodes start indices map is sorted ksort($this->nodesStartMap); //Loop all comment groups in the registry foreach ($this->registry as $group) { $this->findNodeForCommentsGroup($group); } } } /** * Finds the node to attach the given comments group * * @param array $group Comments group * * @return void */ public function findNodeForCommentsGroup($group) { $next = $group["next"]; $prev = $group["prev"]; $comments = $group["comments"]; $leading = true; //If the group of comments has a next token index that appears //in the map of start node indices, add the group to the //corresponding node's leading comments. This associates //comments that appear immediately before a node. //For example: /*comment*/ for (;;){} if (isset($this->nodesStartMap[$next])) { $nodes = $this->nodesStartMap[$next]; } //If the group of comments has a previous token index that appears //in the map of end node indices, add the group to the //corresponding node's trailing comments. This associates //comments that appear immediately after a node. //For example: for (;;){} /*comment*/ elseif (isset($this->nodesEndMap[$prev])) { $nodes = $this->nodesEndMap[$prev]; $leading = false; } //Otherwise, find a node that wraps the comments position. //This associates inner comments: //For example: for /*comment*/ (;;){} else { //Calculate comments group boundaries $start = $comments[0]->location->start->getIndex(); $end = $comments[count($comments) -1]->location->end->getIndex(); $nodes = array(); //Loop all the entries in the start index map foreach ($this->nodesStartMap as $idx => $ns) { //If the index is higher than the start index of the comments //group, stop if ($idx > $start) { break; } foreach ($ns as $node) { //Check if the comments group is inside node indices range if ($node->location->end->getIndex() >= $end) { $nodes[] = $node; } } } //If comments can't be associated with any node, associate it as //leading comments of the program, this happens when the source is //empty if (!$nodes) { $firstNode = array_values($this->nodesStartMap); $nodes = array($firstNode[0][0]); } } //If there are multiple possible nodes to associate the comments to, //find the shortest one if (count($nodes) > 1) { usort($nodes, array($this, "compareNodesLength")); } $this->associateComments($nodes[0], $comments, $leading); } /** * Compares node length * * @param Node\Node $node1 First node * @param Node\Node $node2 Second node * * @return int * * @codeCoverageIgnore */ public function compareNodesLength($node1, $node2) { $loc1 = $node1->location; $length1 = $loc1->end->getIndex() - $loc1->start->getIndex(); $loc2 = $node2->location; $length2 = $loc2->end->getIndex() - $loc2->start->getIndex(); //If the nodes have the same length make sure to choose nodes //different from Program nodes if ($length1 === $length2) { if ($node1 instanceof Node\Program) { $length1 += 1000; } elseif ($node2 instanceof Node\Program) { $length2 += 1000; } } return $length1 < $length2 ? -1 : 1; } /** * Adds comments to the given node * * @param Node\Node $node Node * @param array $comments Array of comments to add * @param bool $leading True to add comments as leading comments * or false to add them as trailing comments * * @return void */ public function associateComments($node, $comments, $leading) { $fn = ($leading ? "Leading" : "Trailing") . "Comments"; $currentComments = $node->{"get$fn"}(); foreach ($comments as $comment) { $loc = $comment->location; $commentNode = new Node\Comment; $commentNode->location->start = $loc->start; $commentNode->location->end = $loc->end; $commentNode->setRawText($comment->value); $currentComments[] = $commentNode; } $node->{"set$fn"}($currentComments); } } * * For the full copyright and license information refer to the LICENSE file * distributed with this source code */ namespace Peast\Syntax\Node; /** * A node that represents a switch statement. * For example: switch (test) {} * * @author Marco Marchiò */ class SwitchStatement extends Node implements Statement { /** * Map of node properties * * @var array */ protected $propertiesMap = array( "discriminant" => true, "cases" => true ); /** * Discriminant expression * * @var Expression */ protected $discriminant; /** * Cases array * * @var SwitchCase[] */ protected $cases = array(); /** * Returns the discriminant expression * * @return Expression */ public function getDiscriminant() { return $this->discriminant; } /** * Sets the discriminant expression * * @param Expression $discriminant Discriminant expression * * @return $this */ public function setDiscriminant(Expression $discriminant) { $this->discriminant = $discriminant; return $this; } /** * Returns the cases array * * @return SwitchCase[] */ public function getCases() { return $this->cases; } /** * Sets the cases array * * @param SwitchCase[] $cases Cases array * * @return $this */ public function setCases($cases) { $this->assertArrayOf($cases, "SwitchCase"); $this->cases = $cases; return $this; } } * * For the full copyright and license information refer to the LICENSE file * distributed with this source code */ namespace Peast\Syntax\Node; /** * A node that represents a "new" expression. * For example: new test() * * @author Marco Marchiò */ class NewExpression extends CallExpression { } * * For the full copyright and license information refer to the LICENSE file * distributed with this source code */ namespace Peast\Syntax\Node; use Peast\Query; use Peast\Selector; /** * Root node for scripts and modules. * * @author Marco Marchiò */ class Program extends Node { /** * Map of node properties * * @var array */ protected $propertiesMap = array( "body" => true, "sourceType" => false ); /** * Source type that is one of the source type constants in the Peast class * * @var string */ protected $sourceType = \Peast\Peast::SOURCE_TYPE_SCRIPT; /** * Program's body * * @var Statement[]|ModuleDeclaration[] */ protected $body = array(); /** * Returns the source type that is one of the source type constants in the * Peast class * * @return string */ public function getSourceType() { return $this->sourceType; } /** * Sets the source type that is one of the source type constants in the * Peast class * * @param string $sourceType Source type * * @return $this */ public function setSourceType($sourceType) { $this->sourceType = $sourceType; return $this; } /** * Returns the program's body * * @return Statement[]|ModuleDeclaration[] */ public function getBody() { return $this->body; } /** * Sets the program's body * * @param Statement[]|ModuleDeclaration[] $body Program's body * * @return $this */ public function setBody($body) { $this->assertArrayOf($body, array("Statement", "ModuleDeclaration")); $this->body = $body; return $this; } /** * Finds nodes matching the given selector. * * @param string $selector Selector * @param array $options Options array. See Query class * documentation for available options * * @return Query * * @throws Selector\Exception */ public function query($selector, $options = array()) { $query = new Query($this, $options); return $query->find($selector); } } * * For the full copyright and license information refer to the LICENSE file * distributed with this source code */ namespace Peast\Syntax\Node; /** * A node that represents a specifier in an import declaration. * For example "{a}" in: import {a} from "test" * * @author Marco Marchiò */ class ImportSpecifier extends ModuleSpecifier { /** * Map of node properties * * @var array */ protected $propertiesMap = array( "imported" => true ); /** * Imported identifier * * @var Identifier|StringLiteral */ protected $imported; /** * Returns the imported identifier * * @return Identifier|StringLiteral */ public function getImported() { return $this->imported; } /** * Sets the imported identifier * * @param Identifier|StringLiteral $imported Imported identifier * * @return $this */ public function setImported($imported) { $this->assertType($imported, array("Identifier", "StringLiteral")); $this->imported = $imported; return $this; } } * * For the full copyright and license information refer to the LICENSE file * distributed with this source code */ namespace Peast\Syntax\Node; /** * A node that represents the throw statement. * For example: throw err * * @author Marco Marchiò */ class ThrowStatement extends Node implements Statement { /** * Map of node properties * * @var array */ protected $propertiesMap = array( "argument" => true ); /** * The thrown expression * * @var Expression */ protected $argument; /** * Returns the thrown expression * * @return Expression */ public function getArgument() { return $this->argument; } /** * Sets the thrown expression * * @param Expression $argument The node to set * * @return $this */ public function setArgument(Expression $argument) { $this->argument = $argument; return $this; } } * * For the full copyright and license information refer to the LICENSE file * distributed with this source code */ namespace Peast\Syntax\Node; /** * A node that represents a for-in statement. * For example: for (var a in b) {} * * @author Marco Marchiò */ class ForInStatement extends Node implements Statement { /** * Map of node properties * * @var array */ protected $propertiesMap = array( "left" => true, "right" => true, "body" => true ); /** * Iteration variable * * @var VariableDeclaration|Expression|Pattern */ protected $left; /** * Iterated object * * @var Expression */ protected $right; /** * Loop body * * @var Statement */ protected $body; /** * Returns the iteration variable * * @return VariableDeclaration|Expression|Pattern */ public function getLeft() { return $this->left; } /** * Sets the iteration variable * * @param VariableDeclaration|Expression|Pattern $left Iteration variable * * @return $this */ public function setLeft($left) { $this->assertType( $left, array("VariableDeclaration", "Expression", "Pattern") ); $this->left = $left; return $this; } /** * Returns the iterated object * * @return Expression */ public function getRight() { return $this->right; } /** * Sets the iterated object * * @param Expression $right Iterated object * * @return $this */ public function setRight(Expression $right) { $this->right = $right; return $this; } /** * Returns the loop body * * @return Statement */ public function getBody() { return $this->body; } /** * Sets the loop body * * @param Statement $body Loop body * * @return $this */ public function setBody(Statement $body) { $this->body = $body; return $this; } } * * For the full copyright and license information refer to the LICENSE file * distributed with this source code */ namespace Peast\Syntax\Node; /** * A node that represents a property definition in class body. * * @author Marco Marchiò */ class PropertyDefinition extends Node { /** * Map of node properties * * @var array */ protected $propertiesMap = array( "key" => true, "value" => true, "computed" => false, "static" => false ); /** * Property key * * @var Expression|PrivateIdentifier */ protected $key; /** * Optional property value * * @var Expression|null */ protected $value; /** * Property computed flag that is true when the property is declared using * the square brackets syntax * * @var bool */ protected $computed = false; /** * Property static flag that is true when the property is declared static * * @var bool */ protected $static = false; /** * Returns the property key * * @return Expression|PrivateIdentifier */ public function getKey() { return $this->key; } /** * Sets the property key * * @param Expression|PrivateIdentifier $key Property key * * @return $this */ public function setKey($key) { $this->assertType($key, array("Expression", "PrivateIdentifier")); $this->key = $key; return $this; } /** * Returns the property value * * @return Expression|null */ public function getValue() { return $this->value; } /** * Sets the property value * * @param Expression|null $value Property value * * @return $this */ public function setValue($value) { $this->assertType($value, "Expression", true); $this->value = $value; return $this; } /** * Returns the property computed flag that is true when the property is * declared using the square brackets syntax * * @return bool */ public function getComputed() { return $this->computed; } /** * Sets the property computed flag that is true when the property is * declared using the square brackets syntax * * @param bool $computed Property computed flag * * @return $this */ public function setComputed($computed) { $this->computed = (bool) $computed; return $this; } /** * Returns the property static flag that is true when the property is * declared static * * @return bool */ public function getStatic() { return $this->static; } /** * Sets the property static flag that is true when the property is * declared static * * @param bool $static Property static flag * * @return $this */ public function setStatic($static) { $this->static = (bool) $static; return $this; } } * * For the full copyright and license information refer to the LICENSE file * distributed with this source code */ namespace Peast\Syntax\Node; /** * A node that represents a namespace import specifier. * For example "* as test" in: import * as test from "test.js". * * @author Marco Marchiò */ class ImportDefaultSpecifier extends ModuleSpecifier { } * * For the full copyright and license information refer to the LICENSE file * distributed with this source code */ namespace Peast\Syntax\Node; /** * A node that represents a with statement. * For example: with (test) {} * * @author Marco Marchiò */ class WithStatement extends Node implements Statement { /** * Map of node properties * * @var array */ protected $propertiesMap = array( "object" => true, "body" => true ); /** * The statement subject * * @var Expression */ protected $object; /** * The statement body * * @var Expression */ protected $body; /** * Returns the statement subject * * @return Expression */ public function getObject() { return $this->object; } /** * Sets the statement subject * * @param Expression $object Statement subject * * @return $this */ public function setObject(Expression $object) { $this->object = $object; return $this; } /** * Returns the statement body * * @return Expression */ public function getBody() { return $this->body; } /** * Sets the statement body * * @param Statement $body Statement body * * @return $this */ public function setBody(Statement $body) { $this->body = $body; return $this; } } * * For the full copyright and license information refer to the LICENSE file * distributed with this source code */ namespace Peast\Syntax\Node; /** * Abstract class for functions. * * @author Marco Marchiò */ abstract class Function_ extends Node { /** * Map of node properties * * @var array */ protected $propertiesMap = array( "id" => true, "params" => true, "body" => true, "generator" => false, "async" => false ); /** * Function name * * @var Identifier */ protected $id; /** * Function parameters array * * @var Pattern[] */ protected $params = array(); /** * Function body * * @var BlockStatement */ protected $body; /** * Generator flag that is true when the function is a generator * * @var bool */ protected $generator = false; /** * Async flag that is true when it is an async function * * @var bool */ protected $async = false; /** * Returns function name * * @return Identifier */ public function getId() { return $this->id; } /** * Sets function name * * @param Identifier $id Function name * * @return $this */ public function setId($id) { $this->assertType($id, "Identifier", true); $this->id = $id; return $this; } /** * Returns function parameters array * * @return Pattern[] */ public function getParams() { return $this->params; } /** * Sets function parameters array * * @param Pattern[] $params Function parameters array * * @return $this */ public function setParams($params) { $this->assertArrayOf($params, "Pattern"); $this->params = $params; return $this; } /** * Returns function body * * @return BlockStatement */ public function getBody() { return $this->body; } /** * Sets function body * * @param BlockStatement $body Function body * * @return $this */ public function setBody($body) { $this->assertType($body, "BlockStatement"); $this->body = $body; return $this; } /** * Returns the generator flag that is true when the function is a generator * * @return bool */ public function getGenerator() { return $this->generator; } /** * Sets the generator flag that is true when the function is a generator * * @param bool $generator Generator flag * * @return $this */ public function setGenerator($generator) { $this->generator = (bool) $generator; return $this; } /** * Returns the async flag that is true when it is an async function * * @return bool */ public function getAsync() { return $this->async; } /** * Sets the async flag that is true when it is an async function * * @param bool $async Async flag * * @return $this */ public function setAsync($async) { $this->async = (bool) $async; return $this; } } * * For the full copyright and license information refer to the LICENSE file * distributed with this source code */ namespace Peast\Syntax\Node; /** * A node that represents a binary expression. * For example: a + b * * @author Marco Marchiò */ class BinaryExpression extends Node implements Expression { /** * Map of node properties * * @var array */ protected $propertiesMap = array( "left" => true, "operator" => false, "right" => true ); /** * Operator * * @var string */ protected $operator; /** * Left expression * * @var Expression|PrivateIdentifier */ protected $left; /** * Right expression * * @var Expression */ protected $right; /** * Returns the operator * * @return string */ public function getOperator() { return $this->operator; } /** * Sets the operator * * @param string $operator Operator * * @return $this */ public function setOperator($operator) { $this->operator = $operator; return $this; } /** * Returns the left expression * * @return Expression|PrivateIdentifier */ public function getLeft() { return $this->left; } /** * Sets the left expression * * @param Expression|PrivateIdentifier $left Left expression * * @return $this */ public function setLeft($left) { $this->assertType($left, array("Expression", "PrivateIdentifier")); $this->left = $left; return $this; } /** * Returns the right expression * * @return Expression */ public function getRight() { return $this->right; } /** * Sets the right expression * * @param Expression $right Right expression * * @return $this */ public function setRight(Expression $right) { $this->right = $right; return $this; } } * * For the full copyright and license information refer to the LICENSE file * distributed with this source code */ namespace Peast\Syntax\Node; /** * A node that represents a for statement. * * @author Marco Marchiò */ class ForStatement extends Node implements Statement { /** * Map of node properties * * @var array */ protected $propertiesMap = array( "init" => true, "test" => true, "update" => true, "body" => true ); /** * Initializer * * @var VariableDeclaration|Expression */ protected $init; /** * Test expression * * @var Expression */ protected $test; /** * Update expression * * @var Expression */ protected $update; /** * Loop body * * @var Statement */ protected $body; /** * Returns the initializer * * @return VariableDeclaration|Expression */ public function getInit() { return $this->init; } /** * Sets the initializer * * @param VariableDeclaration|Expression $init Initializer * * @return $this */ public function setInit($init) { $this->assertType( $init, array("VariableDeclaration", "Expression"), true ); $this->init = $init; return $this; } /** * Returns the test expression * * @return Expression */ public function getTest() { return $this->test; } /** * Sets the test expression * * @param Expression $test Test expression * * @return $this */ public function setTest($test) { $this->assertType($test, "Expression", true); $this->test = $test; return $this; } /** * Returns the update expression * * @return Expression */ public function getUpdate() { return $this->update; } /** * Sets the update expression * * @param Expression $update Update expression * * @return $this */ public function setUpdate($update) { $this->assertType($update, "Expression", true); $this->update = $update; return $this; } /** * Returns the loop body * * @return Statement */ public function getBody() { return $this->body; } /** * Sets the loop body * * @param Statement $body Loop body * * @return $this */ public function setBody(Statement $body) { $this->body = $body; return $this; } } * * For the full copyright and license information refer to the LICENSE file * distributed with this source code */ namespace Peast\Syntax\Node; /** * A node that represents the rest element in array binding patterns or function * parameters. * For example "...rest" in: [a, ...rest] = b * * @author Marco Marchiò */ class RestElement extends Node implements Pattern { /** * Map of node properties * * @var array */ protected $propertiesMap = array( "argument" => true ); /** * The node's argument * * @var Pattern */ protected $argument; /** * Returns the node's argument * * @return Pattern */ public function getArgument() { return $this->argument; } /** * Sets the node's argument * * @param Pattern $argument Node's argument * * @return $this */ public function setArgument(Pattern $argument) { $this->argument = $argument; return $this; } } * * For the full copyright and license information refer to the LICENSE file * distributed with this source code */ namespace Peast\Syntax\Node; /** * A node that represents a numeric literal. * * @author Marco Marchiò */ class NumericLiteral extends Literal { /** * Map of node properties * * @var array */ protected $propertiesMap = array( "format" => false ); //Format constants /** * Decimal number format */ const DECIMAL = "decimal"; /** * Hexadecimal number format */ const HEXADECIMAL = "hexadecimal"; /** * Octal number format */ const OCTAL = "octal"; /** * Binary number format */ const BINARY = "binary"; /** * Node's numeric format * * @var string */ protected $format = self::DECIMAL; /** * Numeric forms conversion rules * * @var array */ protected $forms = array( "b" => array( "check" => "/^0b[01]+[01_]*$/i", "conv" => "bindec", "format" => self::BINARY ), "o" => array( "check" => "/^0o[0-7]+[0-7_]*$/i", "conv" => "octdec", "format" => self::OCTAL ), "x" => array( "check" => "/^0x[0-9a-f]+[0-9a-f_]*$/i", "conv" => "hexdec", "format" => self::HEXADECIMAL ), ); /** * Sets node's value * * @param float $value Value * * @return $this */ public function setValue($value) { $value = (float) $value; $intValue = (int) $value; if ($value == $intValue) { $value = $intValue; } $this->value = $value; //Force recalculation of the raw value return $this->setFormat($this->format); } /** * Sets node's raw value * * @param mixed $raw Raw value * * @return $this * * @throws \Exception */ public function setRaw($raw) { $value = $raw; $format = self::DECIMAL; if (is_string($value) && $value !== "") { //Hexadecimal, binary or octal $startZero = $value[0] === "0"; $form = $startZero && isset($value[1]) ? strtolower($value[1]) : null; //Numeric separator cannot appear at the beginning or at the end of the number if (preg_match("/^_|_$/", $value)) { throw new \Exception("Invalid numeric value"); } elseif (isset($this->forms[$form])) { $formDef = $this->forms[$form]; if (!preg_match($formDef["check"], $value)) { throw new \Exception("Invalid " . $formDef["format"]); } $value = str_replace("_", "", $value); $value = $formDef["conv"]($value); $format = $formDef["format"]; } elseif ($startZero && preg_match("/^0[0-7_]+$/", $value)) { //Legacy octal form $value = str_replace("_", "", $value); $value = octdec($value); $format = self::OCTAL; } elseif ( preg_match("/^([\d_]*\.?[\d_]*)(?:e[+\-]?[\d_]+)?$/i", $value, $match) && $match[1] !== "" && $match[1] !== "." && !preg_match("/_e|e[+-]?_|_$/", $value) ) { $value = str_replace("_", "", $value); } else { throw new \Exception("Invalid numeric value"); } } elseif (!is_int($value) && !is_float($value)) { throw new \Exception("Invalid numeric value"); } $value = (float) $value; $intValue = (int) $value; if ($value == $intValue) { $value = $intValue; } $this->format = $format; $this->value = $value; $this->raw = $raw; return $this; } /** * Returns node's numeric format * * @return string */ public function getFormat() { return $this->format; } /** * Sets node's numeric format * * @param string $format Format, one of the format constants * * @return $this */ public function setFormat($format) { $this->format = $format; switch ($format) { case self::BINARY: $this->raw = "0b" . decbin($this->value); break; case self::OCTAL: $this->raw = "0o" . decoct($this->value); break; case self::HEXADECIMAL: $this->raw = "0x" . dechex($this->value); break; default: $this->raw = (string) $this->value; break; } return $this; } } * * For the full copyright and license information refer to the LICENSE file * distributed with this source code */ namespace Peast\Syntax\Node; /** * A node that represents a unary expression. * For example: !a * * @author Marco Marchiò */ class UnaryExpression extends Node implements Expression { /** * Map of node properties * * @var array */ protected $propertiesMap = array( "argument" => true, "operator" => false, "prefix" => false ); /** * Expression's operator * * @var string */ protected $operator; /** * Prefix flag that is always true since the operator precedes the argument * * @var bool */ protected $prefix = true; /** * Expression's argument * * @var Expression */ protected $argument; /** * Returns the expression's operator * * @return string */ public function getOperator() { return $this->operator; } /** * Sets the expression's operator * * @param string $operator Operator * * @return $this */ public function setOperator($operator) { $this->operator = $operator; return $this; } /** * Returns the prefix flag that is always true since the operator precedes * the argument * * @return bool */ public function getPrefix() { return $this->prefix; } /** * This method does nothing since the prefix flag is always true for * unary expressions * * @param bool $prefix Prefix flag * * @return $this * * @codeCoverageIgnore */ public function setPrefix($prefix) { return $this; } /** * Returns the expression's argument * * @return Expression */ public function getArgument() { return $this->argument; } /** * Sets the expression's argument * * @param Expression $argument Argument * * @return $this */ public function setArgument(Expression $argument) { $this->argument = $argument; return $this; } } * * For the full copyright and license information refer to the LICENSE file * distributed with this source code */ namespace Peast\Syntax\Node; /** * A node that represents a try-catch statement. * * @author Marco Marchiò */ class TryStatement extends Node implements Statement { /** * Map of node properties * * @var array */ protected $propertiesMap = array( "block" => true, "handler" => true, "finalizer" => true ); /** * Wrapped block * * @var BlockStatement */ protected $block; /** * Catch clause * * @var CatchClause */ protected $handler; /** * "finally" block * * @var BlockStatement */ protected $finalizer; /** * Returns the wrapped block * * @return BlockStatement */ public function getBlock() { return $this->block; } /** * Sets the wrapped block * * @param BlockStatement $block Wrapped block * * @return $this */ public function setBlock(BlockStatement $block) { $this->block = $block; return $this; } /** * Returns the catch clause * * @return CatchClause */ public function getHandler() { return $this->handler; } /** * Sets the catch clause * * @param CatchClause $handler Catch clause * * @return $this */ public function setHandler($handler) { $this->assertType($handler, "CatchClause", true); $this->handler = $handler; return $this; } /** * Returns the "finally" block * * @return BlockStatement */ public function getFinalizer() { return $this->finalizer; } /** * Sets the "finally" block * * @param BlockStatement $finalizer The "finally" block * * @return $this */ public function setFinalizer($finalizer) { $this->assertType($finalizer, "BlockStatement", true); $this->finalizer = $finalizer; return $this; } } * * For the full copyright and license information refer to the LICENSE file * distributed with this source code */ namespace Peast\Syntax\Node; /** * A node that represents a default import specifier. * For example "test" in: import test from "test.js". * * @author Marco Marchiò */ class ImportNamespaceSpecifier extends ModuleSpecifier { } * * For the full copyright and license information refer to the LICENSE file * distributed with this source code */ namespace Peast\Syntax\Node; /** * A node that represents an await expression. * * @author Marco Marchiò */ class AwaitExpression extends Node implements Expression { /** * Map of node properties * * @var array */ protected $propertiesMap = array( "argument" => true ); /** * Expression's argument * * @var Expression */ protected $argument; /** * Returns the expression's argument * * @return Expression */ public function getArgument() { return $this->argument; } /** * Sets the expression's argument * * @param Expression $argument Argument * * @return $this */ public function setArgument(Expression $argument) { $this->argument = $argument; return $this; } } * * For the full copyright and license information refer to the LICENSE file * distributed with this source code */ namespace Peast\Syntax\Node; /** * A node that represents an export named declaration. * For example: export {foo} from "bar" * * @author Marco Marchiò */ class ExportNamedDeclaration extends Node implements ModuleDeclaration { /** * Map of node properties * * @var array */ protected $propertiesMap = array( "declaration" => true, "specifiers" => true, "source" => true ); /** * Exported declaration * * @var Declaration */ protected $declaration; /** * Exported specifiers * * @var ExportSpecifier[] */ protected $specifiers = array(); /** * The export source * * @var Literal */ protected $source; /** * Returns the exported declaration * * @return Declaration */ public function getDeclaration() { return $this->declaration; } /** * Sets the exported declaration * * @param Declaration $declaration Exported declaration * * @return $this */ public function setDeclaration($declaration) { $this->assertType($declaration, "Declaration", true); $this->declaration = $declaration; return $this; } /** * Return the exported specifiers * * @return ExportSpecifier[] */ public function getSpecifiers() { return $this->specifiers; } /** * Sets the exported specifiers * * @param ExportSpecifier[] $specifiers Exported specifiers * * @return $this */ public function setSpecifiers($specifiers) { $this->assertArrayOf($specifiers, "ExportSpecifier"); $this->specifiers = $specifiers; return $this; } /** * Returns the export source * * @return Literal */ public function getSource() { return $this->source; } /** * Sets the export source * * @param Literal $source Export source * * @return $this */ public function setSource($source) { $this->assertType($source, "Literal", true); $this->source = $source; return $this; } } * * For the full copyright and license information refer to the LICENSE file * distributed with this source code */ namespace Peast\Syntax\Node; /** * A node that represents an array literal. * For example: [a, b, c] * * @author Marco Marchiò */ class ArrayExpression extends Node implements Expression { /** * Map of node properties * * @var array */ protected $propertiesMap = array( "elements" => true ); /** * Array elements * * @var Expression[]|SpreadElement[] */ protected $elements = array(); /** * Returns array elements * * @return Expression[]|SpreadElement[] */ public function getElements() { return $this->elements; } /** * Sets array elements * * @param Expression[]|SpreadElement[] $elements Array elements to set * * @return $this */ public function setElements($elements) { $this->assertArrayOf( $elements, array("Expression", "SpreadElement"), true ); $this->elements = $elements; return $this; } } * * For the full copyright and license information refer to the LICENSE file * distributed with this source code */ namespace Peast\Syntax\Node; /** * A node that represents a function declaration * For example: function test () {} * * @author Marco Marchiò */ class FunctionDeclaration extends Function_ implements Declaration { /** * Sets the function identifier * * @param Identifier $id Function identifier * * @return $this */ public function setId($id) { $this->assertType($id, "Identifier"); $this->id = $id; return $this; } } * * For the full copyright and license information refer to the LICENSE file * distributed with this source code */ namespace Peast\Syntax\Node; /** * A node that represents a meta property. * For example: new.target * * @author Marco Marchiò */ class MetaProperty extends Node implements Expression { /** * Map of node properties * * @var array */ protected $propertiesMap = array( "meta" => false, "property" => false ); /** * Subject * * @var string */ protected $meta; /** * Property * * @var string */ protected $property; /** * Returns the subject * * @return string */ public function getMeta() { return $this->meta; } /** * Sets the subject * * @param string $meta Subject * * @return $this */ public function setMeta($meta) { $this->meta = $meta; return $this; } /** * Returns the property * * @return string */ public function getProperty() { return $this->property; } /** * Sets the property * * @param string $property Property * * @return $this */ public function setProperty($property) { $this->property = $property; return $this; } } * * For the full copyright and license information refer to the LICENSE file * distributed with this source code */ namespace Peast\Syntax\Node; /** * A node that represents an if statement. * For example: if (test) {} else {} * * @author Marco Marchiò */ class IfStatement extends Node implements Statement { /** * Map of node properties * * @var array */ protected $propertiesMap = array( "test" => true, "consequent" => true, "alternate" => true ); /** * The test expression * * @var Expression */ protected $test; /** * The statement that is activated if the test expression is true * * @var Statement|FunctionDeclaration */ protected $consequent; /** * The "else" statement * * @var Statement */ protected $alternate; /** * Returns the test expression * * @return Expression */ public function getTest() { return $this->test; } /** * Sets the test expression * * @param Expression $test Test expression * * @return $this */ public function setTest(Expression $test) { $this->test = $test; return $this; } /** * Returns the statement that is activated if the test expression is true * * @return Statement|FunctionDeclaration */ public function getConsequent() { return $this->consequent; } /** * Sets the statement that is activated if the test expression is true * * @param Statement|FunctionDeclaration $consequent The consequent expression * * @return $this */ public function setConsequent($consequent) { $this->assertType( $consequent, array("Statement", "FunctionDeclaration"), true ); $this->consequent = $consequent; return $this; } /** * Returns the "else" statement * * @return Statement */ public function getAlternate() { return $this->alternate; } /** * Sets the "else" statement * * @param Statement|FunctionDeclaration $alternate The "else" statement * * @return $this */ public function setAlternate($alternate) { $this->assertType( $alternate, array("Statement", "FunctionDeclaration"), true ); $this->alternate = $alternate; return $this; } } * * For the full copyright and license information refer to the LICENSE file * distributed with this source code */ namespace Peast\Syntax\Node; /** * A node that represents a class declaration. * For example: class test {} * * @author Marco Marchiò */ class ClassDeclaration extends Class_ implements Declaration { /** * Sets the class identifier * * @param Identifier $id Class identifier * * @return $this */ public function setId($id) { $this->assertType($id, "Identifier"); $this->id = $id; return $this; } } * * For the full copyright and license information refer to the LICENSE file * distributed with this source code */ namespace Peast\Syntax\Node; /** * A node that represents a declaration in a VariableDeclaration node. * For example "a=1" in: var a = 1 * * @author Marco Marchiò */ class VariableDeclarator extends Node { /** * Map of node properties * * @var array */ protected $propertiesMap = array( "id" => true, "init" => true ); /** * Declaration identifier or pattern * * @var Pattern */ protected $id; /** * Optional initializer * * @var Expression */ protected $init; /** * Returns the declaration identifier or pattern * * @return Pattern */ public function getId() { return $this->id; } /** * Sets the declaration identifier or pattern * * @param Pattern $id Declaration identifier or pattern * * @return $this */ public function setId(Pattern $id) { $this->id = $id; return $this; } /** * Returns the initializer * * @return Expression */ public function getInit() { return $this->init; } /** * Sets the initializer * * @param Expression $init Initializer * * @return $this */ public function setInit($init) { $this->assertType($init, "Expression", true); $this->init = $init; return $this; } } * * For the full copyright and license information refer to the LICENSE file * distributed with this source code */ namespace Peast\Syntax\Node; /** * A node that represents a private identifier. * * @author Marco Marchiò */ class PrivateIdentifier extends Node { /** * Map of node properties * * @var array */ protected $propertiesMap = array( "name" => false ); /** * The identifier's name * * @var string */ protected $name; /** * Returns the identifier's name * * @return string */ public function getName() { return $this->name; } /** * Sets the identifier's name * * @param string $name The name to set * * @return $this */ public function setName($name) { $this->name = $name; return $this; } } * * For the full copyright and license information refer to the LICENSE file * distributed with this source code */ namespace Peast\Syntax\Node\JSX; use Peast\Syntax\Node\Identifier; /** * A node that represents a JSX identifier. * * @author Marco Marchiò */ class JSXIdentifier extends Identifier { } * * For the full copyright and license information refer to the LICENSE file * distributed with this source code */ namespace Peast\Syntax\Node\JSX; use Peast\Syntax\Node\Node; /** * A node that represents a JSX attribute. * * @author Marco Marchiò */ class JSXAttribute extends Node { /** * Map of node properties * * @var array */ protected $propertiesMap = array( "name" => true, "value" => true ); /** * Attribute name * * @var JSXIdentifier|JSXNamespacedName */ protected $name; /** * Attribute value * * @var Node|null */ protected $value; /** * Returns the attribute name * * @return Node */ public function getName() { return $this->name; } /** * Sets the attribute name * * @param JSXIdentifier|JSXNamespacedName $name Attribute name * * @return $this */ public function setName($name) { $this->assertType($name, array("JSX\\JSXIdentifier", "JSX\\JSXNamespacedName")); $this->name = $name; return $this; } /** * Returns the attribute value * * @return Node|null */ public function getValue() { return $this->value; } /** * Sets the attribute value * * @param Node|null $value Attribute value * * @return $this */ public function setValue($value) { $this->assertType( $value, array( "Literal", "JSX\\JSXExpressionContainer", "JSX\\JSXElement", "JSX\\JSXFragment" ), true ); $this->value = $value; return $this; } } * * For the full copyright and license information refer to the LICENSE file * distributed with this source code */ namespace Peast\Syntax\Node\JSX; use Peast\Syntax\Node\SpreadElement; /** * A node that represents a JSX spread attribute. * * @author Marco Marchiò */ class JSXSpreadAttribute extends SpreadElement { } * * For the full copyright and license information refer to the LICENSE file * distributed with this source code */ namespace Peast\Syntax\Node\JSX; /** * A node that represents a JSX closing element tag. * * @author Marco Marchiò */ class JSXClosingElement extends JSXBoundaryElement { } * * For the full copyright and license information refer to the LICENSE file * distributed with this source code */ namespace Peast\Syntax\Node\JSX; use Peast\Syntax\Node\Node; /** * A node that represents an expression container in JSX. * * @author Marco Marchiò */ class JSXExpressionContainer extends Node { /** * Map of node properties * * @var array */ protected $propertiesMap = array( "expression" => true ); /** * The wrapped expression * * @var \Peast\Syntax\Node\Expression|JSXEmptyExpression */ protected $expression; /** * Returns the wrapped expression * * @return \Peast\Syntax\Node\Expression|JSXEmptyExpression */ public function getExpression() { return $this->expression; } /** * Sets the wrapped expression * * @param \Peast\Syntax\Node\Expression|JSXEmptyExpression $expression Wrapped * expression * * @return $this */ public function setExpression($expression) { $this->assertType( $expression, array("Expression", "JSX\\JSXEmptyExpression") ); $this->expression = $expression; return $this; } } * * For the full copyright and license information refer to the LICENSE file * distributed with this source code */ namespace Peast\Syntax\Node\JSX; use Peast\Syntax\Node\Node; use Peast\Syntax\Node\Expression; /** * A node that represents a spread child expression in JSX. * * @author Marco Marchiò */ class JSXSpreadChild extends Node { /** * Map of node properties * * @var array */ protected $propertiesMap = array( "expression" => true ); /** * The wrapped expression * * @var Expression */ protected $expression; /** * Returns the wrapped expression * * @return Expression */ public function getExpression() { return $this->expression; } /** * Sets the wrapped expression * * @param Expression $expression Wrapped expression * * @return $this */ public function setExpression(Expression $expression) { $this->expression = $expression; return $this; } } * * For the full copyright and license information refer to the LICENSE file * distributed with this source code */ namespace Peast\Syntax\Node\JSX; use Peast\Syntax\Node\Node; /** * A node that represents a JSX closing fragment tag. * * @author Marco Marchiò */ class JSXText extends Node { /** * Map of node properties * * @var array */ protected $propertiesMap = array( "value" => false, "raw" => false ); /** * Node's value * * @var mixed */ protected $value; /** * Node's raw value * * @var string */ protected $raw; /** * Returns node's value * * @return mixed */ public function getValue() { return $this->value; } /** * Sets node's value * * @param mixed $value Value * * @return $this */ public function setValue($value) { $this->value = $value; $this->raw = $value; return $this; } /** * Return node's raw value * * @return string */ public function getRaw() { return $this->raw; } /** * Sets node's raw value * * @param mixed $raw Raw value * * @return $this */ public function setRaw($raw) { return $this->setValue($raw); } } * * For the full copyright and license information refer to the LICENSE file * distributed with this source code */ namespace Peast\Syntax\Node\JSX; use Peast\Syntax\Node\Node; /** * A node that represents a JSX opening fragment tag. * * @author Marco Marchiò */ class JSXOpeningFragment extends Node { } * * For the full copyright and license information refer to the LICENSE file * distributed with this source code */ namespace Peast\Syntax\Node\JSX; use Peast\Syntax\Node\Node; use Peast\Syntax\Node\Expression; /** * A node that represents a JSX fragment. * * @author Marco Marchiò */ class JSXFragment extends Node implements Expression { /** * Map of node properties * * @var array */ protected $propertiesMap = array( "openingFragment" => true, "children" => true, "closingFragment" => true ); /** * Opening fragment node * * @var JSXOpeningFragment */ protected $openingFragment; /** * Children nodes array * * @var Node[] */ protected $children = array(); /** * Closing fragment node * * @var JSXClosingFragment */ protected $closingFragment; /** * Returns the opening fragment node * * @return JSXOpeningFragment */ public function getOpeningFragment() { return $this->openingFragment; } /** * Sets the opening fragment node * * @param JSXOpeningFragment $openingFragment Opening fragment node * * @return $this */ public function setOpeningFragment(JSXOpeningFragment $openingFragment) { $this->openingFragment = $openingFragment; return $this; } /** * Returns the children nodes array * * @return Node[] */ public function getChildren() { return $this->children; } /** * Sets the children nodes array * * @param Node[] $children Children nodes array * * @return $this */ public function setChildren($children) { $this->assertArrayOf($children, array( "JSX\\JSXText", "JSX\\JSXExpressionContainer", "JSX\\JSXSpreadChild", "JSX\\JSXElement", "JSX\\JSXFragment" )); $this->children = $children; return $this; } /** * Returns the closing fragment node * * @return JSXClosingFragment */ public function getClosingFragment() { return $this->closingFragment; } /** * Sets the closing fragment node * * @param JSXClosingFragment $closingFragment Closing fragment node * * @return $this */ public function setClosingFragment(JSXClosingFragment $closingFragment) { $this->closingFragment = $closingFragment; return $this; } } * * For the full copyright and license information refer to the LICENSE file * distributed with this source code */ namespace Peast\Syntax\Node\JSX; use Peast\Syntax\Node\Node; /** * A node that represents a JSX closing fragment tag. * * @author Marco Marchiò */ class JSXClosingFragment extends Node { } * * For the full copyright and license information refer to the LICENSE file * distributed with this source code */ namespace Peast\Syntax\Node\JSX; use Peast\Syntax\Node\Node; use Peast\Syntax\Node\Expression; /** * A node that represents a JSX element. * * @author Marco Marchiò */ class JSXElement extends Node implements Expression { /** * Map of node properties * * @var array */ protected $propertiesMap = array( "openingElement" => true, "children" => true, "closingElement" => true ); /** * Opening element node * * @var JSXOpeningElement */ protected $openingElement; /** * Children nodes array * * @var Node[] */ protected $children = array(); /** * Closing element node * * @var JSXClosingElement|null */ protected $closingElement; /** * Returns the opening element node * * @return JSXOpeningElement */ public function getOpeningElement() { return $this->openingElement; } /** * Sets the opening element node * * @param JSXOpeningElement $openingElement Opening element node * * @return $this */ public function setOpeningElement(JSXOpeningElement $openingElement) { $this->openingElement = $openingElement; return $this; } /** * Returns the children nodes array * * @return Node[] */ public function getChildren() { return $this->children; } /** * Sets the children nodes array * * @param Node[] $children Children nodes array * * @return $this */ public function setChildren($children) { $this->assertArrayOf($children, array( "JSX\\JSXText", "JSX\\JSXExpressionContainer", "JSX\\JSXSpreadChild", "JSX\\JSXElement", "JSX\\JSXFragment" )); $this->children = $children; return $this; } /** * Returns the closing element node * * @return JSXClosingElement|null */ public function getClosingElement() { return $this->closingElement; } /** * Sets the closing element node * * @param JSXClosingElement|null $closingElement Closing element node * * @return $this */ public function setClosingElement($closingElement) { $this->assertType($closingElement, "JSX\\JSXClosingElement", true); $this->closingElement = $closingElement; return $this; } } * * For the full copyright and license information refer to the LICENSE file * distributed with this source code */ namespace Peast\Syntax\Node\JSX; use Peast\Syntax\Node\Node; /** * A node that represents a JSX empty expression. * * @author Marco Marchiò */ class JSXEmptyExpression extends Node { } * * For the full copyright and license information refer to the LICENSE file * distributed with this source code */ namespace Peast\Syntax\Node\JSX; use Peast\Syntax\Node\Node; use Peast\Syntax\Node\Expression; /** * A node that represents a JSX namespaced name. * * @author Marco Marchiò */ class JSXNamespacedName extends Node implements Expression { /** * Map of node properties * * @var array */ protected $propertiesMap = array( "namespace" => false, "name" => false ); /** * Node's namespace * * @var JSXIdentifier */ protected $namespace; /** * Node's name * * @var JSXIdentifier */ protected $name; /** * Returns node's namespace * * @return JSXIdentifier */ public function getNamespace() { return $this->namespace; } /** * Sets node's namespace * * @param JSXIdentifier $namespace Namespace * * @return $this */ public function setNamespace($namespace) { $this->namespace = $namespace; return $this; } /** * Return node's name * * @return JSXIdentifier */ public function getName() { return $this->name; } /** * Sets node's name * * @param JSXIdentifier $name Name * * @return $this */ public function setName($name) { $this->name = $name; return $this; } } * * For the full copyright and license information refer to the LICENSE file * distributed with this source code */ namespace Peast\Syntax\Node\JSX; /** * A node that represents a JSX opening element tag. * * @author Marco Marchiò */ class JSXOpeningElement extends JSXBoundaryElement { /** * Map of node properties * * @var array */ protected $propertiesMap = array( "attributes" => true, "selfClosing" => false ); /** * Children nodes array * * @var JSXAttribute[]|JSXSpreadAttribute[] */ protected $attributes = array(); /** * Self closing tag mode * * @var bool */ protected $selfClosing = false; /** * Returns the children attributes array * * @return JSXAttribute[]|JSXSpreadAttribute[] */ public function getAttributes() { return $this->attributes; } /** * Sets the attributes nodes array * * @param JSXAttribute[]|JSXSpreadAttribute[] $attributes Attributes nodes * array * * @return $this */ public function setAttributes($attributes) { $this->assertArrayOf($attributes, array( "JSX\\JSXAttribute", "JSX\\JSXSpreadAttribute" )); $this->attributes = $attributes; return $this; } /** * Returns the self closing tag mode * * @return bool */ public function getSelfClosing() { return $this->selfClosing; } /** * Sets the self closing tag mode * * @param bool $selfClosing Self closing tag mode * * @return $this */ public function setSelfClosing($selfClosing) { $this->selfClosing = (bool) $selfClosing; return $this; } } * * For the full copyright and license information refer to the LICENSE file * distributed with this source code */ namespace Peast\Syntax\Node\JSX; use Peast\Syntax\Node\Node; /** * A base class for boundary elements. * * @author Marco Marchiò * * @abstract */ abstract class JSXBoundaryElement extends Node { /** * Map of node properties * * @var array */ protected $propertiesMap = array( "name" => true ); /** * Element name * * @var JSXIdentifier|JSXMemberExpression|JSXNamespacedName */ protected $name; /** * Returns the element name * * @return JSXIdentifier|JSXMemberExpression|JSXNamespacedName */ public function getName() { return $this->name; } /** * Sets the element name * * @param JSXIdentifier|JSXMemberExpression|JSXNamespacedName $name Element * name * * @return $this */ public function setName($name) { $this->assertType( $name, array("JSX\\JSXIdentifier", "JSX\\JSXMemberExpression", "JSX\\JSXNamespacedName") ); $this->name = $name; return $this; } } * * For the full copyright and license information refer to the LICENSE file * distributed with this source code */ namespace Peast\Syntax\Node\JSX; use Peast\Syntax\Node\Node; use Peast\Syntax\Node\Expression; /** * A node that represents a JSX member expression. * * @author Marco Marchiò */ class JSXMemberExpression extends Node implements Expression { /** * Map of node properties * * @var array */ protected $propertiesMap = array( "object" => true, "property" => true ); /** * Expression's object * * @var JSXMemberExpression|JSXIdentifier */ protected $object; /** * Expression's property * * @var JSXIdentifier */ protected $property; /** * Returns the expression's object * * @return JSXMemberExpression|JSXIdentifier */ public function getObject() { return $this->object; } /** * Sets the expression's object * * @param JSXMemberExpression|JSXIdentifier $object Object * * @return $this */ public function setObject($object) { $this->assertType($object, array("JSX\\JSXMemberExpression", "JSX\\JSXIdentifier")); $this->object = $object; return $this; } /** * Returns the expression's property * * @return JSXIdentifier */ public function getProperty() { return $this->property; } /** * Sets the expression's property * * @param JSXIdentifier $property Property * * @return $this */ public function setProperty(JSXIdentifier $property) { $this->property = $property; return $this; } } * * For the full copyright and license information refer to the LICENSE file * distributed with this source code */ namespace Peast\Syntax\Node; /** * A node that represents a method declaration in classes and object literals. * * @author Marco Marchiò */ class MethodDefinition extends Node { /** * Map of node properties * * @var array */ protected $propertiesMap = array( "key" => true, "value" => true, "kind" => false, "computed" => false, "static" => false ); //Kind constants /** * Constructor method */ const KIND_CONSTRUCTOR = "constructor"; /** * Standard method */ const KIND_METHOD = "method"; /** * Getter method */ const KIND_GET = "get"; /** * Setter method */ const KIND_SET = "set"; /** * Method's key * * @var Expression|PrivateIdentifier */ protected $key; /** * Method's value * * @var FunctionExpression */ protected $value; /** * Method's kind that is one of the kind constants * * @var string */ protected $kind = self::KIND_METHOD; /** * Computed flag that is true if method's key is declared using square * brackets syntax * * @var bool */ protected $computed = false; /** * Static flag that is true if the method is static * * @var bool */ protected $static = false; /** * Returns the method's key * * @return Expression|PrivateIdentifier */ public function getKey() { return $this->key; } /** * Sets the method's key * * @param Expression|PrivateIdentifier $key Method's key * * @return $this */ public function setKey($key) { $this->assertType($key, array("Expression", "PrivateIdentifier")); $this->key = $key; return $this; } /** * Returns the method's value * * @return FunctionExpression */ public function getValue() { return $this->value; } /** * Sets the method's value * * @param FunctionExpression $value Method's value * * @return $this */ public function setValue(FunctionExpression $value) { $this->value = $value; return $this; } /** * Returns the method's kind that is one of the kind constants * * @return string */ public function getKind() { return $this->kind; } /** * Sets the method's kind that is one of the kind constants * * @param string $kind Method's kind * * @return $this */ public function setKind($kind) { $this->kind = $kind; return $this; } /** * Returns the computed flag that is true if method's key is declared using * square brackets syntax * * @return bool */ public function getComputed() { return $this->computed; } /** * Sets the computed flag that is true if method's key is declared using * square brackets syntax * * @param bool $computed Computed flag * * @return $this */ public function setComputed($computed) { $this->computed = (bool) $computed; return $this; } /** * Returns the static flag that is true if the method is static * * @return bool */ public function getStatic() { return $this->{"static"}; } /** * Sets the static flag that is true if the method is static * * @param bool $static Static flag * * @return $this */ public function setStatic($static) { $this->{"static"} = (bool) $static; return $this; } } * * For the full copyright and license information refer to the LICENSE file * distributed with this source code */ namespace Peast\Syntax\Node; /** * A node that represents an expression statement and wraps another expression. * * @author Marco Marchiò */ class ExpressionStatement extends Node implements Statement { /** * Map of node properties * * @var array */ protected $propertiesMap = array( "expression" => true ); /** * Wrapped expression * * @var Expression */ protected $expression; /** * Returns the wrapped expression * * @return Expression */ public function getExpression() { return $this->expression; } /** * Sets the wrapped expression * * @param Expression $expression Wrapped expression * * @return $this */ public function setExpression(Expression $expression) { $this->expression = $expression; return $this; } } * * For the full copyright and license information refer to the LICENSE file * distributed with this source code */ namespace Peast\Syntax\Node; /** * A node that represents a class body. * * @author Marco Marchiò */ class ClassBody extends Node { /** * Map of node properties * * @var array */ protected $propertiesMap = array( "body" => true ); /** * Class methods and properties * * @var MethodDefinition[]|PropertyDefinition[]|StaticBlock[] */ protected $body = array(); /** * Returns class methods and properties * * @return MethodDefinition[]|PropertyDefinition[]|StaticBlock[] */ public function getBody() { return $this->body; } /** * Sets class methods and properties * * @param MethodDefinition[]|PropertyDefinition[]|StaticBlock[] $body Class methods array * * @return $this */ public function setBody($body) { $this->assertArrayOf( $body, array("MethodDefinition", "PropertyDefinition", "StaticBlock") ); $this->body = $body; return $this; } } * * For the full copyright and license information refer to the LICENSE file * distributed with this source code */ namespace Peast\Syntax\Node; /** * A node that represents a null literal. * * @author Marco Marchiò */ class NullLiteral extends Literal { /** * Node's value * * @var mixed */ protected $value = null; /** * Node's raw value * * @var string */ protected $raw = "null"; /** * Sets node's value * * @param mixed $value Value * * @return $this */ public function setValue($value) { return $this; } } * * For the full copyright and license information refer to the LICENSE file * distributed with this source code */ namespace Peast\Syntax\Node; /** * A node that represents a do-while loop. * For example: do {} while (test) * * @author Marco Marchiò */ class DoWhileStatement extends Node implements Statement { /** * Map of node properties * * @var array */ protected $propertiesMap = array( "body" => true, "test" => true ); /** * The loop body * * @var Statement */ protected $body; /** * The loop condition * * @var Expression */ protected $test; /** * Returns the loop body * * @return Statement */ public function getBody() { return $this->body; } /** * Sets the loop body * * @param Statement $body Loop body * * @return $this */ public function setBody(Statement $body) { $this->body = $body; return $this; } /** * Returns the loop condition * * @return Expression */ public function getTest() { return $this->test; } /** * Sets the loop condition * * @param Expression $test Loop * * @return $this */ public function setTest(Expression $test) { $this->test = $test; return $this; } } * * For the full copyright and license information refer to the LICENSE file * distributed with this source code */ namespace Peast\Syntax\Node; /** * A node that represents an object binding pattern. * For example: var {a, b, c} = d * * @author Marco Marchiò */ class ObjectPattern extends Node implements Pattern { /** * Map of node properties * * @var array */ protected $propertiesMap = array( "properties" => true ); /** * Object properties * * @var Property[] */ protected $properties = array(); /** * Returns object properties * * @return Property[] */ public function getProperties() { return $this->properties; } /** * Sets object properties * * @param Property[] $properties Object properties * * @return $this */ public function setProperties($properties) { $this->assertArrayOf($properties, array("AssignmentProperty", "RestElement")); $this->properties = $properties; return $this; } } * * For the full copyright and license information refer to the LICENSE file * distributed with this source code */ namespace Peast\Syntax\Node; /** * A node that represents the "for...of" statement. * For example: for (var a of b) {} * * @author Marco Marchiò */ class ForOfStatement extends ForInStatement { /** * Map of node properties * * @var array */ protected $propertiesMap = array( "left" => true, "right" => true, "body" => true, "await" => false ); /** * Async iteration flag * * @var bool */ protected $await = false; /** * Returns the async iteration flag * * @return bool */ public function getAwait() { return $this->await; } /** * Sets the async iteration flag * * @param bool $await Async iteration flag * * @return $this */ public function setAwait($await) { $this->await = (bool) $await; return $this; } } * * For the full copyright and license information refer to the LICENSE file * distributed with this source code */ namespace Peast\Syntax\Node; /** * Interface that every pattern node must implement. * * @author Marco Marchiò */ interface Pattern { } * * For the full copyright and license information refer to the LICENSE file * distributed with this source code */ namespace Peast\Syntax\Node; use Peast\Syntax\SourceLocation; use Peast\Syntax\Position; /** * Base class for all the nodes generated by Peast. * * @author Marco Marchiò * * @abstract */ abstract class Node implements \JSONSerializable { /** * Map of node properties * * @var array */ protected $propertiesMap = array( "type" => false, "location" => false, "leadingComments" => false, "trailingComments" => false ); /** * Node location in the source code * * @var SourceLocation */ public $location; /** * Leading comments array * * @var Comment[] */ protected $leadingComments = array(); /** * Trailing comments array * * @var Comment[] */ protected $trailingComments = array(); /** * Class constructor */ public function __construct() { $this->location = new SourceLocation; } /** * Returns node type * * @return string */ public function getType() { $class = explode("\\", get_class($this)); return array_pop($class); } /** * Sets leading comments array * * @param Comment[] $comments Comments array * * @return $this */ public function setLeadingComments($comments) { $this->assertArrayOf($comments, "Comment"); $this->leadingComments = $comments; return $this; } /** * Returns leading comments array * * @return Comment[] */ public function getLeadingComments() { return $this->leadingComments; } /** * Sets trailing comments array * * @param Comment[] $comments Comments array * * @return $this */ public function setTrailingComments($comments) { $this->assertArrayOf($comments, "Comment"); $this->trailingComments = $comments; return $this; } /** * Returns trailing comments array * * @return Comment[] */ public function getTrailingComments() { return $this->trailingComments; } /** * Returns node location in the source code * * @return SourceLocation */ public function getLocation() { return $this->location; } /** * Sets the start position of the node in the source code * * @param Position $position Start position * * @return $this */ public function setStartPosition(Position $position) { $this->location->start = $position; return $this; } /** * Sets the end position of the node in the source code * * @param Position $position Start position * * @return $this */ public function setEndPosition(Position $position) { $this->location->end = $position; return $this; } /** * Traverses the current node and all its child nodes using the given * function * * @param callable $fn Function that will be called on each node * @param array $options Options array. See Traverser class * documentation for available options * * @return $this */ public function traverse(callable $fn, $options = array()) { $traverser = new \Peast\Traverser($options); $traverser->addFunction($fn)->traverse($this); return $this; } /** * Returns a serializable version of the node * * @return array */ #[\ReturnTypeWillChange] public function jsonSerialize() { $ret = array(); $props = \Peast\Syntax\Utils::getNodeProperties($this); foreach ($props as $prop) { $ret[$prop["name"]] = $this->{$prop["getter"]}(); } return $ret; } /** * Renders the current node * * @param \Peast\Formatter\Base $formatter Formatter to use for the * rendering * * @return string */ public function render(\Peast\Formatter\Base $formatter) { $renderer = new \Peast\Renderer(); return $renderer->setFormatter($formatter)->render($this); } /** * Asserts that the given value is an array of defined type * * @param mixed $params Value to check * @param string|array $classes Class or array of classes to check against * @param bool $allowNull If true, null values are allowed * * @return void * * @codeCoverageIgnore */ protected function assertArrayOf($params, $classes, $allowNull = false) { if (!is_array($classes)) { $classes = array($classes); } if (!is_array($params)) { $this->typeError($params, $classes, $allowNull, true); } else { foreach ($params as $param) { foreach ($classes as $class) { if ($param === null && $allowNull) { continue 2; } else { $c = "Peast\\Syntax\\Node\\$class"; if ($param instanceof $c) { continue 2; } } } $this->typeError($param, $classes, $allowNull, true, true); } } } /** * Asserts that the given value respects the defined type * * @param mixed $param Value to check * @param string|array $classes Class or array of classes to check against * @param bool $allowNull If true, null values are allowed * * @return void * * @codeCoverageIgnore */ protected function assertType($param, $classes, $allowNull = false) { if (!is_array($classes)) { $classes = array($classes); } if ($param === null) { if (!$allowNull) { $this->typeError($param, $classes, $allowNull); } } else { foreach ($classes as $class) { $c = "Peast\\Syntax\\Node\\$class"; if ($param instanceof $c) { return; } } $this->typeError($param, $classes, $allowNull); } } /** * Throws an error if the defined type is not supported b * * @param mixed $param The value to check * @param mixed $allowedTypes Class or array of classes to check against * @param bool $allowNull If true, null values are allowed * @param bool $array If true, the value must be an array * @param bool $inArray If true, the value is an array but the content * does not respects the type * * @return void * * @throws \TypeError * * @codeCoverageIgnore */ protected function typeError( $param, $allowedTypes, $allowNull = false, $array = false, $inArray = false ) { $backtrace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS); $method = $backtrace[2]["class"] . "::" . $backtrace[2]["function"]; $msg = "Argument 0 passed to $method must be "; if ($array) { $msg .= "an array of "; } $msg .= implode(" or ", $allowedTypes); if ($allowNull) { $msg .= " or null"; } if (is_object($param)) { $parts = explode("\\", get_class($param)); $type = array_pop($parts); } else { $type = gettype($param); } if ($inArray) { $type = "array of $type"; } $msg .= ", $type given"; if (version_compare(phpversion(), '7', '>=')) { throw new \TypeError($msg); } else { trigger_error($msg, E_USER_ERROR); } } } * * For the full copyright and license information refer to the LICENSE file * distributed with this source code */ namespace Peast\Syntax\Node; /** * A node that represents a tagged template expression. * For example: fn`template` * * @author Marco Marchiò */ class TaggedTemplateExpression extends Node implements Expression { /** * Map of node properties * * @var array */ protected $propertiesMap = array( "tag" => true, "quasi" => true ); /** * Tag expression * * @var Expression */ protected $tag; /** * Template * * @var TemplateLiteral */ protected $quasi; /** * Returns the tag expression * * @return Expression */ public function getTag() { return $this->tag; } /** * Sets the tag expression * * @param Expression $tag Tag expression * * @return $this */ public function setTag(Expression $tag) { $this->tag = $tag; return $this; } /** * Returns the template * * @return TemplateLiteral */ public function getQuasi() { return $this->quasi; } /** * Sets the the template * * @param TemplateLiteral $quasi Template * * @return $this */ public function setQuasi(TemplateLiteral $quasi) { $this->quasi = $quasi; return $this; } } * * For the full copyright and license information refer to the LICENSE file * distributed with this source code */ namespace Peast\Syntax\Node; use Peast\Syntax\Utils; /** * A node that represents a template element. * For example `foo` and `bar` in: `foo${exp}bar` * * @author Marco Marchiò */ class TemplateElement extends Node { /** * Map of node properties * * @var array */ protected $propertiesMap = array( "value" => false, "tail" => false, "rawValue" => false ); /** * Node's value * * @var string */ protected $value; /** * Tail flag that is true when the element is the tail element in a template * * @var bool */ protected $tail = false; /** * Node's raw value * * @var string */ protected $rawValue; /** * Return node's value * * @return string */ public function getValue() { return $this->value; } /** * Sets node's value * * @param mixed $value Value * * @return $this */ public function setValue($value) { $this->value = $value; $this->rawValue = Utils::quoteLiteralString($value, "`"); return $this; } /** * Returns the tail flag that is true when the element is the tail element * in a template * * @return bool */ public function getTail() { return $this->tail; } /** * Sets the tail flag that is true when the element is the tail element * in a template * * @param bool $tail Tail flag * * @return $this */ public function setTail($tail) { $this->tail = (bool) $tail; return $this; } /** * Returns node's raw value * * @return string */ public function getRawValue() { return $this->rawValue; } /** * Sets node's raw value that must be wrapped in templates quotes. * * @param string $rawValue Raw value * * @return $this */ public function setRawValue($rawValue) { $rawValue = preg_replace("#^[`}]|(?:`|\\\$\{)$#", "", $rawValue); $this->setValue(Utils::unquoteLiteralString("`$rawValue`")); $this->rawValue = $rawValue; return $this; } } * * For the full copyright and license information refer to the LICENSE file * distributed with this source code */ namespace Peast\Syntax\Node; /** * A node that represents a template literal. * For example: `this is a ${test()} template` * * @author Marco Marchiò */ class TemplateLiteral extends Node implements Expression { /** * Map of node properties * * @var array */ protected $propertiesMap = array( "parts" => true, "quasis" => false, "expressions" => false ); /** * Array of quasis that are the literal parts of the template * * @var TemplateElement[] */ protected $quasis = array(); /** * Array of expressions inside the template * * @var Expression[] */ protected $expressions = array(); /** * Returns the array of quasis that are the literal parts of the template * * @return TemplateElement[] */ public function getQuasis() { return $this->quasis; } /** * Sets the array of quasis that are the literal parts of the template * * @param TemplateElement[] $quasis Quasis * * @return $this */ public function setQuasis($quasis) { $this->assertArrayOf($quasis, "TemplateElement"); $this->quasis = $quasis; return $this; } /** * Returns the array of expressions inside the template * * @return Expression[] */ public function getExpressions() { return $this->expressions; } /** * Sets the array of expressions inside the template * * @param Expression[] $expressions Expressions * * @return $this */ public function setExpressions($expressions) { $this->assertArrayOf($expressions, "Expression"); $this->expressions = $expressions; return $this; } /** * Returns an array of the template parts (quasis and expressions) * * @return array */ public function getParts() { // It must be a list of quasis and expressions alternated $parts = array(); foreach ($this->quasis as $k => $val) { $parts[] = $val; if (isset($this->expressions[$k])) { $parts[] = $this->expressions[$k]; } } return $parts; } /** * Sets the array of the template parts (quasis and expressions) * * @param array Template parts * * @return $this */ public function setParts($parts) { $this->assertArrayOf($parts, array("Expression", "TemplateElement")); $quasis = $expressions = array(); foreach ($parts as $part) { if ($part instanceof TemplateElement) { $quasis[] = $part; } else { $expressions[] = $part; } } return $this->setQuasis($quasis)->setExpressions($expressions); } /** * Returns a serializable version of the node * * @return array */ #[\ReturnTypeWillChange] public function jsonSerialize() { $ret = parent::jsonSerialize(); unset($ret["parts"]); return $ret; } } * * For the full copyright and license information refer to the LICENSE file * distributed with this source code */ namespace Peast\Syntax\Node; /** * A node that represents a while loop. * For example: while (test) {} * * @author Marco Marchiò */ class WhileStatement extends Node implements Statement { /** * Map of node properties * * @var array */ protected $propertiesMap = array( "test" => true, "body" => true ); /** * The loop condition * * @var Expression */ protected $test; /** * The loop body * * @var Statement */ protected $body; /** * Returns the loop condition * * @return Expression */ public function getTest() { return $this->test; } /** * Sets the loop condition * * @param Expression $test Loop * * @return $this */ public function setTest(Expression $test) { $this->test = $test; return $this; } /** * Returns the loop body * * @return Statement */ public function getBody() { return $this->body; } /** * Sets the loop body * * @param Statement $body Loop body * * @return $this */ public function setBody(Statement $body) { $this->body = $body; return $this; } } * * For the full copyright and license information refer to the LICENSE file * distributed with this source code */ namespace Peast\Syntax\Node; /** * A node that represents a class static block * * @author Marco Marchiò */ class StaticBlock extends BlockStatement { } * * For the full copyright and license information refer to the LICENSE file * distributed with this source code */ namespace Peast\Syntax\Node; /** * A node that represents a logical expression. * For example: a && b * * @author Marco Marchiò */ class LogicalExpression extends Node implements Expression { /** * Map of node properties * * @var array */ protected $propertiesMap = array( "left" => true, "operator" => false, "right" => true ); /** * Operator * * @var string */ protected $operator; /** * Left expression * * @var Expression */ protected $left; /** * Right expression * * @var Expression */ protected $right; /** * Returns the operator * * @return string */ public function getOperator() { return $this->operator; } /** * Sets the operator * * @param string $operator Operator * * @return $this */ public function setOperator($operator) { $this->operator = $operator; return $this; } /** * Returns the left expression * * @return Expression */ public function getLeft() { return $this->left; } /** * Sets the left expression * * @param Expression $left Left expression * * @return $this */ public function setLeft(Expression $left) { $this->left = $left; return $this; } /** * Returns the right expression * * @return Expression */ public function getRight() { return $this->right; } /** * Sets the right expression * * @param Expression $right Right expression * * @return $this */ public function setRight(Expression $right) { $this->right = $right; return $this; } } * * For the full copyright and license information refer to the LICENSE file * distributed with this source code */ namespace Peast\Syntax\Node; /** * A node that represents a BigInt literal. * * @author Marco Marchiò */ class BigIntLiteral extends Literal { /** * Map of node properties * * @var array */ protected $propertiesMap = array( "bigint" => false ); /** * Node's value * * @var mixed */ protected $bigint; /** * Sets node's value * * @param float $value Value * * @return $this */ public function setValue($value) { //Value, Raw and Bigint are always the same value $this->value = $this->raw = $this->bigint = $value; return $this; } /** * Returns node's value * * @return string */ public function getBigint() { return $this->bigint; } /** * Sets node's value * * @param string $bigint Value * * @return $this */ public function setBigint($bigint) { return $this->setValue($bigint); } } * * For the full copyright and license information refer to the LICENSE file * distributed with this source code */ namespace Peast\Syntax\Node; /** * A node that represents a chain expression. * For example: test?.test?.() * * @author Marco Marchiò */ class ChainExpression extends Node implements Expression { /** * Map of node properties * * @var array */ protected $propertiesMap = array( "expression" => true ); /** * The wrapped expression * * @var Expression */ protected $expression; /** * Returns the wrapped expression * * @return Expression */ public function getExpression() { return $this->expression; } /** * Sets the wrapped expression * * @param Expression $expression Wrapped expression * * @return $this */ public function setExpression(Expression $expression) { $this->expression = $expression; return $this; } } * * For the full copyright and license information refer to the LICENSE file * distributed with this source code */ namespace Peast\Syntax\Node; /** * A node that represents a case in a switch statement. * * @author Marco Marchiò */ class SwitchCase extends Node { /** * Map of node properties * * @var array */ protected $propertiesMap = array( "test" => true, "consequent" => true ); /** * Test expression that is null in the "default" case * * @var Expression */ protected $test; /** * Consequent statements array * * @var Statement[] */ protected $consequent = array(); /** * Returns the test expression that is null in the "default" case * * @return Expression */ public function getTest() { return $this->test; } /** * Sets the test expression that is null in the "default" case * * @param Expression $test Test expression * * @return $this */ public function setTest($test) { $this->assertType($test, "Expression", true); $this->test = $test; return $this; } /** * Returns the consequent statements array * * @return Statement[] */ public function getConsequent() { return $this->consequent; } /** * Sets the consequent statements array * * @param Expression[] $consequent Consequent statements array * * @return $this */ public function setConsequent($consequent) { $this->assertArrayOf($consequent, "Statement"); $this->consequent = $consequent; return $this; } } * * For the full copyright and license information refer to the LICENSE file * distributed with this source code */ namespace Peast\Syntax\Node; /** * A node that represents a regular expression literal. * * @author Marco Marchiò */ class RegExpLiteral extends Literal { /** * Map of node properties * * @var array */ protected $propertiesMap = array( "flags" => false, "pattern" => false ); /** * Regex flags * * @var string */ protected $flags = ""; /** * Regex pattern * * @var string */ protected $pattern = ""; /** * Returns node's type * * @return string */ public function getType() { return "RegExpLiteral"; } /** * Returns regex pattern * * @return string */ public function getPattern() { return $this->pattern; } /** * Sets regex pattern * * @param string $pattern Regex pattern * * @return $this */ public function setPattern($pattern) { $this->pattern = $pattern; return $this; } /** * Returns regex flags * * @return string */ public function getFlags() { return $this->flags; } /** * Sets regex flags * * @param string $flags Regex flags * * @return $this */ public function setFlags($flags) { $this->flags = $flags; return $this; } /** * Returns node's raw value * * @return string */ public function getRaw() { return "/" . $this->getPattern() . "/" . $this->getFlags(); } /** * Sets node's raw value that must include delimiters * * @param string $raw Raw value * * @return $this */ public function setRaw($raw) { $parts = explode("/", substr($raw, 1)); $flags = array_pop($parts); $this->setPattern(implode("/", $parts)); $this->setFlags($flags); return $this; } /** * Returns node's value * * @return string */ public function getValue() { return $this->getRaw(); } /** * Sets node's value * * @param mixed $value Value * * @return $this */ public function setValue($value) { return $this->setRaw($value); } } * * For the full copyright and license information refer to the LICENSE file * distributed with this source code */ namespace Peast\Syntax\Node; /** * A node that represents an arrow function. * For example: var fn = (a, b) => console.log(a, b) * * @author Marco Marchiò */ class ArrowFunctionExpression extends Function_ implements Expression { /** * Map of node properties * * @var array */ protected $propertiesMap = array( "expression" => false ); /** * This flag is true when function body is wrapped in curly braces * * @var bool */ protected $expression = false; /** * Sets the function body * * @param BlockStatement|Expression $body Function body * * @return $this */ public function setBody($body) { $this->assertType($body, array("BlockStatement", "Expression")); $this->body = $body; return $this; } /** * Returns the expression flag * * @return bool */ public function getExpression() { return $this->expression; } /** * Sets the expression flag * * @param bool $expression Expression flag * * @return $this */ public function setExpression($expression) { $this->expression = (bool) $expression; return $this; } } * * For the full copyright and license information refer to the LICENSE file * distributed with this source code */ namespace Peast\Syntax\Node; /** * A node that represents the "continue" statement inside loops. * * @author Marco Marchiò */ class ContinueStatement extends Node implements Statement { /** * Map of node properties * * @var array */ protected $propertiesMap = array( "label" => true ); /** * The optional label of the continue statement * * @var Identifier */ protected $label; /** * Returns the node's label * * @return Identifier */ public function getLabel() { return $this->label; } /** * Sets the node's label * * @param Identifier $label Node's label * * @return $this */ public function setLabel($label) { $this->assertType($label, "Identifier", true); $this->label = $label; return $this; } } * * For the full copyright and license information refer to the LICENSE file * distributed with this source code */ namespace Peast\Syntax\Node; /** * Abstract class for literals. * * @author Marco Marchiò */ abstract class Literal extends Node implements Expression { /** * Map of node properties * * @var array */ protected $propertiesMap = array( "value" => false, "raw" => false ); /** * Node's value * * @var mixed */ protected $value; /** * Node's raw value * * @var string */ protected $raw; /** * Returns node's type * * @return string */ public function getType() { return "Literal"; } /** * Returns node's value * * @return mixed */ public function getValue() { return $this->value; } /** * Sets node's value * * @param mixed $value Value * * @return $this */ abstract public function setValue($value); /** * Return node's raw value * * @return string */ public function getRaw() { return $this->raw; } /** * Sets node's raw value * * @param mixed $raw Raw value * * @return $this */ public function setRaw($raw) { return $this->setValue($raw); } } * * For the full copyright and license information refer to the LICENSE file * distributed with this source code */ namespace Peast\Syntax\Node; /** * A node that represents a call expression. * For example: test() * * @author Marco Marchiò */ class CallExpression extends ChainElement { /** * Map of node properties * * @var array */ protected $propertiesMap = array( "callee" => true, "arguments" => true ); /** * The callee expression * * @var Expression|Super */ protected $callee; /** * The arguments array * * @var Expression[]|SpreadElement[] */ protected $arguments = array(); /** * Returns the callee expression * * @return Expression|Super */ public function getCallee() { return $this->callee; } /** * Sets the callee expression * * @param Expression|Super $callee Callee expression * * @return $this */ public function setCallee($callee) { $this->assertType($callee, array("Expression", "Super")); $this->callee = $callee; return $this; } /** * Returns the arguments array * * @return Expression[]|SpreadElement[] */ public function getArguments() { return $this->arguments; } /** * Sets the arguments array * * @param Expression[]|SpreadElement[] $arguments Arguments array * * @return $this */ public function setArguments($arguments) { $this->assertArrayOf($arguments, array("Expression", "SpreadElement")); $this->arguments = $arguments; return $this; } } * * For the full copyright and license information refer to the LICENSE file * distributed with this source code */ namespace Peast\Syntax\Node; use Peast\Syntax\Utils; /** * A node that represents an identifier. * * @author Marco Marchiò */ class Identifier extends Node implements Expression, Pattern { /** * Map of node properties * * @var array */ protected $propertiesMap = array( "name" => false, "rawName" => false, ); /** * The identifier's name * * @var string */ protected $name; /** * The identifier's raw name * * @var string */ protected $rawName; /** * Returns the identifier's name * * @return string */ public function getName() { return $this->name; } /** * Sets the identifier's name * * @param string $name The name to set * * @return $this */ public function setName($name) { $this->name = $this->rawName = $name; return $this; } /** * Returns the identifier's raw name * * @return string */ public function getRawName() { return $this->rawName; } /** * Sets the identifier's raw name * * @param string $name The raw name to set * * @return $this */ public function setRawName($name) { $this->rawName = $name; if (strpos($name, "\\") !== false) { $this->name = preg_replace_callback( "#\\\\u(?:\{([a-fA-F0-9]+)\}|([a-fA-F0-9]{4}))#", function ($match) { return Utils::unicodeToUtf8(hexdec($match[1] ? : $match[2])); }, $name ); } else { $this->name = $name; } return $this; } } * * For the full copyright and license information refer to the LICENSE file * distributed with this source code */ namespace Peast\Syntax\Node; /** * Interface that every expression node must implement. * * @author Marco Marchiò */ interface Expression { } * * For the full copyright and license information refer to the LICENSE file * distributed with this source code */ namespace Peast\Syntax\Node; /** * A node that represents in import declaration. * For example: import a from "mod" * * @author Marco Marchiò */ class ImportDeclaration extends Node implements ModuleDeclaration { /** * Map of node properties * * @var array */ protected $propertiesMap = array( "specifiers" => true, "source" => true ); /** * Import specifiers array * * @var array */ protected $specifiers = array(); /** * Import source * * @var Literal */ protected $source; /** * Returns the import specifiers array * * @return array */ public function getSpecifiers() { return $this->specifiers; } /** * Sets the import specifiers array * * @param array $specifiers Import specifiers array * * @return $this */ public function setSpecifiers($specifiers) { $this->assertArrayOf( $specifiers, array( "ImportSpecifier", "ImportDefaultSpecifier", "ImportNamespaceSpecifier" ) ); $this->specifiers = $specifiers; return $this; } /** * Returns the import source * * @return Literal */ public function getSource() { return $this->source; } /** * Sets the import source * * @param Literal $source Import source * * @return $this */ public function setSource(Literal $source) { $this->source = $source; return $this; } } * * For the full copyright and license information refer to the LICENSE file * distributed with this source code */ namespace Peast\Syntax\Node; /** * A node that represents an array binding pattern. * For example: [a, b, c] = d * * @author Marco Marchiò */ class ArrayPattern extends Node implements Pattern { /** * Map of node properties * * @var array */ protected $propertiesMap = array( "elements" => true ); /** * Array elements * * @var Pattern[] */ protected $elements = array(); /** * Returns array elements * * @return Pattern[] */ public function getElements() { return $this->elements; } /** * Sets array elements * * @param Pattern[] $elements Array elements to set * * @return $this */ public function setElements($elements) { $this->assertArrayOf($elements, "Pattern", true); $this->elements = $elements; return $this; } } * * For the full copyright and license information refer to the LICENSE file * distributed with this source code */ namespace Peast\Syntax\Node; use Peast\Syntax\Utils; /** * A node that represents a string literal. * * @author Marco Marchiò */ class StringLiteral extends Literal { /** * Map of node properties * * @var array */ protected $propertiesMap = array( "format" => false ); //Format constants /** * Double quoted string */ const DOUBLE_QUOTED = "double"; /** * Single quoted string */ const SINGLE_QUOTED = "single"; /** * String format * * @var string */ protected $format = self::DOUBLE_QUOTED; /** * Sets node's value * * @param string $value Value * * @return $this */ public function setValue($value) { $this->value = (string) $value; //Force recalculation of the raw value return $this->setFormat($this->format); } /** * Sets node's raw value * * @param mixed $raw Raw value * * @return $this * * @throws \Exception */ public function setRaw($raw) { if (!is_string($raw) || strlen($raw) < 2) { throw new \Exception("Invalid string"); } $startQuote = $raw[0]; $endQuote = substr($raw, -1); if (($startQuote !== "'" && $startQuote !== '"') || $startQuote !== $endQuote ) { throw new \Exception("Invalid string"); } $this->value = Utils::unquoteLiteralString($raw); $this->setFormat($raw[0] === "'" ? self::SINGLE_QUOTED : self::DOUBLE_QUOTED ); $this->raw = $raw; return $this; } /** * Returns string format * * @return string */ public function getFormat() { return $this->format; } /** * Sets string format * * @param string $format Format, one of the format constants * * @return $this */ public function setFormat($format) { $this->format = $format; $quote = $format === self::SINGLE_QUOTED ? "'" : '"'; $this->raw = Utils::quoteLiteralString($this->value, $quote); return $this; } } * * For the full copyright and license information refer to the LICENSE file * distributed with this source code */ namespace Peast\Syntax\Node; /** * A node that represents the export default declaration. * For example: export default a * * @author Marco Marchiò */ class ExportDefaultDeclaration extends Node implements ModuleDeclaration { /** * Map of node properties * * @var array */ protected $propertiesMap = array( "declaration" => true ); /** * The exported declaration * * @var Declaration|Expression */ protected $declaration; /** * Returns the exported declaration * * @return Declaration|Expression */ public function getDeclaration() { return $this->declaration; } /** * Sets the exported declaration * * @param Declaration|Expression $declaration The exported declaration * * @return $this */ public function setDeclaration($declaration) { $this->assertType($declaration, array("Declaration", "Expression")); $this->declaration = $declaration; return $this; } } * * For the full copyright and license information refer to the LICENSE file * distributed with this source code */ namespace Peast\Syntax\Node; /** * A node that represents a labeled statement. * * @author Marco Marchiò */ class LabeledStatement extends Node implements Statement { /** * Map of node properties * * @var array */ protected $propertiesMap = array( "label" => true, "body" => true ); /** * Label * * @var Identifier */ protected $label; /** * Body * * @var Statement */ protected $body; /** * Returns the label * * @return Identifier */ public function getLabel() { return $this->label; } /** * Sets the label * * @param Identifier $label Label * * @return $this */ public function setLabel(Identifier $label) { $this->label = $label; return $this; } /** * Returns the body * * @return Statement */ public function getBody() { return $this->body; } /** * Sets the body * * @param Statement $body Body * * @return $this */ public function setBody(Statement $body) { $this->body = $body; return $this; } } * * For the full copyright and license information refer to the LICENSE file * distributed with this source code */ namespace Peast\Syntax\Node; /** * Interface that every statement node must implement. * * @author Marco Marchiò */ interface Statement { } * * For the full copyright and license information refer to the LICENSE file * distributed with this source code */ namespace Peast\Syntax\Node; /** * Common interface for function import and export nodes. * * @author Marco Marchiò */ interface ModuleDeclaration { } * * For the full copyright and license information refer to the LICENSE file * distributed with this source code */ namespace Peast\Syntax\Node; /** * A node that represents the catch clause in a try-catch statement. * * @author Marco Marchiò */ class CatchClause extends Node { /** * Map of node properties * * @var array */ protected $propertiesMap = array( "param" => true, "body" => true ); /** * The catch clause parameter * * @var Pattern */ protected $param; /** * The body of the catch clause * * @var BlockStatement */ protected $body; /** * Returns the catch clause parameter * * @return Pattern */ public function getParam() { return $this->param; } /** * Sets the catch clause parameter * * @param Pattern $param Catch clause parameter * * @return $this */ public function setParam($param) { $this->assertType($param, "Pattern", true); $this->param = $param; return $this; } /** * Returns the body of the catch clause * * @return BlockStatement */ public function getBody() { return $this->body; } /** * Sets the body of the catch clause * * @param BlockStatement $body The block of code inside the catch clause * * @return $this */ public function setBody(BlockStatement $body) { $this->body = $body; return $this; } } * * For the full copyright and license information refer to the LICENSE file * distributed with this source code */ namespace Peast\Syntax\Node; /** * A node that represents a "var", "const" or "let" declaration. * For example: var a = 1 * * @author Marco Marchiò */ class VariableDeclaration extends Node implements Declaration { /** * Map of node properties * * @var array */ protected $propertiesMap = array( "declarations" => true, "kind" => false ); //Kind constants /** * "var" kind */ const KIND_VAR = "var"; /** * "let" kind */ const KIND_LET = "let"; /** * "const" kind */ const KIND_CONST = "const"; /** * Declarations array * * @var VariableDeclarator[] */ protected $declarations = array(); /** * Declaration kind that is one of the kind constants * * @var string */ protected $kind = self::KIND_VAR; /** * Returns the declarations array * * @return VariableDeclarator[] */ public function getDeclarations() { return $this->declarations; } /** * Sets the declarations array * * @param VariableDeclarator[] $declarations Declarations array * * @return $this */ public function setDeclarations($declarations) { $this->assertArrayOf($declarations, "VariableDeclarator"); $this->declarations = $declarations; return $this; } /** * Returns the declaration kind that is one of the kind constants * * @return string */ public function getKind() { return $this->kind; } /** * Sets the declaration kind that is one of the kind constants * * @param string $kind Declaration kind * * @return $this */ public function setKind($kind) { $this->kind = $kind; return $this; } } * * For the full copyright and license information refer to the LICENSE file * distributed with this source code */ namespace Peast\Syntax\Node; /** * A node that represents a boolean literal. * * @author Marco Marchiò */ class BooleanLiteral extends Literal { /** * Sets node's value * * @param mixed $value Value * * @return $this */ public function setValue($value) { if ($value === "true") { $this->value = true; } elseif ($value === "false") { $this->value = false; } else { $this->value = (bool) $value; } $this->raw = $this->value ? "true" : "false"; return $this; } } * * For the full copyright and license information refer to the LICENSE file * distributed with this source code */ namespace Peast\Syntax\Node; /** * A node that represents an expression wrapped in round brackets. * * @author Marco Marchiò */ class ParenthesizedExpression extends Node implements Expression { /** * Map of node properties * * @var array */ protected $propertiesMap = array( "expression" => true ); /** * The wrapped expression * * @var Expression */ protected $expression; /** * Returns the wrapped expression * * @return Expression */ public function getExpression() { return $this->expression; } /** * Sets the wrapped expression * * @param Expression $expression Wrapped expression * * @return $this */ public function setExpression(Expression $expression) { $this->expression = $expression; return $this; } } * * For the full copyright and license information refer to the LICENSE file * distributed with this source code */ namespace Peast\Syntax\Node; /** * A node that represents the "break" statement inside loops. * * @author Marco Marchiò */ class BreakStatement extends Node implements Statement { /** * Map of node properties * * @var array */ protected $propertiesMap = array( "label" => true ); /** * The optional label of the break statement * * @var Identifier */ protected $label; /** * Returns the node's label * * @return Identifier */ public function getLabel() { return $this->label; } /** * Sets the node's label * * @param Identifier $label Node's label * * @return $this */ public function setLabel($label) { $this->assertType($label, "Identifier", true); $this->label = $label; return $this; } } * * For the full copyright and license information refer to the LICENSE file * distributed with this source code */ namespace Peast\Syntax\Node; /** * A node that represents a yield statement. * * @author Marco Marchiò */ class YieldExpression extends Node implements Expression { /** * Map of node properties * * @var array */ protected $propertiesMap = array( "argument" => true, "delegate" => false ); /** * Yield argument * * @var Expression */ protected $argument; /** * Delegate flag that is true when the yield keyword is preceded by * * * @var bool */ protected $delegate = false; /** * Returns yield argument * * @return Expression */ public function getArgument() { return $this->argument; } /** * Sets yield argument * * @param Expression $argument Argument * * @return $this */ public function setArgument($argument) { $this->assertType($argument, "Expression", true); $this->argument = $argument; return $this; } /** * Returns the delegate flag that is true when the yield keyword is * preceded by * * * @return bool */ public function getDelegate() { return $this->delegate; } /** * Sets the delegate flag that is true when the yield keyword is * preceded by * * * @param bool $delegate Delegate flag * * @return $this */ public function setDelegate($delegate) { $this->delegate = (bool) $delegate; return $this; } } * * For the full copyright and license information refer to the LICENSE file * distributed with this source code */ namespace Peast\Syntax\Node; /** * A node that represents an "export all" declaration. * For example: export * from "test" * * @author Marco Marchiò */ class ExportAllDeclaration extends Node implements ModuleDeclaration { /** * Map of node properties * * @var array */ protected $propertiesMap = array( "source" => true, "exported" => true ); /** * The export source * * @var Literal */ protected $source; /** * The exported name * * @var Identifier|StringLiteral */ protected $exported; /** * Returns the export source * * @return Literal */ public function getSource() { return $this->source; } /** * Sets the export source * * @param Literal $source Export source * * @return $this */ public function setSource(Literal $source) { $this->source = $source; return $this; } /** * Returns the exported name * * @return Identifier|StringLiteral */ public function getExported() { return $this->exported; } /** * Sets the exported name *W * @param Identifier|StringLiteral $exported Exported name * * @return $this */ public function setExported($exported) { $this->assertType($exported, array("Identifier", "StringLiteral"), true); $this->exported = $exported; return $this; } } * * For the full copyright and license information refer to the LICENSE file * distributed with this source code */ namespace Peast\Syntax\Node; /** * A node that represents the "this" keyword. * * @author Marco Marchiò */ class ThisExpression extends Node implements Expression { } * * For the full copyright and license information refer to the LICENSE file * distributed with this source code */ namespace Peast\Syntax\Node; /** * A node that represents the spread element in array literals or function * calls. * For example "...params" in: test(...params) * * @author Marco Marchiò */ class SpreadElement extends Node { /** * Map of node properties * * @var array */ protected $propertiesMap = array( "argument" => true ); /** * The node's argument * * @var Expression */ protected $argument; /** * Returns the node's argument * * @return Expression */ public function getArgument() { return $this->argument; } /** * Sets the node's argument * * @param Expression $argument Node's argument * * @return $this */ public function setArgument(Expression $argument) { $this->argument = $argument; return $this; } } * * For the full copyright and license information refer to the LICENSE file * distributed with this source code */ namespace Peast\Syntax\Node; /** * A node that represents a chain element. * * @author Marco Marchiò */ abstract class ChainElement extends Node implements Expression { protected $propertiesMap = array( "optional" => false ); /** * Optional flag that is true if the node is in the optional * part of a chain expression * * @var bool */ protected $optional = false; /** * Returns the optional flag that is true if the node is in * the optional part of a chain expression * * @return bool */ public function getOptional() { return $this->optional; } /** * Sets the optional flag that is true if the node is in * the optional part of a chain expression * * @param bool $optional Optional flag * * @return $this */ public function setOptional($optional) { $this->optional = (bool) $optional; return $this; } } * * For the full copyright and license information refer to the LICENSE file * distributed with this source code */ namespace Peast\Syntax\Node; /** * A node that represents a specifier in an export declaration. * For example "{a}" in: export {a} * * @author Marco Marchiò */ class ExportSpecifier extends ModuleSpecifier { /** * Map of node properties * * @var array */ protected $propertiesMap = array( "exported" => true ); /** * Exported identifier * * @var Identifier|StringLiteral */ protected $exported; /** * Returns the exported identifier * * @return Identifier|StringLiteral */ public function getExported() { return $this->exported; } /** * Sets the exported identifier * * @param Identifier|StringLiteral $exported Exported identifier * * @return $this */ public function setExported($exported) { $this->assertType($exported, array("Identifier", "StringLiteral")); $this->exported = $exported; return $this; } } * * For the full copyright and license information refer to the LICENSE file * distributed with this source code */ namespace Peast\Syntax\Node; /** * A node that represents the "super" keyword. * * @author Marco Marchiò */ class Super extends Node { } * * For the full copyright and license information refer to the LICENSE file * distributed with this source code */ namespace Peast\Syntax\Node; /** * A node that represents an empty statement (;). * * @author Marco Marchiò */ class EmptyStatement extends Node implements Statement { } * * For the full copyright and license information refer to the LICENSE file * distributed with this source code */ namespace Peast\Syntax\Node; /** * A node that represents a debugger statement. * For example: debugger; * * @author Marco Marchiò */ class DebuggerStatement extends Node implements Statement { } * * For the full copyright and license information refer to the LICENSE file * distributed with this source code */ namespace Peast\Syntax\Node; /** * A node that represents a comment. * * @author Marco Marchiò */ class Comment extends Node { //Comment kind constants /** * Inline comment */ const KIND_INLINE = "inline"; /** * Multiline comment */ const KIND_MULTILINE = "multiline"; /** * Html open comment */ const KIND_HTML_OPEN = "html-open"; /** * Html close comment */ const KIND_HTML_CLOSE = "html-close"; /** * Map of node properties * * @var array */ protected $propertiesMap = array( "kind" => false, "text" => false ); /** * The comment kind * * @var string */ protected $kind; /** * The comment text * * @var string */ protected $text; /** * Returns the comment kind * * @return string */ public function getKind() { return $this->kind; } /** * Sets the comment kind * * @param string $kind Comment kind * * @return $this */ public function setKind($kind) { $this->kind = $kind; return $this; } /** * Returns the comment text * * @return string */ public function getText() { return $this->text; } /** * Sets the comment text * * @param string $text Comment text * * @return $this */ public function setText($text) { $this->text = $text; return $this; } /** * Returns the comment raw text * * @return string */ public function getRawText() { $text = $this->getText(); $kind = $this->getKind(); if ($kind === self::KIND_INLINE) { return "//" . $text; } elseif ($kind === self::KIND_HTML_OPEN) { return "" . $text; } else { return "/*" . $text . "*/"; } } /** * Sets the comment raw text * * @param string $rawText Comment raw text * * @return $this */ public function setRawText($rawText) { $start = substr($rawText, 0, 2); if ($start === "//") { $kind = self::KIND_INLINE; $text = substr($rawText, 2); } elseif ($start === "/*" && substr($rawText, -2) === "*/") { $kind = self::KIND_MULTILINE; $text = substr($rawText, 2, -2); } elseif ($start === "") { $kind = self::KIND_HTML_CLOSE; $text = substr($rawText, 3); } else { throw new \Exception("Invalid comment"); } return $this->setKind($kind)->setText($text); } /** * Sets leading comments array * * @param Comment[] $comments Comments array * * @return $this */ public function setLeadingComments($comments) { //Comments cannot be attached to other comments return $this; } /** * Sets trailing comments array * * @param Comment[] $comments Comments array * * @return $this */ public function setTrailingComments($comments) { //Comments cannot be attached to other comments return $this; } /** * Returns a serializable version of the node * * @return array */ #[\ReturnTypeWillChange] public function jsonSerialize() { $ret = parent::jsonSerialize(); unset($ret["leadingComments"]); unset($ret["trailingComments"]); return $ret; } } * * For the full copyright and license information refer to the LICENSE file * distributed with this source code */ namespace Peast\Syntax\Node; /** * A node that represents a sequence of expressions. * For example: a, b * * @author Marco Marchiò */ class SequenceExpression extends Node implements Expression { /** * Map of node properties * * @var array */ protected $propertiesMap = array( "expressions" => true ); /** * Expressions array * * @var Expression[] */ protected $expressions = array(); /** * Returns the expressions array * * @return Expression[] */ public function getExpressions() { return $this->expressions; } /** * Sets the expressions array * * @param Expression[] $expressions Expressions array * * @return $this */ public function setExpressions($expressions) { $this->assertArrayOf($expressions, "Expression"); $this->expressions = $expressions; return $this; } } * * For the full copyright and license information refer to the LICENSE file * distributed with this source code */ namespace Peast\Syntax\Node; /** * A node that represents an object literal. * For example: {a: 1, b: 2, c: 3} * * @author Marco Marchiò */ class ObjectExpression extends Node implements Expression { /** * Map of node properties * * @var array */ protected $propertiesMap = array( "properties" => true ); /** * Object properties * * @var Property[] */ protected $properties = array(); /** * Returns object properties * * @return Property[] */ public function getProperties() { return $this->properties; } /** * Sets object properties * * @param Property[] $properties Object properties * * @return $this */ public function setProperties($properties) { $this->assertArrayOf($properties, array("Property", "SpreadElement")); $this->properties = $properties; return $this; } } * * For the full copyright and license information refer to the LICENSE file * distributed with this source code */ namespace Peast\Syntax\Node; /** * Abstract class that export and import specifiers nodes must extend. * * @author Marco Marchiò * * @abstract */ abstract class ModuleSpecifier extends Node { /** * Map of node properties * * @var array */ protected $propertiesMap = array( "local" => true ); /** * Local identifier * * @var Identifier|StringLiteral */ protected $local; /** * Returns the local identifier * * @return Identifier|StringLiteral */ public function getLocal() { return $this->local; } /** * Sets the local identifier * * @param Identifier|StringLiteral $local Local identifier * * @return $this */ public function setLocal($local) { $this->assertType($local, array("Identifier", "StringLiteral")); $this->local = $local; return $this; } } * * For the full copyright and license information refer to the LICENSE file * distributed with this source code */ namespace Peast\Syntax\Node; /** * A node that represents a conditional expression. * For example: test() ? ok() : fail() * * @author Marco Marchiò */ class ConditionalExpression extends Node implements Expression { /** * Map of node properties * * @var array */ protected $propertiesMap = array( "test" => true, "consequent" => true, "alternate" => true ); /** * The test expression * * @var Expression */ protected $test; /** * The consequent expression * * @var Expression */ protected $consequent; /** * The alternate expression * * @var Expression */ protected $alternate; /** * Returns the test expression * * @return Expression */ public function getTest() { return $this->test; } /** * Sets the test expression * * @param Expression $test Test expression * * @return $this */ public function setTest(Expression $test) { $this->test = $test; return $this; } /** * Returns the consequent expression * * @return Expression */ public function getConsequent() { return $this->consequent; } /** * Sets the consequent expression * * @param Expression $consequent Consequent expression * * @return $this */ public function setConsequent(Expression $consequent) { $this->consequent = $consequent; return $this; } /** * Returns the alternate expression * * @return Expression */ public function getAlternate() { return $this->alternate; } /** * Sets the alternate expression * * @param Expression $alternate Alternate expression * * @return $this */ public function setAlternate(Expression $alternate) { $this->alternate = $alternate; return $this; } } * * For the full copyright and license information refer to the LICENSE file * distributed with this source code */ namespace Peast\Syntax\Node; /** * A node that represents an update expression. * For example: a++ * * @author Marco Marchiò */ class UpdateExpression extends Node implements Expression { /** * Map of node properties * * @var array */ protected $propertiesMap = array( "argument" => true, "operator" => false, "prefix" => false ); /** * Expression's operator * * @var string */ protected $operator; /** * Prefix flag that is true when the operator precedes the argument * * @var bool */ protected $prefix = false; /** * Expression's argument * * @var Expression */ protected $argument; /** * Returns the expression's operator * * @return string */ public function getOperator() { return $this->operator; } /** * Sets the expression's operator * * @param string $operator Operator * * @return $this */ public function setOperator($operator) { $this->operator = $operator; return $this; } /** * Returns the prefix flag that is true when the operator precedes the * argument * * @return bool */ public function getPrefix() { return $this->prefix; } /** * Sets the prefix flag that is true when the operator precedes the * argument * * @param bool $prefix Prefix flag * * @return $this */ public function setPrefix($prefix) { $this->prefix = $prefix; return $this; } /** * Returns the expression's argument * * @return Expression */ public function getArgument() { return $this->argument; } /** * Sets the expression's argument * * @param Expression $argument Argument * * @return $this */ public function setArgument(Expression $argument) { $this->argument = $argument; return $this; } } * * For the full copyright and license information refer to the LICENSE file * distributed with this source code */ namespace Peast\Syntax\Node; /** * A node that represents an import expression (dynamic import). * * @author Marco Marchiò */ class ImportExpression extends Node implements Expression { /** * Map of node properties * * @var array */ protected $propertiesMap = array( "source" => true ); /** * The catch clause parameter * * @var Expression */ protected $source; /** * Returns the import source * * @return Expression */ public function getSource() { return $this->source; } /** * Sets the import source * * @param Expression $source Import source * * @return $this */ public function setSource(Expression $source) { $this->source = $source; return $this; } } * * For the full copyright and license information refer to the LICENSE file * distributed with this source code */ namespace Peast\Syntax\Node; /** * Interface that every declaration node must implement. * * @author Marco Marchiò */ interface Declaration extends Statement { } * * For the full copyright and license information refer to the LICENSE file * distributed with this source code */ namespace Peast\Syntax\Node; /** * A node that represents a property in an object literal. * For example "b" in: a = {b: 1} * * @author Marco Marchiò */ class Property extends Node { /** * Map of node properties * * @var array */ protected $propertiesMap = array( "key" => true, "value" => true, "kind" => false, "method" => false, "shorthand" => false, "computed" => false ); //Kind constants /** * The default kind for properties */ const KIND_INIT = "init"; /** * Getter property */ const KIND_GET = "get"; /** * Setter property */ const KIND_SET = "set"; /** * Property key * * @var Expression */ protected $key; /** * Property value * * @var Expression */ protected $value; /** * Property kind that is one of the kind constants * * @var string */ protected $kind = self::KIND_INIT; /** * Property method flag that is true when the property is a method * * @var bool */ protected $method = false; /** * Property shorthand flag that is true when the property is declared * using an identifier and without a value * * @var bool */ protected $shorthand = false; /** * Property computed flag that is true when the property is declared using * the square brackets syntax * * @var bool */ protected $computed = false; /** * Returns the property key * * @return Expression */ public function getKey() { return $this->key; } /** * Sets the property key * * @param Expression $key Property key * * @return $this */ public function setKey(Expression $key) { $this->key = $key; return $this; } /** * Returns the property value * * @return Expression */ public function getValue() { return $this->value; } /** * Sets the property value * * @param Expression $value Property value * * @return $this */ public function setValue($value) { $this->assertType($value, "Expression"); $this->value = $value; return $this; } /** * Returns the property kind that is one of the kind constants * * @return string */ public function getKind() { return $this->kind; } /** * Sets the property kind that is one of the kind constants * * @param string $kind Property kind * * @return $this */ public function setKind($kind) { $this->kind = $kind; return $this; } /** * Returns the property method flag that is true when the property is a * method * * @return bool */ public function getMethod() { return $this->method; } /** * Sets the property method flag that is true when the property is a method * * @param bool $method Method flag * * @return $this */ public function setMethod($method) { $this->method = (bool) $method; return $this; } /** * Returns the property shorthand flag that is true when the property * is declared using an identifier and without a value * * @return bool */ public function getShorthand() { return $this->shorthand; } /** * Sets the property shorthand flag that is true when the property * is declared using an identifier and without a value * * @param bool $shorthand Property shorthand flag * * @return $this */ public function setShorthand($shorthand) { $this->shorthand = (bool) $shorthand; return $this; } /** * Returns the property computed flag that is true when the property is * declared using the square brackets syntax * * @return bool */ public function getComputed() { return $this->computed; } /** * Sets the property computed flag that is true when the property is * declared using the square brackets syntax * * @param bool $computed Property computed flag * * @return $this */ public function setComputed($computed) { $this->computed = (bool) $computed; return $this; } } * * For the full copyright and license information refer to the LICENSE file * distributed with this source code */ namespace Peast\Syntax\Node; /** * A node that represents a member expression. * For example: foo.bar * * @author Marco Marchiò */ class MemberExpression extends ChainElement implements Pattern { /** * Map of node properties * * @var array */ protected $propertiesMap = array( "object" => true, "property" => true, "computed" => false ); /** * Expression's object * * @var Expression|Super */ protected $object; /** * Expression's property * * @var Expression|PrivateIdentifier */ protected $property; /** * Computed flag that is true if the property is declared using square * brackets syntax * * @var bool */ protected $computed = false; /** * Returns the expression's object * * @return Expression|Super */ public function getObject() { return $this->object; } /** * Sets the expression's object * * @param Expression|Super $object Object * * @return $this */ public function setObject($object) { $this->assertType($object, array("Expression", "Super")); $this->object = $object; return $this; } /** * Returns the expression's property * * @return Expression|PrivateIdentifier */ public function getProperty() { return $this->property; } /** * Sets the expression's property * * @param Expression|PrivateIdentifier $property Property * * @return $this */ public function setProperty($property) { $this->assertType($property, array("Expression", "PrivateIdentifier")); $this->property = $property; return $this; } /** * Returns the computed flag that is true if the property is declared * using square brackets syntax * * @return bool */ public function getComputed() { return $this->computed; } /** * Sets the computed flag that is true if the property is declared * using square brackets syntax * * @param bool $computed Computed flag * * @return $this */ public function setComputed($computed) { $this->computed = (bool) $computed; return $this; } } * * For the full copyright and license information refer to the LICENSE file * distributed with this source code */ namespace Peast\Syntax\Node; /** * A node that represents a function expression * For example: var test = function () {} * * @author Marco Marchiò */ class FunctionExpression extends Function_ implements Expression { } * * For the full copyright and license information refer to the LICENSE file * distributed with this source code */ namespace Peast\Syntax\Node; /** * A node that represents a class expression * For example: test = class {} * * @author Marco Marchiò */ class ClassExpression extends Class_ implements Expression { } * * For the full copyright and license information refer to the LICENSE file * distributed with this source code */ namespace Peast\Syntax\Node; /** * A node that represents an assignment in a binding context. * For example "a = b" in: var {a = b} = c * * @author Marco Marchiò */ class AssignmentPattern extends Node implements Pattern { /** * Map of node properties * * @var array */ protected $propertiesMap = array( "left" => true, "right" => true ); /** * The left node of the assignment * * @var Pattern */ protected $left; /** * The right node of the assignment * * @var Expression */ protected $right; /** * Returns the left node of the assignment * * @return Pattern */ public function getLeft() { return $this->left; } /** * Sets the left node of the assignment * * @param Pattern $left Left node * * @return $this */ public function setLeft(Pattern $left) { $this->left = $left; return $this; } /** * Returns the right node of the assignment * * @return Expression */ public function getRight() { return $this->right; } /** * Sets the right node of the assignment * * @param Expression $right Right node * * @return $this */ public function setRight(Expression $right) { $this->right = $right; return $this; } } * * For the full copyright and license information refer to the LICENSE file * distributed with this source code */ namespace Peast\Syntax\Node; /** * A node that represents the return statement inside functions. * For example: return a + 1 * * @author Marco Marchiò */ class ReturnStatement extends Node implements Statement { /** * Map of node properties * * @var array */ protected $propertiesMap = array( "argument" => true ); /** * Optional expression after the return keyword * * @var Expression */ protected $argument; /** * Returns the expression after the return keyword * * @return Expression */ public function getArgument() { return $this->argument; } /** * Sets the expression after the return keyword * * @param Expression $argument The expression to return * * @return $this */ public function setArgument($argument) { $this->assertType($argument, "Expression", true); $this->argument = $argument; return $this; } } * * For the full copyright and license information refer to the LICENSE file * distributed with this source code */ namespace Peast\Syntax\Node; /** * A node that represents an assignment expression. * For example: a = b * * @author Marco Marchiò */ class AssignmentExpression extends Node implements Expression { /** * Map of node properties * * @var array */ protected $propertiesMap = array( "left" => true, "operator" => false, "right" => true ); /** * The assignment operator * * @var string */ protected $operator; /** * The left node of the assignment * * @var Pattern|Expression */ protected $left; /** * The right node of the assignment * * @var Expression */ protected $right; /** * Returns the assignment operator * * @return string */ public function getOperator() { return $this->operator; } /** * Sets the assignment operator * * @param string $operator Assignment operator * * @return $this */ public function setOperator($operator) { $this->operator = $operator; return $this; } /** * Returns the left node of the assignment * * @return Pattern|Expression */ public function getLeft() { return $this->left; } /** * Sets the left node of the assignment * * @param Pattern|Expression $left The node to set * * @return $this */ public function setLeft($left) { $this->assertType($left, array("Pattern", "Expression")); $this->left = $left; return $this; } /** * Returns the right node of the assignment * * @return Expression */ public function getRight() { return $this->right; } /** * Sets the right node of the assignment * * @param Expression $right The node to set * * @return $this */ public function setRight(Expression $right) { $this->right = $right; return $this; } } * * For the full copyright and license information refer to the LICENSE file * distributed with this source code */ namespace Peast\Syntax\Node; /** * A node that represents a property in an object binding pattern. * For example "a" in: var {a} = b * * @author Marco Marchiò */ class AssignmentProperty extends Property { /** * Returns node's type * * @return string */ public function getType() { return "Property"; } /** * Sets the property value * * @param Pattern $value Property value * * @return $this */ public function setValue($value) { $this->assertType($value, "Pattern"); $this->value = $value; return $this; } /** * Sets the property kind that is one of the kind constants * * @param string $kind Property kind * * @return $this * * @codeCoverageIgnore */ public function setKind($kind) { return $this; } /** * Sets the property method flag that is true when the property is a method * * @param bool $method Method flag * * @return $this * * @codeCoverageIgnore */ public function setMethod($method) { return $this; } } * * For the full copyright and license information refer to the LICENSE file * distributed with this source code */ namespace Peast\Syntax\Node; /** * A node that represents a block of code wrapped in curly braces. * * @author Marco Marchiò */ class BlockStatement extends Node implements Statement { /** * Map of node properties * * @var array */ protected $propertiesMap = array( "body" => true ); /** * Block's body * * @var Statement[] */ protected $body = array(); /** * Returns block's body * * @return Statement[] */ public function getBody() { return $this->body; } /** * Sets block's body * * @param Statement[] $body Array of Statements that are the body of the * block * * @return $this */ public function setBody($body) { $this->assertArrayOf($body, "Statement"); $this->body = $body; return $this; } } * * For the full copyright and license information refer to the LICENSE file * distributed with this source code */ namespace Peast\Syntax\Node; /** * Abstract class for classes. * * @author Marco Marchiò */ abstract class Class_ extends Node { /** * Map of node properties * * @var array */ protected $propertiesMap = array( "id" => true, "superClass" => true, "body" => true ); /** * Class name * * @var Identifier */ protected $id; /** * Extended class * * @var Expression */ protected $superClass; /** * Class body * * @var ClassBody */ protected $body; /** * Returns class name * * @return Identifier */ public function getId() { return $this->id; } /** * Sets class name * * @param Identifier $id Class name * * @return $this */ public function setId($id) { $this->assertType($id, "Identifier", true); $this->id = $id; return $this; } /** * Returns extended class * * @return Expression */ public function getSuperClass() { return $this->superClass; } /** * Sets extended class * * @param Expression $superClass Extended class * * @return $this */ public function setSuperClass($superClass) { $this->assertType($superClass, "Expression", true); $this->superClass = $superClass; return $this; } /** * Returns class body * * @return ClassBody */ public function getBody() { return $this->body; } /** * Sets class body * * @param ClassBody $body Class body * * @return $this */ public function setBody($body) { $this->assertType($body, "ClassBody"); $this->body = $body; return $this; } } * * For the full copyright and license information refer to the LICENSE file * distributed with this source code */ namespace Peast\Syntax; /** * Parser class * * @author Marco Marchiò */ class Parser extends ParserAbstract { use JSX\Parser; //Identifier parsing mode constants /** * Everything is allowed as identifier, including keywords, null and booleans */ const ID_ALLOW_ALL = 1; /** * Keywords, null and booleans are not allowed in any situation */ const ID_ALLOW_NOTHING = 2; /** * Keywords, null and booleans are not allowed in any situation, future * reserved words are allowed if not in strict mode. Keywords that depend on * parser context are evaluated only if the parser context allows them. */ const ID_MIXED = 3; /** * Binding identifier parsing rule * * @var int */ protected static $bindingIdentifier = self::ID_MIXED; /** * Labelled identifier parsing rule * * @var int */ protected static $labelledIdentifier = self::ID_MIXED; /** * Identifier reference parsing rule * * @var int */ protected static $identifierReference = self::ID_MIXED; /** * Identifier name parsing rule * * @var int */ protected static $identifierName = self::ID_ALLOW_ALL; /** * Imported binding parsing rule * * @var int */ protected static $importedBinding = self::ID_ALLOW_NOTHING; /** * Assignment operators * * @var array */ protected $assignmentOperators = array( "=", "+=", "-=", "*=", "/=", "%=", "<<=", ">>=", ">>>=", "&=", "^=", "|=", "**=", "&&=", "||=", "??=" ); /** * Logical and binary operators * * @var array */ protected $logicalBinaryOperators = array( "??" => 0, "||" => 0, "&&" => 1, "|" => 2, "^" => 3, "&" => 4, "===" => 5, "!==" => 5, "==" => 5, "!=" => 5, "<=" => 6, ">=" => 6, "<" => 6, ">" => 6, "instanceof" => 6, "in" => 6, ">>>" => 7, "<<" => 7, ">>" => 7, "+" => 8, "-" => 8, "*" => 9, "/" => 9, "%" => 9, "**" => 10 ); /** * Unary operators * * @var array */ protected $unaryOperators = array( "delete", "void", "typeof", "++", "--", "+", "-", "~", "!" ); /** * Postfix operators * * @var array */ protected $postfixOperators = array("--", "++"); /** * Array of keywords that depends on a context property * * @var array */ protected $contextKeywords = array( "yield" => "allowYield", "await" => "allowAwait" ); /** * Initializes parser context * * @return void */ protected function initContext() { $context = array( "allowReturn" => false, "allowIn" => false, "allowYield" => false, "allowAwait" => false ); //If async/await is not enabled remove the //relative context properties if (!$this->features->asyncAwait) { unset($context["allowAwait"]); unset($this->contextKeywords["await"]); } $this->context = (object) $context; } /** * Post initialize operations * * @return void */ protected function postInit() { //Remove exponentiation operator if the feature //is not enabled if (!$this->features->exponentiationOperator) { Utils::removeArrayValue( $this->assignmentOperators, "**=" ); unset($this->logicalBinaryOperators["**"]); } //Remove coalescing operator if the feature //is not enabled if (!$this->features->coalescingOperator) { unset($this->logicalBinaryOperators["??"]); } //Remove logical assignment operators if the //feature is not enabled if (!$this->features->logicalAssignmentOperators) { foreach (array("&&=", "||=", "??=") as $op) { Utils::removeArrayValue( $this->assignmentOperators, $op ); } } } /** * Parses the source * * @return Node\Program */ public function parse() { if ($this->sourceType === \Peast\Peast::SOURCE_TYPE_MODULE) { $this->scanner->setStrictMode(true); $body = $this->parseModuleItemList(); } else { $body = $this->parseStatementList(true); } $node = $this->createNode( "Program", $body ?: $this->scanner->getPosition() ); $node->setSourceType($this->sourceType); if ($body) { $node->setBody($body); } $program = $this->completeNode($node); if ($this->scanner->getToken()) { $this->error(); } //Execute scanner end operations $this->scanner->consumeEnd(); //Emit the EndParsing event and pass the resulting program node as //event data $this->eventsEmitter && $this->eventsEmitter->fire( "EndParsing", array($program) ); return $program; } /** * Converts an expression node to a pattern node * * @param Node\Node $node The node to convert * * @return Node\Node */ protected function expressionToPattern($node) { if ($node instanceof Node\ArrayExpression) { $loc = $node->location; $elems = array(); foreach ($node->getElements() as $elem) { $elems[] = $this->expressionToPattern($elem); } $retNode = $this->createNode("ArrayPattern", $loc->start); $retNode->setElements($elems); $this->completeNode($retNode, $loc->end); } elseif ($node instanceof Node\ObjectExpression) { $loc = $node->location; $props = array(); foreach ($node->getProperties() as $prop) { $props[] = $this->expressionToPattern($prop); } $retNode = $this->createNode("ObjectPattern", $loc->start); $retNode->setProperties($props); $this->completeNode($retNode, $loc->end); } elseif ($node instanceof Node\Property) { $loc = $node->location; $retNode = $this->createNode( "AssignmentProperty", $loc->start ); // If it's a shorthand property convert the value to an assignment // pattern if necessary $value = $node->getValue(); $key = $node->getKey(); if ($value && $node->getShorthand() && !$value instanceof Node\AssignmentExpression && (!$value instanceof Node\Identifier || ( $key instanceof Node\Identifier && $key->getName() !== $value->getName() ))) { $loc = $node->location; $valNode = $this->createNode("AssignmentPattern", $loc->start); $valNode->setLeft($key); $valNode->setRight($value); $this->completeNode($valNode, $loc->end); $value = $valNode; } else { $value = $this->expressionToPattern($value); } $retNode->setValue($value); $retNode->setKey($key); $retNode->setMethod($node->getMethod()); $retNode->setShorthand($node->getShorthand()); $retNode->setComputed($node->getComputed()); $this->completeNode($retNode, $loc->end); } elseif ($node instanceof Node\SpreadElement) { $loc = $node->location; $retNode = $this->createNode("RestElement", $loc->start); $retNode->setArgument( $this->expressionToPattern($node->getArgument()) ); $this->completeNode($retNode, $loc->end); } elseif ($node instanceof Node\AssignmentExpression) { $loc = $node->location; $retNode = $this->createNode("AssignmentPattern", $loc->start); $retNode->setLeft($this->expressionToPattern($node->getLeft())); $retNode->setRight($node->getRight()); $this->completeNode($retNode, $loc->end); } else { $retNode = $node; } return $retNode; } /** * Parses a statement list * * @param bool $parseDirectivePrologues True to parse directive prologues * * @return Node\Node[]|null */ protected function parseStatementList( $parseDirectivePrologues = false ) { $items = array(); //Get directive prologues and check if strict mode is present if ($parseDirectivePrologues) { $oldStrictMode = $this->scanner->getStrictMode(); if ($directives = $this->parseDirectivePrologues()) { $items = array_merge($items, $directives[0]); //If "use strict" is present enable scanner strict mode if (in_array("use strict", $directives[1])) { $this->scanner->setStrictMode(true); } } } while ($item = $this->parseStatementListItem()) { $items[] = $item; } //Apply previous strict mode if ($parseDirectivePrologues) { $this->scanner->setStrictMode($oldStrictMode); } return count($items) ? $items : null; } /** * Parses a statement list item * * @return Node\Statement|Node\Declaration|null */ protected function parseStatementListItem() { if ($declaration = $this->parseDeclaration()) { return $declaration; } elseif ($statement = $this->parseStatement()) { return $statement; } return null; } /** * Parses a statement * * @return Node\Statement|null */ protected function parseStatement() { //Here the token value is checked for performance so that functions won't be //called if not necessary $token = $this->scanner->getToken(); if (!$token) { return null; } $val = $token->value; if ($val === "{" && $statement = $this->parseBlock()) { return $statement; } elseif ($val === "var" && $statement = $this->parseVariableStatement()) { return $statement; } elseif ($val === ";" && $statement = $this->parseEmptyStatement()) { return $statement; } elseif ($val === "if" && $statement = $this->parseIfStatement()) { return $statement; } elseif ( ($val === "for" || $val === "while" || $val === "do" || $val === "switch") && $statement = $this->parseBreakableStatement() ) { return $statement; } elseif ($val == "continue" && $statement = $this->parseContinueStatement()) { return $statement; } elseif ($val === "break" && $statement = $this->parseBreakStatement()) { return $statement; } elseif ( $this->context->allowReturn && $val === "return" && $statement = $this->parseReturnStatement() ) { return $statement; } elseif ($val === "with" && $statement = $this->parseWithStatement()) { return $statement; } elseif ($val === "throw" && $statement = $this->parseThrowStatement()) { return $statement; } elseif ($val === "try" && $statement = $this->parseTryStatement()) { return $statement; } elseif ($val === "debugger" && $statement = $this->parseDebuggerStatement()) { return $statement; } elseif ($statement = $this->parseLabelledStatement()) { return $statement; } elseif ($statement = $this->parseExpressionStatement()) { return $statement; } return null; } /** * Parses a declaration * * @return Node\Declaration|null */ protected function parseDeclaration() { //Here the token value is checked for performance so that functions won't be //called if not necessary $token = $this->scanner->getToken(); if (!$token) { return null; } $val = $token->value; if ($declaration = $this->parseFunctionOrGeneratorDeclaration()) { return $declaration; } elseif ($val === "class" && $declaration = $this->parseClassDeclaration()) { return $declaration; } elseif ( ($val === "let" || $val === "const") && $declaration = $this->isolateContext( array("allowIn" => true), "parseLexicalDeclaration" ) ) { return $declaration; } return null; } /** * Parses a breakable statement * * @return Node\Node|null */ protected function parseBreakableStatement() { if ($statement = $this->parseIterationStatement()) { return $statement; } elseif ($statement = $this->parseSwitchStatement()) { return $statement; } return null; } /** * Parses a block statement * * @return Node\BlockStatement|null */ protected function parseBlock() { if ($token = $this->scanner->consume("{")) { $statements = $this->parseStatementList(); if ($this->scanner->consume("}")) { $node = $this->createNode("BlockStatement", $token); if ($statements) { $node->setBody($statements); } return $this->completeNode($node); } $this->error(); } return null; } /** * Parses a module item list * * @return Node\Node[]|null */ protected function parseModuleItemList() { $items = array(); while ($item = $this->parseModuleItem()) { $items[] = $item; } return count($items) ? $items : null; } /** * Parses an empty statement * * @return Node\EmptyStatement|null */ protected function parseEmptyStatement() { if ($token = $this->scanner->consume(";")) { $node = $this->createNode("EmptyStatement", $token); return $this->completeNode($node); } return null; } /** * Parses a debugger statement * * @return Node\DebuggerStatement|null */ protected function parseDebuggerStatement() { if ($token = $this->scanner->consume("debugger")) { $node = $this->createNode("DebuggerStatement", $token); $this->assertEndOfStatement(); return $this->completeNode($node); } return null; } /** * Parses an if statement * * @return Node\IfStatement|null */ protected function parseIfStatement() { if ($token = $this->scanner->consume("if")) { if ($this->scanner->consume("(") && ($test = $this->isolateContext( array("allowIn" => true), "parseExpression" )) && $this->scanner->consume(")") && ( ($consequent = $this->parseStatement()) || (!$this->scanner->getStrictMode() && $consequent = $this->parseFunctionOrGeneratorDeclaration( false, false )) ) ) { $node = $this->createNode("IfStatement", $token); $node->setTest($test); $node->setConsequent($consequent); if ($this->scanner->consume("else")) { if (($alternate = $this->parseStatement()) || (!$this->scanner->getStrictMode() && $alternate = $this->parseFunctionOrGeneratorDeclaration( false, false )) ) { $node->setAlternate($alternate); return $this->completeNode($node); } } else { return $this->completeNode($node); } } $this->error(); } return null; } /** * Parses a try-catch statement * * @return Node\TryStatement|null */ protected function parseTryStatement() { if ($token = $this->scanner->consume("try")) { if ($block = $this->parseBlock()) { $node = $this->createNode("TryStatement", $token); $node->setBlock($block); if ($handler = $this->parseCatch()) { $node->setHandler($handler); } if ($finalizer = $this->parseFinally()) { $node->setFinalizer($finalizer); } if ($handler || $finalizer) { return $this->completeNode($node); } } $this->error(); } return null; } /** * Parses the catch block of a try-catch statement * * @return Node\CatchClause|null */ protected function parseCatch() { if ($token = $this->scanner->consume("catch")) { $node = $this->createNode("CatchClause", $token); if ($this->scanner->consume("(")) { if (!($param = $this->parseCatchParameter()) || !$this->scanner->consume(")")) { $this->error(); } $node->setParam($param); } elseif (!$this->features->optionalCatchBinding) { $this->error(); } if (!($body = $this->parseBlock())) { $this->error(); } $node->setBody($body); return $this->completeNode($node); } return null; } /** * Parses the catch parameter of a catch block in a try-catch statement * * @return Node\Node|null */ protected function parseCatchParameter() { if ($param = $this->parseIdentifier(static::$bindingIdentifier)) { return $param; } elseif ($param = $this->parseBindingPattern()) { return $param; } return null; } /** * Parses a finally block in a try-catch statement * * @return Node\BlockStatement|null */ protected function parseFinally() { if ($this->scanner->consume("finally")) { if ($block = $this->parseBlock()) { return $block; } $this->error(); } return null; } /** * Parses a continue statement * * @return Node\ContinueStatement|null */ protected function parseContinueStatement() { if ($token = $this->scanner->consume("continue")) { $node = $this->createNode("ContinueStatement", $token); if ($this->scanner->noLineTerminators() && ($label = $this->parseIdentifier(static::$labelledIdentifier)) ) { $node->setLabel($label); $this->assertEndOfStatement(); } else { $this->scanner->consume(";"); } return $this->completeNode($node); } return null; } /** * Parses a break statement * * @return Node\BreakStatement|null */ protected function parseBreakStatement() { if ($token = $this->scanner->consume("break")) { $node = $this->createNode("BreakStatement", $token); if ($this->scanner->noLineTerminators() && ($label = $this->parseIdentifier(static::$labelledIdentifier))) { $node->setLabel($label); $this->assertEndOfStatement(); } else { $this->scanner->consume(";"); } return $this->completeNode($node); } return null; } /** * Parses a return statement * * @return Node\ReturnStatement|null */ protected function parseReturnStatement() { if ($token = $this->scanner->consume("return")) { $node = $this->createNode("ReturnStatement", $token); if ($this->scanner->noLineTerminators()) { $argument = $this->isolateContext( array("allowIn" => true), "parseExpression" ); if ($argument) { $node->setArgument($argument); } } $this->assertEndOfStatement(); return $this->completeNode($node); } return null; } /** * Parses a labelled statement * * @return Node\LabeledStatement|null */ protected function parseLabelledStatement() { if ($label = $this->parseIdentifier(static::$labelledIdentifier, ":")) { $this->scanner->consume(":"); if (($body = $this->parseStatement()) || ($body = $this->parseFunctionOrGeneratorDeclaration( false, false )) ) { //Labelled functions are not allowed in strict mode if ($body instanceof Node\FunctionDeclaration && $this->scanner->getStrictMode()) { $this->error( "Labelled functions are not allowed in strict mode" ); } $node = $this->createNode("LabeledStatement", $label); $node->setLabel($label); $node->setBody($body); return $this->completeNode($node); } $this->error(); } return null; } /** * Parses a throw statement * * @return Node\ThrowStatement|null */ protected function parseThrowStatement() { if ($token = $this->scanner->consume("throw")) { if ($this->scanner->noLineTerminators() && ($argument = $this->isolateContext( array("allowIn" => true), "parseExpression" )) ) { $this->assertEndOfStatement(); $node = $this->createNode("ThrowStatement", $token); $node->setArgument($argument); return $this->completeNode($node); } $this->error(); } return null; } /** * Parses a with statement * * @return Node\WithStatement|null */ protected function parseWithStatement() { if ($token = $this->scanner->consume("with")) { if ($this->scanner->consume("(") && ($object = $this->isolateContext( array("allowIn" => true), "parseExpression" )) && $this->scanner->consume(")") && $body = $this->parseStatement() ) { $node = $this->createNode("WithStatement", $token); $node->setObject($object); $node->setBody($body); return $this->completeNode($node); } $this->error(); } return null; } /** * Parses a switch statement * * @return Node\SwitchStatement|null */ protected function parseSwitchStatement() { if ($token = $this->scanner->consume("switch")) { if ($this->scanner->consume("(") && ($discriminant = $this->isolateContext( array("allowIn" => true), "parseExpression" )) && $this->scanner->consume(")") && ($cases = $this->parseCaseBlock()) !== null ) { $node = $this->createNode("SwitchStatement", $token); $node->setDiscriminant($discriminant); $node->setCases($cases); return $this->completeNode($node); } $this->error(); } return null; } /** * Parses the content of a switch statement * * @return Node\SwitchCase[]|null */ protected function parseCaseBlock() { if ($this->scanner->consume("{")) { $parsedCasesAll = array( $this->parseCaseClauses(), $this->parseDefaultClause(), $this->parseCaseClauses() ); if ($this->scanner->consume("}")) { $cases = array(); foreach ($parsedCasesAll as $parsedCases) { if ($parsedCases) { if (is_array($parsedCases)) { $cases = array_merge($cases, $parsedCases); } else { $cases[] = $parsedCases; } } } return $cases; } elseif ($this->parseDefaultClause()) { $this->error( "Multiple default clause in switch statement" ); } else { $this->error(); } } return null; } /** * Parses cases in a switch statement * * @return Node\SwitchCase[]|null */ protected function parseCaseClauses() { $cases = array(); while ($case = $this->parseCaseClause()) { $cases[] = $case; } return count($cases) ? $cases : null; } /** * Parses a case in a switch statement * * @return Node\SwitchCase|null */ protected function parseCaseClause() { if ($token = $this->scanner->consume("case")) { if (($test = $this->isolateContext( array("allowIn" => true), "parseExpression" )) && $this->scanner->consume(":") ) { $node = $this->createNode("SwitchCase", $token); $node->setTest($test); if ($consequent = $this->parseStatementList()) { $node->setConsequent($consequent); } return $this->completeNode($node); } $this->error(); } return null; } /** * Parses default case in a switch statement * * @return Node\SwitchCase|null */ protected function parseDefaultClause() { if ($token = $this->scanner->consume("default")) { if ($this->scanner->consume(":")) { $node = $this->createNode("SwitchCase", $token); if ($consequent = $this->parseStatementList()) { $node->setConsequent($consequent); } return $this->completeNode($node); } $this->error(); } return null; } /** * Parses an expression statement * * @return Node\ExpressionStatement|null */ protected function parseExpressionStatement() { $lookaheadTokens = array("{", "function", "class", array("let", "[")); if ($this->features->asyncAwait) { array_splice( $lookaheadTokens, 3, 0, array(array("async", true)) ); } if (!$this->scanner->isBefore($lookaheadTokens, true) && $expression = $this->isolateContext( array("allowIn" => true), "parseExpression" ) ) { $this->assertEndOfStatement(); $node = $this->createNode("ExpressionStatement", $expression); $node->setExpression($expression); return $this->completeNode($node); } return null; } /** * Parses a do-while statement * * @return Node\DoWhileStatement|null */ protected function parseDoWhileStatement() { if ($token = $this->scanner->consume("do")) { if (($body = $this->parseStatement()) && $this->scanner->consume("while") && $this->scanner->consume("(") && ($test = $this->isolateContext( array("allowIn" => true), "parseExpression" )) && $this->scanner->consume(")") ) { $node = $this->createNode("DoWhileStatement", $token); $node->setBody($body); $node->setTest($test); $node = $this->completeNode($node); $this->scanner->consume(";"); return $node; } $this->error(); } return null; } /** * Parses a while statement * * @return Node\WhileStatement|null */ protected function parseWhileStatement() { if ($token = $this->scanner->consume("while")) { if ($this->scanner->consume("(") && ($test = $this->isolateContext( array("allowIn" => true), "parseExpression" )) && $this->scanner->consume(")") && $body = $this->parseStatement() ) { $node = $this->createNode("WhileStatement", $token); $node->setTest($test); $node->setBody($body); return $this->completeNode($node); } $this->error(); } return null; } /** * Parses a for(var ...) statement * * @param Token $forToken Token that corresponds to the "for" keyword * * @return Node\Node|null */ protected function parseForVarStatement($forToken) { if (!($varToken = $this->scanner->consume("var"))) { return null; } $state = $this->scanner->getState(); if (($decl = $this->isolateContext( array("allowIn" => false), "parseVariableDeclarationList" )) && ($varEndPosition = $this->scanner->getPosition()) && $this->scanner->consume(";") ) { $init = $this->createNode( "VariableDeclaration", $varToken ); $init->setKind($init::KIND_VAR); $init->setDeclarations($decl); $init = $this->completeNode($init, $varEndPosition); $test = $this->isolateContext( array("allowIn" => true), "parseExpression" ); if ($this->scanner->consume(";")) { $update = $this->isolateContext( array("allowIn" => true), "parseExpression" ); if ($this->scanner->consume(")") && $body = $this->parseStatement() ) { $node = $this->createNode("ForStatement", $forToken); $node->setInit($init); $node->setTest($test); $node->setUpdate($update); $node->setBody($body); return $this->completeNode($node); } } } else { $this->scanner->setState($state); if ($decl = $this->parseForBinding()) { $init = null; if ($this->features->forInInitializer && $decl->getId()->getType() === "Identifier") { $init = $this->parseInitializer(); } if ($init) { $decl->setInit($init); $decl->location->end = $init->location->end; } $left = $this->createNode("VariableDeclaration", $varToken); $left->setKind($left::KIND_VAR); $left->setDeclarations(array($decl)); $left = $this->completeNode($left); if ($this->scanner->consume("in")) { if ($init && $this->scanner->getStrictMode()) { $this->error( "For-in variable initializer not allowed in " . "strict mode" ); } if (($right = $this->isolateContext( array("allowIn" => true), "parseExpression" )) && $this->scanner->consume(")") && $body = $this->parseStatement() ) { $node = $this->createNode( "ForInStatement", $forToken ); $node->setLeft($left); $node->setRight($right); $node->setBody($body); return $this->completeNode($node); } } elseif (!$init && $this->scanner->consume("of")) { if (($right = $this->isolateContext( array("allowIn" => true), "parseAssignmentExpression" )) && $this->scanner->consume(")") && $body = $this->parseStatement() ) { $node = $this->createNode( "ForOfStatement", $forToken ); $node->setLeft($left); $node->setRight($right); $node->setBody($body); return $this->completeNode($node); } } } } $this->error(); } /** * Parses a for(let ...) or for(const ...) statement * * @param Token $forToken Token that corresponds to the "for" keyword * * @return Node\Node|null */ protected function parseForLetConstStatement($forToken) { $afterBracketState = $this->scanner->getState(); if (!($init = $this->parseForDeclaration())) { return null; } if ($this->scanner->consume("in")) { if (($right = $this->isolateContext( array("allowIn" => true), "parseExpression" )) && $this->scanner->consume(")") && $body = $this->parseStatement() ) { $node = $this->createNode("ForInStatement", $forToken); $node->setLeft($init); $node->setRight($right); $node->setBody($body); return $this->completeNode($node); } } elseif ($this->scanner->consume("of")) { if (($right = $this->isolateContext( array("allowIn" => true), "parseAssignmentExpression" )) && $this->scanner->consume(")") && $body = $this->parseStatement() ) { $node = $this->createNode("ForOfStatement", $forToken); $node->setLeft($init); $node->setRight($right); $node->setBody($body); return $this->completeNode($node); } } else { $this->scanner->setState($afterBracketState); if ($init = $this->isolateContext( array("allowIn" => false), "parseLexicalDeclaration" ) ) { $test = $this->isolateContext( array("allowIn" => true), "parseExpression" ); if ($this->scanner->consume(";")) { $update = $this->isolateContext( array("allowIn" => true), "parseExpression" ); if ($this->scanner->consume(")") && $body = $this->parseStatement() ) { $node = $this->createNode("ForStatement", $forToken); $node->setInit($init); $node->setTest($test); $node->setUpdate($update); $node->setBody($body); return $this->completeNode($node); } } } } $this->error(); } /** * Parses a for statement that does not start with var, let or const * * @param Token $forToken Token that corresponds to the "for" keyword * @param bool $hasAwait True if "for" is followed by "await" * * @return Node\Node|null */ protected function parseForNotVarLetConstStatement($forToken, $hasAwait) { $state = $this->scanner->getState(); $notBeforeSB = !$this->scanner->isBefore(array(array("let", "[")), true); if ($notBeforeSB && (($init = $this->isolateContext( array("allowIn" => false), "parseExpression" )) || true) && $this->scanner->consume(";") ) { $test = $this->isolateContext( array("allowIn" => true), "parseExpression" ); if ($this->scanner->consume(";")) { $update = $this->isolateContext( array("allowIn" => true), "parseExpression" ); if ($this->scanner->consume(")") && $body = $this->parseStatement() ) { $node = $this->createNode("ForStatement", $forToken); $node->setInit($init); $node->setTest($test); $node->setUpdate($update); $node->setBody($body); return $this->completeNode($node); } } } else { $this->scanner->setState($state); $beforeLetAsyncOf = $this->scanner->isBefore(array("let", array("async", "of")), true); $left = $this->parseLeftHandSideExpression(); if ($left && $left->getType() === "ChainExpression") { $this->error( "Optional chain can't appear in left-hand side" ); } $left = $this->expressionToPattern($left); if ($notBeforeSB && $left && $this->scanner->consume("in")) { if (($right = $this->isolateContext( array("allowIn" => true), "parseExpression" )) && $this->scanner->consume(")") && $body = $this->parseStatement() ) { $node = $this->createNode("ForInStatement", $forToken); $node->setLeft($left); $node->setRight($right); $node->setBody($body); return $this->completeNode($node); } } elseif (($hasAwait || !$beforeLetAsyncOf) && $left && $this->scanner->consume("of") ) { if (($right = $this->isolateContext( array("allowIn" => true), "parseAssignmentExpression" )) && $this->scanner->consume(")") && $body = $this->parseStatement() ) { $node = $this->createNode("ForOfStatement", $forToken); $node->setLeft($left); $node->setRight($right); $node->setBody($body); return $this->completeNode($node); } } } $this->error(); } /** * Parses do-while, while, for, for-in and for-of statements * * @return Node\Node|null */ protected function parseIterationStatement() { if ($node = $this->parseWhileStatement()) { return $node; } elseif ($node = $this->parseDoWhileStatement()) { return $node; } elseif ($startForToken = $this->scanner->consume("for")) { $forAwait = false; if ($this->features->asyncIterationGenerators && $this->context->allowAwait && $this->scanner->consume("await") ) { $forAwait = true; } if ($this->scanner->consume("(") && ( ($node = $this->parseForVarStatement($startForToken)) || ($node = $this->parseForLetConstStatement($startForToken)) || ($node = $this->parseForNotVarLetConstStatement($startForToken, $forAwait))) ) { if ($forAwait) { if (!$node instanceof Node\ForOfStatement) { $this->error( "Async iteration is allowed only with for-of statements", $startForToken->location->start ); } $node->setAwait(true); } return $node; } $this->error(); } return null; } /** * Checks if an async function can start from the current position. Returns * the async token or null if not found * * @param bool $checkFn If false it won't check if the async keyword is * followed by "function" * * @return Token */ protected function checkAsyncFunctionStart($checkFn = true) { return ($asyncToken = $this->scanner->getToken()) && $asyncToken->value === "async" && ( !$checkFn || (($nextToken = $this->scanner->getNextToken()) && $nextToken->value === "function") ) && $this->scanner->noLineTerminators(true) ? $asyncToken : null; } /** * Parses function or generator declaration * * @param bool $default Default mode * @param bool $allowGenerator True to allow parsing of generators * * @return Node\FunctionDeclaration|null */ protected function parseFunctionOrGeneratorDeclaration( $default = false, $allowGenerator = true ) { $async = null; if ($this->features->asyncAwait && ($async = $this->checkAsyncFunctionStart())) { $this->scanner->consumeToken(); if (!$this->features->asyncIterationGenerators) { $allowGenerator = false; } } if ($token = $this->scanner->consume("function")) { $generator = $allowGenerator && $this->scanner->consume("*"); $id = $this->parseIdentifier(static::$bindingIdentifier); if ($generator || $async) { $flags = array(null); if ($generator) { $flags["allowYield"] = true; } if ($async) { $flags["allowAwait"] = true; } } else { $flags = null; } if (($default || $id) && $this->scanner->consume("(") && ($params = $this->isolateContext( $flags, "parseFormalParameterList" )) !== null && $this->scanner->consume(")") && ($tokenBodyStart = $this->scanner->consume("{")) && (($body = $this->isolateContext( $flags, "parseFunctionBody" )) || true) && $this->scanner->consume("}") ) { $body->location->start = $tokenBodyStart->location->start; $body->location->end = $this->scanner->getPosition(); $node = $this->createNode( "FunctionDeclaration", $async ?: $token ); if ($id) { $node->setId($id); } $node->setParams($params); $node->setBody($body); $node->setGenerator($generator); $node->setAsync((bool) $async); return $this->completeNode($node); } $this->error(); } return null; } /** * Parses function or generator expression * * @return Node\FunctionExpression|null */ protected function parseFunctionOrGeneratorExpression() { $allowGenerator = true; $async = false; if ($this->features->asyncAwait && ($async = $this->checkAsyncFunctionStart())) { $this->scanner->consumeToken(); if (!$this->features->asyncIterationGenerators) { $allowGenerator = false; } } if ($token = $this->scanner->consume("function")) { $generator = $allowGenerator && $this->scanner->consume("*"); if ($generator || $async) { $flags = array(null); if ($generator) { $flags["allowYield"] = true; } if ($async) { $flags["allowAwait"] = true; } } else { $flags = null; } $id = $this->isolateContext( $flags, "parseIdentifier", array(static::$bindingIdentifier) ); if ($this->scanner->consume("(") && ($params = $this->isolateContext( $flags, "parseFormalParameterList" )) !== null && $this->scanner->consume(")") && ($tokenBodyStart = $this->scanner->consume("{")) && (($body = $this->isolateContext( $flags, "parseFunctionBody" )) || true) && $this->scanner->consume("}") ) { $body->location->start = $tokenBodyStart->location->start; $body->location->end = $this->scanner->getPosition(); $node = $this->createNode( "FunctionExpression", $async ?: $token ); $node->setId($id); $node->setParams($params); $node->setBody($body); $node->setGenerator($generator); $node->setAsync((bool) $async); return $this->completeNode($node); } $this->error(); } return null; } /** * Parses yield statement * * @return Node\YieldExpression|null */ protected function parseYieldExpression() { if ($token = $this->scanner->consume("yield")) { $node = $this->createNode("YieldExpression", $token); if ($this->scanner->noLineTerminators()) { $delegate = $this->scanner->consume("*"); $argument = $this->isolateContext( array("allowYield" => true), "parseAssignmentExpression" ); if ($argument) { $node->setArgument($argument); $node->setDelegate($delegate); } } return $this->completeNode($node); } return null; } /** * Parses a parameter list * * @return Node\Node[]|null */ protected function parseFormalParameterList() { $hasComma = false; $list = array(); while ( ($param = $this->parseBindingRestElement()) || $param = $this->parseBindingElement() ) { $hasComma = false; $list[] = $param; if ($param->getType() === "RestElement") { break; } elseif ($this->scanner->consume(",")) { $hasComma = true; } else { break; } } if ($hasComma && !$this->features->trailingCommaFunctionCallDeclaration) { $this->error(); } return $list; } /** * Parses a function body * * @return Node\BlockStatement[]|null */ protected function parseFunctionBody() { $body = $this->isolateContext( array("allowReturn" => true), "parseStatementList", array(true) ); $node = $this->createNode( "BlockStatement", $body ?: $this->scanner->getPosition() ); if ($body) { $node->setBody($body); } return $this->completeNode($node); } /** * Parses a class declaration * * @param bool $default Default mode * * @return Node\ClassDeclaration|null */ protected function parseClassDeclaration($default = false) { if ($token = $this->scanner->consume("class")) { //Class declarations are strict mode by default $prevStrict = $this->scanner->getStrictMode(); $this->scanner->setStrictMode(true); $id = $this->parseIdentifier(static::$bindingIdentifier); if (($default || $id) && $tail = $this->parseClassTail() ) { $node = $this->createNode("ClassDeclaration", $token); if ($id) { $node->setId($id); } if ($tail[0]) { $node->setSuperClass($tail[0]); } $node->setBody($tail[1]); $this->scanner->setStrictMode($prevStrict); return $this->completeNode($node); } $this->error(); } return null; } /** * Parses a class expression * * @return Node\ClassExpression|null */ protected function parseClassExpression() { if ($token = $this->scanner->consume("class")) { //Class expressions are strict mode by default $prevStrict = $this->scanner->getStrictMode(); $this->scanner->setStrictMode(true); $id = $this->parseIdentifier(static::$bindingIdentifier); $tail = $this->parseClassTail(); $node = $this->createNode("ClassExpression", $token); if ($id) { $node->setId($id); } if ($tail[0]) { $node->setSuperClass($tail[0]); } $node->setBody($tail[1]); $this->scanner->setStrictMode($prevStrict); return $this->completeNode($node); } return null; } /** * Parses the code that comes after the class keyword and class name. The * return value is an array where the first item is the extended class, if * any, and the second value is the class body * * @return array|null */ protected function parseClassTail() { $heritage = $this->parseClassHeritage(); if ($token = $this->scanner->consume("{")) { $body = $this->parseClassBody(); if ($this->scanner->consume("}")) { $body->location->start = $token->location->start; $body->location->end = $this->scanner->getPosition(); return array($heritage, $body); } } $this->error(); } /** * Parses the class extends part * * @return Node\Node|null */ protected function parseClassHeritage() { if ($this->scanner->consume("extends")) { if ($superClass = $this->parseLeftHandSideExpression()) { return $superClass; } $this->error(); } return null; } /** * Parses the class body * * @return Node\ClassBody|null */ protected function parseClassBody() { $body = $this->parseClassElementList(); $node = $this->createNode( "ClassBody", $body ?: $this->scanner->getPosition() ); if ($body) { $node->setBody($body); } return $this->completeNode($node); } /** * Parses class elements list * * @return Node\MethodDefinition[]|null */ protected function parseClassElementList() { $items = array(); while ($item = $this->parseClassElement()) { if ($item !== true) { $items[] = $item; } } return count($items) ? $items : null; } /** * Parses a class elements * * @return Node\MethodDefinition|Node\PropertyDefinition|Node\StaticBlock|bool|null */ protected function parseClassElement() { if ($this->scanner->consume(";")) { return true; } if ($this->features->classStaticBlock && $this->scanner->isBefore(array(array("static", "{")), true) ) { return $this->parseClassStaticBlock(); } $staticToken = null; $state = $this->scanner->getState(); //This code handles the case where "static" is the method name if (!$this->scanner->isBefore(array(array("static", "(")), true)) { $staticToken = $this->scanner->consume("static"); } if ($def = $this->parseMethodDefinition()) { if ($staticToken) { $def->setStatic(true); $def->location->start = $staticToken->location->start; } return $def; } else { if ($this->features->classFields) { if ($field = $this->parseFieldDefinition()) { if ($staticToken) { $field->setStatic(true); $field->location->start = $staticToken->location->start; } } elseif ($staticToken) { //Handle the case when "static" is the field name $this->scanner->setState($state); $field = $this->parseFieldDefinition(); } return $field; } elseif ($staticToken) { $this->error(); } } return null; } /** * Parses a let or const declaration * * @return Node\VariableDeclaration|null */ protected function parseLexicalDeclaration() { $state = $this->scanner->getState(); if ($token = $this->scanner->consumeOneOf(array("let", "const"))) { $declarations = $this->charSeparatedListOf( "parseVariableDeclaration" ); if ($declarations) { $this->assertEndOfStatement(); $node = $this->createNode("VariableDeclaration", $token); $node->setKind($token->value); $node->setDeclarations($declarations); return $this->completeNode($node); } // "let" can be used as variable name in non-strict mode if ($this->scanner->getStrictMode() || $token->value !== "let") { $this->error(); } else { $this->scanner->setState($state); } } return null; } /** * Parses a var declaration * * @return Node\VariableDeclaration|null */ protected function parseVariableStatement() { if ($token = $this->scanner->consume("var")) { $declarations = $this->isolateContext( array("allowIn" => true), "parseVariableDeclarationList" ); if ($declarations) { $this->assertEndOfStatement(); $node = $this->createNode("VariableDeclaration", $token); $node->setKind($node::KIND_VAR); $node->setDeclarations($declarations); return $this->completeNode($node); } $this->error(); } return null; } /** * Parses an variable declarations * * @return Node\VariableDeclarator[]|null */ protected function parseVariableDeclarationList() { return $this->charSeparatedListOf( "parseVariableDeclaration" ); } /** * Parses a variable declarations * * @return Node\VariableDeclarator|null */ protected function parseVariableDeclaration() { if ($id = $this->parseIdentifier(static::$bindingIdentifier)) { $node = $this->createNode("VariableDeclarator", $id); $node->setId($id); if ($init = $this->parseInitializer()) { $node->setInit($init); } return $this->completeNode($node); } elseif ($id = $this->parseBindingPattern()) { if ($init = $this->parseInitializer()) { $node = $this->createNode("VariableDeclarator", $id); $node->setId($id); $node->setInit($init); return $this->completeNode($node); } } return null; } /** * Parses a let or const declaration in a for statement definition * * @return Node\VariableDeclaration|null */ protected function parseForDeclaration() { $state = $this->scanner->getState(); if ($token = $this->scanner->consumeOneOf(array("let", "const"))) { if ($declaration = $this->parseForBinding()) { $node = $this->createNode("VariableDeclaration", $token); $node->setKind($token->value); $node->setDeclarations(array($declaration)); return $this->completeNode($node); } // "let" can be used as variable name in non-strict mode if ($this->scanner->getStrictMode() || $token->value !== "let") { $this->error(); } else { $this->scanner->setState($state); } } return null; } /** * Parses a binding pattern or an identifier that come after a const or let * declaration in a for statement definition * * @return Node\VariableDeclarator|null */ protected function parseForBinding() { if (($id = $this->parseIdentifier(static::$bindingIdentifier)) || ($id = $this->parseBindingPattern()) ) { $node = $this->createNode("VariableDeclarator", $id); $node->setId($id); return $this->completeNode($node); } return null; } /** * Parses a module item * * @return Node\Node|null */ protected function parseModuleItem() { if ($item = $this->parseImportDeclaration()) { return $item; } elseif ($item = $this->parseExportDeclaration()) { return $item; } elseif ( $item = $this->isolateContext( array( "allowYield" => false, "allowReturn" => false, "allowAwait" => $this->features->topLevelAwait ), "parseStatementListItem" ) ) { return $item; } return null; } /** * Parses the from keyword and the following string in import and export * declarations * * @return Node\StringLiteral|null */ protected function parseFromClause() { if ($this->scanner->consume("from")) { if ($spec = $this->parseStringLiteral()) { return $spec; } $this->error(); } return null; } /** * Parses an export declaration * * @return Node\ModuleDeclaration|null */ protected function parseExportDeclaration() { if ($token = $this->scanner->consume("export")) { if ($this->scanner->consume("*")) { $exported = null; if ($this->features->exportedNameInExportAll && $this->scanner->consume("as")) { $exported = $this->parseModuleExportName(); if (!$exported) { $this->error(); } } if ($source = $this->parseFromClause()) { $this->assertEndOfStatement(); $node = $this->createNode("ExportAllDeclaration", $token); $node->setSource($source); $node->setExported($exported); return $this->completeNode($node); } } elseif ($this->scanner->consume("default")) { $lookaheadTokens = array("function", "class"); if ($this->features->asyncAwait) { $lookaheadTokens[] = array("async", true); } if (($declaration = $this->isolateContext( array("allowAwait" => $this->features->topLevelAwait), "parseFunctionOrGeneratorDeclaration", array(true) )) || ($declaration = $this->isolateContext( array("allowAwait" => $this->features->topLevelAwait), "parseClassDeclaration", array(true) )) ) { $node = $this->createNode("ExportDefaultDeclaration", $token); $node->setDeclaration($declaration); return $this->completeNode($node); } elseif (!$this->scanner->isBefore( $lookaheadTokens, $this->features->asyncAwait ) && ($declaration = $this->isolateContext( array("allowIn" => true, "allowAwait" => $this->features->topLevelAwait), "parseAssignmentExpression" )) ) { $this->assertEndOfStatement(); $node = $this->createNode( "ExportDefaultDeclaration", $token ); $node->setDeclaration($declaration); return $this->completeNode($node); } } elseif (($specifiers = $this->parseExportClause()) !== null) { $node = $this->createNode("ExportNamedDeclaration", $token); $node->setSpecifiers($specifiers); if ($source = $this->parseFromClause()) { $node->setSource($source); } $this->assertEndOfStatement(); return $this->completeNode($node); } elseif ( ($dec = $this->isolateContext( array("allowAwait" => $this->features->topLevelAwait), "parseVariableStatement" )) || $dec = $this->isolateContext( array("allowAwait" => $this->features->topLevelAwait), "parseDeclaration" ) ) { $node = $this->createNode("ExportNamedDeclaration", $token); $node->setDeclaration($dec); return $this->completeNode($node); } $this->error(); } return null; } /** * Parses an export clause * * @return Node\ExportSpecifier[]|null */ protected function parseExportClause() { if ($this->scanner->consume("{")) { $list = array(); while ($spec = $this->parseExportSpecifier()) { $list[] = $spec; if (!$this->scanner->consume(",")) { break; } } if ($this->scanner->consume("}")) { return $list; } $this->error(); } return null; } /** * Parses an export specifier * * @return Node\ExportSpecifier|null */ protected function parseExportSpecifier() { if ($local = $this->parseModuleExportName()) { $node = $this->createNode("ExportSpecifier", $local); $node->setLocal($local); if ($this->scanner->consume("as")) { if ($exported = $this->parseModuleExportName()) { $node->setExported($exported); return $this->completeNode($node); } $this->error(); } else { $node->setExported($local); return $this->completeNode($node); } } return null; } /** * Parses an export name * * @return Node\Identifier|Node\StringLiteral|null */ protected function parseModuleExportName() { if ($name = $this->parseIdentifier(static::$identifierName)) { return $name; } elseif ($this->features->arbitraryModuleNSNames && ($name = $this->parseStringLiteral()) ) { return $name; } return null; } /** * Parses an import declaration * * @return Node\ModuleDeclaration|null */ protected function parseImportDeclaration() { //Delay parsing of dynamic import so that it is handled //by the relative method if ($this->features->dynamicImport && $this->scanner->isBefore(array(array("import", "(")), true)) { return null; } //Delay parsing of import.meta so that it is handled //by the relative method if ($this->features->importMeta && $this->scanner->isBefore(array(array("import", ".")), true)) { return null; } if ($token = $this->scanner->consume("import")) { if ($source = $this->parseStringLiteral()) { $this->assertEndOfStatement(); $node = $this->createNode("ImportDeclaration", $token); $node->setSource($source); return $this->completeNode($node); } elseif (($specifiers = $this->parseImportClause()) !== null && $source = $this->parseFromClause() ) { $this->assertEndOfStatement(); $node = $this->createNode("ImportDeclaration", $token); $node->setSpecifiers($specifiers); $node->setSource($source); return $this->completeNode($node); } $this->error(); } return null; } /** * Parses an import clause * * @return array|null */ protected function parseImportClause() { if ($spec = $this->parseNameSpaceImport()) { return array($spec); } elseif (($specs = $this->parseNamedImports()) !== null) { return $specs; } elseif ($spec = $this->parseIdentifier(static::$importedBinding)) { $node = $this->createNode("ImportDefaultSpecifier", $spec); $node->setLocal($spec); $ret = array($this->completeNode($node)); if ($this->scanner->consume(",")) { if ($spec = $this->parseNameSpaceImport()) { $ret[] = $spec; return $ret; } elseif (($specs = $this->parseNamedImports()) !== null) { return array_merge($ret, $specs); } $this->error(); } else { return $ret; } } return null; } /** * Parses a namespace import * * @return Node\ImportNamespaceSpecifier|null */ protected function parseNameSpaceImport() { if ($token = $this->scanner->consume("*")) { if ($this->scanner->consume("as") && $local = $this->parseIdentifier(static::$identifierReference) ) { $node = $this->createNode("ImportNamespaceSpecifier", $token); $node->setLocal($local); return $this->completeNode($node); } $this->error(); } return null; } /** * Parses a named imports * * @return Node\ImportSpecifier[]|null */ protected function parseNamedImports() { if ($this->scanner->consume("{")) { $list = array(); while ($spec = $this->parseImportSpecifier()) { $list[] = $spec; if (!$this->scanner->consume(",")) { break; } } if ($this->scanner->consume("}")) { return $list; } $this->error(); } return null; } /** * Parses an import specifier * * @return Node\ImportSpecifier|null */ protected function parseImportSpecifier() { $requiredAs = false; $imported = $this->parseIdentifier(static::$importedBinding); if (!$imported) { $imported = $this->parseModuleExportName(); if (!$imported) { return null; } $requiredAs = true; } $node = $this->createNode("ImportSpecifier", $imported); $node->setImported($imported); if ($this->scanner->consume("as")) { if (!($local = $this->parseIdentifier(static::$importedBinding))) { $this->error(); } $node->setLocal($local); } elseif ($requiredAs) { $this->error(); } else { $node->setLocal($imported); } return $this->completeNode($node); } /** * Parses a binding pattern * * @return Node\ArrayPattern|Node\ObjectPattern|null */ protected function parseBindingPattern() { if ($pattern = $this->parseObjectBindingPattern()) { return $pattern; } elseif ($pattern = $this->parseArrayBindingPattern()) { return $pattern; } return null; } /** * Parses an elisions sequence. It returns the number of elisions or null * if no elision has been found * * @return int */ protected function parseElision() { $count = 0; while ($this->scanner->consume(",")) { $count ++; } return $count ?: null; } /** * Parses an array binding pattern * * @return Node\ArrayPattern|null */ protected function parseArrayBindingPattern() { if ($token = $this->scanner->consume("[")) { $elements = array(); while (true) { if ($elision = $this->parseElision()) { $elements = array_merge( $elements, array_fill(0, $elision, null) ); } if ($element = $this->parseBindingElement()) { $elements[] = $element; if (!$this->scanner->consume(",")) { break; } } elseif ($rest = $this->parseBindingRestElement()) { $elements[] = $rest; break; } else { break; } } if ($this->scanner->consume("]")) { $node = $this->createNode("ArrayPattern", $token); $node->setElements($elements); return $this->completeNode($node); } } return null; } /** * Parses a rest element * * @return Node\RestElement|null */ protected function parseBindingRestElement() { if ($token = $this->scanner->consume("...")) { if (($argument = $this->parseIdentifier(static::$bindingIdentifier)) || ($argument = $this->parseBindingPattern())) { $node = $this->createNode("RestElement", $token); $node->setArgument($argument); return $this->completeNode($node); } $this->error(); } return null; } /** * Parses a binding element * * @return Node\AssignmentPattern|Node\Identifier|null */ protected function parseBindingElement() { if ($el = $this->parseSingleNameBinding()) { return $el; } elseif ($left = $this->parseBindingPattern()) { $right = $this->isolateContext( array("allowIn" => true), "parseInitializer" ); if ($right) { $node = $this->createNode("AssignmentPattern", $left); $node->setLeft($left); $node->setRight($right); return $this->completeNode($node); } else { return $left; } } return null; } /** * Parses single name binding * * @return Node\AssignmentPattern|Node\Identifier|null */ protected function parseSingleNameBinding() { if ($left = $this->parseIdentifier(static::$bindingIdentifier)) { $right = $this->isolateContext( array("allowIn" => true), "parseInitializer" ); if ($right) { $node = $this->createNode("AssignmentPattern", $left); $node->setLeft($left); $node->setRight($right); return $this->completeNode($node); } else { return $left; } } return null; } /** * Parses a property name. The returned value is an array where there first * element is the property name and the second element is a boolean * indicating if it's a computed property * * @return array|null */ protected function parsePropertyName() { if ($token = $this->scanner->consume("[")) { if (($name = $this->isolateContext( array("allowIn" => true), "parseAssignmentExpression" )) && $this->scanner->consume("]") ) { return array($name, true, $token); } $this->error(); } elseif ($name = $this->parseIdentifier(static::$identifierName)) { return array($name, false); } elseif ($name = $this->parseStringLiteral()) { return array($name, false); } elseif ($name = $this->parseNumericLiteral()) { return array($name, false); } return null; } /** * Parses a property name. The returned value is an array where there first * element is the property name and the second element is a boolean * indicating if it's a computed property * * @return array|null */ protected function parseClassElementName() { if ( $this->features->privateMethodsAndFields && ($name = $this->parsePrivateIdentifier()) ) { return array($name, false); } return $this->parsePropertyName(); } /** * Parses a field definition * * @return Node\StaticBlock */ protected function parseClassStaticBlock() { $staticToken = $this->scanner->consume("static"); $this->scanner->consume("{"); $statements = $this->isolateContext( array("allowAwait" => true), "parseStatementList" ); if ($this->scanner->consume("}")) { $node = $this->createNode("StaticBlock", $staticToken); if ($statements) { $node->setBody($statements); } return $this->completeNode($node); } $this->error(); } /** * Parses a field definition * * @return Node\PropertyDefinition|null */ protected function parseFieldDefinition() { $state = $this->scanner->getState(); if ($prop = $this->parseClassElementName()) { $value = $this->isolateContext( array("allowIn" => true), "parseInitializer" ); $this->assertEndOfStatement(); $node = $this->createNode("PropertyDefinition", $prop); $node->setKey($prop[0]); if ($value) { $node->setValue($value); } $node->setComputed($prop[1]); return $this->completeNode($node); } $this->scanner->setState($state); return null; } /** * Parses a method definition * * @return Node\MethodDefinition|null */ protected function parseMethodDefinition() { $state = $this->scanner->getState(); $generator = $error = $async = false; $position = null; $kind = Node\MethodDefinition::KIND_METHOD; if ($token = $this->scanner->consume("get")) { $position = $token; $kind = Node\MethodDefinition::KIND_GET; } elseif ($token = $this->scanner->consume("set")) { $position = $token; $kind = Node\MethodDefinition::KIND_SET; } elseif ($token = $this->scanner->consume("*")) { $position = $token; $error = true; $generator = true; } elseif ($this->features->asyncAwait && ($token = $this->checkAsyncFunctionStart(false))) { $this->scanner->consumeToken(); $position = $token; $error = true; $async = true; if ($this->features->asyncIterationGenerators && ($this->scanner->consume("*"))) { $generator = true; } } //Handle the case where get and set are methods name and not the //definition of a getter/setter if ($kind !== Node\MethodDefinition::KIND_METHOD && $this->scanner->consume("(") ) { $this->scanner->setState($state); $kind = Node\MethodDefinition::KIND_METHOD; $error = false; } if ($prop = $this->parseClassElementName()) { if (!$position) { $position = isset($prop[2]) ? $prop[2] : $prop[0]; } if ($tokenFn = $this->scanner->consume("(")) { if ($generator || $async) { $flags = array(null); if ($generator) { $flags["allowYield"] = true; } if ($async) { $flags["allowAwait"] = true; } } else { $flags = null; } $error = true; $params = array(); if ($kind === Node\MethodDefinition::KIND_SET) { $params = $this->isolateContext( null, "parseBindingElement" ); if ($params) { $params = array($params); } } elseif ($kind === Node\MethodDefinition::KIND_METHOD) { $params = $this->isolateContext( $flags, "parseFormalParameterList" ); } if ($params !== null && $this->scanner->consume(")") && ($tokenBodyStart = $this->scanner->consume("{")) && (($body = $this->isolateContext( $flags, "parseFunctionBody" )) || true) && $this->scanner->consume("}") ) { if ($prop[0] instanceof Node\Identifier && $prop[0]->getName() === "constructor" ) { $kind = Node\MethodDefinition::KIND_CONSTRUCTOR; } $body->location->start = $tokenBodyStart->location->start; $body->location->end = $this->scanner->getPosition(); $nodeFn = $this->createNode("FunctionExpression", $tokenFn); $nodeFn->setParams($params); $nodeFn->setBody($body); $nodeFn->setGenerator($generator); $nodeFn->setAsync($async); $node = $this->createNode("MethodDefinition", $position); $node->setKey($prop[0]); $node->setValue($this->completeNode($nodeFn)); $node->setKind($kind); $node->setComputed($prop[1]); return $this->completeNode($node); } } } if ($error) { $this->error(); } else { $this->scanner->setState($state); } return null; } /** * Parses parameters in an arrow function. If the parameters are wrapped in * round brackets, the returned value is an array where the first element * is the parameters list and the second element is the open round brackets, * this is needed to know the start position * * @return Node\Identifier|array|null */ protected function parseArrowParameters() { if ($param = $this->parseIdentifier(static::$bindingIdentifier, "=>")) { return $param; } elseif ($token = $this->scanner->consume("(")) { $params = $this->parseFormalParameterList(); if ($params !== null && $this->scanner->consume(")")) { return array($params, $token); } } return null; } /** * Parses the body of an arrow function. The returned value is an array * where the first element is the function body and the second element is * a boolean indicating if the body is wrapped in curly braces * * @param bool $async Async body mode * * @return array|null */ protected function parseConciseBody($async = false) { if ($token = $this->scanner->consume("{")) { if (($body = $this->isolateContext( $async ? array(null, "allowAwait" => true) : null, "parseFunctionBody" )) && $this->scanner->consume("}") ) { $body->location->start = $token->location->start; $body->location->end = $this->scanner->getPosition(); return array($body, false); } $this->error(); } elseif (!$this->scanner->isBefore(array("{")) && $body = $this->isolateContext( $this->features->asyncAwait ? array("allowYield" => false, "allowAwait" => $async) : array("allowYield" => false), "parseAssignmentExpression" ) ) { return array($body, true); } return null; } /** * Parses an arrow function * * @return Node\ArrowFunctionExpression|null */ protected function parseArrowFunction() { $state = $this->scanner->getState(); $async = false; if ($this->features->asyncAwait && ($async = $this->checkAsyncFunctionStart(false))) { $this->scanner->consumeToken(); } if (($params = $this->parseArrowParameters()) !== null) { if ($this->scanner->noLineTerminators() && $this->scanner->consume("=>") ) { if ($body = $this->parseConciseBody((bool) $async)) { if (is_array($params)) { $pos = $params[1]; $params = $params[0]; } else { $pos = $params; $params = array($params); } if ($async) { $pos = $async; } $node = $this->createNode("ArrowFunctionExpression", $pos); $node->setParams($params); $node->setBody($body[0]); $node->setExpression($body[1]); $node->setAsync((bool) $async); return $this->completeNode($node); } $this->error(); } } $this->scanner->setState($state); return null; } /** * Parses an object literal * * @return Node\ObjectExpression|null */ protected function parseObjectLiteral() { if ($token = $this->scanner->consume("{")) { $properties = array(); while ($prop = $this->parsePropertyDefinition()) { $properties[] = $prop; if (!$this->scanner->consume(",")) { break; } } if ($this->scanner->consume("}")) { $node = $this->createNode("ObjectExpression", $token); if ($properties) { $node->setProperties($properties); } return $this->completeNode($node); } $this->error(); } return null; } /** * Parses a property in an object literal * * @return Node\Property|null */ protected function parsePropertyDefinition() { if ($this->features->restSpreadProperties && ($prop = $this->parseSpreadElement())) { return $prop; } $state = $this->scanner->getState(); if (($property = $this->parsePropertyName()) && $this->scanner->consume(":") ) { $value = $this->isolateContext( array("allowIn" => true), "parseAssignmentExpression" ); if ($value) { $startPos = isset($property[2]) ? $property[2] : $property[0]; $node = $this->createNode("Property", $startPos); $node->setKey($property[0]); $node->setValue($value); $node->setComputed($property[1]); return $this->completeNode($node); } $this->error(); } $this->scanner->setState($state); if ($property = $this->parseMethodDefinition()) { $node = $this->createNode("Property", $property); $node->setKey($property->getKey()); $node->setValue($property->getValue()); $node->setComputed($property->getComputed()); $kind = $property->getKind(); if ($kind !== Node\MethodDefinition::KIND_GET && $kind !== Node\MethodDefinition::KIND_SET ) { $node->setMethod(true); $node->setKind(Node\Property::KIND_INIT); } else { $node->setKind($kind); } return $this->completeNode($node); } elseif ($key = $this->parseIdentifier(static::$identifierReference)) { $node = $this->createNode("Property", $key); $node->setShorthand(true); $node->setKey($key); $value = $this->isolateContext( array("allowIn" => true), "parseInitializer" ); $node->setValue($value ?: $key); return $this->completeNode($node); } return null; } /** * Parses an initializer * * @return Node\Node|null */ protected function parseInitializer() { if ($this->scanner->consume("=")) { if ($value = $this->parseAssignmentExpression()) { return $value; } $this->error(); } return null; } /** * Parses an object binding pattern * * @return Node\ObjectPattern|null */ protected function parseObjectBindingPattern() { $state = $this->scanner->getState(); if ($token = $this->scanner->consume("{")) { $properties = array(); while ($prop = $this->parseBindingProperty()) { $properties[] = $prop; if (!$this->scanner->consume(",")) { break; } } if ($this->features->restSpreadProperties && ($rest = $this->parseRestProperty())) { $properties[] = $rest; } if ($this->scanner->consume("}")) { $node = $this->createNode("ObjectPattern", $token); if ($properties) { $node->setProperties($properties); } return $this->completeNode($node); } $this->scanner->setState($state); } return null; } /** * Parses a rest property * * @return Node\RestElement|null */ protected function parseRestProperty() { $state = $this->scanner->getState(); if ($token = $this->scanner->consume("...")) { if ($argument = $this->parseIdentifier(static::$bindingIdentifier)) { $node = $this->createNode("RestElement", $token); $node->setArgument($argument); return $this->completeNode($node); } $this->scanner->setState($state); } return null; } /** * Parses a property in an object binding pattern * * @return Node\AssignmentProperty|null */ protected function parseBindingProperty() { $state = $this->scanner->getState(); if (($key = $this->parsePropertyName()) && $this->scanner->consume(":") ) { if ($value = $this->parseBindingElement()) { $startPos = isset($key[2]) ? $key[2] : $key[0]; $node = $this->createNode("AssignmentProperty", $startPos); $node->setKey($key[0]); $node->setComputed($key[1]); $node->setValue($value); return $this->completeNode($node); } $this->scanner->setState($state); return null; } $this->scanner->setState($state); if ($property = $this->parseSingleNameBinding()) { $node = $this->createNode("AssignmentProperty", $property); $node->setShorthand(true); if ($property instanceof Node\AssignmentPattern) { $node->setKey($property->getLeft()); } else { $node->setKey($property); } $node->setValue($property); return $this->completeNode($node); } return null; } /** * Parses an expression * * @return Node\Node|null */ protected function parseExpression() { $list = $this->charSeparatedListOf("parseAssignmentExpression"); if (!$list) { return null; } elseif (count($list) === 1) { return $list[0]; } else { $node = $this->createNode("SequenceExpression", $list); $node->setExpressions($list); return $this->completeNode($node); } } /** * Parses an assignment expression * * @return Node\Node|null */ protected function parseAssignmentExpression() { if ($expr = $this->parseArrowFunction()) { return $expr; } elseif ($this->context->allowYield && $expr = $this->parseYieldExpression()) { return $expr; } elseif ($expr = $this->parseConditionalExpression()) { $exprTypes = array( "ConditionalExpression", "LogicalExpression", "BinaryExpression", "UpdateExpression", "UnaryExpression" ); if (!in_array($expr->getType(), $exprTypes)) { $operators = $this->assignmentOperators; if ($operator = $this->scanner->consumeOneOf($operators)) { if ($expr->getType() === "ChainExpression") { $this->error( "Optional chain can't appear in left-hand side" ); } $right = $this->parseAssignmentExpression(); if ($right) { $node = $this->createNode( "AssignmentExpression", $expr ); $node->setLeft($this->expressionToPattern($expr)); $node->setOperator($operator->value); $node->setRight($right); return $this->completeNode($node); } $this->error(); } } return $expr; } return null; } /** * Parses a conditional expression * * @return Node\Node|null */ protected function parseConditionalExpression() { if ($test = $this->parseLogicalBinaryExpression()) { if ($this->scanner->consume("?")) { $consequent = $this->isolateContext( array("allowIn" => true), "parseAssignmentExpression" ); if ($consequent && $this->scanner->consume(":") && $alternate = $this->parseAssignmentExpression() ) { $node = $this->createNode("ConditionalExpression", $test); $node->setTest($test); $node->setConsequent($consequent); $node->setAlternate($alternate); return $this->completeNode($node); } $this->error(); } else { return $test; } } return null; } /** * Parses a logical or a binary expression * * @return Node\Node|null */ protected function parseLogicalBinaryExpression() { $operators = $this->logicalBinaryOperators; if (!$this->context->allowIn) { unset($operators["in"]); } if (!($exp = $this->parseUnaryExpression())) { if ( !$this->features->classFieldsPrivateIn || !$this->context->allowIn ) { return null; } //Support "#private in x" syntax $state = $this->scanner->getState(); if ( !($exp = $this->parsePrivateIdentifier()) || !$this->scanner->isBefore(array("in")) ) { if ($exp) { $this->scanner->setState($state); } return null; } } $list = array($exp); $coalescingFound = $andOrFound = false; while ($token = $this->scanner->consumeOneOf(array_keys($operators))) { $op = $token->value; // Coalescing and logical expressions can't be used together if ($op === "??") { $coalescingFound = true; } elseif ($op === "&&" || $op === "||") { $andOrFound = true; } if ($coalescingFound && $andOrFound) { $this->error( "Logical expressions must be wrapped in parentheses when " . "inside coalesce expressions" ); } if (!($exp = $this->parseUnaryExpression())) { $this->error(); } $list[] = $op; $list[] = $exp; } $len = count($list); if ($len > 1) { $maxGrade = max($operators); for ($grade = $maxGrade; $grade >= 0; $grade--) { $class = $grade < 2 ? "LogicalExpression" : "BinaryExpression"; $r2l = $grade === 10; //Exponentiation operator must be parsed right to left if ($r2l) { $i = $len - 2; $step = -2; } else { $i = 1; $step = 2; } for (; ($r2l && $i > 0) || (!$r2l && $i < $len); $i += $step) { if ($operators[$list[$i]] === $grade) { $node = $this->createNode($class, $list[$i - 1]); $node->setLeft($list[$i - 1]); $node->setOperator($list[$i]); $node->setRight($list[$i + 1]); $node = $this->completeNode( $node, $list[$i + 1]->location->end ); array_splice($list, $i - 1, 3, array($node)); if (!$r2l) { $i -= $step; } $len = count($list); } } } } return $list[0]; } /** * Parses a unary expression * * @return Node\Node|null */ protected function parseUnaryExpression() { $operators = $this->unaryOperators; if ($this->features->asyncAwait && $this->context->allowAwait) { $operators[] = "await"; } if ($expr = $this->parsePostfixExpression()) { return $expr; } elseif ($token = $this->scanner->consumeOneOf($operators)) { if ($argument = $this->parseUnaryExpression()) { $op = $token->value; //Deleting a variable without accessing its properties is a //syntax error in strict mode if ($op === "delete" && $this->scanner->getStrictMode() && $argument instanceof Node\Identifier) { $this->error( "Deleting an unqualified identifier is not allowed in strict mode" ); } if ($this->features->asyncAwait && $op === "await") { $node = $this->createNode("AwaitExpression", $token); } else { if ($op === "++" || $op === "--") { if ($argument->getType() === "ChainExpression") { $this->error( "Optional chain can't appear in left-hand side" ); } $node = $this->createNode("UpdateExpression", $token); $node->setPrefix(true); } else { $node = $this->createNode("UnaryExpression", $token); } $node->setOperator($op); } $node->setArgument($argument); return $this->completeNode($node); } $this->error(); } return null; } /** * Parses a postfix expression * * @return Node\Node|null */ protected function parsePostfixExpression() { if ($argument = $this->parseLeftHandSideExpression()) { if ($this->scanner->noLineTerminators() && $token = $this->scanner->consumeOneOf($this->postfixOperators) ) { if ($argument->getType() === "ChainExpression") { $this->error( "Optional chain can't appear in left-hand side" ); } $node = $this->createNode("UpdateExpression", $argument); $node->setOperator($token->value); $node->setArgument($argument); return $this->completeNode($node); } return $argument; } return null; } /** * Parses a left hand side expression * * @return Node\Node|null */ protected function parseLeftHandSideExpression() { $object = null; $newTokens = array(); //Parse all occurrences of "new" if ($this->scanner->isBefore(array("new"))) { while ($newToken = $this->scanner->consume("new")) { if ($this->scanner->consume(".")) { //new.target if (!$this->scanner->consume("target")) { $this->error(); } $node = $this->createNode("MetaProperty", $newToken); $node->setMeta("new"); $node->setProperty("target"); $object = $this->completeNode($node); break; } $newTokens[] = $newToken; } } elseif ($this->features->importMeta && $this->sourceType === \Peast\Peast::SOURCE_TYPE_MODULE && $this->scanner->isBefore(array(array("import", ".")), true) ) { //import.meta $importToken = $this->scanner->consume("import"); $this->scanner->consume("."); if (!$this->scanner->consume("meta")) { $this->error(); } $node = $this->createNode("MetaProperty", $importToken); $node->setMeta("import"); $node->setProperty("meta"); $object = $this->completeNode($node); } $newTokensCount = count($newTokens); if (!$object && !($object = $this->parseSuperPropertyOrCall()) && !($this->features->dynamicImport && ($object = $this->parseImportCall()) ) && !($object = $this->parsePrimaryExpression()) ) { if ($newTokensCount) { $this->error(); } return null; } $valid = true; $optionalChain = false; $properties = array(); while (true) { $optional = false; if ($opToken = $this->scanner->consumeOneOf(array("?.", "."))) { $isOptChain = $opToken->value == "?."; if ($isOptChain) { $optionalChain = $optional = true; } if ( ($this->features->privateMethodsAndFields && ($property = $this->parsePrivateIdentifier())) || ($property = $this->parseIdentifier(static::$identifierName)) ) { $valid = true; $properties[] = array( "type"=> "id", "info" => $property, "optional" => $optional ); continue; } else { $valid = false; if (!$isOptChain) { break; } } } if ($this->scanner->consume("[")) { if (($property = $this->isolateContext( array("allowIn" => true), "parseExpression" )) && $this->scanner->consume("]") ) { $valid = true; $properties[] = array( "type" => "computed", "info" => array( $property, $this->scanner->getPosition() ), "optional" => $optional ); } else { $valid = false; break; } } elseif ($property = $this->parseTemplateLiteral(true)) { if ($optionalChain) { $this->error( "Optional chain can't appear in tagged template expressions" ); } $valid = true; $properties[] = array( "type"=> "template", "info" => $property, "optional" => $optional ); } elseif (($args = $this->parseArguments()) !== null) { $valid = true; $properties[] = array( "type"=> "args", "info" => array($args, $this->scanner->getPosition()), "optional" => $optional ); } else { break; } } $propCount = count($properties); if (!$valid) { $this->error(); } elseif (!$propCount && !$newTokensCount) { return $object; } $node = null; $endPos = $object->location->end; $optionalChainStarted = false; foreach ($properties as $i => $property) { $lastNode = $node ?: $object; if ($property["optional"]) { $optionalChainStarted = true; } if ($property["type"] === "args") { if ($newTokensCount) { if ($optionalChainStarted) { $this->error( "Optional chain can't appear in new expressions" ); } $node = $this->createNode( "NewExpression", array_pop($newTokens) ); $newTokensCount--; } else { $node = $this->createNode("CallExpression", $lastNode); $node->setOptional($property["optional"]); } $node->setCallee($lastNode); $node->setArguments($property["info"][0]); $endPos = $property["info"][1]; } elseif ($property["type"] === "id") { $node = $this->createNode("MemberExpression", $lastNode); $node->setObject($lastNode); $node->setOptional($property["optional"]); $node->setProperty($property["info"]); $endPos = $property["info"]->location->end; } elseif ($property["type"] === "computed") { $node = $this->createNode("MemberExpression", $lastNode); $node->setObject($lastNode); $node->setProperty($property["info"][0]); $node->setOptional($property["optional"]); $node->setComputed(true); $endPos = $property["info"][1]; } elseif ($property["type"] === "template") { $node = $this->createNode("TaggedTemplateExpression", $object); $node->setTag($lastNode); $node->setQuasi($property["info"]); $endPos = $property["info"]->location->end; } $node = $this->completeNode($node, $endPos); } //Wrap the result in multiple NewExpression if there are "new" tokens if ($newTokensCount) { for ($i = $newTokensCount - 1; $i >= 0; $i--) { $lastNode = $node ?: $object; $node = $this->createNode("NewExpression", $newTokens[$i]); $node->setCallee($lastNode); $node = $this->completeNode($node); } } //Wrap the result in a chain expression if required if ($optionalChain) { $prevNode = $node; $node = $this->createNode("ChainExpression", $prevNode); $node->setExpression($prevNode); $node = $this->completeNode($node); } return $node; } /** * Parses a spread element * * @return Node\SpreadElement|null */ protected function parseSpreadElement() { if ($token = $this->scanner->consume("...")) { $argument = $this->isolateContext( array("allowIn" => true), "parseAssignmentExpression" ); if ($argument) { $node = $this->createNode("SpreadElement", $token); $node->setArgument($argument); return $this->completeNode($node); } $this->error(); } return null; } /** * Parses an array literal * * @return Node\ArrayExpression|null */ protected function parseArrayLiteral() { if ($token = $this->scanner->consume("[")) { $elements = array(); while (true) { if ($elision = $this->parseElision()) { $elements = array_merge( $elements, array_fill(0, $elision, null) ); } if (($element = $this->parseSpreadElement()) || ($element = $this->isolateContext( array("allowIn" => true), "parseAssignmentExpression" )) ) { $elements[] = $element; if (!$this->scanner->consume(",")) { break; } } else { break; } } if ($this->scanner->consume("]")) { $node = $this->createNode("ArrayExpression", $token); $node->setElements($elements); return $this->completeNode($node); } $this->error(); } return null; } /** * Parses an arguments list wrapped in round brackets * * @return array|null */ protected function parseArguments() { if ($this->scanner->consume("(")) { if (($args = $this->parseArgumentList()) !== null && $this->scanner->consume(")") ) { return $args; } $this->error(); } return null; } /** * Parses an arguments list * * @return array|null */ protected function parseArgumentList() { $list = array(); $hasComma = false; while (true) { $spread = $this->scanner->consume("..."); $exp = $this->isolateContext( array("allowIn" => true), "parseAssignmentExpression" ); if (!$exp) { //If there's no expression and the spread dots have been found //or there is a trailing comma that is not allowed, throw an //error if ($spread || ($hasComma && !$this->features->trailingCommaFunctionCallDeclaration)) { $this->error(); } break; } if ($spread) { $node = $this->createNode("SpreadElement", $spread); $node->setArgument($exp); $list[] = $this->completeNode($node); } else { $list[] = $exp; } if (!$this->scanner->consume(",")) { break; } $hasComma = true; } return $list; } /** * Parses a super call or a super property * * @return Node\Node|null */ protected function parseSuperPropertyOrCall() { if ($token = $this->scanner->consume("super")) { $super = $this->completeNode($this->createNode("Super", $token)); if (($args = $this->parseArguments()) !== null) { $node = $this->createNode("CallExpression", $token); $node->setArguments($args); $node->setCallee($super); return $this->completeNode($node); } $node = $this->createNode("MemberExpression", $token); $node->setObject($super); if ($this->scanner->consume(".")) { if ($property = $this->parseIdentifier(static::$identifierName)) { $node->setProperty($property); return $this->completeNode($node); } } elseif ($this->scanner->consume("[") && ($property = $this->isolateContext( array("allowIn" => true), "parseExpression" )) && $this->scanner->consume("]") ) { $node->setProperty($property); $node->setComputed(true); return $this->completeNode($node); } $this->error(); } return null; } /** * Parses a primary expression * * @return Node\Node|null */ protected function parsePrimaryExpression() { if ($token = $this->scanner->consume("this")) { $node = $this->createNode("ThisExpression", $token); return $this->completeNode($node); } elseif ($exp = $this->parseFunctionOrGeneratorExpression()) { return $exp; } elseif ($exp = $this->parseClassExpression()) { return $exp; } elseif ($exp = $this->parseIdentifier(static::$identifierReference)) { return $exp; } elseif ($exp = $this->parseLiteral()) { return $exp; } elseif ($exp = $this->parseArrayLiteral()) { return $exp; } elseif ($exp = $this->parseObjectLiteral()) { return $exp; } elseif ($exp = $this->parseRegularExpressionLiteral()) { return $exp; } elseif ($exp = $this->parseTemplateLiteral()) { return $exp; } elseif ($this->jsx && ($exp = $this->parseJSXFragment())) { return $exp; } elseif ($this->jsx && ($exp = $this->parseJSXElement())) { return $exp; } elseif ($token = $this->scanner->consume("(")) { if (($exp = $this->isolateContext( array("allowIn" => true), "parseExpression" )) && $this->scanner->consume(")") ) { $node = $this->createNode("ParenthesizedExpression", $token); $node->setExpression($exp); return $this->completeNode($node); } $this->error(); } return null; } /** * Parses a private identifier * * @return Node\PrivateIdentifier|null */ protected function parsePrivateIdentifier() { $token = $this->scanner->getToken(); if (!$token || $token->type !== Token::TYPE_PRIVATE_IDENTIFIER) { return null; } $this->scanner->consumeToken(); $node = $this->createNode("PrivateIdentifier", $token); $node->setName(substr($token->value, 1)); return $this->completeNode($node); } /** * Parses an identifier * * @param int $mode Parsing mode, one of the id parsing mode * constants * @param string $after If a string is passed in this parameter, the * identifier is parsed only if precedes this string * * @return Node\Identifier|null */ protected function parseIdentifier($mode, $after = null) { $token = $this->scanner->getToken(); if (!$token) { return null; } if ($after !== null) { $next = $this->scanner->getNextToken(); if (!$next || $next->value !== $after) { return null; } } $type = $token->type; switch ($type) { case Token::TYPE_BOOLEAN_LITERAL: case Token::TYPE_NULL_LITERAL: if ($mode !== self::ID_ALLOW_ALL) { return null; } break; case Token::TYPE_KEYWORD: if ($mode === self::ID_ALLOW_NOTHING) { return null; } elseif ($mode === self::ID_MIXED && $this->scanner->isStrictModeKeyword($token) ) { return null; } break; default: if ($type !== Token::TYPE_IDENTIFIER) { return null; } break; } //Exclude keywords that depend on parser context $value = $token->value; if ($mode === self::ID_MIXED && isset($this->contextKeywords[$value]) && $this->context->{$this->contextKeywords[$value]} ) { return null; } $this->scanner->consumeToken(); $node = $this->createNode("Identifier", $token); $node->setRawName($value); return $this->completeNode($node); } /** * Parses a literal * * @return Node\Literal|null */ protected function parseLiteral() { if ($token = $this->scanner->getToken()) { if ($token->type === Token::TYPE_NULL_LITERAL) { $this->scanner->consumeToken(); $node = $this->createNode("NullLiteral", $token); return $this->completeNode($node); } elseif ($token->type === Token::TYPE_BOOLEAN_LITERAL) { $this->scanner->consumeToken(); $node = $this->createNode("BooleanLiteral", $token); $node->setRaw($token->value); return $this->completeNode($node); } elseif ($literal = $this->parseStringLiteral()) { return $literal; } elseif ($literal = $this->parseNumericLiteral()) { return $literal; } } return null; } /** * Parses a string literal * * @return Node\StringLiteral|null */ protected function parseStringLiteral() { $token = $this->scanner->getToken(); if ($token && $token->type === Token::TYPE_STRING_LITERAL) { $val = $token->value; $this->checkInvalidEscapeSequences($val); $this->scanner->consumeToken(); $node = $this->createNode("StringLiteral", $token); $node->setRaw($val); return $this->completeNode($node); } return null; } /** * Parses a numeric literal * * @return Node\NumericLiteral|Node\BigIntLiteral|null */ protected function parseNumericLiteral() { $token = $this->scanner->getToken(); if ($token && $token->type === Token::TYPE_NUMERIC_LITERAL) { $val = $token->value; $this->checkInvalidEscapeSequences($val, true); $this->scanner->consumeToken(); $node = $this->createNode("NumericLiteral", $token); $node->setRaw($val); return $this->completeNode($node); } elseif ($token && $token->type === Token::TYPE_BIGINT_LITERAL) { $val = $token->value; $this->checkInvalidEscapeSequences($val, true); $this->scanner->consumeToken(); $node = $this->createNode("BigIntLiteral", $token); $node->setRaw($val); return $this->completeNode($node); } return null; } /** * Parses a template literal * * @param bool $tagged True if the template is tagged * * @return Node\Literal|null */ protected function parseTemplateLiteral($tagged = false) { $token = $this->scanner->getToken(); if (!$token || $token->type !== Token::TYPE_TEMPLATE) { return null; } //Do not parse templates parts $val = $token->value; if ($val[0] !== "`") { return null; } $quasis = $expressions = array(); $valid = false; do { $this->scanner->consumeToken(); $val = $token->value; $this->checkInvalidEscapeSequences($val, false, true, $tagged); $lastChar = substr($val, -1); $quasi = $this->createNode("TemplateElement", $token); $quasi->setRawValue($val); if ($lastChar === "`") { $quasi->setTail(true); $quasis[] = $this->completeNode($quasi); $valid = true; break; } else { $quasis[] = $this->completeNode($quasi); $exp = $this->isolateContext( array("allowIn" => true), "parseExpression" ); if ($exp) { $expressions[] = $exp; } else { $valid = false; break; } } $token = $this->scanner->getToken(); } while ($token && $token->type === Token::TYPE_TEMPLATE); if ($valid) { $node = $this->createNode("TemplateLiteral", $quasis[0]); $node->setQuasis($quasis); $node->setExpressions($expressions); return $this->completeNode($node); } $this->error(); } /** * Parses a regular expression literal * * @return Node\Literal|null */ protected function parseRegularExpressionLiteral() { if ($token = $this->scanner->reconsumeCurrentTokenAsRegexp()) { $this->scanner->consumeToken(); $node = $this->createNode("RegExpLiteral", $token); $node->setRaw($token->value); return $this->completeNode($node); } return null; } /** * Parse directive prologues. The result is an array where the first element * is the array of parsed nodes and the second element is the array of * directive prologues values * * @return array|null */ protected function parseDirectivePrologues() { $directives = $nodes = array(); while (($token = $this->scanner->getToken()) && $token->type === Token::TYPE_STRING_LITERAL ) { $directive = substr($token->value, 1, -1); if ($directive === "use strict") { $directives[] = $directive; $directiveNode = $this->parseStringLiteral(); $this->assertEndOfStatement(); $node = $this->createNode("ExpressionStatement", $directiveNode); $node->setExpression($directiveNode); $nodes[] = $this->completeNode($node); } else { break; } } return count($nodes) ? array($nodes, $directives) : null; } /** * Parses an import call * * @return Node\Node|null */ protected function parseImportCall() { if (($token = $this->scanner->consume("import")) && $this->scanner->consume("(")) { if (($source = $this->isolateContext( array("allowIn" => true), "parseAssignmentExpression" )) && $this->scanner->consume(")") ) { $node = $this->createNode("ImportExpression", $token); $node->setSource($source); return $this->completeNode($node); } $this->error(); } return null; } /** * Checks if the given string or number contains invalid escape sequences * * @param string $val Value to check * @param bool $number True if the value is a number * @param bool $forceLegacyOctalCheck True to force legacy octal * form check * @param bool $taggedTemplate True if the value is a tagged * template * * @return void */ protected function checkInvalidEscapeSequences( $val, $number = false, $forceLegacyOctalCheck = false, $taggedTemplate = false ) { if ($this->features->skipEscapeSeqCheckInTaggedTemplates && $taggedTemplate) { return; } $checkLegacyOctal = $forceLegacyOctalCheck || $this->scanner->getStrictMode(); if ($number) { if ($val && $val[0] === "0" && preg_match("#^0[0-9_]+$#", $val)) { if ($checkLegacyOctal) { $this->error( "Octal literals are not allowed in strict mode" ); } if ($this->features->numericLiteralSeparator && strpos($val, '_') !== false ) { $this->error( "Numeric separators are not allowed in legacy octal numbers" ); } } } elseif (strpos($val, "\\") !== false) { $hex = "0-9a-fA-F"; $invalidSyntax = array( "x[$hex]?[^$hex]", "x[$hex]?$", "u\{\}", "u\{(?:[$hex]*[^$hex\}]+)+[$hex]*\}", "u\{[^\}]*$", "u(?!{)[$hex]{0,3}[^$hex\{]", "u[$hex]{0,3}$" ); if ($checkLegacyOctal) { $invalidSyntax[] = "\d{2}"; $invalidSyntax[] = "[1-7]"; $invalidSyntax[] = "0[89]"; } $reg = "#(\\\\+)(" . implode("|", $invalidSyntax) . ")#"; if (preg_match_all($reg, $val, $matches, PREG_SET_ORDER)) { foreach ($matches as $match) { if (strlen($match[1]) % 2) { $first = $match[2][0]; if ($first === "u") { $err = "Malformed unicode escape sequence"; } elseif ($first === "x") { $err = "Malformed hexadecimal escape sequence"; } else { $err = "Octal literals are not allowed in strict mode"; } $this->error($err); } } } } } } * * For the full copyright and license information refer to the LICENSE file * distributed with this source code */ namespace Peast\Syntax; /** * Parser features class * * @author Marco Marchiò * * @codeCoverageIgnore */ class Features { /** * Exponentiation operator * * @var bool */ public $exponentiationOperator = false; /** * Async/await * * @var bool */ public $asyncAwait = false; /** * Trailing comma in function calls and declarations * * @var bool */ public $trailingCommaFunctionCallDeclaration = false; /** * For-in initializer * * @var bool */ public $forInInitializer = false; /** * Async iteration and generators * * @var bool */ public $asyncIterationGenerators = false; /** * Rest/spread properties * * @var bool */ public $restSpreadProperties = false; /** * Skip escape sequences checks in tagged template * * @var bool */ public $skipEscapeSeqCheckInTaggedTemplates = false; /** * Optional catch binding * * @var bool */ public $optionalCatchBinding = false; /** * Paragraph and line separator in strings * * @var bool */ public $paragraphLineSepInStrings = false; /** * Dynamic import * * @var bool */ public $dynamicImport = false; /** * BigInt literals * * @var bool */ public $bigInt = false; /** * Exported name for export all declarations * * @var bool */ public $exportedNameInExportAll = false; /** * Import.meta * * @var bool */ public $importMeta = false; /** * Coalescing operator * * @var bool */ public $coalescingOperator = false; /** * Optional chaining * * @var bool */ public $optionalChaining = false; /** * Logical assignment operators * * @var bool */ public $logicalAssignmentOperators = false; /** * Numeric literal separator * * @var bool */ public $numericLiteralSeparator = false; /** * Private methods and fields * * @var bool */ public $privateMethodsAndFields = false; /** * Class fields * * @var bool */ public $classFields = false; /** * "in" operator for private fields * * @var bool */ public $classFieldsPrivateIn = false; /** * Top level await * * @var bool */ public $topLevelAwait = false; /** * Class static block * * @var bool */ public $classStaticBlock = false; /** * Arbitrary module namespace identifier names * * @var bool */ public $arbitraryModuleNSNames = false; } * * For the full copyright and license information refer to the LICENSE file * distributed with this source code */ namespace Peast\Syntax; /** * A token emitted by the tokenizer. * * @author Marco Marchiò */ class Token implements \JSONSerializable { //Type constants /** * Boolean literal */ const TYPE_BOOLEAN_LITERAL = "Boolean"; /** * Identifier */ const TYPE_IDENTIFIER = "Identifier"; /** * Private identifier */ const TYPE_PRIVATE_IDENTIFIER = "PrivateIdentifier"; /** * Keyword */ const TYPE_KEYWORD = "Keyword"; /** * Null literal */ const TYPE_NULL_LITERAL = "Null"; /** * Numeric literal */ const TYPE_NUMERIC_LITERAL = "Numeric"; /** * BigInt literal */ const TYPE_BIGINT_LITERAL = "BigInt"; /** * Punctuator */ const TYPE_PUNCTUATOR = "Punctuator"; //This constant is kept only for backward compatibility since it was //first written with a typo const TYPE_PUNCTUTATOR = "Punctuator"; /** * String literal */ const TYPE_STRING_LITERAL = "String"; /** * Regular expression */ const TYPE_REGULAR_EXPRESSION = "RegularExpression"; /** * Template */ const TYPE_TEMPLATE = "Template"; /** * Comment */ const TYPE_COMMENT = "Comment"; /** * JSX text */ const TYPE_JSX_TEXT = "JSXText"; /** * JSX identifier */ const TYPE_JSX_IDENTIFIER = "JSXIdentifier"; /** * Tokens' type that is one of the type constants * * @var string */ public $type; /** * Token's value * * @var string */ public $value; /** * Token's location in the source code * * @var SourceLocation */ public $location; /** * Class constructor * * @param string $type Token's type * @param string $value Token's value */ public function __construct($type, $value) { $this->type = $type; $this->value = $value; $this->location = new SourceLocation(); } /** * Returns the token's type * * @return string */ public function getType() { return $this->type; } /** * Returns the token's value * * @return string */ public function getValue() { return $this->value; } /** * Returns the token's location in the source code * * @return SourceLocation */ public function getLocation() { return $this->location; } /** * Sets the start position of the token in the source code * * @param Position $position Start position * * @return $this */ public function setStartPosition(Position $position) { $this->location->start = $position; return $this; } /** * Sets the end position of the token in the source code * * @param Position $position End position * * @return $this */ public function setEndPosition(Position $position) { $this->location->end = $position; return $this; } /** * Returns a serializable version of the node * * @return array */ #[\ReturnTypeWillChange] public function jsonSerialize() { return array( "type" => $this->type, "value" => $this->value, "location" => $this->location ); } } * * For the full copyright and license information refer to the LICENSE file * distributed with this source code */ namespace Peast\Syntax; /** * This class represents the position in the source code. * * @author Marco Marchiò */ class Position implements \JSONSerializable { /** * Source line * * @var int */ protected $line; /** * Source column * * @var int */ protected $column; /** * Source index * * @var int */ protected $index; /** * Class constructor * * @param int $line Source line * @param int $column Source column * @param int $index Source index */ function __construct($line, $column, $index) { $this->line = $line; $this->column = $column; $this->index = $index; } /** * Returns the source line * * @return int */ public function getLine() { return $this->line; } /** * Returns the source column * * @return int */ public function getColumn() { return $this->column; } /** * Returns the source index * * @return int */ public function getIndex() { return $this->index; } /** * Returns a serializable version of the object * * @return array */ #[\ReturnTypeWillChange] public function jsonSerialize() { return array( "line" => $this->getLine(), "column" => $this->getColumn(), "index" => $this->getIndex() ); } } * * For the full copyright and license information refer to the LICENSE file * distributed with this source code */ namespace Peast\Syntax; /** * Events emitter class. An instance of this class is used by Parser and Scanner * to emit events and attach listeners to them * * @author Marco Marchiò */ class EventsEmitter { /** * Events registry array * * @var array */ protected $eventsRegistry = array(); /** * Attaches a listener function to the given event * * @param string $event Event name * @param callable $listener Listener function * * @return $this */ public function addListener($event, $listener) { if (!isset($this->eventsRegistry[$event])) { $this->eventsRegistry[$event] = array(); } $this->eventsRegistry[$event][] = $listener; return $this; } /** * Fires an event * * @param string $event Event name * @param array $args Arguments to pass to functions attached to the * event * * @return $this */ public function fire($event, $args = array()) { if (isset($this->eventsRegistry[$event])) { foreach ($this->eventsRegistry[$event] as $listener) { call_user_func_array($listener, $args); } } return $this; } } * * For the full copyright and license information refer to the LICENSE file * distributed with this source code */ namespace Peast\Syntax; /** * Syntax exception class. Syntax errors in the source are thrown using this * using this exception class. * * @author Marco Marchiò * * @codeCoverageIgnore */ class Exception extends \Exception { /** * Error position * * @var Position */ protected $position; /** * Class constructor * * @param string $message Error message * @param Position $position Error position */ public function __construct($message, Position $position) { parent::__construct($message); $this->position = $position; } /** * Returns the error position * * @return Position */ public function getPosition() { return $this->position; } } * * For the full copyright and license information refer to the LICENSE file * distributed with this source code */ namespace Peast\Syntax\ES2019; /** * ES2019 features class * * @author Marco Marchiò * * @codeCoverageIgnore */ class Features extends \Peast\Syntax\ES2018\Features { /** * Optional catch binding * * @var bool */ public $optionalCatchBinding = true; /** * Paragraph and line separator in strings * * @var bool */ public $paragraphLineSepInStrings = true; } * * For the full copyright and license information refer to the LICENSE file * distributed with this source code */ namespace Peast\Syntax; /** * Utilities class. * * @author Marco Marchiò */ class Utils { /** * Converts a string to an array of UTF-8 characters * * @param string $str String to convert * @param bool $strictEncoding If false and the string contains invalid * UTF-8 characters, it will replace those * characters with the one defined in the * mbstring.substitute_character setting * * @return array * * @throws EncodingException */ static public function stringToUTF8Array($str, $strictEncoding = true) { if ($str === "") { return array(); } $ret = preg_split('//u', $str, -1, PREG_SPLIT_NO_EMPTY); if (preg_last_error() === PREG_BAD_UTF8_ERROR) { if (!$strictEncoding) { $str = mb_convert_encoding($str, 'UTF-8', 'UTF-8'); $ret = self::stringToUTF8Array($str, false); } else { throw new EncodingException("String contains invalid UTF-8"); } } return $ret; } /** * Converts an unicode code point to UTF-8 * * @param int $num Unicode code point * * @return string * * @codeCoverageIgnore */ static public function unicodeToUtf8($num) { //From: http://stackoverflow.com/questions/1805802/php-convert-unicode-codepoint-to-utf-8#answer-7153133 if ($num <= 0x7F) { return chr($num); } elseif ($num <= 0x7FF) { return chr(($num >> 6) + 192) . chr(($num & 63) + 128); } elseif ($num <= 0xFFFF) { return chr(($num >> 12) + 224) . chr((($num >> 6) & 63) + 128) . chr(($num & 63) + 128); } elseif ($num <= 0x1FFFFF) { return chr(($num >> 18) + 240) . chr((($num >> 12) & 63) + 128) . chr((($num >> 6) & 63) + 128) . chr(($num & 63) + 128); } return ''; } /** * Compiled line terminators cache * * @var array */ protected static $lineTerminatorsCache; /** * Returns line terminators array * * @return array */ protected static function getLineTerminators() { if (!self::$lineTerminatorsCache) { self::$lineTerminatorsCache = array(); foreach (Scanner::$lineTerminatorsChars as $char) { self::$lineTerminatorsCache[] = is_int($char) ? self::unicodeToUtf8($char) : $char; } } return self::$lineTerminatorsCache; } /** * Converts a surrogate pair of Unicode code points to UTF-8 * * @param string $first First Unicode code point * @param string $second Second Unicode code point * * @return string * * @codeCoverageIgnore */ static public function surrogatePairToUtf8($first, $second) { //From: https://stackoverflow.com/questions/39226593/how-to-convert-utf16-surrogate-pairs-to-equivalent-hex-codepoint-in-php $value = ((hexdec($first) & 0x3ff) << 10) | (hexdec($second) & 0x3ff); return self::unicodeToUtf8($value + 0x10000); } /** * This function takes a string as it appears in the source code and returns * an unquoted version of it * * @param string $str The string to unquote * * @return string */ static public function unquoteLiteralString($str) { //Remove quotes $str = substr($str, 1, -1); //Return immediately if the escape character is missing if (strpos($str, "\\") === false) { return $str; } $lineTerminators = self::getLineTerminators(); //Surrogate pairs regex $surrogatePairsReg = sprintf( 'u(?:%1$s|\{%1$s\})\\\\u(?:%2$s|\{%2$s\})', "[dD][89abAB][0-9a-fA-F]{2}", "[dD][c-fC-F][0-9a-fA-F]{2}" ); //Handle escapes $patterns = array( $surrogatePairsReg, "u\{[a-fA-F0-9]+\}", "u[a-fA-F0-9]{4}", "x[a-fA-F0-9]{2}", "0[0-7]{2}", "[1-7][0-7]", "." ); $reg = "/\\\\(" . implode("|", $patterns) . ")/s"; $simpleSequence = array( "n" => "\n", "f" => "\f", "r" => "\r", "t" => "\t", "v" => "\v", "b" => "\x8" ); $replacement = function ($m) use ($simpleSequence, $lineTerminators) { $type = $m[1][0]; if (isset($simpleSequence[$type])) { // \n, \r, \t ... return $simpleSequence[$type]; } elseif ($type === "u" || $type === "x") { //Invalid unicode or hexadecimal sequences if (strlen($m[1]) === 1) { return "\\$type"; } // Surrogate pair if ($type === "u" && strpos($m[1], "\\") !== false) { $points = explode("\\", $m[1]); return Utils::surrogatePairToUtf8( str_replace(array("{", "}", "u"), "", $points[0]), str_replace(array("{", "}", "u"), "", $points[1]) ); } // \uFFFF, \u{FFFF}, \xFF $code = substr($m[1], 1); $code = str_replace(array("{", "}"), "", $code); return Utils::unicodeToUtf8(hexdec($code)); } elseif ($type >= "0" && $type <= "7") { //Invalid octal sequences if (strlen($m[1]) === 1) { return "\\$type"; } // \123 return Utils::unicodeToUtf8(octdec($m[1])); } elseif (in_array($m[1], $lineTerminators)) { // Escaped line terminators return ""; } else { // Escaped characters return $m[1]; } }; return preg_replace_callback($reg, $replacement, $str); } /** * This function converts a string to a quoted javascript string * * @param string $str String to quote * @param string $quote Quote character * * @return string */ static public function quoteLiteralString($str, $quote) { $escape = self::getLineTerminators(); $escape[] = $quote; $escape[] = "\\\\"; $reg = "/(" . implode("|", $escape) . ")/"; $str = preg_replace($reg, "\\\\$1", $str); return $quote . $str . $quote; } /** * Returns the properties map for the given node * * @param mixed $node Node or class to consider * * @return array */ static protected function getPropertiesMap($node) { static $cache = array(); if ($node instanceof \ReflectionClass) { $className = $node->getName(); } else { $className = get_class($node); } if (!isset($cache[$className])) { $class = new \ReflectionClass($className); $parent = $class->getParentClass(); $props = $parent ? self::getPropertiesMap($parent) : array(); $defaults = $class->getDefaultProperties(); if (isset($defaults["propertiesMap"])) { $props = array_merge($props, $defaults["propertiesMap"]); } $cache[$className] = $props; } return $cache[$className]; } /** * Returns the properties list for the given node * * @param Node\Node $node Node to consider * @param bool $traversable If true it returns only traversable properties * * @return array */ static public function getNodeProperties(Node\Node $node, $traversable = false) { $props = self::getPropertiesMap($node); return array_map( function ($prop) { $ucProp = ucfirst($prop); return array( "name" => $prop, "getter" => "get$ucProp", "setter" => "set$ucProp" ); }, array_keys($traversable ? array_filter($props) : $props) ); } /** * Returns an expanded version of the traversable node properties. * The return of the function is an array of node properties * values with arrays flattened * * @param Node\Node $node Node * * @return array */ static public function getExpandedNodeProperties(Node\Node $node) { $ret = array(); $props = self::getNodeProperties($node, true); foreach ($props as $prop) { $val = $node->{$prop["getter"]}(); if (is_array($val)) { $ret = array_merge($ret, $val); } else { $ret[] = $val; } } return $ret; } /** * Delete an array element by value * * @param array $array Array * @param mixed $val Value to remove * * @return void */ static public function removeArrayValue(&$array, $val) { array_splice($array, array_search($val, $array), 1); } } * * For the full copyright and license information refer to the LICENSE file * distributed with this source code */ namespace Peast\Syntax; /** * This class represents a location in the source code with start and end * position. * * @author Marco Marchiò */ class SourceLocation implements \JSONSerializable { /** * Start position * * @var Position */ public $start; /** * End position * * @var Position */ public $end; /** * Returns the start position * * @return Position */ public function getStart() { return $this->start; } /** * Sets the start position * * @param Position $position Start position * * @return $this */ public function setStart(Position $position) { $this->start = $position; return $this; } /** * Returns the end position * * @return Position */ public function getEnd() { return $this->end; } /** * Sets the end position * * @param Position $position End position * * @return $this */ public function setEnd(Position $position) { $this->end = $position; return $this; } /** * Returns a serializable version of the object * * @return array */ #[\ReturnTypeWillChange] public function jsonSerialize() { return array( "start" => $this->start, "end" => $this->end ); } } * * For the full copyright and license information refer to the LICENSE file * distributed with this source code */ namespace Peast\Syntax; /** * Longest Sequence Matcher. Utility class used by the scanner to consume * the longest sequence of character given a set of allowed characters sequences. * * @author Marco Marchiò */ class LSM { /** * Internal sequences map * * @var array */ protected $map = array(); /** * Encoding handle flag * * @var bool */ protected $handleEncoding = false; /** * Class constructor * * @param array $sequences Allowed characters sequences * @param bool $handleEncoding True to handle encoding when matching */ function __construct($sequences, $handleEncoding = false) { $this->handleEncoding = $handleEncoding; foreach ($sequences as $s) { $this->add($s); } } /** * Adds a sequence * * @param string $sequence Sequence to add * * @return $this */ public function add($sequence) { if ($this->handleEncoding) { $s = Utils::stringToUTF8Array($sequence); $first = $s[0]; $len = count($s); } else { $first = $sequence[0]; $len = strlen($sequence); } if (!isset($this->map[$first])) { $this->map[$first] = array( "maxLen" => $len, "map" => array($sequence) ); } else { $this->map[$first]["map"][] = $sequence; $this->map[$first]["maxLen"] = max($this->map[$first]["maxLen"], $len); } return $this; } /** * Removes a sequence * * @param string $sequence Sequence to remove * * @return $this */ public function remove($sequence) { if ($this->handleEncoding) { $s = Utils::stringToUTF8Array($sequence); $first = $s[0]; } else { $first = $sequence[0]; } if (isset($this->map[$first])) { $len = $this->handleEncoding ? count($s) : strlen($sequence); $this->map[$first]["map"] = array_diff( $this->map[$first]["map"], array($sequence) ); if (!count($this->map[$first]["map"])) { unset($this->map[$first]); } elseif ($this->map[$first]["maxLen"] === $len) { // Recalculate the max length if necessary foreach ($this->map[$first]["map"] as $m) { $this->map[$first]["maxLen"] = max( $this->map[$first]["maxLen"], strlen($m) ); } } } return $this; } /** * Executes the match. It returns an array where the first element is the * number of consumed characters and the second element is the match. If * no match is found it returns null. * * @param Scanner $scanner Scanner instance * @param int $index Current index * @param string $char Current character * * @return array|null */ public function match($scanner, $index, $char) { $consumed = 1; $bestMatch = null; if (isset($this->map[$char])) { //If the character is present in the map and it has a max length of //1, match immediately if ($this->map[$char]["maxLen"] === 1) { $bestMatch = array($consumed, $char); } else { //Otherwise consume a number of characters equal to the max //length and find the longest match $buffer = $char; $map = $this->map[$char]["map"]; $maxLen = $this->map[$char]["maxLen"]; do { if (in_array($buffer, $map)) { $bestMatch = array($consumed, $buffer); } $nextChar = $scanner->charAt($index + $consumed); if ($nextChar === null) { break; } $buffer .= $nextChar; $consumed++; } while ($consumed <= $maxLen); } } return $bestMatch; } } * * For the full copyright and license information refer to the LICENSE file * distributed with this source code */ namespace Peast\Syntax; /** * Encoding exception class. Encoding errors while scanning the source are * thrown using this using this exception class. * * @author Marco Marchiò * * @codeCoverageIgnore */ class EncodingException extends \Exception { } * * For the full copyright and license information refer to the LICENSE file * distributed with this source code */ namespace Peast\Syntax\ES2018; /** * ES2018 features class * * @author Marco Marchiò * * @codeCoverageIgnore */ class Features extends \Peast\Syntax\ES2017\Features { /** * Async iteration and generators * * @var bool */ public $asyncIterationGenerators = true; /** * Rest/spread properties * * @var bool */ public $restSpreadProperties = true; /** * Skip escape sequences checks in tagged template * * @var bool */ public $skipEscapeSeqCheckInTaggedTemplates = true; } * * For the full copyright and license information refer to the LICENSE file * distributed with this source code */ namespace Peast\Syntax\ES2017; /** * ES2017 features class * * @author Marco Marchiò * * @codeCoverageIgnore */ class Features extends \Peast\Syntax\ES2016\Features { /** * Async/await * * @var bool */ public $asyncAwait = true; /** * Trailing comma in function calls and declarations * * @var bool */ public $trailingCommaFunctionCallDeclaration = true; /** * For-in initializer * * @var bool */ public $forInInitializer = true; } * * For the full copyright and license information refer to the LICENSE file * distributed with this source code */ namespace Peast\Syntax\ES2016; /** * ES2016 features class * * @author Marco Marchiò * * @codeCoverageIgnore */ class Features extends \Peast\Syntax\ES2015\Features { /** * Exponentiation operator * * @var bool */ public $exponentiationOperator = true; } * * For the full copyright and license information refer to the LICENSE file * distributed with this source code */ namespace Peast\Syntax\ES2020; /** * ES2020 features class * * @author Marco Marchiò * * @codeCoverageIgnore */ class Features extends \Peast\Syntax\ES2019\Features { /** * Dynamic import * * @var bool */ public $dynamicImport = true; /** * BigInt literals * * @var bool */ public $bigInt = true; /** * Exported name for export all declarations * * @var bool */ public $exportedNameInExportAll = true; /** * Import.meta * * @var bool */ public $importMeta = true; /** * Coalescing operator * * @var bool */ public $coalescingOperator = true; /** * Optional chaining * * @var bool */ public $optionalChaining = true; } * * For the full copyright and license information refer to the LICENSE file * distributed with this source code */ namespace Peast\Formatter; /** * Compact formatter. * * @author Marco Marchiò */ class Expanded extends Base { /** * Boolean that indicates if open curly brackets in code blocks must be * on a new line * * @var bool */ protected $newLineBeforeCurlyBracket = true; /** * Boolean that indicates if content inside round brackets must be * surrounded by spaces * * @var bool */ protected $spacesInsideRoundBrackets = true; } * * For the full copyright and license information refer to the LICENSE file * distributed with this source code */ namespace Peast\Formatter; /** * Compact formatter. * * @author Marco Marchiò */ class Compact extends Base { /** * New line character * * @var string */ protected $newLine = ""; /** * Indentation character * * @var string */ protected $indentation = ""; /** * Boolean that indicates if operators must be surrounded by spaces * * @var bool */ protected $spacesAroundOperators = false; /** * Boolean that indicates if blocks of code must be wrapped in curly * brackets also if they contain only one instruction * * @var bool */ protected $alwaysWrapBlocks = false; } * * For the full copyright and license information refer to the LICENSE file * distributed with this source code */ namespace Peast\Formatter; /** * Base class for formatters, all the formatters must extend this class. * * @author Marco Marchiò * * @abstract */ abstract class Base { /** * New line character * * @var string */ protected $newLine = "\n"; /** * Indentation character * * @var string */ protected $indentation = "\t"; /** * Boolean that indicates if open curly brackets in code blocks must be * on a new line * * @var bool */ protected $newLineBeforeCurlyBracket = false; /** * Boolean that indicates if blocks of code must be wrapped in curly * brackets also if they contain only one instruction * * @var bool */ protected $alwaysWrapBlocks = true; /** * Boolean that indicates if operators must be surrounded by spaces * * @var bool */ protected $spacesAroundOperators = true; /** * Boolean that indicates if content inside round brackets must be * surrounded by spaces * * @var bool */ protected $spacesInsideRoundBrackets = false; /** * Returns the new line character * * @return string */ public function getNewLine() { return $this->newLine; } /** * Returns the indentation character * * @return string */ public function getIndentation() { return $this->indentation; } /** * Returns a boolean that indicates if open curly brackets in code blocks * must be on a new line * * @return bool */ public function getNewLineBeforeCurlyBracket() { return $this->newLineBeforeCurlyBracket; } /** * Returns a boolean that indicates if blocks of code must be wrapped in * curly brackets also if they contain only one instruction * * @return bool */ public function getAlwaysWrapBlocks() { return $this->alwaysWrapBlocks; } /** * Returns a boolean that indicates if operators must be surrounded by * spaces * * @return bool */ public function getSpacesAroundOperator() { return $this->spacesAroundOperators; } /** * Returns a boolean that indicates if content inside round brackets must be * surrounded by spaces * * @return bool */ public function getSpacesInsideRoundBrackets() { return $this->spacesInsideRoundBrackets; } } * * For the full copyright and license information refer to the LICENSE file * distributed with this source code */ namespace Peast\Formatter; /** * Pretty print formatter. * * @author Marco Marchiò */ class PrettyPrint extends Base { }## NAME {{name}} {{#shortdesc}} ## DESCRIPTION {{shortdesc}} {{/shortdesc}} ## SYNOPSIS {{synopsis}} {{#alias}} ## ALIAS {{alias}} {{/alias}} {{#has-subcommands}} ## SUBCOMMANDS {{#subcommands}} {{.}} {{/subcommands}} {{/has-subcommands}} {{#is_subcommand}} {{/is_subcommand}} {{#has_subcommands}} {{/has_subcommands}} {{#has_parameters}} ## GLOBAL PARAMETERS {{#parameters}} {{synopsis}} {{desc}} {{/parameters}} {{/has_parameters}} {{#root_command}} Run 'wp help ' to get more information on a specific command. {{/root_command}} ./tests/ ./tests/test-sample.php .DS_Store phpcs.xml phpunit.xml Thumbs.db wp-cli.local.yml node_modules/ *.sql *.tar.gz *.zip { "name": "{{plugin_slug}}", "version": "0.1.0", "main": "Gruntfile.js", "author": "{{plugin_author}}", "scripts" : { "start" : "grunt default" , "readme" : "grunt readme", "i18n" : "grunt i18n" }, "devDependencies": { "grunt": "~0.4.5", "grunt-wp-i18n": "~0.5.0", "grunt-wp-readme-to-markdown": "~1.0.0" } } = 8 ) { echo "The scaffolded tests cannot currently be run on PHP 8.0+. See https://github.com/wp-cli/scaffold-command/issues/285" . PHP_EOL; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped exit( 1 ); } $_tests_dir = getenv( 'WP_TESTS_DIR' ); if ( ! $_tests_dir ) { $_tests_dir = rtrim( sys_get_temp_dir(), '/\\' ) . '/wordpress-tests-lib'; } if ( ! file_exists( $_tests_dir . '/includes/functions.php' ) ) { echo "Could not find {$_tests_dir}/includes/functions.php, have you run bin/install-wp-tests.sh ?" . PHP_EOL; exit( 1 ); } // Give access to tests_add_filter() function. require_once "{$_tests_dir}/includes/functions.php"; /** * Registers theme. */ function _register_theme() { $theme_dir = dirname( __DIR__ ); $current_theme = basename( $theme_dir ); $theme_root = dirname( $theme_dir ); add_filter( 'theme_root', function () use ( $theme_root ) { return $theme_root; } ); register_theme_directory( $theme_root ); add_filter( 'pre_option_template', function () use ( $current_theme ) { return $current_theme; } ); add_filter( 'pre_option_stylesheet', function () use ( $current_theme ) { return $current_theme; } ); } tests_add_filter( 'muplugins_loaded', '_register_theme' ); // Start up the WP testing environment. require "{$_tests_dir}/includes/bootstrap.php"; assertTrue( true ); } } '', // Unused. Messages start at index 1. 1 => __( '{{label_ucfirst}} added.', '{{textdomain}}' ), 2 => __( '{{label_ucfirst}} deleted.', '{{textdomain}}' ), 3 => __( '{{label_ucfirst}} updated.', '{{textdomain}}' ), 4 => __( '{{label_ucfirst}} not added.', '{{textdomain}}' ), 5 => __( '{{label_ucfirst}} not updated.', '{{textdomain}}' ), 6 => __( '{{label_plural_ucfirst}} deleted.', '{{textdomain}}' ), ]; return $messages; } add_filter( 'term_updated_messages', '{{machine_name}}_updated_messages' ); Generally-applicable sniffs for WordPress plugins. . /vendor/ /node_modules/ module.exports = function( grunt ) { 'use strict'; // Project configuration grunt.initConfig( { pkg: grunt.file.readJSON( 'package.json' ), addtextdomain: { options: { textdomain: '{{textdomain}}', }, update_all_domains: { options: { updateDomains: true }, src: [ '*.php', '**/*.php', '!\.git/**/*', '!bin/**/*', '!node_modules/**/*', '!tests/**/*' ] } }, wp_readme_to_markdown: { your_target: { files: { 'README.md': 'readme.txt' } }, }, makepot: { target: { options: { domainPath: '/languages', exclude: [ '\.git/*', 'bin/*', 'node_modules/*', 'tests/*' ], mainFile: '{{plugin_slug}}.php', potFilename: '{{plugin_slug}}.pot', potHeaders: { poedit: true, 'x-poedit-keywordslist': true }, type: 'wp-plugin', updateTimestamp: true } } }, } ); grunt.loadNpmTasks( 'grunt-wp-i18n' ); grunt.loadNpmTasks( 'grunt-wp-readme-to-markdown' ); grunt.registerTask( 'default', [ 'i18n','readme' ] ); grunt.registerTask( 'i18n', ['addtextdomain', 'makepot'] ); grunt.registerTask( 'readme', ['wp_readme_to_markdown'] ); grunt.util.linefeed = '\n'; }; assertTrue( true ); } } register_taxonomy( '{{slug}}', [ {{post_types}} ], [ 'hierarchical' => false, 'public' => true, 'show_in_nav_menus' => true, 'show_ui' => true, 'show_admin_column' => false, 'query_var' => true, 'rewrite' => true, 'capabilities' => [ 'manage_terms' => 'edit_posts', 'edit_terms' => 'edit_posts', 'delete_terms' => 'edit_posts', 'assign_terms' => 'edit_posts', ], 'labels' => [ 'name' => __( '{{label_plural_ucfirst}}', '{{textdomain}}' ), 'singular_name' => _x( '{{label_ucfirst}}', 'taxonomy general name', '{{textdomain}}' ), 'search_items' => __( 'Search {{label_plural_ucfirst}}', '{{textdomain}}' ), 'popular_items' => __( 'Popular {{label_plural_ucfirst}}', '{{textdomain}}' ), 'all_items' => __( 'All {{label_plural_ucfirst}}', '{{textdomain}}' ), 'parent_item' => __( 'Parent {{label_ucfirst}}', '{{textdomain}}' ), 'parent_item_colon' => __( 'Parent {{label_ucfirst}}:', '{{textdomain}}' ), 'edit_item' => __( 'Edit {{label_ucfirst}}', '{{textdomain}}' ), 'update_item' => __( 'Update {{label_ucfirst}}', '{{textdomain}}' ), 'view_item' => __( 'View {{label_ucfirst}}', '{{textdomain}}' ), 'add_new_item' => __( 'Add New {{label_ucfirst}}', '{{textdomain}}' ), 'new_item_name' => __( 'New {{label_ucfirst}}', '{{textdomain}}' ), 'separate_items_with_commas' => __( 'Separate {{label_plural}} with commas', '{{textdomain}}' ), 'add_or_remove_items' => __( 'Add or remove {{label_plural}}', '{{textdomain}}' ), 'choose_from_most_used' => __( 'Choose from the most used {{label_plural}}', '{{textdomain}}' ), 'not_found' => __( 'No {{label_plural}} found.', '{{textdomain}}' ), 'no_terms' => __( 'No {{label_plural}}', '{{textdomain}}' ), 'menu_name' => __( '{{label_plural_ucfirst}}', '{{textdomain}}' ), 'items_list_navigation' => __( '{{label_plural_ucfirst}} list navigation', '{{textdomain}}' ), 'items_list' => __( '{{label_plural_ucfirst}} list', '{{textdomain}}' ), 'most_used' => _x( 'Most Used', '{{slug}}', '{{textdomain}}' ), 'back_to_items' => __( '← Back to {{label_plural_ucfirst}}', '{{textdomain}}' ), ], 'show_in_rest' => true, 'rest_base' => '{{slug}}', 'rest_controller_class' => 'WP_REST_Terms_Controller', ] ); # This file is for unifying the coding style for different editors and IDEs # editorconfig.org # WordPress Coding Standards # https://make.wordpress.org/core/handbook/coding-standards/ root = true [*] charset = utf-8 end_of_line = lf insert_final_newline = true trim_trailing_whitespace = true indent_style = tab indent_size = 4 [{.jshintrc,*.json,*.yml}] indent_style = space indent_size = 2 [{*.txt,wp-config-sample.php}] end_of_line = crlf /** * The following styles get applied inside the editor only. * * Replace them with your own styles or remove the file completely. */ .wp-block-{{namespace}}-{{slug}} { border: 1px dotted #f00; } /** * The following styles get applied both on the front of your site and in the editor. * * Replace them with your own styles or remove the file completely. */ .wp-block-{{namespace}}-{{slug}} { background-color: #000; color: #fff; padding: 2px; } workflows: version: 2 main: jobs: - php56-build - php70-build - php71-build - php72-build - php73-build - php74-build version: 2 job-references: mysql_image: &mysql_image circleci/mysql:5.6 setup_environment: &setup_environment name: "Setup Environment Variables" command: | echo "export PATH=$HOME/.composer/vendor/bin:$PATH" >> $BASH_ENV source /home/circleci/.bashrc install_dependencies: &install_dependencies name: "Install Dependencies" command: | sudo apt-get update && sudo apt-get install subversion sudo -E docker-php-ext-install mysqli sudo sh -c "printf '\ndeb http://ftp.us.debian.org/debian sid main\n' >> /etc/apt/sources.list" sudo apt-get update && sudo apt-get install mysql-client-5.7 php_job: &php_job environment: - WP_TESTS_DIR: "/tmp/wordpress-tests-lib" - WP_CORE_DIR: "/tmp/wordpress/" steps: - checkout - run: *setup_environment - run: *install_dependencies - run: name: "Run Tests" command: | composer global require "phpunit/phpunit=5.7.*" composer global require wp-coding-standards/wpcs phpcs --config-set installed_paths $HOME/.composer/vendor/wp-coding-standards/wpcs phpcs rm -rf $WP_TESTS_DIR $WP_CORE_DIR bash bin/install-wp-tests.sh wordpress_test root '' 127.0.0.1 latest phpunit WP_MULTISITE=1 phpunit jobs: php56-build: <<: *php_job docker: - image: circleci/php:5.6 - image: *mysql_image steps: - checkout - run: *setup_environment - run: *install_dependencies - run: name: "Run Tests" command: | composer global require "phpunit/phpunit=5.7.*" composer global require wp-coding-standards/wpcs phpcs --config-set installed_paths $HOME/.composer/vendor/wp-coding-standards/wpcs phpcs SKIP_DB_CREATE=false {{#wp_versions_to_test}} rm -rf $WP_TESTS_DIR $WP_CORE_DIR bash bin/install-wp-tests.sh wordpress_test root '' 127.0.0.1 {{.}} $SKIP_DB_CREATE phpunit WP_MULTISITE=1 phpunit SKIP_DB_CREATE=true {{/wp_versions_to_test}} php70-build: <<: *php_job docker: - image: circleci/php:7.0 - image: *mysql_image php71-build: <<: *php_job docker: - image: circleci/php:7.1 - image: *mysql_image php72-build: <<: *php_job docker: - image: circleci/php:7.2 - image: *mysql_image php73-build: <<: *php_job docker: - image: circleci/php:7.3 - image: *mysql_image php74-build: <<: *php_job docker: - image: circleci/php:7.4 - image: *mysql_image # A set of files you probably don't want in your WordPress.org distribution .babelrc .deployignore .distignore .editorconfig .eslintignore .eslintrc .git .gitignore .github .gitlab-ci.yml .travis.yml .DS_Store Thumbs.db behat.yml bitbucket-pipelines.yml bin .circleci/config.yml composer.json composer.lock dependencies.yml Gruntfile.js package.json package-lock.json phpunit.xml phpunit.xml.dist multisite.xml multisite.xml.dist .phpcs.xml phpcs.xml .phpcs.xml.dist phpcs.xml.dist README.md webpack.config.js wp-cli.local.yml yarn.lock tests vendor node_modules *.sql *.tar.gz *.zip ( function( wp ) { /** * Registers a new block provided a unique name and an object defining its behavior. * @see https://wordpress.org/gutenberg/handbook/designers-developers/developers/block-api/#registering-a-block */ var registerBlockType = wp.blocks.registerBlockType; /** * Returns a new element of given type. Element is an abstraction layer atop React. * @see https://wordpress.org/gutenberg/handbook/designers-developers/developers/packages/packages-element/ */ var el = wp.element.createElement; /** * Retrieves the translation of text. * @see https://wordpress.org/gutenberg/handbook/designers-developers/developers/packages/packages-i18n/ */ var __ = wp.i18n.__; /** * Every block starts by registering a new block type definition. * @see https://wordpress.org/gutenberg/handbook/designers-developers/developers/block-api/#registering-a-block */ registerBlockType( '{{namespace}}/{{slug}}', { /** * This is the display title for your block, which can be translated with `i18n` functions. * The block inserter will show this name. */ title: __( '{{title_ucfirst_js}}', '{{namespace}}' ), {{#dashicon}} /** * An icon property should be specified to make it easier to identify a block. * These can be any of WordPress’ Dashicons, or a custom svg element. */ icon: '{{dashicon}}', {{/dashicon}} /** * Blocks are grouped into categories to help users browse and discover them. * The categories provided by core are `common`, `embed`, `formatting`, `layout` and `widgets`. */ category: '{{category}}', /** * Optional block extended support features. */ supports: { // Removes support for an HTML mode. html: false, }, /** * The edit function describes the structure of your block in the context of the editor. * This represents what the editor will render when the block is used. * @see https://wordpress.org/gutenberg/handbook/designers-developers/developers/block-api/block-edit-save/#edit * * @param {Object} [props] Properties passed from the editor. * @return {Element} Element to render. */ edit: function( props ) { return el( 'p', { className: props.className }, __( 'Hello from the editor!', '{{namespace}}' ) ); }, /** * The save function defines the way in which the different attributes should be combined * into the final markup, which is then serialized by Gutenberg into `post_content`. * @see https://wordpress.org/gutenberg/handbook/designers-developers/developers/block-api/block-edit-save/#save * * @return {Element} Element to render. */ save: function() { return el( 'p', {}, __( 'Hello from the saved content!', '{{namespace}}' ) ); } } ); } )( window.wp ); pipelines: default: - step: image: php:5.6 name: "PHP 5.6" script: # Install Dependencies - docker-php-ext-install mysqli - apt-get update && apt-get install -y subversion --no-install-recommends # Install PHPCS - curl -o /usr/local/bin/phpcs -fSL https://squizlabs.github.io/PHP_CodeSniffer/phpcs.phar && chmod +x /usr/local/bin/phpcs - phpcs --version # Install WordPress Coding Standards - WPCS_VERSION=0.14.1 - curl -o wpcs.tar.gz -fSL "https://github.com/WordPress-Coding-Standards/WordPress-Coding-Standards/archive/${WPCS_VERSION}.tar.gz" - mkdir -p /var/wpcs && tar -xzf wpcs.tar.gz --directory /var/wpcs --strip-components 1 && rm wpcs.tar.gz - phpcs --config-set show_progress 1 && phpcs --config-set colors 1 && phpcs --config-set installed_paths /var/wpcs ## Run PHPCS - phpcs # Install PHPUnit - PHPUNIT_VERSION=5.7.27 - curl -o /usr/local/bin/phpunit "https://phar.phpunit.de/phpunit-${PHPUNIT_VERSION}.phar" && chmod +x /usr/local/bin/phpunit - phpunit --version ## Run PHPUnit - bash bin/install-wp-tests.sh wordpress_tests root root 127.0.0.1 latest true - phpunit services: - database - step: image: php:7.0 name: "PHP 7.0" script: # Install Dependencies - docker-php-ext-install mysqli - apt-get update && apt-get install -y subversion --no-install-recommends # Install PHPCS - curl -o /usr/local/bin/phpcs -fSL https://squizlabs.github.io/PHP_CodeSniffer/phpcs.phar && chmod +x /usr/local/bin/phpcs - phpcs --version # Install WordPress Coding Standards - WPCS_VERSION=0.14.1 - curl -o wpcs.tar.gz -fSL "https://github.com/WordPress-Coding-Standards/WordPress-Coding-Standards/archive/${WPCS_VERSION}.tar.gz" - mkdir -p /var/wpcs && tar -xzf wpcs.tar.gz --directory /var/wpcs --strip-components 1 && rm wpcs.tar.gz - phpcs --config-set show_progress 1 && phpcs --config-set colors 1 && phpcs --config-set installed_paths /var/wpcs ## Run PHPCS - phpcs # Install PHPUnit - PHPUNIT_VERSION=6.5.6 - curl -o /usr/local/bin/phpunit "https://phar.phpunit.de/phpunit-${PHPUNIT_VERSION}.phar" && chmod +x /usr/local/bin/phpunit - phpunit --version ## Run PHPUnit - bash bin/install-wp-tests.sh wordpress_tests root root 127.0.0.1 latest true - phpunit services: - database - step: image: php:7.1 name: "PHP 7.1" script: # Install Dependencies - docker-php-ext-install mysqli - apt-get update && apt-get install -y subversion --no-install-recommends # Install PHPCS - curl -o /usr/local/bin/phpcs -fSL https://squizlabs.github.io/PHP_CodeSniffer/phpcs.phar && chmod +x /usr/local/bin/phpcs - phpcs --version # Install WordPress Coding Standards - WPCS_VERSION=0.14.1 - curl -o wpcs.tar.gz -fSL "https://github.com/WordPress-Coding-Standards/WordPress-Coding-Standards/archive/${WPCS_VERSION}.tar.gz" - mkdir -p /var/wpcs && tar -xzf wpcs.tar.gz --directory /var/wpcs --strip-components 1 && rm wpcs.tar.gz - phpcs --config-set show_progress 1 && phpcs --config-set colors 1 && phpcs --config-set installed_paths /var/wpcs ## Run PHPCS - phpcs # Install PHPUnit - PHPUNIT_VERSION=6.5.6 - curl -o /usr/local/bin/phpunit "https://phar.phpunit.de/phpunit-${PHPUNIT_VERSION}.phar" && chmod +x /usr/local/bin/phpunit - phpunit --version ## Run PHPUnit - bash bin/install-wp-tests.sh wordpress_tests root root 127.0.0.1 latest true - phpunit services: - database - step: image: php:7.2 name: "PHP 7.2" script: # Install Dependencies - docker-php-ext-install mysqli - apt-get update && apt-get install -y subversion --no-install-recommends # Install PHPCS - curl -o /usr/local/bin/phpcs -fSL https://squizlabs.github.io/PHP_CodeSniffer/phpcs.phar && chmod +x /usr/local/bin/phpcs - phpcs --version # Install WordPress Coding Standards - WPCS_VERSION=0.14.1 - curl -o wpcs.tar.gz -fSL "https://github.com/WordPress-Coding-Standards/WordPress-Coding-Standards/archive/${WPCS_VERSION}.tar.gz" - mkdir -p /var/wpcs && tar -xzf wpcs.tar.gz --directory /var/wpcs --strip-components 1 && rm wpcs.tar.gz - phpcs --config-set show_progress 1 && phpcs --config-set colors 1 && phpcs --config-set installed_paths /var/wpcs ## Run PHPCS - phpcs # Install PHPUnit - PHPUNIT_VERSION=6.5.6 - curl -o /usr/local/bin/phpunit "https://phar.phpunit.de/phpunit-${PHPUNIT_VERSION}.phar" && chmod +x /usr/local/bin/phpunit - phpunit --version ## Run PHPUnit - bash bin/install-wp-tests.sh wordpress_tests root root 127.0.0.1 latest true - phpunit services: - database definitions: services: database: image: mysql:latest environment: MYSQL_DATABASE: 'wordpress_tests' MYSQL_ROOT_PASSWORD: 'root' '', // Unused. Messages start at index 1. /* translators: %s: post permalink */ 1 => sprintf( __( '{{label_ucfirst}} updated. View {{label}}', '{{textdomain}}' ), esc_url( $permalink ) ), 2 => __( 'Custom field updated.', '{{textdomain}}' ), 3 => __( 'Custom field deleted.', '{{textdomain}}' ), 4 => __( '{{label_ucfirst}} updated.', '{{textdomain}}' ), /* translators: %s: date and time of the revision */ 5 => isset( $_GET['revision'] ) ? sprintf( __( '{{label_ucfirst}} restored to revision from %s', '{{textdomain}}' ), wp_post_revision_title( (int) $_GET['revision'], false ) ) : false, // phpcs:ignore WordPress.Security.NonceVerification.Recommended /* translators: %s: post permalink */ 6 => sprintf( __( '{{label_ucfirst}} published. View {{label}}', '{{textdomain}}' ), esc_url( $permalink ) ), 7 => __( '{{label_ucfirst}} saved.', '{{textdomain}}' ), /* translators: %s: post permalink */ 8 => sprintf( __( '{{label_ucfirst}} submitted. Preview {{label}}', '{{textdomain}}' ), esc_url( add_query_arg( 'preview', 'true', $permalink ) ) ), /* translators: 1: Publish box date format, see https://secure.php.net/date 2: Post permalink */ 9 => sprintf( __( '{{label_ucfirst}} scheduled for: %1$s. Preview {{label}}', '{{textdomain}}' ), date_i18n( __( 'M j, Y @ G:i', '{{textdomain}}' ), strtotime( $post->post_date ) ), esc_url( $permalink ) ), /* translators: %s: post permalink */ 10 => sprintf( __( '{{label_ucfirst}} draft updated. Preview {{label}}', '{{textdomain}}' ), esc_url( add_query_arg( 'preview', 'true', $permalink ) ) ), ]; return $messages; } add_filter( 'post_updated_messages', '{{machine_name}}_updated_messages' ); /** * Sets the bulk post updated messages for the `{{machine_name}}` post type. * * @param array $bulk_messages Arrays of messages, each keyed by the corresponding post type. Messages are * keyed with 'updated', 'locked', 'deleted', 'trashed', and 'untrashed'. * @param int[] $bulk_counts Array of item counts for each message, used to build internationalized strings. * @return array Bulk messages for the `{{machine_name}}` post type. */ function {{machine_name}}_bulk_updated_messages( $bulk_messages, $bulk_counts ) { global $post; $bulk_messages['{{slug}}'] = [ /* translators: %s: Number of {{label_plural}}. */ 'updated' => _n( '%s {{label}} updated.', '%s {{label_plural}} updated.', $bulk_counts['updated'], '{{textdomain}}' ), 'locked' => ( 1 === $bulk_counts['locked'] ) ? __( '1 {{label}} not updated, somebody is editing it.', '{{textdomain}}' ) : /* translators: %s: Number of {{label_plural}}. */ _n( '%s {{label}} not updated, somebody is editing it.', '%s {{label_plural}} not updated, somebody is editing them.', $bulk_counts['locked'], '{{textdomain}}' ), /* translators: %s: Number of {{label_plural}}. */ 'deleted' => _n( '%s {{label}} permanently deleted.', '%s {{label_plural}} permanently deleted.', $bulk_counts['deleted'], '{{textdomain}}' ), /* translators: %s: Number of {{label_plural}}. */ 'trashed' => _n( '%s {{label}} moved to the Trash.', '%s {{label_plural}} moved to the Trash.', $bulk_counts['trashed'], '{{textdomain}}' ), /* translators: %s: Number of {{label_plural}}. */ 'untrashed' => _n( '%s {{label}} restored from the Trash.', '%s {{label_plural}} restored from the Trash.', $bulk_counts['untrashed'], '{{textdomain}}' ), ]; return $bulk_messages; } add_filter( 'bulk_post_updated_messages', '{{machine_name}}_bulk_updated_messages', 10, 2 ); [db-host] [wp-version] [skip-database-creation]" exit 1 fi DB_NAME=$1 DB_USER=$2 DB_PASS=$3 DB_HOST=${4-localhost} WP_VERSION=${5-latest} SKIP_DB_CREATE=${6-false} TMPDIR=${TMPDIR-/tmp} TMPDIR=$(echo $TMPDIR | sed -e "s/\/$//") WP_TESTS_DIR=${WP_TESTS_DIR-$TMPDIR/wordpress-tests-lib} WP_CORE_DIR=${WP_CORE_DIR-$TMPDIR/wordpress} download() { if [ `which curl` ]; then curl -s "$1" > "$2"; elif [ `which wget` ]; then wget -nv -O "$2" "$1" fi } if [[ $WP_VERSION =~ ^[0-9]+\.[0-9]+\-(beta|RC)[0-9]+$ ]]; then WP_BRANCH=${WP_VERSION%\-*} WP_TESTS_TAG="branches/$WP_BRANCH" elif [[ $WP_VERSION =~ ^[0-9]+\.[0-9]+$ ]]; then WP_TESTS_TAG="branches/$WP_VERSION" elif [[ $WP_VERSION =~ [0-9]+\.[0-9]+\.[0-9]+ ]]; then if [[ $WP_VERSION =~ [0-9]+\.[0-9]+\.[0] ]]; then # version x.x.0 means the first release of the major version, so strip off the .0 and download version x.x WP_TESTS_TAG="tags/${WP_VERSION%??}" else WP_TESTS_TAG="tags/$WP_VERSION" fi elif [[ $WP_VERSION == 'nightly' || $WP_VERSION == 'trunk' ]]; then WP_TESTS_TAG="trunk" else # http serves a single offer, whereas https serves multiple. we only want one download http://api.wordpress.org/core/version-check/1.7/ /tmp/wp-latest.json grep '[0-9]+\.[0-9]+(\.[0-9]+)?' /tmp/wp-latest.json LATEST_VERSION=$(grep -o '"version":"[^"]*' /tmp/wp-latest.json | sed 's/"version":"//') if [[ -z "$LATEST_VERSION" ]]; then echo "Latest WordPress version could not be found" exit 1 fi WP_TESTS_TAG="tags/$LATEST_VERSION" fi set -ex install_wp() { if [ -d $WP_CORE_DIR ]; then return; fi mkdir -p $WP_CORE_DIR if [[ $WP_VERSION == 'nightly' || $WP_VERSION == 'trunk' ]]; then mkdir -p $TMPDIR/wordpress-trunk rm -rf $TMPDIR/wordpress-trunk/* svn export --quiet https://core.svn.wordpress.org/trunk $TMPDIR/wordpress-trunk/wordpress mv $TMPDIR/wordpress-trunk/wordpress/* $WP_CORE_DIR else if [ $WP_VERSION == 'latest' ]; then local ARCHIVE_NAME='latest' elif [[ $WP_VERSION =~ [0-9]+\.[0-9]+ ]]; then # https serves multiple offers, whereas http serves single. download https://api.wordpress.org/core/version-check/1.7/ $TMPDIR/wp-latest.json if [[ $WP_VERSION =~ [0-9]+\.[0-9]+\.[0] ]]; then # version x.x.0 means the first release of the major version, so strip off the .0 and download version x.x LATEST_VERSION=${WP_VERSION%??} else # otherwise, scan the releases and get the most up to date minor version of the major release local VERSION_ESCAPED=`echo $WP_VERSION | sed 's/\./\\\\./g'` LATEST_VERSION=$(grep -o '"version":"'$VERSION_ESCAPED'[^"]*' $TMPDIR/wp-latest.json | sed 's/"version":"//' | head -1) fi if [[ -z "$LATEST_VERSION" ]]; then local ARCHIVE_NAME="wordpress-$WP_VERSION" else local ARCHIVE_NAME="wordpress-$LATEST_VERSION" fi else local ARCHIVE_NAME="wordpress-$WP_VERSION" fi download https://wordpress.org/${ARCHIVE_NAME}.tar.gz $TMPDIR/wordpress.tar.gz tar --strip-components=1 -zxmf $TMPDIR/wordpress.tar.gz -C $WP_CORE_DIR fi download https://raw.github.com/markoheijnen/wp-mysqli/master/db.php $WP_CORE_DIR/wp-content/db.php } install_test_suite() { # portable in-place argument for both GNU sed and Mac OSX sed if [[ $(uname -s) == 'Darwin' ]]; then local ioption='-i.bak' else local ioption='-i' fi # set up testing suite if it doesn't yet exist if [ ! -d $WP_TESTS_DIR ]; then # set up testing suite mkdir -p $WP_TESTS_DIR rm -rf $WP_TESTS_DIR/{includes,data} svn export --quiet --ignore-externals https://develop.svn.wordpress.org/${WP_TESTS_TAG}/tests/phpunit/includes/ $WP_TESTS_DIR/includes svn export --quiet --ignore-externals https://develop.svn.wordpress.org/${WP_TESTS_TAG}/tests/phpunit/data/ $WP_TESTS_DIR/data fi if [ ! -f wp-tests-config.php ]; then download https://develop.svn.wordpress.org/${WP_TESTS_TAG}/wp-tests-config-sample.php "$WP_TESTS_DIR"/wp-tests-config.php # remove all forward slashes in the end WP_CORE_DIR=$(echo $WP_CORE_DIR | sed "s:/\+$::") sed $ioption "s:dirname( __FILE__ ) . '/src/':'$WP_CORE_DIR/':" "$WP_TESTS_DIR"/wp-tests-config.php sed $ioption "s:__DIR__ . '/src/':'$WP_CORE_DIR/':" "$WP_TESTS_DIR"/wp-tests-config.php sed $ioption "s/youremptytestdbnamehere/$DB_NAME/" "$WP_TESTS_DIR"/wp-tests-config.php sed $ioption "s/yourusernamehere/$DB_USER/" "$WP_TESTS_DIR"/wp-tests-config.php sed $ioption "s/yourpasswordhere/$DB_PASS/" "$WP_TESTS_DIR"/wp-tests-config.php sed $ioption "s|localhost|${DB_HOST}|" "$WP_TESTS_DIR"/wp-tests-config.php fi } recreate_db() { shopt -s nocasematch if [[ $1 =~ ^(y|yes)$ ]] then mysqladmin drop $DB_NAME -f --user="$DB_USER" --password="$DB_PASS"$EXTRA create_db echo "Recreated the database ($DB_NAME)." else echo "Leaving the existing database ($DB_NAME) in place." fi shopt -u nocasematch } create_db() { mysqladmin create $DB_NAME --user="$DB_USER" --password="$DB_PASS"$EXTRA } install_db() { if [ ${SKIP_DB_CREATE} = "true" ]; then return 0 fi # parse DB_HOST for port or socket references local PARTS=(${DB_HOST//\:/ }) local DB_HOSTNAME=${PARTS[0]}; local DB_SOCK_OR_PORT=${PARTS[1]}; local EXTRA="" if ! [ -z $DB_HOSTNAME ] ; then if [ $(echo $DB_SOCK_OR_PORT | grep -e '^[0-9]\{1,\}$') ]; then EXTRA=" --host=$DB_HOSTNAME --port=$DB_SOCK_OR_PORT --protocol=tcp" elif ! [ -z $DB_SOCK_OR_PORT ] ; then EXTRA=" --socket=$DB_SOCK_OR_PORT" elif ! [ -z $DB_HOSTNAME ] ; then EXTRA=" --host=$DB_HOSTNAME --protocol=tcp" fi fi # create database if [ $(mysql --user="$DB_USER" --password="$DB_PASS"$EXTRA --execute='show databases;' | grep ^$DB_NAME$) ] then echo "Reinstalling will delete the existing test database ($DB_NAME)" read -p 'Are you sure you want to proceed? [y/N]: ' DELETE_EXISTING_DB recreate_db $DELETE_EXISTING_DB else create_db fi } install_wp install_test_suite install_db === {{plugin_name}} === Contributors: (this should be a list of wordpress.org userid's) Donate link: https://example.com/ Tags: comments, spam Requires at least: 4.5 Tested up to: {{plugin_tested_up_to}} Requires PHP: 5.6 Stable tag: 0.1.0 License: GPLv2 or later License URI: https://www.gnu.org/licenses/gpl-2.0.html Here is a short description of the plugin. This should be no more than 150 characters. No markup here. == Description == This is the long description. No limit, and you can use Markdown (as well as in the following sections). For backwards compatibility, if this section is missing, the full length of the short description will be used, and Markdown parsed. A few notes about the sections above: * "Contributors" is a comma separated list of wp.org/wp-plugins.org usernames * "Tags" is a comma separated list of tags that apply to the plugin * "Requires at least" is the lowest version that the plugin will work on * "Tested up to" is the highest version that you've *successfully used to test the plugin*. Note that it might work on higher versions... this is just the highest one you've verified. * Stable tag should indicate the Subversion "tag" of the latest stable version, or "trunk," if you use `/trunk/` for stable. Note that the `readme.txt` of the stable tag is the one that is considered the defining one for the plugin, so if the `/trunk/readme.txt` file says that the stable tag is `4.3`, then it is `/tags/4.3/readme.txt` that'll be used for displaying information about the plugin. In this situation, the only thing considered from the trunk `readme.txt` is the stable tag pointer. Thus, if you develop in trunk, you can update the trunk `readme.txt` to reflect changes in your in-development version, without having that information incorrectly disclosed about the current stable version that lacks those changes -- as long as the trunk's `readme.txt` points to the correct stable tag. If no stable tag is provided, it is assumed that trunk is stable, but you should specify "trunk" if that's where you put the stable version, in order to eliminate any doubt. == Installation == This section describes how to install the plugin and get it working. e.g. 1. Upload `plugin-name.php` to the `/wp-content/plugins/` directory 1. Activate the plugin through the 'Plugins' menu in WordPress 1. Place `` in your templates == Frequently Asked Questions == = A question that someone might have = An answer to that question. = What about foo bar? = Answer to foo bar dilemma. == Screenshots == 1. This screen shot description corresponds to screenshot-1.(png|jpg|jpeg|gif). Note that the screenshot is taken from the /assets directory or the directory that contains the stable readme.txt (tags or trunk). Screenshots in the /assets directory take precedence. For example, `/assets/screenshot-1.png` would win over `/tags/4.3/screenshot-1.png` (or jpg, jpeg, gif). 2. This is the second screen shot == Changelog == = 1.0 = * A change since the previous version. * Another change. = 0.5 = * List versions from most recent at top to oldest at bottom. == Upgrade Notice == = 1.0 = Upgrade notices describe the reason a user should upgrade. No more than 300 characters. = 0.5 = This version fixes a security related bug. Upgrade immediately. == Arbitrary section == You may provide arbitrary sections, in the same format as the ones above. This may be of use for extremely complicated plugins where more information needs to be conveyed that doesn't fit into the categories of "description" or "installation." Arbitrary sections will be shown below the built-in sections outlined above. == A brief Markdown Example == Ordered list: 1. Some feature 1. Another feature 1. Something else about the plugin Unordered list: * something * something else * third thing Here's a link to [WordPress](https://wordpress.org/ "Your favorite software") and one to [Markdown's Syntax Documentation][markdown syntax]. Titles are optional, naturally. [markdown syntax]: https://daringfireball.net/projects/markdown/syntax "Markdown is what the parser uses to process much of the readme file" Markdown uses email style notation for blockquotes and I've been told: > Asterisks for *emphasis*. Double it up for **strong**. `` /* Theme Name: {{theme_name}} Theme URI: {{theme_uri}} Description: {{description}} Author: {{author}} Author URI: {{author_uri}} Template: {{parent_theme}} Version: 0.1.0 */ [ 'name' => __( '{{label_plural_ucfirst}}', '{{textdomain}}' ), 'singular_name' => __( '{{label_ucfirst}}', '{{textdomain}}' ), 'all_items' => __( 'All {{label_plural_ucfirst}}', '{{textdomain}}' ), 'archives' => __( '{{label_ucfirst}} Archives', '{{textdomain}}' ), 'attributes' => __( '{{label_ucfirst}} Attributes', '{{textdomain}}' ), 'insert_into_item' => __( 'Insert into {{label}}', '{{textdomain}}' ), 'uploaded_to_this_item' => __( 'Uploaded to this {{label}}', '{{textdomain}}' ), 'featured_image' => _x( 'Featured Image', '{{slug}}', '{{textdomain}}' ), 'set_featured_image' => _x( 'Set featured image', '{{slug}}', '{{textdomain}}' ), 'remove_featured_image' => _x( 'Remove featured image', '{{slug}}', '{{textdomain}}' ), 'use_featured_image' => _x( 'Use as featured image', '{{slug}}', '{{textdomain}}' ), 'filter_items_list' => __( 'Filter {{label_plural}} list', '{{textdomain}}' ), 'items_list_navigation' => __( '{{label_plural_ucfirst}} list navigation', '{{textdomain}}' ), 'items_list' => __( '{{label_plural_ucfirst}} list', '{{textdomain}}' ), 'new_item' => __( 'New {{label_ucfirst}}', '{{textdomain}}' ), 'add_new' => __( 'Add New', '{{textdomain}}' ), 'add_new_item' => __( 'Add New {{label_ucfirst}}', '{{textdomain}}' ), 'edit_item' => __( 'Edit {{label_ucfirst}}', '{{textdomain}}' ), 'view_item' => __( 'View {{label_ucfirst}}', '{{textdomain}}' ), 'view_items' => __( 'View {{label_plural_ucfirst}}', '{{textdomain}}' ), 'search_items' => __( 'Search {{label_plural}}', '{{textdomain}}' ), 'not_found' => __( 'No {{label_plural}} found', '{{textdomain}}' ), 'not_found_in_trash' => __( 'No {{label_plural}} found in trash', '{{textdomain}}' ), 'parent_item_colon' => __( 'Parent {{label_ucfirst}}:', '{{textdomain}}' ), 'menu_name' => __( '{{label_plural_ucfirst}}', '{{textdomain}}' ), ], 'public' => true, 'hierarchical' => false, 'show_ui' => true, 'show_in_nav_menus' => true, 'supports' => [ 'title', 'editor' ], 'has_archive' => true, 'rewrite' => true, 'query_var' => true, 'menu_position' => null, 'menu_icon' => 'dashicons-{{dashicon}}', 'show_in_rest' => true, 'rest_base' => '{{slug}}', 'rest_controller_class' => 'WP_REST_Posts_Controller', ] ); sudo: false dist: trusty language: php notifications: email: on_success: never on_failure: change branches: only: - master cache: directories: - $HOME/.composer/cache matrix: include: - php: 7.4 env: WP_VERSION=latest - php: 7.3 env: WP_VERSION=latest - php: 7.2 env: WP_VERSION=latest - php: 7.1 env: WP_VERSION=latest - php: 7.0 env: WP_VERSION=latest {{#wp_versions_to_test}} - php: 5.6 env: WP_VERSION={{.}} {{/wp_versions_to_test}} - php: 5.6 env: WP_TRAVISCI=phpcs before_script: - export PATH="$HOME/.composer/vendor/bin:$PATH" - | if [ -f ~/.phpenv/versions/$(phpenv version-name)/etc/conf.d/xdebug.ini ]; then phpenv config-rm xdebug.ini else echo "xdebug.ini does not exist" fi - | if [[ ! -z "$WP_VERSION" ]] ; then bash bin/install-wp-tests.sh wordpress_test root '' localhost $WP_VERSION composer global require "phpunit/phpunit=4.8.*|5.7.*" fi - | if [[ "$WP_TRAVISCI" == "phpcs" ]] ; then composer global require wp-coding-standards/wpcs composer global require phpcompatibility/php-compatibility composer global require phpcompatibility/phpcompatibility-paragonie composer global require phpcompatibility/phpcompatibility-wp phpcs --config-set installed_paths $HOME/.composer/vendor/wp-coding-standards/wpcs,$HOME/.composer/vendor/phpcompatibility/php-compatibility,$HOME/.composer/vendor/phpcompatibility/phpcompatibility-paragonie,$HOME/.composer/vendor/phpcompatibility/phpcompatibility-wp fi script: - | if [[ ! -z "$WP_VERSION" ]] ; then phpunit WP_MULTISITE=1 phpunit fi - | if [[ "$WP_TRAVISCI" == "phpcs" ]] ; then phpcs fi github API oauth tokens, typically {\"github.com\":\"\"}.", "additionalProperties": { "type": "string" } }, "gitlab-oauth": { "type": "object", "description": "An object of domain name => gitlab API oauth tokens, typically {\"gitlab.com\":\"\"}.", "additionalProperties": { "type": "string" } }, "gitlab-token": { "type": "object", "description": "An object of domain name => gitlab private tokens, typically {\"gitlab.com\":\"\"}.", "additionalProperties": { "type": "string" } }, "gitlab-protocol": { "enum": ["git", "http", "https"], "description": "A protocol to force use of when creating a repository URL for the `source` value of the package metadata. One of `git` or `http`. By default, Composer will generate a git URL for private repositories and http one for public repos." }, "bearer": { "type": "object", "description": "An object of domain name => bearer authentication token, for example {\"example.com\":\"\"}.", "additionalProperties": { "type": "string" } }, "disable-tls": { "type": "boolean", "description": "Defaults to `false`. If set to true all HTTPS URLs will be tried with HTTP instead and no network level encryption is performed. Enabling this is a security risk and is NOT recommended. The better way is to enable the php_openssl extension in php.ini." }, "secure-http": { "type": "boolean", "description": "Defaults to `true`. If set to true only HTTPS URLs are allowed to be downloaded via Composer. If you really absolutely need HTTP access to something then you can disable it, but using \"Let's Encrypt\" to get a free SSL certificate is generally a better alternative." }, "secure-svn-domains": { "type": "array", "description": "A list of domains which should be trusted/marked as using a secure Subversion/SVN transport. By default svn:// protocol is seen as insecure and will throw. This is a better/safer alternative to disabling `secure-http` altogether.", "items": { "type": "string" } }, "cafile": { "type": "string", "description": "A way to set the path to the openssl CA file. In PHP 5.6+ you should rather set this via openssl.cafile in php.ini, although PHP 5.6+ should be able to detect your system CA file automatically." }, "capath": { "type": "string", "description": "If cafile is not specified or if the certificate is not found there, the directory pointed to by capath is searched for a suitable certificate. capath must be a correctly hashed certificate directory." }, "http-basic": { "type": "object", "description": "An object of domain name => {\"username\": \"...\", \"password\": \"...\"}.", "additionalProperties": { "type": "object", "required": ["username", "password"], "properties": { "username": { "type": "string", "description": "The username used for HTTP Basic authentication" }, "password": { "type": "string", "description": "The password used for HTTP Basic authentication" } } } }, "store-auths": { "type": ["string", "boolean"], "description": "What to do after prompting for authentication, one of: true (store), false (do not store) or \"prompt\" (ask every time), defaults to prompt." }, "vendor-dir": { "type": "string", "description": "The location where all packages are installed, defaults to \"vendor\"." }, "bin-dir": { "type": "string", "description": "The location where all binaries are linked, defaults to \"vendor/bin\"." }, "data-dir": { "type": "string", "description": "The location where old phar files are stored, defaults to \"$home\" except on XDG Base Directory compliant unixes." }, "cache-dir": { "type": "string", "description": "The location where all caches are located, defaults to \"~/.composer/cache\" on *nix and \"%LOCALAPPDATA%\\Composer\" on windows." }, "cache-files-dir": { "type": "string", "description": "The location where files (zip downloads) are cached, defaults to \"{$cache-dir}/files\"." }, "cache-repo-dir": { "type": "string", "description": "The location where repo (git/hg repo clones) are cached, defaults to \"{$cache-dir}/repo\"." }, "cache-vcs-dir": { "type": "string", "description": "The location where vcs infos (git clones, github api calls, etc. when reading vcs repos) are cached, defaults to \"{$cache-dir}/vcs\"." }, "cache-ttl": { "type": "integer", "description": "The default cache time-to-live, defaults to 15552000 (6 months)." }, "cache-files-ttl": { "type": "integer", "description": "The cache time-to-live for files, defaults to the value of cache-ttl." }, "cache-files-maxsize": { "type": ["string", "integer"], "description": "The cache max size for the files cache, defaults to \"300MiB\"." }, "cache-read-only": { "type": ["boolean"], "description": "Whether to use the Composer cache in read-only mode." }, "bin-compat": { "enum": ["auto", "full", "proxy", "symlink"], "description": "The compatibility of the binaries, defaults to \"auto\" (automatically guessed), can be \"full\" (compatible with both Windows and Unix-based systems) and \"proxy\" (only bash-style proxy)." }, "discard-changes": { "type": ["string", "boolean"], "description": "The default style of handling dirty updates, defaults to false and can be any of true, false or \"stash\"." }, "autoloader-suffix": { "type": "string", "description": "Optional string to be used as a suffix for the generated Composer autoloader. When null a random one will be generated." }, "optimize-autoloader": { "type": "boolean", "description": "Always optimize when dumping the autoloader." }, "prepend-autoloader": { "type": "boolean", "description": "If false, the composer autoloader will not be prepended to existing autoloaders, defaults to true." }, "classmap-authoritative": { "type": "boolean", "description": "If true, the composer autoloader will not scan the filesystem for classes that are not found in the class map, defaults to false." }, "apcu-autoloader": { "type": "boolean", "description": "If true, the Composer autoloader will check for APCu and use it to cache found/not-found classes when the extension is enabled, defaults to false." }, "github-domains": { "type": "array", "description": "A list of domains to use in github mode. This is used for GitHub Enterprise setups, defaults to [\"github.com\"].", "items": { "type": "string" } }, "github-expose-hostname": { "type": "boolean", "description": "Defaults to true. If set to false, the OAuth tokens created to access the github API will have a date instead of the machine hostname." }, "gitlab-domains": { "type": "array", "description": "A list of domains to use in gitlab mode. This is used for custom GitLab setups, defaults to [\"gitlab.com\"].", "items": { "type": "string" } }, "use-github-api": { "type": "boolean", "description": "Defaults to true. If set to false, globally disables the use of the GitHub API for all GitHub repositories and clones the repository as it would for any other repository." }, "archive-format": { "type": "string", "description": "The default archiving format when not provided on cli, defaults to \"tar\"." }, "archive-dir": { "type": "string", "description": "The default archive path when not provided on cli, defaults to \".\"." }, "htaccess-protect": { "type": "boolean", "description": "Defaults to true. If set to false, Composer will not create .htaccess files in the composer home, cache, and data directories." }, "sort-packages": { "type": "boolean", "description": "Defaults to false. If set to true, Composer will sort packages when adding/updating a new dependency." }, "lock": { "type": "boolean", "description": "Defaults to true. If set to false, Composer will not create a composer.lock file." }, "platform-check": { "type": ["boolean", "string"], "description": "Defaults to \"php-only\" which checks only the PHP version. Setting to true will also check the presence of required PHP extensions. If set to false, Composer will not create and require a platform_check.php file as part of the autoloader bootstrap." } } }, "extra": { "type": ["object", "array"], "description": "Arbitrary extra data that can be used by plugins, for example, package of type composer-plugin may have a 'class' key defining an installer class name.", "additionalProperties": true }, "scripts": { "type": ["object"], "description": "Script listeners that will be executed before/after some events.", "properties": { "pre-install-cmd": { "type": ["array", "string"], "description": "Occurs before the install command is executed, contains one or more Class::method callables or shell commands." }, "post-install-cmd": { "type": ["array", "string"], "description": "Occurs after the install command is executed, contains one or more Class::method callables or shell commands." }, "pre-update-cmd": { "type": ["array", "string"], "description": "Occurs before the update command is executed, contains one or more Class::method callables or shell commands." }, "post-update-cmd": { "type": ["array", "string"], "description": "Occurs after the update command is executed, contains one or more Class::method callables or shell commands." }, "pre-status-cmd": { "type": ["array", "string"], "description": "Occurs before the status command is executed, contains one or more Class::method callables or shell commands." }, "post-status-cmd": { "type": ["array", "string"], "description": "Occurs after the status command is executed, contains one or more Class::method callables or shell commands." }, "pre-package-install": { "type": ["array", "string"], "description": "Occurs before a package is installed, contains one or more Class::method callables or shell commands." }, "post-package-install": { "type": ["array", "string"], "description": "Occurs after a package is installed, contains one or more Class::method callables or shell commands." }, "pre-package-update": { "type": ["array", "string"], "description": "Occurs before a package is updated, contains one or more Class::method callables or shell commands." }, "post-package-update": { "type": ["array", "string"], "description": "Occurs after a package is updated, contains one or more Class::method callables or shell commands." }, "pre-package-uninstall": { "type": ["array", "string"], "description": "Occurs before a package has been uninstalled, contains one or more Class::method callables or shell commands." }, "post-package-uninstall": { "type": ["array", "string"], "description": "Occurs after a package has been uninstalled, contains one or more Class::method callables or shell commands." }, "pre-autoload-dump": { "type": ["array", "string"], "description": "Occurs before the autoloader is dumped, contains one or more Class::method callables or shell commands." }, "post-autoload-dump": { "type": ["array", "string"], "description": "Occurs after the autoloader is dumped, contains one or more Class::method callables or shell commands." }, "post-root-package-install": { "type": ["array", "string"], "description": "Occurs after the root-package is installed, contains one or more Class::method callables or shell commands." }, "post-create-project-cmd": { "type": ["array", "string"], "description": "Occurs after the create-project command is executed, contains one or more Class::method callables or shell commands." } } }, "scripts-descriptions": { "type": ["object"], "description": "Descriptions for custom commands, shown in console help.", "additionalProperties": { "type": "string" } } }, "definitions": { "authors": { "type": "array", "description": "List of authors that contributed to the package. This is typically the main maintainers, not the full list.", "items": { "type": "object", "additionalProperties": false, "required": [ "name"], "properties": { "name": { "type": "string", "description": "Full name of the author." }, "email": { "type": "string", "description": "Email address of the author.", "format": "email" }, "homepage": { "type": "string", "description": "Homepage URL for the author.", "format": "uri" }, "role": { "type": "string", "description": "Author's role in the project." } } } }, "autoload": { "type": "object", "description": "Description of how the package can be autoloaded.", "properties": { "psr-0": { "type": "object", "description": "This is an object of namespaces (keys) and the directories they can be found in (values, can be arrays of paths) by the autoloader.", "additionalProperties": { "type": ["string", "array"], "items": { "type": "string" } } }, "psr-4": { "type": "object", "description": "This is an object of namespaces (keys) and the PSR-4 directories they can map to (values, can be arrays of paths) by the autoloader.", "additionalProperties": { "type": ["string", "array"], "items": { "type": "string" } } }, "classmap": { "type": "array", "description": "This is an array of paths that contain classes to be included in the class-map generation process." }, "files": { "type": "array", "description": "This is an array of files that are always required on every request." }, "exclude-from-classmap": { "type": "array", "description": "This is an array of patterns to exclude from autoload classmap generation. (e.g. \"exclude-from-classmap\": [\"/test/\", \"/tests/\", \"/Tests/\"]" } } }, "repository": { "type": "object", "anyOf": [ { "$ref": "#/definitions/composer-repository" }, { "$ref": "#/definitions/vcs-repository" }, { "$ref": "#/definitions/path-repository" }, { "$ref": "#/definitions/artifact-repository" }, { "$ref": "#/definitions/pear-repository" }, { "$ref": "#/definitions/package-repository" } ] }, "composer-repository": { "type": "object", "required": ["type", "url"], "properties": { "type": { "type": "string", "enum": ["composer"] }, "url": { "type": "string" }, "canonical": { "type": "boolean" }, "only": { "type": "array", "items": { "type": "string" } }, "exclude": { "type": "array", "items": { "type": "string" } }, "options": { "type": "object", "additionalProperties": true }, "allow_ssl_downgrade": { "type": "boolean" }, "force-lazy-providers": { "type": "boolean" } } }, "vcs-repository": { "type": "object", "required": ["type", "url"], "properties": { "type": { "type": "string", "enum": ["vcs", "github", "git", "gitlab", "bitbucket", "git-bitbucket", "hg", "fossil", "perforce", "svn"] }, "url": { "type": "string" }, "canonical": { "type": "boolean" }, "only": { "type": "array", "items": { "type": "string" } }, "exclude": { "type": "array", "items": { "type": "string" } }, "no-api": { "type": "boolean" }, "secure-http": { "type": "boolean" }, "svn-cache-credentials": { "type": "boolean" }, "trunk-path": { "type": ["string", "boolean"] }, "branches-path": { "type": ["string", "boolean"] }, "tags-path": { "type": ["string", "boolean"] }, "package-path": { "type": "string" }, "depot": { "type": "string" }, "branch": { "type": "string" }, "unique_perforce_client_name": { "type": "string" }, "p4user": { "type": "string" }, "p4password": { "type": "string" } } }, "path-repository": { "type": "object", "required": ["type", "url"], "properties": { "type": { "type": "string", "enum": ["path"] }, "url": { "type": "string" }, "canonical": { "type": "boolean" }, "only": { "type": "array", "items": { "type": "string" } }, "exclude": { "type": "array", "items": { "type": "string" } }, "options": { "type": "object", "properties": { "symlink": { "type": ["boolean", "null"] } }, "additionalProperties": true } } }, "artifact-repository": { "type": "object", "required": ["type", "url"], "properties": { "type": { "type": "string", "enum": ["artifact"] }, "url": { "type": "string" }, "canonical": { "type": "boolean" }, "only": { "type": "array", "items": { "type": "string" } }, "exclude": { "type": "array", "items": { "type": "string" } } } }, "pear-repository": { "type": "object", "required": ["type", "url"], "properties": { "type": { "type": "string", "enum": ["pear"] }, "url": { "type": "string" }, "canonical": { "type": "boolean" }, "only": { "type": "array", "items": { "type": "string" } }, "exclude": { "type": "array", "items": { "type": "string" } }, "vendor-alias": { "type": "string" } } }, "package-repository": { "type": "object", "required": ["type", "package"], "properties": { "type": { "type": "string", "enum": ["package"] }, "canonical": { "type": "boolean" }, "only": { "type": "array", "items": { "type": "string" } }, "exclude": { "type": "array", "items": { "type": "string" } }, "package": { "oneOf": [ { "$ref": "#/definitions/inline-package" }, { "type": "array", "items": { "$ref": "#/definitions/inline-package" } } ] } } }, "inline-package": { "type": "object", "required": ["name", "version"], "properties": { "name": { "type": "string", "description": "Package name, including 'vendor-name/' prefix." }, "type": { "type": "string" }, "target-dir": { "description": "DEPRECATED: Forces the package to be installed into the given subdirectory path. This is used for autoloading PSR-0 packages that do not contain their full path. Use forward slashes for cross-platform compatibility.", "type": "string" }, "description": { "type": "string" }, "keywords": { "type": "array", "items": { "type": "string" } }, "homepage": { "type": "string", "format": "uri" }, "version": { "type": "string" }, "time": { "type": "string" }, "license": { "type": [ "string", "array" ] }, "authors": { "$ref": "#/definitions/authors" }, "require": { "type": "object", "additionalProperties": { "type": "string" } }, "replace": { "type": "object", "additionalProperties": { "type": "string" } }, "conflict": { "type": "object", "additionalProperties": { "type": "string" } }, "provide": { "type": "object", "additionalProperties": { "type": "string" } }, "require-dev": { "type": "object", "additionalProperties": { "type": "string" } }, "suggest": { "type": "object", "additionalProperties": { "type": "string" } }, "extra": { "type": ["object", "array"], "additionalProperties": true }, "autoload": { "$ref": "#/definitions/autoload" }, "archive": { "type": ["object"], "properties": { "exclude": { "type": "array" } } }, "bin": { "type": ["string", "array"], "description": "A set of files, or a single file, that should be treated as binaries and symlinked into bin-dir (from config).", "items": { "type": "string" } }, "include-path": { "type": ["array"], "description": "DEPRECATED: A list of directories which should get added to PHP's include path. This is only present to support legacy projects, and all new code should preferably use autoloading.", "items": { "type": "string" } }, "source": { "type": "object", "required": ["type", "url", "reference"], "properties": { "type": { "type": "string" }, "url": { "type": "string" }, "reference": { "type": "string" }, "mirrors": { "type": "array" } } }, "dist": { "type": "object", "required": ["type", "url"], "properties": { "type": { "type": "string" }, "url": { "type": "string" }, "reference": { "type": "string" }, "shasum": { "type": "string" }, "mirrors": { "type": "array" } } } }, "additionalProperties": true } } } ## ## Bundle of CA Root Certificates ## ## Certificate data from Mozilla as of: Tue Jan 19 04:12:04 2021 GMT ## ## This is a bundle of X.509 certificates of public Certificate Authorities ## (CA). These were automatically extracted from Mozilla's root certificates ## file (certdata.txt). This file can be found in the mozilla source tree: ## https://hg.mozilla.org/releases/mozilla-release/raw-file/default/security/nss/lib/ckfw/builtins/certdata.txt ## ## It contains the certificates in PEM format and therefore ## can be directly used with curl / libcurl / php_curl, or with ## an Apache+mod_ssl webserver for SSL client authentication. ## Just configure this file as the SSLCACertificateFile. ## ## Conversion done with mk-ca-bundle.pl version 1.28. ## SHA256: 3bdc63d1de27058fec943a999a2a8a01fcc6806a611b19221a7727d3d9bbbdfd ## GlobalSign Root CA ================== -----BEGIN CERTIFICATE----- MIIDdTCCAl2gAwIBAgILBAAAAAABFUtaw5QwDQYJKoZIhvcNAQEFBQAwVzELMAkGA1UEBhMCQkUx GTAXBgNVBAoTEEdsb2JhbFNpZ24gbnYtc2ExEDAOBgNVBAsTB1Jvb3QgQ0ExGzAZBgNVBAMTEkds b2JhbFNpZ24gUm9vdCBDQTAeFw05ODA5MDExMjAwMDBaFw0yODAxMjgxMjAwMDBaMFcxCzAJBgNV BAYTAkJFMRkwFwYDVQQKExBHbG9iYWxTaWduIG52LXNhMRAwDgYDVQQLEwdSb290IENBMRswGQYD VQQDExJHbG9iYWxTaWduIFJvb3QgQ0EwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDa DuaZjc6j40+Kfvvxi4Mla+pIH/EqsLmVEQS98GPR4mdmzxzdzxtIK+6NiY6arymAZavpxy0Sy6sc THAHoT0KMM0VjU/43dSMUBUc71DuxC73/OlS8pF94G3VNTCOXkNz8kHp1Wrjsok6Vjk4bwY8iGlb Kk3Fp1S4bInMm/k8yuX9ifUSPJJ4ltbcdG6TRGHRjcdGsnUOhugZitVtbNV4FpWi6cgKOOvyJBNP c1STE4U6G7weNLWLBYy5d4ux2x8gkasJU26Qzns3dLlwR5EiUWMWea6xrkEmCMgZK9FGqkjWZCrX gzT/LCrBbBlDSgeF59N89iFo7+ryUp9/k5DPAgMBAAGjQjBAMA4GA1UdDwEB/wQEAwIBBjAPBgNV HRMBAf8EBTADAQH/MB0GA1UdDgQWBBRge2YaRQ2XyolQL30EzTSo//z9SzANBgkqhkiG9w0BAQUF AAOCAQEA1nPnfE920I2/7LqivjTFKDK1fPxsnCwrvQmeU79rXqoRSLblCKOzyj1hTdNGCbM+w6Dj Y1Ub8rrvrTnhQ7k4o+YviiY776BQVvnGCv04zcQLcFGUl5gE38NflNUVyRRBnMRddWQVDf9VMOyG j/8N7yy5Y0b2qvzfvGn9LhJIZJrglfCm7ymPAbEVtQwdpf5pLGkkeB6zpxxxYu7KyJesF12KwvhH hm4qxFYxldBniYUr+WymXUadDKqC5JlR3XC321Y9YeRq4VzW9v493kHMB65jUr9TU/Qr6cf9tveC X4XSQRjbgbMEHMUfpIBvFSDJ3gyICh3WZlXi/EjJKSZp4A== -----END CERTIFICATE----- GlobalSign Root CA - R2 ======================= -----BEGIN CERTIFICATE----- MIIDujCCAqKgAwIBAgILBAAAAAABD4Ym5g0wDQYJKoZIhvcNAQEFBQAwTDEgMB4GA1UECxMXR2xv YmFsU2lnbiBSb290IENBIC0gUjIxEzARBgNVBAoTCkdsb2JhbFNpZ24xEzARBgNVBAMTCkdsb2Jh bFNpZ24wHhcNMDYxMjE1MDgwMDAwWhcNMjExMjE1MDgwMDAwWjBMMSAwHgYDVQQLExdHbG9iYWxT aWduIFJvb3QgQ0EgLSBSMjETMBEGA1UEChMKR2xvYmFsU2lnbjETMBEGA1UEAxMKR2xvYmFsU2ln bjCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAKbPJA6+Lm8omUVCxKs+IVSbC9N/hHD6 ErPLv4dfxn+G07IwXNb9rfF73OX4YJYJkhD10FPe+3t+c4isUoh7SqbKSaZeqKeMWhG8eoLrvozp s6yWJQeXSpkqBy+0Hne/ig+1AnwblrjFuTosvNYSuetZfeLQBoZfXklqtTleiDTsvHgMCJiEbKjN S7SgfQx5TfC4LcshytVsW33hoCmEofnTlEnLJGKRILzdC9XZzPnqJworc5HGnRusyMvo4KD0L5CL TfuwNhv2GXqF4G3yYROIXJ/gkwpRl4pazq+r1feqCapgvdzZX99yqWATXgAByUr6P6TqBwMhAo6C ygPCm48CAwEAAaOBnDCBmTAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4E FgQUm+IHV2ccHsBqBt5ZtJot39wZhi4wNgYDVR0fBC8wLTAroCmgJ4YlaHR0cDovL2NybC5nbG9i YWxzaWduLm5ldC9yb290LXIyLmNybDAfBgNVHSMEGDAWgBSb4gdXZxwewGoG3lm0mi3f3BmGLjAN BgkqhkiG9w0BAQUFAAOCAQEAmYFThxxol4aR7OBKuEQLq4GsJ0/WwbgcQ3izDJr86iw8bmEbTUsp 9Z8FHSbBuOmDAGJFtqkIk7mpM0sYmsL4h4hO291xNBrBVNpGP+DTKqttVCL1OmLNIG+6KYnX3ZHu 01yiPqFbQfXf5WRDLenVOavSot+3i9DAgBkcRcAtjOj4LaR0VknFBbVPFd5uRHg5h6h+u/N5GJG7 9G+dwfCMNYxdAfvDbbnvRG15RjF+Cv6pgsH/76tuIMRQyV+dTZsXjAzlAcmgQWpzU/qlULRuJQ/7 TBj0/VLZjmmx6BEP3ojY+x1J96relc8geMJgEtslQIxq/H5COEBkEveegeGTLg== -----END CERTIFICATE----- Entrust.net Premium 2048 Secure Server CA ========================================= -----BEGIN CERTIFICATE----- MIIEKjCCAxKgAwIBAgIEOGPe+DANBgkqhkiG9w0BAQUFADCBtDEUMBIGA1UEChMLRW50cnVzdC5u ZXQxQDA+BgNVBAsUN3d3dy5lbnRydXN0Lm5ldC9DUFNfMjA0OCBpbmNvcnAuIGJ5IHJlZi4gKGxp bWl0cyBsaWFiLikxJTAjBgNVBAsTHChjKSAxOTk5IEVudHJ1c3QubmV0IExpbWl0ZWQxMzAxBgNV BAMTKkVudHJ1c3QubmV0IENlcnRpZmljYXRpb24gQXV0aG9yaXR5ICgyMDQ4KTAeFw05OTEyMjQx NzUwNTFaFw0yOTA3MjQxNDE1MTJaMIG0MRQwEgYDVQQKEwtFbnRydXN0Lm5ldDFAMD4GA1UECxQ3 d3d3LmVudHJ1c3QubmV0L0NQU18yMDQ4IGluY29ycC4gYnkgcmVmLiAobGltaXRzIGxpYWIuKTEl MCMGA1UECxMcKGMpIDE5OTkgRW50cnVzdC5uZXQgTGltaXRlZDEzMDEGA1UEAxMqRW50cnVzdC5u ZXQgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkgKDIwNDgpMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A MIIBCgKCAQEArU1LqRKGsuqjIAcVFmQqK0vRvwtKTY7tgHalZ7d4QMBzQshowNtTK91euHaYNZOL Gp18EzoOH1u3Hs/lJBQesYGpjX24zGtLA/ECDNyrpUAkAH90lKGdCCmziAv1h3edVc3kw37XamSr hRSGlVuXMlBvPci6Zgzj/L24ScF2iUkZ/cCovYmjZy/Gn7xxGWC4LeksyZB2ZnuU4q941mVTXTzW nLLPKQP5L6RQstRIzgUyVYr9smRMDuSYB3Xbf9+5CFVghTAp+XtIpGmG4zU/HoZdenoVve8AjhUi VBcAkCaTvA5JaJG/+EfTnZVCwQ5N328mz8MYIWJmQ3DW1cAH4QIDAQABo0IwQDAOBgNVHQ8BAf8E BAMCAQYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUVeSB0RGAvtiJuQijMfmhJAkWuXAwDQYJ KoZIhvcNAQEFBQADggEBADubj1abMOdTmXx6eadNl9cZlZD7Bh/KM3xGY4+WZiT6QBshJ8rmcnPy T/4xmf3IDExoU8aAghOY+rat2l098c5u9hURlIIM7j+VrxGrD9cv3h8Dj1csHsm7mhpElesYT6Yf zX1XEC+bBAlahLVu2B064dae0Wx5XnkcFMXj0EyTO2U87d89vqbllRrDtRnDvV5bu/8j72gZyxKT J1wDLW8w0B62GqzeWvfRqqgnpv55gcR5mTNXuhKwqeBCbJPKVt7+bYQLCIt+jerXmCHG8+c8eS9e nNFMFY3h7CI3zJpDC5fcgJCNs2ebb0gIFVbPv/ErfF6adulZkMV8gzURZVE= -----END CERTIFICATE----- Baltimore CyberTrust Root ========================= -----BEGIN CERTIFICATE----- MIIDdzCCAl+gAwIBAgIEAgAAuTANBgkqhkiG9w0BAQUFADBaMQswCQYDVQQGEwJJRTESMBAGA1UE ChMJQmFsdGltb3JlMRMwEQYDVQQLEwpDeWJlclRydXN0MSIwIAYDVQQDExlCYWx0aW1vcmUgQ3li ZXJUcnVzdCBSb290MB4XDTAwMDUxMjE4NDYwMFoXDTI1MDUxMjIzNTkwMFowWjELMAkGA1UEBhMC SUUxEjAQBgNVBAoTCUJhbHRpbW9yZTETMBEGA1UECxMKQ3liZXJUcnVzdDEiMCAGA1UEAxMZQmFs dGltb3JlIEN5YmVyVHJ1c3QgUm9vdDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAKME uyKrmD1X6CZymrV51Cni4eiVgLGw41uOKymaZN+hXe2wCQVt2yguzmKiYv60iNoS6zjrIZ3AQSsB UnuId9Mcj8e6uYi1agnnc+gRQKfRzMpijS3ljwumUNKoUMMo6vWrJYeKmpYcqWe4PwzV9/lSEy/C G9VwcPCPwBLKBsua4dnKM3p31vjsufFoREJIE9LAwqSuXmD+tqYF/LTdB1kC1FkYmGP1pWPgkAx9 XbIGevOF6uvUA65ehD5f/xXtabz5OTZydc93Uk3zyZAsuT3lySNTPx8kmCFcB5kpvcY67Oduhjpr l3RjM71oGDHweI12v/yejl0qhqdNkNwnGjkCAwEAAaNFMEMwHQYDVR0OBBYEFOWdWTCCR1jMrPoI VDaGezq1BE3wMBIGA1UdEwEB/wQIMAYBAf8CAQMwDgYDVR0PAQH/BAQDAgEGMA0GCSqGSIb3DQEB BQUAA4IBAQCFDF2O5G9RaEIFoN27TyclhAO992T9Ldcw46QQF+vaKSm2eT929hkTI7gQCvlYpNRh cL0EYWoSihfVCr3FvDB81ukMJY2GQE/szKN+OMY3EU/t3WgxjkzSswF07r51XgdIGn9w/xZchMB5 hbgF/X++ZRGjD8ACtPhSNzkE1akxehi/oCr0Epn3o0WC4zxe9Z2etciefC7IpJ5OCBRLbf1wbWsa Y71k5h+3zvDyny67G7fyUIhzksLi4xaNmjICq44Y3ekQEe5+NauQrz4wlHrQMz2nZQ/1/I6eYs9H RCwBXbsdtTLSR9I4LtD+gdwyah617jzV/OeBHRnDJELqYzmp -----END CERTIFICATE----- Entrust Root Certification Authority ==================================== -----BEGIN CERTIFICATE----- MIIEkTCCA3mgAwIBAgIERWtQVDANBgkqhkiG9w0BAQUFADCBsDELMAkGA1UEBhMCVVMxFjAUBgNV BAoTDUVudHJ1c3QsIEluYy4xOTA3BgNVBAsTMHd3dy5lbnRydXN0Lm5ldC9DUFMgaXMgaW5jb3Jw b3JhdGVkIGJ5IHJlZmVyZW5jZTEfMB0GA1UECxMWKGMpIDIwMDYgRW50cnVzdCwgSW5jLjEtMCsG A1UEAxMkRW50cnVzdCBSb290IENlcnRpZmljYXRpb24gQXV0aG9yaXR5MB4XDTA2MTEyNzIwMjM0 MloXDTI2MTEyNzIwNTM0MlowgbAxCzAJBgNVBAYTAlVTMRYwFAYDVQQKEw1FbnRydXN0LCBJbmMu MTkwNwYDVQQLEzB3d3cuZW50cnVzdC5uZXQvQ1BTIGlzIGluY29ycG9yYXRlZCBieSByZWZlcmVu Y2UxHzAdBgNVBAsTFihjKSAyMDA2IEVudHJ1c3QsIEluYy4xLTArBgNVBAMTJEVudHJ1c3QgUm9v dCBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEB ALaVtkNC+sZtKm9I35RMOVcF7sN5EUFoNu3s/poBj6E4KPz3EEZmLk0eGrEaTsbRwJWIsMn/MYsz A9u3g3s+IIRe7bJWKKf44LlAcTfFy0cOlypowCKVYhXbR9n10Cv/gkvJrT7eTNuQgFA/CYqEAOww Cj0Yzfv9KlmaI5UXLEWeH25DeW0MXJj+SKfFI0dcXv1u5x609mhF0YaDW6KKjbHjKYD+JXGIrb68 j6xSlkuqUY3kEzEZ6E5Nn9uss2rVvDlUccp6en+Q3X0dgNmBu1kmwhH+5pPi94DkZfs0Nw4pgHBN rziGLp5/V6+eF67rHMsoIV+2HNjnogQi+dPa2MsCAwEAAaOBsDCBrTAOBgNVHQ8BAf8EBAMCAQYw DwYDVR0TAQH/BAUwAwEB/zArBgNVHRAEJDAigA8yMDA2MTEyNzIwMjM0MlqBDzIwMjYxMTI3MjA1 MzQyWjAfBgNVHSMEGDAWgBRokORnpKZTgMeGZqTx90tD+4S9bTAdBgNVHQ4EFgQUaJDkZ6SmU4DH hmak8fdLQ/uEvW0wHQYJKoZIhvZ9B0EABBAwDhsIVjcuMTo0LjADAgSQMA0GCSqGSIb3DQEBBQUA A4IBAQCT1DCw1wMgKtD5Y+iRDAUgqV8ZyntyTtSx29CW+1RaGSwMCPeyvIWonX9tO1KzKtvn1ISM Y/YPyyYBkVBs9F8U4pN0wBOeMDpQ47RgxRzwIkSNcUesyBrJ6ZuaAGAT/3B+XxFNSRuzFVJ7yVTa v52Vr2ua2J7p8eRDjeIRRDq/r72DQnNSi6q7pynP9WQcCk3RvKqsnyrQ/39/2n3qse0wJcGE2jTS W3iDVuycNsMm4hH2Z0kdkquM++v/eu6FSqdQgPCnXEqULl8FmTxSQeDNtGPPAUO6nIPcj2A781q0 tHuu2guQOHXvgR1m0vdXcDazv/wor3ElhVsT/h5/WrQ8 -----END CERTIFICATE----- Comodo AAA Services root ======================== -----BEGIN CERTIFICATE----- MIIEMjCCAxqgAwIBAgIBATANBgkqhkiG9w0BAQUFADB7MQswCQYDVQQGEwJHQjEbMBkGA1UECAwS R3JlYXRlciBNYW5jaGVzdGVyMRAwDgYDVQQHDAdTYWxmb3JkMRowGAYDVQQKDBFDb21vZG8gQ0Eg TGltaXRlZDEhMB8GA1UEAwwYQUFBIENlcnRpZmljYXRlIFNlcnZpY2VzMB4XDTA0MDEwMTAwMDAw MFoXDTI4MTIzMTIzNTk1OVowezELMAkGA1UEBhMCR0IxGzAZBgNVBAgMEkdyZWF0ZXIgTWFuY2hl c3RlcjEQMA4GA1UEBwwHU2FsZm9yZDEaMBgGA1UECgwRQ29tb2RvIENBIExpbWl0ZWQxITAfBgNV BAMMGEFBQSBDZXJ0aWZpY2F0ZSBTZXJ2aWNlczCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoC ggEBAL5AnfRu4ep2hxxNRUSOvkbIgwadwSr+GB+O5AL686tdUIoWMQuaBtDFcCLNSS1UY8y2bmhG C1Pqy0wkwLxyTurxFa70VJoSCsN6sjNg4tqJVfMiWPPe3M/vg4aijJRPn2jymJBGhCfHdr/jzDUs i14HZGWCwEiwqJH5YZ92IFCokcdmtet4YgNW8IoaE+oxox6gmf049vYnMlhvB/VruPsUK6+3qszW Y19zjNoFmag4qMsXeDZRrOme9Hg6jc8P2ULimAyrL58OAd7vn5lJ8S3frHRNG5i1R8XlKdH5kBjH Ypy+g8cmez6KJcfA3Z3mNWgQIJ2P2N7Sw4ScDV7oL8kCAwEAAaOBwDCBvTAdBgNVHQ4EFgQUoBEK Iz6W8Qfs4q8p74Klf9AwpLQwDgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8wewYDVR0f BHQwcjA4oDagNIYyaHR0cDovL2NybC5jb21vZG9jYS5jb20vQUFBQ2VydGlmaWNhdGVTZXJ2aWNl cy5jcmwwNqA0oDKGMGh0dHA6Ly9jcmwuY29tb2RvLm5ldC9BQUFDZXJ0aWZpY2F0ZVNlcnZpY2Vz LmNybDANBgkqhkiG9w0BAQUFAAOCAQEACFb8AvCb6P+k+tZ7xkSAzk/ExfYAWMymtrwUSWgEdujm 7l3sAg9g1o1QGE8mTgHj5rCl7r+8dFRBv/38ErjHT1r0iWAFf2C3BUrz9vHCv8S5dIa2LX1rzNLz Rt0vxuBqw8M0Ayx9lt1awg6nCpnBBYurDC/zXDrPbDdVCYfeU0BsWO/8tqtlbgT2G9w84FoVxp7Z 8VlIMCFlA2zs6SFz7JsDoeA3raAVGI/6ugLOpyypEBMs1OUIJqsil2D4kF501KKaU73yqWjgom7C 12yxow+ev+to51byrvLjKzg6CYG1a4XXvi3tPxq3smPi9WIsgtRqAEFQ8TmDn5XpNpaYbg== -----END CERTIFICATE----- QuoVadis Root CA ================ -----BEGIN CERTIFICATE----- MIIF0DCCBLigAwIBAgIEOrZQizANBgkqhkiG9w0BAQUFADB/MQswCQYDVQQGEwJCTTEZMBcGA1UE ChMQUXVvVmFkaXMgTGltaXRlZDElMCMGA1UECxMcUm9vdCBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0 eTEuMCwGA1UEAxMlUXVvVmFkaXMgUm9vdCBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTAeFw0wMTAz MTkxODMzMzNaFw0yMTAzMTcxODMzMzNaMH8xCzAJBgNVBAYTAkJNMRkwFwYDVQQKExBRdW9WYWRp cyBMaW1pdGVkMSUwIwYDVQQLExxSb290IENlcnRpZmljYXRpb24gQXV0aG9yaXR5MS4wLAYDVQQD EyVRdW9WYWRpcyBSb290IENlcnRpZmljYXRpb24gQXV0aG9yaXR5MIIBIjANBgkqhkiG9w0BAQEF AAOCAQ8AMIIBCgKCAQEAv2G1lVO6V/z68mcLOhrfEYBklbTRvM16z/Ypli4kVEAkOPcahdxYTMuk J0KX0J+DisPkBgNbAKVRHnAEdOLB1Dqr1607BxgFjv2DrOpm2RgbaIr1VxqYuvXtdj182d6UajtL F8HVj71lODqV0D1VNk7feVcxKh7YWWVJWCCYfqtffp/p1k3sg3Spx2zY7ilKhSoGFPlU5tPaZQeL YzcS19Dsw3sgQUSj7cugF+FxZc4dZjH3dgEZyH0DWLaVSR2mEiboxgx24ONmy+pdpibu5cxfvWen AScOospUxbF6lR1xHkopigPcakXBpBlebzbNw6Kwt/5cOOJSvPhEQ+aQuwIDAQABo4ICUjCCAk4w PQYIKwYBBQUHAQEEMTAvMC0GCCsGAQUFBzABhiFodHRwczovL29jc3AucXVvdmFkaXNvZmZzaG9y ZS5jb20wDwYDVR0TAQH/BAUwAwEB/zCCARoGA1UdIASCAREwggENMIIBCQYJKwYBBAG+WAABMIH7 MIHUBggrBgEFBQcCAjCBxxqBxFJlbGlhbmNlIG9uIHRoZSBRdW9WYWRpcyBSb290IENlcnRpZmlj YXRlIGJ5IGFueSBwYXJ0eSBhc3N1bWVzIGFjY2VwdGFuY2Ugb2YgdGhlIHRoZW4gYXBwbGljYWJs ZSBzdGFuZGFyZCB0ZXJtcyBhbmQgY29uZGl0aW9ucyBvZiB1c2UsIGNlcnRpZmljYXRpb24gcHJh Y3RpY2VzLCBhbmQgdGhlIFF1b1ZhZGlzIENlcnRpZmljYXRlIFBvbGljeS4wIgYIKwYBBQUHAgEW Fmh0dHA6Ly93d3cucXVvdmFkaXMuYm0wHQYDVR0OBBYEFItLbe3TKbkGGew5Oanwl4Rqy+/fMIGu BgNVHSMEgaYwgaOAFItLbe3TKbkGGew5Oanwl4Rqy+/foYGEpIGBMH8xCzAJBgNVBAYTAkJNMRkw FwYDVQQKExBRdW9WYWRpcyBMaW1pdGVkMSUwIwYDVQQLExxSb290IENlcnRpZmljYXRpb24gQXV0 aG9yaXR5MS4wLAYDVQQDEyVRdW9WYWRpcyBSb290IENlcnRpZmljYXRpb24gQXV0aG9yaXR5ggQ6 tlCLMA4GA1UdDwEB/wQEAwIBBjANBgkqhkiG9w0BAQUFAAOCAQEAitQUtf70mpKnGdSkfnIYj9lo fFIk3WdvOXrEql494liwTXCYhGHoG+NpGA7O+0dQoE7/8CQfvbLO9Sf87C9TqnN7Az10buYWnuul LsS/VidQK2K6vkscPFVcQR0kvoIgR13VRH56FmjffU1RcHhXHTMe/QKZnAzNCgVPx7uOpHX6Sm2x gI4JVrmcGmD+XcHXetwReNDWXcG31a0ymQM6isxUJTkxgXsTIlG6Rmyhu576BGxJJnSP0nPrzDCi 5upZIof4l/UO/erMkqQWxFIY6iHOsfHmhIHluqmGKPJDWl0Snawe2ajlCmqnf6CHKc/yiU3U7MXi 5nrQNiOKSnQ2+Q== -----END CERTIFICATE----- QuoVadis Root CA 2 ================== -----BEGIN CERTIFICATE----- MIIFtzCCA5+gAwIBAgICBQkwDQYJKoZIhvcNAQEFBQAwRTELMAkGA1UEBhMCQk0xGTAXBgNVBAoT EFF1b1ZhZGlzIExpbWl0ZWQxGzAZBgNVBAMTElF1b1ZhZGlzIFJvb3QgQ0EgMjAeFw0wNjExMjQx ODI3MDBaFw0zMTExMjQxODIzMzNaMEUxCzAJBgNVBAYTAkJNMRkwFwYDVQQKExBRdW9WYWRpcyBM aW1pdGVkMRswGQYDVQQDExJRdW9WYWRpcyBSb290IENBIDIwggIiMA0GCSqGSIb3DQEBAQUAA4IC DwAwggIKAoICAQCaGMpLlA0ALa8DKYrwD4HIrkwZhR0In6spRIXzL4GtMh6QRr+jhiYaHv5+HBg6 XJxgFyo6dIMzMH1hVBHL7avg5tKifvVrbxi3Cgst/ek+7wrGsxDp3MJGF/hd/aTa/55JWpzmM+Yk lvc/ulsrHHo1wtZn/qtmUIttKGAr79dgw8eTvI02kfN/+NsRE8Scd3bBrrcCaoF6qUWD4gXmuVbB lDePSHFjIuwXZQeVikvfj8ZaCuWw419eaxGrDPmF60Tp+ARz8un+XJiM9XOva7R+zdRcAitMOeGy lZUtQofX1bOQQ7dsE/He3fbE+Ik/0XX1ksOR1YqI0JDs3G3eicJlcZaLDQP9nL9bFqyS2+r+eXyt 66/3FsvbzSUr5R/7mp/iUcw6UwxI5g69ybR2BlLmEROFcmMDBOAENisgGQLodKcftslWZvB1Jdxn wQ5hYIizPtGo/KPaHbDRsSNU30R2be1B2MGyIrZTHN81Hdyhdyox5C315eXbyOD/5YDXC2Og/zOh D7osFRXql7PSorW+8oyWHhqPHWykYTe5hnMz15eWniN9gqRMgeKh0bpnX5UHoycR7hYQe7xFSkyy BNKr79X9DFHOUGoIMfmR2gyPZFwDwzqLID9ujWc9Otb+fVuIyV77zGHcizN300QyNQliBJIWENie J0f7OyHj+OsdWwIDAQABo4GwMIGtMA8GA1UdEwEB/wQFMAMBAf8wCwYDVR0PBAQDAgEGMB0GA1Ud DgQWBBQahGK8SEwzJQTU7tD2A8QZRtGUazBuBgNVHSMEZzBlgBQahGK8SEwzJQTU7tD2A8QZRtGU a6FJpEcwRTELMAkGA1UEBhMCQk0xGTAXBgNVBAoTEFF1b1ZhZGlzIExpbWl0ZWQxGzAZBgNVBAMT ElF1b1ZhZGlzIFJvb3QgQ0EgMoICBQkwDQYJKoZIhvcNAQEFBQADggIBAD4KFk2fBluornFdLwUv Z+YTRYPENvbzwCYMDbVHZF34tHLJRqUDGCdViXh9duqWNIAXINzng/iN/Ae42l9NLmeyhP3ZRPx3 UIHmfLTJDQtyU/h2BwdBR5YM++CCJpNVjP4iH2BlfF/nJrP3MpCYUNQ3cVX2kiF495V5+vgtJodm VjB3pjd4M1IQWK4/YY7yarHvGH5KWWPKjaJW1acvvFYfzznB4vsKqBUsfU16Y8Zsl0Q80m/DShcK +JDSV6IZUaUtl0HaB0+pUNqQjZRG4T7wlP0QADj1O+hA4bRuVhogzG9Yje0uRY/W6ZM/57Es3zrW IozchLsib9D45MY56QSIPMO661V6bYCZJPVsAfv4l7CUW+v90m/xd2gNNWQjrLhVoQPRTUIZ3Ph1 WVaj+ahJefivDrkRoHy3au000LYmYjgahwz46P0u05B/B5EqHdZ+XIWDmbA4CD/pXvk1B+TJYm5X f6dQlfe6yJvmjqIBxdZmv3lh8zwc4bmCXF2gw+nYSL0ZohEUGW6yhhtoPkg3Goi3XZZenMfvJ2II 4pEZXNLxId26F0KCl3GBUzGpn/Z9Yr9y4aOTHcyKJloJONDO1w2AFrR4pTqHTI2KpdVGl/IsELm8 VCLAAVBpQ570su9t+Oza8eOx79+Rj1QqCyXBJhnEUhAFZdWCEOrCMc0u -----END CERTIFICATE----- QuoVadis Root CA 3 ================== -----BEGIN CERTIFICATE----- MIIGnTCCBIWgAwIBAgICBcYwDQYJKoZIhvcNAQEFBQAwRTELMAkGA1UEBhMCQk0xGTAXBgNVBAoT EFF1b1ZhZGlzIExpbWl0ZWQxGzAZBgNVBAMTElF1b1ZhZGlzIFJvb3QgQ0EgMzAeFw0wNjExMjQx OTExMjNaFw0zMTExMjQxOTA2NDRaMEUxCzAJBgNVBAYTAkJNMRkwFwYDVQQKExBRdW9WYWRpcyBM aW1pdGVkMRswGQYDVQQDExJRdW9WYWRpcyBSb290IENBIDMwggIiMA0GCSqGSIb3DQEBAQUAA4IC DwAwggIKAoICAQDMV0IWVJzmmNPTTe7+7cefQzlKZbPoFog02w1ZkXTPkrgEQK0CSzGrvI2RaNgg DhoB4hp7Thdd4oq3P5kazethq8Jlph+3t723j/z9cI8LoGe+AaJZz3HmDyl2/7FWeUUrH556VOij KTVopAFPD6QuN+8bv+OPEKhyq1hX51SGyMnzW9os2l2ObjyjPtr7guXd8lyyBTNvijbO0BNO/79K DDRMpsMhvVAEVeuxu537RR5kFd5VAYwCdrXLoT9CabwvvWhDFlaJKjdhkf2mrk7AyxRllDdLkgbv BNDInIjbC3uBr7E9KsRlOni27tyAsdLTmZw67mtaa7ONt9XOnMK+pUsvFrGeaDsGb659n/je7Mwp p5ijJUMv7/FfJuGITfhebtfZFG4ZM2mnO4SJk8RTVROhUXhA+LjJou57ulJCg54U7QVSWllWp5f8 nT8KKdjcT5EOE7zelaTfi5m+rJsziO+1ga8bxiJTyPbH7pcUsMV8eFLI8M5ud2CEpukqdiDtWAEX MJPpGovgc2PZapKUSU60rUqFxKMiMPwJ7Wgic6aIDFUhWMXhOp8q3crhkODZc6tsgLjoC2SToJyM Gf+z0gzskSaHirOi4XCPLArlzW1oUevaPwV/izLmE1xr/l9A4iLItLRkT9a6fUg+qGkM17uGcclz uD87nSVL2v9A6wIDAQABo4IBlTCCAZEwDwYDVR0TAQH/BAUwAwEB/zCB4QYDVR0gBIHZMIHWMIHT BgkrBgEEAb5YAAMwgcUwgZMGCCsGAQUFBwICMIGGGoGDQW55IHVzZSBvZiB0aGlzIENlcnRpZmlj YXRlIGNvbnN0aXR1dGVzIGFjY2VwdGFuY2Ugb2YgdGhlIFF1b1ZhZGlzIFJvb3QgQ0EgMyBDZXJ0 aWZpY2F0ZSBQb2xpY3kgLyBDZXJ0aWZpY2F0aW9uIFByYWN0aWNlIFN0YXRlbWVudC4wLQYIKwYB BQUHAgEWIWh0dHA6Ly93d3cucXVvdmFkaXNnbG9iYWwuY29tL2NwczALBgNVHQ8EBAMCAQYwHQYD VR0OBBYEFPLAE+CCQz777i9nMpY1XNu4ywLQMG4GA1UdIwRnMGWAFPLAE+CCQz777i9nMpY1XNu4 ywLQoUmkRzBFMQswCQYDVQQGEwJCTTEZMBcGA1UEChMQUXVvVmFkaXMgTGltaXRlZDEbMBkGA1UE AxMSUXVvVmFkaXMgUm9vdCBDQSAzggIFxjANBgkqhkiG9w0BAQUFAAOCAgEAT62gLEz6wPJv92ZV qyM07ucp2sNbtrCD2dDQ4iH782CnO11gUyeim/YIIirnv6By5ZwkajGxkHon24QRiSemd1o417+s hvzuXYO8BsbRd2sPbSQvS3pspweWyuOEn62Iix2rFo1bZhfZFvSLgNLd+LJ2w/w4E6oM3kJpK27z POuAJ9v1pkQNn1pVWQvVDVJIxa6f8i+AxeoyUDUSly7B4f/xI4hROJ/yZlZ25w9Rl6VSDE1JUZU2 Pb+iSwwQHYaZTKrzchGT5Or2m9qoXadNt54CrnMAyNojA+j56hl0YgCUyyIgvpSnWbWCar6ZeXqp 8kokUvd0/bpO5qgdAm6xDYBEwa7TIzdfu4V8K5Iu6H6li92Z4b8nby1dqnuH/grdS/yO9SbkbnBC bjPsMZ57k8HkyWkaPcBrTiJt7qtYTcbQQcEr6k8Sh17rRdhs9ZgC06DYVYoGmRmioHfRMJ6szHXu g/WwYjnPbFfiTNKRCw51KBuav/0aQ/HKd/s7j2G4aSgWQgRecCocIdiP4b0jWy10QJLZYxkNc91p vGJHvOB0K7Lrfb5BG7XARsWhIstfTsEokt4YutUqKLsRixeTmJlglFwjz1onl14LBQaTNx47aTbr qZ5hHY8y2o4M1nQ+ewkk2gF3R8Q7zTSMmfXK4SVhM7JZG+Ju1zdXtg2pEto= -----END CERTIFICATE----- Security Communication Root CA ============================== -----BEGIN CERTIFICATE----- MIIDWjCCAkKgAwIBAgIBADANBgkqhkiG9w0BAQUFADBQMQswCQYDVQQGEwJKUDEYMBYGA1UEChMP U0VDT00gVHJ1c3QubmV0MScwJQYDVQQLEx5TZWN1cml0eSBDb21tdW5pY2F0aW9uIFJvb3RDQTEw HhcNMDMwOTMwMDQyMDQ5WhcNMjMwOTMwMDQyMDQ5WjBQMQswCQYDVQQGEwJKUDEYMBYGA1UEChMP U0VDT00gVHJ1c3QubmV0MScwJQYDVQQLEx5TZWN1cml0eSBDb21tdW5pY2F0aW9uIFJvb3RDQTEw ggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCzs/5/022x7xZ8V6UMbXaKL0u/ZPtM7orw 8yl89f/uKuDp6bpbZCKamm8sOiZpUQWZJtzVHGpxxpp9Hp3dfGzGjGdnSj74cbAZJ6kJDKaVv0uM DPpVmDvY6CKhS3E4eayXkmmziX7qIWgGmBSWh9JhNrxtJ1aeV+7AwFb9Ms+k2Y7CI9eNqPPYJayX 5HA49LY6tJ07lyZDo6G8SVlyTCMwhwFY9k6+HGhWZq/NQV3Is00qVUarH9oe4kA92819uZKAnDfd DJZkndwi92SL32HeFZRSFaB9UslLqCHJxrHty8OVYNEP8Ktw+N/LTX7s1vqr2b1/VPKl6Xn62dZ2 JChzAgMBAAGjPzA9MB0GA1UdDgQWBBSgc0mZaNyFW2XjmygvV5+9M7wHSDALBgNVHQ8EBAMCAQYw DwYDVR0TAQH/BAUwAwEB/zANBgkqhkiG9w0BAQUFAAOCAQEAaECpqLvkT115swW1F7NgE+vGkl3g 0dNq/vu+m22/xwVtWSDEHPC32oRYAmP6SBbvT6UL90qY8j+eG61Ha2POCEfrUj94nK9NrvjVT8+a mCoQQTlSxN3Zmw7vkwGusi7KaEIkQmywszo+zenaSMQVy+n5Bw+SUEmK3TGXX8npN6o7WWWXlDLJ s58+OmJYxUmtYg5xpTKqL8aJdkNAExNnPaJUJRDL8Try2frbSVa7pv6nQTXD4IhhyYjH3zYQIphZ 6rBK+1YWc26sTfcioU+tHXotRSflMMFe8toTyyVCUZVHA4xsIcx0Qu1T/zOLjw9XARYvz6buyXAi FL39vmwLAw== -----END CERTIFICATE----- Sonera Class 2 Root CA ====================== -----BEGIN CERTIFICATE----- MIIDIDCCAgigAwIBAgIBHTANBgkqhkiG9w0BAQUFADA5MQswCQYDVQQGEwJGSTEPMA0GA1UEChMG U29uZXJhMRkwFwYDVQQDExBTb25lcmEgQ2xhc3MyIENBMB4XDTAxMDQwNjA3Mjk0MFoXDTIxMDQw NjA3Mjk0MFowOTELMAkGA1UEBhMCRkkxDzANBgNVBAoTBlNvbmVyYTEZMBcGA1UEAxMQU29uZXJh IENsYXNzMiBDQTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAJAXSjWdyvANlsdE+hY3 /Ei9vX+ALTU74W+oZ6m/AxxNjG8yR9VBaKQTBME1DJqEQ/xcHf+Js+gXGM2RX/uJ4+q/Tl18GybT dXnt5oTjV+WtKcT0OijnpXuENmmz/V52vaMtmdOQTiMofRhj8VQ7Jp12W5dCsv+u8E7s3TmVToMG f+dJQMjFAbJUWmYdPfz56TwKnoG4cPABi+QjVHzIrviQHgCWctRUz2EjvOr7nQKV0ba5cTppCD8P tOFCx4j1P5iop7oc4HFx71hXgVB6XGt0Rg6DA5jDjqhu8nYybieDwnPz3BjotJPqdURrBGAgcVeH nfO+oJAjPYok4doh28MCAwEAAaMzMDEwDwYDVR0TAQH/BAUwAwEB/zARBgNVHQ4ECgQISqCqWITT XjwwCwYDVR0PBAQDAgEGMA0GCSqGSIb3DQEBBQUAA4IBAQBazof5FnIVV0sd2ZvnoiYw7JNn39Yt 0jSv9zilzqsWuasvfDXLrNAPtEwr/IDva4yRXzZ299uzGxnq9LIR/WFxRL8oszodv7ND6J+/3DEI cbCdjdY0RzKQxmUk96BKfARzjzlvF4xytb1LyHr4e4PDKE6cCepnP7JnBBvDFNr450kkkdAdavph Oe9r5yF1BgfYErQhIHBCcYHaPJo2vqZbDWpsmh+Re/n570K6Tk6ezAyNlNzZRZxe7EJQY670XcSx EtzKO6gunRRaBXW37Ndj4ro1tgQIkejanZz2ZrUYrAqmVCY0M9IbwdR/GjqOC6oybtv8TyWf2TLH llpwrN9M -----END CERTIFICATE----- XRamp Global CA Root ==================== -----BEGIN CERTIFICATE----- MIIEMDCCAxigAwIBAgIQUJRs7Bjq1ZxN1ZfvdY+grTANBgkqhkiG9w0BAQUFADCBgjELMAkGA1UE BhMCVVMxHjAcBgNVBAsTFXd3dy54cmFtcHNlY3VyaXR5LmNvbTEkMCIGA1UEChMbWFJhbXAgU2Vj dXJpdHkgU2VydmljZXMgSW5jMS0wKwYDVQQDEyRYUmFtcCBHbG9iYWwgQ2VydGlmaWNhdGlvbiBB dXRob3JpdHkwHhcNMDQxMTAxMTcxNDA0WhcNMzUwMTAxMDUzNzE5WjCBgjELMAkGA1UEBhMCVVMx HjAcBgNVBAsTFXd3dy54cmFtcHNlY3VyaXR5LmNvbTEkMCIGA1UEChMbWFJhbXAgU2VjdXJpdHkg U2VydmljZXMgSW5jMS0wKwYDVQQDEyRYUmFtcCBHbG9iYWwgQ2VydGlmaWNhdGlvbiBBdXRob3Jp dHkwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCYJB69FbS638eMpSe2OAtp87ZOqCwu IR1cRN8hXX4jdP5efrRKt6atH67gBhbim1vZZ3RrXYCPKZ2GG9mcDZhtdhAoWORlsH9KmHmf4MMx foArtYzAQDsRhtDLooY2YKTVMIJt2W7QDxIEM5dfT2Fa8OT5kavnHTu86M/0ay00fOJIYRyO82FE zG+gSqmUsE3a56k0enI4qEHMPJQRfevIpoy3hsvKMzvZPTeL+3o+hiznc9cKV6xkmxnr9A8ECIqs AxcZZPRaJSKNNCyy9mgdEm3Tih4U2sSPpuIjhdV6Db1q4Ons7Be7QhtnqiXtRYMh/MHJfNViPvry xS3T/dRlAgMBAAGjgZ8wgZwwEwYJKwYBBAGCNxQCBAYeBABDAEEwCwYDVR0PBAQDAgGGMA8GA1Ud EwEB/wQFMAMBAf8wHQYDVR0OBBYEFMZPoj0GY4QJnM5i5ASsjVy16bYbMDYGA1UdHwQvMC0wK6Ap oCeGJWh0dHA6Ly9jcmwueHJhbXBzZWN1cml0eS5jb20vWEdDQS5jcmwwEAYJKwYBBAGCNxUBBAMC AQEwDQYJKoZIhvcNAQEFBQADggEBAJEVOQMBG2f7Shz5CmBbodpNl2L5JFMn14JkTpAuw0kbK5rc /Kh4ZzXxHfARvbdI4xD2Dd8/0sm2qlWkSLoC295ZLhVbO50WfUfXN+pfTXYSNrsf16GBBEYgoyxt qZ4Bfj8pzgCT3/3JknOJiWSe5yvkHJEs0rnOfc5vMZnT5r7SHpDwCRR5XCOrTdLaIR9NmXmd4c8n nxCbHIgNsIpkQTG4DmyQJKSbXHGPurt+HBvbaoAPIbzp26a3QPSyi6mx5O+aGtA9aZnuqCij4Tyz 8LIRnM98QObd50N9otg6tamN8jSZxNQQ4Qb9CYQQO+7ETPTsJ3xCwnR8gooJybQDJbw= -----END CERTIFICATE----- Go Daddy Class 2 CA =================== -----BEGIN CERTIFICATE----- MIIEADCCAuigAwIBAgIBADANBgkqhkiG9w0BAQUFADBjMQswCQYDVQQGEwJVUzEhMB8GA1UEChMY VGhlIEdvIERhZGR5IEdyb3VwLCBJbmMuMTEwLwYDVQQLEyhHbyBEYWRkeSBDbGFzcyAyIENlcnRp ZmljYXRpb24gQXV0aG9yaXR5MB4XDTA0MDYyOTE3MDYyMFoXDTM0MDYyOTE3MDYyMFowYzELMAkG A1UEBhMCVVMxITAfBgNVBAoTGFRoZSBHbyBEYWRkeSBHcm91cCwgSW5jLjExMC8GA1UECxMoR28g RGFkZHkgQ2xhc3MgMiBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTCCASAwDQYJKoZIhvcNAQEBBQAD ggENADCCAQgCggEBAN6d1+pXGEmhW+vXX0iG6r7d/+TvZxz0ZWizV3GgXne77ZtJ6XCAPVYYYwhv 2vLM0D9/AlQiVBDYsoHUwHU9S3/Hd8M+eKsaA7Ugay9qK7HFiH7Eux6wwdhFJ2+qN1j3hybX2C32 qRe3H3I2TqYXP2WYktsqbl2i/ojgC95/5Y0V4evLOtXiEqITLdiOr18SPaAIBQi2XKVlOARFmR6j YGB0xUGlcmIbYsUfb18aQr4CUWWoriMYavx4A6lNf4DD+qta/KFApMoZFv6yyO9ecw3ud72a9nmY vLEHZ6IVDd2gWMZEewo+YihfukEHU1jPEX44dMX4/7VpkI+EdOqXG68CAQOjgcAwgb0wHQYDVR0O BBYEFNLEsNKR1EwRcbNhyz2h/t2oatTjMIGNBgNVHSMEgYUwgYKAFNLEsNKR1EwRcbNhyz2h/t2o atTjoWekZTBjMQswCQYDVQQGEwJVUzEhMB8GA1UEChMYVGhlIEdvIERhZGR5IEdyb3VwLCBJbmMu MTEwLwYDVQQLEyhHbyBEYWRkeSBDbGFzcyAyIENlcnRpZmljYXRpb24gQXV0aG9yaXR5ggEAMAwG A1UdEwQFMAMBAf8wDQYJKoZIhvcNAQEFBQADggEBADJL87LKPpH8EsahB4yOd6AzBhRckB4Y9wim PQoZ+YeAEW5p5JYXMP80kWNyOO7MHAGjHZQopDH2esRU1/blMVgDoszOYtuURXO1v0XJJLXVggKt I3lpjbi2Tc7PTMozI+gciKqdi0FuFskg5YmezTvacPd+mSYgFFQlq25zheabIZ0KbIIOqPjCDPoQ HmyW74cNxA9hi63ugyuV+I6ShHI56yDqg+2DzZduCLzrTia2cyvk0/ZM/iZx4mERdEr/VxqHD3VI Ls9RaRegAhJhldXRQLIQTO7ErBBDpqWeCtWVYpoNz4iCxTIM5CufReYNnyicsbkqWletNw+vHX/b vZ8= -----END CERTIFICATE----- Starfield Class 2 CA ==================== -----BEGIN CERTIFICATE----- MIIEDzCCAvegAwIBAgIBADANBgkqhkiG9w0BAQUFADBoMQswCQYDVQQGEwJVUzElMCMGA1UEChMc U3RhcmZpZWxkIFRlY2hub2xvZ2llcywgSW5jLjEyMDAGA1UECxMpU3RhcmZpZWxkIENsYXNzIDIg Q2VydGlmaWNhdGlvbiBBdXRob3JpdHkwHhcNMDQwNjI5MTczOTE2WhcNMzQwNjI5MTczOTE2WjBo MQswCQYDVQQGEwJVUzElMCMGA1UEChMcU3RhcmZpZWxkIFRlY2hub2xvZ2llcywgSW5jLjEyMDAG A1UECxMpU3RhcmZpZWxkIENsYXNzIDIgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkwggEgMA0GCSqG SIb3DQEBAQUAA4IBDQAwggEIAoIBAQC3Msj+6XGmBIWtDBFk385N78gDGIc/oav7PKaf8MOh2tTY bitTkPskpD6E8J7oX+zlJ0T1KKY/e97gKvDIr1MvnsoFAZMej2YcOadN+lq2cwQlZut3f+dZxkqZ JRRU6ybH838Z1TBwj6+wRir/resp7defqgSHo9T5iaU0X9tDkYI22WY8sbi5gv2cOj4QyDvvBmVm epsZGD3/cVE8MC5fvj13c7JdBmzDI1aaK4UmkhynArPkPw2vCHmCuDY96pzTNbO8acr1zJ3o/WSN F4Azbl5KXZnJHoe0nRrA1W4TNSNe35tfPe/W93bC6j67eA0cQmdrBNj41tpvi/JEoAGrAgEDo4HF MIHCMB0GA1UdDgQWBBS/X7fRzt0fhvRbVazc1xDCDqmI5zCBkgYDVR0jBIGKMIGHgBS/X7fRzt0f hvRbVazc1xDCDqmI56FspGowaDELMAkGA1UEBhMCVVMxJTAjBgNVBAoTHFN0YXJmaWVsZCBUZWNo bm9sb2dpZXMsIEluYy4xMjAwBgNVBAsTKVN0YXJmaWVsZCBDbGFzcyAyIENlcnRpZmljYXRpb24g QXV0aG9yaXR5ggEAMAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQEFBQADggEBAAWdP4id0ckaVaGs afPzWdqbAYcaT1epoXkJKtv3L7IezMdeatiDh6GX70k1PncGQVhiv45YuApnP+yz3SFmH8lU+nLM PUxA2IGvd56Deruix/U0F47ZEUD0/CwqTRV/p2JdLiXTAAsgGh1o+Re49L2L7ShZ3U0WixeDyLJl xy16paq8U4Zt3VekyvggQQto8PT7dL5WXXp59fkdheMtlb71cZBDzI0fmgAKhynpVSJYACPq4xJD KVtHCN2MQWplBqjlIapBtJUhlbl90TSrE9atvNziPTnNvT51cKEYWQPJIrSPnNVeKtelttQKbfi3 QBFGmh95DmK/D5fs4C8fF5Q= -----END CERTIFICATE----- DigiCert Assured ID Root CA =========================== -----BEGIN CERTIFICATE----- MIIDtzCCAp+gAwIBAgIQDOfg5RfYRv6P5WD8G/AwOTANBgkqhkiG9w0BAQUFADBlMQswCQYDVQQG EwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cuZGlnaWNlcnQuY29tMSQw IgYDVQQDExtEaWdpQ2VydCBBc3N1cmVkIElEIFJvb3QgQ0EwHhcNMDYxMTEwMDAwMDAwWhcNMzEx MTEwMDAwMDAwWjBlMQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQL ExB3d3cuZGlnaWNlcnQuY29tMSQwIgYDVQQDExtEaWdpQ2VydCBBc3N1cmVkIElEIFJvb3QgQ0Ew ggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCtDhXO5EOAXLGH87dg+XESpa7cJpSIqvTO 9SA5KFhgDPiA2qkVlTJhPLWxKISKityfCgyDF3qPkKyK53lTXDGEKvYPmDI2dsze3Tyoou9q+yHy UmHfnyDXH+Kx2f4YZNISW1/5WBg1vEfNoTb5a3/UsDg+wRvDjDPZ2C8Y/igPs6eD1sNuRMBhNZYW /lmci3Zt1/GiSw0r/wty2p5g0I6QNcZ4VYcgoc/lbQrISXwxmDNsIumH0DJaoroTghHtORedmTpy oeb6pNnVFzF1roV9Iq4/AUaG9ih5yLHa5FcXxH4cDrC0kqZWs72yl+2qp/C3xag/lRbQ/6GW6whf GHdPAgMBAAGjYzBhMA4GA1UdDwEB/wQEAwIBhjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBRF 66Kv9JLLgjEtUYunpyGd823IDzAfBgNVHSMEGDAWgBRF66Kv9JLLgjEtUYunpyGd823IDzANBgkq hkiG9w0BAQUFAAOCAQEAog683+Lt8ONyc3pklL/3cmbYMuRCdWKuh+vy1dneVrOfzM4UKLkNl2Bc EkxY5NM9g0lFWJc1aRqoR+pWxnmrEthngYTffwk8lOa4JiwgvT2zKIn3X/8i4peEH+ll74fg38Fn SbNd67IJKusm7Xi+fT8r87cmNW1fiQG2SVufAQWbqz0lwcy2f8Lxb4bG+mRo64EtlOtCt/qMHt1i 8b5QZ7dsvfPxH2sMNgcWfzd8qVttevESRmCD1ycEvkvOl77DZypoEd+A5wwzZr8TDRRu838fYxAe +o0bJW1sj6W3YQGx0qMmoRBxna3iw/nDmVG3KwcIzi7mULKn+gpFL6Lw8g== -----END CERTIFICATE----- DigiCert Global Root CA ======================= -----BEGIN CERTIFICATE----- MIIDrzCCApegAwIBAgIQCDvgVpBCRrGhdWrJWZHHSjANBgkqhkiG9w0BAQUFADBhMQswCQYDVQQG EwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cuZGlnaWNlcnQuY29tMSAw HgYDVQQDExdEaWdpQ2VydCBHbG9iYWwgUm9vdCBDQTAeFw0wNjExMTAwMDAwMDBaFw0zMTExMTAw MDAwMDBaMGExCzAJBgNVBAYTAlVTMRUwEwYDVQQKEwxEaWdpQ2VydCBJbmMxGTAXBgNVBAsTEHd3 dy5kaWdpY2VydC5jb20xIDAeBgNVBAMTF0RpZ2lDZXJ0IEdsb2JhbCBSb290IENBMIIBIjANBgkq hkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA4jvhEXLeqKTTo1eqUKKPC3eQyaKl7hLOllsBCSDMAZOn TjC3U/dDxGkAV53ijSLdhwZAAIEJzs4bg7/fzTtxRuLWZscFs3YnFo97nh6Vfe63SKMI2tavegw5 BmV/Sl0fvBf4q77uKNd0f3p4mVmFaG5cIzJLv07A6Fpt43C/dxC//AH2hdmoRBBYMql1GNXRor5H 4idq9Joz+EkIYIvUX7Q6hL+hqkpMfT7PT19sdl6gSzeRntwi5m3OFBqOasv+zbMUZBfHWymeMr/y 7vrTC0LUq7dBMtoM1O/4gdW7jVg/tRvoSSiicNoxBN33shbyTApOB6jtSj1etX+jkMOvJwIDAQAB o2MwYTAOBgNVHQ8BAf8EBAMCAYYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUA95QNVbRTLtm 8KPiGxvDl7I90VUwHwYDVR0jBBgwFoAUA95QNVbRTLtm8KPiGxvDl7I90VUwDQYJKoZIhvcNAQEF BQADggEBAMucN6pIExIK+t1EnE9SsPTfrgT1eXkIoyQY/EsrhMAtudXH/vTBH1jLuG2cenTnmCmr EbXjcKChzUyImZOMkXDiqw8cvpOp/2PV5Adg06O/nVsJ8dWO41P0jmP6P6fbtGbfYmbW0W5BjfIt tep3Sp+dWOIrWcBAI+0tKIJFPnlUkiaY4IBIqDfv8NZ5YBberOgOzW6sRBc4L0na4UU+Krk2U886 UAb3LujEV0lsYSEY1QSteDwsOoBrp+uvFRTp2InBuThs4pFsiv9kuXclVzDAGySj4dzp30d8tbQk CAUw7C29C79Fv1C5qfPrmAESrciIxpg0X40KPMbp1ZWVbd4= -----END CERTIFICATE----- DigiCert High Assurance EV Root CA ================================== -----BEGIN CERTIFICATE----- MIIDxTCCAq2gAwIBAgIQAqxcJmoLQJuPC3nyrkYldzANBgkqhkiG9w0BAQUFADBsMQswCQYDVQQG EwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cuZGlnaWNlcnQuY29tMSsw KQYDVQQDEyJEaWdpQ2VydCBIaWdoIEFzc3VyYW5jZSBFViBSb290IENBMB4XDTA2MTExMDAwMDAw MFoXDTMxMTExMDAwMDAwMFowbDELMAkGA1UEBhMCVVMxFTATBgNVBAoTDERpZ2lDZXJ0IEluYzEZ MBcGA1UECxMQd3d3LmRpZ2ljZXJ0LmNvbTErMCkGA1UEAxMiRGlnaUNlcnQgSGlnaCBBc3N1cmFu Y2UgRVYgUm9vdCBDQTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAMbM5XPm+9S75S0t Mqbf5YE/yc0lSbZxKsPVlDRnogocsF9ppkCxxLeyj9CYpKlBWTrT3JTWPNt0OKRKzE0lgvdKpVMS OO7zSW1xkX5jtqumX8OkhPhPYlG++MXs2ziS4wblCJEMxChBVfvLWokVfnHoNb9Ncgk9vjo4UFt3 MRuNs8ckRZqnrG0AFFoEt7oT61EKmEFBIk5lYYeBQVCmeVyJ3hlKV9Uu5l0cUyx+mM0aBhakaHPQ NAQTXKFx01p8VdteZOE3hzBWBOURtCmAEvF5OYiiAhF8J2a3iLd48soKqDirCmTCv2ZdlYTBoSUe h10aUAsgEsxBu24LUTi4S8sCAwEAAaNjMGEwDgYDVR0PAQH/BAQDAgGGMA8GA1UdEwEB/wQFMAMB Af8wHQYDVR0OBBYEFLE+w2kD+L9HAdSYJhoIAu9jZCvDMB8GA1UdIwQYMBaAFLE+w2kD+L9HAdSY JhoIAu9jZCvDMA0GCSqGSIb3DQEBBQUAA4IBAQAcGgaX3NecnzyIZgYIVyHbIUf4KmeqvxgydkAQ V8GK83rZEWWONfqe/EW1ntlMMUu4kehDLI6zeM7b41N5cdblIZQB2lWHmiRk9opmzN6cN82oNLFp myPInngiK3BD41VHMWEZ71jFhS9OMPagMRYjyOfiZRYzy78aG6A9+MpeizGLYAiJLQwGXFK3xPkK mNEVX58Svnw2Yzi9RKR/5CYrCsSXaQ3pjOLAEFe4yHYSkVXySGnYvCoCWw9E1CAx2/S6cCZdkGCe vEsXCS+0yx5DaMkHJ8HSXPfqIbloEpw8nL+e/IBcm2PN7EeqJSdnoDfzAIJ9VNep+OkuE6N36B9K -----END CERTIFICATE----- DST Root CA X3 ============== -----BEGIN CERTIFICATE----- MIIDSjCCAjKgAwIBAgIQRK+wgNajJ7qJMDmGLvhAazANBgkqhkiG9w0BAQUFADA/MSQwIgYDVQQK ExtEaWdpdGFsIFNpZ25hdHVyZSBUcnVzdCBDby4xFzAVBgNVBAMTDkRTVCBSb290IENBIFgzMB4X DTAwMDkzMDIxMTIxOVoXDTIxMDkzMDE0MDExNVowPzEkMCIGA1UEChMbRGlnaXRhbCBTaWduYXR1 cmUgVHJ1c3QgQ28uMRcwFQYDVQQDEw5EU1QgUm9vdCBDQSBYMzCCASIwDQYJKoZIhvcNAQEBBQAD ggEPADCCAQoCggEBAN+v6ZdQCINXtMxiZfaQguzH0yxrMMpb7NnDfcdAwRgUi+DoM3ZJKuM/IUmT rE4Orz5Iy2Xu/NMhD2XSKtkyj4zl93ewEnu1lcCJo6m67XMuegwGMoOifooUMM0RoOEqOLl5CjH9 UL2AZd+3UWODyOKIYepLYYHsUmu5ouJLGiifSKOeDNoJjj4XLh7dIN9bxiqKqy69cK3FCxolkHRy xXtqqzTWMIn/5WgTe1QLyNau7Fqckh49ZLOMxt+/yUFw7BZy1SbsOFU5Q9D8/RhcQPGX69Wam40d utolucbY38EVAjqr2m7xPi71XAicPNaDaeQQmxkqtilX4+U9m5/wAl0CAwEAAaNCMEAwDwYDVR0T AQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYwHQYDVR0OBBYEFMSnsaR7LHH62+FLkHX/xBVghYkQ MA0GCSqGSIb3DQEBBQUAA4IBAQCjGiybFwBcqR7uKGY3Or+Dxz9LwwmglSBd49lZRNI+DT69ikug dB/OEIKcdBodfpga3csTS7MgROSR6cz8faXbauX+5v3gTt23ADq1cEmv8uXrAvHRAosZy5Q6XkjE GB5YGV8eAlrwDPGxrancWYaLbumR9YbK+rlmM6pZW87ipxZzR8srzJmwN0jP41ZL9c8PDHIyh8bw RLtTcm1D9SZImlJnt1ir/md2cXjbDaJWFBM5JDGFoqgCWjBH4d1QB7wCCZAA62RjYJsWvIjJEubS fZGL+T0yjWW06XyxV3bqxbYoOb8VZRzI9neWagqNdwvYkQsEjgfbKbYK7p2CNTUQ -----END CERTIFICATE----- SwissSign Gold CA - G2 ====================== -----BEGIN CERTIFICATE----- MIIFujCCA6KgAwIBAgIJALtAHEP1Xk+wMA0GCSqGSIb3DQEBBQUAMEUxCzAJBgNVBAYTAkNIMRUw EwYDVQQKEwxTd2lzc1NpZ24gQUcxHzAdBgNVBAMTFlN3aXNzU2lnbiBHb2xkIENBIC0gRzIwHhcN MDYxMDI1MDgzMDM1WhcNMzYxMDI1MDgzMDM1WjBFMQswCQYDVQQGEwJDSDEVMBMGA1UEChMMU3dp c3NTaWduIEFHMR8wHQYDVQQDExZTd2lzc1NpZ24gR29sZCBDQSAtIEcyMIICIjANBgkqhkiG9w0B AQEFAAOCAg8AMIICCgKCAgEAr+TufoskDhJuqVAtFkQ7kpJcyrhdhJJCEyq8ZVeCQD5XJM1QiyUq t2/876LQwB8CJEoTlo8jE+YoWACjR8cGp4QjK7u9lit/VcyLwVcfDmJlD909Vopz2q5+bbqBHH5C jCA12UNNhPqE21Is8w4ndwtrvxEvcnifLtg+5hg3Wipy+dpikJKVyh+c6bM8K8vzARO/Ws/BtQpg vd21mWRTuKCWs2/iJneRjOBiEAKfNA+k1ZIzUd6+jbqEemA8atufK+ze3gE/bk3lUIbLtK/tREDF ylqM2tIrfKjuvqblCqoOpd8FUrdVxyJdMmqXl2MT28nbeTZ7hTpKxVKJ+STnnXepgv9VHKVxaSvR AiTysybUa9oEVeXBCsdtMDeQKuSeFDNeFhdVxVu1yzSJkvGdJo+hB9TGsnhQ2wwMC3wLjEHXuend jIj3o02yMszYF9rNt85mndT9Xv+9lz4pded+p2JYryU0pUHHPbwNUMoDAw8IWh+Vc3hiv69yFGkO peUDDniOJihC8AcLYiAQZzlG+qkDzAQ4embvIIO1jEpWjpEA/I5cgt6IoMPiaG59je883WX0XaxR 7ySArqpWl2/5rX3aYT+YdzylkbYcjCbaZaIJbcHiVOO5ykxMgI93e2CaHt+28kgeDrpOVG2Y4OGi GqJ3UM/EY5LsRxmd6+ZrzsECAwEAAaOBrDCBqTAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUw AwEB/zAdBgNVHQ4EFgQUWyV7lqRlUX64OfPAeGZe6Drn8O4wHwYDVR0jBBgwFoAUWyV7lqRlUX64 OfPAeGZe6Drn8O4wRgYDVR0gBD8wPTA7BglghXQBWQECAQEwLjAsBggrBgEFBQcCARYgaHR0cDov L3JlcG9zaXRvcnkuc3dpc3NzaWduLmNvbS8wDQYJKoZIhvcNAQEFBQADggIBACe645R88a7A3hfm 5djV9VSwg/S7zV4Fe0+fdWavPOhWfvxyeDgD2StiGwC5+OlgzczOUYrHUDFu4Up+GC9pWbY9ZIEr 44OE5iKHjn3g7gKZYbge9LgriBIWhMIxkziWMaa5O1M/wySTVltpkuzFwbs4AOPsF6m43Md8AYOf Mke6UiI0HTJ6CVanfCU2qT1L2sCCbwq7EsiHSycR+R4tx5M/nttfJmtS2S6K8RTGRI0Vqbe/vd6m Gu6uLftIdxf+u+yvGPUqUfA5hJeVbG4bwyvEdGB5JbAKJ9/fXtI5z0V9QkvfsywexcZdylU6oJxp mo/a77KwPJ+HbBIrZXAVUjEaJM9vMSNQH4xPjyPDdEFjHFWoFN0+4FFQz/EbMFYOkrCChdiDyyJk vC24JdVUorgG6q2SpCSgwYa1ShNqR88uC1aVVMvOmttqtKay20EIhid392qgQmwLOM7XdVAyksLf KzAiSNDVQTglXaTpXZ/GlHXQRf0wl0OPkKsKx4ZzYEppLd6leNcG2mqeSz53OiATIgHQv2ieY2Br NU0LbbqhPcCT4H8js1WtciVORvnSFu+wZMEBnunKoGqYDs/YYPIvSbjkQuE4NRb0yG5P94FW6Lqj viOvrv1vA+ACOzB2+httQc8Bsem4yWb02ybzOqR08kkkW8mw0FfB+j564ZfJ -----END CERTIFICATE----- SwissSign Silver CA - G2 ======================== -----BEGIN CERTIFICATE----- MIIFvTCCA6WgAwIBAgIITxvUL1S7L0swDQYJKoZIhvcNAQEFBQAwRzELMAkGA1UEBhMCQ0gxFTAT BgNVBAoTDFN3aXNzU2lnbiBBRzEhMB8GA1UEAxMYU3dpc3NTaWduIFNpbHZlciBDQSAtIEcyMB4X DTA2MTAyNTA4MzI0NloXDTM2MTAyNTA4MzI0NlowRzELMAkGA1UEBhMCQ0gxFTATBgNVBAoTDFN3 aXNzU2lnbiBBRzEhMB8GA1UEAxMYU3dpc3NTaWduIFNpbHZlciBDQSAtIEcyMIICIjANBgkqhkiG 9w0BAQEFAAOCAg8AMIICCgKCAgEAxPGHf9N4Mfc4yfjDmUO8x/e8N+dOcbpLj6VzHVxumK4DV644 N0MvFz0fyM5oEMF4rhkDKxD6LHmD9ui5aLlV8gREpzn5/ASLHvGiTSf5YXu6t+WiE7brYT7QbNHm +/pe7R20nqA1W6GSy/BJkv6FCgU+5tkL4k+73JU3/JHpMjUi0R86TieFnbAVlDLaYQ1HTWBCrpJH 6INaUFjpiou5XaHc3ZlKHzZnu0jkg7Y360g6rw9njxcH6ATK72oxh9TAtvmUcXtnZLi2kUpCe2Uu MGoM9ZDulebyzYLs2aFK7PayS+VFheZteJMELpyCbTapxDFkH4aDCyr0NQp4yVXPQbBH6TCfmb5h qAaEuSh6XzjZG6k4sIN/c8HDO0gqgg8hm7jMqDXDhBuDsz6+pJVpATqJAHgE2cn0mRmrVn5bi4Y5 FZGkECwJMoBgs5PAKrYYC51+jUnyEEp/+dVGLxmSo5mnJqy7jDzmDrxHB9xzUfFwZC8I+bRHHTBs ROopN4WSaGa8gzj+ezku01DwH/teYLappvonQfGbGHLy9YR0SslnxFSuSGTfjNFusB3hB48IHpmc celM2KX3RxIfdNFRnobzwqIjQAtz20um53MGjMGg6cFZrEb65i/4z3GcRm25xBWNOHkDRUjvxF3X CO6HOSKGsg0PWEP3calILv3q1h8CAwEAAaOBrDCBqTAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/ BAUwAwEB/zAdBgNVHQ4EFgQUF6DNweRBtjpbO8tFnb0cwpj6hlgwHwYDVR0jBBgwFoAUF6DNweRB tjpbO8tFnb0cwpj6hlgwRgYDVR0gBD8wPTA7BglghXQBWQEDAQEwLjAsBggrBgEFBQcCARYgaHR0 cDovL3JlcG9zaXRvcnkuc3dpc3NzaWduLmNvbS8wDQYJKoZIhvcNAQEFBQADggIBAHPGgeAn0i0P 4JUw4ppBf1AsX19iYamGamkYDHRJ1l2E6kFSGG9YrVBWIGrGvShpWJHckRE1qTodvBqlYJ7YH39F kWnZfrt4csEGDyrOj4VwYaygzQu4OSlWhDJOhrs9xCrZ1x9y7v5RoSJBsXECYxqCsGKrXlcSH9/L 3XWgwF15kIwb4FDm3jH+mHtwX6WQ2K34ArZv02DdQEsixT2tOnqfGhpHkXkzuoLcMmkDlm4fS/Bx /uNncqCxv1yL5PqZIseEuRuNI5c/7SXgz2W79WEE790eslpBIlqhn10s6FvJbakMDHiqYMZWjwFa DGi8aRl5xB9+lwW/xekkUV7U1UtT7dkjWjYDZaPBA61BMPNGG4WQr2W11bHkFlt4dR2Xem1ZqSqP e97Dh4kQmUlzeMg9vVE1dCrV8X5pGyq7O70luJpaPXJhkGaH7gzWTdQRdAtq/gsD/KNVV4n+Ssuu WxcFyPKNIzFTONItaj+CuY0IavdeQXRuwxF+B6wpYJE/OMpXEA29MC/HpeZBoNquBYeaoKRlbEwJ DIm6uNO5wJOKMPqN5ZprFQFOZ6raYlY+hAhm0sQ2fac+EPyI4NSA5QC9qvNOBqN6avlicuMJT+ub DgEj8Z+7fNzcbBGXJbLytGMU0gYqZ4yD9c7qB9iaah7s5Aq7KkzrCWA5zspi2C5u -----END CERTIFICATE----- SecureTrust CA ============== -----BEGIN CERTIFICATE----- MIIDuDCCAqCgAwIBAgIQDPCOXAgWpa1Cf/DrJxhZ0DANBgkqhkiG9w0BAQUFADBIMQswCQYDVQQG EwJVUzEgMB4GA1UEChMXU2VjdXJlVHJ1c3QgQ29ycG9yYXRpb24xFzAVBgNVBAMTDlNlY3VyZVRy dXN0IENBMB4XDTA2MTEwNzE5MzExOFoXDTI5MTIzMTE5NDA1NVowSDELMAkGA1UEBhMCVVMxIDAe BgNVBAoTF1NlY3VyZVRydXN0IENvcnBvcmF0aW9uMRcwFQYDVQQDEw5TZWN1cmVUcnVzdCBDQTCC ASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAKukgeWVzfX2FI7CT8rU4niVWJxB4Q2ZQCQX OZEzZum+4YOvYlyJ0fwkW2Gz4BERQRwdbvC4u/jep4G6pkjGnx29vo6pQT64lO0pGtSO0gMdA+9t DWccV9cGrcrI9f4Or2YlSASWC12juhbDCE/RRvgUXPLIXgGZbf2IzIaowW8xQmxSPmjL8xk037uH GFaAJsTQ3MBv396gwpEWoGQRS0S8Hvbn+mPeZqx2pHGj7DaUaHp3pLHnDi+BeuK1cobvomuL8A/b 01k/unK8RCSc43Oz969XL0Imnal0ugBS8kvNU3xHCzaFDmapCJcWNFfBZveA4+1wVMeT4C4oFVmH ursCAwEAAaOBnTCBmjATBgkrBgEEAYI3FAIEBh4EAEMAQTALBgNVHQ8EBAMCAYYwDwYDVR0TAQH/ BAUwAwEB/zAdBgNVHQ4EFgQUQjK2FvoE/f5dS3rD/fdMQB1aQ68wNAYDVR0fBC0wKzApoCegJYYj aHR0cDovL2NybC5zZWN1cmV0cnVzdC5jb20vU1RDQS5jcmwwEAYJKwYBBAGCNxUBBAMCAQAwDQYJ KoZIhvcNAQEFBQADggEBADDtT0rhWDpSclu1pqNlGKa7UTt36Z3q059c4EVlew3KW+JwULKUBRSu SceNQQcSc5R+DCMh/bwQf2AQWnL1mA6s7Ll/3XpvXdMc9P+IBWlCqQVxyLesJugutIxq/3HcuLHf mbx8IVQr5Fiiu1cprp6poxkmD5kuCLDv/WnPmRoJjeOnnyvJNjR7JLN4TJUXpAYmHrZkUjZfYGfZ nMUFdAvnZyPSCPyI6a6Lf+Ew9Dd+/cYy2i2eRDAwbO4H3tI0/NL/QPZL9GZGBlSm8jIKYyYwa5vR 3ItHuuG51WLQoqD0ZwV4KWMabwTW+MZMo5qxN7SN5ShLHZ4swrhovO0C7jE= -----END CERTIFICATE----- Secure Global CA ================ -----BEGIN CERTIFICATE----- MIIDvDCCAqSgAwIBAgIQB1YipOjUiolN9BPI8PjqpTANBgkqhkiG9w0BAQUFADBKMQswCQYDVQQG EwJVUzEgMB4GA1UEChMXU2VjdXJlVHJ1c3QgQ29ycG9yYXRpb24xGTAXBgNVBAMTEFNlY3VyZSBH bG9iYWwgQ0EwHhcNMDYxMTA3MTk0MjI4WhcNMjkxMjMxMTk1MjA2WjBKMQswCQYDVQQGEwJVUzEg MB4GA1UEChMXU2VjdXJlVHJ1c3QgQ29ycG9yYXRpb24xGTAXBgNVBAMTEFNlY3VyZSBHbG9iYWwg Q0EwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCvNS7YrGxVaQZx5RNoJLNP2MwhR/jx YDiJiQPpvepeRlMJ3Fz1Wuj3RSoC6zFh1ykzTM7HfAo3fg+6MpjhHZevj8fcyTiW89sa/FHtaMbQ bqR8JNGuQsiWUGMu4P51/pinX0kuleM5M2SOHqRfkNJnPLLZ/kG5VacJjnIFHovdRIWCQtBJwB1g 8NEXLJXr9qXBkqPFwqcIYA1gBBCWeZ4WNOaptvolRTnIHmX5k/Wq8VLcmZg9pYYaDDUz+kulBAYV HDGA76oYa8J719rO+TMg1fW9ajMtgQT7sFzUnKPiXB3jqUJ1XnvUd+85VLrJChgbEplJL4hL/VBi 0XPnj3pDAgMBAAGjgZ0wgZowEwYJKwYBBAGCNxQCBAYeBABDAEEwCwYDVR0PBAQDAgGGMA8GA1Ud EwEB/wQFMAMBAf8wHQYDVR0OBBYEFK9EBMJBfkiD2045AuzshHrmzsmkMDQGA1UdHwQtMCswKaAn oCWGI2h0dHA6Ly9jcmwuc2VjdXJldHJ1c3QuY29tL1NHQ0EuY3JsMBAGCSsGAQQBgjcVAQQDAgEA MA0GCSqGSIb3DQEBBQUAA4IBAQBjGghAfaReUw132HquHw0LURYD7xh8yOOvaliTFGCRsoTciE6+ OYo68+aCiV0BN7OrJKQVDpI1WkpEXk5X+nXOH0jOZvQ8QCaSmGwb7iRGDBezUqXbpZGRzzfTb+cn CDpOGR86p1hcF895P4vkp9MmI50mD1hp/Ed+stCNi5O/KU9DaXR2Z0vPB4zmAve14bRDtUstFJ/5 3CYNv6ZHdAbYiNE6KTCEztI5gGIbqMdXSbxqVVFnFUq+NQfk1XWYN3kwFNspnWzFacxHVaIw98xc f8LDmBxrThaA63p4ZUWiABqvDA1VZDRIuJK58bRQKfJPIx/abKwfROHdI3hRW8cW -----END CERTIFICATE----- COMODO Certification Authority ============================== -----BEGIN CERTIFICATE----- MIIEHTCCAwWgAwIBAgIQToEtioJl4AsC7j41AkblPTANBgkqhkiG9w0BAQUFADCBgTELMAkGA1UE BhMCR0IxGzAZBgNVBAgTEkdyZWF0ZXIgTWFuY2hlc3RlcjEQMA4GA1UEBxMHU2FsZm9yZDEaMBgG A1UEChMRQ09NT0RPIENBIExpbWl0ZWQxJzAlBgNVBAMTHkNPTU9ETyBDZXJ0aWZpY2F0aW9uIEF1 dGhvcml0eTAeFw0wNjEyMDEwMDAwMDBaFw0yOTEyMzEyMzU5NTlaMIGBMQswCQYDVQQGEwJHQjEb MBkGA1UECBMSR3JlYXRlciBNYW5jaGVzdGVyMRAwDgYDVQQHEwdTYWxmb3JkMRowGAYDVQQKExFD T01PRE8gQ0EgTGltaXRlZDEnMCUGA1UEAxMeQ09NT0RPIENlcnRpZmljYXRpb24gQXV0aG9yaXR5 MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA0ECLi3LjkRv3UcEbVASY06m/weaKXTuH +7uIzg3jLz8GlvCiKVCZrts7oVewdFFxze1CkU1B/qnI2GqGd0S7WWaXUF601CxwRM/aN5VCaTww xHGzUvAhTaHYujl8HJ6jJJ3ygxaYqhZ8Q5sVW7euNJH+1GImGEaaP+vB+fGQV+useg2L23IwambV 4EajcNxo2f8ESIl33rXp+2dtQem8Ob0y2WIC8bGoPW43nOIv4tOiJovGuFVDiOEjPqXSJDlqR6sA 1KGzqSX+DT+nHbrTUcELpNqsOO9VUCQFZUaTNE8tja3G1CEZ0o7KBWFxB3NH5YoZEr0ETc5OnKVI rLsm9wIDAQABo4GOMIGLMB0GA1UdDgQWBBQLWOWLxkwVN6RAqTCpIb5HNlpW/zAOBgNVHQ8BAf8E BAMCAQYwDwYDVR0TAQH/BAUwAwEB/zBJBgNVHR8EQjBAMD6gPKA6hjhodHRwOi8vY3JsLmNvbW9k b2NhLmNvbS9DT01PRE9DZXJ0aWZpY2F0aW9uQXV0aG9yaXR5LmNybDANBgkqhkiG9w0BAQUFAAOC AQEAPpiem/Yb6dc5t3iuHXIYSdOH5EOC6z/JqvWote9VfCFSZfnVDeFs9D6Mk3ORLgLETgdxb8CP OGEIqB6BCsAvIC9Bi5HcSEW88cbeunZrM8gALTFGTO3nnc+IlP8zwFboJIYmuNg4ON8qa90SzMc/ RxdMosIGlgnW2/4/PEZB31jiVg88O8EckzXZOFKs7sjsLjBOlDW0JB9LeGna8gI4zJVSk/BwJVmc IGfE7vmLV2H0knZ9P4SNVbfo5azV8fUZVqZa+5Acr5Pr5RzUZ5ddBA6+C4OmF4O5MBKgxTMVBbkN +8cFduPYSo38NBejxiEovjBFMR7HeL5YYTisO+IBZQ== -----END CERTIFICATE----- Network Solutions Certificate Authority ======================================= -----BEGIN CERTIFICATE----- MIID5jCCAs6gAwIBAgIQV8szb8JcFuZHFhfjkDFo4DANBgkqhkiG9w0BAQUFADBiMQswCQYDVQQG EwJVUzEhMB8GA1UEChMYTmV0d29yayBTb2x1dGlvbnMgTC5MLkMuMTAwLgYDVQQDEydOZXR3b3Jr IFNvbHV0aW9ucyBDZXJ0aWZpY2F0ZSBBdXRob3JpdHkwHhcNMDYxMjAxMDAwMDAwWhcNMjkxMjMx MjM1OTU5WjBiMQswCQYDVQQGEwJVUzEhMB8GA1UEChMYTmV0d29yayBTb2x1dGlvbnMgTC5MLkMu MTAwLgYDVQQDEydOZXR3b3JrIFNvbHV0aW9ucyBDZXJ0aWZpY2F0ZSBBdXRob3JpdHkwggEiMA0G CSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDkvH6SMG3G2I4rC7xGzuAnlt7e+foS0zwzc7MEL7xx jOWftiJgPl9dzgn/ggwbmlFQGiaJ3dVhXRncEg8tCqJDXRfQNJIg6nPPOCwGJgl6cvf6UDL4wpPT aaIjzkGxzOTVHzbRijr4jGPiFFlp7Q3Tf2vouAPlT2rlmGNpSAW+Lv8ztumXWWn4Zxmuk2GWRBXT crA/vGp97Eh/jcOrqnErU2lBUzS1sLnFBgrEsEX1QV1uiUV7PTsmjHTC5dLRfbIR1PtYMiKagMnc /Qzpf14Dl847ABSHJ3A4qY5usyd2mFHgBeMhqxrVhSI8KbWaFsWAqPS7azCPL0YCorEMIuDTAgMB AAGjgZcwgZQwHQYDVR0OBBYEFCEwyfsA106Y2oeqKtCnLrFAMadMMA4GA1UdDwEB/wQEAwIBBjAP BgNVHRMBAf8EBTADAQH/MFIGA1UdHwRLMEkwR6BFoEOGQWh0dHA6Ly9jcmwubmV0c29sc3NsLmNv bS9OZXR3b3JrU29sdXRpb25zQ2VydGlmaWNhdGVBdXRob3JpdHkuY3JsMA0GCSqGSIb3DQEBBQUA A4IBAQC7rkvnt1frf6ott3NHhWrB5KUd5Oc86fRZZXe1eltajSU24HqXLjjAV2CDmAaDn7l2em5Q 4LqILPxFzBiwmZVRDuwduIj/h1AcgsLj4DKAv6ALR8jDMe+ZZzKATxcheQxpXN5eNK4CtSbqUN9/ GGUsyfJj4akH/nxxH2szJGoeBfcFaMBqEssuXmHLrijTfsK0ZpEmXzwuJF/LWA/rKOyvEZbz3Htv wKeI8lN3s2Berq4o2jUsbzRF0ybh3uxbTydrFny9RAQYgrOJeRcQcT16ohZO9QHNpGxlaKFJdlxD ydi8NmdspZS11My5vWo1ViHe2MPr+8ukYEywVaCge1ey -----END CERTIFICATE----- COMODO ECC Certification Authority ================================== -----BEGIN CERTIFICATE----- MIICiTCCAg+gAwIBAgIQH0evqmIAcFBUTAGem2OZKjAKBggqhkjOPQQDAzCBhTELMAkGA1UEBhMC R0IxGzAZBgNVBAgTEkdyZWF0ZXIgTWFuY2hlc3RlcjEQMA4GA1UEBxMHU2FsZm9yZDEaMBgGA1UE ChMRQ09NT0RPIENBIExpbWl0ZWQxKzApBgNVBAMTIkNPTU9ETyBFQ0MgQ2VydGlmaWNhdGlvbiBB dXRob3JpdHkwHhcNMDgwMzA2MDAwMDAwWhcNMzgwMTE4MjM1OTU5WjCBhTELMAkGA1UEBhMCR0Ix GzAZBgNVBAgTEkdyZWF0ZXIgTWFuY2hlc3RlcjEQMA4GA1UEBxMHU2FsZm9yZDEaMBgGA1UEChMR Q09NT0RPIENBIExpbWl0ZWQxKzApBgNVBAMTIkNPTU9ETyBFQ0MgQ2VydGlmaWNhdGlvbiBBdXRo b3JpdHkwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAAQDR3svdcmCFYX7deSRFtSrYpn1PlILBs5BAH+X 4QokPB0BBO490o0JlwzgdeT6+3eKKvUDYEs2ixYjFq0JcfRK9ChQtP6IHG4/bC8vCVlbpVsLM5ni wz2J+Wos77LTBumjQjBAMB0GA1UdDgQWBBR1cacZSBm8nZ3qQUfflMRId5nTeTAOBgNVHQ8BAf8E BAMCAQYwDwYDVR0TAQH/BAUwAwEB/zAKBggqhkjOPQQDAwNoADBlAjEA7wNbeqy3eApyt4jf/7VG FAkK+qDmfQjGGoe9GKhzvSbKYAydzpmfz1wPMOG+FDHqAjAU9JM8SaczepBGR7NjfRObTrdvGDeA U/7dIOA1mjbRxwG55tzd8/8dLDoWV9mSOdY= -----END CERTIFICATE----- Certigna ======== -----BEGIN CERTIFICATE----- MIIDqDCCApCgAwIBAgIJAP7c4wEPyUj/MA0GCSqGSIb3DQEBBQUAMDQxCzAJBgNVBAYTAkZSMRIw EAYDVQQKDAlEaGlteW90aXMxETAPBgNVBAMMCENlcnRpZ25hMB4XDTA3MDYyOTE1MTMwNVoXDTI3 MDYyOTE1MTMwNVowNDELMAkGA1UEBhMCRlIxEjAQBgNVBAoMCURoaW15b3RpczERMA8GA1UEAwwI Q2VydGlnbmEwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDIaPHJ1tazNHUmgh7stL7q XOEm7RFHYeGifBZ4QCHkYJ5ayGPhxLGWkv8YbWkj4Sti993iNi+RB7lIzw7sebYs5zRLcAglozyH GxnygQcPOJAZ0xH+hrTy0V4eHpbNgGzOOzGTtvKg0KmVEn2lmsxryIRWijOp5yIVUxbwzBfsV1/p ogqYCd7jX5xv3EjjhQsVWqa6n6xI4wmy9/Qy3l40vhx4XUJbzg4ij02Q130yGLMLLGq/jj8UEYkg DncUtT2UCIf3JR7VsmAA7G8qKCVuKj4YYxclPz5EIBb2JsglrgVKtOdjLPOMFlN+XPsRGgjBRmKf Irjxwo1p3Po6WAbfAgMBAAGjgbwwgbkwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUGu3+QTmQ tCRZvgHyUtVF9lo53BEwZAYDVR0jBF0wW4AUGu3+QTmQtCRZvgHyUtVF9lo53BGhOKQ2MDQxCzAJ BgNVBAYTAkZSMRIwEAYDVQQKDAlEaGlteW90aXMxETAPBgNVBAMMCENlcnRpZ25hggkA/tzjAQ/J SP8wDgYDVR0PAQH/BAQDAgEGMBEGCWCGSAGG+EIBAQQEAwIABzANBgkqhkiG9w0BAQUFAAOCAQEA hQMeknH2Qq/ho2Ge6/PAD/Kl1NqV5ta+aDY9fm4fTIrv0Q8hbV6lUmPOEvjvKtpv6zf+EwLHyzs+ ImvaYS5/1HI93TDhHkxAGYwP15zRgzB7mFncfca5DClMoTOi62c6ZYTTluLtdkVwj7Ur3vkj1klu PBS1xp81HlDQwY9qcEQCYsuuHWhBp6pX6FOqB9IG9tUUBguRA3UsbHK1YZWaDYu5Def131TN3ubY 1gkIl2PlwS6wt0QmwCbAr1UwnjvVNioZBPRcHv/PLLf/0P2HQBHVESO7SMAhqaQoLf0V+LBOK/Qw WyH8EZE0vkHve52Xdf+XlcCWWC/qu0bXu+TZLg== -----END CERTIFICATE----- Cybertrust Global Root ====================== -----BEGIN CERTIFICATE----- MIIDoTCCAomgAwIBAgILBAAAAAABD4WqLUgwDQYJKoZIhvcNAQEFBQAwOzEYMBYGA1UEChMPQ3li ZXJ0cnVzdCwgSW5jMR8wHQYDVQQDExZDeWJlcnRydXN0IEdsb2JhbCBSb290MB4XDTA2MTIxNTA4 MDAwMFoXDTIxMTIxNTA4MDAwMFowOzEYMBYGA1UEChMPQ3liZXJ0cnVzdCwgSW5jMR8wHQYDVQQD ExZDeWJlcnRydXN0IEdsb2JhbCBSb290MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA +Mi8vRRQZhP/8NN57CPytxrHjoXxEnOmGaoQ25yiZXRadz5RfVb23CO21O1fWLE3TdVJDm71aofW 0ozSJ8bi/zafmGWgE07GKmSb1ZASzxQG9Dvj1Ci+6A74q05IlG2OlTEQXO2iLb3VOm2yHLtgwEZL AfVJrn5GitB0jaEMAs7u/OePuGtm839EAL9mJRQr3RAwHQeWP032a7iPt3sMpTjr3kfb1V05/Iin 89cqdPHoWqI7n1C6poxFNcJQZZXcY4Lv3b93TZxiyWNzFtApD0mpSPCzqrdsxacwOUBdrsTiXSZT 8M4cIwhhqJQZugRiQOwfOHB3EgZxpzAYXSUnpQIDAQABo4GlMIGiMA4GA1UdDwEB/wQEAwIBBjAP BgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBS2CHsNesysIEyGVjJez6tuhS1wVzA/BgNVHR8EODA2 MDSgMqAwhi5odHRwOi8vd3d3Mi5wdWJsaWMtdHJ1c3QuY29tL2NybC9jdC9jdHJvb3QuY3JsMB8G A1UdIwQYMBaAFLYIew16zKwgTIZWMl7Pq26FLXBXMA0GCSqGSIb3DQEBBQUAA4IBAQBW7wojoFRO lZfJ+InaRcHUowAl9B8Tq7ejhVhpwjCt2BWKLePJzYFa+HMjWqd8BfP9IjsO0QbE2zZMcwSO5bAi 5MXzLqXZI+O4Tkogp24CJJ8iYGd7ix1yCcUxXOl5n4BHPa2hCwcUPUf/A2kaDAtE52Mlp3+yybh2 hO0j9n0Hq0V+09+zv+mKts2oomcrUtW3ZfA5TGOgkXmTUg9U3YO7n9GPp1Nzw8v/MOx8BLjYRB+T X3EJIrduPuocA06dGiBh+4E37F78CkWr1+cXVdCg6mCbpvbjjFspwgZgFJ0tl0ypkxWdYcQBX0jW WL1WMRJOEcgh4LMRkWXbtKaIOM5V -----END CERTIFICATE----- ePKI Root Certification Authority ================================= -----BEGIN CERTIFICATE----- MIIFsDCCA5igAwIBAgIQFci9ZUdcr7iXAF7kBtK8nTANBgkqhkiG9w0BAQUFADBeMQswCQYDVQQG EwJUVzEjMCEGA1UECgwaQ2h1bmdod2EgVGVsZWNvbSBDby4sIEx0ZC4xKjAoBgNVBAsMIWVQS0kg Um9vdCBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTAeFw0wNDEyMjAwMjMxMjdaFw0zNDEyMjAwMjMx MjdaMF4xCzAJBgNVBAYTAlRXMSMwIQYDVQQKDBpDaHVuZ2h3YSBUZWxlY29tIENvLiwgTHRkLjEq MCgGA1UECwwhZVBLSSBSb290IENlcnRpZmljYXRpb24gQXV0aG9yaXR5MIICIjANBgkqhkiG9w0B AQEFAAOCAg8AMIICCgKCAgEA4SUP7o3biDN1Z82tH306Tm2d0y8U82N0ywEhajfqhFAHSyZbCUNs IZ5qyNUD9WBpj8zwIuQf5/dqIjG3LBXy4P4AakP/h2XGtRrBp0xtInAhijHyl3SJCRImHJ7K2RKi lTza6We/CKBk49ZCt0Xvl/T29de1ShUCWH2YWEtgvM3XDZoTM1PRYfl61dd4s5oz9wCGzh1NlDiv qOx4UXCKXBCDUSH3ET00hl7lSM2XgYI1TBnsZfZrxQWh7kcT1rMhJ5QQCtkkO7q+RBNGMD+XPNjX 12ruOzjjK9SXDrkb5wdJfzcq+Xd4z1TtW0ado4AOkUPB1ltfFLqfpo0kR0BZv3I4sjZsN/+Z0V0O WQqraffAsgRFelQArr5T9rXn4fg8ozHSqf4hUmTFpmfwdQcGlBSBVcYn5AGPF8Fqcde+S/uUWH1+ ETOxQvdibBjWzwloPn9s9h6PYq2lY9sJpx8iQkEeb5mKPtf5P0B6ebClAZLSnT0IFaUQAS2zMnao lQ2zepr7BxB4EW/hj8e6DyUadCrlHJhBmd8hh+iVBmoKs2pHdmX2Os+PYhcZewoozRrSgx4hxyy/ vv9haLdnG7t4TY3OZ+XkwY63I2binZB1NJipNiuKmpS5nezMirH4JYlcWrYvjB9teSSnUmjDhDXi Zo1jDiVN1Rmy5nk3pyKdVDECAwEAAaNqMGgwHQYDVR0OBBYEFB4M97Zn8uGSJglFwFU5Lnc/Qkqi MAwGA1UdEwQFMAMBAf8wOQYEZyoHAAQxMC8wLQIBADAJBgUrDgMCGgUAMAcGBWcqAwAABBRFsMLH ClZ87lt4DJX5GFPBphzYEDANBgkqhkiG9w0BAQUFAAOCAgEACbODU1kBPpVJufGBuvl2ICO1J2B0 1GqZNF5sAFPZn/KmsSQHRGoqxqWOeBLoR9lYGxMqXnmbnwoqZ6YlPwZpVnPDimZI+ymBV3QGypzq KOg4ZyYr8dW1P2WT+DZdjo2NQCCHGervJ8A9tDkPJXtoUHRVnAxZfVo9QZQlUgjgRywVMRnVvwdV xrsStZf0X4OFunHB2WyBEXYKCrC/gpf36j36+uwtqSiUO1bd0lEursC9CBWMd1I0ltabrNMdjmEP NXubrjlpC2JgQCA2j6/7Nu4tCEoduL+bXPjqpRugc6bY+G7gMwRfaKonh+3ZwZCc7b3jajWvY9+r GNm65ulK6lCKD2GTHuItGeIwlDWSXQ62B68ZgI9HkFFLLk3dheLSClIKF5r8GrBQAuUBo2M3IUxE xJtRmREOc5wGj1QupyheRDmHVi03vYVElOEMSyycw5KFNGHLD7ibSkNS/jQ6fbjpKdx2qcgw+BRx gMYeNkh0IkFch4LoGHGLQYlE535YW6i4jRPpp2zDR+2zGp1iro2C6pSe3VkQw63d4k3jMdXH7Ojy sP6SHhYKGvzZ8/gntsm+HbRsZJB/9OTEW9c3rkIO3aQab3yIVMUWbuF6aC74Or8NpDyJO3inTmOD BCEIZ43ygknQW/2xzQ+DhNQ+IIX3Sj0rnP0qCglN6oH4EZw= -----END CERTIFICATE----- certSIGN ROOT CA ================ -----BEGIN CERTIFICATE----- MIIDODCCAiCgAwIBAgIGIAYFFnACMA0GCSqGSIb3DQEBBQUAMDsxCzAJBgNVBAYTAlJPMREwDwYD VQQKEwhjZXJ0U0lHTjEZMBcGA1UECxMQY2VydFNJR04gUk9PVCBDQTAeFw0wNjA3MDQxNzIwMDRa Fw0zMTA3MDQxNzIwMDRaMDsxCzAJBgNVBAYTAlJPMREwDwYDVQQKEwhjZXJ0U0lHTjEZMBcGA1UE CxMQY2VydFNJR04gUk9PVCBDQTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALczuX7I JUqOtdu0KBuqV5Do0SLTZLrTk+jUrIZhQGpgV2hUhE28alQCBf/fm5oqrl0Hj0rDKH/v+yv6efHH rfAQUySQi2bJqIirr1qjAOm+ukbuW3N7LBeCgV5iLKECZbO9xSsAfsT8AzNXDe3i+s5dRdY4zTW2 ssHQnIFKquSyAVwdj1+ZxLGt24gh65AIgoDzMKND5pCCrlUoSe1b16kQOA7+j0xbm0bqQfWwCHTD 0IgztnzXdN/chNFDDnU5oSVAKOp4yw4sLjmdjItuFhwvJoIQ4uNllAoEwF73XVv4EOLQunpL+943 AAAaWyjj0pxzPjKHmKHJUS/X3qwzs08CAwEAAaNCMEAwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8B Af8EBAMCAcYwHQYDVR0OBBYEFOCMm9slSbPxfIbWskKHC9BroNnkMA0GCSqGSIb3DQEBBQUAA4IB AQA+0hyJLjX8+HXd5n9liPRyTMks1zJO890ZeUe9jjtbkw9QSSQTaxQGcu8J06Gh40CEyecYMnQ8 SG4Pn0vU9x7Tk4ZkVJdjclDVVc/6IJMCopvDI5NOFlV2oHB5bc0hH88vLbwZ44gx+FkagQnIl6Z0 x2DEW8xXjrJ1/RsCCdtZb3KTafcxQdaIOL+Hsr0Wefmq5L6IJd1hJyMctTEHBDa0GpC9oHRxUIlt vBTjD4au8as+x6AJzKNI0eDbZOeStc+vckNwi/nDhDwTqn6Sm1dTk/pwwpEOMfmbZ13pljheX7Nz TogVZ96edhBiIL5VaZVDADlN9u6wWk5JRFRYX0KD -----END CERTIFICATE----- GeoTrust Primary Certification Authority - G2 ============================================= -----BEGIN CERTIFICATE----- MIICrjCCAjWgAwIBAgIQPLL0SAoA4v7rJDteYD7DazAKBggqhkjOPQQDAzCBmDELMAkGA1UEBhMC VVMxFjAUBgNVBAoTDUdlb1RydXN0IEluYy4xOTA3BgNVBAsTMChjKSAyMDA3IEdlb1RydXN0IElu Yy4gLSBGb3IgYXV0aG9yaXplZCB1c2Ugb25seTE2MDQGA1UEAxMtR2VvVHJ1c3QgUHJpbWFyeSBD ZXJ0aWZpY2F0aW9uIEF1dGhvcml0eSAtIEcyMB4XDTA3MTEwNTAwMDAwMFoXDTM4MDExODIzNTk1 OVowgZgxCzAJBgNVBAYTAlVTMRYwFAYDVQQKEw1HZW9UcnVzdCBJbmMuMTkwNwYDVQQLEzAoYykg MjAwNyBHZW9UcnVzdCBJbmMuIC0gRm9yIGF1dGhvcml6ZWQgdXNlIG9ubHkxNjA0BgNVBAMTLUdl b1RydXN0IFByaW1hcnkgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkgLSBHMjB2MBAGByqGSM49AgEG BSuBBAAiA2IABBWx6P0DFUPlrOuHNxFi79KDNlJ9RVcLSo17VDs6bl8VAsBQps8lL33KSLjHUGMc KiEIfJo22Av+0SbFWDEwKCXzXV2juLaltJLtbCyf691DiaI8S0iRHVDsJt/WYC69IaNCMEAwDwYD VR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYwHQYDVR0OBBYEFBVfNVdRVfslsq0DafwBo/q+ EVXVMAoGCCqGSM49BAMDA2cAMGQCMGSWWaboCd6LuvpaiIjwH5HTRqjySkwCY/tsXzjbLkGTqQ7m ndwxHLKgpxgceeHHNgIwOlavmnRs9vuD4DPTCF+hnMJbn0bWtsuRBmOiBuczrD6ogRLQy7rQkgu2 npaqBA+K -----END CERTIFICATE----- VeriSign Universal Root Certification Authority =============================================== -----BEGIN CERTIFICATE----- MIIEuTCCA6GgAwIBAgIQQBrEZCGzEyEDDrvkEhrFHTANBgkqhkiG9w0BAQsFADCBvTELMAkGA1UE BhMCVVMxFzAVBgNVBAoTDlZlcmlTaWduLCBJbmMuMR8wHQYDVQQLExZWZXJpU2lnbiBUcnVzdCBO ZXR3b3JrMTowOAYDVQQLEzEoYykgMjAwOCBWZXJpU2lnbiwgSW5jLiAtIEZvciBhdXRob3JpemVk IHVzZSBvbmx5MTgwNgYDVQQDEy9WZXJpU2lnbiBVbml2ZXJzYWwgUm9vdCBDZXJ0aWZpY2F0aW9u IEF1dGhvcml0eTAeFw0wODA0MDIwMDAwMDBaFw0zNzEyMDEyMzU5NTlaMIG9MQswCQYDVQQGEwJV UzEXMBUGA1UEChMOVmVyaVNpZ24sIEluYy4xHzAdBgNVBAsTFlZlcmlTaWduIFRydXN0IE5ldHdv cmsxOjA4BgNVBAsTMShjKSAyMDA4IFZlcmlTaWduLCBJbmMuIC0gRm9yIGF1dGhvcml6ZWQgdXNl IG9ubHkxODA2BgNVBAMTL1ZlcmlTaWduIFVuaXZlcnNhbCBSb290IENlcnRpZmljYXRpb24gQXV0 aG9yaXR5MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAx2E3XrEBNNti1xWb/1hajCMj 1mCOkdeQmIN65lgZOIzF9uVkhbSicfvtvbnazU0AtMgtc6XHaXGVHzk8skQHnOgO+k1KxCHfKWGP MiJhgsWHH26MfF8WIFFE0XBPV+rjHOPMee5Y2A7Cs0WTwCznmhcrewA3ekEzeOEz4vMQGn+HLL72 9fdC4uW/h2KJXwBL38Xd5HVEMkE6HnFuacsLdUYI0crSK5XQz/u5QGtkjFdN/BMReYTtXlT2NJ8I AfMQJQYXStrxHXpma5hgZqTZ79IugvHw7wnqRMkVauIDbjPTrJ9VAMf2CGqUuV/c4DPxhGD5WycR tPwW8rtWaoAljQIDAQABo4GyMIGvMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgEGMG0G CCsGAQUFBwEMBGEwX6FdoFswWTBXMFUWCWltYWdlL2dpZjAhMB8wBwYFKw4DAhoEFI/l0xqGrI2O a8PPgGrUSBgsexkuMCUWI2h0dHA6Ly9sb2dvLnZlcmlzaWduLmNvbS92c2xvZ28uZ2lmMB0GA1Ud DgQWBBS2d/ppSEefUxLVwuoHMnYH0ZcHGTANBgkqhkiG9w0BAQsFAAOCAQEASvj4sAPmLGd75JR3 Y8xuTPl9Dg3cyLk1uXBPY/ok+myDjEedO2Pzmvl2MpWRsXe8rJq+seQxIcaBlVZaDrHC1LGmWazx Y8u4TB1ZkErvkBYoH1quEPuBUDgMbMzxPcP1Y+Oz4yHJJDnp/RVmRvQbEdBNc6N9Rvk97ahfYtTx P/jgdFcrGJ2BtMQo2pSXpXDrrB2+BxHw1dvd5Yzw1TKwg+ZX4o+/vqGqvz0dtdQ46tewXDpPaj+P wGZsY6rp2aQW9IHRlRQOfc2VNNnSj3BzgXucfr2YYdhFh5iQxeuGMMY1v/D/w1WIg0vvBZIGcfK4 mJO37M2CYfE45k+XmCpajQ== -----END CERTIFICATE----- NetLock Arany (Class Gold) Főtanúsítvány ======================================== -----BEGIN CERTIFICATE----- MIIEFTCCAv2gAwIBAgIGSUEs5AAQMA0GCSqGSIb3DQEBCwUAMIGnMQswCQYDVQQGEwJIVTERMA8G A1UEBwwIQnVkYXBlc3QxFTATBgNVBAoMDE5ldExvY2sgS2Z0LjE3MDUGA1UECwwuVGFuw7pzw610 dsOhbnlraWFkw7NrIChDZXJ0aWZpY2F0aW9uIFNlcnZpY2VzKTE1MDMGA1UEAwwsTmV0TG9jayBB cmFueSAoQ2xhc3MgR29sZCkgRsWRdGFuw7pzw610dsOhbnkwHhcNMDgxMjExMTUwODIxWhcNMjgx MjA2MTUwODIxWjCBpzELMAkGA1UEBhMCSFUxETAPBgNVBAcMCEJ1ZGFwZXN0MRUwEwYDVQQKDAxO ZXRMb2NrIEtmdC4xNzA1BgNVBAsMLlRhbsO6c8OtdHbDoW55a2lhZMOzayAoQ2VydGlmaWNhdGlv biBTZXJ2aWNlcykxNTAzBgNVBAMMLE5ldExvY2sgQXJhbnkgKENsYXNzIEdvbGQpIEbFkXRhbsO6 c8OtdHbDoW55MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAxCRec75LbRTDofTjl5Bu 0jBFHjzuZ9lk4BqKf8owyoPjIMHj9DrTlF8afFttvzBPhCf2nx9JvMaZCpDyD/V/Q4Q3Y1GLeqVw /HpYzY6b7cNGbIRwXdrzAZAj/E4wqX7hJ2Pn7WQ8oLjJM2P+FpD/sLj916jAwJRDC7bVWaaeVtAk H3B5r9s5VA1lddkVQZQBr17s9o3x/61k/iCa11zr/qYfCGSji3ZVrR47KGAuhyXoqq8fxmRGILdw fzzeSNuWU7c5d+Qa4scWhHaXWy+7GRWF+GmF9ZmnqfI0p6m2pgP8b4Y9VHx2BJtr+UBdADTHLpl1 neWIA6pN+APSQnbAGwIDAKiLo0UwQzASBgNVHRMBAf8ECDAGAQH/AgEEMA4GA1UdDwEB/wQEAwIB BjAdBgNVHQ4EFgQUzPpnk/C2uNClwB7zU/2MU9+D15YwDQYJKoZIhvcNAQELBQADggEBAKt/7hwW qZw8UQCgwBEIBaeZ5m8BiFRhbvG5GK1Krf6BQCOUL/t1fC8oS2IkgYIL9WHxHG64YTjrgfpioTta YtOUZcTh5m2C+C8lcLIhJsFyUR+MLMOEkMNaj7rP9KdlpeuY0fsFskZ1FSNqb4VjMIDw1Z4fKRzC bLBQWV2QWzuoDTDPv31/zvGdg73JRm4gpvlhUbohL3u+pRVjodSVh/GeufOJ8z2FuLjbvrW5Kfna NwUASZQDhETnv0Mxz3WLJdH0pmT1kvarBes96aULNmLazAZfNou2XjG4Kvte9nHfRCaexOYNkbQu dZWAUWpLMKawYqGT8ZvYzsRjdT9ZR7E= -----END CERTIFICATE----- Hongkong Post Root CA 1 ======================= -----BEGIN CERTIFICATE----- MIIDMDCCAhigAwIBAgICA+gwDQYJKoZIhvcNAQEFBQAwRzELMAkGA1UEBhMCSEsxFjAUBgNVBAoT DUhvbmdrb25nIFBvc3QxIDAeBgNVBAMTF0hvbmdrb25nIFBvc3QgUm9vdCBDQSAxMB4XDTAzMDUx NTA1MTMxNFoXDTIzMDUxNTA0NTIyOVowRzELMAkGA1UEBhMCSEsxFjAUBgNVBAoTDUhvbmdrb25n IFBvc3QxIDAeBgNVBAMTF0hvbmdrb25nIFBvc3QgUm9vdCBDQSAxMIIBIjANBgkqhkiG9w0BAQEF AAOCAQ8AMIIBCgKCAQEArP84tulmAknjorThkPlAj3n54r15/gK97iSSHSL22oVyaf7XPwnU3ZG1 ApzQjVrhVcNQhrkpJsLj2aDxaQMoIIBFIi1WpztUlVYiWR8o3x8gPW2iNr4joLFutbEnPzlTCeqr auh0ssJlXI6/fMN4hM2eFvz1Lk8gKgifd/PFHsSaUmYeSF7jEAaPIpjhZY4bXSNmO7ilMlHIhqqh qZ5/dpTCpmy3QfDVyAY45tQM4vM7TG1QjMSDJ8EThFk9nnV0ttgCXjqQesBCNnLsak3c78QA3xMY V18meMjWCnl3v/evt3a5pQuEF10Q6m/hq5URX208o1xNg1vysxmKgIsLhwIDAQABoyYwJDASBgNV HRMBAf8ECDAGAQH/AgEDMA4GA1UdDwEB/wQEAwIBxjANBgkqhkiG9w0BAQUFAAOCAQEADkbVPK7i h9legYsCmEEIjEy82tvuJxuC52pF7BaLT4Wg87JwvVqWuspube5Gi27nKi6Wsxkz67SfqLI37pio l7Yutmcn1KZJ/RyTZXaeQi/cImyaT/JaFTmxcdcrUehtHJjA2Sr0oYJ71clBoiMBdDhViw+5Lmei IAQ32pwL0xch4I+XeTRvhEgCIDMb5jREn5Fw9IBehEPCKdJsEhTkYY2sEJCehFC78JZvRZ+K88ps T/oROhUVRsPNH4NbLUES7VBnQRM9IauUiqpOfMGx+6fWtScvl6tu4B3i0RwsH0Ti/L6RoZz71ilT c4afU9hDDl3WY4JxHYB0yvbiAmvZWg== -----END CERTIFICATE----- SecureSign RootCA11 =================== -----BEGIN CERTIFICATE----- MIIDbTCCAlWgAwIBAgIBATANBgkqhkiG9w0BAQUFADBYMQswCQYDVQQGEwJKUDErMCkGA1UEChMi SmFwYW4gQ2VydGlmaWNhdGlvbiBTZXJ2aWNlcywgSW5jLjEcMBoGA1UEAxMTU2VjdXJlU2lnbiBS b290Q0ExMTAeFw0wOTA0MDgwNDU2NDdaFw0yOTA0MDgwNDU2NDdaMFgxCzAJBgNVBAYTAkpQMSsw KQYDVQQKEyJKYXBhbiBDZXJ0aWZpY2F0aW9uIFNlcnZpY2VzLCBJbmMuMRwwGgYDVQQDExNTZWN1 cmVTaWduIFJvb3RDQTExMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA/XeqpRyQBTvL TJszi1oURaTnkBbR31fSIRCkF/3frNYfp+TbfPfs37gD2pRY/V1yfIw/XwFndBWW4wI8h9uuywGO wvNmxoVF9ALGOrVisq/6nL+k5tSAMJjzDbaTj6nU2DbysPyKyiyhFTOVMdrAG/LuYpmGYz+/3ZMq g6h2uRMft85OQoWPIucuGvKVCbIFtUROd6EgvanyTgp9UK31BQ1FT0Zx/Sg+U/sE2C3XZR1KG/rP O7AxmjVuyIsG0wCR8pQIZUyxNAYAeoni8McDWc/V1uinMrPmmECGxc0nEovMe863ETxiYAcjPitA bpSACW22s293bzUIUPsCh8U+iQIDAQABo0IwQDAdBgNVHQ4EFgQUW/hNT7KlhtQ60vFjmqC+CfZX t94wDgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQEFBQADggEBAKCh OBZmLqdWHyGcBvod7bkixTgm2E5P7KN/ed5GIaGHd48HCJqypMWvDzKYC3xmKbabfSVSSUOrTC4r bnpwrxYO4wJs+0LmGJ1F2FXI6Dvd5+H0LgscNFxsWEr7jIhQX5Ucv+2rIrVls4W6ng+4reV6G4pQ Oh29Dbx7VFALuUKvVaAYga1lme++5Jy/xIWrQbJUb9wlze144o4MjQlJ3WN7WmmWAiGovVJZ6X01 y8hSyn+B/tlr0/cR7SXf+Of5pPpyl4RTDaXQMhhRdlkUbA/r7F+AjHVDg8OFmP9Mni0N5HeDk061 lgeLKBObjBmNQSdJQO7e5iNEOdyhIta6A/I= -----END CERTIFICATE----- Microsec e-Szigno Root CA 2009 ============================== -----BEGIN CERTIFICATE----- MIIECjCCAvKgAwIBAgIJAMJ+QwRORz8ZMA0GCSqGSIb3DQEBCwUAMIGCMQswCQYDVQQGEwJIVTER MA8GA1UEBwwIQnVkYXBlc3QxFjAUBgNVBAoMDU1pY3Jvc2VjIEx0ZC4xJzAlBgNVBAMMHk1pY3Jv c2VjIGUtU3ppZ25vIFJvb3QgQ0EgMjAwOTEfMB0GCSqGSIb3DQEJARYQaW5mb0BlLXN6aWduby5o dTAeFw0wOTA2MTYxMTMwMThaFw0yOTEyMzAxMTMwMThaMIGCMQswCQYDVQQGEwJIVTERMA8GA1UE BwwIQnVkYXBlc3QxFjAUBgNVBAoMDU1pY3Jvc2VjIEx0ZC4xJzAlBgNVBAMMHk1pY3Jvc2VjIGUt U3ppZ25vIFJvb3QgQ0EgMjAwOTEfMB0GCSqGSIb3DQEJARYQaW5mb0BlLXN6aWduby5odTCCASIw DQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAOn4j/NjrdqG2KfgQvvPkd6mJviZpWNwrZuuyjNA fW2WbqEORO7hE52UQlKavXWFdCyoDh2Tthi3jCyoz/tccbna7P7ofo/kLx2yqHWH2Leh5TvPmUpG 0IMZfcChEhyVbUr02MelTTMuhTlAdX4UfIASmFDHQWe4oIBhVKZsTh/gnQ4H6cm6M+f+wFUoLAKA pxn1ntxVUwOXewdI/5n7N4okxFnMUBBjjqqpGrCEGob5X7uxUG6k0QrM1XF+H6cbfPVTbiJfyyvm 1HxdrtbCxkzlBQHZ7Vf8wSN5/PrIJIOV87VqUQHQd9bpEqH5GoP7ghu5sJf0dgYzQ0mg/wu1+rUC AwEAAaOBgDB+MA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgEGMB0GA1UdDgQWBBTLD8bf QkPMPcu1SCOhGnqmKrs0aDAfBgNVHSMEGDAWgBTLD8bfQkPMPcu1SCOhGnqmKrs0aDAbBgNVHREE FDASgRBpbmZvQGUtc3ppZ25vLmh1MA0GCSqGSIb3DQEBCwUAA4IBAQDJ0Q5eLtXMs3w+y/w9/w0o lZMEyL/azXm4Q5DwpL7v8u8hmLzU1F0G9u5C7DBsoKqpyvGvivo/C3NqPuouQH4frlRheesuCDfX I/OMn74dseGkddug4lQUsbocKaQY9hK6ohQU4zE1yED/t+AFdlfBHFny+L/k7SViXITwfn4fs775 tyERzAMBVnCnEJIeGzSBHq2cGsMEPO0CYdYeBvNfOofyK/FFh+U9rNHHV4S9a67c2Pm2G2JwCz02 yULyMtd6YebS2z3PyKnJm9zbWETXbzivf3jTo60adbocwTZ8jx5tHMN1Rq41Bab2XD0h7lbwyYIi LXpUq3DDfSJlgnCW -----END CERTIFICATE----- GlobalSign Root CA - R3 ======================= -----BEGIN CERTIFICATE----- MIIDXzCCAkegAwIBAgILBAAAAAABIVhTCKIwDQYJKoZIhvcNAQELBQAwTDEgMB4GA1UECxMXR2xv YmFsU2lnbiBSb290IENBIC0gUjMxEzARBgNVBAoTCkdsb2JhbFNpZ24xEzARBgNVBAMTCkdsb2Jh bFNpZ24wHhcNMDkwMzE4MTAwMDAwWhcNMjkwMzE4MTAwMDAwWjBMMSAwHgYDVQQLExdHbG9iYWxT aWduIFJvb3QgQ0EgLSBSMzETMBEGA1UEChMKR2xvYmFsU2lnbjETMBEGA1UEAxMKR2xvYmFsU2ln bjCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAMwldpB5BngiFvXAg7aEyiie/QV2EcWt iHL8RgJDx7KKnQRfJMsuS+FggkbhUqsMgUdwbN1k0ev1LKMPgj0MK66X17YUhhB5uzsTgHeMCOFJ 0mpiLx9e+pZo34knlTifBtc+ycsmWQ1z3rDI6SYOgxXG71uL0gRgykmmKPZpO/bLyCiR5Z2KYVc3 rHQU3HTgOu5yLy6c+9C7v/U9AOEGM+iCK65TpjoWc4zdQQ4gOsC0p6Hpsk+QLjJg6VfLuQSSaGjl OCZgdbKfd/+RFO+uIEn8rUAVSNECMWEZXriX7613t2Saer9fwRPvm2L7DWzgVGkWqQPabumDk3F2 xmmFghcCAwEAAaNCMEAwDgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYE FI/wS3+oLkUkrk1Q+mOai97i3Ru8MA0GCSqGSIb3DQEBCwUAA4IBAQBLQNvAUKr+yAzv95ZURUm7 lgAJQayzE4aGKAczymvmdLm6AC2upArT9fHxD4q/c2dKg8dEe3jgr25sbwMpjjM5RcOO5LlXbKr8 EpbsU8Yt5CRsuZRj+9xTaGdWPoO4zzUhw8lo/s7awlOqzJCK6fBdRoyV3XpYKBovHd7NADdBj+1E bddTKJd+82cEHhXXipa0095MJ6RMG3NzdvQXmcIfeg7jLQitChws/zyrVQ4PkX4268NXSb7hLi18 YIvDQVETI53O9zJrlAGomecsMx86OyXShkDOOyyGeMlhLxS67ttVb9+E7gUJTb0o2HLO02JQZR7r kpeDMdmztcpHWD9f -----END CERTIFICATE----- Autoridad de Certificacion Firmaprofesional CIF A62634068 ========================================================= -----BEGIN CERTIFICATE----- MIIGFDCCA/ygAwIBAgIIU+w77vuySF8wDQYJKoZIhvcNAQEFBQAwUTELMAkGA1UEBhMCRVMxQjBA BgNVBAMMOUF1dG9yaWRhZCBkZSBDZXJ0aWZpY2FjaW9uIEZpcm1hcHJvZmVzaW9uYWwgQ0lGIEE2 MjYzNDA2ODAeFw0wOTA1MjAwODM4MTVaFw0zMDEyMzEwODM4MTVaMFExCzAJBgNVBAYTAkVTMUIw QAYDVQQDDDlBdXRvcmlkYWQgZGUgQ2VydGlmaWNhY2lvbiBGaXJtYXByb2Zlc2lvbmFsIENJRiBB NjI2MzQwNjgwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDKlmuO6vj78aI14H9M2uDD Utd9thDIAl6zQyrET2qyyhxdKJp4ERppWVevtSBC5IsP5t9bpgOSL/UR5GLXMnE42QQMcas9UX4P B99jBVzpv5RvwSmCwLTaUbDBPLutN0pcyvFLNg4kq7/DhHf9qFD0sefGL9ItWY16Ck6WaVICqjaY 7Pz6FIMMNx/Jkjd/14Et5cS54D40/mf0PmbR0/RAz15iNA9wBj4gGFrO93IbJWyTdBSTo3OxDqqH ECNZXyAFGUftaI6SEspd/NYrspI8IM/hX68gvqB2f3bl7BqGYTM+53u0P6APjqK5am+5hyZvQWyI plD9amML9ZMWGxmPsu2bm8mQ9QEM3xk9Dz44I8kvjwzRAv4bVdZO0I08r0+k8/6vKtMFnXkIoctX MbScyJCyZ/QYFpM6/EfY0XiWMR+6KwxfXZmtY4laJCB22N/9q06mIqqdXuYnin1oKaPnirjaEbsX LZmdEyRG98Xi2J+Of8ePdG1asuhy9azuJBCtLxTa/y2aRnFHvkLfuwHb9H/TKI8xWVvTyQKmtFLK bpf7Q8UIJm+K9Lv9nyiqDdVF8xM6HdjAeI9BZzwelGSuewvF6NkBiDkal4ZkQdU7hwxu+g/GvUgU vzlN1J5Bto+WHWOWk9mVBngxaJ43BjuAiUVhOSPHG0SjFeUc+JIwuwIDAQABo4HvMIHsMBIGA1Ud EwEB/wQIMAYBAf8CAQEwDgYDVR0PAQH/BAQDAgEGMB0GA1UdDgQWBBRlzeurNR4APn7VdMActHNH DhpkLzCBpgYDVR0gBIGeMIGbMIGYBgRVHSAAMIGPMC8GCCsGAQUFBwIBFiNodHRwOi8vd3d3LmZp cm1hcHJvZmVzaW9uYWwuY29tL2NwczBcBggrBgEFBQcCAjBQHk4AUABhAHMAZQBvACAAZABlACAA bABhACAAQgBvAG4AYQBuAG8AdgBhACAANAA3ACAAQgBhAHIAYwBlAGwAbwBuAGEAIAAwADgAMAAx ADcwDQYJKoZIhvcNAQEFBQADggIBABd9oPm03cXF661LJLWhAqvdpYhKsg9VSytXjDvlMd3+xDLx 51tkljYyGOylMnfX40S2wBEqgLk9am58m9Ot/MPWo+ZkKXzR4Tgegiv/J2Wv+xYVxC5xhOW1//qk R71kMrv2JYSiJ0L1ILDCExARzRAVukKQKtJE4ZYm6zFIEv0q2skGz3QeqUvVhyj5eTSSPi5E6PaP T481PyWzOdxjKpBrIF/EUhJOlywqrJ2X3kjyo2bbwtKDlaZmp54lD+kLM5FlClrD2VQS3a/DTg4f Jl4N3LON7NWBcN7STyQF82xO9UxJZo3R/9ILJUFI/lGExkKvgATP0H5kSeTy36LssUzAKh3ntLFl osS88Zj0qnAHY7S42jtM+kAiMFsRpvAFDsYCA0irhpuF3dvd6qJ2gHN99ZwExEWN57kci57q13XR crHedUTnQn3iV2t93Jm8PYMo6oCTjcVMZcFwgbg4/EMxsvYDNEeyrPsiBsse3RdHHF9mudMaotoR saS8I8nkvof/uZS2+F0gStRf571oe2XyFR7SOqkt6dhrJKyXWERHrVkY8SFlcN7ONGCoQPHzPKTD KCOM/iczQ0CgFzzr6juwcqajuUpLXhZI9LK8yIySxZ2frHI2vDSANGupi5LAuBft7HZT9SQBjLMi 6Et8Vcad+qMUu2WFbm5PEn4KPJ2V -----END CERTIFICATE----- Izenpe.com ========== -----BEGIN CERTIFICATE----- MIIF8TCCA9mgAwIBAgIQALC3WhZIX7/hy/WL1xnmfTANBgkqhkiG9w0BAQsFADA4MQswCQYDVQQG EwJFUzEUMBIGA1UECgwLSVpFTlBFIFMuQS4xEzARBgNVBAMMCkl6ZW5wZS5jb20wHhcNMDcxMjEz MTMwODI4WhcNMzcxMjEzMDgyNzI1WjA4MQswCQYDVQQGEwJFUzEUMBIGA1UECgwLSVpFTlBFIFMu QS4xEzARBgNVBAMMCkl6ZW5wZS5jb20wggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDJ 03rKDx6sp4boFmVqscIbRTJxldn+EFvMr+eleQGPicPK8lVx93e+d5TzcqQsRNiekpsUOqHnJJAK ClaOxdgmlOHZSOEtPtoKct2jmRXagaKH9HtuJneJWK3W6wyyQXpzbm3benhB6QiIEn6HLmYRY2xU +zydcsC8Lv/Ct90NduM61/e0aL6i9eOBbsFGb12N4E3GVFWJGjMxCrFXuaOKmMPsOzTFlUFpfnXC PCDFYbpRR6AgkJOhkEvzTnyFRVSa0QUmQbC1TR0zvsQDyCV8wXDbO/QJLVQnSKwv4cSsPsjLkkxT OTcj7NMB+eAJRE1NZMDhDVqHIrytG6P+JrUV86f8hBnp7KGItERphIPzidF0BqnMC9bC3ieFUCbK F7jJeodWLBoBHmy+E60QrLUk9TiRodZL2vG70t5HtfG8gfZZa88ZU+mNFctKy6lvROUbQc/hhqfK 0GqfvEyNBjNaooXlkDWgYlwWTvDjovoDGrQscbNYLN57C9saD+veIR8GdwYDsMnvmfzAuU8Lhij+ 0rnq49qlw0dpEuDb8PYZi+17cNcC1u2HGCgsBCRMd+RIihrGO5rUD8r6ddIBQFqNeb+Lz0vPqhbB leStTIo+F5HUsWLlguWABKQDfo2/2n+iD5dPDNMN+9fR5XJ+HMh3/1uaD7euBUbl8agW7EekFwID AQABo4H2MIHzMIGwBgNVHREEgagwgaWBD2luZm9AaXplbnBlLmNvbaSBkTCBjjFHMEUGA1UECgw+ SVpFTlBFIFMuQS4gLSBDSUYgQTAxMzM3MjYwLVJNZXJjLlZpdG9yaWEtR2FzdGVpeiBUMTA1NSBG NjIgUzgxQzBBBgNVBAkMOkF2ZGEgZGVsIE1lZGl0ZXJyYW5lbyBFdG9yYmlkZWEgMTQgLSAwMTAx MCBWaXRvcmlhLUdhc3RlaXowDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYwHQYDVR0O BBYEFB0cZQ6o8iV7tJHP5LGx5r1VdGwFMA0GCSqGSIb3DQEBCwUAA4ICAQB4pgwWSp9MiDrAyw6l Fn2fuUhfGI8NYjb2zRlrrKvV9pF9rnHzP7MOeIWblaQnIUdCSnxIOvVFfLMMjlF4rJUT3sb9fbga kEyrkgPH7UIBzg/YsfqikuFgba56awmqxinuaElnMIAkejEWOVt+8Rwu3WwJrfIxwYJOubv5vr8q hT/AQKM6WfxZSzwoJNu0FXWuDYi6LnPAvViH5ULy617uHjAimcs30cQhbIHsvm0m5hzkQiCeR7Cs g1lwLDXWrzY0tM07+DKo7+N4ifuNRSzanLh+QBxh5z6ikixL8s36mLYp//Pye6kfLqCTVyvehQP5 aTfLnnhqBbTFMXiJ7HqnheG5ezzevh55hM6fcA5ZwjUukCox2eRFekGkLhObNA5me0mrZJfQRsN5 nXJQY6aYWwa9SG3YOYNw6DXwBdGqvOPbyALqfP2C2sJbUjWumDqtujWTI6cfSN01RpiyEGjkpTHC ClguGYEQyVB1/OpaFs4R1+7vUIgtYf8/QnMFlEPVjjxOAToZpR9GTnfQXeWBIiGH/pR9hNiTrdZo Q0iy2+tzJOeRf1SktoA+naM8THLCV8Sg1Mw4J87VBp6iSNnpn86CcDaTmjvfliHjWbcM2pE38P1Z WrOZyGlsQyYBNWNgVYkDOnXYukrZVP/u3oDYLdE41V4tC5h9Pmzb/CaIxw== -----END CERTIFICATE----- Chambers of Commerce Root - 2008 ================================ -----BEGIN CERTIFICATE----- MIIHTzCCBTegAwIBAgIJAKPaQn6ksa7aMA0GCSqGSIb3DQEBBQUAMIGuMQswCQYDVQQGEwJFVTFD MEEGA1UEBxM6TWFkcmlkIChzZWUgY3VycmVudCBhZGRyZXNzIGF0IHd3dy5jYW1lcmZpcm1hLmNv bS9hZGRyZXNzKTESMBAGA1UEBRMJQTgyNzQzMjg3MRswGQYDVQQKExJBQyBDYW1lcmZpcm1hIFMu QS4xKTAnBgNVBAMTIENoYW1iZXJzIG9mIENvbW1lcmNlIFJvb3QgLSAyMDA4MB4XDTA4MDgwMTEy Mjk1MFoXDTM4MDczMTEyMjk1MFowga4xCzAJBgNVBAYTAkVVMUMwQQYDVQQHEzpNYWRyaWQgKHNl ZSBjdXJyZW50IGFkZHJlc3MgYXQgd3d3LmNhbWVyZmlybWEuY29tL2FkZHJlc3MpMRIwEAYDVQQF EwlBODI3NDMyODcxGzAZBgNVBAoTEkFDIENhbWVyZmlybWEgUy5BLjEpMCcGA1UEAxMgQ2hhbWJl cnMgb2YgQ29tbWVyY2UgUm9vdCAtIDIwMDgwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoIC AQCvAMtwNyuAWko6bHiUfaN/Gh/2NdW928sNRHI+JrKQUrpjOyhYb6WzbZSm891kDFX29ufyIiKA XuFixrYp4YFs8r/lfTJqVKAyGVn+H4vXPWCGhSRv4xGzdz4gljUha7MI2XAuZPeEklPWDrCQiorj h40G072QDuKZoRuGDtqaCrsLYVAGUvGef3bsyw/QHg3PmTA9HMRFEFis1tPo1+XqxQEHd9ZR5gN/ ikilTWh1uem8nk4ZcfUyS5xtYBkL+8ydddy/Js2Pk3g5eXNeJQ7KXOt3EgfLZEFHcpOrUMPrCXZk NNI5t3YRCQ12RcSprj1qr7V9ZS+UWBDsXHyvfuK2GNnQm05aSd+pZgvMPMZ4fKecHePOjlO+Bd5g D2vlGts/4+EhySnB8esHnFIbAURRPHsl18TlUlRdJQfKFiC4reRB7noI/plvg6aRArBsNlVq5331 lubKgdaX8ZSD6e2wsWsSaR6s+12pxZjptFtYer49okQ6Y1nUCyXeG0+95QGezdIp1Z8XGQpvvwyQ 0wlf2eOKNcx5Wk0ZN5K3xMGtr/R5JJqyAQuxr1yW84Ay+1w9mPGgP0revq+ULtlVmhduYJ1jbLhj ya6BXBg14JC7vjxPNyK5fuvPnnchpj04gftI2jE9K+OJ9dC1vX7gUMQSibMjmhAxhduub+84Mxh2 EQIDAQABo4IBbDCCAWgwEgYDVR0TAQH/BAgwBgEB/wIBDDAdBgNVHQ4EFgQU+SSsD7K1+HnA+mCI G8TZTQKeFxkwgeMGA1UdIwSB2zCB2IAU+SSsD7K1+HnA+mCIG8TZTQKeFxmhgbSkgbEwga4xCzAJ BgNVBAYTAkVVMUMwQQYDVQQHEzpNYWRyaWQgKHNlZSBjdXJyZW50IGFkZHJlc3MgYXQgd3d3LmNh bWVyZmlybWEuY29tL2FkZHJlc3MpMRIwEAYDVQQFEwlBODI3NDMyODcxGzAZBgNVBAoTEkFDIENh bWVyZmlybWEgUy5BLjEpMCcGA1UEAxMgQ2hhbWJlcnMgb2YgQ29tbWVyY2UgUm9vdCAtIDIwMDiC CQCj2kJ+pLGu2jAOBgNVHQ8BAf8EBAMCAQYwPQYDVR0gBDYwNDAyBgRVHSAAMCowKAYIKwYBBQUH AgEWHGh0dHA6Ly9wb2xpY3kuY2FtZXJmaXJtYS5jb20wDQYJKoZIhvcNAQEFBQADggIBAJASryI1 wqM58C7e6bXpeHxIvj99RZJe6dqxGfwWPJ+0W2aeaufDuV2I6A+tzyMP3iU6XsxPpcG1Lawk0lgH 3qLPaYRgM+gQDROpI9CF5Y57pp49chNyM/WqfcZjHwj0/gF/JM8rLFQJ3uIrbZLGOU8W6jx+ekbU RWpGqOt1glanq6B8aBMz9p0w8G8nOSQjKpD9kCk18pPfNKXG9/jvjA9iSnyu0/VU+I22mlaHFoI6 M6taIgj3grrqLuBHmrS1RaMFO9ncLkVAO+rcf+g769HsJtg1pDDFOqxXnrN2pSB7+R5KBWIBpih1 YJeSDW4+TTdDDZIVnBgizVGZoCkaPF+KMjNbMMeJL0eYD6MDxvbxrN8y8NmBGuScvfaAFPDRLLmF 9dijscilIeUcE5fuDr3fKanvNFNb0+RqE4QGtjICxFKuItLcsiFCGtpA8CnJ7AoMXOLQusxI0zcK zBIKinmwPQN/aUv0NCB9szTqjktk9T79syNnFQ0EuPAtwQlRPLJsFfClI9eDdOTlLsn+mCdCxqvG nrDQWzilm1DefhiYtUU79nm06PcaewaD+9CL2rvHvRirCG88gGtAPxkZumWK5r7VXNM21+9AUiRg OGcEMeyP84LG3rlV8zsxkVrctQgVrXYlCg17LofiDKYGvCYQbTed7N14jHyAxfDZd0jQ -----END CERTIFICATE----- Global Chambersign Root - 2008 ============================== -----BEGIN CERTIFICATE----- MIIHSTCCBTGgAwIBAgIJAMnN0+nVfSPOMA0GCSqGSIb3DQEBBQUAMIGsMQswCQYDVQQGEwJFVTFD MEEGA1UEBxM6TWFkcmlkIChzZWUgY3VycmVudCBhZGRyZXNzIGF0IHd3dy5jYW1lcmZpcm1hLmNv bS9hZGRyZXNzKTESMBAGA1UEBRMJQTgyNzQzMjg3MRswGQYDVQQKExJBQyBDYW1lcmZpcm1hIFMu QS4xJzAlBgNVBAMTHkdsb2JhbCBDaGFtYmVyc2lnbiBSb290IC0gMjAwODAeFw0wODA4MDExMjMx NDBaFw0zODA3MzExMjMxNDBaMIGsMQswCQYDVQQGEwJFVTFDMEEGA1UEBxM6TWFkcmlkIChzZWUg Y3VycmVudCBhZGRyZXNzIGF0IHd3dy5jYW1lcmZpcm1hLmNvbS9hZGRyZXNzKTESMBAGA1UEBRMJ QTgyNzQzMjg3MRswGQYDVQQKExJBQyBDYW1lcmZpcm1hIFMuQS4xJzAlBgNVBAMTHkdsb2JhbCBD aGFtYmVyc2lnbiBSb290IC0gMjAwODCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAMDf VtPkOpt2RbQT2//BthmLN0EYlVJH6xedKYiONWwGMi5HYvNJBL99RDaxccy9Wglz1dmFRP+RVyXf XjaOcNFccUMd2drvXNL7G706tcuto8xEpw2uIRU/uXpbknXYpBI4iRmKt4DS4jJvVpyR1ogQC7N0 ZJJ0YPP2zxhPYLIj0Mc7zmFLmY/CDNBAspjcDahOo7kKrmCgrUVSY7pmvWjg+b4aqIG7HkF4ddPB /gBVsIdU6CeQNR1MM62X/JcumIS/LMmjv9GYERTtY/jKmIhYF5ntRQOXfjyGHoiMvvKRhI9lNNgA TH23MRdaKXoKGCQwoze1eqkBfSbW+Q6OWfH9GzO1KTsXO0G2Id3UwD2ln58fQ1DJu7xsepeY7s2M H/ucUa6LcL0nn3HAa6x9kGbo1106DbDVwo3VyJ2dwW3Q0L9R5OP4wzg2rtandeavhENdk5IMagfe Ox2YItaswTXbo6Al/3K1dh3ebeksZixShNBFks4c5eUzHdwHU1SjqoI7mjcv3N2gZOnm3b2u/GSF HTynyQbehP9r6GsaPMWis0L7iwk+XwhSx2LE1AVxv8Rk5Pihg+g+EpuoHtQ2TS9x9o0o9oOpE9Jh wZG7SMA0j0GMS0zbaRL/UJScIINZc+18ofLx/d33SdNDWKBWY8o9PeU1VlnpDsogzCtLkykPAgMB AAGjggFqMIIBZjASBgNVHRMBAf8ECDAGAQH/AgEMMB0GA1UdDgQWBBS5CcqcHtvTbDprru1U8VuT BjUuXjCB4QYDVR0jBIHZMIHWgBS5CcqcHtvTbDprru1U8VuTBjUuXqGBsqSBrzCBrDELMAkGA1UE BhMCRVUxQzBBBgNVBAcTOk1hZHJpZCAoc2VlIGN1cnJlbnQgYWRkcmVzcyBhdCB3d3cuY2FtZXJm aXJtYS5jb20vYWRkcmVzcykxEjAQBgNVBAUTCUE4Mjc0MzI4NzEbMBkGA1UEChMSQUMgQ2FtZXJm aXJtYSBTLkEuMScwJQYDVQQDEx5HbG9iYWwgQ2hhbWJlcnNpZ24gUm9vdCAtIDIwMDiCCQDJzdPp 1X0jzjAOBgNVHQ8BAf8EBAMCAQYwPQYDVR0gBDYwNDAyBgRVHSAAMCowKAYIKwYBBQUHAgEWHGh0 dHA6Ly9wb2xpY3kuY2FtZXJmaXJtYS5jb20wDQYJKoZIhvcNAQEFBQADggIBAICIf3DekijZBZRG /5BXqfEv3xoNa/p8DhxJJHkn2EaqbylZUohwEurdPfWbU1Rv4WCiqAm57OtZfMY18dwY6fFn5a+6 ReAJ3spED8IXDneRRXozX1+WLGiLwUePmJs9wOzL9dWCkoQ10b42OFZyMVtHLaoXpGNR6woBrX/s dZ7LoR/xfxKxueRkf2fWIyr0uDldmOghp+G9PUIadJpwr2hsUF1Jz//7Dl3mLEfXgTpZALVza2Mg 9jFFCDkO9HB+QHBaP9BrQql0PSgvAm11cpUJjUhjxsYjV5KTXjXBjfkK9yydYhz2rXzdpjEetrHH foUm+qRqtdpjMNHvkzeyZi99Bffnt0uYlDXA2TopwZ2yUDMdSqlapskD7+3056huirRXhOukP9Du qqqHW2Pok+JrqNS4cnhrG+055F3Lm6qH1U9OAP7Zap88MQ8oAgF9mOinsKJknnn4SPIVqczmyETr P3iZ8ntxPjzxmKfFGBI/5rsoM0LpRQp8bfKGeS/Fghl9CYl8slR2iK7ewfPM4W7bMdaTrpmg7yVq c5iJWzouE4gev8CSlDQb4ye3ix5vQv/n6TebUB0tovkC7stYWDpxvGjjqsGvHCgfotwjZT+B6q6Z 09gwzxMNTxXJhLynSC34MCN32EZLeW32jO06f2ARePTpm67VVMB0gNELQp/B -----END CERTIFICATE----- Go Daddy Root Certificate Authority - G2 ======================================== -----BEGIN CERTIFICATE----- MIIDxTCCAq2gAwIBAgIBADANBgkqhkiG9w0BAQsFADCBgzELMAkGA1UEBhMCVVMxEDAOBgNVBAgT B0FyaXpvbmExEzARBgNVBAcTClNjb3R0c2RhbGUxGjAYBgNVBAoTEUdvRGFkZHkuY29tLCBJbmMu MTEwLwYDVQQDEyhHbyBEYWRkeSBSb290IENlcnRpZmljYXRlIEF1dGhvcml0eSAtIEcyMB4XDTA5 MDkwMTAwMDAwMFoXDTM3MTIzMTIzNTk1OVowgYMxCzAJBgNVBAYTAlVTMRAwDgYDVQQIEwdBcml6 b25hMRMwEQYDVQQHEwpTY290dHNkYWxlMRowGAYDVQQKExFHb0RhZGR5LmNvbSwgSW5jLjExMC8G A1UEAxMoR28gRGFkZHkgUm9vdCBDZXJ0aWZpY2F0ZSBBdXRob3JpdHkgLSBHMjCCASIwDQYJKoZI hvcNAQEBBQADggEPADCCAQoCggEBAL9xYgjx+lk09xvJGKP3gElY6SKDE6bFIEMBO4Tx5oVJnyfq 9oQbTqC023CYxzIBsQU+B07u9PpPL1kwIuerGVZr4oAH/PMWdYA5UXvl+TW2dE6pjYIT5LY/qQOD +qK+ihVqf94Lw7YZFAXK6sOoBJQ7RnwyDfMAZiLIjWltNowRGLfTshxgtDj6AozO091GB94KPutd fMh8+7ArU6SSYmlRJQVhGkSBjCypQ5Yj36w6gZoOKcUcqeldHraenjAKOc7xiID7S13MMuyFYkMl NAJWJwGRtDtwKj9useiciAF9n9T521NtYJ2/LOdYq7hfRvzOxBsDPAnrSTFcaUaz4EcCAwEAAaNC MEAwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYwHQYDVR0OBBYEFDqahQcQZyi27/a9 BUFuIMGU2g/eMA0GCSqGSIb3DQEBCwUAA4IBAQCZ21151fmXWWcDYfF+OwYxdS2hII5PZYe096ac vNjpL9DbWu7PdIxztDhC2gV7+AJ1uP2lsdeu9tfeE8tTEH6KRtGX+rcuKxGrkLAngPnon1rpN5+r 5N9ss4UXnT3ZJE95kTXWXwTrgIOrmgIttRD02JDHBHNA7XIloKmf7J6raBKZV8aPEjoJpL1E/QYV N8Gb5DKj7Tjo2GTzLH4U/ALqn83/B2gX2yKQOC16jdFU8WnjXzPKej17CuPKf1855eJ1usV2GDPO LPAvTK33sefOT6jEm0pUBsV/fdUID+Ic/n4XuKxe9tQWskMJDE32p2u0mYRlynqI4uJEvlz36hz1 -----END CERTIFICATE----- Starfield Root Certificate Authority - G2 ========================================= -----BEGIN CERTIFICATE----- MIID3TCCAsWgAwIBAgIBADANBgkqhkiG9w0BAQsFADCBjzELMAkGA1UEBhMCVVMxEDAOBgNVBAgT B0FyaXpvbmExEzARBgNVBAcTClNjb3R0c2RhbGUxJTAjBgNVBAoTHFN0YXJmaWVsZCBUZWNobm9s b2dpZXMsIEluYy4xMjAwBgNVBAMTKVN0YXJmaWVsZCBSb290IENlcnRpZmljYXRlIEF1dGhvcml0 eSAtIEcyMB4XDTA5MDkwMTAwMDAwMFoXDTM3MTIzMTIzNTk1OVowgY8xCzAJBgNVBAYTAlVTMRAw DgYDVQQIEwdBcml6b25hMRMwEQYDVQQHEwpTY290dHNkYWxlMSUwIwYDVQQKExxTdGFyZmllbGQg VGVjaG5vbG9naWVzLCBJbmMuMTIwMAYDVQQDEylTdGFyZmllbGQgUm9vdCBDZXJ0aWZpY2F0ZSBB dXRob3JpdHkgLSBHMjCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAL3twQP89o/8ArFv W59I2Z154qK3A2FWGMNHttfKPTUuiUP3oWmb3ooa/RMgnLRJdzIpVv257IzdIvpy3Cdhl+72WoTs bhm5iSzchFvVdPtrX8WJpRBSiUZV9Lh1HOZ/5FSuS/hVclcCGfgXcVnrHigHdMWdSL5stPSksPNk N3mSwOxGXn/hbVNMYq/NHwtjuzqd+/x5AJhhdM8mgkBj87JyahkNmcrUDnXMN/uLicFZ8WJ/X7Nf ZTD4p7dNdloedl40wOiWVpmKs/B/pM293DIxfJHP4F8R+GuqSVzRmZTRouNjWwl2tVZi4Ut0HZbU JtQIBFnQmA4O5t78w+wfkPECAwEAAaNCMEAwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMC AQYwHQYDVR0OBBYEFHwMMh+n2TB/xH1oo2Kooc6rB1snMA0GCSqGSIb3DQEBCwUAA4IBAQARWfol TwNvlJk7mh+ChTnUdgWUXuEok21iXQnCoKjUsHU48TRqneSfioYmUeYs0cYtbpUgSpIB7LiKZ3sx 4mcujJUDJi5DnUox9g61DLu34jd/IroAow57UvtruzvE03lRTs2Q9GcHGcg8RnoNAX3FWOdt5oUw F5okxBDgBPfg8n/Uqgr/Qh037ZTlZFkSIHc40zI+OIF1lnP6aI+xy84fxez6nH7PfrHxBy22/L/K pL/QlwVKvOoYKAKQvVR4CSFx09F9HdkWsKlhPdAKACL8x3vLCWRFCztAgfd9fDL1mMpYjn0q7pBZ c2T5NnReJaH1ZgUufzkVqSr7UIuOhWn0 -----END CERTIFICATE----- Starfield Services Root Certificate Authority - G2 ================================================== -----BEGIN CERTIFICATE----- MIID7zCCAtegAwIBAgIBADANBgkqhkiG9w0BAQsFADCBmDELMAkGA1UEBhMCVVMxEDAOBgNVBAgT B0FyaXpvbmExEzARBgNVBAcTClNjb3R0c2RhbGUxJTAjBgNVBAoTHFN0YXJmaWVsZCBUZWNobm9s b2dpZXMsIEluYy4xOzA5BgNVBAMTMlN0YXJmaWVsZCBTZXJ2aWNlcyBSb290IENlcnRpZmljYXRl IEF1dGhvcml0eSAtIEcyMB4XDTA5MDkwMTAwMDAwMFoXDTM3MTIzMTIzNTk1OVowgZgxCzAJBgNV BAYTAlVTMRAwDgYDVQQIEwdBcml6b25hMRMwEQYDVQQHEwpTY290dHNkYWxlMSUwIwYDVQQKExxT dGFyZmllbGQgVGVjaG5vbG9naWVzLCBJbmMuMTswOQYDVQQDEzJTdGFyZmllbGQgU2VydmljZXMg Um9vdCBDZXJ0aWZpY2F0ZSBBdXRob3JpdHkgLSBHMjCCASIwDQYJKoZIhvcNAQEBBQADggEPADCC AQoCggEBANUMOsQq+U7i9b4Zl1+OiFOxHz/Lz58gE20pOsgPfTz3a3Y4Y9k2YKibXlwAgLIvWX/2 h/klQ4bnaRtSmpDhcePYLQ1Ob/bISdm28xpWriu2dBTrz/sm4xq6HZYuajtYlIlHVv8loJNwU4Pa hHQUw2eeBGg6345AWh1KTs9DkTvnVtYAcMtS7nt9rjrnvDH5RfbCYM8TWQIrgMw0R9+53pBlbQLP LJGmpufehRhJfGZOozptqbXuNC66DQO4M99H67FrjSXZm86B0UVGMpZwh94CDklDhbZsc7tk6mFB rMnUVN+HL8cisibMn1lUaJ/8viovxFUcdUBgF4UCVTmLfwUCAwEAAaNCMEAwDwYDVR0TAQH/BAUw AwEB/zAOBgNVHQ8BAf8EBAMCAQYwHQYDVR0OBBYEFJxfAN+qAdcwKziIorhtSpzyEZGDMA0GCSqG SIb3DQEBCwUAA4IBAQBLNqaEd2ndOxmfZyMIbw5hyf2E3F/YNoHN2BtBLZ9g3ccaaNnRbobhiCPP E95Dz+I0swSdHynVv/heyNXBve6SbzJ08pGCL72CQnqtKrcgfU28elUSwhXqvfdqlS5sdJ/PHLTy xQGjhdByPq1zqwubdQxtRbeOlKyWN7Wg0I8VRw7j6IPdj/3vQQF3zCepYoUz8jcI73HPdwbeyBkd iEDPfUYd/x7H4c7/I9vG+o1VTqkC50cRRj70/b17KSa7qWFiNyi2LSr2EIZkyXCn0q23KXB56jza YyWf/Wi3MOxw+3WKt21gZ7IeyLnp2KhvAotnDU0mV3HaIPzBSlCNsSi6 -----END CERTIFICATE----- AffirmTrust Commercial ====================== -----BEGIN CERTIFICATE----- MIIDTDCCAjSgAwIBAgIId3cGJyapsXwwDQYJKoZIhvcNAQELBQAwRDELMAkGA1UEBhMCVVMxFDAS BgNVBAoMC0FmZmlybVRydXN0MR8wHQYDVQQDDBZBZmZpcm1UcnVzdCBDb21tZXJjaWFsMB4XDTEw MDEyOTE0MDYwNloXDTMwMTIzMTE0MDYwNlowRDELMAkGA1UEBhMCVVMxFDASBgNVBAoMC0FmZmly bVRydXN0MR8wHQYDVQQDDBZBZmZpcm1UcnVzdCBDb21tZXJjaWFsMIIBIjANBgkqhkiG9w0BAQEF AAOCAQ8AMIIBCgKCAQEA9htPZwcroRX1BiLLHwGy43NFBkRJLLtJJRTWzsO3qyxPxkEylFf6Eqdb DuKPHx6GGaeqtS25Xw2Kwq+FNXkyLbscYjfysVtKPcrNcV/pQr6U6Mje+SJIZMblq8Yrba0F8PrV C8+a5fBQpIs7R6UjW3p6+DM/uO+Zl+MgwdYoic+U+7lF7eNAFxHUdPALMeIrJmqbTFeurCA+ukV6 BfO9m2kVrn1OIGPENXY6BwLJN/3HR+7o8XYdcxXyl6S1yHp52UKqK39c/s4mT6NmgTWvRLpUHhww MmWd5jyTXlBOeuM61G7MGvv50jeuJCqrVwMiKA1JdX+3KNp1v47j3A55MQIDAQABo0IwQDAdBgNV HQ4EFgQUnZPGU4teyq8/nx4P5ZmVvCT2lI8wDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMC AQYwDQYJKoZIhvcNAQELBQADggEBAFis9AQOzcAN/wr91LoWXym9e2iZWEnStB03TX8nfUYGXUPG hi4+c7ImfU+TqbbEKpqrIZcUsd6M06uJFdhrJNTxFq7YpFzUf1GO7RgBsZNjvbz4YYCanrHOQnDi qX0GJX0nof5v7LMeJNrjS1UaADs1tDvZ110w/YETifLCBivtZ8SOyUOyXGsViQK8YvxO8rUzqrJv 0wqiUOP2O+guRMLbZjipM1ZI8W0bM40NjD9gN53Tym1+NH4Nn3J2ixufcv1SNUFFApYvHLKac0kh sUlHRUe072o0EclNmsxZt9YCnlpOZbWUrhvfKbAW8b8Angc6F2S1BLUjIZkKlTuXfO8= -----END CERTIFICATE----- AffirmTrust Networking ====================== -----BEGIN CERTIFICATE----- MIIDTDCCAjSgAwIBAgIIfE8EORzUmS0wDQYJKoZIhvcNAQEFBQAwRDELMAkGA1UEBhMCVVMxFDAS BgNVBAoMC0FmZmlybVRydXN0MR8wHQYDVQQDDBZBZmZpcm1UcnVzdCBOZXR3b3JraW5nMB4XDTEw MDEyOTE0MDgyNFoXDTMwMTIzMTE0MDgyNFowRDELMAkGA1UEBhMCVVMxFDASBgNVBAoMC0FmZmly bVRydXN0MR8wHQYDVQQDDBZBZmZpcm1UcnVzdCBOZXR3b3JraW5nMIIBIjANBgkqhkiG9w0BAQEF AAOCAQ8AMIIBCgKCAQEAtITMMxcua5Rsa2FSoOujz3mUTOWUgJnLVWREZY9nZOIG41w3SfYvm4SE Hi3yYJ0wTsyEheIszx6e/jarM3c1RNg1lho9Nuh6DtjVR6FqaYvZ/Ls6rnla1fTWcbuakCNrmreI dIcMHl+5ni36q1Mr3Lt2PpNMCAiMHqIjHNRqrSK6mQEubWXLviRmVSRLQESxG9fhwoXA3hA/Pe24 /PHxI1Pcv2WXb9n5QHGNfb2V1M6+oF4nI979ptAmDgAp6zxG8D1gvz9Q0twmQVGeFDdCBKNwV6gb h+0t+nvujArjqWaJGctB+d1ENmHP4ndGyH329JKBNv3bNPFyfvMMFr20FQIDAQABo0IwQDAdBgNV HQ4EFgQUBx/S55zawm6iQLSwelAQUHTEyL0wDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMC AQYwDQYJKoZIhvcNAQEFBQADggEBAIlXshZ6qML91tmbmzTCnLQyFE2npN/svqe++EPbkTfOtDIu UFUaNU52Q3Eg75N3ThVwLofDwR1t3Mu1J9QsVtFSUzpE0nPIxBsFZVpikpzuQY0x2+c06lkh1QF6 12S4ZDnNye2v7UsDSKegmQGA3GWjNq5lWUhPgkvIZfFXHeVZLgo/bNjR9eUJtGxUAArgFU2HdW23 WJZa3W3SAKD0m0i+wzekujbgfIeFlxoVot4uolu9rxj5kFDNcFn4J2dHy8egBzp90SxdbBk6ZrV9 /ZFvgrG+CJPbFEfxojfHRZ48x3evZKiT3/Zpg4Jg8klCNO1aAFSFHBY2kgxc+qatv9s= -----END CERTIFICATE----- AffirmTrust Premium =================== -----BEGIN CERTIFICATE----- MIIFRjCCAy6gAwIBAgIIbYwURrGmCu4wDQYJKoZIhvcNAQEMBQAwQTELMAkGA1UEBhMCVVMxFDAS BgNVBAoMC0FmZmlybVRydXN0MRwwGgYDVQQDDBNBZmZpcm1UcnVzdCBQcmVtaXVtMB4XDTEwMDEy OTE0MTAzNloXDTQwMTIzMTE0MTAzNlowQTELMAkGA1UEBhMCVVMxFDASBgNVBAoMC0FmZmlybVRy dXN0MRwwGgYDVQQDDBNBZmZpcm1UcnVzdCBQcmVtaXVtMIICIjANBgkqhkiG9w0BAQEFAAOCAg8A MIICCgKCAgEAxBLfqV/+Qd3d9Z+K4/as4Tx4mrzY8H96oDMq3I0gW64tb+eT2TZwamjPjlGjhVtn BKAQJG9dKILBl1fYSCkTtuG+kU3fhQxTGJoeJKJPj/CihQvL9Cl/0qRY7iZNyaqoe5rZ+jjeRFcV 5fiMyNlI4g0WJx0eyIOFJbe6qlVBzAMiSy2RjYvmia9mx+n/K+k8rNrSs8PhaJyJ+HoAVt70VZVs +7pk3WKL3wt3MutizCaam7uqYoNMtAZ6MMgpv+0GTZe5HMQxK9VfvFMSF5yZVylmd2EhMQcuJUmd GPLu8ytxjLW6OQdJd/zvLpKQBY0tL3d770O/Nbua2Plzpyzy0FfuKE4mX4+QaAkvuPjcBukumj5R p9EixAqnOEhss/n/fauGV+O61oV4d7pD6kh/9ti+I20ev9E2bFhc8e6kGVQa9QPSdubhjL08s9NI S+LI+H+SqHZGnEJlPqQewQcDWkYtuJfzt9WyVSHvutxMAJf7FJUnM7/oQ0dG0giZFmA7mn7S5u04 6uwBHjxIVkkJx0w3AJ6IDsBz4W9m6XJHMD4Q5QsDyZpCAGzFlH5hxIrff4IaC1nEWTJ3s7xgaVY5 /bQGeyzWZDbZvUjthB9+pSKPKrhC9IK31FOQeE4tGv2Bb0TXOwF0lkLgAOIua+rF7nKsu7/+6qqo +Nz2snmKtmcCAwEAAaNCMEAwHQYDVR0OBBYEFJ3AZ6YMItkm9UWrpmVSESfYRaxjMA8GA1UdEwEB /wQFMAMBAf8wDgYDVR0PAQH/BAQDAgEGMA0GCSqGSIb3DQEBDAUAA4ICAQCzV00QYk465KzquByv MiPIs0laUZx2KI15qldGF9X1Uva3ROgIRL8YhNILgM3FEv0AVQVhh0HctSSePMTYyPtwni94loMg Nt58D2kTiKV1NpgIpsbfrM7jWNa3Pt668+s0QNiigfV4Py/VpfzZotReBA4Xrf5B8OWycvpEgjNC 6C1Y91aMYj+6QrCcDFx+LmUmXFNPALJ4fqENmS2NuB2OosSw/WDQMKSOyARiqcTtNd56l+0OOF6S L5Nwpamcb6d9Ex1+xghIsV5n61EIJenmJWtSKZGc0jlzCFfemQa0W50QBuHCAKi4HEoCChTQwUHK +4w1IX2COPKpVJEZNZOUbWo6xbLQu4mGk+ibyQ86p3q4ofB4Rvr8Ny/lioTz3/4E2aFooC8k4gmV BtWVyuEklut89pMFu+1z6S3RdTnX5yTb2E5fQ4+e0BQ5v1VwSJlXMbSc7kqYA5YwH2AG7hsj/oFg IxpHYoWlzBk0gG+zrBrjn/B7SK3VAdlntqlyk+otZrWyuOQ9PLLvTIzq6we/qzWaVYa8GKa1qF60 g2xraUDTn9zxw2lrueFtCfTxqlB2Cnp9ehehVZZCmTEJ3WARjQUwfuaORtGdFNrHF+QFlozEJLUb zxQHskD4o55BhrwE0GuWyCqANP2/7waj3VjFhT0+j/6eKeC2uAloGRwYQw== -----END CERTIFICATE----- AffirmTrust Premium ECC ======================= -----BEGIN CERTIFICATE----- MIIB/jCCAYWgAwIBAgIIdJclisc/elQwCgYIKoZIzj0EAwMwRTELMAkGA1UEBhMCVVMxFDASBgNV BAoMC0FmZmlybVRydXN0MSAwHgYDVQQDDBdBZmZpcm1UcnVzdCBQcmVtaXVtIEVDQzAeFw0xMDAx MjkxNDIwMjRaFw00MDEyMzExNDIwMjRaMEUxCzAJBgNVBAYTAlVTMRQwEgYDVQQKDAtBZmZpcm1U cnVzdDEgMB4GA1UEAwwXQWZmaXJtVHJ1c3QgUHJlbWl1bSBFQ0MwdjAQBgcqhkjOPQIBBgUrgQQA IgNiAAQNMF4bFZ0D0KF5Nbc6PJJ6yhUczWLznCZcBz3lVPqj1swS6vQUX+iOGasvLkjmrBhDeKzQ N8O9ss0s5kfiGuZjuD0uL3jET9v0D6RoTFVya5UdThhClXjMNzyR4ptlKymjQjBAMB0GA1UdDgQW BBSaryl6wBE1NSZRMADDav5A1a7WPDAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIBBjAK BggqhkjOPQQDAwNnADBkAjAXCfOHiFBar8jAQr9HX/VsaobgxCd05DhT1wV/GzTjxi+zygk8N53X 57hG8f2h4nECMEJZh0PUUd+60wkyWs6Iflc9nF9Ca/UHLbXwgpP5WW+uZPpY5Yse42O+tYHNbwKM eQ== -----END CERTIFICATE----- Certum Trusted Network CA ========================= -----BEGIN CERTIFICATE----- MIIDuzCCAqOgAwIBAgIDBETAMA0GCSqGSIb3DQEBBQUAMH4xCzAJBgNVBAYTAlBMMSIwIAYDVQQK ExlVbml6ZXRvIFRlY2hub2xvZ2llcyBTLkEuMScwJQYDVQQLEx5DZXJ0dW0gQ2VydGlmaWNhdGlv biBBdXRob3JpdHkxIjAgBgNVBAMTGUNlcnR1bSBUcnVzdGVkIE5ldHdvcmsgQ0EwHhcNMDgxMDIy MTIwNzM3WhcNMjkxMjMxMTIwNzM3WjB+MQswCQYDVQQGEwJQTDEiMCAGA1UEChMZVW5pemV0byBU ZWNobm9sb2dpZXMgUy5BLjEnMCUGA1UECxMeQ2VydHVtIENlcnRpZmljYXRpb24gQXV0aG9yaXR5 MSIwIAYDVQQDExlDZXJ0dW0gVHJ1c3RlZCBOZXR3b3JrIENBMIIBIjANBgkqhkiG9w0BAQEFAAOC AQ8AMIIBCgKCAQEA4/t9o3K6wvDJFIf1awFO4W5AB7ptJ11/91sts1rHUV+rpDKmYYe2bg+G0jAC l/jXaVehGDldamR5xgFZrDwxSjh80gTSSyjoIF87B6LMTXPb865Px1bVWqeWifrzq2jUI4ZZJ88J J7ysbnKDHDBy3+Ci6dLhdHUZvSqeexVUBBvXQzmtVSjF4hq79MDkrjhJM8x2hZ85RdKknvISjFH4 fOQtf/WsX+sWn7Et0brMkUJ3TCXJkDhv2/DM+44el1k+1WBO5gUo7Ul5E0u6SNsv+XLTOcr+H9g0 cvW0QM8xAcPs3hEtF10fuFDRXhmnad4HMyjKUJX5p1TLVIZQRan5SQIDAQABo0IwQDAPBgNVHRMB Af8EBTADAQH/MB0GA1UdDgQWBBQIds3LB/8k9sXN7buQvOKEN0Z19zAOBgNVHQ8BAf8EBAMCAQYw DQYJKoZIhvcNAQEFBQADggEBAKaorSLOAT2mo/9i0Eidi15ysHhE49wcrwn9I0j6vSrEuVUEtRCj jSfeC4Jj0O7eDDd5QVsisrCaQVymcODU0HfLI9MA4GxWL+FpDQ3Zqr8hgVDZBqWo/5U30Kr+4rP1 mS1FhIrlQgnXdAIv94nYmem8J9RHjboNRhx3zxSkHLmkMcScKHQDNP8zGSal6Q10tz6XxnboJ5aj Zt3hrvJBW8qYVoNzcOSGGtIxQbovvi0TWnZvTuhOgQ4/WwMioBK+ZlgRSssDxLQqKi2WF+A5VLxI 03YnnZotBqbJ7DnSq9ufmgsnAjUpsUCV5/nonFWIGUbWtzT1fs45mtk48VH3Tyw= -----END CERTIFICATE----- TWCA Root Certification Authority ================================= -----BEGIN CERTIFICATE----- MIIDezCCAmOgAwIBAgIBATANBgkqhkiG9w0BAQUFADBfMQswCQYDVQQGEwJUVzESMBAGA1UECgwJ VEFJV0FOLUNBMRAwDgYDVQQLDAdSb290IENBMSowKAYDVQQDDCFUV0NBIFJvb3QgQ2VydGlmaWNh dGlvbiBBdXRob3JpdHkwHhcNMDgwODI4MDcyNDMzWhcNMzAxMjMxMTU1OTU5WjBfMQswCQYDVQQG EwJUVzESMBAGA1UECgwJVEFJV0FOLUNBMRAwDgYDVQQLDAdSb290IENBMSowKAYDVQQDDCFUV0NB IFJvb3QgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEK AoIBAQCwfnK4pAOU5qfeCTiRShFAh6d8WWQUe7UREN3+v9XAu1bihSX0NXIP+FPQQeFEAcK0HMMx QhZHhTMidrIKbw/lJVBPhYa+v5guEGcevhEFhgWQxFnQfHgQsIBct+HHK3XLfJ+utdGdIzdjp9xC oi2SBBtQwXu4PhvJVgSLL1KbralW6cH/ralYhzC2gfeXRfwZVzsrb+RH9JlF/h3x+JejiB03HFyP 4HYlmlD4oFT/RJB2I9IyxsOrBr/8+7/zrX2SYgJbKdM1o5OaQ2RgXbL6Mv87BK9NQGr5x+PvI/1r y+UPizgN7gr8/g+YnzAx3WxSZfmLgb4i4RxYA7qRG4kHAgMBAAGjQjBAMA4GA1UdDwEB/wQEAwIB BjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBRqOFsmjd6LWvJPelSDGRjjCDWmujANBgkqhkiG 9w0BAQUFAAOCAQEAPNV3PdrfibqHDAhUaiBQkr6wQT25JmSDCi/oQMCXKCeCMErJk/9q56YAf4lC mtYR5VPOL8zy2gXE/uJQxDqGfczafhAJO5I1KlOy/usrBdlsXebQ79NqZp4VKIV66IIArB6nCWlW QtNoURi+VJq/REG6Sb4gumlc7rh3zc5sH62Dlhh9DrUUOYTxKOkto557HnpyWoOzeW/vtPzQCqVY T0bf+215WfKEIlKuD8z7fDvnaspHYcN6+NOSBB+4IIThNlQWx0DeO4pz3N/GCUzf7Nr/1FNCocny Yh0igzyXxfkZYiesZSLX0zzG5Y6yU8xJzrww/nsOM5D77dIUkR8Hrw== -----END CERTIFICATE----- Security Communication RootCA2 ============================== -----BEGIN CERTIFICATE----- MIIDdzCCAl+gAwIBAgIBADANBgkqhkiG9w0BAQsFADBdMQswCQYDVQQGEwJKUDElMCMGA1UEChMc U0VDT00gVHJ1c3QgU3lzdGVtcyBDTy4sTFRELjEnMCUGA1UECxMeU2VjdXJpdHkgQ29tbXVuaWNh dGlvbiBSb290Q0EyMB4XDTA5MDUyOTA1MDAzOVoXDTI5MDUyOTA1MDAzOVowXTELMAkGA1UEBhMC SlAxJTAjBgNVBAoTHFNFQ09NIFRydXN0IFN5c3RlbXMgQ08uLExURC4xJzAlBgNVBAsTHlNlY3Vy aXR5IENvbW11bmljYXRpb24gUm9vdENBMjCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEB ANAVOVKxUrO6xVmCxF1SrjpDZYBLx/KWvNs2l9amZIyoXvDjChz335c9S672XewhtUGrzbl+dp++ +T42NKA7wfYxEUV0kz1XgMX5iZnK5atq1LXaQZAQwdbWQonCv/Q4EpVMVAX3NuRFg3sUZdbcDE3R 3n4MqzvEFb46VqZab3ZpUql6ucjrappdUtAtCms1FgkQhNBqyjoGADdH5H5XTz+L62e4iKrFvlNV spHEfbmwhRkGeC7bYRr6hfVKkaHnFtWOojnflLhwHyg/i/xAXmODPIMqGplrz95Zajv8bxbXH/1K EOtOghY6rCcMU/Gt1SSwawNQwS08Ft1ENCcadfsCAwEAAaNCMEAwHQYDVR0OBBYEFAqFqXdlBZh8 QIH4D5csOPEK7DzPMA4GA1UdDwEB/wQEAwIBBjAPBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3DQEB CwUAA4IBAQBMOqNErLlFsceTfsgLCkLfZOoc7llsCLqJX2rKSpWeeo8HxdpFcoJxDjrSzG+ntKEj u/Ykn8sX/oymzsLS28yN/HH8AynBbF0zX2S2ZTuJbxh2ePXcokgfGT+Ok+vx+hfuzU7jBBJV1uXk 3fs+BXziHV7Gp7yXT2g69ekuCkO2r1dcYmh8t/2jioSgrGK+KwmHNPBqAbubKVY8/gA3zyNs8U6q tnRGEmyR7jTV7JqR50S+kDFy1UkC9gLl9B/rfNmWVan/7Ir5mUf/NVoCqgTLiluHcSmRvaS0eg29 mvVXIwAHIRc/SjnRBUkLp7Y3gaVdjKozXoEofKd9J+sAro03 -----END CERTIFICATE----- EC-ACC ====== -----BEGIN CERTIFICATE----- MIIFVjCCBD6gAwIBAgIQ7is969Qh3hSoYqwE893EATANBgkqhkiG9w0BAQUFADCB8zELMAkGA1UE BhMCRVMxOzA5BgNVBAoTMkFnZW5jaWEgQ2F0YWxhbmEgZGUgQ2VydGlmaWNhY2lvIChOSUYgUS0w ODAxMTc2LUkpMSgwJgYDVQQLEx9TZXJ2ZWlzIFB1YmxpY3MgZGUgQ2VydGlmaWNhY2lvMTUwMwYD VQQLEyxWZWdldSBodHRwczovL3d3dy5jYXRjZXJ0Lm5ldC92ZXJhcnJlbCAoYykwMzE1MDMGA1UE CxMsSmVyYXJxdWlhIEVudGl0YXRzIGRlIENlcnRpZmljYWNpbyBDYXRhbGFuZXMxDzANBgNVBAMT BkVDLUFDQzAeFw0wMzAxMDcyMzAwMDBaFw0zMTAxMDcyMjU5NTlaMIHzMQswCQYDVQQGEwJFUzE7 MDkGA1UEChMyQWdlbmNpYSBDYXRhbGFuYSBkZSBDZXJ0aWZpY2FjaW8gKE5JRiBRLTA4MDExNzYt SSkxKDAmBgNVBAsTH1NlcnZlaXMgUHVibGljcyBkZSBDZXJ0aWZpY2FjaW8xNTAzBgNVBAsTLFZl Z2V1IGh0dHBzOi8vd3d3LmNhdGNlcnQubmV0L3ZlcmFycmVsIChjKTAzMTUwMwYDVQQLEyxKZXJh cnF1aWEgRW50aXRhdHMgZGUgQ2VydGlmaWNhY2lvIENhdGFsYW5lczEPMA0GA1UEAxMGRUMtQUND MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAsyLHT+KXQpWIR4NA9h0X84NzJB5R85iK w5K4/0CQBXCHYMkAqbWUZRkiFRfCQ2xmRJoNBD45b6VLeqpjt4pEndljkYRm4CgPukLjbo73FCeT ae6RDqNfDrHrZqJyTxIThmV6PttPB/SnCWDaOkKZx7J/sxaVHMf5NLWUhdWZXqBIoH7nF2W4onW4 HvPlQn2v7fOKSGRdghST2MDk/7NQcvJ29rNdQlB50JQ+awwAvthrDk4q7D7SzIKiGGUzE3eeml0a E9jD2z3Il3rucO2n5nzbcc8tlGLfbdb1OL4/pYUKGbio2Al1QnDE6u/LDsg0qBIimAy4E5S2S+zw 0JDnJwIDAQABo4HjMIHgMB0GA1UdEQQWMBSBEmVjX2FjY0BjYXRjZXJ0Lm5ldDAPBgNVHRMBAf8E BTADAQH/MA4GA1UdDwEB/wQEAwIBBjAdBgNVHQ4EFgQUoMOLRKo3pUW/l4Ba0fF4opvpXY0wfwYD VR0gBHgwdjB0BgsrBgEEAfV4AQMBCjBlMCwGCCsGAQUFBwIBFiBodHRwczovL3d3dy5jYXRjZXJ0 Lm5ldC92ZXJhcnJlbDA1BggrBgEFBQcCAjApGidWZWdldSBodHRwczovL3d3dy5jYXRjZXJ0Lm5l dC92ZXJhcnJlbCAwDQYJKoZIhvcNAQEFBQADggEBAKBIW4IB9k1IuDlVNZyAelOZ1Vr/sXE7zDkJ lF7W2u++AVtd0x7Y/X1PzaBB4DSTv8vihpw3kpBWHNzrKQXlxJ7HNd+KDM3FIUPpqojlNcAZQmNa Al6kSBg6hW/cnbw/nZzBh7h6YQjpdwt/cKt63dmXLGQehb+8dJahw3oS7AwaboMMPOhyRp/7SNVe l+axofjk70YllJyJ22k4vuxcDlbHZVHlUIiIv0LVKz3l+bqeLrPK9HOSAgu+TGbrIP65y7WZf+a2 E/rKS03Z7lNGBjvGTq2TWoF+bCpLagVFjPIhpDGQh2xlnJ2lYJU6Un/10asIbvPuW/mIPX64b24D 5EI= -----END CERTIFICATE----- Hellenic Academic and Research Institutions RootCA 2011 ======================================================= -----BEGIN CERTIFICATE----- MIIEMTCCAxmgAwIBAgIBADANBgkqhkiG9w0BAQUFADCBlTELMAkGA1UEBhMCR1IxRDBCBgNVBAoT O0hlbGxlbmljIEFjYWRlbWljIGFuZCBSZXNlYXJjaCBJbnN0aXR1dGlvbnMgQ2VydC4gQXV0aG9y aXR5MUAwPgYDVQQDEzdIZWxsZW5pYyBBY2FkZW1pYyBhbmQgUmVzZWFyY2ggSW5zdGl0dXRpb25z IFJvb3RDQSAyMDExMB4XDTExMTIwNjEzNDk1MloXDTMxMTIwMTEzNDk1MlowgZUxCzAJBgNVBAYT AkdSMUQwQgYDVQQKEztIZWxsZW5pYyBBY2FkZW1pYyBhbmQgUmVzZWFyY2ggSW5zdGl0dXRpb25z IENlcnQuIEF1dGhvcml0eTFAMD4GA1UEAxM3SGVsbGVuaWMgQWNhZGVtaWMgYW5kIFJlc2VhcmNo IEluc3RpdHV0aW9ucyBSb290Q0EgMjAxMTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEB AKlTAOMupvaO+mDYLZU++CwqVE7NuYRhlFhPjz2L5EPzdYmNUeTDN9KKiE15HrcS3UN4SoqS5tdI 1Q+kOilENbgH9mgdVc04UfCMJDGFr4PJfel3r+0ae50X+bOdOFAPplp5kYCvN66m0zH7tSYJnTxa 71HFK9+WXesyHgLacEnsbgzImjeN9/E2YEsmLIKe0HjzDQ9jpFEw4fkrJxIH2Oq9GGKYsFk3fb7u 8yBRQlqD75O6aRXxYp2fmTmCobd0LovUxQt7L/DICto9eQqakxylKHJzkUOap9FNhYS5qXSPFEDH 3N6sQWRstBmbAmNtJGSPRLIl6s5ddAxjMlyNh+UCAwEAAaOBiTCBhjAPBgNVHRMBAf8EBTADAQH/ MAsGA1UdDwQEAwIBBjAdBgNVHQ4EFgQUppFC/RNhSiOeCKQp5dgTBCPuQSUwRwYDVR0eBEAwPqA8 MAWCAy5ncjAFggMuZXUwBoIELmVkdTAGggQub3JnMAWBAy5ncjAFgQMuZXUwBoEELmVkdTAGgQQu b3JnMA0GCSqGSIb3DQEBBQUAA4IBAQAf73lB4XtuP7KMhjdCSk4cNx6NZrokgclPEg8hwAOXhiVt XdMiKahsog2p6z0GW5k6x8zDmjR/qw7IThzh+uTczQ2+vyT+bOdrwg3IBp5OjWEopmr95fZi6hg8 TqBTnbI6nOulnJEWtk2C4AwFSKls9cz4y51JtPACpf1wA+2KIaWuE4ZJwzNzvoc7dIsXRSZMFpGD /md9zU1jZ/rzAxKWeAaNsWftjj++n08C9bMJL/NMh98qy5V8AcysNnq/onN694/BtZqhFLKPM58N 7yLcZnuEvUUXBj08yrl3NI/K6s8/MT7jiOOASSXIl7WdmplNsDz4SgCbZN2fOUvRJ9e4 -----END CERTIFICATE----- Actalis Authentication Root CA ============================== -----BEGIN CERTIFICATE----- MIIFuzCCA6OgAwIBAgIIVwoRl0LE48wwDQYJKoZIhvcNAQELBQAwazELMAkGA1UEBhMCSVQxDjAM BgNVBAcMBU1pbGFuMSMwIQYDVQQKDBpBY3RhbGlzIFMucC5BLi8wMzM1ODUyMDk2NzEnMCUGA1UE AwweQWN0YWxpcyBBdXRoZW50aWNhdGlvbiBSb290IENBMB4XDTExMDkyMjExMjIwMloXDTMwMDky MjExMjIwMlowazELMAkGA1UEBhMCSVQxDjAMBgNVBAcMBU1pbGFuMSMwIQYDVQQKDBpBY3RhbGlz IFMucC5BLi8wMzM1ODUyMDk2NzEnMCUGA1UEAwweQWN0YWxpcyBBdXRoZW50aWNhdGlvbiBSb290 IENBMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAp8bEpSmkLO/lGMWwUKNvUTufClrJ wkg4CsIcoBh/kbWHuUA/3R1oHwiD1S0eiKD4j1aPbZkCkpAW1V8IbInX4ay8IMKx4INRimlNAJZa by/ARH6jDuSRzVju3PvHHkVH3Se5CAGfpiEd9UEtL0z9KK3giq0itFZljoZUj5NDKd45RnijMCO6 zfB9E1fAXdKDa0hMxKufgFpbOr3JpyI/gCczWw63igxdBzcIy2zSekciRDXFzMwujt0q7bd9Zg1f YVEiVRvjRuPjPdA1YprbrxTIW6HMiRvhMCb8oJsfgadHHwTrozmSBp+Z07/T6k9QnBn+locePGX2 oxgkg4YQ51Q+qDp2JE+BIcXjDwL4k5RHILv+1A7TaLndxHqEguNTVHnd25zS8gebLra8Pu2Fbe8l EfKXGkJh90qX6IuxEAf6ZYGyojnP9zz/GPvG8VqLWeICrHuS0E4UT1lF9gxeKF+w6D9Fz8+vm2/7 hNN3WpVvrJSEnu68wEqPSpP4RCHiMUVhUE4Q2OM1fEwZtN4Fv6MGn8i1zeQf1xcGDXqVdFUNaBr8 EBtiZJ1t4JWgw5QHVw0U5r0F+7if5t+L4sbnfpb2U8WANFAoWPASUHEXMLrmeGO89LKtmyuy/uE5 jF66CyCU3nuDuP/jVo23Eek7jPKxwV2dpAtMK9myGPW1n0sCAwEAAaNjMGEwHQYDVR0OBBYEFFLY iDrIn3hm7YnzezhwlMkCAjbQMA8GA1UdEwEB/wQFMAMBAf8wHwYDVR0jBBgwFoAUUtiIOsifeGbt ifN7OHCUyQICNtAwDgYDVR0PAQH/BAQDAgEGMA0GCSqGSIb3DQEBCwUAA4ICAQALe3KHwGCmSUyI WOYdiPcUZEim2FgKDk8TNd81HdTtBjHIgT5q1d07GjLukD0R0i70jsNjLiNmsGe+b7bAEzlgqqI0 JZN1Ut6nna0Oh4lScWoWPBkdg/iaKWW+9D+a2fDzWochcYBNy+A4mz+7+uAwTc+G02UQGRjRlwKx K3JCaKygvU5a2hi/a5iB0P2avl4VSM0RFbnAKVy06Ij3Pjaut2L9HmLecHgQHEhb2rykOLpn7VU+ Xlff1ANATIGk0k9jpwlCCRT8AKnCgHNPLsBA2RF7SOp6AsDT6ygBJlh0wcBzIm2Tlf05fbsq4/aC 4yyXX04fkZT6/iyj2HYauE2yOE+b+h1IYHkm4vP9qdCa6HCPSXrW5b0KDtst842/6+OkfcvHlXHo 2qN8xcL4dJIEG4aspCJTQLas/kx2z/uUMsA1n3Y/buWQbqCmJqK4LL7RK4X9p2jIugErsWx0Hbhz lefut8cl8ABMALJ+tguLHPPAUJ4lueAI3jZm/zel0btUZCzJJ7VLkn5l/9Mt4blOvH+kQSGQQXem OR/qnuOf0GZvBeyqdn6/axag67XH/JJULysRJyU3eExRarDzzFhdFPFqSBX/wge2sY0PjlxQRrM9 vwGYT7JZVEc+NHt4bVaTLnPqZih4zR0Uv6CPLy64Lo7yFIrM6bV8+2ydDKXhlg== -----END CERTIFICATE----- Trustis FPS Root CA =================== -----BEGIN CERTIFICATE----- MIIDZzCCAk+gAwIBAgIQGx+ttiD5JNM2a/fH8YygWTANBgkqhkiG9w0BAQUFADBFMQswCQYDVQQG EwJHQjEYMBYGA1UEChMPVHJ1c3RpcyBMaW1pdGVkMRwwGgYDVQQLExNUcnVzdGlzIEZQUyBSb290 IENBMB4XDTAzMTIyMzEyMTQwNloXDTI0MDEyMTExMzY1NFowRTELMAkGA1UEBhMCR0IxGDAWBgNV BAoTD1RydXN0aXMgTGltaXRlZDEcMBoGA1UECxMTVHJ1c3RpcyBGUFMgUm9vdCBDQTCCASIwDQYJ KoZIhvcNAQEBBQADggEPADCCAQoCggEBAMVQe547NdDfxIzNjpvto8A2mfRC6qc+gIMPpqdZh8mQ RUN+AOqGeSoDvT03mYlmt+WKVoaTnGhLaASMk5MCPjDSNzoiYYkchU59j9WvezX2fihHiTHcDnlk H5nSW7r+f2C/revnPDgpai/lkQtV/+xvWNUtyd5MZnGPDNcE2gfmHhjjvSkCqPoc4Vu5g6hBSLwa cY3nYuUtsuvffM/bq1rKMfFMIvMFE/eC+XN5DL7XSxzA0RU8k0Fk0ea+IxciAIleH2ulrG6nS4zt o3Lmr2NNL4XSFDWaLk6M6jKYKIahkQlBOrTh4/L68MkKokHdqeMDx4gVOxzUGpTXn2RZEm0CAwEA AaNTMFEwDwYDVR0TAQH/BAUwAwEB/zAfBgNVHSMEGDAWgBS6+nEleYtXQSUhhgtx67JkDoshZzAd BgNVHQ4EFgQUuvpxJXmLV0ElIYYLceuyZA6LIWcwDQYJKoZIhvcNAQEFBQADggEBAH5Y//01GX2c GE+esCu8jowU/yyg2kdbw++BLa8F6nRIW/M+TgfHbcWzk88iNVy2P3UnXwmWzaD+vkAMXBJV+JOC yinpXj9WV4s4NvdFGkwozZ5BuO1WTISkQMi4sKUraXAEasP41BIy+Q7DsdwyhEQsb8tGD+pmQQ9P 8Vilpg0ND2HepZ5dfWWhPBfnqFVO76DH7cZEf1T1o+CP8HxVIo8ptoGj4W1OLBuAZ+ytIJ8MYmHV l/9D7S3B2l0pKoU/rGXuhg8FjZBf3+6f9L/uHfuY5H+QK4R4EA5sSVPvFVtlRkpdr7r7OnIdzfYl iB6XzCGcKQENZetX2fNXlrtIzYE= -----END CERTIFICATE----- Buypass Class 2 Root CA ======================= -----BEGIN CERTIFICATE----- MIIFWTCCA0GgAwIBAgIBAjANBgkqhkiG9w0BAQsFADBOMQswCQYDVQQGEwJOTzEdMBsGA1UECgwU QnV5cGFzcyBBUy05ODMxNjMzMjcxIDAeBgNVBAMMF0J1eXBhc3MgQ2xhc3MgMiBSb290IENBMB4X DTEwMTAyNjA4MzgwM1oXDTQwMTAyNjA4MzgwM1owTjELMAkGA1UEBhMCTk8xHTAbBgNVBAoMFEJ1 eXBhc3MgQVMtOTgzMTYzMzI3MSAwHgYDVQQDDBdCdXlwYXNzIENsYXNzIDIgUm9vdCBDQTCCAiIw DQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBANfHXvfBB9R3+0Mh9PT1aeTuMgHbo4Yf5FkNuud1 g1Lr6hxhFUi7HQfKjK6w3Jad6sNgkoaCKHOcVgb/S2TwDCo3SbXlzwx87vFKu3MwZfPVL4O2fuPn 9Z6rYPnT8Z2SdIrkHJasW4DptfQxh6NR/Md+oW+OU3fUl8FVM5I+GC911K2GScuVr1QGbNgGE41b /+EmGVnAJLqBcXmQRFBoJJRfuLMR8SlBYaNByyM21cHxMlAQTn/0hpPshNOOvEu/XAFOBz3cFIqU CqTqc/sLUegTBxj6DvEr0VQVfTzh97QZQmdiXnfgolXsttlpF9U6r0TtSsWe5HonfOV116rLJeff awrbD02TTqigzXsu8lkBarcNuAeBfos4GzjmCleZPe4h6KP1DBbdi+w0jpwqHAAVF41og9JwnxgI zRFo1clrUs3ERo/ctfPYV3Me6ZQ5BL/T3jjetFPsaRyifsSP5BtwrfKi+fv3FmRmaZ9JUaLiFRhn Bkp/1Wy1TbMz4GHrXb7pmA8y1x1LPC5aAVKRCfLf6o3YBkBjqhHk/sM3nhRSP/TizPJhk9H9Z2vX Uq6/aKtAQ6BXNVN48FP4YUIHZMbXb5tMOA1jrGKvNouicwoN9SG9dKpN6nIDSdvHXx1iY8f93ZHs M+71bbRuMGjeyNYmsHVee7QHIJihdjK4TWxPAgMBAAGjQjBAMA8GA1UdEwEB/wQFMAMBAf8wHQYD VR0OBBYEFMmAd+BikoL1RpzzuvdMw964o605MA4GA1UdDwEB/wQEAwIBBjANBgkqhkiG9w0BAQsF AAOCAgEAU18h9bqwOlI5LJKwbADJ784g7wbylp7ppHR/ehb8t/W2+xUbP6umwHJdELFx7rxP462s A20ucS6vxOOto70MEae0/0qyexAQH6dXQbLArvQsWdZHEIjzIVEpMMpghq9Gqx3tOluwlN5E40EI osHsHdb9T7bWR9AUC8rmyrV7d35BH16Dx7aMOZawP5aBQW9gkOLo+fsicdl9sz1Gv7SEr5AcD48S aq/v7h56rgJKihcrdv6sVIkkLE8/trKnToyokZf7KcZ7XC25y2a2t6hbElGFtQl+Ynhw/qlqYLYd DnkM/crqJIByw5c/8nerQyIKx+u2DISCLIBrQYoIwOula9+ZEsuK1V6ADJHgJgg2SMX6OBE1/yWD LfJ6v9r9jv6ly0UsH8SIU653DtmadsWOLB2jutXsMq7Aqqz30XpN69QH4kj3Io6wpJ9qzo6ysmD0 oyLQI+uUWnpp3Q+/QFesa1lQ2aOZ4W7+jQF5JyMV3pKdewlNWudLSDBaGOYKbeaP4NK75t98biGC wWg5TbSYWGZizEqQXsP6JwSxeRV0mcy+rSDeJmAc61ZRpqPq5KM/p/9h3PFaTWwyI0PurKju7koS CTxdccK+efrCh2gdC/1cacwG0Jp9VJkqyTkaGa9LKkPzY11aWOIv4x3kqdbQCtCev9eBCfHJxyYN rJgWVqA= -----END CERTIFICATE----- Buypass Class 3 Root CA ======================= -----BEGIN CERTIFICATE----- MIIFWTCCA0GgAwIBAgIBAjANBgkqhkiG9w0BAQsFADBOMQswCQYDVQQGEwJOTzEdMBsGA1UECgwU QnV5cGFzcyBBUy05ODMxNjMzMjcxIDAeBgNVBAMMF0J1eXBhc3MgQ2xhc3MgMyBSb290IENBMB4X DTEwMTAyNjA4Mjg1OFoXDTQwMTAyNjA4Mjg1OFowTjELMAkGA1UEBhMCTk8xHTAbBgNVBAoMFEJ1 eXBhc3MgQVMtOTgzMTYzMzI3MSAwHgYDVQQDDBdCdXlwYXNzIENsYXNzIDMgUm9vdCBDQTCCAiIw DQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAKXaCpUWUOOV8l6ddjEGMnqb8RB2uACatVI2zSRH sJ8YZLya9vrVediQYkwiL944PdbgqOkcLNt4EemOaFEVcsfzM4fkoF0LXOBXByow9c3EN3coTRiR 5r/VUv1xLXA+58bEiuPwKAv0dpihi4dVsjoT/Lc+JzeOIuOoTyrvYLs9tznDDgFHmV0ST9tD+leh 7fmdvhFHJlsTmKtdFoqwNxxXnUX/iJY2v7vKB3tvh2PX0DJq1l1sDPGzbjniazEuOQAnFN44wOwZ ZoYS6J1yFhNkUsepNxz9gjDthBgd9K5c/3ATAOux9TN6S9ZV+AWNS2mw9bMoNlwUxFFzTWsL8TQH 2xc519woe2v1n/MuwU8XKhDzzMro6/1rqy6any2CbgTUUgGTLT2G/H783+9CHaZr77kgxve9oKeV /afmiSTYzIw0bOIjL9kSGiG5VZFvC5F5GQytQIgLcOJ60g7YaEi7ghM5EFjp2CoHxhLbWNvSO1UQ RwUVZ2J+GGOmRj8JDlQyXr8NYnon74Do29lLBlo3WiXQCBJ31G8JUJc9yB3D34xFMFbG02SrZvPA Xpacw8Tvw3xrizp5f7NJzz3iiZ+gMEuFuZyUJHmPfWupRWgPK9Dx2hzLabjKSWJtyNBjYt1gD1iq j6G8BaVmos8bdrKEZLFMOVLAMLrwjEsCsLa3AgMBAAGjQjBAMA8GA1UdEwEB/wQFMAMBAf8wHQYD VR0OBBYEFEe4zf/lb+74suwvTg75JbCOPGvDMA4GA1UdDwEB/wQEAwIBBjANBgkqhkiG9w0BAQsF AAOCAgEAACAjQTUEkMJAYmDv4jVM1z+s4jSQuKFvdvoWFqRINyzpkMLyPPgKn9iB5btb2iUspKdV cSQy9sgL8rxq+JOssgfCX5/bzMiKqr5qb+FJEMwx14C7u8jYog5kV+qi9cKpMRXSIGrs/CIBKM+G uIAeqcwRpTzyFrNHnfzSgCHEy9BHcEGhyoMZCCxt8l13nIoUE9Q2HJLw5QY33KbmkJs4j1xrG0aG Q0JfPgEHU1RdZX33inOhmlRaHylDFCfChQ+1iHsaO5S3HWCntZznKWlXWpuTekMwGwPXYshApqr8 ZORK15FTAaggiG6cX0S5y2CBNOxv033aSF/rtJC8LakcC6wc1aJoIIAE1vyxjy+7SjENSoYc6+I2 KSb12tjE8nVhz36udmNKekBlk4f4HoCMhuWG1o8O/FMsYOgWYRqiPkN7zTlgVGr18okmAWiDSKIz 6MkEkbIRNBE+6tBDGR8Dk5AM/1E9V/RBbuHLoL7ryWPNbczk+DaqaJ3tvV2XcEQNtg413OEMXbug UZTLfhbrES+jkkXITHHZvMmZUldGL1DPvTVp9D0VzgalLA8+9oG6lLvDu79leNKGef9JOxqDDPDe eOzI8k1MGt6CKfjBWtrt7uYnXuhF0J0cUahoq0Tj0Itq4/g7u9xN12TyUb7mqqta6THuBrxzvxNi Cp/HuZc= -----END CERTIFICATE----- T-TeleSec GlobalRoot Class 3 ============================ -----BEGIN CERTIFICATE----- MIIDwzCCAqugAwIBAgIBATANBgkqhkiG9w0BAQsFADCBgjELMAkGA1UEBhMCREUxKzApBgNVBAoM IlQtU3lzdGVtcyBFbnRlcnByaXNlIFNlcnZpY2VzIEdtYkgxHzAdBgNVBAsMFlQtU3lzdGVtcyBU cnVzdCBDZW50ZXIxJTAjBgNVBAMMHFQtVGVsZVNlYyBHbG9iYWxSb290IENsYXNzIDMwHhcNMDgx MDAxMTAyOTU2WhcNMzMxMDAxMjM1OTU5WjCBgjELMAkGA1UEBhMCREUxKzApBgNVBAoMIlQtU3lz dGVtcyBFbnRlcnByaXNlIFNlcnZpY2VzIEdtYkgxHzAdBgNVBAsMFlQtU3lzdGVtcyBUcnVzdCBD ZW50ZXIxJTAjBgNVBAMMHFQtVGVsZVNlYyBHbG9iYWxSb290IENsYXNzIDMwggEiMA0GCSqGSIb3 DQEBAQUAA4IBDwAwggEKAoIBAQC9dZPwYiJvJK7genasfb3ZJNW4t/zN8ELg63iIVl6bmlQdTQyK 9tPPcPRStdiTBONGhnFBSivwKixVA9ZIw+A5OO3yXDw/RLyTPWGrTs0NvvAgJ1gORH8EGoel15YU NpDQSXuhdfsaa3Ox+M6pCSzyU9XDFES4hqX2iys52qMzVNn6chr3IhUciJFrf2blw2qAsCTz34ZF iP0Zf3WHHx+xGwpzJFu5ZeAsVMhg02YXP+HMVDNzkQI6pn97djmiH5a2OK61yJN0HZ65tOVgnS9W 0eDrXltMEnAMbEQgqxHY9Bn20pxSN+f6tsIxO0rUFJmtxxr1XV/6B7h8DR/Wgx6zAgMBAAGjQjBA MA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgEGMB0GA1UdDgQWBBS1A/d2O2GCahKqGFPr AyGUv/7OyjANBgkqhkiG9w0BAQsFAAOCAQEAVj3vlNW92nOyWL6ukK2YJ5f+AbGwUgC4TeQbIXQb fsDuXmkqJa9c1h3a0nnJ85cp4IaH3gRZD/FZ1GSFS5mvJQQeyUapl96Cshtwn5z2r3Ex3XsFpSzT ucpH9sry9uetuUg/vBa3wW306gmv7PO15wWeph6KU1HWk4HMdJP2udqmJQV0eVp+QD6CSyYRMG7h P0HHRwA11fXT91Q+gT3aSWqas+8QPebrb9HIIkfLzM8BMZLZGOMivgkeGj5asuRrDFR6fUNOuIml e9eiPZaGzPImNC1qkp2aGtAw4l1OBLBfiyB+d8E9lYLRRpo7PHi4b6HQDWSieB4pTpPDpFQUWw== -----END CERTIFICATE----- D-TRUST Root Class 3 CA 2 2009 ============================== -----BEGIN CERTIFICATE----- MIIEMzCCAxugAwIBAgIDCYPzMA0GCSqGSIb3DQEBCwUAME0xCzAJBgNVBAYTAkRFMRUwEwYDVQQK DAxELVRydXN0IEdtYkgxJzAlBgNVBAMMHkQtVFJVU1QgUm9vdCBDbGFzcyAzIENBIDIgMjAwOTAe Fw0wOTExMDUwODM1NThaFw0yOTExMDUwODM1NThaME0xCzAJBgNVBAYTAkRFMRUwEwYDVQQKDAxE LVRydXN0IEdtYkgxJzAlBgNVBAMMHkQtVFJVU1QgUm9vdCBDbGFzcyAzIENBIDIgMjAwOTCCASIw DQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBANOySs96R+91myP6Oi/WUEWJNTrGa9v+2wBoqOAD ER03UAifTUpolDWzU9GUY6cgVq/eUXjsKj3zSEhQPgrfRlWLJ23DEE0NkVJD2IfgXU42tSHKXzlA BF9bfsyjxiupQB7ZNoTWSPOSHjRGICTBpFGOShrvUD9pXRl/RcPHAY9RySPocq60vFYJfxLLHLGv KZAKyVXMD9O0Gu1HNVpK7ZxzBCHQqr0ME7UAyiZsxGsMlFqVlNpQmvH/pStmMaTJOKDfHR+4CS7z p+hnUquVH+BGPtikw8paxTGA6Eian5Rp/hnd2HN8gcqW3o7tszIFZYQ05ub9VxC1X3a/L7AQDcUC AwEAAaOCARowggEWMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFP3aFMSfMN4hvR5COfyrYyNJ 4PGEMA4GA1UdDwEB/wQEAwIBBjCB0wYDVR0fBIHLMIHIMIGAoH6gfIZ6bGRhcDovL2RpcmVjdG9y eS5kLXRydXN0Lm5ldC9DTj1ELVRSVVNUJTIwUm9vdCUyMENsYXNzJTIwMyUyMENBJTIwMiUyMDIw MDksTz1ELVRydXN0JTIwR21iSCxDPURFP2NlcnRpZmljYXRlcmV2b2NhdGlvbmxpc3QwQ6BBoD+G PWh0dHA6Ly93d3cuZC10cnVzdC5uZXQvY3JsL2QtdHJ1c3Rfcm9vdF9jbGFzc18zX2NhXzJfMjAw OS5jcmwwDQYJKoZIhvcNAQELBQADggEBAH+X2zDI36ScfSF6gHDOFBJpiBSVYEQBrLLpME+bUMJm 2H6NMLVwMeniacfzcNsgFYbQDfC+rAF1hM5+n02/t2A7nPPKHeJeaNijnZflQGDSNiH+0LS4F9p0 o3/U37CYAqxva2ssJSRyoWXuJVrl5jLn8t+rSfrzkGkj2wTZ51xY/GXUl77M/C4KzCUqNQT4YJEV dT1B/yMfGchs64JTBKbkTCJNjYy6zltz7GRUUG3RnFX7acM2w4y8PIWmawomDeCTmGCufsYkl4ph X5GOZpIJhzbNi5stPvZR1FDUWSi9g/LMKHtThm3YJohw1+qRzT65ysCQblrGXnRl11z+o+I= -----END CERTIFICATE----- D-TRUST Root Class 3 CA 2 EV 2009 ================================= -----BEGIN CERTIFICATE----- MIIEQzCCAyugAwIBAgIDCYP0MA0GCSqGSIb3DQEBCwUAMFAxCzAJBgNVBAYTAkRFMRUwEwYDVQQK DAxELVRydXN0IEdtYkgxKjAoBgNVBAMMIUQtVFJVU1QgUm9vdCBDbGFzcyAzIENBIDIgRVYgMjAw OTAeFw0wOTExMDUwODUwNDZaFw0yOTExMDUwODUwNDZaMFAxCzAJBgNVBAYTAkRFMRUwEwYDVQQK DAxELVRydXN0IEdtYkgxKjAoBgNVBAMMIUQtVFJVU1QgUm9vdCBDbGFzcyAzIENBIDIgRVYgMjAw OTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAJnxhDRwui+3MKCOvXwEz75ivJn9gpfS egpnljgJ9hBOlSJzmY3aFS3nBfwZcyK3jpgAvDw9rKFs+9Z5JUut8Mxk2og+KbgPCdM03TP1YtHh zRnp7hhPTFiu4h7WDFsVWtg6uMQYZB7jM7K1iXdODL/ZlGsTl28So/6ZqQTMFexgaDbtCHu39b+T 7WYxg4zGcTSHThfqr4uRjRxWQa4iN1438h3Z0S0NL2lRp75mpoo6Kr3HGrHhFPC+Oh25z1uxav60 sUYgovseO3Dvk5h9jHOW8sXvhXCtKSb8HgQ+HKDYD8tSg2J87otTlZCpV6LqYQXY+U3EJ/pure35 11H3a6UCAwEAAaOCASQwggEgMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFNOUikxiEyoZLsyv cop9NteaHNxnMA4GA1UdDwEB/wQEAwIBBjCB3QYDVR0fBIHVMIHSMIGHoIGEoIGBhn9sZGFwOi8v ZGlyZWN0b3J5LmQtdHJ1c3QubmV0L0NOPUQtVFJVU1QlMjBSb290JTIwQ2xhc3MlMjAzJTIwQ0El MjAyJTIwRVYlMjAyMDA5LE89RC1UcnVzdCUyMEdtYkgsQz1ERT9jZXJ0aWZpY2F0ZXJldm9jYXRp b25saXN0MEagRKBChkBodHRwOi8vd3d3LmQtdHJ1c3QubmV0L2NybC9kLXRydXN0X3Jvb3RfY2xh c3NfM19jYV8yX2V2XzIwMDkuY3JsMA0GCSqGSIb3DQEBCwUAA4IBAQA07XtaPKSUiO8aEXUHL7P+ PPoeUSbrh/Yp3uDx1MYkCenBz1UbtDDZzhr+BlGmFaQt77JLvyAoJUnRpjZ3NOhk31KxEcdzes05 nsKtjHEh8lprr988TlWvsoRlFIm5d8sqMb7Po23Pb0iUMkZv53GMoKaEGTcH8gNFCSuGdXzfX2lX ANtu2KZyIktQ1HWYVt+3GP9DQ1CuekR78HlR10M9p9OB0/DJT7naxpeG0ILD5EJt/rDiZE4OJudA NCa1CInXCGNjOCd1HjPqbqjdn5lPdE2BiYBL3ZqXKVwvvoFBuYz/6n1gBp7N1z3TLqMVvKjmJuVv w9y4AyHqnxbxLFS1 -----END CERTIFICATE----- CA Disig Root R2 ================ -----BEGIN CERTIFICATE----- MIIFaTCCA1GgAwIBAgIJAJK4iNuwisFjMA0GCSqGSIb3DQEBCwUAMFIxCzAJBgNVBAYTAlNLMRMw EQYDVQQHEwpCcmF0aXNsYXZhMRMwEQYDVQQKEwpEaXNpZyBhLnMuMRkwFwYDVQQDExBDQSBEaXNp ZyBSb290IFIyMB4XDTEyMDcxOTA5MTUzMFoXDTQyMDcxOTA5MTUzMFowUjELMAkGA1UEBhMCU0sx EzARBgNVBAcTCkJyYXRpc2xhdmExEzARBgNVBAoTCkRpc2lnIGEucy4xGTAXBgNVBAMTEENBIERp c2lnIFJvb3QgUjIwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCio8QACdaFXS1tFPbC w3OeNcJxVX6B+6tGUODBfEl45qt5WDza/3wcn9iXAng+a0EE6UG9vgMsRfYvZNSrXaNHPWSb6Wia xswbP7q+sos0Ai6YVRn8jG+qX9pMzk0DIaPY0jSTVpbLTAwAFjxfGs3Ix2ymrdMxp7zo5eFm1tL7 A7RBZckQrg4FY8aAamkw/dLukO8NJ9+flXP04SXabBbeQTg06ov80egEFGEtQX6sx3dOy1FU+16S GBsEWmjGycT6txOgmLcRK7fWV8x8nhfRyyX+hk4kLlYMeE2eARKmK6cBZW58Yh2EhN/qwGu1pSqV g8NTEQxzHQuyRpDRQjrOQG6Vrf/GlK1ul4SOfW+eioANSW1z4nuSHsPzwfPrLgVv2RvPN3YEyLRa 5Beny912H9AZdugsBbPWnDTYltxhh5EF5EQIM8HauQhl1K6yNg3ruji6DOWbnuuNZt2Zz9aJQfYE koopKW1rOhzndX0CcQ7zwOe9yxndnWCywmZgtrEE7snmhrmaZkCo5xHtgUUDi/ZnWejBBhG93c+A Ak9lQHhcR1DIm+YfgXvkRKhbhZri3lrVx/k6RGZL5DJUfORsnLMOPReisjQS1n6yqEm70XooQL6i Fh/f5DcfEXP7kAplQ6INfPgGAVUzfbANuPT1rqVCV3w2EYx7XsQDnYx5nQIDAQABo0IwQDAPBgNV HRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIBBjAdBgNVHQ4EFgQUtZn4r7CU9eMg1gqtzk5WpC5u Qu0wDQYJKoZIhvcNAQELBQADggIBACYGXnDnZTPIgm7ZnBc6G3pmsgH2eDtpXi/q/075KMOYKmFM tCQSin1tERT3nLXK5ryeJ45MGcipvXrA1zYObYVybqjGom32+nNjf7xueQgcnYqfGopTpti72TVV sRHFqQOzVju5hJMiXn7B9hJSi+osZ7z+Nkz1uM/Rs0mSO9MpDpkblvdhuDvEK7Z4bLQjb/D907Je dR+Zlais9trhxTF7+9FGs9K8Z7RiVLoJ92Owk6Ka+elSLotgEqv89WBW7xBci8QaQtyDW2QOy7W8 1k/BfDxujRNt+3vrMNDcTa/F1balTFtxyegxvug4BkihGuLq0t4SOVga/4AOgnXmt8kHbA7v/zjx mHHEt38OFdAlab0inSvtBfZGR6ztwPDUO+Ls7pZbkBNOHlY667DvlruWIxG68kOGdGSVyCh13x01 utI3gzhTODY7z2zp+WsO0PsE6E9312UBeIYMej4hYvF/Y3EMyZ9E26gnonW+boE+18DrG5gPcFw0 sorMwIUY6256s/daoQe/qUKS82Ail+QUoQebTnbAjn39pCXHR+3/H3OszMOl6W8KjptlwlCFtaOg UxLMVYdh84GuEEZhvUQhuMI9dM9+JDX6HAcOmz0iyu8xL4ysEr3vQCj8KWefshNPZiTEUxnpHikV 7+ZtsH8tZ/3zbBt1RqPlShfppNcL -----END CERTIFICATE----- ACCVRAIZ1 ========= -----BEGIN CERTIFICATE----- MIIH0zCCBbugAwIBAgIIXsO3pkN/pOAwDQYJKoZIhvcNAQEFBQAwQjESMBAGA1UEAwwJQUNDVlJB SVoxMRAwDgYDVQQLDAdQS0lBQ0NWMQ0wCwYDVQQKDARBQ0NWMQswCQYDVQQGEwJFUzAeFw0xMTA1 MDUwOTM3MzdaFw0zMDEyMzEwOTM3MzdaMEIxEjAQBgNVBAMMCUFDQ1ZSQUlaMTEQMA4GA1UECwwH UEtJQUNDVjENMAsGA1UECgwEQUNDVjELMAkGA1UEBhMCRVMwggIiMA0GCSqGSIb3DQEBAQUAA4IC DwAwggIKAoICAQCbqau/YUqXry+XZpp0X9DZlv3P4uRm7x8fRzPCRKPfmt4ftVTdFXxpNRFvu8gM jmoYHtiP2Ra8EEg2XPBjs5BaXCQ316PWywlxufEBcoSwfdtNgM3802/J+Nq2DoLSRYWoG2ioPej0 RGy9ocLLA76MPhMAhN9KSMDjIgro6TenGEyxCQ0jVn8ETdkXhBilyNpAlHPrzg5XPAOBOp0KoVdD aaxXbXmQeOW1tDvYvEyNKKGno6e6Ak4l0Squ7a4DIrhrIA8wKFSVf+DuzgpmndFALW4ir50awQUZ 0m/A8p/4e7MCQvtQqR0tkw8jq8bBD5L/0KIV9VMJcRz/RROE5iZe+OCIHAr8Fraocwa48GOEAqDG WuzndN9wrqODJerWx5eHk6fGioozl2A3ED6XPm4pFdahD9GILBKfb6qkxkLrQaLjlUPTAYVtjrs7 8yM2x/474KElB0iryYl0/wiPgL/AlmXz7uxLaL2diMMxs0Dx6M/2OLuc5NF/1OVYm3z61PMOm3WR 5LpSLhl+0fXNWhn8ugb2+1KoS5kE3fj5tItQo05iifCHJPqDQsGH+tUtKSpacXpkatcnYGMN285J 9Y0fkIkyF/hzQ7jSWpOGYdbhdQrqeWZ2iE9x6wQl1gpaepPluUsXQA+xtrn13k/c4LOsOxFwYIRK Q26ZIMApcQrAZQIDAQABo4ICyzCCAscwfQYIKwYBBQUHAQEEcTBvMEwGCCsGAQUFBzAChkBodHRw Oi8vd3d3LmFjY3YuZXMvZmlsZWFkbWluL0FyY2hpdm9zL2NlcnRpZmljYWRvcy9yYWl6YWNjdjEu Y3J0MB8GCCsGAQUFBzABhhNodHRwOi8vb2NzcC5hY2N2LmVzMB0GA1UdDgQWBBTSh7Tj3zcnk1X2 VuqB5TbMjB4/vTAPBgNVHRMBAf8EBTADAQH/MB8GA1UdIwQYMBaAFNKHtOPfNyeTVfZW6oHlNsyM Hj+9MIIBcwYDVR0gBIIBajCCAWYwggFiBgRVHSAAMIIBWDCCASIGCCsGAQUFBwICMIIBFB6CARAA QQB1AHQAbwByAGkAZABhAGQAIABkAGUAIABDAGUAcgB0AGkAZgBpAGMAYQBjAGkA8wBuACAAUgBh AO0AegAgAGQAZQAgAGwAYQAgAEEAQwBDAFYAIAAoAEEAZwBlAG4AYwBpAGEAIABkAGUAIABUAGUA YwBuAG8AbABvAGcA7QBhACAAeQAgAEMAZQByAHQAaQBmAGkAYwBhAGMAaQDzAG4AIABFAGwAZQBj AHQAcgDzAG4AaQBjAGEALAAgAEMASQBGACAAUQA0ADYAMAAxADEANQA2AEUAKQAuACAAQwBQAFMA IABlAG4AIABoAHQAdABwADoALwAvAHcAdwB3AC4AYQBjAGMAdgAuAGUAczAwBggrBgEFBQcCARYk aHR0cDovL3d3dy5hY2N2LmVzL2xlZ2lzbGFjaW9uX2MuaHRtMFUGA1UdHwROMEwwSqBIoEaGRGh0 dHA6Ly93d3cuYWNjdi5lcy9maWxlYWRtaW4vQXJjaGl2b3MvY2VydGlmaWNhZG9zL3JhaXphY2N2 MV9kZXIuY3JsMA4GA1UdDwEB/wQEAwIBBjAXBgNVHREEEDAOgQxhY2N2QGFjY3YuZXMwDQYJKoZI hvcNAQEFBQADggIBAJcxAp/n/UNnSEQU5CmH7UwoZtCPNdpNYbdKl02125DgBS4OxnnQ8pdpD70E R9m+27Up2pvZrqmZ1dM8MJP1jaGo/AaNRPTKFpV8M9xii6g3+CfYCS0b78gUJyCpZET/LtZ1qmxN YEAZSUNUY9rizLpm5U9EelvZaoErQNV/+QEnWCzI7UiRfD+mAM/EKXMRNt6GGT6d7hmKG9Ww7Y49 nCrADdg9ZuM8Db3VlFzi4qc1GwQA9j9ajepDvV+JHanBsMyZ4k0ACtrJJ1vnE5Bc5PUzolVt3OAJ TS+xJlsndQAJxGJ3KQhfnlmstn6tn1QwIgPBHnFk/vk4CpYY3QIUrCPLBhwepH2NDd4nQeit2hW3 sCPdK6jT2iWH7ehVRE2I9DZ+hJp4rPcOVkkO1jMl1oRQQmwgEh0q1b688nCBpHBgvgW1m54ERL5h I6zppSSMEYCUWqKiuUnSwdzRp+0xESyeGabu4VXhwOrPDYTkF7eifKXeVSUG7szAh1xA2syVP1Xg Nce4hL60Xc16gwFy7ofmXx2utYXGJt/mwZrpHgJHnyqobalbz+xFd3+YJ5oyXSrjhO7FmGYvliAd 3djDJ9ew+f7Zfc3Qn48LFFhRny+Lwzgt3uiP1o2HpPVWQxaZLPSkVrQ0uGE3ycJYgBugl6H8WY3p EfbRD0tVNEYqi4Y7 -----END CERTIFICATE----- TWCA Global Root CA =================== -----BEGIN CERTIFICATE----- MIIFQTCCAymgAwIBAgICDL4wDQYJKoZIhvcNAQELBQAwUTELMAkGA1UEBhMCVFcxEjAQBgNVBAoT CVRBSVdBTi1DQTEQMA4GA1UECxMHUm9vdCBDQTEcMBoGA1UEAxMTVFdDQSBHbG9iYWwgUm9vdCBD QTAeFw0xMjA2MjcwNjI4MzNaFw0zMDEyMzExNTU5NTlaMFExCzAJBgNVBAYTAlRXMRIwEAYDVQQK EwlUQUlXQU4tQ0ExEDAOBgNVBAsTB1Jvb3QgQ0ExHDAaBgNVBAMTE1RXQ0EgR2xvYmFsIFJvb3Qg Q0EwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCwBdvI64zEbooh745NnHEKH1Jw7W2C nJfF10xORUnLQEK1EjRsGcJ0pDFfhQKX7EMzClPSnIyOt7h52yvVavKOZsTuKwEHktSz0ALfUPZV r2YOy+BHYC8rMjk1Ujoog/h7FsYYuGLWRyWRzvAZEk2tY/XTP3VfKfChMBwqoJimFb3u/Rk28OKR Q4/6ytYQJ0lM793B8YVwm8rqqFpD/G2Gb3PpN0Wp8DbHzIh1HrtsBv+baz4X7GGqcXzGHaL3SekV tTzWoWH1EfcFbx39Eb7QMAfCKbAJTibc46KokWofwpFFiFzlmLhxpRUZyXx1EcxwdE8tmx2RRP1W KKD+u4ZqyPpcC1jcxkt2yKsi2XMPpfRaAok/T54igu6idFMqPVMnaR1sjjIsZAAmY2E2TqNGtz99 sy2sbZCilaLOz9qC5wc0GZbpuCGqKX6mOL6OKUohZnkfs8O1CWfe1tQHRvMq2uYiN2DLgbYPoA/p yJV/v1WRBXrPPRXAb94JlAGD1zQbzECl8LibZ9WYkTunhHiVJqRaCPgrdLQABDzfuBSO6N+pjWxn kjMdwLfS7JLIvgm/LCkFbwJrnu+8vyq8W8BQj0FwcYeyTbcEqYSjMq+u7msXi7Kx/mzhkIyIqJdI zshNy/MGz19qCkKxHh53L46g5pIOBvwFItIm4TFRfTLcDwIDAQABoyMwITAOBgNVHQ8BAf8EBAMC AQYwDwYDVR0TAQH/BAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAgEAXzSBdu+WHdXltdkCY4QWwa6g cFGn90xHNcgL1yg9iXHZqjNB6hQbbCEAwGxCGX6faVsgQt+i0trEfJdLjbDorMjupWkEmQqSpqsn LhpNgb+E1HAerUf+/UqdM+DyucRFCCEK2mlpc3INvjT+lIutwx4116KD7+U4x6WFH6vPNOw/KP4M 8VeGTslV9xzU2KV9Bnpv1d8Q34FOIWWxtuEXeZVFBs5fzNxGiWNoRI2T9GRwoD2dKAXDOXC4Ynsg /eTb6QihuJ49CcdP+yz4k3ZB3lLg4VfSnQO8d57+nile98FRYB/e2guyLXW3Q0iT5/Z5xoRdgFlg lPx4mI88k1HtQJAH32RjJMtOcQWh15QaiDLxInQirqWm2BJpTGCjAu4r7NRjkgtevi92a6O2JryP A9gK8kxkRr05YuWW6zRjESjMlfGt7+/cgFhI6Uu46mWs6fyAtbXIRfmswZ/ZuepiiI7E8UuDEq3m i4TWnsLrgxifarsbJGAzcMzs9zLzXNl5fe+epP7JI8Mk7hWSsT2RTyaGvWZzJBPqpK5jwa19hAM8 EHiGG3njxPPyBJUgriOCxLM6AGK/5jYk4Ve6xx6QddVfP5VhK8E7zeWzaGHQRiapIVJpLesux+t3 zqY6tQMzT3bR51xUAV3LePTJDL/PEo4XLSNolOer/qmyKwbQBM0= -----END CERTIFICATE----- TeliaSonera Root CA v1 ====================== -----BEGIN CERTIFICATE----- MIIFODCCAyCgAwIBAgIRAJW+FqD3LkbxezmCcvqLzZYwDQYJKoZIhvcNAQEFBQAwNzEUMBIGA1UE CgwLVGVsaWFTb25lcmExHzAdBgNVBAMMFlRlbGlhU29uZXJhIFJvb3QgQ0EgdjEwHhcNMDcxMDE4 MTIwMDUwWhcNMzIxMDE4MTIwMDUwWjA3MRQwEgYDVQQKDAtUZWxpYVNvbmVyYTEfMB0GA1UEAwwW VGVsaWFTb25lcmEgUm9vdCBDQSB2MTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAMK+ 6yfwIaPzaSZVfp3FVRaRXP3vIb9TgHot0pGMYzHw7CTww6XScnwQbfQ3t+XmfHnqjLWCi65ItqwA 3GV17CpNX8GH9SBlK4GoRz6JI5UwFpB/6FcHSOcZrr9FZ7E3GwYq/t75rH2D+1665I+XZ75Ljo1k B1c4VWk0Nj0TSO9P4tNmHqTPGrdeNjPUtAa9GAH9d4RQAEX1jF3oI7x+/jXh7VB7qTCNGdMJjmhn Xb88lxhTuylixcpecsHHltTbLaC0H2kD7OriUPEMPPCs81Mt8Bz17Ww5OXOAFshSsCPN4D7c3TxH oLs1iuKYaIu+5b9y7tL6pe0S7fyYGKkmdtwoSxAgHNN/Fnct7W+A90m7UwW7XWjH1Mh1Fj+JWov3 F0fUTPHSiXk+TT2YqGHeOh7S+F4D4MHJHIzTjU3TlTazN19jY5szFPAtJmtTfImMMsJu7D0hADnJ oWjiUIMusDor8zagrC/kb2HCUQk5PotTubtn2txTuXZZNp1D5SDgPTJghSJRt8czu90VL6R4pgd7 gUY2BIbdeTXHlSw7sKMXNeVzH7RcWe/a6hBle3rQf5+ztCo3O3CLm1u5K7fsslESl1MpWtTwEhDc TwK7EpIvYtQ/aUN8Ddb8WHUBiJ1YFkveupD/RwGJBmr2X7KQarMCpgKIv7NHfirZ1fpoeDVNAgMB AAGjPzA9MA8GA1UdEwEB/wQFMAMBAf8wCwYDVR0PBAQDAgEGMB0GA1UdDgQWBBTwj1k4ALP1j5qW DNXr+nuqF+gTEjANBgkqhkiG9w0BAQUFAAOCAgEAvuRcYk4k9AwI//DTDGjkk0kiP0Qnb7tt3oNm zqjMDfz1mgbldxSR651Be5kqhOX//CHBXfDkH1e3damhXwIm/9fH907eT/j3HEbAek9ALCI18Bmx 0GtnLLCo4MBANzX2hFxc469CeP6nyQ1Q6g2EdvZR74NTxnr/DlZJLo961gzmJ1TjTQpgcmLNkQfW pb/ImWvtxBnmq0wROMVvMeJuScg/doAmAyYp4Db29iBT4xdwNBedY2gea+zDTYa4EzAvXUYNR0PV G6pZDrlcjQZIrXSHX8f8MVRBE+LHIQ6e4B4N4cB7Q4WQxYpYxmUKeFfyxiMPAdkgS94P+5KFdSpc c41teyWRyu5FrgZLAMzTsVlQ2jqIOylDRl6XK1TOU2+NSueW+r9xDkKLfP0ooNBIytrEgUy7onOT JsjrDNYmiLbAJM+7vVvrdX3pCI6GMyx5dwlppYn8s3CQh3aP0yK7Qs69cwsgJirQmz1wHiRszYd2 qReWt88NkvuOGKmYSdGe/mBEciG5Ge3C9THxOUiIkCR1VBatzvT4aRRkOfujuLpwQMcnHL/EVlP6 Y2XQ8xwOFvVrhlhNGNTkDY6lnVuR3HYkUD/GKvvZt5y11ubQ2egZixVxSK236thZiNSQvxaz2ems WWFUyBy6ysHK4bkgTI86k4mloMy/0/Z1pHWWbVY= -----END CERTIFICATE----- E-Tugra Certification Authority =============================== -----BEGIN CERTIFICATE----- MIIGSzCCBDOgAwIBAgIIamg+nFGby1MwDQYJKoZIhvcNAQELBQAwgbIxCzAJBgNVBAYTAlRSMQ8w DQYDVQQHDAZBbmthcmExQDA+BgNVBAoMN0UtVHXEn3JhIEVCRyBCaWxpxZ9pbSBUZWtub2xvamls ZXJpIHZlIEhpem1ldGxlcmkgQS7Fni4xJjAkBgNVBAsMHUUtVHVncmEgU2VydGlmaWthc3lvbiBN ZXJrZXppMSgwJgYDVQQDDB9FLVR1Z3JhIENlcnRpZmljYXRpb24gQXV0aG9yaXR5MB4XDTEzMDMw NTEyMDk0OFoXDTIzMDMwMzEyMDk0OFowgbIxCzAJBgNVBAYTAlRSMQ8wDQYDVQQHDAZBbmthcmEx QDA+BgNVBAoMN0UtVHXEn3JhIEVCRyBCaWxpxZ9pbSBUZWtub2xvamlsZXJpIHZlIEhpem1ldGxl cmkgQS7Fni4xJjAkBgNVBAsMHUUtVHVncmEgU2VydGlmaWthc3lvbiBNZXJrZXppMSgwJgYDVQQD DB9FLVR1Z3JhIENlcnRpZmljYXRpb24gQXV0aG9yaXR5MIICIjANBgkqhkiG9w0BAQEFAAOCAg8A MIICCgKCAgEA4vU/kwVRHoViVF56C/UYB4Oufq9899SKa6VjQzm5S/fDxmSJPZQuVIBSOTkHS0vd hQd2h8y/L5VMzH2nPbxHD5hw+IyFHnSOkm0bQNGZDbt1bsipa5rAhDGvykPL6ys06I+XawGb1Q5K CKpbknSFQ9OArqGIW66z6l7LFpp3RMih9lRozt6Plyu6W0ACDGQXwLWTzeHxE2bODHnv0ZEoq1+g ElIwcxmOj+GMB6LDu0rw6h8VqO4lzKRG+Bsi77MOQ7osJLjFLFzUHPhdZL3Dk14opz8n8Y4e0ypQ BaNV2cvnOVPAmJ6MVGKLJrD3fY185MaeZkJVgkfnsliNZvcHfC425lAcP9tDJMW/hkd5s3kc91r0 E+xs+D/iWR+V7kI+ua2oMoVJl0b+SzGPWsutdEcf6ZG33ygEIqDUD13ieU/qbIWGvaimzuT6w+Gz rt48Ue7LE3wBf4QOXVGUnhMMti6lTPk5cDZvlsouDERVxcr6XQKj39ZkjFqzAQqptQpHF//vkUAq jqFGOjGY5RH8zLtJVor8udBhmm9lbObDyz51Sf6Pp+KJxWfXnUYTTjF2OySznhFlhqt/7x3U+Lzn rFpct1pHXFXOVbQicVtbC/DP3KBhZOqp12gKY6fgDT+gr9Oq0n7vUaDmUStVkhUXU8u3Zg5mTPj5 dUyQ5xJwx0UCAwEAAaNjMGEwHQYDVR0OBBYEFC7j27JJ0JxUeVz6Jyr+zE7S6E5UMA8GA1UdEwEB /wQFMAMBAf8wHwYDVR0jBBgwFoAULuPbsknQnFR5XPonKv7MTtLoTlQwDgYDVR0PAQH/BAQDAgEG MA0GCSqGSIb3DQEBCwUAA4ICAQAFNzr0TbdF4kV1JI+2d1LoHNgQk2Xz8lkGpD4eKexd0dCrfOAK kEh47U6YA5n+KGCRHTAduGN8qOY1tfrTYXbm1gdLymmasoR6d5NFFxWfJNCYExL/u6Au/U5Mh/jO XKqYGwXgAEZKgoClM4so3O0409/lPun++1ndYYRP0lSWE2ETPo+Aab6TR7U1Q9Jauz1c77NCR807 VRMGsAnb/WP2OogKmW9+4c4bU2pEZiNRCHu8W1Ki/QY3OEBhj0qWuJA3+GbHeJAAFS6LrVE1Uweo a2iu+U48BybNCAVwzDk/dr2l02cmAYamU9JgO3xDf1WKvJUawSg5TB9D0pH0clmKuVb8P7Sd2nCc dlqMQ1DujjByTd//SffGqWfZbawCEeI6FiWnWAjLb1NBnEg4R2gz0dfHj9R0IdTDBZB6/86WiLEV KV0jq9BgoRJP3vQXzTLlyb/IQ639Lo7xr+L0mPoSHyDYwKcMhcWQ9DstliaxLL5Mq+ux0orJ23gT Dx4JnW2PAJ8C2sH6H3p6CcRK5ogql5+Ji/03X186zjhZhkuvcQu02PJwT58yE+Owp1fl2tpDy4Q0 8ijE6m30Ku/Ba3ba+367hTzSU8JNvnHhRdH9I2cNE3X7z2VnIp2usAnRCf8dNL/+I5c30jn6PQ0G C7TbO6Orb1wdtn7os4I07QZcJA== -----END CERTIFICATE----- T-TeleSec GlobalRoot Class 2 ============================ -----BEGIN CERTIFICATE----- MIIDwzCCAqugAwIBAgIBATANBgkqhkiG9w0BAQsFADCBgjELMAkGA1UEBhMCREUxKzApBgNVBAoM IlQtU3lzdGVtcyBFbnRlcnByaXNlIFNlcnZpY2VzIEdtYkgxHzAdBgNVBAsMFlQtU3lzdGVtcyBU cnVzdCBDZW50ZXIxJTAjBgNVBAMMHFQtVGVsZVNlYyBHbG9iYWxSb290IENsYXNzIDIwHhcNMDgx MDAxMTA0MDE0WhcNMzMxMDAxMjM1OTU5WjCBgjELMAkGA1UEBhMCREUxKzApBgNVBAoMIlQtU3lz dGVtcyBFbnRlcnByaXNlIFNlcnZpY2VzIEdtYkgxHzAdBgNVBAsMFlQtU3lzdGVtcyBUcnVzdCBD ZW50ZXIxJTAjBgNVBAMMHFQtVGVsZVNlYyBHbG9iYWxSb290IENsYXNzIDIwggEiMA0GCSqGSIb3 DQEBAQUAA4IBDwAwggEKAoIBAQCqX9obX+hzkeXaXPSi5kfl82hVYAUdAqSzm1nzHoqvNK38DcLZ SBnuaY/JIPwhqgcZ7bBcrGXHX+0CfHt8LRvWurmAwhiCFoT6ZrAIxlQjgeTNuUk/9k9uN0goOA/F vudocP05l03Sx5iRUKrERLMjfTlH6VJi1hKTXrcxlkIF+3anHqP1wvzpesVsqXFP6st4vGCvx970 2cu+fjOlbpSD8DT6IavqjnKgP6TeMFvvhk1qlVtDRKgQFRzlAVfFmPHmBiiRqiDFt1MmUUOyCxGV WOHAD3bZwI18gfNycJ5v/hqO2V81xrJvNHy+SE/iWjnX2J14np+GPgNeGYtEotXHAgMBAAGjQjBA MA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgEGMB0GA1UdDgQWBBS/WSA2AHmgoCJrjNXy YdK4LMuCSjANBgkqhkiG9w0BAQsFAAOCAQEAMQOiYQsfdOhyNsZt+U2e+iKo4YFWz827n+qrkRk4 r6p8FU3ztqONpfSO9kSpp+ghla0+AGIWiPACuvxhI+YzmzB6azZie60EI4RYZeLbK4rnJVM3YlNf vNoBYimipidx5joifsFvHZVwIEoHNN/q/xWA5brXethbdXwFeilHfkCoMRN3zUA7tFFHei4R40cR 3p1m0IvVVGb6g1XqfMIpiRvpb7PO4gWEyS8+eIVibslfwXhjdFjASBgMmTnrpMwatXlajRWc2BQN 9noHV8cigwUtPJslJj0Ys6lDfMjIq2SPDqO/nBudMNva0Bkuqjzx+zOAduTNrRlPBSeOE6Fuwg== -----END CERTIFICATE----- Atos TrustedRoot 2011 ===================== -----BEGIN CERTIFICATE----- MIIDdzCCAl+gAwIBAgIIXDPLYixfszIwDQYJKoZIhvcNAQELBQAwPDEeMBwGA1UEAwwVQXRvcyBU cnVzdGVkUm9vdCAyMDExMQ0wCwYDVQQKDARBdG9zMQswCQYDVQQGEwJERTAeFw0xMTA3MDcxNDU4 MzBaFw0zMDEyMzEyMzU5NTlaMDwxHjAcBgNVBAMMFUF0b3MgVHJ1c3RlZFJvb3QgMjAxMTENMAsG A1UECgwEQXRvczELMAkGA1UEBhMCREUwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCV hTuXbyo7LjvPpvMpNb7PGKw+qtn4TaA+Gke5vJrf8v7MPkfoepbCJI419KkM/IL9bcFyYie96mvr 54rMVD6QUM+A1JX76LWC1BTFtqlVJVfbsVD2sGBkWXppzwO3bw2+yj5vdHLqqjAqc2K+SZFhyBH+ DgMq92og3AIVDV4VavzjgsG1xZ1kCWyjWZgHJ8cblithdHFsQ/H3NYkQ4J7sVaE3IqKHBAUsR320 HLliKWYoyrfhk/WklAOZuXCFteZI6o1Q/NnezG8HDt0Lcp2AMBYHlT8oDv3FdU9T1nSatCQujgKR z3bFmx5VdJx4IbHwLfELn8LVlhgf8FQieowHAgMBAAGjfTB7MB0GA1UdDgQWBBSnpQaxLKYJYO7R l+lwrrw7GWzbITAPBgNVHRMBAf8EBTADAQH/MB8GA1UdIwQYMBaAFKelBrEspglg7tGX6XCuvDsZ bNshMBgGA1UdIAQRMA8wDQYLKwYBBAGwLQMEAQEwDgYDVR0PAQH/BAQDAgGGMA0GCSqGSIb3DQEB CwUAA4IBAQAmdzTblEiGKkGdLD4GkGDEjKwLVLgfuXvTBznk+j57sj1O7Z8jvZfza1zv7v1Apt+h k6EKhqzvINB5Ab149xnYJDE0BAGmuhWawyfc2E8PzBhj/5kPDpFrdRbhIfzYJsdHt6bPWHJxfrrh TZVHO8mvbaG0weyJ9rQPOLXiZNwlz6bb65pcmaHFCN795trV1lpFDMS3wrUU77QR/w4VtfX128a9 61qn8FYiqTxlVMYVqL2Gns2Dlmh6cYGJ4Qvh6hEbaAjMaZ7snkGeRDImeuKHCnE96+RapNLbxc3G 3mB/ufNPRJLvKrcYPqcZ2Qt9sTdBQrC6YB3y/gkRsPCHe6ed -----END CERTIFICATE----- QuoVadis Root CA 1 G3 ===================== -----BEGIN CERTIFICATE----- MIIFYDCCA0igAwIBAgIUeFhfLq0sGUvjNwc1NBMotZbUZZMwDQYJKoZIhvcNAQELBQAwSDELMAkG A1UEBhMCQk0xGTAXBgNVBAoTEFF1b1ZhZGlzIExpbWl0ZWQxHjAcBgNVBAMTFVF1b1ZhZGlzIFJv b3QgQ0EgMSBHMzAeFw0xMjAxMTIxNzI3NDRaFw00MjAxMTIxNzI3NDRaMEgxCzAJBgNVBAYTAkJN MRkwFwYDVQQKExBRdW9WYWRpcyBMaW1pdGVkMR4wHAYDVQQDExVRdW9WYWRpcyBSb290IENBIDEg RzMwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCgvlAQjunybEC0BJyFuTHK3C3kEakE PBtVwedYMB0ktMPvhd6MLOHBPd+C5k+tR4ds7FtJwUrVu4/sh6x/gpqG7D0DmVIB0jWerNrwU8lm PNSsAgHaJNM7qAJGr6Qc4/hzWHa39g6QDbXwz8z6+cZM5cOGMAqNF34168Xfuw6cwI2H44g4hWf6 Pser4BOcBRiYz5P1sZK0/CPTz9XEJ0ngnjybCKOLXSoh4Pw5qlPafX7PGglTvF0FBM+hSo+LdoIN ofjSxxR3W5A2B4GbPgb6Ul5jxaYA/qXpUhtStZI5cgMJYr2wYBZupt0lwgNm3fME0UDiTouG9G/l g6AnhF4EwfWQvTA9xO+oabw4m6SkltFi2mnAAZauy8RRNOoMqv8hjlmPSlzkYZqn0ukqeI1RPToV 7qJZjqlc3sX5kCLliEVx3ZGZbHqfPT2YfF72vhZooF6uCyP8Wg+qInYtyaEQHeTTRCOQiJ/GKubX 9ZqzWB4vMIkIG1SitZgj7Ah3HJVdYdHLiZxfokqRmu8hqkkWCKi9YSgxyXSthfbZxbGL0eUQMk1f iyA6PEkfM4VZDdvLCXVDaXP7a3F98N/ETH3Goy7IlXnLc6KOTk0k+17kBL5yG6YnLUlamXrXXAkg t3+UuU/xDRxeiEIbEbfnkduebPRq34wGmAOtzCjvpUfzUwIDAQABo0IwQDAPBgNVHRMBAf8EBTAD AQH/MA4GA1UdDwEB/wQEAwIBBjAdBgNVHQ4EFgQUo5fW816iEOGrRZ88F2Q87gFwnMwwDQYJKoZI hvcNAQELBQADggIBABj6W3X8PnrHX3fHyt/PX8MSxEBd1DKquGrX1RUVRpgjpeaQWxiZTOOtQqOC MTaIzen7xASWSIsBx40Bz1szBpZGZnQdT+3Btrm0DWHMY37XLneMlhwqI2hrhVd2cDMT/uFPpiN3 GPoajOi9ZcnPP/TJF9zrx7zABC4tRi9pZsMbj/7sPtPKlL92CiUNqXsCHKnQO18LwIE6PWThv6ct Tr1NxNgpxiIY0MWscgKCP6o6ojoilzHdCGPDdRS5YCgtW2jgFqlmgiNR9etT2DGbe+m3nUvriBbP +V04ikkwj+3x6xn0dxoxGE1nVGwvb2X52z3sIexe9PSLymBlVNFxZPT5pqOBMzYzcfCkeF9OrYMh 3jRJjehZrJ3ydlo28hP0r+AJx2EqbPfgna67hkooby7utHnNkDPDs3b69fBsnQGQ+p6Q9pxyz0fa wx/kNSBT8lTR32GDpgLiJTjehTItXnOQUl1CxM49S+H5GYQd1aJQzEH7QRTDvdbJWqNjZgKAvQU6 O0ec7AAmTPWIUb+oI38YB7AL7YsmoWTTYUrrXJ/es69nA7Mf3W1daWhpq1467HxpvMc7hU6eFbm0 FU/DlXpY18ls6Wy58yljXrQs8C097Vpl4KlbQMJImYFtnh8GKjwStIsPm6Ik8KaN1nrgS7ZklmOV hMJKzRwuJIczYOXD -----END CERTIFICATE----- QuoVadis Root CA 2 G3 ===================== -----BEGIN CERTIFICATE----- MIIFYDCCA0igAwIBAgIURFc0JFuBiZs18s64KztbpybwdSgwDQYJKoZIhvcNAQELBQAwSDELMAkG A1UEBhMCQk0xGTAXBgNVBAoTEFF1b1ZhZGlzIExpbWl0ZWQxHjAcBgNVBAMTFVF1b1ZhZGlzIFJv b3QgQ0EgMiBHMzAeFw0xMjAxMTIxODU5MzJaFw00MjAxMTIxODU5MzJaMEgxCzAJBgNVBAYTAkJN MRkwFwYDVQQKExBRdW9WYWRpcyBMaW1pdGVkMR4wHAYDVQQDExVRdW9WYWRpcyBSb290IENBIDIg RzMwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQChriWyARjcV4g/Ruv5r+LrI3HimtFh ZiFfqq8nUeVuGxbULX1QsFN3vXg6YOJkApt8hpvWGo6t/x8Vf9WVHhLL5hSEBMHfNrMWn4rjyduY NM7YMxcoRvynyfDStNVNCXJJ+fKH46nafaF9a7I6JaltUkSs+L5u+9ymc5GQYaYDFCDy54ejiK2t oIz/pgslUiXnFgHVy7g1gQyjO/Dh4fxaXc6AcW34Sas+O7q414AB+6XrW7PFXmAqMaCvN+ggOp+o MiwMzAkd056OXbxMmO7FGmh77FOm6RQ1o9/NgJ8MSPsc9PG/Srj61YxxSscfrf5BmrODXfKEVu+l V0POKa2Mq1W/xPtbAd0jIaFYAI7D0GoT7RPjEiuA3GfmlbLNHiJuKvhB1PLKFAeNilUSxmn1uIZo L1NesNKqIcGY5jDjZ1XHm26sGahVpkUG0CM62+tlXSoREfA7T8pt9DTEceT/AFr2XK4jYIVz8eQQ sSWu1ZK7E8EM4DnatDlXtas1qnIhO4M15zHfeiFuuDIIfR0ykRVKYnLP43ehvNURG3YBZwjgQQvD 6xVu+KQZ2aKrr+InUlYrAoosFCT5v0ICvybIxo/gbjh9Uy3l7ZizlWNof/k19N+IxWA1ksB8aRxh lRbQ694Lrz4EEEVlWFA4r0jyWbYW8jwNkALGcC4BrTwV1wIDAQABo0IwQDAPBgNVHRMBAf8EBTAD AQH/MA4GA1UdDwEB/wQEAwIBBjAdBgNVHQ4EFgQU7edvdlq/YOxJW8ald7tyFnGbxD0wDQYJKoZI hvcNAQELBQADggIBAJHfgD9DCX5xwvfrs4iP4VGyvD11+ShdyLyZm3tdquXK4Qr36LLTn91nMX66 AarHakE7kNQIXLJgapDwyM4DYvmL7ftuKtwGTTwpD4kWilhMSA/ohGHqPHKmd+RCroijQ1h5fq7K pVMNqT1wvSAZYaRsOPxDMuHBR//47PERIjKWnML2W2mWeyAMQ0GaW/ZZGYjeVYg3UQt4XAoeo0L9 x52ID8DyeAIkVJOviYeIyUqAHerQbj5hLja7NQ4nlv1mNDthcnPxFlxHBlRJAHpYErAK74X9sbgz dWqTHBLmYF5vHX/JHyPLhGGfHoJE+V+tYlUkmlKY7VHnoX6XOuYvHxHaU4AshZ6rNRDbIl9qxV6X U/IyAgkwo1jwDQHVcsaxfGl7w/U2Rcxhbl5MlMVerugOXou/983g7aEOGzPuVBj+D77vfoRrQ+Nw mNtddbINWQeFFSM51vHfqSYP1kjHs6Yi9TM3WpVHn3u6GBVv/9YUZINJ0gpnIdsPNWNgKCLjsZWD zYWm3S8P52dSbrsvhXz1SnPnxT7AvSESBT/8twNJAlvIJebiVDj1eYeMHVOyToV7BjjHLPj4sHKN JeV3UvQDHEimUF+IIDBu8oJDqz2XhOdT+yHBTw8imoa4WSr2Rz0ZiC3oheGe7IUIarFsNMkd7Egr O3jtZsSOeWmD3n+M -----END CERTIFICATE----- QuoVadis Root CA 3 G3 ===================== -----BEGIN CERTIFICATE----- MIIFYDCCA0igAwIBAgIULvWbAiin23r/1aOp7r0DoM8Sah0wDQYJKoZIhvcNAQELBQAwSDELMAkG A1UEBhMCQk0xGTAXBgNVBAoTEFF1b1ZhZGlzIExpbWl0ZWQxHjAcBgNVBAMTFVF1b1ZhZGlzIFJv b3QgQ0EgMyBHMzAeFw0xMjAxMTIyMDI2MzJaFw00MjAxMTIyMDI2MzJaMEgxCzAJBgNVBAYTAkJN MRkwFwYDVQQKExBRdW9WYWRpcyBMaW1pdGVkMR4wHAYDVQQDExVRdW9WYWRpcyBSb290IENBIDMg RzMwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCzyw4QZ47qFJenMioKVjZ/aEzHs286 IxSR/xl/pcqs7rN2nXrpixurazHb+gtTTK/FpRp5PIpM/6zfJd5O2YIyC0TeytuMrKNuFoM7pmRL Mon7FhY4futD4tN0SsJiCnMK3UmzV9KwCoWdcTzeo8vAMvMBOSBDGzXRU7Ox7sWTaYI+FrUoRqHe 6okJ7UO4BUaKhvVZR74bbwEhELn9qdIoyhA5CcoTNs+cra1AdHkrAj80//ogaX3T7mH1urPnMNA3 I4ZyYUUpSFlob3emLoG+B01vr87ERRORFHAGjx+f+IdpsQ7vw4kZ6+ocYfx6bIrc1gMLnia6Et3U VDmrJqMz6nWB2i3ND0/kA9HvFZcba5DFApCTZgIhsUfei5pKgLlVj7WiL8DWM2fafsSntARE60f7 5li59wzweyuxwHApw0BiLTtIadwjPEjrewl5qW3aqDCYz4ByA4imW0aucnl8CAMhZa634RylsSqi Md5mBPfAdOhx3v89WcyWJhKLhZVXGqtrdQtEPREoPHtht+KPZ0/l7DxMYIBpVzgeAVuNVejH38DM dyM0SXV89pgR6y3e7UEuFAUCf+D+IOs15xGsIs5XPd7JMG0QA4XN8f+MFrXBsj6IbGB/kE+V9/Yt rQE5BwT6dYB9v0lQ7e/JxHwc64B+27bQ3RP+ydOc17KXqQIDAQABo0IwQDAPBgNVHRMBAf8EBTAD AQH/MA4GA1UdDwEB/wQEAwIBBjAdBgNVHQ4EFgQUxhfQvKjqAkPyGwaZXSuQILnXnOQwDQYJKoZI hvcNAQELBQADggIBADRh2Va1EodVTd2jNTFGu6QHcrxfYWLopfsLN7E8trP6KZ1/AvWkyaiTt3px KGmPc+FSkNrVvjrlt3ZqVoAh313m6Tqe5T72omnHKgqwGEfcIHB9UqM+WXzBusnIFUBhynLWcKzS t/Ac5IYp8M7vaGPQtSCKFWGafoaYtMnCdvvMujAWzKNhxnQT5WvvoxXqA/4Ti2Tk08HS6IT7SdEQ TXlm66r99I0xHnAUrdzeZxNMgRVhvLfZkXdxGYFgu/BYpbWcC/ePIlUnwEsBbTuZDdQdm2NnL9Du DcpmvJRPpq3t/O5jrFc/ZSXPsoaP0Aj/uHYUbt7lJ+yreLVTubY/6CD50qi+YUbKh4yE8/nxoGib Ih6BJpsQBJFxwAYf3KDTuVan45gtf4Od34wrnDKOMpTwATwiKp9Dwi7DmDkHOHv8XgBCH/MyJnmD hPbl8MFREsALHgQjDFSlTC9JxUrRtm5gDWv8a4uFJGS3iQ6rJUdbPM9+Sb3H6QrG2vd+DhcI00iX 0HGS8A85PjRqHH3Y8iKuu2n0M7SmSFXRDw4m6Oy2Cy2nhTXN/VnIn9HNPlopNLk9hM6xZdRZkZFW dSHBd575euFgndOtBBj0fOtek49TSiIp+EgrPk2GrFt/ywaZWWDYWGWVjUTR939+J399roD1B0y2 PpxxVJkES/1Y+Zj0 -----END CERTIFICATE----- DigiCert Assured ID Root G2 =========================== -----BEGIN CERTIFICATE----- MIIDljCCAn6gAwIBAgIQC5McOtY5Z+pnI7/Dr5r0SzANBgkqhkiG9w0BAQsFADBlMQswCQYDVQQG EwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cuZGlnaWNlcnQuY29tMSQw IgYDVQQDExtEaWdpQ2VydCBBc3N1cmVkIElEIFJvb3QgRzIwHhcNMTMwODAxMTIwMDAwWhcNMzgw MTE1MTIwMDAwWjBlMQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQL ExB3d3cuZGlnaWNlcnQuY29tMSQwIgYDVQQDExtEaWdpQ2VydCBBc3N1cmVkIElEIFJvb3QgRzIw ggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDZ5ygvUj82ckmIkzTz+GoeMVSAn61UQbVH 35ao1K+ALbkKz3X9iaV9JPrjIgwrvJUXCzO/GU1BBpAAvQxNEP4HteccbiJVMWWXvdMX0h5i89vq bFCMP4QMls+3ywPgym2hFEwbid3tALBSfK+RbLE4E9HpEgjAALAcKxHad3A2m67OeYfcgnDmCXRw VWmvo2ifv922ebPynXApVfSr/5Vh88lAbx3RvpO704gqu52/clpWcTs/1PPRCv4o76Pu2ZmvA9OP YLfykqGxvYmJHzDNw6YuYjOuFgJ3RFrngQo8p0Quebg/BLxcoIfhG69Rjs3sLPr4/m3wOnyqi+Rn lTGNAgMBAAGjQjBAMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgGGMB0GA1UdDgQWBBTO w0q5mVXyuNtgv6l+vVa1lzan1jANBgkqhkiG9w0BAQsFAAOCAQEAyqVVjOPIQW5pJ6d1Ee88hjZv 0p3GeDgdaZaikmkuOGybfQTUiaWxMTeKySHMq2zNixya1r9I0jJmwYrA8y8678Dj1JGG0VDjA9tz d29KOVPt3ibHtX2vK0LRdWLjSisCx1BL4GnilmwORGYQRI+tBev4eaymG+g3NJ1TyWGqolKvSnAW hsI6yLETcDbYz+70CjTVW0z9B5yiutkBclzzTcHdDrEcDcRjvq30FPuJ7KJBDkzMyFdA0G4Dqs0M jomZmWzwPDCvON9vvKO+KSAnq3T/EyJ43pdSVR6DtVQgA+6uwE9W3jfMw3+qBCe703e4YtsXfJwo IhNzbM8m9Yop5w== -----END CERTIFICATE----- DigiCert Assured ID Root G3 =========================== -----BEGIN CERTIFICATE----- MIICRjCCAc2gAwIBAgIQC6Fa+h3foLVJRK/NJKBs7DAKBggqhkjOPQQDAzBlMQswCQYDVQQGEwJV UzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cuZGlnaWNlcnQuY29tMSQwIgYD VQQDExtEaWdpQ2VydCBBc3N1cmVkIElEIFJvb3QgRzMwHhcNMTMwODAxMTIwMDAwWhcNMzgwMTE1 MTIwMDAwWjBlMQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3 d3cuZGlnaWNlcnQuY29tMSQwIgYDVQQDExtEaWdpQ2VydCBBc3N1cmVkIElEIFJvb3QgRzMwdjAQ BgcqhkjOPQIBBgUrgQQAIgNiAAQZ57ysRGXtzbg/WPuNsVepRC0FFfLvC/8QdJ+1YlJfZn4f5dwb RXkLzMZTCp2NXQLZqVneAlr2lSoOjThKiknGvMYDOAdfVdp+CW7if17QRSAPWXYQ1qAk8C3eNvJs KTmjQjBAMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgGGMB0GA1UdDgQWBBTL0L2p4ZgF UaFNN6KDec6NHSrkhDAKBggqhkjOPQQDAwNnADBkAjAlpIFFAmsSS3V0T8gj43DydXLefInwz5Fy YZ5eEJJZVrmDxxDnOOlYJjZ91eQ0hjkCMHw2U/Aw5WJjOpnitqM7mzT6HtoQknFekROn3aRukswy 1vUhZscv6pZjamVFkpUBtA== -----END CERTIFICATE----- DigiCert Global Root G2 ======================= -----BEGIN CERTIFICATE----- MIIDjjCCAnagAwIBAgIQAzrx5qcRqaC7KGSxHQn65TANBgkqhkiG9w0BAQsFADBhMQswCQYDVQQG EwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cuZGlnaWNlcnQuY29tMSAw HgYDVQQDExdEaWdpQ2VydCBHbG9iYWwgUm9vdCBHMjAeFw0xMzA4MDExMjAwMDBaFw0zODAxMTUx MjAwMDBaMGExCzAJBgNVBAYTAlVTMRUwEwYDVQQKEwxEaWdpQ2VydCBJbmMxGTAXBgNVBAsTEHd3 dy5kaWdpY2VydC5jb20xIDAeBgNVBAMTF0RpZ2lDZXJ0IEdsb2JhbCBSb290IEcyMIIBIjANBgkq hkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAuzfNNNx7a8myaJCtSnX/RrohCgiN9RlUyfuI2/Ou8jqJ kTx65qsGGmvPrC3oXgkkRLpimn7Wo6h+4FR1IAWsULecYxpsMNzaHxmx1x7e/dfgy5SDN67sH0NO 3Xss0r0upS/kqbitOtSZpLYl6ZtrAGCSYP9PIUkY92eQq2EGnI/yuum06ZIya7XzV+hdG82MHauV BJVJ8zUtluNJbd134/tJS7SsVQepj5WztCO7TG1F8PapspUwtP1MVYwnSlcUfIKdzXOS0xZKBgyM UNGPHgm+F6HmIcr9g+UQvIOlCsRnKPZzFBQ9RnbDhxSJITRNrw9FDKZJobq7nMWxM4MphQIDAQAB o0IwQDAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIBhjAdBgNVHQ4EFgQUTiJUIBiV5uNu 5g/6+rkS7QYXjzkwDQYJKoZIhvcNAQELBQADggEBAGBnKJRvDkhj6zHd6mcY1Yl9PMWLSn/pvtsr F9+wX3N3KjITOYFnQoQj8kVnNeyIv/iPsGEMNKSuIEyExtv4NeF22d+mQrvHRAiGfzZ0JFrabA0U WTW98kndth/Jsw1HKj2ZL7tcu7XUIOGZX1NGFdtom/DzMNU+MeKNhJ7jitralj41E6Vf8PlwUHBH QRFXGU7Aj64GxJUTFy8bJZ918rGOmaFvE7FBcf6IKshPECBV1/MUReXgRPTqh5Uykw7+U0b6LJ3/ iyK5S9kJRaTepLiaWN0bfVKfjllDiIGknibVb63dDcY3fe0Dkhvld1927jyNxF1WW6LZZm6zNTfl MrY= -----END CERTIFICATE----- DigiCert Global Root G3 ======================= -----BEGIN CERTIFICATE----- MIICPzCCAcWgAwIBAgIQBVVWvPJepDU1w6QP1atFcjAKBggqhkjOPQQDAzBhMQswCQYDVQQGEwJV UzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cuZGlnaWNlcnQuY29tMSAwHgYD VQQDExdEaWdpQ2VydCBHbG9iYWwgUm9vdCBHMzAeFw0xMzA4MDExMjAwMDBaFw0zODAxMTUxMjAw MDBaMGExCzAJBgNVBAYTAlVTMRUwEwYDVQQKEwxEaWdpQ2VydCBJbmMxGTAXBgNVBAsTEHd3dy5k aWdpY2VydC5jb20xIDAeBgNVBAMTF0RpZ2lDZXJ0IEdsb2JhbCBSb290IEczMHYwEAYHKoZIzj0C AQYFK4EEACIDYgAE3afZu4q4C/sLfyHS8L6+c/MzXRq8NOrexpu80JX28MzQC7phW1FGfp4tn+6O YwwX7Adw9c+ELkCDnOg/QW07rdOkFFk2eJ0DQ+4QE2xy3q6Ip6FrtUPOZ9wj/wMco+I+o0IwQDAP BgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIBhjAdBgNVHQ4EFgQUs9tIpPmhxdiuNkHMEWNp Yim8S8YwCgYIKoZIzj0EAwMDaAAwZQIxAK288mw/EkrRLTnDCgmXc/SINoyIJ7vmiI1Qhadj+Z4y 3maTD/HMsQmP3Wyr+mt/oAIwOWZbwmSNuJ5Q3KjVSaLtx9zRSX8XAbjIho9OjIgrqJqpisXRAL34 VOKa5Vt8sycX -----END CERTIFICATE----- DigiCert Trusted Root G4 ======================== -----BEGIN CERTIFICATE----- MIIFkDCCA3igAwIBAgIQBZsbV56OITLiOQe9p3d1XDANBgkqhkiG9w0BAQwFADBiMQswCQYDVQQG EwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cuZGlnaWNlcnQuY29tMSEw HwYDVQQDExhEaWdpQ2VydCBUcnVzdGVkIFJvb3QgRzQwHhcNMTMwODAxMTIwMDAwWhcNMzgwMTE1 MTIwMDAwWjBiMQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3 d3cuZGlnaWNlcnQuY29tMSEwHwYDVQQDExhEaWdpQ2VydCBUcnVzdGVkIFJvb3QgRzQwggIiMA0G CSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQC/5pBzaN675F1KPDAiMGkz7MKnJS7JIT3yithZwuEp pz1Yq3aaza57G4QNxDAf8xukOBbrVsaXbR2rsnnyyhHS5F/WBTxSD1Ifxp4VpX6+n6lXFllVcq9o k3DCsrp1mWpzMpTREEQQLt+C8weE5nQ7bXHiLQwb7iDVySAdYyktzuxeTsiT+CFhmzTrBcZe7Fsa vOvJz82sNEBfsXpm7nfISKhmV1efVFiODCu3T6cw2Vbuyntd463JT17lNecxy9qTXtyOj4DatpGY QJB5w3jHtrHEtWoYOAMQjdjUN6QuBX2I9YI+EJFwq1WCQTLX2wRzKm6RAXwhTNS8rhsDdV14Ztk6 MUSaM0C/CNdaSaTC5qmgZ92kJ7yhTzm1EVgX9yRcRo9k98FpiHaYdj1ZXUJ2h4mXaXpI8OCiEhtm mnTK3kse5w5jrubU75KSOp493ADkRSWJtppEGSt+wJS00mFt6zPZxd9LBADMfRyVw4/3IbKyEbe7 f/LVjHAsQWCqsWMYRJUadmJ+9oCw++hkpjPRiQfhvbfmQ6QYuKZ3AeEPlAwhHbJUKSWJbOUOUlFH dL4mrLZBdd56rF+NP8m800ERElvlEFDrMcXKchYiCd98THU/Y+whX8QgUWtvsauGi0/C1kVfnSD8 oR7FwI+isX4KJpn15GkvmB0t9dmpsh3lGwIDAQABo0IwQDAPBgNVHRMBAf8EBTADAQH/MA4GA1Ud DwEB/wQEAwIBhjAdBgNVHQ4EFgQU7NfjgtJxXWRM3y5nP+e6mK4cD08wDQYJKoZIhvcNAQEMBQAD ggIBALth2X2pbL4XxJEbw6GiAI3jZGgPVs93rnD5/ZpKmbnJeFwMDF/k5hQpVgs2SV1EY+CtnJYY ZhsjDT156W1r1lT40jzBQ0CuHVD1UvyQO7uYmWlrx8GnqGikJ9yd+SeuMIW59mdNOj6PWTkiU0Tr yF0Dyu1Qen1iIQqAyHNm0aAFYF/opbSnr6j3bTWcfFqK1qI4mfN4i/RN0iAL3gTujJtHgXINwBQy 7zBZLq7gcfJW5GqXb5JQbZaNaHqasjYUegbyJLkJEVDXCLG4iXqEI2FCKeWjzaIgQdfRnGTZ6iah ixTXTBmyUEFxPT9NcCOGDErcgdLMMpSEDQgJlxxPwO5rIHQw0uA5NBCFIRUBCOhVMt5xSdkoF1BN 5r5N0XWs0Mr7QbhDparTwwVETyw2m+L64kW4I1NsBm9nVX9GtUw/bihaeSbSpKhil9Ie4u1Ki7wb /UdKDd9nZn6yW0HQO+T0O/QEY+nvwlQAUaCKKsnOeMzV6ocEGLPOr0mIr/OSmbaz5mEP0oUA51Aa 5BuVnRmhuZyxm7EAHu/QD09CbMkKvO5D+jpxpchNJqU1/YldvIViHTLSoCtU7ZpXwdv6EM8Zt4tK G48BtieVU+i2iW1bvGjUI+iLUaJW+fCmgKDWHrO8Dw9TdSmq6hN35N6MgSGtBxBHEa2HPQfRdbzP 82Z+ -----END CERTIFICATE----- COMODO RSA Certification Authority ================================== -----BEGIN CERTIFICATE----- MIIF2DCCA8CgAwIBAgIQTKr5yttjb+Af907YWwOGnTANBgkqhkiG9w0BAQwFADCBhTELMAkGA1UE BhMCR0IxGzAZBgNVBAgTEkdyZWF0ZXIgTWFuY2hlc3RlcjEQMA4GA1UEBxMHU2FsZm9yZDEaMBgG A1UEChMRQ09NT0RPIENBIExpbWl0ZWQxKzApBgNVBAMTIkNPTU9ETyBSU0EgQ2VydGlmaWNhdGlv biBBdXRob3JpdHkwHhcNMTAwMTE5MDAwMDAwWhcNMzgwMTE4MjM1OTU5WjCBhTELMAkGA1UEBhMC R0IxGzAZBgNVBAgTEkdyZWF0ZXIgTWFuY2hlc3RlcjEQMA4GA1UEBxMHU2FsZm9yZDEaMBgGA1UE ChMRQ09NT0RPIENBIExpbWl0ZWQxKzApBgNVBAMTIkNPTU9ETyBSU0EgQ2VydGlmaWNhdGlvbiBB dXRob3JpdHkwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCR6FSS0gpWsawNJN3Fz0Rn dJkrN6N9I3AAcbxT38T6KhKPS38QVr2fcHK3YX/JSw8Xpz3jsARh7v8Rl8f0hj4K+j5c+ZPmNHrZ FGvnnLOFoIJ6dq9xkNfs/Q36nGz637CC9BR++b7Epi9Pf5l/tfxnQ3K9DADWietrLNPtj5gcFKt+ 5eNu/Nio5JIk2kNrYrhV/erBvGy2i/MOjZrkm2xpmfh4SDBF1a3hDTxFYPwyllEnvGfDyi62a+pG x8cgoLEfZd5ICLqkTqnyg0Y3hOvozIFIQ2dOciqbXL1MGyiKXCJ7tKuY2e7gUYPDCUZObT6Z+pUX 2nwzV0E8jVHtC7ZcryxjGt9XyD+86V3Em69FmeKjWiS0uqlWPc9vqv9JWL7wqP/0uK3pN/u6uPQL OvnoQ0IeidiEyxPx2bvhiWC4jChWrBQdnArncevPDt09qZahSL0896+1DSJMwBGB7FY79tOi4lu3 sgQiUpWAk2nojkxl8ZEDLXB0AuqLZxUpaVICu9ffUGpVRr+goyhhf3DQw6KqLCGqR84onAZFdr+C GCe01a60y1Dma/RMhnEw6abfFobg2P9A3fvQQoh/ozM6LlweQRGBY84YcWsr7KaKtzFcOmpH4MN5 WdYgGq/yapiqcrxXStJLnbsQ/LBMQeXtHT1eKJ2czL+zUdqnR+WEUwIDAQABo0IwQDAdBgNVHQ4E FgQUu69+Aj36pvE8hI6t7jiY7NkyMtQwDgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8w DQYJKoZIhvcNAQEMBQADggIBAArx1UaEt65Ru2yyTUEUAJNMnMvlwFTPoCWOAvn9sKIN9SCYPBMt rFaisNZ+EZLpLrqeLppysb0ZRGxhNaKatBYSaVqM4dc+pBroLwP0rmEdEBsqpIt6xf4FpuHA1sj+ nq6PK7o9mfjYcwlYRm6mnPTXJ9OV2jeDchzTc+CiR5kDOF3VSXkAKRzH7JsgHAckaVd4sjn8OoSg tZx8jb8uk2IntznaFxiuvTwJaP+EmzzV1gsD41eeFPfR60/IvYcjt7ZJQ3mFXLrrkguhxuhoqEwW sRqZCuhTLJK7oQkYdQxlqHvLI7cawiiFwxv/0Cti76R7CZGYZ4wUAc1oBmpjIXUDgIiKboHGhfKp pC3n9KUkEEeDys30jXlYsQab5xoq2Z0B15R97QNKyvDb6KkBPvVWmckejkk9u+UJueBPSZI9FoJA zMxZxuY67RIuaTxslbH9qh17f4a+Hg4yRvv7E491f0yLS0Zj/gA0QHDBw7mh3aZw4gSzQbzpgJHq ZJx64SIDqZxubw5lT2yHh17zbqD5daWbQOhTsiedSrnAdyGN/4fy3ryM7xfft0kL0fJuMAsaDk52 7RH89elWsn2/x20Kk4yl0MC2Hb46TpSi125sC8KKfPog88Tk5c0NqMuRkrF8hey1FGlmDoLnzc7I LaZRfyHBNVOFBkpdn627G190 -----END CERTIFICATE----- USERTrust RSA Certification Authority ===================================== -----BEGIN CERTIFICATE----- MIIF3jCCA8agAwIBAgIQAf1tMPyjylGoG7xkDjUDLTANBgkqhkiG9w0BAQwFADCBiDELMAkGA1UE BhMCVVMxEzARBgNVBAgTCk5ldyBKZXJzZXkxFDASBgNVBAcTC0plcnNleSBDaXR5MR4wHAYDVQQK ExVUaGUgVVNFUlRSVVNUIE5ldHdvcmsxLjAsBgNVBAMTJVVTRVJUcnVzdCBSU0EgQ2VydGlmaWNh dGlvbiBBdXRob3JpdHkwHhcNMTAwMjAxMDAwMDAwWhcNMzgwMTE4MjM1OTU5WjCBiDELMAkGA1UE BhMCVVMxEzARBgNVBAgTCk5ldyBKZXJzZXkxFDASBgNVBAcTC0plcnNleSBDaXR5MR4wHAYDVQQK ExVUaGUgVVNFUlRSVVNUIE5ldHdvcmsxLjAsBgNVBAMTJVVTRVJUcnVzdCBSU0EgQ2VydGlmaWNh dGlvbiBBdXRob3JpdHkwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCAEmUXNg7D2wiz 0KxXDXbtzSfTTK1Qg2HiqiBNCS1kCdzOiZ/MPans9s/B3PHTsdZ7NygRK0faOca8Ohm0X6a9fZ2j Y0K2dvKpOyuR+OJv0OwWIJAJPuLodMkYtJHUYmTbf6MG8YgYapAiPLz+E/CHFHv25B+O1ORRxhFn RghRy4YUVD+8M/5+bJz/Fp0YvVGONaanZshyZ9shZrHUm3gDwFA66Mzw3LyeTP6vBZY1H1dat//O +T23LLb2VN3I5xI6Ta5MirdcmrS3ID3KfyI0rn47aGYBROcBTkZTmzNg95S+UzeQc0PzMsNT79uq /nROacdrjGCT3sTHDN/hMq7MkztReJVni+49Vv4M0GkPGw/zJSZrM233bkf6c0Plfg6lZrEpfDKE Y1WJxA3Bk1QwGROs0303p+tdOmw1XNtB1xLaqUkL39iAigmTYo61Zs8liM2EuLE/pDkP2QKe6xJM lXzzawWpXhaDzLhn4ugTncxbgtNMs+1b/97lc6wjOy0AvzVVdAlJ2ElYGn+SNuZRkg7zJn0cTRe8 yexDJtC/QV9AqURE9JnnV4eeUB9XVKg+/XRjL7FQZQnmWEIuQxpMtPAlR1n6BB6T1CZGSlCBst6+ eLf8ZxXhyVeEHg9j1uliutZfVS7qXMYoCAQlObgOK6nyTJccBz8NUvXt7y+CDwIDAQABo0IwQDAd BgNVHQ4EFgQUU3m/WqorSs9UgOHYm8Cd8rIDZsswDgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQF MAMBAf8wDQYJKoZIhvcNAQEMBQADggIBAFzUfA3P9wF9QZllDHPFUp/L+M+ZBn8b2kMVn54CVVeW FPFSPCeHlCjtHzoBN6J2/FNQwISbxmtOuowhT6KOVWKR82kV2LyI48SqC/3vqOlLVSoGIG1VeCkZ 7l8wXEskEVX/JJpuXior7gtNn3/3ATiUFJVDBwn7YKnuHKsSjKCaXqeYalltiz8I+8jRRa8YFWSQ Eg9zKC7F4iRO/Fjs8PRF/iKz6y+O0tlFYQXBl2+odnKPi4w2r78NBc5xjeambx9spnFixdjQg3IM 8WcRiQycE0xyNN+81XHfqnHd4blsjDwSXWXavVcStkNr/+XeTWYRUc+ZruwXtuhxkYzeSf7dNXGi FSeUHM9h4ya7b6NnJSFd5t0dCy5oGzuCr+yDZ4XUmFF0sbmZgIn/f3gZXHlKYC6SQK5MNyosycdi yA5d9zZbyuAlJQG03RoHnHcAP9Dc1ew91Pq7P8yF1m9/qS3fuQL39ZeatTXaw2ewh0qpKJ4jjv9c J2vhsE/zB+4ALtRZh8tSQZXq9EfX7mRBVXyNWQKV3WKdwrnuWih0hKWbt5DHDAff9Yk2dDLWKMGw sAvgnEzDHNb842m1R0aBL6KCq9NjRHDEjf8tM7qtj3u1cIiuPhnPQCjY/MiQu12ZIvVS5ljFH4gx Q+6IHdfGjjxDah2nGN59PRbxYvnKkKj9 -----END CERTIFICATE----- USERTrust ECC Certification Authority ===================================== -----BEGIN CERTIFICATE----- MIICjzCCAhWgAwIBAgIQXIuZxVqUxdJxVt7NiYDMJjAKBggqhkjOPQQDAzCBiDELMAkGA1UEBhMC VVMxEzARBgNVBAgTCk5ldyBKZXJzZXkxFDASBgNVBAcTC0plcnNleSBDaXR5MR4wHAYDVQQKExVU aGUgVVNFUlRSVVNUIE5ldHdvcmsxLjAsBgNVBAMTJVVTRVJUcnVzdCBFQ0MgQ2VydGlmaWNhdGlv biBBdXRob3JpdHkwHhcNMTAwMjAxMDAwMDAwWhcNMzgwMTE4MjM1OTU5WjCBiDELMAkGA1UEBhMC VVMxEzARBgNVBAgTCk5ldyBKZXJzZXkxFDASBgNVBAcTC0plcnNleSBDaXR5MR4wHAYDVQQKExVU aGUgVVNFUlRSVVNUIE5ldHdvcmsxLjAsBgNVBAMTJVVTRVJUcnVzdCBFQ0MgQ2VydGlmaWNhdGlv biBBdXRob3JpdHkwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAAQarFRaqfloI+d61SRvU8Za2EurxtW2 0eZzca7dnNYMYf3boIkDuAUU7FfO7l0/4iGzzvfUinngo4N+LZfQYcTxmdwlkWOrfzCjtHDix6Ez nPO/LlxTsV+zfTJ/ijTjeXmjQjBAMB0GA1UdDgQWBBQ64QmG1M8ZwpZ2dEl23OA1xmNjmjAOBgNV HQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUwAwEB/zAKBggqhkjOPQQDAwNoADBlAjA2Z6EWCNzklwBB HU6+4WMBzzuqQhFkoJ2UOQIReVx7Hfpkue4WQrO/isIJxOzksU0CMQDpKmFHjFJKS04YcPbWRNZu 9YO6bVi9JNlWSOrvxKJGgYhqOkbRqZtNyWHa0V1Xahg= -----END CERTIFICATE----- GlobalSign ECC Root CA - R4 =========================== -----BEGIN CERTIFICATE----- MIIB4TCCAYegAwIBAgIRKjikHJYKBN5CsiilC+g0mAIwCgYIKoZIzj0EAwIwUDEkMCIGA1UECxMb R2xvYmFsU2lnbiBFQ0MgUm9vdCBDQSAtIFI0MRMwEQYDVQQKEwpHbG9iYWxTaWduMRMwEQYDVQQD EwpHbG9iYWxTaWduMB4XDTEyMTExMzAwMDAwMFoXDTM4MDExOTAzMTQwN1owUDEkMCIGA1UECxMb R2xvYmFsU2lnbiBFQ0MgUm9vdCBDQSAtIFI0MRMwEQYDVQQKEwpHbG9iYWxTaWduMRMwEQYDVQQD EwpHbG9iYWxTaWduMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEuMZ5049sJQ6fLjkZHAOkrprl OQcJFspjsbmG+IpXwVfOQvpzofdlQv8ewQCybnMO/8ch5RikqtlxP6jUuc6MHaNCMEAwDgYDVR0P AQH/BAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFFSwe61FuOJAf/sKbvu+M8k8o4TV MAoGCCqGSM49BAMCA0gAMEUCIQDckqGgE6bPA7DmxCGXkPoUVy0D7O48027KqGx2vKLeuwIgJ6iF JzWbVsaj8kfSt24bAgAXqmemFZHe+pTsewv4n4Q= -----END CERTIFICATE----- GlobalSign ECC Root CA - R5 =========================== -----BEGIN CERTIFICATE----- MIICHjCCAaSgAwIBAgIRYFlJ4CYuu1X5CneKcflK2GwwCgYIKoZIzj0EAwMwUDEkMCIGA1UECxMb R2xvYmFsU2lnbiBFQ0MgUm9vdCBDQSAtIFI1MRMwEQYDVQQKEwpHbG9iYWxTaWduMRMwEQYDVQQD EwpHbG9iYWxTaWduMB4XDTEyMTExMzAwMDAwMFoXDTM4MDExOTAzMTQwN1owUDEkMCIGA1UECxMb R2xvYmFsU2lnbiBFQ0MgUm9vdCBDQSAtIFI1MRMwEQYDVQQKEwpHbG9iYWxTaWduMRMwEQYDVQQD EwpHbG9iYWxTaWduMHYwEAYHKoZIzj0CAQYFK4EEACIDYgAER0UOlvt9Xb/pOdEh+J8LttV7HpI6 SFkc8GIxLcB6KP4ap1yztsyX50XUWPrRd21DosCHZTQKH3rd6zwzocWdTaRvQZU4f8kehOvRnkmS h5SHDDqFSmafnVmTTZdhBoZKo0IwQDAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUwAwEB/zAd BgNVHQ4EFgQUPeYpSJvqB8ohREom3m7e0oPQn1kwCgYIKoZIzj0EAwMDaAAwZQIxAOVpEslu28Yx uglB4Zf4+/2a4n0Sye18ZNPLBSWLVtmg515dTguDnFt2KaAJJiFqYgIwcdK1j1zqO+F4CYWodZI7 yFz9SO8NdCKoCOJuxUnOxwy8p2Fp8fc74SrL+SvzZpA3 -----END CERTIFICATE----- Staat der Nederlanden Root CA - G3 ================================== -----BEGIN CERTIFICATE----- MIIFdDCCA1ygAwIBAgIEAJiiOTANBgkqhkiG9w0BAQsFADBaMQswCQYDVQQGEwJOTDEeMBwGA1UE CgwVU3RhYXQgZGVyIE5lZGVybGFuZGVuMSswKQYDVQQDDCJTdGFhdCBkZXIgTmVkZXJsYW5kZW4g Um9vdCBDQSAtIEczMB4XDTEzMTExNDExMjg0MloXDTI4MTExMzIzMDAwMFowWjELMAkGA1UEBhMC TkwxHjAcBgNVBAoMFVN0YWF0IGRlciBOZWRlcmxhbmRlbjErMCkGA1UEAwwiU3RhYXQgZGVyIE5l ZGVybGFuZGVuIFJvb3QgQ0EgLSBHMzCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAL4y olQPcPssXFnrbMSkUeiFKrPMSjTysF/zDsccPVMeiAho2G89rcKezIJnByeHaHE6n3WWIkYFsO2t x1ueKt6c/DrGlaf1F2cY5y9JCAxcz+bMNO14+1Cx3Gsy8KL+tjzk7FqXxz8ecAgwoNzFs21v0IJy EavSgWhZghe3eJJg+szeP4TrjTgzkApyI/o1zCZxMdFyKJLZWyNtZrVtB0LrpjPOktvA9mxjeM3K Tj215VKb8b475lRgsGYeCasH/lSJEULR9yS6YHgamPfJEf0WwTUaVHXvQ9Plrk7O53vDxk5hUUur mkVLoR9BvUhTFXFkC4az5S6+zqQbwSmEorXLCCN2QyIkHxcE1G6cxvx/K2Ya7Irl1s9N9WMJtxU5 1nus6+N86U78dULI7ViVDAZCopz35HCz33JvWjdAidiFpNfxC95DGdRKWCyMijmev4SH8RY7Ngzp 07TKbBlBUgmhHbBqv4LvcFEhMtwFdozL92TkA1CvjJFnq8Xy7ljY3r735zHPbMk7ccHViLVlvMDo FxcHErVc0qsgk7TmgoNwNsXNo42ti+yjwUOH5kPiNL6VizXtBznaqB16nzaeErAMZRKQFWDZJkBE 41ZgpRDUajz9QdwOWke275dhdU/Z/seyHdTtXUmzqWrLZoQT1Vyg3N9udwbRcXXIV2+vD3dbAgMB AAGjQjBAMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgEGMB0GA1UdDgQWBBRUrfrHkleu yjWcLhL75LpdINyUVzANBgkqhkiG9w0BAQsFAAOCAgEAMJmdBTLIXg47mAE6iqTnB/d6+Oea31BD U5cqPco8R5gu4RV78ZLzYdqQJRZlwJ9UXQ4DO1t3ApyEtg2YXzTdO2PCwyiBwpwpLiniyMMB8jPq KqrMCQj3ZWfGzd/TtiunvczRDnBfuCPRy5FOCvTIeuXZYzbB1N/8Ipf3YF3qKS9Ysr1YvY2WTxB1 v0h7PVGHoTx0IsL8B3+A3MSs/mrBcDCw6Y5p4ixpgZQJut3+TcCDjJRYwEYgr5wfAvg1VUkvRtTA 8KCWAg8zxXHzniN9lLf9OtMJgwYh/WA9rjLA0u6NpvDntIJ8CsxwyXmA+P5M9zWEGYox+wrZ13+b 8KKaa8MFSu1BYBQw0aoRQm7TIwIEC8Zl3d1Sd9qBa7Ko+gE4uZbqKmxnl4mUnrzhVNXkanjvSr0r mj1AfsbAddJu+2gw7OyLnflJNZoaLNmzlTnVHpL3prllL+U9bTpITAjc5CgSKL59NVzq4BZ+Extq 1z7XnvwtdbLBFNUjA9tbbws+eC8N3jONFrdI54OagQ97wUNNVQQXOEpR1VmiiXTTn74eS9fGbbeI JG9gkaSChVtWQbzQRKtqE77RLFi3EjNYsjdj3BP1lB0/QFH1T/U67cjF68IeHRaVesd+QnGTbksV tzDfqu1XhUisHWrdOWnk4Xl4vs4Fv6EM94B7IWcnMFk= -----END CERTIFICATE----- Staat der Nederlanden EV Root CA ================================ -----BEGIN CERTIFICATE----- MIIFcDCCA1igAwIBAgIEAJiWjTANBgkqhkiG9w0BAQsFADBYMQswCQYDVQQGEwJOTDEeMBwGA1UE CgwVU3RhYXQgZGVyIE5lZGVybGFuZGVuMSkwJwYDVQQDDCBTdGFhdCBkZXIgTmVkZXJsYW5kZW4g RVYgUm9vdCBDQTAeFw0xMDEyMDgxMTE5MjlaFw0yMjEyMDgxMTEwMjhaMFgxCzAJBgNVBAYTAk5M MR4wHAYDVQQKDBVTdGFhdCBkZXIgTmVkZXJsYW5kZW4xKTAnBgNVBAMMIFN0YWF0IGRlciBOZWRl cmxhbmRlbiBFViBSb290IENBMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA48d+ifkk SzrSM4M1LGns3Amk41GoJSt5uAg94JG6hIXGhaTK5skuU6TJJB79VWZxXSzFYGgEt9nCUiY4iKTW O0Cmws0/zZiTs1QUWJZV1VD+hq2kY39ch/aO5ieSZxeSAgMs3NZmdO3dZ//BYY1jTw+bbRcwJu+r 0h8QoPnFfxZpgQNH7R5ojXKhTbImxrpsX23Wr9GxE46prfNeaXUmGD5BKyF/7otdBwadQ8QpCiv8 Kj6GyzyDOvnJDdrFmeK8eEEzduG/L13lpJhQDBXd4Pqcfzho0LKmeqfRMb1+ilgnQ7O6M5HTp5gV XJrm0w912fxBmJc+qiXbj5IusHsMX/FjqTf5m3VpTCgmJdrV8hJwRVXj33NeN/UhbJCONVrJ0yPr 08C+eKxCKFhmpUZtcALXEPlLVPxdhkqHz3/KRawRWrUgUY0viEeXOcDPusBCAUCZSCELa6fS/ZbV 0b5GnUngC6agIk440ME8MLxwjyx1zNDFjFE7PZQIZCZhfbnDZY8UnCHQqv0XcgOPvZuM5l5Tnrmd 74K74bzickFbIZTTRTeU0d8JOV3nI6qaHcptqAqGhYqCvkIH1vI4gnPah1vlPNOePqc7nvQDs/nx fRN0Av+7oeX6AHkcpmZBiFxgV6YuCcS6/ZrPpx9Aw7vMWgpVSzs4dlG4Y4uElBbmVvMCAwEAAaNC MEAwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYwHQYDVR0OBBYEFP6rAJCYniT8qcwa ivsnuL8wbqg7MA0GCSqGSIb3DQEBCwUAA4ICAQDPdyxuVr5Os7aEAJSrR8kN0nbHhp8dB9O2tLsI eK9p0gtJ3jPFrK3CiAJ9Brc1AsFgyb/E6JTe1NOpEyVa/m6irn0F3H3zbPB+po3u2dfOWBfoqSmu c0iH55vKbimhZF8ZE/euBhD/UcabTVUlT5OZEAFTdfETzsemQUHSv4ilf0X8rLiltTMMgsT7B/Zq 5SWEXwbKwYY5EdtYzXc7LMJMD16a4/CrPmEbUCTCwPTxGfARKbalGAKb12NMcIxHowNDXLldRqAN b/9Zjr7dn3LDWyvfjFvO5QxGbJKyCqNMVEIYFRIYvdr8unRu/8G2oGTYqV9Vrp9canaW2HNnh/tN f1zuacpzEPuKqf2evTY4SUmH9A4U8OmHuD+nT3pajnnUk+S7aFKErGzp85hwVXIy+TSrK0m1zSBi 5Dp6Z2Orltxtrpfs/J92VoguZs9btsmksNcFuuEnL5O7Jiqik7Ab846+HUCjuTaPPoIaGl6I6lD4 WeKDRikL40Rc4ZW2aZCaFG+XroHPaO+Zmr615+F/+PoTRxZMzG0IQOeLeG9QgkRQP2YGiqtDhFZK DyAthg710tvSeopLzaXoTvFeJiUBWSOgftL2fiFX1ye8FVdMpEbB4IMeDExNH08GGeL5qPQ6gqGy eUN51q1veieQA6TqJIc/2b3Z6fJfUEkc7uzXLg== -----END CERTIFICATE----- IdenTrust Commercial Root CA 1 ============================== -----BEGIN CERTIFICATE----- MIIFYDCCA0igAwIBAgIQCgFCgAAAAUUjyES1AAAAAjANBgkqhkiG9w0BAQsFADBKMQswCQYDVQQG EwJVUzESMBAGA1UEChMJSWRlblRydXN0MScwJQYDVQQDEx5JZGVuVHJ1c3QgQ29tbWVyY2lhbCBS b290IENBIDEwHhcNMTQwMTE2MTgxMjIzWhcNMzQwMTE2MTgxMjIzWjBKMQswCQYDVQQGEwJVUzES MBAGA1UEChMJSWRlblRydXN0MScwJQYDVQQDEx5JZGVuVHJ1c3QgQ29tbWVyY2lhbCBSb290IENB IDEwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCnUBneP5k91DNG8W9RYYKyqU+PZ4ld hNlT3Qwo2dfw/66VQ3KZ+bVdfIrBQuExUHTRgQ18zZshq0PirK1ehm7zCYofWjK9ouuU+ehcCuz/ mNKvcbO0U59Oh++SvL3sTzIwiEsXXlfEU8L2ApeN2WIrvyQfYo3fw7gpS0l4PJNgiCL8mdo2yMKi 1CxUAGc1bnO/AljwpN3lsKImesrgNqUZFvX9t++uP0D1bVoE/c40yiTcdCMbXTMTEl3EASX2MN0C XZ/g1Ue9tOsbobtJSdifWwLziuQkkORiT0/Br4sOdBeo0XKIanoBScy0RnnGF7HamB4HWfp1IYVl 3ZBWzvurpWCdxJ35UrCLvYf5jysjCiN2O/cz4ckA82n5S6LgTrx+kzmEB/dEcH7+B1rlsazRGMzy NeVJSQjKVsk9+w8YfYs7wRPCTY/JTw436R+hDmrfYi7LNQZReSzIJTj0+kuniVyc0uMNOYZKdHzV WYfCP04MXFL0PfdSgvHqo6z9STQaKPNBiDoT7uje/5kdX7rL6B7yuVBgwDHTc+XvvqDtMwt0viAg xGds8AgDelWAf0ZOlqf0Hj7h9tgJ4TNkK2PXMl6f+cB7D3hvl7yTmvmcEpB4eoCHFddydJxVdHix uuFucAS6T6C6aMN7/zHwcz09lCqxC0EOoP5NiGVreTO01wIDAQABo0IwQDAOBgNVHQ8BAf8EBAMC AQYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQU7UQZwNPwBovupHu+QucmVMiONnYwDQYJKoZI hvcNAQELBQADggIBAA2ukDL2pkt8RHYZYR4nKM1eVO8lvOMIkPkp165oCOGUAFjvLi5+U1KMtlwH 6oi6mYtQlNeCgN9hCQCTrQ0U5s7B8jeUeLBfnLOic7iPBZM4zY0+sLj7wM+x8uwtLRvM7Kqas6pg ghstO8OEPVeKlh6cdbjTMM1gCIOQ045U8U1mwF10A0Cj7oV+wh93nAbowacYXVKV7cndJZ5t+qnt ozo00Fl72u1Q8zW/7esUTTHHYPTa8Yec4kjixsU3+wYQ+nVZZjFHKdp2mhzpgq7vmrlR94gjmmmV YjzlVYA211QC//G5Xc7UI2/YRYRKW2XviQzdFKcgyxilJbQN+QHwotL0AMh0jqEqSI5l2xPE4iUX feu+h1sXIFRRk0pTAwvsXcoz7WL9RccvW9xYoIA55vrX/hMUpu09lEpCdNTDd1lzzY9GvlU47/ro kTLql1gEIt44w8y8bckzOmoKaT+gyOpyj4xjhiO9bTyWnpXgSUyqorkqG5w2gXjtw+hG4iZZRHUe 2XWJUc0QhJ1hYMtd+ZciTY6Y5uN/9lu7rs3KSoFrXgvzUeF0K+l+J6fZmUlO+KWA2yUPHGNiiskz Z2s8EIPGrd6ozRaOjfAHN3Gf8qv8QfXBi+wAN10J5U6A7/qxXDgGpRtK4dw4LTzcqx+QGtVKnO7R cGzM7vRX+Bi6hG6H -----END CERTIFICATE----- IdenTrust Public Sector Root CA 1 ================================= -----BEGIN CERTIFICATE----- MIIFZjCCA06gAwIBAgIQCgFCgAAAAUUjz0Z8AAAAAjANBgkqhkiG9w0BAQsFADBNMQswCQYDVQQG EwJVUzESMBAGA1UEChMJSWRlblRydXN0MSowKAYDVQQDEyFJZGVuVHJ1c3QgUHVibGljIFNlY3Rv ciBSb290IENBIDEwHhcNMTQwMTE2MTc1MzMyWhcNMzQwMTE2MTc1MzMyWjBNMQswCQYDVQQGEwJV UzESMBAGA1UEChMJSWRlblRydXN0MSowKAYDVQQDEyFJZGVuVHJ1c3QgUHVibGljIFNlY3RvciBS b290IENBIDEwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQC2IpT8pEiv6EdrCvsnduTy P4o7ekosMSqMjbCpwzFrqHd2hCa2rIFCDQjrVVi7evi8ZX3yoG2LqEfpYnYeEe4IFNGyRBb06tD6 Hi9e28tzQa68ALBKK0CyrOE7S8ItneShm+waOh7wCLPQ5CQ1B5+ctMlSbdsHyo+1W/CD80/HLaXI rcuVIKQxKFdYWuSNG5qrng0M8gozOSI5Cpcu81N3uURF/YTLNiCBWS2ab21ISGHKTN9T0a9SvESf qy9rg3LvdYDaBjMbXcjaY8ZNzaxmMc3R3j6HEDbhuaR672BQssvKplbgN6+rNBM5Jeg5ZuSYeqoS mJxZZoY+rfGwyj4GD3vwEUs3oERte8uojHH01bWRNszwFcYr3lEXsZdMUD2xlVl8BX0tIdUAvwFn ol57plzy9yLxkA2T26pEUWbMfXYD62qoKjgZl3YNa4ph+bz27nb9cCvdKTz4Ch5bQhyLVi9VGxyh LrXHFub4qjySjmm2AcG1hp2JDws4lFTo6tyePSW8Uybt1as5qsVATFSrsrTZ2fjXctscvG29ZV/v iDUqZi/u9rNl8DONfJhBaUYPQxxp+pu10GFqzcpL2UyQRqsVWaFHVCkugyhfHMKiq3IXAAaOReyL 4jM9f9oZRORicsPfIsbyVtTdX5Vy7W1f90gDW/3FKqD2cyOEEBsB5wIDAQABo0IwQDAOBgNVHQ8B Af8EBAMCAQYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQU43HgntinQtnbcZFrlJPrw6PRFKMw DQYJKoZIhvcNAQELBQADggIBAEf63QqwEZE4rU1d9+UOl1QZgkiHVIyqZJnYWv6IAcVYpZmxI1Qj t2odIFflAWJBF9MJ23XLblSQdf4an4EKwt3X9wnQW3IV5B4Jaj0z8yGa5hV+rVHVDRDtfULAj+7A mgjVQdZcDiFpboBhDhXAuM/FSRJSzL46zNQuOAXeNf0fb7iAaJg9TaDKQGXSc3z1i9kKlT/YPyNt GtEqJBnZhbMX73huqVjRI9PHE+1yJX9dsXNw0H8GlwmEKYBhHfpe/3OsoOOJuBxxFcbeMX8S3OFt m6/n6J91eEyrRjuazr8FGF1NFTwWmhlQBJqymm9li1JfPFgEKCXAZmExfrngdbkaqIHWchezxQMx NRF4eKLg6TCMf4DfWN88uieW4oA0beOY02QnrEh+KHdcxiVhJfiFDGX6xDIvpZgF5PgLZxYWxoK4 Mhn5+bl53B/N66+rDt0b20XkeucC4pVd/GnwU2lhlXV5C15V5jgclKlZM57IcXR5f1GJtshquDDI ajjDbp7hNxbqBWJMWxJH7ae0s1hWx0nzfxJoCTFx8G34Tkf71oXuxVhAGaQdp/lLQzfcaFpPz+vC ZHTetBXZ9FRUGi8c15dxVJCO2SCdUyt/q4/i6jC8UDfv8Ue1fXwsBOxonbRJRBD0ckscZOf85muQ 3Wl9af0AVqW3rLatt8o+Ae+c -----END CERTIFICATE----- Entrust Root Certification Authority - G2 ========================================= -----BEGIN CERTIFICATE----- MIIEPjCCAyagAwIBAgIESlOMKDANBgkqhkiG9w0BAQsFADCBvjELMAkGA1UEBhMCVVMxFjAUBgNV BAoTDUVudHJ1c3QsIEluYy4xKDAmBgNVBAsTH1NlZSB3d3cuZW50cnVzdC5uZXQvbGVnYWwtdGVy bXMxOTA3BgNVBAsTMChjKSAyMDA5IEVudHJ1c3QsIEluYy4gLSBmb3IgYXV0aG9yaXplZCB1c2Ug b25seTEyMDAGA1UEAxMpRW50cnVzdCBSb290IENlcnRpZmljYXRpb24gQXV0aG9yaXR5IC0gRzIw HhcNMDkwNzA3MTcyNTU0WhcNMzAxMjA3MTc1NTU0WjCBvjELMAkGA1UEBhMCVVMxFjAUBgNVBAoT DUVudHJ1c3QsIEluYy4xKDAmBgNVBAsTH1NlZSB3d3cuZW50cnVzdC5uZXQvbGVnYWwtdGVybXMx OTA3BgNVBAsTMChjKSAyMDA5IEVudHJ1c3QsIEluYy4gLSBmb3IgYXV0aG9yaXplZCB1c2Ugb25s eTEyMDAGA1UEAxMpRW50cnVzdCBSb290IENlcnRpZmljYXRpb24gQXV0aG9yaXR5IC0gRzIwggEi MA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC6hLZy254Ma+KZ6TABp3bqMriVQRrJ2mFOWHLP /vaCeb9zYQYKpSfYs1/TRU4cctZOMvJyig/3gxnQaoCAAEUesMfnmr8SVycco2gvCoe9amsOXmXz HHfV1IWNcCG0szLni6LVhjkCsbjSR87kyUnEO6fe+1R9V77w6G7CebI6C1XiUJgWMhNcL3hWwcKU s/Ja5CeanyTXxuzQmyWC48zCxEXFjJd6BmsqEZ+pCm5IO2/b1BEZQvePB7/1U1+cPvQXLOZprE4y TGJ36rfo5bs0vBmLrpxR57d+tVOxMyLlbc9wPBr64ptntoP0jaWvYkxN4FisZDQSA/i2jZRjJKRx AgMBAAGjQjBAMA4GA1UdDwEB/wQEAwIBBjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBRqciZ6 0B7vfec7aVHUbI2fkBJmqzANBgkqhkiG9w0BAQsFAAOCAQEAeZ8dlsa2eT8ijYfThwMEYGprmi5Z iXMRrEPR9RP/jTkrwPK9T3CMqS/qF8QLVJ7UG5aYMzyorWKiAHarWWluBh1+xLlEjZivEtRh2woZ Rkfz6/djwUAFQKXSt/S1mja/qYh2iARVBCuch38aNzx+LaUa2NSJXsq9rD1s2G2v1fN2D807iDgi nWyTmsQ9v4IbZT+mD12q/OWyFcq1rca8PdCE6OoGcrBNOTJ4vz4RnAuknZoh8/CbCzB428Hch0P+ vGOaysXCHMnHjf87ElgI5rY97HosTvuDls4MPGmHVHOkc8KT/1EQrBVUAdj8BbGJoX90g5pJ19xO e4pIb4tF9g== -----END CERTIFICATE----- Entrust Root Certification Authority - EC1 ========================================== -----BEGIN CERTIFICATE----- MIIC+TCCAoCgAwIBAgINAKaLeSkAAAAAUNCR+TAKBggqhkjOPQQDAzCBvzELMAkGA1UEBhMCVVMx FjAUBgNVBAoTDUVudHJ1c3QsIEluYy4xKDAmBgNVBAsTH1NlZSB3d3cuZW50cnVzdC5uZXQvbGVn YWwtdGVybXMxOTA3BgNVBAsTMChjKSAyMDEyIEVudHJ1c3QsIEluYy4gLSBmb3IgYXV0aG9yaXpl ZCB1c2Ugb25seTEzMDEGA1UEAxMqRW50cnVzdCBSb290IENlcnRpZmljYXRpb24gQXV0aG9yaXR5 IC0gRUMxMB4XDTEyMTIxODE1MjUzNloXDTM3MTIxODE1NTUzNlowgb8xCzAJBgNVBAYTAlVTMRYw FAYDVQQKEw1FbnRydXN0LCBJbmMuMSgwJgYDVQQLEx9TZWUgd3d3LmVudHJ1c3QubmV0L2xlZ2Fs LXRlcm1zMTkwNwYDVQQLEzAoYykgMjAxMiBFbnRydXN0LCBJbmMuIC0gZm9yIGF1dGhvcml6ZWQg dXNlIG9ubHkxMzAxBgNVBAMTKkVudHJ1c3QgUm9vdCBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eSAt IEVDMTB2MBAGByqGSM49AgEGBSuBBAAiA2IABIQTydC6bUF74mzQ61VfZgIaJPRbiWlH47jCffHy AsWfoPZb1YsGGYZPUxBtByQnoaD41UcZYUx9ypMn6nQM72+WCf5j7HBdNq1nd67JnXxVRDqiY1Ef 9eNi1KlHBz7MIKNCMEAwDgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYE FLdj5xrdjekIplWDpOBqUEFlEUJJMAoGCCqGSM49BAMDA2cAMGQCMGF52OVCR98crlOZF7ZvHH3h vxGU0QOIdeSNiaSKd0bebWHvAvX7td/M/k7//qnmpwIwW5nXhTcGtXsI/esni0qU+eH6p44mCOh8 kmhtc9hvJqwhAriZtyZBWyVgrtBIGu4G -----END CERTIFICATE----- CFCA EV ROOT ============ -----BEGIN CERTIFICATE----- MIIFjTCCA3WgAwIBAgIEGErM1jANBgkqhkiG9w0BAQsFADBWMQswCQYDVQQGEwJDTjEwMC4GA1UE CgwnQ2hpbmEgRmluYW5jaWFsIENlcnRpZmljYXRpb24gQXV0aG9yaXR5MRUwEwYDVQQDDAxDRkNB IEVWIFJPT1QwHhcNMTIwODA4MDMwNzAxWhcNMjkxMjMxMDMwNzAxWjBWMQswCQYDVQQGEwJDTjEw MC4GA1UECgwnQ2hpbmEgRmluYW5jaWFsIENlcnRpZmljYXRpb24gQXV0aG9yaXR5MRUwEwYDVQQD DAxDRkNBIEVWIFJPT1QwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDXXWvNED8fBVnV BU03sQ7smCuOFR36k0sXgiFxEFLXUWRwFsJVaU2OFW2fvwwbwuCjZ9YMrM8irq93VCpLTIpTUnrD 7i7es3ElweldPe6hL6P3KjzJIx1qqx2hp/Hz7KDVRM8Vz3IvHWOX6Jn5/ZOkVIBMUtRSqy5J35DN uF++P96hyk0g1CXohClTt7GIH//62pCfCqktQT+x8Rgp7hZZLDRJGqgG16iI0gNyejLi6mhNbiyW ZXvKWfry4t3uMCz7zEasxGPrb382KzRzEpR/38wmnvFyXVBlWY9ps4deMm/DGIq1lY+wejfeWkU7 xzbh72fROdOXW3NiGUgthxwG+3SYIElz8AXSG7Ggo7cbcNOIabla1jj0Ytwli3i/+Oh+uFzJlU9f py25IGvPa931DfSCt/SyZi4QKPaXWnuWFo8BGS1sbn85WAZkgwGDg8NNkt0yxoekN+kWzqotaK8K gWU6cMGbrU1tVMoqLUuFG7OA5nBFDWteNfB/O7ic5ARwiRIlk9oKmSJgamNgTnYGmE69g60dWIol hdLHZR4tjsbftsbhf4oEIRUpdPA+nJCdDC7xij5aqgwJHsfVPKPtl8MeNPo4+QgO48BdK4PRVmrJ tqhUUy54Mmc9gn900PvhtgVguXDbjgv5E1hvcWAQUhC5wUEJ73IfZzF4/5YFjQIDAQABo2MwYTAf BgNVHSMEGDAWgBTj/i39KNALtbq2osS/BqoFjJP7LzAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB /wQEAwIBBjAdBgNVHQ4EFgQU4/4t/SjQC7W6tqLEvwaqBYyT+y8wDQYJKoZIhvcNAQELBQADggIB ACXGumvrh8vegjmWPfBEp2uEcwPenStPuiB/vHiyz5ewG5zz13ku9Ui20vsXiObTej/tUxPQ4i9q ecsAIyjmHjdXNYmEwnZPNDatZ8POQQaIxffu2Bq41gt/UP+TqhdLjOztUmCypAbqTuv0axn96/Ua 4CUqmtzHQTb3yHQFhDmVOdYLO6Qn+gjYXB74BGBSESgoA//vU2YApUo0FmZ8/Qmkrp5nGm9BC2sG E5uPhnEFtC+NiWYzKXZUmhH4J/qyP5Hgzg0b8zAarb8iXRvTvyUFTeGSGn+ZnzxEk8rUQElsgIfX BDrDMlI1Dlb4pd19xIsNER9Tyx6yF7Zod1rg1MvIB671Oi6ON7fQAUtDKXeMOZePglr4UeWJoBjn aH9dCi77o0cOPaYjesYBx4/IXr9tgFa+iiS6M+qf4TIRnvHST4D2G0CvOJ4RUHlzEhLN5mydLIhy PDCBBpEi6lmt2hkuIsKNuYyH4Ga8cyNfIWRjgEj1oDwYPZTISEEdQLpe/v5WOaHIz16eGWRGENoX kbcFgKyLmZJ956LYBws2J+dIeWCKw9cTXPhyQN9Ky8+ZAAoACxGV2lZFA4gKn2fQ1XmxqI1AbQ3C ekD6819kR5LLU7m7Wc5P/dAVUwHY3+vZ5nbv0CO7O6l5s9UCKc2Jo5YPSjXnTkLAdc0Hz+Ys63su -----END CERTIFICATE----- OISTE WISeKey Global Root GB CA =============================== -----BEGIN CERTIFICATE----- MIIDtTCCAp2gAwIBAgIQdrEgUnTwhYdGs/gjGvbCwDANBgkqhkiG9w0BAQsFADBtMQswCQYDVQQG EwJDSDEQMA4GA1UEChMHV0lTZUtleTEiMCAGA1UECxMZT0lTVEUgRm91bmRhdGlvbiBFbmRvcnNl ZDEoMCYGA1UEAxMfT0lTVEUgV0lTZUtleSBHbG9iYWwgUm9vdCBHQiBDQTAeFw0xNDEyMDExNTAw MzJaFw0zOTEyMDExNTEwMzFaMG0xCzAJBgNVBAYTAkNIMRAwDgYDVQQKEwdXSVNlS2V5MSIwIAYD VQQLExlPSVNURSBGb3VuZGF0aW9uIEVuZG9yc2VkMSgwJgYDVQQDEx9PSVNURSBXSVNlS2V5IEds b2JhbCBSb290IEdCIENBMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA2Be3HEokKtaX scriHvt9OO+Y9bI5mE4nuBFde9IllIiCFSZqGzG7qFshISvYD06fWvGxWuR51jIjK+FTzJlFXHtP rby/h0oLS5daqPZI7H17Dc0hBt+eFf1Biki3IPShehtX1F1Q/7pn2COZH8g/497/b1t3sWtuuMlk 9+HKQUYOKXHQuSP8yYFfTvdv37+ErXNku7dCjmn21HYdfp2nuFeKUWdy19SouJVUQHMD9ur06/4o Qnc/nSMbsrY9gBQHTC5P99UKFg29ZkM3fiNDecNAhvVMKdqOmq0NpQSHiB6F4+lT1ZvIiwNjeOvg GUpuuy9rM2RYk61pv48b74JIxwIDAQABo1EwTzALBgNVHQ8EBAMCAYYwDwYDVR0TAQH/BAUwAwEB /zAdBgNVHQ4EFgQUNQ/INmNe4qPs+TtmFc5RUuORmj0wEAYJKwYBBAGCNxUBBAMCAQAwDQYJKoZI hvcNAQELBQADggEBAEBM+4eymYGQfp3FsLAmzYh7KzKNbrghcViXfa43FK8+5/ea4n32cZiZBKpD dHij40lhPnOMTZTg+XHEthYOU3gf1qKHLwI5gSk8rxWYITD+KJAAjNHhy/peyP34EEY7onhCkRd0 VQreUGdNZtGn//3ZwLWoo4rOZvUPQ82nK1d7Y0Zqqi5S2PTt4W2tKZB4SLrhI6qjiey1q5bAtEui HZeeevJuQHHfaPFlTc58Bd9TZaml8LGXBHAVRgOY1NK/VLSgWH1Sb9pWJmLU2NuJMW8c8CLC02Ic Nc1MaRVUGpCY3useX8p3x8uOPUNpnJpY0CQ73xtAln41rYHHTnG6iBM= -----END CERTIFICATE----- SZAFIR ROOT CA2 =============== -----BEGIN CERTIFICATE----- MIIDcjCCAlqgAwIBAgIUPopdB+xV0jLVt+O2XwHrLdzk1uQwDQYJKoZIhvcNAQELBQAwUTELMAkG A1UEBhMCUEwxKDAmBgNVBAoMH0tyYWpvd2EgSXpiYSBSb3psaWN6ZW5pb3dhIFMuQS4xGDAWBgNV BAMMD1NaQUZJUiBST09UIENBMjAeFw0xNTEwMTkwNzQzMzBaFw0zNTEwMTkwNzQzMzBaMFExCzAJ BgNVBAYTAlBMMSgwJgYDVQQKDB9LcmFqb3dhIEl6YmEgUm96bGljemVuaW93YSBTLkEuMRgwFgYD VQQDDA9TWkFGSVIgUk9PVCBDQTIwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC3vD5Q qEvNQLXOYeeWyrSh2gwisPq1e3YAd4wLz32ohswmUeQgPYUM1ljj5/QqGJ3a0a4m7utT3PSQ1hNK DJA8w/Ta0o4NkjrcsbH/ON7Dui1fgLkCvUqdGw+0w8LBZwPd3BucPbOw3gAeqDRHu5rr/gsUvTaE 2g0gv/pby6kWIK05YO4vdbbnl5z5Pv1+TW9NL++IDWr63fE9biCloBK0TXC5ztdyO4mTp4CEHCdJ ckm1/zuVnsHMyAHs6A6KCpbns6aH5db5BSsNl0BwPLqsdVqc1U2dAgrSS5tmS0YHF2Wtn2yIANwi ieDhZNRnvDF5YTy7ykHNXGoAyDw4jlivAgMBAAGjQjBAMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0P AQH/BAQDAgEGMB0GA1UdDgQWBBQuFqlKGLXLzPVvUPMjX/hd56zwyDANBgkqhkiG9w0BAQsFAAOC AQEAtXP4A9xZWx126aMqe5Aosk3AM0+qmrHUuOQn/6mWmc5G4G18TKI4pAZw8PRBEew/R40/cof5 O/2kbytTAOD/OblqBw7rHRz2onKQy4I9EYKL0rufKq8h5mOGnXkZ7/e7DDWQw4rtTw/1zBLZpD67 oPwglV9PJi8RI4NOdQcPv5vRtB3pEAT+ymCPoky4rc/hkA/NrgrHXXu3UNLUYfrVFdvXn4dRVOul 4+vJhaAlIDf7js4MNIThPIGyd05DpYhfhmehPea0XGG2Ptv+tyjFogeutcrKjSoS75ftwjCkySp6 +/NNIxuZMzSgLvWpCz/UXeHPhJ/iGcJfitYgHuNztw== -----END CERTIFICATE----- Certum Trusted Network CA 2 =========================== -----BEGIN CERTIFICATE----- MIIF0jCCA7qgAwIBAgIQIdbQSk8lD8kyN/yqXhKN6TANBgkqhkiG9w0BAQ0FADCBgDELMAkGA1UE BhMCUEwxIjAgBgNVBAoTGVVuaXpldG8gVGVjaG5vbG9naWVzIFMuQS4xJzAlBgNVBAsTHkNlcnR1 bSBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTEkMCIGA1UEAxMbQ2VydHVtIFRydXN0ZWQgTmV0d29y ayBDQSAyMCIYDzIwMTExMDA2MDgzOTU2WhgPMjA0NjEwMDYwODM5NTZaMIGAMQswCQYDVQQGEwJQ TDEiMCAGA1UEChMZVW5pemV0byBUZWNobm9sb2dpZXMgUy5BLjEnMCUGA1UECxMeQ2VydHVtIENl cnRpZmljYXRpb24gQXV0aG9yaXR5MSQwIgYDVQQDExtDZXJ0dW0gVHJ1c3RlZCBOZXR3b3JrIENB IDIwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQC9+Xj45tWADGSdhhuWZGc/IjoedQF9 7/tcZ4zJzFxrqZHmuULlIEub2pt7uZld2ZuAS9eEQCsn0+i6MLs+CRqnSZXvK0AkwpfHp+6bJe+o CgCXhVqqndwpyeI1B+twTUrWwbNWuKFBOJvR+zF/j+Bf4bE/D44WSWDXBo0Y+aomEKsq09DRZ40b Rr5HMNUuctHFY9rnY3lEfktjJImGLjQ/KUxSiyqnwOKRKIm5wFv5HdnnJ63/mgKXwcZQkpsCLL2p uTRZCr+ESv/f/rOf69me4Jgj7KZrdxYq28ytOxykh9xGc14ZYmhFV+SQgkK7QtbwYeDBoz1mo130 GO6IyY0XRSmZMnUCMe4pJshrAua1YkV/NxVaI2iJ1D7eTiew8EAMvE0Xy02isx7QBlrd9pPPV3WZ 9fqGGmd4s7+W/jTcvedSVuWz5XV710GRBdxdaeOVDUO5/IOWOZV7bIBaTxNyxtd9KXpEulKkKtVB Rgkg/iKgtlswjbyJDNXXcPiHUv3a76xRLgezTv7QCdpw75j6VuZt27VXS9zlLCUVyJ4ueE742pye hizKV/Ma5ciSixqClnrDvFASadgOWkaLOusm+iPJtrCBvkIApPjW/jAux9JG9uWOdf3yzLnQh1vM BhBgu4M1t15n3kfsmUjxpKEV/q2MYo45VU85FrmxY53/twIDAQABo0IwQDAPBgNVHRMBAf8EBTAD AQH/MB0GA1UdDgQWBBS2oVQ5AsOgP46KvPrU+Bym0ToO/TAOBgNVHQ8BAf8EBAMCAQYwDQYJKoZI hvcNAQENBQADggIBAHGlDs7k6b8/ONWJWsQCYftMxRQXLYtPU2sQF/xlhMcQSZDe28cmk4gmb3DW Al45oPePq5a1pRNcgRRtDoGCERuKTsZPpd1iHkTfCVn0W3cLN+mLIMb4Ck4uWBzrM9DPhmDJ2vuA L55MYIR4PSFk1vtBHxgP58l1cb29XN40hz5BsA72udY/CROWFC/emh1auVbONTqwX3BNXuMp8SMo clm2q8KMZiYcdywmdjWLKKdpoPk79SPdhRB0yZADVpHnr7pH1BKXESLjokmUbOe3lEu6LaTaM4tM pkT/WjzGHWTYtTHkpjx6qFcL2+1hGsvxznN3Y6SHb0xRONbkX8eftoEq5IVIeVheO/jbAoJnwTnb w3RLPTYe+SmTiGhbqEQZIfCn6IENLOiTNrQ3ssqwGyZ6miUfmpqAnksqP/ujmv5zMnHCnsZy4Ypo J/HkD7TETKVhk/iXEAcqMCWpuchxuO9ozC1+9eB+D4Kob7a6bINDd82Kkhehnlt4Fj1F4jNy3eFm ypnTycUm/Q1oBEauttmbjL4ZvrHG8hnjXALKLNhvSgfZyTXaQHXyxKcZb55CEJh15pWLYLztxRLX is7VmFxWlgPF7ncGNf/P5O4/E2Hu29othfDNrp2yGAlFw5Khchf8R7agCyzxxN5DaAhqXzvwdmP7 zAYspsbiDrW5viSP -----END CERTIFICATE----- Hellenic Academic and Research Institutions RootCA 2015 ======================================================= -----BEGIN CERTIFICATE----- MIIGCzCCA/OgAwIBAgIBADANBgkqhkiG9w0BAQsFADCBpjELMAkGA1UEBhMCR1IxDzANBgNVBAcT BkF0aGVuczFEMEIGA1UEChM7SGVsbGVuaWMgQWNhZGVtaWMgYW5kIFJlc2VhcmNoIEluc3RpdHV0 aW9ucyBDZXJ0LiBBdXRob3JpdHkxQDA+BgNVBAMTN0hlbGxlbmljIEFjYWRlbWljIGFuZCBSZXNl YXJjaCBJbnN0aXR1dGlvbnMgUm9vdENBIDIwMTUwHhcNMTUwNzA3MTAxMTIxWhcNNDAwNjMwMTAx MTIxWjCBpjELMAkGA1UEBhMCR1IxDzANBgNVBAcTBkF0aGVuczFEMEIGA1UEChM7SGVsbGVuaWMg QWNhZGVtaWMgYW5kIFJlc2VhcmNoIEluc3RpdHV0aW9ucyBDZXJ0LiBBdXRob3JpdHkxQDA+BgNV BAMTN0hlbGxlbmljIEFjYWRlbWljIGFuZCBSZXNlYXJjaCBJbnN0aXR1dGlvbnMgUm9vdENBIDIw MTUwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDC+Kk/G4n8PDwEXT2QNrCROnk8Zlrv bTkBSRq0t89/TSNTt5AA4xMqKKYx8ZEA4yjsriFBzh/a/X0SWwGDD7mwX5nh8hKDgE0GPt+sr+eh iGsxr/CL0BgzuNtFajT0AoAkKAoCFZVedioNmToUW/bLy1O8E00BiDeUJRtCvCLYjqOWXjrZMts+ 6PAQZe104S+nfK8nNLspfZu2zwnI5dMK/IhlZXQK3HMcXM1AsRzUtoSMTFDPaI6oWa7CJ06CojXd FPQf/7J31Ycvqm59JCfnxssm5uX+Zwdj2EUN3TpZZTlYepKZcj2chF6IIbjV9Cz82XBST3i4vTwr i5WY9bPRaM8gFH5MXF/ni+X1NYEZN9cRCLdmvtNKzoNXADrDgfgXy5I2XdGj2HUb4Ysn6npIQf1F GQatJ5lOwXBH3bWfgVMS5bGMSF0xQxfjjMZ6Y5ZLKTBOhE5iGV48zpeQpX8B653g+IuJ3SWYPZK2 fu/Z8VFRfS0myGlZYeCsargqNhEEelC9MoS+L9xy1dcdFkfkR2YgP/SWxa+OAXqlD3pk9Q0Yh9mu iNX6hME6wGkoLfINaFGq46V3xqSQDqE3izEjR8EJCOtu93ib14L8hCCZSRm2Ekax+0VVFqmjZayc Bw/qa9wfLgZy7IaIEuQt218FL+TwA9MmM+eAws1CoRc0CwIDAQABo0IwQDAPBgNVHRMBAf8EBTAD AQH/MA4GA1UdDwEB/wQEAwIBBjAdBgNVHQ4EFgQUcRVnyMjJvXVdctA4GGqd83EkVAswDQYJKoZI hvcNAQELBQADggIBAHW7bVRLqhBYRjTyYtcWNl0IXtVsyIe9tC5G8jH4fOpCtZMWVdyhDBKg2mF+ D1hYc2Ryx+hFjtyp8iY/xnmMsVMIM4GwVhO+5lFc2JsKT0ucVlMC6U/2DWDqTUJV6HwbISHTGzrM d/K4kPFox/la/vot9L/J9UUbzjgQKjeKeaO04wlshYaT/4mWJ3iBj2fjRnRUjtkNaeJK9E10A/+y d+2VZ5fkscWrv2oj6NSU4kQoYsRL4vDY4ilrGnB+JGGTe08DMiUNRSQrlrRGar9KC/eaj8GsGsVn 82800vpzY4zvFrCopEYq+OsS7HK07/grfoxSwIuEVPkvPuNVqNxmsdnhX9izjFk0WaSrT2y7Hxjb davYy5LNlDhhDgcGH0tGEPEVvo2FXDtKK4F5D7Rpn0lQl033DlZdwJVqwjbDG2jJ9SrcR5q+ss7F Jej6A7na+RZukYT1HCjI/CbM1xyQVqdfbzoEvM14iQuODy+jqk+iGxI9FghAD/FGTNeqewjBCvVt J94Cj8rDtSvK6evIIVM4pcw72Hc3MKJP2W/R8kCtQXoXxdZKNYm3QdV8hn9VTYNKpXMgwDqvkPGa JI7ZjnHKe7iG2rKPmT4dEw0SEe7Uq/DpFXYC5ODfqiAeW2GFZECpkJcNrVPSWh2HagCXZWK0vm9q p/UsQu0yrbYhnr68 -----END CERTIFICATE----- Hellenic Academic and Research Institutions ECC RootCA 2015 =========================================================== -----BEGIN CERTIFICATE----- MIICwzCCAkqgAwIBAgIBADAKBggqhkjOPQQDAjCBqjELMAkGA1UEBhMCR1IxDzANBgNVBAcTBkF0 aGVuczFEMEIGA1UEChM7SGVsbGVuaWMgQWNhZGVtaWMgYW5kIFJlc2VhcmNoIEluc3RpdHV0aW9u cyBDZXJ0LiBBdXRob3JpdHkxRDBCBgNVBAMTO0hlbGxlbmljIEFjYWRlbWljIGFuZCBSZXNlYXJj aCBJbnN0aXR1dGlvbnMgRUNDIFJvb3RDQSAyMDE1MB4XDTE1MDcwNzEwMzcxMloXDTQwMDYzMDEw MzcxMlowgaoxCzAJBgNVBAYTAkdSMQ8wDQYDVQQHEwZBdGhlbnMxRDBCBgNVBAoTO0hlbGxlbmlj IEFjYWRlbWljIGFuZCBSZXNlYXJjaCBJbnN0aXR1dGlvbnMgQ2VydC4gQXV0aG9yaXR5MUQwQgYD VQQDEztIZWxsZW5pYyBBY2FkZW1pYyBhbmQgUmVzZWFyY2ggSW5zdGl0dXRpb25zIEVDQyBSb290 Q0EgMjAxNTB2MBAGByqGSM49AgEGBSuBBAAiA2IABJKgQehLgoRc4vgxEZmGZE4JJS+dQS8KrjVP dJWyUWRrjWvmP3CV8AVER6ZyOFB2lQJajq4onvktTpnvLEhvTCUp6NFxW98dwXU3tNf6e3pCnGoK Vlp8aQuqgAkkbH7BRqNCMEAwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYwHQYDVR0O BBYEFLQiC4KZJAEOnLvkDv2/+5cgk5kqMAoGCCqGSM49BAMCA2cAMGQCMGfOFmI4oqxiRaeplSTA GiecMjvAwNW6qef4BENThe5SId6d9SWDPp5YSy/XZxMOIQIwBeF1Ad5o7SofTUwJCA3sS61kFyjn dc5FZXIhF8siQQ6ME5g4mlRtm8rifOoCWCKR -----END CERTIFICATE----- ISRG Root X1 ============ -----BEGIN CERTIFICATE----- MIIFazCCA1OgAwIBAgIRAIIQz7DSQONZRGPgu2OCiwAwDQYJKoZIhvcNAQELBQAwTzELMAkGA1UE BhMCVVMxKTAnBgNVBAoTIEludGVybmV0IFNlY3VyaXR5IFJlc2VhcmNoIEdyb3VwMRUwEwYDVQQD EwxJU1JHIFJvb3QgWDEwHhcNMTUwNjA0MTEwNDM4WhcNMzUwNjA0MTEwNDM4WjBPMQswCQYDVQQG EwJVUzEpMCcGA1UEChMgSW50ZXJuZXQgU2VjdXJpdHkgUmVzZWFyY2ggR3JvdXAxFTATBgNVBAMT DElTUkcgUm9vdCBYMTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAK3oJHP0FDfzm54r Vygch77ct984kIxuPOZXoHj3dcKi/vVqbvYATyjb3miGbESTtrFj/RQSa78f0uoxmyF+0TM8ukj1 3Xnfs7j/EvEhmkvBioZxaUpmZmyPfjxwv60pIgbz5MDmgK7iS4+3mX6UA5/TR5d8mUgjU+g4rk8K b4Mu0UlXjIB0ttov0DiNewNwIRt18jA8+o+u3dpjq+sWT8KOEUt+zwvo/7V3LvSye0rgTBIlDHCN Aymg4VMk7BPZ7hm/ELNKjD+Jo2FR3qyHB5T0Y3HsLuJvW5iB4YlcNHlsdu87kGJ55tukmi8mxdAQ 4Q7e2RCOFvu396j3x+UCB5iPNgiV5+I3lg02dZ77DnKxHZu8A/lJBdiB3QW0KtZB6awBdpUKD9jf 1b0SHzUvKBds0pjBqAlkd25HN7rOrFleaJ1/ctaJxQZBKT5ZPt0m9STJEadao0xAH0ahmbWnOlFu hjuefXKnEgV4We0+UXgVCwOPjdAvBbI+e0ocS3MFEvzG6uBQE3xDk3SzynTnjh8BCNAw1FtxNrQH usEwMFxIt4I7mKZ9YIqioymCzLq9gwQbooMDQaHWBfEbwrbwqHyGO0aoSCqI3Haadr8faqU9GY/r OPNk3sgrDQoo//fb4hVC1CLQJ13hef4Y53CIrU7m2Ys6xt0nUW7/vGT1M0NPAgMBAAGjQjBAMA4G A1UdDwEB/wQEAwIBBjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBR5tFnme7bl5AFzgAiIyBpY 9umbbjANBgkqhkiG9w0BAQsFAAOCAgEAVR9YqbyyqFDQDLHYGmkgJykIrGF1XIpu+ILlaS/V9lZL ubhzEFnTIZd+50xx+7LSYK05qAvqFyFWhfFQDlnrzuBZ6brJFe+GnY+EgPbk6ZGQ3BebYhtF8GaV 0nxvwuo77x/Py9auJ/GpsMiu/X1+mvoiBOv/2X/qkSsisRcOj/KKNFtY2PwByVS5uCbMiogziUwt hDyC3+6WVwW6LLv3xLfHTjuCvjHIInNzktHCgKQ5ORAzI4JMPJ+GslWYHb4phowim57iaztXOoJw TdwJx4nLCgdNbOhdjsnvzqvHu7UrTkXWStAmzOVyyghqpZXjFaH3pO3JLF+l+/+sKAIuvtd7u+Nx e5AW0wdeRlN8NwdCjNPElpzVmbUq4JUagEiuTDkHzsxHpFKVK7q4+63SM1N95R1NbdWhscdCb+ZA JzVcoyi3B43njTOQ5yOf+1CceWxG1bQVs5ZufpsMljq4Ui0/1lvh+wjChP4kqKOJ2qxq4RgqsahD YVvTH9w7jXbyLeiNdd8XM2w9U/t7y0Ff/9yi0GE44Za4rF2LN9d11TPAmRGunUHBcnWEvgJBQl9n JEiU0Zsnvgc/ubhPgXRR4Xq37Z0j4r7g1SgEEzwxA57demyPxgcYxn/eR44/KJ4EBs+lVDR3veyJ m+kXQ99b21/+jh5Xos1AnX5iItreGCc= -----END CERTIFICATE----- AC RAIZ FNMT-RCM ================ -----BEGIN CERTIFICATE----- MIIFgzCCA2ugAwIBAgIPXZONMGc2yAYdGsdUhGkHMA0GCSqGSIb3DQEBCwUAMDsxCzAJBgNVBAYT AkVTMREwDwYDVQQKDAhGTk1ULVJDTTEZMBcGA1UECwwQQUMgUkFJWiBGTk1ULVJDTTAeFw0wODEw MjkxNTU5NTZaFw0zMDAxMDEwMDAwMDBaMDsxCzAJBgNVBAYTAkVTMREwDwYDVQQKDAhGTk1ULVJD TTEZMBcGA1UECwwQQUMgUkFJWiBGTk1ULVJDTTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoC ggIBALpxgHpMhm5/yBNtwMZ9HACXjywMI7sQmkCpGreHiPibVmr75nuOi5KOpyVdWRHbNi63URcf qQgfBBckWKo3Shjf5TnUV/3XwSyRAZHiItQDwFj8d0fsjz50Q7qsNI1NOHZnjrDIbzAzWHFctPVr btQBULgTfmxKo0nRIBnuvMApGGWn3v7v3QqQIecaZ5JCEJhfTzC8PhxFtBDXaEAUwED653cXeuYL j2VbPNmaUtu1vZ5Gzz3rkQUCwJaydkxNEJY7kvqcfw+Z374jNUUeAlz+taibmSXaXvMiwzn15Cou 08YfxGyqxRxqAQVKL9LFwag0Jl1mpdICIfkYtwb1TplvqKtMUejPUBjFd8g5CSxJkjKZqLsXF3mw WsXmo8RZZUc1g16p6DULmbvkzSDGm0oGObVo/CK67lWMK07q87Hj/LaZmtVC+nFNCM+HHmpxffnT tOmlcYF7wk5HlqX2doWjKI/pgG6BU6VtX7hI+cL5NqYuSf+4lsKMB7ObiFj86xsc3i1w4peSMKGJ 47xVqCfWS+2QrYv6YyVZLag13cqXM7zlzced0ezvXg5KkAYmY6252TUtB7p2ZSysV4999AeU14EC ll2jB0nVetBX+RvnU0Z1qrB5QstocQjpYL05ac70r8NWQMetUqIJ5G+GR4of6ygnXYMgrwTJbFaa i0b1AgMBAAGjgYMwgYAwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYwHQYDVR0OBBYE FPd9xf3E6Jobd2Sn9R2gzL+HYJptMD4GA1UdIAQ3MDUwMwYEVR0gADArMCkGCCsGAQUFBwIBFh1o dHRwOi8vd3d3LmNlcnQuZm5tdC5lcy9kcGNzLzANBgkqhkiG9w0BAQsFAAOCAgEAB5BK3/MjTvDD nFFlm5wioooMhfNzKWtN/gHiqQxjAb8EZ6WdmF/9ARP67Jpi6Yb+tmLSbkyU+8B1RXxlDPiyN8+s D8+Nb/kZ94/sHvJwnvDKuO+3/3Y3dlv2bojzr2IyIpMNOmqOFGYMLVN0V2Ue1bLdI4E7pWYjJ2cJ j+F3qkPNZVEI7VFY/uY5+ctHhKQV8Xa7pO6kO8Rf77IzlhEYt8llvhjho6Tc+hj507wTmzl6NLrT Qfv6MooqtyuGC2mDOL7Nii4LcK2NJpLuHvUBKwrZ1pebbuCoGRw6IYsMHkCtA+fdZn71uSANA+iW +YJF1DngoABd15jmfZ5nc8OaKveri6E6FO80vFIOiZiaBECEHX5FaZNXzuvO+FB8TxxuBEOb+dY7 Ixjp6o7RTUaN8Tvkasq6+yO3m/qZASlaWFot4/nUbQ4mrcFuNLwy+AwF+mWj2zs3gyLp1txyM/1d 8iC9djwj2ij3+RvrWWTV3F9yfiD8zYm1kGdNYno/Tq0dwzn+evQoFt9B9kiABdcPUXmsEKvU7ANm 5mqwujGSQkBqvjrTcuFqN1W8rB2Vt2lh8kORdOag0wokRqEIr9baRRmW1FMdW4R58MD3R++Lj8UG rp1MYp3/RgT408m2ECVAdf4WqslKYIYvuu8wd+RU4riEmViAqhOLUTpPSPaLtrM= -----END CERTIFICATE----- Amazon Root CA 1 ================ -----BEGIN CERTIFICATE----- MIIDQTCCAimgAwIBAgITBmyfz5m/jAo54vB4ikPmljZbyjANBgkqhkiG9w0BAQsFADA5MQswCQYD VQQGEwJVUzEPMA0GA1UEChMGQW1hem9uMRkwFwYDVQQDExBBbWF6b24gUm9vdCBDQSAxMB4XDTE1 MDUyNjAwMDAwMFoXDTM4MDExNzAwMDAwMFowOTELMAkGA1UEBhMCVVMxDzANBgNVBAoTBkFtYXpv bjEZMBcGA1UEAxMQQW1hem9uIFJvb3QgQ0EgMTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoC ggEBALJ4gHHKeNXjca9HgFB0fW7Y14h29Jlo91ghYPl0hAEvrAIthtOgQ3pOsqTQNroBvo3bSMgH FzZM9O6II8c+6zf1tRn4SWiw3te5djgdYZ6k/oI2peVKVuRF4fn9tBb6dNqcmzU5L/qwIFAGbHrQ gLKm+a/sRxmPUDgH3KKHOVj4utWp+UhnMJbulHheb4mjUcAwhmahRWa6VOujw5H5SNz/0egwLX0t dHA114gk957EWW67c4cX8jJGKLhD+rcdqsq08p8kDi1L93FcXmn/6pUCyziKrlA4b9v7LWIbxcce VOF34GfID5yHI9Y/QCB/IIDEgEw+OyQmjgSubJrIqg0CAwEAAaNCMEAwDwYDVR0TAQH/BAUwAwEB /zAOBgNVHQ8BAf8EBAMCAYYwHQYDVR0OBBYEFIQYzIU07LwMlJQuCFmcx7IQTgoIMA0GCSqGSIb3 DQEBCwUAA4IBAQCY8jdaQZChGsV2USggNiMOruYou6r4lK5IpDB/G/wkjUu0yKGX9rbxenDIU5PM CCjjmCXPI6T53iHTfIUJrU6adTrCC2qJeHZERxhlbI1Bjjt/msv0tadQ1wUsN+gDS63pYaACbvXy 8MWy7Vu33PqUXHeeE6V/Uq2V8viTO96LXFvKWlJbYK8U90vvo/ufQJVtMVT8QtPHRh8jrdkPSHCa 2XV4cdFyQzR1bldZwgJcJmApzyMZFo6IQ6XU5MsI+yMRQ+hDKXJioaldXgjUkK642M4UwtBV8ob2 xJNDd2ZhwLnoQdeXeGADbkpyrqXRfboQnoZsG4q5WTP468SQvvG5 -----END CERTIFICATE----- Amazon Root CA 2 ================ -----BEGIN CERTIFICATE----- MIIFQTCCAymgAwIBAgITBmyf0pY1hp8KD+WGePhbJruKNzANBgkqhkiG9w0BAQwFADA5MQswCQYD VQQGEwJVUzEPMA0GA1UEChMGQW1hem9uMRkwFwYDVQQDExBBbWF6b24gUm9vdCBDQSAyMB4XDTE1 MDUyNjAwMDAwMFoXDTQwMDUyNjAwMDAwMFowOTELMAkGA1UEBhMCVVMxDzANBgNVBAoTBkFtYXpv bjEZMBcGA1UEAxMQQW1hem9uIFJvb3QgQ0EgMjCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoC ggIBAK2Wny2cSkxKgXlRmeyKy2tgURO8TW0G/LAIjd0ZEGrHJgw12MBvIITplLGbhQPDW9tK6Mj4 kHbZW0/jTOgGNk3Mmqw9DJArktQGGWCsN0R5hYGCrVo34A3MnaZMUnbqQ523BNFQ9lXg1dKmSYXp N+nKfq5clU1Imj+uIFptiJXZNLhSGkOQsL9sBbm2eLfq0OQ6PBJTYv9K8nu+NQWpEjTj82R0Yiw9 AElaKP4yRLuH3WUnAnE72kr3H9rN9yFVkE8P7K6C4Z9r2UXTu/Bfh+08LDmG2j/e7HJV63mjrdvd fLC6HM783k81ds8P+HgfajZRRidhW+mez/CiVX18JYpvL7TFz4QuK/0NURBs+18bvBt+xa47mAEx kv8LV/SasrlX6avvDXbR8O70zoan4G7ptGmh32n2M8ZpLpcTnqWHsFcQgTfJU7O7f/aS0ZzQGPSS btqDT6ZjmUyl+17vIWR6IF9sZIUVyzfpYgwLKhbcAS4y2j5L9Z469hdAlO+ekQiG+r5jqFoz7Mt0 Q5X5bGlSNscpb/xVA1wf+5+9R+vnSUeVC06JIglJ4PVhHvG/LopyboBZ/1c6+XUyo05f7O0oYtlN c/LMgRdg7c3r3NunysV+Ar3yVAhU/bQtCSwXVEqY0VThUWcI0u1ufm8/0i2BWSlmy5A5lREedCf+ 3euvAgMBAAGjQjBAMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgGGMB0GA1UdDgQWBBSw DPBMMPQFWAJI/TPlUq9LhONmUjANBgkqhkiG9w0BAQwFAAOCAgEAqqiAjw54o+Ci1M3m9Zh6O+oA A7CXDpO8Wqj2LIxyh6mx/H9z/WNxeKWHWc8w4Q0QshNabYL1auaAn6AFC2jkR2vHat+2/XcycuUY +gn0oJMsXdKMdYV2ZZAMA3m3MSNjrXiDCYZohMr/+c8mmpJ5581LxedhpxfL86kSk5Nrp+gvU5LE YFiwzAJRGFuFjWJZY7attN6a+yb3ACfAXVU3dJnJUH/jWS5E4ywl7uxMMne0nxrpS10gxdr9HIcW xkPo1LsmmkVwXqkLN1PiRnsn/eBG8om3zEK2yygmbtmlyTrIQRNg91CMFa6ybRoVGld45pIq2WWQ gj9sAq+uEjonljYE1x2igGOpm/HlurR8FLBOybEfdF849lHqm/osohHUqS0nGkWxr7JOcQ3AWEbW aQbLU8uz/mtBzUF+fUwPfHJ5elnNXkoOrJupmHN5fLT0zLm4BwyydFy4x2+IoZCn9Kr5v2c69BoV Yh63n749sSmvZ6ES8lgQGVMDMBu4Gon2nL2XA46jCfMdiyHxtN/kHNGfZQIG6lzWE7OE76KlXIx3 KadowGuuQNKotOrN8I1LOJwZmhsoVLiJkO/KdYE+HvJkJMcYr07/R54H9jVlpNMKVv/1F2Rs76gi JUmTtt8AF9pYfl3uxRuw0dFfIRDH+fO6AgonB8Xx1sfT4PsJYGw= -----END CERTIFICATE----- Amazon Root CA 3 ================ -----BEGIN CERTIFICATE----- MIIBtjCCAVugAwIBAgITBmyf1XSXNmY/Owua2eiedgPySjAKBggqhkjOPQQDAjA5MQswCQYDVQQG EwJVUzEPMA0GA1UEChMGQW1hem9uMRkwFwYDVQQDExBBbWF6b24gUm9vdCBDQSAzMB4XDTE1MDUy NjAwMDAwMFoXDTQwMDUyNjAwMDAwMFowOTELMAkGA1UEBhMCVVMxDzANBgNVBAoTBkFtYXpvbjEZ MBcGA1UEAxMQQW1hem9uIFJvb3QgQ0EgMzBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABCmXp8ZB f8ANm+gBG1bG8lKlui2yEujSLtf6ycXYqm0fc4E7O5hrOXwzpcVOho6AF2hiRVd9RFgdszflZwjr Zt6jQjBAMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgGGMB0GA1UdDgQWBBSrttvXBp43 rDCGB5Fwx5zEGbF4wDAKBggqhkjOPQQDAgNJADBGAiEA4IWSoxe3jfkrBqWTrBqYaGFy+uGh0Psc eGCmQ5nFuMQCIQCcAu/xlJyzlvnrxir4tiz+OpAUFteMYyRIHN8wfdVoOw== -----END CERTIFICATE----- Amazon Root CA 4 ================ -----BEGIN CERTIFICATE----- MIIB8jCCAXigAwIBAgITBmyf18G7EEwpQ+Vxe3ssyBrBDjAKBggqhkjOPQQDAzA5MQswCQYDVQQG EwJVUzEPMA0GA1UEChMGQW1hem9uMRkwFwYDVQQDExBBbWF6b24gUm9vdCBDQSA0MB4XDTE1MDUy NjAwMDAwMFoXDTQwMDUyNjAwMDAwMFowOTELMAkGA1UEBhMCVVMxDzANBgNVBAoTBkFtYXpvbjEZ MBcGA1UEAxMQQW1hem9uIFJvb3QgQ0EgNDB2MBAGByqGSM49AgEGBSuBBAAiA2IABNKrijdPo1MN /sGKe0uoe0ZLY7Bi9i0b2whxIdIA6GO9mif78DluXeo9pcmBqqNbIJhFXRbb/egQbeOc4OO9X4Ri 83BkM6DLJC9wuoihKqB1+IGuYgbEgds5bimwHvouXKNCMEAwDwYDVR0TAQH/BAUwAwEB/zAOBgNV HQ8BAf8EBAMCAYYwHQYDVR0OBBYEFNPsxzplbszh2naaVvuc84ZtV+WBMAoGCCqGSM49BAMDA2gA MGUCMDqLIfG9fhGt0O9Yli/W651+kI0rz2ZVwyzjKKlwCkcO8DdZEv8tmZQoTipPNU0zWgIxAOp1 AE47xDqUEpHJWEadIRNyp4iciuRMStuW1KyLa2tJElMzrdfkviT8tQp21KW8EA== -----END CERTIFICATE----- TUBITAK Kamu SM SSL Kok Sertifikasi - Surum 1 ============================================= -----BEGIN CERTIFICATE----- MIIEYzCCA0ugAwIBAgIBATANBgkqhkiG9w0BAQsFADCB0jELMAkGA1UEBhMCVFIxGDAWBgNVBAcT D0dlYnplIC0gS29jYWVsaTFCMEAGA1UEChM5VHVya2l5ZSBCaWxpbXNlbCB2ZSBUZWtub2xvamlr IEFyYXN0aXJtYSBLdXJ1bXUgLSBUVUJJVEFLMS0wKwYDVQQLEyRLYW11IFNlcnRpZmlrYXN5b24g TWVya2V6aSAtIEthbXUgU00xNjA0BgNVBAMTLVRVQklUQUsgS2FtdSBTTSBTU0wgS29rIFNlcnRp ZmlrYXNpIC0gU3VydW0gMTAeFw0xMzExMjUwODI1NTVaFw00MzEwMjUwODI1NTVaMIHSMQswCQYD VQQGEwJUUjEYMBYGA1UEBxMPR2ViemUgLSBLb2NhZWxpMUIwQAYDVQQKEzlUdXJraXllIEJpbGlt c2VsIHZlIFRla25vbG9qaWsgQXJhc3Rpcm1hIEt1cnVtdSAtIFRVQklUQUsxLTArBgNVBAsTJEth bXUgU2VydGlmaWthc3lvbiBNZXJrZXppIC0gS2FtdSBTTTE2MDQGA1UEAxMtVFVCSVRBSyBLYW11 IFNNIFNTTCBLb2sgU2VydGlmaWthc2kgLSBTdXJ1bSAxMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A MIIBCgKCAQEAr3UwM6q7a9OZLBI3hNmNe5eA027n/5tQlT6QlVZC1xl8JoSNkvoBHToP4mQ4t4y8 6Ij5iySrLqP1N+RAjhgleYN1Hzv/bKjFxlb4tO2KRKOrbEz8HdDc72i9z+SqzvBV96I01INrN3wc wv61A+xXzry0tcXtAA9TNypN9E8Mg/uGz8v+jE69h/mniyFXnHrfA2eJLJ2XYacQuFWQfw4tJzh0 3+f92k4S400VIgLI4OD8D62K18lUUMw7D8oWgITQUVbDjlZ/iSIzL+aFCr2lqBs23tPcLG07xxO9 WSMs5uWk99gL7eqQQESolbuT1dCANLZGeA4fAJNG4e7p+exPFwIDAQABo0IwQDAdBgNVHQ4EFgQU ZT/HiobGPN08VFw1+DrtUgxHV8gwDgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8wDQYJ KoZIhvcNAQELBQADggEBACo/4fEyjq7hmFxLXs9rHmoJ0iKpEsdeV31zVmSAhHqT5Am5EM2fKifh AHe+SMg1qIGf5LgsyX8OsNJLN13qudULXjS99HMpw+0mFZx+CFOKWI3QSyjfwbPfIPP54+M638yc lNhOT8NrF7f3cuitZjO1JVOr4PhMqZ398g26rrnZqsZr+ZO7rqu4lzwDGrpDxpa5RXI4s6ehlj2R e37AIVNMh+3yC1SVUZPVIqUNivGTDj5UDrDYyU7c8jEyVupk+eq1nRZmQnLzf9OxMUP8pI4X8W0j q5Rm+K37DwhuJi1/FwcJsoz7UMCflo3Ptv0AnVoUmr8CRPXBwp8iXqIPoeM= -----END CERTIFICATE----- GDCA TrustAUTH R5 ROOT ====================== -----BEGIN CERTIFICATE----- MIIFiDCCA3CgAwIBAgIIfQmX/vBH6nowDQYJKoZIhvcNAQELBQAwYjELMAkGA1UEBhMCQ04xMjAw BgNVBAoMKUdVQU5HIERPTkcgQ0VSVElGSUNBVEUgQVVUSE9SSVRZIENPLixMVEQuMR8wHQYDVQQD DBZHRENBIFRydXN0QVVUSCBSNSBST09UMB4XDTE0MTEyNjA1MTMxNVoXDTQwMTIzMTE1NTk1OVow YjELMAkGA1UEBhMCQ04xMjAwBgNVBAoMKUdVQU5HIERPTkcgQ0VSVElGSUNBVEUgQVVUSE9SSVRZ IENPLixMVEQuMR8wHQYDVQQDDBZHRENBIFRydXN0QVVUSCBSNSBST09UMIICIjANBgkqhkiG9w0B AQEFAAOCAg8AMIICCgKCAgEA2aMW8Mh0dHeb7zMNOwZ+Vfy1YI92hhJCfVZmPoiC7XJjDp6L3TQs AlFRwxn9WVSEyfFrs0yw6ehGXTjGoqcuEVe6ghWinI9tsJlKCvLriXBjTnnEt1u9ol2x8kECK62p OqPseQrsXzrj/e+APK00mxqriCZ7VqKChh/rNYmDf1+uKU49tm7srsHwJ5uu4/Ts765/94Y9cnrr pftZTqfrlYwiOXnhLQiPzLyRuEH3FMEjqcOtmkVEs7LXLM3GKeJQEK5cy4KOFxg2fZfmiJqwTTQJ 9Cy5WmYqsBebnh52nUpmMUHfP/vFBu8btn4aRjb3ZGM74zkYI+dndRTVdVeSN72+ahsmUPI2JgaQ xXABZG12ZuGR224HwGGALrIuL4xwp9E7PLOR5G62xDtw8mySlwnNR30YwPO7ng/Wi64HtloPzgsM R6flPri9fcebNaBhlzpBdRfMK5Z3KpIhHtmVdiBnaM8Nvd/WHwlqmuLMc3GkL30SgLdTMEZeS1SZ D2fJpcjyIMGC7J0R38IC+xo70e0gmu9lZJIQDSri3nDxGGeCjGHeuLzRL5z7D9Ar7Rt2ueQ5Vfj4 oR24qoAATILnsn8JuLwwoC8N9VKejveSswoAHQBUlwbgsQfZxw9cZX08bVlX5O2ljelAU58VS6Bx 9hoh49pwBiFYFIeFd3mqgnkCAwEAAaNCMEAwHQYDVR0OBBYEFOLJQJ9NzuiaoXzPDj9lxSmIahlR MA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgGGMA0GCSqGSIb3DQEBCwUAA4ICAQDRSVfg p8xoWLoBDysZzY2wYUWsEe1jUGn4H3++Fo/9nesLqjJHdtJnJO29fDMylyrHBYZmDRd9FBUb1Ov9 H5r2XpdptxolpAqzkT9fNqyL7FeoPueBihhXOYV0GkLH6VsTX4/5COmSdI31R9KrO9b7eGZONn35 6ZLpBN79SWP8bfsUcZNnL0dKt7n/HipzcEYwv1ryL3ml4Y0M2fmyYzeMN2WFcGpcWwlyua1jPLHd +PwyvzeG5LuOmCd+uh8W4XAR8gPfJWIyJyYYMoSf/wA6E7qaTfRPuBRwIrHKK5DOKcFw9C+df/KQ HtZa37dG/OaG+svgIHZ6uqbL9XzeYqWxi+7egmaKTjowHz+Ay60nugxe19CxVsp3cbK1daFQqUBD F8Io2c9Si1vIY9RCPqAzekYu9wogRlR+ak8x8YF+QnQ4ZXMn7sZ8uI7XpTrXmKGcjBBV09tL7ECQ 8s1uV9JiDnxXk7Gnbc2dg7sq5+W2O3FYrf3RRbxake5TFW/TRQl1brqQXR4EzzffHqhmsYzmIGrv /EhOdJhCrylvLmrH+33RZjEizIYAfmaDDEL0vTSSwxrqT8p+ck0LcIymSLumoRT2+1hEmRSuqguT aaApJUqlyyvdimYHFngVV3Eb7PVHhPOeMTd61X8kreS8/f3MboPoDKi3QWwH3b08hpcv0g== -----END CERTIFICATE----- TrustCor RootCert CA-1 ====================== -----BEGIN CERTIFICATE----- MIIEMDCCAxigAwIBAgIJANqb7HHzA7AZMA0GCSqGSIb3DQEBCwUAMIGkMQswCQYDVQQGEwJQQTEP MA0GA1UECAwGUGFuYW1hMRQwEgYDVQQHDAtQYW5hbWEgQ2l0eTEkMCIGA1UECgwbVHJ1c3RDb3Ig U3lzdGVtcyBTLiBkZSBSLkwuMScwJQYDVQQLDB5UcnVzdENvciBDZXJ0aWZpY2F0ZSBBdXRob3Jp dHkxHzAdBgNVBAMMFlRydXN0Q29yIFJvb3RDZXJ0IENBLTEwHhcNMTYwMjA0MTIzMjE2WhcNMjkx MjMxMTcyMzE2WjCBpDELMAkGA1UEBhMCUEExDzANBgNVBAgMBlBhbmFtYTEUMBIGA1UEBwwLUGFu YW1hIENpdHkxJDAiBgNVBAoMG1RydXN0Q29yIFN5c3RlbXMgUy4gZGUgUi5MLjEnMCUGA1UECwwe VHJ1c3RDb3IgQ2VydGlmaWNhdGUgQXV0aG9yaXR5MR8wHQYDVQQDDBZUcnVzdENvciBSb290Q2Vy dCBDQS0xMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAv463leLCJhJrMxnHQFgKq1mq jQCj/IDHUHuO1CAmujIS2CNUSSUQIpidRtLByZ5OGy4sDjjzGiVoHKZaBeYei0i/mJZ0PmnK6bV4 pQa81QBeCQryJ3pS/C3Vseq0iWEk8xoT26nPUu0MJLq5nux+AHT6k61sKZKuUbS701e/s/OojZz0 JEsq1pme9J7+wH5COucLlVPat2gOkEz7cD+PSiyU8ybdY2mplNgQTsVHCJCZGxdNuWxu72CVEY4h gLW9oHPY0LJ3xEXqWib7ZnZ2+AYfYW0PVcWDtxBWcgYHpfOxGgMFZA6dWorWhnAbJN7+KIor0Gqw /Hqi3LJ5DotlDwIDAQABo2MwYTAdBgNVHQ4EFgQU7mtJPHo/DeOxCbeKyKsZn3MzUOcwHwYDVR0j BBgwFoAU7mtJPHo/DeOxCbeKyKsZn3MzUOcwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMC AYYwDQYJKoZIhvcNAQELBQADggEBACUY1JGPE+6PHh0RU9otRCkZoB5rMZ5NDp6tPVxBb5UrJKF5 mDo4Nvu7Zp5I/5CQ7z3UuJu0h3U/IJvOcs+hVcFNZKIZBqEHMwwLKeXx6quj7LUKdJDHfXLy11yf ke+Ri7fc7Waiz45mO7yfOgLgJ90WmMCV1Aqk5IGadZQ1nJBfiDcGrVmVCrDRZ9MZyonnMlo2HD6C qFqTvsbQZJG2z9m2GM/bftJlo6bEjhcxwft+dtvTheNYsnd6djtsL1Ac59v2Z3kf9YKVmgenFK+P 3CghZwnS1k1aHBkcjndcw5QkPTJrS37UeJSDvjdNzl/HHk484IkzlQsPpTLWPFp5LBk= -----END CERTIFICATE----- TrustCor RootCert CA-2 ====================== -----BEGIN CERTIFICATE----- MIIGLzCCBBegAwIBAgIIJaHfyjPLWQIwDQYJKoZIhvcNAQELBQAwgaQxCzAJBgNVBAYTAlBBMQ8w DQYDVQQIDAZQYW5hbWExFDASBgNVBAcMC1BhbmFtYSBDaXR5MSQwIgYDVQQKDBtUcnVzdENvciBT eXN0ZW1zIFMuIGRlIFIuTC4xJzAlBgNVBAsMHlRydXN0Q29yIENlcnRpZmljYXRlIEF1dGhvcml0 eTEfMB0GA1UEAwwWVHJ1c3RDb3IgUm9vdENlcnQgQ0EtMjAeFw0xNjAyMDQxMjMyMjNaFw0zNDEy MzExNzI2MzlaMIGkMQswCQYDVQQGEwJQQTEPMA0GA1UECAwGUGFuYW1hMRQwEgYDVQQHDAtQYW5h bWEgQ2l0eTEkMCIGA1UECgwbVHJ1c3RDb3IgU3lzdGVtcyBTLiBkZSBSLkwuMScwJQYDVQQLDB5U cnVzdENvciBDZXJ0aWZpY2F0ZSBBdXRob3JpdHkxHzAdBgNVBAMMFlRydXN0Q29yIFJvb3RDZXJ0 IENBLTIwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCnIG7CKqJiJJWQdsg4foDSq8Gb ZQWU9MEKENUCrO2fk8eHyLAnK0IMPQo+QVqedd2NyuCb7GgypGmSaIwLgQ5WoD4a3SwlFIIvl9Nk RvRUqdw6VC0xK5mC8tkq1+9xALgxpL56JAfDQiDyitSSBBtlVkxs1Pu2YVpHI7TYabS3OtB0PAx1 oYxOdqHp2yqlO/rOsP9+aij9JxzIsekp8VduZLTQwRVtDr4uDkbIXvRR/u8OYzo7cbrPb1nKDOOb XUm4TOJXsZiKQlecdu/vvdFoqNL0Cbt3Nb4lggjEFixEIFapRBF37120Hapeaz6LMvYHL1cEksr1 /p3C6eizjkxLAjHZ5DxIgif3GIJ2SDpxsROhOdUuxTTCHWKF3wP+TfSvPd9cW436cOGlfifHhi5q jxLGhF5DUVCcGZt45vz27Ud+ez1m7xMTiF88oWP7+ayHNZ/zgp6kPwqcMWmLmaSISo5uZk3vFsQP eSghYA2FFn3XVDjxklb9tTNMg9zXEJ9L/cb4Qr26fHMC4P99zVvh1Kxhe1fVSntb1IVYJ12/+Ctg rKAmrhQhJ8Z3mjOAPF5GP/fDsaOGM8boXg25NSyqRsGFAnWAoOsk+xWq5Gd/bnc/9ASKL3x74xdh 8N0JqSDIvgmk0H5Ew7IwSjiqqewYmgeCK9u4nBit2uBGF6zPXQIDAQABo2MwYTAdBgNVHQ4EFgQU 2f4hQG6UnrybPZx9mCAZ5YwwYrIwHwYDVR0jBBgwFoAU2f4hQG6UnrybPZx9mCAZ5YwwYrIwDwYD VR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAYYwDQYJKoZIhvcNAQELBQADggIBAJ5Fngw7tu/h Osh80QA9z+LqBrWyOrsGS2h60COXdKcs8AjYeVrXWoSK2BKaG9l9XE1wxaX5q+WjiYndAfrs3fnp kpfbsEZC89NiqpX+MWcUaViQCqoL7jcjx1BRtPV+nuN79+TMQjItSQzL/0kMmx40/W5ulop5A7Zv 2wnL/V9lFDfhOPXzYRZY5LVtDQsEGz9QLX+zx3oaFoBg+Iof6Rsqxvm6ARppv9JYx1RXCI/hOWB3 S6xZhBqI8d3LT3jX5+EzLfzuQfogsL7L9ziUwOHQhQ+77Sxzq+3+knYaZH9bDTMJBzN7Bj8RpFxw PIXAz+OQqIN3+tvmxYxoZxBnpVIt8MSZj3+/0WvitUfW2dCFmU2Umw9Lje4AWkcdEQOsQRivh7dv DDqPys/cA8GiCcjl/YBeyGBCARsaU1q7N6a3vLqE6R5sGtRk2tRD/pOLS/IseRYQ1JMLiI+h2IYU RpFHmygk71dSTlxCnKr3Sewn6EAes6aJInKc9Q0ztFijMDvd1GpUk74aTfOTlPf8hAs/hCBcNANE xdqtvArBAs8e5ZTZ845b2EzwnexhF7sUMlQMAimTHpKG9n/v55IFDlndmQguLvqcAFLTxWYp5KeX RKQOKIETNcX2b2TmQcTVL8w0RSXPQQCWPUouwpaYT05KnJe32x+SMsj/D1Fu1uwJ -----END CERTIFICATE----- TrustCor ECA-1 ============== -----BEGIN CERTIFICATE----- MIIEIDCCAwigAwIBAgIJAISCLF8cYtBAMA0GCSqGSIb3DQEBCwUAMIGcMQswCQYDVQQGEwJQQTEP MA0GA1UECAwGUGFuYW1hMRQwEgYDVQQHDAtQYW5hbWEgQ2l0eTEkMCIGA1UECgwbVHJ1c3RDb3Ig U3lzdGVtcyBTLiBkZSBSLkwuMScwJQYDVQQLDB5UcnVzdENvciBDZXJ0aWZpY2F0ZSBBdXRob3Jp dHkxFzAVBgNVBAMMDlRydXN0Q29yIEVDQS0xMB4XDTE2MDIwNDEyMzIzM1oXDTI5MTIzMTE3Mjgw N1owgZwxCzAJBgNVBAYTAlBBMQ8wDQYDVQQIDAZQYW5hbWExFDASBgNVBAcMC1BhbmFtYSBDaXR5 MSQwIgYDVQQKDBtUcnVzdENvciBTeXN0ZW1zIFMuIGRlIFIuTC4xJzAlBgNVBAsMHlRydXN0Q29y IENlcnRpZmljYXRlIEF1dGhvcml0eTEXMBUGA1UEAwwOVHJ1c3RDb3IgRUNBLTEwggEiMA0GCSqG SIb3DQEBAQUAA4IBDwAwggEKAoIBAQDPj+ARtZ+odnbb3w9U73NjKYKtR8aja+3+XzP4Q1HpGjOR MRegdMTUpwHmspI+ap3tDvl0mEDTPwOABoJA6LHip1GnHYMma6ve+heRK9jGrB6xnhkB1Zem6g23 xFUfJ3zSCNV2HykVh0A53ThFEXXQmqc04L/NyFIduUd+Dbi7xgz2c1cWWn5DkR9VOsZtRASqnKmc p0yJF4OuowReUoCLHhIlERnXDH19MURB6tuvsBzvgdAsxZohmz3tQjtQJvLsznFhBmIhVE5/wZ0+ fyCMgMsq2JdiyIMzkX2woloPV+g7zPIlstR8L+xNxqE6FXrntl019fZISjZFZtS6mFjBAgMBAAGj YzBhMB0GA1UdDgQWBBREnkj1zG1I1KBLf/5ZJC+Dl5mahjAfBgNVHSMEGDAWgBREnkj1zG1I1KBL f/5ZJC+Dl5mahjAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIBhjANBgkqhkiG9w0BAQsF AAOCAQEABT41XBVwm8nHc2FvcivUwo/yQ10CzsSUuZQRg2dd4mdsdXa/uwyqNsatR5Nj3B5+1t4u /ukZMjgDfxT2AHMsWbEhBuH7rBiVDKP/mZb3Kyeb1STMHd3BOuCYRLDE5D53sXOpZCz2HAF8P11F hcCF5yWPldwX8zyfGm6wyuMdKulMY/okYWLW2n62HGz1Ah3UKt1VkOsqEUc8Ll50soIipX1TH0Xs J5F95yIW6MBoNtjG8U+ARDL54dHRHareqKucBK+tIA5kmE2la8BIWJZpTdwHjFGTot+fDz2LYLSC jaoITmJF4PkL0uDgPFveXHEnJcLmA4GLEFPjx1WitJ/X5g== -----END CERTIFICATE----- SSL.com Root Certification Authority RSA ======================================== -----BEGIN CERTIFICATE----- MIIF3TCCA8WgAwIBAgIIeyyb0xaAMpkwDQYJKoZIhvcNAQELBQAwfDELMAkGA1UEBhMCVVMxDjAM BgNVBAgMBVRleGFzMRAwDgYDVQQHDAdIb3VzdG9uMRgwFgYDVQQKDA9TU0wgQ29ycG9yYXRpb24x MTAvBgNVBAMMKFNTTC5jb20gUm9vdCBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eSBSU0EwHhcNMTYw MjEyMTczOTM5WhcNNDEwMjEyMTczOTM5WjB8MQswCQYDVQQGEwJVUzEOMAwGA1UECAwFVGV4YXMx EDAOBgNVBAcMB0hvdXN0b24xGDAWBgNVBAoMD1NTTCBDb3Jwb3JhdGlvbjExMC8GA1UEAwwoU1NM LmNvbSBSb290IENlcnRpZmljYXRpb24gQXV0aG9yaXR5IFJTQTCCAiIwDQYJKoZIhvcNAQEBBQAD ggIPADCCAgoCggIBAPkP3aMrfcvQKv7sZ4Wm5y4bunfh4/WvpOz6Sl2RxFdHaxh3a3by/ZPkPQ/C Fp4LZsNWlJ4Xg4XOVu/yFv0AYvUiCVToZRdOQbngT0aXqhvIuG5iXmmxX9sqAn78bMrzQdjt0Oj8 P2FI7bADFB0QDksZ4LtO7IZl/zbzXmcCC52GVWH9ejjt/uIZALdvoVBidXQ8oPrIJZK0bnoix/ge oeOy3ZExqysdBP+lSgQ36YWkMyv94tZVNHwZpEpox7Ko07fKoZOI68GXvIz5HdkihCR0xwQ9aqkp k8zruFvh/l8lqjRYyMEjVJ0bmBHDOJx+PYZspQ9AhnwC9FwCTyjLrnGfDzrIM/4RJTXq/LrFYD3Z fBjVsqnTdXgDciLKOsMf7yzlLqn6niy2UUb9rwPW6mBo6oUWNmuF6R7As93EJNyAKoFBbZQ+yODJ gUEAnl6/f8UImKIYLEJAs/lvOCdLToD0PYFH4Ih86hzOtXVcUS4cK38acijnALXRdMbX5J+tB5O2 UzU1/Dfkw/ZdFr4hc96SCvigY2q8lpJqPvi8ZVWb3vUNiSYE/CUapiVpy8JtynziWV+XrOvvLsi8 1xtZPCvM8hnIk2snYxnP/Okm+Mpxm3+T/jRnhE6Z6/yzeAkzcLpmpnbtG3PrGqUNxCITIJRWCk4s bE6x/c+cCbqiM+2HAgMBAAGjYzBhMB0GA1UdDgQWBBTdBAkHovV6fVJTEpKV7jiAJQ2mWTAPBgNV HRMBAf8EBTADAQH/MB8GA1UdIwQYMBaAFN0ECQei9Xp9UlMSkpXuOIAlDaZZMA4GA1UdDwEB/wQE AwIBhjANBgkqhkiG9w0BAQsFAAOCAgEAIBgRlCn7Jp0cHh5wYfGVcpNxJK1ok1iOMq8bs3AD/CUr dIWQPXhq9LmLpZc7tRiRux6n+UBbkflVma8eEdBcHadm47GUBwwyOabqG7B52B2ccETjit3E+ZUf ijhDPwGFpUenPUayvOUiaPd7nNgsPgohyC0zrL/FgZkxdMF1ccW+sfAjRfSda/wZY52jvATGGAsl u1OJD7OAUN5F7kR/q5R4ZJjT9ijdh9hwZXT7DrkT66cPYakylszeu+1jTBi7qUD3oFRuIIhxdRjq erQ0cuAjJ3dctpDqhiVAq+8zD8ufgr6iIPv2tS0a5sKFsXQP+8hlAqRSAUfdSSLBv9jra6x+3uxj MxW3IwiPxg+NQVrdjsW5j+VFP3jbutIbQLH+cU0/4IGiul607BXgk90IH37hVZkLId6Tngr75qNJ vTYw/ud3sqB1l7UtgYgXZSD32pAAn8lSzDLKNXz1PQ/YK9f1JmzJBjSWFupwWRoyeXkLtoh/D1JI Pb9s2KJELtFOt3JY04kTlf5Eq/jXixtunLwsoFvVagCvXzfh1foQC5ichucmj87w7G6KVwuA406y wKBjYZC6VWg3dGq2ktufoYYitmUnDuy2n0Jg5GfCtdpBC8TTi2EbvPofkSvXRAdeuims2cXp71NI WuuA8ShYIc2wBlX7Jz9TkHCpBB5XJ7k= -----END CERTIFICATE----- SSL.com Root Certification Authority ECC ======================================== -----BEGIN CERTIFICATE----- MIICjTCCAhSgAwIBAgIIdebfy8FoW6gwCgYIKoZIzj0EAwIwfDELMAkGA1UEBhMCVVMxDjAMBgNV BAgMBVRleGFzMRAwDgYDVQQHDAdIb3VzdG9uMRgwFgYDVQQKDA9TU0wgQ29ycG9yYXRpb24xMTAv BgNVBAMMKFNTTC5jb20gUm9vdCBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eSBFQ0MwHhcNMTYwMjEy MTgxNDAzWhcNNDEwMjEyMTgxNDAzWjB8MQswCQYDVQQGEwJVUzEOMAwGA1UECAwFVGV4YXMxEDAO BgNVBAcMB0hvdXN0b24xGDAWBgNVBAoMD1NTTCBDb3Jwb3JhdGlvbjExMC8GA1UEAwwoU1NMLmNv bSBSb290IENlcnRpZmljYXRpb24gQXV0aG9yaXR5IEVDQzB2MBAGByqGSM49AgEGBSuBBAAiA2IA BEVuqVDEpiM2nl8ojRfLliJkP9x6jh3MCLOicSS6jkm5BBtHllirLZXI7Z4INcgn64mMU1jrYor+ 8FsPazFSY0E7ic3s7LaNGdM0B9y7xgZ/wkWV7Mt/qCPgCemB+vNH06NjMGEwHQYDVR0OBBYEFILR hXMw5zUE044CkvvlpNHEIejNMA8GA1UdEwEB/wQFMAMBAf8wHwYDVR0jBBgwFoAUgtGFczDnNQTT jgKS++Wk0cQh6M0wDgYDVR0PAQH/BAQDAgGGMAoGCCqGSM49BAMCA2cAMGQCMG/n61kRpGDPYbCW e+0F+S8Tkdzt5fxQaxFGRrMcIQBiu77D5+jNB5n5DQtdcj7EqgIwH7y6C+IwJPt8bYBVCpk+gA0z 5Wajs6O7pdWLjwkspl1+4vAHCGht0nxpbl/f5Wpl -----END CERTIFICATE----- SSL.com EV Root Certification Authority RSA R2 ============================================== -----BEGIN CERTIFICATE----- MIIF6zCCA9OgAwIBAgIIVrYpzTS8ePYwDQYJKoZIhvcNAQELBQAwgYIxCzAJBgNVBAYTAlVTMQ4w DAYDVQQIDAVUZXhhczEQMA4GA1UEBwwHSG91c3RvbjEYMBYGA1UECgwPU1NMIENvcnBvcmF0aW9u MTcwNQYDVQQDDC5TU0wuY29tIEVWIFJvb3QgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkgUlNBIFIy MB4XDTE3MDUzMTE4MTQzN1oXDTQyMDUzMDE4MTQzN1owgYIxCzAJBgNVBAYTAlVTMQ4wDAYDVQQI DAVUZXhhczEQMA4GA1UEBwwHSG91c3RvbjEYMBYGA1UECgwPU1NMIENvcnBvcmF0aW9uMTcwNQYD VQQDDC5TU0wuY29tIEVWIFJvb3QgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkgUlNBIFIyMIICIjAN BgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAjzZlQOHWTcDXtOlG2mvqM0fNTPl9fb69LT3w23jh hqXZuglXaO1XPqDQCEGD5yhBJB/jchXQARr7XnAjssufOePPxU7Gkm0mxnu7s9onnQqG6YE3Bf7w cXHswxzpY6IXFJ3vG2fThVUCAtZJycxa4bH3bzKfydQ7iEGonL3Lq9ttewkfokxykNorCPzPPFTO Zw+oz12WGQvE43LrrdF9HSfvkusQv1vrO6/PgN3B0pYEW3p+pKk8OHakYo6gOV7qd89dAFmPZiw+ B6KjBSYRaZfqhbcPlgtLyEDhULouisv3D5oi53+aNxPN8k0TayHRwMwi8qFG9kRpnMphNQcAb9Zh CBHqurj26bNg5U257J8UZslXWNvNh2n4ioYSA0e/ZhN2rHd9NCSFg83XqpyQGp8hLH94t2S42Oim 9HizVcuE0jLEeK6jj2HdzghTreyI/BXkmg3mnxp3zkyPuBQVPWKchjgGAGYS5Fl2WlPAApiiECto RHuOec4zSnaqW4EWG7WK2NAAe15itAnWhmMOpgWVSbooi4iTsjQc2KRVbrcc0N6ZVTsj9CLg+Slm JuwgUHfbSguPvuUCYHBBXtSuUDkiFCbLsjtzdFVHB3mBOagwE0TlBIqulhMlQg+5U8Sb/M3kHN48 +qvWBkofZ6aYMBzdLNvcGJVXZsb/XItW9XcCAwEAAaNjMGEwDwYDVR0TAQH/BAUwAwEB/zAfBgNV HSMEGDAWgBT5YLvU49U09rj1BoAlp3PbRmmonjAdBgNVHQ4EFgQU+WC71OPVNPa49QaAJadz20Zp qJ4wDgYDVR0PAQH/BAQDAgGGMA0GCSqGSIb3DQEBCwUAA4ICAQBWs47LCp1Jjr+kxJG7ZhcFUZh1 ++VQLHqe8RT6q9OKPv+RKY9ji9i0qVQBDb6Thi/5Sm3HXvVX+cpVHBK+Rw82xd9qt9t1wkclf7nx Y/hoLVUE0fKNsKTPvDxeH3jnpaAgcLAExbf3cqfeIg29MyVGjGSSJuM+LmOW2puMPfgYCdcDzH2G guDKBAdRUNf/ktUM79qGn5nX67evaOI5JpS6aLe/g9Pqemc9YmeuJeVy6OLk7K4S9ksrPJ/psEDz OFSz/bdoyNrGj1E8svuR3Bznm53htw1yj+KkxKl4+esUrMZDBcJlOSgYAsOCsp0FvmXtll9ldDz7 CTUue5wT/RsPXcdtgTpWD8w74a8CLyKsRspGPKAcTNZEtF4uXBVmCeEmKf7GUmG6sXP/wwyc5Wxq lD8UykAWlYTzWamsX0xhk23RO8yilQwipmdnRC652dKKQbNmC1r7fSOl8hqw/96bg5Qu0T/fkreR rwU7ZcegbLHNYhLDkBvjJc40vG93drEQw/cFGsDWr3RiSBd3kmmQYRzelYB0VI8YHMPzA9C/pEN1 hlMYegouCRw2n5H9gooiS9EOUCXdywMMF8mDAAhONU2Ki+3wApRmLER/y5UnlhetCTCstnEXbosX 9hwJ1C07mKVx01QT2WDz9UtmT/rx7iASjbSsV7FFY6GsdqnC+w== -----END CERTIFICATE----- SSL.com EV Root Certification Authority ECC =========================================== -----BEGIN CERTIFICATE----- MIIClDCCAhqgAwIBAgIILCmcWxbtBZUwCgYIKoZIzj0EAwIwfzELMAkGA1UEBhMCVVMxDjAMBgNV BAgMBVRleGFzMRAwDgYDVQQHDAdIb3VzdG9uMRgwFgYDVQQKDA9TU0wgQ29ycG9yYXRpb24xNDAy BgNVBAMMK1NTTC5jb20gRVYgUm9vdCBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eSBFQ0MwHhcNMTYw MjEyMTgxNTIzWhcNNDEwMjEyMTgxNTIzWjB/MQswCQYDVQQGEwJVUzEOMAwGA1UECAwFVGV4YXMx EDAOBgNVBAcMB0hvdXN0b24xGDAWBgNVBAoMD1NTTCBDb3Jwb3JhdGlvbjE0MDIGA1UEAwwrU1NM LmNvbSBFViBSb290IENlcnRpZmljYXRpb24gQXV0aG9yaXR5IEVDQzB2MBAGByqGSM49AgEGBSuB BAAiA2IABKoSR5CYG/vvw0AHgyBO8TCCogbR8pKGYfL2IWjKAMTH6kMAVIbc/R/fALhBYlzccBYy 3h+Z1MzFB8gIH2EWB1E9fVwHU+M1OIzfzZ/ZLg1KthkuWnBaBu2+8KGwytAJKaNjMGEwHQYDVR0O BBYEFFvKXuXe0oGqzagtZFG22XKbl+ZPMA8GA1UdEwEB/wQFMAMBAf8wHwYDVR0jBBgwFoAUW8pe 5d7SgarNqC1kUbbZcpuX5k8wDgYDVR0PAQH/BAQDAgGGMAoGCCqGSM49BAMCA2gAMGUCMQCK5kCJ N+vp1RPZytRrJPOwPYdGWBrssd9v+1a6cGvHOMzosYxPD/fxZ3YOg9AeUY8CMD32IygmTMZgh5Mm m7I1HrrW9zzRHM76JTymGoEVW/MSD2zuZYrJh6j5B+BimoxcSg== -----END CERTIFICATE----- GlobalSign Root CA - R6 ======================= -----BEGIN CERTIFICATE----- MIIFgzCCA2ugAwIBAgIORea7A4Mzw4VlSOb/RVEwDQYJKoZIhvcNAQEMBQAwTDEgMB4GA1UECxMX R2xvYmFsU2lnbiBSb290IENBIC0gUjYxEzARBgNVBAoTCkdsb2JhbFNpZ24xEzARBgNVBAMTCkds b2JhbFNpZ24wHhcNMTQxMjEwMDAwMDAwWhcNMzQxMjEwMDAwMDAwWjBMMSAwHgYDVQQLExdHbG9i YWxTaWduIFJvb3QgQ0EgLSBSNjETMBEGA1UEChMKR2xvYmFsU2lnbjETMBEGA1UEAxMKR2xvYmFs U2lnbjCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAJUH6HPKZvnsFMp7PPcNCPG0RQss grRIxutbPK6DuEGSMxSkb3/pKszGsIhrxbaJ0cay/xTOURQh7ErdG1rG1ofuTToVBu1kZguSgMpE 3nOUTvOniX9PeGMIyBJQbUJmL025eShNUhqKGoC3GYEOfsSKvGRMIRxDaNc9PIrFsmbVkJq3MQbF vuJtMgamHvm566qjuL++gmNQ0PAYid/kD3n16qIfKtJwLnvnvJO7bVPiSHyMEAc4/2ayd2F+4OqM PKq0pPbzlUoSB239jLKJz9CgYXfIWHSw1CM69106yqLbnQneXUQtkPGBzVeS+n68UARjNN9rkxi+ azayOeSsJDa38O+2HBNXk7besvjihbdzorg1qkXy4J02oW9UivFyVm4uiMVRQkQVlO6jxTiWm05O WgtH8wY2SXcwvHE35absIQh1/OZhFj931dmRl4QKbNQCTXTAFO39OfuD8l4UoQSwC+n+7o/hbguy CLNhZglqsQY6ZZZZwPA1/cnaKI0aEYdwgQqomnUdnjqGBQCe24DWJfncBZ4nWUx2OVvq+aWh2IMP 0f/fMBH5hc8zSPXKbWQULHpYT9NLCEnFlWQaYw55PfWzjMpYrZxCRXluDocZXFSxZba/jJvcE+kN b7gu3GduyYsRtYQUigAZcIN5kZeR1BonvzceMgfYFGM8KEyvAgMBAAGjYzBhMA4GA1UdDwEB/wQE AwIBBjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBSubAWjkxPioufi1xzWx/B/yGdToDAfBgNV HSMEGDAWgBSubAWjkxPioufi1xzWx/B/yGdToDANBgkqhkiG9w0BAQwFAAOCAgEAgyXt6NH9lVLN nsAEoJFp5lzQhN7craJP6Ed41mWYqVuoPId8AorRbrcWc+ZfwFSY1XS+wc3iEZGtIxg93eFyRJa0 lV7Ae46ZeBZDE1ZXs6KzO7V33EByrKPrmzU+sQghoefEQzd5Mr6155wsTLxDKZmOMNOsIeDjHfrY BzN2VAAiKrlNIC5waNrlU/yDXNOd8v9EDERm8tLjvUYAGm0CuiVdjaExUd1URhxN25mW7xocBFym Fe944Hn+Xds+qkxV/ZoVqW/hpvvfcDDpw+5CRu3CkwWJ+n1jez/QcYF8AOiYrg54NMMl+68KnyBr 3TsTjxKM4kEaSHpzoHdpx7Zcf4LIHv5YGygrqGytXm3ABdJ7t+uA/iU3/gKbaKxCXcPu9czc8FB1 0jZpnOZ7BN9uBmm23goJSFmH63sUYHpkqmlD75HHTOwY3WzvUy2MmeFe8nI+z1TIvWfspA9MRf/T uTAjB0yPEL+GltmZWrSZVxykzLsViVO6LAUP5MSeGbEYNNVMnbrt9x+vJJUEeKgDu+6B5dpffItK oZB0JaezPkvILFa9x8jvOOJckvB595yEunQtYQEgfn7R8k8HWV+LLUNS60YMlOH1Zkd5d9VUWx+t JDfLRVpOoERIyNiwmcUVhAn21klJwGW45hpxbqCo8YLoRT5s1gLXCmeDBVrJpBA= -----END CERTIFICATE----- OISTE WISeKey Global Root GC CA =============================== -----BEGIN CERTIFICATE----- MIICaTCCAe+gAwIBAgIQISpWDK7aDKtARb8roi066jAKBggqhkjOPQQDAzBtMQswCQYDVQQGEwJD SDEQMA4GA1UEChMHV0lTZUtleTEiMCAGA1UECxMZT0lTVEUgRm91bmRhdGlvbiBFbmRvcnNlZDEo MCYGA1UEAxMfT0lTVEUgV0lTZUtleSBHbG9iYWwgUm9vdCBHQyBDQTAeFw0xNzA1MDkwOTQ4MzRa Fw00MjA1MDkwOTU4MzNaMG0xCzAJBgNVBAYTAkNIMRAwDgYDVQQKEwdXSVNlS2V5MSIwIAYDVQQL ExlPSVNURSBGb3VuZGF0aW9uIEVuZG9yc2VkMSgwJgYDVQQDEx9PSVNURSBXSVNlS2V5IEdsb2Jh bCBSb290IEdDIENBMHYwEAYHKoZIzj0CAQYFK4EEACIDYgAETOlQwMYPchi82PG6s4nieUqjFqdr VCTbUf/q9Akkwwsin8tqJ4KBDdLArzHkdIJuyiXZjHWd8dvQmqJLIX4Wp2OQ0jnUsYd4XxiWD1Ab NTcPasbc2RNNpI6QN+a9WzGRo1QwUjAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUwAwEB/zAd BgNVHQ4EFgQUSIcUrOPDnpBgOtfKie7TrYy0UGYwEAYJKwYBBAGCNxUBBAMCAQAwCgYIKoZIzj0E AwMDaAAwZQIwJsdpW9zV57LnyAyMjMPdeYwbY9XJUpROTYJKcx6ygISpJcBMWm1JKWB4E+J+SOtk AjEA2zQgMgj/mkkCtojeFK9dbJlxjRo/i9fgojaGHAeCOnZT/cKi7e97sIBPWA9LUzm9 -----END CERTIFICATE----- GTS Root R1 =========== -----BEGIN CERTIFICATE----- MIIFWjCCA0KgAwIBAgIQbkepxUtHDA3sM9CJuRz04TANBgkqhkiG9w0BAQwFADBHMQswCQYDVQQG EwJVUzEiMCAGA1UEChMZR29vZ2xlIFRydXN0IFNlcnZpY2VzIExMQzEUMBIGA1UEAxMLR1RTIFJv b3QgUjEwHhcNMTYwNjIyMDAwMDAwWhcNMzYwNjIyMDAwMDAwWjBHMQswCQYDVQQGEwJVUzEiMCAG A1UEChMZR29vZ2xlIFRydXN0IFNlcnZpY2VzIExMQzEUMBIGA1UEAxMLR1RTIFJvb3QgUjEwggIi MA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQC2EQKLHuOhd5s73L+UPreVp0A8of2C+X0yBoJx 9vaMf/vo27xqLpeXo4xL+Sv2sfnOhB2x+cWX3u+58qPpvBKJXqeqUqv4IyfLpLGcY9vXmX7wCl7r aKb0xlpHDU0QM+NOsROjyBhsS+z8CZDfnWQpJSMHobTSPS5g4M/SCYe7zUjwTcLCeoiKu7rPWRnW r4+wB7CeMfGCwcDfLqZtbBkOtdh+JhpFAz2weaSUKK0PfyblqAj+lug8aJRT7oM6iCsVlgmy4HqM LnXWnOunVmSPlk9orj2XwoSPwLxAwAtcvfaHszVsrBhQf4TgTM2S0yDpM7xSma8ytSmzJSq0SPly 4cpk9+aCEI3oncKKiPo4Zor8Y/kB+Xj9e1x3+naH+uzfsQ55lVe0vSbv1gHR6xYKu44LtcXFilWr 06zqkUspzBmkMiVOKvFlRNACzqrOSbTqn3yDsEB750Orp2yjj32JgfpMpf/VjsPOS+C12LOORc92 wO1AK/1TD7Cn1TsNsYqiA94xrcx36m97PtbfkSIS5r762DL8EGMUUXLeXdYWk70paDPvOmbsB4om 3xPXV2V4J95eSRQAogB/mqghtqmxlbCluQ0WEdrHbEg8QOB+DVrNVjzRlwW5y0vtOUucxD/SVRNu JLDWcfr0wbrM7Rv1/oFB2ACYPTrIrnqYNxgFlQIDAQABo0IwQDAOBgNVHQ8BAf8EBAMCAQYwDwYD VR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQU5K8rJnEaK0gnhS9SZizv8IkTcT4wDQYJKoZIhvcNAQEM BQADggIBADiWCu49tJYeX++dnAsznyvgyv3SjgofQXSlfKqE1OXyHuY3UjKcC9FhHb8owbZEKTV1 d5iyfNm9dKyKaOOpMQkpAWBz40d8U6iQSifvS9efk+eCNs6aaAyC58/UEBZvXw6ZXPYfcX3v73sv fuo21pdwCxXu11xWajOl40k4DLh9+42FpLFZXvRq4d2h9mREruZRgyFmxhE+885H7pwoHyXa/6xm ld01D1zvICxi/ZG6qcz8WpyTgYMpl0p8WnK0OdC3d8t5/Wk6kjftbjhlRn7pYL15iJdfOBL07q9b gsiG1eGZbYwE8na6SfZu6W0eX6DvJ4J2QPim01hcDyxC2kLGe4g0x8HYRZvBPsVhHdljUEn2NIVq 4BjFbkerQUIpm/ZgDdIx02OYI5NaAIFItO/Nis3Jz5nu2Z6qNuFoS3FJFDYoOj0dzpqPJeaAcWEr tXvM+SUWgeExX6GjfhaknBZqlxi9dnKlC54dNuYvoS++cJEPqOba+MSSQGwlfnuzCdyyF62ARPBo pY+Udf90WuioAnwMCeKpSwughQtiue+hMZL77/ZRBIls6Kl0obsXs7X9SQ98POyDGCBDTtWTurQ0 sR8WNh8M5mQ5Fkzc4P4dyKliPUDqysU0ArSuiYgzNdwsE3PYJ/HQcu51OyLemGhmW/HGY0dVHLql CFF1pkgl -----END CERTIFICATE----- GTS Root R2 =========== -----BEGIN CERTIFICATE----- MIIFWjCCA0KgAwIBAgIQbkepxlqz5yDFMJo/aFLybzANBgkqhkiG9w0BAQwFADBHMQswCQYDVQQG EwJVUzEiMCAGA1UEChMZR29vZ2xlIFRydXN0IFNlcnZpY2VzIExMQzEUMBIGA1UEAxMLR1RTIFJv b3QgUjIwHhcNMTYwNjIyMDAwMDAwWhcNMzYwNjIyMDAwMDAwWjBHMQswCQYDVQQGEwJVUzEiMCAG A1UEChMZR29vZ2xlIFRydXN0IFNlcnZpY2VzIExMQzEUMBIGA1UEAxMLR1RTIFJvb3QgUjIwggIi MA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDO3v2m++zsFDQ8BwZabFn3GTXd98GdVarTzTuk k3LvCvptnfbwhYBboUhSnznFt+4orO/LdmgUud+tAWyZH8QiHZ/+cnfgLFuv5AS/T3KgGjSY6Dlo 7JUle3ah5mm5hRm9iYz+re026nO8/4Piy33B0s5Ks40FnotJk9/BW9BuXvAuMC6C/Pq8tBcKSOWI m8Wba96wyrQD8Nr0kLhlZPdcTK3ofmZemde4wj7I0BOdre7kRXuJVfeKH2JShBKzwkCX44ofR5Gm dFrS+LFjKBC4swm4VndAoiaYecb+3yXuPuWgf9RhD1FLPD+M2uFwdNjCaKH5wQzpoeJ/u1U8dgbu ak7MkogwTZq9TwtImoS1mKPV+3PBV2HdKFZ1E66HjucMUQkQdYhMvI35ezzUIkgfKtzra7tEscsz cTJGr61K8YzodDqs5xoic4DSMPclQsciOzsSrZYuxsN2B6ogtzVJV+mSSeh2FnIxZyuWfoqjx5RW Ir9qS34BIbIjMt/kmkRtWVtd9QCgHJvGeJeNkP+byKq0rxFROV7Z+2et1VsRnTKaG73Vululycsl aVNVJ1zgyjbLiGH7HrfQy+4W+9OmTN6SpdTi3/UGVN4unUu0kzCqgc7dGtxRcw1PcOnlthYhGXmy 5okLdWTK1au8CcEYof/UVKGFPP0UJAOyh9OktwIDAQABo0IwQDAOBgNVHQ8BAf8EBAMCAQYwDwYD VR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUu//KjiOfT5nK2+JopqUVJxce2Q4wDQYJKoZIhvcNAQEM BQADggIBALZp8KZ3/p7uC4Gt4cCpx/k1HUCCq+YEtN/L9x0Pg/B+E02NjO7jMyLDOfxA325BS0JT vhaI8dI4XsRomRyYUpOM52jtG2pzegVATX9lO9ZY8c6DR2Dj/5epnGB3GFW1fgiTz9D2PGcDFWEJ +YF59exTpJ/JjwGLc8R3dtyDovUMSRqodt6Sm2T4syzFJ9MHwAiApJiS4wGWAqoC7o87xdFtCjMw c3i5T1QWvwsHoaRc5svJXISPD+AVdyx+Jn7axEvbpxZ3B7DNdehyQtaVhJ2Gg/LkkM0JR9SLA3Da WsYDQvTtN6LwG1BUSw7YhN4ZKJmBR64JGz9I0cNv4rBgF/XuIwKl2gBbbZCr7qLpGzvpx0QnRY5r n/WkhLx3+WuXrD5RRaIRpsyF7gpo8j5QOHokYh4XIDdtak23CZvJ/KRY9bb7nE4Yu5UC56Gtmwfu Nmsk0jmGwZODUNKBRqhfYlcsu2xkiAhu7xNUX90txGdj08+JN7+dIPT7eoOboB6BAFDC5AwiWVIQ 7UNWhwD4FFKnHYuTjKJNRn8nxnGbJN7k2oaLDX5rIMHAnuFl2GqjpuiFizoHCBy69Y9Vmhh1fuXs gWbRIXOhNUQLgD1bnF5vKheW0YMjiGZt5obicDIvUiLnyOd/xCxgXS/Dr55FBcOEArf9LAhST4Ld o/DUhgkC -----END CERTIFICATE----- GTS Root R3 =========== -----BEGIN CERTIFICATE----- MIICDDCCAZGgAwIBAgIQbkepx2ypcyRAiQ8DVd2NHTAKBggqhkjOPQQDAzBHMQswCQYDVQQGEwJV UzEiMCAGA1UEChMZR29vZ2xlIFRydXN0IFNlcnZpY2VzIExMQzEUMBIGA1UEAxMLR1RTIFJvb3Qg UjMwHhcNMTYwNjIyMDAwMDAwWhcNMzYwNjIyMDAwMDAwWjBHMQswCQYDVQQGEwJVUzEiMCAGA1UE ChMZR29vZ2xlIFRydXN0IFNlcnZpY2VzIExMQzEUMBIGA1UEAxMLR1RTIFJvb3QgUjMwdjAQBgcq hkjOPQIBBgUrgQQAIgNiAAQfTzOHMymKoYTey8chWEGJ6ladK0uFxh1MJ7x/JlFyb+Kf1qPKzEUU Rout736GjOyxfi//qXGdGIRFBEFVbivqJn+7kAHjSxm65FSWRQmx1WyRRK2EE46ajA2ADDL24Cej QjBAMA4GA1UdDwEB/wQEAwIBBjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBTB8Sa6oC2uhYHP 0/EqEr24Cmf9vDAKBggqhkjOPQQDAwNpADBmAjEAgFukfCPAlaUs3L6JbyO5o91lAFJekazInXJ0 glMLfalAvWhgxeG4VDvBNhcl2MG9AjEAnjWSdIUlUfUk7GRSJFClH9voy8l27OyCbvWFGFPouOOa KaqW04MjyaR7YbPMAuhd -----END CERTIFICATE----- GTS Root R4 =========== -----BEGIN CERTIFICATE----- MIICCjCCAZGgAwIBAgIQbkepyIuUtui7OyrYorLBmTAKBggqhkjOPQQDAzBHMQswCQYDVQQGEwJV UzEiMCAGA1UEChMZR29vZ2xlIFRydXN0IFNlcnZpY2VzIExMQzEUMBIGA1UEAxMLR1RTIFJvb3Qg UjQwHhcNMTYwNjIyMDAwMDAwWhcNMzYwNjIyMDAwMDAwWjBHMQswCQYDVQQGEwJVUzEiMCAGA1UE ChMZR29vZ2xlIFRydXN0IFNlcnZpY2VzIExMQzEUMBIGA1UEAxMLR1RTIFJvb3QgUjQwdjAQBgcq hkjOPQIBBgUrgQQAIgNiAATzdHOnaItgrkO4NcWBMHtLSZ37wWHO5t5GvWvVYRg1rkDdc/eJkTBa 6zzuhXyiQHY7qca4R9gq55KRanPpsXI5nymfopjTX15YhmUPoYRlBtHci8nHc8iMai/lxKvRHYqj QjBAMA4GA1UdDwEB/wQEAwIBBjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBSATNbrdP9JNqPV 2Py1PsVq8JQdjDAKBggqhkjOPQQDAwNnADBkAjBqUFJ0CMRw3J5QdCHojXohw0+WbhXRIjVhLfoI N+4Zba3bssx9BzT1YBkstTTZbyACMANxsbqjYAuG7ZoIapVon+Kz4ZNkfF6Tpt95LY2F45TPI11x zPKwTdb+mciUqXWi4w== -----END CERTIFICATE----- UCA Global G2 Root ================== -----BEGIN CERTIFICATE----- MIIFRjCCAy6gAwIBAgIQXd+x2lqj7V2+WmUgZQOQ7zANBgkqhkiG9w0BAQsFADA9MQswCQYDVQQG EwJDTjERMA8GA1UECgwIVW5pVHJ1c3QxGzAZBgNVBAMMElVDQSBHbG9iYWwgRzIgUm9vdDAeFw0x NjAzMTEwMDAwMDBaFw00MDEyMzEwMDAwMDBaMD0xCzAJBgNVBAYTAkNOMREwDwYDVQQKDAhVbmlU cnVzdDEbMBkGA1UEAwwSVUNBIEdsb2JhbCBHMiBSb290MIICIjANBgkqhkiG9w0BAQEFAAOCAg8A MIICCgKCAgEAxeYrb3zvJgUno4Ek2m/LAfmZmqkywiKHYUGRO8vDaBsGxUypK8FnFyIdK+35KYmT oni9kmugow2ifsqTs6bRjDXVdfkX9s9FxeV67HeToI8jrg4aA3++1NDtLnurRiNb/yzmVHqUwCoV 8MmNsHo7JOHXaOIxPAYzRrZUEaalLyJUKlgNAQLx+hVRZ2zA+te2G3/RVogvGjqNO7uCEeBHANBS h6v7hn4PJGtAnTRnvI3HLYZveT6OqTwXS3+wmeOwcWDcC/Vkw85DvG1xudLeJ1uK6NjGruFZfc8o LTW4lVYa8bJYS7cSN8h8s+1LgOGN+jIjtm+3SJUIsUROhYw6AlQgL9+/V087OpAh18EmNVQg7Mc/ R+zvWr9LesGtOxdQXGLYD0tK3Cv6brxzks3sx1DoQZbXqX5t2Okdj4q1uViSukqSKwxW/YDrCPBe KW4bHAyvj5OJrdu9o54hyokZ7N+1wxrrFv54NkzWbtA+FxyQF2smuvt6L78RHBgOLXMDj6DlNaBa 4kx1HXHhOThTeEDMg5PXCp6dW4+K5OXgSORIskfNTip1KnvyIvbJvgmRlld6iIis7nCs+dwp4wwc OxJORNanTrAmyPPZGpeRaOrvjUYG0lZFWJo8DA+DuAUlwznPO6Q0ibd5Ei9Hxeepl2n8pndntd97 8XplFeRhVmUCAwEAAaNCMEAwDgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0O BBYEFIHEjMz15DD/pQwIX4wVZyF0Ad/fMA0GCSqGSIb3DQEBCwUAA4ICAQATZSL1jiutROTL/7lo 5sOASD0Ee/ojL3rtNtqyzm325p7lX1iPyzcyochltq44PTUbPrw7tgTQvPlJ9Zv3hcU2tsu8+Mg5 1eRfB70VVJd0ysrtT7q6ZHafgbiERUlMjW+i67HM0cOU2kTC5uLqGOiiHycFutfl1qnN3e92mI0A Ds0b+gO3joBYDic/UvuUospeZcnWhNq5NXHzJsBPd+aBJ9J3O5oUb3n09tDh05S60FdRvScFDcH9 yBIw7m+NESsIndTUv4BFFJqIRNow6rSn4+7vW4LVPtateJLbXDzz2K36uGt/xDYotgIVilQsnLAX c47QN6MUPJiVAAwpBVueSUmxX8fjy88nZY41F7dXyDDZQVu5FLbowg+UMaeUmMxq67XhJ/UQqAHo jhJi6IjMtX9Gl8CbEGY4GjZGXyJoPd/JxhMnq1MGrKI8hgZlb7F+sSlEmqO6SWkoaY/X5V+tBIZk bxqgDMUIYs6Ao9Dz7GjevjPHF1t/gMRMTLGmhIrDO7gJzRSBuhjjVFc2/tsvfEehOjPI+Vg7RE+x ygKJBJYoaMVLuCaJu9YzL1DV/pqJuhgyklTGW+Cd+V7lDSKb9triyCGyYiGqhkCyLmTTX8jjfhFn RR8F/uOi77Oos/N9j/gMHyIfLXC0uAE0djAA5SN4p1bXUB+K+wb1whnw0A== -----END CERTIFICATE----- UCA Extended Validation Root ============================ -----BEGIN CERTIFICATE----- MIIFWjCCA0KgAwIBAgIQT9Irj/VkyDOeTzRYZiNwYDANBgkqhkiG9w0BAQsFADBHMQswCQYDVQQG EwJDTjERMA8GA1UECgwIVW5pVHJ1c3QxJTAjBgNVBAMMHFVDQSBFeHRlbmRlZCBWYWxpZGF0aW9u IFJvb3QwHhcNMTUwMzEzMDAwMDAwWhcNMzgxMjMxMDAwMDAwWjBHMQswCQYDVQQGEwJDTjERMA8G A1UECgwIVW5pVHJ1c3QxJTAjBgNVBAMMHFVDQSBFeHRlbmRlZCBWYWxpZGF0aW9uIFJvb3QwggIi MA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCpCQcoEwKwmeBkqh5DFnpzsZGgdT6o+uM4AHrs iWogD4vFsJszA1qGxliG1cGFu0/GnEBNyr7uaZa4rYEwmnySBesFK5pI0Lh2PpbIILvSsPGP2KxF Rv+qZ2C0d35qHzwaUnoEPQc8hQ2E0B92CvdqFN9y4zR8V05WAT558aopO2z6+I9tTcg1367r3CTu eUWnhbYFiN6IXSV8l2RnCdm/WhUFhvMJHuxYMjMR83dksHYf5BA1FxvyDrFspCqjc/wJHx4yGVMR 59mzLC52LqGj3n5qiAno8geK+LLNEOfic0CTuwjRP+H8C5SzJe98ptfRr5//lpr1kXuYC3fUfugH 0mK1lTnj8/FtDw5lhIpjVMWAtuCeS31HJqcBCF3RiJ7XwzJE+oJKCmhUfzhTA8ykADNkUVkLo4KR el7sFsLzKuZi2irbWWIQJUoqgQtHB0MGcIfS+pMRKXpITeuUx3BNr2fVUbGAIAEBtHoIppB/TuDv B0GHr2qlXov7z1CymlSvw4m6WC31MJixNnI5fkkE/SmnTHnkBVfblLkWU41Gsx2VYVdWf6/wFlth WG82UBEL2KwrlRYaDh8IzTY0ZRBiZtWAXxQgXy0MoHgKaNYs1+lvK9JKBZP8nm9rZ/+I8U6laUpS NwXqxhaN0sSZ0YIrO7o1dfdRUVjzyAfd5LQDfwIDAQABo0IwQDAdBgNVHQ4EFgQU2XQ65DA9DfcS 3H5aBZ8eNJr34RQwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAYYwDQYJKoZIhvcNAQEL BQADggIBADaNl8xCFWQpN5smLNb7rhVpLGsaGvdftvkHTFnq88nIua7Mui563MD1sC3AO6+fcAUR ap8lTwEpcOPlDOHqWnzcSbvBHiqB9RZLcpHIojG5qtr8nR/zXUACE/xOHAbKsxSQVBcZEhrxH9cM aVr2cXj0lH2RC47skFSOvG+hTKv8dGT9cZr4QQehzZHkPJrgmzI5c6sq1WnIeJEmMX3ixzDx/BR4 dxIOE/TdFpS/S2d7cFOFyrC78zhNLJA5wA3CXWvp4uXViI3WLL+rG761KIcSF3Ru/H38j9CHJrAb +7lsq+KePRXBOy5nAliRn+/4Qh8st2j1da3Ptfb/EX3C8CSlrdP6oDyp+l3cpaDvRKS+1ujl5BOW F3sGPjLtx7dCvHaj2GU4Kzg1USEODm8uNBNA4StnDG1KQTAYI1oyVZnJF+A83vbsea0rWBmirSwi GpWOvpaQXUJXxPkUAzUrHC1RVwinOt4/5Mi0A3PCwSaAuwtCH60NryZy2sy+s6ODWA2CxR9GUeOc GMyNm43sSet1UNWMKFnKdDTajAshqx7qG+XH/RU+wBeq+yNuJkbL+vmxcmtpzyKEC2IPrNkZAJSi djzULZrtBJ4tBmIQN1IchXIbJ+XMxjHsN+xjWZsLHXbMfjKaiJUINlK73nZfdklJrX+9ZSCyycEr dhh2n1ax -----END CERTIFICATE----- Certigna Root CA ================ -----BEGIN CERTIFICATE----- MIIGWzCCBEOgAwIBAgIRAMrpG4nxVQMNo+ZBbcTjpuEwDQYJKoZIhvcNAQELBQAwWjELMAkGA1UE BhMCRlIxEjAQBgNVBAoMCURoaW15b3RpczEcMBoGA1UECwwTMDAwMiA0ODE0NjMwODEwMDAzNjEZ MBcGA1UEAwwQQ2VydGlnbmEgUm9vdCBDQTAeFw0xMzEwMDEwODMyMjdaFw0zMzEwMDEwODMyMjda MFoxCzAJBgNVBAYTAkZSMRIwEAYDVQQKDAlEaGlteW90aXMxHDAaBgNVBAsMEzAwMDIgNDgxNDYz MDgxMDAwMzYxGTAXBgNVBAMMEENlcnRpZ25hIFJvb3QgQ0EwggIiMA0GCSqGSIb3DQEBAQUAA4IC DwAwggIKAoICAQDNGDllGlmx6mQWDoyUJJV8g9PFOSbcDO8WV43X2KyjQn+Cyu3NW9sOty3tRQgX stmzy9YXUnIo245Onoq2C/mehJpNdt4iKVzSs9IGPjA5qXSjklYcoW9MCiBtnyN6tMbaLOQdLNyz KNAT8kxOAkmhVECe5uUFoC2EyP+YbNDrihqECB63aCPuI9Vwzm1RaRDuoXrC0SIxwoKF0vJVdlB8 JXrJhFwLrN1CTivngqIkicuQstDuI7pmTLtipPlTWmR7fJj6o0ieD5Wupxj0auwuA0Wv8HT4Ks16 XdG+RCYyKfHx9WzMfgIhC59vpD++nVPiz32pLHxYGpfhPTc3GGYo0kDFUYqMwy3OU4gkWGQwFsWq 4NYKpkDfePb1BHxpE4S80dGnBs8B92jAqFe7OmGtBIyT46388NtEbVncSVmurJqZNjBBe3YzIoej wpKGbvlw7q6Hh5UbxHq9MfPU0uWZ/75I7HX1eBYdpnDBfzwboZL7z8g81sWTCo/1VTp2lc5ZmIoJ lXcymoO6LAQ6l73UL77XbJuiyn1tJslV1c/DeVIICZkHJC1kJWumIWmbat10TWuXekG9qxf5kBdI jzb5LdXF2+6qhUVB+s06RbFo5jZMm5BX7CO5hwjCxAnxl4YqKE3idMDaxIzb3+KhF1nOJFl0Mdp/ /TBt2dzhauH8XwIDAQABo4IBGjCCARYwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYw HQYDVR0OBBYEFBiHVuBud+4kNTxOc5of1uHieX4rMB8GA1UdIwQYMBaAFBiHVuBud+4kNTxOc5of 1uHieX4rMEQGA1UdIAQ9MDswOQYEVR0gADAxMC8GCCsGAQUFBwIBFiNodHRwczovL3d3d3cuY2Vy dGlnbmEuZnIvYXV0b3JpdGVzLzBtBgNVHR8EZjBkMC+gLaArhilodHRwOi8vY3JsLmNlcnRpZ25h LmZyL2NlcnRpZ25hcm9vdGNhLmNybDAxoC+gLYYraHR0cDovL2NybC5kaGlteW90aXMuY29tL2Nl cnRpZ25hcm9vdGNhLmNybDANBgkqhkiG9w0BAQsFAAOCAgEAlLieT/DjlQgi581oQfccVdV8AOIt OoldaDgvUSILSo3L6btdPrtcPbEo/uRTVRPPoZAbAh1fZkYJMyjhDSSXcNMQH+pkV5a7XdrnxIxP TGRGHVyH41neQtGbqH6mid2PHMkwgu07nM3A6RngatgCdTer9zQoKJHyBApPNeNgJgH60BGM+RFq 7q89w1DTj18zeTyGqHNFkIwgtnJzFyO+B2XleJINugHA64wcZr+shncBlA2c5uk5jR+mUYyZDDl3 4bSb+hxnV29qao6pK0xXeXpXIs/NX2NGjVxZOob4Mkdio2cNGJHc+6Zr9UhhcyNZjgKnvETq9Emd 8VRY+WCv2hikLyhF3HqgiIZd8zvn/yk1gPxkQ5Tm4xxvvq0OKmOZK8l+hfZx6AYDlf7ej0gcWtSS 6Cvu5zHbugRqh5jnxV/vfaci9wHYTfmJ0A6aBVmknpjZbyvKcL5kwlWj9Omvw5Ip3IgWJJk8jSaY tlu3zM63Nwf9JtmYhST/WSMDmu2dnajkXjjO11INb9I/bbEFa0nOipFGc/T2L/Coc3cOZayhjWZS aX5LaAzHHjcng6WMxwLkFM1JAbBzs/3GkDpv0mztO+7skb6iQ12LAEpmJURw3kAP+HwV96LOPNde E4yBFxgX0b3xdxA61GU5wSesVywlVP+i2k+KYTlerj1KjL0= -----END CERTIFICATE----- emSign Root CA - G1 =================== -----BEGIN CERTIFICATE----- MIIDlDCCAnygAwIBAgIKMfXkYgxsWO3W2DANBgkqhkiG9w0BAQsFADBnMQswCQYDVQQGEwJJTjET MBEGA1UECxMKZW1TaWduIFBLSTElMCMGA1UEChMcZU11ZGhyYSBUZWNobm9sb2dpZXMgTGltaXRl ZDEcMBoGA1UEAxMTZW1TaWduIFJvb3QgQ0EgLSBHMTAeFw0xODAyMTgxODMwMDBaFw00MzAyMTgx ODMwMDBaMGcxCzAJBgNVBAYTAklOMRMwEQYDVQQLEwplbVNpZ24gUEtJMSUwIwYDVQQKExxlTXVk aHJhIFRlY2hub2xvZ2llcyBMaW1pdGVkMRwwGgYDVQQDExNlbVNpZ24gUm9vdCBDQSAtIEcxMIIB IjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAk0u76WaK7p1b1TST0Bsew+eeuGQzf2N4aLTN LnF115sgxk0pvLZoYIr3IZpWNVrzdr3YzZr/k1ZLpVkGoZM0Kd0WNHVO8oG0x5ZOrRkVUkr+PHB1 cM2vK6sVmjM8qrOLqs1D/fXqcP/tzxE7lM5OMhbTI0Aqd7OvPAEsbO2ZLIvZTmmYsvePQbAyeGHW DV/D+qJAkh1cF+ZwPjXnorfCYuKrpDhMtTk1b+oDafo6VGiFbdbyL0NVHpENDtjVaqSW0RM8LHhQ 6DqS0hdW5TUaQBw+jSztOd9C4INBdN+jzcKGYEho42kLVACL5HZpIQ15TjQIXhTCzLG3rdd8cIrH hQIDAQABo0IwQDAdBgNVHQ4EFgQU++8Nhp6w492pufEhF38+/PB3KxowDgYDVR0PAQH/BAQDAgEG MA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQELBQADggEBAFn/8oz1h31xPaOfG1vR2vjTnGs2 vZupYeveFix0PZ7mddrXuqe8QhfnPZHr5X3dPpzxz5KsbEjMwiI/aTvFthUvozXGaCocV685743Q NcMYDHsAVhzNixl03r4PEuDQqqE/AjSxcM6dGNYIAwlG7mDgfrbESQRRfXBgvKqy/3lyeqYdPV8q +Mri/Tm3R7nrft8EI6/6nAYH6ftjk4BAtcZsCjEozgyfz7MjNYBBjWzEN3uBL4ChQEKF6dk4jeih U80Bv2noWgbyRQuQ+q7hv53yrlc8pa6yVvSLZUDp/TGBLPQ5Cdjua6e0ph0VpZj3AYHYhX3zUVxx iN66zB+Afko= -----END CERTIFICATE----- emSign ECC Root CA - G3 ======================= -----BEGIN CERTIFICATE----- MIICTjCCAdOgAwIBAgIKPPYHqWhwDtqLhDAKBggqhkjOPQQDAzBrMQswCQYDVQQGEwJJTjETMBEG A1UECxMKZW1TaWduIFBLSTElMCMGA1UEChMcZU11ZGhyYSBUZWNobm9sb2dpZXMgTGltaXRlZDEg MB4GA1UEAxMXZW1TaWduIEVDQyBSb290IENBIC0gRzMwHhcNMTgwMjE4MTgzMDAwWhcNNDMwMjE4 MTgzMDAwWjBrMQswCQYDVQQGEwJJTjETMBEGA1UECxMKZW1TaWduIFBLSTElMCMGA1UEChMcZU11 ZGhyYSBUZWNobm9sb2dpZXMgTGltaXRlZDEgMB4GA1UEAxMXZW1TaWduIEVDQyBSb290IENBIC0g RzMwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAAQjpQy4LRL1KPOxst3iAhKAnjlfSU2fySU0WXTsuwYc 58Byr+iuL+FBVIcUqEqy6HyC5ltqtdyzdc6LBtCGI79G1Y4PPwT01xySfvalY8L1X44uT6EYGQIr MgqCZH0Wk9GjQjBAMB0GA1UdDgQWBBR8XQKEE9TMipuBzhccLikenEhjQjAOBgNVHQ8BAf8EBAMC AQYwDwYDVR0TAQH/BAUwAwEB/zAKBggqhkjOPQQDAwNpADBmAjEAvvNhzwIQHWSVB7gYboiFBS+D CBeQyh+KTOgNG3qxrdWBCUfvO6wIBHxcmbHtRwfSAjEAnbpV/KlK6O3t5nYBQnvI+GDZjVGLVTv7 jHvrZQnD+JbNR6iC8hZVdyR+EhCVBCyj -----END CERTIFICATE----- emSign Root CA - C1 =================== -----BEGIN CERTIFICATE----- MIIDczCCAlugAwIBAgILAK7PALrEzzL4Q7IwDQYJKoZIhvcNAQELBQAwVjELMAkGA1UEBhMCVVMx EzARBgNVBAsTCmVtU2lnbiBQS0kxFDASBgNVBAoTC2VNdWRocmEgSW5jMRwwGgYDVQQDExNlbVNp Z24gUm9vdCBDQSAtIEMxMB4XDTE4MDIxODE4MzAwMFoXDTQzMDIxODE4MzAwMFowVjELMAkGA1UE BhMCVVMxEzARBgNVBAsTCmVtU2lnbiBQS0kxFDASBgNVBAoTC2VNdWRocmEgSW5jMRwwGgYDVQQD ExNlbVNpZ24gUm9vdCBDQSAtIEMxMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAz+up ufGZBczYKCFK83M0UYRWEPWgTywS4/oTmifQz/l5GnRfHXk5/Fv4cI7gklL35CX5VIPZHdPIWoU/ Xse2B+4+wM6ar6xWQio5JXDWv7V7Nq2s9nPczdcdioOl+yuQFTdrHCZH3DspVpNqs8FqOp099cGX OFgFixwR4+S0uF2FHYP+eF8LRWgYSKVGczQ7/g/IdrvHGPMF0Ybzhe3nudkyrVWIzqa2kbBPrH4V I5b2P/AgNBbeCsbEBEV5f6f9vtKppa+cxSMq9zwhbL2vj07FOrLzNBL834AaSaTUqZX3noleooms lMuoaJuvimUnzYnu3Yy1aylwQ6BpC+S5DwIDAQABo0IwQDAdBgNVHQ4EFgQU/qHgcB4qAzlSWkK+ XJGFehiqTbUwDgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQELBQAD ggEBAMJKVvoVIXsoounlHfv4LcQ5lkFMOycsxGwYFYDGrK9HWS8mC+M2sO87/kOXSTKZEhVb3xEp /6tT+LvBeA+snFOvV71ojD1pM/CjoCNjO2RnIkSt1XHLVip4kqNPEjE2NuLe/gDEo2APJ62gsIq1 NnpSob0n9CAnYuhNlCQT5AoE6TyrLshDCUrGYQTlSTR+08TI9Q/Aqum6VF7zYytPT1DU/rl7mYw9 wC68AivTxEDkigcxHpvOJpkT+xHqmiIMERnHXhuBUDDIlhJu58tBf5E7oke3VIAb3ADMmpDqw8NQ BmIMMMAVSKeoWXzhriKi4gp6D/piq1JM4fHfyr6DDUI= -----END CERTIFICATE----- emSign ECC Root CA - C3 ======================= -----BEGIN CERTIFICATE----- MIICKzCCAbGgAwIBAgIKe3G2gla4EnycqDAKBggqhkjOPQQDAzBaMQswCQYDVQQGEwJVUzETMBEG A1UECxMKZW1TaWduIFBLSTEUMBIGA1UEChMLZU11ZGhyYSBJbmMxIDAeBgNVBAMTF2VtU2lnbiBF Q0MgUm9vdCBDQSAtIEMzMB4XDTE4MDIxODE4MzAwMFoXDTQzMDIxODE4MzAwMFowWjELMAkGA1UE BhMCVVMxEzARBgNVBAsTCmVtU2lnbiBQS0kxFDASBgNVBAoTC2VNdWRocmEgSW5jMSAwHgYDVQQD ExdlbVNpZ24gRUNDIFJvb3QgQ0EgLSBDMzB2MBAGByqGSM49AgEGBSuBBAAiA2IABP2lYa57JhAd 6bciMK4G9IGzsUJxlTm801Ljr6/58pc1kjZGDoeVjbk5Wum739D+yAdBPLtVb4OjavtisIGJAnB9 SMVK4+kiVCJNk7tCDK93nCOmfddhEc5lx/h//vXyqaNCMEAwHQYDVR0OBBYEFPtaSNCAIEDyqOkA B2kZd6fmw/TPMA4GA1UdDwEB/wQEAwIBBjAPBgNVHRMBAf8EBTADAQH/MAoGCCqGSM49BAMDA2gA MGUCMQC02C8Cif22TGK6Q04ThHK1rt0c3ta13FaPWEBaLd4gTCKDypOofu4SQMfWh0/434UCMBwU ZOR8loMRnLDRWmFLpg9J0wD8ofzkpf9/rdcw0Md3f76BB1UwUCAU9Vc4CqgxUQ== -----END CERTIFICATE----- Hongkong Post Root CA 3 ======================= -----BEGIN CERTIFICATE----- MIIFzzCCA7egAwIBAgIUCBZfikyl7ADJk0DfxMauI7gcWqQwDQYJKoZIhvcNAQELBQAwbzELMAkG A1UEBhMCSEsxEjAQBgNVBAgTCUhvbmcgS29uZzESMBAGA1UEBxMJSG9uZyBLb25nMRYwFAYDVQQK Ew1Ib25na29uZyBQb3N0MSAwHgYDVQQDExdIb25na29uZyBQb3N0IFJvb3QgQ0EgMzAeFw0xNzA2 MDMwMjI5NDZaFw00MjA2MDMwMjI5NDZaMG8xCzAJBgNVBAYTAkhLMRIwEAYDVQQIEwlIb25nIEtv bmcxEjAQBgNVBAcTCUhvbmcgS29uZzEWMBQGA1UEChMNSG9uZ2tvbmcgUG9zdDEgMB4GA1UEAxMX SG9uZ2tvbmcgUG9zdCBSb290IENBIDMwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCz iNfqzg8gTr7m1gNt7ln8wlffKWihgw4+aMdoWJwcYEuJQwy51BWy7sFOdem1p+/l6TWZ5Mwc50tf jTMwIDNT2aa71T4Tjukfh0mtUC1Qyhi+AViiE3CWu4mIVoBc+L0sPOFMV4i707mV78vH9toxdCim 5lSJ9UExyuUmGs2C4HDaOym71QP1mbpV9WTRYA6ziUm4ii8F0oRFKHyPaFASePwLtVPLwpgchKOe sL4jpNrcyCse2m5FHomY2vkALgbpDDtw1VAliJnLzXNg99X/NWfFobxeq81KuEXryGgeDQ0URhLj 0mRiikKYvLTGCAj4/ahMZJx2Ab0vqWwzD9g/KLg8aQFChn5pwckGyuV6RmXpwtZQQS4/t+TtbNe/ JgERohYpSms0BpDsE9K2+2p20jzt8NYt3eEV7KObLyzJPivkaTv/ciWxNoZbx39ri1UbSsUgYT2u y1DhCDq+sI9jQVMwCFk8mB13umOResoQUGC/8Ne8lYePl8X+l2oBlKN8W4UdKjk60FSh0Tlxnf0h +bV78OLgAo9uliQlLKAeLKjEiafv7ZkGL7YKTE/bosw3Gq9HhS2KX8Q0NEwA/RiTZxPRN+ZItIsG xVd7GYYKecsAyVKvQv83j+GjHno9UKtjBucVtT+2RTeUN7F+8kjDf8V1/peNRY8apxpyKBpADwID AQABo2MwYTAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIBBjAfBgNVHSMEGDAWgBQXnc0e i9Y5K3DTXNSguB+wAPzFYTAdBgNVHQ4EFgQUF53NHovWOStw01zUoLgfsAD8xWEwDQYJKoZIhvcN AQELBQADggIBAFbVe27mIgHSQpsY1Q7XZiNc4/6gx5LS6ZStS6LG7BJ8dNVI0lkUmcDrudHr9Egw W62nV3OZqdPlt9EuWSRY3GguLmLYauRwCy0gUCCkMpXRAJi70/33MvJJrsZ64Ee+bs7Lo3I6LWld y8joRTnU+kLBEUx3XZL7av9YROXrgZ6voJmtvqkBZss4HTzfQx/0TW60uhdG/H39h4F5ag0zD/ov +BS5gLNdTaqX4fnkGMX41TiMJjz98iji7lpJiCzfeT2OnpA8vUFKOt1b9pq0zj8lMH8yfaIDlNDc eqFS3m6TjRgm/VWsvY+b0s+v54Ysyx8Jb6NvqYTUc79NoXQbTiNg8swOqn+knEwlqLJmOzj/2ZQw 9nKEvmhVEA/GcywWaZMH/rFF7buiVWqw2rVKAiUnhde3t4ZEFolsgCs+l6mc1X5VTMbeRRAc6uk7 nwNT7u56AQIWeNTowr5GdogTPyK7SBIdUgC0An4hGh6cJfTzPV4e0hz5sy229zdcxsshTrD3mUcY hcErulWuBurQB7Lcq9CClnXO0lD+mefPL5/ndtFhKvshuzHQqp9HpLIiyhY6UFfEW0NnxWViA0kB 60PZ2Pierc+xYw5F9KBaLJstxabArahH9CdMOA0uG0k7UvToiIMrVCjU8jVStDKDYmlkDJGcn5fq dBb9HxEGmpv0 -----END CERTIFICATE----- Entrust Root Certification Authority - G4 ========================================= -----BEGIN CERTIFICATE----- MIIGSzCCBDOgAwIBAgIRANm1Q3+vqTkPAAAAAFVlrVgwDQYJKoZIhvcNAQELBQAwgb4xCzAJBgNV BAYTAlVTMRYwFAYDVQQKEw1FbnRydXN0LCBJbmMuMSgwJgYDVQQLEx9TZWUgd3d3LmVudHJ1c3Qu bmV0L2xlZ2FsLXRlcm1zMTkwNwYDVQQLEzAoYykgMjAxNSBFbnRydXN0LCBJbmMuIC0gZm9yIGF1 dGhvcml6ZWQgdXNlIG9ubHkxMjAwBgNVBAMTKUVudHJ1c3QgUm9vdCBDZXJ0aWZpY2F0aW9uIEF1 dGhvcml0eSAtIEc0MB4XDTE1MDUyNzExMTExNloXDTM3MTIyNzExNDExNlowgb4xCzAJBgNVBAYT AlVTMRYwFAYDVQQKEw1FbnRydXN0LCBJbmMuMSgwJgYDVQQLEx9TZWUgd3d3LmVudHJ1c3QubmV0 L2xlZ2FsLXRlcm1zMTkwNwYDVQQLEzAoYykgMjAxNSBFbnRydXN0LCBJbmMuIC0gZm9yIGF1dGhv cml6ZWQgdXNlIG9ubHkxMjAwBgNVBAMTKUVudHJ1c3QgUm9vdCBDZXJ0aWZpY2F0aW9uIEF1dGhv cml0eSAtIEc0MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAsewsQu7i0TD/pZJH4i3D umSXbcr3DbVZwbPLqGgZ2K+EbTBwXX7zLtJTmeH+H17ZSK9dE43b/2MzTdMAArzE+NEGCJR5WIoV 3imz/f3ET+iq4qA7ec2/a0My3dl0ELn39GjUu9CH1apLiipvKgS1sqbHoHrmSKvS0VnM1n4j5pds 8ELl3FFLFUHtSUrJ3hCX1nbB76W1NhSXNdh4IjVS70O92yfbYVaCNNzLiGAMC1rlLAHGVK/XqsEQ e9IFWrhAnoanw5CGAlZSCXqc0ieCU0plUmr1POeo8pyvi73TDtTUXm6Hnmo9RR3RXRv06QqsYJn7 ibT/mCzPfB3pAqoEmh643IhuJbNsZvc8kPNXwbMv9W3y+8qh+CmdRouzavbmZwe+LGcKKh9asj5X xNMhIWNlUpEbsZmOeX7m640A2Vqq6nPopIICR5b+W45UYaPrL0swsIsjdXJ8ITzI9vF01Bx7owVV 7rtNOzK+mndmnqxpkCIHH2E6lr7lmk/MBTwoWdPBDFSoWWG9yHJM6Nyfh3+9nEg2XpWjDrk4JFX8 dWbrAuMINClKxuMrLzOg2qOGpRKX/YAr2hRC45K9PvJdXmd0LhyIRyk0X+IyqJwlN4y6mACXi0mW Hv0liqzc2thddG5msP9E36EYxr5ILzeUePiVSj9/E15dWf10hkNjc0kCAwEAAaNCMEAwDwYDVR0T AQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYwHQYDVR0OBBYEFJ84xFYjwznooHFs6FRM5Og6sb9n MA0GCSqGSIb3DQEBCwUAA4ICAQAS5UKme4sPDORGpbZgQIeMJX6tuGguW8ZAdjwD+MlZ9POrYs4Q jbRaZIxowLByQzTSGwv2LFPSypBLhmb8qoMi9IsabyZIrHZ3CL/FmFz0Jomee8O5ZDIBf9PD3Vht 7LGrhFV0d4QEJ1JrhkzO3bll/9bGXp+aEJlLdWr+aumXIOTkdnrG0CSqkM0gkLpHZPt/B7NTeLUK YvJzQ85BK4FqLoUWlFPUa19yIqtRLULVAJyZv967lDtX/Zr1hstWO1uIAeV8KEsD+UmDfLJ/fOPt jqF/YFOOVZ1QNBIPt5d7bIdKROf1beyAN/BYGW5KaHbwH5Lk6rWS02FREAutp9lfx1/cH6NcjKF+ m7ee01ZvZl4HliDtC3T7Zk6LERXpgUl+b7DUUH8i119lAg2m9IUe2K4GS0qn0jFmwvjO5QimpAKW RGhXxNUzzxkvFMSUHHuk2fCfDrGA4tGeEWSpiBE6doLlYsKA2KSD7ZPvfC+QsDJMlhVoSFLUmQjA JOgc47OlIQ6SwJAfzyBfyjs4x7dtOvPmRLgOMWuIjnDrnBdSqEGULoe256YSxXXfW8AKbnuk5F6G +TaU33fD6Q3AOfF5u0aOq0NZJ7cguyPpVkAh7DE9ZapD8j3fcEThuk0mEDuYn/PIjhs4ViFqUZPT kcpG2om3PVODLAgfi49T3f+sHw== -----END CERTIFICATE----- Microsoft ECC Root Certificate Authority 2017 ============================================= -----BEGIN CERTIFICATE----- MIICWTCCAd+gAwIBAgIQZvI9r4fei7FK6gxXMQHC7DAKBggqhkjOPQQDAzBlMQswCQYDVQQGEwJV UzEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMTYwNAYDVQQDEy1NaWNyb3NvZnQgRUND IFJvb3QgQ2VydGlmaWNhdGUgQXV0aG9yaXR5IDIwMTcwHhcNMTkxMjE4MjMwNjQ1WhcNNDIwNzE4 MjMxNjA0WjBlMQswCQYDVQQGEwJVUzEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMTYw NAYDVQQDEy1NaWNyb3NvZnQgRUNDIFJvb3QgQ2VydGlmaWNhdGUgQXV0aG9yaXR5IDIwMTcwdjAQ BgcqhkjOPQIBBgUrgQQAIgNiAATUvD0CQnVBEyPNgASGAlEvaqiBYgtlzPbKnR5vSmZRogPZnZH6 thaxjG7efM3beaYvzrvOcS/lpaso7GMEZpn4+vKTEAXhgShC48Zo9OYbhGBKia/teQ87zvH2RPUB eMCjVDBSMA4GA1UdDwEB/wQEAwIBhjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBTIy5lycFIM +Oa+sgRXKSrPQhDtNTAQBgkrBgEEAYI3FQEEAwIBADAKBggqhkjOPQQDAwNoADBlAjBY8k3qDPlf Xu5gKcs68tvWMoQZP3zVL8KxzJOuULsJMsbG7X7JNpQS5GiFBqIb0C8CMQCZ6Ra0DvpWSNSkMBaR eNtUjGUBiudQZsIxtzm6uBoiB078a1QWIP8rtedMDE2mT3M= -----END CERTIFICATE----- Microsoft RSA Root Certificate Authority 2017 ============================================= -----BEGIN CERTIFICATE----- MIIFqDCCA5CgAwIBAgIQHtOXCV/YtLNHcB6qvn9FszANBgkqhkiG9w0BAQwFADBlMQswCQYDVQQG EwJVUzEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMTYwNAYDVQQDEy1NaWNyb3NvZnQg UlNBIFJvb3QgQ2VydGlmaWNhdGUgQXV0aG9yaXR5IDIwMTcwHhcNMTkxMjE4MjI1MTIyWhcNNDIw NzE4MjMwMDIzWjBlMQswCQYDVQQGEwJVUzEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9u MTYwNAYDVQQDEy1NaWNyb3NvZnQgUlNBIFJvb3QgQ2VydGlmaWNhdGUgQXV0aG9yaXR5IDIwMTcw ggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDKW76UM4wplZEWCpW9R2LBifOZNt9GkMml 7Xhqb0eRaPgnZ1AzHaGm++DlQ6OEAlcBXZxIQIJTELy/xztokLaCLeX0ZdDMbRnMlfl7rEqUrQ7e S0MdhweSE5CAg2Q1OQT85elss7YfUJQ4ZVBcF0a5toW1HLUX6NZFndiyJrDKxHBKrmCk3bPZ7Pw7 1VdyvD/IybLeS2v4I2wDwAW9lcfNcztmgGTjGqwu+UcF8ga2m3P1eDNbx6H7JyqhtJqRjJHTOoI+ dkC0zVJhUXAoP8XFWvLJjEm7FFtNyP9nTUwSlq31/niol4fX/V4ggNyhSyL71Imtus5Hl0dVe49F yGcohJUcaDDv70ngNXtk55iwlNpNhTs+VcQor1fznhPbRiefHqJeRIOkpcrVE7NLP8TjwuaGYaRS MLl6IE9vDzhTyzMMEyuP1pq9KsgtsRx9S1HKR9FIJ3Jdh+vVReZIZZ2vUpC6W6IYZVcSn2i51BVr lMRpIpj0M+Dt+VGOQVDJNE92kKz8OMHY4Xu54+OU4UZpyw4KUGsTuqwPN1q3ErWQgR5WrlcihtnJ 0tHXUeOrO8ZV/R4O03QK0dqq6mm4lyiPSMQH+FJDOvTKVTUssKZqwJz58oHhEmrARdlns87/I6KJ ClTUFLkqqNfs+avNJVgyeY+QW5g5xAgGwax/Dj0ApQIDAQABo1QwUjAOBgNVHQ8BAf8EBAMCAYYw DwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUCctZf4aycI8awznjwNnpv7tNsiMwEAYJKwYBBAGC NxUBBAMCAQAwDQYJKoZIhvcNAQEMBQADggIBAKyvPl3CEZaJjqPnktaXFbgToqZCLgLNFgVZJ8og 6Lq46BrsTaiXVq5lQ7GPAJtSzVXNUzltYkyLDVt8LkS/gxCP81OCgMNPOsduET/m4xaRhPtthH80 dK2Jp86519efhGSSvpWhrQlTM93uCupKUY5vVau6tZRGrox/2KJQJWVggEbbMwSubLWYdFQl3JPk +ONVFT24bcMKpBLBaYVu32TxU5nhSnUgnZUP5NbcA/FZGOhHibJXWpS2qdgXKxdJ5XbLwVaZOjex /2kskZGT4d9Mozd2TaGf+G0eHdP67Pv0RR0Tbc/3WeUiJ3IrhvNXuzDtJE3cfVa7o7P4NHmJweDy AmH3pvwPuxwXC65B2Xy9J6P9LjrRk5Sxcx0ki69bIImtt2dmefU6xqaWM/5TkshGsRGRxpl/j8nW ZjEgQRCHLQzWwa80mMpkg/sTV9HB8Dx6jKXB/ZUhoHHBk2dxEuqPiAppGWSZI1b7rCoucL5mxAyE 7+WL85MB+GqQk2dLsmijtWKP6T+MejteD+eMuMZ87zf9dOLITzNy4ZQ5bb0Sr74MTnB8G2+NszKT c0QWbej09+CVgI+WXTik9KveCjCHk9hNAHFiRSdLOkKEW39lt2c0Ui2cFmuqqNh7o0JMcccMyj6D 5KbvtwEwXlGjefVwaaZBRA+GsCyRxj3qrg+E -----END CERTIFICATE----- e-Szigno Root CA 2017 ===================== -----BEGIN CERTIFICATE----- MIICQDCCAeWgAwIBAgIMAVRI7yH9l1kN9QQKMAoGCCqGSM49BAMCMHExCzAJBgNVBAYTAkhVMREw DwYDVQQHDAhCdWRhcGVzdDEWMBQGA1UECgwNTWljcm9zZWMgTHRkLjEXMBUGA1UEYQwOVkFUSFUt MjM1ODQ0OTcxHjAcBgNVBAMMFWUtU3ppZ25vIFJvb3QgQ0EgMjAxNzAeFw0xNzA4MjIxMjA3MDZa Fw00MjA4MjIxMjA3MDZaMHExCzAJBgNVBAYTAkhVMREwDwYDVQQHDAhCdWRhcGVzdDEWMBQGA1UE CgwNTWljcm9zZWMgTHRkLjEXMBUGA1UEYQwOVkFUSFUtMjM1ODQ0OTcxHjAcBgNVBAMMFWUtU3pp Z25vIFJvb3QgQ0EgMjAxNzBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABJbcPYrYsHtvxie+RJCx s1YVe45DJH0ahFnuY2iyxl6H0BVIHqiQrb1TotreOpCmYF9oMrWGQd+HWyx7xf58etqjYzBhMA8G A1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgEGMB0GA1UdDgQWBBSHERUI0arBeAyxr87GyZDv vzAEwDAfBgNVHSMEGDAWgBSHERUI0arBeAyxr87GyZDvvzAEwDAKBggqhkjOPQQDAgNJADBGAiEA tVfd14pVCzbhhkT61NlojbjcI4qKDdQvfepz7L9NbKgCIQDLpbQS+ue16M9+k/zzNY9vTlp8tLxO svxyqltZ+efcMQ== -----END CERTIFICATE----- certSIGN Root CA G2 =================== -----BEGIN CERTIFICATE----- MIIFRzCCAy+gAwIBAgIJEQA0tk7GNi02MA0GCSqGSIb3DQEBCwUAMEExCzAJBgNVBAYTAlJPMRQw EgYDVQQKEwtDRVJUU0lHTiBTQTEcMBoGA1UECxMTY2VydFNJR04gUk9PVCBDQSBHMjAeFw0xNzAy MDYwOTI3MzVaFw00MjAyMDYwOTI3MzVaMEExCzAJBgNVBAYTAlJPMRQwEgYDVQQKEwtDRVJUU0lH TiBTQTEcMBoGA1UECxMTY2VydFNJR04gUk9PVCBDQSBHMjCCAiIwDQYJKoZIhvcNAQEBBQADggIP ADCCAgoCggIBAMDFdRmRfUR0dIf+DjuW3NgBFszuY5HnC2/OOwppGnzC46+CjobXXo9X69MhWf05 N0IwvlDqtg+piNguLWkh59E3GE59kdUWX2tbAMI5Qw02hVK5U2UPHULlj88F0+7cDBrZuIt4Imfk abBoxTzkbFpG583H+u/E7Eu9aqSs/cwoUe+StCmrqzWaTOTECMYmzPhpn+Sc8CnTXPnGFiWeI8Mg wT0PPzhAsP6CRDiqWhqKa2NYOLQV07YRaXseVO6MGiKscpc/I1mbySKEwQdPzH/iV8oScLumZfNp dWO9lfsbl83kqK/20U6o2YpxJM02PbyWxPFsqa7lzw1uKA2wDrXKUXt4FMMgL3/7FFXhEZn91Qqh ngLjYl/rNUssuHLoPj1PrCy7Lobio3aP5ZMqz6WryFyNSwb/EkaseMsUBzXgqd+L6a8VTxaJW732 jcZZroiFDsGJ6x9nxUWO/203Nit4ZoORUSs9/1F3dmKh7Gc+PoGD4FapUB8fepmrY7+EF3fxDTvf 95xhszWYijqy7DwaNz9+j5LP2RIUZNoQAhVB/0/E6xyjyfqZ90bp4RjZsbgyLcsUDFDYg2WD7rlc z8sFWkz6GZdr1l0T08JcVLwyc6B49fFtHsufpaafItzRUZ6CeWRgKRM+o/1Pcmqr4tTluCRVLERL iohEnMqE0yo7AgMBAAGjQjBAMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgEGMB0GA1Ud DgQWBBSCIS1mxteg4BXrzkwJd8RgnlRuAzANBgkqhkiG9w0BAQsFAAOCAgEAYN4auOfyYILVAzOB ywaK8SJJ6ejqkX/GM15oGQOGO0MBzwdw5AgeZYWR5hEit/UCI46uuR59H35s5r0l1ZUa8gWmr4UC b6741jH/JclKyMeKqdmfS0mbEVeZkkMR3rYzpMzXjWR91M08KCy0mpbqTfXERMQlqiCA2ClV9+BB /AYm/7k29UMUA2Z44RGx2iBfRgB4ACGlHgAoYXhvqAEBj500mv/0OJD7uNGzcgbJceaBxXntC6Z5 8hMLnPddDnskk7RI24Zf3lCGeOdA5jGokHZwYa+cNywRtYK3qq4kNFtyDGkNzVmf9nGvnAvRCjj5 BiKDUyUM/FHE5r7iOZULJK2v0ZXkltd0ZGtxTgI8qoXzIKNDOXZbbFD+mpwUHmUUihW9o4JFWklW atKcsWMy5WHgUyIOpwpJ6st+H6jiYoD2EEVSmAYY3qXNL3+q1Ok+CHLsIwMCPKaq2LxndD0UF/tU Sxfj03k9bWtJySgOLnRQvwzZRjoQhsmnP+mg7H/rpXdYaXHmgwo38oZJar55CJD2AhZkPuXaTH4M NMn5X7azKFGnpyuqSfqNZSlO42sTp5SjLVFteAxEy9/eCG/Oo2Sr05WE1LlSVHJ7liXMvGnjSG4N 0MedJ5qq+BOS3R7fY581qRY27Iy4g/Q9iY/NtBde17MXQRBdJ3NghVdJIgc= -----END CERTIFICATE----- Trustwave Global Certification Authority ======================================== -----BEGIN CERTIFICATE----- MIIF2jCCA8KgAwIBAgIMBfcOhtpJ80Y1LrqyMA0GCSqGSIb3DQEBCwUAMIGIMQswCQYDVQQGEwJV UzERMA8GA1UECAwISWxsaW5vaXMxEDAOBgNVBAcMB0NoaWNhZ28xITAfBgNVBAoMGFRydXN0d2F2 ZSBIb2xkaW5ncywgSW5jLjExMC8GA1UEAwwoVHJ1c3R3YXZlIEdsb2JhbCBDZXJ0aWZpY2F0aW9u IEF1dGhvcml0eTAeFw0xNzA4MjMxOTM0MTJaFw00MjA4MjMxOTM0MTJaMIGIMQswCQYDVQQGEwJV UzERMA8GA1UECAwISWxsaW5vaXMxEDAOBgNVBAcMB0NoaWNhZ28xITAfBgNVBAoMGFRydXN0d2F2 ZSBIb2xkaW5ncywgSW5jLjExMC8GA1UEAwwoVHJ1c3R3YXZlIEdsb2JhbCBDZXJ0aWZpY2F0aW9u IEF1dGhvcml0eTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBALldUShLPDeS0YLOvR29 zd24q88KPuFd5dyqCblXAj7mY2Hf8g+CY66j96xz0XznswuvCAAJWX/NKSqIk4cXGIDtiLK0thAf LdZfVaITXdHG6wZWiYj+rDKd/VzDBcdu7oaJuogDnXIhhpCujwOl3J+IKMujkkkP7NAP4m1ET4Bq stTnoApTAbqOl5F2brz81Ws25kCI1nsvXwXoLG0R8+eyvpJETNKXpP7ScoFDB5zpET71ixpZfR9o WN0EACyW80OzfpgZdNmcc9kYvkHHNHnZ9GLCQ7mzJ7Aiy/k9UscwR7PJPrhq4ufogXBeQotPJqX+ OsIgbrv4Fo7NDKm0G2x2EOFYeUY+VM6AqFcJNykbmROPDMjWLBz7BegIlT1lRtzuzWniTY+HKE40 Cz7PFNm73bZQmq131BnW2hqIyE4bJ3XYsgjxroMwuREOzYfwhI0Vcnyh78zyiGG69Gm7DIwLdVcE uE4qFC49DxweMqZiNu5m4iK4BUBjECLzMx10coos9TkpoNPnG4CELcU9402x/RpvumUHO1jsQkUm +9jaJXLE9gCxInm943xZYkqcBW89zubWR2OZxiRvchLIrH+QtAuRcOi35hYQcRfO3gZPSEF9NUqj ifLJS3tBEW1ntwiYTOURGa5CgNz7kAXU+FDKvuStx8KU1xad5hePrzb7AgMBAAGjQjBAMA8GA1Ud EwEB/wQFMAMBAf8wHQYDVR0OBBYEFJngGWcNYtt2s9o9uFvo/ULSMQ6HMA4GA1UdDwEB/wQEAwIB BjANBgkqhkiG9w0BAQsFAAOCAgEAmHNw4rDT7TnsTGDZqRKGFx6W0OhUKDtkLSGm+J1WE2pIPU/H PinbbViDVD2HfSMF1OQc3Og4ZYbFdada2zUFvXfeuyk3QAUHw5RSn8pk3fEbK9xGChACMf1KaA0H ZJDmHvUqoai7PF35owgLEQzxPy0QlG/+4jSHg9bP5Rs1bdID4bANqKCqRieCNqcVtgimQlRXtpla 4gt5kNdXElE1GYhBaCXUNxeEFfsBctyV3lImIJgm4nb1J2/6ADtKYdkNy1GTKv0WBpanI5ojSP5R vbbEsLFUzt5sQa0WZ37b/TjNuThOssFgy50X31ieemKyJo90lZvkWx3SD92YHJtZuSPTMaCm/zjd zyBP6VhWOmfD0faZmZ26NraAL4hHT4a/RDqA5Dccprrql5gR0IRiR2Qequ5AvzSxnI9O4fKSTx+O 856X3vOmeWqJcU9LJxdI/uz0UA9PSX3MReO9ekDFQdxhVicGaeVyQYHTtgGJoC86cnn+OjC/QezH Yj6RS8fZMXZC+fc8Y+wmjHMMfRod6qh8h6jCJ3zhM0EPz8/8AKAigJ5Kp28AsEFFtyLKaEjFQqKu 3R3y4G5OBVixwJAWKqQ9EEC+j2Jjg6mcgn0tAumDMHzLJ8n9HmYAsC7TIS+OMxZsmO0QqAfWzJPP 29FpHOTKyeC2nOnOcXHebD8WpHk= -----END CERTIFICATE----- Trustwave Global ECC P256 Certification Authority ================================================= -----BEGIN CERTIFICATE----- MIICYDCCAgegAwIBAgIMDWpfCD8oXD5Rld9dMAoGCCqGSM49BAMCMIGRMQswCQYDVQQGEwJVUzER MA8GA1UECBMISWxsaW5vaXMxEDAOBgNVBAcTB0NoaWNhZ28xITAfBgNVBAoTGFRydXN0d2F2ZSBI b2xkaW5ncywgSW5jLjE6MDgGA1UEAxMxVHJ1c3R3YXZlIEdsb2JhbCBFQ0MgUDI1NiBDZXJ0aWZp Y2F0aW9uIEF1dGhvcml0eTAeFw0xNzA4MjMxOTM1MTBaFw00MjA4MjMxOTM1MTBaMIGRMQswCQYD VQQGEwJVUzERMA8GA1UECBMISWxsaW5vaXMxEDAOBgNVBAcTB0NoaWNhZ28xITAfBgNVBAoTGFRy dXN0d2F2ZSBIb2xkaW5ncywgSW5jLjE6MDgGA1UEAxMxVHJ1c3R3YXZlIEdsb2JhbCBFQ0MgUDI1 NiBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABH77bOYj 43MyCMpg5lOcunSNGLB4kFKA3TjASh3RqMyTpJcGOMoNFWLGjgEqZZ2q3zSRLoHB5DOSMcT9CTqm P62jQzBBMA8GA1UdEwEB/wQFMAMBAf8wDwYDVR0PAQH/BAUDAwcGADAdBgNVHQ4EFgQUo0EGrJBt 0UrrdaVKEJmzsaGLSvcwCgYIKoZIzj0EAwIDRwAwRAIgB+ZU2g6gWrKuEZ+Hxbb/ad4lvvigtwjz RM4q3wghDDcCIC0mA6AFvWvR9lz4ZcyGbbOcNEhjhAnFjXca4syc4XR7 -----END CERTIFICATE----- Trustwave Global ECC P384 Certification Authority ================================================= -----BEGIN CERTIFICATE----- MIICnTCCAiSgAwIBAgIMCL2Fl2yZJ6SAaEc7MAoGCCqGSM49BAMDMIGRMQswCQYDVQQGEwJVUzER MA8GA1UECBMISWxsaW5vaXMxEDAOBgNVBAcTB0NoaWNhZ28xITAfBgNVBAoTGFRydXN0d2F2ZSBI b2xkaW5ncywgSW5jLjE6MDgGA1UEAxMxVHJ1c3R3YXZlIEdsb2JhbCBFQ0MgUDM4NCBDZXJ0aWZp Y2F0aW9uIEF1dGhvcml0eTAeFw0xNzA4MjMxOTM2NDNaFw00MjA4MjMxOTM2NDNaMIGRMQswCQYD VQQGEwJVUzERMA8GA1UECBMISWxsaW5vaXMxEDAOBgNVBAcTB0NoaWNhZ28xITAfBgNVBAoTGFRy dXN0d2F2ZSBIb2xkaW5ncywgSW5jLjE6MDgGA1UEAxMxVHJ1c3R3YXZlIEdsb2JhbCBFQ0MgUDM4 NCBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTB2MBAGByqGSM49AgEGBSuBBAAiA2IABGvaDXU1CDFH Ba5FmVXxERMuSvgQMSOjfoPTfygIOiYaOs+Xgh+AtycJj9GOMMQKmw6sWASr9zZ9lCOkmwqKi6vr /TklZvFe/oyujUF5nQlgziip04pt89ZF1PKYhDhloKNDMEEwDwYDVR0TAQH/BAUwAwEB/zAPBgNV HQ8BAf8EBQMDBwYAMB0GA1UdDgQWBBRVqYSJ0sEyvRjLbKYHTsjnnb6CkDAKBggqhkjOPQQDAwNn ADBkAjA3AZKXRRJ+oPM+rRk6ct30UJMDEr5E0k9BpIycnR+j9sKS50gU/k6bpZFXrsY3crsCMGcl CrEMXu6pY5Jv5ZAL/mYiykf9ijH3g/56vxC+GCsej/YpHpRZ744hN8tRmKVuSw== -----END CERTIFICATE----- NAVER Global Root Certification Authority ========================================= -----BEGIN CERTIFICATE----- MIIFojCCA4qgAwIBAgIUAZQwHqIL3fXFMyqxQ0Rx+NZQTQ0wDQYJKoZIhvcNAQEMBQAwaTELMAkG A1UEBhMCS1IxJjAkBgNVBAoMHU5BVkVSIEJVU0lORVNTIFBMQVRGT1JNIENvcnAuMTIwMAYDVQQD DClOQVZFUiBHbG9iYWwgUm9vdCBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTAeFw0xNzA4MTgwODU4 NDJaFw0zNzA4MTgyMzU5NTlaMGkxCzAJBgNVBAYTAktSMSYwJAYDVQQKDB1OQVZFUiBCVVNJTkVT UyBQTEFURk9STSBDb3JwLjEyMDAGA1UEAwwpTkFWRVIgR2xvYmFsIFJvb3QgQ2VydGlmaWNhdGlv biBBdXRob3JpdHkwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQC21PGTXLVAiQqrDZBb UGOukJR0F0Vy1ntlWilLp1agS7gvQnXp2XskWjFlqxcX0TM62RHcQDaH38dq6SZeWYp34+hInDEW +j6RscrJo+KfziFTowI2MMtSAuXaMl3Dxeb57hHHi8lEHoSTGEq0n+USZGnQJoViAbbJAh2+g1G7 XNr4rRVqmfeSVPc0W+m/6imBEtRTkZazkVrd/pBzKPswRrXKCAfHcXLJZtM0l/aM9BhK4dA9WkW2 aacp+yPOiNgSnABIqKYPszuSjXEOdMWLyEz59JuOuDxp7W87UC9Y7cSw0BwbagzivESq2M0UXZR4 Yb8ObtoqvC8MC3GmsxY/nOb5zJ9TNeIDoKAYv7vxvvTWjIcNQvcGufFt7QSUqP620wbGQGHfnZ3z VHbOUzoBppJB7ASjjw2i1QnK1sua8e9DXcCrpUHPXFNwcMmIpi3Ua2FzUCaGYQ5fG8Ir4ozVu53B A0K6lNpfqbDKzE0K70dpAy8i+/Eozr9dUGWokG2zdLAIx6yo0es+nPxdGoMuK8u180SdOqcXYZai cdNwlhVNt0xz7hlcxVs+Qf6sdWA7G2POAN3aCJBitOUt7kinaxeZVL6HSuOpXgRM6xBtVNbv8ejy YhbLgGvtPe31HzClrkvJE+2KAQHJuFFYwGY6sWZLxNUxAmLpdIQM201GLQIDAQABo0IwQDAdBgNV HQ4EFgQU0p+I36HNLL3s9TsBAZMzJ7LrYEswDgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQFMAMB Af8wDQYJKoZIhvcNAQEMBQADggIBADLKgLOdPVQG3dLSLvCkASELZ0jKbY7gyKoNqo0hV4/GPnrK 21HUUrPUloSlWGB/5QuOH/XcChWB5Tu2tyIvCZwTFrFsDDUIbatjcu3cvuzHV+YwIHHW1xDBE1UB jCpD5EHxzzp6U5LOogMFDTjfArsQLtk70pt6wKGm+LUx5vR1yblTmXVHIloUFcd4G7ad6Qz4G3bx hYTeodoS76TiEJd6eN4MUZeoIUCLhr0N8F5OSza7OyAfikJW4Qsav3vQIkMsRIz75Sq0bBwcupTg E34h5prCy8VCZLQelHsIJchxzIdFV4XTnyliIoNRlwAYl3dqmJLJfGBs32x9SuRwTMKeuB330DTH D8z7p/8Dvq1wkNoL3chtl1+afwkyQf3NosxabUzyqkn+Zvjp2DXrDige7kgvOtB5CTh8piKCk5XQ A76+AqAF3SAi428diDRgxuYKuQl1C/AH6GmWNcf7I4GOODm4RStDeKLRLBT/DShycpWbXgnbiUSY qqFJu3FS8r/2/yehNq+4tneI3TqkbZs0kNwUXTC/t+sX5Ie3cdCh13cV1ELX8vMxmV2b3RZtP+oG I/hGoiLtk/bdmuYqh7GYVPEi92tF4+KOdh2ajcQGjTa3FPOdVGm3jjzVpG2Tgbet9r1ke8LJaDmg kpzNNIaRkPpkUZ3+/uul9XXeifdy -----END CERTIFICATE----- Thawte Server CA ================ -----BEGIN CERTIFICATE----- MIIDEzCCAnygAwIBAgIBATANBgkqhkiG9w0BAQQFADCBxDELMAkGA1UEBhMCWkExFTATBgNVBAgT DFdlc3Rlcm4gQ2FwZTESMBAGA1UEBxMJQ2FwZSBUb3duMR0wGwYDVQQKExRUaGF3dGUgQ29uc3Vs dGluZyBjYzEoMCYGA1UECxMfQ2VydGlmaWNhdGlvbiBTZXJ2aWNlcyBEaXZpc2lvbjEZMBcGA1UE AxMQVGhhd3RlIFNlcnZlciBDQTEmMCQGCSqGSIb3DQEJARYXc2VydmVyLWNlcnRzQHRoYXd0ZS5j b20wHhcNOTYwODAxMDAwMDAwWhcNMjAxMjMxMjM1OTU5WjCBxDELMAkGA1UEBhMCWkExFTATBgNV BAgTDFdlc3Rlcm4gQ2FwZTESMBAGA1UEBxMJQ2FwZSBUb3duMR0wGwYDVQQKExRUaGF3dGUgQ29u c3VsdGluZyBjYzEoMCYGA1UECxMfQ2VydGlmaWNhdGlvbiBTZXJ2aWNlcyBEaXZpc2lvbjEZMBcG A1UEAxMQVGhhd3RlIFNlcnZlciBDQTEmMCQGCSqGSIb3DQEJARYXc2VydmVyLWNlcnRzQHRoYXd0 ZS5jb20wgZ8wDQYJKoZIhvcNAQEBBQADgY0AMIGJAoGBANOkUG7I/1Zr5s9dtuoMaHVHoqrC2oQl /Kj0R1HahbUgdJSGHg91yekIYfUGbTBuFRkC6VLAYttNmZ7iagxEOM3+vuNkCXDF/rFrKbYvScg7 1CcEJRCXL+eQbcAoQpnXTEPew/UhbVSfXcNY4cDk2VuwuNy0e982OsK1ZiIS1ocNAgMBAAGjEzAR MA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQEEBQADgYEAB/pMaVz7lcxG7oWDTSEwjsrZqG9J GubaUeNgcGyEYRGhGshIPllDfU+VPaGLtwtimHp1it2ITk6eQNuozDJ0uW8NxuOzRAvZim+aKZuZ GCg70eNAKJpaPNW15yAbi8qkq43pUdniTCxZqdq5snUb9kLy78fyGPmJvKP/iiMucEc= -----END CERTIFICATE----- Thawte Premium Server CA ======================== -----BEGIN CERTIFICATE----- MIIDJzCCApCgAwIBAgIBATANBgkqhkiG9w0BAQQFADCBzjELMAkGA1UEBhMCWkExFTATBgNVBAgT DFdlc3Rlcm4gQ2FwZTESMBAGA1UEBxMJQ2FwZSBUb3duMR0wGwYDVQQKExRUaGF3dGUgQ29uc3Vs dGluZyBjYzEoMCYGA1UECxMfQ2VydGlmaWNhdGlvbiBTZXJ2aWNlcyBEaXZpc2lvbjEhMB8GA1UE AxMYVGhhd3RlIFByZW1pdW0gU2VydmVyIENBMSgwJgYJKoZIhvcNAQkBFhlwcmVtaXVtLXNlcnZl ckB0aGF3dGUuY29tMB4XDTk2MDgwMTAwMDAwMFoXDTIwMTIzMTIzNTk1OVowgc4xCzAJBgNVBAYT AlpBMRUwEwYDVQQIEwxXZXN0ZXJuIENhcGUxEjAQBgNVBAcTCUNhcGUgVG93bjEdMBsGA1UEChMU VGhhd3RlIENvbnN1bHRpbmcgY2MxKDAmBgNVBAsTH0NlcnRpZmljYXRpb24gU2VydmljZXMgRGl2 aXNpb24xITAfBgNVBAMTGFRoYXd0ZSBQcmVtaXVtIFNlcnZlciBDQTEoMCYGCSqGSIb3DQEJARYZ cHJlbWl1bS1zZXJ2ZXJAdGhhd3RlLmNvbTCBnzANBgkqhkiG9w0BAQEFAAOBjQAwgYkCgYEA0jY2 aovXwlue2oFBYo847kkEVdbQ7xwblRZH7xhINTpS9CtqBo87L+pW46+GjZ4X9560ZXUCTe/LCaIh Udib0GfQug2SBhRz1JPLlyoAnFxODLz6FVL88kRu2hFKbgifLy3j+ao6hnO2RlNYyIkFvYMRuHM/ qgeN9EJN50CdHDcCAwEAAaMTMBEwDwYDVR0TAQH/BAUwAwEB/zANBgkqhkiG9w0BAQQFAAOBgQAm SCwWwlj66BZ0DKqqX1Q/8tfJeGBeXm43YyJ3Nn6yF8Q0ufUIhfzJATj/Tb7yFkJD57taRvvBxhEf 8UqwKEbJw8RCfbz6q1lu1bdRiBHjpIUZa4JMpAwSremkrj/xw0llmozFyD4lt5SZu5IycQfwhl7t UCemDaYj+bvLpgcUQg== -----END CERTIFICATE----- Verisign Class 3 Public Primary Certification Authority ======================================================= -----BEGIN CERTIFICATE----- MIICPDCCAaUCEHC65B0Q2Sk0tjjKewPMur8wDQYJKoZIhvcNAQECBQAwXzELMAkGA1UEBhMCVVMx FzAVBgNVBAoTDlZlcmlTaWduLCBJbmMuMTcwNQYDVQQLEy5DbGFzcyAzIFB1YmxpYyBQcmltYXJ5 IENlcnRpZmljYXRpb24gQXV0aG9yaXR5MB4XDTk2MDEyOTAwMDAwMFoXDTI4MDgwMTIzNTk1OVow XzELMAkGA1UEBhMCVVMxFzAVBgNVBAoTDlZlcmlTaWduLCBJbmMuMTcwNQYDVQQLEy5DbGFzcyAz IFB1YmxpYyBQcmltYXJ5IENlcnRpZmljYXRpb24gQXV0aG9yaXR5MIGfMA0GCSqGSIb3DQEBAQUA A4GNADCBiQKBgQDJXFme8huKARS0EN8EQNvjV69qRUCPhAwL0TPZ2RHP7gJYHyX3KqhEBarsAx94 f56TuZoAqiN91qyFomNFx3InzPRMxnVx0jnvT0Lwdd8KkMaOIG+YD/isI19wKTakyYbnsZogy1Ol hec9vn2a/iRFM9x2Fe0PonFkTGUugWhFpwIDAQABMA0GCSqGSIb3DQEBAgUAA4GBALtMEivPLCYA TxQT3ab7/AoRhIzzKBxnki98tsX63/Dolbwdj2wsqFHMc9ikwFPwTtYmwHYBV4GSXiHx0bH/59Ah WM1pF+NEHJwZRDmJXNycAA9WjQKZ7aKQRUzkuxCkPfAyAw7xzvjoyVGM5mKf5p/AfbdynMk2Omuf Tqj/ZA1k -----END CERTIFICATE----- Verisign Class 3 Public Primary Certification Authority - G2 ============================================================ -----BEGIN CERTIFICATE----- MIIDAjCCAmsCEH3Z/gfPqB63EHln+6eJNMYwDQYJKoZIhvcNAQEFBQAwgcExCzAJBgNVBAYTAlVT MRcwFQYDVQQKEw5WZXJpU2lnbiwgSW5jLjE8MDoGA1UECxMzQ2xhc3MgMyBQdWJsaWMgUHJpbWFy eSBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eSAtIEcyMTowOAYDVQQLEzEoYykgMTk5OCBWZXJpU2ln biwgSW5jLiAtIEZvciBhdXRob3JpemVkIHVzZSBvbmx5MR8wHQYDVQQLExZWZXJpU2lnbiBUcnVz dCBOZXR3b3JrMB4XDTk4MDUxODAwMDAwMFoXDTI4MDgwMTIzNTk1OVowgcExCzAJBgNVBAYTAlVT MRcwFQYDVQQKEw5WZXJpU2lnbiwgSW5jLjE8MDoGA1UECxMzQ2xhc3MgMyBQdWJsaWMgUHJpbWFy eSBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eSAtIEcyMTowOAYDVQQLEzEoYykgMTk5OCBWZXJpU2ln biwgSW5jLiAtIEZvciBhdXRob3JpemVkIHVzZSBvbmx5MR8wHQYDVQQLExZWZXJpU2lnbiBUcnVz dCBOZXR3b3JrMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDMXtERXVxp0KvTuWpMmR9ZmDCO FoUgRm1HP9SFIIThbbP4pO0M8RcPO/mn+SXXwc+EY/J8Y8+iR/LGWzOOZEAEaMGAuWQcRXfH2G71 lSk8UOg013gfqLptQ5GVj0VXXn7F+8qkBOvqlzdUMG+7AUcyM83cV5tkaWH4mx0ciU9cZwIDAQAB MA0GCSqGSIb3DQEBBQUAA4GBAFFNzb5cy5gZnBWyATl4Lk0PZ3BwmcYQWpSkU01UbSuvDV1Ai2TT 1+7eVmGSX6bEHRBhNtMsJzzoKQm5EWR0zLVznxxIqbxhAe7iF6YM40AIOw7n60RzKprxaZLvcRTD Oaxxp5EJb+RxBrO6WVcmeQD2+A2iMzAo1KpYoJ2daZH9 -----END CERTIFICATE----- wp-cli/wp-cli 2.6.0 2022-01-25T23:55:14+00:00 composer/ca-bundle 1.3.1 4c679186f2aca4ab6a0f1b0b9cf9252decb44d0b composer/composer 2.2.5 22c41ef275c7bb64fa28fb2c0871a39666832cb9 composer/metadata-minifier 1.0.0 c549d23829536f0d0e984aaabbf02af91f443207 composer/pcre 1.0.1 67a32d7d6f9f560b726ab25a061b38ff3a80c560 composer/semver 3.2.7 deac27056b57e46faf136fae7b449eeaa71661ee composer/spdx-licenses 1.5.6 a30d487169d799745ca7280bc90fdfa693536901 composer/xdebug-handler 2.0.4 0c1a3925ec58a4ec98e992b9c7d171e9e184be0a gettext/gettext v4.8.6 bbeb8f4d3077663739aecb4551b22e720c0e9efe gettext/languages 2.9.0 ed56dd2c7f4024cc953ed180d25f02f2640e3ffa justinrainbow/json-schema 5.2.11 2ab6744b7296ded80f8cc4f9509abbff393399aa mck89/peast v1.13.11 78c57966f3da5f223636ea0417d71ac6ff61e47f mustache/mustache v2.14.1 579ffa5c96e1d292c060b3dd62811ff01ad8c24e nb/oxymel v0.1.0 cbe626ef55d5c4cc9b5e6e3904b395861ea76e3c psr/log 1.1.4 d49695b909c3b7628b6289db5479a1c204601f11 react/promise v2.8.0 f3cff96a19736714524ca0dd1d4130de73dbbbc4 rmccue/requests v1.8.1 82e6936366eac3af4d836c18b9d8c31028fe4cd5 seld/jsonlint 1.8.3 9ad6ce79c342fbd44df10ea95511a1b24dee5b57 seld/phar-utils 1.2.0 9f3452c93ff423469c0d56450431562ca423dcee symfony/console v3.4.47 a10b1da6fc93080c180bba7219b5ff5b7518fe81 symfony/debug v3.4.47 ab42889de57fdfcfcc0759ab102e2fd4ea72dcae symfony/filesystem v3.4.47 e58d7841cddfed6e846829040dca2cca0ebbbbb3 symfony/finder v3.4.47 b6b6ad3db3edb1b4b1c1896b1975fb684994de6e symfony/polyfill-ctype v1.19.0 aed596913b70fae57be53d86faa2e9ef85a2297b symfony/polyfill-mbstring v1.19.0 b5f7b932ee6fa802fc792eabd77c4c88084517ce symfony/process v3.4.47 b8648cf1d5af12a44a51d07ef9bf980921f15fca wp-cli/cache-command v2.0.9 05378440d8c6d4d2a1a5e5cbc1ba92a5e4bf1c40 wp-cli/checksum-command v2.1.2 ec59a24af2ca97b770a4709b0a1c241eeb4b4cff wp-cli/config-command v2.1.3 cdabbc47dae464a93b10361b9a18e84cf4e72fe2 wp-cli/core-command v2.1.1 a75d052ea000b4f0ec14106c110836b376e95a4f wp-cli/cron-command v2.1.0 bb9fd9645e9a5276d024a59affeda3e6aa8530be wp-cli/db-command v2.0.20 709f58c30d178afcdecaf56068ca9f5e985ed6b9 wp-cli/embed-command v2.0.11 00a901a66aecb4da94a8dace610eb1135fc82386 wp-cli/entity-command v2.2.1 d7d08b05c67651abde5d570851e46498a164cb34 wp-cli/eval-command v2.1.2 5213040ec2167b2748f2689ff6fe24b92a064a90 wp-cli/export-command v2.0.11 8dd137e0c739a59bb3d3de684a219dbb34473e11 wp-cli/extension-command v2.1.4 6401d7ea51084fac40010c2fb305be640675f385 wp-cli/i18n-command v2.2.13 77ece9e2c914bb56ea72b9ee9f00556dece07b3f wp-cli/import-command v2.0.8 a092e3abcca843f1fabf2e9b706a912ae075355f wp-cli/language-command v2.0.12 bbdba69179fc8df597928587111500c8ade40a38 wp-cli/maintenance-mode-command v2.0.8 e65505c973ea9349257a4f33ac9edc78db0b189a wp-cli/media-command v2.0.12 7cbf32c7fa68631619f85a9bea0fb80fca119ba6 wp-cli/mustangostang-spyc 0.6.3 6aa0b4da69ce9e9a2c8402dab8d43cf32c581cc7 wp-cli/package-command v2.2.2 36afdee21d022e6270867aa0cbfef6f453041814 wp-cli/php-cli-tools v0.11.13 a2866855ac1abc53005c102e901553ad5772dc04 wp-cli/rewrite-command v2.0.10 562a0a5a0d51be000de87d7a8a870de13383ecd6 wp-cli/role-command v2.0.9 9abd93952565935084160bc3be49dfb2483bb0b6 wp-cli/scaffold-command v2.0.16 6d92fb363b8ed7473af7f12cf342aaf9d2c96e81 wp-cli/search-replace-command v2.0.16 dbf21560fd91710b2900f5631448657d28f2b380 wp-cli/server-command v2.0.10 50c81f45f1cf09bc0a52e3582b3e56d27ca3c33c wp-cli/shell-command v2.0.11 28a7de3134c9f059900d8fa4aea1d7d618481454 wp-cli/super-admin-command v2.0.10 e6707f3acfff089d19c5c55bba0fd66cd7d6c2fa wp-cli/widget-command v2.1.7 6aedab77f1cd2a0f511b62110c244c32b84dd429 wp-cli/wp-cli v2.6.0 dee13c2baf6bf972484a63f8b8dab48f7220f095 wp-cli/wp-config-transformer v1.3.0 2e90eefc6b8f5166f53aa5414fd8f1a572164ef12.6.0H#eN'I76 GBMB