PageRenderTime 52ms CodeModel.GetById 20ms RepoModel.GetById 0ms app.codeStats 0ms

/src/Backend/Modules/Groups/Actions/Edit.php

http://github.com/forkcms/forkcms
PHP | 631 lines | 389 code | 100 blank | 142 comment | 53 complexity | fb4d33de93c96fb010509c09b46c31ca MD5 | raw file
Possible License(s): MPL-2.0-no-copyleft-exception, MIT, AGPL-3.0, LGPL-2.1, BSD-3-Clause
  1. <?php
  2. namespace Backend\Modules\Groups\Actions;
  3. use Backend\Core\Engine\Authentication as BackendAuthentication;
  4. use Backend\Core\Engine\Base\ActionEdit as BackendBaseActionEdit;
  5. use Backend\Core\Engine\DataGridArray as BackendDataGridArray;
  6. use Backend\Core\Engine\DataGridDatabase as BackendDataGridDatabase;
  7. use Backend\Core\Engine\Form as BackendForm;
  8. use Backend\Core\Language\Language as BL;
  9. use Backend\Core\Engine\Model as BackendModel;
  10. use Backend\Form\Type\DeleteType;
  11. use Backend\Modules\Groups\Engine\Model as BackendGroupsModel;
  12. use Backend\Modules\Users\Engine\Model as BackendUsersModel;
  13. use Symfony\Component\Finder\Finder;
  14. /**
  15. * This is the edit-action, it will display a form to edit a group
  16. */
  17. class Edit extends BackendBaseActionEdit
  18. {
  19. /**
  20. * The action groups
  21. *
  22. * @var array
  23. */
  24. private $actionGroups = [];
  25. /**
  26. * The actions
  27. *
  28. * @var array
  29. */
  30. private $actions = [];
  31. /**
  32. * The dashboard sequence
  33. *
  34. * @var array
  35. */
  36. private $hiddenOnDashboard = [];
  37. /**
  38. * The users datagrid
  39. *
  40. * @var BackendDataGridDatabase
  41. */
  42. private $dataGridUsers;
  43. /**
  44. * The modules
  45. *
  46. * @var array
  47. */
  48. private $modules;
  49. /**
  50. * The widgets
  51. *
  52. * @var array
  53. */
  54. private $widgets;
  55. /**
  56. * The widget instances
  57. *
  58. * @var array
  59. */
  60. private $widgetInstances;
  61. private function bundleActions(): void
  62. {
  63. foreach ($this->modules as $module) {
  64. // loop through actions and add all classnames
  65. foreach ($this->actions[$module['value']] as $key => $action) {
  66. // ajax action?
  67. if (class_exists('Backend\\Modules\\' . $module['value'] . '\\Ajax\\' . $action['value'])) {
  68. // create reflection class
  69. $reflection = new \ReflectionClass('Backend\\Modules\\' . $module['value'] . '\\Ajax\\' . $action['value']);
  70. } else {
  71. // no ajax action? create reflection class
  72. $reflection = new \ReflectionClass('Backend\\Modules\\' . $module['value'] . '\\Actions\\' . $action['value']);
  73. }
  74. // get the tag offset
  75. $offset = mb_strpos($reflection->getDocComment(), ACTION_GROUP_TAG) + mb_strlen(ACTION_GROUP_TAG);
  76. // no tag present? move on!
  77. if (!($offset - mb_strlen(ACTION_GROUP_TAG))) {
  78. continue;
  79. }
  80. // get the group info
  81. $groupInfo = trim(mb_substr($reflection->getDocComment(), $offset, (mb_strpos($reflection->getDocComment(), '*', $offset) - $offset)));
  82. // get name and description
  83. $bits = explode("\t", $groupInfo);
  84. // delete empty values
  85. foreach ($bits as $i => $bit) {
  86. if (empty($bit)) {
  87. unset($bits[$i]);
  88. }
  89. }
  90. // add group to actions
  91. $this->actions[$module['value']][$key]['group'] = $bits[0];
  92. // add group to array
  93. $this->actionGroups[$bits[0]] = end($bits);
  94. }
  95. }
  96. }
  97. public function execute(): void
  98. {
  99. parent::execute();
  100. $this->getData();
  101. $this->loadDataGrids();
  102. $this->loadForm();
  103. $this->validateForm();
  104. $this->loadDeleteForm();
  105. $this->parse();
  106. $this->display();
  107. }
  108. private function getActions(): void
  109. {
  110. $this->actions = [];
  111. $filter = ['Authentication', 'Error', 'Core'];
  112. $modules = [];
  113. $finder = new Finder();
  114. $finder->name('*.php')
  115. ->in(BACKEND_MODULES_PATH . '/*/Actions')
  116. ->in(BACKEND_MODULES_PATH . '/*/Ajax');
  117. foreach ($finder->files() as $file) {
  118. /** @var $file \SplFileInfo */
  119. $module = $file->getPathInfo()->getPathInfo()->getBasename();
  120. // skip some modules
  121. if (in_array($module, $filter)) {
  122. continue;
  123. }
  124. if (BackendAuthentication::isAllowedModule($module)) {
  125. $actionName = $file->getBasename('.php');
  126. $isAjax = $file->getPathInfo()->getBasename() == 'Ajax';
  127. $modules[] = $module;
  128. // ajax-files should be required
  129. if ($isAjax) {
  130. $class = 'Backend\\Modules\\' . $module . '\\Ajax\\' . $actionName;
  131. } else {
  132. $class = 'Backend\\Modules\\' . $module . '\\Actions\\' . $actionName;
  133. }
  134. $reflection = new \ReflectionClass($class);
  135. $phpDoc = trim($reflection->getDocComment());
  136. if ($phpDoc != '') {
  137. $offset = mb_strpos($reflection->getDocComment(), '*', 7);
  138. $description = mb_substr($reflection->getDocComment(), 0, $offset);
  139. $description = str_replace('*', '', $description);
  140. $description = trim(str_replace('/', '', $description));
  141. } else {
  142. $description = '';
  143. }
  144. $this->actions[$module][] = [
  145. 'label' => \SpoonFilter::toCamelCase($actionName),
  146. 'value' => $actionName,
  147. 'description' => $description,
  148. ];
  149. }
  150. }
  151. $modules = array_unique($modules);
  152. foreach ($modules as $module) {
  153. $this->modules[] = [
  154. 'label' => \SpoonFilter::toCamelCase($module),
  155. 'value' => $module,
  156. ];
  157. usort($this->actions[$module], function ($a, $b) {
  158. return strcmp($a["label"], $b["label"]);
  159. });
  160. }
  161. }
  162. private function getData(): void
  163. {
  164. $this->id = $this->getRequest()->query->getInt('id');
  165. // get dashboard sequence
  166. $this->hiddenOnDashboard = BackendGroupsModel::getSetting($this->id, 'hidden_on_dashboard');
  167. // get the record
  168. $this->record = BackendGroupsModel::get($this->id);
  169. // no item found, throw an exceptions, because somebody is fucking with our URL
  170. if (empty($this->record)) {
  171. $this->redirect(BackendModel::createUrlForAction('Index') . '&error=non-existing');
  172. }
  173. $this->getWidgets();
  174. $this->getActions();
  175. $this->bundleActions();
  176. }
  177. private function getWidgets(): void
  178. {
  179. $this->widgets = [];
  180. $this->widgetInstances = [];
  181. $finder = new Finder();
  182. $finder->name('*.php')
  183. ->in(BACKEND_MODULES_PATH . '/*/Widgets');
  184. foreach ($finder->files() as $file) {
  185. $module = $file->getPathInfo()->getPathInfo()->getBasename();
  186. if (BackendAuthentication::isAllowedModule($module)) {
  187. $widgetName = $file->getBasename('.php');
  188. $class = 'Backend\\Modules\\' . $module . '\\Widgets\\' . $widgetName;
  189. if (class_exists($class)) {
  190. // add to array
  191. $this->widgetInstances[] = [
  192. 'module' => $module,
  193. 'widget' => $widgetName,
  194. 'className' => $class,
  195. ];
  196. // create reflection class
  197. $reflection = new \ReflectionClass($class);
  198. $phpDoc = trim($reflection->getDocComment());
  199. if ($phpDoc != '') {
  200. $offset = mb_strpos($reflection->getDocComment(), '*', 7);
  201. $description = mb_substr($reflection->getDocComment(), 0, $offset);
  202. $description = str_replace('*', '', $description);
  203. $description = trim(str_replace('/', '', $description));
  204. } else {
  205. $description = '';
  206. }
  207. // check if model file exists
  208. $pathName = $file->getPathInfo()->getPathInfo()->getRealPath();
  209. if (is_file($pathName . '/engine/model.php')) {
  210. // require model
  211. require_once $pathName . '/engine/model.php';
  212. }
  213. // add to array
  214. $this->widgets[] = [
  215. 'checkbox_name' => \SpoonFilter::toCamelCase($module) . \SpoonFilter::toCamelCase($widgetName),
  216. 'module_name' => $module,
  217. 'label' => \SpoonFilter::toCamelCase($widgetName),
  218. 'value' => $widgetName,
  219. 'description' => $description,
  220. ];
  221. }
  222. }
  223. }
  224. }
  225. private function loadDataGrids(): void
  226. {
  227. $this->dataGridUsers = new BackendDataGridDatabase(BackendGroupsModel::QUERY_ACTIVE_USERS, [$this->id, false]);
  228. // check if this action is allowed
  229. if (BackendAuthentication::isAllowedAction('Edit', 'Users')) {
  230. // add columns
  231. $this->dataGridUsers->addColumn('nickname', \SpoonFilter::ucfirst(BL::lbl('Nickname')), null, BackendModel::createUrlForAction('Edit', 'Users') . '&amp;id=[id]');
  232. $this->dataGridUsers->addColumn('surname', \SpoonFilter::ucfirst(BL::lbl('Surname')), null, BackendModel::createUrlForAction('Edit', 'Users') . '&amp;id=[id]');
  233. $this->dataGridUsers->addColumn('name', \SpoonFilter::ucfirst(BL::lbl('Name')), null, BackendModel::createUrlForAction('Edit', 'Users') . '&amp;id=[id]');
  234. // add column URL
  235. $this->dataGridUsers->setColumnURL('email', BackendModel::createUrlForAction('Edit', 'Users') . '&amp;id=[id]');
  236. // set columns sequence
  237. $this->dataGridUsers->setColumnsSequence('nickname', 'surname', 'name', 'email');
  238. // show users's name, surname and nickname
  239. $this->dataGridUsers->setColumnFunction([new BackendUsersModel(), 'getSetting'], ['[id]', 'surname'], 'surname', false);
  240. $this->dataGridUsers->setColumnFunction([new BackendUsersModel(), 'getSetting'], ['[id]', 'name'], 'name', false);
  241. $this->dataGridUsers->setColumnFunction([new BackendUsersModel(), 'getSetting'], ['[id]', 'nickname'], 'nickname', false);
  242. }
  243. }
  244. private function loadForm(): void
  245. {
  246. $this->form = new BackendForm('edit');
  247. // get selected permissions
  248. $actionPermissions = BackendGroupsModel::getActionPermissions($this->id);
  249. $selectedWidgets = [];
  250. $widgetBoxes = [];
  251. $permissionBoxes = [];
  252. $actionBoxes = [];
  253. // loop through modules
  254. foreach ($this->modules as $key => $module) {
  255. // widgets available?
  256. if (isset($this->widgets)) {
  257. // loop through widgets
  258. foreach ($this->widgets as $j => $widget) {
  259. if ($widget['checkbox_name'] != $module['value'] . $widget['value']) {
  260. continue;
  261. }
  262. if (!isset($this->hiddenOnDashboard[$module['value']]) ||
  263. !in_array($widget['value'], $this->hiddenOnDashboard[$module['value']])) {
  264. $selectedWidgets[$j] = $widget['checkbox_name'];
  265. }
  266. // add widget checkboxes
  267. $widgetBoxes[$j]['check'] = '<span>' . $this->form->addCheckbox('widgets_' . $widget['checkbox_name'], isset($selectedWidgets[$j]) ? $selectedWidgets[$j] : null)->parse() . '</span>';
  268. $widgetBoxes[$j]['module'] = \SpoonFilter::ucfirst(BL::lbl($widget['module_name']));
  269. $widgetBoxes[$j]['widget'] = '<label for="widgets' . \SpoonFilter::toCamelCase($widget['checkbox_name']) . '">' . $widget['label'] . '</label>';
  270. $widgetBoxes[$j]['description'] = $widget['description'];
  271. }
  272. }
  273. $selectedActions = [];
  274. // loop through action permissions
  275. foreach ($actionPermissions as $permission) {
  276. // add to selected actions
  277. if ($permission['module'] == $module['value']) {
  278. $selectedActions[] = $permission['action'];
  279. }
  280. }
  281. // add module labels
  282. $permissionBoxes[$key]['label'] = $module['label'];
  283. // init var
  284. $addedBundles = [];
  285. // loop through actions
  286. foreach ($this->actions[$module['value']] as $i => $action) {
  287. // action is bundled?
  288. if (array_key_exists('group', $action)) {
  289. // bundle not yet in array?
  290. if (!in_array($action['group'], $addedBundles)) {
  291. // assign bundled action boxes
  292. $actionBoxes[$key]['actions'][$i]['check'] = $this->form->addCheckbox('actions_' . $module['label'] . '_' . 'Group_' . \SpoonFilter::ucfirst($action['group']), in_array($action['value'], $selectedActions))->parse();
  293. $actionBoxes[$key]['actions'][$i]['action'] = \SpoonFilter::ucfirst($action['group']);
  294. $actionBoxes[$key]['actions'][$i]['description'] = $this->actionGroups[$action['group']];
  295. // add the group to the added bundles
  296. $addedBundles[] = $action['group'];
  297. }
  298. } else {
  299. // assign action boxes
  300. $actionBoxes[$key]['actions'][$i]['check'] = $this->form->addCheckbox('actions_' . $module['label'] . '_' . $action['label'], in_array($action['value'], $selectedActions))->parse();
  301. $actionBoxes[$key]['actions'][$i]['action'] = '<label for="actions' . \SpoonFilter::toCamelCase($module['label'] . '_' . $action['label']) . '">' . $action['label'] . '</label>';
  302. $actionBoxes[$key]['actions'][$i]['description'] = $action['description'];
  303. }
  304. }
  305. // widgetboxes available?
  306. if (isset($widgetBoxes)) {
  307. // create datagrid
  308. $widgetGrid = new BackendDataGridArray($widgetBoxes);
  309. $widgetGrid->setHeaderLabels(['check' => '<span class="checkboxHolder"><input id="toggleChecksWidgets" type="checkbox" name="toggleChecks" value="toggleChecks" /></span>']);
  310. // get content
  311. $widgets = $widgetGrid->getContent();
  312. }
  313. // create datagrid
  314. $actionGrid = new BackendDataGridArray($actionBoxes[$key]['actions']);
  315. $actionGrid->setHeaderLabels(['check' => '']);
  316. // disable paging
  317. $actionGrid->setPaging(false);
  318. // get content of datagrids
  319. $permissionBoxes[$key]['actions']['dataGrid'] = $actionGrid->getContent();
  320. $permissionBoxes[$key]['chk'] = $this->form->addCheckbox(
  321. $module['label'],
  322. false,
  323. 'inputCheckbox checkBeforeUnload jsSelectAll'
  324. )->parse();
  325. $permissionBoxes[$key]['id'] = \SpoonFilter::toCamelCase($module['label']);
  326. }
  327. // create elements
  328. $this->form->addText('name', $this->record['name']);
  329. $this->form->addDropdown('manage_users', ['Deny', 'Allow']);
  330. $this->form->addDropdown('manage_groups', ['Deny', 'Allow']);
  331. $this->template->assign('permissions', $permissionBoxes);
  332. $this->template->assign('widgets', $widgets ?? false);
  333. }
  334. protected function parse(): void
  335. {
  336. parent::parse();
  337. $this->template->assign('dataGridUsers', ($this->dataGridUsers->getNumResults() != 0) ? $this->dataGridUsers->getContent() : false);
  338. $this->template->assign('item', $this->record);
  339. $this->template->assign('groupName', $this->record['name']);
  340. // only allow deletion of empty groups
  341. $this->template->assign('allowGroupsDelete', $this->dataGridUsers->getNumResults() == 0);
  342. $this->header->appendDetailToBreadcrumbs($this->record['name']);
  343. }
  344. /**
  345. * Update the permissions
  346. *
  347. * @param \SpoonFormElement[] $actionPermissions The action permissions.
  348. * @param array $bundledActionPermissions The bundled action permissions.
  349. */
  350. private function updatePermissions(array $actionPermissions, array $bundledActionPermissions): void
  351. {
  352. $modulesDenied = [];
  353. $modulesGranted = [];
  354. $actionsDenied = [];
  355. $actionsGranted = [];
  356. $checkedModules = [];
  357. $uncheckedModules = [];
  358. // loop through action permissions
  359. foreach ($actionPermissions as $permission) {
  360. // get bits
  361. $bits = explode('_', $permission->getName());
  362. // convert camelcasing to underscore notation
  363. $module = $bits[1];
  364. $action = $bits[2];
  365. // permission checked?
  366. if ($permission->getChecked()) {
  367. // add to granted
  368. $actionsGranted[] = ['group_id' => $this->id, 'module' => $module, 'action' => $action, 'level' => ACTION_RIGHTS_LEVEL];
  369. // if not yet present, add to checked modules
  370. if (!in_array($module, $checkedModules)) {
  371. $checkedModules[] = $module;
  372. }
  373. } else {
  374. // add to denied
  375. $actionsDenied[] = ['group_id' => $this->id, 'module' => $module, 'action' => $action, 'level' => ACTION_RIGHTS_LEVEL];
  376. // if not yet present add to unchecked modules
  377. if (!in_array($module, $uncheckedModules)) {
  378. $uncheckedModules[] = $module;
  379. }
  380. }
  381. }
  382. // loop through bundled action permissions
  383. foreach ($bundledActionPermissions as $permission) {
  384. // get bits
  385. $bits = explode('_', $permission->getName());
  386. // convert camelcasing to underscore notation
  387. $module = $bits[1];
  388. $group = $bits[3];
  389. // loop through actions
  390. foreach ($this->actions[$module] as $moduleAction) {
  391. // permission checked?
  392. if ($permission->getChecked()) {
  393. // add to granted if in the right group
  394. if (in_array($group, $moduleAction)) {
  395. $actionsGranted[] = ['group_id' => $this->id, 'module' => $module, 'action' => $moduleAction['value'], 'level' => ACTION_RIGHTS_LEVEL];
  396. }
  397. // if not yet present, add to checked modules
  398. if (!in_array($module, $checkedModules)) {
  399. $checkedModules[] = $module;
  400. }
  401. } else {
  402. // add to denied
  403. if (in_array($group, $moduleAction)) {
  404. $actionsDenied[] = ['group_id' => $this->id, 'module' => $module, 'action' => $moduleAction['value'], 'level' => ACTION_RIGHTS_LEVEL];
  405. }
  406. // if not yet present add to unchecked modules
  407. if (!in_array($module, $uncheckedModules)) {
  408. $uncheckedModules[] = $module;
  409. }
  410. }
  411. }
  412. }
  413. // loop through granted modules and add to array
  414. foreach ($checkedModules as $module) {
  415. $modulesGranted[] = ['group_id' => $this->id, 'module' => $module];
  416. }
  417. // loop through denied modules and add to array
  418. foreach (array_diff($uncheckedModules, $checkedModules) as $module) {
  419. $modulesDenied[] = ['group_id' => $this->id, 'module' => $module];
  420. }
  421. // add granted permissions
  422. BackendGroupsModel::addModulePermissions($modulesGranted);
  423. BackendGroupsModel::addActionPermissions($actionsGranted);
  424. // delete denied permissions
  425. BackendGroupsModel::deleteModulePermissions($modulesDenied);
  426. BackendGroupsModel::deleteActionPermissions($actionsDenied);
  427. }
  428. /**
  429. * @param \SpoonFormElement[] $widgetPresets The widgets presets.
  430. *
  431. * @return array
  432. */
  433. private function updateWidgets(array $widgetPresets): array
  434. {
  435. // empty dashboard sequence
  436. $this->hiddenOnDashboard = [];
  437. // loop through all widgets
  438. foreach ($this->widgetInstances as $widget) {
  439. if (!BackendModel::isModuleInstalled($widget['module'])) {
  440. continue;
  441. }
  442. foreach ($widgetPresets as $preset) {
  443. if ($preset->getAttribute('id') !== 'widgets' . $widget['module'] . $widget['widget']) {
  444. continue;
  445. }
  446. if (!$preset->getChecked()) {
  447. if (!isset($this->hiddenOnDashboard[$widget['module']])) {
  448. $this->hiddenOnDashboard[$widget['module']] = [];
  449. }
  450. $this->hiddenOnDashboard[$widget['module']][] = $widget['widget'];
  451. }
  452. }
  453. }
  454. // build group
  455. $userGroup = [];
  456. $userGroup['name'] = $this->form->getField('name')->getValue();
  457. $userGroup['id'] = $this->id;
  458. // build setting
  459. $setting = [];
  460. $setting['group_id'] = $this->id;
  461. $setting['name'] = 'hidden_on_dashboard';
  462. $setting['value'] = serialize($this->hiddenOnDashboard);
  463. // update group
  464. BackendGroupsModel::update($userGroup, $setting);
  465. return $userGroup;
  466. }
  467. private function validateForm(): void
  468. {
  469. if ($this->form->isSubmitted()) {
  470. $bundledActionPermissions = [];
  471. // cleanup the submitted fields, ignore fields that were added by hackers
  472. $this->form->cleanupFields();
  473. // get fields
  474. $nameField = $this->form->getField('name');
  475. $actionPermissions = [];
  476. // loop through modules
  477. foreach ($this->modules as $module) {
  478. // loop through actions
  479. foreach ($this->actions[$module['value']] as $action) {
  480. // collect permissions if not bundled
  481. if (!array_key_exists('group', $action)) {
  482. $actionPermissions[] = $this->form->getField('actions_' . $module['label'] . '_' . $action['label']);
  483. }
  484. }
  485. // loop through bundled actions
  486. foreach ($this->actionGroups as $key => $group) {
  487. // loop through all fields
  488. foreach ($this->form->getFields() as $field) {
  489. // field exists?
  490. if ($field->getName() == 'actions_' . $module['label'] . '_' . 'Group_' . \SpoonFilter::ucfirst($key)) {
  491. // add to bundled actions
  492. $bundledActionPermissions[] = $this->form->getField('actions_' . $module['label'] . '_' . 'Group_' . \SpoonFilter::ucfirst($key));
  493. }
  494. }
  495. }
  496. }
  497. // loop through widgets and collect presets
  498. $widgetPresets = [];
  499. foreach ($this->widgets as $widget) {
  500. $widgetPresets[] = $this->form->getField('widgets_' . $widget['checkbox_name']);
  501. }
  502. // validate fields
  503. $nameField->isFilled(BL::err('NameIsRequired'));
  504. // new name given?
  505. if ($nameField->getValue() !== $this->record['name']) {
  506. // group already exists?
  507. if (BackendGroupsModel::alreadyExists($nameField->getValue())) {
  508. $nameField->setError(BL::err('GroupAlreadyExists'));
  509. }
  510. }
  511. // no errors?
  512. if ($this->form->isCorrect()) {
  513. // update widgets
  514. $group = $this->updateWidgets($widgetPresets);
  515. // update permissions
  516. $this->updatePermissions($actionPermissions, $bundledActionPermissions);
  517. // everything is saved, so redirect to the overview
  518. $this->redirect(BackendModel::createUrlForAction('Index') . '&report=edited&var=' . rawurlencode($group['name']) . '&highlight=row-' . $group['id']);
  519. }
  520. }
  521. }
  522. private function loadDeleteForm(): void
  523. {
  524. $deleteForm = $this->createForm(
  525. DeleteType::class,
  526. ['id' => $this->record['id']],
  527. ['module' => $this->getModule()]
  528. );
  529. $this->template->assign('deleteForm', $deleteForm->createView());
  530. }
  531. }