| 1 | <?php |
| 2 | /* |
| 3 | +--------------------------------------------------------------------+ |
| 4 | | Copyright CiviCRM LLC. All rights reserved. | |
| 5 | | | |
| 6 | | This work is published under the GNU AGPLv3 license with some | |
| 7 | | permitted exceptions and without any warranty. For full license | |
| 8 | | and copyright information, see https://civicrm.org/licensing | |
| 9 | +--------------------------------------------------------------------+ |
| 10 | */ |
| 11 | |
| 12 | namespace Civi\Pipe; |
| 13 | |
| 14 | use Civi\Authx\AuthxException; |
| 15 | |
| 16 | /** |
| 17 | * Collection of methods to expose to the pipe session. Any public method will be accessible. |
| 18 | */ |
| 19 | class PublicMethods { |
| 20 | |
| 21 | /** |
| 22 | * How should API errors be reported? |
| 23 | * |
| 24 | * @var string |
| 25 | * - 'array': Traditional array format from civicrm_api(). Maximizes consistency of error data. |
| 26 | * - 'exception': Converted to an exception. Somewhat lossy. Improves out-of-box DX on stricter JSON-RPC clients. |
| 27 | */ |
| 28 | protected $apiError = 'exception'; |
| 29 | |
| 30 | /** |
| 31 | * Should API calls use permission checks? |
| 32 | * |
| 33 | * Note: This property is only consulted on trusted connections. It is ignored on untrusted connections. |
| 34 | * |
| 35 | * @var bool |
| 36 | */ |
| 37 | protected $apiCheckPermissions = TRUE; |
| 38 | |
| 39 | /** |
| 40 | * Send a request to APIv3. |
| 41 | * |
| 42 | * @param \Civi\Pipe\PipeSession $session |
| 43 | * @param array $request |
| 44 | * Tuple: [$entity, $action, $params] |
| 45 | * @return array|\Civi\Api4\Generic\Result|int |
| 46 | */ |
| 47 | public function api3(PipeSession $session, array $request) { |
| 48 | $request[2] = array_merge($request[2] ?? [], ['version' => 3]); |
| 49 | $request[2]['check_permissions'] = !$session->isTrusted() || $this->isCheckPermissions($request[2], 'check_permissions'); |
| 50 | // ^^ Untrusted sessions MUST check perms. All sessions DEFAULT to checking perms. Trusted sessions MAY disable perms. |
| 51 | switch ($this->apiError) { |
| 52 | case 'array': |
| 53 | return civicrm_api(...$request); |
| 54 | |
| 55 | case 'exception': |
| 56 | return civicrm_api3(...$request); |
| 57 | |
| 58 | default: |
| 59 | throw new \CRM_Core_Exception("Invalid API error-handling mode: $this->apiError"); |
| 60 | } |
| 61 | } |
| 62 | |
| 63 | /** |
| 64 | * Send a request to APIv4. |
| 65 | * |
| 66 | * @param \Civi\Pipe\PipeSession $session |
| 67 | * @param array $request |
| 68 | * Tuple: [$entity, $action, $params] |
| 69 | * @return array|\Civi\Api4\Generic\Result|int |
| 70 | */ |
| 71 | public function api4(PipeSession $session, array $request) { |
| 72 | $request[2] = array_merge($request[2] ?? [], ['version' => 4]); |
| 73 | $request[2]['checkPermissions'] = !$session->isTrusted() || $this->isCheckPermissions($request[2], 'checkPermissions'); |
| 74 | // ^^ Untrusted sessions MUST check perms. All sessions DEFAULT to checking perms. Trusted sessions MAY disable perms. |
| 75 | switch ($this->apiError) { |
| 76 | case 'array': |
| 77 | return civicrm_api(...$request); |
| 78 | |
| 79 | case 'exception': |
| 80 | return civicrm_api4(...$request); |
| 81 | |
| 82 | default: |
| 83 | throw new \CRM_Core_Exception("Invalid API error-handling mode: $this->apiError"); |
| 84 | } |
| 85 | } |
| 86 | |
| 87 | /** |
| 88 | * Simple test; send/receive a fragment of data. |
| 89 | * |
| 90 | * @param \Civi\Pipe\PipeSession $session |
| 91 | * @param array $request |
| 92 | * @return array |
| 93 | */ |
| 94 | public function echo(PipeSession $session, array $request) { |
| 95 | return $request; |
| 96 | } |
| 97 | |
| 98 | /** |
| 99 | * Set active user. |
| 100 | * |
| 101 | * @param \Civi\Pipe\PipeSession $session |
| 102 | * @param array{contactId: int, userId: int, user: string, cred: string} $request |
| 103 | * @return array|\Civi\Api4\Generic\Result|int |
| 104 | */ |
| 105 | public function login(PipeSession $session, array $request) { |
| 106 | if (!function_exists('authx_login')) { |
| 107 | throw new \CRM_Core_Exception('Cannot authenticate. Authx is not configured.'); |
| 108 | } |
| 109 | |
| 110 | $redact = function(?array $authx) { |
| 111 | return $authx ? \CRM_Utils_Array::subset($authx, ['contactId', 'userId']) : FALSE; |
| 112 | }; |
| 113 | |
| 114 | $principal = \CRM_Utils_Array::subset($request, ['contactId', 'userId', 'user']); |
| 115 | if ($principal && $session->isTrusted()) { |
| 116 | return $redact(authx_login(['flow' => 'script', 'principal' => $principal])); |
| 117 | } |
| 118 | elseif ($principal && !$session->isTrusted()) { |
| 119 | throw new AuthxException('Session is not trusted.'); |
| 120 | } |
| 121 | elseif (isset($request['cred'])) { |
| 122 | $authn = new \Civi\Authx\Authenticator(); |
| 123 | $authn->setRejectMode('exception'); |
| 124 | if ($authn->auth(NULL, ['flow' => 'pipe', 'cred' => $request['cred']])) { |
| 125 | return $redact(\CRM_Core_Session::singleton()->get('authx')); |
| 126 | } |
| 127 | } |
| 128 | |
| 129 | throw new AuthxException('Cannot authenticate. Must specify principal/credentials.'); |
| 130 | } |
| 131 | |
| 132 | /** |
| 133 | * Set ephemeral session options. |
| 134 | * |
| 135 | * @param \Civi\Pipe\PipeSession $session |
| 136 | * @param array{bufferSize: int, responsePrefix: int} $request |
| 137 | * Any updates to perform. May be empty/omitted. |
| 138 | * @return array{bufferSize: int, responsePrefix: int} |
| 139 | * List of updated options. |
| 140 | * If the list of updates was empty, then return all options. |
| 141 | */ |
| 142 | public function options(PipeSession $session, array $request) { |
| 143 | $storageMap = [ |
| 144 | 'apiCheckPermissions' => $this, |
| 145 | 'apiError' => $this, |
| 146 | 'bufferSize' => $session, |
| 147 | 'responsePrefix' => $session, |
| 148 | ]; |
| 149 | |
| 150 | if (!$session->isTrusted() && array_key_exists('apiCheckPermissions', $request)) { |
| 151 | unset($request['apiCheckPermissions']); |
| 152 | } |
| 153 | |
| 154 | $get = function($storage, $name) { |
| 155 | if (method_exists($storage, 'get' . ucfirst($name))) { |
| 156 | return $storage->{'get' . ucfirst($name)}(); |
| 157 | } |
| 158 | else { |
| 159 | return $storage->{$name}; |
| 160 | } |
| 161 | }; |
| 162 | |
| 163 | $set = function($storage, $name, $value) use ($get) { |
| 164 | if (method_exists($storage, 'set' . ucfirst($name))) { |
| 165 | $storage->{'set' . ucfirst($name)}($value); |
| 166 | } |
| 167 | else { |
| 168 | $storage->{$name} = $value; |
| 169 | } |
| 170 | return $get($storage, $name); |
| 171 | }; |
| 172 | |
| 173 | $result = []; |
| 174 | if (!empty($request)) { |
| 175 | foreach ($request as $name => $value) { |
| 176 | if (isset($storageMap[$name])) { |
| 177 | $result[$name] = $set($storageMap[$name], $name, $value); |
| 178 | } |
| 179 | } |
| 180 | } |
| 181 | else { |
| 182 | foreach ($storageMap as $name => $storage) { |
| 183 | $result[$name] = $get($storage, $name); |
| 184 | } |
| 185 | } |
| 186 | return $result; |
| 187 | } |
| 188 | |
| 189 | private function isCheckPermissions(array $params, string $field) { |
| 190 | return isset($params[$field]) ? $params[$field] : $this->apiCheckPermissions; |
| 191 | } |
| 192 | |
| 193 | } |