Client.php 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689
  1. <?php
  2. declare(strict_types=1);
  3. /**
  4. * CakePHP(tm) : Rapid Development Framework (https://cakephp.org)
  5. * Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
  6. *
  7. * Licensed under The MIT License
  8. * Redistributions of files must retain the above copyright notice.
  9. *
  10. * @copyright Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
  11. * @link https://cakephp.org CakePHP(tm) Project
  12. * @since 3.0.0
  13. * @license https://opensource.org/licenses/mit-license.php MIT License
  14. */
  15. namespace Cake\Http;
  16. use Cake\Core\App;
  17. use Cake\Core\Exception\CakeException;
  18. use Cake\Core\InstanceConfigTrait;
  19. use Cake\Http\Client\Adapter\Curl;
  20. use Cake\Http\Client\Adapter\Stream;
  21. use Cake\Http\Client\AdapterInterface;
  22. use Cake\Http\Client\Request;
  23. use Cake\Http\Client\Response;
  24. use Cake\Http\Cookie\CookieCollection;
  25. use Cake\Http\Cookie\CookieInterface;
  26. use Cake\Utility\Hash;
  27. use InvalidArgumentException;
  28. use Laminas\Diactoros\Uri;
  29. use Psr\Http\Client\ClientInterface;
  30. use Psr\Http\Message\RequestInterface;
  31. use Psr\Http\Message\ResponseInterface;
  32. /**
  33. * The end user interface for doing HTTP requests.
  34. *
  35. * ### Scoped clients
  36. *
  37. * If you're doing multiple requests to the same hostname it's often convenient
  38. * to use the constructor arguments to create a scoped client. This allows you
  39. * to keep your code DRY and not repeat hostnames, authentication, and other options.
  40. *
  41. * ### Doing requests
  42. *
  43. * Once you've created an instance of Client you can do requests
  44. * using several methods. Each corresponds to a different HTTP method.
  45. *
  46. * - get()
  47. * - post()
  48. * - put()
  49. * - delete()
  50. * - patch()
  51. *
  52. * ### Cookie management
  53. *
  54. * Client will maintain cookies from the responses done with
  55. * a client instance. These cookies will be automatically added
  56. * to future requests to matching hosts. Cookies will respect the
  57. * `Expires`, `Path` and `Domain` attributes. You can get the client's
  58. * CookieCollection using cookies()
  59. *
  60. * You can use the 'cookieJar' constructor option to provide a custom
  61. * cookie jar instance you've restored from cache/disk. By default
  62. * an empty instance of Cake\Http\Client\CookieCollection will be created.
  63. *
  64. * ### Sending request bodies
  65. *
  66. * By default any POST/PUT/PATCH/DELETE request with $data will
  67. * send their data as `application/x-www-form-urlencoded` unless
  68. * there are attached files. In that case `multipart/form-data`
  69. * will be used.
  70. *
  71. * When sending request bodies you can use the `type` option to
  72. * set the Content-Type for the request:
  73. *
  74. * ```
  75. * $http->get('/users', [], ['type' => 'json']);
  76. * ```
  77. *
  78. * The `type` option sets both the `Content-Type` and `Accept` header, to
  79. * the same mime type. When using `type` you can use either a full mime
  80. * type or an alias. If you need different types in the Accept and Content-Type
  81. * headers you should set them manually and not use `type`
  82. *
  83. * ### Using authentication
  84. *
  85. * By using the `auth` key you can use authentication. The type sub option
  86. * can be used to specify which authentication strategy you want to use.
  87. * CakePHP comes with a few built-in strategies:
  88. *
  89. * - Basic
  90. * - Digest
  91. * - Oauth
  92. *
  93. * ### Using proxies
  94. *
  95. * By using the `proxy` key you can set authentication credentials for
  96. * a proxy if you need to use one. The type sub option can be used to
  97. * specify which authentication strategy you want to use.
  98. * CakePHP comes with built-in support for basic authentication.
  99. */
  100. class Client implements ClientInterface
  101. {
  102. use InstanceConfigTrait;
  103. /**
  104. * Default configuration for the client.
  105. *
  106. * @var array
  107. */
  108. protected $_defaultConfig = [
  109. 'adapter' => null,
  110. 'host' => null,
  111. 'port' => null,
  112. 'scheme' => 'http',
  113. 'basePath' => '',
  114. 'timeout' => 30,
  115. 'ssl_verify_peer' => true,
  116. 'ssl_verify_peer_name' => true,
  117. 'ssl_verify_depth' => 5,
  118. 'ssl_verify_host' => true,
  119. 'redirect' => false,
  120. 'protocolVersion' => '1.1',
  121. ];
  122. /**
  123. * List of cookies from responses made with this client.
  124. *
  125. * Cookies are indexed by the cookie's domain or
  126. * request host name.
  127. *
  128. * @var \Cake\Http\Cookie\CookieCollection
  129. */
  130. protected $_cookies;
  131. /**
  132. * Adapter for sending requests.
  133. *
  134. * @var \Cake\Http\Client\AdapterInterface
  135. */
  136. protected $_adapter;
  137. /**
  138. * Create a new HTTP Client.
  139. *
  140. * ### Config options
  141. *
  142. * You can set the following options when creating a client:
  143. *
  144. * - host - The hostname to do requests on.
  145. * - port - The port to use.
  146. * - scheme - The default scheme/protocol to use. Defaults to http.
  147. * - basePath - A path to append to the domain to use. (/api/v1/)
  148. * - timeout - The timeout in seconds. Defaults to 30
  149. * - ssl_verify_peer - Whether or not SSL certificates should be validated.
  150. * Defaults to true.
  151. * - ssl_verify_peer_name - Whether or not peer names should be validated.
  152. * Defaults to true.
  153. * - ssl_verify_depth - The maximum certificate chain depth to traverse.
  154. * Defaults to 5.
  155. * - ssl_verify_host - Verify that the certificate and hostname match.
  156. * Defaults to true.
  157. * - redirect - Number of redirects to follow. Defaults to false.
  158. * - adapter - The adapter class name or instance. Defaults to
  159. * \Cake\Http\Client\Adapter\Curl if `curl` extension is loaded else
  160. * \Cake\Http\Client\Adapter\Stream.
  161. * - protocolVersion - The HTTP protocol version to use. Defaults to 1.1
  162. *
  163. * @param array $config Config options for scoped clients.
  164. * @throws \InvalidArgumentException
  165. */
  166. public function __construct(array $config = [])
  167. {
  168. $this->setConfig($config);
  169. $adapter = $this->_config['adapter'];
  170. if ($adapter === null) {
  171. $adapter = Curl::class;
  172. if (!extension_loaded('curl')) {
  173. $adapter = Stream::class;
  174. }
  175. } else {
  176. $this->setConfig('adapter', null);
  177. }
  178. if (is_string($adapter)) {
  179. $adapter = new $adapter();
  180. }
  181. if (!$adapter instanceof AdapterInterface) {
  182. throw new InvalidArgumentException('Adapter must be an instance of Cake\Http\Client\AdapterInterface');
  183. }
  184. $this->_adapter = $adapter;
  185. if (!empty($this->_config['cookieJar'])) {
  186. $this->_cookies = $this->_config['cookieJar'];
  187. $this->setConfig('cookieJar', null);
  188. } else {
  189. $this->_cookies = new CookieCollection();
  190. }
  191. }
  192. /**
  193. * Get the cookies stored in the Client.
  194. *
  195. * @return \Cake\Http\Cookie\CookieCollection
  196. */
  197. public function cookies(): CookieCollection
  198. {
  199. return $this->_cookies;
  200. }
  201. /**
  202. * Adds a cookie to the Client collection.
  203. *
  204. * @param \Cake\Http\Cookie\CookieInterface $cookie Cookie object.
  205. * @return $this
  206. * @throws \InvalidArgumentException
  207. */
  208. public function addCookie(CookieInterface $cookie)
  209. {
  210. if (!$cookie->getDomain() || !$cookie->getPath()) {
  211. throw new InvalidArgumentException('Cookie must have a domain and a path set.');
  212. }
  213. $this->_cookies = $this->_cookies->add($cookie);
  214. return $this;
  215. }
  216. /**
  217. * Client instance returned is scoped to the domain, port, and scheme parsed from the passed URL string. The passed
  218. * string must have a scheme and a domain. Optionally, if a port is included in the string, the port will be scoped
  219. * too. If a path is included in the URL, the client instance will build urls with it prepended.
  220. * Other parts of the url string are ignored.
  221. *
  222. * @param string $url A string URL e.g. https://example.com
  223. * @return static
  224. * @throws \InvalidArgumentException
  225. */
  226. public static function createFromUrl(string $url)
  227. {
  228. $parts = parse_url($url);
  229. if ($parts === false) {
  230. throw new InvalidArgumentException('String ' . $url . ' did not parse');
  231. }
  232. $config = array_intersect_key($parts, ['scheme' => '', 'port' => '', 'host' => '', 'path' => '']);
  233. $config = array_merge(['scheme' => '', 'host' => ''], $config);
  234. if (empty($config['scheme']) || empty($config['host'])) {
  235. throw new InvalidArgumentException('The URL was parsed but did not contain a scheme or host');
  236. }
  237. if (isset($config['path'])) {
  238. $config['basePath'] = $config['path'];
  239. unset($config['path']);
  240. }
  241. return new static($config);
  242. }
  243. /**
  244. * Do a GET request.
  245. *
  246. * The $data argument supports a special `_content` key
  247. * for providing a request body in a GET request. This is
  248. * generally not used, but services like ElasticSearch use
  249. * this feature.
  250. *
  251. * @param string $url The url or path you want to request.
  252. * @param array|string $data The query data you want to send.
  253. * @param array $options Additional options for the request.
  254. * @return \Cake\Http\Client\Response
  255. */
  256. public function get(string $url, $data = [], array $options = []): Response
  257. {
  258. $options = $this->_mergeOptions($options);
  259. $body = null;
  260. if (is_array($data) && isset($data['_content'])) {
  261. $body = $data['_content'];
  262. unset($data['_content']);
  263. }
  264. $url = $this->buildUrl($url, $data, $options);
  265. return $this->_doRequest(
  266. Request::METHOD_GET,
  267. $url,
  268. $body,
  269. $options
  270. );
  271. }
  272. /**
  273. * Do a POST request.
  274. *
  275. * @param string $url The url or path you want to request.
  276. * @param mixed $data The post data you want to send.
  277. * @param array $options Additional options for the request.
  278. * @return \Cake\Http\Client\Response
  279. */
  280. public function post(string $url, $data = [], array $options = []): Response
  281. {
  282. $options = $this->_mergeOptions($options);
  283. $url = $this->buildUrl($url, [], $options);
  284. return $this->_doRequest(Request::METHOD_POST, $url, $data, $options);
  285. }
  286. /**
  287. * Do a PUT request.
  288. *
  289. * @param string $url The url or path you want to request.
  290. * @param mixed $data The request data you want to send.
  291. * @param array $options Additional options for the request.
  292. * @return \Cake\Http\Client\Response
  293. */
  294. public function put(string $url, $data = [], array $options = []): Response
  295. {
  296. $options = $this->_mergeOptions($options);
  297. $url = $this->buildUrl($url, [], $options);
  298. return $this->_doRequest(Request::METHOD_PUT, $url, $data, $options);
  299. }
  300. /**
  301. * Do a PATCH request.
  302. *
  303. * @param string $url The url or path you want to request.
  304. * @param mixed $data The request data you want to send.
  305. * @param array $options Additional options for the request.
  306. * @return \Cake\Http\Client\Response
  307. */
  308. public function patch(string $url, $data = [], array $options = []): Response
  309. {
  310. $options = $this->_mergeOptions($options);
  311. $url = $this->buildUrl($url, [], $options);
  312. return $this->_doRequest(Request::METHOD_PATCH, $url, $data, $options);
  313. }
  314. /**
  315. * Do an OPTIONS request.
  316. *
  317. * @param string $url The url or path you want to request.
  318. * @param mixed $data The request data you want to send.
  319. * @param array $options Additional options for the request.
  320. * @return \Cake\Http\Client\Response
  321. */
  322. public function options(string $url, $data = [], array $options = []): Response
  323. {
  324. $options = $this->_mergeOptions($options);
  325. $url = $this->buildUrl($url, [], $options);
  326. return $this->_doRequest(Request::METHOD_OPTIONS, $url, $data, $options);
  327. }
  328. /**
  329. * Do a TRACE request.
  330. *
  331. * @param string $url The url or path you want to request.
  332. * @param mixed $data The request data you want to send.
  333. * @param array $options Additional options for the request.
  334. * @return \Cake\Http\Client\Response
  335. */
  336. public function trace(string $url, $data = [], array $options = []): Response
  337. {
  338. $options = $this->_mergeOptions($options);
  339. $url = $this->buildUrl($url, [], $options);
  340. return $this->_doRequest(Request::METHOD_TRACE, $url, $data, $options);
  341. }
  342. /**
  343. * Do a DELETE request.
  344. *
  345. * @param string $url The url or path you want to request.
  346. * @param mixed $data The request data you want to send.
  347. * @param array $options Additional options for the request.
  348. * @return \Cake\Http\Client\Response
  349. */
  350. public function delete(string $url, $data = [], array $options = []): Response
  351. {
  352. $options = $this->_mergeOptions($options);
  353. $url = $this->buildUrl($url, [], $options);
  354. return $this->_doRequest(Request::METHOD_DELETE, $url, $data, $options);
  355. }
  356. /**
  357. * Do a HEAD request.
  358. *
  359. * @param string $url The url or path you want to request.
  360. * @param array $data The query string data you want to send.
  361. * @param array $options Additional options for the request.
  362. * @return \Cake\Http\Client\Response
  363. */
  364. public function head(string $url, array $data = [], array $options = []): Response
  365. {
  366. $options = $this->_mergeOptions($options);
  367. $url = $this->buildUrl($url, $data, $options);
  368. return $this->_doRequest(Request::METHOD_HEAD, $url, '', $options);
  369. }
  370. /**
  371. * Helper method for doing non-GET requests.
  372. *
  373. * @param string $method HTTP method.
  374. * @param string $url URL to request.
  375. * @param mixed $data The request body.
  376. * @param array $options The options to use. Contains auth, proxy, etc.
  377. * @return \Cake\Http\Client\Response
  378. */
  379. protected function _doRequest(string $method, string $url, $data, $options): Response
  380. {
  381. $request = $this->_createRequest(
  382. $method,
  383. $url,
  384. $data,
  385. $options
  386. );
  387. return $this->send($request, $options);
  388. }
  389. /**
  390. * Does a recursive merge of the parameter with the scope config.
  391. *
  392. * @param array $options Options to merge.
  393. * @return array Options merged with set config.
  394. */
  395. protected function _mergeOptions(array $options): array
  396. {
  397. return Hash::merge($this->_config, $options);
  398. }
  399. /**
  400. * Sends a PSR-7 request and returns a PSR-7 response.
  401. *
  402. * @param \Psr\Http\Message\RequestInterface $request Request instance.
  403. * @return \Psr\Http\Message\ResponseInterface Response instance.
  404. * @throws \Psr\Http\Client\ClientExceptionInterface If an error happens while processing the request.
  405. */
  406. public function sendRequest(RequestInterface $request): ResponseInterface
  407. {
  408. return $this->send($request, $this->_config);
  409. }
  410. /**
  411. * Send a request.
  412. *
  413. * Used internally by other methods, but can also be used to send
  414. * handcrafted Request objects.
  415. *
  416. * @param \Psr\Http\Message\RequestInterface $request The request to send.
  417. * @param array $options Additional options to use.
  418. * @return \Cake\Http\Client\Response
  419. */
  420. public function send(RequestInterface $request, array $options = []): Response
  421. {
  422. $redirects = 0;
  423. if (isset($options['redirect'])) {
  424. $redirects = (int)$options['redirect'];
  425. unset($options['redirect']);
  426. }
  427. do {
  428. $response = $this->_sendRequest($request, $options);
  429. $handleRedirect = $response->isRedirect() && $redirects-- > 0;
  430. if ($handleRedirect) {
  431. $url = $request->getUri();
  432. $location = $response->getHeaderLine('Location');
  433. $locationUrl = $this->buildUrl($location, [], [
  434. 'host' => $url->getHost(),
  435. 'port' => $url->getPort(),
  436. 'scheme' => $url->getScheme(),
  437. 'protocolRelative' => true,
  438. ]);
  439. $request = $request->withUri(new Uri($locationUrl));
  440. $request = $this->_cookies->addToRequest($request, []);
  441. }
  442. } while ($handleRedirect);
  443. return $response;
  444. }
  445. /**
  446. * Send a request without redirection.
  447. *
  448. * @param \Psr\Http\Message\RequestInterface $request The request to send.
  449. * @param array $options Additional options to use.
  450. * @return \Cake\Http\Client\Response
  451. */
  452. protected function _sendRequest(RequestInterface $request, array $options): Response
  453. {
  454. $responses = $this->_adapter->send($request, $options);
  455. foreach ($responses as $response) {
  456. $this->_cookies = $this->_cookies->addFromResponse($response, $request);
  457. }
  458. return array_pop($responses);
  459. }
  460. /**
  461. * Generate a URL based on the scoped client options.
  462. *
  463. * @param string $url Either a full URL or just the path.
  464. * @param string|array $query The query data for the URL.
  465. * @param array $options The config options stored with Client::config()
  466. * @return string A complete url with scheme, port, host, and path.
  467. */
  468. public function buildUrl(string $url, $query = [], array $options = []): string
  469. {
  470. if (empty($options) && empty($query)) {
  471. return $url;
  472. }
  473. if ($query) {
  474. $q = strpos($url, '?') === false ? '?' : '&';
  475. $url .= $q;
  476. $url .= is_string($query) ? $query : http_build_query($query);
  477. }
  478. $defaults = [
  479. 'host' => null,
  480. 'port' => null,
  481. 'scheme' => 'http',
  482. 'basePath' => '',
  483. 'protocolRelative' => false,
  484. ];
  485. $options += $defaults;
  486. if ($options['protocolRelative'] && preg_match('#^//#', $url)) {
  487. $url = $options['scheme'] . ':' . $url;
  488. }
  489. if (preg_match('#^https?://#', $url)) {
  490. return $url;
  491. }
  492. $defaultPorts = [
  493. 'http' => 80,
  494. 'https' => 443,
  495. ];
  496. $out = $options['scheme'] . '://' . $options['host'];
  497. if ($options['port'] && (int)$options['port'] !== $defaultPorts[$options['scheme']]) {
  498. $out .= ':' . $options['port'];
  499. }
  500. if (!empty($options['basePath'])) {
  501. $out .= '/' . trim($options['basePath'], '/');
  502. }
  503. $out .= '/' . ltrim($url, '/');
  504. return $out;
  505. }
  506. /**
  507. * Creates a new request object based on the parameters.
  508. *
  509. * @param string $method HTTP method name.
  510. * @param string $url The url including query string.
  511. * @param mixed $data The request body.
  512. * @param array $options The options to use. Contains auth, proxy, etc.
  513. * @return \Cake\Http\Client\Request
  514. */
  515. protected function _createRequest(string $method, string $url, $data, $options): Request
  516. {
  517. $headers = (array)($options['headers'] ?? []);
  518. if (isset($options['type'])) {
  519. $headers = array_merge($headers, $this->_typeHeaders($options['type']));
  520. }
  521. if (is_string($data) && !isset($headers['Content-Type']) && !isset($headers['content-type'])) {
  522. $headers['Content-Type'] = 'application/x-www-form-urlencoded';
  523. }
  524. $request = new Request($url, $method, $headers, $data);
  525. /** @var \Cake\Http\Client\Request $request */
  526. $request = $request->withProtocolVersion($this->getConfig('protocolVersion'));
  527. $cookies = $options['cookies'] ?? [];
  528. /** @var \Cake\Http\Client\Request $request */
  529. $request = $this->_cookies->addToRequest($request, $cookies);
  530. if (isset($options['auth'])) {
  531. $request = $this->_addAuthentication($request, $options);
  532. }
  533. if (isset($options['proxy'])) {
  534. $request = $this->_addProxy($request, $options);
  535. }
  536. return $request;
  537. }
  538. /**
  539. * Returns headers for Accept/Content-Type based on a short type
  540. * or full mime-type.
  541. *
  542. * @param string $type short type alias or full mimetype.
  543. * @return string[] Headers to set on the request.
  544. * @throws \Cake\Core\Exception\CakeException When an unknown type alias is used.
  545. * @psalm-return array{Accept: string, Content-Type: string}
  546. */
  547. protected function _typeHeaders(string $type): array
  548. {
  549. if (strpos($type, '/') !== false) {
  550. return [
  551. 'Accept' => $type,
  552. 'Content-Type' => $type,
  553. ];
  554. }
  555. $typeMap = [
  556. 'json' => 'application/json',
  557. 'xml' => 'application/xml',
  558. ];
  559. if (!isset($typeMap[$type])) {
  560. throw new CakeException("Unknown type alias '$type'.");
  561. }
  562. return [
  563. 'Accept' => $typeMap[$type],
  564. 'Content-Type' => $typeMap[$type],
  565. ];
  566. }
  567. /**
  568. * Add authentication headers to the request.
  569. *
  570. * Uses the authentication type to choose the correct strategy
  571. * and use its methods to add headers.
  572. *
  573. * @param \Cake\Http\Client\Request $request The request to modify.
  574. * @param array $options Array of options containing the 'auth' key.
  575. * @return \Cake\Http\Client\Request The updated request object.
  576. */
  577. protected function _addAuthentication(Request $request, array $options): Request
  578. {
  579. $auth = $options['auth'];
  580. /** @var \Cake\Http\Client\Auth\Basic $adapter */
  581. $adapter = $this->_createAuth($auth, $options);
  582. return $adapter->authentication($request, $options['auth']);
  583. }
  584. /**
  585. * Add proxy authentication headers.
  586. *
  587. * Uses the authentication type to choose the correct strategy
  588. * and use its methods to add headers.
  589. *
  590. * @param \Cake\Http\Client\Request $request The request to modify.
  591. * @param array $options Array of options containing the 'proxy' key.
  592. * @return \Cake\Http\Client\Request The updated request object.
  593. */
  594. protected function _addProxy(Request $request, array $options): Request
  595. {
  596. $auth = $options['proxy'];
  597. /** @var \Cake\Http\Client\Auth\Basic $adapter */
  598. $adapter = $this->_createAuth($auth, $options);
  599. return $adapter->proxyAuthentication($request, $options['proxy']);
  600. }
  601. /**
  602. * Create the authentication strategy.
  603. *
  604. * Use the configuration options to create the correct
  605. * authentication strategy handler.
  606. *
  607. * @param array $auth The authentication options to use.
  608. * @param array $options The overall request options to use.
  609. * @return object Authentication strategy instance.
  610. * @throws \Cake\Core\Exception\CakeException when an invalid strategy is chosen.
  611. */
  612. protected function _createAuth(array $auth, array $options)
  613. {
  614. if (empty($auth['type'])) {
  615. $auth['type'] = 'basic';
  616. }
  617. $name = ucfirst($auth['type']);
  618. $class = App::className($name, 'Http/Client/Auth');
  619. if (!$class) {
  620. throw new CakeException(
  621. sprintf('Invalid authentication type %s', $name)
  622. );
  623. }
  624. return new $class($this, $options);
  625. }
  626. }