TagsContext.php 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678
  1. <?php
  2. /**
  3. * @copyright Copyright (c) 2016, ownCloud, Inc.
  4. *
  5. * @author Christoph Wurst <christoph@winzerhof-wurst.at>
  6. * @author Joas Schilling <coding@schilljs.com>
  7. * @author Lukas Reschke <lukas@statuscode.ch>
  8. * @author Phil Davis <phil.davis@inf.org>
  9. * @author Robin Appelman <robin@icewind.nl>
  10. * @author Roeland Jago Douma <roeland@famdouma.nl>
  11. * @author Sergio Bertolin <sbertolin@solidgear.es>
  12. * @author Vincent Petry <vincent@nextcloud.com>
  13. *
  14. * @license AGPL-3.0
  15. *
  16. * This code is free software: you can redistribute it and/or modify
  17. * it under the terms of the GNU Affero General Public License, version 3,
  18. * as published by the Free Software Foundation.
  19. *
  20. * This program is distributed in the hope that it will be useful,
  21. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  22. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  23. * GNU Affero General Public License for more details.
  24. *
  25. * You should have received a copy of the GNU Affero General Public License, version 3,
  26. * along with this program. If not, see <http://www.gnu.org/licenses/>
  27. *
  28. */
  29. require __DIR__ . '/../../vendor/autoload.php';
  30. use Behat\Gherkin\Node\TableNode;
  31. use GuzzleHttp\Client;
  32. use GuzzleHttp\Message\ResponseInterface;
  33. class TagsContext implements \Behat\Behat\Context\Context {
  34. /** @var string */
  35. private $baseUrl;
  36. /** @var Client */
  37. private $client;
  38. /** @var ResponseInterface */
  39. private $response;
  40. /**
  41. * @param string $baseUrl
  42. */
  43. public function __construct($baseUrl) {
  44. $this->baseUrl = $baseUrl;
  45. // in case of ci deployment we take the server url from the environment
  46. $testServerUrl = getenv('TEST_SERVER_URL');
  47. if ($testServerUrl !== false) {
  48. $this->baseUrl = substr($testServerUrl, 0, -5);
  49. }
  50. }
  51. /** @BeforeScenario */
  52. public function setUpScenario() {
  53. $this->client = new Client();
  54. }
  55. /** @AfterScenario */
  56. public function tearDownScenario() {
  57. $user = 'admin';
  58. $tags = $this->requestTagsForUser($user);
  59. foreach ($tags as $tagId => $tag) {
  60. $this->response = $this->client->delete(
  61. $this->baseUrl . '/remote.php/dav/systemtags/' . $tagId,
  62. [
  63. 'auth' => [
  64. $user,
  65. $this->getPasswordForUser($user),
  66. ],
  67. 'headers' => [
  68. 'Content-Type' => 'application/json',
  69. ],
  70. ]
  71. );
  72. }
  73. try {
  74. $this->client->delete(
  75. $this->baseUrl . '/remote.php/webdav/myFileToTag.txt',
  76. [
  77. 'auth' => [
  78. 'user0',
  79. '123456',
  80. ],
  81. 'headers' => [
  82. 'Content-Type' => 'application/json',
  83. ],
  84. ]
  85. );
  86. } catch (\GuzzleHttp\Exception\ClientException $e) {
  87. }
  88. }
  89. /**
  90. * @param string $userName
  91. * @return string
  92. */
  93. private function getPasswordForUser($userName) {
  94. if ($userName === 'admin') {
  95. return 'admin';
  96. }
  97. return '123456';
  98. }
  99. /**
  100. * @param string $user
  101. * @param string $type
  102. * @param string $name
  103. * @param string $groups
  104. */
  105. private function createTag($user, $type, $name, $groups = null) {
  106. $userVisible = true;
  107. $userAssignable = true;
  108. switch ($type) {
  109. case 'normal':
  110. break;
  111. case 'not user-assignable':
  112. $userAssignable = false;
  113. break;
  114. case 'not user-visible':
  115. $userVisible = false;
  116. break;
  117. default:
  118. throw new \Exception('Unsupported type');
  119. }
  120. $body = [
  121. 'name' => $name,
  122. 'userVisible' => $userVisible,
  123. 'userAssignable' => $userAssignable,
  124. ];
  125. if ($groups !== null) {
  126. $body['groups'] = $groups;
  127. }
  128. try {
  129. $this->response = $this->client->post(
  130. $this->baseUrl . '/remote.php/dav/systemtags/',
  131. [
  132. 'auth' => [
  133. $user,
  134. $this->getPasswordForUser($user),
  135. ],
  136. 'headers' => [
  137. 'Content-Type' => 'application/json',
  138. ],
  139. 'body' => json_encode($body)
  140. ]
  141. );
  142. } catch (\GuzzleHttp\Exception\ClientException $e) {
  143. $this->response = $e->getResponse();
  144. }
  145. }
  146. /**
  147. * @When :user creates a :type tag with name :name
  148. * @param string $user
  149. * @param string $type
  150. * @param string $name
  151. * @throws \Exception
  152. */
  153. public function createsATagWithName($user, $type, $name) {
  154. $this->createTag($user, $type, $name);
  155. }
  156. /**
  157. * @When :user creates a :type tag with name :name and groups :groups
  158. * @param string $user
  159. * @param string $type
  160. * @param string $name
  161. * @param string $groups
  162. * @throws \Exception
  163. */
  164. public function createsATagWithNameAndGroups($user, $type, $name, $groups) {
  165. $this->createTag($user, $type, $name, $groups);
  166. }
  167. /**
  168. * @Then The response should have a status code :statusCode
  169. * @param int $statusCode
  170. * @throws \Exception
  171. */
  172. public function theResponseShouldHaveAStatusCode($statusCode) {
  173. if ((int)$statusCode !== $this->response->getStatusCode()) {
  174. throw new \Exception("Expected $statusCode, got " . $this->response->getStatusCode());
  175. }
  176. }
  177. /**
  178. * Returns all tags for a given user
  179. *
  180. * @param string $user
  181. * @return array
  182. */
  183. private function requestTagsForUser($user, $withGroups = false) {
  184. try {
  185. $body = '<?xml version="1.0"?>
  186. <d:propfind xmlns:d="DAV:" xmlns:oc="http://owncloud.org/ns">
  187. <d:prop>
  188. <oc:id />
  189. <oc:display-name />
  190. <oc:user-visible />
  191. <oc:user-assignable />
  192. <oc:can-assign />
  193. ';
  194. if ($withGroups) {
  195. $body .= '<oc:groups />';
  196. }
  197. $body .= '
  198. </d:prop>
  199. </d:propfind>';
  200. $this->response = $this->client->request(
  201. 'PROPFIND',
  202. $this->baseUrl . '/remote.php/dav/systemtags/',
  203. [
  204. 'body' => $body,
  205. 'auth' => [
  206. $user,
  207. $this->getPasswordForUser($user),
  208. ],
  209. 'headers' => [
  210. 'Content-Type' => 'application/json',
  211. ],
  212. ]
  213. );
  214. } catch (\GuzzleHttp\Exception\ClientException $e) {
  215. $this->response = $e->getResponse();
  216. }
  217. $tags = [];
  218. $service = new Sabre\Xml\Service();
  219. $parsed = $service->parse($this->response->getBody()->getContents());
  220. foreach ($parsed as $entry) {
  221. $singleEntry = $entry['value'][1]['value'][0]['value'];
  222. if (empty($singleEntry[0]['value'])) {
  223. continue;
  224. }
  225. // FIXME: use actual property names instead of guessing index position
  226. $tags[$singleEntry[0]['value']] = [
  227. 'display-name' => $singleEntry[1]['value'],
  228. 'user-visible' => $singleEntry[2]['value'],
  229. 'user-assignable' => $singleEntry[3]['value'],
  230. 'can-assign' => $singleEntry[4]['value'],
  231. ];
  232. if (isset($singleEntry[5])) {
  233. $tags[$singleEntry[0]['value']]['groups'] = $singleEntry[5]['value'];
  234. }
  235. }
  236. return $tags;
  237. }
  238. /**
  239. * @Then The following tags should exist for :user
  240. * @param string $user
  241. * @param TableNode $table
  242. * @throws \Exception
  243. */
  244. public function theFollowingTagsShouldExistFor($user, TableNode $table) {
  245. $tags = $this->requestTagsForUser($user);
  246. if (count($table->getRows()) !== count($tags)) {
  247. throw new \Exception(
  248. sprintf(
  249. "Expected %s tags, got %s.",
  250. count($table->getRows()),
  251. count($tags)
  252. )
  253. );
  254. }
  255. foreach ($table->getRowsHash() as $rowDisplayName => $row) {
  256. foreach ($tags as $key => $tag) {
  257. if (
  258. $tag['display-name'] === $rowDisplayName &&
  259. $tag['user-visible'] === $row[0] &&
  260. $tag['user-assignable'] === $row[1]
  261. ) {
  262. unset($tags[$key]);
  263. }
  264. }
  265. }
  266. if (count($tags) !== 0) {
  267. throw new \Exception('Not expected response');
  268. }
  269. }
  270. /**
  271. * @Then the user :user :can assign The :type tag with name :tagName
  272. */
  273. public function theUserCanAssignTheTag($user, $can, $type, $tagName) {
  274. $foundTag = $this->findTag($type, $tagName, $user);
  275. if ($foundTag === null) {
  276. throw new \Exception('No matching tag found');
  277. }
  278. if ($can === 'can') {
  279. $expected = 'true';
  280. } elseif ($can === 'cannot') {
  281. $expected = 'false';
  282. } else {
  283. throw new \Exception('Invalid condition, must be "can" or "cannot"');
  284. }
  285. if ($foundTag['can-assign'] !== $expected) {
  286. throw new \Exception('Tag cannot be assigned by user');
  287. }
  288. }
  289. /**
  290. * @Then The :type tag with name :tagName has the groups :groups
  291. */
  292. public function theTagHasGroup($type, $tagName, $groups) {
  293. $foundTag = $this->findTag($type, $tagName, 'admin', true);
  294. if ($foundTag === null) {
  295. throw new \Exception('No matching tag found');
  296. }
  297. if ($foundTag['groups'] !== $groups) {
  298. throw new \Exception('Tag has groups "' . $foundTag['group'] . '" instead of the expected "' . $groups . '"');
  299. }
  300. }
  301. /**
  302. * @Then :count tags should exist for :user
  303. * @param int $count
  304. * @param string $user
  305. * @throws \Exception
  306. */
  307. public function tagsShouldExistFor($count, $user) {
  308. if ((int)$count !== count($this->requestTagsForUser($user))) {
  309. throw new \Exception("Expected $count tags, got " . count($this->requestTagsForUser($user)));
  310. }
  311. }
  312. /**
  313. * Find tag by type and name
  314. *
  315. * @param string $type tag type
  316. * @param string $tagName tag name
  317. * @param string $user retrieved from which user
  318. * @param bool $withGroups whether to also query the tag's groups
  319. *
  320. * @return array tag values or null if not found
  321. */
  322. private function findTag($type, $tagName, $user = 'admin', $withGroups = false) {
  323. $tags = $this->requestTagsForUser($user, $withGroups);
  324. $userAssignable = 'true';
  325. $userVisible = 'true';
  326. switch ($type) {
  327. case 'normal':
  328. break;
  329. case 'not user-assignable':
  330. $userAssignable = 'false';
  331. break;
  332. case 'not user-visible':
  333. $userVisible = 'false';
  334. break;
  335. default:
  336. throw new \Exception('Unsupported type');
  337. }
  338. $foundTag = null;
  339. foreach ($tags as $tag) {
  340. if ($tag['display-name'] === $tagName
  341. && $tag['user-visible'] === $userVisible
  342. && $tag['user-assignable'] === $userAssignable) {
  343. $foundTag = $tag;
  344. break;
  345. }
  346. }
  347. return $foundTag;
  348. }
  349. /**
  350. * @param string $name
  351. * @return int
  352. */
  353. private function findTagIdByName($name) {
  354. $tags = $this->requestTagsForUser('admin');
  355. $tagId = 0;
  356. foreach ($tags as $id => $tag) {
  357. if ($tag['display-name'] === $name) {
  358. $tagId = $id;
  359. break;
  360. }
  361. }
  362. return (int)$tagId;
  363. }
  364. /**
  365. * @When :user edits the tag with name :oldNmae and sets its name to :newName
  366. * @param string $user
  367. * @param string $oldName
  368. * @param string $newName
  369. * @throws \Exception
  370. */
  371. public function editsTheTagWithNameAndSetsItsNameTo($user, $oldName, $newName) {
  372. $tagId = $this->findTagIdByName($oldName);
  373. if ($tagId === 0) {
  374. throw new \Exception('Could not find tag to rename');
  375. }
  376. try {
  377. $this->response = $this->client->request(
  378. 'PROPPATCH',
  379. $this->baseUrl . '/remote.php/dav/systemtags/' . $tagId,
  380. [
  381. 'body' => '<?xml version="1.0"?>
  382. <d:propertyupdate xmlns:d="DAV:" xmlns:oc="http://owncloud.org/ns">
  383. <d:set>
  384. <d:prop>
  385. <oc:display-name>' . $newName . '</oc:display-name>
  386. </d:prop>
  387. </d:set>
  388. </d:propertyupdate>',
  389. 'auth' => [
  390. $user,
  391. $this->getPasswordForUser($user),
  392. ],
  393. ]
  394. );
  395. } catch (\GuzzleHttp\Exception\ClientException $e) {
  396. $this->response = $e->getResponse();
  397. }
  398. }
  399. /**
  400. * @When :user edits the tag with name :oldNmae and sets its groups to :groups
  401. * @param string $user
  402. * @param string $oldName
  403. * @param string $groups
  404. * @throws \Exception
  405. */
  406. public function editsTheTagWithNameAndSetsItsGroupsTo($user, $oldName, $groups) {
  407. $tagId = $this->findTagIdByName($oldName);
  408. if ($tagId === 0) {
  409. throw new \Exception('Could not find tag to rename');
  410. }
  411. try {
  412. $this->response = $this->client->request(
  413. 'PROPPATCH',
  414. $this->baseUrl . '/remote.php/dav/systemtags/' . $tagId,
  415. [
  416. 'body' => '<?xml version="1.0"?>
  417. <d:propertyupdate xmlns:d="DAV:" xmlns:oc="http://owncloud.org/ns">
  418. <d:set>
  419. <d:prop>
  420. <oc:groups>' . $groups . '</oc:groups>
  421. </d:prop>
  422. </d:set>
  423. </d:propertyupdate>',
  424. 'auth' => [
  425. $user,
  426. $this->getPasswordForUser($user),
  427. ],
  428. ]
  429. );
  430. } catch (\GuzzleHttp\Exception\ClientException $e) {
  431. $this->response = $e->getResponse();
  432. }
  433. }
  434. /**
  435. * @When :user deletes the tag with name :name
  436. * @param string $user
  437. * @param string $name
  438. */
  439. public function deletesTheTagWithName($user, $name) {
  440. $tagId = $this->findTagIdByName($name);
  441. try {
  442. $this->response = $this->client->delete(
  443. $this->baseUrl . '/remote.php/dav/systemtags/' . $tagId,
  444. [
  445. 'auth' => [
  446. $user,
  447. $this->getPasswordForUser($user),
  448. ],
  449. 'headers' => [
  450. 'Content-Type' => 'application/json',
  451. ],
  452. ]
  453. );
  454. } catch (\GuzzleHttp\Exception\ClientException $e) {
  455. $this->response = $e->getResponse();
  456. }
  457. }
  458. /**
  459. * @param string $path
  460. * @param string $user
  461. * @return int
  462. */
  463. private function getFileIdForPath($path, $user) {
  464. $url = $this->baseUrl . '/remote.php/webdav/' . $path;
  465. $credentials = base64_encode($user . ':' . $this->getPasswordForUser($user));
  466. $context = stream_context_create([
  467. 'http' => [
  468. 'method' => 'PROPFIND',
  469. 'header' => "Authorization: Basic $credentials\r\nContent-Type: application/x-www-form-urlencoded",
  470. 'content' => '<?xml version="1.0"?>
  471. <d:propfind xmlns:d="DAV:" xmlns:oc="http://owncloud.org/ns">
  472. <d:prop>
  473. <oc:fileid />
  474. </d:prop>
  475. </d:propfind>'
  476. ]
  477. ]);
  478. $response = file_get_contents($url, false, $context);
  479. preg_match_all('/\<oc:fileid\>(.*?)\<\/oc:fileid\>/', $response, $matches);
  480. return (int)$matches[1][0];
  481. }
  482. /**
  483. * @When /^"([^"]*)" adds the tag "([^"]*)" to "([^"]*)" (shared|owned) by "([^"]*)"$/
  484. * @param string $taggingUser
  485. * @param string $tagName
  486. * @param string $fileName
  487. * @param string $sharingUser
  488. */
  489. public function addsTheTagToSharedBy($taggingUser, $tagName, $fileName, $sharedOrOwnedBy, $sharingUser) {
  490. $fileId = $this->getFileIdForPath($fileName, $sharingUser);
  491. $tagId = $this->findTagIdByName($tagName);
  492. try {
  493. $this->response = $this->client->put(
  494. $this->baseUrl . '/remote.php/dav/systemtags-relations/files/' . $fileId . '/' . $tagId,
  495. [
  496. 'auth' => [
  497. $taggingUser,
  498. $this->getPasswordForUser($taggingUser),
  499. ]
  500. ]
  501. );
  502. } catch (\GuzzleHttp\Exception\ClientException $e) {
  503. $this->response = $e->getResponse();
  504. }
  505. }
  506. /**
  507. * @Then /^"([^"]*)" (shared|owned) by "([^"]*)" has the following tags$/
  508. * @param string $fileName
  509. * @param string $sharingUser
  510. * @param TableNode $table
  511. * @throws \Exception
  512. */
  513. public function sharedByHasTheFollowingTags($fileName, $sharedOrOwnedBy, $sharingUser, TableNode $table) {
  514. $loadedExpectedTags = $table->getTable();
  515. $expectedTags = [];
  516. foreach ($loadedExpectedTags as $expected) {
  517. $expectedTags[] = $expected[0];
  518. }
  519. // Get the real tags
  520. $response = $this->client->request(
  521. 'PROPFIND',
  522. $this->baseUrl . '/remote.php/dav/systemtags-relations/files/' . $this->getFileIdForPath($fileName, $sharingUser),
  523. [
  524. 'auth' => [
  525. $sharingUser,
  526. $this->getPasswordForUser($sharingUser),
  527. ],
  528. 'body' => '<?xml version="1.0"?>
  529. <d:propfind xmlns:d="DAV:" xmlns:oc="http://owncloud.org/ns">
  530. <d:prop>
  531. <oc:id />
  532. <oc:display-name />
  533. <oc:user-visible />
  534. <oc:user-assignable />
  535. </d:prop>
  536. </d:propfind>',
  537. ]
  538. )->getBody()->getContents();
  539. preg_match_all('/\<oc:display-name\>(.*?)\<\/oc:display-name\>/', $response, $realTags);
  540. foreach ($expectedTags as $key => $row) {
  541. foreach ($realTags as $tag) {
  542. if ($tag[0] === $row) {
  543. unset($expectedTags[$key]);
  544. }
  545. }
  546. }
  547. if (count($expectedTags) !== 0) {
  548. throw new \Exception('Not all tags found.');
  549. }
  550. }
  551. /**
  552. * @Then :fileName shared by :sharingUser has the following tags for :user
  553. * @param string $fileName
  554. * @param string $sharingUser
  555. * @param string $user
  556. * @param TableNode $table
  557. * @throws \Exception
  558. */
  559. public function sharedByHasTheFollowingTagsFor($fileName, $sharingUser, $user, TableNode $table) {
  560. $loadedExpectedTags = $table->getTable();
  561. $expectedTags = [];
  562. foreach ($loadedExpectedTags as $expected) {
  563. $expectedTags[] = $expected[0];
  564. }
  565. // Get the real tags
  566. try {
  567. $this->response = $this->client->request(
  568. 'PROPFIND',
  569. $this->baseUrl . '/remote.php/dav/systemtags-relations/files/' . $this->getFileIdForPath($fileName, $sharingUser),
  570. [
  571. 'auth' => [
  572. $user,
  573. $this->getPasswordForUser($user),
  574. ],
  575. 'body' => '<?xml version="1.0"?>
  576. <d:propfind xmlns:d="DAV:" xmlns:oc="http://owncloud.org/ns">
  577. <d:prop>
  578. <oc:id />
  579. <oc:display-name />
  580. <oc:user-visible />
  581. <oc:user-assignable />
  582. </d:prop>
  583. </d:propfind>',
  584. ]
  585. );
  586. } catch (\GuzzleHttp\Exception\ClientException $e) {
  587. $this->response = $e->getResponse();
  588. }
  589. preg_match_all('/\<oc:display-name\>(.*?)\<\/oc:display-name\>/', $this->response->getBody()->getContents(), $realTags);
  590. $realTags = array_filter($realTags);
  591. $expectedTags = array_filter($expectedTags);
  592. foreach ($expectedTags as $key => $row) {
  593. foreach ($realTags as $tag) {
  594. foreach ($tag as $index => $foo) {
  595. if ($tag[$index] === $row) {
  596. unset($expectedTags[$key]);
  597. }
  598. }
  599. }
  600. }
  601. if (count($expectedTags) !== 0) {
  602. throw new \Exception('Not all tags found.');
  603. }
  604. }
  605. /**
  606. * @When :user removes the tag :tagName from :fileName shared by :shareUser
  607. * @param string $user
  608. * @param string $tagName
  609. * @param string $fileName
  610. * @param string $shareUser
  611. */
  612. public function removesTheTagFromSharedBy($user, $tagName, $fileName, $shareUser) {
  613. $tagId = $this->findTagIdByName($tagName);
  614. $fileId = $this->getFileIdForPath($fileName, $shareUser);
  615. try {
  616. $this->response = $this->client->delete(
  617. $this->baseUrl . '/remote.php/dav/systemtags-relations/files/' . $fileId . '/' . $tagId,
  618. [
  619. 'auth' => [
  620. $user,
  621. $this->getPasswordForUser($user),
  622. ],
  623. ]
  624. );
  625. } catch (\GuzzleHttp\Exception\ClientException $e) {
  626. $this->response = $e->getResponse();
  627. }
  628. }
  629. }