├── .gitignore
├── README.md
├── bin
├── cv
└── cv2
├── box.json
├── composer.json
├── composer.lock
├── doc
├── develop.md
├── download.md
└── plugins.md
├── lib
├── .gitrepo
├── README.md
├── composer.json
├── plugin
│ ├── alias-cmd.php
│ └── basic-alias.php
└── src
│ ├── BaseApplication.php
│ ├── Bootstrap.php
│ ├── BuildkitReader.php
│ ├── CmsBootstrap.php
│ ├── Command
│ └── CvCommand.php
│ ├── Config.php
│ ├── Cv.php
│ ├── CvDispatcher.php
│ ├── CvEvent.php
│ ├── CvPlugins.php
│ ├── ErrorHandler.php
│ ├── Log
│ ├── InternalLogger.php
│ ├── Logger.php
│ ├── StderrLogger.php
│ └── SymfonyConsoleLogger.php
│ ├── PharOut
│ ├── PharOut.php
│ └── PharPolicyTrait.php
│ ├── SiteConfigReader.php
│ ├── Top.php
│ └── Util
│ ├── AliasFilter.php
│ ├── BootTrait.php
│ ├── CvArgvInput.php
│ ├── Filesystem.php
│ ├── FilesystemTrait.php
│ ├── IOStack.php
│ ├── OptionalOption.php
│ └── SimulateWeb.php
├── nix
├── buildkit-update.sh
└── buildkit.nix
├── patches
├── psysh-0.11-php84.diff
├── psysh-0.11-php84.txt
├── scc-CompletionHandler.diff
└── scc-CompletionHandler.txt
├── phpunit.xml.dist
├── scoper.inc.php
├── scripts
├── build.sh
├── check-phar.php
├── releaser.php
└── run-tests.sh
├── shell.nix
├── src
├── Application.php
├── ClassAliases.php
├── Command
│ ├── AngularHtmlListCommand.php
│ ├── AngularHtmlShowCommand.php
│ ├── AngularModuleListCommand.php
│ ├── Api4Command.php
│ ├── ApiBatchCommand.php
│ ├── ApiCommand.php
│ ├── BootCommand.php
│ ├── CliCommand.php
│ ├── CoreCheckReqCommand.php
│ ├── CoreInstallCommand.php
│ ├── CoreUninstallCommand.php
│ ├── DebugContainerCommand.php
│ ├── DebugDispatcherCommand.php
│ ├── EditCommand.php
│ ├── EvalCommand.php
│ ├── ExtensionDisableCommand.php
│ ├── ExtensionDownloadCommand.php
│ ├── ExtensionEnableCommand.php
│ ├── ExtensionListCommand.php
│ ├── ExtensionUninstallCommand.php
│ ├── ExtensionUpgradeDbCommand.php
│ ├── FillCommand.php
│ ├── FlushCommand.php
│ ├── HttpCommand.php
│ ├── PathCommand.php
│ ├── PipeCommand.php
│ ├── QueueNextCommand.php
│ ├── ScriptCommand.php
│ ├── SettingGetCommand.php
│ ├── SettingRevertCommand.php
│ ├── SettingSetCommand.php
│ ├── ShowCommand.php
│ ├── SqlCliCommand.php
│ ├── StatusCommand.php
│ ├── UpgradeCommand.php
│ ├── UpgradeDbCommand.php
│ ├── UpgradeDlCommand.php
│ ├── UpgradeGetCommand.php
│ ├── UpgradeReportCommand.php
│ └── UrlCommand.php
├── Encoder.php
├── Exception
│ ├── ProcessErrorException.php
│ └── QueueTaskException.php
├── ExtensionPolyfill
│ ├── PfHelper.php
│ ├── PfQueueDownloader.php
│ └── PfQueueTasks.php
└── Util
│ ├── AbstractPlusParser.php
│ ├── Api4ArgEncoder.php
│ ├── Api4ArgParser.php
│ ├── ArrayUtil.php
│ ├── CliEditor.php
│ ├── ConsoleQueueRunner.php
│ ├── ConsoleSubprocessQueueRunner.php
│ ├── Cv.php
│ ├── Datasource.php
│ ├── DebugDispatcherTrait.php
│ ├── ExtensionTrait.php
│ ├── HeadlessDownloader.php
│ ├── OptionCallbackTrait.php
│ ├── Process.php
│ ├── PsrLogger.php
│ ├── Rand.php
│ ├── Relativizer.php
│ ├── SettingArgParser.php
│ ├── SettingCodec.php
│ ├── SettingTrait.php
│ ├── SetupCommandTrait.php
│ ├── StructuredOutputTrait.php
│ ├── UrlCommandTrait.php
│ └── VerboseApi.php
└── tests
├── CivilTestCase.php
├── Command
├── AngularHtmlListCommandTest.php
├── AngularHtmlShowCommandTest.php
├── AngularModuleListCommandTest.php
├── ApiBatchCommandTest.php
├── ApiCommandTest.php
├── BaseExtensionCommandTest.php
├── BootCommandTest.php
├── CoreLifecycleTest.php
├── DebugContainerCommandTest.php
├── DebugDispatcherCommandTest.php
├── EvalCommandTest.php
├── ExtensionBareDownloadTest.php
├── ExtensionLifecycleTest.php
├── ExtensionListCommandTest.php
├── FillCommandTest.php
├── FlushCommandTest.php
├── HttpCommandTest.php
├── PathCommandTest.php
├── ScriptCommandTest.php
├── SettingLifecycleTest.php
├── ShowCommandTest.php
├── SqlCliCommandTest.php
├── StatusCommandTest.php
├── UpgradeGetCommandTest.php
├── UpgradeReportCommandTest.php
├── UrlCommandTest.php
├── hello-args.php
└── hello-world.php
├── CvDispatcherTest.php
├── CvTestTrait.php
├── Plugin
├── AliasPluginTest.php
├── AliasPluginTest
│ └── dummy-alias.php
├── FluentHelloPluginTest.php
├── FluentHelloPluginTest
│ └── hello.php
├── HelloPluginTest.php
└── HelloPluginTest
│ └── hello.php
├── TopHelperTest.php
├── Util
├── AliasFilterTest.php
├── Api4ArgEncoderTest.php
├── Api4ArgParserTest.php
├── FilesystemTest.php
├── OptionalOptionTest.php
└── ProcessTest.php
├── bootstrap.php
└── fixtures
└── org.example.cvtest
├── LICENSE.txt
├── cvtest.php
├── info.xml
└── make.php
/.gitignore:
--------------------------------------------------------------------------------
1 | /vendor
2 | /.idea
3 | *~
4 | .composer-downloads
5 | /bin/box
6 | /bin/cv.phar
7 | /bin/phpunit
8 | /bin/php-parse
9 | /bin/psysh
10 | /bin/var-dump-server
11 | /extern/
12 | /lib/vendor
13 | /tmp-lib
14 | /build/
15 |
--------------------------------------------------------------------------------
/bin/cv:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env php
2 | setVerbosity(\Symfony\Component\Console\Output\OutputInterface::VERBOSITY_DEBUG);
26 | \Civi\Cv\CmsBootstrap::singleton()->addOptions([
27 | 'output' => $output,
28 | ]);
29 |
30 | $output->writeln("[cv2] Boot CMS");
31 | \Civi\Cv\CmsBootstrap::singleton()->bootCms();
32 |
33 | $output->writeln("[cv2] Boot CiviCRM");
34 | \Civi\Cv\CmsBootstrap::singleton()->bootCivi();
35 |
36 | $output->writeln("Hello from CiviCRM v" . CRM_Utils_System::version());
37 |
--------------------------------------------------------------------------------
/box.json:
--------------------------------------------------------------------------------
1 | {
2 | "chmod": "0755",
3 | "directories": [
4 | "lib/src",
5 | "lib/plugin",
6 | "src"
7 | ],
8 | "finder": [
9 | {
10 | "name": "*.php",
11 | "exclude": ["phpunit", "Tests", "Test", "tests", "test", "composer-patches"],
12 | "in": "vendor"
13 | }
14 | ],
15 | "compactors": ["KevinGH\\Box\\Compactor\\Php", "KevinGH\\Box\\Compactor\\PhpScoper"],
16 | "git-version": "package_version",
17 | "main": "bin/cv",
18 | "output": "bin/cv.phar",
19 | "stub": true
20 | }
21 |
--------------------------------------------------------------------------------
/composer.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "civicrm/cv",
3 | "description": "CLI tool for CiviCRM",
4 | "license": "MIT",
5 | "authors": [
6 | {
7 | "name": "Tim Otten",
8 | "email": "totten@civicrm.org"
9 | }
10 | ],
11 | "require": {
12 | "php": ">=7.3.0",
13 | "ext-json": "*",
14 | "cweagans/composer-patches": "~1.0",
15 | "lesser-evil/shell-verbosity-is-evil": "~1.0",
16 | "symfony/console": "~5.4",
17 | "symfony/process": "~5.4",
18 | "psr/log": "~1.1 || ~2.0 || ~3.0",
19 | "psy/psysh": "@stable",
20 | "stecman/symfony-console-completion": "^0.11.0"
21 | },
22 | "autoload": {
23 | "psr-4": {
24 | "Civi\\Cv\\": ["lib/src/", "src/"]
25 | }
26 | },
27 | "replace": {
28 | "civicrm/cv-lib": "self.version"
29 | },
30 | "bin": [
31 | "bin/cv"
32 | ],
33 | "config": {
34 | "platform": {
35 | "php": "7.3.0"
36 | },
37 | "bin-dir": "bin",
38 | "allow-plugins": {
39 | "civicrm/composer-downloads-plugin": true,
40 | "cweagans/composer-patches": true
41 | }
42 | },
43 | "extra": {
44 | "patches": {
45 | "stecman/symfony-console-completion": {
46 | "Fix warnings on PHP 8.4": "patches/scc-CompletionHandler.diff"
47 | },
48 | "psy/psysh": {
49 | "Fix warnings on PHP 8.4": "patches/psysh-0.11-php84.diff"
50 | }
51 | }
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/lib/.gitrepo:
--------------------------------------------------------------------------------
1 | ; DO NOT EDIT (unless you know what you are doing)
2 | ;
3 | ; This subdirectory is a git "subrepo", and this file is maintained by the
4 | ; git-subrepo command. See https://github.com/git-commands/git-subrepo#readme
5 | ;
6 | [subrepo]
7 | remote = git@github.com:civicrm/cv-lib.git
8 | branch = master
9 | commit = 34cd212d371d541c53f180e7382880dcaeb01d37
10 | method = merge
11 | cmdver = 0.4.1
12 | parent = 15c6b262bb1bf4b160aa2753f1dadae500c67247
13 |
--------------------------------------------------------------------------------
/lib/README.md:
--------------------------------------------------------------------------------
1 | # cv-lib
2 |
3 | `cv-lib` is a subpackage provided by `cv`. It defines the essential core of `cv` -- locating and booting CiviCRM.
4 |
5 | The canonical home for developing this code is in [civicrm/cv](https://github.com/civicrm/cv). It will be periodically published to the read-only
6 | mirror [civicrm/cv-lib](https://github.com/civicrm/cv-lib) to facilitate usage by other projects.
7 |
8 | ## Installation
9 |
10 | ```bash
11 | composer require civicrm/cv-lib
12 | ```
13 |
14 | ## Primary API
15 |
16 | The library provides a handful of supported classes:
17 |
18 | * `Civi\Cv\CmsBootstrap` supports the standard boot protocol. In this protocol, we search for a recognized UF/CMS, start
19 | that, and then start CiviCRM. The advantage of this protocol is that it is more representative of a typical
20 | HTTP-request. (Events and add-ons supported by UF/CMS and CRM will tend to work more normally.)
21 |
22 | Basic usage:
23 |
24 | ```php
25 | Civi\Cv\CmsBootstrap::singleton()->bootCms()->bootCivi();
26 | ```
27 |
28 | Or you can pass in options:
29 |
30 | ```php
31 | $options = [...];
32 | Civi\Cv\CmsBootstrap::singleton()
33 | ->addOptions($options)
34 | ->bootCms()
35 | ->bootCivi();
36 | ```
37 |
38 | End-users may fine-tune the behavior by setting `CIVICRM_BOOT` (as documented in `cv`).
39 |
40 | * `Civi\Cv\Bootstrap` supports the legacy boot protocol. In this protocol, we search for `civicrm.settings.php` and
41 | start CiviCRM. Finally, we use `civicrm-core` API's to start the associated UF/CMS.
42 |
43 | Basic usage:
44 |
45 | ```php
46 | $options = [...];
47 | \Civi\Cv\Bootstrap::singleton()->boot($options);
48 | \CRM_Core_Config::singleton();
49 | \CRM_Utils_System::loadBootStrap([], FALSE);
50 | ```
51 |
52 | End-users may fine-tune the behavior by setting `CIVICRM_SETTING` (as documented in `cv`).
53 |
54 | Both bootstrap mechanisms accept an optional set of hints and overrides.
55 |
56 | For example, by default, `cv-lib` will print errors to STDERR, but you can override the
57 | handling of messages:
58 |
59 | ```php
60 | // Disable all output
61 | $options['log'] = new \Psr\Log\NullLogger();
62 |
63 | // Enable verbose logging to STDOUT/STDERR
64 | $options['log'] = new \Civi\Cv\Log\StderrLogger('Bootstrap', TRUE);
65 |
66 | // Use bridge between psr/log and symfony/console
67 | $options['log'] = new \Symfony\Component\Console\Logger\ConsoleLogger($output);
68 |
69 | // Use the console logger from cv cli. (Requires symfony/console. Looks a bit prettier.)
70 | public function execute(InputInterface $input, OutputInterface $output) {
71 | ...
72 | $options['output'] = $output;
73 | ...
74 | }
75 | ```
76 |
77 | For more info about `$options`, see the docblocks.
78 |
79 | ## Experimental API
80 |
81 | Other classes are included, but their contracts are subject to change. These
82 | include higher-level helpers for building Symfony Console apps that incorporate
83 | Civi bootstrap behaviors.
84 |
85 | * `BootTrait` has previously suggested as an experimentally available API
86 | (circa v0.3.44). It changed significantly (circa v0.3.56), where
87 | `configureBootOptions()` was replaced by `$bootOptions`, `mergeDefaultBootDefinition()`,
88 | and `mergeBootDefinition()`.
89 | * As an alternative, consider the classes `BaseApplication` and `CvCommand` if you aim
90 | to build a tool using Symfony Console and Cv Lib.
91 |
--------------------------------------------------------------------------------
/lib/composer.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "civicrm/cv-lib",
3 | "description": "Bootstrap library for CiviCRM CLI tools",
4 | "license": "MIT",
5 | "authors": [
6 | {
7 | "name": "Tim Otten",
8 | "email": "totten@civicrm.org"
9 | }
10 | ],
11 | "require": {
12 | "php": ">=7.3.0",
13 | "ext-json": "*"
14 | },
15 | "autoload": {
16 | "psr-4": {
17 | "Civi\\Cv\\": ["src/"]
18 | }
19 | },
20 | "config": {
21 | "platform": {
22 | "php": "7.3.0"
23 | }
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/lib/src/BuildkitReader.php:
--------------------------------------------------------------------------------
1 | toAbsolutePath($settingsFile);
17 | $parts = explode('/', str_replace('\\', '/', $settingsFile));
18 | while (!empty($parts)) {
19 | $last = array_pop($parts);
20 | $basePath = implode('/', $parts);
21 | $shFile = "$basePath/$last.sh";
22 | if (is_dir("$basePath/$last") && file_exists($shFile)) {
23 | // Does it look vaguely like a buildkit config file?
24 | if (preg_match('/ADMIN_USER=/', file_get_contents($shFile))) {
25 | return $shFile;
26 | }
27 | }
28 | }
29 | return NULL;
30 | }
31 |
32 | /**
33 | * Parse the key-value paris in buildkit .sh config file.
34 | *
35 | * @param string $shFile
36 | * @return array
37 | */
38 | public static function readShFile($shFile) {
39 | $lines = explode("\n", file_get_contents($shFile));
40 | $result = array();
41 | foreach ($lines as $line) {
42 | if (empty($line) || $line[0] == '#') {
43 | continue;
44 | }
45 | if (preg_match('/^([A-Z0-9_]+)=\"(.*)\"$/', $line, $matches)) {
46 | $result[$matches[1]] = stripcslashes($matches[2]);
47 | }
48 | else {
49 | throw new \RuntimeException("Malformed line [$line]");
50 | }
51 | }
52 | return $result;
53 | }
54 |
55 | }
56 |
--------------------------------------------------------------------------------
/lib/src/Command/CvCommand.php:
--------------------------------------------------------------------------------
1 | mergeBootDefinition($this->getDefinition());
26 | }
27 |
28 | /**
29 | * @param \Symfony\Component\Console\Input\InputInterface $input
30 | * @param \Symfony\Component\Console\Output\OutputInterface $output
31 | */
32 | protected function initialize(InputInterface $input, OutputInterface $output) {
33 | $this->autoboot($input, $output);
34 | parent::initialize($input, $output);
35 | $this->runOptionCallbacks($input, $output);
36 | }
37 |
38 | }
39 |
--------------------------------------------------------------------------------
/lib/src/Config.php:
--------------------------------------------------------------------------------
1 | update(self::getFileName(), function ($rawIn) use ($filter) {
35 | $data = empty($rawIn) ? array() : json_decode($rawIn, TRUE);
36 | $data = call_user_func($filter, $data);
37 | return Encoder::encode($data, 'json-pretty');
38 | });
39 | }
40 |
41 | /**
42 | * @return string
43 | */
44 | public static function getFileName() {
45 | if (getenv('CV_CONFIG')) {
46 | // The user has specifically told us where to go.
47 | return getenv('CV_CONFIG');
48 | }
49 |
50 | // We have to figure out where to go. There are a couple plausible locations...
51 | $candidates = [];
52 | if (getenv('XDG_CONFIG_HOME')) {
53 | $candidates[] = getenv('XDG_CONFIG_HOME') . '/.cv.json';
54 | }
55 | if (getenv('HOME')) {
56 | $candidates[] = getenv('HOME') . '/.cv.json';
57 | }
58 |
59 | // Prefer the first extant config file...
60 | foreach ($candidates as $candidate) {
61 | if (file_exists($candidate)) {
62 | return $candidate;
63 | }
64 | }
65 |
66 | // Or if there is no extant file, then use the first plausible suggestion...
67 | if (isset($candidates[0])) {
68 | return $candidates[0];
69 | }
70 |
71 | throw new \RuntimeException("Failed to determine file path for 'cv.json'.");
72 |
73 | }
74 |
75 | }
76 |
--------------------------------------------------------------------------------
/lib/src/CvDispatcher.php:
--------------------------------------------------------------------------------
1 | listeners[$name])) {
23 | $activeListeners = array_merge($activeListeners, $this->listeners[$name]);
24 | }
25 | }
26 |
27 | usort($activeListeners, function ($a, $b) {
28 | if ($a['priority'] !== $b['priority']) {
29 | return $a['priority'] - $b['priority'];
30 | }
31 | else {
32 | return $a['natPriority'] - $b['natPriority'];
33 | }
34 | });
35 | foreach ($activeListeners as $listener) {
36 | call_user_func($listener['callback'], $event);
37 | }
38 |
39 | return $event;
40 | }
41 |
42 | public function addListener(string $eventName, $callback, int $priority = 0): void {
43 | static $natPriority = 0;
44 | $natPriority++;
45 | $id = $this->getCallbackId($callback);
46 | $this->listeners[$eventName][$id] = ['callback' => $callback, 'priority' => $priority, 'natPriority' => $natPriority];
47 | }
48 |
49 | public function removeListener(string $eventName, $callback) {
50 | $id = $this->getCallbackId($callback);
51 | unset($this->listeners[$eventName][$id]);
52 | }
53 |
54 | /**
55 | * @param $callback
56 | * @return string
57 | */
58 | protected function getCallbackId($callback): string {
59 | if (is_string($callback)) {
60 | return $callback;
61 | }
62 | elseif (is_array($callback)) {
63 | return implode('::', $callback);
64 | }
65 | else {
66 | return spl_object_hash($callback);
67 | }
68 | }
69 |
70 | }
71 |
--------------------------------------------------------------------------------
/lib/src/CvEvent.php:
--------------------------------------------------------------------------------
1 | arguments = $arguments;
24 | }
25 |
26 | /**
27 | * Get argument by key.
28 | *
29 | * @param string $key Key
30 | * @return mixed Contents of array key
31 | *
32 | * @throws \InvalidArgumentException if key is not found
33 | */
34 | public function getArgument($key) {
35 | if ($this->hasArgument($key)) {
36 | return $this->arguments[$key];
37 | }
38 |
39 | throw new \InvalidArgumentException(sprintf('Argument "%s" not found.', $key));
40 | }
41 |
42 | /**
43 | * Add argument to event.
44 | *
45 | * @param string $key Argument name
46 | * @param mixed $value Value
47 | *
48 | * @return $this
49 | */
50 | public function setArgument($key, $value) {
51 | $this->arguments[$key] = $value;
52 |
53 | return $this;
54 | }
55 |
56 | /**
57 | * Getter for all arguments.
58 | *
59 | * @return array
60 | */
61 | public function getArguments() {
62 | return $this->arguments;
63 | }
64 |
65 | /**
66 | * Set args property.
67 | *
68 | * @param array $args Arguments
69 | *
70 | * @return $this
71 | */
72 | public function setArguments(array $args = []) {
73 | $this->arguments = $args;
74 |
75 | return $this;
76 | }
77 |
78 | /**
79 | * Has argument.
80 | *
81 | * @param string $key Key of arguments array
82 | *
83 | * @return bool
84 | */
85 | public function hasArgument($key) {
86 | return \array_key_exists($key, $this->arguments);
87 | }
88 |
89 | /**
90 | * IteratorAggregate for iterating over the object like an array.
91 | *
92 | * @return \ArrayIterator
93 | */
94 | public function getIterator(): \Traversable {
95 | return new \ArrayIterator($this->arguments);
96 | }
97 |
98 | /**
99 | * ArrayAccess for argument getter.
100 | *
101 | * @param string $key Array key
102 | * @return mixed
103 | * @throws \InvalidArgumentException if key does not exist in $this->args
104 | */
105 | #[\ReturnTypeWillChange]
106 | public function &offsetGet($offset) {
107 | return $this->arguments[$offset];
108 | }
109 |
110 | /**
111 | * ArrayAccess for argument setter.
112 | *
113 | * @param string $offset Array key to set
114 | * @param mixed $value Value
115 | */
116 | public function offsetSet($offset, $value): void {
117 | $this->setArgument($offset, $value);
118 | }
119 |
120 | /**
121 | * ArrayAccess for unset argument.
122 | *
123 | * @param string $offset Array key
124 | */
125 | public function offsetUnset($offset): void {
126 | if ($this->hasArgument($offset)) {
127 | unset($this->arguments[$offset]);
128 | }
129 | }
130 |
131 | /**
132 | * ArrayAccess has argument.
133 | *
134 | * @param string $offset Array key
135 | * @return bool
136 | */
137 | public function offsetExists($offset): bool {
138 | return $this->hasArgument($offset);
139 | }
140 |
141 | public function isPropagationStopped(): bool {
142 | return $this->propagationStopped;
143 | }
144 |
145 | public function setPropagationStopped(bool $propagationStopped): void {
146 | $this->propagationStopped = $propagationStopped;
147 | }
148 |
149 | }
150 |
--------------------------------------------------------------------------------
/lib/src/CvPlugins.php:
--------------------------------------------------------------------------------
1 | 'cv', 'appVersion' => '0.3.50']
23 | */
24 | public function init(array $pluginEnv) {
25 | if (getenv('CV_PLUGIN_PATH')) {
26 | $this->paths = explode(PATH_SEPARATOR, getenv('CV_PLUGIN_PATH'));
27 | }
28 | else {
29 | $this->paths = ['/etc/cv/plugin', '/usr/local/share/cv/plugin', '/usr/share/cv/plugin'];
30 | if (getenv('HOME')) {
31 | array_unshift($this->paths, getenv('HOME') . '/.cv/plugin');
32 | }
33 | elseif (getenv('USERPROFILE')) {
34 | array_unshift($this->paths, getenv('USERPROFILE') . '/.cv/plugin');
35 | }
36 | if (getenv('XDG_CONFIG_HOME')) {
37 | array_unshift($this->paths, getenv('XDG_CONFIG_HOME') . '/cv/plugin');
38 | }
39 | }
40 |
41 | // Always load internal plugins
42 | $this->paths['builtin'] = dirname(__DIR__) . '/plugin';
43 |
44 | $this->plugins = [];
45 | foreach ($this->paths as $path) {
46 | if (file_exists($path) && is_dir($path)) {
47 | foreach ($this->findFiles($path, '/\.php$/') as $file) {
48 | $pluginName = preg_replace(';(\d+-)?(.*)(@\w+)?\.php;', '\\2', basename($file));
49 | if ($pluginName === basename($file)) {
50 | throw new \RuntimeException("Malformed plugin name: $file");
51 | }
52 | if (!isset($this->plugins[$pluginName])) {
53 | $this->plugins[$pluginName] = $file;
54 | }
55 | else {
56 | fprintf(STDERR, "WARNING: Plugin %s has multiple definitions (%s, %s)\n", $pluginName, $file, $this->plugins[$pluginName]);
57 | }
58 | }
59 | }
60 | }
61 |
62 | ksort($this->plugins);
63 | foreach ($this->plugins as $pluginName => $pluginFile) {
64 | // FIXME: Refactor so that you can add more plugins post-boot `load("/some/glob*.php")`
65 | $this->load($pluginEnv + [
66 | 'protocol' => 1,
67 | 'name' => $pluginName,
68 | 'file' => $pluginFile,
69 | ]);
70 | }
71 | }
72 |
73 | /**
74 | * @param array $CV_PLUGIN
75 | * Description of the plugin being loaded.
76 | * Keys:
77 | * - version: Protocol version (ex: "1")
78 | * - name: Basenemae of the plugin (eg `hello.php`)
79 | * - file: Logic filename (eg `/etc/cv/plugin/hello.php`)
80 | * @return void
81 | */
82 | protected function load(array $CV_PLUGIN) {
83 | include $CV_PLUGIN['file'];
84 | }
85 |
86 | /**
87 | * @return string[]
88 | */
89 | public function getPaths(): array {
90 | return $this->paths;
91 | }
92 |
93 | /**
94 | * @return array
95 | */
96 | public function getPlugins(): array {
97 | return $this->plugins;
98 | }
99 |
100 | private function findFiles(string $path, string $regex): array {
101 | // NOTE: scandir() works better than glob() in PHAR context.
102 | $files = preg_grep($regex, scandir($path));
103 | return array_map(function ($f) use ($path) {
104 | return "$path/$f";
105 | }, $files);
106 | }
107 |
108 | }
109 |
--------------------------------------------------------------------------------
/lib/src/ErrorHandler.php:
--------------------------------------------------------------------------------
1 | 0) {
34 | $error = error_get_last();
35 | if (isset($error['type']) && ($error['type'] & (E_ERROR | E_PARSE | E_CORE_ERROR | E_COMPILE_ERROR | E_USER_ERROR | E_RECOVERABLE_ERROR))) {
36 | // Something - like a bad eval() - interrupted normal execution.
37 | // Make sure the status code reflects that.
38 | exit(255);
39 | }
40 | }
41 | }
42 |
43 | public static function onError($errorLevel, $message, $filename, $line) {
44 | if ($errorLevel & error_reporting()) {
45 | $errorType = static::getErrorTypes()[$errorLevel] ?: "Unknown[$errorLevel]";
46 | fprintf(STDERR, "[%s] %s at %s:%d\n", $errorType, $message, $filename, $line);
47 | return TRUE;
48 | }
49 | else {
50 | return FALSE;
51 | }
52 | }
53 |
54 | /**
55 | * @param \Throwable $exception
56 | */
57 | public static function onException($exception) {
58 | if (isset(static::$renderer)) {
59 | call_user_func(static::$renderer, $exception);
60 | }
61 | else {
62 | fprintf(STDERR, "Exception: %s (%s)\n%s", $exception->getMessage(), get_class($exception), $exception->getTraceAsString());
63 | }
64 | exit(255);
65 | }
66 |
67 | /**
68 | * @param callable|null $renderer
69 | */
70 | public static function setRenderer(?callable $renderer): void {
71 | self::$renderer = $renderer;
72 | }
73 |
74 | protected static function getErrorTypes(): array {
75 | return array_merge(
76 | [
77 | E_ERROR => 'PHP Error',
78 | E_WARNING => 'PHP Warning',
79 | E_PARSE => 'PHP Parse Error',
80 | E_NOTICE => 'PHP Notice',
81 | E_CORE_ERROR => 'PHP Core Error',
82 | E_CORE_WARNING => 'PHP Core Warning',
83 | E_COMPILE_ERROR => 'PHP Compile Error',
84 | E_COMPILE_WARNING => 'PHP Compile Warning',
85 | E_USER_ERROR => 'PHP User Error',
86 | E_USER_WARNING => 'PHP User Warning',
87 | E_USER_NOTICE => 'PHP User Notice',
88 | E_RECOVERABLE_ERROR => 'PHP Recoverable Fatal Error',
89 | E_DEPRECATED => 'PHP Deprecation',
90 | E_USER_DEPRECATED => 'PHP User Deprecation',
91 | ],
92 | version_compare(phpversion(), '8.4', '<') ? [constant('E_STRICT') => 'PHP Strict Warning'] : []
93 | // https://wiki.php.net/rfc/deprecations_php_8_4#remove_e_strict_error_level_and_deprecate_e_strict_constant
94 | // In theory, once cv shifts to 8.x only, we can simplify this.
95 | );
96 | }
97 |
98 | }
99 |
--------------------------------------------------------------------------------
/lib/src/Log/Logger.php:
--------------------------------------------------------------------------------
1 | verbose = $verbose;
17 | }
18 |
19 | public function log($level, $message, array $context = []) {
20 | if ($this->isAnomolous($level) || $this->verbose) {
21 | $template = "[%s:%s] %s\n";
22 | fprintf(STDERR, $template, $this->topic, $level, $this->interpolate($message, $context));
23 | }
24 | }
25 |
26 | }
27 |
--------------------------------------------------------------------------------
/lib/src/Log/SymfonyConsoleLogger.php:
--------------------------------------------------------------------------------
1 | output = $output;
27 | $this->verbosityLevelMap = [
28 | 'emergency' => OutputInterface::VERBOSITY_NORMAL,
29 | 'alert' => OutputInterface::VERBOSITY_NORMAL,
30 | 'critical' => OutputInterface::VERBOSITY_NORMAL,
31 | 'error' => OutputInterface::VERBOSITY_NORMAL,
32 | 'warning' => OutputInterface::VERBOSITY_NORMAL,
33 | 'notice' => OutputInterface::VERBOSITY_VERBOSE,
34 | 'info' => OutputInterface::VERBOSITY_VERY_VERBOSE,
35 | 'debug' => OutputInterface::VERBOSITY_DEBUG,
36 | ];
37 | }
38 |
39 | public function log($level, $message, array $context = []) {
40 | $output = $this->output;
41 |
42 | if ($output instanceof ConsoleOutputInterface) {
43 | $output = $output->getErrorOutput();
44 | }
45 |
46 | if ($this->isAnomolous($level)) {
47 | $template = '[%s:%s] %s';
48 | }
49 | else {
50 | $template = '[%s:%s] %s';
51 | }
52 |
53 | if ($output->getVerbosity() >= $this->verbosityLevelMap[$level]) {
54 | $output->writeln(sprintf($template, $this->topic, $level, $this->interpolate($message, $context)),
55 | $this->verbosityLevelMap[$level]);
56 | }
57 | }
58 |
59 | protected function decorateInterpolatedValue(string $value): string {
60 | return "$value";
61 | }
62 |
63 | }
64 |
--------------------------------------------------------------------------------
/lib/src/PharOut/PharOut.php:
--------------------------------------------------------------------------------
1 | withAssertion(new class() implements \TYPO3\PharStreamWrapper\Assertable {
55 | use PharPolicyTrait;
56 | })
57 | );
58 | if (in_array('phar', stream_get_wrappers())) {
59 | stream_wrapper_unregister('phar');
60 | stream_wrapper_register('phar', \TYPO3\PharStreamWrapper\PharStreamWrapper::class);
61 | }
62 | }
63 |
64 | }
65 |
--------------------------------------------------------------------------------
/lib/src/PharOut/PharPolicyTrait.php:
--------------------------------------------------------------------------------
1 | mainFile === NULL) {
33 | $this->mainFile = $this->findMainFile();
34 | }
35 |
36 | $baseFile = Helper::determineBaseFile($path);
37 | if ($baseFile === NULL) {
38 | throw new Exception(sprintf('Failed to identify origin of "%s"', $path), 1535198703);
39 | }
40 |
41 | if (($baseFile === $this->mainFile) || (strtolower(pathinfo($baseFile, PATHINFO_EXTENSION)) === 'phar')) {
42 | return TRUE;
43 | }
44 |
45 | throw new Exception(sprintf('File "%s" does not resolve to approved PHAR location.', $path), 1535198703);
46 | }
47 |
48 | /**
49 | * Determine the 'main' script that started this process.
50 | *
51 | * @return string|null
52 | */
53 | private function findMainFile() {
54 | if (!in_array(PHP_SAPI, ['cli', 'phpdbg'])) {
55 | return NULL;
56 | }
57 |
58 | $backtrace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS);
59 | do {
60 | $caller = array_pop($backtrace);
61 | } while (empty($caller['file']) && !empty($backtrace));
62 | return isset($caller['file']) ? Helper::determineBaseFile($caller['file']) : NULL;
63 | }
64 |
65 | }
66 |
--------------------------------------------------------------------------------
/lib/src/Top.php:
--------------------------------------------------------------------------------
1 | getFirstArgument();
23 | if ($firstArg[0] === '@') {
24 | return static::replace($argv, $firstArg, '--site-alias=' . substr($firstArg, 1));
25 | }
26 |
27 | return $argv;
28 | }
29 |
30 | private static function replace(array $original, $old, $new) {
31 | $pos = array_search($old, $original, TRUE);
32 | $original[$pos] = $new;
33 | return $original;
34 | }
35 |
36 | }
37 |
--------------------------------------------------------------------------------
/lib/src/Util/CvArgvInput.php:
--------------------------------------------------------------------------------
1 | originalArgv = $argv;
18 | parent::__construct($argv, $definition);
19 | }
20 |
21 | public function getOriginalArgv(): array {
22 | return $this->originalArgv;
23 | }
24 |
25 | }
26 |
--------------------------------------------------------------------------------
/lib/src/Util/FilesystemTrait.php:
--------------------------------------------------------------------------------
1 |
9 | * @license MIT
10 | */
11 | trait FilesystemTrait {
12 |
13 | /**
14 | * @var string|null
15 | */
16 | private static $lastError;
17 |
18 | /**
19 | * Returns whether the file path is an absolute path.
20 | *
21 | * @param string $file A file path
22 | * @return bool
23 | */
24 | public function isAbsolutePath($file) {
25 | return '' !== (string) $file && (strspn($file, '/\\', 0, 1)
26 | || (\strlen($file) > 3 && ctype_alpha($file[0])
27 | && ':' === $file[1]
28 | && strspn($file, '/\\', 2, 1)
29 | )
30 | || NULL !== parse_url($file, \PHP_URL_SCHEME)
31 | );
32 | }
33 |
34 | /**
35 | * Removes files or directories.
36 | *
37 | * @param string|iterable $files A filename, an array of files, or a \Traversable instance to remove
38 | *
39 | * @throws \RuntimeException When removal fails
40 | */
41 | public function remove($files) {
42 | if ($files instanceof \Traversable) {
43 | $files = iterator_to_array($files, FALSE);
44 | }
45 | elseif (!\is_array($files)) {
46 | $files = [$files];
47 | }
48 | $files = array_reverse($files);
49 | foreach ($files as $file) {
50 | if (is_link($file)) {
51 | // See https://bugs.php.net/52176
52 | if (!(self::box('unlink', $file) || '\\' !== \DIRECTORY_SEPARATOR || self::box('rmdir', $file)) && file_exists($file)) {
53 | throw new \RuntimeException(sprintf('Failed to remove symlink "%s": ', $file) . self::$lastError);
54 | }
55 | }
56 | elseif (is_dir($file)) {
57 | $this->remove(new \FilesystemIterator($file, \FilesystemIterator::CURRENT_AS_PATHNAME | \FilesystemIterator::SKIP_DOTS));
58 |
59 | if (!self::box('rmdir', $file) && file_exists($file)) {
60 | throw new \RuntimeException(sprintf('Failed to remove directory "%s": ', $file) . self::$lastError);
61 | }
62 | }
63 | elseif (!self::box('unlink', $file) && (str_contains(self::$lastError, 'Permission denied') || file_exists($file))) {
64 | throw new \RuntimeException(sprintf('Failed to remove file "%s": ', $file) . self::$lastError);
65 | }
66 | }
67 | }
68 |
69 | /**
70 | * @param callable $func
71 | * File I/O function from PHP stdlib
72 | * @param mixed ...$args
73 | * @return mixed
74 | */
75 | private static function box(callable $func, ...$args) {
76 | self::$lastError = NULL;
77 | set_error_handler(__CLASS__ . '::handleError');
78 | try {
79 | $result = $func(...$args);
80 | restore_error_handler();
81 |
82 | return $result;
83 | }
84 | catch (\Throwable $e) {
85 | }
86 | restore_error_handler();
87 |
88 | throw $e;
89 | }
90 |
91 | /**
92 | * @internal
93 | */
94 | public static function handleError(int $type, string $msg) {
95 | self::$lastError = $msg;
96 | }
97 |
98 | }
99 |
--------------------------------------------------------------------------------
/lib/src/Util/IOStack.php:
--------------------------------------------------------------------------------
1 | stack[0]['app'] ?? NULL);
31 | array_unshift($this->stack, [
32 | 'id' => static::$id,
33 | 'input' => $input,
34 | 'output' => $output,
35 | 'io' => new SymfonyStyle($input, $output),
36 | 'app' => $app,
37 | ]);
38 | return static::$id;
39 | }
40 |
41 | public function pop(): array {
42 | return array_shift($this->stack);
43 | }
44 |
45 | /**
46 | * Get a current property of the current (top) stack-frame.
47 | *
48 | * @param string $property
49 | * One of: 'input', 'output', 'io', 'id'
50 | * @return mixed
51 | */
52 | public function current(string $property) {
53 | return $this->stack[0][$property];
54 | }
55 |
56 | /**
57 | * Lookup a property from a particular stack-frame.
58 | *
59 | * @param scalar $id
60 | * Internal identifier for the stack-frame.
61 | * @param string $property
62 | * One of: 'input', 'output', 'io', 'id'
63 | * @return mixed|null
64 | */
65 | public function get($id, string $property) {
66 | foreach ($this->stack as $item) {
67 | if ($item['id'] === $id) {
68 | return $item[$property];
69 | }
70 | }
71 | return NULL;
72 | }
73 |
74 | public function replace($property, $value) {
75 | $this->stack[0][$property] = $value;
76 | }
77 |
78 | public function reset() {
79 | $this->stack = [];
80 | }
81 |
82 | }
83 |
--------------------------------------------------------------------------------
/lib/src/Util/OptionalOption.php:
--------------------------------------------------------------------------------
1 | Means "--refresh=auto"; see $omittedDefault
14 | * cv en -r ==> Means "--refresh=yes"; see $activeDefault
15 | * cv en -r=yes ==> Means "--refresh=yes"
16 | * cv en -r=no ==> Means "--refresh=no"
17 | *
18 | * @param \CvDeps\Symfony\Component\Console\Input\InputInterface|\Symfony\Component\Console\Input\InputInterface $input
19 | * @param array $rawNames
20 | * Ex: array('-r', '--refresh').
21 | * @param string $omittedDefault
22 | * Value to use if option is completely omitted.
23 | * @param string $activeDefault
24 | * Value to use if option is activated without data.
25 | * @return string
26 | */
27 | public static function parse($input, $rawNames, $omittedDefault, $activeDefault) {
28 | $value = NULL;
29 | foreach ($rawNames as $rawName) {
30 | if ($input->hasParameterOption($rawName)) {
31 | if (NULL === $input->getParameterOption($rawName)) {
32 | return $activeDefault;
33 | }
34 | else {
35 | return $input->getParameterOption($rawName);
36 | }
37 | }
38 | }
39 | return $omittedDefault;
40 | }
41 |
42 | }
43 |
--------------------------------------------------------------------------------
/lib/src/Util/SimulateWeb.php:
--------------------------------------------------------------------------------
1 | $value) {
20 | $_SERVER[$key] = $value;
21 | }
22 | }
23 |
24 | if (ord($_SERVER['SCRIPT_NAME']) != 47) {
25 | $_SERVER['SCRIPT_NAME'] = '/' . $_SERVER['SCRIPT_NAME'];
26 | }
27 | }
28 |
29 | public static function convertUrlToCgiVars(?string $url): array {
30 | if (strpos($url, '://') === FALSE) {
31 | throw new \LogicException("convertUrlToCgiVars() expects a URL");
32 | }
33 |
34 | $parts = parse_url($url);
35 | $result = [];
36 | $result['SERVER_NAME'] = $parts['host'];
37 | if (!empty($parts['port'])) {
38 | $result['HTTP_HOST'] = $parts['host'] . ':' . $parts['port'];
39 | $result['SERVER_PORT'] = $parts['port'];
40 | }
41 | else {
42 | $result['HTTP_HOST'] = $parts['host'];
43 | $result['SERVER_PORT'] = $parts['scheme'] === 'http' ? 80 : 443;
44 | }
45 | if ($parts['scheme'] === 'https') {
46 | $result['HTTPS'] = 'on';
47 | }
48 | return $result;
49 | }
50 |
51 | public static function detectEnvUrl(): ?string {
52 | if ($host = static::detectEnvHost()) {
53 | return static::detectEnvScheme() . '://' . $host;
54 | }
55 | return NULL;
56 | }
57 |
58 | /**
59 | * If the user has environment-variables like HTTP_HOST, take that as a sign of
60 | * the intended host.
61 | *
62 | * @return string|null
63 | */
64 | public static function detectEnvHost(): ?string {
65 | if (array_key_exists('HTTP_HOST', $_SERVER) && strpos($_SERVER['HTTP_HOST'], '//') === FALSE) {
66 | $url = $_SERVER['HTTP_HOST'];
67 | if (array_key_exists('HTTP_PORT', $_SERVER)) {
68 | $url .= $_SERVER['HTTP_PORT'];
69 | }
70 | return $url;
71 | }
72 | return NULL;
73 | }
74 |
75 | public static function detectEnvScheme(): ?string {
76 | return (($_SERVER['SERVER_PORT'] ?? NULL) === 443 || ($_SERVER['HTTPS'] ?? NULL) === 'on') ? 'https' : 'http';
77 | }
78 |
79 | public static function prependDefaultScheme(?string $url): string {
80 | if ($url === NULL || $url === '') {
81 | return $url;
82 | }
83 | elseif (strpos($url, '://') !== FALSE) {
84 | return $url;
85 | }
86 | else {
87 | return static::detectEnvScheme() . '://' . $url;
88 | }
89 | }
90 |
91 | public static function localhost(): string {
92 | return static::prependDefaultScheme('localhost');
93 | }
94 |
95 | }
96 |
--------------------------------------------------------------------------------
/nix/buildkit-update.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 | { # https://stackoverflow.com/a/21100710
3 |
4 | ## Re-generate the buildkit.nix file - with the current 'master' branch.
5 |
6 | set -e
7 |
8 | if [ ! -f "nix/buildkit.nix" ]; then
9 | echo >&2 "Must run in project root"
10 | exit 1
11 | fi
12 |
13 | now=$( date -u '+%Y-%m-%d %H:%M %Z' )
14 | commit=$( git ls-remote https://github.com/civicrm/civicrm-buildkit.git | awk '/refs\/heads\/master$/ { print $1 }' )
15 | url="https://github.com/civicrm/civicrm-buildkit/archive/${commit}.tar.gz"
16 | hash=$( nix-prefetch-url "$url" --type sha256 --unpack )
17 |
18 | function render_file() {
19 | echo "{ pkgs ? import {} }:"
20 | echo ""
21 | echo "## Get civicrm-buildkit from github."
22 | echo "## Based on \"master\" branch circa $now"
23 | echo "import (pkgs.fetchzip {"
24 | echo " url = \"$url\";"
25 | echo " sha256 = \"$hash\";"
26 | echo "})"
27 | echo
28 | echo "## Get a local copy of civicrm-buildkit. (Useful for developing patches.)"
29 | echo "# import ((builtins.getEnv \"HOME\") + \"/buildkit/default.nix\")"
30 | echo "# import ((builtins.getEnv \"HOME\") + \"/bknix/default.nix\")"
31 | }
32 | render_file > nix/buildkit.nix
33 |
34 | exit
35 | }
36 |
--------------------------------------------------------------------------------
/nix/buildkit.nix:
--------------------------------------------------------------------------------
1 | { pkgs ? import {} }:
2 |
3 | ## Get civicrm-buildkit from github.
4 | ## Based on "master" branch circa 2024-02-26 04:30 UTC
5 | import (pkgs.fetchzip {
6 | url = "https://github.com/civicrm/civicrm-buildkit/archive/d6f6b8dd2d5944c35cd78cb319fef21673214b35.tar.gz";
7 | sha256 = "02p2yzdfgv66a2zf8i36h6pjfi78wnp92m3klij7fqbfd9mpvi5a";
8 | })
9 |
10 | ## Get a local copy of civicrm-buildkit. (Useful for developing patches.)
11 | # import ((builtins.getEnv "HOME") + "/buildkit/default.nix")
12 | # import ((builtins.getEnv "HOME") + "/bknix/default.nix")
13 |
--------------------------------------------------------------------------------
/patches/psysh-0.11-php84.txt:
--------------------------------------------------------------------------------
1 | At time of writing, `psy/psysh@0.11.x` is the last version to support PHP 7.x.
2 | Add the PHP 8.4 fixes to it.
3 |
--------------------------------------------------------------------------------
/patches/scc-CompletionHandler.diff:
--------------------------------------------------------------------------------
1 | diff --git a/src/CompletionHandler.php b/src/CompletionHandler.php
2 | index 236687b..871838e 100644
3 | --- a/src/CompletionHandler.php
4 | +++ b/src/CompletionHandler.php
5 | @@ -40,7 +40,7 @@ class CompletionHandler
6 | */
7 | private $commandWordIndex;
8 |
9 | - public function __construct(Application $application, CompletionContext $context = null)
10 | + public function __construct(Application $application, ?CompletionContext $context = null)
11 | {
12 | $this->application = $application;
13 | $this->context = $context;
14 |
15 |
--------------------------------------------------------------------------------
/patches/scc-CompletionHandler.txt:
--------------------------------------------------------------------------------
1 | At time of writing, `stecman/symfony-console-completion@0.11.0` is the last
2 | version to support PHP 7.x. Add the PHP 8.4 fixes to it.
3 |
--------------------------------------------------------------------------------
/phpunit.xml.dist:
--------------------------------------------------------------------------------
1 |
2 |
14 |
15 |
16 | ./src
17 |
18 |
19 |
20 |
21 | ./tests/
22 |
23 |
24 |
25 |
--------------------------------------------------------------------------------
/scoper.inc.php:
--------------------------------------------------------------------------------
1 | 'Cvphar',
5 | 'exclude-namespaces' => [
6 | // Provided by cv
7 | 'CvDeps',
8 |
9 | // Provided by civicrm
10 | 'Civi',
11 | 'Guzzle',
12 | 'Symfony\Component\DependencyInjection',
13 |
14 | // Drupal8+ bootstrap
15 | 'Drupal',
16 | 'Symfony\\Component\\HttpFoundation',
17 | 'Symfony\\Component\\Routing',
18 |
19 | // Joomla bootstrap
20 | 'TYPO3\\PharStreamWrapper',
21 | ],
22 | 'exclude-classes' => [
23 | '/^(CRM_|HTML_|DB_|Log_)/',
24 | '/^PEAR_(Error|Exception)/',
25 | 'DB',
26 | 'Log',
27 | 'JFactory',
28 | 'Civi',
29 | 'Drupal',
30 | ],
31 | 'exclude-functions' => [
32 | '/^civicrm_/',
33 | '/^wp_.*/',
34 | '/^(drupal|backdrop|user|module)_/',
35 | 't',
36 | ],
37 | 'exclude-files' => [
38 | 'vendor/symfony/polyfill-php80/Resources/stubs/Stringable.php'
39 | ],
40 |
41 | // Do not generate wrappers/aliases for `civicrm_api()` etc or various CMS-booting functions.
42 | 'expose-global-functions' => FALSE,
43 | ];
44 |
--------------------------------------------------------------------------------
/scripts/build.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 |
3 | ## Determine the absolute path of the directory with the file
4 | ## usage: absdirname
5 | function absdirname() {
6 | pushd $(dirname $0) >> /dev/null
7 | pwd
8 | popd >> /dev/null
9 | }
10 |
11 | SCRDIR=$(absdirname "$0")
12 | PRJDIR=$(dirname "$SCRDIR")
13 | OUTFILE="$PRJDIR/bin/cv.phar"
14 | set -e
15 |
16 | pushd "$PRJDIR" >> /dev/null
17 | composer install --prefer-dist --no-progress --no-suggest --no-dev
18 | box compile -v
19 | php scripts/check-phar.php "$OUTFILE"
20 | popd >> /dev/null
21 |
--------------------------------------------------------------------------------
/scripts/check-phar.php:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env php
2 | &2 "Unrecognized SUITE=$SUITE" ; hasSuite=1 ; ;;
84 | esac
85 | fi
86 |
87 | if [ -z "$hasSuite" ]; then
88 | usage 1>&2
89 | exit 1
90 | fi
91 |
92 | if [ -n "$compile" ]; then
93 | ./scripts/build.sh
94 | fi
95 | (cd "$CV_TEST_BUILD" && XDEBUG_MODE=off civibuild restore)
96 |
97 | export CV_TEST_BINARY
98 | "$PHPUNIT" "${passthru[@]}"
99 | }
100 |
101 | ################################################
102 | main "$@"
103 | }
104 |
--------------------------------------------------------------------------------
/shell.nix:
--------------------------------------------------------------------------------
1 | /**
2 | * This shell is suitable for compiling PHAR executables.... and not much else.
3 | *
4 | * Ex: `nix-shell --run ./scripts/build.sh`
5 | */
6 |
7 | { pkgs ? import {} }:
8 |
9 | let
10 |
11 | buildkit = (import ./nix/buildkit.nix) { inherit pkgs; };
12 |
13 | in
14 |
15 | pkgs.mkShell {
16 | nativeBuildInputs = buildkit.profiles.base ++ [
17 |
18 | (buildkit.pins.v2305.php82.buildEnv {
19 | extraConfig = ''
20 | memory_limit=-1
21 | '';
22 | })
23 |
24 | buildkit.pkgs.box
25 | buildkit.pkgs.composer
26 | buildkit.pkgs.pogo
27 | buildkit.pkgs.phpunit8
28 | buildkit.pkgs.phpunit9
29 |
30 | pkgs.bash-completion
31 | ];
32 | shellHook = ''
33 | source ${pkgs.bash-completion}/etc/profile.d/bash_completion.sh
34 | '';
35 | }
36 |
--------------------------------------------------------------------------------
/src/ClassAliases.php:
--------------------------------------------------------------------------------
1 | setName('ang:html:list')
23 | ->setAliases(array())
24 | ->setDescription('List Angular HTML files')
25 | ->configureOutputOptions(['tabular' => TRUE, 'fallback' => 'list', 'defaultColumns' => 'file', 'shortcuts' => TRUE])
26 | ->addArgument('filter', InputArgument::OPTIONAL,
27 | 'Filter by filename. For regex filtering, use semicolon delimiter.')
28 | ->setHelp('List Angular HTML files
29 |
30 | Examples:
31 | cv ang:html:list
32 | cv ang:html:list crmUi/*
33 | cv ang:html:list \';(tabset|wizard)\\.html;\'
34 | ');
35 | }
36 |
37 | protected function execute(InputInterface $input, OutputInterface $output): int {
38 | if (!$input->getOption('user')) {
39 | $output->getErrorOutput()->writeln("For a full list, try passing --user=[username].");
40 | }
41 |
42 | $this->sendStandardTable($this->find($input));
43 | return 0;
44 | }
45 |
46 | /**
47 | * Find extensions matching the input args.
48 | *
49 | * @param \Symfony\Component\Console\Input\InputInterface $input
50 | * @return array
51 | */
52 | protected function find($input) {
53 | $regex = $input->getArgument('filter') ? $this->createRegex($input->getArgument('filter')) : NULL;
54 | $ang = \Civi::service('angular');
55 | $rows = array();
56 |
57 | foreach ($ang->getModules() as $name => $module) {
58 | $partials = $ang->getPartials($name);
59 | foreach ($partials as $file => $html) {
60 | $rows[] = array(
61 | 'file' => preg_replace(';^~/;', '', $file),
62 | 'module' => $name,
63 | 'ext' => $module['ext'],
64 | );
65 | }
66 | }
67 |
68 | $rows = array_filter($rows, function ($row) use ($regex) {
69 | if ($regex) {
70 | if (!preg_match($regex, $row['file'])) {
71 | return FALSE;
72 | }
73 | }
74 | return TRUE;
75 | });
76 |
77 | return $rows;
78 | }
79 |
80 | protected function createRegex($filterExpr) {
81 | if ($filterExpr[0] === ';') {
82 | return $filterExpr;
83 | }
84 | // $filterExpr = preg_replace(';^~/;', '', $filterExpr);
85 | $regex = preg_quote($filterExpr, ';');
86 | $regex = str_replace('\\*', '[^/]*', $regex);
87 | $regex = ";^$regex.*$;";
88 | return $regex;
89 | }
90 |
91 | }
92 |
--------------------------------------------------------------------------------
/src/Command/AngularModuleListCommand.php:
--------------------------------------------------------------------------------
1 | setName('ang:module:list')
23 | ->setAliases(array())
24 | ->setDescription('List Angular modules')
25 | ->configureOutputOptions(['tabular' => TRUE, 'fallback' => 'table', 'defaultColumns' => 'name,basePages,requires', 'shortcuts' => TRUE])
26 | ->addArgument('regex', InputArgument::OPTIONAL,
27 | 'Filter extensions by full key or short name')
28 | ->setHelp('List Angular modules
29 |
30 | Examples:
31 | cv ang:module:list
32 | cv ang:module:list /crmUi/
33 | cv ang:module:list --columns=name,ext,extDir
34 | cv ang:module:list \'/crmMail/\' --user=admin --columns=extDir,css
35 | cv ang:module:list --columns=name,js,css --out=json-pretty
36 | ');
37 | }
38 |
39 | protected function execute(InputInterface $input, OutputInterface $output): int {
40 | if (!$input->getOption('user')) {
41 | $output->getErrorOutput()->writeln("For a full list, try passing --user=[username].");
42 | }
43 |
44 | $this->sendStandardTable($this->find($input));
45 | return 0;
46 | }
47 |
48 | /**
49 | * Find extensions matching the input args.
50 | *
51 | * @param \Symfony\Component\Console\Input\InputInterface $input
52 | * @return array
53 | */
54 | protected function find($input) {
55 | $regex = $input->getArgument('regex');
56 | $ang = \Civi::service('angular');
57 | $rows = array();
58 |
59 | foreach ($ang->getModules() as $name => $module) {
60 | $resources = array();
61 | foreach (array('js', 'partials', 'css', 'settings') as $key) {
62 | if (!empty($module[$key])) {
63 | $resources[] = sprintf("%s(%d)", $key, count($module[$key]));
64 | }
65 | }
66 |
67 | if (!isset($module['basePages'])) {
68 | $basePages = 'civicrm/a';
69 | }
70 | elseif (empty($module['basePages'])) {
71 | $basePages = '(as-needed)';
72 | }
73 | else {
74 | $basePages = implode(", ", $module['basePages']);
75 | }
76 |
77 | $rows[] = array(
78 | 'name' => $name,
79 | 'ext' => $module['ext'],
80 | 'extDir' => \CRM_Core_Resources::singleton()->getPath($module['ext'], NULL),
81 | 'resources' => implode(', ', $resources),
82 | 'basePages' => $basePages,
83 | 'js' => isset($module['js']) ? implode(", ", $module['js']) : '',
84 | 'css' => isset($module['css']) ? implode(", ", $module['css']) : '',
85 | 'partials' => isset($module['partials']) ? implode(", ", $module['partials']) : '',
86 | 'requires' => isset($module['requires']) ? implode(', ', $module['requires']) : '',
87 | );
88 | }
89 |
90 | $rows = array_filter($rows, function ($row) use ($regex) {
91 | if ($regex) {
92 | if (!preg_match($regex, $row['ext']) && !preg_match($regex, $row['name'])) {
93 | return FALSE;
94 | }
95 | }
96 | return TRUE;
97 | });
98 |
99 | return $rows;
100 | }
101 |
102 | }
103 |
--------------------------------------------------------------------------------
/src/Command/BootCommand.php:
--------------------------------------------------------------------------------
1 | setName('php:boot')
12 | ->setDescription('Generate PHP bootstrap code');
13 | }
14 |
15 | protected function execute(InputInterface $input, OutputInterface $output): int {
16 | switch ($input->getOption('level')) {
17 | case 'classloader':
18 | $code = implode("\n", [
19 | sprintf('$GLOBALS["civicrm_root"] = %s;', var_export(rtrim($GLOBALS["civicrm_root"], '/'), 1)),
20 | 'require_once $GLOBALS["civicrm_root"] . "/CRM/Core/ClassLoader.php";',
21 | '\CRM_Core_ClassLoader::singleton()->register();',
22 | ]);
23 | break;
24 |
25 | case 'settings':
26 | $code = \Civi\Cv\Bootstrap::singleton()->generate()
27 | . '\CRM_Core_Config::singleton(FALSE);';
28 | break;
29 |
30 | case 'full':
31 | $code = \Civi\Cv\Bootstrap::singleton()->generate()
32 | . '\CRM_Core_Config::singleton();'
33 | . '\CRM_Utils_System::loadBootStrap(array(), FALSE);';
34 | break;
35 |
36 | case 'cms-full':
37 | $code = \Civi\Cv\CmsBootstrap::singleton()->generate(['bootCms', 'bootCivi']);
38 | break;
39 |
40 | case 'none':
41 | $code = '';
42 | break;
43 |
44 | default:
45 | throw new \Exception("Cannot generate boot instructions for given boot level.");
46 | }
47 |
48 | $output->writeln('/*BEGINPHP*/');
49 | $output->writeln($code);
50 | $output->writeln('/*ENDPHP*/');
51 | return 0;
52 | }
53 |
54 | }
55 |
--------------------------------------------------------------------------------
/src/Command/CliCommand.php:
--------------------------------------------------------------------------------
1 | setName('cli')
17 | ->setDescription('Load interactive command line');
18 | }
19 |
20 | protected function execute(InputInterface $input, OutputInterface $output): int {
21 | $cv = new Application();
22 | $sh = new \Psy\Shell();
23 | $sh->addCommands($cv->createCommands());
24 | // When I try making a new matcher, it doesn't seem to get called.
25 | //$sh->addTabCompletionMatchers(array(
26 | // new ApiMatcher(),
27 | //));
28 | $sh->run();
29 | return 0;
30 | }
31 |
32 | }
33 |
--------------------------------------------------------------------------------
/src/Command/CoreCheckReqCommand.php:
--------------------------------------------------------------------------------
1 | setName('core:check-req')
20 | ->setDescription('Check installation requirements')
21 | ->configureOutputOptions(['tabular' => TRUE, 'fallback' => 'table', 'shortcuts' => TRUE, 'defaultColumns' => 'severity,section,name,message'])
22 | ->addOption('filter-warnings', 'w', InputOption::VALUE_NONE, 'Show warnings')
23 | ->addOption('filter-errors', 'e', InputOption::VALUE_NONE, 'Show errors')
24 | ->addOption('filter-infos', 'i', InputOption::VALUE_NONE, 'Show info')
25 | ->configureSetupOptions()
26 | ->setHelp('
27 | Check whether installation requirements are met.
28 |
29 | Example: Show all checks
30 | $ cv core:check-req
31 |
32 | Example: Show only errors
33 | $ cv core:check-req -e
34 |
35 | Example: Show warnings and errors
36 | $ cv core:check-req -we
37 | ');
38 | }
39 |
40 | public function getBootOptions(): array {
41 | return ['default' => 'none', 'allow' => ['none']];
42 | }
43 |
44 | protected function execute(InputInterface $input, OutputInterface $output): int {
45 | $filterSeverities = $this->parseFilter($input);
46 |
47 | $showBootMsgsByDefault = in_array($input->getOption('out'), ['table', 'pretty']);
48 | $setup = $this->bootSetupSubsystem($input, $output, $showBootMsgsByDefault ? 0 : OutputInterface::VERBOSITY_VERBOSE);
49 | $reqs = $setup->checkRequirements();
50 | $messages = array_filter($reqs->getMessages(), function ($m) use ($filterSeverities) {
51 | return in_array($m['severity'], $filterSeverities);
52 | });
53 | $this->sendStandardTable($messages);
54 | return $reqs->getErrors() ? 1 : 0;
55 | }
56 |
57 | /**
58 | * @param \Symfony\Component\Console\Input\InputInterface $input
59 | * @return array
60 | */
61 | protected function parseFilter(InputInterface $input) {
62 | $filterSeverities = array();
63 | if ($input->getOption('filter-warnings')) {
64 | $filterSeverities[] = 'warning';
65 | }
66 | if ($input->getOption('filter-errors')) {
67 | $filterSeverities[] = 'error';
68 | }
69 | if ($input->getOption('filter-infos')) {
70 | $filterSeverities[] = 'info';
71 | }
72 | if ($filterSeverities === array()) {
73 | $filterSeverities = array('warning', 'error', 'info');
74 | }
75 | return $filterSeverities;
76 | }
77 |
78 | }
79 |
--------------------------------------------------------------------------------
/src/Command/CoreUninstallCommand.php:
--------------------------------------------------------------------------------
1 | setName('core:uninstall')
22 | ->setDescription('Purge CiviCRM schema and settings files')
23 | ->configureSetupOptions()
24 | ->addOption('force', 'f', InputOption::VALUE_NONE, 'Remove without any prompt or confirmation')
25 | ->configureOutputOptions()
26 | ->setHelp('
27 | Purge CiviCRM schema and settings files
28 |
29 | TIP: If you have a special system configuration, it may help to pass the same
30 | options for "core:uninstall" as the preceding "core:install".
31 | ');
32 | }
33 |
34 | public function getBootOptions(): array {
35 | return ['default' => 'none', 'allow' => ['none']];
36 | }
37 |
38 | protected function execute(InputInterface $input, OutputInterface $output): int {
39 | $setup = $this->bootSetupSubsystem($input, $output);
40 |
41 | $debugEvent = OptionalOption::parse($input, ['--debug-event'], NULL, '');
42 | if ($debugEvent !== NULL) {
43 | $eventNames = $this->findEventNames($setup->getDispatcher(), $debugEvent);
44 | $this->printEventListeners($output, $setup->getDispatcher(), $eventNames);
45 | return 0;
46 | }
47 |
48 | $installed = $setup->checkInstalled();
49 | if (!$installed->isDatabaseInstalled() && !$installed->isSettingInstalled()) {
50 | $output->writeln("CiviCRM does not appear to be installed.");
51 | return 0;
52 | }
53 |
54 | if ($installed->isDatabaseInstalled()) {
55 | $output->writeln(sprintf("Found civicrm_* database tables in %s", $setup->getModel()->db['database']));
56 | }
57 |
58 | if ($installed->isSettingInstalled()) {
59 | $output->writeln(sprintf("Found %s in %s", basename($setup->getModel()->settingsPath), dirname($setup->getModel()->settingsPath)));
60 | }
61 |
62 | if (!$input->getOption('force')) {
63 | $output->writeln('');
64 | $helper = $this->getHelper('question');
65 | $question = new ConfirmationQuestion('Are you sure want to purge the CiviCRM database and data files? Data may be permanently destroyed. (y/N) ', FALSE);
66 | if (!$helper->ask($input, $output, $question)) {
67 | $output->writeln("Aborted");
68 | return 1;
69 | }
70 | }
71 |
72 | if ($installed->isDatabaseInstalled()) {
73 | $output->writeln(sprintf("Removing civicrm_* database tables in %s", $setup->getModel()->db['database']));
74 | $setup->uninstallDatabase();
75 | }
76 |
77 | if ($installed->isSettingInstalled()) {
78 | $output->writeln(sprintf("Removing %s from %s", basename($setup->getModel()->settingsPath), dirname($setup->getModel()->settingsPath)));
79 | $setup->uninstallFiles();
80 | }
81 |
82 | return 0;
83 | }
84 |
85 | }
86 |
--------------------------------------------------------------------------------
/src/Command/DebugDispatcherCommand.php:
--------------------------------------------------------------------------------
1 | setName('event')
16 | ->setDescription('Inspect events and listeners')
17 | ->addArgument('event', InputArgument::OPTIONAL, 'An event name or regex')
18 | // ->addOption('out', NULL, InputArgument::OPTIONAL, 'Specify return format (json,none,php,pretty,shell)', \Civi\Cv\Encoder::getDefaultFormat())
19 | // ->configureOutputOptions()
20 | ->setHelp('
21 | Dump the list of event listeners
22 |
23 | Examples:
24 | cv debug:event-dispatcher
25 | cv debug:event-dispatcher actionSchedule.getMappings
26 | cv debug:event-dispatcher /^actionSchedule/
27 | ');
28 | }
29 |
30 | protected function initialize(InputInterface $input, OutputInterface $output) {
31 | define('CIVICRM_CONTAINER_CACHE', 'never');
32 | $output->getErrorOutput()->writeln('The debug command ignores the container cache.');
33 | parent::initialize($input, $output);
34 | }
35 |
36 | protected function execute(InputInterface $input, OutputInterface $output): int {
37 | $container = \Civi::container();
38 |
39 | /*
40 | * Workaround: Ensure that API kernel has registered its event listeners.
41 | *
42 | * At time of writing (Mar 2017), civicrm-core has its list of
43 | * event-listeners in two places: Container::createEventDispatcher() and
44 | * Container::createApiKernel(). That's ugly. In the long term, both of
45 | * these should be replaced with a more distributed+consistent mechanism
46 | * (e.g. `services.yml`). However, in the mean-time, we need to ensure
47 | * that the API kernel (if applicable) has its chance to register listeners.
48 | */
49 | if ($container->has('civi_api_kernel')) {
50 | $container->get('civi_api_kernel');
51 | }
52 |
53 | $dispatcher = $container->get('dispatcher');
54 | $eventFilter = $input->getArgument('event');
55 | $eventNames = $this->findEventNames($dispatcher, $eventFilter);
56 | $this->printEventListeners($output, $dispatcher, $eventNames);
57 | return 0;
58 | }
59 |
60 | }
61 |
--------------------------------------------------------------------------------
/src/Command/EditCommand.php:
--------------------------------------------------------------------------------
1 | setName('vars:edit')
25 | ->setDescription('Edit configuration values for this build');
26 | }
27 |
28 | public function __construct($name = NULL) {
29 | parent::__construct($name);
30 | $this->editor = new CliEditor();
31 | $this->editor->setValidator(function ($file) {
32 | $data = json_decode(file_get_contents($file));
33 | if ($data === NULL) {
34 | return array(
35 | FALSE,
36 | '// The JSON document was malformed. Please resolve syntax errors and then remove this message.',
37 | );
38 | }
39 | else {
40 | return array(TRUE, '');
41 | }
42 | });
43 | }
44 |
45 | protected function execute(InputInterface $input, OutputInterface $output): int {
46 | $config = Config::read();
47 | $oldSiteData = empty($config['sites'][CIVICRM_SETTINGS_PATH]) ? array() : $config['sites'][CIVICRM_SETTINGS_PATH];
48 | $oldJson = Encoder::encode($oldSiteData, 'json-pretty');
49 | $newJson = $this->editor->editBuffer($oldJson);
50 | $newSiteData = json_decode($newJson);
51 |
52 | print "NEW DATA\n\n====\n$newJson\n====\n";
53 |
54 | // Config::update(function ($config) use ($newSiteData) {
55 | // $config['sites'][CIVICRM_SETTINGS_PATH] = $newSiteData;
56 | // return $config;
57 | // });
58 | }
59 |
60 | }
61 |
--------------------------------------------------------------------------------
/src/Command/EvalCommand.php:
--------------------------------------------------------------------------------
1 | setName('php:eval')
17 | ->setAliases(array('ev'))
18 | ->setDescription('Evaluate a snippet of PHP code')
19 | ->addArgument('code')
20 | ->addOption('out', NULL, InputArgument::OPTIONAL, 'Specify return format (auto,' . implode(',', Encoder::getFormats()) . ')', 'auto')
21 | ->setHelp('
22 | Evaluate a snippet of PHP code
23 |
24 | Examples:
25 | cv ev \'civicrm_api3("System", "flush", array());\'
26 | cv ev \'if (rand(0,10)<5) echo "heads\n"; else echo "tails\n";\'
27 |
28 | When reading data, you may use "return":
29 | cv ev \'return CRM_Utils_System::version()\'
30 | cv ev \'return CRM_Utils_System::version()\' --out=shell
31 | cv ev \'return CRM_Utils_System::version()\' --out=json
32 |
33 | If the output format is set to "auto". This will be produce silent output -- unless
34 | you use a "return" statement. In that case, it will use the default (' . \Civi\Cv\Encoder::getDefaultFormat() . ').
35 |
36 | NOTE: To change the default output format, set CV_OUTPUT.
37 | ');
38 | }
39 |
40 | protected function execute(InputInterface $input, OutputInterface $output): int {
41 | if ($input->getOption('out') === 'auto') {
42 | $hasReturn = preg_match('/^\s*return[ \t\r\n]/', $input->getArgument('code'))
43 | || preg_match('/[;\{]\s*return[ \t\r\n]/', $input->getArgument('code'));
44 | $input->setOption('out', $hasReturn ? \Civi\Cv\Encoder::getDefaultFormat() : 'none');
45 | }
46 |
47 | $value = eval($input->getArgument('code') . ';');
48 | $this->sendResult($input, $output, $value);
49 | return 0;
50 | }
51 |
52 | }
53 |
--------------------------------------------------------------------------------
/src/Command/ExtensionDisableCommand.php:
--------------------------------------------------------------------------------
1 | setName('ext:disable')
24 | ->setAliases(array('dis'))
25 | ->setDescription('Disable an extension')
26 | ->addArgument('key-or-name', InputArgument::IS_ARRAY, 'One or more extensions to enable. Identify the extension by full key ("org.example.foobar") or short name ("foobar")')
27 | ->setHelp('Disable an extension
28 |
29 | Examples:
30 | cv ext:disable org.example.foobar
31 | cv dis foobar
32 |
33 | Note:
34 | Beginning circa CiviCRM v4.2+, it has been recommended that extensions
35 | include a unique long name ("org.example.foobar") and a unique short
36 | name ("foobar"). However, short names are not strongly guaranteed.
37 |
38 | This subcommand does not output parseable data. For parseable output,
39 | consider using `cv api extension.disable`.
40 | ');
41 | }
42 |
43 | protected function execute(InputInterface $input, OutputInterface $output): int {
44 | [$foundKeys, $missingKeys] = $this->parseKeys($input, $output);
45 |
46 | // Uninstall what's recognized or what looks like an ext key.
47 | $disableKeys = array_merge($foundKeys, preg_grep('/\./', $missingKeys));
48 | $missingKeys = preg_grep('/\./', $missingKeys, PREG_GREP_INVERT);
49 |
50 | foreach ($missingKeys as $key) {
51 | $output->writeln("Ignoring unrecognized extension \"$key\"");
52 | }
53 | foreach ($disableKeys as $key) {
54 | $output->writeln("Disabling extension \"$key\"");
55 | }
56 |
57 | $result = VerboseApi::callApi3Success('Extension', 'disable', array(
58 | 'keys' => $disableKeys,
59 | ));
60 | return empty($result['is_error']) ? 0 : 1;
61 | }
62 |
63 | }
64 |
--------------------------------------------------------------------------------
/src/Command/ExtensionEnableCommand.php:
--------------------------------------------------------------------------------
1 | setName('ext:enable')
25 | ->setAliases(['en', 'ext:install'])
26 | ->setDescription('Enable an extension')
27 | ->addOption('refresh', 'r', InputOption::VALUE_NONE, 'Refresh the local list of extensions (Default: Only refresh on cache-miss)')
28 | ->addOption('ignore-missing', NULL, InputOption::VALUE_NONE, 'If a requested extension is missing, skip it')
29 | ->addArgument('key-or-name', InputArgument::IS_ARRAY, 'One or more extensions to enable. Identify the extension by full key ("org.example.foobar") or short name ("foobar")')
30 | ->setHelp('Enable an extension
31 |
32 | Examples:
33 | cv ext:enable org.example.foobar
34 | cv en foobar
35 |
36 | Note:
37 | Beginning circa CiviCRM v4.2+, it has been recommended that extensions
38 | include a unique long name ("org.example.foobar") and a unique short
39 | name ("foobar"). However, short names are not strongly guaranteed.
40 |
41 | This subcommand does not output parseable data. For parseable output,
42 | consider using `cv api extension.install`.
43 | ');
44 | }
45 |
46 | protected function execute(InputInterface $input, OutputInterface $output): int {
47 | // Refresh extensions if (a) ---refresh enabled or (b) there's a cache-miss.
48 | $refresh = $input->getOption('refresh') ? 'yes' : 'auto';
49 | // $refresh = OptionalOption::parse(array('--refresh', '-r'), 'auto', 'yes');
50 | while (TRUE) {
51 | if ($refresh === 'yes') {
52 | $output->writeln("Refreshing extension cache");
53 | $result = VerboseApi::callApi3Success('Extension', 'refresh', array(
54 | 'local' => TRUE,
55 | 'remote' => FALSE,
56 | ));
57 | if (!empty($result['is_error'])) {
58 | return 1;
59 | }
60 | }
61 |
62 | [$foundKeys, $missingKeys] = $this->parseKeys($input, $output);
63 | if ($refresh == 'auto' && !empty($missingKeys)) {
64 | $output->writeln("Extension cache does not contain requested item(s)");
65 | $refresh = 'yes';
66 | }
67 | else {
68 | break;
69 | }
70 | }
71 |
72 | if ($missingKeys) {
73 | if ($input->getOption('ignore-missing')) {
74 | foreach ($missingKeys as $key) {
75 | $output->getErrorOutput()->writeln("Warning: Skipped unrecognized extension \"$key\"");
76 | }
77 |
78 | }
79 | else {
80 | foreach ($missingKeys as $key) {
81 | $output->getErrorOutput()->writeln("Error: Unrecognized extension \"$key\"");
82 | }
83 | return 1;
84 | }
85 | }
86 |
87 | foreach ($foundKeys as $key) {
88 | $output->writeln("Enabling extension \"$key\"");
89 | }
90 |
91 | $result = VerboseApi::callApi3Success('Extension', 'install', array(
92 | 'keys' => $foundKeys,
93 | ));
94 | return empty($result['is_error']) ? 0 : 1;
95 | }
96 |
97 | }
98 |
--------------------------------------------------------------------------------
/src/Command/ExtensionUninstallCommand.php:
--------------------------------------------------------------------------------
1 | setName('ext:uninstall')
24 | ->setAliases(array())
25 | ->setDescription('Uninstall an extension and purge its data')
26 | ->addArgument('key-or-name', InputArgument::IS_ARRAY, 'One or more extensions to enable. Identify the extension by full key ("org.example.foobar") or short name ("foobar")')
27 | ->setHelp('Uninstall an extension and purge its data.
28 |
29 | Examples:
30 | cv ext:uninstall org.example.foobar
31 | cv ext:uninstall foobar
32 |
33 | Note:
34 | Beginning circa CiviCRM v4.2+, it has been recommended that extensions
35 | include a unique long name ("org.example.foobar") and a unique short
36 | name ("foobar"). However, short names are not strongly guaranteed.
37 |
38 | This subcommand does not output parseable data. For parseable output,
39 | consider using `cv api extension.uninstall`.
40 | ');
41 | }
42 |
43 | protected function execute(InputInterface $input, OutputInterface $output): int {
44 | [$foundKeys, $missingKeys] = $this->parseKeys($input, $output);
45 |
46 | // Uninstall what's recognized or what looks like an ext key.
47 | $uninstallKeys = array_merge($foundKeys, preg_grep('/\./', $missingKeys));
48 | $missingKeys = preg_grep('/\./', $missingKeys, PREG_GREP_INVERT);
49 |
50 | foreach ($missingKeys as $key) {
51 | $output->writeln("Ignoring unrecognized extension \"$key\"");
52 | }
53 | foreach ($uninstallKeys as $key) {
54 | $output->writeln("Uninstalling extension \"$key\"");
55 | }
56 |
57 | $result = VerboseApi::callApi3Success('Extension', 'disable', array(
58 | 'keys' => $uninstallKeys,
59 | ));
60 | if (!empty($result['is_error'])) {
61 | return 1;
62 | }
63 |
64 | $result = VerboseApi::callApi3Success('Extension', 'uninstall', array(
65 | 'keys' => $uninstallKeys,
66 | ));
67 | return empty($result['is_error']) ? 0 : 1;
68 | }
69 |
70 | }
71 |
--------------------------------------------------------------------------------
/src/Command/ExtensionUpgradeDbCommand.php:
--------------------------------------------------------------------------------
1 | setName('ext:upgrade-db')
23 | ->setAliases(array())
24 | ->setDescription('Apply DB upgrades for any extensions (DEPRECATED)')
25 | ->setHelp('Apply DB upgrades for any extensions
26 |
27 | Examples:
28 | cv ext:upgrade-db
29 |
30 | Note:
31 | This subcommand does not output parseable data. For parseable output,
32 | consider using `cv api extension.upgrade`.
33 |
34 | Deprecation:
35 | This command is now deprecated. Use "cv upgrade:db" to perform upgrades
36 | for core and/or extensions.
37 | ');
38 | }
39 |
40 | protected function initialize(InputInterface $input, OutputInterface $output) {
41 | $output->writeln("WARNING: \"ext:upgrade-db\" is deprecated. Use the main \"updb\" command instead.");
42 | parent::initialize($input, $output);
43 | }
44 |
45 | protected function execute(InputInterface $input, OutputInterface $output): int {
46 | $output->writeln("Applying database upgrades from extensions");
47 | $result = VerboseApi::callApi3Success('Extension', 'upgrade', array());
48 | if (!empty($result['is_error'])) {
49 | return 1;
50 | }
51 | return 0;
52 | }
53 |
54 | }
55 |
--------------------------------------------------------------------------------
/src/Command/FillCommand.php:
--------------------------------------------------------------------------------
1 | setName('vars:fill')
23 | ->setDescription('Generate a configuration file for any missing site data')
24 | ->addOption('file', NULL, InputOption::VALUE_REQUIRED, 'Read existing configuration from a file');
25 | }
26 |
27 | public function __construct($name = NULL) {
28 | parent::__construct($name);
29 | $this->defaults = array(
30 | 'ADMIN_EMAIL' => 'admin@example.com',
31 | 'ADMIN_PASS' => 't0ps3cr3t',
32 | 'ADMIN_USER' => 'admin',
33 | 'CIVI_CORE' => '',
34 | 'CIVI_DB_DSN' => 'mysql://dbUser:dbPass@dbHost/dbName?new_link=true',
35 | 'CIVI_FILES' => '',
36 | 'CIVI_SETTINGS' => '',
37 | 'CIVI_SITE_KEY' => '',
38 | 'CIVI_TEMPLATEC' => '',
39 | 'CIVI_UF' => '',
40 | 'CIVI_URL' => '',
41 | 'CIVI_VERSION' => '',
42 | 'CMS_DB_DSN' => 'mysql://dbUser:dbPass@dbHost/dbName?new_link=true',
43 | 'CMS_ROOT' => '',
44 | 'CMS_TITLE' => 'Untitled installation',
45 | 'CMS_URL' => '',
46 | 'CMS_VERSION' => '',
47 | 'DEMO_EMAIL' => 'demo@example.com',
48 | 'DEMO_PASS' => 't0ps3cr3t',
49 | 'DEMO_USER' => 'demo',
50 | 'IS_INSTALLED' => '1',
51 | 'SITE_TOKEN' => md5(openssl_random_pseudo_bytes(256)),
52 | 'SITE_TYPE' => '',
53 | 'TEST_DB_DSN' => 'mysql://dbUser:dbPass@dbHost/dbName?new_link=true',
54 | );
55 | }
56 |
57 | protected function execute(InputInterface $input, OutputInterface $output): int {
58 | if (!$input->getOption('file')) {
59 | $reader = new SiteConfigReader(CIVICRM_SETTINGS_PATH);
60 | $liveData = $reader->compile(array('buildkit', 'home', 'active'));
61 | }
62 | else {
63 | $file = $input->getOption('file');
64 | if (strpos($file, '://') !== FALSE) {
65 | throw new \RuntimeException("Failed to extract current configuration.");
66 | }
67 | if ($file === '/dev/stdin') {
68 | $file = 'php://stdin';
69 | }
70 | $liveData = json_decode(file_get_contents($file), 1);
71 | }
72 |
73 | if ($liveData === NULL) {
74 | throw new \RuntimeException("Failed to extract current configuration.");
75 | }
76 |
77 | $siteConfig = array();
78 | foreach ($this->defaults as $field => $value) {
79 | if (!isset($liveData[$field])) {
80 | $siteConfig[$field] = $value;
81 | }
82 | }
83 |
84 | $output->writeln(sprintf("Site: %s", CIVICRM_SETTINGS_PATH));
85 | if (empty($siteConfig)) {
86 | $output->writeln("No extra fields are required.");
87 | }
88 | else {
89 | $output->writeln(sprintf("These fields were missing. Setting defaults:"));
90 | $output->writeln(Encoder::encode($siteConfig, 'json-pretty'));
91 | Config::update(function ($config) use ($siteConfig, $output) {
92 | if (isset($config['sites'][CIVICRM_SETTINGS_PATH])) {
93 | $config['sites'][CIVICRM_SETTINGS_PATH] = array_merge($siteConfig, $config['sites'][CIVICRM_SETTINGS_PATH]);
94 | }
95 | else {
96 | $config['sites'][CIVICRM_SETTINGS_PATH] = $siteConfig;
97 | }
98 | ksort($config['sites'][CIVICRM_SETTINGS_PATH]);
99 | return $config;
100 | });
101 | $output->writeln(sprintf("Please edit %s", Config::getFileName()));
102 | }
103 | return 0;
104 | }
105 |
106 | }
107 |
--------------------------------------------------------------------------------
/src/Command/FlushCommand.php:
--------------------------------------------------------------------------------
1 | setName('flush')
14 | ->setAliases(array())
15 | ->addOption('triggers', 'T', InputOption::VALUE_NONE, 'Rebuild triggers')
16 | ->setDescription('Flush system caches')
17 | ->setHelp('
18 | Flush system caches
19 | ');
20 | }
21 |
22 | protected function initialize(InputInterface $input, OutputInterface $output) {
23 | // The main reason we have this as separate command -- so we can ignore
24 | // stale class-references that might be retained by the container cache.
25 | define('CIVICRM_CONTAINER_CACHE', 'never');
26 |
27 | // Now we can let the parent proceed with bootstrap...
28 | parent::initialize($input, $output);
29 | }
30 |
31 | protected function execute(InputInterface $input, OutputInterface $output): int {
32 | $params = array();
33 | if ($input->getOption('triggers')) {
34 | $params['triggers'] = TRUE;
35 | }
36 |
37 | $output->writeln("Flushing system caches");
38 | $result = VerboseApi::callApi3Success('System', 'flush', $params);
39 | return empty($result['is_error']) ? 0 : 1;
40 | }
41 |
42 | }
43 |
--------------------------------------------------------------------------------
/src/Command/PipeCommand.php:
--------------------------------------------------------------------------------
1 | setName('pipe')
20 | ->setDescription('Start a Civi::pipe session (JSON-RPC 2.0)')
21 | ->setHelp('Start a Civi::pipe session (JSON-RPC 2.0)
22 |
23 | The Civi::pipe protocol provides a line-oriented session for executing multiple requests in a single CiviCRM instance.
24 |
25 | Callers may request *connection flags*, such as:
26 |
27 | * v: Show version
28 | * l: Show login support
29 | * t: Enable trusted mode
30 | * u: Enable untrusted mode
31 |
32 | Examples:
33 |
34 | $ cv pipe
35 | {"Civi::pipe":{"v":"5.47.alpha1","t":"trusted","l":["nologin"]}}
36 |
37 | $ cv pipe uv
38 | {"Civi::pipe":{"u":"untrusted","v":"5.47.alpha1"}}
39 |
40 | See also: https://docs.civicrm.org/dev/en/latest/framework/pipe
41 | ');
42 | $this->addArgument('connection-flags', InputArgument::OPTIONAL, 'List of connection-flags (Default: Determined by civicrm-core)', NULL);
43 | // Tempting to add separate CLI flags that map to 'connection-flags', but this seems easier given that:
44 | // 1. The existing flags have different meanings (eg (v)erbose vs (v)ersion).
45 | // 2. The connection-flags are owned by civicrm-core.git. Don't want to update this whenever there's a new one.
46 | }
47 |
48 | protected function execute(InputInterface $input, OutputInterface $output): int {
49 | if (!is_callable(['Civi', 'pipe'])) {
50 | fwrite(STDERR, "This version of CiviCRM does not include Civi::pipe() support.\n");
51 | return 1;
52 | }
53 | if ($flags = $input->getArgument('connection-flags')) {
54 | \Civi::pipe($flags);
55 | }
56 | else {
57 | \Civi::pipe();
58 | }
59 | return 0;
60 | }
61 |
62 | }
63 |
--------------------------------------------------------------------------------
/src/Command/QueueNextCommand.php:
--------------------------------------------------------------------------------
1 | setName('console-queue:run-next')
16 | ->setDescription('(INTERNAL) Run the next task in queue')
17 | ->setHidden(TRUE)
18 | ->addOption('steal', NULL, InputOption::VALUE_NONE)
19 | ->addOption('skip', NULL, InputOption::VALUE_NONE)
20 | ->addOption('out', NULL, InputOption::VALUE_REQUIRED, 'Store outcome in a JSON file')
21 | ->addOption('queue', NULL, InputOption::VALUE_REQUIRED, 'Queue name')
22 | ->addOption('queue-spec', NULL, InputOption::VALUE_REQUIRED, 'Queue specification (Base64-JSON)');
23 | }
24 |
25 | protected function execute(InputInterface $input, OutputInterface $output): int {
26 | $outFile = $input->getOption('out');
27 | if ($outFile && file_exists($outFile)) {
28 | $this->writeFile($outFile, '');
29 | }
30 |
31 | $queue = $this->getQueue($input);
32 |
33 | $runner = new CRM_Queue_Runner([
34 | 'queue' => $queue,
35 | ]);
36 | if ($input->getOption('skip')) {
37 | $result = $runner->skipNext($input->getOption('steal'));
38 | }
39 | else {
40 | $result = $runner->runNext($input->getOption('steal'));
41 | }
42 |
43 | if ($outFile) {
44 | $this->writeFile($outFile, json_encode($result, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES));
45 | }
46 |
47 | if (!empty($result['exception'])) {
48 | throw $result['exception'];
49 | }
50 | return empty($result['is_error']) ? 0 : 1;
51 | }
52 |
53 | private function writeFile(string $outFile, string $data): void {
54 | $parent = dirname($outFile);
55 | if (!is_dir($parent)) {
56 | if (!mkdir($parent, 0777, TRUE)) {
57 | throw new \RuntimeException("Failed to create directory $parent");
58 | }
59 | }
60 | $result = file_put_contents($outFile, $data);
61 | if ($result === FALSE) {
62 | throw new \RuntimeException("Failed to write to $outFile");
63 | }
64 | }
65 |
66 | /**
67 | * @param \Symfony\Component\Console\Input\InputInterface $input
68 | * @return \CRM_Queue_Queue
69 | */
70 | private function getQueue(InputInterface $input): \CRM_Queue_Queue {
71 | // cv is evergreen and may be used on old versions, so we accept either --queue-spec (old style, non-persistent queues)
72 | // or --queue (new style, persistent queues).
73 | if ($input->getOption('queue-spec')) {
74 | // For old-fashioned systems which lack support for persistent queue-definitions.
75 | $spec = json_decode(base64_decode($input->getOption('queue-spec')), TRUE);
76 | if (empty($spec)) {
77 | throw new \InvalidArgumentException('Queue spec is empty or malformed');
78 | }
79 | $queue = \CRM_Queue_Service::singleton()->create($spec);
80 | }
81 | elseif ($input->getOption('queue')) {
82 | $queue = Civi::queue($input->getOption('queue'));
83 | }
84 | else {
85 | throw new \LogicException("Must specify either --queue or --queue-spec");
86 | }
87 | return $queue;
88 | }
89 |
90 | }
91 |
--------------------------------------------------------------------------------
/src/Command/ScriptCommand.php:
--------------------------------------------------------------------------------
1 | setName('php:script')
14 | ->setAliases(array('scr'))
15 | ->setDescription('Execute a PHP script')
16 | ->addArgument('script', InputArgument::REQUIRED)
17 | ->addArgument('scriptArguments', InputArgument::IS_ARRAY, 'Optional arguments to pass to the script as $argv');
18 | }
19 |
20 | public function getBootOptions(): array {
21 | return ['auto' => FALSE] + parent::getBootOptions();
22 | }
23 |
24 | protected function execute(InputInterface $input, OutputInterface $output): int {
25 | $fs = new Filesystem();
26 | $origScript = $fs->toAbsolutePath($input->getArgument('script'));
27 | $scriptArguments = $input->getArgument('scriptArguments');
28 |
29 | $origCwd = getcwd();
30 | $this->boot($input, $output);
31 | $postCwd = getcwd();
32 |
33 | // Normal operation: Use the script path provided at input.
34 | if (file_exists($origScript)) {
35 | chdir($origCwd);
36 | $this->runScript($output, $origScript, $scriptArguments);
37 | return 0;
38 | }
39 |
40 | // Backward compat: Try script relative the post-boot CWD.
41 | $postScript = $fs->toAbsolutePath($input->getArgument('script'));
42 | if (file_exists($postScript)) {
43 | $output->getErrorOutput()->writeln("WARNING: Loaded script relative to CMS root -- which is deprecated. Script path should be (a) absolute or (b) relative to CWD.");
44 | chdir($postCwd);
45 | $this->runScript($output, $postScript, $scriptArguments);
46 | return 0;
47 | }
48 |
49 | $output->getErrorOutput()->writeln("Failed to locate script: " . $input->getArgument('script') . "");
50 | return 1;
51 | }
52 |
53 | /**
54 | * @param \Symfony\Component\Console\Output\OutputInterface $output
55 | * @param string $script
56 | * @param array $scriptArguments
57 | */
58 | protected function runScript(OutputInterface $output, string $script, array $scriptArguments = []) {
59 | $output->writeln("[ScriptCommand] Start \"$script\"", OutputInterface::VERBOSITY_DEBUG);
60 | // This puts the script arguments in the same variable scope as the script
61 | // so scripts can access arguments using $argv $argc
62 | $argv = $scriptArguments;
63 | array_unshift($argv, $script);
64 | $argc = count($argv);
65 | // This prevents the script stomping on any variables it shouldn't - like $output
66 | $run = function () use ($argv, $argc, $script) {
67 | require $script;
68 | };
69 | $run();
70 | $output->writeln("[ScriptCommand] Finish \"$script\"", OutputInterface::VERBOSITY_DEBUG);
71 | }
72 |
73 | }
74 |
--------------------------------------------------------------------------------
/src/Command/ShowCommand.php:
--------------------------------------------------------------------------------
1 | setName('vars:show')
16 | ->setDescription('Show the configuration of the local CiviCRM installation')
17 | ->configureOutputOptions();
18 | }
19 |
20 | protected function execute(InputInterface $input, OutputInterface $output): int {
21 | $reader = new SiteConfigReader(CIVICRM_SETTINGS_PATH);
22 | $data = $reader->compile(array('buildkit', 'home', 'active'));
23 | $this->sendResult($input, $output, $data);
24 | return 0;
25 | }
26 |
27 | }
28 |
--------------------------------------------------------------------------------
/src/Command/UpgradeCommand.php:
--------------------------------------------------------------------------------
1 | setName('upgrade')
16 | ->setDescription('Download CiviCRM and upgrade the site')
17 | ->addOption('in', NULL, InputOption::VALUE_REQUIRED, 'Input format (args,json)', 'args')
18 | ->configureOutputOptions()
19 | ->addOption('stability', 's', InputOption::VALUE_REQUIRED, 'Specify the stability of the version to get (nightly, rc, stable)', 'stable')
20 | ->setHelp('Download CiviCRM, extract in place, and upgrade the site, notifying civicrm.org
21 | Examples:
22 | cv upgrade --stability=rc
23 | ');
24 | // parent::configureBootOptions();
25 | }
26 |
27 | protected function execute(InputInterface $input, OutputInterface $output): int {
28 |
29 | throw new \RuntimeException("FIXME: Calls to run() should be escaped, e.g. with Process::sprintf()");
30 |
31 | $exitCode = 0;
32 | $stage = 'Start';
33 | $result = array();
34 | $reportName = \Civi\Cv\Util\Rand::createName();
35 | try {
36 | $stability = $input->getOption('stability');
37 | $stage = 'Lookup';
38 | $dl = \Civi\Cv\Util\Cv::run("upgrade:get --stability=$stability");
39 | $result['dl-data'] = $dl;
40 | if (!empty($dl['error'])) {
41 | throw new \RuntimeException($dl['error'], 1);
42 | }
43 | $stage = 'Start report';
44 | $startReport = \Civi\Cv\Util\Cv::run("upgrade:report --started --revision={$dl['rev']} --name=$reportName");
45 | $stage = 'Download and extract';
46 | $extract = \Civi\Cv\Util\Cv::run("upgrade:dl --url={$dl['url']}");
47 | $result['extract-data'] = $extract;
48 | $stage = 'Download report';
49 | $dlReport = \Civi\Cv\Util\Cv::run("upgrade:report --downloaded --download-url {$dl['url']} --extracted --name $reportName");
50 | $stage = 'Database upgrade';
51 | $db = \Civi\Cv\Util\Cv::run("upgrade:db");
52 | $result['db-upgrade'] = $db;
53 | $messageFile = sys_get_temp_dir() . "/upgrademessages-$reportName.html";
54 | file_put_contents($messageFile, $db['message']);
55 | $stage = 'Upgrade report';
56 | $finishReport = \Civi\Cv\Util\Cv::run("upgrade:report --upgraded --upgrade-messages $messageFile --name $reportName");
57 |
58 | // clean up
59 | $stage = 'Cleanup';
60 | unlink($messageFile);
61 | }
62 | catch (\RuntimeException $e) {
63 | $exitCode = 1;
64 | $result['error-stage'] = $stage;
65 | $result['error-message'] = $e->getMessage();
66 | $problem = "$stage problem: {$result['error_message']}";
67 | $result['fail-report'] = \Civi\Cv\Util\Cv::run("upgrade:report --failed --problem-message '$problem' --name $reportName");
68 | }
69 |
70 | $this->sendResult($input, $output, $result);
71 | return $exitCode;
72 | }
73 |
74 | }
75 |
--------------------------------------------------------------------------------
/src/Command/UpgradeGetCommand.php:
--------------------------------------------------------------------------------
1 | setName('upgrade:get')
24 | ->setDescription('Find out what file you should use to upgrade')
25 | ->configureOutputOptions()
26 | ->addOption('stability', 's', InputOption::VALUE_REQUIRED, 'Specify the stability of the version to get (nightly, rc, stable)', 'stable')
27 | ->addOption('cms', 'c', InputOption::VALUE_REQUIRED, 'Specify the CMS to get (Backdrop, Drupal, Drupal6, Joomla, WordPress) instead of the current site')
28 | ->setHelp('Find out what file you should use to upgrade
29 |
30 | Examples:
31 | cv upgrade:get --stability=rc
32 |
33 | Returns a JSON object with the properties:
34 | rev a unique ID corresponding to the commits that are included
35 | path the path to download a tarball/zipfile
36 | git the corresponding commits of the civicrm repos
37 | vars the site variables from cv vars:show
38 | error only appears if there is an error
39 | ');
40 | }
41 |
42 | public function getBootOptions(): array {
43 | return ['auto' => FALSE] + parent::getBootOptions();
44 | }
45 |
46 | protected function execute(InputInterface $input, OutputInterface $output): int {
47 | $result = array();
48 | $exitCode = 0;
49 | $stability = $input->getOption('stability');
50 | $cms = $input->getOption('cms');
51 | if (empty($cms)) {
52 | $this->boot($input, $output);
53 | if (defined('CIVICRM_UF')) {
54 | $cms = CIVICRM_UF;
55 | }
56 | // REMOVE:
57 | $result['vars'] = $GLOBALS['_CV'];
58 | }
59 | if (empty($cms)) {
60 | throw new \RuntimeException("Cannot determine download URL without CMS");
61 | }
62 |
63 | $url = self::DEFAULT_CHECK_URL . "?stability=" . urlencode($stability);
64 | $lookup = json_decode(file_get_contents($url), TRUE);
65 |
66 | if (empty($lookup)) {
67 | $result = array(
68 | 'error' => "Version not found at $url",
69 | );
70 | $exitCode = 1;
71 | }
72 | else {
73 | if (array_key_exists('rev', $lookup)) {
74 | $result['rev'] = $lookup['rev'];
75 | }
76 | if (array_key_exists('version', $lookup)) {
77 | $result['version'] = $lookup['version'];
78 | }
79 | if (array_key_exists('git', $lookup)) {
80 | $result['git'] = $lookup['git'];
81 | }
82 | if (!empty($lookup['tar'][$cms])) {
83 | $result['url'] = $lookup['tar'][$cms];
84 | }
85 | }
86 |
87 | $this->sendResult($input, $output, $result);
88 | return $exitCode;
89 | }
90 |
91 | }
92 |
--------------------------------------------------------------------------------
/src/Encoder.php:
--------------------------------------------------------------------------------
1 | $v) {
90 | $buf .= (sprintf("%s=%s\n", $k, escapeshellarg($v)));
91 | }
92 | return $buf;
93 | }
94 | else {
95 | return gettype($data);
96 | }
97 |
98 | case 'civicrm.settings.php':
99 | // SpecialFormat: Blurbs for civicrm.settings.php. Used by `setting:get` command.
100 | $buf = '';
101 | foreach ($data as $row) {
102 | if (!isset($row['scope']) || !isset($row['key']) || !isset($row['value'])) {
103 | throw new \RuntimeException("Cannot format result for civicrm.settings.php. Must have columns: scope,key,value");
104 | }
105 | $scope = strpos($row['scope'], 'contact') !== FALSE ? 'contact' : 'domain';
106 | $buf .= sprintf("\$civicrm_setting[%s][%s] = %s;\n",
107 | var_export($scope, TRUE),
108 | var_export($row['key'], TRUE),
109 | var_export($row['value'], TRUE)
110 | );
111 | }
112 | return $buf;
113 |
114 | default:
115 | throw new \RuntimeException('Unknown output format');
116 | }
117 | }
118 |
119 | private static function preferArray($data) {
120 | if (is_object($data)) {
121 | if ($data instanceof \JsonSerializable || $data instanceof \stdClass) {
122 | $data = json_decode(json_encode($data), TRUE);
123 | }
124 | }
125 | return $data;
126 | }
127 |
128 | }
129 |
--------------------------------------------------------------------------------
/src/Exception/ProcessErrorException.php:
--------------------------------------------------------------------------------
1 | process = $process;
12 | if (empty($message)) {
13 | $message = $this->createReport($process);
14 | }
15 | parent::__construct($message, $code, $previous);
16 | }
17 |
18 | /**
19 | * @param \Symfony\Component\Process\Process $process
20 | */
21 | public function setProcess($process) {
22 | $this->process = $process;
23 | }
24 |
25 | /**
26 | * @return \Symfony\Component\Process\Process
27 | */
28 | public function getProcess() {
29 | return $this->process;
30 | }
31 |
32 | public function createReport($process) {
33 | return "Process failed:
34 | [[ COMMAND: {$process->getCommandLine()} ]]
35 | [[ CWD: {$process->getWorkingDirectory()} ]]
36 | [[ EXIT CODE: {$process->getExitCode()} ]]
37 | [[ STDOUT ]]
38 | {$process->getOutput()}
39 | [[ STDERR ]]
40 | {$process->getErrorOutput()}
41 | ";
42 | }
43 |
44 | }
45 |
--------------------------------------------------------------------------------
/src/Exception/QueueTaskException.php:
--------------------------------------------------------------------------------
1 | applyOption($params, $state, $arg);
20 | $state = '_TOP_';
21 | }
22 | // Ex: 'foo=bar', 'fo.oo=bar', 'fo:oo=bar'
23 | elseif (preg_match('/^([a-zA-Z0-9_:\.]+)=(.*)/', $arg, $matches)) {
24 | [, $key, $value] = $matches;
25 | $params[$key] = $this->parseValueExpr($value);
26 | }
27 | // Ex: '+w', '+where'
28 | elseif (preg_match('/^\+([a-zA-Z0-9_]+)$/', $arg, $matches)) {
29 | $state = $matches[1];
30 | }
31 | // Ex: '+l=2', '+l:2'
32 | elseif (preg_match('/^\+([a-zA-Z0-9_]+)[:=](.*)/', $arg, $matches)) {
33 | [, $key, $expr] = $matches;
34 | $this->applyOption($params, $key, $expr);
35 | }
36 | // Ex: '{"foo": "bar"}'
37 | elseif (preg_match('/^\{.*\}$/', $arg)) {
38 | $params = array_merge($params, $this->parseJsonNoisily($arg));
39 | }
40 | else {
41 | throw new \RuntimeException("Unrecognized option format: $arg");
42 | }
43 | }
44 | return $params;
45 | }
46 |
47 | protected static function mergeInto(&$params, $key, $values) {
48 | if (!isset($params[$key])) {
49 | $params[$key] = [];
50 | }
51 | $params[$key] = array_merge($params[$key], $values);
52 | }
53 |
54 | protected static function appendInto(&$params, $key, $values) {
55 | if (!isset($params[$key])) {
56 | $params[$key] = [];
57 | }
58 | $params[$key][] = $values;
59 | }
60 |
61 | /**
62 | * @param string $arg
63 | * @return mixed
64 | */
65 | protected function parseJsonNoisily($arg) {
66 | $values = json_decode($arg, 1);
67 | if ($values === NULL) {
68 | throw new \RuntimeException("Failed to parse JSON: $values");
69 | }
70 | return $values;
71 | }
72 |
73 | /**
74 | * @param $expr
75 | * @return mixed
76 | */
77 | protected function parseValueExpr($expr) {
78 | if ($expr !== '' && strpos('{["\'', $expr[0]) !== FALSE) {
79 | return $this->parseJsonNoisily($expr);
80 | }
81 | else {
82 | return $expr;
83 | }
84 | }
85 |
86 | /**
87 | * @param array $params
88 | * @param string $type
89 | * The name of the plus parameter.
90 | * Ex: "+s foo" ==> "s"
91 | * Ex: "+where foo=bar" ==> "where"
92 | * @param string $expr
93 | * Ex: "+s foo" ==> "foo"
94 | * Ex: "+where foo=bar" ==> "foo=bar"
95 | *
96 | * @return mixed
97 | */
98 | abstract protected function applyOption(array &$params, string $type, string $expr): void;
99 |
100 | public function parseAssignment($expr) {
101 | if (preg_match('/^([a-zA-Z0-9_:\.]+)\s*=\s*(.*)$/', $expr, $matches)) {
102 | return [$matches[1] => $this->parseValueExpr($matches[2])];
103 | }
104 | else {
105 | throw new \RuntimeException("Error parsing \"value\": $expr");
106 | }
107 | }
108 |
109 | }
110 |
--------------------------------------------------------------------------------
/src/Util/Api4ArgParser.php:
--------------------------------------------------------------------------------
1 | operators = implode('|', ArrayUtil::map(\Civi\Api4\Utils\CoreUtil::getOperators(), function($op) {
16 | return preg_quote($op, '/');
17 | }));
18 | }
19 | else {
20 | $this->operators = '\<=|\>=|=|!=|\<|\>|IS NULL|IS NOT NULL|IS EMPTY|IS NOT EMPTY|LIKE|NOT LIKE|IN|NOT IN|CONTAINS|NOT CONTAINS|REGEXP|NOT REGEXP|REGEXP BINARY|NOT REGEXP BINARY';
21 | }
22 | }
23 |
24 | /**
25 | * @param array $params
26 | * @param string $type
27 | * @param string $expr
28 | *
29 | * @return mixed
30 | */
31 | protected function applyOption(array &$params, string $type, string $expr): void {
32 | $aliases = [
33 | 's' => 'select',
34 | 'w' => 'where',
35 | 'o' => 'orderBy',
36 | 'l' => 'limit',
37 | 'v' => 'values',
38 | 'value' => 'values',
39 | ];
40 | $type = $aliases[$type] ?? $type;
41 |
42 | switch ($type) {
43 | case 'select':
44 | self::mergeInto($params, $type, array_map([
45 | $this,
46 | 'parseValueExpr',
47 | ], preg_split('/[, ]/', $expr)));
48 | break;
49 |
50 | case 'where':
51 | self::appendInto($params, $type, $this->parseWhere($expr));
52 | break;
53 |
54 | case 'orderBy':
55 | $keyOrderPairs = explode(',', $expr);
56 | foreach ($keyOrderPairs as $keyOrderPair) {
57 | $keyOrderPair = explode(' ', trim($keyOrderPair));
58 | $sortKey = $keyOrderPair[0];
59 | $sortOrder = isset($keyOrderPair[1]) ? strtoupper($keyOrderPair[1]) : 'ASC';
60 | $params[$type][$sortKey] = $sortOrder;
61 | }
62 | break;
63 |
64 | case 'limit':
65 | if (strpos($expr, '@') !== FALSE) {
66 | [$limit, $offset] = explode('@', $expr);
67 | $params['limit'] = (int) $limit;
68 | $params['offset'] = (int) $offset;
69 | }
70 | else {
71 | $params['limit'] = (int) $expr;
72 | }
73 | break;
74 |
75 | case 'values':
76 | self::mergeInto($params, $type, $this->parseAssignment($expr));
77 | break;
78 |
79 | default:
80 | throw new \RuntimeException("Unrecognized option: +$type");
81 | }
82 | }
83 |
84 | public function parseWhere($expr) {
85 | if (preg_match('/^([a-zA-Z0-9_:\.]+)\s*(' . $this->operators . ')\s*(.*)$/i', $expr, $matches)) {
86 | if (!empty($matches[3])) {
87 | return [$matches[1], strtoupper(trim($matches[2])), $this->parseValueExpr(trim($matches[3]))];
88 | }
89 | else {
90 | return [$matches[1], strtoupper($matches[2])];
91 | }
92 | }
93 | else {
94 | throw new \RuntimeException("Error parsing \"where\": $expr");
95 | }
96 | }
97 |
98 | }
99 |
--------------------------------------------------------------------------------
/src/Util/CliEditor.php:
--------------------------------------------------------------------------------
1 | editFile($tmpFile, $attempts)) {
28 | unlink($tmpFile);
29 | return NULL;
30 | }
31 |
32 | $buffer = file_get_contents($tmpFile);
33 | unlink($tmpFile);
34 | return $buffer;
35 | }
36 |
37 | /**
38 | * Open the editor with a given file.
39 | *
40 | * @param string $file
41 | * @param int $maxAttempts
42 | * @return bool
43 | * TRUE on success.
44 | */
45 | public function editFile($file, $maxAttempts = 2) {
46 | $attempt = 0;
47 | do {
48 | $attempt++;
49 | $cmd = $this->pick();
50 | if (!$cmd) {
51 | return FALSE;
52 | }
53 | passthru("$cmd " . escapeshellarg($file), $return);
54 | if (!$return !== 0) {
55 | return FALSE;
56 | }
57 |
58 | if ($this->validator === NULL) {
59 | $isValid = TRUE;
60 | }
61 | else {
62 | list ($isValid, $message) = call_user_func($this->validator, $file);
63 | if (!$isValid && $attempt >= $maxAttempts) {
64 | return FALSE;
65 | }
66 | if (!$isValid && $message) {
67 | file_put_contents($file, $message . file_get_contents($file));
68 | }
69 | }
70 |
71 | } while (!$isValid);
72 | return TRUE;
73 | }
74 |
75 | /**
76 | * Determine the name of the editor.
77 | *
78 | * @return string|NULL
79 | */
80 | public function pick() {
81 | if (getenv('VISUAL') && $this->findCommand(getenv('VISUAL'))) {
82 | return getenv('VISUAL');
83 | }
84 | elseif (getenv('EDITOR') && $this->findCommand(getenv('EDITOR'))) {
85 | return getenv('EDITOR');
86 | }
87 | elseif ($this->findCommand('editor')) {
88 | return 'editor';
89 | }
90 | elseif ($this->findCommand('vi')) {
91 | return 'vi';
92 | }
93 | else {
94 | return NULL;
95 | }
96 | }
97 |
98 | protected function findCommand($name) {
99 | return Process::findCommand($name);
100 | }
101 |
102 | /**
103 | * @return callable|NULL
104 | */
105 | public function getValidator() {
106 | return $this->validator;
107 | }
108 |
109 | /**
110 | * @param callable|NULL $validator
111 | */
112 | public function setValidator($validator) {
113 | $this->validator = $validator;
114 | }
115 |
116 | }
117 |
--------------------------------------------------------------------------------
/src/Util/ConsoleQueueRunner.php:
--------------------------------------------------------------------------------
1 | io = $io;
45 | $this->queue = $queue;
46 | $this->dryRun = $dryRun;
47 | $this->step = (bool) $step;
48 | }
49 |
50 | /**
51 | * @throws \Exception
52 | */
53 | public function runAll() {
54 | /** @var \Symfony\Component\Console\Style\SymfonyStyle $io */
55 | $io = $this->io;
56 |
57 | $taskCtx = new \CRM_Queue_TaskContext();
58 | $taskCtx->queue = $this->queue;
59 | // WISHLIST: Wrap $output
60 | $taskCtx->log = \Log::singleton('display');
61 | // CRM_Core_Error::createDebugLogger()
62 |
63 | while ($this->queue->numberOfItems()) {
64 | // In case we're retrying a failed job.
65 | $item = $this->queue->stealItem();
66 | $task = $item->data;
67 |
68 | if ($io->getVerbosity() === OutputInterface::VERBOSITY_NORMAL) {
69 | // Symfony progress bar would be prettier, but (when last checked) they didn't allow
70 | // resetting when the queue-length expands dynamically.
71 | $io->write(".");
72 | }
73 | elseif ($io->getVerbosity() === OutputInterface::VERBOSITY_VERBOSE) {
74 | $io->writeln(sprintf("%s", $task->title));
75 | }
76 | elseif ($io->getVerbosity() > OutputInterface::VERBOSITY_VERBOSE) {
77 | $io->writeln(sprintf("%s (%s)", $task->title, self::formatTaskCallback($task)));
78 | }
79 |
80 | $action = 'y';
81 | if ($this->step) {
82 | $action = $io->choice('Execute this step?', ['y' => 'yes', 's' => 'skip', 'a' => 'abort'], 'y');
83 | }
84 | if ($action === 'a') {
85 | throw new \Exception('Aborted');
86 | }
87 |
88 | if ($action === 'y' && !$this->dryRun) {
89 | try {
90 | $isOK = $task->run($taskCtx);
91 | if (!$isOK) {
92 | throw new QueueTaskException('Task returned false');
93 | }
94 | }
95 | catch (\Throwable $e) {
96 | // WISHLIST: For interactive mode, perhaps allow retry/skip?
97 | $io->writeln(sprintf("Error executing task: %s", $task->title));
98 | throw $e;
99 | }
100 | }
101 |
102 | $this->queue->deleteItem($item);
103 | }
104 |
105 | if ($io->getVerbosity() === OutputInterface::VERBOSITY_NORMAL) {
106 | $io->newLine();
107 | }
108 | }
109 |
110 | protected static function formatTaskCallback(\CRM_Queue_Task $task) {
111 | $cb = implode('::', (array) $task->callback);
112 | $args = json_encode($task->arguments, JSON_UNESCAPED_SLASHES);
113 | return sprintf("%s(%s)", $cb, substr($args, 1, -1));
114 | }
115 |
116 | }
117 |
--------------------------------------------------------------------------------
/src/Util/DebugDispatcherTrait.php:
--------------------------------------------------------------------------------
1 | getListeners();
23 | }
24 | elseif ($eventFilter[0] === '/') {
25 | $listenersByEvent = array();
26 | foreach ($dispatcher->getListeners() as $e => $ls) {
27 | if (preg_match($eventFilter, $e)) {
28 | $listenersByEvent[$e] = $ls;
29 | }
30 | }
31 | }
32 | else {
33 | $listenersByEvent = array($eventFilter => $dispatcher->getListeners($eventFilter));
34 | }
35 |
36 | $eventNames = array_keys($listenersByEvent);
37 | sort($eventNames);
38 | return $eventNames;
39 | }
40 |
41 | /**
42 | * @param \Symfony\Component\Console\Output\OutputInterface $output
43 | * @param \Symfony\Component\EventDispatcher\EventDispatcherInterface $dispatcher
44 | * @param array $eventNames
45 | */
46 | public function printEventListeners(OutputInterface $output, $dispatcher, $eventNames) {
47 | $fmt = class_exists('\Civi\Core\Event\EventPrinter')
48 | ? ['\Civi\Core\Event\EventPrinter', 'formatName'] : NULL;
49 |
50 | foreach ($eventNames as $event) {
51 | $rows = array();
52 | $i = 0;
53 | foreach ($dispatcher->getListeners($event) as $listener) {
54 | $handled = FALSE;
55 | if ($fmt != NULL) {
56 | $rows[] = array('#' . ++$i, $fmt($listener));
57 | $handled = TRUE;
58 | }
59 | elseif (is_array($listener)) {
60 | list ($a, $b) = $listener;
61 | if (is_object($a)) {
62 | $rows[] = array('#' . ++$i, get_class($a) . "->$b()");
63 | $handled = TRUE;
64 | }
65 | elseif (is_string($a)) {
66 | $rows[] = array('#' . ++$i, "$a::$b()");
67 | $handled = TRUE;
68 | }
69 | }
70 | elseif (is_string($listener)) {
71 | $handled = TRUE;
72 | $rows[] = array('#' . ++$i, $listener . '()');
73 | }
74 | elseif ($listener instanceof \Civi\Core\Event\ServiceListener) {
75 | $handled = TRUE;
76 | $rows[] = ['#' . ++$i, (string) $listener];
77 | }
78 | else {
79 | try {
80 | $f = new \ReflectionFunction($listener);
81 | $rows[] = array(
82 | '#' . ++$i,
83 | 'closure(' . $f->getFileName() . '@' . $f->getStartLine() . ')',
84 | );
85 | $handled = TRUE;
86 | }
87 | catch (\ReflectionException $e) {
88 | }
89 | }
90 |
91 | if (!$handled) {
92 | $rows[] = array('#' . ++$i, "unidentified");
93 | }
94 | }
95 | $output->writeln("[Event] $event");
96 | $table = new Table($output);
97 | $table->setHeaders(array('Order', 'Callable'));
98 | $table->addRows($rows);
99 | $table->render();
100 | $output->writeln("");
101 | }
102 | }
103 |
104 | }
105 |
--------------------------------------------------------------------------------
/src/Util/ExtensionTrait.php:
--------------------------------------------------------------------------------
1 | addOption('dev', NULL, InputOption::VALUE_NONE, 'Include developmental extensions. (Equivalent to "--filter-status=* --filter-ready=*")')
16 | ->addOption('filter-ver', NULL, InputOption::VALUE_REQUIRED, 'Filter remote extensions by Civi compatibility (Ex: "4.7.15","4.6.20")', '{ver}')
17 | ->addOption('filter-uf', NULL, InputOption::VALUE_REQUIRED, 'Filter remote extensions by CMS compatibility (Ex: "Drupal", "WordPress")', '{uf}')
18 | ->addOption('filter-status', NULL, InputOption::VALUE_REQUIRED, 'Filter remote extensions by stability flag (Ex: "stable", "*")', 'stable')
19 | ->addOption('filter-ready', NULL, InputOption::VALUE_REQUIRED, 'Filter remote extensions based on reviewers\' approval (Ex: "ready", "*")', 'ready');
20 | }
21 |
22 | /**
23 | * Create a URL for the extension feed (based on user options).
24 | *
25 | * @param \Symfony\Component\Console\Input\InputInterface $input
26 | * @return string|NULL
27 | */
28 | public function parseRepoUrl(InputInterface $input) {
29 | if ($input->getOption('dev')) {
30 | $input->setOption('filter-status', '*');
31 | $input->setOption('filter-ready', '*');
32 | }
33 | $parts = array();
34 | foreach (array('ver', 'uf', 'status', 'ready') as $key) {
35 | $value = $input->getOption("filter-" . $key);
36 | if ($value === '*') {
37 | $value = '';
38 | }
39 | $parts[] = $key . '=' . $value;
40 | }
41 | return 'https://civicrm.org/extdir/' . implode('|', $parts);
42 | }
43 |
44 | /**
45 | * @param \Symfony\Component\Console\Input\InputInterface $input
46 | * @param Symfony\Component\Console\Output\OutputInterface $output
47 | * @return array
48 | * Array(0=>$keys, 1=>$errors).
49 | */
50 | protected function parseKeys(InputInterface $input, OutputInterface $output) {
51 | $allKeys = \CRM_Extension_System::singleton()->getFullContainer()->getKeys();
52 | $foundKeys = array();
53 | $missingKeys = array();
54 | $shortMap = NULL;
55 |
56 | foreach ($input->getArgument('key-or-name') as $keyOrName) {
57 | if (in_array($keyOrName, $allKeys)) {
58 | $foundKeys[] = $keyOrName;
59 | continue;
60 | }
61 |
62 | if ($shortMap === NULL) {
63 | $shortMap = $this->getShortMap();
64 | }
65 | if (isset($shortMap[$keyOrName])) {
66 | $foundKeys = array_merge($foundKeys, $shortMap[$keyOrName]);
67 | continue;
68 | }
69 |
70 | $missingKeys[] = $keyOrName;
71 | }
72 |
73 | return array($foundKeys, $missingKeys);
74 | }
75 |
76 | /**
77 | * @return array
78 | * Array(string $shortName => string $longName).
79 | */
80 | protected function getShortMap() {
81 | $map = array();
82 | $mapper = \CRM_Extension_System::singleton()->getMapper();
83 | $container = \CRM_Extension_System::singleton()->getFullContainer();
84 |
85 | foreach ($container->getKeys() as $key) {
86 | $info = $mapper->keyToInfo($key);
87 | if ($info->file) {
88 | $map[$info->file][] = $key;
89 | }
90 | }
91 | return $map;
92 | }
93 |
94 | }
95 |
--------------------------------------------------------------------------------
/src/Util/HeadlessDownloader.php:
--------------------------------------------------------------------------------
1 | exists($outputDir) && !$force) {
25 | throw new \RuntimeException("Directory already exists: $outputDir");
26 | }
27 |
28 | $tmpDir = $outputDir . ".tmp";
29 | if ($fs->exists($tmpDir) && !$force) {
30 | throw new \RuntimeException("Directory already exists: $tmpDir");
31 | }
32 |
33 | $zipFile = tempnam(sys_get_temp_dir(), 'extdl-') . '.zip';
34 | if ($fs->exists($tmpDir) && $force) {
35 | $fs->remove($tmpDir);
36 | }
37 | $this->download($zipUrl, $zipFile);
38 | $extractedZipPath = $this->extractZip($zipFile, $extKey, $tmpDir);
39 | if ($fs->exists($outputDir) && $force) {
40 | $fs->remove($outputDir);
41 | }
42 | rename($extractedZipPath, $outputDir);
43 | rmdir($tmpDir);
44 | unlink($zipFile);
45 | }
46 |
47 | /**
48 | * Determine the name.
49 | *
50 | * @param \ZipArchive $zip
51 | * @return array
52 | */
53 | public function findBaseDirs(ZipArchive $zip) {
54 | $cnt = $zip->numFiles;
55 | $basedirs = array();
56 |
57 | for ($i = 0; $i < $cnt; $i++) {
58 | $filename = $zip->getNameIndex($i);
59 | // hypothetically, ./ or ../ would not be legit here
60 | if (preg_match('/^[^\/]+\/$/', $filename) && $filename != './' && $filename != '../') {
61 | $basedirs[] = rtrim($filename, '/');
62 | }
63 | }
64 |
65 | return $basedirs;
66 | }
67 |
68 | public function guessBasedir(ZipArchive $zip, $expected) {
69 | $candidate = FALSE;
70 | $basedirs = $this->findBaseDirs($zip);
71 | if (in_array($expected, $basedirs)) {
72 | $candidate = $expected;
73 | }
74 | elseif (count($basedirs) == 1) {
75 | $candidate = array_shift($basedirs);
76 | }
77 | if ($candidate !== FALSE && preg_match('/^[a-zA-Z0-9]/', $candidate)) {
78 | return $candidate;
79 | }
80 | else {
81 | return FALSE;
82 | }
83 | }
84 |
85 | public function extractZip($zipFile, $key, $tmpDir) {
86 | $zip = new ZipArchive();
87 | $res = $zip->open($zipFile);
88 | if ($res === TRUE) {
89 | $zipSubDir = $this->guessBasedir($zip, $key);
90 | if ($zipSubDir === FALSE) {
91 | throw new \Exception('Unable to extract the extension: bad directory structure');
92 | }
93 | $extractedZipPath = $tmpDir . DIRECTORY_SEPARATOR . $zipSubDir;
94 | if (is_dir($extractedZipPath)) {
95 | throw new \Exception("$extractedZipPath already exists");
96 | }
97 | if (!is_dir($tmpDir)) {
98 | mkdir($tmpDir, 0777, TRUE);
99 | }
100 | if (!$zip->extractTo($tmpDir)) {
101 | throw new \Exception("Unable to extract the extension to $tmpDir.");
102 | }
103 | $zip->close();
104 | return $extractedZipPath;
105 | }
106 | else {
107 | throw new \Exception('Unable to extract the extension.');
108 | }
109 | }
110 |
111 | public function download($url, $file) {
112 | $fp = fopen($file, 'w');
113 | $ch = curl_init($url);
114 | curl_setopt($ch, CURLOPT_FILE, $fp);
115 | curl_setopt($ch, CURLOPT_FOLLOWLOCATION, TRUE);
116 | curl_exec($ch);
117 | curl_close($ch);
118 | fclose($fp);
119 | }
120 |
121 | }
122 |
--------------------------------------------------------------------------------
/src/Util/OptionCallbackTrait.php:
--------------------------------------------------------------------------------
1 | addOption('foo', ...)
15 | * ->addOptionCallback('foo', function(InputInterface $input, OutputInterface $output, InputOption $option) { ... })
16 | */
17 | trait OptionCallbackTrait {
18 |
19 | /**
20 | * @return \Symfony\Component\Console\Input\InputDefinition
21 | * An InputDefinition instance
22 | * @see \Symfony\Component\Console\Command\Command::getDefinition()
23 | */
24 | abstract public function getDefinition();
25 |
26 | /**
27 | * @var array
28 | * Array([string $optionName, callable $callback]).
29 | */
30 | private $optionCallbacks = [];
31 |
32 | /**
33 | * @param string $name
34 | * The name of the option to
35 | * @param callable $callback
36 | * Function(InputInterface $input, OutputInterface, $output, string $optionName)
37 | * @return $this
38 | */
39 | public function addOptionCallback($name, $callback) {
40 | $this->optionCallbacks[] = [$name, $callback];
41 | return $this;
42 | }
43 |
44 | /**
45 | * @param \Symfony\Component\Console\Input\InputInterface $input
46 | * @param \Symfony\Component\Console\Output\OutputInterface $output
47 | * @return $this
48 | */
49 | protected function runOptionCallbacks(InputInterface $input, OutputInterface $output) {
50 | $defn = $this->getDefinition();
51 |
52 | foreach ($this->optionCallbacks as $optionCallbackDefn) {
53 | list ($optionName, $callback) = $optionCallbackDefn;
54 | if ($defn->hasOption($optionName)) {
55 | $callback($input, $output, $defn->getOption($optionName));
56 | }
57 | }
58 |
59 | return $this;
60 | }
61 |
62 | }
63 |
--------------------------------------------------------------------------------
/src/Util/PsrLogger.php:
--------------------------------------------------------------------------------
1 | internalLog = $internalLog;
17 | }
18 |
19 | public function log($level, $message, array $context = array()): void {
20 | $this->internalLog->log($level, $message, $context);
21 | }
22 |
23 | }
24 |
--------------------------------------------------------------------------------
/src/Util/Rand.php:
--------------------------------------------------------------------------------
1 | prefixes = [
13 | '[civicrm.root]/' => \Civi::paths()->getPath('[civicrm.root]/'),
14 | '[civicrm.packages]/' => \Civi::paths()->getPath('[civicrm.packages]/'),
15 | '[civicrm.files]/' => \Civi::paths()->getPath('[civicrm.files]/'),
16 | '[cms.root]/' => \Civi::paths()->getPath('[cms.root]/'),
17 | ];
18 | }
19 |
20 | /**
21 | * @param string $path
22 | * Ex: '/var/www/sites/all/modules/civicrm/CRM/Foo.php
23 | * @return string
24 | * Ex: '[civicrm.root]/CRM/Foo.php''
25 | */
26 | public function filter(string $path): string {
27 | foreach ($this->prefixes as $prefix => $prefixPath) {
28 | if (\CRM_Utils_File::isChildPath($prefixPath, $path)) {
29 | return $prefix . mb_substr($path, mb_strlen($prefixPath));
30 | }
31 | }
32 | return $path;
33 | }
34 |
35 | }
36 |
--------------------------------------------------------------------------------
/src/Util/SettingCodec.php:
--------------------------------------------------------------------------------
1 | writeln("Calling $entity $action API", OutputInterface::VERBOSITY_DEBUG);
27 | $result = \civicrm_api($entity, $action, $params);
28 | if (!empty($result['is_error']) || $output->isDebug()) {
29 | $data = array(
30 | 'entity' => $entity,
31 | 'action' => $action,
32 | 'params' => $params,
33 | 'result' => $result,
34 | );
35 | if (!empty($result['is_error'])) {
36 | \Civi\Cv\Cv::errorOutput()->writeln("Error: API Call Failed: "
37 | . Encoder::encode($data, 'pretty'));
38 | }
39 | else {
40 | $output->writeln("API success" . Encoder::encode($data, 'pretty'),
41 | OutputInterface::VERBOSITY_DEBUG);
42 | }
43 | }
44 | return $result;
45 | }
46 |
47 | }
48 |
--------------------------------------------------------------------------------
/tests/CivilTestCase.php:
--------------------------------------------------------------------------------
1 | originalCwd = getcwd();
24 | chdir($this->getExampleDir());
25 | $this->cv = dirname(__DIR__) . '/bin/cv';
26 | }
27 |
28 | public function tearDown(): void {
29 | chdir($this->originalCwd);
30 | }
31 |
32 | public function getExampleDir() {
33 | $dir = getenv('CV_TEST_BUILD');
34 | if (empty($dir)) {
35 | throw new \RuntimeException('Environment variable CV_TEST_BUILD must point to a civicrm-cms build');
36 | }
37 | return $dir;
38 | }
39 |
40 | /**
41 | * Create a helper for executing command-tests in our application.
42 | *
43 | * @param array $args must include key "command"
44 | * @return \Symfony\Component\Console\Tester\CommandTester
45 | */
46 | public function createCommandTester($args) {
47 | if (!isset($args['command'])) {
48 | throw new \RuntimeException("Missing mandatory argument: command");
49 | }
50 | $application = new Application();
51 | $command = $application->find($args['command']);
52 | $commandTester = new CommandTester($command);
53 | $commandTester->execute($args);
54 | return $commandTester;
55 | }
56 |
57 | }
58 |
--------------------------------------------------------------------------------
/tests/Command/AngularHtmlListCommandTest.php:
--------------------------------------------------------------------------------
1 | cv('ang:html:list'));
17 | $this->assertMatchesRegularExpression(';crmUi/field.html;', $p->getOutput());
18 |
19 | // matches key
20 | $p = Process::runOk($this->cv('ang:html:list crmUi'));
21 | $this->assertMatchesRegularExpression(';crmUi/field.html;', $p->getOutput());
22 |
23 | // matches name
24 | $p = Process::runOk($this->cv('ang:html:list ";field;"'));
25 | $this->assertMatchesRegularExpression(';crmUi/field.html;', $p->getOutput());
26 |
27 | // matches name
28 | $p = Process::runOk($this->cv('ang:html:list crmAttachment'));
29 | $this->assertDoesNotMatchRegularExpression(';crmUi/field.html;', $p->getOutput());
30 | }
31 |
32 | /**
33 | * Get the extension data in an alternate format, eg JSON.
34 | */
35 | public function testGetJson() {
36 | $p = Process::runOk($this->cv('ang:html:list crmUi/field.html --out=json'));
37 | $data = json_decode($p->getOutput(), 1);
38 | $this->assertEquals(1, count($data));
39 | $this->assertEquals('crmUi/field.html', $data[0]['file']);
40 | }
41 |
42 | }
43 |
--------------------------------------------------------------------------------
/tests/Command/AngularHtmlShowCommandTest.php:
--------------------------------------------------------------------------------
1 | cv('php:eval \'return is_callable(array(Civi::service("angular"),"getRawPartials"));\''));
15 | $supported = json_decode($p->getOutput(), 1);
16 | if (!$supported) {
17 | $this->markTestSkipped("Cannot test: this version of CiviCRM does not support getRawPartials");
18 | }
19 | }
20 |
21 | /**
22 | * List extensions using a regular expression.
23 | */
24 | public function testShow() {
25 |
26 | $p = Process::runOk($this->cv('ang:html:show crmUi/field.html'));
27 | $this->assertMatchesRegularExpression(';div.*ng-transclude;', $p->getOutput());
28 | }
29 |
30 | /**
31 | * List extensions using a regular expression.
32 | */
33 | public function testShowRaw() {
34 | $p = Process::runOk($this->cv('ang:html:show --raw crmUi/field.html'));
35 | $this->assertMatchesRegularExpression(';div.*ng-transclude;', $p->getOutput());
36 | }
37 |
38 | /**
39 | * List extensions using a regular expression.
40 | */
41 | public function testShowDiff() {
42 | // We don't know enough about the system to be sure of what diff to
43 | // expect, but we can at least ensure that it doesn't crash.
44 | $p = Process::runOk($this->cv('ang:html:show --diff crmUi/field.html'));
45 | $this->assertNotEmpty($p->getOutput());
46 | }
47 |
48 | }
49 |
--------------------------------------------------------------------------------
/tests/Command/AngularModuleListCommandTest.php:
--------------------------------------------------------------------------------
1 | cv('ang:module:list'));
17 | $this->assertMatchesRegularExpression(';crmUi.*civicrm/a.*crmResource;', $p->getOutput());
18 |
19 | $p = Process::runOk($this->cv('ang:module:list ";crm;"'));
20 | $this->assertMatchesRegularExpression(';crmUi.*civicrm/a.*crmResource;', $p->getOutput());
21 |
22 | $p = Process::runOk($this->cv('ang:module:list ";foo;"'));
23 | $this->assertDoesNotMatchRegularExpression(';crmUi.*civicrm/a.*crmResource;', $p->getOutput());
24 | }
25 |
26 | }
27 |
--------------------------------------------------------------------------------
/tests/Command/ApiBatchCommandTest.php:
--------------------------------------------------------------------------------
1 | 100)),
20 | array('Contact', 'get', array('id' => 101)),
21 | ))
22 | );
23 | $p = Process::runOk(\Symfony\Component\Process\Process::fromShellCommandline("echo $input | {$this->cv} api:batch"));
24 | $data = json_decode($p->getOutput(), 1);
25 | $this->assertTrue(isset($data[0]['is_error']));
26 | $this->assertTrue(isset($data[1]['is_error']));
27 | $this->assertTrue(isset($data[0]['error_message']) || isset($data[0]['values']));
28 | $this->assertTrue(isset($data[1]['error_message']) || isset($data[1]['values']));
29 | }
30 |
31 | public function testApiBatch_MultiLine() {
32 | // For example API calls, don't care about ID# or existence -- just want a quick dummy call.
33 | $jsonNumArray = json_encode(array(
34 | array('Contact', 'get', array('id' => 100, 'options.ignore-me' => "foo\nbar")),
35 | array('Contact', 'get', array('id' => 101)),
36 | ));
37 | $jsonAssocArray = json_encode(array(
38 | 'foo' => array('Contact', 'get', array('id' => 100)),
39 | 'bar' => array('Contact', 'get', array('id' => 101)),
40 | ));
41 | $input = ($jsonNumArray . "\n" . $jsonAssocArray);
42 |
43 | $p = \Symfony\Component\Process\Process::fromShellCommandline("{$this->cv} api:batch");
44 | $p->setInput($input);
45 | $p = Process::runOk($p);
46 |
47 | $this->assertEmpty($p->getErrorOutput());
48 | $responses = explode("\n", $p->getOutput());
49 |
50 | $dataNumArray = json_decode($responses[0], 1);
51 | $this->assertTrue(isset($dataNumArray[0]['is_error']));
52 | $this->assertTrue(isset($dataNumArray[1]['is_error']));
53 | $this->assertTrue(isset($dataNumArray[0]['error_message']) || isset($dataNumArray[0]['values']));
54 | $this->assertTrue(isset($dataNumArray[1]['error_message']) || isset($dataNumArray[1]['values']));
55 |
56 | $dataAssocArray = json_decode($responses[1], 1);
57 | $this->assertTrue(isset($dataAssocArray['foo']['is_error']));
58 | $this->assertTrue(isset($dataAssocArray['bar']['is_error']));
59 | $this->assertTrue(isset($dataAssocArray['foo']['error_message']) || isset($dataAssocArray['foo']['values']));
60 | $this->assertTrue(isset($dataAssocArray['bar']['error_message']) || isset($dataAssocArray['bar']['values']));
61 | }
62 |
63 | }
64 |
--------------------------------------------------------------------------------
/tests/Command/ApiCommandTest.php:
--------------------------------------------------------------------------------
1 | cv("api System.get"));
17 | $data = json_decode($p->getOutput(), 1);
18 | $this->assertTrue(!empty($data['values']));
19 | foreach ($data['values'] as $row) {
20 | $this->assertTrue(!empty($row['version']));
21 | $this->assertTrue(!empty($row['uf']));
22 | }
23 | }
24 |
25 | public function testCsv() {
26 | $p = Process::runOk($this->cv("api OptionValue.get option_group_id=activity_type return=option_group_id,name rowCount=2 --out=csv"));
27 | $lines = explode("\n", trim($p->getOutput()));
28 | $expected = array(
29 | 'option_group_id,name',
30 | '2,Meeting',
31 | '2,"Phone Call"',
32 | );
33 | $this->assertEquals($expected, $lines);
34 | }
35 |
36 | public function testCsvMisuse() {
37 | $p = Process::runOk($this->cv("api OptionValue.getsingle rowCount=1 --out=csv"));
38 | $this->assertMatchesRegularExpression('/The output format "csv" only works with tabular data. Try using a "get" API. Forcing format to "json-pretty"./', $p->getErrorOutput());
39 | $data = json_decode($p->getOutput(), 1);
40 | $this->assertTrue(!empty($data['option_group_id']));
41 | }
42 |
43 | public function testQuiet() {
44 | $p = Process::runOk($this->cv("api -q System.get"));
45 | $this->assertEmpty($p->getOutput());
46 | $this->assertEmpty($p->getErrorOutput());
47 | }
48 |
49 | public function testQuietError() {
50 | $p = Process::runFail($this->cv("api -q System.getzz"));
51 | $data = json_decode($p->getOutput(), 1);
52 | $this->assertTrue(!empty($data['is_error']));
53 | $this->assertTrue(!empty($data['error_message']));
54 | }
55 |
56 | public function testApiPipe() {
57 | $input = escapeshellarg(json_encode(array(
58 | 'options' => array('limit' => 1),
59 | )));
60 | $p = Process::runOk(\Symfony\Component\Process\Process::fromShellCommandline("echo $input | {$this->cv} api Contact.get --in=json"));
61 | $data = json_decode($p->getOutput(), 1);
62 | $this->assertTrue(!empty($data['values']));
63 | $this->assertEquals(1, count($data['values']));
64 | foreach ($data['values'] as $row) {
65 | $this->assertTrue(!empty($row['display_name']));
66 | }
67 | }
68 |
69 | }
70 |
--------------------------------------------------------------------------------
/tests/Command/BaseExtensionCommandTest.php:
--------------------------------------------------------------------------------
1 | configureRepoOptions();
44 |
45 | $input = new ArgvInput($inputArgv, $c->getDefinition());
46 | $this->assertEquals($expectUrl, $c->parseRepoUrl($input));
47 | }
48 |
49 | }
50 |
--------------------------------------------------------------------------------
/tests/Command/BootCommandTest.php:
--------------------------------------------------------------------------------
1 | cv("php:boot --level=full"));
18 | $this->assertMatchesRegularExpression(';CIVICRM_SETTINGS_PATH;', $phpBoot->getOutput());
19 |
20 | $helloPhp = escapeshellarg($phpBoot->getOutput()
21 | . 'printf("count is %s\n", CRM_Core_DAO::singleValueQuery("select count(*) from civicrm_contact"));'
22 | . 'printf("my admin is %s\n", $GLOBALS["_CV"]["ADMIN_USER"]);'
23 | );
24 | $phpRun = Process::runOk(\Symfony\Component\Process\Process::fromShellCommandline("php -r $helloPhp"));
25 | $this->assertMatchesRegularExpression('/^count is [0-9]+\n/', $phpRun->getOutput());
26 | $this->assertMatchesRegularExpression('/my admin is \w+\n/', $phpRun->getOutput());
27 | }
28 |
29 | public function testBootCmsFull() {
30 | $phpBoot = Process::runOk($this->cv("php:boot --level=cms-full"));
31 | $this->assertMatchesRegularExpression(';BEGINPHP;', $phpBoot->getOutput());
32 | $this->assertMatchesRegularExpression(';ENDPHP;', $phpBoot->getOutput());
33 |
34 | $helloPhp = escapeshellarg($phpBoot->getOutput()
35 | . 'printf("count is %s\n", CRM_Core_DAO::singleValueQuery("select count(*) from civicrm_contact"));'
36 | . 'printf("my admin is %s\n", $GLOBALS["_CV"]["ADMIN_USER"]);'
37 | );
38 | $phpRun = Process::runOk(\Symfony\Component\Process\Process::fromShellCommandline("php -r $helloPhp"));
39 | $this->assertMatchesRegularExpression('/^count is [0-9]+\n/', $phpRun->getOutput());
40 | $this->assertMatchesRegularExpression('/my admin is \w+\n/', $phpRun->getOutput());
41 | }
42 |
43 | public function testBootClassLoader() {
44 | $phpBoot = Process::runOk($this->cv("php:boot --level=classloader"));
45 | $this->assertMatchesRegularExpression(';ClassLoader;', $phpBoot->getOutput());
46 |
47 | // In the classloader level, config vals like CIVICRM_DSN are not loaded.
48 | $helloPhp = escapeshellarg($phpBoot->getOutput()
49 | . '$x=array("a"=>defined("CIVICRM_DSN") ? "yup" : "nope");'
50 | . 'printf("phpr says %s\n", CRM_Utils_Array::value("a",$x));'
51 | );
52 | $phpRun = Process::runOk(\Symfony\Component\Process\Process::fromShellCommandline("php -r $helloPhp"));
53 | $this->assertMatchesRegularExpression('/^phpr says nope$/', $phpRun->getOutput());
54 | }
55 |
56 | public function testBootTest() {
57 | $phpBoot = Process::runOk($this->cv("php:boot --test"));
58 | $this->assertMatchesRegularExpression(';CIVICRM_SETTINGS_PATH;', $phpBoot->getOutput());
59 |
60 | $helloPhp = escapeshellarg($phpBoot->getOutput()
61 | . 'echo CIVICRM_UF;'
62 | );
63 | $phpRun = Process::runOk(\Symfony\Component\Process\Process::fromShellCommandline("php -ddisplay_errors=1 -r $helloPhp"));
64 | $this->assertMatchesRegularExpression('/UnitTests/i', $phpRun->getOutput());
65 | }
66 |
67 | }
68 |
--------------------------------------------------------------------------------
/tests/Command/DebugContainerCommandTest.php:
--------------------------------------------------------------------------------
1 | cv("debug:container"));
17 | $this->assertMatchesRegularExpression('/(cxn_reg_client.*Civi.Cxn.Rpc.RegistrationClient|httpClient.*CRM_Utils_HttpClient|sql_triggers.*Civi.Core.SqlTrigger)/', $p->getOutput());
18 | $this->assertMatchesRegularExpression('/civi_api_kernel.*Civi.API.Kernel/', $p->getOutput());
19 | }
20 |
21 | // public function testName() {
22 | // $p = Process::runOk($this->cv("debug:container hook_civicrm_caseChange"));
23 | // $this->assertMatchesRegularExpression('/hook_civicrm_caseChange/', $p->getOutput());
24 | // $this->assertDoesNotMatchRegularExpression('/hook_civicrm_post/', $p->getOutput());
25 | // $this->assertDoesNotMatchRegularExpression('/civi.token.eval/', $p->getOutput());
26 | // }
27 |
28 | // public function testRegExp() {
29 | // $p = Process::runOk($this->cv("debug:container /^hook/"));
30 | // $this->assertMatchesRegularExpression('/hook_civicrm_caseChange/', $p->getOutput());
31 | // $this->assertMatchesRegularExpression('/hook_civicrm_post/', $p->getOutput());
32 | // $this->assertDoesNotMatchRegularExpression('/civi.token.eval/', $p->getOutput());
33 | // }
34 |
35 | }
36 |
--------------------------------------------------------------------------------
/tests/Command/DebugDispatcherCommandTest.php:
--------------------------------------------------------------------------------
1 | cv("debug:event-dispatcher"));
17 | $this->assertMatchesRegularExpression('/hook_civicrm_caseChange/', $p->getOutput());
18 | $this->assertMatchesRegularExpression('/hook_civicrm_post/', $p->getOutput());
19 | $this->assertMatchesRegularExpression('/civi.token.eval/', $p->getOutput());
20 | }
21 |
22 | public function testName() {
23 | $p = Process::runOk($this->cv("debug:event-dispatcher hook_civicrm_caseChange"));
24 | $this->assertMatchesRegularExpression('/hook_civicrm_caseChange/', $p->getOutput());
25 | $this->assertDoesNotMatchRegularExpression('/hook_civicrm_post/', $p->getOutput());
26 | $this->assertDoesNotMatchRegularExpression('/civi.token.eval/', $p->getOutput());
27 | }
28 |
29 | public function testRegExp() {
30 | $p = Process::runOk($this->cv("debug:event-dispatcher /^hook/"));
31 | $this->assertMatchesRegularExpression('/hook_civicrm_caseChange/', $p->getOutput());
32 | $this->assertMatchesRegularExpression('/hook_civicrm_post/', $p->getOutput());
33 | $this->assertDoesNotMatchRegularExpression('/civi.token.eval/', $p->getOutput());
34 | }
35 |
36 | }
37 |
--------------------------------------------------------------------------------
/tests/Command/ExtensionBareDownloadTest.php:
--------------------------------------------------------------------------------
1 | tmpDir = sys_get_temp_dir() . '/baredl';
19 |
20 | if (self::$first) {
21 | self::$first = FALSE;
22 | $this->cleanup();
23 | }
24 |
25 | $this->removeDir($this->tmpDir);
26 | mkdir($this->tmpDir);
27 | chdir($this->tmpDir);
28 | }
29 |
30 | public function tearDown(): void {
31 | parent::tearDown();
32 | $this->cleanup();
33 | }
34 |
35 | /**
36 | * Download an extension using an explicit URL.
37 | */
38 | public function testDownloadBare() {
39 | $toPath = $this->tmpDir . '/extracted-ext';
40 |
41 | $origFile = dirname(__DIR__) . '/fixtures/org.example.cvtest/info.xml';
42 | $finalFile = "$toPath/info.xml";
43 | $this->assertFalse(file_exists($finalFile), "File $finalFile should not yet exist.");
44 |
45 | $cvTestZip = $this->makeCvTestZip();
46 | $infoXmlPath = $this->makeDownloadManifest($origFile, 'file://' . $cvTestZip);
47 |
48 | $extSpecArg = escapeshellarg("@" . $infoXmlPath);
49 | $toArg = escapeshellarg($toPath);
50 | Process::runOk($this->cv("ext:download -b $extSpecArg --to=$toArg"));
51 |
52 | $this->assertTrue(file_exists($finalFile), "File $finalFile should exist.");
53 | $this->assertEquals(file_get_contents($origFile), file_get_contents($finalFile));
54 | }
55 |
56 | protected function makeDownloadManifest($origFile, $downloadUrl) {
57 | $xml = simplexml_load_string(file_get_contents($origFile));
58 | $xml->addChild('downloadUrl', $downloadUrl);
59 | $newFile = $this->tmpDir . DIRECTORY_SEPARATOR . 'fixme.xml';
60 | file_put_contents($newFile, $xml->saveXML());
61 | return $newFile;
62 | }
63 |
64 | /**
65 | * Make a zip file for the placeholder extension, `org.example.cvtest`.
66 | * @return string
67 | * Path to zip file
68 | */
69 | protected function makeCvTestZip() {
70 | $cvTestSrc = dirname(__DIR__) . '/fixtures/org.example.cvtest';
71 | $makePhp = $cvTestSrc . DIRECTORY_SEPARATOR . 'make.php';
72 | $cvTestZip = $this->tmpDir . DIRECTORY_SEPARATOR . 'cvtest.zip';
73 | Process::runOk(\Symfony\Component\Process\Process::fromShellCommandline(
74 | escapeshellcmd($makePhp) . ' ' . escapeshellarg($cvTestZip),
75 | $cvTestSrc
76 | ));
77 | return $cvTestZip;
78 | }
79 |
80 | /**
81 | * @param string $dir
82 | */
83 | protected function removeDir($dir) {
84 | if (!empty($dir) && file_exists($dir) && is_dir($dir)) {
85 | exec("rm -rf " . escapeshellarg($dir));
86 | }
87 | }
88 |
89 | protected function cleanup() {
90 | $this->removeDir($this->tmpDir);
91 | }
92 |
93 | }
94 |
--------------------------------------------------------------------------------
/tests/Command/FillCommandTest.php:
--------------------------------------------------------------------------------
1 | cv("vars:fill --file=/dev/stdin");
13 | $p->setInput(json_encode(array(
14 | 'ADMIN_USER' => 'admin',
15 | )));
16 | $p->setEnv(array(
17 | 'CV_CONFIG' => $tmpConfigFile,
18 | ));
19 | $p->run();
20 |
21 | $config = json_decode(file_get_contents($tmpConfigFile), 1);
22 | unlink($tmpConfigFile);
23 |
24 | $this->assertMatchesRegularExpression('/Please edit.*' . preg_quote($tmpConfigFile, '/') . '/', $p->getOutput());
25 | $this->assertNotEmpty($config);
26 | $this->assertNotEmpty($config['sites']);
27 | foreach ($config['sites'] as $path => $siteConfig) {
28 | $this->assertEquals('t0ps3cr3t', $siteConfig['ADMIN_PASS']);
29 | $this->assertTrue(!isset($siteConfig['ADMIN_USER']));
30 | }
31 | }
32 |
33 | }
34 |
--------------------------------------------------------------------------------
/tests/Command/FlushCommandTest.php:
--------------------------------------------------------------------------------
1 | cv("flush"));
17 | $this->assertMatchesRegularExpression('/Flushing/', $p->getOutput());
18 | }
19 |
20 | }
21 |
--------------------------------------------------------------------------------
/tests/Command/HttpCommandTest.php:
--------------------------------------------------------------------------------
1 | cvOk('en authx');
23 | }
24 |
25 | public function testAuthorizedGet() {
26 | $body = $this->cvOk("http {$this->login} civicrm/authx/id");
27 | $data = json_decode($body, TRUE);
28 | $this->assertTrue(is_numeric($data['contact_id']), "civicrm/authx/id should return current contact. Received: $body");
29 |
30 | $body = $this->cvOk("http {$this->login} 'civicrm/user?reset=1'");
31 | $this->assertMatchesRegularExpression(':assertMatchesRegularExpression(';Your Group;', $body);
33 | }
34 |
35 | public function testUnauthorizedGet() {
36 | $body = $this->cvOk("http civicrm/authx/id");
37 | $data = json_decode($body, TRUE);
38 | $this->assertTrue(empty($data['contact_id']), "civicrm/authx/id should return anonymous. Received: $body");
39 |
40 | $body = $this->cvFail("http 'civicrm/user?reset=1'");
41 | $this->assertMatchesRegularExpression(':assertDoesNotMatchRegularExpression(';Your Group;', $body);
43 | }
44 |
45 | public function testGetWebService() {
46 | $data = escapeshellarg('params=' . urlencode(json_encode(['limit' => 1])));
47 | $body = $this->cvOk("http {$this->login} civicrm/ajax/api4/Group/get --data $data");
48 | $parsed = json_decode($body, TRUE);
49 | $this->assertTrue(!empty($parsed['values'][0]['title']) && is_string($parsed['values'][0]['title']),
50 | 'Response should include a title. Received: ' . $body);
51 | $this->assertEquals(1, count($parsed['values']), "Response should have been limited to 1 record.");
52 | }
53 |
54 | public function testGetWebServiceVerbose() {
55 | $data = escapeshellarg('params=' . urlencode(json_encode(['limit' => 1])));
56 | $p = Process::runOk($this->cv("http -v {$this->login} civicrm/ajax/api4/Group/get --data $data"));
57 |
58 | $parsed = json_decode($p->getOutput(), TRUE);
59 | $this->assertTrue(!empty($parsed['values'][0]['title']) && is_string($parsed['values'][0]['title']),
60 | 'Response should include a title. Received: ' . $p->getOutput());
61 | $this->assertEquals(1, count($parsed['values']), "Response should have been limited to 1 record.");
62 |
63 | $error = $p->getErrorOutput();
64 | $this->assertMatchesRegularExpression(';> POST http;', $error);
65 | $this->assertMatchesRegularExpression(';> Content-Type: application/x-www-form-urlencoded;', $error);
66 | $this->assertMatchesRegularExpression(';> X-Civi-Auth: Bearer;', $error);
67 | $this->assertMatchesRegularExpression(';< Content-Type: application/json;', $error);
68 | }
69 |
70 | }
71 |
--------------------------------------------------------------------------------
/tests/Command/PathCommandTest.php:
--------------------------------------------------------------------------------
1 | cv('path'));
17 | $this->assertMatchesRegularExpression('/Must use -x, -c, or -d/', $p->getErrorOutput());
18 | }
19 |
20 | public function testExtPaths() {
21 | $vars = $this->cvJsonOk('vars:show');
22 | $this->assertTrue(is_dir($vars['CIVI_CORE']));
23 | $this->assertTrue(file_exists($vars['CIVI_CORE']));
24 |
25 | // Try "cv path -x ".
26 | $plain = rtrim($this->cvOk("path -x civicrm"), "\n");
27 | $this->assertEquals(rtrim($vars['CIVI_CORE'], '/'), $plain);
28 |
29 | $plain = rtrim($this->cvOk("path -x civicrm/"), "\n");
30 | $this->assertEquals($vars['CIVI_CORE'], $plain);
31 |
32 | $plain = rtrim($this->cvOk("path -x civicrm/packages"), "\n");
33 | $this->assertEquals($vars['CIVI_CORE'] . 'packages', $plain);
34 |
35 | $json = $this->cvJsonOk("path -x civicrm --out=json");
36 | $this->assertEquals('ext', $json[0]['type']);
37 | $this->assertEquals('civicrm', $json[0]['expr']);
38 | $this->assertEquals(rtrim($vars['CIVI_CORE'], '/'), $json[0]['value']);
39 | }
40 |
41 | public function testDynamicExprPaths() {
42 | $vars = $this->cvJsonOk('vars:show');
43 | $this->assertTrue(is_dir($vars['CIVI_CORE']));
44 | $this->assertTrue(file_exists($vars['CIVI_CORE']));
45 |
46 | if (version_compare($vars['CIVI_VERSION'], '4.7.0', '<')) {
47 | $this->markTestSkipped('"cv path -d" requires v4.7+');
48 | }
49 |
50 | $plain = rtrim($this->cvOk("path -d '[civicrm.root]'"), "\n");
51 | $this->assertEquals(rtrim($vars['CIVI_CORE'], '/'), $plain);
52 |
53 | $plain = rtrim($this->cvOk("path -d '[civicrm.root]/'"), "\n");
54 | $this->assertEquals($vars['CIVI_CORE'], $plain);
55 |
56 | $plain = rtrim($this->cvOk("path -d '[civicrm.root]/packages'"), "\n");
57 | $this->assertEquals($vars['CIVI_CORE'] . 'packages', $plain);
58 |
59 | $json = $this->cvJsonOk("path -d '[civicrm.root]/packages/DB.php' --out=json");
60 | $this->assertEquals('dynamic', $json[0]['type']);
61 | $this->assertEquals('[civicrm.root]/packages/DB.php', $json[0]['expr']);
62 | $this->assertEquals($vars['CIVI_CORE'] . 'packages/DB.php', $json[0]['value']);
63 | }
64 |
65 | public function testConfigPaths() {
66 | $vars = $this->cvJsonOk('vars:show');
67 | $this->assertTrue(is_dir($vars['CIVI_CORE']));
68 | $this->assertTrue(file_exists($vars['CIVI_CORE']));
69 |
70 | $mandatorySettingNames = array(
71 | 'configAndLogDir',
72 | 'extensionsDir',
73 | 'imageUploadDir',
74 | 'templateCompileDir',
75 | 'uploadDir',
76 | );
77 | foreach ($mandatorySettingNames as $settingName) {
78 | $plain = rtrim($this->cvOk("path -c $settingName"), "\n");
79 | $this->assertTrue(file_exists($plain) && is_dir($plain), "Check $settingName");
80 | }
81 |
82 | $optionalSettingNames = array(
83 | 'customFileUploadDir',
84 | 'customPHPPathDir',
85 | 'customTemplateDir',
86 | 'templateCompileDir/en_US',
87 | );
88 | foreach ($optionalSettingNames as $settingName) {
89 | $plain = rtrim($this->cvOk("path -c $settingName"), "\n");
90 | $this->assertTrue((file_exists($plain) && is_dir($plain)) || empty($plain), "Check $settingName");
91 | }
92 | }
93 |
94 | public function testExtDot() {
95 | $this->assertEquals(
96 | $this->cvOk('path -x.'),
97 | $this->cvOk('path -c extensionsDir')
98 | );
99 | $this->assertEquals(
100 | $this->cvOk('path -x .'),
101 | $this->cvOk('path -c extensionsDir')
102 | );
103 | }
104 |
105 | }
106 |
--------------------------------------------------------------------------------
/tests/Command/ScriptCommandTest.php:
--------------------------------------------------------------------------------
1 | cv("scr $helloPhp"));
19 | $this->assertMatchesRegularExpression('/^version [0-9a-z\.]+$/', $p->getOutput());
20 | }
21 |
22 | public function testPhpScript() {
23 | $helloPhp = escapeshellarg(__DIR__ . '/hello-world.php');
24 | $p = Process::runOk($this->cv("php:script $helloPhp"));
25 | $this->assertMatchesRegularExpression('/^version [0-9a-z\.]+$/', $p->getOutput());
26 | }
27 |
28 | public function testScrNoArg() {
29 | $helloPhpFile = __DIR__ . '/hello-args.php';
30 | $helloPhpEsc = escapeshellarg(__DIR__ . '/hello-args.php');
31 | $p = Process::runOk($this->cv("scr $helloPhpEsc"));
32 | $this->assertEquals("Count: 1\n0: $helloPhpFile\n", $p->getOutput());
33 | }
34 |
35 | public function testScrArgs() {
36 | $helloPhpFile = __DIR__ . '/hello-args.php';
37 | $helloPhpEsc = escapeshellarg(__DIR__ . '/hello-args.php');
38 | $p = Process::runOk($this->cv("scr $helloPhpEsc one 'two and' three"));
39 | $this->assertEquals("Count: 4\n0: $helloPhpFile\n1: one\n2: two and\n3: three\n", $p->getOutput());
40 | }
41 |
42 | }
43 |
--------------------------------------------------------------------------------
/tests/Command/ShowCommandTest.php:
--------------------------------------------------------------------------------
1 | cv("vars:show");
15 | $p->run();
16 | $data = json_decode($p->getOutput(), 1);
17 | $this->assertMatchesRegularExpression('/^([0-9\.\-]|alpha|beta|dev|master|x)+$/', $data['CIVI_VERSION']);
18 | $this->assertMatchesRegularExpression('/^([0-9\.\-]|alpha|beta|dev|master|x)+$/', $data['CMS_VERSION']);
19 | $this->assertTrue(is_dir($data['CMS_ROOT']));
20 | $this->assertTrue(is_dir($data['CIVI_CORE']));
21 | }
22 |
23 | }
24 |
--------------------------------------------------------------------------------
/tests/Command/SqlCliCommandTest.php:
--------------------------------------------------------------------------------
1 | cv("sql")->setInput($query));
18 | $this->assertMatchesRegularExpression('/id\s+display_name/', $p->getOutput());
19 | }
20 |
21 | }
22 |
--------------------------------------------------------------------------------
/tests/Command/StatusCommandTest.php:
--------------------------------------------------------------------------------
1 | cv("status");
15 | $p->run();
16 | $data = $p->getOutput();
17 | $this->assertTrue((bool) preg_match('/| php .* | \d+\.\d+\./', $data));
18 | }
19 |
20 | public function testStatusJson() {
21 | $p = $this->cv("status --out=json");
22 | $p->run();
23 | $data = json_decode($p->getOutput(), 1);
24 | $this->assertTrue((bool) preg_match('/^\d+\.\d+\./', $data['civicrm']['value']));
25 | $this->assertTrue((bool) preg_match('/^\d+\.\d+\./', $data['php']['value']));
26 | }
27 |
28 | }
29 |
--------------------------------------------------------------------------------
/tests/Command/UpgradeGetCommandTest.php:
--------------------------------------------------------------------------------
1 | upgradeRun('stable');
12 | $this->assertMatchesRegularExpression('/\/civicrm-' . $data['version'] . '-(drupal[68]?|joomla|wordpress)+\.(tar\.gz|zip)$/', $data['url']);
13 | $this->stableVersionCheck($data);
14 | }
15 |
16 | public function testGetStableWordpress() {
17 | $data = $this->upgradeRun('stable', 'WordPress');
18 | $this->stableVersionCheck($data);
19 | }
20 |
21 | public function testGetStableJoomla() {
22 | $data = $this->upgradeRun('stable', 'Joomla');
23 | $this->stableVersionCheck($data);
24 | }
25 |
26 | public function testGetStableDrupal() {
27 | $data = $this->upgradeRun('stable', 'Drupal');
28 | $this->stableVersionCheck($data);
29 | }
30 |
31 | public function testGetStableBackdrop() {
32 | $data = $this->upgradeRun('stable', 'Backdrop');
33 | $this->stableVersionCheck($data);
34 | }
35 |
36 | public function testGetNightly() {
37 | $data = $this->upgradeRun('nightly');
38 | $revisionId = $this->analyzeRevision($data);
39 | $this->assertGreaterThanOrEqual((int) date('Ymdhi', strtotime('-2 days')), (int) $revisionId, 'Nightly revision is well over a day old.');
40 | }
41 |
42 | public function testGetRc() {
43 | $data = $this->upgradeRun('rc');
44 | $revisionId = $this->analyzeRevision($data);
45 | }
46 |
47 | protected function stableVersionCheck($data) {
48 | $this->assertMatchesRegularExpression('/^\d+\.\d+\.\d+$/', $data['version']);
49 | $this->assertEquals($data['version'], $data['rev'], 'Stable revision is not the same as version number.');
50 | }
51 |
52 | protected function analyzeRevision($data) {
53 | $this->assertMatchesRegularExpression('/\d+\.\d+\.\d+$/', $data['version']);
54 | $this->assertMatchesRegularExpression('/' . $data['version'] . '-\d+$/', $data['rev'], 'Revision name does not include version number.');
55 | $revisionParts = explode('-', $data['rev']);
56 | array_unshift($revisionParts, 'civicrm');
57 | $revisionId = array_pop($revisionParts);
58 | $revisionParts[] = '(drupal[68]?|joomla|wordpress)';
59 | $revisionParts[] = $revisionId;
60 | $this->assertMatchesRegularExpression('/\/' . implode('-', $revisionParts) . '\.(tar\.gz|zip)$/', $data['url']);
61 | $this->assertLessThanOrEqual((int) date('Ymdhi', strtotime('+1 day')), (int) $revisionId, 'Revision is from the future.');
62 | return $revisionId;
63 | }
64 |
65 | protected function upgradeRun($stability, $cms = NULL) {
66 | $cmsString = ($cms) ? " --cms=$cms" : '';
67 | $p = $this->cv("upgrade:get --stability=$stability$cmsString");
68 | $p->run();
69 | $data = json_decode($p->getOutput(), 1);
70 | $this->assertTrue(isset($data['url']), 'Looking for url in output: ' . $p->getOutput());
71 | switch ($cms) {
72 | case 'WordPress':
73 | case 'Joomla':
74 | $this->assertMatchesRegularExpression('/-' . strtolower($cms) . '\.zip/', $data['url']);
75 | break;
76 |
77 | case 'Drupal':
78 | case 'Drupal6':
79 | case 'Drupal8':
80 | case 'Backdrop':
81 | $this->assertMatchesRegularExpression('/-' . strtolower($cms) . '\.tar\.gz/', $data['url']);
82 | }
83 | $headers = get_headers($data['url']);
84 | $this->assertContains('200 OK', $headers[0], 'URL does not have the file to download.');
85 | return $data;
86 | }
87 |
88 | }
89 |
--------------------------------------------------------------------------------
/tests/Command/hello-args.php:
--------------------------------------------------------------------------------
1 | $val) {
8 | print "$key: $val\n";
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/tests/Command/hello-world.php:
--------------------------------------------------------------------------------
1 | addListener('myapp.foo', function ($event) {
25 | static::addLog('myapp.foo');
26 | });
27 | $d->addListener('myapp.bar', function ($event) {
28 | static::addLog('unrelated');
29 | });
30 | $d->dispatch(new CvEvent(['data' => 'yoyo']), 'myapp.foo')->getArguments();
31 | $this->assertEquals(['myapp.foo'], static::$log);
32 | }
33 |
34 | public function testCallbackTypes() {
35 | $d = new CvDispatcher();
36 | $d->addListener('myapp.foo', function ($event) {
37 | static::addLog("foo one data=" . $event['data']);
38 | });
39 | $d->addListener('myapp.foo', __NAMESPACE__ . '\\cvdispatcher_global_func');
40 | $d->addListener('myapp.foo', [__CLASS__, 'addFooThree']);
41 |
42 | $r = $d->dispatch(new CvEvent(['data' => 'yoyo']), 'myapp.foo')->getArguments();
43 | $this->assertEquals('yoyo', $r['data']);
44 | $this->assertEquals([
45 | 'foo one data=yoyo',
46 | 'called Civi\\Cv\\cvdispatcher_global_func',
47 | 'foo three data=yoyo',
48 | ], static::$log);
49 | }
50 |
51 | public function testAlter() {
52 | $d = new CvDispatcher();
53 | $d->addListener('myapp.foo', function ($event) {
54 | $event['data'] .= ' first';
55 | });
56 | $d->addListener('myapp.foo', function ($event) {
57 | $event['data'] .= ' second';
58 | });
59 | $r = $d->dispatch(new CvEvent(['data' => 'seed']), 'myapp.foo')->getArguments();
60 | $this->assertEquals('seed first second', $r['data']);
61 | }
62 |
63 | public function testPriority() {
64 | $d = new CvDispatcher();
65 | $d->addListener('myapp.foo', function ($event) {
66 | static::addLog('foo.3.1');
67 | }, 3);
68 | $d->addListener('myapp.foo', function ($event) {
69 | static::addLog('foo.-200.1');
70 | }, -200);
71 | $d->addListener('myapp.foo', function ($event) {
72 | static::addLog('foo.1.1');
73 | }, 1);
74 | $d->addListener('myapp.foo', function ($event) {
75 | static::addLog('foo.2.1');
76 | }, 2);
77 | $d->addListener('myapp.foo', function ($event) {
78 | static::addLog('foo.1.2');
79 | }, 1);
80 | $d->addListener('myapp.foo', function ($event) {
81 | static::addLog('foo.1.3');
82 | }, 1);
83 | $d->addListener('*.foo', function ($event) {
84 | static::addLog('wildFoo.1.1');
85 | }, 1);
86 | $d->addListener('*.foo', function ($event) {
87 | static::addLog('wildFoo.2.1');
88 | }, 2);
89 |
90 | $d->dispatch(new CvEvent([]), 'myapp.foo');
91 | $this->assertEquals([
92 | 'foo.-200.1',
93 | 'foo.1.1',
94 | 'foo.1.2',
95 | 'foo.1.3',
96 | 'wildFoo.1.1',
97 | 'foo.2.1',
98 | 'wildFoo.2.1',
99 | 'foo.3.1',
100 | ], static::$log);
101 | }
102 |
103 | public static function addLog(string $message) {
104 | static::$log[] = $message;
105 | }
106 |
107 | public static function addFooThree($event) {
108 | static::addLog("foo three data=" . $event['data']);
109 | }
110 |
111 | }
112 |
--------------------------------------------------------------------------------
/tests/CvTestTrait.php:
--------------------------------------------------------------------------------
1 | getCvPath();
17 | $process = \Symfony\Component\Process\Process::fromShellCommandline("{$cvPath} $command");
18 | return $process;
19 | }
20 |
21 | /**
22 | * Run a `cv` subcommand. Assert success and return output.
23 | *
24 | * @param string $cmd
25 | * @return string
26 | */
27 | protected function cvOk($cmd) {
28 | $p = Process::runOk($this->cv($cmd));
29 | return $p->getOutput();
30 | }
31 |
32 | /**
33 | * Run a `cv` subcommand. Assert success and return output.
34 | *
35 | * @param string $cmd
36 | * @return string
37 | */
38 | protected function cvFail($cmd) {
39 | $p = Process::runFail($this->cv($cmd));
40 | return $p->getErrorOutput() . $p->getOutput();
41 | }
42 |
43 | /**
44 | * Run a `cv` subcommand. Assert success and return output.
45 | *
46 | * @param string $cmd
47 | * @return string
48 | */
49 | protected function cvJsonOk($cmd) {
50 | $p = Process::runOk($this->cv($cmd));
51 | return json_decode($p->getOutput(), 1);
52 | }
53 |
54 | private function getCvPath(): string {
55 | return getenv('CV_TEST_BINARY') ?: dirname(__DIR__) . '/bin/cv';
56 | }
57 |
58 | protected function isCvPharTest(): bool {
59 | return (bool) preg_match(';\.phar$;', $this->getCvPath());
60 | }
61 |
62 | }
63 |
--------------------------------------------------------------------------------
/tests/Plugin/AliasPluginTest.php:
--------------------------------------------------------------------------------
1 | setEnv(['CV_PLUGIN_PATH' => preg_replace(';\.php$;', '', __FILE__)]);
17 | return $process;
18 | }
19 |
20 | public function testDummyAlias() {
21 | $output = $this->cvOk('@dummy ext:list -Li');
22 | $this->assertMatchesRegularExpression(";^DUMMY: '.*/(cv|cv.phar)' --site-alias=dummy 'ext:list' -Li;", $output);
23 | }
24 |
25 | public function testUnknownAlias() {
26 | $output = $this->cvFail('@eldorado ext:list -Li');
27 | $this->assertMatchesRegularExpression('/Unknown site alias: eldorado/', $output);
28 | }
29 |
30 | }
31 |
--------------------------------------------------------------------------------
/tests/Plugin/AliasPluginTest/dummy-alias.php:
--------------------------------------------------------------------------------
1 | 1) {
17 | die("Expect CV_PLUGIN API v1");
18 | }
19 |
20 | Cv::dispatcher()->addListener('*.app.site-alias', function(CvEvent $event) {
21 | if ($event['alias'] === 'dummy') {
22 |
23 | foreach (['app', 'output', 'input'] as $key) {
24 | if (empty($event[$key])) {
25 | throw new \RuntimeException("Event *.app.site-alias is missing value for \"$key\"");
26 | }
27 | }
28 |
29 | /**
30 | * @var \Civi\Cv\Util\CvArgvInput $input
31 | */
32 | $input = $event['input'];
33 |
34 | /**
35 | * @var \CvDeps\Symfony\Component\Console\Output\OutputInterface $output
36 | */
37 | $output = $event['output'];
38 |
39 | $args = array_map(__NAMESPACE__ . '\\escapeString', $input->getOriginalArgv());
40 | $fullCommand = implode(' ', $args);
41 |
42 | $event['transport'] = function() use ($input, $output, $fullCommand) {
43 | $output->writeln("DUMMY: $fullCommand", OutputInterface::OUTPUT_RAW);
44 | };
45 | }
46 | });
47 |
48 | function escapeString(string $expr): string {
49 | return preg_match('{^[\w=-]+$}', $expr) ? $expr : escapeshellarg($expr);
50 | }
51 |
--------------------------------------------------------------------------------
/tests/Plugin/FluentHelloPluginTest.php:
--------------------------------------------------------------------------------
1 | setEnv(['CV_PLUGIN_PATH' => preg_replace(';\.php$;', '', __FILE__)]);
16 | return $process;
17 | }
18 |
19 | public function testRun() {
20 | $output = $this->cvOk('hello:normal');
21 | $this->assertMatchesRegularExpression('/Hey-yo world via parameter.*Hey-yo world via StyleInterface/s', $output);
22 | }
23 |
24 | public function testRunWithName() {
25 | $output = $this->cvOk('hello:normal Alice');
26 | $this->assertMatchesRegularExpression('/Hey-yo Alice via parameter.*Hey-yo Alice via StyleInterface/s', $output);
27 | }
28 |
29 | public function testRun_noboot() {
30 | $output = $this->cvOk('hello:noboot');
31 | $this->assertMatchesRegularExpression('/Hey-yo world via parameter.*Hey-yo world via StyleInterface/s', $output);
32 | }
33 |
34 | public function testRunWithName_noboot() {
35 | $output = $this->cvOk('hello:noboot Bob');
36 | $this->assertMatchesRegularExpression('/Hey-yo Bob via parameter.*Hey-yo Bob via StyleInterface/s', $output);
37 | }
38 |
39 | }
40 |
--------------------------------------------------------------------------------
/tests/Plugin/FluentHelloPluginTest/hello.php:
--------------------------------------------------------------------------------
1 | 1) {
9 | die("Expect CV_PLUGIN API v1");
10 | }
11 |
12 | if (!preg_match(';^[\w_-]+$;', $CV_PLUGIN['appName'])) {
13 | throw new \RuntimeException("Invalid CV_PLUGIN[appName]" . json_encode($CV_PLUGIN['appName']));
14 | }
15 |
16 | if (!preg_match(';^([0-9x\.]+(-[\w-]+)?|UNKNOWN)$;', $CV_PLUGIN['appVersion'])) {
17 | throw new \RuntimeException("Invalid CV_PLUGIN[appVersion]: " . json_encode($CV_PLUGIN['appVersion']));
18 | }
19 |
20 | if ($CV_PLUGIN['name'] !== 'hello') {
21 | throw new \RuntimeException("Invalid CV_PLUGIN[name]");
22 | }
23 | if (realpath($CV_PLUGIN['file']) !== realpath(__FILE__)) {
24 | throw new \RuntimeException("Invalid CV_PLUGIN[file]");
25 | }
26 |
27 | Cv::dispatcher()->addListener('*.app.boot', function ($e) {
28 | Cv::io()->writeln("Hey-yo during initial bootstrap!");
29 | });
30 |
31 | Cv::dispatcher()->addListener('cv.app.commands', function ($e) {
32 |
33 | $e['commands'][] = (new CvCommand('hello:normal'))
34 | ->setDescription('Say a greeting')
35 | ->addArgument('name')
36 | ->setCode(function($input, $output) {
37 | // ASSERT: With setCode(), it's OK to use un-hinted inputs.
38 | if ($input->getArgument('name') !== Cv::input()->getArgument('name')) {
39 | throw new \RuntimeException("Argument \"name\" is inconsistent!");
40 | }
41 | if (!Civi\Core\Container::isContainerBooted()) {
42 | throw new \LogicException("Container should have been booted by CvCommand!");
43 | }
44 | $name = $input->getArgument('name') ?: 'world';
45 | $output->writeln("Hey-yo $name via parameter!");
46 | Cv::io()->writeln("Hey-yo $name via StyleInterface!");
47 | return 0;
48 | });
49 |
50 | $e['commands'][] = (new CvCommand('hello:noboot'))
51 | ->setDescription('Say a greeting')
52 | ->addArgument('name')
53 | ->setBootOptions(['auto' => FALSE])
54 | ->setCode(function(InputInterface $input, OutputInterface $output) {
55 | // ASSERT: With setCode(), it's OK to use hinted inputs.
56 | if ($input->getArgument('name') !== Cv::input()->getArgument('name')) {
57 | throw new \RuntimeException("Argument \"name\" is inconsistent!");
58 | }
59 | if (class_exists('Civi\Core\Container')) {
60 | throw new \LogicException("Container should not have been booted by CvCommand!");
61 | }
62 | $name = $input->getArgument('name') ?: 'world';
63 | $output->writeln("Hey-yo $name via parameter!");
64 | Cv::io()->writeln("Hey-yo $name via StyleInterface!");
65 | return 0;
66 | });
67 |
68 | });
69 |
--------------------------------------------------------------------------------
/tests/Plugin/HelloPluginTest.php:
--------------------------------------------------------------------------------
1 | setEnv(['CV_PLUGIN_PATH' => preg_replace(';\.php$;', '', __FILE__)]);
16 | return $process;
17 | }
18 |
19 | public function testRun() {
20 | $output = $this->cvOk('hello');
21 | $this->assertMatchesRegularExpression('/Hello world via parameter.*Hello world via StyleInterface/s', $output);
22 | }
23 |
24 | public function testRunWithName() {
25 | $output = $this->cvOk('hello Bob');
26 | $this->assertMatchesRegularExpression('/Hello Bob via parameter.*Hello Bob via StyleInterface/s', $output);
27 | }
28 |
29 | }
30 |
--------------------------------------------------------------------------------
/tests/Plugin/HelloPluginTest/hello.php:
--------------------------------------------------------------------------------
1 | 1) {
9 | die("Expect CV_PLUGIN API v1");
10 | }
11 |
12 | if (!preg_match(';^[\w_-]+$;', $CV_PLUGIN['appName'])) {
13 | throw new \RuntimeException("Invalid CV_PLUGIN[appName]" . json_encode($CV_PLUGIN['appName']));
14 | }
15 |
16 | if (!preg_match(';^([0-9x\.]+(-[\w-]+)?|UNKNOWN)$;', $CV_PLUGIN['appVersion'])) {
17 | throw new \RuntimeException("Invalid CV_PLUGIN[appVersion]: " . json_encode($CV_PLUGIN['appVersion']));
18 | }
19 |
20 | if ($CV_PLUGIN['name'] !== 'hello') {
21 | throw new \RuntimeException("Invalid CV_PLUGIN[name]");
22 | }
23 | if (realpath($CV_PLUGIN['file']) !== realpath(__FILE__)) {
24 | throw new \RuntimeException("Invalid CV_PLUGIN[file]");
25 | }
26 |
27 | Cv::dispatcher()->addListener('*.app.boot', function ($e) {
28 | Cv::io()->writeln("Hello during initial bootstrap!");
29 | });
30 |
31 | Cv::dispatcher()->addListener('cv.app.commands', function ($e) {
32 | $e['commands'][] = new class extends Command {
33 |
34 | protected function configure() {
35 | $this->setName('hello')->setDescription('Say a greeting')->addArgument('name');
36 | }
37 |
38 | protected function execute(InputInterface $input, OutputInterface $output): int {
39 | if ($input->getArgument('name') !== Cv::input()->getArgument('name')) {
40 | throw new \RuntimeException("Argument \"name\" is inconsistent!");
41 | }
42 | $name = $input->getArgument('name') ?: 'world';
43 | $output->writeln("Hello $name via parameter!");
44 | Cv::io()->writeln("Hello $name via StyleInterface!");
45 | return 0;
46 | }
47 |
48 | };
49 | });
50 |
--------------------------------------------------------------------------------
/tests/TopHelperTest.php:
--------------------------------------------------------------------------------
1 | isCvPharTest()) {
17 | $exprs = [
18 | 'Cvphar\Fruit\Apple' => '\Fruit\Apple',
19 | '\Cvphar\Fruit\Banana' => '\Fruit\Banana',
20 | 'Fruit\Cherry' => '\Fruit\Cherry',
21 | '\Fruit\Date' => '\Fruit\Date',
22 | ];
23 | }
24 | else {
25 | $exprs = [
26 | 'Fruit\Apple' => '\Fruit\Apple',
27 | '\Fruit\Banana' => '\Fruit\Banana',
28 | ];
29 | }
30 |
31 | foreach ($exprs as $input => $expected) {
32 | $p = Process::runOk($this->cv("ev 'return \Civi\Cv\Top::symbol(getenv(\"SYMBOL\"));'")
33 | ->setEnv(['SYMBOL' => $input]));
34 | $actual = json_decode($p->getOutput());
35 | $this->assertEquals($expected, $actual, "Input ($input) should yield value ($expected).");
36 | }
37 | }
38 |
39 | }
40 |
--------------------------------------------------------------------------------
/tests/Util/AliasFilterTest.php:
--------------------------------------------------------------------------------
1 | addOption('bare', 'b', InputOption::VALUE_NONE, 'Perform a basic download in a non-bootstrapped environment. Implies --level=none, --no-install, and no --refresh. You must specify the download URL.');
54 | // $command->addOption('to', NULL, InputOption::VALUE_OPTIONAL, 'Download to a specific directory (absolute path).');
55 | // $command->addArgument('key-or-name', InputArgument::IS_ARRAY, 'One or more extensions to enable. Identify the extension by full key ("org.example.foobar") or short name ("foobar"). Optionally append a URL.');
56 | // $app->add($command);
57 |
58 | $actualOutput = AliasFilter::filter($inputArray);
59 | $this->assertEquals($expectOutput, $actualOutput);
60 | }
61 |
62 | }
63 |
--------------------------------------------------------------------------------
/tests/Util/FilesystemTest.php:
--------------------------------------------------------------------------------
1 | assertEquals($expected, $fs->isDescendent($child, $parent));
45 | }
46 |
47 | }
48 |
--------------------------------------------------------------------------------
/tests/Util/OptionalOptionTest.php:
--------------------------------------------------------------------------------
1 | push(...$this->createInputOutput($inputArgv));
46 | try {
47 | $this->assertEquals($expectValue, OptionalOption::parse(Cv::input(), ['-r', '--refresh'], 'auto', 'yes'));
48 | }
49 | finally {
50 | Cv::ioStack()->pop();
51 | }
52 | }
53 |
54 | /**
55 | * @return array
56 | * [0 => InputInterface, 1 => OutputInterface]
57 | */
58 | protected function createInputOutput(?array $argv = NULL): array {
59 | $input = new ArgvInput($argv);
60 | $input->setInteractive(FALSE);
61 | $output = new NullOutput();
62 | return [$input, $output];
63 | }
64 |
65 | }
66 |
--------------------------------------------------------------------------------
/tests/Util/ProcessTest.php:
--------------------------------------------------------------------------------
1 | assertEquals(
15 | 'ls \'whiz=1&bang=1\' \'Who\'\\\'\'s on first\'',
16 | ProcessUtil::sprintf('ls %s %s', 'whiz=1&bang=1', "Who's on first")
17 | );
18 |
19 | $this->assertEquals(
20 | 'ls \'/home/foo bar\' /foo/bar',
21 | ProcessUtil::sprintf('ls %s %s', '/home/foo bar', '/foo/bar')
22 | );
23 |
24 | $this->assertEquals(
25 | 'ls /foo/bar',
26 | ProcessUtil::sprintf('ls %s', '/foo/bar')
27 | );
28 |
29 | $echoResult = ProcessUtil::runOk(Process::fromShellCommandline(
30 | ProcessUtil::sprintf('echo %s', 'whiz=1&bang=>')
31 | ));
32 | $this->assertEquals('whiz=1&bang=>', rtrim($echoResult->getOutput()));
33 | }
34 |
35 | public function testRunOk_pass() {
36 | $process = ProcessUtil::runOk(\Symfony\Component\Process\Process::fromShellCommandline("echo times were good"));
37 | $this->assertEquals("times were good", trim($process->getOutput()));
38 | }
39 |
40 | public function testRunOk_fail() {
41 | try {
42 | ProcessUtil::runOk(\Symfony\Component\Process\Process::fromShellCommandline("echo tragedy befell the software > /dev/stderr; exit 1"));
43 | $this->fail("Failed to generate expected exception");
44 | }
45 | catch (\Civi\Cv\Exception\ProcessErrorException $e) {
46 | $this->assertEquals("tragedy befell the software", trim($e->getProcess()->getErrorOutput()));
47 | }
48 | }
49 |
50 | }
51 |
--------------------------------------------------------------------------------
/tests/bootstrap.php:
--------------------------------------------------------------------------------
1 | addPsr4('Civi\\Cv\\', __DIR__);
21 |
--------------------------------------------------------------------------------
/tests/fixtures/org.example.cvtest/LICENSE.txt:
--------------------------------------------------------------------------------
1 | org.civicrm.cvtest
2 | Copyright (c) 2016 Tim Otten
3 |
4 | Permission is hereby granted, free of charge, to any person obtaining a copy
5 | of this software and associated documentation files (the "Software"), to deal
6 | in the Software without restriction, including without limitation the rights
7 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
8 | copies of the Software, and to permit persons to whom the Software is
9 | furnished to do so, subject to the following conditions:
10 |
11 | The above copyright notice and this permission notice shall be included in
12 | all copies or substantial portions of the Software.
13 |
14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
16 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
17 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
18 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
19 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
20 | THE SOFTWARE.
21 |
--------------------------------------------------------------------------------
/tests/fixtures/org.example.cvtest/cvtest.php:
--------------------------------------------------------------------------------
1 | "yes");
5 | }
6 |
--------------------------------------------------------------------------------
/tests/fixtures/org.example.cvtest/info.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | cvtest
4 | cvtest example extension
5 | Placeholder used to test cv functionality
6 | MIT
7 |
8 | Tim Otten
9 | totten@civicrm.org
10 |
11 |
12 | http://FIXME
13 | http://FIXME
14 | http://FIXME
15 | http://www.gnu.org/licenses/agpl-3.0.html
16 |
17 | 2016-12-21
18 | 1.0
19 | alpha
20 |
21 | 4.2
22 |
23 | This is a new, undeveloped module
24 |
25 |
--------------------------------------------------------------------------------
/tests/fixtures/org.example.cvtest/make.php:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env php
2 | \n", $argv[0]));
9 | }
10 |
11 |
12 | if (file_exists($argv[1])) {
13 | unlink($argv[1]);
14 | }
15 |
16 | $zip = new ZipArchive();
17 | if (TRUE !== $zip->open($argv[1], ZipArchive::CREATE)) {
18 | printf("Failed to open %s\n", $argv[1]);
19 | exit(1);
20 | }
21 |
22 | $zip->addEmptyDir('org.example.cvtest/');
23 | $zip->addFile('LICENSE.txt', 'org.example.cvtest/LICENSE.txt');
24 | $zip->addFile('cvtest.php', 'org.example.cvtest/cvtest.php');
25 | $zip->addFile('info.xml', 'org.example.cvtest/info.xml');
26 |
27 | $ok = $zip->close();
28 |
29 | if ($ok) {
30 | printf("Created %s\n", $argv[1]);
31 | exit(0);
32 | }
33 | else {
34 | printf("Failed to create %s\n", $argv[1]);
35 | exit(1);
36 | }
37 |
--------------------------------------------------------------------------------